From aab1dd88e0ca8a49d094dffb2942147bc6753107 Mon Sep 17 00:00:00 2001 From: Vincent Koc <25068+vincentkoc@users.noreply.github.com> Date: Fri, 19 Jun 2026 14:32:17 +0800 Subject: [PATCH] fix(copilot): defer background compaction hooks --- extensions/copilot/src/attempt.test.ts | 32 +++++++++++++++++++++ extensions/copilot/src/attempt.ts | 3 +- extensions/copilot/src/event-bridge.test.ts | 1 + extensions/copilot/src/event-bridge.ts | 6 ++++ 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/extensions/copilot/src/attempt.test.ts b/extensions/copilot/src/attempt.test.ts index fd9e1848a4e4..8371db5fc270 100644 --- a/extensions/copilot/src/attempt.test.ts +++ b/extensions/copilot/src/attempt.test.ts @@ -441,6 +441,38 @@ describe("runCopilotAttempt", () => { expect(beforeCompaction.mock.calls[0]?.[0]).not.toHaveProperty("messages"); }); + it("does not await background compaction hooks before returning a turn", async () => { + const releaseBeforeCompaction = createDeferred(); + const beforeCompaction = vi.fn(async () => releaseBeforeCompaction.promise); + initializeGlobalHookRunner( + createMockPluginRegistry([{ hookName: "before_compaction", handler: beforeCompaction }]), + ); + let activeSession: FakeSession | undefined; + const sdk = makeFakeSdk({ + onCreateSession: (session) => { + activeSession = session; + session.sendAndWait.mockImplementationOnce(async () => { + session.emit("session.compaction_start", {}); + return makeAssistantMessageEvent("done"); + }); + }, + }); + + const result = await runCopilotAttempt(makeParams(), { pool: makeFakePool(sdk) }); + + expect(result.timedOut).toBe(false); + await vi.waitFor(() => { + expect(beforeCompaction).toHaveBeenCalledTimes(1); + }); + expect(activeSession?.disconnect).not.toHaveBeenCalled(); + + releaseBeforeCompaction.resolve(); + activeSession?.emit("session.compaction_complete", { success: true }); + await vi.waitFor(() => { + expect(activeSession?.disconnect).toHaveBeenCalledTimes(1); + }); + }); + it("returns a successful turn while background compaction remains observed", async () => { vi.useFakeTimers(); const sdk = makeFakeSdk({ diff --git a/extensions/copilot/src/attempt.ts b/extensions/copilot/src/attempt.ts index 32a68e3423f3..a41bd7ae87d9 100644 --- a/extensions/copilot/src/attempt.ts +++ b/extensions/copilot/src/attempt.ts @@ -736,7 +736,6 @@ export async function runCopilotAttempt( } const result = await session.sendAndWait(messageOptions, input.timeoutMs); await bridge.awaitDeltaChain(); - await bridge.awaitCompactionChain(); if (!bridge.recordSendResult(result) && !aborted) { // SDK sendAndWait returning undefined is treated as a timeout by the // capability inventory. Do not call session.abort() here: OpenClaw may @@ -778,7 +777,7 @@ export async function runCopilotAttempt( } } finally { settled = true; - if (bridge?.isCompacting() && session && handle) { + if (bridge?.hasObservedCompaction() && session && handle) { timedOutDuringCompaction ||= timedOut; const cleanupAbort = new AbortController(); const abortCleanup = () => cleanupAbort.abort(); diff --git a/extensions/copilot/src/event-bridge.test.ts b/extensions/copilot/src/event-bridge.test.ts index dcad8057a793..e7a5fe012032 100644 --- a/extensions/copilot/src/event-bridge.test.ts +++ b/extensions/copilot/src/event-bridge.test.ts @@ -650,6 +650,7 @@ describe("attachEventBridge", () => { const completion = bridge.awaitCompactionCompletion(); await flushAsync(); + expect(bridge.hasObservedCompaction()).toBe(true); expect(complete).not.toHaveBeenCalled(); session.emit( "session.compaction_complete", diff --git a/extensions/copilot/src/event-bridge.ts b/extensions/copilot/src/event-bridge.ts index 04e137144188..d520ffa22238 100644 --- a/extensions/copilot/src/event-bridge.ts +++ b/extensions/copilot/src/event-bridge.ts @@ -70,6 +70,7 @@ export interface EventBridgeController { awaitCompactionChain(): Promise; awaitCompactionCompletion(): Promise; awaitDeltaChain(): Promise; + hasObservedCompaction(): boolean; isCompacting(): boolean; snapshot(): EventBridgeSnapshot; buildAssistantMessage(args: BuildAssistantMessageArgs): AssistantMessage | undefined; @@ -96,6 +97,7 @@ export function attachEventBridge( let startedCount = 0; let completedCount = 0; let activeCompactionCount = 0; + let observedCompaction = false; let deltaQueue = Promise.resolve(); let deltaChain = Promise.resolve(); let compactionChain = Promise.resolve(); @@ -182,6 +184,7 @@ export function attachEventBridge( }); registerListener(session, unsubscribeFns, "session.compaction_start", () => { + observedCompaction = true; if (activeCompactionCount === 0) { compactionIdle = new Promise((resolve) => { resolveCompactionIdle = resolve; @@ -248,6 +251,9 @@ export function attachEventBridge( awaitDeltaChain() { return deltaChain; }, + hasObservedCompaction() { + return observedCompaction; + }, isCompacting() { return activeCompactionCount > 0; },