mixdog 1.3.0 → 1.3.2

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 (423) hide show
  1. package/NOTICE.md +52 -9
  2. package/README.md +8 -4
  3. package/package.json +2 -2
  4. package/scripts/native-release-status.mjs +238 -0
  5. package/src/defaults/shipped-history.json +8 -1
  6. package/src/defaults/skills/pptx/scripts/qc-pages.mjs +3 -2
  7. package/src/defaults/skills/setup/SKILL.md +23 -1
  8. package/src/defaults/skills/setup/references/actions.md +54 -4
  9. package/src/defaults/skills/setup/references/surfaces.md +23 -3
  10. package/src/defaults/skills/setup-guide/SKILL.md +17 -10
  11. package/src/defaults/skills/visualize/SKILL.md +9 -6
  12. package/src/defaults/skills/visualize/references/craft.md +8 -1
  13. package/src/help.mjs +1 -1
  14. package/src/lib/keychain-cjs.cjs +4 -1
  15. package/src/lib/rules-builder.cjs +14 -7
  16. package/src/output-styles/common.md +13 -8
  17. package/src/rules/agent-mode/AGENT_MODE.md +9 -6
  18. package/src/rules/agent-mode/MIXY.md +11 -6
  19. package/src/rules/lead/LEAD.md +2 -2
  20. package/src/rules/shared/10-tool-workflow.md +4 -0
  21. package/src/rules/shared/20-research.md +2 -0
  22. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +5 -0
  23. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/session-prep.mjs +8 -0
  24. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +6 -1
  25. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +114 -26
  26. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +8 -0
  27. package/src/runtime/agent/orchestrator/agent-runtime/room-supervisor-completion.mjs +5 -4
  28. package/src/runtime/agent/orchestrator/agent-trace.mjs +3 -0
  29. package/src/runtime/agent/orchestrator/cache-break-trace.mjs +4 -0
  30. package/src/runtime/agent/orchestrator/config-presets.mjs +2 -0
  31. package/src/runtime/agent/orchestrator/config-storage.mjs +10 -3
  32. package/src/runtime/agent/orchestrator/config.mjs +11 -0
  33. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +81 -16
  34. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +97 -4
  35. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +6 -10
  36. package/src/runtime/agent/orchestrator/mcp/client.mjs +7 -4
  37. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +80 -31
  38. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +1 -1
  39. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  40. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +67 -38
  41. package/src/runtime/agent/orchestrator/providers/anthropic-turn-result.mjs +2 -0
  42. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +7 -2
  43. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +6 -2
  44. package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +34 -16
  45. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +31 -4
  46. package/src/runtime/agent/orchestrator/providers/cursor.mjs +4 -1
  47. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +11 -8
  48. package/src/runtime/agent/orchestrator/providers/lib/anthropic-prefix-trace.mjs +76 -0
  49. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +19 -1
  50. package/src/runtime/agent/orchestrator/providers/lib/tail-reminder-split.mjs +13 -0
  51. package/src/runtime/agent/orchestrator/providers/lib/tool-call-wire-ids.mjs +65 -0
  52. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +16 -1
  53. package/src/runtime/agent/orchestrator/providers/mixdog-local.mjs +3 -1
  54. package/src/runtime/agent/orchestrator/providers/model-proxy-provider.mjs +1 -0
  55. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +5 -5
  56. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +9 -5
  57. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +9 -1
  58. package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +8 -2
  59. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +10 -2
  60. package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +30 -3
  61. package/src/runtime/agent/orchestrator/providers/openai-model-capabilities.mjs +1 -1
  62. package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +3 -1
  63. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +3 -1
  64. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +61 -8
  65. package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +14 -2
  66. package/src/runtime/agent/orchestrator/providers/registry.mjs +9 -0
  67. package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +17 -0
  68. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +90 -24
  69. package/src/runtime/agent/orchestrator/runtime-core/fs-utils.mjs +8 -1
  70. package/src/runtime/agent/orchestrator/runtime-core/goal-mutations.mjs +1 -1
  71. package/src/runtime/agent/orchestrator/runtime-core/goal-storage.mjs +68 -22
  72. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-exec.mjs +1 -1
  73. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +5 -5
  74. package/src/runtime/agent/orchestrator/runtime-core/route-context-state.mjs +81 -0
  75. package/src/runtime/agent/orchestrator/runtime-core/session-text.mjs +2 -1
  76. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +12 -1
  77. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +17 -6
  78. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +41 -10
  79. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +6 -4
  80. package/src/runtime/agent/orchestrator/runtime-core/workflow/packs.mjs +8 -4
  81. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +2 -0
  82. package/src/runtime/agent/orchestrator/session/agent-mode.mjs +0 -6
  83. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.d.mts +1 -1
  84. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.mjs +22 -4
  85. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +21 -2
  86. package/src/runtime/agent/orchestrator/session/agent-room.mjs +149 -45
  87. package/src/runtime/agent/orchestrator/session/attachment-preflight.mjs +80 -0
  88. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +4 -1
  89. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +14 -11
  90. package/src/runtime/agent/orchestrator/session/compact/forked-summary.mjs +3 -0
  91. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +68 -18
  92. package/src/runtime/agent/orchestrator/session/compact/summary-mode.mjs +16 -0
  93. package/src/runtime/agent/orchestrator/session/compact/summary-route.mjs +87 -0
  94. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +2 -0
  95. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +5 -1
  96. package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +1 -1
  97. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +1 -1
  98. package/src/runtime/agent/orchestrator/session/context-utils.mjs +41 -10
  99. package/src/runtime/agent/orchestrator/session/human-gate/delegation.mjs +230 -0
  100. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +156 -25
  101. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +20 -0
  102. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +21 -9
  103. package/src/runtime/agent/orchestrator/session/human-gate/run-policy.mjs +73 -0
  104. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +55 -27
  105. package/src/runtime/agent/orchestrator/session/human-gate/unattended-port.mjs +15 -0
  106. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +4 -1
  107. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +189 -105
  108. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +9 -0
  109. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +6 -2
  110. package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +111 -9
  111. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +11 -14
  112. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +3 -0
  113. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +38 -5
  114. package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +21 -0
  115. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +2 -1
  116. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +12 -0
  117. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +11 -5
  118. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +46 -4
  119. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +13 -2
  120. package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +18 -8
  121. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +4 -0
  122. package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +6 -0
  123. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +4 -0
  124. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +44 -18
  125. package/src/runtime/agent/orchestrator/session/manager/orchestration-mode-switch-notice.mjs +99 -0
  126. package/src/runtime/agent/orchestrator/session/manager/owner-route-fallbacks.mjs +42 -0
  127. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +3 -9
  128. package/src/runtime/agent/orchestrator/session/manager/pending-spool-file.mjs +1 -1
  129. package/src/runtime/agent/orchestrator/session/manager/pending-spool-shard.mjs +5 -3
  130. package/src/runtime/agent/orchestrator/session/manager/provider-replay.mjs +50 -0
  131. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +40 -1
  132. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +12 -0
  133. package/src/runtime/agent/orchestrator/session/manager/runtime-state.mjs +20 -0
  134. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +62 -47
  135. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +6 -8
  136. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +12 -1
  137. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +6 -15
  138. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +7 -1
  139. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +24 -8
  140. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +1 -1
  141. package/src/runtime/agent/orchestrator/session/manager/workflow-switch-notice.mjs +94 -13
  142. package/src/runtime/agent/orchestrator/session/mixy-name.mjs +5 -2
  143. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +42 -10
  144. package/src/runtime/agent/orchestrator/session/provider-prefix-guard.mjs +21 -3
  145. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +35 -4
  146. package/src/runtime/agent/orchestrator/session/routine-result.mjs +179 -16
  147. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +5 -1
  148. package/src/runtime/agent/orchestrator/session/side-question-store.mjs +3 -1
  149. package/src/runtime/agent/orchestrator/session/store/canonical-reader.mjs +41 -7
  150. package/src/runtime/agent/orchestrator/session/store/lifecycle-barriers.mjs +10 -2
  151. package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +14 -2
  152. package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +15 -1
  153. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-record.mjs +8 -3
  154. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +36 -1
  155. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +4 -1
  156. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +40 -5
  157. package/src/runtime/agent/orchestrator/session/store-summary-last-message.mjs +47 -2
  158. package/src/runtime/agent/orchestrator/session/tail-reminder-rules.mjs +125 -0
  159. package/src/runtime/agent/orchestrator/session/tool-batch/finalize.mjs +4 -0
  160. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +78 -43
  161. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +49 -14
  162. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +134 -17
  163. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +35 -13
  164. package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +25 -5
  165. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +4 -3
  166. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +13 -14
  167. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/transport-artifacts.mjs +9 -0
  168. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -14
  169. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -1
  170. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +6 -14
  171. package/src/runtime/agent/orchestrator/tools/builtin/pdf-test-fixtures.mjs +5 -3
  172. package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +4 -2
  173. package/src/runtime/agent/orchestrator/tools/builtin/read-single-media.mjs +47 -2
  174. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +264 -87
  175. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +10 -4
  176. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +34 -21
  177. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +8 -4
  178. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +9 -30
  179. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +24 -12
  180. package/src/runtime/agent/orchestrator/tools/lib/shell-run-spawn.mjs +3 -2
  181. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +6 -2
  182. package/src/runtime/agent/orchestrator/tools/patch/delete-quarantine.mjs +2 -1
  183. package/src/runtime/agent/orchestrator/tools/patch/replay-capture.mjs +4 -2
  184. package/src/runtime/agent/orchestrator/tools/patch/rollback-state.mjs +33 -2
  185. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +13 -13
  186. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +5 -3
  187. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +3 -1
  188. package/src/runtime/attachments/attachment-reference.mjs +155 -0
  189. package/src/runtime/attachments/limits.mjs +5 -1
  190. package/src/runtime/attachments/office-extract.mjs +3 -6
  191. package/src/runtime/attachments/pdf-extract.mjs +84 -10
  192. package/src/runtime/attachments/store.mjs +55 -30
  193. package/src/runtime/browser-bridge/client.mjs +73 -20
  194. package/src/runtime/channels/lib/runtime-paths.mjs +1 -1
  195. package/src/runtime/channels/lib/scheduler.mjs +14 -7
  196. package/src/runtime/channels/lib/state-file.mjs +15 -15
  197. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +14 -13
  198. package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +22 -18
  199. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +14 -36
  200. package/src/runtime/channels/lib/worker-main.mjs +3 -2
  201. package/src/runtime/effort-judge/compact-tokenizer.mjs +8 -2
  202. package/src/runtime/effort-judge/judge-client.mjs +6 -3
  203. package/src/runtime/effort-judge/model-install.mjs +16 -8
  204. package/src/runtime/effort-judge/model-manifest.json +7 -7
  205. package/src/runtime/local-provider/asset-installer.mjs +10 -4
  206. package/src/runtime/local-provider/catalog.mjs +5 -2
  207. package/src/runtime/local-provider/registered-models.mjs +64 -27
  208. package/src/runtime/local-provider/server.mjs +2 -3
  209. package/src/runtime/media/adapters/image-api.mjs +89 -0
  210. package/src/runtime/media/auth.mjs +64 -1
  211. package/src/runtime/media/catalog.mjs +66 -4
  212. package/src/runtime/media/jobs.mjs +4 -0
  213. package/src/runtime/media/lanes.mjs +46 -5
  214. package/src/runtime/media/media-usage.mjs +17 -0
  215. package/src/runtime/media/renditions.mjs +15 -15
  216. package/src/runtime/media/store.mjs +36 -35
  217. package/src/runtime/media/tool.mjs +8 -1
  218. package/src/runtime/memory/lib/cycle-scheduler/health-ledger.mjs +8 -2
  219. package/src/runtime/memory/lib/embedding-model-files.mjs +30 -7
  220. package/src/runtime/memory/lib/ingest/transcript-tail.mjs +16 -7
  221. package/src/runtime/memory/lib/pg/process.mjs +8 -4
  222. package/src/runtime/memory/lib/project-id-resolver.mjs +5 -1
  223. package/src/runtime/memory/lib/runtime-fetcher.mjs +6 -1
  224. package/src/runtime/office/core/journal.mjs +7 -14
  225. package/src/runtime/office/design/library/design-library-core.mjs +4 -16
  226. package/src/runtime/office/pdf/ocr-engine-worker.mjs +37 -0
  227. package/src/runtime/office/pdf/ocr-engine.mjs +146 -0
  228. package/src/runtime/office/pdf/pdf-ocr.mjs +51 -21
  229. package/src/runtime/office/pdf/pdf-render.mjs +30 -18
  230. package/src/runtime/office/portable/ooxml-validator.mjs +2 -2
  231. package/src/runtime/office/portable/portable-soffice.mjs +4 -0
  232. package/src/runtime/shared/agent-kind-migration.mjs +200 -0
  233. package/src/runtime/shared/agent-kind.mjs +152 -0
  234. package/src/runtime/shared/agent-message-envelope.d.mts +6 -0
  235. package/src/runtime/shared/agent-message-envelope.mjs +44 -0
  236. package/src/runtime/shared/agent-reap-state.d.mts +4 -0
  237. package/src/runtime/shared/agent-route-config.d.mts +1 -0
  238. package/src/runtime/shared/agent-route-config.mjs +22 -8
  239. package/src/runtime/shared/agent-store.mjs +149 -0
  240. package/src/runtime/shared/agent-task-starts.mjs +27 -0
  241. package/src/runtime/shared/atomic-file.d.mts +50 -0
  242. package/src/runtime/shared/atomic-file.mjs +394 -132
  243. package/src/runtime/shared/background-tasks.mjs +7 -4
  244. package/src/runtime/shared/bot-hops.mjs +30 -0
  245. package/src/runtime/shared/bot-split-migration.mjs +457 -0
  246. package/src/runtime/shared/bounded-download.mjs +4 -2
  247. package/src/runtime/shared/config-patch.mjs +20 -1
  248. package/src/runtime/shared/config.mjs +39 -9
  249. package/src/runtime/shared/connector-catalog.d.mts +1 -2
  250. package/src/runtime/shared/connector-catalog.mjs +2 -4
  251. package/src/runtime/shared/connector-triggers.mjs +2 -0
  252. package/src/runtime/shared/defaults-separation.mjs +6 -2
  253. package/src/runtime/shared/delegated-approval-host.mjs +74 -0
  254. package/src/runtime/shared/err-text.mjs +7 -1
  255. package/src/runtime/shared/error-presentation.mjs +113 -10
  256. package/src/runtime/shared/file-has-data.mjs +28 -0
  257. package/src/runtime/shared/handoff-artifacts.d.mts +20 -0
  258. package/src/runtime/shared/handoff-artifacts.mjs +61 -0
  259. package/src/runtime/shared/llm/provider-model-identities.mjs +6 -1
  260. package/src/runtime/shared/llm/usage-accounting.mjs +7 -1
  261. package/src/runtime/shared/llm/usage-ledger-import.mjs +37 -18
  262. package/src/runtime/shared/model-route-fallback.mjs +79 -7
  263. package/src/runtime/shared/pending-agent-work.mjs +54 -0
  264. package/src/runtime/shared/plain-preview.mjs +22 -17
  265. package/src/runtime/shared/process-lifecycle.mjs +8 -12
  266. package/src/runtime/shared/projects.mjs +50 -41
  267. package/src/runtime/shared/provider-accounts.mjs +17 -12
  268. package/src/runtime/shared/routine-trigger.d.mts +6 -1
  269. package/src/runtime/shared/routine-trigger.mjs +14 -6
  270. package/src/runtime/shared/run-child.d.mts +3 -0
  271. package/src/runtime/shared/run-child.mjs +24 -3
  272. package/src/runtime/shared/sensitive-path-rules.d.mts +15 -0
  273. package/src/runtime/shared/sensitive-path-rules.mjs +256 -0
  274. package/src/runtime/shared/session-baseline-store.mjs +8 -2
  275. package/src/runtime/shared/singleton-owner.mjs +7 -6
  276. package/src/runtime/shared/skill-tool-dependencies.mjs +4 -10
  277. package/src/runtime/shared/staged-update.mjs +4 -2
  278. package/src/runtime/shared/statusline/statusline-segments.mjs +4 -15
  279. package/src/runtime/shared/tool-execution-contract.mjs +6 -0
  280. package/src/runtime/shared/transcript-metadata.mjs +28 -3
  281. package/src/runtime/shared/transcript-writer/session-record.mjs +7 -6
  282. package/src/runtime/shared/turn-snapshot-store.mjs +5 -4
  283. package/src/runtime/shared/turn-snapshot.mjs +44 -2
  284. package/src/runtime/shared/usage-readout-totals.mjs +15 -13
  285. package/src/runtime/shared/user-cwd.mjs +9 -4
  286. package/src/runtime/shared/user-data-guard.mjs +46 -44
  287. package/src/runtime/shared/utf8-cut.mjs +10 -0
  288. package/src/runtime/shared/worktrees/activity-gate.mjs +269 -61
  289. package/src/runtime/shared/worktrees/registry.mjs +26 -26
  290. package/src/runtime/tidy/core-install.mjs +26 -4
  291. package/src/runtime/tidy/install.mjs +91 -23
  292. package/src/runtime/web-search/lib/config.mjs +4 -16
  293. package/src/session-runtime/agent-home-delivery.mjs +859 -136
  294. package/src/session-runtime/agent-room-delivery.mjs +167 -19
  295. package/src/session-runtime/agent-task-sessions.mjs +831 -69
  296. package/src/session-runtime/agent-tool-routing.mjs +1 -0
  297. package/src/session-runtime/boot/apis.mjs +58 -24
  298. package/src/session-runtime/boot/config.mjs +1 -0
  299. package/src/session-runtime/boot/facade.mjs +11 -1
  300. package/src/session-runtime/boot/infrastructure.mjs +14 -2
  301. package/src/session-runtime/boot/providers.mjs +16 -6
  302. package/src/session-runtime/boot/shared.mjs +12 -6
  303. package/src/session-runtime/boot/tools.mjs +2 -2
  304. package/src/session-runtime/bot-status.mjs +432 -0
  305. package/src/session-runtime/config-lifecycle/lead-route-sync.mjs +29 -0
  306. package/src/session-runtime/config-lifecycle.mjs +78 -32
  307. package/src/session-runtime/cwd-plugins/core-memory-context.mjs +2 -4
  308. package/src/session-runtime/handoff-attachments.mjs +593 -0
  309. package/src/session-runtime/held-requests.mjs +36 -33
  310. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +10 -1
  311. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +2 -2
  312. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +45 -6
  313. package/src/session-runtime/lifecycle/session-catalog.mjs +12 -2
  314. package/src/session-runtime/lifecycle/teardown.mjs +11 -3
  315. package/src/session-runtime/mcp-glue/connect.mjs +100 -186
  316. package/src/session-runtime/mcp-glue/initial-connect-gate.mjs +57 -0
  317. package/src/session-runtime/mcp-glue/server-connection.mjs +71 -0
  318. package/src/session-runtime/media-api.mjs +5 -2
  319. package/src/session-runtime/model-route/route-persist.mjs +2 -2
  320. package/src/session-runtime/model-route/route-tuning.mjs +11 -2
  321. package/src/session-runtime/model-route/set-route.mjs +82 -2
  322. package/src/session-runtime/native-web-search.mjs +2 -2
  323. package/src/session-runtime/new-session-config.mjs +6 -1
  324. package/src/session-runtime/owned-automation-run.mjs +8 -1
  325. package/src/session-runtime/room-member-gate.mjs +16 -3
  326. package/src/session-runtime/route-resolution.mjs +21 -19
  327. package/src/session-runtime/route-state.mjs +1 -59
  328. package/src/session-runtime/routine-home-turn.mjs +5 -3
  329. package/src/session-runtime/runtime-bootstrap.mjs +5 -3
  330. package/src/session-runtime/runtime-paths.mjs +11 -5
  331. package/src/session-runtime/runtime-tunables.mjs +6 -0
  332. package/src/session-runtime/services/agent-tool/close-flow/close-all.mjs +11 -2
  333. package/src/session-runtime/services/agent-tool/close-flow.mjs +4 -2
  334. package/src/session-runtime/services/agent-tool/execute-send.mjs +1 -0
  335. package/src/session-runtime/services/agent-tool/execute-spawn.mjs +1 -0
  336. package/src/session-runtime/services/agent-tool/helpers.mjs +24 -5
  337. package/src/session-runtime/services/agent-tool/restart-notify.mjs +10 -0
  338. package/src/session-runtime/services/agent-tool/send-flow.mjs +36 -24
  339. package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +4 -0
  340. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +6 -3
  341. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +12 -6
  342. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +5 -8
  343. package/src/session-runtime/services/agent-tool/tool-def.mjs +7 -0
  344. package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +22 -2
  345. package/src/session-runtime/services/agent-tool/worker-index.mjs +4 -6
  346. package/src/session-runtime/services/agent-tool.mjs +6 -0
  347. package/src/session-runtime/services/daemon-crash-capture/daemon-exit-record.mjs +1 -1
  348. package/src/session-runtime/services/daemon-crash-capture/retention.mjs +4 -3
  349. package/src/session-runtime/services/hook-bus/config-loader.mjs +14 -5
  350. package/src/session-runtime/services/hook-bus/rule-file.mjs +49 -11
  351. package/src/session-runtime/services/plugin-admin.mjs +26 -26
  352. package/src/session-runtime/services/provider-admin.mjs +14 -4
  353. package/src/session-runtime/services/schedule-access.mjs +33 -4
  354. package/src/session-runtime/services/seeds.mjs +4 -3
  355. package/src/session-runtime/services/usage-dashboard.mjs +1 -1
  356. package/src/session-runtime/services/usage-stats-model.mjs +2 -1
  357. package/src/session-runtime/session-create/materialize.mjs +2 -3
  358. package/src/session-runtime/session-create/session-options.mjs +15 -2
  359. package/src/session-runtime/settings-builtin-tools-api.mjs +63 -1
  360. package/src/session-runtime/settings-compaction-api.mjs +86 -39
  361. package/src/session-runtime/setup-tool/executor.mjs +20 -3
  362. package/src/session-runtime/setup-tool/extended-actions.mjs +19 -7
  363. package/src/session-runtime/setup-tool/settings-contract.mjs +37 -1
  364. package/src/session-runtime/setup-tool/tool-defs.mjs +20 -3
  365. package/src/session-runtime/skills-api.mjs +12 -5
  366. package/src/session-runtime/tool-policy/agent-profile-fold.mjs +265 -0
  367. package/src/session-runtime/tool-policy/mode-switch.mjs +54 -0
  368. package/src/session-runtime/tool-policy/pending-switches.mjs +59 -0
  369. package/src/session-runtime/tool-policy/policy-stage.mjs +165 -0
  370. package/src/session-runtime/tool-policy/session-switches.mjs +169 -0
  371. package/src/session-runtime/tool-policy/surface-helpers.mjs +114 -0
  372. package/src/session-runtime/tool-policy/workflow-overlay.mjs +145 -0
  373. package/src/session-runtime/tool-policy-refresh.mjs +47 -358
  374. package/src/session-runtime/tool-surface.mjs +5 -3
  375. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +99 -81
  376. package/src/session-runtime/turn/turn-run/turn-timing.mjs +4 -3
  377. package/src/session-runtime/turn/turn-run.mjs +2 -1
  378. package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +54 -7
  379. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +578 -301
  380. package/src/session-runtime/workflow-agents-api/agent-route.mjs +113 -53
  381. package/src/session-runtime/workflow-agents-api/onboarding.mjs +11 -4
  382. package/src/session-runtime/workflow-agents-api/shared.mjs +5 -1
  383. package/src/session-runtime/workflow-agents-api/style-and-mode.mjs +14 -1
  384. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +6 -3
  385. package/src/session-runtime/workspace/workspace-state.mjs +7 -6
  386. package/src/standalone/daemon-agent-control.mjs +4 -1
  387. package/src/standalone/daemon-session-control.mjs +60 -11
  388. package/src/standalone/daemon.mjs +34 -4
  389. package/src/standalone/local-session-runtime.mjs +22 -14
  390. package/src/standalone/session-client.mjs +7 -2
  391. package/src/standalone/session-protocol.mjs +7 -0
  392. package/src/standalone/session-service/agent-tree/agent-turns.mjs +46 -3
  393. package/src/standalone/session-service/agent-tree.mjs +4 -3
  394. package/src/standalone/session-service/session-calls/session-turns.mjs +19 -0
  395. package/src/standalone/session-service.mjs +11 -0
  396. package/src/standalone/session-state-projection.mjs +19 -9
  397. package/src/standalone/session-tool-output.mjs +35 -9
  398. package/src/tui/app/maintenance-pickers/auto-clear-picker.mjs +19 -13
  399. package/src/tui/app/settings-picker/settings-rows.mjs +2 -2
  400. package/src/tui/app/slash-commands.mjs +1 -1
  401. package/src/tui/app/usage-context-panels/context-model.mjs +8 -2
  402. package/src/tui/dist/index.mjs +599 -342
  403. package/src/tui/prompt-history-store.mjs +16 -21
  404. package/src/tui/session/context-state.mjs +4 -0
  405. package/src/tui/session/notice-surface.mjs +3 -4
  406. package/src/tui/session/session-api/intake/abort.mjs +19 -1
  407. package/src/tui/session/session-api/media.mjs +2 -1
  408. package/src/tui/session/session-api/routes-agent-definitions.mjs +75 -0
  409. package/src/tui/session/session-api/routes-write.mjs +203 -0
  410. package/src/tui/session/session-api/routes.mjs +23 -236
  411. package/src/tui/session/session-api/settings.mjs +14 -1
  412. package/src/tui/session/session-flow/drain.mjs +28 -3
  413. package/src/tui/session/session-flow/failed-turn-rewind.mjs +4 -2
  414. package/src/tui/session/session-flow/queue/take-entries.mjs +8 -0
  415. package/src/tui/session/session-flow/steering.mjs +8 -3
  416. package/src/tui/session/tool-approval.mjs +185 -20
  417. package/src/tui/session/transcript-intake.mjs +4 -0
  418. package/src/tui/session/turn-live-call.mjs +11 -3
  419. package/src/tui/session/turn.mjs +36 -17
  420. package/src/tui/transcript-tool-failures.mjs +8 -2
  421. package/src/vendor/statusline/src/gateway/route-meta.mjs +7 -8
  422. package/src/vendor/statusline/src/gateway/session-routes.mjs +6 -7
  423. package/src/rules/agent-mode/ROUTINE.md +0 -6
