@iislee/opencodex 2.11.0 → 2.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS_INSTALL.md +109 -0
- package/README.md +114 -19
- package/bin/ocx.mjs +164 -36
- package/bin/package-main.mjs +1 -1
- package/gui/dist/assets/index-BF38heuV.js +104 -0
- package/gui/dist/assets/index-DMiI18Kv.css +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/alibaba-color.svg +1 -1
- package/gui/dist/provider-icons/antigravity-color.svg +1 -1
- package/gui/dist/provider-icons/claude-color.svg +1 -1
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/commandcode-color.svg +1 -0
- package/gui/dist/provider-icons/copilot-color.svg +1 -1
- package/gui/dist/provider-icons/cursor-color.svg +1 -1
- package/gui/dist/provider-icons/deepseek-color.svg +1 -1
- package/gui/dist/provider-icons/firepass-color.svg +1 -1
- package/gui/dist/provider-icons/fireworks-color.svg +1 -1
- package/gui/dist/provider-icons/gemini-color.svg +1 -1
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
- package/gui/dist/provider-icons/grok.svg +1 -1
- package/gui/dist/provider-icons/groq-color.svg +1 -1
- package/gui/dist/provider-icons/huggingface-color.svg +1 -1
- package/gui/dist/provider-icons/kimi-color.svg +1 -1
- package/gui/dist/provider-icons/kiro-color.svg +2 -2
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
- package/gui/dist/provider-icons/mistral-color.svg +1 -1
- package/gui/dist/provider-icons/moonshot-color.svg +1 -1
- package/gui/dist/provider-icons/nvidia-color.svg +1 -1
- package/gui/dist/provider-icons/ollama-color.svg +1 -1
- package/gui/dist/provider-icons/openai.svg +1 -1
- package/gui/dist/provider-icons/opencode.svg +2 -1
- package/gui/dist/provider-icons/openrouter-color.svg +1 -1
- package/gui/dist/provider-icons/pi.svg +2 -2
- package/gui/dist/provider-icons/qianfan-color.svg +1 -1
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/vllm-color.svg +1 -1
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
- package/package.json +19 -10
- package/src/adapters/anthropic-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +376 -52
- package/src/adapters/base.ts +54 -7
- package/src/adapters/client-fingerprint.ts +18 -12
- package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
- package/src/adapters/command-code.ts +601 -0
- package/src/adapters/cursor/checkpoint-store.ts +303 -0
- package/src/adapters/cursor/cursor-errors.ts +129 -6
- package/src/adapters/cursor/discovery.ts +61 -4
- package/src/adapters/cursor/effort-map.ts +27 -3
- package/src/adapters/cursor/framing.ts +39 -0
- package/src/adapters/cursor/h2-pool.ts +123 -0
- package/src/adapters/cursor/http1-bidi.ts +361 -0
- package/src/adapters/cursor/images.ts +704 -0
- package/src/adapters/cursor/live-models.ts +174 -58
- package/src/adapters/cursor/live-transport.ts +609 -170
- package/src/adapters/cursor/native-exec-common.ts +23 -2
- package/src/adapters/cursor/native-exec-fs.ts +10 -7
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +0 -1
- package/src/adapters/cursor/native-exec.ts +101 -14
- package/src/adapters/cursor/protobuf-events.ts +829 -11
- package/src/adapters/cursor/protobuf-request.ts +383 -65
- package/src/adapters/cursor/request-builder.ts +237 -31
- package/src/adapters/cursor/tool-definitions.ts +242 -12
- package/src/adapters/cursor/tool-result-normalize.ts +92 -0
- package/src/adapters/cursor/transport.ts +22 -0
- package/src/adapters/cursor/types.ts +28 -1
- package/src/adapters/cursor.ts +133 -7
- package/src/adapters/google-antigravity-replay.ts +558 -34
- package/src/adapters/google-antigravity-wire.ts +43 -10
- package/src/adapters/google-http.ts +38 -10
- package/src/adapters/google-tool-schema.ts +80 -15
- package/src/adapters/google-truncation.ts +11 -0
- package/src/adapters/google.ts +618 -74
- package/src/adapters/identity.ts +39 -6
- package/src/adapters/kiro-errors.ts +11 -0
- package/src/adapters/kiro-events.ts +19 -1
- package/src/adapters/kiro-thinking.ts +18 -2
- package/src/adapters/kiro-tools.ts +10 -1
- package/src/adapters/kiro.ts +92 -54
- package/src/adapters/mimo-free.ts +17 -0
- package/src/adapters/openai-chat-url.ts +11 -0
- package/src/adapters/openai-chat.ts +1311 -336
- package/src/adapters/openai-responses-url.ts +16 -0
- package/src/adapters/openai-responses.ts +830 -56
- package/src/adapters/registry.ts +175 -0
- package/src/adapters/responses-tool-schema.ts +67 -0
- package/src/adapters/tool-call-id.ts +119 -0
- package/src/adapters/tool-catalog-nudge.ts +104 -21
- package/src/adapters/xai-web-search.ts +185 -0
- package/src/bridge.ts +428 -61
- package/src/chat/inbound.ts +43 -19
- package/src/chat/outbound.ts +82 -26
- package/src/claude/agents-inject.ts +32 -9
- package/src/claude/context-windows.ts +21 -5
- package/src/claude/desktop-3p.ts +243 -9
- package/src/claude/gateway-cache.ts +41 -4
- package/src/claude/inbound.ts +72 -3
- package/src/claude/model-info.ts +38 -15
- package/src/claude/outbound.ts +70 -16
- package/src/cli/account-api.ts +35 -1
- package/src/cli/account-auth.ts +33 -6
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +389 -2
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +30 -6
- package/src/cli/agent.ts +45 -1
- package/src/cli/claude-agent-startup-sync.ts +73 -0
- package/src/cli/claude-desktop.ts +29 -4
- package/src/cli/claude.ts +103 -34
- package/src/cli/codex-log-guard-doctor.ts +103 -0
- package/src/cli/codex-shim-autorestore.ts +2 -0
- package/src/cli/codex-shim-readiness.ts +76 -0
- package/src/cli/combo.ts +8 -0
- package/src/cli/config-command.ts +74 -10
- package/src/cli/dispatch.ts +593 -0
- package/src/cli/doctor.ts +315 -43
- package/src/cli/ensure-desired-integrations.ts +152 -0
- package/src/cli/export-command.ts +46 -20
- package/src/cli/help.ts +20 -271
- package/src/cli/index.ts +378 -519
- package/src/cli/init.ts +4 -17
- package/src/cli/integrations.ts +120 -2
- package/src/cli/lab.ts +607 -0
- package/src/cli/launcher-context.ts +77 -0
- package/src/cli/minimax.ts +497 -0
- package/src/cli/models-runtime.ts +35 -2
- package/src/cli/models.ts +100 -14
- package/src/cli/observe.ts +92 -3
- package/src/cli/opencode.ts +4 -2
- package/src/cli/provider-runtime.ts +18 -1
- package/src/cli/provider.ts +24 -3
- package/src/cli/ready.ts +301 -0
- package/src/cli/registry.ts +437 -0
- package/src/cli/root.ts +86 -0
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +6 -3
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +10 -3
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +153 -6
- package/src/cli/v2.ts +105 -10
- package/src/cli.ts +1 -1
- package/src/clients/config-export.ts +1358 -21
- package/src/codex/account-label.ts +14 -1
- package/src/codex/account-lifecycle.ts +130 -13
- package/src/codex/account-namespaces.ts +49 -3
- package/src/codex/account-priority.ts +83 -0
- package/src/codex/account-store.ts +29 -2
- package/src/codex/account-usability.ts +25 -2
- package/src/codex/admission.ts +256 -0
- package/src/codex/affinity-debug.ts +162 -0
- package/src/codex/app-server-processes.ts +493 -106
- package/src/codex/app-server-restart-service.ts +232 -0
- package/src/codex/auth-api.ts +849 -242
- package/src/codex/auth-collision.ts +5 -3
- package/src/codex/auth-context.ts +345 -32
- package/src/codex/autostart-health.ts +8 -1
- package/src/codex/catalog/account-models.ts +67 -0
- package/src/codex/catalog/aggregation.ts +68 -10
- package/src/codex/catalog/bundled.ts +331 -33
- package/src/codex/catalog/effort.ts +121 -30
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +529 -45
- package/src/codex/catalog/native-models.ts +72 -0
- package/src/codex/catalog/parsing.ts +224 -30
- package/src/codex/catalog/provider-fetch.ts +1460 -134
- package/src/codex/catalog/sync.ts +1449 -186
- package/src/codex/catalog-admission.ts +199 -0
- package/src/codex/catalog-refresh-status.ts +105 -0
- package/src/codex/catalog-write-serialization.ts +242 -0
- package/src/codex/catalog.ts +6 -3
- package/src/codex/codex-write-lock.ts +384 -0
- package/src/codex/convergence-types.ts +614 -0
- package/src/codex/convergence.ts +651 -0
- package/src/codex/coordinator-doctor.ts +332 -0
- package/src/codex/custom-model-catalog-migration.ts +176 -0
- package/src/codex/desired-state.ts +230 -0
- package/src/codex/features.ts +636 -39
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +407 -0
- package/src/codex/history-lock.ts +242 -0
- package/src/codex/history-migration-guardian.ts +26 -20
- package/src/codex/history-provider.ts +231 -28
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +220 -0
- package/src/codex/inject-coordination.ts +290 -0
- package/src/codex/inject.ts +1073 -152
- package/src/codex/injected-marker.ts +37 -3
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +80 -0
- package/src/codex/journal.ts +66 -4
- package/src/codex/log-guard/inspect.ts +506 -0
- package/src/codex/log-guard/lock.ts +150 -0
- package/src/codex/log-guard/maintenance.ts +403 -0
- package/src/codex/log-guard/path-safety.ts +88 -0
- package/src/codex/log-guard/policy.ts +44 -0
- package/src/codex/log-guard/processes.ts +205 -0
- package/src/codex/log-guard/protection.ts +489 -0
- package/src/codex/log-guard/sqlite-errors.ts +9 -0
- package/src/codex/main-account-cache.ts +24 -0
- package/src/codex/main-account.ts +29 -1
- package/src/codex/management-convergence.ts +167 -0
- package/src/codex/model-cache.ts +56 -10
- package/src/codex/model-entitlements.ts +353 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +178 -0
- package/src/codex/native-main-lock-file.ts +162 -0
- package/src/codex/native-main-owner.ts +329 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1531 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +492 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +682 -0
- package/src/codex/paths.ts +80 -1
- package/src/codex/plan-from-token.ts +140 -0
- package/src/codex/plan.ts +40 -0
- package/src/codex/plugins-doctor.ts +1 -1
- package/src/codex/pool-rotation.ts +74 -4
- package/src/codex/project-config-warnings.ts +20 -6
- package/src/codex/prompt-journal.ts +352 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/quota-rejection.ts +298 -0
- package/src/codex/quota.ts +175 -13
- package/src/codex/refresh.ts +11 -2
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/routing.ts +505 -94
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +1009 -28
- package/src/codex/subagent-model-fallback.ts +350 -35
- package/src/codex/sync.ts +191 -2
- package/src/codex/transition-state.ts +612 -0
- package/src/codex/upstream-host-health.ts +368 -0
- package/src/codex/user-identity.ts +557 -0
- package/src/codex/warmup.ts +187 -81
- package/src/codex/write-coordination.ts +114 -0
- package/src/combos/failover.ts +20 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/request.ts +32 -0
- package/src/combos/types.ts +81 -9
- package/src/config/provider-name.ts +24 -0
- package/src/config.ts +1762 -140
- package/src/generated/compatibility-version.json +3164 -0
- package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
- package/src/grok/inject.ts +16 -5
- package/src/grok/inspect.ts +45 -0
- package/src/grok/sync.ts +2 -2
- package/src/images/loop.ts +152 -29
- package/src/images/plan.ts +23 -13
- package/src/integrations/config-io.ts +269 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/mutation-flight.ts +71 -0
- package/src/integrations/native/ownership-preflight.ts +202 -0
- package/src/integrations/omp-yaml-source.ts +358 -0
- package/src/integrations/owned-refresh.ts +74 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +159 -0
- package/src/integrations/serialize.ts +314 -0
- package/src/integrations/state.ts +361 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer-lock.ts +98 -0
- package/src/integrations/writer.ts +691 -0
- package/src/lab/artifacts/sanitize.ts +586 -0
- package/src/lab/artifacts/secure-fs.ts +475 -0
- package/src/lab/artifacts/store.ts +310 -0
- package/src/lab/automation/budgets.ts +78 -0
- package/src/lab/automation/config-persistence.ts +256 -0
- package/src/lab/automation/constants.ts +39 -0
- package/src/lab/automation/cooldown.ts +103 -0
- package/src/lab/automation/dispatch.ts +211 -0
- package/src/lab/automation/index.ts +13 -0
- package/src/lab/automation/orchestrator.ts +499 -0
- package/src/lab/automation/persistence.ts +512 -0
- package/src/lab/automation/planner.ts +371 -0
- package/src/lab/automation/policy.ts +136 -0
- package/src/lab/automation/queue.ts +191 -0
- package/src/lab/automation/recovery.ts +24 -0
- package/src/lab/automation/route-context.ts +21 -0
- package/src/lab/automation/run-key.ts +44 -0
- package/src/lab/automation/runs-query.ts +34 -0
- package/src/lab/automation/types.ts +160 -0
- package/src/lab/conformance/assertion.ts +325 -0
- package/src/lab/conformance/digest.ts +22 -0
- package/src/lab/conformance/executor.ts +741 -0
- package/src/lab/conformance/fixture-provider.ts +27 -0
- package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
- package/src/lab/conformance/harness-budget.ts +47 -0
- package/src/lab/conformance/index.ts +5 -0
- package/src/lab/conformance/jcs.ts +64 -0
- package/src/lab/conformance/json-pointer.ts +39 -0
- package/src/lab/conformance/manifest.ts +180 -0
- package/src/lab/conformance/mcp-stub.ts +179 -0
- package/src/lab/conformance/negative-controls.ts +164 -0
- package/src/lab/conformance/observation.ts +355 -0
- package/src/lab/conformance/runner.ts +68 -0
- package/src/lab/conformance/sse-normalize.ts +59 -0
- package/src/lab/conformance/suite-manifest.ts +78 -0
- package/src/lab/conformance/types.ts +214 -0
- package/src/lab/constants.ts +126 -0
- package/src/lab/digest.ts +64 -0
- package/src/lab/events/errors.ts +9 -0
- package/src/lab/events/limits.ts +117 -0
- package/src/lab/events/types.ts +229 -0
- package/src/lab/events/validate.ts +781 -0
- package/src/lab/fabric/constants.ts +40 -0
- package/src/lab/fabric/executor.ts +492 -0
- package/src/lab/fabric/index.ts +80 -0
- package/src/lab/fabric/manifest.ts +222 -0
- package/src/lab/fabric/observe.ts +489 -0
- package/src/lab/fabric/patch.ts +79 -0
- package/src/lab/fabric/producer-child.ts +139 -0
- package/src/lab/fabric/producer-isolate.ts +276 -0
- package/src/lab/fabric/producer-protocol.ts +61 -0
- package/src/lab/fabric/scratch.ts +439 -0
- package/src/lab/fabric/subject.ts +106 -0
- package/src/lab/fabric/types.ts +134 -0
- package/src/lab/fabric/verifier.ts +98 -0
- package/src/lab/index.ts +54 -0
- package/src/lab/ledger/artifact-refs.ts +127 -0
- package/src/lab/ledger/invalidation.ts +136 -0
- package/src/lab/ledger/purge.ts +310 -0
- package/src/lab/ledger/store.ts +532 -0
- package/src/lab/live/credential-lease.ts +53 -0
- package/src/lab/live/destination.ts +155 -0
- package/src/lab/live/executor.ts +336 -0
- package/src/lab/live/inert-tools.ts +56 -0
- package/src/lab/live/manifest.ts +85 -0
- package/src/lab/live/mcp-loopback.ts +57 -0
- package/src/lab/live/runner.ts +19 -0
- package/src/lab/live/sandbox.ts +61 -0
- package/src/lab/live/suite-manifest.ts +41 -0
- package/src/lab/live/transport.ts +118 -0
- package/src/lab/live/types.ts +197 -0
- package/src/lab/observe/from-conformance.ts +301 -0
- package/src/lab/observe/from-live.ts +117 -0
- package/src/lab/paths.ts +153 -0
- package/src/lab/projection/rebuild.ts +495 -0
- package/src/lab/projection/schema.ts +135 -0
- package/src/lab/projection/verdicts.ts +474 -0
- package/src/lab/projection/verification.ts +412 -0
- package/src/lab/public/bundle.ts +217 -0
- package/src/lab/public/community-authority.ts +175 -0
- package/src/lab/public/community-files.ts +29 -0
- package/src/lab/public/community.ts +479 -0
- package/src/lab/public/file-safety.ts +155 -0
- package/src/lab/public/ids.ts +26 -0
- package/src/lab/public/index.ts +16 -0
- package/src/lab/public/mutation-lock.ts +424 -0
- package/src/lab/public/operator.ts +353 -0
- package/src/lab/public/origin-purge.ts +79 -0
- package/src/lab/public/origin.ts +203 -0
- package/src/lab/public/privacy.ts +143 -0
- package/src/lab/public/private-file.ts +261 -0
- package/src/lab/public/project.ts +124 -0
- package/src/lab/public/purge-test-fault.ts +21 -0
- package/src/lab/public/purge.ts +223 -0
- package/src/lab/public/registry.ts +44 -0
- package/src/lab/public/revocation.ts +252 -0
- package/src/lab/public/signature.ts +243 -0
- package/src/lab/public/storage.ts +105 -0
- package/src/lab/public/strict-json.ts +206 -0
- package/src/lab/public/time.ts +26 -0
- package/src/lab/public/types.ts +172 -0
- package/src/lab/public/validate.ts +391 -0
- package/src/lab/query/catalog.ts +101 -0
- package/src/lab/query/connection.ts +107 -0
- package/src/lab/query/constants.ts +4 -0
- package/src/lab/query/cursor.ts +132 -0
- package/src/lab/query/dto-map.ts +277 -0
- package/src/lab/query/errors.ts +22 -0
- package/src/lab/query/freshness.ts +53 -0
- package/src/lab/query/index.ts +45 -0
- package/src/lab/query/latest-observation.ts +59 -0
- package/src/lab/query/passive-production.ts +159 -0
- package/src/lab/query/queries.ts +444 -0
- package/src/lab/query/types.ts +266 -0
- package/src/lab/subject/behavior-fingerprint.ts +77 -0
- package/src/lab/subject/installation-salt.ts +112 -0
- package/src/lab/subject/protocol-subject.ts +80 -0
- package/src/lab/subject/route-subject.ts +74 -0
- package/src/lib/app-owned-memory-stores.ts +22 -0
- package/src/lib/bounded-body.ts +153 -9
- package/src/lib/bun-runtime.ts +125 -12
- package/src/lib/bun-stream-caps.ts +13 -9
- package/src/lib/codex-restart-contract.ts +120 -0
- package/src/lib/config-ownership.ts +6 -2
- package/src/lib/destination-policy.ts +65 -1
- package/src/lib/errors.ts +44 -2
- package/src/lib/fabric-task-execution-authority.ts +7 -0
- package/src/lib/fabric-task-host.ts +29 -0
- package/src/lib/lab-activation.ts +223 -0
- package/src/lib/lab-live-execution-authority.ts +13 -0
- package/src/lib/lab-live-host.ts +30 -0
- package/src/lib/lab-live-pinned-sender.ts +56 -0
- package/src/lib/lab-live-route-production.ts +130 -0
- package/src/lib/lab-passive-linker-registration.ts +26 -0
- package/src/lib/local-management-attestation.ts +51 -0
- package/src/lib/local-management-capability.ts +100 -0
- package/src/lib/local-provider-reload-contract.ts +100 -0
- package/src/lib/optional-shutdown-hooks.ts +57 -0
- package/src/lib/pinned-http.ts +145 -26
- package/src/lib/process-control.ts +4 -1
- package/src/lib/provider-outbound.ts +49 -9
- package/src/lib/redact.ts +419 -3
- package/src/lib/self-launch-argv.ts +15 -0
- package/src/lib/server-resource-ownership.ts +71 -0
- package/src/lib/shadow-call.ts +35 -4
- package/src/lib/sse-decoder.ts +41 -0
- package/src/lib/state-store-registrations.ts +10 -2
- package/src/lib/system-restart-contract.ts +73 -0
- package/src/lib/token-estimate.ts +19 -2
- package/src/lib/tool-argument-integers.ts +202 -0
- package/src/lib/translator-budget.ts +44 -0
- package/src/lib/upstream-http-version.ts +57 -0
- package/src/lib/upstream-reachability.ts +95 -0
- package/src/lib/upstream-retry.ts +156 -3
- package/src/lib/windows-atomic-replace.ts +155 -0
- package/src/lib/windows-elevation.ts +70 -2
- package/src/lib/windows-secret-acl.ts +409 -69
- package/src/lib/windows-service-wrappers.ts +72 -0
- package/src/lib/windows-text.ts +106 -0
- package/src/lib/windows-user-principal.ts +341 -0
- package/src/lib/winsw.ts +33 -5
- package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
- package/src/oauth/account-import/index.ts +15 -0
- package/src/oauth/account-import/parser.ts +83 -0
- package/src/oauth/account-import/registry.ts +18 -0
- package/src/oauth/account-import/service.ts +75 -0
- package/src/oauth/account-import/types.ts +91 -0
- package/src/oauth/anthropic.ts +12 -1
- package/src/oauth/callback-server.ts +8 -2
- package/src/oauth/chatgpt.ts +12 -1
- package/src/oauth/command-code.ts +239 -0
- package/src/oauth/cursor.ts +46 -5
- package/src/oauth/google-antigravity.ts +35 -3
- package/src/oauth/health.ts +20 -12
- package/src/oauth/index.ts +398 -66
- package/src/oauth/key-providers.ts +16 -0
- package/src/oauth/kimi.ts +16 -2
- package/src/oauth/kiro.ts +50 -6
- package/src/oauth/local-token-detect.ts +11 -2
- package/src/oauth/log.ts +3 -1
- package/src/oauth/login-cli.ts +88 -28
- package/src/oauth/nous.ts +798 -0
- package/src/oauth/store.ts +119 -21
- package/src/oauth/token-guardian.ts +9 -3
- package/src/pi/models.ts +2 -2
- package/src/providers/alibaba-region-migration.ts +1 -1
- package/src/providers/antigravity-models.ts +521 -31
- package/src/providers/base-url-choices.ts +10 -0
- package/src/providers/codex-capacity.ts +292 -0
- package/src/providers/command-code-efforts.ts +144 -0
- package/src/providers/context-cap.ts +22 -5
- package/src/providers/cursor-pool.ts +72 -0
- package/src/providers/derive.ts +253 -6
- package/src/providers/fastwire.ts +501 -0
- package/src/providers/free-directory.ts +10 -7
- package/src/providers/google-vertex-location.ts +14 -0
- package/src/providers/key-failover.ts +71 -3
- package/src/providers/label.ts +1 -1
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +115 -22
- package/src/providers/model-rename-migration.ts +255 -0
- package/src/providers/model-rename-startup.ts +28 -0
- package/src/providers/openai-sidecar.ts +72 -4
- package/src/providers/openai-tier-startup.ts +31 -2
- package/src/providers/openai-tiers.ts +119 -4
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/opencode-zen-rate-limit.ts +102 -0
- package/src/providers/provider-id-rewrite.ts +29 -0
- package/src/providers/quota.ts +1319 -38
- package/src/providers/registry.ts +1429 -111
- package/src/providers/request-pacing.ts +310 -0
- package/src/providers/service-tier.ts +277 -0
- package/src/providers/slug-codec.ts +42 -6
- package/src/providers/static-model-discovery.ts +86 -0
- package/src/providers/xai-responses-opt-in.ts +15 -0
- package/src/providers/xai-transport.ts +11 -4
- package/src/reasoning-effort.ts +49 -1
- package/src/responses/compaction.ts +26 -1
- package/src/responses/custom-tool-compat.ts +266 -0
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/namespace-tool-compat.ts +355 -0
- package/src/responses/parser.ts +220 -38
- package/src/responses/provider-continuation.ts +98 -0
- package/src/responses/provider-opaque-metadata.ts +73 -0
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/reasoning-replay-cache.ts +426 -0
- package/src/responses/schema.ts +7 -1
- package/src/responses/spill-store.ts +75 -10
- package/src/responses/state.ts +565 -27
- package/src/responses/thought-signature-replay.ts +347 -0
- package/src/responses/tool-search-compat.ts +301 -0
- package/src/responses/truncated-stop-reason.ts +60 -0
- package/src/router.ts +366 -30
- package/src/routing/analytics.ts +378 -0
- package/src/routing/capability.ts +244 -0
- package/src/routing/compatibility/assemble.ts +73 -0
- package/src/routing/compatibility/behavior.ts +278 -0
- package/src/routing/compatibility/catalog.ts +99 -0
- package/src/routing/compatibility/endpoint.ts +52 -0
- package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
- package/src/routing/compatibility/policy.ts +181 -0
- package/src/routing/compatibility/provider-slot.ts +56 -0
- package/src/routing/compatibility/reader.ts +110 -0
- package/src/routing/compatibility/subject.ts +191 -0
- package/src/routing/compatibility/types.ts +64 -0
- package/src/routing/compatibility/version.ts +104 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +495 -0
- package/src/routing/health.ts +412 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +605 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile-namespace.ts +15 -0
- package/src/routing/profile.ts +547 -0
- package/src/routing/quota.ts +145 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +776 -0
- package/src/server/adapter-resolve.ts +2 -29
- package/src/server/auth-cors.ts +267 -46
- package/src/server/background-lifecycle.ts +182 -0
- package/src/server/chat-completions.ts +130 -56
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +426 -0
- package/src/server/claude-messages.ts +159 -43
- package/src/server/direct-local-http.ts +347 -0
- package/src/server/effort-policy.ts +18 -0
- package/src/server/github-copilot-responses-repair.ts +338 -0
- package/src/server/gui-static.ts +39 -10
- package/src/server/images.ts +94 -12
- package/src/server/index.ts +865 -181
- package/src/server/lifecycle.ts +284 -13
- package/src/server/live.ts +136 -17
- package/src/server/local-management-read-client.ts +90 -0
- package/src/server/local-provider-reload-client.ts +137 -0
- package/src/server/management/agent-settings-routes.ts +398 -103
- package/src/server/management/api-key-usage.ts +31 -5
- package/src/server/management/body.ts +6 -0
- package/src/server/management/combo-routes.ts +62 -24
- package/src/server/management/config-routes.ts +464 -51
- package/src/server/management/context.ts +80 -2
- package/src/server/management/integration-routes.ts +498 -0
- package/src/server/management/lab-automation-routes.ts +206 -0
- package/src/server/management/lab-routes.ts +563 -0
- package/src/server/management/logs-usage-routes.ts +101 -32
- package/src/server/management/model-routes.ts +189 -131
- package/src/server/management/model-rows.ts +163 -0
- package/src/server/management/native-integration-routes.ts +769 -0
- package/src/server/management/oauth-account-routes.ts +80 -4
- package/src/server/management/provider-capability-config.ts +48 -0
- package/src/server/management/provider-routes.ts +764 -157
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +380 -0
- package/src/server/management/shared.ts +27 -11
- package/src/server/management/sidebar-routes.ts +47 -31
- package/src/server/management/storage-log-guard-routes.ts +186 -0
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +288 -32
- package/src/server/management/system-routes.ts +77 -0
- package/src/server/management/usage-summary-cache.ts +9 -1
- package/src/server/management/vision-sidecar-options.ts +167 -0
- package/src/server/management/web-search-sidecar-options.ts +120 -0
- package/src/server/management-api.ts +115 -14
- package/src/server/management-auth.ts +220 -5
- package/src/server/passive-route-linker.ts +66 -0
- package/src/server/ports.ts +41 -1
- package/src/server/proxy-liveness.ts +132 -5
- package/src/server/readiness.ts +99 -0
- package/src/server/relay-eager.ts +82 -42
- package/src/server/relay.ts +236 -76
- package/src/server/request-decompress.ts +113 -6
- package/src/server/request-log.ts +235 -22
- package/src/server/responses/agent-task-recovery-cache.ts +143 -0
- package/src/server/responses/agent-task-recovery.ts +465 -0
- package/src/server/responses/collaboration.ts +204 -35
- package/src/server/responses/compact.ts +442 -55
- package/src/server/responses/core.ts +2872 -331
- package/src/server/responses/empty-completion-guard.ts +276 -0
- package/src/server/responses/encrypted-payload.ts +62 -39
- package/src/server/responses/fetch-helpers.ts +79 -4
- package/src/server/responses/input-admission.ts +185 -0
- package/src/server/responses/pacing-overload.ts +13 -0
- package/src/server/responses/policy-fallback.ts +178 -0
- package/src/server/responses/responses-field-backfill.ts +251 -0
- package/src/server/responses/terminal-guard.ts +26 -5
- package/src/server/responses/upstream-error.ts +5 -0
- package/src/server/responses/ws-upstream.ts +308 -0
- package/src/server/responses-custom-tool-repair.ts +282 -0
- package/src/server/responses-item-id-repair.ts +54 -6
- package/src/server/responses-json-events.ts +90 -0
- package/src/server/responses-model-rewrite.ts +29 -0
- package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/responses-terminal-repair.ts +342 -0
- package/src/server/responses-tool-search-repair.ts +267 -0
- package/src/server/responses-undeclared-tool-guard.ts +153 -0
- package/src/server/responses.ts +18 -2
- package/src/server/search.ts +78 -13
- package/src/server/sse-frame-buffer.ts +292 -0
- package/src/server/sse-payload-rewrite.ts +110 -22
- package/src/server/startup-action-control.ts +8 -1
- package/src/server/startup-health-cache.ts +19 -1
- package/src/server/system-env.ts +80 -9
- package/src/server/ws-bridge.ts +39 -38
- package/src/service-manager-probe.ts +892 -0
- package/src/service.ts +1111 -90
- package/src/sidecar/auth.ts +92 -0
- package/src/sidecar/candidates.ts +83 -0
- package/src/storage/cleanup.ts +2 -2
- package/src/storage/scanner.ts +1 -1
- package/src/storage/worker-lifecycle.ts +14 -14
- package/src/tray/windows-tray.ps1 +83 -9
- package/src/tray/windows.ts +43 -16
- package/src/types/accounts.ts +37 -0
- package/src/types/config.ts +845 -0
- package/src/types/provider.ts +545 -0
- package/src/types/request.ts +384 -0
- package/src/types/tools.ts +131 -0
- package/src/types/wire.ts +80 -0
- package/src/types.ts +104 -1236
- package/src/update/index.ts +32 -19
- package/src/update/job.ts +442 -67
- package/src/update/notify.ts +12 -6
- package/src/update/npm-cache-preflight.d.mts +47 -0
- package/src/update/npm-cache-preflight.mjs +201 -0
- package/src/update/transactional-install.d.mts +22 -0
- package/src/update/transactional-install.mjs +259 -0
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +268 -16
- package/src/usage/log.ts +606 -41
- package/src/usage/summary.ts +177 -9
- package/src/usage/user-cost-overlay-reconciler.ts +313 -0
- package/src/usage/user-cost-overlays.ts +314 -0
- package/src/vision/anthropic-describe.ts +10 -6
- package/src/vision/backends.ts +97 -0
- package/src/vision/describe.ts +9 -3
- package/src/vision/eligibility.ts +250 -0
- package/src/vision/index.ts +238 -24
- package/src/vision/reasoning.ts +55 -0
- package/src/vision/routed-describe.ts +175 -0
- package/src/vision/timeout-bounds.ts +9 -0
- package/src/web-search/anthropic-executor.ts +13 -7
- package/src/web-search/backends.ts +108 -0
- package/src/web-search/exa-executor.ts +88 -0
- package/src/web-search/executor.ts +11 -3
- package/src/web-search/gemini-executor.ts +141 -0
- package/src/web-search/index.ts +150 -15
- package/src/web-search/loop.ts +279 -50
- package/src/web-search/parse.ts +125 -30
- package/src/web-search/sources.ts +60 -0
- package/src/web-search/xai-executor.ts +219 -0
- package/gui/dist/assets/index-DTpMHS4F.js +0 -67
- package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
- package/gui/dist/provider-icons/antigravity.svg +0 -1
- package/gui/dist/provider-icons/claude.svg +0 -1
- package/gui/dist/provider-icons/copilot.svg +0 -1
- package/gui/dist/provider-icons/cursor.svg +0 -2
- package/gui/dist/provider-icons/gemini.svg +0 -1
- package/gui/dist/provider-icons/grok-color.svg +0 -1
- package/gui/dist/provider-icons/kiro.svg +0 -14
- package/src/cli/internal-dispatch.ts +0 -20
package/src/codex/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,19 +209,19 @@ function codexAccountPersistenceConflict(
|
|
|
142
209
|
: undefined;
|
|
143
210
|
}
|
|
144
211
|
|
|
145
|
-
function isThirtyDayOnlyPlan(plan: string | null | undefined): boolean {
|
|
146
|
-
const normalized = plan?.trim().toLowerCase();
|
|
147
|
-
return normalized === "go" || normalized === "free";
|
|
148
|
-
}
|
|
149
|
-
|
|
150
212
|
function quotaForPlan<T extends Omit<StoredAccountQuota, "updatedAt"> | StoredAccountQuota | null>(
|
|
151
213
|
quota: T,
|
|
152
|
-
plan:
|
|
214
|
+
plan: unknown,
|
|
153
215
|
): T {
|
|
154
|
-
if (!quota || !
|
|
216
|
+
if (!quota || !isThirtyDayOnlyCodexPlan(plan)) return quota;
|
|
155
217
|
return {
|
|
156
218
|
...(quota.monthlyPercent !== undefined ? { monthlyPercent: quota.monthlyPercent } : {}),
|
|
157
219
|
...(quota.monthlyResetAt !== undefined ? { monthlyResetAt: quota.monthlyResetAt } : {}),
|
|
220
|
+
// A 30-day plan can still carry a burst window, and it blocks the account on its own.
|
|
221
|
+
// Dropping it here would show a healthy card for an account upstream is refusing (#1791).
|
|
222
|
+
...(quota.shortPercent !== undefined ? { shortPercent: quota.shortPercent } : {}),
|
|
223
|
+
...(quota.shortResetAt !== undefined ? { shortResetAt: quota.shortResetAt } : {}),
|
|
224
|
+
...(quota.shortWindowSeconds !== undefined ? { shortWindowSeconds: quota.shortWindowSeconds } : {}),
|
|
158
225
|
...(quota.resetCredits !== undefined ? { resetCredits: quota.resetCredits } : {}),
|
|
159
226
|
...("updatedAt" in quota ? { updatedAt: quota.updatedAt } : {}),
|
|
160
227
|
} as T;
|
|
@@ -165,18 +232,21 @@ function poolAccountDto(
|
|
|
165
232
|
quotaResult: PoolQuotaResult,
|
|
166
233
|
hasCredential: boolean,
|
|
167
234
|
paused: boolean,
|
|
235
|
+
priority: number,
|
|
168
236
|
): CodexAuthAccountDto {
|
|
169
|
-
const
|
|
237
|
+
const plan = codexPlanValue(account.plan);
|
|
238
|
+
const quota = quotaForPlan(quotaResult.quota, plan);
|
|
170
239
|
const needsReauth = !hasCredential || quotaResult.needsReauth || isAccountNeedsReauth(account.id);
|
|
171
240
|
const health = projectCodexAccountHealth({ accountId: account.id, needsReauth });
|
|
172
241
|
return {
|
|
173
242
|
id: account.id,
|
|
174
|
-
email: account.email,
|
|
243
|
+
email: maskEmail(account.email) ?? account.email,
|
|
175
244
|
...(account.alias !== undefined ? { alias: account.alias } : {}),
|
|
176
|
-
...(
|
|
177
|
-
|
|
245
|
+
...(plan !== undefined ? { plan } : {}),
|
|
246
|
+
logLabel: codexAccountLogLabel(account),
|
|
178
247
|
isMain: false,
|
|
179
248
|
paused,
|
|
249
|
+
priority,
|
|
180
250
|
quota: quota ? { ...quota } : null,
|
|
181
251
|
needsReauth,
|
|
182
252
|
hasCredential,
|
|
@@ -185,20 +255,52 @@ function poolAccountDto(
|
|
|
185
255
|
};
|
|
186
256
|
}
|
|
187
257
|
|
|
188
|
-
|
|
258
|
+
interface ResetCreditAuth {
|
|
259
|
+
isMain: boolean;
|
|
260
|
+
accessToken: string;
|
|
261
|
+
chatgptAccountId: string;
|
|
262
|
+
nativeMainLease?: AdmissionLease;
|
|
263
|
+
nativeMainSharedClaimHeld?: true;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function withResetCreditAuth<T>(
|
|
189
267
|
runtimeConfig: OcxConfig,
|
|
190
268
|
accountId: string,
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
| { ok: false; response: Response }
|
|
194
|
-
> {
|
|
269
|
+
operation: (auth: ResetCreditAuth) => Promise<T>,
|
|
270
|
+
): Promise<{ ok: true; value: T } | { ok: false; response: Response }> {
|
|
195
271
|
if (accountId === MAIN_CODEX_ACCOUNT_ID) {
|
|
196
272
|
if (hasLegacyMainCodexPoolAccount(runtimeConfig.codexAccounts)) {
|
|
197
273
|
return { ok: false, response: jsonResponse({ error: "Remove the legacy __main__ pool row before using the Desktop account" }, 409) };
|
|
198
274
|
}
|
|
199
|
-
const
|
|
200
|
-
if (!
|
|
201
|
-
|
|
275
|
+
const nativeMainLease = tryAcquireNativeMainProfileClaim();
|
|
276
|
+
if (!nativeMainLease) return { ok: false, response: nativeMainProfileBusyResponse() };
|
|
277
|
+
try {
|
|
278
|
+
try {
|
|
279
|
+
return await withNativeMainCredentialClaim(async () => {
|
|
280
|
+
const tokens = readCodexTokens();
|
|
281
|
+
if (!tokens) {
|
|
282
|
+
return { ok: false, response: jsonResponse({ error: "Main Codex account not logged in" }, 401) };
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
ok: true,
|
|
286
|
+
value: await operation({
|
|
287
|
+
isMain: true,
|
|
288
|
+
accessToken: tokens.access_token,
|
|
289
|
+
chatgptAccountId: tokens.account_id,
|
|
290
|
+
nativeMainLease,
|
|
291
|
+
nativeMainSharedClaimHeld: true,
|
|
292
|
+
}),
|
|
293
|
+
};
|
|
294
|
+
});
|
|
295
|
+
} catch (error) {
|
|
296
|
+
if (isNativeMainClaimUnavailable(error)) {
|
|
297
|
+
return { ok: false, response: nativeMainProfileBusyResponse() };
|
|
298
|
+
}
|
|
299
|
+
throw error;
|
|
300
|
+
}
|
|
301
|
+
} finally {
|
|
302
|
+
nativeMainLease.release();
|
|
303
|
+
}
|
|
202
304
|
}
|
|
203
305
|
if (!isValidCodexAccountId(accountId)) {
|
|
204
306
|
return { ok: false, response: jsonResponse({ error: "Invalid account id format" }, 400) };
|
|
@@ -207,7 +309,14 @@ async function resolveResetCreditAuth(
|
|
|
207
309
|
return { ok: false, response: jsonResponse({ error: "Unknown Codex account" }, 404) };
|
|
208
310
|
}
|
|
209
311
|
const cred = await getValidCodexToken(accountId);
|
|
210
|
-
return {
|
|
312
|
+
return {
|
|
313
|
+
ok: true,
|
|
314
|
+
value: await operation({
|
|
315
|
+
isMain: false,
|
|
316
|
+
accessToken: cred.accessToken,
|
|
317
|
+
chatgptAccountId: cred.chatgptAccountId,
|
|
318
|
+
}),
|
|
319
|
+
};
|
|
211
320
|
}
|
|
212
321
|
|
|
213
322
|
function safeResetCreditsDto(input: unknown): { credits: { granted_at: string; expires_at: string }[]; available_count?: number } {
|
|
@@ -233,8 +342,41 @@ function safeResetCreditConsumeDto(input: unknown): { code: string } {
|
|
|
233
342
|
return { code: typeof obj.code === "string" ? obj.code : "unknown" };
|
|
234
343
|
}
|
|
235
344
|
|
|
236
|
-
|
|
237
|
-
|
|
345
|
+
type ResetCreditJsonRead =
|
|
346
|
+
| { ok: true; value: unknown }
|
|
347
|
+
| { ok: false };
|
|
348
|
+
|
|
349
|
+
function cancelResponseBodyWithoutWaiting(body: ReadableStream<Uint8Array> | null): void {
|
|
350
|
+
if (!body) return;
|
|
351
|
+
try {
|
|
352
|
+
void body.cancel().catch(() => undefined);
|
|
353
|
+
} catch {
|
|
354
|
+
// Some stream implementations throw synchronously from cancel().
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function readResetCreditJson(
|
|
359
|
+
response: Response,
|
|
360
|
+
signal: AbortSignal,
|
|
361
|
+
): Promise<ResetCreditJsonRead> {
|
|
362
|
+
const declaredLength = Number(response.headers.get("content-length"));
|
|
363
|
+
if (Number.isSafeInteger(declaredLength)
|
|
364
|
+
&& declaredLength >= 0
|
|
365
|
+
&& declaredLength > BOUNDED_BODY_MAX_BYTES) {
|
|
366
|
+
cancelResponseBodyWithoutWaiting(response.body);
|
|
367
|
+
return { ok: false };
|
|
368
|
+
}
|
|
369
|
+
try {
|
|
370
|
+
const body = await readBoundedResponseBody(response, {
|
|
371
|
+
signal,
|
|
372
|
+
maxBytes: BOUNDED_BODY_MAX_BYTES,
|
|
373
|
+
fatalUtf8: true,
|
|
374
|
+
});
|
|
375
|
+
if (!body.displaySafe || body.truncated || !body.text.trim()) return { ok: false };
|
|
376
|
+
return { ok: true, value: JSON.parse(body.text) as unknown };
|
|
377
|
+
} catch {
|
|
378
|
+
return { ok: false };
|
|
379
|
+
}
|
|
238
380
|
}
|
|
239
381
|
|
|
240
382
|
function manualImportDisabledResponse(): Response {
|
|
@@ -254,13 +396,10 @@ async function verifyCodexAccountWarmup(
|
|
|
254
396
|
return { ok: true, validatedAt: Date.now() };
|
|
255
397
|
} catch (err) {
|
|
256
398
|
const reason = codexWarmupFailureReason(err);
|
|
257
|
-
const upstream = err instanceof CodexWarmupError ? err.upstreamDetail : undefined;
|
|
258
399
|
return {
|
|
259
400
|
ok: false,
|
|
260
401
|
response: jsonResponse({
|
|
261
|
-
error:
|
|
262
|
-
? `Codex account warmup failed: ${upstream}`
|
|
263
|
-
: "Codex account warmup failed. Reauthenticate the account and try again.",
|
|
402
|
+
error: "Codex account warmup failed. Reauthenticate the account and try again.",
|
|
264
403
|
code: "codex_warmup_failed",
|
|
265
404
|
reason,
|
|
266
405
|
accountId,
|
|
@@ -291,7 +430,7 @@ const POOL_CACHE_TTL = 5 * 60_000;
|
|
|
291
430
|
const POOL_QUOTA_REFRESH_CONCURRENCY = 4;
|
|
292
431
|
|
|
293
432
|
function nonEmptyPlan(value: unknown): string | null {
|
|
294
|
-
return
|
|
433
|
+
return codexPlanValue(value) ?? null;
|
|
295
434
|
}
|
|
296
435
|
|
|
297
436
|
function isRuntimeConfig(config: OcxConfig): boolean {
|
|
@@ -311,6 +450,100 @@ function saveRuntimeConfig(sourceConfig: OcxConfig, nextConfig: OcxConfig): void
|
|
|
311
450
|
Object.assign(sourceConfig, nextConfig);
|
|
312
451
|
}
|
|
313
452
|
|
|
453
|
+
interface StagedNewCodexAccountState {
|
|
454
|
+
credential: CodexAccountCredentials;
|
|
455
|
+
validatedAt: number;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
type PersistNewCodexAccountOutcome =
|
|
459
|
+
| { status: "committed"; pickerVisibilityChanged: boolean }
|
|
460
|
+
| { status: "publication-failed"; pickerVisibilityChanged: boolean };
|
|
461
|
+
|
|
462
|
+
function codexCredentialPersistenceFailure(accountId: string, catalogRefreshPending: boolean) {
|
|
463
|
+
return {
|
|
464
|
+
error: CODEX_CREDENTIAL_PERSISTENCE_ERROR,
|
|
465
|
+
code: CODEX_CREDENTIAL_PERSISTENCE_CODE,
|
|
466
|
+
accountId,
|
|
467
|
+
needsReauth: true as const,
|
|
468
|
+
...(catalogRefreshPending ? { catalogRefreshPending: true as const } : {}),
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/** Persist config before publishing secret or runtime state under the shared mutation coordinator. */
|
|
473
|
+
function persistNewCodexAccount(
|
|
474
|
+
sourceConfig: OcxConfig,
|
|
475
|
+
runtimeConfig: OcxConfig,
|
|
476
|
+
addedAccount: CodexAccount,
|
|
477
|
+
staged: StagedNewCodexAccountState,
|
|
478
|
+
): PersistNewCodexAccountOutcome {
|
|
479
|
+
return withConfigMutationLockSync(() => {
|
|
480
|
+
const previousConfig = { ...runtimeConfig };
|
|
481
|
+
let pickerVisibilityChanged: boolean;
|
|
482
|
+
try {
|
|
483
|
+
const accounts = [...(runtimeConfig.codexAccounts ?? [])];
|
|
484
|
+
const retainedPickerBindingRestored = codexAccountPickerEnabled(runtimeConfig)
|
|
485
|
+
&& Object.values(runtimeConfig.codexAccountNamespaces ?? {}).includes(addedAccount.id);
|
|
486
|
+
accounts.push(addedAccount);
|
|
487
|
+
runtimeConfig.codexAccounts = accounts;
|
|
488
|
+
|
|
489
|
+
// Presence of the explicit flag distinguishes a dashboard-managed map from
|
|
490
|
+
// a hand-authored legacy map. Preserve manual maps exactly.
|
|
491
|
+
const tracksPickerNamespaces = runtimeConfig.codexAccountPickerEnabled !== undefined;
|
|
492
|
+
if (tracksPickerNamespaces && runtimeConfig.codexAccountNamespaces) {
|
|
493
|
+
runtimeConfig.codexAccountNamespaces = { ...runtimeConfig.codexAccountNamespaces };
|
|
494
|
+
}
|
|
495
|
+
const namespaceAdded = tracksPickerNamespaces
|
|
496
|
+
&& appendDefaultCodexAccountNamespace(runtimeConfig, addedAccount);
|
|
497
|
+
pickerVisibilityChanged = namespaceAdded || retainedPickerBindingRestored;
|
|
498
|
+
saveRuntimeConfig(sourceConfig, runtimeConfig);
|
|
499
|
+
} catch (error) {
|
|
500
|
+
for (const key of Object.keys(runtimeConfig) as Array<keyof OcxConfig>) {
|
|
501
|
+
delete runtimeConfig[key];
|
|
502
|
+
}
|
|
503
|
+
Object.assign(runtimeConfig, previousConfig);
|
|
504
|
+
throw error;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
try {
|
|
508
|
+
saveCodexAccountCredential(addedAccount.id, staged.credential);
|
|
509
|
+
markCodexAccountValidated(addedAccount.id, staged.validatedAt);
|
|
510
|
+
clearAccountNeedsReauth(addedAccount.id);
|
|
511
|
+
} catch {
|
|
512
|
+
// Config is already durable. Return the failure outcome through the coordinator so its
|
|
513
|
+
// generation commit is not rolled back while config.json remains changed.
|
|
514
|
+
return { status: "publication-failed" as const, pickerVisibilityChanged };
|
|
515
|
+
}
|
|
516
|
+
return { status: "committed" as const, pickerVisibilityChanged };
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** Bounded catalog-convergence callback supplied by the management dispatcher. */
|
|
521
|
+
export type CodexAuthCatalogConvergence = () => Promise<CatalogDisposition>;
|
|
522
|
+
|
|
523
|
+
interface AccountNamespaceCatalogRefresh {
|
|
524
|
+
catalogRefreshPending: boolean;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** Collapse post-persistence convergence into the one public recovery bit. */
|
|
528
|
+
async function convergeAccountNamespaceCatalog(
|
|
529
|
+
config: OcxConfig,
|
|
530
|
+
changed: boolean,
|
|
531
|
+
convergeCodexCatalog?: CodexAuthCatalogConvergence,
|
|
532
|
+
): Promise<AccountNamespaceCatalogRefresh> {
|
|
533
|
+
if (!changed || !codexAccountPickerEnabled(config)) {
|
|
534
|
+
return { catalogRefreshPending: false };
|
|
535
|
+
}
|
|
536
|
+
if (!convergeCodexCatalog) return { catalogRefreshPending: true };
|
|
537
|
+
|
|
538
|
+
try {
|
|
539
|
+
const catalogRefresh = normalizeCatalogDisposition(await convergeCodexCatalog());
|
|
540
|
+
if (!catalogRefresh) return { catalogRefreshPending: true };
|
|
541
|
+
return { catalogRefreshPending: catalogRefreshIsPending(catalogRefresh) };
|
|
542
|
+
} catch {
|
|
543
|
+
return { catalogRefreshPending: true };
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
314
547
|
async function mapWithConcurrency<T, R>(
|
|
315
548
|
items: T[],
|
|
316
549
|
concurrency: number,
|
|
@@ -333,12 +566,31 @@ const MAIN_TERMINAL_AUTH_CODES = new Set([
|
|
|
333
566
|
"invalid_refresh_token",
|
|
334
567
|
]);
|
|
335
568
|
|
|
336
|
-
|
|
337
|
-
|
|
569
|
+
/**
|
|
570
|
+
* A WHAM 401 is not itself proof the local credential died. Upstream edges can
|
|
571
|
+
* transiently reject a still-valid access token (region/anti-abuse/rotation
|
|
572
|
+
* races), and fail-closing on every bare 401 makes a healthy main account flip
|
|
573
|
+
* needs-reauth on the next GUI quota poll. Only treat the response as terminal
|
|
574
|
+
* when the body carries a known terminal code or the local access token is not
|
|
575
|
+
* verifiably live (`accessTokenLive`). Liveness must be strict: a JWT whose
|
|
576
|
+
* `exp` cannot be decoded is NOT live — an undecodable token that vouched for
|
|
577
|
+
* itself would make a real 401 permanently transient.
|
|
578
|
+
*/
|
|
579
|
+
async function isTerminalMainAuthResponse(resp: Response, accessTokenLive: boolean): Promise<boolean> {
|
|
580
|
+
if (resp.status === 401) {
|
|
581
|
+
if (!accessTokenLive) return true;
|
|
582
|
+
const code = await readMainAuthErrorCode(resp);
|
|
583
|
+
return typeof code === "string" && MAIN_TERMINAL_AUTH_CODES.has(code);
|
|
584
|
+
}
|
|
338
585
|
if (resp.status !== 403) return false;
|
|
586
|
+
const code = await readMainAuthErrorCode(resp);
|
|
587
|
+
return typeof code === "string" && MAIN_TERMINAL_AUTH_CODES.has(code);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
async function readMainAuthErrorCode(resp: Response): Promise<unknown> {
|
|
339
591
|
try {
|
|
340
592
|
const body = await readBoundedResponseBody(resp, { totalTimeoutMs: 1_000, inactivityTimeoutMs: 1_000 });
|
|
341
|
-
if (!body.displaySafe) return
|
|
593
|
+
if (!body.displaySafe) return undefined;
|
|
342
594
|
const parsed = JSON.parse(body.text) as {
|
|
343
595
|
detail?: { code?: unknown } | string;
|
|
344
596
|
error?: { code?: unknown } | string;
|
|
@@ -349,23 +601,41 @@ async function isTerminalMainAuthResponse(resp: Response): Promise<boolean> {
|
|
|
349
601
|
: typeof parsed.error === "object" && parsed.error !== null
|
|
350
602
|
? parsed.error.code
|
|
351
603
|
: parsed.code;
|
|
352
|
-
return
|
|
604
|
+
return code;
|
|
353
605
|
} catch {
|
|
354
|
-
return
|
|
606
|
+
return undefined;
|
|
355
607
|
}
|
|
356
608
|
}
|
|
357
609
|
|
|
358
610
|
interface MainAccountInfoFetchResult {
|
|
359
611
|
info: MainAccountInfo;
|
|
612
|
+
/** Whether this attempt safely inspected the physical native-main credential. */
|
|
613
|
+
credentialChecked: boolean;
|
|
614
|
+
/** Meaningful only when credentialChecked is true. */
|
|
615
|
+
hasCredential: boolean;
|
|
616
|
+
/** Main identity generation captured while the native-main claim was held. */
|
|
617
|
+
identityGeneration?: number;
|
|
360
618
|
/** Present only when this call freshly parsed a WHAM usage response. */
|
|
361
619
|
freshQuota?: Omit<StoredAccountQuota, "updatedAt">;
|
|
362
620
|
/** Present only when this call's WHAM response included `rate_limit_reset_credits.available_count`. */
|
|
363
621
|
freshResetCredits?: number;
|
|
364
622
|
}
|
|
365
623
|
|
|
624
|
+
export interface MainAccountInfoSnapshot {
|
|
625
|
+
info: MainAccountInfo;
|
|
626
|
+
mainIdentityGeneration: number;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
export async function fetchMainAccountInfoSnapshot(forceRefresh = false): Promise<MainAccountInfoSnapshot> {
|
|
630
|
+
const result = await fetchMainAccountInfoAttempt(forceRefresh, 1);
|
|
631
|
+
return {
|
|
632
|
+
info: result.info,
|
|
633
|
+
mainIdentityGeneration: result.identityGeneration ?? captureMainAccountIdentityGeneration(),
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
|
|
366
637
|
export async function fetchMainAccountInfo(forceRefresh = false): Promise<MainAccountInfo> {
|
|
367
|
-
|
|
368
|
-
return info;
|
|
638
|
+
return (await fetchMainAccountInfoSnapshot(forceRefresh)).info;
|
|
369
639
|
}
|
|
370
640
|
|
|
371
641
|
const EMPTY_MAIN_ACCOUNT_INFO: MainAccountInfo = { email: null, plan: null, quota: null };
|
|
@@ -373,19 +643,72 @@ const EMPTY_MAIN_ACCOUNT_INFO: MainAccountInfo = { email: null, plan: null, quot
|
|
|
373
643
|
async function retryMainAccountInfoIfIdentityChanged(
|
|
374
644
|
requestAccountId: string | null,
|
|
375
645
|
retriesRemaining: number,
|
|
646
|
+
nativeMainLease: AdmissionLease,
|
|
647
|
+
explicitRefresh: boolean,
|
|
376
648
|
): Promise<MainAccountInfoFetchResult | null> {
|
|
377
649
|
const currentAccountId = getMainChatgptAccountId();
|
|
378
650
|
if (currentAccountId === null || currentAccountId === requestAccountId) return null;
|
|
379
651
|
reconcileMainCodexAccountRuntimeState();
|
|
380
652
|
return retriesRemaining > 0
|
|
381
|
-
?
|
|
382
|
-
: { info: EMPTY_MAIN_ACCOUNT_INFO };
|
|
653
|
+
? fetchMainAccountInfoWhileOwned(true, retriesRemaining - 1, nativeMainLease, explicitRefresh)
|
|
654
|
+
: { info: EMPTY_MAIN_ACCOUNT_INFO, credentialChecked: true, hasCredential: true };
|
|
383
655
|
}
|
|
384
656
|
|
|
385
|
-
async function fetchMainAccountInfoAttempt(
|
|
657
|
+
async function fetchMainAccountInfoAttempt(
|
|
658
|
+
forceRefresh: boolean,
|
|
659
|
+
retriesRemaining: number,
|
|
660
|
+
existingNativeMainLease?: AdmissionLease,
|
|
661
|
+
nativeMainSharedClaimHeld = false,
|
|
662
|
+
): Promise<MainAccountInfoFetchResult> {
|
|
663
|
+
const nativeMainLease = existingNativeMainLease ?? tryAcquireNativeMainProfileClaim();
|
|
664
|
+
if (!nativeMainLease) {
|
|
665
|
+
return {
|
|
666
|
+
info: EMPTY_MAIN_ACCOUNT_INFO,
|
|
667
|
+
credentialChecked: false,
|
|
668
|
+
hasCredential: false,
|
|
669
|
+
identityGeneration: captureMainAccountIdentityGeneration(),
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
try {
|
|
673
|
+
const operation = async () => ({
|
|
674
|
+
...await fetchMainAccountInfoWhileOwned(forceRefresh, retriesRemaining, nativeMainLease),
|
|
675
|
+
identityGeneration: captureMainAccountIdentityGeneration(),
|
|
676
|
+
});
|
|
677
|
+
if (nativeMainSharedClaimHeld) return await operation();
|
|
678
|
+
try {
|
|
679
|
+
return await withNativeMainCredentialClaim(operation);
|
|
680
|
+
} catch (error) {
|
|
681
|
+
if (isNativeMainClaimUnavailable(error)) {
|
|
682
|
+
return {
|
|
683
|
+
info: EMPTY_MAIN_ACCOUNT_INFO,
|
|
684
|
+
credentialChecked: false,
|
|
685
|
+
hasCredential: false,
|
|
686
|
+
identityGeneration: captureMainAccountIdentityGeneration(),
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
throw error;
|
|
690
|
+
}
|
|
691
|
+
} finally {
|
|
692
|
+
if (!existingNativeMainLease) nativeMainLease.release();
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
async function fetchMainAccountInfoWhileOwned(
|
|
697
|
+
forceRefresh: boolean,
|
|
698
|
+
retriesRemaining: number,
|
|
699
|
+
nativeMainLease: AdmissionLease,
|
|
700
|
+
/**
|
|
701
|
+
* Whether the *caller* asked for this refresh. `forceRefresh` also means "bypass the
|
|
702
|
+
* cache", and `retryMainAccountInfoIfIdentityChanged` re-enters with it set purely to
|
|
703
|
+
* re-read after the identity changed. Keeping the two apart stops that retry from
|
|
704
|
+
* promoting a background poll into operator intent below.
|
|
705
|
+
*/
|
|
706
|
+
explicitRefresh: boolean = forceRefresh,
|
|
707
|
+
): Promise<MainAccountInfoFetchResult> {
|
|
386
708
|
const writerGeneration = captureConfigGeneration();
|
|
387
709
|
reconcileMainCodexAccountRuntimeState();
|
|
388
710
|
const tokenRead = readCodexTokensResult();
|
|
711
|
+
setMainAccountCredentialPresence(tokenRead.status === "ok");
|
|
389
712
|
if (tokenRead.status !== "ok") {
|
|
390
713
|
// A local read failure is NOT proof of sign-out: a missing file can be a non-atomic rewrite
|
|
391
714
|
// gap, and malformed JSON can be a half-written file. Clearing the cache and marking the
|
|
@@ -394,13 +717,13 @@ async function fetchMainAccountInfoAttempt(forceRefresh: boolean, retriesRemaini
|
|
|
394
717
|
// routing stays fail-closed because getMainAccountToken() re-reads the file itself, and the
|
|
395
718
|
// account DTO still reports hasCredential=false while the file is unreadable.
|
|
396
719
|
const preserved = getMainAccountInfoCache();
|
|
397
|
-
return { info: preserved ?? EMPTY_MAIN_ACCOUNT_INFO };
|
|
720
|
+
return { info: preserved ?? EMPTY_MAIN_ACCOUNT_INFO, credentialChecked: true, hasCredential: false };
|
|
398
721
|
}
|
|
399
722
|
const tokens = tokenRead.tokens;
|
|
400
723
|
const requestAccountId = extractAccountId(tokens.id_token, tokens.access_token) ?? (tokens.account_id || null);
|
|
401
724
|
const cached = getMainAccountInfoCache();
|
|
402
725
|
if (!forceRefresh && cached && Date.now() - cached.ts < MAIN_CACHE_TTL) {
|
|
403
|
-
return { info: cached };
|
|
726
|
+
return { info: cached, credentialChecked: true, hasCredential: true };
|
|
404
727
|
}
|
|
405
728
|
try {
|
|
406
729
|
const resp = await fetch("https://chatgpt.com/backend-api/wham/usage", {
|
|
@@ -408,17 +731,17 @@ async function fetchMainAccountInfoAttempt(forceRefresh: boolean, retriesRemaini
|
|
|
408
731
|
signal: AbortSignal.timeout(8000),
|
|
409
732
|
});
|
|
410
733
|
if (!resp.ok) {
|
|
411
|
-
const terminalAuthFailure = await isTerminalMainAuthResponse(resp);
|
|
412
|
-
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining);
|
|
734
|
+
const terminalAuthFailure = await isTerminalMainAuthResponse(resp, isMainAccountTokenVerifiablyLive());
|
|
735
|
+
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh);
|
|
413
736
|
if (retried) return retried;
|
|
414
737
|
if (terminalAuthFailure) {
|
|
415
738
|
clearMainAccountInfoCache();
|
|
416
739
|
markAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID, writerGeneration);
|
|
417
740
|
}
|
|
418
|
-
return { info: EMPTY_MAIN_ACCOUNT_INFO };
|
|
741
|
+
return { info: EMPTY_MAIN_ACCOUNT_INFO, credentialChecked: true, hasCredential: true };
|
|
419
742
|
}
|
|
420
743
|
const data = (await resp.json()) as WhamUsageResponse;
|
|
421
|
-
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining);
|
|
744
|
+
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh);
|
|
422
745
|
if (retried) return retried;
|
|
423
746
|
const plan = nonEmptyPlan(data.plan_type) ?? nonEmptyPlan(cached?.plan) ?? nonEmptyPlan(getMainAccountPlan());
|
|
424
747
|
const quota = parseUsageQuota({ ...data, ...(plan ? { plan_type: plan } : {}) });
|
|
@@ -430,7 +753,16 @@ async function fetchMainAccountInfoAttempt(forceRefresh: boolean, retriesRemaini
|
|
|
430
753
|
ts: Date.now(),
|
|
431
754
|
};
|
|
432
755
|
setMainAccountInfoCache(result);
|
|
433
|
-
|
|
756
|
+
// Only an explicit refresh may retract a reauth quarantine. A 200 from
|
|
757
|
+
// /wham/usage proves the token authenticates to the usage endpoint; it does not
|
|
758
|
+
// prove the account can serve Responses traffic, which is a different backend path
|
|
759
|
+
// and still answers 403 for a workspace the token may no longer select (#327).
|
|
760
|
+
// Letting the background poll clear the flag put such an account straight back into
|
|
761
|
+
// rotation: the next request failed the same way and re-marked it, so needsReauth
|
|
762
|
+
// never settled and the dashboard kept showing nothing — the symptom #327 reported.
|
|
763
|
+
// An explicit refresh is an operator asking to re-evaluate, normally right after
|
|
764
|
+
// signing in again, so it stays authoritative.
|
|
765
|
+
if (explicitRefresh) clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
|
|
434
766
|
// Mirror main quota + plan into the shared stores so the rotation engine can
|
|
435
767
|
// score and auto-switch the main account exactly like a pool account (Option A).
|
|
436
768
|
setMainAccountPlan(result.plan);
|
|
@@ -439,12 +771,14 @@ async function fetchMainAccountInfoAttempt(forceRefresh: boolean, retriesRemaini
|
|
|
439
771
|
}
|
|
440
772
|
return {
|
|
441
773
|
info: result,
|
|
774
|
+
credentialChecked: true,
|
|
775
|
+
hasCredential: true,
|
|
442
776
|
...(quota ? { freshQuota: quota } : {}),
|
|
443
777
|
...(freshResetCredits !== undefined ? { freshResetCredits } : {}),
|
|
444
778
|
};
|
|
445
779
|
} catch {
|
|
446
|
-
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining);
|
|
447
|
-
return retried ?? { info: EMPTY_MAIN_ACCOUNT_INFO };
|
|
780
|
+
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh);
|
|
781
|
+
return retried ?? { info: EMPTY_MAIN_ACCOUNT_INFO, credentialChecked: true, hasCredential: true };
|
|
448
782
|
}
|
|
449
783
|
}
|
|
450
784
|
|
|
@@ -514,6 +848,8 @@ export interface CodexAuthAccountDto {
|
|
|
514
848
|
logLabel?: string;
|
|
515
849
|
isMain: boolean;
|
|
516
850
|
paused: boolean;
|
|
851
|
+
/** Selection order; higher is used earlier. Always present, 0 when unset. */
|
|
852
|
+
priority: number;
|
|
517
853
|
quota: (StoredAccountQuota | (Omit<StoredAccountQuota, "updatedAt"> & { updatedAt: number })) | null;
|
|
518
854
|
needsReauth?: boolean;
|
|
519
855
|
hasCredential: boolean;
|
|
@@ -551,6 +887,15 @@ function reconcileFreshPoolAccountPlans(runtimeConfig: OcxConfig, updates: Fresh
|
|
|
551
887
|
accepted.push(update);
|
|
552
888
|
if (persistedAccount.plan !== update.plan) {
|
|
553
889
|
persistedAccount.plan = update.plan;
|
|
890
|
+
// WHAM is the authoritative plan source: stamp provenance so a later JWT
|
|
891
|
+
// reconcile cannot overwrite this observation within the same credential
|
|
892
|
+
// generation (src/codex/plan-from-token.ts jwtMayWritePlan). Stamped only
|
|
893
|
+
// alongside a real plan change: a steady-state refresh whose plan is
|
|
894
|
+
// unchanged must stay write-free (no-config-write contract), and an
|
|
895
|
+
// unchanged value needs no fence — a JWT rewrite to the same text is a
|
|
896
|
+
// no-op under the caller's own equality check.
|
|
897
|
+
persistedAccount.planSource = "wham";
|
|
898
|
+
persistedAccount.planCredentialGeneration = update.credentialGeneration;
|
|
554
899
|
changed = true;
|
|
555
900
|
}
|
|
556
901
|
}
|
|
@@ -570,6 +915,8 @@ function reconcileFreshPoolAccountPlans(runtimeConfig: OcxConfig, updates: Fresh
|
|
|
570
915
|
const liveAccount = configuredPoolAccount(runtimeConfig, update.accountId);
|
|
571
916
|
if (liveAccount) {
|
|
572
917
|
liveAccount.plan = update.plan;
|
|
918
|
+
liveAccount.planSource = "wham";
|
|
919
|
+
liveAccount.planCredentialGeneration = update.credentialGeneration;
|
|
573
920
|
}
|
|
574
921
|
}
|
|
575
922
|
}
|
|
@@ -689,6 +1036,60 @@ async function fetchPoolAccountQuota(accountId: string, forceRefresh = false, co
|
|
|
689
1036
|
}
|
|
690
1037
|
|
|
691
1038
|
let primeInFlight: Promise<void> | null = null;
|
|
1039
|
+
let cooldownRecoveryInFlight: Promise<void> | null = null;
|
|
1040
|
+
|
|
1041
|
+
export async function runCodexCooldownRecoveryProbes(config: OcxConfig, now = Date.now()): Promise<void> {
|
|
1042
|
+
const openai = config.providers[OPENAI_CODEX_PROVIDER_ID];
|
|
1043
|
+
if (!openai
|
|
1044
|
+
|| openai.disabled === true
|
|
1045
|
+
|| !isCanonicalOpenAiForwardProvider(openai)
|
|
1046
|
+
|| providerCodexAccountMode(OPENAI_CODEX_PROVIDER_ID, openai) !== "pool") return;
|
|
1047
|
+
if (cooldownRecoveryInFlight) return cooldownRecoveryInFlight;
|
|
1048
|
+
cooldownRecoveryInFlight = (async () => {
|
|
1049
|
+
const claims = claimDueCodexQuotaRecoveryProbes(config, POOL_QUOTA_REFRESH_CONCURRENCY, now);
|
|
1050
|
+
await mapWithConcurrency(claims, POOL_QUOTA_REFRESH_CONCURRENCY, async claim => {
|
|
1051
|
+
const account = configuredPoolAccount(config, claim.accountId);
|
|
1052
|
+
if (!account) {
|
|
1053
|
+
settleCodexQuotaRecoveryProbe(claim, false, {}, now);
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
try {
|
|
1057
|
+
const result = await fetchPoolAccountQuota(claim.accountId, true, account.plan);
|
|
1058
|
+
// Defence in depth: `spark` is already excluded at the claim site, since generic WHAM
|
|
1059
|
+
// cannot prove a spark recovery. Keep the settle-side guard so a future claim change
|
|
1060
|
+
// cannot silently start clearing spark on generic evidence.
|
|
1061
|
+
const recovered = claim.scope !== "spark"
|
|
1062
|
+
&& isCompleteCodexQuotaRecoverySnapshot(result.freshQuota ?? null, result.freshPlan ?? account.plan);
|
|
1063
|
+
settleCodexQuotaRecoveryProbe(claim, recovered, {
|
|
1064
|
+
credentialGeneration: result.freshCredentialGeneration,
|
|
1065
|
+
}, now);
|
|
1066
|
+
} catch {
|
|
1067
|
+
settleCodexQuotaRecoveryProbe(claim, false, {}, now);
|
|
1068
|
+
}
|
|
1069
|
+
});
|
|
1070
|
+
})().catch(() => {
|
|
1071
|
+
// Background recovery is best-effort; routing keeps the cooldown on failure.
|
|
1072
|
+
}).finally(() => { cooldownRecoveryInFlight = null; });
|
|
1073
|
+
return cooldownRecoveryInFlight;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
export function registerCodexCooldownRecoveryProbeWorker(config: OcxConfig): void {
|
|
1077
|
+
registerStateSweepAfterTick({
|
|
1078
|
+
name: "codex-cooldown-recovery",
|
|
1079
|
+
afterTick: () => { void runCodexCooldownRecoveryProbes(config); },
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
export interface PrimeCodexPoolQuotasOptions {
|
|
1084
|
+
/** Test seams for proving fenced/recovery priming performs no native-main work. */
|
|
1085
|
+
reconcileMainAccount?: typeof reconcileMainCodexAccountRuntimeState;
|
|
1086
|
+
readMainTokens?: typeof readCodexTokens;
|
|
1087
|
+
fetchMainInfo?: typeof fetchMainAccountInfo;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function tryAcquireNativeMainPrimeLease(): AdmissionLease | null {
|
|
1091
|
+
return tryAcquireNativeMainProfileClaim();
|
|
1092
|
+
}
|
|
692
1093
|
|
|
693
1094
|
/**
|
|
694
1095
|
* Best-effort prime of pool-account (and main) quota so the rotation engine has
|
|
@@ -705,7 +1106,11 @@ let primeInFlight: Promise<void> | null = null;
|
|
|
705
1106
|
* cost, so the worst case is one WHAM call per account per TTL window. Failures
|
|
706
1107
|
* are swallowed: a blocked WSL network must never crash startup or a request.
|
|
707
1108
|
*/
|
|
708
|
-
export async function primeCodexPoolQuotas(
|
|
1109
|
+
export async function primeCodexPoolQuotas(
|
|
1110
|
+
config: OcxConfig,
|
|
1111
|
+
reason: string,
|
|
1112
|
+
options: PrimeCodexPoolQuotasOptions = {},
|
|
1113
|
+
): Promise<void> {
|
|
709
1114
|
const openai = config.providers[OPENAI_CODEX_PROVIDER_ID];
|
|
710
1115
|
if (
|
|
711
1116
|
!openai
|
|
@@ -714,10 +1119,6 @@ export async function primeCodexPoolQuotas(config: OcxConfig, reason: string): P
|
|
|
714
1119
|
|| providerCodexAccountMode(OPENAI_CODEX_PROVIDER_ID, openai) !== "pool"
|
|
715
1120
|
) return;
|
|
716
1121
|
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
1122
|
primeInFlight = (async () => {
|
|
722
1123
|
const runtimeConfig = getRuntimeConfig(config);
|
|
723
1124
|
const pool = (runtimeConfig.codexAccounts ?? []).filter(isSelectableCodexPoolAccount);
|
|
@@ -725,10 +1126,30 @@ export async function primeCodexPoolQuotas(config: OcxConfig, reason: string): P
|
|
|
725
1126
|
const q = getAccountQuota(a.id);
|
|
726
1127
|
return !q || Date.now() - q.updatedAt >= POOL_CACHE_TTL;
|
|
727
1128
|
});
|
|
728
|
-
const primeMain =
|
|
1129
|
+
const primeMain = async () => {
|
|
1130
|
+
const mainLease = tryAcquireNativeMainPrimeLease();
|
|
1131
|
+
if (!mainLease) return;
|
|
1132
|
+
try {
|
|
1133
|
+
try {
|
|
1134
|
+
await withNativeMainCredentialClaim(async () => {
|
|
1135
|
+
// Keep one local owner and one cross-process reader from physical
|
|
1136
|
+
// identity reconciliation through WHAM and all quota publication.
|
|
1137
|
+
(options.reconcileMainAccount ?? reconcileMainCodexAccountRuntimeState)();
|
|
1138
|
+
if (getAccountQuota(MAIN_CODEX_ACCOUNT_ID)) return;
|
|
1139
|
+
if (!(options.readMainTokens ?? readCodexTokens)()) return;
|
|
1140
|
+
if (options.fetchMainInfo) await options.fetchMainInfo(false);
|
|
1141
|
+
else await fetchMainAccountInfoAttempt(false, 1, mainLease, true);
|
|
1142
|
+
});
|
|
1143
|
+
} catch (error) {
|
|
1144
|
+
if (!isNativeMainClaimUnavailable(error)) throw error;
|
|
1145
|
+
}
|
|
1146
|
+
} finally {
|
|
1147
|
+
mainLease.release();
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
729
1150
|
try {
|
|
730
1151
|
await Promise.allSettled([
|
|
731
|
-
primeMain
|
|
1152
|
+
primeMain(),
|
|
732
1153
|
mapWithConcurrency(stale, POOL_QUOTA_REFRESH_CONCURRENCY, async a => {
|
|
733
1154
|
if (!getCodexAccountCredential(a.id)) return;
|
|
734
1155
|
await fetchPoolAccountQuota(a.id, false, a.plan);
|
|
@@ -750,10 +1171,27 @@ export function clearCodexQuotaPrimeState(): void {
|
|
|
750
1171
|
primeInFlight = null;
|
|
751
1172
|
}
|
|
752
1173
|
|
|
753
|
-
|
|
1174
|
+
/** Test-only reset for the worker-level single-flight. */
|
|
1175
|
+
export function clearCodexCooldownRecoveryProbeState(): void {
|
|
1176
|
+
cooldownRecoveryInFlight = null;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
export function effectiveCodexAuthAccountId(config: OcxConfig): string {
|
|
1180
|
+
return getEffectiveActiveCodexAccountId(config) ?? MAIN_CODEX_ACCOUNT_ID;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
export interface CodexAuthAccountsSnapshot {
|
|
1184
|
+
accounts: CodexAuthAccountDto[];
|
|
1185
|
+
mainIdentityGeneration: number;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
export async function listCodexAuthAccountsSnapshot(
|
|
1189
|
+
config: OcxConfig,
|
|
1190
|
+
forceRefresh = false,
|
|
1191
|
+
): Promise<CodexAuthAccountsSnapshot> {
|
|
754
1192
|
const runtimeConfig = getRuntimeConfig(config);
|
|
755
1193
|
const poolAccounts = (runtimeConfig.codexAccounts ?? []).filter(isSelectableCodexPoolAccount);
|
|
756
|
-
const
|
|
1194
|
+
const mainResult = await fetchMainAccountInfoAttempt(forceRefresh, 1);
|
|
757
1195
|
const refreshedPool = await mapWithConcurrency(poolAccounts, POOL_QUOTA_REFRESH_CONCURRENCY, async account => {
|
|
758
1196
|
const cred = getCodexAccountCredential(account.id);
|
|
759
1197
|
let quotaResult: PoolQuotaResult;
|
|
@@ -796,6 +1234,7 @@ export async function listCodexAuthAccounts(config: OcxConfig, forceRefresh = fa
|
|
|
796
1234
|
{ quota: null, needsReauth: true },
|
|
797
1235
|
false,
|
|
798
1236
|
isCodexAccountPaused(runtimeConfig, accountId),
|
|
1237
|
+
getCodexAccountPriority(runtimeConfig, accountId),
|
|
799
1238
|
)];
|
|
800
1239
|
}
|
|
801
1240
|
const resultGeneration = quotaResult.credentialGeneration ?? quotaResult.freshCredentialGeneration;
|
|
@@ -814,10 +1253,17 @@ export async function listCodexAuthAccounts(config: OcxConfig, forceRefresh = fa
|
|
|
814
1253
|
effectiveQuotaResult,
|
|
815
1254
|
true,
|
|
816
1255
|
isCodexAccountPaused(runtimeConfig, accountId),
|
|
1256
|
+
getCodexAccountPriority(runtimeConfig, accountId),
|
|
817
1257
|
)];
|
|
818
1258
|
});
|
|
819
|
-
const
|
|
820
|
-
const
|
|
1259
|
+
const fetchedMainGeneration = mainResult.identityGeneration ?? captureMainAccountIdentityGeneration();
|
|
1260
|
+
const mainSnapshotLive = isMainAccountIdentityGenerationLive(fetchedMainGeneration);
|
|
1261
|
+
const mainInfo = mainSnapshotLive ? mainResult.info : EMPTY_MAIN_ACCOUNT_INFO;
|
|
1262
|
+
const hasMainCredential = mainSnapshotLive && mainResult.credentialChecked
|
|
1263
|
+
? mainResult.hasCredential
|
|
1264
|
+
: getMainAccountCredentialPresence() ?? false;
|
|
1265
|
+
const mainNeedsReauth = (mainSnapshotLive && mainResult.credentialChecked && !hasMainCredential)
|
|
1266
|
+
|| isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
|
|
821
1267
|
const mainHealth = projectCodexAccountHealth({
|
|
822
1268
|
accountId: MAIN_CODEX_ACCOUNT_ID,
|
|
823
1269
|
needsReauth: mainNeedsReauth,
|
|
@@ -826,14 +1272,30 @@ export async function listCodexAuthAccounts(config: OcxConfig, forceRefresh = fa
|
|
|
826
1272
|
id: MAIN_CODEX_ACCOUNT_ID,
|
|
827
1273
|
email: mainInfo.email ?? "Codex App login",
|
|
828
1274
|
plan: mainInfo.plan,
|
|
1275
|
+
logLabel: "main",
|
|
829
1276
|
isMain: true,
|
|
830
1277
|
paused: isCodexAccountPaused(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
|
|
1278
|
+
priority: getCodexAccountPriority(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
|
|
831
1279
|
hasCredential: hasMainCredential,
|
|
832
1280
|
needsReauth: mainNeedsReauth,
|
|
833
|
-
quota: mainInfo.quota ? {
|
|
1281
|
+
quota: mainInfo.quota ? {
|
|
1282
|
+
...quotaForPlan({
|
|
1283
|
+
...mainInfo.quota,
|
|
1284
|
+
updatedAt: getAccountQuota(MAIN_CODEX_ACCOUNT_ID)?.updatedAt ?? Date.now(),
|
|
1285
|
+
}, mainInfo.plan),
|
|
1286
|
+
} : null,
|
|
834
1287
|
...oauthAccountHealthFields("codex", MAIN_CODEX_ACCOUNT_ID, mainHealth),
|
|
835
1288
|
};
|
|
836
|
-
return
|
|
1289
|
+
return {
|
|
1290
|
+
accounts: [main, ...withQuota],
|
|
1291
|
+
mainIdentityGeneration: mainSnapshotLive
|
|
1292
|
+
? fetchedMainGeneration
|
|
1293
|
+
: captureMainAccountIdentityGeneration(),
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
export async function listCodexAuthAccounts(config: OcxConfig, forceRefresh = false): Promise<CodexAuthAccountDto[]> {
|
|
1298
|
+
return (await listCodexAuthAccountsSnapshot(config, forceRefresh)).accounts;
|
|
837
1299
|
}
|
|
838
1300
|
|
|
839
1301
|
interface PauseExhaustedResult {
|
|
@@ -846,63 +1308,102 @@ function selectFallbackAfterPause(config: OcxConfig, pausedActiveId: string): vo
|
|
|
846
1308
|
reconcileCodexActiveAfterExclusion(config, pausedActiveId);
|
|
847
1309
|
}
|
|
848
1310
|
|
|
849
|
-
async function pauseExhaustedCodexAccounts(
|
|
1311
|
+
async function pauseExhaustedCodexAccounts(
|
|
1312
|
+
config: OcxConfig,
|
|
1313
|
+
persistPausedAccounts: () => void,
|
|
1314
|
+
): Promise<PauseExhaustedResult> {
|
|
850
1315
|
const poolAccounts = (config.codexAccounts ?? []).filter(account => !account.isMain);
|
|
851
|
-
const
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
1316
|
+
const nativeMainLease = tryAcquireNativeMainProfileClaim();
|
|
1317
|
+
try {
|
|
1318
|
+
const performPause = async (mainLease?: AdmissionLease): Promise<PauseExhaustedResult> => {
|
|
1319
|
+
const mainWork = async (): Promise<{
|
|
1320
|
+
shouldPause: boolean;
|
|
1321
|
+
checkedAccountCount: number;
|
|
1322
|
+
failedAccountCount: number;
|
|
1323
|
+
}> => {
|
|
1324
|
+
if (!mainLease) return { shouldPause: false, checkedAccountCount: 0, failedAccountCount: 1 };
|
|
1325
|
+
const mainResult = await fetchMainAccountInfoAttempt(true, 1, mainLease, true);
|
|
1326
|
+
if (!mainResult.credentialChecked || !mainResult.hasCredential) {
|
|
1327
|
+
return { shouldPause: false, checkedAccountCount: 0, failedAccountCount: 0 };
|
|
1328
|
+
}
|
|
1329
|
+
if (!mainResult.freshQuota || !mainResult.info.plan) {
|
|
1330
|
+
return { shouldPause: false, checkedAccountCount: 0, failedAccountCount: 1 };
|
|
1331
|
+
}
|
|
1332
|
+
return {
|
|
1333
|
+
shouldPause: !isCodexAccountPaused(config, MAIN_CODEX_ACCOUNT_ID)
|
|
1334
|
+
&& isCodexQuotaExhausted(mainResult.freshQuota, mainResult.info.plan),
|
|
1335
|
+
checkedAccountCount: 1,
|
|
1336
|
+
failedAccountCount: 0,
|
|
1337
|
+
};
|
|
859
1338
|
};
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
1339
|
+
const [mainResult, poolResults] = await Promise.all([
|
|
1340
|
+
mainWork(),
|
|
1341
|
+
mapWithConcurrency(poolAccounts, POOL_QUOTA_REFRESH_CONCURRENCY, async account => {
|
|
1342
|
+
if (!getCodexAccountCredential(account.id)) return { account, quotaResult: null };
|
|
1343
|
+
try {
|
|
1344
|
+
return {
|
|
1345
|
+
account,
|
|
1346
|
+
quotaResult: await fetchPoolAccountQuota(account.id, true, account.plan),
|
|
1347
|
+
};
|
|
1348
|
+
} catch {
|
|
1349
|
+
// Settle each pool probe independently so a busy/failing account cannot
|
|
1350
|
+
// abandon an already-confirmed main decision before atomic publication.
|
|
1351
|
+
return { account, quotaResult: null };
|
|
1352
|
+
}
|
|
1353
|
+
}),
|
|
1354
|
+
]);
|
|
1355
|
+
|
|
1356
|
+
let checkedAccountCount = mainResult.checkedAccountCount;
|
|
1357
|
+
let failedAccountCount = mainResult.failedAccountCount;
|
|
1358
|
+
const exhaustedIds: string[] = mainResult.shouldPause ? [MAIN_CODEX_ACCOUNT_ID] : [];
|
|
1359
|
+
for (const { account, quotaResult } of poolResults) {
|
|
1360
|
+
const currentAccount = (config.codexAccounts ?? []).find(candidate => candidate.id === account.id && !candidate.isMain);
|
|
1361
|
+
if (!currentAccount) continue;
|
|
1362
|
+
const generation = quotaResult?.freshCredentialGeneration;
|
|
1363
|
+
const plan = quotaResult?.freshPlan ?? currentAccount.plan;
|
|
1364
|
+
if (!quotaResult?.freshQuota || generation === undefined || !isCodexAccountGenerationLive(account.id, generation) || !plan) {
|
|
1365
|
+
failedAccountCount += 1;
|
|
1366
|
+
continue;
|
|
1367
|
+
}
|
|
1368
|
+
checkedAccountCount += 1;
|
|
1369
|
+
if (!isCodexAccountPaused(config, account.id) && isCodexQuotaExhausted(quotaResult.freshQuota, plan)) {
|
|
1370
|
+
exhaustedIds.push(account.id);
|
|
1371
|
+
}
|
|
874
1372
|
}
|
|
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
1373
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
1374
|
+
for (const id of exhaustedIds) {
|
|
1375
|
+
setCodexAccountPaused(config, id, true);
|
|
1376
|
+
clearThreadAccountMapForAccount(id);
|
|
1377
|
+
}
|
|
1378
|
+
for (const id of exhaustedIds) selectFallbackAfterPause(config, id);
|
|
1379
|
+
const result = {
|
|
1380
|
+
pausedAccountIds: exhaustedIds,
|
|
1381
|
+
checkedAccountCount,
|
|
1382
|
+
failedAccountCount,
|
|
1383
|
+
};
|
|
1384
|
+
// Persist while both the in-process admission and cross-process shared
|
|
1385
|
+
// claim still own the physical-main identity used for the decision.
|
|
1386
|
+
if (result.pausedAccountIds.length > 0) persistPausedAccounts();
|
|
1387
|
+
return result;
|
|
1388
|
+
};
|
|
1389
|
+
|
|
1390
|
+
if (!nativeMainLease) return await performPause();
|
|
1391
|
+
try {
|
|
1392
|
+
return await withNativeMainCredentialClaim(() => performPause(nativeMainLease));
|
|
1393
|
+
} catch (error) {
|
|
1394
|
+
if (isNativeMainClaimUnavailable(error)) return await performPause();
|
|
1395
|
+
throw error;
|
|
1396
|
+
}
|
|
1397
|
+
} finally {
|
|
1398
|
+
nativeMainLease?.release();
|
|
897
1399
|
}
|
|
898
|
-
for (const id of exhaustedIds) selectFallbackAfterPause(config, id);
|
|
899
|
-
return { pausedAccountIds: exhaustedIds, checkedAccountCount, failedAccountCount };
|
|
900
1400
|
}
|
|
901
1401
|
|
|
902
1402
|
export async function handleCodexAuthAPI(
|
|
903
1403
|
req: Request,
|
|
904
1404
|
url: URL,
|
|
905
1405
|
config: OcxConfig,
|
|
1406
|
+
convergeCodexCatalog?: CodexAuthCatalogConvergence,
|
|
906
1407
|
): Promise<Response | null> {
|
|
907
1408
|
|
|
908
1409
|
if (url.pathname === "/api/codex-auth/accounts" && req.method === "GET") {
|
|
@@ -911,50 +1412,7 @@ export async function handleCodexAuthAPI(
|
|
|
911
1412
|
}
|
|
912
1413
|
|
|
913
1414
|
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 });
|
|
1415
|
+
return manualImportDisabledResponse();
|
|
958
1416
|
}
|
|
959
1417
|
|
|
960
1418
|
if (url.pathname === "/api/codex-auth/accounts" && req.method === "DELETE") {
|
|
@@ -966,10 +1424,15 @@ export async function handleCodexAuthAPI(
|
|
|
966
1424
|
if (!isValidCodexAccountId(id) && !isLegacyPoolAccount) {
|
|
967
1425
|
return jsonResponse({ error: "Invalid account id format" }, 400);
|
|
968
1426
|
}
|
|
969
|
-
deleteCodexAccount(runtimeConfig, id);
|
|
1427
|
+
const pickerVisibilityChanged = deleteCodexAccount(runtimeConfig, id);
|
|
970
1428
|
saveRuntimeConfig(config, runtimeConfig);
|
|
971
1429
|
reconcileLiveStateStores();
|
|
972
|
-
|
|
1430
|
+
const catalogRefresh = await convergeAccountNamespaceCatalog(
|
|
1431
|
+
runtimeConfig,
|
|
1432
|
+
pickerVisibilityChanged,
|
|
1433
|
+
convergeCodexCatalog,
|
|
1434
|
+
);
|
|
1435
|
+
return jsonResponse({ ok: true, ...catalogRefresh });
|
|
973
1436
|
}
|
|
974
1437
|
|
|
975
1438
|
if (url.pathname === "/api/codex-auth/accounts/alias" && req.method === "PUT") {
|
|
@@ -1018,9 +1481,59 @@ export async function handleCodexAuthAPI(
|
|
|
1018
1481
|
});
|
|
1019
1482
|
}
|
|
1020
1483
|
|
|
1484
|
+
// Deliberately a route of its own rather than a field on the alias PATCH: aliases
|
|
1485
|
+
// are display-only and reject __main__, while selection order is routing metadata
|
|
1486
|
+
// that the Desktop account must be able to carry. Re-ordering never kicks a live
|
|
1487
|
+
// thread, so there is no affinity clearing and no appliesImmediately here.
|
|
1488
|
+
if (url.pathname === "/api/codex-auth/accounts/priority" && req.method === "PUT") {
|
|
1489
|
+
let parsedBody: unknown;
|
|
1490
|
+
try { parsedBody = await req.json(); } catch { return jsonResponse({ error: "Invalid JSON" }, 400); }
|
|
1491
|
+
if (typeof parsedBody !== "object" || parsedBody === null || Array.isArray(parsedBody)) {
|
|
1492
|
+
return jsonResponse({ error: "body must be an object" }, 400);
|
|
1493
|
+
}
|
|
1494
|
+
const body = parsedBody as { id?: unknown; priority?: unknown };
|
|
1495
|
+
const id = typeof body.id === "string" ? body.id.trim() : "";
|
|
1496
|
+
if (!isCodexAccountPriorityKey(id)) {
|
|
1497
|
+
return jsonResponse({ error: "Invalid account id format" }, 400);
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
let priority = DEFAULT_ACCOUNT_PRIORITY;
|
|
1501
|
+
if (body.priority !== null) {
|
|
1502
|
+
const parsed = parseAccountPriority(body.priority);
|
|
1503
|
+
if (parsed === null) {
|
|
1504
|
+
return jsonResponse({
|
|
1505
|
+
error: `priority must be null or an integer ${MIN_ACCOUNT_PRIORITY}-${MAX_ACCOUNT_PRIORITY}`,
|
|
1506
|
+
}, 400);
|
|
1507
|
+
}
|
|
1508
|
+
priority = parsed;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
const runtimeConfig = getRuntimeConfig(config);
|
|
1512
|
+
const exists = id === MAIN_CODEX_ACCOUNT_ID
|
|
1513
|
+
|| (runtimeConfig.codexAccounts ?? []).some(account => isSelectableCodexPoolAccount(account) && account.id === id);
|
|
1514
|
+
if (!exists) return jsonResponse({ error: "Account not found" }, 404);
|
|
1515
|
+
|
|
1516
|
+
setCodexAccountPriority(runtimeConfig, id, priority);
|
|
1517
|
+
// Both a pin and an order are the operator saying which account to use, so the newer
|
|
1518
|
+
// statement wins. Without this a pin made before any order existed — an ordinary
|
|
1519
|
+
// account switch — would outrank the order forever: it blocks preemption and caps
|
|
1520
|
+
// every eligibility list at its own tier until that account drains or is paused.
|
|
1521
|
+
clearCodexAccountPin(runtimeConfig);
|
|
1522
|
+
saveRuntimeConfig(config, runtimeConfig);
|
|
1523
|
+
return jsonResponse({
|
|
1524
|
+
ok: true,
|
|
1525
|
+
id,
|
|
1526
|
+
priority,
|
|
1527
|
+
activeCodexAccountId: getEffectiveActiveCodexAccountId(runtimeConfig) ?? null,
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1021
1531
|
if (url.pathname === "/api/codex-auth/accounts/pause-exhausted" && req.method === "PUT") {
|
|
1022
1532
|
const runtimeConfig = getRuntimeConfig(config);
|
|
1023
|
-
const result = await pauseExhaustedCodexAccounts(
|
|
1533
|
+
const result = await pauseExhaustedCodexAccounts(
|
|
1534
|
+
runtimeConfig,
|
|
1535
|
+
() => saveRuntimeConfig(config, runtimeConfig),
|
|
1536
|
+
);
|
|
1024
1537
|
const { pausedAccountIds, checkedAccountCount, failedAccountCount } = result;
|
|
1025
1538
|
if (checkedAccountCount === 0 && failedAccountCount > 0) {
|
|
1026
1539
|
return jsonResponse({
|
|
@@ -1030,7 +1543,6 @@ export async function handleCodexAuthAPI(
|
|
|
1030
1543
|
failedAccountCount,
|
|
1031
1544
|
}, 502);
|
|
1032
1545
|
}
|
|
1033
|
-
if (pausedAccountIds.length > 0) saveRuntimeConfig(config, runtimeConfig);
|
|
1034
1546
|
return jsonResponse({
|
|
1035
1547
|
ok: true,
|
|
1036
1548
|
pausedAccountIds,
|
|
@@ -1078,6 +1590,15 @@ export async function handleCodexAuthAPI(
|
|
|
1078
1590
|
if (!exists) return jsonResponse({ error: "Account not found" }, 400);
|
|
1079
1591
|
}
|
|
1080
1592
|
runtimeConfig.activeCodexAccountId = body.accountId ?? undefined;
|
|
1593
|
+
// "Use this account now" outranks selection order until the account is spent:
|
|
1594
|
+
// persisted here rather than in resetCodexRoutingForManualSelection, which is
|
|
1595
|
+
// runtime state only. A null id clears the selection instead of making one, so it
|
|
1596
|
+
// must release the pin rather than record one: pinning the `targetAccountId`
|
|
1597
|
+
// fallback would leave a pin that no effective active account matches, which
|
|
1598
|
+
// `isEffectiveCodexAccountPinned` reports as unpinned while the tier filter still
|
|
1599
|
+
// honours it as a ceiling — invisibly capping the pool at the main account's tier.
|
|
1600
|
+
if (body.accountId == null) clearCodexAccountPin(runtimeConfig);
|
|
1601
|
+
else setCodexAccountPin(runtimeConfig, targetAccountId);
|
|
1081
1602
|
resetCodexRoutingForManualSelection(targetAccountId);
|
|
1082
1603
|
saveRuntimeConfig(config, runtimeConfig);
|
|
1083
1604
|
return jsonResponse({ ok: true, activeCodexAccountId: body.accountId, appliesImmediately: true });
|
|
@@ -1087,6 +1608,13 @@ export async function handleCodexAuthAPI(
|
|
|
1087
1608
|
const runtimeConfig = getRuntimeConfig(config);
|
|
1088
1609
|
return jsonResponse({
|
|
1089
1610
|
activeCodexAccountId: getEffectiveActiveCodexAccountId(runtimeConfig) ?? null,
|
|
1611
|
+
pinned: isEffectiveCodexAccountPinned(runtimeConfig),
|
|
1612
|
+
// Which account carries the pin, not just whether the active one does. Under
|
|
1613
|
+
// round-robin or fill-first the pin caps the tier ceiling at its own tier while the
|
|
1614
|
+
// strategy cursor moves freely inside that tier, so `pinned` alone goes false on a
|
|
1615
|
+
// sibling's turn even though the pin is still suppressing every higher tier. The id
|
|
1616
|
+
// lets a surface mark the account the operator actually chose.
|
|
1617
|
+
pinnedAccountId: pinnedCodexAccountId(runtimeConfig) ?? null,
|
|
1090
1618
|
autoSwitchThreshold: runtimeConfig.autoSwitchThreshold ?? 80,
|
|
1091
1619
|
upstreamFailoverThreshold: runtimeConfig.upstreamFailoverThreshold ?? 3,
|
|
1092
1620
|
accountPoolStrategy: normalizeAccountPoolStrategy(runtimeConfig.accountPoolStrategy),
|
|
@@ -1169,24 +1697,47 @@ export async function handleCodexAuthAPI(
|
|
|
1169
1697
|
if (!accountId) return jsonResponse({ error: "accountId required" }, 400);
|
|
1170
1698
|
|
|
1171
1699
|
try {
|
|
1172
|
-
const
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1700
|
+
const result = await withResetCreditAuth(getRuntimeConfig(config), accountId, async auth => {
|
|
1701
|
+
const linkedSignal = signalWithTimeout(8000, req.signal);
|
|
1702
|
+
let detachBodyAbort = () => {};
|
|
1703
|
+
try {
|
|
1704
|
+
let resp: Response;
|
|
1705
|
+
try {
|
|
1706
|
+
resp = await fetch(
|
|
1707
|
+
"https://chatgpt.com/backend-api/wham/rate-limit-reset-credits",
|
|
1708
|
+
{
|
|
1709
|
+
headers: {
|
|
1710
|
+
Authorization: `Bearer ${auth.accessToken}`,
|
|
1711
|
+
"ChatGPT-Account-Id": auth.chatgptAccountId,
|
|
1712
|
+
},
|
|
1713
|
+
signal: linkedSignal.signal,
|
|
1714
|
+
},
|
|
1715
|
+
);
|
|
1716
|
+
} catch (error) {
|
|
1717
|
+
if (linkedSignal.signal.aborted) {
|
|
1718
|
+
return jsonResponse({ error: "Invalid upstream reset-credit response" }, 502);
|
|
1719
|
+
}
|
|
1720
|
+
throw error;
|
|
1721
|
+
}
|
|
1722
|
+
// Own the response body before the bounded reader attaches. If the client
|
|
1723
|
+
// disconnects in that narrow window, Bun otherwise tears down the native
|
|
1724
|
+
// body off the awaited path and can report an unhandled rejection.
|
|
1725
|
+
detachBodyAbort = cancelBodyOnAbort(resp.body, linkedSignal.signal);
|
|
1726
|
+
if (!resp.ok) {
|
|
1727
|
+
await resp.body?.cancel().catch(() => {});
|
|
1728
|
+
return jsonResponse({ error: `Upstream error ${resp.status}` }, resp.status);
|
|
1729
|
+
}
|
|
1730
|
+
const parsed = await readResetCreditJson(resp, linkedSignal.signal);
|
|
1731
|
+
if (!parsed.ok) {
|
|
1732
|
+
return jsonResponse({ error: "Invalid upstream reset-credit response" }, 502);
|
|
1733
|
+
}
|
|
1734
|
+
return jsonResponse(safeResetCreditsDto(parsed.value));
|
|
1735
|
+
} finally {
|
|
1736
|
+
detachBodyAbort();
|
|
1737
|
+
linkedSignal.cleanup();
|
|
1738
|
+
}
|
|
1739
|
+
});
|
|
1740
|
+
return result.ok ? result.value : result.response;
|
|
1190
1741
|
} catch (e) {
|
|
1191
1742
|
return jsonResponse({ error: e instanceof Error ? e.message : "Reset credit lookup failed" }, 500);
|
|
1192
1743
|
}
|
|
@@ -1195,49 +1746,55 @@ export async function handleCodexAuthAPI(
|
|
|
1195
1746
|
if (url.pathname === "/api/codex-auth/reset-credits/consume" && req.method === "POST") {
|
|
1196
1747
|
const body = (await req.json().catch(() => ({}))) as { accountId?: string };
|
|
1197
1748
|
if (!body.accountId) return jsonResponse({ error: "accountId required" }, 400);
|
|
1749
|
+
const accountId = body.accountId;
|
|
1198
1750
|
|
|
1199
1751
|
try {
|
|
1200
|
-
const
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1752
|
+
const operation = await withResetCreditAuth(getRuntimeConfig(config), accountId, async auth => {
|
|
1753
|
+
const idempotencyKey = crypto.randomUUID();
|
|
1754
|
+
const resp = await fetch(
|
|
1755
|
+
"https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume",
|
|
1756
|
+
{
|
|
1757
|
+
method: "POST",
|
|
1758
|
+
headers: {
|
|
1759
|
+
Authorization: `Bearer ${auth.accessToken}`,
|
|
1760
|
+
"ChatGPT-Account-Id": auth.chatgptAccountId,
|
|
1761
|
+
"Content-Type": "application/json",
|
|
1762
|
+
},
|
|
1763
|
+
body: JSON.stringify({ redeem_request_id: idempotencyKey }),
|
|
1764
|
+
signal: AbortSignal.timeout(10_000),
|
|
1212
1765
|
},
|
|
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));
|
|
1766
|
+
);
|
|
1767
|
+
if (!resp.ok) {
|
|
1768
|
+
await resp.body?.cancel().catch(() => {});
|
|
1769
|
+
return jsonResponse({ error: `Upstream error ${resp.status}` }, resp.status);
|
|
1232
1770
|
}
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1771
|
+
const result = safeResetCreditConsumeDto(await resp.json());
|
|
1772
|
+
// After a successful redeem (or an idempotent already_redeemed), refresh WHAM usage
|
|
1773
|
+
// and return remaining only when that refresh freshly parsed available_count.
|
|
1774
|
+
// Do not fall back to a preserved cached resetCredits (failed/omitted refresh).
|
|
1775
|
+
if (result.code === "reset" || result.code === "already_redeemed") {
|
|
1776
|
+
let freshResetCredits: number | undefined;
|
|
1777
|
+
if (auth.isMain) {
|
|
1778
|
+
({ freshResetCredits } = await fetchMainAccountInfoAttempt(
|
|
1779
|
+
true,
|
|
1780
|
+
1,
|
|
1781
|
+
auth.nativeMainLease,
|
|
1782
|
+
auth.nativeMainSharedClaimHeld === true,
|
|
1783
|
+
));
|
|
1784
|
+
} else {
|
|
1785
|
+
const account = configuredPoolAccount(getRuntimeConfig(config), accountId);
|
|
1786
|
+
({ freshResetCredits } = await fetchPoolAccountQuota(accountId, true, account?.plan));
|
|
1787
|
+
}
|
|
1788
|
+
return jsonResponse({
|
|
1789
|
+
code: result.code,
|
|
1790
|
+
...(typeof freshResetCredits === "number" && Number.isFinite(freshResetCredits)
|
|
1791
|
+
? { remaining: freshResetCredits }
|
|
1792
|
+
: {}),
|
|
1793
|
+
});
|
|
1794
|
+
}
|
|
1795
|
+
return jsonResponse(result);
|
|
1796
|
+
});
|
|
1797
|
+
return operation.ok ? operation.value : operation.response;
|
|
1241
1798
|
} catch (e) {
|
|
1242
1799
|
if (e instanceof PoolQuotaProbeBusyError) {
|
|
1243
1800
|
const response = jsonResponse({ error: "server_busy", code: "server_busy" }, 503);
|
|
@@ -1278,7 +1835,7 @@ export async function handleCodexAuthAPI(
|
|
|
1278
1835
|
const loginOwner: CodexLoginStateRow = { status: "starting", startedAt: Date.now() };
|
|
1279
1836
|
codexAuthLoginState.set(flowId, loginOwner);
|
|
1280
1837
|
try {
|
|
1281
|
-
const { startLoginFlow, getLoginStatus } = await import("../oauth");
|
|
1838
|
+
const { startLoginFlow, getLoginStatus, publicOAuthAuthenticationErrorMessage } = await import("../oauth");
|
|
1282
1839
|
const result = await startLoginFlow("chatgpt", { forceLogin: true });
|
|
1283
1840
|
|
|
1284
1841
|
// Open the browser server-side (same pattern as /api/oauth/login in management-api.ts).
|
|
@@ -1391,6 +1948,8 @@ export async function handleCodexAuthAPI(
|
|
|
1391
1948
|
const latestConfig = getRuntimeConfig(config);
|
|
1392
1949
|
const accounts = latestConfig.codexAccounts ?? [];
|
|
1393
1950
|
const existingIdx = accounts.findIndex(account => account.id === accountId);
|
|
1951
|
+
let pickerVisibilityChanged = false;
|
|
1952
|
+
let newAccountPersistence: PersistNewCodexAccountOutcome | null = null;
|
|
1394
1953
|
const commitConflict = codexAccountPersistenceConflict(
|
|
1395
1954
|
latestConfig,
|
|
1396
1955
|
accountId,
|
|
@@ -1406,22 +1965,21 @@ export async function handleCodexAuthAPI(
|
|
|
1406
1965
|
break;
|
|
1407
1966
|
}
|
|
1408
1967
|
|
|
1409
|
-
|
|
1968
|
+
const credential: CodexAccountCredentials = {
|
|
1410
1969
|
accessToken: cred.access,
|
|
1411
1970
|
refreshToken: cred.refresh,
|
|
1412
1971
|
expiresAt: cred.expires,
|
|
1413
1972
|
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
|
-
}
|
|
1973
|
+
};
|
|
1423
1974
|
|
|
1424
1975
|
if (existingIdx >= 0) {
|
|
1976
|
+
saveCodexAccountCredential(accountId, credential);
|
|
1977
|
+
// A successful reauthentication replaces the credential generation. Do not let a
|
|
1978
|
+
// failed optional WHAM probe make the replacement inherit quota from the old record.
|
|
1979
|
+
if (reauth) clearAccountQuota(accountId);
|
|
1980
|
+
markCodexAccountValidated(accountId, warmup.validatedAt);
|
|
1981
|
+
clearAccountNeedsReauth(accountId);
|
|
1982
|
+
if (quota) setAccountQuotaFromParsed(accountId, quota);
|
|
1425
1983
|
// Keep the pool id stable; refresh display metadata after a successful login/reauth.
|
|
1426
1984
|
accounts[existingIdx] = withCodexAccountLogLabel({
|
|
1427
1985
|
...accounts[existingIdx],
|
|
@@ -1432,18 +1990,60 @@ export async function handleCodexAuthAPI(
|
|
|
1432
1990
|
latestConfig.codexAccounts = accounts;
|
|
1433
1991
|
saveRuntimeConfig(config, latestConfig);
|
|
1434
1992
|
} else {
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1993
|
+
const addedAccount = withCodexAccountLogLabel({ id: accountId, email, plan, isMain: false }, accounts);
|
|
1994
|
+
newAccountPersistence = persistNewCodexAccount(
|
|
1995
|
+
config,
|
|
1996
|
+
latestConfig,
|
|
1997
|
+
addedAccount,
|
|
1998
|
+
{
|
|
1999
|
+
credential,
|
|
2000
|
+
validatedAt: warmup.validatedAt,
|
|
2001
|
+
},
|
|
2002
|
+
);
|
|
2003
|
+
pickerVisibilityChanged = newAccountPersistence.pickerVisibilityChanged;
|
|
1438
2004
|
}
|
|
1439
2005
|
reconcileLiveStateStores();
|
|
1440
|
-
|
|
1441
|
-
|
|
2006
|
+
if (newAccountPersistence?.status === "publication-failed") {
|
|
2007
|
+
markAccountNeedsReauth(accountId);
|
|
2008
|
+
}
|
|
2009
|
+
// A new quota row is generation-gated by live account ownership. Reconcile the
|
|
2010
|
+
// durable config owner first so a partial prior sweep cannot reject this write.
|
|
2011
|
+
if (newAccountPersistence?.status === "committed" && quota) {
|
|
2012
|
+
setAccountQuotaFromParsed(accountId, quota);
|
|
2013
|
+
}
|
|
2014
|
+
const { catalogRefreshPending } = await convergeAccountNamespaceCatalog(
|
|
2015
|
+
latestConfig,
|
|
2016
|
+
pickerVisibilityChanged,
|
|
2017
|
+
convergeCodexCatalog,
|
|
2018
|
+
);
|
|
2019
|
+
if (newAccountPersistence?.status === "publication-failed") {
|
|
2020
|
+
setCodexLoginState(flowId, {
|
|
2021
|
+
status: "error",
|
|
2022
|
+
...codexCredentialPersistenceFailure(accountId, catalogRefreshPending),
|
|
2023
|
+
doneAt: Date.now(),
|
|
2024
|
+
});
|
|
2025
|
+
completed = true;
|
|
2026
|
+
} else {
|
|
2027
|
+
setCodexLoginState(flowId, {
|
|
2028
|
+
status: "done",
|
|
2029
|
+
accountId,
|
|
2030
|
+
email,
|
|
2031
|
+
...(catalogRefreshPending ? { catalogRefreshPending: true } : {}),
|
|
2032
|
+
doneAt: Date.now(),
|
|
2033
|
+
});
|
|
2034
|
+
completed = true;
|
|
2035
|
+
}
|
|
1442
2036
|
}
|
|
1443
2037
|
break;
|
|
1444
2038
|
}
|
|
1445
2039
|
if (st.done && st.error) {
|
|
1446
|
-
setCodexLoginState(flowId, {
|
|
2040
|
+
setCodexLoginState(flowId, {
|
|
2041
|
+
status: "error",
|
|
2042
|
+
// startLoginFlow projects background failures before storing login status, so
|
|
2043
|
+
// fixed actionable OAuth messages retain their type-derived remediation here.
|
|
2044
|
+
error: st.error,
|
|
2045
|
+
doneAt: Date.now(),
|
|
2046
|
+
});
|
|
1447
2047
|
completed = true;
|
|
1448
2048
|
break;
|
|
1449
2049
|
}
|
|
@@ -1461,7 +2061,7 @@ export async function handleCodexAuthAPI(
|
|
|
1461
2061
|
? "Configuration is busy; retry login shortly."
|
|
1462
2062
|
: error instanceof CodexCredentialRefreshBusyError || error instanceof CodexCredentialRefreshStaleError
|
|
1463
2063
|
? "Credential refresh is busy; retry login shortly."
|
|
1464
|
-
:
|
|
2064
|
+
: publicOAuthAuthenticationErrorMessage(error);
|
|
1465
2065
|
setCodexLoginState(flowId, {
|
|
1466
2066
|
status: "error",
|
|
1467
2067
|
error: message,
|
|
@@ -1478,7 +2078,7 @@ export async function handleCodexAuthAPI(
|
|
|
1478
2078
|
} catch (e) {
|
|
1479
2079
|
if (codexAuthLoginState.get(flowId) === loginOwner) codexAuthLoginState.delete(flowId);
|
|
1480
2080
|
const msg = e instanceof Error ? e.message : String(e);
|
|
1481
|
-
if (msg
|
|
2081
|
+
if (msg === "A login for chatgpt is already in progress") {
|
|
1482
2082
|
return jsonResponse({ error: msg, status: "pending" }, 409);
|
|
1483
2083
|
}
|
|
1484
2084
|
if (e instanceof CodexCredentialRefreshBusyError || e instanceof CodexCredentialRefreshStaleError) {
|
|
@@ -1486,7 +2086,8 @@ export async function handleCodexAuthAPI(
|
|
|
1486
2086
|
response.headers.set("Retry-After", "1");
|
|
1487
2087
|
return response;
|
|
1488
2088
|
}
|
|
1489
|
-
|
|
2089
|
+
const { publicOAuthAuthenticationErrorMessage } = await import("../oauth");
|
|
2090
|
+
return jsonResponse({ error: publicOAuthAuthenticationErrorMessage(e) }, 500);
|
|
1490
2091
|
}
|
|
1491
2092
|
}
|
|
1492
2093
|
|
|
@@ -1524,7 +2125,13 @@ export async function handleCodexAuthAPI(
|
|
|
1524
2125
|
const reauthStatus = url.searchParams.get("reauth") === "1";
|
|
1525
2126
|
if (flowId) {
|
|
1526
2127
|
const st = codexAuthLoginState.get(flowId);
|
|
1527
|
-
if (
|
|
2128
|
+
if (
|
|
2129
|
+
!st
|
|
2130
|
+
&& accountId
|
|
2131
|
+
&& !reauthStatus
|
|
2132
|
+
&& !isAccountNeedsReauth(accountId)
|
|
2133
|
+
&& getCodexAccountCredential(accountId)
|
|
2134
|
+
) {
|
|
1528
2135
|
return jsonResponse({ status: "done", accountId });
|
|
1529
2136
|
}
|
|
1530
2137
|
return jsonResponse(st ? { ...st, email: st.email ?? undefined } : { status: "expired" });
|