fix(cli): detach a failed Session instance before its cleanup terminate - #595
Merged
Merged
Conversation
`SessionManager` registers a `Session`'s lifecycle events before `createAgent`, so the cleanup `terminate(true)` of an instance whose agent never started published `terminated` as if the live turn's agent had died. `MessageHandler` then finalized the turn and cleared its ACP update target, and when the execution service fell back from a failed ACP resume to a history-replay session every update from the replacement agent was dropped; the turn was recorded as "completed without any agent output". Detach the instance from the manager listeners and the live map before the cleanup terminate. Lifecycle events now publish only for instances a caller received; the rejected `createSession` promise is the sole signal for a startup failure and the caller owns the recovery. Regression test drives the public `createSession` with `createAgent` rejecting `[ACP_RESUME_UNSUPPORTED]` and asserts no `terminated`/`exit` reaches consumers while a replacement under the same id still publishes. Model: claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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.
Related issue
None. Same-repository branch; diagnosed from local daemon logs of two affected sessions on 2026-09-11.
Problem / pressure
A session restored after idle GC whose ACP resume attempt fails (dsh advertises neither
loadSessionnorresume) lost an entire turn of agent output.SessionManagerregisters aSessioninstance's lifecycle events beforecreateAgent, so the cleanupterminate(true)of that never-started instance publishedterminatedas if the live turn's agent had died.MessageHandlerfinalized the turn and cleared its ACP update target; the execution service then fell back, by design, to a history-replay session, and every update from the replacement agent was dropped (Dropping ACP update without an active/finalized assistant entry target, 34,551 thought chunks and 48 message chunks over six minutes). The turn ended ascompleted without any agent outputwhile the agent's own transcript held the full answer.Summary
SessionManager.registerSessionEventskeeps a per-instance detacher in aWeakMap;detachSessionremoves the listeners and drops the instance fromsessionsonly when it is still the mapped instance for that id.createAgentcatch block callsdetachSessionbeforesession.terminate(true). Lifecycle events now publish only for instances a caller received.apps/cli/AGENTS.md(cross-entry contracts) since it binds both the producer (session-manager.ts) and the consumer (message-handler.ts); the decision and rejected alternatives are in the new bug-fix note.session-manager.test.ts.Visual explanation
sequenceDiagram participant X as SessionExecutionService participant M as SessionManager participant S as Session (attempt 1) participant H as MessageHandler participant S2 as Session (fallback) X->>M: createSession(resume=acp-old) M->>S: new + registerSessionEvents S-->>M: createAgent throws ACP_RESUME_UNSUPPORTED rect rgb(255,235,235) Note over M,S: before: terminate(true) → 'terminated' → H.finalizeACPState → turn idle end rect rgb(235,255,235) Note over M,S: after: detachSession(S) then terminate(true) and nothing reaches H end M-->>X: reject X->>M: createSession() (history replay fallback) M->>S2: new + registerSessionEvents S2-->>H: agent updates → routed to the live turnBefore / after
terminatereachesMessageHandler; the live turn is finalized; fallback agent output is dropped; turn recorded as silent failureprompting; fallback agent output lands in the assistant entry; a replacement session under the same id still publishesterminatednormallyexit-driven idle-status writecreateSessionpromise signals the failure; the caller owns recoveryTest plan
apps/cli:vitest run src/session/session-manager.test.ts— 24 passed, including the newfailed agent creationtest.terminatedcalled once).apps/cli:vitest run tests/session-execution-service.test.ts— 102 passed (owner of the fallback path, unchanged).apps/clipnpm typecheckexit 0;oxlint --type-awareno findings on changed lines; prettier clean;pnpm run docs checkno errors.pnpm test:ci; no end-to-end restore against a real resume-less agent.Context handoff
Instructions for reviewing agents
session-manager.tsdetachSessionand thecreateAgentcatch block; confirm no other consumer ofexit/terminated(machine-runtime.ts,lody-fleet.tsterminal cleanup) needs the event for an instance that never leftcreateSession.MessageHandler's listener (rationale in the note); the prepared-session path shares the same catch block.exit; verified only by unit tests, not by an end-to-end restore against a real agent.Authoring context
session-managerwith the trade-off explained in code.MessageHandler; dsh's missingloadSessioncapability is an upstream gap and out of scope.createAgentfailed; the map entry is removed only if it is still that instance.🤖 Generated with Claude Code