@bitkyc08/opencodex 2.57.0 → 2.59.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 +28 -10
- package/gui/dist/assets/index-C5IebErG.js +136 -0
- package/gui/dist/assets/{index-C5-RdDmD.css → index-OESInAjC.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/crusoe.svg +1 -0
- package/gui/dist/provider-icons/opper.svg +3 -0
- package/package.json +2 -2
- package/src/adapters/base.ts +11 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +5 -4
- package/src/adapters/command-code.ts +13 -4
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +65 -1
- package/src/adapters/cursor/effort-map.ts +16 -2
- package/src/adapters/cursor/envelope-echo.ts +55 -2
- package/src/adapters/cursor/live-transport.ts +5 -1
- package/src/adapters/cursor/message-mapper.ts +3 -2
- package/src/adapters/cursor/protobuf-events.ts +110 -11
- package/src/adapters/cursor/protobuf-request.ts +27 -6
- package/src/adapters/cursor/request-builder.ts +14 -3
- package/src/adapters/cursor/text-toolcall.ts +230 -0
- package/src/adapters/cursor/thread-continuity.ts +141 -0
- package/src/adapters/cursor/tool-guidance.ts +5 -4
- package/src/adapters/cursor/types.ts +5 -0
- package/src/adapters/cursor.ts +97 -6
- package/src/adapters/devin/cloud-direct/chat.ts +11 -2
- package/src/adapters/devin/cloud-direct/index.ts +7 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +103 -0
- package/src/adapters/devin.ts +75 -13
- package/src/adapters/google-antigravity-wire.ts +29 -2
- package/src/adapters/google-http.ts +45 -13
- package/src/adapters/google.ts +23 -4
- package/src/adapters/mimo-free.ts +32 -17
- package/src/adapters/ollama-native.ts +42 -8
- package/src/adapters/openai-chat/response-events.ts +61 -0
- package/src/adapters/openai-chat.ts +5 -10
- package/src/adapters/openai-responses/passthrough.ts +40 -5
- package/src/adapters/openai-responses/request-strips.ts +43 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
- package/src/adapters/openai-responses/tool-schema.ts +19 -7
- package/src/adapters/physical-send.ts +50 -0
- package/src/adapters/responses-tool-schema.ts +76 -46
- package/src/adapters/run-turn-queue.ts +17 -4
- package/src/bridge/response-json.ts +2 -2
- package/src/bridge/sse.ts +166 -25
- package/src/claude/context-windows.ts +22 -0
- package/src/claude/outbound.ts +46 -5
- package/src/cli/account-api.ts +4 -3
- package/src/cli/account-extended.ts +22 -2
- package/src/cli/account-orca-import.ts +63 -0
- package/src/cli/account.ts +32 -4
- package/src/cli/capabilities.ts +40 -0
- package/src/cli/claude.ts +29 -1
- package/src/cli/codex-cli-update.ts +97 -2
- package/src/cli/config-command.ts +35 -18
- package/src/cli/dispatch.ts +71 -4
- package/src/cli/doctor.ts +197 -2
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +132 -22
- package/src/cli/models-runtime.ts +33 -4
- package/src/cli/registry.ts +11 -1
- package/src/cli/runtime-api.ts +44 -0
- package/src/cli/start-args.ts +94 -0
- package/src/cli/system-command.ts +72 -1
- package/src/cli/uninstall-client-state.ts +12 -0
- package/src/client/machine-api.ts +4 -3
- package/src/client/machine-listener.ts +14 -1
- package/src/clients/config-export/constants.ts +2 -3
- package/src/clients/config-export.ts +5 -5
- package/src/codex/account-store.ts +81 -5
- package/src/codex/auth-api/pool-quota-probe.ts +14 -3
- package/src/codex/auth-api/routes.ts +17 -2
- package/src/codex/auth-context.ts +58 -20
- package/src/codex/catalog/build-entries.ts +25 -4
- package/src/codex/catalog/derive-entry.ts +8 -1
- package/src/codex/catalog/effort.ts +10 -6
- package/src/codex/catalog/gather-capture.ts +1 -0
- package/src/codex/catalog/model-hints.ts +37 -5
- package/src/codex/catalog/parsing.ts +83 -5
- package/src/codex/catalog/reserve-warn.ts +96 -0
- package/src/codex/catalog/retained-sync.ts +19 -0
- package/src/codex/catalog/routed-gather.ts +42 -3
- package/src/codex/cli-installation-identity.ts +210 -0
- package/src/codex/cli-installation-targets.ts +158 -0
- package/src/codex/convergence.ts +5 -0
- package/src/codex/desktop-switches.ts +145 -0
- package/src/codex/history-job.ts +5 -1
- package/src/codex/history-provider.ts +37 -5
- package/src/codex/history-state-open.ts +105 -0
- package/src/codex/history-worker.ts +14 -1
- package/src/codex/inject/config-toml.ts +44 -2
- package/src/codex/inject/remove.ts +145 -7
- package/src/codex/inject/restore.ts +204 -32
- package/src/codex/inject.ts +6 -9
- package/src/codex/lineage.ts +83 -32
- package/src/codex/loopback-target.ts +40 -0
- package/src/codex/main-account-hard-lock.ts +2 -1
- package/src/codex/main-account.ts +10 -3
- package/src/codex/main-device-reauth.ts +17 -9
- package/src/codex/model-entitlements.ts +60 -1
- package/src/codex/native-profile-startup.ts +64 -20
- package/src/codex/observed-model-denials.ts +137 -0
- package/src/codex/orca-auth-source.ts +94 -0
- package/src/codex/orca-import.ts +219 -0
- package/src/codex/prompt-text-probe.ts +282 -12
- package/src/codex/quota-401-recovery.ts +12 -0
- package/src/codex/quota-types.ts +65 -0
- package/src/codex/quota.ts +24 -19
- package/src/codex/routing/cooldown-math.ts +8 -47
- package/src/codex/routing/pin-drain.ts +57 -0
- package/src/codex/routing.ts +13 -15
- package/src/codex/subagent-model-fallback.ts +94 -0
- package/src/codex/windows-installation-files.ts +224 -0
- package/src/combos/failover.ts +122 -5
- package/src/config/atomic-write.ts +83 -8
- package/src/config/diagnostics.ts +21 -0
- package/src/config/load-degrade.ts +15 -0
- package/src/config/pending-teardown.ts +8 -0
- package/src/config/process-state.ts +36 -3
- package/src/config/provider-relative-send-path.ts +16 -0
- package/src/config/proxy-env.ts +23 -5
- package/src/config/schema/config-schema.ts +23 -0
- package/src/config/schema/leaf-validators.ts +65 -17
- package/src/generated/compatibility-version.json +337 -201
- package/src/generated/model-metadata.ts +1 -1
- package/src/lib/bounded-body.ts +4 -2
- package/src/lib/bounded-subprocess.ts +62 -10
- package/src/lib/destination-policy.ts +48 -6
- package/src/lib/errors.ts +3 -15
- package/src/lib/local-destinations.ts +32 -5
- package/src/lib/provider-outbound.ts +3 -3
- package/src/lib/proxy-env.ts +70 -3
- package/src/lib/request-execution-budget.ts +11 -3
- package/src/lib/response-body-inactivity.ts +193 -0
- package/src/lib/retry-delay.ts +69 -0
- package/src/lib/socks5-fetch.ts +631 -0
- package/src/lib/spend-reservation-ledger.ts +115 -9
- package/src/lib/windows-secret-acl.ts +151 -15
- package/src/lib/windows-user-principal.ts +5 -1
- package/src/lib/workflow-budget.ts +145 -8
- package/src/oauth/account-quota-rank.ts +72 -15
- package/src/oauth/generic-account-failover.ts +40 -27
- package/src/oauth/orcarouter.ts +15 -2
- package/src/oauth/store.ts +8 -0
- package/src/providers/codex-capacity.ts +9 -0
- package/src/providers/derive.ts +6 -0
- package/src/providers/devin-provider-merge-migration.ts +33 -12
- package/src/providers/free-directory.ts +20 -2
- package/src/providers/key-failover.ts +261 -7
- package/src/providers/model-discovery.ts +19 -7
- package/src/providers/model-rename-migration.ts +1 -0
- package/src/providers/openai-sidecar.ts +4 -0
- package/src/providers/opencode-go-transport.ts +14 -5
- package/src/providers/quota/report-cache.ts +3 -0
- package/src/providers/registry/entries-core.ts +11 -0
- package/src/providers/registry/entries-extended.ts +146 -28
- package/src/providers/registry/model-seeds.ts +136 -29
- package/src/providers/registry/types.ts +9 -0
- package/src/responses/apply-patch-envelope.ts +44 -11
- package/src/responses/bridge-search-replay-cache.ts +152 -0
- package/src/responses/code-mode-helper-compat.ts +26 -16
- package/src/responses/custom-tool-compat.ts +1 -1
- package/src/responses/hosted-tool-policy.ts +85 -2
- package/src/responses/schema.ts +9 -2
- package/src/responses/spill-store.ts +17 -0
- package/src/responses/state/body-policy.ts +25 -0
- package/src/responses/state/spill-queue.ts +8 -6
- package/src/responses/state.ts +3 -22
- package/src/router.ts +4 -0
- package/src/server/auth-cors.ts +27 -0
- package/src/server/chat-completions.ts +9 -4
- package/src/server/chat-native-sse.ts +26 -9
- package/src/server/chat-native.ts +10 -4
- package/src/server/claude-messages.ts +24 -2
- package/src/server/gui-static.ts +36 -2
- package/src/server/inbound-body-admission.ts +187 -0
- package/src/server/index/websocket-handler.ts +48 -1
- package/src/server/index.ts +15 -19
- package/src/server/management/api-access.ts +3 -4
- package/src/server/management/config-routes.ts +57 -10
- package/src/server/management/provider-capability-config.ts +35 -7
- package/src/server/management/provider-routes.ts +70 -18
- package/src/server/models-capabilities.ts +24 -3
- package/src/server/proxy-liveness.ts +97 -2
- package/src/server/relay.ts +17 -24
- package/src/server/request-log.ts +25 -1
- package/src/server/responses/adapter-continuation.ts +71 -27
- package/src/server/responses/adapter-delivery.ts +39 -8
- package/src/server/responses/adapter-dispatch.ts +52 -24
- package/src/server/responses/codex-ws-exchange.ts +65 -4
- package/src/server/responses/combo-stream-preflight.ts +68 -5
- package/src/server/responses/compact.ts +60 -11
- package/src/server/responses/core-codex-account.ts +83 -22
- package/src/server/responses/core-combo.ts +26 -0
- package/src/server/responses/core-normalize.ts +12 -5
- package/src/server/responses/core-options.ts +3 -0
- package/src/server/responses/fetch-helpers.ts +72 -3
- package/src/server/responses/native-injection-protocol.ts +42 -0
- package/src/server/responses/native-injection-replay.ts +105 -0
- package/src/server/responses/native-injection.ts +242 -0
- package/src/server/responses/native-response-control.ts +56 -0
- package/src/server/responses/native-response-json.ts +14 -0
- package/src/server/responses/native-response-output.ts +37 -0
- package/src/server/responses/native-steering-log.ts +44 -0
- package/src/server/responses/native-steering-policy.ts +49 -0
- package/src/server/responses/native-steering-replay.ts +126 -0
- package/src/server/responses/native-steering-settings.ts +76 -0
- package/src/server/responses/native-steering.ts +400 -0
- package/src/server/responses/native-tool-results.ts +130 -0
- package/src/server/responses/passthrough-delivery.ts +21 -1
- package/src/server/responses/passthrough-dispatch.ts +146 -49
- package/src/server/responses/passthrough-execution.ts +11 -1
- package/src/server/responses/request-prepare.ts +70 -0
- package/src/server/responses/request-send-budget.ts +84 -7
- package/src/server/responses/request-sidecar-auth.ts +16 -8
- package/src/server/responses/request-spend.ts +38 -9
- package/src/server/responses/request-transport.ts +13 -10
- package/src/server/responses/run-turn-execution.ts +20 -5
- package/src/server/responses/sidecar-execution.ts +2 -0
- package/src/server/responses/ws-upstream.ts +23 -2
- package/src/server/responses-custom-tool-repair.ts +2 -2
- package/src/server/sse-frame-buffer.ts +12 -10
- package/src/server/sse-payload-rewrite.ts +36 -9
- package/src/server/stop-teardown.ts +8 -1
- package/src/server/system-env-shell.ts +5 -1
- package/src/server/system-env.ts +7 -1
- package/src/server/workflow-refusal.ts +56 -2
- package/src/server/ws-bridge.ts +16 -1
- package/src/service/cli.ts +29 -7
- package/src/service/guards.ts +10 -0
- package/src/service/health.ts +43 -0
- package/src/service/state.ts +7 -2
- package/src/types/accounts.ts +4 -0
- package/src/types/config.ts +104 -3
- package/src/types/provider.ts +32 -0
- package/src/types/request.ts +7 -1
- package/src/types/wire.ts +9 -1
- package/src/usage/expected-prices.ts +28 -0
- package/src/usage/log.ts +87 -4
- package/src/web-search/passthrough-bridge.ts +39 -5
- package/gui/dist/assets/index-Cz7CLdif.js +0 -128
|
@@ -6,6 +6,7 @@ import { namespacedToolName } from "../../types";
|
|
|
6
6
|
import type { CursorRunRequest } from "./types";
|
|
7
7
|
import { decodeCursorCallId } from "./call-id";
|
|
8
8
|
import { cursorNeedsExternalToolContinuation, isCursorExternalWireModel } from "./discovery";
|
|
9
|
+
import { stripAssistantEchoedToolEnvelope } from "./envelope-echo";
|
|
9
10
|
import { normalizeCursorToolResultText } from "./tool-result-normalize";
|
|
10
11
|
import { debugProviderDiagnostic } from "../../lib/debug";
|
|
11
12
|
import {
|
|
@@ -75,6 +76,8 @@ export const CURSOR_ROUTING_LEVEL_PARAMETER_ID = "optimization";
|
|
|
75
76
|
export const CURSOR_EXTERNAL_ROOT_BLOB_LIMIT = 192;
|
|
76
77
|
/** Approximate prompt-size guard; tool schemas and protocol framing consume context separately. */
|
|
77
78
|
export const CURSOR_EXTERNAL_ROOT_BYTE_LIMIT = 512 * 1024;
|
|
79
|
+
/** Honest placeholder when native Composer history has a toolCall with no matching toolResult. */
|
|
80
|
+
export const CURSOR_MISSING_TOOL_RESULT = "[missing tool_result for this tool_use in history]";
|
|
78
81
|
/**
|
|
79
82
|
* Byte budget for the serialized arguments named inside ONE replayed tool-result envelope. The
|
|
80
83
|
* invocation identifies the call; the result is the payload. Without an independent cap, a single
|
|
@@ -206,11 +209,13 @@ function assistantRootText(
|
|
|
206
209
|
message: Extract<OcxMessage, { role: "assistant" }>,
|
|
207
210
|
includeThinking: boolean,
|
|
208
211
|
): string {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
212
|
+
const raw = typeof message.content === "string"
|
|
213
|
+
? message.content
|
|
214
|
+
: message.content
|
|
215
|
+
.map(part => (part.type === "text" ? part.text : includeThinking && part.type === "thinking" ? part.thinking : undefined))
|
|
216
|
+
.filter((value): value is string => typeof value === "string" && value.length > 0)
|
|
217
|
+
.join("\n");
|
|
218
|
+
return stripAssistantEchoedToolEnvelope(raw);
|
|
214
219
|
}
|
|
215
220
|
|
|
216
221
|
// Cursor builds the actual model prompt from rootPromptMessagesJson (turns[] is UI/display metadata),
|
|
@@ -1218,6 +1223,20 @@ function argBytes(value: unknown): Uint8Array {
|
|
|
1218
1223
|
}
|
|
1219
1224
|
}
|
|
1220
1225
|
|
|
1226
|
+
function missingToolResultFor(
|
|
1227
|
+
part: Extract<OcxAssistantContentPart, { type: "toolCall" }>,
|
|
1228
|
+
): OcxToolResultMessage {
|
|
1229
|
+
return {
|
|
1230
|
+
role: "toolResult",
|
|
1231
|
+
toolCallId: part.id,
|
|
1232
|
+
toolName: part.name,
|
|
1233
|
+
...(part.namespace ? { toolNamespace: part.namespace } : {}),
|
|
1234
|
+
content: CURSOR_MISSING_TOOL_RESULT,
|
|
1235
|
+
isError: true,
|
|
1236
|
+
timestamp: 0,
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1221
1240
|
function toolCallStep(
|
|
1222
1241
|
part: Extract<OcxAssistantContentPart, { type: "toolCall" }>,
|
|
1223
1242
|
requestScope: CursorBlobRequestScopeToken,
|
|
@@ -1332,7 +1351,9 @@ function conversationTurns(
|
|
|
1332
1351
|
const pendingToolCalls = new Map<string, Extract<OcxAssistantContentPart, { type: "toolCall" }>>();
|
|
1333
1352
|
const flush = () => {
|
|
1334
1353
|
if (!current) return;
|
|
1335
|
-
for (const part of pendingToolCalls.values())
|
|
1354
|
+
for (const part of pendingToolCalls.values()) {
|
|
1355
|
+
current.steps.push(toolCallStep(part, requestScope, missingToolResultFor(part), codeMode));
|
|
1356
|
+
}
|
|
1336
1357
|
turns.push(storeCursorBlob(toBinary(ConversationTurnStructureSchema, create(ConversationTurnStructureSchema, {
|
|
1337
1358
|
turn: {
|
|
1338
1359
|
case: "agentConversationTurn",
|
|
@@ -340,7 +340,13 @@ export function cursorConversationIdFromClientThread(threadId: string, identityS
|
|
|
340
340
|
|
|
341
341
|
/**
|
|
342
342
|
* Resolve the Cursor conversation id for this turn.
|
|
343
|
-
* Priority: force-fresh → isolate helper →
|
|
343
|
+
* Priority: force-fresh → isolate helper → thread remint override → stored conversation id
|
|
344
|
+
* → client thread hash → random.
|
|
345
|
+
*
|
|
346
|
+
* The remint override must beat a stored `_cursorConversationId`. Only the remint path writes
|
|
347
|
+
* the thread store (cursor.ts), so a stored id that disagrees with it is the pre-remint value,
|
|
348
|
+
* and preferring it let a second Responses chain in the same Codex thread keep resuming the
|
|
349
|
+
* conversation the previous turn just rotated away from.
|
|
344
350
|
* Never use OpenAI Responses `previous_response_id` (resp_*) or shared `prompt_cache_key`
|
|
345
351
|
* (cache-cohort fingerprint, not conversation ownership).
|
|
346
352
|
*/
|
|
@@ -351,11 +357,16 @@ export function resolveCursorConversationId(
|
|
|
351
357
|
): string {
|
|
352
358
|
if (options.forceFreshConversation === true) return generatedCursorConversationId();
|
|
353
359
|
if (parsed._cursorIsolateConversation === true) return generatedCursorConversationId();
|
|
354
|
-
if (parsed._cursorConversationId) return parsed._cursorConversationId;
|
|
355
360
|
const threadId = cursorClientThreadOwner(parsed);
|
|
356
|
-
|
|
361
|
+
// A compaction turn carries its own conversation id and must not be pulled onto the parent's
|
|
362
|
+
// thread override. It is isolated in effect without ever setting the isolate flag, which is why
|
|
363
|
+
// the override check has to exclude it explicitly rather than rely on that flag.
|
|
364
|
+
if (threadId && parsed._compactionRequest !== true) {
|
|
357
365
|
const recovered = lookupCursorThreadConversation(threadId, parsed._cursorIdentityScope);
|
|
358
366
|
if (recovered) return recovered;
|
|
367
|
+
}
|
|
368
|
+
if (parsed._cursorConversationId) return parsed._cursorConversationId;
|
|
369
|
+
if (threadId) {
|
|
359
370
|
return cursorConversationIdFromClientThread(`thread:${threadId}`, parsed._cursorIdentityScope);
|
|
360
371
|
}
|
|
361
372
|
return generatedCursorConversationId();
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quarantine textual pseudo tool-call markers that Cursor models emit inside
|
|
3
|
+
* `textDelta` instead of (or in addition to) real `toolCall*` frames.
|
|
4
|
+
*
|
|
5
|
+
* `#2305` only rewrote the display alias *inside* `[TOOL_CALL]…[ARGS]`. The
|
|
6
|
+
* marker still reached Codex/Claude as assistant text, which few-shot-mimics
|
|
7
|
+
* later calls as inert text and stalls multi-tool turns. This drain strips
|
|
8
|
+
* every complete marker from visible text and yields the parsed calls so the
|
|
9
|
+
* protobuf mapper can promote advertised names onto the real tool-call path.
|
|
10
|
+
*
|
|
11
|
+
* Incomplete markers (split across streaming deltas) stay in `pending` until
|
|
12
|
+
* the JSON object closes. Once the retained prefix exceeds the byte cap, the
|
|
13
|
+
* parser switches to a constant-space suppressed scan until the object closes.
|
|
14
|
+
*/
|
|
15
|
+
import { debugProviderDiagnostic } from "../../lib/debug";
|
|
16
|
+
import { normalizeCursorWireName } from "./tool-naming";
|
|
17
|
+
|
|
18
|
+
export const MAX_PENDING_TEXT_TOOLCALL_BYTES = 64 * 1024;
|
|
19
|
+
const TOOL_CALL_OPEN = /\[TOOL_CALL\]/gi;
|
|
20
|
+
|
|
21
|
+
export interface DrainedTextToolCall {
|
|
22
|
+
readonly name: string;
|
|
23
|
+
readonly args: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SuppressedTextToolCallScan {
|
|
27
|
+
phase: "args-tag" | "json-start" | "json";
|
|
28
|
+
argsTagProgress: number;
|
|
29
|
+
depth: number;
|
|
30
|
+
inString: boolean;
|
|
31
|
+
escape: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface DrainTextToolCallsResult {
|
|
35
|
+
readonly text: string;
|
|
36
|
+
readonly pending: string;
|
|
37
|
+
readonly calls: readonly DrainedTextToolCall[];
|
|
38
|
+
readonly suppressed?: SuppressedTextToolCallScan;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function findJsonObjectEnd(source: string, start: number): number | undefined {
|
|
42
|
+
if (source[start] !== "{") return undefined;
|
|
43
|
+
let depth = 0;
|
|
44
|
+
let inString = false;
|
|
45
|
+
let escape = false;
|
|
46
|
+
for (let i = start; i < source.length; i++) {
|
|
47
|
+
const ch = source[i];
|
|
48
|
+
if (inString) {
|
|
49
|
+
if (escape) {
|
|
50
|
+
escape = false;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (ch === "\\") {
|
|
54
|
+
escape = true;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (ch === "\"") inString = false;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (ch === "\"") {
|
|
61
|
+
inString = true;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (ch === "{") depth += 1;
|
|
65
|
+
else if (ch === "}") {
|
|
66
|
+
depth -= 1;
|
|
67
|
+
if (depth === 0) return i + 1;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function byteLength(text: string): number {
|
|
74
|
+
return new TextEncoder().encode(text).byteLength;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function newSuppressedScan(): SuppressedTextToolCallScan {
|
|
78
|
+
return {
|
|
79
|
+
phase: "args-tag",
|
|
80
|
+
argsTagProgress: 0,
|
|
81
|
+
depth: 0,
|
|
82
|
+
inString: false,
|
|
83
|
+
escape: false,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const ARGS_TAG = "[args]";
|
|
88
|
+
|
|
89
|
+
function consumeSuppressedScan(
|
|
90
|
+
state: SuppressedTextToolCallScan,
|
|
91
|
+
source: string,
|
|
92
|
+
start = 0,
|
|
93
|
+
): { readonly state?: SuppressedTextToolCallScan; readonly resumeAt: number } {
|
|
94
|
+
for (let i = start; i < source.length; i++) {
|
|
95
|
+
const ch = source[i] ?? "";
|
|
96
|
+
if (state.phase === "args-tag") {
|
|
97
|
+
const lower = ch.toLowerCase();
|
|
98
|
+
if (lower === ARGS_TAG[state.argsTagProgress]) {
|
|
99
|
+
state.argsTagProgress += 1;
|
|
100
|
+
if (state.argsTagProgress === ARGS_TAG.length) {
|
|
101
|
+
state.phase = "json-start";
|
|
102
|
+
state.argsTagProgress = 0;
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
state.argsTagProgress = lower === ARGS_TAG[0] ? 1 : 0;
|
|
106
|
+
}
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (state.phase === "json-start") {
|
|
110
|
+
if (/\s/.test(ch)) continue;
|
|
111
|
+
if (ch !== "{") {
|
|
112
|
+
debugProviderDiagnostic("cursor", "text-toolcall-invalid-arguments", {
|
|
113
|
+
reason: "arguments-did-not-start-with-object",
|
|
114
|
+
});
|
|
115
|
+
return { resumeAt: source.length };
|
|
116
|
+
}
|
|
117
|
+
state.phase = "json";
|
|
118
|
+
state.depth = 1;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (state.inString) {
|
|
122
|
+
if (state.escape) {
|
|
123
|
+
state.escape = false;
|
|
124
|
+
} else if (ch === "\\") {
|
|
125
|
+
state.escape = true;
|
|
126
|
+
} else if (ch === "\"") {
|
|
127
|
+
state.inString = false;
|
|
128
|
+
}
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (ch === "\"") state.inString = true;
|
|
132
|
+
else if (ch === "{") state.depth += 1;
|
|
133
|
+
else if (ch === "}") {
|
|
134
|
+
state.depth -= 1;
|
|
135
|
+
if (state.depth === 0) return { resumeAt: i + 1 };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return { state, resumeAt: source.length };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function holdOrSuppress(hold: string, afterOpen: number): Pick<DrainTextToolCallsResult, "pending" | "suppressed"> {
|
|
142
|
+
if (byteLength(hold) <= MAX_PENDING_TEXT_TOOLCALL_BYTES) return { pending: hold };
|
|
143
|
+
const suppressed = newSuppressedScan();
|
|
144
|
+
const scanned = consumeSuppressedScan(suppressed, hold, afterOpen);
|
|
145
|
+
return scanned.state ? { pending: "", suppressed: scanned.state } : { pending: "" };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Fold `pending + chunk`, emit surrounding prose, and extract every complete
|
|
150
|
+
* `[TOOL_CALL]name[ARGS]{…}` block. Names are folded through
|
|
151
|
+
* `normalizeCursorWireName` so `mcp_opencodex-responses_*` display aliases
|
|
152
|
+
* become the advertised wire name before promotion.
|
|
153
|
+
*/
|
|
154
|
+
export function drainCursorTextToolCalls(
|
|
155
|
+
pending: string,
|
|
156
|
+
chunk: string,
|
|
157
|
+
suppressed?: SuppressedTextToolCallScan,
|
|
158
|
+
): DrainTextToolCallsResult {
|
|
159
|
+
let combined = pending + chunk;
|
|
160
|
+
if (suppressed) {
|
|
161
|
+
const scanned = consumeSuppressedScan(suppressed, chunk);
|
|
162
|
+
if (scanned.state) return { text: "", pending: "", calls: [], suppressed: scanned.state };
|
|
163
|
+
combined = chunk.slice(scanned.resumeAt);
|
|
164
|
+
}
|
|
165
|
+
let cursor = 0;
|
|
166
|
+
let text = "";
|
|
167
|
+
const calls: DrainedTextToolCall[] = [];
|
|
168
|
+
const opener = new RegExp(TOOL_CALL_OPEN.source, TOOL_CALL_OPEN.flags);
|
|
169
|
+
|
|
170
|
+
while (cursor < combined.length) {
|
|
171
|
+
opener.lastIndex = cursor;
|
|
172
|
+
const match = opener.exec(combined);
|
|
173
|
+
if (!match || match.index === undefined) {
|
|
174
|
+
text += combined.slice(cursor);
|
|
175
|
+
return { text, pending: "", calls };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
text += combined.slice(cursor, match.index);
|
|
179
|
+
const afterOpen = match.index + match[0].length;
|
|
180
|
+
const rest = combined.slice(afterOpen);
|
|
181
|
+
const argsTag = rest.match(/^([^\[\]]*)\[ARGS\]/i);
|
|
182
|
+
if (!argsTag) {
|
|
183
|
+
const hold = combined.slice(match.index);
|
|
184
|
+
return { text, calls, ...holdOrSuppress(hold, afterOpen - match.index) };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const name = argsTag[1]?.trim() ?? "";
|
|
188
|
+
let jsonStart = afterOpen + argsTag[0].length;
|
|
189
|
+
while (jsonStart < combined.length && /\s/.test(combined[jsonStart] ?? "")) jsonStart += 1;
|
|
190
|
+
if (jsonStart >= combined.length) {
|
|
191
|
+
const hold = combined.slice(match.index);
|
|
192
|
+
return { text, calls, ...holdOrSuppress(hold, afterOpen - match.index) };
|
|
193
|
+
}
|
|
194
|
+
if (combined[jsonStart] !== "{") {
|
|
195
|
+
debugProviderDiagnostic("cursor", "text-toolcall-invalid-arguments", {
|
|
196
|
+
reason: "arguments-did-not-start-with-object",
|
|
197
|
+
...(name ? { toolName: normalizeCursorWireName(name) } : {}),
|
|
198
|
+
});
|
|
199
|
+
// No delimiter identifies where a non-JSON payload ends. Resume marker
|
|
200
|
+
// scanning after `[ARGS]`, while suppressing the malformed payload itself.
|
|
201
|
+
opener.lastIndex = jsonStart;
|
|
202
|
+
const nextMarker = opener.exec(combined);
|
|
203
|
+
if (!nextMarker || nextMarker.index === undefined) return { text, pending: "", calls };
|
|
204
|
+
cursor = nextMarker.index;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const jsonEnd = findJsonObjectEnd(combined, jsonStart);
|
|
209
|
+
if (jsonEnd === undefined) {
|
|
210
|
+
const hold = combined.slice(match.index);
|
|
211
|
+
return { text, calls, ...holdOrSuppress(hold, afterOpen - match.index) };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const args = combined.slice(jsonStart, jsonEnd);
|
|
215
|
+
try {
|
|
216
|
+
JSON.parse(args);
|
|
217
|
+
if (name.length > 0) {
|
|
218
|
+
calls.push({ name: normalizeCursorWireName(name), args });
|
|
219
|
+
}
|
|
220
|
+
} catch {
|
|
221
|
+
debugProviderDiagnostic("cursor", "text-toolcall-invalid-arguments", {
|
|
222
|
+
reason: "arguments-json-parse-failed",
|
|
223
|
+
...(name ? { toolName: normalizeCursorWireName(name) } : {}),
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
cursor = jsonEnd;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return { text, pending: "", calls };
|
|
230
|
+
}
|
|
@@ -158,3 +158,144 @@ export function cursorOverflowRemintCountForTests(): number {
|
|
|
158
158
|
pruneOverflowRemints(now());
|
|
159
159
|
return overflowRemintByScope.size;
|
|
160
160
|
}
|
|
161
|
+
|
|
162
|
+
/** Max next-turn conversation-id rotations after incomplete client-tool streams per retained scope. */
|
|
163
|
+
export const CURSOR_INCOMPLETE_TOOL_REMINT_MAX = 3;
|
|
164
|
+
export const CURSOR_INCOMPLETE_TOOL_REMINT_TTL_MS = CURSOR_OVERFLOW_REMINT_TTL_MS;
|
|
165
|
+
export const CURSOR_INCOMPLETE_TOOL_REMINT_MAX_ENTRIES = CURSOR_OVERFLOW_REMINT_MAX_ENTRIES;
|
|
166
|
+
|
|
167
|
+
type IncompleteToolRemintState = {
|
|
168
|
+
remintCount: number;
|
|
169
|
+
updatedAt: number;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* One bounded next-turn remint allowance, keyed by retained thread scope.
|
|
174
|
+
*
|
|
175
|
+
* Each recovery reason owns its own instance. Sharing one budget would let a cheap, frequent
|
|
176
|
+
* failure spend the allowance that a rarer, more expensive recovery depends on.
|
|
177
|
+
*/
|
|
178
|
+
function createCursorRemintBudget(max: number, ttlMs: number, maxEntries: number) {
|
|
179
|
+
const byScope = new Map<string, IncompleteToolRemintState>();
|
|
180
|
+
|
|
181
|
+
const prune = (at: number): void => {
|
|
182
|
+
for (const [scopeKey, entry] of byScope) {
|
|
183
|
+
if (at - entry.updatedAt > ttlMs) byScope.delete(scopeKey);
|
|
184
|
+
}
|
|
185
|
+
while (byScope.size > maxEntries) {
|
|
186
|
+
const oldest = byScope.keys().next().value;
|
|
187
|
+
if (oldest === undefined) break;
|
|
188
|
+
byScope.delete(oldest);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
/** Record one remint; returns false when this budget is exhausted. */
|
|
194
|
+
record(scopeKey: string): boolean {
|
|
195
|
+
const at = now();
|
|
196
|
+
prune(at);
|
|
197
|
+
const existing = byScope.get(scopeKey);
|
|
198
|
+
if (existing && existing.remintCount >= max) {
|
|
199
|
+
existing.updatedAt = at;
|
|
200
|
+
byScope.delete(scopeKey);
|
|
201
|
+
byScope.set(scopeKey, existing);
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
const entry = existing ?? { remintCount: 0, updatedAt: at };
|
|
205
|
+
entry.remintCount += 1;
|
|
206
|
+
entry.updatedAt = at;
|
|
207
|
+
byScope.delete(scopeKey);
|
|
208
|
+
byScope.set(scopeKey, entry);
|
|
209
|
+
prune(at);
|
|
210
|
+
return true;
|
|
211
|
+
},
|
|
212
|
+
clear(scopeKey: string): void {
|
|
213
|
+
byScope.delete(scopeKey);
|
|
214
|
+
},
|
|
215
|
+
clearForTests(): void {
|
|
216
|
+
byScope.clear();
|
|
217
|
+
},
|
|
218
|
+
countForTests(): number {
|
|
219
|
+
prune(now());
|
|
220
|
+
return byScope.size;
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const incompleteToolRemintBudget = createCursorRemintBudget(
|
|
226
|
+
CURSOR_INCOMPLETE_TOOL_REMINT_MAX,
|
|
227
|
+
CURSOR_INCOMPLETE_TOOL_REMINT_TTL_MS,
|
|
228
|
+
CURSOR_INCOMPLETE_TOOL_REMINT_MAX_ENTRIES,
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
/** Incomplete-tool and overflow recovery share ownership scope, but keep independent budgets. */
|
|
232
|
+
export function cursorIncompleteToolRemintScopeKey(
|
|
233
|
+
threadOwner: string | undefined,
|
|
234
|
+
identityScope?: string,
|
|
235
|
+
): string | null {
|
|
236
|
+
return cursorOverflowRemintScopeKey(threadOwner, identityScope);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Record one incomplete-tool remint; returns false when the independent cap is exhausted. */
|
|
240
|
+
export function recordCursorIncompleteToolRemint(scopeKey: string): boolean {
|
|
241
|
+
return incompleteToolRemintBudget.record(scopeKey);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** A clean turn replenishes this recovery without changing the overflow retry budget. */
|
|
245
|
+
export function clearCursorIncompleteToolRemint(scopeKey: string): void {
|
|
246
|
+
incompleteToolRemintBudget.clear(scopeKey);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function clearCursorIncompleteToolRemintForTests(): void {
|
|
250
|
+
incompleteToolRemintBudget.clearForTests();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function cursorIncompleteToolRemintCountForTests(): number {
|
|
254
|
+
return incompleteToolRemintBudget.countForTests();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Max next-turn rotations after a MID-STREAM envelope echo, per retained scope.
|
|
259
|
+
*
|
|
260
|
+
* Deliberately a separate budget from the incomplete-tool allowance. A mid-stream echo is a
|
|
261
|
+
* cheap, repeatable formatting failure, while an incomplete client-tool stream is a rarer
|
|
262
|
+
* structural one; on a shared counter a model that echoes every turn would spend the budget
|
|
263
|
+
* that incomplete-tool recovery depends on. Bounding it at all is the point: the echo has
|
|
264
|
+
* already reached the client and cannot be quarantined, so without a cap a persistently
|
|
265
|
+
* echoing model would remint the conversation on every single turn, forever.
|
|
266
|
+
*/
|
|
267
|
+
export const CURSOR_ENVELOPE_ECHO_REMINT_MAX = 3;
|
|
268
|
+
export const CURSOR_ENVELOPE_ECHO_REMINT_TTL_MS = CURSOR_OVERFLOW_REMINT_TTL_MS;
|
|
269
|
+
export const CURSOR_ENVELOPE_ECHO_REMINT_MAX_ENTRIES = CURSOR_OVERFLOW_REMINT_MAX_ENTRIES;
|
|
270
|
+
|
|
271
|
+
const envelopeEchoRemintBudget = createCursorRemintBudget(
|
|
272
|
+
CURSOR_ENVELOPE_ECHO_REMINT_MAX,
|
|
273
|
+
CURSOR_ENVELOPE_ECHO_REMINT_TTL_MS,
|
|
274
|
+
CURSOR_ENVELOPE_ECHO_REMINT_MAX_ENTRIES,
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
/** Echo recovery shares ownership scope with overflow and incomplete-tool, budget apart. */
|
|
278
|
+
export function cursorEnvelopeEchoRemintScopeKey(
|
|
279
|
+
threadOwner: string | undefined,
|
|
280
|
+
identityScope?: string,
|
|
281
|
+
): string | null {
|
|
282
|
+
return cursorOverflowRemintScopeKey(threadOwner, identityScope);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Record one envelope-echo remint; returns false when the independent cap is exhausted. */
|
|
286
|
+
export function recordCursorEnvelopeEchoRemint(scopeKey: string): boolean {
|
|
287
|
+
return envelopeEchoRemintBudget.record(scopeKey);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** A turn that completed without an echo replenishes only this budget. */
|
|
291
|
+
export function clearCursorEnvelopeEchoRemint(scopeKey: string): void {
|
|
292
|
+
envelopeEchoRemintBudget.clear(scopeKey);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function clearCursorEnvelopeEchoRemintForTests(): void {
|
|
296
|
+
envelopeEchoRemintBudget.clearForTests();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function cursorEnvelopeEchoRemintCountForTests(): number {
|
|
300
|
+
return envelopeEchoRemintBudget.countForTests();
|
|
301
|
+
}
|
|
@@ -4,13 +4,14 @@ import { CODEX_SHELL_BRIDGE_TOOL_NAMES, CODEX_TOOL_SEARCH_TOOL, CODEX_UNIFIED_EX
|
|
|
4
4
|
|
|
5
5
|
export const CURSOR_SHELL_ALIAS_SYSTEM_NOTE =
|
|
6
6
|
'Shell commands use the Codex shell bridge tool shown in this turn\'s catalog (`shell_command` or `exec_command`) with JSON arguments like {"cmd":"..."}. The long `mcp_opencodex-responses_*` display name is the same tool. Prefer it over Cursor-native Shell.';
|
|
7
|
-
const NEIGHBOR_AGENT_TOOL_NAMES = ["Read", "Grep", "Glob", "Bash", "LS"] as const;
|
|
7
|
+
const NEIGHBOR_AGENT_TOOL_NAMES = ["Read", "Grep", "Glob", "Bash", "LS", "Write"] as const;
|
|
8
8
|
const NEIGHBOR_AGENT_TOOL_ALIASES: Record<(typeof NEIGHBOR_AGENT_TOOL_NAMES)[number], readonly string[]> = {
|
|
9
9
|
Read: ["read", "read_file"],
|
|
10
10
|
Grep: ["grep"],
|
|
11
11
|
Glob: ["glob", "find"],
|
|
12
12
|
Bash: ["bash", "shell"],
|
|
13
13
|
LS: ["ls"],
|
|
14
|
+
Write: ["write", "write_file"],
|
|
14
15
|
};
|
|
15
16
|
|
|
16
17
|
export const CURSOR_GENERIC_TOOL_USE_USER_HINT = [
|
|
@@ -22,7 +23,7 @@ export const CURSOR_GENERIC_TOOL_USE_USER_HINT = [
|
|
|
22
23
|
"The Cursor bridge may suspend after the first returned bridge tool call, so emit sibling calls together before any result is needed.",
|
|
23
24
|
"If parallel emission is unavailable, continue with separate shell-bridge calls until the requested count has returned.",
|
|
24
25
|
"Do not use `tool_search`, external MCP, or resource discovery just to pad the count unless explicitly asked.",
|
|
25
|
-
"Do not suggest or switch to neighboring-agent tools such as `Grep`, `Read`, `Glob`, `Bash`, or `
|
|
26
|
+
"Do not suggest or switch to neighboring-agent tools such as `Grep`, `Read`, `Glob`, `Bash`, `LS`, or `Write` unless this turn's catalog lists those exact names or an equivalent listed client tool.",
|
|
26
27
|
].join(" ");
|
|
27
28
|
|
|
28
29
|
|
|
@@ -190,7 +191,7 @@ export function buildCursorToolGuidanceSystemNote(
|
|
|
190
191
|
? CODE_MODE_RESULT_ECHO_SENTENCE + " There is no `require`, no `module`, and no filesystem or network globals; reach the host only through the nested helpers. " + CODE_MODE_HOST_CONTRACT_SENTENCE
|
|
191
192
|
: undefined,
|
|
192
193
|
codeMode
|
|
193
|
-
? "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."
|
|
194
|
+
? "NEVER attempt Cursor-native Shell, Read, Grep, List, Write, 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."
|
|
194
195
|
: undefined,
|
|
195
196
|
hasBareExec
|
|
196
197
|
? `${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.`
|
|
@@ -199,7 +200,7 @@ export function buildCursorToolGuidanceSystemNote(
|
|
|
199
200
|
? "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."
|
|
200
201
|
: undefined,
|
|
201
202
|
hasBareExec
|
|
202
|
-
? `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.`
|
|
203
|
+
? `NEVER attempt Cursor-native Shell, Read, Grep, List, Write, 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.`
|
|
203
204
|
: undefined,
|
|
204
205
|
hasBareExec
|
|
205
206
|
? "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."
|
|
@@ -11,6 +11,11 @@ export interface CursorRequestedModelParameter {
|
|
|
11
11
|
|
|
12
12
|
export interface CursorRunRequest {
|
|
13
13
|
modelId: string;
|
|
14
|
+
/**
|
|
15
|
+
* Normalized Cursor identity scope carried from request parsing so checkpoint
|
|
16
|
+
* observations stay scope-local. Transport metadata only; never serialized on the wire.
|
|
17
|
+
*/
|
|
18
|
+
_cursorIdentityScope?: string;
|
|
14
19
|
/** Cursor model-picker parameters encoded through AgentRunRequest.requested_model. */
|
|
15
20
|
requestedModelParameters?: readonly CursorRequestedModelParameter[];
|
|
16
21
|
/** Cursor Router optimization parameter; valid only while modelId is the `default` wire model. */
|