fix(copilot): defer background compaction hooks

This commit is contained in:
Vincent Koc
2026-06-19 14:32:17 +08:00
committed by Vincent Koc
parent a81a505c72
commit aab1dd88e0
4 changed files with 40 additions and 2 deletions
+32
View File
@@ -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<void>();
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({
+1 -2
View File
@@ -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();
@@ -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",
+6
View File
@@ -70,6 +70,7 @@ export interface EventBridgeController {
awaitCompactionChain(): Promise<void>;
awaitCompactionCompletion(): Promise<void>;
awaitDeltaChain(): Promise<void>;
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<void>((resolve) => {
resolveCompactionIdle = resolve;
@@ -248,6 +251,9 @@ export function attachEventBridge(
awaitDeltaChain() {
return deltaChain;
},
hasObservedCompaction() {
return observedCompaction;
},
isCompacting() {
return activeCompactionCount > 0;
},