@bitkyc08/opencodex 2.42.0 → 2.43.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/AGENTS_INSTALL.md +2 -2
- package/README.md +31 -0
- package/bin/ocx.mjs +10 -1
- package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
- package/gui/dist/assets/index-Djowl68T.js +112 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-codec.ts +304 -0
- package/src/adapters/anthropic-image-normalize.ts +8 -298
- package/src/adapters/anthropic.ts +30 -7
- package/src/adapters/command-code.ts +7 -5
- package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
- package/src/adapters/cursor/images.ts +36 -6
- package/src/adapters/cursor/live-transport.ts +7 -2
- package/src/adapters/cursor/native-exec-desktop.ts +2 -15
- package/src/adapters/cursor/protobuf-request.ts +13 -3
- package/src/adapters/cursor/tool-definitions.ts +5 -670
- package/src/adapters/cursor/tool-guidance.ts +236 -0
- package/src/adapters/cursor/tool-naming.ts +252 -0
- package/src/adapters/cursor/tool-schemas.ts +195 -0
- package/src/adapters/cursor/types.ts +6 -3
- package/src/adapters/exec-tool-result-normalize.ts +1 -1
- package/src/adapters/google-errors.ts +9 -1
- package/src/adapters/google.ts +1 -0
- package/src/adapters/kiro-calibration.ts +181 -0
- package/src/adapters/kiro.ts +135 -3
- package/src/adapters/openai-responses.ts +216 -26
- package/src/adapters/responses-code-mode.ts +59 -0
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/adapters/xai-schema-analysis.ts +86 -0
- package/src/adapters/xai-tool-schema.ts +2 -87
- package/src/adapters/xai-web-search.ts +1 -1
- package/src/bridge.ts +22 -10
- package/src/chat/inbound.ts +11 -3
- package/src/claude/inbound-content-options.ts +60 -0
- package/src/claude/inbound-model-options.ts +142 -0
- package/src/claude/inbound-records.ts +7 -0
- package/src/claude/inbound.ts +10 -202
- package/src/claude/model-info.ts +45 -0
- package/src/cli/account-auth.ts +21 -6
- package/src/cli/capabilities.ts +13 -2
- package/src/cli/claude.ts +232 -39
- package/src/cli/config-command.ts +9 -1
- package/src/cli/dispatch.ts +5 -1
- package/src/cli/doctor.ts +10 -0
- package/src/cli/effort.ts +372 -0
- package/src/cli/export-command.ts +3 -9
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +13 -0
- package/src/cli/init.ts +4 -0
- package/src/cli/model-selection-guidance.ts +30 -0
- package/src/cli/models-runtime.ts +3 -2
- package/src/cli/models.ts +8 -3
- package/src/cli/opencode.ts +4 -1
- package/src/cli/provider-runtime.ts +65 -0
- package/src/cli/provider.ts +8 -0
- package/src/cli/registry.ts +16 -2
- package/src/cli/runtime-api.ts +3 -1
- package/src/cli/star-prompt.ts +22 -6
- package/src/cli/status-probes.ts +168 -0
- package/src/cli/status.ts +5 -168
- package/src/clients/config-export/constants.ts +69 -0
- package/src/clients/config-export/contracts.ts +154 -0
- package/src/clients/config-export/dsh.ts +132 -0
- package/src/clients/config-export/fast-models.ts +29 -0
- package/src/clients/config-export/mcode.ts +83 -0
- package/src/clients/config-export/model-metadata.ts +108 -0
- package/src/clients/config-export/omp.ts +104 -0
- package/src/clients/config-export/zcode.ts +92 -0
- package/src/clients/config-export.ts +18 -710
- package/src/codex/account-lifecycle.ts +20 -3
- package/src/codex/account-usability.ts +2 -0
- package/src/codex/auth-api.ts +170 -23
- package/src/codex/auth-context.ts +328 -24
- package/src/codex/catalog/effort.ts +30 -4
- package/src/codex/catalog/metadata.ts +39 -9
- package/src/codex/catalog/native-models.ts +53 -12
- package/src/codex/catalog/parsing.ts +119 -5
- package/src/codex/catalog/provider-fetch.ts +150 -20
- package/src/codex/catalog/reserve.ts +52 -0
- package/src/codex/catalog/sync.ts +89 -16
- package/src/codex/convergence-types.ts +1 -0
- package/src/codex/convergence.ts +2 -0
- package/src/codex/data/upstream-models.json +1 -1
- package/src/codex/forward-transport-headers.ts +25 -0
- package/src/codex/inject.ts +3 -28
- package/src/codex/legacy-config-keys.ts +68 -0
- package/src/codex/log-guard/inspect-schema.ts +137 -0
- package/src/codex/log-guard/inspect.ts +2 -134
- package/src/codex/loopback-target.ts +54 -0
- package/src/codex/main-account-cache.ts +63 -1
- package/src/codex/main-account-hard-lock.ts +52 -0
- package/src/codex/main-account.ts +3 -1
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/model-entitlements.ts +54 -4
- package/src/codex/project-config-warnings.ts +92 -2
- package/src/codex/prompt-layers/encoding.ts +80 -0
- package/src/codex/prompt-layers/paths.ts +54 -0
- package/src/codex/prompt-layers/revision.ts +55 -0
- package/src/codex/prompt-layers/toml-edit.ts +163 -0
- package/src/codex/prompt-layers/toml-read.ts +181 -0
- package/src/codex/prompt-layers.ts +14 -520
- package/src/codex/quota-auto-refresh-state.ts +16 -0
- package/src/codex/quota-auto-refresh.ts +219 -0
- package/src/codex/quota-types.ts +51 -0
- package/src/codex/quota.ts +252 -93
- package/src/codex/reserve-availability.ts +177 -0
- package/src/codex/routing.ts +28 -9
- package/src/codex/shim.ts +53 -11
- package/src/codex/subagent-model-fallback.ts +23 -3
- package/src/combos/failover.ts +125 -7
- package/src/combos/identifiers.ts +89 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/resolve.ts +80 -9
- package/src/combos/types.ts +20 -93
- package/src/config/subagent-models.ts +24 -0
- package/src/config.ts +156 -13
- package/src/generated/compatibility-version.json +454 -166
- package/src/generated/model-metadata.ts +1 -1
- package/src/integrations/journal.ts +65 -4
- package/src/integrations/store.ts +5 -0
- package/src/lab/events/limits.ts +4 -0
- package/src/lib/destination-policy.ts +31 -2
- package/src/lib/errors.ts +39 -0
- package/src/lib/provider-outbound.ts +69 -3
- package/src/lib/proxy-env.ts +22 -0
- package/src/lib/redact-folding.ts +176 -0
- package/src/lib/redact.ts +2 -175
- package/src/lib/state-store-sweeper.ts +20 -6
- package/src/lib/token-estimate.ts +94 -27
- package/src/lib/windows-user-principal.ts +53 -5
- package/src/oauth/anthropic-routing.ts +99 -3
- package/src/oauth/generic-account-failover.ts +36 -13
- package/src/oauth/index.ts +127 -26
- package/src/oauth/login-cli.ts +5 -0
- package/src/oauth/meta-muse.ts +117 -15
- package/src/oauth/pool-settings-capability.ts +15 -4
- package/src/providers/api-keys.ts +8 -10
- package/src/providers/default-aliases.ts +39 -0
- package/src/providers/derive.ts +10 -2
- package/src/providers/fastwire.ts +36 -7
- package/src/providers/initial-model-selection-runtime.ts +90 -0
- package/src/providers/initial-model-selection.ts +120 -0
- package/src/providers/key-failover.ts +134 -54
- package/src/providers/key-store.ts +11 -1
- package/src/providers/label.ts +1 -1
- package/src/providers/model-discovery.ts +76 -0
- package/src/providers/model-rename-startup.ts +72 -8
- package/src/providers/openai-sidecar.ts +17 -5
- package/src/providers/openai-tiers-destination.ts +102 -0
- package/src/providers/openai-tiers.ts +2 -99
- package/src/providers/opencode-go-transport.ts +41 -0
- package/src/providers/quota-key-accounts.ts +141 -0
- package/src/providers/quota-types.ts +9 -0
- package/src/providers/quota.ts +535 -104
- package/src/providers/registry.ts +60 -17
- package/src/providers/xai-responses-opt-in.ts +31 -5
- package/src/quota/reset-activation.ts +81 -0
- package/src/quota/reset-detector.ts +305 -0
- package/src/quota/reset-notify-config.ts +162 -0
- package/src/quota/reset-observer.ts +125 -0
- package/src/quota/reset-poller.ts +160 -0
- package/src/quota/reset-seen-store.ts +385 -0
- package/src/quota/reset-sinks.ts +199 -0
- package/src/quota/window-mapping.ts +106 -0
- package/src/responses/apply-patch-envelope.ts +46 -0
- package/src/responses/code-mode-helper-compat.ts +39 -1
- package/src/responses/custom-tool-compat.ts +10 -4
- package/src/responses/hosted-tool-policy.ts +12 -4
- package/src/responses/parser-content.ts +133 -0
- package/src/responses/parser-text-format.ts +24 -0
- package/src/responses/parser-tools.ts +188 -0
- package/src/responses/parser.ts +3 -326
- package/src/responses/state.ts +124 -28
- package/src/router.ts +48 -13
- package/src/routing/analytics.ts +1 -0
- package/src/routing/capability.ts +17 -4
- package/src/server/auth-cors.ts +7 -1
- package/src/server/background-lifecycle.ts +23 -1
- package/src/server/chat-completions.ts +25 -3
- package/src/server/claude-messages.ts +62 -5
- package/src/server/effort-row.ts +1 -1
- package/src/server/fast-row.ts +295 -0
- package/src/server/gui-static.ts +30 -4
- package/src/server/index.ts +107 -21
- package/src/server/management/agent-settings-routes.ts +2 -2
- package/src/server/management/combo-routes.ts +37 -9
- package/src/server/management/config-routes.ts +93 -2
- package/src/server/management/integration-routes.ts +108 -0
- package/src/server/management/model-routes.ts +13 -3
- package/src/server/management/model-rows.ts +20 -1
- package/src/server/management/native-integration-routes.ts +4 -1
- package/src/server/management/oauth-account-routes.ts +38 -10
- package/src/server/management/provider-routes.ts +34 -3
- package/src/server/management/quota-reset-routes.ts +57 -0
- package/src/server/management/route-registry.ts +7 -4
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/system-routes.ts +3 -2
- package/src/server/management-api.ts +14 -2
- package/src/server/ports.ts +12 -2
- package/src/server/relay-eager.ts +38 -23
- package/src/server/relay.ts +4 -0
- package/src/server/request-log.ts +6 -0
- package/src/server/responses/codex-ws-correlation.ts +65 -0
- package/src/server/responses/codex-ws-exchange.ts +261 -0
- package/src/server/responses/codex-ws-metadata.ts +134 -0
- package/src/server/responses/codex-ws-pool.ts +162 -0
- package/src/server/responses/codex-ws-request.ts +87 -0
- package/src/server/responses/codex-ws-session.ts +93 -0
- package/src/server/responses/codex-ws-wire.ts +144 -0
- package/src/server/responses/collaboration.ts +41 -1
- package/src/server/responses/compact.ts +105 -12
- package/src/server/responses/core.ts +476 -57
- package/src/server/responses/fetch-helpers.ts +10 -3
- package/src/server/responses/input-admission.ts +16 -9
- package/src/server/responses/responses-field-backfill.ts +1 -1
- package/src/server/responses/ws-upstream.ts +34 -318
- package/src/server/responses-custom-tool-repair.ts +20 -4
- package/src/server/responses-undeclared-tool-guard.ts +100 -8
- package/src/server/safe-response-headers.ts +23 -0
- package/src/server/search.ts +9 -0
- package/src/server/subagent-models-startup.ts +27 -0
- package/src/server/system-env-shell.ts +238 -0
- package/src/server/system-env.ts +7 -234
- package/src/server/ws-bridge.ts +3 -25
- package/src/server/xai-responses-startup.ts +21 -0
- package/src/service-manager-probe.ts +1 -1
- package/src/service.ts +55 -16
- package/src/types/config.ts +108 -12
- package/src/types/provider.ts +36 -7
- package/src/types/tools.ts +26 -1
- package/src/types.ts +2 -0
- package/src/update/notify.ts +8 -2
- package/src/usage/cost.ts +38 -28
- package/src/usage/expected-prices.ts +34 -15
- package/src/usage/log.ts +2 -0
- package/src/usage/model-identity.ts +26 -0
- package/src/usage/summary.ts +15 -1
- package/src/vision/describe.ts +6 -0
- package/src/vision/image-rewrite.ts +108 -0
- package/src/vision/index.ts +19 -306
- package/src/vision/plan.ts +205 -0
- package/src/web-search/executor.ts +6 -0
- package/src/web-search/index.ts +8 -1
- package/gui/dist/assets/index-BU1tE0sr.js +0 -112
- package/gui/dist/assets/index-DL9-iS6J.css +0 -1
package/src/responses/parser.ts
CHANGED
|
@@ -22,9 +22,9 @@ import { extractHostedWebSearch, WEB_SEARCH_TOOL_NAME } from "../web-search/synt
|
|
|
22
22
|
import { buildImageTool, extractHostedImageGeneration, IMAGE_GEN_TOOL_NAME } from "../images/synthetic-tool";
|
|
23
23
|
import { toolSearchDescription, toolSearchParameters } from "./tool-search-compat";
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
25
|
+
import { isObj, inputContentParts, outputTextOf, outputToToolResultContent, toolOutputContainsEncryptedContent } from "./parser-content";
|
|
26
|
+
import { mapToolChoice, buildTools, customToolNamespaces } from "./parser-tools";
|
|
27
|
+
import { parseTextFormat } from "./parser-text-format";
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Wrap a remembered proxy-side signature as provider metadata for a replayed tool call.
|
|
@@ -41,241 +41,7 @@ function replayThoughtSignatureMetadata(
|
|
|
41
41
|
return signature ? { google: { thoughtSignature: signature } } : undefined;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
type InputBlock =
|
|
45
|
-
| { type: "input_text"; text: string }
|
|
46
|
-
| { type: "text"; text: string }
|
|
47
|
-
| { type: "input_image"; image_url?: string; file_id?: string; detail?: string }
|
|
48
|
-
| { type: "input_video"; video_url?: string }
|
|
49
|
-
| { type: "input_file"; file_id?: string; filename?: string; file_data?: string };
|
|
50
|
-
|
|
51
|
-
/** A usable reference string, or undefined. Empty strings and non-strings are not references. */
|
|
52
|
-
function nonEmptyString(value: unknown): string | undefined {
|
|
53
|
-
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function inputContentParts(blocks: unknown): string | OcxContentPart[] {
|
|
57
|
-
if (typeof blocks === "string") return blocks;
|
|
58
|
-
// The catch-all can also hand back a non-array `content` (an object, a number), which would
|
|
59
|
-
// throw at the loop below before any per-block guard runs.
|
|
60
|
-
if (!Array.isArray(blocks)) return [];
|
|
61
|
-
const parts: OcxContentPart[] = [];
|
|
62
|
-
for (const raw of blocks) {
|
|
63
|
-
// A malformed message item fails its strict schema and falls through to inputItemSchema's
|
|
64
|
-
// permissive catch-all, so blocks reaching here are NOT guaranteed to match the declared
|
|
65
|
-
// shape. Validate each field before use, as outputToToolResultContent already does.
|
|
66
|
-
if (!isObj(raw)) continue;
|
|
67
|
-
const block = raw as InputBlock;
|
|
68
|
-
if (block.type === "input_text" || block.type === "text") {
|
|
69
|
-
if (typeof raw.text === "string") parts.push({ type: "text", text: raw.text });
|
|
70
|
-
} else if (block.type === "input_image") {
|
|
71
|
-
const b = block as { image_url?: string; file_id?: string; detail?: string };
|
|
72
|
-
const imageUrl = nonEmptyString(b.image_url);
|
|
73
|
-
const fileId = nonEmptyString(b.file_id);
|
|
74
|
-
const detail = nonEmptyString(b.detail);
|
|
75
|
-
if (imageUrl) {
|
|
76
|
-
// Preserve the image as a structured part — adapters send it as a native image block.
|
|
77
|
-
// NEVER inline the (often base64 data-URL) image_url as text: that explodes the token count.
|
|
78
|
-
parts.push({ type: "image", imageUrl, ...(detail ? { detail: normalizeImageDetail(detail) } : {}) });
|
|
79
|
-
} else if (fileId) {
|
|
80
|
-
parts.push({ type: "text", text: `[image: ${fileId}]` }); // file_id ref → no inline data
|
|
81
|
-
}
|
|
82
|
-
// No usable reference: omit the block. A "[image: ?]" marker would claim an attachment
|
|
83
|
-
// the request never carried, which is worse than dropping malformed input.
|
|
84
|
-
} else if (block.type === "input_video") {
|
|
85
|
-
const videoUrl = nonEmptyString(block.video_url);
|
|
86
|
-
if (videoUrl) parts.push({ type: "video", videoUrl });
|
|
87
|
-
} else if (block.type === "input_file") {
|
|
88
|
-
const b = block as { file_id?: string; filename?: string; file_data?: string };
|
|
89
|
-
const fileId = nonEmptyString(b.file_id);
|
|
90
|
-
const fileData = nonEmptyString(b.file_data);
|
|
91
|
-
const filename = nonEmptyString(b.filename);
|
|
92
|
-
if (fileId) {
|
|
93
|
-
parts.push({ type: "text", text: `[file: ${fileId}]` });
|
|
94
|
-
} else if (fileData) {
|
|
95
|
-
// Inline file_data is often large base64. Preserve only its presence and name, never bytes.
|
|
96
|
-
parts.push({ type: "text", text: filename ? `[file: ${filename}]` : "[file: inline data]" });
|
|
97
|
-
}
|
|
98
|
-
// A bare filename is not a file resource in the Responses schema, so omit it rather than
|
|
99
|
-
// fabricating a "[file: ...]" marker for an attachment that was never sent.
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
// Collapse to a plain string only for a single TEXT part; images must stay structured.
|
|
103
|
-
if (parts.length === 1 && parts[0].type === "text") return parts[0].text;
|
|
104
|
-
return parts;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
type OutputBlock = { type: "output_text"; text: string } | { type: "text"; text: string } | { type: "refusal"; refusal: string };
|
|
108
|
-
|
|
109
|
-
function outputTextOf(blocks: unknown): OcxTextContent[] {
|
|
110
|
-
if (typeof blocks === "string") return blocks.length > 0 ? [{ type: "text", text: blocks }] : [];
|
|
111
|
-
if (!Array.isArray(blocks)) return [];
|
|
112
|
-
const out: OcxTextContent[] = [];
|
|
113
|
-
for (const raw of blocks) {
|
|
114
|
-
// Same catch-all caveat as inputContentParts: validate before use.
|
|
115
|
-
if (!isObj(raw)) continue;
|
|
116
|
-
const b = raw as OutputBlock;
|
|
117
|
-
if (b.type === "output_text" || b.type === "text") {
|
|
118
|
-
if (typeof raw.text === "string") out.push({ type: "text", text: raw.text });
|
|
119
|
-
} else if (b.type === "refusal") {
|
|
120
|
-
if (typeof raw.refusal === "string") out.push({ type: "text", text: `[refusal: ${raw.refusal}]` });
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
return out;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function mapToolChoice(value: unknown): OcxRequestOptions["toolChoice"] {
|
|
127
|
-
if (value === undefined || value === null) return undefined;
|
|
128
|
-
if (value === "auto" || value === "none" || value === "required") return value;
|
|
129
|
-
if (isObj(value) && "type" in value) {
|
|
130
|
-
const t = (value as { type: string }).type;
|
|
131
|
-
if ((t === "function" || t === "custom") && "name" in value) {
|
|
132
|
-
return { name: (value as { name: string }).name };
|
|
133
|
-
}
|
|
134
|
-
// Hosted image tool types (with or without a name) map to the synthetic image_gen wire name.
|
|
135
|
-
if (t === "image_generation" || t === "image_gen") {
|
|
136
|
-
return { name: IMAGE_GEN_TOOL_NAME };
|
|
137
|
-
}
|
|
138
|
-
if (t === "allowed_tools" && Array.isArray(value.tools)) {
|
|
139
|
-
const names = value.tools
|
|
140
|
-
.map(allowedToolName)
|
|
141
|
-
.filter((name): name is string => Boolean(name));
|
|
142
|
-
return names.length > 0
|
|
143
|
-
? { allowedTools: [...new Set(names)], mode: value.mode === "required" ? "required" : "auto" }
|
|
144
|
-
: "none";
|
|
145
|
-
}
|
|
146
|
-
return "auto";
|
|
147
|
-
}
|
|
148
|
-
return undefined;
|
|
149
|
-
}
|
|
150
44
|
|
|
151
|
-
function allowedToolName(tool: unknown): string | undefined {
|
|
152
|
-
if (!isObj(tool)) return undefined;
|
|
153
|
-
if (typeof tool.name === "string" && tool.name.length > 0) return tool.name;
|
|
154
|
-
if (tool.type === "web_search" || tool.type === "web_search_preview") return WEB_SEARCH_TOOL_NAME;
|
|
155
|
-
if (tool.type === "image_generation" || tool.type === "image_gen") return IMAGE_GEN_TOOL_NAME;
|
|
156
|
-
if (tool.type === "tool_search") return "tool_search";
|
|
157
|
-
return undefined;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function buildTools(tools: unknown[] | undefined): OcxTool[] | undefined {
|
|
161
|
-
if (!tools) return undefined;
|
|
162
|
-
const out: OcxTool[] = [];
|
|
163
|
-
const normalizeParameters = (raw: unknown): Record<string, unknown> => {
|
|
164
|
-
if (isObj(raw) && raw.type === "object") return raw;
|
|
165
|
-
return { ...(isObj(raw) ? raw : {}), type: "object" };
|
|
166
|
-
};
|
|
167
|
-
const pushFn = (t: Record<string, unknown>, namespace?: string) => {
|
|
168
|
-
// Hosted image_generation already installed the synthetic root tool. A later
|
|
169
|
-
// ordinary root `image_gen` must not create a second un-namespaced identity.
|
|
170
|
-
if (
|
|
171
|
-
!namespace
|
|
172
|
-
&& t.name === IMAGE_GEN_TOOL_NAME
|
|
173
|
-
&& out.some(tool => tool.name === IMAGE_GEN_TOOL_NAME && !tool.namespace && tool.imageGeneration)
|
|
174
|
-
) {
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
const tool: OcxTool = {
|
|
178
|
-
name: t.name as string,
|
|
179
|
-
description: (t.description as string) ?? "",
|
|
180
|
-
parameters: normalizeParameters(t.parameters),
|
|
181
|
-
};
|
|
182
|
-
if (t.strict !== undefined) tool.strict = t.strict as boolean;
|
|
183
|
-
if (namespace) tool.namespace = namespace;
|
|
184
|
-
out.push(tool);
|
|
185
|
-
};
|
|
186
|
-
const pushCustom = (t: Record<string, unknown>, namespace?: string) => {
|
|
187
|
-
// Hosted image_generation already installed the synthetic root tool. A later
|
|
188
|
-
// root custom `image_gen` would collide on the same wire name with a different
|
|
189
|
-
// `freeform` flag and throw `ambiguous tool catalog`.
|
|
190
|
-
if (
|
|
191
|
-
!namespace
|
|
192
|
-
&& t.name === IMAGE_GEN_TOOL_NAME
|
|
193
|
-
&& out.some(tool => tool.name === IMAGE_GEN_TOOL_NAME && !tool.namespace && tool.imageGeneration)
|
|
194
|
-
) {
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
// Freeform custom tools are lowered to a single string `input` because chat models cannot
|
|
198
|
-
// emit Responses grammar payloads directly. Keep tool-specific input guidance scoped to the
|
|
199
|
-
// tool that owns it: leaking apply_patch syntax into `exec` or another freeform tool teaches
|
|
200
|
-
// routed models that the nested helper name is itself a callable top-level tool.
|
|
201
|
-
const inputDescription = t.name === "apply_patch"
|
|
202
|
-
? "Raw tool input. For apply_patch, begin exactly with `*** Begin Patch` (no trailing `***`), then use its standard patch envelope."
|
|
203
|
-
: "Raw freeform input for this tool.";
|
|
204
|
-
const tool: OcxTool = {
|
|
205
|
-
name: t.name as string,
|
|
206
|
-
description: (t.description as string) ?? "",
|
|
207
|
-
parameters: { type: "object", properties: { input: { type: "string", description: inputDescription } }, required: ["input"] },
|
|
208
|
-
freeform: true,
|
|
209
|
-
};
|
|
210
|
-
if (namespace) tool.namespace = namespace;
|
|
211
|
-
out.push(tool);
|
|
212
|
-
};
|
|
213
|
-
for (const t of tools) {
|
|
214
|
-
if (!isObj(t)) continue;
|
|
215
|
-
if (t.type === "function" && isObj(t.function) && typeof t.function.name === "string" && t.function.name.length > 0) {
|
|
216
|
-
pushFn(t.function as Record<string, unknown>);
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
if (t.type === "function" && typeof t.name === "string") {
|
|
220
|
-
pushFn(t);
|
|
221
|
-
} else if (t.type === "namespace" && Array.isArray(t.tools)) {
|
|
222
|
-
// Codex 0.147 groups its ordinary client tools under the reserved `functions` namespace,
|
|
223
|
-
// including freeform custom tools such as code-mode `exec`. Those children are still
|
|
224
|
-
// top-level Responses tools, so flatten them without a namespace. Other namespace groups
|
|
225
|
-
// are MCP-style and keep their namespace for round-trip routing.
|
|
226
|
-
const builtinFunctions = t.name === "functions";
|
|
227
|
-
const ns = typeof t.name === "string" && !builtinFunctions ? t.name : undefined;
|
|
228
|
-
for (const inner of t.tools as unknown[]) {
|
|
229
|
-
if (isObj(inner) && inner.type === "function" && typeof inner.name === "string") pushFn(inner, ns);
|
|
230
|
-
else if (isObj(inner) && inner.type === "custom" && typeof inner.name === "string") pushCustom(inner, ns);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
else if (t.type === "custom" && typeof t.name === "string") {
|
|
234
|
-
pushCustom(t);
|
|
235
|
-
}
|
|
236
|
-
else if (t.type === "tool_search") {
|
|
237
|
-
// Client-executed tool discovery — the gateway to deferred tools (subagents, extra MCP tools).
|
|
238
|
-
// Expose as a function so chat models can call it; the bridge relays it as a tool_search_call.
|
|
239
|
-
out.push({
|
|
240
|
-
name: "tool_search",
|
|
241
|
-
description: toolSearchDescription(t),
|
|
242
|
-
parameters: normalizeParameters(toolSearchParameters(t)),
|
|
243
|
-
toolSearch: true,
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
else if (t.type === "image_generation" || t.type === "image_gen") {
|
|
247
|
-
// Keep Codex's image_gen visible to routed chat models. The hosted OpenAI tool
|
|
248
|
-
// cannot execute on Grok; the model still has to see a callable image_gen so
|
|
249
|
-
// Codex's client-side /v1/images request can fire and be relayed to xAI.
|
|
250
|
-
// Identity is the un-namespaced synthetic root (`imageGeneration: true`), not
|
|
251
|
-
// the bare name: a namespaced ordinary `image_gen` must not suppress it.
|
|
252
|
-
const synthetic = buildImageTool();
|
|
253
|
-
// Every un-namespaced `image_gen` collides on one wire name, so removing only
|
|
254
|
-
// the first leaves a second root behind and the catalog stays ambiguous.
|
|
255
|
-
// Drop all root collisions, keep namespaced entries, then insert exactly one
|
|
256
|
-
// synthetic root — at the earliest colliding position so declaration order is
|
|
257
|
-
// preserved for models that read the catalog positionally.
|
|
258
|
-
let insertAt = -1;
|
|
259
|
-
for (let i = out.length - 1; i >= 0; i -= 1) {
|
|
260
|
-
const tool = out[i]!;
|
|
261
|
-
if (tool.name !== IMAGE_GEN_TOOL_NAME || tool.namespace) continue;
|
|
262
|
-
out.splice(i, 1);
|
|
263
|
-
insertAt = i;
|
|
264
|
-
}
|
|
265
|
-
if (insertAt >= 0) out.splice(insertAt, 0, synthetic);
|
|
266
|
-
else out.push(synthetic);
|
|
267
|
-
}
|
|
268
|
-
else if (typeof t.name === "string" && t.type !== "web_search" && t.type !== "image_generation") {
|
|
269
|
-
// Any OTHER named tool (e.g. a native/computer-use tool type opencodex doesn't explicitly
|
|
270
|
-
// model) is client-executed — pass it through as a function so the routed model can read and
|
|
271
|
-
// call it naturally; the bridge relays its call as a function_call. Previously such tools were
|
|
272
|
-
// silently dropped, so the model never saw them.
|
|
273
|
-
pushFn(t);
|
|
274
|
-
}
|
|
275
|
-
// Hosted web_search is still dropped here — the web-search sidecar re-injects it.
|
|
276
|
-
}
|
|
277
|
-
return out.length > 0 ? out : undefined;
|
|
278
|
-
}
|
|
279
45
|
|
|
280
46
|
function ensureAssistantPlaceholder(messages: OcxMessage[], modelId: string, now: number): OcxAssistantMessage {
|
|
281
47
|
const last = messages[messages.length - 1];
|
|
@@ -285,45 +51,6 @@ function ensureAssistantPlaceholder(messages: OcxMessage[], modelId: string, now
|
|
|
285
51
|
return placeholder;
|
|
286
52
|
}
|
|
287
53
|
|
|
288
|
-
/**
|
|
289
|
-
* Tool-call output content. Preserves images (e.g. Codex `view_image` returns
|
|
290
|
-
* `input_image` items): returns content parts when any image is present, else a plain joined string.
|
|
291
|
-
* Never inlines an image_url as text (that would explode the token count).
|
|
292
|
-
*/
|
|
293
|
-
function outputToToolResultContent(output: string | unknown[] | undefined): string | OcxContentPart[] {
|
|
294
|
-
if (typeof output === "string") return output;
|
|
295
|
-
if (!Array.isArray(output)) return "";
|
|
296
|
-
const parts: OcxContentPart[] = [];
|
|
297
|
-
let hasImage = false;
|
|
298
|
-
for (const raw of output) {
|
|
299
|
-
if (!isObj(raw)) continue;
|
|
300
|
-
if (raw.type === "output_text" || raw.type === "text" || raw.type === "input_text") {
|
|
301
|
-
if (typeof raw.text === "string") parts.push({ type: "text", text: raw.text });
|
|
302
|
-
} else if (raw.type === "refusal" && typeof raw.refusal === "string") {
|
|
303
|
-
parts.push({ type: "text", text: `[refusal: ${raw.refusal}]` });
|
|
304
|
-
} else if (raw.type === "input_image" && typeof raw.image_url === "string") {
|
|
305
|
-
parts.push({ type: "image", imageUrl: raw.image_url, ...(typeof raw.detail === "string" ? { detail: normalizeImageDetail(raw.detail) } : {}) });
|
|
306
|
-
hasImage = true;
|
|
307
|
-
} else if (raw.type === "encrypted_content") {
|
|
308
|
-
// codex-rs FunctionCallOutputContentItem::EncryptedContent — opaque to routed models.
|
|
309
|
-
parts.push({ type: "text", text: "[encrypted content omitted]" });
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
if (!hasImage) return parts.map(p => (p.type === "text" ? p.text : "")).join("");
|
|
313
|
-
return parts;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function toolOutputContainsEncryptedContent(output: string | unknown[] | undefined): boolean {
|
|
317
|
-
return Array.isArray(output) && output.some(raw => isObj(raw) && raw.type === "encrypted_content");
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* codex-rs ImageDetail allows "original", but chat-completions providers only accept
|
|
322
|
-
* auto|low|high on image_url.detail — degrade "original" to "high" (the codex default).
|
|
323
|
-
*/
|
|
324
|
-
function normalizeImageDetail(detail: string): string {
|
|
325
|
-
return detail === "original" ? "high" : detail;
|
|
326
|
-
}
|
|
327
54
|
|
|
328
55
|
function findToolById(messages: OcxMessage[], callId: string): { name: string; namespace?: string } {
|
|
329
56
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
@@ -366,34 +93,6 @@ function attachPendingReasoningToCallOwner(
|
|
|
366
93
|
|
|
367
94
|
const REASONING_EFFORTS = new Set(["none", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
368
95
|
|
|
369
|
-
/**
|
|
370
|
-
* Namespace a custom tool was declared under, by its bare name.
|
|
371
|
-
*
|
|
372
|
-
* A `custom_tool_call` echoed back by the client carries only the bare name — the bridge
|
|
373
|
-
* emits `{"type":"custom_tool_call","name":"exec"}` even when the tool was declared as
|
|
374
|
-
* `mcp__functions__exec`. Without this lookup the namespace is lost on the return trip,
|
|
375
|
-
* and the adapters replay history through `namespacedToolName(namespace, name)`, which
|
|
376
|
-
* then produces a bare `exec` the provider may not have. Ordinary `function_call` items
|
|
377
|
-
* do not need this: they carry `namespace` on the wire.
|
|
378
|
-
*/
|
|
379
|
-
function customToolNamespaces(tools: unknown): Map<string, string> {
|
|
380
|
-
const out = new Map<string, string>();
|
|
381
|
-
if (!Array.isArray(tools)) return out;
|
|
382
|
-
for (const spec of tools) {
|
|
383
|
-
if (!isObj(spec) || spec.type !== "namespace" || !Array.isArray(spec.tools)) continue;
|
|
384
|
-
const namespace = typeof spec.name === "string" ? spec.name : undefined;
|
|
385
|
-
// Codex 0.147 groups ordinary client tools under the reserved `functions` namespace and
|
|
386
|
-
// buildTools deliberately flattens those without a namespace. Mirror that here, or the
|
|
387
|
-
// reconstruction would invent a namespace the request never advertised.
|
|
388
|
-
if (!namespace || namespace === "functions") continue;
|
|
389
|
-
for (const inner of spec.tools) {
|
|
390
|
-
if (!isObj(inner) || inner.type !== "custom" || typeof inner.name !== "string") continue;
|
|
391
|
-
// Ambiguous bare names are already rejected upstream, so first declaration wins.
|
|
392
|
-
if (!out.has(inner.name)) out.set(inner.name, namespace);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
return out;
|
|
396
|
-
}
|
|
397
96
|
|
|
398
97
|
export function parseRequest(
|
|
399
98
|
body: unknown,
|
|
@@ -859,25 +558,3 @@ export function parseRequest(
|
|
|
859
558
|
...(contextCompactionBoundary ? { _contextCompactionBoundary: true } : {}),
|
|
860
559
|
};
|
|
861
560
|
}
|
|
862
|
-
|
|
863
|
-
/**
|
|
864
|
-
* The Responses `text.format` object when it requests structured output (json_schema or
|
|
865
|
-
* json_object), undefined otherwise. Acceptance is identical to the boolean detector this
|
|
866
|
-
* replaces; unknown or malformed formats are ignored, never rejected, so the native
|
|
867
|
-
* passthrough keeps forwarding whatever the caller sent via `_rawBody`.
|
|
868
|
-
*/
|
|
869
|
-
function parseTextFormat(text: unknown): OcxRequestOptions["textFormat"] {
|
|
870
|
-
if (!isObj(text)) return undefined;
|
|
871
|
-
const format = (text as { format?: unknown }).format;
|
|
872
|
-
if (!isObj(format)) return undefined;
|
|
873
|
-
const f = format as { type?: unknown; name?: unknown; description?: unknown; schema?: unknown; strict?: unknown };
|
|
874
|
-
if (f.type === "json_object") return { type: "json_object" };
|
|
875
|
-
if (f.type !== "json_schema") return undefined;
|
|
876
|
-
return {
|
|
877
|
-
type: "json_schema",
|
|
878
|
-
...(typeof f.name === "string" ? { name: f.name } : {}),
|
|
879
|
-
...(typeof f.description === "string" ? { description: f.description } : {}),
|
|
880
|
-
...(isObj(f.schema) ? { schema: f.schema as Record<string, unknown> } : {}),
|
|
881
|
-
...(typeof f.strict === "boolean" ? { strict: f.strict } : {}),
|
|
882
|
-
};
|
|
883
|
-
}
|
package/src/responses/state.ts
CHANGED
|
@@ -170,6 +170,77 @@ async function snapshotOnDiskMatches(path: string, payload: string, payloadBytes
|
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
172
|
const spillCounters = { writes: 0, writeFailures: 0, readFailures: 0 };
|
|
173
|
+
|
|
174
|
+
export type ResponseSpillWriteFailureCode =
|
|
175
|
+
| "EACLRETRYEXHAUSTED"
|
|
176
|
+
| "ETIMEDOUT"
|
|
177
|
+
| "EACCES"
|
|
178
|
+
| "ENOSPC"
|
|
179
|
+
| "EFBIG"
|
|
180
|
+
| "EIO"
|
|
181
|
+
| "ECAPACITY"
|
|
182
|
+
| "ELOOP"
|
|
183
|
+
| "EUNKNOWN";
|
|
184
|
+
|
|
185
|
+
export type ResponseSpillWriteStatus = "initial" | "healthy" | "degraded";
|
|
186
|
+
|
|
187
|
+
interface ResponseSpillWriteHealth {
|
|
188
|
+
consecutiveFailures: number;
|
|
189
|
+
lastFailureCode: ResponseSpillWriteFailureCode | null;
|
|
190
|
+
lastFailureAt: number | null;
|
|
191
|
+
lastSuccessAt: number | null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const spillWriteHealth: ResponseSpillWriteHealth = {
|
|
195
|
+
consecutiveFailures: 0,
|
|
196
|
+
lastFailureCode: null,
|
|
197
|
+
lastFailureAt: null,
|
|
198
|
+
lastSuccessAt: null,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Collapse filesystem/runtime errors into a fixed privacy-safe diagnostic union.
|
|
203
|
+
* Messages and paths are deliberately ignored: this projection is returned by the
|
|
204
|
+
* authenticated memory endpoint, and a nested `cause` can contain a username or
|
|
205
|
+
* workspace path even when the public wrapper does not.
|
|
206
|
+
*/
|
|
207
|
+
function classifySpillWriteFailure(error: unknown): ResponseSpillWriteFailureCode {
|
|
208
|
+
let cursor = error;
|
|
209
|
+
for (let depth = 0; depth < 4 && cursor && typeof cursor === "object"; depth += 1) {
|
|
210
|
+
const record = cursor as { code?: unknown; cause?: unknown };
|
|
211
|
+
const code = typeof record.code === "string" ? record.code.toUpperCase() : "";
|
|
212
|
+
switch (code) {
|
|
213
|
+
case "EACLRETRYEXHAUSTED": return "EACLRETRYEXHAUSTED";
|
|
214
|
+
case "ETIMEDOUT": return "ETIMEDOUT";
|
|
215
|
+
case "EACCES":
|
|
216
|
+
case "EPERM": return "EACCES";
|
|
217
|
+
case "ENOSPC":
|
|
218
|
+
case "EDQUOT": return "ENOSPC";
|
|
219
|
+
case "EFBIG": return "EFBIG";
|
|
220
|
+
case "EIO": return "EIO";
|
|
221
|
+
case "ECAPACITY": return "ECAPACITY";
|
|
222
|
+
case "ELOOP": return "ELOOP";
|
|
223
|
+
}
|
|
224
|
+
cursor = record.cause;
|
|
225
|
+
}
|
|
226
|
+
return "EUNKNOWN";
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function noteSpillWriteSuccess(): void {
|
|
230
|
+
spillCounters.writes += 1;
|
|
231
|
+
spillWriteHealth.consecutiveFailures = 0;
|
|
232
|
+
spillWriteHealth.lastSuccessAt = now();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function noteSpillWriteFailure(
|
|
236
|
+
error: unknown,
|
|
237
|
+
override?: ResponseSpillWriteFailureCode,
|
|
238
|
+
): void {
|
|
239
|
+
spillCounters.writeFailures += 1;
|
|
240
|
+
spillWriteHealth.consecutiveFailures += 1;
|
|
241
|
+
spillWriteHealth.lastFailureCode = override ?? classifySpillWriteFailure(error);
|
|
242
|
+
spillWriteHealth.lastFailureAt = now();
|
|
243
|
+
}
|
|
173
244
|
/**
|
|
174
245
|
* Admission-boundary observability (test-visible). directSpills: oversized
|
|
175
246
|
* candidates routed straight to durable spill without a resident stay or
|
|
@@ -346,6 +417,7 @@ async function runPendingResponseSpill(job: PendingResponseSpill): Promise<void>
|
|
|
346
417
|
job.running = true;
|
|
347
418
|
const candidate = job.candidate;
|
|
348
419
|
let ref: ResponseSpillRef | null = null;
|
|
420
|
+
let exhaustedAclRetry = false;
|
|
349
421
|
try {
|
|
350
422
|
const state = spillPayloadForResident(candidate);
|
|
351
423
|
try {
|
|
@@ -357,11 +429,16 @@ async function runPendingResponseSpill(job: PendingResponseSpill): Promise<void>
|
|
|
357
429
|
if (!isAclTimeout(error)) throw error;
|
|
358
430
|
// The ACL helper permits exactly one caller-owned recovery budget. The resident generation
|
|
359
431
|
// remains replayable during both attempts, so a transient timeout never becomes a tombstone.
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
432
|
+
try {
|
|
433
|
+
ref = await writeResponseSpillDurablyAsync(job.id, state, {
|
|
434
|
+
aclBudgetMs: responseSpillAsyncAclAttemptBudgetMs(),
|
|
435
|
+
retryTimedOutOnce: true,
|
|
436
|
+
publicationControl: job.publicationControl,
|
|
437
|
+
});
|
|
438
|
+
} catch (retryError) {
|
|
439
|
+
exhaustedAclRetry = isAclTimeout(retryError);
|
|
440
|
+
throw retryError;
|
|
441
|
+
}
|
|
365
442
|
}
|
|
366
443
|
if (ref.payloadBytes > responseSpillPayloadCap()) {
|
|
367
444
|
deleteResponseSpill(ref);
|
|
@@ -376,14 +453,14 @@ async function runPendingResponseSpill(job: PendingResponseSpill): Promise<void>
|
|
|
376
453
|
}
|
|
377
454
|
if (swapResidentForSpill(job.id, candidate, ref)) {
|
|
378
455
|
ref = null;
|
|
379
|
-
|
|
456
|
+
noteSpillWriteSuccess();
|
|
380
457
|
if (job.directAdmission) admissionCounters.directSpills += 1;
|
|
381
458
|
deferSupersededSpill(job.supersededSpill);
|
|
382
459
|
}
|
|
383
|
-
} catch {
|
|
460
|
+
} catch (error) {
|
|
384
461
|
if (ref) deleteResponseSpill(ref);
|
|
385
462
|
if (states.get(job.id) === candidate && !job.cancelled) {
|
|
386
|
-
|
|
463
|
+
noteSpillWriteFailure(error, exhaustedAclRetry ? "EACLRETRYEXHAUSTED" : undefined);
|
|
387
464
|
replaceWithSpillFailure(job.id, candidate);
|
|
388
465
|
deferSupersededSpill(job.supersededSpill);
|
|
389
466
|
}
|
|
@@ -406,7 +483,7 @@ function queuePendingResponseSpill(
|
|
|
406
483
|
): void {
|
|
407
484
|
const inheritedSpill = cancelPendingResponseSpill(id) ?? options.supersededSpill;
|
|
408
485
|
if (pendingResponseSpillBytes + candidate.sizeBytes > MAX_PENDING_RESPONSE_SPILL_BYTES) {
|
|
409
|
-
|
|
486
|
+
noteSpillWriteFailure(null, "ECAPACITY");
|
|
410
487
|
replaceWithSpillFailure(id, candidate);
|
|
411
488
|
deferSupersededSpill(inheritedSpill);
|
|
412
489
|
return;
|
|
@@ -424,7 +501,7 @@ function queuePendingResponseSpill(
|
|
|
424
501
|
if (accountedResponseSpillBytes() + footprint + inheritedBytes > spillByteCap()) {
|
|
425
502
|
enforceSpilledResponseBudget();
|
|
426
503
|
if (accountedResponseSpillBytes() + footprint + inheritedBytes > spillByteCap()) {
|
|
427
|
-
|
|
504
|
+
noteSpillWriteFailure(null, "ECAPACITY");
|
|
428
505
|
replaceWithSpillFailure(id, candidate);
|
|
429
506
|
deferSupersededSpill(inheritedSpill);
|
|
430
507
|
return;
|
|
@@ -560,7 +637,7 @@ function installShutdownFallbackSpill(
|
|
|
560
637
|
enforceSpilledResponseBudget();
|
|
561
638
|
if (accountedResponseSpillBytes() + supersededBytes > spillByteCap()) {
|
|
562
639
|
if (states.get(job.id) === candidate) {
|
|
563
|
-
|
|
640
|
+
noteSpillWriteFailure(null, "ECAPACITY");
|
|
564
641
|
replaceWithSpillFailure(job.id, candidate);
|
|
565
642
|
deferSupersededSpill(job.supersededSpill);
|
|
566
643
|
}
|
|
@@ -581,14 +658,14 @@ function installShutdownFallbackSpill(
|
|
|
581
658
|
}
|
|
582
659
|
if (swapResidentForSpill(job.id, candidate, ref)) {
|
|
583
660
|
ref = null;
|
|
584
|
-
|
|
661
|
+
noteSpillWriteSuccess();
|
|
585
662
|
if (job.directAdmission) admissionCounters.directSpills += 1;
|
|
586
663
|
deferSupersededSpill(job.supersededSpill);
|
|
587
664
|
}
|
|
588
665
|
} catch (error) {
|
|
589
666
|
if (ref) deleteResponseSpill(ref);
|
|
590
667
|
if (states.get(job.id) === candidate) {
|
|
591
|
-
|
|
668
|
+
noteSpillWriteFailure(error);
|
|
592
669
|
replaceWithSpillFailure(job.id, candidate);
|
|
593
670
|
deferSupersededSpill(job.supersededSpill);
|
|
594
671
|
}
|
|
@@ -601,9 +678,10 @@ function installShutdownFallbackSpill(
|
|
|
601
678
|
function terminalizeShutdownFallbackCandidate(
|
|
602
679
|
job: PendingResponseSpill,
|
|
603
680
|
candidate: ResidentResponseState,
|
|
681
|
+
failureCode: ResponseSpillWriteFailureCode = "ETIMEDOUT",
|
|
604
682
|
): void {
|
|
605
683
|
if (states.get(job.id) !== candidate) return;
|
|
606
|
-
|
|
684
|
+
noteSpillWriteFailure(null, failureCode);
|
|
607
685
|
replaceWithSpillFailure(job.id, candidate);
|
|
608
686
|
deferSupersededSpill(job.supersededSpill);
|
|
609
687
|
}
|
|
@@ -652,11 +730,11 @@ function stopAtShutdownTerminalizationPassLimit(
|
|
|
652
730
|
failures.push(Object.assign(new Error("Response spill shutdown terminalization pass limit exceeded"), { code: "ELOOP" }));
|
|
653
731
|
supersedeShutdownFallbackBatch(pending, failures);
|
|
654
732
|
for (const { job, candidate } of pending) {
|
|
655
|
-
terminalizeShutdownFallbackCandidate(job, candidate);
|
|
733
|
+
terminalizeShutdownFallbackCandidate(job, candidate, "ELOOP");
|
|
656
734
|
}
|
|
657
735
|
for (const [id, state] of [...states]) {
|
|
658
736
|
if (state.kind !== "resident") continue;
|
|
659
|
-
|
|
737
|
+
noteSpillWriteFailure(null, "ELOOP");
|
|
660
738
|
replaceWithSpillFailure(id, state);
|
|
661
739
|
}
|
|
662
740
|
recomputeOldestResident();
|
|
@@ -986,7 +1064,7 @@ function replaceSpillEntryAtomically(
|
|
|
986
1064
|
deleteResponseSpill(ref);
|
|
987
1065
|
return;
|
|
988
1066
|
}
|
|
989
|
-
|
|
1067
|
+
noteSpillWriteSuccess();
|
|
990
1068
|
noteStubSwapForTest();
|
|
991
1069
|
// The old generation is NOT unlinked here (review C1-1): the new stub is
|
|
992
1070
|
// only durable once the debounced snapshot flushes — a crash before that
|
|
@@ -996,8 +1074,8 @@ function replaceSpillEntryAtomically(
|
|
|
996
1074
|
while (pendingSpillUnlinks.length > PENDING_SPILL_UNLINKS_MAX) {
|
|
997
1075
|
deleteResponseSpill(pendingSpillUnlinks.shift()!);
|
|
998
1076
|
}
|
|
999
|
-
} catch {
|
|
1000
|
-
|
|
1077
|
+
} catch (error) {
|
|
1078
|
+
noteSpillWriteFailure(error);
|
|
1001
1079
|
// deferSpillUnlink: the durable snapshot may still reference the old
|
|
1002
1080
|
// generation; deleting it now would strand the old stub after a crash.
|
|
1003
1081
|
replaceWithSpillFailure(id, expected, { deferSpillUnlink: true });
|
|
@@ -1087,7 +1165,7 @@ function admitOversizedCandidate(
|
|
|
1087
1165
|
deleteResponseSpill(ref);
|
|
1088
1166
|
return;
|
|
1089
1167
|
}
|
|
1090
|
-
|
|
1168
|
+
noteSpillWriteSuccess();
|
|
1091
1169
|
admissionCounters.directSpills += 1;
|
|
1092
1170
|
noteStubSwapForTest();
|
|
1093
1171
|
if (expected?.kind === "spill") {
|
|
@@ -1099,8 +1177,8 @@ function admitOversizedCandidate(
|
|
|
1099
1177
|
deleteResponseSpill(pendingSpillUnlinks.shift()!);
|
|
1100
1178
|
}
|
|
1101
1179
|
}
|
|
1102
|
-
} catch {
|
|
1103
|
-
|
|
1180
|
+
} catch (error) {
|
|
1181
|
+
noteSpillWriteFailure(error);
|
|
1104
1182
|
replaceWithSpillFailure(id, expected, { deferSpillUnlink: true });
|
|
1105
1183
|
}
|
|
1106
1184
|
}
|
|
@@ -1812,9 +1890,9 @@ function pruneResponses(at = now()): void {
|
|
|
1812
1890
|
...(entry.providerOutputStart !== undefined ? { providerOutputStart: entry.providerOutputStart } : {}),
|
|
1813
1891
|
...(entry.providers ? { providers: entry.providers } : {}),
|
|
1814
1892
|
});
|
|
1815
|
-
if (swapResidentForSpill(oldestId, entry, ref))
|
|
1816
|
-
} catch {
|
|
1817
|
-
|
|
1893
|
+
if (swapResidentForSpill(oldestId, entry, ref)) noteSpillWriteSuccess();
|
|
1894
|
+
} catch (error) {
|
|
1895
|
+
noteSpillWriteFailure(error);
|
|
1818
1896
|
replaceWithSpillFailure(oldestId, entry);
|
|
1819
1897
|
}
|
|
1820
1898
|
}
|
|
@@ -1929,9 +2007,9 @@ export function evictOldestResponseContinuationForBudget(): number {
|
|
|
1929
2007
|
...(entry.providerOutputStart !== undefined ? { providerOutputStart: entry.providerOutputStart } : {}),
|
|
1930
2008
|
...(entry.providers ? { providers: entry.providers } : {}),
|
|
1931
2009
|
});
|
|
1932
|
-
if (swapResidentForSpill(id, entry, ref))
|
|
1933
|
-
} catch {
|
|
1934
|
-
|
|
2010
|
+
if (swapResidentForSpill(id, entry, ref)) noteSpillWriteSuccess();
|
|
2011
|
+
} catch (error) {
|
|
2012
|
+
noteSpillWriteFailure(error);
|
|
1935
2013
|
replaceWithSpillFailure(id, entry);
|
|
1936
2014
|
}
|
|
1937
2015
|
schedulePersist();
|
|
@@ -2107,6 +2185,11 @@ export interface ResponseStateMetrics {
|
|
|
2107
2185
|
oldestAgeMs: number;
|
|
2108
2186
|
spillWrites: number;
|
|
2109
2187
|
spillWriteFailures: number;
|
|
2188
|
+
spillWriteStatus: ResponseSpillWriteStatus;
|
|
2189
|
+
spillWriteConsecutiveFailures: number;
|
|
2190
|
+
spillLastWriteFailureCode: ResponseSpillWriteFailureCode | null;
|
|
2191
|
+
spillLastWriteFailureAt: number | null;
|
|
2192
|
+
spillLastWriteSuccessAt: number | null;
|
|
2110
2193
|
spillReadFailures: number;
|
|
2111
2194
|
replayScopeMismatchDrops: number;
|
|
2112
2195
|
}
|
|
@@ -2150,6 +2233,15 @@ export function responseStateMetrics(): ResponseStateMetrics {
|
|
|
2150
2233
|
oldestAgeMs: states.size > 0 ? at - oldestCreatedAt : 0,
|
|
2151
2234
|
spillWrites: spillCounters.writes,
|
|
2152
2235
|
spillWriteFailures: spillCounters.writeFailures,
|
|
2236
|
+
spillWriteStatus: spillWriteHealth.consecutiveFailures > 0
|
|
2237
|
+
? "degraded"
|
|
2238
|
+
: spillWriteHealth.lastSuccessAt !== null
|
|
2239
|
+
? "healthy"
|
|
2240
|
+
: "initial",
|
|
2241
|
+
spillWriteConsecutiveFailures: spillWriteHealth.consecutiveFailures,
|
|
2242
|
+
spillLastWriteFailureCode: spillWriteHealth.lastFailureCode,
|
|
2243
|
+
spillLastWriteFailureAt: spillWriteHealth.lastFailureAt,
|
|
2244
|
+
spillLastWriteSuccessAt: spillWriteHealth.lastSuccessAt,
|
|
2153
2245
|
spillReadFailures: spillCounters.readFailures,
|
|
2154
2246
|
replayScopeMismatchDrops,
|
|
2155
2247
|
};
|
|
@@ -2268,6 +2360,10 @@ export function clearResponseStateMemoryForTests(): void {
|
|
|
2268
2360
|
spillCounters.writes = 0;
|
|
2269
2361
|
spillCounters.writeFailures = 0;
|
|
2270
2362
|
spillCounters.readFailures = 0;
|
|
2363
|
+
spillWriteHealth.consecutiveFailures = 0;
|
|
2364
|
+
spillWriteHealth.lastFailureCode = null;
|
|
2365
|
+
spillWriteHealth.lastFailureAt = null;
|
|
2366
|
+
spillWriteHealth.lastSuccessAt = null;
|
|
2271
2367
|
replayScopeMismatchDrops = 0;
|
|
2272
2368
|
replayOverlapSkips = 0;
|
|
2273
2369
|
persistAttemptHookForTests = null;
|