@bitkyc08/opencodex 2.84.0 → 2.87.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (518) hide show
  1. package/README.md +0 -4
  2. package/bin/ocx.mjs +61 -3
  3. package/gui/dist/assets/App-ZfYbLHkB.js +58 -0
  4. package/gui/dist/assets/App-wT3mXqFz.css +1 -0
  5. package/gui/dist/assets/{Tray-C9TGtwad.js → Tray-DCd-A1az.js} +1 -1
  6. package/gui/dist/assets/index-CfYDMj34.js +94 -0
  7. package/gui/dist/assets/index-DPeB9HpV.css +1 -0
  8. package/gui/dist/assets/quota-summary-ChvmKGOk.js +1 -0
  9. package/gui/dist/index.html +2 -2
  10. package/package.json +1 -2
  11. package/src/adapters/anthropic/beta-allowlist.ts +91 -11
  12. package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
  13. package/src/adapters/anthropic/client-identity.ts +1 -1
  14. package/src/adapters/anthropic/passthrough.ts +16 -3
  15. package/src/adapters/anthropic-progress.ts +84 -0
  16. package/src/adapters/anthropic.ts +108 -21
  17. package/src/adapters/azure.ts +19 -1
  18. package/src/adapters/base.ts +2 -0
  19. package/src/adapters/claude-cli/adapter.ts +111 -23
  20. package/src/adapters/claude-cli/profiles.ts +2 -1
  21. package/src/adapters/claude-cli/stable-replay.ts +108 -0
  22. package/src/adapters/claude-cli/usage-admission.ts +230 -0
  23. package/src/adapters/client-fingerprint.ts +24 -0
  24. package/src/adapters/coding-agent/protocol.ts +1 -1
  25. package/src/adapters/coding-agent/turn.ts +40 -11
  26. package/src/adapters/cursor/arg-codec.ts +17 -5
  27. package/src/adapters/cursor/catalog.ts +8 -0
  28. package/src/adapters/cursor/effort-map.ts +3 -0
  29. package/src/adapters/cursor/freeform-args.ts +75 -0
  30. package/src/adapters/cursor/protobuf-events.ts +36 -23
  31. package/src/adapters/dsh-account.ts +97 -0
  32. package/src/adapters/google-antigravity-wire.ts +22 -1
  33. package/src/adapters/google.ts +37 -10
  34. package/src/adapters/openai-chat/messages.ts +4 -3
  35. package/src/adapters/openai-chat/passthrough.ts +8 -2
  36. package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
  37. package/src/adapters/openai-chat/response-events.ts +59 -1
  38. package/src/adapters/openai-chat.ts +8 -5
  39. package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
  40. package/src/adapters/openai-responses/passthrough.ts +57 -125
  41. package/src/adapters/openai-responses/request-strips.ts +25 -3
  42. package/src/adapters/openai-responses/transport.ts +81 -0
  43. package/src/adapters/qoder/adapter.ts +44 -3
  44. package/src/adapters/registry.ts +5 -0
  45. package/src/adapters/zed.ts +26 -1
  46. package/src/advisor/activity.ts +55 -0
  47. package/src/advisor/budget.ts +62 -0
  48. package/src/advisor/consult.ts +223 -0
  49. package/src/advisor/context.ts +238 -0
  50. package/src/advisor/disclosure.ts +20 -0
  51. package/src/advisor/evidence-tool.ts +21 -0
  52. package/src/advisor/evidence.ts +117 -0
  53. package/src/advisor/runtime.ts +353 -0
  54. package/src/advisor/settings.ts +144 -0
  55. package/src/advisor/state.ts +369 -0
  56. package/src/advisor/synthetic-tool.ts +33 -0
  57. package/src/bridge/errors.ts +24 -1
  58. package/src/bridge/internal.ts +3 -1
  59. package/src/bridge/response-json.ts +18 -3
  60. package/src/bridge/sse.ts +58 -8
  61. package/src/bridge/terminal-refusal.ts +59 -0
  62. package/src/chat/inbound.ts +30 -6
  63. package/src/chat/outbound.ts +5 -0
  64. package/src/claude/desktop-3p-library.ts +72 -7
  65. package/src/claude/desktop-3p.ts +75 -19
  66. package/src/claude/desktop-first-party.ts +1 -1
  67. package/src/claude/desktop-picker-platform.ts +4 -0
  68. package/src/claude/desktop-picker-profile.ts +11 -4
  69. package/src/claude/desktop-picker.ts +15 -8
  70. package/src/claude/desktop-profile.ts +10 -1
  71. package/src/claude/desktop-remote-store-io.ts +5 -3
  72. package/src/claude/desktop-system-proxy.ts +50 -4
  73. package/src/claude/inbound-content-options.ts +11 -0
  74. package/src/claude/inbound.ts +154 -50
  75. package/src/claude/intercept/local-ca.ts +8 -1
  76. package/src/claude/intercept/picker-ca-startup.ts +2 -1
  77. package/src/claude/intercept/picker-runtime.ts +6 -5
  78. package/src/claude/intercept/picker-trust-windows.ts +338 -0
  79. package/src/claude/intercept/picker-trust.ts +19 -0
  80. package/src/claude/intercept/port-policy.ts +30 -0
  81. package/src/claude/intercept/runtime.ts +97 -94
  82. package/src/claude/model-default-settings.ts +154 -0
  83. package/src/claude/model-info.ts +7 -7
  84. package/src/claude/outbound.ts +51 -19
  85. package/src/cli/account-api.ts +1 -0
  86. package/src/cli/account-policy-dto.ts +6 -0
  87. package/src/cli/account-policy.ts +34 -5
  88. package/src/cli/account-preservation.ts +44 -0
  89. package/src/cli/account.ts +8 -2
  90. package/src/cli/advisor.ts +165 -0
  91. package/src/cli/auxiliary-listener-preflight.ts +37 -0
  92. package/src/cli/capabilities-accounts.ts +20 -1
  93. package/src/cli/capabilities-agents-routing.ts +23 -2
  94. package/src/cli/capabilities-base.ts +39 -11
  95. package/src/cli/capabilities-observe-system.ts +63 -3
  96. package/src/cli/capabilities-provider-models.ts +11 -0
  97. package/src/cli/chatgpt-command.ts +35 -14
  98. package/src/cli/claude.ts +46 -6
  99. package/src/cli/codex-cli-update.ts +151 -41
  100. package/src/cli/codex-provider.ts +44 -0
  101. package/src/cli/combo-input.ts +10 -1
  102. package/src/cli/combo-result.ts +1 -1
  103. package/src/cli/combo.ts +2 -1
  104. package/src/cli/cross-home-owner.ts +17 -14
  105. package/src/cli/dispatch.ts +30 -5
  106. package/src/cli/doctor.ts +15 -1
  107. package/src/cli/ensure-desktop-cold-start.ts +382 -0
  108. package/src/cli/export-command.ts +1 -1
  109. package/src/cli/help.ts +4 -2
  110. package/src/cli/index.ts +14 -5
  111. package/src/cli/integration-plan-dto.ts +3 -0
  112. package/src/cli/log-history.ts +172 -0
  113. package/src/cli/models-runtime-subcommands.ts +1 -0
  114. package/src/cli/models-runtime.ts +72 -8
  115. package/src/cli/observe-stream.ts +2 -0
  116. package/src/cli/observe.ts +16 -4
  117. package/src/cli/provider.ts +27 -5
  118. package/src/cli/registry.ts +41 -9
  119. package/src/cli/restart-scope.ts +3 -1
  120. package/src/cli/runtime-api.ts +5 -3
  121. package/src/cli/system-command.ts +40 -8
  122. package/src/cli/uninstall-client-state.ts +32 -0
  123. package/src/cli/usage-compare.ts +309 -0
  124. package/src/cli/usage-model-search.ts +4 -4
  125. package/src/cli/usage-report.ts +2 -2
  126. package/src/cli/v2.ts +1 -1
  127. package/src/client/machine-listener.ts +1 -3
  128. package/src/client/runtime.ts +1 -0
  129. package/src/clients/client-path.ts +51 -0
  130. package/src/clients/config-export/contracts.ts +3 -1
  131. package/src/clients/config-export/model-metadata.ts +3 -2
  132. package/src/clients/config-export/qoder.ts +93 -0
  133. package/src/clients/config-export.ts +35 -48
  134. package/src/clients/gentle-shell-path.ts +87 -0
  135. package/src/codex/account-auto-switch.ts +2 -0
  136. package/src/codex/account-identity.ts +29 -0
  137. package/src/codex/account-label.ts +1 -1
  138. package/src/codex/account-lifecycle.ts +21 -5
  139. package/src/codex/account-preservation.ts +28 -0
  140. package/src/codex/account-use-remaining.ts +23 -0
  141. package/src/codex/app-server-processes.ts +97 -7
  142. package/src/codex/app-server-restart-service.ts +55 -4
  143. package/src/codex/auth-api/account-list.ts +4 -0
  144. package/src/codex/auth-api/account-pause-group.ts +6 -50
  145. package/src/codex/auth-api/account-preservation.ts +57 -0
  146. package/src/codex/auth-api/pool-mode-gate.ts +5 -2
  147. package/src/codex/auth-api/routes.ts +8 -0
  148. package/src/codex/auth-api/use-remaining-route.ts +36 -0
  149. package/src/codex/auth-api.ts +0 -1
  150. package/src/codex/auth-context.ts +81 -7
  151. package/src/codex/autostart-health.ts +4 -0
  152. package/src/codex/catalog/auto-review.ts +20 -3
  153. package/src/codex/catalog/build-entries.ts +19 -2
  154. package/src/codex/catalog/combo-member.ts +14 -5
  155. package/src/codex/catalog/configured-tiers.ts +50 -0
  156. package/src/codex/catalog/effort.ts +4 -1
  157. package/src/codex/catalog/gather-capture.ts +2 -0
  158. package/src/codex/catalog/metadata.ts +11 -9
  159. package/src/codex/catalog/model-hints.ts +42 -19
  160. package/src/codex/catalog/model-visibility.ts +3 -1
  161. package/src/codex/catalog/native-ultrafast.ts +106 -0
  162. package/src/codex/catalog/parsing.ts +35 -86
  163. package/src/codex/catalog/provider-models.ts +40 -34
  164. package/src/codex/catalog/retained-sync.ts +2 -0
  165. package/src/codex/catalog/routed-gather.ts +33 -16
  166. package/src/codex/catalog/vendor-context.ts +82 -0
  167. package/src/codex/cli-install-provenance.ts +105 -17
  168. package/src/codex/cli-update-plan.ts +545 -0
  169. package/src/codex/config-write-lock.ts +63 -4
  170. package/src/codex/convergence.ts +2 -0
  171. package/src/codex/desktop-app/picker-freshness.ts +96 -0
  172. package/src/codex/desktop-app/types.ts +2 -0
  173. package/src/codex/desktop-app/windows.ts +29 -16
  174. package/src/codex/desktop-app-restart.ts +25 -2
  175. package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
  176. package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
  177. package/src/codex/desktop-compatibility/connection-store.ts +87 -0
  178. package/src/codex/desktop-compatibility/installed-build.ts +73 -0
  179. package/src/codex/desktop-compatibility/json-body.ts +29 -0
  180. package/src/codex/desktop-compatibility/native-identity.ts +62 -0
  181. package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
  182. package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
  183. package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
  184. package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
  185. package/src/codex/desktop-compatibility/runtime.ts +182 -0
  186. package/src/codex/desktop-compatibility/service.ts +30 -0
  187. package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
  188. package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
  189. package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
  190. package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
  191. package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
  192. package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
  193. package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
  194. package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
  195. package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
  196. package/src/codex/desktop-compatibility/windows-key-protection.ts +165 -0
  197. package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
  198. package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
  199. package/src/codex/features.ts +85 -45
  200. package/src/codex/history-provider.ts +1 -1
  201. package/src/codex/inject/config-write-section.ts +4 -1
  202. package/src/codex/inject/history-failure.ts +27 -0
  203. package/src/codex/inject/remove.ts +18 -6
  204. package/src/codex/inject/restore.ts +24 -2
  205. package/src/codex/inject/routing-classify.ts +4 -3
  206. package/src/codex/inject.ts +18 -27
  207. package/src/codex/journal.ts +16 -1
  208. package/src/codex/main-account-hard-lock.ts +30 -1
  209. package/src/codex/main-account.ts +99 -15
  210. package/src/codex/native-main-refresh-mode.ts +217 -0
  211. package/src/codex/native-profile-startup.ts +1 -1
  212. package/src/codex/ocx-compaction-history.ts +12 -10
  213. package/src/codex/paths.ts +86 -1
  214. package/src/codex/provider-routing.ts +174 -0
  215. package/src/codex/refresh.ts +2 -2
  216. package/src/codex/routing/health-store.ts +2 -2
  217. package/src/codex/routing/pool-recovery.ts +79 -0
  218. package/src/codex/routing/selection.ts +4 -3
  219. package/src/codex/routing/thread-affinity.ts +1 -1
  220. package/src/codex/shim-diagnostics.ts +6 -0
  221. package/src/codex/shim-overlay.ts +7 -2
  222. package/src/codex/signed-in-identity.ts +43 -0
  223. package/src/codex/subagent-model-fallback.ts +1 -1
  224. package/src/codex/sync.ts +6 -1
  225. package/src/codex/user-identity.ts +16 -5
  226. package/src/codex/windows-local-appdata.ts +84 -0
  227. package/src/combos/failover.ts +6 -1
  228. package/src/combos/index.ts +1 -0
  229. package/src/combos/resolve.ts +46 -10
  230. package/src/combos/types.ts +15 -0
  231. package/src/config/diagnostics.ts +35 -7
  232. package/src/config/live-reconcile.ts +51 -2
  233. package/src/config/load-degrade.ts +23 -1
  234. package/src/config/opencode-wire-upgrade.ts +54 -0
  235. package/src/config/opencode-zen-upgrade.ts +52 -0
  236. package/src/config/owner-registry.ts +22 -17
  237. package/src/config/paths.ts +10 -3
  238. package/src/config/provider-validation.ts +45 -2
  239. package/src/config/schema/config-schema.ts +39 -0
  240. package/src/config/schema/desktop-compatibility.ts +9 -0
  241. package/src/config/schema/leaf-validators.ts +39 -9
  242. package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
  243. package/src/config/schema/native-reasoning-retention.ts +26 -0
  244. package/src/config.ts +2 -0
  245. package/src/generated/compatibility-version.json +962 -350
  246. package/src/generated/model-metadata.ts +1 -1
  247. package/src/grok/sync.ts +1 -1
  248. package/src/images/loop.ts +46 -8
  249. package/src/integrations/catalog-refresh.ts +1 -1
  250. package/src/integrations/config-io.ts +20 -2
  251. package/src/integrations/journal.ts +15 -1
  252. package/src/integrations/mutation-plan.ts +2 -0
  253. package/src/integrations/registry.ts +15 -0
  254. package/src/integrations/store.ts +3 -0
  255. package/src/integrations/writer.ts +10 -1
  256. package/src/lib/advisor-activation.ts +16 -0
  257. package/src/lib/background-work.ts +65 -0
  258. package/src/lib/bounded-file-read.ts +58 -0
  259. package/src/lib/bun-path-runtime.d.mts +10 -1
  260. package/src/lib/bun-path-runtime.mjs +62 -3
  261. package/src/lib/codex-restart-contract.ts +15 -0
  262. package/src/lib/desktop-proxy-route.ts +30 -0
  263. package/src/lib/desktop-upstream-tunnel.ts +72 -0
  264. package/src/lib/empty-tool-catalog.ts +62 -0
  265. package/src/lib/errors.ts +26 -3
  266. package/src/lib/gcp-adc.ts +96 -9
  267. package/src/lib/gcp-credential-input.ts +12 -0
  268. package/src/lib/lab-live-route-production.ts +5 -0
  269. package/src/lib/local-internal-call-capability.ts +55 -0
  270. package/src/lib/opaque-proxy-tunnel.ts +217 -0
  271. package/src/lib/package-tree-integrity.ts +2 -2
  272. package/src/lib/redact.ts +2 -2
  273. package/src/lib/socks5-fetch.ts +48 -71
  274. package/src/lib/socks5-handshake.ts +112 -0
  275. package/src/lib/translator-budget.ts +7 -1
  276. package/src/oauth/account-quota-rank.ts +69 -2
  277. package/src/oauth/anthropic-account-refusal.ts +20 -2
  278. package/src/oauth/anthropic-oauth-definitions.ts +3 -1
  279. package/src/oauth/anthropic-routing.ts +1 -21
  280. package/src/oauth/anthropic.ts +38 -7
  281. package/src/oauth/callback-server.ts +27 -1
  282. package/src/oauth/chatgpt-device.ts +1 -1
  283. package/src/oauth/chatgpt-shared.ts +83 -0
  284. package/src/oauth/chatgpt.ts +8 -72
  285. package/src/oauth/dsh.ts +203 -0
  286. package/src/oauth/generic-account-failover.ts +62 -12
  287. package/src/oauth/generic-session-affinity.ts +125 -0
  288. package/src/oauth/index.ts +188 -111
  289. package/src/oauth/kiro-device-login.ts +2 -1
  290. package/src/oauth/kiro.ts +7 -1
  291. package/src/oauth/local-token-detect.ts +21 -1
  292. package/src/oauth/login-cli-errors.ts +44 -0
  293. package/src/oauth/login-cli.ts +69 -7
  294. package/src/oauth/login-flow-state.ts +371 -23
  295. package/src/oauth/meta-muse-errors.ts +41 -0
  296. package/src/oauth/meta-muse-keychain.ts +52 -0
  297. package/src/oauth/meta-muse.ts +51 -84
  298. package/src/oauth/model-discovery-auth.ts +3 -0
  299. package/src/oauth/pool-settings-capability.ts +4 -2
  300. package/src/oauth/refresh-flight-retirement.ts +48 -0
  301. package/src/oauth/session-affinity-key.ts +23 -0
  302. package/src/oauth/store-anthropic-instance.ts +9 -2
  303. package/src/oauth/store.ts +12 -5
  304. package/src/protocols/codecs/chat-ir.ts +467 -0
  305. package/src/protocols/codecs/chat.ts +10 -1
  306. package/src/protocols/codecs/messages-ir.ts +500 -0
  307. package/src/protocols/encoders/adapter-events.ts +5 -4
  308. package/src/protocols/encoders/messages.ts +9 -6
  309. package/src/protocols/features.ts +10 -2
  310. package/src/protocols/path.ts +11 -1
  311. package/src/protocols/plan-snapshot.ts +39 -16
  312. package/src/protocols/plan.ts +9 -1
  313. package/src/protocols/provider-summary.ts +2 -1
  314. package/src/protocols/settings.ts +8 -0
  315. package/src/providers/antigravity-account-roster.ts +102 -0
  316. package/src/providers/antigravity-models.ts +85 -7
  317. package/src/providers/api-key-cooldowns.ts +49 -0
  318. package/src/providers/api-key-resolve.ts +11 -6
  319. package/src/providers/api-key-response-ownership.ts +57 -0
  320. package/src/providers/api-key-selection.ts +31 -6
  321. package/src/providers/api-keys.ts +214 -3
  322. package/src/providers/claude-cli-usage.ts +96 -0
  323. package/src/providers/dsh-account-destination.ts +31 -0
  324. package/src/providers/dsh-account-models.ts +41 -0
  325. package/src/providers/gcp-credential-cleanup.ts +59 -0
  326. package/src/providers/key-failover.ts +54 -50
  327. package/src/providers/key-store.ts +7 -2
  328. package/src/providers/model-rename-fields.ts +5 -0
  329. package/src/providers/model-rename-migration.ts +7 -5
  330. package/src/providers/openai-tiers-destination.ts +11 -2
  331. package/src/providers/openai-tiers.ts +5 -12
  332. package/src/providers/opencode-go-transport.ts +3 -1
  333. package/src/providers/quota/account-cache.ts +6 -5
  334. package/src/providers/quota/account-row.ts +34 -0
  335. package/src/providers/quota/antigravity.ts +28 -7
  336. package/src/providers/quota/dsh-account.ts +70 -0
  337. package/src/providers/quota/vendor-probes-oauth.ts +1 -93
  338. package/src/providers/quota-types.ts +2 -0
  339. package/src/providers/quota.ts +14 -20
  340. package/src/providers/registry/entries-core.ts +44 -14
  341. package/src/providers/registry/entries-extended.ts +19 -0
  342. package/src/providers/registry/types.ts +1 -1
  343. package/src/providers/request-pacing.ts +77 -19
  344. package/src/providers/routed-config.ts +339 -0
  345. package/src/providers/service-tier.ts +2 -1
  346. package/src/quota/window-mapping.ts +5 -2
  347. package/src/responses/anthropic-progress.ts +34 -0
  348. package/src/responses/assistant-replay.ts +35 -0
  349. package/src/responses/compaction-history-framing.ts +65 -0
  350. package/src/responses/compaction.ts +6 -1
  351. package/src/responses/incomplete-reason.ts +31 -0
  352. package/src/responses/parser.ts +78 -22
  353. package/src/responses/reasoning-envelope.ts +4 -0
  354. package/src/responses/reasoning-replay-cache.ts +24 -2
  355. package/src/responses/reasoning-retention.ts +173 -0
  356. package/src/responses/retained-compaction.ts +46 -0
  357. package/src/responses/schema.ts +23 -0
  358. package/src/responses/state.ts +32 -29
  359. package/src/router.ts +8 -321
  360. package/src/routing/health.ts +37 -22
  361. package/src/routing/history/build-file.ts +92 -0
  362. package/src/routing/history/indexer.ts +254 -221
  363. package/src/routing/history/ingest.ts +242 -0
  364. package/src/routing/history/rebuild-job.ts +111 -0
  365. package/src/routing/history/rebuild-protocol.ts +15 -0
  366. package/src/routing/history/rebuild-worker.ts +34 -0
  367. package/src/routing/profile.ts +1 -1
  368. package/src/server/admission-model-scope.ts +21 -1
  369. package/src/server/antigravity-direct-eligibility.ts +208 -0
  370. package/src/server/auth-cors.ts +15 -0
  371. package/src/server/chat-completions.ts +82 -10
  372. package/src/server/chat-native-reasoning.ts +123 -0
  373. package/src/server/chat-native-sse.ts +110 -7
  374. package/src/server/chat-native.ts +76 -17
  375. package/src/server/claude-messages.ts +164 -433
  376. package/src/server/effort-policy.ts +1 -1
  377. package/src/server/images.ts +15 -2
  378. package/src/server/index/desktop-compatibility-startup.ts +56 -0
  379. package/src/server/index/model-alias-id.ts +36 -0
  380. package/src/server/index/optional-listeners.ts +2 -0
  381. package/src/server/index/serve-options.ts +11 -3
  382. package/src/server/index/startup-warnings.ts +11 -0
  383. package/src/server/index/websocket-handler.ts +9 -0
  384. package/src/server/index.ts +14 -11
  385. package/src/server/inference/advisor-diagnostics.ts +31 -0
  386. package/src/server/inference/client-encoder-delivery.ts +6 -5
  387. package/src/server/login-shell-path.ts +146 -0
  388. package/src/server/management/advisor-routes.ts +202 -0
  389. package/src/server/management/agent-settings-routes.ts +22 -4
  390. package/src/server/management/claude-desktop-picker-routes.ts +1 -1
  391. package/src/server/management/combo-routes.ts +176 -50
  392. package/src/server/management/companion-routes.ts +4 -0
  393. package/src/server/management/config-routes.ts +77 -6
  394. package/src/server/management/context.ts +4 -0
  395. package/src/server/management/cursor-integration-routes.ts +1 -1
  396. package/src/server/management/desktop-compatibility-routes.ts +46 -0
  397. package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
  398. package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
  399. package/src/server/management/model-routes.ts +13 -8
  400. package/src/server/management/model-rows.ts +14 -0
  401. package/src/server/management/native-integration-routes.ts +2 -7
  402. package/src/server/management/native-reasoning-retention-routes.ts +41 -0
  403. package/src/server/management/oauth-account-routes.ts +87 -14
  404. package/src/server/management/protocol-settings-patch.ts +1 -0
  405. package/src/server/management/provider-gcp-credential-routes.ts +29 -0
  406. package/src/server/management/provider-key-pacing-routes.ts +31 -0
  407. package/src/server/management/provider-overwrite-carry.ts +7 -0
  408. package/src/server/management/provider-routes.ts +25 -30
  409. package/src/server/management/route-registry.ts +21 -1
  410. package/src/server/management/sibling-guard.ts +1 -0
  411. package/src/server/management-api.ts +9 -1
  412. package/src/server/messages-antigravity-direct.ts +195 -0
  413. package/src/server/messages-native-tool-guard.ts +185 -0
  414. package/src/server/messages-native.ts +106 -22
  415. package/src/server/messages-shared.ts +431 -0
  416. package/src/server/relay.ts +1 -1
  417. package/src/server/request-log-terminal-status.ts +1 -1
  418. package/src/server/request-log.ts +13 -1
  419. package/src/server/responses/account-change-state.ts +8 -2
  420. package/src/server/responses/adapter-continuation.ts +48 -19
  421. package/src/server/responses/adapter-delivery.ts +43 -8
  422. package/src/server/responses/adapter-dispatch.ts +48 -4
  423. package/src/server/responses/advisor-plan-slot.ts +27 -0
  424. package/src/server/responses/advisor-retention.ts +117 -0
  425. package/src/server/responses/advisor-slot.ts +368 -0
  426. package/src/server/responses/code-mode-collab-surface.ts +116 -8
  427. package/src/server/responses/codex-ws-wire.ts +15 -8
  428. package/src/server/responses/compact.ts +303 -125
  429. package/src/server/responses/compaction-recovery.ts +18 -10
  430. package/src/server/responses/compaction-retention.ts +61 -0
  431. package/src/server/responses/context-overflow.ts +11 -6
  432. package/src/server/responses/core-auth.ts +6 -0
  433. package/src/server/responses/core-codex-account.ts +11 -1
  434. package/src/server/responses/core-combo-failure.ts +12 -1
  435. package/src/server/responses/core-combo.ts +141 -36
  436. package/src/server/responses/core-errors.ts +3 -3
  437. package/src/server/responses/core-lifetime.ts +7 -2
  438. package/src/server/responses/core-normalize.ts +11 -0
  439. package/src/server/responses/core-opaque-recovery.ts +35 -0
  440. package/src/server/responses/core-options.ts +51 -0
  441. package/src/server/responses/core-pipeline.ts +141 -0
  442. package/src/server/responses/core-replay.ts +25 -6
  443. package/src/server/responses/core.ts +3 -116
  444. package/src/server/responses/fetch-helpers.ts +73 -19
  445. package/src/server/responses/main-hard-lock-routing.ts +109 -0
  446. package/src/server/responses/native-response-control.ts +4 -2
  447. package/src/server/responses/passthrough-delivery.ts +19 -11
  448. package/src/server/responses/passthrough-dispatch.ts +29 -16
  449. package/src/server/responses/request-prepare.ts +144 -17
  450. package/src/server/responses/request-sidecar-auth.ts +2 -2
  451. package/src/server/responses/request-transport.ts +120 -27
  452. package/src/server/responses/response-effects.ts +1 -0
  453. package/src/server/responses/run-turn-execution.ts +166 -38
  454. package/src/server/responses/sidecar-execution.ts +98 -5
  455. package/src/server/responses/terminal-guard.ts +4 -17
  456. package/src/server/responses-undeclared-tool-guard.ts +30 -9
  457. package/src/server/startup-action-control.ts +16 -2
  458. package/src/server/startup-health-cache.ts +1 -0
  459. package/src/service/auth-preflight.ts +71 -0
  460. package/src/service/cli.ts +4 -2
  461. package/src/service/definition.ts +97 -0
  462. package/src/service/desktop-startup.ts +14 -0
  463. package/src/service/guards.ts +1 -61
  464. package/src/service/health-sentinel-linux.ts +112 -0
  465. package/src/service/health-sentinel-main.ts +99 -61
  466. package/src/service/health-sentinel-probe.ts +24 -7
  467. package/src/service/health.ts +5 -89
  468. package/src/service/launchd.ts +27 -7
  469. package/src/service/logging.ts +135 -0
  470. package/src/service/repair.ts +2 -1
  471. package/src/service/state.ts +7 -8
  472. package/src/service/systemd.ts +1 -1
  473. package/src/service/windows-guarded-manager.ts +2 -1
  474. package/src/service/windows-ops.ts +1 -1
  475. package/src/service/windows-scheduler.ts +2 -7
  476. package/src/service/windows-taskxml.ts +2 -2
  477. package/src/service/windows-wscript.ts +14 -0
  478. package/src/service.ts +4 -2
  479. package/src/sidecar/auth.ts +2 -0
  480. package/src/tray/windows-tray.ps1 +43 -24
  481. package/src/types/config.ts +124 -6
  482. package/src/types/opencode-go.ts +61 -0
  483. package/src/types/opencode-zen.ts +29 -0
  484. package/src/types/provider.ts +39 -6
  485. package/src/types/request.ts +38 -2
  486. package/src/types/tools.ts +2 -0
  487. package/src/types/wire.ts +29 -37
  488. package/src/types.ts +1 -0
  489. package/src/update/async-check.ts +1 -1
  490. package/src/update/badge.ts +2 -2
  491. package/src/update/index.ts +88 -15
  492. package/src/update/job-supervised-restart.ts +107 -0
  493. package/src/update/job.ts +32 -32
  494. package/src/update/notify.ts +1 -1
  495. package/src/update/npm-invocation.mjs +12 -2
  496. package/src/update/refresh-scheduler.ts +2 -2
  497. package/src/update/supervised-recovery.d.mts +27 -0
  498. package/src/update/supervised-recovery.mjs +157 -0
  499. package/src/usage/cache-diagnostic.ts +2 -0
  500. package/src/usage/cost.ts +48 -12
  501. package/src/usage/expected-prices.ts +15 -0
  502. package/src/usage/ledger-retention.ts +2 -1
  503. package/src/usage/ledger-scanner.ts +6 -5
  504. package/src/usage/log.ts +17 -8
  505. package/src/usage/summary.ts +22 -9
  506. package/src/usage/user-cost-overlays.ts +14 -19
  507. package/src/usage/user-cost-validation.ts +38 -0
  508. package/src/vision/eligibility.ts +13 -0
  509. package/src/web-search/gemini-executor.ts +2 -2
  510. package/src/web-search/loop.ts +55 -5
  511. package/src/web-search/progress-stream.ts +4 -1
  512. package/src/web-search/run-turn-loop.ts +9 -1
  513. package/assets/sponsors/packycode.png +0 -0
  514. package/gui/dist/assets/App-CSJvmjNs.css +0 -1
  515. package/gui/dist/assets/App-CZtqiGcw.js +0 -56
  516. package/gui/dist/assets/index-BB6uK6tU.css +0 -1
  517. package/gui/dist/assets/index-CTm4l-HQ.js +0 -94
  518. package/gui/dist/assets/quota-summary-BbEeRfPY.js +0 -1
