diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index a18facc40..7ccb50119 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed recoverable context-overflow compaction keeping the failed assistant error turn in visible session history after scheduling the retry. ([#3747](https://github.com/can1357/oh-my-pi/issues/3747)) + ## [16.2.4] - 2026-06-28 ### Fixed diff --git a/packages/coding-agent/src/session/agent-session.ts b/packages/coding-agent/src/session/agent-session.ts index a7b9bc198..b74012f00 100644 --- a/packages/coding-agent/src/session/agent-session.ts +++ b/packages/coding-agent/src/session/agent-session.ts @@ -9482,12 +9482,7 @@ export class AgentSession { const errorIsFromBeforeCompaction = compactionEntry !== null && assistantMessage.timestamp < new Date(compactionEntry.timestamp).getTime(); if (sameModel && !errorIsFromBeforeCompaction && AIError.isContextOverflow(assistantMessage, contextWindow)) { - // Remove the error message from agent state (it IS saved to session for history, - // but we don't want it in context for the retry) - const messages = this.agent.state.messages; - if (messages.length > 0 && messages[messages.length - 1].role === "assistant") { - this.agent.replaceMessages(messages.slice(0, -1)); - } + await this.#discardRecoverableAssistantTurn(assistantMessage); // Try context promotion first - switch to a larger model and retry without compacting const promoted = await this.#tryContextPromotion(assistantMessage); @@ -9512,10 +9507,7 @@ export class AgentSession { // otherwise compaction/handoff. Unlike overflow, the *input* is fine, so we // allow the handoff strategy to actually run. if (sameModel && !errorIsFromBeforeCompaction && assistantMessage.stopReason === "length") { - const messages = this.agent.state.messages; - if (messages.length > 0 && messages[messages.length - 1].role === "assistant") { - this.agent.replaceMessages(messages.slice(0, -1)); - } + await this.#discardRecoverableAssistantTurn(assistantMessage); const promoted = await this.#tryContextPromotion(assistantMessage); if (promoted) { @@ -9797,6 +9789,11 @@ export class AgentSession { } } + async #discardRecoverableAssistantTurn(assistantMessage: AssistantMessage): Promise { + await this.#waitForSessionMessagePersistence(assistantMessage); + this.#discardAssistantTurn(assistantMessage); + } + /** * Drop an assistant turn from BOTH the live agent context and the persisted * session branch (reparenting the leaf to the turn's parent), so a discarded diff --git a/packages/coding-agent/test/agent-session-auto-compaction-progress-guard.test.ts b/packages/coding-agent/test/agent-session-auto-compaction-progress-guard.test.ts index ff0e70b4d..c49592c7f 100644 --- a/packages/coding-agent/test/agent-session-auto-compaction-progress-guard.test.ts +++ b/packages/coding-agent/test/agent-session-auto-compaction-progress-guard.test.ts @@ -382,6 +382,37 @@ describe("AgentSession auto-compaction progress guard", () => { expect(noProgress.length).toBe(0); }); + it("removes the visible overflow error before retrying after compaction", async () => { + seedPriorTurns(); + const continueSpy = vi.spyOn(session.agent, "continue").mockResolvedValue(); + vi.spyOn(session.agent, "prompt").mockResolvedValue(undefined as never); + vi.spyOn(session, "getContextUsage").mockReturnValue({ tokens: 150000, contextWindow: 200000, percent: 75 }); + + const { promise: compactionDone, resolve: onCompactionDone } = Promise.withResolvers(); + 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(continueSpy).toHaveBeenCalledTimes(1); + expect(sessionManager.getBranch()).not.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