mixdog 0.9.2 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (417) hide show
  1. package/package.json +8 -3
  2. package/scripts/anthropic-maxtokens-test.mjs +119 -0
  3. package/scripts/bench/lead-review-tasks-r3.json +20 -0
  4. package/scripts/bench/lead-review-tasks.json +20 -0
  5. package/scripts/bench/r4-mixed-tasks.json +20 -0
  6. package/scripts/bench/review-tasks.json +20 -0
  7. package/scripts/bench/round-codex.json +114 -0
  8. package/scripts/bench/round-mixdog-lead-r3.json +269 -0
  9. package/scripts/bench/round-mixdog-lead.json +269 -0
  10. package/scripts/bench/round-mixdog.json +126 -0
  11. package/scripts/bench/round-r10-bigsample.json +679 -0
  12. package/scripts/bench/round-r11-codexalign.json +257 -0
  13. package/scripts/bench/round-r4-codex.json +114 -0
  14. package/scripts/bench/round-r4-mixed.json +225 -0
  15. package/scripts/bench/round-r5-gpt-lead.json +259 -0
  16. package/scripts/bench/round-r6-codex.json +114 -0
  17. package/scripts/bench/round-r6-solo.json +257 -0
  18. package/scripts/bench/round-r7-full.json +254 -0
  19. package/scripts/bench/round-r8-fulldefault.json +255 -0
  20. package/scripts/bench-run.mjs +215 -29
  21. package/scripts/build-tui.mjs +13 -1
  22. package/scripts/explore-bench.mjs +124 -0
  23. package/scripts/freevar-smoke.mjs +95 -0
  24. package/scripts/hook-bus-test.mjs +191 -0
  25. package/scripts/internal-comms-bench.mjs +1 -0
  26. package/scripts/internal-comms-smoke.mjs +10 -9
  27. package/scripts/mouse-probe.mjs +45 -0
  28. package/scripts/output-style-bench.mjs +13 -6
  29. package/scripts/output-style-smoke.mjs +4 -4
  30. package/scripts/path-suffix-test.mjs +57 -0
  31. package/scripts/provider-toolcall-test.mjs +7 -3
  32. package/scripts/recall-bench.mjs +207 -0
  33. package/scripts/recall-usecase-cases.json +18 -0
  34. package/scripts/recall-usecase-probe.json +6 -0
  35. package/scripts/session-bench.mjs +152 -6
  36. package/scripts/tool-smoke.mjs +30 -67
  37. package/scripts/tui-render-smoke.mjs +90 -0
  38. package/scripts/webhook-smoke.mjs +208 -0
  39. package/src/agents/debugger/AGENT.md +5 -2
  40. package/src/agents/heavy-worker/AGENT.md +21 -11
  41. package/src/agents/maintainer/AGENT.md +4 -0
  42. package/src/agents/reviewer/AGENT.md +3 -2
  43. package/src/agents/worker/AGENT.md +21 -11
  44. package/src/lib/rules-builder.cjs +4 -0
  45. package/src/mixdog-session-runtime.mjs +933 -3731
  46. package/src/output-styles/default.md +34 -9
  47. package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
  48. package/src/output-styles/minimal.md +4 -1
  49. package/src/output-styles/simple.md +22 -7
  50. package/src/repl.mjs +5 -5
  51. package/src/rules/agent/00-common.md +2 -0
  52. package/src/rules/agent/30-explorer.md +8 -11
  53. package/src/rules/lead/lead-brief.md +12 -0
  54. package/src/rules/lead/lead-tool.md +2 -9
  55. package/src/rules/shared/01-tool.md +11 -5
  56. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
  57. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
  58. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
  59. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
  60. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
  61. package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
  62. package/src/runtime/agent/orchestrator/config.mjs +69 -2
  63. package/src/runtime/agent/orchestrator/context/collect.mjs +51 -0
  64. package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
  65. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +63 -21
  66. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
  67. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
  68. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
  69. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +97 -1343
  70. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
  71. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +78 -10
  72. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +1 -13
  73. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
  74. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
  75. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
  76. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
  77. package/src/runtime/agent/orchestrator/providers/gemini.mjs +44 -1014
  78. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +18 -4
  79. package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
  80. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +86 -11
  81. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +348 -0
  82. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
  83. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
  84. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +19 -12
  85. package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
  86. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
  87. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
  88. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +41 -1142
  89. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +732 -0
  90. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
  91. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +303 -2119
  92. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +140 -995
  93. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +227 -0
  94. package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
  95. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +436 -0
  96. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
  97. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
  98. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +38 -12
  99. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +7 -8
  100. package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
  101. package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
  102. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
  103. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
  104. package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
  105. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
  106. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
  107. package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
  108. package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
  109. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +274 -0
  110. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
  111. package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
  112. package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
  113. package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
  114. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +47 -0
  115. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +182 -0
  116. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
  117. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
  118. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
  119. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
  120. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
  121. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
  122. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
  123. package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
  124. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
  125. package/src/runtime/agent/orchestrator/session/loop.mjs +409 -1304
  126. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
  127. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +230 -0
  128. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
  129. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +149 -0
  130. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
  131. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
  132. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
  133. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
  134. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
  135. package/src/runtime/agent/orchestrator/session/manager.mjs +226 -2114
  136. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
  137. package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
  138. package/src/runtime/agent/orchestrator/stall-policy.mjs +3 -3
  139. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
  140. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
  141. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +33 -42
  142. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
  143. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
  144. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
  145. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
  146. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +1 -1
  147. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
  148. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
  149. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
  150. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
  151. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -87
  152. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
  153. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
  154. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +78 -304
  155. package/src/runtime/agent/orchestrator/tools/builtin.mjs +11 -6
  156. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
  157. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
  158. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
  159. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +551 -0
  160. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
  161. package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
  162. package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
  163. package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
  164. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
  165. package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
  166. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1080 -0
  167. package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
  168. package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
  169. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
  170. package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
  171. package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
  172. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
  173. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +36 -4277
  174. package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
  175. package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
  176. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
  177. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
  178. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
  179. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
  180. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
  181. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
  182. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
  183. package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
  184. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -23
  185. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +10 -74
  186. package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
  187. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
  188. package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
  189. package/src/runtime/channels/backends/discord-access.mjs +32 -0
  190. package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
  191. package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
  192. package/src/runtime/channels/backends/discord.mjs +12 -292
  193. package/src/runtime/channels/index.mjs +241 -894
  194. package/src/runtime/channels/lib/backend-dispatch.mjs +44 -0
  195. package/src/runtime/channels/lib/boot-profile.mjs +23 -0
  196. package/src/runtime/channels/lib/crash-log.mjs +106 -0
  197. package/src/runtime/channels/lib/event-pipeline.mjs +18 -1
  198. package/src/runtime/channels/lib/event-queue.mjs +63 -4
  199. package/src/runtime/channels/lib/inbound-routing.mjs +111 -0
  200. package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
  201. package/src/runtime/channels/lib/output-forwarder.mjs +9 -1
  202. package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
  203. package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
  204. package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
  205. package/src/runtime/channels/lib/session-discovery.mjs +56 -4
  206. package/src/runtime/channels/lib/telegram-format.mjs +19 -22
  207. package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
  208. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  209. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
  210. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
  211. package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
  212. package/src/runtime/channels/lib/webhook/deliveries.mjs +312 -0
  213. package/src/runtime/channels/lib/webhook/log.mjs +42 -0
  214. package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
  215. package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
  216. package/src/runtime/channels/lib/webhook.mjs +36 -570
  217. package/src/runtime/channels/lib/whisper-language.mjs +42 -0
  218. package/src/runtime/channels/tool-defs.mjs +11 -130
  219. package/src/runtime/memory/index.mjs +258 -2050
  220. package/src/runtime/memory/lib/core-memory-store.mjs +351 -1
  221. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
  222. package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
  223. package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
  224. package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
  225. package/src/runtime/memory/lib/http-wire.mjs +57 -0
  226. package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
  227. package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
  228. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
  229. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
  230. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
  231. package/src/runtime/memory/lib/memory-cycle2.mjs +72 -837
  232. package/src/runtime/memory/lib/memory-embed.mjs +149 -0
  233. package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
  234. package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
  235. package/src/runtime/memory/lib/memory-recall-store.mjs +22 -2
  236. package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
  237. package/src/runtime/memory/lib/memory.mjs +20 -0
  238. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
  239. package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
  240. package/src/runtime/memory/lib/query-handlers.mjs +780 -0
  241. package/src/runtime/memory/lib/recall-format.mjs +238 -0
  242. package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
  243. package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
  244. package/src/runtime/memory/tool-defs.mjs +6 -14
  245. package/src/runtime/search/lib/http-fetch.mjs +274 -0
  246. package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
  247. package/src/runtime/search/lib/web-tools.mjs +24 -602
  248. package/src/runtime/shared/abort-controller.mjs +1 -1
  249. package/src/runtime/shared/atomic-file.mjs +26 -1
  250. package/src/runtime/shared/background-tasks.mjs +2 -3
  251. package/src/runtime/shared/buffered-appender.mjs +149 -0
  252. package/src/runtime/shared/launcher-control.mjs +2 -2
  253. package/src/runtime/shared/task-notification-envelope.mjs +98 -0
  254. package/src/runtime/shared/tool-execution-contract.mjs +2 -2
  255. package/src/runtime/shared/tool-primitives.mjs +308 -0
  256. package/src/runtime/shared/tool-result-summary.mjs +515 -0
  257. package/src/runtime/shared/tool-surface.mjs +80 -898
  258. package/src/runtime/shared/transcript-writer.mjs +52 -2
  259. package/src/runtime/shared/update-checker.mjs +7 -4
  260. package/src/session-runtime/config-helpers.mjs +291 -0
  261. package/src/session-runtime/config-lifecycle.mjs +232 -0
  262. package/src/session-runtime/cwd-plugins.mjs +226 -0
  263. package/src/session-runtime/effort.mjs +128 -0
  264. package/src/session-runtime/fs-utils.mjs +10 -0
  265. package/src/session-runtime/mcp-glue.mjs +177 -0
  266. package/src/session-runtime/model-capabilities.mjs +130 -0
  267. package/src/session-runtime/model-recency.mjs +111 -0
  268. package/src/session-runtime/native-search.mjs +247 -0
  269. package/src/session-runtime/output-styles.mjs +126 -0
  270. package/src/session-runtime/plugin-mcp.mjs +114 -0
  271. package/src/session-runtime/prewarm.mjs +142 -0
  272. package/src/session-runtime/provider-models.mjs +278 -0
  273. package/src/session-runtime/provider-usage.mjs +120 -0
  274. package/src/session-runtime/quick-model-rows.mjs +170 -0
  275. package/src/session-runtime/quick-search-models.mjs +46 -0
  276. package/src/session-runtime/session-hooks.mjs +93 -0
  277. package/src/session-runtime/session-text.mjs +100 -0
  278. package/src/session-runtime/settings-api.mjs +319 -0
  279. package/src/session-runtime/statusline-route.mjs +35 -0
  280. package/src/session-runtime/tool-catalog.mjs +720 -0
  281. package/src/session-runtime/tool-defs.mjs +84 -0
  282. package/src/session-runtime/warmup-schedulers.mjs +201 -0
  283. package/src/session-runtime/workflow.mjs +358 -0
  284. package/src/standalone/agent-tool/helpers.mjs +237 -0
  285. package/src/standalone/agent-tool/notify.mjs +107 -0
  286. package/src/standalone/agent-tool/provider-init.mjs +143 -0
  287. package/src/standalone/agent-tool/render.mjs +152 -0
  288. package/src/standalone/agent-tool/tool-def.mjs +55 -0
  289. package/src/standalone/agent-tool.mjs +155 -677
  290. package/src/standalone/channel-worker.mjs +7 -9
  291. package/src/standalone/explore-tool.mjs +40 -12
  292. package/src/standalone/hook-bus/config.mjs +207 -0
  293. package/src/standalone/hook-bus/constants.mjs +90 -0
  294. package/src/standalone/hook-bus/handlers.mjs +481 -0
  295. package/src/standalone/hook-bus/payload.mjs +31 -0
  296. package/src/standalone/hook-bus/rules.mjs +77 -0
  297. package/src/standalone/hook-bus.mjs +110 -746
  298. package/src/standalone/memory-runtime-proxy.mjs +7 -0
  299. package/src/standalone/opencode-go-login.mjs +125 -0
  300. package/src/standalone/provider-admin.mjs +15 -19
  301. package/src/standalone/usage-dashboard.mjs +3 -1
  302. package/src/tui/App.jsx +1163 -7571
  303. package/src/tui/app/app-format.mjs +206 -0
  304. package/src/tui/app/channel-pickers.mjs +510 -0
  305. package/src/tui/app/clipboard.mjs +67 -0
  306. package/src/tui/app/core-memory-picker.mjs +210 -0
  307. package/src/tui/app/extension-pickers.mjs +506 -0
  308. package/src/tui/app/input-parsers.mjs +193 -0
  309. package/src/tui/app/maintenance-pickers.mjs +324 -0
  310. package/src/tui/app/model-options.mjs +330 -0
  311. package/src/tui/app/model-picker.mjs +365 -0
  312. package/src/tui/app/onboarding-steps.mjs +400 -0
  313. package/src/tui/app/project-picker.mjs +247 -0
  314. package/src/tui/app/provider-setup-picker.mjs +580 -0
  315. package/src/tui/app/resume-picker.mjs +55 -0
  316. package/src/tui/app/route-pickers.mjs +419 -0
  317. package/src/tui/app/settings-picker.mjs +490 -0
  318. package/src/tui/app/slash-commands.mjs +101 -0
  319. package/src/tui/app/slash-dispatch.mjs +427 -0
  320. package/src/tui/app/text-layout.mjs +46 -0
  321. package/src/tui/app/theme-effort-pickers.mjs +154 -0
  322. package/src/tui/app/transcript-window.mjs +671 -0
  323. package/src/tui/app/use-mouse-input.mjs +460 -0
  324. package/src/tui/app/use-prompt-handlers.mjs +310 -0
  325. package/src/tui/app/use-transcript-scroll.mjs +510 -0
  326. package/src/tui/app/use-transcript-window.mjs +589 -0
  327. package/src/tui/components/ConfirmBar.jsx +1 -1
  328. package/src/tui/components/Picker.jsx +32 -4
  329. package/src/tui/components/PromptInput.jsx +259 -80
  330. package/src/tui/components/SlashCommandPalette.jsx +8 -1
  331. package/src/tui/components/StatusLine.jsx +63 -12
  332. package/src/tui/components/TextEntryPanel.jsx +11 -0
  333. package/src/tui/components/ToolExecution.jsx +56 -588
  334. package/src/tui/components/TranscriptItem.jsx +105 -0
  335. package/src/tui/components/UsagePanel.jsx +18 -4
  336. package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
  337. package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
  338. package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
  339. package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
  340. package/src/tui/components/tool-execution/text-format.mjs +161 -0
  341. package/src/tui/components/tool-output-format.mjs +2 -2
  342. package/src/tui/display-width.mjs +20 -3
  343. package/src/tui/dist/index.mjs +18034 -17188
  344. package/src/tui/engine/agent-envelope.mjs +296 -0
  345. package/src/tui/engine/agent-job-feed.mjs +133 -0
  346. package/src/tui/engine/boot-profile.mjs +21 -0
  347. package/src/tui/engine/labels.mjs +67 -0
  348. package/src/tui/engine/notice-text.mjs +112 -0
  349. package/src/tui/engine/notification-plan.mjs +76 -0
  350. package/src/tui/engine/queue-helpers.mjs +161 -0
  351. package/src/tui/engine/render-timing.mjs +17 -0
  352. package/src/tui/engine/session-stats.mjs +46 -0
  353. package/src/tui/engine/tool-approval.mjs +94 -0
  354. package/src/tui/engine/tool-call-fields.mjs +23 -0
  355. package/src/tui/engine/tool-card-results.mjs +234 -0
  356. package/src/tui/engine/tool-result-status.mjs +135 -0
  357. package/src/tui/engine/tool-result-text.mjs +126 -0
  358. package/src/tui/engine.mjs +405 -1385
  359. package/src/tui/figures.mjs +5 -0
  360. package/src/tui/index.jsx +105 -0
  361. package/src/tui/input-editing.mjs +60 -10
  362. package/src/tui/keyboard-protocol.mjs +2 -2
  363. package/src/tui/lib/voice-recorder.mjs +35 -19
  364. package/src/tui/markdown/format-token.mjs +11 -9
  365. package/src/tui/paste-attachments.mjs +38 -0
  366. package/src/tui/statusline-ansi-bridge.mjs +11 -3
  367. package/src/tui/theme.mjs +6 -0
  368. package/src/tui/themes/base.mjs +2 -2
  369. package/src/tui/themes/kanagawa.mjs +4 -4
  370. package/src/tui/themes/teal.mjs +4 -5
  371. package/src/tui/themes/utils.mjs +1 -1
  372. package/src/ui/statusline-agents.mjs +213 -0
  373. package/src/ui/statusline-format.mjs +146 -0
  374. package/src/ui/statusline-segments.mjs +148 -0
  375. package/src/ui/statusline.mjs +77 -462
  376. package/src/ui/tool-card.mjs +0 -1
  377. package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
  378. package/src/workflows/default/WORKFLOW.md +16 -9
  379. package/src/workflows/sequential/WORKFLOW.md +16 -11
  380. package/src/workflows/solo/WORKFLOW.md +5 -1
  381. package/vendor/ink/build/display-width.js +19 -3
  382. package/vendor/ink/build/ink.js +103 -6
  383. package/vendor/ink/build/log-update.js +17 -3
  384. package/vendor/ink/build/wrap-text.js +125 -0
  385. package/scripts/_test-folder-dialog.mjs +0 -30
  386. package/scripts/fix-brief-fn.mjs +0 -35
  387. package/scripts/fix-format-tool-surface.mjs +0 -24
  388. package/scripts/fix-tool-exec-visible.mjs +0 -42
  389. package/scripts/patch-agent-brief.mjs +0 -48
  390. package/scripts/patch-app.mjs +0 -21
  391. package/scripts/patch-app2.mjs +0 -18
  392. package/scripts/patch-dist-brief.mjs +0 -96
  393. package/scripts/patch-tool-exec.mjs +0 -70
  394. package/src/examples/schedules/SCHEDULE.example.md +0 -32
  395. package/src/examples/webhooks/WEBHOOK.example.md +0 -40
  396. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
  397. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
  398. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
  399. package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
  400. package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
  401. package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
  402. package/src/standalone/agent-task-status.test.mjs +0 -76
  403. package/src/tui/components/tool-output-format.test.mjs +0 -399
  404. package/src/tui/display-width.test.mjs +0 -35
  405. package/src/tui/engine-runtime-notification.test.mjs +0 -115
  406. package/src/tui/engine-tool-result-text.test.mjs +0 -75
  407. package/src/tui/markdown/format-token.test.mjs +0 -354
  408. package/src/tui/markdown/render-ansi.test.mjs +0 -108
  409. package/src/tui/markdown/stream-fence.test.mjs +0 -26
  410. package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
  411. package/src/tui/prompt-history-store.test.mjs +0 -52
  412. package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
  413. package/src/tui/transcript-tool-failures.test.mjs +0 -111
  414. package/src/ui/markdown.test.mjs +0 -70
  415. package/src/ui/statusline-context-label.test.mjs +0 -15
  416. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
  417. package/src/vendor/statusline/bin/statusline-route.test.mjs +0 -80
