-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat(cursor): commit tool-suspended checkpoints for external models #2652
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4d85df3
6efe59a
69a0b71
58313f0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -146,11 +146,20 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda | |
| let completedNormally = false; | ||
| let lastTransport: { captured?: Uint8Array } | undefined; | ||
| let emittedClientTool = false; | ||
| // Ordering proof for tool-suspended checkpoints: true only when the newest captured | ||
| // checkpoint bytes arrived AFTER the turn emitted a client tool call, i.e. upstream | ||
| // serialized its suspended-on-tool-call state. Only that snapshot can safely resume | ||
| // with the covered-prefix + trailing-toolResult path (devlog 260826 050). | ||
| let capturedAfterClientTool = false; | ||
|
|
||
| const commitCapturedCheckpoint = (activeRequest: ReturnType<typeof createCursorRequest>): void => { | ||
| const toolSuspendedCommit = | ||
| emittedClientTool | ||
| && capturedAfterClientTool | ||
| && isCursorExternalWireModel(activeRequest.modelId); | ||
| if ( | ||
| replayUnsafe | ||
| || emittedClientTool | ||
| || (emittedClientTool && !toolSuspendedCommit) | ||
| || activeRequest.contextUsageStoreCheckpoints === false | ||
| || !lastTransport?.captured | ||
| || lastTransport.captured.byteLength === 0 | ||
|
|
@@ -173,7 +182,9 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda | |
| cursor: { | ||
| ...(_parsed._providerContinuation?.cursor ?? {}), | ||
| conversationId: activeRequest.conversationId, | ||
| checkpointUsable: true, | ||
| // A tool-suspended checkpoint is only usable by the immediate trailing-toolResult | ||
| // continuation; the request-builder guard keys on checkpointUsable=false for that. | ||
| checkpointUsable: !toolSuspendedCommit, | ||
| checkpointRef, | ||
| }, | ||
| }; | ||
|
|
@@ -183,6 +194,7 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda | |
| checkpointRefHash: cursorCheckpointRefHash(checkpointRef), | ||
| checkpointBytes: lastTransport.captured.byteLength, | ||
| wireModel: activeRequest.modelId, | ||
| ...(toolSuspendedCommit ? { toolSuspended: true } : {}), | ||
| }); | ||
| }; | ||
|
|
||
|
|
@@ -208,7 +220,10 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda | |
| if (message.type === "done") completedNormally = true; | ||
| if (message.type === "tool_call_end") emittedClientTool = true; | ||
| const captured = capturedCursorCheckpointBytes(activeTransport); | ||
| if (captured) lastTransport = { captured }; | ||
| if (captured) { | ||
| if (captured !== lastTransport?.captured) capturedAfterClientTool = emittedClientTool; | ||
|
Comment on lines
221
to
+224
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
With multiple client tool calls, a checkpoint captured after the first AGENTS.md reference: src/AGENTS.md:L19-L19 Useful? React with 👍 / 👎. |
||
| lastTransport = { captured }; | ||
| } | ||
| const events = mapCursorServerMessage(message, { | ||
| kv, | ||
| writeClient: clientMessage => { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| import { describe, expect, test } from "bun:test"; | ||
| import { createCursorRequest } from "../src/adapters/cursor/request-builder"; | ||
| import type { OcxParsedRequest } from "../src/types/request"; | ||
|
|
||
| function parsedRequest(overrides: Partial<OcxParsedRequest> = {}): OcxParsedRequest { | ||
| return { | ||
| modelId: "cursor/grok-4.6", | ||
| context: { | ||
| systemPrompt: [], | ||
| messages: [{ role: "user", content: "hi" }], | ||
| tools: undefined, | ||
| }, | ||
| options: {}, | ||
| ...overrides, | ||
| } as OcxParsedRequest; | ||
| } | ||
|
|
||
| const CALLER_TOOL = { | ||
| name: "get_weather", | ||
| description: "d", | ||
| parameters: { type: "object", properties: {} }, | ||
| } as const; | ||
|
|
||
| describe("cursor default-catalog suppression (preamble floor)", () => { | ||
| test("bare request with no tools and no thread identity sets the flag", () => { | ||
| const request = createCursorRequest(parsedRequest()); | ||
| expect(request.suppressDefaultCursorToolCatalog).toBe(true); | ||
| }); | ||
|
|
||
| test("_clientThreadId identity keeps the default catalog (flag unset)", () => { | ||
| const request = createCursorRequest(parsedRequest({ _clientThreadId: "thread-1" } as Partial<OcxParsedRequest>)); | ||
| expect(request.suppressDefaultCursorToolCatalog).toBeUndefined(); | ||
| }); | ||
|
|
||
| test("_cursorClientThreadId identity keeps the default catalog (flag unset)", () => { | ||
| const request = createCursorRequest(parsedRequest({ _cursorClientThreadId: "app:x" } as Partial<OcxParsedRequest>)); | ||
| expect(request.suppressDefaultCursorToolCatalog).toBeUndefined(); | ||
| }); | ||
|
|
||
| test("caller-supplied tools never set the flag", () => { | ||
| const request = createCursorRequest(parsedRequest({ | ||
| context: { systemPrompt: [], messages: [{ role: "user", content: "hi" }], tools: [CALLER_TOOL] }, | ||
| } as Partial<OcxParsedRequest>)); | ||
| expect(request.suppressDefaultCursorToolCatalog).toBeUndefined(); | ||
| expect(request.tools?.length).toBe(1); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| import { describe, expect, test } from "bun:test"; | ||
| import { createCursorAdapter as createCursorAdapterProduction } from "../src/adapters/cursor"; | ||
| import { clearCursorCheckpointsForTests, getCursorCheckpoint } from "../src/adapters/cursor/checkpoint-store"; | ||
| import { create, toBinary } from "@bufbuild/protobuf"; | ||
| import { ConversationStateStructureSchema } from "../src/adapters/cursor/gen/agent_pb"; | ||
| import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig } from "../src/types"; | ||
| import type { CursorServerMessage } from "../src/adapters/cursor/types"; | ||
| import { withTestTranslatorBudget } from "./helpers/translator-budget"; | ||
|
|
||
| const createCursorAdapter = (...args: Parameters<typeof createCursorAdapterProduction>) => | ||
| withTestTranslatorBudget(createCursorAdapterProduction(...args)); | ||
|
|
||
| const provider: OcxProviderConfig = { adapter: "cursor", baseUrl: "https://api2.cursor.sh" }; | ||
|
|
||
| const checkpointBytes = toBinary(ConversationStateStructureSchema, create(ConversationStateStructureSchema, { | ||
| pendingToolCalls: ["suspended-fixture"], | ||
| })); | ||
|
|
||
| /** Transport that emits a client tool call, exposing checkpoint bytes only after it. */ | ||
| function toolSuspendedTransport() { | ||
| let capturable: Uint8Array | undefined; | ||
| return { | ||
| async *run() { | ||
| yield { type: "tool_call_start", id: "call_x", name: "get_weather" } satisfies CursorServerMessage; | ||
| yield { type: "tool_call_delta", arguments: "{}" } satisfies CursorServerMessage; | ||
| capturable = checkpointBytes; | ||
| yield { type: "tool_call_end" } satisfies CursorServerMessage; | ||
| yield { type: "done", usage: { inputTokens: 1, outputTokens: 1 } } satisfies CursorServerMessage; | ||
| }, | ||
| writeClient() {}, | ||
| capturedConversationCheckpoint() { | ||
| return capturable; | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| function body(modelId: string): OcxParsedRequest { | ||
| return { | ||
| modelId, | ||
| context: { messages: [{ role: "user", content: "hi", timestamp: 1 }] }, | ||
| stream: false, | ||
| options: {}, | ||
| _cursorConversationId: "cursor_tool_suspend", | ||
| _cursorIdentityScope: "acct-suspend", | ||
| } as OcxParsedRequest; | ||
| } | ||
|
|
||
| describe("tool-suspended checkpoint commit (devlog 260826 050)", () => { | ||
| test("external model commits a tool-suspended checkpoint with checkpointUsable=false", async () => { | ||
| clearCursorCheckpointsForTests(); | ||
| const adapter = createCursorAdapter({ ...provider, apiKey: "cursor-token" }, { createTransport: toolSuspendedTransport }); | ||
| const events: AdapterEvent[] = []; | ||
| await adapter.runTurn?.(body("cursor/grok-4.6"), { headers: new Headers() }, event => events.push(event)); | ||
| const done = events.find(event => event.type === "done"); | ||
| if (done?.type !== "done") throw new Error("expected done"); | ||
| expect(done.providerState?.cursor?.checkpointRef).toBeDefined(); | ||
| expect(done.providerState?.cursor?.checkpointUsable).toBe(false); | ||
| expect(getCursorCheckpoint(done.providerState?.cursor?.checkpointRef)).toBeDefined(); | ||
| clearCursorCheckpointsForTests(); | ||
| }); | ||
|
|
||
| test("native composer model still refuses the tool-suspended commit", async () => { | ||
| clearCursorCheckpointsForTests(); | ||
| const adapter = createCursorAdapter({ ...provider, apiKey: "cursor-token" }, { createTransport: toolSuspendedTransport }); | ||
| const events: AdapterEvent[] = []; | ||
| await adapter.runTurn?.(body("cursor/composer-2.5"), { headers: new Headers() }, event => events.push(event)); | ||
| const done = events.find(event => event.type === "done"); | ||
| if (done?.type !== "done") throw new Error("expected done"); | ||
| expect(done.providerState?.cursor?.checkpointRef).toBeUndefined(); | ||
| clearCursorCheckpointsForTests(); | ||
| }); | ||
|
|
||
| test("checkpoint captured before the tool call is still refused (ordering guard)", async () => { | ||
| clearCursorCheckpointsForTests(); | ||
| const transport = { | ||
| async *run() { | ||
| yield { type: "tool_call_start", id: "call_y", name: "get_weather" } satisfies CursorServerMessage; | ||
| yield { type: "tool_call_delta", arguments: "{}" } satisfies CursorServerMessage; | ||
| yield { type: "tool_call_end" } satisfies CursorServerMessage; | ||
| yield { type: "done", usage: { inputTokens: 1, outputTokens: 1 } } satisfies CursorServerMessage; | ||
| }, | ||
| writeClient() {}, | ||
| capturedConversationCheckpoint() { | ||
| // Bytes available from the very first poll — pre-tool capture. | ||
| return checkpointBytes; | ||
| }, | ||
| }; | ||
| const adapter = createCursorAdapter({ ...provider, apiKey: "cursor-token" }, { createTransport: () => transport }); | ||
| const events: AdapterEvent[] = []; | ||
| await adapter.runTurn?.(body("cursor/grok-4.6"), { headers: new Headers() }, event => events.push(event)); | ||
| const done = events.find(event => event.type === "done"); | ||
| if (done?.type !== "done") throw new Error("expected done"); | ||
| expect(done.providerState?.cursor?.checkpointRef).toBeUndefined(); | ||
| clearCursorCheckpointsForTests(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import { describe, expect, test } from "bun:test"; | ||
| import { | ||
| CURSOR_KNOWN_UNCALLABLE_MODEL_IDS, | ||
| CURSOR_STATIC_MODELS, | ||
| filterCursorConfiguredModelsByLiveDiscovery, | ||
| } from "../src/adapters/cursor/discovery"; | ||
|
|
||
| describe("cursor uncallable-model quarantine (devlog 260826 060)", () => { | ||
| test("static seed no longer carries bare claude-opus-5", () => { | ||
| expect(CURSOR_STATIC_MODELS.some(model => model.id === "claude-opus-5")).toBe(false); | ||
| }); | ||
|
|
||
| test("siblings from other wire families survive", () => { | ||
| expect(CURSOR_STATIC_MODELS.some(model => model.id === "claude-opus-5-fast")).toBe(true); | ||
| }); | ||
|
|
||
| test("live filter drops quarantined ids even when GetUsableModels lists them", () => { | ||
| const configured = [{ id: "claude-opus-5" }, { id: "claude-opus-5-fast" }, { id: "grok-4.6" }]; | ||
| const live = ["claude-opus-5-high", "claude-opus-5-high-fast", "grok-4.6-high"]; | ||
| const filtered = filterCursorConfiguredModelsByLiveDiscovery(configured, live); | ||
| expect(filtered.map(model => model.id)).toEqual(["claude-opus-5-fast", "grok-4.6"]); | ||
| }); | ||
|
|
||
| test("quarantine applies with an empty live list too (stale/static degradation path)", () => { | ||
| const configured = [{ id: "claude-opus-5" }, { id: "auto" }]; | ||
| const filtered = filterCursorConfiguredModelsByLiveDiscovery(configured, []); | ||
| expect(filtered.some(model => model.id === "claude-opus-5")).toBe(false); | ||
| }); | ||
|
|
||
| test("quarantine set stays narrow", () => { | ||
| expect([...CURSOR_KNOWN_UNCALLABLE_MODEL_IDS]).toEqual(["claude-opus-5"]); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
checkpointUsable: falserestriction exists only in the returned continuation metadata, while the committed snapshot is also added to the generic prefix index without that restriction. For a stable client thread that sends full history withoutprevious_response_id,lookupPrefixSnapshotcan therefore recover this tool-suspended snapshot with nocursorState; if the next request ends in a user message rather than the immediate tool result, the guard atrequest-builder.ts:427is bypassed and the request resumes from pending-tool state without appending the uncovered transcript. Store the suspended kind on the snapshot and enforce it for both ref and prefix lookup, or exclude such snapshots from generic prefix recovery.AGENTS.md reference: src/AGENTS.md:L19-L19
Useful? React with 👍 / 👎.