@bitkyc08/opencodex 2.33.0 → 2.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/gui/dist/assets/index-C4TMRloX.js +112 -0
- package/gui/dist/assets/index-DQ-Ie18T.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +79 -2
- package/src/adapters/command-code.ts +40 -3
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/checkpoint-store.ts +15 -10
- package/src/adapters/cursor/discovery.ts +60 -2
- package/src/adapters/cursor/effort-map.ts +79 -1
- package/src/adapters/cursor/envelope-echo.ts +162 -0
- package/src/adapters/cursor/live-models.ts +7 -2
- package/src/adapters/cursor/live-transport.ts +14 -0
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-desktop.ts +23 -0
- package/src/adapters/cursor/native-exec-fs.ts +2 -2
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +5 -2
- package/src/adapters/cursor/native-exec.ts +20 -0
- package/src/adapters/cursor/protobuf-request.ts +68 -14
- package/src/adapters/cursor/request-builder.ts +29 -6
- package/src/adapters/cursor/tool-definitions.ts +11 -2
- package/src/adapters/cursor/tool-result-normalize.ts +32 -1
- package/src/adapters/cursor/types.ts +20 -0
- package/src/adapters/cursor.ts +189 -37
- package/src/adapters/google-antigravity-replay.ts +102 -23
- package/src/adapters/google-errors.ts +44 -12
- package/src/adapters/google.ts +115 -16
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +15 -0
- package/src/adapters/kiro-tools.ts +43 -15
- package/src/adapters/kiro.ts +54 -9
- package/src/adapters/openai-chat.ts +286 -242
- package/src/adapters/openai-responses.ts +334 -24
- package/src/adapters/tool-catalog-nudge.ts +2 -2
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/bridge.ts +67 -24
- package/src/chat/inbound.ts +14 -1
- package/src/chat/outbound.ts +15 -7
- package/src/claude/agents-inject.ts +8 -1
- package/src/claude/outbound.ts +10 -8
- package/src/cli/account-api.ts +32 -4
- package/src/cli/account-extended.ts +1 -1
- package/src/cli/account.ts +29 -5
- package/src/cli/alias.ts +66 -0
- package/src/cli/claude.ts +26 -1
- package/src/cli/dispatch.ts +13 -1
- package/src/cli/help.ts +1 -0
- package/src/cli/init.ts +1 -0
- package/src/cli/models-runtime.ts +95 -0
- package/src/cli/models.ts +13 -7
- package/src/cli/provider-runtime.ts +16 -2
- package/src/cli/registry.ts +6 -1
- package/src/cli/v2.ts +34 -10
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +3 -2
- package/src/codex/app-server-processes.ts +80 -6
- package/src/codex/auth-api.ts +46 -8
- package/src/codex/auth-context.ts +21 -18
- package/src/codex/catalog/aggregation.ts +6 -0
- package/src/codex/catalog/native-models.ts +5 -2
- package/src/codex/catalog/parsing.ts +16 -0
- package/src/codex/catalog/provider-fetch.ts +20 -3
- package/src/codex/catalog/sync.ts +127 -2
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-write-lock.ts +3 -1
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/convergence.ts +22 -2
- package/src/codex/desired-state.ts +2 -2
- package/src/codex/desktop-app-restart.ts +18 -5
- package/src/codex/inject-coordination.ts +83 -0
- package/src/codex/inject.ts +14 -1
- package/src/codex/log-guard/inspect.ts +22 -4
- package/src/codex/model-entitlements.ts +9 -2
- package/src/codex/prompt-layers.ts +371 -25
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota.ts +91 -10
- package/src/codex/routing.ts +9 -0
- package/src/codex/subagent-model-fallback.ts +104 -25
- package/src/codex/transition-state.ts +107 -8
- package/src/combos/types.ts +10 -0
- package/src/compatibility/openai-responses.ts +33 -1
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +107 -9
- package/src/generated/compatibility-version.json +191 -139
- package/src/images/loop.ts +5 -4
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
- package/src/lab/fabric/producer-child.ts +1 -1
- package/src/lib/config-ownership.ts +20 -0
- package/src/lib/errors.ts +11 -2
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/index.ts +57 -5
- package/src/oauth/key-providers.ts +10 -1
- package/src/oauth/kiro.ts +45 -0
- package/src/oauth/types.ts +15 -0
- package/src/providers/command-code-efforts.ts +38 -6
- package/src/providers/context-cap.ts +4 -3
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +28 -0
- package/src/providers/fastwire.ts +7 -1
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/new-model-policy.ts +146 -0
- package/src/providers/provider-id-rewrite.ts +2 -1
- package/src/providers/quota.ts +65 -7
- package/src/providers/registry.ts +160 -68
- package/src/providers/slug-codec.ts +52 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/custom-tool-compat.ts +34 -10
- package/src/responses/parser.ts +4 -0
- package/src/responses/schema.ts +5 -1
- package/src/responses/thought-signature-replay.ts +17 -0
- package/src/router.ts +40 -2
- package/src/routing/analytics.ts +1 -0
- package/src/routing/quota.ts +10 -0
- package/src/server/auth-cors.ts +4 -0
- package/src/server/chat-completions.ts +24 -16
- package/src/server/chat-native-sse.ts +3 -3
- package/src/server/chat-native.ts +30 -11
- package/src/server/claude-messages.ts +1 -1
- package/src/server/index.ts +77 -11
- package/src/server/lifecycle.ts +52 -1
- package/src/server/management/agent-settings-routes.ts +31 -15
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +2 -1
- package/src/server/management/config-routes.ts +27 -9
- package/src/server/management/context.ts +9 -0
- package/src/server/management/model-routes.ts +266 -0
- package/src/server/management/provider-routes.ts +6 -0
- package/src/server/management/routing-profile-routes.ts +2 -2
- package/src/server/management-api.ts +2 -0
- package/src/server/relay-eager.ts +147 -20
- package/src/server/relay.ts +251 -19
- package/src/server/request-log-conversation.ts +21 -0
- package/src/server/request-log.ts +41 -15
- package/src/server/responses/collaboration.ts +37 -3
- package/src/server/responses/combo-stream-preflight.ts +10 -3
- package/src/server/responses/core.ts +494 -114
- package/src/server/responses/empty-completion-guard.ts +35 -0
- package/src/server/responses/fetch-helpers.ts +2 -1
- package/src/server/responses/input-admission.ts +3 -1
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/responses-field-backfill.ts +105 -13
- package/src/server/responses-custom-tool-repair.ts +52 -7
- package/src/server/responses-terminal-repair.ts +25 -4
- package/src/server/sse-frame-buffer.ts +31 -4
- package/src/server/ws-bridge.ts +14 -2
- package/src/types/config.ts +53 -0
- package/src/types/provider.ts +64 -0
- package/src/types/request.ts +11 -3
- package/src/types/tools.ts +7 -5
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +43 -24
- package/src/usage/log.ts +3 -0
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-23-Lf7jR.js +0 -102
- package/gui/dist/assets/index-DxJMDyOr.css +0 -1
|
@@ -43,11 +43,11 @@ function codexNativeMutationRefusal(operation: "write" | "delete", structuredEdi
|
|
|
43
43
|
const structuredHint = structuredEditAvailable
|
|
44
44
|
? " Use the structured edit tools (`edit_file` / `multi_edit`) or the `apply_patch` tool for file edits so Codex can approve the change, enforce sandbox policy, show diffs, and record rollout."
|
|
45
45
|
: " Use the `apply_patch` tool for file edits so Codex can approve the change, enforce sandbox policy, show diffs, and record rollout.";
|
|
46
|
-
return `
|
|
46
|
+
return `Make this ${operation} through the Codex edit path instead.${structuredHint} No file was changed. Do NOT narrate this redirect or comment on tool availability — just make the edit call.`;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
const NATIVE_LOCAL_EXEC_DISABLED =
|
|
50
|
-
"
|
|
50
|
+
"Re-issue this operation NOW through the catalog shell tool (`shell_command` / `exec_command`, or the listed `mcp_opencodex-responses_*` display alias) with the host-shell-safe equivalent: POSIX (`cat`, `head`, `ls`, `rg`, `grep`) or Windows PowerShell (`Get-Content`, `Get-ChildItem`, `Select-String`); use `apply_patch` for file edits. Do NOT narrate this redirect, do NOT comment on tool availability, and do NOT re-announce the task — just make the bridge call.";
|
|
51
51
|
|
|
52
52
|
export function rejectReadExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
|
|
53
53
|
if (execMsg.message.case !== "readArgs") throw new Error("invalid read exec");
|
|
@@ -7,7 +7,7 @@ export interface CursorNativeNetworkDeps {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
const NATIVE_FETCH_DISABLED =
|
|
10
|
-
"
|
|
10
|
+
"Re-issue this fetch NOW through the catalog shell tool `shell_command` (aliases: `exec_command`, `mcp_opencodex-responses_shell_command`, `mcp_opencodex-responses_exec_command`) with curl or wget. Do NOT narrate this redirect or comment on tool availability — just make the bridge call.";
|
|
11
11
|
|
|
12
12
|
export function rejectFetchExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
|
|
13
13
|
if (execMsg.message.case !== "fetchArgs") throw new Error("invalid fetch exec");
|
|
@@ -87,11 +87,14 @@ export function nativeShellDisabledMessage(): string {
|
|
|
87
87
|
// idioms through the Codex bridge on Windows PowerShell 5.1 and looping (#604).
|
|
88
88
|
// Keep this host-shell-neutral: OpenCodex may run on a different OS than the Codex
|
|
89
89
|
// client that executes the bridge (LAN/SSH remote-proxy).
|
|
90
|
+
// Silent-redirect framing (devlog 260826 gap-8): no "blocked/denied/disabled" words —
|
|
91
|
+
// models narrate those as a surface switch ("Shell이 차단되어 전환합니다") and burn turns.
|
|
90
92
|
return (
|
|
91
|
-
"
|
|
93
|
+
"Re-issue this command NOW through the catalog shell tool (`shell_command` or `exec_command`; the long `mcp_opencodex-responses_*` display name is the same tool). "
|
|
92
94
|
+ "Adapt the command for the Codex client host shell before calling the bridge "
|
|
93
95
|
+ "(Windows PowerShell 5.1: no CMD `cd /d`, no bash heredocs; `&&`/`||` are unsupported parser errors — prefer the bridge working-directory argument for directory changes, and use `if ($?) { ... }` for success-gated follow-up steps; do not treat `;` as a substitute for `&&`). "
|
|
94
|
-
+ "Make at most one corrected bridge attempt after a failure, then report the error and stop — do not repeat equivalent failing commands."
|
|
96
|
+
+ "Make at most one corrected bridge attempt after a failure, then report the error and stop — do not repeat equivalent failing commands. "
|
|
97
|
+
+ "Do NOT narrate this redirect, do NOT comment on tool availability, and do NOT re-announce the task — just make the bridge call."
|
|
95
98
|
);
|
|
96
99
|
}
|
|
97
100
|
|
|
@@ -403,6 +403,19 @@ export function storeCursorBlob(data: Uint8Array, requestScope?: CursorBlobReque
|
|
|
403
403
|
return blobId;
|
|
404
404
|
}
|
|
405
405
|
|
|
406
|
+
/**
|
|
407
|
+
* Serve-time integrity for content-addressed blobs (devlog 260826_cursor_responses_gap 080):
|
|
408
|
+
* a raw 32-byte blob id IS the SHA-256 of its bytes, so served data whose digest mismatches
|
|
409
|
+
* the id means in-store corruption — the splice signature behind garbled replayed tool
|
|
410
|
+
* results. Ids longer than 32 bytes (digested-key namespace) and server-minted ids are not
|
|
411
|
+
* content-addressed and always pass.
|
|
412
|
+
*/
|
|
413
|
+
export function cursorBlobServeIntegrityOk(blobId: Uint8Array, served: Uint8Array): boolean {
|
|
414
|
+
if (blobId.byteLength !== 32) return true;
|
|
415
|
+
const digest = createHash("sha256").update(served).digest();
|
|
416
|
+
return digest.equals(Buffer.from(blobId));
|
|
417
|
+
}
|
|
418
|
+
|
|
406
419
|
/**
|
|
407
420
|
* Long-lived pin for blobs referenced by an active Cursor conversation checkpoint.
|
|
408
421
|
* Unlike a request scope, this lease is not sealed and is not released by getBlob hydration.
|
|
@@ -622,6 +635,13 @@ export function handleCursorNativeKv(
|
|
|
622
635
|
if (kvMsg.message.case === "getBlobArgs") {
|
|
623
636
|
const blobKey = key(kvMsg.message.value.blobId);
|
|
624
637
|
const blobData = getBlob(blobKey);
|
|
638
|
+
// Splice-class corruption guard (devlog 260826 080): diagnostic only, never blocks serving.
|
|
639
|
+
if (blobData && !cursorBlobServeIntegrityOk(kvMsg.message.value.blobId, blobData)) {
|
|
640
|
+
debugProviderDiagnostic("cursor", "blob-integrity-mismatch", {
|
|
641
|
+
blobKey: blobKey.slice(0, 18),
|
|
642
|
+
servedBytes: blobData.byteLength,
|
|
643
|
+
});
|
|
644
|
+
}
|
|
625
645
|
if (blobData && requestScope && blobRequestScopes.get(requestScope)?.kind === "request") {
|
|
626
646
|
releaseHydratedBlob(blobKey, requestScope);
|
|
627
647
|
}
|
|
@@ -4,6 +4,7 @@ import { ValueSchema } from "@bufbuild/protobuf/wkt";
|
|
|
4
4
|
import type { OcxAssistantContentPart, OcxMessage, OcxToolResultMessage } from "../../types";
|
|
5
5
|
import { namespacedToolName } from "../../types";
|
|
6
6
|
import type { CursorRunRequest } from "./types";
|
|
7
|
+
import { decodeCursorCallId } from "./call-id";
|
|
7
8
|
import { cursorNeedsExternalToolContinuation, isCursorExternalWireModel } from "./discovery";
|
|
8
9
|
import { normalizeCursorToolResultText } from "./tool-result-normalize";
|
|
9
10
|
import { debugProviderDiagnostic } from "../../lib/debug";
|
|
@@ -217,6 +218,42 @@ function rootPromptMessages(request: CursorRunRequest, requestScope: CursorBlobR
|
|
|
217
218
|
const echoToolResultInRoot = cursorNeedsExternalToolContinuation(request.modelId);
|
|
218
219
|
const lastRawIsToolResult = messages.at(-1)?.role === "toolResult";
|
|
219
220
|
const activeUserIndex = lastRawIsToolResult ? -1 : lastActionIndex(messages);
|
|
221
|
+
// Repetition breaker (devlog 260826 gap-9): external full-replay flattens history to text,
|
|
222
|
+
// so N identical assistant/tool-result rounds replay as N identical lines and PRIME the model
|
|
223
|
+
// to emit the same line again (self-reinforcing loop: S2a 180x, identical-probe repetition).
|
|
224
|
+
// Collapse consecutive duplicates into one entry + a count marker, and count collapses so a
|
|
225
|
+
// strategy-change note can be appended when the pattern is severe.
|
|
226
|
+
let lastReplayText: string | undefined;
|
|
227
|
+
let lastReplayEntry: RootBlobCandidate | undefined;
|
|
228
|
+
let collapsedRepeats = 0;
|
|
229
|
+
let maxRunLength = 1;
|
|
230
|
+
let currentRun = 1;
|
|
231
|
+
const pushDeduped = (
|
|
232
|
+
payload: { role: string; content: [{ type: "text"; text: string }] },
|
|
233
|
+
role: RootBlobCandidate["role"],
|
|
234
|
+
opts: { messageIndex: number; text?: string },
|
|
235
|
+
normalized: string,
|
|
236
|
+
): void => {
|
|
237
|
+
if (externalModel && lastReplayText !== undefined && normalized === lastReplayText && lastReplayEntry) {
|
|
238
|
+
collapsedRepeats++;
|
|
239
|
+
currentRun++;
|
|
240
|
+
if (currentRun > maxRunLength) maxRunLength = currentRun;
|
|
241
|
+
const marked = `${normalized}\n[note: this exact output was produced ${currentRun} times in a row]`;
|
|
242
|
+
const replacement = rootBlobCandidate(
|
|
243
|
+
{ role: payload.role, content: [{ type: "text", text: marked }] },
|
|
244
|
+
role,
|
|
245
|
+
opts,
|
|
246
|
+
);
|
|
247
|
+
entries[entries.indexOf(lastReplayEntry)] = replacement;
|
|
248
|
+
lastReplayEntry = replacement;
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
currentRun = 1;
|
|
252
|
+
const entry = rootBlobCandidate(payload, role, opts);
|
|
253
|
+
entries.push(entry);
|
|
254
|
+
lastReplayText = normalized;
|
|
255
|
+
lastReplayEntry = entry;
|
|
256
|
+
};
|
|
220
257
|
|
|
221
258
|
for (let i = 0; i < messages.length; i++) {
|
|
222
259
|
if (i === activeUserIndex) break;
|
|
@@ -228,6 +265,9 @@ function rootPromptMessages(request: CursorRunRequest, requestScope: CursorBlobR
|
|
|
228
265
|
// A bare string survives blob hydration but external workers reject the completed replay
|
|
229
266
|
// before tokenization (`usedTokens: 0`, then invalid_argument).
|
|
230
267
|
if (text.length > 0) {
|
|
268
|
+
lastReplayText = undefined;
|
|
269
|
+
lastReplayEntry = undefined;
|
|
270
|
+
currentRun = 1;
|
|
231
271
|
entries.push(rootBlobCandidate({
|
|
232
272
|
role: "user",
|
|
233
273
|
content: [{ type: "text", text }],
|
|
@@ -238,11 +278,12 @@ function rootPromptMessages(request: CursorRunRequest, requestScope: CursorBlobR
|
|
|
238
278
|
// Native Composer state can preserve it through ThinkingMessage/history structures.
|
|
239
279
|
const text = assistantRootText(message, !externalModel).trim();
|
|
240
280
|
if (text.length > 0) {
|
|
241
|
-
|
|
281
|
+
pushDeduped(
|
|
242
282
|
{ role: "assistant", content: [{ type: "text", text }] },
|
|
243
283
|
"assistant",
|
|
244
284
|
{ messageIndex: i },
|
|
245
|
-
|
|
285
|
+
text,
|
|
286
|
+
);
|
|
246
287
|
}
|
|
247
288
|
// Assistant tool CALLS are intentionally NOT replayed as visible "[Tool Call]" text here.
|
|
248
289
|
} else if (message.role === "toolResult") {
|
|
@@ -254,13 +295,16 @@ function rootPromptMessages(request: CursorRunRequest, requestScope: CursorBlobR
|
|
|
254
295
|
// node_repl result is an error even when the runtime said isError=false).
|
|
255
296
|
const prefix = normalizedToolResult(message, contentToText(message.content)).isError ? "[Tool Error]" : "[Tool Result]";
|
|
256
297
|
const text = `${prefix}\n${toolResultToText(message)}`;
|
|
257
|
-
|
|
258
|
-
toolResultRootPayload(text),
|
|
259
|
-
"toolResult",
|
|
260
|
-
{ messageIndex: i, text },
|
|
261
|
-
));
|
|
298
|
+
pushDeduped(toolResultRootPayload(text), "toolResult", { messageIndex: i, text }, text);
|
|
262
299
|
}
|
|
263
300
|
}
|
|
301
|
+
// Severe repetition: tell the model ONCE, imperatively, to change strategy.
|
|
302
|
+
if (externalModel && maxRunLength >= 3) {
|
|
303
|
+
entries.push(rootBlobCandidate({
|
|
304
|
+
role: "user",
|
|
305
|
+
content: [{ type: "text", text: `[context note] The transcript above contains the same output repeated ${maxRunLength} times in a row. Repeating it again is a failure. Take a DIFFERENT action now, or state plainly what is blocking progress.` }],
|
|
306
|
+
}, "user", {}));
|
|
307
|
+
}
|
|
264
308
|
|
|
265
309
|
let selected = entries;
|
|
266
310
|
let historyMessageStart = 0;
|
|
@@ -436,6 +480,7 @@ function decodeResultParts(message: OcxToolResultMessage): DecodedResultPart[] |
|
|
|
436
480
|
if (typeof content === "string") return undefined;
|
|
437
481
|
return content.map((part): DecodedResultPart => {
|
|
438
482
|
if (part.type === "text") return { kind: "text", text: part.text };
|
|
483
|
+
if (part.type === "video") return { kind: "text", text: "[video]" };
|
|
439
484
|
const decoded = decodeInlineImage(part.imageUrl);
|
|
440
485
|
return decoded ? { kind: "image", ...decoded } : { kind: "undecodable" };
|
|
441
486
|
});
|
|
@@ -530,7 +575,7 @@ function toolResultToText(message: OcxToolResultMessage): string {
|
|
|
530
575
|
const normalized = normalizedToolResult(message, contentToText(message.content));
|
|
531
576
|
return [
|
|
532
577
|
"[tool_result]",
|
|
533
|
-
`call_id: ${message.toolCallId}`,
|
|
578
|
+
`call_id: ${decodeCursorCallId(message.toolCallId)}`,
|
|
534
579
|
`name: ${namespacedToolName(message.toolNamespace, message.toolName)}`,
|
|
535
580
|
`is_error: ${normalized.isError}`,
|
|
536
581
|
"output:",
|
|
@@ -591,7 +636,7 @@ function toolCallStep(
|
|
|
591
636
|
args: create(McpArgsSchema, {
|
|
592
637
|
name: toolName,
|
|
593
638
|
toolName,
|
|
594
|
-
toolCallId: part.id,
|
|
639
|
+
toolCallId: decodeCursorCallId(part.id),
|
|
595
640
|
providerIdentifier: OCX_RESPONSES_TOOL_PROVIDER,
|
|
596
641
|
args,
|
|
597
642
|
}),
|
|
@@ -837,8 +882,10 @@ function buildPreparedCursorRunRequest(
|
|
|
837
882
|
? "userMessageAction"
|
|
838
883
|
: "resumeAction";
|
|
839
884
|
const actionText = externalToolContinuation
|
|
840
|
-
? CURSOR_EXTERNAL_TOOL_CONTINUATION_TEXT
|
|
841
|
-
:
|
|
885
|
+
? (request.echoRetryContinuationText ?? CURSOR_EXTERNAL_TOOL_CONTINUATION_TEXT)
|
|
886
|
+
: request.echoRetryContinuationText
|
|
887
|
+
? `${text}\n\n[correction] ${request.echoRetryContinuationText}`
|
|
888
|
+
: text;
|
|
842
889
|
const action = create(ConversationActionSchema, {
|
|
843
890
|
action: actionCase === "userMessageAction"
|
|
844
891
|
? {
|
|
@@ -965,12 +1012,15 @@ function buildPreparedCursorRunRequest(
|
|
|
965
1012
|
displayName: request.modelId,
|
|
966
1013
|
displayNameShort: request.modelId,
|
|
967
1014
|
aliases: [],
|
|
1015
|
+
...(request.maxMode === true ? { maxMode: true } : {}),
|
|
968
1016
|
}),
|
|
969
1017
|
} : {}),
|
|
970
|
-
...(requestedModelParameters.length > 0 ? {
|
|
1018
|
+
...(requestedModelParameters.length > 0 || request.maxMode === true ? {
|
|
971
1019
|
requestedModel: create(RequestedModelSchema, {
|
|
972
1020
|
modelId: request.modelId,
|
|
973
|
-
|
|
1021
|
+
// Max Mode must be raised on BOTH RequestedModel and ModelDetails; missing either
|
|
1022
|
+
// can invalid_argument upstream (devlog 260826 070).
|
|
1023
|
+
maxMode: request.maxMode === true,
|
|
974
1024
|
parameters: requestedModelParameters.map(parameter =>
|
|
975
1025
|
create(RequestedModel_ModelParameterbytesSchema, parameter)),
|
|
976
1026
|
}),
|
|
@@ -988,7 +1038,11 @@ function buildPreparedCursorRunRequest(
|
|
|
988
1038
|
// the event-state `clientToolNames` use (live-transport.ts). Advertising the raw `request.tools`
|
|
989
1039
|
// here would let mcp_tools expose a tool that the event state does not recognize for a generic
|
|
990
1040
|
// tool-count prompt, so a call to it would be rejected as an unknown Responses tool.
|
|
991
|
-
|
|
1041
|
+
// An explicitly empty McpTools wrapper (bare API callers) suppresses Cursor's default
|
|
1042
|
+
// native catalog; an absent field lets identified Codex sessions keep it (devlog 260826 040).
|
|
1043
|
+
...(mcpToolDefs.length > 0 || request.suppressDefaultCursorToolCatalog === true
|
|
1044
|
+
? { mcpTools: create(McpToolsSchema, { mcpTools: mcpToolDefs }) }
|
|
1045
|
+
: {}),
|
|
992
1046
|
});
|
|
993
1047
|
|
|
994
1048
|
const message = create(AgentClientMessageSchema, {
|
|
@@ -10,6 +10,8 @@ import type {
|
|
|
10
10
|
import { isAllowedToolChoice, namespacedToolName, toolChoiceAliases, type OcxTool, type OcxToolChoice } from "../../types";
|
|
11
11
|
import type { CursorRequestMessage, CursorRequestedModelParameter, CursorRunRequest } from "./types";
|
|
12
12
|
import { cursorCheckpointModelAffinityId, cursorWireModelSelection, type CursorRoutingLevel } from "./discovery";
|
|
13
|
+
import { cursorUltraBaseModelId } from "./discovery";
|
|
14
|
+
import { decodeCursorCallId } from "./call-id";
|
|
13
15
|
import { cursorEffortSuffix, cursorRequestWireModelIdWithEffort } from "./effort-map";
|
|
14
16
|
import {
|
|
15
17
|
cursorMcpToolEncodedSize,
|
|
@@ -188,13 +190,19 @@ function normalizeCursorModelId(modelId: string, reasoning?: string): {
|
|
|
188
190
|
modelId: string;
|
|
189
191
|
requestedModelParameters?: readonly CursorRequestedModelParameter[];
|
|
190
192
|
routingLevel?: CursorRoutingLevel;
|
|
193
|
+
maxMode?: boolean;
|
|
191
194
|
} {
|
|
192
|
-
|
|
195
|
+
// Synthetic ultra (-1m) picker rows resolve to their wire base with Max Mode on
|
|
196
|
+
// (devlog 260826 070); the marker never reaches the wire.
|
|
197
|
+
const ultraBase = cursorUltraBaseModelId(modelId);
|
|
198
|
+
const selection = cursorWireModelSelection(ultraBase ?? modelId);
|
|
199
|
+
const maxMode = ultraBase !== undefined ? { maxMode: true } : {};
|
|
193
200
|
const id = selection.modelId;
|
|
194
201
|
const suffix = cursorEffortSuffix(id, reasoning);
|
|
195
202
|
if ((id === "grok-4.5-fast" || id === "grok-4.6-fast") && suffix) {
|
|
196
203
|
return {
|
|
197
204
|
...selection,
|
|
205
|
+
...maxMode,
|
|
198
206
|
modelId: id.slice(0, -"-fast".length),
|
|
199
207
|
requestedModelParameters: [
|
|
200
208
|
{ id: "effort", value: suffix },
|
|
@@ -202,7 +210,7 @@ function normalizeCursorModelId(modelId: string, reasoning?: string): {
|
|
|
202
210
|
],
|
|
203
211
|
};
|
|
204
212
|
}
|
|
205
|
-
return { ...selection, modelId: suffix ? cursorRequestWireModelIdWithEffort(id, suffix) : id };
|
|
213
|
+
return { ...selection, ...maxMode, modelId: suffix ? cursorRequestWireModelIdWithEffort(id, suffix) : id };
|
|
206
214
|
}
|
|
207
215
|
|
|
208
216
|
function contentPartToText(part: OcxContentPart | OcxAssistantContentPart): string | undefined {
|
|
@@ -226,7 +234,7 @@ function contentPartToText(part: OcxContentPart | OcxAssistantContentPart): stri
|
|
|
226
234
|
function toolResultToText(message: OcxToolResultMessage): string {
|
|
227
235
|
return [
|
|
228
236
|
"[tool_result]",
|
|
229
|
-
`call_id: ${message.toolCallId}`,
|
|
237
|
+
`call_id: ${decodeCursorCallId(message.toolCallId)}`,
|
|
230
238
|
`name: ${namespacedToolName(message.toolNamespace, message.toolName)}`,
|
|
231
239
|
`is_error: ${message.isError}`,
|
|
232
240
|
"output:",
|
|
@@ -298,7 +306,7 @@ export function cursorConversationIdFromClientThread(threadId: string, identityS
|
|
|
298
306
|
|
|
299
307
|
/**
|
|
300
308
|
* Resolve the Cursor conversation id for this turn.
|
|
301
|
-
* Priority: force-fresh → isolate helper → remembered →
|
|
309
|
+
* Priority: force-fresh → isolate helper → remembered → client thread owner → random.
|
|
302
310
|
* Never use OpenAI Responses `previous_response_id` (resp_*) or shared `prompt_cache_key`
|
|
303
311
|
* (cache-cohort fingerprint, not conversation ownership).
|
|
304
312
|
*/
|
|
@@ -310,7 +318,7 @@ export function resolveCursorConversationId(
|
|
|
310
318
|
if (options.forceFreshConversation === true) return generatedCursorConversationId();
|
|
311
319
|
if (parsed._cursorIsolateConversation === true) return generatedCursorConversationId();
|
|
312
320
|
if (parsed._cursorConversationId) return parsed._cursorConversationId;
|
|
313
|
-
const threadId = parsed
|
|
321
|
+
const threadId = cursorClientThreadOwner(parsed);
|
|
314
322
|
if (threadId) {
|
|
315
323
|
const recovered = lookupCursorThreadConversation(threadId, parsed._cursorIdentityScope);
|
|
316
324
|
if (recovered) return recovered;
|
|
@@ -319,6 +327,10 @@ export function resolveCursorConversationId(
|
|
|
319
327
|
return generatedCursorConversationId();
|
|
320
328
|
}
|
|
321
329
|
|
|
330
|
+
export function cursorClientThreadOwner(parsed: OcxParsedRequest): string | undefined {
|
|
331
|
+
return parsed._clientThreadId?.trim() || parsed._cursorClientThreadId?.trim() || undefined;
|
|
332
|
+
}
|
|
333
|
+
|
|
322
334
|
function updateFramed(hash: ReturnType<typeof createHash>, value: string): void {
|
|
323
335
|
const bytes = Buffer.from(value, "utf8");
|
|
324
336
|
const length = Buffer.allocUnsafe(4);
|
|
@@ -361,6 +373,7 @@ function lookupPrefixSnapshot(
|
|
|
361
373
|
const modelId = cursorCheckpointModelAffinityId(request.modelId);
|
|
362
374
|
for (let covered = parsed.context.messages.length; covered >= 1; covered--) {
|
|
363
375
|
const snapshot = getCursorCheckpointForPrefix({
|
|
376
|
+
conversationId: request.conversationId,
|
|
364
377
|
prefixDigest: cursorCoveredPrefixDigest(parsed, covered),
|
|
365
378
|
systemDigest,
|
|
366
379
|
coveredMessageCount: covered,
|
|
@@ -404,10 +417,14 @@ function resolveCursorCheckpoint(
|
|
|
404
417
|
snapshot = getCursorCheckpoint(ref);
|
|
405
418
|
if (!snapshot) return { reason: "expired" };
|
|
406
419
|
} else {
|
|
420
|
+
if (
|
|
421
|
+
isolated
|
|
422
|
+
|| (!parsed._cursorConversationId && !cursorClientThreadOwner(parsed))
|
|
423
|
+
) return { reason: "missing_ref" };
|
|
407
424
|
snapshot = lookupPrefixSnapshot(parsed, request, identityScope);
|
|
408
425
|
if (!snapshot) return { reason: "missing_ref" };
|
|
409
426
|
}
|
|
410
|
-
if (
|
|
427
|
+
if (snapshot.conversationId !== request.conversationId) {
|
|
411
428
|
return { reason: "conversation_changed" };
|
|
412
429
|
}
|
|
413
430
|
if (snapshot.identityScope !== identityScope) return { reason: "identity_changed" };
|
|
@@ -436,6 +453,7 @@ export function createCursorRequest(
|
|
|
436
453
|
modelId: model.modelId,
|
|
437
454
|
...(model.requestedModelParameters ? { requestedModelParameters: model.requestedModelParameters } : {}),
|
|
438
455
|
...(model.routingLevel ? { routingLevel: model.routingLevel } : {}),
|
|
456
|
+
...(model.maxMode ? { maxMode: true } : {}),
|
|
439
457
|
conversationId: resolveCursorConversationId(parsed, model.modelId, options),
|
|
440
458
|
system: [...(parsed.context.systemPrompt ?? []), ...(limitNote ? [limitNote] : [])],
|
|
441
459
|
messages,
|
|
@@ -443,6 +461,11 @@ export function createCursorRequest(
|
|
|
443
461
|
...(parsed._compactionRequest === true || parsed._contextCompactionBoundary === true ? { contextUsageReset: true } : {}),
|
|
444
462
|
...(parsed._compactionRequest === true ? { contextUsageStoreCheckpoints: false } : {}),
|
|
445
463
|
...(budget.tools.length ? { tools: budget.tools } : {}),
|
|
464
|
+
// Bare API caller (no tools, no Codex thread identity): suppress Cursor's default
|
|
465
|
+
// native tool catalog instead of paying its ~10-15K token preamble (devlog 260826 040).
|
|
466
|
+
...(budget.tools.length === 0 && !cursorClientThreadOwner(parsed)
|
|
467
|
+
? { suppressDefaultCursorToolCatalog: true }
|
|
468
|
+
: {}),
|
|
446
469
|
...(parsed.options.toolChoice ? { toolChoice: parsed.options.toolChoice } : {}),
|
|
447
470
|
...(parsed.options.parallelToolCalls !== undefined ? { parallelToolCalls: parsed.options.parallelToolCalls } : {}),
|
|
448
471
|
};
|
|
@@ -656,6 +656,9 @@ export function buildCursorToolGuidanceSystemNote(
|
|
|
656
656
|
codeMode
|
|
657
657
|
? "In code mode the isolate returns nothing on its own: call `text(...)` (or `notify(...)`) on any value you need to see, or the call completes with empty output. There is no `require`, no `module`, and no filesystem or network globals; reach the host only through the nested helpers."
|
|
658
658
|
: undefined,
|
|
659
|
+
codeMode
|
|
660
|
+
? "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."
|
|
661
|
+
: undefined,
|
|
659
662
|
hasBareExec
|
|
660
663
|
? `${shellBridgeLabel} is the Codex Responses shell bridge for this turn, exposed through Cursor's tool protocol; it is not an external MCP server tool. \`shell_command\` and \`exec_command\` are aliases of the same bridge.`
|
|
661
664
|
: undefined,
|
|
@@ -663,7 +666,10 @@ export function buildCursorToolGuidanceSystemNote(
|
|
|
663
666
|
? "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."
|
|
664
667
|
: undefined,
|
|
665
668
|
hasBareExec
|
|
666
|
-
? `
|
|
669
|
+
? `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.`
|
|
670
|
+
: undefined,
|
|
671
|
+
hasBareExec
|
|
672
|
+
? "Tool-selection commentary is forbidden: for any shell, read, grep, list, or file operation, your FIRST visible action is the bridge call itself — never a sentence about which tool you will use, which tool was redirected, or switching surfaces. Words like 차단/전환/blocked/switching must not appear in your output for tool-routing reasons."
|
|
667
673
|
: undefined,
|
|
668
674
|
hostShellNote,
|
|
669
675
|
"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.",
|
|
@@ -675,6 +681,9 @@ export function buildCursorToolGuidanceSystemNote(
|
|
|
675
681
|
? `For file edits, prefer the structured edit tools ${quotedNames(structuredEditNames)} — they take replacements that OpenCodex converts into Codex \`apply_patch\` changes. Include exact leading whitespace in old_string/new_string. Use \`apply_patch\` directly only with a \`*** Begin Patch\` envelope and bare \`@@\` hunks (never git-style \`@@ -n,m +n,m @@\`); never emit patch-like plain text as tool arguments.`
|
|
676
682
|
: "For file edits, use the `apply_patch` tool, not built-in file write/delete tools."
|
|
677
683
|
: undefined,
|
|
684
|
+
hasApplyPatch
|
|
685
|
+
? "Creating or modifying file CONTENT via shell redirection (`>`, `>>`, `printf`/`echo` into a file, `cat <<EOF`, `sed -i`) is forbidden while apply_patch or the structured edit tools are advertised — use those edit tools so the change is reviewable. Shell output redirection is fine for logs/scratch pipes that are not the deliverable file."
|
|
686
|
+
: undefined,
|
|
678
687
|
hasBareExec
|
|
679
688
|
? "For tool-count demos, each counted tool must be a separate Codex shell-bridge invocation/result; do not collapse several requested tools into one chained shell command."
|
|
680
689
|
: undefined,
|
|
@@ -687,7 +696,7 @@ export function buildCursorToolGuidanceSystemNote(
|
|
|
687
696
|
: undefined,
|
|
688
697
|
"Do not count or report a tool call unless a tool result was actually returned.",
|
|
689
698
|
hasBareExec
|
|
690
|
-
? `
|
|
699
|
+
? `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.`
|
|
691
700
|
: undefined,
|
|
692
701
|
].filter((note): note is string => typeof note === "string");
|
|
693
702
|
return notes.join(" ");
|
|
@@ -29,6 +29,31 @@ function isNodeReplOrComputerUseTool(toolName?: string, toolNamespace?: string):
|
|
|
29
29
|
return lower.startsWith("mcp__node_repl") || lower.startsWith("mcp__computer_use");
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Codex exec / shell-bridge tool names (flat and MCP-prefixed display aliases). An empty result
|
|
34
|
+
* here is almost always a code-mode cell that never called text()/notify() — the cursor model
|
|
35
|
+
* reads the blank [tool_result], concludes prior results were lost, and spirals into
|
|
36
|
+
* re-orientation retries (devlog 260826_cursor_responses_gap, live subagent transcripts).
|
|
37
|
+
*/
|
|
38
|
+
function isCodexExecBridgeTool(toolName?: string, toolNamespace?: string): boolean {
|
|
39
|
+
if (toolNamespace && toolNamespace.includes("opencodex-responses")) return true;
|
|
40
|
+
if (!toolName) return false;
|
|
41
|
+
const lower = toolName.toLowerCase();
|
|
42
|
+
return (
|
|
43
|
+
lower === "exec"
|
|
44
|
+
|| lower === "exec_command"
|
|
45
|
+
|| lower === "shell_command"
|
|
46
|
+
// Codex CLI/desktop native tool names: the multi-round "이전 출력이 비어 있어 처음부터"
|
|
47
|
+
// restart loop reproduced via codex exec because `shell` was not in this set
|
|
48
|
+
// (devlog 260826 gap-8 QA round 2).
|
|
49
|
+
|| lower === "shell"
|
|
50
|
+
|| lower === "local_shell"
|
|
51
|
+
|| lower === "container.exec"
|
|
52
|
+
|| lower.startsWith("mcp_opencodex-responses_")
|
|
53
|
+
|| lower.startsWith("mcp__opencodex-responses__")
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
32
57
|
/** Failure states the Computer Use / node_repl runtime reports as PLAIN TEXT inside a non-error result. */
|
|
33
58
|
const RUNTIME_FAILURE_GUIDANCE: ReadonlyArray<{ marker: string; guidance: string }> = [
|
|
34
59
|
{
|
|
@@ -80,6 +105,13 @@ export function normalizeCursorToolResultText(
|
|
|
80
105
|
changed: true,
|
|
81
106
|
};
|
|
82
107
|
}
|
|
108
|
+
if (isCodexExecBridgeTool(options.toolName, options.toolNamespace) && EMPTY_EXEC_OUTPUT_REGEX.test(text.trim())) {
|
|
109
|
+
return {
|
|
110
|
+
text: "[empty output: the exec cell completed but emitted nothing. This is NOT lost context and NOT a blocked tool — in code mode call text(...) or notify(...) on any value you need to see (a bare await tools.exec_command(...) is not echoed automatically); in shell mode the command simply printed nothing. Do not re-run the same call expecting different output.]",
|
|
111
|
+
isError: false,
|
|
112
|
+
changed: true,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
83
115
|
if (!isError) {
|
|
84
116
|
for (const { marker, guidance } of RUNTIME_FAILURE_GUIDANCE) {
|
|
85
117
|
if (text.includes(marker)) {
|
|
@@ -89,4 +121,3 @@ export function normalizeCursorToolResultText(
|
|
|
89
121
|
}
|
|
90
122
|
return { text, isError, changed: false };
|
|
91
123
|
}
|
|
92
|
-
|
|
@@ -15,6 +15,26 @@ export interface CursorRunRequest {
|
|
|
15
15
|
requestedModelParameters?: readonly CursorRequestedModelParameter[];
|
|
16
16
|
/** Cursor Router optimization parameter; valid only while modelId is the `default` wire model. */
|
|
17
17
|
routingLevel?: CursorRoutingLevel;
|
|
18
|
+
/**
|
|
19
|
+
* Cursor Max Mode (ultra/big-context). Set from a synthetic `-1m` picker variant; the wire
|
|
20
|
+
* keeps the original model id and raises RequestedModel.maxMode + ModelDetails.maxMode
|
|
21
|
+
* (both fields — missing either can invalid_argument upstream). Devlog 260826 070.
|
|
22
|
+
*/
|
|
23
|
+
maxMode?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Bare API callers (no caller tools, no Codex thread identity) pay a ~10-15K input-token
|
|
26
|
+
* preamble because an absent AgentRunRequest.mcp_tools field makes Cursor inject its default
|
|
27
|
+
* native tool catalog. When true, an explicitly empty McpTools wrapper is serialized instead,
|
|
28
|
+
* suppressing that default. Codex-identified sessions keep the absent-field behavior.
|
|
29
|
+
*/
|
|
30
|
+
suppressDefaultCursorToolCatalog?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Corrective active-turn text for the single envelope-echo retry (devlog 260826 gap-10).
|
|
33
|
+
* When set on an external tool-result continuation, buildPreparedCursorRunRequest uses it as
|
|
34
|
+
* the userMessageAction text instead of the standard continuation text; rawMessages stay
|
|
35
|
+
* untouched so history replay is unchanged.
|
|
36
|
+
*/
|
|
37
|
+
echoRetryContinuationText?: string;
|
|
18
38
|
conversationId: string;
|
|
19
39
|
system: string[];
|
|
20
40
|
messages: CursorRequestMessage[];
|