@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.
Files changed (353) hide show
  1. package/bin/ocx.mjs +11 -10
  2. package/gui/dist/assets/App-CppMz-6w.js +54 -0
  3. package/gui/dist/assets/App-lCIslZ1L.css +1 -0
  4. package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-CJWQqtVv.js} +1 -1
  5. package/gui/dist/assets/index-DQt9srNi.js +94 -0
  6. package/gui/dist/assets/index-DmQB8D_R.css +1 -0
  7. package/gui/dist/assets/quota-summary-BMsyoZH2.js +1 -0
  8. package/gui/dist/index.html +4 -3
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic/beta-allowlist.ts +19 -2
  11. package/src/adapters/anthropic/client-identity.ts +13 -4
  12. package/src/adapters/anthropic/native-client-preamble.ts +31 -0
  13. package/src/adapters/anthropic/passthrough.ts +103 -25
  14. package/src/adapters/antigravity-refusal.ts +28 -0
  15. package/src/adapters/cursor/protobuf-request.ts +6 -0
  16. package/src/adapters/cursor/request-builder.ts +3 -0
  17. package/src/adapters/cursor/structured-output.ts +13 -0
  18. package/src/adapters/cursor/types.ts +2 -0
  19. package/src/adapters/devin/cloud-direct/chat.ts +7 -2
  20. package/src/adapters/devin/trajectory.ts +49 -0
  21. package/src/adapters/devin.ts +13 -0
  22. package/src/adapters/ollama-native.ts +62 -19
  23. package/src/adapters/openai-responses/passthrough.ts +133 -103
  24. package/src/adapters/openai-responses/request-strips.ts +10 -4
  25. package/src/adapters/zed.ts +26 -4
  26. package/src/bridge/response-json.ts +14 -14
  27. package/src/bridge/sse.ts +15 -5
  28. package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
  29. package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
  30. package/src/claude/agents-inject.ts +4 -43
  31. package/src/claude/gateway-cache.ts +16 -13
  32. package/src/claude/inbound-model-options.ts +9 -2
  33. package/src/claude/intercept/cli-catalog.ts +27 -16
  34. package/src/claude/intercept/client-hello.ts +103 -0
  35. package/src/claude/intercept/model-bindings.ts +3 -2
  36. package/src/claude/intercept/picker-bootstrap.ts +37 -16
  37. package/src/claude/intercept/picker-budget.ts +41 -0
  38. package/src/claude/intercept/picker-listener.ts +279 -66
  39. package/src/claude/outbound.ts +2 -0
  40. package/src/claude/subagent-force-status.ts +53 -0
  41. package/src/claude/subagent-model.ts +93 -0
  42. package/src/cli/access-audio-input.ts +84 -0
  43. package/src/cli/access-audio-live.ts +99 -0
  44. package/src/cli/access-audio.ts +123 -0
  45. package/src/cli/access-data-client.ts +127 -0
  46. package/src/cli/access-data-plane.ts +104 -0
  47. package/src/cli/access-data-response.ts +76 -0
  48. package/src/cli/access.ts +65 -2
  49. package/src/cli/account-api.ts +33 -6
  50. package/src/cli/account-auth.ts +111 -42
  51. package/src/cli/account-key-quota.ts +118 -0
  52. package/src/cli/account-main.ts +11 -4
  53. package/src/cli/account-next-actions.ts +63 -0
  54. package/src/cli/account-policy-dto.ts +68 -0
  55. package/src/cli/account-policy.ts +167 -0
  56. package/src/cli/account-target.ts +8 -0
  57. package/src/cli/account.ts +37 -9
  58. package/src/cli/agent-runtime-settings.ts +130 -0
  59. package/src/cli/agent-settings.ts +101 -0
  60. package/src/cli/agent.ts +29 -2
  61. package/src/cli/alias.ts +7 -2
  62. package/src/cli/aside-profiles.ts +2 -2
  63. package/src/cli/capabilities-access-remote.ts +147 -0
  64. package/src/cli/capabilities-accounts.ts +195 -0
  65. package/src/cli/capabilities-agents-routing.ts +414 -0
  66. package/src/cli/capabilities-base.ts +783 -0
  67. package/src/cli/capabilities-command.ts +1 -0
  68. package/src/cli/capabilities-integrations.ts +264 -0
  69. package/src/cli/capabilities-lab.ts +214 -0
  70. package/src/cli/capabilities-observe-system.ts +662 -0
  71. package/src/cli/capabilities-provider-models.ts +302 -0
  72. package/src/cli/capabilities.ts +30 -1190
  73. package/src/cli/capability-types.ts +54 -0
  74. package/src/cli/catalog-command-result.ts +57 -0
  75. package/src/cli/chatgpt-command.ts +35 -9
  76. package/src/cli/claude-agent-startup-sync.ts +17 -0
  77. package/src/cli/claude-desktop-profile.ts +100 -0
  78. package/src/cli/claude-desktop.ts +39 -6
  79. package/src/cli/claude.ts +22 -6
  80. package/src/cli/codex-shim-autorestore.ts +2 -1
  81. package/src/cli/codex-shim-readiness.ts +109 -0
  82. package/src/cli/combo-input.ts +88 -0
  83. package/src/cli/combo-result.ts +52 -0
  84. package/src/cli/combo-stats.ts +123 -0
  85. package/src/cli/combo.ts +37 -14
  86. package/src/cli/companion-timeline.ts +142 -0
  87. package/src/cli/companion-usage.ts +146 -0
  88. package/src/cli/companion.ts +12 -3
  89. package/src/cli/config-command.ts +24 -8
  90. package/src/cli/connect.ts +19 -0
  91. package/src/cli/dispatch.ts +55 -82
  92. package/src/cli/doctor.ts +22 -4
  93. package/src/cli/export-command.ts +20 -9
  94. package/src/cli/gui.ts +8 -3
  95. package/src/cli/help-catalog.ts +101 -0
  96. package/src/cli/help-models-context.ts +17 -0
  97. package/src/cli/help-navigation.ts +50 -0
  98. package/src/cli/help-recovery.ts +66 -0
  99. package/src/cli/help.ts +69 -11
  100. package/src/cli/index.ts +38 -27
  101. package/src/cli/injection-follow.ts +35 -0
  102. package/src/cli/inspect.ts +5 -0
  103. package/src/cli/integration-aside-sync.ts +69 -0
  104. package/src/cli/integration-cursor.ts +106 -0
  105. package/src/cli/integration-input.ts +60 -0
  106. package/src/cli/integration-journal.ts +49 -0
  107. package/src/cli/integration-plan-dto.ts +156 -0
  108. package/src/cli/integration-preview.ts +215 -0
  109. package/src/cli/integrations.ts +24 -18
  110. package/src/cli/json-input.ts +134 -0
  111. package/src/cli/link.ts +32 -4
  112. package/src/cli/local-sync-result.ts +41 -0
  113. package/src/cli/log-follow.ts +84 -0
  114. package/src/cli/log-view-filter.ts +146 -0
  115. package/src/cli/logout-command.ts +44 -0
  116. package/src/cli/model-picker-ordering.ts +137 -0
  117. package/src/cli/models-custom-input.ts +107 -0
  118. package/src/cli/models-custom-runtime.ts +112 -0
  119. package/src/cli/models-order.ts +194 -0
  120. package/src/cli/models-runtime-subcommands.ts +2 -0
  121. package/src/cli/models-runtime.ts +9 -1
  122. package/src/cli/models.ts +66 -81
  123. package/src/cli/observe-stream.ts +142 -0
  124. package/src/cli/observe.ts +75 -34
  125. package/src/cli/opencode.ts +54 -10
  126. package/src/cli/provider-batch.ts +77 -0
  127. package/src/cli/provider-lifecycle-runtime.ts +170 -0
  128. package/src/cli/provider-result.ts +124 -0
  129. package/src/cli/provider-runtime.ts +15 -12
  130. package/src/cli/provider-settings.ts +143 -0
  131. package/src/cli/provider.ts +116 -64
  132. package/src/cli/registry.ts +90 -24
  133. package/src/cli/remote-workspace-hub.ts +83 -0
  134. package/src/cli/remote-workspace.ts +6 -1
  135. package/src/cli/restart-failure.ts +27 -0
  136. package/src/cli/root.ts +42 -9
  137. package/src/cli/route-policy-write.ts +137 -0
  138. package/src/cli/route-policy.ts +8 -0
  139. package/src/cli/runtime-api.ts +57 -80
  140. package/src/cli/secret-args.ts +149 -0
  141. package/src/cli/settings-result.ts +30 -0
  142. package/src/cli/storage.ts +77 -7
  143. package/src/cli/system-command.ts +60 -2
  144. package/src/cli/system-restart-client.ts +20 -3
  145. package/src/cli/system-settings-parity.ts +83 -0
  146. package/src/cli/tray-proxy.ts +3 -2
  147. package/src/cli/update-restart-candidate.ts +21 -0
  148. package/src/cli/update-restart-child.ts +81 -0
  149. package/src/cli/update-restart-home.ts +38 -0
  150. package/src/cli/update-restart-transport.ts +193 -0
  151. package/src/cli/update-restart.ts +260 -0
  152. package/src/cli/usage-model-search.ts +58 -0
  153. package/src/cli/usage-report.ts +13 -5
  154. package/src/cli/v2-input.ts +66 -0
  155. package/src/cli/v2-local-output.ts +37 -0
  156. package/src/cli/v2-runtime.ts +90 -0
  157. package/src/cli/v2.ts +102 -144
  158. package/src/clients/config-export/contracts.ts +100 -0
  159. package/src/clients/config-export/droid.ts +54 -2
  160. package/src/clients/config-export/dsh.ts +21 -0
  161. package/src/clients/config-export/kilo.ts +12 -2
  162. package/src/clients/config-export/model-metadata.ts +10 -4
  163. package/src/clients/config-export/reasoning-metadata.ts +47 -0
  164. package/src/clients/config-export.ts +53 -67
  165. package/src/clients/omo-role-models.ts +17 -3
  166. package/src/codex/account-credit-use.ts +4 -2
  167. package/src/codex/account-runtime-state.ts +21 -0
  168. package/src/codex/account-store.ts +11 -47
  169. package/src/codex/account-usability.ts +11 -8
  170. package/src/codex/auth-api/account-list.ts +73 -63
  171. package/src/codex/auth-api/main-account-probe.ts +25 -12
  172. package/src/codex/auth-api/pool-mode-gate.ts +6 -9
  173. package/src/codex/auth-api/pool-quota-probe.ts +2 -4
  174. package/src/codex/auth-api/reset-credit-service.ts +3 -2
  175. package/src/codex/auth-context.ts +30 -11
  176. package/src/codex/catalog/aggregation.ts +3 -0
  177. package/src/codex/catalog/build-entries.ts +1 -1
  178. package/src/codex/catalog/bundled.ts +9 -2
  179. package/src/codex/catalog/heal-observation.ts +66 -0
  180. package/src/codex/catalog/model-hints.ts +2 -1
  181. package/src/codex/catalog/model-visibility.ts +6 -3
  182. package/src/codex/catalog/parsing.ts +2 -0
  183. package/src/codex/catalog/provider-models.ts +13 -4
  184. package/src/codex/catalog/publication-observer.ts +19 -0
  185. package/src/codex/catalog/remote.ts +13 -15
  186. package/src/codex/catalog/restore.ts +19 -6
  187. package/src/codex/catalog/retained-sync.ts +60 -11
  188. package/src/codex/catalog/routed-removal.ts +116 -0
  189. package/src/codex/catalog/write-audit-contract.ts +21 -0
  190. package/src/codex/catalog/write-audit.ts +171 -0
  191. package/src/codex/catalog-self-heal.ts +284 -0
  192. package/src/codex/catalog-write-serialization.ts +71 -7
  193. package/src/codex/chatgpt-refresh-failure.ts +56 -0
  194. package/src/codex/codex-home-owner.ts +124 -0
  195. package/src/codex/convergence.ts +57 -16
  196. package/src/codex/features.ts +8 -1
  197. package/src/codex/inject/restore.ts +46 -4
  198. package/src/codex/inject.ts +11 -1
  199. package/src/codex/internal/catalog-writer.ts +120 -13
  200. package/src/codex/journal.ts +49 -13
  201. package/src/codex/main-account.ts +144 -11
  202. package/src/codex/management-convergence.ts +3 -0
  203. package/src/codex/quota-refresh-outcome.ts +17 -2
  204. package/src/codex/quota-types.ts +36 -1
  205. package/src/codex/quota.ts +67 -16
  206. package/src/codex/refresh.ts +20 -9
  207. package/src/codex/routing/cooldown-math.ts +10 -4
  208. package/src/codex/routing/selection.ts +11 -4
  209. package/src/codex/routing.ts +9 -9
  210. package/src/codex/runtime.ts +7 -1
  211. package/src/codex/shim-diagnostics.ts +80 -0
  212. package/src/codex/shim-fingerprint.ts +7 -2
  213. package/src/codex/shim-migration.ts +153 -0
  214. package/src/codex/shim-overlay.ts +509 -0
  215. package/src/codex/shim-path-resolution.ts +145 -0
  216. package/src/codex/shim-probe.ts +4 -3
  217. package/src/codex/shim-state-file.ts +93 -7
  218. package/src/codex/shim.ts +54 -107
  219. package/src/codex/subagent-model-fallback.ts +2 -1
  220. package/src/codex/sync.ts +24 -4
  221. package/src/combos/failover.ts +101 -1
  222. package/src/combos/index.ts +1 -0
  223. package/src/combos/jev.ts +13 -3
  224. package/src/combos/resolve.ts +12 -0
  225. package/src/config/atomic-write.ts +13 -8
  226. package/src/config/diagnostics.ts +23 -1
  227. package/src/config/load-degrade.ts +7 -0
  228. package/src/config/provider-validation.ts +3 -0
  229. package/src/config/schema/chatgpt-desktop.ts +22 -0
  230. package/src/config/schema/compaction-triggers.ts +8 -0
  231. package/src/config/schema/config-schema.ts +43 -14
  232. package/src/config/schema/leaf-validators.ts +8 -3
  233. package/src/config/subagent-models.ts +12 -0
  234. package/src/generated/compatibility-version.json +645 -237
  235. package/src/grok/session-identity.ts +33 -0
  236. package/src/integrations/catalog-refresh.ts +1 -1
  237. package/src/integrations/merge.ts +13 -5
  238. package/src/integrations/mutation-plan.ts +50 -5
  239. package/src/integrations/omp-yaml-source.ts +181 -1
  240. package/src/integrations/ownership-policy.ts +66 -0
  241. package/src/integrations/registry.ts +50 -1
  242. package/src/integrations/state.ts +106 -19
  243. package/src/integrations/target.ts +25 -2
  244. package/src/integrations/writer.ts +107 -35
  245. package/src/lib/gui-pair-capability.ts +17 -0
  246. package/src/lib/package-tree-integrity.ts +42 -1
  247. package/src/lib/provider-client-headers.ts +62 -0
  248. package/src/lib/proxy-env.ts +3 -0
  249. package/src/lib/test-home-guard.ts +2 -1
  250. package/src/oauth/anthropic-account-refusal.ts +19 -8
  251. package/src/oauth/anthropic-continuity.ts +65 -0
  252. package/src/oauth/anthropic-identity.ts +75 -0
  253. package/src/oauth/anthropic-routing.ts +20 -15
  254. package/src/oauth/anthropic.ts +34 -3
  255. package/src/oauth/generic-account-failover.ts +21 -0
  256. package/src/oauth/health.ts +32 -5
  257. package/src/oauth/index.ts +42 -35
  258. package/src/oauth/local-token-detect.ts +7 -0
  259. package/src/oauth/pool-settings-capability.ts +9 -2
  260. package/src/oauth/refresh-policy.ts +2 -0
  261. package/src/oauth/store.ts +55 -6
  262. package/src/oauth/types.ts +9 -0
  263. package/src/protocols/encoders/messages.ts +4 -3
  264. package/src/protocols/plan-snapshot.ts +1 -1
  265. package/src/protocols/settings.ts +28 -4
  266. package/src/providers/antigravity-effort-families.ts +75 -0
  267. package/src/providers/antigravity-models.ts +54 -16
  268. package/src/providers/antigravity-wire-snapshot.ts +66 -0
  269. package/src/providers/derive.ts +2 -0
  270. package/src/providers/model-rename-fields.ts +3 -0
  271. package/src/providers/new-model-policy.ts +48 -10
  272. package/src/providers/registry/entries-extended.ts +3 -0
  273. package/src/providers/registry/model-ids.ts +2 -0
  274. package/src/providers/registry/model-seeds.ts +5 -0
  275. package/src/providers/registry/types.ts +4 -0
  276. package/src/responses/compaction.ts +19 -0
  277. package/src/router.ts +6 -0
  278. package/src/server/auth-cors.ts +9 -0
  279. package/src/server/caller-session-identity.ts +32 -0
  280. package/src/server/chat-completions.ts +10 -0
  281. package/src/server/claude-messages.ts +29 -15
  282. package/src/server/droid-reasoning-default.ts +50 -0
  283. package/src/server/github-copilot-responses-repair.ts +3 -6
  284. package/src/server/grok-responses-control-frame.ts +2 -2
  285. package/src/server/grok-responses-snapshot-repair.ts +4 -5
  286. package/src/server/gui-session.ts +33 -11
  287. package/src/server/index/claude-intercept-lifecycle.ts +4 -0
  288. package/src/server/index/serve-options.ts +13 -7
  289. package/src/server/index.ts +1 -1
  290. package/src/server/management/agent-settings-routes.ts +12 -134
  291. package/src/server/management/config-routes.ts +1 -1
  292. package/src/server/management/context.ts +8 -1
  293. package/src/server/management/cursor-integration-routes.ts +3 -3
  294. package/src/server/management/integration-routes.ts +69 -11
  295. package/src/server/management/link-routes.ts +20 -13
  296. package/src/server/management/model-routes.ts +3 -3
  297. package/src/server/management/model-row-export-metadata.ts +150 -0
  298. package/src/server/management/model-rows.ts +148 -9
  299. package/src/server/management/native-integration-routes.ts +2 -2
  300. package/src/server/management/oauth-account-routes.ts +96 -30
  301. package/src/server/management/route-registry.ts +29 -27
  302. package/src/server/management/shared.ts +19 -5
  303. package/src/server/management/subagent-model-routes.ts +214 -0
  304. package/src/server/messages-native-eligibility.ts +3 -6
  305. package/src/server/messages-native-oauth.ts +82 -35
  306. package/src/server/messages-native.ts +78 -16
  307. package/src/server/relay.ts +14 -44
  308. package/src/server/request-log-generation-window.ts +38 -0
  309. package/src/server/request-log.ts +13 -0
  310. package/src/server/responses/adapter-dispatch.ts +152 -28
  311. package/src/server/responses/codex-auth-error.ts +10 -2
  312. package/src/server/responses/combo-stream-preflight.ts +29 -3
  313. package/src/server/responses/compaction-routing.ts +10 -1
  314. package/src/server/responses/core-auth.ts +67 -30
  315. package/src/server/responses/core-codex-account.ts +6 -3
  316. package/src/server/responses/core-combo-failure.ts +5 -1
  317. package/src/server/responses/core-combo-native.ts +7 -1
  318. package/src/server/responses/core-combo.ts +64 -7
  319. package/src/server/responses/core-normalize.ts +7 -0
  320. package/src/server/responses/core-options.ts +5 -0
  321. package/src/server/responses/fetch-helpers.ts +9 -0
  322. package/src/server/responses/passthrough-delivery.ts +1 -1
  323. package/src/server/responses/policy-fallback.ts +1 -1
  324. package/src/server/responses/request-prepare.ts +5 -0
  325. package/src/server/responses/request-spend.ts +30 -30
  326. package/src/server/responses-custom-tool-repair.ts +2 -2
  327. package/src/server/responses-hosted-image-display.ts +70 -9
  328. package/src/server/responses-terminal-repair.ts +15 -2
  329. package/src/server/sse-frame-buffer.ts +38 -36
  330. package/src/server/sse-payload-rewrite.ts +78 -49
  331. package/src/server/ws-bridge.ts +1 -11
  332. package/src/service/cli.ts +6 -1
  333. package/src/service/ownership-mutation-lease.d.mts +26 -0
  334. package/src/service/ownership-mutation-lease.mjs +76 -1
  335. package/src/service/windows-taskxml.ts +1 -2
  336. package/src/service-manager-probe.ts +36 -2
  337. package/src/types/config.ts +6 -0
  338. package/src/types/provider.ts +28 -10
  339. package/src/update/index.ts +2 -1
  340. package/src/update/npm-invocation.mjs +35 -3
  341. package/src/update/proxy-liveness-probe.mjs +38 -8
  342. package/src/update/update-failure-guidance.d.mts +6 -0
  343. package/src/update/update-failure-guidance.mjs +30 -5
  344. package/src/usage/expected-prices.ts +9 -0
  345. package/src/usage/log.ts +7 -0
  346. package/src/usage/summary.ts +82 -1
  347. package/src/web-search/loop.ts +11 -8
  348. package/src/web-search/run-turn-loop.ts +10 -7
  349. package/gui/dist/assets/App-CddorHdA.js +0 -51
  350. package/gui/dist/assets/App-DMudtISW.css +0 -1
  351. package/gui/dist/assets/index-Btze1I3T.css +0 -1
  352. package/gui/dist/assets/index-CvnY7E6i.js +0 -86
  353. package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
