mixdog 0.9.2 → 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 (417) hide show
  1. package/package.json +8 -3
  2. package/scripts/anthropic-maxtokens-test.mjs +119 -0
  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/review-tasks.json +20 -0
  7. package/scripts/bench/round-codex.json +114 -0
  8. package/scripts/bench/round-mixdog-lead-r3.json +269 -0
  9. package/scripts/bench/round-mixdog-lead.json +269 -0
  10. package/scripts/bench/round-mixdog.json +126 -0
  11. package/scripts/bench/round-r10-bigsample.json +679 -0
  12. package/scripts/bench/round-r11-codexalign.json +257 -0
  13. package/scripts/bench/round-r4-codex.json +114 -0
  14. package/scripts/bench/round-r4-mixed.json +225 -0
  15. package/scripts/bench/round-r5-gpt-lead.json +259 -0
  16. package/scripts/bench/round-r6-codex.json +114 -0
  17. package/scripts/bench/round-r6-solo.json +257 -0
  18. package/scripts/bench/round-r7-full.json +254 -0
  19. package/scripts/bench/round-r8-fulldefault.json +255 -0
  20. package/scripts/bench-run.mjs +215 -29
  21. package/scripts/build-tui.mjs +13 -1
  22. package/scripts/explore-bench.mjs +124 -0
  23. package/scripts/freevar-smoke.mjs +95 -0
  24. package/scripts/hook-bus-test.mjs +191 -0
  25. package/scripts/internal-comms-bench.mjs +1 -0
  26. package/scripts/internal-comms-smoke.mjs +10 -9
  27. package/scripts/mouse-probe.mjs +45 -0
  28. package/scripts/output-style-bench.mjs +13 -6
  29. package/scripts/output-style-smoke.mjs +4 -4
  30. package/scripts/path-suffix-test.mjs +57 -0
  31. package/scripts/provider-toolcall-test.mjs +7 -3
  32. package/scripts/recall-bench.mjs +207 -0
  33. package/scripts/recall-usecase-cases.json +18 -0
  34. package/scripts/recall-usecase-probe.json +6 -0
  35. package/scripts/session-bench.mjs +152 -6
  36. package/scripts/tool-smoke.mjs +30 -67
  37. package/scripts/tui-render-smoke.mjs +90 -0
  38. package/scripts/webhook-smoke.mjs +208 -0
  39. package/src/agents/debugger/AGENT.md +5 -2
  40. package/src/agents/heavy-worker/AGENT.md +21 -11
  41. package/src/agents/maintainer/AGENT.md +4 -0
  42. package/src/agents/reviewer/AGENT.md +3 -2
  43. package/src/agents/worker/AGENT.md +21 -11
  44. package/src/lib/rules-builder.cjs +4 -0
  45. package/src/mixdog-session-runtime.mjs +933 -3731
  46. package/src/output-styles/default.md +34 -9
  47. package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
  48. package/src/output-styles/minimal.md +4 -1
  49. package/src/output-styles/simple.md +22 -7
  50. package/src/repl.mjs +5 -5
  51. package/src/rules/agent/00-common.md +2 -0
  52. package/src/rules/agent/30-explorer.md +8 -11
  53. package/src/rules/lead/lead-brief.md +12 -0
  54. package/src/rules/lead/lead-tool.md +2 -9
  55. package/src/rules/shared/01-tool.md +11 -5
  56. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
  57. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
  58. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
  59. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
  60. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
  61. package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
  62. package/src/runtime/agent/orchestrator/config.mjs +69 -2
  63. package/src/runtime/agent/orchestrator/context/collect.mjs +51 -0
  64. package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
  65. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +63 -21
  66. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
  67. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
  68. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
  69. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +97 -1343
  70. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
  71. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +78 -10
  72. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +1 -13
  73. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
  74. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
  75. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
  76. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
  77. package/src/runtime/agent/orchestrator/providers/gemini.mjs +44 -1014
  78. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +18 -4
  79. package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
  80. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +86 -11
  81. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +348 -0
  82. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
  83. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
  84. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +19 -12
  85. package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
  86. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
  87. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
  88. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +41 -1142
  89. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +732 -0
  90. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
  91. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +303 -2119
  92. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +140 -995
  93. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +227 -0
  94. package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
  95. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +436 -0
  96. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
  97. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
  98. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +38 -12
  99. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +7 -8
  100. package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
  101. package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
  102. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
  103. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
  104. package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
  105. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
  106. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
  107. package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
  108. package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
  109. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +274 -0
  110. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
  111. package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
  112. package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
  113. package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
  114. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +47 -0
  115. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +182 -0
  116. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
  117. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
  118. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
  119. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
  120. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
  121. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
  122. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
  123. package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
  124. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
  125. package/src/runtime/agent/orchestrator/session/loop.mjs +409 -1304
  126. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
  127. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +230 -0
  128. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
  129. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +149 -0
  130. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
  131. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
  132. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
  133. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
  134. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
  135. package/src/runtime/agent/orchestrator/session/manager.mjs +226 -2114
  136. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
  137. package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
  138. package/src/runtime/agent/orchestrator/stall-policy.mjs +3 -3
  139. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
  140. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
  141. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +33 -42
  142. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
  143. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
  144. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
  145. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
  146. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +1 -1
  147. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
  148. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
  149. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
  150. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
  151. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -87
  152. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
  153. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
  154. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +78 -304
  155. package/src/runtime/agent/orchestrator/tools/builtin.mjs +11 -6
  156. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
  157. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
  158. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
  159. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +551 -0
  160. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
  161. package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
  162. package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
  163. package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
  164. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
  165. package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
  166. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1080 -0
  167. package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
  168. package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
  169. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
  170. package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
  171. package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
  172. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
  173. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +36 -4277
  174. package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
  175. package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
  176. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
  177. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
  178. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
  179. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
  180. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
  181. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
  182. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
  183. package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
  184. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -23
  185. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +10 -74
  186. package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
  187. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
  188. package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
  189. package/src/runtime/channels/backends/discord-access.mjs +32 -0
  190. package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
  191. package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
  192. package/src/runtime/channels/backends/discord.mjs +12 -292
  193. package/src/runtime/channels/index.mjs +241 -894
  194. package/src/runtime/channels/lib/backend-dispatch.mjs +44 -0
  195. package/src/runtime/channels/lib/boot-profile.mjs +23 -0
  196. package/src/runtime/channels/lib/crash-log.mjs +106 -0
  197. package/src/runtime/channels/lib/event-pipeline.mjs +18 -1
  198. package/src/runtime/channels/lib/event-queue.mjs +63 -4
  199. package/src/runtime/channels/lib/inbound-routing.mjs +111 -0
  200. package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
  201. package/src/runtime/channels/lib/output-forwarder.mjs +9 -1
  202. package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
  203. package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
  204. package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
  205. package/src/runtime/channels/lib/session-discovery.mjs +56 -4
  206. package/src/runtime/channels/lib/telegram-format.mjs +19 -22
  207. package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
  208. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  209. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
  210. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
  211. package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
  212. package/src/runtime/channels/lib/webhook/deliveries.mjs +312 -0
  213. package/src/runtime/channels/lib/webhook/log.mjs +42 -0
  214. package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
  215. package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
  216. package/src/runtime/channels/lib/webhook.mjs +36 -570
  217. package/src/runtime/channels/lib/whisper-language.mjs +42 -0
  218. package/src/runtime/channels/tool-defs.mjs +11 -130
  219. package/src/runtime/memory/index.mjs +258 -2050
  220. package/src/runtime/memory/lib/core-memory-store.mjs +351 -1
  221. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
  222. package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
  223. package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
  224. package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
  225. package/src/runtime/memory/lib/http-wire.mjs +57 -0
  226. package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
  227. package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
  228. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
  229. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
  230. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
  231. package/src/runtime/memory/lib/memory-cycle2.mjs +72 -837
  232. package/src/runtime/memory/lib/memory-embed.mjs +149 -0
  233. package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
  234. package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
  235. package/src/runtime/memory/lib/memory-recall-store.mjs +22 -2
  236. package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
  237. package/src/runtime/memory/lib/memory.mjs +20 -0
  238. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
  239. package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
  240. package/src/runtime/memory/lib/query-handlers.mjs +780 -0
  241. package/src/runtime/memory/lib/recall-format.mjs +238 -0
  242. package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
  243. package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
  244. package/src/runtime/memory/tool-defs.mjs +6 -14
  245. package/src/runtime/search/lib/http-fetch.mjs +274 -0
  246. package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
  247. package/src/runtime/search/lib/web-tools.mjs +24 -602
  248. package/src/runtime/shared/abort-controller.mjs +1 -1
  249. package/src/runtime/shared/atomic-file.mjs +26 -1
  250. package/src/runtime/shared/background-tasks.mjs +2 -3
  251. package/src/runtime/shared/buffered-appender.mjs +149 -0
  252. package/src/runtime/shared/launcher-control.mjs +2 -2
  253. package/src/runtime/shared/task-notification-envelope.mjs +98 -0
  254. package/src/runtime/shared/tool-execution-contract.mjs +2 -2
  255. package/src/runtime/shared/tool-primitives.mjs +308 -0
  256. package/src/runtime/shared/tool-result-summary.mjs +515 -0
  257. package/src/runtime/shared/tool-surface.mjs +80 -898
  258. package/src/runtime/shared/transcript-writer.mjs +52 -2
  259. package/src/runtime/shared/update-checker.mjs +7 -4
  260. package/src/session-runtime/config-helpers.mjs +291 -0
  261. package/src/session-runtime/config-lifecycle.mjs +232 -0
  262. package/src/session-runtime/cwd-plugins.mjs +226 -0
  263. package/src/session-runtime/effort.mjs +128 -0
  264. package/src/session-runtime/fs-utils.mjs +10 -0
  265. package/src/session-runtime/mcp-glue.mjs +177 -0
  266. package/src/session-runtime/model-capabilities.mjs +130 -0
  267. package/src/session-runtime/model-recency.mjs +111 -0
  268. package/src/session-runtime/native-search.mjs +247 -0
  269. package/src/session-runtime/output-styles.mjs +126 -0
  270. package/src/session-runtime/plugin-mcp.mjs +114 -0
  271. package/src/session-runtime/prewarm.mjs +142 -0
  272. package/src/session-runtime/provider-models.mjs +278 -0
  273. package/src/session-runtime/provider-usage.mjs +120 -0
  274. package/src/session-runtime/quick-model-rows.mjs +170 -0
  275. package/src/session-runtime/quick-search-models.mjs +46 -0
  276. package/src/session-runtime/session-hooks.mjs +93 -0
  277. package/src/session-runtime/session-text.mjs +100 -0
  278. package/src/session-runtime/settings-api.mjs +319 -0
  279. package/src/session-runtime/statusline-route.mjs +35 -0
  280. package/src/session-runtime/tool-catalog.mjs +720 -0
  281. package/src/session-runtime/tool-defs.mjs +84 -0
  282. package/src/session-runtime/warmup-schedulers.mjs +201 -0
  283. package/src/session-runtime/workflow.mjs +358 -0
  284. package/src/standalone/agent-tool/helpers.mjs +237 -0
  285. package/src/standalone/agent-tool/notify.mjs +107 -0
  286. package/src/standalone/agent-tool/provider-init.mjs +143 -0
  287. package/src/standalone/agent-tool/render.mjs +152 -0
  288. package/src/standalone/agent-tool/tool-def.mjs +55 -0
  289. package/src/standalone/agent-tool.mjs +155 -677
  290. package/src/standalone/channel-worker.mjs +7 -9
  291. package/src/standalone/explore-tool.mjs +40 -12
  292. package/src/standalone/hook-bus/config.mjs +207 -0
  293. package/src/standalone/hook-bus/constants.mjs +90 -0
  294. package/src/standalone/hook-bus/handlers.mjs +481 -0
  295. package/src/standalone/hook-bus/payload.mjs +31 -0
  296. package/src/standalone/hook-bus/rules.mjs +77 -0
  297. package/src/standalone/hook-bus.mjs +110 -746
  298. package/src/standalone/memory-runtime-proxy.mjs +7 -0
  299. package/src/standalone/opencode-go-login.mjs +125 -0
  300. package/src/standalone/provider-admin.mjs +15 -19
  301. package/src/standalone/usage-dashboard.mjs +3 -1
  302. package/src/tui/App.jsx +1163 -7571
  303. package/src/tui/app/app-format.mjs +206 -0
  304. package/src/tui/app/channel-pickers.mjs +510 -0
  305. package/src/tui/app/clipboard.mjs +67 -0
  306. package/src/tui/app/core-memory-picker.mjs +210 -0
  307. package/src/tui/app/extension-pickers.mjs +506 -0
  308. package/src/tui/app/input-parsers.mjs +193 -0
  309. package/src/tui/app/maintenance-pickers.mjs +324 -0
  310. package/src/tui/app/model-options.mjs +330 -0
  311. package/src/tui/app/model-picker.mjs +365 -0
  312. package/src/tui/app/onboarding-steps.mjs +400 -0
  313. package/src/tui/app/project-picker.mjs +247 -0
  314. package/src/tui/app/provider-setup-picker.mjs +580 -0
  315. package/src/tui/app/resume-picker.mjs +55 -0
  316. package/src/tui/app/route-pickers.mjs +419 -0
  317. package/src/tui/app/settings-picker.mjs +490 -0
  318. package/src/tui/app/slash-commands.mjs +101 -0
  319. package/src/tui/app/slash-dispatch.mjs +427 -0
  320. package/src/tui/app/text-layout.mjs +46 -0
  321. package/src/tui/app/theme-effort-pickers.mjs +154 -0
  322. package/src/tui/app/transcript-window.mjs +671 -0
  323. package/src/tui/app/use-mouse-input.mjs +460 -0
  324. package/src/tui/app/use-prompt-handlers.mjs +310 -0
  325. package/src/tui/app/use-transcript-scroll.mjs +510 -0
  326. package/src/tui/app/use-transcript-window.mjs +589 -0
  327. package/src/tui/components/ConfirmBar.jsx +1 -1
  328. package/src/tui/components/Picker.jsx +32 -4
  329. package/src/tui/components/PromptInput.jsx +259 -80
  330. package/src/tui/components/SlashCommandPalette.jsx +8 -1
  331. package/src/tui/components/StatusLine.jsx +63 -12
  332. package/src/tui/components/TextEntryPanel.jsx +11 -0
  333. package/src/tui/components/ToolExecution.jsx +56 -588
  334. package/src/tui/components/TranscriptItem.jsx +105 -0
  335. package/src/tui/components/UsagePanel.jsx +18 -4
  336. package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
  337. package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
  338. package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
  339. package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
  340. package/src/tui/components/tool-execution/text-format.mjs +161 -0
  341. package/src/tui/components/tool-output-format.mjs +2 -2
  342. package/src/tui/display-width.mjs +20 -3
  343. package/src/tui/dist/index.mjs +18034 -17188
  344. package/src/tui/engine/agent-envelope.mjs +296 -0
  345. package/src/tui/engine/agent-job-feed.mjs +133 -0
  346. package/src/tui/engine/boot-profile.mjs +21 -0
  347. package/src/tui/engine/labels.mjs +67 -0
  348. package/src/tui/engine/notice-text.mjs +112 -0
  349. package/src/tui/engine/notification-plan.mjs +76 -0
  350. package/src/tui/engine/queue-helpers.mjs +161 -0
  351. package/src/tui/engine/render-timing.mjs +17 -0
  352. package/src/tui/engine/session-stats.mjs +46 -0
  353. package/src/tui/engine/tool-approval.mjs +94 -0
  354. package/src/tui/engine/tool-call-fields.mjs +23 -0
  355. package/src/tui/engine/tool-card-results.mjs +234 -0
  356. package/src/tui/engine/tool-result-status.mjs +135 -0
  357. package/src/tui/engine/tool-result-text.mjs +126 -0
  358. package/src/tui/engine.mjs +405 -1385
  359. package/src/tui/figures.mjs +5 -0
  360. package/src/tui/index.jsx +105 -0
  361. package/src/tui/input-editing.mjs +60 -10
  362. package/src/tui/keyboard-protocol.mjs +2 -2
  363. package/src/tui/lib/voice-recorder.mjs +35 -19
  364. package/src/tui/markdown/format-token.mjs +11 -9
  365. package/src/tui/paste-attachments.mjs +38 -0
  366. package/src/tui/statusline-ansi-bridge.mjs +11 -3
  367. package/src/tui/theme.mjs +6 -0
  368. package/src/tui/themes/base.mjs +2 -2
  369. package/src/tui/themes/kanagawa.mjs +4 -4
  370. package/src/tui/themes/teal.mjs +4 -5
  371. package/src/tui/themes/utils.mjs +1 -1
  372. package/src/ui/statusline-agents.mjs +213 -0
  373. package/src/ui/statusline-format.mjs +146 -0
  374. package/src/ui/statusline-segments.mjs +148 -0
  375. package/src/ui/statusline.mjs +77 -462
  376. package/src/ui/tool-card.mjs +0 -1
  377. package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
  378. package/src/workflows/default/WORKFLOW.md +16 -9
  379. package/src/workflows/sequential/WORKFLOW.md +16 -11
  380. package/src/workflows/solo/WORKFLOW.md +5 -1
  381. package/vendor/ink/build/display-width.js +19 -3
  382. package/vendor/ink/build/ink.js +103 -6
  383. package/vendor/ink/build/log-update.js +17 -3
  384. package/vendor/ink/build/wrap-text.js +125 -0
  385. package/scripts/_test-folder-dialog.mjs +0 -30
  386. package/scripts/fix-brief-fn.mjs +0 -35
  387. package/scripts/fix-format-tool-surface.mjs +0 -24
  388. package/scripts/fix-tool-exec-visible.mjs +0 -42
  389. package/scripts/patch-agent-brief.mjs +0 -48
  390. package/scripts/patch-app.mjs +0 -21
  391. package/scripts/patch-app2.mjs +0 -18
  392. package/scripts/patch-dist-brief.mjs +0 -96
  393. package/scripts/patch-tool-exec.mjs +0 -70
  394. package/src/examples/schedules/SCHEDULE.example.md +0 -32
  395. package/src/examples/webhooks/WEBHOOK.example.md +0 -40
  396. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
  397. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
  398. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
  399. package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
  400. package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
  401. package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
  402. package/src/standalone/agent-task-status.test.mjs +0 -76
  403. package/src/tui/components/tool-output-format.test.mjs +0 -399
  404. package/src/tui/display-width.test.mjs +0 -35
  405. package/src/tui/engine-runtime-notification.test.mjs +0 -115
  406. package/src/tui/engine-tool-result-text.test.mjs +0 -75
  407. package/src/tui/markdown/format-token.test.mjs +0 -354
  408. package/src/tui/markdown/render-ansi.test.mjs +0 -108
  409. package/src/tui/markdown/stream-fence.test.mjs +0 -26
  410. package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
  411. package/src/tui/prompt-history-store.test.mjs +0 -52
  412. package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
  413. package/src/tui/transcript-tool-failures.test.mjs +0 -111
  414. package/src/ui/markdown.test.mjs +0 -70
  415. package/src/ui/statusline-context-label.test.mjs +0 -15
  416. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
  417. package/src/vendor/statusline/bin/statusline-route.test.mjs +0 -80
