@iislee/opencodex 2.11.0 → 2.31.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 +114 -19
- package/bin/ocx.mjs +164 -36
- package/bin/package-main.mjs +1 -1
- package/gui/dist/assets/index-BF38heuV.js +104 -0
- package/gui/dist/assets/index-DMiI18Kv.css +1 -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 +19 -10
- package/src/adapters/anthropic-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +376 -52
- package/src/adapters/base.ts +54 -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 +601 -0
- package/src/adapters/cursor/checkpoint-store.ts +303 -0
- package/src/adapters/cursor/cursor-errors.ts +129 -6
- package/src/adapters/cursor/discovery.ts +61 -4
- package/src/adapters/cursor/effort-map.ts +27 -3
- 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 +174 -58
- package/src/adapters/cursor/live-transport.ts +609 -170
- package/src/adapters/cursor/native-exec-common.ts +23 -2
- 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 +0 -1
- package/src/adapters/cursor/native-exec.ts +101 -14
- package/src/adapters/cursor/protobuf-events.ts +829 -11
- package/src/adapters/cursor/protobuf-request.ts +383 -65
- package/src/adapters/cursor/request-builder.ts +237 -31
- package/src/adapters/cursor/tool-definitions.ts +242 -12
- package/src/adapters/cursor/tool-result-normalize.ts +92 -0
- package/src/adapters/cursor/transport.ts +22 -0
- package/src/adapters/cursor/types.ts +28 -1
- package/src/adapters/cursor.ts +133 -7
- package/src/adapters/google-antigravity-replay.ts +558 -34
- package/src/adapters/google-antigravity-wire.ts +43 -10
- 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 +618 -74
- package/src/adapters/identity.ts +39 -6
- 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 +10 -1
- package/src/adapters/kiro.ts +92 -54
- package/src/adapters/mimo-free.ts +17 -0
- package/src/adapters/openai-chat-url.ts +11 -0
- package/src/adapters/openai-chat.ts +1311 -336
- package/src/adapters/openai-responses-url.ts +16 -0
- package/src/adapters/openai-responses.ts +830 -56
- package/src/adapters/registry.ts +175 -0
- package/src/adapters/responses-tool-schema.ts +67 -0
- package/src/adapters/tool-call-id.ts +119 -0
- package/src/adapters/tool-catalog-nudge.ts +104 -21
- package/src/adapters/xai-web-search.ts +185 -0
- package/src/bridge.ts +428 -61
- package/src/chat/inbound.ts +43 -19
- package/src/chat/outbound.ts +82 -26
- package/src/claude/agents-inject.ts +32 -9
- package/src/claude/context-windows.ts +21 -5
- 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 +70 -16
- package/src/cli/account-api.ts +35 -1
- package/src/cli/account-auth.ts +33 -6
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +389 -2
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +30 -6
- package/src/cli/agent.ts +45 -1
- package/src/cli/claude-agent-startup-sync.ts +73 -0
- package/src/cli/claude-desktop.ts +29 -4
- package/src/cli/claude.ts +103 -34
- 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 +593 -0
- package/src/cli/doctor.ts +315 -43
- package/src/cli/ensure-desired-integrations.ts +152 -0
- package/src/cli/export-command.ts +46 -20
- package/src/cli/help.ts +20 -271
- package/src/cli/index.ts +378 -519
- package/src/cli/init.ts +4 -17
- 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 +35 -2
- package/src/cli/models.ts +100 -14
- package/src/cli/observe.ts +92 -3
- package/src/cli/opencode.ts +4 -2
- package/src/cli/provider-runtime.ts +18 -1
- package/src/cli/provider.ts +24 -3
- package/src/cli/ready.ts +301 -0
- package/src/cli/registry.ts +437 -0
- package/src/cli/root.ts +86 -0
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +6 -3
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +10 -3
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +153 -6
- package/src/cli/v2.ts +105 -10
- package/src/cli.ts +1 -1
- package/src/clients/config-export.ts +1358 -21
- package/src/codex/account-label.ts +14 -1
- package/src/codex/account-lifecycle.ts +130 -13
- package/src/codex/account-namespaces.ts +49 -3
- package/src/codex/account-priority.ts +83 -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 +493 -106
- package/src/codex/app-server-restart-service.ts +232 -0
- package/src/codex/auth-api.ts +849 -242
- package/src/codex/auth-collision.ts +5 -3
- package/src/codex/auth-context.ts +345 -32
- package/src/codex/autostart-health.ts +8 -1
- package/src/codex/catalog/account-models.ts +67 -0
- package/src/codex/catalog/aggregation.ts +68 -10
- package/src/codex/catalog/bundled.ts +331 -33
- package/src/codex/catalog/effort.ts +121 -30
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +529 -45
- package/src/codex/catalog/native-models.ts +72 -0
- package/src/codex/catalog/parsing.ts +224 -30
- package/src/codex/catalog/provider-fetch.ts +1460 -134
- package/src/codex/catalog/sync.ts +1449 -186
- 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 +384 -0
- package/src/codex/convergence-types.ts +614 -0
- package/src/codex/convergence.ts +651 -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/features.ts +636 -39
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +407 -0
- package/src/codex/history-lock.ts +242 -0
- package/src/codex/history-migration-guardian.ts +26 -20
- package/src/codex/history-provider.ts +231 -28
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +220 -0
- package/src/codex/inject-coordination.ts +290 -0
- package/src/codex/inject.ts +1073 -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 +80 -0
- package/src/codex/journal.ts +66 -4
- package/src/codex/log-guard/inspect.ts +506 -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 +353 -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 +492 -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 +682 -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 +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/quota-rejection.ts +298 -0
- package/src/codex/quota.ts +175 -13
- package/src/codex/refresh.ts +11 -2
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/routing.ts +505 -94
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +1009 -28
- package/src/codex/subagent-model-fallback.ts +350 -35
- package/src/codex/sync.ts +191 -2
- package/src/codex/transition-state.ts +612 -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 +20 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/request.ts +32 -0
- package/src/combos/types.ts +81 -9
- package/src/config/provider-name.ts +24 -0
- package/src/config.ts +1762 -140
- package/src/generated/compatibility-version.json +3164 -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 +152 -29
- package/src/images/plan.ts +23 -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.ts +111 -0
- package/src/integrations/registry.ts +159 -0
- package/src/integrations/serialize.ts +314 -0
- package/src/integrations/state.ts +361 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer-lock.ts +98 -0
- package/src/integrations/writer.ts +691 -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/app-owned-memory-stores.ts +22 -0
- package/src/lib/bounded-body.ts +153 -9
- 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 +6 -2
- package/src/lib/destination-policy.ts +65 -1
- package/src/lib/errors.ts +44 -2
- 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/pinned-http.ts +145 -26
- package/src/lib/process-control.ts +4 -1
- 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/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 +202 -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 +8 -2
- package/src/oauth/chatgpt.ts +12 -1
- package/src/oauth/command-code.ts +239 -0
- package/src/oauth/cursor.ts +46 -5
- package/src/oauth/google-antigravity.ts +35 -3
- package/src/oauth/health.ts +20 -12
- package/src/oauth/index.ts +398 -66
- package/src/oauth/key-providers.ts +16 -0
- package/src/oauth/kimi.ts +16 -2
- package/src/oauth/kiro.ts +50 -6
- package/src/oauth/local-token-detect.ts +11 -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/store.ts +119 -21
- package/src/oauth/token-guardian.ts +9 -3
- 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/base-url-choices.ts +10 -0
- package/src/providers/codex-capacity.ts +292 -0
- package/src/providers/command-code-efforts.ts +144 -0
- package/src/providers/context-cap.ts +22 -5
- package/src/providers/cursor-pool.ts +72 -0
- package/src/providers/derive.ts +253 -6
- package/src/providers/fastwire.ts +501 -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 +1 -1
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +115 -22
- package/src/providers/model-rename-migration.ts +255 -0
- package/src/providers/model-rename-startup.ts +28 -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 +29 -0
- package/src/providers/quota.ts +1319 -38
- package/src/providers/registry.ts +1429 -111
- package/src/providers/request-pacing.ts +310 -0
- package/src/providers/service-tier.ts +277 -0
- package/src/providers/slug-codec.ts +42 -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 +11 -4
- package/src/reasoning-effort.ts +49 -1
- package/src/responses/compaction.ts +26 -1
- package/src/responses/custom-tool-compat.ts +266 -0
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/namespace-tool-compat.ts +355 -0
- package/src/responses/parser.ts +220 -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 +426 -0
- package/src/responses/schema.ts +7 -1
- package/src/responses/spill-store.ts +75 -10
- package/src/responses/state.ts +565 -27
- package/src/responses/thought-signature-replay.ts +347 -0
- package/src/responses/tool-search-compat.ts +301 -0
- package/src/responses/truncated-stop-reason.ts +60 -0
- package/src/router.ts +366 -30
- package/src/routing/analytics.ts +378 -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 +145 -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 +267 -46
- package/src/server/background-lifecycle.ts +182 -0
- package/src/server/chat-completions.ts +130 -56
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +426 -0
- package/src/server/claude-messages.ts +159 -43
- 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 +865 -181
- package/src/server/lifecycle.ts +284 -13
- 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 +398 -103
- package/src/server/management/api-key-usage.ts +31 -5
- package/src/server/management/body.ts +6 -0
- package/src/server/management/combo-routes.ts +62 -24
- package/src/server/management/config-routes.ts +464 -51
- package/src/server/management/context.ts +80 -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 +101 -32
- package/src/server/management/model-routes.ts +189 -131
- package/src/server/management/model-rows.ts +163 -0
- package/src/server/management/native-integration-routes.ts +769 -0
- package/src/server/management/oauth-account-routes.ts +80 -4
- package/src/server/management/provider-capability-config.ts +48 -0
- package/src/server/management/provider-routes.ts +764 -157
- package/src/server/management/request-history-routes.ts +191 -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 +288 -32
- 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 +115 -14
- package/src/server/management-auth.ts +220 -5
- package/src/server/passive-route-linker.ts +66 -0
- package/src/server/ports.ts +41 -1
- package/src/server/proxy-liveness.ts +132 -5
- package/src/server/readiness.ts +99 -0
- package/src/server/relay-eager.ts +82 -42
- package/src/server/relay.ts +236 -76
- package/src/server/request-decompress.ts +113 -6
- package/src/server/request-log.ts +235 -22
- 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/collaboration.ts +204 -35
- package/src/server/responses/compact.ts +442 -55
- package/src/server/responses/core.ts +2872 -331
- package/src/server/responses/empty-completion-guard.ts +276 -0
- package/src/server/responses/encrypted-payload.ts +62 -39
- package/src/server/responses/fetch-helpers.ts +79 -4
- package/src/server/responses/input-admission.ts +185 -0
- package/src/server/responses/pacing-overload.ts +13 -0
- package/src/server/responses/policy-fallback.ts +178 -0
- package/src/server/responses/responses-field-backfill.ts +251 -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 +308 -0
- package/src/server/responses-custom-tool-repair.ts +282 -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 +342 -0
- package/src/server/responses-tool-search-repair.ts +267 -0
- package/src/server/responses-undeclared-tool-guard.ts +153 -0
- package/src/server/responses.ts +18 -2
- package/src/server/search.ts +78 -13
- package/src/server/sse-frame-buffer.ts +292 -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 +39 -38
- package/src/service-manager-probe.ts +892 -0
- package/src/service.ts +1111 -90
- package/src/sidecar/auth.ts +92 -0
- package/src/sidecar/candidates.ts +83 -0
- package/src/storage/cleanup.ts +2 -2
- 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 +845 -0
- package/src/types/provider.ts +545 -0
- package/src/types/request.ts +384 -0
- package/src/types/tools.ts +131 -0
- package/src/types/wire.ts +80 -0
- package/src/types.ts +104 -1236
- package/src/update/index.ts +32 -19
- package/src/update/job.ts +442 -67
- 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 +259 -0
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +268 -16
- package/src/usage/log.ts +606 -41
- package/src/usage/summary.ts +177 -9
- 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 +9 -3
- 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 +88 -0
- package/src/web-search/executor.ts +11 -3
- package/src/web-search/gemini-executor.ts +141 -0
- package/src/web-search/index.ts +150 -15
- package/src/web-search/loop.ts +279 -50
- 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,
|
|
@@ -624,11 +813,12 @@ function isCodexAccountSelectable(
|
|
|
624
813
|
accountId: string,
|
|
625
814
|
now: number,
|
|
626
815
|
quotaScope?: CodexQuotaScope,
|
|
816
|
+
selectionOptions?: CodexAccountUsabilityOptions,
|
|
627
817
|
): boolean {
|
|
628
818
|
return !isCodexAccountPaused(config, accountId)
|
|
629
819
|
&& getCodexQuotaHealthSnapshot(accountId, quotaScope, now) === null
|
|
630
820
|
&& !isCodexAccountSoftAvoided(accountId, now)
|
|
631
|
-
&& isCodexAccountUsable(config, accountId);
|
|
821
|
+
&& isCodexAccountUsable(config, accountId, selectionOptions);
|
|
632
822
|
}
|
|
633
823
|
|
|
634
824
|
function threadAffinityScope(quotaScope?: CodexQuotaScope): ThreadAffinityScope {
|
|
@@ -736,7 +926,8 @@ function getEligiblePoolAccounts(
|
|
|
736
926
|
excludeId?: string,
|
|
737
927
|
now = Date.now(),
|
|
738
928
|
quotaScope?: CodexQuotaScope,
|
|
739
|
-
|
|
929
|
+
selectionOptions?: CodexAccountUsabilityOptions,
|
|
930
|
+
): readonly string[] {
|
|
740
931
|
const ids = (config.codexAccounts ?? [])
|
|
741
932
|
.filter(account => isSelectableCodexPoolAccount(account)
|
|
742
933
|
&& account.id !== excludeId
|
|
@@ -744,7 +935,7 @@ function getEligiblePoolAccounts(
|
|
|
744
935
|
&& !isAccountNeedsReauth(account.id))
|
|
745
936
|
.filter(account => getCodexQuotaHealthSnapshot(account.id, quotaScope, now) === null)
|
|
746
937
|
.filter(account => !isCodexAccountSoftAvoided(account.id, now))
|
|
747
|
-
.filter(account => isCodexAccountUsable(config, account.id))
|
|
938
|
+
.filter(account => isCodexAccountUsable(config, account.id, selectionOptions))
|
|
748
939
|
.map(account => account.id);
|
|
749
940
|
// The main Codex account is not stored in config.codexAccounts; include it as a
|
|
750
941
|
// first-class rotation candidate when its read-only token is usable (Option A).
|
|
@@ -754,30 +945,48 @@ function getEligiblePoolAccounts(
|
|
|
754
945
|
&& !isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID)
|
|
755
946
|
&& getCodexQuotaHealthSnapshot(MAIN_CODEX_ACCOUNT_ID, quotaScope, now) === null
|
|
756
947
|
&& !isCodexAccountSoftAvoided(MAIN_CODEX_ACCOUNT_ID, now)
|
|
757
|
-
&& isCodexAccountUsable(config, MAIN_CODEX_ACCOUNT_ID)
|
|
948
|
+
&& isCodexAccountUsable(config, MAIN_CODEX_ACCOUNT_ID, selectionOptions)
|
|
758
949
|
) {
|
|
759
950
|
ids.unshift(MAIN_CODEX_ACCOUNT_ID);
|
|
760
951
|
}
|
|
761
|
-
|
|
952
|
+
// Single choke point for selection order: every strategy, failover, and preview
|
|
953
|
+
// reaches the pool through here, so tiering applies once rather than per picker.
|
|
954
|
+
// Eligibility above is unchanged — this only narrows an already-eligible list.
|
|
955
|
+
return selectPriorityTier(
|
|
956
|
+
ids,
|
|
957
|
+
codexAccountPriorityLookup(config),
|
|
958
|
+
id => hasCodexQuotaHeadroom(config, id),
|
|
959
|
+
pinnedCodexAccountId(config),
|
|
960
|
+
);
|
|
762
961
|
}
|
|
763
962
|
|
|
764
963
|
function listEligibleCodexAccountIds(
|
|
765
964
|
config: OcxConfig,
|
|
766
965
|
now: number,
|
|
767
966
|
quotaScope?: CodexQuotaScope,
|
|
768
|
-
|
|
769
|
-
|
|
967
|
+
selectionOptions?: CodexAccountUsabilityOptions,
|
|
968
|
+
): readonly string[] {
|
|
969
|
+
return getEligiblePoolAccounts(config, undefined, now, quotaScope, selectionOptions);
|
|
770
970
|
}
|
|
771
971
|
|
|
772
972
|
function stickyLimitForConfig(config: OcxConfig): number {
|
|
773
973
|
return normalizeAccountPoolStickyLimit(config.accountPoolStickyLimit);
|
|
774
974
|
}
|
|
775
975
|
|
|
776
|
-
|
|
976
|
+
/**
|
|
977
|
+
* Whether an account still has quota to give under the auto-switch threshold.
|
|
978
|
+
*
|
|
979
|
+
* Fill-first and the priority tier filter share this predicate, and share both of
|
|
980
|
+
* its escape hatches. A disabled threshold means only health, pause, and reauth
|
|
981
|
+
* may drain an account; unknown usage is a guess, so it must neither force
|
|
982
|
+
* fill-first off the active account nor drain a tier that was simply never
|
|
983
|
+
* primed. A genuinely exhausted account 429s into cooldown and leaves
|
|
984
|
+
* eligibility on its own.
|
|
985
|
+
*/
|
|
986
|
+
function hasCodexQuotaHeadroom(config: OcxConfig, accountId: string): boolean {
|
|
777
987
|
const threshold = config.autoSwitchThreshold ?? 80;
|
|
778
988
|
if (threshold <= 0) return true;
|
|
779
989
|
const usage = computeCodexUsageScore(getAccountQuota(accountId), getPoolAccountPlan(config, accountId));
|
|
780
|
-
// Unknown usage must not force fill-first to abandon the active account.
|
|
781
990
|
if (isUnknownUsage(usage)) return true;
|
|
782
991
|
return usage < threshold;
|
|
783
992
|
}
|
|
@@ -790,37 +999,39 @@ function pickFillFirstCodexAccount(
|
|
|
790
999
|
config: OcxConfig,
|
|
791
1000
|
now: number,
|
|
792
1001
|
quotaScope?: CodexQuotaScope,
|
|
1002
|
+
selectionOptions?: CodexAccountUsabilityOptions,
|
|
793
1003
|
): string | null {
|
|
794
|
-
const eligible = listEligibleCodexAccountIds(config, now, quotaScope);
|
|
1004
|
+
const eligible = listEligibleCodexAccountIds(config, now, quotaScope, selectionOptions);
|
|
795
1005
|
if (eligible.length === 0) return null;
|
|
796
1006
|
|
|
797
1007
|
const active = getEffectiveActiveCodexAccountId(config);
|
|
798
|
-
if (active && eligible.includes(active) &&
|
|
1008
|
+
if (active && eligible.includes(active) && hasCodexQuotaHeadroom(config, active)) {
|
|
799
1009
|
return active;
|
|
800
1010
|
}
|
|
801
1011
|
|
|
802
|
-
return pickNextFillFirstCodexAccount(config, active ?? null, eligible, now);
|
|
1012
|
+
return pickNextFillFirstCodexAccount(config, active ?? null, eligible, now, selectionOptions);
|
|
803
1013
|
}
|
|
804
1014
|
|
|
805
1015
|
/** Next eligible account in stable order after `afterId` (wrapping). */
|
|
806
1016
|
function pickNextFillFirstCodexAccount(
|
|
807
1017
|
config: OcxConfig,
|
|
808
1018
|
afterId: string | null,
|
|
809
|
-
eligible = listEligibleCodexAccountIds(config, Date.now()),
|
|
1019
|
+
eligible: readonly string[] = listEligibleCodexAccountIds(config, Date.now()),
|
|
810
1020
|
_now = Date.now(),
|
|
1021
|
+
selectionOptions?: CodexAccountUsabilityOptions,
|
|
811
1022
|
): string | null {
|
|
812
1023
|
if (eligible.length === 0) return null;
|
|
813
1024
|
const ordered = [...eligible].sort((a, b) => a.localeCompare(b));
|
|
814
1025
|
if (!afterId) {
|
|
815
1026
|
// Prefer an under-threshold account when starting with no active cursor.
|
|
816
1027
|
for (const id of ordered) {
|
|
817
|
-
if (
|
|
1028
|
+
if (hasCodexQuotaHeadroom(config, id)) return id;
|
|
818
1029
|
}
|
|
819
1030
|
return ordered[0] ?? null;
|
|
820
1031
|
}
|
|
821
1032
|
|
|
822
1033
|
const allConfigured = [
|
|
823
|
-
...(isCodexAccountUsable(config, MAIN_CODEX_ACCOUNT_ID) || afterId === MAIN_CODEX_ACCOUNT_ID
|
|
1034
|
+
...(isCodexAccountUsable(config, MAIN_CODEX_ACCOUNT_ID, selectionOptions) || afterId === MAIN_CODEX_ACCOUNT_ID
|
|
824
1035
|
? [MAIN_CODEX_ACCOUNT_ID]
|
|
825
1036
|
: []),
|
|
826
1037
|
...(config.codexAccounts ?? []).filter(account => !account.isMain).map(account => account.id),
|
|
@@ -829,7 +1040,7 @@ function pickNextFillFirstCodexAccount(
|
|
|
829
1040
|
const startIdx = stableAll.indexOf(afterId);
|
|
830
1041
|
if (startIdx < 0) {
|
|
831
1042
|
for (const id of ordered) {
|
|
832
|
-
if (
|
|
1043
|
+
if (hasCodexQuotaHeadroom(config, id)) return id;
|
|
833
1044
|
}
|
|
834
1045
|
return ordered[0] ?? null;
|
|
835
1046
|
}
|
|
@@ -840,7 +1051,7 @@ function pickNextFillFirstCodexAccount(
|
|
|
840
1051
|
const candidate = stableAll[(startIdx + step) % stableAll.length]!;
|
|
841
1052
|
if (!eligible.includes(candidate)) continue;
|
|
842
1053
|
if (!fallback) fallback = candidate;
|
|
843
|
-
if (
|
|
1054
|
+
if (hasCodexQuotaHeadroom(config, candidate)) return candidate;
|
|
844
1055
|
}
|
|
845
1056
|
return fallback ?? ordered[0] ?? null;
|
|
846
1057
|
}
|
|
@@ -866,6 +1077,7 @@ function pickUnboundStrategyAccount(
|
|
|
866
1077
|
now: number,
|
|
867
1078
|
commit: boolean,
|
|
868
1079
|
quotaScope?: CodexQuotaScope,
|
|
1080
|
+
selectionOptions?: CodexAccountUsabilityOptions,
|
|
869
1081
|
): string | null {
|
|
870
1082
|
const strategy = normalizeAccountPoolStrategy(config.accountPoolStrategy);
|
|
871
1083
|
if (strategy === "quota") return null;
|
|
@@ -873,7 +1085,7 @@ function pickUnboundStrategyAccount(
|
|
|
873
1085
|
|
|
874
1086
|
let picked: string | null = null;
|
|
875
1087
|
if (strategy === "round-robin") {
|
|
876
|
-
const eligible = listEligibleCodexAccountIds(config, now, quotaScope);
|
|
1088
|
+
const eligible = listEligibleCodexAccountIds(config, now, quotaScope, selectionOptions);
|
|
877
1089
|
const limit = stickyLimitForConfig(config);
|
|
878
1090
|
if (!commit) {
|
|
879
1091
|
return peekRoundRobinAccount(poolKey, eligible, limit);
|
|
@@ -887,7 +1099,7 @@ function pickUnboundStrategyAccount(
|
|
|
887
1099
|
}
|
|
888
1100
|
|
|
889
1101
|
if (strategy === "fill-first") {
|
|
890
|
-
picked = pickFillFirstCodexAccount(config, now, quotaScope);
|
|
1102
|
+
picked = pickFillFirstCodexAccount(config, now, quotaScope, selectionOptions);
|
|
891
1103
|
if (!picked) return null;
|
|
892
1104
|
if (commit) {
|
|
893
1105
|
if (!isIndependentCodexQuotaScope(quotaScope)) rememberActiveCodexAccount(config, picked);
|
|
@@ -911,10 +1123,11 @@ function pickLowerUsageAccount(
|
|
|
911
1123
|
activeUsage: number,
|
|
912
1124
|
now: number,
|
|
913
1125
|
quotaScope?: CodexQuotaScope,
|
|
1126
|
+
selectionOptions?: CodexAccountUsabilityOptions,
|
|
914
1127
|
): string {
|
|
915
1128
|
let best = active;
|
|
916
1129
|
let bestUsage = activeUsage;
|
|
917
|
-
for (const id of getEligiblePoolAccounts(config, active, now, quotaScope)) {
|
|
1130
|
+
for (const id of getEligiblePoolAccounts(config, active, now, quotaScope, selectionOptions)) {
|
|
918
1131
|
const usage = computeCodexUsageScore(getAccountQuota(id), getPoolAccountPlan(config, id));
|
|
919
1132
|
if (usage < bestUsage) {
|
|
920
1133
|
best = id;
|
|
@@ -924,15 +1137,11 @@ function pickLowerUsageAccount(
|
|
|
924
1137
|
return best;
|
|
925
1138
|
}
|
|
926
1139
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
excludeId?: string,
|
|
930
|
-
now = Date.now(),
|
|
931
|
-
quotaScope?: CodexQuotaScope,
|
|
932
|
-
): string | null {
|
|
1140
|
+
/** Coolest account in an already-selected candidate list; first index wins ties. */
|
|
1141
|
+
function pickLowestUsageAmong(config: OcxConfig, ids: readonly string[]): string | null {
|
|
933
1142
|
let best: string | null = null;
|
|
934
1143
|
let bestUsage = Number.POSITIVE_INFINITY;
|
|
935
|
-
for (const id of
|
|
1144
|
+
for (const id of ids) {
|
|
936
1145
|
const usage = computeCodexUsageScore(getAccountQuota(id), getPoolAccountPlan(config, id));
|
|
937
1146
|
if (usage < bestUsage) {
|
|
938
1147
|
best = id;
|
|
@@ -942,6 +1151,16 @@ export function pickLowestUsageCodexAccount(
|
|
|
942
1151
|
return best;
|
|
943
1152
|
}
|
|
944
1153
|
|
|
1154
|
+
export function pickLowestUsageCodexAccount(
|
|
1155
|
+
config: OcxConfig,
|
|
1156
|
+
excludeId?: string,
|
|
1157
|
+
now = Date.now(),
|
|
1158
|
+
quotaScope?: CodexQuotaScope,
|
|
1159
|
+
selectionOptions?: CodexAccountUsabilityOptions,
|
|
1160
|
+
): string | null {
|
|
1161
|
+
return pickLowestUsageAmong(config, getEligiblePoolAccounts(config, excludeId, now, quotaScope, selectionOptions));
|
|
1162
|
+
}
|
|
1163
|
+
|
|
945
1164
|
/**
|
|
946
1165
|
* Strategy-aware alternate after a cooled/excluded account (same-request 429 retry
|
|
947
1166
|
* and active promotion). Quota keeps lowest-usage; fill-first advances stable order;
|
|
@@ -952,17 +1171,22 @@ export function pickAlternateCodexAccount(
|
|
|
952
1171
|
excludeId: string,
|
|
953
1172
|
now = Date.now(),
|
|
954
1173
|
quotaScope?: CodexQuotaScope,
|
|
1174
|
+
selectionOptions?: CodexAccountUsabilityOptions,
|
|
955
1175
|
): string | null {
|
|
956
1176
|
const strategy = normalizeAccountPoolStrategy(config.accountPoolStrategy);
|
|
1177
|
+
// The exclusion is passed into eligibility rather than post-filtered off its
|
|
1178
|
+
// result: when the excluded account is the only healthy member of the top
|
|
1179
|
+
// tier, the tier walk must be free to descend instead of selecting that tier
|
|
1180
|
+
// and then handing back an empty list.
|
|
957
1181
|
if (strategy === "round-robin") {
|
|
958
|
-
const eligible =
|
|
1182
|
+
const eligible = getEligiblePoolAccounts(config, excludeId, now, quotaScope, selectionOptions);
|
|
959
1183
|
return pickRoundRobinAccount(codexPoolKeyForScope(quotaScope), eligible, stickyLimitForConfig(config));
|
|
960
1184
|
}
|
|
961
1185
|
if (strategy === "fill-first") {
|
|
962
|
-
const eligible =
|
|
963
|
-
return pickNextFillFirstCodexAccount(config, excludeId, eligible, now);
|
|
1186
|
+
const eligible = getEligiblePoolAccounts(config, excludeId, now, quotaScope, selectionOptions);
|
|
1187
|
+
return pickNextFillFirstCodexAccount(config, excludeId, eligible, now, selectionOptions);
|
|
964
1188
|
}
|
|
965
|
-
return pickLowestUsageCodexAccount(config, excludeId, now, quotaScope);
|
|
1189
|
+
return pickLowestUsageCodexAccount(config, excludeId, now, quotaScope, selectionOptions);
|
|
966
1190
|
}
|
|
967
1191
|
|
|
968
1192
|
/** Effective active: automatic runtime cursor, else operator/persisted selection. */
|
|
@@ -970,6 +1194,17 @@ export function getEffectiveActiveCodexAccountId(config: OcxConfig): string | un
|
|
|
970
1194
|
return runtimeActiveCodexAccountId ?? config.activeCodexAccountId;
|
|
971
1195
|
}
|
|
972
1196
|
|
|
1197
|
+
/**
|
|
1198
|
+
* Whether the account routing is currently on is there because an operator asked
|
|
1199
|
+
* for it, rather than because a strategy landed on it. Surfaces read this instead
|
|
1200
|
+
* of comparing the stored pin themselves, which would report a pin that a later
|
|
1201
|
+
* automatic pick has already moved past.
|
|
1202
|
+
*/
|
|
1203
|
+
export function isEffectiveCodexAccountPinned(config: OcxConfig): boolean {
|
|
1204
|
+
const pinned = pinnedCodexAccountId(config);
|
|
1205
|
+
return pinned !== undefined && pinned === getEffectiveActiveCodexAccountId(config);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
973
1208
|
/**
|
|
974
1209
|
* Automatic strategy / failover cursor only — never mutates `config.activeCodexAccountId`
|
|
975
1210
|
* so an unrelated `saveConfig` cannot persist transient rotation as operator selection.
|
|
@@ -978,10 +1213,22 @@ function rememberActiveCodexAccount(_config: OcxConfig, accountId: string): void
|
|
|
978
1213
|
runtimeActiveCodexAccountId = accountId;
|
|
979
1214
|
}
|
|
980
1215
|
|
|
1216
|
+
/**
|
|
1217
|
+
* End the manual pin when routing moves to a different account. Returns whether
|
|
1218
|
+
* the pin changed so the caller can fold it into a write it was already making.
|
|
1219
|
+
*/
|
|
1220
|
+
function releaseCodexAccountPinFor(config: OcxConfig, accountId: string): boolean {
|
|
1221
|
+
const pinned = pinnedCodexAccountId(config);
|
|
1222
|
+
if (pinned === undefined || pinned === accountId) return false;
|
|
1223
|
+
clearCodexAccountPin(config);
|
|
1224
|
+
return true;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
981
1227
|
/** Persist operator (or quota-strategy) active selection to config + disk. */
|
|
982
1228
|
function setActiveCodexAccount(config: OcxConfig, accountId: string): void {
|
|
983
1229
|
runtimeActiveCodexAccountId = undefined;
|
|
984
|
-
|
|
1230
|
+
const releasedPin = releaseCodexAccountPinFor(config, accountId);
|
|
1231
|
+
if (config.activeCodexAccountId === accountId && !releasedPin) return;
|
|
985
1232
|
config.activeCodexAccountId = accountId;
|
|
986
1233
|
saveConfigPreservingClaudeCode(config);
|
|
987
1234
|
}
|
|
@@ -992,6 +1239,10 @@ function promoteActiveCodexAccount(config: OcxConfig, accountId: string): void {
|
|
|
992
1239
|
setActiveCodexAccount(config, accountId);
|
|
993
1240
|
return;
|
|
994
1241
|
}
|
|
1242
|
+
// Runtime-only, like the cursor itself: a caller that persists (pause, delete)
|
|
1243
|
+
// saves this release with its own write; a transient failover does not, so the
|
|
1244
|
+
// pin survives a restart that also clears the failure history behind it.
|
|
1245
|
+
releaseCodexAccountPinFor(config, accountId);
|
|
995
1246
|
rememberActiveCodexAccount(config, accountId);
|
|
996
1247
|
}
|
|
997
1248
|
|
|
@@ -1010,6 +1261,10 @@ export function reconcileCodexActiveAfterExclusion(
|
|
|
1010
1261
|
if (config.activeCodexAccountId === excludedAccountId) {
|
|
1011
1262
|
config.activeCodexAccountId = undefined;
|
|
1012
1263
|
}
|
|
1264
|
+
// Excluding an account revokes any manual pin on it even when it was not the
|
|
1265
|
+
// effective active — otherwise a paused account keeps acting as a tier ceiling,
|
|
1266
|
+
// suppressing every higher-ordered account while being unusable itself.
|
|
1267
|
+
clearCodexAccountPin(config, excludedAccountId);
|
|
1013
1268
|
if (!wasEffective) return getEffectiveActiveCodexAccountId(config) ?? null;
|
|
1014
1269
|
|
|
1015
1270
|
runtimeActiveCodexAccountId = undefined;
|
|
@@ -1022,11 +1277,60 @@ function isUnknownUsage(usage: number): boolean {
|
|
|
1022
1277
|
return usage >= CODEX_UNKNOWN_USAGE_SCORE;
|
|
1023
1278
|
}
|
|
1024
1279
|
|
|
1280
|
+
/**
|
|
1281
|
+
* Move an unbound request back up when a higher tier regains headroom — the
|
|
1282
|
+
* weekly-reset case. Returns null when nothing should change.
|
|
1283
|
+
*
|
|
1284
|
+
* Downward moves are deliberately left to {@link applyQuotaAutoSwitch}: this only
|
|
1285
|
+
* fires when the tier filter has already excluded `active`, and only toward a
|
|
1286
|
+
* tier that strictly outranks it. Threads bound by affinity never reach here.
|
|
1287
|
+
*/
|
|
1288
|
+
function pickPriorityPreemption(
|
|
1289
|
+
config: OcxConfig,
|
|
1290
|
+
active: string,
|
|
1291
|
+
now: number,
|
|
1292
|
+
quotaScope?: CodexQuotaScope,
|
|
1293
|
+
selectionOptions?: CodexAccountUsabilityOptions,
|
|
1294
|
+
): string | null {
|
|
1295
|
+
const eligible = getEligiblePoolAccounts(config, undefined, now, quotaScope, selectionOptions);
|
|
1296
|
+
if (eligible.length === 0 || eligible.includes(active)) return null;
|
|
1297
|
+
const pinned = pinnedCodexAccountId(config);
|
|
1298
|
+
// A live pin already lowered the tier ceiling; never preempt past an explicit
|
|
1299
|
+
// operator choice. Same liveness test the tier filter applies, so preview and
|
|
1300
|
+
// resolve agree even before the pin is garbage-collected.
|
|
1301
|
+
if (pinned !== undefined && eligible.includes(pinned) && hasCodexQuotaHeadroom(config, pinned)) return null;
|
|
1302
|
+
const priorityOf = codexAccountPriorityLookup(config);
|
|
1303
|
+
if (priorityOf(eligible[0]!) <= priorityOf(active)) return null;
|
|
1304
|
+
// Members without headroom are in the tier only because a sibling has some;
|
|
1305
|
+
// picking one would hand the request straight back to a drained account.
|
|
1306
|
+
return pickLowestUsageAmong(config, eligible.filter(id => hasCodexQuotaHeadroom(config, id)));
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
/**
|
|
1310
|
+
* Release a pin whose account is durably drained. "Use this account now" ends
|
|
1311
|
+
* when the account crosses the auto-switch threshold or stops being selectable
|
|
1312
|
+
* at all — never on a transient cooldown or soft-avoid, which it recovers from
|
|
1313
|
+
* on its own. Clearing the pin also removes the condition, so this writes at
|
|
1314
|
+
* most once per pin.
|
|
1315
|
+
*/
|
|
1316
|
+
function releaseDrainedCodexAccountPin(config: OcxConfig): void {
|
|
1317
|
+
const pinned = pinnedCodexAccountId(config);
|
|
1318
|
+
if (pinned === undefined) return;
|
|
1319
|
+
const drained = !isCodexAccountUsable(config, pinned)
|
|
1320
|
+
|| isAccountNeedsReauth(pinned)
|
|
1321
|
+
|| isCodexAccountPaused(config, pinned)
|
|
1322
|
+
|| !hasCodexQuotaHeadroom(config, pinned);
|
|
1323
|
+
if (!drained) return;
|
|
1324
|
+
clearCodexAccountPin(config);
|
|
1325
|
+
saveConfigPreservingClaudeCode(config);
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1025
1328
|
function applyQuotaAutoSwitch(
|
|
1026
1329
|
config: OcxConfig,
|
|
1027
1330
|
active: string,
|
|
1028
1331
|
now: number,
|
|
1029
1332
|
quotaScope?: CodexQuotaScope,
|
|
1333
|
+
selectionOptions?: CodexAccountUsabilityOptions,
|
|
1030
1334
|
): string {
|
|
1031
1335
|
const threshold = config.autoSwitchThreshold ?? 80;
|
|
1032
1336
|
if (threshold <= 0) return active;
|
|
@@ -1036,7 +1340,7 @@ function applyQuotaAutoSwitch(
|
|
|
1036
1340
|
// threshold. Wait for quota priming instead of rotating among guesses.
|
|
1037
1341
|
if (isUnknownUsage(activeUsage)) return active;
|
|
1038
1342
|
if (activeUsage < threshold) return active;
|
|
1039
|
-
const best = pickLowerUsageAccount(config, active, activeUsage, now, quotaScope);
|
|
1343
|
+
const best = pickLowerUsageAccount(config, active, activeUsage, now, quotaScope, selectionOptions);
|
|
1040
1344
|
if (best !== active) {
|
|
1041
1345
|
if (!isIndependentCodexQuotaScope(quotaScope)) setActiveCodexAccount(config, best);
|
|
1042
1346
|
return best;
|
|
@@ -1053,11 +1357,23 @@ function shouldFailover(config: OcxConfig, accountId: string, now: number): bool
|
|
|
1053
1357
|
return !!health && health.consecutiveFailures >= threshold;
|
|
1054
1358
|
}
|
|
1055
1359
|
|
|
1056
|
-
function applyFailureFailover(
|
|
1360
|
+
function applyFailureFailover(
|
|
1361
|
+
config: OcxConfig,
|
|
1362
|
+
active: string,
|
|
1363
|
+
now: number,
|
|
1364
|
+
quotaScope?: CodexQuotaScope,
|
|
1365
|
+
selectionOptions?: CodexAccountUsabilityOptions,
|
|
1366
|
+
): string {
|
|
1057
1367
|
if (!shouldFailover(config, active, now)) return active;
|
|
1058
|
-
const best = pickAlternateCodexAccount(config, active, now);
|
|
1368
|
+
const best = pickAlternateCodexAccount(config, active, now, quotaScope, selectionOptions);
|
|
1059
1369
|
if (best) {
|
|
1060
|
-
|
|
1370
|
+
// The scope still routes away from the failing account — that is this request's
|
|
1371
|
+
// own decision — but an independent one must not persist a new shared active
|
|
1372
|
+
// account. recordCodexUpstreamOutcome only suppresses the promotion it makes at
|
|
1373
|
+
// the moment of the failure; the streak outlives the soft avoid, so a later
|
|
1374
|
+
// scoped resolve reaches here with the streak still tripped and would otherwise
|
|
1375
|
+
// move the shared cursor after all.
|
|
1376
|
+
if (!isIndependentCodexQuotaScope(quotaScope)) promoteActiveCodexAccount(config, best);
|
|
1061
1377
|
return best;
|
|
1062
1378
|
}
|
|
1063
1379
|
return active;
|
|
@@ -1086,13 +1402,14 @@ export function previewCodexAccountForRequest(
|
|
|
1086
1402
|
config: OcxConfig,
|
|
1087
1403
|
now = Date.now(),
|
|
1088
1404
|
quotaScope?: CodexQuotaScope,
|
|
1405
|
+
selectionOptions?: CodexAccountUsabilityOptions,
|
|
1089
1406
|
): string | null {
|
|
1090
1407
|
const entry = threadId ? getThreadAffinity(threadId, quotaScope) : undefined;
|
|
1091
1408
|
if (threadId && entry) {
|
|
1092
1409
|
if (
|
|
1093
1410
|
!isThreadAffinityExpired(entry, now)
|
|
1094
1411
|
&& isThreadAffinityGenerationLive(entry)
|
|
1095
|
-
&& isCodexAccountSelectable(config, entry.accountId, now, quotaScope)
|
|
1412
|
+
&& isCodexAccountSelectable(config, entry.accountId, now, quotaScope, selectionOptions)
|
|
1096
1413
|
&& !shouldFailover(config, entry.accountId, now)
|
|
1097
1414
|
) {
|
|
1098
1415
|
// Quota strategy only: non-quota strategies keep affinity for ongoing threads
|
|
@@ -1106,7 +1423,14 @@ export function previewCodexAccountForRequest(
|
|
|
1106
1423
|
getPoolAccountPlan(config, entry.accountId),
|
|
1107
1424
|
);
|
|
1108
1425
|
if (!isUnknownUsage(usage) && usage >= threshold) {
|
|
1109
|
-
const best = pickLowerUsageAccount(
|
|
1426
|
+
const best = pickLowerUsageAccount(
|
|
1427
|
+
config,
|
|
1428
|
+
entry.accountId,
|
|
1429
|
+
usage,
|
|
1430
|
+
now,
|
|
1431
|
+
quotaScope,
|
|
1432
|
+
selectionOptions,
|
|
1433
|
+
);
|
|
1110
1434
|
if (best !== entry.accountId) return best;
|
|
1111
1435
|
}
|
|
1112
1436
|
}
|
|
@@ -1116,37 +1440,48 @@ export function previewCodexAccountForRequest(
|
|
|
1116
1440
|
// Stale/unusable affinity is ignored for preview (no map mutation).
|
|
1117
1441
|
}
|
|
1118
1442
|
|
|
1119
|
-
const strategyPick = pickUnboundStrategyAccount(
|
|
1443
|
+
const strategyPick = pickUnboundStrategyAccount(
|
|
1444
|
+
config,
|
|
1445
|
+
threadId,
|
|
1446
|
+
now,
|
|
1447
|
+
false,
|
|
1448
|
+
quotaScope,
|
|
1449
|
+
selectionOptions,
|
|
1450
|
+
);
|
|
1120
1451
|
if (strategyPick) return strategyPick;
|
|
1121
1452
|
|
|
1122
1453
|
let active = getEffectiveActiveCodexAccountId(config) ?? null;
|
|
1123
1454
|
if (!active) {
|
|
1124
|
-
return pickLowestUsageCodexAccount(config, undefined, now, quotaScope);
|
|
1455
|
+
return pickLowestUsageCodexAccount(config, undefined, now, quotaScope, selectionOptions);
|
|
1125
1456
|
}
|
|
1126
|
-
if (!isCodexAccountSelectable(config, active, now, quotaScope)) {
|
|
1127
|
-
const fallback = pickLowestUsageCodexAccount(config, active, now, quotaScope);
|
|
1457
|
+
if (!isCodexAccountSelectable(config, active, now, quotaScope, selectionOptions)) {
|
|
1458
|
+
const fallback = pickLowestUsageCodexAccount(config, active, now, quotaScope, selectionOptions);
|
|
1128
1459
|
if (fallback) active = fallback;
|
|
1129
|
-
else if (
|
|
1460
|
+
else if (
|
|
1461
|
+
hasConfiguredPoolAccount(config, active, selectionOptions)
|
|
1462
|
+
&& !isCodexAccountPaused(config, active)
|
|
1463
|
+
) return active;
|
|
1130
1464
|
else return null;
|
|
1131
1465
|
}
|
|
1466
|
+
active = pickPriorityPreemption(config, active, now, quotaScope, selectionOptions) ?? active;
|
|
1132
1467
|
|
|
1133
1468
|
const threshold = config.autoSwitchThreshold ?? 80;
|
|
1134
1469
|
if (threshold > 0) {
|
|
1135
1470
|
const usage = computeCodexUsageScore(getAccountQuota(active), getPoolAccountPlan(config, active));
|
|
1136
1471
|
if (!isUnknownUsage(usage) && usage >= threshold) {
|
|
1137
|
-
active = pickLowerUsageAccount(config, active, usage, now, quotaScope);
|
|
1472
|
+
active = pickLowerUsageAccount(config, active, usage, now, quotaScope, selectionOptions);
|
|
1138
1473
|
}
|
|
1139
1474
|
}
|
|
1140
1475
|
if (shouldFailover(config, active, now)) {
|
|
1141
|
-
const best = pickLowestUsageCodexAccount(config, active, now, quotaScope);
|
|
1476
|
+
const best = pickLowestUsageCodexAccount(config, active, now, quotaScope, selectionOptions);
|
|
1142
1477
|
if (best) active = best;
|
|
1143
1478
|
}
|
|
1144
|
-
if (!isCodexAccountUsable(config, active)) {
|
|
1145
|
-
return hasConfiguredPoolAccount(config, active) ? active : null;
|
|
1479
|
+
if (!isCodexAccountUsable(config, active, selectionOptions)) {
|
|
1480
|
+
return hasConfiguredPoolAccount(config, active, selectionOptions) ? active : null;
|
|
1146
1481
|
}
|
|
1147
1482
|
if (isCodexAccountPaused(config, active)) return null;
|
|
1148
1483
|
if (getCodexQuotaHealthSnapshot(active, quotaScope, now)) {
|
|
1149
|
-
return hasConfiguredPoolAccount(config, active) ? active : null;
|
|
1484
|
+
return hasConfiguredPoolAccount(config, active, selectionOptions) ? active : null;
|
|
1150
1485
|
}
|
|
1151
1486
|
return active;
|
|
1152
1487
|
}
|
|
@@ -1156,7 +1491,14 @@ export function resolveCodexAccountForThreadDetailed(
|
|
|
1156
1491
|
config: OcxConfig,
|
|
1157
1492
|
now = Date.now(),
|
|
1158
1493
|
quotaScope?: CodexQuotaScope,
|
|
1494
|
+
selectionOptions?: CodexAccountUsabilityOptions,
|
|
1159
1495
|
): CodexThreadResolution {
|
|
1496
|
+
// Retiring a spent manual pin is independent of affinity: an existing thread
|
|
1497
|
+
// keeps its account below, but the operator's tier ceiling must not silently
|
|
1498
|
+
// revive after quota resets. Independent model scopes must never persist a
|
|
1499
|
+
// change to shared routing state.
|
|
1500
|
+
if (!isIndependentCodexQuotaScope(quotaScope)) releaseDrainedCodexAccountPin(config);
|
|
1501
|
+
|
|
1160
1502
|
const entry = threadId ? getThreadAffinity(threadId, quotaScope) : undefined;
|
|
1161
1503
|
if (threadId && entry) {
|
|
1162
1504
|
if (isThreadAffinityExpired(entry, now)) {
|
|
@@ -1165,7 +1507,7 @@ export function resolveCodexAccountForThreadDetailed(
|
|
|
1165
1507
|
}
|
|
1166
1508
|
if (
|
|
1167
1509
|
isThreadAffinityGenerationLive(entry)
|
|
1168
|
-
&& isCodexAccountSelectable(config, entry.accountId, now, quotaScope)
|
|
1510
|
+
&& isCodexAccountSelectable(config, entry.accountId, now, quotaScope, selectionOptions)
|
|
1169
1511
|
// Affined threads must leave a failing account once the streak trips failover
|
|
1170
1512
|
// (soft-avoid covers the first-hit case; this catches post-avoid residual streaks).
|
|
1171
1513
|
&& !shouldFailover(config, entry.accountId, now)
|
|
@@ -1192,7 +1534,7 @@ export function resolveCodexAccountForThreadDetailed(
|
|
|
1192
1534
|
if (overThreshold || now - entry.lastReevalAt >= CODEX_THREAD_AFFINITY_REEVAL_INTERVAL_MS) {
|
|
1193
1535
|
entry.lastReevalAt = now;
|
|
1194
1536
|
if (overThreshold) {
|
|
1195
|
-
const best = pickLowerUsageAccount(config, entry.accountId, usage, now, quotaScope);
|
|
1537
|
+
const best = pickLowerUsageAccount(config, entry.accountId, usage, now, quotaScope, selectionOptions);
|
|
1196
1538
|
if (best !== entry.accountId) {
|
|
1197
1539
|
if (!isIndependentCodexQuotaScope(quotaScope)) setActiveCodexAccount(config, best);
|
|
1198
1540
|
bindThreadAffinity(threadId, best, now, quotaScope); // rebinds + resets clocks
|
|
@@ -1206,35 +1548,53 @@ export function resolveCodexAccountForThreadDetailed(
|
|
|
1206
1548
|
deleteThreadAffinity(threadId, quotaScope);
|
|
1207
1549
|
}
|
|
1208
1550
|
|
|
1209
|
-
const strategyPick = pickUnboundStrategyAccount(config, threadId, now, true, quotaScope);
|
|
1551
|
+
const strategyPick = pickUnboundStrategyAccount(config, threadId, now, true, quotaScope, selectionOptions);
|
|
1210
1552
|
if (strategyPick) return { status: "selected", accountId: strategyPick };
|
|
1211
1553
|
|
|
1212
1554
|
let active = getEffectiveActiveCodexAccountId(config);
|
|
1213
1555
|
if (!active) {
|
|
1214
|
-
const selected = pickLowestUsageCodexAccount(config, undefined, now, quotaScope);
|
|
1556
|
+
const selected = pickLowestUsageCodexAccount(config, undefined, now, quotaScope, selectionOptions);
|
|
1215
1557
|
if (!selected) return { status: "none" };
|
|
1216
1558
|
if (!isIndependentCodexQuotaScope(quotaScope)) setActiveCodexAccount(config, selected);
|
|
1217
1559
|
active = selected;
|
|
1218
1560
|
}
|
|
1219
|
-
if (!isCodexAccountSelectable(config, active, now, quotaScope)) {
|
|
1220
|
-
const fallback = pickLowestUsageCodexAccount(config, active, now, quotaScope);
|
|
1561
|
+
if (!isCodexAccountSelectable(config, active, now, quotaScope, selectionOptions)) {
|
|
1562
|
+
const fallback = pickLowestUsageCodexAccount(config, active, now, quotaScope, selectionOptions);
|
|
1221
1563
|
if (fallback) {
|
|
1222
1564
|
if (!isIndependentCodexQuotaScope(quotaScope)) setActiveCodexAccount(config, fallback);
|
|
1223
1565
|
active = fallback;
|
|
1224
|
-
} else if (
|
|
1566
|
+
} else if (
|
|
1567
|
+
hasConfiguredPoolAccount(config, active, selectionOptions)
|
|
1568
|
+
&& !isCodexAccountPaused(config, active)
|
|
1569
|
+
) {
|
|
1225
1570
|
return { status: "selected", accountId: active };
|
|
1226
1571
|
} else {
|
|
1227
1572
|
return { status: "none" };
|
|
1228
1573
|
}
|
|
1229
1574
|
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1575
|
+
// Before applyQuotaAutoSwitch: its sync disk write would otherwise persist a
|
|
1576
|
+
// move inside the drained tier that preemption immediately overrides.
|
|
1577
|
+
const preempted = pickPriorityPreemption(config, active, now, quotaScope, selectionOptions);
|
|
1578
|
+
if (preempted) {
|
|
1579
|
+
// Runtime-only, like every other automatic pick: config.activeCodexAccountId
|
|
1580
|
+
// stays the operator's selection and getEffectiveActiveCodexAccountId is what
|
|
1581
|
+
// surfaces this to the API and dashboard. An independent quota group must not
|
|
1582
|
+
// move the shared cursor at all — its ordering decision is its own.
|
|
1583
|
+
if (!isIndependentCodexQuotaScope(quotaScope)) rememberActiveCodexAccount(config, preempted);
|
|
1584
|
+
active = preempted;
|
|
1585
|
+
}
|
|
1586
|
+
active = applyQuotaAutoSwitch(config, active, now, quotaScope, selectionOptions);
|
|
1587
|
+
active = applyFailureFailover(config, active, now, quotaScope, selectionOptions);
|
|
1588
|
+
if (!isCodexAccountUsable(config, active, selectionOptions)) {
|
|
1589
|
+
return hasConfiguredPoolAccount(config, active, selectionOptions)
|
|
1590
|
+
? { status: "selected", accountId: active }
|
|
1591
|
+
: { status: "none" };
|
|
1234
1592
|
}
|
|
1235
1593
|
if (isCodexAccountPaused(config, active)) return { status: "none" };
|
|
1236
1594
|
if (getCodexQuotaHealthSnapshot(active, quotaScope, now)) {
|
|
1237
|
-
return hasConfiguredPoolAccount(config, active
|
|
1595
|
+
return hasConfiguredPoolAccount(config, active, selectionOptions)
|
|
1596
|
+
? { status: "selected", accountId: active }
|
|
1597
|
+
: { status: "none" };
|
|
1238
1598
|
}
|
|
1239
1599
|
if (threadId) bindThreadAffinity(threadId, active, now, quotaScope);
|
|
1240
1600
|
return { status: "selected", accountId: active };
|
|
@@ -1246,11 +1606,18 @@ export function recordCodexUpstreamOutcome(
|
|
|
1246
1606
|
outcome: CodexUpstreamOutcome,
|
|
1247
1607
|
meta: CodexUpstreamOutcomeMeta = {},
|
|
1248
1608
|
): void {
|
|
1609
|
+
// Host-level evidence is account-independent (#914): a pre-connection
|
|
1610
|
+
// reachability failure is recorded in the (provider, host) ledger even when
|
|
1611
|
+
// there is no account to attribute, or the account's writer generation is
|
|
1612
|
+
// stale — the early returns below must not gate it.
|
|
1613
|
+
if (outcome === "connect_neutral" && meta.hostKey) {
|
|
1614
|
+
recordUpstreamHostFailure(meta.hostKey, { code: meta.lastFailureCode, now: meta.now ?? Date.now() });
|
|
1615
|
+
}
|
|
1249
1616
|
if (!accountId) return;
|
|
1250
1617
|
const writerGeneration = meta.writerGeneration ?? captureConfigGeneration();
|
|
1251
1618
|
if (writerGeneration < lastReconciledGeneration && !liveHealthAccountIds.has(accountId)) return;
|
|
1252
1619
|
const now = meta.now ?? Date.now();
|
|
1253
|
-
const outcomeClass = classifyCodexUpstreamOutcome(outcome);
|
|
1620
|
+
const outcomeClass = classifyCodexUpstreamOutcome(outcome, meta.denial);
|
|
1254
1621
|
const quotaScope = codexQuotaScopeForModel(meta.modelId);
|
|
1255
1622
|
if (outcomeClass === "success") {
|
|
1256
1623
|
const scopedProbe = meta.probeQuotaScope
|
|
@@ -1310,7 +1677,38 @@ export function recordCodexUpstreamOutcome(
|
|
|
1310
1677
|
return;
|
|
1311
1678
|
}
|
|
1312
1679
|
|
|
1680
|
+
if (outcomeClass === "neutral") {
|
|
1681
|
+
// A proven pre-connection reachability failure (DNS / TCP refusal) or a
|
|
1682
|
+
// relayed 3xx is host-level, not account evidence: rotation cannot repair
|
|
1683
|
+
// it and must not happen (#914). Conclude any owned probe lease, record the
|
|
1684
|
+
// failure under the (provider, host) ledger when one is named, and leave
|
|
1685
|
+
// account health, thread affinity, and the active account untouched.
|
|
1686
|
+
const current = upstreamHealth.get(accountId);
|
|
1687
|
+
const scopedProbe = meta.probeQuotaScope
|
|
1688
|
+
? scopedHealthFor(accountId, meta.probeQuotaScope)
|
|
1689
|
+
: undefined;
|
|
1690
|
+
if (scopedProbe && meta.probeQuotaScope && ownsProbeLease(scopedProbe, meta)) {
|
|
1691
|
+
setScopedHealth(accountId, meta.probeQuotaScope, withProbeLeaseReleased(scopedProbe, now));
|
|
1692
|
+
}
|
|
1693
|
+
if (ownsProbeLease(current, meta)) {
|
|
1694
|
+
upstreamHealth.set(accountId, withProbeLeaseReleased(current!, now));
|
|
1695
|
+
}
|
|
1696
|
+
return;
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1313
1699
|
const lastFailureStatus = typeof outcome === "number" ? outcome : 0;
|
|
1700
|
+
if (outcomeClass === "workspace") {
|
|
1701
|
+
// The credential is valid; this account just cannot reach this workspace (#1789).
|
|
1702
|
+
// Record the failure so routing stops preferring it, but do not mark it for
|
|
1703
|
+
// reauthentication and do not sweep its thread affinities: telling the user to
|
|
1704
|
+
// re-login is wrong advice that cannot fix a workspace grant.
|
|
1705
|
+
upstreamHealth.set(accountId, {
|
|
1706
|
+
consecutiveFailures: (upstreamHealth.get(accountId)?.consecutiveFailures ?? 0) + 1,
|
|
1707
|
+
lastFailureStatus,
|
|
1708
|
+
lastFailureAt: now,
|
|
1709
|
+
});
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1314
1712
|
if (outcomeClass === "credential") {
|
|
1315
1713
|
// 401/403 quarantines the account for reauth. That supersedes quota state
|
|
1316
1714
|
// entirely: a cooldown (and any probe lease) on an unusable account is moot.
|
|
@@ -1354,7 +1752,7 @@ export function recordCodexUpstreamOutcome(
|
|
|
1354
1752
|
// The shared native scope is the existing account-wide native behavior:
|
|
1355
1753
|
// threads must leave it and new requests should prefer an eligible account.
|
|
1356
1754
|
// Spark remains isolated so a same-account Terra/Luna combo fallback can run.
|
|
1357
|
-
if (quotaScope === "shared") {
|
|
1755
|
+
if (quotaScope === "shared" && !meta.fixedAccount) {
|
|
1358
1756
|
clearThreadAccountMapForAccount(accountId);
|
|
1359
1757
|
notePoolRotationFailure(POOL_KEY_CODEX, accountId);
|
|
1360
1758
|
if (getEffectiveActiveCodexAccountId(config) === accountId) {
|
|
@@ -1399,17 +1797,24 @@ export function recordCodexUpstreamOutcome(
|
|
|
1399
1797
|
...(prior?.lastProbeAt !== undefined ? { lastProbeAt: prior.lastProbeAt } : {}),
|
|
1400
1798
|
}),
|
|
1401
1799
|
});
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
//
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1800
|
+
if (!meta.fixedAccount) {
|
|
1801
|
+
clearThreadAccountMapForAccount(accountId);
|
|
1802
|
+
// An independent native quota request may discover an account-wide throttle,
|
|
1803
|
+
// but it still must not advance the shared RR ring or active cursor. The next
|
|
1804
|
+
// shared request observes the cooldown and chooses its own fallback.
|
|
1805
|
+
if (!isIndependentCodexQuotaScope(quotaScope)) {
|
|
1806
|
+
notePoolRotationFailure(POOL_KEY_CODEX, accountId);
|
|
1807
|
+
const effectiveActive = getEffectiveActiveCodexAccountId(config);
|
|
1808
|
+
if (effectiveActive === accountId) {
|
|
1809
|
+
// Same-request 429 retry already picked via excludeAccountId — reuse it so
|
|
1810
|
+
// round-robin does not advance the ring a second time.
|
|
1811
|
+
const reused = meta.promoteAccountId && meta.promoteAccountId !== accountId
|
|
1812
|
+
? meta.promoteAccountId
|
|
1813
|
+
: null;
|
|
1814
|
+
const fallback = reused ?? pickAlternateCodexAccount(config, accountId, now, quotaScope);
|
|
1815
|
+
if (fallback) promoteActiveCodexAccount(config, fallback);
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1413
1818
|
}
|
|
1414
1819
|
return;
|
|
1415
1820
|
}
|
|
@@ -1454,15 +1859,21 @@ export function recordCodexUpstreamOutcome(
|
|
|
1454
1859
|
// thread is still pinned to the FAILING account — a late failure from account A
|
|
1455
1860
|
// must not delete a newer healthy binding to account B (race: T→A, A fails,
|
|
1456
1861
|
// T→B, late A failure must not delete B's mapping).
|
|
1457
|
-
if (failoverReady && meta.threadId) {
|
|
1862
|
+
if (!meta.fixedAccount && failoverReady && meta.threadId) {
|
|
1458
1863
|
deleteThreadAffinitiesForAccount(meta.threadId, accountId);
|
|
1459
1864
|
}
|
|
1460
1865
|
// Once the account is past the failover streak, clear every thread still pinned
|
|
1461
1866
|
// to it — matching 429 affinity behavior so "continue" cannot stay on a bad peer.
|
|
1462
|
-
if (shouldFailover(config, accountId, now)) {
|
|
1867
|
+
if (!meta.fixedAccount && shouldFailover(config, accountId, now)) {
|
|
1463
1868
|
clearThreadAccountMapForAccount(accountId);
|
|
1464
1869
|
}
|
|
1465
|
-
if (
|
|
1870
|
+
if (
|
|
1871
|
+
!meta.fixedAccount
|
|
1872
|
+
&& !isIndependentCodexQuotaScope(quotaScope)
|
|
1873
|
+
&& getEffectiveActiveCodexAccountId(config) === accountId
|
|
1874
|
+
) {
|
|
1875
|
+
applyFailureFailover(config, accountId, now, quotaScope);
|
|
1876
|
+
}
|
|
1466
1877
|
}
|
|
1467
1878
|
|
|
1468
1879
|
export function formatCodexProviderForLog(providerName: string, accountId: string | null, config: OcxConfig): string {
|