@@ -0,0 +1,79 @@
1
+ /**
2
+ * When another stored pool account can serve again, for a main-account hard-lock refusal (#7046).
3
+ *
4
+ * A pool request that fell through to the main login and met the main-account quota guard used
5
+ * to be told to come back at main's own reset, which can be days away while a sibling pool
6
+ * account recovers within hours. This module answers one question: the earliest KNOWN instant at
7
+ * which a stored pool account's every temporary hold ends. It changes no routing state, selects
8
+ * nothing, and never lifts the main guard. The client retries at that time, and ordinary selection
9
+ * decides again from fresh state; a reset timestamp is a hint, not proof of fresh quota.
10
+ *
11
+ * Conservative by construction (the policy Jun is asked to confirm in #7046):
12
+ * - An account is a candidate only when it is a selectable stored pool account that is not
13
+ * paused, plan-excluded, quarantined for sign-in, otherwise unusable, or denied for the model.
14
+ * Those are operator or credential states no clock lifts.
15
+ * - Its recovery time is the LATEST of every routing hold with a known end: quota-health cooldown,
16
+ * quota avoidance, transient soft avoidance, pool-refresh backoff, and the credits-off
17
+ * usage-limit reset. An account under a hold whose end is unknown, or under the sliding
18
+ * failure-window guard (whose end moves with every failure), is skipped rather than guessed.
19
+ * - For an account-gated model, only accounts the resolved entitlement roster admits count, and
20
+ * with no resolved roster nothing does.
21
+ * - An account with no recorded hold is skipped: nothing says when it became unavailable, so
22
+ * nothing says when it will be available again.
23
+ * - Main itself is never a candidate; its own reset stays on the refusal.
24
+ */
25
+ import { isCodexAccountPaused } from "../account-pause";
26
+ import { codexUsageLimitResetAt } from "../account-credit-use";
27
+ import { isSelectableCodexPoolAccount } from "../account-id";
28
+ import { isAccountNeedsReauth } from "../account-runtime-state";
29
+ import { isCodexAccountUsable } from "../account-usability";
30
+ import { getCodexPoolRefreshCooldownUntil } from "../pool-refresh-backoff";
31
+ import { getAccountQuota } from "../quota";
32
+ import type { OcxConfig } from "../../types";
33
+ import { failureWindowSteersNewThreads } from "./failure-window";
34
+ import {
35
+ codexQuotaAvoidUntil, getCodexAccountSoftAvoidUntil, getCodexQuotaHealthSnapshot, type CodexQuotaScope,
36
+ } from "./health-store";
37
+ import { getPoolAccountPlanForSelection, isCodexAccountHeldAtUsageLimit, isCodexAccountPlanExcluded } from "./selection";
38
+
39
+ export function nextCodexPoolRecoveryAt(
40
+ config: OcxConfig,
41
+ now: number,
42
+ quotaScope?: CodexQuotaScope,
43
+ options: {
44
+ deniedAccountIds?: ReadonlySet<string>;
45
+ /** When set, only these accounts may serve the model (account-gated models). */
46
+ eligibleAccountIds?: ReadonlySet<string>;
47
+ } = {},
48
+ ): number | undefined {
49
+ let earliest: number | undefined;
50
+ for (const account of config.codexAccounts ?? []) {
51
+ if (!isSelectableCodexPoolAccount(account)) continue;
52
+ const id = account.id;
53
+ if (options.deniedAccountIds?.has(id) || (options.eligibleAccountIds && !options.eligibleAccountIds.has(id))
54
+ || isCodexAccountPaused(config, id)
55
+ || isCodexAccountPlanExcluded(config, id) || isAccountNeedsReauth(id)
56
+ || !isCodexAccountUsable(config, id) || failureWindowSteersNewThreads(config, id, now)) continue;
57
+ const holds: number[] = [];
58
+ const health = getCodexQuotaHealthSnapshot(id, quotaScope, now);
59
+ if (health) {
60
+ if (typeof health.cooldownUntil !== "number" || !Number.isFinite(health.cooldownUntil)) continue;
61
+ holds.push(health.cooldownUntil);
62
+ }
63
+ const avoidUntil = codexQuotaAvoidUntil(id, quotaScope, now);
64
+ if (avoidUntil !== null) holds.push(avoidUntil);
65
+ const softAvoidUntil = getCodexAccountSoftAvoidUntil(id, now);
66
+ if (softAvoidUntil !== null) holds.push(softAvoidUntil);
67
+ const refreshCooldownUntil = getCodexPoolRefreshCooldownUntil(id, now);
68
+ if (refreshCooldownUntil !== null) holds.push(refreshCooldownUntil);
69
+ if (isCodexAccountHeldAtUsageLimit(config, id, now)) {
70
+ const resetAt = codexUsageLimitResetAt(getAccountQuota(id), getPoolAccountPlanForSelection(config, id), now);
71
+ if (resetAt === undefined) continue;
72
+ holds.push(resetAt);
73
+ }
74
+ if (holds.length === 0) continue;
75
+ const recoveryAt = Math.max(...holds);
76
+ if (recoveryAt > now && (earliest === undefined || recoveryAt < earliest)) earliest = recoveryAt;
77
+ }
78
+ return earliest;
79
+ }
@@ -1,7 +1,8 @@
1
1
  import { getEffectiveCodexAutoSwitchThreshold } from "../account-auto-switch";
