mixdog 0.9.3 → 0.9.4

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 (329) hide show
  1. package/package.json +7 -3
  2. package/scripts/bench/lead-review-tasks-r3.json +20 -0
  3. package/scripts/bench/lead-review-tasks.json +20 -0
  4. package/scripts/bench/r4-mixed-tasks.json +20 -0
  5. package/scripts/bench/review-tasks.json +20 -0
  6. package/scripts/bench/round-codex.json +114 -0
  7. package/scripts/bench/round-mixdog-lead-r3.json +269 -0
  8. package/scripts/bench/round-mixdog-lead.json +269 -0
  9. package/scripts/bench/round-mixdog.json +126 -0
  10. package/scripts/bench/round-r10-bigsample.json +679 -0
  11. package/scripts/bench/round-r11-codexalign.json +257 -0
  12. package/scripts/bench/round-r4-codex.json +114 -0
  13. package/scripts/bench/round-r4-mixed.json +225 -0
  14. package/scripts/bench/round-r5-gpt-lead.json +259 -0
  15. package/scripts/bench/round-r6-codex.json +114 -0
  16. package/scripts/bench/round-r6-solo.json +257 -0
  17. package/scripts/bench/round-r7-full.json +254 -0
  18. package/scripts/bench/round-r8-fulldefault.json +255 -0
  19. package/scripts/bench-run.mjs +215 -29
  20. package/scripts/freevar-smoke.mjs +95 -0
  21. package/scripts/internal-comms-bench.mjs +1 -0
  22. package/scripts/internal-comms-smoke.mjs +10 -9
  23. package/scripts/mouse-probe.mjs +45 -0
  24. package/scripts/output-style-bench.mjs +13 -6
  25. package/scripts/output-style-smoke.mjs +4 -4
  26. package/scripts/provider-toolcall-test.mjs +7 -3
  27. package/scripts/recall-usecase-cases.json +18 -0
  28. package/scripts/recall-usecase-probe.json +6 -0
  29. package/scripts/session-bench.mjs +152 -6
  30. package/scripts/tool-smoke.mjs +23 -63
  31. package/scripts/tui-render-smoke.mjs +90 -0
  32. package/scripts/webhook-smoke.mjs +208 -0
  33. package/src/agents/debugger/AGENT.md +4 -1
  34. package/src/agents/heavy-worker/AGENT.md +6 -5
  35. package/src/agents/maintainer/AGENT.md +4 -0
  36. package/src/agents/reviewer/AGENT.md +2 -1
  37. package/src/agents/worker/AGENT.md +8 -4
  38. package/src/lib/rules-builder.cjs +4 -0
  39. package/src/mixdog-session-runtime.mjs +632 -2042
  40. package/src/output-styles/default.md +34 -9
  41. package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
  42. package/src/output-styles/minimal.md +4 -1
  43. package/src/output-styles/simple.md +22 -7
  44. package/src/rules/agent/00-common.md +2 -0
  45. package/src/rules/lead/lead-brief.md +12 -0
  46. package/src/rules/lead/lead-tool.md +0 -11
  47. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
  48. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
  49. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
  50. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
  51. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
  52. package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
  53. package/src/runtime/agent/orchestrator/config.mjs +69 -2
  54. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +62 -20
  55. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
  56. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
  57. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +81 -1281
  58. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
  59. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -3
  60. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
  61. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
  62. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
  63. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
  64. package/src/runtime/agent/orchestrator/providers/gemini.mjs +43 -1013
  65. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -3
  66. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +86 -11
  67. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +348 -0
  68. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
  69. package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
  70. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
  71. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
  72. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +40 -1143
  73. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +732 -0
  74. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
  75. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +297 -2123
  76. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +130 -1002
  77. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +227 -0
  78. package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
  79. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +436 -0
  80. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
  81. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
  82. package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
  83. package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
  84. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
  85. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
  86. package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
  87. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
  88. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
  89. package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
  90. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +14 -2
  91. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
  92. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -3
  93. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +182 -0
  94. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
  95. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
  96. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
  97. package/src/runtime/agent/orchestrator/session/loop.mjs +251 -397
  98. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
  99. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +7 -4
  100. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +12 -0
  101. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
  102. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
  103. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
  104. package/src/runtime/agent/orchestrator/session/manager.mjs +166 -1087
  105. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
  106. package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
  107. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
  108. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
  109. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +32 -41
  110. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
  111. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
  112. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
  113. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
  114. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -92
  115. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
  116. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
  117. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +28 -265
  118. package/src/runtime/agent/orchestrator/tools/builtin.mjs +0 -6
  119. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +57 -3
  120. package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
  121. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +10 -122
  122. package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
  123. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
  124. package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
  125. package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
  126. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
  127. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
  128. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
  129. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
  130. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
  131. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
  132. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
  133. package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
  134. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +0 -21
  135. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -72
  136. package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
  137. package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
  138. package/src/runtime/channels/backends/discord-access.mjs +32 -0
  139. package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
  140. package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
  141. package/src/runtime/channels/backends/discord.mjs +12 -292
  142. package/src/runtime/channels/index.mjs +229 -663
  143. package/src/runtime/channels/lib/backend-dispatch.mjs +44 -0
  144. package/src/runtime/channels/lib/event-pipeline.mjs +18 -1
  145. package/src/runtime/channels/lib/event-queue.mjs +63 -4
  146. package/src/runtime/channels/lib/inbound-routing.mjs +111 -0
  147. package/src/runtime/channels/lib/output-forwarder.mjs +1 -1
  148. package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
  149. package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
  150. package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
  151. package/src/runtime/channels/lib/session-discovery.mjs +56 -4
  152. package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
  153. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  154. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
  155. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
  156. package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
  157. package/src/runtime/channels/lib/webhook/deliveries.mjs +312 -0
  158. package/src/runtime/channels/lib/webhook/log.mjs +42 -0
  159. package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
  160. package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
  161. package/src/runtime/channels/lib/webhook.mjs +36 -570
  162. package/src/runtime/channels/tool-defs.mjs +11 -130
  163. package/src/runtime/memory/index.mjs +201 -1948
  164. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
  165. package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
  166. package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
  167. package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
  168. package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
  169. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
  170. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
  171. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
  172. package/src/runtime/memory/lib/memory-cycle2.mjs +24 -842
  173. package/src/runtime/memory/lib/memory-embed.mjs +149 -0
  174. package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
  175. package/src/runtime/memory/lib/memory-recall-store.mjs +22 -2
  176. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
  177. package/src/runtime/memory/lib/query-handlers.mjs +780 -0
  178. package/src/runtime/memory/lib/recall-format.mjs +55 -0
  179. package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
  180. package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
  181. package/src/runtime/memory/tool-defs.mjs +5 -13
  182. package/src/runtime/search/lib/http-fetch.mjs +274 -0
  183. package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
  184. package/src/runtime/search/lib/web-tools.mjs +24 -602
  185. package/src/runtime/shared/atomic-file.mjs +26 -1
  186. package/src/runtime/shared/launcher-control.mjs +2 -2
  187. package/src/runtime/shared/tool-primitives.mjs +308 -0
  188. package/src/runtime/shared/tool-result-summary.mjs +515 -0
  189. package/src/runtime/shared/tool-surface.mjs +80 -898
  190. package/src/runtime/shared/transcript-writer.mjs +23 -0
  191. package/src/runtime/shared/update-checker.mjs +7 -4
  192. package/src/session-runtime/config-helpers.mjs +84 -2
  193. package/src/session-runtime/config-lifecycle.mjs +232 -0
  194. package/src/session-runtime/cwd-plugins.mjs +226 -0
  195. package/src/session-runtime/mcp-glue.mjs +177 -0
  196. package/src/session-runtime/model-recency.mjs +111 -0
  197. package/src/session-runtime/native-search.mjs +247 -0
  198. package/src/session-runtime/output-styles.mjs +11 -9
  199. package/src/session-runtime/prewarm.mjs +142 -0
  200. package/src/session-runtime/provider-models.mjs +278 -0
  201. package/src/session-runtime/provider-usage.mjs +120 -0
  202. package/src/session-runtime/quick-model-rows.mjs +170 -0
  203. package/src/session-runtime/quick-search-models.mjs +46 -0
  204. package/src/session-runtime/session-hooks.mjs +93 -0
  205. package/src/session-runtime/settings-api.mjs +319 -0
  206. package/src/session-runtime/tool-catalog.mjs +29 -29
  207. package/src/session-runtime/tool-defs.mjs +84 -0
  208. package/src/session-runtime/warmup-schedulers.mjs +201 -0
  209. package/src/standalone/agent-tool/helpers.mjs +237 -0
  210. package/src/standalone/agent-tool/notify.mjs +107 -0
  211. package/src/standalone/agent-tool/provider-init.mjs +143 -0
  212. package/src/standalone/agent-tool/render.mjs +152 -0
  213. package/src/standalone/agent-tool/tool-def.mjs +55 -0
  214. package/src/standalone/agent-tool.mjs +110 -671
  215. package/src/standalone/channel-worker.mjs +4 -7
  216. package/src/standalone/explore-tool.mjs +30 -9
  217. package/src/standalone/hook-bus/config.mjs +207 -0
  218. package/src/standalone/hook-bus/constants.mjs +90 -0
  219. package/src/standalone/hook-bus/handlers.mjs +481 -0
  220. package/src/standalone/hook-bus/payload.mjs +31 -0
  221. package/src/standalone/hook-bus/rules.mjs +77 -0
  222. package/src/standalone/hook-bus.mjs +77 -870
  223. package/src/standalone/memory-runtime-proxy.mjs +7 -0
  224. package/src/standalone/opencode-go-login.mjs +5 -1
  225. package/src/standalone/provider-admin.mjs +1 -16
  226. package/src/standalone/usage-dashboard.mjs +3 -1
  227. package/src/tui/App.jsx +945 -8094
  228. package/src/tui/app/app-format.mjs +206 -0
  229. package/src/tui/app/channel-pickers.mjs +510 -0
  230. package/src/tui/app/clipboard.mjs +67 -0
  231. package/src/tui/app/core-memory-picker.mjs +210 -0
  232. package/src/tui/app/extension-pickers.mjs +506 -0
  233. package/src/tui/app/input-parsers.mjs +193 -0
  234. package/src/tui/app/maintenance-pickers.mjs +324 -0
  235. package/src/tui/app/model-options.mjs +330 -0
  236. package/src/tui/app/model-picker.mjs +365 -0
  237. package/src/tui/app/onboarding-steps.mjs +400 -0
  238. package/src/tui/app/project-picker.mjs +247 -0
  239. package/src/tui/app/provider-setup-picker.mjs +580 -0
  240. package/src/tui/app/resume-picker.mjs +55 -0
  241. package/src/tui/app/route-pickers.mjs +419 -0
  242. package/src/tui/app/settings-picker.mjs +490 -0
  243. package/src/tui/app/slash-commands.mjs +101 -0
  244. package/src/tui/app/slash-dispatch.mjs +427 -0
  245. package/src/tui/app/text-layout.mjs +46 -0
  246. package/src/tui/app/theme-effort-pickers.mjs +154 -0
  247. package/src/tui/app/transcript-window.mjs +671 -0
  248. package/src/tui/app/use-mouse-input.mjs +460 -0
  249. package/src/tui/app/use-prompt-handlers.mjs +310 -0
  250. package/src/tui/app/use-transcript-scroll.mjs +510 -0
  251. package/src/tui/app/use-transcript-window.mjs +589 -0
  252. package/src/tui/components/ConfirmBar.jsx +1 -1
  253. package/src/tui/components/Picker.jsx +32 -4
  254. package/src/tui/components/PromptInput.jsx +23 -101
  255. package/src/tui/components/SlashCommandPalette.jsx +8 -1
  256. package/src/tui/components/StatusLine.jsx +63 -12
  257. package/src/tui/components/TextEntryPanel.jsx +11 -0
  258. package/src/tui/components/ToolExecution.jsx +52 -594
  259. package/src/tui/components/TranscriptItem.jsx +105 -0
  260. package/src/tui/components/UsagePanel.jsx +18 -4
  261. package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
  262. package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
  263. package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
  264. package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
  265. package/src/tui/components/tool-execution/text-format.mjs +161 -0
  266. package/src/tui/display-width.mjs +20 -3
  267. package/src/tui/dist/index.mjs +19652 -18630
  268. package/src/tui/engine/agent-job-feed.mjs +133 -0
  269. package/src/tui/engine/notification-plan.mjs +76 -0
  270. package/src/tui/engine/render-timing.mjs +17 -0
  271. package/src/tui/engine/tool-approval.mjs +94 -0
  272. package/src/tui/engine/tool-card-results.mjs +234 -0
  273. package/src/tui/engine/tool-result-status.mjs +135 -0
  274. package/src/tui/engine.mjs +122 -562
  275. package/src/tui/figures.mjs +5 -0
  276. package/src/tui/index.jsx +105 -0
  277. package/src/tui/input-editing.mjs +2 -2
  278. package/src/tui/markdown/format-token.mjs +4 -1
  279. package/src/tui/statusline-ansi-bridge.mjs +11 -3
  280. package/src/tui/theme.mjs +6 -0
  281. package/src/ui/statusline-agents.mjs +213 -0
  282. package/src/ui/statusline-format.mjs +146 -0
  283. package/src/ui/statusline-segments.mjs +148 -0
  284. package/src/ui/statusline.mjs +67 -501
  285. package/src/ui/tool-card.mjs +0 -1
  286. package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
  287. package/src/workflows/default/WORKFLOW.md +1 -1
  288. package/src/workflows/sequential/WORKFLOW.md +1 -1
  289. package/vendor/ink/build/display-width.js +19 -3
  290. package/vendor/ink/build/ink.js +103 -6
  291. package/vendor/ink/build/log-update.js +17 -3
  292. package/vendor/ink/build/wrap-text.js +125 -0
  293. package/scripts/_test-folder-dialog.mjs +0 -30
  294. package/scripts/fix-brief-fn.mjs +0 -35
  295. package/scripts/fix-format-tool-surface.mjs +0 -24
  296. package/scripts/fix-tool-exec-visible.mjs +0 -42
  297. package/scripts/patch-agent-brief.mjs +0 -48
  298. package/scripts/patch-app.mjs +0 -21
  299. package/scripts/patch-app2.mjs +0 -18
  300. package/scripts/patch-dist-brief.mjs +0 -96
  301. package/scripts/patch-tool-exec.mjs +0 -70
  302. package/src/examples/schedules/SCHEDULE.example.md +0 -32
  303. package/src/examples/webhooks/WEBHOOK.example.md +0 -40
  304. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
  305. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
  306. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
  307. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +0 -162
  308. package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
  309. package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
  310. package/src/runtime/shared/task-notification-envelope.test.mjs +0 -107
  311. package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
  312. package/src/standalone/agent-task-status.test.mjs +0 -76
  313. package/src/tui/components/tool-output-format.test.mjs +0 -399
  314. package/src/tui/display-width.test.mjs +0 -35
  315. package/src/tui/engine-runtime-notification.test.mjs +0 -115
  316. package/src/tui/engine-tool-result-text.test.mjs +0 -75
  317. package/src/tui/input-editing.selection.test.mjs +0 -75
  318. package/src/tui/markdown/format-token.test.mjs +0 -354
  319. package/src/tui/markdown/render-ansi.test.mjs +0 -108
  320. package/src/tui/markdown/stream-fence.test.mjs +0 -26
  321. package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
  322. package/src/tui/paste-fix.test.mjs +0 -119
  323. package/src/tui/prompt-history-store.test.mjs +0 -52
  324. package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
  325. package/src/tui/transcript-tool-failures.test.mjs +0 -111
  326. package/src/ui/markdown.test.mjs +0 -70
  327. package/src/ui/statusline-context-label.test.mjs +0 -15
  328. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
  329. package/src/vendor/statusline/bin/statusline-route.test.mjs +0 -80
