mixdog 0.9.151 → 0.9.153

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 (307) hide show
  1. package/LICENSES/browser-import-NOTICE.txt +11 -0
  2. package/NOTICE.md +11 -0
  3. package/README.md +108 -68
  4. package/package.json +17 -4
  5. package/scripts/computer-task-cost-report.mjs +144 -0
  6. package/scripts/run-office-live-tests.mjs +32 -0
  7. package/scripts/tool-stress.mjs +0 -2
  8. package/src/agents/front-worker/AGENT.md +13 -0
  9. package/src/agents/front-worker/agent.json +6 -0
  10. package/src/agents/heavy-worker/AGENT.md +10 -11
  11. package/src/agents/heavy-worker/agent.json +1 -1
  12. package/src/agents/maintainer/AGENT.md +0 -2
  13. package/src/agents/reviewer/AGENT.md +9 -13
  14. package/src/agents/reviewer/agent.json +1 -1
  15. package/src/agents/security/AGENT.md +14 -0
  16. package/src/agents/security/agent.json +6 -0
  17. package/src/agents/worker/AGENT.md +8 -8
  18. package/src/agents/worker/agent.json +1 -1
  19. package/src/agents/writer/AGENT.md +14 -0
  20. package/src/agents/writer/agent.json +6 -0
  21. package/src/defaults/skills/setup/SKILL.md +18 -27
  22. package/src/headless-exec.mjs +3 -10
  23. package/src/headless-exec.test.mjs +2 -7
  24. package/src/rules/agent/00-common.md +4 -7
  25. package/src/rules/lead/01-general.md +0 -3
  26. package/src/rules/shared/10-tool-workflow.md +10 -2
  27. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +183 -26
  28. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.test.mjs +378 -0
  29. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +4 -0
  30. package/src/runtime/agent/orchestrator/cache-break-trace.mjs +109 -0
  31. package/src/runtime/agent/orchestrator/cache-break-trace.test.mjs +115 -0
  32. package/src/runtime/agent/orchestrator/config.mjs +13 -68
  33. package/src/runtime/agent/orchestrator/context/collect-skills.test.mjs +153 -0
  34. package/src/runtime/agent/orchestrator/context/collect.mjs +49 -45
  35. package/src/runtime/agent/orchestrator/mcp/client.mjs +242 -15
  36. package/src/runtime/agent/orchestrator/mcp/features.test.mjs +66 -0
  37. package/src/runtime/agent/orchestrator/mcp/security.test.mjs +36 -20
  38. package/src/runtime/agent/orchestrator/providers/admission-scheduler-metrics.test.mjs +47 -0
  39. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +16 -3
  40. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +54 -33
  41. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -0
  42. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
  43. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +7 -0
  44. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +3 -8
  45. package/src/runtime/agent/orchestrator/providers/gemini.mjs +7 -0
  46. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +19 -3
  47. package/src/runtime/agent/orchestrator/providers/lib/provider-replay.mjs +50 -0
  48. package/src/runtime/agent/orchestrator/providers/lib/sse-framing.mjs +93 -0
  49. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +30 -9
  50. package/src/runtime/agent/orchestrator/providers/media-parity.test.mjs +59 -0
  51. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +3 -3
  52. package/src/runtime/agent/orchestrator/providers/openai-codex-identity.test.mjs +71 -0
  53. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +18 -26
  54. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +8 -0
  55. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +7 -0
  56. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +25 -2
  57. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +35 -4
  58. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +47 -2
  59. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +103 -12
  60. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +13 -0
  61. package/src/runtime/agent/orchestrator/providers/openai-turn-state.mjs +68 -0
  62. package/src/runtime/agent/orchestrator/providers/openai-ws-delta-parity.test.mjs +122 -6
  63. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +36 -11
  64. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +144 -93
  65. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +18 -13
  66. package/src/runtime/agent/orchestrator/providers/provider-replay.test.mjs +288 -0
  67. package/src/runtime/agent/orchestrator/providers/sse-framing.test.mjs +719 -0
  68. package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +399 -39
  69. package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +26 -0
  70. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +86 -7
  71. package/src/runtime/agent/orchestrator/session/agent-loop.test.mjs +222 -0
  72. package/src/runtime/agent/orchestrator/session/browser-snapshot-supersession.mjs +61 -0
  73. package/src/runtime/agent/orchestrator/session/browser-snapshot-supersession.test.mjs +83 -0
  74. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +13 -0
  75. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +26 -2
  76. package/src/runtime/agent/orchestrator/session/compact-policy.test.mjs +112 -0
  77. package/src/runtime/agent/orchestrator/session/compaction-read-reset.test.mjs +192 -0
  78. package/src/runtime/agent/orchestrator/session/context-usage-restart.test.mjs +72 -0
  79. package/src/runtime/agent/orchestrator/session/context-utils.mjs +100 -15
  80. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +17 -1
  81. package/src/runtime/agent/orchestrator/session/eager-dispatch.test.mjs +74 -0
  82. package/src/runtime/agent/orchestrator/session/image-strip-recovery.mjs +12 -8
  83. package/src/runtime/agent/orchestrator/session/image-strip-recovery.test.mjs +20 -1
  84. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +110 -2
  85. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +3 -16
  86. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.test.mjs +28 -0
  87. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +2 -2
  88. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +6 -0
  89. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +15 -1
  90. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +6 -0
  91. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +111 -39
  92. package/src/runtime/agent/orchestrator/session/manager/ask-session.test.mjs +46 -0
  93. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +30 -6
  94. package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +3 -0
  95. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +27 -3
  96. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +26 -54
  97. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.test.mjs +63 -1
  98. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +3 -0
  99. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +4 -110
  100. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +580 -0
  101. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.test.mjs +593 -0
  102. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +119 -190
  103. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +115 -0
  104. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +43 -8
  105. package/src/runtime/agent/orchestrator/session/provider-prefix-guard.mjs +129 -12
  106. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +9 -2
  107. package/src/runtime/agent/orchestrator/session/store/listing.mjs +8 -1
  108. package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +15 -1
  109. package/src/runtime/agent/orchestrator/session/store-live-publication.test.mjs +83 -0
  110. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +36 -2
  111. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +157 -30
  112. package/src/runtime/agent/orchestrator/session/store-summary-reader.test.mjs +64 -0
  113. package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +57 -0
  114. package/src/runtime/agent/orchestrator/session/store-summary-visibility.test.mjs +219 -0
  115. package/src/runtime/agent/orchestrator/session/store.mjs +26 -2
  116. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +22 -0
  117. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool-cwd-env.test.mjs +14 -0
  118. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -10
  119. package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +30 -14
  120. package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +15 -4
  121. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +73 -15
  122. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +76 -2
  123. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +24 -683
  124. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +64 -72
  125. package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +2 -5
  126. package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.mjs +75 -0
  127. package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.test.mjs +24 -0
  128. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +6 -6
  129. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +12 -0
  130. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +4 -1
  131. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +24 -5
  132. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.test.mjs +44 -0
  133. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +12 -2
  134. package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +35 -21
  135. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +28 -13
  136. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.test.mjs +54 -0
  137. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
  138. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  139. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +12 -0
  140. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
  141. package/src/runtime/attachments/pdf-extract.mjs +3 -0
  142. package/src/runtime/attachments/pdfjs-runtime.mjs +13 -0
  143. package/src/runtime/bridge-clients.test.mjs +827 -0
  144. package/src/runtime/browser-bridge/action-schema.mjs +251 -0
  145. package/src/runtime/browser-bridge/client.mjs +152 -0
  146. package/src/runtime/browser-bridge/tool-defs.mjs +159 -0
  147. package/src/runtime/computer-bridge/action-schema.mjs +653 -0
  148. package/src/runtime/computer-bridge/client.mjs +231 -0
  149. package/src/runtime/computer-bridge/tool-defs.mjs +33 -0
  150. package/src/runtime/media/store.mjs +18 -8
  151. package/src/runtime/media/store.test.mjs +14 -0
  152. package/src/runtime/memory/index.mjs +1 -1
  153. package/src/runtime/memory/lib/transcript-ingest.mjs +8 -6
  154. package/src/runtime/memory/lib/transcript-ingest.test.mjs +52 -0
  155. package/src/runtime/office/assurance-benchmark.mjs +358 -0
  156. package/src/runtime/office/assurance.mjs +833 -0
  157. package/src/runtime/office/benchmark.mjs +496 -0
  158. package/src/runtime/office/capabilities.mjs +551 -0
  159. package/src/runtime/office/com-adapter.mjs +283 -0
  160. package/src/runtime/office/content-model.mjs +183 -0
  161. package/src/runtime/office/contract-benchmark.mjs +105 -0
  162. package/src/runtime/office/design-library.mjs +1562 -0
  163. package/src/runtime/office/design-system.mjs +1966 -0
  164. package/src/runtime/office/index.mjs +2413 -0
  165. package/src/runtime/office/journal.mjs +82 -0
  166. package/src/runtime/office/office-assurance.test.mjs +371 -0
  167. package/src/runtime/office/office-com-host.ps1 +3781 -0
  168. package/src/runtime/office/office-com-session-host.ps1 +855 -0
  169. package/src/runtime/office/office-live-runtime.test.mjs +819 -0
  170. package/src/runtime/office/office-runtime.test.mjs +2070 -0
  171. package/src/runtime/office/ooxml-validator.mjs +271 -0
  172. package/src/runtime/office/pagination.mjs +132 -0
  173. package/src/runtime/office/pdf-adapter.mjs +591 -0
  174. package/src/runtime/office/pdf-analysis.mjs +372 -0
  175. package/src/runtime/office/pdf-render.mjs +167 -0
  176. package/src/runtime/office/pdf-security.mjs +63 -0
  177. package/src/runtime/office/polish-benchmark.mjs +190 -0
  178. package/src/runtime/office/portable-ooxml.mjs +1867 -0
  179. package/src/runtime/office/quality-live-benchmark.mjs +486 -0
  180. package/src/runtime/office/quality-pipeline.mjs +136 -0
  181. package/src/runtime/office/tabular.mjs +302 -0
  182. package/src/runtime/office/templates/mixdog-executive.pptx +0 -0
  183. package/src/runtime/office/templates/mixdog-executive.pptx.mixdog.json +190 -0
  184. package/src/runtime/office/tool-defs.mjs +113 -0
  185. package/src/runtime/office/visual-diff.mjs +89 -0
  186. package/src/runtime/office/xlsx-assertions.mjs +172 -0
  187. package/src/runtime/office/xlsx-contract.mjs +147 -0
  188. package/src/runtime/shared/agent-route-config.mjs +9 -0
  189. package/src/runtime/shared/child-spawn-gate.mjs +17 -0
  190. package/src/runtime/shared/child-spawn-remote.mjs +69 -7
  191. package/src/runtime/shared/child-spawn-remote.test.mjs +124 -0
  192. package/src/runtime/shared/pristine-execution-contract.json +2 -1
  193. package/src/runtime/shared/provider-api-key.mjs +1 -0
  194. package/src/runtime/shared/session-runtime-health.mjs +91 -0
  195. package/src/runtime/shared/session-runtime-health.test.mjs +53 -0
  196. package/src/runtime/shared/skill-document.mjs +86 -0
  197. package/src/runtime/shared/skill-document.test.mjs +81 -0
  198. package/src/runtime/shared/tool-execution-contract.mjs +16 -0
  199. package/src/runtime/shared/tool-surface.mjs +27 -0
  200. package/src/runtime/shared/turn-snapshot-store.mjs +1 -0
  201. package/src/runtime/shared/turn-snapshot.mjs +32 -9
  202. package/src/runtime/shared/user-cwd.mjs +33 -14
  203. package/src/runtime/shared/user-cwd.test.mjs +43 -0
  204. package/src/session-runtime/agent-disable.test.mjs +20 -1
  205. package/src/session-runtime/context-status.mjs +9 -3
  206. package/src/session-runtime/cwd-plugins.mjs +26 -26
  207. package/src/session-runtime/cwd-plugins.test.mjs +75 -0
  208. package/src/session-runtime/cwd-tool-routing.test.mjs +5 -0
  209. package/src/session-runtime/deferred-tool-delta.mjs +104 -0
  210. package/src/session-runtime/deferred-tool-delta.test.mjs +40 -0
  211. package/src/session-runtime/global-extensions.mjs +26 -0
  212. package/src/session-runtime/global-extensions.test.mjs +28 -0
  213. package/src/session-runtime/global-settings-session-identity.test.mjs +76 -0
  214. package/src/session-runtime/goal-runtime.mjs +1141 -0
  215. package/src/session-runtime/goal-runtime.test.mjs +645 -0
  216. package/src/session-runtime/lifecycle-api.mjs +58 -11
  217. package/src/session-runtime/lifecycle-api.test.mjs +183 -0
  218. package/src/session-runtime/mcp-glue.mjs +49 -47
  219. package/src/session-runtime/plugin-mcp-standard.test.mjs +177 -0
  220. package/src/session-runtime/plugin-mcp.mjs +96 -22
  221. package/src/session-runtime/prewarm.mjs +1 -10
  222. package/src/session-runtime/provider-request-snapshot.mjs +33 -9
  223. package/src/session-runtime/provider-request-snapshot.test.mjs +27 -5
  224. package/src/session-runtime/resource-api-global.test.mjs +109 -0
  225. package/src/session-runtime/resource-api.mjs +168 -116
  226. package/src/session-runtime/runtime-core.mjs +208 -29
  227. package/src/session-runtime/runtime-tunables.mjs +6 -8
  228. package/src/session-runtime/session-lifecycle.mjs +34 -12
  229. package/src/session-runtime/session-title.mjs +2 -2
  230. package/src/session-runtime/session-turn-api.mjs +37 -25
  231. package/src/session-runtime/skills-api.mjs +79 -25
  232. package/src/session-runtime/skills-api.test.mjs +99 -0
  233. package/src/session-runtime/tool-catalog-data.mjs +1 -1
  234. package/src/session-runtime/tool-catalog-schema.mjs +0 -5
  235. package/src/session-runtime/tool-catalog.mjs +42 -112
  236. package/src/session-runtime/tool-defs.mjs +6 -1
  237. package/src/session-runtime/tool-policy-surface.test.mjs +93 -17
  238. package/src/session-runtime/tool-surface.mjs +30 -8
  239. package/src/session-runtime/workflow-agents-api.mjs +8 -25
  240. package/src/session-runtime/workflow.mjs +8 -6
  241. package/src/standalone/agent-dispatch-broker.mjs +5 -9
  242. package/src/standalone/agent-tool/helpers.mjs +2 -2
  243. package/src/standalone/agent-tool/job-views.mjs +3 -1
  244. package/src/standalone/agent-tool/notify.mjs +11 -9
  245. package/src/standalone/agent-tool/notify.test.mjs +37 -0
  246. package/src/standalone/agent-tool/render.mjs +3 -0
  247. package/src/standalone/agent-tool/spawn-flow.mjs +61 -16
  248. package/src/standalone/agent-tool/spawn-flow.test.mjs +41 -0
  249. package/src/standalone/agent-tool/tool-def.mjs +1 -1
  250. package/src/standalone/agent-tool/worker-index.mjs +123 -0
  251. package/src/standalone/agent-tool/worker-index.test.mjs +132 -0
  252. package/src/standalone/agent-tool/worker-rows.mjs +2 -1
  253. package/src/standalone/agent-tool.mjs +57 -10
  254. package/src/standalone/daemon.mjs +113 -15
  255. package/src/standalone/hook-bus/config.mjs +1 -0
  256. package/src/standalone/plugin-admin.mjs +17 -0
  257. package/src/standalone/plugin-admin.test.mjs +7 -0
  258. package/src/standalone/provider-admin.mjs +1 -0
  259. package/src/standalone/session-protocol.mjs +5 -0
  260. package/src/standalone/session-runtime-agent-control-client.mjs +133 -0
  261. package/src/standalone/session-runtime-agent-control-client.test.mjs +71 -0
  262. package/src/standalone/session-runtime-dispatch-cancel.test.mjs +139 -0
  263. package/src/standalone/session-runtime-host-factory.mjs +17 -0
  264. package/src/standalone/session-runtime-host-factory.test.mjs +31 -0
  265. package/src/standalone/session-runtime-host-health.test.mjs +5 -0
  266. package/src/standalone/session-runtime-host.mjs +709 -58
  267. package/src/standalone/session-runtime-inline-host.mjs +258 -0
  268. package/src/standalone/session-runtime-inline-host.test.mjs +385 -0
  269. package/src/standalone/session-runtime-provider-cooldown.mjs +78 -0
  270. package/src/standalone/session-runtime-provider-cooldown.test.mjs +80 -0
  271. package/src/standalone/session-runtime-shard-host.test.mjs +642 -0
  272. package/src/standalone/session-runtime-shard-router.mjs +149 -0
  273. package/src/standalone/session-runtime-shard-router.test.mjs +101 -0
  274. package/src/standalone/session-runtime-worker.mjs +302 -26
  275. package/src/standalone/session-service-agent-tree.test.mjs +984 -0
  276. package/src/standalone/session-service-goal-recovery.test.mjs +177 -0
  277. package/src/standalone/session-service.mjs +711 -11
  278. package/src/tui/app/app-view.jsx +1 -0
  279. package/src/tui/app/core-memory-picker.mjs +2 -0
  280. package/src/tui/app/extension-pickers.mjs +1 -1
  281. package/src/tui/app/maintenance-pickers.mjs +3 -3
  282. package/src/tui/app/model-options.mjs +2 -0
  283. package/src/tui/app/model-picker.mjs +14 -4
  284. package/src/tui/app/panel-handoff.test.mjs +311 -0
  285. package/src/tui/app/project-picker.mjs +1 -0
  286. package/src/tui/app/route-pickers.mjs +21 -6
  287. package/src/tui/app/settings-picker.mjs +26 -3
  288. package/src/tui/app/slash-commands.mjs +1 -0
  289. package/src/tui/app/slash-dispatch.mjs +73 -22
  290. package/src/tui/app/theme-effort-pickers.mjs +7 -2
  291. package/src/tui/components/Picker.jsx +10 -1
  292. package/src/tui/dist/index.mjs +187 -45
  293. package/src/tui/lib/voice-setup.mjs +4 -1
  294. package/src/tui/session/agent-message-source.test.mjs +37 -0
  295. package/src/tui/session/completion-card-restore.test.mjs +50 -0
  296. package/src/tui/session/context-state.mjs +3 -13
  297. package/src/tui/session/goal-continuation.mjs +141 -0
  298. package/src/tui/session/goal-continuation.test.mjs +203 -0
  299. package/src/tui/session/queue-helpers.mjs +5 -2
  300. package/src/tui/session/session-action-surface.test.mjs +190 -0
  301. package/src/tui/session/session-api-ext.mjs +48 -8
  302. package/src/tui/session/session-api.mjs +73 -1
  303. package/src/tui/session/session-flow.mjs +31 -1
  304. package/src/tui/session/turn.mjs +88 -15
  305. package/src/tui/session-local.mjs +39 -3
  306. package/src/vendor/statusline/src/gateway/route-meta.mjs +31 -0
  307. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +0 -301
