fix(session): restore overflow turn when compaction skips

Drop the persisted assistant error before #runAutoCompaction so the kept region is clean, then re-append it on COMPACTION_CHECK_NONE without a fresh compaction entry — covers no-model, hook-cancel, and compaction-error paths. Same rollback for response.incomplete recovery.

Fixes #3747
This commit is contained in:
roboomp
2026-06-28 19:51:12 +00:00
parent 9f79d09a8d
commit 4394184331
2 changed files with 82 additions and 7 deletions
@@ -443,6 +443,42 @@ describe("AgentSession auto-compaction progress guard", () => {
);
});
it("restores the persisted overflow error when compaction skips without committing", async () => {
// `#runAutoCompaction` returning COMPACTION_CHECK_NONE without writing a
// compaction summary (no available model, hook cancel, compaction error)
// MUST NOT erase the user-visible assistant error: the transcript would
// otherwise lose the only explanation of why the turn stopped.
seedPriorTurns();
vi.spyOn(modelRegistry, "getAvailable").mockReturnValue([]);
const promptSpy = vi.spyOn(session.agent, "prompt").mockResolvedValue(undefined as never);
const continueSpy = vi.spyOn(session.agent, "continue").mockResolvedValue();
const { promise: compactionDone, resolve: onCompactionDone } = Promise.withResolvers<void>();
session.subscribe(event => {
if (event.type === "auto_compaction_end") onCompactionDone();
});
const assistantMsg = overflowAssistant();
session.agent.emitExternalEvent({ type: "message_end", message: assistantMsg });
session.agent.emitExternalEvent({ type: "agent_end", messages: [assistantMsg] });
await compactionDone;
await session.waitForIdle();
expect(promptSpy).not.toHaveBeenCalled();
expect(continueSpy).not.toHaveBeenCalled();
expect(sessionManager.getBranch()).toContainEqual(
expect.objectContaining({
type: "message",
message: expect.objectContaining({
role: "assistant",
stopReason: "error",
errorMessage: assistantMsg.errorMessage,
}),
}),
);
});
it("retries a small-window overflow when the default reserve exceeds the model window", async () => {
// Bundled 4k/8k models can be smaller than the default absolute reserve
// (16,384). Retry fit must clamp that reserve; otherwise the budget goes