Skip to content

[Bug] Cursor replay clips completed-call arguments at 2 KiB despite spare aggregate budget #4516

Closed

Description

@stephen-drew

Client or integration

Codex App

Area

Provider adapter

Summary

Separate linked defect requested in #3506 (comment).

A completed call can contain a value beyond the 2 KiB argument prefix. When its result confirms success without repeating that value, the Cursor replay roots omit the value even with almost the entire 512 KiB replay budget unused. The result survives, but the model cannot recover the omitted argument from those roots.

Expected: after admitting retained history and result output, use spare aggregate bytes to retain complete recent invocations where they fit. Preserve result priority, retained history, call identity/order, checkpoint-carried bytes, the 192-root/512-KiB bounds and UTF-8 accounting. Keep bounded prefixes when the complete invocation cannot fit. Do not remove the per-call admission cap outright.

This is the independently reproduced argument-loss problem, not a claim that it causes the four under-2-KiB rejected patches in #3506. Please keep that broader issue separate and open.

Reproduction

  1. In an OpenCodex checkout with its normal Bun dependencies installed, save the fixture below as .tmp/cursor-continuity/upstream-argument-fixture.ts.
  2. Run the baseline commands below. The fixture passes through the real Responses parser, Cursor request builder, protobuf encoder and KV root hydration. It does not contact a provider or execute a tool.
  3. The synthetic successful call is 4,693 UTF-8 bytes. Its unique marker occurs after the prefix cutoff; neither the user message nor successful tool output contains it. Observe markerPresent=false while resultPreserved=true.
  4. In a disposable checkout, apply the candidate encoder diff below, then run with --expect-retained. The marker and exact original arguments, including Unicode and JavaScript replacement literals, must round-trip. --checkpoint repeats this with a non-empty synthetic checkpoint and the tool result in the replay suffix.
  5. The script prints the full synthetic input and hydrated roots. These are offline reconstructions, not a live Cursor payload capture or a model response.
bun .tmp/cursor-continuity/upstream-argument-fixture.ts
bun .tmp/cursor-continuity/upstream-argument-fixture.ts --checkpoint
# After applying the candidate to a disposable checkout:
bun .tmp/cursor-continuity/upstream-argument-fixture.ts --expect-retained
bun .tmp/cursor-continuity/upstream-argument-fixture.ts --checkpoint --expect-retained

The optional --source argument substitutes only the encoder source in the isolated Bun process. We used it to compare the exact baseline and focused candidate without changing installed runtime files or the existing development candidate.

Version

Installed OpenCodex 2.52.0; reported Codex Desktop task runtime 0.153.4. Fresh offline fixture and regression environment: OpenCodex checkout 2.50.0 commit 2d4d7a2, Bun 1.4.2.

The official baseline encoder from that commit is byte-identical to the currently installed 2.52.0 encoder: SHA-256 c5d009802d7b1154697ac6a3823cff3cf70e07db2afbffde484bead322af3e74. The surrounding parser/builder dependencies in this offline run are from the stated 2.50.0 checkout; this is not an assertion that all dependencies across the releases are identical.

The focused candidate encoder SHA-256 is d962f8e1334e09047b8bab30bdd054fff52e9b4e0536836153afc6342af354eb. It adds only the spare-budget expansion block to that official baseline, excluding our earlier brief/history retention patches. It is not installed.

Operating system

Windows 11 25H2, x64.

Provider and model

cursor / grok-4.6; the fixture requests xhigh. This records the requested selection, not independently verified backend effort. Fresh runs below make zero provider requests.

Logs or error output

Fresh deterministic results:

Encoder Mode Marker retained Result retained Roots Root bytes
Baseline full replay false true 4 6011
Candidate full replay true true 4 9054
Baseline synthetic checkpoint plus suffix false true 2 2421
Candidate synthetic checkpoint plus suffix true true 2 5464