@@ -61,3 +61,44 @@ test('agent job completion uses immutable caller owner and never invokes stale n
61
61
  assert.match(deliveries.at(-1).text, /owner handoff/);
62
62
  cleanupBackgroundTasks({ surface: 'agent', force: true });
63
63
  });
64
+
65
+ test('a user-cancelled Agent turn settles the worker row as cancelled, not error', async () => {
66
+ const updates = [];
67
+ const flow = createSpawnFlow({
68
+ mgr: {
69
+ getSession: () => ({ messages: [] }),
70
+ },
71
+ upsertWorkerSessionDeferred(_session, _tag, patch) {
72
+ updates.push(patch);
73
+ },
74
+ emitSubagentEvent() {},
75
+ scheduleReap() {},
76
+ createTurnReviewCollector: () => ({
77
+ onToolResult() {},
78
+ complete() {},
79
+ }),
80
+ sessionSurface: {
81
+ canonical: true,
82
+ canRun: () => true,
83
+ async runTurn() {
84
+ throw new Error('cancelled by agent close');
85
+ },
86
+ },
87
+ });
88
+ const job = { taskId: 'task-agent-cancelled', status: 'cancelled' };
89
+
90
+ await assert.rejects(flow.runSpawn({
91
+ args: {},
92
+ tag: 'cancelled-worker',
93
+ session: { id: 'sess_cancelled_worker' },
94
+ agent: 'worker',
95
+ preset: { provider: 'test-provider', model: 'test-model' },
96
+ presetName: 'worker-route',
97
+ workerCwd: 'C:\\Project\\tree',
98
+ prompt: 'cancel me',
99
+ watchdogPolicy: null,
100
+ }, { callerSessionId: 'sess_lead' }, job), /cancelled by agent close/);
101
+
102
+ assert.equal(updates.at(-1).status, 'cancelled');
103
+ assert.equal(updates.at(-1).stage, 'cancelled');
104
+ });
@@ -20,7 +20,7 @@ export const AGENT_TOOL = {
20
20
  destructiveHint: true,
21
21
  idempotentHint: false,
22
22
  openWorldHint: true,
23
- agentHidden: true,
23
+ agentHidden: false,
24
24
  },
