From 7c45d9dca5afbae4960abc008ab4c249e2b68e29 Mon Sep 17 00:00:00 2001 From: luvs01 Date: Sun, 23 Aug 2026 10:19:30 +0900 Subject: [PATCH] fix(cursor): bind prefix checkpoints to conversations --- src/adapters/cursor/checkpoint-store.ts | 2 ++ src/adapters/cursor/request-builder.ts | 3 +- tests/cursor-request-builder.test.ts | 46 +++++++++++++++++++++++-- 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/adapters/cursor/checkpoint-store.ts b/src/adapters/cursor/checkpoint-store.ts index 337b07caa5..d34dc2ae9c 100644 --- a/src/adapters/cursor/checkpoint-store.ts +++ b/src/adapters/cursor/checkpoint-store.ts @@ -236,6 +236,7 @@ export function commitCursorCheckpoint(input: { } export function getCursorCheckpointForPrefix(input: { + conversationId: string; prefixDigest: string; systemDigest: string; coveredMessageCount: number; @@ -250,6 +251,7 @@ export function getCursorCheckpointForPrefix(input: { const snapshot = getCursorCheckpoint(ref); if (!snapshot) return undefined; const identityScope = input.identityScope?.trim() || "local"; + if (snapshot.conversationId !== input.conversationId) return undefined; if (snapshot.systemDigest !== input.systemDigest) return undefined; if (snapshot.coveredMessageCount !== input.coveredMessageCount) return undefined; if (snapshot.identityScope !== identityScope) return undefined; diff --git a/src/adapters/cursor/request-builder.ts b/src/adapters/cursor/request-builder.ts index 9d0e83cbbf..94f73df029 100644 --- a/src/adapters/cursor/request-builder.ts +++ b/src/adapters/cursor/request-builder.ts @@ -361,6 +361,7 @@ function lookupPrefixSnapshot( const modelId = cursorCheckpointModelAffinityId(request.modelId); for (let covered = parsed.context.messages.length; covered >= 1; covered--) { const snapshot = getCursorCheckpointForPrefix({ + conversationId: request.conversationId, prefixDigest: cursorCoveredPrefixDigest(parsed, covered), systemDigest, coveredMessageCount: covered, @@ -407,7 +408,7 @@ function resolveCursorCheckpoint( snapshot = lookupPrefixSnapshot(parsed, request, identityScope); if (!snapshot) return { reason: "missing_ref" }; } - if (!isolated && snapshot.conversationId !== request.conversationId && ref) { + if (!isolated && snapshot.conversationId !== request.conversationId) { return { reason: "conversation_changed" }; } if (snapshot.identityScope !== identityScope) return { reason: "identity_changed" }; diff --git a/tests/cursor-request-builder.test.ts b/tests/cursor-request-builder.test.ts index 21548e26f4..d8ee7ed56f 100644 --- a/tests/cursor-request-builder.test.ts +++ b/tests/cursor-request-builder.test.ts @@ -184,7 +184,7 @@ describe("Cursor request builder", () => { expect(helper.conversationId).not.toBe(main.conversationId); }); - test("isolated helper turns keep their own cache and never reuse the parent checkpoint", () => { + test("isolated helper turns never reuse parent or sibling checkpoints", () => { clearCursorCheckpointsForTests(); const parentBytes = toBinary(ConversationStateStructureSchema, create(ConversationStateStructureSchema, { pendingToolCalls: ["parent-fixture"], @@ -254,8 +254,9 @@ describe("Cursor request builder", () => { }, }); expect(second.conversationId).not.toBe("cursor_parent_real"); - expect(second.continuationMode).toBe("checkpoint"); - expect(second.checkpointBytes?.byteLength).toBe(helperBytes.byteLength); + expect(second.continuationMode).toBe("full-replay"); + expect(second.checkpointInvalidationReason).toBe("missing_ref"); + expect(second.checkpointBytes).toBeUndefined(); expect(getCursorCheckpoint(parentRef)?.ref).toBe(parentRef); clearCursorCheckpointsForTests(); }); @@ -1224,6 +1225,7 @@ describe("Cursor request builder", () => { })).toBeDefined(); const followUp = createCursorRequest({ ...firstTurn, + _cursorConversationId: built.conversationId, context: { messages: [ { role: "user", content: "unique sol prompt 7f3c", timestamp: 1 }, @@ -1236,4 +1238,42 @@ describe("Cursor request builder", () => { expect(followUp.checkpointBytes?.byteLength).toBe(checkpointBytes.byteLength); clearCursorCheckpointsForTests(); }); + + test("does not reuse a unique covered prefix from an unrelated conversation", () => { + clearCursorCheckpointsForTests(); + const firstTurn = { + ...base, + modelId: "cursor/gpt-5.6-sol", + _cursorIdentityScope: "acct-1", + context: { messages: [{ role: "user" as const, content: "shared prefix", timestamp: 1 }] }, + }; + const checkpointBytes = toBinary(ConversationStateStructureSchema, create(ConversationStateStructureSchema, { + pendingToolCalls: ["private-state"], + })); + expect(commitCursorCheckpoint({ + conversationId: "cursor_original", + identityScope: "acct-1", + modelId: cursorCheckpointModelAffinityId(createCursorRequest(firstTurn).modelId), + checkpointBytes, + coveredMessageCount: 1, + prefixDigest: cursorCoveredPrefixDigest(firstTurn, 1), + systemDigest: cursorInstructionDigest(firstTurn), + })).toBeDefined(); + + const unrelated = createCursorRequest({ + ...firstTurn, + _cursorConversationId: "cursor_unrelated", + context: { + messages: [ + ...firstTurn.context.messages, + { role: "assistant" as const, content: [{ type: "text" as const, text: "reply" }], timestamp: 2 }, + { role: "user" as const, content: "continue", timestamp: 3 }, + ], + }, + }); + expect(unrelated.continuationMode).toBe("full-replay"); + expect(unrelated.checkpointInvalidationReason).toBe("missing_ref"); + expect(unrelated.checkpointBytes).toBeUndefined(); + clearCursorCheckpointsForTests(); + }); });