@bitkyc08/opencodex 2.86.0 → 2.88.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 (400) hide show
  1. package/gui/dist/assets/App-CHC89EXQ.js +58 -0
  2. package/gui/dist/assets/App-wT3mXqFz.css +1 -0
  3. package/gui/dist/assets/{Tray-DyR9yBee.js → Tray-B5bC7AWL.js} +1 -1
  4. package/gui/dist/assets/index-B5bsvxor.js +94 -0
  5. package/gui/dist/assets/index-DPeB9HpV.css +1 -0
  6. package/gui/dist/assets/quota-summary-D8y2wTsB.js +1 -0
  7. package/gui/dist/index.html +2 -2
  8. package/package.json +1 -1
  9. package/src/adapters/anthropic/beta-allowlist.ts +9 -8
  10. package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
  11. package/src/adapters/anthropic/passthrough.ts +7 -23
  12. package/src/adapters/anthropic-progress.ts +84 -0
  13. package/src/adapters/anthropic-rejection-memo.ts +71 -0
  14. package/src/adapters/anthropic-replay.ts +135 -0
  15. package/src/adapters/anthropic-tool-catalog.ts +11 -0
  16. package/src/adapters/anthropic.ts +151 -33
  17. package/src/adapters/base.ts +2 -0
  18. package/src/adapters/claude-cli/adapter.ts +111 -23
  19. package/src/adapters/claude-cli/profiles.ts +2 -1
  20. package/src/adapters/claude-cli/stable-replay.ts +108 -0
  21. package/src/adapters/claude-cli/usage-admission.ts +230 -0
  22. package/src/adapters/coding-agent/protocol.ts +1 -1
  23. package/src/adapters/coding-agent/turn.ts +18 -2
  24. package/src/adapters/dsh-account.ts +97 -0
  25. package/src/adapters/google-progress-history.ts +53 -0
  26. package/src/adapters/google.ts +52 -9
  27. package/src/adapters/openai-chat/messages.ts +4 -3
  28. package/src/adapters/openai-chat/passthrough.ts +8 -2
  29. package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
  30. package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
  31. package/src/adapters/openai-responses/passthrough.ts +57 -125
  32. package/src/adapters/openai-responses/request-strips.ts +25 -3
  33. package/src/adapters/openai-responses/transport.ts +81 -0
  34. package/src/adapters/qoder/adapter.ts +4 -2
  35. package/src/adapters/registry.ts +5 -0
  36. package/src/adapters/zed.ts +26 -1
  37. package/src/advisor/activity.ts +55 -0
  38. package/src/advisor/budget.ts +62 -0
  39. package/src/advisor/consult.ts +4 -1
  40. package/src/advisor/context.ts +16 -4
  41. package/src/advisor/evidence-tool.ts +21 -0
  42. package/src/advisor/evidence.ts +117 -0
  43. package/src/advisor/runtime.ts +79 -31
  44. package/src/advisor/settings.ts +11 -2
  45. package/src/bridge/errors.ts +19 -1
  46. package/src/bridge/internal.ts +7 -1
  47. package/src/bridge/response-json.ts +18 -3
  48. package/src/bridge/sse.ts +59 -8
  49. package/src/bridge/terminal-refusal.ts +59 -0
  50. package/src/claude/desktop-3p-library.ts +32 -7
  51. package/src/claude/desktop-3p.ts +55 -13
  52. package/src/claude/desktop-first-party.ts +1 -1
  53. package/src/claude/desktop-picker-platform.ts +4 -0
  54. package/src/claude/desktop-picker.ts +15 -8
  55. package/src/claude/desktop-system-proxy.ts +50 -4
  56. package/src/claude/inbound-content-options.ts +77 -0
  57. package/src/claude/inbound.ts +33 -0
  58. package/src/claude/intercept/picker-ca-startup.ts +2 -1
  59. package/src/claude/intercept/picker-runtime.ts +6 -5
  60. package/src/claude/intercept/picker-trust-windows.ts +338 -0
  61. package/src/claude/intercept/picker-trust.ts +19 -0
  62. package/src/claude/intercept/port-policy.ts +30 -0
  63. package/src/claude/intercept/runtime.ts +97 -94
  64. package/src/claude/outbound.ts +46 -13
  65. package/src/cli/account-api.ts +1 -0
  66. package/src/cli/account-policy-dto.ts +6 -0
  67. package/src/cli/account-policy.ts +34 -5
  68. package/src/cli/account-preservation.ts +44 -0
  69. package/src/cli/account.ts +8 -2
  70. package/src/cli/advisor.ts +17 -3
  71. package/src/cli/capabilities-accounts.ts +20 -1
  72. package/src/cli/capabilities-agents-routing.ts +5 -2
  73. package/src/cli/capabilities-base.ts +38 -10
  74. package/src/cli/capabilities-observe-system.ts +63 -3
  75. package/src/cli/capabilities-provider-models.ts +11 -0
  76. package/src/cli/codex-cli-update.ts +151 -41
  77. package/src/cli/codex-provider.ts +44 -0
  78. package/src/cli/combo-input.ts +10 -1
  79. package/src/cli/combo-result.ts +1 -1
  80. package/src/cli/combo.ts +2 -1
  81. package/src/cli/cross-home-owner.ts +33 -16
  82. package/src/cli/dispatch.ts +25 -4
  83. package/src/cli/doctor.ts +6 -1
  84. package/src/cli/ensure-desktop-cold-start.ts +60 -7
  85. package/src/cli/help.ts +3 -2
  86. package/src/cli/index.ts +1 -1
  87. package/src/cli/integration-plan-dto.ts +1 -0
  88. package/src/cli/log-history.ts +172 -0
  89. package/src/cli/loopback-family.ts +31 -0
  90. package/src/cli/models-runtime-subcommands.ts +1 -0
  91. package/src/cli/models-runtime.ts +72 -8
  92. package/src/cli/observe-stream.ts +2 -0
  93. package/src/cli/observe.ts +16 -4
  94. package/src/cli/provider.ts +27 -5
  95. package/src/cli/registry.ts +28 -8
  96. package/src/cli/runtime-api.ts +5 -3
  97. package/src/cli/system-command.ts +40 -8
  98. package/src/cli/uninstall-client-state.ts +32 -0
  99. package/src/cli/usage-compare.ts +309 -0
  100. package/src/cli/usage-model-search.ts +4 -4
  101. package/src/cli/usage-report.ts +2 -2
  102. package/src/cli/v2.ts +1 -1
  103. package/src/client/machine-listener.ts +1 -3
  104. package/src/client/runtime.ts +1 -0
  105. package/src/clients/client-path.ts +51 -0
  106. package/src/clients/config-export/contracts.ts +2 -1
  107. package/src/clients/config-export.ts +18 -44
  108. package/src/clients/gentle-shell-path.ts +87 -0
  109. package/src/codex/account-auto-switch.ts +2 -0
  110. package/src/codex/account-identity.ts +29 -0
  111. package/src/codex/account-label.ts +1 -1
  112. package/src/codex/account-lifecycle.ts +21 -5
  113. package/src/codex/account-preservation.ts +28 -0
  114. package/src/codex/account-use-remaining.ts +23 -0
  115. package/src/codex/app-server-processes.ts +97 -7
  116. package/src/codex/app-server-restart-service.ts +55 -4
  117. package/src/codex/auth-api/account-list.ts +4 -0
  118. package/src/codex/auth-api/account-pause-group.ts +6 -50
  119. package/src/codex/auth-api/account-preservation.ts +57 -0
  120. package/src/codex/auth-api/routes.ts +8 -0
  121. package/src/codex/auth-api/use-remaining-route.ts +36 -0
  122. package/src/codex/auth-api.ts +0 -1
  123. package/src/codex/auth-context.ts +2 -2
  124. package/src/codex/catalog/auto-review.ts +20 -3
  125. package/src/codex/catalog/build-entries.ts +9 -1
  126. package/src/codex/catalog/combo-member.ts +14 -5
  127. package/src/codex/catalog/configured-tiers.ts +1 -1
  128. package/src/codex/catalog/gather-capture.ts +2 -0
  129. package/src/codex/catalog/metadata.ts +11 -9
  130. package/src/codex/catalog/model-hints.ts +21 -16
  131. package/src/codex/catalog/model-visibility.ts +3 -1
  132. package/src/codex/catalog/parsing.ts +30 -86
  133. package/src/codex/catalog/provider-models.ts +36 -34
  134. package/src/codex/catalog/routed-gather.ts +14 -13
  135. package/src/codex/cli-install-provenance.ts +105 -17
  136. package/src/codex/cli-update-plan.ts +545 -0
  137. package/src/codex/config-write-lock.ts +23 -2
  138. package/src/codex/desktop-app/picker-freshness.ts +96 -0
  139. package/src/codex/desktop-app-restart.ts +21 -0
  140. package/src/codex/desktop-compatibility/installed-build.ts +24 -6
  141. package/src/codex/desktop-compatibility/routing-preflight.ts +1 -1
  142. package/src/codex/desktop-compatibility/runtime.ts +12 -3
  143. package/src/codex/desktop-compatibility/windows-key-protection.ts +115 -9
  144. package/src/codex/features.ts +85 -45
  145. package/src/codex/inject/history-failure.ts +27 -0
  146. package/src/codex/inject/restore.ts +24 -2
  147. package/src/codex/inject/routing-classify.ts +4 -3
  148. package/src/codex/inject.ts +18 -27
  149. package/src/codex/main-account-hard-lock.ts +30 -1
  150. package/src/codex/native-profile-startup.ts +1 -1
  151. package/src/codex/ocx-compaction-history.ts +12 -10
  152. package/src/codex/paths.ts +86 -1
  153. package/src/codex/provider-routing.ts +174 -0
  154. package/src/codex/refresh.ts +2 -2
  155. package/src/codex/routing/health-store.ts +1 -1
  156. package/src/codex/routing/selection.ts +4 -3
  157. package/src/codex/routing/thread-affinity.ts +1 -1
  158. package/src/codex/signed-in-identity.ts +43 -0
  159. package/src/codex/subagent-model-fallback.ts +1 -1
  160. package/src/codex/sync.ts +6 -1
  161. package/src/codex/user-identity.ts +16 -5
  162. package/src/codex/websocket-activity.ts +161 -0
  163. package/src/codex/websocket-registry.ts +25 -1
  164. package/src/codex/windows-local-appdata.ts +84 -0
  165. package/src/combos/failover.ts +6 -1
  166. package/src/combos/index.ts +1 -0
  167. package/src/combos/resolve.ts +46 -10
  168. package/src/combos/types.ts +15 -0
  169. package/src/config/diagnostics.ts +33 -6
  170. package/src/config/live-reconcile.ts +38 -2
  171. package/src/config/load-degrade.ts +17 -0
  172. package/src/config/opencode-wire-upgrade.ts +54 -0
  173. package/src/config/opencode-zen-upgrade.ts +53 -0
  174. package/src/config/owner-registry.ts +22 -17
  175. package/src/config/provider-validation.ts +45 -2
  176. package/src/config/schema/config-schema.ts +22 -0
  177. package/src/config/schema/leaf-validators.ts +38 -9
  178. package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
  179. package/src/config/schema/native-reasoning-retention.ts +26 -0
  180. package/src/config.ts +2 -0
  181. package/src/generated/compatibility-version.json +657 -297
  182. package/src/grok/sync.ts +1 -1
  183. package/src/images/loop.ts +116 -27
  184. package/src/integrations/catalog-refresh.ts +1 -1
  185. package/src/integrations/mutation-plan.ts +1 -0
  186. package/src/integrations/registry.ts +8 -0
  187. package/src/lib/codex-restart-contract.ts +15 -0
  188. package/src/lib/desktop-upstream-tunnel.ts +18 -53
  189. package/src/lib/empty-tool-catalog.ts +62 -0
  190. package/src/lib/errors.ts +19 -3
  191. package/src/lib/gcp-adc.ts +96 -9
  192. package/src/lib/gcp-credential-input.ts +12 -0
  193. package/src/lib/lab-live-route-production.ts +5 -0
  194. package/src/lib/opaque-proxy-tunnel.ts +217 -0
  195. package/src/lib/redact.ts +2 -2
  196. package/src/lib/request-failure-model.ts +2 -0
  197. package/src/oauth/account-quota-rank.ts +69 -2
  198. package/src/oauth/anthropic-account-refusal.ts +20 -2
  199. package/src/oauth/anthropic-oauth-definitions.ts +3 -1
  200. package/src/oauth/anthropic-routing.ts +1 -21
  201. package/src/oauth/anthropic.ts +38 -7
  202. package/src/oauth/callback-server.ts +27 -1
  203. package/src/oauth/chatgpt-device.ts +1 -1
  204. package/src/oauth/chatgpt-shared.ts +83 -0
  205. package/src/oauth/chatgpt.ts +8 -72
  206. package/src/oauth/dsh.ts +203 -0
  207. package/src/oauth/generic-account-failover.ts +40 -11
  208. package/src/oauth/generic-session-affinity.ts +125 -0
  209. package/src/oauth/index.ts +184 -109
  210. package/src/oauth/local-token-detect.ts +21 -1
  211. package/src/oauth/login-cli-errors.ts +44 -0
  212. package/src/oauth/login-cli.ts +69 -7
  213. package/src/oauth/login-flow-state.ts +371 -23
  214. package/src/oauth/meta-muse-errors.ts +41 -0
  215. package/src/oauth/meta-muse-keychain.ts +52 -0
  216. package/src/oauth/meta-muse.ts +51 -84
  217. package/src/oauth/model-discovery-auth.ts +3 -0
  218. package/src/oauth/pool-settings-capability.ts +4 -2
  219. package/src/oauth/refresh-flight-retirement.ts +48 -0
  220. package/src/oauth/session-affinity-key.ts +23 -0
  221. package/src/oauth/store-anthropic-instance.ts +9 -2
  222. package/src/oauth/store.ts +12 -5
  223. package/src/protocols/encoders/adapter-events.ts +6 -5
  224. package/src/protocols/encoders/messages.ts +9 -6
  225. package/src/protocols/provider-summary.ts +2 -1
  226. package/src/providers/api-key-cooldowns.ts +49 -0
  227. package/src/providers/api-key-resolve.ts +11 -6
  228. package/src/providers/api-key-response-ownership.ts +57 -0
  229. package/src/providers/api-key-selection.ts +31 -6
  230. package/src/providers/api-keys.ts +214 -3
  231. package/src/providers/claude-cli-usage.ts +96 -0
  232. package/src/providers/dsh-account-destination.ts +31 -0
  233. package/src/providers/dsh-account-models.ts +41 -0
  234. package/src/providers/gcp-credential-cleanup.ts +59 -0
  235. package/src/providers/key-failover.ts +54 -50
  236. package/src/providers/key-store.ts +7 -2
  237. package/src/providers/model-rename-fields.ts +3 -0
  238. package/src/providers/model-rename-migration.ts +2 -2
  239. package/src/providers/openai-tiers-destination.ts +11 -2
  240. package/src/providers/openai-tiers.ts +5 -12
  241. package/src/providers/opencode-go-transport.ts +3 -1
  242. package/src/providers/opencode-zen-rate-limit.ts +7 -2
  243. package/src/providers/quota/account-cache.ts +6 -5
  244. package/src/providers/quota/account-row.ts +34 -0
  245. package/src/providers/quota/antigravity.ts +28 -7
  246. package/src/providers/quota/dsh-account.ts +70 -0
  247. package/src/providers/quota/vendor-probes-oauth.ts +1 -93
  248. package/src/providers/quota-types.ts +2 -0
  249. package/src/providers/quota.ts +14 -20
  250. package/src/providers/registry/entries-core.ts +37 -8
  251. package/src/providers/registry/types.ts +1 -1
  252. package/src/providers/request-pacing.ts +77 -19
  253. package/src/providers/routed-config.ts +339 -0
  254. package/src/providers/service-tier.ts +2 -1
  255. package/src/quota/window-mapping.ts +5 -2
  256. package/src/responses/anthropic-progress.ts +34 -0
  257. package/src/responses/assistant-replay.ts +35 -0
  258. package/src/responses/compaction-history-framing.ts +65 -0
  259. package/src/responses/compaction.ts +6 -1
  260. package/src/responses/incomplete-reason.ts +31 -0
  261. package/src/responses/parser.ts +50 -5
  262. package/src/responses/reasoning-envelope.ts +4 -0
  263. package/src/responses/reasoning-replay-cache.ts +74 -6
  264. package/src/responses/reasoning-retention.ts +173 -0
  265. package/src/responses/retained-compaction.ts +46 -0
  266. package/src/responses/schema.ts +23 -0
  267. package/src/router.ts +8 -321
  268. package/src/routing/analytics.ts +7 -15
  269. package/src/routing/health.ts +37 -22
  270. package/src/routing/history/indexer.ts +6 -5
  271. package/src/routing/history/ingest.ts +6 -2
  272. package/src/routing/profile.ts +1 -1
  273. package/src/server/admission-model-scope.ts +17 -0
  274. package/src/server/auth-cors.ts +11 -0
  275. package/src/server/chat-completions.ts +7 -1
  276. package/src/server/chat-native-reasoning.ts +123 -0
  277. package/src/server/chat-native-sse.ts +110 -7
  278. package/src/server/chat-native.ts +76 -17
  279. package/src/server/claude-messages.ts +56 -414
  280. package/src/server/effort-policy.ts +1 -1
  281. package/src/server/images.ts +15 -2
  282. package/src/server/index/live-sideband.ts +3 -2
  283. package/src/server/index/model-alias-id.ts +36 -0
  284. package/src/server/index/optional-listeners.ts +2 -0
  285. package/src/server/index/serve-options.ts +20 -6
  286. package/src/server/index/startup-warnings.ts +2 -0
  287. package/src/server/index/websocket-handler.ts +18 -4
  288. package/src/server/index.ts +8 -8
  289. package/src/server/inference/advisor-diagnostics.ts +31 -0
  290. package/src/server/lifecycle.ts +13 -1
  291. package/src/server/management/advisor-routes.ts +15 -4
  292. package/src/server/management/claude-desktop-picker-routes.ts +1 -1
  293. package/src/server/management/combo-routes.ts +176 -50
  294. package/src/server/management/companion-routes.ts +1 -1
  295. package/src/server/management/config-routes.ts +75 -4
  296. package/src/server/management/cursor-integration-routes.ts +1 -1
  297. package/src/server/management/model-routes.ts +11 -6
  298. package/src/server/management/model-rows.ts +14 -0
  299. package/src/server/management/native-integration-routes.ts +2 -7
  300. package/src/server/management/native-reasoning-retention-routes.ts +41 -0
  301. package/src/server/management/oauth-account-routes.ts +87 -14
  302. package/src/server/management/provider-gcp-credential-routes.ts +29 -0
  303. package/src/server/management/provider-key-pacing-routes.ts +31 -0
  304. package/src/server/management/provider-overwrite-carry.ts +7 -0
  305. package/src/server/management/provider-routes.ts +25 -30
  306. package/src/server/management/route-registry.ts +10 -1
  307. package/src/server/management-api.ts +3 -1
  308. package/src/server/messages-native-tool-guard.ts +185 -0
  309. package/src/server/messages-native.ts +117 -24
  310. package/src/server/messages-shared.ts +431 -0
  311. package/src/server/relay.ts +1 -1
  312. package/src/server/request-log-terminal-status.ts +1 -1
  313. package/src/server/request-log.ts +6 -1
  314. package/src/server/responses/account-change-state.ts +8 -2
  315. package/src/server/responses/adapter-continuation.ts +47 -19
  316. package/src/server/responses/adapter-delivery.ts +72 -12
  317. package/src/server/responses/adapter-dispatch.ts +46 -3
  318. package/src/server/responses/advisor-retention.ts +117 -0
  319. package/src/server/responses/advisor-slot.ts +73 -41
  320. package/src/server/responses/anthropic-thinking-recovery.ts +153 -0
  321. package/src/server/responses/codex-ws-wire.ts +15 -8
  322. package/src/server/responses/combo-stream-preflight.ts +3 -2
  323. package/src/server/responses/compact.ts +303 -125
  324. package/src/server/responses/compaction-recovery.ts +20 -10
  325. package/src/server/responses/compaction-retention.ts +61 -0
  326. package/src/server/responses/context-overflow.ts +11 -6
  327. package/src/server/responses/core-auth.ts +6 -0
  328. package/src/server/responses/core-codex-account.ts +11 -1
  329. package/src/server/responses/core-combo-failure.ts +2 -0
  330. package/src/server/responses/core-combo.ts +121 -36
  331. package/src/server/responses/core-errors.ts +3 -3
  332. package/src/server/responses/core-lifetime.ts +8 -4
  333. package/src/server/responses/core-opaque-recovery.ts +35 -0
  334. package/src/server/responses/core-options.ts +7 -0
  335. package/src/server/responses/core-pipeline.ts +141 -0
  336. package/src/server/responses/core-replay.ts +23 -3
  337. package/src/server/responses/core.ts +2 -115
  338. package/src/server/responses/fetch-helpers.ts +73 -19
  339. package/src/server/responses/main-hard-lock-routing.ts +109 -0
  340. package/src/server/responses/native-response-control.ts +4 -2
  341. package/src/server/responses/passthrough-delivery.ts +3 -1
  342. package/src/server/responses/passthrough-dispatch.ts +15 -13
  343. package/src/server/responses/passthrough-execution.ts +3 -2
  344. package/src/server/responses/request-prepare.ts +77 -12
  345. package/src/server/responses/request-sidecar-auth.ts +2 -2
  346. package/src/server/responses/request-transport.ts +104 -27
  347. package/src/server/responses/response-effects.ts +2 -0
  348. package/src/server/responses/response-tool-ownership.ts +21 -0
  349. package/src/server/responses/run-turn-execution.ts +96 -30
  350. package/src/server/responses/sidecar-execution.ts +63 -9
  351. package/src/server/responses/sidecar-send-budget.ts +2 -2
  352. package/src/server/responses/terminal-guard.ts +2 -16
  353. package/src/server/responses-undeclared-tool-guard.ts +1 -1
  354. package/src/server/ws-bridge.ts +2 -0
  355. package/src/service/auth-preflight.ts +71 -0
  356. package/src/service/cli.ts +4 -2
  357. package/src/service/definition.ts +97 -0
  358. package/src/service/guards.ts +1 -61
  359. package/src/service/health-sentinel-linux.ts +112 -0
  360. package/src/service/health-sentinel-main.ts +76 -57
  361. package/src/service/health.ts +5 -89
  362. package/src/service/launchd.ts +27 -7
  363. package/src/service/logging.ts +72 -9
  364. package/src/service/repair.ts +2 -1
  365. package/src/service/state.ts +7 -8
  366. package/src/service/systemd.ts +1 -1
  367. package/src/service/windows-guarded-manager.ts +2 -1
  368. package/src/service/windows-ops.ts +1 -1
  369. package/src/service/windows-scheduler.ts +2 -7
  370. package/src/service/windows-taskxml.ts +2 -2
  371. package/src/service/windows-wscript.ts +14 -0
  372. package/src/service.ts +4 -2
  373. package/src/sidecar/auth.ts +2 -0
  374. package/src/types/config.ts +64 -3
  375. package/src/types/opencode-go.ts +61 -0
  376. package/src/types/opencode-zen.ts +35 -0
  377. package/src/types/provider.ts +32 -6
  378. package/src/types/request.ts +31 -2
  379. package/src/types/wire.ts +31 -37
  380. package/src/types.ts +2 -0
  381. package/src/update/npm-invocation.mjs +12 -2
  382. package/src/usage/cost.ts +48 -12
  383. package/src/usage/expected-prices.ts +3 -0
  384. package/src/usage/ledger-retention.ts +2 -1
  385. package/src/usage/ledger-scanner.ts +6 -5
  386. package/src/usage/log.ts +17 -8
  387. package/src/usage/summary.ts +30 -11
  388. package/src/usage/telemetry-contract.ts +1 -0
  389. package/src/usage/user-cost-overlays.ts +14 -19
  390. package/src/usage/user-cost-validation.ts +38 -0
  391. package/src/vision/eligibility.ts +13 -0
  392. package/src/web-search/anthropic-history.ts +59 -0
  393. package/src/web-search/loop.ts +236 -73
  394. package/src/web-search/progress-stream.ts +15 -2
  395. package/src/web-search/run-turn-loop.ts +9 -1
  396. package/gui/dist/assets/App-BjlttVLy.js +0 -58
  397. package/gui/dist/assets/App-CSJvmjNs.css +0 -1
  398. package/gui/dist/assets/index-BB6uK6tU.css +0 -1
  399. package/gui/dist/assets/index-BMssP4k3.js +0 -94
  400. package/gui/dist/assets/quota-summary-D8M1bc_R.js +0 -1
