Mirrored registry status from pre-wire session run-state transitions and required live session corroboration before a peer can sustain bare hub waits.
Fixes#8634
Consulted the persisted subagent reviver factory before reclaiming an
unadopted parked ref. A valid cold reviver now preserves the existing agent;
factory failures also fail closed rather than deleting a potentially
recoverable generation. Reclaim invariants are rechecked after the async
probe to close lifecycle races.
Added regression coverage proving an unadopted disk-restored ref remains
registered and messageable through ensureLive.
Fixes#8490
A parked, session-less agent-registry entry with no reviver permanently
poisoned its agent id for the process lifetime. `registerIfAvailable(input,
null)` refuses any existing entry, so a fresh subagent spawn reusing the id
died post-registration with "already owned by another session generation",
and messages to it failed with "is parked and cannot be revived". Such
corpses are left by an isolated run's park or an interrupted construction,
and there was no reclaim or GC path.
Add `AgentLifecycleManager.reclaimDeadCorpse`, which unregisters a ref only
when it is parked, holds no live session, is not owned by the manager (no
in-memory reviver) and has no park/revive in flight. The fresh-spawn path in
`createAgentSession` calls it on a collision and retries registration, so the
id becomes reusable. The registry's strict CAS contract is untouched; the
corpse's transcript stays readable at history://<id>.
Fixes#8490
SessionManager.open writes title+session before createAgentSession
claims the id. Agent Hub parked that stub, so the spawn CAS failed
with "already owned by another session generation" and the row
could not be revived (no session_init).
Clear the disposed singleton after teardown so a later top-level session receives a fresh manager with an active registry subscription. Keep the retired manager marked disposed so its in-flight revivals still reject and clean up late sessions.
Add regression coverage for sequential top-level lifecycle owners cold-reviving in the same process.
Fixes#8114
A cold persisted-agent revival could complete after
AgentLifecycleManager.dispose() and still attach its live session. When
teardown lands during the reviver-factory await, the id is not yet tracked
in #adopted, so dispose() skips it and leaves the parked registry ref
intact; the late factory then cold-adopts and #revive attaches a session
plus a TTL timer onto the torn-down manager, leaking a live session graph
and timers past teardown.
Set a #disposed flag at the top of dispose() and recheck it after the
reviver-factory await and after the reviver await. A late revive now
rejects deterministically and disposes any session it already built,
preserving singleflight for concurrent callers.
Fixes#8114
Two cleanups from review.
The offline walk matched a served turn to its `model_change` by also
accepting a `:level` thinking suffix, but no writer produces one: every
`appendModelChange` call site records a bare `provider/id` or a
`formatModelStringWithRouting` selector, and only the latter decorates —
with `@upstream`, never a level. Speculative matching in the part of the
change that already reconstructs the most from raw strings. Removed, with
its test narrowed to the format writers actually emit.
Attribution was also retained for the life of the recovery component, so
switching sessions in place could report a model from a different
transcript by name. It is now tagged with the session file it was earned
in and ignored when that no longer matches — the same self-invalidating
anchor `#ensurePersistedMessageKeys` uses, so no mutation call site has
to remember to clear anything. Anchoring on the file rather than the leaf
keeps attribution across compaction and appends, which stay within the
transcript that earned it.
An Agent Hub row reported a subagent as having run on a model that never
spoke. All 97 of its requests, 421K tokens and $6.19 of cost were served
by the primary; a transient stall then armed a fallback, that fallback
errored on its first request with an exhausted quota, and the run died.
Attribution followed the routing switch rather than the output.
Three surfaces lied independently, each re-deriving "the current model"
and calling it the run's model: the executor's progress snapshot, the
session's fallback selector that the hub row reads first, and the
transcript walk behind a settled row.
Sessions now own attribution. `AgentSession.servingModel` names the model
that produced this session's output, holding the last model that actually
served while a candidate is armed but unproven. A switch is a routing
decision, not evidence the target can produce anything, so the answer only
moves once a turn on the target settles.
Consumers read it instead of reconstructing it. The executor's observer
dropped its own event bookkeeping: that bus also carries advisor turns
running on a different model, and it was reading `retry_fallback_applied`
as proof of service. The hub row reads the same getter, so the main
session — which has no executor progress and no persisted history — stops
rendering an unproven candidate as its plain configured model. A fallback
armed before anything has served is still shown, marked as a fallback,
because there is no earlier work to miscredit there.
One predicate decides "this turn produced output", shared by the live
session and the offline replay so they cannot disagree. `error` and
`aborted` are both failures — a stalled stream is finalized as `aborted`
with its partial block still attached, so a stop reason alone proves
nothing — and a turn needs actionable content, which a `length` stop
burning its budget on unsigned thinking does not have. It tolerates
malformed content blocks: transcripts outlive the shapes that wrote them,
and one bad line previously blanked a whole row's history.
Ordering matters at two swap sites. Both the chain advance and the
cooldown-expiry restore move the model and fan `model_changed` out to
subscribers synchronously, so each now updates fallback state before the
swap rather than after; otherwise an observer reading attribution inside
that window sees the incoming candidate carrying the outgoing one's proof.
A startup-selected fallback owns the run from its first request only on a
fresh session. A resumed transcript already holds turns another model
produced, so there the candidate stays unproven until it answers.
`retryFallbackModel` is removed: every consumer reads `servingModel`, and
keeping a parallel derived getter alive for tests is the duplicate surface
this change set exists to remove.
- 28 symbols across discovery, mcp header policy, agent-hub projection and
rendering, the agent registry, shell tokenizing and changelog comparison
were exported but referenced only inside their own module; they are now
module-private, shrinking the deep-import surface.
- Kept AGENT_PLUGIN_MANIFEST_SCHEMA, AGENT_PLUGIN_MCP_SCHEMA,
parseAgentPluginManifest, clearAgentPluginRootCache and mergeMCPHeaders
exported: each is a seam for tests that defend real parsing or header
precedence behavior.
- Nothing reachable from an explicit exports entry or public barrel changed.
A parked revive holds the same AgentRef while constructing a new session.
If the Hub tombstoned that ref before revive completed, the reviver could
still attach its session and set the terminal ref back to idle because
identity alone remained unchanged.
- Made aborted registry refs terminal: reject revival claims, late session
attachment, and status transitions out of aborted.
- Made lifecycle revival accept only an untouched detached parked ref or the
exact running session already claimed by createAgentSession; dispose and
reject every terminal/stale result.
- Added a delayed-revival regression test and claim-before-kill CAS checks.
Fixes#7250
Preserving aborted refs on dispose exposed a latent invariant break: the
executor's hard-abort path (finalizeSubagentLifecycle) set status `aborted`
and disposed the session without detaching it. With the ref now retained, it
kept a dangling pointer to the disposed session, and ensureLive returns any
non-null ref.session before its revivability check — so hub focus / transcript
chat could route into a dead session.
- finalizeSubagentLifecycle: detach the session before disposing on the
terminal hard-abort path, upholding the AgentRef invariant (session === null
when aborted).
- release(tombstone): detach before dispose too (capture the live session
first), same invariant.
- unregisterUnlessParked: preserve `aborted` refs only when already detached;
an aborted ref still holding a live session is a bug and is unregistered
rather than kept reachable.
- Regression test now asserts ensureLive rejects a tombstoned id as terminal.
Fixes#7250
A live-session hub kill did not stick: release(tombstone) awaited the
wrapped session dispose first, and createAgentSession's unregisterUnlessParked
removed any non-parked ref, so the subsequent detach/setStatus no-oped and the
ref was gone — leaving the reopen resurrection for idle/running agents.
- release(tombstone) now marks the ref `aborted` BEFORE disposing, so the
dispose guard preserves it; the session is detached afterward.
- unregisterUnlessParked now also spares terminal `aborted` refs (matching
the documented "hard-killed, terminal" retention and finalizeSubagentLifecycle).
- Regression test now uses a session stub that mirrors the real wrapped
dispose (unregister unless parked/aborted), so it fails if the tombstone is
set after dispose.
Fixes#7250
The Agent Hub kill path called AgentLifecycleManager.release(), which
disposes the session and then unregisters the ref while leaving the
on-disk <id>.jsonl intact. On the next hub open, registerPersistedSubagents
rescans the transcript tree and its `if (!registry.get(id))` guard cannot
distinguish an explicit kill from a normally-parked agent, so it re-adopts
the killed id as a fresh `parked` row.
Add a `tombstone` option to release() that mirrors finalizeSubagentLifecycle's
genuine-kill path: dispose and detach the session but keep the ref registered
as terminal `aborted` instead of removing it. The kept-registered id makes the
rescan guard skip it, and the transcript stays on disk (still reachable via
history://<id>, per #5261). The hub kill button now passes tombstone: true.
Fixes#7250
Moved persisted roster discovery out of the Agent Hub UI so model-facing coordination can use the same disk-backed registration path.
Rehydrated parked peers before an empty hub list response and added a crash-resume regression test.
Fixes#5864
The new isParking/has checks consulted the global lifecycle for every send, so a custom-registry IrcBus (which falls back to the global manager) could gate a live recipient on unrelated global park state for the same id. Add AgentLifecycleManager.manages(registry) and apply the mid-park/adopted gate only when the lifecycle owns this bus's registry; the parked-status path (read from the bus's own registry) is unchanged.
Fixes#5633
park() detached the live session only after session.dispose() resolved,
so during the dispose window the registry still exposed ref.session at
idle status. Concurrent ensureLive()/hub-send handed out or injected into
the dying session, reporting success while the message was dropped once
detach committed.
- Replace the #parking Set guard with a #parks map of in-flight park state
that is cancelable until the session is detached.
- park() now yields a cancel window, then detaches + flips status to
parked BEFORE dispose(), and coalesces concurrent park calls.
- ensureLive() cancels a pre-detach park (keeps the live session) or waits
for detach+dispose then performs one coalesced revive; never returns a
disposing session.
- release()/dispose() drain any in-flight park; the idle re-arm skips while
a park owns the transition.
- IrcBus.send() gates parkable recipients through ensureLive and derives
the revived receipt from session identity, so receipts/unread counts
reflect actual delivery.
Fixes#5633
- Added the `xd://` virtual device protocol (`internal-urls/xd-protocol.ts`, `tools/xdev.ts`): tools declaring `loadMode: "discoverable"` are unmounted from the request tools array and driven via `read xd://` (list/docs+schema) and `write xd://<tool>` (execute), gated by the `tools.xdev` setting (default on) and inlined into the system prompt.
- Merged the `irc`, `job`, and `launch` tools into a single `hub` tool (`tools/hub/`, `async/job-manager.ts`): messaging keeps `send`/`inbox`/`list`, job control maps to `wait`/`cancel`/`jobs`, process supervision keeps `start`/`logs`/`stop`/`restart`/`describe` with `ps`, and the unified `wait` races background jobs against peer messages; SDK `IrcTool`/`JobTool`/`LaunchTool` are replaced by `HubTool`.
- Removed the hidden `resolve` tool in favor of the `xd://resolve`/`xd://reject`/`xd://propose` resolution devices, auto-including `write` whenever a deferrable tool or plan mode is present.
- Removed the BM25 tool-discovery system: the `search_tool_bm25` tool, the `tool-discovery` module, the `tools.discoveryMode`/`mcp.discoveryMode`/`mcp.discoveryDefaultServers`/`tools.essentialOverride` settings, per-tool MCP selection, and the `mcp_tool_selection` message type.
- Unified tool presentation on `ToolLoadMode` (`essential`|`discoverable`), replacing the custom-tool `xdev?: boolean` opt-out; custom, extension, MCP, RPC host, image-generation, and TTS tools now default to `discoverable`, and added a `satisfies` predicate to `SoftToolRequirement`.
- Removed the standalone `ssh` command tool and `ssh/ssh-executor` (the `ssh://` read/write/search protocol stays), and made `--tools` address hidden built-ins.
- Updated collab-web to render `xd://` dispatches and `hub` op families, dropped the `search_tool_bm25`/`ssh`/`report-finding` renderers, refreshed tool docs and prompts, and migrated the affected tests and changelogs.
Vibe worker roster lived only in a process-local Map, so a resumed parent
session started with an empty registry and vibe_send failed with
"Unknown vibe session". Persist a versioned, parent-scoped lifecycle
journal (spawn/turn/tombstone events), rehydrate validated idle workers
through the persisted-subagent reviver on resume, and gate the flow with
generation/CAS protection so stale finalizers cannot clobber a
replacement worker. Killed transcripts stay readable but non-revivable;
mode-exit commits tombstones atomically with the mode change and rolls
back cleanly on storage failure.
Ported from @mastertyko's fork branch fix/vibe-session-persistence.
Fixes#5303
- Implemented `AdvisorTranscriptRecorder` to persist advisor sessions to append-only `__advisor.jsonl` files.
- Integrated transcript recording into agent sessions with managed flushing, atomic file switching, and synthetic turn attribution.
- Restricted advisor-kind agents by excluding them from rosters, history protocols, messaging, and interactive agent commands.
- Reserved the `__advisor` filename stem across the output manager and task registry to prevent task ID collisions.
- Fixed cold revival flow so parked subagents are restored from persisted sessions at startup.
- Fixed session-init persistence to include spawns and readSummarize fields for replay accuracy.
- Fixed latest-session lookup by adding peekSessionInit for lock-free persisted contract access.
- Added lifecycle and session tests for cold-revive success, decline, and retry paths.
Add a display-only `activity` field to AgentRef plus `setActivity`, fed
from the subagent progress chokepoint with a short gist of the agent's
latest intent (or current tool). Render it in the `irc list` output, the
subagent peer roster, and the TUI peer card, beside the role-derived
display name. setActivity emits no event — the roster reads on demand —
so the per-tool-call rate stays off the registry listener path. Peers
with no activity render without a dangling clause.
Refs #2470
Op: extend
Introduces AgentLifecycleManager: when the task executor adopts a finished subagent the manager arms a TTL timer on idle, parks the agent on expiry (disposes the live session, keeps the AgentRef + sessionFile), and revives it on demand via an injected reviver. Only this manager flips parked ↔ idle. AgentRegistry now annotates session: AgentRef["session"] as null exactly when parked/aborted, and sdk.ts wires the lifecycle dispose into main-session teardown, derives agentKind once, and gates ref unregistration on parking so a parked agent stays addressable (history://, revive).
- Changed `AgentOutputManager` to use requested names verbatim, adding `-2`/`-3` suffixes only on repeats (e.g. `Anna`, `Anna-2`).
- Renamed main agent id from `0-Main` to `Main`; nested ids now use dot notation without numeric prefix (e.g. `Parent.Child`).
- Updated task widget to render dotted hierarchy as `Parent>Child` breadcrumb without leading index.
- Resume scan now tracks seen names instead of a counter to avoid clobbering prior outputs.
- Pre-registered agents in the global registry before rebuilding the system prompt so peers can discover each other in initial IRC blocks.
- Replaced immediate registry replacement with attachSession to bind the live session and latest sessionFile to the pre-registered entry.
- Ensured pre-registered agents are unregistered during creation failure cleanup when no session was established.
- Added `AgentRegistry` singleton with session registration/unregistration and IRC routing metadata for peer lookups.
- Added IRC messaging prompts and tooling with `irc.enabled` setting, `list/send` tool paths, and peer roster rendering.
- Changed `/btw` to session-side `runEphemeralTurn`, added background IRC exchange flushing, and fixed empty-input checks.
- Added unit tests for IRC tool and BtwController ephemeral behavior, including disabled, busy, not-found, and abort cases.