The candidate passed 27 tests / 0 failures / 76 assertions in cursor-tool-result-invocation.test.ts with only that encoder substituted. This includes the three proposed regression cases below: exact successful-call retention, newest-first expansion under budget pressure without dropping earlier results, and checkpoint-covered call lookup. The standalone fixture additionally checks UTF-8 root sizes, root count and exact Unicode/literal argument round-trip. These are focused checks for a candidate, not a full release qualification.

Historical, separate live synthetic probes on 11 September 2026: bounded argument replay produced MISSING; complete arguments in the same text-role format produced exact recall; the earlier combined candidate also produced exact recall. Those used different synthetic random markers and an earlier local encoder base. They support the isolated recall defect but are not fresh live validation of the focused candidate attached here. No new live capture or provider request was made for this report.

Screenshots and supporting files

All contents below are synthetic or repository code. No production task history, protobuf, checkpoint, local account data or credentials are attached.

Standalone fixture (Bun TypeScript)
// Save as .tmp/cursor-continuity/upstream-argument-fixture.ts in an OpenCodex checkout.
// Offline synthetic reconstruction only. No provider, credentials or real tool execution.
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';

const sourceIndex = process.argv.indexOf('--source');
const encoderPath = resolve(import.meta.dir, '../../src/adapters/cursor/protobuf-request.ts');
const source = readFileSync(sourceIndex >= 0 ? process.argv[sourceIndex + 1] : encoderPath, 'utf8');
Bun.plugin({ name: 'synthetic-encoder-only', setup(build) {
  build.onLoad({ filter: /protobuf-request\.ts$/ }, args => {
    assert.equal(resolve(args.path), encoderPath);
    return { loader: 'ts', contents: source };
  });
}});
const { parseRequest } = await import('../../src/responses/parser');
const { createCursorRequest } = await import('../../src/adapters/cursor/request-builder');
const { prepareCursorRunRequest } = await import('../../src/adapters/cursor/protobuf-request');
const { handleCursorNativeKv, releaseCursorBlobRequestScope, storeCursorBlob } = await import('../../src/adapters/cursor/native-exec');
const { create, fromBinary, toBinary } = await import('@bufbuild/protobuf');
const { AgentClientMessageSchema, KvServerMessageSchema, GetBlobArgsSchema, ConversationStateStructureSchema } = await import('../../src/adapters/cursor/gen/agent_pb');

const rejected = process.argv.includes('--rejected-patch');
const checkpoint = process.argv.includes('--checkpoint');
const marker = 'SYNTHETIC_TAIL_71e463b9';
const call = rejected
  ? 'text(await tools.apply_patch(`*** Begin Patch ***\n*** Update File: fixture.txt\n@@\n-old\n+new\n*** End Patch`));'
  : '// synthetic padding\n'.repeat(220) + 'write_value("' + marker + '", "界🙂", "$&", "$$", "$`", "$\'")';
const output = rejected
  ? "apply_patch verification failed: invalid patch: The first line of the patch must be '*** Begin Patch'"
  : 'Success. Updated fixture.txt.';