@@ -1,4 +1,5 @@
1
1
  import type { Server } from "bun";
2
+ import { supportsDesktopPicker } from "../desktop-picker-platform";
2
3
  import type { OcxConfig } from "../../types";
3
4
  import { getConfigDir } from "../../config/paths";
4
5
  import type { DesktopPickerController } from "../desktop-picker";
@@ -12,6 +13,7 @@ import { discardPickerCaKey } from "./picker-ca";
12
13
  import { preparePersistentPickerAuthority } from "./picker-ca-startup";
13
14
  import type { PickerCaStore } from "./picker-ca-store";
14
15
  import type { PickerRouteInput } from "./picker-models";
16
+ import { CLAUDE_INTERCEPT_PORT_OFFSET, claudeInterceptEnabled, claudeInterceptProxyPort, claudePickerProxyPort } from "./port-policy";
15
17
  import { createPickerRuntime, pickerDesired, type CreatePickerRuntimeOptions, type PickerRuntime } from "./picker-runtime";
16
18
  import type { SecurityRunner } from "./picker-trust";
17
19
  import { ensureClaudeInterceptProxyToken, readClaudeInterceptProxyToken } from "./proxy-auth";
@@ -28,7 +30,7 @@ import { buildClaudeInterceptEnv, migrateClaudeInterceptSettings } from "./setti
28
30
  * proxy. Desktop also hands that proxy to the Claude Code processes it spawns, so the choice is per
