@bitkyc08/opencodex 2.76.0 → 2.78.0-preview.20261005
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/bin/ocx.mjs +11 -10
- package/gui/dist/assets/App-CppMz-6w.js +54 -0
- package/gui/dist/assets/App-lCIslZ1L.css +1 -0
- package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-CJWQqtVv.js} +1 -1
- package/gui/dist/assets/index-DQt9srNi.js +94 -0
- package/gui/dist/assets/index-DmQB8D_R.css +1 -0
- package/gui/dist/assets/quota-summary-BMsyoZH2.js +1 -0
- package/gui/dist/index.html +4 -3
- package/package.json +1 -1
- package/src/adapters/anthropic/beta-allowlist.ts +19 -2
- package/src/adapters/anthropic/client-identity.ts +13 -4
- package/src/adapters/anthropic/native-client-preamble.ts +31 -0
- package/src/adapters/anthropic/passthrough.ts +103 -25
- package/src/adapters/antigravity-refusal.ts +28 -0
- package/src/adapters/cursor/protobuf-request.ts +6 -0
- package/src/adapters/cursor/request-builder.ts +3 -0
- package/src/adapters/cursor/structured-output.ts +13 -0
- package/src/adapters/cursor/types.ts +2 -0
- package/src/adapters/devin/cloud-direct/chat.ts +7 -2
- package/src/adapters/devin/trajectory.ts +49 -0
- package/src/adapters/devin.ts +13 -0
- package/src/adapters/ollama-native.ts +62 -19
- package/src/adapters/openai-responses/passthrough.ts +133 -103
- package/src/adapters/openai-responses/request-strips.ts +10 -4
- package/src/adapters/zed.ts +26 -4
- package/src/bridge/response-json.ts +14 -14
- package/src/bridge/sse.ts +15 -5
- package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
- package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
- package/src/claude/agents-inject.ts +4 -43
- package/src/claude/gateway-cache.ts +16 -13
- package/src/claude/inbound-model-options.ts +9 -2
- package/src/claude/intercept/cli-catalog.ts +27 -16
- package/src/claude/intercept/client-hello.ts +103 -0
- package/src/claude/intercept/model-bindings.ts +3 -2
- package/src/claude/intercept/picker-bootstrap.ts +37 -16
- package/src/claude/intercept/picker-budget.ts +41 -0
- package/src/claude/intercept/picker-listener.ts +279 -66
- package/src/claude/outbound.ts +2 -0
- package/src/claude/subagent-force-status.ts +53 -0
- package/src/claude/subagent-model.ts +93 -0
- package/src/cli/access-audio-input.ts +84 -0
- package/src/cli/access-audio-live.ts +99 -0
- package/src/cli/access-audio.ts +123 -0
- package/src/cli/access-data-client.ts +127 -0
- package/src/cli/access-data-plane.ts +104 -0
- package/src/cli/access-data-response.ts +76 -0
- package/src/cli/access.ts +65 -2
- package/src/cli/account-api.ts +33 -6
- package/src/cli/account-auth.ts +111 -42
- package/src/cli/account-key-quota.ts +118 -0
- package/src/cli/account-main.ts +11 -4
- package/src/cli/account-next-actions.ts +63 -0
- package/src/cli/account-policy-dto.ts +68 -0
- package/src/cli/account-policy.ts +167 -0
- package/src/cli/account-target.ts +8 -0
- package/src/cli/account.ts +37 -9
- package/src/cli/agent-runtime-settings.ts +130 -0
- package/src/cli/agent-settings.ts +101 -0
- package/src/cli/agent.ts +29 -2
- package/src/cli/alias.ts +7 -2
- package/src/cli/aside-profiles.ts +2 -2
- package/src/cli/capabilities-access-remote.ts +147 -0
- package/src/cli/capabilities-accounts.ts +195 -0
- package/src/cli/capabilities-agents-routing.ts +414 -0
- package/src/cli/capabilities-base.ts +783 -0
- package/src/cli/capabilities-command.ts +1 -0
- package/src/cli/capabilities-integrations.ts +264 -0
- package/src/cli/capabilities-lab.ts +214 -0
- package/src/cli/capabilities-observe-system.ts +662 -0
- package/src/cli/capabilities-provider-models.ts +302 -0
- package/src/cli/capabilities.ts +30 -1190
- package/src/cli/capability-types.ts +54 -0
- package/src/cli/catalog-command-result.ts +57 -0
- package/src/cli/chatgpt-command.ts +35 -9
- package/src/cli/claude-agent-startup-sync.ts +17 -0
- package/src/cli/claude-desktop-profile.ts +100 -0
- package/src/cli/claude-desktop.ts +39 -6
- package/src/cli/claude.ts +22 -6
- package/src/cli/codex-shim-autorestore.ts +2 -1
- package/src/cli/codex-shim-readiness.ts +109 -0
- package/src/cli/combo-input.ts +88 -0
- package/src/cli/combo-result.ts +52 -0
- package/src/cli/combo-stats.ts +123 -0
- package/src/cli/combo.ts +37 -14
- package/src/cli/companion-timeline.ts +142 -0
- package/src/cli/companion-usage.ts +146 -0
- package/src/cli/companion.ts +12 -3
- package/src/cli/config-command.ts +24 -8
- package/src/cli/connect.ts +19 -0
- package/src/cli/dispatch.ts +55 -82
- package/src/cli/doctor.ts +22 -4
- package/src/cli/export-command.ts +20 -9
- package/src/cli/gui.ts +8 -3
- package/src/cli/help-catalog.ts +101 -0
- package/src/cli/help-models-context.ts +17 -0
- package/src/cli/help-navigation.ts +50 -0
- package/src/cli/help-recovery.ts +66 -0
- package/src/cli/help.ts +69 -11
- package/src/cli/index.ts +38 -27
- package/src/cli/injection-follow.ts +35 -0
- package/src/cli/inspect.ts +5 -0
- package/src/cli/integration-aside-sync.ts +69 -0
- package/src/cli/integration-cursor.ts +106 -0
- package/src/cli/integration-input.ts +60 -0
- package/src/cli/integration-journal.ts +49 -0
- package/src/cli/integration-plan-dto.ts +156 -0
- package/src/cli/integration-preview.ts +215 -0
- package/src/cli/integrations.ts +24 -18
- package/src/cli/json-input.ts +134 -0
- package/src/cli/link.ts +32 -4
- package/src/cli/local-sync-result.ts +41 -0
- package/src/cli/log-follow.ts +84 -0
- package/src/cli/log-view-filter.ts +146 -0
- package/src/cli/logout-command.ts +44 -0
- package/src/cli/model-picker-ordering.ts +137 -0
- package/src/cli/models-custom-input.ts +107 -0
- package/src/cli/models-custom-runtime.ts +112 -0
- package/src/cli/models-order.ts +194 -0
- package/src/cli/models-runtime-subcommands.ts +2 -0
- package/src/cli/models-runtime.ts +9 -1
- package/src/cli/models.ts +66 -81
- package/src/cli/observe-stream.ts +142 -0
- package/src/cli/observe.ts +75 -34
- package/src/cli/opencode.ts +54 -10
- package/src/cli/provider-batch.ts +77 -0
- package/src/cli/provider-lifecycle-runtime.ts +170 -0
- package/src/cli/provider-result.ts +124 -0
- package/src/cli/provider-runtime.ts +15 -12
- package/src/cli/provider-settings.ts +143 -0
- package/src/cli/provider.ts +116 -64
- package/src/cli/registry.ts +90 -24
- package/src/cli/remote-workspace-hub.ts +83 -0
- package/src/cli/remote-workspace.ts +6 -1
- package/src/cli/restart-failure.ts +27 -0
- package/src/cli/root.ts +42 -9
- package/src/cli/route-policy-write.ts +137 -0
- package/src/cli/route-policy.ts +8 -0
- package/src/cli/runtime-api.ts +57 -80
- package/src/cli/secret-args.ts +149 -0
- package/src/cli/settings-result.ts +30 -0
- package/src/cli/storage.ts +77 -7
- package/src/cli/system-command.ts +60 -2
- package/src/cli/system-restart-client.ts +20 -3
- package/src/cli/system-settings-parity.ts +83 -0
- package/src/cli/tray-proxy.ts +3 -2
- package/src/cli/update-restart-candidate.ts +21 -0
- package/src/cli/update-restart-child.ts +81 -0
- package/src/cli/update-restart-home.ts +38 -0
- package/src/cli/update-restart-transport.ts +193 -0
- package/src/cli/update-restart.ts +260 -0
- package/src/cli/usage-model-search.ts +58 -0
- package/src/cli/usage-report.ts +13 -5
- package/src/cli/v2-input.ts +66 -0
- package/src/cli/v2-local-output.ts +37 -0
- package/src/cli/v2-runtime.ts +90 -0
- package/src/cli/v2.ts +102 -144
- package/src/clients/config-export/contracts.ts +100 -0
- package/src/clients/config-export/droid.ts +54 -2
- package/src/clients/config-export/dsh.ts +21 -0
- package/src/clients/config-export/kilo.ts +12 -2
- package/src/clients/config-export/model-metadata.ts +10 -4
- package/src/clients/config-export/reasoning-metadata.ts +47 -0
- package/src/clients/config-export.ts +53 -67
- package/src/clients/omo-role-models.ts +17 -3
- package/src/codex/account-credit-use.ts +4 -2
- package/src/codex/account-runtime-state.ts +21 -0
- package/src/codex/account-store.ts +11 -47
- package/src/codex/account-usability.ts +11 -8
- package/src/codex/auth-api/account-list.ts +73 -63
- package/src/codex/auth-api/main-account-probe.ts +25 -12
- package/src/codex/auth-api/pool-mode-gate.ts +6 -9
- package/src/codex/auth-api/pool-quota-probe.ts +2 -4
- package/src/codex/auth-api/reset-credit-service.ts +3 -2
- package/src/codex/auth-context.ts +30 -11
- package/src/codex/catalog/aggregation.ts +3 -0
- package/src/codex/catalog/build-entries.ts +1 -1
- package/src/codex/catalog/bundled.ts +9 -2
- package/src/codex/catalog/heal-observation.ts +66 -0
- package/src/codex/catalog/model-hints.ts +2 -1
- package/src/codex/catalog/model-visibility.ts +6 -3
- package/src/codex/catalog/parsing.ts +2 -0
- package/src/codex/catalog/provider-models.ts +13 -4
- package/src/codex/catalog/publication-observer.ts +19 -0
- package/src/codex/catalog/remote.ts +13 -15
- package/src/codex/catalog/restore.ts +19 -6
- package/src/codex/catalog/retained-sync.ts +60 -11
- package/src/codex/catalog/routed-removal.ts +116 -0
- package/src/codex/catalog/write-audit-contract.ts +21 -0
- package/src/codex/catalog/write-audit.ts +171 -0
- package/src/codex/catalog-self-heal.ts +284 -0
- package/src/codex/catalog-write-serialization.ts +71 -7
- package/src/codex/chatgpt-refresh-failure.ts +56 -0
- package/src/codex/codex-home-owner.ts +124 -0
- package/src/codex/convergence.ts +57 -16
- package/src/codex/features.ts +8 -1
- package/src/codex/inject/restore.ts +46 -4
- package/src/codex/inject.ts +11 -1
- package/src/codex/internal/catalog-writer.ts +120 -13
- package/src/codex/journal.ts +49 -13
- package/src/codex/main-account.ts +144 -11
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/quota-refresh-outcome.ts +17 -2
- package/src/codex/quota-types.ts +36 -1
- package/src/codex/quota.ts +67 -16
- package/src/codex/refresh.ts +20 -9
- package/src/codex/routing/cooldown-math.ts +10 -4
- package/src/codex/routing/selection.ts +11 -4
- package/src/codex/routing.ts +9 -9
- package/src/codex/runtime.ts +7 -1
- package/src/codex/shim-diagnostics.ts +80 -0
- package/src/codex/shim-fingerprint.ts +7 -2
- package/src/codex/shim-migration.ts +153 -0
- package/src/codex/shim-overlay.ts +509 -0
- package/src/codex/shim-path-resolution.ts +145 -0
- package/src/codex/shim-probe.ts +4 -3
- package/src/codex/shim-state-file.ts +93 -7
- package/src/codex/shim.ts +54 -107
- package/src/codex/subagent-model-fallback.ts +2 -1
- package/src/codex/sync.ts +24 -4
- package/src/combos/failover.ts +101 -1
- package/src/combos/index.ts +1 -0
- package/src/combos/jev.ts +13 -3
- package/src/combos/resolve.ts +12 -0
- package/src/config/atomic-write.ts +13 -8
- package/src/config/diagnostics.ts +23 -1
- package/src/config/load-degrade.ts +7 -0
- package/src/config/provider-validation.ts +3 -0
- package/src/config/schema/chatgpt-desktop.ts +22 -0
- package/src/config/schema/compaction-triggers.ts +8 -0
- package/src/config/schema/config-schema.ts +43 -14
- package/src/config/schema/leaf-validators.ts +8 -3
- package/src/config/subagent-models.ts +12 -0
- package/src/generated/compatibility-version.json +645 -237
- package/src/grok/session-identity.ts +33 -0
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/merge.ts +13 -5
- package/src/integrations/mutation-plan.ts +50 -5
- package/src/integrations/omp-yaml-source.ts +181 -1
- package/src/integrations/ownership-policy.ts +66 -0
- package/src/integrations/registry.ts +50 -1
- package/src/integrations/state.ts +106 -19
- package/src/integrations/target.ts +25 -2
- package/src/integrations/writer.ts +107 -35
- package/src/lib/gui-pair-capability.ts +17 -0
- package/src/lib/package-tree-integrity.ts +42 -1
- package/src/lib/provider-client-headers.ts +62 -0
- package/src/lib/proxy-env.ts +3 -0
- package/src/lib/test-home-guard.ts +2 -1
- package/src/oauth/anthropic-account-refusal.ts +19 -8
- package/src/oauth/anthropic-continuity.ts +65 -0
- package/src/oauth/anthropic-identity.ts +75 -0
- package/src/oauth/anthropic-routing.ts +20 -15
- package/src/oauth/anthropic.ts +34 -3
- package/src/oauth/generic-account-failover.ts +21 -0
- package/src/oauth/health.ts +32 -5
- package/src/oauth/index.ts +42 -35
- package/src/oauth/local-token-detect.ts +7 -0
- package/src/oauth/pool-settings-capability.ts +9 -2
- package/src/oauth/refresh-policy.ts +2 -0
- package/src/oauth/store.ts +55 -6
- package/src/oauth/types.ts +9 -0
- package/src/protocols/encoders/messages.ts +4 -3
- package/src/protocols/plan-snapshot.ts +1 -1
- package/src/protocols/settings.ts +28 -4
- package/src/providers/antigravity-effort-families.ts +75 -0
- package/src/providers/antigravity-models.ts +54 -16
- package/src/providers/antigravity-wire-snapshot.ts +66 -0
- package/src/providers/derive.ts +2 -0
- package/src/providers/model-rename-fields.ts +3 -0
- package/src/providers/new-model-policy.ts +48 -10
- package/src/providers/registry/entries-extended.ts +3 -0
- package/src/providers/registry/model-ids.ts +2 -0
- package/src/providers/registry/model-seeds.ts +5 -0
- package/src/providers/registry/types.ts +4 -0
- package/src/responses/compaction.ts +19 -0
- package/src/router.ts +6 -0
- package/src/server/auth-cors.ts +9 -0
- package/src/server/caller-session-identity.ts +32 -0
- package/src/server/chat-completions.ts +10 -0
- package/src/server/claude-messages.ts +29 -15
- package/src/server/droid-reasoning-default.ts +50 -0
- package/src/server/github-copilot-responses-repair.ts +3 -6
- package/src/server/grok-responses-control-frame.ts +2 -2
- package/src/server/grok-responses-snapshot-repair.ts +4 -5
- package/src/server/gui-session.ts +33 -11
- package/src/server/index/claude-intercept-lifecycle.ts +4 -0
- package/src/server/index/serve-options.ts +13 -7
- package/src/server/index.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +12 -134
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/context.ts +8 -1
- package/src/server/management/cursor-integration-routes.ts +3 -3
- package/src/server/management/integration-routes.ts +69 -11
- package/src/server/management/link-routes.ts +20 -13
- package/src/server/management/model-routes.ts +3 -3
- package/src/server/management/model-row-export-metadata.ts +150 -0
- package/src/server/management/model-rows.ts +148 -9
- package/src/server/management/native-integration-routes.ts +2 -2
- package/src/server/management/oauth-account-routes.ts +96 -30
- package/src/server/management/route-registry.ts +29 -27
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/subagent-model-routes.ts +214 -0
- package/src/server/messages-native-eligibility.ts +3 -6
- package/src/server/messages-native-oauth.ts +82 -35
- package/src/server/messages-native.ts +78 -16
- package/src/server/relay.ts +14 -44
- package/src/server/request-log-generation-window.ts +38 -0
- package/src/server/request-log.ts +13 -0
- package/src/server/responses/adapter-dispatch.ts +152 -28
- package/src/server/responses/codex-auth-error.ts +10 -2
- package/src/server/responses/combo-stream-preflight.ts +29 -3
- package/src/server/responses/compaction-routing.ts +10 -1
- package/src/server/responses/core-auth.ts +67 -30
- package/src/server/responses/core-codex-account.ts +6 -3
- package/src/server/responses/core-combo-failure.ts +5 -1
- package/src/server/responses/core-combo-native.ts +7 -1
- package/src/server/responses/core-combo.ts +64 -7
- package/src/server/responses/core-normalize.ts +7 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/fetch-helpers.ts +9 -0
- package/src/server/responses/passthrough-delivery.ts +1 -1
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/server/responses/request-prepare.ts +5 -0
- package/src/server/responses/request-spend.ts +30 -30
- package/src/server/responses-custom-tool-repair.ts +2 -2
- package/src/server/responses-hosted-image-display.ts +70 -9
- package/src/server/responses-terminal-repair.ts +15 -2
- package/src/server/sse-frame-buffer.ts +38 -36
- package/src/server/sse-payload-rewrite.ts +78 -49
- package/src/server/ws-bridge.ts +1 -11
- package/src/service/cli.ts +6 -1
- package/src/service/ownership-mutation-lease.d.mts +26 -0
- package/src/service/ownership-mutation-lease.mjs +76 -1
- package/src/service/windows-taskxml.ts +1 -2
- package/src/service-manager-probe.ts +36 -2
- package/src/types/config.ts +6 -0
- package/src/types/provider.ts +28 -10
- package/src/update/index.ts +2 -1
- package/src/update/npm-invocation.mjs +35 -3
- package/src/update/proxy-liveness-probe.mjs +38 -8
- package/src/update/update-failure-guidance.d.mts +6 -0
- package/src/update/update-failure-guidance.mjs +30 -5
- package/src/usage/expected-prices.ts +9 -0
- package/src/usage/log.ts +7 -0
- package/src/usage/summary.ts +82 -1
- package/src/web-search/loop.ts +11 -8
- package/src/web-search/run-turn-loop.ts +10 -7
- package/gui/dist/assets/App-CddorHdA.js +0 -51
- package/gui/dist/assets/App-DMudtISW.css +0 -1
- package/gui/dist/assets/index-Btze1I3T.css +0 -1
- package/gui/dist/assets/index-CvnY7E6i.js +0 -86
- package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
package/src/codex/quota.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseCodexCredits } from "./credits";
|
|
1
2
|
import { forgetMainAccountUsage, observeMainAccountUsage, type FreshWindow } from "./main-account-external-usage";
|
|
2
3
|
import { closeSync, constants as fsConstants, existsSync, fstatSync, openSync, readSync, unlinkSync } from "node:fs";
|
|
3
4
|
import { join } from "node:path";
|
|
@@ -11,7 +12,7 @@ import { getObservedMainQuotaIdentityKey, isMainQuotaWriterLive, type MainQuotaW
|
|
|
11
12
|
|
|
12
13
|
import { CodexQuotaHistory, QUOTA_HISTORY_LIMITS, type QuotaHistoryWindow } from "./quota-history";
|
|
13
14
|
import { isPoolQuotaWriterLive, poolQuotaHistoryIdentity } from "./account-store";
|
|
14
|
-
import { CODEX_EXHAUSTED_USAGE_PERCENT, MAIN_ACCOUNT_HARD_LOCK_MIN_PERCENT, resetAtToMs } from "./quota-types";
|
|
15
|
+
import { CODEX_EXHAUSTED_USAGE_PERCENT, MAIN_ACCOUNT_HARD_LOCK_MIN_PERCENT, hasSpendableCodexCredits, resetAtToMs } from "./quota-types";
|
|
15
16
|
import type { PoolQuotaWriter, StoredAccountQuota, WhamUsageResponse, WhamUsageWindow } from "./quota-types";
|
|
16
17
|
|
|
17
18
|
export type { StoredAccountQuota, WhamUsageResponse } from "./quota-types";
|
|
@@ -83,10 +84,12 @@ function mayCommitAccountQuota(accountId: string, writerGeneration: number): boo
|
|
|
83
84
|
// actually exhausted account is still eligible for threshold rotation.
|
|
84
85
|
export const CODEX_UNKNOWN_USAGE_SCORE = 101;
|
|
85
86
|
export function isCodexQuotaExhausted(
|
|
86
|
-
quota: Pick<StoredAccountQuota, "weeklyPercent" | "monthlyPercent" | "shortPercent"> | null,
|
|
87
|
+
quota: Pick<StoredAccountQuota, "weeklyPercent" | "monthlyPercent" | "shortPercent" | "credits"> | null,
|
|
87
88
|
plan?: unknown,
|
|
89
|
+
allowCredits = false,
|
|
88
90
|
): boolean {
|
|
89
91
|
if (!quota) return false;
|
|
92
|
+
if (allowCredits && hasSpendableCodexCredits(quota)) return false;
|
|
90
93
|
// The burst window counts on EVERY plan. It is upstream-enforced independently, so an
|
|
91
94
|
// account at 100% there is blocked regardless of which longer window governs its plan;
|
|
92
95
|
// omitting it would route traffic straight into a 429 (#1791).
|
|
@@ -118,10 +121,11 @@ export function codexQuotaWindowForPlan(plan?: unknown): "monthly" | "weekly" {
|
|
|
118
121
|
}
|
|
119
122
|
|
|
120
123
|
export function isCompleteCodexQuotaRecoverySnapshot(
|
|
121
|
-
quota: Pick<StoredAccountQuota, "weeklyPercent" | "monthlyPercent" | "monthlyIsPrimaryWindow" | "shortPercent"> | null,
|
|
124
|
+
quota: Pick<StoredAccountQuota, "weeklyPercent" | "monthlyPercent" | "monthlyIsPrimaryWindow" | "shortPercent" | "credits"> | null,
|
|
122
125
|
plan?: unknown,
|
|
126
|
+
allowCredits = false,
|
|
123
127
|
): boolean {
|
|
124
|
-
if (!quota || isCodexQuotaExhausted(quota, plan)) return false;
|
|
128
|
+
if (!quota || isCodexQuotaExhausted(quota, plan, allowCredits)) return false;
|
|
125
129
|
// Recovery still fails closed on MISSING EVIDENCE — a credits-only or windowless payload
|
|
126
130
|
// carries no usage reading at all and must never clear a cooldown. What it does not do is
|
|
127
131
|
// fail closed on an unfamiliar plan NAME, which only ever meant "cooled forever".
|
|
@@ -277,6 +281,10 @@ function snapshotHasCustom(quota: Omit<StoredAccountQuota, "updatedAt">): boolea
|
|
|
277
281
|
function snapshotHasUsage(quota: Omit<StoredAccountQuota, "updatedAt">): boolean {
|
|
278
282
|
return snapshotHasWeekly(quota) || snapshotHasMonthly(quota) || snapshotHasShort(quota) || snapshotHasCustom(quota);
|
|
279
283
|
}
|
|
284
|
+
|
|
285
|
+
function snapshotHasCredits(quota: Omit<StoredAccountQuota, "updatedAt">): boolean {
|
|
286
|
+
return quota.resetCredits !== undefined || quota.credits !== undefined;
|
|
287
|
+
}
|
|
280
288
|
/**
|
|
281
289
|
* Publish parsed display quota and separately validated policy evidence after writer checks.
|
|
282
290
|
* A null policy observation retains only the matching main identity's previous evidence;
|
|
@@ -296,7 +304,7 @@ export function setAccountQuotaFromParsed(
|
|
|
296
304
|
): void {
|
|
297
305
|
quota = withoutRetiredCodexQuota(quota);
|
|
298
306
|
policyQuota = withoutRetiredCodexQuota(policyQuota);
|
|
299
|
-
if (!quota || (!snapshotHasUsage(quota) && quota
|
|
307
|
+
if (!quota || (!snapshotHasUsage(quota) && !snapshotHasCredits(quota))) return;
|
|
300
308
|
if (!mayCommitAccountQuota(accountId, writerGeneration)) return;
|
|
301
309
|
const isMain = accountId === MAIN_CODEX_ACCOUNT_ID;
|
|
302
310
|
if (isMain && mainWriter && !isMainQuotaWriterLive(mainWriter)) return;
|
|
@@ -343,7 +351,7 @@ export function setAccountQuotaFromParsed(
|
|
|
343
351
|
}
|
|
344
352
|
schedulePersistAccountQuotas();
|
|
345
353
|
// Credits carry the previous usage tuple; they must not refresh its observation clock.
|
|
346
|
-
if (!(quota
|
|
354
|
+
if (!(snapshotHasCredits(quota) && !snapshotHasUsage(quota))) {
|
|
347
355
|
if (!isMain && policyQuota && livePoolEvidence) observeCodexLowQuota(accountId, policyQuota);
|
|
348
356
|
notifyCodexQuotaSnapshot(accountId, next);
|
|
349
357
|
}
|
|
@@ -363,7 +371,11 @@ function mergeAccountQuota(
|
|
|
363
371
|
quota = withoutRetiredCodexQuota(quota) ?? {};
|
|
364
372
|
existing = withoutRetiredCodexQuota(existing ?? null) ?? undefined;
|
|
365
373
|
const next: StoredAccountQuota = { updatedAt };
|
|
366
|
-
const creditsOnly = quota
|
|
374
|
+
const creditsOnly = snapshotHasCredits(quota) && !snapshotHasUsage(quota);
|
|
375
|
+
// An explicit empty/invalid credits summary retracts the old balance. Partial headers
|
|
376
|
+
// retain the original observation clock instead of extending its spending authority.
|
|
377
|
+
if (quota.credits !== undefined) next.credits = quota.credits;
|
|
378
|
+
else if (existing?.credits !== undefined) next.credits = existing.credits;
|
|
367
379
|
|
|
368
380
|
if (creditsOnly) {
|
|
369
381
|
if (existing?.weeklyPercent !== undefined) next.weeklyPercent = existing.weeklyPercent;
|
|
@@ -373,7 +385,8 @@ function mergeAccountQuota(
|
|
|
373
385
|
if (existing?.monthlyIsPrimaryWindow === true) next.monthlyIsPrimaryWindow = true;
|
|
374
386
|
assignCarriedShort(next, existing, updatedAt, policyEvidence);
|
|
375
387
|
if (existing?.customWindows !== undefined) next.customWindows = existing.customWindows;
|
|
376
|
-
next.resetCredits = quota.resetCredits;
|
|
388
|
+
if (quota.resetCredits !== undefined) next.resetCredits = quota.resetCredits;
|
|
389
|
+
else if (existing?.resetCredits !== undefined) next.resetCredits = existing.resetCredits;
|
|
377
390
|
return stampCodexQuotaUsageObservation(next, quota, existing);
|
|
378
391
|
}
|
|
379
392
|
|
|
@@ -525,7 +538,7 @@ export function withoutRetiredCodexQuota<T extends Omit<StoredAccountQuota, "upd
|
|
|
525
538
|
const next = { ...quota };
|
|
526
539
|
if (kept.length > 0) next.customWindows = kept;
|
|
527
540
|
else delete next.customWindows;
|
|
528
|
-
return snapshotHasUsage(next) || next
|
|
541
|
+
return snapshotHasUsage(next) || snapshotHasCredits(next) ? next as T : null;
|
|
529
542
|
}
|
|
530
543
|
|
|
531
544
|
export function parseUpstreamQuotaHeaders(headers: Headers, options?: { modelId?: string }): Omit<StoredAccountQuota, "updatedAt"> | null {
|
|
@@ -644,6 +657,7 @@ export function updateAccountQuota(
|
|
|
644
657
|
...(existing?.monthlyResetAt !== undefined ? { monthlyResetAt: existing.monthlyResetAt } : {}),
|
|
645
658
|
...(existing?.customWindows !== undefined ? { customWindows: existing.customWindows } : {}),
|
|
646
659
|
...(existing?.resetCredits !== undefined ? { resetCredits: existing.resetCredits } : {}),
|
|
660
|
+
...(existing?.credits !== undefined ? { credits: existing.credits } : {}),
|
|
647
661
|
updatedAt: Date.now(),
|
|
648
662
|
};
|
|
649
663
|
assignCarriedShort(quota, existing, quota.updatedAt);
|
|
@@ -702,6 +716,20 @@ function readMainPolicyQuota(value: unknown): MainPolicyQuota | null {
|
|
|
702
716
|
if (typeof number === "number" && Number.isFinite(number) && number >= 0) quota[field] = number;
|
|
703
717
|
}
|
|
704
718
|
if (quota.monthlyPercent !== undefined && raw.monthlyIsPrimaryWindow === true) quota.monthlyIsPrimaryWindow = true;
|
|
719
|
+
// Retain the independent credit timestamp on restart; it still expires at its original deadline.
|
|
720
|
+
if (raw.credits === null) quota.credits = null;
|
|
721
|
+
else if (raw.credits && typeof raw.credits === "object" && !Array.isArray(raw.credits)) {
|
|
722
|
+
const stored = raw.credits as Record<string, unknown>;
|
|
723
|
+
if (typeof stored.observedAt === "number" && Number.isFinite(stored.observedAt) && stored.observedAt >= 0) {
|
|
724
|
+
quota.credits = { observedAt: stored.observedAt };
|
|
725
|
+
for (const key of ["hasCredits", "unlimited", "overageLimitReached", "allowed"] as const) {
|
|
726
|
+
if (typeof stored[key] === "boolean") quota.credits[key] = stored[key];
|
|
727
|
+
}
|
|
728
|
+
if (typeof stored.balance === "number" && Number.isFinite(stored.balance) && stored.balance >= 0) {
|
|
729
|
+
quota.credits.balance = stored.balance;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
705
733
|
return { identityKey: entry.identityKey, quota };
|
|
706
734
|
}
|
|
707
735
|
|
|
@@ -845,7 +873,7 @@ function filterMainPolicyMonthlyQuota(
|
|
|
845
873
|
delete filtered.monthlyResetAt;
|
|
846
874
|
delete filtered.monthlyIsPrimaryWindow;
|
|
847
875
|
// Null retains the matching prior observation; an empty object would merge away evidence.
|
|
848
|
-
return hasKnownQuotaValue(filtered) || filtered
|
|
876
|
+
return hasKnownQuotaValue(filtered) || snapshotHasCredits(filtered) ? filtered : null;
|
|
849
877
|
}
|
|
850
878
|
|
|
851
879
|
/**
|
|
@@ -876,21 +904,44 @@ function isMeasuredLongWindow(window: WhamUsageWindow | null | undefined): boole
|
|
|
876
904
|
return isExplicitLongWindow(window) && normalizeUsagePercent(window?.used_percent) !== undefined;
|
|
877
905
|
}
|
|
878
906
|
|
|
907
|
+
/** Absent/null controls impose no veto; a present control requires reached:false. */
|
|
908
|
+
function spendControlAllowsCredits(control: unknown): boolean | undefined {
|
|
909
|
+
if (control === undefined || control === null) return undefined;
|
|
910
|
+
return typeof control === "object" && !Array.isArray(control)
|
|
911
|
+
&& "reached" in control && control.reached === false;
|
|
912
|
+
}
|
|
913
|
+
|
|
879
914
|
/**
|
|
880
915
|
* Normalize WHAM windows into the display snapshot, preserving declared short-window shape.
|
|
881
916
|
* Finite percentages are clamped for compatibility; policy callers must validate raw readings
|
|
882
|
-
* separately. Return null when
|
|
917
|
+
* separately. Return null when no usage window or credit observation/retraction is available.
|
|
883
918
|
*/
|
|
884
919
|
export function parseUsageQuota(data: WhamUsageResponse): Omit<StoredAccountQuota, "updatedAt"> | null {
|
|
885
920
|
const resetCredits = typeof data.rate_limit_reset_credits?.available_count === "number"
|
|
886
921
|
? data.rate_limit_reset_credits.available_count
|
|
887
922
|
: undefined;
|
|
888
923
|
|
|
889
|
-
if (!data.rate_limit) {
|
|
890
|
-
return resetCredits !== undefined ? { resetCredits } : null;
|
|
891
|
-
}
|
|
892
|
-
|
|
893
924
|
const quota: Omit<StoredAccountQuota, "updatedAt"> = {};
|
|
925
|
+
const creditsAllowed = spendControlAllowsCredits(data.spend_control);
|
|
926
|
+
if (resetCredits !== undefined) quota.resetCredits = resetCredits;
|
|
927
|
+
if (data.credits !== undefined) {
|
|
928
|
+
quota.credits = null;
|
|
929
|
+
const parsed = parseCodexCredits(data.credits);
|
|
930
|
+
if (parsed) {
|
|
931
|
+
const balance = parsed.balance === undefined ? undefined : Number(parsed.balance);
|
|
932
|
+
quota.credits = {
|
|
933
|
+
observedAt: Date.now(),
|
|
934
|
+
...(parsed.hasCredits !== undefined ? { hasCredits: parsed.hasCredits } : {}),
|
|
935
|
+
...(parsed.unlimited !== undefined ? { unlimited: parsed.unlimited } : {}),
|
|
936
|
+
...(parsed.overageLimitReached !== undefined ? { overageLimitReached: parsed.overageLimitReached } : {}),
|
|
937
|
+
// Included-plan refusal is precisely when opted-in credits take over, not a credit veto.
|
|
938
|
+
// A declared spending control permits credits only with an explicit unreached verdict.
|
|
939
|
+
...(creditsAllowed !== undefined ? { allowed: creditsAllowed } : {}),
|
|
940
|
+
...(balance !== undefined && Number.isFinite(balance) ? { balance } : {}),
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
} else if (data.rate_limit?.allowed === false || creditsAllowed === false) quota.credits = null;
|
|
944
|
+
if (!data.rate_limit) return snapshotHasCredits(quota) ? quota : null;
|
|
894
945
|
const thirtyDayOnly = codexQuotaWindowForPlan(data.plan_type) === "monthly";
|
|
895
946
|
const primaryWindow = data.rate_limit.primary_window;
|
|
896
947
|
const secondaryWindow = data.rate_limit.secondary_window;
|
|
@@ -951,7 +1002,7 @@ export function parseUsageQuota(data: WhamUsageResponse): Omit<StoredAccountQuot
|
|
|
951
1002
|
|
|
952
1003
|
if (resetCredits !== undefined) quota.resetCredits = resetCredits;
|
|
953
1004
|
|
|
954
|
-
return hasKnownQuotaValue(quota) ||
|
|
1005
|
+
return hasKnownQuotaValue(quota) || snapshotHasCredits(quota) ? quota : null;
|
|
955
1006
|
}
|
|
956
1007
|
|
|
957
1008
|
|
package/src/codex/refresh.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
2
3
|
import { invalidateCodexModelsCache, syncCatalogModels } from "./catalog";
|
|
3
4
|
import type { ComboCatalogOmission } from "./catalog/aggregation";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
5
|
+
import { getCodexHome } from "./paths";
|
|
6
|
+
import { withCatalogWriteSerialization, CatalogWritePermitRefusal } from "./catalog-write-serialization";
|
|
7
|
+
import { replaceCodexModelsCache } from "./internal/catalog-writer";
|
|
6
8
|
import type { OcxConfig } from "../types";
|
|
7
9
|
import type { CodexCatalogSyncOptions } from "./catalog/sync";
|
|
8
10
|
|
|
@@ -14,8 +16,12 @@ export interface CodexCatalogRefreshResult {
|
|
|
14
16
|
cacheSynced: boolean;
|
|
15
17
|
comboOmissions: ComboCatalogOmission[];
|
|
16
18
|
refreshOutcome?: "committed" | "refused";
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Desired OFF observed under K during the catalog commit, or a write refused because it would
|
|
21
|
+
* empty routed namespaces config.json does not back (#6529); no cache write either way.
|
|
22
|
+
*/
|
|
23
|
+
skippedReason?: "desired_disabled" | "unbacked_routed_removal" | "foreign_owner" | "owner_unknown";
|
|
24
|
+
protectedRoutedNamespaces?: number;
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
interface RefreshDeps {
|
|
@@ -31,8 +37,13 @@ const defaultDeps: RefreshDeps = {
|
|
|
31
37
|
};
|
|
32
38
|
|
|
33
39
|
export function syncCodexModelsCacheFromCatalog(catalogPath: string): void {
|
|
34
|
-
const
|
|
35
|
-
|
|
40
|
+
const owningCodexHome = getCodexHome();
|
|
41
|
+
const outcome = withCatalogWriteSerialization(owningCodexHome, permit =>
|
|
42
|
+
replaceCodexModelsCache(permit, owningCodexHome, {
|
|
43
|
+
path: join(owningCodexHome, "models_cache.json"),
|
|
44
|
+
content: readFileSync(catalogPath, "utf8"),
|
|
45
|
+
}), { intent: "cache", writer: "cache-from-catalog" });
|
|
46
|
+
if (outcome.kind === "unavailable") throw new CatalogWritePermitRefusal(`Catalog cache synchronization unavailable (${outcome.reason}).`);
|
|
36
47
|
}
|
|
37
48
|
|
|
38
49
|
/**
|
|
@@ -50,9 +61,9 @@ export async function refreshCodexModelCatalog(
|
|
|
50
61
|
const catalogExists = deps.existsSync(result.path);
|
|
51
62
|
const catalogWritten = result.catalogWritten === true;
|
|
52
63
|
const comboOmissions = result.comboOmissions ?? [];
|
|
53
|
-
if (result.skippedReason
|
|
54
|
-
// The commit path observed OFF under K. Invalidate
|
|
55
|
-
// models cache here would be exactly the
|
|
64
|
+
if (result.skippedReason !== undefined || result.refreshOutcome === "refused") {
|
|
65
|
+
// The commit path observed OFF, or an unbacked routed removal, under K. Invalidate
|
|
66
|
+
// nothing: rewriting the models cache here would be exactly the write the skip refused.
|
|
56
67
|
return { ...result, catalogExists, catalogWritten: false, cacheSynced: false, comboOmissions };
|
|
57
68
|
}
|
|
58
69
|
if (!catalogExists) {
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
CODEX_UNKNOWN_USAGE_SCORE,
|
|
4
4
|
} from "../quota";
|
|
5
5
|
import { isThirtyDayOnlyCodexPlan } from "../plan";
|
|
6
|
-
import { isTerminalShortWindow } from "../quota-types";
|
|
6
|
+
import { hasSpendableCodexCredits, isTerminalShortWindow, type CodexSpendableCredits } from "../quota-types";
|
|
7
7
|
import type { CodexQuotaScope } from "./health-store";
|
|
8
8
|
import type { TransientProbeGrant } from "./thread-affinity";
|
|
9
9
|
|
|
@@ -107,7 +107,8 @@ export function computeCodexUsageScore(quota: {
|
|
|
107
107
|
shortPercent?: number;
|
|
108
108
|
shortResetAt?: number;
|
|
109
109
|
shortObservedAt?: number;
|
|
110
|
-
|
|
110
|
+
credits?: CodexSpendableCredits | null;
|
|
111
|
+
} | null, plan?: unknown, now: number = Date.now(), allowCredits = false): number {
|
|
111
112
|
if (!quota) return CODEX_UNKNOWN_USAGE_SCORE;
|
|
112
113
|
const finite = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value);
|
|
113
114
|
const longWindows = isThirtyDayOnlyCodexPlan(plan)
|
|
@@ -126,10 +127,15 @@ export function computeCodexUsageScore(quota: {
|
|
|
126
127
|
// correct for uncertainty and wrong for a measured refusal: the account stays selected,
|
|
127
128
|
// `applyQuotaAutoSwitch` never fires, and the pool wedges on an exhausted credential.
|
|
128
129
|
if (knownLong.length === 0) {
|
|
129
|
-
return isTerminalShortWindow(quota, now)
|
|
130
|
+
return isTerminalShortWindow(quota, now)
|
|
131
|
+
? (allowCredits && hasSpendableCodexCredits(quota, now)) ? CODEX_EXHAUSTED_USAGE_PERCENT - 1 : CODEX_EXHAUSTED_USAGE_PERCENT
|
|
132
|
+
: CODEX_UNKNOWN_USAGE_SCORE;
|
|
130
133
|
}
|
|
131
134
|
const values = finite(quota.shortPercent) ? [...knownLong, quota.shortPercent] : knownLong;
|
|
132
|
-
|
|
135
|
+
const score = Math.max(...values);
|
|
136
|
+
// Keep included-capacity accounts ahead of a paid overage account, but do not
|
|
137
|
+
// classify the latter as exhausted solely from its subscription percentages.
|
|
138
|
+
return (allowCredits && hasSpendableCodexCredits(quota, now)) ? Math.min(score, CODEX_EXHAUSTED_USAGE_PERCENT - 1) : score;
|
|
133
139
|
}
|
|
134
140
|
|
|
135
141
|
// `isTerminalShortWindow` moved to ../quota-types, the leaf the dashboard can import. Routing
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getEffectiveCodexAutoSwitchThreshold } from "../account-auto-switch";
|
|
2
|
-
import { isCodexAccountHeldForCredits } from "../account-credit-use";
|
|
2
|
+
import { codexAccountUsesCreditsAfterLimit, isCodexAccountHeldForCredits } from "../account-credit-use";
|
|
3
3
|
import { isCodexAccountPaused } from "../account-pause";
|
|
4
4
|
import { codexAccountPriorityLookup, pinnedCodexAccountId } from "../account-priority";
|
|
5
5
|
import { isSelectableCodexPoolAccount } from "../account-id";
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from "../pool-rotation";
|
|
17
17
|
import { CODEX_UNKNOWN_USAGE_SCORE, getAccountQuota, resetAtToMs } from "../quota";
|
|
18
18
|
import { codexPlanKey } from "../plan";
|
|
19
|
-
import { MAIN_CODEX_ACCOUNT_ID, getMainAccountPlan
|
|
19
|
+
import { MAIN_CODEX_ACCOUNT_ID, getMainAccountPlan } from "../main-account";
|
|
20
20
|
import type { OcxConfig } from "../../types";
|
|
21
21
|
import { CODEX_FAILURE_WINDOW_MS, computeCodexUsageScore } from "./cooldown-math";
|
|
22
22
|
import {
|
|
@@ -220,7 +220,6 @@ export function getEligiblePoolAccounts(
|
|
|
220
220
|
if (
|
|
221
221
|
excludeId !== MAIN_CODEX_ACCOUNT_ID
|
|
222
222
|
&& !isCodexAccountPaused(config, MAIN_CODEX_ACCOUNT_ID)
|
|
223
|
-
&& (!isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID) || hasMainAccountRefreshGrant())
|
|
224
223
|
&& getCodexQuotaHealthSnapshot(MAIN_CODEX_ACCOUNT_ID, quotaScope, now) === null
|
|
225
224
|
&& !isCodexAccountSoftAvoided(MAIN_CODEX_ACCOUNT_ID, now)
|
|
226
225
|
// The main login is not in `config.codexAccounts`, so it never passes through the
|
|
@@ -293,6 +292,7 @@ export function hasCodexQuotaHeadroom(
|
|
|
293
292
|
getAccountQuota(accountId),
|
|
294
293
|
getPoolAccountPlanForSelection(config, accountId, selectionOptions),
|
|
295
294
|
now,
|
|
295
|
+
codexAccountUsesCreditsAfterLimit(config, accountId),
|
|
296
296
|
);
|
|
297
297
|
if (isUnknownUsage(usage)) return true;
|
|
298
298
|
return usage < threshold;
|
|
@@ -344,6 +344,7 @@ export function hasCodexSharedStateQuotaHeadroom(
|
|
|
344
344
|
getAccountQuota(accountId),
|
|
345
345
|
getPoolAccountPlanForSelection(config, accountId, selectionOptions),
|
|
346
346
|
now,
|
|
347
|
+
codexAccountUsesCreditsAfterLimit(config, accountId),
|
|
347
348
|
);
|
|
348
349
|
return isUnknownUsage(usage) || usage < 100;
|
|
349
350
|
}
|
|
@@ -537,7 +538,7 @@ export function sharedStateSelectionOptions(
|
|
|
537
538
|
selectionOptions?: CodexAccountUsabilityOptions,
|
|
538
539
|
): Pick<
|
|
539
540
|
CodexAccountUsabilityOptions,
|
|
540
|
-
"nativeMainSelectionOnly" | "isMainAccountTokenLive"
|
|
541
|
+
"nativeMainSelectionOnly" | "isMainAccountTokenLive" | "requestOwnedMainCredential"
|
|
541
542
|
> | undefined {
|
|
542
543
|
if (!selectionOptions) return undefined;
|
|
543
544
|
return {
|
|
@@ -547,6 +548,9 @@ export function sharedStateSelectionOptions(
|
|
|
547
548
|
...(selectionOptions.isMainAccountTokenLive
|
|
548
549
|
? { isMainAccountTokenLive: selectionOptions.isMainAccountTokenLive }
|
|
549
550
|
: {}),
|
|
551
|
+
...(selectionOptions.requestOwnedMainCredential !== undefined
|
|
552
|
+
? { requestOwnedMainCredential: selectionOptions.requestOwnedMainCredential }
|
|
553
|
+
: {}),
|
|
550
554
|
};
|
|
551
555
|
}
|
|
552
556
|
|
|
@@ -585,6 +589,7 @@ export function pickLowerUsageAccount(
|
|
|
585
589
|
getAccountQuota(id),
|
|
586
590
|
getPoolAccountPlanForSelection(config, id, selectionOptions),
|
|
587
591
|
now,
|
|
592
|
+
codexAccountUsesCreditsAfterLimit(config, id),
|
|
588
593
|
);
|
|
589
594
|
if (usage < bestUsage) {
|
|
590
595
|
best = id;
|
|
@@ -608,6 +613,7 @@ export function pickLowestUsageAmong(
|
|
|
608
613
|
getAccountQuota(id),
|
|
609
614
|
getPoolAccountPlanForSelection(config, id, selectionOptions),
|
|
610
615
|
now,
|
|
616
|
+
codexAccountUsesCreditsAfterLimit(config, id),
|
|
611
617
|
);
|
|
612
618
|
if (usage < bestUsage) {
|
|
613
619
|
best = id;
|
|
@@ -785,6 +791,7 @@ export function applyQuotaAutoSwitch(
|
|
|
785
791
|
quota,
|
|
786
792
|
getPoolAccountPlanForSelection(config, active, selectionOptions),
|
|
787
793
|
now,
|
|
794
|
+
codexAccountUsesCreditsAfterLimit(config, active),
|
|
788
795
|
);
|
|
789
796
|
// Unknown usage is not evidence that a user's explicit selection crossed the
|
|
790
797
|
// threshold. Wait for quota priming instead of rotating among guesses.
|
package/src/codex/routing.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { codexAccountUsesCreditsAfterLimit } from "./account-credit-use";
|
|
1
2
|
import { clearIdleWindowSteering, pickIdleWindowAccount } from "./routing/idle-window";
|
|
2
3
|
import { getEffectiveCodexAutoSwitchThreshold } from "./account-auto-switch";
|
|
3
4
|
import { codexQuotaHasFreshUsage } from "./quota-observation-freshness";
|
|
@@ -522,7 +523,7 @@ function pickAffinityPriorityFailback(
|
|
|
522
523
|
|| now - quota.updatedAt >= CODEX_PRIORITY_FAILBACK_REFRESH_MS
|
|
523
524
|
|| (quota.shortObservedAt !== undefined
|
|
524
525
|
&& now - quota.shortObservedAt >= CODEX_PRIORITY_FAILBACK_REFRESH_MS)) return false;
|
|
525
|
-
const usage = computeCodexUsageScore(quota, plan, now);
|
|
526
|
+
const usage = computeCodexUsageScore(quota, plan, now, codexAccountUsesCreditsAfterLimit(config, id));
|
|
526
527
|
const threshold = getEffectiveCodexAutoSwitchThreshold(config, id);
|
|
527
528
|
return !isUnknownUsage(usage) && usage < 100 && (threshold <= 0 || usage < threshold);
|
|
528
529
|
});
|
|
@@ -575,7 +576,7 @@ function previewReusableAffinityAccount(
|
|
|
575
576
|
const usage = computeCodexUsageScore(
|
|
576
577
|
getAccountQuota(entry.accountId),
|
|
577
578
|
getPoolAccountPlanForSelection(config, entry.accountId, selectionOptions),
|
|
578
|
-
|
|
579
|
+
now, codexAccountUsesCreditsAfterLimit(config, entry.accountId),
|
|
579
580
|
);
|
|
580
581
|
// Preview must agree with resolve: this is the second copy of the same rule, and the
|
|
581
582
|
// suite asserts the two answer identically.
|
|
@@ -605,7 +606,7 @@ function resetFirstAffinityReplacement(
|
|
|
605
606
|
): string | null {
|
|
606
607
|
const threshold = getEffectiveCodexAutoSwitchThreshold(config, entry.accountId);
|
|
607
608
|
if (threshold <= 0) return null;
|
|
608
|
-
const usage = computeCodexUsageScore(getAccountQuota(entry.accountId), getPoolAccountPlanForSelection(config, entry.accountId, selectionOptions), now);
|
|
609
|
+
const usage = computeCodexUsageScore(getAccountQuota(entry.accountId), getPoolAccountPlanForSelection(config, entry.accountId, selectionOptions), now, codexAccountUsesCreditsAfterLimit(config, entry.accountId));
|
|
609
610
|
if (!mayRebindAffinityForQuota(config, entry.accountId, usage, threshold, selectionOptions)) return null;
|
|
610
611
|
const candidates = getEligiblePoolAccounts(config, entry.accountId, now, quotaScope, selectionOptions, true)
|
|
611
612
|
// Headroom alone answers true for an UNMEASURED account, which is the right default for an
|
|
@@ -618,7 +619,7 @@ function resetFirstAffinityReplacement(
|
|
|
618
619
|
return !isUnknownUsage(computeCodexUsageScore(
|
|
619
620
|
getAccountQuota(id),
|
|
620
621
|
getPoolAccountPlanForSelection(config, id, selectionOptions),
|
|
621
|
-
now,
|
|
622
|
+
now, codexAccountUsesCreditsAfterLimit(config, id),
|
|
622
623
|
));
|
|
623
624
|
});
|
|
624
625
|
return pickResetFirstCodexAccount(config, candidates, now, selectionOptions);
|
|
@@ -664,10 +665,9 @@ function pickCacheSafeQuotaReplacement(
|
|
|
664
665
|
).filter(id => hasCodexQuotaHeadroom(config, id, selectionOptions, now));
|
|
665
666
|
const best = pickLowestUsageAmong(config, candidates, selectionOptions, now);
|
|
666
667
|
if (best === null || best === boundAccountId) return null;
|
|
667
|
-
const bestUsage = computeCodexUsageScore(
|
|
668
|
-
getAccountQuota(best),
|
|
668
|
+
const bestUsage = computeCodexUsageScore(getAccountQuota(best),
|
|
669
669
|
getPoolAccountPlanForSelection(config, best, selectionOptions),
|
|
670
|
-
now,
|
|
670
|
+
now, codexAccountUsesCreditsAfterLimit(config, best),
|
|
671
671
|
);
|
|
672
672
|
return bestUsage < boundUsage ? best : null;
|
|
673
673
|
}
|
|
@@ -698,7 +698,7 @@ function reevaluateAffinityQuota(
|
|
|
698
698
|
? computeCodexUsageScore(
|
|
699
699
|
getAccountQuota(entry.accountId),
|
|
700
700
|
getPoolAccountPlanForSelection(config, entry.accountId, selectionOptions),
|
|
701
|
-
|
|
701
|
+
now, codexAccountUsesCreditsAfterLimit(config, entry.accountId),
|
|
702
702
|
)
|
|
703
703
|
: 0;
|
|
704
704
|
// One bar, used for BOTH the rebind decision and the re-score interval. Keying the short
|
|
@@ -823,7 +823,7 @@ export function previewCodexAccountForRequest(
|
|
|
823
823
|
const usage = computeCodexUsageScore(
|
|
824
824
|
getAccountQuota(active),
|
|
825
825
|
getPoolAccountPlanForSelection(config, active, selectionOptions),
|
|
826
|
-
now,
|
|
826
|
+
now, codexAccountUsesCreditsAfterLimit(config, active),
|
|
827
827
|
);
|
|
828
828
|
if (!isUnknownUsage(usage) && usage >= threshold) {
|
|
829
829
|
active = pickLowerUsageAccount(config, active, usage, now, quotaScope, selectionOptions);
|
package/src/codex/runtime.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { delimiter, join } from "node:path";
|
|
|
5
5
|
import { atomicWriteFile, getConfigDir } from "../config";
|
|
6
6
|
import { codexExecInvocation, isSpawnableCodexCandidate } from "./exec-invocation";
|
|
7
7
|
import { resolveCodexHomeDir } from "./home";
|
|
8
|
+
import { decodeOverlayState } from "./shim-state-file";
|
|
8
9
|
import { redactSecretString, redactUserPath } from "../lib/redact";
|
|
9
10
|
|
|
10
11
|
export type CodexRuntimeSource =
|
|
@@ -516,13 +517,18 @@ function shimCandidates(deps: ResolveCodexRuntimeDeps): string[] {
|
|
|
516
517
|
const platform = deps.platform ?? process.platform;
|
|
517
518
|
try {
|
|
518
519
|
const state = JSON.parse(read(join(configDir, "codex-shim.json"), "utf8")) as {
|
|
520
|
+
schema?: unknown;
|
|
521
|
+
mode?: unknown;
|
|
519
522
|
wrapperPath?: unknown;
|
|
520
523
|
originalPath?: unknown;
|
|
521
524
|
backupPath?: unknown;
|
|
522
525
|
wrappers?: Array<{ wrapperPath?: unknown; originalPath?: unknown; backupPath?: unknown }>;
|
|
523
526
|
};
|
|
527
|
+
const overlay = decodeOverlayState(state, configDir);
|
|
528
|
+
if ((state.schema !== undefined || state.mode !== undefined) && !overlay) return [];
|
|
524
529
|
const files = Array.isArray(state.wrappers) && state.wrappers.length > 0 ? state.wrappers : [state];
|
|
525
|
-
const out: string[] =
|
|
530
|
+
const out: string[] = overlay && isSpawnableCodexCandidate(overlay.launcherPath, platform)
|
|
531
|
+
? [overlay.launcherPath] : [];
|
|
526
532
|
for (const file of files) {
|
|
527
533
|
for (const value of [file.backupPath, file.originalPath, file.wrapperPath]) {
|
|
528
534
|
if (typeof value !== "string" || value.length === 0) continue;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readFileSync } from "node:fs";
|
|
2
|
+
import { SHIM_MARKER, UNIX_SHIM_REVISION_MARKER } from "./shim-templates";
|
|
3
|
+
import { readStateResult, stateFiles, statePath } from "./shim-state-file";
|
|
4
|
+
import { overlayDiagnostic } from "./shim-overlay";
|
|
5
|
+
|
|
6
|
+
export function isShim(path: string): boolean {
|
|
7
|
+
try {
|
|
8
|
+
return readFileSync(path, "utf8").includes(SHIM_MARKER);
|
|
9
|
+
} catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isHealthyShim(path: string, platform: NodeJS.Platform): boolean {
|
|
15
|
+
try {
|
|
16
|
+
const content = readFileSync(path, "utf8");
|
|
17
|
+
if (content.length < 180 || !content.includes(SHIM_MARKER) || !content.includes("ensure")) return false;
|
|
18
|
+
if (platform !== "win32" && !content.includes(UNIX_SHIM_REVISION_MARKER)) return false;
|
|
19
|
+
if (platform !== "win32" && (lstatSync(path).mode & 0o111) === 0) return false;
|
|
20
|
+
return true;
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface CodexShimDiagnostic {
|
|
27
|
+
installed: boolean;
|
|
28
|
+
healthy: boolean;
|
|
29
|
+
summary: string;
|
|
30
|
+
runnable?: boolean;
|
|
31
|
+
active?: boolean | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Structured, secret-free shim state for CLI/GUI lifecycle diagnostics. */
|
|
35
|
+
export function diagnoseCodexShim(): CodexShimDiagnostic {
|
|
36
|
+
const result = readStateResult();
|
|
37
|
+
const state = result.state;
|
|
38
|
+
if (!state) {
|
|
39
|
+
if (result.present) {
|
|
40
|
+
return {
|
|
41
|
+
installed: true,
|
|
42
|
+
healthy: false,
|
|
43
|
+
runnable: false,
|
|
44
|
+
active: null,
|
|
45
|
+
summary: result.warning ?? `Codex autostart shim state is invalid or corrupt at ${statePath()}. Reinstall or remove the shim.`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
installed: false,
|
|
50
|
+
healthy: false,
|
|
51
|
+
summary: "Codex autostart shim is not installed.",
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (state.mode === "path-overlay") return overlayDiagnostic(state);
|
|
55
|
+
const files = stateFiles(state);
|
|
56
|
+
const healthy = files.length > 0 && files.every(file => file.preserveOnly
|
|
57
|
+
? existsSync(file.backupPath) && !existsSync(file.originalPath)
|
|
58
|
+
: existsSync(file.wrapperPath)
|
|
59
|
+
&& (existsSync(file.backupPath) || (file.realPath ? existsSync(file.realPath) : false))
|
|
60
|
+
&& isHealthyShim(file.wrapperPath, state.platform));
|
|
61
|
+
const summary = files.map(file => {
|
|
62
|
+
const wrapper = existsSync(file.wrapperPath)
|
|
63
|
+
? isShim(file.wrapperPath)
|
|
64
|
+
? "shim present"
|
|
65
|
+
: "present but not an opencodex shim"
|
|
66
|
+
: "missing";
|
|
67
|
+
const backup = existsSync(file.backupPath) ? "present" : "missing";
|
|
68
|
+
return `Codex autostart shim: wrapper ${wrapper} at ${file.wrapperPath}; original backup ${backup} at ${file.backupPath}.`;
|
|
69
|
+
}).join("\n");
|
|
70
|
+
// Presence alone is not health: a damaged wrapper still reads "shim present", so state the verdict.
|
|
71
|
+
const migration = healthy && state.platform !== "win32"
|
|
72
|
+
? " Legacy Unix shim installed in place; automatic repair does not migrate it. Run ocx codex-shim install, then source the printed codex-shell-env.sh path and add that line after PATH setup in your shell startup file."
|
|
73
|
+
: "";
|
|
74
|
+
return { installed: true, healthy, summary: healthy ? `${summary}${migration}`
|
|
75
|
+
: `${summary}\nCodex autostart shim is unhealthy. Run ocx codex-shim install to repair it.` };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function codexShimStatus(): string {
|
|
79
|
+
return diagnoseCodexShim().summary;
|
|
80
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
closeSync,
|
|
3
|
+
constants,
|
|
4
|
+
fstatSync,
|
|
3
5
|
existsSync,
|
|
4
6
|
lstatSync,
|
|
5
7
|
linkSync,
|
|
@@ -33,8 +35,9 @@ interface StableShimPathProbe {
|
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
function readShimProbePrefix(path: string): string {
|
|
36
|
-
const fd = openSync(path, "r");
|
|
38
|
+
const fd = openSync(path, process.platform === "win32" ? "r" : constants.O_RDONLY | constants.O_NONBLOCK);
|
|
37
39
|
try {
|
|
40
|
+
if (process.platform !== "win32" && !fstatSync(fd).isFile()) throw new Error("Shim probe entry is not a regular file");
|
|
38
41
|
const buffer = Buffer.allocUnsafe(CODEX_SHIM_PROBE_BYTES);
|
|
39
42
|
const bytesRead = readSync(fd, buffer, 0, buffer.length, 0);
|
|
40
43
|
return buffer.toString("utf8", 0, bytesRead);
|
|
@@ -162,14 +165,16 @@ function shimPathFingerprint(path: string): ShimPathFingerprint | null {
|
|
|
162
165
|
* fails EEXIST on an occupied destination. Checking existence and then renaming
|
|
163
166
|
* would reintroduce exactly the race this function exists to close.
|
|
164
167
|
*/
|
|
165
|
-
function restoreWithoutReplacing(from: string, to: string): void {
|
|
168
|
+
function restoreWithoutReplacing(from: string, to: string, guard?: () => void): void {
|
|
166
169
|
const source = lstatSync(from);
|
|
167
170
|
if (source.isSymbolicLink()) {
|
|
168
171
|
symlinkSync(readlinkSync(from), to);
|
|
172
|
+
guard?.();
|
|
169
173
|
unlinkSync(from);
|
|
170
174
|
return;
|
|
171
175
|
}
|
|
172
176
|
linkSync(from, to);
|
|
177
|
+
guard?.();
|
|
173
178
|
unlinkSync(from);
|
|
174
179
|
}
|
|
175
180
|
|