2
2
  import { codexAccountUsesCreditsAfterLimit, isCodexAccountHeldForCredits } from "../account-credit-use";
3
3
  import { isCodexAccountPaused } from "../account-pause";
4
- import { codexAccountPriorityLookup, pinnedCodexAccountId } from "../account-priority";
4
+ import { pinnedCodexAccountId } from "../account-priority";
5
+ import { codexSelectionPriorityLookup } from "../account-preservation";
5
6
  import { isSelectableCodexPoolAccount } from "../account-id";
6
7
  import { isAccountNeedsReauth } from "../account-runtime-state";
7
8
  import { isCodexAccountUsable, type CodexAccountUsabilityOptions } from "../account-usability";
@@ -248,7 +249,7 @@ export function getEligiblePoolAccounts(
248
249
  const pinned = pinnedCodexAccountId(config);
249
250
  return selectPriorityTier(
250
251
  withoutModelDeniedAccounts(ids, selectionOptions?.deniedModelAccountIds, pinned),
251
- codexAccountPriorityLookup(config),
252
+ codexSelectionPriorityLookup(config),
252
253
  id => hasCodexQuotaHeadroom(config, id, selectionOptions, now),
253
254
  pinned,
254
255
  );
@@ -767,7 +768,7 @@ export function pickPriorityPreemption(
767
768
  && eligible.includes(pinned)
768
769
  && hasCodexQuotaHeadroom(config, pinned, selectionOptions, now)
769
770
  ) return null;
770
- const priorityOf = codexAccountPriorityLookup(config);
771
+ const priorityOf = codexSelectionPriorityLookup(config);
771
772
  if (priorityOf(eligible[0]!) <= priorityOf(active)) return null;
772
773
  // Members without headroom are in the tier only because a sibling has some;
773
774
  // picking one would hand the request straight back to a drained account.
@@ -1,5 +1,5 @@
1
1
  import { isCodexAccountGenerationLive, readCodexAccountRecord } from "../account-store";
2
- import { MAIN_CODEX_ACCOUNT_ID } from "../main-account";
2
+ import { MAIN_CODEX_ACCOUNT_ID } from "../account-id";
3
3
  import { retainedUtf8Bytes } from "../../lib/admission";
4
4
  import { clearAllCodexPoolRefreshFailures } from "../pool-refresh-backoff";
5
5
  import type { CodexThreadLineage } from "../lineage";
@@ -29,6 +29,12 @@ export interface CodexShimDiagnostic {
29
29
  summary: string;
30
30
  runnable?: boolean;
31
31
  active?: boolean | null;
32
+ /**
33
+ * PATH overlay only: the wrapper and its private state are intact and runnable, but this process
34
+ * could not see it first on PATH. Reinstalling cannot change that; the shell must source the
35
+ * environment file. The dashboard's PATH can also differ from the user's shell (#6971).
36
+ */
37
+ activationPending?: boolean;
32
38
  }
33
39
 
34
40
  /** Structured, secret-free shim state for CLI/GUI lifecycle diagnostics. */
@@ -44,6 +44,10 @@ export function overlayActivationHint(configDir = getConfigDir()): string {
44
44
  function sameId(left: OverlayIdentity | null | undefined, right: OverlayIdentity | null | undefined): boolean {
45
45
  return !!left && !!right && left.dev === right.dev && left.ino === right.ino;
46
46
  }
47
+ /** APFS device numbers can change after reboot; persisted ownership still requires the same inode. */
48
+ function samePersistedInode(current: OverlayIdentity, recorded: OverlayIdentity | undefined): boolean {
49
+ return !!recorded && current.ino === recorded.ino;
50
+ }
47
51
  function lexical(path: string): Stats | null {
48
52
  try { return lstatSync(path); }
49
53
  catch (error) { if (fileErrorCode(error) === "ENOENT") return null; throw error; }
@@ -128,7 +132,7 @@ function shellEnvironment(): string {
128
132
  'unset ocx_shim_dir ocx_shim_rest ocx_shim_path ocx_shim_more ocx_shim_part', ""].join("\n");
129
133
  }
130
134
  function ownedWrapper(value: Snapshot, state: OverlayState): boolean {
131
- return sameId(value.identity, state.wrapperIdentity)
135
+ return samePersistedInode(value.identity, state.wrapperIdentity)
132
136
  && value.content.includes(SHIM_MARKER) && value.content.includes(`exec ${shQuote(state.launcherPath)} "$@"`);
133
137
  }
134
138
  function requireOverlayIdentities(state: OverlayState): void {
@@ -137,7 +141,7 @@ function requireOverlayIdentities(state: OverlayState): void {
137
141
  }
138
142
  }
139
143
  function ownedEnvironment(value: Snapshot, state: OverlayState): boolean {
140
- return sameId(value.identity, state.envIdentity) && value.content === shellEnvironment();
144
+ return samePersistedInode(value.identity, state.envIdentity) && value.content === shellEnvironment();
141
145
  }
142
146
  function revalidateOverlay(state: OverlayState, saved: Snapshot | null, wrapper: Snapshot | null, env: Snapshot | null): void {
143
147
  if (!unchanged(statePath(), saved) || !unchanged(state.wrapperPath, wrapper) || !unchanged(overlayPaths().env, env)) {
@@ -201,6 +205,7 @@ export function overlayDiagnostic(state: ShimState): CodexShimDiagnostic {
201
205
  inspection?.revalidate();
202
206
  } catch (error) { active = null; runnable = false; integrityMessage = failure(error).message; }
203
207
  return { installed: true, healthy: complete && runnable && active === true, runnable, active,
208
+ activationPending: complete && runnable && active !== true,
204
209
  summary: `Codex PATH shim: ${complete && runnable ? "ready" : "unhealthy"} at ${state.wrapperPath}; launcher ${state.launcherPath}; PATH ${active === null ? "unverified" : active ? "active" : "inactive"}. ${complete && runnable ? overlayActivationHint() : integrityMessage || "Run ocx codex-shim install to repair the shim."}` };
205
210
  }
206
211
 
@@ -0,0 +1,43 @@
1
+ import { extractEmail, inspectChatGptDomainClaim } from "../oauth/chatgpt";
2
+ import type { CodexTokens } from "./auth-collision";
3
+
4
+ export type CodexLoginIdentity = Readonly<{ accountId: string; email: string }>;
5
+ let observed: CodexLoginIdentity | undefined;
6
+
7
+ export function codexLoginIdentity(accountId: unknown, email: unknown): CodexLoginIdentity | undefined {
8
+ const member = typeof email === "string" ? email.trim().toLowerCase() : "";
9
+ const workspace = typeof accountId === "string" ? accountId.trim() : "";
10
+ return workspace && member ? { accountId: workspace, email: member } : undefined;
11
+ }
12
+
13
+ /** Undefined is insufficient evidence; null means conflicting or malformed token evidence. */
14
+ export function codexTokenIdentity(tokens: CodexTokens): CodexLoginIdentity | undefined | null {
15
+ if (typeof tokens.access_token !== "string" || typeof tokens.account_id !== "string"
16
+ || (tokens.id_token !== undefined && typeof tokens.id_token !== "string")) return null;
17
+ const claims = [tokens.id_token, tokens.access_token]
18
+ .filter((token): token is string => typeof token === "string")
19
+ .map(inspectChatGptDomainClaim);
20
+ if (claims.some(claim => claim.kind === "invalid")) return null;
21
+ const claimIds = claims.flatMap(claim => claim.kind === "valid" ? [claim.accountId] : []);
22
+ const accountId = tokens.account_id.trim() || claimIds[0];
23
+ if (claimIds.some(claim => claim !== accountId)) return null;
24
+ const emails = new Set([extractEmail(tokens.id_token), extractEmail(undefined, tokens.access_token)]
25
+ .flatMap(email => email?.trim() ? [email.trim().toLowerCase()] : []));
26
+ if (emails.size > 1) return null;
27
+ return codexLoginIdentity(accountId, [...emails][0]);
28
+ }
29
+
30
+ /** Publish only from an existing physical-login observation, never an incoming bearer. */
31
+ export function observeSignedInCodexIdentity(tokens?: CodexTokens): void {
32
+ if (!tokens) { observed = undefined; return; }
33
+ const next = codexTokenIdentity(tokens);
34
+ // Conflicting claims can be a partial external write, never proof of a new login.
35
+ if (next !== null) observed = next;
36
+ }
37
+
38
+ /** Routing reads memory only, including while the native profile is fenced. */
39
+ export function getSignedInCodexIdentity(): CodexLoginIdentity | undefined { return observed; }
40
+
41
+ export function sameCodexLogin(a: CodexLoginIdentity, b: CodexLoginIdentity | undefined): boolean {
42
+ return a.accountId === b?.accountId && a.email === b.email;
43
+ }
@@ -591,7 +591,7 @@ export function maybePrimeSubagentQuota(
591
591
  if (subagentQuotaPrimeForTests) {
592
592
  await subagentQuotaPrimeForTests(config, "subagent-spawn");
593
593
  } else {
594
- const { primeCodexPoolQuotas } = await import("./auth-api");
594
+ const { primeCodexPoolQuotas } = await import("./auth-api/pool-mode-gate");
595
595
  await primeCodexPoolQuotas(config, "subagent-spawn");
596
596
  }
597
597
  quotaPrimedAt.set("global", Date.now());
package/src/codex/sync.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { approvedCodexProviders } from "./provider-routing";
1
2
  import { currentExternalCodexModelProvider, injectCodexConfig } from "./inject";
2
3
  import { FOREIGN_CODEX_HOME_OWNER_MESSAGE, UNKNOWN_CODEX_HOME_OWNER_MESSAGE, unbackedRoutedRemovalMessage } from "./catalog/routed-removal";
3
4
  import { printProjectCodexConfigWarnings, groupProjectCodexConfigWarningsByPath, type ProjectCodexConfigWarning } from "./project-config-warnings";
@@ -33,6 +34,8 @@ export interface CodexSyncResult {
33
34
  catalogPath: string | null;
34
35
  catalogExists: boolean;
35
36
  catalogWritten: boolean;
37
+ /** Committed configuration-byte change, never inferred from a successful no-op. */
38
+ configChanged?: boolean;
36
39
  cacheSynced: boolean;
37
40
  /**
38
41
  * Whether the catalog owner committed a validated catalog or refused the
@@ -188,7 +191,7 @@ export async function syncModelsToCodex(
188
191
  };
189
192
  }
190
193
 
191
- if (externalProvider) {
194
+ if (externalProvider && !approvedCodexProviders().includes(externalProvider)) {
192
195
  if (catalogEvenWhenNotInjected) {
193
196
  // External providers own config.toml, and the injector removes the OpenCodex
194
197
  // journal for external providers (inject.ts). This explicit catalog-only sync
@@ -214,6 +217,7 @@ export async function syncModelsToCodex(
214
217
  return {
215
218
  status: "applied",
216
219
  ok: result.success,
220
+ ...(result.success && result.configChanged === true ? { configChanged: true } : {}),
217
221
  added: 0,
218
222
  catalogPath: null,
219
223
  catalogExists: false,
@@ -332,6 +336,7 @@ export async function syncModelsToCodex(
332
336
  return {
333
337
  status: "applied",
334
338
  ok: result.success,
339
+ ...(result.success && result.configChanged === true ? { configChanged: true } : {}),
335
340
  added,
336
341
  ...(refreshOutcome ? { refreshOutcome } : {}),
337
342
  catalogPath,
@@ -19,6 +19,7 @@ import { isAbsolute, join, resolve } from "node:path";
19
19
 
20
20
  import { resolveTrustedWindowsPowerShellExe } from "../lib/windows-elevation";
21
21
  import { WINDOWS_PRINCIPAL_LOOKUP_TIMEOUT_MS } from "../lib/windows-user-principal";
22
+ import { resolveWindowsLocalAppData } from "./windows-local-appdata";
22
23
 
23
24
  import type {
24
25
  ResolveCodexCoordinatorDatabasePath,
@@ -209,10 +210,11 @@ export function decodeWindowsIdentityPowerShellOutputForTests(output: Uint8Array
209
210
  /**
210
211
  * Per-process memo for the Windows lookups.
211
212
  *
212
- * Both values -- the effective token's SID and its known-folder local AppData --
213
+ * Both helper values -- the effective token's SID and its known-folder local AppData --
213
214
  * are fixed for the lifetime of a process: neither can change without a new logon
214
215
  * token, and the lookups deliberately ignore the environment, so nothing a caller
215
- * does between two calls can alter the answer. Each call otherwise spawns a fresh
216
+ * does between two calls can alter the answer. The native x64 known-folder path
217
+ * avoids this helper entirely. Each fallback call otherwise spawns a fresh
216
218
  * PowerShell, roughly 150ms for the SID and 310ms for the folder, and the
217
219
  * coordinator asks for both on every config write and lock acquisition. That cost
218
220
  * pushed real multi-process injection tests past their budget while proving
@@ -265,6 +267,15 @@ function resolveWindowsSid(): string {
265
267
  return sid.toUpperCase();
266
268
  }
267
269
 
270
+ function windowsLocalAppData(): string {
271
+ try {
272
+ return resolveWindowsLocalAppData(() => powershellValue(windowsLocalAppDataExpression()));
273
+ } catch (cause) {
274
+ if (cause instanceof CodexUserIdentityRefusal) throw cause;
275
+ refuse("Windows LocalAppData resolution failed.", cause);
276
+ }
277
+ }
278
+
268
279
  export const resolveEffectiveUserIdentity: ResolveEffectiveUserIdentity = () => {
269
280
  if (process.platform === "win32") {
270
281
  return { platform: "win32", sid: resolveWindowsSid() };
@@ -374,7 +385,7 @@ export function probeCodexCoordinatorNamespace(identity: UserIdentity): Coordina
374
385
  }
375
386
 
376
387
  if (!SID_PATTERN.test(identity.sid)) refuse("The coordinator identity contains an invalid SID.");
377
- const localAppData = powershellValue(windowsLocalAppDataExpression());
388
+ const localAppData = windowsLocalAppData();
378
389
  if (!isAbsolute(localAppData)) refuse("Windows LocalAppData resolution returned a relative path.");
379
390
  const root = resolve(localAppData, "OpenCodex", "Runtime", "v1", identity.sid.toUpperCase());
380
391
  let entry;
@@ -404,10 +415,10 @@ export function probeCodexCoordinatorNamespace(identity: UserIdentity): Coordina
404
415
 
405
416
  function resolveWindowsRuntimeRoot(identity: Extract<UserIdentity, { platform: "win32" }>): string {
406
417
  if (!SID_PATTERN.test(identity.sid)) refuse("The coordinator identity contains an invalid SID.");
407
- const localAppData = powershellValue(windowsLocalAppDataExpression());
418
+ const localAppData = windowsLocalAppData();
408
419
  if (!isAbsolute(localAppData)) refuse("Windows LocalAppData resolution returned a relative path.");
409
420
 
410
- // The SID and known-folder values come from the effective token/.NET OS APIs,
421
+ // The SID and known-folder values come from effective-token OS APIs,
411
422
  // never USERPROFILE or LOCALAPPDATA. WP11 adds descriptor/reparse/ACL checks at
412
423
  // the stable-database open boundary where those checks can cover SQLite too.
413
424
  const root = resolve(localAppData, "OpenCodex", "Runtime", "v1", identity.sid.toUpperCase());
@@ -0,0 +1,84 @@
1
+ import { dlopen, ptr, read, type Pointer } from "bun:ffi";
2
+ import { win32 } from "node:path";
3
+ import { resolveTrustedWindowsSystemDirectory } from "../lib/windows-elevation";
4
+
5
+ // FOLDERID_LocalAppData: GUID fields Data1/Data2/Data3 are little-endian.
6
+ const LOCAL_APPDATA_ID = Buffer.from("8527b3f1ba6fcf4f9d557b8e7f157091", "hex");
7
+ const KF_FLAG_DEFAULT_PATH = 0x400;
8
+ const MAX_PATH_CODE_UNITS = 32_768;
9
+
10
+ export interface WindowsKnownFolderApi {
11
+ getPath(id: Uint8Array, flags: number, token: null, output: BigUint64Array): number;
12
+ readCodeUnit(address: number, byteOffset: number): number;
13
+ free(address: number): void;
14
+ }
15
+
16
+ /** Same current-token/default-path API as the PowerShell fallback, without Add-Type. */
17
+ export function readWindowsLocalAppData(api: WindowsKnownFolderApi): string {
18
+ const output = new BigUint64Array(1);
19
+ let address = 0;
20
+ try {
21
+ const hr = api.getPath(LOCAL_APPDATA_ID, KF_FLAG_DEFAULT_PATH, null, output);
22
+ address = Number(output[0]);
23
+ if (hr !== 0 || !Number.isSafeInteger(address) || address <= 0) {
24
+ throw new Error("Windows LocalAppData known-folder lookup failed.");
25
+ }
26
+ const units: number[] = [];
27
+ for (let index = 0; index < MAX_PATH_CODE_UNITS; index += 1) {
28
+ const unit = api.readCodeUnit(address, index * 2);
29
+ if (unit === 0) {
30
+ const path = String.fromCharCode(...units);
31
+ if (!win32.isAbsolute(path) || !/^[a-z]:\\|^\\\\/i.test(path)) {
32
+ throw new Error("Windows LocalAppData known-folder lookup returned an invalid path.");
33
+ }
34
+ return path;
35
+ }
36
+ units.push(unit);
37
+ }
38
+ throw new Error("Windows LocalAppData known-folder lookup returned an unterminated path.");
39
+ } finally {
40
+ // SHGetKnownFolderPath requires CoTaskMemFree even on a failed HRESULT.
41
+ if (address > 0 && Number.isSafeInteger(address)) api.free(address);
42
+ }
43
+ }
44
+
45
+ let nativeLookup: (() => string) | null | undefined;
46
+
47
+ function loadNativeLookup(): (() => string) | null {
48
+ if (nativeLookup !== undefined) return nativeLookup;
49
+ if (process.platform !== "win32" || process.arch !== "x64") return null;
50
+ try {
51
+ const systemDirectory = resolveTrustedWindowsSystemDirectory();
52
+ const shell = dlopen(win32.join(systemDirectory, "shell32.dll"), {
53
+ SHGetKnownFolderPath: { args: ["ptr", "u32", "ptr", "ptr"], returns: "i32" },
54
+ });
55
+ let ole;
56
+ try {
57
+ ole = dlopen(win32.join(systemDirectory, "ole32.dll"), {
58
+ CoTaskMemFree: { args: ["ptr"], returns: "void" },
59
+ });
60
+ } catch (error) {
61
+ shell.close();
62
+ throw error;
63
+ }
64
+ const api: WindowsKnownFolderApi = {
65
+ getPath: (id, flags, token, output) => shell.symbols.SHGetKnownFolderPath(
66
+ ptr(id), flags, token, ptr(output),
67
+ ),
68
+ readCodeUnit: (address, offset) => read.u16(address as Pointer, offset),
69
+ free: address => { ole.symbols.CoTaskMemFree(address as Pointer); },
70
+ };
71
+ nativeLookup = () => readWindowsLocalAppData(api);
72
+ } catch {
73
+ nativeLookup = null;
74
+ }
75
+ return nativeLookup;
76
+ }
77
+
78
+ /** Unsupported FFI keeps the bounded trusted helper; an OS API failure still refuses. */
79
+ export function resolveWindowsLocalAppData(
80
+ powershellLookup: () => string,
81
+ lookup: (() => string) | null = loadNativeLookup(),
82
+ ): string {
83
+ return lookup ? lookup() : powershellLookup();
84
+ }
@@ -403,6 +403,9 @@ const REQUEST_SHAPE_FAILURE_CODES = new Set([
403
403
  "tool_catalog_too_large",
404
404
  "cursor_root_envelope_limit",
405
405
  "target_incompatible",
406
+ // A route that cannot read the request's media (image input to a text-only CLI route) is
407
+ // healthy for every other request, so cooling it would only block unrelated turns.
408
+ "unsupported_input_modality",
406
409
  ]);
407
410
 
408
411
  /** Credential/billing failures that every target sharing the provider inherits. */
@@ -836,7 +839,9 @@ export function comboFailureDecision(
836
839
  // says nothing about the next combo target. Structured code only, plus the explicit prose
837
840
  // form upstreams emit when they carry no code, so an unrelated 400 stays terminal.
838
841
  const failureCode = normalizedFailureCode(options?.code || error.code);
839
- if (["model_not_found", "model_unavailable", "unsupported_model"].includes(failureCode)) {
842
+ // The input-modality refusal is the same kind of verdict: this route cannot read the turn's
843
+ // media, and a later target may. Its 400 would otherwise end the chain as invalid_request_error.
844
+ if (["model_not_found", "model_unavailable", "unsupported_model", "unsupported_input_modality"].includes(failureCode)) {
840
845
  return "hop";
841
846
  }
842
847
  if (modelRefusal === "refusal") return "hop";
@@ -22,6 +22,7 @@ export {
22
22
  export {
23
23
  advanceComboAfterFailure,
24
24
  clearComboSelectionState,
25
+ comboExclusionReasons,
25
26
  NoAvailableComboTargetsError,
26
27
  noteComboFailure,
27
28
  noteComboSuccess,
@@ -55,17 +55,49 @@ export class UnknownComboError extends Error {
55
55
  export class NoAvailableComboTargetsError extends Error {
56
56
  readonly code = "combo_unavailable";
57
57
 
58
- constructor(readonly comboId: string) {
59
- super(`No available targets for combo: ${comboId}`);
58
+ constructor(readonly comboId: string, readonly reasons = "") {
59
+ super(`No available targets for combo: ${comboId}${reasons ? ` (${reasons})` : ""}`);
60
60
  this.name = "NoAvailableComboTargetsError";
61
61
  }
62
62
  }
63
63
 
64
+ function providerExclusionReason(config: OcxConfig, target: OcxComboTarget, now: number): string | undefined {
65
+ const provider = Object.hasOwn(config.providers, target.provider) ? config.providers[target.provider] : undefined;
66
+ if (!provider) return "provider missing";
67
+ if (provider.disabled === true) return "provider disabled";
68
+ return cachedProviderQuotaIsExhausted(getCachedProviderRoutingQuota(target.provider, provider, now), now, target.model)
69
+ ? "quota exhausted"
70
+ : undefined;
71
+ }
72
+
64
73
  function targetProviderIsUsable(config: OcxConfig, target: OcxComboTarget, now: number): boolean {
65
- if (!Object.hasOwn(config.providers, target.provider)) return false;
66
- const provider = config.providers[target.provider];
67
- if (!provider || provider.disabled === true) return false;
68
- return !cachedProviderQuotaIsExhausted(getCachedProviderRoutingQuota(target.provider, provider, now), now, target.model);
74
+ return providerExclusionReason(config, target, now) === undefined;
75
+ }
76
+
77
+ /**
78
+ * Why each target of a combo cannot be picked right now, for a zero-attempt refusal. Only the
79
+ * configured provider/model and a coarse label are named, never account or request data.
80
+ */
81
+ export function comboExclusionReasons(
82
+ config: OcxConfig,
83
+ comboId: string,
84
+ eligible?: (target: NormalizedComboTarget) => boolean,
85
+ now = Date.now(),
86
+ ): string {
87
+ return (getCombo(config, comboId)?.targets ?? []).flatMap(target => {
88
+ let reason = providerExclusionReason(config, target, now);
89
+ const cooldown = reason ? undefined : earliestComboCooldown(comboId, [target], now);
90
+ if (cooldown) reason = `cooldown ${Math.max(1, Math.ceil((cooldown.expiry - now) / 1000))}s`;
91
+ if (!reason && eligible) {
92
+ try {
93
+ if (!eligible(target)) reason = "ineligible for this request";
94
+ } catch {
95
+ // An eligibility check can exhaust the translator budget; the target still was not picked.
96
+ reason = "ineligible for this request";
97
+ }
98
+ }
99
+ return reason ? [`${targetKey(target)}: ${reason}`] : [];
100
+ }).join("; ");
69
101
  }
70
102
 
71
103
  /** Capture usable providers' quota cooldowns; request eligibility is deferred to exhaustion. */
@@ -100,6 +132,9 @@ function quotaWindowExhausted(percent: number | undefined, resetAt: number | und
100
132
  const MODEL_FAMILY_WINDOW_LABELS = new Set(["fable", "opus", "sonnet"]);
101
133
 
102
134
  function customWindowAppliesToModel(window: ProviderQuotaWindow, model: string | undefined): boolean {
135
+ // Exact Antigravity model readings are diagnostic only. They are not permission or
136
+ // provider-wide exhaustion evidence until a separate eligibility contract consumes them.
137
+ if (window.modelId !== undefined) return false;
103
138
  if (window.scope !== "model" || model === undefined) return true;
104
139
  const family = window.label.trim().toLowerCase();
105
140
  if (!MODEL_FAMILY_WINDOW_LABELS.has(family)) return true;
@@ -408,9 +443,10 @@ export function advanceComboAfterFailure(
408
443
  // including the pool's OWN "all accounts are cooled" 429, which refuses locally before it picks
409
444
  // one. That is what keeps this from trading a blackhole for a hammer: when nothing else is
410
445
  // holding the target back, the target cooldown still does.
411
- const accountScoped = options.cooldownScope === "target"
412
- && options.status === 429
413
- && options.failedAccount !== undefined;
446
+ // A 402 credit refusal is the provider-scoped twin: the caller names an account only after the
447
+ // pool has cooled it and still holds an eligible sibling.
448
+ const accountScoped = options.failedAccount !== undefined
449
+ && (options.status === 429 ? options.cooldownScope === "target" : options.status === 402);
414
450
  // "none" records no cooldown at all: the failure described the request, not the target, so
415
451
  // the target must stay immediately selectable for the next (differently shaped) request.
416
452
  if (options.cooldownScope !== "none" && !accountScoped) {
@@ -612,6 +648,6 @@ export function tryPickComboModel(config: OcxConfig, modelId: string, preview =
612
648
  if (!comboId) return null;
613
649
  if (!getCombo(config, comboId)) throw new UnknownComboError(comboId);
614
650
  const picked = pickComboTarget(config, comboId, { preview });
615
- if (!picked) throw new NoAvailableComboTargetsError(comboId);
651
+ if (!picked) throw new NoAvailableComboTargetsError(comboId, comboExclusionReasons(config, comboId));
616
652
  return picked;
617
653
  }
@@ -39,12 +39,16 @@ export interface NormalizedComboTarget {
39
39
  weight: number;
40
40
  /** Emergency-only target, deferred under `cooldownWaitPolicy` (#5691). */
41
41
  lastResort: boolean;
42
+ /** Pay-per-use target; requires key consent or loopback-only operator consent. */
43
+ metered: boolean;
42
44
  reasoningEfforts?: OcxComboDefaultEffort[];
43
45
  /** Optional JEV decision description. */
44
46
  modelProfile?: string;
45
47
  }
46
48
 
47
49
  export interface NormalizedComboConfig {
50
+ /** Operator consent, effective only on a loopback-only proxy. */
51
+ allowMeteredTargets?: boolean;
48
52
  strategy: OcxComboStrategy;
49
53
  stickyLimit: number;
50
54
  cooldownMs?: number;
@@ -164,6 +168,9 @@ export function comboConfigIssues(
164
168
  }
165
169
 
166
170
  const body = raw as Record<string, unknown>;
171
+ if (body.allowMeteredTargets !== undefined && typeof body.allowMeteredTargets !== "boolean") {
172
+ issues.push({ path: ["allowMeteredTargets"], message: "allowMeteredTargets must be a boolean" });
173
+ }
167
174
  if (typeof body.alias === "string") {
168
175
  const alias = body.alias.toLowerCase();
169
176
  for (const [providerName, provider] of Object.entries(providers)) {
@@ -427,6 +434,12 @@ export function comboConfigIssues(
427
434
  message: `targets[${i}].lastResort must be a boolean`,
428
435
  });
429
436
  }
437
+ if (target.metered !== undefined && typeof target.metered !== "boolean") {
438
+ issues.push({
439
+ path: ["targets", i, "metered"],
440
+ message: `targets[${i}].metered must be a boolean`,
441
+ });
442
+ }
430
443
  if (target.modelProfile !== undefined
431
444
  && (typeof target.modelProfile !== "string"
432
445
  || target.modelProfile.trim().length === 0
@@ -477,6 +490,7 @@ export function normalizeComboConfig(raw: OcxComboConfig): NormalizedComboConfig
477
490
  : null;
478
491
  return {
479
492
  strategy: raw.strategy ?? "failover",
493
+ ...(raw.allowMeteredTargets === true ? { allowMeteredTargets: true } : {}),
480
494
  stickyLimit: raw.stickyLimit ?? 1,
481
495
  cooldownMs: raw.cooldownMs,
482
496
  waitForCooldownMs: raw.waitForCooldownMs ?? COMBO_DEFAULT_WAIT_FOR_COOLDOWN_MS,
@@ -503,6 +517,7 @@ export function normalizeComboConfig(raw: OcxComboConfig): NormalizedComboConfig
503
517
  ? { modelProfile: target.modelProfile.trim() }
504
518
  : {}),
505
519
  lastResort: target.lastResort === true,
520
+ metered: target.metered === true,
506
521
  })),
507
522
  };
508
523
  }