@bitkyc08/opencodex 2.76.0 → 2.78.0-preview.20261005

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (353) hide show
  1. package/bin/ocx.mjs +11 -10
  2. package/gui/dist/assets/App-CppMz-6w.js +54 -0
  3. package/gui/dist/assets/App-lCIslZ1L.css +1 -0
  4. package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-CJWQqtVv.js} +1 -1
  5. package/gui/dist/assets/index-DQt9srNi.js +94 -0
  6. package/gui/dist/assets/index-DmQB8D_R.css +1 -0
  7. package/gui/dist/assets/quota-summary-BMsyoZH2.js +1 -0
  8. package/gui/dist/index.html +4 -3
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic/beta-allowlist.ts +19 -2
  11. package/src/adapters/anthropic/client-identity.ts +13 -4
  12. package/src/adapters/anthropic/native-client-preamble.ts +31 -0
  13. package/src/adapters/anthropic/passthrough.ts +103 -25
  14. package/src/adapters/antigravity-refusal.ts +28 -0
  15. package/src/adapters/cursor/protobuf-request.ts +6 -0
  16. package/src/adapters/cursor/request-builder.ts +3 -0
  17. package/src/adapters/cursor/structured-output.ts +13 -0
  18. package/src/adapters/cursor/types.ts +2 -0
  19. package/src/adapters/devin/cloud-direct/chat.ts +7 -2
  20. package/src/adapters/devin/trajectory.ts +49 -0
  21. package/src/adapters/devin.ts +13 -0
  22. package/src/adapters/ollama-native.ts +62 -19
  23. package/src/adapters/openai-responses/passthrough.ts +133 -103
  24. package/src/adapters/openai-responses/request-strips.ts +10 -4
  25. package/src/adapters/zed.ts +26 -4
  26. package/src/bridge/response-json.ts +14 -14
  27. package/src/bridge/sse.ts +15 -5
  28. package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
  29. package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
  30. package/src/claude/agents-inject.ts +4 -43
  31. package/src/claude/gateway-cache.ts +16 -13
  32. package/src/claude/inbound-model-options.ts +9 -2
  33. package/src/claude/intercept/cli-catalog.ts +27 -16
  34. package/src/claude/intercept/client-hello.ts +103 -0
  35. package/src/claude/intercept/model-bindings.ts +3 -2
  36. package/src/claude/intercept/picker-bootstrap.ts +37 -16
  37. package/src/claude/intercept/picker-budget.ts +41 -0
  38. package/src/claude/intercept/picker-listener.ts +279 -66
  39. package/src/claude/outbound.ts +2 -0
  40. package/src/claude/subagent-force-status.ts +53 -0
  41. package/src/claude/subagent-model.ts +93 -0
  42. package/src/cli/access-audio-input.ts +84 -0
  43. package/src/cli/access-audio-live.ts +99 -0
  44. package/src/cli/access-audio.ts +123 -0
  45. package/src/cli/access-data-client.ts +127 -0
  46. package/src/cli/access-data-plane.ts +104 -0
  47. package/src/cli/access-data-response.ts +76 -0
  48. package/src/cli/access.ts +65 -2
  49. package/src/cli/account-api.ts +33 -6
  50. package/src/cli/account-auth.ts +111 -42
  51. package/src/cli/account-key-quota.ts +118 -0
  52. package/src/cli/account-main.ts +11 -4
  53. package/src/cli/account-next-actions.ts +63 -0
  54. package/src/cli/account-policy-dto.ts +68 -0
  55. package/src/cli/account-policy.ts +167 -0
  56. package/src/cli/account-target.ts +8 -0
  57. package/src/cli/account.ts +37 -9
  58. package/src/cli/agent-runtime-settings.ts +130 -0
  59. package/src/cli/agent-settings.ts +101 -0
  60. package/src/cli/agent.ts +29 -2
  61. package/src/cli/alias.ts +7 -2
  62. package/src/cli/aside-profiles.ts +2 -2
  63. package/src/cli/capabilities-access-remote.ts +147 -0
  64. package/src/cli/capabilities-accounts.ts +195 -0
  65. package/src/cli/capabilities-agents-routing.ts +414 -0
  66. package/src/cli/capabilities-base.ts +783 -0
  67. package/src/cli/capabilities-command.ts +1 -0
  68. package/src/cli/capabilities-integrations.ts +264 -0
  69. package/src/cli/capabilities-lab.ts +214 -0
  70. package/src/cli/capabilities-observe-system.ts +662 -0
  71. package/src/cli/capabilities-provider-models.ts +302 -0
  72. package/src/cli/capabilities.ts +30 -1190
  73. package/src/cli/capability-types.ts +54 -0
  74. package/src/cli/catalog-command-result.ts +57 -0
  75. package/src/cli/chatgpt-command.ts +35 -9
  76. package/src/cli/claude-agent-startup-sync.ts +17 -0
  77. package/src/cli/claude-desktop-profile.ts +100 -0
  78. package/src/cli/claude-desktop.ts +39 -6
  79. package/src/cli/claude.ts +22 -6
  80. package/src/cli/codex-shim-autorestore.ts +2 -1
  81. package/src/cli/codex-shim-readiness.ts +109 -0
  82. package/src/cli/combo-input.ts +88 -0
  83. package/src/cli/combo-result.ts +52 -0
  84. package/src/cli/combo-stats.ts +123 -0
  85. package/src/cli/combo.ts +37 -14
  86. package/src/cli/companion-timeline.ts +142 -0
  87. package/src/cli/companion-usage.ts +146 -0
  88. package/src/cli/companion.ts +12 -3
  89. package/src/cli/config-command.ts +24 -8
  90. package/src/cli/connect.ts +19 -0
  91. package/src/cli/dispatch.ts +55 -82
  92. package/src/cli/doctor.ts +22 -4
  93. package/src/cli/export-command.ts +20 -9
  94. package/src/cli/gui.ts +8 -3
  95. package/src/cli/help-catalog.ts +101 -0
  96. package/src/cli/help-models-context.ts +17 -0
  97. package/src/cli/help-navigation.ts +50 -0
  98. package/src/cli/help-recovery.ts +66 -0
  99. package/src/cli/help.ts +69 -11
  100. package/src/cli/index.ts +38 -27
  101. package/src/cli/injection-follow.ts +35 -0
  102. package/src/cli/inspect.ts +5 -0
  103. package/src/cli/integration-aside-sync.ts +69 -0
  104. package/src/cli/integration-cursor.ts +106 -0
  105. package/src/cli/integration-input.ts +60 -0
  106. package/src/cli/integration-journal.ts +49 -0
  107. package/src/cli/integration-plan-dto.ts +156 -0
  108. package/src/cli/integration-preview.ts +215 -0
  109. package/src/cli/integrations.ts +24 -18
  110. package/src/cli/json-input.ts +134 -0
  111. package/src/cli/link.ts +32 -4
  112. package/src/cli/local-sync-result.ts +41 -0
  113. package/src/cli/log-follow.ts +84 -0
  114. package/src/cli/log-view-filter.ts +146 -0
  115. package/src/cli/logout-command.ts +44 -0
  116. package/src/cli/model-picker-ordering.ts +137 -0
  117. package/src/cli/models-custom-input.ts +107 -0
  118. package/src/cli/models-custom-runtime.ts +112 -0
  119. package/src/cli/models-order.ts +194 -0
  120. package/src/cli/models-runtime-subcommands.ts +2 -0
  121. package/src/cli/models-runtime.ts +9 -1
  122. package/src/cli/models.ts +66 -81
  123. package/src/cli/observe-stream.ts +142 -0
  124. package/src/cli/observe.ts +75 -34
  125. package/src/cli/opencode.ts +54 -10
  126. package/src/cli/provider-batch.ts +77 -0
  127. package/src/cli/provider-lifecycle-runtime.ts +170 -0
  128. package/src/cli/provider-result.ts +124 -0
  129. package/src/cli/provider-runtime.ts +15 -12
  130. package/src/cli/provider-settings.ts +143 -0
  131. package/src/cli/provider.ts +116 -64
  132. package/src/cli/registry.ts +90 -24
  133. package/src/cli/remote-workspace-hub.ts +83 -0
  134. package/src/cli/remote-workspace.ts +6 -1
  135. package/src/cli/restart-failure.ts +27 -0
  136. package/src/cli/root.ts +42 -9
  137. package/src/cli/route-policy-write.ts +137 -0
  138. package/src/cli/route-policy.ts +8 -0
  139. package/src/cli/runtime-api.ts +57 -80
  140. package/src/cli/secret-args.ts +149 -0
  141. package/src/cli/settings-result.ts +30 -0
  142. package/src/cli/storage.ts +77 -7
  143. package/src/cli/system-command.ts +60 -2
  144. package/src/cli/system-restart-client.ts +20 -3
  145. package/src/cli/system-settings-parity.ts +83 -0
  146. package/src/cli/tray-proxy.ts +3 -2
  147. package/src/cli/update-restart-candidate.ts +21 -0
  148. package/src/cli/update-restart-child.ts +81 -0
  149. package/src/cli/update-restart-home.ts +38 -0
  150. package/src/cli/update-restart-transport.ts +193 -0
  151. package/src/cli/update-restart.ts +260 -0
  152. package/src/cli/usage-model-search.ts +58 -0
  153. package/src/cli/usage-report.ts +13 -5
  154. package/src/cli/v2-input.ts +66 -0
  155. package/src/cli/v2-local-output.ts +37 -0
  156. package/src/cli/v2-runtime.ts +90 -0
  157. package/src/cli/v2.ts +102 -144
  158. package/src/clients/config-export/contracts.ts +100 -0
  159. package/src/clients/config-export/droid.ts +54 -2
  160. package/src/clients/config-export/dsh.ts +21 -0
  161. package/src/clients/config-export/kilo.ts +12 -2
  162. package/src/clients/config-export/model-metadata.ts +10 -4
  163. package/src/clients/config-export/reasoning-metadata.ts +47 -0
  164. package/src/clients/config-export.ts +53 -67
  165. package/src/clients/omo-role-models.ts +17 -3
  166. package/src/codex/account-credit-use.ts +4 -2
  167. package/src/codex/account-runtime-state.ts +21 -0
  168. package/src/codex/account-store.ts +11 -47
  169. package/src/codex/account-usability.ts +11 -8
  170. package/src/codex/auth-api/account-list.ts +73 -63
  171. package/src/codex/auth-api/main-account-probe.ts +25 -12
  172. package/src/codex/auth-api/pool-mode-gate.ts +6 -9
  173. package/src/codex/auth-api/pool-quota-probe.ts +2 -4
  174. package/src/codex/auth-api/reset-credit-service.ts +3 -2
  175. package/src/codex/auth-context.ts +30 -11
  176. package/src/codex/catalog/aggregation.ts +3 -0
  177. package/src/codex/catalog/build-entries.ts +1 -1
  178. package/src/codex/catalog/bundled.ts +9 -2
  179. package/src/codex/catalog/heal-observation.ts +66 -0
  180. package/src/codex/catalog/model-hints.ts +2 -1
  181. package/src/codex/catalog/model-visibility.ts +6 -3
  182. package/src/codex/catalog/parsing.ts +2 -0
  183. package/src/codex/catalog/provider-models.ts +13 -4
  184. package/src/codex/catalog/publication-observer.ts +19 -0
  185. package/src/codex/catalog/remote.ts +13 -15
  186. package/src/codex/catalog/restore.ts +19 -6
  187. package/src/codex/catalog/retained-sync.ts +60 -11
  188. package/src/codex/catalog/routed-removal.ts +116 -0
  189. package/src/codex/catalog/write-audit-contract.ts +21 -0
  190. package/src/codex/catalog/write-audit.ts +171 -0
  191. package/src/codex/catalog-self-heal.ts +284 -0
  192. package/src/codex/catalog-write-serialization.ts +71 -7
  193. package/src/codex/chatgpt-refresh-failure.ts +56 -0
  194. package/src/codex/codex-home-owner.ts +124 -0
  195. package/src/codex/convergence.ts +57 -16
  196. package/src/codex/features.ts +8 -1
  197. package/src/codex/inject/restore.ts +46 -4
  198. package/src/codex/inject.ts +11 -1
  199. package/src/codex/internal/catalog-writer.ts +120 -13
  200. package/src/codex/journal.ts +49 -13
  201. package/src/codex/main-account.ts +144 -11
  202. package/src/codex/management-convergence.ts +3 -0
  203. package/src/codex/quota-refresh-outcome.ts +17 -2
  204. package/src/codex/quota-types.ts +36 -1
  205. package/src/codex/quota.ts +67 -16
  206. package/src/codex/refresh.ts +20 -9
  207. package/src/codex/routing/cooldown-math.ts +10 -4
  208. package/src/codex/routing/selection.ts +11 -4
  209. package/src/codex/routing.ts +9 -9
  210. package/src/codex/runtime.ts +7 -1
  211. package/src/codex/shim-diagnostics.ts +80 -0
  212. package/src/codex/shim-fingerprint.ts +7 -2
  213. package/src/codex/shim-migration.ts +153 -0
  214. package/src/codex/shim-overlay.ts +509 -0
  215. package/src/codex/shim-path-resolution.ts +145 -0
  216. package/src/codex/shim-probe.ts +4 -3
  217. package/src/codex/shim-state-file.ts +93 -7
  218. package/src/codex/shim.ts +54 -107
  219. package/src/codex/subagent-model-fallback.ts +2 -1
  220. package/src/codex/sync.ts +24 -4
  221. package/src/combos/failover.ts +101 -1
  222. package/src/combos/index.ts +1 -0
  223. package/src/combos/jev.ts +13 -3
  224. package/src/combos/resolve.ts +12 -0
  225. package/src/config/atomic-write.ts +13 -8
  226. package/src/config/diagnostics.ts +23 -1
  227. package/src/config/load-degrade.ts +7 -0
  228. package/src/config/provider-validation.ts +3 -0
  229. package/src/config/schema/chatgpt-desktop.ts +22 -0
  230. package/src/config/schema/compaction-triggers.ts +8 -0
  231. package/src/config/schema/config-schema.ts +43 -14
  232. package/src/config/schema/leaf-validators.ts +8 -3
  233. package/src/config/subagent-models.ts +12 -0
  234. package/src/generated/compatibility-version.json +645 -237
  235. package/src/grok/session-identity.ts +33 -0
  236. package/src/integrations/catalog-refresh.ts +1 -1
  237. package/src/integrations/merge.ts +13 -5
  238. package/src/integrations/mutation-plan.ts +50 -5
  239. package/src/integrations/omp-yaml-source.ts +181 -1
  240. package/src/integrations/ownership-policy.ts +66 -0
  241. package/src/integrations/registry.ts +50 -1
  242. package/src/integrations/state.ts +106 -19
  243. package/src/integrations/target.ts +25 -2
  244. package/src/integrations/writer.ts +107 -35
  245. package/src/lib/gui-pair-capability.ts +17 -0
  246. package/src/lib/package-tree-integrity.ts +42 -1
  247. package/src/lib/provider-client-headers.ts +62 -0
  248. package/src/lib/proxy-env.ts +3 -0
  249. package/src/lib/test-home-guard.ts +2 -1
  250. package/src/oauth/anthropic-account-refusal.ts +19 -8
  251. package/src/oauth/anthropic-continuity.ts +65 -0
  252. package/src/oauth/anthropic-identity.ts +75 -0
  253. package/src/oauth/anthropic-routing.ts +20 -15
  254. package/src/oauth/anthropic.ts +34 -3
  255. package/src/oauth/generic-account-failover.ts +21 -0
  256. package/src/oauth/health.ts +32 -5
  257. package/src/oauth/index.ts +42 -35
  258. package/src/oauth/local-token-detect.ts +7 -0
  259. package/src/oauth/pool-settings-capability.ts +9 -2
  260. package/src/oauth/refresh-policy.ts +2 -0
  261. package/src/oauth/store.ts +55 -6
  262. package/src/oauth/types.ts +9 -0
  263. package/src/protocols/encoders/messages.ts +4 -3
  264. package/src/protocols/plan-snapshot.ts +1 -1
  265. package/src/protocols/settings.ts +28 -4
  266. package/src/providers/antigravity-effort-families.ts +75 -0
  267. package/src/providers/antigravity-models.ts +54 -16
  268. package/src/providers/antigravity-wire-snapshot.ts +66 -0
  269. package/src/providers/derive.ts +2 -0
  270. package/src/providers/model-rename-fields.ts +3 -0
  271. package/src/providers/new-model-policy.ts +48 -10
  272. package/src/providers/registry/entries-extended.ts +3 -0
  273. package/src/providers/registry/model-ids.ts +2 -0
  274. package/src/providers/registry/model-seeds.ts +5 -0
  275. package/src/providers/registry/types.ts +4 -0
  276. package/src/responses/compaction.ts +19 -0
  277. package/src/router.ts +6 -0
  278. package/src/server/auth-cors.ts +9 -0
  279. package/src/server/caller-session-identity.ts +32 -0
  280. package/src/server/chat-completions.ts +10 -0
  281. package/src/server/claude-messages.ts +29 -15
  282. package/src/server/droid-reasoning-default.ts +50 -0
  283. package/src/server/github-copilot-responses-repair.ts +3 -6
  284. package/src/server/grok-responses-control-frame.ts +2 -2
  285. package/src/server/grok-responses-snapshot-repair.ts +4 -5
  286. package/src/server/gui-session.ts +33 -11
  287. package/src/server/index/claude-intercept-lifecycle.ts +4 -0
  288. package/src/server/index/serve-options.ts +13 -7
  289. package/src/server/index.ts +1 -1
  290. package/src/server/management/agent-settings-routes.ts +12 -134
  291. package/src/server/management/config-routes.ts +1 -1
  292. package/src/server/management/context.ts +8 -1
  293. package/src/server/management/cursor-integration-routes.ts +3 -3
  294. package/src/server/management/integration-routes.ts +69 -11
  295. package/src/server/management/link-routes.ts +20 -13
  296. package/src/server/management/model-routes.ts +3 -3
  297. package/src/server/management/model-row-export-metadata.ts +150 -0
  298. package/src/server/management/model-rows.ts +148 -9
  299. package/src/server/management/native-integration-routes.ts +2 -2
  300. package/src/server/management/oauth-account-routes.ts +96 -30
  301. package/src/server/management/route-registry.ts +29 -27
  302. package/src/server/management/shared.ts +19 -5
  303. package/src/server/management/subagent-model-routes.ts +214 -0
  304. package/src/server/messages-native-eligibility.ts +3 -6
  305. package/src/server/messages-native-oauth.ts +82 -35
  306. package/src/server/messages-native.ts +78 -16
  307. package/src/server/relay.ts +14 -44
  308. package/src/server/request-log-generation-window.ts +38 -0
  309. package/src/server/request-log.ts +13 -0
  310. package/src/server/responses/adapter-dispatch.ts +152 -28
  311. package/src/server/responses/codex-auth-error.ts +10 -2
  312. package/src/server/responses/combo-stream-preflight.ts +29 -3
  313. package/src/server/responses/compaction-routing.ts +10 -1
  314. package/src/server/responses/core-auth.ts +67 -30
  315. package/src/server/responses/core-codex-account.ts +6 -3
  316. package/src/server/responses/core-combo-failure.ts +5 -1
  317. package/src/server/responses/core-combo-native.ts +7 -1
  318. package/src/server/responses/core-combo.ts +64 -7
  319. package/src/server/responses/core-normalize.ts +7 -0
  320. package/src/server/responses/core-options.ts +5 -0
  321. package/src/server/responses/fetch-helpers.ts +9 -0
  322. package/src/server/responses/passthrough-delivery.ts +1 -1
  323. package/src/server/responses/policy-fallback.ts +1 -1
  324. package/src/server/responses/request-prepare.ts +5 -0
  325. package/src/server/responses/request-spend.ts +30 -30
  326. package/src/server/responses-custom-tool-repair.ts +2 -2
  327. package/src/server/responses-hosted-image-display.ts +70 -9
  328. package/src/server/responses-terminal-repair.ts +15 -2
  329. package/src/server/sse-frame-buffer.ts +38 -36
  330. package/src/server/sse-payload-rewrite.ts +78 -49
  331. package/src/server/ws-bridge.ts +1 -11
  332. package/src/service/cli.ts +6 -1
  333. package/src/service/ownership-mutation-lease.d.mts +26 -0
  334. package/src/service/ownership-mutation-lease.mjs +76 -1
  335. package/src/service/windows-taskxml.ts +1 -2
  336. package/src/service-manager-probe.ts +36 -2
  337. package/src/types/config.ts +6 -0
  338. package/src/types/provider.ts +28 -10
  339. package/src/update/index.ts +2 -1
  340. package/src/update/npm-invocation.mjs +35 -3
  341. package/src/update/proxy-liveness-probe.mjs +38 -8
  342. package/src/update/update-failure-guidance.d.mts +6 -0
  343. package/src/update/update-failure-guidance.mjs +30 -5
  344. package/src/usage/expected-prices.ts +9 -0
  345. package/src/usage/log.ts +7 -0
  346. package/src/usage/summary.ts +82 -1
  347. package/src/web-search/loop.ts +11 -8
  348. package/src/web-search/run-turn-loop.ts +10 -7
  349. package/gui/dist/assets/App-CddorHdA.js +0 -51
  350. package/gui/dist/assets/App-DMudtISW.css +0 -1
  351. package/gui/dist/assets/index-Btze1I3T.css +0 -1
  352. package/gui/dist/assets/index-CvnY7E6i.js +0 -86
  353. package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
