Skip to content

fix(coding-agent): reject reconnect-loop requests on socket close instead of parking them - #1909

Merged
xeophon merged 96 commits into
mainfrom
fix/daemon-client-reconnect-recoverable
Sep 1, 2026
Merged

fix(coding-agent): reject reconnect-loop requests on socket close instead of parking them#1909
xeophon merged 96 commits into
mainfrom
fix/daemon-client-reconnect-recoverable

Conversation

@snimu

@snimu snimu commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Fixes #1905. Linear: ENG-5811.

Summary

  • the DaemonAgentConnection reconnect loop and the update-restart restore path awaited their own attach / getInitialSnapshot / list requests with request recovery enabled: a socket close during those awaits cleared the request timeout and parked the request as awaitingReconnect, to be resent after the next daemon_hello — but that hello requires client.connect(), which requires the stuck loop to advance: a circular wait that never settles (the close callback dedups into the same stuck promise)
  • all five park sites now send their requests with the existing recoverable: false option (introduced for roster_subscribe in 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 reconnects
  • no new mechanisms: park-and-replay remains the default for requests whose re-issue is not already owned by a bounded retry loop (static attach, external snapshot callers)

Validation

  • two real-unix-socket regression tests (scripted daemon, real DaemonClient, no client-internal mocks): both hang to timeout with the fix reverted and pass in ~0.6s with it
  • five mutants (drop recoverable: false per site) each killed
  • full coding-agent suite A/B vs base a1870b6d5: failing-name lists byte-identical (82 pre-existing environmental failures both sides); passed delta exactly the 2 new tests
  • tsgo --noEmit and biome check --error-on-warnings clean

Stacked on #1900 (feat/agent-roster-push), which introduces the recoverable request 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, or list parked in awaitingReconnect until the next daemon_hello—which only the same stuck retry loop could obtain.

DaemonAgentConnection now threads an optional { recoverable?: boolean } through attach() and getInitialSnapshot() into requestData, and the bounded reconnect and restoreConnectionAfterUpdate loops call those paths (and list) with recoverable: false so 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.

DaemonClient docs 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

  • Introduces a daemon-wide agent_roster capability (protocol revision 24): the supervisor maintains a live AgentRoster seeded from disk and the spawn ledger, workers emit roster_delta and roster_heartbeat frames, and clients subscribe via roster_subscribe/roster_unsubscribe to receive incremental roster_update pushes instead of polling.
  • Reworks agents-view-mode.ts and interactive-mode.ts to consume a shared AgentsViewRosterStore backed by the persistent daemon client, removing live-catalog polling timers and deferring saved-session fetches until search text is present.
  • Workers without the agent_roster capability are detected at auth via PreRosterWorkerError; the supervisor attempts to restart them through restartPreRosterWorker, otherwise marks them failed.
  • Adds tombstoneSavedSessionDelete in rlm-ledger.ts to pre-tombstone spawn-ledger edges before file deletion, preventing resurrected children from getting incorrect lineage. liveEdges now filters to edges whose parent and child transcripts still exist, keeping orphaned children as roots.
  • Fixes reconnect deadlocks in daemon-client.ts and daemon-agent-connection.ts: requests marked { recoverable: false } are rejected on socket close instead of parked behind a stuck hello, so the reconnect loop can complete resync.
  • Risk: protocol revision bumped from 23 to 24; roster_subscribe, roster_unsubscribe, and roster_update require protocol 7 with agent_roster capability. SessionSummary gains additive fields (rosterStatus, statusLabel, lastHeardFromAt) validated by isDaemonSessionSummary for backward compat. Pre-roster daemons/workers will be restarted or marked failed rather than served from legacy summary caches.

Macroscope summarized d885351.

@snimu
snimu force-pushed the feat/agent-roster-push branch from a1870b6 to d7e0973 Compare August 31, 2026 15:37
@snimu
snimu force-pushed the fix/daemon-client-reconnect-recoverable branch from 8d17672 to fdb0bbc Compare August 31, 2026 15:39
@snimu
snimu force-pushed the feat/agent-roster-push branch from d7e0973 to db18389 Compare August 31, 2026 16:04
@snimu
snimu force-pushed the fix/daemon-client-reconnect-recoverable branch from fdb0bbc to 707c34a Compare August 31, 2026 16:04
@snimu
snimu force-pushed the feat/agent-roster-push branch from db18389 to 32f3abc Compare August 31, 2026 16:08
@snimu
snimu force-pushed the fix/daemon-client-reconnect-recoverable branch from 707c34a to aaabe6c Compare August 31, 2026 16:08
@snimu
snimu force-pushed the feat/agent-roster-push branch from 32f3abc to 4a961d6 Compare August 31, 2026 16:53
@snimu
snimu force-pushed the fix/daemon-client-reconnect-recoverable branch from aaabe6c to 5382bd5 Compare August 31, 2026 16:53
@snimu
snimu force-pushed the feat/agent-roster-push branch from 4a961d6 to a2744ff Compare August 31, 2026 17:34
@snimu
snimu force-pushed the fix/daemon-client-reconnect-recoverable branch from 5382bd5 to e4b0aea Compare August 31, 2026 17:34
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
snimu force-pushed the feat/agent-roster-push branch from a2744ff to 7f1eccd Compare August 31, 2026 18:22
@snimu
snimu force-pushed the fix/daemon-client-reconnect-recoverable branch from e4b0aea to 024e834 Compare August 31, 2026 18:22
@snimu
snimu force-pushed the feat/agent-roster-push branch from 7f1eccd to d607820 Compare August 31, 2026 18:51
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
snimu force-pushed the fix/daemon-client-reconnect-recoverable branch from 024e834 to e8528f3 Compare August 31, 2026 18:51
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
snimu force-pushed the feat/agent-roster-push branch from d607820 to 5457338 Compare August 31, 2026 19:15
@snimu
snimu force-pushed the fix/daemon-client-reconnect-recoverable branch from e8528f3 to 15c5075 Compare August 31, 2026 19:15
@snimu
snimu requested review from sethkarten and xeophon August 31, 2026 19:35
xeophon
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
snimu added 11 commits September 1, 2026 10:31
…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.
@snimu
snimu dismissed xeophon’s stale review September 1, 2026 08:34

The merge-base changed after approval.

@snimu
snimu force-pushed the feat/agent-roster-push branch from 0c490a5 to cd675e7 Compare September 1, 2026 08:34
@snimu
snimu force-pushed the fix/daemon-client-reconnect-recoverable branch from aa2a4bf to 7c1c6d8 Compare September 1, 2026 08:34
… 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
snimu force-pushed the fix/daemon-client-reconnect-recoverable branch from 7c1c6d8 to ae22984 Compare September 1, 2026 08:48
@snimu
snimu requested a review from xeophon September 1, 2026 08:59
xeophon
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>
Base automatically changed from feat/agent-roster-push to main September 1, 2026 09:51
@xeophon
xeophon dismissed their stale review September 1, 2026 09:51

The base branch was changed.

# 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
xeophon merged commit 15ef456 into main Sep 1, 2026
22 checks passed
@xeophon
xeophon deleted the fix/daemon-client-reconnect-recoverable branch September 1, 2026 09:59
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Daemon client request recovery can deadlock the connection reconnect loop (attach/getInitialSnapshot park behind their own loop)

2 participants