From 2ec4401bcda9da7401ac33f09a513e2fdf904484 Mon Sep 17 00:00:00 2001 From: can1357 Date: Mon, 9 Mar 2026 15:54:24 +0100 Subject: [PATCH] fix: isolate auto-compaction abort state Fixes #275 --- .../coding-agent/src/session/agent-session.ts | 28 ++++++++++--------- ...gent-session-auto-compaction-queue.test.ts | 23 +++++++-------- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/packages/coding-agent/src/session/agent-session.ts b/packages/coding-agent/src/session/agent-session.ts index 8ad0527b6..428956f86 100644 --- a/packages/coding-agent/src/session/agent-session.ts +++ b/packages/coding-agent/src/session/agent-session.ts @@ -3726,11 +3726,11 @@ export class AgentSession { let action: "context-full" | "handoff" = compactionSettings.strategy === "handoff" && reason !== "overflow" ? "handoff" : "context-full"; await this.#emitSessionEvent({ type: "auto_compaction_start", reason, action }); - // Properly abort and null existing controller before replacing - if (this.#autoCompactionAbortController) { - this.#autoCompactionAbortController.abort(); - } - this.#autoCompactionAbortController = new AbortController(); + // Abort any older auto-compaction before installing this run's controller. + this.#autoCompactionAbortController?.abort(); + const autoCompactionAbortController = new AbortController(); + this.#autoCompactionAbortController = autoCompactionAbortController; + const autoCompactionSignal = autoCompactionAbortController.signal; try { if (compactionSettings.strategy === "handoff" && reason !== "overflow") { @@ -3740,7 +3740,7 @@ export class AgentSession { signal: this.#autoCompactionAbortController.signal, }); if (!handoffResult) { - const aborted = this.#autoCompactionAbortController.signal.aborted; + const aborted = autoCompactionSignal.aborted; if (aborted) { await this.#emitSessionEvent({ type: "auto_compaction_end", @@ -3824,7 +3824,7 @@ export class AgentSession { preparation, branchEntries: pathEntries, customInstructions: undefined, - signal: this.#autoCompactionAbortController.signal, + signal: autoCompactionSignal, })) as SessionBeforeCompactResult | undefined; if (hookResult?.cancel) { @@ -3889,7 +3889,7 @@ export class AgentSession { candidate, apiKey, undefined, - this.#autoCompactionAbortController.signal, + autoCompactionSignal, { promptOverride: hookPrompt, extraContext: hookContext, @@ -3898,7 +3898,7 @@ export class AgentSession { ); break; } catch (error) { - if (this.#autoCompactionAbortController.signal.aborted) { + if (autoCompactionSignal.aborted) { throw error; } @@ -3942,7 +3942,7 @@ export class AgentSession { error: message, model: `${candidate.provider}/${candidate.id}`, }); - await abortableSleep(delayMs, this.#autoCompactionAbortController.signal); + await abortableSleep(delayMs, autoCompactionSignal); } } @@ -3966,7 +3966,7 @@ export class AgentSession { preserveData = { ...(preserveData ?? {}), ...(compactResult.preserveData ?? {}) }; } - if (this.#autoCompactionAbortController.signal.aborted) { + if (autoCompactionSignal.aborted) { await this.#emitSessionEvent({ type: "auto_compaction_end", action, @@ -4056,7 +4056,7 @@ export class AgentSession { }); } } catch (error) { - if (this.#autoCompactionAbortController?.signal.aborted) { + if (autoCompactionSignal.aborted) { await this.#emitSessionEvent({ type: "auto_compaction_end", action, @@ -4079,7 +4079,9 @@ export class AgentSession { : `Auto-compaction failed: ${errorMessage}`, }); } finally { - this.#autoCompactionAbortController = undefined; + if (this.#autoCompactionAbortController === autoCompactionAbortController) { + this.#autoCompactionAbortController = undefined; + } } } diff --git a/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts b/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts index 93bceabe6..8e1991db9 100644 --- a/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts +++ b/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts @@ -25,8 +25,8 @@ function getRuntimeSignals(): string[] { } /** - * Regression test: auto-compaction completion should resume the agent loop when - * there are queued agent-level messages (follow-up/steering/custom). + * Regression test: auto-compaction completion should resume the agent loop when + * there are queued agent-level messages (follow-up/steering/custom). */ describe("AgentSession auto-compaction queue resume", () => { let tempDir: TempDir; @@ -60,17 +60,17 @@ describe("AgentSession auto-compaction queue resume", () => { "\t\t};", "\t});", '\tpi.on("auto_compaction_start", async (event) => {', - `\t\tconst signals = globalThis.${runtimeSignalStoreKey} ?? (globalThis.${runtimeSignalStoreKey} = []);`, - '\t\tsignals.push("compaction:start:" + event.reason);', - "\t});", + `\t\tconst signals = globalThis.${runtimeSignalStoreKey} ?? (globalThis.${runtimeSignalStoreKey} = []);`, + '\t\tsignals.push("compaction:start:" + event.reason);', + "\t});", '\tpi.on("auto_compaction_end", async (event) => {', - `\t\tconst signals = globalThis.${runtimeSignalStoreKey} ?? (globalThis.${runtimeSignalStoreKey} = []);`, - '\t\tsignals.push("compaction:end:" + (event.aborted ? "aborted" : "ok"));', - "\t});", + `\t\tconst signals = globalThis.${runtimeSignalStoreKey} ?? (globalThis.${runtimeSignalStoreKey} = []);`, + '\t\tsignals.push("compaction:end:" + (event.aborted ? "aborted" : "ok"));', + "\t});", '\tpi.on("todo_reminder", async (event) => {', - `\t\tconst signals = globalThis.${runtimeSignalStoreKey} ?? (globalThis.${runtimeSignalStoreKey} = []);`, - '\t\tsignals.push("todo:" + event.attempt + "/" + event.maxAttempts);', - "\t});", + `\t\tconst signals = globalThis.${runtimeSignalStoreKey} ?? (globalThis.${runtimeSignalStoreKey} = []);`, + '\t\tsignals.push("todo:" + event.attempt + "/" + event.maxAttempts);', + "\t});", "}", ].join("\n"), ); @@ -239,4 +239,5 @@ describe("AgentSession auto-compaction queue resume", () => { expect(getRuntimeSignals()).toContain("todo:1/3"); expect(continueSpy).toHaveBeenCalledTimes(1); }); + });