@bitkyc08/opencodex 2.83.0 → 2.86.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 (405) hide show
  1. package/README.md +0 -4
  2. package/bin/ocx.mjs +61 -3
  3. package/gui/dist/assets/App-BjlttVLy.js +58 -0
  4. package/gui/dist/assets/Tray-DyR9yBee.js +1 -0
  5. package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +1 -1
  6. package/gui/dist/assets/index-BMssP4k3.js +94 -0
  7. package/gui/dist/assets/quota-summary-D8M1bc_R.js +1 -0
  8. package/gui/dist/index.html +2 -2
  9. package/package.json +2 -3
  10. package/src/adapters/anthropic/beta-allowlist.ts +87 -8
  11. package/src/adapters/anthropic/client-identity.ts +1 -1
  12. package/src/adapters/anthropic/passthrough.ts +33 -4
  13. package/src/adapters/azure.ts +19 -1
  14. package/src/adapters/client-fingerprint.ts +24 -0
  15. package/src/adapters/coding-agent/protocol.ts +15 -8
  16. package/src/adapters/coding-agent/turn.ts +22 -9
  17. package/src/adapters/cursor/arg-codec.ts +17 -5
  18. package/src/adapters/cursor/catalog.ts +8 -0
  19. package/src/adapters/cursor/effort-map.ts +3 -0
  20. package/src/adapters/cursor/freeform-args.ts +75 -0
  21. package/src/adapters/cursor/protobuf-events.ts +36 -23
  22. package/src/adapters/google-antigravity-wire.ts +22 -1
  23. package/src/adapters/google.ts +2 -2
  24. package/src/adapters/ollama-native.ts +29 -1
  25. package/src/adapters/openai-chat/messages.ts +8 -4
  26. package/src/adapters/openai-chat/response-events.ts +59 -1
  27. package/src/adapters/openai-chat.ts +8 -5
  28. package/src/adapters/openai-responses/empty-assistant.ts +32 -0
  29. package/src/adapters/openai-responses/passthrough.ts +3 -0
  30. package/src/adapters/openai-responses/request-strips.ts +10 -2
  31. package/src/adapters/qoder/adapter.ts +56 -15
  32. package/src/advisor/consult.ts +220 -0
  33. package/src/advisor/context.ts +226 -0
  34. package/src/advisor/disclosure.ts +20 -0
  35. package/src/advisor/runtime.ts +305 -0
  36. package/src/advisor/settings.ts +135 -0
  37. package/src/advisor/state.ts +369 -0
  38. package/src/advisor/synthetic-tool.ts +33 -0
  39. package/src/bridge/errors.ts +24 -0
  40. package/src/bridge.ts +1 -1
  41. package/src/chat/inbound.ts +30 -6
  42. package/src/chat/outbound.ts +5 -0
  43. package/src/claude/context-windows.ts +54 -1
  44. package/src/claude/desktop-3p-library.ts +43 -3
  45. package/src/claude/desktop-3p.ts +22 -8
  46. package/src/claude/desktop-picker-profile.ts +11 -4
  47. package/src/claude/desktop-profile.ts +10 -1
  48. package/src/claude/desktop-remote-store-io.ts +5 -3
  49. package/src/claude/inbound.ts +121 -50
  50. package/src/claude/intercept/connect-proxy.ts +10 -3
  51. package/src/claude/intercept/local-ca.ts +8 -1
  52. package/src/claude/intercept/picker-ca-persistence.ts +3 -1
  53. package/src/claude/intercept/picker-listener.ts +49 -1
  54. package/src/claude/intercept/settings-no-proxy.ts +146 -0
  55. package/src/claude/intercept/settings.ts +101 -9
  56. package/src/claude/model-default-settings.ts +154 -0
  57. package/src/claude/model-info.ts +7 -7
  58. package/src/claude/outbound.ts +13 -6
  59. package/src/cli/account-api.ts +65 -0
  60. package/src/cli/account-auth.ts +26 -2
  61. package/src/cli/account.ts +41 -5
  62. package/src/cli/advisor.ts +151 -0
  63. package/src/cli/auxiliary-listener-preflight.ts +37 -0
  64. package/src/cli/capabilities-access-remote.ts +8 -0
  65. package/src/cli/capabilities-agents-routing.ts +18 -0
  66. package/src/cli/capabilities-base.ts +14 -2
  67. package/src/cli/catalog-command-result.ts +2 -1
  68. package/src/cli/chatgpt-command.ts +35 -14
  69. package/src/cli/claude.ts +60 -11
  70. package/src/cli/config-command.ts +8 -2
  71. package/src/cli/debug.ts +8 -0
  72. package/src/cli/dispatch.ts +21 -4
  73. package/src/cli/doctor.ts +9 -0
  74. package/src/cli/ensure-desktop-cold-start.ts +329 -0
  75. package/src/cli/export-command.ts +15 -2
  76. package/src/cli/help.ts +2 -1
  77. package/src/cli/hub-disable.ts +99 -0
  78. package/src/cli/hub.ts +4 -2
  79. package/src/cli/index.ts +26 -8
  80. package/src/cli/integration-plan-dto.ts +2 -0
  81. package/src/cli/link-child.ts +104 -0
  82. package/src/cli/link-enrollment.ts +96 -0
  83. package/src/cli/link.ts +39 -24
  84. package/src/cli/observe-stream.ts +6 -1
  85. package/src/cli/observe.ts +10 -2
  86. package/src/cli/provider.ts +12 -0
  87. package/src/cli/registry.ts +20 -7
  88. package/src/cli/restart-scope.ts +3 -1
  89. package/src/cli/runtime-api.ts +27 -2
  90. package/src/cli/status.ts +104 -20
  91. package/src/client/connect.ts +39 -4
  92. package/src/client/hub-client.ts +14 -7
  93. package/src/client/hub-state.ts +25 -14
  94. package/src/client/link-home.ts +248 -0
  95. package/src/client/link-management-relay.ts +61 -0
  96. package/src/client/link-relay.ts +6 -1
  97. package/src/client/link-status.ts +14 -0
  98. package/src/client/machine-api.ts +14 -7
  99. package/src/client/machine-listener.ts +29 -4
  100. package/src/clients/config-export/contracts.ts +1 -0
  101. package/src/clients/config-export/model-metadata.ts +3 -2
  102. package/src/clients/config-export/qoder.ts +93 -0
  103. package/src/clients/config-export.ts +59 -16
  104. package/src/clients/omo-role-models.ts +6 -3
  105. package/src/codex/agent-role-models.ts +26 -8
  106. package/src/codex/auth-api/account-list.ts +17 -2
  107. package/src/codex/auth-api/account-selection.ts +17 -7
  108. package/src/codex/auth-api/pool-mode-gate.ts +5 -2
  109. package/src/codex/auth-api/routes.ts +5 -1
  110. package/src/codex/auth-context.ts +79 -5
  111. package/src/codex/autostart-health.ts +7 -1
  112. package/src/codex/catalog/build-entries.ts +11 -2
  113. package/src/codex/catalog/bundled.ts +5 -1
  114. package/src/codex/catalog/configured-tiers.ts +50 -0
  115. package/src/codex/catalog/effort.ts +6 -3
  116. package/src/codex/catalog/metadata.ts +4 -2
  117. package/src/codex/catalog/model-hints.ts +21 -3
  118. package/src/codex/catalog/native-models.ts +16 -2
  119. package/src/codex/catalog/native-ultrafast.ts +106 -0
  120. package/src/codex/catalog/parsing.ts +5 -0
  121. package/src/codex/catalog/provider-models.ts +4 -0
  122. package/src/codex/catalog/retained-sync.ts +2 -0
  123. package/src/codex/catalog/routed-gather.ts +22 -4
  124. package/src/codex/catalog/vendor-context.ts +82 -0
  125. package/src/codex/config-write-lock.ts +40 -2
  126. package/src/codex/convergence.ts +2 -0
  127. package/src/codex/desktop-app/types.ts +2 -0
  128. package/src/codex/desktop-app/windows.ts +29 -16
  129. package/src/codex/desktop-app-restart.ts +4 -2
  130. package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
  131. package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
  132. package/src/codex/desktop-compatibility/connection-store.ts +87 -0
  133. package/src/codex/desktop-compatibility/installed-build.ts +55 -0
  134. package/src/codex/desktop-compatibility/json-body.ts +29 -0
  135. package/src/codex/desktop-compatibility/native-identity.ts +62 -0
  136. package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
  137. package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
  138. package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
  139. package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
  140. package/src/codex/desktop-compatibility/runtime.ts +173 -0
  141. package/src/codex/desktop-compatibility/service.ts +30 -0
  142. package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
  143. package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
  144. package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
  145. package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
  146. package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
  147. package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
  148. package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
  149. package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
  150. package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
  151. package/src/codex/desktop-compatibility/windows-key-protection.ts +59 -0
  152. package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
  153. package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
  154. package/src/codex/history-provider.ts +1 -1
  155. package/src/codex/inject/config-write-section.ts +4 -1
  156. package/src/codex/inject/remove.ts +18 -6
  157. package/src/codex/journal.ts +16 -1
  158. package/src/codex/main-account.ts +99 -15
  159. package/src/codex/native-main-refresh-mode.ts +217 -0
  160. package/src/codex/native-profile-startup.ts +4 -1
  161. package/src/codex/prompt-journal.ts +20 -2
  162. package/src/codex/prompt-layers/encoding.ts +49 -0
  163. package/src/codex/prompt-layers/import-source.ts +30 -0
  164. package/src/codex/prompt-layers/toml-edit.ts +269 -37
  165. package/src/codex/prompt-layers/toml-read.ts +52 -4
  166. package/src/codex/prompt-layers.ts +383 -20
  167. package/src/codex/prompt-lock-claim.ts +13 -3
  168. package/src/codex/prompt-lock-evidence.ts +3 -3
  169. package/src/codex/prompt-lock-owner.ts +14 -6
  170. package/src/codex/prompt-text-probe.ts +34 -5
  171. package/src/codex/quota.ts +13 -6
  172. package/src/codex/role-auto-assign.ts +2 -5
  173. package/src/codex/routing/active-account.ts +21 -0
  174. package/src/codex/routing/health-store.ts +1 -1
  175. package/src/codex/routing/pool-recovery.ts +79 -0
  176. package/src/codex/routing.ts +1 -1
  177. package/src/codex/runtime.ts +196 -8
  178. package/src/codex/shim-diagnostics.ts +6 -0
  179. package/src/codex/shim-overlay.ts +15 -6
  180. package/src/codex/shim.ts +1 -1
  181. package/src/combos/failover.ts +3 -0
  182. package/src/config/api-key-reconcile.ts +34 -0
  183. package/src/config/diagnostics.ts +18 -1
  184. package/src/config/feature-flags.ts +5 -0
  185. package/src/config/live-reconcile.ts +56 -29
  186. package/src/config/live-topology.ts +29 -0
  187. package/src/config/load-degrade.ts +8 -2
  188. package/src/config/paths.ts +13 -3
  189. package/src/config/salvage.ts +3 -1
  190. package/src/config/schema/config-schema.ts +19 -0
  191. package/src/config/schema/desktop-compatibility.ts +9 -0
  192. package/src/config/schema/leaf-validators.ts +1 -0
  193. package/src/generated/compatibility-version.json +713 -285
  194. package/src/generated/model-metadata.ts +1 -1
  195. package/src/grok/grpc-web.ts +23 -1
  196. package/src/grok/reset-coupon-ledger.ts +75 -5
  197. package/src/grok/reset-coupons.ts +4 -2
  198. package/src/integrations/catalog-refresh.ts +1 -1
  199. package/src/integrations/config-io.ts +20 -2
  200. package/src/integrations/journal.ts +15 -1
  201. package/src/integrations/mutation-plan.ts +14 -7
  202. package/src/integrations/registry.ts +52 -11
  203. package/src/integrations/state.ts +19 -7
  204. package/src/integrations/store.ts +3 -0
  205. package/src/integrations/target.ts +42 -5
  206. package/src/integrations/writer.ts +24 -7
  207. package/src/lib/advisor-activation.ts +16 -0
  208. package/src/lib/app-owned-memory-stores.ts +4 -0
  209. package/src/lib/app-owned-memory.ts +11 -0
  210. package/src/lib/background-work.ts +65 -0
  211. package/src/lib/bounded-file-read.ts +58 -0
  212. package/src/lib/bun-path-runtime.d.mts +10 -1
  213. package/src/lib/bun-path-runtime.mjs +62 -3
  214. package/src/lib/desktop-proxy-route.ts +30 -0
  215. package/src/lib/desktop-upstream-tunnel.ts +107 -0
  216. package/src/lib/errors.ts +7 -0
  217. package/src/lib/json-byte-size.ts +31 -0
  218. package/src/lib/local-internal-call-capability.ts +55 -0
  219. package/src/lib/optional-shutdown-hooks.ts +35 -0
  220. package/src/lib/outbound-credential-registry.ts +210 -0
  221. package/src/lib/package-tree-integrity.ts +2 -2
  222. package/src/lib/passive-read.ts +35 -0
  223. package/src/lib/shadow-call.ts +44 -5
  224. package/src/lib/socks5-fetch.ts +48 -71
  225. package/src/lib/socks5-handshake.ts +112 -0
  226. package/src/lib/translator-budget.ts +7 -1
  227. package/src/link/codex-settings.ts +155 -0
  228. package/src/link/management-routes.ts +89 -0
  229. package/src/link/remote-probe.ts +38 -0
  230. package/src/link/ssh-argv.ts +26 -5
  231. package/src/link/ssh-runner.ts +58 -27
  232. package/src/link/status-projection.ts +22 -4
  233. package/src/link/supervisor.ts +144 -49
  234. package/src/link/tunnel-state.ts +11 -4
  235. package/src/oauth/anthropic-continuity.ts +4 -3
  236. package/src/oauth/anthropic-identity.ts +13 -2
  237. package/src/oauth/anthropic-model-quota.ts +13 -1
  238. package/src/oauth/anthropic-routing.ts +230 -23
  239. package/src/oauth/anthropic.ts +18 -4
  240. package/src/oauth/generic-account-failover.ts +29 -5
  241. package/src/oauth/google-antigravity.ts +24 -7
  242. package/src/oauth/index.ts +28 -17
  243. package/src/oauth/kiro-account-identity.ts +106 -0
  244. package/src/oauth/kiro-device-login.ts +2 -1
  245. package/src/oauth/kiro.ts +55 -6
  246. package/src/oauth/plan.ts +29 -0
  247. package/src/oauth/store.ts +40 -3
  248. package/src/oauth/types.ts +2 -0
  249. package/src/plugins/loader.ts +11 -0
  250. package/src/protocols/codecs/chat-ir.ts +467 -0
  251. package/src/protocols/codecs/chat.ts +10 -1
  252. package/src/protocols/codecs/messages-ir.ts +500 -0
  253. package/src/protocols/features.ts +10 -2
  254. package/src/protocols/path.ts +11 -1
  255. package/src/protocols/plan-snapshot.ts +39 -16
  256. package/src/protocols/plan.ts +9 -1
  257. package/src/protocols/settings.ts +8 -0
  258. package/src/providers/anthropic-billing-ownership.ts +74 -0
  259. package/src/providers/anthropic-billing-recovery.ts +31 -0
  260. package/src/providers/anthropic-billing-refusal.ts +55 -0
  261. package/src/providers/anthropic-reset-admission.ts +62 -0
  262. package/src/providers/anthropic-reset-grant-ledger.ts +28 -6
  263. package/src/providers/antigravity-account-roster.ts +102 -0
  264. package/src/providers/antigravity-models.ts +85 -7
  265. package/src/providers/api-key-resolve.ts +17 -2
  266. package/src/providers/key-failover.ts +52 -5
  267. package/src/providers/model-rename-fields.ts +2 -0
  268. package/src/providers/model-rename-migration.ts +5 -3
  269. package/src/providers/model-rename-startup.ts +5 -3
  270. package/src/providers/quota/account-cache.ts +11 -0
  271. package/src/providers/quota/anthropic-account-quota.ts +4 -2
  272. package/src/providers/quota/report-cache.ts +14 -0
  273. package/src/providers/quota/vendor-probes-oauth.ts +3 -3
  274. package/src/providers/quota.ts +4 -1
  275. package/src/providers/registry/entries-core.ts +21 -20
  276. package/src/providers/registry/entries-extended.ts +19 -1
  277. package/src/providers/registry/model-seeds.ts +17 -0
  278. package/src/providers/request-pacing.ts +9 -0
  279. package/src/providers/stale-no-vision-seed-migration.ts +72 -0
  280. package/src/responses/compaction.ts +14 -0
  281. package/src/responses/parser.ts +37 -20
  282. package/src/responses/state.ts +32 -29
  283. package/src/routing/history/build-file.ts +92 -0
  284. package/src/routing/history/indexer.ts +248 -216
  285. package/src/routing/history/ingest.ts +238 -0
  286. package/src/routing/history/rebuild-job.ts +111 -0
  287. package/src/routing/history/rebuild-protocol.ts +15 -0
  288. package/src/routing/history/rebuild-worker.ts +34 -0
  289. package/src/server/admission-model-scope.ts +4 -1
  290. package/src/server/antigravity-direct-eligibility.ts +208 -0
  291. package/src/server/auth-cors.ts +46 -0
  292. package/src/server/chat-completions.ts +75 -9
  293. package/src/server/claude-messages.ts +110 -20
  294. package/src/server/configured-credentials.ts +194 -0
  295. package/src/server/index/desktop-compatibility-startup.ts +56 -0
  296. package/src/server/index/link-management.ts +58 -0
  297. package/src/server/index/optional-listeners.ts +5 -1
  298. package/src/server/index/serve-options.ts +15 -2
  299. package/src/server/index/startup-warnings.ts +9 -0
  300. package/src/server/index/websocket-handler.ts +9 -0
  301. package/src/server/index.ts +13 -10
  302. package/src/server/inference/client-encoder-delivery.ts +6 -5
  303. package/src/server/lifecycle.ts +2 -1
  304. package/src/server/login-shell-path.ts +146 -0
  305. package/src/server/management/advisor-routes.ts +191 -0
  306. package/src/server/management/agent-settings-routes.ts +50 -14
  307. package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
  308. package/src/server/management/codex-agent-role-routes.ts +19 -6
  309. package/src/server/management/codex-prompt-routes.ts +136 -5
  310. package/src/server/management/codex-role-auto-assign.ts +1 -1
  311. package/src/server/management/companion-routes.ts +4 -0
  312. package/src/server/management/config-routes.ts +2 -2
  313. package/src/server/management/context.ts +4 -0
  314. package/src/server/management/desktop-compatibility-routes.ts +46 -0
  315. package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
  316. package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
  317. package/src/server/management/grok-coupon-routes.ts +78 -27
  318. package/src/server/management/link-enrollment.ts +192 -0
  319. package/src/server/management/link-route-state.ts +185 -0
  320. package/src/server/management/link-routes.ts +82 -402
  321. package/src/server/management/model-routes.ts +2 -2
  322. package/src/server/management/model-rows.ts +3 -1
  323. package/src/server/management/protocol-settings-patch.ts +1 -0
  324. package/src/server/management/provider-overwrite-carry.ts +10 -0
  325. package/src/server/management/provider-routes.ts +1 -0
  326. package/src/server/management/route-registry.ts +16 -3
  327. package/src/server/management/shared.ts +3 -0
  328. package/src/server/management/sibling-guard.ts +1 -0
  329. package/src/server/management-api.ts +9 -1
  330. package/src/server/messages-antigravity-direct.ts +195 -0
  331. package/src/server/messages-native.ts +73 -43
  332. package/src/server/relay-eager.ts +2 -0
  333. package/src/server/request-log-filter.ts +6 -1
  334. package/src/server/request-log-terminal-status.ts +63 -6
  335. package/src/server/request-log.ts +7 -0
  336. package/src/server/responses/adapter-continuation.ts +35 -18
  337. package/src/server/responses/adapter-delivery.ts +24 -2
  338. package/src/server/responses/adapter-dispatch.ts +261 -81
  339. package/src/server/responses/advisor-plan-slot.ts +27 -0
  340. package/src/server/responses/advisor-slot.ts +336 -0
  341. package/src/server/responses/code-mode-collab-surface.ts +313 -0
  342. package/src/server/responses/codex-ws-exchange.ts +147 -6
  343. package/src/server/responses/codex-ws-wire.ts +203 -0
  344. package/src/server/responses/collaboration.ts +36 -7
  345. package/src/server/responses/compact.ts +126 -48
  346. package/src/server/responses/context-overflow.ts +13 -1
  347. package/src/server/responses/core-codex-account.ts +105 -1
  348. package/src/server/responses/core-combo-failure.ts +24 -1
  349. package/src/server/responses/core-combo.ts +28 -2
  350. package/src/server/responses/core-errors.ts +29 -1
  351. package/src/server/responses/core-lifetime.ts +2 -0
  352. package/src/server/responses/core-normalize.ts +11 -0
  353. package/src/server/responses/core-options.ts +48 -0
  354. package/src/server/responses/core-replay.ts +7 -4
  355. package/src/server/responses/core.ts +1 -1
  356. package/src/server/responses/deferred-collab-surface.ts +158 -0
  357. package/src/server/responses/deferred-send-reporter.ts +25 -0
  358. package/src/server/responses/fetch-helpers.ts +12 -1
  359. package/src/server/responses/passthrough-delivery.ts +20 -11
  360. package/src/server/responses/passthrough-dispatch.ts +29 -7
  361. package/src/server/responses/request-prepare.ts +74 -9
  362. package/src/server/responses/request-transport.ts +22 -8
  363. package/src/server/responses/run-turn-execution.ts +70 -8
  364. package/src/server/responses/sidecar-execution.ts +89 -5
  365. package/src/server/responses/terminal-guard.ts +2 -1
  366. package/src/server/responses/ws-upstream.ts +9 -1
  367. package/src/server/responses-undeclared-tool-guard.ts +30 -9
  368. package/src/server/startup-action-control.ts +38 -4
  369. package/src/server/startup-health-cache.ts +5 -0
  370. package/src/server/system-env-shell.ts +7 -2
  371. package/src/server/system-env.ts +11 -1
  372. package/src/service/cli.ts +5 -1
  373. package/src/service/desktop-startup.ts +161 -34
  374. package/src/service/desktop-supervision.d.mts +27 -1
  375. package/src/service/desktop-supervision.mjs +111 -5
  376. package/src/service/health-sentinel-launch.ts +176 -0
  377. package/src/service/health-sentinel-main.ts +466 -0
  378. package/src/service/health-sentinel-policy.ts +241 -0
  379. package/src/service/health-sentinel-probe.ts +71 -0
  380. package/src/service/health-sentinel-windows.ts +87 -0
  381. package/src/service/linux-linger.ts +20 -0
  382. package/src/service/logging.ts +72 -0
  383. package/src/tray/windows-tray.ps1 +43 -24
  384. package/src/types/config.ts +68 -6
  385. package/src/types/provider.ts +7 -0
  386. package/src/types/request.ts +15 -0
  387. package/src/types/tools.ts +2 -0
  388. package/src/update/async-check.ts +1 -1
  389. package/src/update/badge.ts +2 -2
  390. package/src/update/index.ts +88 -15
  391. package/src/update/job-supervised-restart.ts +107 -0
  392. package/src/update/job.ts +32 -32
  393. package/src/update/notify.ts +1 -1
  394. package/src/update/refresh-scheduler.ts +2 -2
  395. package/src/update/supervised-recovery.d.mts +27 -0
  396. package/src/update/supervised-recovery.mjs +157 -0
  397. package/src/usage/cache-diagnostic.ts +2 -0
  398. package/src/usage/expected-prices.ts +16 -0
  399. package/src/usage/log.ts +19 -1
  400. package/src/web-search/gemini-executor.ts +2 -2
  401. package/assets/sponsors/packycode.png +0 -0
  402. package/gui/dist/assets/App-Ba8uG696.js +0 -54
  403. package/gui/dist/assets/Tray-IImbNPRy.js +0 -1
  404. package/gui/dist/assets/index-BHRZdJuv.js +0 -94
  405. package/gui/dist/assets/quota-summary-DiJO6sYY.js +0 -1
