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
@@ -1,67 +1,393 @@
1
1
  /**
2
- * Off-thread parse + projection of a large stored transcript.
2
+ * Off-thread read + parse + projection of a stored transcript.
3
3
  *
4
- * The strict record parse and the transcript projection of a multi-megabyte
5
- * session took 0.4-3s of daemon event-loop time (`slow stored projection`).
6
- * This module runs exactly that pure pipeline (readTopLevelLifecycleRecord →
7
- * projectStoredTranscript with no turn checkpoint) on a lazily started worker
8
- * thread and hands back the same projection value. The worker loads its own
9
- * module graph; only the finished projection crosses back.
4
+ * A cold pane read used to cost the daemon event loop the whole pipeline: a
5
+ * chunked async read whose every 512 KB chunk waited for a loop turn, a
6
+ * SHA-256 of the text, the strict record parse and a projection that meters
7
+ * every message of the transcript (`slow stored projection`). Under a busy
8
+ * loop each read stretched to seconds. This worker runs exactly that pipeline
9
+ * (stat → read → stat, digest, readTopLevelLifecycleRecord →
10
+ * projectStoredTranscript with no turn checkpoint) and hands back the
11
+ * digest and the same projection value; only the finished window crosses
12
+ * back.
13
+ *
14
+ * Inside the worker, one projection base (recovery, prepared session and
15
+ * whole-transcript context gauge) per session content is shared by every
16
+ * window and every concurrent request of it, and a new version of a session
17
+ * reuses the parsed messages its previous version already metered wherever
18
+ * they are unchanged, so an appended turn re-meters only the new tail.
19
+ * Bodies read but not yet built are bounded (`createBaseBuilds`): room is
20
+ * reserved before a record is read, and a newer version of a session
21
+ * replaces its queued older build.
10
22
  *
11
23
  * Client (main thread): `projectStoredTranscriptOffThread`. Worker side is
12
24
  * active only when this file is loaded as a worker of that client.
13
25
  */
26
+ import { availableParallelism } from 'node:os';
14
27
  import { isMainThread, workerData } from 'node:worker_threads';
15
28
  import { createWorkerRequestClient, serveWorkerRequests } from '../../../shared/worker-requests.mjs';
16
29
 
17
- /** Records at least this many characters go to the worker; smaller ones cost
18
- * less on the main thread than the round trip. */
19
- export const OFFLOAD_MIN_CHARS = 1_000_000;
30
+ /** Records of at least this many bytes go to the worker. Smaller ones cost
31
+ * only a few ms here, and their parse can then be handed to the resume that
32
+ * follows a pane read. */
33
+ export const OFFLOAD_MIN_CHARS = 256 * 1024;
20
34
  const WORKER_KIND = 'stored-transcript-projection';
21
35
  const IDLE_EXIT_MS = 120_000;
36
+ // A restore burst projects every pane at once; two threads halve its tail.
37
+ // A session always goes to the same worker, which holds its base.
38
+ const POOL_SIZE = Math.max(1, Math.min(2, availableParallelism() - 1));
22
39
 
23
- let client = null;
40
+ const clients = [];
24
41
 
