@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
package/src/grok/sync.ts CHANGED
@@ -18,7 +18,7 @@ export interface GrokSyncDeps {
18
18
  }
19
19
 
20
20
  async function defaultFetchAllModels(config: OcxConfig): Promise<CatalogModel[]> {
21
- const { fetchAllModels } = await import("../server/management-api");
21
+ const { fetchAllModels } = await import("../server/management/shared");
22
22
  return fetchAllModels(config);
23
23
  }
24
24
 
@@ -1,3 +1,5 @@
1
+ import type { createAnthropicThinkingRecovery } from "../server/responses/anthropic-thinking-recovery";
2
+ import { bindAnthropicRejectionResponse } from "../adapters/anthropic-rejection-memo";
1
3
  /**
2
4
  * Media bridge agentic loop — supports both image and video generation sidecars.
3
5
  *
@@ -20,9 +22,10 @@ import { isAnthropicInstanceId } from "../providers/anthropic-instance-id";
20
22
  import { cloneProviderOpaqueToolCallMetadata } from "../responses/provider-opaque-metadata";
21
23
  import type { AttemptRecoveryKind } from "../usage/log";
22
24
  import { bridgeToResponsesSSE } from "../bridge";
25
+ import { isTerminalRefusalIncomplete } from "../bridge/terminal-refusal";
23
26
  import { clearableDeadline, idleDeadline } from "../lib/abort";
24
27
  import { readBoundedResponseBody } from "../lib/bounded-body";
25
- import { applyUpstreamRecoveryInit, fetchWithResetRetry, prepareSameTarget429Wait } from "../lib/upstream-retry";
28
+ import { applyUpstreamRecoveryInit, fetchWithResetRetry, prepareSameTarget429Wait, isNonReplayableResponse, SendBudgetExhaustedError } from "../lib/upstream-retry";
26
29
  import { rateLimitRetryDelayMs } from "../providers/key-failover";
27
30
  import { releaseProviderRequestSlot, sendTrackingRequestSlot, type ProviderRequestSlot } from "../providers/request-pacing";
28
31
  import {
@@ -32,13 +35,14 @@ import {
32
35
  TRANSLATOR_MAX_TURN_BYTES,
33
36
  TranslatorBudgetExceededError,
34
37
  } from "../lib/translator-budget";
35
- import { parseStreamWithProgress, RoutedModelInactivityError, WebSearchStreamProtocolError } from "../web-search/progress-stream";
38
+ import { parseStreamWithProgress, RoutedModelInactivityError, WebSearchStreamProtocolError, WebSearchAdapterEventError } from "../web-search/progress-stream";
36
39
  import { fulfillImageCall } from "./fulfill";
37
40
  import { parseVideoCallArgs, pollVideoWithHeartbeats, buildVideoResult, createVideoBudget } from "./fulfill-video";
38
41
  import { submitVideoJob } from "./xai-video-client";
39
42
  import { downloadVideoToArtifact, createImageBudget, pruneArtifacts } from "./artifacts";
40
43
  import { IMAGE_GEN_TOOL_NAME, VIDEO_GEN_TOOL_NAME } from "./synthetic-tool";
41
44
  import type { ImageBridgePlan, VideoBridgePlan } from "./types";
45
+ import { guardEmptyToolCatalogStream, type ClientToolCatalogRestriction } from "../lib/empty-tool-catalog";
42
46
 
43
47
  const SSE_HEADERS = {
44
48
  "Content-Type": "text/event-stream",
@@ -241,7 +245,11 @@ function extractIterationThinking(events: AdapterEvent[]): OcxThinkingContent[]
241
245
  };
242
246
 
243
247
  for (const e of events) {
244
- if (e.type === "thinking_delta") {
248
+ if (e.type === "text_delta" && e.anthropicThinkingSignature) {
249
+ flushVisible();
250
+ flushRaw();
251
+ parts.push({ type: "thinking", thinking: e.text, signature: e.anthropicThinkingSignature });
252
+ } else if (e.type === "thinking_delta") {
245
253
  flushRaw();
246
254
  thinking += e.thinking;
247
255
  } else if (e.type === "reasoning_raw_delta") {
@@ -264,17 +272,18 @@ function extractIterationThinking(events: AdapterEvent[]): OcxThinkingContent[]
264
272
  return parts;
265
273
  }
266
274
 
267
- function jsonError(status: number, message: string): Response {
268
- return new Response(JSON.stringify({ error: { message, type: "upstream_error", code: null } }), {
275
+ function jsonError(status: number, message: string, event?: Extract<AdapterEvent, { type: "error" }>): Response {
276
+ return new Response(JSON.stringify({ error: { message, type: event?.errorType ?? "upstream_error", code: event?.code ?? null,
277
+ ...(event?.retryable === false ? { retryable: false } : {}) } }), {
269
278
  status,
270
- headers: { "Content-Type": "application/json" },
279
+ headers: { "Content-Type": "application/json", ...(event?.retryable === false ? { "x-should-retry": "false" } : {}) },
271
280
  });
272
281
  }
273
282
 
274
283
  /** Hard provider/parse failure inside an iteration. The eager first iteration converts it to a
275
284
  * non-2xx jsonError; later (already-streaming) iterations surface it as an in-stream error event. */