@@ -0,0 +1,146 @@
1
+ /**
2
+ * The user's login-shell PATH, for installers the dashboard runs under OpenCodex Desktop (#6971).
3
+ *
4
+ * A Finder or login launch gives the Desktop app, and so its proxy sidecar, only the system PATH
5
+ * (`/usr/bin:/bin:/usr/sbin:/sbin`). `ocx codex-shim install` scans PATH for the native Codex
6
+ * launcher, so from the dashboard it reported "Native Codex launcher not found" for Homebrew, npm,
7
+ * bun, nvm, fnm, volta and mise installs, and an npm launcher had no `node` for its probe.
8
+ *
9
+ * This asks the user's shell once, as an interactive login shell would start, and keeps only PATH:
10
+ * the value is framed by markers so startup-file output cannot leak into it. The read has a
11
+ * deadline, runs in its own process group so a hung startup file is killed with all of its
12
+ * children, and its result, including failure, is cached so repeated dashboard actions do not
13
+ * spawn shells. It is scoped to the shim installer's child environment; the service definition's
14
+ * PATH has its own policy.
15
+ */
16
+ import { spawn as nodeSpawn, type ChildProcess } from "node:child_process";
17
+ import { userInfo } from "node:os";
18
+ import { basename, isAbsolute } from "node:path";
19
+
20
+ const BEGIN = "__OCX_LOGIN_PATH_BEGIN__";
21
+ const END = "__OCX_LOGIN_PATH_END__";
22
+ export const LOGIN_SHELL_PATH_TIMEOUT_MS = 5_000;
23
+ export const LOGIN_SHELL_PATH_CACHE_MS = 5 * 60_000;
24
+ const MAX_OUTPUT_BYTES = 64 * 1024;
25
+ /** After the shell exits, how long its stdout may stay open before a background job is assumed to hold it. */
26
+ const STDOUT_DRAIN_MS = 500;
27
+
28
+ export function loginShellPathArgs(shell: string): string[] {
29
+ const print = basename(shell) === "fish"
30
+ ? `printf '%s%s%s\\n' ${BEGIN} (string join : $PATH) ${END}`
31
+ : `printf '%s%s%s\\n' ${BEGIN} "$PATH" ${END}`;
32
+ return ["-l", "-i", "-c", print];
33
+ }
34
+
35
+ /** The framed PATH, only when every entry is absolute. */
36
+ export function parseLoginShellPath(output: string): string | null {
37
+ const start = output.lastIndexOf(BEGIN);
38
+ if (start < 0) return null;
39
+ const end = output.indexOf(END, start + BEGIN.length);
40
+ if (end < 0) return null;
41
+ const value = output.slice(start + BEGIN.length, end);
42
+ if (!value || /[\r\n\0]/.test(value)) return null;
43
+ const entries = value.split(":").filter(Boolean);
44
+ return entries.length > 0 && entries.every(entry => isAbsolute(entry)) ? entries.join(":") : null;
45
+ }
46
+
47
+ /** Ordered union: the shell's entries first, then any the process had that the shell did not. */
48
+ export function mergePathLists(preferred: string, fallback: string | undefined): string {
49
+ // POSIX only: the login shell runs on macOS and Linux, where PATH is colon-separated.
50
+ return [...new Set([...preferred.split(":"), ...(fallback ?? "").split(":")].filter(Boolean))].join(":");
51
+ }
52
+
53
+ export interface LoginShellPathDeps {
54
+ platform?: NodeJS.Platform;
55
+ shell?: string | null;
56
+ env?: NodeJS.ProcessEnv;
57
+ spawn?: (command: string, args: string[], options: Parameters<typeof nodeSpawn>[2]) => ChildProcess;
58
+ killGroup?: (pid: number) => void;
59
+ timeoutMs?: number;
60
+ now?: () => number;
61
+ }
62
+
63
+ let cached: { at: number; value: string | null } | null = null;
64
+
65
+ export function resetLoginShellPathCacheForTests(): void {
66
+ cached = null;
67
+ }
68
+
69
+ function defaultShell(env: NodeJS.ProcessEnv): string | null {
70
+ const fromEnv = env.SHELL?.trim();
71
+ if (fromEnv && isAbsolute(fromEnv)) return fromEnv;
72
+ try {
73
+ const shell = userInfo().shell;
74
+ return shell && isAbsolute(shell) ? shell : null;
75
+ } catch { return null; }
76
+ }
77
+
78
+ function killProcessGroup(pid: number): void {
79
+ try { process.kill(-pid, "SIGKILL"); } catch { /* already gone */ }
80
+ }
81
+
82
+ export async function readLoginShellPath(deps: LoginShellPathDeps = {}): Promise<string | null> {
83
+ const platform = deps.platform ?? process.platform;
84
+ if (platform === "win32") return null;
85
+ const now = deps.now ?? Date.now;
86
+ if (cached && now() - cached.at < LOGIN_SHELL_PATH_CACHE_MS) return cached.value;
87
+ const env = deps.env ?? process.env;
88
+ const shell = deps.shell === undefined ? defaultShell(env) : deps.shell;
89
+ const value = shell ? await probe(shell, env, deps) : null;
90
+ cached = { at: now(), value };
91
+ return value;
92
+ }
93
+
94
+ function probe(shell: string, env: NodeJS.ProcessEnv, deps: LoginShellPathDeps): Promise<string | null> {
95
+ const spawn = deps.spawn ?? nodeSpawn;
96
+ const killGroup = deps.killGroup ?? killProcessGroup;
97
+ return new Promise(resolve => {
98
+ let child: ChildProcess;
99
+ try {
100
+ child = spawn(shell, loginShellPathArgs(shell), {
101
+ detached: true,
102
+ stdio: ["ignore", "pipe", "ignore"],
103
+ // Only what a login shell needs to find its startup files; no proxy or credential state.
104
+ env: Object.fromEntries(Object.entries({
105
+ HOME: env.HOME, USER: env.USER, LOGNAME: env.LOGNAME, SHELL: shell, LANG: env.LANG,
106
+ PATH: env.PATH, TERM: "dumb",
107
+ }).filter(([, value]) => value !== undefined)) as NodeJS.ProcessEnv,
108
+ });
109
+ } catch { resolve(null); return; }
110
+ let output = "";
111
+ let settled = false;
112
+ let exitCode: number | null = null;
113
+ let drain: ReturnType<typeof setTimeout> | undefined;
114
+ const finish = (value: string | null, killGroupMembers: boolean) => {
115
+ if (settled) return;
116
+ settled = true;
117
+ clearTimeout(timer);
118
+ clearTimeout(drain);
119
+ // A hung startup file, or a background job still holding the pipe, is killed with its group.
120
+ // A shell that exited and closed its output is left alone: its group id may already be free.
121
+ if (killGroupMembers && child.pid) killGroup(child.pid);
122
+ resolve(value);
123
+ };
124
+ const timer = setTimeout(() => finish(null, true), deps.timeoutMs ?? LOGIN_SHELL_PATH_TIMEOUT_MS);
125
+ child.stdout?.setEncoding("utf8");
126
+ child.stdout?.on("data", (chunk: string) => {
127
+ output += chunk;
128
+ if (output.length > MAX_OUTPUT_BYTES) finish(null, true);
129
+ });
130
+ const parsed = () => exitCode === 0 ? parseLoginShellPath(output) : null;
131
+ child.once("error", () => finish(null, true));
132
+ // Parse only after stdout closes: `exit` can arrive before the last chunk.
133
+ child.once("close", code => { exitCode ??= code; finish(parsed(), false); });
134
+ child.once("exit", code => {
135
+ exitCode = code;
136
+ drain = setTimeout(() => finish(parsed(), true), STDOUT_DRAIN_MS);
137
+ });
138
+ });
139
+ }
140
+
141
+ /** `env` with the login-shell PATH in front, or unchanged when it cannot be read. */
142
+ export async function withLoginShellPath(env: NodeJS.ProcessEnv, deps: LoginShellPathDeps = {}): Promise<NodeJS.ProcessEnv> {
143
+ const loginPath = await readLoginShellPath({ ...deps, env });
144
+ return loginPath ? { ...env, PATH: mergePathLists(loginPath, env.PATH) } : env;
145
+ }
146
+
@@ -0,0 +1,191 @@
1
+ /**
2
+ * GET / PUT /api/advisor/settings — the advisor sidecar's management surface.
3
+ *
4
+ * GET answers the RESOLVED settings (defaults applied, sources marked) plus availability, so the
5
+ * GUI/CLI show real runtime state rather than stored fields. PUT is a strict partial patch:
6
+ * unknown keys and wrong types are refused, values are validated against the same resolver the
7
+ * runtime reads, and the patch persists through the locked config writer with the snapshot-
8
+ * restore discipline used by PATCH /api/protocols/settings — a refused or failed write never
9
+ * leaves the live config serving a state the file does not hold.
10
+ */
11
+ import { activateAdvisor } from "../../lib/advisor-activation";
12
+ import { jsonResponse } from "../auth-cors";
13
+ import { readManagementJsonBodyOr } from "./body";
14
+ import type { ManagementContext } from "./context";
15
+ import {
16
+ ADVISOR_CONTEXT_SHARING_CONSENT_VERSION,
17
+ ADVISOR_EFFORTS,
18
+ advisorContextSharingBlocked,
19
+ advisorRunnable,
20
+ isCurrentAdvisorContextSharingConsent,
21
+ isValidAdvisorEffort,
22
+ isValidAdvisorPolicy,
23
+ resolveAdvisorSettings,
24
+ type AdvisorEffort,
25
+ type AdvisorPolicy,
26
+ } from "../../advisor/settings";
27
+
28
+ const INVALID_BODY = Symbol("invalid-body");
29
+
30
+ interface AdvisorPatch {
31
+ enabled?: boolean;
32
+ model?: string;
33
+ effort?: AdvisorEffort;
34
+ policy?: AdvisorPolicy;
35
+ timeoutMs?: number;
36
+ /** "v1" records current consent. null removes it. Enabling does not imply this field. */
37
+ contextSharingConsent?: typeof ADVISOR_CONTEXT_SHARING_CONSENT_VERSION | null;
38
+ reset?: boolean;
39
+ }
40
+
41
+ type ParsedPatch = { ok: true; patch: AdvisorPatch } | { ok: false; code: string; message: string };
42
+
43
+ const PATCH_KEYS = new Set(["enabled", "model", "effort", "policy", "timeoutMs", "contextSharingConsent", "reset"]);
44
+
45
+ type Rec = Record<string, unknown>;
46
+ function isRec(value: unknown): value is Rec {
47
+ return !!value && typeof value === "object" && !Array.isArray(value);
48
+ }
49
+
50
+ /** Strict: unknown keys and wrong types are refused; messages name the field, never the value. */
51
+ export function parseAdvisorSettingsPatch(body: unknown): ParsedPatch {
52
+ if (!isRec(body)) return { ok: false, code: "invalid_body", message: "body must be a JSON object" };
53
+ if (Object.keys(body).length === 0) return { ok: false, code: "empty_body", message: "body must set at least one of enabled, model, effort, policy, timeoutMs, contextSharingConsent or reset" };
54
+ for (const key of Object.keys(body)) {
55
+ if (!PATCH_KEYS.has(key)) return { ok: false, code: "unknown_field", message: "body accepts only enabled, model, effort, policy, timeoutMs, contextSharingConsent and reset" };
56
+ }
57
+ const patch: AdvisorPatch = {};
58
+ if (body.reset !== undefined) {
59
+ if (body.reset !== true) return { ok: false, code: "invalid_reset", message: "reset must be true when present" };
60
+ if (Object.keys(body).length > 1) return { ok: false, code: "reset_with_fields", message: "reset must be the only field when present" };
61
+ patch.reset = true;
62
+ return { ok: true, patch };
63
+ }
64
+ if (body.enabled !== undefined) {
65
+ if (typeof body.enabled !== "boolean") return { ok: false, code: "invalid_enabled", message: "enabled must be a boolean" };
66
+ patch.enabled = body.enabled;
67
+ }
68
+ if (body.model !== undefined) {
69
+ // An empty or whitespace-only value is the supported "clear the model" state, not an error:
70
+ // the resolver defaults to an empty model and `advisorRunnable` requires a non-blank one. The
71
+ // only structural rules are the string type and the trimmed length bound.
72
+ if (typeof body.model !== "string" || body.model.trim().length > 200) {
73
+ return {
74
+ ok: false,
75
+ code: "invalid_model",
76
+ message: "model must be a string whose trimmed value is at most 200 characters; an empty value clears the advisor model",
77
+ };
78
+ }
79
+ patch.model = body.model.trim();
80
+ }
81
+ if (body.effort !== undefined) {
82
+ if (!isValidAdvisorEffort(body.effort)) {
83
+ return { ok: false, code: "invalid_effort", message: `effort must be one of ${ADVISOR_EFFORTS.join(", ")}` };
84
+ }
85
+ patch.effort = body.effort;
86
+ }
87
+ if (body.policy !== undefined) {
88
+ if (!isValidAdvisorPolicy(body.policy)) {
89
+ return { ok: false, code: "invalid_policy", message: 'policy must be "manual" or "preflight"' };
90
+ }
91
+ patch.policy = body.policy;
92
+ }
93
+ if (body.timeoutMs !== undefined) {
94
+ if (typeof body.timeoutMs !== "number" || !Number.isFinite(body.timeoutMs) || body.timeoutMs < 1_000 || body.timeoutMs > 600_000) {
95
+ return { ok: false, code: "invalid_timeout", message: "timeoutMs must be a number between 1000 and 600000" };
96
+ }
97
+ patch.timeoutMs = Math.floor(body.timeoutMs);
98
+ }
99
+ if (body.contextSharingConsent !== undefined) {
100
+ if (body.contextSharingConsent === null) {
101
+ patch.contextSharingConsent = null;
102
+ } else if (isCurrentAdvisorContextSharingConsent(body.contextSharingConsent)) {
103
+ patch.contextSharingConsent = body.contextSharingConsent;
104
+ } else {
105
+ return {
106
+ ok: false,
107
+ code: "invalid_context_sharing_consent",
108
+ message: `contextSharingConsent must be "${ADVISOR_CONTEXT_SHARING_CONSENT_VERSION}" or null`,
109
+ };
110
+ }
111
+ }
112
+ return { ok: true, patch };
113
+ }
114
+
115
+ function advisorInfo(config: ManagementContext["config"]): Record<string, unknown> {
116
+ const settings = resolveAdvisorSettings(config);
117
+ const warning = settings.enabled && settings.model.trim() === ""
118
+ ? "advisor_enabled_without_model"
119
+ : advisorContextSharingBlocked(settings)
120
+ ? "advisor_context_sharing_consent_required"
121
+ : undefined;
122
+ return {
123
+ settings,
124
+ runnable: advisorRunnable(settings),
125
+ // Enabled without a model cannot consult. Enabled with a model but without current
126
+ // consent must not send task context; the warning names that block.
127
+ ...(warning ? { warning } : {}),
128
+ };
129
+ }
130
+
131
+ function applyPatchInMemory(config: ManagementContext["config"], patch: AdvisorPatch): void {
132
+ if (patch.reset) {
133
+ delete config.advisor;
134
+ return;
135
+ }
136
+ const current: Rec = isRec(config.advisor) ? { ...config.advisor } : {};
137
+ if (patch.enabled !== undefined) current.enabled = patch.enabled;
138
+ if (patch.model !== undefined) current.model = patch.model;
139
+ if (patch.effort !== undefined) current.effort = patch.effort;
140
+ if (patch.policy !== undefined) current.policy = patch.policy;
141
+ if (patch.timeoutMs !== undefined) current.timeoutMs = patch.timeoutMs;
142
+ if (patch.contextSharingConsent === null) delete current.contextSharingConsent;
143
+ else if (patch.contextSharingConsent !== undefined) current.contextSharingConsent = patch.contextSharingConsent;
144
+ config.advisor = current as ManagementContext["config"]["advisor"];
145
+ }
146
+
147
+ function isConfigLockContention(error: unknown): boolean {
148
+ if (!error || typeof error !== "object") return false;
149
+ if ((error as { code?: unknown }).code !== "CONFIG_MUTATION_LOCK_UNAVAILABLE") return false;
150
+ return (error as { cause?: { code?: unknown } }).cause?.code === "SQLITE_BUSY";
151
+ }
152
+
153
+ export async function handleAdvisorRoutes(ctx: ManagementContext): Promise<Response | null> {
154
+ const { url, req, config } = ctx;
155
+ if (url.pathname !== "/api/advisor/settings") return null;
156
+
157
+ if (req.method === "GET") return jsonResponse(advisorInfo(config), 200, req, config);
158
+ if (req.method === "PUT") return putAdvisorSettings(ctx);
159
+ return null;
160
+ }
161
+
162
+ async function putAdvisorSettings(ctx: ManagementContext): Promise<Response> {
163
+ const { req, config } = ctx;
164
+
165
+ const body = await readManagementJsonBodyOr(req, INVALID_BODY);
166
+ const parsed = body === INVALID_BODY
167
+ ? { ok: false as const, code: "invalid_json", message: "body must be valid JSON" }
168
+ : parseAdvisorSettingsPatch(body);
169
+ if (!parsed.ok) return jsonResponse({ error: { code: parsed.code, message: parsed.message } }, 400, req, config);
170
+
171
+ // Resolve the writer BEFORE touching the live config: a resolution failure must leave the
172
+ // in-memory config exactly as it was, not rely on the snapshot restore below.
173
+ // `deps.` first: route tests with an in-memory fixture must never write the real config.
174
+ const persist = ctx.deps.saveConfigPreservingClaudeCode
175
+ ?? (await import("../../config")).saveConfigPreservingClaudeCode;
176
+ const snapshot = isRec(config.advisor) ? { ...config.advisor } : undefined;
177
+ applyPatchInMemory(config, parsed.patch);
178
+ try {
179
+ persist(config);
180
+ } catch (error) {
181
+ // Undo in memory too: a live config that serves a state the file does not hold would
182
+ // mislead every GET until the next restart.
183
+ if (snapshot === undefined) delete config.advisor;
184
+ else config.advisor = snapshot as ManagementContext["config"]["advisor"];
185
+ return isConfigLockContention(error)
186
+ ? jsonResponse({ error: { code: "config_busy", message: "Another process is saving the configuration. Try again in a moment." } }, 409, req, config)
187
+ : jsonResponse({ error: { code: "write_failed", message: "The configuration could not be saved." } }, 500, req, config);
188
+ }
189
+ activateAdvisor(config);
190
+ return jsonResponse(advisorInfo(config), 200, req, config);
191
+ }
@@ -1,6 +1,7 @@
1
1
  import { ensureManagementClaudeIntercept, interceptStartRefusal, interceptStatus } from "./claude-intercept-routes";
