@bitkyc08/opencodex 2.64.0-preview.20260923 → 2.65.0-preview.20260925
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 -25
- package/bin/ocx.mjs +16 -2
- package/gui/dist/assets/App-8NMiZxT0.css +1 -0
- package/gui/dist/assets/App-CsYvvpr3.js +50 -0
- package/gui/dist/assets/Tray-DUvc_Wul.js +1 -0
- package/gui/dist/assets/{index-DiBRuK-d.css → index--EWgGQvZ.css} +1 -1
- package/gui/dist/assets/{index-BEq4OCOz.js → index-BB0iHG8-.js} +12 -12
- package/gui/dist/assets/tray-data-f0lTZ4sF.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -4
- package/src/adapters/anthropic.ts +30 -1
- package/src/adapters/claude-cli/adapter.ts +218 -0
- package/src/adapters/claude-cli/profiles.ts +38 -0
- package/src/adapters/codebuddy/profiles.ts +2 -0
- package/src/adapters/coding-agent/profile.ts +12 -4
- package/src/adapters/coding-agent/turn.ts +16 -6
- package/src/adapters/command-code-tool-text.ts +133 -23
- package/src/adapters/cursor/catalog.ts +4 -8
- package/src/adapters/cursor/discovery.ts +10 -5
- package/src/adapters/cursor/effort-map.ts +2 -6
- package/src/adapters/cursor/envelope-echo.ts +10 -5
- package/src/adapters/cursor/request-builder.ts +9 -4
- package/src/adapters/cursor/thread-continuity.ts +53 -0
- package/src/adapters/cursor.ts +29 -13
- package/src/adapters/devin/cloud-direct/chat.ts +3 -1
- package/src/adapters/google-tool-schema.ts +26 -3
- package/src/adapters/identity.ts +214 -2
- package/src/adapters/openai-chat/passthrough.ts +1 -0
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +569 -0
- package/src/adapters/openai-chat.ts +43 -15
- package/src/adapters/openai-responses/canonical-forward.ts +17 -10
- package/src/adapters/openai-responses/passthrough.ts +19 -4
- package/src/adapters/openai-responses/request-strips.ts +25 -0
- package/src/adapters/openai-responses/web-search.ts +22 -4
- package/src/adapters/qoder/profiles.ts +2 -0
- package/src/adapters/registry.ts +8 -0
- package/src/adapters/run-turn-queue.ts +7 -0
- package/src/bridge/response-json.ts +6 -4
- package/src/bridge/sse.ts +7 -5
- package/src/claude/alias.ts +73 -26
- package/src/claude/context-windows.ts +15 -6
- package/src/claude/desktop-3p-library.ts +3 -2
- package/src/claude/desktop-3p.ts +1 -1
- package/src/claude/desktop-first-party.ts +63 -22
- package/src/claude/desktop-picker-profile.ts +302 -0
- package/src/claude/desktop-picker.ts +365 -0
- package/src/claude/desktop-risk.ts +20 -0
- package/src/claude/gateway-cache.ts +13 -4
- package/src/claude/inbound.ts +5 -2
- package/src/claude/intercept/connect-proxy.ts +106 -29
- package/src/claude/intercept/local-ca.ts +76 -18
- package/src/claude/intercept/model-bindings.ts +145 -0
- package/src/claude/intercept/picker-bootstrap.ts +141 -0
- package/src/claude/intercept/picker-ca.ts +126 -0
- package/src/claude/intercept/picker-listener.ts +213 -0
- package/src/claude/intercept/picker-models.ts +101 -0
- package/src/claude/intercept/picker-runtime.ts +330 -0
- package/src/claude/intercept/picker-trust.ts +125 -0
- package/src/claude/intercept/runtime.ts +149 -2
- package/src/claude/model-info.ts +18 -4
- package/src/cli/agent.ts +16 -2
- package/src/cli/capabilities.ts +71 -2
- package/src/cli/claude-desktop.ts +319 -33
- package/src/cli/claude.ts +15 -7
- package/src/cli/codex-shim-autorestore.ts +1 -0
- package/src/cli/dispatch.ts +29 -23
- package/src/cli/effort.ts +16 -6
- package/src/cli/ensure-desired-integrations.ts +29 -4
- package/src/cli/ready.ts +1 -1
- package/src/cli/registry.ts +8 -0
- package/src/cli/restart-scope.ts +9 -1
- package/src/cli/runtime-api.ts +17 -0
- package/src/cli/system-command.ts +11 -12
- package/src/client/connect.ts +12 -2
- package/src/client/state.ts +39 -1
- package/src/clients/config-export.ts +57 -9
- package/src/codex/auth-context.ts +51 -11
- package/src/codex/catalog/derive-entry.ts +5 -5
- package/src/codex/catalog/gather-capture.ts +25 -6
- package/src/codex/catalog/metadata.ts +3 -3
- package/src/codex/catalog/parsing.ts +39 -1
- package/src/codex/catalog/provider-models.ts +8 -6
- package/src/codex/catalog/retained-sync.ts +17 -7
- package/src/codex/catalog/sync.ts +2 -2
- package/src/codex/desktop-app-restart.ts +8 -0
- package/src/codex/desktop-switches.ts +9 -1
- package/src/codex/home.ts +43 -4
- package/src/codex/inject/config-toml.ts +137 -0
- package/src/codex/inject/plan.ts +23 -0
- package/src/codex/inject/remove.ts +21 -1
- package/src/codex/inject.ts +63 -8
- package/src/codex/journal.ts +36 -0
- package/src/codex/main-account-hard-lock.ts +14 -1
- package/src/codex/main-account-policy-wait.ts +109 -0
- package/src/codex/native-profile-startup.ts +24 -6
- package/src/codex/native-residue.ts +28 -10
- package/src/codex/quota-types.ts +8 -1
- package/src/codex/quota.ts +49 -7
- package/src/codex/runtime.ts +31 -10
- package/src/combos/failover.ts +7 -6
- package/src/combos/resolve.ts +91 -14
- package/src/combos/types.ts +18 -1
- package/src/config/load-degrade.ts +2 -0
- package/src/config/schema/config-schema.ts +21 -2
- package/src/config/schema/leaf-validators.ts +6 -3
- package/src/config/subagent-models.ts +3 -1
- package/src/generated/compatibility-version.json +275 -163
- package/src/images/artifacts.ts +14 -0
- package/src/integrations/owned-refresh.ts +1 -1
- package/src/integrations/ownership-policy.ts +32 -1
- package/src/integrations/state.ts +3 -1
- package/src/integrations/writer.ts +9 -0
- package/src/lib/config-ownership.ts +3 -0
- package/src/lib/errors.ts +87 -0
- package/src/lib/package-tree-integrity.ts +1 -1
- package/src/lib/request-execution-budget.ts +42 -14
- package/src/lib/request-resend-gate.ts +4 -3
- package/src/lib/retry-delay.ts +7 -1
- package/src/lib/tool-envelope-echo-filter.ts +236 -0
- package/src/lib/upstream-retry.ts +20 -12
- package/src/oauth/index.ts +1 -0
- package/src/oauth/kiro-credentials.ts +14 -1
- package/src/oauth/login-cli.ts +1 -0
- package/src/providers/derive.ts +4 -0
- package/src/providers/fast-opt-in.ts +31 -0
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/provider-id-rewrite.ts +2 -0
- package/src/providers/quota/vendor-probes-key.ts +8 -2
- package/src/providers/registry/entries-core.ts +45 -2
- package/src/providers/registry/entries-extended.ts +99 -12
- package/src/providers/registry/model-ids.ts +2 -0
- package/src/providers/registry/types.ts +7 -1
- package/src/providers/resolved-model-policy.ts +11 -4
- package/src/providers/service-tier.ts +10 -3
- package/src/responses/code-mode-helper-compat.ts +3 -0
- package/src/responses/parser.ts +45 -3
- package/src/router.ts +6 -0
- package/src/server/auth-cors.ts +8 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/claude-messages.ts +69 -14
- package/src/server/grok-upstream-envelope-echo.ts +120 -0
- package/src/server/index/claude-intercept-lifecycle.ts +24 -0
- package/src/server/index/serve-options.ts +2 -2
- package/src/server/index.ts +7 -0
- package/src/server/management/agent-settings-routes.ts +204 -112
- package/src/server/management/claude-desktop-picker-routes.ts +128 -0
- package/src/server/management/combo-routes.ts +59 -2
- package/src/server/management/config-routes.ts +81 -39
- package/src/server/management/context.ts +3 -0
- package/src/server/management/native-integration-routes.ts +169 -93
- package/src/server/management/provider-routes.ts +46 -4
- package/src/server/management/route-registry.ts +10 -0
- package/src/server/management/routing-profile-routes.ts +9 -0
- package/src/server/management/sidebar-routes.ts +52 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/proxy-liveness.ts +38 -3
- package/src/server/request-log-account-rotation.ts +53 -0
- package/src/server/request-log.ts +3 -16
- package/src/server/responses/adapter-dispatch.ts +17 -1
- package/src/server/responses/agent-task-recovery.ts +49 -26
- package/src/server/responses/codex-ws-exchange.ts +18 -7
- package/src/server/responses/codex-ws-wire.ts +27 -4
- package/src/server/responses/core-combo.ts +21 -3
- package/src/server/responses/core-normalize.ts +7 -0
- package/src/server/responses/core-opaque-recovery.ts +3 -2
- package/src/server/responses/encrypted-payload.ts +15 -14
- package/src/server/responses/fetch-helpers.ts +1 -1
- package/src/server/responses/input-admission.ts +10 -6
- package/src/server/responses/native-response-control.ts +2 -2
- package/src/server/responses/passthrough-delivery.ts +182 -12
- package/src/server/responses/passthrough-dispatch.ts +102 -44
- package/src/server/responses/policy-fallback.ts +3 -0
- package/src/server/responses/policy-refusal.ts +67 -0
- package/src/server/responses/request-send-budget.ts +4 -0
- package/src/server/responses/run-turn-execution.ts +42 -11
- package/src/server/responses/ws-upstream.ts +2 -1
- package/src/server/system-env-shell.ts +0 -1
- package/src/server/system-env.ts +25 -4
- package/src/service/cli.ts +34 -3
- package/src/tray/assets/opencodex-tray-offline-update.ico +0 -0
- package/src/tray/assets/opencodex-tray-online-update.ico +0 -0
- package/src/tray/assets/opencodex-tray-warning-update.ico +0 -0
- package/src/tray/windows-tray.ps1 +188 -7
- package/src/tray/windows.ts +23 -2
- package/src/types/config.ts +52 -12
- package/src/types/provider.ts +22 -5
- package/src/types/tools.ts +11 -5
- package/src/types.ts +1 -0
- package/src/update/async-check.ts +109 -0
- package/src/update/badge.ts +11 -17
- package/src/update/check-types.ts +9 -0
- package/src/update/desktop-badge.ts +102 -0
- package/src/update/index.ts +48 -8
- package/src/update/install-detection.d.mts +29 -2
- package/src/update/install-detection.mjs +213 -7
- package/src/update/job.ts +17 -12
- package/src/update/notify.ts +16 -11
- package/src/update/pnpm-owner-worker.ts +12 -0
- package/src/update/refresh-scheduler.ts +141 -0
- package/src/usage/cost.ts +31 -4
- package/src/usage/expected-prices.ts +57 -0
- package/assets/download-linux.svg +0 -10
- package/assets/download-macos.svg +0 -10
- package/assets/download-windows.svg +0 -10
- package/gui/dist/assets/App-D5eN1TID.js +0 -50
- package/gui/dist/assets/App-I5AnaSLh.css +0 -1
- package/gui/dist/assets/Tray-Bh_ErQDh.js +0 -1
- package/gui/dist/assets/usage-companion-chart-DBG_37kQ.js +0 -1
|
@@ -22,6 +22,12 @@ import { validatesRestoredValue } from "./command-code-restored-schema";
|
|
|
22
22
|
* A text block that opens with `<tool_call>` is therefore held instead of streamed. It is dropped
|
|
23
23
|
* when a native call proves it is a duplicate, or restored on an eligible clean MiMo finish when
|
|
24
24
|
* it names a declared tool with arguments that fit its schema. Other markup is released unchanged.
|
|
25
|
+
* MiMo can also append the markup after ordinary prose inside one text block; the stream filter
|
|
26
|
+
* splits such a delta at the marker and holds the markup part the same way (#5698; a marker split
|
|
27
|
+
* across deltas after prose is still released as text).
|
|
28
|
+
* A malformed envelope that still opens and closes around a declared function name, but that the
|
|
29
|
+
* strict parser rejects, is dropped instead of released when the native call for that same function
|
|
30
|
+
* arrives, and on the clean-finish path, so the echo never reaches the client.
|
|
25
31
|
* Later text waits behind unresolved markup within the same byte bound.
|
|
26
32
|
*/
|
|
27
33
|
|
|
@@ -81,6 +87,21 @@ function tryJson(value: string): unknown {
|
|
|
81
87
|
try { return JSON.parse(value); } catch { return undefined; }
|
|
82
88
|
}
|
|
83
89
|
|
|
90
|
+
/**
|
|
91
|
+
* The declared function name of an envelope echo the strict parser rejected, or undefined when the
|
|
92
|
+
* text is anything else: malformed parameter tags, a missing `</function>`, garbage inside the body.
|
|
93
|
+
* Opening and closing as an envelope with a known function name is enough to keep it off the client
|
|
94
|
+
* once the native duplicate call for that same name — which carries the canonical execution —
|
|
95
|
+
* arrives; a native call for another tool says nothing about this envelope.
|
|
96
|
+
*/
|
|
97
|
+
function looseEnvelopeName(text: string, declared: CommandCodeDeclaredTools | undefined): string | undefined {
|
|
98
|
+
const trimmed = text.trim();
|
|
99
|
+
if (!trimmed.startsWith(TOOL_CALL_MARKER) || !trimmed.endsWith("</tool_call>")) return undefined;
|
|
100
|
+
if (trimmed.slice(TOOL_CALL_MARKER.length).includes(TOOL_CALL_MARKER)) return undefined;
|
|
101
|
+
const fn = /<function=([^>\s]+)>/.exec(trimmed);
|
|
102
|
+
return fn !== null && (declared?.has(fn[1]!) ?? false) ? fn[1]! : undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
84
105
|
function deepEqual(left: unknown, right: unknown): boolean {
|
|
85
106
|
if (Object.is(left, right)) return true;
|
|
86
107
|
if (typeof left !== "object" || typeof right !== "object" || left === null || right === null) return false;
|
|
@@ -251,7 +272,10 @@ const encoder = new TextEncoder();
|
|
|
251
272
|
export class CommandCodeToolTextFilter {
|
|
252
273
|
private readonly openInputs = new Map<string, string>();
|
|
253
274
|
private readonly blocks = new Map<string, TextBlock>();
|
|
254
|
-
/**
|
|
275
|
+
/**
|
|
276
|
+
* Only blocks still deciding whether their text is markup (state "probing") need boundary visits;
|
|
277
|
+
* a held block is deliberately absent, so no interleaved event can interrupt it.
|
|
278
|
+
*/
|
|
255
279
|
private readonly activeProbes = new Map<string, TextBlock>();
|
|
256
280
|
/** Held blocks in arrival order, including ended ones awaiting a verdict. */
|
|
257
281
|
private held: TextBlock[] = [];
|
|
@@ -283,6 +307,13 @@ export class CommandCodeToolTextFilter {
|
|
|
283
307
|
for (const [key, block] of this.activeProbes) {
|
|
284
308
|
this.queueOperations++;
|
|
285
309
|
if (key === exceptKey) continue;
|
|
310
|
+
// Guard only: a held block is not tracked here (textDelta adds probing blocks and drops them
|
|
311
|
+
// from the map on the transition to held). It must stay held until settle, in arrival order —
|
|
312
|
+
// interrupting it on interleaved events (reasoning deltas, other blocks, the native call
|
|
313
|
+
// itself) cleared complete and malformed envelopes alike and released the echoed call as text.
|
|
314
|
+
// The queued-byte bound (makeRoom) still caps memory and wait, flushing everything as text if a
|
|
315
|
+
// held envelope never resolves while the stream keeps producing.
|
|
316
|
+
if (block.state !== "probing") continue;
|
|
286
317
|
this.activeProbes.delete(key);
|
|
287
318
|
block.interrupted = true;
|
|
288
319
|
block.state = "queued";
|
|
@@ -310,8 +341,44 @@ export class CommandCodeToolTextFilter {
|
|
|
310
341
|
block = { id: key, markupParts: [], probe: "", bytes: 0, state: "probing", ended: false, interrupted: false, candidates: new Set(this.openInputs.keys()) };
|
|
311
342
|
this.blocks.set(key, block);
|
|
312
343
|
}
|
|
313
|
-
|
|
314
|
-
|
|
344
|
+
// MiMo can append tool-call markup after ordinary prose inside one text block. The probe below
|
|
345
|
+
// only recognizes a block that opens with the marker, so a marker arriving after prose would
|
|
346
|
+
// reach the client (captured 2026-09-23 from xiaomi/mimo-v2.6-pro: prose, then
|
|
347
|
+
// "<tool_call><function=exec>..." echoed by the gateway as one text delta). Split the delta at
|
|
348
|
+
// the marker: prose keeps its queued or streamed path, the markup starts a fresh probe block
|
|
349
|
+
// and follows the normal hold-and-restore route. A probing block that has consumed nothing but
|
|
350
|
+
// whitespace keeps its probe instead, because that probe already holds the marker.
|
|
351
|
+
if (block.state !== "held") {
|
|
352
|
+
const markerIndex = text.indexOf(TOOL_CALL_MARKER);
|
|
353
|
+
const whitespaceLead = markerIndex > 0 && block.state === "probing" && block.probe === ""
|
|
354
|
+
&& text.slice(0, markerIndex).trim() === "";
|
|
355
|
+
// markerIndex === 0 on a probing block is the ordinary hold path; on any other state the
|
|
356
|
+
// block is ordinary text and the marker must still start a fresh probe block.
|
|
357
|
+
if (!whitespaceLead && (markerIndex > 0 || (markerIndex === 0 && block.state !== "probing"))) {
|
|
358
|
+
const prose = markerIndex > 0 ? text.slice(0, markerIndex) : "";
|
|
359
|
+
const marked = markerIndex > 0 ? text.slice(markerIndex) : text;
|
|
360
|
+
let proseEvents: AdapterEvent[] = [];
|
|
361
|
+
if (prose) {
|
|
362
|
+
if (block.state === "streaming" && this.head === this.pending.length) {
|
|
363
|
+
proseEvents = [{ type: "text_delta", text: prose }];
|
|
364
|
+
} else {
|
|
365
|
+
if (block.state === "dropped" || block.state === "streaming") {
|
|
366
|
+
block = { id: key, markupParts: [], probe: "", bytes: 0, state: "queued", ended: false, interrupted: true, candidates: new Set() };
|
|
367
|
+
this.blocks.set(key, block);
|
|
368
|
+
}
|
|
369
|
+
proseEvents = this.queueProseDelta(block, prose);
|
|
370
|
+
this.probeBlockText(block, prose);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (block.state === "queued") block.state = "streaming";
|
|
374
|
+
this.activeProbes.delete(key);
|
|
375
|
+
const probeBlock: TextBlock = { id: key, markupParts: [], probe: "", bytes: 0, state: "probing", ended: false, interrupted: false, candidates: new Set(this.openInputs.keys()) };
|
|
376
|
+
this.blocks.set(key, probeBlock);
|
|
377
|
+
return [...boundaryEvents, ...proseEvents, ...this.textDelta(id, marked)];
|
|
378
|
+
}
|
|
379
|
+
if (markerIndex === -1 && block.state === "streaming" && this.head === this.pending.length) {
|
|
380
|
+
return [...boundaryEvents, { type: "text_delta", text }];
|
|
381
|
+
}
|
|
315
382
|
}
|
|
316
383
|
// Once a duplicate is dropped, later text is a new chunk at its own wire position.
|
|
317
384
|
if (block.state === "dropped" || block.state === "streaming") {
|
|
@@ -320,7 +387,7 @@ export class CommandCodeToolTextFilter {
|
|
|
320
387
|
}
|
|
321
388
|
const preceding = this.makeRoom(encoder.encode(text).byteLength);
|
|
322
389
|
this.retain(block, text);
|
|
323
|
-
if (block.state === "probing"
|
|
390
|
+
if (block.state === "probing") this.activeProbes.set(key, block);
|
|
324
391
|
const bytes = encoder.encode(text).byteLength;
|
|
325
392
|
const tail = this.pending.at(-1);
|
|
326
393
|
if (tail?.kind === "chunk" && tail.block === block && this.head < this.pending.length) {
|
|
@@ -332,24 +399,8 @@ export class CommandCodeToolTextFilter {
|
|
|
332
399
|
this.queueOperations++;
|
|
333
400
|
}
|
|
334
401
|
this.queuedBytes += bytes;
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
if (!block.probe && char.trim() === "") continue;
|
|
338
|
-
block.probe += char;
|
|
339
|
-
if (!TOOL_CALL_MARKER.startsWith(block.probe)) {
|
|
340
|
-
block.state = "queued";
|
|
341
|
-
block.markupParts = [];
|
|
342
|
-
break;
|
|
343
|
-
}
|
|
344
|
-
if (block.probe === TOOL_CALL_MARKER) {
|
|
345
|
-
block.state = "held";
|
|
346
|
-
block.probe = "";
|
|
347
|
-
this.held.push(block);
|
|
348
|
-
break;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
if (block.state !== "probing" && block.state !== "held") this.activeProbes.delete(key);
|
|
402
|
+
this.probeBlockText(block, text);
|
|
403
|
+
if (block.state !== "probing") this.activeProbes.delete(key);
|
|
353
404
|
return [...boundaryEvents, ...preceding, ...this.limitPending()];
|
|
354
405
|
}
|
|
355
406
|
|
|
@@ -411,7 +462,8 @@ export class CommandCodeToolTextFilter {
|
|
|
411
462
|
remaining.push(block);
|
|
412
463
|
continue;
|
|
413
464
|
}
|
|
414
|
-
const
|
|
465
|
+
const text = block.markupParts.join("");
|
|
466
|
+
const markup = parseToolCallMarkup(text);
|
|
415
467
|
if (markup && markup.name === name && markupMatchesInput(markup, input)) {
|
|
416
468
|
this.drop(block);
|
|
417
469
|
block.state = "dropped";
|
|
@@ -420,6 +472,16 @@ export class CommandCodeToolTextFilter {
|
|
|
420
472
|
}
|
|
421
473
|
block.candidates.delete(id);
|
|
422
474
|
if (block.candidates.size === 0) {
|
|
475
|
+
// A malformed envelope cannot match a native input, but it is still an envelope: when the
|
|
476
|
+
// native call is for the function it declares, that call carries the execution, so drop the
|
|
477
|
+
// echo rather than releasing it as text. A native call for any other tool proves nothing
|
|
478
|
+
// about this envelope, so it keeps the release-as-text path below.
|
|
479
|
+
if (markup === undefined && looseEnvelopeName(text, this.declared) === name) {
|
|
480
|
+
this.drop(block);
|
|
481
|
+
block.state = "dropped";
|
|
482
|
+
this.activeProbes.delete(block.id);
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
423
485
|
block.state = "queued";
|
|
424
486
|
block.markupParts = [];
|
|
425
487
|
this.activeProbes.delete(block.id);
|
|
@@ -472,6 +534,16 @@ export class CommandCodeToolTextFilter {
|
|
|
472
534
|
events.push({ type: "tool_call_end" });
|
|
473
535
|
salvaged = true;
|
|
474
536
|
lastTextBlock = undefined;
|
|
537
|
+
} else if (restore && !block.interrupted && markup === undefined
|
|
538
|
+
&& looseEnvelopeName(block.markupParts.join(""), this.declared) !== undefined) {
|
|
539
|
+
// A malformed envelope is still an envelope: the native duplicate (observed in every
|
|
540
|
+
// capture) carries the call, so the echo is dropped rather than rendered as text.
|
|
541
|
+
// Releasing it would put the raw markup back on screen; restoring it could execute a
|
|
542
|
+
// second time alongside the native call. The parser must have rejected the text, so an
|
|
543
|
+
// envelope that parses but does not fit its schema keeps the release-as-text contract.
|
|
544
|
+
this.drop(block);
|
|
545
|
+
block.state = "dropped";
|
|
546
|
+
lastTextBlock = undefined;
|
|
475
547
|
} else {
|
|
476
548
|
block.state = "queued";
|
|
477
549
|
block.markupParts = [];
|
|
@@ -501,6 +573,44 @@ export class CommandCodeToolTextFilter {
|
|
|
501
573
|
block.bytes += bytes;
|
|
502
574
|
}
|
|
503
575
|
|
|
576
|
+
/** The incremental open-of-block probe: decide whether the block's text is tool-call markup. */
|
|
577
|
+
private probeBlockText(block: TextBlock, text: string): void {
|
|
578
|
+
if (block.state !== "probing") return;
|
|
579
|
+
for (const char of text) {
|
|
580
|
+
if (!block.probe && char.trim() === "") continue;
|
|
581
|
+
block.probe += char;
|
|
582
|
+
if (!TOOL_CALL_MARKER.startsWith(block.probe)) {
|
|
583
|
+
block.state = "queued";
|
|
584
|
+
block.markupParts = [];
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
if (block.probe === TOOL_CALL_MARKER) {
|
|
588
|
+
block.state = "held";
|
|
589
|
+
block.probe = "";
|
|
590
|
+
this.held.push(block);
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/** Route ordinary prose through the queued wire path (shared by the mid-stream marker split). */
|
|
597
|
+
private queueProseDelta(block: TextBlock, prose: string): AdapterEvent[] {
|
|
598
|
+
const preceding = this.makeRoom(encoder.encode(prose).byteLength);
|
|
599
|
+
this.retain(block, prose);
|
|
600
|
+
const bytes = encoder.encode(prose).byteLength;
|
|
601
|
+
const tail = this.pending.at(-1);
|
|
602
|
+
if (tail?.kind === "chunk" && tail.block === block && this.head < this.pending.length) {
|
|
603
|
+
tail.parts.push(prose);
|
|
604
|
+
tail.bytes += bytes;
|
|
605
|
+
this.queueOperations++;
|
|
606
|
+
} else {
|
|
607
|
+
this.pending.push({ kind: "chunk", block, parts: [prose], bytes });
|
|
608
|
+
this.queueOperations++;
|
|
609
|
+
}
|
|
610
|
+
this.queuedBytes += bytes;
|
|
611
|
+
return preceding;
|
|
612
|
+
}
|
|
613
|
+
|
|
504
614
|
private drop(block: TextBlock): void {
|
|
505
615
|
this.budget.releaseRetained(block.bytes, { kind: "live_transient" });
|
|
506
616
|
this.queuedBytes = Math.max(0, this.queuedBytes - block.bytes);
|
|
@@ -184,19 +184,15 @@ export const CURSOR_CAPABILITIES: Record<string, CursorCapability> = {
|
|
|
184
184
|
thinkingFast: { levels: FULL, order: T },
|
|
185
185
|
},
|
|
186
186
|
},
|
|
187
|
-
// 260923 Claude Opus 5.5:
|
|
188
|
-
//
|
|
189
|
-
// The ladders mirror the measured claude-opus-5 rows (fast stops at high) until the live
|
|
190
|
-
// GetUsableModels roster is dumped; the live filter drops any id the account cannot use.
|
|
187
|
+
// 260923 Claude Opus 5.5: live GetUsableModels roster advertises flat effort-suffixed
|
|
188
|
+
// wire ids (claude-opus-5-5-{low..max} and -fast) rather than a thinking variant.
|
|
191
189
|
"claude-opus-5-5": {
|
|
192
190
|
displayName: "Claude Opus 5.5",
|
|
193
191
|
window: CONTEXT_1M,
|
|
194
|
-
defaultVariant: "
|
|
192
|
+
defaultVariant: "regular",
|
|
195
193
|
variants: {
|
|
196
194
|
regular: { levels: FULL },
|
|
197
|
-
|
|
198
|
-
fast: { levels: ["low", "medium", "high"] },
|
|
199
|
-
thinkingFast: { levels: FULL, order: T },
|
|
195
|
+
fast: { levels: FULL },
|
|
200
196
|
},
|
|
201
197
|
},
|
|
202
198
|
"glm-5.2": {
|
|
@@ -270,15 +270,20 @@ export function isCursorExternalWireModel(modelId: string): boolean {
|
|
|
270
270
|
* Observed on live Cursor Connect traffic (2026-08-20): `composer-2.5` (the
|
|
271
271
|
* standard, non-fast build) resumes a tool-result turn with server-side native
|
|
272
272
|
* tool calls (read/grep/exec) instead of answering, or completes with zero text
|
|
273
|
-
* (empty `content` + `stop`).
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
273
|
+
* (empty `content` + `stop`). Sending the same continuation as an explicit user
|
|
274
|
+
* message (external path) makes the model answer reliably.
|
|
275
|
+
*
|
|
276
|
+
* `composer-2.5-fast` was left on resumeAction after that same capture because
|
|
277
|
+
* it answered on that path then. A later live proxy log (2026-09-21) shows
|
|
278
|
+
* `cursor/composer-2.5-fast completed with no output text and no tool call` on
|
|
279
|
+
* the Chat Completions → Responses bridge used by OpenAI-compatible clients
|
|
280
|
+
* (GJC executor). That is the same empty-stop shape, so the fast id uses the
|
|
281
|
+
* external continuation path too.
|
|
277
282
|
*/
|
|
278
283
|
export function cursorNeedsExternalToolContinuation(modelId: string): boolean {
|
|
279
284
|
if (isCursorExternalWireModel(modelId)) return true;
|
|
280
285
|
const wire = cursorCodexToWireModelId(modelId).trim().toLowerCase();
|
|
281
|
-
return wire === "composer-2.5";
|
|
286
|
+
return wire === "composer-2.5" || wire === "composer-2.5-fast";
|
|
282
287
|
}
|
|
283
288
|
|
|
284
289
|
function stripCursorEffortSuffix(wireModelId: string): string {
|
|
@@ -36,9 +36,9 @@ const CURSOR_MODEL_EFFORT_TIERS: Record<string, readonly string[]> = {
|
|
|
36
36
|
"claude-opus-4-8-fast": ["low", "medium", "high", "xhigh", "max"],
|
|
37
37
|
"claude-opus-5": ["low", "medium", "high", "xhigh", "max"],
|
|
38
38
|
"claude-opus-5-fast": ["low", "medium", "high"],
|
|
39
|
-
// 260923 Opus 5.5
|
|
39
|
+
// 260923 Opus 5.5: live GetUsableModels roster advertises low..max in both regular and fast forms.
|
|
40
40
|
"claude-opus-5-5": ["low", "medium", "high", "xhigh", "max"],
|
|
41
|
-
"claude-opus-5-5-fast": ["low", "medium", "high"],
|
|
41
|
+
"claude-opus-5-5-fast": ["low", "medium", "high", "xhigh", "max"],
|
|
42
42
|
"claude-sonnet-5": ["low", "medium", "high", "xhigh", "max"],
|
|
43
43
|
"glm-5.2": ["high", "max"],
|
|
44
44
|
// 260825 live GetUsableModels. gemini-3.6-flash was the first Cursor model exposing
|
|
@@ -56,8 +56,6 @@ const CURSOR_MODEL_EFFORT_TIERS: Record<string, readonly string[]> = {
|
|
|
56
56
|
// 4.6-opus thinks only at high/max, 4.5-opus only at high, 4.6-sonnet only at medium.
|
|
57
57
|
"claude-opus-5-thinking": ["low", "medium", "high", "xhigh", "max"],
|
|
58
58
|
"claude-opus-5-thinking-fast": ["low", "medium", "high", "xhigh", "max"],
|
|
59
|
-
"claude-opus-5-5-thinking": ["low", "medium", "high", "xhigh", "max"],
|
|
60
|
-
"claude-opus-5-5-thinking-fast": ["low", "medium", "high", "xhigh", "max"],
|
|
61
59
|
"claude-opus-4-8-thinking": ["low", "medium", "high", "xhigh", "max"],
|
|
62
60
|
"claude-opus-4-8-thinking-fast": ["low", "medium", "high", "xhigh", "max"],
|
|
63
61
|
"claude-opus-4-7-thinking": ["low", "medium", "high", "xhigh", "max"],
|
|
@@ -139,8 +137,6 @@ const CANONICAL_CODEX_EFFORT_ORDER = ["low", "medium", "high", "xhigh", "max"] a
|
|
|
139
137
|
const CURSOR_THINKING_FAMILIES: Readonly<Record<string, { source: string; order: "thinking-then-effort" | "effort-then-thinking" | "bare" }>> = {
|
|
140
138
|
"claude-opus-5-thinking": { source: "claude-opus-5", order: "thinking-then-effort" },
|
|
141
139
|
"claude-opus-5-thinking-fast": { source: "claude-opus-5-fast", order: "thinking-then-effort" },
|
|
142
|
-
"claude-opus-5-5-thinking": { source: "claude-opus-5-5", order: "thinking-then-effort" },
|
|
143
|
-
"claude-opus-5-5-thinking-fast": { source: "claude-opus-5-5-fast", order: "thinking-then-effort" },
|
|
144
140
|
"claude-opus-4-8-thinking": { source: "claude-opus-4-8", order: "thinking-then-effort" },
|
|
145
141
|
"claude-opus-4-8-thinking-fast": { source: "claude-opus-4-8-fast", order: "thinking-then-effort" },
|
|
146
142
|
"claude-opus-4-7-thinking": { source: "claude-opus-4-7", order: "thinking-then-effort" },
|
|
@@ -12,11 +12,13 @@
|
|
|
12
12
|
* cursor.ts to retry before any invalid text reaches the client.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { closedFenceLines, isWholeLineEchoMarker } from "../../lib/tool-envelope-echo-filter";
|
|
16
|
+
|
|
15
17
|
const ECHO_MARKERS = ["[Tool Result]", "[Tool Error]", "[tool_result]"] as const;
|
|
18
|
+
const REPLAY_ECHO_PREFIXES = ["[Tool call:", "[Tool Call]", "[Tool Result", "[Tool Error", "[tool_result"] as const;
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
+
// Whole-line only, the same rule as the live filter: prose that starts with a marker survives.
|
|
21
|
+
const isEchoMarkerLine = isWholeLineEchoMarker;
|
|
20
22
|
|
|
21
23
|
/**
|
|
22
24
|
* Drop echoed tool-result envelopes from assistant history before Cursor root replay.
|
|
@@ -42,15 +44,18 @@ function isEchoMarkerLine(line: string): boolean {
|
|
|
42
44
|
* Only whole-line markers count, so prose such as "the string [Tool Result] appeared" survives.
|
|
43
45
|
*/
|
|
44
46
|
export function stripAssistantEchoedToolEnvelope(text: string): string {
|
|
45
|
-
if (!text || !
|
|
47
|
+
if (!text || !REPLAY_ECHO_PREFIXES.some(marker => text.includes(marker))) return text;
|
|
46
48
|
const newline = text.includes("\r\n") ? "\r\n" : "\n";
|
|
47
49
|
const lines = text.split(/\r?\n/);
|
|
48
50
|
const kept: string[] = [];
|
|
49
51
|
let dropped = false;
|
|
50
52
|
let index = 0;
|
|
53
|
+
// A marker inside a fenced code block that closes is an example the model showed, not an echo;
|
|
54
|
+
// the live filter releases it as code, so replay keeps it too. An unclosed block shields nothing.
|
|
55
|
+
const shielded = closedFenceLines(lines);
|
|
51
56
|
while (index < lines.length) {
|
|
52
57
|
const line = lines[index] ?? "";
|
|
53
|
-
if (!isEchoMarkerLine(line)) {
|
|
58
|
+
if (shielded[index] || !isEchoMarkerLine(line)) {
|
|
54
59
|
kept.push(line);
|
|
55
60
|
index += 1;
|
|
56
61
|
continue;
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
isCursorExecutionPathTool,
|
|
27
27
|
isCursorWaitTool,
|
|
28
28
|
} from "./tool-definitions";
|
|
29
|
-
import { lookupCursorThreadConversation } from "./thread-continuity";
|
|
29
|
+
import { lookupCursorThreadConversation, resolveCursorConversationRewrite } from "./thread-continuity";
|
|
30
30
|
import {
|
|
31
31
|
getCursorCheckpoint,
|
|
32
32
|
getCursorCheckpointForPrefix,
|
|
@@ -367,11 +367,16 @@ export function resolveCursorConversationId(
|
|
|
367
367
|
// the override check has to exclude it explicitly rather than rely on that flag.
|
|
368
368
|
if (threadId && parsed._compactionRequest !== true) {
|
|
369
369
|
const recovered = lookupCursorThreadConversation(threadId, parsed._cursorIdentityScope);
|
|
370
|
-
if (recovered) return recovered;
|
|
370
|
+
if (recovered) return resolveCursorConversationRewrite(recovered, parsed._cursorIdentityScope);
|
|
371
|
+
}
|
|
372
|
+
if (parsed._cursorConversationId) {
|
|
373
|
+
return resolveCursorConversationRewrite(parsed._cursorConversationId, parsed._cursorIdentityScope);
|
|
371
374
|
}
|
|
372
|
-
if (parsed._cursorConversationId) return parsed._cursorConversationId;
|
|
373
375
|
if (threadId) {
|
|
374
|
-
return
|
|
376
|
+
return resolveCursorConversationRewrite(
|
|
377
|
+
cursorConversationIdFromClientThread(`thread:${threadId}`, parsed._cursorIdentityScope),
|
|
378
|
+
parsed._cursorIdentityScope,
|
|
379
|
+
);
|
|
375
380
|
}
|
|
376
381
|
return generatedCursorConversationId();
|
|
377
382
|
}
|
|
@@ -6,10 +6,56 @@
|
|
|
6
6
|
* recovered id instead of recomputing the stale deterministic thread hash.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
|
|
9
11
|
const OVERRIDE_TTL_MS = 60 * 60 * 1000;
|
|
10
12
|
const OVERRIDE_MAX_ENTRIES = 2048;
|
|
11
13
|
|
|
12
14
|
const overrides = new Map<string, { conversationId: string; updatedAt: number }>();
|
|
15
|
+
const conversationRewrites = new Map<string, { to: string; root: string; updatedAt: number }>();
|
|
16
|
+
|
|
17
|
+
function rewriteKey(conversationId: string, identityScope?: string): string {
|
|
18
|
+
const identity = createHash("sha256").update(identityScope?.trim() || "local").digest("hex");
|
|
19
|
+
return `${identity}\0${conversationId}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function pruneRewrites(at: number): void {
|
|
23
|
+
for (const [key, entry] of conversationRewrites) {
|
|
24
|
+
if (at - entry.updatedAt > OVERRIDE_TTL_MS) conversationRewrites.delete(key);
|
|
25
|
+
}
|
|
26
|
+
while (conversationRewrites.size > OVERRIDE_MAX_ENTRIES) {
|
|
27
|
+
const oldest = conversationRewrites.keys().next().value;
|
|
28
|
+
if (oldest === undefined) break;
|
|
29
|
+
conversationRewrites.delete(oldest);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function rememberCursorConversationRewrite(from: string, to: string, identityScope?: string): void {
|
|
34
|
+
if (!from || !to || from === to) return;
|
|
35
|
+
const at = now();
|
|
36
|
+
pruneRewrites(at);
|
|
37
|
+
const scope = rewriteKey("", identityScope);
|
|
38
|
+
const root = conversationRewrites.get(rewriteKey(from, identityScope))?.root ?? from;
|
|
39
|
+
const redirects = [...conversationRewrites].filter(([key, entry]) => key.startsWith(scope) && entry.root === root);
|
|
40
|
+
for (const [key] of redirects) {
|
|
41
|
+
conversationRewrites.delete(key);
|
|
42
|
+
conversationRewrites.set(key, { to, root, updatedAt: at });
|
|
43
|
+
}
|
|
44
|
+
conversationRewrites.set(rewriteKey(from, identityScope), { to, root, updatedAt: at });
|
|
45
|
+
conversationRewrites.set(rewriteKey(to, identityScope), { to, root, updatedAt: at });
|
|
46
|
+
pruneRewrites(at);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function resolveCursorConversationRewrite(conversationId: string, identityScope?: string): string {
|
|
50
|
+
const at = now();
|
|
51
|
+
pruneRewrites(at);
|
|
52
|
+
const key = rewriteKey(conversationId, identityScope);
|
|
53
|
+
const entry = conversationRewrites.get(key);
|
|
54
|
+
if (!entry) return conversationId;
|
|
55
|
+
conversationRewrites.delete(key);
|
|
56
|
+
conversationRewrites.set(key, { ...entry, updatedAt: at });
|
|
57
|
+
return entry.to;
|
|
58
|
+
}
|
|
13
59
|
|
|
14
60
|
function now(): number {
|
|
15
61
|
return Date.now();
|
|
@@ -64,6 +110,7 @@ export function lookupCursorThreadConversation(
|
|
|
64
110
|
|
|
65
111
|
export function clearCursorThreadContinuityForTests(): void {
|
|
66
112
|
overrides.clear();
|
|
113
|
+
conversationRewrites.clear();
|
|
67
114
|
}
|
|
68
115
|
|
|
69
116
|
/** Max conversation-id remints after the first surfaced overflow per retained scope. */
|
|
@@ -278,7 +325,13 @@ const envelopeEchoRemintBudget = createCursorRemintBudget(
|
|
|
278
325
|
export function cursorEnvelopeEchoRemintScopeKey(
|
|
279
326
|
threadOwner: string | undefined,
|
|
280
327
|
identityScope?: string,
|
|
328
|
+
conversationId?: string,
|
|
281
329
|
): string | null {
|
|
330
|
+
if (conversationId) {
|
|
331
|
+
pruneRewrites(now());
|
|
332
|
+
const root = conversationRewrites.get(rewriteKey(conversationId, identityScope))?.root ?? conversationId;
|
|
333
|
+
return `echo\0${rewriteKey(root, identityScope)}`;
|
|
334
|
+
}
|
|
282
335
|
return cursorOverflowRemintScopeKey(threadOwner, identityScope);
|
|
283
336
|
}
|
|
284
337
|
|
package/src/adapters/cursor.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { ProviderAdapter } from "./base";
|
|
|
4
4
|
import { isTranslatorBudgetExceededError } from "../lib/translator-budget";
|
|
5
5
|
import { cursorExecDeniedMessage, cursorRequestDeclaresFullAccess } from "./cursor/exec-policy";
|
|
6
6
|
import { isCursorBenignCancelError, isCursorIncompleteToolCallMessage, isCursorInvalidArgumentError, isCursorOverflowRemintCandidate, isCursorRootEnvelopeError, safeCursorErrorMessage, type CursorSizeContext } from "./cursor/cursor-errors";
|
|
7
|
-
import { cursorCheckpointModelAffinityId, inferCursorContextWindow, isCursorExternalWireModel } from "./cursor/discovery";
|
|
7
|
+
import { cursorCheckpointModelAffinityId, cursorNeedsExternalToolContinuation, inferCursorContextWindow, isCursorExternalWireModel } from "./cursor/discovery";
|
|
8
8
|
import { createCursorKvStore, type CursorKvStore } from "./cursor/kv-store";
|
|
9
9
|
import { mapCursorServerMessage } from "./cursor/message-mapper";
|
|
10
10
|
import {
|
|
@@ -31,6 +31,7 @@ import { debugProviderDiagnostic } from "../lib/debug";
|
|
|
31
31
|
import { isDebugEnabled } from "../lib/debug-settings";
|
|
32
32
|
import { createAdapterTierMetadata } from "../providers/fastwire";
|
|
33
33
|
import { estimateTokens } from "../lib/token-estimate";
|
|
34
|
+
import { ToolEnvelopeEchoFilter } from "../lib/tool-envelope-echo-filter";
|
|
34
35
|
import {
|
|
35
36
|
clearCursorIncompleteToolRemint,
|
|
36
37
|
cursorIncompleteToolRemintScopeKey,
|
|
@@ -41,6 +42,7 @@ import {
|
|
|
41
42
|
recordCursorIncompleteToolRemint,
|
|
42
43
|
recordCursorEnvelopeEchoRemint,
|
|
43
44
|
recordCursorOverflowRemint,
|
|
45
|
+
rememberCursorConversationRewrite,
|
|
44
46
|
rememberCursorThreadConversation,
|
|
45
47
|
shouldSkipCursorOverflowRemint,
|
|
46
48
|
shouldSurfaceCursorOverflowFirst,
|
|
@@ -299,13 +301,18 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
299
301
|
// Armed for ANY external turn whose replayed history contains a tool result — echo
|
|
300
302
|
// priming was observed live on user-action rounds too (the envelope lives in the
|
|
301
303
|
// flattened history regardless of which role ends the input).
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
&& (_parsed.context.messages ?? []).some(message => message.role === "toolResult");
|
|
304
|
+
const replaysToolResult = (_parsed.context.messages ?? []).some(message => message.role === "toolResult");
|
|
305
|
+
const armEchoSniffer = isCursorExternalWireModel(activeRequest.modelId) && replaysToolResult;
|
|
305
306
|
const echoSniffer = armEchoSniffer ? new CursorEnvelopeEchoSniffer() : undefined;
|
|
306
307
|
// Mid-stream observer (devlog 260828 F1/F2): diagnostic-only; armed with the
|
|
307
308
|
// prefix sniffer because both fire on flattened tool-result replay priming.
|
|
308
|
-
|
|
309
|
+
// The composer-2.5 builds are native-wire but also replay the tool result as root text
|
|
310
|
+
// (cursorNeedsExternalToolContinuation), so the client-visible strip and the next-turn
|
|
311
|
+
// remint follow that predicate. The prefix retry above stays external-only: its
|
|
312
|
+
// corrective continuation text is encoded for external wire models alone.
|
|
313
|
+
const armMidstreamEcho = cursorNeedsExternalToolContinuation(activeRequest.modelId) && replaysToolResult;
|
|
314
|
+
const midstreamObserver = armMidstreamEcho ? new CursorMidstreamEchoObserver() : undefined;
|
|
315
|
+
const midstreamFilter = armMidstreamEcho ? new ToolEnvelopeEchoFilter() : undefined;
|
|
309
316
|
const armRoutingCommentarySniffer =
|
|
310
317
|
isCursorExternalWireModel(activeRequest.modelId)
|
|
311
318
|
&& (
|
|
@@ -321,8 +328,11 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
321
328
|
// Exactly-once observation: every client-bound text delta passes through here
|
|
322
329
|
// exactly once — held deltas only on release, ordinary deltas at emit time.
|
|
323
330
|
const emitTextObserved = (event: AdapterEvent): void => {
|
|
324
|
-
if (event.type
|
|
325
|
-
|
|
331
|
+
if (event.type !== "text_delta") { emit(event); return; }
|
|
332
|
+
midstreamObserver?.feed(event.text);
|
|
333
|
+
const text = midstreamFilter?.feed(event.text) ?? event.text;
|
|
334
|
+
if (midstreamFilter?.matched) sawMidstreamEnvelopeEcho = true;
|
|
335
|
+
if (text) emit({ ...event, text });
|
|
326
336
|
};
|
|
327
337
|
const releaseGuardHeld = () => {
|
|
328
338
|
for (const held of guardHeld) {
|
|
@@ -453,8 +463,13 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
453
463
|
}
|
|
454
464
|
if (event.type !== "heartbeat") emittedOutput = true;
|
|
455
465
|
if (event.type === "done") {
|
|
466
|
+
const suffix = midstreamFilter?.finish();
|
|
467
|
+
// A fenced marker released by hold overflow is unproven code: remint to be safe.
|
|
468
|
+
if (midstreamFilter?.matched || midstreamFilter?.unverifiedMarker) sawMidstreamEnvelopeEcho = true;
|
|
469
|
+
if (suffix) emit({ type: "text_delta", text: suffix });
|
|
470
|
+
// The observer is diagnostic only: the fence-aware filter's verdict decides the
|
|
471
|
+
// remint, so a quoted marker in a closed code block does not rotate the thread.
|
|
456
472
|
const midstreamFindings = midstreamObserver?.findings() ?? [];
|
|
457
|
-
if (midstreamFindings.length > 0) sawMidstreamEnvelopeEcho = true;
|
|
458
473
|
for (const finding of midstreamFindings) {
|
|
459
474
|
debugProviderDiagnostic("cursor", "midstream-envelope-echo", {
|
|
460
475
|
wireModel: activeRequest.modelId,
|
|
@@ -507,6 +522,9 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
507
522
|
// the next turn does not recompute the stale deterministic thread hash. Isolated helper /
|
|
508
523
|
// compaction turns must not park their throwaway id under the parent or Desktop owner.
|
|
509
524
|
const threadOwner = cursorClientThreadOwner(_parsed);
|
|
525
|
+
if (_parsed._cursorIsolateConversation !== true) {
|
|
526
|
+
rememberCursorConversationRewrite(failedConversationId, next.conversationId, _parsed._cursorIdentityScope);
|
|
527
|
+
}
|
|
510
528
|
if (threadOwner && _parsed._cursorIsolateConversation !== true) {
|
|
511
529
|
rememberCursorThreadConversation(
|
|
512
530
|
threadOwner,
|
|
@@ -629,11 +647,8 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
629
647
|
} else if (!sawIncompleteToolCall && completedNormally && incompleteToolRemintScopeKey) {
|
|
630
648
|
clearCursorIncompleteToolRemint(incompleteToolRemintScopeKey);
|
|
631
649
|
}
|
|
632
|
-
//
|
|
633
|
-
//
|
|
634
|
-
// the envelope. It cannot be quarantined, so the recovery is the same as the
|
|
635
|
-
// incomplete-tool case: leave this turn alone and rotate the next turn's id, otherwise
|
|
636
|
-
// the stored echo is replayed and primes the model to echo again.
|
|
650
|
+
// Strip a split mid-stream marker before it reaches the client, then rotate the next
|
|
651
|
+
// conversation because the upstream checkpoint may still contain the echoed envelope.
|
|
637
652
|
//
|
|
638
653
|
// Its own budget, not the incomplete-tool one: echoing is cheap and repeatable while an
|
|
639
654
|
// incomplete client-tool stream is rare and structural, so a shared counter would let a
|
|
@@ -645,6 +660,7 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
645
660
|
? cursorEnvelopeEchoRemintScopeKey(
|
|
646
661
|
cursorClientThreadOwner(_parsed),
|
|
647
662
|
_parsed._cursorIdentityScope,
|
|
663
|
+
request.conversationId,
|
|
648
664
|
)
|
|
649
665
|
: null;
|
|
650
666
|
if (sawMidstreamEnvelopeEcho && !sawIncompleteToolCall && envelopeEchoRemintScopeKey) {
|
|
@@ -1559,7 +1559,9 @@ export async function* streamChatEvents(req: CloudChatRequest): AsyncGenerator<C
|
|
|
1559
1559
|
// trace id are the only upstream-controlled fields that reach the error.
|
|
1560
1560
|
// The stated delay rides along in our own words so a client can still
|
|
1561
1561
|
// tell how long to wait when local retry gives up, exceeds its cap, or
|
|
1562
|
-
// is disabled
|
|
1562
|
+
// is disabled. The `~` marks the delay as approximate; the shared
|
|
1563
|
+
// parser accepts it once after Retry-After, so the outer client cooldown
|
|
1564
|
+
// reads this same delay back from the message.
|
|
1563
1565
|
throw new CloudChatError(
|
|
1564
1566
|
`Cognition chat failed${trailerError.code ? ` (${trailerError.code})` : ''} ` +
|
|
1565
1567
|
`(cloud trace ID: ${trailerError.traceId ?? 'n/a'})` +
|
|
@@ -604,6 +604,28 @@ function sanitizeProperties(
|
|
|
604
604
|
return properties;
|
|
605
605
|
}
|
|
606
606
|
|
|
607
|
+
/**
|
|
608
|
+
* Gemini rejects an array declaration that carries no `items` (#5689), so no return from
|
|
609
|
+
* `sanitizeSchema` may leave an array incomplete. A string item keeps the declaration valid: it
|
|
610
|
+
* narrows an unconstrained item rather than widening a constraint, so the loss report, which counts
|
|
611
|
+
* widened or dropped constraints, does not record it. The synthesized node is part of the emitted
|
|
612
|
+
* tree and charges the node budget like any other, because a wide enough fan-out of `items`-less
|
|
613
|
+
* arrays otherwise pushed the output past MAX_SCHEMA_NODES. When the budget cannot pay for that
|
|
614
|
+
* item, the array is omitted (`BUDGET_EXHAUSTED`) for its caller to drop instead of being emitted
|
|
615
|
+
* bare, which Gemini would reject for the whole request. A parent whose own `items` came back
|
|
616
|
+
* exhausted reaches this same rule, so an incomplete array is never nested in a retained one.
|
|
617
|
+
*/
|
|
618
|
+
function completeArrayItems(out: Schema, state: SanitizeState): SanitizeResult {
|
|
619
|
+
if (out.type !== "array" || Object.hasOwn(out, "items")) return out;
|
|
620
|
+
if (state.remainingNodes <= 0) {
|
|
621
|
+
reportBudgetExhausted(state);
|
|
622
|
+
return BUDGET_EXHAUSTED;
|
|
623
|
+
}
|
|
624
|
+
state.remainingNodes -= 1;
|
|
625
|
+
out.items = { type: "string" };
|
|
626
|
+
return out;
|
|
627
|
+
}
|
|
628
|
+
|
|
607
629
|
function sanitizeSchema(
|
|
608
630
|
node: unknown,
|
|
609
631
|
defs: Map<string, unknown>,
|
|
@@ -725,7 +747,8 @@ function sanitizeSchema(
|
|
|
725
747
|
|
|
726
748
|
if (state.remainingNodes <= 0) {
|
|
727
749
|
if (Object.hasOwn(node, "items") || Object.hasOwn(node, "anyOf")) reportBudgetExhausted(state);
|
|
728
|
-
|
|
750
|
+
// An array that the budget stopped before its `items` traversal takes the same rule.
|
|
751
|
+
return completeArrayItems(out, state);
|
|
729
752
|
}
|
|
730
753
|
|
|
731
754
|
if (Array.isArray(node.items)) {
|
|
@@ -739,7 +762,7 @@ function sanitizeSchema(
|
|
|
739
762
|
|
|
740
763
|
if (state.remainingNodes <= 0) {
|
|
741
764
|
if (Object.hasOwn(node, "anyOf")) reportBudgetExhausted(state);
|
|
742
|
-
return out;
|
|
765
|
+
return completeArrayItems(out, state);
|
|
743
766
|
}
|
|
744
767
|
if (Object.hasOwn(node, "anyOf")) {
|
|
745
768
|
const normalized = normalizeAnyOf(node.anyOf, defs, depth, refDepth, state);
|
|
@@ -753,7 +776,7 @@ function sanitizeSchema(
|
|
|
753
776
|
}
|
|
754
777
|
Object.assign(out, normalized);
|
|
755
778
|
}
|
|
756
|
-
return out;
|
|
779
|
+
return completeArrayItems(out, state);
|
|
757
780
|
}
|
|
758
781
|
|
|
759
782
|
export function sanitizeGeminiToolParametersWithReport(
|