mixdog 1.2.5 → 1.2.6

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 (290) hide show
  1. package/package.json +1 -1
  2. package/scripts/lib/gate-scheduler.mjs +2 -2
  3. package/scripts/lib/run-node-tests.mjs +5 -14
  4. package/scripts/provider-pingpong-live.mjs +9 -9
  5. package/src/agents/advisor/AGENT.md +4 -4
  6. package/src/agents/advisor/agent.json +1 -1
  7. package/src/agents/reviewer/AGENT.md +2 -3
  8. package/src/agents/reviewer/agent.json +1 -1
  9. package/src/defaults/shipped-history.json +10 -0
  10. package/src/defaults/skills/browser-use/SKILL.md +2 -1
  11. package/src/defaults/skills/computer-use/SKILL.md +3 -1
  12. package/src/defaults/skills/visualize/SKILL.md +3 -2
  13. package/src/lib/rules-builder.cjs +1 -1
  14. package/src/output-styles/common.md +22 -6
  15. package/src/output-styles/detailed.md +7 -5
  16. package/src/output-styles/minimal.md +7 -3
  17. package/src/output-styles/simple.md +2 -3
  18. package/src/rules/agent/AGENT.md +3 -1
  19. package/src/rules/lead/LEAD.md +4 -3
  20. package/src/rules/shared/10-tool-workflow.md +2 -1
  21. package/src/rules/shared/60-verification.md +4 -5
  22. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -1
  23. package/src/runtime/agent/orchestrator/config.mjs +6 -0
  24. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +1 -1
  25. package/src/runtime/agent/orchestrator/internal-agents.mjs +10 -0
  26. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +53 -60
  27. package/src/runtime/agent/orchestrator/mcp/client.mjs +7 -4
  28. package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +2 -3
  29. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +9 -2
  30. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +40 -19
  31. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +3 -2
  32. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events/content-block-start.mjs +4 -1
  33. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events.mjs +2 -0
  34. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn.mjs +3 -0
  35. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +2 -1
  36. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +3 -1
  37. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +1 -2
  38. package/src/runtime/agent/orchestrator/providers/catalog-deadline.mjs +163 -0
  39. package/src/runtime/agent/orchestrator/providers/catalog-failures.mjs +34 -0
  40. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +38 -14
  41. package/src/runtime/agent/orchestrator/providers/lib/oauth-page.mjs +100 -26
  42. package/src/runtime/agent/orchestrator/providers/lib/send-callbacks.mjs +24 -0
  43. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +13 -0
  44. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +2 -1
  45. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -0
  46. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/chunk-events.mjs +4 -0
  47. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +3 -1
  48. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +2 -0
  49. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +49 -4
  50. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +0 -7
  51. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +7 -1
  52. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +1 -0
  53. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +2 -0
  54. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +1 -0
  55. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events.mjs +5 -1
  56. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +11 -2
  57. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +1 -0
  58. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +45 -16
  59. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +1 -0
  60. package/src/runtime/agent/orchestrator/providers/openai-ws-connect.mjs +196 -0
  61. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +74 -9
  62. package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +14 -1
  63. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -0
  64. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth-login.mjs +194 -0
  65. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth-tokens.mjs +353 -0
  66. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +293 -0
  67. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +33 -100
  68. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +23 -14
  69. package/src/runtime/agent/orchestrator/providers/registry.mjs +41 -27
  70. package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +2 -3
  71. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +3 -8
  72. package/src/runtime/agent/orchestrator/session/agent-pool-progress.mjs +15 -3
  73. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +74 -13
  74. package/src/runtime/agent/orchestrator/session/compact/forked-summary.mjs +102 -0
  75. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +8 -2
  76. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +16 -0
  77. package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +9 -0
  78. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +39 -2
  79. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +9 -19
  80. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +2 -3
  81. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -0
  82. package/src/runtime/agent/orchestrator/session/manager/ask-turn-steering-drain.mjs +13 -5
  83. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +10 -7
  84. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +62 -22
  85. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +23 -1
  86. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +81 -34
  87. package/src/runtime/agent/orchestrator/session/manager.mjs +2 -2
  88. package/src/runtime/agent/orchestrator/session/request-fork.mjs +47 -0
  89. package/src/runtime/agent/orchestrator/session/side-question-store.mjs +16 -1
  90. package/src/runtime/agent/orchestrator/session/side-question.mjs +5 -43
  91. package/src/runtime/agent/orchestrator/session/store/fs-probe.mjs +10 -0
  92. package/src/runtime/agent/orchestrator/session/store/lifecycle-barriers.mjs +73 -6
  93. package/src/runtime/agent/orchestrator/session/store/load-session.mjs +40 -0
  94. package/src/runtime/agent/orchestrator/session/store/save-fault.mjs +15 -0
  95. package/src/runtime/agent/orchestrator/session/store/session-paths.mjs +12 -0
  96. package/src/runtime/agent/orchestrator/session/store-agent-worker-header.mjs +36 -0
  97. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +258 -43
  98. package/src/runtime/agent/orchestrator/session/store-record-strict-scan.mjs +306 -0
  99. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +70 -21
  100. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +46 -30
  101. package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +144 -45
  102. package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +32 -13
  103. package/src/runtime/agent/orchestrator/session/store-transcript-worker.mjs +364 -38
  104. package/src/runtime/agent/orchestrator/session/store.mjs +9 -2
  105. package/src/runtime/agent/orchestrator/session/task-wait-control.mjs +43 -12
  106. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +23 -6
  107. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -6
  108. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +13 -7
  109. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/bashism-rewrite.mjs +1 -1
  110. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/benign-exit.mjs +7 -1
  111. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +40 -7
  112. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +37 -34
  113. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/inline-hoist.mjs +1 -1
  114. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +0 -4
  115. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +6 -0
  116. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +29 -27
  117. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +1 -1
  118. package/src/runtime/agent/orchestrator/tools/builtin/find-files-tool.mjs +28 -11
  119. package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +12 -3
  120. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +203 -101
  121. package/src/runtime/agent/orchestrator/tools/builtin/git-repo-rw-lock.mjs +90 -41
  122. package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +17 -1
  123. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-delivery.mjs +199 -0
  124. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-task-output.mjs +165 -39
  125. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +25 -13
  126. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +7 -6
  127. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +19 -4
  128. package/src/runtime/agent/orchestrator/tools/builtin/read-line-collector.mjs +5 -3
  129. package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +25 -7
  130. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +2 -1
  131. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-render.mjs +3 -1
  132. package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +12 -5
  133. package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +61 -22
  134. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +32 -8
  135. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +193 -19
  136. package/src/runtime/agent/orchestrator/tools/builtin/shell-direct-exe.mjs +57 -7
  137. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +119 -32
  138. package/src/runtime/agent/orchestrator/tools/builtin/shell-lossless-compact.mjs +26 -1
  139. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +13 -28
  140. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +90 -11
  141. package/src/runtime/agent/orchestrator/tools/builtin/wait-budgets.mjs +23 -0
  142. package/src/runtime/agent/orchestrator/tools/builtin.mjs +54 -10
  143. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +22 -28
  144. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +11 -7
  145. package/src/runtime/agent/orchestrator/tools/destructive-warning.mjs +126 -9
  146. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
  147. package/src/runtime/agent/orchestrator/tools/lib/owned-shell-trees.mjs +15 -6
  148. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +40 -2
  149. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +2 -2
  150. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promotion-results.mjs +13 -3
  151. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +2 -0
  152. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +3 -1
  153. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +1 -1
  154. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +35 -4
  155. package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +7 -2
  156. package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +39 -23
  157. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +6 -2
  158. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +13 -13
  159. package/src/runtime/attachments/office-extract.mjs +11 -9
  160. package/src/runtime/memory/lib/embedding-tokenizer.mjs +32 -18
  161. package/src/runtime/memory/lib/runtime-fetcher.mjs +1 -1
  162. package/src/runtime/memory/lib/tool-call-handler.mjs +7 -5
  163. package/src/runtime/office/authoring/pptx-script-normalize.mjs +11 -8
  164. package/src/runtime/office/core/tabular.mjs +1 -1
  165. package/src/runtime/office/design/library/design-template-inspect.mjs +7 -4
  166. package/src/runtime/office/portable/docx-formatting.mjs +1 -1
  167. package/src/runtime/office/portable/docx-runs.mjs +1 -1
  168. package/src/runtime/office/portable/docx-tracked-edits.mjs +2 -2
  169. package/src/runtime/office/portable/portable-docx-edits.mjs +11 -10
  170. package/src/runtime/office/portable/portable-docx-operations.mjs +21 -17
  171. package/src/runtime/office/portable/portable-docx-parts.mjs +15 -9
  172. package/src/runtime/office/portable/portable-docx-styles.mjs +4 -1
  173. package/src/runtime/office/portable/portable-docx-xml.mjs +9 -9
  174. package/src/runtime/office/portable/portable-opc.mjs +27 -13
  175. package/src/runtime/office/portable/portable-pivot.mjs +3 -3
  176. package/src/runtime/office/portable/portable-pptx-chart.mjs +3 -3
  177. package/src/runtime/office/portable/portable-pptx-charts.mjs +24 -18
  178. package/src/runtime/office/portable/portable-pptx-core.mjs +5 -5
  179. package/src/runtime/office/portable/portable-pptx-deck.mjs +8 -4
  180. package/src/runtime/office/portable/portable-pptx-package.mjs +7 -4
  181. package/src/runtime/office/portable/portable-pptx-shapes.mjs +18 -9
  182. package/src/runtime/office/portable/portable-sheet-parts.mjs +9 -3
  183. package/src/runtime/office/portable/portable-sheet-xml.mjs +5 -5
  184. package/src/runtime/office/portable/portable-xlsx-charts.mjs +4 -1
  185. package/src/runtime/office/portable/portable-xlsx-drawings.mjs +10 -6
  186. package/src/runtime/office/portable/portable-xlsx-operations.mjs +8 -5
  187. package/src/runtime/office/portable/portable-xlsx-pivot-ops.mjs +1 -1
  188. package/src/runtime/office/portable/portable-xlsx-reference-shift.mjs +11 -7
  189. package/src/runtime/office/portable/portable-xlsx-sheet-edits.mjs +6 -3
  190. package/src/runtime/office/portable/portable-xml.mjs +1 -1
  191. package/src/runtime/shared/atomic-file.mjs +22 -13
  192. package/src/runtime/shared/background-tasks.mjs +60 -20
  193. package/src/runtime/shared/child-spawn-gate.mjs +26 -16
  194. package/src/runtime/shared/config.mjs +0 -10
  195. package/src/runtime/shared/file-lock.mjs +7 -1
  196. package/src/runtime/shared/file-permissions-worker.mjs +38 -0
  197. package/src/runtime/shared/file-permissions.d.mts +19 -4
  198. package/src/runtime/shared/file-permissions.mjs +310 -4
  199. package/src/runtime/shared/git-runner-worker.mjs +6 -78
  200. package/src/runtime/shared/llm/http-agent.mjs +7 -3
  201. package/src/runtime/shared/llm/provider-catalog-registry.mjs +10 -0
  202. package/src/runtime/shared/llm/usage-accounting.mjs +2 -0
  203. package/src/runtime/shared/llm/usage-ledger-import.mjs +32 -3
  204. package/src/runtime/shared/llm/usage-ledger-rollup.mjs +12 -5
  205. package/src/runtime/shared/llm/usage-ledger-worker.mjs +1 -1
  206. package/src/runtime/shared/llm/usage-ledger.mjs +20 -5
  207. package/src/runtime/shared/memory-snapshot.mjs +5 -1
  208. package/src/runtime/shared/notify-trace.mjs +8 -1
  209. package/src/runtime/shared/pristine-execution-contract.json +5 -1
  210. package/src/runtime/shared/process-runner-worker.mjs +7 -0
  211. package/src/runtime/shared/process-runner.mjs +38 -0
  212. package/src/runtime/shared/provider-accounts.mjs +2 -1
  213. package/src/runtime/shared/provider-api-key.mjs +0 -1
  214. package/src/runtime/shared/read-row-numbers.mjs +1 -1
  215. package/src/runtime/shared/resource-admission.mjs +60 -18
  216. package/src/runtime/shared/run-child.mjs +84 -0
  217. package/src/runtime/shared/session-activity-signal.mjs +23 -0
  218. package/src/runtime/shared/shell-output-summary.mjs +23 -0
  219. package/src/runtime/shared/singleton-owner.mjs +126 -42
  220. package/src/runtime/shared/tool-output-limit.mjs +4 -2
  221. package/src/runtime/shared/tool-output-marker.mjs +58 -0
  222. package/src/runtime/shared/user-data-guard.mjs +28 -9
  223. package/src/runtime/shared/worktrees/activity-gate.mjs +33 -8
  224. package/src/runtime/shared/worktrees/git-head.mjs +20 -2
  225. package/src/runtime/shared/worktrees/lock.mjs +3 -2
  226. package/src/runtime/terminal-bridge/tool-defs.mjs +1 -1
  227. package/src/runtime/tidy/runners/biome.mjs +3 -1
  228. package/src/runtime/tidy/shadow.mjs +2 -1
  229. package/src/runtime/web-search/index.mjs +5 -7
  230. package/src/runtime/web-search/lib/formatter.mjs +42 -6
  231. package/src/runtime/web-search/tool-defs.mjs +6 -1
  232. package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +4 -3
  233. package/src/session-runtime/lifecycle/session-switch/resume.mjs +7 -4
  234. package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +3 -3
  235. package/src/session-runtime/lifecycle/shared.mjs +19 -2
  236. package/src/session-runtime/lifecycle/teardown/session-close.mjs +2 -0
  237. package/src/session-runtime/lifecycle/teardown.mjs +5 -2
  238. package/src/session-runtime/lifecycle-api.mjs +3 -1
  239. package/src/session-runtime/provider-auth/login.mjs +10 -6
  240. package/src/session-runtime/provider-auth/setup.mjs +19 -2
  241. package/src/session-runtime/provider-catalog-cache.mjs +101 -25
  242. package/src/session-runtime/provider-models/catalog-load.mjs +14 -3
  243. package/src/session-runtime/provider-models/row-hydration.mjs +4 -0
  244. package/src/session-runtime/provider-models.mjs +76 -10
  245. package/src/session-runtime/provider-readiness.mjs +4 -0
  246. package/src/session-runtime/services/agent-tool/close-flow.mjs +2 -1
  247. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +10 -2
  248. package/src/session-runtime/services/agent-tool/job-views/session-progress.mjs +13 -4
  249. package/src/session-runtime/services/agent-tool/notify.mjs +15 -4
  250. package/src/session-runtime/services/agent-tool/restart-notify.mjs +134 -0
  251. package/src/session-runtime/services/agent-tool/send-flow.mjs +24 -1
  252. package/src/session-runtime/services/agent-tool/spawn-flow/deferred-spawn.mjs +2 -1
  253. package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +4 -3
  254. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +5 -1
  255. package/src/session-runtime/services/agent-tool/tag-registry/tag-lookup.mjs +8 -3
  256. package/src/session-runtime/services/agent-tool/tag-registry.mjs +3 -0
  257. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +4 -0
  258. package/src/session-runtime/services/agent-tool/worker-index.mjs +44 -10
  259. package/src/session-runtime/services/agent-tool.mjs +20 -0
  260. package/src/session-runtime/services/provider-admin.mjs +92 -20
  261. package/src/session-runtime/services/usage-dashboard-model.mjs +2 -2
  262. package/src/session-runtime/services/usage-dashboard.mjs +27 -69
  263. package/src/session-runtime/turn/session-ops.mjs +2 -2
  264. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +1 -0
  265. package/src/session-runtime/usage-stats-api.mjs +6 -1
  266. package/src/standalone/channel-transport/http-routes.mjs +10 -9
  267. package/src/standalone/daemon-desktop-runtime.mjs +20 -7
  268. package/src/standalone/daemon-owner.mjs +117 -25
  269. package/src/standalone/daemon.mjs +43 -13
  270. package/src/standalone/session-call-cache.mjs +6 -0
  271. package/src/standalone/session-protocol.mjs +1 -1
  272. package/src/standalone/session-service/retention/busy.mjs +7 -2
  273. package/src/standalone/session-transport/routes.mjs +7 -6
  274. package/src/tui/app/model-options.mjs +39 -2
  275. package/src/tui/app/model-picker.mjs +16 -2
  276. package/src/tui/app/provider-setup-picker.mjs +1 -2
  277. package/src/tui/dist/index.mjs +385 -104
  278. package/src/tui/session/notice-text.mjs +0 -1
  279. package/src/tui/session/session-api/intake/submission.mjs +7 -13
  280. package/src/tui/session/session-api/integrations.mjs +1 -5
  281. package/src/tui/session/session-flow/drain.mjs +8 -0
  282. package/src/tui/session/session-flow/steering.mjs +46 -0
  283. package/src/tui/session/session-flow.mjs +2 -0
  284. package/src/tui/session/turn-stream.mjs +33 -0
  285. package/src/tui/session/turn.mjs +30 -3
  286. package/src/ui/statusline.mjs +1 -6
  287. package/src/ui/streaming-markdown-heal.mjs +24 -7
  288. package/src/vendor/statusline/bin/statusline-route.mjs +0 -1
  289. package/src/vendor/statusline/src/gateway/route-meta.mjs +0 -1
  290. package/src/workflows/default/WORKFLOW.md +9 -13