@@ -0,0 +1,732 @@
1
+ /**
2
+ * openai-oauth-http-sse.mjs — HTTP/SSE fallback transport for openai-oauth.
3
+ *
4
+ * Extracted from openai-oauth.mjs. Used when the WebSocket transport is
5
+ * unhealthy (see _shouldUseOpenAIHttpFallback / shouldFallbackTransport).
6
+ * Owns SSE frame parsing, the single-emit tool-call dedupe contract
7
+ * (scripts/openai-oauth-http-sse-toolcall-smoke.mjs) and fallback headers.
8
+ */
9
+ import { randomBytes } from 'crypto';
10
+ import {
11
+ traceAgentFetch,
12
+ traceAgentSse,
13
+ traceAgentUsage,
14
+ } from '../agent-trace.mjs';
15
+ import {
16
+ PROVIDER_HTTP_RESPONSE_TIMEOUT_MS,
17
+ PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS,
18
+ PROVIDER_SSE_IDLE_WATCHDOG_ENABLED,
19
+ streamStalledError,
20
+ createTimeoutSignal,
21
+ createPassthroughSignal,
22
+ } from '../stall-policy.mjs';
23
+ import { populateHttpStatusFromMessage, shouldFallbackTransport } from './retry-classifier.mjs';
24
+ import { getLlmDispatcher } from '../../../shared/llm/http-agent.mjs';
25
+ import { makeInvalidToolArgsMarker } from './openai-compat-stream.mjs';
26
+ import { createLeakGuard, createToolCallDedupe, dedupeToolCallList } from './anthropic-leaked-toolcall.mjs';
27
+ import { customToolCallFromResponseItem } from './custom-tool-wire.mjs';
28
+ import { CODEX_OAUTH_ORIGINATOR, CODEX_RESPONSES_URL, _displayCodexModel } from './openai-oauth.mjs';
29
+
30
+ export function _envFlag(name, fallback = true) {
31
+ const raw = process.env[name];
32
+ if (raw == null || raw === '') return fallback;
33
+ return !['0', 'false', 'off', 'no'].includes(String(raw).toLowerCase());
34
+ }
35
+
36
+ export function _envPositiveInt(name, fallback) {
37
+ const raw = process.env[name];
38
+ if (raw == null || raw === '') return fallback;
39
+ const n = Number(raw);
40
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
41
+ }
42
+
43
+ // Completed function_call.arguments parse for the OpenAI Responses stream.
44
+ // A function_call item arrives only on a completion/done signal, so a
45
+ // non-empty-but-malformed
46
+ // arguments string is deterministic bad JSON — NOT mid-stream truncation.
47
+ // Empty/whitespace input legitimately means "no arguments" → {}. A non-empty
48
+ // string that fails JSON.parse is surfaced as an invalid-args MARKER (instead
49
+ // of being silently swallowed to {}) so the dispatch loop turns it into an
50
+ // is_error tool_result and the model self-corrects in the same turn.
51
+ function _parseJsonObject(value) {
52
+ const text = typeof value === 'string' ? value : (value == null ? '' : String(value));
53
+ if (text.trim() === '') return {};
54
+ try {
55
+ const parsed = JSON.parse(text);
56
+ return parsed && typeof parsed === 'object' ? parsed : {};
57
+ } catch (err) {
58
+ return makeInvalidToolArgsMarker(text, err instanceof Error ? err.message : String(err));
59
+ }
60
+ }
61
+
62
+ function _extractCachedTokens(usage) {
63
+ const details = usage?.input_tokens_details || usage?.prompt_tokens_details || {};
64
+ return Number(details.cached_tokens ?? details.cached ?? usage?.cached_tokens ?? 0) || 0;
65
+ }
66
+
67
+ function _sseEventsFromBuffer(buffer) {
68
+ const frames = [];
69
+ let rest = buffer.replace(/\r\n/g, '\n');
70
+ let idx;
71
+ while ((idx = rest.indexOf('\n\n')) >= 0) {
72
+ frames.push(rest.slice(0, idx));
73
+ rest = rest.slice(idx + 2);
74
+ }
75
+ return { frames, rest };
76
+ }
77
+
78
+ function _parseSseFrame(frame) {
79
+ const lines = String(frame || '').split('\n');
80
+ const data = [];
81
+ for (const line of lines) {
82
+ if (!line || line.startsWith(':')) continue;
83
+ if (line.startsWith('data:')) data.push(line.slice(5).trimStart());
84
+ }
85
+ if (!data.length) return null;
86
+ const raw = data.join('\n').trim();
87
+ if (!raw || raw === '[DONE]') return null;
88
+ try { return JSON.parse(raw); } catch { return null; }
89
+ }
90
+
91
+ function _incompleteReasonFromEvent(event) {
92
+ const reasonObj = event?.response?.incomplete_details
93
+ || event?.incomplete_details
94
+ || event?.response?.status_details
95
+ || null;
96
+ return String(reasonObj?.reason || event?.response?.status || 'incomplete');
97
+ }
98
+
99
+ function _isMaxOutputIncompleteReason(reason) {
100
+ return /^(?:max_output_tokens|max_tokens|length|output_token_limit)$/i.test(String(reason || '').trim());
101
+ }
102
+
103
+ function _pushOutputTextAnnotations(part, citations, citationKeys) {
104
+ const annotations = Array.isArray(part?.annotations) ? part.annotations : [];
105
+ for (const raw of annotations) {
106
+ const url = raw?.url || raw?.uri || raw?.href || '';
107
+ if (!url || citationKeys.has(url)) continue;
108
+ citationKeys.add(url);
109
+ citations.push({
110
+ title: raw?.title || '',
111
+ url,
112
+ snippet: raw?.snippet || raw?.text || raw?.description || '',
113
+ source: 'openai-oauth',
114
+ });
115
+ }
116
+ }
117
+
118
+ function _buildOpenAIHttpFallbackHeaders({ auth, cacheKey }) {
119
+ const headers = {
120
+ Authorization: `Bearer ${auth.access_token}`,
121
+ 'Content-Type': 'application/json',
122
+ Accept: 'text/event-stream',
123
+ 'OpenAI-Beta': 'responses=experimental',
124
+ originator: CODEX_OAUTH_ORIGINATOR,
125
+ 'chatgpt-account-id': auth.account_id || '',
126
+ 'x-client-request-id': randomBytes(16).toString('hex'),
127
+ };
128
+ if (cacheKey) headers.session_id = String(cacheKey);
129
+ return headers;
130
+ }
131
+
132
+ // WS→HTTP/SSE fallback predicate → shared shouldFallbackTransport
133
+ // (retry-classifier.mjs). The per-provider env flag is computed here and passed
134
+ // as `enabled`; the deny-order + allow-list are identical to the former copy.
135
+ export function _shouldUseOpenAIHttpFallback(err, externalSignal) {
136
+ return shouldFallbackTransport(err, {
137
+ signal: externalSignal,
138
+ enabled: _envFlag('MIXDOG_OPENAI_OAUTH_HTTP_FALLBACK', true),
139
+ });
140
+ }
141
+
142
+ // Exported for the single-emit regression smoke (scripts/openai-oauth-
143
+ // http-sse-toolcall-smoke.mjs): the SSE stream can surface the same
144
+ // function_call across response.function_call_arguments.done +
145
+ // response.output_item.done + response.completed, and onToolCall must fire
146
+ // exactly once per call id. No production caller imports this name; the
147
+ // provider invokes it internally.
148
+ export async function sendViaHttpSse({
149
+ auth,
150
+ body,
151
+ opts,
152
+ onStreamDelta,
153
+ onToolCall,
154
+ onTextDelta,
155
+ onStageChange,
156
+ externalSignal,
157
+ poolKey,
158
+ cacheKey,
159
+ iteration,
160
+ useModel,
161
+ fetchFn = fetch,
162
+ } = {}) {
163
+ // P1 audit fix: no fixed wall-clock total cap on the HTTP/SSE fallback
164
+ // stream. The old createTimeoutSignal(..., PROVIDER_GENERATE_TOTAL_TIMEOUT_MS)
165
+ // killed a healthy, still-streaming turn purely on elapsed time, unlike
166
+ // every other streaming provider path (anthropic-oauth uses the same
167
+ // createPassthroughSignal pattern — see anthropic-oauth.mjs "Option A").
168
+ // The stream is bounded instead by:
169
+ // (a) headerTimeout below (PROVIDER_HTTP_RESPONSE_TIMEOUT_MS) for a
170
+ // socket that never sends the initial response,
171
+ // (b) the SEMANTIC idle watchdog (_armSemanticIdle /
172
+ // PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS), which resets on every
173
+ // meaningful() chunk — a live stream stays alive, a truly silent
174
+ // one still aborts, and
175
+ // (c) externalSignal (client disconnect / replaced-by-newer-request).
176
+ const totalTimeout = createPassthroughSignal(externalSignal);
177
+ const headerTimeout = createTimeoutSignal(
178
+ totalTimeout.signal,
179
+ PROVIDER_HTTP_RESPONSE_TIMEOUT_MS,
180
+ 'OpenAI OAuth HTTP fallback initial response',
181
+ );
182
+ const headers = _buildOpenAIHttpFallbackHeaders({ auth, cacheKey });
183
+ const fetchStartedAt = Date.now();
184
+ let response;
185
+ try {
186
+ try { onStageChange?.('requesting'); } catch {}
187
+ response = await fetchFn(CODEX_RESPONSES_URL, {
188
+ method: 'POST',
189
+ headers,
190
+ body: JSON.stringify(body),
191
+ signal: headerTimeout.signal,
192
+ dispatcher: getLlmDispatcher(),
193
+ });
194
+ } catch (err) {
195
+ if (headerTimeout.signal?.aborted && headerTimeout.signal.reason instanceof Error) throw headerTimeout.signal.reason;
196
+ throw err;
197
+ } finally {
198
+ headerTimeout.cleanup();
199
+ }
200
+
201
+ traceAgentFetch({
202
+ sessionId: poolKey,
203
+ headersMs: Date.now() - fetchStartedAt,
204
+ httpStatus: response.status,
205
+ provider: 'openai-oauth',
206
+ model: useModel,
207
+ transport: 'http',
208
+ });
209
+
210
+ if (!response.ok) {
211
+ const text = await response.text().catch(() => '');
212
+ const err = new Error(`OpenAI OAuth HTTP fallback ${response.status}: ${text.slice(0, 200)}`);
213
+ err.httpStatus = response.status;
214
+ err.headers = response.headers;
215
+ populateHttpStatusFromMessage(err, text);
216
+ totalTimeout.cleanup();
217
+ throw err;
218
+ }
219
+ if (!response.body) {
220
+ totalTimeout.cleanup();
221
+ throw new Error('OpenAI OAuth HTTP fallback returned no response body');
222
+ }
223
+
224
+ try { onStageChange?.('streaming'); } catch {}
225
+ const sseStartedAt = Date.now();
226
+ const reader = response.body.getReader();
227
+ const decoder = new TextDecoder();
228
+ // After headerTimeout.cleanup() the in-flight fetch no longer carries a live
229
+ // signal, so a totalTimeout / external abort that fires during a pending
230
+ // reader.read() would otherwise leave the pooled request hanging. Keep the
231
+ // reader tied to totalTimeout for the whole stream: on abort, cancel the
232
+ // reader so the awaited read() unblocks and the socket is released back to
233
+ // the shared pool instead of leaking. reader.cancel() may resolve the
234
+ // pending read() as {done:true} rather than rejecting, which would let a
235
+ // partial response surface as success — so record the abort reason and
236
+ // re-throw it after the loop unblocks (see below).
237
+ let _streamAbortReason = null;
238
+ let _onTotalAbort = null;
239
+ if (totalTimeout.signal) {
240
+ _onTotalAbort = () => {
241
+ const reason = totalTimeout.signal.reason;
242
+ _streamAbortReason = reason instanceof Error
243
+ ? reason
244
+ : new Error('OpenAI OAuth HTTP fallback aborted');
245
+ try { reader.cancel(_streamAbortReason).catch(() => {}); } catch {}
246
+ };
247
+ if (totalTimeout.signal.aborted) _onTotalAbort();
248
+ else totalTimeout.signal.addEventListener('abort', _onTotalAbort, { once: true });
249
+ }
250
+ // SEMANTIC idle watchdog: reset ONLY on meaningful() (text/reasoning/tool
251
+ // deltas), never on raw bytes/keepalive frames, so a stream that emits some
252
+ // deltas then goes silent trips a short, named terminal failure instead of
253
+ // hanging until the 30-min agent watchdog. Disablable via the shared env.
254
+ let _semanticIdleTimer = null;
255
+ const _clearSemanticIdle = () => {
256
+ if (_semanticIdleTimer) { clearTimeout(_semanticIdleTimer); _semanticIdleTimer = null; }
257
+ };
258
+ const _armSemanticIdle = () => {
259
+ if (!PROVIDER_SSE_IDLE_WATCHDOG_ENABLED || !(PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS > 0)) return;
260
+ _clearSemanticIdle();
261
+ _semanticIdleTimer = setTimeout(() => {
262
+ _streamAbortReason = streamStalledError('OpenAI OAuth HTTP fallback', PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS, { emittedToolCall: emittedToolCallIds.size > 0 });
263
+ // Partial-final recovery: attach the
264
+ // streamed partial state so the agent loop can accept a wedged FINAL
265
+ // no-tool summary as a successful partial-final instead of dropping
266
+ // the result. pendingToolUse gates out any mid-flight tool call.
267
+ try {
268
+ _streamAbortReason.partialContent = content;
269
+ _streamAbortReason.partialToolCalls = toolCalls.length ? toolCalls.slice() : undefined;
270
+ _streamAbortReason.pendingToolUse = pendingCalls.size > 0 || emittedToolCallIds.size > 0;
271
+ _streamAbortReason.partialModel = model || undefined;
272
+ } catch { /* best-effort enrichment */ }
273
+ try { reader.cancel(_streamAbortReason).catch(() => {}); } catch {}
274
+ }, PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS);
275
+ try { _semanticIdleTimer.unref?.(); } catch {}
276
+ };
277
+ let buffer = '';
278
+ let content = '';
279
+ let model = '';
280
+ let responseId = '';
281
+ let serviceTier = '';
282
+ let usage = null;
283
+ let ttftMs = null;
284
+ const toolCalls = [];
285
+ const pendingCalls = new Map();
286
+ const reasoningItems = [];
287
+ const citations = [];
288
+ const citationKeys = new Set();
289
+ const webSearchCalls = [];
290
+ const webSearchCallKeys = new Set();
291
+ let completed = false;
292
+ let stopReason = null;
293
+ // Gateway live-text relay invariant: set once a non-empty text chunk has
294
+ // been forwarded to the client. A failure afterwards is non-retryable —
295
+ // the rendered text cannot be withdrawn and a re-request would concatenate
296
+ // a second attempt.
297
+ let emittedText = false;
298
+
299
+ // Single-emit guard for tool calls (matches the WS path's
300
+ // emittedToolCall intent). The HTTP/SSE event stream can surface the
301
+ // same function_call across multiple frames — response.function_call_arguments.done,
302
+ // response.output_item.done, and the final response.completed.output
303
+ // bundle. Each frame independently completes the call (id + name) and
304
+ // would re-invoke onToolCall, double-executing a side-effecting tool.
305
+ // Route every emit through emitToolCall: it fires the callback exactly
306
+ // once per unique call id, the first time the call is complete. A call
307
+ // whose id/name only arrives in a later frame is NOT dropped — its
308
+ // first complete frame still emits; only redundant re-emits are
309
+ // suppressed.
310
+ const emittedToolCallIds = new Set();
311
+ // Fix 2: cross-path name+args dedupe. A text-leaked synthetic and an
312
+ // identical native function_call must fire onToolCall exactly once.
313
+ const _toolDedupe = createToolCallDedupe();
314
+ const emitToolCall = (call) => {
315
+ if (!call || !call.id) return;
316
+ if (emittedToolCallIds.has(call.id)) return;
317
+ emittedToolCallIds.add(call.id);
318
+ if (!_toolDedupe.shouldDispatch(call.name, call.arguments)) return;
319
+ try { onToolCall?.(call); } catch {}
320
+ };
321
+
322
+ // Leaked tool-call guard. The model sometimes emits a tool call as plain
323
+ // text (XML `<invoke>`/`<function_calls>` or gpt-oss harmony
324
+ // `<|channel|>...to=functions.NAME...<|call|>`) inside
325
+ // `response.output_text.delta` instead of a native function_call. Route
326
+ // text through the guard so leaked calls are suppressed from the visible
327
+ // stream, synthesized (native `call_...` id shape), and dispatched like
328
+ // native ones. Known tool names come from the request body so recovery
329
+ // only fires for tools the model was actually offered. Additive: the
330
+ // native function_call path is untouched.
331
+ const _leakKnownTools = new Set(
332
+ (Array.isArray(body?.tools) ? body.tools : [])
333
+ .map((t) => (typeof t?.name === 'string' ? t.name : null))
334
+ .filter(Boolean),
335
+ );
336
+ const leakGuard = createLeakGuard({ knownToolNames: _leakKnownTools, harmony: true });
337
+ const dispatchLeakedCall = (recovered) => {
338
+ let args = recovered?.arguments;
339
+ if (args === null || typeof args !== 'object' || Array.isArray(args)) args = {};
340
+ const call = {
341
+ id: `call_leaked_${randomBytes(8).toString('hex')}`,
342
+ name: recovered.name,
343
+ arguments: args,
344
+ };
345
+ toolCalls.push(call);
346
+ emitToolCall(call);
347
+ };
348
+ const relayLeakText = (delta) => {
349
+ if (!leakGuard.enabled) {
350
+ content += delta || '';
351
+ if (delta && onTextDelta) {
352
+ emittedText = true;
353
+ try { onTextDelta(delta); } catch {}
354
+ }
355
+ return;
356
+ }
357
+ const { text, calls } = leakGuard.push(delta);
358
+ if (text) {
359
+ content += text;
360
+ if (onTextDelta) {
361
+ emittedText = true;
362
+ try { onTextDelta(text); } catch {}
363
+ }
364
+ }
365
+ for (const c of calls) dispatchLeakedCall(c);
366
+ };
367
+ const flushLeak = () => {
368
+ if (!leakGuard.enabled) return;
369
+ const { text, calls } = leakGuard.flush();
370
+ if (text) {
371
+ content += text;
372
+ if (onTextDelta) {
373
+ emittedText = true;
374
+ try { onTextDelta(text); } catch {}
375
+ }
376
+ }
377
+ for (const c of calls) dispatchLeakedCall(c);
378
+ };
379
+
380
+ const pushWebSearchCall = (item) => {
381
+ if (!item || item.type !== 'web_search_call') return;
382
+ const key = item.id || JSON.stringify(item.action || item);
383
+ if (webSearchCallKeys.has(key)) return;
384
+ webSearchCallKeys.add(key);
385
+ webSearchCalls.push({ id: item.id || '', status: item.status || '', action: item.action || null });
386
+ };
387
+ const pushReasoningItem = (item) => {
388
+ if (item?.type === 'reasoning' && item.encrypted_content && !reasoningItems.some(r => r.id === item.id)) {
389
+ reasoningItems.push({
390
+ id: item.id || '',
391
+ encrypted_content: item.encrypted_content,
392
+ summary: Array.isArray(item.summary) ? item.summary : [],
393
+ });
394
+ }
395
+ };
396
+ const pushToolSearchCall = (item) => {
397
+ if (!item || item.type !== 'tool_search_call') return;
398
+ const callId = item.call_id || item.id || '';
399
+ if (!callId || toolCalls.some(t => t.id === callId)) return;
400
+ let args = {};
401
+ if (item.arguments && typeof item.arguments === 'object') {
402
+ args = item.arguments;
403
+ } else if (typeof item.arguments === 'string' && item.arguments.trim()) {
404
+ // Non-empty but malformed tool_search arguments are deterministic
405
+ // bad JSON (the item is only emitted on completion). Surface an
406
+ // invalid-args marker instead of swallowing to {} so the model can
407
+ // self-correct in the same turn.
408
+ args = _parseJsonObject(item.arguments);
409
+ }
410
+ const call = {
411
+ id: callId,
412
+ name: 'tool_search',
413
+ arguments: args,
414
+ nativeType: 'tool_search_call',
415
+ };
416
+ toolCalls.push(call);
417
+ emitToolCall(call);
418
+ };
419
+ const pushCustomToolCall = (item) => {
420
+ const call = customToolCallFromResponseItem(item);
421
+ if (!call || toolCalls.some(t => t.id === call.id)) return;
422
+ toolCalls.push(call);
423
+ emitToolCall(call);
424
+ };
425
+ const meaningful = () => {
426
+ if (ttftMs == null) ttftMs = Date.now() - sseStartedAt;
427
+ _armSemanticIdle();
428
+ try { onStreamDelta?.(); } catch {}
429
+ };
430
+ const handleEvent = (event) => {
431
+ if (!event || typeof event.type !== 'string') return;
432
+ switch (event.type) {
433
+ case 'response.created':
434
+ if (event.response?.model) model = event.response.model;
435
+ if (event.response?.id) responseId = event.response.id;
436
+ break;
437
+ case 'response.output_text.delta':
438
+ meaningful();
439
+ relayLeakText(event.delta || '');
440
+ break;
441
+ case 'response.reasoning_text.delta':
442
+ case 'response.reasoning_summary_text.delta':
443
+ meaningful();
444
+ break;
445
+ case 'response.output_item.added':
446
+ if (event.item?.type === 'function_call') {
447
+ pendingCalls.set(event.item.id || '', {
448
+ name: event.item.name || '',
449
+ callId: event.item.call_id || '',
450
+ });
451
+ } else if (event.item?.type === 'tool_search_call') {
452
+ // Mark tool_search as in-flight the moment the item is
453
+ // added, mirroring function_call above, so the semantic
454
+ // idle watchdog's pendingToolUse gate (pendingCalls.size)
455
+ // sees a mid-flight tool_search and never lets stall
456
+ // recovery drop it before response.output_item.done.
457
+ // kind:'tool_search' tags the entry so the shared
458
+ // function_call_arguments.done handler (below) never
459
+ // mistakes it for a function call by id collision/empty id.
460
+ if (event.item.id) {
461
+ pendingCalls.set(event.item.id, {
462
+ name: 'tool_search',
463
+ callId: event.item.call_id || '',
464
+ kind: 'tool_search',
465
+ });
466
+ }
467
+ }
468
+ break;
469
+ case 'response.function_call_arguments.delta':
470
+ meaningful();
471
+ break;
472
+ case 'response.function_call_arguments.done': {
473
+ const itemId = event.item_id || '';
474
+ const pending = pendingCalls.get(itemId);
475
+ if (pending?.kind === 'tool_search') { meaningful(); break; }
476
+ const call = {
477
+ id: pending?.callId || event.call_id || '',
478
+ name: pending?.name || event.name || '',
479
+ arguments: _parseJsonObject(event.arguments),
480
+ _pendingItemId: itemId,
481
+ };
482
+ toolCalls.push(call);
483
+ if (call.id && call.name) {
484
+ delete call._pendingItemId;
485
+ emitToolCall(call);
486
+ }
487
+ meaningful();
488
+ break;
489
+ }
490
+ case 'response.custom_tool_call_input.delta':
491
+ meaningful();
492
+ break;
493
+ case 'response.output_item.done': {
494
+ const item = event.item || {};
495
+ pushReasoningItem(item);
496
+ pushWebSearchCall(item);
497
+ if (item.type === 'function_call') {
498
+ const tc = toolCalls.find(t => t._pendingItemId === (item.id || ''));
499
+ if (tc) {
500
+ if (!tc.id && item.call_id) tc.id = item.call_id;
501
+ if (!tc.name && item.name) tc.name = item.name;
502
+ if (tc.id && tc.name) {
503
+ delete tc._pendingItemId;
504
+ emitToolCall(tc);
505
+ }
506
+ }
507
+ } else if (item.type === 'tool_search_call') {
508
+ pendingCalls.delete(item.id || '');
509
+ pushToolSearchCall(item);
510
+ } else if (item.type === 'custom_tool_call') {
511
+ pushCustomToolCall(item);
512
+ meaningful();
513
+ }
514
+ break;
515
+ }
516
+ case 'response.completed': {
517
+ const resp = event.response || {};
518
+ serviceTier = resp.service_tier || resp.serviceTier || serviceTier;
519
+ if (!model && resp.model) model = resp.model;
520
+ if (!responseId && resp.id) responseId = resp.id;
521
+ if (resp.usage) {
522
+ usage = {
523
+ inputTokens: resp.usage.input_tokens || 0,
524
+ outputTokens: resp.usage.output_tokens || 0,
525
+ cachedTokens: _extractCachedTokens(resp.usage),
526
+ promptTokens: resp.usage.input_tokens || 0,
527
+ raw: serviceTier ? { ...resp.usage, service_tier: serviceTier } : resp.usage,
528
+ };
529
+ }
530
+ for (const item of resp.output || []) {
531
+ if (item.type === 'message') {
532
+ for (const part of item.content || []) {
533
+ if (!content && part.type === 'output_text') {
534
+ // Completed-output fallback (no streamed text).
535
+ // Route through the leak guard so a tool call
536
+ // leaked only in the final bundle is recovered
537
+ // rather than surfaced as visible content. push
538
+ // with final=true flushes fully (no held tail).
539
+ if (leakGuard.enabled) {
540
+ const { text, calls } = leakGuard.push(part.text || '', true);
541
+ content += text;
542
+ for (const c of calls) dispatchLeakedCall(c);
543
+ } else {
544
+ content += part.text || '';
545
+ }
546
+ }
547
+ if (part.type === 'output_text') _pushOutputTextAnnotations(part, citations, citationKeys);
548
+ }
549
+ } else if (item.type === 'reasoning') {
550
+ pushReasoningItem(item);
551
+ } else if (item.type === 'web_search_call') {
552
+ pushWebSearchCall(item);
553
+ } else if (item.type === 'tool_search_call') {
554
+ pushToolSearchCall(item);
555
+ } else if (item.type === 'custom_tool_call') {
556
+ pushCustomToolCall(item);
557
+ meaningful();
558
+ } else if (item.type === 'function_call') {
559
+ // Match the still-pending placeholder by item id, or
560
+ // an already-recorded call by its canonical call_id —
561
+ // so a call completed at args.done / output_item.done
562
+ // is reused here rather than re-pushed as a duplicate.
563
+ const tc = toolCalls.find(t =>
564
+ t._pendingItemId === (item.id || '')
565
+ || (item.call_id && t.id === item.call_id));
566
+ if (tc) {
567
+ if (!tc.id && item.call_id) tc.id = item.call_id;
568
+ if (!tc.name && item.name) tc.name = item.name;
569
+ if (tc.id && tc.name) {
570
+ delete tc._pendingItemId;
571
+ emitToolCall(tc);
572
+ }
573
+ } else if (item.call_id && item.name) {
574
+ const call = {
575
+ id: item.call_id,
576
+ name: item.name,
577
+ arguments: _parseJsonObject(item.arguments),
578
+ };
579
+ toolCalls.push(call);
580
+ emitToolCall(call);
581
+ }
582
+ }
583
+ }
584
+ completed = true;
585
+ break;
586
+ }
587
+ case 'response.done':
588
+ if (!event.response || event.response.status === 'completed') completed = true;
589
+ else if (event.response.status === 'failed') {
590
+ const msg = event.response?.error?.message || 'response.done failed';
591
+ const err = new Error(`OpenAI OAuth HTTP fallback response.done failed: ${msg}`);
592
+ populateHttpStatusFromMessage(err, msg);
593
+ throw err;
594
+ } else if (event.response.status === 'incomplete') {
595
+ const reason = _incompleteReasonFromEvent(event);
596
+ if (_isMaxOutputIncompleteReason(reason)) {
597
+ completed = true;
598
+ stopReason = 'length';
599
+ break;
600
+ }
601
+ throw new Error(`OpenAI OAuth HTTP fallback response.done incomplete: ${reason}`);
602
+ }
603
+ break;
604
+ case 'response.failed': {
605
+ const msg = event.response?.error?.message || event.error?.message || event.message || 'response.failed';
606
+ const err = new Error(`OpenAI OAuth HTTP fallback response.failed: ${msg}`);
607
+ populateHttpStatusFromMessage(err, msg);
608
+ throw err;
609
+ }
610
+ case 'response.incomplete': {
611
+ const reason = _incompleteReasonFromEvent(event);
612
+ if (_isMaxOutputIncompleteReason(reason)) {
613
+ completed = true;
614
+ stopReason = 'length';
615
+ break;
616
+ }
617
+ throw new Error(`OpenAI OAuth HTTP fallback response.incomplete: ${reason}`);
618
+ }
619
+ case 'error': {
620
+ const msg = event.message || event.error?.message || 'unknown';
621
+ const err = new Error(`OpenAI OAuth HTTP fallback error: ${msg}`);
622
+ populateHttpStatusFromMessage(err, msg);
623
+ throw err;
624
+ }
625
+ default:
626
+ break;
627
+ }
628
+ };
629
+
630
+ try {
631
+ while (true) {
632
+ if (totalTimeout.signal?.aborted) {
633
+ _clearSemanticIdle();
634
+ const reason = totalTimeout.signal.reason;
635
+ throw reason instanceof Error ? reason : new Error('OpenAI OAuth HTTP fallback aborted');
636
+ }
637
+ if (_streamAbortReason) throw _streamAbortReason;
638
+ const { value, done } = await reader.read();
639
+ if (done) break;
640
+ buffer += decoder.decode(value, { stream: true });
641
+ const parsed = _sseEventsFromBuffer(buffer);
642
+ buffer = parsed.rest;
643
+ for (const frame of parsed.frames) {
644
+ const event = _parseSseFrame(frame);
645
+ if (event) handleEvent(event);
646
+ }
647
+ }
648
+ // The read() above can unblock via reader.cancel() as {done:true} on an
649
+ // external/total-timeout abort. Surface that as the abort/timeout error
650
+ // instead of treating the partial stream as a successful response.
651
+ if (_streamAbortReason) throw _streamAbortReason;
652
+ buffer += decoder.decode();
653
+ const parsed = _sseEventsFromBuffer(buffer + '\n\n');
654
+ for (const frame of parsed.frames) {
655
+ const event = _parseSseFrame(frame);
656
+ if (event) handleEvent(event);
657
+ }
658
+ // Flush any partial-sentinel tail held back mid-stream so legitimate
659
+ // trailing text is never lost (streamed-text path).
660
+ flushLeak();
661
+ } catch (err) {
662
+ // Live-text invariant: once a non-empty chunk has been relayed it
663
+ // cannot be withdrawn — flag the error so no upstream layer retries.
664
+ if (emittedText && err) { try { err.liveTextEmitted = true; err.unsafeToRetry = true; } catch {} }
665
+ throw err;
666
+ } finally {
667
+ _clearSemanticIdle();
668
+ try { reader.releaseLock?.(); } catch {}
669
+ if (_onTotalAbort && totalTimeout.signal) {
670
+ try { totalTimeout.signal.removeEventListener('abort', _onTotalAbort); } catch {}
671
+ }
672
+ totalTimeout.cleanup();
673
+ }
674
+
675
+ const unresolved = toolCalls.find(t => t._pendingItemId);
676
+ if (unresolved) {
677
+ throw new Error(`OpenAI OAuth HTTP fallback function_call salvage failed: missing call_id/name for item_id=${unresolved._pendingItemId || '?'}`);
678
+ }
679
+ if (!completed && !content && !toolCalls.length) {
680
+ throw new Error('OpenAI OAuth HTTP fallback ended before response.completed');
681
+ }
682
+
683
+ const liveModel = model || useModel;
684
+ traceAgentSse({
685
+ sessionId: poolKey,
686
+ sseParseMs: Date.now() - sseStartedAt,
687
+ ttftMs,
688
+ provider: 'openai-oauth',
689
+ model: liveModel,
690
+ transport: 'sse',
691
+ });
692
+ if (usage) {
693
+ traceAgentUsage({
694
+ sessionId: poolKey,
695
+ iteration,
696
+ inputTokens: usage.inputTokens || 0,
697
+ outputTokens: usage.outputTokens || 0,
698
+ cachedTokens: usage.cachedTokens || 0,
699
+ promptTokens: usage.promptTokens || 0,
700
+ model: liveModel,
701
+ modelDisplay: _displayCodexModel(liveModel),
702
+ responseId: responseId || null,
703
+ rawUsage: usage.raw || null,
704
+ provider: 'openai-oauth',
705
+ serviceTier,
706
+ });
707
+ }
708
+ // Dedupe the returned array by name+args (Fix 2, array side): a synthetic
709
+ // leaked call and an identical native function_call must not both survive,
710
+ // else the agent loop executes the side-effecting tool twice.
711
+ const _returnedToolCalls = toolCalls.length
712
+ ? dedupeToolCallList(toolCalls.map(({ _pendingItemId, ...t }) => t))
713
+ : undefined;
714
+ return {
715
+ content,
716
+ model: liveModel,
717
+ reasoningItems: reasoningItems.length ? reasoningItems : undefined,
718
+ toolCalls: _returnedToolCalls,
719
+ citations: citations.length ? citations : undefined,
720
+ webSearchCalls: webSearchCalls.length ? webSearchCalls : undefined,
721
+ usage: usage || undefined,
722
+ stopReason: stopReason || undefined,
723
+ // P1 audit fix: text-only max-output cutoff (openai-oauth HTTP/SSE
724
+ // fallback maps status:'incomplete'/reason=max_output_tokens to
725
+ // stopReason='length' above and treats it as success). Flag it so
726
+ // loop.mjs can surface a truncation warning instead of accepting
727
+ // silently-cut content as a clean final answer.
728
+ ...(stopReason === 'length' && content.length > 0 ? { truncated: true } : {}),
729
+ responseId: responseId || undefined,
730
+ serviceTier: serviceTier || undefined,
731
+ };
732
+ }