mixdog 0.9.151 → 0.9.153

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 (307) hide show
  1. package/LICENSES/browser-import-NOTICE.txt +11 -0
  2. package/NOTICE.md +11 -0
  3. package/README.md +108 -68
  4. package/package.json +17 -4
  5. package/scripts/computer-task-cost-report.mjs +144 -0
  6. package/scripts/run-office-live-tests.mjs +32 -0
  7. package/scripts/tool-stress.mjs +0 -2
  8. package/src/agents/front-worker/AGENT.md +13 -0
  9. package/src/agents/front-worker/agent.json +6 -0
  10. package/src/agents/heavy-worker/AGENT.md +10 -11
  11. package/src/agents/heavy-worker/agent.json +1 -1
  12. package/src/agents/maintainer/AGENT.md +0 -2
  13. package/src/agents/reviewer/AGENT.md +9 -13
  14. package/src/agents/reviewer/agent.json +1 -1
  15. package/src/agents/security/AGENT.md +14 -0
  16. package/src/agents/security/agent.json +6 -0
  17. package/src/agents/worker/AGENT.md +8 -8
  18. package/src/agents/worker/agent.json +1 -1
  19. package/src/agents/writer/AGENT.md +14 -0
  20. package/src/agents/writer/agent.json +6 -0
  21. package/src/defaults/skills/setup/SKILL.md +18 -27
  22. package/src/headless-exec.mjs +3 -10
  23. package/src/headless-exec.test.mjs +2 -7
  24. package/src/rules/agent/00-common.md +4 -7
  25. package/src/rules/lead/01-general.md +0 -3
  26. package/src/rules/shared/10-tool-workflow.md +10 -2
  27. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +183 -26
  28. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.test.mjs +378 -0
  29. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +4 -0
  30. package/src/runtime/agent/orchestrator/cache-break-trace.mjs +109 -0
  31. package/src/runtime/agent/orchestrator/cache-break-trace.test.mjs +115 -0
  32. package/src/runtime/agent/orchestrator/config.mjs +13 -68
  33. package/src/runtime/agent/orchestrator/context/collect-skills.test.mjs +153 -0
  34. package/src/runtime/agent/orchestrator/context/collect.mjs +49 -45
  35. package/src/runtime/agent/orchestrator/mcp/client.mjs +242 -15
  36. package/src/runtime/agent/orchestrator/mcp/features.test.mjs +66 -0
  37. package/src/runtime/agent/orchestrator/mcp/security.test.mjs +36 -20
  38. package/src/runtime/agent/orchestrator/providers/admission-scheduler-metrics.test.mjs +47 -0
  39. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +16 -3
  40. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +54 -33
  41. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -0
  42. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
  43. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +7 -0
  44. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +3 -8
  45. package/src/runtime/agent/orchestrator/providers/gemini.mjs +7 -0
  46. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +19 -3
  47. package/src/runtime/agent/orchestrator/providers/lib/provider-replay.mjs +50 -0
  48. package/src/runtime/agent/orchestrator/providers/lib/sse-framing.mjs +93 -0
  49. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +30 -9
  50. package/src/runtime/agent/orchestrator/providers/media-parity.test.mjs +59 -0
  51. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +3 -3
  52. package/src/runtime/agent/orchestrator/providers/openai-codex-identity.test.mjs +71 -0
  53. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +18 -26
  54. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +8 -0
  55. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +7 -0
  56. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +25 -2
  57. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +35 -4
  58. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +47 -2
  59. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +103 -12
  60. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +13 -0
  61. package/src/runtime/agent/orchestrator/providers/openai-turn-state.mjs +68 -0
  62. package/src/runtime/agent/orchestrator/providers/openai-ws-delta-parity.test.mjs +122 -6
  63. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +36 -11
  64. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +144 -93
  65. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +18 -13
  66. package/src/runtime/agent/orchestrator/providers/provider-replay.test.mjs +288 -0
  67. package/src/runtime/agent/orchestrator/providers/sse-framing.test.mjs +719 -0
  68. package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +399 -39
  69. package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +26 -0
  70. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +86 -7
  71. package/src/runtime/agent/orchestrator/session/agent-loop.test.mjs +222 -0
  72. package/src/runtime/agent/orchestrator/session/browser-snapshot-supersession.mjs +61 -0
  73. package/src/runtime/agent/orchestrator/session/browser-snapshot-supersession.test.mjs +83 -0
  74. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +13 -0
  75. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +26 -2
  76. package/src/runtime/agent/orchestrator/session/compact-policy.test.mjs +112 -0
  77. package/src/runtime/agent/orchestrator/session/compaction-read-reset.test.mjs +192 -0
  78. package/src/runtime/agent/orchestrator/session/context-usage-restart.test.mjs +72 -0
  79. package/src/runtime/agent/orchestrator/session/context-utils.mjs +100 -15
  80. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +17 -1
  81. package/src/runtime/agent/orchestrator/session/eager-dispatch.test.mjs +74 -0
  82. package/src/runtime/agent/orchestrator/session/image-strip-recovery.mjs +12 -8
  83. package/src/runtime/agent/orchestrator/session/image-strip-recovery.test.mjs +20 -1
  84. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +110 -2
  85. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +3 -16
  86. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.test.mjs +28 -0
  87. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +2 -2
  88. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +6 -0
  89. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +15 -1
  90. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +6 -0
  91. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +111 -39
  92. package/src/runtime/agent/orchestrator/session/manager/ask-session.test.mjs +46 -0
  93. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +30 -6
  94. package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +3 -0
  95. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +27 -3
  96. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +26 -54
  97. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.test.mjs +63 -1
  98. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +3 -0
  99. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +4 -110
  100. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +580 -0
  101. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.test.mjs +593 -0
  102. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +119 -190
  103. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +115 -0
  104. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +43 -8
  105. package/src/runtime/agent/orchestrator/session/provider-prefix-guard.mjs +129 -12
  106. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +9 -2
  107. package/src/runtime/agent/orchestrator/session/store/listing.mjs +8 -1
  108. package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +15 -1
  109. package/src/runtime/agent/orchestrator/session/store-live-publication.test.mjs +83 -0
  110. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +36 -2
  111. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +157 -30
  112. package/src/runtime/agent/orchestrator/session/store-summary-reader.test.mjs +64 -0
  113. package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +57 -0
  114. package/src/runtime/agent/orchestrator/session/store-summary-visibility.test.mjs +219 -0
  115. package/src/runtime/agent/orchestrator/session/store.mjs +26 -2
  116. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +22 -0
  117. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool-cwd-env.test.mjs +14 -0
  118. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -10
  119. package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +30 -14
  120. package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +15 -4
  121. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +73 -15
  122. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +76 -2
  123. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +24 -683
  124. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +64 -72
  125. package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +2 -5
  126. package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.mjs +75 -0
  127. package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.test.mjs +24 -0
  128. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +6 -6
  129. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +12 -0
  130. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +4 -1
  131. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +24 -5
  132. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.test.mjs +44 -0
  133. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +12 -2
  134. package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +35 -21
  135. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +28 -13
  136. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.test.mjs +54 -0
  137. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
  138. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  139. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +12 -0
  140. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
  141. package/src/runtime/attachments/pdf-extract.mjs +3 -0
  142. package/src/runtime/attachments/pdfjs-runtime.mjs +13 -0
  143. package/src/runtime/bridge-clients.test.mjs +827 -0
  144. package/src/runtime/browser-bridge/action-schema.mjs +251 -0
  145. package/src/runtime/browser-bridge/client.mjs +152 -0
  146. package/src/runtime/browser-bridge/tool-defs.mjs +159 -0
  147. package/src/runtime/computer-bridge/action-schema.mjs +653 -0
  148. package/src/runtime/computer-bridge/client.mjs +231 -0
  149. package/src/runtime/computer-bridge/tool-defs.mjs +33 -0
  150. package/src/runtime/media/store.mjs +18 -8
  151. package/src/runtime/media/store.test.mjs +14 -0
  152. package/src/runtime/memory/index.mjs +1 -1
  153. package/src/runtime/memory/lib/transcript-ingest.mjs +8 -6
  154. package/src/runtime/memory/lib/transcript-ingest.test.mjs +52 -0
  155. package/src/runtime/office/assurance-benchmark.mjs +358 -0
  156. package/src/runtime/office/assurance.mjs +833 -0
  157. package/src/runtime/office/benchmark.mjs +496 -0
  158. package/src/runtime/office/capabilities.mjs +551 -0
  159. package/src/runtime/office/com-adapter.mjs +283 -0
  160. package/src/runtime/office/content-model.mjs +183 -0
  161. package/src/runtime/office/contract-benchmark.mjs +105 -0
  162. package/src/runtime/office/design-library.mjs +1562 -0
  163. package/src/runtime/office/design-system.mjs +1966 -0
  164. package/src/runtime/office/index.mjs +2413 -0
  165. package/src/runtime/office/journal.mjs +82 -0
  166. package/src/runtime/office/office-assurance.test.mjs +371 -0
  167. package/src/runtime/office/office-com-host.ps1 +3781 -0
  168. package/src/runtime/office/office-com-session-host.ps1 +855 -0
  169. package/src/runtime/office/office-live-runtime.test.mjs +819 -0
  170. package/src/runtime/office/office-runtime.test.mjs +2070 -0
  171. package/src/runtime/office/ooxml-validator.mjs +271 -0
  172. package/src/runtime/office/pagination.mjs +132 -0
  173. package/src/runtime/office/pdf-adapter.mjs +591 -0
  174. package/src/runtime/office/pdf-analysis.mjs +372 -0
  175. package/src/runtime/office/pdf-render.mjs +167 -0
  176. package/src/runtime/office/pdf-security.mjs +63 -0
  177. package/src/runtime/office/polish-benchmark.mjs +190 -0
  178. package/src/runtime/office/portable-ooxml.mjs +1867 -0
  179. package/src/runtime/office/quality-live-benchmark.mjs +486 -0
  180. package/src/runtime/office/quality-pipeline.mjs +136 -0
  181. package/src/runtime/office/tabular.mjs +302 -0
  182. package/src/runtime/office/templates/mixdog-executive.pptx +0 -0
  183. package/src/runtime/office/templates/mixdog-executive.pptx.mixdog.json +190 -0
  184. package/src/runtime/office/tool-defs.mjs +113 -0
  185. package/src/runtime/office/visual-diff.mjs +89 -0
  186. package/src/runtime/office/xlsx-assertions.mjs +172 -0
  187. package/src/runtime/office/xlsx-contract.mjs +147 -0
  188. package/src/runtime/shared/agent-route-config.mjs +9 -0
  189. package/src/runtime/shared/child-spawn-gate.mjs +17 -0
  190. package/src/runtime/shared/child-spawn-remote.mjs +69 -7
  191. package/src/runtime/shared/child-spawn-remote.test.mjs +124 -0
  192. package/src/runtime/shared/pristine-execution-contract.json +2 -1
  193. package/src/runtime/shared/provider-api-key.mjs +1 -0
  194. package/src/runtime/shared/session-runtime-health.mjs +91 -0
  195. package/src/runtime/shared/session-runtime-health.test.mjs +53 -0
  196. package/src/runtime/shared/skill-document.mjs +86 -0
  197. package/src/runtime/shared/skill-document.test.mjs +81 -0
  198. package/src/runtime/shared/tool-execution-contract.mjs +16 -0
  199. package/src/runtime/shared/tool-surface.mjs +27 -0
  200. package/src/runtime/shared/turn-snapshot-store.mjs +1 -0
  201. package/src/runtime/shared/turn-snapshot.mjs +32 -9
  202. package/src/runtime/shared/user-cwd.mjs +33 -14
  203. package/src/runtime/shared/user-cwd.test.mjs +43 -0
  204. package/src/session-runtime/agent-disable.test.mjs +20 -1
  205. package/src/session-runtime/context-status.mjs +9 -3
  206. package/src/session-runtime/cwd-plugins.mjs +26 -26
  207. package/src/session-runtime/cwd-plugins.test.mjs +75 -0
  208. package/src/session-runtime/cwd-tool-routing.test.mjs +5 -0
  209. package/src/session-runtime/deferred-tool-delta.mjs +104 -0
  210. package/src/session-runtime/deferred-tool-delta.test.mjs +40 -0
  211. package/src/session-runtime/global-extensions.mjs +26 -0
  212. package/src/session-runtime/global-extensions.test.mjs +28 -0
  213. package/src/session-runtime/global-settings-session-identity.test.mjs +76 -0
  214. package/src/session-runtime/goal-runtime.mjs +1141 -0
  215. package/src/session-runtime/goal-runtime.test.mjs +645 -0
  216. package/src/session-runtime/lifecycle-api.mjs +58 -11
  217. package/src/session-runtime/lifecycle-api.test.mjs +183 -0
  218. package/src/session-runtime/mcp-glue.mjs +49 -47
  219. package/src/session-runtime/plugin-mcp-standard.test.mjs +177 -0
  220. package/src/session-runtime/plugin-mcp.mjs +96 -22
  221. package/src/session-runtime/prewarm.mjs +1 -10
  222. package/src/session-runtime/provider-request-snapshot.mjs +33 -9
  223. package/src/session-runtime/provider-request-snapshot.test.mjs +27 -5
  224. package/src/session-runtime/resource-api-global.test.mjs +109 -0
  225. package/src/session-runtime/resource-api.mjs +168 -116
  226. package/src/session-runtime/runtime-core.mjs +208 -29
  227. package/src/session-runtime/runtime-tunables.mjs +6 -8
  228. package/src/session-runtime/session-lifecycle.mjs +34 -12
  229. package/src/session-runtime/session-title.mjs +2 -2
  230. package/src/session-runtime/session-turn-api.mjs +37 -25
  231. package/src/session-runtime/skills-api.mjs +79 -25
  232. package/src/session-runtime/skills-api.test.mjs +99 -0
  233. package/src/session-runtime/tool-catalog-data.mjs +1 -1
  234. package/src/session-runtime/tool-catalog-schema.mjs +0 -5
  235. package/src/session-runtime/tool-catalog.mjs +42 -112
  236. package/src/session-runtime/tool-defs.mjs +6 -1
  237. package/src/session-runtime/tool-policy-surface.test.mjs +93 -17
  238. package/src/session-runtime/tool-surface.mjs +30 -8
  239. package/src/session-runtime/workflow-agents-api.mjs +8 -25
  240. package/src/session-runtime/workflow.mjs +8 -6
  241. package/src/standalone/agent-dispatch-broker.mjs +5 -9
  242. package/src/standalone/agent-tool/helpers.mjs +2 -2
  243. package/src/standalone/agent-tool/job-views.mjs +3 -1
  244. package/src/standalone/agent-tool/notify.mjs +11 -9
  245. package/src/standalone/agent-tool/notify.test.mjs +37 -0
  246. package/src/standalone/agent-tool/render.mjs +3 -0
  247. package/src/standalone/agent-tool/spawn-flow.mjs +61 -16
  248. package/src/standalone/agent-tool/spawn-flow.test.mjs +41 -0
  249. package/src/standalone/agent-tool/tool-def.mjs +1 -1
  250. package/src/standalone/agent-tool/worker-index.mjs +123 -0
  251. package/src/standalone/agent-tool/worker-index.test.mjs +132 -0
  252. package/src/standalone/agent-tool/worker-rows.mjs +2 -1
  253. package/src/standalone/agent-tool.mjs +57 -10
  254. package/src/standalone/daemon.mjs +113 -15
  255. package/src/standalone/hook-bus/config.mjs +1 -0
  256. package/src/standalone/plugin-admin.mjs +17 -0
  257. package/src/standalone/plugin-admin.test.mjs +7 -0
  258. package/src/standalone/provider-admin.mjs +1 -0
  259. package/src/standalone/session-protocol.mjs +5 -0
  260. package/src/standalone/session-runtime-agent-control-client.mjs +133 -0
  261. package/src/standalone/session-runtime-agent-control-client.test.mjs +71 -0
  262. package/src/standalone/session-runtime-dispatch-cancel.test.mjs +139 -0
  263. package/src/standalone/session-runtime-host-factory.mjs +17 -0
  264. package/src/standalone/session-runtime-host-factory.test.mjs +31 -0
  265. package/src/standalone/session-runtime-host-health.test.mjs +5 -0
  266. package/src/standalone/session-runtime-host.mjs +709 -58
  267. package/src/standalone/session-runtime-inline-host.mjs +258 -0
  268. package/src/standalone/session-runtime-inline-host.test.mjs +385 -0
  269. package/src/standalone/session-runtime-provider-cooldown.mjs +78 -0
  270. package/src/standalone/session-runtime-provider-cooldown.test.mjs +80 -0
  271. package/src/standalone/session-runtime-shard-host.test.mjs +642 -0
  272. package/src/standalone/session-runtime-shard-router.mjs +149 -0
  273. package/src/standalone/session-runtime-shard-router.test.mjs +101 -0
  274. package/src/standalone/session-runtime-worker.mjs +302 -26
  275. package/src/standalone/session-service-agent-tree.test.mjs +984 -0
  276. package/src/standalone/session-service-goal-recovery.test.mjs +177 -0
  277. package/src/standalone/session-service.mjs +711 -11
  278. package/src/tui/app/app-view.jsx +1 -0
  279. package/src/tui/app/core-memory-picker.mjs +2 -0
  280. package/src/tui/app/extension-pickers.mjs +1 -1
  281. package/src/tui/app/maintenance-pickers.mjs +3 -3
  282. package/src/tui/app/model-options.mjs +2 -0
  283. package/src/tui/app/model-picker.mjs +14 -4
  284. package/src/tui/app/panel-handoff.test.mjs +311 -0
  285. package/src/tui/app/project-picker.mjs +1 -0
  286. package/src/tui/app/route-pickers.mjs +21 -6
  287. package/src/tui/app/settings-picker.mjs +26 -3
  288. package/src/tui/app/slash-commands.mjs +1 -0
  289. package/src/tui/app/slash-dispatch.mjs +73 -22
  290. package/src/tui/app/theme-effort-pickers.mjs +7 -2
  291. package/src/tui/components/Picker.jsx +10 -1
  292. package/src/tui/dist/index.mjs +187 -45
  293. package/src/tui/lib/voice-setup.mjs +4 -1
  294. package/src/tui/session/agent-message-source.test.mjs +37 -0
  295. package/src/tui/session/completion-card-restore.test.mjs +50 -0
  296. package/src/tui/session/context-state.mjs +3 -13
  297. package/src/tui/session/goal-continuation.mjs +141 -0
  298. package/src/tui/session/goal-continuation.test.mjs +203 -0
  299. package/src/tui/session/queue-helpers.mjs +5 -2
  300. package/src/tui/session/session-action-surface.test.mjs +190 -0
  301. package/src/tui/session/session-api-ext.mjs +48 -8
  302. package/src/tui/session/session-api.mjs +73 -1
  303. package/src/tui/session/session-flow.mjs +31 -1
  304. package/src/tui/session/turn.mjs +88 -15
  305. package/src/tui/session-local.mjs +39 -3
  306. package/src/vendor/statusline/src/gateway/route-meta.mjs +31 -0
  307. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +0 -301