@@ -9,11 +9,27 @@
9
9
  * home (agent-home-delivery.mjs), which posts it and wakes the agent once to
10
10
  * relay it. A turn blocked on the user's answer or approval posts one faint
11
11
  * line instead; the card stays in the task session for the user.
12
+ *
13
+ * An agent's send to any other session reports the same way, by an origin for
14
+ * that send only (reportOrigin). Every reporting turn is also watched durably
15
+ * (agent-home-delivery.mjs `watches`) until its end is queued, so a daemon
16
+ * restart before that re-attaches it (recover): an ended turn reports once
17
+ * from the target's record, a queued or running one is watched to its end.
18
+ *
19
+ * A group-room member (its member session `sess_rmem_<room>_<agent>`) opens
20
+ * and drives task sessions the same way; its reports carry `from` (the member
21
+ * session), so they land in the ROOM's log as that member's row and in its own
22
+ * member session, never in its home (agent-home-delivery.mjs enqueueTask).
12
23
  */
13
24
  import { randomUUID } from 'node:crypto';
14
25
  import { realpathSync, statSync } from 'node:fs';
15
26
  import { basename, resolve } from 'node:path';
16
27
  import { clean } from '../runtime/agent/orchestrator/runtime-core/session-text.mjs';
28
+ import { collectTurnArtifacts, handoffContent, sessionRoots, turnSlice } from './handoff-attachments.mjs';
29
+ import { roomMemberOfSession } from '../runtime/agent/orchestrator/session/agent-room.mjs';
30
+ import { wrapAgentMessage } from '../runtime/shared/agent-message-envelope.mjs';
31
+ import { sessionsWithPendingWork } from '../runtime/shared/pending-agent-work.mjs';
32
+ import { agentTaskIdOfExecution } from './services/agent-tool/restart-notify.mjs';
17
33
 
