mixdog 1.3.4 → 1.4.0

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 (472) hide show
  1. package/LICENSES/oh-my-pi-MIT.txt +23 -0
  2. package/NOTICE.md +17 -2
  3. package/README.md +11 -0
  4. package/package.json +3 -11
  5. package/scripts/daemon-stub.mjs +1 -1
  6. package/scripts/embed-google-oauth-client.mjs +49 -0
  7. package/scripts/gate-local.mjs +4 -2
  8. package/scripts/lib/test-spawn-ledger.mjs +46 -1
  9. package/scripts/native-release-status.mjs +2 -2
  10. package/scripts/recall-history-cases.json +1 -1
  11. package/scripts/suite-health.mjs +10 -1
  12. package/scripts/test.mjs +12 -11
  13. package/src/agents/reviewer/AGENT.md +7 -11
  14. package/src/agents/reviewer/agent.json +1 -1
  15. package/src/defaults/shipped-history.json +25 -1
  16. package/src/defaults/skills/browser-use/SKILL.md +77 -17
  17. package/src/defaults/skills/code-tidy/SKILL.md +142 -270
  18. package/src/defaults/skills/code-tidy/references/agent-cleanup.md +4 -4
  19. package/src/defaults/skills/code-tidy/references/cleanup-report.md +31 -76
  20. package/src/defaults/skills/code-tidy/references/dead-code.md +6 -12
  21. package/src/defaults/skills/computer-use/SKILL.md +33 -9
  22. package/src/defaults/skills/setup/SKILL.md +29 -2
  23. package/src/defaults/skills/setup/references/actions.md +117 -28
  24. package/src/defaults/skills/setup/references/surfaces.md +77 -35
  25. package/src/defaults/skills/setup-guide/SKILL.md +3 -3
  26. package/src/defaults/skills/visualize/SKILL.md +2 -1
  27. package/src/lib/keychain-cjs.cjs +359 -29
  28. package/src/lib/rules-builder.cjs +10 -8
  29. package/src/rules/agent/AGENT.md +6 -6
  30. package/src/rules/agent-mode/AGENT_MODE.md +10 -2
  31. package/src/rules/agent-mode/MIXY.md +12 -0
  32. package/src/rules/lead/LEAD.md +18 -20
  33. package/src/rules/shared/05-tool-calls.md +5 -6
  34. package/src/rules/shared/10-tool-workflow.md +2 -0
  35. package/src/rules/shared/60-verification.md +7 -5
  36. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -3
  37. package/src/runtime/agent/orchestrator/agent-runtime/heartbeat-completion.mjs +193 -0
  38. package/src/runtime/agent/orchestrator/agent-runtime/title-completion.mjs +3 -1
  39. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +3 -1
  40. package/src/runtime/agent/orchestrator/config.mjs +34 -0
  41. package/src/runtime/agent/orchestrator/context/collect.mjs +3 -0
  42. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +72 -67
  43. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +54 -1
  44. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +3 -3
  45. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +177 -73
  46. package/src/runtime/agent/orchestrator/mcp/client.mjs +25 -2
  47. package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +31 -2
  48. package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +16 -3
  49. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +42 -7
  50. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +21 -2
  51. package/src/runtime/agent/orchestrator/mcp/oauth-shared.mjs +23 -0
  52. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +11 -1
  53. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +230 -137
  54. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +64 -7
  55. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +18 -5
  56. package/src/runtime/agent/orchestrator/providers/anthropic-cache-warm.mjs +165 -0
  57. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  58. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +4 -1
  59. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/gzip-post.mjs +14 -14
  60. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +160 -69
  61. package/src/runtime/agent/orchestrator/providers/anthropic-overload-fallback.mjs +25 -3
  62. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +64 -33
  63. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +135 -87
  64. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +52 -8
  65. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +10 -1
  66. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +22 -6
  67. package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +15 -4
  68. package/src/runtime/agent/orchestrator/providers/antigravity-stream.mjs +17 -2
  69. package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +142 -104
  70. package/src/runtime/agent/orchestrator/providers/cache-warm-admission.mjs +90 -0
  71. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
  72. package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +10 -4
  73. package/src/runtime/agent/orchestrator/providers/cursor-wire-protobuf.mjs +10 -217
  74. package/src/runtime/agent/orchestrator/providers/cursor-wire-transport.mjs +25 -9
  75. package/src/runtime/agent/orchestrator/providers/cursor.mjs +22 -6
  76. package/src/runtime/agent/orchestrator/providers/devin-auth.mjs +409 -0
  77. package/src/runtime/agent/orchestrator/providers/devin-wire-families.mjs +187 -0
  78. package/src/runtime/agent/orchestrator/providers/devin-wire-identity.mjs +342 -0
  79. package/src/runtime/agent/orchestrator/providers/devin-wire-models.mjs +541 -0
  80. package/src/runtime/agent/orchestrator/providers/devin-wire-protobuf.mjs +305 -0
  81. package/src/runtime/agent/orchestrator/providers/devin-wire-request.mjs +558 -0
  82. package/src/runtime/agent/orchestrator/providers/devin-wire-schema.mjs +880 -0
  83. package/src/runtime/agent/orchestrator/providers/devin-wire-session.mjs +190 -0
  84. package/src/runtime/agent/orchestrator/providers/devin-wire-tool-args.mjs +351 -0
  85. package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +560 -0
  86. package/src/runtime/agent/orchestrator/providers/devin.mjs +491 -0
  87. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +3 -0
  88. package/src/runtime/agent/orchestrator/providers/gemini-output-limit.mjs +46 -0
  89. package/src/runtime/agent/orchestrator/providers/gemini-reasoning-relay.mjs +30 -0
  90. package/src/runtime/agent/orchestrator/providers/gemini-request-body.mjs +7 -11
  91. package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +61 -22
  92. package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +1 -0
  93. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +71 -4
  94. package/src/runtime/agent/orchestrator/providers/gemini.mjs +25 -2
  95. package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
  96. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +96 -5
  97. package/src/runtime/agent/orchestrator/providers/lib/oauth-token-utils.mjs +17 -3
  98. package/src/runtime/agent/orchestrator/providers/lib/tail-reminder-split.mjs +23 -5
  99. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +52 -10
  100. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +8 -1
  101. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +1 -0
  102. package/src/runtime/agent/orchestrator/providers/npm-cli-version.mjs +11 -3
  103. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +19 -1
  104. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +39 -3
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +3 -1
  106. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +2 -0
  107. package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +6 -1
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +10 -9
  109. package/src/runtime/agent/orchestrator/providers/openai-ws-send/send-context.mjs +22 -3
  110. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/attempt-context.mjs +43 -5
  111. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/handshake-failure.mjs +3 -1
  112. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/stream-failure.mjs +6 -3
  113. package/src/runtime/agent/orchestrator/providers/opencode-go-gemini.mjs +1 -0
  114. package/src/runtime/agent/orchestrator/providers/provider-availability.mjs +19 -0
  115. package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +65 -1
  116. package/src/runtime/agent/orchestrator/providers/quota-reading.mjs +38 -12
  117. package/src/runtime/agent/orchestrator/providers/registry.mjs +43 -3
  118. package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +39 -7
  119. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +6 -0
  120. package/src/runtime/agent/orchestrator/providers/wire-protobuf-codec.mjs +368 -0
  121. package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +12 -7
  122. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +2 -0
  123. package/src/runtime/agent/orchestrator/runtime-core/compaction-config-keys.mjs +62 -0
  124. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +37 -16
  125. package/src/runtime/agent/orchestrator/runtime-core/deferred-tool-delta.mjs +100 -23
  126. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +1 -1
  127. package/src/runtime/agent/orchestrator/runtime-core/reaction-tool-defs.mjs +133 -0
  128. package/src/runtime/agent/orchestrator/runtime-core/session-text.mjs +2 -0
  129. package/src/runtime/agent/orchestrator/runtime-core/tool-bridge.mjs +19 -4
  130. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +3 -0
  131. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +125 -30
  132. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +23 -5
  133. package/src/runtime/agent/orchestrator/runtime-core/tool-profile.mjs +35 -10
  134. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +57 -10
  135. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +9 -6
  136. package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +1 -1
  137. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +18 -0
  138. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +0 -3
  139. package/src/runtime/agent/orchestrator/session/agent-room.mjs +10 -4
  140. package/src/runtime/agent/orchestrator/session/cache-warm.mjs +792 -0
  141. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +148 -25
  142. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +475 -58
  143. package/src/runtime/agent/orchestrator/session/compaction-archive.mjs +42 -17
  144. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +8 -0
  145. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +47 -9
  146. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +6 -2
  147. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +36 -4
  148. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +14 -2
  149. package/src/runtime/agent/orchestrator/session/loop/assistant-commit.mjs +8 -0
  150. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +18 -22
  151. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +32 -3
  152. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +11 -2
  153. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +4 -1
  154. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +51 -3
  155. package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +17 -3
  156. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +10 -2
  157. package/src/runtime/agent/orchestrator/session/loop/tool-exec/after-hook.mjs +2 -1
  158. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +14 -4
  159. package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +16 -2
  160. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +16 -1
  161. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +10 -0
  162. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +3 -10
  163. package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +23 -5
  164. package/src/runtime/agent/orchestrator/session/loop/turn-limits.mjs +60 -0
  165. package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +12 -2
  166. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +19 -1
  167. package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +2 -0
  168. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +133 -90
  169. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +14 -0
  170. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -1
  171. package/src/runtime/agent/orchestrator/session/manager/ask-turn-state.mjs +31 -4
  172. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +30 -18
  173. package/src/runtime/agent/orchestrator/session/manager/owner-route-fallbacks.mjs +23 -13
  174. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +11 -2
  175. package/src/runtime/agent/orchestrator/session/manager/provider-replay.mjs +13 -0
  176. package/src/runtime/agent/orchestrator/session/manager/route-fallback-chain.mjs +129 -0
  177. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +20 -2
  178. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +2 -0
  179. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +8 -0
  180. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +1 -1
  181. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +13 -0
  182. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +7 -2
  183. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +102 -15
  184. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +18 -5
  185. package/src/runtime/agent/orchestrator/session/message-ids.mjs +5 -3
  186. package/src/runtime/agent/orchestrator/session/message-reactions.mjs +89 -0
  187. package/src/runtime/agent/orchestrator/session/message-reactions.test-support.mjs +75 -0
  188. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +19 -8
  189. package/src/runtime/agent/orchestrator/session/prompt-cache-idle-gaps.mjs +113 -0
  190. package/src/runtime/agent/orchestrator/session/prompt-cache-touch-store.mjs +127 -0
  191. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +0 -2
  192. package/src/runtime/agent/orchestrator/session/routine-result.mjs +47 -11
  193. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +20 -2
  194. package/src/runtime/agent/orchestrator/session/sensitive-projection.mjs +100 -0
  195. package/src/runtime/agent/orchestrator/session/sensitive-values.mjs +257 -0
  196. package/src/runtime/agent/orchestrator/session/side-question-store.mjs +20 -7
  197. package/src/runtime/agent/orchestrator/session/side-question.mjs +18 -0
  198. package/src/runtime/agent/orchestrator/session/skill-update-notice.mjs +60 -0
  199. package/src/runtime/agent/orchestrator/session/store/live-state.mjs +6 -3
  200. package/src/runtime/agent/orchestrator/session/store/save-delta.mjs +9 -5
  201. package/src/runtime/agent/orchestrator/session/store/serialize.mjs +9 -8
  202. package/src/runtime/agent/orchestrator/session/store/session-persistence-events.mjs +29 -0
  203. package/src/runtime/agent/orchestrator/session/store-agent-worker-header-cache.mjs +85 -0
  204. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +133 -18
  205. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +12 -2
  206. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +44 -14
  207. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +84 -3
  208. package/src/runtime/agent/orchestrator/session/stored-attachment-history.mjs +32 -0
  209. package/src/runtime/agent/orchestrator/session/tail-reminder-rules.mjs +11 -5
  210. package/src/runtime/agent/orchestrator/session/thinking-binding.mjs +108 -0
  211. package/src/runtime/agent/orchestrator/session/tool-batch/pre-dispatch.mjs +2 -2
  212. package/src/runtime/agent/orchestrator/session/tool-batch/record-outcome.mjs +2 -1
  213. package/src/runtime/agent/orchestrator/session/tool-batch/state.mjs +4 -0
  214. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +5 -0
  215. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +25 -2
  216. package/src/runtime/agent/orchestrator/session/transcript-restore/tool-result-status.mjs +9 -7
  217. package/src/runtime/agent/orchestrator/session/transcript-restore/tool-result-text.mjs +2 -2
  218. package/src/runtime/agent/orchestrator/session/turn-failure.mjs +5 -1
  219. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +5 -2
  220. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +12 -1
  221. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/misuse-guard.mjs +240 -0
  222. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/result-format.mjs +1 -1
  223. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/shell-cwd.mjs +38 -0
  224. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +25 -5
  225. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +65 -7
  226. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +7 -1
  227. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +36 -13
  228. package/src/runtime/browser-bridge/action-schema.mjs +13 -2
  229. package/src/runtime/browser-bridge/client.mjs +73 -8
  230. package/src/runtime/browser-bridge/input-fields.mjs +8 -9
  231. package/src/runtime/browser-bridge/mail-links.mjs +107 -0
  232. package/src/runtime/browser-bridge/tool-defs.mjs +1 -1
  233. package/src/runtime/channels/lib/config.mjs +6 -61
  234. package/src/runtime/channels/lib/crash-log.mjs +1 -1
  235. package/src/runtime/channels/lib/event-pipeline.mjs +4 -19
  236. package/src/runtime/channels/lib/event-queue.mjs +3 -18
  237. package/src/runtime/channels/lib/owned-runtime/automation-services.mjs +2 -2
  238. package/src/runtime/channels/lib/owned-runtime/automation-start.mjs +3 -4
  239. package/src/runtime/channels/lib/owned-runtime/config-reload.mjs +4 -59
  240. package/src/runtime/channels/lib/owned-runtime/owned-start.mjs +33 -115
  241. package/src/runtime/channels/lib/owned-runtime/owned-stop.mjs +5 -22
  242. package/src/runtime/channels/lib/owned-runtime/state.mjs +2 -8
  243. package/src/runtime/channels/lib/owned-runtime.mjs +6 -23
  244. package/src/runtime/channels/lib/parent-bridge.mjs +2 -2
  245. package/src/runtime/channels/lib/runtime-paths.mjs +4 -43
  246. package/src/runtime/channels/lib/schedule-errors.mjs +51 -0
  247. package/src/runtime/channels/lib/scheduler.mjs +135 -154
  248. package/src/runtime/channels/lib/schedules-db.mjs +83 -11
  249. package/src/runtime/channels/lib/status-snapshot.mjs +1 -1
  250. package/src/runtime/channels/lib/tool-dispatch.mjs +14 -2
  251. package/src/runtime/channels/lib/webhooks-db.mjs +5 -9
  252. package/src/runtime/channels/lib/worker-bootstrap.mjs +0 -1
  253. package/src/runtime/channels/lib/worker-ipc.mjs +0 -1
  254. package/src/runtime/channels/lib/worker-main.mjs +19 -34
  255. package/src/runtime/computer-bridge/action-schema.mjs +42 -1
  256. package/src/runtime/computer-bridge/actions.mjs +9 -1
  257. package/src/runtime/computer-bridge/client.mjs +180 -8
  258. package/src/runtime/computer-bridge/core-actions.mjs +2 -2
  259. package/src/runtime/computer-bridge/error-recovery.mjs +27 -2
  260. package/src/runtime/computer-bridge/linux-readiness.mjs +65 -23
  261. package/src/runtime/computer-bridge/pending-continuation.mjs +1 -1
  262. package/src/runtime/connectors/connector-accounts-mcp.mjs +230 -0
  263. package/src/runtime/connectors/gmail-app-password.mjs +172 -39
  264. package/src/runtime/connectors/google-mcp.mjs +10 -12
  265. package/src/runtime/memory/lib/pg/adapter.mjs +77 -70
  266. package/src/runtime/memory/lib/pg/supervisor.mjs +24 -12
  267. package/src/runtime/memory/lib/runtime-fetcher.mjs +23 -5
  268. package/src/runtime/office/com/com-adapter.mjs +17 -1
  269. package/src/runtime/office/com/office-com-host.ps1 +51 -1
  270. package/src/runtime/office/pdf/cfb-fixture.mjs +328 -0
  271. package/src/runtime/office/pdf/cfb-protection.mjs +311 -0
  272. package/src/runtime/office/pdf/document-preview.mjs +326 -8
  273. package/src/runtime/office/pdf/ooxml-crypto.mjs +469 -0
  274. package/src/runtime/office/pdf/ooxml-key-worker.mjs +14 -0
  275. package/src/runtime/office/portable/portable-soffice.mjs +45 -5
  276. package/src/runtime/session-search/session-search-db.mjs +73 -9
  277. package/src/runtime/session-search/session-search-ingest.mjs +110 -20
  278. package/src/runtime/shared/abort-race.mjs +89 -0
  279. package/src/runtime/shared/agent-avatar.d.mts +0 -1
  280. package/src/runtime/shared/agent-kind.mjs +4 -3
  281. package/src/runtime/shared/bot-hops.mjs +62 -2
  282. package/src/runtime/shared/bounded-download.mjs +8 -3
  283. package/src/runtime/shared/config.mjs +51 -35
  284. package/src/runtime/shared/connector-accounts.mjs +20 -10
  285. package/src/runtime/shared/connector-catalog.d.mts +33 -2
  286. package/src/runtime/shared/connector-catalog.mjs +166 -8
  287. package/src/runtime/shared/connector-triggers.mjs +22 -19
  288. package/src/runtime/shared/defaults-separation.mjs +2 -14
  289. package/src/runtime/shared/delegated-approval-host.mjs +103 -11
  290. package/src/runtime/shared/developer-options.mjs +32 -1
  291. package/src/runtime/shared/err-text.mjs +24 -15
  292. package/src/runtime/shared/file-permissions.d.mts +0 -8
  293. package/src/runtime/shared/file-permissions.mjs +0 -11
  294. package/src/runtime/shared/git-direct.test-support.mjs +19 -0
  295. package/src/runtime/shared/google-oauth-client.generated.json +4 -0
  296. package/src/runtime/shared/google-oauth-client.mjs +34 -9
  297. package/src/runtime/shared/heartbeat-interval.d.mts +4 -0
  298. package/src/runtime/shared/heartbeat-interval.mjs +19 -0
  299. package/src/runtime/shared/idle-auto-clear-gate.mjs +104 -0
  300. package/src/runtime/shared/llm/anthropic-betas.mjs +7 -0
  301. package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +12 -0
  302. package/src/runtime/shared/llm/provider-catalog-registry.mjs +1 -0
  303. package/src/runtime/shared/llm/usage-rollup.mjs +1 -1
  304. package/src/runtime/shared/llm/well-formed.mjs +66 -0
  305. package/src/runtime/shared/mixy-presence.mjs +22 -0
  306. package/src/runtime/shared/orchestration.d.mts +2 -0
  307. package/src/runtime/shared/pg-schema-db.mjs +27 -16
  308. package/src/runtime/shared/profile-config.cjs +1 -1
  309. package/src/runtime/shared/prompt-origin.d.mts +4 -0
  310. package/src/runtime/shared/prompt-origin.mjs +21 -0
  311. package/src/runtime/shared/provider-accounts.mjs +231 -59
  312. package/src/runtime/shared/provider-api-key.mjs +19 -8
  313. package/src/runtime/shared/provider-display-name.mjs +29 -0
  314. package/src/runtime/shared/provider-timeout-phase.mjs +2 -0
  315. package/src/runtime/shared/routine-trigger.d.mts +2 -0
  316. package/src/runtime/shared/routine-trigger.mjs +52 -0
  317. package/src/runtime/shared/sensitive-path-rules.d.mts +2 -10
  318. package/src/runtime/shared/sensitive-path-rules.mjs +19 -41
  319. package/src/runtime/shared/session-activity-signal.mjs +34 -1
  320. package/src/runtime/shared/session-sidebar-host.d.mts +1 -1
  321. package/src/runtime/shared/session-sidebar-host.mjs +7 -3
  322. package/src/runtime/shared/session-turn-prelude.mjs +18 -0
  323. package/src/runtime/shared/tool-execution-contract.mjs +2 -0
  324. package/src/runtime/shared/tool-result-summary.mjs +1 -14
  325. package/src/runtime/shared/transcript-metadata.mjs +38 -0
  326. package/src/runtime/shared/user-data-guard.mjs +30 -2
  327. package/src/runtime/shared/worktrees/activity-gate.mjs +12 -5
  328. package/src/runtime/shared/worktrees/test-support.mjs +9 -4
  329. package/src/runtime/terminal-bridge/client.mjs +10 -1
  330. package/src/runtime/visualize-bridge/client.mjs +27 -8
  331. package/src/runtime/visualize-bridge/tool-defs.mjs +22 -8
  332. package/src/runtime/visualize-bridge/tool-meta.d.mts +5 -0
  333. package/src/runtime/visualize-bridge/tool-meta.mjs +22 -0
  334. package/src/runtime/web-search/lib/fetch-pipeline.mjs +2 -2
  335. package/src/session-runtime/agent-home-delivery.mjs +486 -179
  336. package/src/session-runtime/agent-room-delivery.mjs +28 -1
  337. package/src/session-runtime/agent-task-sessions.mjs +561 -199
  338. package/src/session-runtime/boot/apis.mjs +9 -0
  339. package/src/session-runtime/boot/begin.mjs +3 -2
  340. package/src/session-runtime/boot/infrastructure.mjs +4 -0
  341. package/src/session-runtime/boot/providers.mjs +3 -0
  342. package/src/session-runtime/boot/tools.mjs +8 -1
  343. package/src/session-runtime/bot-chain.mjs +32 -0
  344. package/src/session-runtime/bot-status.mjs +7 -4
  345. package/src/session-runtime/channel-config-api.mjs +63 -17
  346. package/src/session-runtime/config-lifecycle.mjs +6 -0
  347. package/src/session-runtime/connector-merge-migration.mjs +305 -0
  348. package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +132 -28
  349. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +2 -0
  350. package/src/session-runtime/internal-tool-executor/reaction-tool.mjs +45 -0
  351. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +72 -5
  352. package/src/session-runtime/internal-tool-executor.mjs +4 -0
  353. package/src/session-runtime/lifecycle/inheritance.mjs +16 -3
  354. package/src/session-runtime/lifecycle/session-catalog.mjs +3 -1
  355. package/src/session-runtime/lifecycle/teardown/runtime-stops.mjs +50 -4
  356. package/src/session-runtime/lifecycle/teardown.mjs +23 -5
  357. package/src/session-runtime/mcp-glue/server-catalog.mjs +61 -3
  358. package/src/session-runtime/mcp-oauth-login.mjs +5 -3
  359. package/src/session-runtime/mixy-heartbeat.mjs +927 -0
  360. package/src/session-runtime/model-route/route-tuning.mjs +10 -6
  361. package/src/session-runtime/notification-bus.mjs +31 -117
  362. package/src/session-runtime/notification-completion-queue.mjs +54 -0
  363. package/src/session-runtime/notification-listeners.mjs +76 -0
  364. package/src/session-runtime/owned-automation-run.mjs +35 -13
  365. package/src/session-runtime/prewarm/channel-start.mjs +3 -7
  366. package/src/session-runtime/provider-auth/login.mjs +7 -0
  367. package/src/session-runtime/provider-models/row-hydration.mjs +13 -2
  368. package/src/session-runtime/resource-mcp-api.mjs +143 -224
  369. package/src/session-runtime/resource-mcp-connector-accounts.mjs +163 -0
  370. package/src/session-runtime/resource-mcp-oauth-api.mjs +217 -0
  371. package/src/session-runtime/resource-mcp-secrets.mjs +82 -0
  372. package/src/session-runtime/resource-mcp-servers.mjs +78 -0
  373. package/src/session-runtime/resource-mcp-store.mjs +49 -0
  374. package/src/session-runtime/routine-home-turn.mjs +33 -6
  375. package/src/session-runtime/routine-owner.mjs +40 -9
  376. package/src/session-runtime/schedule-session-run.mjs +7 -3
  377. package/src/session-runtime/services/agent-tool/close-flow.mjs +3 -0
  378. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +8 -12
  379. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +7 -2
  380. package/src/session-runtime/services/agent-tool/tag-registry.mjs +1 -0
  381. package/src/session-runtime/services/agent-tool/worker-index/awaiting-user-sync.mjs +51 -0
  382. package/src/session-runtime/services/agent-tool/worker-index/disposal.mjs +55 -0
  383. package/src/session-runtime/services/agent-tool/worker-index/released-store.mjs +123 -0
  384. package/src/session-runtime/services/agent-tool/worker-index/restart-settlement.mjs +112 -0
  385. package/src/session-runtime/services/agent-tool/worker-index/row-reads.mjs +36 -0
  386. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +2 -0
  387. package/src/session-runtime/services/agent-tool/worker-index/row-store.mjs +17 -4
  388. package/src/session-runtime/services/agent-tool/worker-index/row-writes.mjs +58 -0
  389. package/src/session-runtime/services/agent-tool/worker-index.mjs +41 -241
  390. package/src/session-runtime/services/agent-tool/worker-rows.mjs +3 -0
  391. package/src/session-runtime/services/channel-admin.mjs +73 -31
  392. package/src/session-runtime/services/channel-session-router.mjs +4 -2
  393. package/src/session-runtime/services/channel-worker.mjs +1 -1
  394. package/src/session-runtime/services/provider-admin.mjs +128 -25
  395. package/src/session-runtime/services/schedule-access.mjs +14 -0
  396. package/src/session-runtime/services/seeds.mjs +0 -28
  397. package/src/session-runtime/services/usage-dashboard-model.mjs +1 -0
  398. package/src/session-runtime/session-compaction-sync.mjs +27 -0
  399. package/src/session-runtime/session-create/session-options.mjs +8 -4
  400. package/src/session-runtime/settings-builtin-tools-api.mjs +11 -0
  401. package/src/session-runtime/settings-compaction-api.mjs +2 -40
  402. package/src/session-runtime/settings-system-api.mjs +28 -1
  403. package/src/session-runtime/setup-tool/agent-settings-actions.mjs +65 -0
  404. package/src/session-runtime/setup-tool/executor.mjs +187 -29
  405. package/src/session-runtime/setup-tool/extended-actions.mjs +33 -2
  406. package/src/session-runtime/setup-tool/session-route-actions.mjs +152 -0
  407. package/src/session-runtime/setup-tool/settings-contract.mjs +68 -6
  408. package/src/session-runtime/setup-tool/tool-defs.mjs +92 -6
  409. package/src/session-runtime/task-session-route.mjs +99 -0
  410. package/src/session-runtime/tool-policy/agent-profile-fold.mjs +17 -13
  411. package/src/session-runtime/tool-policy/policy-stage.mjs +21 -4
  412. package/src/session-runtime/tool-policy/surface-helpers.mjs +2 -3
  413. package/src/session-runtime/tool-surface.mjs +4 -0
  414. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +1 -0
  415. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +98 -21
  416. package/src/session-runtime/workflow-agents-api/style-and-mode.mjs +4 -0
  417. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +6 -1
  418. package/src/session-runtime/workspace/transitions.mjs +10 -2
  419. package/src/standalone/channel-transport/client-registry/client-records.mjs +65 -0
  420. package/src/standalone/channel-transport/client-registry/client-registration.mjs +118 -0
  421. package/src/standalone/channel-transport/client-registry.mjs +24 -163
  422. package/src/standalone/channel-transport.mjs +1 -1
  423. package/src/standalone/daemon-channels-loader.mjs +10 -8
  424. package/src/standalone/daemon-session-control/list-page.mjs +72 -0
  425. package/src/standalone/daemon-session-control/request.mjs +51 -0
  426. package/src/standalone/daemon-session-control/room-host.mjs +57 -0
  427. package/src/standalone/daemon-session-control/route-control.mjs +48 -0
  428. package/src/standalone/daemon-session-control/send-turn.mjs +60 -0
  429. package/src/standalone/daemon-session-control/worktree-control.mjs +40 -0
  430. package/src/standalone/daemon-session-control.mjs +161 -157
  431. package/src/standalone/daemon.mjs +33 -2
  432. package/src/standalone/session-client.mjs +7 -3
  433. package/src/standalone/session-protocol.mjs +10 -0
  434. package/src/standalone/session-service/agent-tree/agent-cancel.mjs +6 -2
  435. package/src/standalone/session-service/agent-tree/agent-child.mjs +1 -0
  436. package/src/standalone/session-service/quota-resume.mjs +3 -0
  437. package/src/standalone/session-service/retention/busy.mjs +65 -1
  438. package/src/standalone/session-service/session-calls/session-action.mjs +23 -2
  439. package/src/standalone/session-service/session-calls/session-turns.mjs +6 -0
  440. package/src/standalone/session-service/session-calls/session-views.mjs +8 -0
  441. package/src/standalone/session-service.mjs +13 -0
  442. package/src/standalone/session-tool-output.mjs +21 -10
  443. package/src/tui/app/model-options.mjs +4 -27
  444. package/src/tui/dist/index.mjs +81 -65
  445. package/src/tui/session/agent-job-feed/model-notification.mjs +3 -0
  446. package/src/tui/session/initial-state.mjs +3 -0
  447. package/src/tui/session/notice-surface.mjs +23 -1
  448. package/src/tui/session/queue-helpers.mjs +3 -42
  449. package/src/tui/session/session-api/integrations.mjs +1 -0
  450. package/src/tui/session/session-api/lifecycle.mjs +8 -0
  451. package/src/tui/session/session-api/routes-agent-definitions.mjs +14 -2
  452. package/src/tui/session/session-api/routes-write.mjs +5 -1
  453. package/src/tui/session/session-api/routes.mjs +1 -0
  454. package/src/tui/session/session-api/settings.mjs +30 -2
  455. package/src/tui/session/session-flow/auto-clear.mjs +4 -33
  456. package/src/tui/session/session-flow/drain-closed-queue.mjs +38 -0
  457. package/src/tui/session/session-flow/drain-enqueue.mjs +86 -0
  458. package/src/tui/session/session-flow/drain-retry.mjs +35 -0
  459. package/src/tui/session/session-flow/drain.mjs +118 -149
  460. package/src/tui/session/session-flow/queue/entry-shape.mjs +5 -0
  461. package/src/tui/session/session-flow/queue/take-entries.mjs +5 -2
  462. package/src/tui/session/tool-approval-delegation.mjs +64 -0
  463. package/src/tui/session/tool-approval-request.mjs +39 -0
  464. package/src/tui/session/tool-approval-settle.mjs +62 -0
  465. package/src/tui/session/tool-approval.mjs +69 -115
  466. package/src/tui/session/tui-steering-persist.mjs +8 -1
  467. package/src/tui/session/turn-stream.mjs +36 -3
  468. package/src/tui/session/turn.mjs +23 -2
  469. package/src/vendor/statusline/bin/statusline-route.mjs +1 -219
  470. package/src/vendor/statusline/src/gateway/route-meta.mjs +35 -13
  471. package/scripts/code-graph-description-contract.mjs +0 -188
  472. package/src/runtime/channels/lib/owned-runtime/ownership-refresh.mjs +0 -36