2
2
  import { persistCommittedDesktopGateway } from "../../claude/desktop-gateway-state";
3
3
  import { captureDesktopAppliedMarker, commitDesktopAppliedMarker } from "../../claude/desktop-applied-marker";
4
+ import { isPassiveRead } from "../../lib/passive-read";
4
5
  import { randomUUID } from "node:crypto";
5
6
  import { readFileSync } from "node:fs";
6
7
  import { dirname } from "node:path";
@@ -230,6 +231,32 @@ async function injectionModelOptions(config: OcxConfig, models: readonly Catalog
230
231
  return [...nativeModels, ...routedModels];
231
232
  }
232
233
 
234
+ /**
235
+ * The GET /api/injection-model body. `routed` is the discovered routed catalog, or `null` in a
236
+ * passive read (a Remote Link Child's, #6959). That body lists the native
237
+ * options and the current pick, which is all a read-only Child shows.
238
+ */
239
+ export async function injectionModelView(config: OcxConfig, routed: readonly CatalogModel[] | null) {
240
+ const { CODEX_REASONING_LEVELS } = await import("../../reasoning-effort");
241
+ const available = await injectionModelOptions(config, routed ?? []);
242
+ const picked = config.injectionModel;
243
+ if (routed === null && picked && !available.some(option => option.namespaced === picked)) {
244
+ const slash = picked.indexOf("/");
245
+ available.push(slash > 0
246
+ ? { provider: picked.slice(0, slash), model: picked.slice(slash + 1), namespaced: picked }
247
+ : { provider: "openai", model: picked, namespaced: picked });
248
+ }
249
+ return {
250
+ multiAgentGuidanceEnabled: multiAgentGuidanceEnabled(config),
251
+ syncCodexSubagentDefaults: subagentDefaultSyncEffective(config),
252
+ model: config.injectionModel ?? null,
253
+ effort: config.injectionEffort ?? null,
254
+ prompt: config.injectionPrompt ?? null,
255
+ efforts: CODEX_REASONING_LEVELS.map(l => l.effort),
256
+ available,
257
+ };
258
+ }
259
+
233
260
  export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise<Response | null> {
234
261
  const { req, url, config, deps, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
235
262
 
@@ -620,16 +647,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
620
647
  // effort the prompt tells the agent to pass to spawn_agent. GET returns the current
621
648
  // picks + available models/efforts; PUT sets or clears them.
622
649
  if (url.pathname === "/api/injection-model" && req.method === "GET") {
623
- const { CODEX_REASONING_LEVELS } = await import("../../reasoning-effort");
624
- return jsonResponse({
625
- multiAgentGuidanceEnabled: multiAgentGuidanceEnabled(config),
626
- syncCodexSubagentDefaults: subagentDefaultSyncEffective(config),
627
- model: config.injectionModel ?? null,
628
- effort: config.injectionEffort ?? null,
629
- prompt: config.injectionPrompt ?? null,
630
- efforts: CODEX_REASONING_LEVELS.map(l => l.effort),
631
- available: await injectionModelOptions(config, await fetchAllModels(config)),
632
- });
650
+ return jsonResponse(await injectionModelView(config, isPassiveRead() ? null : await fetchAllModels(config)));
633
651
  }
634
652
  // Read-only: sizes the described delegated work and proposes a model and effort from the
635
653
  // same options GET offers. The page applies an accepted proposal through the PUT below.
@@ -1379,6 +1397,24 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1379
1397
  }
1380
1398
 
1381
1399
  // Claude Code inbound settings (GUI "Claude ON" toggle + Claude page).
1400
+ if (url.pathname === "/api/claude-code/model-defaults" && (req.method === "GET" || req.method === "POST")) {
1401
+ const { previewClaudeModelDefaults, applyClaudeModelDefaults, ModelDefaultsRecoveryError } = await import("../../claude/model-default-settings");
1402
+ try {
1403
+ if (req.method === "GET") return jsonResponse(previewClaudeModelDefaults(config.claudeCode?.tierModels));
1404
+ let body: unknown;
1405
+ try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
1406
+ if (!body || typeof body !== "object" || Array.isArray(body) || Object.keys(body).length !== 1 ||
1407
+ !("fingerprint" in body) || typeof body.fingerprint !== "string" || !/^[a-f0-9]{64}$/.test(body.fingerprint)) {
1408
+ return jsonResponse({ error: "fingerprint is required" }, 400);
1409
+ }
1410
+ return jsonResponse(applyClaudeModelDefaults(config.claudeCode?.tierModels, body.fingerprint));
1411
+ } catch (error) {
1412
+ rethrowManagementBodyTooLarge(error);
1413
+ if (error instanceof ModelDefaultsRecoveryError) return jsonResponse({ error: error.message, code: error.message, backupPath: error.backupPath }, 500);
1414
+ const code = error instanceof Error && error.message === "settings_changed" ? "settings_changed" : "settings_unavailable";
1415
+ return jsonResponse({ error: code, code }, code === "settings_changed" ? 409 : 500);
1416
+ }
1417
+ }
1382
1418
  if (url.pathname === "/api/claude-code" && req.method === "GET") {
1383
1419
  const models = await fetchAllModels(config);
1384
1420
  const { listCatalogNativeSlugs } = await import("../../codex/catalog");
@@ -1495,10 +1531,10 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1495
1531
  });
