@bitkyc08/opencodex 2.59.0 → 2.60.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/gui/dist/assets/index-BTuCbqQd.css +1 -0
- package/gui/dist/assets/index-DoBVdPHP.js +134 -0
- package/gui/dist/index.html +2 -2
- package/package.json +4 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +4 -1
- package/src/adapters/base.ts +23 -0
- package/src/adapters/coding-agent/turn.ts +22 -2
- package/src/adapters/command-code.ts +50 -3
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/request-builder.ts +7 -1
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +3 -1
- package/src/adapters/devin/cloud-direct/chat.ts +3 -1
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
- package/src/adapters/devin.ts +50 -13
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +29 -5
- package/src/adapters/google-http.ts +49 -10
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +37 -6
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +5 -1
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +15 -3
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/bridge/sse.ts +3 -151
- package/src/cli/account-extended.ts +4 -4
- package/src/cli/dispatch.ts +3 -3
- package/src/cli/doctor.ts +28 -9
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +7 -2
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/client/machine-api.ts +2 -2
- package/src/client/machine-listener.ts +2 -2
- package/src/client/runtime.ts +26 -2
- package/src/codex/account-store.ts +65 -0
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/pool-quota-probe.ts +30 -7
- package/src/codex/catalog/gather-capture.ts +21 -2
- package/src/codex/catalog/model-hints.ts +29 -28
- package/src/codex/catalog/parsing.ts +7 -0
- package/src/codex/catalog/provider-models.ts +19 -2
- package/src/codex/catalog/retained-sync.ts +22 -26
- package/src/codex/catalog/routed-gather.ts +19 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +20 -166
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/model-cache.ts +47 -0
- package/src/codex/model-entitlements.ts +29 -10
- package/src/codex/observed-model-denials.ts +101 -8
- package/src/codex/prompt-text-probe.ts +9 -6
- package/src/codex/routing.ts +7 -1
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +22 -4
- package/src/combos/failover.ts +3 -0
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/diagnostics.ts +22 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +18 -0
- package/src/config/proxy-env.ts +8 -2
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +4 -0
- package/src/config/schema/leaf-validators.ts +12 -0
- package/src/config.ts +2 -2
- package/src/generated/compatibility-version.json +249 -169
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/mutation-plan.ts +815 -0
- package/src/integrations/writer.ts +85 -99
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +23 -0
- package/src/lib/errors.ts +79 -0
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-outbound.ts +50 -2
- package/src/lib/socks5-fetch.ts +136 -26
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +218 -27
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +1 -0
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +3 -107
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/key-failover.ts +66 -18
- package/src/providers/model-rename-migration.ts +55 -1
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/vendor-probes-key.ts +1 -1
- package/src/providers/registry/entries-core.ts +39 -17
- package/src/providers/registry/entries-extended.ts +35 -1
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +9 -0
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/responses/apply-patch-envelope.ts +0 -12
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +91 -115
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- package/src/server/auth-cors.ts +3 -0
- package/src/server/chat-completions.ts +5 -2
- package/src/server/claude-messages.ts +6 -3
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/index/serve-options.ts +86 -29
- package/src/server/index/spend-ledger-lifecycle.ts +66 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +16 -13
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/config-routes.ts +18 -2
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/route-registry.ts +14 -0
- package/src/server/management/shared.ts +10 -3
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/management-auth.ts +15 -1
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +119 -10
- package/src/server/request-log.ts +55 -2
- package/src/server/request-metrics.ts +236 -0
- package/src/server/responses/adapter-continuation.ts +3 -3
- package/src/server/responses/adapter-dispatch.ts +11 -6
- package/src/server/responses/compact.ts +32 -10
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +8 -3
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +6 -12
- package/src/server/responses/core-opaque-recovery.ts +1 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- package/src/server/responses/passthrough-delivery.ts +31 -14
- package/src/server/responses/passthrough-dispatch.ts +36 -9
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +67 -17
- package/src/server/responses/request-send-budget.ts +5 -1
- package/src/server/responses/request-sidecar-auth.ts +1 -1
- package/src/server/responses/request-transport.ts +2 -2
- package/src/server/responses/run-turn-execution.ts +25 -3
- package/src/server/responses/sidecar-execution.ts +17 -2
- package/src/server/responses/ws-upstream.ts +14 -27
- package/src/server/responses-custom-tool-repair.ts +27 -54
- package/src/server/responses-undeclared-tool-guard.ts +31 -1
- package/src/server/sse-payload-rewrite.ts +1 -1
- package/src/tray/windows-tray.ps1 +155 -3
- package/src/types/config.ts +11 -3
- package/src/types/provider.ts +18 -0
- package/src/types/request.ts +2 -0
- package/src/types/tools.ts +14 -0
- package/src/types.ts +1 -0
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/gui/dist/assets/index-C5IebErG.js +0 -136
- package/gui/dist/assets/index-OESInAjC.css +0 -1
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-DoBVdPHP.js"></script>
|
|
20
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BTuCbqQd.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
|
23
23
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bitkyc08/opencodex",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.60.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",
|
|
@@ -83,6 +83,9 @@
|
|
|
83
83
|
"@types/bun": "1.4.0",
|
|
84
84
|
"typescript": "7.0.2"
|
|
85
85
|
},
|
|
86
|
+
"optionalDependencies": {
|
|
87
|
+
"wreq-js": "2.3.1"
|
|
88
|
+
},
|
|
86
89
|
"overrides": {
|
|
87
90
|
"@hono/node-server": "2.1.0",
|
|
88
91
|
"fast-uri": "^3.1.7",
|
|
@@ -41,7 +41,9 @@ export const MAX_INPUT_BASE64_LENGTH = 64 * MiB;
|
|
|
41
41
|
* would widen the adapter contract with no demonstrated need.
|
|
42
42
|
*/
|
|
43
43
|
export const IMAGE_NORMALIZE_CONCURRENCY = 4;
|
|
44
|
-
|
|
44
|
+
// Keep one decoded RGBA surface below 64 MiB. Native codecs may allocate additional
|
|
45
|
+
// working buffers, so this is paired with process-wide admission in the normalizer.
|
|
46
|
+
export const MAX_INPUT_PIXELS = 16_000_000;
|
|
45
47
|
|
|
46
48
|
|
|
47
49
|
/** Formats Anthropic accepts as-is; anything else must be transcoded or dropped. */
|
|
@@ -50,6 +52,8 @@ const PASSTHROUGH_MEDIA = new Set(["image/jpeg", "image/png", "image/gif", "imag
|
|
|
50
52
|
export interface NormalizeOptions {
|
|
51
53
|
/** Shift every image's starting ladder position down (413 retry tightening; 030). */
|
|
52
54
|
tierBias?: number;
|
|
55
|
+
/** Cancels queued native decode work and stops pulling more images. */
|
|
56
|
+
abortSignal?: AbortSignal;
|
|
53
57
|
/** Test seam: replaces the Bun.Image encode path (audit round 1, blocker 6). */
|
|
54
58
|
encode?: EncodeFn;
|
|
55
59
|
/** Test seam: replaces the pass-through decode validation (C-gate round 1, blocker 1). */
|
|
@@ -275,6 +279,9 @@ export const bunImageEncode: EncodeFn = async (input, spec, quality) => {
|
|
|
275
279
|
const meta = await image.metadata();
|
|
276
280
|
const w = typeof meta.width === "number" ? meta.width : 0;
|
|
277
281
|
const h = typeof meta.height === "number" ? meta.height : 0;
|
|
282
|
+
if (w <= 0 || h <= 0 || w * h > MAX_INPUT_PIXELS) {
|
|
283
|
+
throw new Error("image dimensions exceed the safe decode limit");
|
|
284
|
+
}
|
|
278
285
|
let pipeline = new Bun.Image(input);
|
|
279
286
|
if (w > spec.maxEdge || h > spec.maxEdge) {
|
|
280
287
|
const scale = spec.maxEdge / Math.max(w, h);
|
|
@@ -290,7 +297,14 @@ export const bunImageEncode: EncodeFn = async (input, spec, quality) => {
|
|
|
290
297
|
* instead of riding pass-through to an Anthropic 400 (C-gate round 1, blocker 1).
|
|
291
298
|
*/
|
|
292
299
|
export const bunImageValidate: ValidateFn = async input => {
|
|
293
|
-
|
|
300
|
+
const image = new Bun.Image(input);
|
|
301
|
+
const meta = await image.metadata();
|
|
302
|
+
const w = typeof meta.width === "number" ? meta.width : 0;
|
|
303
|
+
const h = typeof meta.height === "number" ? meta.height : 0;
|
|
304
|
+
if (w <= 0 || h <= 0 || w * h > MAX_INPUT_PIXELS) {
|
|
305
|
+
throw new Error("image dimensions exceed the safe decode limit");
|
|
306
|
+
}
|
|
307
|
+
await image.resize(1, 1).jpeg({ quality: 1 }).toBuffer();
|
|
294
308
|
};
|
|
295
309
|
|
|
296
310
|
/**
|
|
@@ -32,6 +32,37 @@ import { recordedEmittedPosition, recordEmittedPosition } from "./anthropic-imag
|
|
|
32
32
|
import { IMAGE_NORMALIZE_CONCURRENCY, MAX_INPUT_BASE64_LENGTH, MAX_INPUT_PIXELS } from "./anthropic-image-codec";
|
|
33
33
|
import type { NormalizeOptions } from "./anthropic-image-codec";
|
|
34
34
|
|
|
35
|
+
const IMAGE_DECODE_PROCESS_CONCURRENCY = IMAGE_NORMALIZE_CONCURRENCY;
|
|
36
|
+
let activeImageDecodes = 0;
|
|
37
|
+
const imageDecodeWaiters: Array<() => void> = [];
|
|
38
|
+
|
|
39
|
+
async function enterImageDecode(signal?: AbortSignal): Promise<() => void> {
|
|
40
|
+
if (signal?.aborted) throw signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
41
|
+
let transferred = false;
|
|
42
|
+
if (activeImageDecodes >= IMAGE_DECODE_PROCESS_CONCURRENCY) {
|
|
43
|
+
await new Promise<void>((resolve, reject) => {
|
|
44
|
+
const admit = (): void => {
|
|
45
|
+
transferred = true;
|
|
46
|
+
signal?.removeEventListener("abort", abort);
|
|
47
|
+
resolve();
|
|
48
|
+
};
|
|
49
|
+
const abort = (): void => {
|
|
50
|
+
const index = imageDecodeWaiters.indexOf(admit);
|
|
51
|
+
if (index >= 0) imageDecodeWaiters.splice(index, 1);
|
|
52
|
+
reject(signal?.reason ?? new DOMException("Aborted", "AbortError"));
|
|
53
|
+
};
|
|
54
|
+
imageDecodeWaiters.push(admit);
|
|
55
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
if (!transferred) activeImageDecodes++;
|
|
59
|
+
return () => {
|
|
60
|
+
const next = imageDecodeWaiters.shift();
|
|
61
|
+
if (next) next();
|
|
62
|
+
else activeImageDecodes--;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
35
66
|
const UNDECODABLE_TEXT = "[image omitted: undecodable or corrupt image data]";
|
|
36
67
|
const BOMB_TEXT = "[image omitted: image too large to process safely]";
|
|
37
68
|
const OVERFLOW_DROP_TEXT = "[image omitted: total image payload exceeded the provider request budget; older images were dropped]";
|
|
@@ -104,8 +135,24 @@ export async function normalizeImageTargets(targets: NormalizeTarget[], options:
|
|
|
104
135
|
const budget = options.budget ?? TOTAL_IMAGE_BASE64_BUDGET;
|
|
105
136
|
const overflowAction = options.overflowAction ?? "none";
|
|
106
137
|
const processLimit = options.processLimit ?? Number.POSITIVE_INFINITY;
|
|
138
|
+
const abortSignal = options.abortSignal;
|
|
107
139
|
const n = targets.length;
|
|
108
140
|
|
|
141
|
+
const process = async (b64: string, pos: number, mediaType: string) => {
|
|
142
|
+
if (abortSignal?.aborted) throw abortSignal.reason ?? new DOMException("Aborted", "AbortError");
|
|
143
|
+
const leave = await enterImageDecode(abortSignal);
|
|
144
|
+
try {
|
|
145
|
+
// Re-check after admission: an abort landing between dequeue and decode-start
|
|
146
|
+
// must not begin decoding (an in-flight native decode cannot be interrupted).
|
|
147
|
+
if (abortSignal?.aborted) throw abortSignal.reason ?? new DOMException("Aborted", "AbortError");
|
|
148
|
+
const result = await processAt(b64, pos, mediaType, encode, validate);
|
|
149
|
+
if (abortSignal?.aborted) throw abortSignal.reason ?? new DOMException("Aborted", "AbortError");
|
|
150
|
+
return result;
|
|
151
|
+
} finally {
|
|
152
|
+
leave();
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
109
156
|
// sourceB64/sourceMedia are the ORIGINAL input (encode source + cache identity);
|
|
110
157
|
// size always reflects the bytes currently ON the wire for this target (the core is
|
|
111
158
|
// the only mutator, so tracked size cannot drift from reality).
|
|
@@ -167,7 +214,7 @@ export async function normalizeImageTargets(targets: NormalizeTarget[], options:
|
|
|
167
214
|
// age tier on a cold store, which is the pre-#4532 behaviour.
|
|
168
215
|
const recorded = recordedEmittedPosition(b64, sourceMedia);
|
|
169
216
|
const pos = Math.min((recorded ?? initialPosition(newestFirstIndex, 0)) + Math.max(0, bias), TERMINAL_POS);
|
|
170
|
-
const result = await
|
|
217
|
+
const result = await process(b64, pos, sourceMedia);
|
|
171
218
|
if (result.kind === "failed") {
|
|
172
219
|
target.drop(UNDECODABLE_TEXT);
|
|
173
220
|
if (target.retainsBytesOnDrop) {
|
|
@@ -209,7 +256,7 @@ export async function normalizeImageTargets(targets: NormalizeTarget[], options:
|
|
|
209
256
|
while (sum > budget) {
|
|
210
257
|
const entry = entries.find((e): e is Entry => e !== null && !e.done);
|
|
211
258
|
if (!entry) break; // all terminal — overflowAction below decides
|
|
212
|
-
const result = await
|
|
259
|
+
const result = await process(entry.sourceB64, entry.pos + 1, entry.sourceMedia);
|
|
213
260
|
if (result.kind === "failed") {
|
|
214
261
|
entry.target.drop(UNDECODABLE_TEXT);
|
|
215
262
|
if (entry.target.retainsBytesOnDrop) {
|
|
@@ -945,7 +945,10 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
945
945
|
// Primary image layer: resize/re-encode to fit Anthropic limits without dropping
|
|
946
946
|
// (anthropic-image-normalize.ts); the guard below remains the deterministic backstop.
|
|
947
947
|
// imageTierBias > 0 = upstream-413 tightened retry (030): start every image one tier lower.
|
|
948
|
-
await normalizeAnthropicImages(messages, {
|
|
948
|
+
await normalizeAnthropicImages(messages, {
|
|
949
|
+
tierBias: incoming?.imageTierBias ?? 0,
|
|
950
|
+
abortSignal: incoming?.abortSignal,
|
|
951
|
+
});
|
|
949
952
|
// Anthropic rejects many-image requests (>20 images) carrying any image over
|
|
950
953
|
// 2000px per side; see anthropic-image-guard.ts for the full limit policy.
|
|
951
954
|
enforceAnthropicImageLimits(messages);
|
package/src/adapters/base.ts
CHANGED
|
@@ -31,11 +31,34 @@ export interface IncomingMeta {
|
|
|
31
31
|
* behind it (#4546).
|
|
32
32
|
*/
|
|
33
33
|
sendBudget?: RequestExecutionBudget;
|
|
34
|
+
/**
|
|
35
|
+
* Physical-send observations for runTurn adapters. Without the same callback carried by
|
|
36
|
+
* AdapterFetchContext, an adapter-owned replay spends the shared budget but remains absent
|
|
37
|
+
* from the request's sendCount.
|
|
38
|
+
*/
|
|
39
|
+
onPhysicalSend?: (send: { ordinal: number; recovery?: AttemptRecoveryKind }) => void;
|
|
40
|
+
/**
|
|
41
|
+
* Recovery refusals for runTurn adapters. A refused replay is not a send, so this separate
|
|
42
|
+
* channel explains why recovery stopped without inflating physical-send telemetry.
|
|
43
|
+
*/
|
|
44
|
+
onRecoveryWithheld?: (withheld: { reason: AttemptRecoveryWithheld }) => void;
|
|
34
45
|
}
|
|
35
46
|
|
|
36
47
|
export interface ProviderAdapter {
|
|
37
48
|
name: string;
|
|
38
49
|
|
|
50
|
+
/**
|
|
51
|
+
* This adapter reports every physical inference send through `IncomingMeta.onPhysicalSend`,
|
|
52
|
+
* including its first.
|
|
53
|
+
*
|
|
54
|
+
* The caller normally logs the first send before handing control over, which is correct for a
|
|
55
|
+
* transport whose sends it can see. An adapter that admits its own sends through the shared
|
|
56
|
+
* budget can have that first send refused, and a send logged before admission is a send the
|
|
57
|
+
* log claims and the wire never made. Setting this moves the first send's accounting to the
|
|
58
|
+
* boundary where it is actually dispatched.
|
|
59
|
+
*/
|
|
60
|
+
reportsPhysicalSends?: boolean;
|
|
61
|
+
|
|
39
62
|
/**
|
|
40
63
|
* Convert an already-read provider HTTP error into client-safe text. This hook must be pure and
|
|
41
64
|
* return fully redacted output: callers may pass untrusted provider headers and payload text.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawn as nodeSpawn, type ChildProcess, type SpawnOptions } from "node:child_process";
|
|
1
|
+
import { execFileSync, spawn as nodeSpawn, type ChildProcess, type SpawnOptions } from "node:child_process";
|
|
2
2
|
import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig } from "../../types";
|
|
3
3
|
import { commandInvocation } from "../../lib/win-exec";
|
|
4
4
|
import type { IncomingMeta } from "../base";
|
|
@@ -8,6 +8,9 @@ import { resolveCodingAgentBinary, resolveProfileByBaseUrl, type CodingAgentProv
|
|
|
8
8
|
/** Injectable spawn for tests; production uses node:child_process. */
|
|
9
9
|
export type SpawnFn = (command: string, args: readonly string[], options: SpawnOptions) => ChildProcess;
|
|
10
10
|
|
|
11
|
+
/** Injectable Windows process-tree terminator; production uses taskkill /T /F. */
|
|
12
|
+
export type KillWindowsProcessTreeFn = (pid: number) => void;
|
|
13
|
+
|
|
11
14
|
/** Per-turn injectables: spawn/which seams for tests plus wall-clock ceilings for timeout, kill grace, and bounded reap. */
|
|
12
15
|
export interface CodingAgentDeps {
|
|
13
16
|
spawn?: SpawnFn;
|
|
@@ -20,6 +23,8 @@ export interface CodingAgentDeps {
|
|
|
20
23
|
reapTimeoutMs?: number;
|
|
21
24
|
/** Test seam for Windows command-shim invocation. */
|
|
22
25
|
platform?: NodeJS.Platform;
|
|
26
|
+
/** Test seam for terminating a Windows CLI and all descendants. */
|
|
27
|
+
killWindowsProcessTree?: KillWindowsProcessTreeFn;
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
const DEFAULT_TIMEOUT_MS = 300_000;
|
|
@@ -27,6 +32,14 @@ const DEFAULT_KILL_GRACE_MS = 2_000;
|
|
|
27
32
|
/** Bound captured stderr so an error message can never carry an unbounded (or secret) payload. */
|
|
28
33
|
const MAX_STDERR_BYTES = 8 * 1024;
|
|
29
34
|
|
|
35
|
+
function killWindowsProcessTree(pid: number): void {
|
|
36
|
+
const taskkill = `${process.env.SystemRoot ?? "C:\\Windows"}\\System32\\taskkill.exe`;
|
|
37
|
+
execFileSync(taskkill, ["/PID", String(pid), "/T", "/F"], {
|
|
38
|
+
stdio: "pipe",
|
|
39
|
+
windowsHide: true,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
30
43
|
/** Env keys a CLI needs to run; everything else is dropped so the child env is scoped and deterministic. */
|
|
31
44
|
const INHERITED_ENV_KEYS = [
|
|
32
45
|
"PATH", "HOME", "USERPROFILE", "LANG", "LC_ALL", "LC_CTYPE", "TMPDIR", "TEMP", "TMP",
|
|
@@ -93,6 +106,7 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
|
|
|
93
106
|
const timeoutMs = deps.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
94
107
|
const killGraceMs = deps.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
|
|
95
108
|
const reapTimeoutMs = deps.reapTimeoutMs ?? (killGraceMs * 2 + 250);
|
|
109
|
+
const platform = deps.platform ?? process.platform;
|
|
96
110
|
|
|
97
111
|
if (incoming.abortSignal?.aborted) {
|
|
98
112
|
emit({ type: "error", message: "Coding-agent turn was aborted before start." });
|
|
@@ -140,7 +154,7 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
|
|
|
140
154
|
|
|
141
155
|
const args = buildArgs(profile, parsed, provider);
|
|
142
156
|
const env = buildEnv(profile, apiKey);
|
|
143
|
-
const invocation = commandInvocation(binary, args,
|
|
157
|
+
const invocation = commandInvocation(binary, args, platform, { env });
|
|
144
158
|
|
|
145
159
|
let child: ChildProcess;
|
|
146
160
|
try {
|
|
@@ -197,6 +211,12 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
|
|
|
197
211
|
const kill = (): void => {
|
|
198
212
|
if (killed || child.killed) return;
|
|
199
213
|
killed = true;
|
|
214
|
+
if (platform === "win32" && child.pid !== undefined) {
|
|
215
|
+
try {
|
|
216
|
+
(deps.killWindowsProcessTree ?? killWindowsProcessTree)(child.pid);
|
|
217
|
+
return;
|
|
218
|
+
} catch { /* fall back to terminating the direct child */ }
|
|
219
|
+
}
|
|
200
220
|
try { child.kill("SIGTERM"); } catch { /* already gone */ }
|
|
201
221
|
killTimer = setTimeout(() => {
|
|
202
222
|
try { child.kill("SIGKILL"); } catch { /* already gone */ }
|
|
@@ -8,7 +8,7 @@ import type { AdapterFetchContext, AdapterRequest, ProviderAdapter } from "./bas
|
|
|
8
8
|
import type { TranslatorBudget } from "../lib/translator-budget";
|
|
9
9
|
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
10
10
|
import { debugDroppedFrame } from "../lib/debug";
|
|
11
|
-
import { configuredReasoningEfforts } from "../reasoning-effort";
|
|
11
|
+
import { configuredReasoningEfforts, modelRecordValue } from "../reasoning-effort";
|
|
12
12
|
import { commandCodeReasoningEfforts, refreshCommandCodeReasoningEfforts } from "../providers/command-code-efforts";
|
|
13
13
|
import { identifyRoutedModel } from "./identity";
|
|
14
14
|
import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge";
|
|
@@ -483,13 +483,51 @@ async function fetchCommandCode(request: AdapterRequest, ctx: AdapterFetchContex
|
|
|
483
483
|
}
|
|
484
484
|
}
|
|
485
485
|
|
|
486
|
+
/**
|
|
487
|
+
* Has the operator declared their own ladders authoritative for this provider?
|
|
488
|
+
*
|
|
489
|
+
* Comparing a configured row against the shipped table cannot answer this. `providerConfigSeed`
|
|
490
|
+
* copies the whole table into every materialized preset, and both enrichment and routing keep a
|
|
491
|
+
* persisted row over the current seed, so a row written by an older release keeps its old value
|
|
492
|
+
* and starts LOOKING like an operator edit the moment the shipped table is corrected — at which
|
|
493
|
+
* point the stale row would outrank the correction and disable the rejection repair below.
|
|
494
|
+
* Provenance has to be declared rather than inferred, so it is: `providerConfigSeed` never
|
|
495
|
+
* writes this flag, which makes its presence something only a human can have caused.
|
|
496
|
+
*/
|
|
497
|
+
function operatorChoseCommandCodeLadder(provider: OcxProviderConfig, canonicalId: string): boolean {
|
|
498
|
+
if (provider.modelReasoningEffortsAuthoritative !== true) return false;
|
|
499
|
+
return modelRecordValue(provider.modelReasoningEfforts, canonicalId) !== undefined;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Resolve the ladder this request may draw a wire effort from.
|
|
504
|
+
*
|
|
505
|
+
* The shipped table is a default, not a ceiling the operator cannot reach past. Until this
|
|
506
|
+
* existed the adapter read `commandCodeReasoningEfforts() ?? configuredReasoningEfforts()`, so a
|
|
507
|
+
* model with a row ignored configuration outright while a model without one honoured it — and
|
|
508
|
+
* the catalog disagreed with both, because `configuredReasoningEfforts` is what advertises the
|
|
509
|
+
* picker. An operator who widened a pinned row saw the wider ladder offered in Codex and then
|
|
510
|
+
* watched the adapter strip the rung on the way out (#5096).
|
|
511
|
+
*
|
|
512
|
+
* An operator who sets `modelReasoningEffortsAuthoritative` now resolves through the same
|
|
513
|
+
* function the catalog uses, so the picker and the wire agree, and sanitization, tier healing and
|
|
514
|
+
* learned-refusal dropping apply to it. Every other provider keeps the shipped table, including a
|
|
515
|
+
* value learned by a profile refresh.
|
|
516
|
+
*/
|
|
517
|
+
function commandCodeEffortLadder(provider: OcxProviderConfig, canonicalId: string): readonly string[] | undefined {
|
|
518
|
+
if (operatorChoseCommandCodeLadder(provider, canonicalId)) {
|
|
519
|
+
return configuredReasoningEfforts(provider, canonicalId);
|
|
520
|
+
}
|
|
521
|
+
return commandCodeReasoningEfforts(canonicalId) ?? configuredReasoningEfforts(provider, canonicalId);
|
|
522
|
+
}
|
|
523
|
+
|
|
486
524
|
function supportedCommandCodeEffort(provider: OcxProviderConfig, modelId: string, requested: string | undefined): string | undefined {
|
|
487
525
|
if (!requested || requested === "none") return undefined;
|
|
488
526
|
// Compatibility ids (deepseek-v4-flash / glm-5.2) must resolve to their canonical
|
|
489
527
|
// Command Code id before the effort lookup, or legacy requests silently lose the
|
|
490
528
|
// reasoning effort because the official table is keyed by the canonical ids.
|
|
491
529
|
const canonicalId = canonicalCommandCodeModelId(modelId);
|
|
492
|
-
const supported =
|
|
530
|
+
const supported = commandCodeEffortLadder(provider, canonicalId);
|
|
493
531
|
if (!supported) return undefined;
|
|
494
532
|
// Only remap xhigh/ultra→max for models whose official profile documents that
|
|
495
533
|
// aliasing (deepseek v4, glm-5.2). Muse Spark's upstream accepts xhigh as a
|
|
@@ -501,7 +539,11 @@ function supportedCommandCodeEffort(provider: OcxProviderConfig, modelId: string
|
|
|
501
539
|
lower === "zai-org/glm-5.2";
|
|
502
540
|
if (requested === "xhigh" && !supported.includes("xhigh") && supported.includes("max")) {
|
|
503
541
|
wire = "max";
|
|
504
|
-
} else if (requested === "ultra" && needsAlias && supported.includes("max")) {
|
|
542
|
+
} else if (requested === "ultra" && needsAlias && !supported.includes("ultra") && supported.includes("max")) {
|
|
543
|
+
// The xhigh branch above already refuses to alias a rung the ladder advertises; ultra has to
|
|
544
|
+
// match it. No shipped row offers ultra, so this changes nothing for the built-in table — but
|
|
545
|
+
// an authoritative operator ladder that does offer it would otherwise advertise ultra in the
|
|
546
|
+
// picker and quietly send max, which is the catalog/wire disagreement this file just fixed.
|
|
505
547
|
wire = "max";
|
|
506
548
|
}
|
|
507
549
|
return (supported as readonly string[]).includes(wire) ? wire : undefined;
|
|
@@ -576,6 +618,11 @@ export function createCommandCodeAdapter(provider: OcxProviderConfig): ProviderA
|
|
|
576
618
|
try { return (JSON.parse(request.body) as { params?: { model?: unknown } }).params?.model; } catch { return undefined; }
|
|
577
619
|
})();
|
|
578
620
|
if (typeof modelId !== "string") return response;
|
|
621
|
+
// An operator who wrote this ladder authorized the rung deliberately. Replaying the turn
|
|
622
|
+
// without it would answer at the provider default and hide a wrong configuration behind a
|
|
623
|
+
// successful-looking response, so the upstream rejection is what the caller gets. The
|
|
624
|
+
// downgrade below stays for the shipped table, where the rung was never the caller's idea.
|
|
625
|
+
if (operatorChoseCommandCodeLadder(provider, canonicalCommandCodeModelId(modelId))) return response;
|
|
579
626
|
const refreshed = await refreshCommandCodeReasoningEfforts(modelId, executor);
|
|
580
627
|
if (!refreshed || refreshed.includes(currentEffort)) return response;
|
|
581
628
|
const retry = requestWithoutReasoningEffort(request);
|
|
@@ -42,6 +42,7 @@ export interface CursorCheckpointSnapshot {
|
|
|
42
42
|
coveredMessageCount?: number;
|
|
43
43
|
prefixDigest?: string;
|
|
44
44
|
systemDigest?: string;
|
|
45
|
+
toolSuspended?: boolean;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
interface CursorCheckpointStore {
|
|
@@ -229,6 +230,7 @@ export function commitCursorCheckpoint(input: {
|
|
|
229
230
|
coveredMessageCount?: number;
|
|
230
231
|
prefixDigest?: string;
|
|
231
232
|
systemDigest?: string;
|
|
233
|
+
toolSuspended?: boolean;
|
|
232
234
|
}): string | undefined {
|
|
233
235
|
if (!input.conversationId || !input.modelId || input.checkpointBytes.byteLength === 0) return undefined;
|
|
234
236
|
if (input.checkpointBytes.byteLength > CURSOR_CHECKPOINT_MAX_TOTAL_BYTES) return undefined;
|
|
@@ -258,6 +260,7 @@ export function commitCursorCheckpoint(input: {
|
|
|
258
260
|
...(input.coveredMessageCount !== undefined ? { coveredMessageCount: input.coveredMessageCount } : {}),
|
|
259
261
|
...(input.prefixDigest ? { prefixDigest: input.prefixDigest } : {}),
|
|
260
262
|
...(input.systemDigest ? { systemDigest: input.systemDigest } : {}),
|
|
263
|
+
...(input.toolSuspended ? { toolSuspended: true } : {}),
|
|
261
264
|
};
|
|
262
265
|
const blobIds = collectCheckpointBlobIds(input.checkpointBytes);
|
|
263
266
|
if (blobIds === undefined) return undefined;
|
|
@@ -305,15 +305,18 @@ export function isCursorRouterModelId(modelId: string): boolean {
|
|
|
305
305
|
export function filterCursorConfiguredModelsByLiveDiscovery<T extends { id: string }>(
|
|
306
306
|
configured: readonly T[],
|
|
307
307
|
liveIds: readonly string[],
|
|
308
|
+
maxModeLiveIds: readonly string[] = [],
|
|
308
309
|
): T[] {
|
|
309
|
-
return configured.filter(model =>
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
310
|
+
return configured.filter(model => {
|
|
311
|
+
if (CURSOR_KNOWN_UNCALLABLE_MODEL_IDS.has(model.id)) return false;
|
|
312
|
+
if (isCursorRouterModelId(model.id)) return true;
|
|
313
|
+
const ultraBase = cursorUltraBaseModelId(model.id);
|
|
314
|
+
return isCursorModelAvailableForAccount(ultraBase ?? model.id, liveIds)
|
|
315
|
+
// Successful discovery is authoritative: synthetic ultra rows additionally require the
|
|
316
|
+
// account-specific Max Mode capability. Discovery failures bypass this filter and retain
|
|
317
|
+
// the static seed under the caller's existing degraded-catalog policy.
|
|
318
|
+
&& (ultraBase === undefined || isCursorModelAvailableForAccount(ultraBase, maxModeLiveIds));
|
|
319
|
+
});
|
|
317
320
|
}
|
|
318
321
|
|
|
319
322
|
/**
|
|
@@ -87,7 +87,7 @@ import {
|
|
|
87
87
|
type BackgroundShellTerminationReport,
|
|
88
88
|
} from "./native-exec-shell";
|
|
89
89
|
import type { CursorClientMessage, CursorRunRequest, CursorServerMessage } from "./types";
|
|
90
|
-
import type { CursorTransport, CursorTransportFactoryInput } from "./transport";
|
|
90
|
+
import type { CursorStreamHealthClock, CursorTransport, CursorTransportFactoryInput } from "./transport";
|
|
91
91
|
import { CursorHttp1BidiConnection } from "./http1-bidi";
|
|
92
92
|
import { isPinnedHttp1 } from "../../lib/upstream-http-version";
|
|
93
93
|
|
|
@@ -107,6 +107,16 @@ const CURSOR_STREAM_SILENCE_FAIL_MS = 30_000;
|
|
|
107
107
|
* Reset on every decoded frame that is not liveness-only.
|
|
108
108
|
*/
|
|
109
109
|
const CURSOR_STREAM_HEARTBEAT_ONLY_FAIL_MS = 90_000;
|
|
110
|
+
/**
|
|
111
|
+
* The production T04 clock. The watchdog reads time and schedules its one timer through this
|
|
112
|
+
* object so a test can supply a clock it advances by hand; nothing else about the transport's
|
|
113
|
+
* timers moves with it.
|
|
114
|
+
*/
|
|
115
|
+
const REAL_STREAM_HEALTH_CLOCK: CursorStreamHealthClock = {
|
|
116
|
+
now: () => Date.now(),
|
|
117
|
+
setTimeout: (callback, ms) => setTimeout(callback, ms),
|
|
118
|
+
clearTimeout: timer => clearTimeout(timer),
|
|
119
|
+
};
|
|
110
120
|
/**
|
|
111
121
|
* After `turnEnded` is decoded, the application turn is complete. A server that keeps
|
|
112
122
|
* HTTP/2 open past this point cannot hold the turn hostage (senpi #1062): we close our side
|
|
@@ -494,6 +504,8 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
494
504
|
private lastInboundFrameAt = 0;
|
|
495
505
|
private lastMeaningfulFrameAt = 0;
|
|
496
506
|
private streamHealthFail?: (error: Error) => void;
|
|
507
|
+
/** Time source for the T04 watchdog only; the globals unless a test injects one. */
|
|
508
|
+
private readonly streamHealthClock: CursorStreamHealthClock;
|
|
497
509
|
private committed = false;
|
|
498
510
|
private expectedClose = false;
|
|
499
511
|
/**
|
|
@@ -542,6 +554,7 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
542
554
|
// so the transport-level race test can drive it deterministically.
|
|
543
555
|
this.clientToolFinalizeGraceMs = input.clientToolFinalizeGraceMs ?? CLIENT_TOOL_FINALIZE_GRACE_MS;
|
|
544
556
|
this.activeClientToolFinalizeGraceMs = this.clientToolFinalizeGraceMs;
|
|
557
|
+
this.streamHealthClock = input.streamHealthClock ?? REAL_STREAM_HEALTH_CLOCK;
|
|
545
558
|
// Desktop (computer-use / record-screen) executors are available even with no MCP servers.
|
|
546
559
|
this.desktopDeps = desktopDepsFromConfig(input.provider.desktopExecutor);
|
|
547
560
|
this.execContext = {
|
|
@@ -847,7 +860,7 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
847
860
|
|
|
848
861
|
private clearStreamHealthTimer(): void {
|
|
849
862
|
if (this.streamHealthTimer) {
|
|
850
|
-
clearTimeout(this.streamHealthTimer);
|
|
863
|
+
this.streamHealthClock.clearTimeout(this.streamHealthTimer);
|
|
851
864
|
this.streamHealthTimer = undefined;
|
|
852
865
|
}
|
|
853
866
|
this.streamHealthFail = undefined;
|
|
@@ -858,24 +871,28 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
858
871
|
* failAndClear; the timer owns nothing else. Never armed before the first decoded
|
|
859
872
|
* frame (the first-frame timer covers dial + first response), and disarmed by
|
|
860
873
|
* every settle / expected-close path alongside the other timers.
|
|
874
|
+
*
|
|
875
|
+
* Both clocks and the timer go through `streamHealthClock`, which is the globals in
|
|
876
|
+
* production. The `elapsedMs` diagnostic below deliberately stays on the wall clock,
|
|
877
|
+
* because `turnStartedAt` is stamped there and the pair has to subtract coherently.
|
|
861
878
|
*/
|
|
862
879
|
private armStreamHealthTimer(fail: (error: Error) => void): void {
|
|
863
|
-
if (this.streamHealthTimer) clearTimeout(this.streamHealthTimer);
|
|
880
|
+
if (this.streamHealthTimer) this.streamHealthClock.clearTimeout(this.streamHealthTimer);
|
|
864
881
|
if (this.expectedClose) return;
|
|
865
882
|
this.streamHealthFail = fail;
|
|
866
883
|
const silenceMs = this.input.streamSilenceFailMs ?? CURSOR_STREAM_SILENCE_FAIL_MS;
|
|
867
884
|
const heartbeatOnlyMs = this.input.streamHeartbeatOnlyFailMs ?? CURSOR_STREAM_HEARTBEAT_ONLY_FAIL_MS;
|
|
868
|
-
const now =
|
|
885
|
+
const now = this.streamHealthClock.now();
|
|
869
886
|
const deadline = Math.min(
|
|
870
887
|
this.lastInboundFrameAt + silenceMs,
|
|
871
888
|
this.lastMeaningfulFrameAt + heartbeatOnlyMs,
|
|
872
889
|
);
|
|
873
|
-
this.streamHealthTimer = setTimeout(() => {
|
|
890
|
+
this.streamHealthTimer = this.streamHealthClock.setTimeout(() => {
|
|
874
891
|
this.streamHealthTimer = undefined;
|
|
875
892
|
const failFn = this.streamHealthFail;
|
|
876
893
|
if (!failFn || this.expectedClose) return;
|
|
877
|
-
const stalledFor =
|
|
878
|
-
const meaningfulStalledFor =
|
|
894
|
+
const stalledFor = this.streamHealthClock.now() - this.lastInboundFrameAt;
|
|
895
|
+
const meaningfulStalledFor = this.streamHealthClock.now() - this.lastMeaningfulFrameAt;
|
|
879
896
|
if (stalledFor < silenceMs && meaningfulStalledFor < heartbeatOnlyMs) {
|
|
880
897
|
// A frame landed between arming and firing — re-arm for the fresh deadline.
|
|
881
898
|
this.armStreamHealthTimer(failFn);
|
|
@@ -906,7 +923,7 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
906
923
|
* heartbeat-only threshold.
|
|
907
924
|
*/
|
|
908
925
|
private noteInboundFrame(livenessOnly: boolean): void {
|
|
909
|
-
const now =
|
|
926
|
+
const now = this.streamHealthClock.now();
|
|
910
927
|
this.lastInboundFrameAt = now;
|
|
911
928
|
if (!livenessOnly) this.lastMeaningfulFrameAt = now;
|
|
912
929
|
if (this.streamHealthFail) this.armStreamHealthTimer(this.streamHealthFail);
|
|
@@ -1288,7 +1305,7 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
1288
1305
|
const decodedUpdate = decoded.message.case === "interactionUpdate" ? decoded.message.value.message?.case : undefined;
|
|
1289
1306
|
const livenessOnly = decodedUpdate === "heartbeat" || decoded.message.case === "conversationCheckpointUpdate";
|
|
1290
1307
|
if (!this.streamHealthFail) {
|
|
1291
|
-
const now =
|
|
1308
|
+
const now = this.streamHealthClock.now();
|
|
1292
1309
|
this.lastInboundFrameAt = now;
|
|
1293
1310
|
this.lastMeaningfulFrameAt = now;
|
|
1294
1311
|
this.streamHealthFail = failAndClear;
|
|
@@ -476,7 +476,13 @@ function resolveCursorCheckpoint(
|
|
|
476
476
|
if (cursorCheckpointModelAffinityId(snapshot.modelId) !== cursorCheckpointModelAffinityId(request.modelId)) {
|
|
477
477
|
return { reason: "model_changed" };
|
|
478
478
|
}
|
|
479
|
-
|
|
479
|
+
// The continuation state only exists on the ref path; a ref-less prefix hit carries
|
|
480
|
+
// the suspension on the snapshot itself, or a non-toolResult request could resume
|
|
481
|
+
// bytes upstream serialized mid-tool-call.
|
|
482
|
+
if (
|
|
483
|
+
parsed.context.messages.at(-1)?.role !== "toolResult"
|
|
484
|
+
&& (snapshot.toolSuspended === true || cursorState?.checkpointUsable === false)
|
|
485
|
+
) {
|
|
480
486
|
return { reason: "trailing_tool_result" };
|
|
481
487
|
}
|
|
482
488
|
const lineage = lineageMismatch(parsed, snapshot);
|
|
@@ -55,6 +55,25 @@ export interface CursorTransportFactoryInput {
|
|
|
55
55
|
* client thread. Distinct from the per-transport native-exec/shell owner.
|
|
56
56
|
*/
|
|
57
57
|
sessionId?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Test-only clock for the T04 inbound stream-health watchdog. Production omits it and the
|
|
60
|
+
* transport uses the global timers; injecting here must not change scheduling semantics.
|
|
61
|
+
*
|
|
62
|
+
* It exists because the watchdog's contract — a meaningful frame re-arms both deadlines — can
|
|
63
|
+
* only be asserted against real timers by keeping a synthetic server delivering frames faster
|
|
64
|
+
* than the silence budget for several multiples of it. That is an assertion about how busy the
|
|
65
|
+
* machine is, and it failed in the unsharded macOS lane for exactly that reason while the
|
|
66
|
+
* watchdog was working correctly. With the clock injected, virtual time advances only where
|
|
67
|
+
* the test says it does, so no amount of runner contention can expire a deadline.
|
|
68
|
+
*/
|
|
69
|
+
streamHealthClock?: CursorStreamHealthClock;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The three time primitives the T04 watchdog uses. Production binds all three to the globals. */
|
|
73
|
+
export interface CursorStreamHealthClock {
|
|
74
|
+
now(): number;
|
|
75
|
+
setTimeout(callback: () => void, ms: number): ReturnType<typeof setTimeout>;
|
|
76
|
+
clearTimeout(timer: ReturnType<typeof setTimeout>): void;
|
|
58
77
|
}
|
|
59
78
|
|
|
60
79
|
export type CursorTransportFactory = (input: CursorTransportFactoryInput) => CursorTransport;
|
package/src/adapters/cursor.ts
CHANGED
|
@@ -254,6 +254,7 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
254
254
|
coveredMessageCount,
|
|
255
255
|
prefixDigest: cursorCoveredPrefixDigest(_parsed, coveredMessageCount),
|
|
256
256
|
systemDigest: cursorInstructionDigest(_parsed),
|
|
257
|
+
toolSuspended: toolSuspendedCommit,
|
|
257
258
|
});
|
|
258
259
|
if (!checkpointRef) return;
|
|
259
260
|
if (previousRef && previousRef !== checkpointRef) invalidateCursorCheckpoint(previousRef);
|
|
@@ -263,7 +264,8 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
263
264
|
...(_parsed._providerContinuation?.cursor ?? {}),
|
|
264
265
|
conversationId: activeRequest.conversationId,
|
|
265
266
|
// A tool-suspended checkpoint is only usable by the immediate trailing-toolResult
|
|
266
|
-
// continuation; the request-builder guard keys on checkpointUsable=false
|
|
267
|
+
// continuation; the request-builder guard keys on this checkpointUsable=false and
|
|
268
|
+
// the snapshot's persisted toolSuspended flag for that.
|
|
267
269
|
checkpointUsable: !toolSuspendedCommit,
|
|
268
270
|
checkpointRef,
|
|
269
271
|
},
|
|
@@ -1055,6 +1055,8 @@ export interface CloudChatRequest {
|
|
|
1055
1055
|
catalog?: CacheEntry | null;
|
|
1056
1056
|
/** Abort signal — closes the fetch stream. */
|
|
1057
1057
|
signal?: AbortSignal;
|
|
1058
|
+
/** Executor for the inference POST only; catalog and JWT RPCs retain their own transport. */
|
|
1059
|
+
executor?: typeof globalThis.fetch;
|
|
1058
1060
|
}
|
|
1059
1061
|
|
|
1060
1062
|
export class CloudChatError extends Error {
|
|
@@ -1216,7 +1218,7 @@ export async function* streamChatEvents(req: CloudChatRequest): AsyncGenerator<C
|
|
|
1216
1218
|
|
|
1217
1219
|
let resp: Response;
|
|
1218
1220
|
try {
|
|
1219
|
-
resp = await fetch(`${host}/exa.api_server_pb.ApiServerService/GetChatMessage`, {
|
|
1221
|
+
resp = await (req.executor ?? globalThis.fetch)(`${host}/exa.api_server_pb.ApiServerService/GetChatMessage`, {
|
|
1220
1222
|
method: 'POST',
|
|
1221
1223
|
headers: {
|
|
1222
1224
|
'Content-Type': 'application/connect+proto',
|