@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/auth-api.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
ConfigMutationLockError,
|
|
3
|
+
loadConfig,
|
|
4
|
+
mutatePersistedConfig,
|
|
5
|
+
saveConfigPreservingClaudeCode,
|
|
6
|
+
withConfigMutationLockSync,
|
|
7
|
+
} from "../config";
|
|
8
|
+
import { codexAccountLogLabel, withCodexAccountLogLabel } from "./account-label";
|
|
3
9
|
import {
|
|
4
10
|
getCodexAccountCredential,
|
|
5
11
|
getValidCodexToken,
|
|
@@ -14,27 +20,52 @@ import {
|
|
|
14
20
|
TokenRefreshError,
|
|
15
21
|
} from "./account-store";
|
|
16
22
|
import { deleteCodexAccount, reconcileMainCodexAccountRuntimeState } from "./account-lifecycle";
|
|
23
|
+
import {
|
|
24
|
+
appendDefaultCodexAccountNamespace,
|
|
25
|
+
codexAccountPickerEnabled,
|
|
26
|
+
} from "./account-namespaces";
|
|
27
|
+
import {
|
|
28
|
+
catalogRefreshIsPending,
|
|
29
|
+
normalizeCatalogDisposition,
|
|
30
|
+
} from "./catalog-refresh-status";
|
|
17
31
|
import { isCodexAccountPaused, setCodexAccountPaused } from "./account-pause";
|
|
18
32
|
import {
|
|
33
|
+
clearCodexAccountPin,
|
|
34
|
+
getCodexAccountPriority,
|
|
35
|
+
isCodexAccountPriorityKey,
|
|
36
|
+
pinnedCodexAccountId,
|
|
37
|
+
setCodexAccountPin,
|
|
38
|
+
setCodexAccountPriority,
|
|
39
|
+
} from "./account-priority";
|
|
40
|
+
import {
|
|
41
|
+
claimDueCodexQuotaRecoveryProbes,
|
|
19
42
|
clearCodexAccountCooldown,
|
|
20
43
|
clearThreadAccountMapForAccount,
|
|
21
44
|
getEffectiveActiveCodexAccountId,
|
|
45
|
+
isEffectiveCodexAccountPinned,
|
|
22
46
|
reconcileCodexActiveAfterExclusion,
|
|
23
47
|
resetCodexRoutingForManualSelection,
|
|
48
|
+
settleCodexQuotaRecoveryProbe,
|
|
24
49
|
} from "./routing";
|
|
25
50
|
import {
|
|
51
|
+
DEFAULT_ACCOUNT_PRIORITY,
|
|
52
|
+
MAX_ACCOUNT_PRIORITY,
|
|
53
|
+
MIN_ACCOUNT_PRIORITY,
|
|
26
54
|
normalizeAccountPoolStickyLimit,
|
|
27
55
|
normalizeAccountPoolStrategy,
|
|
28
56
|
parseAccountPoolStickyLimit,
|
|
29
57
|
parseAccountPoolStrategy,
|
|
58
|
+
parseAccountPriority,
|
|
30
59
|
} from "./pool-rotation";
|
|
31
60
|
import { checkAccountIdCollision, getMainChatgptAccountId, readCodexTokens, readCodexTokensResult } from "./auth-collision";
|
|
61
|
+
import { codexPlanValue, isThirtyDayOnlyCodexPlan } from "./plan";
|
|
32
62
|
export { checkAccountIdCollision, getMainChatgptAccountId } from "./auth-collision";
|
|
33
63
|
export { clearAccountNeedsReauth, isAccountNeedsReauth, markAccountNeedsReauth } from "./account-runtime-state";
|
|
34
64
|
import { clearAccountNeedsReauth, isAccountNeedsReauth, markAccountNeedsReauth } from "./account-runtime-state";
|
|
35
65
|
import {
|
|
36
66
|
clearAccountQuota,
|
|
37
67
|
getAccountQuota,
|
|
68
|
+
isCompleteCodexQuotaRecoverySnapshot,
|
|
38
69
|
isCodexQuotaExhausted,
|
|
39
70
|
listAccountQuotas,
|
|
40
71
|
parseUsageQuota,
|
|
@@ -51,24 +82,30 @@ export {
|
|
|
51
82
|
setAccountQuotaFromParsed,
|
|
52
83
|
updateAccountQuota,
|
|
53
84
|
} from "./quota";
|
|
54
|
-
import { extractAccountId
|
|
55
|
-
import { getMainAccountPlan, MAIN_CODEX_ACCOUNT_ID, setMainAccountPlan } from "./main-account";
|
|
56
|
-
import { captureConfigGeneration } from "../lib/state-store-sweeper";
|
|
85
|
+
import { extractAccountId } from "../oauth/chatgpt";
|
|
86
|
+
import { getMainAccountPlan, isMainAccountTokenVerifiablyLive, MAIN_CODEX_ACCOUNT_ID, setMainAccountPlan } from "./main-account";
|
|
87
|
+
import { captureConfigGeneration, registerStateSweepAfterTick } from "../lib/state-store-sweeper";
|
|
57
88
|
import { reconcileLiveStateStores } from "../lib/state-store-registrations";
|
|
58
89
|
import {
|
|
90
|
+
captureMainAccountIdentityGeneration,
|
|
59
91
|
clearMainAccountInfoCache,
|
|
92
|
+
getMainAccountCredentialPresence,
|
|
60
93
|
getMainAccountInfoCache,
|
|
94
|
+
isMainAccountIdentityGenerationLive,
|
|
95
|
+
setMainAccountCredentialPresence,
|
|
61
96
|
setMainAccountInfoCache,
|
|
62
97
|
type MainAccountInfo,
|
|
63
98
|
} from "./main-account-cache";
|
|
64
99
|
export { clearMainAccountInfoCache } from "./main-account-cache";
|
|
65
100
|
import { maskEmail } from "../lib/privacy";
|
|
66
|
-
import {
|
|
101
|
+
import { codexWarmupFailureReason, warmCodexAccount } from "./warmup";
|
|
67
102
|
export { maskEmail } from "../lib/privacy";
|
|
68
|
-
import type { CodexAccount, OcxConfig } from "../types";
|
|
103
|
+
import type { CodexAccount, CodexAccountCredentials, OcxConfig } from "../types";
|
|
104
|
+
import type { CatalogDisposition } from "./convergence-types";
|
|
69
105
|
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
|
|
70
106
|
import { providerCodexAccountMode } from "../providers/registry";
|
|
71
|
-
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
107
|
+
import { BOUNDED_BODY_MAX_BYTES, readBoundedResponseBody } from "../lib/bounded-body";
|
|
108
|
+
import { cancelBodyOnAbort, signalWithTimeout } from "../lib/abort";
|
|
72
109
|
import {
|
|
73
110
|
oauthAccountHealthFields,
|
|
74
111
|
projectCodexAccountHealth,
|
|
@@ -82,7 +119,20 @@ import {
|
|
|
82
119
|
isValidCodexAccountId,
|
|
83
120
|
} from "./account-id";
|
|
84
121
|
import { codexAccountIdNamespaceCollisionError } from "./account-namespace-match";
|
|
85
|
-
import { ResourceAdmissionError } from "../lib/admission";
|
|
122
|
+
import { ResourceAdmissionError, type AdmissionLease } from "../lib/admission";
|
|
123
|
+
import { tryAcquireNativeMainProfileClaim } from "./native-main-admission";
|
|
124
|
+
import { withNativeMainSharedClaim } from "./native-main-claim";
|
|
125
|
+
import { resolveNativeProfileContext } from "./native-profile-store";
|
|
126
|
+
import { NativeProfileError } from "./native-profile-types";
|
|
127
|
+
|
|
128
|
+
function isNativeMainClaimUnavailable(error: unknown): error is NativeProfileError {
|
|
129
|
+
return error instanceof NativeProfileError
|
|
130
|
+
&& (error.code === "NATIVE_MAIN_CLAIM_BUSY" || error.code === "NATIVE_MAIN_CLAIM_UNAVAILABLE");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function withNativeMainCredentialClaim<T>(operation: () => Promise<T>): Promise<T> {
|
|
134
|
+
return withNativeMainSharedClaim(resolveNativeProfileContext(), operation);
|
|
135
|
+
}
|
|
86
136
|
|
|
87
137
|
function jsonResponse(data: unknown, status = 200): Response {
|
|
88
138
|
return new Response(JSON.stringify(data), {
|
|
@@ -91,11 +141,28 @@ function jsonResponse(data: unknown, status = 200): Response {
|
|
|
91
141
|
});
|
|
92
142
|
}
|
|
93
143
|
|
|
94
|
-
|
|
144
|
+
function nativeMainProfileBusyResponse(): Response {
|
|
145
|
+
const response = jsonResponse({ error: "server_busy", code: "server_busy" }, 503);
|
|
146
|
+
response.headers.set("Retry-After", "1");
|
|
147
|
+
return response;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const CODEX_CREDENTIAL_PERSISTENCE_ERROR = "Account was saved, but credential setup did not complete. Reauthenticate or remove the account.";
|
|
151
|
+
const CODEX_CREDENTIAL_PERSISTENCE_CODE = "codex_credential_persistence_failed";
|
|
95
152
|
|
|
96
153
|
const MAX_CODEX_LOGIN_STATE_ROWS = 32;
|
|
97
154
|
const CODEX_LOGIN_TERMINAL_TTL_MS = 300_000;
|
|
98
|
-
interface CodexLoginStateRow {
|
|
155
|
+
interface CodexLoginStateRow {
|
|
156
|
+
status: string;
|
|
157
|
+
startedAt: number;
|
|
158
|
+
accountId?: string;
|
|
159
|
+
email?: string;
|
|
160
|
+
error?: string;
|
|
161
|
+
code?: string;
|
|
162
|
+
needsReauth?: boolean;
|
|
163
|
+
catalogRefreshPending?: boolean;
|
|
164
|
+
doneAt?: number;
|
|
165
|
+
}
|
|
99
166
|
const codexAuthLoginState = new Map<string, CodexLoginStateRow>();
|
|
100
167
|
export class CodexLoginStateBusyError extends ResourceAdmissionError {
|
|
101
168
|
constructor() { super("codex_login_state_rows", MAX_CODEX_LOGIN_STATE_ROWS); this.name = "CodexLoginStateBusyError"; }
|
|
@@ -142,21 +209,59 @@ function codexAccountPersistenceConflict(
|
|
|
142
209
|
: undefined;
|
|
143
210
|
}
|
|
144
211
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
212
|
+
/**
|
|
213
|
+
* The exact label `parseUsageQuota` emits for the Codex Spark window (quota.ts).
|
|
214
|
+
* Matching on the label rather than on "is a custom window" is load-bearing: the same array
|
|
215
|
+
* carries Cursor's First-party models / API usage, Anthropic's Fable / Opus / Sonnet,
|
|
216
|
+
* Antigravity's Gem / Cla, Kimi's subscription credits and a dozen dynamic provider meters.
|
|
217
|
+
*/
|
|
218
|
+
const CODEX_SPARK_WINDOW_LABEL = "GPT-5.3-Codex-Spark Weekly";
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Drop the Spark window unless the operator asked for it (default hidden).
|
|
222
|
+
*
|
|
223
|
+
* Applied at the DTO boundary, never at parse or cache time: custom windows participate in
|
|
224
|
+
* quota-presence checks, snapshot reconciliation and capacity aggregation, so removing Spark
|
|
225
|
+
* upstream of this point would change routing state rather than display.
|
|
226
|
+
*
|
|
227
|
+
* Both GUI surfaces funnel through here — the Codex Auth rows directly, and /api/provider-quotas
|
|
228
|
+
* via listCodexAuthAccountsSnapshot — so one filter covers both. Filtering only one would leave
|
|
229
|
+
* the other still rendering the row the operator switched off.
|
|
230
|
+
*/
|
|
231
|
+
export function withSparkVisibility<T extends Omit<StoredAccountQuota, "updatedAt"> | StoredAccountQuota | null>(
|
|
232
|
+
quota: T,
|
|
233
|
+
): T {
|
|
234
|
+
if (!quota?.customWindows?.length) return quota;
|
|
235
|
+
if (loadConfig().showCodexSparkQuota === true) return quota;
|
|
236
|
+
const kept = quota.customWindows.filter(window => window.label !== CODEX_SPARK_WINDOW_LABEL);
|
|
237
|
+
if (kept.length === quota.customWindows.length) return quota;
|
|
238
|
+
// An empty list is dropped rather than serialized: an absent field and an empty array should
|
|
239
|
+
// not be two different ways of saying "no custom windows" on the wire.
|
|
240
|
+
const next = { ...quota } as Record<string, unknown>;
|
|
241
|
+
if (kept.length > 0) next.customWindows = kept;
|
|
242
|
+
else delete next.customWindows;
|
|
243
|
+
return next as T;
|
|
148
244
|
}
|
|
149
245
|
|
|
246
|
+
|
|
150
247
|
function quotaForPlan<T extends Omit<StoredAccountQuota, "updatedAt"> | StoredAccountQuota | null>(
|
|
151
248
|
quota: T,
|
|
152
|
-
plan:
|
|
249
|
+
plan: unknown,
|
|
153
250
|
): T {
|
|
154
|
-
|
|
251
|
+
const visible = withSparkVisibility(quota);
|
|
252
|
+
if (!visible || !isThirtyDayOnlyCodexPlan(plan)) return visible;
|
|
253
|
+
const quotaWindows = visible;
|
|
155
254
|
return {
|
|
156
|
-
...(
|
|
157
|
-
...(
|
|
158
|
-
|
|
159
|
-
|
|
255
|
+
...(quotaWindows.monthlyPercent !== undefined ? { monthlyPercent: quotaWindows.monthlyPercent } : {}),
|
|
256
|
+
...(quotaWindows.monthlyResetAt !== undefined ? { monthlyResetAt: quotaWindows.monthlyResetAt } : {}),
|
|
257
|
+
// A 30-day plan can still carry a burst window, and it blocks the account on its own.
|
|
258
|
+
// Dropping it here would show a healthy card for an account upstream is refusing (#1791).
|
|
259
|
+
...(quotaWindows.shortPercent !== undefined ? { shortPercent: quotaWindows.shortPercent } : {}),
|
|
260
|
+
...(quotaWindows.shortResetAt !== undefined ? { shortResetAt: quotaWindows.shortResetAt } : {}),
|
|
261
|
+
...(quotaWindows.shortWindowSeconds !== undefined ? { shortWindowSeconds: quotaWindows.shortWindowSeconds } : {}),
|
|
262
|
+
...(quotaWindows.customWindows !== undefined ? { customWindows: quotaWindows.customWindows } : {}),
|
|
263
|
+
...(quotaWindows.resetCredits !== undefined ? { resetCredits: quotaWindows.resetCredits } : {}),
|
|
264
|
+
...("updatedAt" in quotaWindows ? { updatedAt: quotaWindows.updatedAt } : {}),
|
|
160
265
|
} as T;
|
|
161
266
|
}
|
|
162
267
|
|
|
@@ -165,18 +270,21 @@ function poolAccountDto(
|
|
|
165
270
|
quotaResult: PoolQuotaResult,
|
|
166
271
|
hasCredential: boolean,
|
|
167
272
|
paused: boolean,
|
|
273
|
+
priority: number,
|
|
168
274
|
): CodexAuthAccountDto {
|
|
169
|
-
const
|
|
275
|
+
const plan = codexPlanValue(account.plan);
|
|
276
|
+
const quota = quotaForPlan(quotaResult.quota, plan);
|
|
170
277
|
const needsReauth = !hasCredential || quotaResult.needsReauth || isAccountNeedsReauth(account.id);
|
|
171
278
|
const health = projectCodexAccountHealth({ accountId: account.id, needsReauth });
|
|
172
279
|
return {
|
|
173
280
|
id: account.id,
|
|
174
|
-
email: account.email,
|
|
281
|
+
email: maskEmail(account.email) ?? account.email,
|
|
175
282
|
...(account.alias !== undefined ? { alias: account.alias } : {}),
|
|
176
|
-
...(
|
|
177
|
-
|
|
283
|
+
...(plan !== undefined ? { plan } : {}),
|
|
284
|
+
logLabel: codexAccountLogLabel(account),
|
|
178
285
|
isMain: false,
|
|
179
286
|
paused,
|
|
287
|
+
priority,
|
|
180
288
|
quota: quota ? { ...quota } : null,
|
|
181
289
|
needsReauth,
|
|
182
290
|
hasCredential,
|
|
@@ -185,20 +293,52 @@ function poolAccountDto(
|
|
|
185
293
|
};
|
|
186
294
|
}
|
|
187
295
|
|
|
188
|
-
|
|
296
|
+
interface ResetCreditAuth {
|
|
297
|
+
isMain: boolean;
|
|
298
|
+
accessToken: string;
|
|
299
|
+
chatgptAccountId: string;
|
|
300
|
+
nativeMainLease?: AdmissionLease;
|
|
301
|
+
nativeMainSharedClaimHeld?: true;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function withResetCreditAuth<T>(
|
|
189
305
|
runtimeConfig: OcxConfig,
|
|
190
306
|
accountId: string,
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
| { ok: false; response: Response }
|
|
194
|
-
> {
|
|
307
|
+
operation: (auth: ResetCreditAuth) => Promise<T>,
|
|
308
|
+
): Promise<{ ok: true; value: T } | { ok: false; response: Response }> {
|
|
195
309
|
if (accountId === MAIN_CODEX_ACCOUNT_ID) {
|
|
196
310
|
if (hasLegacyMainCodexPoolAccount(runtimeConfig.codexAccounts)) {
|
|
197
311
|
return { ok: false, response: jsonResponse({ error: "Remove the legacy __main__ pool row before using the Desktop account" }, 409) };
|
|
198
312
|
}
|
|
199
|
-
const
|
|
200
|
-
if (!
|
|
201
|
-
|
|
313
|
+
const nativeMainLease = tryAcquireNativeMainProfileClaim();
|
|
314
|
+
if (!nativeMainLease) return { ok: false, response: nativeMainProfileBusyResponse() };
|
|
315
|
+
try {
|
|
316
|
+
try {
|
|
317
|
+
return await withNativeMainCredentialClaim(async () => {
|
|
318
|
+
const tokens = readCodexTokens();
|
|
319
|
+
if (!tokens) {
|
|
320
|
+
return { ok: false, response: jsonResponse({ error: "Main Codex account not logged in" }, 401) };
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
ok: true,
|
|
324
|
+
value: await operation({
|
|
325
|
+
isMain: true,
|
|
326
|
+
accessToken: tokens.access_token,
|
|
327
|
+
chatgptAccountId: tokens.account_id,
|
|
328
|
+
nativeMainLease,
|
|
329
|
+
nativeMainSharedClaimHeld: true,
|
|
330
|
+
}),
|
|
331
|
+
};
|
|
332
|
+
});
|
|
333
|
+
} catch (error) {
|
|
334
|
+
if (isNativeMainClaimUnavailable(error)) {
|
|
335
|
+
return { ok: false, response: nativeMainProfileBusyResponse() };
|
|
336
|
+
}
|
|
337
|
+
throw error;
|
|
338
|
+
}
|
|
339
|
+
} finally {
|
|
340
|
+
nativeMainLease.release();
|
|
341
|
+
}
|
|
202
342
|
}
|
|
203
343
|
if (!isValidCodexAccountId(accountId)) {
|
|
204
344
|
return { ok: false, response: jsonResponse({ error: "Invalid account id format" }, 400) };
|
|
@@ -207,7 +347,14 @@ async function resolveResetCreditAuth(
|
|
|
207
347
|
return { ok: false, response: jsonResponse({ error: "Unknown Codex account" }, 404) };
|
|
208
348
|
}
|
|
209
349
|
const cred = await getValidCodexToken(accountId);
|
|
210
|
-
return {
|
|
350
|
+
return {
|
|
351
|
+
ok: true,
|
|
352
|
+
value: await operation({
|
|
353
|
+
isMain: false,
|
|
354
|
+
accessToken: cred.accessToken,
|
|
355
|
+
chatgptAccountId: cred.chatgptAccountId,
|
|
356
|
+
}),
|
|
357
|
+
};
|
|
211
358
|
}
|
|
212
359
|
|
|
213
360
|
function safeResetCreditsDto(input: unknown): { credits: { granted_at: string; expires_at: string }[]; available_count?: number } {
|
|
@@ -233,8 +380,41 @@ function safeResetCreditConsumeDto(input: unknown): { code: string } {
|
|
|
233
380
|
return { code: typeof obj.code === "string" ? obj.code : "unknown" };
|
|
234
381
|
}
|
|
235
382
|
|
|
236
|
-
|
|
237
|
-
|
|
383
|
+
type ResetCreditJsonRead =
|
|
384
|
+
| { ok: true; value: unknown }
|
|
385
|
+
| { ok: false };
|
|
386
|
+
|
|
387
|
+
function cancelResponseBodyWithoutWaiting(body: ReadableStream<Uint8Array> | null): void {
|
|
388
|
+
if (!body) return;
|
|
389
|
+
try {
|
|
390
|
+
void body.cancel().catch(() => undefined);
|
|
391
|
+
} catch {
|
|
392
|
+
// Some stream implementations throw synchronously from cancel().
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function readResetCreditJson(
|
|
397
|
+
response: Response,
|
|
398
|
+
signal: AbortSignal,
|
|
399
|
+
): Promise<ResetCreditJsonRead> {
|
|
400
|
+
const declaredLength = Number(response.headers.get("content-length"));
|
|
401
|
+
if (Number.isSafeInteger(declaredLength)
|
|
402
|
+
&& declaredLength >= 0
|
|
403
|
+
&& declaredLength > BOUNDED_BODY_MAX_BYTES) {
|
|
404
|
+
cancelResponseBodyWithoutWaiting(response.body);
|
|
405
|
+
return { ok: false };
|
|
406
|
+
}
|
|
407
|
+
try {
|
|
408
|
+
const body = await readBoundedResponseBody(response, {
|
|
409
|
+
signal,
|
|
410
|
+
maxBytes: BOUNDED_BODY_MAX_BYTES,
|
|
411
|
+
fatalUtf8: true,
|
|
412
|
+
});
|
|
413
|
+
if (!body.displaySafe || body.truncated || !body.text.trim()) return { ok: false };
|
|
414
|
+
return { ok: true, value: JSON.parse(body.text) as unknown };
|
|
415
|
+
} catch {
|
|
416
|
+
return { ok: false };
|
|
417
|
+
}
|
|
238
418
|
}
|
|
239
419
|
|
|
240
420
|
function manualImportDisabledResponse(): Response {
|
|
@@ -254,13 +434,10 @@ async function verifyCodexAccountWarmup(
|
|
|
254
434
|
return { ok: true, validatedAt: Date.now() };
|
|
255
435
|
} catch (err) {
|
|
256
436
|
const reason = codexWarmupFailureReason(err);
|
|
257
|
-
const upstream = err instanceof CodexWarmupError ? err.upstreamDetail : undefined;
|
|
258
437
|
return {
|
|
259
438
|
ok: false,
|
|
260
439
|
response: jsonResponse({
|
|
261
|
-
error:
|
|
262
|
-
? `Codex account warmup failed: ${upstream}`
|
|
263
|
-
: "Codex account warmup failed. Reauthenticate the account and try again.",
|
|
440
|
+
error: "Codex account warmup failed. Reauthenticate the account and try again.",
|
|
264
441
|
code: "codex_warmup_failed",
|
|
265
442
|
reason,
|
|
266
443
|
accountId,
|
|
@@ -291,7 +468,7 @@ const POOL_CACHE_TTL = 5 * 60_000;
|
|
|
291
468
|
const POOL_QUOTA_REFRESH_CONCURRENCY = 4;
|
|
292
469
|
|
|
293
470
|
function nonEmptyPlan(value: unknown): string | null {
|
|
294
|
-
return
|
|
471
|
+
return codexPlanValue(value) ?? null;
|
|
295
472
|
}
|
|
296
473
|
|
|
297
474
|
function isRuntimeConfig(config: OcxConfig): boolean {
|
|
@@ -311,6 +488,100 @@ function saveRuntimeConfig(sourceConfig: OcxConfig, nextConfig: OcxConfig): void
|
|
|
311
488
|
Object.assign(sourceConfig, nextConfig);
|
|
312
489
|
}
|
|
313
490
|
|
|
491
|
+
interface StagedNewCodexAccountState {
|
|
492
|
+
credential: CodexAccountCredentials;
|
|
493
|
+
validatedAt: number;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
type PersistNewCodexAccountOutcome =
|
|
497
|
+
| { status: "committed"; pickerVisibilityChanged: boolean }
|
|
498
|
+
| { status: "publication-failed"; pickerVisibilityChanged: boolean };
|
|
499
|
+
|
|
500
|
+
function codexCredentialPersistenceFailure(accountId: string, catalogRefreshPending: boolean) {
|
|
501
|
+
return {
|
|
502
|
+
error: CODEX_CREDENTIAL_PERSISTENCE_ERROR,
|
|
503
|
+
code: CODEX_CREDENTIAL_PERSISTENCE_CODE,
|
|
504
|
+
accountId,
|
|
505
|
+
needsReauth: true as const,
|
|
506
|
+
...(catalogRefreshPending ? { catalogRefreshPending: true as const } : {}),
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/** Persist config before publishing secret or runtime state under the shared mutation coordinator. */
|
|
511
|
+
function persistNewCodexAccount(
|
|
512
|
+
sourceConfig: OcxConfig,
|
|
513
|
+
runtimeConfig: OcxConfig,
|
|
514
|
+
addedAccount: CodexAccount,
|
|
515
|
+
staged: StagedNewCodexAccountState,
|
|
516
|
+
): PersistNewCodexAccountOutcome {
|
|
517
|
+
return withConfigMutationLockSync(() => {
|
|
518
|
+
const previousConfig = { ...runtimeConfig };
|
|
519
|
+
let pickerVisibilityChanged: boolean;
|
|
520
|
+
try {
|
|
521
|
+
const accounts = [...(runtimeConfig.codexAccounts ?? [])];
|
|
522
|
+
const retainedPickerBindingRestored = codexAccountPickerEnabled(runtimeConfig)
|
|
523
|
+
&& Object.values(runtimeConfig.codexAccountNamespaces ?? {}).includes(addedAccount.id);
|
|
524
|
+
accounts.push(addedAccount);
|
|
525
|
+
runtimeConfig.codexAccounts = accounts;
|
|
526
|
+
|
|
527
|
+
// Presence of the explicit flag distinguishes a dashboard-managed map from
|
|
528
|
+
// a hand-authored legacy map. Preserve manual maps exactly.
|
|
529
|
+
const tracksPickerNamespaces = runtimeConfig.codexAccountPickerEnabled !== undefined;
|
|
530
|
+
if (tracksPickerNamespaces && runtimeConfig.codexAccountNamespaces) {
|
|
531
|
+
runtimeConfig.codexAccountNamespaces = { ...runtimeConfig.codexAccountNamespaces };
|
|
532
|
+
}
|
|
533
|
+
const namespaceAdded = tracksPickerNamespaces
|
|
534
|
+
&& appendDefaultCodexAccountNamespace(runtimeConfig, addedAccount);
|
|
535
|
+
pickerVisibilityChanged = namespaceAdded || retainedPickerBindingRestored;
|
|
536
|
+
saveRuntimeConfig(sourceConfig, runtimeConfig);
|
|
537
|
+
} catch (error) {
|
|
538
|
+
for (const key of Object.keys(runtimeConfig) as Array<keyof OcxConfig>) {
|
|
539
|
+
delete runtimeConfig[key];
|
|
540
|
+
}
|
|
541
|
+
Object.assign(runtimeConfig, previousConfig);
|
|
542
|
+
throw error;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
try {
|
|
546
|
+
saveCodexAccountCredential(addedAccount.id, staged.credential);
|
|
547
|
+
markCodexAccountValidated(addedAccount.id, staged.validatedAt);
|
|
548
|
+
clearAccountNeedsReauth(addedAccount.id);
|
|
549
|
+
} catch {
|
|
550
|
+
// Config is already durable. Return the failure outcome through the coordinator so its
|
|
551
|
+
// generation commit is not rolled back while config.json remains changed.
|
|
552
|
+
return { status: "publication-failed" as const, pickerVisibilityChanged };
|
|
553
|
+
}
|
|
554
|
+
return { status: "committed" as const, pickerVisibilityChanged };
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/** Bounded catalog-convergence callback supplied by the management dispatcher. */
|
|
559
|
+
export type CodexAuthCatalogConvergence = () => Promise<CatalogDisposition>;
|
|
560
|
+
|
|
561
|
+
interface AccountNamespaceCatalogRefresh {
|
|
562
|
+
catalogRefreshPending: boolean;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/** Collapse post-persistence convergence into the one public recovery bit. */
|
|
566
|
+
async function convergeAccountNamespaceCatalog(
|
|
567
|
+
config: OcxConfig,
|
|
568
|
+
changed: boolean,
|
|
569
|
+
convergeCodexCatalog?: CodexAuthCatalogConvergence,
|
|
570
|
+
): Promise<AccountNamespaceCatalogRefresh> {
|
|
571
|
+
if (!changed || !codexAccountPickerEnabled(config)) {
|
|
572
|
+
return { catalogRefreshPending: false };
|
|
573
|
+
}
|
|
574
|
+
if (!convergeCodexCatalog) return { catalogRefreshPending: true };
|
|
575
|
+
|
|
576
|
+
try {
|
|
577
|
+
const catalogRefresh = normalizeCatalogDisposition(await convergeCodexCatalog());
|
|
578
|
+
if (!catalogRefresh) return { catalogRefreshPending: true };
|
|
579
|
+
return { catalogRefreshPending: catalogRefreshIsPending(catalogRefresh) };
|
|
580
|
+
} catch {
|
|
581
|
+
return { catalogRefreshPending: true };
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
314
585
|
async function mapWithConcurrency<T, R>(
|
|
315
586
|
items: T[],
|
|
316
587
|
concurrency: number,
|
|
@@ -333,12 +604,31 @@ const MAIN_TERMINAL_AUTH_CODES = new Set([
|
|
|
333
604
|
"invalid_refresh_token",
|
|
334
605
|
]);
|
|
335
606
|
|
|
336
|
-
|
|
337
|
-
|
|
607
|
+
/**
|
|
608
|
+
* A WHAM 401 is not itself proof the local credential died. Upstream edges can
|
|
609
|
+
* transiently reject a still-valid access token (region/anti-abuse/rotation
|
|
610
|
+
* races), and fail-closing on every bare 401 makes a healthy main account flip
|
|
611
|
+
* needs-reauth on the next GUI quota poll. Only treat the response as terminal
|
|
612
|
+
* when the body carries a known terminal code or the local access token is not
|
|
613
|
+
* verifiably live (`accessTokenLive`). Liveness must be strict: a JWT whose
|
|
614
|
+
* `exp` cannot be decoded is NOT live — an undecodable token that vouched for
|
|
615
|
+
* itself would make a real 401 permanently transient.
|
|
616
|
+
*/
|
|
617
|
+
async function isTerminalMainAuthResponse(resp: Response, accessTokenLive: boolean): Promise<boolean> {
|
|
618
|
+
if (resp.status === 401) {
|
|
619
|
+
if (!accessTokenLive) return true;
|
|
620
|
+
const code = await readMainAuthErrorCode(resp);
|
|
621
|
+
return typeof code === "string" && MAIN_TERMINAL_AUTH_CODES.has(code);
|
|
622
|
+
}
|
|
338
623
|
if (resp.status !== 403) return false;
|
|
624
|
+
const code = await readMainAuthErrorCode(resp);
|
|
625
|
+
return typeof code === "string" && MAIN_TERMINAL_AUTH_CODES.has(code);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
async function readMainAuthErrorCode(resp: Response): Promise<unknown> {
|
|
339
629
|
try {
|
|
340
630
|
const body = await readBoundedResponseBody(resp, { totalTimeoutMs: 1_000, inactivityTimeoutMs: 1_000 });
|
|
341
|
-
if (!body.displaySafe) return
|
|
631
|
+
if (!body.displaySafe) return undefined;
|
|
342
632
|
const parsed = JSON.parse(body.text) as {
|
|
343
633
|
detail?: { code?: unknown } | string;
|
|
344
634
|
error?: { code?: unknown } | string;
|
|
@@ -349,23 +639,41 @@ async function isTerminalMainAuthResponse(resp: Response): Promise<boolean> {
|
|
|
349
639
|
: typeof parsed.error === "object" && parsed.error !== null
|
|
350
640
|
? parsed.error.code
|
|
351
641
|
: parsed.code;
|
|
352
|
-
return
|
|
642
|
+
return code;
|
|
353
643
|
} catch {
|
|
354
|
-
return
|
|
644
|
+
return undefined;
|
|
355
645
|
}
|
|
356
646
|
}
|
|
357
647
|
|
|
358
648
|
interface MainAccountInfoFetchResult {
|
|
359
649
|
info: MainAccountInfo;
|
|
650
|
+
/** Whether this attempt safely inspected the physical native-main credential. */
|
|
651
|
+
credentialChecked: boolean;
|
|
652
|
+
/** Meaningful only when credentialChecked is true. */
|
|
653
|
+
hasCredential: boolean;
|
|
654
|
+
/** Main identity generation captured while the native-main claim was held. */
|
|
655
|
+
identityGeneration?: number;
|
|
360
656
|
/** Present only when this call freshly parsed a WHAM usage response. */
|
|
361
657
|
freshQuota?: Omit<StoredAccountQuota, "updatedAt">;
|
|
362
658
|
/** Present only when this call's WHAM response included `rate_limit_reset_credits.available_count`. */
|
|
363
659
|
freshResetCredits?: number;
|
|
364
660
|
}
|
|
365
661
|
|
|
662
|
+
export interface MainAccountInfoSnapshot {
|
|
663
|
+
info: MainAccountInfo;
|
|
664
|
+
mainIdentityGeneration: number;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
export async function fetchMainAccountInfoSnapshot(forceRefresh = false): Promise<MainAccountInfoSnapshot> {
|
|
668
|
+
const result = await fetchMainAccountInfoAttempt(forceRefresh, 1);
|
|
669
|
+
return {
|
|
670
|
+
info: result.info,
|
|
671
|
+
mainIdentityGeneration: result.identityGeneration ?? captureMainAccountIdentityGeneration(),
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
|
|
366
675
|
export async function fetchMainAccountInfo(forceRefresh = false): Promise<MainAccountInfo> {
|
|
367
|
-
|
|
368
|
-
return info;
|
|
676
|
+
return (await fetchMainAccountInfoSnapshot(forceRefresh)).info;
|
|
369
677
|
}
|
|
370
678
|
|
|
371
679
|
const EMPTY_MAIN_ACCOUNT_INFO: MainAccountInfo = { email: null, plan: null, quota: null };
|
|
@@ -373,19 +681,72 @@ const EMPTY_MAIN_ACCOUNT_INFO: MainAccountInfo = { email: null, plan: null, quot
|
|
|
373
681
|
async function retryMainAccountInfoIfIdentityChanged(
|
|
374
682
|
requestAccountId: string | null,
|
|
375
683
|
retriesRemaining: number,
|
|
684
|
+
nativeMainLease: AdmissionLease,
|
|
685
|
+
explicitRefresh: boolean,
|
|
376
686
|
): Promise<MainAccountInfoFetchResult | null> {
|
|
377
687
|
const currentAccountId = getMainChatgptAccountId();
|
|
378
688
|
if (currentAccountId === null || currentAccountId === requestAccountId) return null;
|
|
379
689
|
reconcileMainCodexAccountRuntimeState();
|
|
380
690
|
return retriesRemaining > 0
|
|
381
|
-
?
|
|
382
|
-
: { info: EMPTY_MAIN_ACCOUNT_INFO };
|
|
691
|
+
? fetchMainAccountInfoWhileOwned(true, retriesRemaining - 1, nativeMainLease, explicitRefresh)
|
|
692
|
+
: { info: EMPTY_MAIN_ACCOUNT_INFO, credentialChecked: true, hasCredential: true };
|
|
383
693
|
}
|
|
384
694
|
|
|
385
|
-
async function fetchMainAccountInfoAttempt(
|
|
695
|
+
async function fetchMainAccountInfoAttempt(
|
|
696
|
+
forceRefresh: boolean,
|
|
697
|
+
retriesRemaining: number,
|
|
698
|
+
existingNativeMainLease?: AdmissionLease,
|
|
699
|
+
nativeMainSharedClaimHeld = false,
|
|
700
|
+
): Promise<MainAccountInfoFetchResult> {
|
|
701
|
+
const nativeMainLease = existingNativeMainLease ?? tryAcquireNativeMainProfileClaim();
|
|
702
|
+
if (!nativeMainLease) {
|
|
703
|
+
return {
|
|
704
|
+
info: EMPTY_MAIN_ACCOUNT_INFO,
|
|
705
|
+
credentialChecked: false,
|
|
706
|
+
hasCredential: false,
|
|
707
|
+
identityGeneration: captureMainAccountIdentityGeneration(),
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
try {
|
|
711
|
+
const operation = async () => ({
|
|
712
|
+
...await fetchMainAccountInfoWhileOwned(forceRefresh, retriesRemaining, nativeMainLease),
|
|
713
|
+
identityGeneration: captureMainAccountIdentityGeneration(),
|
|
714
|
+
});
|
|
715
|
+
if (nativeMainSharedClaimHeld) return await operation();
|
|
716
|
+
try {
|
|
717
|
+
return await withNativeMainCredentialClaim(operation);
|
|
718
|
+
} catch (error) {
|
|
719
|
+
if (isNativeMainClaimUnavailable(error)) {
|
|
720
|
+
return {
|
|
721
|
+
info: EMPTY_MAIN_ACCOUNT_INFO,
|
|
722
|
+
credentialChecked: false,
|
|
723
|
+
hasCredential: false,
|
|
724
|
+
identityGeneration: captureMainAccountIdentityGeneration(),
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
throw error;
|
|
728
|
+
}
|
|
729
|
+
} finally {
|
|
730
|
+
if (!existingNativeMainLease) nativeMainLease.release();
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
async function fetchMainAccountInfoWhileOwned(
|
|
735
|
+
forceRefresh: boolean,
|
|
736
|
+
retriesRemaining: number,
|
|
737
|
+
nativeMainLease: AdmissionLease,
|
|
738
|
+
/**
|
|
739
|
+
* Whether the *caller* asked for this refresh. `forceRefresh` also means "bypass the
|
|
740
|
+
* cache", and `retryMainAccountInfoIfIdentityChanged` re-enters with it set purely to
|
|
741
|
+
* re-read after the identity changed. Keeping the two apart stops that retry from
|
|
742
|
+
* promoting a background poll into operator intent below.
|
|
743
|
+
*/
|
|
744
|
+
explicitRefresh: boolean = forceRefresh,
|
|
745
|
+
): Promise<MainAccountInfoFetchResult> {
|
|
386
746
|
const writerGeneration = captureConfigGeneration();
|
|
387
747
|
reconcileMainCodexAccountRuntimeState();
|
|
388
748
|
const tokenRead = readCodexTokensResult();
|
|
749
|
+
setMainAccountCredentialPresence(tokenRead.status === "ok");
|
|
389
750
|
if (tokenRead.status !== "ok") {
|
|
390
751
|
// A local read failure is NOT proof of sign-out: a missing file can be a non-atomic rewrite
|
|
391
752
|
// gap, and malformed JSON can be a half-written file. Clearing the cache and marking the
|
|
@@ -394,13 +755,13 @@ async function fetchMainAccountInfoAttempt(forceRefresh: boolean, retriesRemaini
|
|
|
394
755
|
// routing stays fail-closed because getMainAccountToken() re-reads the file itself, and the
|
|
395
756
|
// account DTO still reports hasCredential=false while the file is unreadable.
|
|
396
757
|
const preserved = getMainAccountInfoCache();
|
|
397
|
-
return { info: preserved ?? EMPTY_MAIN_ACCOUNT_INFO };
|
|
758
|
+
return { info: preserved ?? EMPTY_MAIN_ACCOUNT_INFO, credentialChecked: true, hasCredential: false };
|
|
398
759
|
}
|
|
399
760
|
const tokens = tokenRead.tokens;
|
|
400
761
|
const requestAccountId = extractAccountId(tokens.id_token, tokens.access_token) ?? (tokens.account_id || null);
|
|
401
762
|
const cached = getMainAccountInfoCache();
|
|
402
763
|
if (!forceRefresh && cached && Date.now() - cached.ts < MAIN_CACHE_TTL) {
|
|
403
|
-
return { info: cached };
|
|
764
|
+
return { info: cached, credentialChecked: true, hasCredential: true };
|
|
404
765
|
}
|
|
405
766
|
try {
|
|
406
767
|
const resp = await fetch("https://chatgpt.com/backend-api/wham/usage", {
|
|
@@ -408,17 +769,17 @@ async function fetchMainAccountInfoAttempt(forceRefresh: boolean, retriesRemaini
|
|
|
408
769
|
signal: AbortSignal.timeout(8000),
|
|
409
770
|
});
|
|
410
771
|
if (!resp.ok) {
|
|
411
|
-
const terminalAuthFailure = await isTerminalMainAuthResponse(resp);
|
|
412
|
-
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining);
|
|
772
|
+
const terminalAuthFailure = await isTerminalMainAuthResponse(resp, isMainAccountTokenVerifiablyLive());
|
|
773
|
+
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh);
|
|
413
774
|
if (retried) return retried;
|
|
414
775
|
if (terminalAuthFailure) {
|
|
415
776
|
clearMainAccountInfoCache();
|
|
416
777
|
markAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID, writerGeneration);
|
|
417
778
|
}
|
|
418
|
-
return { info: EMPTY_MAIN_ACCOUNT_INFO };
|
|
779
|
+
return { info: EMPTY_MAIN_ACCOUNT_INFO, credentialChecked: true, hasCredential: true };
|
|
419
780
|
}
|
|
420
781
|
const data = (await resp.json()) as WhamUsageResponse;
|
|
421
|
-
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining);
|
|
782
|
+
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh);
|
|
422
783
|
if (retried) return retried;
|
|
423
784
|
const plan = nonEmptyPlan(data.plan_type) ?? nonEmptyPlan(cached?.plan) ?? nonEmptyPlan(getMainAccountPlan());
|
|
424
785
|
const quota = parseUsageQuota({ ...data, ...(plan ? { plan_type: plan } : {}) });
|
|
@@ -430,7 +791,16 @@ async function fetchMainAccountInfoAttempt(forceRefresh: boolean, retriesRemaini
|
|
|
430
791
|
ts: Date.now(),
|
|
431
792
|
};
|
|
432
793
|
setMainAccountInfoCache(result);
|
|
433
|
-
|
|
794
|
+
// Only an explicit refresh may retract a reauth quarantine. A 200 from
|
|
795
|
+
// /wham/usage proves the token authenticates to the usage endpoint; it does not
|
|
796
|
+
// prove the account can serve Responses traffic, which is a different backend path
|
|
797
|
+
// and still answers 403 for a workspace the token may no longer select (#327).
|
|
798
|
+
// Letting the background poll clear the flag put such an account straight back into
|
|
799
|
+
// rotation: the next request failed the same way and re-marked it, so needsReauth
|
|
800
|
+
// never settled and the dashboard kept showing nothing — the symptom #327 reported.
|
|
801
|
+
// An explicit refresh is an operator asking to re-evaluate, normally right after
|
|
802
|
+
// signing in again, so it stays authoritative.
|
|
803
|
+
if (explicitRefresh) clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
|
|
434
804
|
// Mirror main quota + plan into the shared stores so the rotation engine can
|
|
435
805
|
// score and auto-switch the main account exactly like a pool account (Option A).
|
|
436
806
|
setMainAccountPlan(result.plan);
|
|
@@ -439,12 +809,14 @@ async function fetchMainAccountInfoAttempt(forceRefresh: boolean, retriesRemaini
|
|
|
439
809
|
}
|
|
440
810
|
return {
|
|
441
811
|
info: result,
|
|
812
|
+
credentialChecked: true,
|
|
813
|
+
hasCredential: true,
|
|
442
814
|
...(quota ? { freshQuota: quota } : {}),
|
|
443
815
|
...(freshResetCredits !== undefined ? { freshResetCredits } : {}),
|
|
444
816
|
};
|
|
445
817
|
} catch {
|
|
446
|
-
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining);
|
|
447
|
-
return retried ?? { info: EMPTY_MAIN_ACCOUNT_INFO };
|
|
818
|
+
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh);
|
|
819
|
+
return retried ?? { info: EMPTY_MAIN_ACCOUNT_INFO, credentialChecked: true, hasCredential: true };
|
|
448
820
|
}
|
|
449
821
|
}
|
|
450
822
|
|
|
@@ -514,6 +886,8 @@ export interface CodexAuthAccountDto {
|
|
|
514
886
|
logLabel?: string;
|
|
515
887
|
isMain: boolean;
|
|
516
888
|
paused: boolean;
|
|
889
|
+
/** Selection order; higher is used earlier. Always present, 0 when unset. */
|
|
890
|
+
priority: number;
|
|
517
891
|
quota: (StoredAccountQuota | (Omit<StoredAccountQuota, "updatedAt"> & { updatedAt: number })) | null;
|
|
518
892
|
needsReauth?: boolean;
|
|
519
893
|
hasCredential: boolean;
|
|
@@ -551,6 +925,15 @@ function reconcileFreshPoolAccountPlans(runtimeConfig: OcxConfig, updates: Fresh
|
|
|
551
925
|
accepted.push(update);
|
|
552
926
|
if (persistedAccount.plan !== update.plan) {
|
|
553
927
|
persistedAccount.plan = update.plan;
|
|
928
|
+
// WHAM is the authoritative plan source: stamp provenance so a later JWT
|
|
929
|
+
// reconcile cannot overwrite this observation within the same credential
|
|
930
|
+
// generation (src/codex/plan-from-token.ts jwtMayWritePlan). Stamped only
|
|
931
|
+
// alongside a real plan change: a steady-state refresh whose plan is
|
|
932
|
+
// unchanged must stay write-free (no-config-write contract), and an
|
|
933
|
+
// unchanged value needs no fence — a JWT rewrite to the same text is a
|
|
934
|
+
// no-op under the caller's own equality check.
|
|
935
|
+
persistedAccount.planSource = "wham";
|
|
936
|
+
persistedAccount.planCredentialGeneration = update.credentialGeneration;
|
|
554
937
|
changed = true;
|
|
555
938
|
}
|
|
556
939
|
}
|
|
@@ -570,6 +953,8 @@ function reconcileFreshPoolAccountPlans(runtimeConfig: OcxConfig, updates: Fresh
|
|
|
570
953
|
const liveAccount = configuredPoolAccount(runtimeConfig, update.accountId);
|
|
571
954
|
if (liveAccount) {
|
|
572
955
|
liveAccount.plan = update.plan;
|
|
956
|
+
liveAccount.planSource = "wham";
|
|
957
|
+
liveAccount.planCredentialGeneration = update.credentialGeneration;
|
|
573
958
|
}
|
|
574
959
|
}
|
|
575
960
|
}
|
|
@@ -689,6 +1074,60 @@ async function fetchPoolAccountQuota(accountId: string, forceRefresh = false, co
|
|
|
689
1074
|
}
|
|
690
1075
|
|
|
691
1076
|
let primeInFlight: Promise<void> | null = null;
|
|
1077
|
+
let cooldownRecoveryInFlight: Promise<void> | null = null;
|
|
1078
|
+
|
|
1079
|
+
export async function runCodexCooldownRecoveryProbes(config: OcxConfig, now = Date.now()): Promise<void> {
|
|
1080
|
+
const openai = config.providers[OPENAI_CODEX_PROVIDER_ID];
|
|
1081
|
+
if (!openai
|
|
1082
|
+
|| openai.disabled === true
|
|
1083
|
+
|| !isCanonicalOpenAiForwardProvider(openai)
|
|
1084
|
+
|| providerCodexAccountMode(OPENAI_CODEX_PROVIDER_ID, openai) !== "pool") return;
|
|
1085
|
+
if (cooldownRecoveryInFlight) return cooldownRecoveryInFlight;
|
|
1086
|
+
cooldownRecoveryInFlight = (async () => {
|
|
1087
|
+
const claims = claimDueCodexQuotaRecoveryProbes(config, POOL_QUOTA_REFRESH_CONCURRENCY, now);
|
|
1088
|
+
await mapWithConcurrency(claims, POOL_QUOTA_REFRESH_CONCURRENCY, async claim => {
|
|
1089
|
+
const account = configuredPoolAccount(config, claim.accountId);
|
|
1090
|
+
if (!account) {
|
|
1091
|
+
settleCodexQuotaRecoveryProbe(claim, false, {}, now);
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
try {
|
|
1095
|
+
const result = await fetchPoolAccountQuota(claim.accountId, true, account.plan);
|
|
1096
|
+
// Defence in depth: `spark` is already excluded at the claim site, since generic WHAM
|
|
1097
|
+
// cannot prove a spark recovery. Keep the settle-side guard so a future claim change
|
|
1098
|
+
// cannot silently start clearing spark on generic evidence.
|
|
1099
|
+
const recovered = claim.scope !== "spark"
|
|
1100
|
+
&& isCompleteCodexQuotaRecoverySnapshot(result.freshQuota ?? null, result.freshPlan ?? account.plan);
|
|
1101
|
+
settleCodexQuotaRecoveryProbe(claim, recovered, {
|
|
1102
|
+
credentialGeneration: result.freshCredentialGeneration,
|
|
1103
|
+
}, now);
|
|
1104
|
+
} catch {
|
|
1105
|
+
settleCodexQuotaRecoveryProbe(claim, false, {}, now);
|
|
1106
|
+
}
|
|
1107
|
+
});
|
|
1108
|
+
})().catch(() => {
|
|
1109
|
+
// Background recovery is best-effort; routing keeps the cooldown on failure.
|
|
1110
|
+
}).finally(() => { cooldownRecoveryInFlight = null; });
|
|
1111
|
+
return cooldownRecoveryInFlight;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
export function registerCodexCooldownRecoveryProbeWorker(config: OcxConfig): void {
|
|
1115
|
+
registerStateSweepAfterTick({
|
|
1116
|
+
name: "codex-cooldown-recovery",
|
|
1117
|
+
afterTick: () => { void runCodexCooldownRecoveryProbes(config); },
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
export interface PrimeCodexPoolQuotasOptions {
|
|
1122
|
+
/** Test seams for proving fenced/recovery priming performs no native-main work. */
|
|
1123
|
+
reconcileMainAccount?: typeof reconcileMainCodexAccountRuntimeState;
|
|
1124
|
+
readMainTokens?: typeof readCodexTokens;
|
|
1125
|
+
fetchMainInfo?: typeof fetchMainAccountInfo;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
function tryAcquireNativeMainPrimeLease(): AdmissionLease | null {
|
|
1129
|
+
return tryAcquireNativeMainProfileClaim();
|
|
1130
|
+
}
|
|
692
1131
|
|
|
693
1132
|
/**
|
|
694
1133
|
* Best-effort prime of pool-account (and main) quota so the rotation engine has
|
|
@@ -705,7 +1144,11 @@ let primeInFlight: Promise<void> | null = null;
|
|
|
705
1144
|
* cost, so the worst case is one WHAM call per account per TTL window. Failures
|
|
706
1145
|
* are swallowed: a blocked WSL network must never crash startup or a request.
|
|
707
1146
|
*/
|
|
708
|
-
export async function primeCodexPoolQuotas(
|
|
1147
|
+
export async function primeCodexPoolQuotas(
|
|
1148
|
+
config: OcxConfig,
|
|
1149
|
+
reason: string,
|
|
1150
|
+
options: PrimeCodexPoolQuotasOptions = {},
|
|
1151
|
+
): Promise<void> {
|
|
709
1152
|
const openai = config.providers[OPENAI_CODEX_PROVIDER_ID];
|
|
710
1153
|
if (
|
|
711
1154
|
!openai
|
|
@@ -714,10 +1157,6 @@ export async function primeCodexPoolQuotas(config: OcxConfig, reason: string): P
|
|
|
714
1157
|
|| providerCodexAccountMode(OPENAI_CODEX_PROVIDER_ID, openai) !== "pool"
|
|
715
1158
|
) return;
|
|
716
1159
|
if (primeInFlight) return primeInFlight;
|
|
717
|
-
// Seed the observed physical main identity before startup/lazy priming can populate quota or
|
|
718
|
-
// plan state. Otherwise the first post-startup account switch sees no previous identity and
|
|
719
|
-
// skips the purge that protects the stable __main__ alias.
|
|
720
|
-
reconcileMainCodexAccountRuntimeState();
|
|
721
1160
|
primeInFlight = (async () => {
|
|
722
1161
|
const runtimeConfig = getRuntimeConfig(config);
|
|
723
1162
|
const pool = (runtimeConfig.codexAccounts ?? []).filter(isSelectableCodexPoolAccount);
|
|
@@ -725,10 +1164,30 @@ export async function primeCodexPoolQuotas(config: OcxConfig, reason: string): P
|
|
|
725
1164
|
const q = getAccountQuota(a.id);
|
|
726
1165
|
return !q || Date.now() - q.updatedAt >= POOL_CACHE_TTL;
|
|
727
1166
|
});
|
|
728
|
-
const primeMain =
|
|
1167
|
+
const primeMain = async () => {
|
|
1168
|
+
const mainLease = tryAcquireNativeMainPrimeLease();
|
|
1169
|
+
if (!mainLease) return;
|
|
1170
|
+
try {
|
|
1171
|
+
try {
|
|
1172
|
+
await withNativeMainCredentialClaim(async () => {
|
|
1173
|
+
// Keep one local owner and one cross-process reader from physical
|
|
1174
|
+
// identity reconciliation through WHAM and all quota publication.
|
|
1175
|
+
(options.reconcileMainAccount ?? reconcileMainCodexAccountRuntimeState)();
|
|
1176
|
+
if (getAccountQuota(MAIN_CODEX_ACCOUNT_ID)) return;
|
|
1177
|
+
if (!(options.readMainTokens ?? readCodexTokens)()) return;
|
|
1178
|
+
if (options.fetchMainInfo) await options.fetchMainInfo(false);
|
|
1179
|
+
else await fetchMainAccountInfoAttempt(false, 1, mainLease, true);
|
|
1180
|
+
});
|
|
1181
|
+
} catch (error) {
|
|
1182
|
+
if (!isNativeMainClaimUnavailable(error)) throw error;
|
|
1183
|
+
}
|
|
1184
|
+
} finally {
|
|
1185
|
+
mainLease.release();
|
|
1186
|
+
}
|
|
1187
|
+
};
|
|
729
1188
|
try {
|
|
730
1189
|
await Promise.allSettled([
|
|
731
|
-
primeMain
|
|
1190
|
+
primeMain(),
|
|
732
1191
|
mapWithConcurrency(stale, POOL_QUOTA_REFRESH_CONCURRENCY, async a => {
|
|
733
1192
|
if (!getCodexAccountCredential(a.id)) return;
|
|
734
1193
|
await fetchPoolAccountQuota(a.id, false, a.plan);
|
|
@@ -750,10 +1209,27 @@ export function clearCodexQuotaPrimeState(): void {
|
|
|
750
1209
|
primeInFlight = null;
|
|
751
1210
|
}
|
|
752
1211
|
|
|
753
|
-
|
|
1212
|
+
/** Test-only reset for the worker-level single-flight. */
|
|
1213
|
+
export function clearCodexCooldownRecoveryProbeState(): void {
|
|
1214
|
+
cooldownRecoveryInFlight = null;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
export function effectiveCodexAuthAccountId(config: OcxConfig): string {
|
|
1218
|
+
return getEffectiveActiveCodexAccountId(config) ?? MAIN_CODEX_ACCOUNT_ID;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
export interface CodexAuthAccountsSnapshot {
|
|
1222
|
+
accounts: CodexAuthAccountDto[];
|
|
1223
|
+
mainIdentityGeneration: number;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
export async function listCodexAuthAccountsSnapshot(
|
|
1227
|
+
config: OcxConfig,
|
|
1228
|
+
forceRefresh = false,
|
|
1229
|
+
): Promise<CodexAuthAccountsSnapshot> {
|
|
754
1230
|
const runtimeConfig = getRuntimeConfig(config);
|
|
755
1231
|
const poolAccounts = (runtimeConfig.codexAccounts ?? []).filter(isSelectableCodexPoolAccount);
|
|
756
|
-
const
|
|
1232
|
+
const mainResult = await fetchMainAccountInfoAttempt(forceRefresh, 1);
|
|
757
1233
|
const refreshedPool = await mapWithConcurrency(poolAccounts, POOL_QUOTA_REFRESH_CONCURRENCY, async account => {
|
|
758
1234
|
const cred = getCodexAccountCredential(account.id);
|
|
759
1235
|
let quotaResult: PoolQuotaResult;
|
|
@@ -796,6 +1272,7 @@ export async function listCodexAuthAccounts(config: OcxConfig, forceRefresh = fa
|
|
|
796
1272
|
{ quota: null, needsReauth: true },
|
|
797
1273
|
false,
|
|
798
1274
|
isCodexAccountPaused(runtimeConfig, accountId),
|
|
1275
|
+
getCodexAccountPriority(runtimeConfig, accountId),
|
|
799
1276
|
)];
|
|
800
1277
|
}
|
|
801
1278
|
const resultGeneration = quotaResult.credentialGeneration ?? quotaResult.freshCredentialGeneration;
|
|
@@ -814,10 +1291,17 @@ export async function listCodexAuthAccounts(config: OcxConfig, forceRefresh = fa
|
|
|
814
1291
|
effectiveQuotaResult,
|
|
815
1292
|
true,
|
|
816
1293
|
isCodexAccountPaused(runtimeConfig, accountId),
|
|
1294
|
+
getCodexAccountPriority(runtimeConfig, accountId),
|
|
817
1295
|
)];
|
|
818
1296
|
});
|
|
819
|
-
const
|
|
820
|
-
const
|
|
1297
|
+
const fetchedMainGeneration = mainResult.identityGeneration ?? captureMainAccountIdentityGeneration();
|
|
1298
|
+
const mainSnapshotLive = isMainAccountIdentityGenerationLive(fetchedMainGeneration);
|
|
1299
|
+
const mainInfo = mainSnapshotLive ? mainResult.info : EMPTY_MAIN_ACCOUNT_INFO;
|
|
1300
|
+
const hasMainCredential = mainSnapshotLive && mainResult.credentialChecked
|
|
1301
|
+
? mainResult.hasCredential
|
|
1302
|
+
: getMainAccountCredentialPresence() ?? false;
|
|
1303
|
+
const mainNeedsReauth = (mainSnapshotLive && mainResult.credentialChecked && !hasMainCredential)
|
|
1304
|
+
|| isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
|
|
821
1305
|
const mainHealth = projectCodexAccountHealth({
|
|
822
1306
|
accountId: MAIN_CODEX_ACCOUNT_ID,
|
|
823
1307
|
needsReauth: mainNeedsReauth,
|
|
@@ -826,14 +1310,30 @@ export async function listCodexAuthAccounts(config: OcxConfig, forceRefresh = fa
|
|
|
826
1310
|
id: MAIN_CODEX_ACCOUNT_ID,
|
|
827
1311
|
email: mainInfo.email ?? "Codex App login",
|
|
828
1312
|
plan: mainInfo.plan,
|
|
1313
|
+
logLabel: "main",
|
|
829
1314
|
isMain: true,
|
|
830
1315
|
paused: isCodexAccountPaused(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
|
|
1316
|
+
priority: getCodexAccountPriority(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
|
|
831
1317
|
hasCredential: hasMainCredential,
|
|
832
1318
|
needsReauth: mainNeedsReauth,
|
|
833
|
-
quota: mainInfo.quota ? {
|
|
1319
|
+
quota: mainInfo.quota ? {
|
|
1320
|
+
...quotaForPlan({
|
|
1321
|
+
...mainInfo.quota,
|
|
1322
|
+
updatedAt: getAccountQuota(MAIN_CODEX_ACCOUNT_ID)?.updatedAt ?? Date.now(),
|
|
1323
|
+
}, mainInfo.plan),
|
|
1324
|
+
} : null,
|
|
834
1325
|
...oauthAccountHealthFields("codex", MAIN_CODEX_ACCOUNT_ID, mainHealth),
|
|
835
1326
|
};
|
|
836
|
-
return
|
|
1327
|
+
return {
|
|
1328
|
+
accounts: [main, ...withQuota],
|
|
1329
|
+
mainIdentityGeneration: mainSnapshotLive
|
|
1330
|
+
? fetchedMainGeneration
|
|
1331
|
+
: captureMainAccountIdentityGeneration(),
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
export async function listCodexAuthAccounts(config: OcxConfig, forceRefresh = false): Promise<CodexAuthAccountDto[]> {
|
|
1336
|
+
return (await listCodexAuthAccountsSnapshot(config, forceRefresh)).accounts;
|
|
837
1337
|
}
|
|
838
1338
|
|
|
839
1339
|
interface PauseExhaustedResult {
|
|
@@ -846,63 +1346,102 @@ function selectFallbackAfterPause(config: OcxConfig, pausedActiveId: string): vo
|
|
|
846
1346
|
reconcileCodexActiveAfterExclusion(config, pausedActiveId);
|
|
847
1347
|
}
|
|
848
1348
|
|
|
849
|
-
async function pauseExhaustedCodexAccounts(
|
|
1349
|
+
async function pauseExhaustedCodexAccounts(
|
|
1350
|
+
config: OcxConfig,
|
|
1351
|
+
persistPausedAccounts: () => void,
|
|
1352
|
+
): Promise<PauseExhaustedResult> {
|
|
850
1353
|
const poolAccounts = (config.codexAccounts ?? []).filter(account => !account.isMain);
|
|
851
|
-
const
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
1354
|
+
const nativeMainLease = tryAcquireNativeMainProfileClaim();
|
|
1355
|
+
try {
|
|
1356
|
+
const performPause = async (mainLease?: AdmissionLease): Promise<PauseExhaustedResult> => {
|
|
1357
|
+
const mainWork = async (): Promise<{
|
|
1358
|
+
shouldPause: boolean;
|
|
1359
|
+
checkedAccountCount: number;
|
|
1360
|
+
failedAccountCount: number;
|
|
1361
|
+
}> => {
|
|
1362
|
+
if (!mainLease) return { shouldPause: false, checkedAccountCount: 0, failedAccountCount: 1 };
|
|
1363
|
+
const mainResult = await fetchMainAccountInfoAttempt(true, 1, mainLease, true);
|
|
1364
|
+
if (!mainResult.credentialChecked || !mainResult.hasCredential) {
|
|
1365
|
+
return { shouldPause: false, checkedAccountCount: 0, failedAccountCount: 0 };
|
|
1366
|
+
}
|
|
1367
|
+
if (!mainResult.freshQuota || !mainResult.info.plan) {
|
|
1368
|
+
return { shouldPause: false, checkedAccountCount: 0, failedAccountCount: 1 };
|
|
1369
|
+
}
|
|
1370
|
+
return {
|
|
1371
|
+
shouldPause: !isCodexAccountPaused(config, MAIN_CODEX_ACCOUNT_ID)
|
|
1372
|
+
&& isCodexQuotaExhausted(mainResult.freshQuota, mainResult.info.plan),
|
|
1373
|
+
checkedAccountCount: 1,
|
|
1374
|
+
failedAccountCount: 0,
|
|
1375
|
+
};
|
|
859
1376
|
};
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
1377
|
+
const [mainResult, poolResults] = await Promise.all([
|
|
1378
|
+
mainWork(),
|
|
1379
|
+
mapWithConcurrency(poolAccounts, POOL_QUOTA_REFRESH_CONCURRENCY, async account => {
|
|
1380
|
+
if (!getCodexAccountCredential(account.id)) return { account, quotaResult: null };
|
|
1381
|
+
try {
|
|
1382
|
+
return {
|
|
1383
|
+
account,
|
|
1384
|
+
quotaResult: await fetchPoolAccountQuota(account.id, true, account.plan),
|
|
1385
|
+
};
|
|
1386
|
+
} catch {
|
|
1387
|
+
// Settle each pool probe independently so a busy/failing account cannot
|
|
1388
|
+
// abandon an already-confirmed main decision before atomic publication.
|
|
1389
|
+
return { account, quotaResult: null };
|
|
1390
|
+
}
|
|
1391
|
+
}),
|
|
1392
|
+
]);
|
|
1393
|
+
|
|
1394
|
+
let checkedAccountCount = mainResult.checkedAccountCount;
|
|
1395
|
+
let failedAccountCount = mainResult.failedAccountCount;
|
|
1396
|
+
const exhaustedIds: string[] = mainResult.shouldPause ? [MAIN_CODEX_ACCOUNT_ID] : [];
|
|
1397
|
+
for (const { account, quotaResult } of poolResults) {
|
|
1398
|
+
const currentAccount = (config.codexAccounts ?? []).find(candidate => candidate.id === account.id && !candidate.isMain);
|
|
1399
|
+
if (!currentAccount) continue;
|
|
1400
|
+
const generation = quotaResult?.freshCredentialGeneration;
|
|
1401
|
+
const plan = quotaResult?.freshPlan ?? currentAccount.plan;
|
|
1402
|
+
if (!quotaResult?.freshQuota || generation === undefined || !isCodexAccountGenerationLive(account.id, generation) || !plan) {
|
|
1403
|
+
failedAccountCount += 1;
|
|
1404
|
+
continue;
|
|
1405
|
+
}
|
|
1406
|
+
checkedAccountCount += 1;
|
|
1407
|
+
if (!isCodexAccountPaused(config, account.id) && isCodexQuotaExhausted(quotaResult.freshQuota, plan)) {
|
|
1408
|
+
exhaustedIds.push(account.id);
|
|
1409
|
+
}
|
|
874
1410
|
}
|
|
875
|
-
} else {
|
|
876
|
-
failedAccountCount += 1;
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
for (const { account, quotaResult } of poolResults) {
|
|
880
|
-
const currentAccount = (config.codexAccounts ?? []).find(candidate => candidate.id === account.id && !candidate.isMain);
|
|
881
|
-
if (!currentAccount) continue;
|
|
882
|
-
const generation = quotaResult?.freshCredentialGeneration;
|
|
883
|
-
const plan = quotaResult?.freshPlan ?? currentAccount.plan;
|
|
884
|
-
if (!quotaResult?.freshQuota || generation === undefined || !isCodexAccountGenerationLive(account.id, generation) || !plan) {
|
|
885
|
-
failedAccountCount += 1;
|
|
886
|
-
continue;
|
|
887
|
-
}
|
|
888
|
-
checkedAccountCount += 1;
|
|
889
|
-
if (!isCodexAccountPaused(config, account.id) && isCodexQuotaExhausted(quotaResult.freshQuota, plan)) {
|
|
890
|
-
exhaustedIds.push(account.id);
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
1411
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
1412
|
+
for (const id of exhaustedIds) {
|
|
1413
|
+
setCodexAccountPaused(config, id, true);
|
|
1414
|
+
clearThreadAccountMapForAccount(id);
|
|
1415
|
+
}
|
|
1416
|
+
for (const id of exhaustedIds) selectFallbackAfterPause(config, id);
|
|
1417
|
+
const result = {
|
|
1418
|
+
pausedAccountIds: exhaustedIds,
|
|
1419
|
+
checkedAccountCount,
|
|
1420
|
+
failedAccountCount,
|
|
1421
|
+
};
|
|
1422
|
+
// Persist while both the in-process admission and cross-process shared
|
|
1423
|
+
// claim still own the physical-main identity used for the decision.
|
|
1424
|
+
if (result.pausedAccountIds.length > 0) persistPausedAccounts();
|
|
1425
|
+
return result;
|
|
1426
|
+
};
|
|
1427
|
+
|
|
1428
|
+
if (!nativeMainLease) return await performPause();
|
|
1429
|
+
try {
|
|
1430
|
+
return await withNativeMainCredentialClaim(() => performPause(nativeMainLease));
|
|
1431
|
+
} catch (error) {
|
|
1432
|
+
if (isNativeMainClaimUnavailable(error)) return await performPause();
|
|
1433
|
+
throw error;
|
|
1434
|
+
}
|
|
1435
|
+
} finally {
|
|
1436
|
+
nativeMainLease?.release();
|
|
897
1437
|
}
|
|
898
|
-
for (const id of exhaustedIds) selectFallbackAfterPause(config, id);
|
|
899
|
-
return { pausedAccountIds: exhaustedIds, checkedAccountCount, failedAccountCount };
|
|
900
1438
|
}
|
|
901
1439
|
|
|
902
1440
|
export async function handleCodexAuthAPI(
|
|
903
1441
|
req: Request,
|
|
904
1442
|
url: URL,
|
|
905
1443
|
config: OcxConfig,
|
|
1444
|
+
convergeCodexCatalog?: CodexAuthCatalogConvergence,
|
|
906
1445
|
): Promise<Response | null> {
|
|
907
1446
|
|
|
908
1447
|
if (url.pathname === "/api/codex-auth/accounts" && req.method === "GET") {
|
|
@@ -911,50 +1450,7 @@ export async function handleCodexAuthAPI(
|
|
|
911
1450
|
}
|
|
912
1451
|
|
|
913
1452
|
if (url.pathname === "/api/codex-auth/accounts" && req.method === "POST") {
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
let body: { id: string; email: string; plan?: string; accessToken: string; refreshToken: string; chatgptAccountId: string };
|
|
917
|
-
try { body = (await req.json()) as typeof body; } catch { return jsonResponse({ error: "Invalid JSON" }, 400); }
|
|
918
|
-
if (!body.id || !body.email || !body.accessToken || !body.refreshToken || !body.chatgptAccountId) {
|
|
919
|
-
return jsonResponse({ error: "Missing required fields" }, 400);
|
|
920
|
-
}
|
|
921
|
-
if (!isValidCodexAccountId(body.id)) {
|
|
922
|
-
return jsonResponse({ error: "Invalid account id format" }, 400);
|
|
923
|
-
}
|
|
924
|
-
if (body.accessToken.length > 10_000 || body.refreshToken.length > 10_000) {
|
|
925
|
-
return jsonResponse({ error: "Input too large" }, 400);
|
|
926
|
-
}
|
|
927
|
-
const runtimeConfig = getRuntimeConfig(config);
|
|
928
|
-
const preflightConflict = codexAccountPersistenceConflict(runtimeConfig, body.id, "create");
|
|
929
|
-
if (preflightConflict) return jsonResponse({ error: preflightConflict }, 400);
|
|
930
|
-
// 1.1: Duplicate check is scoped by personal vs workspace plan bucket.
|
|
931
|
-
const derivedAccountId = extractAccountId(undefined, body.accessToken) ?? body.chatgptAccountId;
|
|
932
|
-
const collision = checkAccountIdCollision(derivedAccountId, body.email, body.plan);
|
|
933
|
-
if (collision.collision) {
|
|
934
|
-
return jsonResponse({ error: collision.reason }, 400);
|
|
935
|
-
}
|
|
936
|
-
// 4.2: use JWT exp for expiresAt instead of hardcoded 1 hour
|
|
937
|
-
const payload = decodeJwtPayload(body.accessToken);
|
|
938
|
-
const exp = typeof payload?.exp === "number" ? payload.exp * 1000 : Date.now() + 3600_000;
|
|
939
|
-
const warmup = await verifyCodexAccountWarmup(body.id, body.accessToken, derivedAccountId);
|
|
940
|
-
if (!warmup.ok) return warmup.response;
|
|
941
|
-
const latestConfig = getRuntimeConfig(config);
|
|
942
|
-
const commitConflict = codexAccountPersistenceConflict(latestConfig, body.id, "create");
|
|
943
|
-
if (commitConflict) return jsonResponse({ error: commitConflict }, 400);
|
|
944
|
-
saveCodexAccountCredential(body.id, {
|
|
945
|
-
accessToken: body.accessToken,
|
|
946
|
-
refreshToken: body.refreshToken,
|
|
947
|
-
expiresAt: exp,
|
|
948
|
-
chatgptAccountId: derivedAccountId,
|
|
949
|
-
});
|
|
950
|
-
markCodexAccountValidated(body.id, warmup.validatedAt);
|
|
951
|
-
clearAccountNeedsReauth(body.id);
|
|
952
|
-
const accounts = latestConfig.codexAccounts ?? [];
|
|
953
|
-
accounts.push(withCodexAccountLogLabel({ id: body.id, email: body.email, plan: body.plan, isMain: false }, accounts));
|
|
954
|
-
latestConfig.codexAccounts = accounts;
|
|
955
|
-
saveRuntimeConfig(config, latestConfig);
|
|
956
|
-
reconcileLiveStateStores();
|
|
957
|
-
return jsonResponse({ ok: true });
|
|
1453
|
+
return manualImportDisabledResponse();
|
|
958
1454
|
}
|
|
959
1455
|
|
|
960
1456
|
if (url.pathname === "/api/codex-auth/accounts" && req.method === "DELETE") {
|
|
@@ -966,10 +1462,15 @@ export async function handleCodexAuthAPI(
|
|
|
966
1462
|
if (!isValidCodexAccountId(id) && !isLegacyPoolAccount) {
|
|
967
1463
|
return jsonResponse({ error: "Invalid account id format" }, 400);
|
|
968
1464
|
}
|
|
969
|
-
deleteCodexAccount(runtimeConfig, id);
|
|
1465
|
+
const pickerVisibilityChanged = deleteCodexAccount(runtimeConfig, id);
|
|
970
1466
|
saveRuntimeConfig(config, runtimeConfig);
|
|
971
1467
|
reconcileLiveStateStores();
|
|
972
|
-
|
|
1468
|
+
const catalogRefresh = await convergeAccountNamespaceCatalog(
|
|
1469
|
+
runtimeConfig,
|
|
1470
|
+
pickerVisibilityChanged,
|
|
1471
|
+
convergeCodexCatalog,
|
|
1472
|
+
);
|
|
1473
|
+
return jsonResponse({ ok: true, ...catalogRefresh });
|
|
973
1474
|
}
|
|
974
1475
|
|
|
975
1476
|
if (url.pathname === "/api/codex-auth/accounts/alias" && req.method === "PUT") {
|
|
@@ -1018,9 +1519,59 @@ export async function handleCodexAuthAPI(
|
|
|
1018
1519
|
});
|
|
1019
1520
|
}
|
|
1020
1521
|
|
|
1522
|
+
// Deliberately a route of its own rather than a field on the alias PATCH: aliases
|
|
1523
|
+
// are display-only and reject __main__, while selection order is routing metadata
|
|
1524
|
+
// that the Desktop account must be able to carry. Re-ordering never kicks a live
|
|
1525
|
+
// thread, so there is no affinity clearing and no appliesImmediately here.
|
|
1526
|
+
if (url.pathname === "/api/codex-auth/accounts/priority" && req.method === "PUT") {
|
|
1527
|
+
let parsedBody: unknown;
|
|
1528
|
+
try { parsedBody = await req.json(); } catch { return jsonResponse({ error: "Invalid JSON" }, 400); }
|
|
1529
|
+
if (typeof parsedBody !== "object" || parsedBody === null || Array.isArray(parsedBody)) {
|
|
1530
|
+
return jsonResponse({ error: "body must be an object" }, 400);
|
|
1531
|
+
}
|
|
1532
|
+
const body = parsedBody as { id?: unknown; priority?: unknown };
|
|
1533
|
+
const id = typeof body.id === "string" ? body.id.trim() : "";
|
|
1534
|
+
if (!isCodexAccountPriorityKey(id)) {
|
|
1535
|
+
return jsonResponse({ error: "Invalid account id format" }, 400);
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
let priority = DEFAULT_ACCOUNT_PRIORITY;
|
|
1539
|
+
if (body.priority !== null) {
|
|
1540
|
+
const parsed = parseAccountPriority(body.priority);
|
|
1541
|
+
if (parsed === null) {
|
|
1542
|
+
return jsonResponse({
|
|
1543
|
+
error: `priority must be null or an integer ${MIN_ACCOUNT_PRIORITY}-${MAX_ACCOUNT_PRIORITY}`,
|
|
1544
|
+
}, 400);
|
|
1545
|
+
}
|
|
1546
|
+
priority = parsed;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
const runtimeConfig = getRuntimeConfig(config);
|
|
1550
|
+
const exists = id === MAIN_CODEX_ACCOUNT_ID
|
|
1551
|
+
|| (runtimeConfig.codexAccounts ?? []).some(account => isSelectableCodexPoolAccount(account) && account.id === id);
|
|
1552
|
+
if (!exists) return jsonResponse({ error: "Account not found" }, 404);
|
|
1553
|
+
|
|
1554
|
+
setCodexAccountPriority(runtimeConfig, id, priority);
|
|
1555
|
+
// Both a pin and an order are the operator saying which account to use, so the newer
|
|
1556
|
+
// statement wins. Without this a pin made before any order existed — an ordinary
|
|
1557
|
+
// account switch — would outrank the order forever: it blocks preemption and caps
|
|
1558
|
+
// every eligibility list at its own tier until that account drains or is paused.
|
|
1559
|
+
clearCodexAccountPin(runtimeConfig);
|
|
1560
|
+
saveRuntimeConfig(config, runtimeConfig);
|
|
1561
|
+
return jsonResponse({
|
|
1562
|
+
ok: true,
|
|
1563
|
+
id,
|
|
1564
|
+
priority,
|
|
1565
|
+
activeCodexAccountId: getEffectiveActiveCodexAccountId(runtimeConfig) ?? null,
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1021
1569
|
if (url.pathname === "/api/codex-auth/accounts/pause-exhausted" && req.method === "PUT") {
|
|
1022
1570
|
const runtimeConfig = getRuntimeConfig(config);
|
|
1023
|
-
const result = await pauseExhaustedCodexAccounts(
|
|
1571
|
+
const result = await pauseExhaustedCodexAccounts(
|
|
1572
|
+
runtimeConfig,
|
|
1573
|
+
() => saveRuntimeConfig(config, runtimeConfig),
|
|
1574
|
+
);
|
|
1024
1575
|
const { pausedAccountIds, checkedAccountCount, failedAccountCount } = result;
|
|
1025
1576
|
if (checkedAccountCount === 0 && failedAccountCount > 0) {
|
|
1026
1577
|
return jsonResponse({
|
|
@@ -1030,7 +1581,6 @@ export async function handleCodexAuthAPI(
|
|
|
1030
1581
|
failedAccountCount,
|
|
1031
1582
|
}, 502);
|
|
1032
1583
|
}
|
|
1033
|
-
if (pausedAccountIds.length > 0) saveRuntimeConfig(config, runtimeConfig);
|
|
1034
1584
|
return jsonResponse({
|
|
1035
1585
|
ok: true,
|
|
1036
1586
|
pausedAccountIds,
|
|
@@ -1078,6 +1628,15 @@ export async function handleCodexAuthAPI(
|
|
|
1078
1628
|
if (!exists) return jsonResponse({ error: "Account not found" }, 400);
|
|
1079
1629
|
}
|
|
1080
1630
|
runtimeConfig.activeCodexAccountId = body.accountId ?? undefined;
|
|
1631
|
+
// "Use this account now" outranks selection order until the account is spent:
|
|
1632
|
+
// persisted here rather than in resetCodexRoutingForManualSelection, which is
|
|
1633
|
+
// runtime state only. A null id clears the selection instead of making one, so it
|
|
1634
|
+
// must release the pin rather than record one: pinning the `targetAccountId`
|
|
1635
|
+
// fallback would leave a pin that no effective active account matches, which
|
|
1636
|
+
// `isEffectiveCodexAccountPinned` reports as unpinned while the tier filter still
|
|
1637
|
+
// honours it as a ceiling — invisibly capping the pool at the main account's tier.
|
|
1638
|
+
if (body.accountId == null) clearCodexAccountPin(runtimeConfig);
|
|
1639
|
+
else setCodexAccountPin(runtimeConfig, targetAccountId);
|
|
1081
1640
|
resetCodexRoutingForManualSelection(targetAccountId);
|
|
1082
1641
|
saveRuntimeConfig(config, runtimeConfig);
|
|
1083
1642
|
return jsonResponse({ ok: true, activeCodexAccountId: body.accountId, appliesImmediately: true });
|
|
@@ -1087,6 +1646,13 @@ export async function handleCodexAuthAPI(
|
|
|
1087
1646
|
const runtimeConfig = getRuntimeConfig(config);
|
|
1088
1647
|
return jsonResponse({
|
|
1089
1648
|
activeCodexAccountId: getEffectiveActiveCodexAccountId(runtimeConfig) ?? null,
|
|
1649
|
+
pinned: isEffectiveCodexAccountPinned(runtimeConfig),
|
|
1650
|
+
// Which account carries the pin, not just whether the active one does. Under
|
|
1651
|
+
// round-robin or fill-first the pin caps the tier ceiling at its own tier while the
|
|
1652
|
+
// strategy cursor moves freely inside that tier, so `pinned` alone goes false on a
|
|
1653
|
+
// sibling's turn even though the pin is still suppressing every higher tier. The id
|
|
1654
|
+
// lets a surface mark the account the operator actually chose.
|
|
1655
|
+
pinnedAccountId: pinnedCodexAccountId(runtimeConfig) ?? null,
|
|
1090
1656
|
autoSwitchThreshold: runtimeConfig.autoSwitchThreshold ?? 80,
|
|
1091
1657
|
upstreamFailoverThreshold: runtimeConfig.upstreamFailoverThreshold ?? 3,
|
|
1092
1658
|
accountPoolStrategy: normalizeAccountPoolStrategy(runtimeConfig.accountPoolStrategy),
|
|
@@ -1169,24 +1735,47 @@ export async function handleCodexAuthAPI(
|
|
|
1169
1735
|
if (!accountId) return jsonResponse({ error: "accountId required" }, 400);
|
|
1170
1736
|
|
|
1171
1737
|
try {
|
|
1172
|
-
const
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1738
|
+
const result = await withResetCreditAuth(getRuntimeConfig(config), accountId, async auth => {
|
|
1739
|
+
const linkedSignal = signalWithTimeout(8000, req.signal);
|
|
1740
|
+
let detachBodyAbort = () => {};
|
|
1741
|
+
try {
|
|
1742
|
+
let resp: Response;
|
|
1743
|
+
try {
|
|
1744
|
+
resp = await fetch(
|
|
1745
|
+
"https://chatgpt.com/backend-api/wham/rate-limit-reset-credits",
|
|
1746
|
+
{
|
|
1747
|
+
headers: {
|
|
1748
|
+
Authorization: `Bearer ${auth.accessToken}`,
|
|
1749
|
+
"ChatGPT-Account-Id": auth.chatgptAccountId,
|
|
1750
|
+
},
|
|
1751
|
+
signal: linkedSignal.signal,
|
|
1752
|
+
},
|
|
1753
|
+
);
|
|
1754
|
+
} catch (error) {
|
|
1755
|
+
if (linkedSignal.signal.aborted) {
|
|
1756
|
+
return jsonResponse({ error: "Invalid upstream reset-credit response" }, 502);
|
|
1757
|
+
}
|
|
1758
|
+
throw error;
|
|
1759
|
+
}
|
|
1760
|
+
// Own the response body before the bounded reader attaches. If the client
|
|
1761
|
+
// disconnects in that narrow window, Bun otherwise tears down the native
|
|
1762
|
+
// body off the awaited path and can report an unhandled rejection.
|
|
1763
|
+
detachBodyAbort = cancelBodyOnAbort(resp.body, linkedSignal.signal);
|
|
1764
|
+
if (!resp.ok) {
|
|
1765
|
+
await resp.body?.cancel().catch(() => {});
|
|
1766
|
+
return jsonResponse({ error: `Upstream error ${resp.status}` }, resp.status);
|
|
1767
|
+
}
|
|
1768
|
+
const parsed = await readResetCreditJson(resp, linkedSignal.signal);
|
|
1769
|
+
if (!parsed.ok) {
|
|
1770
|
+
return jsonResponse({ error: "Invalid upstream reset-credit response" }, 502);
|
|
1771
|
+
}
|
|
1772
|
+
return jsonResponse(safeResetCreditsDto(parsed.value));
|
|
1773
|
+
} finally {
|
|
1774
|
+
detachBodyAbort();
|
|
1775
|
+
linkedSignal.cleanup();
|
|
1776
|
+
}
|
|
1777
|
+
});
|
|
1778
|
+
return result.ok ? result.value : result.response;
|
|
1190
1779
|
} catch (e) {
|
|
1191
1780
|
return jsonResponse({ error: e instanceof Error ? e.message : "Reset credit lookup failed" }, 500);
|
|
1192
1781
|
}
|
|
@@ -1195,49 +1784,55 @@ export async function handleCodexAuthAPI(
|
|
|
1195
1784
|
if (url.pathname === "/api/codex-auth/reset-credits/consume" && req.method === "POST") {
|
|
1196
1785
|
const body = (await req.json().catch(() => ({}))) as { accountId?: string };
|
|
1197
1786
|
if (!body.accountId) return jsonResponse({ error: "accountId required" }, 400);
|
|
1787
|
+
const accountId = body.accountId;
|
|
1198
1788
|
|
|
1199
1789
|
try {
|
|
1200
|
-
const
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1790
|
+
const operation = await withResetCreditAuth(getRuntimeConfig(config), accountId, async auth => {
|
|
1791
|
+
const idempotencyKey = crypto.randomUUID();
|
|
1792
|
+
const resp = await fetch(
|
|
1793
|
+
"https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume",
|
|
1794
|
+
{
|
|
1795
|
+
method: "POST",
|
|
1796
|
+
headers: {
|
|
1797
|
+
Authorization: `Bearer ${auth.accessToken}`,
|
|
1798
|
+
"ChatGPT-Account-Id": auth.chatgptAccountId,
|
|
1799
|
+
"Content-Type": "application/json",
|
|
1800
|
+
},
|
|
1801
|
+
body: JSON.stringify({ redeem_request_id: idempotencyKey }),
|
|
1802
|
+
signal: AbortSignal.timeout(10_000),
|
|
1212
1803
|
},
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
if (!resp.ok) {
|
|
1218
|
-
await resp.body?.cancel().catch(() => {});
|
|
1219
|
-
return jsonResponse({ error: `Upstream error ${resp.status}` }, resp.status);
|
|
1220
|
-
}
|
|
1221
|
-
const result = safeResetCreditConsumeDto(await resp.json());
|
|
1222
|
-
// After a successful redeem (or an idempotent already_redeemed), refresh WHAM usage
|
|
1223
|
-
// and return remaining only when that refresh freshly parsed available_count.
|
|
1224
|
-
// Do not fall back to a preserved cached resetCredits (failed/omitted refresh).
|
|
1225
|
-
if (result.code === "reset" || result.code === "already_redeemed") {
|
|
1226
|
-
let freshResetCredits: number | undefined;
|
|
1227
|
-
if (auth.isMain) {
|
|
1228
|
-
({ freshResetCredits } = await fetchMainAccountInfoAttempt(true, 1));
|
|
1229
|
-
} else {
|
|
1230
|
-
const account = configuredPoolAccount(getRuntimeConfig(config), body.accountId);
|
|
1231
|
-
({ freshResetCredits } = await fetchPoolAccountQuota(body.accountId, true, account?.plan));
|
|
1804
|
+
);
|
|
1805
|
+
if (!resp.ok) {
|
|
1806
|
+
await resp.body?.cancel().catch(() => {});
|
|
1807
|
+
return jsonResponse({ error: `Upstream error ${resp.status}` }, resp.status);
|
|
1232
1808
|
}
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1809
|
+
const result = safeResetCreditConsumeDto(await resp.json());
|
|
1810
|
+
// After a successful redeem (or an idempotent already_redeemed), refresh WHAM usage
|
|
1811
|
+
// and return remaining only when that refresh freshly parsed available_count.
|
|
1812
|
+
// Do not fall back to a preserved cached resetCredits (failed/omitted refresh).
|
|
1813
|
+
if (result.code === "reset" || result.code === "already_redeemed") {
|
|
1814
|
+
let freshResetCredits: number | undefined;
|
|
1815
|
+
if (auth.isMain) {
|
|
1816
|
+
({ freshResetCredits } = await fetchMainAccountInfoAttempt(
|
|
1817
|
+
true,
|
|
1818
|
+
1,
|
|
1819
|
+
auth.nativeMainLease,
|
|
1820
|
+
auth.nativeMainSharedClaimHeld === true,
|
|
1821
|
+
));
|
|
1822
|
+
} else {
|
|
1823
|
+
const account = configuredPoolAccount(getRuntimeConfig(config), accountId);
|
|
1824
|
+
({ freshResetCredits } = await fetchPoolAccountQuota(accountId, true, account?.plan));
|
|
1825
|
+
}
|
|
1826
|
+
return jsonResponse({
|
|
1827
|
+
code: result.code,
|
|
1828
|
+
...(typeof freshResetCredits === "number" && Number.isFinite(freshResetCredits)
|
|
1829
|
+
? { remaining: freshResetCredits }
|
|
1830
|
+
: {}),
|
|
1831
|
+
});
|
|
1832
|
+
}
|
|
1833
|
+
return jsonResponse(result);
|
|
1834
|
+
});
|
|
1835
|
+
return operation.ok ? operation.value : operation.response;
|
|
1241
1836
|
} catch (e) {
|
|
1242
1837
|
if (e instanceof PoolQuotaProbeBusyError) {
|
|
1243
1838
|
const response = jsonResponse({ error: "server_busy", code: "server_busy" }, 503);
|
|
@@ -1249,7 +1844,7 @@ export async function handleCodexAuthAPI(
|
|
|
1249
1844
|
}
|
|
1250
1845
|
|
|
1251
1846
|
if (url.pathname === "/api/codex-auth/login" && req.method === "POST") {
|
|
1252
|
-
const body = (await req.json().catch(() => ({}))) as { id?: string; reauth?: boolean };
|
|
1847
|
+
const body = (await req.json().catch(() => ({}))) as { id?: string; reauth?: boolean; openBrowser?: unknown };
|
|
1253
1848
|
const requestedAccountId = body.id?.trim();
|
|
1254
1849
|
const reauth = body.reauth === true;
|
|
1255
1850
|
if (requestedAccountId && !isValidCodexAccountId(requestedAccountId)) {
|
|
@@ -1278,12 +1873,14 @@ export async function handleCodexAuthAPI(
|
|
|
1278
1873
|
const loginOwner: CodexLoginStateRow = { status: "starting", startedAt: Date.now() };
|
|
1279
1874
|
codexAuthLoginState.set(flowId, loginOwner);
|
|
1280
1875
|
try {
|
|
1281
|
-
const { startLoginFlow, getLoginStatus } = await import("../oauth");
|
|
1876
|
+
const { startLoginFlow, getLoginStatus, publicOAuthAuthenticationErrorMessage } = await import("../oauth");
|
|
1282
1877
|
const result = await startLoginFlow("chatgpt", { forceLogin: true });
|
|
1283
1878
|
|
|
1284
1879
|
// Open the browser server-side (same pattern as /api/oauth/login in management-api.ts).
|
|
1285
1880
|
// The GUI's window.open is popup-blocked because it runs after an await, not a direct click.
|
|
1286
|
-
|
|
1881
|
+
// Both login routes share one resolver so this surface cannot drift from the other.
|
|
1882
|
+
const { shouldOpenBrowserForLogin } = await import("../oauth/open-browser-choice");
|
|
1883
|
+
if (result.url && shouldOpenBrowserForLogin(body.openBrowser, runtimeConfig)) {
|
|
1287
1884
|
const { openUrl } = await import("../lib/open-url");
|
|
1288
1885
|
openUrl(result.url);
|
|
1289
1886
|
}
|
|
@@ -1391,6 +1988,8 @@ export async function handleCodexAuthAPI(
|
|
|
1391
1988
|
const latestConfig = getRuntimeConfig(config);
|
|
1392
1989
|
const accounts = latestConfig.codexAccounts ?? [];
|
|
1393
1990
|
const existingIdx = accounts.findIndex(account => account.id === accountId);
|
|
1991
|
+
let pickerVisibilityChanged = false;
|
|
1992
|
+
let newAccountPersistence: PersistNewCodexAccountOutcome | null = null;
|
|
1394
1993
|
const commitConflict = codexAccountPersistenceConflict(
|
|
1395
1994
|
latestConfig,
|
|
1396
1995
|
accountId,
|
|
@@ -1406,22 +2005,21 @@ export async function handleCodexAuthAPI(
|
|
|
1406
2005
|
break;
|
|
1407
2006
|
}
|
|
1408
2007
|
|
|
1409
|
-
|
|
2008
|
+
const credential: CodexAccountCredentials = {
|
|
1410
2009
|
accessToken: cred.access,
|
|
1411
2010
|
refreshToken: cred.refresh,
|
|
1412
2011
|
expiresAt: cred.expires,
|
|
1413
2012
|
chatgptAccountId: oauthAccountId,
|
|
1414
|
-
}
|
|
1415
|
-
// A successful reauthentication replaces the credential generation. Do not let a
|
|
1416
|
-
// failed optional WHAM probe make the replacement inherit quota from the old record.
|
|
1417
|
-
if (reauth) clearAccountQuota(accountId);
|
|
1418
|
-
markCodexAccountValidated(accountId, warmup.validatedAt);
|
|
1419
|
-
clearAccountNeedsReauth(accountId);
|
|
1420
|
-
if (quota) {
|
|
1421
|
-
setAccountQuotaFromParsed(accountId, quota);
|
|
1422
|
-
}
|
|
2013
|
+
};
|
|
1423
2014
|
|
|
1424
2015
|
if (existingIdx >= 0) {
|
|
2016
|
+
saveCodexAccountCredential(accountId, credential);
|
|
2017
|
+
// A successful reauthentication replaces the credential generation. Do not let a
|
|
2018
|
+
// failed optional WHAM probe make the replacement inherit quota from the old record.
|
|
2019
|
+
if (reauth) clearAccountQuota(accountId);
|
|
2020
|
+
markCodexAccountValidated(accountId, warmup.validatedAt);
|
|
2021
|
+
clearAccountNeedsReauth(accountId);
|
|
2022
|
+
if (quota) setAccountQuotaFromParsed(accountId, quota);
|
|
1425
2023
|
// Keep the pool id stable; refresh display metadata after a successful login/reauth.
|
|
1426
2024
|
accounts[existingIdx] = withCodexAccountLogLabel({
|
|
1427
2025
|
...accounts[existingIdx],
|
|
@@ -1432,18 +2030,60 @@ export async function handleCodexAuthAPI(
|
|
|
1432
2030
|
latestConfig.codexAccounts = accounts;
|
|
1433
2031
|
saveRuntimeConfig(config, latestConfig);
|
|
1434
2032
|
} else {
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
2033
|
+
const addedAccount = withCodexAccountLogLabel({ id: accountId, email, plan, isMain: false }, accounts);
|
|
2034
|
+
newAccountPersistence = persistNewCodexAccount(
|
|
2035
|
+
config,
|
|
2036
|
+
latestConfig,
|
|
2037
|
+
addedAccount,
|
|
2038
|
+
{
|
|
2039
|
+
credential,
|
|
2040
|
+
validatedAt: warmup.validatedAt,
|
|
2041
|
+
},
|
|
2042
|
+
);
|
|
2043
|
+
pickerVisibilityChanged = newAccountPersistence.pickerVisibilityChanged;
|
|
1438
2044
|
}
|
|
1439
2045
|
reconcileLiveStateStores();
|
|
1440
|
-
|
|
1441
|
-
|
|
2046
|
+
if (newAccountPersistence?.status === "publication-failed") {
|
|
2047
|
+
markAccountNeedsReauth(accountId);
|
|
2048
|
+
}
|
|
2049
|
+
// A new quota row is generation-gated by live account ownership. Reconcile the
|
|
2050
|
+
// durable config owner first so a partial prior sweep cannot reject this write.
|
|
2051
|
+
if (newAccountPersistence?.status === "committed" && quota) {
|
|
2052
|
+
setAccountQuotaFromParsed(accountId, quota);
|
|
2053
|
+
}
|
|
2054
|
+
const { catalogRefreshPending } = await convergeAccountNamespaceCatalog(
|
|
2055
|
+
latestConfig,
|
|
2056
|
+
pickerVisibilityChanged,
|
|
2057
|
+
convergeCodexCatalog,
|
|
2058
|
+
);
|
|
2059
|
+
if (newAccountPersistence?.status === "publication-failed") {
|
|
2060
|
+
setCodexLoginState(flowId, {
|
|
2061
|
+
status: "error",
|
|
2062
|
+
...codexCredentialPersistenceFailure(accountId, catalogRefreshPending),
|
|
2063
|
+
doneAt: Date.now(),
|
|
2064
|
+
});
|
|
2065
|
+
completed = true;
|
|
2066
|
+
} else {
|
|
2067
|
+
setCodexLoginState(flowId, {
|
|
2068
|
+
status: "done",
|
|
2069
|
+
accountId,
|
|
2070
|
+
email,
|
|
2071
|
+
...(catalogRefreshPending ? { catalogRefreshPending: true } : {}),
|
|
2072
|
+
doneAt: Date.now(),
|
|
2073
|
+
});
|
|
2074
|
+
completed = true;
|
|
2075
|
+
}
|
|
1442
2076
|
}
|
|
1443
2077
|
break;
|
|
1444
2078
|
}
|
|
1445
2079
|
if (st.done && st.error) {
|
|
1446
|
-
setCodexLoginState(flowId, {
|
|
2080
|
+
setCodexLoginState(flowId, {
|
|
2081
|
+
status: "error",
|
|
2082
|
+
// startLoginFlow projects background failures before storing login status, so
|
|
2083
|
+
// fixed actionable OAuth messages retain their type-derived remediation here.
|
|
2084
|
+
error: st.error,
|
|
2085
|
+
doneAt: Date.now(),
|
|
2086
|
+
});
|
|
1447
2087
|
completed = true;
|
|
1448
2088
|
break;
|
|
1449
2089
|
}
|
|
@@ -1461,7 +2101,7 @@ export async function handleCodexAuthAPI(
|
|
|
1461
2101
|
? "Configuration is busy; retry login shortly."
|
|
1462
2102
|
: error instanceof CodexCredentialRefreshBusyError || error instanceof CodexCredentialRefreshStaleError
|
|
1463
2103
|
? "Credential refresh is busy; retry login shortly."
|
|
1464
|
-
:
|
|
2104
|
+
: publicOAuthAuthenticationErrorMessage(error);
|
|
1465
2105
|
setCodexLoginState(flowId, {
|
|
1466
2106
|
status: "error",
|
|
1467
2107
|
error: message,
|
|
@@ -1478,7 +2118,7 @@ export async function handleCodexAuthAPI(
|
|
|
1478
2118
|
} catch (e) {
|
|
1479
2119
|
if (codexAuthLoginState.get(flowId) === loginOwner) codexAuthLoginState.delete(flowId);
|
|
1480
2120
|
const msg = e instanceof Error ? e.message : String(e);
|
|
1481
|
-
if (msg
|
|
2121
|
+
if (msg === "A login for chatgpt is already in progress") {
|
|
1482
2122
|
return jsonResponse({ error: msg, status: "pending" }, 409);
|
|
1483
2123
|
}
|
|
1484
2124
|
if (e instanceof CodexCredentialRefreshBusyError || e instanceof CodexCredentialRefreshStaleError) {
|
|
@@ -1486,7 +2126,8 @@ export async function handleCodexAuthAPI(
|
|
|
1486
2126
|
response.headers.set("Retry-After", "1");
|
|
1487
2127
|
return response;
|
|
1488
2128
|
}
|
|
1489
|
-
|
|
2129
|
+
const { publicOAuthAuthenticationErrorMessage } = await import("../oauth");
|
|
2130
|
+
return jsonResponse({ error: publicOAuthAuthenticationErrorMessage(e) }, 500);
|
|
1490
2131
|
}
|
|
1491
2132
|
}
|
|
1492
2133
|
|
|
@@ -1524,7 +2165,13 @@ export async function handleCodexAuthAPI(
|
|
|
1524
2165
|
const reauthStatus = url.searchParams.get("reauth") === "1";
|
|
1525
2166
|
if (flowId) {
|
|
1526
2167
|
const st = codexAuthLoginState.get(flowId);
|
|
1527
|
-
if (
|
|
2168
|
+
if (
|
|
2169
|
+
!st
|
|
2170
|
+
&& accountId
|
|
2171
|
+
&& !reauthStatus
|
|
2172
|
+
&& !isAccountNeedsReauth(accountId)
|
|
2173
|
+
&& getCodexAccountCredential(accountId)
|
|
2174
|
+
) {
|
|
1528
2175
|
return jsonResponse({ status: "done", accountId });
|
|
1529
2176
|
}
|
|
1530
2177
|
return jsonResponse(st ? { ...st, email: st.email ?? undefined } : { status: "expired" });
|