Skip to content
Merged
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
1 change: 1 addition & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions packages/coding-agent/docs/compaction.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}`,
});
});
}

/**
Expand Down
1 change: 1 addition & 0 deletions packages/coding-agent/src/core/agent-session-methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ export interface AgentSessionMethodSurface {

prompt(text: string, options?: PromptOptions): Promise<void>;
_runAgentPrompt(messages: AgentMessage | AgentMessage[]): Promise<void>;
_runAgentContinue(): Promise<void>;
_continueQueuedAgentMessages(): Promise<void>;
_tryExecuteBuiltinSlashCommand(text: string): Promise<boolean>;
_tryExecuteExtensionCommand(text: string): Promise<boolean>;
Expand Down
7 changes: 7 additions & 0 deletions packages/coding-agent/src/core/agent-session-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,12 @@ export async function _runAgentPrompt(this: AgentSession, messages: AgentMessage
await this._continueQueuedAgentMessages();
}

export async function _runAgentContinue(this: AgentSession): Promise<void> {
await this.agent.continue();
await this.waitForRetry();
await this._continueQueuedAgentMessages();
}


export async function _continueQueuedAgentMessages(this: AgentSession): Promise<void> {
await this._agentEventQueue;
Expand Down Expand Up @@ -385,6 +391,7 @@ export async function sendUserMessage(this: AgentSession,
export const agentSessionPromptMethods = {
prompt,
_runAgentPrompt,
_runAgentContinue,
_continueQueuedAgentMessages,
_tryExecuteBuiltinSlashCommand,
_tryExecuteExtensionCommand,
Expand Down
1 change: 1 addition & 0 deletions packages/coding-agent/src/core/agent-session-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,14 @@ export function applyChatSessionAgentEvent<
changed = true;
break;
}
case "agent_continue_error": {
const continueError = event as Extract<AgentSessionEvent, { type: "agent_continue_error" }>;
state.sdkBusy = false;
state.statusMessage = continueError.errorMessage;
state.workingMessage = undefined;
changed = true;
break;
}
default:
changed = false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> }, "_continueQueuedAgentMessages").mockResolvedValue();

const runAutoCompaction = (
session as unknown as {
Expand All @@ -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;
Expand All @@ -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<void> }, "_continueQueuedAgentMessages").mockResolvedValue();

const runAutoCompaction = (
session as unknown as {
Expand All @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> }, "_continueQueuedAgentMessages").mockResolvedValue();

const runAutoCompaction = (
session as unknown as {
_runAutoCompaction: (reason: "overflow" | "threshold", willRetry: boolean) => Promise<void>;
}
)._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<void>;
}
)._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!;

Expand Down
Loading