diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index e01030390..cc1cd055f 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -7,6 +7,7 @@ ### Fixed - Disabled Undici's default 10-second connect timeout in Atomic's global proxy-aware HTTP dispatcher so headless or sandboxed runs behind policy proxies can wait for slow provider CONNECT establishment instead of surfacing spurious `Connection error.` failures. +- Resumed post-compaction queued work through the full agent continuation lifecycle and surfaced continuation failures, preventing sessions from appearing dead after auto-compaction or failed tool-call recovery ([#1570](https://github.com/bastani-inc/atomic/issues/1570)). ## [0.9.4-alpha.3] - 2026-06-30 diff --git a/packages/coding-agent/docs/compaction.md b/packages/coding-agent/docs/compaction.md index 98fc8d402..cc4ad72c3 100644 --- a/packages/coding-agent/docs/compaction.md +++ b/packages/coding-agent/docs/compaction.md @@ -100,6 +100,8 @@ By default, `reserveTokens` is 16384 tokens. Configure it in `~/.atomic/agent/se You can also trigger compaction manually with `/compact`. Custom summary instructions are not accepted because Verbatim Compaction is deletion-only and retained transcript content stays verbatim. +If auto-compaction runs while a turn still has queued work (for example a failed tool-call result or a follow-up queued during compaction), Atomic resumes through the same continuation lifecycle as a normal queued turn: provider retry handling runs, additional queued messages drain, and any post-compaction resume failure is surfaced instead of being swallowed silently. + ### Image Context and Compaction Image content blocks (screenshots, pasted images, image-bearing tool results) are expensive: providers fold image tokens into their reported prompt/input usage, so image-heavy conversations reach the compaction threshold sooner. Atomic accounts for this in two complementary ways: diff --git a/packages/coding-agent/src/core/agent-session-auto-compaction.ts b/packages/coding-agent/src/core/agent-session-auto-compaction.ts index 56d13b705..21de0a43b 100644 --- a/packages/coding-agent/src/core/agent-session-auto-compaction.ts +++ b/packages/coding-agent/src/core/agent-session-auto-compaction.ts @@ -157,7 +157,14 @@ export function _schedulePostAutoCompactionContinuationProbe(this: AgentSession, */ export function _resumeAfterAutoCompaction(this: AgentSession): void { - this.agent.continue().catch(() => {}); + void this._runAgentContinue().catch((error) => { + const message = error instanceof Error ? error.message : String(error); + this._emit({ + type: "agent_continue_error", + source: "post_compaction", + errorMessage: `Post-compaction continuation failed: ${message}`, + }); + }); } /** diff --git a/packages/coding-agent/src/core/agent-session-methods.ts b/packages/coding-agent/src/core/agent-session-methods.ts index 6f4692ad6..e4490afd4 100644 --- a/packages/coding-agent/src/core/agent-session-methods.ts +++ b/packages/coding-agent/src/core/agent-session-methods.ts @@ -132,6 +132,7 @@ export interface AgentSessionMethodSurface { prompt(text: string, options?: PromptOptions): Promise; _runAgentPrompt(messages: AgentMessage | AgentMessage[]): Promise; + _runAgentContinue(): Promise; _continueQueuedAgentMessages(): Promise; _tryExecuteBuiltinSlashCommand(text: string): Promise; _tryExecuteExtensionCommand(text: string): Promise; diff --git a/packages/coding-agent/src/core/agent-session-prompt.ts b/packages/coding-agent/src/core/agent-session-prompt.ts index 0245bf133..05c28d372 100644 --- a/packages/coding-agent/src/core/agent-session-prompt.ts +++ b/packages/coding-agent/src/core/agent-session-prompt.ts @@ -185,6 +185,12 @@ export async function _runAgentPrompt(this: AgentSession, messages: AgentMessage await this._continueQueuedAgentMessages(); } +export async function _runAgentContinue(this: AgentSession): Promise { + await this.agent.continue(); + await this.waitForRetry(); + await this._continueQueuedAgentMessages(); +} + export async function _continueQueuedAgentMessages(this: AgentSession): Promise { await this._agentEventQueue; @@ -385,6 +391,7 @@ export async function sendUserMessage(this: AgentSession, export const agentSessionPromptMethods = { prompt, _runAgentPrompt, + _runAgentContinue, _continueQueuedAgentMessages, _tryExecuteBuiltinSlashCommand, _tryExecuteExtensionCommand, diff --git a/packages/coding-agent/src/core/agent-session-types.ts b/packages/coding-agent/src/core/agent-session-types.ts index 64483333e..91751bca5 100644 --- a/packages/coding-agent/src/core/agent-session-types.ts +++ b/packages/coding-agent/src/core/agent-session-types.ts @@ -60,6 +60,7 @@ export type AgentSessionEvent = willRetry: false; errorMessage?: string; } + | { type: "agent_continue_error"; source: "post_compaction"; errorMessage: string } | { type: "auto_retry_start"; attempt: number; maxAttempts: number; delayMs: number; errorMessage: string } | { type: "auto_retry_end"; success: boolean; attempt: number; finalError?: string }; diff --git a/packages/coding-agent/src/modes/interactive/components/chat-session-host-events.ts b/packages/coding-agent/src/modes/interactive/components/chat-session-host-events.ts index c3c6389f7..bbf88c7db 100644 --- a/packages/coding-agent/src/modes/interactive/components/chat-session-host-events.ts +++ b/packages/coding-agent/src/modes/interactive/components/chat-session-host-events.ts @@ -130,6 +130,14 @@ export function applyChatSessionAgentEvent< changed = true; break; } + case "agent_continue_error": { + const continueError = event as Extract; + state.sdkBusy = false; + state.statusMessage = continueError.errorMessage; + state.workingMessage = undefined; + changed = true; + break; + } default: changed = false; } diff --git a/packages/coding-agent/src/modes/interactive/interactive-agent-events.ts b/packages/coding-agent/src/modes/interactive/interactive-agent-events.ts index 133b4998b..f304d627b 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-agent-events.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-agent-events.ts @@ -429,6 +429,12 @@ InteractiveModeBase.prototype.handleEvent = async function(this: InteractiveMode this.ui.requestRender(); break; } + + case "agent_continue_error": { + this.showError(event.errorMessage); + this.ui.requestRender(); + break; + } } }; diff --git a/packages/coding-agent/test/agent-session-auto-compaction-queue-01.suite.ts b/packages/coding-agent/test/agent-session-auto-compaction-queue-01.suite.ts index 3f3ab0234..97d51a267 100644 --- a/packages/coding-agent/test/agent-session-auto-compaction-queue-01.suite.ts +++ b/packages/coding-agent/test/agent-session-auto-compaction-queue-01.suite.ts @@ -150,6 +150,7 @@ describe("AgentSession auto-compaction queue resume", () => { expect(session.agent.hasQueuedMessages()).toBe(true); const continueSpy = vi.spyOn(session.agent, "continue").mockResolvedValue(); + const drainSpy = vi.spyOn(session as unknown as { _continueQueuedAgentMessages: () => Promise }, "_continueQueuedAgentMessages").mockResolvedValue(); const runAutoCompaction = ( session as unknown as { @@ -161,6 +162,7 @@ describe("AgentSession auto-compaction queue resume", () => { await vi.advanceTimersByTimeAsync(100); expect(continueSpy).toHaveBeenCalledTimes(1); + expect(drainSpy).toHaveBeenCalledTimes(1); }); it("should resume when compaction_end listener asynchronously queues work before the deferred probe", async () => { let queuedAtCompactionEnd: boolean | undefined; @@ -184,6 +186,7 @@ describe("AgentSession auto-compaction queue resume", () => { expect(session.agent.hasQueuedMessages()).toBe(false); const continueSpy = vi.spyOn(session.agent, "continue").mockResolvedValue(); + const drainSpy = vi.spyOn(session as unknown as { _continueQueuedAgentMessages: () => Promise }, "_continueQueuedAgentMessages").mockResolvedValue(); const runAutoCompaction = ( session as unknown as { @@ -202,6 +205,7 @@ describe("AgentSession auto-compaction queue resume", () => { await vi.advanceTimersByTimeAsync(100); expect(continueSpy).toHaveBeenCalledTimes(1); + expect(drainSpy).toHaveBeenCalledTimes(1); }); it("should suppress deferred continuation when streaming starts before the probe", async () => { session.agent.followUp({ diff --git a/packages/coding-agent/test/agent-session-auto-compaction-queue-02.suite.ts b/packages/coding-agent/test/agent-session-auto-compaction-queue-02.suite.ts index 76cc8c9ba..0c2d3b295 100644 --- a/packages/coding-agent/test/agent-session-auto-compaction-queue-02.suite.ts +++ b/packages/coding-agent/test/agent-session-auto-compaction-queue-02.suite.ts @@ -124,6 +124,58 @@ describe("AgentSession auto-compaction queue resume", () => { } }); + it("should run the full continuation lifecycle after threshold compaction resume", async () => { + session.agent.followUp({ + role: "custom", + customType: "test", + content: [{ type: "text", text: "Queued custom" }], + display: false, + timestamp: Date.now(), + }); + const continueSpy = vi.spyOn(session.agent, "continue").mockResolvedValue(); + const waitSpy = vi.spyOn(session, "waitForRetry").mockResolvedValue(); + const drainSpy = vi.spyOn(session as unknown as { _continueQueuedAgentMessages: () => Promise }, "_continueQueuedAgentMessages").mockResolvedValue(); + + const runAutoCompaction = ( + session as unknown as { + _runAutoCompaction: (reason: "overflow" | "threshold", willRetry: boolean) => Promise; + } + )._runAutoCompaction.bind(session); + + await runAutoCompaction("threshold", false); + await vi.advanceTimersByTimeAsync(100); + + expect(continueSpy).toHaveBeenCalledTimes(1); + expect(waitSpy).toHaveBeenCalledTimes(1); + expect(drainSpy).toHaveBeenCalledTimes(1); + }); + + it("should surface post-compaction continuation failures", async () => { + session.agent.followUp({ + role: "custom", + customType: "test", + content: [{ type: "text", text: "Queued custom" }], + display: false, + timestamp: Date.now(), + }); + const errors: string[] = []; + session.subscribe((event) => { + if (event.type === "agent_continue_error") errors.push(event.errorMessage); + }); + vi.spyOn(session.agent, "continue").mockRejectedValue(new Error("boom")); + + const runAutoCompaction = ( + session as unknown as { + _runAutoCompaction: (reason: "overflow" | "threshold", willRetry: boolean) => Promise; + } + )._runAutoCompaction.bind(session); + + await runAutoCompaction("threshold", false); + await vi.advanceTimersByTimeAsync(100); + + expect(errors).toEqual(["Post-compaction continuation failed: boom"]); + }); + it("should trigger threshold compaction for error messages using last successful usage", async () => { const model = session.model!;