@@ -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.resetCredits === undefined)) return;
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.resetCredits !== undefined && !snapshotHasUsage(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.resetCredits !== undefined && !snapshotHasUsage(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.resetCredits !== undefined ? next as T : null;
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.resetCredits !== undefined ? filtered : null;
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 neither a quota value/window nor reset credits are available.
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) || resetCredits !== undefined ? quota : null;
1005
+ return hasKnownQuotaValue(quota) || snapshotHasCredits(quota) ? quota : null;
955
1006
  }
956
1007
 
957
1008
 
@@ -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 { CODEX_MODELS_CACHE_PATH } from "./paths";
5
- import { atomicWriteFile } from "../config";
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
- /** Desired OFF observed under K during the catalog commit; no cache write either. */
18
- skippedReason?: "desired_disabled";
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 content = readFileSync(catalogPath, "utf8");
35
- atomicWriteFile(CODEX_MODELS_CACHE_PATH, content);
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 === "desired_disabled" || result.refreshOutcome === "refused") {
54
- // The commit path observed OFF under K. Invalidate nothing: rewriting the
55
- // models cache here would be exactly the routed-cache write the skip refused.
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
- } | null, plan?: unknown, now: number = Date.now()): number {
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) ? CODEX_EXHAUSTED_USAGE_PERCENT : CODEX_UNKNOWN_USAGE_SCORE;
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
- return Math.max(...values);
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, hasMainAccountRefreshGrant } from "../main-account";
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.
@@ -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
- now,
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
- now,
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);
@@ -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