mixdog 0.9.3 → 0.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (382) hide show
  1. package/README.md +112 -38
  2. package/package.json +10 -3
  3. package/scripts/bench/lead-review-tasks-r3.json +20 -0
  4. package/scripts/bench/lead-review-tasks.json +20 -0
  5. package/scripts/bench/r4-mixed-tasks.json +20 -0
  6. package/scripts/bench/r5-orchestrated-task.json +7 -0
  7. package/scripts/bench/review-tasks.json +20 -0
  8. package/scripts/bench/round-codex.json +114 -0
  9. package/scripts/bench/round-mixdog-lead-r3.json +269 -0
  10. package/scripts/bench/round-mixdog-lead.json +269 -0
  11. package/scripts/bench/round-mixdog.json +126 -0
  12. package/scripts/bench/round-r10-bigsample.json +679 -0
  13. package/scripts/bench/round-r11-codexalign.json +257 -0
  14. package/scripts/bench/round-r13-clientmeta.json +464 -0
  15. package/scripts/bench/round-r14-betafeatures.json +466 -0
  16. package/scripts/bench/round-r15-fulldefault.json +462 -0
  17. package/scripts/bench/round-r16-sessionid.json +466 -0
  18. package/scripts/bench/round-r17-wirebytes.json +456 -0
  19. package/scripts/bench/round-r18-prewarm.json +468 -0
  20. package/scripts/bench/round-r19-clean.json +472 -0
  21. package/scripts/bench/round-r20-prewarm-clean.json +475 -0
  22. package/scripts/bench/round-r21-delta-retry.json +473 -0
  23. package/scripts/bench/round-r22-full-probe.json +693 -0
  24. package/scripts/bench/round-r23-itemprobe.json +701 -0
  25. package/scripts/bench/round-r24-shapefix.json +677 -0
  26. package/scripts/bench/round-r25-serial.json +464 -0
  27. package/scripts/bench/round-r26-parallel3.json +671 -0
  28. package/scripts/bench/round-r27-parallel10.json +894 -0
  29. package/scripts/bench/round-r28-parallel10-stagger.json +882 -0
  30. package/scripts/bench/round-r29-parallel10-stagger166.json +886 -0
  31. package/scripts/bench/round-r30-instid.json +253 -0
  32. package/scripts/bench/round-r31-upgradeprobe.json +256 -0
  33. package/scripts/bench/round-r32-vs-codex-lead.json +254 -0
  34. package/scripts/bench/round-r33-vs-codex-codex.json +115 -0
  35. package/scripts/bench/round-r34-orchestrated.json +120 -0
  36. package/scripts/bench/round-r35-orchestrated-codex.json +61 -0
  37. package/scripts/bench/round-r36-orchestrated-capped.json +128 -0
  38. package/scripts/bench/round-r4-codex.json +114 -0
  39. package/scripts/bench/round-r4-mixed.json +225 -0
  40. package/scripts/bench/round-r5-gpt-lead.json +259 -0
  41. package/scripts/bench/round-r6-codex.json +114 -0
  42. package/scripts/bench/round-r6-solo.json +257 -0
  43. package/scripts/bench/round-r7-full.json +254 -0
  44. package/scripts/bench/round-r8-fulldefault.json +255 -0
  45. package/scripts/bench-run.mjs +251 -32
  46. package/scripts/freevar-smoke.mjs +95 -0
  47. package/scripts/internal-comms-bench.mjs +3 -4
  48. package/scripts/internal-comms-smoke.mjs +10 -9
  49. package/scripts/model-catalog-audit.mjs +209 -0
  50. package/scripts/model-list-sanitize-test.mjs +37 -0
  51. package/scripts/mouse-probe.mjs +45 -0
  52. package/scripts/output-style-bench.mjs +13 -6
  53. package/scripts/output-style-smoke.mjs +4 -4
  54. package/scripts/provider-toolcall-test.mjs +7 -3
  55. package/scripts/recall-bench.mjs +76 -13
  56. package/scripts/recall-quality-cases.json +12 -0
  57. package/scripts/recall-usecase-cases.json +18 -0
  58. package/scripts/session-bench.mjs +152 -6
  59. package/scripts/tool-smoke.mjs +25 -65
  60. package/scripts/tui-render-smoke.mjs +90 -0
  61. package/scripts/webhook-smoke.mjs +208 -0
  62. package/src/agents/debugger/AGENT.md +4 -1
  63. package/src/agents/heavy-worker/AGENT.md +9 -8
  64. package/src/agents/maintainer/AGENT.md +4 -0
  65. package/src/agents/reviewer/AGENT.md +2 -1
  66. package/src/agents/scheduler-task/AGENT.md +2 -3
  67. package/src/agents/webhook-handler/AGENT.md +2 -3
  68. package/src/agents/worker/AGENT.md +10 -7
  69. package/src/app.mjs +12 -1
  70. package/src/headless-role.mjs +7 -1
  71. package/src/lib/rules-builder.cjs +4 -0
  72. package/src/mixdog-session-runtime.mjs +647 -2056
  73. package/src/output-styles/default.md +30 -9
  74. package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
  75. package/src/output-styles/minimal.md +8 -6
  76. package/src/output-styles/simple.md +21 -7
  77. package/src/rules/agent/00-common.md +6 -3
  78. package/src/rules/agent/30-explorer.md +16 -5
  79. package/src/rules/lead/01-general.md +5 -5
  80. package/src/rules/lead/lead-brief.md +15 -0
  81. package/src/rules/lead/lead-tool.md +6 -15
  82. package/src/rules/shared/01-tool.md +17 -21
  83. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +8 -3
  84. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
  85. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
  86. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
  87. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
  88. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
  89. package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
  90. package/src/runtime/agent/orchestrator/config.mjs +69 -2
  91. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +62 -20
  92. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
  93. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
  94. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +81 -1281
  95. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
  96. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -3
  97. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
  98. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
  99. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
  100. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
  101. package/src/runtime/agent/orchestrator/providers/gemini.mjs +43 -1013
  102. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -3
  103. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +105 -11
  104. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +356 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
  106. package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
  107. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
  108. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
  109. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +40 -1143
  110. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +740 -0
  111. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
  112. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +349 -2131
  113. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +143 -1002
  114. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +229 -0
  115. package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
  116. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +465 -0
  117. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
  118. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
  119. package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +80 -0
  120. package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
  121. package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
  122. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
  123. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
  124. package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
  125. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
  126. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
  127. package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
  128. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +14 -2
  129. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
  130. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -3
  131. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +275 -0
  132. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
  133. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
  134. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
  135. package/src/runtime/agent/orchestrator/session/loop.mjs +278 -402
  136. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
  137. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +7 -4
  138. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +12 -0
  139. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
  140. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
  141. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
  142. package/src/runtime/agent/orchestrator/session/manager.mjs +166 -1087
  143. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
  144. package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
  145. package/src/runtime/agent/orchestrator/stall-policy.mjs +20 -1
  146. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
  147. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
  148. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +35 -44
  149. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
  150. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
  151. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
  152. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
  153. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -92
  154. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
  155. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
  156. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +28 -265
  157. package/src/runtime/agent/orchestrator/tools/builtin.mjs +0 -6
  158. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +57 -3
  159. package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
  160. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +10 -122
  161. package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
  162. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
  163. package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
  164. package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
  165. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
  166. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
  167. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
  168. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
  169. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
  170. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
  171. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
  172. package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
  173. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +0 -21
  174. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -72
  175. package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
  176. package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
  177. package/src/runtime/channels/backends/discord-access.mjs +32 -0
  178. package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
  179. package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
  180. package/src/runtime/channels/backends/discord.mjs +27 -318
  181. package/src/runtime/channels/backends/telegram.mjs +8 -12
  182. package/src/runtime/channels/index.mjs +247 -701
  183. package/src/runtime/channels/lib/backend-dispatch.mjs +46 -0
  184. package/src/runtime/channels/lib/config.mjs +37 -149
  185. package/src/runtime/channels/lib/event-pipeline.mjs +22 -5
  186. package/src/runtime/channels/lib/event-queue.mjs +78 -13
  187. package/src/runtime/channels/lib/inbound-routing.mjs +74 -0
  188. package/src/runtime/channels/lib/interaction-workflows.mjs +5 -113
  189. package/src/runtime/channels/lib/output-forwarder.mjs +1 -1
  190. package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
  191. package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
  192. package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
  193. package/src/runtime/channels/lib/scheduler.mjs +27 -113
  194. package/src/runtime/channels/lib/session-discovery.mjs +56 -4
  195. package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
  196. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  197. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
  198. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
  199. package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
  200. package/src/runtime/channels/lib/webhook/deliveries.mjs +313 -0
  201. package/src/runtime/channels/lib/webhook/log.mjs +42 -0
  202. package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
  203. package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
  204. package/src/runtime/channels/lib/webhook.mjs +43 -616
  205. package/src/runtime/channels/tool-defs.mjs +11 -130
  206. package/src/runtime/memory/index.mjs +210 -1948
  207. package/src/runtime/memory/lib/core-memory-store.mjs +5 -1
  208. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
  209. package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
  210. package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
  211. package/src/runtime/memory/lib/ko-morph.mjs +195 -0
  212. package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
  213. package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
  214. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
  215. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
  216. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
  217. package/src/runtime/memory/lib/memory-cycle2.mjs +24 -842
  218. package/src/runtime/memory/lib/memory-embed.mjs +149 -0
  219. package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
  220. package/src/runtime/memory/lib/memory-recall-store.mjs +69 -12
  221. package/src/runtime/memory/lib/memory-text-utils.mjs +46 -0
  222. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
  223. package/src/runtime/memory/lib/query-handlers.mjs +802 -0
  224. package/src/runtime/memory/lib/recall-format.mjs +55 -0
  225. package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
  226. package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
  227. package/src/runtime/memory/tool-defs.mjs +5 -13
  228. package/src/runtime/search/lib/http-fetch.mjs +274 -0
  229. package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
  230. package/src/runtime/search/lib/web-tools.mjs +24 -602
  231. package/src/runtime/shared/atomic-file.mjs +26 -1
  232. package/src/runtime/shared/config.mjs +14 -4
  233. package/src/runtime/shared/launcher-control.mjs +2 -2
  234. package/src/runtime/shared/markdown-frontmatter.mjs +19 -0
  235. package/src/runtime/shared/schedules-store.mjs +13 -3
  236. package/src/runtime/shared/tool-execution-contract.mjs +2 -2
  237. package/src/runtime/shared/tool-primitives.mjs +308 -0
  238. package/src/runtime/shared/tool-result-summary.mjs +515 -0
  239. package/src/runtime/shared/tool-surface.mjs +80 -898
  240. package/src/runtime/shared/transcript-writer.mjs +23 -0
  241. package/src/runtime/shared/update-checker.mjs +7 -4
  242. package/src/session-runtime/config-helpers.mjs +119 -2
  243. package/src/session-runtime/config-lifecycle.mjs +232 -0
  244. package/src/session-runtime/cwd-plugins.mjs +226 -0
  245. package/src/session-runtime/mcp-glue.mjs +177 -0
  246. package/src/session-runtime/model-recency.mjs +111 -0
  247. package/src/session-runtime/native-search.mjs +247 -0
  248. package/src/session-runtime/output-styles.mjs +11 -9
  249. package/src/session-runtime/prewarm.mjs +142 -0
  250. package/src/session-runtime/provider-models.mjs +278 -0
  251. package/src/session-runtime/provider-usage.mjs +120 -0
  252. package/src/session-runtime/quick-model-rows.mjs +205 -0
  253. package/src/session-runtime/quick-search-models.mjs +47 -0
  254. package/src/session-runtime/session-hooks.mjs +93 -0
  255. package/src/session-runtime/settings-api.mjs +352 -0
  256. package/src/session-runtime/tool-catalog.mjs +29 -29
  257. package/src/session-runtime/tool-defs.mjs +84 -0
  258. package/src/session-runtime/warmup-schedulers.mjs +201 -0
  259. package/src/session-runtime/workflow.mjs +1 -1
  260. package/src/standalone/agent-tool/helpers.mjs +237 -0
  261. package/src/standalone/agent-tool/notify.mjs +107 -0
  262. package/src/standalone/agent-tool/provider-init.mjs +143 -0
  263. package/src/standalone/agent-tool/render.mjs +152 -0
  264. package/src/standalone/agent-tool/tool-def.mjs +55 -0
  265. package/src/standalone/agent-tool.mjs +138 -669
  266. package/src/standalone/channel-admin.mjs +102 -90
  267. package/src/standalone/channel-worker.mjs +4 -7
  268. package/src/standalone/explore-tool.mjs +64 -14
  269. package/src/standalone/hook-bus/config.mjs +207 -0
  270. package/src/standalone/hook-bus/constants.mjs +90 -0
  271. package/src/standalone/hook-bus/handlers.mjs +481 -0
  272. package/src/standalone/hook-bus/payload.mjs +31 -0
  273. package/src/standalone/hook-bus/rules.mjs +77 -0
  274. package/src/standalone/hook-bus.mjs +77 -870
  275. package/src/standalone/memory-runtime-proxy.mjs +7 -0
  276. package/src/standalone/opencode-go-login.mjs +5 -1
  277. package/src/standalone/provider-admin.mjs +1 -16
  278. package/src/standalone/usage-dashboard.mjs +3 -1
  279. package/src/tui/App.jsx +1059 -8110
  280. package/src/tui/app/app-format.mjs +213 -0
  281. package/src/tui/app/channel-pickers.mjs +508 -0
  282. package/src/tui/app/clipboard.mjs +67 -0
  283. package/src/tui/app/core-memory-picker.mjs +210 -0
  284. package/src/tui/app/extension-pickers.mjs +506 -0
  285. package/src/tui/app/input-parsers.mjs +193 -0
  286. package/src/tui/app/maintenance-pickers.mjs +356 -0
  287. package/src/tui/app/model-options.mjs +334 -0
  288. package/src/tui/app/model-picker.mjs +365 -0
  289. package/src/tui/app/onboarding-steps.mjs +400 -0
  290. package/src/tui/app/project-picker.mjs +247 -0
  291. package/src/tui/app/provider-setup-picker.mjs +580 -0
  292. package/src/tui/app/resume-picker.mjs +55 -0
  293. package/src/tui/app/route-pickers.mjs +419 -0
  294. package/src/tui/app/settings-picker.mjs +489 -0
  295. package/src/tui/app/slash-commands.mjs +101 -0
  296. package/src/tui/app/slash-dispatch.mjs +427 -0
  297. package/src/tui/app/text-layout.mjs +46 -0
  298. package/src/tui/app/theme-effort-pickers.mjs +154 -0
  299. package/src/tui/app/transcript-window.mjs +677 -0
  300. package/src/tui/app/use-mouse-input.mjs +460 -0
  301. package/src/tui/app/use-prompt-handlers.mjs +310 -0
  302. package/src/tui/app/use-transcript-scroll.mjs +512 -0
  303. package/src/tui/app/use-transcript-window.mjs +607 -0
  304. package/src/tui/components/ConfirmBar.jsx +10 -7
  305. package/src/tui/components/Picker.jsx +64 -15
  306. package/src/tui/components/PromptInput.jsx +33 -102
  307. package/src/tui/components/SlashCommandPalette.jsx +8 -1
  308. package/src/tui/components/StatusLine.jsx +69 -15
  309. package/src/tui/components/TextEntryPanel.jsx +11 -0
  310. package/src/tui/components/ToolExecution.jsx +52 -594
  311. package/src/tui/components/TranscriptItem.jsx +105 -0
  312. package/src/tui/components/UsagePanel.jsx +18 -4
  313. package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
  314. package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
  315. package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
  316. package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
  317. package/src/tui/components/tool-execution/text-format.mjs +161 -0
  318. package/src/tui/display-width.mjs +20 -3
  319. package/src/tui/dist/index.mjs +13553 -12384
  320. package/src/tui/engine/agent-job-feed.mjs +133 -0
  321. package/src/tui/engine/notification-plan.mjs +76 -0
  322. package/src/tui/engine/render-timing.mjs +17 -0
  323. package/src/tui/engine/tool-approval.mjs +94 -0
  324. package/src/tui/engine/tool-card-results.mjs +234 -0
  325. package/src/tui/engine/tool-result-status.mjs +135 -0
  326. package/src/tui/engine.mjs +170 -574
  327. package/src/tui/figures.mjs +5 -0
  328. package/src/tui/index.jsx +65 -1
  329. package/src/tui/input-editing.mjs +2 -2
  330. package/src/tui/markdown/format-token.mjs +4 -1
  331. package/src/tui/statusline-ansi-bridge.mjs +11 -3
  332. package/src/tui/theme.mjs +6 -0
  333. package/src/ui/statusline-agents.mjs +213 -0
  334. package/src/ui/statusline-format.mjs +146 -0
  335. package/src/ui/statusline-segments.mjs +148 -0
  336. package/src/ui/statusline.mjs +77 -501
  337. package/src/ui/tool-card.mjs +0 -1
  338. package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
  339. package/src/workflows/default/WORKFLOW.md +16 -18
  340. package/src/workflows/sequential/WORKFLOW.md +16 -18
  341. package/vendor/ink/build/display-width.js +19 -3
  342. package/vendor/ink/build/ink.js +112 -7
  343. package/vendor/ink/build/log-update.js +17 -3
  344. package/vendor/ink/build/wrap-text.js +125 -0
  345. package/scripts/_test-folder-dialog.mjs +0 -30
  346. package/scripts/fix-brief-fn.mjs +0 -35
  347. package/scripts/fix-format-tool-surface.mjs +0 -24
  348. package/scripts/fix-tool-exec-visible.mjs +0 -42
  349. package/scripts/patch-agent-brief.mjs +0 -48
  350. package/scripts/patch-app.mjs +0 -21
  351. package/scripts/patch-app2.mjs +0 -18
  352. package/scripts/patch-dist-brief.mjs +0 -96
  353. package/scripts/patch-tool-exec.mjs +0 -70
  354. package/src/examples/schedules/SCHEDULE.example.md +0 -32
  355. package/src/examples/webhooks/WEBHOOK.example.md +0 -40
  356. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
  357. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
  358. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
  359. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +0 -162
  360. package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
  361. package/src/runtime/channels/lib/holidays.mjs +0 -138
  362. package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
  363. package/src/runtime/shared/task-notification-envelope.test.mjs +0 -107
  364. package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
  365. package/src/standalone/agent-task-status.test.mjs +0 -76
  366. package/src/tui/components/tool-output-format.test.mjs +0 -399
  367. package/src/tui/display-width.test.mjs +0 -35
  368. package/src/tui/engine-runtime-notification.test.mjs +0 -115
  369. package/src/tui/engine-tool-result-text.test.mjs +0 -75
  370. package/src/tui/input-editing.selection.test.mjs +0 -75
  371. package/src/tui/markdown/format-token.test.mjs +0 -354
  372. package/src/tui/markdown/render-ansi.test.mjs +0 -108
  373. package/src/tui/markdown/stream-fence.test.mjs +0 -26
  374. package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
  375. package/src/tui/paste-fix.test.mjs +0 -119
  376. package/src/tui/prompt-history-store.test.mjs +0 -52
  377. package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
  378. package/src/tui/transcript-tool-failures.test.mjs +0 -111
  379. package/src/ui/markdown.test.mjs +0 -70
  380. package/src/ui/statusline-context-label.test.mjs +0 -15
  381. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
  382. package/src/vendor/statusline/bin/statusline-route.test.mjs +0 -80
