@bitkyc08/opencodex 2.59.0 → 2.61.0-preview.20260922
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 +64 -0
- package/README.md +28 -1
- package/bin/ocx.mjs +382 -209
- package/gui/dist/assets/App-E64Rzjap.js +50 -0
- package/gui/dist/assets/Tray-_nfzD8k4.js +1 -0
- package/gui/dist/assets/index-DpdfZWMK.js +86 -0
- package/gui/dist/assets/index-_bpvxJu0.css +1 -0
- package/gui/dist/assets/usage-companion-chart-DtoK7T6h.js +1 -0
- package/gui/dist/favicon.png +0 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/stepfun-color.svg +1 -0
- package/package.json +8 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +20 -1
- package/src/adapters/base.ts +23 -0
- package/src/adapters/coding-agent/protocol.ts +36 -6
- package/src/adapters/coding-agent/turn.ts +32 -4
- package/src/adapters/command-code.ts +52 -4
- package/src/adapters/cursor/catalog.ts +51 -7
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/protobuf-request.ts +6 -3
- package/src/adapters/cursor/request-builder.ts +20 -4
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +14 -3
- package/src/adapters/declaration-carrier.ts +45 -0
- package/src/adapters/devin/cloud-direct/chat.ts +3 -1
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
- package/src/adapters/devin.ts +125 -36
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +34 -7
- package/src/adapters/google-errors.ts +7 -1
- package/src/adapters/google-http.ts +49 -10
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +66 -11
- package/src/adapters/image.ts +4 -1
- package/src/adapters/input-media-guard.ts +21 -9
- package/src/adapters/kiro/usage.ts +3 -2
- package/src/adapters/kiro-tool-fallback.ts +1 -1
- package/src/adapters/ollama-native.ts +6 -0
- package/src/adapters/openai-chat/developer-role.ts +61 -0
- package/src/adapters/openai-chat/messages.ts +46 -27
- package/src/adapters/openai-chat/parallel-tool-calls.ts +32 -0
- package/src/adapters/openai-chat/passthrough.ts +33 -9
- package/src/adapters/openai-chat/reasoning-wire.ts +89 -0
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +23 -58
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +17 -3
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/adapters/registry.ts +3 -2
- package/src/adapters/run-turn-queue.ts +178 -29
- package/src/adapters/xai-web-search.ts +16 -1
- package/src/bridge/errors.ts +8 -2
- package/src/bridge/response-json.ts +9 -1
- package/src/bridge/sse.ts +13 -151
- package/src/chat/inbound.ts +141 -5
- package/src/claude/desktop-3p.ts +7 -1
- package/src/claude/desktop-first-party.ts +183 -0
- package/src/claude/desktop-gateway-state.ts +41 -0
- package/src/claude/inbound-content-options.ts +6 -0
- package/src/claude/inbound.ts +32 -6
- package/src/claude/intercept/connect-proxy.ts +179 -0
- package/src/claude/intercept/listener.ts +122 -0
- package/src/claude/intercept/local-ca.ts +298 -0
- package/src/claude/intercept/runtime.ts +98 -0
- package/src/claude/intercept/settings.ts +189 -0
- package/src/cli/access.ts +87 -0
- package/src/cli/account-auth.ts +19 -0
- package/src/cli/account-extended.ts +4 -4
- package/src/cli/capabilities.ts +31 -0
- package/src/cli/claude-desktop.ts +206 -16
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/companion.ts +56 -0
- package/src/cli/dispatch.ts +46 -7
- package/src/cli/doctor.ts +28 -9
- package/src/cli/ensure-desired-integrations.ts +43 -5
- package/src/cli/help.ts +7 -9
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +203 -59
- package/src/cli/init.ts +8 -0
- package/src/cli/integrations.ts +7 -1
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/cli/registry.ts +41 -2
- package/src/cli/resolve.ts +230 -0
- package/src/cli/root.ts +24 -1
- package/src/cli/start-ownership-publication.ts +56 -0
- package/src/cli/status-probes.ts +2 -18
- package/src/cli/status.ts +62 -0
- package/src/cli/stop-report.ts +143 -0
- package/src/cli/uninstall-plan.ts +9 -0
- package/src/client/machine-api.ts +2 -2
- package/src/client/machine-listener.ts +4 -7
- package/src/client/runtime.ts +26 -2
- package/src/clients/aside-profiles.ts +4 -0
- package/src/clients/config-export/zcode-store.ts +157 -0
- package/src/clients/config-export.ts +36 -0
- package/src/codex/account-store.ts +65 -0
- package/src/codex/app-server-processes.ts +72 -40
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/login-flow.ts +6 -1
- package/src/codex/auth-api/pool-quota-probe.ts +30 -7
- package/src/codex/autostart-health.ts +28 -0
- package/src/codex/catalog/build-entries.ts +2 -2
- package/src/codex/catalog/effort.ts +3 -3
- package/src/codex/catalog/gather-capture.ts +21 -2
- package/src/codex/catalog/model-hints.ts +29 -28
- package/src/codex/catalog/parsing.ts +7 -0
- package/src/codex/catalog/provider-models.ts +43 -17
- package/src/codex/catalog/retained-sync.ts +24 -28
- package/src/codex/catalog/routed-gather.ts +19 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/convergence.ts +2 -2
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +31 -166
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/inject/config-toml.ts +41 -6
- package/src/codex/inject/paginated-openai-compat.ts +90 -0
- package/src/codex/inject.ts +18 -15
- package/src/codex/injected-marker.ts +18 -0
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/main-account.ts +6 -0
- package/src/codex/model-cache.ts +99 -6
- package/src/codex/model-entitlement-admission.ts +59 -0
- package/src/codex/model-entitlements.ts +116 -54
- package/src/codex/native-main-admission.ts +83 -0
- package/src/codex/observed-model-denials.ts +101 -8
- package/src/codex/prompt-text-probe.ts +9 -6
- package/src/codex/routing/health-store.ts +39 -0
- package/src/codex/routing/selection.ts +37 -1
- package/src/codex/routing.ts +12 -42
- package/src/codex/shim-templates.ts +29 -3
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +22 -4
- package/src/combos/failover.ts +3 -0
- package/src/companion/settings.ts +132 -0
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/atomic-write.ts +117 -5
- package/src/config/diagnostics.ts +22 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +52 -7
- package/src/config/process-state.ts +1 -1
- package/src/config/proxy-env.ts +8 -2
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +31 -1
- package/src/config/schema/leaf-validators.ts +59 -0
- package/src/config.ts +3 -3
- package/src/generated/compatibility-version.json +604 -280
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/config-io.ts +44 -10
- package/src/integrations/merge.ts +120 -13
- package/src/integrations/mutation-plan.ts +921 -0
- package/src/integrations/registry.ts +38 -0
- package/src/integrations/state.ts +78 -45
- package/src/integrations/target.ts +208 -0
- package/src/integrations/writer.ts +134 -110
- package/src/lab/conformance/fixture-provider.ts +5 -0
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/browser-launch-notice.ts +59 -0
- package/src/lib/bun-runtime.ts +6 -2
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +63 -0
- package/src/lib/errors.ts +79 -0
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/open-url.ts +51 -7
- package/src/lib/package-tree-integrity.ts +2 -1
- package/src/lib/package-version.ts +8 -0
- package/src/lib/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-egress.ts +310 -0
- package/src/lib/provider-outbound.ts +109 -16
- package/src/lib/proxy-env.ts +82 -7
- package/src/lib/request-execution-budget.ts +72 -0
- package/src/lib/request-failure-attribution.ts +183 -0
- package/src/lib/request-failure-model.ts +236 -0
- package/src/lib/request-resend-gate.ts +138 -0
- package/src/lib/socks5-fetch.ts +136 -26
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +218 -27
- package/src/lib/standalone.ts +16 -0
- package/src/lib/upstream-retry.ts +167 -16
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/lib/winsw.ts +2 -2
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +1 -0
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +27 -108
- package/src/oauth/login-cli.ts +80 -29
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/providers/api-key-resolve.ts +133 -0
- package/src/providers/api-key-selection.ts +5 -1
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/key-failover.ts +97 -19
- package/src/providers/key-store.ts +34 -110
- package/src/providers/model-rename-fields.ts +147 -0
- package/src/providers/model-rename-migration.ts +179 -38
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/vendor-probes-key.ts +38 -23
- package/src/providers/reasoning-metadata.ts +43 -18
- package/src/providers/registry/entries-core.ts +47 -20
- package/src/providers/registry/entries-extended.ts +63 -4
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +56 -10
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/providers/xai-transport.ts +12 -1
- package/src/reasoning-effort.ts +8 -0
- package/src/responses/apply-patch-envelope.ts +0 -12
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- package/src/responses/function-call-compat.ts +38 -1
- package/src/responses/inline-document.ts +65 -0
- package/src/responses/input-media.ts +42 -8
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/muse-tool-name-alias.ts +19 -0
- package/src/responses/parser-content.ts +8 -2
- package/src/responses/parser-tools.ts +3 -0
- package/src/responses/parser.ts +3 -1
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/schema.ts +3 -0
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +108 -117
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- package/src/server/admission-model-scope.ts +219 -0
- package/src/server/audio-live.ts +9 -3
- package/src/server/audio-upstream.ts +18 -0
- package/src/server/auth-cors.ts +29 -0
- package/src/server/chat-completions.ts +60 -4
- package/src/server/chat-native.ts +19 -4
- package/src/server/claude-messages.ts +61 -20
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/grok-responses-snapshot-repair.ts +113 -11
- package/src/server/gui-freshness.ts +103 -0
- package/src/server/gui-static.ts +7 -9
- package/src/server/images.ts +59 -6
- package/src/server/index/claude-intercept-lifecycle.ts +49 -0
- package/src/server/index/serve-options.ts +142 -39
- package/src/server/index/spend-ledger-lifecycle.ts +92 -0
- package/src/server/index/startup-warnings.ts +24 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +34 -38
- package/src/server/lifecycle.ts +4 -4
- package/src/server/live-call-bindings.ts +6 -0
- package/src/server/live.ts +88 -3
- package/src/server/management/agent-settings-routes.ts +121 -36
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/companion-routes.ts +77 -0
- package/src/server/management/config-routes.ts +18 -2
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/logs-usage-routes.ts +19 -0
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/native-integration-routes.ts +103 -6
- package/src/server/management/oauth-account-routes.ts +45 -7
- package/src/server/management/route-registry.ts +20 -0
- package/src/server/management/shared.ts +28 -4
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management/usage-timeline-routes.ts +44 -0
- package/src/server/management-api.ts +10 -9
- package/src/server/management-auth.ts +15 -1
- package/src/server/proxy-liveness.ts +75 -0
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +138 -12
- package/src/server/request-log-failure-attribution.ts +99 -0
- package/src/server/request-log.ts +169 -2
- package/src/server/request-metrics.ts +298 -0
- package/src/server/responses/adapter-continuation.ts +3 -3
- package/src/server/responses/adapter-dispatch.ts +11 -6
- package/src/server/responses/codex-ws-wire.ts +34 -8
- package/src/server/responses/combo-stream-preflight.ts +168 -6
- package/src/server/responses/compact.ts +43 -10
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +8 -3
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +6 -12
- package/src/server/responses/core-opaque-recovery.ts +91 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- package/src/server/responses/fetch-helpers.ts +124 -8
- package/src/server/responses/input-admission.ts +10 -0
- package/src/server/responses/passthrough-delivery.ts +45 -15
- package/src/server/responses/passthrough-dispatch.ts +215 -44
- package/src/server/responses/passthrough-error.ts +27 -8
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +109 -18
- package/src/server/responses/request-send-budget.ts +17 -1
- package/src/server/responses/request-sidecar-auth.ts +1 -1
- package/src/server/responses/request-transport.ts +26 -6
- package/src/server/responses/reset-replay.ts +108 -0
- package/src/server/responses/run-turn-execution.ts +25 -3
- package/src/server/responses/sidecar-execution.ts +17 -2
- package/src/server/responses/ws-upstream.ts +14 -27
- package/src/server/responses-custom-tool-repair.ts +27 -54
- package/src/server/responses-request-tool-scope.ts +214 -0
- package/src/server/responses-undeclared-tool-guard.ts +35 -2
- package/src/server/search.ts +25 -1
- package/src/server/sse-payload-rewrite.ts +1 -1
- package/src/server/usage-ledger-retention.ts +73 -0
- package/src/service/cli.ts +48 -2
- package/src/service/health.ts +3 -2
- package/src/service/install-state-contract.d.mts +27 -0
- package/src/service/install-state-contract.mjs +34 -0
- package/src/service/launchd.ts +1 -1
- package/src/service/orchestration.ts +2 -4
- package/src/service/ownership-compatibility.ts +164 -0
- package/src/service/ownership-mutation-lease.d.mts +32 -0
- package/src/service/ownership-mutation-lease.mjs +211 -0
- package/src/service/repair.ts +45 -1
- package/src/service/state-lock.ts +269 -0
- package/src/service/state-record.d.mts +36 -0
- package/src/service/state-record.mjs +138 -0
- package/src/service/state.ts +582 -68
- package/src/service/windows-taskxml.ts +11 -10
- package/src/service.ts +7 -3
- package/src/tray/windows-tray.ps1 +156 -4
- package/src/types/config.ts +48 -3
- package/src/types/provider.ts +91 -0
- package/src/types/request.ts +30 -2
- package/src/types/tools.ts +33 -0
- package/src/types.ts +4 -0
- package/src/update/index.ts +207 -63
- package/src/update/job.ts +9 -5
- package/src/update/ownership-transaction.ts +47 -0
- package/src/update/restart-ownership.ts +54 -0
- package/src/update/runtime-ownership.d.mts +40 -0
- package/src/update/runtime-ownership.mjs +122 -0
- package/src/usage/attempt-delivery.ts +198 -0
- package/src/usage/cache-diagnostic.ts +305 -0
- package/src/usage/failure-fingerprint.ts +118 -0
- package/src/usage/failure-projection-cache.ts +174 -0
- package/src/usage/failure-projection.ts +174 -0
- package/src/usage/ledger-retention.ts +165 -0
- package/src/usage/log.ts +126 -79
- package/src/usage/request-outcome.ts +150 -0
- package/src/usage/retention-contract.ts +28 -0
- package/src/usage/summary.ts +2 -2
- package/src/usage/telemetry-contract.ts +237 -0
- package/src/usage/timeline.ts +236 -0
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/alpha-search.ts +21 -1
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/gui/dist/assets/index-C5IebErG.js +0 -136
- package/gui/dist/assets/index-OESInAjC.css +0 -1
|
@@ -35,12 +35,13 @@ import { clearThreadAccountMap } from "../../codex/routing";
|
|
|
35
35
|
import { primeCodexPoolQuotas } from "../../codex/auth-api";
|
|
36
36
|
import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
|
|
37
37
|
import { resolveCodexHomeDir } from "../../codex/home";
|
|
38
|
-
import { readUsageEntries } from "../../usage/log";
|
|
38
|
+
import { isKnownRequestFailureCause, isKnownRequestFailureStage, readUsageEntries } from "../../usage/log";
|
|
39
39
|
import { getUsageDebugLogEntries } from "../../usage/debug";
|
|
40
40
|
import { cacheObservationFromUsage, parseRange, parseUsageSurface, summarizeUsage } from "../../usage/summary";
|
|
41
41
|
import { stripCodexRuntimeProviderFields } from "../../codex/auth-context";
|
|
42
42
|
import { getProviderRegistryEntry, providerMatchesRegistryTransport } from "../../providers/registry";
|
|
43
43
|
import { getDebugLogEntries } from "../../lib/debug-log-buffer";
|
|
44
|
+
import { resendPermission } from "../../lib/request-failure-model";
|
|
44
45
|
import { getInjectionDebugLogEntries } from "../../lib/injection-debug-log";
|
|
45
46
|
import {
|
|
46
47
|
clearDebugSettings,
|
|
@@ -210,12 +211,27 @@ export function costResult(entry: MetricSource): CostResult {
|
|
|
210
211
|
* a Logs-page metric, and widening a separate endpoint's response shape is not this change's
|
|
211
212
|
* business. Flipping it on later is one argument.
|
|
212
213
|
*/
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Whether this proxy could have sent the row again, derived at READ time from the stage and
|
|
217
|
+
* cause the recorder stored.
|
|
218
|
+
*
|
|
219
|
+
* Deliberately not persisted. The verdict is a function of two tables that this build owns, and
|
|
220
|
+
* a row written months ago must not be able to assert a permission the current tables would
|
|
221
|
+
* refuse -- the whole point of INV-RESEND-01 is that the refusal rules are one statement, and a
|
|
222
|
+
* stored verdict would be a second one with no way to correct it.
|
|
223
|
+
*/
|
|
224
|
+
function resendVerdict(row: { failureStage?: string; failureCause?: string }): { resendPermission?: string } {
|
|
225
|
+
if (!isKnownRequestFailureStage(row.failureStage) || !isKnownRequestFailureCause(row.failureCause)) return {};
|
|
226
|
+
return { resendPermission: resendPermission(row.failureStage, row.failureCause) };
|
|
227
|
+
}
|
|
213
228
|
export function requestLogDto(
|
|
214
229
|
entry: RequestLogEntry,
|
|
215
230
|
{ includeDecodeRate = true }: { includeDecodeRate?: boolean } = {},
|
|
216
231
|
): Record<string, unknown> {
|
|
217
232
|
return {
|
|
218
233
|
...entry,
|
|
234
|
+
...resendVerdict(entry),
|
|
219
235
|
displayMetrics: {
|
|
220
236
|
tokPerSecond: tokPerSecondResult(entry),
|
|
221
237
|
// The parent uses the REQUEST's own TTFT. A combo parent must not borrow an attempt's,
|
|
@@ -227,6 +243,7 @@ export function requestLogDto(
|
|
|
227
243
|
? {
|
|
228
244
|
attempts: entry.attempts.map(attempt => ({
|
|
229
245
|
...attempt,
|
|
246
|
+
...resendVerdict(attempt),
|
|
230
247
|
displayMetrics: {
|
|
231
248
|
tokPerSecond: tokPerSecondResult(attempt),
|
|
232
249
|
// Each attempt measures its own attempt-relative TTFT.
|
|
@@ -245,12 +262,19 @@ export function requestLogDto(
|
|
|
245
262
|
* share the same fetch, the same per-provider cache (dedups Codex's frequent /v1/models polling),
|
|
246
263
|
* and the same stale fallback when a provider blips, instead of a parallel uncached copy.
|
|
247
264
|
*/
|
|
248
|
-
export async function fetchAllModels(
|
|
265
|
+
export async function fetchAllModels(
|
|
266
|
+
config: OcxConfig,
|
|
267
|
+
/** Filled with each provider's content revision as of the moment its rows were chosen. */
|
|
268
|
+
providerContentRevisions?: Map<string, string>,
|
|
269
|
+
): Promise<CatalogModel[]> {
|
|
249
270
|
const { gatherRoutedModels } = await import("../../codex/catalog");
|
|
250
271
|
const baseline = captureInitialSelectionBaseline(config);
|
|
251
|
-
if (!baseline) return gatherRoutedModels(config);
|
|
272
|
+
if (!baseline) return gatherRoutedModels(config, providerContentRevisions ? { providerContentRevisions } : undefined);
|
|
252
273
|
const outcomes: Array<{ provider: string; state: "authoritative" | "degraded" }> = [];
|
|
253
|
-
const models = await gatherRoutedModels(config, {
|
|
274
|
+
const models = await gatherRoutedModels(config, {
|
|
275
|
+
providerModelOutcomes: outcomes,
|
|
276
|
+
...(providerContentRevisions ? { providerContentRevisions } : {}),
|
|
277
|
+
});
|
|
254
278
|
finalizeInitialModelSelection(config, baseline, uniqueCatalogModelsForPublicList(models),
|
|
255
279
|
outcomes.filter(outcome => outcome.state === "authoritative").map(outcome => outcome.provider));
|
|
256
280
|
return models;
|
|
@@ -36,6 +36,7 @@ import { isServiceViable } from "../../service";
|
|
|
36
36
|
import { readRuntimePort } from "../../config/process-state";
|
|
37
37
|
import { withProcessRuntimeProvenance } from "../../lib/bun-runtime";
|
|
38
38
|
import { selfLaunchArgv } from "../../lib/self-launch-argv";
|
|
39
|
+
import { spendLedgerRestartEnvironment } from "../../lib/spend-ledger-owner";
|
|
39
40
|
import {
|
|
40
41
|
MEMORY_DRAIN_RESTART_MS,
|
|
41
42
|
REPLACEMENT_READY_TIMEOUT_MS,
|
|
@@ -225,8 +226,12 @@ function spawnDetachedStart(
|
|
|
225
226
|
return new Promise<void>((resolve, reject) => {
|
|
226
227
|
let child: ReturnType<typeof spawn>;
|
|
227
228
|
try {
|
|
228
|
-
const
|
|
229
|
-
delete
|
|
229
|
+
const sourceEnv: NodeJS.ProcessEnv = { ...process.env };
|
|
230
|
+
delete sourceEnv.OCX_SERVICE;
|
|
231
|
+
const env = spendLedgerRestartEnvironment(
|
|
232
|
+
sourceEnv,
|
|
233
|
+
waitForHealthBeforeParentExit ? undefined : process.pid,
|
|
234
|
+
);
|
|
230
235
|
child = spawn(process.execPath, launchArgs, {
|
|
231
236
|
detached: true,
|
|
232
237
|
stdio: "ignore",
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
} from "../../lib/codex-restart-contract";
|
|
40
40
|
import { jsonResponse } from "../auth-cors";
|
|
41
41
|
import { getInspectionCounters } from "../relay";
|
|
42
|
+
import { spendLedgerDiagnosticsSnapshot } from "../../lib/spend-reservation-ledger";
|
|
42
43
|
import type {
|
|
43
44
|
performCodexRestart,
|
|
44
45
|
readCodexAppServerState,
|
|
@@ -60,6 +61,7 @@ export async function handleSystemRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
60
61
|
version,
|
|
61
62
|
uptime: process.uptime(),
|
|
62
63
|
pid: process.pid,
|
|
64
|
+
spendLedger: spendLedgerDiagnosticsSnapshot(),
|
|
63
65
|
});
|
|
64
66
|
}
|
|
65
67
|
if (url.pathname === "/api/system/memory" && req.method === "GET") {
|
|
@@ -66,6 +66,7 @@ const pinnedAggregates = new Set<RetainedUsageAggregate>();
|
|
|
66
66
|
let baseFlight: Promise<UsageAggregateResult> | null = null;
|
|
67
67
|
const filteredFlights = new Map<string, Promise<UsageAggregateResult>>();
|
|
68
68
|
const retainedFilteredAggregates = new Map<string, RetainedUsageAggregate>();
|
|
69
|
+
const MAX_CONCURRENT_FILTERED_AGGREGATES = 4;
|
|
69
70
|
|
|
70
71
|
function currentTimeZone(): string {
|
|
71
72
|
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
@@ -283,6 +284,9 @@ export async function getFilteredUsageAggregate(filter: {
|
|
|
283
284
|
]);
|
|
284
285
|
const existing = filteredFlights.get(key);
|
|
285
286
|
if (existing) return existing;
|
|
287
|
+
if (filteredFlights.size >= MAX_CONCURRENT_FILTERED_AGGREGATES) {
|
|
288
|
+
throw new Error("too many concurrent filtered usage aggregates");
|
|
289
|
+
}
|
|
286
290
|
|
|
287
291
|
const flight = refreshFilteredAggregate(key, normalizedFilter, fixedWindow);
|
|
288
292
|
filteredFlights.set(key, flight);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { readUsageSnapshotForManagement } from "../../usage/log";
|
|
2
|
+
import { createTimelineAccumulator, parseTimelineQuery } from "../../usage/timeline";
|
|
3
|
+
import { jsonResponse } from "../auth-cors";
|
|
4
|
+
import type { ManagementContext } from "./context";
|
|
5
|
+
|
|
6
|
+
const TIMELINE_CACHE_TTL_MS = 15_000;
|
|
7
|
+
const cache = new Map<string, { expiresAt: number; promise: Promise<ReturnType<ReturnType<typeof createTimelineAccumulator>["finish"]>> }>();
|
|
8
|
+
|
|
9
|
+
export async function handleUsageTimelineRoutes(ctx: ManagementContext): Promise<Response | undefined> {
|
|
10
|
+
const { req, url } = ctx;
|
|
11
|
+
if (url.pathname !== "/api/usage/timeline" || req.method !== "GET") return undefined;
|
|
12
|
+
const query = parseTimelineQuery(url.searchParams, Date.now());
|
|
13
|
+
if ("error" in query) return jsonResponse(query, 400, req, ctx.config);
|
|
14
|
+
const bucketMs = query.bucketMinutes * 60_000;
|
|
15
|
+
const roundedNow = Math.floor(query.now / bucketMs) * bucketMs;
|
|
16
|
+
const normalized = { ...query, now: roundedNow };
|
|
17
|
+
const key = JSON.stringify(normalized);
|
|
18
|
+
const current = Date.now();
|
|
19
|
+
const cached = cache.get(key);
|
|
20
|
+
if (cached && cached.expiresAt > current) return jsonResponse(await cached.promise, 200, req, ctx.config);
|
|
21
|
+
let promise: Promise<ReturnType<ReturnType<typeof createTimelineAccumulator>["finish"]>>;
|
|
22
|
+
promise = (async () => {
|
|
23
|
+
const accumulator = createTimelineAccumulator(normalized);
|
|
24
|
+
const snapshot = await readUsageSnapshotForManagement(ctx.config.managementUsageMaxReadBytes);
|
|
25
|
+
for (const entry of snapshot.entries) accumulator.add(entry);
|
|
26
|
+
return {
|
|
27
|
+
...accumulator.finish(),
|
|
28
|
+
truncated: snapshot.truncatedPrefixBytes > 0 || snapshot.entriesTruncated,
|
|
29
|
+
};
|
|
30
|
+
})().catch(error => {
|
|
31
|
+
const entry = cache.get(key);
|
|
32
|
+
if (entry?.promise === promise) cache.delete(key);
|
|
33
|
+
throw error;
|
|
34
|
+
});
|
|
35
|
+
cache.set(key, { expiresAt: current + TIMELINE_CACHE_TTL_MS, promise });
|
|
36
|
+
try {
|
|
37
|
+
return jsonResponse(await promise, 200, req, ctx.config);
|
|
38
|
+
} finally {
|
|
39
|
+
setTimeout(() => {
|
|
40
|
+
const entry = cache.get(key);
|
|
41
|
+
if (entry?.promise === promise && entry.expiresAt <= Date.now()) cache.delete(key);
|
|
42
|
+
}, TIMELINE_CACHE_TTL_MS + 1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { remoteWorkspaceEnabled } from "../remote-control/workspace-activation";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { readFileSync } from "node:fs";
|
|
4
3
|
import type { CatalogModel } from "../codex/catalog";
|
|
5
4
|
import { catalogModelSlug, invalidateCodexModelsCache, nativeContextLimits, nativeModelRows, uniqueCatalogModelsForPublicList } from "../codex/catalog";
|
|
6
5
|
import {
|
|
@@ -63,6 +62,7 @@ import { handleLogsUsageRoutes } from "./management/logs-usage-routes";
|
|
|
63
62
|
import { handleStorageLogGuardRoutes } from "./management/storage-log-guard-routes";
|
|
64
63
|
import { handleRequestHistoryRoutes } from "./management/request-history-routes";
|
|
65
64
|
import { handleRoutingAnalyticsRoutes } from "./management/routing-analytics-routes";
|
|
65
|
+
import { handleMetricsRoutes } from "./management/metrics-routes";
|
|
66
66
|
import { handleProviderRoutes } from "./management/provider-routes";
|
|
67
67
|
import { handleModelRoutes } from "./management/model-routes";
|
|
68
68
|
import { handleAgentSettingsRoutes } from "./management/agent-settings-routes";
|
|
@@ -70,6 +70,8 @@ import { handleOauthAccountRoutes } from "./management/oauth-account-routes";
|
|
|
70
70
|
import { handleComboRoutes } from "./management/combo-routes";
|
|
71
71
|
import { handleSystemRoutes } from "./management/system-routes";
|
|
72
72
|
import { handleSidebarRoutes } from "./management/sidebar-routes";
|
|
73
|
+
import { handleUsageTimelineRoutes } from "./management/usage-timeline-routes";
|
|
74
|
+
import { handleCompanionRoutes } from "./management/companion-routes";
|
|
73
75
|
import { handleCodexPromptRoutes } from "./management/codex-prompt-routes";
|
|
74
76
|
import { handleIntegrationRoutes } from "./management/integration-routes";
|
|
75
77
|
import { handleNativeIntegrationRoutes } from "./management/native-integration-routes";
|
|
@@ -83,15 +85,11 @@ import type { CatalogDisposition, ConvergeCodex } from "../codex/convergence-typ
|
|
|
83
85
|
import { normalizeCatalogDisposition } from "../codex/catalog-refresh-status";
|
|
84
86
|
import { managementBodyTooLargeResponse } from "./management/body";
|
|
85
87
|
import { handleSessionRoutes } from "./management/session-routes";
|
|
88
|
+
import { packageVersion } from "../lib/package-version";
|
|
86
89
|
|
|
87
90
|
// installed npm version instead of a stale hardcode.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version as string;
|
|
91
|
-
} catch {
|
|
92
|
-
return "0.0.0";
|
|
93
|
-
}
|
|
94
|
-
})();
|
|
91
|
+
const MANAGEMENT_VERSION_FALLBACK = "0.0.0";
|
|
92
|
+
export const VERSION = packageVersion(MANAGEMENT_VERSION_FALLBACK);
|
|
95
93
|
|
|
96
94
|
const managementConvergenceBindings = new WeakMap<object, Readonly<{
|
|
97
95
|
factory: (config: Readonly<OcxConfig>) => ConvergeCodex;
|
|
@@ -265,7 +263,7 @@ export async function handleManagementAPI(
|
|
|
265
263
|
} catch { /* best-effort */ }
|
|
266
264
|
}
|
|
267
265
|
const ctx: ManagementContext = { req, url, config, deps, version: VERSION, principal, sessionControl, convergeCodexCatalog, syncClaudeAgentDefsBestEffort };
|
|
268
|
-
let routed: Response | null;
|
|
266
|
+
let routed: Response | null | undefined;
|
|
269
267
|
try {
|
|
270
268
|
routed = handleSessionRoutes(ctx)
|
|
271
269
|
?? (await handleRemoteWorkspaceRoutesOnDemand(ctx))
|
|
@@ -276,6 +274,7 @@ export async function handleManagementAPI(
|
|
|
276
274
|
?? (await handleQuotaResetRoutesOnDemand(ctx))
|
|
277
275
|
?? (await handleWorkflowBudgetRoutesOnDemand(ctx))
|
|
278
276
|
?? (await handleGrokCouponRoutesOnDemand(ctx))
|
|
277
|
+
?? handleMetricsRoutes(ctx)
|
|
279
278
|
?? (await handleRoutingAnalyticsRoutes(ctx))
|
|
280
279
|
?? (await handleRoutingProfileRoutesOnDemand(ctx))
|
|
281
280
|
?? (await handleProviderRoutes(ctx))
|
|
@@ -289,6 +288,8 @@ export async function handleManagementAPI(
|
|
|
289
288
|
?? (await handleComboRoutes(ctx))
|
|
290
289
|
?? (await handleSystemRoutes(ctx))
|
|
291
290
|
?? (await handleLabRoutesOnDemand(ctx))
|
|
291
|
+
?? (await handleUsageTimelineRoutes(ctx))
|
|
292
|
+
?? (await handleCompanionRoutes(ctx))
|
|
292
293
|
?? (await handleSidebarRoutes(ctx));
|
|
293
294
|
} catch (error) {
|
|
294
295
|
const tooLarge = managementBodyTooLargeResponse(error, req, config);
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
writeFileSync,
|
|
13
13
|
} from "node:fs";
|
|
14
14
|
import { dirname, join } from "node:path";
|
|
15
|
-
import { adminApiTokenFilePath } from "../lib/admin-secrets";
|
|
15
|
+
import { adminApiTokenFilePath, opencodeCatalogToken } from "../lib/admin-secrets";
|
|
16
16
|
import {
|
|
17
17
|
LOCAL_MANAGEMENT_CAPABILITY_HEADER,
|
|
18
18
|
LOCAL_MANAGEMENT_CAPABILITY_EXPIRES_AT_HEADER,
|
|
@@ -297,6 +297,7 @@ export function createManagementSessionControl(state: ManagementAuthState): Mana
|
|
|
297
297
|
*/
|
|
298
298
|
export type ManagementPrincipal =
|
|
299
299
|
| "admin-token"
|
|
300
|
+
| "opencode-catalog-token"
|
|
300
301
|
| "gui-session"
|
|
301
302
|
| "gui-pair-capability"
|
|
302
303
|
| "local-read-capability"
|
|
@@ -484,6 +485,16 @@ function requestManagementCredential(req: Request): string | null {
|
|
|
484
485
|
|| null;
|
|
485
486
|
}
|
|
486
487
|
|
|
488
|
+
function isOpencodeCatalogRequest(req: Request): boolean {
|
|
489
|
+
if (req.method !== "GET") return false;
|
|
490
|
+
try {
|
|
491
|
+
const url = new URL(req.url);
|
|
492
|
+
return url.pathname === "/api/models" && url.search === "";
|
|
493
|
+
} catch {
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
487
498
|
function resolveManagementAdmission(
|
|
488
499
|
req: Request,
|
|
489
500
|
state: ManagementAuthState,
|
|
@@ -500,6 +511,9 @@ function resolveManagementAdmission(
|
|
|
500
511
|
else if (state.available) {
|
|
501
512
|
const actual = requestManagementCredential(req);
|
|
502
513
|
if (actual && equalSecret(actual, state.token)) principal = "admin-token";
|
|
514
|
+
else if (actual && isOpencodeCatalogRequest(req) && equalSecret(actual, opencodeCatalogToken(state.token))) {
|
|
515
|
+
principal = "opencode-catalog-token";
|
|
516
|
+
}
|
|
503
517
|
else if (config && authorizeGuiSessionRequest(req, config, state).ok) principal = "gui-session";
|
|
504
518
|
}
|
|
505
519
|
if (principal) admittedManagementRequests.set(req, principal);
|
|
@@ -32,6 +32,8 @@ export interface HealthzIdentity {
|
|
|
32
32
|
guiPairCapability?: unknown;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
export type EndpointLiveness = "live" | "dead" | "unknown";
|
|
36
|
+
|
|
35
37
|
export interface LivenessIo {
|
|
36
38
|
fetchFn?: typeof fetch;
|
|
37
39
|
readPidFn?: () => number | null;
|
|
@@ -85,6 +87,11 @@ export const START_OWNERSHIP_LIVENESS: Pick<LivenessIo, "timeoutMs" | "attempts"
|
|
|
85
87
|
attempts: 3,
|
|
86
88
|
};
|
|
87
89
|
|
|
90
|
+
type LivenessFetch = (
|
|
91
|
+
input: string | URL | Request,
|
|
92
|
+
init?: RequestInit,
|
|
93
|
+
) => Promise<Response>;
|
|
94
|
+
|
|
88
95
|
export interface LiveProxy {
|
|
89
96
|
pid: number | null;
|
|
90
97
|
port: number;
|
|
@@ -148,6 +155,74 @@ export function isOpencodexHealthz(body: HealthzIdentity | null): boolean {
|
|
|
148
155
|
return body.status === "ok" && typeof body.version === "string" && typeof body.uptime === "number";
|
|
149
156
|
}
|
|
150
157
|
|
|
158
|
+
/**
|
|
159
|
+
* "Nothing is listening" is narrower than "the probe failed". Only a connect-phase refusal
|
|
160
|
+
* proves the endpoint is free; a timeout, reset, or other transport failure leaves the
|
|
161
|
+
* question open.
|
|
162
|
+
*/
|
|
163
|
+
export function isConnectionRefused(error: unknown): boolean {
|
|
164
|
+
const visit = (current: unknown, depth: number): boolean => {
|
|
165
|
+
if (depth >= 4) return false;
|
|
166
|
+
if (current === null || (typeof current !== "object" && typeof current !== "function")) return false;
|
|
167
|
+
const record = current as { code?: unknown; cause?: unknown; errors?: unknown };
|
|
168
|
+
if (record.code === "ECONNREFUSED" || record.code === "ConnectionRefused") return true;
|
|
169
|
+
if (typeof record.code === "string" && record.code.endsWith("ECONNREFUSED")) return true;
|
|
170
|
+
if (Array.isArray(record.errors) && record.errors.length > 0) {
|
|
171
|
+
// One connect attempt fanned out over several addresses reports a single AggregateError.
|
|
172
|
+
// Only a unanimous refusal proves the endpoint is free: a bundle that mixes ECONNREFUSED
|
|
173
|
+
// with a timeout means one address answered nothing at all, and an address whose state is
|
|
174
|
+
// unreadable is unknown, not absence. Collapsing it to "refused" is how a second runtime
|
|
175
|
+
// gets started on a port that already has one.
|
|
176
|
+
return record.errors.every(error => visit(error, depth + 1));
|
|
177
|
+
}
|
|
178
|
+
return visit(record.cause, depth + 1);
|
|
179
|
+
};
|
|
180
|
+
return visit(error, 0);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function classifyHealthz(
|
|
184
|
+
url: string,
|
|
185
|
+
fetchFn: LivenessFetch,
|
|
186
|
+
timeoutMs: number,
|
|
187
|
+
): Promise<EndpointLiveness> {
|
|
188
|
+
try {
|
|
189
|
+
const response = await fetchFn(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
190
|
+
if (response.status !== 200) return "unknown";
|
|
191
|
+
const body = (await response.json().catch(() => undefined)) as HealthzIdentity | null | undefined;
|
|
192
|
+
if (body === undefined) return "unknown";
|
|
193
|
+
return isOpencodexHealthz(body) ? "live" : "dead";
|
|
194
|
+
} catch (error) {
|
|
195
|
+
return isConnectionRefused(error) ? "dead" : "unknown";
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Tri-state probe of one endpoint, the in-process counterpart of
|
|
201
|
+
* `src/update/proxy-liveness-probe.mjs`. Only a connect-phase refusal or a clean 200 that is
|
|
202
|
+
* not ours proves "dead"; a timeout, reset, non-200 or unreadable body leaves the question
|
|
203
|
+
* open. Loopback endpoints are checked on both IPv4 and IPv6 because a listener may bind only
|
|
204
|
+
* one family. Runs in-process because a compiled standalone binary cannot fork `execPath -e`.
|
|
205
|
+
*/
|
|
206
|
+
export async function probeEndpointLiveness(
|
|
207
|
+
endpoint: { port: number; hostname?: string },
|
|
208
|
+
io: Pick<LivenessIo, "fetchFn" | "timeoutMs"> = {},
|
|
209
|
+
): Promise<EndpointLiveness> {
|
|
210
|
+
if (!Number.isFinite(endpoint.port) || endpoint.port <= 0 || endpoint.port > 65535) return "dead";
|
|
211
|
+
const fetchFn = io.fetchFn ?? directLocalHttpFetch;
|
|
212
|
+
const timeoutMs = io.timeoutMs ?? 1500;
|
|
213
|
+
let sawUnknown = false;
|
|
214
|
+
for (const hostname of loopbackProbeHosts(endpoint.hostname)) {
|
|
215
|
+
const result = await classifyHealthz(
|
|
216
|
+
`http://${hostname}:${endpoint.port}/healthz`,
|
|
217
|
+
fetchFn,
|
|
218
|
+
timeoutMs,
|
|
219
|
+
);
|
|
220
|
+
if (result === "live") return "live";
|
|
221
|
+
if (result === "unknown") sawUnknown = true;
|
|
222
|
+
}
|
|
223
|
+
return sawUnknown ? "unknown" : "dead";
|
|
224
|
+
}
|
|
225
|
+
|
|
151
226
|
/** Identity-checked /healthz probe; null when unreachable, non-OK, or not our proxy. */
|
|
152
227
|
export async function proxyIdentityAt(
|
|
153
228
|
port: number,
|
package/src/server/readiness.ts
CHANGED
|
@@ -3,10 +3,30 @@
|
|
|
3
3
|
*
|
|
4
4
|
* `GET /healthz` answers "is the process alive and serving HTTP?" the instant the
|
|
5
5
|
* listener binds. Readiness is stricter: the proxy is "ready" only after the
|
|
6
|
-
* post-startup Codex catalog/config sync (`syncModelsToCodex`) has
|
|
7
|
-
* `ok=true
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* post-startup Codex catalog/config sync (`syncModelsToCodex`) has SETTLED and
|
|
7
|
+
* reported `ok=true`. Until it settles the process is live (Codex can open a
|
|
8
|
+
* socket) but not ready — a request would race the sync — so clients back off.
|
|
9
|
+
*
|
|
10
|
+
* What readiness is NOT (#5181): it is not a verdict on the local Codex client's
|
|
11
|
+
* artifacts. A nonempty `warning` used to be terminal here, which made a
|
|
12
|
+
* degradation of files written into the local Codex home permanently un-ready a
|
|
13
|
+
* proxy that was serving every other provider correctly. In a single-replica
|
|
14
|
+
* Kubernetes deployment that removed the only Service endpoint.
|
|
15
|
+
*
|
|
16
|
+
* `ok` and `warning` are separate fields in the sync result because they answer
|
|
17
|
+
* separate questions, and the gate must not conflate them. `ok` is the sync's own
|
|
18
|
+
* verdict on whether the essential work — config injection, and the write
|
|
19
|
+
* admission that precedes it — succeeded. `warning` names a degradation the sync
|
|
20
|
+
* itself decided to continue past: no catalog source so Codex keeps its native
|
|
21
|
+
* catalog, combos omitted from the catalog, a conversation-history relabel left
|
|
22
|
+
* to Codex's own writer, or a caught catalog-refresh exception after which
|
|
23
|
+
* injection still runs and still reports its own `ok`. None of those stops this
|
|
24
|
+
* process from accepting HTTP or routing to a provider, so none of them may close
|
|
25
|
+
* the gate.
|
|
26
|
+
*
|
|
27
|
+
* That boundary is not new, only extended: the Claude Code roster reconciliation
|
|
28
|
+
* in `src/cli/claude-agent-startup-sync.ts` already delays the ready transition
|
|
29
|
+
* without being allowed to fail it, on the same reasoning.
|
|
10
30
|
*
|
|
11
31
|
* Design contract (per P1 review):
|
|
12
32
|
* - NO module-global mutable state. Each `startServer` invocation gets its own
|
|
@@ -65,8 +85,11 @@ export interface SyncOutcomeLike {
|
|
|
65
85
|
|
|
66
86
|
/**
|
|
67
87
|
* Drive the gate from the post-startup sync. Awaits `syncFn`; the gate goes to
|
|
68
|
-
* `ready`
|
|
69
|
-
*
|
|
88
|
+
* `ready` on `ok=true`. A throw, `null`, or `ok !== true` transitions to
|
|
89
|
+
* `failed`; a nonempty `warning` does not, because the sync that produced it
|
|
90
|
+
* still reported the essential work done (see the file header, #5181). A throw
|
|
91
|
+
* and `null` stay terminal because neither is a classified outcome: the startup
|
|
92
|
+
* path did not reach a verdict, so the gate cannot claim one. Used by `handleStart`
|
|
70
93
|
* so the startup transition is unit-testable without spawning the proxy. Returns
|
|
71
94
|
* the raw sync outcome so a caller that also needs the #1046 write flags (did the
|
|
72
95
|
* sync actually write the catalog/cache?) can keep them without a second call.
|
|
@@ -90,10 +113,6 @@ export async function runStartupReadinessSync(
|
|
|
90
113
|
gate.markFailed();
|
|
91
114
|
return result;
|
|
92
115
|
}
|
|
93
|
-
if (result.warning !== undefined && result.warning !== "") {
|
|
94
|
-
gate.markFailed();
|
|
95
|
-
return result;
|
|
96
|
-
}
|
|
97
116
|
gate.markReady();
|
|
98
117
|
return result;
|
|
99
118
|
}
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
createSseTerminalOutputBoundary,
|
|
31
31
|
doneFrame,
|
|
32
32
|
failedTailFrame,
|
|
33
|
+
refusalFailedTailFrame,
|
|
33
34
|
upstreamErrorTailFrame,
|
|
34
35
|
} from "./relay";
|
|
35
36
|
import {
|
|
@@ -127,6 +128,23 @@ export function relaySseEagerBounded(
|
|
|
127
128
|
return null;
|
|
128
129
|
}
|
|
129
130
|
};
|
|
131
|
+
/**
|
|
132
|
+
* Same tail, except that a refusal the boundary already captured outranks the
|
|
133
|
+
* read failure that followed it — the upstream ended the turn before the
|
|
134
|
+
* socket did (#5176).
|
|
135
|
+
*/
|
|
136
|
+
const encodeTerminalTail = (error: unknown): Uint8Array | null => {
|
|
137
|
+
const refusalCode = terminalBoundary.upstreamRefusalCode();
|
|
138
|
+
const refusalMessage = terminalBoundary.upstreamError();
|
|
139
|
+
if (refusalCode === undefined || refusalMessage === undefined) {
|
|
140
|
+
return encodeFailedTail(error);
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
return refusalFailedTailFrame(terminalEncoder, refusalMessage, refusalCode);
|
|
144
|
+
} catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
130
148
|
const rewriteDecoder = activeRewrite ? new TextDecoder() : null;
|
|
131
149
|
const rewriteEncoder = activeRewrite ? new TextEncoder() : null;
|
|
132
150
|
const rewriteBudget = opts?.rewriteBudget;
|
|
@@ -313,7 +331,11 @@ export function relaySseEagerBounded(
|
|
|
313
331
|
const upstreamError = terminalBoundary.upstreamError() ?? opts?.upstreamError;
|
|
314
332
|
const upstreamErrorFrame = upstreamError === undefined
|
|
315
333
|
? adapterEofFrame
|
|
316
|
-
: upstreamErrorTailFrame(
|
|
334
|
+
: upstreamErrorTailFrame(
|
|
335
|
+
terminalEncoder,
|
|
336
|
+
upstreamError,
|
|
337
|
+
terminalBoundary.upstreamRefusalCode(),
|
|
338
|
+
);
|
|
317
339
|
queuedBytes += upstreamErrorFrame.byteLength + terminalSentinel.byteLength;
|
|
318
340
|
try {
|
|
319
341
|
controllerRef?.enqueue(upstreamErrorFrame);
|
|
@@ -439,7 +461,7 @@ export function relaySseEagerBounded(
|
|
|
439
461
|
// getters, toString) that re-entrantly cancel the client or abort the
|
|
440
462
|
// upstream. Build the tail FIRST, then re-check eligibility before
|
|
441
463
|
// committing to the synthetic terminal (adversarial review blocker).
|
|
442
|
-
const tail =
|
|
464
|
+
const tail = encodeTerminalTail(err);
|
|
443
465
|
if (tail && canDeliver()) {
|
|
444
466
|
// Inspection and client framing have separate bounded parsers. If
|
|
445
467
|
// inspection resynchronized after an oversized frame and observed a
|