From 47f0c308161231c82e7220c1669430acbea4e046 Mon Sep 17 00:00:00 2001 From: Yeachan Heo Date: Fri, 14 Aug 2026 17:23:26 +0000 Subject: [PATCH] fix(session): track terminal assistant before admission wait MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Canonical message admission serialization made externally emitted terminals race their own message_end: the agent_end handler could read #lastAssistantMessage before the message_end handler assigned it, so post-turn continuation logic (deep-interview stop handling) ran against the previous turn's assistant — double-processing one stop and skipping another. Capture the terminal assistant synchronously, before the admission wait, mirroring the reservation that is already synchronous. T2 mid-run maintenance is updated to the chronological contract: with canonical admission persisting seed order, the oldest orphan tool result falls outside the protectRecentTurns fence, so maintenance correctly prunes (canonical truncate notice) instead of compacting; the paired result, both steering messages, and the codex close are still asserted. Lore-id: 71c2e9b0 Constraint: must not reorder persistence — only the synchronous bookkeeping capture moves Rejected: awaiting admission inside the agent_end handler | would serialize terminals behind unrelated spills Confidence: high Scope-risk: narrow Reversibility: trivial Tested: deep-interview continuation 22/22; mid-run maintenance 19/19; midrun compaction 18/18; spill 5/5; cursor-exec + concurrent 58/58; coding-agent check green Not-tested: live provider terminals in production traffic --- .../coding-agent/src/session/agent-session.ts | 12 ++++++++++-- .../agent-session-midrun-maintenance.test.ts | 19 +++++++++++++------ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/packages/coding-agent/src/session/agent-session.ts b/packages/coding-agent/src/session/agent-session.ts index 047716b27a..b271f00899 100644 --- a/packages/coding-agent/src/session/agent-session.ts +++ b/packages/coding-agent/src/session/agent-session.ts @@ -4762,6 +4762,14 @@ export class AgentSession { this.#silentAbortPending = false; } + // Track the terminal assistant synchronously, before any admission wait: + // the agent_end handler for an externally emitted terminal can otherwise + // read a stale #lastAssistantMessage (missing the just-emitted stop) and + // run post-turn continuation logic against the previous turn. + if (event.type === "message_end" && event.message.role === "assistant") { + this.#lastAssistantMessage = event.message; + } + // Canonical persistence follows synchronous message_end reservation order. // Only the admission predecessor and this event's own pre-admission work are // inside the lane; release before extension delivery and unrelated post-work. @@ -5082,9 +5090,9 @@ export class AgentSession { this.#markTtsrInjected(this.#extractTtsrRuleNames(event.message.details)); } - // Track assistant message for auto-compaction (checked on agent_end) + // (#lastAssistantMessage is captured synchronously before the + // admission wait above.) if (event.message.role === "assistant") { - this.#lastAssistantMessage = event.message; const assistantMsg = event.message as AssistantMessage; const currentGrantsAnthropicPriority = this.serviceTier === "priority" || this.serviceTier === "claude-only"; diff --git a/packages/coding-agent/test/agent-session-midrun-maintenance.test.ts b/packages/coding-agent/test/agent-session-midrun-maintenance.test.ts index b8be4ac520..3ef73805c0 100644 --- a/packages/coding-agent/test/agent-session-midrun-maintenance.test.ts +++ b/packages/coding-agent/test/agent-session-midrun-maintenance.test.ts @@ -521,13 +521,16 @@ describe("AgentSession mid-run maintenance outcomes", () => { { role: "user", content: "second distinct steering", timestamp: Date.now() }, ]); - // With protectRecentTurns: 2 (default), the session manager's canonical - // entry ordering places the large orphan tool results inside the fence - // window, so they are not eligible for pruning — maintenance falls - // through to compaction. The short-circuit extension produces a - // compaction entry that preserves the recent paired result and steering. + // Canonical message admission is chronological, so the branch persists + // exactly the seeded order: the three orphan tool results land BEFORE the + // two trailing steering turns. With protectRecentTurns: 2 (default) the + // fence starts at the first steering message, so the oldest orphan output + // falls outside the 40k-token protect window and is prunable — maintenance + // prunes instead of compacting. The rewrite must still preserve the recent + // paired result, both steering messages, and replace the pruned output with + // a canonical truncate notice. const outcome = await session.runMidRunMaintenanceForTests(contextOf(session)); - expect(outcome).toBe("compacted"); + expect(outcome).toBe("pruned"); const persisted = session.sessionManager .getBranch() .flatMap(entry => @@ -543,6 +546,10 @@ describe("AgentSession mid-run maintenance outcomes", () => { persisted.filter(message => message.role === "user" && message.content === "second distinct steering"), ).toHaveLength(1); expect(closed).toBeGreaterThanOrEqual(1); + const prunedOldest = persisted.find(message => message.toolCallId === "old-output-1"); + expect(prunedOldest).toBeDefined(); + expect(JSON.stringify(prunedOldest?.content)).toContain("[Output truncated"); + expect(getLatestCompactionEntry(session.sessionManager.getBranch())).toBeNull(); }); it("fails closed when tool-output artifact persistence is unavailable", async () => {