29
31
  * client: Claude Code (no User-Agent on CONNECT) trusts only the intercept CA and gets the
30
32
  * api.anthropic.com intercept and nothing else; the app itself (a browser User-Agent) trusts only
31
- * the login keychain and never meets the api.anthropic.com intercept, and only its claude.ai
33
+ * the OS root store and never meets the api.anthropic.com intercept, and only its claude.ai
32
34
  * tunnels may be terminated by the picker runtime (src/claude/intercept/picker-runtime.ts).
33
35
  */
34
36
 
@@ -41,25 +43,7 @@ const PICKER_AUTHORITY_ERROR_CODES = new Set([
41
43
  "picker_ca_unsupported_platform",
42
44
  ]);
43
45
 
44
- export const CLAUDE_INTERCEPT_PORT_OFFSET = 100;
45
-
46
- export function claudeInterceptEnabled(config: Pick<OcxConfig, "claudeCode" | "runtimeRole">): boolean {
47
- if (config.runtimeRole === "client") return false;
48
- if (config.claudeCode?.enabled === false) return false;
49
- return config.claudeCode?.intercept?.enabled !== false;
50
- }
51
-
52
- export function claudeInterceptProxyPort(config: Pick<OcxConfig, "claudeCode">, publicPort: number): number {
53
- const configured = config.claudeCode?.intercept?.port;
54
- if (typeof configured === "number" && Number.isInteger(configured) && configured >= 1 && configured <= 65535) return configured;
55
- return publicPort + CLAUDE_INTERCEPT_PORT_OFFSET;
56
- }
57
-
58
- /** Desktop's egress proxy for picker mode: the port after the intercept proxy (before it at 65535). */
59
- export function claudePickerProxyPort(config: Pick<OcxConfig, "claudeCode">, publicPort: number): number {
60
- const interceptPort = claudeInterceptProxyPort(config, publicPort);
61
- return interceptPort < 65535 ? interceptPort + 1 : interceptPort - 1;
62
- }
46
+ export { CLAUDE_INTERCEPT_PORT_OFFSET, claudeInterceptEnabled, claudeInterceptProxyPort, claudePickerProxyPort } from "./port-policy";
63
47
 