@@ -0,0 +1,163 @@
1
+ // Bounded, single-flight, cancellable provider catalog work, shared by the
2
+ // registry's catalog refreshes and the session runtime's shared catalog.
3
+ //
4
+ // - Deadline: a caller waits at most `timeoutMs` and then gets an ETIMEDOUT
5
+ // error entry instead of the provider's answer, so one hung provider cannot
6
+ // hold a catalog read or a startup/forced refresh.
7
+ // - Cancellation: on that deadline the operation's AbortSignal fires; a
8
+ // provider whose listModels honors `{ signal }` stops its work there.
9
+ // - Single flight: per provider at most one operation is pending in this
10
+ // process. A caller that finds one of its own kind joins it (with its own
11
+ // deadline); one that finds the other kind (a refresh vs a model list)
12
+ // waits for it inside its deadline before starting its own. An operation
13
+ // leaves the table only when the underlying work settles, not when a
14
+ // caller gives up, so repeated timed-out reads never pile up work; once an
15
+ // operation overran one deadline, later callers fail fast on it instead of
16
+ // each waiting out another deadline, until it settles.
17
+ // - Generations: one auth/account generation spans refreshes and lists alike
18
+ // (bumpProviderCatalogGeneration). Work of the other kind pending from an
19
+ // older generation, or from an older provider instance, never blocks or
20
+ // fails a caller of the newer one: that caller starts afresh. Same-kind
21
+ // work is never joined across generations, instances or a caller-supplied
22
+ // `epoch` (the catalog revision a list belongs to) either, unless it
23
+ // stalled (below).
24
+ // - Stalled work: an operation that overran its deadline (its signal fired)
25
+ // and still has not settled ignores cancellation. Until it settles, that
26
+ // provider name is not asked again for the same kind, not by a newer
27
+ // generation, epoch or provider instance: callers get an ETIMEDOUT entry
28
+ // marked `stalled` at once. Otherwise every account change, catalog
29
+ // revision or registry rebuild would leave one more never-ending call
30
+ // behind in the provider. Superseded calls can only overlap inside one
31
+ // deadline window, before any of them stalls.
32
+ import { clearProviderCatalogFailures, recordProviderCatalogOutcome } from './catalog-failures.mjs';
33
+
34
+ export const PROVIDER_CATALOG_TIMEOUT_MS = 4_000;
35
+
36
+ /** name -> the latest op started for that provider (either kind). */
37
+ const pendingOps = new Map();
38
+ /** `${kind}\0${name}` -> Set of the unsettled ops of that kind, of any
39
+ * generation, epoch or provider instance; emptied sets are removed. */
40
+ const runningOps = new Map();
41
+ /** `${kind}\0${name}` -> sequence number of the latest op started of that kind. */
42
+ const latestOpSeq = new Map();
43
+ let opSeq = 0;
44
+ let generation = 0;
45
+
46
+ const runKey = (name, kind) => `${kind}\0${name}`;
47
+
48
+ /** Opens a new auth/account generation for every provider's catalog work. */
49
+ export function bumpProviderCatalogGeneration() {
50
+ generation += 1;
51
+ // Failures of the previous account say nothing about the new one.
52
+ clearProviderCatalogFailures();
53
+ }
54
+
55
+ /** Provider names with catalog work still pending (diagnostics and tests). */
56
+ export function pendingProviderCatalogOps() {
57
+ return [...pendingOps.keys()];
58
+ }
59
+
60
+ /** `{ name, kind }` of every operation that ignored its deadline's abort and
61
+ * is still unsettled (diagnostics and tests). */
62
+ export function stalledProviderCatalogOps() {
63
+ return [...runningOps.values()]
64
+ .flatMap((ops) => [...ops])
65
+ .filter((op) => op.timedOut)
66
+ .map(({ name, kind }) => ({ name, kind }));
67
+ }
68
+
69
+ const isStalled = (name, kind) => [...(runningOps.get(runKey(name, kind)) || [])].some((op) => op.timedOut);
70
+
71
+ function startOp(name, provider, kind, run, epoch, opGeneration) {
72
+ const controller = new AbortController();
73
+ const settled = Promise.resolve()
74
+ .then(() => run(controller.signal))
75
+ .then(
76
+ (value) => ({ value }),
77
+ (error) => ({ error })
78
+ );
79
+ const seq = ++opSeq;
80
+ const op = { name, provider, kind, epoch, generation: opGeneration, seq, controller, settled, done: false };
81
+ const key = runKey(name, kind);
82
+ latestOpSeq.set(key, seq);
83
+ const running = runningOps.get(key) || new Set();
84
+ running.add(op);
85
+ runningOps.set(key, running);
86
+ pendingOps.set(name, op);
87
+ void settled.then(() => {
88
+ op.done = true;
89
+ if (pendingOps.get(name) === op) pendingOps.delete(name);
90
+ running.delete(op);
91
+ if (running.size === 0 && runningOps.get(key) === running) runningOps.delete(key);
92
+ });
93
+ return op;
94
+ }
95
+
96
+ function timeoutError(message) {
97
+ const error = new Error(message);
98
+ error.code = 'ETIMEDOUT';
99
+ return error;
100
+ }
101
+
102
+ /** Resolves `{ value }`, `{ error }`, or `{ error, timedOut: true }`; never rejects.
103
+ * The outcome is also kept as the provider's latest catalog failure (or
104
+ * recovery) for views that explain an empty list (catalog-failures.mjs),
105
+ * but only while it is current: a caller of a superseded generation, or one
106
+ * answered by an op that a newer op of its kind replaced, publishes nothing. */
107
+ export async function boundedProviderCatalogOp(op) {
108
+ const callerGeneration = generation;
109
+ const answered = { op: null };
110
+ const outcome = await runBoundedOp(op, callerGeneration, answered);
111
+ const currentOp = !answered.op || latestOpSeq.get(runKey(op.name, op.kind)) === answered.op.seq;
112
+ if (callerGeneration === generation && currentOp) recordProviderCatalogOutcome(op.name, outcome.error);
113
+ return outcome;
114
+ }
115
+
116
+ async function runBoundedOp(
117
+ { name, provider, kind, run, timeoutMs = PROVIDER_CATALOG_TIMEOUT_MS, epoch = 0 },
118
+ callerGeneration,
119
+ answered
120
+ ) {
121
+ let timer = null;
122
+ let watched = null;
123
+ const deadline = new Promise((resolve) => {
124
+ timer = setTimeout(() => {
125
+ const error = timeoutError(`provider "${name}" catalog ${kind} did not finish within ${timeoutMs}ms`);
126
+ if (watched) {
127
+ // Stalled until it settles: an op that honors the abort settles (and
128
+ // leaves the table) a few microtasks later, one that ignores it stays.
129
+ watched.timedOut = true;
130
+ watched.controller.abort(error);
131
+ }
132
+ resolve({ error, timedOut: true });
133
+ }, timeoutMs);
134
+ });
135
+ try {
136
+ for (;;) {
137
+ let op = pendingOps.get(name);
138
+ const superseded =
139
+ !op || op.provider !== provider || op.generation < callerGeneration || (op.kind === kind && op.epoch !== epoch);
140
+ if (superseded) {
141
+ if (isStalled(name, kind)) {
142
+ const error = timeoutError(`provider "${name}" catalog ${kind} ignored its deadline and is still pending`);
143
+ return { error, timedOut: true, stalled: true };
144
+ }
145
+ op = startOp(name, provider, kind, run, epoch, callerGeneration);
146
+ } else if (op.timedOut) {
147
+ // Already overran a deadline and still not settled: fail fast instead
148
+ // of making every later caller wait out another deadline for it.
149
+ const error = timeoutError(`provider "${name}" catalog ${op.kind} is still pending after its deadline`);
150
+ return { error, timedOut: true };
151
+ }
152
+ watched = op;
153
+ if (op.kind === kind) {
154
+ answered.op = op;
155
+ return await Promise.race([op.settled, deadline]);
156
+ }
157
+ const outcome = await Promise.race([op.settled.then(() => null), deadline]);
158
+ if (outcome) return outcome;
159
+ }
160
+ } finally {
161
+ clearTimeout(timer);
162
+ }
163
+ }
@@ -0,0 +1,34 @@
1
+ // The latest catalog failure of each provider, kept so a view that lists a
2
+ // provider's models can say WHY the list is empty (expired key, spent credits,
3
+ // timeout) instead of showing a silent 0/0. Cleared by the provider's next
4
+ // answer and by an account change.
5
+ import { safeErrorDetails } from '../../../shared/error-presentation.mjs';
6
+ import { providerErrorKind } from './provider-error-kind.mjs';
7
+
8
+ const MESSAGE_LIMIT = 300;
9
+ const failures = new Map();
10
+
11
+ /** Records a provider's catalog failure (`error`) or its recovery (no error). */
12
+ export function recordProviderCatalogOutcome(name, error) {
13
+ if (!error) {
14
+ failures.delete(name);
15
+ return;
16
+ }
17
+ const reason = typeof error.providerError?.message === 'string' ? error.providerError.message : '';
18
+ const text = safeErrorDetails(reason ? `${error.message}: ${reason}` : error.message);
19
+ failures.set(name, {
20
+ // Plain text, never a JSON envelope: describeError reads the kind below.
21
+ message: text.replace(/\s+/g, ' ').slice(0, MESSAGE_LIMIT),
22
+ errorKind: providerErrorKind(error),
23
+ at: Date.now(),
24
+ });
25
+ }
26
+
27
+ /** `{ message, errorKind, at }` of the provider's latest failure, or null. */
28
+ export function providerCatalogFailure(name) {
29
+ return failures.get(name) ?? null;
30
+ }
31
+
32
+ export function clearProviderCatalogFailures() {
33
+ failures.clear();
34
+ }
@@ -4,7 +4,7 @@ import { createServer } from 'node:http';
4
4
  import { randomBytes } from 'node:crypto';