@@ -1,472 +1,104 @@
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
- import { presentErrorText } from '../runtime/shared/err-text.mjs';
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.
138
- const TERMINAL_REAP_MS = envTimeoutMs('MIXDOG_AGENT_TERMINAL_REAP_MS', 60 * 60_000);
67
+ // 5m (was 1h): aligned with the BP4 message-tail cache TTL — past 5m the
68
+ // session's prompt cache is cold anyway, so same-tag reuse pays a full prefix
69
+ // rewrite; a fresh respawn (send dead-tag fallback) starts smaller and cheaper.
70
+ // Trace replay (2026-07): agent tail cost 1h 1.77M -> 5m 1.11M (-37%).
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
+ }
139
88
  // Independent hard cap for the spawn *prep* phase (ensureProvider /
140
89
  // prepareAgentSession / catalog+rules load). Kept separate from the
141
90
  // first-response watchdog so prep cannot hang a whole fanout before the model
142
91
  // request starts. Set MIXDOG_AGENT_SPAWN_PREP_TIMEOUT_MS=0 to fully disable the
143
92
  // cap and restore strictly-unbounded prep.
144
93
  const DEFAULT_SPAWN_PREP_TIMEOUT_MS = envTimeoutMs('MIXDOG_AGENT_SPAWN_PREP_TIMEOUT_MS', 120_000);
145
- const ACTIVE_STAGES = new Set(['connecting', 'requesting', 'streaming', 'tool_running', 'running', 'cancelling']);
146
-
147
- // Process-wide TTL cache for agent AGENT.md frontmatter permission. The file
148
- // rarely changes, but readAgentFrontmatterPermission() otherwise pays several
149
- // existsSync()+readFileSync() calls on EVERY spawn — multiplied across a
150
- // parallel fanout that is pure redundant synchronous I/O on the event loop,
151
- // which blocks every other concurrent spawn. Short TTL keeps edits picked up
152
- // quickly while collapsing burst reads.
153
- const _frontmatterPermCache = new Map(); // key -> { value, atMs }
154
- const FRONTMATTER_PERM_CACHE_TTL_MS = envTimeoutMs('MIXDOG_AGENT_FRONTMATTER_TTL_MS', 5_000);
155
-
156
- function clean(value) {
157
- return String(value ?? '').trim();
158
- }
159
-
160
- function agentTagOf(session) {
161
- return clean(session?.agentTag);
162
- }
163
-
164
- function normalizeAgentName(value) {
165
- const id = clean(value).toLowerCase().replace(/[\s_]+/g, '-');
166
- if (id === 'explorer') return 'explore';
167
- if (id === 'maint' || id === 'maintenance' || id === 'memory') return 'maintainer';
168
- if (id === 'heavy' || id === 'heavyworker') return 'heavy-worker';
169
- if (id === 'review') return 'reviewer';
170
- if (id === 'debug') return 'debugger';
171
- return id;
172
- }
173
-
174
- function readAgentFrontmatterPermission(agent, dataDir) {
175
- const cleanAgent = clean(agent);
176
- if (!cleanAgent) return null;
177
- const cacheKey = `${dataDir || ''}\u0000${cleanAgent}`;
178
- const cached = _frontmatterPermCache.get(cacheKey);
179
- if (cached && Date.now() - cached.atMs < FRONTMATTER_PERM_CACHE_TTL_MS) {
180
- return cached.value;
181
- }
182
- const candidates = [];
183
- if (dataDir) {
184
- candidates.push(join(dataDir, 'agents', cleanAgent, 'AGENT.md'));
185
- candidates.push(join(dataDir, 'agents', `${cleanAgent}.md`));
186
- }
187
- candidates.push(join(STANDALONE_SOURCE_ROOT, 'agents', cleanAgent, 'AGENT.md'));
188
- candidates.push(join(STANDALONE_SOURCE_ROOT, 'agents', `${cleanAgent}.md`));
189
- let resolved = null;
190
- for (const file of candidates) {
191
- if (!existsSync(file)) continue;
192
- const fm = parseMarkdownFrontmatter(readFileSync(file, 'utf8'));
193
- const permission = normalizeAgentPermissionOrNone(fm.permission);
194
- if (permission) { resolved = permission; break; }
195
- }
196
- _frontmatterPermCache.set(cacheKey, { value: resolved, atMs: Date.now() });
197
- return resolved;
198
- }
199
-
200
- function positiveInt(value) {
201
- const n = Number(value);
202
- return Number.isFinite(n) && n > 0 ? Math.floor(n) : null;
203
- }
204
-
205
- function terminalPidForContext(context = {}) {
206
- return positiveInt(context?.clientHostPid);
207
- }
208
-
209
- function agentScope(args = {}, context = {}) {
210
- const scope = clean(args.scope || args.terminal || args.term).toLowerCase();
211
- if (args.allTerminals === true || scope === 'all' || scope === 'global') return {};
212
- return context || {};
213
- }
214
-
215
- function sessionMatchesContext(session, context = {}) {
216
- const wantedPid = terminalPidForContext(context);
217
- if (!wantedPid) return true;
218
- const sessionPid = positiveInt(session?.clientHostPid);
219
- return !!sessionPid && sessionPid === wantedPid;
220
- }
221
94
 
222
- function rowMatchesContext(row, context = {}) {
223
- const wantedPid = terminalPidForContext(context);
224
- if (!wantedPid) return true;
225
- const rowPid = positiveInt(row?.clientHostPid);
226
- return !!rowPid && rowPid === wantedPid;
227
- }
228
-
229
- function nonNegativeInt(value) {
230
- if (value === undefined || value === null || value === '') return null;
231
- const n = Number(value);
232
- return Number.isFinite(n) && n >= 0 ? Math.floor(n) : null;
233
- }
234
-
235
- function presetKey(preset) {
236
- return clean(preset?.id || preset?.name);
237
- }
238
-
239
- function bridgeRouteForStatusline(preset = {}) {
240
- const provider = clean(preset.provider);
241
- const model = clean(preset.model);
242
- if (!provider || !model) return null;
243
- const out = {
244
- mode: 'fixed',
245
- defaultProvider: provider,
246
- defaultModel: model,
247
- };
248
- const id = clean(preset.id);
249
- const name = clean(preset.name);
250
- const modelDisplay = clean(preset.modelDisplay || preset.display || preset.displayName);
251
- const effort = clean(preset.effort);
252
- if (id) out.presetId = id;
253
- if (name) out.presetName = name;
254
- if (modelDisplay) out.modelDisplay = modelDisplay;
255
- if (effort) {
256
- out.effort = effort;
257
- out.displayEffort = effort;
95
+ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultCwd, onSubagentEvent }) {
96
+ // Optional bridge to the standard hook bus for SubagentStart / SubagentStop.
97
+ // Best-effort: a hook error must never affect worker spawn/finish.
98
+ function emitSubagentEvent(phase, agent, extra = {}) {
99
+ if (typeof onSubagentEvent !== 'function') return;
100
+ try { onSubagentEvent(phase, { agent_type: agent || null, ...extra }); } catch { /* best-effort */ }
258
101
  }
259
- if (preset.fast === true || preset.fast === false) out.fast = preset.fast;
260
- return out;
261
- }
262
-
263
- function writeAgentStatuslineRoute(sessionId, preset) {
264
- const route = bridgeRouteForStatusline(preset);
265
- if (!sessionId || !route) return false;
266
- try { return writeGatewaySessionRoute(sessionId, route); } catch { return false; }
267
- }
268
-
269
- function clearAgentStatuslineRoute(sessionId) {
270
- if (!sessionId) return false;
271
- try { return clearGatewaySessionRoute(sessionId); } catch { return false; }
272
- }
273
-
274
- function findPreset(config, key) {
275
- const wanted = clean(key).toLowerCase();
276
- if (!wanted) return null;
277
- const presets = Array.isArray(config?.presets) ? config.presets : [];
278
- return presets.find((p) => {
279
- return clean(p?.id).toLowerCase() === wanted || clean(p?.name).toLowerCase() === wanted;
280
- }) || null;
281
- }
282
-
283
- function synthesizePreset(config, key) {
284
- const alias = PRESET_ALIASES.get(clean(key).toLowerCase());
285
- if (!alias) return null;
286
- const base = findPreset(config, alias.base);
287
- if (!base) return null;
288
- return {
289
- ...base,
290
- id: alias.id,
291
- name: alias.name,
292
- effort: alias.effort,
293
- };
294
- }
295
-
296
- function normalizeAgentRoute(routeLike, fallbackProvider = '') {
297
- const provider = clean(routeLike?.provider) || clean(fallbackProvider);
298
- const model = clean(routeLike?.model);
299
- if (!provider || !model) return null;
300
- return {
301
- provider,
302
- model,
303
- effort: clean(routeLike?.effort) || undefined,
304
- fast: routeLike?.fast === true,
305
- };
306
- }
307
-
308
- function agentPresetName(agent) {
309
- return `AGENT ${String(agent || '').toUpperCase()}`;
310
- }
311
-
312
- async function resolvePrompt(args, cwd) {
313
- const prompt = clean(args.prompt || args.message);
314
- const file = clean(args.file);
315
- if (prompt && file) throw new Error('agent: provide only one of prompt/message or file');
316
- if (prompt) return prompt;
317
- if (file) {
318
- const target = isAbsolute(file) ? file : resolve(cwd || process.cwd(), file);
319
- return readFileSync(target, 'utf8');
320
- }
321
- throw new Error('agent: prompt/message/file is required');
322
- }
323
-
324
- function withCwdHeader(prompt, cwd) {
325
- if (!cwd) return prompt;
326
- if (String(prompt).startsWith('[effective-cwd]')) return prompt;
327
- return `[effective-cwd] ${cwd}\n\n${prompt}`;
328
- }
329
-
330
- function oneLine(value, max = 180) {
331
- const text = String(value ?? '').replace(/\s+/g, ' ').trim();
332
- return text.length > max ? `${text.slice(0, max)}...` : text;
333
- }
334
-
335
- function compactIso(value) {
336
- const text = clean(value);
337
- if (!text) return '';
338
- return text.replace('T', ' ').replace(/\.\d+Z$/, 'Z');
339
- }
340
-
341
- function formatElapsedMs(ms) {
342
- const n = Number(ms);
343
- if (!Number.isFinite(n) || n < 0) return '';
344
- const totalSec = Math.floor(n / 1000);
345
- if (totalSec < 60) return `${totalSec}s`;
346
- const min = Math.floor(totalSec / 60);
347
- const sec = totalSec % 60;
348
- if (min < 60) return sec ? `${min}m${sec}s` : `${min}m`;
349
- const hr = Math.floor(min / 60);
350
- const remMin = min % 60;
351
- return remMin ? `${hr}h${remMin}m` : `${hr}h`;
352
- }
353
-
354
- function elapsedFromStamps(startedAt, finishedAt, status) {
355
- const start = Date.parse(clean(startedAt));
356
- if (!Number.isFinite(start)) return null;
357
- const finish = Date.parse(clean(finishedAt));
358
- const end = Number.isFinite(finish) ? finish : Date.now();
359
- const label = formatElapsedMs(end - start);
360
- if (!label) return null;
361
- return Number.isFinite(finish) ? label : `${label} (running)`;
362
- }
363
-
364
- function stripFinalAnswerWrapper(value) {
365
- const text = String(value ?? '').trim();
366
- const match = /^<final-answer\b[^>]*>([\s\S]*?)<\/final-answer>\s*$/i.exec(text);
367
- return match ? match[1].trim() : text;
368
- }
369
-
370
- function renderResult(value) {
371
- if (value === undefined || value === null) return '';
372
- if (typeof value === 'string') return value;
373
- if (value && typeof value === 'object') {
374
- const lines = [];
375
-
376
- if (Array.isArray(value.workers) || Array.isArray(value.jobs)) {
377
- const workers = Array.isArray(value.workers) ? value.workers : [];
378
- lines.push(`agents: ${workers.length}`);
379
- for (const worker of workers) {
380
- const tokens = worker.windowTokens ? ` ctx=${worker.windowTokens}${worker.windowCap ? `/${worker.windowCap}` : ''}` : '';
381
- const terminal = worker.clientHostPid ? ` term=${worker.clientHostPid}` : '';
382
- const base = `- ${worker.tag} ${worker.agent || 'agent'} ${worker.status || 'idle'}/${worker.worker_stage || worker.stage || 'idle'} ${worker.provider}/${worker.model}${terminal}${tokens}`;
383
- lines.push(appendAgentProgressKv(base, worker));
384
- }
385
- const jobs = Array.isArray(value.jobs) ? value.jobs : [];
386
- lines.push(`tasks: ${jobs.length}`);
387
- for (const job of jobs) {
388
- const target = job.tag || job.sessionId || '-';
389
- const terminal = job.clientHostPid ? ` term=${job.clientHostPid}` : '';
390
- const base = `- ${job.task_id} ${job.type} ${job.status} target=${target}${terminal}${job.error ? ` error=${presentErrorText(job.error, { surface: 'agent' })}` : ''}`;
391
- lines.push(appendAgentProgressKv(base, job));
392
- }
393
- if (workers.length === 0 && jobs.length === 0) lines.push('(no agents or tasks)');
394
- return lines.join('\n');
395
- }
396
-
397
- if (value.task_id) {
398
- lines.push(`agent task: ${value.task_id}`);
399
- lines.push(`status: ${value.status}`);
400
- if (value.type) lines.push(`type: ${value.type}`);
401
- if (value.reused) lines.push('reused: true');
402
- if (value.tag || value.sessionId) lines.push(`target: ${value.tag || '-'} ${value.sessionId || ''}`.trim());
403
- if (value.agent) lines.push(`agent: ${value.agent}`);
404
- if (value.provider && value.model) lines.push(`model: ${value.provider}/${value.model}`);
405
- if (value.effort) lines.push(`effort: ${value.effort}`);
406
- if (value.fast === true || value.fast === false) lines.push(`fast: ${value.fast ? 'on' : 'off'}`);
407
- if (value.maxLoopIterations) {
408
- const limitParts = [];
409
- if (value.maxLoopIterations) limitParts.push(`loop=${value.maxLoopIterations}`);
410
- lines.push(`limits: ${limitParts.join(' ')}`);
411
- }
412
- if (value.stage || value.workerStatus) lines.push(`worker: ${value.workerStatus || 'unknown'}/${value.stage || 'unknown'}`);
413
- if (value.worker_stage) lines.push(`worker_stage: ${value.worker_stage}`);
414
- if (value.last_progress) lines.push(`last_progress: ${value.last_progress}`);
415
- if (Number.isFinite(value.silent_for)) lines.push(`silent_for: ${value.silent_for}s`);
416
- if (value.watchdog) lines.push(`watchdog: ${value.watchdog}`);
417
- if (Number.isFinite(value.queued_followups)) lines.push(`queued_followups: ${value.queued_followups}`);
418
- if (value.diagnostic) lines.push(`diagnostic: ${value.diagnostic}`);
419
- if (value.startedAt) lines.push(`started: ${compactIso(value.startedAt)}`);
420
- if (value.finishedAt) lines.push(`finished: ${compactIso(value.finishedAt)}`);
421
- {
422
- const elapsed = elapsedFromStamps(value.startedAt, value.finishedAt, value.status);
423
- if (elapsed) lines.push(`elapsed: ${elapsed}`);
424
- }
425
- if (value.error) lines.push(`error: ${presentErrorText(value.error, { surface: 'agent' })}`);
426
- if (value.status === 'running') lines.push('notification: completion will be delivered to the owner session; use read/status only for manual recovery.');
427
- if (value.result !== undefined) {
428
- const result = value.result;
429
- const content = typeof result === 'string' ? result : result?.content;
430
- if (content) lines.push('', stripFinalAnswerWrapper(content));
431
- else lines.push('', JSON.stringify(result, null, 2));
432
- }
433
- return lines.join('\n');
434
- }
435
-
436
- if (value.queued) {
437
- return [
438
- 'agent message queued',
439
- value.reused ? 'reused: true' : null,
440
- `target: ${value.tag || '-'} ${value.sessionId || ''}`.trim(),
441
- value.agent ? `agent: ${value.agent}` : null,
442
- `queueDepth: ${value.queueDepth ?? 1}`,
443
- ].filter(Boolean).join('\n');
444
- }
445
-
446
- if (value.closed !== undefined) {
447
- return [
448
- `agent close: ${value.closed ? 'ok' : 'not closed'}`,
449
- value.tag ? `tag: ${value.tag}` : null,
450
- value.sessionId ? `sessionId: ${value.sessionId}` : null,
451
- value.task_id ? `task_id: ${value.task_id}` : null,
452
- value.forgotten ? 'forgotten: true' : null,
453
- ].filter(Boolean).join('\n');
454
- }
455
-
456
- if (value.content !== undefined) {
457
- const header = [
458
- value.respawned ? 'agent respawned' : 'agent result',
459
- value.tag ? `tag=${value.tag}` : null,
460
- value.agent ? `agent=${value.agent}` : null,
461
- value.provider && value.model ? `${value.provider}/${value.model}` : null,
462
- ].filter(Boolean).join(' ');
463
- return `${header}\n${stripFinalAnswerWrapper(value.content)}`;
464
- }
465
- }
466
- return JSON.stringify(value, null, 2);
467
- }
468
-
469
- export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultCwd }) {
470
102
  const tags = new Map();
471
103
  const tagAgents = new Map();
472
104
  const tagCwds = new Map();
@@ -494,7 +126,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
494
126
  const t = workerRowTime(row);
495
127
  if (!t) return true;
496
128
  if (!isTerminalWorkerStatus(row.status || row.stage)) return true;
497
- return Date.now() - t < TERMINAL_REAP_MS;
129
+ return Date.now() - t < reapMsForProvider(row.provider);
498
130
  }
499
131
 
500
132
  function normalizeWorkerRows(value) {
@@ -903,9 +535,10 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
903
535
  return true;
904
536
  }
905
537
 
906
- function scheduleReap(sessionId) {
538
+ function scheduleReap(sessionId, provider = null) {
907
539
  if (!sessionId) return;
908
540
  cancelReap(sessionId);
541
+ const reapProvider = provider || getLiveSession(sessionId)?.provider || null;
909
542
  const handle = setTimeout(() => {
910
543
  reapTimers.delete(sessionId);
911
544
  try { mgr.hideSessionFromList?.(sessionId); } catch {}
@@ -914,7 +547,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
914
547
  removeWorkerRow({ tag, sessionId });
915
548
  clearAgentStatuslineRoute(sessionId);
916
549
  try { mgr.closeSession(sessionId, 'terminal-reap'); } catch {}
917
- }, TERMINAL_REAP_MS);
550
+ }, reapMsForProvider(reapProvider));
918
551
  handle.unref?.();
919
552
  reapTimers.set(sessionId, handle);
920
553
  }
@@ -927,140 +560,10 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
927
560
  return ACTIVE_STAGES.has(session?.status || '');
928
561
  }
929
562
 
930
- // Provider init de-dup. Four goals that must not conflict:
931
- // (a) a parallel spawn fanout that all targets the SAME provider with the
932
- // SAME effective config performs at most ONE initProviders() pass
933
- // instead of N serially-awaited registry rebuilds,
934
- // (b) a provider CONFIG CHANGE still reaches initProviders() so the
935
- // registry's own signature guard can re-initialize it,
936
- // (c) two DIFFERENT config signatures for the same provider never init
937
- // concurrently — otherwise a slow init of the OLD config could land
938
- // after a fast init of the NEW config and revert the live registry to
939
- // stale config, and
940
- // (d) a SUPERSEDED request never resolves before the provider is actually
941
- // ready: even when its own (stale) init is dropped to satisfy (c), the
942
- // caller (a spawn about to run prepareSpawn) must still WAIT for the
943
- // latest init to finish, or it would proceed against an unprepared /
944
- // stale provider.
945
- //
946
- // Skip cache + in-flight collapse are keyed on `provider + signature(effective
947
- // config)`. To satisfy (c) we SERIALIZE all inits per provider on a chain
948
- // promise and re-check the latest-requested signature inside the chain: a
949
- // request superseded by a newer signature drops its own init. To satisfy (d)
950
- // such a dropped request does not resolve immediately — it awaits the
951
- // provider's latest settled init (tracked as a rolling "ready" promise) so the
952
- // caller only proceeds once the newest config is live.
953
- // Per-provider state. `chain` serializes the ACTUAL initProviders() calls so
954
- // two different config signatures never run concurrently (goal c). `latestGen`
955
- // / `latestSig` track the newest requested config. `ready` is a rolling
956
- // deferred that resolves only when the LATEST requested init has completed —
957
- // a superseded caller awaits the ready deferred captured at call time, and
958
- // when a newer request arrives the older deferred ADOPTS the newer one, so a
959
- // superseded caller transitively waits for the latest init (goal d).
960
- const _providerState = new Map(); // provider -> state
961
- const _providerInitPending = new Map(); // provider -> { sigKey, promise } identical-sig collapse
962
- // Upper bound on how long a queued init waits for the PRIOR chain link before
963
- // proceeding anyway. A prior init that HANGS (never settles) must not poison
964
- // the chain and wedge every later request behind it. A hung init can never
965
- // *complete* against the registry, so it cannot land-after and clobber a
966
- // newer config (goal c only fears slow-but-completing inits) — so proceeding
967
- // once the gate expires is safe. Defaults to the spawn-prep cap; 0 disables.
968
- const PROVIDER_CHAIN_GATE_MS = DEFAULT_SPAWN_PREP_TIMEOUT_MS;
969
- function providerRegistered(provider) {
970
- return typeof reg.getProvider !== 'function' || Boolean(reg.getProvider(provider));
971
- }
972
- function effectiveProviderConfig(config, provider) {
973
- const providers = { ...(config.providers || {}) };
974
- providers[provider] = { ...(providers[provider] || {}), enabled: true };
975
- return providers;
976
- }
977
- function providerStateFor(provider) {
978
- let s = _providerState.get(provider);
979
- if (!s) {
980
- s = { chain: Promise.resolve(), completedSig: null, latestSig: null, latestGen: 0, ready: null };
981
- _providerState.set(provider, s);
982
- }
983
- return s;
984
- }
985
- function providerInitSignature(provider, effectiveProviders) {
986
- let body;
987
- try { body = JSON.stringify(effectiveProviders); }
988
- catch { body = String(Date.now()); } // unserializable → force a fresh init
989
- return `${provider}\u0000${body}`;
990
- }
991
- function gateOnPrior(prior) {
992
- const settled = Promise.resolve(prior).catch(() => {});
993
- if (!(PROVIDER_CHAIN_GATE_MS > 0)) return settled;
994
- return new Promise((resolve) => {
995
- let done = false;
996
- const finish = () => { if (!done) { done = true; resolve(); } };
997
- const timer = setTimeout(finish, PROVIDER_CHAIN_GATE_MS);
998
- timer.unref?.();
999
- settled.then(() => { clearTimeout(timer); finish(); }, () => { clearTimeout(timer); finish(); });
1000
- });
1001
- }
1002
- function ensureProvider(config, provider) {
1003
- const effective = effectiveProviderConfig(config, provider);
1004
- const sigKey = providerInitSignature(provider, effective);
1005
- const registered = () => providerRegistered(provider);
1006
- const s = providerStateFor(provider);
1007
- // Completed-skip: this exact effective config is already live for this
1008
- // provider. A config change flips sigKey so we fall through; a torn-down
1009
- // provider (no longer registered) also does.
1010
- if (s.completedSig === sigKey && registered()) return Promise.resolve();
1011
- // Identical-sig collapse: a request with the SAME sigKey is already in
1012
- // flight — share its caller promise.
1013
- const pending = _providerInitPending.get(provider);
1014
- if (pending && pending.sigKey === sigKey) return pending.promise;
1015
- // New generation. Repoint the rolling `ready` deferred to THIS gen and make
1016
- // the previous gen's deferred ADOPT the new one, so any superseded caller
1017
- // awaiting an older deferred transitively waits for the newest init (d).
1018
- const gen = ++s.latestGen;
1019
- s.latestSig = sigKey;
1020
- const prevReady = s.ready;
1021
- let resolveReady;
1022
- const readyPromise = new Promise((r) => { resolveReady = r; });
1023
- s.ready = { gen, promise: readyPromise, resolve: resolveReady };
1024
- if (prevReady && prevReady.gen < gen) {
1025
- try { prevReady.resolve(readyPromise); } catch { /* already settled */ }
1026
- }
1027
- // Serialize the ACTUAL init behind the prior chain link (gated so a hung
1028
- // prior cannot wedge the chain). A superseded gen's chain link settles
1029
- // quickly — it never awaits a later gen — so there is no deadlock.
1030
- const prior = s.chain;
1031
- const chainLink = gateOnPrior(prior).then(async () => {
1032
- if (s.latestGen !== gen) {
1033
- // Superseded before we ran: drop our (stale) init entirely (goal c).
1034
- // Our `ready` deferred already adopts the newer gen, so the caller below
1035
- // still waits for the latest init. Settle now to release the chain.
1036
- return;
1037
- }
1038
- try {
1039
- if (!(s.completedSig === sigKey && registered())) {
1040
- await reg.initProviders(effective);
1041
- s.completedSig = sigKey;
1042
- }
1043
- } finally {
1044
- // ALWAYS release this gen's waiters once we are the latest — even on a
1045
- // registry init failure. Adopting (superseded) callers chained onto this
1046
- // deferred would otherwise hang forever; instead they proceed and their
1047
- // own createSession()/prep-timeout surfaces the unprepared provider.
1048
- resolveReady();
1049
- }
1050
- });
1051
- // Next chain link waits on us (settled, never poisoned).
1052
- s.chain = chainLink.catch(() => {});
1053
- // The CALLER awaits the ready deferred (resolves only when the LATEST init
1054
- // for this provider completes), not just the chain link — so a superseded
1055
- // caller blocks until the newest config is live (goal d). chainLink is
1056
- // awaited first so a registry init error surfaces to this caller.
1057
- const callerPromise = chainLink.then(() => readyPromise).finally(() => {
1058
- const cur = _providerInitPending.get(provider);
1059
- if (cur && cur.promise === callerPromise) _providerInitPending.delete(provider);
1060
- });
1061
- _providerInitPending.set(provider, { sigKey, promise: callerPromise });
1062
- return callerPromise;
1063
- }
563
+ // Provider init de-dup lives in ./agent-tool/provider-init.mjs; the factory
564
+ // keeps its per-provider chain/ready state private per agent instance. The
565
+ // chain-gate defaults to the spawn-prep cap (see provider-init.mjs comments).
566
+ const { ensureProvider } = createProviderInit(reg, DEFAULT_SPAWN_PREP_TIMEOUT_MS);
1064
567
 
1065
568
  function resolvePreset(config, args) {
1066
569
  if (args.provider && args.model) {
@@ -1218,9 +721,23 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1218
721
 
1219
722
  function getJob(args, context = {}) {
1220
723
  const taskId = taskIdFromArgs(args);
1221
- if (!taskId) throw new Error('agent read/status: task_id is required');
1222
- const task = getBackgroundTask(taskId, { surface: 'agent', context });
1223
- if (!task) throw new Error(`agent read/status: task "${taskId}" not found`);
724
+ if (taskId) {
725
+ const task = getBackgroundTask(taskId, { surface: 'agent', context });
726
+ if (!task) throw new Error(`agent read/status: task "${taskId}" not found`);
727
+ return task;
728
+ }
729
+ // Fall back to tag/sessionId resolution, same precedence as close()
730
+ // (agent-tool.mjs close()): clean(args.tag || args.sessionId || ...).
731
+ const target = clean(args.tag || args.sessionId);
732
+ if (!target) throw new Error('agent read/status: task_id, tag, or sessionId is required');
733
+ const candidates = listBackgroundTasks({ surface: 'agent', context })
734
+ .filter(Boolean)
735
+ .filter((row) => row.tag === target || row.sessionId === target);
736
+ if (!candidates.length) throw new Error(`agent read/status: no task found for tag/sessionId "${target}"`);
737
+ // Prefer most recent when multiple tasks match the same tag.
738
+ candidates.sort((a, b) => new Date(b.startedAt || 0) - new Date(a.startedAt || 0));
739
+ const task = getBackgroundTask(candidates[0].task_id, { surface: 'agent', context });
740
+ if (!task) throw new Error(`agent read/status: task "${candidates[0].task_id}" not found`);
1224
741
  return task;
1225
742
  }
1226
743
 
@@ -1246,6 +763,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1246
763
  tag: meta.tag || null,
1247
764
  sessionId: meta.sessionId || null,
1248
765
  agent: meta.agent || null,
766
+ ...(meta.respawned === true ? { respawned: true, note: 'previous session reaped — fresh session, no prior context; re-supply anchors if needed' } : {}),
1249
767
  preset: meta.preset || null,
1250
768
  provider: meta.provider || null,
1251
769
  model: meta.model || null,
@@ -1323,102 +841,9 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1323
841
  removeWorkerRow({ tag: prepared.tag, sessionId: prepared.session.id });
1324
842
  }
1325
843
 
1326
- function enqueueCompletionMessage(sessionId, text, meta = {}) {
1327
- const target = clean(sessionId);
1328
- if (!target || typeof mgr.enqueuePendingMessage !== 'function') return false;
1329
- try {
1330
- const visible = modelVisibleToolCompletionMessage(text, meta);
1331
- return Boolean(visible && mgr.enqueuePendingMessage(target, visible) > 0);
1332
- } catch {
1333
- return false;
1334
- }
1335
- }
1336
-
1337
- // Wire the canonical completion fallback to this agent surface's owner-session
1338
- // enqueue so notifyTaskCompletion can deliver via callerSessionId when no
1339
- // notifyFn is present or it declines. Registered once per agent (the closure
1340
- // captures mgr); signatures align: (callerSessionId, message, meta).
1341
- setBackgroundTaskEnqueueFallback((sessionId, text, meta) => enqueueCompletionMessage(sessionId, text, meta));
1342
-
1343
- function workerNotifyFn(workerSessionId, notifyContext = {}) {
1344
- const workerId = clean(workerSessionId);
1345
- const ownerSessionId = clean(notifyContext?.callerSessionId || notifyContext?.sessionId);
1346
- const upstream = typeof notifyContext?.notifyFn === 'function' ? notifyContext.notifyFn : null;
1347
- return (text, meta = {}) => {
1348
- let ownerDelivered = false;
1349
- if (upstream) {
1350
- try {
1351
- const result = upstream(text, meta);
1352
- ownerDelivered = result !== false;
1353
- if (ownerDelivered) Promise.resolve(result).catch(() => {});
1354
- } catch {
1355
- ownerDelivered = false;
1356
- }
1357
- }
1358
- if (!ownerDelivered && ownerSessionId) {
1359
- ownerDelivered = enqueueCompletionMessage(ownerSessionId, text, meta);
1360
- }
1361
- const workerDelivered = workerId && workerId !== ownerSessionId
1362
- ? enqueueCompletionMessage(workerId, text, meta)
1363
- : ownerDelivered;
1364
- return ownerSessionId ? ownerDelivered : workerDelivered;
1365
- };
1366
- }
1367
-
1368
- function notifyOwnerAgentCompletionEarly(job, resultValue, notifyContext = {}) {
1369
- if (!job || job._earlyCompletionNotified === true) return false;
1370
- const ownerSessionId = clean(notifyContext?.callerSessionId || notifyContext?.sessionId);
1371
- const upstream = typeof notifyContext?.notifyFn === 'function' ? notifyContext.notifyFn : null;
1372
- const finishedAt = new Date().toISOString();
1373
- // An abnormal-empty finish carries an `error` — the early preview must NOT
1374
- // present it as a benign `completed` card, or the Lead sees success before
1375
- // the later `failed` reconcile lands. Mirror the terminal status/instruction.
1376
- const earlyStatus = resultValue && resultValue.error ? 'failed' : 'completed';
1377
- const snapshot = {
1378
- ...job,
1379
- status: earlyStatus,
1380
- finishedAt,
1381
- finishedAtMs: Date.now(),
1382
- result: resultValue,
1383
- resultType: job.resultType || 'agent_task_result',
1384
- meta: sanitizeTaskMeta(job.meta || {}),
1385
- ...(resultValue && resultValue.error ? { error: resultValue.error } : {}),
1386
- };
1387
- // An early notification is only a header-only *preview*: it fires before
1388
- // the worker's session is persisted to signal the running→completed
1389
- // transition. It deliberately carries NO result body — the canonical
1390
- // notifyTaskCompletion delivers the body exactly once via the
1391
- // reconcile/finally path, so omitting it here keeps notifications
1392
- // exact-once with no duplicate body.
1393
- const text = renderBackgroundTask(snapshot, { includeResult: false });
1394
- const meta = {
1395
- type: snapshot.resultType,
1396
- execution_surface: 'agent',
1397
- execution_id: job.taskId || null,
1398
- status: earlyStatus,
1399
- instruction: `The async agent task ${job.taskId || ''} has finished (${earlyStatus}) - review this result in your next step.`,
1400
- ...(ownerSessionId ? { caller_session_id: ownerSessionId } : {}),
1401
- };
1402
- let delivered = false;
1403
- if (upstream) {
1404
- try {
1405
- const result = upstream(text, meta);
1406
- delivered = result !== false;
1407
- if (delivered) Promise.resolve(result).catch(() => {});
1408
- } catch {
1409
- delivered = false;
1410
- }
1411
- }
1412
- if (!delivered && ownerSessionId) {
1413
- delivered = enqueueCompletionMessage(ownerSessionId, text, meta);
1414
- }
1415
- if (delivered) {
1416
- // Mark only that a header-only preview fired. The canonical
1417
- // notifyTaskCompletion still owns the single body-carrying notification.
1418
- job._earlyCompletionNotified = true;
1419
- }
1420
- return delivered;
1421
- }
844
+ // Owner/worker completion notification lives in ./agent-tool/notify.mjs; the
845
+ // factory captures mgr and registers the canonical completion fallback.
846
+ const { workerNotifyFn, notifyOwnerAgentCompletionEarly } = createNotify(mgr);
1422
847
 
1423
848
  function startJob(type, meta, run, notifyContext = null) {
1424
849
  const clientHostPid = terminalPidForContext(notifyContext);
@@ -1545,7 +970,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1545
970
  const config = cfgMod.loadConfig();
1546
971
  const agent = normalizeAgentName(args.agent);
1547
972
  if (!agent) throw new Error('agent spawn: agent is required');
1548
- const agentPermission = readAgentFrontmatterPermission(agent, dataDir);
973
+ const agentPermission = readAgentFrontmatterPermission(agent, dataDir, STANDALONE_SOURCE_ROOT);
1549
974
  const agentPerm = normalizeAgentPermission(agentPermission) || null;
1550
975
  const { presetName, preset } = resolvePreset(config, args);
1551
976
  await ensureProvider(config, preset.provider);
@@ -1619,6 +1044,8 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1619
1044
  const { args, tag, session, agent, preset, presetName, workerCwd, prompt, watchdogPolicy } = prepared;
1620
1045
  const watchdog = startProgressIdleWatchdog(session.id, watchdogPolicy);
1621
1046
  let finalStatus = 'idle';
1047
+ // SubagentStart: a worker session is about to run its first turn.
1048
+ emitSubagentEvent('start', agent, { session_id: session.id, tag });
1622
1049
  upsertWorkerSessionDeferred(session, tag, {
1623
1050
  agent,
1624
1051
  preset: presetKey(preset) || presetName,
@@ -1710,7 +1137,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1710
1137
  try {
1711
1138
  reconcileBackgroundTask(job.taskId, {
1712
1139
  status: 'failed',
1713
- error: presentErrorText(error, { surface: 'agent' }),
1140
+ error,
1714
1141
  terminalReason: 'agent-stream-stalled',
1715
1142
  });
1716
1143
  } catch {}
@@ -1747,6 +1174,8 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1747
1174
  } catch {}
1748
1175
  }
1749
1176
  scheduleReap(session.id);
1177
+ // SubagentStop: worker finished (terminal), regardless of outcome.
1178
+ emitSubagentEvent('stop', agent, { session_id: session.id, tag, status: finalStatus });
1750
1179
  }
1751
1180
  }
1752
1181
 
@@ -1836,7 +1265,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1836
1265
  try {
1837
1266
  reconcileBackgroundTask(job.taskId, {
1838
1267
  status: 'failed',
1839
- error: presentErrorText(error, { surface: 'agent' }),
1268
+ error,
1840
1269
  terminalReason: 'agent-stream-stalled',
1841
1270
  });
1842
1271
  } catch {}
@@ -1928,6 +1357,16 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1928
1357
  if (tag) forgetTag(tag);
1929
1358
  removeWorkerRow({ tag, sessionId });
1930
1359
  clearAgentStatuslineRoute(sessionId);
1360
+ // Cancel any running background task bound to this session BEFORE closing
1361
+ // the session. Otherwise closeSession rejects the in-flight runSpawn with
1362
+ // "Session closed: closeSession" and the catch path reconciles the task as
1363
+ // `failed` — a user-initiated close must surface as `cancelled` instead.
1364
+ // (The explicit task_id path below stays as a no-op fallback: cancel is
1365
+ // idempotent once terminal.)
1366
+ for (const row of listBackgroundTasks({ surface: 'agent', context: scopedContext })) {
1367
+ if (row.sessionId !== sessionId && row.tag !== target) continue;
1368
+ cancelBackgroundTask(row.task_id, 'cancelled by agent close');
1369
+ }
1931
1370
  const ok = mgr.closeSession(sessionId, 'cli-agent-close');
1932
1371
  if (task?.taskId) cancelBackgroundTask(task.taskId, 'cancelled by agent close');
1933
1372
  return { closed: ok, tag, sessionId, task_id: task?.taskId || null };
@@ -1996,8 +1435,47 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1996
1435
  if (type === 'cancel') return renderResult(close(args, scopedContext));
1997
1436
  if (type === 'close') return renderResult(close(args, scopedContext));
1998
1437
  if (type === 'send') {
1999
- const prepared = await prepareSend(args, scopedContext);
2000
- return dispatchToExistingSession(prepared, notifyContext);
1438
+ try {
1439
+ const prepared = await prepareSend(args, scopedContext);
1440
+ return dispatchToExistingSession(prepared, notifyContext);
1441
+ } catch (err) {
1442
+ // Reaped/dead-tag fallback: with the 5m terminal-reap window a
1443
+ // same-scope follow-up often lands after the session is gone.
1444
+ // Instead of bouncing an error back to Lead (who would just
1445
+ // re-issue the same content as a spawn), respawn a FRESH session
1446
+ // under the same tag with the message as its brief. `respawned:
1447
+ // true` in the result tells Lead the worker has no prior session
1448
+ // context — re-supply anchors on the next send if needed.
1449
+ // Only tag-addressed sends fall back; explicit sessionId sends
1450
+ // keep erroring (caller pinned a specific session on purpose).
1451
+ const fallbackTag = clean(args.tag);
1452
+ const isDeadTarget = /not found|is closed/i.test(String(err?.message || ''));
1453
+ if (!fallbackTag || fallbackTag.startsWith('sess_') || !isDeadTarget) throw err;
1454
+ const prompt = clean(args.message || args.prompt);
1455
+ if (!prompt) throw err;
1456
+ // Inherit the dead session's agent/cwd from the tag registry so the
1457
+ // auto-respawn doesn't die on "agent spawn: agent is required" when
1458
+ // Lead's send (correctly) omitted them. Read BEFORE forgetTag wipes
1459
+ // the registry entries.
1460
+ const inheritedAgent = clean(args.agent) || clean(tagAgents.get(fallbackTag));
1461
+ const inheritedCwd = clean(args.cwd) || clean(tagCwds.get(fallbackTag));
1462
+ if (!inheritedAgent) throw err;
1463
+ // Clear the terminal trace so the fresh spawn isn't rejected by
1464
+ // the lingering-trace guard, then run the normal deferred spawn.
1465
+ try { forgetTag(fallbackTag); } catch {}
1466
+ try { removeWorkerRow({ tag: fallbackTag }); } catch {}
1467
+ const spawnArgs = {
1468
+ ...args,
1469
+ type: 'spawn',
1470
+ tag: fallbackTag,
1471
+ prompt,
1472
+ message: undefined,
1473
+ agent: inheritedAgent,
1474
+ ...(inheritedCwd ? { cwd: inheritedCwd } : {}),
1475
+ };
1476
+ const job = startDeferredSpawnJob(spawnArgs, callerCwd, context, notifyContext, { respawned: true });
1477
+ return renderResult(renderJob(job, false));
1478
+ }
2001
1479
  }
2002
1480
  if (type === 'spawn') {
2003
1481
  // Explicit-tag spawn priority (auto nextTag always creates a fresh session):
@@ -2033,7 +1511,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
2033
1511
  }
2034
1512
  throw new Error(`agent: unknown type "${type}"`);
2035
1513
  } catch (err) {
2036
- return `Error: ${presentErrorText(err, { surface: 'agent' })}`;
1514
+ return errorLine(err, { surface: 'agent' });
2037
1515
  }
2038
1516
  }
2039
1517