@@ -13,6 +13,12 @@ import {
13
13
  _withCodexWsClientMetadata,
14
14
  } from './openai-codex-metadata.mjs';
15
15
  import { codexOriginator, codexUserAgent } from './codex-client-meta.mjs';
16
+ import { _captureTurnStateFromEvent } from './openai-ws-stream.mjs';
17
+ import { _buildOpenAIHttpFallbackHeaders } from './openai-oauth-http-sse.mjs';
18
+ import {
19
+ _clearCodexTurnStatesForTest,
20
+ retireCodexTurnStateOwner,
21
+ } from './openai-turn-state.mjs';
16
22
 
17
23
  test('Codex wire identity is a real time-based UUIDv7 and remains session-stable', () => {
18
24
  const now = Date.now();
@@ -151,3 +157,68 @@ test('compaction shares the session identity and cache slot with its turns', ()
151
157
  assert.equal(turnMetadata.session_id, turn.codexSessionId);
152
158
  assert.equal(turnMetadata.thread_id, turn.codexThreadId);
153
159
  });
160
+
161
+ test('Codex turn state follows the logical turn across WS reconnect and HTTP fallback', (t) => {
162
+ _clearCodexTurnStatesForTest();
163
+ t.after(_clearCodexTurnStatesForTest);
164
+ const poolKey = 'turn-state-parity-session';
165
+ const cacheKey = mintUuidV7();
166
+ const turnId = mintUuidV7();
167
+ const nextTurnId = mintUuidV7();
168
+ const context = {
169
+ poolKey,
170
+ cacheKey,
171
+ sendOpts: { turnId },
172
+ };
173
+
174
+ const firstEntry = {};
175
+ const firstFrame = _withCodexWsClientMetadata({}, firstEntry, true, context);
176
+ assert.equal(firstFrame.client_metadata['x-codex-turn-state'], undefined);
177
+
178
+ _captureTurnStateFromEvent(firstEntry, {
179
+ type: 'response.metadata',
180
+ headers: { 'x-codex-turn-state': 'ts-first' },
181
+ });
182
+ _captureTurnStateFromEvent(firstEntry, {
183
+ type: 'response.metadata',
184
+ headers: { 'x-codex-turn-state': 'ts-later' },
185
+ });
186
+
187
+ const siblingEntry = {};
188
+ const siblingFrame = _withCodexWsClientMetadata({}, siblingEntry, true, context);
189
+ assert.equal(siblingFrame.client_metadata['x-codex-turn-state'], undefined);
190
+
191
+ const httpHeaders = _buildOpenAIHttpFallbackHeaders({
192
+ auth: { type: 'openai-oauth', access_token: 'token', account_id: 'account' },
193
+ cacheKey,
194
+ poolKey,
195
+ turnId,
196
+ });
197
+ assert.equal(httpHeaders['x-codex-turn-state'], 'ts-first');
198
+
199
+ retireCodexTurnStateOwner(poolKey, firstEntry);
200
+ const reconnectEntry = {};
201
+ const reconnectFrame = _withCodexWsClientMetadata({}, reconnectEntry, true, context);
202
+ assert.equal(reconnectFrame.client_metadata['x-codex-turn-state'], 'ts-first');
203
+
204
+ const nextTurnFrame = _withCodexWsClientMetadata({}, reconnectEntry, true, {
205
+ ...context,
206
+ sendOpts: { turnId: nextTurnId },
207
+ });
208
+ assert.equal(nextTurnFrame.client_metadata['x-codex-turn-state'], undefined);
209
+ const nextTurnHttpHeaders = _buildOpenAIHttpFallbackHeaders({
210
+ auth: { type: 'openai-oauth', access_token: 'token', account_id: 'account' },
211
+ cacheKey,
212
+ poolKey,
213
+ turnId: nextTurnId,
214
+ });
215
+ assert.equal(nextTurnHttpHeaders['x-codex-turn-state'], undefined);
216
+
217
+ const directHeaders = _buildOpenAIHttpFallbackHeaders({
218
+ auth: { type: 'openai-direct', apiKey: 'key' },
219
+ cacheKey,
220
+ poolKey,
221
+ turnId,
222
+ });
223
+ assert.equal(directHeaders['x-codex-turn-state'], undefined);
224
+ });
@@ -6,6 +6,7 @@ import { createHash, randomUUID } from 'crypto';
6
6
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
7
7
  import { join } from 'path';
8
8
  import { getPluginData } from '../config.mjs';
9
+ import { activateCodexTurnState } from './openai-turn-state.mjs';
9
10
 
10
11
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
11
12
  let _installationId = null;
@@ -206,39 +207,30 @@ export function _codexWsCompatibilityHeaders(context = {}) {
206
207
  export function _withCodexWsClientMetadata(frame, entry, enabled, context = {}) {
207
208
  if (!enabled || !frame || typeof frame !== 'object') return frame;
208
209
  const base = _codexMetadataBase(entry, context);
209
- const requestKind = _codexRequestKind(context?.sendOpts, context?.poolKey || '');
210
- const isPrewarmRequest = requestKind === 'prewarm';
210
+ const explicitLogicalTurnId = _cleanMetaString(
211
+ context?.sendOpts?.turnId
212
+ || context?.sendOpts?.codexTurnId
213
+ || context?.sendOpts?.session?.turnId,
214
+ );
215
+ const logicalTurnId = explicitLogicalTurnId
216
+ ? _codexUuidV7(explicitLogicalTurnId)
217
+ : base.turn_id;
218
+ const turnStateScope = _cleanMetaString(context?.poolKey);
211
219
  const metadata = {
212
220
  ...base,
213
221
  ...(frame.client_metadata && typeof frame.client_metadata === 'object' ? frame.client_metadata : {}),
214
222
  'x-codex-ws-stream-request-start-ms': String(Date.now()),
215
223
  };
216
224
  if (entry && typeof entry === 'object') {
217
- // x-codex-turn-state is scoped to ONE turn while
218
- // pooled sockets span turns: attribute a captured token to the FIRST
219
- // turn that observes it, then drop it once turn_id moves on. An empty
220
- // turn_id (parity prewarm) is a valid owner, so the check is against
221
- // null rather than falsiness.
222
- if (entry.turnState) {
223
- if (entry.turnStateTurnId == null) {
224
- entry.turnStateTurnId = base.turn_id;
225
- } else if (entry.turnStateTurnId !== base.turn_id) {
226
- // Codex startup prewarm owns the handshake turn-state until
227
- // the first real turn consumes that prewarmed client session.
228
- // Adopt it once across prewarm→turn; ordinary turn changes
229
- // still retire the old token.
230
- if (entry.turnStateFromPrewarm === true && !isPrewarmRequest) {
231
- entry.turnStateTurnId = base.turn_id;
232
- } else {
233
- entry.turnState = null;
234
- entry.turnStateTurnId = null;
235
- }
236
- }
237
- }
238
- if (!isPrewarmRequest && entry.turnStateFromPrewarm === true) {
239
- entry.turnStateFromPrewarm = false;
225
+ const priorTurnId = entry.turnStateTurnId;
226
+ if (turnStateScope && logicalTurnId) {
227
+ entry.turnState = activateCodexTurnState(turnStateScope, logicalTurnId, entry);
228
+ } else if (priorTurnId != null && priorTurnId !== logicalTurnId) {
229
+ entry.turnState = null;
240
230
  }
241
- entry.currentTurnId = base.turn_id;
231
+ entry.turnStateScope = turnStateScope || null;
232
+ entry.turnStateTurnId = logicalTurnId || null;
233
+ entry.currentTurnId = logicalTurnId || null;
242
234
  }
243
235
  if (entry?.turnState) metadata['x-codex-turn-state'] = String(entry.turnState);
244
236
  return { ...frame, client_metadata: metadata };
@@ -14,6 +14,14 @@ export const OPENAI_COMPAT_PRESETS = {
14
14
  baseURL: 'https://opencode.ai/zen/go/v1',
15
15
  defaultModel: 'glm-5.2',
16
16
  },
17
+ openrouter: {
18
+ baseURL: 'https://openrouter.ai/api/v1',
19
+ defaultModel: 'openrouter/auto',
20
+ extraHeaders: {
21
+ 'HTTP-Referer': 'https://github.com/tribgames/mixdog',
22
+ 'X-OpenRouter-Title': 'mixdog',
23
+ },
24
+ },
17
25
  ollama: {
18
26
  baseURL: 'http://localhost:11434/v1',
19
27
  defaultModel: 'llama3.3:latest',
@@ -296,6 +296,7 @@ export async function consumeCompatChatCompletionStream(stream, {
296
296
  let sawFirstEvent = false;
297
297
  let content = '';
298
298
  let reasoningContent = '';
299
+ const reasoningDetails = [];
299
300
  // Invariant flag for the gateway live-text relay: set once a non-empty
300
301
  // text chunk has been forwarded to the client. A failure after this point
301
302
  // must be treated as permanent — the rendered text cannot be withdrawn and
@@ -435,6 +436,10 @@ export async function consumeCompatChatCompletionStream(stream, {
435
436
  // use reasoning, and some local compatibility shims expose
436
437
  // thinking. They are aliases, never concatenate multiple aliases
437
438
  // from the same chunk.
439
+ if (Array.isArray(choice?.delta?.reasoning_details) && choice.delta.reasoning_details.length) {
440
+ reasoningDetails.push(...choice.delta.reasoning_details);
441
+ reportProgress('reasoning');
442
+ }
438
443
  const reasoningDelta = typeof choice?.delta?.reasoning_content === 'string'
439
444
  ? choice.delta.reasoning_content
440
445
  : typeof choice?.delta?.reasoning === 'string'
@@ -513,6 +518,7 @@ export async function consumeCompatChatCompletionStream(stream, {
513
518
  const message = {
514
519
  content: content || null,
515
520
  ...(reasoningContent ? { reasoning_content: reasoningContent } : {}),
521
+ ...(reasoningDetails.length ? { reasoning_details: reasoningDetails } : {}),
516
522
  };
517
523
  const rawToolCalls = [...toolAcc.values()]
518
524
  .sort((a, b) => a._order - b._order)
@@ -557,6 +563,7 @@ export async function consumeCompatChatCompletionStream(stream, {
557
563
  toolCalls,
558
564
  stopReason,
559
565
  reasoningContent: reasoningContent || null,
566
+ reasoningDetails: reasoningDetails.length ? reasoningDetails : null,
560
567
  rawUsage,
561
568
  };
562
569
  }
@@ -20,6 +20,7 @@ import {
20
20
  import {
21
21
  customToolCallFromResponseItem,
22
22
  } from './custom-tool-wire.mjs';
23
+ import { providerReplayItems } from './lib/provider-replay.mjs';
23
24
 
24
25
  export function positiveTokenInt(value) {
25
26
  const n = Number(value);
@@ -56,6 +57,12 @@ export function toOpenAIMessages(messages, providerName, options = {}) {
56
57
  || providerName === 'xai';
57
58
  const out = [];
58
59
  const pendingToolMedia = [];
60
+ const openRouterReasoningDetails = (message) => {
61
+ if (providerName !== 'openrouter') return null;
62
+ const metadata = message?.providerMetadata?.openrouter;
63
+ const details = metadata?.reasoning_details ?? metadata?.reasoningDetails;
64
+ return Array.isArray(details) && details.length ? details : null;
65
+ };
59
66
  const flushToolMedia = () => {
60
67
  if (!pendingToolMedia.length) return;
61
68
  out.push({ role: 'user', content: pendingToolMedia.splice(0) });
@@ -83,11 +90,20 @@ export function toOpenAIMessages(messages, providerName, options = {}) {
83
90
  })),
84
91
  };
85
92
  if (replaysReasoningContent && m.reasoningContent) msg.reasoning_content = m.reasoningContent;
93
+ const reasoningDetails = openRouterReasoningDetails(m);
94
+ if (reasoningDetails) msg.reasoning_details = reasoningDetails;
86
95
  out.push(msg);
87
96
  continue;
88
97
  }
89
- if (m.role === 'assistant' && replaysReasoningContent && m.reasoningContent) {
90
- out.push({ role: m.role, content: normalizeContentForOpenAIChat(m.content, { role: 'assistant' }), reasoning_content: m.reasoningContent });
98
+ const reasoningDetails = m.role === 'assistant' ? openRouterReasoningDetails(m) : null;
99
+ if (m.role === 'assistant' && ((replaysReasoningContent && m.reasoningContent) || reasoningDetails)) {
100
+ const msg = {
101
+ role: m.role,
102
+ content: normalizeContentForOpenAIChat(m.content, { role: 'assistant' }),
103
+ };
104
+ if (replaysReasoningContent && m.reasoningContent) msg.reasoning_content = m.reasoningContent;
105
+ if (reasoningDetails) msg.reasoning_details = reasoningDetails;
106
+ out.push(msg);
91
107
  continue;
92
108
  }
93
109
  out.push({ role: m.role, content: normalizeContentForOpenAIChat(m.content, { role: m.role }) });
@@ -392,6 +408,13 @@ export function toXaiResponsesInput(messages, providerState, options = {}) {
392
408
  const m = messages[messageIndex];
393
409
  if (!includeSystem && m.role === 'system') continue;
394
410
  if (m.role !== 'tool') flushToolMedia();
411
+ const orderedReplay = m.role === 'assistant'
412
+ ? providerReplayItems(m, 'xai-responses')
413
+ : undefined;
414
+ if (orderedReplay?.length) {
415
+ input.push(...orderedReplay);
416
+ continue;
417
+ }
395
418
  const reasoningItems = reasoningByMessageIndex.get(messageIndex);
396
419
  if (reasoningItems) input.push(...reasoningItems);
397
420
  if (dropToolHistory && m.role === 'tool') continue;
@@ -64,6 +64,7 @@ import {
64
64
  writeXaiResponsesCacheTrace,
65
65
  } from './openai-compat-xai.mjs';
66
66
  import { envFlag as _envFlag } from './lib/env-utils.mjs';
67
+ import { createProviderReplay } from './lib/provider-replay.mjs';
67
68
 
68
69
  const requireOpenAI = createRequire(import.meta.url);
69
70
  let _OpenAI = null;
@@ -168,10 +169,33 @@ function normalizeReasoningEffort(value, allowed) {
168
169
  return allowed.includes(effort) ? effort : null;
169
170
  }
170
171
 
172
+ export function compatReportedCostUsd(providerName, usage) {
173
+ if (providerName === 'openrouter') {
174
+ const cost = Number(usage?.cost);
175
+ return Number.isFinite(cost) && cost >= 0 ? cost : undefined;
176
+ }
177
+ if (providerName === 'xai') {
178
+ const ticks = Number(usage?.cost_in_usd_ticks);
179
+ return Number.isFinite(ticks) && ticks >= 0
180
+ ? Number((ticks * 1e-10).toFixed(8))
181
+ : undefined;
182
+ }
183
+ return undefined;
184
+ }
185
+
171
186
  // Keep provider extensions isolated: fields accepted by one OpenAI-compatible
172
187
  // backend are frequently rejected by another even when the core Chat schema is
173
188
  // shared.
174
189
  export function applyCompatProviderChatOptions(params, providerName, opts = {}, config = {}, modelInfo = null) {
190
+ if (providerName === 'openrouter') {
191
+ const effort = normalizeReasoningEffort(
192
+ opts.openRouterReasoningEffort ?? opts.effort ?? config?.reasoningEffort,
193
+ ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'],
194
+ );
195
+ if (effort === 'none') params.reasoning = { enabled: false };
196
+ else if (effort) params.reasoning = { enabled: true, effort };
197
+ return params;
198
+ }
175
199
  if (providerName === 'xai') {
176
200
  const reasoningEffort = normalizeXaiReasoningEffort(opts.xaiReasoningEffort
177
201
  ?? opts.effort
@@ -594,6 +618,9 @@ export class OpenAICompatProvider {
594
618
  const reasoningContent = (capturesReasoningContent && typeof assembled.reasoningContent === 'string')
595
619
  ? assembled.reasoningContent
596
620
  : null;
621
+ const reasoningDetails = this.name === 'openrouter' && Array.isArray(choice?.message?.reasoning_details)
622
+ ? choice.message.reasoning_details
623
+ : null;
597
624
  return {
598
625
  content: assembled.content || '',
599
626
  // Streamed chunks can omit `model`; fall back to the requested
@@ -609,6 +636,9 @@ export class OpenAICompatProvider {
609
636
  // instead of silently treating a truncated answer as complete.
610
637
  ...(stopReason === 'length' && (assembled.content || '').length > 0 ? { truncated: true } : {}),
611
638
  ...(reasoningContent ? { reasoningContent } : {}),
639
+ ...(reasoningDetails?.length ? {
640
+ providerMetadata: { openrouter: { reasoning_details: reasoningDetails } },
641
+ } : {}),
612
642
  usage: response.usage ? (() => {
613
643
  const input = response.usage.prompt_tokens ?? response.usage.input_tokens ?? 0;
614
644
  const cached = extractCompatCachedTokens(response.usage);
@@ -616,10 +646,7 @@ export class OpenAICompatProvider {
616
646
  // (1 tick = $1e-10, per docs.x.ai). Surface it as costUsd so the
617
647
  // session manager skips the catalog-rate fallback and records the
618
648
  // provider-billed value verbatim.
619
- const ticks = response.usage.cost_in_usd_ticks;
620
- const costUsd = typeof ticks === 'number' && ticks >= 0
621
- ? Number((ticks * 1e-10).toFixed(8))
622
- : undefined;
649
+ const costUsd = compatReportedCostUsd(this.name, response.usage);
623
650
  return {
624
651
  inputTokens: input,
625
652
  outputTokens: response.usage.completion_tokens ?? response.usage.output_tokens ?? 0,
@@ -884,6 +911,7 @@ export class OpenAICompatProvider {
884
911
  // any rejection as a provider-side drop.)
885
912
  const nextPreviousResponseId = response.id;
886
913
  const encryptedReasoningItems = encryptedXaiReasoningItems(response.output);
914
+ const providerReplay = createProviderReplay('xai-responses', response.output);
887
915
  const encryptedReasoningHistory = [
888
916
  ...(Array.isArray(opts.providerState?.xaiResponses?.encryptedReasoningHistory)
889
917
  ? opts.providerState.xaiResponses.encryptedReasoningHistory
@@ -904,6 +932,7 @@ export class OpenAICompatProvider {
904
932
  ...(streamed.stopReason === 'length' && (streamed.content || '').length > 0 ? { truncated: true } : {}),
905
933
  citations: searchSources.citations.length ? searchSources.citations : undefined,
906
934
  webSearchCalls: searchSources.webSearchCalls.length ? searchSources.webSearchCalls : undefined,
935
+ providerReplay,
907
936
  providerState: {
908
937
  ...(opts.providerState || {}),
909
938
  xaiResponses: {
@@ -1022,6 +1051,7 @@ export class OpenAICompatProvider {
1022
1051
  // Same rationale as the HTTP path above: `length` stop keeps the chain.
1023
1052
  const nextPreviousResponseId = responseId;
1024
1053
  const encryptedReasoningItems = encryptedXaiReasoningItems(result.responseItems);
1054
+ const providerReplay = createProviderReplay('xai-responses', result.responseItems);
1025
1055
  const encryptedReasoningHistory = [
1026
1056
  ...(Array.isArray(opts.providerState?.xaiResponses?.encryptedReasoningHistory)
1027
1057
  ? opts.providerState.xaiResponses.encryptedReasoningHistory
@@ -1080,6 +1110,7 @@ export class OpenAICompatProvider {
1080
1110
  // P1 audit fix: same truncated signal as the HTTP path (see
1081
1111
  // _doSendXaiResponses above) for the WebSocket transport.
1082
1112
  ...(result.stopReason === 'length' && (result.content || '').length > 0 ? { truncated: true } : {}),
1113
+ providerReplay,
1083
1114
  providerState: {
1084
1115
  ...(opts.providerState || {}),
1085
1116
  xaiResponses: {
@@ -38,8 +38,13 @@ import { createLeakGuard, createToolCallDedupe, dedupeToolCallList } from './ant
38
38
  import { customToolCallFromResponseItem } from './custom-tool-wire.mjs';
39
39
  import { CODEX_OAUTH_ORIGINATOR, CODEX_RESPONSES_URL, _displayCodexModel } from './openai-oauth.mjs';
40
40
  import { createActiveToolItemTracker } from './tool-stream-state.mjs';
41
+ import { createProviderReplay } from './lib/provider-replay.mjs';
41
42
  import { parseProviderJsonBatch } from './stream-json-pool.mjs';
42
43
  import { envFlag as _envFlag } from './lib/env-utils.mjs';
44
+ import {
45
+ activateCodexTurnState,
46
+ captureCodexTurnState,
47
+ } from './openai-turn-state.mjs';
43
48
  export { envPositiveInt as _envPositiveInt } from './lib/env-utils.mjs';
44
49
  export { _envFlag };
45
50
 
@@ -163,7 +168,13 @@ function _pushOutputTextAnnotations(part, citations, citationKeys) {
163
168
  }
164
169
  }
165
170
 
166
- function _buildOpenAIHttpFallbackHeaders({ auth, cacheKey, statelessConversation = false }) {
171
+ export function _buildOpenAIHttpFallbackHeaders({
172
+ auth,
173
+ cacheKey,
174
+ statelessConversation = false,
175
+ poolKey = null,
176
+ turnId = null,
177
+ }) {
167
178
  if (auth?.type === 'openai-direct') {
168
179
  // Public API-key auth: Bearer <OPENAI_API_KEY>, no chatgpt-account-id /
169
180
  // originator (mirrors openai-ws-pool _buildHandshakeHeaders' direct
@@ -194,6 +205,8 @@ function _buildOpenAIHttpFallbackHeaders({ auth, cacheKey, statelessConversation
194
205
  headers['session-id'] = sid;
195
206
  headers['thread-id'] = sid;
196
207
  }
208
+ const turnState = activateCodexTurnState(poolKey, turnId);
209
+ if (turnState) headers['x-codex-turn-state'] = turnState;
197
210
  return headers;
198
211
  }
199
212
 
@@ -250,7 +263,14 @@ export async function sendViaHttpSse({
250
263
  const totalTimeout = createPassthroughSignal(externalSignal);
251
264
  const statelessConversation = opts?.statelessConversation === true
252
265
  || _envFlag('MIXDOG_OAI_STATELESS_HTTP', false);
253
- const headers = _buildOpenAIHttpFallbackHeaders({ auth, cacheKey, statelessConversation });
266
+ const turnId = opts?.turnId || opts?.codexTurnId || opts?.session?.turnId || null;
267
+ const headers = _buildOpenAIHttpFallbackHeaders({
268
+ auth,
269
+ cacheKey,
270
+ statelessConversation,
271
+ poolKey,
272
+ turnId,
273
+ });
254
274
  const fetchStartedAt = Date.now();
255
275
  const responsesUrl = auth?.type === 'openai-direct'
256
276
  ? OPENAI_DIRECT_RESPONSES_URL
@@ -379,6 +399,10 @@ export async function sendViaHttpSse({
379
399
  totalTimeout.cleanup();
380
400
  throw err;
381
401
  }
402
+ if (auth?.type !== 'openai-direct') {
403
+ const responseTurnState = response.headers?.get?.('x-codex-turn-state');
404
+ if (responseTurnState) captureCodexTurnState(poolKey, turnId, responseTurnState);
405
+ }
382
406
  if (!response.body) {
383
407
  totalTimeout.cleanup();
384
408
  throw Object.assign(new Error('OpenAI OAuth HTTP fallback returned no response body'), { initialResponseError: true });
@@ -504,6 +528,8 @@ export async function sendViaHttpSse({
504
528
  const clearActiveToolItem = _toolTracker.clear;
505
529
  let _toolInFlight = false;
506
530
  const reasoningItems = [];
531
+ const responseItems = [];
532
+ const responseItemKeys = new Set();
507
533
  const citations = [];
508
534
  const citationKeys = new Set();
509
535
  const webSearchCalls = [];
@@ -663,6 +689,16 @@ export async function sendViaHttpSse({
663
689
  });
664
690
  }
665
691
  };
692
+ const pushResponseItem = (item) => {
693
+ if (!item || typeof item !== 'object') return;
694
+ let fallbackKey = '';
695
+ try { fallbackKey = JSON.stringify(item); } catch {}
696
+ const key = `${item.type || 'unknown'}:${item.id || item.call_id || fallbackKey}`;
697
+ if (responseItemKeys.has(key)) return;
698
+ responseItemKeys.add(key);
699
+ try { responseItems.push(structuredClone(item)); }
700
+ catch { responseItems.push({ ...item }); }
701
+ };
666
702
  const pushToolSearchCall = (item) => {
667
703
  if (!item || item.type !== 'tool_search_call') return;
668
704
  const callId = item.call_id || item.id || '';
@@ -800,6 +836,7 @@ export async function sendViaHttpSse({
800
836
  break;
801
837
  case 'response.output_item.done': {
802
838
  const item = event.item || {};
839
+ pushResponseItem(item);
803
840
  pushReasoningItem(item);
804
841
  pushWebSearchCall(item);
805
842
  if (item.type === 'function_call') {
@@ -852,6 +889,7 @@ export async function sendViaHttpSse({
852
889
  }
853
890
  let reportedBundleProgress = false;
854
891
  for (const item of resp.output || []) {
892
+ pushResponseItem(item);
855
893
  if (item.type === 'message') {
856
894
  for (const part of item.content || []) {
857
895
  if (!content && part.type === 'output_text') {
@@ -1060,6 +1098,11 @@ export async function sendViaHttpSse({
1060
1098
  // trailing text is never lost (streamed-text path).
1061
1099
  flushLeak();
1062
1100
  } catch (err) {
1101
+ if (err && !err.partialProviderReplay) {
1102
+ try {
1103
+ err.partialProviderReplay = createProviderReplay('openai-responses', responseItems);
1104
+ } catch {}
1105
+ }
1063
1106
  // Live-text invariant: once a non-empty chunk has been relayed it
1064
1107
  // cannot be withdrawn — flag the error so no upstream layer retries.
1065
1108
  if (emittedText && err) { try { err.liveTextEmitted = true; err.unsafeToRetry = true; } catch {} }
@@ -1100,6 +1143,7 @@ export async function sendViaHttpSse({
1100
1143
  try {
1101
1144
  err.partialContent = content;
1102
1145
  err.partialToolCalls = toolCalls.length ? toolCalls.slice() : undefined;
1146
+ err.partialProviderReplay = createProviderReplay('openai-responses', responseItems);
1103
1147
  err.partialModel = model || undefined;
1104
1148
  err.pendingToolUse = pendingCalls.size > 0 || activeToolItems.size > 0 || _toolInFlight === true;
1105
1149
  } catch { /* best-effort enrichment */ }
@@ -1141,6 +1185,7 @@ export async function sendViaHttpSse({
1141
1185
  content,
1142
1186
  model: liveModel,
1143
1187
  reasoningItems: reasoningItems.length ? reasoningItems : undefined,
1188
+ providerReplay: createProviderReplay('openai-responses', responseItems),
1144
1189
  toolCalls: _returnedToolCalls,
1145
1190
  citations: citations.length ? citations : undefined,
1146
1191
  webSearchCalls: webSearchCalls.length ? webSearchCalls : undefined,