@@ -7,8 +7,13 @@
7
7
  * Only turns the origin agent starts (the brief, a later `send` from it)
8
8
  * report: when such a turn ends its result is queued durably for the agent's
9
9
  * home (agent-home-delivery.mjs), which posts it and wakes the agent once to
10
- * relay it. A turn blocked on the user's answer or approval posts one faint
11
- * line instead; the card stays in the task session for the user.
10
+ * relay it. The session's other answers in that chain (given while work it
11
+ * handed to its agents still runs) are queued as progress updates that wake
12
+ * nobody: the agent reads them before its next turn, whatever starts it
13
+ * (agent-home-delivery.mjs postBeforeTurn), and the final report never
14
+ * repeats one that already carried its answer. A turn blocked on the user's
15
+ * answer or approval posts one faint line instead; the card stays in the task
16
+ * session for the user.
12
17
  *
13
18
  * An agent's send to any other session reports the same way, by an origin for
14
19
  * that send only (reportOrigin). Every reporting turn is also watched durably
@@ -33,10 +38,12 @@ import { clean } from '../runtime/agent/orchestrator/runtime-core/session-text.m
33
38
  import { collectTurnArtifacts, handoffContent, sessionRoots, turnSlice } from './handoff-attachments.mjs';
34
39
  import { roomMemberOfSession } from '../runtime/agent/orchestrator/session/agent-room.mjs';
