mixdog 0.9.3 → 0.9.5

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