Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions packages/coding-agent/src/session/agent-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment on lines +4769 to +4770

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Correlate the cached assistant with each terminal event

When a host bridge or replay emits two complete lifecycles back-to-back without awaiting the asynchronous handlers, the second message_end executes this assignment before the first agent_end resumes from #emitSessionEvent and #goalRuntime.onAgentEnd. The first terminal then consumes the second turn's assistant at #lastAssistantMessage ?? fallbackAssistant and clears the field, so retry, compaction, and deep-interview handling can skip the first stop and process the second stop twice. Cache assistants by terminal/run identity, or prefer the assistant carried by the matching agent_end, rather than using one shared mutable slot.

Useful? React with 👍 / 👎.

}

// 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.
Expand Down Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =>
Expand All @@ -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 () => {
Expand Down
Loading