mixdog 1.2.5 → 1.2.6

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 (290) hide show
  1. package/package.json +1 -1
  2. package/scripts/lib/gate-scheduler.mjs +2 -2
  3. package/scripts/lib/run-node-tests.mjs +5 -14
  4. package/scripts/provider-pingpong-live.mjs +9 -9
  5. package/src/agents/advisor/AGENT.md +4 -4
  6. package/src/agents/advisor/agent.json +1 -1
  7. package/src/agents/reviewer/AGENT.md +2 -3
  8. package/src/agents/reviewer/agent.json +1 -1
  9. package/src/defaults/shipped-history.json +10 -0
  10. package/src/defaults/skills/browser-use/SKILL.md +2 -1
  11. package/src/defaults/skills/computer-use/SKILL.md +3 -1
  12. package/src/defaults/skills/visualize/SKILL.md +3 -2
  13. package/src/lib/rules-builder.cjs +1 -1
  14. package/src/output-styles/common.md +22 -6
  15. package/src/output-styles/detailed.md +7 -5
  16. package/src/output-styles/minimal.md +7 -3
  17. package/src/output-styles/simple.md +2 -3
  18. package/src/rules/agent/AGENT.md +3 -1
  19. package/src/rules/lead/LEAD.md +4 -3
  20. package/src/rules/shared/10-tool-workflow.md +2 -1
  21. package/src/rules/shared/60-verification.md +4 -5
  22. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -1
  23. package/src/runtime/agent/orchestrator/config.mjs +6 -0
  24. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +1 -1
  25. package/src/runtime/agent/orchestrator/internal-agents.mjs +10 -0
  26. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +53 -60
  27. package/src/runtime/agent/orchestrator/mcp/client.mjs +7 -4
  28. package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +2 -3
  29. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +9 -2
  30. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +40 -19
  31. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +3 -2
  32. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events/content-block-start.mjs +4 -1
  33. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events.mjs +2 -0
  34. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn.mjs +3 -0
  35. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +2 -1
  36. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +3 -1
  37. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +1 -2
  38. package/src/runtime/agent/orchestrator/providers/catalog-deadline.mjs +163 -0
  39. package/src/runtime/agent/orchestrator/providers/catalog-failures.mjs +34 -0
  40. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +38 -14
  41. package/src/runtime/agent/orchestrator/providers/lib/oauth-page.mjs +100 -26
  42. package/src/runtime/agent/orchestrator/providers/lib/send-callbacks.mjs +24 -0
  43. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +13 -0
  44. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +2 -1
  45. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -0
  46. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/chunk-events.mjs +4 -0
  47. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +3 -1
  48. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +2 -0
  49. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +49 -4
  50. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +0 -7
  51. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +7 -1
  52. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +1 -0
  53. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +2 -0
  54. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +1 -0
  55. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events.mjs +5 -1
  56. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +11 -2
  57. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +1 -0
  58. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +45 -16
  59. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +1 -0
  60. package/src/runtime/agent/orchestrator/providers/openai-ws-connect.mjs +196 -0
  61. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +74 -9
  62. package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +14 -1
  63. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -0
  64. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth-login.mjs +194 -0
  65. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth-tokens.mjs +353 -0
  66. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +293 -0
  67. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +33 -100
  68. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +23 -14
  69. package/src/runtime/agent/orchestrator/providers/registry.mjs +41 -27
  70. package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +2 -3
  71. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +3 -8
  72. package/src/runtime/agent/orchestrator/session/agent-pool-progress.mjs +15 -3
  73. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +74 -13
  74. package/src/runtime/agent/orchestrator/session/compact/forked-summary.mjs +102 -0
  75. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +8 -2
  76. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +16 -0
  77. package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +9 -0
  78. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +39 -2
  79. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +9 -19
  80. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +2 -3
  81. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -0
  82. package/src/runtime/agent/orchestrator/session/manager/ask-turn-steering-drain.mjs +13 -5
  83. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +10 -7
  84. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +62 -22
  85. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +23 -1
  86. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +81 -34
  87. package/src/runtime/agent/orchestrator/session/manager.mjs +2 -2
  88. package/src/runtime/agent/orchestrator/session/request-fork.mjs +47 -0
  89. package/src/runtime/agent/orchestrator/session/side-question-store.mjs +16 -1
  90. package/src/runtime/agent/orchestrator/session/side-question.mjs +5 -43
  91. package/src/runtime/agent/orchestrator/session/store/fs-probe.mjs +10 -0
  92. package/src/runtime/agent/orchestrator/session/store/lifecycle-barriers.mjs +73 -6
  93. package/src/runtime/agent/orchestrator/session/store/load-session.mjs +40 -0
  94. package/src/runtime/agent/orchestrator/session/store/save-fault.mjs +15 -0
  95. package/src/runtime/agent/orchestrator/session/store/session-paths.mjs +12 -0
  96. package/src/runtime/agent/orchestrator/session/store-agent-worker-header.mjs +36 -0
  97. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +258 -43
  98. package/src/runtime/agent/orchestrator/session/store-record-strict-scan.mjs +306 -0
  99. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +70 -21
  100. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +46 -30
  101. package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +144 -45
  102. package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +32 -13
  103. package/src/runtime/agent/orchestrator/session/store-transcript-worker.mjs +364 -38
  104. package/src/runtime/agent/orchestrator/session/store.mjs +9 -2
  105. package/src/runtime/agent/orchestrator/session/task-wait-control.mjs +43 -12
  106. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +23 -6
  107. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -6
  108. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +13 -7
  109. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/bashism-rewrite.mjs +1 -1
  110. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/benign-exit.mjs +7 -1
  111. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +40 -7
  112. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +37 -34
  113. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/inline-hoist.mjs +1 -1
  114. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +0 -4
  115. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +6 -0
  116. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +29 -27
  117. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +1 -1
  118. package/src/runtime/agent/orchestrator/tools/builtin/find-files-tool.mjs +28 -11
  119. package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +12 -3
  120. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +203 -101
  121. package/src/runtime/agent/orchestrator/tools/builtin/git-repo-rw-lock.mjs +90 -41
  122. package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +17 -1
  123. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-delivery.mjs +199 -0
  124. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-task-output.mjs +165 -39
  125. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +25 -13
  126. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +7 -6
  127. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +19 -4
  128. package/src/runtime/agent/orchestrator/tools/builtin/read-line-collector.mjs +5 -3
  129. package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +25 -7
  130. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +2 -1
  131. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-render.mjs +3 -1
  132. package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +12 -5
  133. package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +61 -22
  134. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +32 -8
  135. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +193 -19
  136. package/src/runtime/agent/orchestrator/tools/builtin/shell-direct-exe.mjs +57 -7
  137. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +119 -32
  138. package/src/runtime/agent/orchestrator/tools/builtin/shell-lossless-compact.mjs +26 -1
  139. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +13 -28
  140. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +90 -11
  141. package/src/runtime/agent/orchestrator/tools/builtin/wait-budgets.mjs +23 -0
  142. package/src/runtime/agent/orchestrator/tools/builtin.mjs +54 -10
  143. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +22 -28
  144. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +11 -7
  145. package/src/runtime/agent/orchestrator/tools/destructive-warning.mjs +126 -9
  146. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
  147. package/src/runtime/agent/orchestrator/tools/lib/owned-shell-trees.mjs +15 -6
  148. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +40 -2
  149. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +2 -2
  150. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promotion-results.mjs +13 -3
  151. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +2 -0
  152. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +3 -1
  153. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +1 -1
  154. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +35 -4
  155. package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +7 -2
  156. package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +39 -23
  157. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +6 -2
  158. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +13 -13
  159. package/src/runtime/attachments/office-extract.mjs +11 -9
  160. package/src/runtime/memory/lib/embedding-tokenizer.mjs +32 -18
  161. package/src/runtime/memory/lib/runtime-fetcher.mjs +1 -1
  162. package/src/runtime/memory/lib/tool-call-handler.mjs +7 -5
  163. package/src/runtime/office/authoring/pptx-script-normalize.mjs +11 -8
  164. package/src/runtime/office/core/tabular.mjs +1 -1
  165. package/src/runtime/office/design/library/design-template-inspect.mjs +7 -4
  166. package/src/runtime/office/portable/docx-formatting.mjs +1 -1
  167. package/src/runtime/office/portable/docx-runs.mjs +1 -1
  168. package/src/runtime/office/portable/docx-tracked-edits.mjs +2 -2
  169. package/src/runtime/office/portable/portable-docx-edits.mjs +11 -10
  170. package/src/runtime/office/portable/portable-docx-operations.mjs +21 -17
  171. package/src/runtime/office/portable/portable-docx-parts.mjs +15 -9
  172. package/src/runtime/office/portable/portable-docx-styles.mjs +4 -1
  173. package/src/runtime/office/portable/portable-docx-xml.mjs +9 -9
  174. package/src/runtime/office/portable/portable-opc.mjs +27 -13
  175. package/src/runtime/office/portable/portable-pivot.mjs +3 -3
  176. package/src/runtime/office/portable/portable-pptx-chart.mjs +3 -3
  177. package/src/runtime/office/portable/portable-pptx-charts.mjs +24 -18
  178. package/src/runtime/office/portable/portable-pptx-core.mjs +5 -5
  179. package/src/runtime/office/portable/portable-pptx-deck.mjs +8 -4
  180. package/src/runtime/office/portable/portable-pptx-package.mjs +7 -4
  181. package/src/runtime/office/portable/portable-pptx-shapes.mjs +18 -9
  182. package/src/runtime/office/portable/portable-sheet-parts.mjs +9 -3
  183. package/src/runtime/office/portable/portable-sheet-xml.mjs +5 -5
  184. package/src/runtime/office/portable/portable-xlsx-charts.mjs +4 -1
  185. package/src/runtime/office/portable/portable-xlsx-drawings.mjs +10 -6
  186. package/src/runtime/office/portable/portable-xlsx-operations.mjs +8 -5
  187. package/src/runtime/office/portable/portable-xlsx-pivot-ops.mjs +1 -1
  188. package/src/runtime/office/portable/portable-xlsx-reference-shift.mjs +11 -7
  189. package/src/runtime/office/portable/portable-xlsx-sheet-edits.mjs +6 -3
  190. package/src/runtime/office/portable/portable-xml.mjs +1 -1
  191. package/src/runtime/shared/atomic-file.mjs +22 -13
  192. package/src/runtime/shared/background-tasks.mjs +60 -20
  193. package/src/runtime/shared/child-spawn-gate.mjs +26 -16
  194. package/src/runtime/shared/config.mjs +0 -10
  195. package/src/runtime/shared/file-lock.mjs +7 -1
  196. package/src/runtime/shared/file-permissions-worker.mjs +38 -0
  197. package/src/runtime/shared/file-permissions.d.mts +19 -4
  198. package/src/runtime/shared/file-permissions.mjs +310 -4
  199. package/src/runtime/shared/git-runner-worker.mjs +6 -78
  200. package/src/runtime/shared/llm/http-agent.mjs +7 -3
  201. package/src/runtime/shared/llm/provider-catalog-registry.mjs +10 -0
  202. package/src/runtime/shared/llm/usage-accounting.mjs +2 -0
  203. package/src/runtime/shared/llm/usage-ledger-import.mjs +32 -3
  204. package/src/runtime/shared/llm/usage-ledger-rollup.mjs +12 -5
  205. package/src/runtime/shared/llm/usage-ledger-worker.mjs +1 -1
  206. package/src/runtime/shared/llm/usage-ledger.mjs +20 -5
  207. package/src/runtime/shared/memory-snapshot.mjs +5 -1
  208. package/src/runtime/shared/notify-trace.mjs +8 -1
  209. package/src/runtime/shared/pristine-execution-contract.json +5 -1
  210. package/src/runtime/shared/process-runner-worker.mjs +7 -0
  211. package/src/runtime/shared/process-runner.mjs +38 -0
  212. package/src/runtime/shared/provider-accounts.mjs +2 -1
  213. package/src/runtime/shared/provider-api-key.mjs +0 -1
  214. package/src/runtime/shared/read-row-numbers.mjs +1 -1
  215. package/src/runtime/shared/resource-admission.mjs +60 -18
  216. package/src/runtime/shared/run-child.mjs +84 -0
  217. package/src/runtime/shared/session-activity-signal.mjs +23 -0
  218. package/src/runtime/shared/shell-output-summary.mjs +23 -0
  219. package/src/runtime/shared/singleton-owner.mjs +126 -42
  220. package/src/runtime/shared/tool-output-limit.mjs +4 -2
  221. package/src/runtime/shared/tool-output-marker.mjs +58 -0
  222. package/src/runtime/shared/user-data-guard.mjs +28 -9
  223. package/src/runtime/shared/worktrees/activity-gate.mjs +33 -8
  224. package/src/runtime/shared/worktrees/git-head.mjs +20 -2
  225. package/src/runtime/shared/worktrees/lock.mjs +3 -2
  226. package/src/runtime/terminal-bridge/tool-defs.mjs +1 -1
  227. package/src/runtime/tidy/runners/biome.mjs +3 -1
  228. package/src/runtime/tidy/shadow.mjs +2 -1
  229. package/src/runtime/web-search/index.mjs +5 -7
  230. package/src/runtime/web-search/lib/formatter.mjs +42 -6
  231. package/src/runtime/web-search/tool-defs.mjs +6 -1
  232. package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +4 -3
  233. package/src/session-runtime/lifecycle/session-switch/resume.mjs +7 -4
  234. package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +3 -3
  235. package/src/session-runtime/lifecycle/shared.mjs +19 -2
  236. package/src/session-runtime/lifecycle/teardown/session-close.mjs +2 -0
  237. package/src/session-runtime/lifecycle/teardown.mjs +5 -2
  238. package/src/session-runtime/lifecycle-api.mjs +3 -1
  239. package/src/session-runtime/provider-auth/login.mjs +10 -6
  240. package/src/session-runtime/provider-auth/setup.mjs +19 -2
  241. package/src/session-runtime/provider-catalog-cache.mjs +101 -25
  242. package/src/session-runtime/provider-models/catalog-load.mjs +14 -3
  243. package/src/session-runtime/provider-models/row-hydration.mjs +4 -0
  244. package/src/session-runtime/provider-models.mjs +76 -10
  245. package/src/session-runtime/provider-readiness.mjs +4 -0
  246. package/src/session-runtime/services/agent-tool/close-flow.mjs +2 -1
  247. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +10 -2
  248. package/src/session-runtime/services/agent-tool/job-views/session-progress.mjs +13 -4
  249. package/src/session-runtime/services/agent-tool/notify.mjs +15 -4
  250. package/src/session-runtime/services/agent-tool/restart-notify.mjs +134 -0
  251. package/src/session-runtime/services/agent-tool/send-flow.mjs +24 -1
  252. package/src/session-runtime/services/agent-tool/spawn-flow/deferred-spawn.mjs +2 -1
  253. package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +4 -3
  254. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +5 -1
  255. package/src/session-runtime/services/agent-tool/tag-registry/tag-lookup.mjs +8 -3
  256. package/src/session-runtime/services/agent-tool/tag-registry.mjs +3 -0
  257. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +4 -0
  258. package/src/session-runtime/services/agent-tool/worker-index.mjs +44 -10
  259. package/src/session-runtime/services/agent-tool.mjs +20 -0
  260. package/src/session-runtime/services/provider-admin.mjs +92 -20
  261. package/src/session-runtime/services/usage-dashboard-model.mjs +2 -2
  262. package/src/session-runtime/services/usage-dashboard.mjs +27 -69
  263. package/src/session-runtime/turn/session-ops.mjs +2 -2
  264. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +1 -0
  265. package/src/session-runtime/usage-stats-api.mjs +6 -1
  266. package/src/standalone/channel-transport/http-routes.mjs +10 -9
  267. package/src/standalone/daemon-desktop-runtime.mjs +20 -7
  268. package/src/standalone/daemon-owner.mjs +117 -25
  269. package/src/standalone/daemon.mjs +43 -13
  270. package/src/standalone/session-call-cache.mjs +6 -0
  271. package/src/standalone/session-protocol.mjs +1 -1
  272. package/src/standalone/session-service/retention/busy.mjs +7 -2
  273. package/src/standalone/session-transport/routes.mjs +7 -6
  274. package/src/tui/app/model-options.mjs +39 -2
  275. package/src/tui/app/model-picker.mjs +16 -2
  276. package/src/tui/app/provider-setup-picker.mjs +1 -2
  277. package/src/tui/dist/index.mjs +385 -104
  278. package/src/tui/session/notice-text.mjs +0 -1
  279. package/src/tui/session/session-api/intake/submission.mjs +7 -13
  280. package/src/tui/session/session-api/integrations.mjs +1 -5
  281. package/src/tui/session/session-flow/drain.mjs +8 -0
  282. package/src/tui/session/session-flow/steering.mjs +46 -0
  283. package/src/tui/session/session-flow.mjs +2 -0
  284. package/src/tui/session/turn-stream.mjs +33 -0
  285. package/src/tui/session/turn.mjs +30 -3
  286. package/src/ui/statusline.mjs +1 -6
  287. package/src/ui/streaming-markdown-heal.mjs +24 -7
  288. package/src/vendor/statusline/bin/statusline-route.mjs +0 -1
  289. package/src/vendor/statusline/src/gateway/route-meta.mjs +0 -1
  290. package/src/workflows/default/WORKFLOW.md +9 -13