@@ -7,12 +7,12 @@
7
7
  * openai-oauth-ws.mjs; this file owns auth, model catalog, request-body
8
8
  * shape, and HTTP/SSE fallback when WebSocket transport is unhealthy.
9
9
  */
10
- import { createServer } from 'http';
11
- import { randomBytes, createHash } from 'crypto';
10
+ import { createHash } from 'crypto';
12
11
  import { readFileSync, existsSync, mkdirSync, statSync, unlinkSync } from 'fs';
13
12
  import { join } from 'path';
14
13
  import { getPluginData } from '../config.mjs';
15
14
  import { enrichModels } from './model-catalog.mjs';
15
+ import { sanitizeModelList } from './model-list-sanitize.mjs';
16
16
  import { writeJsonAtomicSync } from '../../../shared/atomic-file.mjs';
17
17
  import { makeModelCache } from './model-cache.mjs';
18
18
 
@@ -51,20 +51,43 @@ import {
51
51
  isResponsesFreeformTool,
52
52
  toResponsesCustomTool,
53
53
  } from './custom-tool-wire.mjs';
54
+ import {
55
+ sendViaHttpSse,
56
+ _envFlag,
57
+ _envPositiveInt,
58
+ _shouldUseOpenAIHttpFallback,
59
+ } from './openai-oauth-http-sse.mjs';
60
+ import { createOpenAIOAuthLogin } from './openai-oauth-login.mjs';
61
+ import {
62
+ _displayCodexModel,
63
+ _codexFamily,
64
+ _normalizeCodexModel,
65
+ _compareVersion,
66
+ _isMainCodexFamily,
67
+ _markLatestCodex,
68
+ } from './openai-codex-model.mjs';
69
+ export { _displayCodexModel };
70
+
71
+ // Legacy import path for scripts/tool-smoke.mjs (single-emit SSE smoke).
72
+ export { sendViaHttpSse };
54
73
  // --- Constants ---
