@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
package/gui/dist/index.html
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
} catch (e) {}
|
|
17
17
|
})();
|
|
18
18
|
</script>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
20
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-C5IebErG.js"></script>
|
|
20
|
+
<link rel="stylesheet" crossorigin href="/assets/index-OESInAjC.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
|
23
23
|
<div id="root"></div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Crusoe</title><path d="M12 0L4.583 6.583c-3.23 2.869-3.23 7.965 0 10.834L12 24l7.417-6.583c3.23-2.869 3.23-7.965 0-10.834L12 0z" fill="url(#lobe-icons-crusoe-_R_0_)"></path><defs><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-crusoe-_R_0_" x1="18.919" x2="4.853" y1="5.595" y2="18.301"><stop stop-color="#F4BF45"></stop><stop offset=".35" stop-color="#E48047"></stop><stop offset=".69" stop-color="#C73361"></stop><stop offset="1" stop-color="#A42F5F"></stop></linearGradient></defs></svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 315 315" fill="#000000">
|
|
2
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M159.78 315C71.53 315 0 244.49 0 157.5C0 -18.9499 159.78 0.650075 159.78 0.650075C159.78 87.2201 88.36 157.4 0.2 157.5C149.8 157.64 159.78 315 159.78 315ZM160.52 217.98C160.52 217.98 156.94 161.65 105.04 157.52C120.6 157.34 160.52 151.54 160.52 96.5601C160.52 151.54 200.44 157.34 216 157.52C164.1 161.63 160.52 217.98 160.52 217.98Z"/>
|
|
3
|
+
</svg>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bitkyc08/opencodex",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.59.0",
|
|
4
4
|
"description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./bin/package-main.mjs",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"overrides": {
|
|
87
87
|
"@hono/node-server": "2.1.0",
|
|
88
88
|
"fast-uri": "^3.1.7",
|
|
89
|
-
"hono": "4.13.
|
|
89
|
+
"hono": "4.13.8",
|
|
90
90
|
"ip-address": "^10.4.0",
|
|
91
91
|
"qs": "^6.16.0"
|
|
92
92
|
},
|
package/src/adapters/base.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AdapterEvent, OcxParsedRequest } from "../types";
|
|
2
2
|
import type { TranslatorBudget } from "../lib/translator-budget";
|
|
3
3
|
import type { RequestExecutionBudget } from "../lib/request-execution-budget";
|
|
4
|
-
import type { AttemptRecoveryKind } from "../usage/log";
|
|
4
|
+
import type { AttemptRecoveryKind, AttemptRecoveryWithheld } from "../usage/log";
|
|
5
5
|
import type { AdapterTierMetadata } from "../providers/fastwire";
|
|
6
6
|
|
|
7
7
|
/** Metadata about the caller's incoming request, for auth-forwarding adapters. */
|
|
@@ -168,6 +168,16 @@ export interface AdapterFetchContext {
|
|
|
168
168
|
* to `sendCount` and no regression could assert a count for them (#4546).
|
|
169
169
|
*/
|
|
170
170
|
onPhysicalSend?: (send: { ordinal: number; recovery?: AttemptRecoveryKind }) => void;
|
|
171
|
+
/**
|
|
172
|
+
* Observes a recovery this adapter was ready to make and did not, because the send budget
|
|
173
|
+
* refused the dispatch.
|
|
174
|
+
*
|
|
175
|
+
* Separate from `onPhysicalSend` because nothing was sent: folding it in would inflate
|
|
176
|
+
* `sendCount`, the one number that means "requests this proxy actually made". Without it a
|
|
177
|
+
* log with one send cannot distinguish "no recovery was eligible" from "one was and the
|
|
178
|
+
* budget withheld it", and those need opposite follow-ups (#5044).
|
|
179
|
+
*/
|
|
180
|
+
onRecoveryWithheld?: (withheld: { reason: AttemptRecoveryWithheld }) => void;
|
|
171
181
|
}
|
|
172
182
|
|
|
173
183
|
/**
|
|
@@ -5,10 +5,10 @@ export const CODEBUDDY_SCAFFOLD_ERROR_CODE = "vendor_scaffold_detected";
|
|
|
5
5
|
|
|
6
6
|
// The observed control protocol uses FULLWIDTH VERTICAL LINE (U+FF5C). Detection stays
|
|
7
7
|
// deliberately narrower than the marker spelling: a calls control line must be followed by an
|
|
8
|
-
// invoke line
|
|
8
|
+
// invoke line with a non-empty tool name. That distinguishes an agent scaffold from prose quoting or
|
|
9
9
|
// discussing one tag.
|
|
10
10
|
const DSML_CALLS_LINE = "<||dsml|| calls>";
|
|
11
|
-
const DSML_INVOKE_PREFIX = "<||dsml|| invoke name=\"
|
|
11
|
+
const DSML_INVOKE_PREFIX = "<||dsml|| invoke name=\"";
|
|
12
12
|
|
|
13
13
|
export interface CodeBuddyScaffoldFilterResult {
|
|
14
14
|
/** Bytes released from a suffix withheld by an earlier event on this channel. */
|
|
@@ -39,7 +39,7 @@ function prefixAtEnd(text: string, at: number, expected: string): boolean {
|
|
|
39
39
|
* Control tags are recognized only at column zero and outside fenced Markdown. Inline code,
|
|
40
40
|
* quoted strings, blockquotes, indented source, and prose all add syntax before the tag and are
|
|
41
41
|
* therefore forwarded unchanged. A calls line alone is harmless; refusal requires the observed
|
|
42
|
-
* two-line calls-plus-
|
|
42
|
+
* two-line calls-plus-named-invoke grammar.
|
|
43
43
|
*/
|
|
44
44
|
function scan(
|
|
45
45
|
text: string,
|
|
@@ -77,7 +77,8 @@ function scan(
|
|
|
77
77
|
|
|
78
78
|
if (invokeAt >= 0) {
|
|
79
79
|
const invokeRest = text.slice(invokeAt).toLowerCase();
|
|
80
|
-
|
|
80
|
+
const invokeNameStart = invokeRest[DSML_INVOKE_PREFIX.length];
|
|
81
|
+
if (invokeRest.startsWith(DSML_INVOKE_PREFIX) && invokeNameStart && !/[\s"]/.test(invokeNameStart)) {
|
|
81
82
|
return { safe: text.slice(0, index), held: "", fail: true, fence, lineStart };
|
|
82
83
|
}
|
|
83
84
|
if (invokeRest.length === 0 || DSML_INVOKE_PREFIX.startsWith(invokeRest)) {
|
|
@@ -13,6 +13,8 @@ import { commandCodeReasoningEfforts, refreshCommandCodeReasoningEfforts } from
|
|
|
13
13
|
import { identifyRoutedModel } from "./identity";
|
|
14
14
|
import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge";
|
|
15
15
|
import { parseDataUrl } from "./image";
|
|
16
|
+
import { createAdapterPhysicalSend } from "./physical-send";
|
|
17
|
+
import { SendBudgetExhaustedError } from "../lib/upstream-retry";
|
|
16
18
|
|
|
17
19
|
// Retain the short ids emitted by the first local integration. New requests use the live catalog's
|
|
18
20
|
// provider-native IDs directly; this map is compatibility-only and is not a model fallback list.
|
|
@@ -469,7 +471,7 @@ async function fetchCommandCode(request: AdapterRequest, ctx: AdapterFetchContex
|
|
|
469
471
|
const timer = setTimeout(() => timeout.abort(new DOMException("Timeout elapsed", "TimeoutError")), ctx?.timeoutMs ?? 200_000);
|
|
470
472
|
const callerSignal = ctx?.abortSignal ?? new AbortController().signal;
|
|
471
473
|
try {
|
|
472
|
-
return await
|
|
474
|
+
return await executor(request.url, {
|
|
473
475
|
method: request.method,
|
|
474
476
|
headers: request.headers,
|
|
475
477
|
body: request.body,
|
|
@@ -556,7 +558,8 @@ export function createCommandCodeAdapter(provider: OcxProviderConfig): ProviderA
|
|
|
556
558
|
};
|
|
557
559
|
},
|
|
558
560
|
async fetchResponse(request: AdapterRequest, ctx?: AdapterFetchContext): Promise<Response> {
|
|
559
|
-
const
|
|
561
|
+
const send = createAdapterPhysicalSend(ctx, executor);
|
|
562
|
+
const response = await send({ url: request.url, dispatch: physical => fetchCommandCode(request, ctx, physical) });
|
|
560
563
|
if (response.ok) return response;
|
|
561
564
|
const currentEffort = (() => {
|
|
562
565
|
try { return (JSON.parse(request.body) as { params?: { reasoning_effort?: unknown } }).params?.reasoning_effort; } catch { return undefined; }
|
|
@@ -577,8 +580,14 @@ export function createCommandCodeAdapter(provider: OcxProviderConfig): ProviderA
|
|
|
577
580
|
if (!refreshed || refreshed.includes(currentEffort)) return response;
|
|
578
581
|
const retry = requestWithoutReasoningEffort(request);
|
|
579
582
|
if (!retry) return response;
|
|
580
|
-
try {
|
|
581
|
-
|
|
583
|
+
try {
|
|
584
|
+
return await send({ url: retry.url, sendClass: "repair", recovery: "reasoning-effort-downgrade",
|
|
585
|
+
beforeDispatch: () => { try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ } },
|
|
586
|
+
dispatch: physical => fetchCommandCode(retry, ctx, physical) });
|
|
587
|
+
} catch (error) {
|
|
588
|
+
if (error instanceof SendBudgetExhaustedError) return response;
|
|
589
|
+
throw error;
|
|
590
|
+
}
|
|
582
591
|
},
|
|
583
592
|
async *parseStream(response: Response, budget: TranslatorBudget): AsyncGenerator<AdapterEvent> {
|
|
584
593
|
let sawFinish = false;
|
|
@@ -60,6 +60,8 @@ const CONTEXT_500K = 500 * K;
|
|
|
60
60
|
const CONTEXT_1M = 1_000 * K;
|
|
61
61
|
/** Gemini publishes the exact power-of-two window, not a rounded 1M. */
|
|
62
62
|
const CONTEXT_GEMINI = 1_048_576;
|
|
63
|
+
/** Meta publishes 1,048,576 for both Muse Spark 1.3 tiers (dev.meta.ai/docs/models). */
|
|
64
|
+
const CONTEXT_MUSE = 1_048_576;
|
|
63
65
|
|
|
64
66
|
const FULL = ["low", "medium", "high", "xhigh", "max"] as const;
|
|
65
67
|
const T = "thinking-then-effort" as const;
|
|
@@ -211,6 +213,15 @@ export const CURSOR_CAPABILITIES: Record<string, CursorCapability> = {
|
|
|
211
213
|
defaultVariant: "regular",
|
|
212
214
|
variants: { regular: { levels: ["low", "medium", "high"] } },
|
|
213
215
|
},
|
|
216
|
+
// Seeded from the live GetUsableModels roster attached to #4820, which advertises six
|
|
217
|
+
// muse-spark-1.3 effort variants. The ladder stops at xhigh on purpose: see the matching
|
|
218
|
+
// effort-map entry for why Cursor advertising `-max` is not evidence that it runs.
|
|
219
|
+
"muse-spark-1.3": {
|
|
220
|
+
displayName: "Muse Spark 1.3",
|
|
221
|
+
window: CONTEXT_MUSE,
|
|
222
|
+
defaultVariant: "regular",
|
|
223
|
+
variants: { regular: { levels: ["minimal", "low", "medium", "high", "xhigh"] } },
|
|
224
|
+
},
|
|
214
225
|
"kimi-k3": {
|
|
215
226
|
displayName: "Kimi K3",
|
|
216
227
|
window: CONTEXT_1M,
|
|
@@ -46,6 +46,21 @@ export class CursorStreamTruncatedError extends Error {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
export const CURSOR_INCOMPLETE_TOOL_CALL_MESSAGE_PREFIX =
|
|
50
|
+
"Cursor stream ended with incomplete tool call(s):";
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* True when Cursor ended the stream with a client tool still open. The adapter fail-closes
|
|
54
|
+
* the current turn (no partial `tool_call_start`) and remints the conversation afterwards
|
|
55
|
+
* so the next turn does not resume a session left waiting for `mcpResult`.
|
|
56
|
+
*/
|
|
57
|
+
export function isCursorIncompleteToolCallMessage(value: unknown): boolean {
|
|
58
|
+
const message = typeof value === "string" ? value : errorMessage(value);
|
|
59
|
+
const lower = message.toLowerCase();
|
|
60
|
+
return lower.includes(CURSOR_INCOMPLETE_TOOL_CALL_MESSAGE_PREFIX.toLowerCase())
|
|
61
|
+
|| lower.includes("tool call(s) left incomplete");
|
|
62
|
+
}
|
|
63
|
+
|
|
49
64
|
/**
|
|
50
65
|
* A cancel-shaped stream failure that WE did not request. `cancelCursorRun` is the only place
|
|
51
66
|
* that cancels our own stream, and it sets `expectedClose` first, so a cancel arriving without it
|
|
@@ -24,8 +24,54 @@ const CONTEXT_272K = 272_000;
|
|
|
24
24
|
const CONTEXT_262K = 262_144;
|
|
25
25
|
const CONTEXT_256K = 256_000;
|
|
26
26
|
const CONTEXT_200K = 200_000;
|
|
27
|
+
export const CURSOR_OBSERVED_CONTEXT_WINDOW_MAX_ENTRIES = 2_048;
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Process-local ceilings from `ConversationTokenDetails.maxTokens` on live
|
|
31
|
+
* checkpoints. Each observation belongs to the Cursor identity scope that
|
|
32
|
+
* produced it; plan-gated accounts sharing one proxy must not overwrite each
|
|
33
|
+
* other's overflow prior (senpi `cursor-context-limit`).
|
|
34
|
+
*/
|
|
35
|
+
const observedCursorContextWindows = new Map<string, number>();
|
|
36
|
+
|
|
37
|
+
interface CursorContextWindowOptions {
|
|
38
|
+
identityScope?: string;
|
|
39
|
+
observed?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizeObservedWindowKey(modelId: string, identityScope?: string): string {
|
|
43
|
+
return `${identityScope?.trim() || "local"}\0${modelId.trim().toLowerCase()}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function recordObservedCursorContextWindow(
|
|
47
|
+
modelId: string,
|
|
48
|
+
maxTokens: number | undefined,
|
|
49
|
+
options: Pick<CursorContextWindowOptions, "identityScope"> = {},
|
|
50
|
+
): void {
|
|
51
|
+
if (!modelId.trim()) return;
|
|
52
|
+
if (typeof maxTokens !== "number" || !Number.isFinite(maxTokens) || maxTokens <= 0) return;
|
|
53
|
+
const key = normalizeObservedWindowKey(modelId, options.identityScope);
|
|
54
|
+
observedCursorContextWindows.delete(key);
|
|
55
|
+
observedCursorContextWindows.set(key, Math.floor(maxTokens));
|
|
56
|
+
while (observedCursorContextWindows.size > CURSOR_OBSERVED_CONTEXT_WINDOW_MAX_ENTRIES) {
|
|
57
|
+
const oldest = observedCursorContextWindows.keys().next().value;
|
|
58
|
+
if (oldest === undefined) break;
|
|
59
|
+
observedCursorContextWindows.delete(oldest);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function observedCursorContextWindow(
|
|
64
|
+
modelId: string,
|
|
65
|
+
options: Pick<CursorContextWindowOptions, "identityScope"> = {},
|
|
66
|
+
): number | undefined {
|
|
67
|
+
return observedCursorContextWindows.get(normalizeObservedWindowKey(modelId, options.identityScope));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function resetObservedCursorContextWindowsForTests(): void {
|
|
71
|
+
observedCursorContextWindows.clear();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function inferCursorContextWindowHeuristic(modelId: string): number {
|
|
29
75
|
const id = modelId.trim().toLowerCase();
|
|
30
76
|
if (id.includes("1m")) return CONTEXT_1M;
|
|
31
77
|
if (id.startsWith("gemini-")) return CONTEXT_1M;
|
|
@@ -40,6 +86,24 @@ export function inferCursorContextWindow(modelId: string): number {
|
|
|
40
86
|
return CURSOR_DEFAULT_CONTEXT_WINDOW;
|
|
41
87
|
}
|
|
42
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Infer a conservative context window for a Cursor model id.
|
|
91
|
+
*
|
|
92
|
+
* A positive explicit observation wins, then an identity-scoped process-local
|
|
93
|
+
* checkpoint `maxTokens`, then the id heuristic. Cursor's `AvailableModelsResponse`
|
|
94
|
+
* does not currently include per-model context window metadata.
|
|
95
|
+
*/
|
|
96
|
+
export function inferCursorContextWindow(
|
|
97
|
+
modelId: string,
|
|
98
|
+
options: CursorContextWindowOptions = {},
|
|
99
|
+
): number {
|
|
100
|
+
const { observed } = options;
|
|
101
|
+
if (typeof observed === "number" && Number.isFinite(observed) && observed > 0) {
|
|
102
|
+
return Math.floor(observed);
|
|
103
|
+
}
|
|
104
|
+
return observedCursorContextWindow(modelId, options) ?? inferCursorContextWindowHeuristic(modelId);
|
|
105
|
+
}
|
|
106
|
+
|
|
43
107
|
function normalizeInputModalities(input: string[] | undefined): string[] {
|
|
44
108
|
const values = (input ?? [...CURSOR_DEFAULT_INPUT_MODALITIES])
|
|
45
109
|
.map(item => item.trim())
|
|
@@ -38,8 +38,10 @@ const CURSOR_MODEL_EFFORT_TIERS: Record<string, readonly string[]> = {
|
|
|
38
38
|
"claude-opus-5-fast": ["low", "medium", "high"],
|
|
39
39
|
"claude-sonnet-5": ["low", "medium", "high", "xhigh", "max"],
|
|
40
40
|
"glm-5.2": ["high", "max"],
|
|
41
|
-
// 260825 live GetUsableModels. gemini-3.6-flash
|
|
42
|
-
// listing
|
|
41
|
+
// 260825 live GetUsableModels. gemini-3.6-flash was the first Cursor model exposing
|
|
42
|
+
// `minimal`; listing a rung here is also what admits the suffix into
|
|
43
|
+
// CANONICAL_EFFORT_SUFFIXES below. muse-spark-1.3 now carries it too, so `minimal` no
|
|
44
|
+
// longer depends on this single row.
|
|
43
45
|
"gemini-3.6-flash": ["minimal", "low", "medium", "high"],
|
|
44
46
|
"gemini-3.7-flash": ["low", "medium", "high"],
|
|
45
47
|
// 260903 preemptive: gemini-3.8-flash seeded ahead of Cursor's lineup update, the same way
|
|
@@ -91,6 +93,18 @@ const CURSOR_MODEL_EFFORT_TIERS: Record<string, readonly string[]> = {
|
|
|
91
93
|
"gpt-5.6-sol": ["low", "medium", "high", "xhigh", "max"],
|
|
92
94
|
"gpt-5.6-terra": ["low", "medium", "high", "xhigh", "max"],
|
|
93
95
|
"gpt-5.6-luna": ["low", "medium", "high", "xhigh", "max"],
|
|
96
|
+
// 260916 live GetUsableModels (#4820) advertises muse-spark-1.3 at minimal, low, medium,
|
|
97
|
+
// high, xhigh AND max. The seed stops at xhigh deliberately.
|
|
98
|
+
//
|
|
99
|
+
// Meta publishes minimal..xhigh for Muse Spark and lists no `max` at all
|
|
100
|
+
// (dev.meta.ai/docs/reasoning), and an independent OpenCode Zen probe of
|
|
101
|
+
// muse-spark-1.3-contributor-free rejected max with `unknown variant` — both already
|
|
102
|
+
// recorded on META_MUSE_REASONING_EFFORTS in src/providers/registry/model-seeds.ts.
|
|
103
|
+
// Cursor advertising a wire id is not evidence that Run accepts it; that is exactly the
|
|
104
|
+
// advertised-but-not-callable shape CURSOR_KNOWN_UNCALLABLE_MODEL_IDS was created for.
|
|
105
|
+
// Publishing the rung anyway would invent a capability on two sources' contrary evidence.
|
|
106
|
+
// Add `max` here once a Cursor Run at max is observed to succeed.
|
|
107
|
+
"muse-spark-1.3": ["minimal", "low", "medium", "high", "xhigh"],
|
|
94
108
|
};
|
|
95
109
|
|
|
96
110
|
/** All effort suffixes accepted when matching live Cursor model ids to configured base ids. */
|
|
@@ -13,6 +13,58 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
const ECHO_MARKERS = ["[Tool Result]", "[Tool Error]", "[tool_result]"] as const;
|
|
16
|
+
|
|
17
|
+
function isEchoMarkerLine(line: string): boolean {
|
|
18
|
+
return (ECHO_MARKERS as readonly string[]).includes(line.replace(/^[ \t]+/, ""));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Drop echoed tool-result envelopes from assistant history before Cursor root replay.
|
|
23
|
+
*
|
|
24
|
+
* The prefix sniffer catches an echo that STARTS a turn, but grok-4.6 routinely writes a real
|
|
25
|
+
* sentence first and pastes the envelope after it. That text has already reached the client and
|
|
26
|
+
* is stored as assistant output, so replaying it verbatim re-primes the next turn with the very
|
|
27
|
+
* envelope the model is copying.
|
|
28
|
+
*
|
|
29
|
+
* Scope starts AT the marker line and runs to the next blank line, rather than to the end of
|
|
30
|
+
* the message. The echoed envelope has no terminator we can recognise — we build it as a marker
|
|
31
|
+
* line plus arbitrary result text (protobuf-request.ts), and the observed copies are not
|
|
32
|
+
* byte-exact, so matching against the replayed envelope is not available either. Truncating to
|
|
33
|
+
* the end of the message was the alternative, and it discards a genuine answer whenever the
|
|
34
|
+
* model resumes after the echo. A blank line is the one boundary the model reliably writes when
|
|
35
|
+
* it goes back to prose.
|
|
36
|
+
*
|
|
37
|
+
* The tradeoff is explicit: an echoed envelope whose pasted result itself contains a blank line
|
|
38
|
+
* leaves its remainder in replay. That is the safer direction to be wrong in — conversation
|
|
39
|
+
* remint, not this filter, is the primary defence against a poisoned conversation, and this only
|
|
40
|
+
* stops the transcript from feeding itself.
|
|
41
|
+
*
|
|
42
|
+
* Only whole-line markers count, so prose such as "the string [Tool Result] appeared" survives.
|
|
43
|
+
*/
|
|
44
|
+
export function stripAssistantEchoedToolEnvelope(text: string): string {
|
|
45
|
+
if (!text || !ECHO_MARKERS.some(marker => text.includes(marker))) return text;
|
|
46
|
+
const newline = text.includes("\r\n") ? "\r\n" : "\n";
|
|
47
|
+
const lines = text.split(/\r?\n/);
|
|
48
|
+
const kept: string[] = [];
|
|
49
|
+
let dropped = false;
|
|
50
|
+
let index = 0;
|
|
51
|
+
while (index < lines.length) {
|
|
52
|
+
const line = lines[index] ?? "";
|
|
53
|
+
if (!isEchoMarkerLine(line)) {
|
|
54
|
+
kept.push(line);
|
|
55
|
+
index += 1;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
dropped = true;
|
|
59
|
+
index += 1;
|
|
60
|
+
// The envelope body is the contiguous non-blank run after the marker. The blank line that
|
|
61
|
+
// ends it is left in place, so surviving prose on either side stays separated.
|
|
62
|
+
while (index < lines.length && (lines[index] ?? "").trim() !== "") index += 1;
|
|
63
|
+
}
|
|
64
|
+
if (!dropped) return text;
|
|
65
|
+
return kept.join(newline).trimEnd();
|
|
66
|
+
}
|
|
67
|
+
|
|
16
68
|
const MAX_SNIFF_BYTES = 40;
|
|
17
69
|
/** Mid-stream observer: max leading whitespace on a line before matching disarms. */
|
|
18
70
|
const MAX_MIDSTREAM_LINE_INDENT = 128;
|
|
@@ -66,8 +118,9 @@ export interface MidstreamEchoFinding {
|
|
|
66
118
|
* MIDDLE of an agent message — after legitimate leading text — one of them
|
|
67
119
|
* carrying a whitespace-spliced call-id ("fc_x mar-y" instead of "fc_x-y").
|
|
68
120
|
* Deltas at that point have already reached the client, so this observer
|
|
69
|
-
* never throws and never withholds output
|
|
70
|
-
*
|
|
121
|
+
* never throws and never withholds output. It records findings so the adapter
|
|
122
|
+
* can emit a structured diagnostic and remint the conversation for the next
|
|
123
|
+
* turn at turn end. Only fixed marker
|
|
71
124
|
* enums, numeric offsets, and corruption booleans are retained — never
|
|
72
125
|
* content bytes.
|
|
73
126
|
*/
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
createCursorContextUsageTracker,
|
|
21
21
|
createCursorProtobufEventState,
|
|
22
22
|
finalizeTurnEvents,
|
|
23
|
+
hasBufferedTextToolCalls,
|
|
23
24
|
mapCursorProtobufServerMessage,
|
|
24
25
|
mapSyntheticMcpExecToToolEvents,
|
|
25
26
|
reportableContextTokens,
|
|
@@ -732,6 +733,8 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
732
733
|
syntheticStructuredEditToolNames,
|
|
733
734
|
translatorBudget: this.translatorBudget,
|
|
734
735
|
contextUsage,
|
|
736
|
+
wireModelId: request.modelId,
|
|
737
|
+
identityScope: request._cursorIdentityScope,
|
|
735
738
|
...(prepared.estimatedInputTokens !== undefined
|
|
736
739
|
? { estimatedInputTokens: prepared.estimatedInputTokens }
|
|
737
740
|
: {}),
|
|
@@ -1265,6 +1268,7 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
1265
1268
|
state.openToolCalls.size > 0
|
|
1266
1269
|
|| this.sawAssistantText
|
|
1267
1270
|
|| hasPendingClientToolFinalization
|
|
1271
|
+
|| hasBufferedTextToolCalls(state)
|
|
1268
1272
|
)
|
|
1269
1273
|
) {
|
|
1270
1274
|
const terminal = hasPendingClientToolFinalization && state.openToolCalls.size === 0
|
|
@@ -1431,7 +1435,7 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
1431
1435
|
settler.settleFinish();
|
|
1432
1436
|
return;
|
|
1433
1437
|
}
|
|
1434
|
-
if (this.framesReceived > 0 && this.sawAssistantText) {
|
|
1438
|
+
if (this.framesReceived > 0 && (this.sawAssistantText || hasBufferedTextToolCalls(state))) {
|
|
1435
1439
|
for (const event of finalizeTurnEvents(state)) push(event);
|
|
1436
1440
|
releaseBacklogLease();
|
|
1437
1441
|
settler.settleFinish();
|
|
@@ -46,7 +46,8 @@ export function mapCursorServerMessage(
|
|
|
46
46
|
state.writeClient(cursorExecResult(message.requestId, message.execCase));
|
|
47
47
|
return [];
|
|
48
48
|
case "local_side_effect":
|
|
49
|
-
// Internal retry-safety signal only; keep the bridge alive without producing protocol output
|
|
50
|
-
|
|
49
|
+
// Internal retry-safety signal only; keep the bridge alive without producing protocol output,
|
|
50
|
+
// while preventing server-level failover from replaying the completed local operation.
|
|
51
|
+
return [{ type: "heartbeat", replayUnsafe: true }];
|
|
51
52
|
}
|
|
52
53
|
}
|
|
@@ -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
|
}
|