@bitkyc08/opencodex 2.65.0 → 2.66.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 +14 -0
- package/gui/dist/assets/App-BVjA0T6O.js +51 -0
- package/gui/dist/assets/App-gkoJIEOV.css +1 -0
- package/gui/dist/assets/{Tray-D4bA1vff.js → Tray-BCFtiWwD.js} +1 -1
- package/gui/dist/assets/index-ComVsdSk.css +1 -0
- package/gui/dist/assets/index-vO4dgfiU.js +86 -0
- package/gui/dist/assets/{tray-data-BZXhB3qz.js → tray-data-C4FiNaxk.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -2
- package/src/adapters/anthropic/beta-allowlist.ts +80 -0
- package/src/adapters/anthropic/passthrough.ts +221 -0
- package/src/adapters/anthropic-image-codec.ts +56 -19
- package/src/adapters/anthropic-image-normalize.ts +20 -10
- package/src/adapters/anthropic.ts +55 -24
- package/src/adapters/coding-agent/protocol.ts +29 -19
- package/src/adapters/cursor/live-transport.ts +18 -3
- package/src/adapters/cursor/native-exec-shell.ts +18 -63
- package/src/adapters/cursor/native-exec.ts +5 -2
- package/src/adapters/cursor/native-foreground-shell.ts +44 -0
- package/src/adapters/devin/cloud-direct/chat.ts +7 -1
- package/src/adapters/exec-tool-result-normalize.ts +4 -1
- package/src/adapters/google.ts +12 -0
- package/src/adapters/ollama-native.ts +32 -1
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +131 -34
- package/src/adapters/openai-chat.ts +6 -4
- package/src/adapters/openai-responses/passthrough.ts +1 -0
- package/src/adapters/openai-responses/reasoning.ts +45 -1
- package/src/bridge/response-json.ts +10 -2
- package/src/chat/outbound.ts +104 -52
- package/src/claude/claude-code-block.ts +16 -0
- package/src/claude/context-windows.ts +52 -4
- package/src/claude/desktop-first-party.ts +25 -8
- package/src/claude/inbound.ts +22 -0
- package/src/claude/intercept/connect-proxy.ts +37 -4
- package/src/claude/intercept/proxy-auth.ts +166 -0
- package/src/claude/intercept/runtime.ts +21 -0
- package/src/claude/intercept/settings.ts +33 -12
- package/src/claude/outbound.ts +45 -27
- package/src/cli/account-extended.ts +47 -15
- package/src/cli/account-target.ts +115 -0
- package/src/cli/account.ts +20 -28
- package/src/cli/agent.ts +10 -1
- package/src/cli/api-protocols.ts +206 -0
- package/src/cli/capabilities.ts +86 -0
- package/src/cli/combo.ts +2 -2
- package/src/cli/connect.ts +127 -3
- package/src/cli/dispatch.ts +8 -0
- package/src/cli/doctor.ts +33 -0
- package/src/cli/help.ts +2 -0
- package/src/cli/link.ts +288 -0
- package/src/cli/registry.ts +23 -0
- package/src/cli/runtime-api.ts +79 -0
- package/src/cli/system-command.ts +21 -2
- package/src/client/connect.ts +125 -23
- package/src/client/hub-relay.ts +12 -8
- package/src/client/link-join.ts +323 -0
- package/src/client/link-relay.ts +241 -0
- package/src/client/link-state.ts +110 -0
- package/src/client/link-teardown.ts +63 -0
- package/src/client/link-tunnel.ts +465 -0
- package/src/client/machine-listener.ts +14 -5
- package/src/client/runtime.ts +91 -42
- package/src/client/state.ts +4 -0
- package/src/codex/catalog/build-entries.ts +4 -0
- package/src/codex/catalog/derive-entry.ts +5 -0
- package/src/codex/catalog/parsing.ts +4 -0
- package/src/codex/catalog/retained-sync.ts +4 -1
- package/src/codex/desktop-switches.ts +88 -8
- package/src/codex/inject/plan.ts +16 -3
- package/src/codex/inject.ts +3 -0
- package/src/codex/internal/catalog-writer.ts +13 -2
- package/src/combos/failover.ts +3 -2
- package/src/combos/index.ts +12 -0
- package/src/combos/jev.ts +646 -0
- package/src/combos/request.ts +5 -0
- package/src/combos/resolve.ts +17 -15
- package/src/combos/types.ts +47 -3
- package/src/config/atomic-write.ts +3 -0
- package/src/config/live-reconcile.ts +20 -4
- package/src/config/persist-unlocked.ts +20 -7
- package/src/config/schema/config-schema.ts +16 -0
- package/src/config/schema/leaf-validators.ts +38 -1
- package/src/generated/compatibility-version.json +416 -128
- package/src/integrations/omp-yaml-source.ts +109 -4
- package/src/lab/automation/orchestrator.ts +60 -4
- package/src/lab/events/limits.ts +1 -1
- package/src/lib/bounded-body.ts +55 -38
- package/src/lib/config-ownership.ts +11 -22
- package/src/lib/local-desktop-snapshot-capability.ts +56 -0
- package/src/lib/local-management-capability.ts +25 -3
- package/src/lib/optional-shutdown-hooks.ts +17 -0
- package/src/lib/state-store-registrations.ts +2 -2
- package/src/lib/windows-elevation.ts +62 -25
- package/src/lib/windows-secret-acl.ts +114 -23
- package/src/link/admission-wait.ts +73 -0
- package/src/link/compensation.ts +100 -0
- package/src/link/fingerprint.ts +21 -0
- package/src/link/paths.ts +19 -0
- package/src/link/ports.ts +12 -0
- package/src/link/routes.ts +32 -0
- package/src/link/ssh-argv.ts +170 -0
- package/src/link/ssh-config.ts +152 -0
- package/src/link/ssh-runner.ts +161 -0
- package/src/link/status-projection.ts +96 -0
- package/src/link/store.ts +139 -0
- package/src/link/supervisor.ts +404 -0
- package/src/link/tunnel-state.ts +91 -0
- package/src/protocols/baseline.ts +88 -0
- package/src/protocols/codecs/chat.ts +17 -0
- package/src/protocols/codecs/messages.ts +18 -0
- package/src/protocols/codecs/responses.ts +17 -0
- package/src/protocols/contract.ts +185 -0
- package/src/protocols/dto.ts +267 -0
- package/src/protocols/encoders/adapter-events.ts +876 -0
- package/src/protocols/encoders/chat.ts +243 -0
- package/src/protocols/encoders/messages.ts +462 -0
- package/src/protocols/envelope.ts +43 -0
- package/src/protocols/features.ts +295 -0
- package/src/protocols/guard.ts +33 -0
- package/src/protocols/opaque-state.ts +132 -0
- package/src/protocols/path.ts +39 -0
- package/src/protocols/plan-snapshot.ts +226 -0
- package/src/protocols/plan.ts +192 -0
- package/src/protocols/provider-summary.ts +101 -0
- package/src/protocols/settings.ts +118 -0
- package/src/protocols/shadow-plan.ts +36 -0
- package/src/protocols/shadow.ts +62 -0
- package/src/protocols/trace.ts +335 -0
- package/src/providers/model-rename-migration.ts +3 -3
- package/src/providers/registry/entries-core.ts +6 -1
- package/src/providers/registry/entries-extended.ts +15 -0
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/types.ts +6 -0
- package/src/providers/registry.ts +1 -1
- package/src/remote-control/workspace-hub.ts +5 -4
- package/src/responses/apply-patch-envelope.ts +6 -1
- package/src/responses/code-mode-shell-input.ts +4 -2
- package/src/responses/freeform-wrapper-scan.ts +51 -24
- package/src/responses/progressive-freeform-input.ts +14 -20
- package/src/responses/spill-store.ts +187 -33
- package/src/responses/state/snapshot-select.ts +49 -0
- package/src/responses/state/spill-inspect.ts +46 -0
- package/src/responses/state/spill-queue.ts +16 -0
- package/src/responses/state.ts +51 -27
- package/src/server/audio-client.ts +8 -3
- package/src/server/audio-upstream.ts +9 -4
- package/src/server/auth-cors.ts +25 -8
- package/src/server/chat-completions.ts +71 -11
- package/src/server/chat-native-eligibility.ts +74 -0
- package/src/server/chat-native.ts +139 -74
- package/src/server/claude-messages.ts +140 -22
- package/src/server/hub-usage.ts +4 -3
- package/src/server/index/link-listener.ts +195 -0
- package/src/server/index/optional-listeners.ts +115 -0
- package/src/server/index/serve-options.ts +44 -10
- package/src/server/index.ts +10 -9
- package/src/server/inference/attempt.ts +51 -0
- package/src/server/inference/client-encoder-delivery.ts +235 -0
- package/src/server/inference/client-wire-log.ts +49 -0
- package/src/server/inference/client-wire.ts +69 -0
- package/src/server/inference/context.ts +15 -0
- package/src/server/inference/final-log.ts +37 -0
- package/src/server/local-desktop-snapshot-auth.ts +57 -0
- package/src/server/management/agent-settings-routes.ts +15 -13
- package/src/server/management/api-access.ts +11 -1
- package/src/server/management/config-routes.ts +3 -6
- package/src/server/management/context.ts +25 -0
- package/src/server/management/link-routes.ts +535 -0
- package/src/server/management/logs-usage-routes.ts +31 -1
- package/src/server/management/native-integration-routes.ts +6 -11
- package/src/server/management/oauth-account-routes.ts +42 -16
- package/src/server/management/protocol-routes.ts +159 -0
- package/src/server/management/protocol-settings-patch.ts +145 -0
- package/src/server/management/provider-patch-transaction.ts +61 -0
- package/src/server/management/provider-routes.ts +39 -18
- package/src/server/management/route-registry.ts +12 -0
- package/src/server/management/sidebar-routes.ts +8 -3
- package/src/server/management/system-routes.ts +18 -1
- package/src/server/management/usage-aggregate-cache.ts +206 -6
- package/src/server/management-api.ts +26 -1
- package/src/server/management-auth.ts +19 -5
- package/src/server/messages-native-eligibility.ts +161 -0
- package/src/server/messages-native-oauth.ts +149 -0
- package/src/server/messages-native.ts +790 -0
- package/src/server/relay.ts +9 -0
- package/src/server/request-log-filter.ts +92 -0
- package/src/server/request-log.ts +31 -70
- package/src/server/responses/adapter-delivery.ts +43 -16
- package/src/server/responses/agent-task-recovery.ts +90 -22
- package/src/server/responses/codex-ws-exchange.ts +4 -0
- package/src/server/responses/codex-ws-wire.ts +10 -1
- package/src/server/responses/core-combo-native.ts +323 -0
- package/src/server/responses/core-combo.ts +224 -13
- package/src/server/responses/core-options.ts +22 -0
- package/src/server/responses/core.ts +14 -26
- package/src/server/responses/request-sidecar-auth.ts +8 -3
- package/src/server/responses/run-turn-execution.ts +257 -63
- package/src/server/responses/sidecar-execution.ts +7 -4
- package/src/server/system-env.ts +7 -0
- package/src/service/diagnostics.ts +7 -1
- package/src/service/launchd.ts +26 -37
- package/src/service/windows-ops.ts +24 -20
- package/src/types/config.ts +58 -1
- package/src/types.ts +2 -0
- package/src/update/transactional-install.d.mts +0 -1
- package/src/update/transactional-install.mjs +17 -17
- package/src/usage/jev-stats.ts +495 -0
- package/src/usage/log.ts +24 -3
- package/src/web-search/run-turn-loop.ts +568 -0
- package/gui/dist/assets/App-8NMiZxT0.css +0 -1
- package/gui/dist/assets/App-DGLte6IR.js +0 -50
- package/gui/dist/assets/index--EWgGQvZ.css +0 -1
- package/gui/dist/assets/index-Bi1K37Wl.js +0 -86
|
@@ -523,6 +523,54 @@ function anthropicKeyUsesBearer(provider: OcxProviderConfig): boolean {
|
|
|
523
523
|
return provider.apiKeyTransport === "bearer";
|
|
524
524
|
}
|
|
525
525
|
|
|
526
|
+
/** The `anthropic-version` every Messages request from this proxy pins. */
|
|
527
|
+
export const ANTHROPIC_API_VERSION = "2023-06-01";
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* The fixed headers of every Messages request this proxy builds, before credentials. Shared by
|
|
531
|
+
* the adapter and the managed native lane so both pin the same version and client identity.
|
|
532
|
+
*/
|
|
533
|
+
export function anthropicBaseRequestHeaders(stream: boolean | undefined): Record<string, string> {
|
|
534
|
+
return {
|
|
535
|
+
"Content-Type": "application/json",
|
|
536
|
+
"anthropic-version": ANTHROPIC_API_VERSION,
|
|
537
|
+
"Accept": stream ? "text/event-stream" : "application/json",
|
|
538
|
+
"User-Agent": "@anthropic-ai/sdk/0.74.0",
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/** Key-auth credential placement: `x-api-key`, or a bearer when the provider asks for one. */
|
|
543
|
+
export function applyAnthropicKeyAuth(headers: Record<string, string>, provider: OcxProviderConfig): void {
|
|
544
|
+
if (typeof provider.apiKey !== "string") return;
|
|
545
|
+
if (anthropicKeyUsesBearer(provider)) headers["Authorization"] = `Bearer ${provider.apiKey}`;
|
|
546
|
+
else headers["x-api-key"] = provider.apiKey;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* OAuth (Claude Pro/Max) credential placement: the bearer, the OAuth beta pair and the Claude
|
|
551
|
+
* Code client fingerprint. Shared by the adapter and the managed native lane.
|
|
552
|
+
*/
|
|
553
|
+
export function applyAnthropicOAuthAuth(headers: Record<string, string>, accessToken: string): void {
|
|
554
|
+
headers["Authorization"] = `Bearer ${accessToken}`;
|
|
555
|
+
headers["anthropic-beta"] = ANTHROPIC_OAUTH_BETA;
|
|
556
|
+
// Match the real Claude Code CLI request fingerprint: a valid OAuth token with an empty
|
|
557
|
+
// header set is a non-first-party signature. (cch billing-header signing is intentionally
|
|
558
|
+
// out of scope — brittle and version-coupled.)
|
|
559
|
+
Object.assign(headers, CLAUDE_CODE_HEADERS);
|
|
560
|
+
headers["X-Claude-Code-Session-Id"] = claudeCodeSessionId(accessToken);
|
|
561
|
+
headers["x-client-request-id"] = crypto.randomUUID();
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/** The provider's Messages endpoint, refusing a base URL with an unresolved `{placeholder}`. */
|
|
565
|
+
export function resolveAnthropicMessagesUrl(provider: Pick<OcxProviderConfig, "baseUrl">): string {
|
|
566
|
+
const url = anthropicMessagesUrl(provider.baseUrl);
|
|
567
|
+
const unresolvedPlaceholder = url.match(/\{[^}]*\}/)?.[0];
|
|
568
|
+
if (unresolvedPlaceholder) {
|
|
569
|
+
throw new Error(`anthropic baseUrl contains unresolved ${unresolvedPlaceholder}`);
|
|
570
|
+
}
|
|
571
|
+
return url;
|
|
572
|
+
}
|
|
573
|
+
|
|
526
574
|
/** Map a Responses reasoning effort to an Anthropic extended-thinking budget (tokens, >= 1024). */
|
|
527
575
|
function reasoningBudget(effort: string): number {
|
|
528
576
|
switch (effort) {
|
|
@@ -802,6 +850,9 @@ function messagesToAnthropicFormat(
|
|
|
802
850
|
if (text) preface.push({ type: "text", text });
|
|
803
851
|
} else if (part.type === "thinking") {
|
|
804
852
|
const t = part as OcxThinkingContent;
|
|
853
|
+
// History minted under another serving identity (or already rejected as opaque) is not
|
|
854
|
+
// this destination's to verify: drop its opaque blocks, as the Responses passthrough does.
|
|
855
|
+
if (parsed._stripReasoningEncryptedContent === true) continue;
|
|
805
856
|
// Redacted blocks replay verbatim FIRST (they preceded the visible thinking block
|
|
806
857
|
// in the original stream order preserved by the bridge envelope).
|
|
807
858
|
for (const data of t.redacted ?? []) {
|
|
@@ -1129,35 +1180,15 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
1129
1180
|
body.tool_choice = { ...settledToolChoice, disable_parallel_tool_use: true };
|
|
1130
1181
|
}
|
|
1131
1182
|
|
|
1132
|
-
const url =
|
|
1133
|
-
const unresolvedPlaceholder = url.match(/\{[^}]*\}/)?.[0];
|
|
1134
|
-
if (unresolvedPlaceholder) {
|
|
1135
|
-
throw new Error(`anthropic baseUrl contains unresolved ${unresolvedPlaceholder}`);
|
|
1136
|
-
}
|
|
1183
|
+
const url = resolveAnthropicMessagesUrl(provider);
|
|
1137
1184
|
// Anthropic fast mode: `speed` is only accepted beside its beta; without it the API
|
|
1138
1185
|
// answers 400 "speed: Extra inputs are not permitted". The beta is merged below, after
|
|
1139
1186
|
// any header override, so a request never carries one without the other.
|
|
1140
1187
|
const fastSpeed = anthropicFastSpeed(parsed, provider);
|
|
1141
1188
|
if (fastSpeed) body.speed = fastSpeed.value;
|
|
1142
|
-
const headers
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
"Accept": parsed.stream ? "text/event-stream" : "application/json",
|
|
1146
|
-
"User-Agent": "@anthropic-ai/sdk/0.74.0",
|
|
1147
|
-
};
|
|
1148
|
-
if (isOAuth) {
|
|
1149
|
-
headers["Authorization"] = `Bearer ${provider.apiKey}`;
|
|
1150
|
-
headers["anthropic-beta"] = ANTHROPIC_OAUTH_BETA;
|
|
1151
|
-
// Match the real Claude Code CLI request fingerprint: a valid OAuth token with an empty
|
|
1152
|
-
// header set is a non-first-party signature. (cch billing-header signing is intentionally
|
|
1153
|
-
// out of scope — brittle and version-coupled.)
|
|
1154
|
-
Object.assign(headers, CLAUDE_CODE_HEADERS);
|
|
1155
|
-
headers["X-Claude-Code-Session-Id"] = claudeCodeSessionId(provider.apiKey);
|
|
1156
|
-
headers["x-client-request-id"] = crypto.randomUUID();
|
|
1157
|
-
} else {
|
|
1158
|
-
if (anthropicKeyUsesBearer(provider)) headers["Authorization"] = `Bearer ${provider.apiKey}`;
|
|
1159
|
-
else headers["x-api-key"] = provider.apiKey;
|
|
1160
|
-
}
|
|
1189
|
+
const headers = anthropicBaseRequestHeaders(parsed.stream);
|
|
1190
|
+
if (isOAuth) applyAnthropicOAuthAuth(headers, provider.apiKey);
|
|
1191
|
+
else applyAnthropicKeyAuth(headers, provider);
|
|
1161
1192
|
if (provider.headers) Object.assign(headers, provider.headers);
|
|
1162
1193
|
mergeAnthropicBetaHeader(headers, fastSpeed?.betas ?? []);
|
|
1163
1194
|
|
|
@@ -80,14 +80,11 @@ export async function* readJsonLines(
|
|
|
80
80
|
const maxLineBytes = limits.maxLineBytes ?? MAX_STREAM_LINE_BYTES;
|
|
81
81
|
const maxTotalBytes = limits.maxTotalBytes ?? MAX_STREAM_TOTAL_BYTES;
|
|
82
82
|
const decoder = new TextDecoder();
|
|
83
|
-
|
|
84
|
-
let
|
|
83
|
+
let parts: string[] = [];
|
|
84
|
+
let lineBytes = 0;
|
|
85
85
|
let totalBytes = 0;
|
|
86
86
|
|
|
87
87
|
const flushLine = function* (line: string): Generator<StreamMessage> {
|
|
88
|
-
if (encoder.encode(line).byteLength > maxLineBytes) {
|
|
89
|
-
throw new CodingAgentStreamLimitError("Coding-agent stream line exceeded the byte ceiling");
|
|
90
|
-
}
|
|
91
88
|
const trimmed = line.trim();
|
|
92
89
|
if (!trimmed) return; // Blank lines and whitespace-only lines are ignored as padding.
|
|
93
90
|
let parsed: unknown;
|
|
@@ -108,26 +105,39 @@ export async function* readJsonLines(
|
|
|
108
105
|
yield parsed as StreamMessage;
|
|
109
106
|
};
|
|
110
107
|
|
|
108
|
+
// Decode continuously for split UTF-8/BOM semantics, but search and measure only new
|
|
109
|
+
// decoded segments. Joining once per frame avoids repeatedly flattening a growing rope.
|
|
110
|
+
const consume = function* (text: string): Generator<StreamMessage> {
|
|
111
|
+
let start = 0;
|
|
112
|
+
while (start < text.length) {
|
|
113
|
+
const newline = text.indexOf("\n", start);
|
|
114
|
+
const end = newline < 0 ? text.length : newline;
|
|
115
|
+
const part = text.slice(start, end);
|
|
116
|
+
lineBytes += Buffer.byteLength(part);
|
|
117
|
+
if (lineBytes > maxLineBytes) {
|
|
118
|
+
throw new CodingAgentStreamLimitError("Coding-agent stream line exceeded the byte ceiling");
|
|
119
|
+
}
|
|
120
|
+
if (part) parts.push(part);
|
|
121
|
+
if (newline < 0) break;
|
|
122
|
+
const line = parts.join("");
|
|
123
|
+
parts = [];
|
|
124
|
+
lineBytes = 0;
|
|
125
|
+
yield* flushLine(line);
|
|
126
|
+
start = newline + 1;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
111
130
|
for await (const chunk of chunks) {
|
|
112
131
|
totalBytes += chunk.byteLength;
|
|
113
132
|
if (totalBytes > maxTotalBytes) {
|
|
114
133
|
throw new CodingAgentStreamLimitError("Coding-agent stream exceeded the total byte ceiling");
|
|
115
134
|
}
|
|
116
|
-
|
|
117
|
-
let newline = buffer.indexOf("\n");
|
|
118
|
-
while (newline >= 0) {
|
|
119
|
-
const line = buffer.slice(0, newline);
|
|
120
|
-
buffer = buffer.slice(newline + 1);
|
|
121
|
-
yield* flushLine(line);
|
|
122
|
-
newline = buffer.indexOf("\n");
|
|
123
|
-
}
|
|
124
|
-
if (encoder.encode(buffer).byteLength > maxLineBytes) {
|
|
125
|
-
throw new CodingAgentStreamLimitError("Coding-agent stream line exceeded the byte ceiling");
|
|
126
|
-
}
|
|
135
|
+
yield* consume(decoder.decode(chunk, { stream: true }));
|
|
127
136
|
}
|
|
128
|
-
// Flush
|
|
129
|
-
|
|
130
|
-
|
|
137
|
+
// Flush incomplete UTF-8 through the same decoded-byte accounting before parsing EOF.
|
|
138
|
+
yield* consume(decoder.decode());
|
|
139
|
+
const finalLine = parts.join("");
|
|
140
|
+
if (finalLine.trim()) yield* flushLine(finalLine);
|
|
131
141
|
}
|
|
132
142
|
|
|
133
143
|
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CursorForegroundShellOwner } from "./native-foreground-shell";
|
|
1
2
|
import http2 from "node:http2";
|
|
2
3
|
import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
|
|
3
4
|
import { namespacedToolName, type OcxProviderConfig, type OcxUsage } from "../../types";
|
|
@@ -531,6 +532,8 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
531
532
|
private mcpPrepared?: Promise<void>;
|
|
532
533
|
private releaseMcpObservation?: () => void;
|
|
533
534
|
private blobRequestScope?: CursorBlobRequestScopeToken;
|
|
535
|
+
private readonly foregroundShellOwner = new CursorForegroundShellOwner();
|
|
536
|
+
private requestSignal?: AbortSignal;
|
|
534
537
|
private shellCleanup?: Promise<BackgroundShellTerminationReport>;
|
|
535
538
|
// Per-turn diagnostic counters/timestamps when provider debug is on (`ocx debug provider on`). Stamped in open(), cleared on
|
|
536
539
|
// close; safe to read after a stream failure because open() owns the only writer before run().
|
|
@@ -610,6 +613,7 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
610
613
|
}
|
|
611
614
|
|
|
612
615
|
async *run(request: CursorRunRequest, signal?: AbortSignal): AsyncIterable<CursorServerMessage> {
|
|
616
|
+
this.requestSignal = signal;
|
|
613
617
|
const queue: Array<{ message: CursorServerMessage; bytes: number }> = [];
|
|
614
618
|
let notify: (() => void) | undefined;
|
|
615
619
|
let done = false;
|
|
@@ -947,10 +951,14 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
947
951
|
}
|
|
948
952
|
|
|
949
953
|
private startShellCleanup(): Promise<BackgroundShellTerminationReport> {
|
|
950
|
-
return this.shellCleanup ??=
|
|
954
|
+
return this.shellCleanup ??= Promise.all([
|
|
955
|
+
terminateBackgroundShellsForSession(this.shellOwnerId),
|
|
956
|
+
this.foregroundShellOwner.close(),
|
|
957
|
+
]).then(([backgroundReport]) => backgroundReport);
|
|
951
958
|
}
|
|
952
959
|
|
|
953
960
|
async close(): Promise<void> {
|
|
961
|
+
const shellCleanup = this.startShellCleanup();
|
|
954
962
|
if (this.heartbeat) clearInterval(this.heartbeat);
|
|
955
963
|
if (this.turnEndedCloseTimer) clearTimeout(this.turnEndedCloseTimer);
|
|
956
964
|
this.clearPendingFinalize();
|
|
@@ -963,10 +971,11 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
963
971
|
this.releaseMcpObservation?.();
|
|
964
972
|
this.releaseMcpObservation = undefined;
|
|
965
973
|
void this.mcpManager?.dispose();
|
|
966
|
-
await
|
|
974
|
+
await shellCleanup;
|
|
967
975
|
}
|
|
968
976
|
|
|
969
977
|
private cancelCursorRun(): void {
|
|
978
|
+
void this.startShellCleanup();
|
|
970
979
|
this.expectedClose = true;
|
|
971
980
|
this.clearPendingFinalize();
|
|
972
981
|
if (this.heartbeat) clearInterval(this.heartbeat);
|
|
@@ -1164,6 +1173,7 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
1164
1173
|
fail,
|
|
1165
1174
|
finish,
|
|
1166
1175
|
clearTimer: () => {
|
|
1176
|
+
void this.startShellCleanup();
|
|
1167
1177
|
this.clearFirstFrameTimer();
|
|
1168
1178
|
this.clearStreamHealthTimer();
|
|
1169
1179
|
},
|
|
@@ -1403,6 +1413,8 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
1403
1413
|
failAndClear(realErr);
|
|
1404
1414
|
};
|
|
1405
1415
|
const onStreamEnd = () => {
|
|
1416
|
+
// Native frameWork can be awaiting a shell; cancel before waiting for it to drain.
|
|
1417
|
+
void this.startShellCleanup();
|
|
1406
1418
|
this.clearFirstFrameTimer();
|
|
1407
1419
|
debugProviderDiagnostic("cursor", "stream-end", {
|
|
1408
1420
|
committed: this.committed,
|
|
@@ -1496,6 +1508,7 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
1496
1508
|
stream!.on("trailers", onTrailers);
|
|
1497
1509
|
stream!.on("error", onStreamError);
|
|
1498
1510
|
stream!.on("end", onStreamEnd);
|
|
1511
|
+
stream!.on("close", () => { void this.startShellCleanup(); });
|
|
1499
1512
|
}
|
|
1500
1513
|
|
|
1501
1514
|
signal?.addEventListener("abort", () => {
|
|
@@ -1569,7 +1582,9 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
1569
1582
|
// an eventual invalid_argument cannot cause the adapter's fresh-conversation fallback to
|
|
1570
1583
|
// run the same local action twice.
|
|
1571
1584
|
push({ type: "local_side_effect" });
|
|
1572
|
-
const replies = await handleCursorNativeExec(message.message.value,
|
|
1585
|
+
const replies = await handleCursorNativeExec(message.message.value, {
|
|
1586
|
+
...this.execContext, foregroundShellOwner: this.foregroundShellOwner, signal: this.requestSignal,
|
|
1587
|
+
});
|
|
1573
1588
|
for (const reply of replies) this.writeConnectFrame(encodeConnectFrame(reply));
|
|
1574
1589
|
return;
|
|
1575
1590
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { spawn,
|
|
1
|
+
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
+
import { foregroundShellUnavailableMessage, runForegroundShell, type CursorForegroundShellOwner } from "./native-foreground-shell";
|
|
3
4
|
import { create } from "@bufbuild/protobuf";
|
|
4
5
|
import {
|
|
5
6
|
BackgroundShellSpawnErrorSchema,
|
|
@@ -123,39 +124,11 @@ export function rejectShellExecForPolicy(execMsg: ExecServerMessage, hint?: stri
|
|
|
123
124
|
return execBytes(execMsg, "shellResult", rejectedShellResult(args.command, resolve(args.workingDirectory || process.cwd()), Date.now(), hint));
|
|
124
125
|
}
|
|
125
126
|
|
|
126
|
-
export function shellExec(execMsg: ExecServerMessage): Uint8Array {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const result = spawnSync(args.command, { cwd, shell: true, encoding: "utf8", timeout: args.hardTimeout || 120_000 });
|
|
132
|
-
const elapsed = Date.now() - started;
|
|
133
|
-
const stdout = String(result.stdout ?? "");
|
|
134
|
-
const stderr = String(result.stderr ?? "");
|
|
135
|
-
const code = typeof result.status === "number" ? result.status : 1;
|
|
136
|
-
if (code === 0) {
|
|
137
|
-
return execBytes(execMsg, "shellResult", create(ShellResultSchema, {
|
|
138
|
-
result: {
|
|
139
|
-
case: "success",
|
|
140
|
-
value: create(ShellSuccessSchema, { command: args.command, workingDirectory: cwd, exitCode: code, signal: "", stdout, stderr, executionTime: elapsed }),
|
|
141
|
-
},
|
|
142
|
-
}));
|
|
143
|
-
}
|
|
144
|
-
return execBytes(execMsg, "shellResult", create(ShellResultSchema, {
|
|
145
|
-
result: {
|
|
146
|
-
case: "failure",
|
|
147
|
-
value: create(ShellFailureSchema, {
|
|
148
|
-
command: args.command,
|
|
149
|
-
workingDirectory: cwd,
|
|
150
|
-
exitCode: code,
|
|
151
|
-
signal: String(result.signal ?? ""),
|
|
152
|
-
stdout,
|
|
153
|
-
stderr,
|
|
154
|
-
executionTime: elapsed,
|
|
155
|
-
aborted: !!result.error,
|
|
156
|
-
}),
|
|
157
|
-
},
|
|
158
|
-
}));
|
|
127
|
+
export function shellExec(execMsg: ExecServerMessage, redirectHint?: string): Uint8Array {
|
|
128
|
+
// Synchronous shellArgs needs the same admission fence as shellStreamArgs;
|
|
129
|
+
// changing wire shape must not bypass the missing descendant owner.
|
|
130
|
+
// Without a catalog hint, keep the default bridge redirect (#604) after the reason.
|
|
131
|
+
return rejectShellExecForPolicy(execMsg, foregroundShellUnavailableMessage(redirectHint ?? nativeShellDisabledMessage()));
|
|
159
132
|
}
|
|
160
133
|
|
|
161
134
|
export function rejectShellStreamExecForPolicy(execMsg: ExecServerMessage, hint?: string): Uint8Array[] {
|
|
@@ -178,7 +151,12 @@ export function rejectShellStreamExecForPolicy(execMsg: ExecServerMessage, hint?
|
|
|
178
151
|
];
|
|
179
152
|
}
|
|
180
153
|
|
|
181
|
-
export async function shellStreamExec(
|
|
154
|
+
export async function shellStreamExec(
|
|
155
|
+
execMsg: ExecServerMessage,
|
|
156
|
+
owner?: CursorForegroundShellOwner,
|
|
157
|
+
signal?: AbortSignal,
|
|
158
|
+
redirectHint?: string,
|
|
159
|
+
): Promise<Uint8Array[]> {
|
|
182
160
|
if (execMsg.message.case !== "shellStreamArgs") throw new Error("invalid shell stream exec");
|
|
183
161
|
const args = execMsg.message.value;
|
|
184
162
|
const cwd = resolve(args.workingDirectory || process.cwd());
|
|
@@ -188,36 +166,13 @@ export async function shellStreamExec(execMsg: ExecServerMessage): Promise<Uint8
|
|
|
188
166
|
event: { case: "start", value: create(ShellStreamStartSchema, { sandboxPolicy: args.requestedSandboxPolicy }) },
|
|
189
167
|
})),
|
|
190
168
|
];
|
|
191
|
-
const result = await
|
|
192
|
-
|
|
193
|
-
let stdout = "";
|
|
194
|
-
let stderr = "";
|
|
195
|
-
let aborted = false;
|
|
196
|
-
const timeout = setTimeout(() => {
|
|
197
|
-
aborted = true;
|
|
198
|
-
child.kill();
|
|
199
|
-
}, args.hardTimeout || 120_000);
|
|
200
|
-
child.stdout.on("data", chunk => {
|
|
201
|
-
stdout += String(chunk);
|
|
202
|
-
});
|
|
203
|
-
child.stderr.on("data", chunk => {
|
|
204
|
-
stderr += String(chunk);
|
|
205
|
-
});
|
|
206
|
-
child.on("close", code => {
|
|
207
|
-
clearTimeout(timeout);
|
|
208
|
-
resolvePromise({ stdout, stderr, code: code ?? 1, aborted });
|
|
209
|
-
});
|
|
210
|
-
child.on("error", err => {
|
|
211
|
-
clearTimeout(timeout);
|
|
212
|
-
resolvePromise({ stdout, stderr: stderr + errorText(err), code: 1, aborted });
|
|
213
|
-
});
|
|
214
|
-
});
|
|
215
|
-
if (result.stdout) {
|
|
169
|
+
const result = await runForegroundShell(args.command, cwd, args.hardTimeout ?? 120_000, owner, signal, redirectHint ?? nativeShellDisabledMessage());
|
|
170
|
+
if (result.stdout && !result.aborted) {
|
|
216
171
|
replies.push(execBytes(execMsg, "shellStream", create(ShellStreamSchema, {
|
|
217
172
|
event: { case: "stdout", value: create(ShellStreamStdoutSchema, { data: result.stdout }) },
|
|
218
173
|
})));
|
|
219
174
|
}
|
|
220
|
-
if (result.stderr) {
|
|
175
|
+
if (result.stderr && !result.aborted) {
|
|
221
176
|
replies.push(execBytes(execMsg, "shellStream", create(ShellStreamSchema, {
|
|
222
177
|
event: { case: "stderr", value: create(ShellStreamStderrSchema, { data: result.stderr }) },
|
|
223
178
|
})));
|
|
@@ -237,7 +192,7 @@ export async function shellStreamExec(execMsg: ExecServerMessage): Promise<Uint8
|
|
|
237
192
|
command: args.command,
|
|
238
193
|
workingDirectory: cwd,
|
|
239
194
|
exitCode: result.code,
|
|
240
|
-
signal:
|
|
195
|
+
signal: result.signal,
|
|
241
196
|
stdout: result.stdout,
|
|
242
197
|
stderr: result.stderr,
|
|
243
198
|
executionTime: Date.now() - started,
|
|
@@ -251,7 +206,7 @@ export async function shellStreamExec(execMsg: ExecServerMessage): Promise<Uint8
|
|
|
251
206
|
command: args.command,
|
|
252
207
|
workingDirectory: cwd,
|
|
253
208
|
exitCode: result.code,
|
|
254
|
-
signal:
|
|
209
|
+
signal: result.signal,
|
|
255
210
|
stdout: result.stdout,
|
|
256
211
|
stderr: result.stderr,
|
|
257
212
|
executionTime: Date.now() - started,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CursorForegroundShellOwner } from "./native-foreground-shell";
|
|
1
2
|
import { createHash } from "node:crypto";
|
|
2
3
|
import { enforceAppOwnedMemoryBudget } from "../../lib/app-owned-memory";
|
|
3
4
|
import { create } from "@bufbuild/protobuf";
|
|
@@ -66,6 +67,8 @@ export type CursorNativeExecDeps = CursorNativeNetworkDeps & CursorNativeToolDep
|
|
|
66
67
|
export interface CursorNativeExecContext extends CursorNativeExecDeps {
|
|
67
68
|
/** Stable owner for background shells created by this transport session. */
|
|
68
69
|
sessionId?: string;
|
|
70
|
+
foregroundShellOwner?: CursorForegroundShellOwner;
|
|
71
|
+
signal?: AbortSignal;
|
|
69
72
|
mcpToolDefs?: McpToolDefinition[];
|
|
70
73
|
clientToolDefs?: McpToolDefinition[];
|
|
71
74
|
/** Unsafe opt-in escape hatch for Cursor server-driven local fs/shell/fetch execution. */
|
|
@@ -706,8 +709,8 @@ export async function handleCursorNativeExec(execMsg: ExecServerMessage, deps: C
|
|
|
706
709
|
if (execCase === "deleteArgs") return [deps.rejectNativeFileMutations ? rejectDeleteExecForApplyPatch(execMsg, deps.structuredEditAvailable === true) : deleteExec(execMsg)];
|
|
707
710
|
if (execCase === "lsArgs") return [lsExec(execMsg)];
|
|
708
711
|
if (execCase === "grepArgs") return [grepExec(execMsg)];
|
|
709
|
-
if (execCase === "shellArgs") return [shellExec(execMsg)];
|
|
710
|
-
if (execCase === "shellStreamArgs") return shellStreamExec(execMsg);
|
|
712
|
+
if (execCase === "shellArgs") return [shellExec(execMsg, deps.nativeExecRedirectHint)];
|
|
713
|
+
if (execCase === "shellStreamArgs") return shellStreamExec(execMsg, deps.foregroundShellOwner, deps.signal, deps.nativeExecRedirectHint);
|
|
711
714
|
if (execCase === "backgroundShellSpawnArgs") return [backgroundShellSpawnExec(execMsg, deps.sessionId ?? "")];
|
|
712
715
|
if (execCase === "writeShellStdinArgs") return [writeShellStdinExec(execMsg, deps.sessionId ?? "")];
|
|
713
716
|
if (execCase === "fetchArgs") return [await fetchExec(execMsg, deps)];
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export interface ForegroundShellResult {
|
|
2
|
+
stdout: string;
|
|
3
|
+
stderr: string;
|
|
4
|
+
code: number;
|
|
5
|
+
aborted: boolean;
|
|
6
|
+
signal: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Keep the transport's shutdown fence even while foreground execution is unavailable. */
|
|
10
|
+
export class CursorForegroundShellOwner {
|
|
11
|
+
private closed = false;
|
|
12
|
+
|
|
13
|
+
get activeCount(): number { return 0; }
|
|
14
|
+
get isClosed(): boolean { return this.closed; }
|
|
15
|
+
|
|
16
|
+
async close(): Promise<void> { this.closed = true; }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function foregroundShellUnavailableMessage(redirectHint?: string): string {
|
|
20
|
+
return "Cursor foreground native shell is unavailable without kernel-backed descendant ownership. "
|
|
21
|
+
+ (redirectHint ?? "Use the client shell tool instead.");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function runForegroundShell(
|
|
25
|
+
_command: string,
|
|
26
|
+
_cwd: string,
|
|
27
|
+
_hardTimeout: number,
|
|
28
|
+
owner = new CursorForegroundShellOwner(),
|
|
29
|
+
signal?: AbortSignal,
|
|
30
|
+
redirectHint?: string,
|
|
31
|
+
): Promise<ForegroundShellResult> {
|
|
32
|
+
// A POSIX process group is not a descendant owner: a child can leave it with
|
|
33
|
+
// setsid(), including after closing its inherited pipes. Windows needs a job
|
|
34
|
+
// object for the same lifetime guarantee. Neither backend exists here, so reject
|
|
35
|
+
// BEFORE spawn rather than reporting group disappearance as complete cleanup.
|
|
36
|
+
const cancelled = owner.isClosed || signal?.aborted;
|
|
37
|
+
return Promise.resolve({
|
|
38
|
+
stdout: "",
|
|
39
|
+
stderr: cancelled ? "Cursor shell cancelled." : foregroundShellUnavailableMessage(redirectHint),
|
|
40
|
+
code: 1,
|
|
41
|
+
aborted: true,
|
|
42
|
+
signal: "",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -1289,7 +1289,13 @@ export async function* streamChatEvents(req: CloudChatRequest): AsyncGenerator<C
|
|
|
1289
1289
|
// The status line is carried on the error. A cap or an expired credential
|
|
1290
1290
|
// delivered instead as a Connect EOS trailer is mapped by
|
|
1291
1291
|
// connectTrailerHttpStatus at the trailer sites below.
|
|
1292
|
-
|
|
1292
|
+
const error = new CloudChatError(`GetChatMessage failed (HTTP ${resp.status})`, undefined, undefined, resp.status);
|
|
1293
|
+
// No consumer will drain this body. Cancellation must neither replace the
|
|
1294
|
+
// status error nor delay it if a transport's cancel promise never settles.
|
|
1295
|
+
try {
|
|
1296
|
+
void resp.body?.cancel(error).catch(() => undefined);
|
|
1297
|
+
} catch { /* Non-conforming streams can throw synchronously from cancel. */ }
|
|
1298
|
+
throw error;
|
|
1293
1299
|
}
|
|
1294
1300
|
if (!resp.body) {
|
|
1295
1301
|
throw new CloudChatError('GetChatMessage response had no body stream');
|
|
@@ -20,8 +20,11 @@
|
|
|
20
20
|
* `function_call_output` is parsed with `isError: false`. Cursor combines this set with
|
|
21
21
|
* `isFailedEmptyExecWrapper` below for Computer Use, where a failed wrapper is separately marked
|
|
22
22
|
* `isError`.
|
|
23
|
+
*
|
|
24
|
+
* `(?=(X))\1` pins each wildcard run to its maximal match — without it, adjacent `\n+`/`\s*`
|
|
25
|
+
* runs can repartition a newline block combinatorially (the ReDoS shape this had before).
|
|
23
26
|
*/
|
|
24
|
-
export const EMPTY_EXEC_OUTPUT_REGEX = /^(?:(?:Script completed|Command finished|Execution finished)[^\n]
|
|
27
|
+
export const EMPTY_EXEC_OUTPUT_REGEX = /^(?:(?:Script completed|Command finished|Execution finished)(?=([^\n]*))\1(?=(\n+))\2)?(?:Wall time(?=([^\n]*))\3(?=(\n+))\4)?(?:Output:(?=(\s*))\5)?(?:<empty>)?(?=(\s*))\6$/;
|
|
25
28
|
|
|
26
29
|
function skipFailedWrapperBlankSeparators(text: string, start: number): number {
|
|
27
30
|
let index = start;
|
package/src/adapters/google.ts
CHANGED
|
@@ -456,6 +456,18 @@ function messagesToGeminiFormat(
|
|
|
456
456
|
}
|
|
457
457
|
}
|
|
458
458
|
|
|
459
|
+
// A functionCall turn may not open the request: the upstream requires it to follow a user or
|
|
460
|
+
// function-response turn, and rejects with "function call turn comes immediately after a user
|
|
461
|
+
// turn or after a function response turn" (HTTP 400, #5008). Context compaction can truncate a
|
|
462
|
+
// long history so it opens on an assistant tool call. Prepend a user nudge, the same repair
|
|
463
|
+
// Kiro applies to assistant-head turns (src/adapters/kiro/payload.ts). A model head carrying
|
|
464
|
+
// only text is left alone: no upstream rule against it is demonstrated, and repairing it would
|
|
465
|
+
// inject a turn into valid requests.
|
|
466
|
+
const firstTurn = contents[0] as { role?: string; parts?: Array<{ functionCall?: unknown }> } | undefined;
|
|
467
|
+
if (firstTurn?.role === "model" && firstTurn.parts?.some(p => p.functionCall !== undefined)) {
|
|
468
|
+
contents.unshift({ role: "user", parts: [{ text: "(continue)" }] });
|
|
469
|
+
}
|
|
470
|
+
|
|
459
471
|
// Gemini API and Claude-on-Antigravity reject assistant-tail (model-tail in Gemini terms)
|
|
460
472
|
// histories. Gemini fails upstream with "Requests ending with a model turn are not supported"
|
|
461
473
|
// (HTTP 400), while Claude fails with "This model does not support assistant message prefill.
|
|
@@ -88,6 +88,7 @@ interface NativeStreamToolCall {
|
|
|
88
88
|
nativeIndex?: number;
|
|
89
89
|
arguments: Record<string, unknown>;
|
|
90
90
|
argumentBytes: number;
|
|
91
|
+
metadataBytes: number;
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
interface NativeStreamState {
|
|
@@ -106,6 +107,10 @@ type NativeReadResult = { done: false; value: Uint8Array } | { done: true; value
|
|
|
106
107
|
|
|
107
108
|
const NATIVE_THINK_VALUES = new Set(["low", "medium", "high", "max"]);
|
|
108
109
|
const NATIVE_TOOL_ID_MAX_LENGTH = 256;
|
|
110
|
+
const NATIVE_TOOL_NAME_MAX_BYTES = 1024;
|
|
111
|
+
const NATIVE_MAX_PENDING_TOOL_CALLS = 128;
|
|
112
|
+
// Account for the retained Map key and call bookkeeping in addition to the provider's name.
|
|
113
|
+
const NATIVE_TOOL_CALL_BOOKKEEPING_BYTES = 128;
|
|
109
114
|
const NATIVE_TOOL_ID_CONTROL = /[\u0000-\u001f\u007f]/u;
|
|
110
115
|
|
|
111
116
|
function isRecord(value: unknown): value is JsonRecord {
|
|
@@ -598,6 +603,10 @@ function nativeMessageEvents(message: JsonRecord, state: NativeStreamState, budg
|
|
|
598
603
|
}
|
|
599
604
|
const fn = rawCall.function;
|
|
600
605
|
if (typeof fn.name !== "string" || !fn.name.trim()) throw new Error("ollama-native response tool call had no name");
|
|
606
|
+
const nameBytes = new TextEncoder().encode(fn.name).byteLength;
|
|
607
|
+
if (nameBytes > NATIVE_TOOL_NAME_MAX_BYTES) {
|
|
608
|
+
throw new Error(`ollama-native response tool call name exceeded ${NATIVE_TOOL_NAME_MAX_BYTES} bytes`);
|
|
609
|
+
}
|
|
601
610
|
const args = assertObjectArguments(fn.arguments, "response tool call");
|
|
602
611
|
const index = isFiniteNonNegativeInteger(fn.index) ? fn.index : undefined;
|
|
603
612
|
const nativeId = validNativeToolCallId(rawCall.id);
|
|
@@ -610,6 +619,9 @@ function nativeMessageEvents(message: JsonRecord, state: NativeStreamState, budg
|
|
|
610
619
|
if (!existing && !state.allowParallelToolCalls && state.toolCalls.size > 0) {
|
|
611
620
|
throw new Error("ollama-native provider emitted parallel tool calls while parallelToolCalls:false was requested");
|
|
612
621
|
}
|
|
622
|
+
if (!existing && state.toolCalls.size >= NATIVE_MAX_PENDING_TOOL_CALLS) {
|
|
623
|
+
throw new Error(`ollama-native response exceeded ${NATIVE_MAX_PENDING_TOOL_CALLS} pending tool calls`);
|
|
624
|
+
}
|
|
613
625
|
if (existing) {
|
|
614
626
|
if (existing.name !== fn.name) throw new Error("ollama-native response reused a tool-call index for another function");
|
|
615
627
|
if (nativeId && existing.nativeId && nativeId !== existing.nativeId) {
|
|
@@ -627,12 +639,25 @@ function nativeMessageEvents(message: JsonRecord, state: NativeStreamState, budg
|
|
|
627
639
|
...(index !== undefined ? { nativeIndex: index } : {}),
|
|
628
640
|
arguments: args,
|
|
629
641
|
argumentBytes: 0,
|
|
642
|
+
metadataBytes: 0,
|
|
630
643
|
};
|
|
631
644
|
budget.openCall(call.budgetKey);
|
|
632
645
|
try {
|
|
646
|
+
const metadataBytes = nameBytes + NATIVE_TOOL_CALL_BOOKKEEPING_BYTES;
|
|
647
|
+
// Name and bookkeeping are retained call metadata, not arguments: charging them to the
|
|
648
|
+
// call's budget key would consume the per-call argument allowance. Keep them on the
|
|
649
|
+
// shared retained budget and release them when the call closes.
|
|
650
|
+
budget.chargeRetained(metadataBytes, {
|
|
651
|
+
kind: "tool_args",
|
|
652
|
+
});
|
|
653
|
+
call.metadataBytes = metadataBytes;
|
|
633
654
|
replaceNativeToolArguments(call, args, budget);
|
|
634
655
|
state.toolCalls.set(key, call);
|
|
635
656
|
} catch (error) {
|
|
657
|
+
if (call.metadataBytes > 0) {
|
|
658
|
+
budget.releaseRetained(call.metadataBytes, { kind: "tool_args" });
|
|
659
|
+
call.metadataBytes = 0;
|
|
660
|
+
}
|
|
636
661
|
budget.closeCall(call.budgetKey);
|
|
637
662
|
throw error;
|
|
638
663
|
}
|
|
@@ -687,7 +712,13 @@ function replaceNativeToolArguments(
|
|
|
687
712
|
}
|
|
688
713
|
|
|
689
714
|
function releaseNativeStateBuffers(state: NativeStreamState, budget: TranslatorBudget): void {
|
|
690
|
-
for (const call of state.toolCalls.values())
|
|
715
|
+
for (const call of state.toolCalls.values()) {
|
|
716
|
+
if (call.metadataBytes > 0) {
|
|
717
|
+
budget.releaseRetained(call.metadataBytes, { kind: "tool_args" });
|
|
718
|
+
call.metadataBytes = 0;
|
|
719
|
+
}
|
|
720
|
+
budget.closeCall(call.budgetKey);
|
|
721
|
+
}
|
|
691
722
|
}
|
|
692
723
|
|
|
693
724
|
function nativeBodyMessage(value: unknown): JsonRecord {
|