@@ -34,6 +34,7 @@ import {
34
34
  import { captureRouteStaticPolicy, routeConcreteModel } from "../../router";
35
35
  import { comboDefaultEffort, concreteComboRequestBody, getCombo } from "../../combos";
36
36
  import { supportedLadderFor } from "../effort-policy";
37
+ import { applyDroidReasoningDefault } from "../droid-reasoning-default";
37
38
  import { resolveWireProtocolOverride } from "../adapter-resolve";
38
39
  import { resolveOpenCodeGoTransport } from "../../providers/opencode-go-transport";
39
40
  import { getOrAllocateRequestSessionLane } from "../request-log-conversation";
@@ -105,10 +106,11 @@ export function createComboProtocolLanes(input: {
105
106
  config: OcxConfig;
106
107
  logCtx: RequestLogContext;
107
108
  admission: DataPlaneAdmission | undefined;
109
+ droidDefaultEffort?: string;
108
110
  comboId: string;
109
111
  targets: readonly ComboTarget[];
110
112
  }): ComboProtocolLanes | undefined {
111
- const { source, req, config, logCtx, admission, comboId, targets } = input;
113
+ const { source, req, config, logCtx, admission, droidDefaultEffort, comboId, targets } = input;
112
114
  if (!source) return undefined;
113
115
  const envelope = source.envelope;
114
116
  const reject = resolveProtocolSettings(config).unrepresentable === "reject";
@@ -207,6 +209,10 @@ export function createComboProtocolLanes(input: {
207
209
  const route = settle(target, targetRoute);
208
210
  const body = nativeBody(target, route);
209
211
  if (!route || !body) return undefined;
212
+ applyDroidReasoningDefault(body, droidDefaultEffort, {
213
+ provider: targetRoute.provider,
214
+ modelId: targetRoute.modelId,
215
+ });
210
216
  applyComboEffort(body, config, comboId, target, targetRoute);
211
217
  return { route, body, sendBudget: targetSendBudget };
212
218
  },
@@ -1,8 +1,10 @@
1
+ import type { CodexAccountModelRefusal } from "../../combos/failover";
1
2
  import {
2
3
  isCodexReasoningEffort,
3
4
  isDeclaredReasoningEffort,
4
5
  resolveEffortAtOrBelow,
5
6
  } from "../../reasoning-effort";
7
+ import { resolveAdmissionModelScope, routeAllowedByScope } from "../admission-model-scope";
6
8
  import { recordAttemptRequestedEffort } from "../request-log";
7
9
  import {
8
10
  CODEX_TEXT_GUARDED_BUDGET_POLICY,
@@ -26,6 +28,7 @@ import {
26
28
  concreteComboRequestBody,
27
29
  comboDefaultEffort,
28
30
  isComboTargetInCooldown,
31
+ snapshotComboQuotaCooldowns,
29
32
  noteComboSuccess,
30
33
  comboFailureDecision,
31
34
  advanceComboAfterFailure,
@@ -65,9 +68,12 @@ import {
65
68
  recoverEncryptedAgentTaskWithResult,
66
69
  } from "./agent-task-recovery";
67
70
  import { isThreadSpawnRequest, supportedLadderFor } from "../effort-policy";
71
+ import { applyDroidResponsesReasoningDefault } from "../droid-reasoning-default";
72
+ import { isPlainObject } from "../../lib/plain-data";
68
73
  import {
69
74
  clientCancelledResponse,
70
75
  comboUnavailable,
76
+ comboUnavailableResponse,
71
77
  targetIncompatibleResponse,
72
78
  unreadableEncryptedAgentTaskResponse,
73
79
  } from "./core-errors";
@@ -327,6 +333,7 @@ export async function executeComboResponses(
327
333
  config,
328
334
  logCtx,
329
335
  admission: options.admission,
336
+ droidDefaultEffort: options.droidDefaultEffort,
330
337
  comboId,
331
338
  targets: combo.targets,
332
339
  });
@@ -407,8 +414,8 @@ export async function executeComboResponses(
407
414
  }
408
415
  };
409
416
  const adoptFailedChildLog = (childLog: RequestLogContext): void => {
410
- // Attempts remain the complete physical history; the logical row mirrors the most recent
411
- // failed target so an exhausted combo still has useful top-level reasoning diagnostics.
417
+ // Attempts remain the complete physical history; the logical row mirrors the failed
418
+ // target whose response is returned, including an earlier quota refusal on exhaustion.
412
419
  Object.assign(logCtx, childLog, {
413
420
  requestedModel,
414
421
  model: requestedModel,
@@ -572,6 +579,9 @@ export async function executeComboResponses(
572
579
  candidates: choices.map(choice => choice.candidate),
573
580
  fallback,
574
581
  config,
582
+ isDestinationAllowed: (providerName, modelId) => routeAllowedByScope(
583
+ resolveAdmissionModelScope(config, options.admission), { providerName, modelId },
584
+ ),
575
585
  ...(combo.decisionProvider ? { decisionProvider: combo.decisionProvider } : {}),
576
586
  ...(combo.decisionModel
577
587
  ? {
@@ -668,6 +678,41 @@ export async function executeComboResponses(
668
678
  // is gone, so carry the loop's own classification decision instead of re-deriving a
669
679
  // weaker one from the status alone (#4149).
670
680
  let lastFailureClassifiesOverflow = false;
681
+ // The first quota/rate-limit refusal of the ladder. A fallback that then refuses its own
682
+ // credential or plan (401/403, or a 400 the ladder hopped past) says nothing about why the
683
+ // request failed: the primary target ran out. Returning that fallback refusal told clients
684
+ // "OpenAI account pool has no usable account credential" while every Claude account was spent.
685
+ let quotaFailure: Response | undefined;
686
+ let quotaFailedChildLog: RequestLogContext | undefined;
687
+ // Snapshotted before dispatch: a 401/403 inside this ladder cools its own provider, and that
688
+ // fresh cooldown is not a reason the request found no target.
689
+ const quotaCooldownSnapshot = snapshotComboQuotaCooldowns(config, comboId);
690
+ const exhaustedFailure = (): Response => {
691
+ const returnsQuota = lastFailure && [400, 401, 403].includes(lastFailure.status) && quotaFailure;
692
+ const failedChildLog = returnsQuota ? quotaFailedChildLog : lastFailedChildLog;
693
+ if (failedChildLog) adoptFailedChildLog(failedChildLog);
694
+ if (!lastFailure || ![400, 401, 403].includes(lastFailure.status)) return lastFailure!;
695
+ if (quotaFailure) return quotaFailure;
696
+ // The quota-refused target was cooled before this request picked, so the ladder never saw
697
+ // its refusal. The cooldown, with its Retry-After, is still the honest answer.
698
+ let quotaCooldownUntil: number | undefined;
699
+ const now = Date.now();
700
+ for (const { target, cooldownUntil } of quotaCooldownSnapshot) {
701
+ if (cooldownUntil <= now) continue;
702
+ try {
703
+ if (!targetEligible(target)) continue;
704
+ } catch {
705
+ // An eligibility check can exhaust the translator budget; it is not quota evidence.
706
+ continue;
707
+ }
708
+ quotaCooldownUntil = Math.min(quotaCooldownUntil ?? cooldownUntil, cooldownUntil);
709
+ }
710
+ const remainingQuotaCooldownMs = quotaCooldownUntil === undefined ? 0 : quotaCooldownUntil - Date.now();
711
+ return remainingQuotaCooldownMs > 0
712
+ ? comboUnavailableResponse(`No available targets for combo: ${comboId}`, {
713
+ retryAfter: String(Math.max(1, Math.ceil(remainingQuotaCooldownMs / 1000))),
714
+ }) : lastFailure;
715
+ };
671
716
  while (pick) {
672
717
  if (options.abortSignal?.aborted) return clientCancelledResponse();
673
718
  const firstComboTarget = comboTargetsDispatched === 0;
@@ -690,8 +735,7 @@ export async function executeComboResponses(
690
735
  // return the last real upstream answer with its status, headers and any quota body
691
736
  // intact rather than to mint a synthetic error, and a later target only exists because
692
737
  // an earlier one already recorded one.
693
- if (lastFailedChildLog) adoptFailedChildLog(lastFailedChildLog);
694
- return lastFailure!;
738
+ return exhaustedFailure();
695
739
  }
696
740
  const targetSendBudget = comboSendScope
697
741
  ? comboTargetSendBudget(comboSendScope, combo.targets.length - 1 - comboTargetsDispatched)
@@ -709,8 +753,13 @@ export async function executeComboResponses(
709
753
  modelId: targetRoute.modelId,
710
754
  });
711
755
  const initialJevDecision = firstComboTarget ? jevDecision : undefined;
756
+ const childInput = options.droidDefaultEffort && isPlainObject(body) ? { ...body } : body;
757
+ applyDroidResponsesReasoningDefault(childInput, options.droidDefaultEffort, {
758
+ provider: targetRoute.provider,
759
+ modelId: targetRoute.modelId,
760
+ });
712
761
  const childBody = concreteComboRequestBody(
713
- body,
762
+ childInput,
714
763
  pick.target,
715
764
  initialJevDecision ? initialJevDecision.effort : comboDefaultEffort(config, comboId),
716
765
  initialJevDecision?.effort === null ? [] : targetReasoningEfforts,
@@ -773,6 +822,7 @@ export async function executeComboResponses(
773
822
  const completedTarget = { provider: pick.target.provider, model: pick.target.model };
774
823
  const writerGeneration = pick.writerGeneration;
775
824
  let consumedChildFailure: ConsumedComboFailure | undefined;
825
+ let preflightModelRefusal: CodexAccountModelRefusal | undefined;
776
826
  const callbackGate = createChildPassthroughCallbackGate({
777
827
  ...options,
778
828
  onResponseComplete: model => {
@@ -895,6 +945,7 @@ export async function executeComboResponses(
895
945
  callbackGate.discard();
896
946
  terminalRecorder?.("failed", preflight.response.status);
897
947
  response = preflight.response;
948
+ preflightModelRefusal = preflight.codexModelRefusal;
898
949
  } else {
899
950
  response = preflight.response;
900
951
  if (nativePassthrough) markNativePassthroughSseResponse(response);
@@ -937,6 +988,7 @@ export async function executeComboResponses(
937
988
  try {
938
989
  failure = consumedChildFailure
939
990
  ?? await consumeComboFailure(response, options.abortSignal);
991
+ if (preflightModelRefusal !== undefined) failure = { ...failure, codexModelRefusal: preflightModelRefusal };
940
992
  } catch (error) {
941
993
  if (options.abortSignal?.aborted) {
942
994
  retainCancelledAttempt();
@@ -963,6 +1015,10 @@ export async function executeComboResponses(
963
1015
  );
964
1016
  attemptRetained = true;
965
1017
  lastFailure = failure.response;
1018
+ if (!quotaFailure && (lastFailure.status === 429 || lastFailure.status === 402)) {
1019
+ quotaFailure = lastFailure;
1020
+ quotaFailedChildLog = childLog;
1021
+ }
966
1022
  lastFailedChildLog = childLog;
967
1023
  // A replacement that answers 200 is unmarked, and its zero-output failure only exists once
968
1024
  // preflight has rebuilt the stream as a fresh Response. A spent grant never hops: a status the
@@ -981,6 +1037,7 @@ export async function executeComboResponses(
981
1037
  ? "stop"
982
1038
  : comboFailureDecision(failure.response.status, failure.classificationText, {
983
1039
  code: failure.upstreamCode,
1040
+ codexModelRefusal: failure.codexModelRefusal,
984
1041
  });
985
1042
  const wantsStream = (rawBody as { stream?: unknown } | null)?.stream === true;
986
1043
  // Local byte admission has its own diagnostic; do not relabel it as an upstream refusal.
@@ -1124,9 +1181,9 @@ export async function executeComboResponses(
1124
1181
  }
1125
1182
  // Waiting or recovery may have observed cancellation after the check above.
1126
1183
  if (options.abortSignal?.aborted) return clientCancelledResponse();
1127
- adoptFailedChildLog(childLog);
1128
1184
  }
1129
1185
  }
1186
+ const failure = exhaustedFailure();
1130
1187
  if (
1131
1188
  lastFailure?.status === 413
1132
1189
  && lastFailureClassifiesOverflow
@@ -1135,5 +1192,5 @@ export async function executeComboResponses(
1135
1192
  ? streamingContextOverflowResponse(requestedModel, options.translatorBudget)
1136
1193
  : jsonContextOverflowResponse();
1137
1194
  }
1138
- return lastFailure!;
1195
+ return failure;
1139
1196
  }
@@ -38,6 +38,7 @@ import { isInjectionDebugEnabled } from "../../lib/debug-settings";
38
38
  import { injectionDebugLog } from "../../lib/injection-debug-log";
39
39
  import { recordAttemptRequestedEffort } from "../request-log";
40
40
  import type { ResolvedFastPolicy } from "../../providers/fastwire";
41
+ import { applyDroidResponsesReasoningDefault } from "../droid-reasoning-default";
41
42
  import { applyXaiOauthFastModel, xaiOauthFastModelForDecision } from "../../providers/xai-fast-model";
42
43
 
43
44
  /** Preview the billed xAI lane without mutating request state or resolving credentials. */
@@ -140,9 +141,15 @@ export async function applyFinalRouteRequestNormalization(args: {
140
141
  inboundWire: InboundWire;
141
142
  inboundTransport?: "websocket";
142
143
  claudeGoAffinity?: HandleResponsesOptions["claudeGoAffinity"];
144
+ droidDefaultEffort?: HandleResponsesOptions["droidDefaultEffort"];
143
145
  }): Promise<void> {
144
146
  const { parsed, route, config, req, logCtx, inboundWire, inboundTransport } = args;
145
147
  const effortSelector = prepareEffortNormalization(parsed, route);
148
+ if (applyDroidResponsesReasoningDefault(
149
+ parsed._rawBody,
150
+ args.droidDefaultEffort,
151
+ { provider: route.provider, modelId: route.modelId },
152
+ )) parsed.options.reasoning = args.droidDefaultEffort;
146
153
 
147
154
  // Only Anthropic message routes retain the Codex-facing selector. Other providers must keep
148
155
  // their existing response.model contract even when their public and wire model ids differ.
@@ -1,3 +1,4 @@
1
+ import type { CodexAccountModelRefusal } from "../../combos/failover";
1
2
  import type { NativeResponseControl } from "./native-response-control";
2
3
  import type { AdapterEvent, OcxUsage, OcxProviderContinuationState, OcxConfig } from "../../types";
3
4
  import type { RouteResult } from "../../router";
@@ -24,6 +25,8 @@ import type { PolicyRequestScope } from "./policy-request-scope";
24
25
  export interface ConsumedComboFailure {
25
26
  response: Response;
26
27
  classificationText: string;
28
+ /** Complete bounded-envelope evidence captured before display truncation; never serialized. */
29
+ codexModelRefusal?: CodexAccountModelRefusal;
27
30
  /** Structured upstream `error.code` when present in the failure body. */
28
31
  upstreamCode?: string;
29
32
  /** Complete structured provider type, retained for conservative recovery classification. */
@@ -131,6 +134,8 @@ export interface HandleResponsesOptions {
131
134
  * it. Omitted means a genuine Responses inbound.
132
135
  */
133
136
  inboundWire?: InboundWire;
137
+ /** Droid's per-request effort default; each concrete combo or policy target applies it only if its ladder allows it. */
138
+ droidDefaultEffort?: string;
134
139
  /** PF-07: the Chat source a combo child may send natively; set only by the Chat ingress. */
135
140
  protocolSource?: import("./core-combo-native").ComboProtocolSource;
136
141
  /** Internal transport identity for route-scoped upstream compatibility policy. */
@@ -192,8 +192,17 @@ export function sendWithConnectionPolicy(
192
192
  const egressInit = redirectedToLoopback
193
193
  ? { proxy: false as const }
194
194
  : decide ? providerEgressSendInit(egress, physicalFetch, input) : {};
195
+ // Bun can reset large native Codex string uploads before receiving headers while
196
+ // the identical UTF-8 buffer succeeds. Convert only at the final HTTP send so
197
+ // WebSocket selection still sees the serialized string and no retry is added.
198
+ const target = input instanceof Request ? input.url : String(input);
199
+ const body = init?.body;
200
+ const largeCodexBody = typeof body === "string"
201
+ && /^https:\/\/chatgpt\.com\/backend-api\/codex\/responses(?:\/compact)?$/.test(target)
202
+ && Buffer.byteLength(body, "utf8") >= 1024 * 1024;
195
203
  return physicalFetch(input, {
196
204
  ...init,
205
+ ...(largeCodexBody ? { body: Buffer.from(body, "utf8") } : {}),
197
206
  headers,
198
207
  redirect: "manual",
199
208
  ...(fresh ? { keepalive: false } : {}),
@@ -1329,7 +1329,7 @@ export async function deliverPassthroughResponse(
1329
1329
  clientJson = imageDisplay.json(clientJson);
1330
1330
  } catch (error) {
1331
1331
  if (error instanceof RangeError) {
1332
- return formatErrorResponse(502, "upstream_error", "hosted image result count exceeds local display limit");
1332
+ return formatErrorResponse(502, "upstream_error", "hosted image result exceeds local display limits");
1333
1333
  }
1334
1334
  throw error;
1335
1335
  } finally { imageDisplay.dispose?.(); }
@@ -111,7 +111,7 @@ async function shouldHopPolicyCandidate(response: Response, signal?: AbortSignal
111
111
  if (isPolicyCandidateRefusal(response)) return true;
112
112
  try {
113
113
  const inspected = await readBoundedResponseBody(response.clone(), { signal });
114
- const text = inspected.displaySafe ? inspected.text : "";
114
+ const text = inspected.displaySafe && !inspected.truncated ? inspected.text : "";
115
115
  return comboFailureDecision(response.status, text, { code: errorCodeFromText(text) }) === "hop";
116
116
  } catch {
117
117
  return false;
@@ -676,6 +676,7 @@ export async function prepareResponsesRequest(
676
676
  route,
677
677
  options,
678
678
  credentialDomainWasRewritten,
679
+ config,
679
680
  );
680
681
  // Does the CALLER own the credential this request will authenticate with? Validated exactly
681
682
  // the way final auth validates it: the route ownership predicate AND the caller-bearer check
@@ -736,6 +737,7 @@ export async function prepareResponsesRequest(
736
737
  // recovery or a profile drain fences the physical identity, final auth drops main and preview
737
738
  // has to drop it too. A hardcoded `true` would be wrong in the second case and `false` in
738
739
  // the first.
740
+ requestOwnedMainCredential: previewRequestScopedMainCredential,
739
741
  isMainAccountTokenLive: previewRequestScopedMainCredential
740
742
  ? () => previewRequestOwnedMainCredentialLive
741
743
  : undefined,
@@ -962,6 +964,7 @@ export async function prepareResponsesRequest(
962
964
  route,
963
965
  options,
964
966
  credentialDomainWasRewritten,
967
+ config,
965
968
  );
966
969
  const recoveryRequestScopedMainCredential = codexRouteCredentialOwnership(
967
970
  recoveryAuthHeaders,
@@ -993,6 +996,7 @@ export async function prepareResponsesRequest(
993
996
  const recoverySelectionOptions = {
994
997
  nativeMainSelectionOnly: !recoveryNativeMainBlocked
995
998
  && recoverySelectionAdmission?.mainProfileDraining === true,
999
+ requestOwnedMainCredential: recoveryRequestScopedMainCredential,
996
1000
  isMainAccountTokenLive: recoveryRequestScopedMainCredential
997
1001
  ? () => recoveryRequestOwnedMainCredentialLive
998
1002
  : undefined,
@@ -1222,6 +1226,7 @@ export async function prepareResponsesRequest(
1222
1226
  inboundWire,
1223
1227
  inboundTransport: options.inboundTransport,
1224
1228
  claudeGoAffinity: options.claudeGoAffinity,
1229
+ droidDefaultEffort: options.droidDefaultEffort,
1225
1230
  });
1226
1231
  try {
1227
1232
  assertPolicyNormalizedRoute(policyScope, admittedPolicyRoute, route);
@@ -1,9 +1,9 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import type { RequestSendObserver } from "../../lib/request-execution-budget";
3
- import { sharedSpendLedger, type SpendReservationLedger } from "../../lib/spend-reservation-ledger";
3
+ import { sharedSpendLedger, type SpendReservationLedger, type SpendScopes } from "../../lib/spend-reservation-ledger";
4
4
  import { SpendLedgerOwnerError } from "../../lib/spend-ledger-owner";
5
5
  import { markLocalRequestLogRefusal, type RequestLogContext } from "../request-log";
6
- import { recordWorkflowRefusalEvent, workflowDenialSummary } from "../../lib/workflow-budget";
6
+ import { recordWorkflowRefusalEvent, workflowDenialSummary, type WorkflowDenial } from "../../lib/workflow-budget";
7
7
 
8
8
  /** The terminal usage a request reported, in the only two fields the ledger books. */
9
9
  export interface TerminalSpendUsage {
@@ -81,44 +81,44 @@ export function createRequestSpendTracker(
81
81
  // short of its limit forever and refuse nothing.
82
82
  const alreadySent = options?.alreadySent === true;
83
83
  const sendId = randomUUID();
84
- const decision = ledger().reserve({
84
+ const bookedLedger = ledger();
85
+ const scopes: SpendScopes = {
86
+ ...(rootId !== undefined ? { rootId } : {}),
87
+ // The existing privacy-safe log label is aliased again by the ledger.
88
+ ...(logCtx.accountLogLabel !== undefined ? { identityId: logCtx.accountLogLabel } : {}),
89
+ ...(logCtx.provider !== undefined ? { poolId: logCtx.provider } : {}),
90
+ };
91
+ const policy = bookedLedger.policy;
92
+ const enforced = (scopes.rootId !== undefined && policy.root.maxTokens !== undefined)
93
+ || (scopes.identityId !== undefined && policy.identity.maxTokens !== undefined)
94
+ || (scopes.poolId !== undefined && policy.pool.maxTokens !== undefined);
95
+ const decision = bookedLedger.reserve({
85
96
  sendId,
86
- scopes: {
87
- ...(rootId !== undefined ? { rootId } : {}),
88
- // Already the privacy-safe label the request log uses, and the ledger aliases it
89
- // again on the way to disk. A raw credential never reaches either.
90
- ...(logCtx.accountLogLabel !== undefined ? { identityId: logCtx.accountLogLabel } : {}),
91
- ...(logCtx.provider !== undefined ? { poolId: logCtx.provider } : {}),
92
- },
97
+ scopes,
93
98
  inputTokens: logCtx.spendInputEstimateTokens ?? logCtx.usageLogInputTokens ?? 0,
94
99
  outputCeilingTokens: logCtx.spendOutputCeilingTokens ?? 0,
95
100
  ...(alreadySent ? { alreadySent: true } : {}),
96
101
  });
97
102
  if (!decision.reserved) {
103
+ // An applicable ceiling cannot authorize an unbooked send, including when tracking
104
+ // capacity is full. Unconfigured/nonapplicable requests remain observe-only, and a
105
+ // physical send reported after dispatch cannot be refused retroactively.
106
+ if (alreadySent || !enforced) return true;
98
107
  refusals += 1;
99
108
  const denial = decision.denial;
100
- // A ceiling refuses, and so does a ledger that cannot make the reservation durable
101
- // under one. That second case is the whole reason this store is on disk: admitting a
102
- // send whose record a restart would forget is how an exhausted budget comes back with
103
- // a fresh allowance, and the ledger raises those two denials ONLY when a limit is
104
- // configured -- so an install that configured nothing is still never refused here.
105
- // Capacity and a duplicate send id stay permissive: they say the ledger cannot account
106
- // for this send, which is a degradation to report, not an outage to cause.
107
- if (denial.reason === "reserve-not-durable" || denial.reason === "journal-corrupt") {
108
- return alreadySent;
109
- }
110
- if (denial.reason !== "spend-limit-exceeded") return true;
111
- // This send is refused, and the dispatch path that asked will report an exhausted send
112
- // budget -- from there, that is all it can see. The row is where an operator actually
113
- // looks, so the ceiling is named on it here: a locally assigned code wins in
114
- // addFinalRequestLog, so the request that CROSSED the ceiling reads as a spend refusal
115
- // rather than as the ordinary budget exhaustion it would otherwise be indistinguishable
116
- // from. The event ring gets the same pair so /api/workflow-budget agrees with the row.
117
- const detail = { scope: denial.scope, limit: denial.limit, projected: denial.projected };
118
- const summary = workflowDenialSummary("workflow-spend-exhausted", detail);
109
+ const reason: WorkflowDenial = denial.reason === "duplicate-send-id"
110
+ ? "workflow-send-replayed"
111
+ : denial.reason === "reserve-not-durable" || denial.reason === "journal-corrupt"
112
+ ? "workflow-spend-undurable"
113
+ : denial.reason === "tracking-capacity-exhausted"
114
+ ? "workflow-tracking-exhausted"
115
+ : "workflow-spend-exhausted";
116
+ const detail = denial.reason === "spend-limit-exceeded"
117
+ ? { scope: denial.scope, limit: denial.limit, projected: denial.projected } : undefined;
118
+ const summary = workflowDenialSummary(reason, detail);
119
119
  markLocalRequestLogRefusal(logCtx, summary.code);
120
120
  logCtx.errorCode = summary.code;
121
- recordWorkflowRefusalEvent(rootId, "workflow-spend-exhausted", Date.now(), detail);
121
+ recordWorkflowRefusalEvent(rootId, reason, Date.now(), detail);
122
122
  return false;
123
123
  }
124
124
  live.push(sendId);
@@ -13,6 +13,7 @@ import {
13
13
  } from "../responses/custom-tool-compat";
14
14
  import {
15
15
  replaceSseDataPayload,
16
+ splitSseBlock,
16
17
  sseDataPayload,
17
18
  type SseBlockRewrite,
18
19
  } from "./sse-payload-rewrite";
@@ -22,8 +23,7 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
22
23
  }
23
24
 
24
25
  function replaceSseEventName(block: string, type: string): string {
25
- const newline = block.includes("\r\n") ? "\r\n" : "\n";
26
- const lines = block.split(/\r?\n/);
26
+ const { newline, lines } = splitSseBlock(block);
27
27
  let replaced = false;
28
28
  const next = lines.map(line => {
29
29
  if (!replaced && line.startsWith("event:")) {
@@ -2,6 +2,8 @@ import { mkdirSync, writeFileSync, unlinkSync } from "node:fs";
2
2
  import { createHash, randomUUID } from "node:crypto";
3
3
  import { join, posix } from "node:path";
4
4
  import { getConfigDir } from "../config";
5
+ import { jsonUtf8Bytes } from "../lib/json-byte-size";
6
+ import { TRANSLATOR_MAX_TURN_BYTES } from "../lib/translator-budget";
5
7
  import { recordOwnedConfigPath } from "../lib/config-ownership";
6
8
  import {
7
9
  chargeImageBudget, createImageBudget, decodeValidatedImageBase64,
@@ -12,6 +14,16 @@ import { sseDataPayload, replaceSseDataPayload, type SseBlockRewrite } from "./s
12
14
  type Row = Record<string, any>;
13
15
  const object = (v: unknown): v is Row => !!v && typeof v === "object" && !Array.isArray(v);
14
16
  const originators = new Set(["codex_cli_rs", "Codex Desktop", "codex_app", "codex_work_desktop"]);
17
+ const MAX_DISPLAY_ITEMS = 128;
18
+ const MAX_ITEM_METADATA_BYTES = 64 * 1024;
19
+ const MAX_RETAINED_METADATA_BYTES = 1024 * 1024;
20
+ const displayKey = (item: Row, index: number) => typeof item.id === "string" ? "id:" + item.id : "index:" + index;
21
+
22
+ function displayBytes(value: unknown, limit: number): number {
23
+ try { return jsonUtf8Bytes(value, limit); }
24
+ catch { throw new RangeError("hosted image result exceeds local display limits"); }
25
+ }
26
+
15
27
  const generatedName = /^img-codex-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\.(png|jpg|webp|gif)$/;
16
28
 
17
29
  /** One spelling per local path: file URL or plain, either separator, dot segments, folded case. */
@@ -71,13 +83,20 @@ export function createHostedImageDisplayRewrite(): SseBlockRewrite & { json(text
71
83
  const states = new Map<string, { message: Row; added: boolean; done: boolean }>();
72
84
  let extraSequence = 0;
73
85
  let wroteArtifact = false;
86
+ let retainedMetadataBytes = 0;
74
87
 
75
88
  function stateFor(item: Row, index: number) {
76
- const key = typeof item.id === "string" ? item.id : "index:" + index;
89
+ // Validate every occurrence, including repeats whose state is already complete.
90
+ const metadataBytes = displayBytes({ id: item.id,
91
+ metadata: item.internal_chat_message_metadata_passthrough }, MAX_ITEM_METADATA_BYTES);
92
+ const key = displayKey(item, index);
77
93
  let state = states.get(key);
78
94
  if (state) return state;
79
- // Bound metadata independently of the existing 100 MiB decoded-image budget.
80
- if (states.size >= 128) throw new RangeError("hosted image result count exceeds local display limit");
95
+ if (states.size >= MAX_DISPLAY_ITEMS) throw new RangeError("hosted image result count exceeds local display limit");
96
+ if (metadataBytes > MAX_RETAINED_METADATA_BYTES - retainedMetadataBytes) {
97
+ throw new RangeError("hosted image result exceeds local display limits");
98
+ }
99
+ retainedMetadataBytes += metadataBytes;
81
100
  const id = "msg_ocx_img_" + createHash("sha256").update(key).digest("hex").slice(0, 32);
82
101
  state = { added: false, done: false, message: {
83
102
  // Persist image results outside the client's collapsible progress messages.
@@ -147,15 +166,51 @@ export function createHostedImageDisplayRewrite(): SseBlockRewrite & { json(text
147
166
  return events;
148
167
  }
149
168
 
150
- function snapshot(response: Row, terminal: boolean, events?: Row[]): Row {
169
+ function projectedMessage(message: Row): Row {
170
+ if (message.status !== "in_progress") return message;
171
+ // Longest possible local filename and JSON text size, without decoding or writing an image.
172
+ const pathText = "![Generated image](<" + join(getArtifactsDir(), "img-codex-" + "0".repeat(36) + ".webp") + ">)";
173
+ const fallback = "The completed image result has no supported image data for local display.";
174
+ const text = displayBytes(pathText, TRANSLATOR_MAX_TURN_BYTES) > displayBytes(fallback, TRANSLATOR_MAX_TURN_BYTES)
175
+ ? pathText : fallback;
176
+ return { ...message, status: "incomplete", content: [{ type: "output_text", text, annotations: [] }] };
177
+ }
178
+
179
+ function lifecycleBytes(message: Row): number {
180
+ // At most two item copies and four text copies, plus fixed event fields, ids and numeric indices.
181
+ return 2 * displayBytes(message, TRANSLATOR_MAX_TURN_BYTES)
182
+ + 4 * displayBytes(message.content[0].text, TRANSLATOR_MAX_TURN_BYTES) + 2048;
183
+ }
184
+
185
+ function snapshot(response: Row, terminal: boolean, events?: Row[], envelope?: Row): Row {
151
186
  if (!Array.isArray(response.output)) return response;
152
- // Reject oversized snapshots before saving artifacts that cannot be delivered.
153
- const keys = new Set(states.keys());
187
+ // Preflight the complete snapshot before any artifact writes or expanded serialization.
188
+ const keys = new Set<string>();
189
+ const ids = new Set<string>();
154
190
  for (const [index, item] of response.output.entries()) {
155
191
  if (!object(item) || item.type !== "image_generation_call") continue;
156
- keys.add(typeof item.id === "string" ? item.id : "index:" + index);
157
- if (keys.size > 128) throw new RangeError("hosted image result count exceeds local display limit");
192
+ const key = displayKey(item, index);
193
+ if (keys.has(key)) throw new RangeError("duplicate hosted image identity in local display snapshot");
194
+ keys.add(key);
195
+ if (typeof item.id === "string") ids.add(item.id);
196
+ stateFor(item, index);
197
+ }
198
+ if (keys.size === 0) return response;
199
+ const seen = new Set<string>();
200
+ for (const item of response.output) {
201
+ if (!object(item) || !ids.has(item.id)) continue;
202
+ if (seen.has(item.id)) throw new RangeError("conflicting hosted image identity in local display snapshot");
203
+ seen.add(item.id);
158
204
  }
205
+ let extraBytes = 32; // Sequence-number growth on a rewritten SSE envelope.
206
+ const preview = { ...response, output: response.output.map((item: unknown, index: number) => {
207
+ if (!object(item) || item.type !== "image_generation_call") return item;
208
+ const state = stateFor(item, index);
209
+ const message = terminal ? projectedMessage(state.message) : state.message;
210
+ if (events && terminal && !state.done) extraBytes += lifecycleBytes(message);
211
+ return message;
212
+ }) };
213
+ displayBytes(envelope ? { ...envelope, response: preview } : preview, TRANSLATOR_MAX_TURN_BYTES - extraBytes);
159
214
  let changed = false;
160
215
  const output = response.output.map((item: unknown, index: number) => {
161
216
  if (!object(item) || item.type !== "image_generation_call") return item;
@@ -182,9 +237,14 @@ export function createHostedImageDisplayRewrite(): SseBlockRewrite & { json(text
182
237
  && (hasIndex || typeof event.item.id === "string")) {
183
238
  if (event.type === "response.output_item.added") {
184
239
  const state = stateFor(event.item, index);
240
+ if (!state.added) displayBytes({ ...event, item: state.message }, TRANSLATOR_MAX_TURN_BYTES - 32);
185
241
  result = state.added ? [] : [{ ...event, item: state.message }];
186
242
  state.added = true;
187
243
  } else if (event.type === "response.output_item.done") {
244
+ const state = stateFor(event.item, index);
245
+ if (!state.done && lifecycleBytes(projectedMessage(state.message)) > TRANSLATOR_MAX_TURN_BYTES) {
246
+ throw new RangeError("hosted image result exceeds local display limits");
247
+ }
188
248
  result = doneEvents(event.item, index);
189
249
  }
190
250
  } else if (typeof event.type === "string" && event.type.startsWith("response.image_generation_call.")) {
@@ -193,7 +253,7 @@ export function createHostedImageDisplayRewrite(): SseBlockRewrite & { json(text
193
253
  } else if (object(event.response)) {
194
254
  const terminal = ["response.completed", "response.incomplete", "response.failed"].includes(event.type);
195
255
  const injected: Row[] = [];
196
- const response = snapshot(event.response, terminal, injected);
256
+ const response = snapshot(event.response, terminal, injected, event);
197
257
  if (response !== event.response) result = [...injected, { ...event, response }];
198
258
  }
199
259
  const sequence = Number.isSafeInteger(event.sequence_number) ? event.sequence_number + extraSequence : undefined;
@@ -216,6 +276,7 @@ export function createHostedImageDisplayRewrite(): SseBlockRewrite & { json(text
216
276
  };
217
277
  rewrite.dispose = () => {
218
278
  states.clear();
279
+ retainedMetadataBytes = 0;
219
280
  if (wroteArtifact) {
220
281
  wroteArtifact = false;
221
282
  pruneArtifacts();
@@ -91,6 +91,7 @@ export function relayResponsesSseWithTerminalRepair(
91
91
  let maxSequence = -1;
92
92
  let buffer = "";
93
93
  let bufferBytes = 0;
94
+ let pendingLineFeed = false;
94
95
  let timer: unknown;
95
96
  let timerGeneration = 0;
96
97
  let realTerminalSeen = false;
@@ -284,6 +285,7 @@ export function relayResponsesSseWithTerminalRepair(
284
285
  let emitted = false;
285
286
  while ((next = nextSseBlock(buffer))) {
286
287
  replaceBuffer(next.rest);
288
+ pendingLineFeed = next.delimiter.endsWith("\r") && buffer.length === 0;
287
289
  const kind = inspectPayload(sseDataPayload(next.block));
288
290
  if (kind === "done" && !realTerminalSeen) {
289
291
  emitSynthetic(completeCandidate() ? "completed" : "incomplete", controller);
@@ -327,9 +329,20 @@ export function relayResponsesSseWithTerminalRepair(
327
329
  controller.close();
328
330
  return;
329
331
  }
330
- appendBuffer(decoder.decode(value, { stream: true }));
332
+ let fragment = decoder.decode(value, { stream: true });
333
+ let continuedDelimiter = false;
334
+ if (pendingLineFeed && fragment.length > 0) {
335
+ pendingLineFeed = false;
336
+ if (fragment.startsWith("\n")) {
337
+ // This byte completes an already-forwarded CR delimiter, not a new EOF suffix.
338
+ controller.enqueue(encoder.encode("\n"));
339
+ fragment = fragment.slice(1);
340
+ continuedDelimiter = true;
341
+ }
342
+ }
343
+ appendBuffer(fragment);
331
344
  const result = emitBlocks(controller);
332
- if (result.closed || result.emitted) return;
345
+ if (result.closed || result.emitted || continuedDelimiter) return;
333
346
  }
334
347
  } catch (error) {
335
348
  if (disposed) return;