fix(coding-agent): reject reconnect-loop requests on socket close instead of parking them - #1909
Merged
Merged
Conversation
snimu
force-pushed
the
feat/agent-roster-push
branch
from
August 31, 2026 15:37
a1870b6 to
d7e0973
Compare
snimu
force-pushed
the
fix/daemon-client-reconnect-recoverable
branch
from
August 31, 2026 15:39
8d17672 to
fdb0bbc
Compare
snimu
force-pushed
the
feat/agent-roster-push
branch
from
August 31, 2026 16:04
d7e0973 to
db18389
Compare
snimu
force-pushed
the
fix/daemon-client-reconnect-recoverable
branch
from
August 31, 2026 16:04
fdb0bbc to
707c34a
Compare
snimu
force-pushed
the
feat/agent-roster-push
branch
from
August 31, 2026 16:08
db18389 to
32f3abc
Compare
snimu
force-pushed
the
fix/daemon-client-reconnect-recoverable
branch
from
August 31, 2026 16:08
707c34a to
aaabe6c
Compare
snimu
force-pushed
the
feat/agent-roster-push
branch
from
August 31, 2026 16:53
32f3abc to
4a961d6
Compare
snimu
force-pushed
the
fix/daemon-client-reconnect-recoverable
branch
from
August 31, 2026 16:53
aaabe6c to
5382bd5
Compare
snimu
force-pushed
the
feat/agent-roster-push
branch
from
August 31, 2026 17:34
4a961d6 to
a2744ff
Compare
snimu
force-pushed
the
fix/daemon-client-reconnect-recoverable
branch
from
August 31, 2026 17:34
5382bd5 to
e4b0aea
Compare
snimu
added a commit
that referenced
this pull request
Aug 31, 2026
…red fixtures One store-lifecycle test now carries capability-miss, raced-push replay, hello re-keying, attach serialization, and dispose; the wire compat E2E and the chat-bar E2E share one supervisor boot (bystander, coalescing, repaint, and the owned-session snapshot fallback ride the same socket). Cut with accepted residual risk: the mid-flight park test (PR #1909 carries the recoverable-park suite), the watchdog staleness push test (label rendering stays pinned in the rows test), the view-instance anchor/failed-delete pins (one-line UI fixes, self-healing paths), the handshake-exit pin, and the latch test's stale-success ordering (generation guard predates this PR). Comment pass two drops restating docs (amend, hello key, two wire-field docs).
snimu
force-pushed
the
feat/agent-roster-push
branch
from
August 31, 2026 18:22
a2744ff to
7f1eccd
Compare
snimu
force-pushed
the
fix/daemon-client-reconnect-recoverable
branch
from
August 31, 2026 18:22
e4b0aea to
024e834
Compare
snimu
force-pushed
the
feat/agent-roster-push
branch
from
August 31, 2026 18:51
7f1eccd to
d607820
Compare
snimu
added a commit
that referenced
this pull request
Aug 31, 2026
…red fixtures One store-lifecycle test now carries capability-miss, raced-push replay, hello re-keying, attach serialization, and dispose; the wire compat E2E and the chat-bar E2E share one supervisor boot (bystander, coalescing, repaint, and the owned-session snapshot fallback ride the same socket). Cut with accepted residual risk: the mid-flight park test (PR #1909 carries the recoverable-park suite), the watchdog staleness push test (label rendering stays pinned in the rows test), the view-instance anchor/failed-delete pins (one-line UI fixes, self-healing paths), the handshake-exit pin, and the latch test's stale-success ordering (generation guard predates this PR). Comment pass two drops restating docs (amend, hello key, two wire-field docs).
snimu
force-pushed
the
fix/daemon-client-reconnect-recoverable
branch
from
August 31, 2026 18:51
024e834 to
e8528f3
Compare
snimu
added a commit
that referenced
this pull request
Aug 31, 2026
…red fixtures One store-lifecycle test now carries capability-miss, raced-push replay, hello re-keying, attach serialization, and dispose; the wire compat E2E and the chat-bar E2E share one supervisor boot (bystander, coalescing, repaint, and the owned-session snapshot fallback ride the same socket). Cut with accepted residual risk: the mid-flight park test (PR #1909 carries the recoverable-park suite), the watchdog staleness push test (label rendering stays pinned in the rows test), the view-instance anchor/failed-delete pins (one-line UI fixes, self-healing paths), the handshake-exit pin, and the latch test's stale-success ordering (generation guard predates this PR). Comment pass two drops restating docs (amend, hello key, two wire-field docs).
snimu
force-pushed
the
feat/agent-roster-push
branch
from
August 31, 2026 19:15
d607820 to
5457338
Compare
snimu
force-pushed
the
fix/daemon-client-reconnect-recoverable
branch
from
August 31, 2026 19:15
e8528f3 to
15c5075
Compare
xeophon
previously approved these changes
Sep 1, 2026
…from the ledger Workers now push roster deltas to the supervisor on session events (roster_delta/roster_heartbeat worker frames, compute-on-event and send-only-if-changed, plus a 15s unref'd heartbeat tick). The supervisor keeps one roster ledger seeded at startup from the session catalog and the RLM spawn ledger (tombstones excluded), classifies status exactly once at write via classifyAgentStatus, and serves list, selector matching, family catalogs, and peer rosters from it. Deletions this enables: - handleList per-worker fan-out with its 5s timeout and silent stale summaries; list now does zero worker round-trips. - Event-triggered blanket refreshWorkerSummaries (kept only as a per-worker shim for legacy workers that do not advertise the roster capability in their worker_auth response). - mergeSessionLists; 'list all' is served from the already-merged ledger. - streamingMessage off the list wire (recovery/adoption refresh still seeds the stream reconstructor). Visibility and liveness: - Admitted child runs appear as queued roster rows before their session exists and merge into the session row when it binds. - Close, passivation, and eviction flip rows to inactive; rows are removed only for discarded drafts and spawn-ledger delete records. - A dead worker's rows are marked recovering natively on socket close and failed when recovery gives up; one 15s unref'd watchdog stamps lastHeardFromAt on rows of workers silent for more than 45s. busyClientOwnedSessionCount and daemon-launch busy checks are pinned by tests; roster frames live in the worker protocol, not the client schema, so no client protocol change ships in this part. Part 2 of 3 for the event-driven daemon-owned agent roster. ENG-5794
- list keeps its resident-only contract: non-all list emits only sessions with an activeSessionId; queued child runs and passivated rows stay ledger-internal, and list all carries the non-resident rows (owned rows keep their workerState/workerPid). sessionDir on list all now filters rows by sessions dir (including its sibling session-artifacts tree) instead of being ignored. - Offline saved-session renames and deletes, and worker-side saved-session deletes, now write the roster ledger. - Supervisor (re)authentication makes the worker send a replacing roster snapshot; rows absent from the snapshot passivate when a transcript exists and are removed otherwise. Pending state commits only after a frame reaches an authenticated supervisor, and the supervisor registers its frame listener before authenticating so the snapshot cannot race. - Remaining worker.summaries read paths (wake fallback, create reuse and readiness) moved to the ledger; create-forward and rename refreshes are gated to legacy workers, with the returned summary written to the ledger. - The roster wire summary keeps modelFallbackMessage for the active-open path. - Queued-run supersession has one mechanism (session rows overwrite queued rows at flush); the run-lifecycle cleanup in observeRosterChildUpdate is pinned by a bind-then-close test, and the saved-delete test proves the supervisor removes the ledger row end-to-end. - Worker roster reporter state is created lazily so prototype-based fixtures exercising worker_auth cannot crash the flush path. ENG-5794
- Non-all list restores the pre-roster population exactly: worker-owned rows (materialized and passivated) stay listed; sessionless queued-child rows are served by no list form; seeded/offline rows remain all-only. - The reauth snapshot is the worker's complete roster: composition always runs (delivery-gated separately), passivated rows persist in a lastComposed map independent of delivery, and pending removedAgentIds ride the snapshot frame so deletions survive a disconnect; the supervisor applies removals after replacement. - Queued-run supersession has one mechanism at the queued-entry lifecycle: observeRosterChildUpdate deletes the queued row when the child's session is bound and its write guard rejects late queued updates for bound children; roster composition order carries no semantics (verified by insertion-order reversal). - list-all sessionDir scoping matches artifact-dir children through their owning root's sessions dir instead of the shared sibling artifacts tree, so sibling session dirs no longer leak each other's subagents. - Worker roster reporter state is a plain field initializer again; the prototype-based worker_auth fixture constructs the state it needs. ENG-5794
- A child run that terminates before binding is a roster removal, never a passivated phantom row. - list all rescans the disk per call (supervisor-local catalog subprocess, no worker round-trips) and merges with the ledger, which wins for rows it knows; sessionDir defaults to the configured sessions dir, the seed scan passes it too, and name validation reads the same per-call catalog path. Seeding now exists for selectors, name checks, and liveness only. - Saved-session deletes publish removals only when the file was actually deleted, resolve the roster agent id through the ledger entry or spawn edge (childId for subagents), append the spawn-ledger tombstone so deleted subagents never reseed, and offline deletes of worker-owned passivated files forward to the owning worker instead of being rejected as active. - Roster frames respect backpressure: a non-drained socket gets no writes, delivery requires an accepted write, undelivered state stays uncommitted, and a drain re-flushes it. - Roster agent ids qualify child ids by parent path: child ids are 32-bit and uniqueness-checked only per parent (agent-session mkdir loop), so bare ids collide across parents at scale. - findWorker's miss path refreshes all workers once, closing the just-bound-but-unflushed routing window without reviving the hot-path fan-out; a summaries refresh no longer overwrites roster deltas that landed while its list request was in flight. - Seeded artifact-dir rows hydrate their real cwd lazily from the transcript header on first list-all use, keeping startup free of per-child file reads. ENG-5794
- Both remaining removal producers (rlm subagent deletion and discarded bound-child drafts) publish parent-qualified agent ids through one shared resolution (rosterAgentIdForRlmChild), matching the qualified row keys. - Delivery authority is the live supervisor claim: hasAuthenticated- SupervisorClient and broadcastRosterFrame require supervisorClaims membership, so a revoked socket can never satisfy delivery. - Generation-acked tombstone retention closes the kernel-write-vs-consumed gap: every roster frame carries a monotonic generation, delivered removals are retained as tombstones, the supervisor acks its last consumed generation in worker_auth, the reauth snapshot replays newer tombstones, and the worker prunes acked ones (recreated agents drop their stale tombstones at composition). - A refresh response staler than a mid-flight delta is discarded entirely (one bounded retry) instead of partially applied, and the eviction snapshot reads the roster so a busy delta always outranks a stale list. - Saved-child deletes append the spawn-ledger tombstone FIRST and abort on append failure; a tombstoned-but-undeleted file is the accepted orphan of a failed delete and keeps its roster row for retry. ENG-5794
…wn ledger Child-ness of a saved-session delete target now comes from worker-held state (the file-indexed composed roster entry or the transcript's parent metadata), never from a ledger read that can fail. For a child target an edges() rejection or a failed tombstone append aborts before file deletion with the error surfaced and no removal published; top-level targets never touch the spawn ledger. ENG-5794
…guard stay push-clean The subagents-bar roster callback now requests a render, so a push with no accompanying session event paints immediately. Agents-view exit closes the socket before store disposal (the supervisor drops the subscription with the client) and dispose serializes behind attach with a fire-and-forget unsubscribe, so a wedged daemon cannot block exit and an in-flight attach cannot leave a dangling listener. The pre-delete liveness probe keeps its narrower plain-list verdict local instead of overwriting the pushed catalog, so a failed delete no longer hides queued/passivated rows on the next heartbeat reconcile.
…-daemon string liveCatalogReady could never be false once the view runs: the field, its dead savedCatalogReady initializer, and the liveCatalog* fixture leftovers are gone, and shouldApplyScopeResolution takes only the saved-catalog readiness. The stale-daemon capability message now has one owner (STALE_ROSTER_DAEMON_MESSAGE in roster-store).
…ect-child linkage The existing animation tick now rebuilds rows while a stale-age label is on screen (labels are baked in at row build, so a repaint alone cannot advance them), and the subagents bar parents children through the same getParentKeys linkage as the view tree via isDirectAgentChild, so parentSessionId-only children count in the bar exactly as they render in the view.
…havior Deletes narration comments (86 -> 20 added lines, keeping only genuine invariants: subscribe-reply race buffer, attach serialization, unparkable subscribe, removal-once/privacy set, drain-resync queueing, the cancelled removal signal, and wire-field docs), merges the roster store's lifecycle/race micro-pins into single behavior tests, consolidates the saved-search latch suite into one test, drops the remount-gate near-duplicate, slims the bar and view-instance suites, and deletes the roster-soak dev harness - its correctness claims (seed, coalesce, loss-gap resync, owned-row visibility, store convergence) are all pinned by the real-socket tests; only the scale/heap-plateau sweep is lost, which CI never ran.
…as no direct children A client-owned session's rows are invisible to the public roster by design, so the push-fed bar showed zero while live subagents existed. The roster wins whenever it reports at least one direct child for this parent; otherwise the connection snapshots carry the counts for that render.
…red fixtures One store-lifecycle test now carries capability-miss, raced-push replay, hello re-keying, attach serialization, and dispose; the wire compat E2E and the chat-bar E2E share one supervisor boot (bystander, coalescing, repaint, and the owned-session snapshot fallback ride the same socket). Cut with accepted residual risk: the mid-flight park test (PR #1909 carries the recoverable-park suite), the watchdog staleness push test (label rendering stays pinned in the rows test), the view-instance anchor/failed-delete pins (one-line UI fixes, self-healing paths), the handshake-exit pin, and the latch test's stale-success ordering (generation guard predates this PR). Comment pass two drops restating docs (amend, hello key, two wire-field docs).
…rks after row rebuilds The store's update dispatch now isolates each listener like the connection's emit does, so one throwing consumer cannot break the others or the process. The staleness sweep restamps any still-silent worker's rows that lack the mark instead of stamping only on the first stale pass - AgentRoster.write rebuilds rows from the incoming payload and dropped it, leaving 'last heard' labels missing after any supervisor-side write until the worker recovered and went stale again; mark equality keeps repeat sweeps push-free and the sweep stays the mark's single owner.
…own roster presence total > 0 conflated a client-owned session with a public parent whose children all left the roster, reviving stale snapshots (roster deletions emit no cancelled event to the client) and disagreeing with the agents view. The discriminator is now the parent session's own row: fall back only while connectionState.sessionId is absent from the pushed summaries, so a public parent with zero roster children shows zero.
…r the scoped reseed
snimu
dismissed
xeophon’s stale review
September 1, 2026 08:34
The merge-base changed after approval.
snimu
force-pushed
the
feat/agent-roster-push
branch
from
September 1, 2026 08:34
0c490a5 to
cd675e7
Compare
snimu
force-pushed
the
fix/daemon-client-reconnect-recoverable
branch
from
September 1, 2026 08:34
aa2a4bf to
7c1c6d8
Compare
… subscribe A transient roster_subscribe failure inside attach() rejected the reconnect loop's otherwise-complete recovery (and the update-restart reattach), ending in a closed, unusable session because a bar accessory failed. The seam now swallows the transient class - the bar degrades and the next reconnect or rebind re-attaches through the same line - while capability-miss stays the non-fatal false it already was and the agents view keeps its own hard-require path.
…tead of parking them Requests issued from the DaemonAgentConnection reconnect loop and the update-restart restore loop parked as awaitingReconnect when the socket closed mid-flight, but the daemon_hello that would replay them can only be produced by the same stuck loop: the connection deadlocked and never resynced or emitted closed. Thread the existing recoverable:false request option through attach()/getInitialSnapshot() and the raw list request so loop-owned requests reject into the loop's own bounded retry. Non-loop callers keep the default park-and-replay recovery.
snimu
force-pushed
the
fix/daemon-client-reconnect-recoverable
branch
from
September 1, 2026 08:48
7c1c6d8 to
ae22984
Compare
xeophon
previously approved these changes
Sep 1, 2026
xeophon
added a commit
that referenced
this pull request
Sep 1, 2026
…iew and subagents bar (#1900) * feat(coding-agent): event-driven supervisor agent roster; serve list from the ledger Workers now push roster deltas to the supervisor on session events (roster_delta/roster_heartbeat worker frames, compute-on-event and send-only-if-changed, plus a 15s unref'd heartbeat tick). The supervisor keeps one roster ledger seeded at startup from the session catalog and the RLM spawn ledger (tombstones excluded), classifies status exactly once at write via classifyAgentStatus, and serves list, selector matching, family catalogs, and peer rosters from it. Deletions this enables: - handleList per-worker fan-out with its 5s timeout and silent stale summaries; list now does zero worker round-trips. - Event-triggered blanket refreshWorkerSummaries (kept only as a per-worker shim for legacy workers that do not advertise the roster capability in their worker_auth response). - mergeSessionLists; 'list all' is served from the already-merged ledger. - streamingMessage off the list wire (recovery/adoption refresh still seeds the stream reconstructor). Visibility and liveness: - Admitted child runs appear as queued roster rows before their session exists and merge into the session row when it binds. - Close, passivation, and eviction flip rows to inactive; rows are removed only for discarded drafts and spawn-ledger delete records. - A dead worker's rows are marked recovering natively on socket close and failed when recovery gives up; one 15s unref'd watchdog stamps lastHeardFromAt on rows of workers silent for more than 45s. busyClientOwnedSessionCount and daemon-launch busy checks are pinned by tests; roster frames live in the worker protocol, not the client schema, so no client protocol change ships in this part. Part 2 of 3 for the event-driven daemon-owned agent roster. ENG-5794 * fix(coding-agent): review fixes for the supervisor agent roster - list keeps its resident-only contract: non-all list emits only sessions with an activeSessionId; queued child runs and passivated rows stay ledger-internal, and list all carries the non-resident rows (owned rows keep their workerState/workerPid). sessionDir on list all now filters rows by sessions dir (including its sibling session-artifacts tree) instead of being ignored. - Offline saved-session renames and deletes, and worker-side saved-session deletes, now write the roster ledger. - Supervisor (re)authentication makes the worker send a replacing roster snapshot; rows absent from the snapshot passivate when a transcript exists and are removed otherwise. Pending state commits only after a frame reaches an authenticated supervisor, and the supervisor registers its frame listener before authenticating so the snapshot cannot race. - Remaining worker.summaries read paths (wake fallback, create reuse and readiness) moved to the ledger; create-forward and rename refreshes are gated to legacy workers, with the returned summary written to the ledger. - The roster wire summary keeps modelFallbackMessage for the active-open path. - Queued-run supersession has one mechanism (session rows overwrite queued rows at flush); the run-lifecycle cleanup in observeRosterChildUpdate is pinned by a bind-then-close test, and the saved-delete test proves the supervisor removes the ledger row end-to-end. - Worker roster reporter state is created lazily so prototype-based fixtures exercising worker_auth cannot crash the flush path. ENG-5794 * fix(coding-agent): roster review fixes round two - Non-all list restores the pre-roster population exactly: worker-owned rows (materialized and passivated) stay listed; sessionless queued-child rows are served by no list form; seeded/offline rows remain all-only. - The reauth snapshot is the worker's complete roster: composition always runs (delivery-gated separately), passivated rows persist in a lastComposed map independent of delivery, and pending removedAgentIds ride the snapshot frame so deletions survive a disconnect; the supervisor applies removals after replacement. - Queued-run supersession has one mechanism at the queued-entry lifecycle: observeRosterChildUpdate deletes the queued row when the child's session is bound and its write guard rejects late queued updates for bound children; roster composition order carries no semantics (verified by insertion-order reversal). - list-all sessionDir scoping matches artifact-dir children through their owning root's sessions dir instead of the shared sibling artifacts tree, so sibling session dirs no longer leak each other's subagents. - Worker roster reporter state is a plain field initializer again; the prototype-based worker_auth fixture constructs the state it needs. ENG-5794 * fix(coding-agent): roster bot-review fixes - A child run that terminates before binding is a roster removal, never a passivated phantom row. - list all rescans the disk per call (supervisor-local catalog subprocess, no worker round-trips) and merges with the ledger, which wins for rows it knows; sessionDir defaults to the configured sessions dir, the seed scan passes it too, and name validation reads the same per-call catalog path. Seeding now exists for selectors, name checks, and liveness only. - Saved-session deletes publish removals only when the file was actually deleted, resolve the roster agent id through the ledger entry or spawn edge (childId for subagents), append the spawn-ledger tombstone so deleted subagents never reseed, and offline deletes of worker-owned passivated files forward to the owning worker instead of being rejected as active. - Roster frames respect backpressure: a non-drained socket gets no writes, delivery requires an accepted write, undelivered state stays uncommitted, and a drain re-flushes it. - Roster agent ids qualify child ids by parent path: child ids are 32-bit and uniqueness-checked only per parent (agent-session mkdir loop), so bare ids collide across parents at scale. - findWorker's miss path refreshes all workers once, closing the just-bound-but-unflushed routing window without reviving the hot-path fan-out; a summaries refresh no longer overwrites roster deltas that landed while its list request was in flight. - Seeded artifact-dir rows hydrate their real cwd lazily from the transcript header on first list-all use, keeping startup free of per-child file reads. ENG-5794 * fix(coding-agent): roster bot-review fixes round two - Both remaining removal producers (rlm subagent deletion and discarded bound-child drafts) publish parent-qualified agent ids through one shared resolution (rosterAgentIdForRlmChild), matching the qualified row keys. - Delivery authority is the live supervisor claim: hasAuthenticated- SupervisorClient and broadcastRosterFrame require supervisorClaims membership, so a revoked socket can never satisfy delivery. - Generation-acked tombstone retention closes the kernel-write-vs-consumed gap: every roster frame carries a monotonic generation, delivered removals are retained as tombstones, the supervisor acks its last consumed generation in worker_auth, the reauth snapshot replays newer tombstones, and the worker prunes acked ones (recreated agents drop their stale tombstones at composition). - A refresh response staler than a mid-flight delta is discarded entirely (one bounded retry) instead of partially applied, and the eviction snapshot reads the roster so a busy delta always outranks a stale list. - Saved-child deletes append the spawn-ledger tombstone FIRST and abort on append failure; a tombstoned-but-undeleted file is the accepted orphan of a failed delete and keeps its roster row for retry. ENG-5794 * fix(coding-agent): child deletes never proceed past an unreadable spawn ledger Child-ness of a saved-session delete target now comes from worker-held state (the file-indexed composed roster entry or the transcript's parent metadata), never from a ledger read that can fail. For a child target an edges() rejection or a failed tombstone append aborts before file deletion with the error surfaced and no removal published; top-level targets never touch the spawn ledger. ENG-5794 * fix(coding-agent): classify unreadable delete targets through the spawn ledger Saved-session delete targets discriminate three ways: a readable no-parent transcript (or composed top-level row) is positively top-level and skips the spawn ledger; a positively-child target keeps the unguarded tombstone-first path; an UNKNOWN target (no composed row, unreadable or corrupt header — readSessionInfo's null is normalized so it cannot pass as readable) classifies via the ledger, where an edge means child, no edge means top-level, and a failed read aborts before deletion with no removal published. ENG-5794 * fix(coding-agent): roster bot-review fixes round three - Offline deletes honor descriptor-based ownership: a worker owning the file without a claimed roster row forwards when reachable and rejects with a retryable error when its socket is down, so a transcript is never deleted underneath a live owner. - The supervisor offline delete uses the worker path's three-way discrimination (positively top-level, positively child, unknown-via- ledger with abort on an unreadable read), so catalog-seeded children without rlmChildId and unreadable targets still tombstone first. - The list session-dir parent walk uses a visited-set cycle guard instead of a hop cap, staying correct at any depth. - Seeded artifact rows derive their session id from the transcript filename so persisted-session-id selectors resolve before any worker delta; edge.childId remains the child identifier. - Worker frames carry their source connection and are dropped when a superseded connection's buffer flushes after a reconnect. - list all treats the disk as authoritative for non-resident rows, propagates scan failures instead of shrinking the list, and preserves the newest-first catalog order with worker rows replacing their scanned files in place. ENG-5794 * fix(coding-agent): roster bot-review fixes round four - Worker frames accept exactly the current client and the in-flight replacement (worker.pendingClient, set before authentication and cleared in a finally on success or rollback), so a replacing connection's immediate snapshot is never discarded while the old client is still installed. - Offline deletes reclaim a dead failed registration through the existing reclaim machinery before proceeding; live or recovering owners keep the retryable rejection. - Depth-33 parent chains and self-cycles are pinned for session-dir scoping, and seeded artifact rows are pinned to resolve by their persisted transcript id when the filename differs from the child id. ENG-5794 * refactor(coding-agent): drop the lossless roster channel; disk is durable truth - Deltas become best-effort freshness hints: any undelivered, refused, or backpressured write just marks a pending snapshot, and one full replacing snapshot flows on (re)connect or drain. Generation counters, delivered-commit bookkeeping, tombstone retention with ack and prune, and the worker_auth rosterGeneration ack all go away. - The supervisor applies a snapshot atomically: it replaces the worker's rows, deletes absent rows outright, then reseeds subagent families from the spawn ledger with tombstoned edges filtered out. Tombstone-first delete classification stays on both delete paths. - Undelivered removal ids stay pending and ride the first delivered frame, so removals of unattributed rows survive backpressure. - The startup catalog seed goes away; list all, name checks, and worker matching already read disk per call, so only the spawn-ledger seed remains. - The roster test suite consolidates into lifecycle, delivery, delete- path, and regression groups: one queued-child lifecycle scenario, one snapshot escalation pin, one snapshot-replace-and-reseed pin, and an ownership-routing table replace the per-round accretions; the depth-33 walk pin drops with the machinery it guarded. ENG-5794 * perf(coding-agent): cache roster row serializations across flushes Change detection reuses the previous flush's JSON strings, so a churny flush stringifies each current row once instead of twice. ENG-5794 * refactor(coding-agent): restart pre-roster workers on adoption; drop the legacy shim - connectWorker rejects a worker_auth response without the roster capability, so adoption of a pre-roster worker routes through the existing recoverWorker machinery and respawns it from the current binary; sessions reload idle and resume on the next prompt. - The rosterCapable flag, the legacy event-refresh branch, both conditional refresh call sites, and the refresh-vs-delta race guard go away; deltas own the roster and pulled summaries only feed recovery stream seeding, eviction checks, and descriptor pointers. - syncWorkerSummariesIntoRoster shrinks to a gap filler: launch and recovery pulls fill missing rows and claim workerless seeded rows (registry children no delta composes) without ever overwriting delta-fed rows, so no ordering guard is needed. - Tests seed rosters via writeRosterEntry, eviction fixtures seed the delta-fed rows they previously got from refresh syncs, and a new pin covers the adoption restart routing. ENG-5794 * fix(coding-agent): roster rework review fixes, supervisor and worker halves - Worker frames adopt real socket semantics: a write queued under backpressure IS delivered, so pending state clears on it; only an absent, destroyed, or unauthenticated claim socket is a loss gap, and one replacing snapshot closes it. Drains never resend queued frames. - Gap fills are epoch-guarded: every applied roster frame bumps a supervisor-local per-worker counter, a pull that straddled a frame re-pulls once, and a still-moving epoch skips the fill entirely so a stale list can never resurrect a just-removed row. - Snapshot applies pre-read the spawn ledger and queue later frames behind them per worker, so replacement, absentee deletion, and the tombstone-filtered reseed land atomically with no transient removal. - The startup catalog seed returns: a push-only view needs saved top-level rows in the ledger itself. Rows stay slim and list-all keeps its per-call disk rescan. - Pre-roster adoption performs a real bare restart: the durable descriptor is the whole respawn context, the old process is killed only under its observed identity, and launchWorker respawns from the current binary. Pinned end-to-end against a real supervisor with a capability-less fake worker, no recovery mocks. - Model, thinking-level, and rename changes reach subscribers: the thinking_level_changed trigger joins the roster event set and the four model/thinking handlers schedule a flush. ENG-5794 * test(coding-agent): await owned process exits before teardown rmSync The shared afterEach now awaits every tracked child and worker pid before deleting temp directories, and rmSync retries transient failures, so a dying worker's log writer cannot race the cleanup into ENOTEMPTY. Hardened in the shared helper because every test in this file spawns supervisors and workers through the same teardown. ENG-5794 * fix(coding-agent): serialize roster pulls with frame applies and harden owner resolution - bump the roster epoch at frame receipt and route pull gap-fills through the one per-worker apply chain (chainWorkerRosterApply) - resolve delete owners through findWorkerBySessionFile, which now also consults pulled worker summaries for unflushed child rows - restartPreRosterWorker launches a replacement only against a confirmed-stopped predecessor; unverifiable live processes keep the worker failed - canonicalize session paths in findActiveSessionByFile so the active guard matches the tombstone/removal side across symlinks - flush the roster projection after execute_bash_and_wait * test(coding-agent): pin snapshot/pull serialization, pre-roster restart guard, symlink delete guard - a pull fill queued behind an in-flight snapshot re-claims reseeded rows - an unverifiable live pre-roster worker stays failed with no replacement - delete_saved_session through a symlink hits the active-session guard * fix(coding-agent): abort queued roster applies for unregistered workers; launch only on a confirmed-stopped predecessor - chained frame applies and pull fills re-check the worker registration before running and after the snapshot's ledger pre-read, so a stop can never be overwritten by a resumed apply - a failed partial apply schedules one gap-fill pull as repair - restartPreRosterWorker launches only when the final identity verdict is gone or replaced; a current-to-unknown flip keeps the worker failed * test(coding-agent): let the stop land mid pre-read in the snapshot-abort pin * fix(coding-agent): single-flight roster repair pull with a logged failure - a per-worker marker caps repair pulls at one in flight; repeated apply failures reuse it and a failing repair cannot respawn itself - a failed repair logs one warning naming the worker * test(coding-agent): reduce the roster suite to distinct behavior pins - drop the delete round-trip, supervisor unknown-target classification, modelFallbackMessage projection, discarded-draft removal ids, and the duplicated ledger-read-abort scenario; each surviving pin is named in the review ledger - one makeOfflineSupervisor helper replaces four hand-rolled real supervisor constructions; the queued-child test now also pins delta removals * test(coding-agent): drop an unused import after the projection pin removal * test(coding-agent): fix formatting after the removal-id pin cut * test(coding-agent): final reviewer-directed roster suite cuts - collision qualification folds into the queued-child lifecycle pin - one population matrix covers seeding, resident worker rows, and eviction; the standalone passivated-children test is absorbed - the supervisor staleness sweep pin moves to the push-layer test only - the two pre-roster restart scenarios become one named table - the real-socket test drops its fixed sleep; the top-level delete pin asserts the exact removed session id * test(coding-agent): biome format for the population matrix * test(coding-agent): pin resident and seeded rows side by side in one live list-all * chore(coding-agent): comment sweep — one-line present-tense rationale, drop dead fixture fields - condense the moved two-line busy-projection comment and the test section banners to one line each - present-tense fixes in two test comments - delete the dead rosterCapable/lastFrameAt/rosterStale fixture fields * fix(coding-agent): republish retry/tool transitions and guard pulled root pointers - auto_retry_* and tool_execution_* events join the roster flush triggers: they flip isSessionActive/activity and isRunningTools; the flush already coalesces per tick and sends only changed rows - the pulled root descriptor persists through the per-worker apply chain under the epoch guard, so a stale list can never clobber pointers a frame updated mid-pull * refactor(coding-agent): rename AgentRosterLedger to AgentRoster * refactor(coding-agent): one owner each for busy/status adapters, registration flags, delete tombstone policy, and the roster heartbeat contract - isSessionSummaryBusy and classifySessionRosterStatus move into agent-roster.ts (re-exported from daemon-session-list.ts for existing importers); classifyWorkerRosterEntry now delegates instead of re-inlining the busy predicate. - The user-delete classification + tombstone-first policy lives once in rlm-ledger.ts (tombstoneSavedSessionDelete); the worker and supervisor delete_saved_session routes both call it. - passivatedWorkerRosterEntry never freezes hasRegisteredHeartbeat/hasRegisteredCronJob: the worker flush recomputes them from the cron store via the extracted scheduledJobRegistrations index (the one registration truth); callers without a cron store strip them. - ROSTER_HEARTBEAT_INTERVAL_MS moves next to the roster capability in daemon-worker-protocol.ts; the supervisor staleness threshold derives from it (three missed heartbeats) instead of restating 45s. * fix(coding-agent): supervisor roster correctness batch - Offline delete_saved_session asserts client access to the owning worker before forwarding or reclaiming: a foreign client's delete of a client-owned worker's passivated session is an unknown target again. - Adopted pre-roster workers with an owner are parked through recoverWorker (their launch env lives only with the owning client) instead of a bare descriptor respawn that would drop it. - A worker's queued-child rows are removed, not passivated, when its registration goes away: a terminal unbound run owns no transcript, and the fileless ghost row nothing could list or delete is gone. - Snapshot reseeds keep a passive registry child's previous worker claim, and gap fills also replace synthetic ledger seeds, so passive children stop flapping out of the non-all list and stale frozen rows stop feeding eviction. - hydrateSeededEntry re-checks the row after its header read; a frame that rebinds the agentId mid-read is never clobbered with the stale seed. - matchWorkers and findSummaryInWorker skip queued-child rows: there is no session to route to, and a queued name must not create false ambiguity. - familyCatalogEntries is fail-closed again: a failed catalog scan propagates instead of silently shrinking name-uniqueness checks. - The idle-eviction pull is documented as a responsiveness gate; the decision data comes from the delta-fed roster. - handleList list-all merge drops the O(n^2) includes() and overlaps seeded-row header reads. * test(coding-agent): pin the roster correctness batch - foreign client delete of a client-owned worker's passivated session rejects as unknown - worker unregistration removes queued rows instead of passivating unlistable ghosts - hydrateSeededEntry never clobbers a row rebound during its header read - passive registry children keep their worker claim across snapshots that omit them and stay in the non-all list; the queued gap fill also replaces the synthetic ledger seed with the pulled summary - the worker reporter fixture carries the real lastComposedJson field * fix(coding-agent): let composed session rows beat lingering queued markers addRuntime registers a child session before the bind-reporting rlm_child_update arrives; a roster flush in that window replaced the resident row with its sessionless queued stub. Session rows now win at compose time and clear the stale queued marker. * fix(coding-agent): keep unserved worker files listed as inactive rows in list all A client-owned worker's row sits in activeByFile even when the client is not served it; the list-all merge then dropped both the live row and the catalog row, hiding the session entirely. The on-disk scan is public (no list surface filters it by ownership), so the file lists as a plain inactive row again, exactly like before the roster ledger. * fix(coding-agent): guard roster applies against dead registrations and unreadable ledgers - Unchained (fast-path) deltas now re-check registration currency exactly like chained applies: a late frame from an unregistered or replaced worker registration cannot resurrect its rows with a stale claim. - A snapshot whose spawn-ledger pre-read fails skips the absentee sweep and reseed (it cannot tell registry children from stale rows without edges), keeps applying the snapshot's own entries, and schedules the single-flight repair pull instead of silently deleting passive children. * fix(coding-agent): drop client-owned workers' roster rows on unregistration Passivating an owned worker's rows strips the workerId and turns private rows into public inactive rows (path/cwd/name/message metadata) served to every client through offline list paths and roster reads. Client-owned workers are ephemeral, so their rows die with the registration; the public disk scan still lists whatever files actually persist. * fix(coding-agent): re-verify pid identity at the last moment before the recovery SIGKILL * fix(coding-agent): import the moved busy predicate for the empty-session evictability rule * fix(coding-agent): serve empty-detach eviction from the roster with write-through pulls Adapts the empty-session last-detach eviction (from the idle-eviction fix round on main) to the roster world with one decision source: - isEmptyDetachEvictionCandidate reads the worker's non-queued roster rows instead of the worker.summaries pull cache. - The hook's two pulls stay as responsiveness gates and now write through: syncRosterFromWorkerSummaries (formerly the gap fill) lets a worker's own rows take the pull's fields, so the post-drain re-read deterministically sees a schedule registered by a mutation admitted mid-refresh. The pull-epoch guard keeps every write-through at least as fresh as the row it replaces, and rows claimed by another worker are never stolen. - The detach-eviction tests seed the supervisor roster like the other adapted suites (matchWorkers is roster-backed). Semantics of the empty-detach eviction are unchanged: empty + unnamed + not busy + no registrations + no attached clients, last detach only, client-owned workers excluded, fence coordination intact. * fix(coding-agent): flush the roster on plain cron job add and cancel cronStore.onHeartbeatChange only fires on the heartbeat catalog signature, and cron_add/cron_cancel emit no session event, so hasRegisteredCronJob on the roster row went stale: the idle sweep could evict a worker whose only reason to stay resident was a fresh cron job, or keep a cancelled one pinned forever. The handlers flush explicitly, like set_model does for events that have no session-event carrier. * fix(coding-agent): keep the hydrated summary when a snapshot reseeds a claimed child The absentee reseed wrote a synthetic ledger seed (no lastActivityAt, messageCount 0, artifact-dir cwd) over a previously hydrated claimed row. Every worker snapshot goes through this for passive registry children, and Date.parse(undefined) = NaN made canEvictWorker permanently false while the degraded row persisted; plain list served the degraded fields too. The reseed now rewrites the previous entry's summary (claim and data both survive); only rows with no prior entry get the synthetic workerless seed. * chore(coding-agent): roster review nits - flushRoster's queuedChildren loop var is an agentId (parent-qualified), not a bare childId; name it so. - set/cycle_thinking_level drop their explicit roster flushes: an actual change emits thinking_level_changed, which is a trigger already (the set_model flushes stay - model changes emit no session event). - Non-worker daemons no longer accumulate removedAgentIds that no flush ever drains. - The changelog stops presenting recovering/last-heard-from as user-visible in this PR; the surfaces that display them ship in the follow-up. * fix(coding-agent): scope pending roster removals to one incarnation and fence applies on socket close - A pending removal now records the sessionId it removes. A row composed again under the same agentId with a different sessionId (or a re-admitted queued run) is a new incarnation and cancels the stale removal instead of being suppressed from every flush including the reconnect snapshot; the removed incarnation itself stays suppressed mid-teardown so a deleted child cannot ghost back as a passivated row. - isWorkerRosterApplyCurrent also requires a live (or authenticating) connection: an apply left in flight by a closed socket can no longer rewrite rows and drop the recovering label handleWorkerClose just set. Reconnection resumes applies through the pending client. * chore(coding-agent): slim roster comments and consolidate roster tests Comments: 153 -> 33 added src comment lines. Kept only notes resolving real ambiguity (pull-epoch guard, close fence, reseed/NaN rationale, incarnation suppression, privacy rules, backpressure delivery assumption, pid-recycle and SIGKILL-wait justifications, wire-schema notes); deleted all narration. Tests: one behavior test per contract. Merged into their parent behavior test: bind-window compose-wins, trigger republish, queued rows ledger-internal, queued-ghost flip, offline rename + failed-disk delete, client-owned inactive list row, unchained-delta currency, set_model no-carrier flush, reseed data quality, qualified removal ids. Deleted pins whose behavior another test or the process-suite E2E already proves: undelivered-change escalation, real- socket backpressured snapshot, recovering-on-close (asserted in the close- fence test), frame-source trust, seeded selector resolution, root-pointer epoch persist, miss-path refresh routing, hydrate race, sessions-dir topology scoping, delivery-semantics mock twin, descriptor-path delete-routing variant. * fix(coding-agent): roster identity and staleness fixes from the sixth review round - The offline delete's roster cleanup deletes only the row object it observed: a write during the tombstone/unlink awaits replaces the row, and deleting by agentId alone would kill the replacement. - Subagent roster ids fall back to the live parent id when the parent has no session path (--no-session parents never write ledger edges), so children of two such parents cannot collide on the per-parent 32-bit child id. - An archived top-level close (killed/completed/replaced; not shutdown/update) publishes a roster removal instead of leaving a passivated "live" ghost: the worker's list no longer carries the session and the disk scan serves the archived file honestly. Subagent rows keep passivating, mirroring the registry's completed children. - Roster applies are fenced by their own source connection: an apply parked on the spawn-ledger read by a dead connection can no longer resume during a reconnect's pre-auth window and clear the recovering labels, while the authenticating connection's own post-auth snapshot still applies immediately. * chore(coding-agent): second slim pass on roster tests and comments - One shared roster-seeding fixture (test/fixtures/roster-seed.ts) replaces the five per-suite copies. - Deleted mechanism pins with accepted residual risk: flush change-dedup and trigger-set micro-pins (the lifecycle test still pins the closed-session flip), the single-flight repair pull, the mid-pull epoch skip, the crafted late-update guard phase, and the second pre-roster identity scenario. - Another comment pass: dropped notes that restate the guard beside them. * fix(coding-agent): seventh review round — spawn-append scoping, stat-reconciled seeds, one file-ownership source - pendingRlmSpawnAppends is keyed by parent + childId at every site: child ids are only unique per parent, and a cross-parent collision made one admission await the wrong ledger append while the other proceeded without awaiting its own durable spawn record. - The roster's ledger seeding and snapshot reseeds read liveEdges(), the ledger's own stat-reconciled view (the rule family() already owned): rows whose transcript was removed out-of-band never serve in list --all. Tombstone-first covers in-band deletes; this covers external removal. - findWorkerBySessionFile no longer consults the stale pull cache: the roster claim and the durable descriptor paths are the ownership sources, so a removed row cannot route a create back to a worker that would answer with its root session. - classifyWorkerRosterEntry is module-private (no consumer outside the module). - The changelog notes the client-owned exception to inactive-row retention. * fix(coding-agent): remove, not passivate, rows renamed by in-place session swaps new_session/switch_session/fork swap the runtime under the same state: the activeSessionId survives while the sessionId (and so the top-level agentId) changes. The old agentId vanished from composition without a close, so the passivation-retention loop kept serving it as a stale claimed row that plain list never carried before the roster. The flush loop now treats a vanished row whose activeSessionId still composes under a different agentId as a removal — one owner for every swap origin, no per-command bookkeeping — and the pending-removal cancel rule also revives resident top-level rows (switch-back, resume-after-archive) while the resident-subagent teardown race stays suppressed. * fix(coding-agent): ninth review round — one family snapshot, family-scoped reseeds, filter-all tombstones - family() builds its child suppression from the same single replay + stat snapshot that emits child rows: a sessions-dir child whose parent transcript vanished degrades to a root row instead of disappearing, and a concurrent cross-process append can no longer make the two views disagree. - Snapshot reseeds are scoped to the snapshotting worker's own family: the reseed exists to restore that worker's absentee-swept registry children, and resurrecting other families' unclaimed rows leaked a client-owned worker's just-dropped children back into list --all as public rows (the ownership record is already gone by then, so this scoping IS the privacy rule). - Transcript deletes tombstone every edge matching the path: appendSpawn's per-process uniqueness check leaves a cross-process TOCTOU window, and a raced duplicate left live would resurrect a later recreation as a subagent. - The changelog states the staleness behavior honestly: rows are as fresh as the worker's last delta, silence is annotated rather than hidden. * feat(coding-agent): roster subscription push and agents-view consumption Adds roster_subscribe/roster_unsubscribe and capability-gated roster_update pushes (agent_roster, schema revision 24); the agents view holds a shared DaemonClient and roster store across scope transitions, renders ledger statuses and labels (queued/recovering/failed, staleness), falls back to the legacy poll path only against daemons without the capability, and fetches the saved catalog once per view instance when a search query needs deep text. The supervisor coalesces pushes per macrotask and resyncs backpressured subscribers on drain. ENG-5794 * test(coding-agent): give launch fixtures the roster push buffers * feat(coding-agent): finalize roster push consumption in the agents view Navigation issues no daemon requests (pinned), the lazy saved-catalog fetch happens once per view instance only when a query is typed, and rows synthesized from the ledger carry rosterStatus so sections, labels, and staleness render the classify-once verdicts. Adds the changelog fragment. ENG-5794 * fix(coding-agent): roster push review fixes - The subagents bar follows the roster's terminal rule: a done/error run with no session evidence across its history (daemon session id, live activity, or session token accounting) is dropped like a cancelled one, on both connection kinds; children with transcripts keep their rows. The bar/view equality test now drives the real update handler through a lifecycle matrix (unbound-error, queued, bound, heartbeat-only, passivated, recovering) against roster-derived sections. - Visibility transitions are roster pushes: a row claimed by a client-owned worker reaches subscribers as a removal, and promotion re-enqueues the worker's rows. - A refused drain resync re-arms rosterResyncPending so the next drain retries instead of stranding the subscriber; pinned through the real connection drain listener. - The watchdog staleness stamp and clear are pinned end-to-end to a subscriber push. - Saved-sibling name validation prefers the ledger's rosterStatus like the other fallbacks. - Queued and bound child rows share one stable identity (the qualified roster agent id) in row identities and reconciliation aliases, so selection survives the bind push without duplicate rows. ENG-5794 * fix(coding-agent): make subagent bound-ness sticky across terminal projections Terminal merges clear the session-evidence display fields, so a repeated terminal projection saw an evidence-free snapshot and removed a transcript-bearing child. Bound-ness is now a sticky everBound snapshot field set the first time evidence (daemon session id, live activity, or session token accounting) is observed, and the terminal drop rule reads it, keeping repeated terminal projections idempotent while a never-bound run still cannot fabricate evidence. Saved-sibling name validation now reads the ledger row's status through the session-file index instead of a rosterStatus field that inactive summaries never carry. ENG-5794 * refactor(coding-agent): push-only roster consumers and churn hardening - The agents view drops its poll fallback as dead code: exact-version forced restart already ships, so a daemon without the agent_roster capability is a hard error naming the stale daemon, refreshes reapply the pushed store locally, and reconnects re-attach the subscription. - The daemon-mode subagents bar consumes the pushed roster through a store shared per connection (subscribeAgentRoster on AgentConnection), counting direct children with the same ledger statuses the view renders; the in-process connection keeps the sanctioned snapshot-to-classifier path. The lifecycle equality matrix now pins push-fed bar == view. - A drain-time roster resync clears its pending flag even when the write reports backpressure, since socket.write queues the payload either way: one resync per loss gap, never one per drain. - scripts/roster-soak.ts drives a real supervisor socket with thousands of churning sessions, depth-40 chains, and a deliberately slow subscriber, asserting convergence, coalesced resyncs, bounded heap, and answered commands. ENG-5794 * test(coding-agent): align view fixtures with the push-only refresh path Fixture fallout from removing the poll fallback and the legacy refresh shim: query-changed and reply fixtures stub the saved-catalog fetch, the handoff-scope pins feed the pushed store instead of a failing list request, the rename pin asserts one local reapply, and the monitor seed helper writes delta-shaped rows directly. Biome formatting rides along. ENG-5794 * fix(coding-agent): push-only view fixes for the rework review round - The daemon-mode bar fails hard on a stale daemon: subscribing to the roster is awaited during session (re)binding, the in-flight forced reattach after reconnect throws instead of ignoring a refusal, and the snapshot->classifier path survives only on in-process connections. A production-path pin (real supervisor socket, real DaemonAgentConnection and store) proves the bar counts pushed rows, not stale snapshots. - The staleness watchdog stamps rows only on the transition into stale; repeat sweeps of an already-stale worker emit zero mutations. - roster_unsubscribe clears any pending resync and drains re-check the subscription before resyncing. - A snapshot apply never surfaces a live spawn-ledger edge as a transient removal, pinned over the push surface. - The soak asserts exact payload equality for both subscribers, requires the induced loss gap to resolve through coalesced resyncs, bounds list latencies, and names the worker-frame integration pin it leaves to vitest. refreshBothCatalogs and the poll-era comments go away; fixtures drop the last poll-model stubs. ENG-5794 * fix(coding-agent): push-only consumer fixes for the roster review round - buffer roster_update pushes racing the subscribe reply and replay them after the snapshot resync (AgentsViewRosterStore.attach) - await the parsed daemon_hello inside attach so a fresh connection is never misread as missing the agent_roster capability - re-arm the lazy saved-catalog load when its fetch fails, and refresh the loaded catalog after renames and deactivations - classify roster_subscribe/roster_unsubscribe as read-only so command journal replays cannot skip re-subscribing a new socket - roster-soak: try/finally lifecycle; any rejection cleans up and exits nonzero instead of hanging * test(coding-agent): pin subscribe-race replay and subscription journal classification - pushes racing the roster_subscribe reply replay after the snapshot resync - roster_subscribe/roster_unsubscribe stay out of the mutation journal * test(coding-agent): rename pins the loaded-saved-catalog refresh contract * fix(coding-agent): persistent saved-catalog gate with generation-safe re-arm - persistentState.savedCatalogLoaded survives view remounts and gates the rename/deactivate/delete catalog refreshes - the per-instance search fetch re-arms only while no catalog exists, so a superseded fetch's false return cannot force refetches or clear data - reconnect-timeout status tells the truth: reconnect stopped * test(coding-agent): pin the persistent saved-catalog gate and the superseded-fetch race * fix(coding-agent): re-arm the saved-catalog search latch only from the current fetch - refreshSavedSessions owns the re-arm: it fires on the current generation's failure (or a skipped start) while no catalog exists, so a superseded settle can never disarm the latch under a pending fetch - remount coverage moves to a production-constructor test; the hand-built harness variant is deleted * test(coding-agent): give the 502 refresh harness the real re-arm helper * test(coding-agent): fold view roster near-duplicates into their surviving pins - store apply/removal/resync test also pins one listener emission per tick - one row-label test covers queued, recovering, and stale ledger states - the zero-request refresh test also drives row navigation * test(coding-agent): drop a stray blank line from the view roster suite * test(coding-agent): final reviewer-directed view suite cuts - drop the hand-wired reconcile-batch adapter test and the cross-surface bar matrix; the two unique history branches move next to the other updateSubagentSummary pins - the labels test also pins the stable queued-to-bound row identity - the zero-request refresh pin sheds its navigation half * test(coding-agent): tsgo and format fixes for the moved bar pins * chore(coding-agent): comment sweep — one-line present-tense rationale, drop dead soak/view fixture fields - condense the anchor-identity and soak-header comments to one line each - present-tense fix in the soak convergence check label - delete the dead rosterCapable fixture fields * fix(coding-agent): bot-round view fixes — bar degrade, handshake exit, restored-query fetch, serialized attach - the subagents bar degrades to child snapshots when the roster subscribe fails; a session rebind never hard-fails on it (the agents view keeps the hard error) - the view arms its close-driven reconnect only after a successful roster attach, so a handshake failure exits cleanly instead of racing a background reconnect against client disposal - one armSavedSearchFetch latch serves typed and restored queries; run() arms it for a restored non-empty query - AgentsViewRosterStore serializes attaches, so a stale attempt settling late can never detach a newer subscription * fix(coding-agent): loaded-catalog arm gate and capability/transient split on the roster attach seam - armSavedSearchFetch honors the persistent savedCatalogLoaded gate: a remounted view with a restored query never refetches a loaded catalog - AgentsViewRosterStore.attach returns false only for a missing capability; transport and subscribe failures detach their own listener and throw, so the connection reconnect loop retries the rebind instead of resyncing with a dead subscription; the chat bar keeps catching both * fix(coding-agent): unparkable roster subscribe and hello-keyed subscription identity - roster_subscribe opts out of request-recovery parking (new per-request recoverable option): a close mid-subscribe rejects into the callers own bounded retry loop instead of deadlocking the connection reconnect, whose parked request could only be revived by the hello that same loop was stuck producing - the store keys its live subscription to the connection hello: a reconnected transport re-subscribes naturally and the force flag is deleted from attach and every call site * fix(coding-agent): restore the AgentRosterStatus type import after the ledger rebase * docs(coding-agent): correct the roster changelog — the poll path is removed, not kept * fix(coding-agent): drop never-bound terminal child runs at the producer AgentSession now owns the roster rule end to end: getRlmChildSnapshots skips terminal runs that never bound a session (covering seed/replace/state paths), and a pre-bind failure emits its terminal update as cancelled - the wire's existing removal signal - so event consumers need no second predicate. Deletes the everBound sticky marker, its snapshot field, and hasSubagentSessionEvidence. The failure still reaches the parent as rlm_child_failure and stays listed with its true error status in listRlmSubagents. * refactor(coding-agent): funnel roster label/staleness stamps through one store channel AgentRoster.amend patches statusLabel/lastHeardFromAt in place and notifies, so markWorkerRosterEntries, the staleness sweep, and the promotion re-publish stop bypassing the store with manual onRosterMutation calls; onMutation now has one caller channel. * test(coding-agent): pin old-client/new-daemon roster compat An unsubscribed socket on a live supervisor never receives roster_update, and the pre-roster list validator stays open to the additive rosterStatus/ statusLabel/lastHeardFromAt summary fields; both directions of the schema-24 wire change are now pinned. * fix(coding-agent): re-arm agents-view reconnect and the saved search fetch after outages The 15s heartbeat poll now restarts the reconnect loop over a dead socket instead of leaving the view permanently offline after one 120s window (the 1s poll used to do this), and a connected poll failure no longer overwrites a sticky notice. A successful reconnect re-arms the lazy saved-catalog fetch through the one arm predicate so a query that outlived the outage regains its deep-search matches. * fix(coding-agent): push-era agents-view polish — anchor settle, /name disarm, hour ages Each roster push settles a missing restored selection anchor (rebuilds re-arm it and no poll clears it anymore), a successful /name disarms the composer like /kill, the delete-confirm list RPC documents itself as a deliberate authoritative liveness check, last-heard ages gain an hour unit, and refreshSessions loses its dead boolean (the 'refresh failed' rename status was unreachable). * style(coding-agent): drop rebase-introduced blank lines around the roster imports * fix(coding-agent): publish roster removals at most once, never for owned-only rows flushRosterUpdates now gates removed ids on a published-ids set: rows born to client-owned workers emit nothing (no repeated no-op reconciles in every subscriber, no leak of private roster ids that embed transcript paths), a published row claimed by an owned worker leaves the surface exactly once, and promotion re-publishes through the existing empty amend. Seeds and resyncs register their ids so later disappearances stay removable. * fix(coding-agent): roster push repaints the bar; teardown and delete-guard stay push-clean The subagents-bar roster callback now requests a render, so a push with no accompanying session event paints immediately. Agents-view exit closes the socket before store disposal (the supervisor drops the subscription with the client) and dispose serializes behind attach with a fire-and-forget unsubscribe, so a wedged daemon cannot block exit and an in-flight attach cannot leave a dangling listener. The pre-delete liveness probe keeps its narrower plain-list verdict local instead of overwriting the pushed catalog, so a failed delete no longer hides queued/passivated rows on the next heartbeat reconcile. * chore(coding-agent): sweep poll-era sediment and the duplicated stale-daemon string liveCatalogReady could never be false once the view runs: the field, its dead savedCatalogReady initializer, and the liveCatalog* fixture leftovers are gone, and shouldApplyScopeResolution takes only the saved-catalog readiness. The stale-daemon capability message now has one owner (STALE_ROSTER_DAEMON_MESSAGE in roster-store). * fix(coding-agent): tick stale last-heard ages and share the bar's direct-child linkage The existing animation tick now rebuilds rows while a stale-age label is on screen (labels are baked in at row build, so a repaint alone cannot advance them), and the subagents bar parents children through the same getParentKeys linkage as the view tree via isDirectAgentChild, so parentSessionId-only children count in the bar exactly as they render in the view. * chore(coding-agent): slim roster comments and tests to one pin per behavior Deletes narration comments (86 -> 20 added lines, keeping only genuine invariants: subscribe-reply race buffer, attach serialization, unparkable subscribe, removal-once/privacy set, drain-resync queueing, the cancelled removal signal, and wire-field docs), merges the roster store's lifecycle/race micro-pins into single behavior tests, consolidates the saved-search latch suite into one test, drops the remount-gate near-duplicate, slims the bar and view-instance suites, and deletes the roster-soak dev harness - its correctness claims (seed, coalesce, loss-gap resync, owned-row visibility, store convergence) are all pinned by the real-socket tests; only the scale/heap-plateau sweep is lost, which CI never ran. * fix(coding-agent): fall back to snapshot bar counts when the roster has no direct children A client-owned session's rows are invisible to the public roster by design, so the push-fed bar showed zero while live subagents existed. The roster wins whenever it reports at least one direct child for this parent; otherwise the connection snapshots carry the counts for that render. * chore(coding-agent): second slim round — merge roster suites onto shared fixtures One store-lifecycle test now carries capability-miss, raced-push replay, hello re-keying, attach serialization, and dispose; the wire compat E2E and the chat-bar E2E share one supervisor boot (bystander, coalescing, repaint, and the owned-session snapshot fallback ride the same socket). Cut with accepted residual risk: the mid-flight park test (PR #1909 carries the recoverable-park suite), the watchdog staleness push test (label rendering stays pinned in the rows test), the view-instance anchor/failed-delete pins (one-line UI fixes, self-healing paths), the handshake-exit pin, and the latch test's stale-success ordering (generation guard predates this PR). Comment pass two drops restating docs (amend, hello key, two wire-field docs). * test(coding-agent): fold the monitor suite's roster seeding into the shared fixture * fix(coding-agent): isolate roster consumers and restamp last-heard marks after row rebuilds The store's update dispatch now isolates each listener like the connection's emit does, so one throwing consumer cannot break the others or the process. The staleness sweep restamps any still-silent worker's rows that lack the mark instead of stamping only on the first stale pass - AgentRoster.write rebuilds rows from the incoming payload and dropped it, leaving 'last heard' labels missing after any supervisor-side write until the worker recovered and went stale again; mark equality keeps repeat sweeps push-free and the sweep stays the mark's single owner. * test(coding-agent): back the flicker test's live edge with real transcript files * fix(coding-agent): gate the bar's snapshot fallback on the session's own roster presence total > 0 conflated a client-owned session with a public parent whose children all left the roster, reviving stale snapshots (roster deletions emit no cancelled event to the client) and disagreeing with the agents view. The discriminator is now the parent session's own row: fall back only while connectionState.sessionId is absent from the pushed summaries, so a public parent with zero roster children shows zero. * test(coding-agent): give the flicker test's worker its family root for the scoped reseed * fix(coding-agent): never fail a recovered session on the roster bar's subscribe A transient roster_subscribe failure inside attach() rejected the reconnect loop's otherwise-complete recovery (and the update-restart reattach), ending in a closed, unusable session because a bar accessory failed. The seam now swallows the transient class - the bar degrades and the next reconnect or rebind re-attaches through the same line - while capability-miss stays the non-fatal false it already was and the agents view keeps its own hard-require path. * fix(coding-agent): hydrate seeded roster cwd Fixes #1900 * fix(coding-agent): close roster refresh races Fixes #1900 * fix(coding-agent): finalize roster status handling Fixes #1900 * fix(coding-agent): limit rendered roster labels Fixes #1900 * fix(coding-agent): preserve unloaded saved scopes Fixes #1900 --------- Co-authored-by: Xeophon <46377542+xeophon@users.noreply.github.com>
# Conflicts: # packages/coding-agent/src/modes/agents-view/agents-view-mode.ts # packages/coding-agent/src/modes/daemon/daemon-client.ts # packages/coding-agent/src/modes/daemon/daemon-supervisor.ts # packages/coding-agent/test/agents-view-mode.test.ts
xeophon
approved these changes
Sep 1, 2026
olety
added a commit
to oneiron-dev/prime-agent
that referenced
this pull request
Sep 1, 2026
Takes upstream's event-driven supervisor roster ledger + push (PrimeIntellect-ai#1897, PrimeIntellect-ai#1900), direct TUI<->worker transport (ENG-5817), daemon startup/recovery hardening (PrimeIntellect-ai#1929, PrimeIntellect-ai#1909), single-dump kernel snapshots (PrimeIntellect-ai#1945), empty-draft eviction (PrimeIntellect-ai#1946), rlm_child_update suppression (PrimeIntellect-ai#1944), bash-skill preview (PrimeIntellect-ai#1911). Fork laws re-expressed on the roster architecture: - stable-target follow-up honesty kept (capability proof via worker hello, target_unavailable never not_found when ownership unproven) - schema revision 26 (union of fork rev-24 stable-target + upstream rev-24/25 roster+transport); digest minted by the repo's own algorithm - summary freshness reuse + single-flight + staleness + root-omission rejection restored on upstream's refresh pull - adoption/recovery never fails a live worker on a slow or root-omitting catalog: get_state root seed + stale mark + bounded background rehydration - repl.py keeps fork prune-on-aggregate-overflow - delete handlers keep fork persistence reporting; eviction fence test keeps the stronger two-worker contention variant Known test debt (deferred to post-Wave cleanup per owner): roster-era fixture migrations in daemon-supervisor-monitor (2), plus un-triaged failures in package-command-paths, agent-session-recursion, daemon-runtime-stress, 4600-supervisor-singleton, 4603-worker-recovery, 4606-update-restart- coordinator, agents-view-roster. Production laws preserved; failures are fixture-era artifacts or mechanism assertions to rewrite.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1905. Linear: ENG-5811.
Summary
DaemonAgentConnectionreconnect loop and the update-restart restore path awaited their ownattach/getInitialSnapshot/listrequests with request recovery enabled: a socket close during those awaits cleared the request timeout and parked the request asawaitingReconnect, to be resent after the nextdaemon_hello— but that hello requiresclient.connect(), which requires the stuck loop to advance: a circular wait that never settles (the close callback dedups into the same stuck promise)recoverable: falseoption (introduced forroster_subscribein feat(coding-agent): roster subscription push consumed by the agents view and subagents bar #1900): a mid-flight close rejects into the loop's own catch/retry, which resets the transport and reconnectsValidation
DaemonClient, no client-internal mocks): both hang to timeout with the fix reverted and pass in ~0.6s with itrecoverable: falseper site) each killeda1870b6d5: failing-name lists byte-identical (82 pre-existing environmental failures both sides); passed delta exactly the 2 new teststsgo --noEmitandbiome check --error-on-warningscleanStacked on #1900 (
feat/agent-roster-push), which introduces therecoverablerequest option; retarget/rebase onto main after it merges.Note
Medium Risk
Touches daemon reconnect and post-update restore paths; incorrect recoverable flags could regress transient disconnect handling, but the change is narrowly scoped to loops that already own retries.
Overview
Fixes a reconnect deadlock (#1905) where a daemon socket drop during supervisor recovery or post-update session restore could leave
attach, snapshot fetches, orlistparked inawaitingReconnectuntil the nextdaemon_hello—which only the same stuck retry loop could obtain.DaemonAgentConnectionnow threads an optional{ recoverable?: boolean }throughattach()andgetInitialSnapshot()intorequestData, and the boundedreconnectandrestoreConnectionAfterUpdateloops call those paths (andlist) withrecoverable: falseso a mid-flight close rejects into the loop’s catch/retry instead of parking. Default recoverable behavior is unchanged for one-off attach/snapshot callers.DaemonClientdocs clarify that any caller with its own retry loop must opt out of parking. Two unix-socket regression tests cover recovery after cuts mid-attach/snapshot and mid-update-restore.Reviewed by Cursor Bugbot for commit d885351. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add agent roster push protocol and reject parked reconnect-loop requests
agent_rostercapability (protocol revision 24): the supervisor maintains a liveAgentRosterseeded from disk and the spawn ledger, workers emitroster_deltaandroster_heartbeatframes, and clients subscribe viaroster_subscribe/roster_unsubscribeto receive incrementalroster_updatepushes instead of polling.AgentsViewRosterStorebacked by the persistent daemon client, removing live-catalog polling timers and deferring saved-session fetches until search text is present.agent_rostercapability are detected at auth viaPreRosterWorkerError; the supervisor attempts to restart them throughrestartPreRosterWorker, otherwise marks themfailed.tombstoneSavedSessionDeletein rlm-ledger.ts to pre-tombstone spawn-ledger edges before file deletion, preventing resurrected children from getting incorrect lineage.liveEdgesnow filters to edges whose parent and child transcripts still exist, keeping orphaned children as roots.{ recoverable: false }are rejected on socket close instead of parked behind a stuck hello, so the reconnect loop can complete resync.roster_subscribe,roster_unsubscribe, androster_updaterequire protocol 7 withagent_rostercapability.SessionSummarygains additive fields (rosterStatus,statusLabel,lastHeardFromAt) validated byisDaemonSessionSummaryfor backward compat. Pre-roster daemons/workers will be restarted or markedfailedrather than served from legacy summary caches.Macroscope summarized d885351.