@@ -8,7 +8,7 @@ import {
8
8
  // eval), so the static cycle through session/manager.mjs is safe — same
9
9
  // pattern as loop/tool-exec.mjs.
10
10
  import { enqueuePendingMessage, markCompletionEntry } from '../../session/manager.mjs';
11
- import { completeBackgroundTask } from '../../../../shared/background-tasks.mjs';
11
+ import { completeBackgroundTask, getBackgroundTask } from '../../../../shared/background-tasks.mjs';
12
12
  import {
13
13
  cancelNativeTask,
14
14
  getNativeTask,
@@ -35,29 +35,45 @@ import {
35
35
  retireShellJobRecord,
36
36
  takeReleaseRequest,
37
37
  } from './lib/shell-job-records.mjs';
38
- import { createProcessTreeTracker, killAfterSnapshot, watchTermination } from '../lib/shell-descendants.mjs';
38
+ import {
39
+ createProcessTreeTracker,
40
+ killAfterSnapshot,
41
+ PROCESS_TABLE_READ_BOUND_MS,
42
+ watchTermination,
43
+ withinBound,
44
+ } from '../lib/shell-descendants.mjs';
39
45
  import {
40
46
  flushPendingOwnerCancels,
47
+ prepareOwnedShellTreeKills,
41
48
  registerOwnedShellTree,
42
49
  unregisterOwnedShellTree,
43
50
  } from '../lib/owned-shell-trees.mjs';
44
51
 
45
- // Pre-kill snapshot of a tracked background job: the promotion baseline, or a
46
- // fresh read when that did not complete. Marks the job killed, so its settle
47
- // relies on this snapshot instead of an after-exit observation.
52
+ // Bound on how long a runtime/session shutdown waits for the sweep of what a
53
+ // cancelled job started outside its process group.
54
+ const SHUTDOWN_TREE_CONFIRM_MS = 3_000;
55
+ // Lead of the pre-deadline snapshot over a native timeout kill: one table
56
+ // read's bound plus margin.
57
+ const PRE_DEADLINE_CAPTURE_MS = PROCESS_TABLE_READ_BOUND_MS + 1_000;
58
+
59
+ // Pre-kill snapshot of a tracked background job: always a fresh read, since
60
+ // the promotion baseline predates whatever the job started since (a server,
61
+ // an npm child); the baseline still stands when the fresh read fails. Marks
62
+ // the job killed, so its settle relies on this snapshot instead of an
63
+ // after-exit observation.
48
64
  function prepareTrackedKill(entry) {
49
65
  entry.killed = true;
50
- if (entry.tracker.complete) return true;
51
- return (entry.baseline || Promise.resolve(false)).then(
52
- (complete) => complete || entry.tracker.capture().catch(() => false)
66
+ return (entry.baseline || Promise.resolve(false)).then(() =>
67
+ entry.tracker.capture().catch(() => entry.tracker.complete)
53
68
  );
54
69
  }
55
70
  import { isPidAlive } from '../../../../shared/pid-liveness.mjs';
56
71
  import {
57
72
  renderShellCompletionEnvelope,
58
- renderShellCompletionNotice,
59
73
  shellCompletionInstruction,
60
74
  } from '../../../../shared/task-notification-envelope.mjs';
75
+ import { formatDuration } from '../../../../shared/shell-output-summary.mjs';
76
+ import { readShellTaskOutput } from './lib/shell-task-output.mjs';
61
77
 
62
78
  export { shellJobPublicTaskResult } from './lib/shell-job-insights.mjs';
63
79
 
@@ -105,7 +121,17 @@ function trackShellJobRecord(task, options) {
105
121
  adoptShellWorkspaceAdmission(jobId, hold);
106
122
  }
107
123
  const tracker = task?.cwd ? createProcessTreeTracker({ pid: task.pid }) : null;
108
- const entry = { tracker, killed: false, baseline: null };
124
+ let resolveTerminated = null;
125
+ const entry = {
126
+ tracker,
127
+ killed: false,
128
+ baseline: null,
129
+ // Settles once every process of the job is confirmed gone (or the user
130
+ // force-released it): what `task cancel` waits on before it reports.
131
+ terminated: new Promise((resolve) => {
132
+ resolveTerminated = resolve;
133
+ }),
134
+ };
109
135
  if (tracker) {
110
136
  backgroundTrackers.set(jobId, entry);
111
137
  // Baseline identities while the promoted root is known to run, so a later
@@ -117,9 +143,24 @@ function trackShellJobRecord(task, options) {
117
143
  }
118
144
  let unsubscribe = null;
119
145
  let settled = false;
146
+ // The native runner enforces the job's deadline itself, with no snapshot
147
+ // from here: one is taken shortly before it, so the timeout kill has the
148
+ // identities of what the job started since its promotion.
149
+ const deadlineMs = Math.floor(Number(options?.timeoutMs) || 0);
150
+ const deadlineCapture =
151
+ tracker && deadlineMs > 0
152
+ ? setTimeout(
153
+ () => {
154
+ if (!settled) void tracker.capture().catch(() => false);
155
+ },
156
+ Math.max(0, deadlineMs - PRE_DEADLINE_CAPTURE_MS)
157
+ )
158
+ : null;
159
+ deadlineCapture?.unref?.();
120
160
  const settle = (detail) => {
121
161
  if (settled || !detail || detail.status === 'running') return;
122
162
  settled = true;
163
+ clearTimeout(deadlineCapture);
123
164
  try {
124
165
  unsubscribe?.();
125
166
  } catch {}
@@ -144,10 +185,13 @@ function trackShellJobRecord(task, options) {
144
185
  stop();
145
186
  clearTerminationHold(jobId);
146
187
  if (backgroundTrackers.get(jobId) === entry) backgroundTrackers.delete(jobId);
188
+ resolveTerminated(true);
147
189
  return completeShellJobRecord(jobId, normalized);
148
190
  };
149
191
  const stop = watchTermination({
150
192
  tracker,
193
+ // A killed job is not over while processes it started still run.
194
+ sweep: killed,
151
195
  onTerminated: finish,
152
196
  releaseRequested: () => takeReleaseRequest(jobId),
153
197
  onUnconfirmed: () => {
@@ -226,7 +270,18 @@ function shellJobFailureCause(detail) {
226
270
  return `exited with code ${exitCode}`;
227
271
  }
228
272
 
229
- export function buildShellCompletion(jobId, detail) {
273
+ function completionStreams(detail) {
274
+ return {
275
+ stdout_path: detail?.stdoutPath || null,
276
+ stderr_path: detail?.mergeStderr === true ? null : detail?.stderrPath || null,
277
+ stdout_preview: detail?.stdoutPreview || '',
278
+ stderr_preview: detail?.mergeStderr === true ? '' : detail?.stderrPreview || '',
279
+ };
280
+ }
281
+
282
+ /** `output`: the unseen output to deliver (the task cursor's remainder). By
283
+ * default the whole capture is read with a fresh cursor, consuming nothing. */
284
+ export function buildShellCompletion(jobId, detail, { output } = {}) {
230
285
  const exitCode = typeof detail?.exitCode === 'number' ? detail.exitCode : null;
231
286
  const status = detail?.status || 'unknown';
232
287
  const taskStatus = shellJobTaskStatus(detail);
@@ -234,29 +289,36 @@ export function buildShellCompletion(jobId, detail) {
234
289
  // native task has no `error` of its own.
235
290
  const failureCause = taskStatus === 'failed' ? shellJobFailureCause(detail) : null;
236
291
  const reported = failureCause && !detail?.error ? { ...detail, error: failureCause } : detail;
292
+ const startedMs = Date.parse(detail?.startedAt || '');
293
+ const finishedMs = Date.parse(detail?.finishedAt || '');
294
+ // The completion carries the result itself — exit code, duration and the
295
+ // output (whole when short, else a summary with the log path) — so the
296
+ // agent never needs a follow-up read for it.
237
297
  const body = renderShellCompletionEnvelope({
238
298
  jobId,
239
299
  status: taskStatus,
240
300
  exitCode,
241
301
  command: detail?.command,
242
- summary: detail?.summary || failureCause || null,
243
- stdoutPreview: detail?.stdoutPreview,
244
- stderrPreview: detail?.stderrPreview,
245
- mergeStderr: detail?.mergeStderr,
246
302
  outputFile: detail?.stdoutPath,
247
303
  error: failureCause,
304
+ result: [
305
+ [
306
+ exitCode !== null ? `exit code: ${exitCode}` : null,
307
+ Number.isFinite(startedMs) && Number.isFinite(finishedMs)
308
+ ? `duration: ${formatDuration(finishedMs - startedMs)}`
309
+ : null,
310
+ ]
311
+ .filter(Boolean)
312
+ .join(', ') || null,
313
+ output ?? readShellTaskOutput({ status: 'completed' }, completionStreams(detail), { terminal: true }),
314
+ ]
315
+ .filter(Boolean)
316
+ .join('\n'),
248
317
  });
249
318
  return {
250
319
  taskStatus,
251
320
  body,
252
- notification: renderShellCompletionNotice({
253
- jobId,
254
- status: taskStatus,
255
- exitCode,
256
- command: detail?.command,
257
- outputFile: detail?.stdoutPath,
258
- error: failureCause,
259
- }),
321
+ notification: body,
260
322
  instruction: shellCompletionInstruction({ jobId, status, exitCode }),
261
323
  result: shellJobPublicTaskResult(reported),
262
324
  error: failureCause,
@@ -343,6 +405,15 @@ export function killShellJob(jobId) {
343
405
  return { ...attachJobInsights(normalizeShellJobDetail(detail)), killed: wasRunning };
344
406
  }
345
407
 
408
+ /** True once every process of a tracked background job is confirmed gone,
409
+ * false while any is still alive when `timeoutMs` passes. A job without a
410
+ * process tracker (or one already finished) has nothing left to confirm. */
411
+ export async function waitShellJobTerminated(jobId, timeoutMs) {
412
+ const entry = backgroundTrackers.get(String(jobId || ''));
413
+ if (!entry) return true;
414
+ return withinBound(entry.terminated, timeoutMs, false);
415
+ }
416
+
346
417
  export function clearShellJobNotifyCtx(jobId) {
347
418
  jobNotifyCtxByJobId.delete(jobId);
348
419
  }
@@ -383,7 +454,14 @@ export function watchBackgroundShellJob(jobId, notifyCtx) {
383
454
  backgroundShellJobWatchers.delete(jobId);
384
455
  jobNotifyCtxByJobId.delete(jobId);
385
456
  const enriched = attachJobInsights(normalizeShellJobDetail(detail));
386
- const completion = buildShellCompletion(jobId, enriched);
457
+ // The notice carries only what the agent has not seen yet: the task's
458
+ // cursor remainder. Kept on the task so a wait/read that reaches the
459
+ // finished task first returns that same remainder (and suppresses the
460
+ // notice) instead of an empty read.
461
+ const task = getBackgroundTask(jobId);
462
+ const output = task ? readShellTaskOutput(task, completionStreams(enriched), { terminal: true }) : undefined;
463
+ if (task) task.completionOutput = output;
464
+ const completion = buildShellCompletion(jobId, enriched, { output });
387
465
  const completedTask = completeBackgroundTask(jobId, {
388
466
  status: completion.taskStatus,
389
467
  result: completion.result,
@@ -471,7 +549,7 @@ export async function promoteForegroundShellJob({
471
549
  }
472
550
  // Added before tracking: the record's settlement removes it again.
473
551
  promotedJobIds.add(native.jobId);
474
- trackShellJobRecord(native, { ownerSessionId, clientHostPid, startedAtMs: started });
552
+ trackShellJobRecord(native, { ownerSessionId, clientHostPid, startedAtMs: started, timeoutMs });
475
553
  return native;
476
554
  }
477
555
 
@@ -593,18 +671,26 @@ export async function shutdownShellJobs(_reason = 'runtime-close', { scope = nul
593
671
  (task) => task.status === 'running' && (!scoped || String(task.ownerSessionId || '') === ownerSessionId)
594
672
  );
595
673
  let cancelledWatchers = 0;
596
- for (const task of jobs) {
597
- if (cancelBackgroundShellJobWatch(task.jobId)) cancelledWatchers += 1;
598
- try {
599
- cancelNativeTask(task.jobId);
600
- } catch {}
601
- jobNotifyCtxByJobId.delete(task.jobId);
602
- }
674
+ // Identities first, as every other kill does: once a root dies, what it
675
+ // started outside its process group is reparented and can no longer be
676
+ // found. The cancel is sent at the snapshot bound regardless.
677
+ await killAfterSnapshot(Promise.all(prepareOwnedShellTreeKills(scoped ? ownerSessionId : null)), () => {
678
+ for (const task of jobs) {
679
+ if (cancelBackgroundShellJobWatch(task.jobId)) cancelledWatchers += 1;
680
+ try {
681
+ cancelNativeTask(task.jobId);
682
+ } catch {}
683
+ jobNotifyCtxByJobId.delete(task.jobId);
684
+ }
685
+ });
603
686
  const settled = await Promise.all(jobs.map((task) => waitNativeTask(task.jobId, 1_200).catch(() => null)));
604
687
  // A process still running after the wait is not forgotten: it keeps being
605
688
  // stopped until its exit is confirmed.
606
689
  const unconfirmed = jobs.filter((_, index) => settled[index]?.status === 'running').map((task) => task.jobId);
607
690
  for (const jobId of unconfirmed) retryShutdownSurvivor(jobId, 1);
691
+ // The roots are gone; what they started outside their group is swept by
692
+ // each job's termination watch. Shutdown waits for that, bounded.
693
+ const treesGone = await Promise.all(jobs.map((task) => waitShellJobTerminated(task.jobId, SHUTDOWN_TREE_CONFIRM_MS)));
608
694
  if (scoped) {
609
695
  // The owning session is gone: nothing can query its tasks again, so
610
696
  // drop ALL of its retained state — not only the ones that happened to
@@ -624,6 +710,7 @@ export async function shutdownShellJobs(_reason = 'runtime-close', { scope = nul
624
710
  cancelledJobs: jobs.length,
625
711
  confirmedJobs: settled.filter((task) => task && task.status !== 'running').length,
626
712
  unconfirmedJobs: unconfirmed,
713
+ treeUnconfirmedJobs: jobs.filter((_, index) => !treesGone[index]).map((task) => task.jobId),
627
714
  cancelledWatchers,
628
715
  };
629
716
  }
@@ -213,6 +213,11 @@ function buildSuccessSummary(command, stdout) {
213
213
  return null;
214
214
  }
215
215
 
216
+ // A line redrawn with `\r` shows only its last non-empty frame.
217
+ function lastVisibleFrame(frames) {
218
+ return [...frames].reverse().find((frame) => frame !== '') ?? '';
219
+ }
220
+
216
221
  export function foldCarriageReturnFrames(value) {
217
222
  const text = String(value ?? '');
218
223
  if (!/\r(?!\n)/.test(text)) return null;
@@ -223,12 +228,32 @@ export function foldCarriageReturnFrames(value) {
223
228
  const frames = line.split('\r');
224
229
  if (frames.length === 1) return line;
225
230
  omitted += frames.length - 1;
226
- return [...frames].reverse().find((frame) => frame !== '') ?? '';
231
+ return lastVisibleFrame(frames);
227
232
  });
228
233
  if (!omitted) return null;
229
234
  return `${out.join('\n')}\n... [${omitted} overwritten progress frames omitted]`;
230
235
  }
231
236
 
237
+ /** The same `\r` folding for output that arrives in chunks: the line still
238
+ * open at a chunk's end keeps its state, so a progress bar redrawn across
239
+ * deliveries shows its latest frame once, while plain continuation text of
240
+ * that line is passed through as is. */
241
+ export function createCarriageReturnFolder() {
242
+ let open = '';
243
+ return function fold(chunk) {
244
+ const lines = String(chunk ?? '')
245
+ .replace(/\r\n/g, '\n')
246
+ .split('\n');
247
+ const out = lines.map((line, index) => {
248
+ const redraw = line.includes('\r');
249
+ const visible = redraw ? lastVisibleFrame((index === 0 ? open + line : line).split('\r')) : line;
250
+ if (index === lines.length - 1) open = index === 0 && !redraw ? open + line : visible;
251
+ return visible;
252
+ });
253
+ return out.join('\n');
254
+ };
255
+ }
256
+
232
257
  function hasDiagnosticHazard(value) {
233
258
  return /\b(?:errors?|warnings?|failed|failure|panic|fatal|xpass|xfailed|cancelled|todo|skipped)\b/i.test(
234
259
  String(value ?? '')
@@ -1,5 +1,6 @@
1
1
  import { countSplitLines } from './path-utils.mjs';
2
2
  import { TOOL_OUTPUT_MAX_BYTES } from './tool-output-limit.mjs';
3
+ import { DELIVERY_FULL_CHARS, fitPieces, shellTextPiece } from './lib/shell-delivery.mjs';
3
4
 
4
5
  const SHELL_OUTPUT_MAX_BYTES = TOOL_OUTPUT_MAX_BYTES;
5
6
 
@@ -7,11 +8,6 @@ export const SMART_BASH_MAX_LINES = 400;
7
8
  export const SMART_BASH_MAX_BYTES = TOOL_OUTPUT_MAX_BYTES;
8
9
  export const SMART_BASH_HEAD_LINES = 80;
9
10
  export const SMART_BASH_TAIL_LINES = 80;
10
- export const BACKGROUND_PARTIAL_OUTPUT_MAX_BYTES = 10 * 1024;
11
- const BACKGROUND_PARTIAL_OUTPUT_HEAD_BYTES = 2 * 1024;
12
-
13
- const BACKGROUND_PARTIAL_TRUNCATION_MARKER = '\n\n... [partial output truncated; head and tail shown] ...\n\n';
14
-
15
11
  function utf8Prefix(value, maxBytes) {
16
12
  const buffer = Buffer.from(String(value ?? ''), 'utf8');
17
13
  if (buffer.length <= maxBytes) return buffer.toString('utf8');
@@ -20,31 +16,20 @@ function utf8Prefix(value, maxBytes) {
20
16
  return buffer.subarray(0, end).toString('utf8');
21
17
  }
22
18
 
23
- function utf8Suffix(value, maxBytes) {
24
- const buffer = Buffer.from(String(value ?? ''), 'utf8');
25
- if (buffer.length <= maxBytes) return buffer.toString('utf8');
26
- let start = buffer.length - maxBytes;
27
- while (start < buffer.length && (buffer[start] & 0xc0) === 0x80) start += 1;
28
- return buffer.subarray(start).toString('utf8');
29
- }
30
-
19
+ // Partial output of a promoted command that has no task record, in the shared
20
+ // delivery shape (lib/shell-delivery.mjs).
31
21
  export function renderBackgroundPartialOutput(stdout, stderr) {
32
- const sections = [];
33
- const out = String(stdout ?? '');
34
- const err = String(stderr ?? '');
35
- if (out) sections.push(`[partial stdout]\n${out}`);
36
- if (err) sections.push(`[partial stderr]\n${err}`);
37
- const merged = sections.join('\n\n');
38
- if (!merged) return '';
39
- if (Buffer.byteLength(merged, 'utf8') <= BACKGROUND_PARTIAL_OUTPUT_MAX_BYTES) return merged;
40
-
41
- const markerBytes = Buffer.byteLength(BACKGROUND_PARTIAL_TRUNCATION_MARKER, 'utf8');
42
- const tailBytes = BACKGROUND_PARTIAL_OUTPUT_MAX_BYTES - BACKGROUND_PARTIAL_OUTPUT_HEAD_BYTES - markerBytes;
43
- return (
44
- utf8Prefix(merged, BACKGROUND_PARTIAL_OUTPUT_HEAD_BYTES) +
45
- BACKGROUND_PARTIAL_TRUNCATION_MARKER +
46
- utf8Suffix(merged, tailBytes)
22
+ const streams = [
23
+ ['stdout', String(stdout ?? '')],
24
+ ['stderr', String(stderr ?? '')],
25
+ ].filter(([, text]) => text);
26
+ const headings = streams.map(([label]) => `[partial ${label}]\n`);
27
+ const fixed = headings.reduce((sum, heading) => sum + heading.length, 2 * Math.max(0, streams.length - 1));
28
+ const bodies = fitPieces(
29
+ streams.map(([label, text]) => shellTextPiece(text, { label })),
30
+ DELIVERY_FULL_CHARS - fixed
47
31
  );
32
+ return streams.map((_, index) => `${headings[index]}${bodies[index]}`).join('\n\n');
48
33
  }
49
34
 
50
35
  export function smartMiddleTruncate(content) {
@@ -9,10 +9,13 @@ import {
9
9
  shellJobPublicTaskResult,
10
10
  shellJobTaskStatus,
11
11
  waitForShellJob,
12
+ waitShellJobTerminated,
12
13
  } from './shell-jobs.mjs';
14
+ import { beginSessionTaskWait } from '../../session/manager/runtime-liveness.mjs';
13
15
  import { TASK_WAIT_TIMEOUT_DEFAULT_MS, TASK_WAIT_TIMEOUT_MAX_MS, TASK_WAIT_TIMEOUT_MIN_MS } from './builtin-tools.mjs';
16
+ import { AGENT_TASK_WAIT_DEFAULT_MS } from './wait-budgets.mjs';
14
17
  import { getAbortSignalForSession } from '../../session/abort-lookup.mjs';
15
- import { beginInterruptibleTaskWait } from '../../session/task-wait-control.mjs';
18
+ import { TASK_WAIT_WAKE_NOTIFICATION, beginInterruptibleTaskWait } from '../../session/task-wait-control.mjs';
16
19
  import {
17
20
  acknowledgeBackgroundTaskCompletion,
18
21
  cancelBackgroundTask,
@@ -31,6 +34,18 @@ import { readShellTaskOutput } from './lib/shell-task-output.mjs';
31
34
  // wait returns the moment the native task settles, so this only bounds a
32
35
  // process that ignores the kill.
33
36
  const TASK_CANCEL_CONFIRM_MS = 5_000;
37
+ // Ceiling for confirming that every process the task started is gone once its
38
+ // root exited: a sweep is a table read, the kill and a second read (each read
39
+ // ~1-2 s on Windows).
40
+ const TASK_CANCEL_TREE_CONFIRM_MS = 15_000;
41
+
42
+ function cancelPendingText(taskId) {
43
+ return [
44
+ 'status: cancel-pending',
45
+ `task_id: ${taskId}`,
46
+ 'the kill was sent and the command itself exited, but processes it started are still alive. They keep being terminated and stay tracked (the folder stays held) until they are gone; re-run task cancel or task read to check again.',
47
+ ].join('\n');
48
+ }
34
49
 
35
50
  function recoveredTaskMatches(record, options = {}) {
36
51
  const context =
@@ -84,12 +99,22 @@ function renderTaskCancelSuccess(taskId, task) {
84
99
 
85
100
  // Clamp lives next to the caller; the bounds themselves are published by the
86
101
  // tool schema, so an out-of-range request is corrected rather than rejected.
87
- function resolveTaskWaitTimeoutMs(value) {
102
+ function resolveTaskWaitTimeoutMs(value, defaultMs = TASK_WAIT_TIMEOUT_DEFAULT_MS) {
88
103
  const raw = Number(value);
89
- if (!Number.isFinite(raw) || raw <= 0) return TASK_WAIT_TIMEOUT_DEFAULT_MS;
104
+ if (!Number.isFinite(raw) || raw <= 0) return defaultMs;
90
105
  return Math.min(TASK_WAIT_TIMEOUT_MAX_MS, Math.max(TASK_WAIT_TIMEOUT_MIN_MS, Math.floor(raw)));
91
106
  }
92
107
 
108
+ // Agents run on the 5 m prompt cache, so their default wakes before it lapses;
109
+ // the Lead keeps the 1 h cache and the longer default.
110
+ async function defaultTaskWaitMs(sessionId) {
111
+ if (!sessionId) return TASK_WAIT_TIMEOUT_DEFAULT_MS;
112
+ const { getSession } = await import('../../session/manager.mjs');
113
+ const session = getSession(String(sessionId));
114
+ const agentOwned = session?.owner === 'agent' || session?.visibility === 'agent-only';
115
+ return agentOwned ? AGENT_TASK_WAIT_DEFAULT_MS : TASK_WAIT_TIMEOUT_DEFAULT_MS;
116
+ }
117
+
93
118
  // Read-only counts of this runtime's shell work owned by OTHER sessions.
94
119
  // Admission is runtime-wide while task list/cancel are session-scoped, so
95
120
  // without this those holders are invisible. Counts only: another session's
@@ -128,8 +153,15 @@ async function listTasks(options) {
128
153
  return sections.join('\n');
129
154
  }
130
155
 
131
- // Blocks until the shell task settles, the timeout elapses, or the turn/user
132
- // interrupts; returns whether new user input cut the wait short.
156
+ // Ceiling hits per still-running task, so a wait that keeps timing out on the
157
+ // same job can suggest a different move. Keyed by the task object, so the count
158
+ // goes with the task however it settles or is removed.
159
+ const waitCeilingHits = new WeakMap();
160
+
161
+ // Blocks until the shell task settles, the timeout elapses, the turn is
162
+ // cancelled, or anything deliverable is queued for this session (user input,
163
+ // steering, or any task notification: an agent or another shell job
164
+ // finishing). Returns what cut the wait short.
133
165
  async function waitForShellTask(task, taskId, args, options) {
134
166
  // The turn's abort signal must cut the wait short; a cancelled turn
135
167
  // cannot sit out the remaining ceiling.
@@ -140,9 +172,12 @@ async function waitForShellTask(task, taskId, args, options) {
140
172
  turnSignal = null;
141
173
  }
142
174
  const waitControl = beginInterruptibleTaskWait(options?.sessionId, turnSignal);
175
+ const releaseWaitState = beginSessionTaskWait(options?.sessionId);
143
176
  const waitSignal = waitControl.signal;
144
- const waitTimeoutMs = resolveTaskWaitTimeoutMs(args.timeout_ms);
177
+ const waitTimeoutMs = resolveTaskWaitTimeoutMs(args.timeout_ms, await defaultTaskWaitMs(options?.sessionId));
145
178
  let interruptedByUser = false;
179
+ let notified = false;
180
+ let notifiedTaskId = null;
146
181
  try {
147
182
  // A shell task whose descendants outlived the shell process has no
148
183
  // native job to subscribe to — the process it belonged to is gone.
@@ -175,9 +210,12 @@ async function waitForShellTask(task, taskId, args, options) {
175
210
  }
176
211
  } finally {
177
212
  interruptedByUser = waitControl.interruptedByUser;
213
+ notified = waitControl.wakeReason === TASK_WAIT_WAKE_NOTIFICATION;
214
+ notifiedTaskId = waitControl.wakeDetail;
178
215
  waitControl.dispose();
216
+ releaseWaitState();
179
217
  }
180
- return interruptedByUser;
218
+ return { interruptedByUser, notified, notifiedTaskId };
181
219
  }
182
220
 
183
221
  // Outcome of a shell cancel: `outcome` is 'cancelled' only when the process is
@@ -208,6 +246,11 @@ async function cancelShellTaskOutcome(task, taskId, options) {
208
246
  ].join('\n'),
209
247
  };
210
248
  }
249
+ // The root exited; what it started outside its process group is swept
250
+ // by the job's termination watch. Cancelled means all of it is gone.
251
+ if (confirmed && !(await waitShellJobTerminated(taskId, TASK_CANCEL_TREE_CONFIRM_MS))) {
252
+ return { outcome: 'unconfirmed', text: cancelPendingText(taskId) };
253
+ }
211
254
  if (!confirmed) {
212
255
  // No terminal event and no observable task: absence of
213
256
  // state is not proof of termination. Stop tracking it so it
@@ -228,6 +271,10 @@ async function cancelShellTaskOutcome(task, taskId, options) {
228
271
  cancelBackgroundShellJobWatch(taskId);
229
272
  clearShellJobNotifyCtx(taskId);
230
273
  cancelBackgroundTask(taskId, 'cancelled by task control');
274
+ // A task whose own cancel hook kills (what a finished command left running)
275
+ // reports what that kill confirmed.
276
+ const hookKill = job ? null : await task.cancelOutcome;
277
+ if (hookKill && hookKill.terminated !== true) return { outcome: 'unconfirmed', text: cancelPendingText(taskId) };
231
278
  return job || wasRunning
232
279
  ? {
233
280
  outcome: 'cancelled',
@@ -323,27 +370,59 @@ export async function executeTaskTool(args, options = {}) {
323
370
 
324
371
  if (action === 'read' || action === 'wait') {
325
372
  let waitInterruptedByUser = false;
373
+ let waitNotified = false;
374
+ let notifiedTaskId = null;
326
375
  if (action === 'wait') {
327
376
  if (!isShellTask) return `Error: task wait is only available for shell tasks: ${taskId}`;
328
- waitInterruptedByUser = await waitForShellTask(task, taskId, args, options);
377
+ ({
378
+ interruptedByUser: waitInterruptedByUser,
379
+ notified: waitNotified,
380
+ notifiedTaskId,
381
+ } = await waitForShellTask(task, taskId, args, options));
329
382
  }
330
383
  if (isShellTask) refreshShellTask(taskId, { includeRunning: true });
331
384
  const latest = getBackgroundTask(taskId, { context: options }) || task;
385
+ // A finished task whose completion notice is still unconsumed returns that
386
+ // notice's output (the unseen remainder) once; everything else reads the
387
+ // cursor. A wait that leaves the job running carries the progress digest.
388
+ const pendingCompletionOutput =
389
+ isShellTask &&
390
+ latest.status !== 'running' &&
391
+ latest.completionAcknowledged !== true &&
392
+ args.output !== 'tail' &&
393
+ typeof latest.completionOutput === 'string'
394
+ ? latest.completionOutput
395
+ : null;
332
396
  const visible =
333
397
  isShellTask && latest.result && typeof latest.result === 'object'
334
- ? { ...latest, resultText: readShellTaskOutput(latest, latest.result, { output: args.output }) }
398
+ ? {
399
+ ...latest,
400
+ resultText:
401
+ pendingCompletionOutput ??
402
+ readShellTaskOutput(latest, latest.result, {
403
+ output: args.output,
404
+ digest: action === 'wait' && latest.status === 'running',
405
+ }),
406
+ }
335
407
  : latest;
336
408
  let rendered = renderBackgroundTask(visible, { includeResult: true });
337
409
  if (action === 'wait' && latest.status === 'running') {
338
410
  if (waitInterruptedByUser) {
339
411
  rendered +=
340
- '\n\nWait interrupted by new user input; task still running. Handle the new message now and reply to it before waiting on this task again.';
412
+ '\n\nWait ended by new user input; task still running in the background. Handle the new message first.';
413
+ } else if (waitNotified) {
414
+ const from = notifiedTaskId && notifiedTaskId !== taskId ? ` (task_id: ${notifiedTaskId})` : '';
415
+ rendered += `\n\nWait ended because a notification arrived${from}; it is delivered to you now. This task (${taskId}) is still running in the background.`;
341
416
  } else {
417
+ const hits = (waitCeilingHits.get(latest) || 0) + 1;
418
+ waitCeilingHits.set(latest, hits);
342
419
  // In-band report cue: the static "periodic reports" rule does not
343
420
  // survive a long tool loop, so a ceiling-hit wait carries the
344
421
  // protocol reminder itself.
345
422
  rendered +=
346
- '\n\nWait ceiling reached; task still running. If periodic reports were requested, write the user-facing report now, then call task wait for the next interval; otherwise continue independent work or end the turn.';
423
+ hits > 1
424
+ ? `\n\nWait ceiling reached again (${hits} times); task still running. Check whether it is progressing (task read output:tail, or the log file) and cancel it if it is stuck; otherwise continue other work — its result is delivered automatically.`
425
+ : '\n\nWait ceiling reached; task still running and its result is delivered automatically. If periodic reports were requested, write the user-facing report now; otherwise continue independent work or end the turn.';
347
426
  }
348
427
  }
349
428
  if (acknowledgeBackgroundTaskCompletion(taskId, { context: options })) {
@@ -0,0 +1,23 @@
1
+ // The one place the shell hold and the task wait budgets are set. Every
2
+ // schema, clamp, description and result text reads them from here.
3
+ //
4
+ // Shell hold: how long a foreground command may hold the tool call before it
5
+ // moves to the background (it keeps running; completion is delivered
6
+ // automatically). New user input ends the hold early. `wait_ms` picks a
7
+ // different hold for one call, clamped to MIN..MAX.
8
+ export const SHELL_HOLD_DEFAULT_MS = 60_000;
9
+ export const SHELL_HOLD_MIN_MS = 1_000;
10
+ export const SHELL_HOLD_MAX_MS = 600_000;
11
+
12
+ // `task wait` ceiling. The wait returns the moment the task settles or new
13
+ // user input arrives, so the ceiling only bounds a still-running task. The
14
+ // floor stops a caller from turning wait into a busy poll. An agent's default
15
+ // stays under the 5 m prompt-cache TTL with room for the turn's own generation
16
+ // time, so a wait that runs to the ceiling wakes to a warm cache.
17
+ export const TASK_WAIT_DEFAULT_MS = 300_000;
18
+ export const AGENT_TASK_WAIT_DEFAULT_MS = 240_000;
19
+ export const TASK_WAIT_MIN_MS = 10_000;
20
+ export const TASK_WAIT_MAX_MS = 3_600_000;
21
+
22
+ /** Whole seconds for descriptions and result text. */
23
+ export const budgetSeconds = (ms) => Math.round(ms / 1000);