@bitkyc08/opencodex 2.56.0 → 2.58.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/bin/ocx.mjs +10 -0
- package/gui/dist/assets/{index-D4zuyIxQ.js → index-BbrHOIY0.js} +21 -21
- package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/adapters/codebuddy/adapter.ts +2 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +249 -0
- package/src/adapters/command-code.ts +12 -3
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +65 -1
- package/src/adapters/cursor/envelope-echo.ts +8 -2
- package/src/adapters/cursor/live-transport.ts +5 -1
- package/src/adapters/cursor/protobuf-events.ts +110 -11
- package/src/adapters/cursor/protobuf-request.ts +19 -1
- package/src/adapters/cursor/text-toolcall.ts +230 -0
- package/src/adapters/cursor/thread-continuity.ts +67 -0
- package/src/adapters/cursor/types.ts +5 -0
- package/src/adapters/cursor.ts +55 -5
- package/src/adapters/google-http.ts +38 -13
- package/src/adapters/google.ts +7 -7
- package/src/adapters/kiro/payload.ts +17 -3
- package/src/adapters/kiro/reasoning.ts +70 -7
- package/src/adapters/kiro/stream.ts +8 -2
- package/src/adapters/kiro/wire.ts +2 -1
- package/src/adapters/kiro-events.ts +21 -13
- package/src/adapters/mimo-free.ts +32 -17
- package/src/adapters/ollama-native.ts +42 -8
- package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
- package/src/adapters/openai-chat/tool-schema.ts +25 -7
- package/src/adapters/openai-chat.ts +8 -8
- package/src/adapters/openai-responses/passthrough.ts +62 -5
- package/src/adapters/openai-responses/request-strips.ts +43 -0
- package/src/adapters/physical-send.ts +50 -0
- package/src/bridge/errors.ts +26 -2
- package/src/bridge/response-json.ts +8 -2
- package/src/bridge/sse.ts +20 -2
- package/src/claude/desktop-profile.ts +66 -9
- package/src/claude/outbound.ts +32 -4
- package/src/cli/account-main.ts +1 -1
- package/src/cli/capabilities.ts +2 -2
- package/src/cli/combo.ts +10 -1
- package/src/cli/config-command.ts +35 -18
- package/src/cli/dispatch.ts +17 -4
- package/src/cli/index.ts +92 -7
- package/src/cli/registry.ts +2 -1
- package/src/cli/system-command.ts +74 -5
- package/src/cli/uninstall-client-state.ts +12 -0
- package/src/clients/config-export.ts +7 -3
- package/src/codex/account-label.ts +14 -3
- package/src/codex/account-store.ts +113 -26
- package/src/codex/account-usability.ts +21 -0
- package/src/codex/auth-api/login-flow.ts +14 -2
- package/src/codex/auth-api/reset-credit-service.ts +11 -2
- package/src/codex/auth-context.ts +199 -15
- package/src/codex/catalog/aggregation.ts +80 -1
- package/src/codex/catalog/model-visibility.ts +1 -0
- package/src/codex/catalog/remote.ts +30 -0
- package/src/codex/catalog/retained-sync.ts +9 -1
- package/src/codex/catalog/routed-gather.ts +38 -1
- package/src/codex/cli-install-provenance.ts +7 -1
- package/src/codex/convergence.ts +7 -2
- package/src/codex/desktop-app/types.ts +11 -2
- package/src/codex/desktop-app/windows.ts +5 -5
- package/src/codex/desktop-switches.ts +145 -0
- package/src/codex/history-job.ts +5 -1
- package/src/codex/history-provider.ts +33 -4
- package/src/codex/history-worker.ts +14 -1
- package/src/codex/inject/remove.ts +145 -7
- package/src/codex/inject/restore.ts +231 -32
- package/src/codex/inject.ts +12 -16
- package/src/codex/loopback-target.ts +9 -0
- package/src/codex/model-entitlements.ts +152 -15
- package/src/codex/native-profile-startup.ts +64 -20
- package/src/codex/pool-refresh-backoff.ts +12 -3
- package/src/codex/quota-rejection.ts +104 -15
- package/src/codex/routing/cache-affinity.ts +70 -0
- package/src/codex/routing/cooldown-math.ts +10 -0
- package/src/codex/routing/selection.ts +79 -2
- package/src/codex/routing/thread-affinity.ts +50 -2
- package/src/codex/routing/transient-hold-dispatch.ts +141 -0
- package/src/codex/routing.ts +29 -49
- package/src/codex/warmup.ts +1 -1
- package/src/combos/failover.ts +85 -0
- package/src/combos/request.ts +17 -10
- package/src/combos/types.ts +23 -2
- package/src/config/atomic-write.ts +83 -8
- package/src/config/pending-teardown.ts +31 -0
- package/src/config/schema/config-schema.ts +2 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +272 -180
- package/src/images/loop.ts +1 -1
- package/src/lib/bounded-subprocess.ts +62 -10
- package/src/lib/errors.ts +17 -0
- package/src/lib/request-execution-budget.ts +147 -21
- package/src/lib/spend-reservation-ledger.ts +18 -0
- package/src/lib/state-store-registrations.ts +6 -2
- package/src/lib/test-home-guard.ts +85 -1
- package/src/lib/upstream-retry.ts +77 -10
- package/src/lib/windows-elevation.ts +76 -14
- package/src/lib/windows-secret-acl.ts +151 -15
- package/src/lib/windows-user-principal.ts +5 -1
- package/src/oauth/index.ts +2 -2
- package/src/oauth/key-providers.ts +2 -2
- package/src/providers/derive.ts +6 -0
- package/src/providers/kiro-models.ts +4 -3
- package/src/providers/label.ts +19 -1
- package/src/providers/model-discovery.ts +35 -7
- package/src/providers/registry/entries-core.ts +18 -0
- package/src/providers/registry/entries-extended.ts +59 -28
- package/src/providers/registry/model-seeds.ts +71 -17
- package/src/providers/registry/types.ts +9 -0
- package/src/responses/reasoning-envelope.ts +6 -3
- 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/routing/identity-domains.ts +21 -14
- package/src/routing/probe-lease.ts +103 -1
- package/src/server/auth-cors.ts +1 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/chat-native.ts +37 -9
- package/src/server/index/live-sideband.ts +37 -1
- package/src/server/index/websocket-handler.ts +54 -3
- package/src/server/index.ts +5 -5
- package/src/server/inspection-tee.ts +107 -0
- package/src/server/live.ts +46 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/config-routes.ts +27 -5
- package/src/server/models-capabilities.ts +24 -3
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +14 -19
- package/src/server/request-log.ts +127 -3
- package/src/server/response-log-body.ts +153 -0
- package/src/server/responses/account-change-state.ts +74 -0
- package/src/server/responses/adapter-continuation.ts +33 -7
- package/src/server/responses/adapter-delivery.ts +5 -11
- package/src/server/responses/adapter-dispatch.ts +84 -13
- package/src/server/responses/codex-ws-exchange.ts +65 -4
- package/src/server/responses/codex-ws-wire.ts +5 -0
- package/src/server/responses/collaboration.ts +74 -4
- package/src/server/responses/combo-session-recall.ts +68 -8
- package/src/server/responses/combo-stream-preflight.ts +68 -5
- package/src/server/responses/compact.ts +54 -13
- package/src/server/responses/core-auth.ts +2 -0
- package/src/server/responses/core-codex-account.ts +51 -3
- package/src/server/responses/core-combo.ts +129 -23
- package/src/server/responses/core-errors.ts +18 -0
- package/src/server/responses/core-options.ts +3 -0
- package/src/server/responses/core-replay.ts +105 -32
- package/src/server/responses/core.ts +3 -3
- package/src/server/responses/encrypted-payload.ts +0 -1
- package/src/server/responses/fetch-helpers.ts +4 -1
- package/src/server/responses/input-admission.ts +126 -6
- 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 +30 -6
- package/src/server/responses/passthrough-dispatch.ts +61 -11
- package/src/server/responses/passthrough-error.ts +38 -2
- package/src/server/responses/request-prepare.ts +173 -22
- package/src/server/responses/request-send-budget.ts +97 -2
- package/src/server/responses/request-spend.ts +147 -0
- package/src/server/responses/request-transport.ts +62 -3
- package/src/server/responses/run-turn-execution.ts +59 -31
- package/src/server/responses/sidecar-execution.ts +7 -13
- package/src/server/responses/terminal-guard.ts +65 -4
- package/src/server/responses/ws-upstream.ts +21 -1
- package/src/server/responses-undeclared-tool-guard.ts +9 -5
- package/src/server/stop-teardown.ts +8 -1
- package/src/server/ws-bridge.ts +16 -1
- package/src/service/cli.ts +13 -1
- package/src/service/windows-ops.ts +210 -16
- package/src/service/windows-scheduler.ts +28 -21
- package/src/service.ts +1 -1
- package/src/types/config.ts +8 -1
- package/src/types/provider.ts +13 -0
- package/src/types/request.ts +8 -5
- package/src/types/tools.ts +24 -0
- package/src/types.ts +2 -0
- package/src/update/index.ts +10 -0
- package/src/update/stop-contract.d.mts +1 -0
- package/src/update/stop-contract.mjs +19 -0
- package/src/update/stop-decision.d.mts +1 -1
- package/src/update/stop-decision.mjs +12 -3
- package/src/usage/log.ts +1 -1
- package/src/vision/anthropic-describe.ts +1 -1
- package/src/vision/describe.ts +5 -5
- package/src/web-search/anthropic-executor.ts +1 -1
- package/src/web-search/exa-executor.ts +1 -1
- package/src/web-search/executor.ts +1 -1
- package/src/web-search/gemini-executor.ts +1 -1
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/ollama-executor.ts +1 -1
- package/src/web-search/parse.ts +67 -14
- package/src/web-search/passthrough-bridge.ts +64 -31
- package/src/web-search/xai-executor.ts +1 -1
|
@@ -11,13 +11,19 @@ import {
|
|
|
11
11
|
isCodexShellBridgeToolName,
|
|
12
12
|
isCursorStructuredEditToolName,
|
|
13
13
|
normalizeCursorWireName,
|
|
14
|
-
normalizeCursorTextToolMarkers,
|
|
15
14
|
OCX_RESPONSES_TOOL_PROVIDER,
|
|
16
15
|
resolveShellBridgeAliasKey,
|
|
17
16
|
responsesToolNameFromCursorWire,
|
|
18
17
|
} from "./tool-definitions";
|
|
18
|
+
import {
|
|
19
|
+
drainCursorTextToolCalls,
|
|
20
|
+
type DrainedTextToolCall,
|
|
21
|
+
type SuppressedTextToolCallScan,
|
|
22
|
+
} from "./text-toolcall";
|
|
23
|
+
import { recordObservedCursorContextWindow } from "./discovery";
|
|
19
24
|
import type { CursorServerMessage } from "./types";
|
|
20
25
|
import type { TranslatorBudget } from "../../lib/translator-budget";
|
|
26
|
+
import { CURSOR_INCOMPLETE_TOOL_CALL_MESSAGE_PREFIX } from "./cursor-errors";
|
|
21
27
|
|
|
22
28
|
const DEFAULT_CONTEXT_USAGE_MAX_ENTRIES = 200;
|
|
23
29
|
const DEFAULT_CONTEXT_USAGE_TTL_MS = 60 * 60 * 1_000;
|
|
@@ -176,6 +182,23 @@ export interface CursorProtobufEventState {
|
|
|
176
182
|
*/
|
|
177
183
|
syntheticStructuredEditToolNames?: ReadonlySet<string>;
|
|
178
184
|
translatorBudget?: TranslatorBudget;
|
|
185
|
+
/**
|
|
186
|
+
* Incomplete `[TOOL_CALL]…[ARGS]{` prefix held across `textDelta` frames so a
|
|
187
|
+
* marker split by the stream cannot leak into assistant text.
|
|
188
|
+
*/
|
|
189
|
+
pendingTextToolCall?: string;
|
|
190
|
+
/** Constant-space scanner used after an incomplete textual marker exceeds its retained byte cap. */
|
|
191
|
+
suppressedTextToolCall?: SuppressedTextToolCallScan;
|
|
192
|
+
/** Parsed textual fallback calls held until turn finalization establishes that no real frame won. */
|
|
193
|
+
bufferedTextToolCalls?: DrainedTextToolCall[];
|
|
194
|
+
/** True once this turn carries any real client-tool frame, including an incomplete one. */
|
|
195
|
+
sawRealClientToolCall?: boolean;
|
|
196
|
+
/** Monotonic id suffix for tool calls promoted from text markers. */
|
|
197
|
+
textToolCallSeq?: number;
|
|
198
|
+
/** Wire model id used to record checkpoint `maxTokens` for the next turn. */
|
|
199
|
+
wireModelId?: string;
|
|
200
|
+
/** Normalized Cursor identity scope that owns the observed checkpoint ceiling. */
|
|
201
|
+
identityScope: string;
|
|
179
202
|
}
|
|
180
203
|
|
|
181
204
|
|
|
@@ -215,6 +238,10 @@ export function createCursorProtobufEventState(options: {
|
|
|
215
238
|
*/
|
|
216
239
|
estimatedInputTokens?: number;
|
|
217
240
|
translatorBudget?: TranslatorBudget;
|
|
241
|
+
/** Wire model id for recording checkpoint `maxTokens` into the process-local window map. */
|
|
242
|
+
wireModelId?: string;
|
|
243
|
+
/** Cursor request identity scope; normalized identically to request-builder continuity. */
|
|
244
|
+
identityScope?: string;
|
|
218
245
|
} = {}): CursorProtobufEventState {
|
|
219
246
|
return {
|
|
220
247
|
// Cursor provides no authoritative usage frame; token counts are heuristic estimates from
|
|
@@ -244,6 +271,8 @@ export function createCursorProtobufEventState(options: {
|
|
|
244
271
|
&& options.estimatedInputTokens > 0
|
|
245
272
|
? { estimatedInputTokens: options.estimatedInputTokens }
|
|
246
273
|
: {}),
|
|
274
|
+
...(options.wireModelId?.trim() ? { wireModelId: options.wireModelId.trim() } : {}),
|
|
275
|
+
identityScope: options.identityScope?.trim() || "local",
|
|
247
276
|
};
|
|
248
277
|
}
|
|
249
278
|
|
|
@@ -1048,6 +1077,7 @@ export function mapSyntheticMcpExecToToolEvents(
|
|
|
1048
1077
|
): CursorServerMessage[] {
|
|
1049
1078
|
if (args.providerIdentifier !== OCX_RESPONSES_TOOL_PROVIDER) return [];
|
|
1050
1079
|
if (options.state?.terminated) return [];
|
|
1080
|
+
if (options.state) options.state.sawRealClientToolCall = true;
|
|
1051
1081
|
if (options.allowEmptyArgs !== true && !hasMcpArgBytes(args)) return [];
|
|
1052
1082
|
const cursorWireName = mcpWireNameFromArgs(args);
|
|
1053
1083
|
if (!cursorWireName) return [{ type: "error", message: "Cursor requested a Responses tool without a tool name" }];
|
|
@@ -1117,6 +1147,11 @@ function recordToolCall(state: CursorProtobufEventState, callId: string, cursorW
|
|
|
1117
1147
|
return [];
|
|
1118
1148
|
}
|
|
1119
1149
|
|
|
1150
|
+
function recordRealToolCall(state: CursorProtobufEventState, callId: string, cursorWireName: string): CursorServerMessage[] {
|
|
1151
|
+
state.sawRealClientToolCall = true;
|
|
1152
|
+
return recordToolCall(state, callId, cursorWireName);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1120
1155
|
/**
|
|
1121
1156
|
* Emit a completed client tool call as one atomic unit: `tool_call_start` (deferred from open time),
|
|
1122
1157
|
* the full normalized arguments delta when present, then `tool_call_end`. The call must already be
|
|
@@ -1232,33 +1267,70 @@ export function mapCursorProtobufServerMessage(
|
|
|
1232
1267
|
if (state.terminated) return [];
|
|
1233
1268
|
|
|
1234
1269
|
if (serverMessage.message.case === "conversationCheckpointUpdate") {
|
|
1235
|
-
const
|
|
1270
|
+
const tokenDetails = serverMessage.message.value.tokenDetails;
|
|
1271
|
+
const usedTokens = tokenDetails?.usedTokens ?? 0;
|
|
1236
1272
|
// `usedTokens` is the ABSOLUTE conversation context size, not a per-turn output delta. Track it
|
|
1237
1273
|
// separately (monotonic max) and surface it as `done.usage.totalTokens`; folding it into
|
|
1238
1274
|
// `outputTokens` (which also accumulates `tokenDelta`) double-counts in Codex. See contextTokens.
|
|
1239
1275
|
observeContextTokens(state, usedTokens);
|
|
1276
|
+
// First checkpoints often send maxTokens=0 (senpi). Only a positive ceiling
|
|
1277
|
+
// replaces the id heuristic for the next turn's overflow vs 429 size prior.
|
|
1278
|
+
if (state.wireModelId) {
|
|
1279
|
+
recordObservedCursorContextWindow(state.wireModelId, tokenDetails?.maxTokens, {
|
|
1280
|
+
identityScope: state.identityScope,
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1240
1283
|
return [];
|
|
1241
1284
|
}
|
|
1242
1285
|
|
|
1243
1286
|
if (serverMessage.message.case !== "interactionUpdate") return [];
|
|
1244
1287
|
const update = serverMessage.message.value.message;
|
|
1245
1288
|
switch (update.case) {
|
|
1246
|
-
case "textDelta":
|
|
1247
|
-
//
|
|
1248
|
-
// the
|
|
1249
|
-
//
|
|
1250
|
-
|
|
1289
|
+
case "textDelta": {
|
|
1290
|
+
// Textual `[TOOL_CALL]name[ARGS]{…}` is not assistant prose. Leaving it in
|
|
1291
|
+
// the text channel (even after #2305 renamed the display alias) leaks a
|
|
1292
|
+
// synthetic protocol marker that later turns few-shot-mimic as inert text.
|
|
1293
|
+
// Strip complete markers, buffer advertised fallbacks until finalize,
|
|
1294
|
+
// and hold or suppress-scan an incomplete opener across deltas.
|
|
1295
|
+
const chunk = update.value.text ?? "";
|
|
1296
|
+
if (!chunk && !state.pendingTextToolCall && !state.suppressedTextToolCall) return [];
|
|
1297
|
+
const drained = drainCursorTextToolCalls(
|
|
1298
|
+
state.pendingTextToolCall ?? "",
|
|
1299
|
+
chunk,
|
|
1300
|
+
state.suppressedTextToolCall,
|
|
1301
|
+
);
|
|
1302
|
+
if (drained.pending) state.pendingTextToolCall = drained.pending;
|
|
1303
|
+
else delete state.pendingTextToolCall;
|
|
1304
|
+
if (drained.suppressed) state.suppressedTextToolCall = drained.suppressed;
|
|
1305
|
+
else delete state.suppressedTextToolCall;
|
|
1306
|
+
const out: CursorServerMessage[] = [];
|
|
1307
|
+
if (drained.text) out.push({ type: "text", text: drained.text });
|
|
1308
|
+
for (const call of drained.calls) {
|
|
1309
|
+
const advertised = resolveAdvertisedClientToolName(state, call.name);
|
|
1310
|
+
if (
|
|
1311
|
+
state.sawRealClientToolCall
|
|
1312
|
+
|| !state.clientToolNames
|
|
1313
|
+
|| !advertised
|
|
1314
|
+
|| (state.bufferedTextToolCalls?.length ?? 0) >= state.maxClientToolCalls
|
|
1315
|
+
) continue;
|
|
1316
|
+
(state.bufferedTextToolCalls ??= []).push({
|
|
1317
|
+
name: advertised,
|
|
1318
|
+
args: normalizeJsonText(call.args, advertised, state),
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
return out;
|
|
1322
|
+
}
|
|
1251
1323
|
case "thinkingDelta":
|
|
1252
1324
|
return update.value.text ? [{ type: "thinking", thinking: update.value.text }] : [];
|
|
1253
1325
|
case "toolCallStarted": {
|
|
1254
1326
|
const name = mcpCursorWireName(update.value.toolCall);
|
|
1255
1327
|
// Record the open call but defer the outward tool_call_start to completion (atomic emission).
|
|
1256
|
-
return name ?
|
|
1328
|
+
return name ? recordRealToolCall(state, update.value.callId, name) : [];
|
|
1257
1329
|
}
|
|
1258
1330
|
case "partialToolCall": {
|
|
1259
1331
|
const out: CursorServerMessage[] = [];
|
|
1260
1332
|
const name = mcpCursorWireName(update.value.toolCall);
|
|
1261
|
-
if (name) out.push(...
|
|
1333
|
+
if (name) out.push(...recordRealToolCall(state, update.value.callId, name));
|
|
1262
1334
|
if (out.some(event => event.type === "error")) return out;
|
|
1263
1335
|
// Buffer cumulative args; do not emit a delta. Args are emitted once, normalized, at completion.
|
|
1264
1336
|
if (state.openToolCalls.has(update.value.callId)) {
|
|
@@ -1274,6 +1346,7 @@ export function mapCursorProtobufServerMessage(
|
|
|
1274
1346
|
const out: CursorServerMessage[] = [];
|
|
1275
1347
|
if (state.completedToolCalls.has(update.value.callId)) return [];
|
|
1276
1348
|
const name = mcpCursorWireName(update.value.toolCall);
|
|
1349
|
+
if (name) state.sawRealClientToolCall = true;
|
|
1277
1350
|
const args = mcpArgsFromToolCall(update.value.toolCall);
|
|
1278
1351
|
const openBeforeStart = state.openToolCalls.get(update.value.callId);
|
|
1279
1352
|
// Empty-arg completion handling:
|
|
@@ -1362,20 +1435,46 @@ export function resolvedTurnUsage(state: CursorProtobufEventState): OcxUsage {
|
|
|
1362
1435
|
* with corrupt/empty arguments. Emit an explicit error instead of done (fail-closed).
|
|
1363
1436
|
* Mirrors kiro-truncation.ts behavior.
|
|
1364
1437
|
*/
|
|
1438
|
+
/**
|
|
1439
|
+
* True when this turn holds textual fallback tool calls that only turn finalization can emit.
|
|
1440
|
+
*
|
|
1441
|
+
* Cursor can close a stream with a clean Connect END_STREAM and no turnEnded frame. The transport
|
|
1442
|
+
* finalizes that path only for a turn it can see is unfinished, and a turn whose entire visible
|
|
1443
|
+
* text was a stripped marker looks empty from the outside. Without this the deferred fallback
|
|
1444
|
+
* would be dropped exactly when the marker was the turn's only content.
|
|
1445
|
+
*/
|
|
1446
|
+
export function hasBufferedTextToolCalls(state: CursorProtobufEventState): boolean {
|
|
1447
|
+
return (state.bufferedTextToolCalls?.length ?? 0) > 0;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1365
1450
|
export function finalizeTurnEvents(state: CursorProtobufEventState): CursorServerMessage[] {
|
|
1366
1451
|
state.terminated = true;
|
|
1452
|
+
delete state.pendingTextToolCall;
|
|
1453
|
+
delete state.suppressedTextToolCall;
|
|
1454
|
+
const bufferedTextToolCalls = state.bufferedTextToolCalls ?? [];
|
|
1455
|
+
delete state.bufferedTextToolCalls;
|
|
1367
1456
|
if (state.openToolCalls.size > 0) {
|
|
1368
1457
|
const openCallIds = [...state.openToolCalls.keys()];
|
|
1369
1458
|
const openIds = openCallIds.join(", ");
|
|
1370
1459
|
// Clear so a second turnEnded (should not happen, but defensive) doesn't re-emit.
|
|
1371
1460
|
for (const callId of openCallIds) state.translatorBudget?.closeCall(callId);
|
|
1372
1461
|
state.openToolCalls.clear();
|
|
1373
|
-
return [{ type: "error", message:
|
|
1462
|
+
return [{ type: "error", message: `${CURSOR_INCOMPLETE_TOOL_CALL_MESSAGE_PREFIX} ${openIds}. Arguments may be truncated; the call was not committed.` }];
|
|
1463
|
+
}
|
|
1464
|
+
const out: CursorServerMessage[] = [];
|
|
1465
|
+
if (!state.sawRealClientToolCall) {
|
|
1466
|
+
for (const call of bufferedTextToolCalls) {
|
|
1467
|
+
state.textToolCallSeq = (state.textToolCallSeq ?? 0) + 1;
|
|
1468
|
+
const callId = `textcall_${state.textToolCallSeq}`;
|
|
1469
|
+
out.push(...recordToolCall(state, callId, call.name));
|
|
1470
|
+
if (state.openToolCalls.has(callId)) out.push(...commitToolCall(state, callId, call.args));
|
|
1471
|
+
}
|
|
1374
1472
|
}
|
|
1375
1473
|
// Surface the absolute context size (when Cursor reported a checkpoint) as both totalTokens and
|
|
1376
1474
|
// the estimated input side of Codex's visible `input + output` counter. Codex status lines can
|
|
1377
1475
|
// render the additive pair instead of total_tokens, so leaving inputTokens at 0 makes a 16k-context
|
|
1378
1476
|
// first turn display as "9 used". Keep outputTokens as the per-turn delta and clamp the inferred
|
|
1379
1477
|
// input to 0 in case Cursor reports a checkpoint smaller than the streamed output delta.
|
|
1380
|
-
|
|
1478
|
+
out.push({ type: "done", usage: resolvedTurnUsage(state) });
|
|
1479
|
+
return out;
|
|
1381
1480
|
}
|
|
@@ -75,6 +75,8 @@ export const CURSOR_ROUTING_LEVEL_PARAMETER_ID = "optimization";
|
|
|
75
75
|
export const CURSOR_EXTERNAL_ROOT_BLOB_LIMIT = 192;
|
|
76
76
|
/** Approximate prompt-size guard; tool schemas and protocol framing consume context separately. */
|
|
77
77
|
export const CURSOR_EXTERNAL_ROOT_BYTE_LIMIT = 512 * 1024;
|
|
78
|
+
/** Honest placeholder when native Composer history has a toolCall with no matching toolResult. */
|
|
79
|
+
export const CURSOR_MISSING_TOOL_RESULT = "[missing tool_result for this tool_use in history]";
|
|
78
80
|
/**
|
|
79
81
|
* Byte budget for the serialized arguments named inside ONE replayed tool-result envelope. The
|
|
80
82
|
* invocation identifies the call; the result is the payload. Without an independent cap, a single
|
|
@@ -1218,6 +1220,20 @@ function argBytes(value: unknown): Uint8Array {
|
|
|
1218
1220
|
}
|
|
1219
1221
|
}
|
|
1220
1222
|
|
|
1223
|
+
function missingToolResultFor(
|
|
1224
|
+
part: Extract<OcxAssistantContentPart, { type: "toolCall" }>,
|
|
1225
|
+
): OcxToolResultMessage {
|
|
1226
|
+
return {
|
|
1227
|
+
role: "toolResult",
|
|
1228
|
+
toolCallId: part.id,
|
|
1229
|
+
toolName: part.name,
|
|
1230
|
+
...(part.namespace ? { toolNamespace: part.namespace } : {}),
|
|
1231
|
+
content: CURSOR_MISSING_TOOL_RESULT,
|
|
1232
|
+
isError: true,
|
|
1233
|
+
timestamp: 0,
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1221
1237
|
function toolCallStep(
|
|
1222
1238
|
part: Extract<OcxAssistantContentPart, { type: "toolCall" }>,
|
|
1223
1239
|
requestScope: CursorBlobRequestScopeToken,
|
|
@@ -1332,7 +1348,9 @@ function conversationTurns(
|
|
|
1332
1348
|
const pendingToolCalls = new Map<string, Extract<OcxAssistantContentPart, { type: "toolCall" }>>();
|
|
1333
1349
|
const flush = () => {
|
|
1334
1350
|
if (!current) return;
|
|
1335
|
-
for (const part of pendingToolCalls.values())
|
|
1351
|
+
for (const part of pendingToolCalls.values()) {
|
|
1352
|
+
current.steps.push(toolCallStep(part, requestScope, missingToolResultFor(part), codeMode));
|
|
1353
|
+
}
|
|
1336
1354
|
turns.push(storeCursorBlob(toBinary(ConversationTurnStructureSchema, create(ConversationTurnStructureSchema, {
|
|
1337
1355
|
turn: {
|
|
1338
1356
|
case: "agentConversationTurn",
|
|
@@ -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,70 @@ 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
|
+
const incompleteToolRemintByScope = new Map<string, IncompleteToolRemintState>();
|
|
173
|
+
|
|
174
|
+
function pruneIncompleteToolRemints(at: number): void {
|
|
175
|
+
for (const [scopeKey, entry] of incompleteToolRemintByScope) {
|
|
176
|
+
if (at - entry.updatedAt > CURSOR_INCOMPLETE_TOOL_REMINT_TTL_MS) {
|
|
177
|
+
incompleteToolRemintByScope.delete(scopeKey);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
while (incompleteToolRemintByScope.size > CURSOR_INCOMPLETE_TOOL_REMINT_MAX_ENTRIES) {
|
|
181
|
+
const oldest = incompleteToolRemintByScope.keys().next().value;
|
|
182
|
+
if (oldest === undefined) break;
|
|
183
|
+
incompleteToolRemintByScope.delete(oldest);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Incomplete-tool and overflow recovery share ownership scope, but keep independent budgets. */
|
|
188
|
+
export function cursorIncompleteToolRemintScopeKey(
|
|
189
|
+
threadOwner: string | undefined,
|
|
190
|
+
identityScope?: string,
|
|
191
|
+
): string | null {
|
|
192
|
+
return cursorOverflowRemintScopeKey(threadOwner, identityScope);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Record one incomplete-tool remint; returns false when the independent cap is exhausted. */
|
|
196
|
+
export function recordCursorIncompleteToolRemint(scopeKey: string): boolean {
|
|
197
|
+
const at = now();
|
|
198
|
+
pruneIncompleteToolRemints(at);
|
|
199
|
+
const existing = incompleteToolRemintByScope.get(scopeKey);
|
|
200
|
+
if (existing && existing.remintCount >= CURSOR_INCOMPLETE_TOOL_REMINT_MAX) {
|
|
201
|
+
existing.updatedAt = at;
|
|
202
|
+
incompleteToolRemintByScope.delete(scopeKey);
|
|
203
|
+
incompleteToolRemintByScope.set(scopeKey, existing);
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
const entry = existing ?? { remintCount: 0, updatedAt: at };
|
|
207
|
+
entry.remintCount += 1;
|
|
208
|
+
entry.updatedAt = at;
|
|
209
|
+
incompleteToolRemintByScope.delete(scopeKey);
|
|
210
|
+
incompleteToolRemintByScope.set(scopeKey, entry);
|
|
211
|
+
pruneIncompleteToolRemints(at);
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** A clean turn replenishes this recovery without changing the overflow retry budget. */
|
|
216
|
+
export function clearCursorIncompleteToolRemint(scopeKey: string): void {
|
|
217
|
+
incompleteToolRemintByScope.delete(scopeKey);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function clearCursorIncompleteToolRemintForTests(): void {
|
|
221
|
+
incompleteToolRemintByScope.clear();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function cursorIncompleteToolRemintCountForTests(): number {
|
|
225
|
+
pruneIncompleteToolRemints(now());
|
|
226
|
+
return incompleteToolRemintByScope.size;
|
|
227
|
+
}
|
|
@@ -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. */
|