@bitkyc08/opencodex 2.82.0 → 2.84.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/App-CZtqiGcw.js +56 -0
- package/gui/dist/assets/Tray-C9TGtwad.js +1 -0
- package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +1 -1
- package/gui/dist/assets/index-CTm4l-HQ.js +94 -0
- package/gui/dist/assets/{quota-summary-DTBC8n7-.js → quota-summary-BbEeRfPY.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +2 -2
- package/src/adapters/coding-agent/protocol.ts +15 -8
- package/src/adapters/cursor/catalog.ts +2 -2
- package/src/adapters/cursor/effort-map.ts +1 -1
- package/src/adapters/devin/live-models.ts +2 -2
- package/src/adapters/google-truncation.ts +22 -0
- package/src/adapters/google.ts +7 -3
- package/src/adapters/ollama-native.ts +47 -15
- package/src/adapters/openai-chat/messages.ts +8 -4
- package/src/adapters/openai-responses/empty-assistant.ts +32 -0
- package/src/adapters/openai-responses/passthrough.ts +5 -0
- package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
- package/src/adapters/openai-responses/reasoning.ts +11 -1
- package/src/adapters/openai-responses/request-strips.ts +10 -2
- package/src/adapters/qoder/adapter.ts +16 -14
- package/src/bridge/errors.ts +26 -1
- package/src/bridge.ts +1 -1
- package/src/claude/context-windows.ts +57 -4
- package/src/claude/desktop-picker.ts +1 -1
- package/src/claude/intercept/connect-proxy.ts +10 -3
- package/src/claude/intercept/picker-ca-persistence.ts +3 -1
- package/src/claude/intercept/picker-listener.ts +49 -1
- package/src/claude/intercept/settings-no-proxy.ts +146 -0
- package/src/claude/intercept/settings.ts +101 -9
- package/src/cli/account-api.ts +65 -0
- package/src/cli/account-auth.ts +44 -12
- package/src/cli/account.ts +41 -5
- package/src/cli/capabilities-access-remote.ts +8 -0
- package/src/cli/capabilities-base.ts +13 -1
- package/src/cli/catalog-command-result.ts +2 -1
- package/src/cli/claude.ts +15 -6
- package/src/cli/config-command.ts +8 -2
- package/src/cli/debug.ts +8 -0
- package/src/cli/dispatch.ts +16 -3
- package/src/cli/export-command.ts +14 -1
- package/src/cli/hub-disable.ts +99 -0
- package/src/cli/hub.ts +4 -2
- package/src/cli/index.ts +13 -4
- package/src/cli/link-child.ts +104 -0
- package/src/cli/link-enrollment.ts +96 -0
- package/src/cli/link.ts +39 -24
- package/src/cli/observe-stream.ts +6 -1
- package/src/cli/observe.ts +10 -2
- package/src/cli/provider.ts +12 -0
- package/src/cli/registry.ts +5 -4
- package/src/cli/resolve.ts +1 -1
- package/src/cli/runtime-api.ts +27 -2
- package/src/cli/status.ts +104 -20
- package/src/client/connect.ts +39 -4
- package/src/client/hub-client.ts +14 -7
- package/src/client/hub-state.ts +25 -14
- package/src/client/link-home.ts +248 -0
- package/src/client/link-management-relay.ts +61 -0
- package/src/client/link-relay.ts +6 -1
- package/src/client/link-status.ts +14 -0
- package/src/client/machine-api.ts +14 -7
- package/src/client/machine-listener.ts +29 -4
- package/src/clients/config-export/model-metadata.ts +16 -0
- package/src/clients/config-export/zcode-store.ts +46 -11
- package/src/clients/config-export/zcode.ts +3 -5
- package/src/clients/config-export.ts +47 -17
- package/src/clients/omo-role-models.ts +6 -3
- package/src/codex/agent-role-models.ts +26 -8
- package/src/codex/auth-api/account-list.ts +17 -2
- package/src/codex/auth-api/account-selection.ts +17 -7
- package/src/codex/auth-api/pool-mode-gate.ts +4 -5
- package/src/codex/auth-api/routes.ts +5 -1
- package/src/codex/auth-context.ts +8 -3
- package/src/codex/autostart-health.ts +3 -1
- package/src/codex/catalog/bundled.ts +5 -1
- package/src/codex/catalog/effort.ts +2 -2
- package/src/codex/catalog/metadata.ts +4 -2
- package/src/codex/catalog/native-models.ts +17 -3
- package/src/codex/catalog/provider-models.ts +25 -0
- package/src/codex/desktop-app/darwin.ts +1 -1
- package/src/codex/desktop-app/handoff.ts +1 -1
- package/src/codex/desktop-app/linux.ts +1 -1
- package/src/codex/desktop-app/lock.ts +1 -1
- package/src/codex/desktop-app/types.ts +1 -1
- package/src/codex/desktop-app/windows.ts +1 -1
- package/src/codex/desktop-app-restart.ts +1 -1
- package/src/codex/main-account.ts +6 -7
- package/src/codex/native-profile-startup.ts +4 -1
- package/src/codex/prompt-journal.ts +20 -2
- package/src/codex/prompt-layers/encoding.ts +49 -0
- package/src/codex/prompt-layers/import-source.ts +30 -0
- package/src/codex/prompt-layers/toml-edit.ts +269 -37
- package/src/codex/prompt-layers/toml-read.ts +52 -4
- package/src/codex/prompt-layers.ts +383 -20
- package/src/codex/prompt-lock-claim.ts +13 -3
- package/src/codex/prompt-lock-evidence.ts +3 -3
- package/src/codex/prompt-lock-owner.ts +14 -6
- package/src/codex/prompt-text-probe.ts +34 -5
- package/src/codex/quota.ts +13 -6
- package/src/codex/role-auto-assign.ts +2 -5
- package/src/codex/routing/active-account.ts +21 -0
- package/src/codex/routing.ts +1 -1
- package/src/codex/runtime.ts +196 -8
- package/src/codex/shim-overlay.ts +8 -4
- package/src/codex/shim.ts +1 -1
- package/src/combos/failover.ts +3 -0
- package/src/config/api-key-reconcile.ts +34 -0
- package/src/config/diagnostics.ts +16 -0
- package/src/config/feature-flags.ts +5 -0
- package/src/config/live-reconcile.ts +43 -29
- package/src/config/live-topology.ts +29 -0
- package/src/config/load-degrade.ts +2 -1
- package/src/config/paths.ts +3 -0
- package/src/config/salvage.ts +3 -1
- package/src/config/schema/config-schema.ts +2 -0
- package/src/generated/compatibility-version.json +373 -217
- package/src/grok/grpc-web.ts +23 -1
- package/src/grok/inject.ts +245 -221
- package/src/grok/reset-coupon-ledger.ts +293 -57
- package/src/grok/reset-coupons.ts +15 -6
- package/src/integrations/cursor-detect.ts +1 -1
- package/src/integrations/mutation-plan.ts +13 -7
- package/src/integrations/registry.ts +45 -11
- package/src/integrations/state.ts +19 -7
- package/src/integrations/target.ts +42 -5
- package/src/integrations/writer.ts +14 -6
- package/src/lib/app-owned-memory-stores.ts +4 -0
- package/src/lib/app-owned-memory.ts +11 -0
- package/src/lib/errors.ts +25 -0
- package/src/lib/json-byte-size.ts +31 -0
- package/src/lib/optional-shutdown-hooks.ts +35 -0
- package/src/lib/outbound-credential-registry.ts +210 -0
- package/src/lib/passive-read.ts +35 -0
- package/src/lib/redact.ts +2 -2
- package/src/lib/shadow-call.ts +44 -5
- package/src/link/codex-settings.ts +155 -0
- package/src/link/management-routes.ts +89 -0
- package/src/link/remote-probe.ts +38 -0
- package/src/link/ssh-argv.ts +26 -5
- package/src/link/ssh-runner.ts +58 -27
- package/src/link/status-projection.ts +22 -4
- package/src/link/supervisor.ts +144 -49
- package/src/link/tunnel-state.ts +11 -4
- package/src/oauth/anthropic-continuity.ts +4 -3
- package/src/oauth/anthropic-identity.ts +13 -2
- package/src/oauth/anthropic-model-quota.ts +13 -1
- package/src/oauth/anthropic-routing.ts +230 -23
- package/src/oauth/anthropic.ts +18 -4
- package/src/oauth/google-antigravity.ts +24 -7
- package/src/oauth/index.ts +25 -16
- package/src/oauth/kiro-account-identity.ts +106 -0
- package/src/oauth/kiro.ts +48 -5
- package/src/oauth/meta-muse-device.ts +1 -1
- package/src/oauth/plan.ts +29 -0
- package/src/oauth/store.ts +40 -3
- package/src/oauth/types.ts +3 -1
- package/src/plugins/loader.ts +11 -0
- package/src/providers/anthropic-billing-ownership.ts +74 -0
- package/src/providers/anthropic-billing-recovery.ts +31 -0
- package/src/providers/anthropic-billing-refusal.ts +55 -0
- package/src/providers/anthropic-fast.ts +1 -1
- package/src/providers/anthropic-reset-admission.ts +62 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +29 -7
- package/src/providers/api-key-resolve.ts +17 -2
- package/src/providers/devin-cli-authmode-migration.ts +1 -1
- package/src/providers/devin-provider-merge-migration.ts +1 -1
- package/src/providers/github-copilot-auto.ts +169 -0
- package/src/providers/key-failover.ts +52 -5
- package/src/providers/model-presets.ts +1 -1
- package/src/providers/model-rename-startup.ts +5 -3
- package/src/providers/muse-subscription-usage.ts +1 -1
- package/src/providers/quota/account-cache.ts +11 -0
- package/src/providers/quota/anthropic-account-quota.ts +4 -2
- package/src/providers/quota/report-cache.ts +14 -0
- package/src/providers/quota/vendor-probes-oauth.ts +3 -3
- package/src/providers/quota.ts +4 -1
- package/src/providers/registry/entries-core.ts +20 -20
- package/src/providers/registry/entries-extended.ts +4 -1
- package/src/providers/registry/model-seeds.ts +20 -3
- package/src/providers/request-pacing.ts +9 -0
- package/src/providers/stale-no-vision-seed-migration.ts +72 -0
- package/src/providers/xai-fast-model.ts +1 -1
- package/src/responses/compaction.ts +14 -0
- package/src/responses/parser.ts +7 -1
- package/src/server/auth-cors.ts +42 -0
- package/src/server/configured-credentials.ts +194 -0
- package/src/server/inbound-body-admission.ts +24 -7
- package/src/server/index/link-listener.ts +30 -17
- package/src/server/index/link-management.ts +58 -0
- package/src/server/index/optional-listeners.ts +5 -1
- package/src/server/index/serve-options.ts +21 -2
- package/src/server/index.ts +1 -1
- package/src/server/lifecycle.ts +33 -5
- package/src/server/management/agent-settings-routes.ts +28 -10
- package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
- package/src/server/management/codex-agent-role-routes.ts +19 -6
- package/src/server/management/codex-prompt-routes.ts +136 -5
- package/src/server/management/codex-role-auto-assign.ts +1 -1
- package/src/server/management/grok-coupon-routes.ts +333 -57
- package/src/server/management/link-enrollment.ts +192 -0
- package/src/server/management/link-route-state.ts +185 -0
- package/src/server/management/link-routes.ts +92 -390
- package/src/server/management/model-rows.ts +3 -1
- package/src/server/management/provider-overwrite-carry.ts +10 -0
- package/src/server/management/provider-routes.ts +1 -0
- package/src/server/management/route-registry.ts +7 -5
- package/src/server/management/shared.ts +3 -0
- package/src/server/management-api.ts +3 -1
- package/src/server/messages-native.ts +68 -42
- package/src/server/relay-eager.ts +4 -0
- package/src/server/relay.ts +18 -7
- package/src/server/request-log-failure-attribution.ts +3 -0
- package/src/server/request-log-filter.ts +6 -1
- package/src/server/request-log-terminal-status.ts +243 -0
- package/src/server/request-log.ts +15 -118
- package/src/server/responses/adapter-continuation.ts +9 -1
- package/src/server/responses/adapter-dispatch.ts +293 -81
- package/src/server/responses/code-mode-collab-surface.ts +205 -0
- package/src/server/responses/codex-ws-exchange.ts +147 -6
- package/src/server/responses/codex-ws-wire.ts +203 -0
- package/src/server/responses/collaboration.ts +36 -7
- package/src/server/responses/compact.ts +129 -50
- package/src/server/responses/context-overflow.ts +13 -1
- package/src/server/responses/core-codex-account.ts +105 -1
- package/src/server/responses/core-combo-failure.ts +14 -0
- package/src/server/responses/core-combo.ts +7 -2
- package/src/server/responses/core-errors.ts +29 -1
- package/src/server/responses/core-opaque-recovery.ts +5 -5
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/core.ts +6 -6
- package/src/server/responses/deferred-collab-surface.ts +158 -0
- package/src/server/responses/deferred-send-reporter.ts +25 -0
- package/src/server/responses/empty-completion-guard.ts +18 -2
- package/src/server/responses/fetch-helpers.ts +12 -1
- package/src/server/responses/jev-model-invoke.ts +1 -1
- package/src/server/responses/passthrough-delivery.ts +4 -1
- package/src/server/responses/passthrough-dispatch.ts +130 -64
- package/src/server/responses/passthrough-execution.ts +3 -3
- package/src/server/responses/request-prepare.ts +6 -4
- package/src/server/responses/request-transport.ts +131 -13
- package/src/server/responses/run-turn-execution.ts +13 -5
- package/src/server/responses/sidecar-execution.ts +16 -2
- package/src/server/responses/spend-storage-error.ts +33 -0
- package/src/server/responses/ws-upstream.ts +9 -1
- package/src/server/startup-action-control.ts +22 -2
- package/src/server/startup-health-cache.ts +4 -0
- package/src/server/system-env-shell.ts +7 -2
- package/src/server/system-env.ts +11 -1
- package/src/service/cli.ts +5 -1
- package/src/service/desktop-startup.ts +147 -34
- package/src/service/desktop-supervision.d.mts +27 -1
- package/src/service/desktop-supervision.mjs +111 -5
- package/src/service/health-sentinel-launch.ts +176 -0
- package/src/service/health-sentinel-main.ts +447 -0
- package/src/service/health-sentinel-policy.ts +241 -0
- package/src/service/health-sentinel-probe.ts +54 -0
- package/src/service/health-sentinel-windows.ts +87 -0
- package/src/service/linux-linger.ts +20 -0
- package/src/types/config.ts +5 -0
- package/src/types/request.ts +5 -0
- package/src/usage/expected-prices.ts +5 -1
- package/src/usage/log.ts +19 -1
- package/gui/dist/assets/App-Ds_qL7o8.js +0 -54
- package/gui/dist/assets/Tray-BqHF5Trd.js +0 -1
- package/gui/dist/assets/index-D3FBCNiP.js +0 -94
|
@@ -12,6 +12,7 @@ import { homedir } from "node:os";
|
|
|
12
12
|
import { assertDroidSettingsUnambiguous } from "./droid-settings";
|
|
13
13
|
import { readPath } from "./merge";
|
|
14
14
|
import type { OwnershipRecord } from "./ownership";
|
|
15
|
+
import type { IntegrationIO } from "./config-io";
|
|
15
16
|
import { dirname, join } from "node:path";
|
|
16
17
|
import {
|
|
17
18
|
ClientPathError,
|
|
@@ -49,6 +50,8 @@ import {
|
|
|
49
50
|
raycastConfigPath,
|
|
50
51
|
zcodeConfigPath,
|
|
51
52
|
zcodeHomeDir,
|
|
53
|
+
zcodeLegacyHomeDir,
|
|
54
|
+
ZcodeSettingsPathError,
|
|
52
55
|
zcodeProviderStorePath,
|
|
53
56
|
buildZcodeStoreContribution,
|
|
54
57
|
zcodeStoreSchemaEstablished,
|
|
@@ -75,12 +78,21 @@ import {
|
|
|
75
78
|
*/
|
|
76
79
|
export type IntegrationClientId = ExportClientId;
|
|
77
80
|
|
|
81
|
+
export interface ResolvedIntegrationPaths {
|
|
82
|
+
configPath: string;
|
|
83
|
+
detectDir: string;
|
|
84
|
+
/** Snapshot of a mutable store resolution from the same bootstrap read. */
|
|
85
|
+
storePath?: string;
|
|
86
|
+
/** The default config path is only a recovery hint when the active root is unknown. */
|
|
87
|
+
pathError?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
78
90
|
export interface IntegrationClientSpec {
|
|
79
91
|
id: IntegrationClientId;
|
|
80
92
|
/** The client's config file, honoring that client's own environment override. */
|
|
81
|
-
configPath: (env?: NodeJS.ProcessEnv, home?: string) => string;
|
|
93
|
+
configPath: (env?: NodeJS.ProcessEnv, home?: string, io?: Pick<IntegrationIO, "readText">) => string;
|
|
82
94
|
/** Directory whose existence is the cheap "is it installed?" signal. */
|
|
83
|
-
detectDir: (env?: NodeJS.ProcessEnv, home?: string) => string;
|
|
95
|
+
detectDir: (env?: NodeJS.ProcessEnv, home?: string, io?: Pick<IntegrationIO, "readText">) => string;
|
|
84
96
|
/**
|
|
85
97
|
* The provider store this client reads INSTEAD of `configPath`.
|
|
86
98
|
*
|
|
@@ -98,7 +110,7 @@ export interface IntegrationClientSpec {
|
|
|
98
110
|
* client, never merged into on a guess.
|
|
99
111
|
*/
|
|
100
112
|
currentStore?: {
|
|
101
|
-
path: (env?: NodeJS.ProcessEnv, home?: string) => string;
|
|
113
|
+
path: (env?: NodeJS.ProcessEnv, home?: string, io?: Pick<IntegrationIO, "readText">) => string;
|
|
102
114
|
format: ConfigFormat;
|
|
103
115
|
establishes: (parsed: unknown) => boolean;
|
|
104
116
|
buildContribution: BuildContribution;
|
|
@@ -139,7 +151,7 @@ export interface IntegrationClientSpec {
|
|
|
139
151
|
* Droid uses the same seam to check competing settings against the export
|
|
140
152
|
* context before status, preview, or mutation proceeds.
|
|
141
153
|
*/
|
|
142
|
-
resolvePaths?: (env?: NodeJS.ProcessEnv, home?: string, exportContext?: ExportContext
|
|
154
|
+
resolvePaths?: (env?: NodeJS.ProcessEnv, home?: string, exportContext?: ExportContext, io?: Pick<IntegrationIO, "readText">) => ResolvedIntegrationPaths;
|
|
143
155
|
/**
|
|
144
156
|
* Where the client's config WOULD live, for a client whose real path cannot
|
|
145
157
|
* be resolved yet.
|
|
@@ -183,10 +195,11 @@ export function resolveIntegrationPaths(
|
|
|
183
195
|
env: NodeJS.ProcessEnv = process.env,
|
|
184
196
|
home: string = homedir(),
|
|
185
197
|
exportContext?: ExportContext,
|
|
186
|
-
|
|
198
|
+
io?: Pick<IntegrationIO, "readText">,
|
|
199
|
+
): ResolvedIntegrationPaths {
|
|
187
200
|
const spec = INTEGRATION_CLIENTS[clientId];
|
|
188
|
-
if (spec.resolvePaths) return spec.resolvePaths(env, home, exportContext);
|
|
189
|
-
return { configPath: spec.configPath(env, home), detectDir: spec.detectDir(env, home) };
|
|
201
|
+
if (spec.resolvePaths) return spec.resolvePaths(env, home, exportContext, io);
|
|
202
|
+
return { configPath: spec.configPath(env, home, io), detectDir: spec.detectDir(env, home, io) };
|
|
190
203
|
}
|
|
191
204
|
|
|
192
205
|
export function assertDroidPathsUnambiguous(root: string, exportContext?: ExportContext): void {
|
|
@@ -357,15 +370,31 @@ export const INTEGRATION_CLIENTS: Record<IntegrationClientId, IntegrationClientS
|
|
|
357
370
|
},
|
|
358
371
|
zcode: {
|
|
359
372
|
id: "zcode",
|
|
360
|
-
configPath: (env = process.env, home = homedir()) => zcodeConfigPath(env, home),
|
|
361
|
-
detectDir: (env = process.env, home = homedir()) => zcodeHomeDir(env, home),
|
|
373
|
+
configPath: (env = process.env, home = homedir(), io) => zcodeConfigPath(env, home, io),
|
|
374
|
+
detectDir: (env = process.env, home = homedir(), io) => zcodeHomeDir(env, home, io),
|
|
375
|
+
resolvePaths: (env = process.env, home = homedir(), _context, io) => {
|
|
376
|
+
let root: string;
|
|
377
|
+
let pathError: string | undefined;
|
|
378
|
+
try { root = zcodeHomeDir(env, home, io); }
|
|
379
|
+
catch (error) {
|
|
380
|
+
if (!(error instanceof ZcodeSettingsPathError)) throw error;
|
|
381
|
+
root = zcodeLegacyHomeDir(env, home);
|
|
382
|
+
pathError = error.message;
|
|
383
|
+
}
|
|
384
|
+
// Passing the resolved root prevents a second bootstrap read. An explicit
|
|
385
|
+
// provider-file override outranks both the setting and ZCODE_DATA_DIR.
|
|
386
|
+
const storePath = zcodeProviderStorePath({ ...env, ZCODE_DATA_DIR: root }, home, io);
|
|
387
|
+
return { configPath: join(root, "v2", "config.json"), detectDir: root, storePath, ...(pathError ? { pathError } : {}) };
|
|
388
|
+
},
|
|
389
|
+
bindsDriftedRecord: (path, env = process.env, home = homedir()) =>
|
|
390
|
+
path === join(zcodeLegacyHomeDir(env, home), "v2", "config.json"),
|
|
362
391
|
/*
|
|
363
392
|
* ZCode 3.14 reads its providers from `v2/provider_config.json` and reaches
|
|
364
393
|
* `v2/config.json` only through the import that seeded it. Where the new
|
|
365
394
|
* file exists the import is spent, so our write is read by nobody (#5348).
|
|
366
395
|
*/
|
|
367
396
|
currentStore: {
|
|
368
|
-
path: (env = process.env, home = homedir()) => zcodeProviderStorePath(env, home),
|
|
397
|
+
path: (env = process.env, home = homedir(), io) => zcodeProviderStorePath(env, home, io),
|
|
369
398
|
format: "json",
|
|
370
399
|
establishes: zcodeStoreSchemaEstablished,
|
|
371
400
|
buildContribution: buildZcodeStoreContribution,
|
|
@@ -496,7 +525,7 @@ export function boundIntegrationConfigPath(input: {
|
|
|
496
525
|
if (
|
|
497
526
|
record && record.clientId === input.clientId &&
|
|
498
527
|
record.configPath !== input.resolvedPath &&
|
|
499
|
-
input.statKind(record.configPath) === "file" &&
|
|
528
|
+
(input.clientId === "zcode" ? input.statKind(record.configPath) !== "missing" : input.statKind(record.configPath) === "file") &&
|
|
500
529
|
INTEGRATION_CLIENTS[input.clientId].bindsDriftedRecord?.(record.configPath, input.env, input.home) === true
|
|
501
530
|
) {
|
|
502
531
|
return record.configPath;
|
|
@@ -529,6 +558,11 @@ export function restoreOwnershipCollision(input: {
|
|
|
529
558
|
}): string | null {
|
|
530
559
|
const currentPath = input.currentPath;
|
|
531
560
|
if (currentPath === null || currentPath === input.journaledPath) return null;
|
|
561
|
+
// A relocated ZCode store must not lose its single record to an older
|
|
562
|
+
// legacy undo, even when both journaled locations are legal for this home.
|
|
563
|
+
if (input.clientId === "zcode") {
|
|
564
|
+
return `that operation was recorded for ${input.journaledPath}, but ${currentPath} currently owns this integration`;
|
|
565
|
+
}
|
|
532
566
|
const binds = INTEGRATION_CLIENTS[input.clientId].bindsDriftedRecord;
|
|
533
567
|
if (!binds) return null;
|
|
534
568
|
if (binds(input.journaledPath, input.env, input.home) !== true) return null;
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
assertDroidPathsUnambiguous,
|
|
40
40
|
assertDroidRecordedSettingsUnambiguous,
|
|
41
41
|
resolveIntegrationPaths,
|
|
42
|
+
type ResolvedIntegrationPaths,
|
|
42
43
|
unresolvedPathHintFor,
|
|
43
44
|
type IntegrationClientId,
|
|
44
45
|
} from "./registry";
|
|
@@ -482,7 +483,7 @@ export interface IntegrationStateInput {
|
|
|
482
483
|
store?: IntegrationStateStore;
|
|
483
484
|
io?: IntegrationIO;
|
|
484
485
|
/** Internal explicit profile target; never accepted as a caller-provided path. */
|
|
485
|
-
resolvedPaths?:
|
|
486
|
+
resolvedPaths?: ResolvedIntegrationPaths;
|
|
486
487
|
}
|
|
487
488
|
|
|
488
489
|
export function exportContextOf(input: {
|
|
@@ -592,11 +593,11 @@ export function readIntegrationState(input: IntegrationStateInput): IntegrationS
|
|
|
592
593
|
*/
|
|
593
594
|
let installed: boolean;
|
|
594
595
|
let effective: IntegrationTarget;
|
|
595
|
-
let record: OwnershipRecord | null;
|
|
596
|
+
let record: OwnershipRecord | null = null;
|
|
596
597
|
try {
|
|
597
598
|
// One resolution for both, so a client whose paths come from mutable state
|
|
598
599
|
// cannot report one account's install beside another account's config path.
|
|
599
|
-
const paths = resolveStatePaths(input);
|
|
600
|
+
const paths = resolveStatePaths(input, io);
|
|
600
601
|
installed = io.statKind(paths.detectDir) === "dir";
|
|
601
602
|
if (input.clientId === "cline") io = createClineIO(io, paths.configPath, store);
|
|
602
603
|
/*
|
|
@@ -611,7 +612,7 @@ export function readIntegrationState(input: IntegrationStateInput): IntegrationS
|
|
|
611
612
|
statKind: io.statKind, env: input.env, home: input.home,
|
|
612
613
|
});
|
|
613
614
|
effective = resolveIntegrationTarget({
|
|
614
|
-
clientId: input.clientId, configPath: recordedPath, io, record, env: input.env, home: input.home,
|
|
615
|
+
clientId: input.clientId, configPath: recordedPath, io, record, env: input.env, home: input.home, resolvedPaths: paths,
|
|
615
616
|
});
|
|
616
617
|
} catch (error) {
|
|
617
618
|
if (!(error instanceof ClientPathError)) throw error;
|
|
@@ -626,6 +627,10 @@ export function readIntegrationState(input: IntegrationStateInput): IntegrationS
|
|
|
626
627
|
* yet. A client that can name where its config would go gets `installed:
|
|
627
628
|
* false` and that location, which reads as "not installed" in the UI.
|
|
628
629
|
*/
|
|
630
|
+
if (input.clientId === "zcode" && record) return {
|
|
631
|
+
clientId: input.clientId, state: "unsafe", installed: false, configPath: record.configPath,
|
|
632
|
+
reason: "unresolvable-path", appliedAt: record.appliedAt, lastOpId: record.opId, ...retention,
|
|
633
|
+
};
|
|
629
634
|
const hint = unresolvedPathHintFor(input.clientId, input.env, input.home);
|
|
630
635
|
return {
|
|
631
636
|
clientId: input.clientId,
|
|
@@ -638,6 +643,12 @@ export function readIntegrationState(input: IntegrationStateInput): IntegrationS
|
|
|
638
643
|
}
|
|
639
644
|
|
|
640
645
|
const configPath = effective.configPath;
|
|
646
|
+
if (effective.pathError) return {
|
|
647
|
+
clientId: input.clientId, state: "unsafe", installed, configPath,
|
|
648
|
+
reason: "unresolvable-path",
|
|
649
|
+
...(record?.configPath === configPath ? { appliedAt: record.appliedAt, lastOpId: record.opId } : {}),
|
|
650
|
+
...retention,
|
|
651
|
+
};
|
|
641
652
|
if (input.clientId === "kilo") {
|
|
642
653
|
const candidates = inspectKiloCandidates({ io, selectedPath: configPath, env: input.env, home: input.home });
|
|
643
654
|
if (candidates.kind !== "ok") return {
|
|
@@ -715,9 +726,10 @@ export function readIntegrationState(input: IntegrationStateInput): IntegrationS
|
|
|
715
726
|
};
|
|
716
727
|
}
|
|
717
728
|
|
|
718
|
-
function resolveStatePaths(input: IntegrationStateInput
|
|
729
|
+
function resolveStatePaths(input: IntegrationStateInput, io: IntegrationIO): ResolvedIntegrationPaths {
|
|
719
730
|
const context = exportContextOf(input);
|
|
720
|
-
const paths = input.
|
|
731
|
+
const paths = input.clientId === "zcode" || !input.resolvedPaths
|
|
732
|
+
? resolveIntegrationPaths(input.clientId, input.env, input.home, context, io) : input.resolvedPaths;
|
|
721
733
|
if (input.clientId === "droid" && input.resolvedPaths) assertDroidPathsUnambiguous(paths.detectDir, context);
|
|
722
734
|
return paths;
|
|
723
735
|
}
|
|
@@ -727,7 +739,7 @@ export function readOwnedDroidReasoningDefaults(input: IntegrationStateInput): R
|
|
|
727
739
|
const store = input.store ?? createIntegrationStateStore();
|
|
728
740
|
const io = input.io ?? store.io();
|
|
729
741
|
try {
|
|
730
|
-
const paths = resolveStatePaths(input);
|
|
742
|
+
const paths = resolveStatePaths(input, io);
|
|
731
743
|
const record = store.readRecords().droid;
|
|
732
744
|
if (!record || record.clientId !== "droid" || record.configPath !== paths.configPath) return {};
|
|
733
745
|
const effective = resolveIntegrationTarget({
|
|
@@ -23,11 +23,11 @@
|
|
|
23
23
|
* nesting into a file holding the user's other providers would trade a
|
|
24
24
|
* silent no-op for a silent loss.
|
|
25
25
|
*/
|
|
26
|
-
import { EXPORT_CLIENTS, ClientPathError, type BuildContribution, type ConfigFormat } from "../clients/config-export";
|
|
26
|
+
import { EXPORT_CLIENTS, ClientPathError, zcodeLegacyHomeDir, zcodeProviderStorePath, type BuildContribution, type ConfigFormat } from "../clients/config-export";
|
|
27
27
|
import { PARSE_FAILED, loadTarget, parseConfig, type IntegrationIO } from "./config-io";
|
|
28
28
|
import { AmbiguousSelectorError, InvalidSelectorError, readPath } from "./merge";
|
|
29
29
|
import type { OwnershipRecord } from "./ownership";
|
|
30
|
-
import { INTEGRATION_CLIENTS, type IntegrationClientId } from "./registry";
|
|
30
|
+
import { INTEGRATION_CLIENTS, resolveIntegrationPaths, type ResolvedIntegrationPaths, type IntegrationClientId } from "./registry";
|
|
31
31
|
|
|
32
32
|
export interface IntegrationTarget {
|
|
33
33
|
/** The file this operation reads, writes, journals and records ownership for. */
|
|
@@ -51,6 +51,8 @@ export interface IntegrationTarget {
|
|
|
51
51
|
* to is as effective as it ever was.
|
|
52
52
|
*/
|
|
53
53
|
readonly ineffective: IneffectiveWrite | null;
|
|
54
|
+
/** Active root unknown; the named config is only a historical removal target. */
|
|
55
|
+
readonly pathError?: string;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
export interface IneffectiveWrite {
|
|
@@ -153,12 +155,44 @@ export function resolveIntegrationTarget(args: {
|
|
|
153
155
|
record: OwnershipRecord | null;
|
|
154
156
|
env?: NodeJS.ProcessEnv;
|
|
155
157
|
home?: string;
|
|
158
|
+
resolvedPaths?: ResolvedIntegrationPaths;
|
|
156
159
|
}): IntegrationTarget {
|
|
157
160
|
const { clientId, configPath, io, record } = args;
|
|
158
161
|
const declared = INTEGRATION_CLIENTS[clientId].currentStore;
|
|
159
162
|
if (!declared) return configFileTarget(clientId, configPath, null);
|
|
160
|
-
const
|
|
163
|
+
const paths = clientId === "zcode"
|
|
164
|
+
? args.resolvedPaths ?? resolveIntegrationPaths(clientId, args.env, args.home, undefined, io)
|
|
165
|
+
: undefined;
|
|
166
|
+
const storePath = paths?.storePath ?? declared.path(args.env, args.home, io);
|
|
161
167
|
const kind = io.statKind(storePath);
|
|
168
|
+
const owned = record !== null && record.clientId === clientId ? record : null;
|
|
169
|
+
const historicalStore = clientId === "zcode"
|
|
170
|
+
? zcodeProviderStorePath({ ...(args.env ?? process.env), ZCODE_DATA_DIR: zcodeLegacyHomeDir(args.env, args.home) }, args.home, io)
|
|
171
|
+
: null;
|
|
172
|
+
if (clientId === "zcode" && owned && owned.configPath !== configPath && owned.configPath !== storePath
|
|
173
|
+
&& owned.configPath !== historicalStore
|
|
174
|
+
&& INTEGRATION_CLIENTS[clientId].bindsDriftedRecord?.(owned.configPath, args.env, args.home) !== true
|
|
175
|
+
&& io.statKind(owned.configPath) !== "missing") {
|
|
176
|
+
throw new ClientPathError(`ZCode ownership remains at ${owned.configPath}. Its location cannot be bound to the current home or selectors; restore the original selectors and disable it before enabling a different data root. Ownership and recovery evidence are retained.`);
|
|
177
|
+
}
|
|
178
|
+
// An explicit provider-file selector can establish the active target even
|
|
179
|
+
// when the bootstrap is unreadable, but absence cannot justify a legacy write.
|
|
180
|
+
if (paths?.pathError && (!(args.env ?? process.env).ZCODE_PERSONAL_PROVIDER_CONFIG_FILE?.trim() || kind !== "file")) {
|
|
181
|
+
const removal = owned?.configPath === historicalStore
|
|
182
|
+
? storeTarget(declared, historicalStore)
|
|
183
|
+
: configFileTarget(clientId, configPath, null);
|
|
184
|
+
return { ...removal, pathError: paths.pathError };
|
|
185
|
+
}
|
|
186
|
+
if (historicalStore && owned?.configPath === historicalStore && historicalStore !== storePath
|
|
187
|
+
&& recordedBlockStillPresent(io, declared.format, historicalStore, owned)) {
|
|
188
|
+
return storeTarget(declared, historicalStore, { store: storePath, why: "owned-config-file" });
|
|
189
|
+
}
|
|
190
|
+
// Relocation must not orphan a block at the default historical path, even
|
|
191
|
+
// when the relocated store has not been created yet.
|
|
192
|
+
if (clientId === "zcode" && owned?.configPath === configPath && configPath !== paths?.configPath
|
|
193
|
+
&& recordedBlockStillPresent(io, EXPORT_CLIENTS[clientId].format, configPath, owned)) {
|
|
194
|
+
return configFileTarget(clientId, configPath, { store: storePath, why: "owned-config-file" });
|
|
195
|
+
}
|
|
162
196
|
if (kind === "missing") {
|
|
163
197
|
const missing = declared.missingStore;
|
|
164
198
|
return missing?.readsStore(storePath, path => io.statKind(path)) === true
|
|
@@ -182,7 +216,6 @@ export function resolveIntegrationTarget(args: {
|
|
|
182
216
|
* file holds it. Disable has to remove what we wrote from where we wrote it,
|
|
183
217
|
* and an apply must never leave a block in one file while writing another.
|
|
184
218
|
*/
|
|
185
|
-
const owned = record !== null && record.clientId === clientId ? record : null;
|
|
186
219
|
if (owned?.configPath === storePath && recordedBlockStillPresent(io, declared.format, storePath, owned)) {
|
|
187
220
|
return storeTarget(declared, storePath, established ? null : unestablished);
|
|
188
221
|
}
|
|
@@ -214,6 +247,7 @@ export function declaredIntegrationTarget(args: {
|
|
|
214
247
|
resolvedConfigPath: string;
|
|
215
248
|
env?: NodeJS.ProcessEnv;
|
|
216
249
|
home?: string;
|
|
250
|
+
io?: Pick<IntegrationIO, "readText">;
|
|
217
251
|
}): IntegrationTarget | null {
|
|
218
252
|
const { clientId, configPath, resolvedConfigPath } = args;
|
|
219
253
|
if (configPath === resolvedConfigPath) return configFileTarget(clientId, configPath, null);
|
|
@@ -222,8 +256,11 @@ export function declaredIntegrationTarget(args: {
|
|
|
222
256
|
}
|
|
223
257
|
const declared = INTEGRATION_CLIENTS[clientId].currentStore;
|
|
224
258
|
if (!declared) return null;
|
|
259
|
+
if (clientId === "zcode" && configPath === zcodeProviderStorePath(
|
|
260
|
+
{ ...(args.env ?? process.env), ZCODE_DATA_DIR: zcodeLegacyHomeDir(args.env, args.home) }, args.home, args.io,
|
|
261
|
+
)) return storeTarget(declared, configPath);
|
|
225
262
|
try {
|
|
226
|
-
if (declared.path(args.env, args.home) !== configPath) return null;
|
|
263
|
+
if (declared.path(args.env, args.home, args.io) !== configPath) return null;
|
|
227
264
|
} catch (error) {
|
|
228
265
|
// A store the operator relocated with a path we cannot resolve proves
|
|
229
266
|
// nothing about the row, so the row is not a legal target.
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
assertDroidPathsUnambiguous,
|
|
40
40
|
isLoopbackOnly,
|
|
41
41
|
resolveIntegrationPaths,
|
|
42
|
+
type ResolvedIntegrationPaths,
|
|
42
43
|
restoreOwnershipCollision,
|
|
43
44
|
type IntegrationClientId,
|
|
44
45
|
} from "./registry";
|
|
@@ -643,8 +644,15 @@ export function restoreIntegration(input: IntegrationRestoreInput): WriteOutcome
|
|
|
643
644
|
if (entry.clientId !== input.clientId) throw new Error("restore input names a different client than the operation");
|
|
644
645
|
|
|
645
646
|
const clientId = entry.clientId;
|
|
646
|
-
|
|
647
|
-
|
|
647
|
+
let resolvedPath: string;
|
|
648
|
+
try {
|
|
649
|
+
resolvedPath = clientId === "zcode" || !input.resolvedPaths
|
|
650
|
+
? resolveIntegrationPaths(clientId, input.env, input.home, undefined, io).configPath
|
|
651
|
+
: input.resolvedPaths.configPath;
|
|
652
|
+
} catch (error) {
|
|
653
|
+
if (!(error instanceof ClientPathError)) throw error;
|
|
654
|
+
return refuse(clientId, "unsafe", "unsafe", error.message);
|
|
655
|
+
}
|
|
648
656
|
const configPath = entry.configPath;
|
|
649
657
|
/*
|
|
650
658
|
* Restore acts on the path the operation was journaled against. Resolving a
|
|
@@ -655,7 +663,7 @@ export function restoreIntegration(input: IntegrationRestoreInput): WriteOutcome
|
|
|
655
663
|
* shape those bytes are in, which is what the state below is measured against.
|
|
656
664
|
*/
|
|
657
665
|
const rowTarget = declaredIntegrationTarget({
|
|
658
|
-
clientId, configPath, resolvedConfigPath: resolvedPath, env: input.env, home: input.home,
|
|
666
|
+
clientId, configPath, resolvedConfigPath: resolvedPath, env: input.env, home: input.home, io,
|
|
659
667
|
});
|
|
660
668
|
if (rowTarget === null) {
|
|
661
669
|
return refuse(clientId, "conflict", "conflict",
|
|
@@ -814,7 +822,7 @@ type FrozenIntegrationInput = IntegrationWriteInput & {
|
|
|
814
822
|
io: IntegrationIO;
|
|
815
823
|
env: NodeJS.ProcessEnv;
|
|
816
824
|
home: string;
|
|
817
|
-
resolvedPaths:
|
|
825
|
+
resolvedPaths: ResolvedIntegrationPaths;
|
|
818
826
|
};
|
|
819
827
|
|
|
820
828
|
/**
|
|
@@ -854,7 +862,7 @@ function freezeIntegrationInput(input: IntegrationWriteInput): FrozenIntegration
|
|
|
854
862
|
*/
|
|
855
863
|
const resolvedPaths = input.resolvedPaths
|
|
856
864
|
? { ...input.resolvedPaths }
|
|
857
|
-
: resolveIntegrationPaths(input.clientId, env, home, exportContextOf({ ...input, config, models: models.value }));
|
|
865
|
+
: resolveIntegrationPaths(input.clientId, env, home, exportContextOf({ ...input, config, models: models.value }), io);
|
|
858
866
|
/*
|
|
859
867
|
* The configuration and the roster are seams like the others, and they were the two still held
|
|
860
868
|
* by reference. A coordinated write plans from this input, awaits the writer lock and a
|
|
@@ -899,7 +907,7 @@ function storeLockFile(frozen: FrozenIntegrationInput): string | null {
|
|
|
899
907
|
if (!declared?.lockFile) return null;
|
|
900
908
|
let storePath: string;
|
|
901
909
|
try {
|
|
902
|
-
storePath = declared.path(frozen.env, frozen.home);
|
|
910
|
+
storePath = declared.path(frozen.env, frozen.home, frozen.io);
|
|
903
911
|
} catch (error) {
|
|
904
912
|
if (error instanceof ClientPathError) return null;
|
|
905
913
|
throw error;
|
|
@@ -169,12 +169,16 @@ export const APP_OWNED_RETAINED_STORE_REGISTRATIONS = [
|
|
|
169
169
|
category: "caches",
|
|
170
170
|
snapshot: usageSummaryRetainedStoreSnapshot,
|
|
171
171
|
evictOldest: evictOldestUsageSummaryForBudget,
|
|
172
|
+
// Deletes one in-memory summary entry.
|
|
173
|
+
passiveEvictable: true,
|
|
172
174
|
},
|
|
173
175
|
{
|
|
174
176
|
id: "usage_snapshot",
|
|
175
177
|
category: "caches",
|
|
176
178
|
snapshot: usageSnapshotRetainedStoreSnapshot,
|
|
177
179
|
evictOldest: evictOldestUsageSnapshot,
|
|
180
|
+
// Drops the retained ledger snapshot or one unpinned aggregate, both in memory.
|
|
181
|
+
passiveEvictable: true,
|
|
178
182
|
},
|
|
179
183
|
{
|
|
180
184
|
id: "cursor_blobs",
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isPassiveRead } from "./passive-read";
|
|
2
|
+
|
|
1
3
|
export type AppOwnedRetainedCategory = "logs" | "caches" | "blobs" | "continuation";
|
|
2
4
|
export type AppOwnedObservedCategory = "translator" | "serialized_tails";
|
|
3
5
|
|
|
@@ -14,6 +16,11 @@ export interface RetainedStoreRegistration {
|
|
|
14
16
|
category: AppOwnedRetainedCategory;
|
|
15
17
|
snapshot(): RetainedStoreSnapshot;
|
|
16
18
|
evictOldest(): number;
|
|
19
|
+
/**
|
|
20
|
+
* Eviction only releases memory: no spill, snapshot or other write. Only such stores are
|
|
21
|
+
* demoted inside a passive read (#6959); the rest wait for this process's own next enforcement.
|
|
22
|
+
*/
|
|
23
|
+
passiveEvictable?: true;
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
export interface ObservedBufferRegistration {
|
|
@@ -168,11 +175,15 @@ function nextCandidate(
|
|
|
168
175
|
snapshots: ReadonlyMap<string, RetainedStoreSnapshot>,
|
|
169
176
|
ineligible: Set<string>,
|
|
170
177
|
): RetainedStoreRegistration | undefined {
|
|
178
|
+
// A passive read (a Remote Link Child's, #6959) demotes only stores whose eviction is memory-only:
|
|
179
|
+
// a continuation spills to disk and a replay cache rewrites its snapshot.
|
|
180
|
+
const passive = isPassiveRead();
|
|
171
181
|
for (const category of CATEGORY_ORDER) {
|
|
172
182
|
let candidate: RetainedStoreRegistration | undefined;
|
|
173
183
|
let candidateAt = Number.POSITIVE_INFINITY;
|
|
174
184
|
for (const registration of retainedStores.values()) {
|
|
175
185
|
if (registration.category !== category || ineligible.has(registration.id)) continue;
|
|
186
|
+
if (passive && registration.passiveEvictable !== true) continue;
|
|
176
187
|
const snapshot = snapshots.get(registration.id);
|
|
177
188
|
if (!snapshot || snapshot.evictableBytes <= 0) continue;
|
|
178
189
|
const oldestAt = snapshot.oldestAt;
|
package/src/lib/errors.ts
CHANGED
|
@@ -17,6 +17,7 @@ export const ENCRYPTED_FUNCTION_OUTPUT_REJECTION =
|
|
|
17
17
|
* string for a client to be able to tell this apart from a provider rate limit.
|
|
18
18
|
*/
|
|
19
19
|
export const SEND_BUDGET_EXHAUSTED_CODE = "request_send_budget_exhausted";
|
|
20
|
+
export const SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE = "spend_ledger_storage_unavailable";
|
|
20
21
|
|
|
21
22
|
/** First nonblank string across the canonical upstream error paths, in priority order. */
|
|
22
23
|
export function upstreamErrorMessageFromPayload(payload: unknown): string | undefined {
|
|
@@ -357,6 +358,9 @@ export function isUpstreamResetReplayRefusedMessage(text: string): boolean {
|
|
|
357
358
|
|
|
358
359
|
export function classifyError(status: number, type: string, message: string): OcxErrorPayload {
|
|
359
360
|
const text = message.toLowerCase();
|
|
361
|
+
if (type === SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE) {
|
|
362
|
+
return { message, type: "server_error", code: SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE };
|
|
363
|
+
}
|
|
360
364
|
if (type === "previous_response_not_found") {
|
|
361
365
|
return { message, type: "invalid_request_error", code: "previous_response_not_found" };
|
|
362
366
|
}
|
|
@@ -639,6 +643,24 @@ export function adapterFailureFromMessage(message: string): { httpStatus: number
|
|
|
639
643
|
return { httpStatus, error };
|
|
640
644
|
}
|
|
641
645
|
|
|
646
|
+
/** Narrow structured classes carried through a synthesized bare-error terminal. */
|
|
647
|
+
export function recognizedUpstreamError(error: { type?: unknown; code?: unknown }):
|
|
648
|
+
{ type: string; code: string; httpStatus: 429 | 503 } | undefined {
|
|
649
|
+
const code = typeof error.code === "string" ? error.code : undefined;
|
|
650
|
+
// An explicit unknown code must not gain a recognized class from its type or copy.
|
|
651
|
+
if (code !== undefined && !["rate_limit_exceeded", "rate_limit_error", "server_is_overloaded", "overloaded_error"].includes(code)) return undefined;
|
|
652
|
+
if (code === "rate_limit_exceeded" || code === "rate_limit_error"
|
|
653
|
+
|| (code === undefined && error.type === "rate_limit_error")) {
|
|
654
|
+
return { type: "rate_limit_error", code: code ?? "rate_limit_exceeded", httpStatus: 429 };
|
|
655
|
+
}
|
|
656
|
+
if (code === "server_is_overloaded" || code === "overloaded_error"
|
|
657
|
+
|| (code === undefined && error.type === "overloaded_error")) {
|
|
658
|
+
return { type: error.type === "overloaded_error" ? "overloaded_error" : "server_error",
|
|
659
|
+
code: code ?? "server_is_overloaded", httpStatus: 503 };
|
|
660
|
+
}
|
|
661
|
+
return undefined;
|
|
662
|
+
}
|
|
663
|
+
|
|
642
664
|
/** Map a terminal Responses error object to the HTTP status we record in /api/logs. */
|
|
643
665
|
export function httpStatusFromTerminalError(error: {
|
|
644
666
|
type?: string;
|
|
@@ -646,10 +668,13 @@ export function httpStatusFromTerminalError(error: {
|
|
|
646
668
|
message?: string;
|
|
647
669
|
} | undefined): number {
|
|
648
670
|
if (!error) return 502;
|
|
671
|
+
if (error.code === SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE) return 503;
|
|
649
672
|
if (error.code === "client_closed_request" || error.code === "client_cancelled") return 499;
|
|
650
673
|
if (isCyberPolicyCode(error.code) || (error.message ? isCyberPolicyMessage(error.message) : false)) {
|
|
651
674
|
return 400;
|
|
652
675
|
}
|
|
676
|
+
const recognized = recognizedUpstreamError(error);
|
|
677
|
+
if (recognized) return recognized.httpStatus;
|
|
653
678
|
if (error.type === "rate_limit_error" || error.code === "rate_limit_exceeded") return 429;
|
|
654
679
|
if (error.type === "authentication_error" || error.code === "invalid_api_key") return 401;
|
|
655
680
|
if (
|
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
import { TRANSLATOR_MAX_TURN_BYTES, TranslatorBudgetExceededError } from "./translator-budget";
|
|
2
2
|
|
|
3
|
+
/** Count one JSON string formed from parts without joining, serializing or encoding it. */
|
|
4
|
+
export function jsonStringPartsUtf8Bytes(parts: readonly string[], limit = TRANSLATOR_MAX_TURN_BYTES): number {
|
|
5
|
+
let bytes = 0;
|
|
6
|
+
let highSurrogate = false;
|
|
7
|
+
const add = (count: number) => {
|
|
8
|
+
if (count > limit - bytes) throw new TranslatorBudgetExceededError("request_copies", limit);
|
|
9
|
+
bytes += count;
|
|
10
|
+
};
|
|
11
|
+
add(2); // The joined string has one pair of JSON quotes.
|
|
12
|
+
for (const text of parts) {
|
|
13
|
+
if (text.length > limit - bytes) throw new TranslatorBudgetExceededError("request_copies", limit);
|
|
14
|
+
for (let i = 0; i < text.length; i++) {
|
|
15
|
+
const code = text.charCodeAt(i);
|
|
16
|
+
if (highSurrogate) {
|
|
17
|
+
highSurrogate = false;
|
|
18
|
+
if (code >= 0xdc00 && code <= 0xdfff) { add(4); continue; }
|
|
19
|
+
add(6);
|
|
20
|
+
}
|
|
21
|
+
if (code >= 0xd800 && code <= 0xdbff) highSurrogate = true;
|
|
22
|
+
else if (code === 0x22 || code === 0x5c || code === 8 || code === 9 || code === 10 || code === 12 || code === 13) add(2);
|
|
23
|
+
else if (code < 0x20) add(6);
|
|
24
|
+
else if (code < 0x80) add(1);
|
|
25
|
+
else if (code < 0x800) add(2);
|
|
26
|
+
else if (code >= 0xdc00 && code <= 0xdfff) add(6);
|
|
27
|
+
else add(3);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (highSurrogate) add(6);
|
|
31
|
+
return bytes;
|
|
32
|
+
}
|
|
33
|
+
|
|
3
34
|
/** Measure plain JSON data without allocating its serialized string or UTF-8 copy. */
|
|
4
35
|
export function jsonUtf8Bytes(value: unknown, limit = TRANSLATOR_MAX_TURN_BYTES): number {
|
|
5
36
|
let bytes = 0;
|
|
@@ -70,5 +70,40 @@ export function runOptionalShutdownHooks(): void {
|
|
|
70
70
|
/** Test-only reset so an isolated lifecycle test does not inherit registrations. */
|
|
71
71
|
export function resetOptionalShutdownHooksForTests(): void {
|
|
72
72
|
hooks.clear();
|
|
73
|
+
drainStartHooks.clear();
|
|
74
|
+
drainStartHooksRan = false;
|
|
73
75
|
hooksRan = false;
|
|
74
76
|
}
|
|
77
|
+
|
|
78
|
+
const drainStartHooks = new Map<string, ShutdownHook>();
|
|
79
|
+
let drainStartHooksRan = false;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Register work that must run the moment a drain begins, before in-flight turns are awaited.
|
|
83
|
+
* The service health sentinel (#6671) uses it: a deliberate stop whose drain stalls must stay a
|
|
84
|
+
* stop, so the sentinel is withdrawn before the drain can look unresponsive. Same contract as
|
|
85
|
+
* {@link registerOptionalShutdownHook}: keyed, synchronous, best-effort, detachable.
|
|
86
|
+
*/
|
|
87
|
+
export function registerDrainStartHook(key: string, hook: ShutdownHook): () => void {
|
|
88
|
+
drainStartHooks.set(key, hook);
|
|
89
|
+
return () => {
|
|
90
|
+
if (drainStartHooks.get(key) === hook) drainStartHooks.delete(key);
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Run every drain-start hook. Never throws. */
|
|
95
|
+
export function runDrainStartHooks(): void {
|
|
96
|
+
drainStartHooksRan = true;
|
|
97
|
+
for (const [key, hook] of [...drainStartHooks]) {
|
|
98
|
+
try {
|
|
99
|
+
hook();
|
|
100
|
+
} catch (err) {
|
|
101
|
+
console.warn(`[shutdown] drain-start hook "${key}" failed:`, err instanceof Error ? err.message : err);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Whether a drain has begun in this process; work that must not start mid-drain checks this. */
|
|
107
|
+
export function didRunDrainStartHooks(): boolean {
|
|
108
|
+
return drainStartHooksRan;
|
|
109
|
+
}
|