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
@@ -20,10 +20,21 @@ import {
20
20
  materializePromptSubmission,
21
21
  preparePromptSubmissionForProvider,
22
22
  } from '../runtime/attachments/store.mjs';
23
- import { hasActiveBackgroundTasks } from '../runtime/shared/background-tasks.mjs';
23
+ import {
24
+ cancelBackgroundTasks,
25
+ hasActiveBackgroundTasks,
26
+ } from '../runtime/shared/background-tasks.mjs';
24
27
 
25
28
  const MAX_CLONE_DEPTH = 24;
29
+ const SESSION_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
26
30
  const requireDesktopService = createRequire(import.meta.url);
31
+ const EXTERNAL_SESSION_ACTIONS = new Set([
32
+ ...SESSION_READ_ACTION_SET,
33
+ ...SESSION_CONFIGURE_ACTION_SET,
34
+ 'submitAsync',
35
+ 'abort',
36
+ 'resolveToolApproval',
37
+ ]);
27
38
 
28
39
  async function loadDesktopServiceModule(moduleUrl) {
29
40
  const parsed = new URL(moduleUrl);
@@ -97,6 +108,11 @@ export function createSessionService({
97
108
  createSessionRuntime = null,
98
109
  sessionExists = null,
99
110
  readStoredSession = null,
111
+ readStoredGoal = null,
112
+ listStoredActiveGoalSessionIds = null,
113
+ subscribeExternalSessionStates = null,
114
+ invokeExternalSessionAction = null,
115
+ readExternalSessionState = null,
100
116
  listSessions = null,
101
117
  getRemoteSessionState = null,
102
118
  desktopRuntime = null,
@@ -123,6 +139,15 @@ export function createSessionService({
123
139
  // addressed by clients; sessionId is the only identity outside this module.
124
140
  const sessions = new Set();
125
141
  const sessionsById = new Map();
142
+ // Agent children are catalog metadata over ordinary daemon-owned sessions.
143
+ // Their transcript/execution state remains exclusively in `sessionsById`;
144
+ // this index carries only the Parent–Child relationship and public Agent
145
+ // routing fields needed before/after a turn.
146
+ const agentSessions = new Map();
147
+ const agentChildren = new Map();
148
+ const agentCancelRuns = new Map();
149
+ let agentRehydrated = false;
150
+ let agentRehydratePromise = null;
126
151
  let busyEntries = 0;
127
152
  // Desktop adapters are keyed by their exact module URL so a dev preview and
128
153
  // installed build never share module state accidentally.
@@ -138,7 +163,7 @@ export function createSessionService({
138
163
  // destroying anything, this sweep is the ONLY reclaim path besides shutdown.
139
164
  const IDLE_EVICT_MS = Number(idleEvictMs) > 0
140
165
  ? Number(idleEvictMs)
141
- : Math.max(60_000, Number(process.env.MIXDOG_SESSION_IDLE_EVICT_MS) || 2 * 60_000);
166
+ : Math.max(60_000, Number(process.env.MIXDOG_SESSION_IDLE_EVICT_MS) || 5 * 60_000);
142
167
  const EVICT_SWEEP_MS = Number(evictSweepMs) > 0 ? Number(evictSweepMs) : 30_000;
143
168
  let evictTimer = null;
144
169
 
@@ -170,6 +195,7 @@ export function createSessionService({
170
195
  // the entry the moment one materializes, so its live frames start flowing
171
196
  // without a second subscribe round-trip.
172
197
  const pendingViewers = new Map(); // sessionId -> Set<clientToken>
198
+ const externalViewEntries = new Map(); // sessionId -> ordinary projected entry
173
199
 
174
200
  function trackPendingViewer(sessionId, ctx) {
175
201
  const token = subscriberToken(ctx);
@@ -216,6 +242,10 @@ export function createSessionService({
216
242
  startEvictionSweep();
217
243
  log(`session ${currentSessionId(entry) || '(creating)'} unwatched (client ${token} gone) — retained`);
218
244
  }
245
+ for (const [sessionId, entry] of [...externalViewEntries]) {
246
+ entry.subscribers?.delete(token);
247
+ if ((entry.subscribers?.size || 0) === 0) externalViewEntries.delete(sessionId);
248
+ }
219
249
  for (const service of desktopServicesById.values()) {
220
250
  service.subscribers.delete(token);
221
251
  }
@@ -428,11 +458,22 @@ export function createSessionService({
428
458
  if (entry) entry.indexedSessionId = '';
429
459
  return '';
430
460
  }
461
+ // External agent projections use the same frame machinery but are not a
462
+ // daemon execution owner. Keeping them out of sessionsById lets a later
463
+ // ordinary session materialization adopt the viewers and take authority.
464
+ if (entry.externalView === true) return nextId;
431
465
  const existing = sessionsById.get(nextId);
432
466
  if (existing && existing !== entry && !existing.disposed) {
433
467
  // Do not redirect an established address to a second session runtime. The load
434
468
  throw new Error(`duplicate session address: ${nextId}`);
435
469
  }
470
+ const external = externalViewEntries.get(nextId);
471
+ if (external) {
472
+ externalViewEntries.delete(nextId);
473
+ for (const token of external.subscribers || []) {
474
+ addSubscriber(entry, { clientToken: token });
475
+ }
476
+ }
436
477
  sessionsById.set(nextId, entry);
437
478
  entry.indexedSessionId = nextId;
438
479
  return nextId;
@@ -460,6 +501,72 @@ export function createSessionService({
460
501
  }, entry.subscribers);
461
502
  }
462
503
 
504
+ function externalEntryForView(sessionId) {
505
+ return externalViewEntries.get(String(sessionId || '')) || null;
506
+ }
507
+
508
+ function publishExternalSessionState(update) {
509
+ const sessionId = String(update?.sessionId || '');
510
+ const snapshot = update?.snapshot;
511
+ if (!sessionId || !snapshot || typeof snapshot !== 'object') return;
512
+ // A daemon-owned runtime is the canonical owner if this address was
513
+ // materialized. External agent projection frames can arrive one tick late
514
+ // after that promotion and must not overwrite it.
515
+ if (sessionOwner(sessionId)) return;
516
+ let entry = externalViewEntries.get(sessionId);
517
+ if (!entry) {
518
+ if (!pendingViewers.has(sessionId)) return;
519
+ let state = { ...snapshot, sessionId };
520
+ const runtime = {
521
+ isWireSafe: true,
522
+ externalAction: typeof invokeExternalSessionAction === 'function',
523
+ getState: () => state,
524
+ setState: (next) => { state = next; },
525
+ };
526
+ Object.defineProperties(runtime, {
527
+ id: { get: () => sessionId },
528
+ provider: { get: () => String(state.provider || '') },
529
+ model: { get: () => String(state.model || '') },
530
+ session: {
531
+ get: () => ({
532
+ id: sessionId,
533
+ provider: String(state.provider || ''),
534
+ model: String(state.model || ''),
535
+ }),
536
+ },
537
+ });
538
+ if (typeof invokeExternalSessionAction === 'function') {
539
+ for (const name of EXTERNAL_SESSION_ACTIONS) {
540
+ runtime[name] = (...args) => invokeExternalSessionAction(sessionId, name, args);
541
+ }
542
+ }
543
+ entry = {
544
+ runtime,
545
+ subscribers: new Set(),
546
+ disposed: false,
547
+ timer: null,
548
+ lastPublishedAt: 0,
549
+ publishedSessionId: '',
550
+ indexedSessionId: '',
551
+ addressedSessionId: sessionId,
552
+ revision: revisionEpoch,
553
+ busy: null,
554
+ externalView: true,
555
+ };
556
+ externalViewEntries.set(sessionId, entry);
557
+ adoptPendingViewers(entry, sessionId);
558
+ } else {
559
+ entry.runtime.setState({ ...snapshot, sessionId });
560
+ }
561
+ const step = advance(entry);
562
+ if (step.changed) publishStep(entry, step);
563
+ }
564
+
565
+ const unsubscribeExternalSessionStates =
566
+ typeof subscribeExternalSessionStates === 'function'
567
+ ? subscribeExternalSessionStates(publishExternalSessionState)
568
+ : () => {};
569
+
463
570
  /** Response body for the CALLER, which announced the revision it holds. */
464
571
  function bodyForClient(step, baseRevision) {
465
572
  if (!step.changed && baseRevision === step.revision) return { revision: step.revision };
@@ -522,9 +629,13 @@ export function createSessionService({
522
629
  cwd: params.cwd || process.cwd(),
523
630
  provider: params.provider,
524
631
  model: params.model,
632
+ effort: params.effort,
633
+ fast: params.fast,
634
+ modelParameters: params.modelParameters,
525
635
  toolMode: params.toolMode || 'full',
526
636
  remote: params.remote === true,
527
637
  desktopSession: params.desktopSession ?? null,
638
+ sessionProfile: params.sessionProfile ?? null,
528
639
  });
529
640
  const entry = {
530
641
  runtime, cwd: params.cwd || process.cwd(), timer: null, disposed: false,
@@ -595,11 +706,64 @@ export function createSessionService({
595
706
  return entry;
596
707
  }
597
708
 
709
+ /** Bind a retained external Agent snapshot into the ordinary view entry
710
+ * without materializing a daemon runtime. publishExternalSessionState
711
+ * adopts pending viewers so a completed turn is visible immediately. */
712
+ async function bindExternalSessionView(sessionId) {
713
+ const id = String(sessionId || '');
714
+ if (!id) return null;
715
+ const owner = sessionOwner(id);
716
+ if (owner) return owner;
717
+ const existing = externalEntryForView(id);
718
+ if (existing) return existing;
719
+ if (typeof readExternalSessionState !== 'function') return null;
720
+
721
+ // Register the address before the retained-snapshot read. Even a
722
+ // synchronous reader crosses an `await` boundary, so a newer external
723
+ // frame can otherwise arrive in that microtask, find no pending viewer,
724
+ // and be dropped before the stale retained snapshot is bound.
725
+ const placeholder = new Set();
726
+ const ownsPlaceholder = !pendingViewers.has(id);
727
+ if (ownsPlaceholder) pendingViewers.set(id, placeholder);
728
+ const clearPlaceholder = () => {
729
+ if (ownsPlaceholder
730
+ && pendingViewers.get(id) === placeholder
731
+ && placeholder.size === 0) {
732
+ pendingViewers.delete(id);
733
+ }
734
+ };
735
+
736
+ let snapshot = null;
737
+ try {
738
+ snapshot = await readExternalSessionState(id);
739
+ } catch (error) {
740
+ log(`external session state read failed session=${id}: ${error?.message || error}`);
741
+ const raced = sessionOwner(id) || externalEntryForView(id);
742
+ clearPlaceholder();
743
+ return raced;
744
+ }
745
+ const raced = sessionOwner(id) || externalEntryForView(id);
746
+ if (raced) {
747
+ clearPlaceholder();
748
+ return raced;
749
+ }
750
+ if (!snapshot || typeof snapshot !== 'object') {
751
+ clearPlaceholder();
752
+ return null;
753
+ }
754
+ publishExternalSessionState({ sessionId: id, snapshot });
755
+ const bound = sessionOwner(id) || externalEntryForView(id);
756
+ if (!bound) clearPlaceholder();
757
+ return bound;
758
+ }
759
+
598
760
  /** The runtime hosting sessionId: the existing owner, or a fresh load. One
599
761
  * load per session at a time — concurrent panes converge on one session runtime. */
600
762
  async function entryForSession(sessionId, hints = {}) {
601
763
  const owner = sessionOwner(sessionId);
602
764
  if (owner) return owner;
765
+ const external = await bindExternalSessionView(sessionId);
766
+ if (external?.runtime?.externalAction === true) return external;
603
767
  const inFlight = sessionLoads.get(sessionId);
604
768
  if (inFlight) return inFlight;
605
769
  let loading;
@@ -646,9 +810,19 @@ export function createSessionService({
646
810
  return null;
647
811
  }
648
812
  if (!snapshot || typeof snapshot !== 'object') return null;
813
+ let goal;
814
+ if (typeof readStoredGoal === 'function') {
815
+ try {
816
+ goal = await readStoredGoal(sessionId) ?? null;
817
+ } catch (err) {
818
+ log(`stored Goal projection failed session=${sessionId}: ${err?.message || err}`);
819
+ goal = null;
820
+ }
821
+ }
649
822
  return sanitizeForWire({
650
823
  ...snapshot,
651
824
  sessionId,
825
+ ...(typeof readStoredGoal === 'function' ? { goal } : {}),
652
826
  queued: Array.isArray(snapshot.queued) ? snapshot.queued : [],
653
827
  });
654
828
  }
@@ -744,7 +918,12 @@ export function createSessionService({
744
918
  if (typeof listSessions !== 'function') {
745
919
  throw new Error('session catalog is unavailable');
746
920
  }
747
- const sessions = await listSessions(options || {});
921
+ const sessions = await listSessions({
922
+ ...(options || {}),
923
+ // Agent-only records are an internal ancestry/reuse source, never part
924
+ // of the ordinary session catalog returned over the public transport.
925
+ includeAgentOnly: false,
926
+ });
748
927
  const remoteSession = typeof getRemoteSessionState === 'function'
749
928
  ? await getRemoteSessionState()
750
929
  : null;
@@ -911,7 +1090,9 @@ export function createSessionService({
911
1090
  }
912
1091
  const id = String(sessionId || '');
913
1092
  if (!id) throw new TypeError('sessionId is required');
914
- const live = await liveEntryForView(id);
1093
+ const live = await liveEntryForView(id)
1094
+ || externalEntryForView(id)
1095
+ || await bindExternalSessionView(id);
915
1096
  if (live) {
916
1097
  const step = advance(live);
917
1098
  retainUnwatched(live, 'headless session read');
@@ -922,7 +1103,7 @@ export function createSessionService({
922
1103
  }
923
1104
  if (typeof readStoredSession === 'function') {
924
1105
  const projection = await storedSessionProjection(id, openHints);
925
- const lateOwner = sessionOwner(id);
1106
+ const lateOwner = sessionOwner(id) || externalEntryForView(id);
926
1107
  if (lateOwner) {
927
1108
  const step = advance(lateOwner);
928
1109
  retainUnwatched(lateOwner, 'headless session read');
@@ -952,7 +1133,9 @@ export function createSessionService({
952
1133
  ) {
953
1134
  const id = String(sessionId || '');
954
1135
  if (!id) throw new TypeError('sessionId is required');
955
- const live = await liveEntryForView(id);
1136
+ const live = await liveEntryForView(id)
1137
+ || externalEntryForView(id)
1138
+ || await bindExternalSessionView(id);
956
1139
  if (live) {
957
1140
  addSubscriber(live, ctx);
958
1141
  const step = advance(live);
@@ -964,7 +1147,7 @@ export function createSessionService({
964
1147
  // guarantees either adoption or the live re-check below.
965
1148
  trackPendingViewer(id, ctx);
966
1149
  const projection = await storedSessionProjection(id, openHints);
967
- const lateOwner = sessionOwner(id);
1150
+ const lateOwner = sessionOwner(id) || externalEntryForView(id);
968
1151
  if (lateOwner) {
969
1152
  dropPendingViewer(id, ctx);
970
1153
  addSubscriber(lateOwner, ctx);
@@ -988,7 +1171,15 @@ export function createSessionService({
988
1171
  if (!id) throw new TypeError('sessionId is required');
989
1172
  dropPendingViewer(id, ctx);
990
1173
  const owner = sessionOwner(id);
991
- if (!owner) return { sessionId: id, unsubscribed: true };
1174
+ if (!owner) {
1175
+ const external = externalEntryForView(id);
1176
+ if (external) {
1177
+ const token = subscriberToken(ctx);
1178
+ if (token) external.subscribers?.delete(token);
1179
+ if ((external.subscribers?.size || 0) === 0) externalViewEntries.delete(id);
1180
+ }
1181
+ return { sessionId: id, unsubscribed: true };
1182
+ }
992
1183
  const entry = owner;
993
1184
  const token = subscriberToken(ctx);
994
1185
  if (token) entry.subscribers?.delete(token);
@@ -1022,7 +1213,18 @@ export function createSessionService({
1022
1213
  // Await intake only: submitAsync resolves once the prompt is represented by
1023
1214
  // the queue/user row, while provider execution remains daemon-owned and
1024
1215
  // detached.
1025
- const accepted = await Promise.resolve(target.call(entry.runtime, intake.prompt, intake.options));
1216
+ const submissionOptions = entry.runtime.externalAction === true
1217
+ ? {
1218
+ ...intake.options,
1219
+ transcriptMeta: {
1220
+ ...(intake.options?.transcriptMeta && typeof intake.options.transcriptMeta === 'object'
1221
+ ? intake.options.transcriptMeta
1222
+ : {}),
1223
+ sender: 'user',
1224
+ },
1225
+ }
1226
+ : intake.options;
1227
+ const accepted = await Promise.resolve(target.call(entry.runtime, intake.prompt, submissionOptions));
1026
1228
  const firstSubmit = accepted === true && entry.reservedOnly;
1027
1229
  if (accepted === true) {
1028
1230
  entry.reservedOnly = false;
@@ -1047,6 +1249,42 @@ export function createSessionService({
1047
1249
  return entry.runtime;
1048
1250
  }
1049
1251
 
1252
+ async function recoverActiveGoals() {
1253
+ if (typeof listStoredActiveGoalSessionIds !== 'function') {
1254
+ return { found: 0, resumed: 0, skipped: 0, failed: 0 };
1255
+ }
1256
+ let listed;
1257
+ try {
1258
+ listed = await listStoredActiveGoalSessionIds();
1259
+ } catch (err) {
1260
+ log(`active Goal discovery failed: ${err?.message || err}`);
1261
+ return { found: 0, resumed: 0, skipped: 0, failed: 1 };
1262
+ }
1263
+ const sessionIds = [...new Set(Array.isArray(listed) ? listed : [])]
1264
+ .map((sessionId) => String(sessionId || ''))
1265
+ .filter((sessionId) => SESSION_ID_PATTERN.test(sessionId));
1266
+ let resumed = 0;
1267
+ let skipped = 0;
1268
+ let failed = 0;
1269
+ for (const sessionId of sessionIds) {
1270
+ try {
1271
+ if (typeof readStoredGoal === 'function') {
1272
+ const goal = await readStoredGoal(sessionId);
1273
+ if (goal?.status !== 'active') {
1274
+ skipped += 1;
1275
+ continue;
1276
+ }
1277
+ }
1278
+ await materializeSession(sessionId);
1279
+ resumed += 1;
1280
+ } catch (err) {
1281
+ failed += 1;
1282
+ log(`active Goal recovery failed session=${sessionId}: ${err?.message || err}`);
1283
+ }
1284
+ }
1285
+ return { found: sessionIds.length, resumed, skipped, failed };
1286
+ }
1287
+
1050
1288
  async function abortSession({
1051
1289
  sessionId, open: openHints = {}, options = {},
1052
1290
  baseRevision = null,
@@ -1056,7 +1294,32 @@ export function createSessionService({
1056
1294
  const entry = await entryForSession(id, openHints || {});
1057
1295
  const target = entry.runtime.abort;
1058
1296
  if (typeof target !== 'function') throw new TypeError('session action abort is unavailable');
1059
- const rawResult = await target.call(entry.runtime, options || {});
1297
+ const state = entry.runtime.getState?.() || {};
1298
+ const parentId = String(state.parentSessionId || state.ownerSessionId || '').trim();
1299
+ const isLegacyAgentChild = String(state.owner || '').trim().toLowerCase() === 'agent'
1300
+ && String(state.agent || '').trim().toLowerCase() !== 'lead'
1301
+ && parentId
1302
+ && parentId !== id;
1303
+ const abortsAgentTurn = agentSessions.has(id)
1304
+ || String(state.visibility || '').trim().toLowerCase() === 'agent-only'
1305
+ || isLegacyAgentChild;
1306
+ let rawResult;
1307
+ try {
1308
+ rawResult = target.call(entry.runtime, options || {});
1309
+ } finally {
1310
+ // Lead cancellation must leave delegated work alive. An Agent cancelling
1311
+ // its own turn, however, is the live parent signal for Agent work nested
1312
+ // under that turn; task cancellation reaches the child's own controller
1313
+ // without enumerating durable sessions.
1314
+ if (abortsAgentTurn) {
1315
+ cancelBackgroundTasks({
1316
+ surface: 'agent',
1317
+ callerSessionId: id,
1318
+ reason: 'parent Agent turn aborted',
1319
+ });
1320
+ }
1321
+ }
1322
+ rawResult = await rawResult;
1060
1323
  const abortResult = rawResult && typeof rawResult === 'object'
1061
1324
  ? rawResult
1062
1325
  : { aborted: rawResult === true };
@@ -1066,6 +1329,437 @@ export function createSessionService({
1066
1329
  return sessionResult(entry, step, baseRevision, abortResult);
1067
1330
  }
1068
1331
 
1332
+ function linkAgentDescriptor(descriptor) {
1333
+ const sessionId = String(descriptor?.id || '').trim();
1334
+ const parentSessionId = String(descriptor?.parentSessionId || '').trim();
1335
+ if (!sessionId || !parentSessionId) {
1336
+ throw new TypeError('agent child requires session and parent ids');
1337
+ }
1338
+ const previous = agentSessions.get(sessionId);
1339
+ if (previous?.parentSessionId && previous.parentSessionId !== parentSessionId) {
1340
+ agentChildren.get(previous.parentSessionId)?.delete(sessionId);
1341
+ }
1342
+ const linked = {
1343
+ ...(previous || {}),
1344
+ ...descriptor,
1345
+ id: sessionId,
1346
+ parentSessionId,
1347
+ ownerSessionId: String(
1348
+ descriptor.ownerSessionId
1349
+ || previous?.ownerSessionId
1350
+ || agentSessions.get(parentSessionId)?.ownerSessionId
1351
+ || parentSessionId,
1352
+ ),
1353
+ owner: 'agent',
1354
+ visibility: 'agent-only',
1355
+ closed: descriptor.closed === true,
1356
+ };
1357
+ agentSessions.set(sessionId, linked);
1358
+ let children = agentChildren.get(parentSessionId);
1359
+ if (!children) agentChildren.set(parentSessionId, children = new Set());
1360
+ children.add(sessionId);
1361
+ return linked;
1362
+ }
1363
+
1364
+ function validLinkedSessionId(value, ownId = '') {
1365
+ const id = String(value || '').trim();
1366
+ return SESSION_ID_PATTERN.test(id) && id !== ownId ? id : '';
1367
+ }
1368
+
1369
+ function storedAgentCandidate(row) {
1370
+ const id = String(row?.id || '').trim();
1371
+ if (!SESSION_ID_PATTERN.test(id)) return null;
1372
+ const parentSessionId = validLinkedSessionId(
1373
+ row?.parentSessionId || row?.ownerSessionId,
1374
+ id,
1375
+ );
1376
+ if (!parentSessionId) return null;
1377
+ const declaredVisibility = String(
1378
+ row?.visibility || row?.sessionVisibility || '',
1379
+ ).trim().toLowerCase() === 'agent-only';
1380
+ const legacyAgentChild = String(row?.owner || '').trim().toLowerCase() === 'agent';
1381
+ if (!declaredVisibility && !legacyAgentChild) return null;
1382
+ return { row, id, parentSessionId };
1383
+ }
1384
+
1385
+ function lastStoredAgentHandoff(row) {
1386
+ if (typeof row?.lastHandoff === 'string') return row.lastHandoff;
1387
+ const messages = Array.isArray(row?.messages) ? row.messages : [];
1388
+ const assistant = [...messages].reverse().find((message) => (
1389
+ message?.role === 'assistant'
1390
+ && (typeof message.content === 'string' ? message.content.trim() : message.content)
1391
+ ));
1392
+ if (!assistant) return '';
1393
+ return typeof assistant.content === 'string'
1394
+ ? assistant.content
1395
+ : JSON.stringify(assistant.content);
1396
+ }
1397
+
1398
+ /** Rebuild the Agent-only routing layer from lightweight durable summaries
1399
+ * after daemon replacement. Transcripts remain store/runtime owned and are
1400
+ * loaded by exact canonical session id only when a caller needs one. */
1401
+ async function rehydrateAgentSessions() {
1402
+ if (agentRehydrated) return agentSessions.size;
1403
+ if (agentRehydratePromise) return agentRehydratePromise;
1404
+ if (typeof listSessions !== 'function') {
1405
+ agentRehydrated = true;
1406
+ return agentSessions.size;
1407
+ }
1408
+ let loading;
1409
+ loading = (async () => {
1410
+ const stored = await listSessions({
1411
+ includeAgentOnly: true,
1412
+ summaryOnly: true,
1413
+ refreshFromStorage: false,
1414
+ });
1415
+ let candidates = (Array.isArray(stored) ? stored : [])
1416
+ .map(storedAgentCandidate)
1417
+ .filter(Boolean);
1418
+ if (typeof readStoredSession === 'function') {
1419
+ candidates = await Promise.all(candidates.map(async (candidate) => {
1420
+ if (candidate.row?.parentSessionId) return candidate;
1421
+ try {
1422
+ const metadata = await readStoredSession(candidate.id, { metadataOnly: true });
1423
+ return storedAgentCandidate({
1424
+ ...candidate.row,
1425
+ ...(metadata && typeof metadata === 'object' ? metadata : {}),
1426
+ id: candidate.id,
1427
+ }) || candidate;
1428
+ } catch (error) {
1429
+ log(`agent metadata migration failed session=${candidate.id}: ${error?.message || error}`);
1430
+ return candidate;
1431
+ }
1432
+ }));
1433
+ }
1434
+ const byId = new Map(candidates.map((candidate) => [candidate.id, candidate]));
1435
+ const roots = new Map();
1436
+ const resolveRoot = (candidate, seen = new Set()) => {
1437
+ if (!candidate || seen.has(candidate.id)) return candidate?.parentSessionId || '';
1438
+ if (roots.has(candidate.id)) return roots.get(candidate.id);
1439
+ seen.add(candidate.id);
1440
+ const explicitOwner = validLinkedSessionId(candidate.row?.ownerSessionId, candidate.id);
1441
+ if (explicitOwner && explicitOwner !== candidate.parentSessionId) {
1442
+ roots.set(candidate.id, explicitOwner);
1443
+ return explicitOwner;
1444
+ }
1445
+ const parent = byId.get(candidate.parentSessionId);
1446
+ const root = parent ? resolveRoot(parent, seen) : (explicitOwner || candidate.parentSessionId);
1447
+ roots.set(candidate.id, root);
1448
+ return root;
1449
+ };
1450
+ for (const candidate of candidates) {
1451
+ // A child created while the summary load was in flight is newer than
1452
+ // the stored row and must never be rolled back by rehydration.
1453
+ if (agentSessions.has(candidate.id)) continue;
1454
+ const row = candidate.row;
1455
+ linkAgentDescriptor({
1456
+ id: candidate.id,
1457
+ parentSessionId: candidate.parentSessionId,
1458
+ ownerSessionId: resolveRoot(candidate),
1459
+ owner: 'agent',
1460
+ visibility: 'agent-only',
1461
+ agent: row.agent || row.sourceName || 'worker',
1462
+ agentTag: row.agentTag || row.tag || null,
1463
+ cwd: row.cwd || process.cwd(),
1464
+ provider: row.provider || null,
1465
+ model: row.model || null,
1466
+ presetName: row.presetName || row.preset || row.profileId || null,
1467
+ effort: row.effort || null,
1468
+ fast: row.fast === true,
1469
+ modelParameters: row.modelParameters || null,
1470
+ taskType: row.taskType || null,
1471
+ maxLoopIterations: row.maxLoopIterations,
1472
+ permission: row.permission || null,
1473
+ permissionMode: row.permissionMode || null,
1474
+ toolPermission: row.toolPermission || null,
1475
+ schemaAllowedTools: Array.isArray(row.schemaAllowedTools)
1476
+ ? row.schemaAllowedTools
1477
+ : null,
1478
+ sourceType: row.sourceType || 'agent',
1479
+ sourceName: row.sourceName || row.agent || 'agent',
1480
+ clientHostPid: row.clientHostPid || null,
1481
+ createdAt: row.createdAt || null,
1482
+ updatedAt: row.updatedAt || row.lastUsedAt || null,
1483
+ status: row.closed === true ? 'closed' : (row.status || 'idle'),
1484
+ stage: row.closed === true ? 'closed' : (row.stage || row.status || 'idle'),
1485
+ messageCount: Number(row.messageCount)
1486
+ || (Array.isArray(row.messages) ? row.messages.length : 0),
1487
+ lastHandoff: lastStoredAgentHandoff(row),
1488
+ closed: row.closed === true,
1489
+ });
1490
+ }
1491
+ agentRehydrated = true;
1492
+ return agentSessions.size;
1493
+ })().finally(() => {
1494
+ if (agentRehydratePromise === loading) agentRehydratePromise = null;
1495
+ });
1496
+ agentRehydratePromise = loading;
1497
+ return loading;
1498
+ }
1499
+
1500
+ function agentDescriptor(sessionId) {
1501
+ const id = String(sessionId || '').trim();
1502
+ const descriptor = agentSessions.get(id);
1503
+ if (!descriptor) return null;
1504
+ const owner = sessionOwner(id);
1505
+ const state = owner?.runtime?.getState?.() || {};
1506
+ const status = descriptor.closed
1507
+ ? (descriptor.status || 'closed')
1508
+ : stateBusy(state)
1509
+ ? 'running'
1510
+ : (descriptor.status || 'idle');
1511
+ return {
1512
+ ...descriptor,
1513
+ status,
1514
+ stage: status,
1515
+ messageCount: Array.isArray(state.items) && state.items.length > 0
1516
+ ? state.items.length
1517
+ : Math.max(0, Number(descriptor.messageCount) || 0),
1518
+ updatedAt: descriptor.updatedAt || Date.now(),
1519
+ };
1520
+ }
1521
+
1522
+ function rootOwnerSessionId(sessionId) {
1523
+ const id = String(sessionId || '').trim();
1524
+ return agentSessions.get(id)?.ownerSessionId || id || null;
1525
+ }
1526
+
1527
+ async function createAgentChild({ spec = {}, prompt = '', tag = null } = {}) {
1528
+ await rehydrateAgentSessions();
1529
+ const parentSessionId = String(spec.parentSessionId || '').trim();
1530
+ if (!parentSessionId) throw new TypeError('agent child parentSessionId is required');
1531
+ const ownerSessionId = String(
1532
+ spec.ownerSessionId
1533
+ || agentSessions.get(parentSessionId)?.ownerSessionId
1534
+ || parentSessionId,
1535
+ );
1536
+ const preset = spec.preset && typeof spec.preset === 'object' ? spec.preset : {};
1537
+ const provider = String(preset.provider || spec.provider || '').trim();
1538
+ const model = String(preset.model || spec.model || '').trim();
1539
+ if (!provider || !model) throw new Error('agent child route is incomplete');
1540
+ const sessionProfile = {
1541
+ owner: 'agent',
1542
+ agent: String(spec.agent || 'worker'),
1543
+ parentSessionId,
1544
+ ownerSessionId,
1545
+ visibility: 'agent-only',
1546
+ agentTag: String(spec.agentTag || tag || '').trim() || null,
1547
+ taskType: spec.taskType || null,
1548
+ maxLoopIterations: spec.maxLoopIterations,
1549
+ permission: spec.permission || null,
1550
+ permissionMode: spec.permissionMode || null,
1551
+ schemaAllowedTools: Array.isArray(spec.schemaAllowedTools)
1552
+ ? spec.schemaAllowedTools
1553
+ : null,
1554
+ sourceType: spec.sourceType || 'agent',
1555
+ sourceName: spec.sourceName || spec.agent || 'agent',
1556
+ clientHostPid: spec.clientHostPid || null,
1557
+ };
1558
+ const created = await createSession({
1559
+ cwd: spec.cwd || process.cwd(),
1560
+ provider,
1561
+ model,
1562
+ effort: preset.effort,
1563
+ fast: preset.fast === true,
1564
+ modelParameters: preset.modelParameters,
1565
+ toolMode: 'full',
1566
+ sessionProfile,
1567
+ });
1568
+ const descriptor = linkAgentDescriptor({
1569
+ id: created.sessionId,
1570
+ ...sessionProfile,
1571
+ cwd: spec.cwd || process.cwd(),
1572
+ provider,
1573
+ model,
1574
+ presetName: preset.id || preset.name || null,
1575
+ effort: preset.effort || null,
1576
+ fast: preset.fast === true,
1577
+ createdAt: Date.now(),
1578
+ updatedAt: Date.now(),
1579
+ status: 'idle',
1580
+ stage: 'idle',
1581
+ });
1582
+ void prompt;
1583
+ return { session: descriptor, effectiveCwd: descriptor.cwd };
1584
+ }
1585
+
1586
+ async function runAgentTurn({
1587
+ session,
1588
+ prompt,
1589
+ context = null,
1590
+ onToolResult,
1591
+ onTerminalResult,
1592
+ } = {}) {
1593
+ await rehydrateAgentSessions();
1594
+ const sessionId = String(session?.id || session || '').trim();
1595
+ const descriptor = agentSessions.get(sessionId);
1596
+ if (!descriptor || descriptor.closed) {
1597
+ throw new Error(`agent session ${sessionId || '(empty)'} is closed`);
1598
+ }
1599
+ const entry = await entryForSession(sessionId, {
1600
+ cwd: descriptor.cwd,
1601
+ provider: descriptor.provider,
1602
+ model: descriptor.model,
1603
+ toolMode: 'full',
1604
+ });
1605
+ const target = entry.runtime.submitAndWait;
1606
+ if (typeof target !== 'function') {
1607
+ throw new TypeError('session runtime must implement submitAndWait');
1608
+ }
1609
+ descriptor.status = 'running';
1610
+ descriptor.stage = 'running';
1611
+ descriptor.updatedAt = Date.now();
1612
+ try {
1613
+ const options = {
1614
+ id: `agent-turn-${randomUUID()}`,
1615
+ mode: 'prompt',
1616
+ priority: 'next',
1617
+ context,
1618
+ transcriptMeta: { sender: 'lead' },
1619
+ ...(entry.runtime.isWireSafe === true || typeof onToolResult !== 'function'
1620
+ ? {}
1621
+ : { onToolResult }),
1622
+ };
1623
+ const detail = await target.call(entry.runtime, String(prompt || ''), options);
1624
+ if (detail?.status === 'failed') {
1625
+ throw new Error(String(detail.error || 'agent session turn failed'));
1626
+ }
1627
+ if (detail?.status === 'cancelled') {
1628
+ throw new Error('agent session turn cancelled');
1629
+ }
1630
+ const result = detail?.result || { content: '' };
1631
+ descriptor.status = 'idle';
1632
+ descriptor.stage = 'idle';
1633
+ descriptor.lastHandoff = typeof result?.content === 'string' ? result.content : '';
1634
+ try { onTerminalResult?.(result); } catch {}
1635
+ return result;
1636
+ } catch (error) {
1637
+ descriptor.status = /cancel/i.test(String(error?.message || '')) ? 'cancelled' : 'error';
1638
+ descriptor.stage = descriptor.status;
1639
+ throw error;
1640
+ } finally {
1641
+ descriptor.updatedAt = Date.now();
1642
+ retainUnwatched(entry, 'agent child idle');
1643
+ }
1644
+ }
1645
+
1646
+ function cancelAgentTree(sessionId, reason = 'agent session cancelled') {
1647
+ const id = String(sessionId || '').trim();
1648
+ if (!id) return Promise.resolve(false);
1649
+ const active = agentCancelRuns.get(id);
1650
+ if (active) return active;
1651
+ let run;
1652
+ run = (async () => {
1653
+ await rehydrateAgentSessions();
1654
+ cancelBackgroundTasks({
1655
+ surface: 'agent',
1656
+ callerSessionId: id,
1657
+ reason,
1658
+ });
1659
+ const children = [...(agentChildren.get(id) || [])];
1660
+ await Promise.all(children.map((childId) => cancelAgentTree(childId, reason)));
1661
+ const descriptor = agentSessions.get(id);
1662
+ if (!descriptor) return children.length > 0;
1663
+ if (descriptor.closed) return true;
1664
+ let entry = sessionOwner(id);
1665
+ if (!entry) {
1666
+ try {
1667
+ entry = await entryForSession(id, {
1668
+ cwd: descriptor.cwd,
1669
+ provider: descriptor.provider,
1670
+ model: descriptor.model,
1671
+ toolMode: 'full',
1672
+ });
1673
+ } catch (error) {
1674
+ log(`agent cancel load failed session=${id}: ${error?.message || error}`);
1675
+ }
1676
+ }
1677
+ const closeCanonical = entry?.runtime?.closeCanonicalSession;
1678
+ if (typeof closeCanonical !== 'function') {
1679
+ throw new TypeError('session runtime must implement closeCanonicalSession');
1680
+ }
1681
+ const closed = await closeCanonical.call(entry.runtime, reason);
1682
+ if (closed !== true) throw new Error(`agent session ${id} could not be durably closed`);
1683
+ descriptor.closed = true;
1684
+ descriptor.status = 'closed';
1685
+ descriptor.stage = 'closed';
1686
+ descriptor.updatedAt = Date.now();
1687
+ return true;
1688
+ })().finally(() => {
1689
+ if (agentCancelRuns.get(id) === run) agentCancelRuns.delete(id);
1690
+ });
1691
+ agentCancelRuns.set(id, run);
1692
+ return run;
1693
+ }
1694
+
1695
+ async function cancelAgentDescendants(parentSessionId, reason = 'parent session cancelled') {
1696
+ await rehydrateAgentSessions();
1697
+ const parentId = String(parentSessionId || '');
1698
+ cancelBackgroundTasks({
1699
+ surface: 'agent',
1700
+ callerSessionId: parentId,
1701
+ reason,
1702
+ });
1703
+ const children = [...(agentChildren.get(parentId) || [])];
1704
+ if (!children.length) return false;
1705
+ await Promise.all(children.map((childId) => cancelAgentTree(childId, reason)));
1706
+ return true;
1707
+ }
1708
+
1709
+ function agentDescendantSessionIds(parentSessionId) {
1710
+ const descendants = [];
1711
+ const seen = new Set();
1712
+ const visit = (parentId) => {
1713
+ for (const childId of agentChildren.get(String(parentId || '')) || []) {
1714
+ if (seen.has(childId)) continue;
1715
+ seen.add(childId);
1716
+ descendants.push(childId);
1717
+ visit(childId);
1718
+ }
1719
+ };
1720
+ visit(parentSessionId);
1721
+ return descendants;
1722
+ }
1723
+
1724
+ const agentSurface = Object.freeze({
1725
+ canonical: true,
1726
+ canRun: (session) => Boolean(agentSessions.get(String(session?.id || ''))),
1727
+ createChild: createAgentChild,
1728
+ runTurn: runAgentTurn,
1729
+ });
1730
+
1731
+ const agentManager = Object.freeze({
1732
+ rehydrateAgentSessions,
1733
+ descendantSessionIds: agentDescendantSessionIds,
1734
+ getSession: (sessionId) => agentDescriptor(sessionId),
1735
+ listSessions: ({ includeClosed = false } = {}) => [...agentSessions.keys()]
1736
+ .map(agentDescriptor)
1737
+ .filter((session) => session && (includeClosed || session.closed !== true)),
1738
+ getSessionRuntime: (sessionId) => {
1739
+ const session = agentDescriptor(sessionId);
1740
+ return session ? { stage: session.stage || session.status || 'idle' } : null;
1741
+ },
1742
+ async readSessionHandoff(sessionId) {
1743
+ const id = String(sessionId || '').trim();
1744
+ const descriptor = agentSessions.get(id);
1745
+ if (!descriptor) return '';
1746
+ if (typeof descriptor.lastHandoff === 'string' && descriptor.lastHandoff.trim()) {
1747
+ return descriptor.lastHandoff;
1748
+ }
1749
+ if (typeof readStoredSession !== 'function') return '';
1750
+ const stored = await readStoredSession(id, { includeMessages: true });
1751
+ const handoff = lastStoredAgentHandoff(stored);
1752
+ if (handoff) descriptor.lastHandoff = handoff;
1753
+ return handoff;
1754
+ },
1755
+ async closeSession(sessionId, reason = 'agent session closed') {
1756
+ await rehydrateAgentSessions();
1757
+ return cancelAgentTree(sessionId, reason);
1758
+ },
1759
+ unloadSessionRuntime: () => false,
1760
+ hideSessionFromList: () => false,
1761
+ });
1762
+
1069
1763
  async function approveSession({
1070
1764
  sessionId, approvalId, decision, open: openHints = {}, baseRevision = null,
1071
1765
  } = {}) {
@@ -1280,6 +1974,10 @@ export function createSessionService({
1280
1974
 
1281
1975
  async function stop(reason = 'service stop') {
1282
1976
  closed = true;
1977
+ try { unsubscribeExternalSessionStates(); } catch {}
1978
+ externalViewEntries.clear();
1979
+ agentSessions.clear();
1980
+ agentChildren.clear();
1283
1981
  if (evictTimer) { clearInterval(evictTimer); evictTimer = null; }
1284
1982
  const services = [...new Set(desktopServicesById.values())];
1285
1983
  desktopServicesById.clear();
@@ -1297,8 +1995,10 @@ export function createSessionService({
1297
1995
 
1298
1996
  return {
1299
1997
  handleCall, listSessionCatalog, createSession, readSession, subscribeSession, unsubscribeSession,
1300
- submitSession, materializeSession, abortSession, approveSession,
1998
+ submitSession, materializeSession, recoverActiveGoals, abortSession, approveSession,
1301
1999
  configureSession, stop, releaseClient,
2000
+ agentSurface, agentManager, agentDescriptor, rootOwnerSessionId, rehydrateAgentSessions,
2001
+ cancelAgentTree, cancelAgentDescendants,
1302
2002
  get size() { return sessions.size; },
1303
2003
  /** Live work the daemon must not abandon (self-shutdown guard). */
1304
2004
  get busyCount() { return liveBusyCount(); },