@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
@@ -0,0 +1,49 @@
1
+ import { devinCacheIdentity } from "./cloud-direct/chat";
2
+
3
+ const TRAJECTORY_MAX = 256;
4
+ const trajectories = new Map<string, { id: string; active: boolean }>();
5
+
6
+ export interface DevinTrajectoryClaim {
7
+ trajectoryId?: string;
8
+ release(): void;
9
+ }
10
+
11
+ /** One process-local trajectory per named conversation, never shared by live turns. */
12
+ export function claimDevinTrajectory(
13
+ apiKey: string,
14
+ host: string,
15
+ conversation: string | null | undefined,
16
+ parentThreadId?: string,
17
+ ): DevinTrajectoryClaim {
18
+ if (!conversation) return { release() {} };
19
+ // Tag both forms so a standalone JSON-like ID cannot alias a parent/own pair.
20
+ const identity = parentThreadId
21
+ ? ["codex-child", parentThreadId, conversation]
22
+ : ["standalone", conversation];
23
+ // Only the fixed digest and UUID enter the retained map, never the token or conversation.
24
+ const key = new Bun.CryptoHasher("sha256")
25
+ .update(JSON.stringify([devinCacheIdentity(apiKey, host), identity])).digest("hex");
26
+ let entry = trajectories.get(key);
27
+ if (entry?.active) return { trajectoryId: crypto.randomUUID(), release() {} };
28
+ if (!entry) {
29
+ if (trajectories.size >= TRAJECTORY_MAX) {
30
+ const inactive = [...trajectories].find(([, candidate]) => !candidate.active);
31
+ if (!inactive) return { trajectoryId: crypto.randomUUID(), release() {} };
32
+ trajectories.delete(inactive[0]);
33
+ }
34
+ entry = { id: crypto.randomUUID(), active: true };
35
+ }
36
+ entry.active = true;
37
+ trajectories.delete(key);
38
+ trajectories.set(key, entry);
39
+ const claimed = entry;
40
+ let released = false;
41
+ return {
42
+ trajectoryId: claimed.id,
43
+ release() {
44
+ if (released) return;
45
+ released = true;
46
+ claimed.active = false;
47
+ },
48
+ };
49
+ }
@@ -18,6 +18,7 @@ import { DEVIN_DEFAULT_API_SERVER, resolveDevinApiServer } from "../oauth/devin"
18
18
  import { devinAssistantReasoning, encodeDevinSignature, hasAnthropicSignature } from "./devin/reasoning-signature";
19
19
  import { SendBudgetExhaustedError } from "../lib/upstream-retry";
20
20
  import { devinContextOverflowEvent, isDevinHistoryOverflow } from "./devin/context-overflow";
21
+ import { claimDevinTrajectory, type DevinTrajectoryClaim } from "./devin/trajectory";
21
22
 