25
25
  description: 'Run scoped agent work as background tasks. spawn/send return task_id immediately. Reuse one tag for one scope for its whole lifetime; give distinct tags only to independent scopes. Never mint a new tag because a session expired or was lost — same-tag spawn respawns it with the full brief. Wait for completion notifications; use status/read only for manual recovery.',
26
26
  inputSchema: {
@@ -7,6 +7,7 @@ import { readFileSync, statSync } from 'node:fs';
7
7
  import { resolve } from 'node:path';
8
8
 
9
9
  import { updateJsonAtomicSync } from '../../runtime/shared/atomic-file.mjs';
10
+ import { resolveAgentTerminalReapMs } from '../../session-runtime/config-helpers.mjs';
10
11
  import { WORKER_INDEX_FILE } from './tool-def.mjs';
11
12
  import { agentTagOf, clean, positiveInt, rowMatchesContext } from './helpers.mjs';
12
13
  import {
@@ -17,8 +18,37 @@ import {
17
18
  workerRowKey,
18
19
  } from './worker-rows.mjs';
19
20
 
21
+ const ACTIVE_WORKER_STATUS =
22
+ /^(?:connecting|requesting|streaming|tool[-_\s]?running|running|queued|pending|starting|cancelling)$/i;
23
+ const WORKER_POOL_FRESH_MS = 2 * 60 * 1000;
24
+ const exitFlushes = new Set();
25
+ let exitFlushInstalled = false;
26
+
27
+ function registerExitFlush(flush) {
28
+ exitFlushes.add(flush);
29
+ if (exitFlushInstalled) return;
30
+ exitFlushInstalled = true;
31
+ process.on('exit', () => {
32
+ for (const entry of exitFlushes) {
33
+ try { entry(); } catch { /* exit flush is best effort */ }
34
+ }
35
+ });
36
+ }
37
+
38
+ function runtimeAlive(pid) {
39
+ const id = Number(pid) || 0;
40
+ if (id <= 0 || id === process.pid) return true;
41
+ try {
42
+ process.kill(id, 0);
43
+ return true;
44
+ } catch (error) {
45
+ return error?.code === 'EPERM';
46
+ }
47
+ }
48
+
20
49
  export function createWorkerIndex({ dataDir, cfgMod, mgr, tags, tagAgents, tagCwds }) {
21
50
  const pendingMutators = [];
51
+ const activeWorkerKeys = new Set();
22
52
  let flushTimer = null;
23
53
  // Mtime-keyed parse cache. A single spawn calls refreshTagsFromSessions /
24
54
  // resolveTag / nextTag, which each re-read and re-parse this file; across a
@@ -48,6 +78,7 @@ export function createWorkerIndex({ dataDir, cfgMod, mgr, tags, tagAgents, tagCw
48
78
  .map((row) => ({
49
79
  tag: clean(row.tag),
50
80
  sessionId: clean(row.sessionId),
81
+ parentSessionId: clean(row.parentSessionId) || null,
51
82
  ownerSessionId: clean(row.ownerSessionId || row.parentSessionId) || null,
52
83
  agent: clean(row.agent) || null,
53
84
  provider: clean(row.provider) || null,
@@ -64,6 +95,7 @@ export function createWorkerIndex({ dataDir, cfgMod, mgr, tags, tagAgents, tagCw
64
95
  turnStartedAt: clean(row.turnStartedAt) || null,
65
96
  reapAt: clean(row.reapAt) || null,
66
97
  clientHostPid: positiveInt(row.clientHostPid),
98
+ runtimePid: positiveInt(row.runtimePid),
67
99
  cwd: clean(row.cwd) || null,
68
100
  task_id: clean(row.task_id || row.taskId) || null,
69
101
  error: clean(row.error) || null,
@@ -159,6 +191,47 @@ export function createWorkerIndex({ dataDir, cfgMod, mgr, tags, tagAgents, tagCw
159
191
  }
160
192
  }
161
193
 
194
+ function workerHeartbeatFresh(sessionId, now) {
195
+ const id = clean(sessionId);
196
+ if (!dataDir || !id) return false;
197
+ try {
198
+ const mtimeMs = statSync(resolve(dataDir, 'sessions', `${id}.hb`)).mtimeMs || 0;
199
+ return mtimeMs > 0 && now - mtimeMs <= WORKER_POOL_FRESH_MS;
200
+ } catch {
201
+ return false;
202
+ }
203
+ }
204
+
205
+ function staleActiveWorkerRow(row, now) {
206
+ const active = ACTIVE_WORKER_STATUS.test(clean(row?.status))
207
+ || ACTIVE_WORKER_STATUS.test(clean(row?.stage));
208
+ if (!active) return false;
209
+ if (positiveInt(row.runtimePid) && !runtimeAlive(row.runtimePid)) return true;
210
+ if (workerHeartbeatFresh(row.sessionId, now)) return false;
211
+ const updated = Date.parse(clean(row.updatedAt)) || 0;
212
+ return !(updated > 0 && now - updated <= WORKER_POOL_FRESH_MS);
213
+ }
214
+
215
+ function terminalReapAt(row, now) {
216
+ let reapMs = null;
217
+ try { reapMs = resolveAgentTerminalReapMs(cfgMod.loadConfig(), row?.provider); }
218
+ catch { reapMs = null; }
219
+ return reapMs == null ? null : new Date(now + reapMs).toISOString();
220
+ }
221
+
222
+ function idleWorkerRow(row, now, touch = false) {
223
+ const stamp = new Date(now).toISOString();
224
+ return {
225
+ ...row,
226
+ status: 'idle',
227
+ stage: 'idle',
228
+ turnStartedAt: null,
229
+ finishedAt: touch ? stamp : (clean(row.finishedAt) || clean(row.updatedAt) || stamp),
230
+ updatedAt: touch ? stamp : (clean(row.updatedAt) || stamp),
231
+ reapAt: touch ? terminalReapAt(row, now) : (clean(row.reapAt) || terminalReapAt(row, now)),
232
+ };
233
+ }
234
+
162
235
  // Spawn-path writes are batched onto one microtask so a parallel fanout pays
163
236
  // a single locked rewrite instead of one per worker.
164
237
  function flushWorkerIndexMutations() {
@@ -197,6 +270,9 @@ export function createWorkerIndex({ dataDir, cfgMod, mgr, tags, tagAgents, tagCw
197
270
  return {
198
271
  tag,
199
272
  sessionId,
273
+ parentSessionId: clean(extra.parentSessionId)
274
+ || clean(session?.parentSessionId)
275
+ || null,
200
276
  ownerSessionId: clean(extra.ownerSessionId || extra.parentSessionId)
201
277
  || clean(session?.ownerSessionId || session?.parentSessionId)
202
278
  || null,
@@ -219,6 +295,7 @@ export function createWorkerIndex({ dataDir, cfgMod, mgr, tags, tagAgents, tagCw
219
295
  // the new absolute deadline immediately afterwards via scheduleReap().
220
296
  reapAt: clean(extra.reapAt) || null,
221
297
  clientHostPid: positiveInt(extra.clientHostPid) || positiveInt(session?.clientHostPid),
298
+ runtimePid: positiveInt(extra.runtimePid) || process.pid,
222
299
  cwd: clean(session?.cwd) || clean(extra.cwd) || null,
223
300
  task_id: clean(extra.task_id || extra.taskId) || null,
224
301
  error: clean(extra.error) || null,
@@ -234,6 +311,10 @@ export function createWorkerIndex({ dataDir, cfgMod, mgr, tags, tagAgents, tagCw
234
311
  function upsertWorkerRow(row, { defer = false } = {}) {
235
312
  const normalized = normalizeWorkerRows({ workers: [row] })[0];
236
313
  if (!normalized) return false;
314
+ const key = workerRowKey(normalized);
315
+ if (ACTIVE_WORKER_STATUS.test(normalized.status)
316
+ || ACTIVE_WORKER_STATUS.test(normalized.stage)) activeWorkerKeys.add(key);
317
+ else activeWorkerKeys.delete(key);
237
318
  const bindTag = !isLeadPoolAgent(normalized.agent);
238
319
  if (bindTag) {
239
320
  tags.set(normalized.tag, normalized.sessionId);
@@ -288,6 +369,45 @@ export function createWorkerIndex({ dataDir, cfgMod, mgr, tags, tagAgents, tagCw
288
369
  return rows;
289
370
  }
290
371
 
372
+ function flushActiveWorkerRows() {
373
+ if (!activeWorkerKeys.size) return;
374
+ const keys = [...activeWorkerKeys];
375
+ activeWorkerKeys.clear();
376
+ const now = Date.now();
377
+ writeWorkerRows((byKey) => {
378
+ for (const key of keys) {
379
+ const current = byKey.get(key);
380
+ if (!current || positiveInt(current.runtimePid) !== process.pid) continue;
381
+ const active = ACTIVE_WORKER_STATUS.test(clean(current.status))
382
+ || ACTIVE_WORKER_STATUS.test(clean(current.stage));
383
+ if (active) byKey.set(key, idleWorkerRow(current, now, true));
384
+ }
385
+ });
386
+ }
387
+
388
+ function recoverStaleWorkerRows() {
389
+ const now = Date.now();
390
+ const stale = readAllWorkerRows().filter((row) => staleActiveWorkerRow(row, now));
391
+ if (!stale.length) return false;
392
+ writeWorkerRows((byKey) => {
393
+ for (const row of stale) {
394
+ const key = workerRowKey(row);
395
+ const current = byKey.get(key);
396
+ if (!current || !staleActiveWorkerRow(current, now)) continue;
397
+ byKey.set(key, idleWorkerRow(current, now));
398
+ }
399
+ });
400
+ return true;
401
+ }
402
+
403
+ function flushWorkerIndexOnExit() {
404
+ flushWorkerIndexMutations();
405
+ flushActiveWorkerRows();
406
+ }
407
+
408
+ recoverStaleWorkerRows();
409
+ registerExitFlush(flushWorkerIndexOnExit);
410
+
291
411
  return {
292
412
  workerIndexPath,
293
413
  invalidateWorkerRowsCache: invalidateCache,
@@ -304,5 +424,8 @@ export function createWorkerIndex({ dataDir, cfgMod, mgr, tags, tagAgents, tagCw
304
424
  upsertWorkerSessionDeferred,
305
425
  removeWorkerRow,
306
426
  refreshTagsFromIndex,
427
+ flushActiveWorkerRows,
428
+ flushWorkerIndexOnExit,
429
+ recoverStaleWorkerRows,
307
430
  };
308
431
  }
@@ -0,0 +1,132 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import test from 'node:test';
6
+
7
+ import { listStoredAgentWorkers } from '../../runtime/agent/orchestrator/session/store-summary-reader.mjs';
8
+ import { createWorkerIndex } from './worker-index.mjs';
9
+
10
+ function workerIndex(root) {
11
+ return createWorkerIndex({
12
+ dataDir: root,
13
+ cfgMod: { loadConfig: () => ({}) },
14
+ mgr: { getSessionRuntime: () => null },
15
+ tags: new Map(),
16
+ tagAgents: new Map(),
17
+ tagCwds: new Map(),
18
+ });
19
+ }
20
+
21
+ function writeWorkers(root, workers) {
22
+ writeFileSync(join(root, 'agent-workers.json'), JSON.stringify({ workers }));
23
+ }
24
+
25
+ test('worker boot recovery settles a running row whose runtime died', () => {
26
+ const root = mkdtempSync(join(tmpdir(), 'mixdog-worker-recover-'));
27
+ const previous = process.env.MIXDOG_DATA_DIR;
28
+ process.env.MIXDOG_DATA_DIR = root;
29
+ try {
30
+ mkdirSync(join(root, 'sessions'));
31
+ const now = Date.now();
32
+ writeWorkers(root, {
33
+ crashed: {
34
+ tag: 'crashed',
35
+ sessionId: 'crashed',
36
+ ownerSessionId: 'lead-a',
37
+ agent: 'worker',
38
+ status: 'running',
39
+ stage: 'running',
40
+ runtimePid: 0x7fffffff,
41
+ turnStartedAt: new Date(now - 5_000).toISOString(),
42
+ updatedAt: new Date(now - 5_000).toISOString(),
43
+ },
44
+ live: {
45
+ tag: 'live',
46
+ sessionId: 'live',
47
+ ownerSessionId: 'lead-a',
48
+ agent: 'worker',
49
+ status: 'running',
50
+ stage: 'running',
51
+ runtimePid: process.pid,
52
+ turnStartedAt: new Date(now - 5_000).toISOString(),
53
+ updatedAt: new Date(now - 5_000).toISOString(),
54
+ },
55
+ });
56
+
57
+ workerIndex(root);
58
+
59
+ const stored = JSON.parse(readFileSync(join(root, 'agent-workers.json'), 'utf8'));
60
+ assert.equal(stored.workers.crashed.status, 'idle');
61
+ assert.equal(stored.workers.crashed.stage, 'idle');
62
+ assert.equal(stored.workers.crashed.turnStartedAt, null);
63
+ assert.equal(stored.workers.live.status, 'running');
64
+
65
+ const pool = listStoredAgentWorkers();
66
+ assert.equal(pool.find((row) => row.sessionId === 'crashed')?.status, 'idle');
67
+ assert.equal(pool.find((row) => row.sessionId === 'live')?.status, 'running');
68
+ } finally {
69
+ if (previous === undefined) delete process.env.MIXDOG_DATA_DIR;
70
+ else process.env.MIXDOG_DATA_DIR = previous;
71
+ rmSync(root, { recursive: true, force: true });
72
+ }
73
+ });
74
+
75
+ test('cold pool reader does not trust an expired running worker row', () => {
76
+ const root = mkdtempSync(join(tmpdir(), 'mixdog-worker-cold-stale-'));
77
+ const previous = process.env.MIXDOG_DATA_DIR;
78
+ process.env.MIXDOG_DATA_DIR = root;
79
+ try {
80
+ mkdirSync(join(root, 'sessions'));
81
+ const old = new Date(Date.now() - 3 * 60 * 1000).toISOString();
82
+ writeWorkers(root, {
83
+ stale: {
84
+ tag: 'stale',
85
+ sessionId: 'stale',
86
+ ownerSessionId: 'lead-a',
87
+ agent: 'worker',
88
+ status: 'running',
89
+ stage: 'running',
90
+ turnStartedAt: old,
91
+ updatedAt: old,
92
+ },
93
+ });
94
+
95
+ const row = listStoredAgentWorkers().find((entry) => entry.sessionId === 'stale');
96
+ assert.equal(row?.status, 'idle');
97
+ assert.equal(row?.stage, 'idle');
98
+ assert.equal(row?.turnStartedAt, null);
99
+ } finally {
100
+ if (previous === undefined) delete process.env.MIXDOG_DATA_DIR;
101
+ else process.env.MIXDOG_DATA_DIR = previous;
102
+ rmSync(root, { recursive: true, force: true });
103
+ }
104
+ });
105
+
106
+ test('worker exit flush commits queued state before settling active work', () => {
107
+ const root = mkdtempSync(join(tmpdir(), 'mixdog-worker-exit-flush-'));
108
+ try {
109
+ mkdirSync(join(root, 'sessions'));
110
+ const index = workerIndex(root);
111
+ index.upsertWorkerSessionDeferred({
112
+ id: 'exit-worker',
113
+ ownerSessionId: 'lead-a',
114
+ agentTag: 'exit-worker',
115
+ agent: 'worker',
116
+ createdAt: new Date().toISOString(),
117
+ }, 'exit-worker', {
118
+ status: 'running',
119
+ stage: 'running',
120
+ turnStartedAt: new Date().toISOString(),
121
+ });
122
+
123
+ index.flushWorkerIndexOnExit();
124
+
125
+ const stored = JSON.parse(readFileSync(join(root, 'agent-workers.json'), 'utf8'));
126
+ assert.equal(stored.workers['exit-worker'].status, 'idle');
127
+ assert.equal(stored.workers['exit-worker'].stage, 'idle');
128
+ assert.equal(stored.workers['exit-worker'].turnStartedAt, null);
129
+ } finally {
130
+ rmSync(root, { recursive: true, force: true });
131
+ }
132
+ });
@@ -74,7 +74,7 @@ export function applyWorkerRowUpsert(byKey, normalized) {
74
74
  if (!key) return;
75
75
  const prev = byKey.get(key) || {};
76
76
  const merged = { ...prev, ...normalized };
77
- for (const field of ['ownerSessionId', 'agent', 'provider', 'model', 'preset', 'effort', 'fast', 'clientHostPid', 'cwd', 'task_id', 'permission', 'toolPermission', 'turnStartedAt']) {
77
+ for (const field of ['parentSessionId', 'ownerSessionId', 'agent', 'provider', 'model', 'preset', 'effort', 'fast', 'clientHostPid', 'runtimePid', 'cwd', 'task_id', 'permission', 'toolPermission', 'turnStartedAt']) {
78
78
  if ((merged[field] === null || merged[field] === '') && prev[field] != null && prev[field] !== '') {
79
79
  merged[field] = prev[field];
80
80
  }
@@ -89,6 +89,7 @@ export function applyWorkerRowUpsert(byKey, normalized) {
89
89
  export function workerRowToSession(row = {}) {
90
90
  return {
91
91
  id: row.sessionId,
92
+ parentSessionId: row.parentSessionId || row.ownerSessionId || null,
92
93
  ownerSessionId: row.ownerSessionId || null,
93
94
  agentTag: row.tag,
94
95
  agent: row.agent || null,
@@ -97,10 +97,13 @@ export function createStandaloneAgent({
97
97
  mcpScopeId = null,
98
98
  onSubagentEvent,
99
99
  notifySessionCompletion,
100
+ sessionSurface = null,
100
101
  awaitKeychainPrewarm = async () => {},
101
102
  isKeychainPrewarmReady = () => true,
102
103
  }) {
103
104
  const mgr = baseMgr;
105
+ const canUseSessionSurface = (session) => typeof sessionSurface?.runTurn === 'function'
106
+ && sessionSurface.canRun?.(session) !== false;
104
107
  const statusListeners = new Set();
105
108
  const notifyStatusChange = () => {
106
109
  for (const listener of [...statusListeners]) {
@@ -118,6 +121,8 @@ export function createStandaloneAgent({
118
121
  notifyContext?.callerSessionId
119
122
  || notifyContext?.sessionId
120
123
  || notifyContext?.routingSessionId
124
+ || session?.parentSessionId
125
+ || notifyContext?.ownerSessionId
121
126
  || session?.ownerSessionId,
122
127
  );
123
128
  const handle = beginAgentTurnReview(ownerSessionId, session?.id, { tag, agent });
@@ -230,6 +235,7 @@ export function createStandaloneAgent({
230
235
  emitSubagentEvent,
231
236
  notifyStatusChange,
232
237
  notifySessionCompletion,
238
+ sessionSurface,
233
239
  scheduleReap,
234
240
  cfgMod,
235
241
  dataDir,
@@ -288,11 +294,12 @@ export function createStandaloneAgent({
288
294
  provider: session.provider,
289
295
  model: session.model,
290
296
  content: result?.content || '',
297
+ ...(sessionSurface?.canonical === true ? { handoffOnly: true } : {}),
291
298
  ...(abnormalError ? { error: abnormalError } : {}),
292
299
  };
293
300
  };
294
301
  handoffMsgStart = resolveHandoffMessageStartIndex(mgr.getSession(sessionId));
295
- const result = await mgr.askSession(sessionId, prompt, args.context || null, null, session.cwd || defaultCwd, null, {
302
+ const turnHooks = {
296
303
  notifyFn: workerNotifyFn(sessionId, notifyContext || {}),
297
304
  onToolResult: (message) => turnReview.onToolResult(message),
298
305
  ...(job ? {
@@ -304,7 +311,24 @@ export function createStandaloneAgent({
304
311
  reconcileJobTerminalResult(job, value);
305
312
  },
306
313
  } : {}),
307
- });
314
+ };
315
+ const result = canUseSessionSurface(session)
316
+ ? await sessionSurface.runTurn({
317
+ session,
318
+ prompt,
319
+ context: args.context || null,
320
+ cwd: session.cwd || defaultCwd,
321
+ ...turnHooks,
322
+ })
323
+ : await mgr.askSession(
324
+ sessionId,
325
+ prompt,
326
+ args.context || null,
327
+ null,
328
+ session.cwd || defaultCwd,
329
+ null,
330
+ turnHooks,
331
+ );
308
332
  // Early preview no longer suppresses the canonical body notification;
309
333
  // notifyTaskCompletion fires once with output via resolve/reconcile.
310
334
  const finalValue = completionValue(result);
@@ -359,7 +383,9 @@ export function createStandaloneAgent({
359
383
  // live session (reuse path). Busy sessions queue the prompt; idle ones run a
360
384
  // background send job that continues the existing session (context kept).
361
385
  function dispatchToExistingSession(prepared, notifyContext, extras = {}) {
362
- if (isSessionBusy(prepared.sessionId) && typeof mgr.enqueuePendingMessage === 'function') {
386
+ if (!canUseSessionSurface(prepared.session)
387
+ && isSessionBusy(prepared.sessionId)
388
+ && typeof mgr.enqueuePendingMessage === 'function') {
363
389
  const queueDepth = mgr.enqueuePendingMessage(prepared.sessionId, prepared.prompt);
364
390
  return renderResult({
365
391
  queued: true,
@@ -383,7 +409,7 @@ export function createStandaloneAgent({
383
409
  return renderResult({ ...extras, ...renderJob(job, false) });
384
410
  }
385
411
 
386
- function close(args, context = {}) {
412
+ async function close(args, context = {}) {
387
413
  const scopedContext = agentScope(args, context);
388
414
  refreshTagsFromSessions({ scanSessions: wantsSessionScan(args), context: scopedContext });
389
415
  const taskId = taskIdFromArgs(args);
@@ -415,6 +441,9 @@ export function createStandaloneAgent({
415
441
  throw new Error(`agent close: target "${target}" is a Lead session`);
416
442
  }
417
443
  cancelReap(sessionId);
444
+ const descendantSessionIds = typeof mgr.descendantSessionIds === 'function'
445
+ ? mgr.descendantSessionIds(sessionId)
446
+ : [];
418
447
  const tag = tagForSession(sessionId);
419
448
  clearAgentStatuslineRoute(sessionId);
420
449
  // Cancel any running background task bound to this session BEFORE closing
@@ -430,13 +459,18 @@ export function createStandaloneAgent({
430
459
  // Close (and stamp cancelStatus) BEFORE dropping the worker row. Removing
431
460
  // the row first left the next pool read with only a leftover heartbeat,
432
461
  // which republished the session as `running`.
433
- const ok = mgr.closeSession(sessionId, 'cli-agent-close');
462
+ const ok = await mgr.closeSession(sessionId, 'cli-agent-close');
434
463
  if (task?.taskId) cancelBackgroundTask(task.taskId, 'cancelled by agent close');
435
464
  // Drop the row only once the cancellation is DURABLY recorded. A close that
436
465
  // failed still forgot the row, leaving the next pool read with a fresh
437
466
  // heartbeat and no cancelled state — which republished the session as
438
467
  // `running`, the exact lingering row this whole path exists to prevent.
439
468
  if (ok) forgetTerminalSession(tag, sessionId);
469
+ if (ok) {
470
+ for (const descendantSessionId of descendantSessionIds) {
471
+ removeWorkerRow({ sessionId: descendantSessionId });
472
+ }
473
+ }
440
474
  return { closed: ok, tag, sessionId, task_id: task?.taskId || null };
441
475
  }
442
476
 
@@ -455,7 +489,7 @@ export function createStandaloneAgent({
455
489
  };
456
490
  }
457
491
 
458
- function closeAll(reason = 'cli-agent-close-all', scope = {}) {
492
+ async function closeAll(reason = 'cli-agent-close-all', scope = {}) {
459
493
  // Scoped teardown (one Lead closing/deleting/switching) must close ONLY
460
494
  // that Lead's workers: the worker index and task registry are shared by
461
495
  // every Lead in the process, and the old unscoped sweep wiped sibling
@@ -467,7 +501,7 @@ export function createStandaloneAgent({
467
501
  const failed = [];
468
502
  for (const { tag, session } of agentSessionEntries({ scanSessions: false, context })) {
469
503
  try {
470
- closed.push(close({ sessionId: session.id }, context));
504
+ closed.push(await close({ sessionId: session.id }, context));
471
505
  } catch (err) {
472
506
  failed.push({ tag, error: presentErrorText(err, { surface: 'agent' }) });
473
507
  }
@@ -538,12 +572,25 @@ export function createStandaloneAgent({
538
572
  const callerCwd = clean(context.cwd || context.callerCwd);
539
573
  const scopedContext = agentScope(args, context);
540
574
  const notifyContext = context;
575
+ if (typeof mgr.rehydrateAgentSessions === 'function') {
576
+ await mgr.rehydrateAgentSessions();
577
+ // Rebuild tag ownership from canonical records even if a daemon crash
578
+ // happened before the auxiliary worker row was flushed.
579
+ refreshTagsFromSessions({ scanSessions: true, context: scopedContext });
580
+ }
541
581
  if (type === 'list') return renderResult({ workers: list({ scanSessions: wantsSessionScan(args), context: scopedContext }), jobs: listJobs(scopedContext) });
542
582
  if (type === 'status') return renderResult(renderJob(getJobOrWorker(args, scopedContext), false));
543
- if (type === 'read') return renderResult(renderJob(getJobOrWorker(args, scopedContext), true));
583
+ if (type === 'read') {
584
+ const job = getJobOrWorker(args, scopedContext);
585
+ if (job?.taskId == null && typeof mgr.readSessionHandoff === 'function') {
586
+ const handoff = await mgr.readSessionHandoff(job.meta?.sessionId);
587
+ if (handoff) job.result = handoff;
588
+ }
589
+ return renderResult(renderJob(job, true));
590
+ }
544
591
  if (type === 'cleanup') return renderResult(cleanup(args, scopedContext));
545
- if (type === 'cancel') return renderResult(close(args, scopedContext));
546
- if (type === 'close') return renderResult(close(args, scopedContext));
592
+ if (type === 'cancel') return renderResult(await close(args, scopedContext));
593
+ if (type === 'close') return renderResult(await close(args, scopedContext));
547
594
  if (type === 'send') {
548
595
  try {
549
596
  const prepared = await prepareSend(args, scopedContext);