18
34
  const TITLE_LIMIT = 60;
19
35
 
@@ -51,87 +67,826 @@ export function resolveTaskProject(project, { projects = [], cwd = process.cwd()
51
67
  const turnStatus = (detail) =>
52
68
  ({ done: 'completed', completed: 'completed', failed: 'failed' })[detail?.status] || 'cancelled';
53
69
 
70
+ const INTERRUPTED = /^\[Request interrupted/;
71
+
72
+ const messageText = (content) =>
73
+ typeof content === 'string'
74
+ ? content
75
+ : (Array.isArray(content) ? content : [])
76
+ .map((part) => (typeof part === 'string' ? part : part?.type === 'text' ? part.text || '' : ''))
77
+ .join('');
78
+
79
+ /** The submissions a stored user message delivered: a turn's prompt
80
+ * (`checkpointId`, `submissionIds` of a merged batch) or a steered message. */
81
+ export function submissionIdsOf(message) {
82
+ if (message?.role !== 'user') return [];
83
+ const meta = message.meta || {};
84
+ const transcript = meta.transcript || {};
85
+ return [
86
+ transcript.checkpointId,
87
+ ...(Array.isArray(transcript.submissionIds) ? transcript.submissionIds : []),
88
+ ...(Array.isArray(meta.submissionIds) ? meta.submissionIds : []),
89
+ ];
90
+ }
91
+
92
+ /** The end of the turn that took submission `runId`, from a session's stored
93
+ * `messages`: `{ status, text }` once that turn recorded its end (its
94
+ * completion, an interruption marker, or the next turn's prompt), 'open'
95
+ * while it has none, null when no turn took it. */
96
+ export function submissionOutcome(messages, runId) {
97
+ const list = Array.isArray(messages) ? messages : [];
98
+ const start = list.findIndex((message) => submissionIdsOf(message).includes(runId));
99
+ if (start < 0) return null;
100
+ for (let index = start; index < list.length; index += 1) {
101
+ const message = list[index];
102
+ const completion = message?.meta?.transcript?.completion;
103
+ if (completion) {
104
+ const status = turnStatus(completion);
105
+ const text = status === 'failed' ? completion.detail || '' : message.role === 'assistant' ? message.content : '';
106
+ return { status, text: messageText(text) };
107
+ }
108
+ if (index === start || message?.role !== 'user') continue;
109
+ if (INTERRUPTED.test(messageText(message.content)) || message.meta?.transcript?.checkpointId) {
110
+ return { status: 'cancelled', text: '' };
111
+ }
112
+ }
113
+ return 'open';
114
+ }
115
+
116
+ // `agent` tool calls that can start a job (a spawn, or a send that begins one).
117
+ const STARTING_AGENT_CALLS = new Set(['spawn', 'send']);
118
+
119
+ const callArguments = (call) => {
120
+ const raw = call?.arguments ?? call?.function?.arguments;
121
+ if (raw && typeof raw === 'object') return raw;
122
+ try {
123
+ return JSON.parse(String(raw || '{}')) || {};
124
+ } catch {
125
+ return {};
126
+ }
127
+ };
128
+
129
+ /** The agent task ids a woken turn's prompt delivered, from its stored
130
+ * provenance: the batch's `consumedExecutionIds` (session-flow/drain.mjs)
131
+ * or the single `execution` of a queue-fed turn; null for an id that names
132
+ * no task. Empty when the turn recorded none. */
133
+ function consumedTaskIds(message) {
134
+ const recorded = message?.meta?.transcript?.consumedExecutionIds;
135
+ const ids = Array.isArray(recorded) ? recorded : message?.meta?.execution?.id ? [message.meta.execution.id] : [];
136
+ return ids.map((id) => (id ? agentTaskIdOfExecution(id) : null));
137
+ }
138
+
139
+ /** The latest turn of the chain that submission `runId` started in a
140
+ * session's stored `messages`, from structured provenance only: runId's own
141
+ * turn, then each turn that took a result of an agent task the chain started
142
+ * — as its prompt (a woken turn) or steered into it while it ran, even a turn
143
+ * the user started (its `agent` spawn/send results record
144
+ * `agentTaskIds`, tool-batch/finalize.mjs; the tasks those turns start join
145
+ * the chain). A turn the user typed, or one woken only by other agents,
146
+ * never joins it. `ambiguous` is set when provenance a chain decision needs
147
+ * is missing — a starting agent call of the chain without `agentTaskIds`,
148
+ * or a woken turn naming no task — so its answer may not be the final one.
149
+ * `{ id, outcome, at, ambiguous }`: that turn's submission id, its
150
+ * submissionOutcome and when its end was recorded (0 when unknown); null
151
+ * when no turn took runId. */
152
+ export function latestTurnOutcome(messages, runId) {
153
+ const list = Array.isArray(messages) ? messages : [];
154
+ const start = list.findIndex((message) => submissionIdsOf(message).includes(runId));
155
+ if (start < 0) return null;
156
+ const started = new Set();
157
+ const startingCalls = new Set();
158
+ let at = start;
159
+ let id = runId;
160
+ let inChain = true;
161
+ let ambiguous = false;
162
+ // The turn the walk is in: where its prompt is stored and its submission id.
163
+ let turnAt = start;
164
+ let turnId = runId;
165
+ for (let index = start; index < list.length; index += 1) {
166
+ const message = list[index];
167
+ const checkpointId = index > start && message?.role === 'user' ? message.meta?.transcript?.checkpointId : '';
168
+ if (checkpointId) {
169
+ turnAt = index;
170
+ turnId = checkpointId;
171
+ inChain = false;
172
+ }
173
+ // A completion delivered as a turn's prompt (queue/bus wake), or steered
174
+ // into a turn already running (loop/steering.mjs, no checkpoint of its
175
+ // own): the turn that took the chain's result joins the chain, even one
176
+ // the user started, so its answer becomes the candidate.
177
+ const delivered =
178
+ index > start && message?.role === 'user' && message.meta?.source === 'task-notification'
179
+ ? consumedTaskIds(message)
180
+ : null;
181
+ if (delivered && !inChain) {
182
+ if (delivered.some((taskId) => taskId && started.has(taskId))) {
183
+ inChain = true;
184
+ at = turnAt;
185
+ id = turnId;
186
+ } else if (!delivered.length || delivered.includes(null)) {
187
+ ambiguous = true;
188
+ }
189
+ }
190
+ if (checkpointId || delivered) continue;
191
+ if (!inChain) continue;
192
+ if (message?.role === 'assistant') {
193
+ for (const call of Array.isArray(message.toolCalls) ? message.toolCalls : []) {
194
+ const name = String(call?.name ?? call?.function?.name ?? '');
195
+ const type = String(callArguments(call).type || 'spawn').trim();
196
+ if (name === 'agent' && STARTING_AGENT_CALLS.has(type) && call?.id) startingCalls.add(call.id);
197
+ }
198
+ continue;
199
+ }
200
+ if (message?.role !== 'tool' || !startingCalls.has(message.toolCallId)) continue;
201
+ if (Array.isArray(message.agentTaskIds)) for (const taskId of message.agentTaskIds) started.add(String(taskId));
202
+ else if (message.toolKind !== 'error') ambiguous = true;
203
+ }
204
+ const turn = list.slice(at);
205
+ const ended = turn.find((message) => message?.meta?.transcript?.completion);
206
+ return { id, outcome: submissionOutcome(turn, id), at: Number(ended?.meta?.transcript?.at) || 0, ambiguous };
207
+ }
208
+
209
+ /** Whether task session `sessionId` still waits on agent work it handed off,
210
+ * from `agentWorking` (Lead ids with an Agent session in flight), `owed`
211
+ * (agent results queued for it that no turn has taken yet), `pool` (agent
212
+ * pool rows: the desktop notifier's rule, pending-agent-work.mjs) and
213
+ * `agentTaskOwners` (the sessions owning a queued, spawning or running agent
214
+ * task — one still waiting for admission has no turn in flight, and its pool
215
+ * row reads `unknown`). An agent task owned by an Agent session under it, at
216
+ * any depth, counts too: every descendant's pool row names the Lead as owner. */
217
+ export function pendingWorkOf(
218
+ sessionId,
219
+ { agentWorking = new Set(), owed = [], pool = [], agentTaskOwners = new Set() }
220
+ ) {
221
+ if (agentWorking.has(sessionId) || owed.length > 0 || agentTaskOwners.has(sessionId)) return true;
222
+ if (sessionsWithPendingWork(pool).has(sessionId)) return true;
223
+ return pool.some((row) => row?.ownerSessionId === sessionId && agentTaskOwners.has(String(row.sessionId || '')));
224
+ }
225
+
226
+ /** The agent results queued for `sessionId` that no turn has taken yet (its
227
+ * durable pending queue: an agent's completion, a restart failure): `{ text,
228
+ * meta }` as the notification bus delivers them. */
229
+ export async function owedAgentResults(sessionId) {
230
+ const [{ _getPendingMessagesForSession }, { completionIsDelivered, isCompletionNotificationEntry }] =
231
+ await Promise.all([
232
+ import('../runtime/agent/orchestrator/session/manager/pending-messages.mjs'),
233
+ import('../runtime/agent/orchestrator/session/manager/pending-message-entry.mjs'),
234
+ ]);
235
+ return _getPendingMessagesForSession(sessionId)
236
+ .filter((entry) => isCompletionNotificationEntry(entry) && !completionIsDelivered(entry))
237
+ .map((entry) => ({
238
+ text: String(entry.text || entry.content || ''),
239
+ meta: {
240
+ type: 'agent_task_result',
241
+ execution_surface: entry.execution?.surface || 'agent',
242
+ execution_id: entry.executionId || entry.execution?.id || null,
243
+ status: entry.execution?.status || null,
244
+ caller_session_id: sessionId,
245
+ },
246
+ }));
247
+ }
248
+
249
+ /** pendingWorkOf over this process's state: the in-flight runtimes, the
250
+ * durable pending queue, the agent pool and the background agent tasks. */
251
+ export async function taskSessionPendingWork(sessionId, { owedResults = owedAgentResults } = {}) {
252
+ const [{ inFlightSessionActivity }, { listStoredAgentWorkersAsync }, { activeBackgroundTaskSessionIds }] =
253
+ await Promise.all([
254
+ import('../runtime/agent/orchestrator/session/manager/runtime-state.mjs'),
255
+ import('../runtime/agent/orchestrator/session/store-summary-reader.mjs'),
256
+ import('../runtime/shared/background-tasks.mjs'),
257
+ ]);
258
+ const { working, agentWorking } = inFlightSessionActivity();
259
+ const agentTaskOwners = activeBackgroundTaskSessionIds({ surface: 'agent' });
260
+ if (pendingWorkOf(sessionId, { agentWorking, agentTaskOwners })) return true;
261
+ if ((await owedResults(sessionId)).length) return true;
262
+ const pool = await listStoredAgentWorkersAsync({ runningSessionIds: working });
263
+ return pendingWorkOf(sessionId, { pool, agentTaskOwners });
264
+ }
265
+
266
+ // A task session idle with nothing pending must stay so this long before its
267
+ // latest answer is final: a finished agent's result can start the Lead's next
268
+ // turn right after (the desktop notifier's quiet period).
269
+ const FINAL_SETTLE_MS = 2500;
270
+ // While a report waits on pending work it re-checks this often (state changes
271
+ // of the session re-check at once).
272
+ const FINAL_POLL_MS = 15_000;
273
+
274
+ const REPORT_RETRY_DELAYS_MS = [200, 1000, 5000];
275
+
276
+ const LOST_TEXT = 'The message never ran: the app restarted before its turn started.';
277
+ const GONE_TEXT = 'The session no longer exists.';
278
+ const RESUME_FAILED_TEXT =
279
+ 'The agent task session could not continue after the app restarted: its agents’ results never reached it. Check that session.';
280
+ const PENDING_CAP_MS = 30 * 60_000;
281
+ const STILL_WORKING_TEXT =
282
+ 'Still in progress: its agents are still working 30+ minutes after this answer. This is the latest answer; the final one is reported when they finish.';
283
+ // Deliveries of a restarted task session's queued agent results before its
284
+ // report gives up as RESUME_FAILED_TEXT.
285
+ const OWED_WAKE_TRIES = 3;
286
+
287
+ // Why a send or create reports nothing back (its `reportsBackReason`).
288
+ const NOTIFY_OFF_REASON = 'this agent task session was created with notify: false, so its turns do not report back';
289
+ const WATCH_CAP_REASON =
290
+ 'this chat already waits on as many unreported session results as it can track; earlier ones must report first';
291
+
292
+ // The last-used model search stops after this many catalog pages.
293
+ const LAST_USED_ROUTE_MAX_PAGES = 20;
294
+
295
+ /** A report's `from` when its origin is a group-room member session: the
296
+ * report goes to that member's room instead of the agent's home. */
297
+ const roomFrom = (originSessionId) => (roomMemberOfSession(originSessionId) ? { from: originSessionId } : {});
298
+
299
+ /** The route of `modelRef`: a provider/model[@effort…] ref (a superseded
300
+ * model runs on its successor) or a preset name. */
301
+ async function configModelRoute(modelRef) {
302
+ const [{ modelRouteFields, parseScheduleModelRef }, { migrateCodexRoute }, { loadConfig }] = await Promise.all([
303
+ import('../runtime/shared/schedule-model-ref.mjs'),
304
+ import('../runtime/agent/orchestrator/providers/openai-oauth-catalog-store.mjs'),
305
+ import('../runtime/agent/orchestrator/config.mjs'),
306
+ ]);
307
+ const ref = parseScheduleModelRef(modelRef);
308
+ if (ref && typeof ref === 'object') return modelRouteFields(migrateCodexRoute(ref));
309
+ const preset = loadConfig({ secrets: false }).presets?.find((p) => p.id === ref || p.name === ref);
310
+ if (!preset) throw new Error(`model "${ref}" is neither a provider/model route nor a known preset name`);
311
+ return modelRouteFields(preset);
312
+ }
313
+
314
+ /** The Lead (Main) route of the config — what a desktop New task starts on.
315
+ * Null while none is chosen. */
316
+ async function configLeadRoute() {
317
+ const [{ modelRouteFields }, { resolveRoute }, { loadConfig }] = await Promise.all([
318
+ import('../runtime/shared/schedule-model-ref.mjs'),
319
+ import('./boot/shared.mjs'),
320
+ import('../runtime/agent/orchestrator/config.mjs'),
321
+ ]);
322
+ const route = resolveRoute(loadConfig({ secrets: false }), {});
323
+ return clean(route?.provider) && clean(route?.model) ? modelRouteFields(route) : null;
324
+ }
325
+
326
+ /** The model the user last used in a coding session (the runtime's stand-in
327
+ * for the desktop's last New-task model): the first row that has one in the
328
+ * catalog, which lists newest activity first. Pages on (bounded) until a
329
+ * row qualifies; null when none does. */
330
+ async function lastUsedSessionRoute(sessionService) {
331
+ let cursor = null;
332
+ for (let page = 0; page < LAST_USED_ROUTE_MAX_PAGES; page += 1) {
333
+ const listed = await sessionService.listSessionCatalog({ page: { limit: 50, cursor } });
334
+ const used = (Array.isArray(listed?.sessions) ? listed.sessions : []).find(
335
+ (row) => row?.sessionMode !== 'agent' && clean(row?.provider) && clean(row?.model)
336
+ );
337
+ if (used) return { provider: clean(used.provider), model: clean(used.model) };
338
+ cursor = listed?.nextCursor || null;
339
+ if (!cursor) return null;
340
+ }
341
+ return null;
342
+ }
343
+
344
+ /** Wait for the final answer of the task turn `runId` of `sessionId` ended
345
+ * completed: the end of the latest turn since it (a Lead that handed work
346
+ * to agents ends its turn and answers in the turn their results wake),
347
+ * once the session is idle with no pending agent work (pendingWork) for
348
+ * `settleMs`. `immediate` is runId's own end, when this process saw it.
349
+ * `finish(status, text)` runs once; `check` re-evaluates (call it on every
350
+ * state change of `runtime`), `stop` ends the wait. */
351
+ function followTaskToFinal(
352
+ { loadSession, owedResults, deliverCompletion, pendingWork, settleMs, pollMs, pendingCapMs, log },
353
+ { sessionId, runId, runtime, immediate = null, wakeOwed = false, finish, progress = null }
354
+ ) {
355
+ let progressed = false;
356
+ let pendingSince = 0;
357
+ // Agent work still pending `pendingCapMs` after the chain's latest turn
358
+ // ended: `progress(text)` reports that answer once, marked still in
359
+ // progress, and the wait for the final one goes on.
360
+ async function reportProgress() {
361
+ if (progressed || typeof progress !== 'function') return;
362
+ const latest = latestTurnOutcome((await loadSession(sessionId))?.messages, runId);
363
+ let end = latest?.outcome && latest.outcome !== 'open' ? latest.outcome : null;
364
+ if (immediate && (!latest || latest.id === runId)) end = immediate;
365
+ if (!end) return;
366
+ pendingSince ||= Date.now();
367
+ if (Date.now() - (latest?.at || pendingSince) < pendingCapMs) return;
368
+ progressed = true;
369
+ await progress(`${STILL_WORKING_TEXT}\n\n${end.text}`);
370
+ }
371
+ let done = false;
372
+ let checking = false;
373
+ let again = false;
374
+ let candidate = '';
375
+ let candidateAt = 0;
376
+ let timer = null;
377
+ let wakes = 0;
378
+ let wokeAt = 0;
379
+ const unwoken = (why) => {
380
+ log(`${sessionId} could not take its agents' results after the restart: ${why}`);
381
+ return { key: 'unwoken', status: 'failed', text: RESUME_FAILED_TEXT, now: true };
382
+ };
383
+ // `wakeOwed` (a restarted watch): deliver the agent results queued for
384
+ // the idle session through the notification bus so a turn takes them, at
385
+ // most OWED_WAKE_TRIES times, a poll apart; null while that turn is owed.
386
+ async function wakeForOwed() {
387
+ const owed = await owedResults(sessionId);
388
+ if (!owed.length) return undefined;
389
+ if (Date.now() - wokeAt < pollMs) return null;
390
+ if (wakes >= OWED_WAKE_TRIES) return unwoken(`still not taken after ${wakes} deliveries`);
391
+ wakes += 1;
392
+ wokeAt = Date.now();
393
+ let delivered = false;
394
+ try {
395
+ if (typeof deliverCompletion === 'function') {
396
+ const results = await Promise.all(owed.map(({ text, meta }) => deliverCompletion(sessionId, text, meta)));
397
+ delivered = results.some((result) => result === true);
398
+ }
399
+ } catch (error) {
400
+ return unwoken(error?.message || error);
401
+ }
402
+ return delivered ? null : unwoken('no session runtime took the delivery');
403
+ }
404
+ const arm = (ms) => {
405
+ clearTimeout(timer);
406
+ timer = setTimeout(() => void check().catch(failed), ms);
407
+ timer.unref?.();
408
+ };
409
+ const stop = () => {
410
+ done = true;
411
+ clearTimeout(timer);
412
+ };
413
+ const failed = (error) => log(`following ${sessionId} to its final answer failed: ${error?.message || error}`);
414
+ // The end to report now, or null while the task still works.
415
+ async function ready() {
416
+ if (runtime?.getState?.()?.busy) return null;
417
+ if (wakeOwed) {
418
+ const woke = await wakeForOwed();
419
+ if (woke !== undefined) return woke;
420
+ }
421
+ if (await pendingWork(sessionId)) {
422
+ await reportProgress();
423
+ return null;
424
+ }
425
+ const stored = await loadSession(sessionId);
426
+ if (!stored && !immediate) return { key: 'gone', status: 'cancelled', text: GONE_TEXT };
427
+ const latest = latestTurnOutcome(stored?.messages, runId);
428
+ // Provenance the chain needs is missing: its answer may not be final.
429
+ if (latest?.ambiguous) {
430
+ await reportProgress();
431
+ return null;
432
+ }
433
+ if (immediate && (!latest || latest.id === runId)) return { key: runId, ...immediate };
434
+ if (!latest?.outcome || latest.outcome === 'open') return null;
435
+ return { key: `${latest.id}:${latest.outcome.status}`, ...latest.outcome };
436
+ }
437
+ async function check() {
438
+ if (done) return;
439
+ if (checking) {
440
+ again = true;
441
+ return;
442
+ }
443
+ checking = true;
444
+ try {
445
+ do {
446
+ again = false;
447
+ const end = await ready();
448
+ if (done) return;
449
+ if (!end) {
450
+ candidate = '';
451
+ arm(pollMs);
452
+ } else if (!end.now && settleMs > 0 && (end.key !== candidate || Date.now() - candidateAt < settleMs)) {
453
+ // Final only if it is still the answer after the quiet period.
454
+ if (end.key !== candidate) {
455
+ candidate = end.key;
456
+ candidateAt = Date.now();
457
+ }
458
+ arm(candidateAt + settleMs - Date.now());
459
+ } else {
460
+ stop();
461
+ await finish(end.status, end.text);
462
+ }
463
+ } while (again && !done);
464
+ } finally {
465
+ checking = false;
466
+ }
467
+ }
468
+ return { check: () => check().catch(failed), stop };
469
+ }
470
+
471
+ /** Store the durable watch of turn `id` before it can start, so no end goes
472
+ * unreported across a restart; a turn whose watch could not be stored runs
473
+ * without a report (reportsBack false) rather than claim one. `noReport`:
474
+ * why the turn will not report back, told to the bot (reportsBackReason). */
475
+ async function watchTurn({ watches, owner, log }, sessionId, origin, id) {
476
+ if (origin.notify === false) return { notify: false, noReport: NOTIFY_OFF_REASON };
477
+ try {
478
+ await watches.add({
479
+ runId: id,
480
+ agentId: origin.agentId,
481
+ homeSessionId: origin.sessionId,
482
+ taskSessionId: sessionId,
483
+ title: origin.title,
484
+ ...(origin.target === 'session' ? { target: 'session' } : {}),
485
+ ...(origin.textOnly ? { textOnly: true } : {}),
486
+ ...(origin.hop > 0 ? { hop: origin.hop } : {}),
487
+ owner,
488
+ at: Date.now(),
489
+ });
490
+ return { notify: true, noReport: '' };
491
+ } catch (error) {
492
+ log(`watch of ${sessionId} failed, sending without a report: ${error?.message || error}`);
493
+ return {
494
+ notify: false,
495
+ noReport:
496
+ error?.code === 'REPORT_WATCH_CAP'
497
+ ? WATCH_CAP_REASON
498
+ : `its report could not be recorded (${error?.message || error}); check that session for the result`,
499
+ };
500
+ }
501
+ }
502
+
503
+ /** Deliver report `payload` of task session `sessionId` to agent `agentId`,
504
+ * retrying after each of `retryDelaysMs`; the last failure throws. */
505
+ async function deliverWithRetries({ deliver, retryDelaysMs, log }, agentId, sessionId, payload) {
506
+ for (let attempt = 0; ; attempt += 1) {
507
+ try {
508
+ return await deliver(agentId, payload);
509
+ } catch (error) {
510
+ if (attempt >= retryDelaysMs.length) throw error;
511
+ log(`report of ${sessionId} failed (try ${attempt + 1}), retrying: ${error?.message || error}`);
512
+ await new Promise((resolveWait) => setTimeout(resolveWait, retryDelaysMs[attempt]));
513
+ }
514
+ }
515
+ }
516
+
517
+ /** Start task session `sessionId` in its own worktree; a repo that cannot
518
+ * have one keeps the Project folder. */
519
+ async function requestTaskWorktree(sessions, sessionId, title, requestId) {
520
+ try {
521
+ await sessions.configureSession(
522
+ {
523
+ sessionId,
524
+ action: 'requestWorktree',
525
+ args: [{ provision: true, carryChanges: 'leave', prompt: title, txId: requestId }],
526
+ },
527
+ { revision: 1 }
528
+ );
529
+ } catch (error) {
530
+ if (error?.code !== 'WORKTREE_UNSUPPORTED_REPO') throw error;
531
+ }
532
+ }
533
+
534
+ /** The origin a `send` of agent `agentId` (from its home or room member
535
+ * session `callerSessionId`) reports to: the stored one of a task session
536
+ * it created, else one for this send only — never stored on the target,
537
+ * whose own origin and owner stay as they are. Either reports to the
538
+ * conversation the send came from. Null for a caller that is no agent
539
+ * conversation. */
540
+ async function sendReportOrigin({ loadSession, agentName }, sessionId, agentId, callerSessionId) {
541
+ if (!agentId) return null;
542
+ const target = await loadSession(sessionId);
543
+ const origin = target?.taskOrigin;
544
+ if (origin?.agentId === agentId) {
545
+ return callerSessionId && callerSessionId !== origin.sessionId ? { ...origin, sessionId: callerSessionId } : origin;
546
+ }
547
+ const title = (clean(target?.title) || sessionId).slice(0, TITLE_LIMIT);
548
+ // Files come back only from sessions the agent created (above) or its own
549
+ // room member threads; any other session reports its text alone.
550
+ const own = roomMemberOfSession(sessionId)?.agentId === agentId;
551
+ return {
552
+ agentId,
553
+ sessionId: callerSessionId,
554
+ title,
555
+ notify: true,
556
+ target: 'session',
557
+ ...(own ? {} : { textOnly: true, envelopeName: await agentName(agentId) }),
558
+ };
559
+ }
560
+
561
+ /** Re-attach one watch a stopped daemon left: report the turn's end once
562
+ * from the target's record, or watch it until it ends. */
563
+ async function rearmTaskWatch(
564
+ { deliver, loadSession, materializeSession, queuedSubmissionIds, artifactFields, followToFinal, log },
565
+ watch
566
+ ) {
567
+ const sessionId = watch.taskSessionId;
568
+ const finish = async (status, text = '') =>
569
+ deliver(watch.agentId, {
570
+ runId: `${watch.runId}_end`,
571
+ taskSessionId: sessionId,
572
+ title: watch.title,
573
+ status,
574
+ text,
575
+ ...(watch.target === 'session' ? { target: 'session' } : {}),
576
+ ...(watch.hop > 0 ? { hop: watch.hop } : {}),
577
+ ...roomFrom(watch.homeSessionId),
578
+ watchId: watch.runId,
579
+ ...(watch.textOnly || text === GONE_TEXT || text === LOST_TEXT
580
+ ? {}
581
+ : await artifactFields(sessionId, watch.runId)),
582
+ });
583
+ if (!(await loadSession(sessionId))) return finish('cancelled', GONE_TEXT);
584
+ let runtime;
585
+ try {
586
+ runtime = await materializeSession(sessionId);
587
+ } catch (error) {
588
+ // It can never take its agents' results: say so once instead of waiting.
589
+ log(`${sessionId} could not be loaded after the restart: ${error?.message || error}`);
590
+ return finish('failed', RESUME_FAILED_TEXT);
591
+ }
592
+ let done = false;
593
+ let checking = false;
594
+ let again = false;
595
+ let unsubscribe = () => {};
596
+ // Set once the watched turn is found ended completed: the report then
597
+ // waits for the task's final answer, as in the process that sent it.
598
+ let follow = null;
599
+ const check = async () => {
600
+ if (done) return;
601
+ if (follow) return follow.check();
602
+ if (checking) {
603
+ again = true;
604
+ return;
605
+ }
606
+ checking = true;
607
+ try {
608
+ do {
609
+ again = false;
610
+ const outcome = submissionOutcome((await loadSession(sessionId))?.messages, watch.runId);
611
+ const state = runtime?.getState?.() || {};
612
+ let end = outcome && outcome !== 'open' ? outcome : null;
613
+ if (!end && !state.busy) {
614
+ const queued = (state.queued || []).some((entry) => entry?.id === watch.runId);
615
+ if (outcome === 'open' && !queued) end = { status: 'cancelled', text: '' };
616
+ else if (!outcome && !queued && !(await queuedSubmissionIds(sessionId)).includes(watch.runId)) {
617
+ end = { status: 'cancelled', text: LOST_TEXT };
618
+ }
619
+ }
620
+ if (end?.status === 'completed') {
621
+ follow = followToFinal({
622
+ sessionId,
623
+ runId: watch.runId,
624
+ runtime,
625
+ wakeOwed: true,
626
+ progress: (text) =>
627
+ deliver(watch.agentId, {
628
+ runId: `${watch.runId}_progress`,
629
+ taskSessionId: sessionId,
630
+ title: watch.title,
631
+ status: 'working',
632
+ text,
633
+ ...(watch.target === 'session' ? { target: 'session' } : {}),
634
+ ...(watch.hop > 0 ? { hop: watch.hop } : {}),
635
+ ...roomFrom(watch.homeSessionId),
636
+ }),
637
+ finish: async (status, text) => {
638
+ done = true;
639
+ unsubscribe();
640
+ await finish(status, text);
641
+ },
642
+ });
643
+ } else if (end) {
644
+ done = true;
645
+ unsubscribe();
646
+ await finish(end.status, end.text);
647
+ }
648
+ } while (again && !done && !follow);
649
+ } finally {
650
+ checking = false;
651
+ }
652
+ if (follow) await follow.check();
653
+ };
654
+ const failed = (error) => log(`re-attaching ${sessionId} failed: ${error?.message || error}`);
655
+ unsubscribe = runtime?.subscribe?.(() => void check().catch(failed)) || unsubscribe;
656
+ await check();
657
+ }
658
+
54
659
  export function createAgentTaskSessions({
55
660
  getSessionService,
56
661
  deliver = async (agentId, report) => (await import('./agent-home-delivery.mjs')).deliverAgentTask(agentId, report),
662
+ // The durable watch of each reporting turn until its end is queued.
663
+ watches = {
664
+ add: async (watch) => (await import('./agent-home-delivery.mjs')).watchAgentTask(watch),
665
+ list: async () => (await import('./agent-home-delivery.mjs')).agentTaskWatches(),
666
+ remove: async (runId) => (await import('./agent-home-delivery.mjs')).unwatchAgentTask(runId),
667
+ },
668
+ // Submissions still queued (durably) for a session after a restart.
669
+ queuedSubmissionIds = async (sessionId) =>
670
+ (await (await import('../tui/session/tui-steering-persist.mjs')).readTuiSteeringPersist(sessionId))
671
+ .map((row) => row?.submissionId)
672
+ .filter(Boolean),
57
673
  loadSession = async (sessionId) =>
58
674
  (await import('../runtime/agent/orchestrator/session/store.mjs')).loadSession(sessionId),
59
675
  listProjects = async () => (await import('../runtime/shared/projects.mjs')).listProjects(),
60
676
  newTaskDefault = async () =>
61
677
  (await import('../runtime/agent/orchestrator/config.mjs')).loadConfig({ secrets: false }).worktree?.newTaskDefault,
62
- modelRoute = async (modelRef) => {
63
- const [{ modelRouteFields, parseScheduleModelRef }, { migrateCodexRoute }, { loadConfig }] = await Promise.all([
64
- import('../runtime/shared/schedule-model-ref.mjs'),
65
- import('../runtime/agent/orchestrator/providers/openai-oauth-catalog-store.mjs'),
66
- import('../runtime/agent/orchestrator/config.mjs'),
67
- ]);
68
- // A provider/model[@effort…] ref (a superseded model runs on its
69
- // successor) or a preset name.
70
- const ref = parseScheduleModelRef(modelRef);
71
- if (ref && typeof ref === 'object') return modelRouteFields(migrateCodexRoute(ref));
72
- const preset = loadConfig({ secrets: false }).presets?.find((p) => p.id === ref || p.name === ref);
73
- if (!preset) throw new Error(`model "${ref}" is neither a provider/model route nor a known preset name`);
74
- return modelRouteFields(preset);
678
+ modelRoute = configModelRoute,
679
+ // The Lead (Main) route of the config — what a desktop New task starts on.
680
+ leadRoute = configLeadRoute,
681
+ // The model the user last used in a coding session.
682
+ lastUsedRoute = lastUsedSessionRoute,
683
+ // The files turn `runId` of `sessionId` produced (handoff-attachments.mjs),
684
+ // read from its live runtime when one is given, else its stored record.
685
+ turnArtifacts = async (sessionId, runId, runtime = null) => {
686
+ const stored = await loadSession(sessionId);
687
+ const live = typeof runtime?.readModelMessages === 'function' ? await runtime.readModelMessages(0) : null;
688
+ const messages = turnSlice(Array.isArray(live?.messages) ? live.messages : stored?.messages, { runId });
689
+ return collectTurnArtifacts(messages, { cwd: stored?.cwd, roots: sessionRoots(stored) });
75
690
  },
691
+ // The name a bot goes by in the envelope of its messages to other sessions.
692
+ agentName = async (agentId) =>
693
+ (await import('../runtime/agent/orchestrator/context/role-instructions.mjs')).agentDisplayName(agentId),
694
+ // Waits before each in-process retry of a report that could not be queued.
695
+ retryDelaysMs = REPORT_RETRY_DELAYS_MS,
696
+ // Whether a task session still waits on agent work it handed off; its
697
+ // turn's end then is not the final answer yet.
698
+ pendingWork = taskSessionPendingWork,
699
+ settleMs = FINAL_SETTLE_MS,
700
+ pollMs = FINAL_POLL_MS,
701
+ // After a restart: the agent results queued for a task session, and the
702
+ // notification-bus delivery (the session runtime host's
703
+ // notifySessionCompletion) that makes it take them in a turn — the restart
704
+ // left nothing to wake it. `settleAgentResults` first turns the agents the
705
+ // restart killed into queued failure results (agent-tool restart-notify).
706
+ owedResults = owedAgentResults,
707
+ deliverCompletion = null,
708
+ settleAgentResults = async () => {},
709
+ // Pending agent work this long after the latest answer reports that answer
710
+ // as still in progress (once); the final report still follows.
711
+ pendingCapMs = PENDING_CAP_MS,
76
712
  log = (line) => process.stderr.write(`mixdog task sessions: ${line}\n`),
77
713
  }) {
714
+ /** The report fields of turn `runId`'s files; none when it produced none. */
715
+ const artifactFields = async (sessionId, runId, runtime = null) => {
716
+ try {
717
+ const { files, omitted } = await turnArtifacts(sessionId, runId, runtime);
718
+ return files.length ? { files, ...(omitted ? { omitted } : {}) } : {};
719
+ } catch (error) {
720
+ log(`collecting the files of ${sessionId} failed: ${error?.message || error}`);
721
+ return {};
722
+ }
723
+ };
724
+ // Marks this process's watches: recovery re-attaches only an earlier one's.
725
+ const processToken = randomUUID();
78
726
  const service = () => {
79
727
  const value = getSessionService();
80
728
  if (!value) throw new Error('session: session service is unavailable');
81
729
  return value;
82
730
  };
83
731
 
732
+ const followToFinal = (follow) =>
733
+ followTaskToFinal(
734
+ { loadSession, owedResults, deliverCompletion, pendingWork, settleMs, pollMs, pendingCapMs, log },
735
+ follow
736
+ );
737
+
84
738
  /** Submit `prompt` as the origin agent's turn; its end (and a wait for the
85
739
  * user in it) reports to the agent's home. Resolves on intake. */
86
740
  async function agentTurn(sessionId, prompt, origin, id = `atask_${randomUUID()}`, beforeReport = null) {
87
741
  const sessions = service();
742
+ const target = origin.target === 'session' ? { target: 'session' } : {};
743
+ const { notify, noReport } = await watchTurn({ watches, owner: processToken, log }, sessionId, origin, id);
744
+ const dropWatch = () =>
745
+ watches.remove(id).catch((error) => log(`dropping the watch of ${sessionId} failed: ${error?.message || error}`));
746
+ let runtime = null;
747
+ // A report that cannot be queued is retried in this process; when every try
748
+ // fails the durable watch stays and restart recovery delivers it (the queue
749
+ // dedupes by runId, so a retry never posts twice).
88
750
  const report = (status, text = '') =>
89
751
  Promise.resolve(beforeReport)
90
- .then(() =>
91
- deliver(origin.agentId, {
92
- runId: `${id}_${status === 'blocked' ? 'blocked' : 'end'}`,
752
+ .then(async () =>
753
+ deliverWithRetries({ deliver, retryDelaysMs, log }, origin.agentId, sessionId, {
754
+ // 'working': a still-in-progress answer (followToFinal), not the end.
755
+ runId: `${id}_${{ blocked: 'blocked', working: 'progress' }[status] || 'end'}`,
93
756
  taskSessionId: sessionId,
94
757
  title: origin.title,
95
758
  status,
96
759
  text,
760
+ ...target,
761
+ ...(origin.hop > 0 ? { hop: origin.hop } : {}),
762
+ ...roomFrom(origin.sessionId),
763
+ // A turn's end brings back the files it produced, when the agent
764
+ // may receive them (reportOrigin `textOnly`).
765
+ ...(status === 'blocked' || status === 'working'
766
+ ? {}
767
+ : { watchId: id, ...(origin.textOnly ? {} : await artifactFields(sessionId, id, runtime)) }),
97
768
  })
98
769
  )
99
770
  .catch((error) => log(`report of ${sessionId} failed: ${error?.message || error}`));
100
- const notify = origin.notify !== false;
101
771
  let unwatch = () => {};
102
- if (notify) {
103
- const runtime = await sessions.materializeSession(sessionId);
104
- let blocked = false;
105
- unwatch =
106
- runtime?.subscribe?.(() => {
107
- if (blocked || !runtime.getState?.()?.toolApproval) return;
108
- blocked = true;
109
- void report('blocked');
110
- }) || unwatch;
111
- }
112
- const result = await sessions.submitSession({
113
- sessionId,
114
- prompt,
115
- options: {
116
- id,
117
- mode: 'prompt',
118
- transcriptMeta: { sender: 'lead' },
119
- ...(notify
120
- ? {
121
- onSettled: (detail) => {
122
- unwatch();
123
- void report(turnStatus(detail), detail?.result?.content || detail?.error || '');
124
- },
125
- }
126
- : {}),
772
+ // Set once the turn ended completed: the report waits for the final answer.
773
+ let follow = null;
774
+ let result;
775
+ try {
776
+ if (notify) {
777
+ runtime = await sessions.materializeSession(sessionId);
778
+ let blocked = false;
779
+ unwatch =
780
+ runtime?.subscribe?.(() => {
781
+ follow?.check();
782
+ if (blocked || !runtime.getState?.()?.toolApproval) return;
783
+ blocked = true;
784
+ void report('blocked');
785
+ }) || unwatch;
786
+ }
787
+ // A bot's send to a session that is not its own task session reaches the
788
+ // model inside the sender envelope (`envelopeName`, set by reportOrigin).
789
+ const sent = origin.envelopeName
790
+ ? wrapAgentMessage(prompt, { name: origin.envelopeName, agentId: origin.agentId })
791
+ : prompt;
792
+ result = await sessions.submitSession({
793
+ sessionId,
794
+ prompt: sent,
795
+ options: {
796
+ id,
797
+ mode: 'prompt',
798
+ // `fromAgent`: the sending bot, recorded when it writes into another
799
+ // bot's home chat (otherwise the row would read as the user's);
800
+ // `hop`: its place in a bot-to-bot chain (bot-hops.mjs).
801
+ transcriptMeta: {
802
+ sender: 'lead',
803
+ ...(origin.envelopeName ? { fromAgent: origin.agentId } : {}),
804
+ ...(origin.hop > 0 ? { hop: origin.hop } : {}),
805
+ },
806
+ ...(notify
807
+ ? {
808
+ onSettled: (detail) => {
809
+ const status = turnStatus(detail);
810
+ const text = detail?.result?.content || detail?.error || '';
811
+ // A failed or cancelled turn reports at once; a completed one
812
+ // reports the task's final answer (followToFinal).
813
+ if (status !== 'completed') {
814
+ unwatch();
815
+ void report(status, text);
816
+ return;
817
+ }
818
+ follow = followToFinal({
819
+ sessionId,
820
+ runId: id,
821
+ runtime,
822
+ immediate: { status, text },
823
+ finish: async (endStatus, endText) => {
824
+ unwatch();
825
+ await report(endStatus, endText);
826
+ },
827
+ progress: (progressText) => report('working', progressText),
828
+ });
829
+ follow.check();
830
+ },
831
+ }
832
+ : {}),
833
+ },
834
+ });
835
+ } catch (error) {
836
+ unwatch();
837
+ if (notify) await dropWatch();
838
+ throw error;
839
+ }
840
+ const accepted = result?.accepted === true;
841
+ if (!accepted) {
842
+ unwatch();
843
+ if (notify) await dropWatch();
844
+ }
845
+ return {
846
+ accepted,
847
+ reportsBack: accepted && notify,
848
+ ...(accepted && !notify ? { reportsBackReason: noReport } : {}),
849
+ };
850
+ }
851
+
852
+ const rearm = (watch) =>
853
+ rearmTaskWatch(
854
+ {
855
+ deliver,
856
+ loadSession,
857
+ materializeSession: (sessionId) => service().materializeSession(sessionId),
858
+ queuedSubmissionIds,
859
+ artifactFields,
860
+ followToFinal,
861
+ log,
127
862
  },
128
- });
129
- if (result?.accepted !== true) unwatch();
130
- return result?.accepted === true;
863
+ watch
864
+ );
865
+
866
+ /** Daemon start: re-attach every reporting turn an earlier process watched.
867
+ * Resolves once each is reported or watched. */
868
+ async function recover() {
869
+ const pending = (await watches.list()).filter((watch) => watch?.owner !== processToken);
870
+ // The agents the restart killed become queued failure results first, so
871
+ // each re-attached task session has them to take (followToFinal wakeOwed).
872
+ if (pending.length) {
873
+ await Promise.resolve()
874
+ .then(settleAgentResults)
875
+ .catch((error) => log(`settling the agents of the previous process failed: ${error?.message || error}`));
876
+ }
877
+ await Promise.all(
878
+ pending.map((watch) =>
879
+ rearm(watch).catch((error) => log(`re-attaching ${watch?.taskSessionId} failed: ${error?.message || error}`))
880
+ )
881
+ );
882
+ return pending.length;
131
883
  }
132
884
 
133
885
  /** Create the task session for `brief` and start it; returns at once. */
134
- async function create(args = {}, { callerSessionId, callerAgentId, callerCwd } = {}) {
886
+ async function create(
887
+ args = {},
888
+ { callerSessionId, callerAgentId, callerCwd, callerHop = 0, attachmentParts = [] } = {}
889
+ ) {
135
890
  if (!callerAgentId) throw new Error('session: create is only for an agent conversation');
136
891
  const brief = typeof args.brief === 'string' ? args.brief.trim() : '';
137
892
  if (!brief) throw new Error('session: brief is required for action=create');
@@ -145,7 +900,13 @@ export function createAgentTaskSessions({
145
900
  title,
146
901
  notify: args.notify !== false,
147
902
  };
148
- const route = clean(args.model) ? await modelRoute(clean(args.model)) : {};
903
+ // A task session runs in coding mode under Lead: an explicit model wins,
904
+ // else the Lead route, else the last-used model. Never a model-less
905
+ // session — the composer would show no model and its first turn would fail.
906
+ const route = clean(args.model)
907
+ ? await modelRoute(clean(args.model))
908
+ : (await leadRoute()) || (await lastUsedRoute(service()));
909
+ if (!route) throw new Error('session: no model to start the task session on. Choose a Lead model first.');
149
910
  const sessions = service();
150
911
  const created = await sessions.createSession({
151
912
  ...route,
@@ -157,44 +918,45 @@ export function createAgentTaskSessions({
157
918
  if (!sessionId) throw new Error('session: task session creation returned no session id');
158
919
  // The user's New task default: a Project task starts in its own worktree.
159
920
  if (where.project && (await newTaskDefault()) === 'worktree') {
160
- try {
161
- await sessions.configureSession(
162
- {
163
- sessionId,
164
- action: 'requestWorktree',
165
- args: [{ provision: true, carryChanges: 'leave', prompt: title, txId: requestId }],
166
- },
167
- { revision: 1 }
168
- );
169
- } catch (error) {
170
- if (error?.code !== 'WORKTREE_UNSUPPORTED_REPO') throw error;
171
- }
921
+ await requestTaskWorktree(sessions, sessionId, title, requestId);
172
922
  }
173
923
  // "Started" lands in the home before any report of the brief's turn.
174
924
  let started = () => {};
175
925
  const ready = new Promise((resolveReady) => {
176
926
  started = resolveReady;
177
927
  });
178
- const accepted = await agentTurn(sessionId, brief, taskOrigin, requestId, ready);
928
+ // The brief carries the files the agent forwarded (workspace-tools.mjs).
929
+ const prompt = handoffContent(brief, attachmentParts);
930
+ const { accepted, reportsBack, reportsBackReason } = await agentTurn(
931
+ sessionId,
932
+ prompt,
933
+ callerHop > 0 ? { ...taskOrigin, hop: callerHop } : taskOrigin,
934
+ requestId,
935
+ ready
936
+ );
179
937
  if (!accepted) throw new Error(`session: task session ${sessionId} did not accept the brief`);
180
- if (taskOrigin.notify) {
938
+ if (reportsBack) {
181
939
  await deliver(callerAgentId, {
182
940
  runId: `${requestId}_started`,
183
941
  taskSessionId: sessionId,
184
942
  title,
185
943
  status: 'started',
944
+ ...roomFrom(callerSessionId),
186
945
  }).catch((error) => log(`start line of ${sessionId} failed: ${error?.message || error}`));
187
946
  }
188
947
  started();
189
- return { session_id: sessionId, title, project: where.project?.path ?? null };
948
+ // Started, not done: the final answer is reported later.
949
+ return {
950
+ session_id: sessionId,
951
+ status: 'started',
952
+ title,
953
+ project: where.project?.path ?? null,
954
+ ...(taskOrigin.notify && !reportsBack ? { reportsBack: false, reportsBackReason } : {}),
955
+ };
190
956
  }
191
957
 
192
- /** The origin of a task session, when `agentId` created it. */
193
- async function originFor(sessionId, agentId) {
194
- if (!agentId) return null;
195
- const origin = (await loadSession(sessionId))?.taskOrigin;
196
- return origin?.agentId === agentId ? origin : null;
197
- }
958
+ const reportOrigin = (sessionId, agentId, callerSessionId) =>
959
+ sendReportOrigin({ loadSession, agentName }, sessionId, agentId, callerSessionId);
198
960
 
199
- return { create, originFor, send: (sessionId, prompt, origin) => agentTurn(sessionId, prompt, origin) };
961
+ return { create, reportOrigin, recover, send: (sessionId, prompt, origin) => agentTurn(sessionId, prompt, origin) };
200
962
  }