35
40
  import { wrapAgentMessage } from '../runtime/shared/agent-message-envelope.mjs';
36
- import { sessionsWithPendingWork } from '../runtime/shared/pending-agent-work.mjs';
41
+ import { hasPendingAgentWork, sessionsWithPendingWork } from '../runtime/shared/pending-agent-work.mjs';
37
42
  import { agentTaskIdOfExecution } from './services/agent-tool/restart-notify.mjs';
43
+ import { botChainOf } from '../runtime/shared/bot-hops.mjs';
38
44
  import { worktreeError, worktreeNameError } from '../runtime/shared/worktrees/contract.mjs';
39
45
  import { samePath } from '../runtime/shared/worktrees/paths.mjs';
46
+ import { readModelCatalog, routeByDisplayName, withEffort } from './task-session-route.mjs';
40
47
  import { assertWorktreeMutationAllowed } from './services/worktree-access.mjs';
41
48
 
42
49
  const TITLE_LIMIT = 60;
@@ -77,12 +84,15 @@ const turnStatus = (detail) =>
77
84
 
78
85
  const INTERRUPTED = /^\[Request interrupted/;
79
86
 
80
- const messageText = (content) =>
81
- typeof content === 'string'
82
- ? content
83
- : (Array.isArray(content) ? content : [])
84
- .map((part) => (typeof part === 'string' ? part : part?.type === 'text' ? part.text || '' : ''))
85
- .join('');
87
+ const partText = (part) => {
88
+ if (typeof part === 'string') return part;
89
+ return part?.type === 'text' ? part.text || '' : '';
90
+ };
91
+
92
+ const messageText = (content) => {
93
+ if (typeof content === 'string') return content;
94
+ return (Array.isArray(content) ? content : []).map(partText).join('');
95
+ };
86
96
 
87
97
  /** The submissions a stored user message delivered: a turn's prompt
88
98
  * (`checkpointId`, `submissionIds` of a merged batch) or a steered message. */
@@ -104,13 +114,21 @@ export function submissionIdsOf(message) {
104
114
  export function submissionOutcome(messages, runId) {
105
115
  const list = Array.isArray(messages) ? messages : [];
106
116
  const start = list.findIndex((message) => submissionIdsOf(message).includes(runId));
107
- if (start < 0) return null;
117
+ return start < 0 ? null : outcomeFrom(list, start);
118
+ }
119
+
120
+ /** submissionOutcome of the turn whose prompt is `list[start]`, read in
121
+ * place: the scan stops at that turn's end (its completion, an
122
+ * interruption, or the next turn's prompt). */
123
+ function outcomeFrom(list, start) {
108
124
  for (let index = start; index < list.length; index += 1) {
109
125
  const message = list[index];
110
126
  const completion = message?.meta?.transcript?.completion;
111
127
  if (completion) {
112
128
  const status = turnStatus(completion);
113
- const text = status === 'failed' ? completion.detail || '' : message.role === 'assistant' ? message.content : '';
129
+ let text = '';
130
+ if (status === 'failed') text = completion.detail || '';
131
+ else if (message.role === 'assistant') text = message.content;
114
132
  return { status, text: messageText(text) };
115
133
  }
116
134
  if (index === start || message?.role !== 'user') continue;
@@ -140,7 +158,9 @@ const callArguments = (call) => {
140
158
  * no task. Empty when the turn recorded none. */
141
159
  function consumedTaskIds(message) {
142
160
  const recorded = message?.meta?.transcript?.consumedExecutionIds;
143
- const ids = Array.isArray(recorded) ? recorded : message?.meta?.execution?.id ? [message.meta.execution.id] : [];
161
+ let ids = [];
162
+ if (Array.isArray(recorded)) ids = recorded;
163
+ else if (message?.meta?.execution?.id) ids = [message.meta.execution.id];
144
164
  return ids.map((id) => (id ? agentTaskIdOfExecution(id) : null));
145
165
  }
146
166
 
@@ -158,10 +178,27 @@ function consumedTaskIds(message) {
158
178
  * submissionOutcome and when its end was recorded (0 when unknown); null
159
179
  * when no turn took runId. */
160
180
  export function latestTurnOutcome(messages, runId) {
181
+ const chain = turnChain(messages, runId);
182
+ return chain && { id: chain.id, outcome: chain.outcome, at: chain.at, ambiguous: chain.ambiguous };
183
+ }
184
+
185
+ /** latestTurnOutcome with what a report's wait needs besides: `answers`,
186
+ * every turn of the chain that recorded its end, oldest first (`{ id,
187
+ * status, text }`: its submission id and submissionOutcome); `taskIds`, the
188
+ * agent tasks the chain's turns started — the only tasks proven the chain's
189
+ * — and `busyIsOthers`: the session's last stored turn is one outside the
190
+ * chain that recorded no end yet, and a stored turn took the result of every
191
+ * one of `taskIds` (only those make a turn the chain's) — a busy session
192
+ * then runs that other turn, and no result of the chain can be on its way
193
+ * into a turn (woken by it, or steered into the running one) whose prompt or
194
+ * steer is not stored yet. */
195
+ function turnChain(messages, runId) {
161
196
  const list = Array.isArray(messages) ? messages : [];
162
197
  const start = list.findIndex((message) => submissionIdsOf(message).includes(runId));
163
198
  if (start < 0) return null;
164
199
  const started = new Set();
200
+ // The agent tasks whose results a stored turn took (as its prompt or steered in).
201
+ const taken = new Set();
165
202
  const startingCalls = new Set();
166
203
  let at = start;
167
204
  let id = runId;
@@ -170,6 +207,8 @@ export function latestTurnOutcome(messages, runId) {
170
207
  // The turn the walk is in: where its prompt is stored and its submission id.
171
208
  let turnAt = start;
172
209
  let turnId = runId;
210
+ // Every turn of the chain, in order: where its prompt is stored, its id.
211
+ const turns = [{ at: start, id: runId }];
173
212
  for (let index = start; index < list.length; index += 1) {
174
213
  const message = list[index];
175
214
  const checkpointId = index > start && message?.role === 'user' ? message.meta?.transcript?.checkpointId : '';
@@ -186,11 +225,13 @@ export function latestTurnOutcome(messages, runId) {
186
225
  index > start && message?.role === 'user' && message.meta?.source === 'task-notification'
187
226
  ? consumedTaskIds(message)
188
227
  : null;
228
+ for (const taskId of delivered || []) if (taskId) taken.add(taskId);
189
229
  if (delivered && !inChain) {
190
230
  if (delivered.some((taskId) => taskId && started.has(taskId))) {
191
231
  inChain = true;
192
232
  at = turnAt;
193
233
  id = turnId;
234
+ if (turns.at(-1).id !== turnId) turns.push({ at: turnAt, id: turnId });
194
235
  } else if (!delivered.length || delivered.includes(null)) {
195
236
  ambiguous = true;
196
237
  }
@@ -209,9 +250,47 @@ export function latestTurnOutcome(messages, runId) {
209
250
  if (Array.isArray(message.agentTaskIds)) for (const taskId of message.agentTaskIds) started.add(String(taskId));
210
251
  else if (message.toolKind !== 'error') ambiguous = true;
211
252
  }
212
- const turn = list.slice(at);
213
- const ended = turn.find((message) => message?.meta?.transcript?.completion);
214
- return { id, outcome: submissionOutcome(turn, id), at: Number(ended?.meta?.transcript?.at) || 0, ambiguous };
253
+ // Read in place, never on copied suffixes: each chain turn's outcome scan
254
+ // stops at that turn's end, so the turns' scans cover disjoint stretches
255
+ // and the whole walk stays linear in the transcript.
256
+ let ended = null;
257
+ for (let index = at; index < list.length && !ended; index += 1) {
258
+ if (list[index]?.meta?.transcript?.completion) ended = list[index];
259
+ }
260
+ const last = turnId === id ? null : outcomeFrom(list, turnAt);
261
+ const answers = [];
262
+ let outcome = null;
263
+ for (const chainTurn of turns) {
264
+ const turnOutcome = outcomeFrom(list, chainTurn.at);
265
+ if (chainTurn.at === at) outcome = turnOutcome;
266
+ if (turnOutcome !== 'open') answers.push({ id: chainTurn.id, ...turnOutcome });
267
+ }
268
+ return {
269
+ id,
270
+ outcome,
271
+ at: Number(ended?.meta?.transcript?.at) || 0,
272
+ ambiguous,
273
+ answers,
274
+ taskIds: started,
275
+ busyIsOthers: last === 'open' && [...started].every((taskId) => taken.has(taskId)),
276
+ };
277
+ }
278
+
279
+ /** The agent task an owed result reports; null when it names none. */
280
+ function owedTaskId(result) {
281
+ const executionId = result?.meta?.execution_id;
282
+ return executionId ? agentTaskIdOfExecution(executionId) : null;
283
+ }
284
+
285
+ /** The `owed` results that may be the chain's of agent tasks `taskIds`: its
286
+ * tasks' and those naming no task (their provenance is missing); every one
287
+ * for a null `taskIds` (the whole session). */
288
+ function owedOf(owed, taskIds) {
289
+ if (!taskIds) return owed;
290
+ return owed.filter((result) => {
291
+ const taskId = owedTaskId(result);
292
+ return !taskId || taskIds.has(taskId);
293
+ });
215
294
  }
216
295
 
217
296
  /** Whether task session `sessionId` still waits on agent work it handed off,
@@ -221,20 +300,76 @@ export function latestTurnOutcome(messages, runId) {
221
300
  * `agentTaskOwners` (the sessions owning a queued, spawning or running agent
222
301
  * task — one still waiting for admission has no turn in flight, and its pool
223
302
  * row reads `unknown`). An agent task owned by an Agent session under it, at
224
- * any depth, counts too: every descendant's pool row names the Lead as owner. */
303
+ * any depth, counts too: every descendant's pool row names the Lead as owner.
304
+ * `taskIds` narrows it to one chain's agent tasks (chainPendingWork). */
225
305
  export function pendingWorkOf(
226
306
  sessionId,
227
- { agentWorking = new Set(), owed = [], pool = [], agentTaskOwners = new Set() }
307
+ {
308
+ agentWorking = new Set(),
309
+ owed = [],
310
+ pool = [],
311
+ agentTaskOwners = new Set(),
312
+ taskIds = null,
313
+ working = new Set(),
314
+ agentTasks = [],
315
+ }
228
316
  ) {
317
+ if (taskIds) return chainPendingWork(sessionId, taskIds, { owed, pool, agentTaskOwners, working, agentTasks });
229
318
  if (agentWorking.has(sessionId) || owed.length > 0 || agentTaskOwners.has(sessionId)) return true;
230
319
  if (sessionsWithPendingWork(pool).has(sessionId)) return true;
231
320
  return pool.some((row) => row?.ownerSessionId === sessionId && agentTaskOwners.has(String(row.sessionId || '')));
232
321
  }
233
322
 
323
+ /** The agent work of the chain of Lead `sessionId` whose own agent tasks are
324
+ * `taskIds`. `taskIds`: those tasks alone, the only ones proven the chain's
325
+ * (no record says which task started a child agent). `sessions`: the Agent
326
+ * sessions those tasks run in (`agentTasks` `sessionId`, a pool row's
327
+ * `taskId`) and every Agent session under one (pool `parentSessionId`),
328
+ * never the Lead — work there may be the chain's, so it only holds the
329
+ * report (it may be another's: a session the chain's agent ran in can be
330
+ * sent another task, which can spawn children of its own). */
331
+ export function chainClosure(sessionId, taskIds, { pool = [], agentTasks = [] } = {}) {
332
+ const owned = new Set(taskIds);
333
+ const sessions = new Set();
334
+ const join = (id) => {
335
+ if (!id || id === sessionId || sessions.has(id)) return false;
336
+ sessions.add(id);
337
+ return true;
338
+ };
339
+ for (const task of agentTasks) if (owned.has(task.taskId)) join(task.sessionId);
340
+ for (const row of pool) if (owned.has(String(row?.taskId || ''))) join(String(row.sessionId || ''));
341
+ for (let grown = true; grown; ) {
342
+ grown = false;
343
+ for (const row of pool) {
344
+ if (sessions.has(String(row?.parentSessionId || '')) && join(String(row.sessionId || ''))) grown = true;
345
+ }
346
+ }
347
+ return { taskIds: owned, sessions };
348
+ }
349
+
350
+ /** pendingWorkOf of the chain of agent tasks `taskIds` alone, with all its
351
+ * work (chainClosure): a task it owns or one of its Agent sessions runs
352
+ * (`agentTasks`: `{ taskId, sessionId, running }` of this process's agent
353
+ * tasks; a pool row's `taskId`) still queued or running, or its result owed
354
+ * (or an owed result naming no task), or one of those sessions with a turn
355
+ * in flight (`working`), owning a queued or running agent task, or pending
356
+ * on its pool row. Other work of the session never counts. */
357
+ function chainPendingWork(sessionId, taskIds, { owed, pool, agentTaskOwners, working, agentTasks }) {
358
+ const chain = chainClosure(sessionId, taskIds, { pool, agentTasks });
359
+ const sessionOf = new Map();
360
+ for (const row of pool) if (row?.taskId) sessionOf.set(String(row.taskId), String(row.sessionId || ''));
361
+ for (const task of agentTasks) if (task.sessionId) sessionOf.set(task.taskId, task.sessionId);
362
+ const ofChain = (taskId) => chain.taskIds.has(taskId) || chain.sessions.has(sessionOf.get(taskId));
363
+ if (owed.some((result) => !owedTaskId(result) || ofChain(owedTaskId(result)))) return true;
364
+ if (agentTasks.some((task) => task.running && ofChain(task.taskId))) return true;
365
+ for (const id of chain.sessions) if (working.has(id) || agentTaskOwners.has(id)) return true;
366
+ return pool.some((row) => chain.sessions.has(String(row?.sessionId || '')) && hasPendingAgentWork(row));
367
+ }
368
+
234
369
  /** The agent results queued for `sessionId` that no turn has taken yet (its
235
370
  * durable pending queue: an agent's completion, a restart failure): `{ text,
236
371
  * meta }` as the notification bus delivers them. */
237
- export async function owedAgentResults(sessionId) {
372
+ async function owedAgentResults(sessionId) {
238
373
  const [{ _getPendingMessagesForSession }, { completionIsDelivered, isCompletionNotificationEntry }] =
239
374
  await Promise.all([
240
375
  import('../runtime/agent/orchestrator/session/manager/pending-messages.mjs'),
@@ -255,22 +390,37 @@ export async function owedAgentResults(sessionId) {
255
390
  }
256
391
 
257
392
  /** pendingWorkOf over this process's state: the in-flight runtimes, the
258
- * durable pending queue, the agent pool and the background agent tasks. */
259
- export async function taskSessionPendingWork(sessionId, { owedResults = owedAgentResults } = {}) {
260
- const [{ inFlightSessionActivity }, { listStoredAgentWorkersAsync }, { activeBackgroundTaskSessionIds }] =
261
- await Promise.all([
262
- import('../runtime/agent/orchestrator/session/manager/runtime-state.mjs'),
263
- import('../runtime/agent/orchestrator/session/store-summary-reader.mjs'),
264
- import('../runtime/shared/background-tasks.mjs'),
265
- ]);
393
+ * durable pending queue, the agent pool and the background agent tasks;
394
+ * `taskIds` narrows it to one chain's agent tasks. */
395
+ export async function taskSessionPendingWork(sessionId, { owedResults = owedAgentResults, taskIds = null } = {}) {
396
+ const [
397
+ { inFlightSessionActivity },
398
+ { listStoredAgentWorkersAsync },
399
+ { activeBackgroundTaskSessionIds, listBackgroundTasks },
400
+ ] = await Promise.all([
401
+ import('../runtime/agent/orchestrator/session/manager/runtime-state.mjs'),
402
+ import('../runtime/agent/orchestrator/session/store-summary-reader.mjs'),
403
+ import('../runtime/shared/background-tasks.mjs'),
404
+ ]);
266
405
  const { working, agentWorking } = inFlightSessionActivity();
267
406
  const agentTaskOwners = activeBackgroundTaskSessionIds({ surface: 'agent' });
268
- if (pendingWorkOf(sessionId, { agentWorking, agentTaskOwners })) return true;
269
- if ((await owedResults(sessionId)).length) return true;
407
+ const agentTasks = taskIds ? agentTaskRows(listBackgroundTasks) : [];
408
+ const live = { agentWorking, agentTaskOwners, taskIds, working, agentTasks };
409
+ if (pendingWorkOf(sessionId, live)) return true;
410
+ const owed = await owedResults(sessionId);
411
+ if (pendingWorkOf(sessionId, { ...live, owed })) return true;
270
412
  const pool = await listStoredAgentWorkersAsync({ runningSessionIds: working });
271
- return pendingWorkOf(sessionId, { pool, agentTaskOwners });
413
+ return pendingWorkOf(sessionId, { ...live, owed, pool });
272
414
  }
273
415
 
416
+ /** This process's agent tasks as pendingWorkOf reads them. */
417
+ const agentTaskRows = (listBackgroundTasks) =>
418
+ listBackgroundTasks({ surface: 'agent' }).map((task) => ({
419
+ taskId: task.task_id,
420
+ sessionId: task.sessionId || null,
421
+ running: task.status === 'running',
422
+ }));
423
+
274
424
  // A task session idle with nothing pending must stay so this long before its
275
425
  // latest answer is final: a finished agent's result can start the Lead's next
276
426
  // turn right after (the desktop notifier's quiet period).
@@ -285,9 +435,6 @@ const LOST_TEXT = 'The message never ran: the app restarted before its turn star
285
435
  const GONE_TEXT = 'The session no longer exists.';
286
436
  const RESUME_FAILED_TEXT =
287
437
  'The agent task session could not continue after the app restarted: its agents’ results never reached it. Check that session.';
288
- const PENDING_CAP_MS = 30 * 60_000;
289
- const STILL_WORKING_TEXT =
290
- '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.';
291
438
  // Deliveries of a restarted task session's queued agent results before its
292
439
  // report gives up as RESUME_FAILED_TEXT.
293
440
  const OWED_WAKE_TRIES = 3;
@@ -305,8 +452,9 @@ const LAST_USED_ROUTE_MAX_PAGES = 20;
305
452
  const roomFrom = (originSessionId) => (roomMemberOfSession(originSessionId) ? { from: originSessionId } : {});
306
453
 
307
454
  /** The route of `modelRef`: a provider/model[@effort…] ref (a superseded
308
- * model runs on its successor) or a preset name. */
309
- async function configModelRoute(modelRef) {
455
+ * model runs on its successor), a preset name, else whatever `byName` makes
456
+ * of it (a catalog display name; it throws when it names none). */
457
+ async function configModelRoute(modelRef, byName) {
310
458
  const [{ modelRouteFields, parseScheduleModelRef }, { migrateCodexRoute }, { loadConfig }] = await Promise.all([
311
459
  import('../runtime/shared/schedule-model-ref.mjs'),
312
460
  import('../runtime/agent/orchestrator/providers/openai-oauth-catalog-store.mjs'),
@@ -315,13 +463,12 @@ async function configModelRoute(modelRef) {
315
463
  const ref = parseScheduleModelRef(modelRef);
316
464
  if (ref && typeof ref === 'object') return modelRouteFields(migrateCodexRoute(ref));
317
465
  const preset = loadConfig({ secrets: false }).presets?.find((p) => p.id === ref || p.name === ref);
318
- if (!preset) throw new Error(`model "${ref}" is neither a provider/model route nor a known preset name`);
319
- return modelRouteFields(preset);
466
+ return preset ? modelRouteFields(preset) : byName(ref);
320
467
  }
321
468
 
322
469
  /** The Lead (Main) route of the config — what a desktop New task starts on.
323
470
  * Null while none is chosen. */
324
- async function configLeadRoute() {
471
+ export async function configLeadRoute() {
325
472
  const [{ modelRouteFields }, { resolveRoute }, { loadConfig }] = await Promise.all([
326
473
  import('../runtime/shared/schedule-model-ref.mjs'),
327
474
  import('./boot/shared.mjs'),
@@ -352,29 +499,60 @@ async function lastUsedSessionRoute(sessionService) {
352
499
  /** Wait for the final answer of the task turn `runId` of `sessionId` ended
353
500
  * completed: the end of the latest turn since it (a Lead that handed work
354
501
  * to agents ends its turn and answers in the turn their results wake),
355
- * once the session is idle with no pending agent work (pendingWork) for
356
- * `settleMs`. `immediate` is runId's own end, when this process saw it.
357
- * `finish(status, text)` runs once; `check` re-evaluates (call it on every
358
- * state change of `runtime`), `stop` ends the wait. */
502
+ * once that chain has no turn running and no pending agent work
503
+ * (pendingWork of its agent tasks) for `settleMs`. Work of the session
504
+ * outside the chain (a turn the user typed, its agents) never holds it;
505
+ * without the provenance to tell (no stored turn, `ambiguous`) the whole
506
+ * session must be idle with none pending. `immediate` is runId's own end,
507
+ * when this process saw it.
508
+ * Every other answer of the chain — each ended turn a later turn of the
509
+ * chain followed, and the latest while its work still runs or its
510
+ * provenance is missing — goes to `interim({ id, status, text })` once, in
511
+ * chain order (`id`: the submission of the turn that gave it): a progress
512
+ * update, which wakes nobody. Every check lists all the chain's ended turns,
513
+ * so answers that end close together are each reported. `progressedThrough`:
514
+ * the last turn an earlier process reported (the watch's durable mark):
515
+ * it and the turns before it are not reported again.
516
+ * `finish(status, text, answerId)` runs once (`answerId`: the turn whose
517
+ * answer it is, none for a report of no turn); `check` re-evaluates (call it
518
+ * on every state change of `runtime`), `stop` ends the wait. */
359
519
  function followTaskToFinal(
360
- { loadSession, owedResults, deliverCompletion, pendingWork, settleMs, pollMs, pendingCapMs, log },
361
- { sessionId, runId, runtime, immediate = null, wakeOwed = false, finish, progress = null }
520
+ { loadSession, owedResults, deliverCompletion, pendingWork, settleMs, pollMs, log },
521
+ { sessionId, runId, runtime, immediate = null, wakeOwed = false, finish, interim = null, progressedThrough = '' }
362
522
  ) {
363
- let progressed = false;
364
- let pendingSince = 0;
365
- // Agent work still pending `pendingCapMs` after the chain's latest turn
366
- // ended: `progress(text)` reports that answer once, marked still in
367
- // progress, and the wait for the final one goes on.
368
- async function reportProgress() {
369
- if (progressed || typeof progress !== 'function') return;
370
- const latest = latestTurnOutcome((await loadSession(sessionId))?.messages, runId);
371
- let end = latest?.outcome && latest.outcome !== 'open' ? latest.outcome : null;
372
- if (immediate && (!latest || latest.id === runId)) end = immediate;
373
- if (!end) return;
374
- pendingSince ||= Date.now();
375
- if (Date.now() - (latest?.at || pendingSince) < pendingCapMs) return;
376
- progressed = true;
377
- await progress(`${STILL_WORKING_TEXT}\n\n${end.text}`);
523
+ // The chain's ended answers, oldest first, `{ id, status, text }`; runId's
524
+ // own is `immediate` when this process saw it. `latest`: turnChain's.
525
+ const answersOf = (latest) => {
526
+ const stored = latest?.answers || [];
527
+ if (!immediate) return stored;
528
+ return [{ id: runId, ...immediate }, ...stored.filter((answer) => answer.id !== runId)];
529
+ };
530
+ // The answers already handed to `interim`, by turn.
531
+ const interimIds = new Set();
532
+ /** Hand `answer` to `interim` once; false when that failed (logged): it is
533
+ * tried again at the next check, and no later answer goes out before it. */
534
+ async function reportInterim(answer) {
535
+ if (!answer?.id || !String(answer.text || '').trim() || typeof interim !== 'function') return true;
536
+ if (interimIds.has(answer.id)) return true;
537
+ try {
538
+ await interim(answer);
539
+ } catch (error) {
540
+ log(`progress update of ${sessionId} failed, retried at the next check: ${error?.message || error}`);
541
+ return false;
542
+ }
543
+ interimIds.add(answer.id);
544
+ return true;
545
+ }
546
+ /** Report, in order, every one of `answers` past the durable mark but
547
+ * `held` (the final candidate, reported only when it proves not final);
548
+ * false, stopping there, at the first that could not be queued. */
549
+ async function reportEarlier(answers, held) {
550
+ const through = progressedThrough ? answers.findIndex((answer) => answer.id === progressedThrough) : -1;
551
+ for (const [index, answer] of answers.entries()) {
552
+ if (index <= through) interimIds.add(answer.id);
553
+ else if (answer !== held && !(await reportInterim(answer))) return false;
554
+ }
555
+ return true;
378
556
  }
379
557
  let done = false;
380
558
  let checking = false;
@@ -383,6 +561,7 @@ function followTaskToFinal(
383
561
  let candidateAt = 0;
384
562
  let timer = null;
385
563
  let wakes = 0;
564
+ let otherWakes = 0;
386
565
  let wokeAt = 0;
387
566
  const unwoken = (why) => {
388
567
  log(`${sessionId} could not take its agents' results after the restart: ${why}`);
@@ -391,23 +570,32 @@ function followTaskToFinal(
391
570
  // `wakeOwed` (a restarted watch): deliver the agent results queued for
392
571
  // the idle session through the notification bus so a turn takes them, at
393
572
  // most OWED_WAKE_TRIES times, a poll apart; null while that turn is owed.
394
- async function wakeForOwed() {
573
+ // Every owed result is delivered, but only those of the chain's agent tasks
574
+ // (owedOf `taskIds`) hold the report or fail it; deliveries of the others
575
+ // alone have a bound of their own (`otherWakes`).
576
+ async function wakeForOwed(taskIds) {
395
577
  const owed = await owedResults(sessionId);
396
578
  if (!owed.length) return undefined;
397
- if (Date.now() - wokeAt < pollMs) return null;
398
- if (wakes >= OWED_WAKE_TRIES) return unwoken(`still not taken after ${wakes} deliveries`);
399
- wakes += 1;
400
- wokeAt = Date.now();
401
- let delivered = false;
402
- try {
403
- if (typeof deliverCompletion === 'function') {
404
- const results = await Promise.all(owed.map(({ text, meta }) => deliverCompletion(sessionId, text, meta)));
405
- delivered = results.some((result) => result === true);
406
- }
407
- } catch (error) {
408
- return unwoken(error?.message || error);
579
+ const chain = new Set(owedOf(owed, taskIds));
580
+ const hold = chain.size ? null : undefined;
581
+ if (Date.now() - wokeAt < pollMs) return hold;
582
+ if (chain.size ? wakes >= OWED_WAKE_TRIES : otherWakes >= OWED_WAKE_TRIES) {
583
+ return chain.size ? unwoken(`still not taken after ${wakes} deliveries`) : undefined;
409
584
  }
410
- return delivered ? null : unwoken('no session runtime took the delivery');
585
+ if (chain.size) wakes += 1;
586
+ else otherWakes += 1;
587
+ wokeAt = Date.now();
588
+ const outcomes = await Promise.allSettled(
589
+ owed.map(
590
+ async ({ text, meta }) =>
591
+ typeof deliverCompletion === 'function' && (await deliverCompletion(sessionId, text, meta)) === true
592
+ )
593
+ );
594
+ if (!chain.size) return undefined;
595
+ const ofChain = outcomes.filter((_, index) => chain.has(owed[index]));
596
+ const failure = ofChain.find((outcome) => outcome.status === 'rejected');
597
+ if (failure) return unwoken(failure.reason?.message || failure.reason);
598
+ return ofChain.some((outcome) => outcome.value === true) ? null : unwoken('no session runtime took the delivery');
411
599
  }
412
600
  const arm = (ms) => {
413
601
  clearTimeout(timer);
@@ -421,26 +609,36 @@ function followTaskToFinal(
421
609
  const failed = (error) => log(`following ${sessionId} to its final answer failed: ${error?.message || error}`);
422
610
  // The end to report now, or null while the task still works.
423
611
  async function ready() {
424
- if (runtime?.getState?.()?.busy) return null;
425
- if (wakeOwed) {
426
- const woke = await wakeForOwed();
612
+ const stored = await loadSession(sessionId);
613
+ const latest = turnChain(stored?.messages, runId);
614
+ // The chain's agent tasks; null (the whole session) without its provenance.
615
+ const taskIds = latest && !latest.ambiguous ? latest.taskIds : null;
616
+ // Every answer of the chain but its latest turn's is no final one: each
617
+ // goes out as progress, whatever the session does now.
618
+ const answers = answersOf(latest);
619
+ const latestId = latest ? latest.id : runId;
620
+ const answer = answers.at(-1)?.id === latestId ? answers.at(-1) : null;
621
+ // An answer that could not be queued holds the chain (later answers, the
622
+ // final report) until a later check queues it: the poll retries it.
623
+ if (!(await reportEarlier(answers, answer))) return null;
624
+ // A busy session may run the chain's turn unless that turn is provably another's.
625
+ const busy = Boolean(runtime?.getState?.()?.busy);
626
+ if (busy && !(taskIds && latest.busyIsOthers)) return null;
627
+ if (wakeOwed && !busy) {
628
+ const woke = await wakeForOwed(taskIds);
427
629
  if (woke !== undefined) return woke;
428
630
  }
429
- if (await pendingWork(sessionId)) {
430
- await reportProgress();
631
+ if (await pendingWork(sessionId, { taskIds })) {
632
+ await reportInterim(answer);
431
633
  return null;
432
634
  }
433
- const stored = await loadSession(sessionId);
434
635
  if (!stored && !immediate) return { key: 'gone', status: 'cancelled', text: GONE_TEXT };
435
- const latest = latestTurnOutcome(stored?.messages, runId);
436
636
  // Provenance the chain needs is missing: its answer may not be final.
437
637
  if (latest?.ambiguous) {
438
- await reportProgress();
638
+ await reportInterim(answer);
439
639
  return null;
440
640
  }
441
- if (immediate && (!latest || latest.id === runId)) return { key: runId, ...immediate };
442
- if (!latest?.outcome || latest.outcome === 'open') return null;
443
- return { key: `${latest.id}:${latest.outcome.status}`, ...latest.outcome };
641
+ return answer && { key: `${answer.id}:${answer.status}`, ...answer };
444
642
  }
445
643
  async function check() {
446
644
  if (done) return;
@@ -466,7 +664,7 @@ function followTaskToFinal(
466
664
  arm(candidateAt + settleMs - Date.now());
467
665
  } else {
468
666
  stop();
469
- await finish(end.status, end.text);
667
+ await finish(end.status, end.text, end.id);
470
668
  }
471
669
  } while (again && !done);
472
670
  } finally {
@@ -624,7 +822,7 @@ async function rearmTaskWatch(
624
822
  // The turn that answers: the watched submission's own, or the one it
625
823
  // follows (a room member's turn under another submission id).
626
824
  const turnId = watch.follows || watch.runId;
627
- const finish = async (status, text = '') =>
825
+ const finish = async (status, text = '', answerId = '') =>
628
826
  deliver(watch.agentId, {
629
827
  runId: `${watch.runId}_end`,
630
828
  taskSessionId: sessionId,
@@ -635,9 +833,8 @@ async function rearmTaskWatch(
635
833
  ...(watch.hop > 0 ? { hop: watch.hop } : {}),
636
834
  ...roomFrom(watch.homeSessionId),
637
835
  watchId: watch.runId,
638
- ...(watch.textOnly || text === GONE_TEXT || text === LOST_TEXT
639
- ? {}
640
- : await artifactFields(sessionId, turnId)),
836
+ ...progressOf(watch.runId, answerId),
837
+ ...(watch.textOnly || text === GONE_TEXT || text === LOST_TEXT ? {} : await artifactFields(sessionId, turnId)),
641
838
  });
642
839
  if (!(await loadSession(sessionId))) return finish('cancelled', GONE_TEXT);
643
840
  let runtime;
@@ -682,21 +879,25 @@ async function rearmTaskWatch(
682
879
  runId: turnId,
683
880
  runtime,
684
881
  wakeOwed: true,
685
- progress: (text) =>
882
+ // The turns an earlier process already reported as progress.
883
+ progressedThrough: watch.progressed || '',
884
+ interim: (answer) =>
686
885
  deliver(watch.agentId, {
687
- runId: `${watch.runId}_progress`,
886
+ runId: progressRunId(watch.runId, answer.id),
688
887
  taskSessionId: sessionId,
689
888
  title: watch.title,
690
889
  status: 'working',
691
- text,
890
+ text: answer.text,
891
+ progressWatchId: watch.runId,
892
+ answerId: answer.id,
692
893
  ...(watch.target === 'session' ? { target: 'session' } : {}),
693
894
  ...(watch.hop > 0 ? { hop: watch.hop } : {}),
694
895
  ...roomFrom(watch.homeSessionId),
695
896
  }),
696
- finish: async (status, text) => {
897
+ finish: async (status, text, answerId) => {
697
898
  done = true;
698
899
  unsubscribe();
699
- await finish(status, text);
900
+ await finish(status, text, answerId);
700
901
  },
701
902
  });
702
903
  } else if (end) {
@@ -715,6 +916,161 @@ async function rearmTaskWatch(
715
916
  await check();
716
917
  }
717
918
 
919
+ /** The settlement of sends one turn answers: they settle together with the
920
+ * same `detail` object (a send steered into the turn while it ran, or queued
921
+ * and taken as one batch), and that turn reports once, closing every one of
922
+ * them. `nextSeq()` numbers the sends in submission order: the earliest of
923
+ * the sends one turn answers reports for them. */
924
+ function createSendSettlement(log) {
925
+ const settledTurns = new WeakMap();
926
+ let sendCount = 0;
927
+
928
+ /** Add send `member` (`{ seq, id, unwatch, settle }`) to the settlement
929
+ * `detail` of its turn, per reporting conversation. The sends of one turn
930
+ * settle in the same tick; after it the earliest (the turn's own, whose
931
+ * record holds the whole turn) settles with all their ids and the others
932
+ * stand down. */
933
+ function join(detail, origin, member) {
934
+ const key = `${origin.agentId}\0${origin.sessionId}`;
935
+ let groups = settledTurns.get(detail);
936
+ if (!groups) {
937
+ groups = new Map();
938
+ settledTurns.set(detail, groups);
939
+ }
940
+ let members = groups.get(key);
941
+ if (!members) {
942
+ members = [];
943
+ groups.set(key, members);
944
+ const settling = members;
945
+ void Promise.resolve()
946
+ .then(() => {
947
+ const [first, ...others] = settling.sort((a, b) => a.seq - b.seq);
948
+ for (const other of others) other.unwatch();
949
+ first.settle(settling.map((entry) => entry.id));
950
+ })
951
+ .catch((error) => log(`settling the sends of one turn failed: ${error?.message || error}`));
952
+ }
953
+ members.push(member);
954
+ }
955
+
956
+ const nextSeq = () => {
957
+ sendCount += 1;
958
+ return sendCount;
959
+ };
960
+
961
+ return { join, nextSeq };
962
+ }
963
+
964
+ /** The queue row of send `id`'s progress update carrying the answer of turn
965
+ * `answerId` (followToFinal `interim`): one per answer, across restarts. */
966
+ const progressRunId = (id, answerId) => `${id}_progress_${answerId}`;
967
+
968
+ /** The queue row of send `id`'s report of `status`. 'working': a progress
969
+ * update (progressRunId), not the end. A wait for approval belongs to the
970
+ * target session, not to this send: every watcher of it names the same row,
971
+ * so it posts once. */
972
+ const turnReportRunId = (sessionId, id, status, { approvalId = '', answerId = '' } = {}) => {
973
+ if (status === 'blocked') return `${sessionId}_${approvalId}_blocked`;
974
+ return status === 'working' ? progressRunId(id, answerId) : `${id}_end`;
975
+ };
976
+
977
+ /** The report fields naming the progress update that may have carried the
978
+ * final answer of turn `answerId` already (the queue then does not repeat
979
+ * it); none for a report of no turn. */
980
+ const progressOf = (id, answerId) => (answerId ? { progressRunId: progressRunId(id, answerId) } : {});
981
+
982
+ /** The handler of send `id`'s task-session state: a wait for the user's
983
+ * approval reports blocked once; a later, different one reports again. */
984
+ function approvalWaitReporter(id, report) {
985
+ // The approval wait last reported.
986
+ let reportedApproval = null;
987
+ return (state) => {
988
+ if (!state.toolApproval) {
989
+ reportedApproval = null;
990
+ return;
991
+ }
992
+ const approvalId = String(state.toolApproval.id ?? 'approval');
993
+ if (approvalId === reportedApproval) return;
994
+ // A send still queued behind the running turn is not the turn
995
+ // waiting: that turn's own watcher reports it.
996
+ if ((state.queued || []).some((entry) => entry?.id === id)) return;
997
+ reportedApproval = approvalId;
998
+ void report('blocked', '', { approvalId });
999
+ };
1000
+ }
1001
+
1002
+ /** The transcript meta of a send from `origin`. `fromAgent`: the sending bot,
1003
+ * recorded when it writes into another bot's home chat (otherwise the row
1004
+ * would read as the user's); `hop`: its place in a bot-to-bot chain
1005
+ * (bot-hops.mjs). */
1006
+ const sendTranscriptMeta = (origin) => ({
1007
+ sender: 'lead',
1008
+ ...(origin.envelopeName ? { fromAgent: origin.agentId } : {}),
1009
+ ...(origin.hop > 0 ? { hop: origin.hop } : {}),
1010
+ // The bots that handed this work on, the sender last (bot-hops.mjs).
1011
+ ...(origin.botChain?.length ? { botChain: origin.botChain } : {}),
1012
+ });
1013
+
1014
+ /** What a send tells its caller once submitted. */
1015
+ const sendResult = (accepted, notify, id, noReport) => ({
1016
+ accepted,
1017
+ // Names this send; its terminal report carries it back (routine.sendId).
1018
+ ...(accepted && notify ? { sendId: id } : {}),
1019
+ reportsBack: accepted && notify,
1020
+ ...(accepted && !notify ? { reportsBackReason: noReport } : {}),
1021
+ });
1022
+
1023
+ /** create's brief and `worktree` location, checked before anything runs. */
1024
+ function createRequest(args, callerAgentId, callerScope) {
1025
+ if (!callerAgentId) throw new Error('session: create is only for an agent conversation');
1026
+ const brief = typeof args.brief === 'string' ? args.brief.trim() : '';
1027
+ if (!brief) throw new Error('session: brief is required for action=create');
1028
+ const location = taskLocation(args.worktree);
1029
+ if (location.mode === 'worktree') assertWorktreeMutationAllowed(callerScope, 'session: create with a worktree');
1030
+ return { brief, location };
1031
+ }
1032
+
1033
+ /** Where create starts (resolveTaskProject). An explicit worktree is never
1034
+ * silently dropped: it needs a registered Project. */
1035
+ function createWhere(args, projects, callerCwd, location) {
1036
+ const where = resolveTaskProject(args.project, { projects, cwd: callerCwd || process.cwd() });
1037
+ const registered = where.project && projects.some((entry) => samePath(entry.path, where.project.path));
1038
+ if (location.mode === 'worktree' && !registered) {
1039
+ throw taskWorktreeFailure(
1040
+ worktreeError('WORKTREE_PROJECT_UNKNOWN', 'A worktree needs a registered Project; run list-projects.', {
1041
+ projectPath: where.project?.path ?? null,
1042
+ })
1043
+ );
1044
+ }
1045
+ return where;
1046
+ }
1047
+
1048
+ /** The worktree failure `error` of task session `sessionId` whose empty
1049
+ * session could not be dropped (`dropFailure`): the model must know the
1050
+ * empty session is still there to delete. */
1051
+ const orphanSessionError = (error, sessionId, dropFailure) =>
1052
+ Object.assign(
1053
+ new Error(
1054
+ `${error?.message || error} The empty task session ${sessionId} could not be deleted either (${dropFailure?.message || dropFailure}); delete it with session action=delete session_id=${sessionId}.`
1055
+ ),
1056
+ {
1057
+ ...(typeof error?.code === 'string' ? { code: error.code } : {}),
1058
+ ...(error?.details ? { details: error.details } : {}),
1059
+ orphanSessionId: sessionId,
1060
+ cause: error,
1061
+ }
1062
+ );
1063
+
1064
+ /** `ready` resolves once `started()` runs: "Started" lands in the home before
1065
+ * any report of the brief's turn. */
1066
+ function startedGate() {
1067
+ let started = () => {};
1068
+ const ready = new Promise((resolveReady) => {
1069
+ started = resolveReady;
1070
+ });
1071
+ return { ready, started };
1072
+ }
1073
+
718
1074
  export function createAgentTaskSessions({
719
1075
  getSessionService,
720
1076
  deliver = async (agentId, report) => (await import('./agent-home-delivery.mjs')).deliverAgentTask(agentId, report),
@@ -737,6 +1093,8 @@ export function createAgentTaskSessions({
737
1093
  newTaskDefault = async () =>
738
1094
  (await import('../runtime/agent/orchestrator/config.mjs')).loadConfig({ secrets: false }).worktree?.newTaskDefault,
739
1095
  modelRoute = configModelRoute,
1096
+ // The model menu's catalog rows, read through the calling session's runtime.
1097
+ modelCatalog = (sessionId) => readModelCatalog(getSessionService(), sessionId),
740
1098
  // The Lead (Main) route of the config — what a desktop New task starts on.
741
1099
  leadRoute = configLeadRoute,
742
1100
  // The model the user last used in a coding session.
@@ -767,9 +1125,6 @@ export function createAgentTaskSessions({
767
1125
  owedResults = owedAgentResults,
768
1126
  deliverCompletion = null,
769
1127
  settleAgentResults = async () => {},
770
- // Pending agent work this long after the latest answer reports that answer
771
- // as still in progress (once); the final report still follows.
772
- pendingCapMs = PENDING_CAP_MS,
773
1128
  log = (line) => process.stderr.write(`mixdog task sessions: ${line}\n`),
774
1129
  }) {
775
1130
  /** The report fields of turn `runId`'s files; none when it produced none. */
@@ -791,61 +1146,106 @@ export function createAgentTaskSessions({
791
1146
  };
792
1147
 
793
1148
  const followToFinal = (follow) =>
794
- followTaskToFinal(
795
- { loadSession, owedResults, deliverCompletion, pendingWork, settleMs, pollMs, pendingCapMs, log },
796
- follow
797
- );
1149
+ followTaskToFinal({ loadSession, owedResults, deliverCompletion, pendingWork, settleMs, pollMs, log }, follow);
1150
+
1151
+ const settlement = createSendSettlement(log);
798
1152
 
799
1153
  /** Submit `prompt` as the origin agent's turn; its end (and a wait for the
800
1154
  * user in it) reports to the agent's home. Resolves on intake. `submit`
801
1155
  * takes the turn in another way than a prompt to `sessionId` (a message
802
1156
  * into a group room, whose member answers in `sessionId`): given `{ id,
803
1157
  * onSettled }`, it resolves `{ accepted }`. */
804
- async function agentTurn(sessionId, prompt, origin, id = `atask_${randomUUID()}`, beforeReport = null, submit = null) {
1158
+ async function agentTurn(
1159
+ sessionId,
1160
+ prompt,
1161
+ origin,
1162
+ id = `atask_${randomUUID()}`,
1163
+ beforeReport = null,
1164
+ submit = null
1165
+ ) {
805
1166
  const sessions = service();
806
1167
  const target = origin.target === 'session' ? { target: 'session' } : {};
807
1168
  const { notify, noReport } = await watchTurn({ watches, owner: processToken, log }, sessionId, origin, id);
808
1169
  const dropWatch = () =>
809
1170
  watches.remove(id).catch((error) => log(`dropping the watch of ${sessionId} failed: ${error?.message || error}`));
810
1171
  let runtime = null;
1172
+ // The sends this turn's end closes: this one, plus any that its turn
1173
+ // answered too (createSendSettlement).
1174
+ let sendIds = [id];
811
1175
  // A report that cannot be queued is retried in this process; when every try
812
1176
  // fails the durable watch stays and restart recovery delivers it (the queue
813
1177
  // dedupes by runId, so a retry never posts twice).
814
- const report = (status, text = '') =>
815
- Promise.resolve(beforeReport)
816
- .then(async () =>
817
- deliverWithRetries({ deliver, retryDelaysMs, log }, origin.agentId, sessionId, {
818
- // 'working': a still-in-progress answer (followToFinal), not the end.
819
- runId: `${id}_${{ blocked: 'blocked', working: 'progress' }[status] || 'end'}`,
820
- taskSessionId: sessionId,
821
- title: origin.title,
822
- status,
823
- text,
824
- ...target,
825
- ...(origin.hop > 0 ? { hop: origin.hop } : {}),
826
- ...roomFrom(origin.sessionId),
827
- // A turn's end brings back the files it produced, when the agent
828
- // may receive them (reportOrigin `textOnly`).
829
- ...(status === 'blocked' || status === 'working'
830
- ? {}
831
- : { watchId: id, ...(origin.textOnly ? {} : await artifactFields(sessionId, id, runtime)) }),
832
- })
833
- )
834
- .catch((error) => log(`report of ${sessionId} failed: ${error?.message || error}`));
1178
+ // `answerId`: the turn whose answer a progress update or the end carries.
1179
+ // `deliverReport` rejects once every try failed (a progress update is
1180
+ // tried again at the follow's next check); `report` logs it instead.
1181
+ const deliverReport = (status, text = '', { approvalId = '', answerId = '' } = {}) =>
1182
+ Promise.resolve(beforeReport).then(async () =>
1183
+ deliverWithRetries({ deliver, retryDelaysMs, log }, origin.agentId, sessionId, {
1184
+ runId: turnReportRunId(sessionId, id, status, { approvalId, answerId }),
1185
+ taskSessionId: sessionId,
1186
+ title: origin.title,
1187
+ status,
1188
+ text,
1189
+ ...target,
1190
+ ...(origin.hop > 0 ? { hop: origin.hop } : {}),
1191
+ ...roomFrom(origin.sessionId),
1192
+ // A progress update moves this send's durable mark to its turn
1193
+ // (agent-home-delivery.mjs enqueueTask), in the same write.
1194
+ ...(status === 'working' ? { progressWatchId: id, answerId } : {}),
1195
+ // A turn's end brings back the files it produced, when the agent
1196
+ // may receive them (reportOrigin `textOnly`).
1197
+ ...(status === 'blocked' || status === 'working'
1198
+ ? {}
1199
+ : {
1200
+ watchId: id,
1201
+ ...(sendIds.length > 1 ? { watchIds: sendIds } : {}),
1202
+ ...progressOf(id, answerId),
1203
+ ...(origin.textOnly ? {} : await artifactFields(sessionId, id, runtime)),
1204
+ }),
1205
+ })
1206
+ );
1207
+ const report = (status, text = '', options = {}) =>
1208
+ deliverReport(status, text, options).catch((error) =>
1209
+ log(`report of ${sessionId} failed: ${error?.message || error}`)
1210
+ );
835
1211
  let unwatch = () => {};
836
1212
  // Set once the turn ended completed: the report waits for the final answer.
837
1213
  let follow = null;
838
1214
  let result;
1215
+ /** This send ends the turn `detail` for the sends `answered` (createSendSettlement). */
1216
+ const settleTurn = (detail, answered) => {
1217
+ sendIds = answered;
1218
+ const status = turnStatus(detail);
1219
+ const text = detail?.result?.content || detail?.error || '';
1220
+ // A failed or cancelled turn reports at once; a completed one
1221
+ // reports the task's final answer (followToFinal).
1222
+ if (status !== 'completed') {
1223
+ unwatch();
1224
+ void report(status, text);
1225
+ return;
1226
+ }
1227
+ follow = followToFinal({
1228
+ sessionId,
1229
+ runId: id,
1230
+ runtime,
1231
+ immediate: { status, text },
1232
+ finish: async (endStatus, endText, answerId) => {
1233
+ unwatch();
1234
+ await report(endStatus, endText, { answerId });
1235
+ },
1236
+ // Each other answer of its chain: a progress update that wakes nobody.
1237
+ interim: (answer) => deliverReport('working', answer.text, { answerId: answer.id }),
1238
+ });
1239
+ follow.check();
1240
+ };
839
1241
  try {
840
1242
  if (notify) {
841
1243
  runtime = await sessions.materializeSession(sessionId);
842
- let blocked = false;
1244
+ const onState = approvalWaitReporter(id, report);
843
1245
  unwatch =
844
1246
  runtime?.subscribe?.(() => {
845
1247
  follow?.check();
846
- if (blocked || !runtime.getState?.()?.toolApproval) return;
847
- blocked = true;
848
- void report('blocked');
1248
+ onState(runtime.getState?.() || {});
849
1249
  }) || unwatch;
850
1250
  }
851
1251
  // A bot's send to a session that is not its own task session reaches the
@@ -854,46 +1254,24 @@ export function createAgentTaskSessions({
854
1254
  ? wrapAgentMessage(prompt, { name: origin.envelopeName, agentId: origin.agentId })
855
1255
  : prompt;
856
1256
  const take = submit ?? ((options) => sessions.submitSession({ sessionId, prompt: sent, options }));
1257
+ const seq = settlement.nextSeq();
857
1258
  result = await take({
858
1259
  id,
859
1260
  mode: 'prompt',
860
- // `fromAgent`: the sending bot, recorded when it writes into another
861
- // bot's home chat (otherwise the row would read as the user's);
862
- // `hop`: its place in a bot-to-bot chain (bot-hops.mjs).
863
- transcriptMeta: {
864
- sender: 'lead',
865
- ...(origin.envelopeName ? { fromAgent: origin.agentId } : {}),
866
- ...(origin.hop > 0 ? { hop: origin.hop } : {}),
867
- },
1261
+ transcriptMeta: sendTranscriptMeta(origin),
868
1262
  ...(notify
869
1263
  ? {
870
1264
  // A room answers this send in a turn under another submission
871
1265
  // id (one turn for several sends, or a fresh turn for one cut
872
1266
  // short): the watch follows that turn, recorded before it runs.
873
1267
  follow: (submissionId) => watches.follow?.(id, submissionId),
874
- onSettled: (detail) => {
875
- const status = turnStatus(detail);
876
- const text = detail?.result?.content || detail?.error || '';
877
- // A failed or cancelled turn reports at once; a completed one
878
- // reports the task's final answer (followToFinal).
879
- if (status !== 'completed') {
880
- unwatch();
881
- void report(status, text);
882
- return;
883
- }
884
- follow = followToFinal({
885
- sessionId,
886
- runId: id,
887
- runtime,
888
- immediate: { status, text },
889
- finish: async (endStatus, endText) => {
890
- unwatch();
891
- await report(endStatus, endText);
892
- },
893
- progress: (progressText) => report('working', progressText),
894
- });
895
- follow.check();
896
- },
1268
+ onSettled: (detail) =>
1269
+ settlement.join(detail, origin, {
1270
+ seq,
1271
+ id,
1272
+ unwatch: () => unwatch(),
1273
+ settle: (answered) => settleTurn(detail, answered),
1274
+ }),
897
1275
  }
898
1276
  : {}),
899
1277
  });
@@ -907,11 +1285,7 @@ export function createAgentTaskSessions({
907
1285
  unwatch();
908
1286
  if (notify) await dropWatch();
909
1287
  }
910
- return {
911
- accepted,
912
- reportsBack: accepted && notify,
913
- ...(accepted && !notify ? { reportsBackReason: noReport } : {}),
914
- };
1288
+ return sendResult(accepted, notify, id, noReport);
915
1289
  }
916
1290
 
917
1291
  const rearm = (watch) =>
@@ -954,24 +1328,18 @@ export function createAgentTaskSessions({
954
1328
  * caller is refused and the New task default keeps it in the Project folder. */
955
1329
  async function create(
956
1330
  args = {},
957
- { callerSessionId, callerAgentId, callerCwd, callerHop = 0, callerScope, attachmentParts = [] } = {}
1331
+ {
1332
+ callerSessionId,
1333
+ callerAgentId,
1334
+ callerCwd,
1335
+ callerHop = 0,
1336
+ callerBotChain = [],
1337
+ callerScope,
1338
+ attachmentParts = [],
1339
+ } = {}
958
1340
  ) {
959
- if (!callerAgentId) throw new Error('session: create is only for an agent conversation');
960
- const brief = typeof args.brief === 'string' ? args.brief.trim() : '';
961
- if (!brief) throw new Error('session: brief is required for action=create');
962
- const location = taskLocation(args.worktree);
963
- if (location.mode === 'worktree') assertWorktreeMutationAllowed(callerScope, 'session: create with a worktree');
964
- const projects = await listProjects();
965
- const where = resolveTaskProject(args.project, { projects, cwd: callerCwd || process.cwd() });
966
- // An explicit worktree is never silently dropped: it needs a registered Project.
967
- const registered = where.project && projects.some((entry) => samePath(entry.path, where.project.path));
968
- if (location.mode === 'worktree' && !registered) {
969
- throw taskWorktreeFailure(
970
- worktreeError('WORKTREE_PROJECT_UNKNOWN', 'A worktree needs a registered Project; run list-projects.', {
971
- projectPath: where.project?.path ?? null,
972
- })
973
- );
974
- }
1341
+ const { brief, location } = createRequest(args, callerAgentId, callerScope);
1342
+ const where = createWhere(args, await listProjects(), callerCwd, location);
975
1343
  const requestId = `atask_${randomUUID()}`;
976
1344
  const title = (clean(args.title) || clean(brief.split('\n')[0])).slice(0, TITLE_LIMIT);
977
1345
  const taskOrigin = {
@@ -984,10 +1352,18 @@ export function createAgentTaskSessions({
984
1352
  // A task session runs in coding mode under Lead: an explicit model wins,
985
1353
  // else the Lead route, else the last-used model. Never a model-less
986
1354
  // session — the composer would show no model and its first turn would fail.
987
- const route = clean(args.model)
988
- ? await modelRoute(clean(args.model))
1355
+ // The catalog is read only when a display name or an effort needs it.
1356
+ let catalog = null;
1357
+ const catalogRows = async () => (catalog ??= await modelCatalog(callerSessionId));
1358
+ let route = clean(args.model)
1359
+ ? await modelRoute(clean(args.model), async (text) =>
1360
+ routeByDisplayName(text, await catalogRows(), args.effort !== undefined)
1361
+ )
989
1362
  : (await leadRoute()) || (await lastUsedRoute(service()));
990
1363
  if (!route) throw new Error('session: no model to start the task session on. Choose a Lead model first.');
1364
+ // An explicit effort is the setting the menu's effort pick makes (session.effort,
1365
+ // Auto effort off); it wins over one a model ref or name carried.
1366
+ if (args.effort !== undefined) route = withEffort(route, args.effort, await catalogRows());
991
1367
  const sessions = service();
992
1368
  // Where it starts: the `worktree` option, else the user's New task default
993
1369
  // for a caller that may provision one. Decided before the session exists,
@@ -1031,31 +1407,17 @@ export function createAgentTaskSessions({
1031
1407
  }
1032
1408
  if (dropFailure) log(`dropping ${sessionId} failed: ${dropFailure?.message || dropFailure}`);
1033
1409
  if (!dropFailure) throw error;
1034
- // The model must know the empty session is still there to delete.
1035
- throw Object.assign(
1036
- new Error(
1037
- `${error?.message || error} The empty task session ${sessionId} could not be deleted either (${dropFailure?.message || dropFailure}); delete it with session action=delete session_id=${sessionId}.`
1038
- ),
1039
- {
1040
- ...(typeof error?.code === 'string' ? { code: error.code } : {}),
1041
- ...(error?.details ? { details: error.details } : {}),
1042
- orphanSessionId: sessionId,
1043
- cause: error,
1044
- }
1045
- );
1410
+ throw orphanSessionError(error, sessionId, dropFailure);
1046
1411
  }
1047
1412
  }
1048
- // "Started" lands in the home before any report of the brief's turn.
1049
- let started = () => {};
1050
- const ready = new Promise((resolveReady) => {
1051
- started = resolveReady;
1052
- });
1413
+ const { ready, started } = startedGate();
1053
1414
  // The brief carries the files the agent forwarded (workspace-tools.mjs).
1054
1415
  const prompt = handoffContent(brief, attachmentParts);
1416
+ const botChain = botChainOf(callerBotChain);
1055
1417
  const { accepted, reportsBack, reportsBackReason } = await agentTurn(
1056
1418
  sessionId,
1057
1419
  prompt,
1058
- callerHop > 0 ? { ...taskOrigin, hop: callerHop } : taskOrigin,
1420
+ { ...taskOrigin, ...(callerHop > 0 ? { hop: callerHop } : {}), ...(botChain.length ? { botChain } : {}) },
1059
1421
  requestId,
1060
1422
  ready
1061
1423
  );