25
- /**
26
- * Parse and project `text` for `sessionId`. Resolves `{ unreadable: true }`
27
- * when the record is not a strict, identity-matching session record, else
28
- * `{ value, lifecycle }` where `lifecycle` is `{ id, closed, generation }`.
29
- * Rejects when the worker cannot answer; callers fall back to the main thread.
30
- */
31
- export function projectStoredTranscriptOffThread({ sessionId, text, itemLimit }) {
32
- client ||= createWorkerRequestClient(new URL(import.meta.url), {
42
+ function clientFor(sessionId) {
43
+ let hash = 0;
44
+ for (let index = 0; index < sessionId.length; index += 1) hash = (hash * 31 + sessionId.charCodeAt(index)) >>> 0;
45
+ const slot = hash % POOL_SIZE;
46
+ clients[slot] ||= createWorkerRequestClient(new URL(import.meta.url), {
33
47
  idleExitMs: IDLE_EXIT_MS,
34
48
  workerData: { kind: WORKER_KIND },
35
49
  });
36
- return client({ sessionId, text, itemLimit });
50
+ return clients[slot];
37
51
  }
38
52
 
39
- /** Whether a projection worker is attached (a request now skips its start). */
40
- export function storedTranscriptWorkerRunning() {
41
- return client?.running() === true;
53
+ /**
54
+ * Read, digest and project the record at `recordPath` for `sessionId`.
55
+ * Resolves `{ state, timing }` with state absent/unreadable when the file
56
+ * cannot be read; otherwise `{ state, textChars, textHash, stamp, timing }`
57
+ * plus `same: true` when the content is `known` ({ textChars, textHash }),
58
+ * `unreadable: true` when it is not a strict, identity-matching session
59
+ * record, or `{ value, lifecycle }` (`{ id, closed, generation }`). `stamp` is
60
+ * the full file stamp the bytes provably came from, or null. Rejects when the
61
+ * worker cannot answer; callers then run the pipeline on their own thread.
62
+ */
63
+ export function projectStoredTranscriptOffThread({ sessionId, recordPath, itemLimit, known = null }) {
64
+ return clientFor(sessionId)({ sessionId, recordPath, itemLimit, known });
42
65
  }
43
66
 
44
- async function project({ sessionId, text, itemLimit }) {
45
- const importStartedAt = performance.now();
46
- const [{ readTopLevelLifecycleRecord, isLifecycleUnreadable }, { projectStoredTranscript }] = await Promise.all([
67
+ /** Whether the projection worker of `sessionId` is attached (a request now
68
+ * skips its start). */
69
+ export function storedTranscriptWorkerRunning(sessionId) {
70
+ return clientFor(sessionId).running() === true;
71
+ }
72
+
73
+ /** Drop this session's projection base and queued builds in its worker (it
74
+ * went live). Never starts a worker just to forget. */
75
+ export function forgetStoredTranscriptOffThread(sessionId) {
76
+ const request = clientFor(sessionId);
77
+ if (request.running() === true) request({ forget: sessionId }).catch(() => {});
78
+ }
79
+
80
+ // ── Worker side ─────────────────────────────────────────────────────────────
81
+
82
+ /** The answer a superseded or forgotten queued build gives its waiters: read
83
+ * the record again (it now holds that newer content, or none). */
84
+ export const RESTART_READ = Symbol('restart-read');
85
+
86
+ // Source characters of records read but not yet built: reserved from the
87
+ // file size BEFORE a record is read and held until its build settles, it
88
+ // joins another build, or it is dropped. A record alone larger than this is
89
+ // admitted only when nothing else is reserved.
90
+ const BUILD_CHARS_LIMIT = 16 * 1024 * 1024;
91
+ // Source characters of the retained bases (their parsed records and memos
92
+ // stay alive with them). The newest base is kept even when it alone exceeds
93
+ // this; the worker's idle exit releases everything.
94
+ const BASE_CHARS_LIMIT = 16 * 1024 * 1024;
95
+
96
+ /**
97
+ * Admission, build queue and retained bases of one worker.
98
+ * `build(sessionId, text, previousMessages, timing)` builds a base; it runs
99
+ * one at a time, in arrival order (metering yields between slices, so
100
+ * concurrent builds would share the thread round-robin and every pane of a
101
+ * restore burst would get its answer only at the end).
102
+ *
103
+ * A caller `admit`s the record's size before reading it, then `submit`s the
104
+ * text with its ticket (or `release`s it): retained or in-progress builds of
105
+ * the same content are shared; a newer version of a session replaces its
106
+ * queued older build, whose waiters get RESTART_READ. The previous version's
107
+ * messages are looked up when a build starts, so no queued build keeps an
108
+ * evicted or replaced base alive.
109
+ */
110
+ export function createBaseBuilds({ build, buildCharsLimit = BUILD_CHARS_LIMIT, baseCharsLimit = BASE_CHARS_LIMIT }) {
111
+ /** sessionId -> { textHash, chars, built } (Map order is LRU). */
112
+ const bases = new Map();
113
+ let baseChars = 0;
114
+ /** Unstarted builds: { sessionId, text, textHash, chars, ticket, timing, waiters, retain }. */
115
+ const queue = [];
116
+ let running = null;
117
+ let draining = false;
118
+ /** Waiting for room: { chars, resolve } in arrival order. */
119
+ const admissions = [];
120
+ let reservedChars = 0;
121
+ let peakReservedChars = 0;
122
+ let builds = 0;
123
+
124
+ const dropBase = (sessionId) => {
125
+ const entry = bases.get(sessionId);
126
+ if (!entry) return;
127
+ bases.delete(sessionId);
128
+ baseChars -= entry.chars;
129
+ };
130
+
131
+ const retainBase = (sessionId, entry) => {
132
+ dropBase(sessionId);
133
+ bases.set(sessionId, entry);
134
+ baseChars += entry.chars;
135
+ while (baseChars > baseCharsLimit && bases.size > 1) dropBase(bases.keys().next().value);
136
+ };
137
+
138
+ const reserve = (ticket, chars) => {
139
+ reservedChars += chars - ticket.chars;
140
+ ticket.chars = chars;
141
+ peakReservedChars = Math.max(peakReservedChars, reservedChars);
142
+ };
143
+
144
+ const pump = () => {
145
+ while (admissions.length > 0 && (reservedChars === 0 || reservedChars + admissions[0].chars <= buildCharsLimit)) {
146
+ const { chars, resolve } = admissions.shift();
147
+ const ticket = { chars: 0, released: false };
148
+ reserve(ticket, chars);
149
+ resolve(ticket);
150
+ }
151
+ };
152
+
153
+ const release = (ticket) => {
154
+ if (!ticket || ticket.released) return;
155
+ ticket.released = true;
156
+ reservedChars -= ticket.chars;
157
+ ticket.chars = 0;
158
+ pump();
159
+ };
160
+
161
+ // An unstarted build nobody needs any more: its body and reservation go,
162
+ // its waiters read again.
163
+ const drop = (job) => {
164
+ queue.splice(queue.indexOf(job), 1);
165
+ job.text = null;
166
+ release(job.ticket);
167
+ for (const waiter of job.waiters.splice(0)) waiter.resolve(RESTART_READ);
168
+ };
169
+
170
+ async function drain() {
171
+ if (draining) return;
172
+ draining = true;
173
+ try {
174
+ while (queue.length > 0) {
175
+ const job = queue.shift();
176
+ running = job;
177
+ let outcome;
178
+ try {
179
+ const previous = bases.get(job.sessionId)?.built.messages ?? null;
180
+ outcome = { value: await build(job.sessionId, job.text, previous, job.timing) };
181
+ } catch (error) {
182
+ outcome = { error };
183
+ }
184
+ running = null;
185
+ job.text = null;
186
+ release(job.ticket);
187
+ builds += 1;
188
+ const { value, error } = outcome;
189
+ if (job.retain && value && !value.unreadable) {
190
+ retainBase(job.sessionId, { textHash: job.textHash, chars: job.chars, built: value });
191
+ }
192
+ for (const waiter of job.waiters.splice(0)) {
193
+ if (error) waiter.reject(error);
194
+ else waiter.resolve(value);
195
+ }
196
+ }
197
+ } finally {
198
+ draining = false;
199
+ }
200
+ }
201
+
202
+ return {
203
+ /** Resolves a ticket reserving `chars` once they fit the budget. */
204
+ admit(chars) {
205
+ const admitted = new Promise((resolve) => admissions.push({ chars: Math.max(0, chars || 0), resolve }));
206
+ pump();
207
+ return admitted;
208
+ },
209
+ /** The record read is `chars` long (its size was only an estimate). */
210
+ resize(ticket, chars) {
211
+ if (!ticket.released) reserve(ticket, chars);
212
+ },
213
+ release,
214
+ /** The base of this exact content (the ticket passes to the build or is
215
+ * released), or RESTART_READ. */
216
+ submit(sessionId, text, textHash, ticket, timing) {
217
+ const chars = text.length;
218
+ const retained = bases.get(sessionId);
219
+ if (retained && retained.textHash === textHash && retained.chars === chars) {
220
+ release(ticket);
221
+ retainBase(sessionId, retained);
222
+ timing.base = 'shared';
223
+ return Promise.resolve(retained.built);
224
+ }
225
+ const sameContent = (job) =>
226
+ !!job && job.sessionId === sessionId && job.textHash === textHash && job.chars === chars;
227
+ let job = sameContent(running) ? running : queue.find(sameContent);
228
+ if (job) {
229
+ release(ticket);
230
+ timing.base = 'shared';
231
+ } else {
232
+ for (const older of queue.filter((queued) => queued.sessionId === sessionId)) drop(older);
233
+ job = { sessionId, text, textHash, chars, ticket, timing, waiters: [], retain: true };
234
+ queue.push(job);
235
+ }
236
+ const answer = new Promise((resolve, reject) => job.waiters.push({ resolve, reject }));
237
+ void drain();
238
+ return answer;
239
+ },
240
+ /** The session went live: drop its base and queued builds; one already
241
+ * running answers its waiters but is not retained. */
242
+ forget(sessionId) {
243
+ dropBase(sessionId);
244
+ for (const queued of queue.filter((job) => job.sessionId === sessionId)) drop(queued);
245
+ if (running?.sessionId === sessionId) running.retain = false;
246
+ },
247
+ stats() {
248
+ return {
249
+ reservedChars,
250
+ peakReservedChars,
251
+ queuedBuilds: queue.length,
252
+ queuedChars: queue.reduce((sum, job) => sum + job.chars, 0),
253
+ waitingAdmissions: admissions.length,
254
+ builds,
255
+ retainedBases: bases.size,
256
+ baseChars,
257
+ };
258
+ },
259
+ };
260
+ }
261
+
262
+ /** Whether two JSON.parse results are the same value, key order included
263
+ * (estimators serialize them). */
264
+ export function sameJsonValue(left, right) {
265
+ if (left === right) return typeof left !== 'number' || Object.is(left, right);
266
+ if (!left || !right || typeof left !== 'object' || typeof right !== 'object') return false;
267
+ if (Array.isArray(left)) {
268
+ if (!Array.isArray(right) || left.length !== right.length) return false;
269
+ for (let index = 0; index < left.length; index += 1) {
270
+ if (!sameJsonValue(left[index], right[index])) return false;
271
+ }
272
+ return true;
273
+ }
274
+ if (Array.isArray(right)) return false;
275
+ const leftKeys = Object.keys(left);
276
+ const rightKeys = Object.keys(right);
277
+ if (leftKeys.length !== rightKeys.length) return false;
278
+ for (let index = 0; index < leftKeys.length; index += 1) {
279
+ const key = leftKeys[index];
280
+ if (rightKeys[index] !== key || !sameJsonValue(left[key], right[key])) return false;
281
+ }
282
+ return true;
283
+ }
284
+
285
+ /**
286
+ * Replace the leading messages of `next` that are the same value as those of
287
+ * `previous` with the previous objects, whose per-message meter memos the
288
+ * projection then reads instead of re-metering. Returns how many were reused.
289
+ * Projections never mutate messages, and the comparison is against each
290
+ * previous object as it is now, so the result is the projection of `next`.
291
+ */
292
+ export function reuseParsedPrefix(next, previous) {
293
+ if (!Array.isArray(next) || !Array.isArray(previous)) return 0;
294
+ const shared = Math.min(next.length, previous.length);
295
+ let index = 0;
296
+ while (index < shared && sameJsonValue(next[index], previous[index])) {
297
+ next[index] = previous[index];
298
+ index += 1;
299
+ }
300
+ return index;
301
+ }
302
+
303
+ async function buildBase(sessionId, text, previousMessages, timing) {
304
+ const [{ readTopLevelLifecycleRecord, isLifecycleUnreadable }, { prepareStoredTranscriptBase }] = await Promise.all([
47
305
  import('./lifecycle-scan.mjs'),
48
306
  import('./store-transcript-projection.mjs'),
49
307
  ]);
50
308
  const parseStartedAt = performance.now();
51
309
  const record = readTopLevelLifecycleRecord(text);
52
310
  if (isLifecycleUnreadable(record) || record.id !== sessionId) return { unreadable: true };
53
- const projectStartedAt = performance.now();
54
- const value = await projectStoredTranscript(sessionId, record.doc, {
55
- itemLimit,
56
- includeMessages: false,
57
- checkpointAbsent: true,
58
- });
59
- const timing = {
60
- importMs: parseStartedAt - importStartedAt,
61
- parseMs: projectStartedAt - parseStartedAt,
62
- projectMs: performance.now() - projectStartedAt,
311
+ const messages = record.doc.messages;
312
+ timing.reusedMessages = reuseParsedPrefix(messages, previousMessages);
313
+ timing.parseMs = performance.now() - parseStartedAt;
314
+ const base = await prepareStoredTranscriptBase(sessionId, record.doc, { checkpointAbsent: true });
315
+ return {
316
+ base,
317
+ messages,
318
+ lifecycle: { id: record.id, closed: record.closed, generation: record.generation },
63
319
  };
64
- return { value, lifecycle: { id: record.id, closed: record.closed, generation: record.generation }, timing };
65
320
  }
66
321
 
67
- if (!isMainThread && workerData?.kind === WORKER_KIND) serveWorkerRequests(project);
322
+ const baseBuilds = createBaseBuilds({ build: buildBase });
323
+
324
+ async function project({ sessionId, recordPath, itemLimit, known }) {
325
+ const importStartedAt = performance.now();
326
+ const [{ statSessionStamp, sameSessionStamp }, { readTextFile, PROBE_PRESENT }, { transcriptTextHash }, projection] =
327
+ await Promise.all([
328
+ import('./store/canonical-reader.mjs'),
329
+ import('./store/fs-probe.mjs'),
330
+ import('./store-transcript-cache.mjs'),
331
+ import('./store-transcript-projection.mjs'),
332
+ ]);
333
+ const timing = { importMs: performance.now() - importStartedAt };
334
+ const observe = () => {
335
+ try {
336
+ return statSessionStamp(recordPath);
337
+ } catch {
338
+ return null;
339
+ }
340
+ };
341
+ for (let restarts = 0; ; restarts += 1) {
342
+ if (restarts > 0) timing.restarts = restarts;
343
+ // Room for the body is reserved before it is read (bytes bound chars).
344
+ const sized = observe();
345
+ const admitStartedAt = performance.now();
346
+ const ticket = await baseBuilds.admit(sized ? Number(sized.size) : 0);
347
+ timing.admitMs = performance.now() - admitStartedAt;
348
+ let submitted = false;
349
+ try {
350
+ const readStartedAt = performance.now();
351
+ const before = observe();
352
+ let body = readTextFile(recordPath);
353
+ if (body.state !== PROBE_PRESENT) return { state: body.state, timing };
354
+ const stamp = before && sameSessionStamp(before, observe()) ? before : null;
355
+ let text = body.text;
356
+ body = null;
357
+ baseBuilds.resize(ticket, text.length);
358
+ const digest = {
359
+ state: PROBE_PRESENT,
360
+ textChars: text.length,
361
+ textHash: transcriptTextHash(text),
362
+ stamp,
363
+ timing,
364
+ };
365
+ timing.readMs = performance.now() - readStartedAt;
366
+ if (known && known.textHash === digest.textHash && known.textChars === digest.textChars) {
367
+ return { ...digest, same: true };
368
+ }
369
+ const projectStartedAt = performance.now();
370
+ submitted = true;
371
+ const pending = baseBuilds.submit(sessionId, text, digest.textHash, ticket, timing);
372
+ // Only the build may keep the body; a joined read lets go of its copy.
373
+ text = null;
374
+ const built = await pending;
375
+ if (built === RESTART_READ) continue;
376
+ if (built.unreadable) return { ...digest, unreadable: true };
377
+ const value = await projection.windowStoredTranscript(sessionId, built.base, {
378
+ itemLimit,
379
+ includeMessages: false,
380
+ });
381
+ timing.projectMs = performance.now() - projectStartedAt;
382
+ return { ...digest, value, lifecycle: built.lifecycle };
383
+ } finally {
384
+ if (!submitted) baseBuilds.release(ticket);
385
+ }
386
+ }
387
+ }
388
+
389
+ if (!isMainThread && workerData?.kind === WORKER_KIND) {
390
+ serveWorkerRequests((payload) => {
391
+ return payload?.forget ? baseBuilds.forget(payload.forget) : project(payload);
392
+ });
393
+ }
@@ -701,9 +701,16 @@ async function _doSave(payload) {
701
701
  // Durable lifecycle barriers (tombstone plant + detach generation bump) live
702
702
  // in store/lifecycle-barriers.mjs; re-exported so prior importers of
703
703
  // store.mjs stay unchanged.
704
- export { markSessionClosed, bumpSessionGeneration } from './store/lifecycle-barriers.mjs';
704
+ export {
705
+ markSessionClosed,
706
+ bumpSessionGeneration,
707
+ markSessionClosedAsync,
708
+ markSessionClosedAttempt,
709
+ bumpSessionGenerationAttempt,
710
+ isLifecycleRetry,
711
+ } from './store/lifecycle-barriers.mjs';
705
712
  // Session reads live in store/load-session.mjs, same facade contract.
706
- export { loadSession } from './store/load-session.mjs';
713
+ export { loadSession, loadSessionStatus } from './store/load-session.mjs';
707
714
 
708
715
  // Hard delete + owned-agent enumeration live in store/session-delete.mjs;
709
716
  // re-exported here so prior importers of store.mjs stay unchanged.
@@ -1,14 +1,21 @@
1
- // Per-session wake control for `task wait`.
1
+ // Per-session wake control for `task wait` and the foreground shell hold.
2
2
  //
3
3
  // This is intentionally separate from the turn AbortController: new user input
4
- // should release only the passive wait, let the tool return a running snapshot,
5
- // and then flow through the normal post-tool steering boundary. The background
6
- // task itself keeps running.
4
+ // or a queued task notification releases only the passive wait, lets the tool
5
+ // return a running snapshot, and then flows through the normal post-tool
6
+ // steering boundary. The background task itself keeps running.
7
7
  const waitersBySession = new Map();
8
8
  const steeringChecks = new Set();
9
9
 
10
+ // Why a wait was released early: user input/steering, or a deliverable task
11
+ // notification queued for the session.
12
+ export const TASK_WAIT_WAKE_USER = 'user-message';
13
+ export const TASK_WAIT_WAKE_NOTIFICATION = 'notification';
14
+
10
15
  // Scoped to an active turn. Read the live queue at wait entry rather than
11
16
  // retaining a wake signal that could outlive a consumed or reclaimed prompt.
17
+ // A check returns a wake reason (or true for user input) when something
18
+ // deliverable is already queued for the session, else a falsy value.
12
19
  export function registerTaskWaitSteeringCheck(check) {
13
20
  steeringChecks.add(check);
14
21
  return () => steeringChecks.delete(check);
@@ -32,6 +39,8 @@ export function beginInterruptibleTaskWait(sessionId, parentSignal = null) {
32
39
  const waiter = {
33
40
  controller,
34
41
  interruptedByUser: false,
42
+ wakeReason: null,
43
+ wakeDetail: null,
35
44
  };
36
45
  let waiters = waitersBySession.get(key);
37
46
  if (!waiters) {
@@ -53,9 +62,18 @@ export function beginInterruptibleTaskWait(sessionId, parentSignal = null) {
53
62
  }
54
63
  }
55
64
 
56
- if (!controller.signal.aborted && [...steeringChecks].some((check) => check(key))) {
57
- waiter.interruptedByUser = true;
58
- controller.abort('user-message');
65
+ if (!controller.signal.aborted) {
66
+ let queued = null;
67
+ for (const check of steeringChecks) {
68
+ const found = check(key);
69
+ if (!found) continue;
70
+ const reason = found === true ? TASK_WAIT_WAKE_USER : String(found);
71
+ // User input outranks a notification: its result text asks the model
72
+ // to handle the new message first.
73
+ if (!queued || reason === TASK_WAIT_WAKE_USER) queued = reason;
74
+ if (queued === TASK_WAIT_WAKE_USER) break;
75
+ }
76
+ if (queued) wakeWaiter(waiter, queued, null);
59
77
  }
60
78
 
61
79
  return {
@@ -63,6 +81,12 @@ export function beginInterruptibleTaskWait(sessionId, parentSignal = null) {
63
81
  get interruptedByUser() {
64
82
  return waiter.interruptedByUser;
65
83
  },
84
+ get wakeReason() {
85
+ return waiter.wakeReason;
86
+ },
87
+ get wakeDetail() {
88
+ return waiter.wakeDetail;
89
+ },
66
90
  dispose() {
67
91
  if (disposed) return;
68
92
  disposed = true;
@@ -77,17 +101,24 @@ export function beginInterruptibleTaskWait(sessionId, parentSignal = null) {
77
101
  };
78
102
  }
79
103
 
80
- export function interruptTaskWaitForSession(sessionId, reason = 'user-message') {
104
+ function wakeWaiter(waiter, reason, detail) {
105
+ waiter.interruptedByUser = reason !== TASK_WAIT_WAKE_NOTIFICATION;
106
+ waiter.wakeReason = reason;
107
+ waiter.wakeDetail = detail || null;
108
+ try {
109
+ waiter.controller.abort(reason);
110
+ } catch {}
111
+ }
112
+
113
+ // `detail` names what woke the wait (the notified task id) for its result text.
114
+ export function interruptTaskWaitForSession(sessionId, reason = TASK_WAIT_WAKE_USER, detail = null) {
81
115
  const waiters = waitersBySession.get(sessionKey(sessionId));
82
116
  if (!waiters?.size) return 0;
83
117
  let interrupted = 0;
84
118
  for (const waiter of [...waiters]) {
85
119
  if (waiter.controller.signal.aborted) continue;
86
- waiter.interruptedByUser = true;
87
120
  interrupted += 1;
88
- try {
89
- waiter.controller.abort(reason);
90
- } catch {}
121
+ wakeWaiter(waiter, reason, detail);
91
122
  }
92
123
  return interrupted;
93
124
  }
@@ -19,11 +19,12 @@ import { getPluginData } from '../config.mjs';
19
19
  import { countSplitLines, normalizeOutputPath } from '../tools/builtin/path-utils.mjs';
20
20
  import { classifyResultKind } from './result-classification.mjs';
21
21
  import { registerSessionPurgeHook } from './store.mjs';
22
- import { SETUP_OUTPUT_MAX_BYTES } from '../../../shared/tool-output-limit.mjs';
22
+ import { SETUP_OUTPUT_MAX_BYTES, capMarker } from '../../../shared/tool-output-limit.mjs';
23
23
 
24
24
  const TOOL_RESULT_OFFLOAD_THRESHOLD_CHARS = 50_000;
25
25
  const TOOL_RESULT_SETUP_THRESHOLD_CHARS = SETUP_OUTPUT_MAX_BYTES;
26
26
  const TOOL_RESULT_PREVIEW_CHARS = 512;
27
+ const TOOL_RESULT_GIT_PREVIEW_CHARS = 8_000;
27
28
  const TOOL_RESULT_SHELL_THRESHOLD_CHARS = 30_000;
28
29
  const TOOL_RESULT_SEARCH_THRESHOLD_CHARS = 50_000;
29
30
  const TOOL_RESULT_GREP_THRESHOLD_CHARS = 20_000;
@@ -38,7 +39,8 @@ const ARTIFACT_READ_FLAGS = constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (
38
39
  // Per-tool persistence limits are per-tool maxResultSizeChars values rather
39
40
  // than a single global value: grep persists at 20k, glob and list/find_* at
40
41
  // the 50k system default (deliberately tighter than the common 100k), and
41
- // shell/bash_session/task at 30k. Read/head/tail/diff stay inline
42
+ // bash_session/terminal at 30k. shell/task stay inline: the shell delivery
43
+ // helper already capped them, with the full log on disk. Read/head/tail/diff stay inline
42
44
  // (Infinity) — they are self-bound by FileRead semantics and the upstream
43
45
  // READ_MAX_SIZE_BYTES cap, so persisting to a sidecar to be re-read would be
44
46
  // circular. These values keep context-rich IO tools from turning into "read
@@ -59,10 +61,13 @@ const INLINE_THRESHOLD_BY_TOOL = new Map([
59
61
  ['tree', TOOL_RESULT_SEARCH_THRESHOLD_CHARS],
60
62
  ['find_files', TOOL_RESULT_SEARCH_THRESHOLD_CHARS],
61
63
  ['code_graph', TOOL_RESULT_SEARCH_THRESHOLD_CHARS],
62
- ['shell', TOOL_RESULT_SHELL_THRESHOLD_CHARS],
64
+ // shell and task results are already capped by the shared shell delivery
65
+ // (tools/builtin/lib/shell-delivery.mjs) and name their full log on disk;
66
+ // offloading them again would cut a capped result down to a preview.
67
+ ['shell', Infinity],
63
68
  ['bash_session', TOOL_RESULT_SHELL_THRESHOLD_CHARS],
64
69
  ['terminal', TOOL_RESULT_SHELL_THRESHOLD_CHARS],
65
- ['task', TOOL_RESULT_SHELL_THRESHOLD_CHARS],
70
+ ['task', Infinity],
66
71
  ['setup', TOOL_RESULT_SETUP_THRESHOLD_CHARS],
67
72
  ]);
68
73
 
@@ -110,6 +115,8 @@ function offloadableTextLength(result) {
110
115
  }
111
116
 
112
117
  const AGGREGATE_OFFLOAD_EXCLUDED_TOOLS = new Set([
118
+ 'shell',
119
+ 'task',
113
120
  'read',
114
121
  'head',
115
122
  'tail',
@@ -321,11 +328,21 @@ async function offloadText(sessionId, toolCallId, toolName, text, channel) {
321
328
  });
322
329
  if (!artifact) return text;
323
330
 
324
- const { preview, truncated } = buildPreview(text);
331
+ // git offloads were fetched 95% of the time from a 512-char preview, so git
332
+ // keeps a head+tail large enough to answer from directly.
333
+ const previewChars = String(toolName || '').toLowerCase() === 'git' ? TOOL_RESULT_GIT_PREVIEW_CHARS : undefined;
334
+ const { preview, truncated } = buildPreview(text, previewChars);
325
335
  const sizeKb = Math.max(1, Math.round(text.length / 1024));
326
336
  const displayPath = normalizeOutputPath(artifact.path);
327
337
  const header = `${TOOL_RESULT_OFFLOAD_PREFIX} ${toolName} → ${displayPath} (${sizeKb} KB, ${artifact.lines} lines)]`;
328
- const suffix = truncated ? '\n[preview truncated; full output preserved at the artifact path above]' : '';
338
+ // The header already names the path; git repeats it after its long preview,
339
+ // where the header is far above the cut.
340
+ let suffix = '';
341
+ if (truncated) {
342
+ suffix = previewChars
343
+ ? `\n${capMarker({ limit: `${previewChars} chars`, narrow: 'read it with offset/limit', fullPath: displayPath })}`
344
+ : '\n[preview truncated; full output preserved at the artifact path above]';
345
+ }
329
346
  return `${header}\n\n${preview}${suffix}`;
330
347
  }
331
348
 
@@ -12,6 +12,7 @@
12
12
  import { coerceReadFamilyPathArg, coerceShapeFlex, hasGlobMagic, normalizeGrepArgs } from './path-utils.mjs';
13
13
  import { hasOwn } from '../../../../shared/object.mjs';
14
14
  import { CODE_GRAPH_FILE_MODES, CODE_GRAPH_MODES } from '../code-graph-tool-defs.mjs';
15
+ import { SHELL_HOLD_MAX_MS, SHELL_HOLD_MIN_MS } from './wait-budgets.mjs';
15
16
 
16
17
  const MAX_INT = 100000;
17
18
  export const PUBLIC_PATH_BATCH_LIMIT = 10;
@@ -737,7 +738,7 @@ function guardRead(a) {
737
738
  if (err) return err;
738
739
  for (const ek of ['offset', 'limit']) {
739
740
  const eErr = checkIntInRange(entry, ek, 0, ek === 'offset' ? READ_OFFSET_MAX : MAX_INT);
740
- if (eErr) return eErr.replace(`"${ek}"`, `"path[${i}].${ek}"`);
741
+ if (eErr) return eErr.replace(`"${ek}"`, () => `"path[${i}].${ek}"`);
741
742
  }
742
743
  }
743
744
  }
@@ -779,16 +780,21 @@ function guardRead(a) {
779
780
  }
780
781
 
781
782
  function guardShell(a) {
782
- const allowed = new Set(['command', 'timeout_ms', 'wait_ms']);
783
+ const allowed = new Set(['command', 'timeout_ms', 'wait_ms', 'background']);
783
784
  const unsupported = Object.keys(a).find((key) => !allowed.has(key));
784
785
  if (unsupported) {
785
- return `Error: shell arg "${unsupported}" is unsupported; use only command, timeout_ms, and wait_ms`;
786
+ return `Error: shell arg "${unsupported}" is unsupported; use only command, timeout_ms, wait_ms, and background`;
787
+ }
788
+ if (hasOwn(a, 'background') && typeof a.background !== 'boolean') {
789
+ return `Error: shell arg "background" must be a boolean (got ${describeType(a.background)})`;
786
790
  }
787
791
  // Over-max is an unambiguous "wait as long as allowed"; clamp like the other
788
792
  // saturating integer args instead of failing the call.
789
- if (hasOwn(a, 'wait_ms') && Number.isInteger(a.wait_ms) && a.wait_ms > 30000) a.wait_ms = 30000;
790
- if (hasOwn(a, 'wait_ms') && (!Number.isInteger(a.wait_ms) || a.wait_ms < 1000)) {
791
- return `Error: shell arg "wait_ms" must be an integer from 10000 to 30000, e.g. wait_ms:20000 (got ${JSON.stringify(a.wait_ms)})`;
793
+ if (hasOwn(a, 'wait_ms') && Number.isInteger(a.wait_ms) && a.wait_ms > SHELL_HOLD_MAX_MS) {
794
+ a.wait_ms = SHELL_HOLD_MAX_MS;
795
+ }
796
+ if (hasOwn(a, 'wait_ms') && (!Number.isInteger(a.wait_ms) || a.wait_ms < SHELL_HOLD_MIN_MS)) {
797
+ return `Error: shell arg "wait_ms" must be an integer from ${SHELL_HOLD_MIN_MS} to ${SHELL_HOLD_MAX_MS}, e.g. wait_ms:120000 (got ${JSON.stringify(a.wait_ms)})`;
792
798
  }
793
799
  if (!hasOwn(a, 'command')) {
794
800
  return 'Error: shell requires "command"';