@@ -1,137 +1,66 @@
1
- import { existsSync, readFileSync, statSync } from 'node:fs';
2
- import { dirname, isAbsolute, join, resolve } from 'node:path';
1
+ import { readFileSync, statSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import {
5
5
  cancelBackgroundTask,
6
6
  cleanupBackgroundTasks,
7
7
  getBackgroundTask,
8
8
  listBackgroundTasks,
9
- renderBackgroundTask,
10
9
  reconcileBackgroundTask,
11
- setBackgroundTaskEnqueueFallback,
12
10
  startBackgroundTask,
13
11
  sanitizeTaskMeta,
14
12
  taskIdFromArgs,
15
13
  } from '../runtime/shared/background-tasks.mjs';
16
- import { modelVisibleToolCompletionMessage } from '../runtime/shared/tool-execution-contract.mjs';
17
14
  import { presentErrorText, errorLine } from '../runtime/shared/err-text.mjs';
18
15
  import { updateJsonAtomicSync } from '../runtime/shared/atomic-file.mjs';
19
- import {
20
- normalizeAgentPermission,
21
- normalizeAgentPermissionOrNone,
22
- parseMarkdownFrontmatter,
23
- } from '../runtime/shared/markdown-frontmatter.mjs';
16
+ import { normalizeAgentPermission } from '../runtime/shared/markdown-frontmatter.mjs';
24
17
  import { prepareAgentSession } from '../runtime/agent/orchestrator/agent-runtime/session-builder.mjs';
25
18
  import {
26
19
  agentWatchdogPolicyActive,
27
20
  evaluateAgentWatchdogAbort,
28
21
  resolveAgentWatchdogPolicy,
29
22
  } from '../runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs';
30
- import {
31
- appendAgentProgressKv,
32
- buildAgentTaskProgressFields,
33
- } from './agent-task-status.mjs';
23
+ import { buildAgentTaskProgressFields } from './agent-task-status.mjs';
34
24
  import { AGENT_OWNER } from '../runtime/agent/orchestrator/agent-owner.mjs';
35
- import { clearGatewaySessionRoute, writeGatewaySessionRoute } from '../vendor/statusline/src/gateway/session-routes.mjs';
36
25
  import { isKnownProvider } from './provider-admin.mjs';
26
+ import {
27
+ ACTIVE_STAGES,
28
+ AGENT_TOOL,
29
+ DEFAULT_AGENT_PRESETS,
30
+ DEFAULT_PROVIDER,
31
+ WORKER_INDEX_FILE,
32
+ } from './agent-tool/tool-def.mjs';
33
+ import {
34
+ agentPresetName,
35
+ agentScope,
36
+ agentTagOf,
37
+ clean,
38
+ clearAgentStatuslineRoute,
39
+ envTimeoutMs,
40
+ findPreset,
41
+ nonNegativeInt,
42
+ normalizeAgentName,
43
+ normalizeAgentRoute,
44
+ positiveInt,
45
+ presetKey,
46
+ readAgentFrontmatterPermission,
47
+ resolvePrompt,
48
+ rowMatchesContext,
49
+ sessionMatchesContext,
50
+ synthesizePreset,
51
+ terminalPidForContext,
52
+ withCwdHeader,
53
+ writeAgentStatuslineRoute,
54
+ } from './agent-tool/helpers.mjs';
55
+ import { abnormalEmptyFinishError, renderResult } from './agent-tool/render.mjs';
56
+ import { createProviderInit } from './agent-tool/provider-init.mjs';
57
+ import { createNotify } from './agent-tool/notify.mjs';
58
+ // Re-export the static tool descriptor so importers of this facade keep the
59
+ // identical public surface (`import { AGENT_TOOL } from './agent-tool.mjs'`).
60
+ export { AGENT_TOOL };
37
61
 
38
62
  const STANDALONE_SOURCE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
39
63
 
40
- const PRESET_ALIASES = new Map([
41
- ['opus-xhigh', { base: 'opus-high', effort: 'xhigh', id: 'opus-xhigh', name: 'OPUS XHIGH' }],
42
- ]);
43
-
44
- const DEFAULT_AGENT_PRESETS = Object.freeze({
45
- explore: 'sonnet-high',
46
- maintainer: 'haiku',
47
- worker: 'sonnet-high',
48
- 'heavy-worker': 'sonnet-high',
49
- reviewer: 'opus-xhigh',
50
- debugger: 'opus-xhigh',
51
- });
52
-
53
- // Mirrors DEFAULT_PROVIDER in mixdog-session-runtime.mjs. Used only as the
54
- // last-resort fallback when a stored agent route omits its provider and the
55
- // config carries no defaultProvider.
56
- const DEFAULT_PROVIDER = 'anthropic-oauth';
57
-
58
- export const AGENT_TOOL = {
59
- name: 'agent',
60
- title: 'Agent',
61
- annotations: {
62
- title: 'Agent',
63
- readOnlyHint: false,
64
- destructiveHint: true,
65
- idempotentHint: false,
66
- openWorldHint: true,
67
- agentHidden: true,
68
- },
69
- description: 'Delegate scoped work. Agent handoffs always start background tasks and return task IDs immediately. Spawn independent scopes in parallel with distinct tags; for the same scope/tag, use send or spawn with the same tag to reuse the live session. Wait for the completion notification before dependent work. Do not call status/read after spawn; status/read are manual recovery only.',
70
- inputSchema: {
71
- type: 'object',
72
- properties: {
73
- type: { type: 'string', enum: ['spawn', 'send', 'list', 'close', 'cancel', 'status', 'read', 'cleanup'], description: 'Action. Default spawn.' },
74
- task_id: { type: 'string', description: 'Manual recovery task ID.' },
75
- agent: { type: 'string', description: 'Workflow agent id.' },
76
- tag: { type: 'string', description: 'Stable scope handle. Reuse the same tag for follow-up on the same scope; use distinct tags only for independent scopes.' },
77
- sessionId: { type: 'string', description: 'Raw sess_ id.' },
78
- prompt: { type: 'string', description: 'Scoped task brief.' },
79
- message: { type: 'string', description: 'Follow-up for send/reuse, or brief.' },
80
- file: { type: 'string', description: 'Prompt file.' },
81
- cwd: { type: 'string', description: 'Working directory.' },
82
- context: { type: 'string', description: 'Extra agent context.' },
83
- },
84
- additionalProperties: true,
85
- },
86
- };
87
-
88
- const WORKER_INDEX_FILE = 'agent-workers.json';
89
-
90
- // A worker that hits the loop iteration ceiling, gets truncated mid-synthesis,
91
- // or produces an empty terminal turn returns content:'' but never throws — so
92
- // the background task would otherwise reconcile as a benign `completed` empty
93
- // success. That is wrong: an empty final answer is an error. loop.mjs is the
94
- // single classifier: it tags result.terminationReason for abnormal finishes
95
- // (carried through manager.mjs terminalResultPreview). We key purely off that
96
- // here. iteration_cap / truncated are real problems for EVERY agent (hidden
97
- // too). The plain `empty` case is tagged by the loop ONLY for public agents;
98
- // hidden agents (explorer/cycle/…) legitimately emit empty terminal turns and
99
- // are left untagged, so they stay benign.
100
- function abnormalEmptyFinishError(result, agent) {
101
- // The loop (loop.mjs) is the single classifier: it tags terminationReason
102
- // ONLY for abnormal finishes, and gates the `empty` case behind !hidden.
103
- // So we key purely off terminationReason here — no separate content/hidden
104
- // check, which previously (a) exempted hidden agents from cap/truncated and
105
- // (b) let a capped tool-call turn with preamble text slip through as success.
106
- const reason = result?.terminationReason;
107
- if (!reason) return null;
108
- const iterations = result?.iterations ?? 0;
109
- const toolCallsTotal = result?.toolCallsTotal ?? 0;
110
- const maxLoopIterations = result?.maxLoopIterations ?? 0;
111
- const stopReason = result?.stopReason ?? result?.stop_reason ?? null;
112
- switch (reason) {
113
- case 'iteration_cap':
114
- // Real problem for EVERY agent (hidden too): the loop never terminated on
115
- // its own contract, so any preamble text is not a trustworthy final answer.
116
- return `agent '${agent}' hit the loop iteration ceiling (${maxLoopIterations} iterations, ${toolCallsTotal} tool calls) without producing a final answer`;
117
- case 'truncated':
118
- return `agent '${agent}' response was truncated (stopReason=${stopReason}) before a final answer (${iterations} iterations, ${toolCallsTotal} tool calls)`;
119
- case 'empty':
120
- // Only tagged for PUBLIC agents (hidden agents legitimately emit empty
121
- // terminal turns and are left untagged by the loop).
122
- return `agent '${agent}' finished without a final answer (stopReason=${stopReason ?? 'none'}, ${iterations} iterations, ${toolCallsTotal} tool calls)`;
123
- default:
124
- return null;
125
- }
126
- }
127
-
128
- function envTimeoutMs(name, fallback) {
129
- const raw = process.env[name];
130
- if (raw === undefined || raw === '') return fallback;
131
- const n = Number(raw);
132
- return Number.isFinite(n) && n >= 0 ? Math.floor(n) : fallback;
133
- }
134
-
135
64
  // Grace window during which a terminated/idle worker row is kept around so the
136
65
  // same terminal can re-use (or cleanly re-spawn) the same tag. Cached as a
137
66
  // constant like the other timeouts; override with MIXDOG_AGENT_TERMINAL_REAP_MS.
@@ -140,334 +69,52 @@ function envTimeoutMs(name, fallback) {
140
69
  // rewrite; a fresh respawn (send dead-tag fallback) starts smaller and cheaper.
141
70
  // Trace replay (2026-07): agent tail cost 1h 1.77M -> 5m 1.11M (-37%).
142
71
  const TERMINAL_REAP_MS = envTimeoutMs('MIXDOG_AGENT_TERMINAL_REAP_MS', 5 * 60_000);
72
+ // Provider-aware reap windows: the 5m default matches Anthropic's 5m agent
73
+ // tail TTL, but providers with long-lived automatic prefix caches (OpenAI 24h
74
+ // retention, DeepSeek hours-days, Groq 2h) keep a warm cache well past 5m, so
75
+ // same-tag reuse stays cheaper than a respawn. Capped at 1h — holding closed
76
+ // worker sessions longer hoards manager memory for a reuse pattern that
77
+ // rarely spans hours. Explicit MIXDOG_AGENT_TERMINAL_REAP_MS overrides all.
78
+ const PROVIDER_REAP_MS = {
79
+ 'openai': 60 * 60_000,
80
+ 'deepseek': 60 * 60_000,
81
+ 'groq': 60 * 60_000,
82
+ 'openai-oauth': 10 * 60_000,
83
+ };
84
+ function reapMsForProvider(provider) {
85
+ if (clean(process.env.MIXDOG_AGENT_TERMINAL_REAP_MS)) return TERMINAL_REAP_MS;
86
+ return PROVIDER_REAP_MS[clean(provider)] || TERMINAL_REAP_MS;
87
+ }
143
88
  // Independent hard cap for the spawn *prep* phase (ensureProvider /
144
89
  // prepareAgentSession / catalog+rules load). Kept separate from the
145
90
  // first-response watchdog so prep cannot hang a whole fanout before the model
146
91
  // request starts. Set MIXDOG_AGENT_SPAWN_PREP_TIMEOUT_MS=0 to fully disable the
147
92
  // cap and restore strictly-unbounded prep.
148
93
  const DEFAULT_SPAWN_PREP_TIMEOUT_MS = envTimeoutMs('MIXDOG_AGENT_SPAWN_PREP_TIMEOUT_MS', 120_000);
149
- const ACTIVE_STAGES = new Set(['connecting', 'requesting', 'streaming', 'tool_running', 'running', 'cancelling']);
150
-
151
- // Process-wide TTL cache for agent AGENT.md frontmatter permission. The file
152
- // rarely changes, but readAgentFrontmatterPermission() otherwise pays several
153
- // existsSync()+readFileSync() calls on EVERY spawn — multiplied across a
154
- // parallel fanout that is pure redundant synchronous I/O on the event loop,
155
- // which blocks every other concurrent spawn. Short TTL keeps edits picked up
156
- // quickly while collapsing burst reads.
157
- const _frontmatterPermCache = new Map(); // key -> { value, atMs }
158
- const FRONTMATTER_PERM_CACHE_TTL_MS = envTimeoutMs('MIXDOG_AGENT_FRONTMATTER_TTL_MS', 5_000);
159
-
160
- function clean(value) {
161
- return String(value ?? '').trim();
162
- }
163
-
164
- function agentTagOf(session) {
165
- return clean(session?.agentTag);
166
- }
167
-
168
- function normalizeAgentName(value) {
169
- const id = clean(value).toLowerCase().replace(/[\s_]+/g, '-');
170
- if (id === 'explorer') return 'explore';
171
- if (id === 'maint' || id === 'maintenance' || id === 'memory') return 'maintainer';
172
- if (id === 'heavy' || id === 'heavyworker') return 'heavy-worker';
173
- if (id === 'review') return 'reviewer';
174
- if (id === 'debug') return 'debugger';
175
- return id;
176
- }
177
-
178
- function readAgentFrontmatterPermission(agent, dataDir) {
179
- const cleanAgent = clean(agent);
180
- if (!cleanAgent) return null;
181
- const cacheKey = `${dataDir || ''}\u0000${cleanAgent}`;
182
- const cached = _frontmatterPermCache.get(cacheKey);
183
- if (cached && Date.now() - cached.atMs < FRONTMATTER_PERM_CACHE_TTL_MS) {
184
- return cached.value;
185
- }
186
- const candidates = [];
187
- if (dataDir) {
188
- candidates.push(join(dataDir, 'agents', cleanAgent, 'AGENT.md'));
189
- candidates.push(join(dataDir, 'agents', `${cleanAgent}.md`));
190
- }
191
- candidates.push(join(STANDALONE_SOURCE_ROOT, 'agents', cleanAgent, 'AGENT.md'));
192
- candidates.push(join(STANDALONE_SOURCE_ROOT, 'agents', `${cleanAgent}.md`));
193
- let resolved = null;
194
- for (const file of candidates) {
195
- if (!existsSync(file)) continue;
196
- const fm = parseMarkdownFrontmatter(readFileSync(file, 'utf8'));
197
- const permission = normalizeAgentPermissionOrNone(fm.permission);
198
- if (permission) { resolved = permission; break; }
199
- }
200
- _frontmatterPermCache.set(cacheKey, { value: resolved, atMs: Date.now() });
201
- return resolved;
202
- }
203
-
204
- function positiveInt(value) {
205
- const n = Number(value);
206
- return Number.isFinite(n) && n > 0 ? Math.floor(n) : null;
207
- }
208
-
209
- function terminalPidForContext(context = {}) {
210
- return positiveInt(context?.clientHostPid);
211
- }
212
-
213
- function agentScope(args = {}, context = {}) {
214
- const scope = clean(args.scope || args.terminal || args.term).toLowerCase();
215
- if (args.allTerminals === true || scope === 'all' || scope === 'global') return {};
216
- return context || {};
217
- }
218
-
219
- function sessionMatchesContext(session, context = {}) {
220
- const wantedPid = terminalPidForContext(context);
221
- if (!wantedPid) return true;
222
- const sessionPid = positiveInt(session?.clientHostPid);
223
- return !!sessionPid && sessionPid === wantedPid;
224
- }
225
-
226
- function rowMatchesContext(row, context = {}) {
227
- const wantedPid = terminalPidForContext(context);
228
- if (!wantedPid) return true;
229
- const rowPid = positiveInt(row?.clientHostPid);
230
- return !!rowPid && rowPid === wantedPid;
231
- }
232
-
233
- function nonNegativeInt(value) {
234
- if (value === undefined || value === null || value === '') return null;
235
- const n = Number(value);
236
- return Number.isFinite(n) && n >= 0 ? Math.floor(n) : null;
237
- }
238
-
239
- function presetKey(preset) {
240
- return clean(preset?.id || preset?.name);
241
- }
242
-
243
- function bridgeRouteForStatusline(preset = {}) {
244
- const provider = clean(preset.provider);
245
- const model = clean(preset.model);
246
- if (!provider || !model) return null;
247
- const out = {
248
- mode: 'fixed',
249
- defaultProvider: provider,
250
- defaultModel: model,
251
- };
252
- const id = clean(preset.id);
253
- const name = clean(preset.name);
254
- const modelDisplay = clean(preset.modelDisplay || preset.display || preset.displayName);
255
- const effort = clean(preset.effort);
256
- if (id) out.presetId = id;
257
- if (name) out.presetName = name;
258
- if (modelDisplay) out.modelDisplay = modelDisplay;
259
- if (effort) {
260
- out.effort = effort;
261
- out.displayEffort = effort;
262
- }
263
- if (preset.fast === true || preset.fast === false) out.fast = preset.fast;
264
- return out;
265
- }
266
-
267
- function writeAgentStatuslineRoute(sessionId, preset) {
268
- const route = bridgeRouteForStatusline(preset);
269
- if (!sessionId || !route) return false;
270
- try { return writeGatewaySessionRoute(sessionId, route); } catch { return false; }
271
- }
272
-
273
- function clearAgentStatuslineRoute(sessionId) {
274
- if (!sessionId) return false;
275
- try { return clearGatewaySessionRoute(sessionId); } catch { return false; }
276
- }
277
-
278
- function findPreset(config, key) {
279
- const wanted = clean(key).toLowerCase();
280
- if (!wanted) return null;
281
- const presets = Array.isArray(config?.presets) ? config.presets : [];
282
- return presets.find((p) => {
283
- return clean(p?.id).toLowerCase() === wanted || clean(p?.name).toLowerCase() === wanted;
284
- }) || null;
285
- }
286
-
287
- function synthesizePreset(config, key) {
288
- const alias = PRESET_ALIASES.get(clean(key).toLowerCase());
289
- if (!alias) return null;
290
- const base = findPreset(config, alias.base);
291
- if (!base) return null;
292
- return {
293
- ...base,
294
- id: alias.id,
295
- name: alias.name,
296
- effort: alias.effort,
297
- };
298
- }
299
-
300
- function normalizeAgentRoute(routeLike, fallbackProvider = '') {
301
- const provider = clean(routeLike?.provider) || clean(fallbackProvider);
302
- const model = clean(routeLike?.model);
303
- if (!provider || !model) return null;
304
- return {
305
- provider,
306
- model,
307
- effort: clean(routeLike?.effort) || undefined,
308
- fast: routeLike?.fast === true,
309
- };
310
- }
311
-
312
- function agentPresetName(agent) {
313
- return `AGENT ${String(agent || '').toUpperCase()}`;
314
- }
315
-
316
- async function resolvePrompt(args, cwd) {
317
- const prompt = clean(args.prompt || args.message);
318
- const file = clean(args.file);
319
- if (prompt && file) throw new Error('agent: provide only one of prompt/message or file');
320
- if (prompt) return prompt;
321
- if (file) {
322
- const target = isAbsolute(file) ? file : resolve(cwd || process.cwd(), file);
323
- return readFileSync(target, 'utf8');
324
- }
325
- throw new Error('agent: prompt/message/file is required');
326
- }
327
-
328
- function withCwdHeader(prompt, cwd) {
329
- if (!cwd) return prompt;
330
- if (String(prompt).startsWith('[effective-cwd]')) return prompt;
331
- return `[effective-cwd] ${cwd}\n\n${prompt}`;
332
- }
333
94
 
334
- function oneLine(value, max = 180) {
335
- const text = String(value ?? '').replace(/\s+/g, ' ').trim();
336
- return text.length > max ? `${text.slice(0, max)}...` : text;
337
- }
338
-
339
- function compactIso(value) {
340
- const text = clean(value);
341
- if (!text) return '';
342
- return text.replace('T', ' ').replace(/\.\d+Z$/, 'Z');
343
- }
344
-
345
- function formatElapsedMs(ms) {
346
- const n = Number(ms);
347
- if (!Number.isFinite(n) || n < 0) return '';
348
- const totalSec = Math.floor(n / 1000);
349
- if (totalSec < 60) return `${totalSec}s`;
350
- const min = Math.floor(totalSec / 60);
351
- const sec = totalSec % 60;
352
- if (min < 60) return sec ? `${min}m${sec}s` : `${min}m`;
353
- const hr = Math.floor(min / 60);
354
- const remMin = min % 60;
355
- return remMin ? `${hr}h${remMin}m` : `${hr}h`;
356
- }
357
-
358
- function elapsedFromStamps(startedAt, finishedAt, status) {
359
- const start = Date.parse(clean(startedAt));
360
- if (!Number.isFinite(start)) return null;
361
- const finish = Date.parse(clean(finishedAt));
362
- const end = Number.isFinite(finish) ? finish : Date.now();
363
- const label = formatElapsedMs(end - start);
364
- if (!label) return null;
365
- return Number.isFinite(finish) ? label : `${label} (running)`;
366
- }
367
-
368
- function stripFinalAnswerWrapper(value) {
369
- const text = String(value ?? '').trim();
370
- const match = /^<final-answer\b[^>]*>([\s\S]*?)<\/final-answer>\s*$/i.exec(text);
371
- return match ? match[1].trim() : text;
372
- }
373
-
374
- function renderResult(value) {
375
- if (value === undefined || value === null) return '';
376
- if (typeof value === 'string') return value;
377
- if (value && typeof value === 'object') {
378
- const lines = [];
379
-
380
- if (Array.isArray(value.workers) || Array.isArray(value.jobs)) {
381
- const workers = Array.isArray(value.workers) ? value.workers : [];
382
- lines.push(`agents: ${workers.length}`);
383
- for (const worker of workers) {
384
- const tokens = worker.windowTokens ? ` ctx=${worker.windowTokens}${worker.windowCap ? `/${worker.windowCap}` : ''}` : '';
385
- const terminal = worker.clientHostPid ? ` term=${worker.clientHostPid}` : '';
386
- const base = `- ${worker.tag} ${worker.agent || 'agent'} ${worker.status || 'idle'}/${worker.worker_stage || worker.stage || 'idle'} ${worker.provider}/${worker.model}${terminal}${tokens}`;
387
- lines.push(appendAgentProgressKv(base, worker));
388
- }
389
- const jobs = Array.isArray(value.jobs) ? value.jobs : [];
390
- lines.push(`tasks: ${jobs.length}`);
391
- for (const job of jobs) {
392
- const target = job.tag || job.sessionId || '-';
393
- const terminal = job.clientHostPid ? ` term=${job.clientHostPid}` : '';
394
- const base = `- ${job.task_id} ${job.type} ${job.status} target=${target}${terminal}${job.error ? ` error=${job.error}` : ''}`;
395
- lines.push(appendAgentProgressKv(base, job));
396
- }
397
- if (workers.length === 0 && jobs.length === 0) lines.push('(no agents or tasks)');
398
- return lines.join('\n');
399
- }
400
-
401
- if (value.task_id) {
402
- lines.push(`agent task: ${value.task_id}`);
403
- lines.push(`status: ${value.status}`);
404
- if (value.type) lines.push(`type: ${value.type}`);
405
- if (value.reused) lines.push('reused: true');
406
- if (value.tag || value.sessionId) lines.push(`target: ${value.tag || '-'} ${value.sessionId || ''}`.trim());
407
- if (value.agent) lines.push(`agent: ${value.agent}`);
408
- if (value.provider && value.model) lines.push(`model: ${value.provider}/${value.model}`);
409
- if (value.effort) lines.push(`effort: ${value.effort}`);
410
- if (value.fast === true || value.fast === false) lines.push(`fast: ${value.fast ? 'on' : 'off'}`);
411
- if (value.maxLoopIterations) {
412
- const limitParts = [];
413
- if (value.maxLoopIterations) limitParts.push(`loop=${value.maxLoopIterations}`);
414
- lines.push(`limits: ${limitParts.join(' ')}`);
415
- }
416
- if (value.stage || value.workerStatus) lines.push(`worker: ${value.workerStatus || 'unknown'}/${value.stage || 'unknown'}`);
417
- if (value.worker_stage) lines.push(`worker_stage: ${value.worker_stage}`);
418
- if (value.last_progress) lines.push(`last_progress: ${value.last_progress}`);
419
- if (Number.isFinite(value.silent_for)) lines.push(`silent_for: ${value.silent_for}s`);
420
- if (value.watchdog) lines.push(`watchdog: ${value.watchdog}`);
421
- if (Number.isFinite(value.queued_followups)) lines.push(`queued_followups: ${value.queued_followups}`);
422
- if (value.diagnostic) lines.push(`diagnostic: ${value.diagnostic}`);
423
- if (value.startedAt) lines.push(`started: ${compactIso(value.startedAt)}`);
424
- if (value.finishedAt) lines.push(`finished: ${compactIso(value.finishedAt)}`);
425
- {
426
- const elapsed = elapsedFromStamps(value.startedAt, value.finishedAt, value.status);
427
- if (elapsed) lines.push(`elapsed: ${elapsed}`);
428
- }
429
- if (value.error) lines.push(`error: ${value.error}`);
430
- if (value.status === 'running') lines.push('notification: completion will be delivered to the owner session; use read/status only for manual recovery.');
431
- if (value.result !== undefined) {
432
- const result = value.result;
433
- const content = typeof result === 'string' ? result : result?.content;
434
- if (content) lines.push('', stripFinalAnswerWrapper(content));
435
- else lines.push('', JSON.stringify(result, null, 2));
436
- }
437
- return lines.join('\n');
438
- }
439
-
440
- if (value.queued) {
441
- return [
442
- 'agent message queued',
443
- value.reused ? 'reused: true' : null,
444
- `target: ${value.tag || '-'} ${value.sessionId || ''}`.trim(),
445
- value.agent ? `agent: ${value.agent}` : null,
446
- `queueDepth: ${value.queueDepth ?? 1}`,
447
- ].filter(Boolean).join('\n');
448
- }
449
-
450
- if (value.closed !== undefined) {
451
- return [
452
- `agent close: ${value.closed ? 'ok' : 'not closed'}`,
453
- value.tag ? `tag: ${value.tag}` : null,
454
- value.sessionId ? `sessionId: ${value.sessionId}` : null,
455
- value.task_id ? `task_id: ${value.task_id}` : null,
456
- value.forgotten ? 'forgotten: true' : null,
457
- ].filter(Boolean).join('\n');
458
- }
459
-
460
- if (value.content !== undefined) {
461
- const header = [
462
- value.respawned ? 'agent respawned' : 'agent result',
463
- value.tag ? `tag=${value.tag}` : null,
464
- value.agent ? `agent=${value.agent}` : null,
465
- value.provider && value.model ? `${value.provider}/${value.model}` : null,
466
- ].filter(Boolean).join(' ');
467
- return `${header}\n${stripFinalAnswerWrapper(value.content)}`;
468
- }
469
- }
470
- return JSON.stringify(value, null, 2);
95
+ // Global spawn-start stagger: unlimited-N parallel fan-out otherwise fires all
96
+ // first provider calls in the same instant, racing the server-side prompt-
97
+ // cache write/propagation window. Bench (parallel-10, identical-ms starts):
98
+ // 12.5% cache miss; 3000ms lane stagger: 4.3%; 166ms: 6.0%. 2000ms default
99
+ // picked as a low-cost middle ground. Chain (not a fixed lane count) so it
100
+ // scales to any N: each new spawn's start is pushed to at least STAGGER_MS
101
+ // after the previous spawn's start; sequential/non-overlapping spawns (now
102
+ // already past the window) pay zero added latency. MIXDOG_SPAWN_STAGGER_MS=0
103
+ // disables. Applied inside the deferred job body (see startDeferredSpawnJob)
104
+ // so the agent tool call itself still returns task_id immediately.
105
+ const SPAWN_STAGGER_MS = envTimeoutMs('MIXDOG_SPAWN_STAGGER_MS', 2000);
106
+ let lastSpawnStartAt = 0;
107
+ async function waitForSpawnStagger() {
108
+ if (SPAWN_STAGGER_MS <= 0) return;
109
+ const now = Date.now();
110
+ const myStart = Math.max(now, lastSpawnStartAt + SPAWN_STAGGER_MS);
111
+ lastSpawnStartAt = myStart;
112
+ const delay = myStart - now;
113
+ if (delay <= 0) return;
114
+ await new Promise((resolve) => {
115
+ const t = setTimeout(resolve, delay);
116
+ t.unref?.();
117
+ });
471
118
  }
472
119
 
473
120
  export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultCwd, onSubagentEvent }) {
@@ -504,7 +151,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
504
151
  const t = workerRowTime(row);
505
152
  if (!t) return true;
506
153
  if (!isTerminalWorkerStatus(row.status || row.stage)) return true;
507
- return Date.now() - t < TERMINAL_REAP_MS;
154
+ return Date.now() - t < reapMsForProvider(row.provider);
508
155
  }
509
156
 
510
157
  function normalizeWorkerRows(value) {
@@ -913,9 +560,10 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
913
560
  return true;
914
561
  }
915
562
 
916
- function scheduleReap(sessionId) {
563
+ function scheduleReap(sessionId, provider = null) {
917
564
  if (!sessionId) return;
918
565
  cancelReap(sessionId);
566
+ const reapProvider = provider || getLiveSession(sessionId)?.provider || null;
919
567
  const handle = setTimeout(() => {
920
568
  reapTimers.delete(sessionId);
921
569
  try { mgr.hideSessionFromList?.(sessionId); } catch {}
@@ -924,7 +572,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
924
572
  removeWorkerRow({ tag, sessionId });
925
573
  clearAgentStatuslineRoute(sessionId);
926
574
  try { mgr.closeSession(sessionId, 'terminal-reap'); } catch {}
927
- }, TERMINAL_REAP_MS);
575
+ }, reapMsForProvider(reapProvider));
928
576
  handle.unref?.();
929
577
  reapTimers.set(sessionId, handle);
930
578
  }
