@bitkyc08/opencodex 2.84.0 → 2.87.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/README.md +0 -4
- package/bin/ocx.mjs +61 -3
- package/gui/dist/assets/App-ZfYbLHkB.js +58 -0
- package/gui/dist/assets/App-wT3mXqFz.css +1 -0
- package/gui/dist/assets/{Tray-C9TGtwad.js → Tray-DCd-A1az.js} +1 -1
- package/gui/dist/assets/index-CfYDMj34.js +94 -0
- package/gui/dist/assets/index-DPeB9HpV.css +1 -0
- package/gui/dist/assets/quota-summary-ChvmKGOk.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -2
- package/src/adapters/anthropic/beta-allowlist.ts +91 -11
- package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
- package/src/adapters/anthropic/client-identity.ts +1 -1
- package/src/adapters/anthropic/passthrough.ts +16 -3
- package/src/adapters/anthropic-progress.ts +84 -0
- package/src/adapters/anthropic.ts +108 -21
- package/src/adapters/azure.ts +19 -1
- package/src/adapters/base.ts +2 -0
- package/src/adapters/claude-cli/adapter.ts +111 -23
- package/src/adapters/claude-cli/profiles.ts +2 -1
- package/src/adapters/claude-cli/stable-replay.ts +108 -0
- package/src/adapters/claude-cli/usage-admission.ts +230 -0
- package/src/adapters/client-fingerprint.ts +24 -0
- package/src/adapters/coding-agent/protocol.ts +1 -1
- package/src/adapters/coding-agent/turn.ts +40 -11
- package/src/adapters/cursor/arg-codec.ts +17 -5
- package/src/adapters/cursor/catalog.ts +8 -0
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/cursor/freeform-args.ts +75 -0
- package/src/adapters/cursor/protobuf-events.ts +36 -23
- package/src/adapters/dsh-account.ts +97 -0
- package/src/adapters/google-antigravity-wire.ts +22 -1
- package/src/adapters/google.ts +37 -10
- package/src/adapters/openai-chat/messages.ts +4 -3
- package/src/adapters/openai-chat/passthrough.ts +8 -2
- package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
- package/src/adapters/openai-chat/response-events.ts +59 -1
- package/src/adapters/openai-chat.ts +8 -5
- package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
- package/src/adapters/openai-responses/passthrough.ts +57 -125
- package/src/adapters/openai-responses/request-strips.ts +25 -3
- package/src/adapters/openai-responses/transport.ts +81 -0
- package/src/adapters/qoder/adapter.ts +44 -3
- package/src/adapters/registry.ts +5 -0
- package/src/adapters/zed.ts +26 -1
- package/src/advisor/activity.ts +55 -0
- package/src/advisor/budget.ts +62 -0
- package/src/advisor/consult.ts +223 -0
- package/src/advisor/context.ts +238 -0
- package/src/advisor/disclosure.ts +20 -0
- package/src/advisor/evidence-tool.ts +21 -0
- package/src/advisor/evidence.ts +117 -0
- package/src/advisor/runtime.ts +353 -0
- package/src/advisor/settings.ts +144 -0
- package/src/advisor/state.ts +369 -0
- package/src/advisor/synthetic-tool.ts +33 -0
- package/src/bridge/errors.ts +24 -1
- package/src/bridge/internal.ts +3 -1
- package/src/bridge/response-json.ts +18 -3
- package/src/bridge/sse.ts +58 -8
- package/src/bridge/terminal-refusal.ts +59 -0
- package/src/chat/inbound.ts +30 -6
- package/src/chat/outbound.ts +5 -0
- package/src/claude/desktop-3p-library.ts +72 -7
- package/src/claude/desktop-3p.ts +75 -19
- package/src/claude/desktop-first-party.ts +1 -1
- package/src/claude/desktop-picker-platform.ts +4 -0
- package/src/claude/desktop-picker-profile.ts +11 -4
- package/src/claude/desktop-picker.ts +15 -8
- package/src/claude/desktop-profile.ts +10 -1
- package/src/claude/desktop-remote-store-io.ts +5 -3
- package/src/claude/desktop-system-proxy.ts +50 -4
- package/src/claude/inbound-content-options.ts +11 -0
- package/src/claude/inbound.ts +154 -50
- package/src/claude/intercept/local-ca.ts +8 -1
- package/src/claude/intercept/picker-ca-startup.ts +2 -1
- package/src/claude/intercept/picker-runtime.ts +6 -5
- package/src/claude/intercept/picker-trust-windows.ts +338 -0
- package/src/claude/intercept/picker-trust.ts +19 -0
- package/src/claude/intercept/port-policy.ts +30 -0
- package/src/claude/intercept/runtime.ts +97 -94
- package/src/claude/model-default-settings.ts +154 -0
- package/src/claude/model-info.ts +7 -7
- package/src/claude/outbound.ts +51 -19
- package/src/cli/account-api.ts +1 -0
- package/src/cli/account-policy-dto.ts +6 -0
- package/src/cli/account-policy.ts +34 -5
- package/src/cli/account-preservation.ts +44 -0
- package/src/cli/account.ts +8 -2
- package/src/cli/advisor.ts +165 -0
- package/src/cli/auxiliary-listener-preflight.ts +37 -0
- package/src/cli/capabilities-accounts.ts +20 -1
- package/src/cli/capabilities-agents-routing.ts +23 -2
- package/src/cli/capabilities-base.ts +39 -11
- package/src/cli/capabilities-observe-system.ts +63 -3
- package/src/cli/capabilities-provider-models.ts +11 -0
- package/src/cli/chatgpt-command.ts +35 -14
- package/src/cli/claude.ts +46 -6
- package/src/cli/codex-cli-update.ts +151 -41
- package/src/cli/codex-provider.ts +44 -0
- package/src/cli/combo-input.ts +10 -1
- package/src/cli/combo-result.ts +1 -1
- package/src/cli/combo.ts +2 -1
- package/src/cli/cross-home-owner.ts +17 -14
- package/src/cli/dispatch.ts +30 -5
- package/src/cli/doctor.ts +15 -1
- package/src/cli/ensure-desktop-cold-start.ts +382 -0
- package/src/cli/export-command.ts +1 -1
- package/src/cli/help.ts +4 -2
- package/src/cli/index.ts +14 -5
- package/src/cli/integration-plan-dto.ts +3 -0
- package/src/cli/log-history.ts +172 -0
- package/src/cli/models-runtime-subcommands.ts +1 -0
- package/src/cli/models-runtime.ts +72 -8
- package/src/cli/observe-stream.ts +2 -0
- package/src/cli/observe.ts +16 -4
- package/src/cli/provider.ts +27 -5
- package/src/cli/registry.ts +41 -9
- package/src/cli/restart-scope.ts +3 -1
- package/src/cli/runtime-api.ts +5 -3
- package/src/cli/system-command.ts +40 -8
- package/src/cli/uninstall-client-state.ts +32 -0
- package/src/cli/usage-compare.ts +309 -0
- package/src/cli/usage-model-search.ts +4 -4
- package/src/cli/usage-report.ts +2 -2
- package/src/cli/v2.ts +1 -1
- package/src/client/machine-listener.ts +1 -3
- package/src/client/runtime.ts +1 -0
- package/src/clients/client-path.ts +51 -0
- package/src/clients/config-export/contracts.ts +3 -1
- package/src/clients/config-export/model-metadata.ts +3 -2
- package/src/clients/config-export/qoder.ts +93 -0
- package/src/clients/config-export.ts +35 -48
- package/src/clients/gentle-shell-path.ts +87 -0
- package/src/codex/account-auto-switch.ts +2 -0
- package/src/codex/account-identity.ts +29 -0
- package/src/codex/account-label.ts +1 -1
- package/src/codex/account-lifecycle.ts +21 -5
- package/src/codex/account-preservation.ts +28 -0
- package/src/codex/account-use-remaining.ts +23 -0
- package/src/codex/app-server-processes.ts +97 -7
- package/src/codex/app-server-restart-service.ts +55 -4
- package/src/codex/auth-api/account-list.ts +4 -0
- package/src/codex/auth-api/account-pause-group.ts +6 -50
- package/src/codex/auth-api/account-preservation.ts +57 -0
- package/src/codex/auth-api/pool-mode-gate.ts +5 -2
- package/src/codex/auth-api/routes.ts +8 -0
- package/src/codex/auth-api/use-remaining-route.ts +36 -0
- package/src/codex/auth-api.ts +0 -1
- package/src/codex/auth-context.ts +81 -7
- package/src/codex/autostart-health.ts +4 -0
- package/src/codex/catalog/auto-review.ts +20 -3
- package/src/codex/catalog/build-entries.ts +19 -2
- package/src/codex/catalog/combo-member.ts +14 -5
- package/src/codex/catalog/configured-tiers.ts +50 -0
- package/src/codex/catalog/effort.ts +4 -1
- package/src/codex/catalog/gather-capture.ts +2 -0
- package/src/codex/catalog/metadata.ts +11 -9
- package/src/codex/catalog/model-hints.ts +42 -19
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/native-ultrafast.ts +106 -0
- package/src/codex/catalog/parsing.ts +35 -86
- package/src/codex/catalog/provider-models.ts +40 -34
- package/src/codex/catalog/retained-sync.ts +2 -0
- package/src/codex/catalog/routed-gather.ts +33 -16
- package/src/codex/catalog/vendor-context.ts +82 -0
- package/src/codex/cli-install-provenance.ts +105 -17
- package/src/codex/cli-update-plan.ts +545 -0
- package/src/codex/config-write-lock.ts +63 -4
- package/src/codex/convergence.ts +2 -0
- package/src/codex/desktop-app/picker-freshness.ts +96 -0
- package/src/codex/desktop-app/types.ts +2 -0
- package/src/codex/desktop-app/windows.ts +29 -16
- package/src/codex/desktop-app-restart.ts +25 -2
- package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
- package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
- package/src/codex/desktop-compatibility/connection-store.ts +87 -0
- package/src/codex/desktop-compatibility/installed-build.ts +73 -0
- package/src/codex/desktop-compatibility/json-body.ts +29 -0
- package/src/codex/desktop-compatibility/native-identity.ts +62 -0
- package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
- package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
- package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
- package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
- package/src/codex/desktop-compatibility/runtime.ts +182 -0
- package/src/codex/desktop-compatibility/service.ts +30 -0
- package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
- package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
- package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
- package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
- package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
- package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
- package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
- package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
- package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
- package/src/codex/desktop-compatibility/windows-key-protection.ts +165 -0
- package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
- package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
- package/src/codex/features.ts +85 -45
- package/src/codex/history-provider.ts +1 -1
- package/src/codex/inject/config-write-section.ts +4 -1
- package/src/codex/inject/history-failure.ts +27 -0
- package/src/codex/inject/remove.ts +18 -6
- package/src/codex/inject/restore.ts +24 -2
- package/src/codex/inject/routing-classify.ts +4 -3
- package/src/codex/inject.ts +18 -27
- package/src/codex/journal.ts +16 -1
- package/src/codex/main-account-hard-lock.ts +30 -1
- package/src/codex/main-account.ts +99 -15
- package/src/codex/native-main-refresh-mode.ts +217 -0
- package/src/codex/native-profile-startup.ts +1 -1
- package/src/codex/ocx-compaction-history.ts +12 -10
- package/src/codex/paths.ts +86 -1
- package/src/codex/provider-routing.ts +174 -0
- package/src/codex/refresh.ts +2 -2
- package/src/codex/routing/health-store.ts +2 -2
- package/src/codex/routing/pool-recovery.ts +79 -0
- package/src/codex/routing/selection.ts +4 -3
- package/src/codex/routing/thread-affinity.ts +1 -1
- package/src/codex/shim-diagnostics.ts +6 -0
- package/src/codex/shim-overlay.ts +7 -2
- package/src/codex/signed-in-identity.ts +43 -0
- package/src/codex/subagent-model-fallback.ts +1 -1
- package/src/codex/sync.ts +6 -1
- package/src/codex/user-identity.ts +16 -5
- package/src/codex/windows-local-appdata.ts +84 -0
- package/src/combos/failover.ts +6 -1
- package/src/combos/index.ts +1 -0
- package/src/combos/resolve.ts +46 -10
- package/src/combos/types.ts +15 -0
- package/src/config/diagnostics.ts +35 -7
- package/src/config/live-reconcile.ts +51 -2
- package/src/config/load-degrade.ts +23 -1
- package/src/config/opencode-wire-upgrade.ts +54 -0
- package/src/config/opencode-zen-upgrade.ts +52 -0
- package/src/config/owner-registry.ts +22 -17
- package/src/config/paths.ts +10 -3
- package/src/config/provider-validation.ts +45 -2
- package/src/config/schema/config-schema.ts +39 -0
- package/src/config/schema/desktop-compatibility.ts +9 -0
- package/src/config/schema/leaf-validators.ts +39 -9
- package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
- package/src/config/schema/native-reasoning-retention.ts +26 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +962 -350
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/sync.ts +1 -1
- package/src/images/loop.ts +46 -8
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/config-io.ts +20 -2
- package/src/integrations/journal.ts +15 -1
- package/src/integrations/mutation-plan.ts +2 -0
- package/src/integrations/registry.ts +15 -0
- package/src/integrations/store.ts +3 -0
- package/src/integrations/writer.ts +10 -1
- package/src/lib/advisor-activation.ts +16 -0
- package/src/lib/background-work.ts +65 -0
- package/src/lib/bounded-file-read.ts +58 -0
- package/src/lib/bun-path-runtime.d.mts +10 -1
- package/src/lib/bun-path-runtime.mjs +62 -3
- package/src/lib/codex-restart-contract.ts +15 -0
- package/src/lib/desktop-proxy-route.ts +30 -0
- package/src/lib/desktop-upstream-tunnel.ts +72 -0
- package/src/lib/empty-tool-catalog.ts +62 -0
- package/src/lib/errors.ts +26 -3
- package/src/lib/gcp-adc.ts +96 -9
- package/src/lib/gcp-credential-input.ts +12 -0
- package/src/lib/lab-live-route-production.ts +5 -0
- package/src/lib/local-internal-call-capability.ts +55 -0
- package/src/lib/opaque-proxy-tunnel.ts +217 -0
- package/src/lib/package-tree-integrity.ts +2 -2
- package/src/lib/redact.ts +2 -2
- package/src/lib/socks5-fetch.ts +48 -71
- package/src/lib/socks5-handshake.ts +112 -0
- package/src/lib/translator-budget.ts +7 -1
- package/src/oauth/account-quota-rank.ts +69 -2
- package/src/oauth/anthropic-account-refusal.ts +20 -2
- package/src/oauth/anthropic-oauth-definitions.ts +3 -1
- package/src/oauth/anthropic-routing.ts +1 -21
- package/src/oauth/anthropic.ts +38 -7
- package/src/oauth/callback-server.ts +27 -1
- package/src/oauth/chatgpt-device.ts +1 -1
- package/src/oauth/chatgpt-shared.ts +83 -0
- package/src/oauth/chatgpt.ts +8 -72
- package/src/oauth/dsh.ts +203 -0
- package/src/oauth/generic-account-failover.ts +62 -12
- package/src/oauth/generic-session-affinity.ts +125 -0
- package/src/oauth/index.ts +188 -111
- package/src/oauth/kiro-device-login.ts +2 -1
- package/src/oauth/kiro.ts +7 -1
- package/src/oauth/local-token-detect.ts +21 -1
- package/src/oauth/login-cli-errors.ts +44 -0
- package/src/oauth/login-cli.ts +69 -7
- package/src/oauth/login-flow-state.ts +371 -23
- package/src/oauth/meta-muse-errors.ts +41 -0
- package/src/oauth/meta-muse-keychain.ts +52 -0
- package/src/oauth/meta-muse.ts +51 -84
- package/src/oauth/model-discovery-auth.ts +3 -0
- package/src/oauth/pool-settings-capability.ts +4 -2
- package/src/oauth/refresh-flight-retirement.ts +48 -0
- package/src/oauth/session-affinity-key.ts +23 -0
- package/src/oauth/store-anthropic-instance.ts +9 -2
- package/src/oauth/store.ts +12 -5
- package/src/protocols/codecs/chat-ir.ts +467 -0
- package/src/protocols/codecs/chat.ts +10 -1
- package/src/protocols/codecs/messages-ir.ts +500 -0
- package/src/protocols/encoders/adapter-events.ts +5 -4
- package/src/protocols/encoders/messages.ts +9 -6
- package/src/protocols/features.ts +10 -2
- package/src/protocols/path.ts +11 -1
- package/src/protocols/plan-snapshot.ts +39 -16
- package/src/protocols/plan.ts +9 -1
- package/src/protocols/provider-summary.ts +2 -1
- package/src/protocols/settings.ts +8 -0
- package/src/providers/antigravity-account-roster.ts +102 -0
- package/src/providers/antigravity-models.ts +85 -7
- package/src/providers/api-key-cooldowns.ts +49 -0
- package/src/providers/api-key-resolve.ts +11 -6
- package/src/providers/api-key-response-ownership.ts +57 -0
- package/src/providers/api-key-selection.ts +31 -6
- package/src/providers/api-keys.ts +214 -3
- package/src/providers/claude-cli-usage.ts +96 -0
- package/src/providers/dsh-account-destination.ts +31 -0
- package/src/providers/dsh-account-models.ts +41 -0
- package/src/providers/gcp-credential-cleanup.ts +59 -0
- package/src/providers/key-failover.ts +54 -50
- package/src/providers/key-store.ts +7 -2
- package/src/providers/model-rename-fields.ts +5 -0
- package/src/providers/model-rename-migration.ts +7 -5
- package/src/providers/openai-tiers-destination.ts +11 -2
- package/src/providers/openai-tiers.ts +5 -12
- package/src/providers/opencode-go-transport.ts +3 -1
- package/src/providers/quota/account-cache.ts +6 -5
- package/src/providers/quota/account-row.ts +34 -0
- package/src/providers/quota/antigravity.ts +28 -7
- package/src/providers/quota/dsh-account.ts +70 -0
- package/src/providers/quota/vendor-probes-oauth.ts +1 -93
- package/src/providers/quota-types.ts +2 -0
- package/src/providers/quota.ts +14 -20
- package/src/providers/registry/entries-core.ts +44 -14
- package/src/providers/registry/entries-extended.ts +19 -0
- package/src/providers/registry/types.ts +1 -1
- package/src/providers/request-pacing.ts +77 -19
- package/src/providers/routed-config.ts +339 -0
- package/src/providers/service-tier.ts +2 -1
- package/src/quota/window-mapping.ts +5 -2
- package/src/responses/anthropic-progress.ts +34 -0
- package/src/responses/assistant-replay.ts +35 -0
- package/src/responses/compaction-history-framing.ts +65 -0
- package/src/responses/compaction.ts +6 -1
- package/src/responses/incomplete-reason.ts +31 -0
- package/src/responses/parser.ts +78 -22
- package/src/responses/reasoning-envelope.ts +4 -0
- package/src/responses/reasoning-replay-cache.ts +24 -2
- package/src/responses/reasoning-retention.ts +173 -0
- package/src/responses/retained-compaction.ts +46 -0
- package/src/responses/schema.ts +23 -0
- package/src/responses/state.ts +32 -29
- package/src/router.ts +8 -321
- package/src/routing/health.ts +37 -22
- package/src/routing/history/build-file.ts +92 -0
- package/src/routing/history/indexer.ts +254 -221
- package/src/routing/history/ingest.ts +242 -0
- package/src/routing/history/rebuild-job.ts +111 -0
- package/src/routing/history/rebuild-protocol.ts +15 -0
- package/src/routing/history/rebuild-worker.ts +34 -0
- package/src/routing/profile.ts +1 -1
- package/src/server/admission-model-scope.ts +21 -1
- package/src/server/antigravity-direct-eligibility.ts +208 -0
- package/src/server/auth-cors.ts +15 -0
- package/src/server/chat-completions.ts +82 -10
- package/src/server/chat-native-reasoning.ts +123 -0
- package/src/server/chat-native-sse.ts +110 -7
- package/src/server/chat-native.ts +76 -17
- package/src/server/claude-messages.ts +164 -433
- package/src/server/effort-policy.ts +1 -1
- package/src/server/images.ts +15 -2
- package/src/server/index/desktop-compatibility-startup.ts +56 -0
- package/src/server/index/model-alias-id.ts +36 -0
- package/src/server/index/optional-listeners.ts +2 -0
- package/src/server/index/serve-options.ts +11 -3
- package/src/server/index/startup-warnings.ts +11 -0
- package/src/server/index/websocket-handler.ts +9 -0
- package/src/server/index.ts +14 -11
- package/src/server/inference/advisor-diagnostics.ts +31 -0
- package/src/server/inference/client-encoder-delivery.ts +6 -5
- package/src/server/login-shell-path.ts +146 -0
- package/src/server/management/advisor-routes.ts +202 -0
- package/src/server/management/agent-settings-routes.ts +22 -4
- package/src/server/management/claude-desktop-picker-routes.ts +1 -1
- package/src/server/management/combo-routes.ts +176 -50
- package/src/server/management/companion-routes.ts +4 -0
- package/src/server/management/config-routes.ts +77 -6
- package/src/server/management/context.ts +4 -0
- package/src/server/management/cursor-integration-routes.ts +1 -1
- package/src/server/management/desktop-compatibility-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
- package/src/server/management/model-routes.ts +13 -8
- package/src/server/management/model-rows.ts +14 -0
- package/src/server/management/native-integration-routes.ts +2 -7
- package/src/server/management/native-reasoning-retention-routes.ts +41 -0
- package/src/server/management/oauth-account-routes.ts +87 -14
- package/src/server/management/protocol-settings-patch.ts +1 -0
- package/src/server/management/provider-gcp-credential-routes.ts +29 -0
- package/src/server/management/provider-key-pacing-routes.ts +31 -0
- package/src/server/management/provider-overwrite-carry.ts +7 -0
- package/src/server/management/provider-routes.ts +25 -30
- package/src/server/management/route-registry.ts +21 -1
- package/src/server/management/sibling-guard.ts +1 -0
- package/src/server/management-api.ts +9 -1
- package/src/server/messages-antigravity-direct.ts +195 -0
- package/src/server/messages-native-tool-guard.ts +185 -0
- package/src/server/messages-native.ts +106 -22
- package/src/server/messages-shared.ts +431 -0
- package/src/server/relay.ts +1 -1
- package/src/server/request-log-terminal-status.ts +1 -1
- package/src/server/request-log.ts +13 -1
- package/src/server/responses/account-change-state.ts +8 -2
- package/src/server/responses/adapter-continuation.ts +48 -19
- package/src/server/responses/adapter-delivery.ts +43 -8
- package/src/server/responses/adapter-dispatch.ts +48 -4
- package/src/server/responses/advisor-plan-slot.ts +27 -0
- package/src/server/responses/advisor-retention.ts +117 -0
- package/src/server/responses/advisor-slot.ts +368 -0
- package/src/server/responses/code-mode-collab-surface.ts +116 -8
- package/src/server/responses/codex-ws-wire.ts +15 -8
- package/src/server/responses/compact.ts +303 -125
- package/src/server/responses/compaction-recovery.ts +18 -10
- package/src/server/responses/compaction-retention.ts +61 -0
- package/src/server/responses/context-overflow.ts +11 -6
- package/src/server/responses/core-auth.ts +6 -0
- package/src/server/responses/core-codex-account.ts +11 -1
- package/src/server/responses/core-combo-failure.ts +12 -1
- package/src/server/responses/core-combo.ts +141 -36
- package/src/server/responses/core-errors.ts +3 -3
- package/src/server/responses/core-lifetime.ts +7 -2
- package/src/server/responses/core-normalize.ts +11 -0
- package/src/server/responses/core-opaque-recovery.ts +35 -0
- package/src/server/responses/core-options.ts +51 -0
- package/src/server/responses/core-pipeline.ts +141 -0
- package/src/server/responses/core-replay.ts +25 -6
- package/src/server/responses/core.ts +3 -116
- package/src/server/responses/fetch-helpers.ts +73 -19
- package/src/server/responses/main-hard-lock-routing.ts +109 -0
- package/src/server/responses/native-response-control.ts +4 -2
- package/src/server/responses/passthrough-delivery.ts +19 -11
- package/src/server/responses/passthrough-dispatch.ts +29 -16
- package/src/server/responses/request-prepare.ts +144 -17
- package/src/server/responses/request-sidecar-auth.ts +2 -2
- package/src/server/responses/request-transport.ts +120 -27
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +166 -38
- package/src/server/responses/sidecar-execution.ts +98 -5
- package/src/server/responses/terminal-guard.ts +4 -17
- package/src/server/responses-undeclared-tool-guard.ts +30 -9
- package/src/server/startup-action-control.ts +16 -2
- package/src/server/startup-health-cache.ts +1 -0
- package/src/service/auth-preflight.ts +71 -0
- package/src/service/cli.ts +4 -2
- package/src/service/definition.ts +97 -0
- package/src/service/desktop-startup.ts +14 -0
- package/src/service/guards.ts +1 -61
- package/src/service/health-sentinel-linux.ts +112 -0
- package/src/service/health-sentinel-main.ts +99 -61
- package/src/service/health-sentinel-probe.ts +24 -7
- package/src/service/health.ts +5 -89
- package/src/service/launchd.ts +27 -7
- package/src/service/logging.ts +135 -0
- package/src/service/repair.ts +2 -1
- package/src/service/state.ts +7 -8
- package/src/service/systemd.ts +1 -1
- package/src/service/windows-guarded-manager.ts +2 -1
- package/src/service/windows-ops.ts +1 -1
- package/src/service/windows-scheduler.ts +2 -7
- package/src/service/windows-taskxml.ts +2 -2
- package/src/service/windows-wscript.ts +14 -0
- package/src/service.ts +4 -2
- package/src/sidecar/auth.ts +2 -0
- package/src/tray/windows-tray.ps1 +43 -24
- package/src/types/config.ts +124 -6
- package/src/types/opencode-go.ts +61 -0
- package/src/types/opencode-zen.ts +29 -0
- package/src/types/provider.ts +39 -6
- package/src/types/request.ts +38 -2
- package/src/types/tools.ts +2 -0
- package/src/types/wire.ts +29 -37
- package/src/types.ts +1 -0
- package/src/update/async-check.ts +1 -1
- package/src/update/badge.ts +2 -2
- package/src/update/index.ts +88 -15
- package/src/update/job-supervised-restart.ts +107 -0
- package/src/update/job.ts +32 -32
- package/src/update/notify.ts +1 -1
- package/src/update/npm-invocation.mjs +12 -2
- package/src/update/refresh-scheduler.ts +2 -2
- package/src/update/supervised-recovery.d.mts +27 -0
- package/src/update/supervised-recovery.mjs +157 -0
- package/src/usage/cache-diagnostic.ts +2 -0
- package/src/usage/cost.ts +48 -12
- package/src/usage/expected-prices.ts +15 -0
- package/src/usage/ledger-retention.ts +2 -1
- package/src/usage/ledger-scanner.ts +6 -5
- package/src/usage/log.ts +17 -8
- package/src/usage/summary.ts +22 -9
- package/src/usage/user-cost-overlays.ts +14 -19
- package/src/usage/user-cost-validation.ts +38 -0
- package/src/vision/eligibility.ts +13 -0
- package/src/web-search/gemini-executor.ts +2 -2
- package/src/web-search/loop.ts +55 -5
- package/src/web-search/progress-stream.ts +4 -1
- package/src/web-search/run-turn-loop.ts +9 -1
- package/assets/sponsors/packycode.png +0 -0
- package/gui/dist/assets/App-CSJvmjNs.css +0 -1
- package/gui/dist/assets/App-CZtqiGcw.js +0 -56
- package/gui/dist/assets/index-BB6uK6tU.css +0 -1
- package/gui/dist/assets/index-CTm4l-HQ.js +0 -94
- package/gui/dist/assets/quota-summary-BbEeRfPY.js +0 -1
package/src/cli/runtime-api.ts
CHANGED
|
@@ -279,13 +279,15 @@ export function takeBooleanOption(args: string[], flag: string): boolean | undef
|
|
|
279
279
|
}
|
|
280
280
|
|
|
281
281
|
/** Parse a decimal safe integer, allowing separators only between digits. */
|
|
282
|
-
export function takeIntegerOption(args: string[], flag: string, options: { min?: number } = {}): number | undefined {
|
|
282
|
+
export function takeIntegerOption(args: string[], flag: string, options: { min?: number; max?: number } = {}): number | undefined {
|
|
283
283
|
const raw = takeOption(args, flag);
|
|
284
284
|
if (raw === undefined) return undefined;
|
|
285
285
|
const decimal = raw.trim();
|
|
286
286
|
const value = /^[+-]?\d+(?:[_,]\d+)*$/.test(decimal) ? Number(decimal.replace(/[_,]/g, "")) : NaN;
|
|
287
|
-
|
|
288
|
-
|
|
287
|
+
const { min, max } = options;
|
|
288
|
+
if (!Number.isSafeInteger(value) || value < (min ?? Number.MIN_SAFE_INTEGER) || value > (max ?? Number.MAX_SAFE_INTEGER)) {
|
|
289
|
+
const bound = min !== undefined && max !== undefined ? ` between ${min} and ${max}` : min !== undefined ? ` >= ${min}` : max !== undefined ? ` <= ${max}` : "";
|
|
290
|
+
throw new CliUsageError(`${flag} must be an integer${bound}`);
|
|
289
291
|
}
|
|
290
292
|
return value;
|
|
291
293
|
}
|
|
@@ -28,6 +28,7 @@ const USAGE = `Usage:
|
|
|
28
28
|
ocx system codex-cli-update check [--json]
|
|
29
29
|
ocx system codex-cli-update attest [--json]
|
|
30
30
|
ocx system codex-cli-update attest --candidate <absolute-path> --npm-prefix <absolute-path> --npm-cli <absolute-path> --node <absolute-path> [--json]
|
|
31
|
+
ocx system codex-cli-update plan [--channel latest] [--json]
|
|
31
32
|
ocx system update check [--channel <latest|preview>] [--json]
|
|
32
33
|
ocx system update run [--channel <latest|preview>] [--restart <on|off>] --yes [--json]
|
|
33
34
|
ocx system update status <job-id> [--json]
|
|
@@ -270,24 +271,55 @@ export async function handleSystemCommand(argv: string[], deps: RuntimeApiDeps =
|
|
|
270
271
|
const args = [...rest]; const wantsJson = takeFlag(args, "--json"); rejectArgs(args, USAGE);
|
|
271
272
|
printData(await runtimeRequest("/api/system/codex-app-server", {}, deps), wantsJson);
|
|
272
273
|
} else if (sub === "codex-restart") {
|
|
273
|
-
// --yes required: this
|
|
274
|
-
//
|
|
274
|
+
// --yes required: this interrupts running Codex turns, and when the desktop picker predates
|
|
275
|
+
// the catalog it also quits and relaunches the app, which can discard unsaved client state.
|
|
275
276
|
const args = [...rest];
|
|
276
277
|
const wantsJson = takeFlag(args, "--json");
|
|
277
278
|
const yes = takeFlag(args, "--yes");
|
|
278
279
|
if (!yes) throw new CliUsageError(
|
|
279
|
-
"system codex-restart requires --yes: this fully quits and relaunches the Codex desktop app, so unsaved composer drafts, model-picker selections, and pending approval prompts may be lost
|
|
280
|
+
"system codex-restart requires --yes: this restarts the Codex app-servers, interrupting running turns, and when the desktop app's model picker predates the current catalog (or that cannot be determined) it fully quits and relaunches the Codex desktop app, so unsaved composer drafts, model-picker selections, and pending approval prompts may be lost",
|
|
280
281
|
USAGE,
|
|
281
282
|
);
|
|
282
283
|
rejectArgs(args, USAGE);
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
wantsJson,
|
|
286
|
-
["Codex desktop app and app-server restart requested. Unsaved composer drafts, model-picker selections, and pending approval prompts may be lost."],
|
|
287
|
-
);
|
|
284
|
+
const result = await runtimeRequest("/api/system/codex-restart", { method: "POST" }, deps);
|
|
285
|
+
printData(result, wantsJson, codexRestartLines(result));
|
|
288
286
|
} else if (sub === "update") await update(rest, deps);
|
|
289
287
|
else throw new CliUsageError(`unknown system command ${sub}`, USAGE);
|
|
290
288
|
});
|
|
291
289
|
}
|
|
292
290
|
|
|
291
|
+
/**
|
|
292
|
+
* Operator text for a codex-restart outcome. It reports what happened to the desktop app,
|
|
293
|
+
* because whether unsaved client state survived is the one thing the operator needs to know.
|
|
294
|
+
*/
|
|
295
|
+
export function codexRestartLines(result: unknown): string[] {
|
|
296
|
+
const view = recordValue(result);
|
|
297
|
+
const desktop = recordValue(view?.desktopApp);
|
|
298
|
+
const reason = typeof desktop?.reason === "string" ? desktop.reason : undefined;
|
|
299
|
+
// Only a reported `stale` reading may claim the picker predated the catalog; an unreadable
|
|
300
|
+
// reading also takes the full restart, and an older proxy reports no reading at all.
|
|
301
|
+
const why = desktop?.pickerState === "stale"
|
|
302
|
+
? "its model picker predated the current catalog"
|
|
303
|
+
: "its model picker could not be confirmed current";
|
|
304
|
+
const stopped = Array.isArray(view?.stopped) ? view.stopped.length : 0;
|
|
305
|
+
const appServers = view?.code === "stopped"
|
|
306
|
+
? `Stopped ${stopped} Codex app-server(s); their owners start replacements.`
|
|
307
|
+
: view?.code === "partially_stopped"
|
|
308
|
+
? "Some Codex app-servers did not exit; stop them manually if Codex stays stale."
|
|
309
|
+
: view?.code === "enumeration_unavailable"
|
|
310
|
+
? "Could not list Codex app-servers, so none were stopped."
|
|
311
|
+
: "No Codex app-server needed stopping.";
|
|
312
|
+
if (reason === "picker_current") {
|
|
313
|
+
return [appServers, "The Codex desktop app was left running: its model picker already shows the current catalog, so unsaved drafts and pending approvals were kept."];
|
|
314
|
+
}
|
|
315
|
+
if (reason === "no_targets") return [appServers, "The Codex desktop app is not running."];
|
|
316
|
+
if (desktop?.relaunch === "started") {
|
|
317
|
+
return [appServers, `The Codex desktop app was quit and relaunched because ${why}. Unsaved composer drafts, model-picker selections, and pending approval prompts may have been lost.`];
|
|
318
|
+
}
|
|
319
|
+
if (desktop?.attempted === true) {
|
|
320
|
+
return [appServers, `The Codex desktop app restart did not complete (${reason ?? "no outcome reported"}). Unsaved composer drafts, model-picker selections, and pending approval prompts may have been lost.`];
|
|
321
|
+
}
|
|
322
|
+
return [appServers, `The Codex desktop app was not restarted (${reason ?? "no outcome reported"}).`];
|
|
323
|
+
}
|
|
324
|
+
|
|
293
325
|
export const SYSTEM_USAGE = USAGE;
|
|
@@ -5,9 +5,13 @@ import { assertClientLifecycleHeld, withClientLifecycle } from "../client/lifecy
|
|
|
5
5
|
import { inspectRemoteDesktopCleanup, readDesktopDisconnectReceipt } from "../claude/desktop-remote-store";
|
|
6
6
|
import { removeOwnedConfigState, type ConfigRemovalResult } from "../lib/config-ownership";
|
|
7
7
|
import { windowsSecretAclReapPendingAtOrBelow } from "../lib/windows-secret-acl";
|
|
8
|
+
import { supportsDesktopPicker } from "../claude/desktop-picker-platform";
|
|
8
9
|
import { sharedTeardownAuthorized, type UninstallObservation } from "./uninstall-plan";
|
|
9
10
|
import { cleanupOwnedIntegrationsBeforeUninstall } from "./uninstall-integrations";
|
|
10
11
|
|
|
12
|
+
/** Outcome of retiring the Desktop picker profile and every picker root this install trusted. */
|
|
13
|
+
export interface PickerUninstallCleanup { ok: boolean; residual?: string[] }
|
|
14
|
+
|
|
11
15
|
export interface UninstallClientStateDeps {
|
|
12
16
|
readConnection: typeof readClientConnectionState;
|
|
13
17
|
inspectDesktop: typeof inspectRemoteDesktopCleanup;
|
|
@@ -18,6 +22,25 @@ export interface UninstallClientStateDeps {
|
|
|
18
22
|
remove: () => ConfigRemovalResult;
|
|
19
23
|
/** True while a timed-out icacls child still owns a path at or below the config directory. */
|
|
20
24
|
aclReapPending: (rootPath: string) => boolean;
|
|
25
|
+
/** Release the picker profile, then untrust the current and any pending predecessor picker root. */
|
|
26
|
+
removePickerArtifacts: () => Promise<PickerUninstallCleanup>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The picker root lives in the OS trust store (login keychain, CurrentUser Root on Windows), outside
|
|
31
|
+
* the config directory. Deleting config first would erase ca.pem and the pending-untrust journal,
|
|
32
|
+
* the only records naming which root to remove, and strand a trusted root after uninstall.
|
|
33
|
+
*/
|
|
34
|
+
async function removePickerArtifactsBeforeUninstall(): Promise<PickerUninstallCleanup> {
|
|
35
|
+
if (!supportsDesktopPicker()) return { ok: true };
|
|
36
|
+
const configDir = getConfigDir();
|
|
37
|
+
const [{ removeDesktopPickerArtifacts }, { drainPendingPickerCaUntrust }] = await Promise.all([
|
|
38
|
+
import("../claude/desktop-picker"),
|
|
39
|
+
import("../claude/intercept/picker-ca-cleanup"),
|
|
40
|
+
]);
|
|
41
|
+
const removed = await removeDesktopPickerArtifacts({ configDir });
|
|
42
|
+
if (!removed.ok) return removed;
|
|
43
|
+
return await drainPendingPickerCaUntrust(configDir) ? { ok: true } : { ok: false, residual: ["pending_trust"] };
|
|
21
44
|
}
|
|
22
45
|
|
|
23
46
|
const defaults: UninstallClientStateDeps = {
|
|
@@ -29,6 +52,7 @@ const defaults: UninstallClientStateDeps = {
|
|
|
29
52
|
cleanupIntegrations: cleanupOwnedIntegrationsBeforeUninstall,
|
|
30
53
|
remove: () => removeOwnedConfigState(getConfigDir()),
|
|
31
54
|
aclReapPending: rootPath => windowsSecretAclReapPendingAtOrBelow(rootPath),
|
|
55
|
+
removePickerArtifacts: removePickerArtifactsBeforeUninstall,
|
|
32
56
|
};
|
|
33
57
|
|
|
34
58
|
/** Restore connection-owned client artifacts before removing their ownership/recovery records. */
|
|
@@ -67,6 +91,14 @@ export async function removeOwnedConfigAfterDesktopCleanup(
|
|
|
67
91
|
throw new Error("Client cleanup refused: finish Desktop recovery before uninstalling.");
|
|
68
92
|
}
|
|
69
93
|
|
|
94
|
+
// Picker cleanup takes L itself, so it runs before the final lock. A residual keeps every
|
|
95
|
+
// record the retry needs: the config directory still names the root to untrust.
|
|
96
|
+
const picker = await deps.removePickerArtifacts();
|
|
97
|
+
if (!picker.ok) {
|
|
98
|
+
throw new Error(`Client cleanup refused: Claude Desktop picker cleanup left ${picker.residual?.join(", ") || "state"} behind; `
|
|
99
|
+
+ "approve the certificate removal prompt or run 'ocx claude desktop picker off', then rerun 'ocx uninstall'.");
|
|
100
|
+
}
|
|
101
|
+
|
|
70
102
|
// A connection can appear while asynchronous cleanup is finishing. The final
|
|
71
103
|
// inspection and actual remover share L, but never hold C while deleting its DB.
|
|
72
104
|
return deps.withLifecycle(async held => {
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compare a bounded usage range with the previous equal window.
|
|
3
|
+
*
|
|
4
|
+
* Two sequential GET /api/usage reads with acknowledged custom bounds.
|
|
5
|
+
* `--range all` is refused: there is no previous equal window for open history.
|
|
6
|
+
*/
|
|
7
|
+
import { readClientConnectionState, sameClientConnectionOwner } from "../client/state";
|
|
8
|
+
import { fetchHubUsage } from "../client/hub-client";
|
|
9
|
+
import { readServiceApiTokenState, serviceApiTokenFingerprint } from "../lib/service-secrets";
|
|
10
|
+
import { runningProxyUpdateHeaders } from "../oauth/login-cli";
|
|
11
|
+
import { redactSecretString } from "../lib/redact";
|
|
12
|
+
import { rangeWindow, USAGE_SURFACES } from "../usage/summary";
|
|
13
|
+
import type { UsageTimeWindow } from "../usage/time-range";
|
|
14
|
+
import {
|
|
15
|
+
CliUsageError,
|
|
16
|
+
printData,
|
|
17
|
+
rejectArgs,
|
|
18
|
+
RuntimeApiError,
|
|
19
|
+
runtimeRequest,
|
|
20
|
+
runtimeBaseUrl,
|
|
21
|
+
takeFlag,
|
|
22
|
+
takeOption,
|
|
23
|
+
takeOptionWithSyntax,
|
|
24
|
+
type RuntimeApiDeps,
|
|
25
|
+
} from "./runtime-api";
|
|
26
|
+
|
|
27
|
+
export const USAGE_COMPARE_USAGE = `Usage:
|
|
28
|
+
ocx usage compare [--range <today|1d|7d|30d>] [--surface <all|codex|claude|grok>]
|
|
29
|
+
[--provider <name>] [--model <id>] [--api-key-id <id>] [--json]`;
|
|
30
|
+
|
|
31
|
+
export type CompareRange = "today" | "7d" | "30d";
|
|
32
|
+
export type UsageCompareDeps = RuntimeApiDeps & { now?: () => number };
|
|
33
|
+
|
|
34
|
+
const METRICS = ["requests", "totalTokens", "inputTokens", "outputTokens", "cachedInputTokens", "estimatedCostUsd"] as const;
|
|
35
|
+
type Metric = typeof METRICS[number];
|
|
36
|
+
type Totals = Partial<Record<Metric, number>>;
|
|
37
|
+
type ScopedUsage = {
|
|
38
|
+
customWindow?: boolean;
|
|
39
|
+
since?: number | null;
|
|
40
|
+
until?: number;
|
|
41
|
+
summary?: Totals;
|
|
42
|
+
usageIncomplete?: boolean;
|
|
43
|
+
filter?: Record<string, unknown>;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function query(params: Record<string, string | number | undefined>): string {
|
|
47
|
+
const search = new URLSearchParams();
|
|
48
|
+
for (const [key, value] of Object.entries(params)) {
|
|
49
|
+
if (value !== undefined) search.set(key, String(value));
|
|
50
|
+
}
|
|
51
|
+
const encoded = search.toString();
|
|
52
|
+
return encoded ? `?${encoded}` : "";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseCompareRange(raw: string): CompareRange {
|
|
56
|
+
if (raw === "all") throw new CliUsageError("--range all has no previous equal window; use today, 1d, 7d or 30d", USAGE_COMPARE_USAGE);
|
|
57
|
+
if (raw === "today" || raw === "1d") return "today";
|
|
58
|
+
if (raw === "7d" || raw === "30d") return raw;
|
|
59
|
+
throw new CliUsageError("--range must be one of today, 1d, 7d, 30d", USAGE_COMPARE_USAGE);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Named range at `now`, then the same range evaluated one equal window earlier. */
|
|
63
|
+
export function compareUsageWindows(range: CompareRange, now: number): {
|
|
64
|
+
current: UsageTimeWindow;
|
|
65
|
+
previous: UsageTimeWindow;
|
|
66
|
+
days: number;
|
|
67
|
+
} {
|
|
68
|
+
const days = range === "today" ? 1 : range === "7d" ? 7 : 30;
|
|
69
|
+
const { since } = rangeWindow(range, now);
|
|
70
|
+
if (since === null) throw new Error("compare requires a bounded usage range");
|
|
71
|
+
const prior = new Date(now);
|
|
72
|
+
prior.setDate(prior.getDate() - days);
|
|
73
|
+
const priorNow = prior.getTime();
|
|
74
|
+
const { since: previousSince } = rangeWindow(range, priorNow);
|
|
75
|
+
if (previousSince === null) throw new Error("compare requires a bounded usage range");
|
|
76
|
+
return {
|
|
77
|
+
current: Object.freeze({ since, until: now }),
|
|
78
|
+
previous: Object.freeze({ since: previousSince, until: priorNow }),
|
|
79
|
+
days,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function metric(value: unknown): number | undefined {
|
|
84
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function totalsOf(report: ScopedUsage): Totals {
|
|
88
|
+
const row = report.summary ?? {};
|
|
89
|
+
const totals: Totals = {};
|
|
90
|
+
for (const key of METRICS) {
|
|
91
|
+
const value = metric((row as Totals)[key]);
|
|
92
|
+
if (value !== undefined) totals[key] = value;
|
|
93
|
+
}
|
|
94
|
+
return totals;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function delta(current?: number, previous?: number): number | undefined {
|
|
98
|
+
if (current === undefined || previous === undefined) return undefined;
|
|
99
|
+
return current - previous;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function changePct(current?: number, previous?: number): number | null | undefined {
|
|
103
|
+
if (current === undefined || previous === undefined) return undefined;
|
|
104
|
+
if (previous === 0) return current === 0 ? 0 : null;
|
|
105
|
+
return (current - previous) / previous * 100;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function count(value: number | undefined): string {
|
|
109
|
+
return value === undefined ? "—" : value.toLocaleString("en-US");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function usd(value: number | undefined): string {
|
|
113
|
+
return value === undefined ? "—" : `~$${value.toFixed(4)}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function signed(value: number | undefined, money: boolean): string {
|
|
117
|
+
if (value === undefined) return "—";
|
|
118
|
+
const abs = money ? usd(Math.abs(value)) : count(Math.abs(value));
|
|
119
|
+
if (value === 0) return money ? usd(0) : "0";
|
|
120
|
+
return `${value > 0 ? "+" : "-"}${abs}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function pct(value: number | null | undefined): string {
|
|
124
|
+
if (value === undefined) return "—";
|
|
125
|
+
if (value === null) return "n/a";
|
|
126
|
+
const abs = Math.abs(value).toFixed(1);
|
|
127
|
+
if (value === 0) return "0.0%";
|
|
128
|
+
return `${value > 0 ? "+" : "-"}${abs}%`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function iso(ms: number): string {
|
|
132
|
+
return new Date(ms).toISOString();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function formatCompare(input: {
|
|
136
|
+
range: CompareRange;
|
|
137
|
+
days: number;
|
|
138
|
+
currentWindow: UsageTimeWindow;
|
|
139
|
+
previousWindow: UsageTimeWindow;
|
|
140
|
+
current: Totals;
|
|
141
|
+
previous: Totals;
|
|
142
|
+
incomplete: boolean;
|
|
143
|
+
}): string[] {
|
|
144
|
+
const lines = [
|
|
145
|
+
`Usage compare — ${input.range} vs previous ${input.days} local day${input.days === 1 ? "" : "s"} (equal window)`,
|
|
146
|
+
`Current ${iso(input.currentWindow.since)} to ${iso(input.currentWindow.until)}`,
|
|
147
|
+
`Previous ${iso(input.previousWindow.since)} to ${iso(input.previousWindow.until)}`,
|
|
148
|
+
"",
|
|
149
|
+
];
|
|
150
|
+
if (input.incomplete) {
|
|
151
|
+
lines.push("WARNING: Usage is incomplete; some records could not be included. Totals reflect readable records only.", "");
|
|
152
|
+
}
|
|
153
|
+
const labels: Record<Metric, string> = {
|
|
154
|
+
requests: "Requests",
|
|
155
|
+
totalTokens: "Tokens",
|
|
156
|
+
inputTokens: "Input",
|
|
157
|
+
outputTokens: "Output",
|
|
158
|
+
cachedInputTokens: "Cached",
|
|
159
|
+
estimatedCostUsd: "Est. cost",
|
|
160
|
+
};
|
|
161
|
+
const rows = METRICS.map(key => {
|
|
162
|
+
const money = key === "estimatedCostUsd";
|
|
163
|
+
const show = money ? usd : count;
|
|
164
|
+
return [
|
|
165
|
+
labels[key],
|
|
166
|
+
show(input.current[key]),
|
|
167
|
+
show(input.previous[key]),
|
|
168
|
+
signed(delta(input.current[key], input.previous[key]), money),
|
|
169
|
+
pct(changePct(input.current[key], input.previous[key])),
|
|
170
|
+
];
|
|
171
|
+
});
|
|
172
|
+
const header = ["METRIC", "CURRENT", "PREVIOUS", "DELTA", "CHANGE"];
|
|
173
|
+
const widths = header.map((h, i) => Math.max(h.length, ...rows.map(row => row[i]!.length)));
|
|
174
|
+
const line = (cols: string[]) => cols.map((c, i) => c.padEnd(widths[i]!)).join(" ").trimEnd();
|
|
175
|
+
lines.push(line(header), ...rows.map(line), "", "Not a billing receipt. Subscription usage or provider credits may apply instead.");
|
|
176
|
+
return lines;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Private command-local authority; neither credentials nor fingerprints enter output.
|
|
180
|
+
function captureUsageOwner() {
|
|
181
|
+
const connection = readClientConnectionState();
|
|
182
|
+
if (connection.kind === "invalid" || connection.kind === "mismatched") {
|
|
183
|
+
throw new Error(`Client usage unavailable: ${connection.reason}`);
|
|
184
|
+
}
|
|
185
|
+
const token = readServiceApiTokenState();
|
|
186
|
+
if (connection.kind === "connected" && (token.kind !== "present" || token.fingerprint !== connection.value.tokenFingerprint)) {
|
|
187
|
+
throw new Error("Client usage unavailable: the enrolled data key is missing or changed; repair the client connection");
|
|
188
|
+
}
|
|
189
|
+
const headers = runningProxyUpdateHeaders();
|
|
190
|
+
const fingerprint = connection.kind === "connected" ? connection.value.tokenFingerprint
|
|
191
|
+
: serviceApiTokenFingerprint(headers.get("X-OpenCodex-API-Key") ?? "");
|
|
192
|
+
return { connection, token, headers, fingerprint };
|
|
193
|
+
}
|
|
194
|
+
type UsageOwner = ReturnType<typeof captureUsageOwner>;
|
|
195
|
+
|
|
196
|
+
function assertUsageOwner(owner: UsageOwner): void {
|
|
197
|
+
const current = readClientConnectionState();
|
|
198
|
+
const expected = owner.connection;
|
|
199
|
+
let same = current.kind === expected.kind;
|
|
200
|
+
if (expected.kind === "connected") {
|
|
201
|
+
const token = readServiceApiTokenState();
|
|
202
|
+
same = current.kind === "connected" && sameClientConnectionOwner(current.value, expected.value)
|
|
203
|
+
&& current.value.transport === expected.value.transport
|
|
204
|
+
&& JSON.stringify(current.value.link) === JSON.stringify(expected.value.link)
|
|
205
|
+
&& current.value.tokenFingerprint === owner.fingerprint
|
|
206
|
+
&& token.kind === "present" && token.fingerprint === owner.fingerprint;
|
|
207
|
+
} else {
|
|
208
|
+
same &&= serviceApiTokenFingerprint(runningProxyUpdateHeaders().get("X-OpenCodex-API-Key") ?? "") === owner.fingerprint;
|
|
209
|
+
}
|
|
210
|
+
if (!same) throw new Error("Usage owner changed during comparison; no comparison was produced. Retry for the current connection and credential.");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function readUsage(
|
|
214
|
+
suffix: string,
|
|
215
|
+
window: UsageTimeWindow,
|
|
216
|
+
apiKeyId: string | undefined,
|
|
217
|
+
deps: RuntimeApiDeps,
|
|
218
|
+
owner: UsageOwner,
|
|
219
|
+
): Promise<ScopedUsage> {
|
|
220
|
+
assertUsageOwner(owner);
|
|
221
|
+
const { connection, token } = owner;
|
|
222
|
+
let result: ScopedUsage;
|
|
223
|
+
if (connection.kind === "connected") {
|
|
224
|
+
if (apiKeyId !== undefined) {
|
|
225
|
+
throw new CliUsageError("--api-key-id is unavailable on connected clients; run on the hub for a selected key, or omit it for this client's usage", USAGE_COMPARE_USAGE);
|
|
226
|
+
}
|
|
227
|
+
if (token.kind !== "present" || token.fingerprint !== connection.value.tokenFingerprint) {
|
|
228
|
+
throw new Error("Client usage unavailable: the enrolled data key is missing or changed; repair the client connection");
|
|
229
|
+
}
|
|
230
|
+
const link = connection.value.transport === "link" && connection.value.link;
|
|
231
|
+
const peerFetch = deps.fetchImpl
|
|
232
|
+
?? (link ? (await import("../client/link-home")).linkBoundFetch(connection.value, token.token) : undefined);
|
|
233
|
+
result = await fetchHubUsage(connection.value.serverUrl, token.token, new URLSearchParams(suffix.replace(/^\?/, "")), {
|
|
234
|
+
fetchImpl: peerFetch, timeoutMs: 60_000, ...(link ? { peer: "Home" as const } : {}),
|
|
235
|
+
});
|
|
236
|
+
} else if (apiKeyId === undefined) {
|
|
237
|
+
result = await runtimeRequest(`/api/usage${suffix}`, { headers: owner.headers }, deps);
|
|
238
|
+
} else {
|
|
239
|
+
try {
|
|
240
|
+
result = await runtimeRequest(`/api/usage${suffix}`, { redirect: "error", credentials: "omit", headers: owner.headers }, deps);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
if (error instanceof RuntimeApiError && error.code === "proxy_not_running") throw error;
|
|
243
|
+
throw new Error("Key-scoped usage could not be read. Check runtime access and retry.");
|
|
244
|
+
}
|
|
245
|
+
if (result.filter?.apiKeyId !== apiKeyId) {
|
|
246
|
+
throw new Error("The server did not confirm the requested API key scope. Upgrade and restart the proxy, then retry.");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (result?.customWindow !== true || result.since !== window.since || result.until !== window.until) {
|
|
250
|
+
throw new Error("The server did not confirm the requested custom usage window. Upgrade and restart the proxy, then retry.");
|
|
251
|
+
}
|
|
252
|
+
assertUsageOwner(owner);
|
|
253
|
+
return result;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Compare the current named range with the previous equal window through two /api/usage reads. */
|
|
257
|
+
export async function handleUsageCompareCommand(argv: string[], deps: UsageCompareDeps = {}): Promise<void> {
|
|
258
|
+
const args = [...argv];
|
|
259
|
+
const wantsJson = takeFlag(args, "--json");
|
|
260
|
+
const range = parseCompareRange(takeOption(args, "--range") ?? "30d");
|
|
261
|
+
const surface = takeOption(args, "--surface") ?? "all";
|
|
262
|
+
const provider = takeOption(args, "--provider");
|
|
263
|
+
const model = takeOption(args, "--model");
|
|
264
|
+
const rawKeyId = takeOptionWithSyntax(args, "--api-key-id")?.value;
|
|
265
|
+
const apiKeyId = rawKeyId?.trim();
|
|
266
|
+
if (apiKeyId !== undefined && !apiKeyId) throw new CliUsageError("--api-key-id must not be blank", USAGE_COMPARE_USAGE);
|
|
267
|
+
if (takeOption(args, "--since") !== undefined || takeOption(args, "--until") !== undefined) {
|
|
268
|
+
throw new CliUsageError("compare derives both windows; do not pass --since or --until", USAGE_COMPARE_USAGE);
|
|
269
|
+
}
|
|
270
|
+
if (!USAGE_SURFACES.includes(surface as (typeof USAGE_SURFACES)[number])) {
|
|
271
|
+
throw new CliUsageError(`--surface must be one of ${USAGE_SURFACES.join(", ")}`, USAGE_COMPARE_USAGE);
|
|
272
|
+
}
|
|
273
|
+
rejectArgs(args.map(redactSecretString), USAGE_COMPARE_USAGE);
|
|
274
|
+
const now = deps.now?.() ?? Date.now();
|
|
275
|
+
if (!Number.isSafeInteger(now) || now < 0) throw new CliUsageError("compare clock is invalid", USAGE_COMPARE_USAGE);
|
|
276
|
+
const windows = compareUsageWindows(range, now);
|
|
277
|
+
const filters = { range, surface, provider, model, apiKeyId };
|
|
278
|
+
const owner = captureUsageOwner();
|
|
279
|
+
const readDeps = owner.connection.kind === "disconnected" ? { ...deps, baseUrl: await runtimeBaseUrl(deps) } : deps;
|
|
280
|
+
const currentReport = await readUsage(query({ ...filters, since: windows.current.since, until: windows.current.until }), windows.current, apiKeyId, readDeps, owner);
|
|
281
|
+
const previousReport = await readUsage(query({ ...filters, since: windows.previous.since, until: windows.previous.until }), windows.previous, apiKeyId, readDeps, owner);
|
|
282
|
+
const current = totalsOf(currentReport);
|
|
283
|
+
const previous = totalsOf(previousReport);
|
|
284
|
+
const deltaTotals: Totals = {};
|
|
285
|
+
const change: Partial<Record<Metric, number | null>> = {};
|
|
286
|
+
for (const key of METRICS) {
|
|
287
|
+
const d = delta(current[key], previous[key]);
|
|
288
|
+
if (d !== undefined) deltaTotals[key] = d;
|
|
289
|
+
const pctValue = changePct(current[key], previous[key]);
|
|
290
|
+
if (pctValue !== undefined) change[key] = pctValue;
|
|
291
|
+
}
|
|
292
|
+
const incomplete = currentReport.usageIncomplete === true || previousReport.usageIncomplete === true;
|
|
293
|
+
const result = {
|
|
294
|
+
schemaVersion: 1,
|
|
295
|
+
range,
|
|
296
|
+
surface,
|
|
297
|
+
days: windows.days,
|
|
298
|
+
filter: { provider: provider ?? null, model: model ?? null, apiKeyId: apiKeyId ?? null },
|
|
299
|
+
current: { since: windows.current.since, until: windows.current.until, summary: current, usageIncomplete: currentReport.usageIncomplete === true },
|
|
300
|
+
previous: { since: windows.previous.since, until: windows.previous.until, summary: previous, usageIncomplete: previousReport.usageIncomplete === true },
|
|
301
|
+
delta: deltaTotals,
|
|
302
|
+
change,
|
|
303
|
+
incomplete,
|
|
304
|
+
};
|
|
305
|
+
printData(result, wantsJson, wantsJson ? undefined : formatCompare({
|
|
306
|
+
range, days: windows.days, currentWindow: windows.current, previousWindow: windows.previous,
|
|
307
|
+
current, previous, incomplete,
|
|
308
|
+
}));
|
|
309
|
+
}
|
|
@@ -5,7 +5,7 @@ export interface UsageModelView {
|
|
|
5
5
|
query: string;
|
|
6
6
|
matchedModelCount: number;
|
|
7
7
|
returnedModelCount: number;
|
|
8
|
-
limit:
|
|
8
|
+
limit: number;
|
|
9
9
|
truncated: boolean;
|
|
10
10
|
}
|
|
11
11
|
|
|
@@ -36,7 +36,7 @@ export function takeUsageSearchOption(args: string[]): string | undefined {
|
|
|
36
36
|
return value;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
export function selectUsageModelView<T extends { models?: unknown }>(report: T, rawQuery: string): Omit<T, "models"> & { models: SearchableModel[]; modelView: UsageModelView } {
|
|
39
|
+
export function selectUsageModelView<T extends { models?: unknown }>(report: T, rawQuery: string, limit = 100): Omit<T, "models"> & { models: SearchableModel[]; modelView: UsageModelView } {
|
|
40
40
|
if (!report || !Array.isArray(report.models)) throw new Error("Invalid usage model rows; retry after checking the runtime.");
|
|
41
41
|
const models = report.models.map((raw: unknown): SearchableModel => {
|
|
42
42
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("Invalid usage model row.");
|
|
@@ -53,6 +53,6 @@ export function selectUsageModelView<T extends { models?: unknown }>(report: T,
|
|
|
53
53
|
const matching = models.toSorted((a, b) => b.totalTokens - a.totalTokens).filter(row => !query
|
|
54
54
|
|| row.model.toLowerCase().includes(query) || row.provider.toLowerCase().includes(query)
|
|
55
55
|
|| (row.resolvedModel ?? "").toLowerCase().includes(query));
|
|
56
|
-
const selected = matching.slice(0,
|
|
57
|
-
return { ...report, models: selected, modelView: { query, matchedModelCount: matching.length, returnedModelCount: selected.length, limit
|
|
56
|
+
const selected = matching.slice(0, limit);
|
|
57
|
+
return { ...report, models: selected, modelView: { query, matchedModelCount: matching.length, returnedModelCount: selected.length, limit, truncated: matching.length > selected.length } };
|
|
58
58
|
}
|
package/src/cli/usage-report.ts
CHANGED
|
@@ -113,7 +113,7 @@ function describeScope(data: UsageReportInput): string {
|
|
|
113
113
|
return terminalText(parts.join(", "));
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
export function formatUsageReport(data: UsageReportInput, options?: { modelView?: UsageModelView }): string[] {
|
|
116
|
+
export function formatUsageReport(data: UsageReportInput, options?: { modelView?: UsageModelView; modelRowLimit?: number }): string[] {
|
|
117
117
|
const summary = data.summary ?? {};
|
|
118
118
|
const lines: string[] = [describeScope(data), ""];
|
|
119
119
|
if (data.source === "hub" && data.scope === "client") {
|
|
@@ -192,7 +192,7 @@ export function formatUsageReport(data: UsageReportInput, options?: { modelView?
|
|
|
192
192
|
const models = view ? (data.models ?? []) : (data.models ?? []).filter(row => row.requests > 0);
|
|
193
193
|
if (models.length > 0) {
|
|
194
194
|
lines.push("");
|
|
195
|
-
const shown = models.slice(0, view?.limit ?? MAX_MODEL_ROWS);
|
|
195
|
+
const shown = models.slice(0, view?.limit ?? options?.modelRowLimit ?? MAX_MODEL_ROWS);
|
|
196
196
|
lines.push(...table(
|
|
197
197
|
["MODEL", "PROVIDER", "REQUESTS", "TOKENS", "EST. COST"],
|
|
198
198
|
shown.map(row => [row.model ?? "-", row.provider, count(row.requests), count(row.totalTokens), usd(row.estimatedCostUsd)]),
|
package/src/cli/v2.ts
CHANGED
|
@@ -148,7 +148,7 @@ function printLocalStatus(deps: V2CliDeps): number {
|
|
|
148
148
|
const modeHint = getMultiAgentModeHintText();
|
|
149
149
|
log.log(`multi_agent_mode_hint_text: ${modeHint === null ? "(unset — effort-derived policy: ultra=proactive, else explicit)" : JSON.stringify(modeHint)}`);
|
|
150
150
|
if (isEnabled() && hasMaxThreads()) {
|
|
151
|
-
log.log("WARNING: [agents] max_threads is set — codex refuses to start while multi_agent_v2 is enabled. Remove
|
|
151
|
+
log.log("WARNING: [agents] max_concurrent_threads_per_session / max_threads is set — codex refuses to start while multi_agent_v2 is enabled. Remove the agents limit from config.toml (concurrency lives in features.multi_agent_v2.max_concurrent_threads_per_session).");
|
|
152
152
|
}
|
|
153
153
|
return 0;
|
|
154
154
|
}
|
|
@@ -107,9 +107,7 @@ export function startMachineListener(
|
|
|
107
107
|
const machineApiDeps: MachineApiDeps = {
|
|
108
108
|
sync: deps.machineApi?.sync ?? syncConnectedClient,
|
|
109
109
|
disconnect: deps.machineApi?.disconnect ?? disconnectClient,
|
|
110
|
-
scheduleStandaloneRecycle: deps.machineApi?.scheduleStandaloneRecycle ?? (
|
|
111
|
-
void import("./runtime").then(module => module.scheduleStandaloneRecycle(tokenFingerprint));
|
|
112
|
-
}),
|
|
110
|
+
scheduleStandaloneRecycle: deps.machineApi?.scheduleStandaloneRecycle ?? (() => {}),
|
|
113
111
|
hubReachability: deps.machineApi?.hubReachability ?? (() => hubReachability),
|
|
114
112
|
setHubReachability: deps.machineApi?.setHubReachability ?? (value => { hubReachability = value; }),
|
|
115
113
|
...(linkMode ? { linkHomeName: () => linkHomeName(connection, sidecarOrNull) } : {}),
|
package/src/client/runtime.ts
CHANGED
|
@@ -250,6 +250,7 @@ export async function bindClientListener(
|
|
|
250
250
|
try {
|
|
251
251
|
const server = startListener(port, {
|
|
252
252
|
state: request.state,
|
|
253
|
+
machineApi: { scheduleStandaloneRecycle },
|
|
253
254
|
...(request.linkStatus ? { linkStatus: request.linkStatus } : {}),
|
|
254
255
|
...(request.linkKeySource ? { linkKeySource: request.linkKeySource } : {}),
|
|
255
256
|
...(request.linkTunnel ? { linkTunnel: request.linkTunnel } : {}),
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { isAbsolute, join } from "node:path";
|
|
3
|
+
import type { OpencodeLaunchEnv } from "./config-export/contracts";
|
|
4
|
+
|
|
5
|
+
// Leaf owner of client path validation, shared by config-export and gentle-shell-path
|
|
6
|
+
// without an import cycle between them.
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Expand `~` against the effective home; REFUSE anything still relative.
|
|
10
|
+
*
|
|
11
|
+
* A first attempt at this called `resolve()` and claimed the path would "mean
|
|
12
|
+
* the same thing next time". It does not: `resolve()` only anchors the current
|
|
13
|
+
* invocation, so applying from one directory and disabling from another still
|
|
14
|
+
* resolved two different files — the second reported "not applied" and left
|
|
15
|
+
* the managed block behind with nothing claiming it.
|
|
16
|
+
*
|
|
17
|
+
* There is no honest way to recover the other process's cwd, so a relative
|
|
18
|
+
* selector is refused at the boundary instead of being silently anchored to
|
|
19
|
+
* whichever directory we happened to start in. A `~` path IS stable, because
|
|
20
|
+
* it anchors to the effective home rather than the cwd.
|
|
21
|
+
*/
|
|
22
|
+
export class ClientPathError extends Error {}
|
|
23
|
+
|
|
24
|
+
export function absoluteClientPath(raw: string, home: string, variable: string): string {
|
|
25
|
+
const trimmed = raw.trim();
|
|
26
|
+
if (trimmed === "~") return home;
|
|
27
|
+
if (trimmed.startsWith("~/") || trimmed.startsWith("~\\")) return join(home, trimmed.slice(2));
|
|
28
|
+
if (!isAbsolute(trimmed)) {
|
|
29
|
+
throw new ClientPathError(
|
|
30
|
+
`${variable} must be an absolute path or start with ~; "${trimmed}" depends on the working directory, `
|
|
31
|
+
+ "so opencodex and the client would disagree about which file it names.",
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return trimmed;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Pi resolves its agent directory from `PI_CODING_AGENT_DIR`, falling back to
|
|
39
|
+
* `~/.pi/agent`. `ompAgentDir` below already reads that variable — OMP is a Pi
|
|
40
|
+
* derivative — so Pi's own resolver honoring it is what makes the two agree
|
|
41
|
+
* rather than a new claim about Pi's contract.
|
|
42
|
+
*
|
|
43
|
+
* A relative override is refused for the same reason MCode's and ZCode's are: a
|
|
44
|
+
* background proxy and a foreground client can have different working
|
|
45
|
+
* directories, and would otherwise disagree about which file is named.
|
|
46
|
+
*/
|
|
47
|
+
export function piAgentDir(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
48
|
+
const override = env.PI_CODING_AGENT_DIR?.trim();
|
|
49
|
+
if (override) return absoluteClientPath(override, home, "PI_CODING_AGENT_DIR");
|
|
50
|
+
return join(home, ".pi", "agent");
|
|
51
|
+
}
|
|
@@ -202,6 +202,7 @@ export interface ExportContext {
|
|
|
202
202
|
export type DroidReasoningDefaults = Record<string, string>;
|
|
203
203
|
|
|
204
204
|
export type ExportClientId =
|
|
205
|
+
| "qoder"
|
|
205
206
|
| "opencode"
|
|
206
207
|
| "pi"
|
|
207
208
|
| "omp"
|
|
@@ -219,7 +220,8 @@ export type ExportClientId =
|
|
|
219
220
|
| "cline"
|
|
220
221
|
| "commandcode"
|
|
221
222
|
| "kilo"
|
|
222
|
-
| "droid"
|
|
223
|
+
| "droid"
|
|
224
|
+
| "gentle-shell";
|
|
223
225
|
|
|
224
226
|
export interface ExportClientSpec {
|
|
225
227
|
id: ExportClientId;
|
|
@@ -9,8 +9,9 @@ import { shouldInjectApiAuthHeader } from "../../codex/inject";
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
13
|
-
* non-positive value
|
|
12
|
+
* Validate the supplied catalog window (which may be a labelled vendor estimate).
|
|
13
|
+
* A missing, non-finite, or non-positive value omits every context-derived field;
|
|
14
|
+
* this serializer does not independently infer a window.
|
|
14
15
|
*/
|
|
15
16
|
export function authoritativeContextWindow(contextWindow: number | undefined): number | undefined {
|
|
16
17
|
if (typeof contextWindow === "number" && Number.isFinite(contextWindow) && contextWindow > 0) {
|