Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/adapters/cursor/checkpoint-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ export function commitCursorCheckpoint(input: {
}

export function getCursorCheckpointForPrefix(input: {
conversationId: string;
prefixDigest: string;
systemDigest: string;
coveredMessageCount: number;
Expand All @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion src/adapters/cursor/request-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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" };
Expand Down
46 changes: 43 additions & 3 deletions tests/cursor-request-builder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down Expand Up @@ -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();
});
Expand Down Expand Up @@ -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 },
Expand All @@ -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();
});
});
Loading