22
23
  /**
23
24
  * Combine two usage frames from one turn by keeping the larger count per field.
@@ -743,6 +744,7 @@ export function createDevinAdapter(
743
744
  let contextWindow: number | undefined;
744
745
  let messages: ChatHistoryItem[] = [];
745
746
  let tools: ToolDef[] | undefined;
747
+ let trajectory: DevinTrajectoryClaim | undefined;
746
748
 
747
749
  const closeOpenTool = () => {
748
750
  if (!openToolId) return;
@@ -751,6 +753,14 @@ export function createDevinAdapter(
751
753
  };
752
754
 
753
755
  try {
756
+ const ownThreadId = incoming.headers.get("thread-id")?.trim() || parsed._codexOwnThreadId?.trim();
757
+ const conversation = ownThreadId
758
+ || incoming.headers.get("session_id")?.trim() || incoming.headers.get("session-id")?.trim()
759
+ || incoming.headers.get("x-session-affinity")?.trim()
760
+ || (!incoming.headers.has("x-codex-parent-thread-id") ? parsed._clientThreadId : undefined);
761
+ trajectory = claimDevinTrajectory(apiKey, host, conversation,
762
+ ownThreadId ? incoming.headers.get("x-codex-parent-thread-id")?.trim() || undefined : undefined);
763
+ const trajectoryId = trajectory.trajectoryId;
754
764
  // Read the selected UID's catalog row, not the picker's collapsed base.
755
765
  contextWindow = resolveDevinContextWindow(provider, modelUid, catalog?.byUid.get(modelUid));
756
766
  messages = mapOcxMessagesToDevin(parsed);
@@ -780,6 +790,7 @@ export function createDevinAdapter(
780
790
  messages,
781
791
  tools,
782
792
  cascadeId,
793
+ trajectoryId,
783
794
  completionOpts: {
784
795
  ...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
785
796
  ...(typeof parsed.options.temperature === "number" ? { temperature: parsed.options.temperature } : {}),
@@ -961,6 +972,8 @@ export function createDevinAdapter(
961
972
  ...(error instanceof CloudChatError && error.code ? { code: error.code } : {}),
962
973
  ...(usage ? { usage } : {}),
963
974
  });
975
+ } finally {
976
+ trajectory?.release();
964
977
  }
965
978
  },
966
979
  };
@@ -71,12 +71,13 @@ interface PendingToolCall {
71
71
  namespace?: string;
72
72
  wireName: string;
73
73
  order: number;
74
- result?: OcxMessage & { role: "toolResult" };
74
+ results: Array<OcxMessage & { role: "toolResult" }>;
75
75
  }
76
76
 
77
77
  interface PendingToolBatch {
78
78
  calls: PendingToolCall[];
79
79
  byId: Map<string, PendingToolCall>;
80
+ unresolvedCount: number;
80
81
  }
81
82
 
82
83
  interface NativeStreamToolCall {
@@ -302,6 +303,12 @@ function assistantTextThinkingAndCalls(message: OcxAssistantMessage): {
302
303
  };
303
304
  }
304
305
 
306
+ /**
307
+ * Build native replay messages without mutating the parsed history. Keep tool results
308
+ * beside their originating batch, deferring intervening conversation until settlement.
309
+ * Reserve replayed call IDs for response translation and mark missing results explicitly.
310
+ * @throws When call IDs are invalid or results are orphaned or mismatched.
311
+ */
305
312
  function buildNativeMessages(
306
313
  parsed: OcxParsedRequest,
307
314
  reservedToolCallIds: Set<string>,
@@ -317,25 +324,27 @@ function buildNativeMessages(
317
324
  // owned by this adapter/request lifecycle rather than process-global state.
318
325
  reservedToolCallIds.clear();
319
326
  let pending: PendingToolBatch | undefined;
327
+ const issuedCalls = new Map<string, PendingToolCall>();
320
328
  // Codex records mid-turn injections (a PostToolUse hook verdict, a context notice) between an
321
329
  // assistant tool call and that call's own tool result. Native Ollama needs the call and its
322
330
  // results adjacent, so those conversational messages wait here instead of closing the batch
323
331
  // early. The openai-chat adapter defers them the same way; refusing the replay killed the turn.
324
332
  let deferred: OllamaNativeMessage[] = [];
325
333
 
334
+ /** Emit held conversation messages in their recorded arrival order after settlement. */
326
335
  const releaseDeferred = (): void => {
327
336
  if (deferred.length === 0) return;
328
337
  messages.push(...deferred);
329
338
  deferred = [];
330
339
  };
331
340
 
341
+ /** Settle the open batch with genuine results or unknown markers, then release deferred messages. */
332
342
  const flushPending = (): void => {
333
343
  if (!pending) return;
334
344
  for (const call of pending.calls) {
335
- if (!call.result) {
336
- // No result exists anywhere in the replayed history: the turn was interrupted, or the
337
- // result never reached it. State exactly that instead of inventing an outcome, and keep
338
- // the conversation replayable.
345
+ if (call.results.length === 0) {
346
+ // No result was available when this batch settled. A later notification is carried
347
+ // separately, not used to invent an outcome at this earlier conversation boundary.
339
348
  messages.push({
340
349
  role: "tool",
341
350
  tool_call_id: call.id,
@@ -347,13 +356,18 @@ function buildNativeMessages(
347
356
  });
348
357
  continue;
349
358
  }
350
- const translated = contentToNative(call.result.content, "tool result");
359
+ // A code-mode cell can yield, then notify repeatedly. Join once at settlement rather
360
+ // than repeatedly copying an ever-growing result, and never mutate the source history.
361
+ const fragments = call.results.map(result => ({
362
+ ...contentToNative(result.content, "tool result"), isError: result.isError,
363
+ }));
364
+ const images = fragments.flatMap(fragment => fragment.images ?? []);
351
365
  messages.push({
352
366
  role: "tool",
353
367
  tool_call_id: call.id,
354
368
  tool_name: call.wireName,
355
- content: translated.content,
356
- ...(translated.images ? { images: translated.images } : {}),
369
+ content: fragments.map(fragment => `${fragment.isError ? "ERROR: " : ""}${fragment.content}`).join("\n"),
370
+ ...(images.length ? { images } : {}),
357
371
  });
358
372
  }
359
373
  pending = undefined;
@@ -362,27 +376,37 @@ function buildNativeMessages(
362
376
 
363
377
  for (const message of parsed.context.messages) {
364
378
  if (message.role === "toolResult") {
365
- if (!pending) {
366
- throw new Error(`ollama-native orphan tool result ${message.toolCallId || "<missing-id>"}`);
367
- }
368
- const call = pending.byId.get(message.toolCallId);
379
+ const call = issuedCalls.get(message.toolCallId);
369
380
  if (!call) {
370
- throw new Error(`ollama-native tool result ${message.toolCallId || "<missing-id>"} has no originating call`);
371
- }
372
- if (call.result) {
373
- throw new Error(`ollama-native duplicate tool result for ${message.toolCallId}`);
381
+ throw new Error(pending
382
+ ? `ollama-native tool result ${message.toolCallId || "<missing-id>"} has no originating call`
383
+ : `ollama-native orphan tool result ${message.toolCallId || "<missing-id>"}`);
374
384
  }
375
385
  if (call.name !== message.toolName || call.namespace !== message.toolNamespace) {
376
386
  throw new Error(`ollama-native tool result ${message.toolCallId} names the wrong originating tool`);
377
387
  }
378
- call.result = message;
388
+ if (pending?.byId.has(call.id)) {
389
+ if (call.results.length === 0) pending.unresolvedCount--;
390
+ call.results.push(message);
391
+ } else {
392
+ // Reopening a settled batch would split a later call/result pair. Keep known late
393
+ // output explicitly attributed as conversation text instead of dropping it or
394
+ // fabricating another executable call. Unknown/mismatched identities still fail above.
395
+ const translated = contentToNative(message.content, "late tool result");
396
+ const late: OllamaNativeMessage = { role: "user",
397
+ content: `[ocx] additional output for previously issued tool "${call.wireName}" (${call.id}):\n${message.isError ? "ERROR: " : ""}${translated.content}`,
398
+ ...(translated.images ? { images: translated.images } : {}),
399
+ };
400
+ if (pending) deferred.push(late);
401
+ else messages.push(late);
402
+ }
379
403
  continue;
380
404
  }
381
405
 
382
406
  // Native Ollama requires the whole assistant tool-call turn followed by its tool results. A
383
407
  // conversational message that arrives while the batch is still open is held aside instead of
384
408
  // closing it, so the call keeps its results adjacent; it is released right after the batch
385
- // flushes. Anything else (a new assistant turn) settles the batch first.
409
+ // flushes. A new assistant tool-call batch settles the preceding batch first.
386
410
  if (pending) {
387
411
  if (message.role === "user" || message.role === "developer") {
388
412
  const translated = message.role === "user"
@@ -393,6 +417,19 @@ function buildNativeMessages(
393
417
  : { role: "system", content: translated.content });
394
418
  continue;
395
419
  }
420
+ if (message.role === "assistant" && pending.unresolvedCount > 0) {
421
+ const extracted = assistantTextThinkingAndCalls(message);
422
+ if (extracted.calls.length === 0) {
423
+ // Commentary can follow the call items but precede their recorded results.
424
+ // Keep the batch open and release its text/thinking after the actual results.
425
+ deferred.push({
426
+ role: "assistant",
427
+ content: extracted.content,
428
+ ...(extracted.thinking ? { thinking: extracted.thinking } : {}),
429
+ });
430
+ continue;
431
+ }
432
+ }
396
433
  flushPending();
397
434
  }
398
435
 
@@ -427,8 +464,10 @@ function buildNativeMessages(
427
464
  namespace: call.namespace,
428
465
  wireName,
429
466
  order: index,
467
+ results: [],
430
468
  };
431
469
  wireCalls.push(pendingCall);
470
+ issuedCalls.set(call.id, pendingCall);
432
471
  return {
433
472
  type: "function" as const,
434
473
  id: call.id,
@@ -443,7 +482,11 @@ function buildNativeMessages(
443
482
  };
444
483
  messages.push(native);
445
484
  if (wireCalls.length > 0) {
446
- pending = { calls: wireCalls, byId: new Map(wireCalls.map(call => [call.id, call])) };
485
+ pending = {
486
+ calls: wireCalls,
487
+ byId: new Map(wireCalls.map(call => [call.id, call])),
488
+ unresolvedCount: wireCalls.length,
489
+ };
447
490
  }
448
491
  break;
449
492
  }
@@ -8,7 +8,7 @@ import { Buffer } from "node:buffer";
8
8
  import type { IncomingMeta, ProviderAdapter } from "../base";
9
9
  import { namespacedToolName, type AdapterEvent, type OcxParsedRequest, type OcxProviderConfig, type OcxUsage, type TierDecision } from "../../types";
10
10
  import { applyCodexRoutingHint, CODEX_RESPONSES_LITE_HEADER, CODEX_ROUTING_HINT_HEADER } from "../../codex/forward-transport-headers";
11
- import { COMPACT_PROMPT, compactionItemToText, decodeCompactionSummary, isCompactionItemType } from "../../responses/compaction";
11
+ import { COMPACT_PROMPT, compactionItemToText, decodeCompactionSummary, isCompactionItemType, transferCompactionCiphertextLease } from "../../responses/compaction";
12
12
  import { decodeServerSentEvents } from "../../lib/sse-decoder";
13
13
  import {
14
14
  CODEX_FORWARD_BASE_URL,
@@ -46,6 +46,7 @@ import { applyTierDecisionToResponsesBody, normalizeCanonicalForwardContinuation
46
46
  import { normalizeImageGenClientTools, preferConfiguredHostedTools } from "./image-gen";
47
47
  import { stripMuseSparkUnsupportedWebSearchFields, stripOpenAiOnlyWebSearchFields } from "./web-search";
48
48
  import { observeOutbound } from "../../usage/cache-diagnostic";
49
+ import { normalizeForwardedClientHeaderName } from "../../lib/provider-client-headers";
49
50
  import { normalizeMuseToolChoice } from "./muse-tool-choice";
50
51
 
51
52
  /**
@@ -94,6 +95,22 @@ function applyCallerUserAgentFallback(
94
95
  if (userAgent) headers["User-Agent"] = userAgent;
95
96
  }
96
97
 
98
+ /** Copy only explicitly opted-in caller metadata; provider-owned headers remain authoritative. */
99
+ function applyConfiguredClientHeaderForwarding(
100
+ headers: Record<string, string>,
101
+ incoming: IncomingMeta,
102
+ provider: OcxProviderConfig,
103
+ ): void {
104
+ if (!Array.isArray(provider.forwardClientHeaders)) return;
105
+ for (const rawName of provider.forwardClientHeaders.slice(0, 64)) {
106
+ const name = normalizeForwardedClientHeaderName(rawName);
107
+ if (name === null) continue;
108
+ if (Object.keys(headers).some(existing => existing.toLowerCase() === name)) continue;
109
+ const value = incoming.headers.get(name);
110
+ if (value) headers[name] = value;
111
+ }
112
+ }
113
+
97
114
  /** Replace every `input_image` part under a routed-compaction body with a short marker. */
98
115
  function stripInputImagesDeep(value: unknown): unknown {
99
116
  if (Array.isArray(value)) return value.map(stripInputImagesDeep);
@@ -252,8 +269,9 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
252
269
  if (provider.headers) Object.assign(headers, provider.headers);
253
270
  }
254
271
  // Some Responses-compatible gateways select their Codex compatibility path from the real
255
- // client fingerprint. This is a single non-credential fallback, not broader caller-header
256
- // forwarding. Static provider headers remain authoritative in either auth mode.
272
+ // client fingerprint. Additional caller metadata is opt-in; static provider headers remain
273
+ // authoritative in either auth mode.
274
+ applyConfiguredClientHeaderForwarding(headers, incoming, provider);
257
275
  applyCallerUserAgentFallback(headers, incoming);
258
276
 
259
277
  const forward = provider.authMode === "forward";
@@ -374,7 +392,9 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
374
392
  // OpenAI model inheriting history a routed provider had damaged.
375
393
  outBody = repairLegacyDottedToolCallNames(outBody);
376
394
  if (!isCanonicalOpenAiForwardProvider(provider)) {
377
- outBody = stripInternalChatMessageMetadataPassthrough(outBody);
395
+ if (provider.preserveResponsesMessageMetadata !== true) {
396
+ outBody = stripInternalChatMessageMetadataPassthrough(outBody);
397
+ }
378
398
  // The same class of private field, one level up, but keyed on the DESTINATION rather than
379
399
  // on the canonical surface alone. `src/server/responses/compact.ts` spreads the caller's
380
400
  // raw body into the native `/responses/compact` request without passing through this
@@ -486,6 +506,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
486
506
  ),
487
507
  ),
488
508
  isXaiResponsesDestination(provider),
509
+ provider.preserveResponsesInputItemIds === true,
489
510
  ),
490
511
  isXaiSchemaTarget(provider),
491
512
  );
@@ -590,112 +611,121 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
590
611
  let compactionEncryptedContent: string | undefined;
591
612
  let compactionEncryptedContentBytes = 0;
592
613
  let completedSeen = false;
593
- for await (const event of decodeServerSentEvents(response.body, { translatorBudget: budget })) {
594
- let payload: unknown;
595
- try { payload = JSON.parse(event.data); } catch { continue; }
596
- if (!isPlainObject(payload)) continue;
597
- switch (payload.type) {
598
- case "response.output_text.delta":
599
- if (typeof payload.delta === "string") {
600
- const next = deltas + payload.delta;
601
- const nextBytes = appendedUtf8Bytes(deltasBytes, deltasLastCodeUnit, payload.delta);
602
- const reservation = budget.reserveTransient(nextBytes, { kind: "retained_collectors" });
603
- deltas = next;
604
- reservation.commitRetained();
605
- budget.releaseRetained(deltasBytes, { kind: "retained_collectors" });
606
- deltasBytes = nextBytes;
607
- if (payload.delta.length > 0) deltasLastCodeUnit = payload.delta.charCodeAt(payload.delta.length - 1);
608
- }
609
- break;
610
- case "response.output_text.done":
611
- if (typeof payload.text === "string") {
612
- const next = doneText + payload.text;
613
- const nextBytes = appendedUtf8Bytes(doneTextBytes, doneTextLastCodeUnit, payload.text);
614
- const reservation = budget.reserveTransient(nextBytes, { kind: "retained_collectors" });
615
- doneText = next;
616
- reservation.commitRetained();
617
- budget.releaseRetained(doneTextBytes, { kind: "retained_collectors" });
618
- doneTextBytes = nextBytes;
619
- if (payload.text.length > 0) doneTextLastCodeUnit = payload.text.charCodeAt(payload.text.length - 1);
620
- }
621
- break;
622
- case "response.failed":
623
- case "error":
624
- yield { type: "error", message: responsesErrorMessage(payload.response ?? payload) };
625
- return;
626
- case "response.incomplete":
627
- yield { type: "incomplete", reason: responsesErrorMessage(payload.response ?? payload) };
628
- return;
629
- case "response.completed":
630
- {
631
- completedSeen = true;
632
- const responsePayload = isPlainObject(payload.response) ? payload.response : undefined;
633
- const output = Array.isArray(responsePayload?.output) ? responsePayload.output : [];
634
- const compaction = output.find(item => isPlainObject(item) && item.type === "compaction");
635
- if (isPlainObject(compaction) && typeof compaction.encrypted_content === "string") {
636
- const nextEncryptedContent = compaction.encrypted_content;
637
- const nextEncryptedContentBytes = Buffer.byteLength(nextEncryptedContent, "utf8");
638
- const reservation = budget.reserveTransient(nextEncryptedContentBytes, { kind: "retained_collectors" });
639
- compactionEncryptedContent = nextEncryptedContent;
614
+ let ciphertextTransferred = false;
615
+ try {
616
+ for await (const event of decodeServerSentEvents(response.body, { translatorBudget: budget })) {
617
+ let payload: unknown;
618
+ try { payload = JSON.parse(event.data); } catch { continue; }
619
+ if (!isPlainObject(payload)) continue;
620
+ switch (payload.type) {
621
+ case "response.output_text.delta":
622
+ if (typeof payload.delta === "string") {
623
+ const next = deltas + payload.delta;
624
+ const nextBytes = appendedUtf8Bytes(deltasBytes, deltasLastCodeUnit, payload.delta);
625
+ const reservation = budget.reserveTransient(nextBytes, { kind: "retained_collectors" });
626
+ deltas = next;
640
627
  reservation.commitRetained();
641
- budget.releaseRetained(compactionEncryptedContentBytes, { kind: "retained_collectors" });
642
- compactionEncryptedContentBytes = nextEncryptedContentBytes;
628
+ budget.releaseRetained(deltasBytes, { kind: "retained_collectors" });
629
+ deltasBytes = nextBytes;
630
+ if (payload.delta.length > 0) deltasLastCodeUnit = payload.delta.charCodeAt(payload.delta.length - 1);
643
631
  }
644
- const next = responsesPayloadText(payload.response);
645
- const nextBytes = Buffer.byteLength(next, "utf8");
646
- const reservation = budget.reserveTransient(nextBytes, { kind: "retained_collectors" });
647
- snapshot = next;
648
- reservation.commitRetained();
649
- budget.releaseRetained(snapshotBytes, { kind: "retained_collectors" });
650
- snapshotBytes = nextBytes;
651
- }
652
- {
653
- const nextUsage = usageFromResponsesPayload(payload.response);
654
- // The attached raw usage object can be event-sized (unknown keys carry arbitrary
655
- // values); it stays reachable until the terminal yields, so charge it like the
656
- // adjacent retained collectors or it would defeat the per-request memory cap.
657
- const nextRawBytes = nextUsage?.rawUsage === undefined ? 0
658
- : Buffer.byteLength(JSON.stringify(nextUsage.rawUsage), "utf8");
659
- if (nextRawBytes > 0) {
660
- const reservation = budget.reserveTransient(nextRawBytes, { kind: "retained_collectors" });
661
- usage = nextUsage;
632
+ break;
633
+ case "response.output_text.done":
634
+ if (typeof payload.text === "string") {
635
+ const next = doneText + payload.text;
636
+ const nextBytes = appendedUtf8Bytes(doneTextBytes, doneTextLastCodeUnit, payload.text);
637
+ const reservation = budget.reserveTransient(nextBytes, { kind: "retained_collectors" });
638
+ doneText = next;
662
639
  reservation.commitRetained();
663
- } else {
664
- usage = nextUsage;
640
+ budget.releaseRetained(doneTextBytes, { kind: "retained_collectors" });
641
+ doneTextBytes = nextBytes;
642
+ if (payload.text.length > 0) doneTextLastCodeUnit = payload.text.charCodeAt(payload.text.length - 1);
665
643
  }
666
- if (usageRawBytes > 0) {
667
- budget.releaseRetained(usageRawBytes, { kind: "retained_collectors" });
644
+ break;
645
+ case "response.failed":
646
+ case "error":
647
+ yield { type: "error", message: responsesErrorMessage(payload.response ?? payload) };
648
+ return;
649
+ case "response.incomplete":
650
+ yield { type: "incomplete", reason: responsesErrorMessage(payload.response ?? payload) };
651
+ return;
652
+ case "response.completed":
653
+ {
654
+ completedSeen = true;
655
+ const responsePayload = isPlainObject(payload.response) ? payload.response : undefined;
656
+ const output = Array.isArray(responsePayload?.output) ? responsePayload.output : [];
657
+ const compaction = output.find(item => isPlainObject(item) && item.type === "compaction");
658
+ if (isPlainObject(compaction) && typeof compaction.encrypted_content === "string") {
659
+ const nextEncryptedContent = compaction.encrypted_content;
660
+ const nextEncryptedContentBytes = Buffer.byteLength(nextEncryptedContent, "utf8");
661
+ const reservation = budget.reserveTransient(nextEncryptedContentBytes, { kind: "retained_collectors" });
662
+ compactionEncryptedContent = nextEncryptedContent;
663
+ reservation.commitRetained();
664
+ budget.releaseRetained(compactionEncryptedContentBytes, { kind: "retained_collectors" });
665
+ compactionEncryptedContentBytes = nextEncryptedContentBytes;
666
+ }
667
+ const next = responsesPayloadText(payload.response);
668
+ const nextBytes = Buffer.byteLength(next, "utf8");
669
+ const reservation = budget.reserveTransient(nextBytes, { kind: "retained_collectors" });
670
+ snapshot = next;
671
+ reservation.commitRetained();
672
+ budget.releaseRetained(snapshotBytes, { kind: "retained_collectors" });
673
+ snapshotBytes = nextBytes;
668
674
  }
669
- usageRawBytes = nextRawBytes;
670
- }
671
- break;
672
- }
673
- // Buffered text is still upstream progress, but gateway keepalives are not.
674
- // Yield after accounting, directly to the consumer: no progress queue or content leak.
675
- if (
676
- !completedSeen
677
- && (payload.type === "response.output_text.delta"
678
- || payload.type === "response.reasoning_summary_text.delta"
679
- || payload.type === "response.reasoning_text.delta")
680
- && typeof payload.delta === "string"
681
- && payload.delta.length > 0
682
- ) {
683
- yield { type: "heartbeat" };
675
+ {
676
+ const nextUsage = usageFromResponsesPayload(payload.response);
677
+ // The attached raw usage object can be event-sized (unknown keys carry arbitrary
678
+ // values); it stays reachable until the terminal yields, so charge it like the
679
+ // adjacent retained collectors or it would defeat the per-request memory cap.
680
+ const nextRawBytes = nextUsage?.rawUsage === undefined ? 0
681
+ : Buffer.byteLength(JSON.stringify(nextUsage.rawUsage), "utf8");
682
+ if (nextRawBytes > 0) {
683
+ const reservation = budget.reserveTransient(nextRawBytes, { kind: "retained_collectors" });
684
+ usage = nextUsage;
685
+ reservation.commitRetained();
686
+ } else {
687
+ usage = nextUsage;
688
+ }
689
+ if (usageRawBytes > 0) {
690
+ budget.releaseRetained(usageRawBytes, { kind: "retained_collectors" });
691
+ }
692
+ usageRawBytes = nextRawBytes;
693
+ }
694
+ break;
695
+ }
696
+ // Buffered text is still upstream progress, but gateway keepalives are not.
697
+ // Yield after accounting, directly to the consumer: no progress queue or content leak.
698
+ if (
699
+ !completedSeen
700
+ && (payload.type === "response.output_text.delta"
701
+ || payload.type === "response.reasoning_summary_text.delta"
702
+ || payload.type === "response.reasoning_text.delta")
703
+ && typeof payload.delta === "string"
704
+ && payload.delta.length > 0
705
+ ) {
706
+ yield { type: "heartbeat" };
707
+ }
684
708
  }
709
+ // Gateways differ in which of these they emit; prefer the authoritative
710
+ // completed snapshot so text is never double-counted.
711
+ const text = snapshot || doneText || deltas;
712
+ if (text) yield { type: "text_delta", text };
713
+ const done: AdapterEvent = {
714
+ type: "done",
715
+ ...(usage ? { usage } : {}),
716
+ ...(compactionEncryptedContent ? { compactionEncryptedContent } : {}),
717
+ };
718
+ // Exact event ownership survives buffered collection without charging a second copy.
719
+ transferCompactionCiphertextLease(done, budget, compactionEncryptedContentBytes);
720
+ ciphertextTransferred = true;
721
+ yield done;
722
+ } finally {
723
+ budget.releaseRetained(
724
+ deltasBytes + doneTextBytes + snapshotBytes + usageRawBytes
725
+ + (ciphertextTransferred ? 0 : compactionEncryptedContentBytes),
726
+ { kind: "retained_collectors" },
727
+ );
685
728
  }
686
- // Gateways differ in which of these they emit; prefer the authoritative
687
- // completed snapshot so text is never double-counted.
688
- const text = snapshot || doneText || deltas;
689
- if (text) yield { type: "text_delta", text };
690
- budget.releaseRetained(
691
- deltasBytes + doneTextBytes + snapshotBytes + usageRawBytes,
692
- { kind: "retained_collectors" },
693
- );
694
- yield {
695
- type: "done",
696
- ...(usage ? { usage } : {}),
697
- ...(compactionEncryptedContent ? { compactionEncryptedContent } : {}),
698
- };
699
729
  },
700
730
 
701
731
  async parseResponse(response: Response, budget: TranslatorBudget): Promise<AdapterEvent[]> {
@@ -191,12 +191,18 @@ export function stripRejectedSamplingParams(
191
191
  }
192
192
 
193
193
  /**
194
- * When `store` is false, the upstream API does not persist response items. Any item ID
194
+ * When `store: false`, the upstream cannot retrieve stored turns by ID. An ID
195
195
  * forwarded in `input` is then interpreted as a reference to a stored item that does not
196
- * exist, producing a 404. Strip all item IDs in this case — `call_id` pairing is unaffected.
196
+ * exist, producing a 404. Strip all item IDs in this case unless `preserveIds` is true
197
+ * — `call_id` pairing is unaffected. Also repairs missing or broken xAI custom_tool_call IDs
198
+ * when `requireCustomCallIds` is true, regardless of `preserveIds`.
197
199
  * Matches codex-rs behavior (core/src/client.rs:918-925).
198
200
  */
199
- export function stripItemIdsWhenUnstored(body: unknown, requireCustomCallIds = false): unknown {
201
+ export function stripItemIdsWhenUnstored(
202
+ body: unknown,
203
+ requireCustomCallIds = false,
204
+ preserveIds = false,
205
+ ): unknown {
200
206
  const repairCustomCallIds = requireCustomCallIds === true;
201
207
  if (!isPlainObject(body) || (body.store !== false && !repairCustomCallIds)) return body;
202
208
  if (!Array.isArray(body.input)) return body;
@@ -226,7 +232,7 @@ export function stripItemIdsWhenUnstored(body: unknown, requireCustomCallIds = f
226
232
  return item;
227
233
  }
228
234
  }
229
- if (body.store !== false || !("id" in item)) return item;
235
+ if (preserveIds || body.store !== false || !("id" in item)) return item;
230
236
  changed = true;
231
237
  const next = { ...item };
232
238
  delete next.id;
@@ -5,7 +5,8 @@ import { createGoogleAdapter } from "./google";
5
5
  import { createOpenAIChatAdapter } from "./openai-chat";
6
6
  import { createResponsesPassthroughAdapter } from "./openai-responses";
7
7
  import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig } from "../types";
8
- import { createTranslatorBudget, type TranslatorBudget } from "../lib/translator-budget";
8
+ import { createTranslatorBudget, releaseTranslatedEvent, retainTranslatedEvent, type TranslatorBudget } from "../lib/translator-budget";
9
+ import { releaseCompactionCiphertextLease } from "../responses/compaction";
9
10
  import { redactSecretString } from "../lib/redact";
10
11
  import {
11
12
  normalizeZedProvider,
@@ -365,12 +366,33 @@ export function createZedAdapter(provider: OcxProviderConfig): ProviderAdapter {
365
366
  status: response.status,
366
367
  headers: { "Content-Type": "text/event-stream" },
367
368
  });
368
- yield* delegate.adapter.parseStream(translated, budget);
369
+ try {
370
+ yield* delegate.adapter.parseStream(translated, budget);
371
+ } finally {
372
+ // Some delegates only unlock their reader when a collector stops early.
373
+ try { await translated.body?.cancel(); } catch { /* already closed/errored */ }
374
+ }
369
375
  },
370
376
  async parseResponse(response: Response, budget: TranslatorBudget): Promise<AdapterEvent[]> {
371
377
  const events: AdapterEvent[] = [];
372
- for await (const event of adapter.parseStream(response, budget)) events.push(event);
373
- return events;
378
+ try {
379
+ for await (const event of adapter.parseStream(response, budget)) {
380
+ try {
381
+ retainTranslatedEvent(event, budget, events.at(-1));
382
+ } catch (error) {
383
+ releaseCompactionCiphertextLease(event, budget);
384
+ throw error;
385
+ }
386
+ events.push(event);
387
+ }
388
+ return events;
389
+ } catch (error) {
390
+ for (const event of events) {
391
+ releaseTranslatedEvent(event, budget);
392
+ releaseCompactionCiphertextLease(event, budget);
393
+ }
394
+ throw error;
395
+ }
374
396
  },
375
397
  };
376
398
  return adapter;