chore: merge Prime upstream v0.9.4 into pylon - #55
Merged
Merged
Conversation
…rimeIntellect-ai#1893) * feat(coding-agent): render Mermaid code blocks as inline Unicode diagrams Ports pi-mono's Mermaid rendering (grok-mermaid) into prime-agent: - pi-tui Markdown gains an options.transform hook that rewrites source Markdown with the exact content width before parsing (ported from upstream pi-tui). - New mermaid.ts transform replaces top-level mermaid code blocks with themed Unicode diagrams, falls back to the raw block when the diagram is wider than the message, and appends a themed warning line for partially rendered final diagrams. - AssistantMessageComponent applies the transform to text blocks only (never thinking blocks) and tracks streaming state so the streaming-to-final transition re-renders. - New markdown.mermaid setting (off/final/streaming, default streaming) with a settings-selector entry. Linear: ENG-5673 * test(coding-agent): kill mermaid mutation survivors Adversarial review mutation testing found three surviving mutants: - width guard flipped to >= (exact-fit diagrams would fall back) - codeSpan fence reduced to a single backtick (backtick labels break) - isStreaming inverted at the message_end call site (final mode never renders) Adds an exact-width boundary test, a backtick-label test, and a handleEvent-level final-mode transition test that kill them. * build(coding-agent): pin grok-mermaid exactly for renderer-output stability * test(coding-agent): collapse redundant mermaid coverage * docs(coding-agent): tighten mermaid comments * fix(coding-agent): tolerate non-object markdown settings values Normalize a malformed markdown settings value at the migration boundary (mirroring the telemetry handling) so changing the Mermaid mode cannot throw, and add the missing tui changelog fragment. * test(coding-agent): drop mermaid.test.ts * chore(coding-agent): one-line mermaid codeSpan comments
…ns (PrimeIntellect-ai#1920) * fix(coding-agent): report an empty resident session as idle instead of pinning it at working (ENG-5809) * feat(coding-agent): evict an empty unnamed session's worker when its last client disconnects (ENG-5809) * test(coding-agent): kill surviving empty-session eviction mutants (ENG-5809 review) Adversarial-review killer tests for three mutants that survived the shipped suite: dropping the heartbeat/cron registration checks from isEvictableEmptySessionSummary, dropping the client-owned worker exclusion, and removing the post-refresh identity/stopping re-check in evictEmptySessionOnLastDetach. * chore(coding-agent): fold empty-session tests and trim comments * fix(coding-agent): drain admitted mutations before deciding empty-session eviction (ENG-5809 Macroscope) * chore(coding-agent): drop comments that restate the code * fix(coding-agent): share one eviction-fence acquisition between the idle sweep and detach eviction (ENG-5809 review) * refactor(coding-agent): extract the shared fenced passivation primitive (ENG-5809)
…t launch a worker (PrimeIntellect-ai#1918) * fix(coding-agent): surface worker spawn failures (EMFILE) as clean create errors When the daemon hits its fd limit, spawn fails with EMFILE and leaves child.stdio undefined; launchWorker crashed with a raw TypeError that masked the real error and reached the client as an unhandled throw with a stack dump. - daemon-supervisor: race the child spawn/error events so a failed spawn deterministically fails the create with the real spawn error, enriched with the resident worker count and a ulimit hint for EMFILE/ENFILE. - CLI: wrap generic daemon create failures in DaemonSessionCreateError and handle it at both createDaemonClientConnection call-site boundaries as a one-line error with exit code 1. * test(coding-agent): assert spawn-failure hint is EMFILE/ENFILE-only (kills errno-unconditional-hint mutant) * chore(coding-agent): trim comments in the spawn-failure change
…ellect-ai#1927) * fix(ai): cache Anthropic tool results * docs(ai): add cache fix changelog fragment * test(ai): trim Anthropic cache regression * test(ai): use Haiku cache fixture
…ibility, subagents bar rename (PrimeIntellect-ai#1895) * feat(coding-agent): shared agent-status classifier, honest worker visibility, subagents bar rename Add classifyAgentStatus as the one status formula for every agent surface; classifySessionRosterStatus (agents view) and the new classifySubagentSnapshotStatus (subagents bar) are thin adapters over it, so the two surfaces can no longer diverge on what running/idle/inactive mean. Visibility fixes: - Sessions of non-ready workers are no longer hidden from the agents view; the worker state (starting/recovering/stopping/failed) renders as the row's status label instead. - A resident message-less subagent session is lifecycle "live" (it is a spawned worker, not a user draft); top-level drafts stay hidden. - Subagent summary bar label renamed from "agents" to "subagents". Part 1 of 3 for the event-driven daemon-owned agent roster. ENG-5794 * fix(coding-agent): drop the redundant roster-status alias and add the changelog fragment Addresses PR PrimeIntellect-ai#1895 review: SessionRosterStatus was an alias with no external importers, and the visibility comment restated the code. ENG-5794 * refactor(coding-agent): derive subagents bar border fill from the label width * test(coding-agent): fold the classifier pins into one compact contract test
… from one ledger (PrimeIntellect-ai#1897) * 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.
…iew and subagents bar (PrimeIntellect-ai#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>
…tead of parking them (PrimeIntellect-ai#1909) * 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): reject reconnect-loop requests on socket close instead 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. * docs(coding-agent): state the loop-ownership rule on the recoverable request option * docs(coding-agent): add the reconnect-park changelog fragment --------- Co-authored-by: Xeophon <46377542+xeophon@users.noreply.github.com>
…cells (PrimeIntellect-ai#1911) * fix(coding-agent): preview bash-skill calls with literal commands as bash In the collapsed ipython cell line, python cells like r = await bash('git status') now render as bash · git status instead of the python wrapper (which redactNoise erased entirely for commands >=160 chars). Literal-first-arg bash() calls on the scorer-chosen line are routed through previewBashCommand, matching %%bash cells. Non-literal arguments keep the python preview. ENG-5802 * test(coding-agent): kill guard-drop mutant in bash-skill preview extraction previewIpythonCode must keep the python preview when the quoted literal is not the whole first argument (string concatenation, escaped quotes). The existing tests let a mutant that drops the comma/paren-after-close- quote guard survive; this pins the fallback behavior. * fix(coding-agent): reject escaped-quote mis-cuts in bash-skill extraction * fix(coding-agent): evaluate bash-skill literals and gate extraction inside strings Replace the regex-tail + indexOf extraction with a small python string-literal scanner: backslash consumes the next char (raw-string rule included), cooked strings unescape standard sequences, and an unterminated scan falls back to the python preview. The preview now shows the evaluated command (real newlines, unescaped quotes) instead of raw source text, and triple-quoted literals with escaped quotes can no longer be mis-cut. Reuse the scanner to detect when the scorer's chosen line sits inside an unterminated triple-quoted string opened earlier; skip bash-skill extraction there so string text like a docstring never previews as a bash command that did not run. * test(coding-agent): kill scanner and gate mutants in bash-skill extraction Killer tests for three surviving mutants: raw-string backslash handling (raw close at backslash-quote), the closed-and-reopened multiline gate state, and unclosed-literal extraction of a partial value. * fix(coding-agent): fall back on value-changing escapes in bash-skill literals * fix(coding-agent): redact authorization headers in previews; tighten tests and comments
…ENG-5817) (PrimeIntellect-ai#1926) * feat(coding-agent): protocol groundwork for direct worker peer transport Schema revision 25 adds the capability-gated get_direct_worker_transport command, the single-use DaemonPeerTransportTicket shape, and the peer grant, peer_auth, and worker_register_peer_transport wire types. DAEMON_COMMAND_PLANE classifies every daemon command as session- or control-plane at compile time so unclassified commands can never route to a direct transport, and the per-command hello compatibility predicate moves to daemon-protocol.ts so the routed client and DaemonClient share one check. Worker descriptors gain the per-incarnation workerInstanceId that peer grants bind to. * feat(coding-agent): worker-side direct peer transport admission Workers accept supervisor-registered single-use peer grants (worker_register_peer_transport; bounded, TTL-capped, burned before their token is compared with sha256 + timingSafeEqual) and authenticate direct session clients via peer_auth on their own socket. A pre-auth socket may only authenticate; a failed peer_auth or any other command ends it. Authenticated peers are pinned to the session plane of their granted session, so worker control commands, other sessions, and unknown command types are rejected. Grants and live peers are fenced during update preparation, archive shutdown, and worker shutdown; compact assistant deltas stay a supervisor-only encoding. Session summaries report directAttachedClients so the supervisor roster, attachment counts, and idle eviction can stay honest. * feat(coding-agent): supervisor issues direct worker transport tickets get_direct_worker_transport asks the supervisor for a single-use ticket to a resident worker's own socket. The supervisor pre-registers a 10s grant in the worker over its authenticated channel, binds it to the fresh per-incarnation workerInstanceId (launched via env, echoed in worker_auth, persisted on the descriptor), verifies the worker process identity is current, and stamps the socket's dev/ino into the ticket so clients can detect a swapped socket. Workers advertise peer-transport support in their worker_auth capabilities; older workers never get grants and client-owned workers are refused at issue time. Public summaries and idle-eviction snapshots add worker-reported direct attachments to the supervisor-side count so eviction and the roster stay honest. * feat(coding-agent): route session commands over a direct worker transport DaemonAgentConnection.attach upgrades its supervisor connection with a direct link to the session's worker when the supervisor advertises direct_peer_transport: it redeems a single-use ticket, verifies the socket's dev/ino, and wraps both sockets in a DaemonRoutedClient. Session-plane commands the worker's own hello serves go direct; control-plane commands, an old worker, or any direct-path failure fall back silently to supervisor routing, and a malformed direct frame closes the link instead of throwing into the client. An established direct link outlives supervisor-socket loss: the session keeps streaming while the control plane reconnects in the background. Watchers and owned sessions stay on the shared supervisor socket, and a successful reattach drops the now-stale direct link. * docs(coding-agent): add the direct session transport changelog fragment * fix(coding-agent): schedule supervisor recovery from the socket role, not the live claim The fence check revokes a stale supervisor claim before ending its socket, so the disconnect cleanup can no longer consult supervisorClaims to decide whether to probe supervisor availability; the authentication role survives revocation and excludes direct session peers. Also drop the redundant shuttingDown clause from peer_auth (shutdown already fences admissions and clears grants) and teach the affected daemon fakes the fields the transport now touches. * fix(coding-agent): keep reasoned closes authoritative on the direct transport Review fixes for the direct session transport: - Peer fencing now writes daemon_closing with the real reason before ending direct sockets, and the archive shutdown closes sessions first so direct peers read session_closed "killed" instead of a fake daemon shutdown; a clean daemon stop therefore stays terminal and never respawns the daemon. - The connection checks the authoritative shutdown/update close reason before the surviving-direct-link reroute, so reasoned closes stay terminal (stop) or take the update restoration path (restart) even while the direct link is healthy. - worker_auth enforces the instance binding only when the authenticating supervisor presents one, so a downgraded supervisor still adopts live workers; the peer-transport fencing between current builds is unchanged. - One helper owns the direct-plus-supervisor attachment sum; peer_auth drops the same-process re-check of the worker's own instance id (registration is the single enforcement point; the client-presented field stays checked) and the peer command gate drops the updateRestart clause that fencing makes unreachable. * chore(coding-agent): slim direct-transport comments to genuine ambiguity Drop comments that narrate the adjacent code and squeeze multi-line docs to one line, keeping the load-bearing notes: grant burn-before-compare, the archive close-before-fence ordering, the reason-before-FIN contract, the fail-fast rule on dropped request options, the shutdown-window fence reachability, and the command-plane classification rule. * fix(coding-agent): resync only when a reconnect re-established session state Control-plane recovery with a healthy direct link completed a reconnect cycle per supervisor-socket flap (attach rides the direct link, so every cycle succeeded instantly) and each cycle emitted session_resynced, re-rendering an unchanged transcript up to 15 times during one supervisor replacement. The reconnect loop now emits session_resynced only when the session transport itself was re-established through a supervisor attach; a held direct link streamed state throughout, so recovery emits just the connected status. * fix(coding-agent): one close-handler owner; direct loss always falls back A direct-transport loss is never itself a session loss: the connection now reconnects through a supervisor re-attach even without a recoverDaemon hook (the supervisor socket is still healthy; a worker that actually died keeps signalling through the authoritative session_closed path). The transport close handler is extracted into one method and the control-plane close saved during an initial direct attach replays through it, so a daemon shutdown during initial attach stays terminal instead of respawning the daemon. * fix(coding-agent): flush the roster when direct viewers attach or detach A direct peer joining or leaving an idle session changes directAttachedClients without any session event to carry it, so the supervisor's roster (and roster-based idle eviction) held a stale count until unrelated activity. The attach handler and the shared detach path now schedule a roster flush for session_client sockets; supervisor-relayed viewers keep their existing event-driven cadence. * test(coding-agent): supervisor-swap E2E expects no resync while the direct link holds The resident-worker restart test predates the direct transport: its client now rides the worker's own socket through the supervisor swap, so the recovery emits connected without a session_resynced. The end-to-end pin now asserts the new semantics (no resync, no close, state queryable), matching the unit pins from the resync-emission fix. * fix(coding-agent): reject the attach when a shutdown lands during initial direct attach Replaying a saved supervisor-shutdown close through the close handler inside the static attach emitted the terminal event before any listener could subscribe, and terminalCloseEmitted then swallowed every later signal: a daemon stop during first attach produced a zombie connected TUI. No listener can exist during construction, so a terminal saved close now rejects the attach with the socket-close error — the pre-transport semantics every call site already handles; non-terminal saved closes keep replaying through the single close handler. * fix(coding-agent): compose pause invalidation with direct-loss fallback; re-acquire direct after reattach A direct-socket drop while a session input pause was held hit the fail-closed pause branch before the recoverable-direct-loss ordering, terminalizing a session the supervisor could still serve and skipping update restoration. The fence stays fail-closed only for control-plane losses; a lost direct link clears it (holders still learn through the invalidation generation) and falls through to the normal close ordering. A successful in-window reattach dropped the stale direct link but never acquired one for the target, leaving the session supervisor-routed for good. The routed client can now upgrade itself with the same silent ticket dance as the initial attach (one shared acquisition helper), and reattachSession invokes it on success only — a rejected reattach still keeps the old link. * fix(coding-agent): revert the reattach transport upgrade; never park the ticket request The post-reattach upgradeDirectTransport was half a mechanism: its own pin showed no attach ever crosses the new link, so events stayed on the supervisor relay while requests silently switched sockets, and supervisor attachment bookkeeping leaked. Post-reattach supervisor routing is the honest state until a designed-as-one-piece re-upgrade exists. With its only other caller gone, the ticket acquisition folds back into createDaemonSessionTransport, and the ticket request now opts out of reconnect parking (recoverable:false) so a supervisor drop mid-acquire fails into supervisor routing instead of pending the attach forever. * fix(coding-agent): held-direct recovery is control-plane only and unbounded While the direct link is held, the reconnect loop's success is connect plus hello: no re-attach and no snapshot cross a socket that never stopped serving the session, which deletes the snapshot-restream-over-live-socket failure class and makes the resync skip structural. The bounded reconnect deadline arms only once the direct link is gone (re-armed at the transition), so a supervisor staying down never terminally closes a healthy direct stream; a mid-recovery direct death reruns the loop as a normal bounded session-plane reconnect. * docs(coding-agent): state the peer grant's scope and threat model on its type The session pin is scoped to the active-session slot and guards against routing accidents, not privilege escalation: ticket holders already hold supervisor access. * fix(coding-agent): rebind the roster subscription during held-direct recovery Control-plane-only recovery skips attach(), which was the roster store's only rebind seam, so a supervisor blip under a healthy direct link left the agents view subscribed to the dead hello. The subscription is a control-plane accessory: held-direct recovery now re-attaches the store non-fatally after connect + hello, and the session-plane path keeps its single rebind inside attach(). * chore(coding-agent): slim transport comments and one subsumed test assert Drop docs that restate their declarations (transport interface, role union), the close-handler narration the predicate name now carries, and squeeze three multi-line whys to one line each. The half-open handshake test loses its zero-resync assert: the deadline-policy pin and the supervisor-swap E2E both pin held-mode zero-resync across the whole recovery. * fix(coding-agent): unparkable fallback attach; one liveness check after the held-recovery awaits The supervisor-side retry of a failed initial direct attach owns its bounded outcome, so it now opts out of reconnect parking and, on failure, rejects with the control-plane close saved during the window — a daemon stop racing the retry reaches the caller as the authoritative reason instead of a pend or a generic socket error. Held-direct recovery revalidates once after its last await (the roster re-attach): a terminal close inside the window stops the loop without emitting connected, and a direct loss inside it falls through to the bounded session-plane rerun instead of joining the finished recovery. * fix(coding-agent): held recovery stands down for an in-flight update restoration The post-await validation only re-derived two of the close handler's three outcomes, so an update close landing inside the held roster re-attach fell into the bounded session-plane rerun, whose deadline could close the client under the running restoration. The check now consults the handler's own dispatch outputs — terminalCloseEmitted, updateRestartPending, or the recoverable close that joined this loop — so a window close lands in exactly the branch the handler chose.
…eIntellect-ai#1929) * fix(coding-agent): harden daemon recovery and socket ownership * fix(coding-agent): address daemon recovery review findings * fix(coding-agent): preserve prompt admission scheduling * test(coding-agent): consolidate daemon recovery coverage * fix(coding-agent): preserve safe shutdown outcomes * fix(coding-agent): close recovery admission gaps * refactor(coding-agent): one process-identity oracle for recovery verdicts Adoption and recovery classify liveness through processIdentity() instead of re-pairing isProcessAlive+getProcessStartId at each site; raw start-id reads remain only where the value itself is persisted. The sole surviving kill of a live worker is the identity-verified pre-roster replacement, now pinned: adoption of a live pre-roster worker still kills-and-relaunches, while an unverifiable identity still parks failed with no replacement. * fix(coding-agent): bound live-worker probing at ten defer rounds A hung-but-alive worker (or a live pid with no verifiable start id) probed forever: ~11s probe pass, 5s defer, repeat. After MAX_DEFERRED_RECOVERY_ROUNDS (10 rounds, ~2.5 minutes) the worker parks failed — user-visible through the existing roster failed status — with its process left alive for a manual retry_worker, which resets the round count like any successful recovery. * refactor(coding-agent): type the worker probe timeout isDaemonWorkerProbeTimeout classified by message prefixes defined in two other files. The timeout sites in daemon-worker-client.ts (hello, response) and the supervisor's connect deadline now throw DaemonWorkerProbeTimeoutError, and the classifier is a plain instanceof — one truth, same shape as DaemonWorkerAuthenticationError. * docs(coding-agent): state the leak-over-kill recovery tradeoff The recovery SIGKILL removal applies to every failure class, not just probe timeouts. Verified: a live verified-identity failed worker IS reclaimed by the next fresh create (graceful stop), so the true leak is only the unverifiable survivor — the deliberate fail-safe. One comment at the decision point and a changelog line make the tradeoff explicit. * fix(coding-agent): kill before cleanup in the pre-roster replacement The carve ran recoverUncertainWorkerOperations (interruption marking, orphan reaping) before the identity-gated SIGKILL, so destructive cleanup hit a still-active worker; pre-rebase the kill preceded cleanup inside the old helper. Reordered: recheck + SIGKILL + bounded teardown wait first, cleanup only after the predecessor is confirmed gone/replaced, and an unverifiable survivor parks failed with NO destructive cleanup at all (matching recovery's rule that a possibly-live worker is never cleaned destructively). The carve pin asserts the order and the no-cleanup branch. * fix(coding-agent): never release a registry guard a successor reclaimed proper-lockfile verified: compromise detection is timer-driven (mtime updates), and release() removes the lockfile unconditionally when the steal has not been detected yet — so a caller resuming from a stall past the stale threshold could delete the successor'"'"'s lock. The guard directory'"'"'s inode is the ownership identity (a steal is rmdir+mkdir): it is captured at acquisition and checked synchronously where the timer cannot run — before returning an action'"'"'s result and before release. A stolen-but-undetected guard is left to its own updater, which notices the foreign mtime and cleans itself. A truly synchronous stall still cannot be preempted mid-action; that limitation is stated at the guard. --------- Co-authored-by: Sebastian <sebastian@primeintellect.ai>
* fix(coding-agent): single-dump kernel snapshots (ENG-5819) The kernel snapshot pipeline serialized the aggregate payload dict into an in-memory buffer, copied it out with getvalue(), and only then wrote the file - on top of the per-variable blob dict, a ~3.9x-payload transient memory spike on every debounced snapshot (measured 963MB overhead for a 250MB namespace). The aggregate dict now pickles straight into the staged temp file through a size-capped pass-through writer (measured 253MB overhead, ~1x payload); the strict max_bytes contract and the largest-fitting-prefix fallback are preserved by rewinding the temp file between prefix attempts. _SnapshotBuffer is replaced by the same _CappedWriter over a plain BytesIO for the per-variable dumps. * chore(coding-agent): strip narrative comments from the snapshot dump path
…aches (PrimeIntellect-ai#1946) Direct-transport clients attach and detach on the worker socket, so the supervisor-socket cleanup that owns empty-session eviction never saw their last detach and empty drafts lingered until an idle sweep. The worker already reports the drop: its peer attach/detach roster flush delivers a summary whose directAttachedClients falls to zero, so the supervisor's roster write funnel now triggers the existing last-detach eviction on that transition — covering both clean detach and unclean socket drop, which share the worker's detach path. The eviction candidate check counts direct viewers through the shared attachment sum, so a remaining direct client blocks a routed client's last detach and vice versa. Fixes ENG-5831.
…snapshot (PrimeIntellect-ai#1944) * fix(coding-agent): suppress rlm_child_update emits with an unchanged snapshot Child streaming re-emits the parent's child snapshot on every assistant delta; once the answer preview caps and activity is steady, the repeats are byte-identical and only add wire and render noise for every attached client. Emit only when the serialized snapshot actually changed. * fix(coding-agent): bound the kernel stderr diagnostic buffer to a tail kernelStderr accumulated for the kernel's lifetime while every reader takes at most the last 1 KiB; keep only an 8 KiB tail. * chore(coding-agent): drop narrating comments from the relay-hygiene diff * revert(coding-agent): move the kernel stderr bound out to its own PR
…i#1631) * fix(coding-agent): replace TUI process after update * fix(coding-agent): guard unsafe execve failures * fix(coding-agent): restore the previous cwd when a thrown execve falls back The relaunch chdirs before replacing the process; a thrown execve (Node >=26.1) left the old process on the update cwd while the child fallback ran. The seam now restores the captured previous cwd on the throw path so a failed attempt leaves no silent process-state difference. * docs(coding-agent): qualify the update-relaunch changelog with the platform restriction --------- Co-authored-by: Sebastian <sebastian@primeintellect.ai>
… lines (PrimeIntellect-ai#1948) * fix(coding-agent): show the expand hint on sent agent-message summary lines * test(coding-agent): drop the exact-line sent-message rendering test
…ct-ai#1955) * revert(coding-agent): restore subagent cleanup guidance Reverts PrimeIntellect-ai#1952 so agents delete direct subagents when they are no longer needed instead of retaining them until a user explicitly requests deletion. Adds prompt coverage for the intended cleanup policy. * test(coding-agent): keep prompt rollback free of copy assertions
…ies without changing list --all (PrimeIntellect-ai#1951) * fix(coding-agent): scope the roster seed to registered workers' families without changing list --all Reworks the closed PrimeIntellect-ai#1941: the boot seed now covers only registered workers' descendant families (catalog rows stay catalog-owned), but subagent rows of families with no registered worker are still served by list --all, read on demand from the spawn ledger with the same fields, statuses, and liveEdges ordering as the seeded rows had. The sessionDir ancestry walk falls back to ledger edges when the ancestor rows are not roster-resident. * fix(coding-agent): hydrate on-demand ledger list rows sequentially The dead-family list --all path fanned out one concurrent transcript read per unseeded ledger child (Promise.all), a shape the boot seed deliberately avoids; a large dead-family ledger could exhaust file descriptors. Hydrate inside the edge loop instead, one read at a time, matching seedRosterLedger.
…or a doomed kernel snapshot (PrimeIntellect-ai#1954) * feat(coding-agent): make the final trace flush detachable and the kernel snapshot optional on dispose AgentSessionRuntime.dispose() and AgentSession.disposeAsync() now take options: traceFlush "detach" fires the final trace upload without awaiting it (failures land in agent-traces.log), and kernelSnapshot:false skips the kernel's final snapshot at the provisioner shutdown boundary. Defaults keep today's blocking flush and snapshot on every caller. Session replacement (new/resume/fork teardown) detaches the flush: the outgoing session file and the process both outlive the swap. * fix(coding-agent): stop RLM subagent deletion from awaiting the trace upload and writing a doomed kernel snapshot Deleting a resident child blocked on the full session trace upload (15s timeout x3 retries, and a flat 60s sleep on HTTP 429 without retry-after) and wrote a final kernel snapshot that the artifact sweep removed milliseconds later. The daemon now detaches the flush on every close that the daemon and the session file outlive, and the delete paths dispose the kernel with snapshot:false. Daemon shutdown, update restarts, and worker archive-and-shutdown still await the flush; passivation still writes the snapshot. Measured on a resident child with a pending upload: 3s-network flush 3024ms -> 10ms; 429 without retry-after 60120ms -> 9ms. Linear: ENG-5837 * refactor(coding-agent): replace per-close trace-flush policy with an exit barrier Review round: disposal policy did not propagate through nested closes (hosted children), the closingSessions join (delete inheriting a passivation close's awaited flush; exit joins never upgrading an in-flight detached close), or startup-abort kernel teardown. Instead of threading policy through every close, disposal now never awaits the trace upload; the four exit owners (daemon shutdown, update restart, worker archive-and-shutdown, in-process connection dispose) drain all scheduled and in-flight uploads through one barrier (flushAllPendingAgentTraceUploads). This makes the nested/concurrent propagation bugs structurally impossible. kernelSnapshot:false stays threaded from the delete paths and now also covers a dispose that aborts a kernel startup in flight. * fix(coding-agent): run the trace-upload exit barrier even when teardown throws The in-process connection dispose and the daemon shutdown loop exit regardless of a thrown teardown (print/acp swallow the dispose error and exit; a rejected shutdown() is an unhandled rejection), so the barrier moves into a finally. The teardown error still propagates. The other two owners (update-restart commit, worker archive-and-shutdown) keep the daemon alive on a throw, so their detached uploads finish on their own.
* chore: prepare v0.9.0 release * chore: add missing list marker to a 0.9.0 changelog entry
…rimeIntellect-ai#1960) * fix(coding-agent): load the saved catalog when the agents view opens v0.9.0 regression: the Inactive section was empty on a fresh agents view until a search query was typed. The saved catalog was loaded only for search (a PrimeIntellect-ai#1900 optimization premised on the roster boot seed carrying the saved corpus as inactive rows); PrimeIntellect-ai#1951 scoped that seed to live families, so the view must load what it displays. The load stays progressive and once-per-view. * docs: tighten the catalog-load comment
* ci: prefer Research tickets for Prime Agent * ci: require RES tickets or explicit opt-out
….x (PrimeIntellect-ai#1993) * fix(ai): bump impersonated Claude Code version to 2.1.257 for Fable 5.x The Anthropic API now gates Fable 5.x models on Claude Code >= 2.1.251 and rejects the previously pinned 2.1.75 identity for OAuth requests. Fixes PrimeIntellect-ai#1962 * chore(ai): drop the identity-header test and rationale comment per review
…ct-ai#1992) * fix(installer): support npm 12 remote dependency policy * fix(installer): allow verified npm 12 postinstall * refactor(installer): simplify npm 12 coverage * refactor(installer): consolidate installer checks
PrimeIntellect-ai#2002) * fix: register ACP MCP tools as native callable tools via cpython proxy * fix: add MCP proxy tool names to allowlist so model can see them * fix: add missing details field to ACP MCP tool execute results * fix(coding-agent): harden ACP MCP proxy lifecycle Fixes PrimeIntellect-ai#2002 * fix(coding-agent): reject MCP without cpython Fixes PrimeIntellect-ai#2002 * refactor(coding-agent): narrow generic MCP accessor Fixes PrimeIntellect-ai#2002 * fix(coding-agent): cap ACP MCP server names so composed tool names fit provider limits --------- Co-authored-by: Sebastian <sebastian@primeintellect.ai>
* chore: prepare v0.10.0 release * chore: set release version to v0.9.4 Fixes PrimeIntellect-ai#2118
Fixes ENG-6000 and ENG-6001. Refs ENG-5999.
…meIntellect-ai#2124) * fix(ci): accept engineering and research Linear tickets * fix(ci): clarify the Research ticket project fixes PrimeIntellect-ai#2124 * fix(ci): clarify Prime Agent board routing fixes PrimeIntellect-ai#2124
…xt (RES-1318) (PrimeIntellect-ai#2098) * feat(coding-agent): keep the system prompt static across refinements and deliver harness state in-context Fixes RES-1318. Applying a refinement no longer rebuilds and swaps agent.state.systemPrompt, so the provider prefix cache survives every refinement. Instead: - _applyRefine appends one durable refinement_notice custom message per applied refinement at the apply seam, labeled [auto-refinement] / [user-refinement] / [self-refinement] by source, with the trigger line and applied edits in the digest's own notation (rollbacks print as rollbacks). Zero-applied-edit refinements emit nothing and the notice never starts a turn. convertToLlm passes the new type through as a user message while the refinement_outcome audit message stays filtered and untouched. - The harness digest injection is removed from the system prompt entirely (buildSystemPrompt loses the harnessState option). The digest now appears at cold context boundaries only, reusing formatHarnessStateForPrompt byte-identically: fresh sessions get a digest custom message as the first context message; compaction attaches a digest snapshot to the compaction entry mechanically (never through the summarizer) so the post-compaction head message renders memories-first before the summary on both the initial and update-merge paths; resumes append a fresh digest at the tail only when the newest digest in live context no longer matches disk state (identity dedupe), which is also the migration path for pre-change sessions. The new compaction entry field and custom message types ride the existing schema-tolerant JSON entry stream (optional field, generic custom messages): backward-compatible, no daemon protocol or schema revision change. * test(coding-agent): adapt message-sequence pins to the session-start harness digest Every session now opens with a harness_digest custom message, so tests that pinned exact message sequences, first-message identity, provider-context text lists, or entry counts see one extra leading custom message. Adds a conversationMessages helper to the suite harness (filters the digest) and updates the affected assertions; provider-context probes now select the last user message instead of the first. * fix(coding-agent): harden harness digest recency and keep digests out of the summarizer Review fixes on PrimeIntellect-ai#2098: - _latestContextHarnessDigest picked the last digest by array position, but retained pre-compaction messages are presented after the compaction head while being chronologically older, so an old retained digest could defeat resume dedupe. Recency is now the greatest message timestamp among all in-context digest carriers. - The session-start harness digest entry was fed to the first compaction's summarizer as an ordinary custom message. prepareCompaction now excludes harness_digest entries from summarizer input; the digest is only attached mechanically at the new compaction head. * fix(coding-agent): label non-serialized refine.run notices as self-refinement Review fix on PrimeIntellect-ai#2098: _consumePendingRequestedRefine forwarded agent-callable refine.run requests through refine() without a source, so the notice carried the [user-refinement] label in non-serialized sessions. refine() now accepts an explicit source override and the pending-request consumer passes "self". * test(coding-agent): pin the self source on the pending refine.run consumer * fix(coding-agent): treat the digest as boundary-injected mechanical context everywhere Review round on PrimeIntellect-ai#2098 (three findings): - Untouched sessions stay empty: the fresh-session digest is now injected lazily at the first committed turn instead of at construction, so abandoned-draft cleanup and raw message-count emptiness checks (daemon-session-list lifecycle/activity, isEmptyDraftContent, branch seedability, hasExistingSession) keep seeing an empty session. Non-empty contexts (resume) still inject at construction with identity dedupe. - Tree navigation is a cold boundary: navigateTree rebuilds the context, so it now runs the same refresh-iff-stale digest check as resume (fork/clone/switch construct a new session and were already covered by the constructor path). - Headless terminal selection skips harness_digest like other non-output customs, so resume-and-print returns the saved final assistant output. Sweep of other message-kind-sensitive consumers: branch summarization now excludes harness_digest from summarizer input (same rule as compaction); estimateContextTokens and side-question seeding intentionally keep the digest (it is live context); _repliedToParentSinceTask and getUserMessagesForForking are kind-filtered already. * chore(coding-agent): trim narrative comments and redundant test asserts on the digest work Comments cut to one-line invariant guards; duplicate digest-notation and content asserts removed where a kept unit pin already covers the same rendering (labels and notation stay pinned in refinement-outcome-message.test, source plumbing stays pinned per path). No behavioral change. * fix(coding-agent): scope the first-turn digest to the turn that delivers it Review fix on PrimeIntellect-ai#2098 (Bugbot thread 2919fcf3): the first-turn digest was persisted directly at commit, before agent.prompt ran, so cancelling or clearing that first turn stripped the turn's captured messages but left the digest in state and in the session file - resurfacing the emptiness-check class through the cancel path. The digest now rides the turn's next-turn delivery records: it persists at message_end like the rest of the turn, first-turn cancellation strips and restores it with the turn, and the lazy injection re-delivers on the next committed turn. * test(coding-agent): event-order pins include the digest custom pair on the first turn * fix(coding-agent): deliver the first-turn digest on skip-policy turns and re-arm on cancel Two follow-up review findings on the lazy-injection shape: - Custom-triggered turns use the skip next-turn-context policy, so a fresh session whose first turn was sendCustomMessage(triggerTurn) ran without the digest. The pending digest now attaches on every committed first turn; skip policy still withholds ordinary pending next-turn messages. - A cancelled first turn whose digest record was already durable dropped the digest from live context without restoring it (restorable records skip durable ones). Lazy injection now owns digest delivery uniformly: cancelled turns never restore the digest message and instead re-arm the pending flag, so the next committed turn re-delivers a fresh one. * fix(coding-agent): never park the harness digest as pending next-turn context Review fix on PrimeIntellect-ai#2098 (Bugbot skip-turn-digest-lost-on-failure): a failed commit parked the undelivered digest in _pendingNextTurnMessages, which skip-policy custom-trigger turns never drain, and a parked copy plus a re-armed injection could double-deliver. Parking now filters the digest out and re-arms _harnessDigestPending instead, keeping lazy injection the single owner of digest delivery.
* fix(ci): accept engineering and research Linear tickets * fix(ci): clarify the Research ticket project fixes PrimeIntellect-ai#2124 * fix(ai): identify OpenCode conversations and application Fixes ENG-6009. * fix(ci): clarify Prime Agent board routing fixes PrimeIntellect-ai#2124
…-ai#2128) * fix(ci): accept engineering and research Linear tickets * fix(coding-agent): preserve literal prompt arguments Refs: ENG-6014 and discussion PrimeIntellect-ai#2106. * fix(ci): clarify the Research ticket project fixes PrimeIntellect-ai#2124 * fix(ci): clarify Prime Agent board routing fixes PrimeIntellect-ai#2124
…#2127) * fix(ci): accept engineering and research Linear tickets * fix(ai): omit tool results without surviving calls Fixes PrimeIntellect-ai#984. Linear: ENG-6012 * fix(ci): clarify the Research ticket project fixes PrimeIntellect-ai#2124 * fix(ci): clarify Prime Agent board routing fixes PrimeIntellect-ai#2124
* fix(ci): accept engineering and research Linear tickets * fix(ci): clarify the Research ticket project fixes PrimeIntellect-ai#2124 * fix(ai): recognize LiteLLM context limit rejections Refs: ENG-6011. Reported in discussion PrimeIntellect-ai#2088. * fix(ci): clarify Prime Agent board routing fixes PrimeIntellect-ai#2124
…Intellect-ai#2122) * fix(coding-agent): resume active goals after manual compaction * fix(coding-agent): retain each compaction abort controller * refactor(coding-agent): keep compaction fix minimal * fix(coding-agent): resume goals behind queued session commands
…lect-ai#2160) * ci: benchmark prime agent pull request updates Refs ENG-6043 * docs: clarify benchmark installation prerequisites Refs ENG-6043 * ci: reduce benchmark debounce to two seconds Refs ENG-6043; fixes PrimeIntellect-ai#2160 * ci: run benchmark harness tests in existing CI Refs ENG-6043; fixes PrimeIntellect-ai#2160 * ci: benchmark local runtime performance without inference Refs ENG-6043; fixes PrimeIntellect-ai#2160 * test: calibrate benchmark noise thresholds Refs ENG-6043; fixes PrimeIntellect-ai#2160
…ntellect-ai#2178) * fix: simplify benchmark comments and replace old results Refs ENG-6043; fixes PrimeIntellect-ai#2160 * fix: scale benchmark colours by percentage Refs ENG-6043; fixes PrimeIntellect-ai#2178.
…lect-ai#2148) * show the recorded model on inactive agents-view rows * show the recorded model in the actions panel for inactive rows
…-ai#2134) * fix(ci): accept engineering and research Linear tickets * fix(ci): clarify the Research ticket project fixes PrimeIntellect-ai#2124 * fix(coding-agent): include the lazy Bedrock provider in CLI bundles Fixes ENG-6006 * fix(ci): clarify Prime Agent board routing fixes PrimeIntellect-ai#2124 * fix(coding-agent): preserve structured Bedrock logs fixes PrimeIntellect-ai#2134 * test(coding-agent): await Bedrock fixture daemon shutdown fixes PrimeIntellect-ai#2134 * fix(ci): require complete Linear ticket identifiers * test(coding-agent): guarantee Bedrock fixture cleanup * test(coding-agent): allow context before Bedrock prompts * test(coding-agent): allow context before literal prompts * fix(ci): isolate Ubuntu package sources
…i#2130) * fix(ci): accept engineering and research Linear tickets * fix(ci): clarify the Research ticket project fixes PrimeIntellect-ai#2124 * fix(coding-agent): preserve reasoning in auxiliary calls Refs ENG-6010 and discussions PrimeIntellect-ai#1482 and PrimeIntellect-ai#2081. * fix(ci): clarify Prime Agent board routing fixes PrimeIntellect-ai#2124 * fix(coding-agent): reserve refinement reasoning output budget Remove fixed output caps when reasoning is enabled and reserve prompt context within the model limit. Keep truncation failures bounded without retrying. fixes PrimeIntellect-ai#2130 * fix: preserve refinement context and valid thinking budgets Estimate refinement tokens and shorten only older trajectory content when space is tight. Enforce valid minimum thinking budgets for budget-based Claude requests. fixes PrimeIntellect-ai#2130 * fix(ci): isolate Ubuntu package sources (cherry picked from commit 7656902) * test(coding-agent): allow context before literal prompts (cherry picked from commit 59e9714) * fix(coding-agent): bound dense refinement context (fixes PrimeIntellect-ai#2130)
…ellect-ai#2152) * rename rlm.run to rlm.spawn and drop the callable shim The kernel API for spawning a child is now the single explicit call await rlm.spawn(prompt, ...). Calling rlm(...) raises a TypeError that names the replacement, so a model imitating an older transcript can correct itself in one turn. The host request type stays "rlm.run" so kernels and hosts of different versions keep working. * teach rlm.spawn in the system prompt and host errors The system prompt, subagent guidance, refinement guidance, and spawn error messages now name rlm.spawn instead of the removed callable form. The kernel readiness check requires rlm.spawn and rejects a runtime that still exposes rlm.run, so a stale kernel venv is rebuilt instead of serving the old surface. * update docs to the rlm.spawn call form Docs, README, the subagent example, and the Python skill reference now show await rlm.spawn(...). The runtime doc keeps the "rlm.run" host request type where it describes the wire protocol. * keep one comment for the unchanged rlm.run wire type The runtime comment at the host_request call is the only place the spawn/wire name difference needs explaining. * require an explicit child name and finish the rename cleanup rlm.spawn(prompt, *, name, model=None, thinking=None) now rejects a nameless spawn at the Python signature, so every child gets a stable name the parent chose. The host keeps its generated-name fallback for wire payloads without a name, because older kernels still send those. Reading the removed rlm.run attribute now raises an AttributeError that names rlm.spawn, matching the not-callable error, so both stale habits recover in one turn. Docs no longer describe rlm as callable, and the bootstrap failure text says an interrupted upgrade rebuild needs network once more. * name the child in the runtime sequence diagram The diagram showed a spawn call that now fails at the signature.
…racket grammar (RES-1321) (PrimeIntellect-ai#2115) * feat(coding-agent): keep the system prompt static across refinements and deliver harness state in-context Fixes RES-1318. Applying a refinement no longer rebuilds and swaps agent.state.systemPrompt, so the provider prefix cache survives every refinement. Instead: - _applyRefine appends one durable refinement_notice custom message per applied refinement at the apply seam, labeled [auto-refinement] / [user-refinement] / [self-refinement] by source, with the trigger line and applied edits in the digest's own notation (rollbacks print as rollbacks). Zero-applied-edit refinements emit nothing and the notice never starts a turn. convertToLlm passes the new type through as a user message while the refinement_outcome audit message stays filtered and untouched. - The harness digest injection is removed from the system prompt entirely (buildSystemPrompt loses the harnessState option). The digest now appears at cold context boundaries only, reusing formatHarnessStateForPrompt byte-identically: fresh sessions get a digest custom message as the first context message; compaction attaches a digest snapshot to the compaction entry mechanically (never through the summarizer) so the post-compaction head message renders memories-first before the summary on both the initial and update-merge paths; resumes append a fresh digest at the tail only when the newest digest in live context no longer matches disk state (identity dedupe), which is also the migration path for pre-change sessions. The new compaction entry field and custom message types ride the existing schema-tolerant JSON entry stream (optional field, generic custom messages): backward-compatible, no daemon protocol or schema revision change. * test(coding-agent): adapt message-sequence pins to the session-start harness digest Every session now opens with a harness_digest custom message, so tests that pinned exact message sequences, first-message identity, provider-context text lists, or entry counts see one extra leading custom message. Adds a conversationMessages helper to the suite harness (filters the digest) and updates the affected assertions; provider-context probes now select the last user message instead of the first. * fix(coding-agent): harden harness digest recency and keep digests out of the summarizer Review fixes on PrimeIntellect-ai#2098: - _latestContextHarnessDigest picked the last digest by array position, but retained pre-compaction messages are presented after the compaction head while being chronologically older, so an old retained digest could defeat resume dedupe. Recency is now the greatest message timestamp among all in-context digest carriers. - The session-start harness digest entry was fed to the first compaction's summarizer as an ordinary custom message. prepareCompaction now excludes harness_digest entries from summarizer input; the digest is only attached mechanically at the new compaction head. * fix(coding-agent): label non-serialized refine.run notices as self-refinement Review fix on PrimeIntellect-ai#2098: _consumePendingRequestedRefine forwarded agent-callable refine.run requests through refine() without a source, so the notice carried the [user-refinement] label in non-serialized sessions. refine() now accepts an explicit source override and the pending-request consumer passes "self". * test(coding-agent): pin the self source on the pending refine.run consumer * fix(coding-agent): treat the digest as boundary-injected mechanical context everywhere Review round on PrimeIntellect-ai#2098 (three findings): - Untouched sessions stay empty: the fresh-session digest is now injected lazily at the first committed turn instead of at construction, so abandoned-draft cleanup and raw message-count emptiness checks (daemon-session-list lifecycle/activity, isEmptyDraftContent, branch seedability, hasExistingSession) keep seeing an empty session. Non-empty contexts (resume) still inject at construction with identity dedupe. - Tree navigation is a cold boundary: navigateTree rebuilds the context, so it now runs the same refresh-iff-stale digest check as resume (fork/clone/switch construct a new session and were already covered by the constructor path). - Headless terminal selection skips harness_digest like other non-output customs, so resume-and-print returns the saved final assistant output. Sweep of other message-kind-sensitive consumers: branch summarization now excludes harness_digest from summarizer input (same rule as compaction); estimateContextTokens and side-question seeding intentionally keep the digest (it is live context); _repliedToParentSinceTask and getUserMessagesForForking are kind-filtered already. * chore(coding-agent): trim narrative comments and redundant test asserts on the digest work Comments cut to one-line invariant guards; duplicate digest-notation and content asserts removed where a kept unit pin already covers the same rendering (labels and notation stay pinned in refinement-outcome-message.test, source plumbing stays pinned per path). No behavioral change. * fix(coding-agent): scope the first-turn digest to the turn that delivers it Review fix on PrimeIntellect-ai#2098 (Bugbot thread 2919fcf3): the first-turn digest was persisted directly at commit, before agent.prompt ran, so cancelling or clearing that first turn stripped the turn's captured messages but left the digest in state and in the session file - resurfacing the emptiness-check class through the cancel path. The digest now rides the turn's next-turn delivery records: it persists at message_end like the rest of the turn, first-turn cancellation strips and restores it with the turn, and the lazy injection re-delivers on the next committed turn. * test(coding-agent): event-order pins include the digest custom pair on the first turn * fix(coding-agent): deliver the first-turn digest on skip-policy turns and re-arm on cancel Two follow-up review findings on the lazy-injection shape: - Custom-triggered turns use the skip next-turn-context policy, so a fresh session whose first turn was sendCustomMessage(triggerTurn) ran without the digest. The pending digest now attaches on every committed first turn; skip policy still withholds ordinary pending next-turn messages. - A cancelled first turn whose digest record was already durable dropped the digest from live context without restoring it (restorable records skip durable ones). Lazy injection now owns digest delivery uniformly: cancelled turns never restore the digest message and instead re-arm the pending flag, so the next committed turn re-delivers a fresh one. * fix(coding-agent): never park the harness digest as pending next-turn context Review fix on PrimeIntellect-ai#2098 (Bugbot skip-turn-digest-lost-on-failure): a failed commit parked the undelivered digest in _pendingNextTurnMessages, which skip-policy custom-trigger turns never drain, and a parked copy plus a re-armed injection could double-deliver. Parking now filters the digest out and re-arms _harnessDigestPending instead, keeping lazy injection the single owner of digest delivery. * feat(coding-agent): unify synthetic user-channel messages under one bracket grammar Every machine-injected user-channel message now opens with one grammar: `[<kind>(: <qualifier>)( <address>)]`, a blank line, then the payload. - agent_message: `[agent-message from <relationship>:<name>]`; message id and endpoint ids live in details only. Detection keys on customType; the old header parser stays for persisted legacy transcripts (accept-both, emit-new). - heartbeat_prompt: `[heartbeat: <schedule> run#<n>]` instead of the raw prompt masquerading as the user. - async_bash_completion: `[bash-done pid:<pid> exit:<code>]`; the standing BashHandle inspection hint is gone (it duplicated the system prompt). - rlm child notices: `[child-exited: no-reply|cancelled child:<name>]` and `[child-failed child:<name>]`. - goal_context: `[goal: continuation|budget-limit|objective-updated]` replaces the <goal_context> XML wrapper. - ipython_state_restored: `[python-state-restored]` label (customType keeps its string); goal prose says "Python REPL" instead of "ipython". - compactionSummary/branchSummary/harness digest prefixes gain `[compaction-summary]`/`[branch-summary]`/`[harness-digest]` first lines. clearQueuedAgentMessages now matches queued actions by the delivered custom message's customType, with the legacy text parser as fallback. Fixes RES-1321 * fix(coding-agent): sanitize names interpolated into bracket headers One owner (sanitizeMessageHeaderValue) strips brackets, newlines, commas, and ":" from header-interpolated names: an RLM child name can no longer close the [child-failed]/[child-exited] header early, and an unlabeled sender named "parent:root" can no longer render as a relationship label. Addresses the Bugbot/Macroscope review findings on PR 2115. * fix(coding-agent): keep the heartbeat header on one line for whitespace-bearing schedules parseAgentCronSchedule's every/in regexes accept embedded newlines, so the schedule shown in the [heartbeat: ...] header goes through the shared header sanitizer; the exact expression stays in details.schedule. * fix(coding-agent): bring the remaining built-in injectors into the bracket grammar Review sweep of every built-in customType against convertToLlm found three model-visible kinds still emitting unmarked content: - autonomous_status -> `[autonomous-status: on|off]` + usage payload - autonomous continuations -> `[autonomous-continuation]` + prompt, and `[autonomous-continuation: gate-failed]` owned by buildAutonomousGateFailureContinuation (covers the headless injector too) - prime-agent.update_complete -> `[update-complete]` + notice - ipython_state (kernel persisted through compaction) -> `[python-state]` All other custom types are either grammar-compliant or filtered out of LLM context by convertToLlm / entry-only. A table-driven pin now walks every constructor-backed synthetic kind and asserts a grammar-conforming first line. * test(coding-agent): adapt daemon-mode and model-extension pins to the bracket grammar The full CI matrix (first run since the PR retargeted main) caught two expectations outside test/suite still pinning pre-grammar text: the retained- subagent send now asserts the exact `[agent-message from ...]` prompt, and the model_select injected-prompt pin carries the `[heartbeat: ...]` header.
* fold the twin agent rosters into one Family discovery had two entry points: agent_message.list_agents listed the nuclear family including members that were only on disk, while agent_observe.list_agents listed live sessions plus passive subagents. Neither was a superset, so an agent had to call both to know who it could reach. agent_observe.list_agents is now the single roster. It is built from the family catalog that send() already uses to resolve receiver_role and receiver_name, so membership has one definition. Entries carry the distinction as data: a member with no live session here has no activeSessionId, keeps status "inactive", and reports only persisted facts, while live members keep their runtime detail. Every entry carries its relationship, so a roster row maps directly onto a send. agent_message.list_agents is gone from the skill module and the docs; its host request now fails with a message naming agent_observe.list_agents, so an old kernel or a copied old transcript self-corrects. Fixes RES-1335 * correct the observe read scope and order the roster snapshot The observe skill doc claimed transcript reads never work on inactive members; the daemon hydrates a passive child on demand, so only root siblings in another worker are unreadable. Build the resident-session map after the family catalog resolves, so a session that becomes resident during the catalog's disk and supervisor IO is still reported with its live fields. * bound roster previews and keep peer rows honest A saved root's first user message could be any size, so copying it onto every sibling row made one roster reply carry the opening prompts of every saved session. Saved roots no longer carry a first message at all, and the previews a row can carry share one cap with the live latest-message preview. A peer working in another worker is live, so its row now keeps the active session id the peer summary reports instead of claiming an active session with no id. * say which rows carry a first-message preview
…us command (PrimeIntellect-ai#2206) * feat(coding-agent): accept autonomous budget flags from the /autonomous command /autonomous on now accepts the same budget options as the --autonomous-* CLI flags, so an interactive run uses a user-defined continuation, turn, token, or time budget instead of always stopping after the default three continuations. - Flags: --max-continuations, --max-turns, --max-tokens, --timeout-ms, --gate (repeatable), --gate-retries, --gate-timeout-ms; the full CLI spellings (--autonomous-max-continuations, ...) work as aliases. - Both --flag <value> and --flag=<value> forms are accepted, and quoted gate commands keep their spaces. - Only named fields change; unspecified limits and gates keep the session/CLI-configured or default values. - Invalid values, unknown flags, and flags passed to off/status fail with a usage error and do not enable autonomous mode. - The autonomous status text now also reports the time budget and the configured gate commands. * feat(coding-agent): accept unlimited and separator-formatted autonomous budgets The four /autonomous budget limits now accept an explicit unlimited value (a JSON-safe no-cap sentinel rendered as unlimited in the status), and numeric values accept comma or underscore digit separators so large budgets like --max-tokens 100,000,000,000 type naturally. Gate flags keep strict positive-integer values. * feat(coding-agent): bound /autonomous runs only by the named budget flags When any budget flag is named, the unnamed budget limits become unlimited so only the flags the user passes (plus gates) decide when the run stops; /autonomous on --max-tokens 100,000 no longer stops at the default three continuations. With no budget flags at all, the configured or default limits still apply.
Retain caller-owned catch-up environments, lifecycle proofs and bounded transport while adopting upstream retry, catalog and roster changes. Reconcile schema 32 and isolated process fixtures. Closes #44.
Fixes #55 integration CI failures while retaining ownership and cleanup checks.
Fixes #55 checkpoint finding: retry after journal recovery without clearing ownership, lease, or active cleanup fences.
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.
Integrate Prime upstream 0.9.4 at
1eee2938b4eeb7a4d72e17035adda669a89b63dewhile preserving Pylon owned-session, bounded-ingress, cleanup, recovery, and publication boundaries. Retain upstream ancestry through mergeaeb1c6368194dc6c88d2e64a6d0c1bfca7c2f7bb; follow-upa70feb1a6273e0e74b5c22e8544ad18c8eed65c1preserves actual worker replay through native synchronization. Final protocol7/schema33, identityprotocol-7-schema-33-5924c5b19b8b.Final head
514e40454e048707a8467940677170c2e15a8a39also corrects CI-exposed recovery self-parking and shutdown acknowledgment ordering. Recovery-owned attachment/snapshot requests use the existing nonrecoverable inner request mode; external parking remains unchanged. Shutdown checks ownership, journals and writes its reply before cleanup. A failed journal attempt releases only its own pending fence after current ownership and lease proof, allowing safe retry. Direct attachment restores the frozen upstream roster notification. Compaction and cleanup fixtures now assert their actual distinct outcomes; no compaction runtime changes or relaxed timeouts.CI correction verification: 567 owned/recovery tests passed with 9 skips; the final failed-journal recovery correction passed 36 targeted tests, including two tests that failed on the previous source. Required checks and normal hooks passed. The checkpoint and single finding recheck passed at the final exact tree. Final-head hosted CI is green, including Windows named-pipe, process smoke, all coding-agent shards, reproducibility, and macOS/Linux artifact installs. The final private pack passed exact digest checks, R4 and all 10 release-contract tests.
Adopt upstream retry ownership, model catalog, harness digests, roster/direct-peer transport, structured recovery errors and worker scheduling. Remove superseded fork retry code. Preserve exact caller-owned environment validation; unknown or partial replay never becomes synthetic complete. Inherited release/Linear and unapproved benchmark workflows stay excluded. PR#54 is outside the frozen adoption range.
Validation: the required integration matrix passed82files/1,369tests with16inheritedskips; bounded four-worker execution. Follow-up replay matrix passed673tests; final48protocol/regression tests and a cache mutant confirmed the new guard.
npm run checkand actual normal hooks passed. Clean pinned Node22.23.2 private pack produced exactly five artifacts, digest checks passed, R4 exported four features with recoverable:true, release-contract tests10/10passed.Live Pylon0.9.4 contract re-proved at final514e404: native card, both runtime modes, first turn with3tools/ready checkpoint, same-thread denied and approved writes, Stop with interrupted state/ready composer/no toast passed. Restart produced interrupted-with-reason rather than a hang. Manual builds intentionally lack managed adoption authority; postrestart ownership quarantine remains preserved. Managed restart/continuation and full final web/mobile acceptance remain pending safe publication under#53. This PR does not authorize a pre-#53 preview publication.
Two test-fixture corrections were reviewed at checkpoints: private HOME prevents consulting the machine-wide supervisor registry; authoritative zombie-aware liveness replaces a kill(pid,0)-only helper. No ownership guard, assertion, or request budget was relaxed. Full decisions and verification are recorded in
.pylon/upstream-review.md.Per-file conflict decisions (41):
.github/workflows/build-binaries.yml.github/workflows/linear-ticket.yml.github/workflows/ci.ymlpackages/ai/package.jsonpackages/ai/src/providers/openai-completions.tspackages/ai/src/providers/simple-options.tspackages/ai/src/types.tspackages/ai/src/utils/stream-failure.tspackages/ai/test/anthropic-sse-parsing.test.tspackages/ai/test/prime-inference-models.test.tspackages/ai/test/stream-failure.test.tspackages/coding-agent/docs/settings.mdpackages/coding-agent/src/cli/daemon-launch.tspackages/coding-agent/src/core/agent-session.tspackages/coding-agent/src/core/compaction/branch-summarization.tspackages/coding-agent/src/core/compaction/compaction.tspackages/coding-agent/src/core/refinement/refinement.tspackages/coding-agent/src/core/sdk.tspackages/coding-agent/src/core/settings-manager.tspackages/coding-agent/src/core/side-question.tspackages/coding-agent/src/core/system-prompt.tspackages/coding-agent/src/main.tspackages/coding-agent/src/modes/agent-connection/daemon-agent-connection.tspackages/coding-agent/src/modes/agent-connection/in-process-agent-connection.tspackages/coding-agent/src/modes/daemon/command-recovery-journal.tspackages/coding-agent/src/modes/daemon/daemon-client.tspackages/coding-agent/src/modes/daemon/daemon-mode.tspackages/coding-agent/src/modes/daemon/daemon-protocol.tspackages/coding-agent/src/modes/daemon/daemon-supervisor.tspackages/coding-agent/src/modes/daemon/daemon-worker-client.tspackages/coding-agent/src/modes/daemon/daemon-worker-protocol.tspackages/coding-agent/test/agent-connection-daemon.test.tspackages/coding-agent/test/agent-session-recursion.test.tspackages/coding-agent/test/daemon-mode.test.tspackages/coding-agent/test/daemon-protocol.test.tspackages/coding-agent/test/daemon-supervisor-admission.test.tspackages/coding-agent/test/daemon-supervisor-monitor.test.tspackages/coding-agent/test/daemon-supervisor-process.test.tspackages/coding-agent/test/suite/agent-session-retry-events.test.tspackages/coding-agent/test/suite/regressions/4602-snapshot-transfer-idempotency.test.tspackages/coding-agent/test/system-prompt.test.tsThe table records the integration schema32; the reviewed replay follow-up advances the final wire shape to33. The caller-owned capability literal is centralized in sdk-features.ts and referenced by the protocol rather than duplicated for a grep count.
Resolves #44. Related:pylon-code/pylon#114; publication remains tracked in#53. Merge with a merge commit to preserve upstream ancestry.
Model:GPT-6 Astra. Harness:Codex in Pylon.