diff --git a/devlog/_plan/260826_cursor_responses_gap/025_ultra_k3_research.md b/devlog/_plan/260826_cursor_responses_gap/025_ultra_k3_research.md index cd25d3e6e1c..a9592a10b1d 100644 --- a/devlog/_plan/260826_cursor_responses_gap/025_ultra_k3_research.md +++ b/devlog/_plan/260826_cursor_responses_gap/025_ultra_k3_research.md @@ -11,7 +11,7 @@ Claim ledger — status per cxc-search discipline: | Some models show 1M "Max Context" in Cursor table (Fable/Opus/Sonnet 5, Gemini) | verified | same (primary) | | Ultra = 20x usage ($400 API-agent allowance), NOT an exclusive catalog | verified | cursor.com/pricing + forum staff (primary) | | Max Mode currently documented for legacy request-based plans | verified | prod.cursor.com/help/ai-features/max-mode (primary) | -| K3 1M specifically unlocked on Ultra | UNVERIFIED — user observation; no primary source; Reddit says the 1M option appeared then disappeared | reddit 2026-08 (lead) | +| K3 1M specifically unlocked on Ultra | user-confirmed 2026-08-26 (operator saw the 1M option live in Cursor on the Ultra plan); public primary source still absent | user observation (authoritative for this deployment) + reddit lead | | Wire: max mode = RequestedModel.max_mode (field 2) AND ModelDetails.max_mode (field 7); missing either can invalid_argument | lead (2 impl sources) | oh-my-pi #4969, cursor-opencode-provider | | 1M exposure pattern: synthetic -1m picker variant w/ limit.context=1M, wire sends original id + maxMode | lead | cursor-opencode-provider README | | In-repo: GetUsableModels ModelDetails.maxMode=true observed on 28 -fast ids (260822); no contextTokenLimit field | verified (own probe) | devlog/_plan/260822_senpi_cursor_transfer/210_maxmode.md | diff --git a/devlog/_plan/260826_cursor_responses_gap/080_stall_corruption_diag.md b/devlog/_plan/260826_cursor_responses_gap/080_stall_corruption_diag.md index c6b63bceb7e..688548e09f9 100644 --- a/devlog/_plan/260826_cursor_responses_gap/080_stall_corruption_diag.md +++ b/devlog/_plan/260826_cursor_responses_gap/080_stall_corruption_diag.md @@ -22,6 +22,30 @@ subagent. Honest scope: diagnostics capture, not a behavior fix. ## Accept criteria +## Implementation notes (wpF, 2026-08-26) + +- Item 1 (diagnostics) was already satisfied by the baseline: the + run-request diagnostic logs continuationMode + + checkpointInvalidationReason + rootBlobs/rootBytes + (protobuf-request.ts:934-949). No change needed. +- Item 2 landed as a serve-time digest check in native-exec.ts + getBlobArgs: blobs are content-addressed (SHA-256 id), so a served + payload whose digest mismatches its raw 32-byte id is in-store + corruption — the splice signature. Emits + `blob-integrity-mismatch` debug diagnostic (key prefix + byte length + only; no payload). + +## G2 stall capture procedure (next occurrence) + +1. Reproduce with the SAME thread in the Codex app; note wall-clock time. +2. Mirror the request via curl (session log has the request id): + `curl -N http://localhost:10100/v1/responses -H 'Content-Type: application/json' --data-binary @req.json | tee stall.sse` +3. Enable debug diagnostics (OCX debug env) and capture the + run-request + checkpoint-continuation lines for the stalling turn. +4. Evidence to file here: last SSE event before silence, whether + response.completed arrived, continuationMode of the turn, and any + blob-integrity-mismatch lines. + - Diagnostic line appears for cursor turns under debug flag (test with debug seam). - Integrity check triggers on an injected mutated blob (unit test with diff --git a/src/adapters/cursor.ts b/src/adapters/cursor.ts index 39b85293f1e..18a8e8aecf7 100644 --- a/src/adapters/cursor.ts +++ b/src/adapters/cursor.ts @@ -163,7 +163,19 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda || activeRequest.contextUsageStoreCheckpoints === false || !lastTransport?.captured || lastTransport.captured.byteLength === 0 - ) return; + ) { + // Refusal diagnostics (devlog 260826 050/080): name the exact guard so a live + // missing_ref chain can be attributed without instrumented rebuilds. + debugProviderDiagnostic("cursor", "checkpoint-commit-refused", { + replayUnsafe, + emittedClientTool, + capturedAfterClientTool, + externalModel: isCursorExternalWireModel(activeRequest.modelId), + storeCheckpoints: activeRequest.contextUsageStoreCheckpoints !== false, + capturedBytes: lastTransport?.captured?.byteLength ?? 0, + }); + return; + } const previousRef = _parsed._providerContinuation?.cursor?.checkpointRef; const coveredMessageCount = _parsed.context.messages.length; const checkpointRef = commitCursorCheckpoint({ diff --git a/src/adapters/cursor/live-transport.ts b/src/adapters/cursor/live-transport.ts index ad48ec67134..716547c8010 100644 --- a/src/adapters/cursor/live-transport.ts +++ b/src/adapters/cursor/live-transport.ts @@ -415,6 +415,20 @@ export function finalizeAfterDrain(state: ReturnType 1) { + const advertised = request.tools?.length ?? 0; + return Math.max( + baseGraceMs, + Math.min( + GENERIC_TOOL_COUNT_MAX_FINALIZE_GRACE_MS, + Math.max(GENERIC_TOOL_COUNT_MIN_FINALIZE_GRACE_MS, advertised * GENERIC_TOOL_COUNT_PER_TOOL_GRACE_MS), + ), + ); + } if (!cursorRequestHasShellAlias(request.tools) || !isGenericToolUseCountDemoPrompt(text)) return baseGraceMs; const requestedCount = requestedCursorToolUseCount(text); const expandedGraceMs = requestedCount diff --git a/src/adapters/cursor/native-exec.ts b/src/adapters/cursor/native-exec.ts index aee9ddac383..921928c9aae 100644 --- a/src/adapters/cursor/native-exec.ts +++ b/src/adapters/cursor/native-exec.ts @@ -403,6 +403,19 @@ export function storeCursorBlob(data: Uint8Array, requestScope?: CursorBlobReque return blobId; } +/** + * Serve-time integrity for content-addressed blobs (devlog 260826_cursor_responses_gap 080): + * a raw 32-byte blob id IS the SHA-256 of its bytes, so served data whose digest mismatches + * the id means in-store corruption — the splice signature behind garbled replayed tool + * results. Ids longer than 32 bytes (digested-key namespace) and server-minted ids are not + * content-addressed and always pass. + */ +export function cursorBlobServeIntegrityOk(blobId: Uint8Array, served: Uint8Array): boolean { + if (blobId.byteLength !== 32) return true; + const digest = createHash("sha256").update(served).digest(); + return digest.equals(Buffer.from(blobId)); +} + /** * Long-lived pin for blobs referenced by an active Cursor conversation checkpoint. * Unlike a request scope, this lease is not sealed and is not released by getBlob hydration. @@ -622,6 +635,13 @@ export function handleCursorNativeKv( if (kvMsg.message.case === "getBlobArgs") { const blobKey = key(kvMsg.message.value.blobId); const blobData = getBlob(blobKey); + // Splice-class corruption guard (devlog 260826 080): diagnostic only, never blocks serving. + if (blobData && !cursorBlobServeIntegrityOk(kvMsg.message.value.blobId, blobData)) { + debugProviderDiagnostic("cursor", "blob-integrity-mismatch", { + blobKey: blobKey.slice(0, 18), + servedBytes: blobData.byteLength, + }); + } if (blobData && requestScope && blobRequestScopes.get(requestScope)?.kind === "request") { releaseHydratedBlob(blobKey, requestScope); } diff --git a/src/adapters/cursor/tool-definitions.ts b/src/adapters/cursor/tool-definitions.ts index d8fe23e4fe8..fd6556edf9f 100644 --- a/src/adapters/cursor/tool-definitions.ts +++ b/src/adapters/cursor/tool-definitions.ts @@ -663,7 +663,7 @@ export function buildCursorToolGuidanceSystemNote( ? "Your tool list may display it under a longer `mcp_opencodex-responses_shell_command` / `mcp_opencodex-responses_exec_command` name; those are the SAME tool — call whichever your list shows, and do not comment on the naming difference to the user." : undefined, hasBareExec - ? `Prefer the Codex shell bridge over Cursor-native Shell/Read. If a Cursor-native file read, directory listing, grep, or shell operation is rejected, continue with the listed catalog tool ${shellBridgeLabel}.` + ? `NEVER attempt Cursor-native Shell, Read, Grep, List, or any tool not in the catalog above — they are not executed locally in this environment and every attempt wastes a turn and can stall the session. ${shellBridgeLabel} is the ONLY shell surface; go to it directly on the FIRST attempt, never as a fallback after probing a native tool. Do not narrate switching surfaces ("native is blocked, using the bridge instead") — there is exactly one surface.` : undefined, hostShellNote, "Cursor product features (Chronicle, screen recording, Notes, Plans, background agents) are available only if this turn's catalog lists a matching tool; do not offer or promise them otherwise.", @@ -687,7 +687,7 @@ export function buildCursorToolGuidanceSystemNote( : undefined, "Do not count or report a tool call unless a tool result was actually returned.", hasBareExec - ? `If a Cursor-native file read, directory listing, grep, or shell operation is rejected by the runtime, use ${shellBridgeLabel} with an equivalent host-shell-safe command (POSIX: \`cat\`/\`ls\`/\`rg\`; Windows PowerShell: \`Get-Content\`/\`Get-ChildItem\`/\`Select-String\`). For file edits, use ${structuredEditNames.length > 0 ? `the structured edit tools (${quotedNames(structuredEditNames)}) or ` : ""}\`apply_patch\` when available.` + ? `For every file read, directory listing, grep, or shell operation use ${shellBridgeLabel} directly with host-shell-safe commands (POSIX: \`cat\`/\`ls\`/\`rg\`; Windows PowerShell: \`Get-Content\`/\`Get-ChildItem\`/\`Select-String\`). For file edits, use ${structuredEditNames.length > 0 ? `the structured edit tools (${quotedNames(structuredEditNames)}) or ` : ""}\`apply_patch\` when available.` : undefined, ].filter((note): note is string => typeof note === "string"); return notes.join(" "); diff --git a/tests/cursor-blob-integrity.test.ts b/tests/cursor-blob-integrity.test.ts new file mode 100644 index 00000000000..b97d2e537fe --- /dev/null +++ b/tests/cursor-blob-integrity.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, test } from "bun:test"; +import { createHash } from "node:crypto"; +import { cursorBlobServeIntegrityOk, storeCursorBlob } from "../src/adapters/cursor/native-exec"; + +describe("cursor blob serve-time integrity (devlog 260826 080)", () => { + test("content-addressed blob passes when bytes match the id", () => { + const data = new TextEncoder().encode('{"role":"user","content":"clean"}'); + const id = storeCursorBlob(data); + expect(id.byteLength).toBe(32); + expect(cursorBlobServeIntegrityOk(id, data)).toBe(true); + }); + + test("mutated bytes are detected (splice fault injection)", () => { + const data = new TextEncoder().encode('{"role":"assistant","content":"[tool_result] output"}'); + const id = new Uint8Array(createHash("sha256").update(data).digest()); + const corrupted = new TextEncoder().encode('{"role":"assistant","content":"[ martool_result] output"}'); + expect(cursorBlobServeIntegrityOk(id, corrupted)).toBe(false); + }); + + test("non-content-addressed ids (not 32 bytes) always pass", () => { + const served = new TextEncoder().encode("anything"); + expect(cursorBlobServeIntegrityOk(new Uint8Array(8), served)).toBe(true); + expect(cursorBlobServeIntegrityOk(new Uint8Array(64), served)).toBe(true); + }); +}); diff --git a/tests/cursor-tool-definitions.test.ts b/tests/cursor-tool-definitions.test.ts index 54467e41ceb..05ed5e4f5ca 100644 --- a/tests/cursor-tool-definitions.test.ts +++ b/tests/cursor-tool-definitions.test.ts @@ -334,8 +334,9 @@ describe("Cursor tool definitions", () => { expect(note).toContain("current tool catalog as ground truth"); expect(note).toContain("This turn does not expose neighboring-agent tool names `Read`, `Grep`, `Glob`, `Bash`, `LS`"); expect(note).toContain("not an external MCP server tool"); - expect(note).toContain("Prefer the Codex shell bridge over Cursor-native Shell/Read"); - expect(note).toContain("continue with the listed catalog tool `exec_command`"); + expect(note).toContain("NEVER attempt Cursor-native Shell, Read, Grep, List"); + expect(note).toContain("`exec_command` is the ONLY shell surface"); + expect(note).toContain("never as a fallback after probing a native tool"); expect(note).not.toContain("such as `shell_command` / `exec_command`"); expect(note).not.toContain("Never tell the user"); expect(note).not.toContain("silently call"); @@ -353,8 +354,8 @@ describe("Cursor tool definitions", () => { expect(note).toContain("`shell_command`"); expect(note).toContain("`shell_command` and `exec_command` are aliases of the same bridge"); expect(note).toContain("mcp_opencodex-responses_shell_command"); - expect(note).toContain("Prefer the Codex shell bridge over Cursor-native Shell/Read"); - expect(note).toContain("continue with the listed catalog tool `shell_command`"); + expect(note).toContain("NEVER attempt Cursor-native Shell, Read, Grep, List"); + expect(note).toContain("`shell_command` is the ONLY shell surface"); expect(note).not.toContain("Never tell the user"); expect(note).not.toContain("silently call"); });