1496
1532
  }
1497
1533
  if (url.pathname === "/api/claude-code" && req.method === "PUT") {
1498
- // NOTE: model / tierModels / maxContextTokens / alwaysEnableEffort are
1534
+ // NOTE: model / maxContextTokens / alwaysEnableEffort are
1499
1535
  // CONFIG-ONLY back-compat fields — the GUI no longer offers controls for them
1500
- // (default model is owned by Claude Code's /model picker; roster agents
1501
- // supersede tiers; auto-context supersedes the max-context pair; effort rides
1536
+ // (default model is owned by Claude Code's /model picker;
1537
+ // auto-context supersedes the max-context pair; effort rides
1502
1538
  // regardless on 2.1.207). PUT keeps validating them so hand-written configs
1503
1539
  // and older GUIs stay safe; GUI saves omit them and the spread preserves them.
1504
1540
  let parsedBody: unknown;
@@ -1817,7 +1853,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1817
1853
  }
1818
1854
  }
1819
1855
  if (body.tierModels !== undefined) {
1820
- // CONFIG-ONLY back-compat (GUI pickers removed — roster agents supersede tiers).
1856
+ // Family defaults also serve third-party callers that request native tier aliases.
1821
1857
  if (body.tierModels === null) {
1822
1858
  delete next.tierModels;
1823
1859
  } else if (!isPlainObject(body.tierModels)) {
@@ -19,6 +19,7 @@ import { captureAnthropicPhysicalSendOwnership, anthropicPhysicalSendOwnershipIs
19
19
  import { anthropicCooldownRecoveryFor } from "../../providers/quota/anthropic-cooldown-recovery";
20
20
  import { captureProviderAccountQuotaEpoch } from "../../providers/quota/account-cache";
21
21
  import { captureOAuthAccountSelection, getAccountSet, getAccountCredentialWithStatus, listAccounts } from "../../oauth/store";
22
+ import { admitAnthropicAccountAfterReset, type AnthropicResetAdmissionEvidence } from "../../providers/anthropic-reset-admission";
22
23
  import {
23
24
  ANTHROPIC_RESET_GRANT_ID_RE,
24
25
  AnthropicResetGrantError,
@@ -74,6 +75,8 @@ export interface AnthropicResetGrantRouteDeps {
74
75
  listAccountIds: () => string[];
75
76
  activeAccountId: () => string | null;
76
77
  accessTokenFor: (accountId: string) => Promise<string>;
78
+ /** Lift local admission after a journaled reset, bounded by its journal evidence. Tests omit it. */
79
+ admitReset?: (accountId: string, evidence: AnthropicResetAdmissionEvidence) => Promise<void>;
77
80
  }
78
81
 
79
82
  function defaultDepsFor(ctx: ManagementContext, provider: AnthropicInstanceId): AnthropicResetGrantRouteDeps | null {
@@ -109,6 +112,7 @@ function defaultDepsFor(ctx: ManagementContext, provider: AnthropicInstanceId):
109
112
  if (!targetCurrent() || !owner) throw new Error("reset owner changed");
110
113
  return snapshot.accessToken;
111
114
  },
115
+ admitReset: (accountId, evidence) => admitAnthropicAccountAfterReset(provider, accountId, evidence),
112
116
  };
113
117
  }
114
118
 
@@ -209,6 +213,16 @@ async function handleConsume(ctx: ManagementContext, injected?: AnthropicResetGr
209
213
  }
210
214
  if (deps.isCurrent?.() === false) return scopedFail(401, "auth_failed");
211
215
  const ledger = { journalPath: deps.journalPath, now: deps.now?.() };
216
+ // Retry-after cooldowns survive a usage probe. A journaled reset, including a
217
+ // replay after the process died before admission, has to lift this account,
218
+ // but only refusals recorded before the claim was journaled and only for the
219
+ // windows the journal says were cleared. A failed reread must not turn a
220
+ // spent grant into an error.
221
+ const admitJournaledReset = async (code: string, evidence: AnthropicResetAdmissionEvidence): Promise<void> => {
222
+ if (code !== "reset" || deps.isCurrent?.() === false) return;
223
+ try { await deps.admitReset?.(accountId, evidence); }
224
+ catch { /* The grant is already spent. */ }
225
+ };
212
226
 
213
227
  // A new operation must pass the spend gate on a fresh read. A same-id retry of
214
228
  // an open record skips it: that attempt was already gated, and upstream
@@ -242,6 +256,7 @@ async function handleConsume(ctx: ManagementContext, injected?: AnthropicResetGr
242
256
  }
243
257
  switch (begin.kind) {
244
258
  case "replay":
259
+ await admitJournaledReset(begin.code, { claimedAt: begin.claimedAt, cleared: begin.cleared });
245
260
  return jsonResponse(
246
261
  { provider, code: begin.code, replayed: true, resetsLeft: begin.resetsLeft, accountId, grantId, operationId },
247
262
  200, ctx.req, ctx.config,
@@ -268,7 +283,7 @@ async function handleConsume(ctx: ManagementContext, injected?: AnthropicResetGr
268
283
  let settled;
269
284
  try {
270
285
  settled = settleAnthropicResetOperation(
271
- { operationId, code: answer.code, resetsLeft: answer.resetsLeft },
286
+ { operationId, code: answer.code, resetsLeft: answer.resetsLeft, cleared: answer.cleared },
272
287
  { journalPath: deps.journalPath, now: deps.now?.() },
273
288
  );
274
289
  } catch {
@@ -278,6 +293,7 @@ async function handleConsume(ctx: ManagementContext, injected?: AnthropicResetGr
278
293
  // Report what the journal holds. An overlapping same-id attempt may have
279
294
  // settled first; its answer is the canonical one.
280
295
  const replayed = settled.code !== answer.code || settled.resetsLeft !== answer.resetsLeft;
296
+ await admitJournaledReset(settled.code, { claimedAt: settled.claimedAt, cleared: settled.cleared });
281
297
  return jsonResponse(
282
298
  {
283
299
  provider,
@@ -96,12 +96,18 @@ export async function handleCodexAgentRoleRoutes(ctx: ManagementContext): Promis
96
96
  return jsonResponse({ error: "invalid JSON body" }, 400, req, config);
97
97
  }
98
98
 
99
- let model: string;
100
- let effort: string | undefined;
99
+ // An omitted model leaves the role's model pin (or its inheritance) alone, an omitted effort
100
+ // keeps the effort, and an effort of null clears it. A body with neither changes nothing, which
101
+ // is a client mistake rather than a save, so it is refused before any role file is read.
102
+ if (body?.model === undefined && body?.effort === undefined) {
103
+ return jsonResponse({ error: "send a model, an effort, or both", code: "empty_update" }, 400, req, config);
104
+ }
105
+ let model: string | undefined;
106
+ let effort: string | null | undefined;
101
107
  let toml: { status: "written" | "unchanged" };
102
108
  try {
103
- model = roles.validateAgentRoleModel(body?.model);
104
- effort = body?.effort === undefined || body.effort === null ? undefined : roles.validateAgentRoleEffort(body.effort);
109
+ model = body?.model === undefined ? undefined : roles.validateAgentRoleModel(body.model);
110
+ effort = body?.effort === undefined ? undefined : body.effort === null ? null : roles.validateAgentRoleEffort(body.effort);
105
111
  toml = roles.writeCodexAgentRoleModel(role, model, codexHome, effort);
106
112
  } catch (error) {
107
113
  if (error instanceof roles.AgentRoleModelError) {
@@ -115,10 +121,17 @@ export async function handleCodexAgentRoleRoutes(ctx: ManagementContext): Promis
115
121
  // The role TOML is what Codex obeys, so its write stands even when the omo mirror cannot follow.
116
122
  let omoStatus: ReturnType<typeof omo.writeOmoRoleModel> | "write_failed";
117
123
  try {
118
- const reasoning = effort === undefined ? undefined : omo.omoReasoningFor(effort);
124
+ const reasoning = effort === undefined ? undefined : effort === null ? null : omo.omoReasoningFor(effort);
119
125
  omoStatus = omo.writeOmoRoleModel(role, model, omo.omoJsoncPath(), reasoning);
120
126
  } catch {
121
127
  omoStatus = "write_failed";
122
128
  }
123
- return jsonResponse({ ok: true, role, model, ...(effort ? { effort } : {}), toml, omoJsonc: { status: omoStatus } }, 200, req, config);
129
+ return jsonResponse({
130
+ ok: true,
131
+ role,
132
+ ...(model !== undefined ? { model } : {}),
133
+ ...(effort !== undefined ? { effort } : {}),
134
+ toml,
135
+ omoJsonc: { status: omoStatus },
136
+ }, 200, req, config);
124
137
  }