mixdog 1.2.4 → 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 (292) 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/mobile-release.mjs +5 -4
  5. package/scripts/provider-pingpong-live.mjs +9 -9
  6. package/src/agents/advisor/AGENT.md +4 -4
  7. package/src/agents/advisor/agent.json +1 -1
  8. package/src/agents/reviewer/AGENT.md +2 -3
  9. package/src/agents/reviewer/agent.json +1 -1
  10. package/src/defaults/shipped-history.json +11 -0
  11. package/src/defaults/skills/browser-use/SKILL.md +2 -1
  12. package/src/defaults/skills/computer-use/SKILL.md +3 -1
  13. package/src/defaults/skills/visualize/SKILL.md +3 -2
  14. package/src/lib/rules-builder.cjs +1 -1
  15. package/src/output-styles/common.md +22 -5
  16. package/src/output-styles/detailed.md +7 -5
  17. package/src/output-styles/minimal.md +7 -3
  18. package/src/output-styles/simple.md +2 -3
  19. package/src/rules/agent/AGENT.md +3 -1
  20. package/src/rules/lead/LEAD.md +10 -7
  21. package/src/rules/shared/10-tool-workflow.md +2 -1
  22. package/src/rules/shared/60-verification.md +4 -5
  23. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -1
  24. package/src/runtime/agent/orchestrator/config.mjs +6 -0
  25. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +1 -1
  26. package/src/runtime/agent/orchestrator/internal-agents.mjs +10 -0
  27. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +53 -60
  28. package/src/runtime/agent/orchestrator/mcp/client.mjs +7 -4
  29. package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +2 -3
  30. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +9 -2
  31. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +40 -19
  32. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +3 -2
  33. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events/content-block-start.mjs +4 -1
  34. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events.mjs +2 -0
  35. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn.mjs +3 -0
  36. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +2 -1
  37. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +3 -1
  38. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +1 -2
  39. package/src/runtime/agent/orchestrator/providers/catalog-deadline.mjs +163 -0
  40. package/src/runtime/agent/orchestrator/providers/catalog-failures.mjs +34 -0
  41. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +38 -14
  42. package/src/runtime/agent/orchestrator/providers/lib/oauth-page.mjs +100 -26
  43. package/src/runtime/agent/orchestrator/providers/lib/send-callbacks.mjs +24 -0
  44. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +13 -0
  45. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +2 -1
  46. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -0
  47. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/chunk-events.mjs +4 -0
  48. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +3 -1
  49. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +2 -0
  50. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +49 -4
  51. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +0 -7
  52. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +7 -1
  53. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +1 -0
  54. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +2 -0
  55. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +1 -0
  56. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events.mjs +5 -1
  57. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +11 -2
  58. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +1 -0
  59. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +45 -16
  60. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +1 -0
  61. package/src/runtime/agent/orchestrator/providers/openai-ws-connect.mjs +196 -0
  62. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +74 -9
  63. package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +14 -1
  64. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -0
  65. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth-login.mjs +194 -0
  66. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth-tokens.mjs +353 -0
  67. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +293 -0
  68. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +33 -100
  69. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +23 -14
  70. package/src/runtime/agent/orchestrator/providers/registry.mjs +41 -27
  71. package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +2 -3
  72. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +3 -8
  73. package/src/runtime/agent/orchestrator/session/agent-pool-progress.mjs +15 -3
  74. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +74 -13
  75. package/src/runtime/agent/orchestrator/session/compact/forked-summary.mjs +102 -0
  76. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +8 -2
  77. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +16 -0
  78. package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +9 -0
  79. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +39 -2
  80. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +9 -19
  81. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +2 -3
  82. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -0
  83. package/src/runtime/agent/orchestrator/session/manager/ask-turn-steering-drain.mjs +13 -5
  84. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +10 -7
  85. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +62 -22
  86. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +23 -1
  87. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +81 -34
  88. package/src/runtime/agent/orchestrator/session/manager.mjs +2 -2
  89. package/src/runtime/agent/orchestrator/session/request-fork.mjs +47 -0
  90. package/src/runtime/agent/orchestrator/session/side-question-store.mjs +16 -1
  91. package/src/runtime/agent/orchestrator/session/side-question.mjs +5 -43
  92. package/src/runtime/agent/orchestrator/session/store/fs-probe.mjs +10 -0
  93. package/src/runtime/agent/orchestrator/session/store/lifecycle-barriers.mjs +73 -6
  94. package/src/runtime/agent/orchestrator/session/store/load-session.mjs +40 -0
  95. package/src/runtime/agent/orchestrator/session/store/save-fault.mjs +15 -0
  96. package/src/runtime/agent/orchestrator/session/store/session-paths.mjs +12 -0
  97. package/src/runtime/agent/orchestrator/session/store-agent-worker-header.mjs +36 -0
  98. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +255 -32
  99. package/src/runtime/agent/orchestrator/session/store-record-strict-scan.mjs +306 -0
  100. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +70 -21
  101. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +46 -30
  102. package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +144 -45
  103. package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +32 -13
  104. package/src/runtime/agent/orchestrator/session/store-transcript-worker.mjs +364 -38
  105. package/src/runtime/agent/orchestrator/session/store.mjs +9 -2
  106. package/src/runtime/agent/orchestrator/session/task-wait-control.mjs +43 -12
  107. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +23 -6
  108. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -6
  109. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +13 -7
  110. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/bashism-rewrite.mjs +1 -1
  111. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/benign-exit.mjs +7 -1
  112. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +40 -7
  113. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +37 -34
  114. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/inline-hoist.mjs +1 -1
  115. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +0 -4
  116. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +6 -0
  117. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +29 -27
  118. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +1 -1
  119. package/src/runtime/agent/orchestrator/tools/builtin/find-files-tool.mjs +28 -11
  120. package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +12 -3
  121. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +203 -101
  122. package/src/runtime/agent/orchestrator/tools/builtin/git-repo-rw-lock.mjs +90 -41
  123. package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +17 -1
  124. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-delivery.mjs +199 -0
  125. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-task-output.mjs +165 -39
  126. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +25 -13
  127. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +7 -6
  128. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +19 -4
  129. package/src/runtime/agent/orchestrator/tools/builtin/read-line-collector.mjs +5 -3
  130. package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +25 -7
  131. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +2 -1
  132. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-render.mjs +3 -1
  133. package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +12 -5
  134. package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +61 -22
  135. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +32 -8
  136. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +193 -19
  137. package/src/runtime/agent/orchestrator/tools/builtin/shell-direct-exe.mjs +57 -7
  138. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +119 -32
  139. package/src/runtime/agent/orchestrator/tools/builtin/shell-lossless-compact.mjs +26 -1
  140. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +13 -28
  141. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +90 -11
  142. package/src/runtime/agent/orchestrator/tools/builtin/wait-budgets.mjs +23 -0
  143. package/src/runtime/agent/orchestrator/tools/builtin.mjs +54 -10
  144. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +22 -28
  145. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +11 -7
  146. package/src/runtime/agent/orchestrator/tools/destructive-warning.mjs +126 -9
  147. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
  148. package/src/runtime/agent/orchestrator/tools/lib/owned-shell-trees.mjs +15 -6
  149. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +40 -2
  150. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +2 -2
  151. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promotion-results.mjs +13 -3
  152. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +2 -0
  153. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +3 -1
  154. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +1 -1
  155. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +35 -4
  156. package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +7 -2
  157. package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +39 -23
  158. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +6 -2
  159. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +13 -13
  160. package/src/runtime/attachments/office-extract.mjs +11 -9
  161. package/src/runtime/memory/lib/embedding-tokenizer.mjs +32 -18
  162. package/src/runtime/memory/lib/runtime-fetcher.mjs +1 -1
  163. package/src/runtime/memory/lib/tool-call-handler.mjs +7 -5
  164. package/src/runtime/office/authoring/pptx-script-normalize.mjs +11 -8
  165. package/src/runtime/office/core/tabular.mjs +1 -1
  166. package/src/runtime/office/design/library/design-template-inspect.mjs +7 -4
  167. package/src/runtime/office/portable/docx-formatting.mjs +1 -1
  168. package/src/runtime/office/portable/docx-runs.mjs +1 -1
  169. package/src/runtime/office/portable/docx-tracked-edits.mjs +2 -2
  170. package/src/runtime/office/portable/portable-docx-edits.mjs +11 -10
  171. package/src/runtime/office/portable/portable-docx-operations.mjs +21 -17
  172. package/src/runtime/office/portable/portable-docx-parts.mjs +15 -9
  173. package/src/runtime/office/portable/portable-docx-styles.mjs +4 -1
  174. package/src/runtime/office/portable/portable-docx-xml.mjs +9 -9
  175. package/src/runtime/office/portable/portable-opc.mjs +27 -13
  176. package/src/runtime/office/portable/portable-pivot.mjs +3 -3
  177. package/src/runtime/office/portable/portable-pptx-chart.mjs +3 -3
  178. package/src/runtime/office/portable/portable-pptx-charts.mjs +24 -18
  179. package/src/runtime/office/portable/portable-pptx-core.mjs +5 -5
  180. package/src/runtime/office/portable/portable-pptx-deck.mjs +8 -4
  181. package/src/runtime/office/portable/portable-pptx-package.mjs +7 -4
  182. package/src/runtime/office/portable/portable-pptx-shapes.mjs +18 -9
  183. package/src/runtime/office/portable/portable-sheet-parts.mjs +9 -3
  184. package/src/runtime/office/portable/portable-sheet-xml.mjs +5 -5
  185. package/src/runtime/office/portable/portable-xlsx-charts.mjs +4 -1
  186. package/src/runtime/office/portable/portable-xlsx-drawings.mjs +10 -6
  187. package/src/runtime/office/portable/portable-xlsx-operations.mjs +8 -5
  188. package/src/runtime/office/portable/portable-xlsx-pivot-ops.mjs +1 -1
  189. package/src/runtime/office/portable/portable-xlsx-reference-shift.mjs +11 -7
  190. package/src/runtime/office/portable/portable-xlsx-sheet-edits.mjs +6 -3
  191. package/src/runtime/office/portable/portable-xml.mjs +1 -1
  192. package/src/runtime/shared/atomic-file.mjs +22 -13
  193. package/src/runtime/shared/background-tasks.mjs +60 -20
  194. package/src/runtime/shared/child-spawn-gate.mjs +26 -16
  195. package/src/runtime/shared/config.mjs +0 -10
  196. package/src/runtime/shared/file-lock.mjs +7 -1
  197. package/src/runtime/shared/file-permissions-worker.mjs +38 -0
  198. package/src/runtime/shared/file-permissions.d.mts +19 -4
  199. package/src/runtime/shared/file-permissions.mjs +310 -4
  200. package/src/runtime/shared/git-runner-worker.mjs +6 -78
  201. package/src/runtime/shared/llm/http-agent.mjs +7 -3
  202. package/src/runtime/shared/llm/provider-catalog-registry.mjs +10 -0
  203. package/src/runtime/shared/llm/usage-accounting.mjs +2 -0
  204. package/src/runtime/shared/llm/usage-ledger-import.mjs +32 -3
  205. package/src/runtime/shared/llm/usage-ledger-rollup.mjs +12 -5
  206. package/src/runtime/shared/llm/usage-ledger-worker.mjs +1 -1
  207. package/src/runtime/shared/llm/usage-ledger.mjs +20 -5
  208. package/src/runtime/shared/memory-snapshot.mjs +5 -1
  209. package/src/runtime/shared/notify-trace.mjs +8 -1
  210. package/src/runtime/shared/pristine-execution-contract.json +5 -1
  211. package/src/runtime/shared/process-runner-worker.mjs +7 -0
  212. package/src/runtime/shared/process-runner.mjs +38 -0
  213. package/src/runtime/shared/provider-accounts.mjs +2 -1
  214. package/src/runtime/shared/provider-api-key.mjs +0 -1
  215. package/src/runtime/shared/read-row-numbers.mjs +1 -1
  216. package/src/runtime/shared/resource-admission.mjs +60 -18
  217. package/src/runtime/shared/run-child.mjs +84 -0
  218. package/src/runtime/shared/session-activity-signal.mjs +23 -0
  219. package/src/runtime/shared/shell-output-summary.mjs +23 -0
  220. package/src/runtime/shared/singleton-owner.mjs +126 -42
  221. package/src/runtime/shared/tool-output-limit.mjs +4 -2
  222. package/src/runtime/shared/tool-output-marker.mjs +58 -0
  223. package/src/runtime/shared/user-data-guard.mjs +28 -9
  224. package/src/runtime/shared/worktrees/activity-gate.mjs +33 -8
  225. package/src/runtime/shared/worktrees/git-head.mjs +20 -2
  226. package/src/runtime/shared/worktrees/lock.mjs +3 -2
  227. package/src/runtime/terminal-bridge/tool-defs.mjs +1 -1
  228. package/src/runtime/tidy/runners/biome.mjs +3 -1
  229. package/src/runtime/tidy/shadow.mjs +2 -1
  230. package/src/runtime/web-search/index.mjs +5 -7
  231. package/src/runtime/web-search/lib/formatter.mjs +42 -6
  232. package/src/runtime/web-search/tool-defs.mjs +6 -1
  233. package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +4 -3
  234. package/src/session-runtime/lifecycle/session-switch/resume.mjs +7 -4
  235. package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +3 -3
  236. package/src/session-runtime/lifecycle/shared.mjs +19 -2
  237. package/src/session-runtime/lifecycle/teardown/session-close.mjs +2 -0
  238. package/src/session-runtime/lifecycle/teardown.mjs +5 -2
  239. package/src/session-runtime/lifecycle-api.mjs +3 -1
  240. package/src/session-runtime/provider-auth/login.mjs +10 -6
  241. package/src/session-runtime/provider-auth/setup.mjs +19 -2
  242. package/src/session-runtime/provider-catalog-cache.mjs +101 -25
  243. package/src/session-runtime/provider-models/catalog-load.mjs +14 -3
  244. package/src/session-runtime/provider-models/row-hydration.mjs +4 -0
  245. package/src/session-runtime/provider-models.mjs +76 -10
  246. package/src/session-runtime/provider-readiness.mjs +4 -0
  247. package/src/session-runtime/services/agent-tool/close-flow.mjs +2 -1
  248. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +10 -2
  249. package/src/session-runtime/services/agent-tool/job-views/session-progress.mjs +13 -4
  250. package/src/session-runtime/services/agent-tool/notify.mjs +15 -4
  251. package/src/session-runtime/services/agent-tool/restart-notify.mjs +134 -0
  252. package/src/session-runtime/services/agent-tool/send-flow.mjs +24 -1
  253. package/src/session-runtime/services/agent-tool/spawn-flow/deferred-spawn.mjs +2 -1
  254. package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +4 -3
  255. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +5 -1
  256. package/src/session-runtime/services/agent-tool/tag-registry/tag-lookup.mjs +8 -3
  257. package/src/session-runtime/services/agent-tool/tag-registry.mjs +3 -0
  258. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +4 -0
  259. package/src/session-runtime/services/agent-tool/worker-index.mjs +44 -10
  260. package/src/session-runtime/services/agent-tool.mjs +20 -0
  261. package/src/session-runtime/services/provider-admin.mjs +92 -20
  262. package/src/session-runtime/services/usage-dashboard-model.mjs +2 -2
  263. package/src/session-runtime/services/usage-dashboard.mjs +27 -69
  264. package/src/session-runtime/turn/session-ops.mjs +2 -2
  265. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +1 -0
  266. package/src/session-runtime/usage-stats-api.mjs +6 -1
  267. package/src/standalone/channel-transport/http-routes.mjs +10 -9
  268. package/src/standalone/daemon-desktop-runtime.mjs +20 -7
  269. package/src/standalone/daemon-owner.mjs +117 -25
  270. package/src/standalone/daemon.mjs +43 -13
  271. package/src/standalone/desktop-service-registry.mjs +32 -5
  272. package/src/standalone/session-call-cache.mjs +6 -0
  273. package/src/standalone/session-protocol.mjs +1 -1
  274. package/src/standalone/session-service/retention/busy.mjs +7 -2
  275. package/src/standalone/session-transport/routes.mjs +7 -6
  276. package/src/tui/app/model-options.mjs +39 -2
  277. package/src/tui/app/model-picker.mjs +16 -2
  278. package/src/tui/app/provider-setup-picker.mjs +1 -2
  279. package/src/tui/dist/index.mjs +436 -123
  280. package/src/tui/session/notice-text.mjs +0 -1
  281. package/src/tui/session/session-api/intake/submission.mjs +7 -13
  282. package/src/tui/session/session-api/integrations.mjs +1 -5
  283. package/src/tui/session/session-flow/drain.mjs +8 -0
  284. package/src/tui/session/session-flow/steering.mjs +46 -0
  285. package/src/tui/session/session-flow.mjs +2 -0
  286. package/src/tui/session/turn-stream.mjs +33 -0
  287. package/src/tui/session/turn.mjs +30 -3
  288. package/src/ui/statusline.mjs +1 -6
  289. package/src/ui/streaming-markdown-heal.mjs +94 -25
  290. package/src/vendor/statusline/bin/statusline-route.mjs +0 -1
  291. package/src/vendor/statusline/src/gateway/route-meta.mjs +0 -1
  292. package/src/workflows/default/WORKFLOW.md +9 -13