64
48
  export type ClaudeInterceptOutcome = { ok: true; state: ClaudeInterceptState } | { ok: false; reason: "disabled" | "client_role" | "ephemeral_port" | "port_in_use" | "stopped" | "failed"; port?: number; message?: string };
65
49
 
@@ -144,7 +128,7 @@ export interface StartClaudeInterceptOptions<T> {
144
128
  createPicker?: (options: CreatePickerRuntimeOptions) => PickerRuntime;
145
129
  /** Test seam: bind real CONNECT handlers on kernel-assigned ports without probe-and-release races. */
146
130
  startProxy?: typeof startConnectProxy;
147
- /** Test seams: the macOS `security` runner and platform for the picker runtime and controller. */
131
+ /** Test seams: the platform trust runner and platform for the picker runtime and controller. */
148
132
  pickerCaStore?: PickerCaStore;
149
133
  pickerSecurity?: SecurityRunner;
150
134
  pickerPlatform?: NodeJS.Platform;
@@ -167,18 +151,6 @@ export async function startClaudeIntercept<T>(options: StartClaudeInterceptOptio
167
151
  const startProxy = options.startProxy ?? startConnectProxy;
168
152
  const authToken = ensureClaudeInterceptProxyToken(configDir);
169
153
  const leaf = issueLocalInterceptLeaf(ca, CLAUDE_INTERCEPT_HOSTS);
170
- // Refresh an env we already own (e.g. a pre-auth proxy URL left by an upgrade) before the
171
- // authenticated proxy takes over the port — a plain `ocx start` after an update would
172
- // otherwise 407 every CONNECT until the next `ocx ensure` or apply. Only `stale` state is
173
- // rewritten, so installs that never applied first-party are untouched.
174
- try {
175
- const proxyPort = claudeInterceptProxyPort(options.config, options.publicPort);
176
- migrateClaudeInterceptSettings(buildClaudeInterceptEnv(proxyPort, claudeInterceptCaCertPath(configDir), authToken));
177
- } catch (error) {
178
- // A skipped rewrite degrades to the pre-migration behaviour and ensure/apply retries it,
179
- // but silence here leaves upgraded clients hitting 407 with no recorded cause.
180
- console.warn(`[claude-intercept] settings migration skipped: ${error instanceof Error ? error.message : String(error)}`);
181
- }
182
154
  const listener = startClaudeInterceptListener<T>({
183
155
  leaf,
184
156
  dispatch: options.dispatch,
@@ -203,15 +175,25 @@ export async function startClaudeIntercept<T>(options: StartClaudeInterceptOptio
203
175
  await listener.stop(true);
204
176
  throw new ClaudeInterceptProxyBindError(error, claudeInterceptProxyPort(options.config, options.publicPort));
205
177
  }
178
+ // Publish only a successfully bound endpoint, using the handle's actual port. A failed
179
+ // fixed-port bind leaves settings untouched; an ephemeral test binder never publishes :0
180
+ // or a probed port another process can steal. Only stale, owned env blocks are rewritten.
181
+ try {
182
+ migrateClaudeInterceptSettings(buildClaudeInterceptEnv(proxy.port, claudeInterceptCaCertPath(configDir), authToken));
183
+ } catch (error) {
184
+ // Ensure/apply retries a skipped migration; upgraded clients otherwise keep hitting 407.
185
+ console.warn(`[claude-intercept] settings migration skipped: ${error instanceof Error ? error.message : String(error)}`);
186
+ }
206
187
  // Widened on purpose: assignments happen in nested awaits the catch below must still see.
207
188
  let picker = null as PickerRuntime | null;
208
189
  let pickerProxy = null as ConnectProxyHandle | null;
209
190
  let controller = null as DesktopPickerController | null;
210
191
  let pickerProxyLive = false;
192
+ let stopped = false;
211
193
  let pickerReason: ClaudeInterceptState["pickerReason"] = null;
212
194
  let pickerFailurePort: number | undefined;
213
195
  try {
214
- if (options.loadPickerRoutes && (options.pickerPlatform ?? process.platform) === "darwin") {
196
+ if (options.loadPickerRoutes && supportsDesktopPicker(options.pickerPlatform ?? process.platform)) {
215
197
  // Remove any legacy plaintext key before inspecting or activating picker state.
216
198
  discardPickerCaKey(configDir);
217
199
  const { inspectDesktopPickerProfile } = await import("../desktop-picker-profile");
@@ -225,11 +207,15 @@ export async function startClaudeIntercept<T>(options: StartClaudeInterceptOptio
225
207
  });
226
208
  const pickerProfileApplied = pickerProfile.kind === "applied";
227
209
  let pickerBlocked = false;
210
+ let pickerWired = false;
228
211
  try {
229
212
  const { observeClaudeDesktopMode, resolveClaudeDesktopMode } = await import("../desktop-first-party");
230
213
  const fresh = options.config;
231
214
  const platform = options.pickerPlatform ?? process.platform;
232
- if (pickerProfileApplied || pickerDesired(fresh, resolveClaudeDesktopMode(fresh, observeClaudeDesktopMode(fresh)), platform)) {
215
+ const pickerRequired = pickerProfileApplied || pickerDesired(fresh, resolveClaudeDesktopMode(fresh, observeClaudeDesktopMode(fresh)), platform);
216
+ // Preserve macOS's dormant relay; Windows defers binding until enable or profile recovery.
217
+ pickerWired = platform !== "win32" || pickerRequired;
218
+ if (pickerRequired) {
233
219
  await preparePersistentPickerAuthority({ configDir, store: options.pickerCaStore, security: options.pickerSecurity, platform });
234
220
  }
235
221
  } catch (error) {
@@ -276,63 +262,76 @@ export async function startClaudeIntercept<T>(options: StartClaudeInterceptOptio
276
262
  const runtime = picker;
277
263
  const interceptPort = listener.port!;
278
264
  const pickerPort = claudePickerProxyPort(options.config, options.publicPort);
279
- try {
280
- pickerProxy = await startProxy(pickerPort, {
281
- interceptPort,
282
- // No authToken: Desktop's egressProxyUrl cannot present proxy credentials, so this
283
- // listener stays an unauthenticated loopback relay until the profile format can carry
284
- // one. The intercept proxy above is the credential-bearing hop.
285
- // No host list here: the choice below depends on which client opened the tunnel.
286
- interceptHosts: [],
287
- selectTunnel: (host, port, request) => {
288
- // Desktop hands its pinned egress proxy to the Claude Code processes it spawns, so their
289
- // api.anthropic.com traffic arrives here too and gets the same intercept as on the Claude
290
- // Code proxy. Those processes trust only the intercept CA, so the picker never terminates
291
- // their claude.ai tunnels; only the app's own (browser) CONNECTs reach the picker.
292
- if (!isBrowserConnect(request)) {
293
- return port === 443 && (CLAUDE_INTERCEPT_HOSTS as readonly string[]).includes(host.toLowerCase())
294
- ? { kind: "intercept", port: interceptPort }
295
- : { kind: "blind" };
296
- }
297
- return runtime.selectTunnel(host, port);
265
+ const bindPickerProxy = async (): Promise<void> => {
266
+ if (stopped || pickerProxyLive) return;
267
+ try {
268
+ pickerProxy = await startProxy(pickerPort, {
269
+ interceptPort,
270
+ // No authToken: Desktop's egressProxyUrl cannot present proxy credentials, so this
271
+ // listener stays an unauthenticated loopback relay until the profile format can carry
272
+ // one. The intercept proxy above is the credential-bearing hop.
273
+ // No host list here: the choice below depends on which client opened the tunnel.
274
+ interceptHosts: [],
275
+ selectTunnel: (host, port, request) => {
276
+ // Desktop hands its pinned egress proxy to the Claude Code processes it spawns, so their
277
+ // api.anthropic.com traffic arrives here too and gets the same intercept as on the Claude
278
+ // Code proxy. Those processes trust only the intercept CA, so the picker never terminates
279
+ // their claude.ai tunnels; only the app's own (browser) CONNECTs reach the picker.
280
+ if (!isBrowserConnect(request)) {
281
+ return port === 443 && (CLAUDE_INTERCEPT_HOSTS as readonly string[]).includes(host.toLowerCase())
282
+ ? { kind: "intercept", port: interceptPort }
283
+ : { kind: "blind" };
284
+ }
285
+ return runtime.selectTunnel(host, port);
286
+ },
287
+ });
288
+ if (stopped) { await pickerProxy.close(); return; }
289
+ pickerProxyLive = true;
290
+ pickerReason = null;
291
+ pickerFailurePort = undefined;
292
+ } catch (error) {
293
+ // Picker mode is optional: a busy port leaves the intercept pair running without it.
294
+ // The selected profile row survives; a dormant enable can retry after the port is free.
295
+ pickerReason = error && typeof error === "object" && "code" in error && error.code === "EADDRINUSE" ? "port_in_use" : "failed";
296
+ if (pickerReason === "port_in_use") pickerFailurePort = pickerPort;
297
+ console.warn(`⚠ Claude Desktop picker proxy could not start: ${error instanceof Error ? error.message : String(error)}`);
298
+ throw error;
299
+ }
300
+ };
301
+ if (pickerWired) {
302
+ try { await bindPickerProxy(); }
303
+ catch { await runtime.stop(); picker = null; }
304
+ }
305
+ if (picker) {
306
+ // Dynamic: the controller reaches desktop-first-party, which imports this module.
307
+ const [{ createDesktopPickerController }, { loadConfig }, persistPreference] = await Promise.all([
308
+ import("../desktop-picker"),
309
+ import("../../config"),
310
+ createPickerPreferenceWriter(options.config),
311
+ ]);
312
+ controller = createDesktopPickerController({
313
+ runtime: picker,
314
+ readConfig: loadConfig,
315
+ persistPreference,
316
+ proxyPort: () => (pickerProxyLive ? pickerProxy?.port ?? null : null),
317
+ ensureProxy: async () => {
318
+ await bindPickerProxy();
319
+ if (!pickerProxyLive) return;
320
+ await runtime.start();
321
+ if (!stopped && activeController === controller) activePicker = runtime;
298
322
  },
323
+ configDir,
324
+ persistentAuthority: { store: options.pickerCaStore },
325
+ ...(options.pickerSecurity ? { security: options.pickerSecurity } : {}),
326
+ ...(options.pickerPlatform ? { platform: options.pickerPlatform } : {}),
299
327
  });
300
- pickerProxyLive = true;
301
- } catch (error) {
302
- // Picker mode is optional: a busy port leaves the intercept pair running without it.
303
- // The selected profile row survives, so the next startup retries the restore once the
304
- // port is free again.
305
- pickerReason = error && typeof error === "object" && "code" in error && error.code === "EADDRINUSE" ? "port_in_use" : "failed";
306
- if (pickerReason === "port_in_use") pickerFailurePort = pickerPort;
307
- console.warn(`⚠ Claude Desktop picker proxy could not start: ${error instanceof Error ? error.message : String(error)}`);
308
- await runtime.stop();
309
- picker = null;
310
- }
311
- }
312
- if (picker) {
313
- // Dynamic: the controller reaches desktop-first-party, which imports this module.
314
- const [{ createDesktopPickerController }, { loadConfig }, persistPreference] = await Promise.all([
315
- import("../desktop-picker"),
316
- import("../../config"),
317
- createPickerPreferenceWriter(options.config),
318
- ]);
319
- const boundProxy = pickerProxy;
320
- controller = createDesktopPickerController({
321
- runtime: picker,
322
- readConfig: loadConfig,
323
- persistPreference,
324
- proxyPort: () => (pickerProxyLive && boundProxy ? boundProxy.port : null),
325
- configDir,
326
- persistentAuthority: { store: options.pickerCaStore },
327
- ...(options.pickerSecurity ? { security: options.pickerSecurity } : {}),
328
- ...(options.pickerPlatform ? { platform: options.pickerPlatform } : {}),
329
- });
330
- await picker.start();
331
- // Restore inspects the retained identity without installing trust. Explicit enable/trust
332
- // owns consent when trust was revoked or legacy migration introduced a new authority.
333
- if (pickerProfileApplied && controller) {
334
- void controller.enable({ persist: false, context: "server", allowTrustPrompt: false })
335
- .catch(error => console.warn(`⚠ Claude Desktop picker restore failed: ${error instanceof Error ? error.message : String(error)}`));
328
+ if (pickerWired) await picker.start();
329
+ // Restore inspects the retained identity without installing trust. Explicit enable/trust
330
+ // owns consent when trust was revoked or legacy migration introduced a new authority.
331
+ if (pickerProfileApplied && controller) {
332
+ void controller.enable({ persist: false, context: "server", allowTrustPrompt: false })
333
+ .catch(error => console.warn(`⚠ Claude Desktop picker restore failed: ${error instanceof Error ? error.message : String(error)}`));
334
+ }
336
335
  }
337
336
  }
338
337
  }
@@ -348,21 +347,25 @@ export async function startClaudeIntercept<T>(options: StartClaudeInterceptOptio
348
347
  throw error;
349
348
  }
350
349
  const state: ClaudeInterceptState = {
351
- pickerReason,
352
- pickerFailurePort,
350
+ get pickerReason() { return pickerReason; },
351
+ get pickerFailurePort() { return pickerFailurePort; },
353
352
  proxyPort: proxy.port,
354
353
  caCertPath: claudeInterceptCaCertPath(configDir),
355
- pickerProxyPort: pickerProxy?.port ?? null,
354
+ get pickerProxyPort() { return pickerProxyLive ? pickerProxy?.port ?? null : null; },
356
355
  };
357
356
  activeState = state;
358
- activePicker = picker;
357
+ activePicker = pickerProxyLive ? picker : null;
359
358
  activeController = controller;
360
359
  const ownPicker = picker;
361
360
  const ownController = controller;
362
361
  return {
363
362
  ...state,
363
+ get pickerProxyPort() { return state.pickerProxyPort; },
364
+ get pickerReason() { return state.pickerReason; },
365
+ get pickerFailurePort() { return state.pickerFailurePort; },
364
366
  listener,
365
367
  stop: async () => {
368
+ stopped = true;
366
369
  if (activeState === state) activeState = null;
367
370
  if (activePicker === ownPicker) activePicker = null;
368
371
  if (activeController === ownController) activeController = null;
@@ -1,3 +1,4 @@
1
+ import { anthropicProgressEnvelope, isAnthropicProgressMirror } from "../responses/anthropic-progress";
1
2
  /**
2
3
  * Claude Code outbound: internal /v1/responses output -> Anthropic Messages API shapes.
3
4
  *
@@ -222,10 +223,28 @@ export function sanitizeWebSearchInput(input: unknown): Rec {
222
223
  /**
223
224
  * Map a Responses `web_search_call` item to its Anthropic pair: the server_tool_use
224
225
  * input (query/queries) and the web_search_tool_result content (hits, or the error
225
- * object when the search failed). Shared by the SSE and JSON translation paths.
226
+ * object when the search failed). An item carrying `anthropic_blocks` came from Anthropic's
227
+ * own server-side call and maps back to those blocks verbatim (`srvtoolu_` id,
228
+ * `encrypted_content`, web_fetch), so the client can replay them on its next turn.
229
+ * Shared by the SSE, JSON and direct translation paths.
226
230
  */
227
- export function webSearchPairFromItem(item: Rec): { id: string; input: Rec; resultContent: unknown; completed: boolean } {
231
+ export function webSearchPairFromItem(item: Rec): {
232
+ id: string; name: string; resultType: string; input: Rec; resultContent: unknown; completed: boolean;
233
+ } {
228
234
  const action = isRec(item.action) ? item.action : {};
235
+ const completed = item.status !== "failed";
236
+ const { use, result } = isRec(item.anthropic_blocks) ? item.anthropic_blocks : {};
237
+ if (isRec(use) && isRec(result) && typeof use.id === "string" && isRec(use.input)
238
+ && (use.name === "web_search" || use.name === "web_fetch")
239
+ && result.tool_use_id === use.id && result.type === `${use.name}_tool_result`) {
240
+ return { id: use.id, name: use.name, resultType: result.type, input: use.input, resultContent: result.content, completed };
241
+ }
242
+ const id = typeof item.id === "string" && item.id.length > 0 ? item.id : `srvtoolu_${uuid()}`;
243
+ // An item emitted before `anthropic_blocks` carried a server web_fetch as `fetch_result`.
244
+ if (item.fetch_result !== undefined) {
245
+ const input = { url: typeof action.url === "string" ? action.url : "" };
246
+ return { id, name: "web_fetch", resultType: "web_fetch_tool_result", input, resultContent: item.fetch_result, completed };
247
+ }
229
248
  const queries = Array.isArray(action.queries)
230
249
  ? action.queries.filter((q): q is string => typeof q === "string" && q.length > 0)
231
250
  : [];
@@ -233,7 +252,6 @@ export function webSearchPairFromItem(item: Rec): { id: string; input: Rec; resu
233
252
  const input = sanitizeWebSearchInput(
234
253
  queries.length > 1 ? { queries } : { query: queries[0] ?? query },
235
254
  );
236
- const completed = item.status !== "failed";
237
255
  let resultContent: unknown;
238
256
  if (completed) {
239
257
  const hits: Rec[] = [];
@@ -248,8 +266,7 @@ export function webSearchPairFromItem(item: Rec): { id: string; input: Rec; resu
248
266
  } else {
249
267
  resultContent = { type: "web_search_tool_result_error", error_code: "unavailable" };
250
268
  }
251
- const id = typeof item.id === "string" && item.id.length > 0 ? item.id : `srvtoolu_${uuid()}`;
252
- return { id, input, resultContent, completed };
269
+ return { id, name: "web_search", resultType: "web_search_tool_result", input, resultContent, completed };
253
270
  }
254
271
 
255
272
  /**
@@ -553,7 +570,13 @@ export function responsesSseToAnthropicSse(
553
570
  terminalDelivered = true;
554
571
  };
555
572
 
573
+ let progressMirrorId: string | undefined;
556
574
  const handleFrame = (eventName: string, data: Rec) => {
575
+ const frameItem = isRec(data.item) ? data.item : null;
576
+ if (progressMirrorId && (data.item_id === progressMirrorId || frameItem?.id === progressMirrorId)) {
577
+ if (eventName === "response.output_item.done") progressMirrorId = undefined;
578
+ return;
579
+ }
557
580
  switch (eventName) {
558
581
  case "response.created":
559
582
  case "response.in_progress": {
@@ -705,7 +728,7 @@ export function responsesSseToAnthropicSse(
705
728
  const toolIndex = blockIndex++;
706
729
  emit("content_block_start", {
707
730
  type: "content_block_start", index: toolIndex,
708
- content_block: { type: "server_tool_use", id: pair.id, name: "web_search" },
731
+ content_block: { type: "server_tool_use", id: pair.id, name: pair.name },
709
732
  });
710
733
  emit("content_block_delta", {
711
734
  type: "content_block_delta", index: toolIndex,
@@ -715,10 +738,10 @@ export function responsesSseToAnthropicSse(
715
738
  const resultIndex = blockIndex++;
716
739
  emit("content_block_start", {
717
740
  type: "content_block_start", index: resultIndex,
718
- content_block: { type: "web_search_tool_result", tool_use_id: pair.id, content: pair.resultContent },
741
+ content_block: { type: pair.resultType, tool_use_id: pair.id, content: pair.resultContent },
719
742
  });
720
743
  emit("content_block_stop", { type: "content_block_stop", index: resultIndex });
721
- if (pair.completed) webSearchRequests++;
744
+ if (pair.completed && pair.name === "web_search") webSearchRequests++;
722
745
  break;
723
746
  }
724
747
  // Close the matching open block (message/reasoning items close implicitly on
@@ -772,6 +795,15 @@ export function responsesSseToAnthropicSse(
772
795
  ensureBlock("thinking");
773
796
  }
774
797
  if (open?.kind === "thinking") {
798
+ if (env?.upd && typeof env.txt === "string") {
799
+ // This plaintext was deliberately surfaced as commentary, unlike hidden reasoning.
800
+ progressMirrorId = env.upd;
801
+ const bytes = Buffer.byteLength(env.txt);
802
+ translatorBudget.chargeRetained(bytes, { kind: "reasoning" });
803
+ translatorBudget.releaseRetained(open.thinkingBufBytes ?? 0, { kind: "reasoning" });
804
+ open.thinkingBuf = env.txt;
805
+ open.thinkingBufBytes = bytes;
806
+ }
775
807
  if (env?.sig) open.reasoningSig = env.sig;
776
808
  if (native) open.reasoningNative = typeof item.id === "string" && item.id.length > 0 ? { enc: native, id: item.id } : { enc: native };
777
809
  closeOpenBlock();
@@ -976,7 +1008,8 @@ export function responsesJsonToAnthropicMessage(json: unknown, model: string, tr
976
1008
  let sawToolUse = false;
977
1009
  let webSearchRequests = 0;
978
1010
 
979
- for (const raw of output) {
1011
+ for (const [index, raw] of output.entries()) {
1012
+ if (index > 0 && isAnthropicProgressMirror(raw, anthropicProgressEnvelope(output[index - 1], translatorBudget))) continue;
980
1013
  if (!isRec(raw)) continue;
981
1014
  switch (raw.type) {
982
1015
  case "message": {
@@ -1010,7 +1043,7 @@ export function responsesJsonToAnthropicMessage(json: unknown, model: string, tr
1010
1043
  const native = nativeEncryptedReasoning(encrypted);
1011
1044
  const tag = native && nativeReasoningTagFor?.(native);
1012
1045
  if (parts.length > 0 || env?.sig || tag) {
1013
- const text = parts.join("\n\n");
1046
+ const text = env?.upd ? env.txt! : parts.join("\n\n");
1014
1047
  const nat = tag ? { enc: native, model, tag, ...(typeof raw.id === "string" && raw.id.length > 0 ? { id: raw.id } : {}) } : undefined;
1015
1048
  content.push({ type: "thinking", thinking: text, signature: env?.sig ?? encodeReasoningEnvelope({ txt: text, ...(nat ? { nat } : {}) }, translatorBudget) });
1016
1049
  }
@@ -1034,9 +1067,9 @@ export function responsesJsonToAnthropicMessage(json: unknown, model: string, tr
1034
1067
  case "web_search_call": {
1035
1068
  // Server-side search: emit the Anthropic pair. Does NOT set sawToolUse.
1036
1069
  const pair = webSearchPairFromItem(raw);
1037
- content.push({ type: "server_tool_use", id: pair.id, name: "web_search", input: pair.input });
1038
- content.push({ type: "web_search_tool_result", tool_use_id: pair.id, content: pair.resultContent });
1039
- if (pair.completed) webSearchRequests++;
1070
+ content.push({ type: "server_tool_use", id: pair.id, name: pair.name, input: pair.input });
1071
+ content.push({ type: pair.resultType, tool_use_id: pair.id, content: pair.resultContent });
1072
+ if (pair.completed && pair.name === "web_search") webSearchRequests++;
1040
1073
  break;
1041
1074
  }
1042
1075
  default:
@@ -306,6 +306,7 @@ export interface ProviderQuotaWindowDto {
306
306
  label: string;
307
307
  percent: number;
308
308
  resetAt?: number;
309
+ modelId?: string;
309
310
  }
310
311
 
311
312
  export interface ProviderQuotaDto extends CodexQuotaDto {
@@ -8,6 +8,7 @@ export const threshold = z.number().int().min(0).max(100);
8
8
  const nullableThreshold = threshold.nullable();
9
9
  const row = z.object({
10
10
  id: accountId, alias: text.optional(), autoSwitchThresholdOverride: nullableThreshold.optional(),
11
+ useRemainingQuota: z.boolean().optional(),
11
12
  quota: z.object({ shortWindowSeconds: z.number().finite().optional(), shortResetAt: z.number().finite().optional(),
12
13
  weeklyResetAt: z.number().finite().optional() }).nullable().optional(),
13
14
  quotaAutoRefresh: z.object({ fiveHourAvailable: z.boolean(), weeklyAvailable: z.boolean(),
@@ -39,6 +40,11 @@ export const poolSchema = z.object({
39
40
  export const thresholdReceipt = z.object({ ok: z.literal(true), id: accountId,
40
41
  autoSwitchThresholdOverride: nullableThreshold, autoSwitchThreshold: threshold });
41
42
  export const creditReceipt = z.object({ ok: z.literal(true), id: accountId, creditsAfterLimit: z.boolean() });
43
+ export const remainingQuotaReceipt = z.object({ ok: z.literal(true), id: accountId,
44
+ useRemainingQuota: z.boolean(), autoSwitchThreshold: threshold,
45
+ mainAccountHardLock: z.object({ enabled: z.boolean(), state: z.enum(["off", "unknown", "ready", "blocked"]),
46
+ thresholds: z.object({ short: threshold, long: threshold }) }).optional(),
47
+ });
42
48
  export const creditAllReceipt = z.object({ ok: z.literal(true), all: z.boolean(),
43
49
  ids: z.array(accountId).refine(ids => new Set(ids).size === ids.length) });
44
50
  const window = z.enum(["five_hour", "seven_day", "seven_day_overage_included"]);
@@ -5,14 +5,15 @@ import { resolveCodexAccountTargetFromRows } from "./account-target";
5
5
  import { runCatalogAction } from "./catalog-command-result";
6
6
  import { serializeManagementJson } from "./json-input";
7
7
  import { creditAllReceipt, creditReceipt, grantsSchema, parseDto, poolSchema, record,
8
- rosterSchema, threshold, thresholdReceipt } from "./account-policy-dto";
8
+ rosterSchema, threshold, thresholdReceipt, remainingQuotaReceipt } from "./account-policy-dto";
9
9
  import { CliUsageError, RuntimeApiError, printData, runtimeBaseUrl, runtimeRequest, summaryLines,
10
10
  takeFlag, takeOptionWithSyntax, type RuntimeApiDeps } from "./runtime-api";
11
11
 
12
- type Subcommand = "pool" | "auto-switch" | "credits" | "quota-activation" | "anthropic-reset-grants";
12
+ type Subcommand = "pool" | "auto-switch" | "credits" | "quota-activation" | "anthropic-reset-grants" | "use-remaining";
13
13
  const USAGE = "Usage: ocx account pool <provider> [--enabled on|off] [--threshold 0-100] [--strategy NAME] [--sticky 1-100] [--quota-window NAME] [--json]\n"
14
14
  + " ocx account auto-switch openai <status|on|off|inherit|threshold N> --account ID [--json]\n"
15
15
  + " ocx account credits openai <ID on|off|--all on|off> [--json]\n"
16
+ + " ocx account use-remaining openai ID <on|off|status> [--json]\n"
16
17
  + " ocx account quota-activation openai ID --window <fiveHour|weekly> <on|off> [--json]\n"
17
18
  + " ocx account anthropic-reset-grants [ID] [--provider anthropic|anthropic2] [--json]";
18
19
  const READ_ERRORS = { no_account: "No matching Anthropic OAuth account.", auth_failed: "Sign in to this Anthropic account again.",
@@ -53,6 +54,32 @@ function output(data: Record<string, unknown>, json: boolean, note?: string): nu
53
54
  }
54
55
  function requireEqual(actual: unknown, expected: unknown): void { if (actual !== expected) throw new Error("Unverified account policy result"); }
55
56
 
57
+ async function useRemaining(args: string[], json: boolean, deps: RuntimeApiDeps): Promise<number> {
58
+ if (args.shift() !== "openai") usage();
59
+ const requested = selector(args.shift()), action = args.shift();
60
+ if (!["on", "off", "status"].includes(action ?? "")) usage();
61
+ done(args);
62
+ const pinned = { ...deps, baseUrl: await runtimeBaseUrl(deps) };
63
+ const { id } = await target(pinned, requested);
64
+ const path = "/api/codex-auth/accounts/use-remaining";
65
+ const result = parseDto(remainingQuotaReceipt, await request(
66
+ action === "status" ? path + "?id=" + encodeURIComponent(id) : path,
67
+ pinned, action === "status" ? undefined : { id, useRemainingQuota: action === "on" },
68
+ ));
69
+ requireEqual(result.id, id);
70
+ if (action !== "status") requireEqual(result.useRemainingQuota, action === "on");
71
+ if (result.useRemainingQuota) {
72
+ requireEqual(result.autoSwitchThreshold, 0);
73
+ if (id === MAIN_CODEX_ACCOUNT_ID) {
74
+ requireEqual(result.mainAccountHardLock?.thresholds.short, 100);
75
+ requireEqual(result.mainAccountHardLock?.thresholds.long, 100);
76
+ }
77
+ }
78
+ return output(result, json, result.useRemainingQuota
79
+ ? "Use remaining included quota: on. Paid-credit permission is unchanged."
80
+ : "Use remaining included quota: off. Configured usage thresholds apply.");
81
+ }
82
+
56
83
  async function pool(args: string[], json: boolean, deps: RuntimeApiDeps): Promise<number> {
57
84
  const provider = selector(args.shift()).toLowerCase();
58
85
  if (!isValidProviderName(provider)) usage();
@@ -63,7 +90,8 @@ async function pool(args: string[], json: boolean, deps: RuntimeApiDeps): Promis
63
90
  if (limit !== undefined) patch.autoSwitchThreshold = integer(limit, 0);
64
91
  if (sticky !== undefined) patch.stickyLimit = integer(sticky, 1);
65
92
  if (strategy !== undefined) {
66
- if (!["quota", "round-robin", "fill-first", ...(provider === "openai" ? ["reset-first"] : []),
93
+ // reset-first is a Codex and generic-pool strategy; the Anthropic pool does not run it.
94
+ if (!["quota", "round-robin", "fill-first", ...(isAnthropicInstanceId(provider) ? [] : ["reset-first"]),
67
95
  ...(provider === "kiro" ? ["least-loaded"] : [])].includes(strategy)) usage();
68
96
  patch.strategy = strategy;
69
97
  }
@@ -122,12 +150,12 @@ async function codex(sub: "auto-switch" | "credits" | "quota-activation", args:
122
150
  const override = selected?.autoSwitchThresholdOverride;
123
151
  if (override === undefined) throw new Error("Missing account threshold evidence");
124
152
  const active = record(await request("/api/codex-auth/active", pinned));
125
- const effective = override ?? parseDto(threshold, active.autoSwitchThreshold);
153
+ const effective = selected?.useRemainingQuota ? 0 : override ?? parseDto(threshold, active.autoSwitchThreshold);
126
154
  return output({ ok: true, id, autoSwitchThresholdOverride: override, autoSwitchThreshold: effective }, json);
127
155
  }
128
156
  const result = parseDto(thresholdReceipt, await request("/api/codex-auth/auto-switch", pinned, { id, threshold: value }));
129
157
  requireEqual(result.id, id); requireEqual(result.autoSwitchThresholdOverride, value);
130
- if (value !== null) requireEqual(result.autoSwitchThreshold, value);
158
+ if (value !== null && !rows.find(row => row.id === id)?.useRemainingQuota) requireEqual(result.autoSwitchThreshold, value);
131
159
  return output(result, json, "Account threshold saved.");
132
160
  }
133
161
  if (sub === "credits") {
@@ -157,6 +185,7 @@ export async function handleAccountPolicyCommand(sub: Subcommand, args: string[]
157
185
  const rest = [...args], json = takeFlag(rest, "--json");
158
186
  if (rest.includes("--json")) usage();
159
187
  if (sub === "pool") return pool(rest, json, deps);
188
+ if (sub === "use-remaining") return useRemaining(rest, json, deps);
160
189
  if (sub !== "anthropic-reset-grants") return codex(sub, rest, json, deps);
161
190
  const selectedProvider = option(rest, "--provider");
162
191
  const provider = selectedProvider ?? "anthropic";
@@ -0,0 +1,44 @@
1
+ import { apiError, apiJson, proxyUnreachable, resolveBaseUrl, type AccountDeps } from "./account-api";
2
+
3
+ /** This operation follows the selected runtime; never read a local account/config roster. */
4
+ export async function cmdPreserveSignedIn(args: string[], deps: AccountDeps): Promise<number> {
5
+ const provider = args.shift();
6
+ let mode = "status";
7
+ let hasMode = false;
8
+ let json = false;
9
+ let applyToBound = false;
10
+ for (const arg of args) {
11
+ if (arg === "--json" && !json) json = true;
12
+ else if (arg === "--apply-to-bound" && !applyToBound) applyToBound = true;
13
+ else if (["on", "off", "status"].includes(arg) && !hasMode) { mode = arg; hasMode = true; }
14
+ else return usage();
15
+ }
16
+ if (provider !== "openai" || (applyToBound && mode !== "on")) return usage();
17
+ const baseUrl = await resolveBaseUrl(deps);
18
+ if (!baseUrl) return proxyUnreachable();
19
+ const result = await apiJson(deps, baseUrl, mode === "status" ? "GET" : "PUT", "/api/codex-auth/preserve-signed-in",
20
+ mode === "status" ? undefined : { enabled: mode === "on", applyToBound });
21
+ if (result.status === 0) return proxyUnreachable(result.transportError);
22
+ if (result.status !== 200) return apiError(result.json, "Signed-in quota policy unavailable", result.status);
23
+ if (typeof result.json.enabled !== "boolean" || !Array.isArray(result.json.affectedAccountIds)
24
+ || !result.json.affectedAccountIds.every(id => typeof id === "string")
25
+ || (result.json.nextAccountId !== null && typeof result.json.nextAccountId !== "string")) {
26
+ return apiError({}, "Invalid signed-in quota policy response", 502);
27
+ }
28
+ if (json) console.log(JSON.stringify(result.json, null, 2));
29
+ else {
30
+ console.log(`Preserve signed-in quota: ${result.json.enabled ? "on" : "off"}`);
31
+ console.log(`Affected pool entries: ${result.json.affectedAccountIds.join(", ")}`);
32
+ console.log(`Next unbound choice: ${result.json.nextAccountId ?? "none"}`);
33
+ console.log(`Bindings: ${result.json.bindings}; manual pin: ${result.json.pinnedAccountId ?? "none"}`);
34
+ if (result.json.priorityFailbackEnabled === true) console.log("Existing priority failback can move bound work when its quota and freshness conditions hold.");
35
+ console.log("Fallback can consume signed-in quota. Exact-account models override this preference.");
36
+ console.log("Use on --apply-to-bound to release affected bindings and pins for the next request.");
37
+ }
38
+ return 0;
39
+ }
40
+
41
+ function usage(): number {
42
+ console.error("Usage: ocx account preserve-signed-in openai [on|off|status] [--apply-to-bound] [--json]");
43
+ return 1;
44
+ }
@@ -54,6 +54,7 @@ const ACCOUNT_USAGE = `Usage:
54
54
  ocx account auto-switch <provider> <on|off|status|threshold <0-100>> [--json]
55
55
  ocx account auto-switch <anthropic|anthropic2> <on|off|status|inherit|threshold <0-100>> --account <id> [--json]
56
56
  ocx account alias <provider> <account-or-key-id|alias> <display-name|-> [--json]
57
+ ocx account preserve-signed-in openai [on|off|status] [--apply-to-bound] [--json]
57
58
  ocx account priority <provider> <account-id|alias|main> [<-100..100|first|earlier|normal|later|last|reset>] [--json]
58
59
  ocx account pause <provider> <account-id|alias|main> [--json]
59
60
  ocx account resume <provider> <account-id|alias|main> [--json]
@@ -69,6 +70,7 @@ const ACCOUNT_USAGE = `Usage:
69
70
  ocx account login <provider> [--id <account-id>] [--reauth] [--open-browser on|off] [--add-account on|off] [--code -] [--no-wait] [--json]
70
71
  ocx account pool <provider> [--enabled on|off] [--threshold N] [--strategy NAME] [--sticky N] [--quota-window W] [--json]
71
72
  ocx account credits openai <ID on|off|--all on|off> [--json]
73
+ ocx account use-remaining openai <ID|main> <on|off|status> [--json]
72
74
  ocx account quota-activation openai ID --window fiveHour|weekly <on|off> [--json]
73
75
  ocx account anthropic-reset-grants [ID] [--provider anthropic|anthropic2] [--json]
74
76
  ocx account code <provider> [--flow <flow-id>] [--json] (reads the code from stdin)
@@ -459,11 +461,11 @@ async function cmdClear(rest: string[], deps: AccountDeps): Promise<number> {
459
461
  export async function cmdAccount(args: string[], deps: AccountDeps = {}): Promise<number> {
460
462
  const [sub, ...rest] = args;
461
463
  try {
462
- if (["pool", "credits", "quota-activation", "anthropic-reset-grants"].includes(sub ?? "")
464
+ if (["pool", "credits", "quota-activation", "anthropic-reset-grants", "use-remaining"].includes(sub ?? "")
463
465
  || (sub === "auto-switch" && rest[0]?.trim().toLowerCase() === "openai"
464
466
  && rest.some(arg => arg === "--account" || arg.startsWith("--account=")))) {
465
467
  const { handleAccountPolicyCommand } = await import("./account-policy");
466
- return handleAccountPolicyCommand(sub as "pool" | "auto-switch" | "credits" | "quota-activation" | "anthropic-reset-grants", rest, deps);
468
+ return handleAccountPolicyCommand(sub as "pool" | "auto-switch" | "credits" | "quota-activation" | "anthropic-reset-grants" | "use-remaining", rest, deps);
467
469
  }
468
470
  if (sub === "list") return await cmdList(rest, deps);
469
471
  if (sub === "history") {
@@ -476,6 +478,10 @@ export async function cmdAccount(args: string[], deps: AccountDeps = {}): Promis
476
478
  if (sub === "refresh") return await cmdRefresh(rest, deps);
477
479
  if (sub === "auto-switch") return await cmdAutoSwitch(rest, deps);
478
480
  if (sub === "alias" || sub === "rename") return await cmdAlias(rest, deps);
481
+ if (sub === "preserve-signed-in") {
482
+ const { cmdPreserveSignedIn } = await import("./account-preservation");
483
+ return cmdPreserveSignedIn(rest, deps);
484
+ }
479
485
  if (sub === "priority") return await cmdPriority(rest, deps);
480
486
  // #2702: the server routes existed and only the CLI caller was missing, so these were
481
487
  // dashboard-only capabilities.