@@ -937,140 +585,10 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
937
585
  return ACTIVE_STAGES.has(session?.status || '');
938
586
  }
939
587
 
940
- // Provider init de-dup. Four goals that must not conflict:
941
- // (a) a parallel spawn fanout that all targets the SAME provider with the
942
- // SAME effective config performs at most ONE initProviders() pass
943
- // instead of N serially-awaited registry rebuilds,
944
- // (b) a provider CONFIG CHANGE still reaches initProviders() so the
945
- // registry's own signature guard can re-initialize it,
946
- // (c) two DIFFERENT config signatures for the same provider never init
947
- // concurrently — otherwise a slow init of the OLD config could land
948
- // after a fast init of the NEW config and revert the live registry to
949
- // stale config, and
950
- // (d) a SUPERSEDED request never resolves before the provider is actually
951
- // ready: even when its own (stale) init is dropped to satisfy (c), the
952
- // caller (a spawn about to run prepareSpawn) must still WAIT for the
953
- // latest init to finish, or it would proceed against an unprepared /
954
- // stale provider.
955
- //
956
- // Skip cache + in-flight collapse are keyed on `provider + signature(effective
957
- // config)`. To satisfy (c) we SERIALIZE all inits per provider on a chain
958
- // promise and re-check the latest-requested signature inside the chain: a
959
- // request superseded by a newer signature drops its own init. To satisfy (d)
960
- // such a dropped request does not resolve immediately — it awaits the
961
- // provider's latest settled init (tracked as a rolling "ready" promise) so the
962
- // caller only proceeds once the newest config is live.
963
- // Per-provider state. `chain` serializes the ACTUAL initProviders() calls so
964
- // two different config signatures never run concurrently (goal c). `latestGen`
965
- // / `latestSig` track the newest requested config. `ready` is a rolling
966
- // deferred that resolves only when the LATEST requested init has completed —
967
- // a superseded caller awaits the ready deferred captured at call time, and
968
- // when a newer request arrives the older deferred ADOPTS the newer one, so a
969
- // superseded caller transitively waits for the latest init (goal d).
970
- const _providerState = new Map(); // provider -> state
971
- const _providerInitPending = new Map(); // provider -> { sigKey, promise } identical-sig collapse
972
- // Upper bound on how long a queued init waits for the PRIOR chain link before
973
- // proceeding anyway. A prior init that HANGS (never settles) must not poison
974
- // the chain and wedge every later request behind it. A hung init can never
975
- // *complete* against the registry, so it cannot land-after and clobber a
976
- // newer config (goal c only fears slow-but-completing inits) — so proceeding
977
- // once the gate expires is safe. Defaults to the spawn-prep cap; 0 disables.
978
- const PROVIDER_CHAIN_GATE_MS = DEFAULT_SPAWN_PREP_TIMEOUT_MS;
979
- function providerRegistered(provider) {
980
- return typeof reg.getProvider !== 'function' || Boolean(reg.getProvider(provider));
981
- }
982
- function effectiveProviderConfig(config, provider) {
983
- const providers = { ...(config.providers || {}) };
984
- providers[provider] = { ...(providers[provider] || {}), enabled: true };
985
- return providers;
986
- }
987
- function providerStateFor(provider) {
988
- let s = _providerState.get(provider);
989
- if (!s) {
990
- s = { chain: Promise.resolve(), completedSig: null, latestSig: null, latestGen: 0, ready: null };
991
- _providerState.set(provider, s);
992
- }
993
- return s;
994
- }
995
- function providerInitSignature(provider, effectiveProviders) {
996
- let body;
997
- try { body = JSON.stringify(effectiveProviders); }
998
- catch { body = String(Date.now()); } // unserializable → force a fresh init
999
- return `${provider}\u0000${body}`;
1000
- }
1001
- function gateOnPrior(prior) {
1002
- const settled = Promise.resolve(prior).catch(() => {});
1003
- if (!(PROVIDER_CHAIN_GATE_MS > 0)) return settled;
1004
- return new Promise((resolve) => {
1005
- let done = false;
1006
- const finish = () => { if (!done) { done = true; resolve(); } };
1007
- const timer = setTimeout(finish, PROVIDER_CHAIN_GATE_MS);
1008
- timer.unref?.();
1009
- settled.then(() => { clearTimeout(timer); finish(); }, () => { clearTimeout(timer); finish(); });
1010
- });
1011
- }
1012
- function ensureProvider(config, provider) {
1013
- const effective = effectiveProviderConfig(config, provider);
1014
- const sigKey = providerInitSignature(provider, effective);
1015
- const registered = () => providerRegistered(provider);
1016
- const s = providerStateFor(provider);
1017
- // Completed-skip: this exact effective config is already live for this
1018
- // provider. A config change flips sigKey so we fall through; a torn-down
1019
- // provider (no longer registered) also does.
1020
- if (s.completedSig === sigKey && registered()) return Promise.resolve();
1021
- // Identical-sig collapse: a request with the SAME sigKey is already in
1022
- // flight — share its caller promise.
1023
- const pending = _providerInitPending.get(provider);
1024
- if (pending && pending.sigKey === sigKey) return pending.promise;
1025
- // New generation. Repoint the rolling `ready` deferred to THIS gen and make
1026
- // the previous gen's deferred ADOPT the new one, so any superseded caller
1027
- // awaiting an older deferred transitively waits for the newest init (d).
1028
- const gen = ++s.latestGen;
1029
- s.latestSig = sigKey;
1030
- const prevReady = s.ready;
1031
- let resolveReady;
1032
- const readyPromise = new Promise((r) => { resolveReady = r; });
1033
- s.ready = { gen, promise: readyPromise, resolve: resolveReady };
1034
- if (prevReady && prevReady.gen < gen) {
1035
- try { prevReady.resolve(readyPromise); } catch { /* already settled */ }
1036
- }
1037
- // Serialize the ACTUAL init behind the prior chain link (gated so a hung
1038
- // prior cannot wedge the chain). A superseded gen's chain link settles
1039
- // quickly — it never awaits a later gen — so there is no deadlock.
1040
- const prior = s.chain;
1041
- const chainLink = gateOnPrior(prior).then(async () => {
1042
- if (s.latestGen !== gen) {
1043
- // Superseded before we ran: drop our (stale) init entirely (goal c).
1044
- // Our `ready` deferred already adopts the newer gen, so the caller below
1045
- // still waits for the latest init. Settle now to release the chain.
1046
- return;
1047
- }
1048
- try {
1049
- if (!(s.completedSig === sigKey && registered())) {
1050
- await reg.initProviders(effective);
1051
- s.completedSig = sigKey;
1052
- }
1053
- } finally {
1054
- // ALWAYS release this gen's waiters once we are the latest — even on a
1055
- // registry init failure. Adopting (superseded) callers chained onto this
1056
- // deferred would otherwise hang forever; instead they proceed and their
1057
- // own createSession()/prep-timeout surfaces the unprepared provider.
1058
- resolveReady();
1059
- }
1060
- });
1061
- // Next chain link waits on us (settled, never poisoned).
1062
- s.chain = chainLink.catch(() => {});
1063
- // The CALLER awaits the ready deferred (resolves only when the LATEST init
1064
- // for this provider completes), not just the chain link — so a superseded
1065
- // caller blocks until the newest config is live (goal d). chainLink is
1066
- // awaited first so a registry init error surfaces to this caller.
1067
- const callerPromise = chainLink.then(() => readyPromise).finally(() => {
1068
- const cur = _providerInitPending.get(provider);
1069
- if (cur && cur.promise === callerPromise) _providerInitPending.delete(provider);
1070
- });
1071
- _providerInitPending.set(provider, { sigKey, promise: callerPromise });
1072
- return callerPromise;
1073
- }
588
+ // Provider init de-dup lives in ./agent-tool/provider-init.mjs; the factory
589
+ // keeps its per-provider chain/ready state private per agent instance. The
590
+ // chain-gate defaults to the spawn-prep cap (see provider-init.mjs comments).
591
+ const { ensureProvider } = createProviderInit(reg, DEFAULT_SPAWN_PREP_TIMEOUT_MS);
1074
592
 