276
285
  class LoopError extends Error {
277
- constructor(readonly status: number, message: string) {
286
+ constructor(readonly status: number, message: string, readonly event?: Extract<AdapterEvent, { type: "error" }>) {
278
287
  super(message);
279
288
  this.name = "LoopError";
280
289
  }
@@ -285,7 +294,9 @@ class LoopError extends Error {
285
294
  * metadata, and the optional image/video bridge plans.
286
295
  */
287
296
  export interface ImageBridgeDeps {
297
+ thinkingRecovery?: ReturnType<typeof createAnthropicThinkingRecovery>;
288
298
  parsed: OcxParsedRequest;
299
+ clientToolCatalogRestriction?: ClientToolCatalogRestriction;
289
300
  adapter: ProviderAdapter;
290
301
  incomingMeta: IncomingMeta;
291
302
  plan?: ImageBridgePlan;
@@ -314,11 +325,17 @@ export interface ImageBridgeDeps {
314
325
  /** Provider-specific fetch (e.g. xAI transport wrapper). Falls back to global fetch. */
315
326
  fetchImpl?: typeof globalThis.fetch;
316
327
  /** Bind physical dispatch to this iteration's built request; pacing remains owned by the loop. */
317
- fetchForRequest?: (request: AdapterRequest, parsed: OcxParsedRequest, adapter: ProviderAdapter) => typeof globalThis.fetch;
328
+ fetchForRequest?: (request: AdapterRequest, parsed: OcxParsedRequest, adapter: ProviderAdapter, recovery?: AttemptRecoveryKind) => typeof globalThis.fetch;
318
329
  /** Reserve the routed provider's next request-start slot before each adapter dispatch. */
319
330
  waitForRequestSlot?: (signal?: AbortSignal) => Promise<ProviderRequestSlot | void>;
320
331
  /** Raw adapter usage at the terminal event, pre wire-normalization (see bridgeToResponsesSSE onUsage). */
321
332
  onUsage?: (usage: OcxUsage | undefined) => void;
333
+ /**
334
+ * After an `on429` rotation, offered the usage of the hidden iterations the left credential
335
+ * served. Returning true means it was billed to that credential's attempt, so `onUsage` will not
336
+ * report it again; the client terminal still includes it.
337
+ */
338
+ onRotatedUsage?: (usage: OcxUsage) => boolean;
322
339
  /**
323
340
  * Optional 429 failover for the routed (non-xAI) model. Return a rebuilt adapter for the
324
341
  * rotated credential, or null when the pool is exhausted. Async hooks support OAuth refresh;
@@ -353,6 +370,8 @@ export interface ImageBridgeDeps {
353
370
  onCompletedResponse?: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) => void;
354
371
  /** WebSocket Responses path only — leave response id empty for protocol compatibility. */
355
372
  forceEmptyResponseId?: boolean;
373
+ /** Responses inbound only: deliver a non-retryable upstream refusal as `response.failed`. */
374
+ terminalRefusalAsFailed?: boolean;
356
375
  }
357
376
 
358
377
  /**
@@ -377,6 +396,10 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
377
396
  let paidImageCalls = 0;
378
397
  let paidVideoCalls = 0;
379
398
  let hiddenUsage: OcxUsage | undefined;
399
+ // Hidden usage already billed through onRotatedUsage: still on the client terminal, not in
400
+ // ledgerUsage, which is what onUsage reports once a rotation has moved some.
401
+ let rotatedUsage: OcxUsage | undefined;
402
+ let ledgerUsage: OcxUsage | undefined;
380
403
  let activeProducers = 0;
381
404
 
382
405
  const addUsage = (a: OcxUsage | undefined, b: OcxUsage | undefined): OcxUsage | undefined => {
@@ -445,6 +468,8 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
445
468
  interface IterationResponse {
446
469
  response: Response;
447
470
  responseAdapter: ProviderAdapter;
471
+ parsed?: OcxParsedRequest;
472
+ retryThinking?: () => Promise<IterationResponse>;
448
473
  /** runTurn events are already bounded; consume them without replaying or charging twice. */
449
474
  collectedEvents?: AdapterEvent[];
450
475
  }
@@ -569,6 +594,7 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
569
594
  // runTurn adapters signal errors via {type:"error"} events, not HTTP status codes.
570
595
  const errorEvent = events.find(e => e.type === "error");
571
596
  if (errorEvent && errorEvent.type === "error") {
597
+ hiddenUsage = addUsage(hiddenUsage, errorEvent.usage);
572
598
  if (errorEvent.code === "translation_buffer_limit") {
573
599
  throw new TranslatorBudgetExceededError("retained_collectors", TRANSLATOR_MAX_TURN_BYTES);
574
600
  }
@@ -614,7 +640,7 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
614
640
  cachedRequest = request;
615
641
  cachedAdapter = requestAdapter;
616
642
  }
617
- const requestFetch = deps.fetchForRequest?.(request, iterParsed, requestAdapter) ?? fetchImpl;
643
+ const requestFetch = deps.fetchForRequest?.(request, iterParsed, requestAdapter, recovery) ?? fetchImpl;
618
644
  let response: Response;
619
645
  try {
620
646
  if (requestAdapter.fetchResponse) {
@@ -665,7 +691,8 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
665
691
  } finally {
666
692
  request.releaseBodyObservation?.();
667
693
  }
668
- return { response, responseAdapter: requestAdapter };
694
+ bindAnthropicRejectionResponse(request, response);
695
+ return { response, responseAdapter: requestAdapter, parsed: iterParsed };
669
696
  };
670
697
 
671
698
  let prepared = await fetchOnce(adapter);
@@ -709,15 +736,28 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
709
736
  const rotated = await deps.on429(prepared.response.headers.get("retry-after"), prepared.response.headers,
710
737
  iterParsed, prepared.response);
711
738
  if (!rotated) break;
739
+ if (hiddenUsage && deps.onRotatedUsage?.(hiddenUsage)) {
740
+ rotatedUsage = addUsage(rotatedUsage, hiddenUsage);
741
+ hiddenUsage = undefined;
742
+ }
712
743
  try { void prepared.response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
713
744
  adapter = rotated.adapter;
714
745
  yield { type: "heartbeat" };
715
746
  prepared = await fetchOnce(adapter, rotated.recoveryKind);
716
747
  }
717
748
 
749
+ if (await deps.thinkingRecovery?.prepareHttp(prepared.response, prepared.responseAdapter.name, iterParsed)) {
750
+ try { void prepared.response.body?.cancel().catch(() => {}); } catch { /* closed */ }
751
+ cachedRequest = undefined;
752
+ prepared = await fetchOnce(adapter, "opaque-blob-rejection");
753
+ }
718
754
  // Final headers have arrived. Clear only the deadline timer before ANY body read.
719
755
  headerDeadline.clear();
720
756
  if (!prepared.response.ok) {
757
+ if (deps.thinkingRecovery && prepared.responseAdapter.name === "anthropic") {
758
+ const event = await deps.thinkingRecovery.projectHttpFailure(prepared.response, signal);
759
+ throw new LoopError(event.status ?? prepared.response.status, event.message, event);
760
+ }
721
761
  let body: Awaited<ReturnType<typeof readBoundedResponseBody>>;
722
762
  try {
723
763
  body = await readBoundedResponseBody(prepared.response, { signal });
@@ -738,9 +778,21 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
738
778
  const suffix = formatted ? `: ${formatted.slice(0, 400)}` : "";
739
779
  throw new LoopError(prepared.response.status, `Provider error ${prepared.response.status}${suffix}`);
740
780
  }
741
- return prepared;
781
+ return { ...prepared, retryThinking: async () => {
782
+ cachedRequest = undefined;
783
+ headerDeadline = clearableDeadline(connectTimeoutMs, signal);
784
+ try { return await fetchOnce(adapter, "opaque-blob-rejection"); }
785
+ catch (error) {
786
+ if (signal.aborted) throw new LoopError(499, "client closed request during image-bridge");
787
+ if (headerDeadline.didExpire()) throw new LoopError(504,
788
+ `Provider response-header timeout after ${connectTimeoutMs}ms during image-bridge`);
789
+ throw error;
790
+ } finally { headerDeadline.clear(); deps.onIterationEnd?.(); }
791
+ } };
742
792
  } catch (error) {
743
793
  if (isTranslatorBudgetExceededError(error)) throw error;
794
+ if (error instanceof SendBudgetExhaustedError) throw new LoopError(429, error.message,
795
+ { type: "error", status: 429, errorType: "rate_limit_error", code: error.code, message: error.message, retryable: false });
744
796
  if (headerDeadline.didExpire()) {
745
797
  throw new LoopError(504, `Provider response-header timeout after ${connectTimeoutMs}ms during image-bridge`);
746
798
  }
@@ -767,12 +819,31 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
767
819
  const iterationBudget = prepared.collectedEvents ? undefined : createIterationEventBudget();
768
820
  try {
769
821
  if (iterationBudget) {
770
- const parse = prepared.responseAdapter.parseStream.bind(prepared.responseAdapter);
771
- for await (const event of parseStreamWithProgress(prepared.response, parse, {
772
- signal,
773
- inactivityTimeoutMs: stallTimeoutMs,
774
- translatorBudget,
775
- })) {
822
+ const parse = async function* (current: IterationResponse): AsyncGenerator<AdapterEvent> {
823
+ if (!current.response.ok && deps.thinkingRecovery) {
824
+ yield await deps.thinkingRecovery.projectHttpFailure(current.response, signal);
825
+ return;
826
+ }
827
+ try {
828
+ yield* parseStreamWithProgress(current.response, current.responseAdapter.parseStream.bind(current.responseAdapter), {
829
+ signal, inactivityTimeoutMs: stallTimeoutMs, translatorBudget,
830
+ preserveAdapterError: deps.thinkingRecovery !== undefined,
831
+ onErrorUsage: usage => { hiddenUsage = addUsage(hiddenUsage, usage); },
832
+ });
833
+ } catch (error) {
834
+ if (!(error instanceof WebSearchAdapterEventError)) throw error;
835
+ if (!error.replaySafe) yield { type: "heartbeat", replayUnsafe: true };
836
+ yield isNonReplayableResponse(current.response)
837
+ ? { ...error.event, message: "Provider stream error: upstream diagnostic withheld for a non-replayable failure", retryable: false }
838
+ : error.event;
839
+ }
840
+ };
841
+ const source = deps.thinkingRecovery && prepared.retryThinking
842
+ ? deps.thinkingRecovery.wrap(parse(prepared), async function* () {
843
+ yield* parse(await prepared.retryThinking!());
844
+ }, () => prepared.responseAdapter.name, !isNonReplayableResponse(prepared.response), prepared.response, prepared.parsed)
845
+ : parse(prepared);
846
+ for await (const event of source) {
776
847
  if (event.type === "heartbeat") yield event;
777
848
  else {
778
849
  iterationBudget.retain(event);
@@ -786,6 +857,9 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
786
857
  throw error;
787
858
  }
788
859
  if (signal.aborted) throw new LoopError(499, "client closed request during image-bridge");
860
+ if (error instanceof LoopError) throw error;
861
+ if (error instanceof SendBudgetExhaustedError) throw new LoopError(429, error.message,
862
+ { type: "error", status: 429, errorType: "rate_limit_error", code: error.code, message: error.message, retryable: false });
789
863
  if (error instanceof RoutedModelInactivityError) throw new LoopError(504, error.message);
790
864
  if (error instanceof WebSearchStreamProtocolError) throw new LoopError(502, error.message);
791
865
  throw new LoopError(502, `Provider stream error: ${error instanceof Error ? error.message : String(error)}`);
@@ -803,8 +877,9 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
803
877
  if (terminal.code === "translation_buffer_limit") {
804
878
  throw new TranslatorBudgetExceededError("retained_collectors", TRANSLATOR_MAX_TURN_BYTES);
805
879
  }
806
- throw new LoopError(502, terminal.message);
880
+ throw new LoopError(terminal.status ?? 502, terminal.message, terminal);
807
881
  }
882
+ deps.thinkingRecovery?.observeIteration(events);
808
883
  return scanEventsForImageCall(events, mediaToolNames);
809
884
  };
810
885
 
@@ -819,7 +894,9 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
819
894
  firstPrepared = await prepareIterationDrained(maxRounds <= 0);
820
895
  } catch (e) {
821
896
  if (abortSignal) abortSignal.removeEventListener("abort", linkAbort);
822
- if (e instanceof LoopError) return jsonError(e.status, e.message);
897
+ if (e instanceof LoopError) return jsonError(e.status, e.message, e.event);
898
+ if (e instanceof SendBudgetExhaustedError) return jsonError(429, e.message,
899
+ { type: "error", status: 429, errorType: "rate_limit_error", code: e.code, message: e.message, retryable: false });
823
900
  throw e;
824
901
  }
825
902
  }
@@ -863,13 +940,17 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
863
940
  // Loop (fulfill + re-ask) ONLY when the model's actionable output is purely image_gen. A
864
941
  // real tool call means this turn is terminal for Codex — finalize so those calls reach
865
942
  // Codex. forceFinal also finalizes.
866
- const shouldLoop = split.calls.length > 0 && !split.hasRealToolCall && !forceFinal;
943
+ // A refused iteration also finalizes: fulfilling its media call would re-ask the model
944
+ // with the refused context (#7148).
945
+ const shouldLoop = split.calls.length > 0 && !split.hasRealToolCall && !forceFinal
946
+ && !split.passthrough.some(isTerminalRefusalIncomplete);
867
947
  if (!shouldLoop) {
868
- if (hiddenUsage) {
948
+ if (hiddenUsage || rotatedUsage) {
869
949
  for (let i = split.passthrough.length - 1; i >= 0; i--) {
870
950
  const e = split.passthrough[i];
871
951
  if (e?.type === "done" || e?.type === "incomplete") {
872
- split.passthrough[i] = { ...e, usage: addUsage(hiddenUsage, e.usage) };
952
+ ledgerUsage = addUsage(hiddenUsage, e.usage);
953
+ split.passthrough[i] = { ...e, usage: addUsage(rotatedUsage, ledgerUsage) };
873
954
  break;
874
955
  }
875
956
  }
@@ -881,6 +962,10 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
881
962
  // Discarded iteration still contributed tokens — accumulate for the final onUsage.
882
963
  takeUsageFrom(split.passthrough);
883
964
 
965
+ for (const event of split.passthrough) {
966
+ if (event.type === "text_delta" && event.anthropicThinkingSignature) yield event;
967
+ }
968
+
884
969
  // Fulfill each image/video call, then inject ONE assistant turn (thinking once + all tool
885
970
  // calls) so Anthropic extended-thinking continuations stay valid across parallel calls.
886
971
  const iterationThinking = extractIterationThinking(split.passthrough);
@@ -1052,6 +1137,8 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
1052
1137
  });
1053
1138
  }
1054
1139
  } catch (e) {
1140
+ ledgerUsage = hiddenUsage;
1141
+ const usage = addUsage(rotatedUsage, hiddenUsage);
1055
1142
  if (isTranslatorBudgetExceededError(e)) {
1056
1143
  yield {
1057
1144
  type: "error",
@@ -1059,14 +1146,14 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
1059
1146
  errorType: "upstream_error",
1060
1147
  code: e.code,
1061
1148
  message: "upstream translation buffer exceeded the safe limit",
1062
- ...(hiddenUsage ? { usage: hiddenUsage } : {}),
1149
+ ...(usage ? { usage } : {}),
1063
1150
  };
1064
1151
  } else {
1065
1152
  yield {
1066
1153
  type: "error",
1067
1154
  message: e instanceof LoopError ? e.message : (e instanceof Error ? e.message : String(e)),
1068
- ...(e instanceof LoopError ? { status: e.status } : {}),
1069
- ...(hiddenUsage ? { usage: hiddenUsage } : {}),
1155
+ ...(e instanceof LoopError ? { status: e.status, ...(e.event ?? {}) } : {}),
1156
+ ...(usage ? { usage } : {}),
1070
1157
  };
1071
1158
  }
1072
1159
  return;
@@ -1079,13 +1166,15 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
1079
1166
  }
1080
1167
 
1081
1168
  const sse = bridgeToResponsesSSE(
1082
- produce(), parsed._responseModelId ?? parsed.modelId, toolNsMap, freeform, toolSearch, () => {
1169
+ guardEmptyToolCatalogStream(produce(), deps.clientToolCatalogRestriction, translatorBudget), parsed._responseModelId ?? parsed.modelId, toolNsMap, freeform, toolSearch, () => {
1083
1170
  internalAbort.abort("client closed responses stream");
1084
1171
  }, 2_000,
1085
1172
  {
1086
1173
  translatorBudget,
1087
1174
  replayCacheScope: parsed._reasoningReplayScope,
1175
+ ...(deps.clientToolCatalogRestriction ? { ...deps.clientToolCatalogRestriction, enforceDeclaredToolNames: false } : {}),
1088
1176
  ...(deps.forceEmptyResponseId ? { responseId: "" } : {}),
1177
+ ...(deps.terminalRefusalAsFailed ? { terminalRefusalAsFailed: true } : {}),
1089
1178
  hideThinkingSummary: parsed.options.hideThinkingSummary,
1090
1179
  hideRawReasoning: parsed.options.hideRawReasoning,
1091
1180
  stallTimeoutSec: deps.stallTimeoutSec,
@@ -1093,7 +1182,7 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
1093
1182
  ...(deps.onUsage ? {
1094
1183
  // Terminal done/incomplete already includes hiddenUsage (merged above). Do not
1095
1184
  // add it again here or request logs double-count multi-iteration image turns.
1096
- onUsage: (usage: OcxUsage | undefined) => deps.onUsage?.(usage),
1185
+ onUsage: (usage: OcxUsage | undefined) => deps.onUsage?.(rotatedUsage ? ledgerUsage : usage),
1097
1186
  } : {}),
1098
1187
  ...(deps.onCompletedResponse ? { onCompletedResponse: deps.onCompletedResponse } : {}),
1099
1188
  },
@@ -12,7 +12,7 @@ import {
12
12
  /** Refresh only previously connected clients; a refused file never blocks its peers. */
13
13
  export async function refreshOwnedCatalogIntegrations(
14
14
  input: Omit<OwnedIntegrationRefreshInput, "clientId">,
15
- clientIds: readonly IntegrationClientId[] = ["pi", "aside", "raycast", "omo", "commandcode", "droid", "opencode", "kilo", "qoder"],
15
+ clientIds: readonly IntegrationClientId[] = ["pi", "aside", "raycast", "omo", "commandcode", "droid", "opencode", "kilo", "qoder", "gentle-shell"],
16
16
  options: { refreshOnly?: boolean; admit?: () => boolean } = {},
17
17
  ): Promise<OwnedIntegrationRefreshOutcome[]> {
18
18
  // Client files are shared with the live proxy a sibling instance runs beside; their entries
@@ -134,6 +134,7 @@ const CLIENT_MANAGED_PATHS = {
134
134
  qoder: [["providers", OPENCODE_PROVIDER_ID], ["modelConfigs", "customModels", DYNAMIC_SEGMENT]],
135
135
  opencode: [["provider", OPENCODE_PROVIDER_ID], ["providers", OPENCODE_PROVIDER_ID]],
136
136
  pi: [["providers", OPENCODE_PROVIDER_ID]],
137
+ "gentle-shell": [["providers", OPENCODE_PROVIDER_ID]],
137
138
  omp: [["providers", OPENCODE_PROVIDER_ID]],
138
139
  hermes: [["providers", OPENCODE_PROVIDER_ID]],
139
140
  openclaw: [["models", "providers", OPENCODE_PROVIDER_ID]],
@@ -9,6 +9,7 @@
9
9
  * Design of record: devlog/_fin/260802_client_toggle_api/021 §1.
10
10
  */
11
11
  import { homedir } from "node:os";
12
+ import { gentleShellPaths } from "../clients/gentle-shell-path";
12
13
  import { assertDroidSettingsUnambiguous } from "./droid-settings";
13
14
  import { readPath } from "./merge";
14
15
  import type { OwnershipRecord } from "./ownership";
@@ -505,6 +506,13 @@ export const INTEGRATION_CLIENTS: Record<IntegrationClientId, IntegrationClientS
505
506
  return { configPath: droidConfigPath(env, home), detectDir };
506
507
  },
507
508
  },
509
+ "gentle-shell": {
510
+ id: "gentle-shell",
511
+ configPath: (env, home, io) => gentleShellPaths(env, home, io).configPath,
512
+ detectDir: (env, home, io) => gentleShellPaths(env, home, io).detectDir,
513
+ resolvePaths: (env, home, _context, io) => gentleShellPaths(env, home, io),
514
+ unresolvedPathHint: (_env, home = homedir()) => join(home, ".gentle-shell", "agent", "models.json"),
515
+ },
508
516
  };
509
517
 
510
518
  export const INTEGRATION_CLIENT_IDS: readonly IntegrationClientId[] =
@@ -36,6 +36,10 @@ export interface CodexAppServerStateResponse {
36
36
  * The desktop half of a restart. Scalar-only for the same reason as the rest of this
37
37
  * contract: pid lists and a closed-vocabulary reason, never a command line or an OS
38
38
  * error message, both of which routinely embed a path or a username.
39
+ *
40
+ * `reason: "picker_current"` means the shell was deliberately left running because it
41
+ * launched after the current catalog was written, so only the app-servers restarted and
42
+ * the operator's unsaved client state was kept (#4761).
39
43
  */
40
44
  export interface CodexDesktopRestartSummary {
41
45
  attempted: boolean;
@@ -43,8 +47,17 @@ export interface CodexDesktopRestartSummary {
43
47
  surviving: number[];
44
48
  relaunch: "started" | "skipped";
45
49
  reason?: string;
50
+ /**
51
+ * Why the desktop was or was not restarted: `stale` (the shell predates the bound
52
+ * catalog, or this request rebound it) and `unknown` (the evidence was unreadable) both
53
+ * take the full restart. Absent on a proxy older than #4761.
54
+ */
55
+ pickerState?: CodexDesktopPickerState;
46
56
  }
47
57
 
58
+ export type CodexDesktopPickerState = "current" | "stale" | "unknown" | "not_running";
59
+ const DESKTOP_PICKER_STATES: readonly string[] = ["current", "stale", "unknown", "not_running"];
60
+
48
61
  /** POST response. All four arrays are pid lists — never command lines. */
49
62
  export interface CodexRestartResponse {
50
63
  success: boolean;
@@ -130,6 +143,8 @@ export function isCodexRestartResponse(value: unknown): value is CodexRestartRes
130
143
  if (!isPidList(d.stopped) || !isPidList(d.surviving)) return false;
131
144
  if (d.relaunch !== "started" && d.relaunch !== "skipped") return false;
132
145
  if (d.reason !== undefined && typeof d.reason !== "string") return false;
146
+ if (d.pickerState !== undefined
147
+ && (typeof d.pickerState !== "string" || !DESKTOP_PICKER_STATES.includes(d.pickerState))) return false;
133
148
  // A started relaunch cannot have left anything behind: the ladder refuses to
134
149
  // relaunch beside a survivor precisely so a second shell never appears.
135
150
  if (d.relaunch === "started" && (d.surviving as number[]).length > 0) return false;
@@ -1,10 +1,10 @@
1
- import { connect as tcpConnect, isIP, type Socket } from "node:net";
2
- import { checkServerIdentity, connect as tlsConnect, type TLSSocket } from "node:tls";
1
+ import { connect as tcpConnect, type Socket } from "node:net";
2
+ import { connect as tlsConnect, type TLSSocket } from "node:tls";
3
3
  import { desktopProxyFor } from "./desktop-proxy-route";
4
- import { socks5Credentials, socks5Handshake } from "./socks5-handshake";
4
+ import { dialTunnel, type OpaqueProxyRoute } from "./opaque-proxy-tunnel";
5
5
 
6
- // Shares the SOCKS exchange extracted by lcxhh521 in PR #5947 (efdccdbfac3f).
7
- // Lifecycle here additionally bounds the entire dial and handles close/abort/header limits.
6
+ // The shared opaque dialer owns proxy framing; this caller owns fixed-target
7
+ // TLS and bounds the entire connection, including the target TLS handshake.
8
8
  export interface DesktopTunnelOptions {
9
9
  proxy?: string | false;
10
10
  signal?: AbortSignal;
@@ -15,37 +15,6 @@ export interface DesktopTunnelOptions {
15
15
  proxyCa?: string;
16
16
  }
17
17
  const fail = () => new Error("desktop_upstream_connection_failed");
18
- class HandshakeReader {
19
- private buffer: Buffer<ArrayBufferLike> = Buffer.alloc(0);
20
- private failure: Error | null = null;
21
- private waiting: { ready: () => boolean; resolve: () => void; reject: (error: Error) => void } | null = null;
22
- private onData = (chunk: Buffer) => {
23
- if (this.buffer.length + chunk.length > 65536) { this.onFailure(); return; }
24
- this.buffer = Buffer.concat([this.buffer, chunk]);
25
- if (this.waiting?.ready()) { const current = this.waiting; this.waiting = null; current.resolve(); }
26
- };
27
- private onFailure = () => { this.failure = fail(); const current = this.waiting; this.waiting = null; current?.reject(this.failure); };
28
- constructor(private socket: Socket, private signal: AbortSignal) {
29
- socket.on("data", this.onData); socket.once("error", this.onFailure); socket.once("end", this.onFailure); socket.once("close", this.onFailure);
30
- signal.addEventListener("abort", this.onFailure, { once: true }); if (signal.aborted) this.onFailure(); socket.resume();
31
- }
32
- write(bytes: Uint8Array | string): void { if (this.failure) throw this.failure; this.socket.write(bytes); }
33
- private async until(ready: () => boolean): Promise<void> {
34
- if (this.failure) throw this.failure;
35
- if (ready()) return;
36
- if (this.waiting) throw fail();
37
- await new Promise<void>((resolve, reject) => { this.waiting = { ready, resolve, reject }; });
38
- }
39
- private take(length: number): Buffer { const out = this.buffer.subarray(0, length); this.buffer = this.buffer.subarray(length); return out; }
40
- async readExact(length: number): Promise<Buffer> { await this.until(() => this.buffer.length >= length); return this.take(length); }
41
- async readHead(): Promise<string> { await this.until(() => this.buffer.indexOf("\r\n\r\n") >= 0); return this.take(this.buffer.indexOf("\r\n\r\n") + 4).toString("latin1"); }
42
- dispose(): void {
43
- this.socket.pause(); this.socket.removeListener("data", this.onData); this.socket.removeListener("error", this.onFailure);
44
- this.socket.removeListener("end", this.onFailure); this.socket.removeListener("close", this.onFailure);
45
- this.signal.removeEventListener("abort", this.onFailure);
46
- if (this.buffer.length) this.socket.unshift(this.buffer); this.buffer = Buffer.alloc(0); this.onFailure();
47
- }
48
- }
49
18
  function waitConnected(socket: Socket, event: "connect" | "secureConnect", signal: AbortSignal): Promise<void> {
50
19
  return new Promise((resolve, reject) => {
51
20
  const dispose = () => { socket.off(event, done); socket.off("error", failure); socket.off("close", failure); signal.removeEventListener("abort", failure); };
@@ -76,25 +45,21 @@ export async function dialDesktopUpstream(options: DesktopTunnelOptions = {}): P
76
45
  const url = new URL(proxy), protocol = url.protocol;
77
46
  if (!["http:", "https:", "socks5:", "socks5h:"].includes(protocol) || !url.hostname || url.search || url.hash || url.pathname !== "" && url.pathname !== "/") throw fail();
78
47
  const host = url.hostname.replace(/^\[|\]$/g, ""), port = Number(url.port) || (protocol === "https:" ? 443 : protocol.startsWith("socks") ? 1080 : 80);
79
- raw = own(tcpConnect({ host, port })); await waitConnected(raw, "connect", signal);
80
- if (protocol === "https:") {
81
- raw = own(tlsConnect({ socket: raw, servername: isIP(host) ? undefined : host, ca: options.proxyCa,
82
- rejectUnauthorized: true, checkServerIdentity: (_name, certificate) => checkServerIdentity(host, certificate), ALPNProtocols: ["http/1.1"] }));
83
- await waitConnected(raw, "secureConnect", signal);
84
- }
85
- const reader = new HandshakeReader(raw, signal);
86
- try {
87
- if (protocol.startsWith("socks")) await socks5Handshake(reader, target, socks5Credentials(url));
88
- else {
89
- const authority = `${target.host}:${target.port}`;
90
- const lines = [`CONNECT ${authority} HTTP/1.1`, `Host: ${authority}`];
91
- if (url.username || url.password) lines.push(`Proxy-Authorization: Basic ${Buffer.from(`${decodeURIComponent(url.username)}:${decodeURIComponent(url.password)}`).toString("base64")}`);
92
- reader.write(lines.join("\r\n") + "\r\n\r\n");
93
- if (!/^HTTP\/1\.[01] 2\d\d(?: |\r)/.test(await reader.readHead())) throw fail();
94
- }
95
- } finally { reader.dispose(); }
48
+ const credentials = url.username || url.password
49
+ ? { username: decodeURIComponent(url.username), password: decodeURIComponent(url.password) }
50
+ : {};
51
+ const endpoint = { host, port, ...credentials };
52
+ const route: OpaqueProxyRoute = protocol === "https:"
53
+ ? { ...endpoint, kind: "https", proxyCa: options.proxyCa }
54
+ : { ...endpoint, kind: protocol.slice(0, -1) as "http" | "socks5" | "socks5h" };
55
+ const authority = `${target.host.includes(":") ? `[${target.host}]` : target.host}:${target.port}`;
56
+ // The route resolver remains authoritative; a failed proxy never becomes DIRECT.
57
+ raw = own(await dialTunnel(authority, route, { signal, timeoutMs: timeout, maxHeaderBytes: 65536 }));
96
58
  }
97
59
  if (signal.aborted || raw.destroyed) throw fail();
60
+ // This consumer historically closes its writable side after peer EOF. The
61
+ // shared opaque transport preserves half-closes for callers that need them.
62
+ raw.allowHalfOpen = false;
98
63
  const secured = own(tlsConnect({ socket: raw, servername: "chatgpt.com", ca: options.ca, rejectUnauthorized: true, ALPNProtocols: ["http/1.1"] }));
99
64
  // The CONNECT reader paused its stream before handing buffered bytes back. Bun's
100
65
  // TLS-over-TLS path needs the outer TLS stream explicitly resumed for the new handshake.
@@ -0,0 +1,62 @@
1
+ import { normalizeDeclaredToolName, type AdapterEvent } from "../types";
2
+ import { UNDECLARED_TOOL_CALL_ERROR_CODE } from "./errors";
3
+ import { releaseTranslatedEvent, type TranslatorBudget } from "./translator-budget";
4
+ import { releaseCompactionCiphertextLease } from "../responses/compaction";
5
+
6
+ /** Snapshot of current-turn declarations when an explicit empty catalog activates enforcement. */
7
+ export interface ClientToolCatalogRestriction {
8
+ declaredToolNames: ReadonlySet<string>;
9
+ bareCustomToolNames: ReadonlySet<string>;
10
+ }
11
+
12
+ export function emptyToolCatalogRefusal(
13
+ event: AdapterEvent,
14
+ catalog: ClientToolCatalogRestriction | undefined,
15
+ ): Extract<AdapterEvent, { type: "error" }> | undefined {
16
+ if (!catalog) return;
17
+ if (event.type === "tool_call_start") {
18
+ const name = normalizeDeclaredToolName(event.name, catalog.declaredToolNames, undefined, catalog.bareCustomToolNames);
19
+ if (catalog.declaredToolNames.has(name)) return;
20
+ } else if (catalog.declaredToolNames.size !== 0
21
+ || (event.type !== "tool_call_delta" && event.type !== "tool_call_end")) return;
22
+ return {
23
+ type: "error", status: 502, errorType: "upstream_error", code: UNDECLARED_TOOL_CALL_ERROR_CODE,
24
+ message: "routed provider emitted a client tool call outside the current request's explicit tool catalog",
25
+ };
26
+ }
27
+
28
+ /** Runs after hosted tool loops, before client delivery and continuation-state folding. */
29
+ export async function* guardEmptyToolCatalogStream(
30
+ events: AsyncIterable<AdapterEvent>,
31
+ catalog: ClientToolCatalogRestriction | undefined,
32
+ budget: TranslatorBudget,
33
+ ): AsyncGenerator<AdapterEvent> {
34
+ for await (const event of events) {
35
+ const refusal = emptyToolCatalogRefusal(event, catalog);
36
+ if (refusal) {
37
+ releaseTranslatedEvent(event, budget);
38
+ releaseCompactionCiphertextLease(event, budget);
39
+ }
40
+ yield refusal ?? event;
41
+ if (refusal) return;
42
+ }
43
+ }
44
+
45
+ export function guardEmptyToolCatalogEvents(
46
+ events: AdapterEvent[],
47
+ catalog: ClientToolCatalogRestriction | undefined,
48
+ budget: TranslatorBudget,
49
+ ): AdapterEvent[] {
50
+ if (!catalog) return events;
51
+ const index = events.findIndex(event => emptyToolCatalogRefusal(event, catalog) !== undefined);
52
+ if (index < 0) return events;
53
+ const refusal = emptyToolCatalogRefusal(events[index]!, catalog)!;
54
+ for (const event of events.slice(index)) {
55
+ if ((event.type === "done" || event.type === "incomplete" || event.type === "error") && event.usage) {
56
+ refusal.usage = event.usage;
57
+ }
58
+ releaseTranslatedEvent(event, budget);
59
+ releaseCompactionCiphertextLease(event, budget);
60
+ }
61
+ return [...events.slice(0, index), refusal];
62
+ }