assert(!output.includes(marker));
const input = {
  model: 'cursor/grok-4.6',
  reasoning: { effort: 'xhigh' },
  tools: [{ type: 'custom', name: 'exec', description: 'Synthetic tool. No real files or execution.', format: { type: 'text' } }],
  input: [
    { role: 'user', content: rejected ? 'Correct the rejected patch formatting.' : 'Return the value from the end of the completed call. Return MISSING if absent.' },
    { type: 'custom_tool_call', name: 'exec', call_id: 'synthetic_call', input: call },
    { type: 'custom_tool_call_output', call_id: 'synthetic_call', output: [{ type: 'input_text', text: output }] },
  ],
};
const request = createCursorRequest(parseRequest(input));
request.conversationId = 'offline-synthetic-fixture';
request.contextUsageStoreCheckpoints = false;
if (checkpoint) {
  const carried = storeCursorBlob(new TextEncoder().encode(JSON.stringify({ role: 'user', content: [{ type: 'text', text: 'Synthetic covered prefix.' }] })));
  request.checkpointBytes = toBinary(ConversationStateStructureSchema, create(ConversationStateStructureSchema, { rootPromptMessagesJson: [carried] }));
  request.continuationMode = 'checkpoint';
  request.checkpointSuffixStart = 2;
}
const prepared = prepareCursorRunRequest(request);
const wire = fromBinary(AgentClientMessageSchema, prepared.bytes);
assert.equal(wire.message.case, 'runRequest');
const roots = (wire.message as any).value.conversationState.rootPromptMessagesJson.map((blobId: Uint8Array) => {
  const reply = fromBinary(AgentClientMessageSchema, handleCursorNativeKv(create(KvServerMessageSchema, { id: 1, message: { case: 'getBlobArgs', value: create(GetBlobArgsSchema, { blobId }) } }), prepared.blobRequestScope));
  assert.equal(reply.message.case, 'kvClientMessage');
  const kv = (reply.message as any).value.message;
  assert.equal(kv.case, 'getBlobResult');
  return JSON.parse(new TextDecoder().decode(kv.value.blobData));
});
releaseCursorBlobRequestScope(prepared.blobRequestScope);
const rootText = roots.map((root: any) => typeof root.content === 'string' ? root.content : (root.content ?? []).map((item: any) => item.text ?? '').join('\n')).join('\n');
const rootBytes = roots.reduce((total: number, root: any) => total + Buffer.byteLength(JSON.stringify(root)), 0);
assert(rootText.includes(output), 'Tool feedback was lost');
assert(roots.length <= 192 && rootBytes <= 512 * 1024, 'Replay bounds exceeded');
if (checkpoint) assert(rootText.includes('Synthetic covered prefix.'), 'Checkpoint root absent');
const markerPresent = rootText.includes(marker);
if (process.argv.includes('--expect-retained')) {
  assert(markerPresent, 'Completed-call tail was lost');
  const line = rootText.split('\n').find(line => line.startsWith('invoked: exec with '));
  assert(line, 'Invocation absent');
  const args = JSON.parse(line.slice('invoked: exec with '.length));
  assert.equal(args.input, call, 'Arguments did not round-trip exactly');
}
console.log(JSON.stringify({
  evidence: 'Offline reconstruction from a synthetic input, not a live payload capture or model response',
  encoderSha256: new Bun.CryptoHasher('sha256').update(source).digest('hex'),
  mode: checkpoint ? 'synthetic checkpoint plus suffix' : 'full replay',
  scenario: rejected ? 'rejected patch' : 'completed-call argument retention',
  providerRequests: 0, realToolExecutions: 0,
  callBytes: Buffer.byteLength(call), markerPresent, resultPreserved: rootText.includes(output),
  rootCount: roots.length, rootBytes, input, hydratedRoots: roots,
}, null, 2));
Focused candidate encoder diff
diff --git a/src/adapters/cursor/protobuf-request.ts b/src/adapters/cursor/protobuf-request.ts
--- a/src/adapters/cursor/protobuf-request.ts
+++ b/src/adapters/cursor/protobuf-request.ts
@@ -694,6 +694,38 @@ function rootPromptMessages(
     historyMessageStart = firstKept?.messageIndex ?? (messages.length);
   }
 