55
74
  const CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
56
- const CODEX_OAUTH_ORIGINATOR = 'codex_cli_rs';
75
+ // Exported for openai-oauth-http-sse.mjs (fallback transport headers/URL).
76
+ export const CODEX_OAUTH_ORIGINATOR = 'codex_cli_rs';
57
77
  const TOKEN_URL = 'https://auth.openai.com/oauth/token';
58
- const CODEX_RESPONSES_URL = 'https://chatgpt.com/backend-api/codex/responses';
59
- // Version string baked into the models endpoint query — the OAuth backend rejects the
60
- // request without it. Keep close to the latest published @openai/codex CLI because
61
- // older versions trigger a visibility-filtered catalog (e.g. only rollout
62
- // models). Bump when the real CLI bumps.
63
- // OpenAI OAuth backend gates new model exposures (e.g. gpt-5.5 only on >= 0.130.0)
64
- // on the client_version header. Resolve dynamically from npm so newly-shipped
65
- // models surface within a day instead of waiting on a hardcoded bump here.
66
- // Cached 24h in-process; npm failure falls back to the floor below.
67
- const CODEX_CLIENT_VERSION_FLOOR = '0.130.0';
78
+ export const CODEX_RESPONSES_URL = 'https://chatgpt.com/backend-api/codex/responses';
79
+ // Version string baked into the models endpoint query — the OAuth backend
80
+ // rejects the request without it, and gates new model exposures (e.g.
81
+ // gpt-5.5 only on >= 0.130.0) on this client_version header; older versions
82
+ // trigger a visibility-filtered catalog (e.g. only rollout models). Resolved
83
+ // dynamically from npm so newly-shipped models surface within a day instead
84
+ // of waiting on a hardcoded bump here. Cached 24h in-process; npm failure
85
+ // falls back to the floor below.
86
+ // Offline fallback only _resolveCodexClientVersion() fetches the live
87
+ // @openai/codex latest from npm first. Bumped to the current release
88
+ // (0.142.5, verified 2026-07-03) so the offline path stays close to what the
89
+ // backend expects for client-version gating.
90
+ const CODEX_CLIENT_VERSION_FLOOR = '0.142.5';
68
91
  const CODEX_VERSION_CACHE_TTL_MS = 24 * 60 * 60_000;
69
92
  let _codexVersionCache = { value: null, fetchedAt: 0 };
70
93
 
@@ -119,10 +142,6 @@ let _codexRefreshInFlight = null;
119
142
  let _oauthRefreshInFlight = null;
120
143
  let _lastCodexListModelsError = '';
121
144
 
122
- export function getOpenAIOAuthModelCatalogError() {
123
- return _lastCodexListModelsError;
124
- }
125
-
126
145
  function _codexCatalogHas(id) {
127
146
  if (!id || !Array.isArray(_inMemoryCodexCatalog)) return false;
128
147
  return _inMemoryCodexCatalog.some(m => m.id === id);
@@ -156,106 +175,6 @@ export function codexModelSupportsServiceTier(id, serviceTier) {
156
175
  return tiers.some(t => t?.id === serviceTier);
157
176
  }
158
177
 
159
- // OAuth catalog returns dated ids (gpt-5.4-mini-2026-03-17). Strip the trailing
160
- // -YYYY-MM-DD to get the version alias (gpt-5.4-mini). Unknown shapes pass
161
- // through unchanged.
162
- function _displayCodexModel(id) {
163
- if (!id || typeof id !== 'string') return id;
164
- return id.replace(/-\d{4}-\d{2}-\d{2}$/, '');
165
- }
166
-
167
- function _positiveCodexContextWindow(value) {
168
- const n = Number(value);
169
- return Number.isFinite(n) && n > 0 ? Math.floor(n) : null;
170
- }
171
-
172
- function _codexContextWindowFromApi(m) {
173
- return _positiveCodexContextWindow(m?.context_window)
174
- || _positiveCodexContextWindow(m?.max_context_window)
175
- || null;
176
- }
177
-
178
- function _normalizeCodexModel(m) {
179
- const id = m?.slug || m?.id;
180
- const family = _codexFamily(id);
181
- const serviceTiers = Array.isArray(m?.service_tiers)
182
- ? m.service_tiers
183
- .map(t => ({
184
- id: String(t?.id || '').trim(),
185
- name: String(t?.name || '').trim(),
186
- description: String(t?.description || '').trim(),
187
- }))
188
- .filter(t => t.id)
189
- : [];
190
- const additionalSpeedTiers = Array.isArray(m?.additional_speed_tiers)
191
- ? m.additional_speed_tiers.map(t => String(t || '').trim()).filter(Boolean)
192
- : [];
193
- // Catalog ids are version aliases without separate display dating.
194
- return {
195
- id,
196
- name: m?.display_name || id,
197
- display: m?.display_name || id,
198
- family,
199
- provider: 'openai-oauth',
200
- contextWindow: _codexContextWindowFromApi(m),
201
- maxContextWindow: _positiveCodexContextWindow(m?.max_context_window),
202
- outputTokens: m?.max_output_tokens || m?.output_tokens || 32768,
203
- autoCompactTokenLimit: m?.auto_compact_token_limit || null,
204
- effectiveContextWindowPercent: m?.effective_context_window_percent || null,
205
- tier: 'version',
206
- latest: false,
207
- description: m?.description || '',
208
- reasoningLevels: (m?.supported_reasoning_levels || []).map(r => r.effort),
209
- serviceTiers,
210
- defaultServiceTier: m?.default_service_tier || null,
211
- additionalSpeedTiers,
212
- };
213
- }
214
-
215
- function _codexFamily(id) {
216
- const s = String(id || '').toLowerCase();
217
- if (s.includes('nano')) return 'gpt-nano';
218
- if (s.includes('mini')) return 'gpt-mini';
219
- if (s.includes('codex')) return 'gpt-codex';
220
- if (s.startsWith('gpt-5.5')) return 'gpt-5.5';
221
- if (s.startsWith('gpt-5.4')) return 'gpt-5.4';
222
- if (s.startsWith('gpt-5.2')) return 'gpt-5.2';
223
- if (s.startsWith('gpt-5')) return 'gpt-5';
224
- return 'gpt';
225
- }
226
-
227
- // Compare two model ids by the X.Y version embedded in `gpt-X.Y`. Mirrors
228
- // anthropic-oauth's _compareVersion; these ids have no trailing date so
229
- // the version lives in the dotted number, not a -YYYY-MM-DD suffix.
230
- function _compareVersion(a, b) {
231
- const na = (String(a).match(/gpt-(\d+)\.(\d+)/) || []).slice(1).map(Number);
232
- const nb = (String(b).match(/gpt-(\d+)\.(\d+)/) || []).slice(1).map(Number);
233
- for (let i = 0; i < Math.max(na.length, nb.length); i++) {
234
- if ((na[i] || 0) !== (nb[i] || 0)) return (na[i] || 0) - (nb[i] || 0);
235
- }
236
- return String(a).localeCompare(String(b));
237
- }
238
-
239
- // Main gpt-5 chat family only: exclude the mini/nano/codex variants so "latest"
240
- // resolves to the flagship, not a smaller sibling.
241
- function _isMainCodexFamily(family) {
242
- return typeof family === 'string' && family.startsWith('gpt-5');
243
- }
244
-
245
- // Mark the highest-version model per family as `latest: true`. VERSION-based
246
- // (ids carry no `created`), mirroring anthropic-oauth's per-family pass.
247
- function _markLatestCodex(models) {
248
- const byFamily = new Map();
249
- for (const m of models) {
250
- if (!m?.id) continue;
251
- const cur = byFamily.get(m.family);
252
- if (!cur || _compareVersion(m.id, cur.id) > 0) {
253
- byFamily.set(m.family, m);
254
- }
255
- }
256
- for (const m of byFamily.values()) m.latest = true;
257
- }
258
-
259
178
  // Newest MAIN gpt-5 chat model by version, read from the SYNC in-memory
260
179
  // catalog mirror. Returns null until populated; callers warm via
261
180
  // ensureLatestCodexModel when null.
@@ -578,7 +497,35 @@ function toOpenAIResponsesTool(t) {
578
497
  };
579
498
  }
580
499
 
500
+ // codex build_reasoning() (core/src/client.rs:785-805) only attaches the
501
+ // reasoning object when model_info.supports_reasoning_summaries; models
502
+ // without summary support get NO reasoning field at all. Mirror that via the
503
+ // cached codex catalog; unknown models default to true (gpt-5 family all
504
+ // support summaries) so a cold catalog cannot strip reasoning from the wire.
505
+ function _codexModelSupportsReasoningSummaries(id) {
506
+ const info = _findCachedCodexModel(id);
507
+ if (!info) return true;
508
+ const flags = [info.supportsReasoningSummaries, info.supports_reasoning_summaries, info.supportsReasoning, info.supports_reasoning];
509
+ for (const flag of flags) {
510
+ if (typeof flag === 'boolean') return flag;
511
+ }
512
+ return true;
513
+ }
514
+
515
+ // codex reasoning_effort_for_request (core/src/client.rs): `ultra` collapses to
516
+ // `max` on the wire — the openai-oauth backend does not accept `ultra`. Every
517
+ // other effort passes through unchanged; empty/unknown falls back to medium.
518
+ function _normalizeReasoningEffort(effort) {
519
+ const e = String(effort || '').trim().toLowerCase();
520
+ if (!e) return 'medium';
521
+ if (e === 'ultra') return 'max';
522
+ return e;
523
+ }
524
+
581
525
  export function buildRequestBody(messages, model, tools, sendOpts) {
526
+ // codex reasoning_effort_for_request: `ultra` collapses to `max` on the
527
+ // wire (the only remap; every other effort passes through). Default medium.
528
+ // Kept inline (not a module const) so buildRequestBody stays self-contained.
582
529
  // Extract system/instructions
583
530
  const systemMsgs = messages.filter(m => m.role === 'system');
584
531
  const instructions = systemMsgs.map(m => m.content).join('\n\n') || 'You are a helpful assistant.';
@@ -587,7 +534,7 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
587
534
  providerState: opts.providerState,
588
535
  model,
589
536
  });
590
- // Match the body shape pi-mono and the official OpenAI CLI ship so the
537
+ // Match the request body shape the OAuth backend expects so the
591
538
  // server-side auto-cache routes correctly. text.verbosity / include /
592
539
  // tool_choice / parallel_tool_calls are all inert without side effects
593
540
  // for most callers but their presence affects how the OAuth backend classifies the
@@ -597,17 +544,32 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
597
544
  const value = String(item || '').trim();
598
545
  if (value && !include.includes(value)) include.push(value);
599
546
  }
547
+ // Field order MIRRORS codex-rs ResponsesApiRequest (common.rs struct order):
548
+ // model, instructions, input, tools, tool_choice, parallel_tool_calls,
549
+ // reasoning, store, stream, include, service_tier, prompt_cache_key, text.
550
+ // JSON serialization order is load-bearing for the server prompt cache
551
+ // (exact-prefix match): matching codex's byte layout keeps our requests on
552
+ // the same cache-routing shape codex warms. tools/service_tier/
553
+ // prompt_cache_key are appended below in the same relative order.
600
554
  const body = {
601
555
  model,
602
556
  instructions,
603
557
  input,
558
+ tool_choice: opts.toolChoice || 'auto',
559
+ parallel_tool_calls: true,
560
+ // codex build_reasoning() sends { effort, summary } — summary defaults to
561
+ // ReasoningSummary::Auto (protocol config_types.rs), serialized lowercase
562
+ // as "auto". Matching this keeps our reasoning object byte-identical to
563
+ // codex so the server prompt-cache prefix hash lines up. codex also
564
+ // normalizes `ultra` -> `max` on the wire (reasoning_effort_for_request
565
+ // in core/src/client.rs); the openai-oauth backend does not accept
566
+ // `ultra` as a wire value, so mirror that mapping here.
567
+ ...(_codexModelSupportsReasoningSummaries(model)
568
+ ? { reasoning: { effort: _normalizeReasoningEffort(opts.effort), summary: 'auto' } }
569
+ : {}),
604
570
  store: process.env.MIXDOG_OAI_STORE === 'true' ? true : false,
605
571
  stream: true,
606
- reasoning: { effort: opts.effort || 'medium' },
607
- text: { verbosity: 'medium' },
608
572
  include,
609
- tool_choice: opts.toolChoice || 'auto',
610
- parallel_tool_calls: true,
611
573
  };
612
574
  const maxOutputTokens = Number(opts.maxOutputTokens ?? opts.outputTokens ?? opts.max_output_tokens);
613
575
  if (_envFlag('MIXDOG_OPENAI_OAUTH_SEND_MAX_OUTPUT_TOKENS', false)
@@ -618,28 +580,30 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
618
580
  if (opts.fast === true) {
619
581
  // 'priority' is the only fast-class value the OpenAI OAuth backend
620
582
  // accepts on the wire: 'fast' is hard-rejected ("Unsupported
621
- // service_tier: fast", probed 2026-06-11). Match official CLI behavior:
622
- // only send the request value when the model catalog advertises it.
583
+ // service_tier: fast", probed 2026-06-11). Only send the request value
584
+ // when the model catalog advertises it.
623
585
  if (codexModelSupportsServiceTier(model, 'priority')) {
624
586
  body.service_tier = 'priority';
625
587
  }
626
588
  }
627
589
  // Add tools. `nativeTools` are server-hosted Responses tools (for
628
590
  // example web_search) and must be passed through without wrapping them as
629
- // function tools.
591
+ // function tools. codex places `tools` right after `input` (before
592
+ // tool_choice); we insert it there via a rebuilt object so serialization
593
+ // order matches, rather than appending it last.
630
594
  const functionTools = tools?.length ? tools.map(toOpenAIResponsesTool) : [];
631
595
  const nativeTools = Array.isArray(opts.nativeTools)
632
596
  ? opts.nativeTools.filter(t => t && typeof t === 'object')
633
597
  : [];
634
- if (functionTools.length || nativeTools.length) {
635
- body.tools = [...nativeTools, ...functionTools];
636
- }
598
+ const toolsList = (functionTools.length || nativeTools.length)
599
+ ? [...nativeTools, ...functionTools]
600
+ : null;
637
601
  const promptCacheProvider = opts.promptCacheProvider || 'openai-oauth';
638
602
  const promptCacheLane = opts.promptCacheLane || resolveProviderPromptCacheLane(promptCacheProvider, opts);
639
- body.prompt_cache_key = buildStableProviderPromptCacheKey(promptCacheProvider, opts, {
603
+ const promptCacheKey = buildStableProviderPromptCacheKey(promptCacheProvider, opts, {
640
604
  model,
641
605
  instructions,
642
- tools: body.tools || [],
606
+ tools: toolsList || [],
643
607
  effort: body.reasoning?.effort,
644
608
  fast: opts.fast === true,
645
609
  serviceTier: body.service_tier || '',
@@ -648,698 +612,41 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
648
612
  cacheLaneSlot: promptCacheLane.slot,
649
613
  cacheLaneShards: promptCacheLane.shards,
650
614
  });
615
+ // codex only serializes `text` when a verbosity/schema is configured
616
+ // (common.rs create_text_param_for_request returns None otherwise); it has
617
+ // no unconditional { verbosity: 'medium' }. Sending an extra text object
618
+ // shifts every following byte vs codex's prefix layout.
619
+ const verbosity = typeof opts.verbosity === 'string' && opts.verbosity.trim()
620
+ ? opts.verbosity.trim().toLowerCase()
621
+ : null;
622
+ // Rebuild the body in codex struct order so JSON serialization is
623
+ // byte-compatible with codex: ... input, tools, tool_choice,
624
+ // parallel_tool_calls, reasoning, store, stream, include, service_tier,
625
+ // prompt_cache_key, text. service_tier is only present when fast set it.
626
+ const ordered = {
627
+ model: body.model,
628
+ instructions: body.instructions,
629
+ input: body.input,
630
+ ...(toolsList ? { tools: toolsList } : {}),
631
+ tool_choice: body.tool_choice,
632
+ parallel_tool_calls: body.parallel_tool_calls,
633
+ reasoning: body.reasoning,
634
+ store: body.store,
635
+ stream: body.stream,
636
+ include: body.include,
637
+ ...(body.service_tier ? { service_tier: body.service_tier } : {}),
638
+ prompt_cache_key: promptCacheKey,
639
+ ...(verbosity ? { text: { verbosity } } : {}),
640
+ ...(body.max_output_tokens ? { max_output_tokens: body.max_output_tokens } : {}),
641
+ };
651
642
  // NOTE: prompt_cache_retention is a public OpenAI Responses API parameter,
652
643
  // but the openai-oauth endpoint still rejects it ("Unsupported parameter:
653
644
  // prompt_cache_retention", re-probed 2026-06-22). Leave retention on the
654
645
  // openai-oauth server default; public OpenAI direct injects 24h separately.
655
- return body;
656
- }
657
-
658
- function _envFlag(name, fallback = true) {
659
- const raw = process.env[name];
660
- if (raw == null || raw === '') return fallback;
661
- return !['0', 'false', 'off', 'no'].includes(String(raw).toLowerCase());
662
- }
663
-
664
- function _envPositiveInt(name, fallback) {
665
- const raw = process.env[name];
666
- if (raw == null || raw === '') return fallback;
667
- const n = Number(raw);
668
- return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
669
- }
670
-
671
- // Completed function_call.arguments parse for the OpenAI Responses stream.
672
- // Native convergence (openai-oauth / anthropic-oauth / opencode): a function_call item
673
- // arrives only on a completion/done signal, so a non-empty-but-malformed
674
- // arguments string is deterministic bad JSON — NOT mid-stream truncation.
675
- // Empty/whitespace input legitimately means "no arguments" → {}. A non-empty
676
- // string that fails JSON.parse is surfaced as an invalid-args MARKER (instead
677
- // of being silently swallowed to {}) so the dispatch loop turns it into an
678
- // is_error tool_result and the model self-corrects in the same turn.
679
- function _parseJsonObject(value) {
680
- const text = typeof value === 'string' ? value : (value == null ? '' : String(value));
681
- if (text.trim() === '') return {};
682
- try {
683
- const parsed = JSON.parse(text);
684
- return parsed && typeof parsed === 'object' ? parsed : {};
685
- } catch (err) {
686
- return makeInvalidToolArgsMarker(text, err instanceof Error ? err.message : String(err));
687
- }
646
+ return ordered;
688
647
  }
689
648
 
690
- function _extractCachedTokens(usage) {
691
- const details = usage?.input_tokens_details || usage?.prompt_tokens_details || {};
692
- return Number(details.cached_tokens ?? details.cached ?? usage?.cached_tokens ?? 0) || 0;
693
- }
694
-
695
- function _sseEventsFromBuffer(buffer) {
696
- const frames = [];
697
- let rest = buffer.replace(/\r\n/g, '\n');
698
- let idx;
699
- while ((idx = rest.indexOf('\n\n')) >= 0) {
700
- frames.push(rest.slice(0, idx));
701
- rest = rest.slice(idx + 2);
702
- }
703
- return { frames, rest };
704
- }
705
-
706
- function _parseSseFrame(frame) {
707
- const lines = String(frame || '').split('\n');
708
- const data = [];
709
- for (const line of lines) {
710
- if (!line || line.startsWith(':')) continue;
711
- if (line.startsWith('data:')) data.push(line.slice(5).trimStart());
712
- }
713
- if (!data.length) return null;
714
- const raw = data.join('\n').trim();
715
- if (!raw || raw === '[DONE]') return null;
716
- try { return JSON.parse(raw); } catch { return null; }
717
- }
718
-
719
- function _incompleteReasonFromEvent(event) {
720
- const reasonObj = event?.response?.incomplete_details
721
- || event?.incomplete_details
722
- || event?.response?.status_details
723
- || null;
724
- return String(reasonObj?.reason || event?.response?.status || 'incomplete');
725
- }
726
-
727
- function _isMaxOutputIncompleteReason(reason) {
728
- return /^(?:max_output_tokens|max_tokens|length|output_token_limit)$/i.test(String(reason || '').trim());
729
- }
730
-
731
- function _pushOutputTextAnnotations(part, citations, citationKeys) {
732
- const annotations = Array.isArray(part?.annotations) ? part.annotations : [];
733
- for (const raw of annotations) {
734
- const url = raw?.url || raw?.uri || raw?.href || '';
735
- if (!url || citationKeys.has(url)) continue;
736
- citationKeys.add(url);
737
- citations.push({
738
- title: raw?.title || '',
739
- url,
740
- snippet: raw?.snippet || raw?.text || raw?.description || '',
741
- source: 'openai-oauth',
742
- });
743
- }
744
- }
745
-
746
- function _buildOpenAIHttpFallbackHeaders({ auth, cacheKey }) {
747
- const headers = {
748
- Authorization: `Bearer ${auth.access_token}`,
749
- 'Content-Type': 'application/json',
750
- Accept: 'text/event-stream',
751
- 'OpenAI-Beta': 'responses=experimental',
752
- originator: CODEX_OAUTH_ORIGINATOR,
753
- 'chatgpt-account-id': auth.account_id || '',
754
- 'x-client-request-id': randomBytes(16).toString('hex'),
755
- };
756
- if (cacheKey) headers.session_id = String(cacheKey);
757
- return headers;
758
- }
759
-
760
- // WS→HTTP/SSE fallback predicate → shared shouldFallbackTransport
761
- // (retry-classifier.mjs). The per-provider env flag is computed here and passed
762
- // as `enabled`; the deny-order + allow-list are identical to the former copy.
763
- function _shouldUseOpenAIHttpFallback(err, externalSignal) {
764
- return shouldFallbackTransport(err, {
765
- signal: externalSignal,
766
- enabled: _envFlag('MIXDOG_OPENAI_OAUTH_HTTP_FALLBACK', true),
767
- });
768
- }
769
-
770
- // Exported for the single-emit regression smoke (scripts/openai-oauth-
771
- // http-sse-toolcall-smoke.mjs): the SSE stream can surface the same
772
- // function_call across response.function_call_arguments.done +
773
- // response.output_item.done + response.completed, and onToolCall must fire
774
- // exactly once per call id. No production caller imports this name; the
775
- // provider invokes it internally.
776
- export async function sendViaHttpSse({
777
- auth,
778
- body,
779
- opts,
780
- onStreamDelta,
781
- onToolCall,
782
- onTextDelta,
783
- onStageChange,
784
- externalSignal,
785
- poolKey,
786
- cacheKey,
787
- iteration,
788
- useModel,
789
- fetchFn = fetch,
790
- } = {}) {
791
- // P1 audit fix: no fixed wall-clock total cap on the HTTP/SSE fallback
792
- // stream. The old createTimeoutSignal(..., PROVIDER_GENERATE_TOTAL_TIMEOUT_MS)
793
- // killed a healthy, still-streaming turn purely on elapsed time, unlike
794
- // every other streaming provider path (anthropic-oauth uses the same
795
- // createPassthroughSignal pattern — see anthropic-oauth.mjs "Option A").
796
- // The stream is bounded instead by:
797
- // (a) headerTimeout below (PROVIDER_HTTP_RESPONSE_TIMEOUT_MS) for a
798
- // socket that never sends the initial response,
799
- // (b) the SEMANTIC idle watchdog (_armSemanticIdle /
800
- // PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS), which resets on every
801
- // meaningful() chunk — a live stream stays alive, a truly silent
802
- // one still aborts, and
803
- // (c) externalSignal (client disconnect / replaced-by-newer-request).
804
- const totalTimeout = createPassthroughSignal(externalSignal);
805
- const headerTimeout = createTimeoutSignal(
806
- totalTimeout.signal,
807
- PROVIDER_HTTP_RESPONSE_TIMEOUT_MS,
808
- 'OpenAI OAuth HTTP fallback initial response',
809
- );
810
- const headers = _buildOpenAIHttpFallbackHeaders({ auth, cacheKey });
811
- const fetchStartedAt = Date.now();
812
- let response;
813
- try {
814
- try { onStageChange?.('requesting'); } catch {}
815
- response = await fetchFn(CODEX_RESPONSES_URL, {
816
- method: 'POST',
817
- headers,
818
- body: JSON.stringify(body),
819
- signal: headerTimeout.signal,
820
- dispatcher: getLlmDispatcher(),
821
- });
822
- } catch (err) {
823
- if (headerTimeout.signal?.aborted && headerTimeout.signal.reason instanceof Error) throw headerTimeout.signal.reason;
824
- throw err;
825
- } finally {
826
- headerTimeout.cleanup();
827
- }
828
-
829
- traceAgentFetch({
830
- sessionId: poolKey,
831
- headersMs: Date.now() - fetchStartedAt,
832
- httpStatus: response.status,
833
- provider: 'openai-oauth',
834
- model: useModel,
835
- transport: 'http',
836
- });
837
-
838
- if (!response.ok) {
839
- const text = await response.text().catch(() => '');
840
- const err = new Error(`OpenAI OAuth HTTP fallback ${response.status}: ${text.slice(0, 200)}`);
841
- err.httpStatus = response.status;
842
- err.headers = response.headers;
843
- populateHttpStatusFromMessage(err, text);
844
- totalTimeout.cleanup();
845
- throw err;
846
- }
847
- if (!response.body) {
848
- totalTimeout.cleanup();
849
- throw new Error('OpenAI OAuth HTTP fallback returned no response body');
850
- }
851
-
852
- try { onStageChange?.('streaming'); } catch {}
853
- const sseStartedAt = Date.now();
854
- const reader = response.body.getReader();
855
- const decoder = new TextDecoder();
856
- // After headerTimeout.cleanup() the in-flight fetch no longer carries a live
857
- // signal, so a totalTimeout / external abort that fires during a pending
858
- // reader.read() would otherwise leave the pooled request hanging. Keep the
859
- // reader tied to totalTimeout for the whole stream: on abort, cancel the
860
- // reader so the awaited read() unblocks and the socket is released back to
861
- // the shared pool instead of leaking. reader.cancel() may resolve the
862
- // pending read() as {done:true} rather than rejecting, which would let a
863
- // partial response surface as success — so record the abort reason and
864
- // re-throw it after the loop unblocks (see below).
865
- let _streamAbortReason = null;
866
- let _onTotalAbort = null;
867
- if (totalTimeout.signal) {
868
- _onTotalAbort = () => {
869
- const reason = totalTimeout.signal.reason;
870
- _streamAbortReason = reason instanceof Error
871
- ? reason
872
- : new Error('OpenAI OAuth HTTP fallback aborted');
873
- try { reader.cancel(_streamAbortReason).catch(() => {}); } catch {}
874
- };
875
- if (totalTimeout.signal.aborted) _onTotalAbort();
876
- else totalTimeout.signal.addEventListener('abort', _onTotalAbort, { once: true });
877
- }
878
- // SEMANTIC idle watchdog: reset ONLY on meaningful() (text/reasoning/tool
879
- // deltas), never on raw bytes/keepalive frames, so a stream that emits some
880
- // deltas then goes silent trips a short, named terminal failure instead of
881
- // hanging until the 30-min agent watchdog. Disablable via the shared env.
882
- let _semanticIdleTimer = null;
883
- const _clearSemanticIdle = () => {
884
- if (_semanticIdleTimer) { clearTimeout(_semanticIdleTimer); _semanticIdleTimer = null; }
885
- };
886
- const _armSemanticIdle = () => {
887
- if (!PROVIDER_SSE_IDLE_WATCHDOG_ENABLED || !(PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS > 0)) return;
888
- _clearSemanticIdle();
889
- _semanticIdleTimer = setTimeout(() => {
890
- _streamAbortReason = streamStalledError('OpenAI OAuth HTTP fallback', PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS, { emittedToolCall: emittedToolCallIds.size > 0 });
891
- // Partial-final recovery parity with anthropic-oauth: attach the
892
- // streamed partial state so the agent loop can accept a wedged FINAL
893
- // no-tool summary as a successful partial-final instead of dropping
894
- // the result. pendingToolUse gates out any mid-flight tool call.
895
- try {
896
- _streamAbortReason.partialContent = content;
897
- _streamAbortReason.partialToolCalls = toolCalls.length ? toolCalls.slice() : undefined;
898
- _streamAbortReason.pendingToolUse = pendingCalls.size > 0 || emittedToolCallIds.size > 0;
899
- _streamAbortReason.partialModel = model || undefined;
900
- } catch { /* best-effort enrichment */ }
901
- try { reader.cancel(_streamAbortReason).catch(() => {}); } catch {}
902
- }, PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS);
903
- try { _semanticIdleTimer.unref?.(); } catch {}
904
- };
905
- let buffer = '';
906
- let content = '';
907
- let model = '';
908
- let responseId = '';
909
- let serviceTier = '';
910
- let usage = null;
911
- let ttftMs = null;
912
- const toolCalls = [];
913
- const pendingCalls = new Map();
914
- const reasoningItems = [];
915
- const citations = [];
916
- const citationKeys = new Set();
917
- const webSearchCalls = [];
918
- const webSearchCallKeys = new Set();
919
- let completed = false;
920
- let stopReason = null;
921
- // Gateway live-text relay invariant: set once a non-empty text chunk has
922
- // been forwarded to the client. A failure afterwards is non-retryable —
923
- // the rendered text cannot be withdrawn and a re-request would concatenate
924
- // a second attempt.
925
- let emittedText = false;
926
-
927
- // Single-emit guard for tool calls (matches the WS path's
928
- // emittedToolCall intent). The HTTP/SSE event stream can surface the
929
- // same function_call across multiple frames — response.function_call_arguments.done,
930
- // response.output_item.done, and the final response.completed.output
931
- // bundle. Each frame independently completes the call (id + name) and
932
- // would re-invoke onToolCall, double-executing a side-effecting tool.
933
- // Route every emit through emitToolCall: it fires the callback exactly
934
- // once per unique call id, the first time the call is complete. A call
935
- // whose id/name only arrives in a later frame is NOT dropped — its
936
- // first complete frame still emits; only redundant re-emits are
937
- // suppressed.
938
- const emittedToolCallIds = new Set();
939
- // Fix 2: cross-path name+args dedupe. A text-leaked synthetic and an
940
- // identical native function_call must fire onToolCall exactly once.
941
- const _toolDedupe = createToolCallDedupe();
942
- const emitToolCall = (call) => {
943
- if (!call || !call.id) return;
944
- if (emittedToolCallIds.has(call.id)) return;
945
- emittedToolCallIds.add(call.id);
946
- if (!_toolDedupe.shouldDispatch(call.name, call.arguments)) return;
947
- try { onToolCall?.(call); } catch {}
948
- };
949
-
950
- // Leaked tool-call guard. The model sometimes emits a tool call as plain
951
- // text (XML `<invoke>`/`<function_calls>` or gpt-oss harmony
952
- // `<|channel|>...to=functions.NAME...<|call|>`) inside
953
- // `response.output_text.delta` instead of a native function_call. Route
954
- // text through the guard so leaked calls are suppressed from the visible
955
- // stream, synthesized (native `call_...` id shape), and dispatched like
956
- // native ones. Known tool names come from the request body so recovery
957
- // only fires for tools the model was actually offered. Additive: the
958
- // native function_call path is untouched.
959
- const _leakKnownTools = new Set(
960
- (Array.isArray(body?.tools) ? body.tools : [])
961
- .map((t) => (typeof t?.name === 'string' ? t.name : null))
962
- .filter(Boolean),
963
- );
964
- const leakGuard = createLeakGuard({ knownToolNames: _leakKnownTools, harmony: true });
965
- const dispatchLeakedCall = (recovered) => {
966
- let args = recovered?.arguments;
967
- if (args === null || typeof args !== 'object' || Array.isArray(args)) args = {};
968
- const call = {
969
- id: `call_leaked_${randomBytes(8).toString('hex')}`,
970
- name: recovered.name,
971
- arguments: args,
972
- };
973
- toolCalls.push(call);
974
- emitToolCall(call);
975
- };
976
- const relayLeakText = (delta) => {
977
- if (!leakGuard.enabled) {
978
- content += delta || '';
979
- if (delta && onTextDelta) {
980
- emittedText = true;
981
- try { onTextDelta(delta); } catch {}
982
- }
983
- return;
984
- }
985
- const { text, calls } = leakGuard.push(delta);
986
- if (text) {
987
- content += text;
988
- if (onTextDelta) {
989
- emittedText = true;
990
- try { onTextDelta(text); } catch {}
991
- }
992
- }
993
- for (const c of calls) dispatchLeakedCall(c);
994
- };
995
- const flushLeak = () => {
996
- if (!leakGuard.enabled) return;
997
- const { text, calls } = leakGuard.flush();
998
- if (text) {
999
- content += text;
1000
- if (onTextDelta) {
1001
- emittedText = true;
1002
- try { onTextDelta(text); } catch {}
1003
- }
1004
- }
1005
- for (const c of calls) dispatchLeakedCall(c);
1006
- };
1007
-
1008
- const pushWebSearchCall = (item) => {
1009
- if (!item || item.type !== 'web_search_call') return;
1010
- const key = item.id || JSON.stringify(item.action || item);
1011
- if (webSearchCallKeys.has(key)) return;
1012
- webSearchCallKeys.add(key);
1013
- webSearchCalls.push({ id: item.id || '', status: item.status || '', action: item.action || null });
1014
- };
1015
- const pushReasoningItem = (item) => {
1016
- if (item?.type === 'reasoning' && item.encrypted_content && !reasoningItems.some(r => r.id === item.id)) {
1017
- reasoningItems.push({
1018
- id: item.id || '',
1019
- encrypted_content: item.encrypted_content,
1020
- summary: Array.isArray(item.summary) ? item.summary : [],
1021
- });
1022
- }
1023
- };
1024
- const pushToolSearchCall = (item) => {
1025
- if (!item || item.type !== 'tool_search_call') return;
1026
- const callId = item.call_id || item.id || '';
1027
- if (!callId || toolCalls.some(t => t.id === callId)) return;
1028
- let args = {};
1029
- if (item.arguments && typeof item.arguments === 'object') {
1030
- args = item.arguments;
1031
- } else if (typeof item.arguments === 'string' && item.arguments.trim()) {
1032
- // Non-empty but malformed tool_search arguments are deterministic
1033
- // bad JSON (the item is only emitted on completion). Surface an
1034
- // invalid-args marker instead of swallowing to {} so the model can
1035
- // self-correct in the same turn.
1036
- args = _parseJsonObject(item.arguments);
1037
- }
1038
- const call = {
1039
- id: callId,
1040
- name: 'tool_search',
1041
- arguments: args,
1042
- nativeType: 'tool_search_call',
1043
- };
1044
- toolCalls.push(call);
1045
- emitToolCall(call);
1046
- };
1047
- const pushCustomToolCall = (item) => {
1048
- const call = customToolCallFromResponseItem(item);
1049
- if (!call || toolCalls.some(t => t.id === call.id)) return;
1050
- toolCalls.push(call);
1051
- emitToolCall(call);
1052
- };
1053
- const meaningful = () => {
1054
- if (ttftMs == null) ttftMs = Date.now() - sseStartedAt;
1055
- _armSemanticIdle();
1056
- try { onStreamDelta?.(); } catch {}
1057
- };
1058
- const handleEvent = (event) => {
1059
- if (!event || typeof event.type !== 'string') return;
1060
- switch (event.type) {
1061
- case 'response.created':
1062
- if (event.response?.model) model = event.response.model;
1063
- if (event.response?.id) responseId = event.response.id;
1064
- break;
1065
- case 'response.output_text.delta':
1066
- meaningful();
1067
- relayLeakText(event.delta || '');
1068
- break;
1069
- case 'response.reasoning_text.delta':
1070
- case 'response.reasoning_summary_text.delta':
1071
- meaningful();
1072
- break;
1073
- case 'response.output_item.added':
1074
- if (event.item?.type === 'function_call') {
1075
- pendingCalls.set(event.item.id || '', {
1076
- name: event.item.name || '',
1077
- callId: event.item.call_id || '',
1078
- });
1079
- }
1080
- break;
1081
- case 'response.function_call_arguments.delta':
1082
- meaningful();
1083
- break;
1084
- case 'response.function_call_arguments.done': {
1085
- const itemId = event.item_id || '';
1086
- const pending = pendingCalls.get(itemId);
1087
- const call = {
1088
- id: pending?.callId || event.call_id || '',
1089
- name: pending?.name || event.name || '',
1090
- arguments: _parseJsonObject(event.arguments),
1091
- _pendingItemId: itemId,
1092
- };
1093
- toolCalls.push(call);
1094
- if (call.id && call.name) {
1095
- delete call._pendingItemId;
1096
- emitToolCall(call);
1097
- }
1098
- meaningful();
1099
- break;
1100
- }
1101
- case 'response.custom_tool_call_input.delta':
1102
- meaningful();
1103
- break;
1104
- case 'response.output_item.done': {
1105
- const item = event.item || {};
1106
- pushReasoningItem(item);
1107
- pushWebSearchCall(item);
1108
- if (item.type === 'function_call') {
1109
- const tc = toolCalls.find(t => t._pendingItemId === (item.id || ''));
1110
- if (tc) {
1111
- if (!tc.id && item.call_id) tc.id = item.call_id;
1112
- if (!tc.name && item.name) tc.name = item.name;
1113
- if (tc.id && tc.name) {
1114
- delete tc._pendingItemId;
1115
- emitToolCall(tc);
1116
- }
1117
- }
1118
- } else if (item.type === 'tool_search_call') {
1119
- pushToolSearchCall(item);
1120
- } else if (item.type === 'custom_tool_call') {
1121
- pushCustomToolCall(item);
1122
- meaningful();
1123
- }
1124
- break;
1125
- }
1126
- case 'response.completed': {
1127
- const resp = event.response || {};
1128
- serviceTier = resp.service_tier || resp.serviceTier || serviceTier;
1129
- if (!model && resp.model) model = resp.model;
1130
- if (!responseId && resp.id) responseId = resp.id;
1131
- if (resp.usage) {
1132
- usage = {
1133
- inputTokens: resp.usage.input_tokens || 0,
1134
- outputTokens: resp.usage.output_tokens || 0,
1135
- cachedTokens: _extractCachedTokens(resp.usage),
1136
- promptTokens: resp.usage.input_tokens || 0,
1137
- raw: serviceTier ? { ...resp.usage, service_tier: serviceTier } : resp.usage,
1138
- };
1139
- }
1140
- for (const item of resp.output || []) {
1141
- if (item.type === 'message') {
1142
- for (const part of item.content || []) {
1143
- if (!content && part.type === 'output_text') {
1144
- // Completed-output fallback (no streamed text).
1145
- // Route through the leak guard so a tool call
1146
- // leaked only in the final bundle is recovered
1147
- // rather than surfaced as visible content. push
1148
- // with final=true flushes fully (no held tail).
1149
- if (leakGuard.enabled) {
1150
- const { text, calls } = leakGuard.push(part.text || '', true);
1151
- content += text;
1152
- for (const c of calls) dispatchLeakedCall(c);
1153
- } else {
1154
- content += part.text || '';
1155
- }
1156
- }
1157
- if (part.type === 'output_text') _pushOutputTextAnnotations(part, citations, citationKeys);
1158
- }
1159
- } else if (item.type === 'reasoning') {
1160
- pushReasoningItem(item);
1161
- } else if (item.type === 'web_search_call') {
1162
- pushWebSearchCall(item);
1163
- } else if (item.type === 'tool_search_call') {
1164
- pushToolSearchCall(item);
1165
- } else if (item.type === 'custom_tool_call') {
1166
- pushCustomToolCall(item);
1167
- meaningful();
1168
- } else if (item.type === 'function_call') {
1169
- // Match the still-pending placeholder by item id, or
1170
- // an already-recorded call by its canonical call_id —
1171
- // so a call completed at args.done / output_item.done
1172
- // is reused here rather than re-pushed as a duplicate.
1173
- const tc = toolCalls.find(t =>
1174
- t._pendingItemId === (item.id || '')
1175
- || (item.call_id && t.id === item.call_id));
1176
- if (tc) {
1177
- if (!tc.id && item.call_id) tc.id = item.call_id;
1178
- if (!tc.name && item.name) tc.name = item.name;
1179
- if (tc.id && tc.name) {
1180
- delete tc._pendingItemId;
1181
- emitToolCall(tc);
1182
- }
1183
- } else if (item.call_id && item.name) {
1184
- const call = {
1185
- id: item.call_id,
1186
- name: item.name,
1187
- arguments: _parseJsonObject(item.arguments),
1188
- };
1189
- toolCalls.push(call);
1190
- emitToolCall(call);
1191
- }
1192
- }
1193
- }
1194
- completed = true;
1195
- break;
1196
- }
1197
- case 'response.done':
1198
- if (!event.response || event.response.status === 'completed') completed = true;
1199
- else if (event.response.status === 'failed') {
1200
- const msg = event.response?.error?.message || 'response.done failed';
1201
- const err = new Error(`OpenAI OAuth HTTP fallback response.done failed: ${msg}`);
1202
- populateHttpStatusFromMessage(err, msg);
1203
- throw err;
1204
- } else if (event.response.status === 'incomplete') {
1205
- const reason = _incompleteReasonFromEvent(event);
1206
- if (_isMaxOutputIncompleteReason(reason)) {
1207
- completed = true;
1208
- stopReason = 'length';
1209
- break;
1210
- }
1211
- throw new Error(`OpenAI OAuth HTTP fallback response.done incomplete: ${reason}`);
1212
- }
1213
- break;
1214
- case 'response.failed': {
1215
- const msg = event.response?.error?.message || event.error?.message || event.message || 'response.failed';
1216
- const err = new Error(`OpenAI OAuth HTTP fallback response.failed: ${msg}`);
1217
- populateHttpStatusFromMessage(err, msg);
1218
- throw err;
1219
- }
1220
- case 'response.incomplete': {
1221
- const reason = _incompleteReasonFromEvent(event);
1222
- if (_isMaxOutputIncompleteReason(reason)) {
1223
- completed = true;
1224
- stopReason = 'length';
1225
- break;
1226
- }
1227
- throw new Error(`OpenAI OAuth HTTP fallback response.incomplete: ${reason}`);
1228
- }
1229
- case 'error': {
1230
- const msg = event.message || event.error?.message || 'unknown';
1231
- const err = new Error(`OpenAI OAuth HTTP fallback error: ${msg}`);
1232
- populateHttpStatusFromMessage(err, msg);
1233
- throw err;
1234
- }
1235
- default:
1236
- break;
1237
- }
1238
- };
1239
-
1240
- try {
1241
- while (true) {
1242
- if (totalTimeout.signal?.aborted) {
1243
- _clearSemanticIdle();
1244
- const reason = totalTimeout.signal.reason;
1245
- throw reason instanceof Error ? reason : new Error('OpenAI OAuth HTTP fallback aborted');
1246
- }
1247
- if (_streamAbortReason) throw _streamAbortReason;
1248
- const { value, done } = await reader.read();
1249
- if (done) break;
1250
- buffer += decoder.decode(value, { stream: true });
1251
- const parsed = _sseEventsFromBuffer(buffer);
1252
- buffer = parsed.rest;
1253
- for (const frame of parsed.frames) {
1254
- const event = _parseSseFrame(frame);
1255
- if (event) handleEvent(event);
1256
- }
1257
- }
1258
- // The read() above can unblock via reader.cancel() as {done:true} on an
1259
- // external/total-timeout abort. Surface that as the abort/timeout error
1260
- // instead of treating the partial stream as a successful response.
1261
- if (_streamAbortReason) throw _streamAbortReason;
1262
- buffer += decoder.decode();
1263
- const parsed = _sseEventsFromBuffer(buffer + '\n\n');
1264
- for (const frame of parsed.frames) {
1265
- const event = _parseSseFrame(frame);
1266
- if (event) handleEvent(event);
1267
- }
1268
- // Flush any partial-sentinel tail held back mid-stream so legitimate
1269
- // trailing text is never lost (streamed-text path).
1270
- flushLeak();
1271
- } catch (err) {
1272
- // Live-text invariant: once a non-empty chunk has been relayed it
1273
- // cannot be withdrawn — flag the error so no upstream layer retries.
1274
- if (emittedText && err) { try { err.liveTextEmitted = true; err.unsafeToRetry = true; } catch {} }
1275
- throw err;
1276
- } finally {
1277
- _clearSemanticIdle();
1278
- try { reader.releaseLock?.(); } catch {}
1279
- if (_onTotalAbort && totalTimeout.signal) {
1280
- try { totalTimeout.signal.removeEventListener('abort', _onTotalAbort); } catch {}
1281
- }
1282
- totalTimeout.cleanup();
1283
- }
1284
-
1285
- const unresolved = toolCalls.find(t => t._pendingItemId);
1286
- if (unresolved) {
1287
- throw new Error(`OpenAI OAuth HTTP fallback function_call salvage failed: missing call_id/name for item_id=${unresolved._pendingItemId || '?'}`);
1288
- }
1289
- if (!completed && !content && !toolCalls.length) {
1290
- throw new Error('OpenAI OAuth HTTP fallback ended before response.completed');
1291
- }
1292
-
1293
- const liveModel = model || useModel;
1294
- traceAgentSse({
1295
- sessionId: poolKey,
1296
- sseParseMs: Date.now() - sseStartedAt,
1297
- ttftMs,
1298
- provider: 'openai-oauth',
1299
- model: liveModel,
1300
- transport: 'sse',
1301
- });
1302
- if (usage) {
1303
- traceAgentUsage({
1304
- sessionId: poolKey,
1305
- iteration,
1306
- inputTokens: usage.inputTokens || 0,
1307
- outputTokens: usage.outputTokens || 0,
1308
- cachedTokens: usage.cachedTokens || 0,
1309
- promptTokens: usage.promptTokens || 0,
1310
- model: liveModel,
1311
- modelDisplay: _displayCodexModel(liveModel),
1312
- responseId: responseId || null,
1313
- rawUsage: usage.raw || null,
1314
- provider: 'openai-oauth',
1315
- serviceTier,
1316
- });
1317
- }
1318
- // Dedupe the returned array by name+args (Fix 2, array side): a synthetic
1319
- // leaked call and an identical native function_call must not both survive,
1320
- // else the agent loop executes the side-effecting tool twice.
1321
- const _returnedToolCalls = toolCalls.length
1322
- ? dedupeToolCallList(toolCalls.map(({ _pendingItemId, ...t }) => t))
1323
- : undefined;
1324
- return {
1325
- content,
1326
- model: liveModel,
1327
- reasoningItems: reasoningItems.length ? reasoningItems : undefined,
1328
- toolCalls: _returnedToolCalls,
1329
- citations: citations.length ? citations : undefined,
1330
- webSearchCalls: webSearchCalls.length ? webSearchCalls : undefined,
1331
- usage: usage || undefined,
1332
- stopReason: stopReason || undefined,
1333
- // P1 audit fix: text-only max-output cutoff (openai-oauth HTTP/SSE
1334
- // fallback maps status:'incomplete'/reason=max_output_tokens to
1335
- // stopReason='length' above and treats it as success). Flag it so
1336
- // loop.mjs can surface a truncation warning instead of accepting
1337
- // silently-cut content as a clean final answer.
1338
- ...(stopReason === 'length' && content.length > 0 ? { truncated: true } : {}),
1339
- responseId: responseId || undefined,
1340
- serviceTier: serviceTier || undefined,
1341
- };
1342
- }
649
+ // --- HTTP/SSE fallback transport: extracted to openai-oauth-http-sse.mjs ---
1343
650
 
1344
651
  // --- Provider ---
1345
652
  export class OpenAIOAuthProvider {
@@ -1490,18 +797,12 @@ export class OpenAIOAuthProvider {
1490
797
  const _authP = this.ensureAuth();
1491
798
  let auth = await _authP;
1492
799
  const body = await _bodyP;
1493
- // poolKey cacheKey by design (see openai-oauth-ws.mjs header note).
1494
- // poolKey is per-session so parallel reviewer/worker callers each
1495
- // get their own socket bucket a sibling cannot grab a mid-turn
1496
- // entry and trip the backend's "No tool call found for function call
1497
- // output with call_id …" rejection. cacheKey is prefix-scoped
1498
- // (base namespace + model/system/tools hash) and feeds both
1499
- // `body.prompt_cache_key` and the handshake `session_id` header, so
1500
- // compatible prefixes share cache without main/worker lanes evicting
1501
- // each other.
1502
- // poolKey defaults to sessionId (per-session socket isolation); cacheKey
1503
- // never falls back to sessionId, so a fresh session still reuses the
1504
- // warm prefix cache for the same route/prefix.
800
+ // poolKey != cacheKey by design (see openai-oauth-ws.mjs header note).
801
+ // poolKey is per-session so parallel reviewer/worker callers each get
802
+ // their own socket bucket. cacheKey is the Codex-style prompt_cache_key:
803
+ // by default it is the session/thread identity (clamped to 64 chars) and
804
+ // feeds both `body.prompt_cache_key` and the OAuth WS handshake
805
+ // `session_id`, so each long-lived thread keeps a stable cache shard.
1505
806
  const poolKey = opts.sessionId || null;
1506
807
  const cacheKey = body.prompt_cache_key || resolveProviderCacheKey(opts, 'openai-oauth');
1507
808
  const iteration = Number.isFinite(Number(opts.iteration)) ? Number(opts.iteration) : null;
@@ -1717,7 +1018,7 @@ export class OpenAIOAuthProvider {
1717
1018
  const items = Array.isArray(data?.models) ? data.models : [];
1718
1019
  const normalized = items.map(m => _normalizeCodexModel(m));
1719
1020
  _markLatestCodex(normalized);
1720
- const enriched = await enrichModels(normalized);
1021
+ const enriched = sanitizeModelList((await enrichModels(normalized)).filter(Boolean), { provider: 'openai-oauth' });
1721
1022
  await _saveCodexModelCache(enriched);
1722
1023
  _lastCodexListModelsError = '';
1723
1024
  return enriched;
@@ -1755,7 +1056,7 @@ export class OpenAIOAuthProvider {
1755
1056
  const items = Array.isArray(data?.models) ? data.models : [];
1756
1057
  const normalized = items.map(m => _normalizeCodexModel(m));
1757
1058
  _markLatestCodex(normalized);
1758
- const enriched = await enrichModels(normalized);
1059
+ const enriched = sanitizeModelList((await enrichModels(normalized)).filter(Boolean), { provider: 'openai-oauth' });
1759
1060
  await _saveCodexModelCache(enriched);
1760
1061
  if (!process.env.MIXDOG_QUIET_PROVIDER_LOG) process.stderr.write(`[openai-oauth] catalog refreshed (${enriched.length} models)\n`);
1761
1062
  return enriched;
@@ -1774,168 +1075,12 @@ export class OpenAIOAuthProvider {
1774
1075
  }
1775
1076
  }
1776
1077
 
1777
- const AUTHORIZE_URL = 'https://auth.openai.com/oauth/authorize';
1778
- const CODEX_OAUTH_SCOPE = 'openid profile email offline_access api.connectors.read api.connectors.invoke';
1779
- const CALLBACK_HOST = '127.0.0.1';
1780
- const CALLBACK_PORT = 1455;
1781
- const CALLBACK_PATH = '/auth/callback';
1782
- const REDIRECT_URI = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
1783
- const LOGIN_TIMEOUT_MS = 5 * 60_000;
1784
- const TOKEN_TIMEOUT_MS = 30_000;
1785
-
1786
- function generatePKCE() {
1787
- const verifier = randomBytes(64).toString('base64url');
1788
- const challenge = createHash('sha256').update(verifier).digest('base64url');
1789
- return { verifier, challenge };
1790
- }
1791
-
1792
- function _scrubOAuthLoginBody(text) {
1793
- return String(text || '')
1794
- .replace(/"access_token"\s*:\s*"[^"]+"/g, '"access_token":"[REDACTED]"')
1795
- .replace(/"refresh_token"\s*:\s*"[^"]+"/g, '"refresh_token":"[REDACTED]"')
1796
- .replace(/"id_token"\s*:\s*"[^"]+"/g, '"id_token":"[REDACTED]"')
1797
- .replace(/[A-Za-z0-9_-]{32,}\.[A-Za-z0-9._-]+/g, '[REDACTED]');
1798
- }
1799
-
1800
- function _parseOAuthCodeInput(input) {
1801
- const value = String(input || '').trim();
1802
- if (!value) return { code: '', state: '' };
1803
- try {
1804
- const url = new URL(value);
1805
- const code = url.searchParams.get('code') || '';
1806
- const state = url.searchParams.get('state') || '';
1807
- if (code || state) return { code, state };
1808
- } catch { /* not a URL */ }
1809
- if (value.includes('#')) {
1810
- const [code, state] = value.split('#', 2);
1811
- return { code: String(code || '').trim(), state: String(state || '').trim() };
1812
- }
1813
- if (value.includes('code=')) {
1814
- const params = new URLSearchParams(value.startsWith('?') ? value.slice(1) : value);
1815
- return { code: params.get('code') || '', state: params.get('state') || '' };
1816
- }
1817
- return { code: value, state: '' };
1818
- }
1819
-
1820
- async function exchangeAuthorizationCode({ pkce, code }) {
1821
- const cleanCode = String(code || '').trim();
1822
- if (!cleanCode) throw new Error('[openai-oauth] authorization code is required');
1823
- const tokenRes = await fetch(TOKEN_URL, {
1824
- method: 'POST',
1825
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
1826
- body: new URLSearchParams({
1827
- grant_type: 'authorization_code',
1828
- code: cleanCode,
1829
- redirect_uri: REDIRECT_URI,
1830
- client_id: CLIENT_ID,
1831
- code_verifier: pkce.verifier,
1832
- }),
1833
- redirect: 'error',
1834
- signal: AbortSignal.timeout(TOKEN_TIMEOUT_MS),
1835
- });
1836
- if (!tokenRes.ok) {
1837
- const text = await tokenRes.text().catch(() => '');
1838
- throw new Error(`[openai-oauth] token exchange ${tokenRes.status}: ${_scrubOAuthLoginBody(text).slice(0, 500)}`);
1839
- }
1840
- const json = await tokenRes.json();
1841
- if (!json.access_token || !json.refresh_token) {
1842
- throw new Error('[openai-oauth] token exchange response missing access_token or refresh_token');
1843
- }
1844
- const expiresAt = (typeof json.expires_in === 'number'
1845
- ? Date.now() + json.expires_in * 1000
1846
- : 0) || _expiryFromAccessToken(json.access_token);
1847
- const tokens = {
1848
- access_token: json.access_token,
1849
- refresh_token: json.refresh_token,
1850
- expires_at: expiresAt,
1851
- account_id: extractAccountId(json.access_token),
1852
- };
1853
- saveTokens(tokens);
1854
- return tokens;
1855
- }
1856
-
1857
- export async function beginOAuthLogin() {
1858
- const pkce = generatePKCE();
1859
- const state = randomBytes(16).toString('hex');
1860
- const url = new URL(AUTHORIZE_URL);
1861
- url.searchParams.set('response_type', 'code');
1862
- url.searchParams.set('client_id', CLIENT_ID);
1863
- url.searchParams.set('redirect_uri', REDIRECT_URI);
1864
- url.searchParams.set('scope', CODEX_OAUTH_SCOPE);
1865
- url.searchParams.set('code_challenge', pkce.challenge);
1866
- url.searchParams.set('code_challenge_method', 'S256');
1867
- url.searchParams.set('id_token_add_organizations', 'true');
1868
- url.searchParams.set('codex_cli_simplified_flow', 'true');
1869
- url.searchParams.set('state', state);
1870
- url.searchParams.set('originator', CODEX_OAUTH_ORIGINATOR);
1871
-
1872
- let server = null;
1873
- let timeout = null;
1874
- let finish = null;
1875
- const waitForCallback = new Promise((resolve, reject) => {
1876
- let settled = false;
1877
- finish = (value, error = null) => {
1878
- if (settled) return;
1879
- settled = true;
1880
- if (timeout) clearTimeout(timeout);
1881
- try { server?.close(); } catch { /* already closed */ }
1882
- if (error) reject(error);
1883
- else resolve(value);
1884
- };
1885
- server = createServer(async (req, res) => {
1886
- const u = new URL(req.url || '/', `http://${CALLBACK_HOST}:${CALLBACK_PORT}`);
1887
- if (u.pathname !== CALLBACK_PATH) {
1888
- res.writeHead(404);
1889
- res.end();
1890
- return;
1891
- }
1892
- const code = u.searchParams.get('code');
1893
- if (!code || u.searchParams.get('state') !== state) {
1894
- res.writeHead(400);
1895
- res.end('Invalid');
1896
- finish(null);
1897
- return;
1898
- }
1899
- res.writeHead(200, { 'Content-Type': 'text/html' });
1900
- res.end('<html><body><h2>OpenAI OAuth login successful! You can close this tab.</h2></body></html>');
1901
- try {
1902
- const tokens = await exchangeAuthorizationCode({ pkce, code });
1903
- finish(tokens);
1904
- } catch (err) {
1905
- finish(null, err instanceof Error ? err : new Error(String(err)));
1906
- }
1907
- });
1908
- timeout = setTimeout(() => finish(null), LOGIN_TIMEOUT_MS);
1909
- server.listen(CALLBACK_PORT, CALLBACK_HOST, async () => {
1910
- process.stderr.write(`\n[openai-oauth] Open this URL to log in to ChatGPT (OpenAI OAuth):\n${url.toString()}\n\n`);
1911
- try {
1912
- const { openInBrowser } = await import('../../../shared/open-url.mjs');
1913
- openInBrowser(url.toString());
1914
- } catch (err) {
1915
- process.stderr.write(`[openai-oauth] browser open failed: ${String(err?.message || err).slice(0, 200)}\n`);
1916
- }
1917
- });
1918
- server.on('error', (err) => finish(null, new Error(`[openai-oauth] callback server failed on ${CALLBACK_HOST}:${CALLBACK_PORT}: ${err?.message || err}`)));
1919
- });
1920
-
1921
- return {
1922
- provider: 'openai-oauth',
1923
- url: url.toString(),
1924
- waitForCallback,
1925
- completeCode: async (input) => {
1926
- const parsed = _parseOAuthCodeInput(input);
1927
- if (parsed.state && parsed.state !== state) throw new Error('[openai-oauth] OAuth state mismatch');
1928
- const tokens = await exchangeAuthorizationCode({ pkce, code: parsed.code });
1929
- finish?.(tokens);
1930
- return tokens;
1931
- },
1932
- cancel: () => {
1933
- finish?.(null);
1934
- },
1935
- };
1936
- }
1937
-
1938
- export async function loginOAuth() {
1939
- const login = await beginOAuthLogin();
1940
- return await login.waitForCallback;
1941
- }
1078
+ // --- OAuth PKCE login flow: extracted to openai-oauth-login.mjs ---
1079
+ const { beginOAuthLogin, loginOAuth } = createOpenAIOAuthLogin({
1080
+ clientId: CLIENT_ID,
1081
+ originator: CODEX_OAUTH_ORIGINATOR,
1082
+ extractAccountId,
1083
+ expiryFromAccessToken: _expiryFromAccessToken,
1084
+ saveTokens,
1085
+ });
1086
+ export { beginOAuthLogin, loginOAuth };