1075
593
  function resolvePreset(config, args) {
1076
594
  if (args.provider && args.model) {
@@ -1228,9 +746,23 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1228
746
 
1229
747
  function getJob(args, context = {}) {
1230
748
  const taskId = taskIdFromArgs(args);
1231
- if (!taskId) throw new Error('agent read/status: task_id is required');
1232
- const task = getBackgroundTask(taskId, { surface: 'agent', context });
1233
- if (!task) throw new Error(`agent read/status: task "${taskId}" not found`);
749
+ if (taskId) {
750
+ const task = getBackgroundTask(taskId, { surface: 'agent', context });
751
+ if (!task) throw new Error(`agent read/status: task "${taskId}" not found`);
752
+ return task;
753
+ }
754
+ // Fall back to tag/sessionId resolution, same precedence as close()
755
+ // (agent-tool.mjs close()): clean(args.tag || args.sessionId || ...).
756
+ const target = clean(args.tag || args.sessionId);
757
+ if (!target) throw new Error('agent read/status: task_id, tag, or sessionId is required');
758
+ const candidates = listBackgroundTasks({ surface: 'agent', context })
759
+ .filter(Boolean)
760
+ .filter((row) => row.tag === target || row.sessionId === target);
761
+ if (!candidates.length) throw new Error(`agent read/status: no task found for tag/sessionId "${target}"`);
762
+ // Prefer most recent when multiple tasks match the same tag.
763
+ candidates.sort((a, b) => new Date(b.startedAt || 0) - new Date(a.startedAt || 0));
764
+ const task = getBackgroundTask(candidates[0].task_id, { surface: 'agent', context });
765
+ if (!task) throw new Error(`agent read/status: task "${candidates[0].task_id}" not found`);
1234
766
  return task;
1235
767
  }
1236
768
 
@@ -1334,102 +866,9 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1334
866
  removeWorkerRow({ tag: prepared.tag, sessionId: prepared.session.id });
1335
867
  }
1336
868
 
1337
- function enqueueCompletionMessage(sessionId, text, meta = {}) {
1338
- const target = clean(sessionId);
1339
- if (!target || typeof mgr.enqueuePendingMessage !== 'function') return false;
1340
- try {
1341
- const visible = modelVisibleToolCompletionMessage(text, meta);
1342
- return Boolean(visible && mgr.enqueuePendingMessage(target, visible) > 0);
1343
- } catch {
1344
- return false;
1345
- }
1346
- }
1347
-
1348
- // Wire the canonical completion fallback to this agent surface's owner-session
1349
- // enqueue so notifyTaskCompletion can deliver via callerSessionId when no
1350
- // notifyFn is present or it declines. Registered once per agent (the closure
1351
- // captures mgr); signatures align: (callerSessionId, message, meta).
1352
- setBackgroundTaskEnqueueFallback((sessionId, text, meta) => enqueueCompletionMessage(sessionId, text, meta));
1353
-
1354
- function workerNotifyFn(workerSessionId, notifyContext = {}) {
1355
- const workerId = clean(workerSessionId);
1356
- const ownerSessionId = clean(notifyContext?.callerSessionId || notifyContext?.sessionId);
1357
- const upstream = typeof notifyContext?.notifyFn === 'function' ? notifyContext.notifyFn : null;
1358
- return (text, meta = {}) => {
1359
- let ownerDelivered = false;
1360
- if (upstream) {
1361
- try {
1362
- const result = upstream(text, meta);
1363
- ownerDelivered = result !== false;
1364
- if (ownerDelivered) Promise.resolve(result).catch(() => {});
1365
- } catch {
1366
- ownerDelivered = false;
1367
- }
1368
- }
1369
- if (!ownerDelivered && ownerSessionId) {
1370
- ownerDelivered = enqueueCompletionMessage(ownerSessionId, text, meta);
1371
- }
1372
- const workerDelivered = workerId && workerId !== ownerSessionId
1373
- ? enqueueCompletionMessage(workerId, text, meta)
1374
- : ownerDelivered;
1375
- return ownerSessionId ? ownerDelivered : workerDelivered;
1376
- };
1377
- }
1378
-
1379
- function notifyOwnerAgentCompletionEarly(job, resultValue, notifyContext = {}) {
1380
- if (!job || job._earlyCompletionNotified === true) return false;
1381
- const ownerSessionId = clean(notifyContext?.callerSessionId || notifyContext?.sessionId);
1382
- const upstream = typeof notifyContext?.notifyFn === 'function' ? notifyContext.notifyFn : null;
1383
- const finishedAt = new Date().toISOString();
1384
- // An abnormal-empty finish carries an `error` — the early preview must NOT
1385
- // present it as a benign `completed` card, or the Lead sees success before
1386
- // the later `failed` reconcile lands. Mirror the terminal status/instruction.
1387
- const earlyStatus = resultValue && resultValue.error ? 'failed' : 'completed';
1388
- const snapshot = {
1389
- ...job,
1390
- status: earlyStatus,
1391
- finishedAt,
1392
- finishedAtMs: Date.now(),
1393
- result: resultValue,
1394
- resultType: job.resultType || 'agent_task_result',
1395
- meta: sanitizeTaskMeta(job.meta || {}),
1396
- ...(resultValue && resultValue.error ? { error: resultValue.error } : {}),
1397
- };
1398
- // An early notification is only a header-only *preview*: it fires before
1399
- // the worker's session is persisted to signal the running→completed
1400
- // transition. It deliberately carries NO result body — the canonical
1401
- // notifyTaskCompletion delivers the body exactly once via the
1402
- // reconcile/finally path, so omitting it here keeps notifications
1403
- // exact-once with no duplicate body.
1404
- const text = renderBackgroundTask(snapshot, { includeResult: false });
1405
- const meta = {
1406
- type: snapshot.resultType,
1407
- execution_surface: 'agent',
1408
- execution_id: job.taskId || null,
1409
- status: earlyStatus,
1410
- instruction: `The async agent task ${job.taskId || ''} has finished (${earlyStatus}) - review this result in your next step.`,
1411
- ...(ownerSessionId ? { caller_session_id: ownerSessionId } : {}),
1412
- };
1413
- let delivered = false;
1414
- if (upstream) {
1415
- try {
1416
- const result = upstream(text, meta);
1417
- delivered = result !== false;
1418
- if (delivered) Promise.resolve(result).catch(() => {});
1419
- } catch {
1420
- delivered = false;
1421
- }
1422
- }
1423
- if (!delivered && ownerSessionId) {
1424
- delivered = enqueueCompletionMessage(ownerSessionId, text, meta);
1425
- }
1426
- if (delivered) {
1427
- // Mark only that a header-only preview fired. The canonical
1428
- // notifyTaskCompletion still owns the single body-carrying notification.
1429
- job._earlyCompletionNotified = true;
1430
- }
1431
- return delivered;
1432
- }
869
+ // Owner/worker completion notification lives in ./agent-tool/notify.mjs; the
870
+ // factory captures mgr and registers the canonical completion fallback.
871
+ const { workerNotifyFn, notifyOwnerAgentCompletionEarly } = createNotify(mgr);
1433
872
 
1434
873
  function startJob(type, meta, run, notifyContext = null) {
1435
874
  const clientHostPid = terminalPidForContext(notifyContext);
@@ -1467,6 +906,11 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1467
906
 
1468
907
  function startDeferredSpawnJob(args, callerCwd, context, notifyContext, extras = {}) {
1469
908
  return startJob('spawn', pendingSpawnMeta(args, extras), async (job) => {
909
+ // Stagger concurrent spawn starts before prep/model-call begins (see
910
+ // SPAWN_STAGGER_MS above). Runs inside the job body, so the tool call
911
+ // that queued this job already returned task_id before this awaits.
912
+ await waitForSpawnStagger();
913
+ if (job?.status === 'cancelled') return null;
1470
914
  // prepareSpawn (ensureProvider/prepareAgentSession) runs before runSpawn
1471
915
  // installs its progress watchdog, so guard prep with an internal env-
1472
916
  // backed cap rather than exposing per-call timeout knobs on the agent
@@ -1556,7 +1000,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1556
1000
  const config = cfgMod.loadConfig();
1557
1001
  const agent = normalizeAgentName(args.agent);
1558
1002
  if (!agent) throw new Error('agent spawn: agent is required');
1559
- const agentPermission = readAgentFrontmatterPermission(agent, dataDir);
1003
+ const agentPermission = readAgentFrontmatterPermission(agent, dataDir, STANDALONE_SOURCE_ROOT);
1560
1004
  const agentPerm = normalizeAgentPermission(agentPermission) || null;
1561
1005
  const { presetName, preset } = resolvePreset(config, args);
1562
1006
  await ensureProvider(config, preset.provider);
@@ -1943,6 +1387,16 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1943
1387
  if (tag) forgetTag(tag);
1944
1388
  removeWorkerRow({ tag, sessionId });
1945
1389
  clearAgentStatuslineRoute(sessionId);
1390
+ // Cancel any running background task bound to this session BEFORE closing
1391
+ // the session. Otherwise closeSession rejects the in-flight runSpawn with
1392
+ // "Session closed: closeSession" and the catch path reconciles the task as
1393
+ // `failed` — a user-initiated close must surface as `cancelled` instead.
1394
+ // (The explicit task_id path below stays as a no-op fallback: cancel is
1395
+ // idempotent once terminal.)
1396
+ for (const row of listBackgroundTasks({ surface: 'agent', context: scopedContext })) {
1397
+ if (row.sessionId !== sessionId && row.tag !== target) continue;
1398
+ cancelBackgroundTask(row.task_id, 'cancelled by agent close');
1399
+ }
1946
1400
  const ok = mgr.closeSession(sessionId, 'cli-agent-close');
1947
1401
  if (task?.taskId) cancelBackgroundTask(task.taskId, 'cancelled by agent close');
1948
1402
  return { closed: ok, tag, sessionId, task_id: task?.taskId || null };
@@ -2029,11 +1483,26 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
2029
1483
  if (!fallbackTag || fallbackTag.startsWith('sess_') || !isDeadTarget) throw err;
2030
1484
  const prompt = clean(args.message || args.prompt);
2031
1485
  if (!prompt) throw err;
1486
+ // Inherit the dead session's agent/cwd from the tag registry so the
1487
+ // auto-respawn doesn't die on "agent spawn: agent is required" when
1488
+ // Lead's send (correctly) omitted them. Read BEFORE forgetTag wipes
1489
+ // the registry entries.
1490
+ const inheritedAgent = clean(args.agent) || clean(tagAgents.get(fallbackTag));
1491
+ const inheritedCwd = clean(args.cwd) || clean(tagCwds.get(fallbackTag));
1492
+ if (!inheritedAgent) throw err;
2032
1493
  // Clear the terminal trace so the fresh spawn isn't rejected by
2033
1494
  // the lingering-trace guard, then run the normal deferred spawn.
2034
1495
  try { forgetTag(fallbackTag); } catch {}
2035
1496
  try { removeWorkerRow({ tag: fallbackTag }); } catch {}
2036
- const spawnArgs = { ...args, type: 'spawn', tag: fallbackTag, prompt, message: undefined };
1497
+ const spawnArgs = {
1498
+ ...args,
1499
+ type: 'spawn',
1500
+ tag: fallbackTag,
1501
+ prompt,
1502
+ message: undefined,
1503
+ agent: inheritedAgent,
1504
+ ...(inheritedCwd ? { cwd: inheritedCwd } : {}),
1505
+ };
2037
1506
  const job = startDeferredSpawnJob(spawnArgs, callerCwd, context, notifyContext, { respawned: true });
2038
1507
  return renderResult(renderJob(job, false));
2039
1508
  }