5
5
  import { getLlmDispatcher } from '../../../shared/llm/http-agent.mjs';
6
6
  import { createOAuthPkce, parseOAuthCodeInput } from './lib/oauth-pkce.mjs';
7
- import { OAUTH_PAGE_CONTENT_TYPE, oauthSuccessHtml } from './lib/oauth-page.mjs';
7
+ import { OAUTH_PAGE_CONTENT_TYPE, oauthErrorHtml, oauthSuccessHtml } from './lib/oauth-page.mjs';
8
8
  import { emailFromJwts } from './lib/oauth-token-utils.mjs';
9
9
  import {
10
10
  CLIENT_ID,
@@ -26,7 +26,8 @@ export function generatePKCE() {
26
26
  return createOAuthPkce();
27
27
  }
28
28
 
29
- export async function exchangeAuthorizationCode({ discovery, pkce, code }) {
29
+ export async function exchangeAuthorizationCode({ discovery, pkce, code, signal = null }) {
30
+ signal?.throwIfAborted();
30
31
  const cleanCode = String(code || '').trim();
31
32
  if (!cleanCode) throw new Error('[grok-oauth] authorization code is required');
32
33
  const tokenRes = await fetch(discovery.token_endpoint, {
@@ -47,7 +48,9 @@ export async function exchangeAuthorizationCode({ discovery, pkce, code }) {
47
48
  // Secret-bearing (authorization code + verifier): refuse
48
49
  // redirects so they can't be replayed to an untrusted host.
49
50
  redirect: 'error',
50
- signal: AbortSignal.timeout(TOKEN_TIMEOUT_MS),
51
+ signal: signal
52
+ ? AbortSignal.any([signal, AbortSignal.timeout(TOKEN_TIMEOUT_MS)])
53
+ : AbortSignal.timeout(TOKEN_TIMEOUT_MS),
51
54
  dispatcher: getLlmDispatcher(),
52
55
  });
53
56
  if (!tokenRes.ok) {
@@ -72,6 +75,8 @@ export async function exchangeAuthorizationCode({ discovery, pkce, code }) {
72
75
  ? { email: emailFromJwts(json.id_token, json.access_token) }
73
76
  : {}),
74
77
  };
78
+ // A cancelled login never stores the account it was exchanging.
79
+ signal?.throwIfAborted();
75
80
  saveTokens(tokens);
76
81
  return tokens;
77
82
  }
@@ -96,11 +101,30 @@ export async function beginOAuthLogin({ openBrowser = true } = {}) {
96
101
  let server = null;
97
102
  let timeout = null;
98
103
  let finish = null;
104
+ let exchangePromise = null;
105
+ const controller = new AbortController();
106
+ // Browser and manual callbacks share one exchange: an authorization code is
107
+ // single-use, and a duplicate must not settle the login before the first.
108
+ const acceptCode = (code) => {
109
+ if (exchangePromise) return exchangePromise;
110
+ controller.signal.throwIfAborted();
111
+ // The browser deadline ends when a code arrives; the exchange has its own.
112
+ if (timeout) clearTimeout(timeout);
113
+ timeout = null;
114
+ exchangePromise = exchangeAuthorizationCode({ discovery, pkce, code, signal: controller.signal });
115
+ exchangePromise.then(
116
+ (tokens) => finish(tokens),
117
+ (err) => finish(null, err instanceof Error ? err : new Error(String(err)))
118
+ );
119
+ return exchangePromise;
120
+ };
99
121
  const waitForCallback = new Promise((resolve, reject) => {
100
122
  let settled = false;
101
123
  finish = (value, error = null) => {
102
124
  if (settled) return;
103
125
  settled = true;
126
+ // Work arriving after the login settled (or was cancelled) is rejected.
127
+ controller.abort();
104
128
  if (timeout) clearTimeout(timeout);
105
129
  try {
106
130
  server?.close();
@@ -120,17 +144,18 @@ export async function beginOAuthLogin({ openBrowser = true } = {}) {
120
144
  const code = u.searchParams.get('code');
121
145
  if (!code || u.searchParams.get('state') !== state) {
122
146
  // Reject this request only; the valid callback may still arrive.
123
- res.writeHead(400);
124
- res.end('Invalid');
147
+ res.writeHead(400, { 'Content-Type': OAUTH_PAGE_CONTENT_TYPE });
148
+ res.end(oauthErrorHtml('The sign-in callback was invalid or did not match this login.'));
125
149
  return;
126
150
  }
127
- res.writeHead(200, { 'Content-Type': OAUTH_PAGE_CONTENT_TYPE });
128
- res.end(oauthSuccessHtml('Grok'));
129
151
  try {
130
- const tokens = await exchangeAuthorizationCode({ discovery, pkce, code });
131
- finish(tokens);
132
- } catch (err) {
133
- finish(null, err instanceof Error ? err : new Error(String(err)));
152
+ await acceptCode(code);
153
+ res.writeHead(200, { 'Content-Type': OAUTH_PAGE_CONTENT_TYPE });
154
+ res.end(oauthSuccessHtml('Grok'));
155
+ } catch {
156
+ // The shared exchange reports its error through waitForCallback.
157
+ res.writeHead(500, { 'Content-Type': OAUTH_PAGE_CONTENT_TYPE });
158
+ res.end(oauthErrorHtml('Grok could not complete the token exchange.'));
134
159
  }
135
160
  });
136
161
  timeout = setTimeout(() => finish(null), LOGIN_TIMEOUT_MS);
@@ -161,9 +186,8 @@ export async function beginOAuthLogin({ openBrowser = true } = {}) {
161
186
  completeCode: async (input) => {
162
187
  const parsed = parseOAuthCodeInput(input, { allowHashState: true });
163
188
  if (parsed.state && parsed.state !== state) throw new Error('[grok-oauth] OAuth state mismatch');
164
- const tokens = await exchangeAuthorizationCode({ discovery, pkce, code: parsed.code });
165
- finish?.(tokens);
166
- return tokens;
189
+ if (!parsed.code) throw new Error('[grok-oauth] authorization code is required');
190
+ return await acceptCode(parsed.code);
167
191
  },
168
192
  cancel: () => {
169
193
  finish?.(null);
@@ -31,17 +31,61 @@ function escapeHtml(value) {
31
31
  .replaceAll("'", '&#39;');
32
32
  }
33
33
 
34
- function renderPage({ heading, message, details }) {
35
- const title = escapeHtml(heading);
34
+ // The callback servers do not know the app language, so the default is
35
+ // bilingual (Korean first). A known language picks one copy.
36
+ const COPY = {
37
+ ko: {
38
+ successTitle: '로그인 완료',
39
+ successBody: (name) => `${name} 로그인이 완료되었습니다. 이 탭을 닫고 Mixdog로 돌아가 주세요.`,
40
+ failureTitle: '로그인 실패',
41
+ failureBody: 'Mixdog로 돌아가 다시 시도해 주세요.',
42
+ reason: '사유',
43
+ close: '닫기',
44
+ },
45
+ en: {
46
+ successTitle: 'Sign-in complete',
47
+ successBody: (name) => `You are signed in to ${name}. You can close this tab and return to Mixdog.`,
48
+ failureTitle: 'Sign-in failed',
49
+ failureBody: 'Return to Mixdog and try again.',
50
+ reason: 'Reason',
51
+ close: 'Close',
52
+ },
53
+ };
54
+
55
+ function renderPage({ kind, providerName, reason, lang }) {
56
+ const langs = lang === 'ko' || lang === 'en' ? [lang] : ['ko', 'en'];
57
+ const blocks = langs
58
+ .map((code, index) => {
59
+ const copy = COPY[code];
60
+ const title = kind === 'success' ? copy.successTitle : copy.failureTitle;
61
+ const body = kind === 'success' ? copy.successBody(providerName) : copy.failureBody;
62
+ const tag = index === 0 ? 'h1' : 'h2';
63
+ return `<section lang="${code}" class="${index === 0 ? 'primary' : 'secondary'}">
64
+ <${tag}>${escapeHtml(title)}</${tag}>
65
+ <p>${escapeHtml(body)}</p>
66
+ </section>`;
67
+ })
68
+ .join('\n ');
69
+ const reasonBlock = reason
70
+ ? `<div class="reason"><span>${langs.map((code) => COPY[code].reason).join(' / ')}</span>${escapeHtml(reason)}</div>`
71
+ : '';
72
+ const close = langs.map((code) => COPY[code].close).join(' · ');
73
+ const documentTitle = escapeHtml(
74
+ `${langs.map((code) => COPY[code][kind === 'success' ? 'successTitle' : 'failureTitle']).join(' / ')} · Mixdog`
75
+ );
36
76
  return `<!doctype html>
37
- <html lang="en">
77
+ <html lang="${langs[0]}">
38
78
  <head>
39
79
  <meta charset="utf-8" />
40
80
  <meta name="viewport" content="width=device-width, initial-scale=1" />
41
- <title>${title} · Mixdog</title>
81
+ <meta name="color-scheme" content="dark light" />
82
+ <title>${documentTitle}</title>
42
83
  <style>
84
+ :root { --bg: #f6f7f9; --fg: #0f172a; --muted: #475569; --card: #ffffff; --line: #d9dee6; --ok: #15803d; --bad: #b91c1c; --link: #1d4ed8; }
85
+ @media (prefers-color-scheme: dark) {
86
+ :root { --bg: #07080b; --fg: #f8fafc; --muted: #94a3b8; --card: #0f1116; --line: #232733; --ok: #4ade80; --bad: #f87171; --link: #93c5fd; }
87
+ }
43
88
  * { box-sizing: border-box; }
44
- html { color-scheme: dark; }
45
89
  body {
46
90
  margin: 0;
47
91
  min-height: 100vh;
@@ -49,32 +93,65 @@ function renderPage({ heading, message, details }) {
49
93
  align-items: center;
50
94
  justify-content: center;
51
95
  padding: 24px;
52
- background: #07080b;
53
- color: #f8fafc;
54
- font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
96
+ background: var(--bg);
97
+ color: var(--fg);
98
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "Malgun Gothic", "Apple SD Gothic Neo", "Noto Sans KR", Roboto, Arial, sans-serif;
55
99
  text-align: center;
56
100
  }
57
- main { width: 100%; max-width: 560px; display: flex; flex-direction: column; align-items: center; }
58
- .logo { width: 72px; height: 72px; margin-bottom: 24px; }
101
+ main {
102
+ width: 100%;
103
+ max-width: 520px;
104
+ padding: 40px 32px 32px;
105
+ background: var(--card);
106
+ border: 1px solid var(--line);
107
+ border-radius: 16px;
108
+ display: flex;
109
+ flex-direction: column;
110
+ align-items: center;
111
+ }
112
+ .logo { width: 72px; height: 72px; margin-bottom: 20px; }
59
113
  .logo svg { width: 100%; height: 100%; display: block; }
60
- h1 { margin: 0 0 10px; font-size: 28px; line-height: 1.15; font-weight: 650; }
61
- p { margin: 0; line-height: 1.7; color: #94a3b8; font-size: 15px; }
62
- .details {
63
- margin-top: 16px;
64
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
114
+ h1, h2 { margin: 0 0 6px; line-height: 1.2; font-weight: 650; }
115
+ h1 { font-size: 28px; color: ${kind === 'success' ? 'var(--ok)' : 'var(--bad)'}; }
116
+ h2 { font-size: 16px; color: var(--muted); font-weight: 600; }
117
+ p { margin: 0; line-height: 1.7; color: var(--muted); font-size: 15px; }
118
+ section.secondary { margin-top: 18px; }
119
+ .reason {
120
+ margin-top: 20px;
121
+ width: 100%;
122
+ padding: 12px 14px;
123
+ border: 1px solid var(--line);
124
+ border-radius: 10px;
125
+ text-align: left;
126
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
65
127
  font-size: 13px;
66
- color: #94a3b8;
128
+ color: var(--fg);
67
129
  white-space: pre-wrap;
68
130
  word-break: break-word;
69
131
  }
132
+ .reason span { display: block; margin-bottom: 4px; color: var(--muted); font-family: inherit; font-size: 12px; }
133
+ button {
134
+ margin-top: 24px;
135
+ min-width: 120px;
136
+ height: 40px;
137
+ padding: 0 20px;
138
+ border: 1px solid var(--line);
139
+ border-radius: 10px;
140
+ background: transparent;
141
+ color: var(--fg);
142
+ font: inherit;
143
+ font-size: 14px;
144
+ cursor: pointer;
145
+ }
146
+ button:hover { border-color: var(--muted); }
70
147
  </style>
71
148
  </head>
72
149
  <body>
73
150
  <main>
74
151
  <div class="logo">${LOGO_SVG}</div>
75
- <h1>${title}</h1>
76
- <p>${escapeHtml(message)}</p>
77
- ${details ? `<div class="details">${escapeHtml(details)}</div>` : ''}
152
+ ${blocks}
153
+ ${reasonBlock}
154
+ <button type="button" onclick="window.close()">${escapeHtml(close)}</button>
78
155
  </main>
79
156
  </body>
80
157
  </html>`;
@@ -82,13 +159,10 @@ function renderPage({ heading, message, details }) {
82
159
 
83
160
  export const OAUTH_PAGE_CONTENT_TYPE = 'text/html; charset=utf-8';
84
161
 
85
- export function oauthSuccessHtml(providerName) {
86
- return renderPage({
87
- heading: 'Authentication successful',
88
- message: `Signed in to ${providerName}. You can close this tab and return to Mixdog.`,
89
- });
162
+ export function oauthSuccessHtml(providerName, { lang } = {}) {
163
+ return renderPage({ kind: 'success', providerName, lang });
90
164
  }
91
165
 
92
- export function oauthErrorHtml(message, details) {
93
- return renderPage({ heading: 'Authentication failed', message, details });
166
+ export function oauthErrorHtml(reason, { lang } = {}) {
167
+ return renderPage({ kind: 'failure', reason, lang });
94
168
  }
@@ -8,5 +8,29 @@ export function streamCallbacks(opts) {
8
8
  onToolCall: functionOrNull(opts.onToolCall),
9
9
  onTextDelta: functionOrNull(opts.onTextDelta),
10
10
  onTextReset: functionOrNull(opts.onTextReset),
11
+ onStreamItemStart: functionOrNull(opts.onStreamItemStart),
11
12
  };
12
13
  }
14
+
15
+ /** Tell the caller a provider output item started streaming: `{ kind: 'tool' }`
16
+ * for a client tool call whose arguments are still on the way, `{ kind:
17
+ * 'text', phase }` for an assistant message that names its phase
18
+ * (`commentary` / `final_answer`). A listener failure never reaches the
19
+ * stream. */
20
+ export function reportStreamItemStart(onStreamItemStart, detail) {
21
+ if (typeof onStreamItemStart !== 'function' || !detail) return;
22
+ try {
23
+ onStreamItemStart(detail);
24
+ } catch {}
25
+ }
26
+
27
+ const ASSISTANT_PHASES = new Set(['commentary', 'final_answer']);
28
+
29
+ /** What a Responses output item tells `reportStreamItemStart`: a client tool
30
+ * call, an assistant message carrying its phase, or nothing (null). */
31
+ export function responsesItemStart(item) {
32
+ const type = item?.type;
33
+ if (type === 'function_call' || type === 'custom_tool_call' || type === 'tool_search_call') return { kind: 'tool' };
34
+ if (type === 'message' && ASSISTANT_PHASES.has(item.phase)) return { kind: 'text', phase: item.phase };
35
+ return null;
36
+ }
@@ -5,6 +5,7 @@ import { resolvePluginData } from '../../../shared/plugin-paths.mjs';
5
5
  import { boundProviderAuthPath } from '../../../shared/provider-auth-binding.mjs';
6
6
  import { cursorTokenExpiry } from './cursor-auth.mjs';
7
7
  import { isValidMuseCredentialDocument } from './muse-code-oauth-tokens.mjs';
8
+ import { isValidOpenCodeGoCredentialDocument } from './opencode-go-oauth-tokens.mjs';
8
9
  import { developerOptionEnabled, developerOptionForProvider } from '../../../shared/developer-options.mjs';
9
10
  import { modelProxyClientMode } from '../../../shared/model-proxy-mode.mjs';
10
11
 
@@ -198,6 +199,17 @@ function museCodeOAuthState() {
198
199
  );
199
200
  }
200
201
 
202
+ function openCodeGoOAuthState() {
203
+ const paths = [
204
+ boundProviderAuthPath('opencode-go-oauth') ||
205
+ process.env.OPENCODE_GO_OAUTH_CREDENTIALS_PATH ||
206
+ join(resolvePluginData(), 'opencode-go-oauth.json'),
207
+ ];
208
+ return memoProbe('opencode-go-oauth', paths, () =>
209
+ resolveProbeState(paths, (docs) => docs.some(isValidOpenCodeGoCredentialDocument))
210
+ );
211
+ }
212
+
201
213
  // Antigravity needs a resolved Cloud project alongside the tokens: without it
202
214
  // every request 400s, so a half-finished login must not enable the provider.
203
215
  function antigravityOAuthState() {
@@ -270,6 +282,7 @@ const OAUTH_PROBE_STATES = new Map([
270
282
  ['mimo-oauth', mimoOAuthState],
271
283
  ['cursor-oauth', cursorOAuthState],
272
284
  ['antigravity-oauth', antigravityOAuthState],
285
+ ['opencode-go-oauth', openCodeGoOAuthState],
273
286
  ]);
274
287
 
275
288
  // Dev-only OAuth providers (Antigravity, Cursor). Each is hidden and treated as
@@ -869,7 +869,8 @@ async function measureUsage(provider, providerObj, routeInfo) {
869
869
  provider === 'cursor-api' ||
870
870
  provider === 'antigravity-oauth' ||
871
871
  provider === 'muse-code-oauth' ||
872
- provider === 'command-code-oauth') &&
872
+ provider === 'command-code-oauth' ||
873
+ provider === 'opencode-go-oauth') &&
873
874
  typeof providerObj?.getUsageSnapshot === 'function'
874
875
  ) {
875
876
  return await providerObj.getUsageSnapshot();
@@ -187,6 +187,7 @@ export async function sendCompatChat(provider, messages, useModel, tools, opts)
187
187
  onStreamDelta: opts.onStreamDelta,
188
188
  onToolCall: opts.onToolCall,
189
189
  onTextDelta: opts.onTextDelta,
190
+ onStreamItemStart: opts.onStreamItemStart,
190
191
  parseToolCalls,
191
192
  // Known tool names for the leaked-tool-call guard:
192
193
  // recovered leaked calls only synthesize when they name
@@ -4,6 +4,7 @@
4
4
  * reasoning aliases, tool_calls deltas, finish reason and usage.
5
5
  */
6
6
  import { mergeToolCallDelta } from './tool-call-acc.mjs';
7
+ import { reportStreamItemStart } from '../lib/send-callbacks.mjs';
7
8
  import { appendText, drainLeakedCalls, relayText, reportProgress, reportTransport } from './stream-state.mjs';
8
9
  import { chatContentParts } from '../openai-compat-reasoning-field.mjs';
9
10
 
@@ -58,7 +59,10 @@ export function applyCompatChunk(state, chunk) {
58
59
  if (Array.isArray(delta?.tool_calls) && delta.tool_calls.length) {
59
60
  reportProgress(state, 'tool');
60
61
  }
62
+ const toolsBefore = state.toolAcc.byKey.size;
61
63
  mergeToolCallDelta(state.toolAcc, delta?.tool_calls);
64
+ // A tool call's first delta: its arguments are still streaming.
65
+ if (state.toolAcc.byKey.size > toolsBefore) reportStreamItemStart(state.onStreamItemStart, { kind: 'tool' });
62
66
  if (choice?.finish_reason) {
63
67
  state.stopReason = choice.finish_reason;
64
68
  drainLeakedCalls(state);
@@ -22,6 +22,7 @@ export function createCompatStreamState({
22
22
  onStreamDelta,
23
23
  onToolCall,
24
24
  onTextDelta,
25
+ onStreamItemStart,
25
26
  }) {
26
27
  const toolDedupe = createToolCallDedupe();
27
28
  return {
@@ -30,6 +31,7 @@ export function createCompatStreamState({
30
31
  onStreamDelta,
31
32
  onToolCall,
32
33
  onTextDelta,
34
+ onStreamItemStart,
33
35
  semanticIdleDeadlineAt: 0,
34
36
  sawFirstEvent: false,
35
37
  content: '',
@@ -86,7 +88,7 @@ export function isIncompleteFinishReason(finishReason) {
86
88
 
87
89
  /** Whether `finish_reason` confirms a normal end of the turn: present, not an
88
90
  * incomplete outcome and not a gateway-reported failure (network_error/error). */
89
- export function isNormalFinishReason(finishReason) {
91
+ function isNormalFinishReason(finishReason) {
90
92
  return (
91
93
  Boolean(finishReason) && !isIncompleteFinishReason(finishReason) && !failingFinishReasonError(finishReason, '')
92
94
  );
@@ -39,6 +39,7 @@ export async function consumeCompatChatCompletionStream(
39
39
  onStreamDelta,
40
40
  onToolCall,
41
41
  onTextDelta,
42
+ onStreamItemStart,
42
43
  parseToolCalls,
43
44
  knownToolNames,
44
45
  semanticIdleTimeoutMs,
@@ -55,6 +56,7 @@ export async function consumeCompatChatCompletionStream(
55
56
  onStreamDelta,
56
57
  onToolCall,
57
58
  onTextDelta,
59
+ onStreamItemStart,
58
60
  });
59
61
  reportTransport(state);
60
62
  const iterator = stream[Symbol.asyncIterator]();