+  // Reserve history and result output first, then spend only unused root bytes on complete
+  // invocations, newest first. The 2 KiB admission prefix alone can hide a successful write's
+  // final value even in a tiny conversation. Expansion must never evict another retained root
+  // or recreate output already truncated by the history budget.
+  if (externalModel && replayedCalls) {
+    let spareBytes = CURSOR_EXTERNAL_ROOT_BYTE_LIMIT - carriedRoots.byteLength
+      - selected.reduce((sum, entry) => sum + entry.byteLength, 0);
+    for (let index = selected.length - 1; index >= 0 && spareBytes > 0; index--) {
+      const entry = selected[index]!;
+      if (entry.role !== "toolResult" || entry.messageIndex === undefined || entry.outputElided) continue;
+      const message = messages[entry.messageIndex];
+      if (message?.role !== "toolResult") continue;
+      const call = callBefore(replayedCalls, decodeCursorCallId(message.toolCallId), knownCallsOffset + entry.messageIndex);
+      if (!call) continue;
+      const fullArgs = serializeToolCallArguments(call.arguments);
+      if (fullArgs === undefined) continue;
+      const fullArgsBytes = encoder.encode(fullArgs).byteLength;
+      if (fullArgsBytes <= CURSOR_INVOCATION_ARGUMENTS_BYTE_LIMIT
+        || fullArgsBytes - CURSOR_INVOCATION_ARGUMENTS_BYTE_LIMIT > spareBytes) continue;
+      const payload = JSON.parse(entry.serialized) as { role: "assistant"; content: [{ type: "text"; text: string }] };
+      const boundedLine = `\n${toolInvocationLine(call)}\n`;
+      if (!payload.content[0].text.includes(boundedLine)) continue;
+      const text = payload.content[0].text.replace(boundedLine, () => `\ninvoked: ${namespacedToolName(call.namespace, call.name)} with ${fullArgs}\n`);
+      const expanded = rootBlobCandidate(toolResultRootPayload(text), "toolResult", { messageIndex: entry.messageIndex, text });
+      const extraBytes = expanded.byteLength - entry.byteLength;
+      if (extraBytes <= spareBytes) {
+        selected[index] = expanded;
+        spareBytes -= extraBytes;
+      }
+    }
+  }
+
   return {
     ids: selected.map(entry => storeCursorBlob(entry.data, requestScope)),
     byteLength: selected.reduce((sum, entry) => sum + entry.byteLength, 0),
Three regression tests added to the existing invocation suite
diff --git a/tests/providers/cursor/cursor-tool-result-invocation.test.ts b/tests/providers/cursor/cursor-tool-result-invocation.test.ts
index aa3b9d16d..1397956eb 100644
--- a/tests/providers/cursor/cursor-tool-result-invocation.test.ts
+++ b/tests/providers/cursor/cursor-tool-result-invocation.test.ts
@@ -131,6 +131,54 @@ function resultRoot(bytes: Uint8Array): string | undefined {
  * decode the real wire payload, since roots are what Cursor builds the model prompt from.
  */
 describe("cursor replayed tool results name their invocation", () => {
+  test("spare replay space preserves the complete successful call", () => {
+    const input = '// fixture preparation\n'.repeat(250) + 'write_verified_value("COMPLETED_VALUE", "$&", "$$", "$`", "$\'")';
+    const messages: OcxMessage[] = [
+      { role: "user", content: "Return the value written by the completed call.", timestamp: 1 },
+      { role: "assistant", content: [{ type: "toolCall", id: CALL_ID, name: "exec", arguments: { input } }], timestamp: 2 },
+      { role: "toolResult", toolCallId: CALL_ID, toolName: "exec", content: "Write completed successfully.", isError: false, timestamp: 3 },
+    ];
+    const root = resultRoot(encode(messages, "grok-4.6-high"));
+    expect(root).toContain("COMPLETED_VALUE");
+    expect(root).toContain("Write completed successfully.");
+    expect(root).not.toContain("[arguments truncated]");
+    const invocation = root!.split("\n").find(line => line.startsWith("invoked: exec with "))!;
+    expect(JSON.parse(invocation.slice("invoked: exec with ".length))).toEqual({ input });
+  });
+
+  test("spare replay space restores the newest call without evicting prior results", () => {
+    const oldInput = 'x'.repeat(12000) + 'OLDER_VALUE';
+    const newInput = 'y'.repeat(12000) + 'LATEST_VALUE';
+    const messages: OcxMessage[] = [
+      { role: "user", content: "Both writes are complete.", timestamp: 1 },
+      { role: "assistant", content: [{ type: "toolCall", id: "old_call", name: "exec", arguments: { input: oldInput } }], timestamp: 2 },
+      { role: "toolResult", toolCallId: "old_call", toolName: "exec", content: "OLDER_OUTPUT", isError: false, timestamp: 3 },
+      { role: "assistant", content: [{ type: "toolCall", id: "new_call", name: "exec", arguments: { input: newInput } }], timestamp: 4 },
+      { role: "toolResult", toolCallId: "new_call", toolName: "exec", content: "LATEST_OUTPUT", isError: false, timestamp: 5 },
+    ];
+    const bytes = encodeCursorRunRequest({ modelId: "grok-4.6-high", conversationId: "spare-space", system: ['s'.repeat(512 * 1024 - 20 * 1024)], rawMessages: messages, messages: [{ role: "tool", content: "LATEST_OUTPUT" }] });
+    const text = rootTexts(bytes).join("\n");
+    expect(text).toContain("LATEST_VALUE");
+    expect(text).not.toContain("OLDER_VALUE");
+    expect(text).toContain("OLDER_OUTPUT");
+    expect(text).toContain("LATEST_OUTPUT");
+    expect(text).toContain("Both writes are complete.");
+  });
+
+  test("spare replay space restores a checkpoint-covered call in its result suffix", () => {
+    const input = 'q'.repeat(6000) + 'CHECKPOINT_CALL_TAIL';
+    const messages: OcxMessage[] = [
+      { role: "user", content: "Complete the write.", timestamp: 1 },
+      { role: "assistant", content: [{ type: "toolCall", id: CALL_ID, name: "exec", arguments: { input } }], timestamp: 2 },
+      { role: "toolResult", toolCallId: CALL_ID, toolName: "exec", content: "CHECKPOINT_RESULT", isError: false, timestamp: 3 },
+    ];
+    const bytes = encodeCheckpoint(messages, "grok-4.6-high", 2);
+    const text = rootTexts(bytes).join("\n");
+    expect(text).toContain("covered by checkpoint");
+    expect(text).toContain("CHECKPOINT_CALL_TAIL");
+    expect(text).toContain("CHECKPOINT_RESULT");
+  });
+
   test("the result envelope names the tool and arguments that produced it", () => {
     const root = resultRoot(encode(history(), "grok-4.6-high"));
     expect(root).toBeDefined();
Exact hydrated roots BEFORE, full replay (synthetic offline reconstruction)
[
  {
    "role": "system",
    "content": "You are a helpful assistant."
  },
  {
    "role": "system",
    "content": "Cursor tool calls: available tool names are exactly `exec`. Use the current tool catalog as ground truth and call only those exact names with their listed argument keys. This turn does not expose neighboring-agent tool names `Read`, `Grep`, `Glob`, `Bash`, `LS`; do not call or suggest them unless the catalog lists them. `exec` is Codex code mode: its body is JavaScript evaluated in a V8 isolate, not a shell command and not Node. Shell, file edits, and MCP are nested helpers called INSIDE that body as `await tools.<name>(...)`, for example `await tools.exec_command({cmd: \"ls\"})`. Read the tool description and the isolate global `ALL_TOOLS` (not `tools.ALL_TOOLS`) for helpers this turn provides; absence from the top-level catalog or from `exec`'s description is not absence. Those nested helpers are not themselves top-level tools, so do not call `exec_command` or `shell_command` at the top level here. Nested `tools.apply_patch(input)` is host-executed: the string must begin exactly with `*** Begin Patch` and end with `*** End Patch`, each marker line being three asterisks, one space, the two words, then end of line with no further asterisks. OpenCodex does not rewrite JavaScript inside exec, so extra asterisks on a marker line are rejected by Codex before the file is touched. Nothing in the isolate is echoed automatically: a bare trailing `await tools.<name>(...)` or final expression value is DISCARDED, and the cell reports empty output. Pass anything you need to read to `text(...)` (or `notify(...)`) in the same cell — for example `text(JSON.stringify(await tools.exec_command({cmd: 'ls'})))` — and treat an empty result as your own missing `text(...)` call rather than a failed command or lost context. There is no `require`, no `module`, and no filesystem or network globals; reach the host only through the nested helpers. Host contract for the nested helpers: `tools.apply_patch(patch)` takes exactly one string, never an object such as `{input: ...}`; the patch text opens with the bare marker line `*** Begin Patch` and closes with the bare marker line `*** End Patch`, written without a code fence, prose, or extra asterisks on those lines (blank lines or indentation around the markers are tolerated; a decorated or missing marker is rejected). The isolate has no `import`, `require`, or module loader; use the globals the exec tool description lists (for example `tools`, `text`, `notify`, `store`/`load`, `ALL_TOOLS`). For a command that may outlive `yield_time_ms`, let `tools.exec_command` return a `session_id` and poll it on later calls with `tools.write_stdin({session_id, chars: \"\"})` instead of blocking a shell in a sleep loop. NEVER attempt Cursor-native Shell, Read, Grep, List, or any tool absent from the catalog — they are not executed in this environment and every probe wastes a turn. The exec code cell (with its nested helpers) is the ONLY execution surface; go to it directly on the FIRST attempt and do not narrate switching surfaces. 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. For independent read-only tool-count or batch requests, prefer one response containing multiple tool calls before waiting for results when the runtime supports parallel tool calls. Do not count or report a tool call unless a tool result was actually returned."
  },
  {
    "role": "user",
    "content": [
      {
        "type": "text",
        "text": "Return the value from the end of the completed call. Return MISSING if absent."
      }
    ]
  },
  {
    "role": "assistant",
    "content": [
      {
        "type": "text",
        "text": "[Tool Result]\n[tool_result]\ncall_id: synthetic_call\nname: exec\ninvoked: exec with {\"input\":\"// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic…[arguments truncated]\nis_error: false\noutput:\nSuccess. Updated fixture.txt."
      }
    ]
  }
]
Exact hydrated roots AFTER, full replay (synthetic offline reconstruction)
[
  {
    "role": "system",
    "content": "You are a helpful assistant."
  },
  {
    "role": "system",
    "content": "Cursor tool calls: available tool names are exactly `exec`. Use the current tool catalog as ground truth and call only those exact names with their listed argument keys. This turn does not expose neighboring-agent tool names `Read`, `Grep`, `Glob`, `Bash`, `LS`; do not call or suggest them unless the catalog lists them. `exec` is Codex code mode: its body is JavaScript evaluated in a V8 isolate, not a shell command and not Node. Shell, file edits, and MCP are nested helpers called INSIDE that body as `await tools.<name>(...)`, for example `await tools.exec_command({cmd: \"ls\"})`. Read the tool description and the isolate global `ALL_TOOLS` (not `tools.ALL_TOOLS`) for helpers this turn provides; absence from the top-level catalog or from `exec`'s description is not absence. Those nested helpers are not themselves top-level tools, so do not call `exec_command` or `shell_command` at the top level here. Nested `tools.apply_patch(input)` is host-executed: the string must begin exactly with `*** Begin Patch` and end with `*** End Patch`, each marker line being three asterisks, one space, the two words, then end of line with no further asterisks. OpenCodex does not rewrite JavaScript inside exec, so extra asterisks on a marker line are rejected by Codex before the file is touched. Nothing in the isolate is echoed automatically: a bare trailing `await tools.<name>(...)` or final expression value is DISCARDED, and the cell reports empty output. Pass anything you need to read to `text(...)` (or `notify(...)`) in the same cell — for example `text(JSON.stringify(await tools.exec_command({cmd: 'ls'})))` — and treat an empty result as your own missing `text(...)` call rather than a failed command or lost context. There is no `require`, no `module`, and no filesystem or network globals; reach the host only through the nested helpers. Host contract for the nested helpers: `tools.apply_patch(patch)` takes exactly one string, never an object such as `{input: ...}`; the patch text opens with the bare marker line `*** Begin Patch` and closes with the bare marker line `*** End Patch`, written without a code fence, prose, or extra asterisks on those lines (blank lines or indentation around the markers are tolerated; a decorated or missing marker is rejected). The isolate has no `import`, `require`, or module loader; use the globals the exec tool description lists (for example `tools`, `text`, `notify`, `store`/`load`, `ALL_TOOLS`). For a command that may outlive `yield_time_ms`, let `tools.exec_command` return a `session_id` and poll it on later calls with `tools.write_stdin({session_id, chars: \"\"})` instead of blocking a shell in a sleep loop. NEVER attempt Cursor-native Shell, Read, Grep, List, or any tool absent from the catalog — they are not executed in this environment and every probe wastes a turn. The exec code cell (with its nested helpers) is the ONLY execution surface; go to it directly on the FIRST attempt and do not narrate switching surfaces. 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. For independent read-only tool-count or batch requests, prefer one response containing multiple tool calls before waiting for results when the runtime supports parallel tool calls. Do not count or report a tool call unless a tool result was actually returned."
  },
  {
    "role": "user",
    "content": [
      {
        "type": "text",
        "text": "Return the value from the end of the completed call. Return MISSING if absent."
      }
    ]
  },
  {
    "role": "assistant",
    "content": [
      {
        "type": "text",
        "text": "[Tool Result]\n[tool_result]\ncall_id: synthetic_call\nname: exec\ninvoked: exec with {\"input\":\"// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\n// synthetic padding\\nwrite_value(\\\"SYNTHETIC_TAIL_71e463b9\\\", \\\"界🙂\\\", \\\"$&\\\", \\\"$$\\\", \\\"$`\\\", \\\"$'\\\")\"}\nis_error: false\noutput:\nSuccess. Updated fixture.txt."
      }
    ]
  }
]

Related #3506 diagnostic: the same fixture accepts --rejected-patch and --rejected-patch --checkpoint. Fresh baseline runs preserve the exact rejection text in both modes (4,227 and 677 root bytes respectively). This is a negative control for feedback loss, not a reproduction of the live repetition. A historical minimal live correction probe also corrected the formatting, so no failing synthetic model-response pair is claimed. Exact live capture instrumentation remains separate from this report.

Redacted configuration

{
  "client": "Codex App",
  "provider": "cursor",
  "model": "grok-4.6",
  "fixture": "offline; no credentials, accounts, network or real tool execution"
}

Checks

  • I searched existing issues and documentation.
  • I removed secrets, tokens, account details, request credentials, and personal data.

Activity

  1. added
    bugSomething isn't working
    providerProvider adapters, OpenAI-compat presets, upstream API quirks
    on Sep 13, 2026
  2. lidge-jun commented on Sep 13, 2026

    @lidge-jun
    Owner

    리뷰 · 우선순위 73 / 80

    이 이슈는 Cursor 외부 모델 리플레이에서 "성공한 툴 호출 인자"가 2 KiB 접두사로 잘리는데, 전체 512 KiB / 192 root 예산은 거의 남는 상황을 독립 재현한 것입니다. #3506과 연결하되, under-2KiB 패치 거절 네 건의 원인 주장과는 분리해 달라고 본문이 분명히 요청합니다. 그 분리는 맞습니다.

    지금 dev(8e6c99608)의 src/adapters/cursor/protobuf-request.ts를 보면 CURSOR_EXTERNAL_ROOT_BLOB_LIMIT = 192, CURSOR_EXTERNAL_ROOT_BYTE_LIMIT = 512 * 1024, CURSOR_INVOCATION_ARGUMENTS_BYTE_LIMIT = 2 * 1024가 있습니다. toolCallArgumentsText는 직렬화 인자가 2 KiB를 넘으면 …[arguments truncated] 마커를 붙여 자릅니다. 주석도 "큰 write_file 인자 하나가 히스토리 예산을 삼켜 결과 출력이 잘리는 것"을 막기 위한 독립 캡이라고 설명합니다. 즉 캡 자체는 이유가 있습니다.

    문제는 캡이 "항상 2 KiB"로 고정되어, 결과와 retained history를 넣은 뒤에도 aggregate에 여유가 있을 때 최근 완료 호출의 전체 인자를 되살리지 못한다는 점입니다. 리포터 fixture는 4,693바이트 성공 호출의 마커가 접두사 밖에 있어 markerPresent=false / resultPreserved=true를 보여 줍니다. 후보 인코더는 spare-budget expansion만 추가해 마커와 원문을 살리고, root 수와 UTF-8 회계, checkpoint 경로도 유지한다고 합니다. 기대 동작 서술이 구체적입니다. 결과 우선, retained history, call id/order, checkpoint bytes, 192/512KiB, UTF-8, 못 넣으면 bounded prefix 유지, per-call 캡 자체를 삭제하지는 말 것.

    재현이 offline이고 provider를 치지 않으며, baseline encoder SHA와 candidate SHA까지 적혀 있어 검증 비용이 낮습니다. Windows 11 + cursor/grok-4.6 요청 맥락도 적혀 있지만, 결함 자체는 encoder 정책이라 플랫폼 무관에 가깝습니다.

    경로/심볼 - CURSOR_INVOCATION_ARGUMENTS_BYTE_LIMIT / toolCallArgumentsText - 현재 dev는 per-call 하드캡만 있다. spare 예산을 쓰는 2차 패스가 없다. 이슈가 지적하는 공백과 일치한다.
    경로/심볼 - 결과 우선 원칙 - 600 KiB 인자 때문에 결과가 사라지던 옛 실패로 돌아가면 안 된다. 후보가 "완전한 인자 확장"을 결과/히스토리 다음 순위로 두는 설계인지 코드 리뷰에서 확인해야 한다.
    경로/심볼 - #3506 분리 - 이 이슈를 #3506의 네 거절 패치 재심으로 바꾸지 말라는 요청을 지키자. 라벨/연결만 유지하고 구현 PR은 이 이슈를 Fixes로 둔다.
    경로/심볼 - fixture 경로 .tmp/cursor-continuity/upstream-argument-fixture.ts - 본문 스크립트를 저장소 테스트로 옮길 때 tests/providers/cursor/ 쪽에 정식 회귀로 넣는 편이 좋다.

    메인테이너의 판단이 필요한 지점

    • spare-budget 확장을 protobuf-request 안에서 2차 pass로 넣을지, 별 헬퍼로 뺄지
    • newest-first로 어떤 완료 호출부터 전체 인자를 살릴지(이슈가 제안한 newest-first와 일치하는지)
    • #3506과의 트래킹 문구를 이슈 본문에 고정할지

    너의 추천
    우선순위 높게 받아들여라. 구현 PR은 per-call 캡을 삭제하지 말고, 결과·히스토리 인정 후 남은 aggregate로 최근 완료 호출을 확장하는 방식 + fixture를 tests/providers/cursor/ 회귀로 이식하면 된다. #3506은 열린 채로 두고 이 이슈만 따로 처리하라.

    이 댓글은 grok-bot이 작성했습니다

  3. added a commit that references this issue on Sep 13, 2026
    f7e4af0
  4. lidge-jun commented on Sep 13, 2026

    @lidge-jun
    Owner

    Fixed on dev at 72335fc6ac.

    restoreClippedInvocationArguments runs after the root set is final and spends only what is genuinely left over: spare = CURSOR_EXTERNAL_ROOT_BYTE_LIMIT − carriedRoots − Σ selected. It walks newest first, skips roots that already lost their own output, and never drops or reorders a retained root. The per-call 2 KiB admission cap is untouched, so the property this could most plausibly have broken is still enforced — the two 600 KiB tests that prove the cap bites under pressure are unchanged and green.

    So the case in the report now holds: a completed call whose unique value sits past the 2 KiB prefix keeps that value when the envelope has room, instead of losing it while most of the 192-root, 512 KiB budget goes unused.

    Landed as #4543 (f7e4af08058126b0ebde38dcb8e552f7f01f85a6, CI run 34776361364) with the regressions in #4554 (56c956715ca5ae0d55bb01408814b24d8ca880c8, CI run 34779412583), each verified at its exact head.

    This closes #4516 only. It makes no claim about #3506 — the four rejected patches there were all under the cap, and that issue stays open on its own evidence.

  5. added 2 commits that reference this issue on Sep 17, 2026
    df9599f
    56131d3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingproviderProvider adapters, OpenAI-compat presets, upstream API quirks

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions