mixdog 0.9.1 → 0.9.3

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 (301) hide show
  1. package/package.json +9 -1
  2. package/scripts/_bench-cwc.json +20 -0
  3. package/scripts/agent-loop-policy-test.mjs +37 -0
  4. package/scripts/agent-parallel-smoke.mjs +54 -10
  5. package/scripts/anthropic-maxtokens-test.mjs +119 -0
  6. package/scripts/background-task-meta-smoke.mjs +1 -1
  7. package/scripts/bench-run.mjs +262 -0
  8. package/scripts/build-tui.mjs +13 -1
  9. package/scripts/compact-smoke.mjs +12 -0
  10. package/scripts/compact-trigger-migration-smoke.mjs +67 -1
  11. package/scripts/explore-bench.mjs +124 -0
  12. package/scripts/hook-bus-test.mjs +191 -0
  13. package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
  14. package/scripts/internal-comms-bench.mjs +727 -0
  15. package/scripts/internal-comms-smoke.mjs +75 -0
  16. package/scripts/lead-workflow-smoke.mjs +4 -4
  17. package/scripts/live-worker-smoke.mjs +9 -9
  18. package/scripts/output-style-bench.mjs +285 -0
  19. package/scripts/output-style-smoke.mjs +13 -10
  20. package/scripts/patch-replay.mjs +90 -0
  21. package/scripts/path-suffix-test.mjs +57 -0
  22. package/scripts/provider-stream-stall-test.mjs +276 -0
  23. package/scripts/provider-toolcall-test.mjs +599 -1
  24. package/scripts/recall-bench.mjs +207 -0
  25. package/scripts/routing-corpus.mjs +281 -0
  26. package/scripts/session-bench.mjs +1526 -0
  27. package/scripts/session-diag.mjs +595 -0
  28. package/scripts/task-bench.mjs +207 -0
  29. package/scripts/tool-failures.mjs +6 -6
  30. package/scripts/tool-smoke.mjs +310 -67
  31. package/scripts/toolcall-args-test.mjs +81 -0
  32. package/src/agents/debugger/AGENT.md +4 -4
  33. package/src/agents/heavy-worker/AGENT.md +20 -9
  34. package/src/agents/reviewer/AGENT.md +4 -4
  35. package/src/agents/worker/AGENT.md +17 -9
  36. package/src/app.mjs +10 -6
  37. package/src/defaults/{hidden-roles.json → agents.json} +7 -7
  38. package/src/examples/schedules/SCHEDULE.example.md +32 -0
  39. package/src/examples/webhooks/WEBHOOK.example.md +40 -0
  40. package/src/headless-role.mjs +14 -14
  41. package/src/help.mjs +1 -0
  42. package/src/lib/rules-builder.cjs +32 -54
  43. package/src/mixdog-session-runtime.mjs +1040 -2036
  44. package/src/output-styles/default.md +12 -7
  45. package/src/output-styles/minimal.md +25 -0
  46. package/src/output-styles/oneline.md +21 -0
  47. package/src/output-styles/simple.md +10 -9
  48. package/src/repl.mjs +17 -7
  49. package/src/rules/agent/00-common.md +7 -5
  50. package/src/rules/agent/30-explorer.md +8 -12
  51. package/src/rules/lead/01-general.md +3 -1
  52. package/src/rules/lead/lead-tool.md +13 -2
  53. package/src/rules/shared/01-tool.md +23 -12
  54. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
  55. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
  56. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
  57. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
  58. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
  59. package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
  60. package/src/runtime/agent/orchestrator/config.mjs +3 -0
  61. package/src/runtime/agent/orchestrator/context/collect.mjs +182 -67
  62. package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
  63. package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
  64. package/src/runtime/agent/orchestrator/mcp/client.mjs +100 -18
  65. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
  66. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
  67. package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
  68. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
  69. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +313 -84
  70. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +104 -38
  71. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +28 -33
  72. package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
  73. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
  74. package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
  75. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
  76. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
  77. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +227 -31
  78. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +83 -6
  79. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +229 -115
  80. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +213 -33
  81. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
  82. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +49 -17
  83. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
  84. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +22 -17
  85. package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
  86. package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
  87. package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
  88. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +262 -0
  89. package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
  90. package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
  91. package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
  92. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +49 -0
  93. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
  94. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
  95. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
  96. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
  97. package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
  98. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
  99. package/src/runtime/agent/orchestrator/session/loop.mjs +513 -1000
  100. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +227 -0
  101. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
  102. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +137 -0
  103. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
  104. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
  105. package/src/runtime/agent/orchestrator/session/manager.mjs +232 -1152
  106. package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
  107. package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
  108. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
  109. package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
  110. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -24
  111. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
  112. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
  113. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
  114. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +162 -0
  115. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
  116. package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
  117. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
  118. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
  119. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
  120. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
  121. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
  122. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +14 -5
  123. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
  124. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
  125. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +23 -2
  126. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +50 -39
  127. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
  128. package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
  129. package/src/runtime/agent/orchestrator/tools/builtin.mjs +70 -1
  130. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
  131. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
  132. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
  133. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +497 -0
  134. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
  135. package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
  136. package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
  137. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
  138. package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
  139. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1192 -0
  140. package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
  141. package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
  142. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
  143. package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
  144. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
  145. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +39 -4189
  146. package/src/runtime/agent/orchestrator/tools/patch.mjs +119 -5
  147. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -2
  148. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -2
  149. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
  150. package/src/runtime/channels/backends/discord.mjs +99 -9
  151. package/src/runtime/channels/backends/telegram.mjs +501 -0
  152. package/src/runtime/channels/index.mjs +437 -1439
  153. package/src/runtime/channels/lib/boot-profile.mjs +23 -0
  154. package/src/runtime/channels/lib/config.mjs +54 -2
  155. package/src/runtime/channels/lib/crash-log.mjs +106 -0
  156. package/src/runtime/channels/lib/format.mjs +4 -2
  157. package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
  158. package/src/runtime/channels/lib/output-forwarder.mjs +88 -67
  159. package/src/runtime/channels/lib/runtime-paths.mjs +29 -0
  160. package/src/runtime/channels/lib/scheduler.mjs +1 -1
  161. package/src/runtime/channels/lib/telegram-format.mjs +280 -0
  162. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  163. package/src/runtime/channels/lib/transcript-discovery.mjs +19 -1
  164. package/src/runtime/channels/lib/webhook.mjs +59 -31
  165. package/src/runtime/channels/lib/whisper-language.mjs +42 -0
  166. package/src/runtime/channels/tool-defs.mjs +1 -1
  167. package/src/runtime/memory/index.mjs +465 -345
  168. package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
  169. package/src/runtime/memory/lib/core-memory-store.mjs +352 -2
  170. package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
  171. package/src/runtime/memory/lib/http-wire.mjs +57 -0
  172. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
  173. package/src/runtime/memory/lib/memory-cycle2.mjs +65 -9
  174. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
  175. package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
  176. package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
  177. package/src/runtime/memory/lib/memory.mjs +121 -4
  178. package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
  179. package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
  180. package/src/runtime/memory/lib/recall-format.mjs +183 -0
  181. package/src/runtime/memory/lib/session-ingest.mjs +107 -0
  182. package/src/runtime/memory/lib/trace-store.mjs +69 -22
  183. package/src/runtime/memory/tool-defs.mjs +10 -7
  184. package/src/runtime/shared/abort-controller.mjs +1 -1
  185. package/src/runtime/shared/background-tasks.mjs +2 -3
  186. package/src/runtime/shared/buffered-appender.mjs +149 -0
  187. package/src/runtime/shared/channel-notification-routing.mjs +12 -0
  188. package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
  189. package/src/runtime/shared/config.mjs +9 -0
  190. package/src/runtime/shared/llm/http-agent.mjs +12 -5
  191. package/src/runtime/shared/schedules-store.mjs +21 -19
  192. package/src/runtime/shared/task-notification-envelope.mjs +98 -0
  193. package/src/runtime/shared/task-notification-envelope.test.mjs +107 -0
  194. package/src/runtime/shared/tool-execution-contract.mjs +2 -2
  195. package/src/runtime/shared/tool-surface.mjs +98 -13
  196. package/src/runtime/shared/transcript-writer.mjs +156 -0
  197. package/src/runtime/shared/update-checker.mjs +214 -0
  198. package/src/session-runtime/config-helpers.mjs +209 -0
  199. package/src/session-runtime/effort.mjs +128 -0
  200. package/src/session-runtime/fs-utils.mjs +10 -0
  201. package/src/session-runtime/model-capabilities.mjs +130 -0
  202. package/src/session-runtime/output-styles.mjs +124 -0
  203. package/src/session-runtime/plugin-mcp.mjs +114 -0
  204. package/src/session-runtime/session-text.mjs +100 -0
  205. package/src/session-runtime/statusline-route.mjs +35 -0
  206. package/src/session-runtime/tool-catalog.mjs +720 -0
  207. package/src/session-runtime/workflow.mjs +358 -0
  208. package/src/standalone/agent-tool.mjs +302 -117
  209. package/src/standalone/channel-admin.mjs +133 -40
  210. package/src/standalone/channel-worker.mjs +10 -292
  211. package/src/standalone/explore-tool.mjs +12 -5
  212. package/src/standalone/hook-bus.mjs +165 -8
  213. package/src/standalone/memory-runtime-proxy.mjs +3 -1
  214. package/src/standalone/opencode-go-login.mjs +121 -0
  215. package/src/standalone/provider-admin.mjs +25 -3
  216. package/src/standalone/usage-dashboard.mjs +1 -1
  217. package/src/tui/App.jsx +2883 -778
  218. package/src/tui/components/ConfirmBar.jsx +47 -0
  219. package/src/tui/components/ContextPanel.jsx +5 -3
  220. package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
  221. package/src/tui/components/Markdown.jsx +22 -98
  222. package/src/tui/components/Message.jsx +14 -35
  223. package/src/tui/components/Picker.jsx +87 -12
  224. package/src/tui/components/PromptInput.jsx +355 -22
  225. package/src/tui/components/QueuedCommands.jsx +1 -1
  226. package/src/tui/components/SlashCommandPalette.jsx +8 -5
  227. package/src/tui/components/Spinner.jsx +7 -7
  228. package/src/tui/components/StatusLine.jsx +40 -21
  229. package/src/tui/components/TextEntryPanel.jsx +51 -7
  230. package/src/tui/components/ToolExecution.jsx +183 -101
  231. package/src/tui/components/TurnDone.jsx +4 -4
  232. package/src/tui/components/UsagePanel.jsx +1 -1
  233. package/src/tui/components/tool-output-format.mjs +161 -23
  234. package/src/tui/components/tool-output-format.test.mjs +87 -0
  235. package/src/tui/display-width.mjs +69 -0
  236. package/src/tui/display-width.test.mjs +35 -0
  237. package/src/tui/dist/index.mjs +6731 -2333
  238. package/src/tui/engine/agent-envelope.mjs +296 -0
  239. package/src/tui/engine/boot-profile.mjs +21 -0
  240. package/src/tui/engine/labels.mjs +67 -0
  241. package/src/tui/engine/notice-text.mjs +112 -0
  242. package/src/tui/engine/queue-helpers.mjs +161 -0
  243. package/src/tui/engine/session-stats.mjs +46 -0
  244. package/src/tui/engine/tool-call-fields.mjs +23 -0
  245. package/src/tui/engine/tool-result-text.mjs +126 -0
  246. package/src/tui/engine.mjs +569 -948
  247. package/src/tui/index.jsx +117 -7
  248. package/src/tui/input-editing.mjs +58 -8
  249. package/src/tui/input-editing.selection.test.mjs +75 -0
  250. package/src/tui/keyboard-protocol.mjs +42 -0
  251. package/src/tui/lib/voice-recorder.mjs +469 -0
  252. package/src/tui/markdown/format-token.mjs +128 -76
  253. package/src/tui/markdown/format-token.test.mjs +61 -19
  254. package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
  255. package/src/tui/markdown/render-ansi.test.mjs +1 -1
  256. package/src/tui/markdown/streaming-markdown.mjs +167 -0
  257. package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
  258. package/src/tui/markdown/table-layout.mjs +9 -9
  259. package/src/tui/paste-attachments.mjs +36 -9
  260. package/src/tui/paste-fix.test.mjs +119 -0
  261. package/src/tui/prompt-history-store.mjs +129 -0
  262. package/src/tui/prompt-history-store.test.mjs +52 -0
  263. package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
  264. package/src/tui/theme.mjs +41 -657
  265. package/src/tui/themes/base.mjs +86 -0
  266. package/src/tui/themes/basic.mjs +85 -0
  267. package/src/tui/themes/catppuccin.mjs +72 -0
  268. package/src/tui/themes/dracula.mjs +70 -0
  269. package/src/tui/themes/everforest.mjs +71 -0
  270. package/src/tui/themes/gruvbox.mjs +71 -0
  271. package/src/tui/themes/index.mjs +71 -0
  272. package/src/tui/themes/indigo.mjs +78 -0
  273. package/src/tui/themes/kanagawa.mjs +80 -0
  274. package/src/tui/themes/light.mjs +81 -0
  275. package/src/tui/themes/nord.mjs +72 -0
  276. package/src/tui/themes/onedark.mjs +16 -0
  277. package/src/tui/themes/rosepine.mjs +70 -0
  278. package/src/tui/themes/teal.mjs +80 -0
  279. package/src/tui/themes/tokyonight.mjs +79 -0
  280. package/src/tui/themes/utils.mjs +106 -0
  281. package/src/tui/themes/warm.mjs +79 -0
  282. package/src/tui/transcript-tool-failures.mjs +13 -2
  283. package/src/ui/markdown.mjs +1 -1
  284. package/src/ui/model-display.mjs +2 -2
  285. package/src/ui/statusline.mjs +75 -27
  286. package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
  287. package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
  288. package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
  289. package/src/workflows/default/WORKFLOW.md +46 -12
  290. package/src/workflows/sequential/WORKFLOW.md +51 -0
  291. package/src/workflows/solo/WORKFLOW.md +12 -1
  292. package/vendor/ink/build/display-width.js +62 -0
  293. package/vendor/ink/build/ink.js +100 -12
  294. package/vendor/ink/build/measure-text.js +4 -1
  295. package/vendor/ink/build/output.js +115 -9
  296. package/vendor/ink/build/render-node-to-output.js +4 -1
  297. package/vendor/ink/build/render.js +4 -0
  298. package/src/output-styles/extreme-simple.md +0 -20
  299. package/src/rules/lead/04-workflow.md +0 -51
  300. package/src/workflows/default/workflow.json +0 -13
  301. package/src/workflows/solo/workflow.json +0 -7
@@ -14,7 +14,7 @@ import {
14
14
  taskIdFromArgs,
15
15
  } from '../runtime/shared/background-tasks.mjs';
16
16
  import { modelVisibleToolCompletionMessage } from '../runtime/shared/tool-execution-contract.mjs';
17
- import { presentErrorText } from '../runtime/shared/err-text.mjs';
17
+ import { presentErrorText, errorLine } from '../runtime/shared/err-text.mjs';
18
18
  import { updateJsonAtomicSync } from '../runtime/shared/atomic-file.mjs';
19
19
  import {
20
20
  normalizeAgentPermission,
@@ -33,6 +33,7 @@ import {
33
33
  } from './agent-task-status.mjs';
34
34
  import { AGENT_OWNER } from '../runtime/agent/orchestrator/agent-owner.mjs';
35
35
  import { clearGatewaySessionRoute, writeGatewaySessionRoute } from '../vendor/statusline/src/gateway/session-routes.mjs';
36
+ import { isKnownProvider } from './provider-admin.mjs';
36
37
 
37
38
  const STANDALONE_SOURCE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
38
39
 
@@ -44,11 +45,16 @@ const DEFAULT_AGENT_PRESETS = Object.freeze({
44
45
  explore: 'sonnet-high',
45
46
  maintainer: 'haiku',
46
47
  worker: 'sonnet-high',
47
- 'heavy-worker': 'opus-high',
48
+ 'heavy-worker': 'sonnet-high',
48
49
  reviewer: 'opus-xhigh',
49
50
  debugger: 'opus-xhigh',
50
51
  });
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
+
52
58
  export const AGENT_TOOL = {
53
59
  name: 'agent',
54
60
  title: 'Agent',
@@ -60,17 +66,17 @@ export const AGENT_TOOL = {
60
66
  openWorldHint: true,
61
67
  agentHidden: true,
62
68
  },
63
- description: 'Delegate scoped work. Agent handoffs always start background tasks and return task IDs immediately. Spawn independent agents in parallel with distinct tags; keep Lead work moving. Wait for the completion notification before dependent work. Do not call status/read after spawn; status/read are manual recovery only.',
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.',
64
70
  inputSchema: {
65
71
  type: 'object',
66
72
  properties: {
67
73
  type: { type: 'string', enum: ['spawn', 'send', 'list', 'close', 'cancel', 'status', 'read', 'cleanup'], description: 'Action. Default spawn.' },
68
74
  task_id: { type: 'string', description: 'Manual recovery task ID.' },
69
75
  agent: { type: 'string', description: 'Workflow agent id.' },
70
- tag: { type: 'string', description: 'Stable distinct handle; choose a role-index tag by role, e.g. worker01, heavy-worker01, reviewer01.' },
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.' },
71
77
  sessionId: { type: 'string', description: 'Raw sess_ id.' },
72
78
  prompt: { type: 'string', description: 'Scoped task brief.' },
73
- message: { type: 'string', description: 'Follow-up or brief.' },
79
+ message: { type: 'string', description: 'Follow-up for send/reuse, or brief.' },
74
80
  file: { type: 'string', description: 'Prompt file.' },
75
81
  cwd: { type: 'string', description: 'Working directory.' },
76
82
  context: { type: 'string', description: 'Extra agent context.' },
@@ -80,6 +86,45 @@ export const AGENT_TOOL = {
80
86
  };
81
87
 
82
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
+
83
128
  function envTimeoutMs(name, fallback) {
84
129
  const raw = process.env[name];
85
130
  if (raw === undefined || raw === '') return fallback;
@@ -90,7 +135,11 @@ function envTimeoutMs(name, fallback) {
90
135
  // Grace window during which a terminated/idle worker row is kept around so the
91
136
  // same terminal can re-use (or cleanly re-spawn) the same tag. Cached as a
92
137
  // constant like the other timeouts; override with MIXDOG_AGENT_TERMINAL_REAP_MS.
93
- const TERMINAL_REAP_MS = envTimeoutMs('MIXDOG_AGENT_TERMINAL_REAP_MS', 60 * 60_000);
138
+ // 5m (was 1h): aligned with the BP4 message-tail cache TTL — past 5m the
139
+ // session's prompt cache is cold anyway, so same-tag reuse pays a full prefix
140
+ // rewrite; a fresh respawn (send dead-tag fallback) starts smaller and cheaper.
141
+ // Trace replay (2026-07): agent tail cost 1h 1.77M -> 5m 1.11M (-37%).
142
+ const TERMINAL_REAP_MS = envTimeoutMs('MIXDOG_AGENT_TERMINAL_REAP_MS', 5 * 60_000);
94
143
  // Independent hard cap for the spawn *prep* phase (ensureProvider /
95
144
  // prepareAgentSession / catalog+rules load). Kept separate from the
96
145
  // first-response watchdog so prep cannot hang a whole fanout before the model
@@ -126,21 +175,21 @@ function normalizeAgentName(value) {
126
175
  return id;
127
176
  }
128
177
 
129
- function readAgentFrontmatterPermission(role, dataDir) {
130
- const cleanRole = clean(role);
131
- if (!cleanRole) return null;
132
- const cacheKey = `${dataDir || ''}\u0000${cleanRole}`;
178
+ function readAgentFrontmatterPermission(agent, dataDir) {
179
+ const cleanAgent = clean(agent);
180
+ if (!cleanAgent) return null;
181
+ const cacheKey = `${dataDir || ''}\u0000${cleanAgent}`;
133
182
  const cached = _frontmatterPermCache.get(cacheKey);
134
183
  if (cached && Date.now() - cached.atMs < FRONTMATTER_PERM_CACHE_TTL_MS) {
135
184
  return cached.value;
136
185
  }
137
186
  const candidates = [];
138
187
  if (dataDir) {
139
- candidates.push(join(dataDir, 'agents', cleanRole, 'AGENT.md'));
140
- candidates.push(join(dataDir, 'agents', `${cleanRole}.md`));
188
+ candidates.push(join(dataDir, 'agents', cleanAgent, 'AGENT.md'));
189
+ candidates.push(join(dataDir, 'agents', `${cleanAgent}.md`));
141
190
  }
142
- candidates.push(join(STANDALONE_SOURCE_ROOT, 'agents', cleanRole, 'AGENT.md'));
143
- candidates.push(join(STANDALONE_SOURCE_ROOT, 'agents', `${cleanRole}.md`));
191
+ candidates.push(join(STANDALONE_SOURCE_ROOT, 'agents', cleanAgent, 'AGENT.md'));
192
+ candidates.push(join(STANDALONE_SOURCE_ROOT, 'agents', `${cleanAgent}.md`));
144
193
  let resolved = null;
145
194
  for (const file of candidates) {
146
195
  if (!existsSync(file)) continue;
@@ -248,8 +297,8 @@ function synthesizePreset(config, key) {
248
297
  };
249
298
  }
250
299
 
251
- function normalizeAgentRoute(routeLike) {
252
- const provider = clean(routeLike?.provider);
300
+ function normalizeAgentRoute(routeLike, fallbackProvider = '') {
301
+ const provider = clean(routeLike?.provider) || clean(fallbackProvider);
253
302
  const model = clean(routeLike?.model);
254
303
  if (!provider || !model) return null;
255
304
  return {
@@ -260,8 +309,8 @@ function normalizeAgentRoute(routeLike) {
260
309
  };
261
310
  }
262
311
 
263
- function agentPresetName(role) {
264
- return `AGENT ${String(role || '').toUpperCase()}`;
312
+ function agentPresetName(agent) {
313
+ return `AGENT ${String(agent || '').toUpperCase()}`;
265
314
  }
266
315
 
267
316
  async function resolvePrompt(args, cwd) {
@@ -334,7 +383,7 @@ function renderResult(value) {
334
383
  for (const worker of workers) {
335
384
  const tokens = worker.windowTokens ? ` ctx=${worker.windowTokens}${worker.windowCap ? `/${worker.windowCap}` : ''}` : '';
336
385
  const terminal = worker.clientHostPid ? ` term=${worker.clientHostPid}` : '';
337
- const base = `- ${worker.tag} ${worker.role || 'agent'} ${worker.status || 'idle'}/${worker.worker_stage || worker.stage || 'idle'} ${worker.provider}/${worker.model}${terminal}${tokens}`;
386
+ const base = `- ${worker.tag} ${worker.agent || 'agent'} ${worker.status || 'idle'}/${worker.worker_stage || worker.stage || 'idle'} ${worker.provider}/${worker.model}${terminal}${tokens}`;
338
387
  lines.push(appendAgentProgressKv(base, worker));
339
388
  }
340
389
  const jobs = Array.isArray(value.jobs) ? value.jobs : [];
@@ -342,7 +391,7 @@ function renderResult(value) {
342
391
  for (const job of jobs) {
343
392
  const target = job.tag || job.sessionId || '-';
344
393
  const terminal = job.clientHostPid ? ` term=${job.clientHostPid}` : '';
345
- const base = `- ${job.task_id} ${job.type} ${job.status} target=${target}${terminal}${job.error ? ` error=${presentErrorText(job.error, { surface: 'agent' })}` : ''}`;
394
+ const base = `- ${job.task_id} ${job.type} ${job.status} target=${target}${terminal}${job.error ? ` error=${job.error}` : ''}`;
346
395
  lines.push(appendAgentProgressKv(base, job));
347
396
  }
348
397
  if (workers.length === 0 && jobs.length === 0) lines.push('(no agents or tasks)');
@@ -355,7 +404,7 @@ function renderResult(value) {
355
404
  if (value.type) lines.push(`type: ${value.type}`);
356
405
  if (value.reused) lines.push('reused: true');
357
406
  if (value.tag || value.sessionId) lines.push(`target: ${value.tag || '-'} ${value.sessionId || ''}`.trim());
358
- if (value.role) lines.push(`agent: ${value.role}`);
407
+ if (value.agent) lines.push(`agent: ${value.agent}`);
359
408
  if (value.provider && value.model) lines.push(`model: ${value.provider}/${value.model}`);
360
409
  if (value.effort) lines.push(`effort: ${value.effort}`);
361
410
  if (value.fast === true || value.fast === false) lines.push(`fast: ${value.fast ? 'on' : 'off'}`);
@@ -377,7 +426,7 @@ function renderResult(value) {
377
426
  const elapsed = elapsedFromStamps(value.startedAt, value.finishedAt, value.status);
378
427
  if (elapsed) lines.push(`elapsed: ${elapsed}`);
379
428
  }
380
- if (value.error) lines.push(`error: ${presentErrorText(value.error, { surface: 'agent' })}`);
429
+ if (value.error) lines.push(`error: ${value.error}`);
381
430
  if (value.status === 'running') lines.push('notification: completion will be delivered to the owner session; use read/status only for manual recovery.');
382
431
  if (value.result !== undefined) {
383
432
  const result = value.result;
@@ -393,7 +442,7 @@ function renderResult(value) {
393
442
  'agent message queued',
394
443
  value.reused ? 'reused: true' : null,
395
444
  `target: ${value.tag || '-'} ${value.sessionId || ''}`.trim(),
396
- value.role ? `agent: ${value.role}` : null,
445
+ value.agent ? `agent: ${value.agent}` : null,
397
446
  `queueDepth: ${value.queueDepth ?? 1}`,
398
447
  ].filter(Boolean).join('\n');
399
448
  }
@@ -412,7 +461,7 @@ function renderResult(value) {
412
461
  const header = [
413
462
  value.respawned ? 'agent respawned' : 'agent result',
414
463
  value.tag ? `tag=${value.tag}` : null,
415
- value.role ? `agent=${value.role}` : null,
464
+ value.agent ? `agent=${value.agent}` : null,
416
465
  value.provider && value.model ? `${value.provider}/${value.model}` : null,
417
466
  ].filter(Boolean).join(' ');
418
467
  return `${header}\n${stripFinalAnswerWrapper(value.content)}`;
@@ -421,9 +470,15 @@ function renderResult(value) {
421
470
  return JSON.stringify(value, null, 2);
422
471
  }
423
472
 
424
- export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultCwd }) {
473
+ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultCwd, onSubagentEvent }) {
474
+ // Optional bridge to the standard hook bus for SubagentStart / SubagentStop.
475
+ // Best-effort: a hook error must never affect worker spawn/finish.
476
+ function emitSubagentEvent(phase, agent, extra = {}) {
477
+ if (typeof onSubagentEvent !== 'function') return;
478
+ try { onSubagentEvent(phase, { agent_type: agent || null, ...extra }); } catch { /* best-effort */ }
479
+ }
425
480
  const tags = new Map();
426
- const tagRoles = new Map();
481
+ const tagAgents = new Map();
427
482
  const tagCwds = new Map();
428
483
  const reapTimers = new Map();
429
484
  const workerIndexMutators = [];
@@ -463,7 +518,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
463
518
  .map((row) => ({
464
519
  tag: clean(row.tag),
465
520
  sessionId: clean(row.sessionId),
466
- role: clean(row.role) || null,
521
+ agent: clean(row.agent) || null,
467
522
  provider: clean(row.provider) || null,
468
523
  model: clean(row.model) || null,
469
524
  preset: clean(row.preset) || null,
@@ -562,7 +617,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
562
617
  if (!key) return;
563
618
  const prev = byKey.get(key) || {};
564
619
  const merged = { ...prev, ...normalized };
565
- for (const field of ['role', 'provider', 'model', 'preset', 'effort', 'fast', 'clientHostPid', 'cwd', 'task_id', 'permission', 'toolPermission']) {
620
+ for (const field of ['agent', 'provider', 'model', 'preset', 'effort', 'fast', 'clientHostPid', 'cwd', 'task_id', 'permission', 'toolPermission']) {
566
621
  if ((merged[field] === null || merged[field] === '') && prev[field] != null && prev[field] !== '') {
567
622
  merged[field] = prev[field];
568
623
  }
@@ -610,7 +665,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
610
665
  return {
611
666
  tag,
612
667
  sessionId,
613
- role: clean(extra.role) || clean(session?.role) || null,
668
+ agent: clean(extra.agent) || clean(session?.agent) || null,
614
669
  provider: clean(extra.provider) || clean(session?.provider) || null,
615
670
  model: clean(extra.model) || clean(session?.model) || null,
616
671
  preset: clean(extra.preset) || clean(session?.presetName) || null,
@@ -637,7 +692,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
637
692
  return {
638
693
  id: row.sessionId,
639
694
  agentTag: row.tag,
640
- role: row.role || null,
695
+ agent: row.agent || null,
641
696
  provider: row.provider || null,
642
697
  model: row.model || null,
643
698
  presetName: row.preset || null,
@@ -661,7 +716,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
661
716
  const normalized = normalizeWorkerRows({ workers: [row] })[0];
662
717
  if (!normalized) return false;
663
718
  tags.set(normalized.tag, normalized.sessionId);
664
- if (normalized.role) tagRoles.set(normalized.tag, normalized.role);
719
+ if (normalized.agent) tagAgents.set(normalized.tag, normalized.agent);
665
720
  if (normalized.cwd) tagCwds.set(normalized.tag, normalized.cwd);
666
721
  if (defer) {
667
722
  return queueWorkerIndexMutation((byKey) => applyWorkerRowUpsert(byKey, normalized));
@@ -698,7 +753,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
698
753
  for (const row of rows) {
699
754
  if (!row.tag || !row.sessionId) continue;
700
755
  tags.set(row.tag, row.sessionId);
701
- if (row.role) tagRoles.set(row.tag, row.role);
756
+ if (row.agent) tagAgents.set(row.tag, row.agent);
702
757
  if (row.cwd) tagCwds.set(row.tag, row.cwd);
703
758
  }
704
759
  return rows;
@@ -790,14 +845,14 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
790
845
  return rows;
791
846
  }
792
847
 
793
- function nextTag(role, context = {}) {
848
+ function nextTag(agent, context = {}) {
794
849
  refreshTagsFromSessions({ context });
795
- // Auto tags are role + a per-role local index with NO hyphen
796
- // ("worker3", "heavy-worker7", or "agent1" when the role is unset). The
797
- // index is the max existing `^role(\d+)$` + 1, escaping the role so a
798
- // hyphenated role ("heavy-worker") is matched literally. Keep incrementing
850
+ // Auto tags are agent + a per-agent local index with NO hyphen
851
+ // ("worker3", "heavy-worker7", or "agent1" when the agent is unset). The
852
+ // index is the max existing `^agent(\d+)$` + 1, escaping the agent so a
853
+ // hyphenated agent ("heavy-worker") is matched literally. Keep incrementing
799
854
  // on any live collision.
800
- const base = clean(role) || 'agent';
855
+ const base = clean(agent) || 'agent';
801
856
  const escaped = base.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
802
857
  const re = new RegExp(`^${escaped}(\\d+)$`);
803
858
  let maxN = 0;
@@ -827,7 +882,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
827
882
  if (!tag || tags.has(tag)) continue;
828
883
  if (!sessionMatchesContext(session, context)) continue;
829
884
  tags.set(tag, session.id);
830
- if (session.role) tagRoles.set(tag, session.role);
885
+ if (session.agent) tagAgents.set(tag, session.agent);
831
886
  if (session.cwd) tagCwds.set(tag, session.cwd);
832
887
  upsertWorkerSessionDeferred(session, tag);
833
888
  }
@@ -836,7 +891,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
836
891
  function bindTag(tag, session, extra = {}) {
837
892
  if (!tag || !session?.id) return;
838
893
  tags.set(tag, session.id);
839
- if (session.role) tagRoles.set(tag, session.role);
894
+ if (session.agent) tagAgents.set(tag, session.agent);
840
895
  if (session.cwd) tagCwds.set(tag, session.cwd);
841
896
  upsertWorkerSessionDeferred(session, tag, extra);
842
897
  }
@@ -845,7 +900,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
845
900
  if (!tag) return;
846
901
  const sessionId = tags.get(tag) || '';
847
902
  tags.delete(tag);
848
- tagRoles.delete(tag);
903
+ tagAgents.delete(tag);
849
904
  tagCwds.delete(tag);
850
905
  removeWorkerRow({ tag, sessionId });
851
906
  }
@@ -1034,10 +1089,14 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1034
1089
  };
1035
1090
  }
1036
1091
 
1037
- const agentName = normalizeAgentName(args.agent || args.role);
1092
+ const agentName = normalizeAgentName(args.agent);
1093
+ const configuredDefault = clean(config?.defaultProvider);
1094
+ const fallbackProvider = configuredDefault && isKnownProvider(configuredDefault)
1095
+ ? configuredDefault
1096
+ : DEFAULT_PROVIDER;
1038
1097
  const agentRoute = !clean(args.preset)
1039
- ? (normalizeAgentRoute(config?.agents?.[agentName])
1040
- || (agentName === 'maintainer' ? normalizeAgentRoute(config?.agents?.maintenance) : null))
1098
+ ? (normalizeAgentRoute(config?.agents?.[agentName], fallbackProvider)
1099
+ || (agentName === 'maintainer' ? normalizeAgentRoute(config?.agents?.maintenance, fallbackProvider) : null))
1041
1100
  : null;
1042
1101
  if (agentRoute) {
1043
1102
  return {
@@ -1073,11 +1132,11 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1073
1132
  const stage = session.stage || (status === 'idle' || status === 'error' || status === 'closed'
1074
1133
  ? status
1075
1134
  : (runtime?.stage || status));
1076
- const progress = sessionProgressExtras(sessionId, session.role || null, now);
1135
+ const progress = sessionProgressExtras(sessionId, session.agent || null, now);
1077
1136
  rows.push({
1078
1137
  tag,
1079
1138
  sessionId,
1080
- role: session.role || null,
1139
+ agent: session.agent || null,
1081
1140
  provider: session.provider,
1082
1141
  model: session.model,
1083
1142
  preset: session.presetName || null,
@@ -1133,7 +1192,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1133
1192
  if (!session) return null;
1134
1193
  const runtime = mgr.getSessionRuntime?.(sessionId);
1135
1194
  const status = session.closed === true ? 'closed' : (session.status || 'idle');
1136
- const progress = sessionProgressExtras(sessionId, session.role || null);
1195
+ const progress = sessionProgressExtras(sessionId, session.agent || null);
1137
1196
  return {
1138
1197
  workerStatus: status,
1139
1198
  stage: progress.worker_stage || runtime?.stage || status,
@@ -1151,7 +1210,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1151
1210
  status: task.status,
1152
1211
  tag: task.tag || null,
1153
1212
  sessionId: task.sessionId || null,
1154
- role: task.role || null,
1213
+ agent: task.agent || null,
1155
1214
  preset: task.preset || null,
1156
1215
  provider: task.provider || null,
1157
1216
  model: task.model || null,
@@ -1162,7 +1221,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1162
1221
  finishedAt: task.finishedAt || null,
1163
1222
  error: task.error || null,
1164
1223
  ...jobWorkerSnapshot(task.sessionId),
1165
- ...sessionProgressExtras(task.sessionId, task.role || null, Date.now(), task.status),
1224
+ ...sessionProgressExtras(task.sessionId, task.agent || null, Date.now(), task.status),
1166
1225
  }));
1167
1226
  return wantedPid ? rows.filter((row) => positiveInt(row.clientHostPid) === wantedPid) : rows;
1168
1227
  }
@@ -1177,7 +1236,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1177
1236
 
1178
1237
  function renderJob(job, includeResult = false) {
1179
1238
  const meta = job.meta || {};
1180
- let progress = sessionProgressExtras(meta.sessionId, meta.role || null, Date.now(), job.status);
1239
+ let progress = sessionProgressExtras(meta.sessionId, meta.agent || null, Date.now(), job.status);
1181
1240
  // Spawn is deferred: before the worker session exists, sessionProgressExtras
1182
1241
  // returns {} and the status card would show only "status: running" with no
1183
1242
  // stage/progress. Fill a minimal stage so the caller can tell the job is
@@ -1196,7 +1255,8 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1196
1255
  status: job.status,
1197
1256
  tag: meta.tag || null,
1198
1257
  sessionId: meta.sessionId || null,
1199
- role: meta.role || null,
1258
+ agent: meta.agent || null,
1259
+ ...(meta.respawned === true ? { respawned: true, note: 'previous session reaped — fresh session, no prior context; re-supply anchors if needed' } : {}),
1200
1260
  preset: meta.preset || null,
1201
1261
  provider: meta.provider || null,
1202
1262
  model: meta.model || null,
@@ -1217,7 +1277,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1217
1277
  ...(extras || {}),
1218
1278
  tag: prepared.tag,
1219
1279
  sessionId: prepared.session.id,
1220
- role: prepared.role,
1280
+ agent: prepared.agent,
1221
1281
  preset: presetKey(prepared.preset) || prepared.presetName,
1222
1282
  provider: prepared.preset.provider,
1223
1283
  model: prepared.preset.model,
@@ -1228,7 +1288,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1228
1288
  }
1229
1289
 
1230
1290
  function pendingSpawnMeta(args = {}, extras = {}) {
1231
- const role = normalizeAgentName(args.agent || args.role);
1291
+ const agent = normalizeAgentName(args.agent);
1232
1292
  // Best-effort resolve the default preset so the pending "Spawn …" card can
1233
1293
  // already show the model (e.g. "Spawn Heavy Worker (Opus 4.8)") even when
1234
1294
  // the caller did not pass an explicit provider/model. Never throw: fall back
@@ -1242,7 +1302,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1242
1302
  ...(extras || {}),
1243
1303
  tag: clean(args.tag) || null,
1244
1304
  sessionId: null,
1245
- role: role || null,
1305
+ agent: agent || null,
1246
1306
  preset: clean(args.preset) || presetKey(resolved) || null,
1247
1307
  provider: clean(args.provider) || clean(resolved?.provider) || null,
1248
1308
  model: clean(args.model) || clean(resolved?.model) || null,
@@ -1260,7 +1320,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1260
1320
  ...job.input,
1261
1321
  tag: next.tag || job.input.tag || null,
1262
1322
  sessionId: next.sessionId || job.input.sessionId || null,
1263
- role: next.role || job.input.role || null,
1323
+ agent: next.agent || job.input.agent || null,
1264
1324
  };
1265
1325
  }
1266
1326
  job.label = next.tag || next.sessionId || job.label;
@@ -1321,14 +1381,19 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1321
1381
  const ownerSessionId = clean(notifyContext?.callerSessionId || notifyContext?.sessionId);
1322
1382
  const upstream = typeof notifyContext?.notifyFn === 'function' ? notifyContext.notifyFn : null;
1323
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';
1324
1388
  const snapshot = {
1325
1389
  ...job,
1326
- status: 'completed',
1390
+ status: earlyStatus,
1327
1391
  finishedAt,
1328
1392
  finishedAtMs: Date.now(),
1329
1393
  result: resultValue,
1330
1394
  resultType: job.resultType || 'agent_task_result',
1331
1395
  meta: sanitizeTaskMeta(job.meta || {}),
1396
+ ...(resultValue && resultValue.error ? { error: resultValue.error } : {}),
1332
1397
  };
1333
1398
  // An early notification is only a header-only *preview*: it fires before
1334
1399
  // the worker's session is persisted to signal the running→completed
@@ -1341,8 +1406,8 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1341
1406
  type: snapshot.resultType,
1342
1407
  execution_surface: 'agent',
1343
1408
  execution_id: job.taskId || null,
1344
- status: 'completed',
1345
- instruction: `The async agent task ${job.taskId || ''} has finished (completed) - review this result in your next step.`,
1409
+ status: earlyStatus,
1410
+ instruction: `The async agent task ${job.taskId || ''} has finished (${earlyStatus}) - review this result in your next step.`,
1346
1411
  ...(ownerSessionId ? { caller_session_id: ownerSessionId } : {}),
1347
1412
  };
1348
1413
  let delivered = false;
@@ -1377,7 +1442,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1377
1442
  surface: 'agent',
1378
1443
  operation: type,
1379
1444
  label: jobMeta?.tag || jobMeta?.sessionId || type,
1380
- input: { type, tag: jobMeta?.tag || null, sessionId: jobMeta?.sessionId || null, role: jobMeta?.role || null },
1445
+ input: { type, tag: jobMeta?.tag || null, sessionId: jobMeta?.sessionId || null, agent: jobMeta?.agent || null },
1381
1446
  context: notifyContext,
1382
1447
  meta: jobMeta,
1383
1448
  resultType: 'agent_task_result',
@@ -1406,8 +1471,10 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1406
1471
  // installs its progress watchdog, so guard prep with an internal env-
1407
1472
  // backed cap rather than exposing per-call timeout knobs on the agent
1408
1473
  // tool surface.
1409
- const prepDeadlineMs = DEFAULT_SPAWN_PREP_TIMEOUT_MS;
1474
+ const prepDeadlineMs = nonNegativeInt(args.spawnPrepTimeoutMs ?? args.prepTimeoutMs)
1475
+ ?? DEFAULT_SPAWN_PREP_TIMEOUT_MS;
1410
1476
  let prepared;
1477
+ const prepState = { timedOut: false };
1411
1478
  if (prepDeadlineMs > 0) {
1412
1479
  let prepTimer = null;
1413
1480
  let timedOut = false;
@@ -1415,13 +1482,14 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1415
1482
  // prepareSpawn promise may still resolve later with a fully-built
1416
1483
  // session/tag/route — attach a cleanup so the late-arriving prepared is
1417
1484
  // torn down, otherwise the orphaned tag would collide on re-spawn.
1418
- const prepPromise = prepareSpawn(args, callerCwd, context);
1485
+ const prepPromise = prepareSpawn(args, callerCwd, context, prepState);
1419
1486
  prepPromise.then((late) => {
1420
1487
  if (timedOut) closePreparedSpawn(late, 'agent-spawn-prep-timeout');
1421
1488
  }, () => {});
1422
1489
  const timeout = new Promise((_resolve, reject) => {
1423
1490
  prepTimer = setTimeout(() => {
1424
1491
  timedOut = true;
1492
+ prepState.timedOut = true;
1425
1493
  reject(new Error(`agent spawn prep timed out (${prepDeadlineMs}ms) before model request`));
1426
1494
  }, prepDeadlineMs);
1427
1495
  prepTimer.unref?.();
@@ -1432,7 +1500,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1432
1500
  if (prepTimer) clearTimeout(prepTimer);
1433
1501
  }
1434
1502
  } else {
1435
- prepared = await prepareSpawn(args, callerCwd, context);
1503
+ prepared = await prepareSpawn(args, callerCwd, context, prepState);
1436
1504
  }
1437
1505
  mergeJobMeta(job, preparedSpawnMeta(prepared, extras));
1438
1506
  upsertWorkerSessionDeferred(prepared.session, prepared.tag, {
@@ -1483,17 +1551,20 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1483
1551
  };
1484
1552
  }
1485
1553
 
1486
- async function prepareSpawn(args, callerCwd = null, context = {}) {
1554
+ async function prepareSpawn(args, callerCwd = null, context = {}, prepState = null) {
1487
1555
  refreshTagsFromSessions({ context });
1488
1556
  const config = cfgMod.loadConfig();
1489
- const role = normalizeAgentName(args.agent || args.role);
1490
- if (!role) throw new Error('agent spawn: agent is required');
1491
- const agentPermission = readAgentFrontmatterPermission(role, dataDir);
1492
- const rolePermission = normalizeAgentPermission(agentPermission) || null;
1557
+ const agent = normalizeAgentName(args.agent);
1558
+ if (!agent) throw new Error('agent spawn: agent is required');
1559
+ const agentPermission = readAgentFrontmatterPermission(agent, dataDir);
1560
+ const agentPerm = normalizeAgentPermission(agentPermission) || null;
1493
1561
  const { presetName, preset } = resolvePreset(config, args);
1494
1562
  await ensureProvider(config, preset.provider);
1563
+ if (prepState?.timedOut) {
1564
+ throw new Error('agent spawn prep timed out before session bind');
1565
+ }
1495
1566
 
1496
- const tag = clean(args.tag) || nextTag(role, context);
1567
+ const tag = clean(args.tag) || nextTag(agent, context);
1497
1568
  // Any resolved same-tag binding in this terminal (live or lingering trace)
1498
1569
  // blocks a fresh spawn. execute() routes live reuse before prepareSpawn.
1499
1570
  if (resolveTag(tag, context, { scanSessions: wantsSessionScan(args) })) {
@@ -1502,25 +1573,28 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1502
1573
  const baseCwd = resolve(callerCwd || defaultCwd || process.cwd());
1503
1574
  const workerCwd = clean(args.cwd) ? resolve(baseCwd, args.cwd) : baseCwd;
1504
1575
  const prompt = withCwdHeader(await resolvePrompt(args, workerCwd), workerCwd);
1576
+ if (prepState?.timedOut) {
1577
+ throw new Error('agent spawn prep timed out before session bind');
1578
+ }
1505
1579
  const runtimeSpec = cfgMod.resolveRuntimeSpec(preset, { lane: 'agent', agentId: tag });
1506
1580
  const maxLoopIterations = positiveInt(args.maxLoopIterations) || null;
1507
- const watchdogPolicy = resolveAgentWatchdogPolicy(role);
1581
+ const watchdogPolicy = resolveAgentWatchdogPolicy(agent);
1508
1582
  const { session, effectiveCwd } = prepareAgentSession({
1509
- role,
1583
+ agent,
1510
1584
  presetName,
1511
1585
  preset,
1512
1586
  runtimeSpec,
1513
1587
  owner: AGENT_OWNER,
1514
1588
  cwd: workerCwd,
1515
1589
  sourceType: 'cli',
1516
- sourceName: role,
1590
+ sourceName: agent,
1517
1591
  parentSessionId: clean(context?.callerSessionId || context?.sessionId) || null,
1518
1592
  ownerSessionId: clean(context?.callerSessionId || context?.sessionId) || null,
1519
1593
  clientHostPid: terminalPidForContext(context) || null,
1520
1594
  agentTag: tag,
1521
1595
  taskType: clean(args.taskType) || clean(args.typeHint) || undefined,
1522
1596
  maxLoopIterations: maxLoopIterations || undefined,
1523
- permission: rolePermission || undefined,
1597
+ permission: agentPerm || undefined,
1524
1598
  cacheKeyOverride: args.cacheKey || undefined,
1525
1599
  });
1526
1600
  // Lead sessions write a gateway-session route when created; agent sessions
@@ -1528,7 +1602,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1528
1602
  // or the vendored L1/L2 statusline cannot resolve the agent route/model.
1529
1603
  writeAgentStatuslineRoute(session.id, preset);
1530
1604
  bindTag(tag, session, {
1531
- role,
1605
+ agent,
1532
1606
  preset: presetKey(preset) || presetName,
1533
1607
  provider: preset.provider,
1534
1608
  model: preset.model,
@@ -1542,7 +1616,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1542
1616
  args,
1543
1617
  tag,
1544
1618
  session,
1545
- role,
1619
+ agent,
1546
1620
  preset,
1547
1621
  presetName,
1548
1622
  workerCwd: effectiveCwd || workerCwd,
@@ -1553,11 +1627,13 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1553
1627
  }
1554
1628
 
1555
1629
  async function runSpawn(prepared, notifyContext = null, job = null) {
1556
- const { args, tag, session, role, preset, presetName, workerCwd, prompt, watchdogPolicy } = prepared;
1630
+ const { args, tag, session, agent, preset, presetName, workerCwd, prompt, watchdogPolicy } = prepared;
1557
1631
  const watchdog = startProgressIdleWatchdog(session.id, watchdogPolicy);
1558
1632
  let finalStatus = 'idle';
1633
+ // SubagentStart: a worker session is about to run its first turn.
1634
+ emitSubagentEvent('start', agent, { session_id: session.id, tag });
1559
1635
  upsertWorkerSessionDeferred(session, tag, {
1560
- role,
1636
+ agent,
1561
1637
  preset: presetKey(preset) || presetName,
1562
1638
  provider: preset.provider,
1563
1639
  model: preset.model,
@@ -1567,17 +1643,26 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1567
1643
  stage: 'running',
1568
1644
  });
1569
1645
  try {
1570
- const completionValue = (result) => ({
1571
- tag,
1572
- sessionId: session.id,
1573
- role,
1574
- preset: presetKey(preset) || presetName,
1575
- provider: preset.provider,
1576
- model: preset.model,
1577
- effort: preset.effort || null,
1578
- fast: preset.fast === true,
1579
- content: result?.content || '',
1580
- });
1646
+ const completionValue = (result) => {
1647
+ // Promote an abnormal finish (iteration cap, truncation, or a public
1648
+ // agent's empty terminal turn) to an explicit error, so the Lead
1649
+ // receives it as a failure with an accurate reason instead of a silent
1650
+ // `completed` empty result. Keyed off loop.mjs terminationReason;
1651
+ // hidden agents finishing normally-empty are left untagged (benign).
1652
+ const abnormalError = abnormalEmptyFinishError(result, agent);
1653
+ return {
1654
+ tag,
1655
+ sessionId: session.id,
1656
+ agent,
1657
+ preset: presetKey(preset) || presetName,
1658
+ provider: preset.provider,
1659
+ model: preset.model,
1660
+ effort: preset.effort || null,
1661
+ fast: preset.fast === true,
1662
+ content: result?.content || '',
1663
+ ...(abnormalError ? { error: abnormalError } : {}),
1664
+ };
1665
+ };
1581
1666
  const result = await mgr.askSession(session.id, prompt, args.context || null, null, workerCwd, null, {
1582
1667
  notifyFn: workerNotifyFn(session.id, notifyContext || {}),
1583
1668
  ...(job ? {
@@ -1591,11 +1676,21 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1591
1676
  // reconcile remains a backup for the error/no-terminal-result path.
1592
1677
  if (job?.taskId) {
1593
1678
  try {
1594
- reconcileBackgroundTask(job.taskId, {
1595
- status: 'completed',
1596
- result: value,
1597
- terminalReason: 'agent-terminal-result',
1598
- });
1679
+ // An empty/abnormal finish is a failure, not a completion:
1680
+ // reconcile as `failed` with the accurate error so the Lead
1681
+ // card renders `error: …` instead of a header-only empty card.
1682
+ reconcileBackgroundTask(job.taskId, value.error
1683
+ ? {
1684
+ status: 'failed',
1685
+ result: value,
1686
+ error: value.error,
1687
+ terminalReason: 'agent-empty-final',
1688
+ }
1689
+ : {
1690
+ status: 'completed',
1691
+ result: value,
1692
+ terminalReason: 'agent-terminal-result',
1693
+ });
1599
1694
  } catch {}
1600
1695
  }
1601
1696
  },
@@ -1604,14 +1699,40 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1604
1699
  // The early preview no longer promises body suppression, so the canonical
1605
1700
  // notifyTaskCompletion is left to fire exactly once with output via the
1606
1701
  // resolve/reconcile/finally path.
1607
- return completionValue(result);
1702
+ const finalValue = completionValue(result);
1703
+ // Non-job return path (or job path where the terminal-result reconcile
1704
+ // already ran): if the finish was abnormal-empty, surface it as a thrown
1705
+ // error so finalStatus becomes 'error' and the caller's error path (and
1706
+ // the finally reconcile below, as `failed`) render the accurate reason.
1707
+ if (finalValue.error) {
1708
+ finalStatus = 'error';
1709
+ if (job) job._terminalResultValue = finalValue;
1710
+ throw new Error(finalValue.error);
1711
+ }
1712
+ return finalValue;
1608
1713
  } catch (error) {
1609
1714
  finalStatus = 'error';
1715
+ // Part C: a mid-stream stall (StreamStalledError / ESTREAMSTALL) throws
1716
+ // here WITHOUT a terminal result, so the finally reconcile below (gated on
1717
+ // _terminalResultValue) would be skipped and only the outer task-reject
1718
+ // path would notify. Belt-and-suspenders: reconcile this job to `failed`
1719
+ // now so the owner (Lead) always gets a failure notification instead of a
1720
+ // task stranded in `running`. Idempotent — completeBackgroundTask no-ops
1721
+ // once terminal, so the outer reject path can't double-notify.
1722
+ if (job?.taskId && job._terminalResultValue === undefined) {
1723
+ try {
1724
+ reconcileBackgroundTask(job.taskId, {
1725
+ status: 'failed',
1726
+ error,
1727
+ terminalReason: 'agent-stream-stalled',
1728
+ });
1729
+ } catch {}
1730
+ }
1610
1731
  throw error;
1611
1732
  } finally {
1612
1733
  watchdog?.stop?.();
1613
1734
  upsertWorkerSessionDeferred(session, tag, {
1614
- role,
1735
+ agent,
1615
1736
  preset: presetKey(preset) || presetName,
1616
1737
  provider: preset.provider,
1617
1738
  model: preset.model,
@@ -1631,11 +1752,16 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1631
1752
  reconcileBackgroundTask(job.taskId, {
1632
1753
  status: finalStatus === 'error' ? 'failed' : 'completed',
1633
1754
  result: job._terminalResultValue,
1755
+ ...(finalStatus === 'error' && job._terminalResultValue?.error
1756
+ ? { error: job._terminalResultValue.error }
1757
+ : {}),
1634
1758
  terminalReason: 'agent-finally-reconcile',
1635
1759
  });
1636
1760
  } catch {}
1637
1761
  }
1638
1762
  scheduleReap(session.id);
1763
+ // SubagentStop: worker finished (terminal), regardless of outcome.
1764
+ emitSubagentEvent('stop', agent, { session_id: session.id, tag, status: finalStatus });
1639
1765
  }
1640
1766
  }
1641
1767
 
@@ -1658,20 +1784,27 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1658
1784
 
1659
1785
  async function runSend(prepared, notifyContext = null, job = null) {
1660
1786
  const { args, session, sessionId, prompt } = prepared;
1661
- const sendRole = session.role || normalizeAgentName(args.agent || args.role);
1662
- const watchdog = startProgressIdleWatchdog(sessionId, resolveAgentWatchdogPolicy(sendRole));
1787
+ const sendAgent = session.agent || normalizeAgentName(args.agent);
1788
+ const watchdog = startProgressIdleWatchdog(sessionId, resolveAgentWatchdogPolicy(sendAgent));
1663
1789
  const tag = tagForSession(sessionId);
1664
1790
  let finalStatus = 'idle';
1665
1791
  upsertWorkerSessionDeferred(session, tag, { status: 'running', stage: 'running' });
1666
1792
  try {
1667
- const completionValue = (result) => ({
1668
- tag,
1669
- sessionId,
1670
- role: session.role || null,
1671
- provider: session.provider,
1672
- model: session.model,
1673
- content: result?.content || '',
1674
- });
1793
+ const completionValue = (result) => {
1794
+ // Same abnormal-empty → error promotion as runSpawn: a reused/`send`
1795
+ // worker that hits the cap, truncates, or finishes empty must surface
1796
+ // as a failure with an accurate reason, not a silent completed empty.
1797
+ const abnormalError = abnormalEmptyFinishError(result, session.agent || sendAgent);
1798
+ return {
1799
+ tag,
1800
+ sessionId,
1801
+ agent: session.agent || null,
1802
+ provider: session.provider,
1803
+ model: session.model,
1804
+ content: result?.content || '',
1805
+ ...(abnormalError ? { error: abnormalError } : {}),
1806
+ };
1807
+ };
1675
1808
  const result = await mgr.askSession(sessionId, prompt, args.context || null, null, session.cwd || defaultCwd, null, {
1676
1809
  notifyFn: workerNotifyFn(sessionId, notifyContext || {}),
1677
1810
  ...(job ? {
@@ -1683,11 +1816,18 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1683
1816
  // post-result save can't strand the task in `running`. Idempotent.
1684
1817
  if (job?.taskId) {
1685
1818
  try {
1686
- reconcileBackgroundTask(job.taskId, {
1687
- status: 'completed',
1688
- result: value,
1689
- terminalReason: 'agent-terminal-result',
1690
- });
1819
+ reconcileBackgroundTask(job.taskId, value.error
1820
+ ? {
1821
+ status: 'failed',
1822
+ result: value,
1823
+ error: value.error,
1824
+ terminalReason: 'agent-empty-final',
1825
+ }
1826
+ : {
1827
+ status: 'completed',
1828
+ result: value,
1829
+ terminalReason: 'agent-terminal-result',
1830
+ });
1691
1831
  } catch {}
1692
1832
  }
1693
1833
  },
@@ -1695,9 +1835,27 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1695
1835
  });
1696
1836
  // Early preview no longer suppresses the canonical body notification;
1697
1837
  // notifyTaskCompletion fires once with output via resolve/reconcile.
1698
- return completionValue(result);
1838
+ const finalValue = completionValue(result);
1839
+ if (finalValue.error) {
1840
+ finalStatus = 'error';
1841
+ if (job) job._terminalResultValue = finalValue;
1842
+ throw new Error(finalValue.error);
1843
+ }
1844
+ return finalValue;
1699
1845
  } catch (error) {
1700
1846
  finalStatus = 'error';
1847
+ // Part C (send path mirror): a mid-stream stall throws with no terminal
1848
+ // result — reconcile to `failed` so the owner is notified rather than the
1849
+ // task hanging in `running`. Idempotent (see runSpawn note).
1850
+ if (job?.taskId && job._terminalResultValue === undefined) {
1851
+ try {
1852
+ reconcileBackgroundTask(job.taskId, {
1853
+ status: 'failed',
1854
+ error,
1855
+ terminalReason: 'agent-stream-stalled',
1856
+ });
1857
+ } catch {}
1858
+ }
1701
1859
  throw error;
1702
1860
  } finally {
1703
1861
  watchdog?.stop?.();
@@ -1713,6 +1871,9 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1713
1871
  reconcileBackgroundTask(job.taskId, {
1714
1872
  status: finalStatus === 'error' ? 'failed' : 'completed',
1715
1873
  result: job._terminalResultValue,
1874
+ ...(finalStatus === 'error' && job._terminalResultValue?.error
1875
+ ? { error: job._terminalResultValue.error }
1876
+ : {}),
1716
1877
  terminalReason: 'agent-finally-reconcile',
1717
1878
  });
1718
1879
  } catch {}
@@ -1737,14 +1898,14 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1737
1898
  ...extras,
1738
1899
  tag: tagForSession(prepared.sessionId),
1739
1900
  sessionId: prepared.sessionId,
1740
- role: prepared.session.role || null,
1901
+ agent: prepared.session.agent || null,
1741
1902
  queueDepth,
1742
1903
  });
1743
1904
  }
1744
1905
  const job = startJob('send', {
1745
1906
  tag: tagForSession(prepared.sessionId),
1746
1907
  sessionId: prepared.sessionId,
1747
- role: prepared.session.role || null,
1908
+ agent: prepared.session.agent || null,
1748
1909
  provider: prepared.session.provider || null,
1749
1910
  model: prepared.session.model || null,
1750
1911
  preset: prepared.session.presetName || null,
@@ -1770,7 +1931,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1770
1931
  }
1771
1932
  const sessionId = resolveTag(target, scopedContext, { scanSessions: wantsSessionScan(args) });
1772
1933
  if (!sessionId) {
1773
- if (!target.startsWith('sess_') && tagRoles.has(target)) {
1934
+ if (!target.startsWith('sess_') && tagAgents.has(target)) {
1774
1935
  forgetTag(target);
1775
1936
  if (task?.taskId) cancelBackgroundTask(task.taskId, 'cancelled by agent close');
1776
1937
  return { closed: true, forgotten: true, tag: target, sessionId: null, task_id: task?.taskId || null };
@@ -1829,7 +1990,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1829
1990
  const value = clean(tag);
1830
1991
  if (!value || value.startsWith('sess_')) return false;
1831
1992
  if (resolveTag(value, context, { excludeTerminalTraces: true })) return false; // live -> reuse, not trace
1832
- if (tagRoles.has(value)) return true;
1993
+ if (tagAgents.has(value)) return true;
1833
1994
  return readWorkerRows(context).some((row) => clean(row.tag) === value);
1834
1995
  }
1835
1996
 
@@ -1850,8 +2011,32 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1850
2011
  if (type === 'cancel') return renderResult(close(args, scopedContext));
1851
2012
  if (type === 'close') return renderResult(close(args, scopedContext));
1852
2013
  if (type === 'send') {
1853
- const prepared = await prepareSend(args, scopedContext);
1854
- return dispatchToExistingSession(prepared, notifyContext);
2014
+ try {
2015
+ const prepared = await prepareSend(args, scopedContext);
2016
+ return dispatchToExistingSession(prepared, notifyContext);
2017
+ } catch (err) {
2018
+ // Reaped/dead-tag fallback: with the 5m terminal-reap window a
2019
+ // same-scope follow-up often lands after the session is gone.
2020
+ // Instead of bouncing an error back to Lead (who would just
2021
+ // re-issue the same content as a spawn), respawn a FRESH session
2022
+ // under the same tag with the message as its brief. `respawned:
2023
+ // true` in the result tells Lead the worker has no prior session
2024
+ // context — re-supply anchors on the next send if needed.
2025
+ // Only tag-addressed sends fall back; explicit sessionId sends
2026
+ // keep erroring (caller pinned a specific session on purpose).
2027
+ const fallbackTag = clean(args.tag);
2028
+ const isDeadTarget = /not found|is closed/i.test(String(err?.message || ''));
2029
+ if (!fallbackTag || fallbackTag.startsWith('sess_') || !isDeadTarget) throw err;
2030
+ const prompt = clean(args.message || args.prompt);
2031
+ if (!prompt) throw err;
2032
+ // Clear the terminal trace so the fresh spawn isn't rejected by
2033
+ // the lingering-trace guard, then run the normal deferred spawn.
2034
+ try { forgetTag(fallbackTag); } catch {}
2035
+ try { removeWorkerRow({ tag: fallbackTag }); } catch {}
2036
+ const spawnArgs = { ...args, type: 'spawn', tag: fallbackTag, prompt, message: undefined };
2037
+ const job = startDeferredSpawnJob(spawnArgs, callerCwd, context, notifyContext, { respawned: true });
2038
+ return renderResult(renderJob(job, false));
2039
+ }
1855
2040
  }
1856
2041
  if (type === 'spawn') {
1857
2042
  // Explicit-tag spawn priority (auto nextTag always creates a fresh session):
@@ -1887,7 +2072,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1887
2072
  }
1888
2073
  throw new Error(`agent: unknown type "${type}"`);
1889
2074
  } catch (err) {
1890
- return `Error: ${presentErrorText(err, { surface: 'agent' })}`;
2075
+ return errorLine(err, { surface: 'agent' });
1891
2076
  }
1892
2077
  }
1893
2078