@bitkyc08/opencodex 2.59.0 → 2.61.0-preview.20260922
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 +64 -0
- package/README.md +28 -1
- package/bin/ocx.mjs +382 -209
- package/gui/dist/assets/App-E64Rzjap.js +50 -0
- package/gui/dist/assets/Tray-_nfzD8k4.js +1 -0
- package/gui/dist/assets/index-DpdfZWMK.js +86 -0
- package/gui/dist/assets/index-_bpvxJu0.css +1 -0
- package/gui/dist/assets/usage-companion-chart-DtoK7T6h.js +1 -0
- package/gui/dist/favicon.png +0 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/stepfun-color.svg +1 -0
- package/package.json +8 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +20 -1
- package/src/adapters/base.ts +23 -0
- package/src/adapters/coding-agent/protocol.ts +36 -6
- package/src/adapters/coding-agent/turn.ts +32 -4
- package/src/adapters/command-code.ts +52 -4
- package/src/adapters/cursor/catalog.ts +51 -7
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/protobuf-request.ts +6 -3
- package/src/adapters/cursor/request-builder.ts +20 -4
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +14 -3
- package/src/adapters/declaration-carrier.ts +45 -0
- package/src/adapters/devin/cloud-direct/chat.ts +3 -1
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
- package/src/adapters/devin.ts +125 -36
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +34 -7
- package/src/adapters/google-errors.ts +7 -1
- package/src/adapters/google-http.ts +49 -10
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +66 -11
- package/src/adapters/image.ts +4 -1
- package/src/adapters/input-media-guard.ts +21 -9
- package/src/adapters/kiro/usage.ts +3 -2
- package/src/adapters/kiro-tool-fallback.ts +1 -1
- package/src/adapters/ollama-native.ts +6 -0
- package/src/adapters/openai-chat/developer-role.ts +61 -0
- package/src/adapters/openai-chat/messages.ts +46 -27
- package/src/adapters/openai-chat/parallel-tool-calls.ts +32 -0
- package/src/adapters/openai-chat/passthrough.ts +33 -9
- package/src/adapters/openai-chat/reasoning-wire.ts +89 -0
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +23 -58
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +17 -3
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/adapters/registry.ts +3 -2
- package/src/adapters/run-turn-queue.ts +178 -29
- package/src/adapters/xai-web-search.ts +16 -1
- package/src/bridge/errors.ts +8 -2
- package/src/bridge/response-json.ts +9 -1
- package/src/bridge/sse.ts +13 -151
- package/src/chat/inbound.ts +141 -5
- package/src/claude/desktop-3p.ts +7 -1
- package/src/claude/desktop-first-party.ts +183 -0
- package/src/claude/desktop-gateway-state.ts +41 -0
- package/src/claude/inbound-content-options.ts +6 -0
- package/src/claude/inbound.ts +32 -6
- package/src/claude/intercept/connect-proxy.ts +179 -0
- package/src/claude/intercept/listener.ts +122 -0
- package/src/claude/intercept/local-ca.ts +298 -0
- package/src/claude/intercept/runtime.ts +98 -0
- package/src/claude/intercept/settings.ts +189 -0
- package/src/cli/access.ts +87 -0
- package/src/cli/account-auth.ts +19 -0
- package/src/cli/account-extended.ts +4 -4
- package/src/cli/capabilities.ts +31 -0
- package/src/cli/claude-desktop.ts +206 -16
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/companion.ts +56 -0
- package/src/cli/dispatch.ts +46 -7
- package/src/cli/doctor.ts +28 -9
- package/src/cli/ensure-desired-integrations.ts +43 -5
- package/src/cli/help.ts +7 -9
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +203 -59
- package/src/cli/init.ts +8 -0
- package/src/cli/integrations.ts +7 -1
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/cli/registry.ts +41 -2
- package/src/cli/resolve.ts +230 -0
- package/src/cli/root.ts +24 -1
- package/src/cli/start-ownership-publication.ts +56 -0
- package/src/cli/status-probes.ts +2 -18
- package/src/cli/status.ts +62 -0
- package/src/cli/stop-report.ts +143 -0
- package/src/cli/uninstall-plan.ts +9 -0
- package/src/client/machine-api.ts +2 -2
- package/src/client/machine-listener.ts +4 -7
- package/src/client/runtime.ts +26 -2
- package/src/clients/aside-profiles.ts +4 -0
- package/src/clients/config-export/zcode-store.ts +157 -0
- package/src/clients/config-export.ts +36 -0
- package/src/codex/account-store.ts +65 -0
- package/src/codex/app-server-processes.ts +72 -40
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/login-flow.ts +6 -1
- package/src/codex/auth-api/pool-quota-probe.ts +30 -7
- package/src/codex/autostart-health.ts +28 -0
- package/src/codex/catalog/build-entries.ts +2 -2
- package/src/codex/catalog/effort.ts +3 -3
- package/src/codex/catalog/gather-capture.ts +21 -2
- package/src/codex/catalog/model-hints.ts +29 -28
- package/src/codex/catalog/parsing.ts +7 -0
- package/src/codex/catalog/provider-models.ts +43 -17
- package/src/codex/catalog/retained-sync.ts +24 -28
- package/src/codex/catalog/routed-gather.ts +19 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/convergence.ts +2 -2
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +31 -166
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/inject/config-toml.ts +41 -6
- package/src/codex/inject/paginated-openai-compat.ts +90 -0
- package/src/codex/inject.ts +18 -15
- package/src/codex/injected-marker.ts +18 -0
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/main-account.ts +6 -0
- package/src/codex/model-cache.ts +99 -6
- package/src/codex/model-entitlement-admission.ts +59 -0
- package/src/codex/model-entitlements.ts +116 -54
- package/src/codex/native-main-admission.ts +83 -0
- package/src/codex/observed-model-denials.ts +101 -8
- package/src/codex/prompt-text-probe.ts +9 -6
- package/src/codex/routing/health-store.ts +39 -0
- package/src/codex/routing/selection.ts +37 -1
- package/src/codex/routing.ts +12 -42
- package/src/codex/shim-templates.ts +29 -3
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +22 -4
- package/src/combos/failover.ts +3 -0
- package/src/companion/settings.ts +132 -0
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/atomic-write.ts +117 -5
- package/src/config/diagnostics.ts +22 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +52 -7
- package/src/config/process-state.ts +1 -1
- package/src/config/proxy-env.ts +8 -2
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +31 -1
- package/src/config/schema/leaf-validators.ts +59 -0
- package/src/config.ts +3 -3
- package/src/generated/compatibility-version.json +604 -280
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/config-io.ts +44 -10
- package/src/integrations/merge.ts +120 -13
- package/src/integrations/mutation-plan.ts +921 -0
- package/src/integrations/registry.ts +38 -0
- package/src/integrations/state.ts +78 -45
- package/src/integrations/target.ts +208 -0
- package/src/integrations/writer.ts +134 -110
- package/src/lab/conformance/fixture-provider.ts +5 -0
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/browser-launch-notice.ts +59 -0
- package/src/lib/bun-runtime.ts +6 -2
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +63 -0
- package/src/lib/errors.ts +79 -0
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/open-url.ts +51 -7
- package/src/lib/package-tree-integrity.ts +2 -1
- package/src/lib/package-version.ts +8 -0
- package/src/lib/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-egress.ts +310 -0
- package/src/lib/provider-outbound.ts +109 -16
- package/src/lib/proxy-env.ts +82 -7
- package/src/lib/request-execution-budget.ts +72 -0
- package/src/lib/request-failure-attribution.ts +183 -0
- package/src/lib/request-failure-model.ts +236 -0
- package/src/lib/request-resend-gate.ts +138 -0
- package/src/lib/socks5-fetch.ts +136 -26
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +218 -27
- package/src/lib/standalone.ts +16 -0
- package/src/lib/upstream-retry.ts +167 -16
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/lib/winsw.ts +2 -2
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +1 -0
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +27 -108
- package/src/oauth/login-cli.ts +80 -29
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/providers/api-key-resolve.ts +133 -0
- package/src/providers/api-key-selection.ts +5 -1
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/key-failover.ts +97 -19
- package/src/providers/key-store.ts +34 -110
- package/src/providers/model-rename-fields.ts +147 -0
- package/src/providers/model-rename-migration.ts +179 -38
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/vendor-probes-key.ts +38 -23
- package/src/providers/reasoning-metadata.ts +43 -18
- package/src/providers/registry/entries-core.ts +47 -20
- package/src/providers/registry/entries-extended.ts +63 -4
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +56 -10
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/providers/xai-transport.ts +12 -1
- package/src/reasoning-effort.ts +8 -0
- package/src/responses/apply-patch-envelope.ts +0 -12
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- package/src/responses/function-call-compat.ts +38 -1
- package/src/responses/inline-document.ts +65 -0
- package/src/responses/input-media.ts +42 -8
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/muse-tool-name-alias.ts +19 -0
- package/src/responses/parser-content.ts +8 -2
- package/src/responses/parser-tools.ts +3 -0
- package/src/responses/parser.ts +3 -1
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/schema.ts +3 -0
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +108 -117
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- package/src/server/admission-model-scope.ts +219 -0
- package/src/server/audio-live.ts +9 -3
- package/src/server/audio-upstream.ts +18 -0
- package/src/server/auth-cors.ts +29 -0
- package/src/server/chat-completions.ts +60 -4
- package/src/server/chat-native.ts +19 -4
- package/src/server/claude-messages.ts +61 -20
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/grok-responses-snapshot-repair.ts +113 -11
- package/src/server/gui-freshness.ts +103 -0
- package/src/server/gui-static.ts +7 -9
- package/src/server/images.ts +59 -6
- package/src/server/index/claude-intercept-lifecycle.ts +49 -0
- package/src/server/index/serve-options.ts +142 -39
- package/src/server/index/spend-ledger-lifecycle.ts +92 -0
- package/src/server/index/startup-warnings.ts +24 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +34 -38
- package/src/server/lifecycle.ts +4 -4
- package/src/server/live-call-bindings.ts +6 -0
- package/src/server/live.ts +88 -3
- package/src/server/management/agent-settings-routes.ts +121 -36
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/companion-routes.ts +77 -0
- package/src/server/management/config-routes.ts +18 -2
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/logs-usage-routes.ts +19 -0
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/native-integration-routes.ts +103 -6
- package/src/server/management/oauth-account-routes.ts +45 -7
- package/src/server/management/route-registry.ts +20 -0
- package/src/server/management/shared.ts +28 -4
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management/usage-timeline-routes.ts +44 -0
- package/src/server/management-api.ts +10 -9
- package/src/server/management-auth.ts +15 -1
- package/src/server/proxy-liveness.ts +75 -0
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +138 -12
- package/src/server/request-log-failure-attribution.ts +99 -0
- package/src/server/request-log.ts +169 -2
- package/src/server/request-metrics.ts +298 -0
- package/src/server/responses/adapter-continuation.ts +3 -3
- package/src/server/responses/adapter-dispatch.ts +11 -6
- package/src/server/responses/codex-ws-wire.ts +34 -8
- package/src/server/responses/combo-stream-preflight.ts +168 -6
- package/src/server/responses/compact.ts +43 -10
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +8 -3
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +6 -12
- package/src/server/responses/core-opaque-recovery.ts +91 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- package/src/server/responses/fetch-helpers.ts +124 -8
- package/src/server/responses/input-admission.ts +10 -0
- package/src/server/responses/passthrough-delivery.ts +45 -15
- package/src/server/responses/passthrough-dispatch.ts +215 -44
- package/src/server/responses/passthrough-error.ts +27 -8
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +109 -18
- package/src/server/responses/request-send-budget.ts +17 -1
- package/src/server/responses/request-sidecar-auth.ts +1 -1
- package/src/server/responses/request-transport.ts +26 -6
- package/src/server/responses/reset-replay.ts +108 -0
- package/src/server/responses/run-turn-execution.ts +25 -3
- package/src/server/responses/sidecar-execution.ts +17 -2
- package/src/server/responses/ws-upstream.ts +14 -27
- package/src/server/responses-custom-tool-repair.ts +27 -54
- package/src/server/responses-request-tool-scope.ts +214 -0
- package/src/server/responses-undeclared-tool-guard.ts +35 -2
- package/src/server/search.ts +25 -1
- package/src/server/sse-payload-rewrite.ts +1 -1
- package/src/server/usage-ledger-retention.ts +73 -0
- package/src/service/cli.ts +48 -2
- package/src/service/health.ts +3 -2
- package/src/service/install-state-contract.d.mts +27 -0
- package/src/service/install-state-contract.mjs +34 -0
- package/src/service/launchd.ts +1 -1
- package/src/service/orchestration.ts +2 -4
- package/src/service/ownership-compatibility.ts +164 -0
- package/src/service/ownership-mutation-lease.d.mts +32 -0
- package/src/service/ownership-mutation-lease.mjs +211 -0
- package/src/service/repair.ts +45 -1
- package/src/service/state-lock.ts +269 -0
- package/src/service/state-record.d.mts +36 -0
- package/src/service/state-record.mjs +138 -0
- package/src/service/state.ts +582 -68
- package/src/service/windows-taskxml.ts +11 -10
- package/src/service.ts +7 -3
- package/src/tray/windows-tray.ps1 +156 -4
- package/src/types/config.ts +48 -3
- package/src/types/provider.ts +91 -0
- package/src/types/request.ts +30 -2
- package/src/types/tools.ts +33 -0
- package/src/types.ts +4 -0
- package/src/update/index.ts +207 -63
- package/src/update/job.ts +9 -5
- package/src/update/ownership-transaction.ts +47 -0
- package/src/update/restart-ownership.ts +54 -0
- package/src/update/runtime-ownership.d.mts +40 -0
- package/src/update/runtime-ownership.mjs +122 -0
- package/src/usage/attempt-delivery.ts +198 -0
- package/src/usage/cache-diagnostic.ts +305 -0
- package/src/usage/failure-fingerprint.ts +118 -0
- package/src/usage/failure-projection-cache.ts +174 -0
- package/src/usage/failure-projection.ts +174 -0
- package/src/usage/ledger-retention.ts +165 -0
- package/src/usage/log.ts +126 -79
- package/src/usage/request-outcome.ts +150 -0
- package/src/usage/retention-contract.ts +28 -0
- package/src/usage/summary.ts +2 -2
- package/src/usage/telemetry-contract.ts +237 -0
- package/src/usage/timeline.ts +236 -0
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/alpha-search.ts +21 -1
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/gui/dist/assets/index-C5IebErG.js +0 -136
- package/gui/dist/assets/index-OESInAjC.css +0 -1
|
@@ -48,8 +48,55 @@ const OBSERVED_DENIAL_TTL_MS = 6 * 60 * 60_000;
|
|
|
48
48
|
/** Bounded like the roster cache: pool size times flagship count, with room to spare. */
|
|
49
49
|
const OBSERVED_DENIAL_MAX_ENTRIES = 512;
|
|
50
50
|
|
|
51
|
-
/**
|
|
52
|
-
|
|
51
|
+
/**
|
|
52
|
+
* One entry of refusal evidence.
|
|
53
|
+
*
|
|
54
|
+
* `generation` is the credential generation the refusal was observed under (#4952). Evidence
|
|
55
|
+
* is about a CREDENTIAL, not an account id: reauthenticating the same internal account can
|
|
56
|
+
* swap the subscription underneath it, so a refusal from the previous credential says nothing
|
|
57
|
+
* about the replacement. Carrying the generation lets a reader ignore superseded evidence and
|
|
58
|
+
* lets a late reply from an older generation be refused rather than applied.
|
|
59
|
+
*
|
|
60
|
+
* It is OPTIONAL because not every account that can be refused has one. A `main-pool` context
|
|
61
|
+
* — the stored main login taking part in rotation — carries a real `accountId` and a
|
|
62
|
+
* `writerGeneration`, but no pool credential generation, because its credential lives in
|
|
63
|
+
* `auth.json` rather than the pool store. Dropping its evidence would have silently reverted
|
|
64
|
+
* #4906 for that account: the pool would re-send the same model to the login that just refused
|
|
65
|
+
* it, on every request, forever. An entry without a generation is account-scoped, and the
|
|
66
|
+
* generation fences below simply do not apply to it. This is the same rule the quota writer
|
|
67
|
+
* already uses in `core-codex-account.ts`, where an absent credential generation skips the
|
|
68
|
+
* liveness check instead of discarding the write.
|
|
69
|
+
*/
|
|
70
|
+
interface ObservedDenial {
|
|
71
|
+
expiresAt: number;
|
|
72
|
+
generation?: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** `accountId\u0000modelId` -> entry. Insertion order is the eviction order. */
|
|
76
|
+
const observedDenials = new Map<string, ObservedDenial>();
|
|
77
|
+
|
|
78
|
+
/** Whether `generation` is still the account's live credential. */
|
|
79
|
+
type GenerationLiveCheck = (accountId: string, generation: number) => boolean;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Opens one liveness check.
|
|
83
|
+
*
|
|
84
|
+
* A FACTORY rather than a bare predicate because the production implementation reads the
|
|
85
|
+
* credential store, and a lookup can ask about several accounts. Loading once per lookup and
|
|
86
|
+
* closing over that snapshot is the shape `loadCodexAccountRecordSnapshot` exists for; a bare
|
|
87
|
+
* predicate would reload and reparse the whole store per row, on the request path.
|
|
88
|
+
*
|
|
89
|
+
* Injected rather than imported at module scope so this stays a leaf module: `account-store`
|
|
90
|
+
* reads the credential file, and a unit test of this map should not have to stand one up.
|
|
91
|
+
*/
|
|
92
|
+
type GenerationLiveCheckFactory = () => GenerationLiveCheck;
|
|
93
|
+
|
|
94
|
+
let beginGenerationLiveCheck: GenerationLiveCheckFactory = () => () => true;
|
|
95
|
+
|
|
96
|
+
/** Wire the liveness predicate. Called once at startup; tests substitute their own. */
|
|
97
|
+
export function setObservedDenialGenerationCheck(begin: GenerationLiveCheckFactory): void {
|
|
98
|
+
beginGenerationLiveCheck = begin;
|
|
99
|
+
}
|
|
53
100
|
|
|
54
101
|
function denialKey(accountId: string, modelId: string): string {
|
|
55
102
|
return `${accountId}\u0000${modelId}`;
|
|
@@ -72,12 +119,24 @@ function modelIdOfDenialKey(key: string): string {
|
|
|
72
119
|
export function recordObservedCodexModelDenial(
|
|
73
120
|
accountId: string,
|
|
74
121
|
modelId: string,
|
|
122
|
+
generation: number | undefined,
|
|
75
123
|
now = Date.now(),
|
|
76
124
|
): void {
|
|
125
|
+
// A refusal dispatched under generation G can arrive after G+1 has been saved. Reject it
|
|
126
|
+
// BEFORE touching the map at all, not merely when this key already holds newer evidence:
|
|
127
|
+
// with no entry for this key the stale row would otherwise be inserted, and at the entry
|
|
128
|
+
// bound it would evict a valid row that nothing can restore (#4952).
|
|
129
|
+
if (generation !== undefined && !beginGenerationLiveCheck()(accountId, generation)) return;
|
|
77
130
|
const key = denialKey(accountId, modelId);
|
|
131
|
+
const existing = observedDenials.get(key);
|
|
132
|
+
// Second fence, for the window where the replacement credential has been dispatched but the
|
|
133
|
+
// store read above still answers live. Both sides must name a generation to be comparable;
|
|
134
|
+
// an account-scoped entry is not older or newer than a credential-scoped one.
|
|
135
|
+
if (existing !== undefined && existing.generation !== undefined && generation !== undefined
|
|
136
|
+
&& existing.generation > generation) return;
|
|
78
137
|
// Delete before set so the refreshed entry moves to the back of the eviction order.
|
|
79
138
|
observedDenials.delete(key);
|
|
80
|
-
observedDenials.set(key, now + OBSERVED_DENIAL_TTL_MS);
|
|
139
|
+
observedDenials.set(key, { expiresAt: now + OBSERVED_DENIAL_TTL_MS, generation });
|
|
81
140
|
while (observedDenials.size > OBSERVED_DENIAL_MAX_ENTRIES) {
|
|
82
141
|
const oldest = observedDenials.keys().next();
|
|
83
142
|
if (oldest.done) break;
|
|
@@ -91,8 +150,20 @@ export function recordObservedCodexModelDenial(
|
|
|
91
150
|
* A success is newer and stronger evidence than the refusal that preceded it: whatever the
|
|
92
151
|
* entitlement was when upstream refused, it is not that now.
|
|
93
152
|
*/
|
|
94
|
-
export function clearObservedCodexModelDenial(
|
|
95
|
-
|
|
153
|
+
export function clearObservedCodexModelDenial(
|
|
154
|
+
accountId: string,
|
|
155
|
+
modelId: string,
|
|
156
|
+
generation: number | undefined,
|
|
157
|
+
): void {
|
|
158
|
+
const key = denialKey(accountId, modelId);
|
|
159
|
+
const existing = observedDenials.get(key);
|
|
160
|
+
if (!existing) return;
|
|
161
|
+
// The mirror of the write fence: a success dispatched under G arriving after G+1 was
|
|
162
|
+
// refused must not clear the replacement's evidence (#4952). Equal generations clear,
|
|
163
|
+
// because that is the ordinary "this account just served this model" case.
|
|
164
|
+
if (existing.generation !== undefined && generation !== undefined
|
|
165
|
+
&& existing.generation > generation) return;
|
|
166
|
+
observedDenials.delete(key);
|
|
96
167
|
}
|
|
97
168
|
|
|
98
169
|
/**
|
|
@@ -119,19 +190,41 @@ export function forgetObservedCodexModelDenialsForAccount(accountId: string | nu
|
|
|
119
190
|
export function observedDeniedCodexAccountIdsForModel(
|
|
120
191
|
modelId: string | undefined,
|
|
121
192
|
now = Date.now(),
|
|
193
|
+
options: { excludeAccountIds?: ReadonlySet<string> } = {},
|
|
122
194
|
): ReadonlySet<string> | undefined {
|
|
123
195
|
if (!modelId) return undefined;
|
|
124
196
|
const denied = new Set<string>();
|
|
125
|
-
|
|
126
|
-
|
|
197
|
+
// Opened lazily and at most once: a lookup that matches no credential-scoped row must not
|
|
198
|
+
// read the credential store at all.
|
|
199
|
+
let isLive: GenerationLiveCheck | undefined;
|
|
200
|
+
for (const [key, entry] of [...observedDenials]) {
|
|
201
|
+
if (entry.expiresAt <= now) {
|
|
127
202
|
observedDenials.delete(key);
|
|
128
203
|
continue;
|
|
129
204
|
}
|
|
130
|
-
|
|
205
|
+
// Model and the caller's exclusion fence FIRST. The issue asks for identity validation
|
|
206
|
+
// after the exclusion read, and an excluded account — a draining profile switch, or a
|
|
207
|
+
// request-owned credential — must produce no credential-store read on its behalf.
|
|
208
|
+
if (modelIdOfDenialKey(key) !== modelId) continue;
|
|
209
|
+
const accountId = accountIdOfDenialKey(key);
|
|
210
|
+
if (options.excludeAccountIds?.has(accountId)) continue;
|
|
211
|
+
if (entry.generation !== undefined) {
|
|
212
|
+
isLive ??= beginGenerationLiveCheck();
|
|
213
|
+
// Superseded evidence stops denying the replacement. It is SKIPPED, not deleted: the
|
|
214
|
+
// predicate cannot tell "this account reauthenticated" from "the credential store could
|
|
215
|
+
// not be read", and deleting on the second would throw away valid evidence that a
|
|
216
|
+
// transient read failure was never entitled to touch. Skipping already delivers the
|
|
217
|
+
// routing outcome the issue asks for, on every read, without depending on the
|
|
218
|
+
// conditional account-wide forget that cannot run when no roster was ever cached.
|
|
219
|
+
// Superseded rows still leave by TTL, by eviction, and by the account-wide forget.
|
|
220
|
+
if (!isLive(accountId, entry.generation)) continue;
|
|
221
|
+
}
|
|
222
|
+
denied.add(accountId);
|
|
131
223
|
}
|
|
132
224
|
return denied.size > 0 ? denied : undefined;
|
|
133
225
|
}
|
|
134
226
|
|
|
135
227
|
export function resetObservedCodexModelDenialsForTests(): void {
|
|
136
228
|
observedDenials.clear();
|
|
229
|
+
beginGenerationLiveCheck = () => () => true;
|
|
137
230
|
}
|
|
@@ -75,7 +75,7 @@ const UNMAPPED_LAYER_IDS = [
|
|
|
75
75
|
// The Rust source names a <git_attribution> marker pair, but a world-state section is
|
|
76
76
|
// DIFF-rendered: it emits nothing on a turn where its state has not changed. Live
|
|
77
77
|
// `codex debug prompt-input` (codex-cli 0.145.0, 32978 bytes) showed no such block and
|
|
78
|
-
// no attribution text. Listing the id here reports "
|
|
78
|
+
// no attribution text. Listing the id here reports "unmapped" honestly instead of
|
|
79
79
|
// claiming a tag this extractor has never actually matched - the same mistake the
|
|
80
80
|
// header above records for permissions.
|
|
81
81
|
"git-attribution",
|
|
@@ -85,7 +85,7 @@ export interface LayerText {
|
|
|
85
85
|
/** Rendered text, when this layer produced a section on the probed turn. */
|
|
86
86
|
text: string | null;
|
|
87
87
|
/** Why the text is absent, when it is. */
|
|
88
|
-
reason: "ok" | "empty-source" | "not-rendered" | "not-exposed" | "unavailable";
|
|
88
|
+
reason: "ok" | "empty-source" | "not-rendered" | "not-exposed" | "unmapped" | "unavailable";
|
|
89
89
|
bytes: number;
|
|
90
90
|
/**
|
|
91
91
|
* `expanded` is text Codex sends as written. `template` is a catalog
|
|
@@ -103,7 +103,7 @@ export interface LayerText {
|
|
|
103
103
|
|
|
104
104
|
/**
|
|
105
105
|
* Why the base prompt is or is not readable, at the granularity a reader can act
|
|
106
|
-
* on. `LayerText.reason` has
|
|
106
|
+
* on. `LayerText.reason` has six coarse values and cannot express any of this,
|
|
107
107
|
* which is why the detailed answer travels on its own record.
|
|
108
108
|
*/
|
|
109
109
|
export type BasePromptReason =
|
|
@@ -374,7 +374,7 @@ function readBasePrompt(codexHome: string): BasePromptText {
|
|
|
374
374
|
/**
|
|
375
375
|
* Project the base prompt onto the legacy `base-instructions` layer slot.
|
|
376
376
|
*
|
|
377
|
-
* The slot is lossy by construction -
|
|
377
|
+
* The slot is lossy by construction - six coarse reasons, no renderer that reads
|
|
378
378
|
* `representation` - so it carries only what it can carry honestly: published text
|
|
379
379
|
* when the source is text Codex sends, and otherwise no text at all. A template is
|
|
380
380
|
* deliberately NOT `ok` here, because the dialog labels every `ok` layer "Text
|
|
@@ -944,9 +944,12 @@ export async function probePromptText(
|
|
|
944
944
|
|
|
945
945
|
// Layers whose rendered tag we have not confirmed against live output. Leaving
|
|
946
946
|
// them absent made the GUI fall through to "unavailable", which claims the probe
|
|
947
|
-
// failed when it succeeded.
|
|
947
|
+
// failed when it succeeded. Keep this distinct from the base prompt's
|
|
948
|
+
// "not-exposed", which is confirmed to travel outside the printable message
|
|
949
|
+
// list - reusing it showed a base-prompt-specific explanation for unrelated
|
|
950
|
+
// layers.
|
|
948
951
|
for (const id of UNMAPPED_LAYER_IDS) {
|
|
949
|
-
layers[id] ??= { text: null, reason: "
|
|
952
|
+
layers[id] ??= { text: null, reason: "unmapped", bytes: 0 };
|
|
950
953
|
}
|
|
951
954
|
return { ok: true, codexHome, layers, base, ...(reportedRuntime ? { runtime: reportedRuntime } : {}) };
|
|
952
955
|
}
|
|
@@ -350,6 +350,45 @@ export function deleteAccountHealth(accountId: string): void {
|
|
|
350
350
|
upstreamHealth.delete(accountId);
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
+
export function carriesQuotaRefusal(health: CodexUpstreamHealth | undefined): boolean {
|
|
354
|
+
return health?.lastFailureStatus === 429 || health?.lastFailureStatus === 402;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Has this account refused a request on quota without serving one since?
|
|
359
|
+
*
|
|
360
|
+
* Thread affinity is a prompt-cache optimization and every rule around it is a preference:
|
|
361
|
+
* `autoSwitchThreshold` is a hint that an account is getting busy, and `pool.cacheAffinity`
|
|
362
|
+
* deliberately raises that bar further. A refusal is not a preference, and once the account has
|
|
363
|
+
* told THIS thread it cannot serve, the binding has nothing left to optimize.
|
|
364
|
+
*
|
|
365
|
+
* The distinction matters because the cooldown a 429 writes is deliberately short. A reset
|
|
366
|
+
* announcement is advisory — plan quota routinely frees up before the advertised instant — so
|
|
367
|
+
* {@link CODEX_MAX_RESET_DERIVED_COOLDOWN_MS} caps it at 15 minutes. The five-hour window that
|
|
368
|
+
* announcement describes is not capped, so an account whose burst window is spent looks
|
|
369
|
+
* selectable again long before it is. For an unbound request that is correct: going back to find
|
|
370
|
+
* out is how the pool learns the window moved. For a BOUND thread it is a loop with no exit —
|
|
371
|
+
* the cooldown lapses, the account still scores lowest on the only window this proxy has a
|
|
372
|
+
* reading for (its weekly bar, untouched by a burst limit), the thread rebinds, and earns the
|
|
373
|
+
* identical 429. Cleared affinity does not help: the next request re-derives the same choice.
|
|
374
|
+
* From the Codex side that reads exactly as reported — a new session rotates normally while an
|
|
375
|
+
* existing one is locked to an exhausted account until the proxy is restarted, because a restart
|
|
376
|
+
* is the only thing that drops the binding and the stale health together.
|
|
377
|
+
*
|
|
378
|
+
* `lastFailureStatus` is the right evidence because of when it ends: {@link preservedCooldownFields}
|
|
379
|
+
* strips it from every recovery write, so it survives exactly until the account actually serves a
|
|
380
|
+
* request again. Nothing here blocks that — selection is untouched, so unbound traffic still probes
|
|
381
|
+
* the account and the first success releases every thread this refused.
|
|
382
|
+
*
|
|
383
|
+
* Scope follows where the refusal was recorded. An account-wide throttle lands in
|
|
384
|
+
* `upstreamHealth` and releases every lane; a reset-derived refusal lands against one native
|
|
385
|
+
* quota group, so a spent Spark window still cannot displace the same thread's Terra binding.
|
|
386
|
+
*/
|
|
387
|
+
export function hasUnrecoveredCodexQuotaRefusal(accountId: string, quotaScope?: CodexQuotaScope): boolean {
|
|
388
|
+
if (carriesQuotaRefusal(getAccountHealth(accountId))) return true;
|
|
389
|
+
return quotaScope !== undefined && carriesQuotaRefusal(scopedHealthFor(accountId, quotaScope));
|
|
390
|
+
}
|
|
391
|
+
|
|
353
392
|
export function listScopedHealthEntries(accountId: string): Array<[CodexQuotaScope, CodexUpstreamHealth]> {
|
|
354
393
|
return [...(quotaScopedHealth.get(accountId) ?? [])];
|
|
355
394
|
}
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
dropSpentCredentialFailure,
|
|
23
23
|
getAccountHealth,
|
|
24
24
|
getCodexQuotaHealthSnapshot,
|
|
25
|
+
hasUnrecoveredCodexQuotaRefusal,
|
|
25
26
|
isCodexAccountSoftAvoided,
|
|
26
27
|
isCodexQuotaAvoided,
|
|
27
28
|
isIndependentCodexQuotaScope,
|
|
@@ -276,6 +277,40 @@ export function isCacheAffinityEnabled(config: OcxConfig): boolean {
|
|
|
276
277
|
return config.pool?.cacheAffinity !== false;
|
|
277
278
|
}
|
|
278
279
|
|
|
280
|
+
/**
|
|
281
|
+
* Whether quota may retire shared state while cache affinity is active.
|
|
282
|
+
*
|
|
283
|
+
* A threshold crossing is a hint that an account is getting busy, not evidence it cannot
|
|
284
|
+
* serve — the same bar {@link mayRebindAffinityForQuota} applies to a live binding. Shared
|
|
285
|
+
* state held across a model detour gets that exhaustion boundary for the same reason: the
|
|
286
|
+
* detour is request-scoped, so retiring the binding over a hint pays a cold prefix for
|
|
287
|
+
* nothing. New/unbound selection still reads {@link hasCodexQuotaHeadroom}; only
|
|
288
|
+
* preservation of an existing shared selection or thread binding qualifies here. Like the
|
|
289
|
+
* live-binding rule, the configured threshold plays no role once retention applies: a
|
|
290
|
+
* genuinely exhausted (>=100%) account releases even with threshold switching disabled,
|
|
291
|
+
* while the fallback above keeps a disabled threshold's "never drained on quota alone".
|
|
292
|
+
*/
|
|
293
|
+
export function hasCodexSharedStateQuotaHeadroom(
|
|
294
|
+
config: OcxConfig,
|
|
295
|
+
accountId: string,
|
|
296
|
+
quotaScope: CodexQuotaScope | undefined,
|
|
297
|
+
selectionOptions?: CodexAccountUsabilityOptions,
|
|
298
|
+
now: number = Date.now(),
|
|
299
|
+
): boolean {
|
|
300
|
+
if (
|
|
301
|
+
!isCacheAffinityEnabled(config)
|
|
302
|
+
|| accountPoolStrategyForScope(config, quotaScope) !== "quota"
|
|
303
|
+
) {
|
|
304
|
+
return hasCodexQuotaHeadroom(config, accountId, selectionOptions, now);
|
|
305
|
+
}
|
|
306
|
+
const usage = computeCodexUsageScore(
|
|
307
|
+
getAccountQuota(accountId),
|
|
308
|
+
getPoolAccountPlanForSelection(config, accountId, selectionOptions),
|
|
309
|
+
now,
|
|
310
|
+
);
|
|
311
|
+
return isUnknownUsage(usage) || usage < 100;
|
|
312
|
+
}
|
|
313
|
+
|
|
279
314
|
/** Earliest future shared short/weekly reset; missing evidence and ties use usage order. */
|
|
280
315
|
export function pickResetFirstCodexAccount(
|
|
281
316
|
config: OcxConfig,
|
|
@@ -726,7 +761,8 @@ export function isHealthySharedCodexSelection(
|
|
|
726
761
|
selectionOptions: CodexAccountUsabilityOptions | undefined,
|
|
727
762
|
): boolean {
|
|
728
763
|
return isCodexAccountSelectable(config, accountId, now, quotaScope, selectionOptions)
|
|
729
|
-
&&
|
|
764
|
+
&& hasCodexSharedStateQuotaHeadroom(config, accountId, quotaScope, selectionOptions, now)
|
|
765
|
+
&& !hasUnrecoveredCodexQuotaRefusal(accountId, quotaScope)
|
|
730
766
|
&& !shouldFailover(config, accountId, now);
|
|
731
767
|
}
|
|
732
768
|
|
package/src/codex/routing.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { saveConfigPreservingClaudeCode } from "../config";
|
|
2
|
-
import { isCodexAccountGenerationLive } from "./account-store";
|
|
2
|
+
import { isCodexAccountGenerationLive, registerCodexRefreshGenerationHandoff } from "./account-store";
|
|
3
|
+
import { handOffThreadAffinityGeneration } from "./routing/thread-affinity";
|
|
3
4
|
import { codexAccountLogLabel } from "./account-label";
|
|
4
5
|
import { isCodexAccountPaused } from "./account-pause";
|
|
5
6
|
import { clearCodexAccountPin, pinnedCodexAccountId } from "./account-priority";
|
|
@@ -27,6 +28,7 @@ import {
|
|
|
27
28
|
type CodexUpstreamOutcomeMeta,
|
|
28
29
|
} from "./routing/cooldown-math";
|
|
29
30
|
import {
|
|
31
|
+
carriesQuotaRefusal,
|
|
30
32
|
codexPoolKeyForScope,
|
|
31
33
|
codexQuotaScopeForModel,
|
|
32
34
|
deleteAccountHealth,
|
|
@@ -37,6 +39,7 @@ import {
|
|
|
37
39
|
getCodexAccountCooldownUntil,
|
|
38
40
|
getCodexAccountSoftAvoidUntil,
|
|
39
41
|
getCodexQuotaHealthSnapshot,
|
|
42
|
+
hasUnrecoveredCodexQuotaRefusal,
|
|
40
43
|
isCodexAccountSoftAvoided,
|
|
41
44
|
isCodexQuotaAvoided,
|
|
42
45
|
isHealthAccountAdmissible,
|
|
@@ -92,6 +95,7 @@ import {
|
|
|
92
95
|
getEligiblePoolAccounts,
|
|
93
96
|
getPoolAccountPlanForSelection,
|
|
94
97
|
hasCodexQuotaHeadroom,
|
|
98
|
+
hasCodexSharedStateQuotaHeadroom,
|
|
95
99
|
isCodexAccountPlanExcluded,
|
|
96
100
|
isCodexAccountSelectable,
|
|
97
101
|
isHealthySharedCodexSelection,
|
|
@@ -205,6 +209,11 @@ export {
|
|
|
205
209
|
} from "./routing/active-account";
|
|
206
210
|
export { codexAccountPinDrainReason } from "./routing/pin-drain";
|
|
207
211
|
export type { CodexPinDrainReason } from "./routing/pin-drain";
|
|
212
|
+
|
|
213
|
+
// A shared refresh can outlive the request that opened it. Register the affinity
|
|
214
|
+
// handoff with the flight so a detached G -> G+1 commit cannot strand bindings at G.
|
|
215
|
+
registerCodexRefreshGenerationHandoff(handOffThreadAffinityGeneration);
|
|
216
|
+
|
|
208
217
|
function hasConfiguredPoolAccount(
|
|
209
218
|
config: OcxConfig,
|
|
210
219
|
accountId: string,
|
|
@@ -483,45 +492,6 @@ export function resolveCodexAccountForThread(
|
|
|
483
492
|
return resolution.status === "selected" ? resolution.accountId : null;
|
|
484
493
|
}
|
|
485
494
|
|
|
486
|
-
function carriesQuotaRefusal(health: CodexUpstreamHealth | undefined): boolean {
|
|
487
|
-
return health?.lastFailureStatus === 429 || health?.lastFailureStatus === 402;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
/**
|
|
491
|
-
* Has this account refused a request on quota without serving one since?
|
|
492
|
-
*
|
|
493
|
-
* Thread affinity is a prompt-cache optimization and every rule around it is a preference:
|
|
494
|
-
* `autoSwitchThreshold` is a hint that an account is getting busy, and `pool.cacheAffinity`
|
|
495
|
-
* deliberately raises that bar further. A refusal is not a preference, and once the account has
|
|
496
|
-
* told THIS thread it cannot serve, the binding has nothing left to optimize.
|
|
497
|
-
*
|
|
498
|
-
* The distinction matters because the cooldown a 429 writes is deliberately short. A reset
|
|
499
|
-
* announcement is advisory — plan quota routinely frees up before the advertised instant — so
|
|
500
|
-
* {@link CODEX_MAX_RESET_DERIVED_COOLDOWN_MS} caps it at 15 minutes. The five-hour window that
|
|
501
|
-
* announcement describes is not capped, so an account whose burst window is spent looks
|
|
502
|
-
* selectable again long before it is. For an unbound request that is correct: going back to find
|
|
503
|
-
* out is how the pool learns the window moved. For a BOUND thread it is a loop with no exit —
|
|
504
|
-
* the cooldown lapses, the account still scores lowest on the only window this proxy has a
|
|
505
|
-
* reading for (its weekly bar, untouched by a burst limit), the thread rebinds, and earns the
|
|
506
|
-
* identical 429. Cleared affinity does not help: the next request re-derives the same choice.
|
|
507
|
-
* From the Codex side that reads exactly as reported — a new session rotates normally while an
|
|
508
|
-
* existing one is locked to an exhausted account until the proxy is restarted, because a restart
|
|
509
|
-
* is the only thing that drops the binding and the stale health together.
|
|
510
|
-
*
|
|
511
|
-
* `lastFailureStatus` is the right evidence because of when it ends: {@link preservedCooldownFields}
|
|
512
|
-
* strips it from every recovery write, so it survives exactly until the account actually serves a
|
|
513
|
-
* request again. Nothing here blocks that — selection is untouched, so unbound traffic still probes
|
|
514
|
-
* the account and the first success releases every thread this refused.
|
|
515
|
-
*
|
|
516
|
-
* Scope follows where the refusal was recorded. An account-wide throttle lands in
|
|
517
|
-
* `upstreamHealth` and releases every lane; a reset-derived refusal lands against one native
|
|
518
|
-
* quota group, so a spent Spark window still cannot displace the same thread's Terra binding.
|
|
519
|
-
*/
|
|
520
|
-
function hasUnrecoveredCodexQuotaRefusal(accountId: string, quotaScope?: CodexQuotaScope): boolean {
|
|
521
|
-
if (carriesQuotaRefusal(getAccountHealth(accountId))) return true;
|
|
522
|
-
return quotaScope !== undefined && carriesQuotaRefusal(scopedHealthFor(accountId, quotaScope));
|
|
523
|
-
}
|
|
524
|
-
|
|
525
495
|
function previewReusableAffinityAccount(
|
|
526
496
|
entry: ThreadAffinityEntry | undefined,
|
|
527
497
|
config: OcxConfig,
|
|
@@ -946,7 +916,7 @@ export function resolveCodexAccountForThreadDetailed(
|
|
|
946
916
|
// the account has already told this thread it cannot serve it.
|
|
947
917
|
const quotaRefused = hasUnrecoveredCodexQuotaRefusal(entry.accountId, quotaScope);
|
|
948
918
|
const healthyForSharedAffinity = selectableForSharedState
|
|
949
|
-
&&
|
|
919
|
+
&& hasCodexSharedStateQuotaHeadroom(config, entry.accountId, quotaScope, sharedSelectionOptions, now)
|
|
950
920
|
&& !quotaRefused
|
|
951
921
|
&& !failoverReady;
|
|
952
922
|
if (
|
|
@@ -1144,7 +1114,7 @@ export function resolveCodexAccountForThreadDetailed(
|
|
|
1144
1114
|
sharedSelectionOptions,
|
|
1145
1115
|
);
|
|
1146
1116
|
const activeHealthyForSharedSelection = activeSelectableForSharedState
|
|
1147
|
-
&&
|
|
1117
|
+
&& hasCodexSharedStateQuotaHeadroom(config, active, quotaScope, sharedSelectionOptions, now)
|
|
1148
1118
|
&& !shouldFailover(config, active, now);
|
|
1149
1119
|
if (!isCodexAccountSelectable(config, active, now, quotaScope, selectionOptions)) {
|
|
1150
1120
|
const fallback = pickLowestUsageCodexAccount(config, active, now, quotaScope, selectionOptions);
|
|
@@ -4,11 +4,29 @@ import { serviceApiTokenFilePath } from "../lib/service-secrets";
|
|
|
4
4
|
import { windowsEnvIndirectBatchValue } from "../lib/win-paths";
|
|
5
5
|
|
|
6
6
|
const SHIM_MARKER = "opencodex codex autostart shim";
|
|
7
|
-
const UNIX_SHIM_REVISION_MARKER = "opencodex unix codex shim revision
|
|
7
|
+
const UNIX_SHIM_REVISION_MARKER = "opencodex unix codex shim revision 3";
|
|
8
8
|
|
|
9
9
|
const CODEX_SHIM_REENTRY_EXIT_CODE = 126;
|
|
10
10
|
const CODEX_SHIM_REENTRY_DIAGNOSTIC = "opencodex: saved Codex launcher resolved back to the autostart shim; run ocx codex-shim uninstall and reinstall Codex before enabling codexAutoStart.";
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Said once, on stderr, when `ocx ensure` could not bring the proxy up (#5261).
|
|
14
|
+
*
|
|
15
|
+
* The shim used to discard both of ensure's streams and ignore its exit status, so a failed
|
|
16
|
+
* autostart was completely silent: Codex launched against injected routing pointing at a port
|
|
17
|
+
* nothing was listening on, and every request — sign-in included — failed with no mention of
|
|
18
|
+
* opencodex anywhere.
|
|
19
|
+
*
|
|
20
|
+
* Ensure's own streams stay discarded rather than being let through. Ensure prints progress and
|
|
21
|
+
* warnings on exit-zero runs too, and a wrapper that leaked those would put noise in front of
|
|
22
|
+
* every ordinary Codex launch, which is how a diagnostic gets ignored. The exit status is the
|
|
23
|
+
* signal; this line is the whole message.
|
|
24
|
+
*
|
|
25
|
+
* It names `ocx restore` because bringing the proxy back is only half the choice. A user who
|
|
26
|
+
* cannot sign in needs the way out that does not require the proxy at all.
|
|
27
|
+
*/
|
|
28
|
+
export const CODEX_SHIM_ENSURE_FAILED_DIAGNOSTIC = "opencodex: proxy autostart failed; launching Codex anyway. Run 'ocx doctor' for details, or 'ocx restore' to hand Codex back to its own account.";
|
|
29
|
+
|
|
12
30
|
const CODEX_INTERNAL_COMMANDS = [
|
|
13
31
|
"app-server",
|
|
14
32
|
"archive",
|
|
@@ -136,7 +154,9 @@ case "$ocx_subcommand" in
|
|
|
136
154
|
;;
|
|
137
155
|
*)
|
|
138
156
|
if [ -z "$OCX_SHIM_BYPASS" ]; then
|
|
139
|
-
${BUN_RUNTIME_SOURCE_ENV}=${shQuote(bunRuntimeSource)} ${BUN_RUNTIME_PATH_ENV}=${shQuote(bunPath)} ${shQuote(bunPath)} ${shQuote(cliPath)} ensure >/dev/null 2>&1
|
|
157
|
+
if ! ${BUN_RUNTIME_SOURCE_ENV}=${shQuote(bunRuntimeSource)} ${BUN_RUNTIME_PATH_ENV}=${shQuote(bunPath)} ${shQuote(bunPath)} ${shQuote(cliPath)} ensure >/dev/null 2>&1; then
|
|
158
|
+
printf '%s\\n' ${shQuote(CODEX_SHIM_ENSURE_FAILED_DIAGNOSTIC)} >&2
|
|
159
|
+
fi
|
|
140
160
|
fi
|
|
141
161
|
;;
|
|
142
162
|
esac
|
|
@@ -197,6 +217,7 @@ setlocal\r
|
|
|
197
217
|
${windowsBatchSet(BUN_RUNTIME_SOURCE_ENV, bunRuntimeSource)}\r
|
|
198
218
|
${windowsBatchSet(BUN_RUNTIME_PATH_ENV, bunPath)}\r
|
|
199
219
|
"%OCX_BUN%" "%OCX_CLI%" ensure >nul 2>nul\r
|
|
220
|
+
if errorlevel 1 echo ${CODEX_SHIM_ENSURE_FAILED_DIAGNOSTIC} 1>&2\r
|
|
200
221
|
endlocal\r
|
|
201
222
|
:run_codex\r
|
|
202
223
|
"%OCX_REAL_CODEX%" %*\r
|
|
@@ -239,13 +260,18 @@ if (-not $skipEnsure) {
|
|
|
239
260
|
$priorRuntimePath = $env:${BUN_RUNTIME_PATH_ENV}
|
|
240
261
|
$env:${BUN_RUNTIME_SOURCE_ENV} = ${psString(bunRuntimeSource)}
|
|
241
262
|
$env:${BUN_RUNTIME_PATH_ENV} = ${psString(bunPath)}
|
|
242
|
-
|
|
263
|
+
$ocxEnsureFailed = $false
|
|
264
|
+
# Caught, not propagated: a throwing ensure used to escape this wrapper and Codex never
|
|
265
|
+
# launched at all, which is a lockout produced by the autostart helper itself (#5261).
|
|
266
|
+
try { & ${psString(bunPath)} ${psString(cliPath)} ensure *> $null; if ($LASTEXITCODE -ne 0) { $ocxEnsureFailed = $true } }
|
|
267
|
+
catch { $ocxEnsureFailed = $true }
|
|
243
268
|
finally {
|
|
244
269
|
if ($null -eq $priorRuntimeSource) { Remove-Item Env:\\${BUN_RUNTIME_SOURCE_ENV} -ErrorAction SilentlyContinue }
|
|
245
270
|
else { $env:${BUN_RUNTIME_SOURCE_ENV} = $priorRuntimeSource }
|
|
246
271
|
if ($null -eq $priorRuntimePath) { Remove-Item Env:\\${BUN_RUNTIME_PATH_ENV} -ErrorAction SilentlyContinue }
|
|
247
272
|
else { $env:${BUN_RUNTIME_PATH_ENV} = $priorRuntimePath }
|
|
248
273
|
}
|
|
274
|
+
if ($ocxEnsureFailed) { [Console]::Error.WriteLine(${psString(CODEX_SHIM_ENSURE_FAILED_DIAGNOSTIC)}) }
|
|
249
275
|
}
|
|
250
276
|
& ${psString(realCodexPath)} @args
|
|
251
277
|
$codexExitCode = $LASTEXITCODE
|
package/src/codex/shim.ts
CHANGED
|
@@ -241,7 +241,7 @@ function backupPathFor(path: string): string {
|
|
|
241
241
|
*/
|
|
242
242
|
function destroyedShimMessage(file: ShimFileState): string {
|
|
243
243
|
const wrapper = existsSync(file.wrapperPath)
|
|
244
|
-
?
|
|
244
|
+
? stableShimPathProbe(file.wrapperPath)?.prefix.includes(SHIM_MARKER) ? "present but unusable" : "present but not an opencodex shim"
|
|
245
245
|
: "missing";
|
|
246
246
|
const backup = existsSync(file.backupPath) ? "present" : "missing";
|
|
247
247
|
const base = `Codex autostart shim not restored: wrapper ${wrapper} at ${file.wrapperPath}; original backup ${backup} at ${file.backupPath}.`;
|
|
@@ -905,8 +905,16 @@ export function hasCodexAgentModelFallbackField(role: string, codexHome = CODEX_
|
|
|
905
905
|
}
|
|
906
906
|
|
|
907
907
|
/** Roles whose TOML still carries `model_fallback`, including empty arrays. */
|
|
908
|
-
export function scanCodexAgentRolesWithTomlModelFallback(
|
|
909
|
-
|
|
908
|
+
export function scanCodexAgentRolesWithTomlModelFallback(
|
|
909
|
+
codexHome = CODEX_HOME,
|
|
910
|
+
onListError?: (cause: unknown) => void,
|
|
911
|
+
): string[] {
|
|
912
|
+
try {
|
|
913
|
+
return listCodexAgentRoles(codexHome).filter(role => hasCodexAgentModelFallbackField(role, codexHome));
|
|
914
|
+
} catch (cause) {
|
|
915
|
+
onListError?.(cause);
|
|
916
|
+
return [];
|
|
917
|
+
}
|
|
910
918
|
}
|
|
911
919
|
|
|
912
920
|
const TOML_MODEL_KEY = /^\s*(?:model|"model"|'model')\s*=/;
|
|
@@ -984,9 +992,19 @@ const OPENCODEX_DERIVED_ROLE_MARKERS = ["generated-by: opencodex", "ocx-route:"]
|
|
|
984
992
|
* authorizes writing to, repairing, or removing these files, and the marker-based ownership rules
|
|
985
993
|
* that govern the files opencodex does write are unchanged.
|
|
986
994
|
*/
|
|
987
|
-
export function scanOpencodexDerivedCodexAgentRolesWithoutModelPin(
|
|
995
|
+
export function scanOpencodexDerivedCodexAgentRolesWithoutModelPin(
|
|
996
|
+
codexHome = CODEX_HOME,
|
|
997
|
+
onListError?: (cause: unknown) => void,
|
|
998
|
+
): string[] {
|
|
988
999
|
const findings: string[] = [];
|
|
989
|
-
|
|
1000
|
+
let roles: string[];
|
|
1001
|
+
try {
|
|
1002
|
+
roles = listCodexAgentRoles(codexHome);
|
|
1003
|
+
} catch (cause) {
|
|
1004
|
+
onListError?.(cause);
|
|
1005
|
+
return [];
|
|
1006
|
+
}
|
|
1007
|
+
for (const role of roles) {
|
|
990
1008
|
let content: string;
|
|
991
1009
|
try {
|
|
992
1010
|
content = readFileSync(join(codexHome, "agents", `${role}.toml`), "utf8");
|
package/src/combos/failover.ts
CHANGED
|
@@ -399,6 +399,9 @@ function isRequestLocalTargetIncompatibility(status: number, message: string, co
|
|
|
399
399
|
if (errorCode === "unsupported_value"
|
|
400
400
|
&& (e.param === "reasoning.effort" || e.param === "reasoning_effort")
|
|
401
401
|
&& e.message.startsWith("Unsupported value:") && e.message.includes("not supported")) return true;
|
|
402
|
+
if (e.code === null
|
|
403
|
+
&& e.param === "reasoning_effort"
|
|
404
|
+
&& /^Function tools with reasoning_effort are not supported for gpt-6-astra(?:-\d{4}-\d{2}-\d{2})? in \/v1\/chat\/completions\. To use function tools, use \/v1\/responses or set reasoning_effort to 'none'\.$/.test(e.message)) return true;
|
|
402
405
|
return e.param === "input"
|
|
403
406
|
&& (errorCode === "" || errorCode === "invalid_request_error")
|
|
404
407
|
&& /^Model '[^']{1,256}' does not support image inputs\./.test(e.message);
|