Merge remote-tracking branch 'origin/master' into paperclip/pr-5732
This commit is contained in:
commit
60eacbef34
4 changed files with 46 additions and 15 deletions
|
|
@ -5,6 +5,10 @@
|
|||
|
||||
### Fixed
|
||||
|
||||
- **A background child stream no longer bumps its parent session to the top of the sidebar.** When a child (branched/background) stream was active, its activity re-sorted the parent session's sidebar time-bucket, yanking the parent around while you were working elsewhere. The sidebar sort no longer keys the parent's bucket off child-stream activity, so parents stay put. Thanks @rodboev. (#5729, #5699)
|
||||
|
||||
- **The embedded terminal now recovers from a dropped connection instead of silently freezing.** A transport-level failure on the terminal's output stream (session expired, gateway restarted, network drop) fired an EventSource `error` with no handler, so the terminal froze with no feedback. It now shows a "connection lost, reconnecting…" / "disconnected" line, lets the browser auto-reconnect a recoverable connection, and tears down a permanently-closed one so a restart can reconnect — notifying once per outage so a flapping connection can't flood the pane. Thanks @ai-ag2026. (#5786)
|
||||
|
||||
- **The OpenCode Go model picker now lists the current flat-rate catalog.** The static `opencode-go` model snapshot was stale; it's been re-synced from the public opencode.ai/go docs + models endpoint (adds MiniMax M3, Kimi K2.7 Code, GLM-5.2, Qwen 3.7 Max/Plus, and more), keeping preview/free-only Zen models out of the Go picker. Thanks @ai-ag2026. (#5761)
|
||||
|
||||
- **The background-task completion drain thread can no longer spin at 100% CPU when the process registry is (re)initializing.** If the registry didn't yet expose its `completion_queue`, the drain loop's broad `except Exception: continue` swallowed the `AttributeError` and immediately retried with no delay — a hot loop. The loop now reads the queue defensively (`getattr(..., None)`) and backs off on the stop event when it's missing, catches `queue.Empty` separately as the normal idle path, and logs (no longer silently swallows) any unexpected queue error before backing off. No delivery latency is added — real completions are still drained on the 1 Hz blocking `get`. Thanks @ai-ag2026. (#5782, #2476, #4633)
|
||||
|
|
|
|||
|
|
@ -6214,7 +6214,6 @@ function _attachChildSessionsToSidebarRows(collapsedRows, rawSessions, rawRefere
|
|||
const childActivitySec=Number(childActivityRaw);
|
||||
if(Number.isFinite(childActivitySec)&&childActivitySec>Number(parentRow._child_session_latest_at||0)){
|
||||
parentRow._child_session_latest_at=childActivitySec;
|
||||
parentRow._sidebar_activity_at=Math.max(Number(parentRow.last_message_at||parentRow.updated_at||parentRow.created_at||0), childActivitySec);
|
||||
}
|
||||
const childAttention=childRow&&childRow.attention&&typeof childRow.attention==='object'?childRow.attention:null;
|
||||
if(!childAttention||!childAttention.kind||!Number.isFinite(Number(childAttention.count))||Number(childAttention.count)<=0) return;
|
||||
|
|
|
|||
|
|
@ -431,6 +431,34 @@ function _connectTerminalOutput(){
|
|||
try{if(source&&source.readyState!==2)source.close();}catch(_){}
|
||||
TERMINAL_UI.source=null;
|
||||
});
|
||||
// A successful (re)connect clears the notify latch so the NEXT outage notifies
|
||||
// again — "once per outage", not "once per source lifetime". Without this, a
|
||||
// source that errors, auto-reconnects, then drops a second time would stay
|
||||
// silent (guard already true, not CLOSED) — the exact freeze this handler
|
||||
// prevents.
|
||||
source.addEventListener('open',()=>{
|
||||
if(TERMINAL_UI.source!==source)return;
|
||||
source._terminalErrNotified=false;
|
||||
});
|
||||
// Transport-level failures (session expired, gateway killed, network drop)
|
||||
// fire 'error' rather than a terminal_* event; without this the terminal froze
|
||||
// with no feedback and no telemetry. Let the browser auto-reconnect a merely
|
||||
// CONNECTING source (no manual loop/backoff), but surface a permanently CLOSED
|
||||
// one and tear it down so a restart can reconnect. Notify once per outage so a
|
||||
// flapping connection can't flood the pane or telemetry.
|
||||
source.addEventListener('error',()=>{
|
||||
if(TERMINAL_UI.source!==source)return;
|
||||
const closed=source.readyState===EventSource.CLOSED;
|
||||
if(closed||!source._terminalErrNotified){
|
||||
source._terminalErrNotified=true;
|
||||
if(typeof recordClientSSEError==='function')recordClientSSEError('terminal',{ready_state:source?source.readyState:null,reason:'terminal EventSource.onerror'});
|
||||
if(TERMINAL_UI.term)TERMINAL_UI.term.writeln('\r\n[terminal '+(closed?'disconnected':'connection lost, reconnecting…')+']\r\n');
|
||||
}
|
||||
if(closed){
|
||||
try{if(source&&source.readyState!==2)source.close();}catch(_){}
|
||||
TERMINAL_UI.source=null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function _startComposerTerminal(restart=false){
|
||||
|
|
|
|||
|
|
@ -1082,7 +1082,7 @@ console.log(JSON.stringify(rows));
|
|||
assert "_child_sessions" not in rows[0]
|
||||
|
||||
|
||||
def test_nested_fork_bubbles_latest_child_timestamp_for_sorting():
|
||||
def test_nested_fork_tracks_latest_child_without_changing_parent_bucket_timestamp():
|
||||
js = SESSIONS_JS_PATH.read_text(encoding="utf-8")
|
||||
source = f"""
|
||||
const src = {js!r};
|
||||
|
|
@ -1112,15 +1112,15 @@ console.log(JSON.stringify({{row: rows[0], timestampMs: _sessionTimestampMs(rows
|
|||
result = json.loads(_run_node(source))
|
||||
row = result["row"]
|
||||
assert row["session_id"] == "parent"
|
||||
# Keep the parent row's persisted timestamp intact, but use newest attached
|
||||
# child/subagent activity for sidebar sorting/date grouping.
|
||||
# Keep the parent row's persisted timestamp intact while still recording
|
||||
# the newest attached child/subagent metadata.
|
||||
assert row["last_message_at"] == 10
|
||||
assert row["_child_session_latest_at"] == 20
|
||||
assert row["_sidebar_activity_at"] == 20
|
||||
assert result["timestampMs"] == 20_000
|
||||
assert "_sidebar_activity_at" not in row
|
||||
assert result["timestampMs"] == 10_000
|
||||
|
||||
|
||||
def test_hidden_archived_lineage_child_bubbles_running_state_and_activity_to_visible_parent():
|
||||
def test_hidden_archived_lineage_child_bubbles_running_state_and_latest_child_timestamp_without_changing_parent_bucket():
|
||||
js = SESSIONS_JS_PATH.read_text(encoding="utf-8")
|
||||
source = f"""
|
||||
const src = {js!r};
|
||||
|
|
@ -1179,11 +1179,11 @@ console.log(JSON.stringify({{row: rows[0], count: rows.length, timestampMs: _ses
|
|||
assert "_child_sessions" not in row
|
||||
assert row["_child_session_streaming"] is True
|
||||
assert row["_child_session_latest_at"] == 30
|
||||
assert row["_sidebar_activity_at"] == 30
|
||||
assert result["timestampMs"] == 30_000
|
||||
assert "_sidebar_activity_at" not in row
|
||||
assert result["timestampMs"] == 10_000
|
||||
|
||||
|
||||
def test_archived_lineage_child_without_root_id_falls_back_to_parent_for_bubbling():
|
||||
def test_archived_lineage_child_without_root_id_preserves_parent_bucket_timestamp():
|
||||
js = SESSIONS_JS_PATH.read_text(encoding="utf-8")
|
||||
source = f"""
|
||||
const src = {js!r};
|
||||
|
|
@ -1241,8 +1241,8 @@ console.log(JSON.stringify({{row: rows[0], count: rows.length, timestampMs: _ses
|
|||
assert "_child_sessions" not in row
|
||||
assert row["_child_session_streaming"] is True
|
||||
assert row["_child_session_latest_at"] == 30
|
||||
assert row["_sidebar_activity_at"] == 30
|
||||
assert result["timestampMs"] == 30_000
|
||||
assert "_sidebar_activity_at" not in row
|
||||
assert result["timestampMs"] == 10_000
|
||||
|
||||
|
||||
def test_non_archived_reference_only_lineage_row_does_not_bubble_to_visible_parent():
|
||||
|
|
@ -1308,7 +1308,7 @@ console.log(JSON.stringify({{row: rows[0], count: rows.length, timestampMs: _ses
|
|||
assert result["timestampMs"] == 10_000
|
||||
|
||||
|
||||
def test_stale_active_stream_id_on_hidden_archived_child_does_not_bubble_streaming_state():
|
||||
def test_stale_active_stream_id_on_hidden_archived_child_keeps_parent_bucket_and_latest_child_timestamp():
|
||||
js = SESSIONS_JS_PATH.read_text(encoding="utf-8")
|
||||
source = f"""
|
||||
const src = {js!r};
|
||||
|
|
@ -1367,8 +1367,8 @@ console.log(JSON.stringify({{row: rows[0], count: rows.length, timestampMs: _ses
|
|||
assert "_child_sessions" not in row
|
||||
assert "_child_session_streaming" not in row
|
||||
assert row["_child_session_latest_at"] == 30
|
||||
assert row["_sidebar_activity_at"] == 30
|
||||
assert result["timestampMs"] == 30_000
|
||||
assert "_sidebar_activity_at" not in row
|
||||
assert result["timestampMs"] == 10_000
|
||||
|
||||
|
||||
def test_collapsed_lineage_segments_do_not_render_as_child_sessions():
|
||||
|
|
|
|||
Loading…
Reference in a new issue