@@ -4,6 +4,7 @@ import { normalizeErrorMessage } from '../path-diagnostics.mjs';
4
4
  import { isBinaryBuffer } from '../binary-file.mjs';
5
5
  import { isUncPath, isWindowsDevicePath, hasUnsafeWin32Component } from '../device-paths.mjs';
6
6
  import { normalizeOutputPath, resolveAgainstCwd } from '../path-utils.mjs';
7
+ import { windowMarker } from '../tool-output-limit.mjs';
7
8
 
8
9
  /** Report the requested path's result; suggestions never retarget the operation. */
9
10
  export async function readFamilyPathEnoentOrError(workDir, fullPath, inputPath, err) {
@@ -52,8 +53,23 @@ export function guardedWalkRoot(inputPath, workDir) {
52
53
  return { fullPath };
53
54
  }
54
55
 
56
+ // Code-unit path order: the tiebreak that makes a size/mtime sort total, so
57
+ // equal keys cannot reorder with the walk (and shift across offset pages).
58
+ export function comparePathsStable(a, b) {
59
+ const x = String(a);
60
+ const y = String(b);
61
+ if (x === y) return 0;
62
+ return x < y ? -1 : 1;
63
+ }
64
+
55
65
  export function pageContinuationLine(offset, shown, total) {
56
- return `... [entries ${offset + 1}-${offset + shown} of ${total}; pass offset:${offset + shown} to continue]`;
66
+ return windowMarker({
67
+ unit: 'entries',
68
+ from: offset + 1,
69
+ to: offset + shown,
70
+ total,
71
+ nextOffset: offset + shown,
72
+ });
57
73
  }
58
74
 
59
75
  const LINE_COUNT_MAX_BYTES = 2 * 1024 * 1024;
@@ -0,0 +1,199 @@
1
+ // One shape for every piece of shell output handed to an agent — a foreground
2
+ // result, a background move, task wait/read and a completion notice:
3
+ // 1. ANSI stripped, then `\r` progress frames folded to their last frame
4
+ // (before anything is counted or cut);
5
+ // 2. a line longer than DELIVERY_LINE_MAX_CHARS is cut: `… [N chars cut]`;
6
+ // 3. DELIVERY_FULL_CHARS or less goes whole; otherwise a head of at most
7
+ // DELIVERY_HEAD_LINES lines (≤ DELIVERY_HEAD_MAX_CHARS), the marker, and
8
+ // the last ~DELIVERY_TAIL_CHARS;
9
+ // 4. the marker is the same everywhere and always ends with the full log:
10
+ // `[… N lines, X KB omitted — read with offset/limit or grep, full log: <path>]`;
11
+ // 5. anything shaped like a harness/system tag is escaped.
12
+ // MIXDOG_SHELL_DELIVERY_MAX_CHARS is the one knob (the whole-delivery cap); the
13
+ // head and tail sizes are fixed. It is separate from the 50 KB
14
+ // MIXDOG_TOOL_OUTPUT_MAX_BYTES that the other tools use.
15
+ import { randomBytes } from 'node:crypto';
16
+ import { mkdirSync, writeFileSync } from 'node:fs';
17
+ import { join } from 'node:path';
18
+ import { getPluginData } from '../../../config.mjs';
19
+ import { stripAnsi } from '../../shell-exec-output.mjs';
20
+ import { createCarriageReturnFolder } from '../shell-lossless-compact.mjs';
21
+ import { positiveEnvInt } from '../../../../../shared/tool-output-limit.mjs';
22
+ import { escapeHarnessTags } from '../../../../../shared/shell-output-summary.mjs';
23
+
24
+ export const DELIVERY_FULL_CHARS = positiveEnvInt('MIXDOG_SHELL_DELIVERY_MAX_CHARS', 6_000);
25
+ const DELIVERY_HEAD_LINES = 10;
26
+ const DELIVERY_HEAD_MAX_CHARS = 1_000;
27
+ const DELIVERY_TAIL_CHARS = 2_000;
28
+ export const DELIVERY_LINE_MAX_CHARS = 2_000;
29
+
30
+ export { createCarriageReturnFolder };
31
+
32
+ const newlines = (text) => {
33
+ let count = 0;
34
+ for (let i = text.indexOf('\n'); i >= 0; i = text.indexOf('\n', i + 1)) count += 1;
35
+ return count;
36
+ };
37
+
38
+ export function formatKb(bytes) {
39
+ return `${(Math.max(0, bytes) / 1024).toFixed(1)} KB`;
40
+ }
41
+
42
+ /** The omission marker — identical on every delivery path, path last. */
43
+ export function omissionMarker({ lines, bytes, path }) {
44
+ return `[… ${lines} lines, ${formatKb(bytes)} omitted — read with offset/limit or grep, full log: ${path}]`;
45
+ }
46
+
47
+ // `cut` is the number of chars removed from over-long lines.
48
+ function cutLongLines(text) {
49
+ if (text.length <= DELIVERY_LINE_MAX_CHARS) return { text, cut: 0 };
50
+ let cut = 0;
51
+ const cutText = text
52
+ .split('\n')
53
+ .map((line) => {
54
+ if (line.length <= DELIVERY_LINE_MAX_CHARS) return line;
55
+ cut += line.length - DELIVERY_LINE_MAX_CHARS;
56
+ return `${line.slice(0, DELIVERY_LINE_MAX_CHARS)} … [${line.length - DELIVERY_LINE_MAX_CHARS} chars cut]`;
57
+ })
58
+ .join('\n');
59
+ return { text: cutText, cut };
60
+ }
61
+
62
+ /** Step 1 for one chunk (the cut of step 2 is made where the text is
63
+ * delivered, so the uncut text can be archived). `fold` carries `\r` state
64
+ * across chunks of the same stream; omitted, the chunk is folded on its own. */
65
+ export function prepareShellText(text, fold = createCarriageReturnFolder()) {
66
+ return fold(stripAnsi(String(text ?? '')));
67
+ }
68
+
69
+ // `scale` is the share of the delivery budget a stream was given.
70
+ function headOf(text, scale = 1) {
71
+ let end = 0;
72
+ for (let line = 0; line < DELIVERY_HEAD_LINES && end < text.length; line += 1) {
73
+ const newline = text.indexOf('\n', end);
74
+ end = newline < 0 ? text.length : newline + 1;
75
+ }
76
+ return text.slice(0, Math.min(end, Math.floor(DELIVERY_HEAD_MAX_CHARS * scale)));
77
+ }
78
+
79
+ function tailOf(text, floor = 0, scale = 1) {
80
+ const start = Math.max(floor, text.length - Math.floor(DELIVERY_TAIL_CHARS * scale));
81
+ if (start === floor) return text.slice(start);
82
+ // Start on a line boundary when one is near.
83
+ const newline = text.indexOf('\n', start);
84
+ return newline >= 0 && newline - start < 200 && newline + 1 < text.length
85
+ ? text.slice(newline + 1)
86
+ : text.slice(start);
87
+ }
88
+
89
+ /** head + marker + tail; `omitted` counts what lies between them. */
90
+ function joinHeadTail(head, tail, omitted) {
91
+ const gap = head && !head.endsWith('\n') ? '\n' : '';
92
+ return `${head}${gap}${omissionMarker(omitted)}\n${tail}`;
93
+ }
94
+
95
+ // One stream's delivery: `natural` is the length it would have whole (escaped,
96
+ // markers included; Infinity when it can only be shown at its ends) and
97
+ // `render(budget)` the text within `budget` chars. Streams share one budget.
98
+ export const EMPTY_PIECE = { natural: 0, render: () => '' };
99
+
100
+ /** Step 3 when only the two ends of a long range were read: `total` is the
101
+ * whole range's bytes and lines. */
102
+ export function edgesPiece(headText, tailText, { totalBytes, totalLines, path }) {
103
+ return {
104
+ natural: Number.POSITIVE_INFINITY,
105
+ render: (budget) => {
106
+ const scale = budget / DELIVERY_FULL_CHARS;
107
+ const head = headOf(escapeHarnessTags(headText), scale);
108
+ const tail = tailOf(escapeHarnessTags(tailText), 0, scale);
109
+ return joinHeadTail(head, tail, {
110
+ lines: Math.max(0, totalLines - newlines(head) - newlines(tail)),
111
+ bytes: Math.max(0, totalBytes - Buffer.byteLength(head) - Buffer.byteLength(tail)),
112
+ path,
113
+ });
114
+ },
115
+ };
116
+ }
117
+
118
+ export { newlines as countNewlines };
119
+
120
+ /** Steps 2, 3 and 5 on prepared text. `path` names where the whole (uncut)
121
+ * text can be read; it is resolved only when something is removed. */
122
+ export function textPiece(text, { path }) {
123
+ let archive;
124
+ const where = () => (archive ??= typeof path === 'function' ? path() : path);
125
+ const { text: cutText, cut } = cutLongLines(text);
126
+ const shown = escapeHarnessTags(cutText);
127
+ // Cut long lines are removed content too: the whole text carries the marker.
128
+ const whole = cut
129
+ ? `${shown}${shown.endsWith('\n') ? '' : '\n'}${omissionMarker({ lines: 0, bytes: cut, path: where() })}\n`
130
+ : shown;
131
+ return {
132
+ natural: whole.length,
133
+ render: (budget) => {
134
+ if (whole.length <= budget) return whole;
135
+ const scale = budget / DELIVERY_FULL_CHARS;
136
+ const head = headOf(shown, scale);
137
+ const tail = tailOf(shown, head.length, scale);
138
+ const middle = shown.slice(head.length, shown.length - tail.length);
139
+ return joinHeadTail(head, tail, { lines: newlines(middle), bytes: Buffer.byteLength(middle), path: where() });
140
+ },
141
+ };
142
+ }
143
+
144
+ /** Max-min fair split of `budget` over the streams' natural lengths: a stream
145
+ * that fits its fair share is given exactly what it needs, the rest share
146
+ * what remains. */
147
+ function shareBudget(lengths, budget) {
148
+ const shares = lengths.map(() => 0);
149
+ let open = lengths.map((_, index) => index);
150
+ let left = Math.max(0, budget);
151
+ while (open.length) {
152
+ const fair = left / open.length;
153
+ const fits = open.filter((index) => lengths[index] <= fair);
154
+ if (!fits.length) {
155
+ for (const index of open) shares[index] = fair;
156
+ break;
157
+ }
158
+ for (const index of fits) {
159
+ shares[index] = lengths[index];
160
+ left -= lengths[index];
161
+ }
162
+ open = open.filter((index) => !fits.includes(index));
163
+ }
164
+ return shares;
165
+ }
166
+
167
+ /** Deliver streams within one shared budget (default: the whole-delivery cap),
168
+ * each escaped; the caller deducts what it adds around them. */
169
+ export function fitPieces(pieces, budget = DELIVERY_FULL_CHARS) {
170
+ const shares = shareBudget(
171
+ pieces.map((piece) => piece.natural),
172
+ budget
173
+ );
174
+ return pieces.map((piece, index) => escapeHarnessTags(piece.render(shares[index])));
175
+ }
176
+
177
+ /** Text that has no log of its own is kept where the marker can name it:
178
+ * the shell-output folder, swept with the spill files. */
179
+ export function persistDeliveryLog(text, label = 'output') {
180
+ const dir = join(getPluginData(), 'shell-output');
181
+ mkdirSync(dir, { recursive: true });
182
+ const file = join(dir, `delivered_${Date.now().toString(36)}_${randomBytes(4).toString('hex')}.${label}`);
183
+ writeFileSync(file, text);
184
+ return file;
185
+ }
186
+
187
+ /** Steps 1–5 for a whole string, as a piece. Without `path`, the original
188
+ * (uncut) text is saved when anything is removed, so the marker can name it. */
189
+ export function shellTextPiece(text, { path = null, label = 'output', fold } = {}) {
190
+ const prepared = prepareShellText(text, fold);
191
+ return textPiece(prepared, { path: path || (() => persistDeliveryLog(prepared, label)) });
192
+ }
193
+
194
+ export function deliverShellText(text, options) {
195
+ return fitPieces([shellTextPiece(text, options)])[0];
196
+ }
197
+
198
+ /** Step 5 alone, for text the caller already capped. */
199
+ export { escapeHarnessTags };
@@ -1,11 +1,34 @@
1
+ // Incremental delivery of a shell task's output. Each task (the authorized
2
+ // task object) owns one cursor per stream; every wait, read, background move
3
+ // and completion notice delivers only the bytes after it, in the shared
4
+ // delivery shape (lib/shell-delivery.mjs). The cursor always advances over the
5
+ // whole unseen range, even when its middle is omitted: omitted bytes are never
6
+ // re-sent, the marker's log path is how to get them back.
1
7
  import { closeSync, fstatSync, openSync, readSync } from 'node:fs';
2
8
  import { stripAnsi } from '../../shell-exec-output.mjs';
3
9
  import { FileEncodingSniffer, ShellStreamDecoder, alignedOffset, oemTextEncoding } from '../../shell-codepage.mjs';
10
+ import {
11
+ DELIVERY_FULL_CHARS,
12
+ countNewlines,
13
+ createCarriageReturnFolder,
14
+ edgesPiece,
15
+ escapeHarnessTags,
16
+ fitPieces,
17
+ formatKb,
18
+ omissionMarker,
19
+ persistDeliveryLog,
20
+ prepareShellText,
21
+ textPiece,
22
+ } from './shell-delivery.mjs';
4
23
 
5
- import { TOOL_OUTPUT_PAGE_BYTES, TOOL_OUTPUT_TAIL_BYTES } from '../../../../../shared/tool-output-budget.mjs';
6
24
  const cursors = new WeakMap();
7
25
  const ESC = '\x1b';
8
26
  const PENDING_ANSI_INTRODUCER = new RegExp(`${ESC}(?:\\[[0-?]*[ -/]*|\\](?:(?!${ESC}\\\\)[^\x07\x9c])*)?$`);
27
+ // An unseen range up to this size is read whole; a longer one only at its two
28
+ // ends (the middle is counted, never loaded).
29
+ const FULL_READ_BYTES = 64 * 1024;
30
+ const EDGE_READ_BYTES = 16 * 1024;
31
+ const COUNT_CHUNK_BYTES = 1 << 20;
9
32
 
10
33
  function newCursor() {
11
34
  return {
@@ -13,6 +36,7 @@ function newCursor() {
13
36
  preview: '',
14
37
  decoder: new ShellStreamDecoder(),
15
38
  ansi: '',
39
+ fold: createCarriageReturnFolder(),
16
40
  sniffer: new FileEncodingSniffer(),
17
41
  committed: false,
18
42
  };
@@ -26,13 +50,6 @@ function pendingAnsiIntroducer(sequence) {
26
50
  return '\x1b';
27
51
  }
28
52
 
29
- // First index at or after `start` that does not sit inside a UTF-8 sequence.
30
- function utf8Boundary(bytes, start, end) {
31
- let index = start;
32
- while (index < end && (bytes[index] & 0xc0) === 0x80) index++;
33
- return index;
34
- }
35
-
36
53
  function visibleText(cursor, text) {
37
54
  const value = cursor.ansi + text;
38
55
  // Retain only the introducer of an unfinished CSI/OSC, not its potentially
@@ -45,21 +62,123 @@ function visibleText(cursor, text) {
45
62
  return stripAnsi(value.slice(0, pending.index));
46
63
  }
47
64
 
65
+ function readBytes(fd, start, length) {
66
+ const buffer = Buffer.alloc(Math.max(0, length));
67
+ const count = length > 0 ? readSync(fd, buffer, 0, length, start) : 0;
68
+ return buffer.subarray(0, count);
69
+ }
70
+
71
+ function countNewlinesInRange(fd, start, end) {
72
+ let count = 0;
73
+ const buffer = Buffer.alloc(Math.min(COUNT_CHUNK_BYTES, Math.max(1, end - start)));
74
+ for (let position = start; position < end; ) {
75
+ const bytes = readSync(fd, buffer, 0, Math.min(buffer.length, end - position), position);
76
+ if (bytes <= 0) break;
77
+ for (let i = 0; i < bytes; i += 1) if (buffer[i] === 10) count += 1;
78
+ position += bytes;
79
+ }
80
+ return count;
81
+ }
82
+
83
+ function decodeVisible(cursor, bytes, flush) {
84
+ let decoded = cursor.decoder.write(bytes);
85
+ if (flush) decoded += cursor.decoder.end();
86
+ return prepareShellText(visibleText(cursor, decoded), cursor.fold);
87
+ }
88
+
89
+ // One unseen file range [start, end) in the delivery shape. A long range is
90
+ // read only at its ends; the tail starts a fresh decoder at a character
91
+ // boundary, as the original paging did when it skipped bytes.
92
+ function readRange(fd, cursor, { start, end, label, flush, path }) {
93
+ const total = end - start;
94
+ if (total <= FULL_READ_BYTES) {
95
+ const text = decodeVisible(cursor, readBytes(fd, start, total), flush);
96
+ return { piece: textPiece(text, { path }), bytes: total, lines: countNewlines(text), text };
97
+ }
98
+ const headText = decodeVisible(cursor, readBytes(fd, start, EDGE_READ_BYTES), false);
99
+ const tailStart = alignedOffset(fd, end - EDGE_READ_BYTES, label || 'utf-8');
100
+ cursor.decoder = new ShellStreamDecoder(oemTextEncoding(), label);
101
+ cursor.ansi = '';
102
+ cursor.fold = createCarriageReturnFolder();
103
+ const tailText = decodeVisible(cursor, readBytes(fd, tailStart, end - tailStart), flush);
104
+ const lines = countNewlinesInRange(fd, start, end);
105
+ return {
106
+ piece: edgesPiece(headText, tailText, { totalBytes: total, totalLines: lines, path }),
107
+ bytes: total,
108
+ lines,
109
+ text: tailText,
110
+ };
111
+ }
112
+
113
+ const PERCENT_RE = /(\d{1,3}(?:\.\d+)?)\s?%/g;
114
+ const ETA_RE = /\b(?:ETA|eta|remaining|left)\b[:=\s]*(\d+(?::\d{2}){1,2}|\d+(?:\.\d+)?\s?(?:h|m|s|min|sec)\b)/g;
115
+
116
+ function lastMatch(pattern, text) {
117
+ let found = null;
118
+ for (const match of text.matchAll(pattern)) found = match;
119
+ return found;
120
+ }
121
+
122
+ /** What changed since the agent last saw the job: growth, last line, any
123
+ * visible percent/ETA. Used where a still-running job is reported. */
124
+ function progressDigest(growth) {
125
+ const bytes = growth.reduce((sum, entry) => sum + entry.bytes, 0);
126
+ const lines = growth.reduce((sum, entry) => sum + entry.lines, 0);
127
+ const text = growth
128
+ .map((entry) => entry.text)
129
+ .filter(Boolean)
130
+ .join('\n');
131
+ const parts = [
132
+ bytes > 0 ? `+${formatKb(bytes)}, +${lines} lines since you last saw it` : 'no new output since you last saw it',
133
+ ];
134
+ const last = text
135
+ .split('\n')
136
+ .reverse()
137
+ .find((line) => line.trim());
138
+ if (last) parts.push(`last line: ${JSON.stringify(last.trim().slice(0, 200))}`);
139
+ const percent = lastMatch(PERCENT_RE, text);
140
+ if (percent && Number(percent[1]) <= 100) parts.push(`${percent[1]}%`);
141
+ const eta = lastMatch(ETA_RE, text);
142
+ if (eta) parts.push(`ETA ${eta[1].trim()}`);
143
+ return `[progress] ${parts.join(' · ')} — still running, not an error`;
144
+ }
145
+
146
+ /** A whole finished log as a piece of the delivery (a foreground command whose
147
+ * capture spilled to disk), read with a fresh cursor. */
148
+ export function shellLogPiece(path) {
149
+ const cursor = newCursor();
150
+ let fd;
151
+ try {
152
+ fd = openSync(path, 'r');
153
+ const size = fstatSync(fd).size;
154
+ const label = cursor.sniffer.probe(fd, size, true);
155
+ if (label) cursor.decoder.force(label);
156
+ return readRange(fd, cursor, { start: 0, end: size, label, flush: true, path }).piece;
157
+ } finally {
158
+ if (fd !== undefined) closeSync(fd);
159
+ }
160
+ }
161
+
48
162
  // Cursor ownership follows the authorized task object, not a globally supplied
49
163
  // task id. Bounded synchronous reads make concurrent task reads non-overlapping.
50
164
  // Only bytes actually read advance the cursor; previews never stand in for a
51
165
  // file offset, since a preview may contain an omitted middle.
52
- // Explicit tail reads never change the incremental cursor. A terminal read
53
- // prioritizes the final diagnostics; any skipped bytes remain in the raw file.
54
- export function readShellTaskOutput(task, result = {}, { output = 'new' } = {}) {
166
+ // Explicit tail reads (`output: 'tail'`) never change the incremental cursor.
167
+ // `terminal` flushes the decoders (the producer finished); `digest` prefixes
168
+ // the progress digest for a job reported while still running.
169
+ export function readShellTaskOutput(
170
+ task,
171
+ result = {},
172
+ { output = 'new', terminal = task.status !== 'running', digest = false } = {}
173
+ ) {
55
174
  const replay = output === 'tail';
56
- const terminal = task.status !== 'running';
57
- const limit = terminal || replay ? TOOL_OUTPUT_TAIL_BYTES : TOOL_OUTPUT_PAGE_BYTES;
175
+ const growth = [];
58
176
  let streams = replay ? new Map() : cursors.get(task);
59
177
  if (!streams) {
60
178
  streams = new Map();
61
179
  cursors.set(task, streams);
62
180
  }
181
+ // A note, or a stream's body: stdout and stderr share one delivery budget.
63
182
  const sections = [];
64
183
  for (const stream of ['stdout', 'stderr']) {
65
184
  const path = result[`${stream}_path`];
@@ -73,15 +192,15 @@ export function readShellTaskOutput(task, result = {}, { output = 'new' } = {})
73
192
  if (!path) {
74
193
  const preview = String(result[`${stream}_preview`] || '');
75
194
  const delta = preview.startsWith(cursor.preview) ? preview.slice(cursor.preview.length) : preview;
76
- const bytes = Buffer.from(delta);
77
- const start = utf8Boundary(bytes, Math.max(0, bytes.length - limit), bytes.length);
78
- if (start) {
79
- sections.push(`[${stream}: ${start} preview bytes omitted; no original log path available]`);
80
- cursor.ansi = '';
81
- }
82
- const text = visibleText(cursor, bytes.subarray(start).toString('utf8'));
195
+ const text = prepareShellText(visibleText(cursor, delta), cursor.fold);
83
196
  cursor.preview = preview;
84
- if (text) sections.push(`[${stream}]\n${text}`);
197
+ growth.push({ bytes: Buffer.byteLength(delta), lines: countNewlines(text), text });
198
+ // A preview has no log of its own: a capped one is saved for the marker.
199
+ if (text)
200
+ sections.push({
201
+ prefix: `[${stream}]\n`,
202
+ piece: textPiece(text, { path: () => persistDeliveryLog(text, stream) }),
203
+ });
85
204
  continue;
86
205
  }
87
206
  let fd;
@@ -102,30 +221,37 @@ export function readShellTaskOutput(task, result = {}, { output = 'new' } = {})
102
221
  cursor.decoder.force(label);
103
222
  }
104
223
  const readEnd = label ? size : cursor.sniffer.safeEnd(size);
105
- let start = terminal || replay ? Math.max(cursor.offset, readEnd - limit) : cursor.offset;
106
- if (start > cursor.offset) {
107
- start = Math.min(readEnd, alignedOffset(fd, start, label || 'utf-8'));
108
- sections.push(`[${stream}: ${start - cursor.offset} earlier bytes omitted; read ${path} for the original log]`);
224
+ let start = cursor.offset;
225
+ let earlier = '';
226
+ if (replay && readEnd - start > FULL_READ_BYTES) {
227
+ // A replay shows the recent part; everything before it is counted.
228
+ start = Math.min(readEnd, alignedOffset(fd, readEnd - FULL_READ_BYTES, label || 'utf-8'));
229
+ earlier = omissionMarker({ lines: countNewlinesInRange(fd, 0, start), bytes: start, path });
109
230
  cursor.decoder = new ShellStreamDecoder(oemTextEncoding(), label);
110
- cursor.ansi = '';
111
- }
112
- const buffer = Buffer.alloc(Math.min(limit, Math.max(0, readEnd - start)));
113
- const count = buffer.length ? readSync(fd, buffer, 0, buffer.length, start) : 0;
114
- cursor.offset = start + count;
115
- let decoded = cursor.decoder.write(buffer.subarray(0, count));
116
- if (terminal && cursor.offset === size) decoded += cursor.decoder.end();
117
- const text = visibleText(cursor, decoded);
118
- if (text) sections.push(`[${stream}]\n${text}`);
119
- if (cursor.offset < size) {
120
- sections.push(
121
- `[${stream}: ${size - cursor.offset} unread bytes; task read continues, read ${path} for the original log]`
122
- );
123
231
  }
232
+ const range = readRange(fd, cursor, { start, end: readEnd, label, flush: terminal && readEnd === size, path });
233
+ cursor.offset = readEnd;
234
+ growth.push(range);
235
+ if (range.piece.natural)
236
+ sections.push({ prefix: `[${stream}]\n${earlier ? `${earlier}\n` : ''}`, piece: range.piece });
124
237
  } catch (error) {
125
238
  sections.push(`[${stream} read error: ${error.message}]`);
126
239
  } finally {
127
240
  if (fd !== undefined) closeSync(fd);
128
241
  }
129
242
  }
130
- return sections.join('\n\n') || (replay ? '(no output)' : '(no new output)');
243
+ // Notes and prefixes come off the budget; the streams split what is left.
244
+ const fixed = sections.reduce(
245
+ (sum, item) => sum + (item.prefix ?? item).length,
246
+ 2 * Math.max(0, sections.length - 1)
247
+ );
248
+ const pieces = sections.filter((item) => item.piece);
249
+ const bodies = fitPieces(
250
+ pieces.map((item) => item.piece),
251
+ DELIVERY_FULL_CHARS - fixed
252
+ );
253
+ const body =
254
+ sections.map((item) => (item.piece ? `${item.prefix}${bodies[pieces.indexOf(item)]}` : item)).join('\n\n') ||
255
+ (replay ? '(no output)' : '(no new output)');
256
+ return escapeHarnessTags(digest ? `${progressDigest(growth)}\n\n${body}` : body);
131
257
  }
@@ -6,10 +6,16 @@ import { markScopedCacheIncomplete, markScopedCacheUnsafe } from '../../session/
6
6
  import { cacheGet, cacheSet, lstatPathsForMtime } from './cache-layers.mjs';
7
7
  import { runScopedSearchInFlight } from './lib/scoped-search-flight.mjs';
8
8
  import { NOISE_DIR_NAMES, walkDir } from './glob-walk.mjs';
9
- import { TOOL_OUTPUT_MAX_BYTES } from './tool-output-limit.mjs';
9
+ import { TOOL_OUTPUT_MAX_BYTES, capMarker } from './tool-output-limit.mjs';
10
10
  import { runRgWindowedLines } from './native-search-runner.mjs';
11
11
  import { tryServeListMetadata } from './native-search-client.mjs';
12
- import { countTextFileLines, guardedWalkRoot, normalizeListHeadLimit, pageContinuationLine } from './lib/list-helpers.mjs';
12
+ import {
13
+ countTextFileLines,
14
+ guardedWalkRoot,
15
+ comparePathsStable,
16
+ normalizeListHeadLimit,
17
+ pageContinuationLine,
18
+ } from './lib/list-helpers.mjs';
13
19
  import { reportToolProgress } from './lib/tool-progress.mjs';
14
20
  import { displayRelPath, statWalkRoot } from './list-tool-shared.mjs';
15
21
  import { executeFindFilesTool } from './find-files-tool.mjs';
@@ -338,9 +344,9 @@ async function fillRowMetadata(rows, workDir, options) {
338
344
  }
339
345
 
340
346
  function sortListRows(rows, sort) {
341
- if (sort === 'mtime') rows.sort((a, b) => b.mtimeMs - a.mtimeMs);
342
- else if (sort === 'size') rows.sort((a, b) => b.size - a.size);
343
- else rows.sort((a, b) => a.path.localeCompare(b.path));
347
+ if (sort === 'mtime') rows.sort((a, b) => b.mtimeMs - a.mtimeMs || comparePathsStable(a.path, b.path));
348
+ else if (sort === 'size') rows.sort((a, b) => b.size - a.size || comparePathsStable(a.path, b.path));
349
+ else rows.sort((a, b) => a.path.localeCompare(b.path) || comparePathsStable(a.path, b.path));
344
350
  }
345
351
 
346
352
  // What an empty page means: offset past the end is a windowing condition,
@@ -390,15 +396,17 @@ async function scanList(request, cacheKey, workDir, options) {
390
396
  if (root.error) return root.error;
391
397
  if (!root.stat.isDirectory()) {
392
398
  if (!root.stat.isFile()) return `Error: not a directory — ${normalizeOutputPath(fullPath)}`;
393
- const out = listFileLine(fullPath, root.stat, meta, meta ? await countTextFileLines(fullPath, root.stat.size) : null);
399
+ const out = listFileLine(
400
+ fullPath,
401
+ root.stat,
402
+ meta,
403
+ meta ? await countTextFileLines(fullPath, root.stat.size) : null
404
+ );
394
405
  cacheSet(cacheKey, out, { scopes: [fullPath] });
395
406
  return out;
396
407
  }
397
- const { rows, walkResult, walkWarnings, truncatedByCap, nativeDeep, cacheSafe, nativeMetaMissing } = await collectListRows(
398
- request,
399
- workDir,
400
- options
401
- );
408
+ const { rows, walkResult, walkWarnings, truncatedByCap, nativeDeep, cacheSafe, nativeMetaMissing } =
409
+ await collectListRows(request, workDir, options);
402
410
  const rootFailure = walkRootFailureLine(options, walkResult, walkWarnings, 'list walk');
403
411
  if (rootFailure) return rootFailure;
404
412
  if (!nativeDeep && needsGlobalStat) await fillRowMetadata(rows, workDir, options);
@@ -439,7 +447,11 @@ async function renderListPage({ request, rows, walkWarnings, truncatedByCap, nat
439
447
  if (paged) lines.push(pageContinuationLine(offset, sliced.length, rows.length));
440
448
  if (truncatedByCap) {
441
449
  lines.push(
442
- `... walk truncated at ${LIST_ABSOLUTE_CAP} rows or ${LIST_WALK_TIMEOUT_MS}ms timeout; narrow the path or lower depth for a complete listing`
450
+ capMarker({
451
+ limit: `${LIST_ABSOLUTE_CAP} rows or ${LIST_WALK_TIMEOUT_MS}ms`,
452
+ shown: 'listing incomplete',
453
+ narrow: 'narrow the path or lower depth',
454
+ })
443
455
  );
444
456
  }
445
457
  for (const warning of walkWarnings) lines.push(directoryReadFailureLine(warning, true));
@@ -516,7 +528,7 @@ function renderTreeOutput(lines, walkWarnings, { headLimit, offset }) {
516
528
  outputCharTruncated = true;
517
529
  out =
518
530
  out.slice(0, TOOL_OUTPUT_MAX_BYTES) +
519
- `\n... [output truncated at ${Math.round(TOOL_OUTPUT_MAX_BYTES / 1024)} KB; narrow path or lower depth]`;
531
+ `\n${capMarker({ limit: TOOL_OUTPUT_MAX_BYTES, narrow: 'narrow path or lower depth' })}`;
520
532
  }
521
533
  return { out, outputCharTruncated };
522
534
  }
@@ -1,4 +1,5 @@
1
1
  import { READ_BATCH_RANGE_COALESCE_GAP_LINES } from './read-constants.mjs';
2
+ import { windowMarker } from './tool-output-limit.mjs';
2
3
 
3
4
  // Slices a `read` body (full mode) to the line window [origOffset+1,
4
5
  // origOffset+origLimit] (1-based line numbers in the body). Lines whose
@@ -81,15 +82,15 @@ export function sliceReadBodyByLines(body, origOffset, origLimit, readOffsetBase
81
82
  else if (finiteLast) requestedLast = lastLine;
82
83
  const emittedStart = keptFirst ?? firstLine;
83
84
  const emittedLast = keptLast !== null ? Math.min(requestedLast, keptLast) : requestedLast;
84
- const totalPart = haveTotal ? ` of ${totalNum}` : '';
85
85
  const moreToRead = haveTotal ? emittedLast < totalNum : finiteLast;
86
86
  // Report the next caller coordinate without prescribing another read;
87
87
  // whatever remains fits one wider read, not a walk window by window.
88
- let continuationPart = '';
89
- if (moreToRead && Number.isFinite(emittedLast)) {
90
- continuationPart = `; pass offset:${emittedLast + readOffsetBase} to continue`;
91
- }
92
- const newFooter = `[lines ${emittedStart}-${emittedLast}${totalPart}${continuationPart}]`;
88
+ const newFooter = windowMarker({
89
+ from: emittedStart,
90
+ to: emittedLast,
91
+ total: haveTotal ? totalNum : null,
92
+ nextOffset: moreToRead && Number.isFinite(emittedLast) ? emittedLast + readOffsetBase : null,
93
+ });
93
94
  return kept.join('\n') + (kept.length ? '\n' : '') + newFooter;
94
95
  }
95
96
 
@@ -1,6 +1,6 @@
1
1
  import { READ_LINE_NO_SEP } from '../../../../shared/read-row-numbers.mjs';
2
2
  import { mergeReadRanges } from './read-ranges.mjs';
3
- import { TOOL_OUTPUT_MAX_BYTES, positiveEnvInt } from './tool-output-limit.mjs';
3
+ import { TOOL_OUTPUT_MAX_BYTES, capMarker, positiveEnvInt } from './tool-output-limit.mjs';
4
4
 
5
5
  // Smart-truncate cap: a no-window read returns the file until this cap, past
6
6
  // which head+tail are shown and the model pages the rest with offset (footer
@@ -18,9 +18,23 @@ const READ_MAX_RENDERED_LINE_CHARS = 2_000;
18
18
  // (runtime/shared/read-row-numbers.mjs).
19
19
  export const LINE_NO_SEP = READ_LINE_NO_SEP;
20
20
 
21
- export function buildSmartReadTruncationMarker(totalLines, fileBytes) {
21
+ // Cap marker for the elided middle of a no-window read. With the omitted
22
+ // line span it names the continuation offset; without, narrowing advice.
23
+ export function buildSmartReadTruncationMarker(totalLines, fileBytes, omitted = null) {
22
24
  const kb = Math.max(1, Math.round((Number(fileBytes) || 0) / 1024));
23
- return `... [TRUNCATED - ${totalLines} lines / ${kb} KB; read located windows with {file_path, offset, limit}] ...`;
25
+ const limit = fileBytes > SMART_READ_MAX_BYTES ? SMART_READ_MAX_BYTES : `${SMART_READ_MAX_LINES} lines`;
26
+ if (!omitted) {
27
+ return capMarker({
28
+ limit,
29
+ shown: `${totalLines} lines / ${kb} KB`,
30
+ narrow: 'read located windows with {file_path, offset, limit}',
31
+ });
32
+ }
33
+ return capMarker({
34
+ limit,
35
+ shown: `lines ${omitted.from}-${omitted.to} of ${totalLines} omitted (${kb} KB file)`,
36
+ nextOffset: omitted.from,
37
+ });
24
38
  }
25
39
 
26
40
  function rangeFromRenderedReadRows(rows, fallbackStartLine = 1) {
@@ -56,9 +70,10 @@ export function smartReadTruncate(renderedWithLineNos, totalLines, fileBytes) {
56
70
  const tailRows = rows.slice(tailStart);
57
71
  const head = headRows.join('\n');
58
72
  const tail = tailRows.join('\n');
59
- const marker = buildSmartReadTruncationMarker(totalLines, fileBytes);
73
+ const marker = buildSmartReadTruncationMarker(totalLines, fileBytes, { from: headCount + 1, to: tailStart });
60
74
  return {
61
75
  text: `${head}\n${marker}\n${tail}`,
76
+ marker,
62
77
  truncated: true,
63
78
  totalLines,
64
79
  ranges: mergeReadRanges(