From 54dddda68db6fd4e07cdcb72f1e387bcdf907a99 Mon Sep 17 00:00:00 2001 From: Vincent Koc <25068+vincentkoc@users.noreply.github.com> Date: Sun, 21 Jun 2026 01:34:33 +0800 Subject: [PATCH] fix(copilot): preserve replacement session reuse --- extensions/copilot/harness.test.ts | 45 ++++++++++++++++++++++++++++++ extensions/copilot/harness.ts | 6 ++-- extensions/copilot/src/attempt.ts | 2 ++ 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/extensions/copilot/harness.test.ts b/extensions/copilot/harness.test.ts index a3b72f14868..eac70885726 100644 --- a/extensions/copilot/harness.test.ts +++ b/extensions/copilot/harness.test.ts @@ -1551,6 +1551,51 @@ describe("createCopilotAgentHarness", () => { await flushAsyncWork(); }); + it("clears the reset block when storing a replacement session fails", async () => { + const cleanup = createDeferred<"aborted" | "completed" | "deadline">(); + const sessionStore = makeSessionStoreMock(); + sessionStore.store.register.mockImplementation(() => { + throw new Error("sqlite register failed"); + }); + mocks.runCopilotAttempt.mockImplementation(async (_params, deps) => { + const sdkSessionId = + mocks.runCopilotAttempt.mock.calls.length === 1 + ? "sdk-sess-background" + : "sdk-sess-replacement"; + deps.onSessionEstablished?.({ + sdkSessionId, + pooledClient: { key: {} as any, client: {} as any }, + sessionConfig: TEST_SESSION_CONFIG, + }); + if (sdkSessionId === "sdk-sess-background") { + deps.onDeferredCompaction?.({ + abort: () => undefined, + cleanup: cleanup.promise, + sdkSessionId, + }); + } + return ATTEMPT_RESULT; + }); + const harness = createCopilotAgentHarness({ + pool: makePoolMock(), + sessionStore: sessionStore.store, + }); + const params = makeCompactParams({ sessionId: "oc-sess-store-failure" }); + + await harness.runAttempt(params); + await harness.runAttempt(params); + await harness.runAttempt(params); + + expect(mocks.runCopilotAttempt.mock.calls[1]?.[0]).not.toMatchObject({ + initialReplayState: expect.objectContaining({ sdkSessionId: "sdk-sess-background" }), + }); + expect(mocks.runCopilotAttempt.mock.calls[2]?.[0]).toMatchObject({ + initialReplayState: expect.objectContaining({ sdkSessionId: "sdk-sess-replacement" }), + }); + cleanup.resolve("completed"); + await flushAsyncWork(); + }); + it("calls the SDK history compaction RPC without requiring a workspace sidecar", async () => { const beforeCompaction = vi.fn(); const afterCompaction = vi.fn(); diff --git a/extensions/copilot/harness.ts b/extensions/copilot/harness.ts index e6aa2bde8ec..02b0a1795e3 100644 --- a/extensions/copilot/harness.ts +++ b/extensions/copilot/harness.ts @@ -621,7 +621,7 @@ export function createCopilotAgentHarness( sessionConfig, ...sessionAuthFields(poolAcquire.auth), }); - const persisted = registerStoredBinding(options?.sessionStore, openclawSessionId, { + registerStoredBinding(options?.sessionStore, openclawSessionId, { schemaVersion: 2, sdkSessionId, compatKey: currentCompatKey, @@ -629,9 +629,7 @@ export function createCopilotAgentHarness( ...sessionAuthFields(poolAcquire.auth), updatedAt: Date.now(), }); - if (persisted) { - resetBlockedStoredSessions.delete(openclawSessionId); - } + resetBlockedStoredSessions.delete(openclawSessionId); } : undefined, onDeferredCompaction: openclawSessionId diff --git a/extensions/copilot/src/attempt.ts b/extensions/copilot/src/attempt.ts index bddcaa6fce7..cf848a8e094 100644 --- a/extensions/copilot/src/attempt.ts +++ b/extensions/copilot/src/attempt.ts @@ -837,6 +837,8 @@ export async function runCopilotAttempt( } params.abortSignal?.removeEventListener("abort", onAbort); } else { + // `sendAndWait` resolves on `session.idle`, which the SDK defines as + // no background agents in flight. Only an observed compaction needs retention. await bridge?.awaitCompactionChain(); bridge?.detach(); params.abortSignal?.removeEventListener("abort", onAbort);