@iislee/opencodex 2.11.0 → 2.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS_INSTALL.md +109 -0
- package/README.md +117 -22
- package/bin/ocx.mjs +245 -88
- package/bin/package-main.mjs +1 -1
- package/gui/dist/assets/index-Be1sDpWw.css +1 -0
- package/gui/dist/assets/index-D4OKgUV4.js +114 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/alibaba-color.svg +1 -1
- package/gui/dist/provider-icons/antigravity-color.svg +1 -1
- package/gui/dist/provider-icons/claude-color.svg +1 -1
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/commandcode-color.svg +1 -0
- package/gui/dist/provider-icons/copilot-color.svg +1 -1
- package/gui/dist/provider-icons/cursor-color.svg +1 -1
- package/gui/dist/provider-icons/deepseek-color.svg +1 -1
- package/gui/dist/provider-icons/firepass-color.svg +1 -1
- package/gui/dist/provider-icons/fireworks-color.svg +1 -1
- package/gui/dist/provider-icons/gemini-color.svg +1 -1
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
- package/gui/dist/provider-icons/grok.svg +1 -1
- package/gui/dist/provider-icons/groq-color.svg +1 -1
- package/gui/dist/provider-icons/huggingface-color.svg +1 -1
- package/gui/dist/provider-icons/kimi-color.svg +1 -1
- package/gui/dist/provider-icons/kiro-color.svg +2 -2
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
- package/gui/dist/provider-icons/mistral-color.svg +1 -1
- package/gui/dist/provider-icons/moonshot-color.svg +1 -1
- package/gui/dist/provider-icons/nvidia-color.svg +1 -1
- package/gui/dist/provider-icons/ollama-color.svg +1 -1
- package/gui/dist/provider-icons/openai.svg +1 -1
- package/gui/dist/provider-icons/opencode.svg +2 -1
- package/gui/dist/provider-icons/openrouter-color.svg +1 -1
- package/gui/dist/provider-icons/pi.svg +2 -2
- package/gui/dist/provider-icons/qianfan-color.svg +1 -1
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/vllm-color.svg +1 -1
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
- package/package.json +21 -10
- package/src/adapters/anthropic-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +475 -62
- package/src/adapters/base.ts +82 -7
- package/src/adapters/client-fingerprint.ts +18 -12
- package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
- package/src/adapters/command-code.ts +637 -0
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/catalog.ts +541 -0
- package/src/adapters/cursor/checkpoint-store.ts +308 -0
- package/src/adapters/cursor/cursor-errors.ts +144 -6
- package/src/adapters/cursor/discovery.ts +122 -14
- package/src/adapters/cursor/effort-map.ts +106 -4
- package/src/adapters/cursor/envelope-echo.ts +290 -0
- package/src/adapters/cursor/framing.ts +39 -0
- package/src/adapters/cursor/h2-pool.ts +123 -0
- package/src/adapters/cursor/http1-bidi.ts +361 -0
- package/src/adapters/cursor/images.ts +704 -0
- package/src/adapters/cursor/live-models.ts +180 -59
- package/src/adapters/cursor/live-transport.ts +623 -170
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-common.ts +23 -2
- package/src/adapters/cursor/native-exec-desktop.ts +23 -0
- package/src/adapters/cursor/native-exec-fs.ts +10 -7
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +5 -3
- package/src/adapters/cursor/native-exec.ts +121 -14
- package/src/adapters/cursor/protobuf-events.ts +829 -11
- package/src/adapters/cursor/protobuf-request.ts +447 -75
- package/src/adapters/cursor/request-builder.ts +271 -35
- package/src/adapters/cursor/tool-definitions.ts +252 -12
- package/src/adapters/cursor/tool-result-normalize.ts +115 -0
- package/src/adapters/cursor/transport.ts +22 -0
- package/src/adapters/cursor/types.ts +48 -1
- package/src/adapters/cursor.ts +336 -39
- package/src/adapters/exec-tool-result-normalize.ts +99 -0
- package/src/adapters/google-antigravity-replay.ts +713 -41
- package/src/adapters/google-antigravity-wire.ts +48 -10
- package/src/adapters/google-errors.ts +44 -12
- package/src/adapters/google-http.ts +38 -10
- package/src/adapters/google-tool-schema.ts +80 -15
- package/src/adapters/google-truncation.ts +11 -0
- package/src/adapters/google.ts +742 -86
- package/src/adapters/identity.ts +39 -6
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +27 -0
- package/src/adapters/kiro-errors.ts +11 -0
- package/src/adapters/kiro-events.ts +19 -1
- package/src/adapters/kiro-thinking.ts +18 -2
- package/src/adapters/kiro-tools.ts +65 -17
- package/src/adapters/kiro.ts +270 -73
- package/src/adapters/mimo-free.ts +17 -0
- package/src/adapters/openai-chat-url.ts +11 -0
- package/src/adapters/openai-chat.ts +1377 -344
- package/src/adapters/openai-responses-url.ts +16 -0
- package/src/adapters/openai-responses.ts +1164 -64
- package/src/adapters/registry.ts +175 -0
- package/src/adapters/responses-tool-schema.ts +67 -0
- package/src/adapters/run-turn-queue.ts +36 -1
- package/src/adapters/tool-call-id.ts +119 -0
- package/src/adapters/tool-catalog-nudge.ts +105 -21
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/adapters/xai-web-search.ts +186 -0
- package/src/bridge.ts +512 -92
- package/src/chat/inbound.ts +57 -20
- package/src/chat/outbound.ts +97 -33
- package/src/claude/agents-inject.ts +40 -10
- package/src/claude/context-windows.ts +37 -14
- package/src/claude/desktop-3p.ts +243 -9
- package/src/claude/gateway-cache.ts +41 -4
- package/src/claude/inbound.ts +72 -3
- package/src/claude/model-info.ts +38 -15
- package/src/claude/outbound.ts +88 -21
- package/src/cli/access.ts +46 -3
- package/src/cli/account-api.ts +150 -19
- package/src/cli/account-auth.ts +33 -6
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +648 -28
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +97 -19
- package/src/cli/agent.ts +53 -2
- package/src/cli/alias.ts +66 -0
- package/src/cli/capabilities-command.ts +94 -0
- package/src/cli/capabilities.ts +496 -0
- package/src/cli/claude-agent-startup-sync.ts +73 -0
- package/src/cli/claude-desktop.ts +60 -15
- package/src/cli/claude.ts +129 -35
- package/src/cli/codex-log-guard-doctor.ts +103 -0
- package/src/cli/codex-shim-autorestore.ts +2 -0
- package/src/cli/codex-shim-readiness.ts +76 -0
- package/src/cli/combo.ts +8 -0
- package/src/cli/config-command.ts +74 -10
- package/src/cli/dispatch.ts +821 -0
- package/src/cli/doctor.ts +440 -56
- package/src/cli/ensure-desired-integrations.ts +152 -0
- package/src/cli/export-command.ts +46 -20
- package/src/cli/help.ts +34 -274
- package/src/cli/index.ts +417 -528
- package/src/cli/init.ts +5 -17
- package/src/cli/inspect.ts +230 -0
- package/src/cli/integrations.ts +120 -2
- package/src/cli/lab.ts +607 -0
- package/src/cli/launcher-context.ts +77 -0
- package/src/cli/minimax.ts +497 -0
- package/src/cli/models-runtime.ts +130 -2
- package/src/cli/models.ts +118 -16
- package/src/cli/observe.ts +123 -11
- package/src/cli/opencode.ts +4 -2
- package/src/cli/provider-runtime.ts +34 -3
- package/src/cli/provider.ts +25 -3
- package/src/cli/ready.ts +301 -0
- package/src/cli/registry.ts +476 -0
- package/src/cli/root.ts +86 -0
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +57 -10
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +51 -4
- package/src/cli/storage.ts +234 -0
- package/src/cli/system-command.ts +16 -0
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +153 -6
- package/src/cli/usage-report.ts +184 -0
- package/src/cli/v2.ts +137 -18
- package/src/cli/version-skew.ts +46 -0
- package/src/cli.ts +1 -1
- package/src/clients/config-export.ts +1442 -23
- package/src/clients/effective-status.ts +30 -2
- package/src/clients/probes/cline.ts +81 -0
- package/src/codex/account-label.ts +35 -1
- package/src/codex/account-lifecycle.ts +130 -13
- package/src/codex/account-namespaces.ts +49 -3
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +84 -0
- package/src/codex/account-store.ts +29 -2
- package/src/codex/account-usability.ts +25 -2
- package/src/codex/admission.ts +256 -0
- package/src/codex/affinity-debug.ts +162 -0
- package/src/codex/app-server-processes.ts +570 -107
- package/src/codex/app-server-restart-service.ts +232 -0
- package/src/codex/auth-api.ts +893 -246
- package/src/codex/auth-collision.ts +5 -3
- package/src/codex/auth-context.ts +348 -32
- package/src/codex/autostart-health.ts +24 -1
- package/src/codex/catalog/account-models.ts +67 -0
- package/src/codex/catalog/aggregation.ts +86 -10
- package/src/codex/catalog/bundled.ts +331 -33
- package/src/codex/catalog/effort.ts +138 -32
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +555 -45
- package/src/codex/catalog/native-models.ts +75 -0
- package/src/codex/catalog/parsing.ts +269 -30
- package/src/codex/catalog/provider-fetch.ts +1626 -144
- package/src/codex/catalog/sync.ts +1576 -187
- package/src/codex/catalog-admission.ts +199 -0
- package/src/codex/catalog-refresh-status.ts +105 -0
- package/src/codex/catalog-write-serialization.ts +242 -0
- package/src/codex/catalog.ts +6 -3
- package/src/codex/codex-write-lock.ts +386 -0
- package/src/codex/convergence-types.ts +614 -0
- package/src/codex/convergence.ts +676 -0
- package/src/codex/coordinator-doctor.ts +332 -0
- package/src/codex/custom-model-catalog-migration.ts +176 -0
- package/src/codex/desired-state.ts +230 -0
- package/src/codex/desktop-app-restart.ts +355 -0
- package/src/codex/features.ts +636 -39
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +436 -0
- package/src/codex/history-lock.ts +242 -0
- package/src/codex/history-manifest.ts +112 -0
- package/src/codex/history-migration-guardian.ts +30 -24
- package/src/codex/history-provider.ts +1016 -235
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +223 -0
- package/src/codex/inject-coordination.ts +373 -0
- package/src/codex/inject.ts +1114 -152
- package/src/codex/injected-marker.ts +37 -3
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +81 -0
- package/src/codex/journal.ts +66 -4
- package/src/codex/log-guard/inspect.ts +524 -0
- package/src/codex/log-guard/lock.ts +150 -0
- package/src/codex/log-guard/maintenance.ts +403 -0
- package/src/codex/log-guard/path-safety.ts +88 -0
- package/src/codex/log-guard/policy.ts +44 -0
- package/src/codex/log-guard/processes.ts +205 -0
- package/src/codex/log-guard/protection.ts +489 -0
- package/src/codex/log-guard/sqlite-errors.ts +9 -0
- package/src/codex/main-account-cache.ts +24 -0
- package/src/codex/main-account.ts +29 -1
- package/src/codex/management-convergence.ts +167 -0
- package/src/codex/model-cache.ts +56 -10
- package/src/codex/model-entitlements.ts +360 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +178 -0
- package/src/codex/native-main-lock-file.ts +162 -0
- package/src/codex/native-main-owner.ts +329 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1531 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +622 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +675 -0
- package/src/codex/paths.ts +80 -1
- package/src/codex/plan-from-token.ts +140 -0
- package/src/codex/plan.ts +40 -0
- package/src/codex/plugins-doctor.ts +1 -1
- package/src/codex/pool-rotation.ts +74 -4
- package/src/codex/project-config-warnings.ts +20 -6
- package/src/codex/prompt-journal.ts +352 -0
- package/src/codex/prompt-layers.ts +1313 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota-rejection.ts +298 -0
- package/src/codex/quota.ts +264 -21
- package/src/codex/refresh.ts +11 -2
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/routing.ts +514 -94
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +1065 -31
- package/src/codex/subagent-model-fallback.ts +437 -43
- package/src/codex/sync.ts +191 -2
- package/src/codex/transition-state.ts +720 -0
- package/src/codex/upstream-host-health.ts +368 -0
- package/src/codex/user-identity.ts +557 -0
- package/src/codex/warmup.ts +187 -81
- package/src/codex/write-coordination.ts +114 -0
- package/src/combos/failover.ts +47 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/request.ts +32 -0
- package/src/combos/types.ts +91 -9
- package/src/compatibility/index.ts +26 -0
- package/src/compatibility/manifest.ts +253 -0
- package/src/compatibility/openai-responses.ts +121 -0
- package/src/config/atomic-write.ts +219 -0
- package/src/config/paths.ts +40 -0
- package/src/config/process-state.ts +309 -0
- package/src/config/provider-name.ts +24 -0
- package/src/config/provider-validation.ts +177 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +1713 -709
- package/src/generated/compatibility-version.json +3324 -0
- package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
- package/src/grok/inject.ts +16 -5
- package/src/grok/inspect.ts +45 -0
- package/src/grok/sync.ts +2 -2
- package/src/images/loop.ts +157 -33
- package/src/images/plan.ts +24 -13
- package/src/integrations/config-io.ts +269 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/mutation-flight.ts +71 -0
- package/src/integrations/native/ownership-preflight.ts +202 -0
- package/src/integrations/omp-yaml-source.ts +358 -0
- package/src/integrations/owned-refresh.ts +74 -0
- package/src/integrations/ownership-policy.ts +160 -0
- package/src/integrations/ownership.ts +155 -0
- package/src/integrations/registry.ts +166 -0
- package/src/integrations/serialize.ts +314 -0
- package/src/integrations/state.ts +433 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer-lock.ts +98 -0
- package/src/integrations/writer.ts +715 -0
- package/src/lab/artifacts/sanitize.ts +586 -0
- package/src/lab/artifacts/secure-fs.ts +475 -0
- package/src/lab/artifacts/store.ts +310 -0
- package/src/lab/automation/budgets.ts +78 -0
- package/src/lab/automation/config-persistence.ts +256 -0
- package/src/lab/automation/constants.ts +39 -0
- package/src/lab/automation/cooldown.ts +103 -0
- package/src/lab/automation/dispatch.ts +211 -0
- package/src/lab/automation/index.ts +13 -0
- package/src/lab/automation/orchestrator.ts +499 -0
- package/src/lab/automation/persistence.ts +512 -0
- package/src/lab/automation/planner.ts +371 -0
- package/src/lab/automation/policy.ts +136 -0
- package/src/lab/automation/queue.ts +191 -0
- package/src/lab/automation/recovery.ts +24 -0
- package/src/lab/automation/route-context.ts +21 -0
- package/src/lab/automation/run-key.ts +44 -0
- package/src/lab/automation/runs-query.ts +34 -0
- package/src/lab/automation/types.ts +160 -0
- package/src/lab/conformance/assertion.ts +325 -0
- package/src/lab/conformance/digest.ts +22 -0
- package/src/lab/conformance/executor.ts +741 -0
- package/src/lab/conformance/fixture-provider.ts +27 -0
- package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
- package/src/lab/conformance/harness-budget.ts +47 -0
- package/src/lab/conformance/index.ts +5 -0
- package/src/lab/conformance/jcs.ts +64 -0
- package/src/lab/conformance/json-pointer.ts +39 -0
- package/src/lab/conformance/manifest.ts +180 -0
- package/src/lab/conformance/mcp-stub.ts +179 -0
- package/src/lab/conformance/negative-controls.ts +164 -0
- package/src/lab/conformance/observation.ts +355 -0
- package/src/lab/conformance/runner.ts +68 -0
- package/src/lab/conformance/sse-normalize.ts +59 -0
- package/src/lab/conformance/suite-manifest.ts +78 -0
- package/src/lab/conformance/types.ts +214 -0
- package/src/lab/constants.ts +126 -0
- package/src/lab/digest.ts +64 -0
- package/src/lab/events/errors.ts +9 -0
- package/src/lab/events/limits.ts +117 -0
- package/src/lab/events/types.ts +229 -0
- package/src/lab/events/validate.ts +781 -0
- package/src/lab/fabric/constants.ts +40 -0
- package/src/lab/fabric/executor.ts +492 -0
- package/src/lab/fabric/index.ts +80 -0
- package/src/lab/fabric/manifest.ts +222 -0
- package/src/lab/fabric/observe.ts +489 -0
- package/src/lab/fabric/patch.ts +79 -0
- package/src/lab/fabric/producer-child.ts +139 -0
- package/src/lab/fabric/producer-isolate.ts +276 -0
- package/src/lab/fabric/producer-protocol.ts +61 -0
- package/src/lab/fabric/scratch.ts +439 -0
- package/src/lab/fabric/subject.ts +106 -0
- package/src/lab/fabric/types.ts +134 -0
- package/src/lab/fabric/verifier.ts +98 -0
- package/src/lab/index.ts +54 -0
- package/src/lab/ledger/artifact-refs.ts +127 -0
- package/src/lab/ledger/invalidation.ts +136 -0
- package/src/lab/ledger/purge.ts +310 -0
- package/src/lab/ledger/store.ts +532 -0
- package/src/lab/live/credential-lease.ts +53 -0
- package/src/lab/live/destination.ts +155 -0
- package/src/lab/live/executor.ts +336 -0
- package/src/lab/live/inert-tools.ts +56 -0
- package/src/lab/live/manifest.ts +85 -0
- package/src/lab/live/mcp-loopback.ts +57 -0
- package/src/lab/live/runner.ts +19 -0
- package/src/lab/live/sandbox.ts +61 -0
- package/src/lab/live/suite-manifest.ts +41 -0
- package/src/lab/live/transport.ts +118 -0
- package/src/lab/live/types.ts +197 -0
- package/src/lab/observe/from-conformance.ts +301 -0
- package/src/lab/observe/from-live.ts +117 -0
- package/src/lab/paths.ts +153 -0
- package/src/lab/projection/rebuild.ts +495 -0
- package/src/lab/projection/schema.ts +135 -0
- package/src/lab/projection/verdicts.ts +474 -0
- package/src/lab/projection/verification.ts +412 -0
- package/src/lab/public/bundle.ts +217 -0
- package/src/lab/public/community-authority.ts +175 -0
- package/src/lab/public/community-files.ts +29 -0
- package/src/lab/public/community.ts +479 -0
- package/src/lab/public/file-safety.ts +155 -0
- package/src/lab/public/ids.ts +26 -0
- package/src/lab/public/index.ts +16 -0
- package/src/lab/public/mutation-lock.ts +424 -0
- package/src/lab/public/operator.ts +353 -0
- package/src/lab/public/origin-purge.ts +79 -0
- package/src/lab/public/origin.ts +203 -0
- package/src/lab/public/privacy.ts +143 -0
- package/src/lab/public/private-file.ts +261 -0
- package/src/lab/public/project.ts +124 -0
- package/src/lab/public/purge-test-fault.ts +21 -0
- package/src/lab/public/purge.ts +223 -0
- package/src/lab/public/registry.ts +44 -0
- package/src/lab/public/revocation.ts +252 -0
- package/src/lab/public/signature.ts +243 -0
- package/src/lab/public/storage.ts +105 -0
- package/src/lab/public/strict-json.ts +206 -0
- package/src/lab/public/time.ts +26 -0
- package/src/lab/public/types.ts +172 -0
- package/src/lab/public/validate.ts +391 -0
- package/src/lab/query/catalog.ts +101 -0
- package/src/lab/query/connection.ts +107 -0
- package/src/lab/query/constants.ts +4 -0
- package/src/lab/query/cursor.ts +132 -0
- package/src/lab/query/dto-map.ts +277 -0
- package/src/lab/query/errors.ts +22 -0
- package/src/lab/query/freshness.ts +53 -0
- package/src/lab/query/index.ts +45 -0
- package/src/lab/query/latest-observation.ts +59 -0
- package/src/lab/query/passive-production.ts +159 -0
- package/src/lab/query/queries.ts +444 -0
- package/src/lab/query/types.ts +266 -0
- package/src/lab/subject/behavior-fingerprint.ts +77 -0
- package/src/lab/subject/installation-salt.ts +112 -0
- package/src/lab/subject/protocol-subject.ts +80 -0
- package/src/lab/subject/route-subject.ts +74 -0
- package/src/lib/admin-secrets.ts +24 -0
- package/src/lib/app-owned-memory-stores.ts +22 -0
- package/src/lib/bounded-body.ts +167 -11
- package/src/lib/bun-runtime.ts +125 -12
- package/src/lib/bun-stream-caps.ts +13 -9
- package/src/lib/codex-restart-contract.ts +120 -0
- package/src/lib/config-ownership.ts +26 -2
- package/src/lib/destination-policy.ts +65 -1
- package/src/lib/errors.ts +80 -5
- package/src/lib/fabric-task-execution-authority.ts +7 -0
- package/src/lib/fabric-task-host.ts +29 -0
- package/src/lib/lab-activation.ts +223 -0
- package/src/lib/lab-live-execution-authority.ts +13 -0
- package/src/lib/lab-live-host.ts +30 -0
- package/src/lib/lab-live-pinned-sender.ts +56 -0
- package/src/lib/lab-live-route-production.ts +130 -0
- package/src/lib/lab-passive-linker-registration.ts +26 -0
- package/src/lib/local-management-attestation.ts +51 -0
- package/src/lib/local-management-capability.ts +100 -0
- package/src/lib/local-provider-reload-contract.ts +100 -0
- package/src/lib/optional-shutdown-hooks.ts +57 -0
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/lib/pinned-http.ts +145 -26
- package/src/lib/process-control.ts +6 -2
- package/src/lib/provider-outbound.ts +49 -9
- package/src/lib/redact.ts +419 -3
- package/src/lib/self-launch-argv.ts +15 -0
- package/src/lib/server-resource-ownership.ts +71 -0
- package/src/lib/service-secrets.ts +15 -0
- package/src/lib/shadow-call.ts +35 -4
- package/src/lib/sse-decoder.ts +41 -0
- package/src/lib/state-store-registrations.ts +10 -2
- package/src/lib/system-restart-contract.ts +73 -0
- package/src/lib/token-estimate.ts +19 -2
- package/src/lib/tool-argument-integers.ts +253 -0
- package/src/lib/translator-budget.ts +44 -0
- package/src/lib/upstream-http-version.ts +57 -0
- package/src/lib/upstream-reachability.ts +95 -0
- package/src/lib/upstream-retry.ts +156 -3
- package/src/lib/windows-atomic-replace.ts +155 -0
- package/src/lib/windows-elevation.ts +70 -2
- package/src/lib/windows-secret-acl.ts +409 -69
- package/src/lib/windows-service-wrappers.ts +72 -0
- package/src/lib/windows-text.ts +106 -0
- package/src/lib/windows-user-principal.ts +341 -0
- package/src/lib/winsw.ts +33 -5
- package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
- package/src/oauth/account-import/index.ts +15 -0
- package/src/oauth/account-import/parser.ts +83 -0
- package/src/oauth/account-import/registry.ts +18 -0
- package/src/oauth/account-import/service.ts +75 -0
- package/src/oauth/account-import/types.ts +91 -0
- package/src/oauth/anthropic.ts +12 -1
- package/src/oauth/callback-server.ts +30 -4
- package/src/oauth/chatgpt.ts +12 -1
- package/src/oauth/cline.ts +203 -0
- package/src/oauth/command-code.ts +239 -0
- package/src/oauth/cursor.ts +46 -5
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/google-antigravity.ts +35 -3
- package/src/oauth/health.ts +20 -12
- package/src/oauth/index.ts +462 -71
- package/src/oauth/key-providers.ts +25 -0
- package/src/oauth/kimi.ts +25 -3
- package/src/oauth/kiro.ts +95 -6
- package/src/oauth/local-token-detect.ts +13 -2
- package/src/oauth/log.ts +3 -1
- package/src/oauth/login-cli.ts +88 -28
- package/src/oauth/nous.ts +798 -0
- package/src/oauth/open-browser-choice.ts +26 -0
- package/src/oauth/store.ts +133 -26
- package/src/oauth/token-guardian.ts +9 -3
- package/src/oauth/types.ts +15 -0
- package/src/pi/models.ts +2 -2
- package/src/providers/alibaba-region-migration.ts +1 -1
- package/src/providers/antigravity-models.ts +521 -31
- package/src/providers/auto-compact-budget.ts +65 -0
- package/src/providers/base-url-choices.ts +10 -0
- package/src/providers/codex-capacity.ts +292 -0
- package/src/providers/command-code-efforts.ts +176 -0
- package/src/providers/context-cap.ts +26 -8
- package/src/providers/cursor-pool.ts +72 -0
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +281 -6
- package/src/providers/fastwire.ts +507 -0
- package/src/providers/free-directory.ts +10 -7
- package/src/providers/google-vertex-location.ts +14 -0
- package/src/providers/key-failover.ts +71 -3
- package/src/providers/label.ts +35 -2
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +115 -22
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/model-rename-migration.ts +255 -0
- package/src/providers/model-rename-startup.ts +28 -0
- package/src/providers/new-model-policy.ts +146 -0
- package/src/providers/openai-sidecar.ts +72 -4
- package/src/providers/openai-tier-startup.ts +31 -2
- package/src/providers/openai-tiers.ts +119 -4
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/opencode-zen-rate-limit.ts +102 -0
- package/src/providers/provider-id-rewrite.ts +30 -0
- package/src/providers/quota.ts +1379 -40
- package/src/providers/registry.ts +1545 -112
- package/src/providers/request-pacing.ts +310 -0
- package/src/providers/service-tier.ts +277 -0
- package/src/providers/slug-codec.ts +94 -6
- package/src/providers/static-model-discovery.ts +86 -0
- package/src/providers/xai-responses-opt-in.ts +15 -0
- package/src/providers/xai-transport.ts +32 -4
- package/src/reasoning-effort.ts +68 -3
- package/src/responses/apply-patch-envelope.ts +63 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/compaction.ts +26 -1
- package/src/responses/custom-tool-compat.ts +384 -0
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/namespace-tool-compat.ts +435 -0
- package/src/responses/parser.ts +225 -38
- package/src/responses/provider-continuation.ts +98 -0
- package/src/responses/provider-opaque-metadata.ts +73 -0
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/reasoning-replay-cache.ts +504 -0
- package/src/responses/schema.ts +12 -2
- package/src/responses/spill-store.ts +75 -10
- package/src/responses/state.ts +659 -31
- package/src/responses/thought-signature-replay.ts +364 -0
- package/src/responses/tool-search-compat.ts +301 -0
- package/src/responses/truncated-stop-reason.ts +60 -0
- package/src/responses/turn-termination.ts +107 -0
- package/src/router.ts +405 -31
- package/src/routing/analytics.ts +379 -0
- package/src/routing/capability.ts +244 -0
- package/src/routing/compatibility/assemble.ts +73 -0
- package/src/routing/compatibility/behavior.ts +278 -0
- package/src/routing/compatibility/catalog.ts +99 -0
- package/src/routing/compatibility/endpoint.ts +52 -0
- package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
- package/src/routing/compatibility/policy.ts +181 -0
- package/src/routing/compatibility/provider-slot.ts +56 -0
- package/src/routing/compatibility/reader.ts +110 -0
- package/src/routing/compatibility/subject.ts +191 -0
- package/src/routing/compatibility/types.ts +64 -0
- package/src/routing/compatibility/version.ts +104 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +495 -0
- package/src/routing/health.ts +412 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +605 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile-namespace.ts +15 -0
- package/src/routing/profile.ts +547 -0
- package/src/routing/quota.ts +155 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +776 -0
- package/src/server/adapter-resolve.ts +2 -29
- package/src/server/auth-cors.ts +289 -48
- package/src/server/background-lifecycle.ts +182 -0
- package/src/server/chat-completions.ts +154 -72
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +445 -0
- package/src/server/claude-messages.ts +160 -44
- package/src/server/direct-local-http.ts +347 -0
- package/src/server/effort-policy.ts +18 -0
- package/src/server/github-copilot-responses-repair.ts +338 -0
- package/src/server/gui-static.ts +39 -10
- package/src/server/images.ts +94 -12
- package/src/server/index.ts +1021 -190
- package/src/server/lifecycle.ts +336 -14
- package/src/server/live.ts +136 -17
- package/src/server/local-management-read-client.ts +90 -0
- package/src/server/local-provider-reload-client.ts +137 -0
- package/src/server/management/agent-settings-routes.ts +433 -116
- package/src/server/management/api-key-usage.ts +31 -5
- package/src/server/management/body.ts +6 -0
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +64 -25
- package/src/server/management/config-routes.ts +511 -51
- package/src/server/management/context.ts +89 -2
- package/src/server/management/integration-routes.ts +498 -0
- package/src/server/management/lab-automation-routes.ts +206 -0
- package/src/server/management/lab-routes.ts +563 -0
- package/src/server/management/logs-usage-routes.ts +127 -52
- package/src/server/management/model-routes.ts +461 -133
- package/src/server/management/model-rows.ts +167 -0
- package/src/server/management/native-integration-routes.ts +770 -0
- package/src/server/management/oauth-account-routes.ts +90 -8
- package/src/server/management/provider-capability-config.ts +48 -0
- package/src/server/management/provider-routes.ts +836 -159
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/route-registry.ts +332 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +380 -0
- package/src/server/management/shared.ts +27 -11
- package/src/server/management/sidebar-routes.ts +47 -31
- package/src/server/management/storage-log-guard-routes.ts +186 -0
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +289 -33
- package/src/server/management/system-routes.ts +77 -0
- package/src/server/management/usage-summary-cache.ts +9 -1
- package/src/server/management/vision-sidecar-options.ts +167 -0
- package/src/server/management/web-search-sidecar-options.ts +120 -0
- package/src/server/management-api.ts +126 -23
- package/src/server/management-auth.ts +220 -5
- package/src/server/passive-route-linker.ts +66 -0
- package/src/server/port-reclaim.ts +1 -1
- package/src/server/ports.ts +41 -1
- package/src/server/proxy-liveness.ts +161 -10
- package/src/server/readiness.ts +99 -0
- package/src/server/relay-eager.ts +221 -54
- package/src/server/relay.ts +479 -87
- package/src/server/request-decompress.ts +113 -6
- package/src/server/request-log-conversation.ts +51 -0
- package/src/server/request-log.ts +298 -31
- package/src/server/responses/agent-task-recovery-cache.ts +143 -0
- package/src/server/responses/agent-task-recovery.ts +465 -0
- package/src/server/responses/codex-auth-error.ts +55 -0
- package/src/server/responses/collaboration.ts +239 -36
- package/src/server/responses/combo-stream-preflight.ts +178 -0
- package/src/server/responses/compact.ts +444 -72
- package/src/server/responses/core.ts +3579 -436
- package/src/server/responses/empty-completion-guard.ts +333 -0
- package/src/server/responses/encrypted-payload.ts +62 -39
- package/src/server/responses/fetch-helpers.ts +89 -99
- package/src/server/responses/input-admission.ts +187 -0
- package/src/server/responses/pacing-overload.ts +13 -0
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/policy-fallback.ts +178 -0
- package/src/server/responses/responses-field-backfill.ts +343 -0
- package/src/server/responses/terminal-guard.ts +26 -5
- package/src/server/responses/upstream-error.ts +5 -0
- package/src/server/responses/ws-upstream.ts +381 -0
- package/src/server/responses-custom-tool-repair.ts +363 -0
- package/src/server/responses-item-id-repair.ts +54 -6
- package/src/server/responses-json-events.ts +90 -0
- package/src/server/responses-model-rewrite.ts +29 -0
- package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/responses-terminal-repair.ts +363 -0
- package/src/server/responses-tool-search-repair.ts +267 -0
- package/src/server/responses-undeclared-tool-guard.ts +376 -0
- package/src/server/responses.ts +18 -2
- package/src/server/search.ts +78 -13
- package/src/server/sse-frame-buffer.ts +319 -0
- package/src/server/sse-payload-rewrite.ts +110 -22
- package/src/server/startup-action-control.ts +8 -1
- package/src/server/startup-health-cache.ts +19 -1
- package/src/server/system-env.ts +80 -9
- package/src/server/ws-bridge.ts +53 -40
- package/src/service-manager-probe.ts +892 -0
- package/src/service.ts +1153 -94
- package/src/sidecar/auth.ts +92 -0
- package/src/sidecar/candidates.ts +83 -0
- package/src/storage/cleanup.ts +2 -2
- package/src/storage/policy-job.ts +14 -4
- package/src/storage/policy.ts +88 -23
- package/src/storage/scanner.ts +1 -1
- package/src/storage/worker-lifecycle.ts +14 -14
- package/src/tray/windows-tray.ps1 +83 -9
- package/src/tray/windows.ts +43 -16
- package/src/types/accounts.ts +37 -0
- package/src/types/config.ts +911 -0
- package/src/types/provider.ts +614 -0
- package/src/types/request.ts +397 -0
- package/src/types/tools.ts +236 -0
- package/src/types/wire.ts +80 -0
- package/src/types.ts +107 -1237
- package/src/update/index.ts +37 -23
- package/src/update/job.ts +445 -68
- package/src/update/notify.ts +12 -6
- package/src/update/npm-cache-preflight.d.mts +47 -0
- package/src/update/npm-cache-preflight.mjs +201 -0
- package/src/update/transactional-install.d.mts +22 -0
- package/src/update/transactional-install.mjs +266 -0
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +287 -16
- package/src/usage/log.ts +666 -50
- package/src/usage/summary.ts +388 -17
- package/src/usage/user-cost-overlay-reconciler.ts +313 -0
- package/src/usage/user-cost-overlays.ts +314 -0
- package/src/vision/anthropic-describe.ts +10 -6
- package/src/vision/backends.ts +97 -0
- package/src/vision/describe.ts +27 -16
- package/src/vision/eligibility.ts +250 -0
- package/src/vision/index.ts +238 -24
- package/src/vision/reasoning.ts +55 -0
- package/src/vision/routed-describe.ts +175 -0
- package/src/vision/timeout-bounds.ts +9 -0
- package/src/web-search/anthropic-executor.ts +13 -7
- package/src/web-search/backends.ts +108 -0
- package/src/web-search/exa-executor.ts +119 -0
- package/src/web-search/executor.ts +21 -6
- package/src/web-search/gemini-executor.ts +141 -0
- package/src/web-search/index.ts +150 -15
- package/src/web-search/loop.ts +284 -54
- package/src/web-search/parse.ts +125 -30
- package/src/web-search/sources.ts +60 -0
- package/src/web-search/xai-executor.ts +219 -0
- package/gui/dist/assets/index-DTpMHS4F.js +0 -67
- package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
- package/gui/dist/provider-icons/antigravity.svg +0 -1
- package/gui/dist/provider-icons/claude.svg +0 -1
- package/gui/dist/provider-icons/copilot.svg +0 -1
- package/gui/dist/provider-icons/cursor.svg +0 -2
- package/gui/dist/provider-icons/gemini.svg +0 -1
- package/gui/dist/provider-icons/grok-color.svg +0 -1
- package/gui/dist/provider-icons/kiro.svg +0 -14
- package/src/cli/internal-dispatch.ts +0 -20
package/src/codex/routing.ts
CHANGED
|
@@ -3,7 +3,8 @@ import { saveConfigPreservingClaudeCode } from "../config";
|
|
|
3
3
|
import { isCodexAccountGenerationLive, readCodexAccountRecord } from "./account-store";
|
|
4
4
|
import { codexAccountLogLabel } from "./account-label";
|
|
5
5
|
import { isCodexAccountPaused } from "./account-pause";
|
|
6
|
-
import {
|
|
6
|
+
import { clearCodexAccountPin, codexAccountPriorityLookup, pinnedCodexAccountId } from "./account-priority";
|
|
7
|
+
import { isCodexAccountUsable, type CodexAccountUsabilityOptions } from "./account-usability";
|
|
7
8
|
import { isAccountNeedsReauth, markAccountNeedsReauth } from "./account-runtime-state";
|
|
8
9
|
import {
|
|
9
10
|
POOL_KEY_CODEX,
|
|
@@ -14,14 +15,17 @@ import {
|
|
|
14
15
|
peekRoundRobinAccount,
|
|
15
16
|
pickRoundRobinAccount,
|
|
16
17
|
seedPoolRotationAccount,
|
|
18
|
+
selectPriorityTier,
|
|
17
19
|
} from "./pool-rotation";
|
|
18
20
|
import { CODEX_UNKNOWN_USAGE_SCORE, getAccountQuota } from "./quota";
|
|
21
|
+
import { isThirtyDayOnlyCodexPlan } from "./plan";
|
|
19
22
|
import { MAIN_CODEX_ACCOUNT_ID, getMainAccountPlan } from "./main-account";
|
|
20
23
|
import { isSelectableCodexPoolAccount } from "./account-id";
|
|
21
24
|
import type { OcxConfig } from "../types";
|
|
22
25
|
import { captureConfigGeneration, type GenerationContext } from "../lib/state-store-sweeper";
|
|
23
26
|
import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
|
|
24
27
|
import { retainedUtf8Bytes } from "../lib/admission";
|
|
28
|
+
import { recordUpstreamHostFailure } from "./upstream-host-health";
|
|
25
29
|
|
|
26
30
|
type ThreadAffinityEntry = {
|
|
27
31
|
accountId: string;
|
|
@@ -122,8 +126,9 @@ const quotaScopedHealth = new Map<string, Map<CodexQuotaScope, CodexUpstreamHeal
|
|
|
122
126
|
let lastReconciledGeneration = 0;
|
|
123
127
|
let liveHealthAccountIds = new Set<string>();
|
|
124
128
|
|
|
125
|
-
export type CodexUpstreamOutcome = number | "connect_error" | "timeout";
|
|
126
|
-
export type CodexUpstreamOutcomeClass = "success" | "credential"
|
|
129
|
+
export type CodexUpstreamOutcome = number | "connect_error" | "timeout" | "connect_neutral";
|
|
130
|
+
export type CodexUpstreamOutcomeClass = "success" | "credential"
|
|
131
|
+
| "workspace" | "quota" | "transient" | "caller" | "neutral" | "unknown";
|
|
127
132
|
export type CodexCooldownSource = "retry-after" | "reset-derived" | "default";
|
|
128
133
|
/**
|
|
129
134
|
* Native Codex quota groups known to be independent upstream. Keep the mapping
|
|
@@ -133,6 +138,20 @@ export type CodexCooldownSource = "retry-after" | "reset-derived" | "default";
|
|
|
133
138
|
*/
|
|
134
139
|
export type CodexQuotaScope = "shared" | "spark";
|
|
135
140
|
|
|
141
|
+
export type CodexQuotaRecoveryProbeClaim = {
|
|
142
|
+
accountId: string;
|
|
143
|
+
scope?: CodexQuotaScope;
|
|
144
|
+
leaseId: string;
|
|
145
|
+
cooldownGeneration: number;
|
|
146
|
+
credentialGeneration: number;
|
|
147
|
+
/** Claim-time `replacedAt`; unchanged after a probe-owned refresh, stamped on external replacement. */
|
|
148
|
+
credentialReplacedAt?: number;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export type CodexQuotaRecoveryProbeProof = {
|
|
152
|
+
credentialGeneration?: number;
|
|
153
|
+
};
|
|
154
|
+
|
|
136
155
|
/**
|
|
137
156
|
* Requests without a resolved native model retain the historic one-account-per-
|
|
138
157
|
* thread behavior. Requests with a known quota scope get an independent
|
|
@@ -173,10 +192,26 @@ export type CodexUpstreamOutcomeMeta = {
|
|
|
173
192
|
retryAfter?: string | null;
|
|
174
193
|
resetAt?: unknown | unknown[];
|
|
175
194
|
now?: number;
|
|
195
|
+
/** (provider, host) ledger key for account-neutral reachability failures (#914). */
|
|
196
|
+
hostKey?: string;
|
|
197
|
+
/**
|
|
198
|
+
* Upstream denial evidence for a 403. A workspace/entitlement denial means the CREDENTIAL
|
|
199
|
+
* is fine and the account simply cannot reach this workspace, so it must not be quarantined
|
|
200
|
+
* for reauthentication (#1789). Absent evidence keeps the historical credential handling.
|
|
201
|
+
*/
|
|
202
|
+
denial?: "workspace" | "entitlement";
|
|
203
|
+
/** Stable transport code recorded alongside a neutral host failure. */
|
|
204
|
+
lastFailureCode?: string;
|
|
176
205
|
/** Native model selected for this request; used only for confirmed scoped quotas. */
|
|
177
206
|
modelId?: string;
|
|
178
207
|
/** When set, clears affinity for this thread immediately on transient failure. */
|
|
179
208
|
threadId?: string | null;
|
|
209
|
+
/**
|
|
210
|
+
* Suppress Pool rotation and quota/transient affinity mutations for an account-qualified
|
|
211
|
+
* request. Credential failures still sweep stale affinities because reauthentication is
|
|
212
|
+
* account-wide.
|
|
213
|
+
*/
|
|
214
|
+
fixedAccount?: boolean;
|
|
180
215
|
/**
|
|
181
216
|
* Probe lease held by this request, when it was admitted through an active
|
|
182
217
|
* quota cooldown. Only the outcome carrying the current lease may clear the
|
|
@@ -195,8 +230,14 @@ export type CodexUpstreamOutcomeMeta = {
|
|
|
195
230
|
writerGeneration?: number;
|
|
196
231
|
};
|
|
197
232
|
|
|
198
|
-
function hasConfiguredPoolAccount(
|
|
199
|
-
|
|
233
|
+
function hasConfiguredPoolAccount(
|
|
234
|
+
config: OcxConfig,
|
|
235
|
+
accountId: string,
|
|
236
|
+
selectionOptions?: CodexAccountUsabilityOptions,
|
|
237
|
+
): boolean {
|
|
238
|
+
if (accountId === MAIN_CODEX_ACCOUNT_ID) {
|
|
239
|
+
return isCodexAccountUsable(config, accountId, selectionOptions);
|
|
240
|
+
}
|
|
200
241
|
return (config.codexAccounts ?? [])
|
|
201
242
|
.some(account => isSelectableCodexPoolAccount(account) && account.id === accountId);
|
|
202
243
|
}
|
|
@@ -281,23 +322,42 @@ function deleteScopedHealth(accountId: string, scope: CodexQuotaScope): void {
|
|
|
281
322
|
export function computeCodexUsageScore(quota: {
|
|
282
323
|
weeklyPercent?: number;
|
|
283
324
|
monthlyPercent?: number;
|
|
284
|
-
|
|
325
|
+
shortPercent?: number;
|
|
326
|
+
} | null, plan?: unknown): number {
|
|
285
327
|
if (!quota) return CODEX_UNKNOWN_USAGE_SCORE;
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
328
|
+
const finite = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value);
|
|
329
|
+
const longWindows = isThirtyDayOnlyCodexPlan(plan)
|
|
330
|
+
? [quota.monthlyPercent]
|
|
331
|
+
: [quota.weeklyPercent, quota.monthlyPercent];
|
|
332
|
+
const knownLong = longWindows.filter(finite);
|
|
333
|
+
// The short burst window only REFINES a known long-window position; it cannot stand in for
|
|
334
|
+
// one. A snapshot carrying just `shortPercent: 0` would otherwise score a flat 0 and make an
|
|
335
|
+
// account whose weekly/monthly usage is entirely unverified look like the emptiest in the
|
|
336
|
+
// pool, so `pickLowestUsageAmong` would send every request to it. Unknown has to stay
|
|
337
|
+
// unknown until a governing window is actually observed.
|
|
338
|
+
if (knownLong.length === 0) return CODEX_UNKNOWN_USAGE_SCORE;
|
|
339
|
+
const values = finite(quota.shortPercent) ? [...knownLong, quota.shortPercent] : knownLong;
|
|
340
|
+
return Math.max(...values);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function classifyCodexUpstreamOutcome(
|
|
344
|
+
outcome: CodexUpstreamOutcome,
|
|
345
|
+
denial?: "workspace" | "entitlement",
|
|
346
|
+
): CodexUpstreamOutcomeClass {
|
|
347
|
+
if (outcome === "connect_neutral") return "neutral";
|
|
298
348
|
if (outcome === "connect_error" || outcome === "timeout") return "transient";
|
|
299
349
|
if (!Number.isFinite(outcome)) return "unknown";
|
|
300
350
|
if (outcome >= 200 && outcome < 300) return "success";
|
|
351
|
+
// Explicit 3xx policy (#914): a redirect response is relayed as-is and is
|
|
352
|
+
// never account or host health evidence — it proves the host is reachable
|
|
353
|
+
// and says nothing about the credential. Relayed as the neutral class so a
|
|
354
|
+
// stray 3xx cannot increment an account's transient streak.
|
|
355
|
+
if (outcome >= 300 && outcome < 400) return "neutral";
|
|
356
|
+
// 401 is always a credential problem. A 403 is only a credential problem when nothing
|
|
357
|
+
// tells us otherwise: a workspace/entitlement denial (#1789) means the credential is valid
|
|
358
|
+
// and the account simply lacks access here, so quarantining it for reauth is wrong advice.
|
|
359
|
+
// Absent denial evidence the historical mapping stands, so the change fails safe.
|
|
360
|
+
if (outcome === 403 && denial !== undefined) return "workspace";
|
|
301
361
|
if (outcome === 401 || outcome === 403) return "credential";
|
|
302
362
|
// 402 Payment Required is treated as quota exhaustion for pool cooldown/failover
|
|
303
363
|
// (same-request alternate retry records this outcome for the depleted account).
|
|
@@ -405,6 +465,135 @@ function canAcquireQuotaProbeLease(health: CodexUpstreamHealth | undefined, now:
|
|
|
405
465
|
return now - origin >= CODEX_QUOTA_PROBE_INTERVAL_MS;
|
|
406
466
|
}
|
|
407
467
|
|
|
468
|
+
/**
|
|
469
|
+
* Claim due reset-derived cooldown probes without consulting account selection.
|
|
470
|
+
* Pool credentials only: the main account has no quota-refresh single-flight.
|
|
471
|
+
*/
|
|
472
|
+
export function claimDueCodexQuotaRecoveryProbes(
|
|
473
|
+
config: OcxConfig,
|
|
474
|
+
limit: number,
|
|
475
|
+
now = Date.now(),
|
|
476
|
+
): CodexQuotaRecoveryProbeClaim[] {
|
|
477
|
+
const boundedLimit = Math.max(0, Math.floor(limit));
|
|
478
|
+
if (boundedLimit === 0) return [];
|
|
479
|
+
const candidates: Array<{
|
|
480
|
+
accountId: string;
|
|
481
|
+
scope?: CodexQuotaScope;
|
|
482
|
+
health: CodexUpstreamHealth;
|
|
483
|
+
credentialGeneration: number;
|
|
484
|
+
credentialReplacedAt?: number;
|
|
485
|
+
order: number;
|
|
486
|
+
}> = [];
|
|
487
|
+
for (const [order, account] of (config.codexAccounts ?? []).entries()) {
|
|
488
|
+
if (!isSelectableCodexPoolAccount(account)
|
|
489
|
+
|| isCodexAccountPaused(config, account.id)
|
|
490
|
+
|| isAccountNeedsReauth(account.id)) continue;
|
|
491
|
+
const record = readCodexAccountRecord(account.id);
|
|
492
|
+
if (!record?.credential || record.deletedAt != null) continue;
|
|
493
|
+
const due = [
|
|
494
|
+
{ scope: undefined, health: upstreamHealth.get(account.id) },
|
|
495
|
+
...[...(quotaScopedHealth.get(account.id) ?? [])].map(([scope, health]) => ({ scope, health })),
|
|
496
|
+
].filter((entry): entry is { scope?: CodexQuotaScope; health: CodexUpstreamHealth } =>
|
|
497
|
+
// `spark` is deliberately never claimed. `GET /backend-api/wham/usage` takes no scope
|
|
498
|
+
// parameter and returns generic weekly/monthly windows, so its result can never prove a
|
|
499
|
+
// spark recovery — a claim here would spend an upstream call to settle `false` every
|
|
500
|
+
// time, and (with one claim per account per pass) delay the shared scope that CAN recover.
|
|
501
|
+
entry.scope !== "spark"
|
|
502
|
+
&& entry.health?.cooldownSource === "reset-derived"
|
|
503
|
+
&& canAcquireQuotaProbeLease(entry.health, now))
|
|
504
|
+
.sort((a, b) =>
|
|
505
|
+
(a.health.lastProbeAt ?? a.health.cooldownSince ?? 0)
|
|
506
|
+
- (b.health.lastProbeAt ?? b.health.cooldownSince ?? 0));
|
|
507
|
+
const candidate = due[0];
|
|
508
|
+
if (candidate) candidates.push({
|
|
509
|
+
accountId: account.id,
|
|
510
|
+
...(candidate.scope ? { scope: candidate.scope } : {}),
|
|
511
|
+
health: candidate.health,
|
|
512
|
+
credentialGeneration: record.generation,
|
|
513
|
+
...(record.replacedAt !== undefined ? { credentialReplacedAt: record.replacedAt } : {}),
|
|
514
|
+
order,
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
candidates.sort((a, b) => {
|
|
518
|
+
const age = (a.health.lastProbeAt ?? a.health.cooldownSince ?? 0)
|
|
519
|
+
- (b.health.lastProbeAt ?? b.health.cooldownSince ?? 0);
|
|
520
|
+
return age || a.order - b.order;
|
|
521
|
+
});
|
|
522
|
+
return candidates.slice(0, boundedLimit).map(candidate => {
|
|
523
|
+
const leaseId = randomUUID();
|
|
524
|
+
const next = {
|
|
525
|
+
...candidate.health,
|
|
526
|
+
probeLeaseId: leaseId,
|
|
527
|
+
probeLeaseGeneration: candidate.health.cooldownGeneration ?? 0,
|
|
528
|
+
lastProbeAt: now,
|
|
529
|
+
};
|
|
530
|
+
if (candidate.scope) setScopedHealth(candidate.accountId, candidate.scope, next);
|
|
531
|
+
else upstreamHealth.set(candidate.accountId, next);
|
|
532
|
+
return {
|
|
533
|
+
accountId: candidate.accountId,
|
|
534
|
+
...(candidate.scope ? { scope: candidate.scope } : {}),
|
|
535
|
+
leaseId,
|
|
536
|
+
cooldownGeneration: candidate.health.cooldownGeneration ?? 0,
|
|
537
|
+
credentialGeneration: candidate.credentialGeneration,
|
|
538
|
+
...(candidate.credentialReplacedAt !== undefined
|
|
539
|
+
? { credentialReplacedAt: candidate.credentialReplacedAt }
|
|
540
|
+
: {}),
|
|
541
|
+
};
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/** Settle one background recovery claim without mutating account-wide outcome state. */
|
|
546
|
+
export function settleCodexQuotaRecoveryProbe(
|
|
547
|
+
claim: CodexQuotaRecoveryProbeClaim,
|
|
548
|
+
recovered: boolean,
|
|
549
|
+
proof: CodexQuotaRecoveryProbeProof,
|
|
550
|
+
now = Date.now(),
|
|
551
|
+
): boolean {
|
|
552
|
+
const health = claim.scope
|
|
553
|
+
? scopedHealthFor(claim.accountId, claim.scope)
|
|
554
|
+
: upstreamHealth.get(claim.accountId);
|
|
555
|
+
if (!health || health.probeLeaseId !== claim.leaseId) return false;
|
|
556
|
+
const currentRecord = readCodexAccountRecord(claim.accountId);
|
|
557
|
+
const proofGeneration = proof.credentialGeneration;
|
|
558
|
+
// A probe-owned token refresh (getValidCodexToken) advances the credential generation by
|
|
559
|
+
// exactly one while preserving `replacedAt`; an external credential replacement bumps the
|
|
560
|
+
// generation too but stamps a fresh `replacedAt`. Accept the +1 transition only when the
|
|
561
|
+
// claim-time lineage is intact AND the generation the fresh quota was proven under is live.
|
|
562
|
+
const generationFenced = proofGeneration !== undefined
|
|
563
|
+
&& (proofGeneration === claim.credentialGeneration
|
|
564
|
+
? isCodexAccountGenerationLive(claim.accountId, proofGeneration)
|
|
565
|
+
: proofGeneration === claim.credentialGeneration + 1
|
|
566
|
+
&& currentRecord?.replacedAt === claim.credentialReplacedAt
|
|
567
|
+
&& isCodexAccountGenerationLive(claim.accountId, proofGeneration));
|
|
568
|
+
const fenced = (health.cooldownGeneration ?? 0) === claim.cooldownGeneration
|
|
569
|
+
&& (health.probeLeaseGeneration ?? 0) === claim.cooldownGeneration
|
|
570
|
+
&& generationFenced;
|
|
571
|
+
if (!recovered || !fenced) {
|
|
572
|
+
const released = withProbeLeaseReleased(health, now);
|
|
573
|
+
if (claim.scope) setScopedHealth(claim.accountId, claim.scope, released);
|
|
574
|
+
else upstreamHealth.set(claim.accountId, released);
|
|
575
|
+
return false;
|
|
576
|
+
}
|
|
577
|
+
if (claim.scope) {
|
|
578
|
+
deleteScopedHealth(claim.accountId, claim.scope);
|
|
579
|
+
} else {
|
|
580
|
+
const {
|
|
581
|
+
cooldownUntil: _until,
|
|
582
|
+
cooldownSince: _since,
|
|
583
|
+
cooldownSource: _source,
|
|
584
|
+
probeLeaseId: _leaseId,
|
|
585
|
+
probeLeaseGeneration: _leaseGeneration,
|
|
586
|
+
...rest
|
|
587
|
+
} = health;
|
|
588
|
+
upstreamHealth.set(claim.accountId, {
|
|
589
|
+
...rest,
|
|
590
|
+
cooldownGeneration: claim.cooldownGeneration + 1,
|
|
591
|
+
lastProbeAt: now,
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
return true;
|
|
595
|
+
}
|
|
596
|
+
|
|
408
597
|
/** Acquire the recovery probe for one confirmed model-specific quota group. */
|
|
409
598
|
export function tryAcquireCodexQuotaScopeProbeLease(
|
|
410
599
|
accountId: string,
|
|
@@ -423,6 +612,15 @@ export function tryAcquireCodexQuotaScopeProbeLease(
|
|
|
423
612
|
return probeLeaseId;
|
|
424
613
|
}
|
|
425
614
|
|
|
615
|
+
/** Side-effect-free check for a confirmed model-specific quota probe. */
|
|
616
|
+
export function canAcquireCodexQuotaScopeProbeLease(
|
|
617
|
+
accountId: string,
|
|
618
|
+
scope: CodexQuotaScope,
|
|
619
|
+
now = Date.now(),
|
|
620
|
+
): boolean {
|
|
621
|
+
return canAcquireQuotaProbeLease(scopedHealthFor(accountId, scope), now);
|
|
622
|
+
}
|
|
623
|
+
|
|
426
624
|
/**
|
|
427
625
|
* Hand a probe lease back without recording an upstream outcome. Used by paths
|
|
428
626
|
* that take a lease and then fail before any request reaches upstream.
|
|
@@ -624,11 +822,12 @@ function isCodexAccountSelectable(
|
|
|
624
822
|
accountId: string,
|
|
625
823
|
now: number,
|
|
626
824
|
quotaScope?: CodexQuotaScope,
|
|
825
|
+
selectionOptions?: CodexAccountUsabilityOptions,
|
|
627
826
|
): boolean {
|
|
628
827
|
return !isCodexAccountPaused(config, accountId)
|
|
629
828
|
&& getCodexQuotaHealthSnapshot(accountId, quotaScope, now) === null
|
|
630
829
|
&& !isCodexAccountSoftAvoided(accountId, now)
|
|
631
|
-
&& isCodexAccountUsable(config, accountId);
|
|
830
|
+
&& isCodexAccountUsable(config, accountId, selectionOptions);
|
|
632
831
|
}
|
|
633
832
|
|
|
634
833
|
function threadAffinityScope(quotaScope?: CodexQuotaScope): ThreadAffinityScope {
|
|
@@ -736,7 +935,8 @@ function getEligiblePoolAccounts(
|
|
|
736
935
|
excludeId?: string,
|
|
737
936
|
now = Date.now(),
|
|
738
937
|
quotaScope?: CodexQuotaScope,
|
|
739
|
-
|
|
938
|
+
selectionOptions?: CodexAccountUsabilityOptions,
|
|
939
|
+
): readonly string[] {
|
|
740
940
|
const ids = (config.codexAccounts ?? [])
|
|
741
941
|
.filter(account => isSelectableCodexPoolAccount(account)
|
|
742
942
|
&& account.id !== excludeId
|
|
@@ -744,7 +944,7 @@ function getEligiblePoolAccounts(
|
|
|
744
944
|
&& !isAccountNeedsReauth(account.id))
|
|
745
945
|
.filter(account => getCodexQuotaHealthSnapshot(account.id, quotaScope, now) === null)
|
|
746
946
|
.filter(account => !isCodexAccountSoftAvoided(account.id, now))
|
|
747
|
-
.filter(account => isCodexAccountUsable(config, account.id))
|
|
947
|
+
.filter(account => isCodexAccountUsable(config, account.id, selectionOptions))
|
|
748
948
|
.map(account => account.id);
|
|
749
949
|
// The main Codex account is not stored in config.codexAccounts; include it as a
|
|
750
950
|
// first-class rotation candidate when its read-only token is usable (Option A).
|
|
@@ -754,30 +954,48 @@ function getEligiblePoolAccounts(
|
|
|
754
954
|
&& !isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID)
|
|
755
955
|
&& getCodexQuotaHealthSnapshot(MAIN_CODEX_ACCOUNT_ID, quotaScope, now) === null
|
|
756
956
|
&& !isCodexAccountSoftAvoided(MAIN_CODEX_ACCOUNT_ID, now)
|
|
757
|
-
&& isCodexAccountUsable(config, MAIN_CODEX_ACCOUNT_ID)
|
|
957
|
+
&& isCodexAccountUsable(config, MAIN_CODEX_ACCOUNT_ID, selectionOptions)
|
|
758
958
|
) {
|
|
759
959
|
ids.unshift(MAIN_CODEX_ACCOUNT_ID);
|
|
760
960
|
}
|
|
761
|
-
|
|
961
|
+
// Single choke point for selection order: every strategy, failover, and preview
|
|
962
|
+
// reaches the pool through here, so tiering applies once rather than per picker.
|
|
963
|
+
// Eligibility above is unchanged — this only narrows an already-eligible list.
|
|
964
|
+
return selectPriorityTier(
|
|
965
|
+
ids,
|
|
966
|
+
codexAccountPriorityLookup(config),
|
|
967
|
+
id => hasCodexQuotaHeadroom(config, id),
|
|
968
|
+
pinnedCodexAccountId(config),
|
|
969
|
+
);
|
|
762
970
|
}
|
|
763
971
|
|
|
764
972
|
function listEligibleCodexAccountIds(
|
|
765
973
|
config: OcxConfig,
|
|
766
974
|
now: number,
|
|
767
975
|
quotaScope?: CodexQuotaScope,
|
|
768
|
-
|
|
769
|
-
|
|
976
|
+
selectionOptions?: CodexAccountUsabilityOptions,
|
|
977
|
+
): readonly string[] {
|
|
978
|
+
return getEligiblePoolAccounts(config, undefined, now, quotaScope, selectionOptions);
|
|
770
979
|
}
|
|
771
980
|
|
|
772
981
|
function stickyLimitForConfig(config: OcxConfig): number {
|
|
773
982
|
return normalizeAccountPoolStickyLimit(config.accountPoolStickyLimit);
|
|
774
983
|
}
|
|
775
984
|
|
|
776
|
-
|
|
985
|
+
/**
|
|
986
|
+
* Whether an account still has quota to give under the auto-switch threshold.
|
|
987
|
+
*
|
|
988
|
+
* Fill-first and the priority tier filter share this predicate, and share both of
|
|
989
|
+
* its escape hatches. A disabled threshold means only health, pause, and reauth
|
|
990
|
+
* may drain an account; unknown usage is a guess, so it must neither force
|
|
991
|
+
* fill-first off the active account nor drain a tier that was simply never
|
|
992
|
+
* primed. A genuinely exhausted account 429s into cooldown and leaves
|
|
993
|
+
* eligibility on its own.
|
|
994
|
+
*/
|
|
995
|
+
function hasCodexQuotaHeadroom(config: OcxConfig, accountId: string): boolean {
|
|
777
996
|
const threshold = config.autoSwitchThreshold ?? 80;
|
|
778
997
|
if (threshold <= 0) return true;
|
|
779
998
|
const usage = computeCodexUsageScore(getAccountQuota(accountId), getPoolAccountPlan(config, accountId));
|
|
780
|
-
// Unknown usage must not force fill-first to abandon the active account.
|
|
781
999
|
if (isUnknownUsage(usage)) return true;
|
|
782
1000
|
return usage < threshold;
|
|
783
1001
|
}
|
|
@@ -790,37 +1008,39 @@ function pickFillFirstCodexAccount(
|
|
|
790
1008
|
config: OcxConfig,
|
|
791
1009
|
now: number,
|
|
792
1010
|
quotaScope?: CodexQuotaScope,
|
|
1011
|
+
selectionOptions?: CodexAccountUsabilityOptions,
|
|
793
1012
|
): string | null {
|
|
794
|
-
const eligible = listEligibleCodexAccountIds(config, now, quotaScope);
|
|
1013
|
+
const eligible = listEligibleCodexAccountIds(config, now, quotaScope, selectionOptions);
|
|
795
1014
|
if (eligible.length === 0) return null;
|
|
796
1015
|
|
|
797
1016
|
const active = getEffectiveActiveCodexAccountId(config);
|
|
798
|
-
if (active && eligible.includes(active) &&
|
|
1017
|
+
if (active && eligible.includes(active) && hasCodexQuotaHeadroom(config, active)) {
|
|
799
1018
|
return active;
|
|
800
1019
|
}
|
|
801
1020
|
|
|
802
|
-
return pickNextFillFirstCodexAccount(config, active ?? null, eligible, now);
|
|
1021
|
+
return pickNextFillFirstCodexAccount(config, active ?? null, eligible, now, selectionOptions);
|
|
803
1022
|
}
|
|
804
1023
|
|
|
805
1024
|
/** Next eligible account in stable order after `afterId` (wrapping). */
|
|
806
1025
|
function pickNextFillFirstCodexAccount(
|
|
807
1026
|
config: OcxConfig,
|
|
808
1027
|
afterId: string | null,
|
|
809
|
-
eligible = listEligibleCodexAccountIds(config, Date.now()),
|
|
1028
|
+
eligible: readonly string[] = listEligibleCodexAccountIds(config, Date.now()),
|
|
810
1029
|
_now = Date.now(),
|
|
1030
|
+
selectionOptions?: CodexAccountUsabilityOptions,
|
|
811
1031
|
): string | null {
|
|
812
1032
|
if (eligible.length === 0) return null;
|
|
813
1033
|
const ordered = [...eligible].sort((a, b) => a.localeCompare(b));
|
|
814
1034
|
if (!afterId) {
|
|
815
1035
|
// Prefer an under-threshold account when starting with no active cursor.
|
|
816
1036
|
for (const id of ordered) {
|
|
817
|
-
if (
|
|
1037
|
+
if (hasCodexQuotaHeadroom(config, id)) return id;
|
|
818
1038
|
}
|
|
819
1039
|
return ordered[0] ?? null;
|
|
820
1040
|
}
|
|
821
1041
|
|
|
822
1042
|
const allConfigured = [
|
|
823
|
-
...(isCodexAccountUsable(config, MAIN_CODEX_ACCOUNT_ID) || afterId === MAIN_CODEX_ACCOUNT_ID
|
|
1043
|
+
...(isCodexAccountUsable(config, MAIN_CODEX_ACCOUNT_ID, selectionOptions) || afterId === MAIN_CODEX_ACCOUNT_ID
|
|
824
1044
|
? [MAIN_CODEX_ACCOUNT_ID]
|
|
825
1045
|
: []),
|
|
826
1046
|
...(config.codexAccounts ?? []).filter(account => !account.isMain).map(account => account.id),
|
|
@@ -829,7 +1049,7 @@ function pickNextFillFirstCodexAccount(
|
|
|
829
1049
|
const startIdx = stableAll.indexOf(afterId);
|
|
830
1050
|
if (startIdx < 0) {
|
|
831
1051
|
for (const id of ordered) {
|
|
832
|
-
if (
|
|
1052
|
+
if (hasCodexQuotaHeadroom(config, id)) return id;
|
|
833
1053
|
}
|
|
834
1054
|
return ordered[0] ?? null;
|
|
835
1055
|
}
|
|
@@ -840,7 +1060,7 @@ function pickNextFillFirstCodexAccount(
|
|
|
840
1060
|
const candidate = stableAll[(startIdx + step) % stableAll.length]!;
|
|
841
1061
|
if (!eligible.includes(candidate)) continue;
|
|
842
1062
|
if (!fallback) fallback = candidate;
|
|
843
|
-
if (
|
|
1063
|
+
if (hasCodexQuotaHeadroom(config, candidate)) return candidate;
|
|
844
1064
|
}
|
|
845
1065
|
return fallback ?? ordered[0] ?? null;
|
|
846
1066
|
}
|
|
@@ -866,6 +1086,7 @@ function pickUnboundStrategyAccount(
|
|
|
866
1086
|
now: number,
|
|
867
1087
|
commit: boolean,
|
|
868
1088
|
quotaScope?: CodexQuotaScope,
|
|
1089
|
+
selectionOptions?: CodexAccountUsabilityOptions,
|
|
869
1090
|
): string | null {
|
|
870
1091
|
const strategy = normalizeAccountPoolStrategy(config.accountPoolStrategy);
|
|
871
1092
|
if (strategy === "quota") return null;
|
|
@@ -873,7 +1094,7 @@ function pickUnboundStrategyAccount(
|
|
|
873
1094
|
|
|
874
1095
|
let picked: string | null = null;
|
|
875
1096
|
if (strategy === "round-robin") {
|
|
876
|
-
const eligible = listEligibleCodexAccountIds(config, now, quotaScope);
|
|
1097
|
+
const eligible = listEligibleCodexAccountIds(config, now, quotaScope, selectionOptions);
|
|
877
1098
|
const limit = stickyLimitForConfig(config);
|
|
878
1099
|
if (!commit) {
|
|
879
1100
|
return peekRoundRobinAccount(poolKey, eligible, limit);
|
|
@@ -887,7 +1108,7 @@ function pickUnboundStrategyAccount(
|
|
|
887
1108
|
}
|
|
888
1109
|
|
|
889
1110
|
if (strategy === "fill-first") {
|
|
890
|
-
picked = pickFillFirstCodexAccount(config, now, quotaScope);
|
|
1111
|
+
picked = pickFillFirstCodexAccount(config, now, quotaScope, selectionOptions);
|
|
891
1112
|
if (!picked) return null;
|
|
892
1113
|
if (commit) {
|
|
893
1114
|
if (!isIndependentCodexQuotaScope(quotaScope)) rememberActiveCodexAccount(config, picked);
|
|
@@ -911,10 +1132,11 @@ function pickLowerUsageAccount(
|
|
|
911
1132
|
activeUsage: number,
|
|
912
1133
|
now: number,
|
|
913
1134
|
quotaScope?: CodexQuotaScope,
|
|
1135
|
+
selectionOptions?: CodexAccountUsabilityOptions,
|
|
914
1136
|
): string {
|
|
915
1137
|
let best = active;
|
|
916
1138
|
let bestUsage = activeUsage;
|
|
917
|
-
for (const id of getEligiblePoolAccounts(config, active, now, quotaScope)) {
|
|
1139
|
+
for (const id of getEligiblePoolAccounts(config, active, now, quotaScope, selectionOptions)) {
|
|
918
1140
|
const usage = computeCodexUsageScore(getAccountQuota(id), getPoolAccountPlan(config, id));
|
|
919
1141
|
if (usage < bestUsage) {
|
|
920
1142
|
best = id;
|
|
@@ -924,15 +1146,11 @@ function pickLowerUsageAccount(
|
|
|
924
1146
|
return best;
|
|
925
1147
|
}
|
|
926
1148
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
excludeId?: string,
|
|
930
|
-
now = Date.now(),
|
|
931
|
-
quotaScope?: CodexQuotaScope,
|
|
932
|
-
): string | null {
|
|
1149
|
+
/** Coolest account in an already-selected candidate list; first index wins ties. */
|
|
1150
|
+
function pickLowestUsageAmong(config: OcxConfig, ids: readonly string[]): string | null {
|
|
933
1151
|
let best: string | null = null;
|
|
934
1152
|
let bestUsage = Number.POSITIVE_INFINITY;
|
|
935
|
-
for (const id of
|
|
1153
|
+
for (const id of ids) {
|
|
936
1154
|
const usage = computeCodexUsageScore(getAccountQuota(id), getPoolAccountPlan(config, id));
|
|
937
1155
|
if (usage < bestUsage) {
|
|
938
1156
|
best = id;
|
|
@@ -942,6 +1160,16 @@ export function pickLowestUsageCodexAccount(
|
|
|
942
1160
|
return best;
|
|
943
1161
|
}
|
|
944
1162
|
|
|
1163
|
+
export function pickLowestUsageCodexAccount(
|
|
1164
|
+
config: OcxConfig,
|
|
1165
|
+
excludeId?: string,
|
|
1166
|
+
now = Date.now(),
|
|
1167
|
+
quotaScope?: CodexQuotaScope,
|
|
1168
|
+
selectionOptions?: CodexAccountUsabilityOptions,
|
|
1169
|
+
): string | null {
|
|
1170
|
+
return pickLowestUsageAmong(config, getEligiblePoolAccounts(config, excludeId, now, quotaScope, selectionOptions));
|
|
1171
|
+
}
|
|
1172
|
+
|
|
945
1173
|
/**
|
|
946
1174
|
* Strategy-aware alternate after a cooled/excluded account (same-request 429 retry
|
|
947
1175
|
* and active promotion). Quota keeps lowest-usage; fill-first advances stable order;
|
|
@@ -952,17 +1180,22 @@ export function pickAlternateCodexAccount(
|
|
|
952
1180
|
excludeId: string,
|
|
953
1181
|
now = Date.now(),
|
|
954
1182
|
quotaScope?: CodexQuotaScope,
|
|
1183
|
+
selectionOptions?: CodexAccountUsabilityOptions,
|
|
955
1184
|
): string | null {
|
|
956
1185
|
const strategy = normalizeAccountPoolStrategy(config.accountPoolStrategy);
|
|
1186
|
+
// The exclusion is passed into eligibility rather than post-filtered off its
|
|
1187
|
+
// result: when the excluded account is the only healthy member of the top
|
|
1188
|
+
// tier, the tier walk must be free to descend instead of selecting that tier
|
|
1189
|
+
// and then handing back an empty list.
|
|
957
1190
|
if (strategy === "round-robin") {
|
|
958
|
-
const eligible =
|
|
1191
|
+
const eligible = getEligiblePoolAccounts(config, excludeId, now, quotaScope, selectionOptions);
|
|
959
1192
|
return pickRoundRobinAccount(codexPoolKeyForScope(quotaScope), eligible, stickyLimitForConfig(config));
|
|
960
1193
|
}
|
|
961
1194
|
if (strategy === "fill-first") {
|
|
962
|
-
const eligible =
|
|
963
|
-
return pickNextFillFirstCodexAccount(config, excludeId, eligible, now);
|
|
1195
|
+
const eligible = getEligiblePoolAccounts(config, excludeId, now, quotaScope, selectionOptions);
|
|
1196
|
+
return pickNextFillFirstCodexAccount(config, excludeId, eligible, now, selectionOptions);
|
|
964
1197
|
}
|
|
965
|
-
return pickLowestUsageCodexAccount(config, excludeId, now, quotaScope);
|
|
1198
|
+
return pickLowestUsageCodexAccount(config, excludeId, now, quotaScope, selectionOptions);
|
|
966
1199
|
}
|
|
967
1200
|
|
|
968
1201
|
/** Effective active: automatic runtime cursor, else operator/persisted selection. */
|
|
@@ -970,6 +1203,17 @@ export function getEffectiveActiveCodexAccountId(config: OcxConfig): string | un
|
|
|
970
1203
|
return runtimeActiveCodexAccountId ?? config.activeCodexAccountId;
|
|
971
1204
|
}
|
|
972
1205
|
|
|
1206
|
+
/**
|
|
1207
|
+
* Whether the account routing is currently on is there because an operator asked
|
|
1208
|
+
* for it, rather than because a strategy landed on it. Surfaces read this instead
|
|
1209
|
+
* of comparing the stored pin themselves, which would report a pin that a later
|
|
1210
|
+
* automatic pick has already moved past.
|
|
1211
|
+
*/
|
|
1212
|
+
export function isEffectiveCodexAccountPinned(config: OcxConfig): boolean {
|
|
1213
|
+
const pinned = pinnedCodexAccountId(config);
|
|
1214
|
+
return pinned !== undefined && pinned === getEffectiveActiveCodexAccountId(config);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
973
1217
|
/**
|
|
974
1218
|
* Automatic strategy / failover cursor only — never mutates `config.activeCodexAccountId`
|
|
975
1219
|
* so an unrelated `saveConfig` cannot persist transient rotation as operator selection.
|
|
@@ -978,10 +1222,22 @@ function rememberActiveCodexAccount(_config: OcxConfig, accountId: string): void
|
|
|
978
1222
|
runtimeActiveCodexAccountId = accountId;
|
|
979
1223
|
}
|
|
980
1224
|
|
|
1225
|
+
/**
|
|
1226
|
+
* End the manual pin when routing moves to a different account. Returns whether
|
|
1227
|
+
* the pin changed so the caller can fold it into a write it was already making.
|
|
1228
|
+
*/
|
|
1229
|
+
function releaseCodexAccountPinFor(config: OcxConfig, accountId: string): boolean {
|
|
1230
|
+
const pinned = pinnedCodexAccountId(config);
|
|
1231
|
+
if (pinned === undefined || pinned === accountId) return false;
|
|
1232
|
+
clearCodexAccountPin(config);
|
|
1233
|
+
return true;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
981
1236
|
/** Persist operator (or quota-strategy) active selection to config + disk. */
|
|
982
1237
|
function setActiveCodexAccount(config: OcxConfig, accountId: string): void {
|
|
983
1238
|
runtimeActiveCodexAccountId = undefined;
|
|
984
|
-
|
|
1239
|
+
const releasedPin = releaseCodexAccountPinFor(config, accountId);
|
|
1240
|
+
if (config.activeCodexAccountId === accountId && !releasedPin) return;
|
|
985
1241
|
config.activeCodexAccountId = accountId;
|
|
986
1242
|
saveConfigPreservingClaudeCode(config);
|
|
987
1243
|
}
|
|
@@ -992,6 +1248,10 @@ function promoteActiveCodexAccount(config: OcxConfig, accountId: string): void {
|
|
|
992
1248
|
setActiveCodexAccount(config, accountId);
|
|
993
1249
|
return;
|
|
994
1250
|
}
|
|
1251
|
+
// Runtime-only, like the cursor itself: a caller that persists (pause, delete)
|
|
1252
|
+
// saves this release with its own write; a transient failover does not, so the
|
|
1253
|
+
// pin survives a restart that also clears the failure history behind it.
|
|
1254
|
+
releaseCodexAccountPinFor(config, accountId);
|
|
995
1255
|
rememberActiveCodexAccount(config, accountId);
|
|
996
1256
|
}
|
|
997
1257
|
|
|
@@ -1010,6 +1270,10 @@ export function reconcileCodexActiveAfterExclusion(
|
|
|
1010
1270
|
if (config.activeCodexAccountId === excludedAccountId) {
|
|
1011
1271
|
config.activeCodexAccountId = undefined;
|
|
1012
1272
|
}
|
|
1273
|
+
// Excluding an account revokes any manual pin on it even when it was not the
|
|
1274
|
+
// effective active — otherwise a paused account keeps acting as a tier ceiling,
|
|
1275
|
+
// suppressing every higher-ordered account while being unusable itself.
|
|
1276
|
+
clearCodexAccountPin(config, excludedAccountId);
|
|
1013
1277
|
if (!wasEffective) return getEffectiveActiveCodexAccountId(config) ?? null;
|
|
1014
1278
|
|
|
1015
1279
|
runtimeActiveCodexAccountId = undefined;
|
|
@@ -1022,11 +1286,60 @@ function isUnknownUsage(usage: number): boolean {
|
|
|
1022
1286
|
return usage >= CODEX_UNKNOWN_USAGE_SCORE;
|
|
1023
1287
|
}
|
|
1024
1288
|
|
|
1289
|
+
/**
|
|
1290
|
+
* Move an unbound request back up when a higher tier regains headroom — the
|
|
1291
|
+
* weekly-reset case. Returns null when nothing should change.
|
|
1292
|
+
*
|
|
1293
|
+
* Downward moves are deliberately left to {@link applyQuotaAutoSwitch}: this only
|
|
1294
|
+
* fires when the tier filter has already excluded `active`, and only toward a
|
|
1295
|
+
* tier that strictly outranks it. Threads bound by affinity never reach here.
|
|
1296
|
+
*/
|
|
1297
|
+
function pickPriorityPreemption(
|
|
1298
|
+
config: OcxConfig,
|
|
1299
|
+
active: string,
|
|
1300
|
+
now: number,
|
|
1301
|
+
quotaScope?: CodexQuotaScope,
|
|
1302
|
+
selectionOptions?: CodexAccountUsabilityOptions,
|
|
1303
|
+
): string | null {
|
|
1304
|
+
const eligible = getEligiblePoolAccounts(config, undefined, now, quotaScope, selectionOptions);
|
|
1305
|
+
if (eligible.length === 0 || eligible.includes(active)) return null;
|
|
1306
|
+
const pinned = pinnedCodexAccountId(config);
|
|
1307
|
+
// A live pin already lowered the tier ceiling; never preempt past an explicit
|
|
1308
|
+
// operator choice. Same liveness test the tier filter applies, so preview and
|
|
1309
|
+
// resolve agree even before the pin is garbage-collected.
|
|
1310
|
+
if (pinned !== undefined && eligible.includes(pinned) && hasCodexQuotaHeadroom(config, pinned)) return null;
|
|
1311
|
+
const priorityOf = codexAccountPriorityLookup(config);
|
|
1312
|
+
if (priorityOf(eligible[0]!) <= priorityOf(active)) return null;
|
|
1313
|
+
// Members without headroom are in the tier only because a sibling has some;
|
|
1314
|
+
// picking one would hand the request straight back to a drained account.
|
|
1315
|
+
return pickLowestUsageAmong(config, eligible.filter(id => hasCodexQuotaHeadroom(config, id)));
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
/**
|
|
1319
|
+
* Release a pin whose account is durably drained. "Use this account now" ends
|
|
1320
|
+
* when the account crosses the auto-switch threshold or stops being selectable
|
|
1321
|
+
* at all — never on a transient cooldown or soft-avoid, which it recovers from
|
|
1322
|
+
* on its own. Clearing the pin also removes the condition, so this writes at
|
|
1323
|
+
* most once per pin.
|
|
1324
|
+
*/
|
|
1325
|
+
function releaseDrainedCodexAccountPin(config: OcxConfig): void {
|
|
1326
|
+
const pinned = pinnedCodexAccountId(config);
|
|
1327
|
+
if (pinned === undefined) return;
|
|
1328
|
+
const drained = !isCodexAccountUsable(config, pinned)
|
|
1329
|
+
|| isAccountNeedsReauth(pinned)
|
|
1330
|
+
|| isCodexAccountPaused(config, pinned)
|
|
1331
|
+
|| !hasCodexQuotaHeadroom(config, pinned);
|
|
1332
|
+
if (!drained) return;
|
|
1333
|
+
clearCodexAccountPin(config);
|
|
1334
|
+
saveConfigPreservingClaudeCode(config);
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1025
1337
|
function applyQuotaAutoSwitch(
|
|
1026
1338
|
config: OcxConfig,
|
|
1027
1339
|
active: string,
|
|
1028
1340
|
now: number,
|
|
1029
1341
|
quotaScope?: CodexQuotaScope,
|
|
1342
|
+
selectionOptions?: CodexAccountUsabilityOptions,
|
|
1030
1343
|
): string {
|
|
1031
1344
|
const threshold = config.autoSwitchThreshold ?? 80;
|
|
1032
1345
|
if (threshold <= 0) return active;
|
|
@@ -1036,7 +1349,7 @@ function applyQuotaAutoSwitch(
|
|
|
1036
1349
|
// threshold. Wait for quota priming instead of rotating among guesses.
|
|
1037
1350
|
if (isUnknownUsage(activeUsage)) return active;
|
|
1038
1351
|
if (activeUsage < threshold) return active;
|
|
1039
|
-
const best = pickLowerUsageAccount(config, active, activeUsage, now, quotaScope);
|
|
1352
|
+
const best = pickLowerUsageAccount(config, active, activeUsage, now, quotaScope, selectionOptions);
|
|
1040
1353
|
if (best !== active) {
|
|
1041
1354
|
if (!isIndependentCodexQuotaScope(quotaScope)) setActiveCodexAccount(config, best);
|
|
1042
1355
|
return best;
|
|
@@ -1053,11 +1366,23 @@ function shouldFailover(config: OcxConfig, accountId: string, now: number): bool
|
|
|
1053
1366
|
return !!health && health.consecutiveFailures >= threshold;
|
|
1054
1367
|
}
|
|
1055
1368
|
|
|
1056
|
-
function applyFailureFailover(
|
|
1369
|
+
function applyFailureFailover(
|
|
1370
|
+
config: OcxConfig,
|
|
1371
|
+
active: string,
|
|
1372
|
+
now: number,
|
|
1373
|
+
quotaScope?: CodexQuotaScope,
|
|
1374
|
+
selectionOptions?: CodexAccountUsabilityOptions,
|
|
1375
|
+
): string {
|
|
1057
1376
|
if (!shouldFailover(config, active, now)) return active;
|
|
1058
|
-
const best = pickAlternateCodexAccount(config, active, now);
|
|
1377
|
+
const best = pickAlternateCodexAccount(config, active, now, quotaScope, selectionOptions);
|
|
1059
1378
|
if (best) {
|
|
1060
|
-
|
|
1379
|
+
// The scope still routes away from the failing account — that is this request's
|
|
1380
|
+
// own decision — but an independent one must not persist a new shared active
|
|
1381
|
+
// account. recordCodexUpstreamOutcome only suppresses the promotion it makes at
|
|
1382
|
+
// the moment of the failure; the streak outlives the soft avoid, so a later
|
|
1383
|
+
// scoped resolve reaches here with the streak still tripped and would otherwise
|
|
1384
|
+
// move the shared cursor after all.
|
|
1385
|
+
if (!isIndependentCodexQuotaScope(quotaScope)) promoteActiveCodexAccount(config, best);
|
|
1061
1386
|
return best;
|
|
1062
1387
|
}
|
|
1063
1388
|
return active;
|
|
@@ -1086,13 +1411,14 @@ export function previewCodexAccountForRequest(
|
|
|
1086
1411
|
config: OcxConfig,
|
|
1087
1412
|
now = Date.now(),
|
|
1088
1413
|
quotaScope?: CodexQuotaScope,
|
|
1414
|
+
selectionOptions?: CodexAccountUsabilityOptions,
|
|
1089
1415
|
): string | null {
|
|
1090
1416
|
const entry = threadId ? getThreadAffinity(threadId, quotaScope) : undefined;
|
|
1091
1417
|
if (threadId && entry) {
|
|
1092
1418
|
if (
|
|
1093
1419
|
!isThreadAffinityExpired(entry, now)
|
|
1094
1420
|
&& isThreadAffinityGenerationLive(entry)
|
|
1095
|
-
&& isCodexAccountSelectable(config, entry.accountId, now, quotaScope)
|
|
1421
|
+
&& isCodexAccountSelectable(config, entry.accountId, now, quotaScope, selectionOptions)
|
|
1096
1422
|
&& !shouldFailover(config, entry.accountId, now)
|
|
1097
1423
|
) {
|
|
1098
1424
|
// Quota strategy only: non-quota strategies keep affinity for ongoing threads
|
|
@@ -1106,7 +1432,14 @@ export function previewCodexAccountForRequest(
|
|
|
1106
1432
|
getPoolAccountPlan(config, entry.accountId),
|
|
1107
1433
|
);
|
|
1108
1434
|
if (!isUnknownUsage(usage) && usage >= threshold) {
|
|
1109
|
-
const best = pickLowerUsageAccount(
|
|
1435
|
+
const best = pickLowerUsageAccount(
|
|
1436
|
+
config,
|
|
1437
|
+
entry.accountId,
|
|
1438
|
+
usage,
|
|
1439
|
+
now,
|
|
1440
|
+
quotaScope,
|
|
1441
|
+
selectionOptions,
|
|
1442
|
+
);
|
|
1110
1443
|
if (best !== entry.accountId) return best;
|
|
1111
1444
|
}
|
|
1112
1445
|
}
|
|
@@ -1116,37 +1449,48 @@ export function previewCodexAccountForRequest(
|
|
|
1116
1449
|
// Stale/unusable affinity is ignored for preview (no map mutation).
|
|
1117
1450
|
}
|
|
1118
1451
|
|
|
1119
|
-
const strategyPick = pickUnboundStrategyAccount(
|
|
1452
|
+
const strategyPick = pickUnboundStrategyAccount(
|
|
1453
|
+
config,
|
|
1454
|
+
threadId,
|
|
1455
|
+
now,
|
|
1456
|
+
false,
|
|
1457
|
+
quotaScope,
|
|
1458
|
+
selectionOptions,
|
|
1459
|
+
);
|
|
1120
1460
|
if (strategyPick) return strategyPick;
|
|
1121
1461
|
|
|
1122
1462
|
let active = getEffectiveActiveCodexAccountId(config) ?? null;
|
|
1123
1463
|
if (!active) {
|
|
1124
|
-
return pickLowestUsageCodexAccount(config, undefined, now, quotaScope);
|
|
1464
|
+
return pickLowestUsageCodexAccount(config, undefined, now, quotaScope, selectionOptions);
|
|
1125
1465
|
}
|
|
1126
|
-
if (!isCodexAccountSelectable(config, active, now, quotaScope)) {
|
|
1127
|
-
const fallback = pickLowestUsageCodexAccount(config, active, now, quotaScope);
|
|
1466
|
+
if (!isCodexAccountSelectable(config, active, now, quotaScope, selectionOptions)) {
|
|
1467
|
+
const fallback = pickLowestUsageCodexAccount(config, active, now, quotaScope, selectionOptions);
|
|
1128
1468
|
if (fallback) active = fallback;
|
|
1129
|
-
else if (
|
|
1469
|
+
else if (
|
|
1470
|
+
hasConfiguredPoolAccount(config, active, selectionOptions)
|
|
1471
|
+
&& !isCodexAccountPaused(config, active)
|
|
1472
|
+
) return active;
|
|
1130
1473
|
else return null;
|
|
1131
1474
|
}
|
|
1475
|
+
active = pickPriorityPreemption(config, active, now, quotaScope, selectionOptions) ?? active;
|
|
1132
1476
|
|
|
1133
1477
|
const threshold = config.autoSwitchThreshold ?? 80;
|
|
1134
1478
|
if (threshold > 0) {
|
|
1135
1479
|
const usage = computeCodexUsageScore(getAccountQuota(active), getPoolAccountPlan(config, active));
|
|
1136
1480
|
if (!isUnknownUsage(usage) && usage >= threshold) {
|
|
1137
|
-
active = pickLowerUsageAccount(config, active, usage, now, quotaScope);
|
|
1481
|
+
active = pickLowerUsageAccount(config, active, usage, now, quotaScope, selectionOptions);
|
|
1138
1482
|
}
|
|
1139
1483
|
}
|
|
1140
1484
|
if (shouldFailover(config, active, now)) {
|
|
1141
|
-
const best = pickLowestUsageCodexAccount(config, active, now, quotaScope);
|
|
1485
|
+
const best = pickLowestUsageCodexAccount(config, active, now, quotaScope, selectionOptions);
|
|
1142
1486
|
if (best) active = best;
|
|
1143
1487
|
}
|
|
1144
|
-
if (!isCodexAccountUsable(config, active)) {
|
|
1145
|
-
return hasConfiguredPoolAccount(config, active) ? active : null;
|
|
1488
|
+
if (!isCodexAccountUsable(config, active, selectionOptions)) {
|
|
1489
|
+
return hasConfiguredPoolAccount(config, active, selectionOptions) ? active : null;
|
|
1146
1490
|
}
|
|
1147
1491
|
if (isCodexAccountPaused(config, active)) return null;
|
|
1148
1492
|
if (getCodexQuotaHealthSnapshot(active, quotaScope, now)) {
|
|
1149
|
-
return hasConfiguredPoolAccount(config, active) ? active : null;
|
|
1493
|
+
return hasConfiguredPoolAccount(config, active, selectionOptions) ? active : null;
|
|
1150
1494
|
}
|
|
1151
1495
|
return active;
|
|
1152
1496
|
}
|
|
@@ -1156,7 +1500,14 @@ export function resolveCodexAccountForThreadDetailed(
|
|
|
1156
1500
|
config: OcxConfig,
|
|
1157
1501
|
now = Date.now(),
|
|
1158
1502
|
quotaScope?: CodexQuotaScope,
|
|
1503
|
+
selectionOptions?: CodexAccountUsabilityOptions,
|
|
1159
1504
|
): CodexThreadResolution {
|
|
1505
|
+
// Retiring a spent manual pin is independent of affinity: an existing thread
|
|
1506
|
+
// keeps its account below, but the operator's tier ceiling must not silently
|
|
1507
|
+
// revive after quota resets. Independent model scopes must never persist a
|
|
1508
|
+
// change to shared routing state.
|
|
1509
|
+
if (!isIndependentCodexQuotaScope(quotaScope)) releaseDrainedCodexAccountPin(config);
|
|
1510
|
+
|
|
1160
1511
|
const entry = threadId ? getThreadAffinity(threadId, quotaScope) : undefined;
|
|
1161
1512
|
if (threadId && entry) {
|
|
1162
1513
|
if (isThreadAffinityExpired(entry, now)) {
|
|
@@ -1165,7 +1516,7 @@ export function resolveCodexAccountForThreadDetailed(
|
|
|
1165
1516
|
}
|
|
1166
1517
|
if (
|
|
1167
1518
|
isThreadAffinityGenerationLive(entry)
|
|
1168
|
-
&& isCodexAccountSelectable(config, entry.accountId, now, quotaScope)
|
|
1519
|
+
&& isCodexAccountSelectable(config, entry.accountId, now, quotaScope, selectionOptions)
|
|
1169
1520
|
// Affined threads must leave a failing account once the streak trips failover
|
|
1170
1521
|
// (soft-avoid covers the first-hit case; this catches post-avoid residual streaks).
|
|
1171
1522
|
&& !shouldFailover(config, entry.accountId, now)
|
|
@@ -1192,7 +1543,7 @@ export function resolveCodexAccountForThreadDetailed(
|
|
|
1192
1543
|
if (overThreshold || now - entry.lastReevalAt >= CODEX_THREAD_AFFINITY_REEVAL_INTERVAL_MS) {
|
|
1193
1544
|
entry.lastReevalAt = now;
|
|
1194
1545
|
if (overThreshold) {
|
|
1195
|
-
const best = pickLowerUsageAccount(config, entry.accountId, usage, now, quotaScope);
|
|
1546
|
+
const best = pickLowerUsageAccount(config, entry.accountId, usage, now, quotaScope, selectionOptions);
|
|
1196
1547
|
if (best !== entry.accountId) {
|
|
1197
1548
|
if (!isIndependentCodexQuotaScope(quotaScope)) setActiveCodexAccount(config, best);
|
|
1198
1549
|
bindThreadAffinity(threadId, best, now, quotaScope); // rebinds + resets clocks
|
|
@@ -1206,35 +1557,53 @@ export function resolveCodexAccountForThreadDetailed(
|
|
|
1206
1557
|
deleteThreadAffinity(threadId, quotaScope);
|
|
1207
1558
|
}
|
|
1208
1559
|
|
|
1209
|
-
const strategyPick = pickUnboundStrategyAccount(config, threadId, now, true, quotaScope);
|
|
1560
|
+
const strategyPick = pickUnboundStrategyAccount(config, threadId, now, true, quotaScope, selectionOptions);
|
|
1210
1561
|
if (strategyPick) return { status: "selected", accountId: strategyPick };
|
|
1211
1562
|
|
|
1212
1563
|
let active = getEffectiveActiveCodexAccountId(config);
|
|
1213
1564
|
if (!active) {
|
|
1214
|
-
const selected = pickLowestUsageCodexAccount(config, undefined, now, quotaScope);
|
|
1565
|
+
const selected = pickLowestUsageCodexAccount(config, undefined, now, quotaScope, selectionOptions);
|
|
1215
1566
|
if (!selected) return { status: "none" };
|
|
1216
1567
|
if (!isIndependentCodexQuotaScope(quotaScope)) setActiveCodexAccount(config, selected);
|
|
1217
1568
|
active = selected;
|
|
1218
1569
|
}
|
|
1219
|
-
if (!isCodexAccountSelectable(config, active, now, quotaScope)) {
|
|
1220
|
-
const fallback = pickLowestUsageCodexAccount(config, active, now, quotaScope);
|
|
1570
|
+
if (!isCodexAccountSelectable(config, active, now, quotaScope, selectionOptions)) {
|
|
1571
|
+
const fallback = pickLowestUsageCodexAccount(config, active, now, quotaScope, selectionOptions);
|
|
1221
1572
|
if (fallback) {
|
|
1222
1573
|
if (!isIndependentCodexQuotaScope(quotaScope)) setActiveCodexAccount(config, fallback);
|
|
1223
1574
|
active = fallback;
|
|
1224
|
-
} else if (
|
|
1575
|
+
} else if (
|
|
1576
|
+
hasConfiguredPoolAccount(config, active, selectionOptions)
|
|
1577
|
+
&& !isCodexAccountPaused(config, active)
|
|
1578
|
+
) {
|
|
1225
1579
|
return { status: "selected", accountId: active };
|
|
1226
1580
|
} else {
|
|
1227
1581
|
return { status: "none" };
|
|
1228
1582
|
}
|
|
1229
1583
|
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1584
|
+
// Before applyQuotaAutoSwitch: its sync disk write would otherwise persist a
|
|
1585
|
+
// move inside the drained tier that preemption immediately overrides.
|
|
1586
|
+
const preempted = pickPriorityPreemption(config, active, now, quotaScope, selectionOptions);
|
|
1587
|
+
if (preempted) {
|
|
1588
|
+
// Runtime-only, like every other automatic pick: config.activeCodexAccountId
|
|
1589
|
+
// stays the operator's selection and getEffectiveActiveCodexAccountId is what
|
|
1590
|
+
// surfaces this to the API and dashboard. An independent quota group must not
|
|
1591
|
+
// move the shared cursor at all — its ordering decision is its own.
|
|
1592
|
+
if (!isIndependentCodexQuotaScope(quotaScope)) rememberActiveCodexAccount(config, preempted);
|
|
1593
|
+
active = preempted;
|
|
1594
|
+
}
|
|
1595
|
+
active = applyQuotaAutoSwitch(config, active, now, quotaScope, selectionOptions);
|
|
1596
|
+
active = applyFailureFailover(config, active, now, quotaScope, selectionOptions);
|
|
1597
|
+
if (!isCodexAccountUsable(config, active, selectionOptions)) {
|
|
1598
|
+
return hasConfiguredPoolAccount(config, active, selectionOptions)
|
|
1599
|
+
? { status: "selected", accountId: active }
|
|
1600
|
+
: { status: "none" };
|
|
1234
1601
|
}
|
|
1235
1602
|
if (isCodexAccountPaused(config, active)) return { status: "none" };
|
|
1236
1603
|
if (getCodexQuotaHealthSnapshot(active, quotaScope, now)) {
|
|
1237
|
-
return hasConfiguredPoolAccount(config, active
|
|
1604
|
+
return hasConfiguredPoolAccount(config, active, selectionOptions)
|
|
1605
|
+
? { status: "selected", accountId: active }
|
|
1606
|
+
: { status: "none" };
|
|
1238
1607
|
}
|
|
1239
1608
|
if (threadId) bindThreadAffinity(threadId, active, now, quotaScope);
|
|
1240
1609
|
return { status: "selected", accountId: active };
|
|
@@ -1246,11 +1615,18 @@ export function recordCodexUpstreamOutcome(
|
|
|
1246
1615
|
outcome: CodexUpstreamOutcome,
|
|
1247
1616
|
meta: CodexUpstreamOutcomeMeta = {},
|
|
1248
1617
|
): void {
|
|
1618
|
+
// Host-level evidence is account-independent (#914): a pre-connection
|
|
1619
|
+
// reachability failure is recorded in the (provider, host) ledger even when
|
|
1620
|
+
// there is no account to attribute, or the account's writer generation is
|
|
1621
|
+
// stale — the early returns below must not gate it.
|
|
1622
|
+
if (outcome === "connect_neutral" && meta.hostKey) {
|
|
1623
|
+
recordUpstreamHostFailure(meta.hostKey, { code: meta.lastFailureCode, now: meta.now ?? Date.now() });
|
|
1624
|
+
}
|
|
1249
1625
|
if (!accountId) return;
|
|
1250
1626
|
const writerGeneration = meta.writerGeneration ?? captureConfigGeneration();
|
|
1251
1627
|
if (writerGeneration < lastReconciledGeneration && !liveHealthAccountIds.has(accountId)) return;
|
|
1252
1628
|
const now = meta.now ?? Date.now();
|
|
1253
|
-
const outcomeClass = classifyCodexUpstreamOutcome(outcome);
|
|
1629
|
+
const outcomeClass = classifyCodexUpstreamOutcome(outcome, meta.denial);
|
|
1254
1630
|
const quotaScope = codexQuotaScopeForModel(meta.modelId);
|
|
1255
1631
|
if (outcomeClass === "success") {
|
|
1256
1632
|
const scopedProbe = meta.probeQuotaScope
|
|
@@ -1310,7 +1686,38 @@ export function recordCodexUpstreamOutcome(
|
|
|
1310
1686
|
return;
|
|
1311
1687
|
}
|
|
1312
1688
|
|
|
1689
|
+
if (outcomeClass === "neutral") {
|
|
1690
|
+
// A proven pre-connection reachability failure (DNS / TCP refusal) or a
|
|
1691
|
+
// relayed 3xx is host-level, not account evidence: rotation cannot repair
|
|
1692
|
+
// it and must not happen (#914). Conclude any owned probe lease, record the
|
|
1693
|
+
// failure under the (provider, host) ledger when one is named, and leave
|
|
1694
|
+
// account health, thread affinity, and the active account untouched.
|
|
1695
|
+
const current = upstreamHealth.get(accountId);
|
|
1696
|
+
const scopedProbe = meta.probeQuotaScope
|
|
1697
|
+
? scopedHealthFor(accountId, meta.probeQuotaScope)
|
|
1698
|
+
: undefined;
|
|
1699
|
+
if (scopedProbe && meta.probeQuotaScope && ownsProbeLease(scopedProbe, meta)) {
|
|
1700
|
+
setScopedHealth(accountId, meta.probeQuotaScope, withProbeLeaseReleased(scopedProbe, now));
|
|
1701
|
+
}
|
|
1702
|
+
if (ownsProbeLease(current, meta)) {
|
|
1703
|
+
upstreamHealth.set(accountId, withProbeLeaseReleased(current!, now));
|
|
1704
|
+
}
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1313
1708
|
const lastFailureStatus = typeof outcome === "number" ? outcome : 0;
|
|
1709
|
+
if (outcomeClass === "workspace") {
|
|
1710
|
+
// The credential is valid; this account just cannot reach this workspace (#1789).
|
|
1711
|
+
// Record the failure so routing stops preferring it, but do not mark it for
|
|
1712
|
+
// reauthentication and do not sweep its thread affinities: telling the user to
|
|
1713
|
+
// re-login is wrong advice that cannot fix a workspace grant.
|
|
1714
|
+
upstreamHealth.set(accountId, {
|
|
1715
|
+
consecutiveFailures: (upstreamHealth.get(accountId)?.consecutiveFailures ?? 0) + 1,
|
|
1716
|
+
lastFailureStatus,
|
|
1717
|
+
lastFailureAt: now,
|
|
1718
|
+
});
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1314
1721
|
if (outcomeClass === "credential") {
|
|
1315
1722
|
// 401/403 quarantines the account for reauth. That supersedes quota state
|
|
1316
1723
|
// entirely: a cooldown (and any probe lease) on an unusable account is moot.
|
|
@@ -1354,7 +1761,7 @@ export function recordCodexUpstreamOutcome(
|
|
|
1354
1761
|
// The shared native scope is the existing account-wide native behavior:
|
|
1355
1762
|
// threads must leave it and new requests should prefer an eligible account.
|
|
1356
1763
|
// Spark remains isolated so a same-account Terra/Luna combo fallback can run.
|
|
1357
|
-
if (quotaScope === "shared") {
|
|
1764
|
+
if (quotaScope === "shared" && !meta.fixedAccount) {
|
|
1358
1765
|
clearThreadAccountMapForAccount(accountId);
|
|
1359
1766
|
notePoolRotationFailure(POOL_KEY_CODEX, accountId);
|
|
1360
1767
|
if (getEffectiveActiveCodexAccountId(config) === accountId) {
|
|
@@ -1399,17 +1806,24 @@ export function recordCodexUpstreamOutcome(
|
|
|
1399
1806
|
...(prior?.lastProbeAt !== undefined ? { lastProbeAt: prior.lastProbeAt } : {}),
|
|
1400
1807
|
}),
|
|
1401
1808
|
});
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
//
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1809
|
+
if (!meta.fixedAccount) {
|
|
1810
|
+
clearThreadAccountMapForAccount(accountId);
|
|
1811
|
+
// An independent native quota request may discover an account-wide throttle,
|
|
1812
|
+
// but it still must not advance the shared RR ring or active cursor. The next
|
|
1813
|
+
// shared request observes the cooldown and chooses its own fallback.
|
|
1814
|
+
if (!isIndependentCodexQuotaScope(quotaScope)) {
|
|
1815
|
+
notePoolRotationFailure(POOL_KEY_CODEX, accountId);
|
|
1816
|
+
const effectiveActive = getEffectiveActiveCodexAccountId(config);
|
|
1817
|
+
if (effectiveActive === accountId) {
|
|
1818
|
+
// Same-request 429 retry already picked via excludeAccountId — reuse it so
|
|
1819
|
+
// round-robin does not advance the ring a second time.
|
|
1820
|
+
const reused = meta.promoteAccountId && meta.promoteAccountId !== accountId
|
|
1821
|
+
? meta.promoteAccountId
|
|
1822
|
+
: null;
|
|
1823
|
+
const fallback = reused ?? pickAlternateCodexAccount(config, accountId, now, quotaScope);
|
|
1824
|
+
if (fallback) promoteActiveCodexAccount(config, fallback);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1413
1827
|
}
|
|
1414
1828
|
return;
|
|
1415
1829
|
}
|
|
@@ -1454,15 +1868,21 @@ export function recordCodexUpstreamOutcome(
|
|
|
1454
1868
|
// thread is still pinned to the FAILING account — a late failure from account A
|
|
1455
1869
|
// must not delete a newer healthy binding to account B (race: T→A, A fails,
|
|
1456
1870
|
// T→B, late A failure must not delete B's mapping).
|
|
1457
|
-
if (failoverReady && meta.threadId) {
|
|
1871
|
+
if (!meta.fixedAccount && failoverReady && meta.threadId) {
|
|
1458
1872
|
deleteThreadAffinitiesForAccount(meta.threadId, accountId);
|
|
1459
1873
|
}
|
|
1460
1874
|
// Once the account is past the failover streak, clear every thread still pinned
|
|
1461
1875
|
// to it — matching 429 affinity behavior so "continue" cannot stay on a bad peer.
|
|
1462
|
-
if (shouldFailover(config, accountId, now)) {
|
|
1876
|
+
if (!meta.fixedAccount && shouldFailover(config, accountId, now)) {
|
|
1463
1877
|
clearThreadAccountMapForAccount(accountId);
|
|
1464
1878
|
}
|
|
1465
|
-
if (
|
|
1879
|
+
if (
|
|
1880
|
+
!meta.fixedAccount
|
|
1881
|
+
&& !isIndependentCodexQuotaScope(quotaScope)
|
|
1882
|
+
&& getEffectiveActiveCodexAccountId(config) === accountId
|
|
1883
|
+
) {
|
|
1884
|
+
applyFailureFailover(config, accountId, now, quotaScope);
|
|
1885
|
+
}
|
|
1466
1886
|
}
|
|
1467
1887
|
|
|
1468
1888
|
export function formatCodexProviderForLog(providerName: string, accountId: string | null, config: OcxConfig): string {
|