mixdog 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (285) hide show
  1. package/README.md +47 -23
  2. package/package.json +33 -27
  3. package/scripts/_test-folder-dialog.mjs +30 -0
  4. package/scripts/agent-parallel-smoke.mjs +388 -0
  5. package/scripts/agent-tag-reuse-smoke.mjs +183 -0
  6. package/scripts/background-task-meta-smoke.mjs +38 -0
  7. package/scripts/boot-smoke.mjs +52 -9
  8. package/scripts/build-runtime-linux.sh +348 -0
  9. package/scripts/build-runtime-macos.sh +217 -0
  10. package/scripts/build-runtime-windows.ps1 +242 -0
  11. package/scripts/compact-active-turn-test.mjs +68 -0
  12. package/scripts/compact-smoke.mjs +859 -129
  13. package/scripts/compact-trigger-migration-smoke.mjs +187 -0
  14. package/scripts/fix-brief-fn.mjs +35 -0
  15. package/scripts/fix-format-tool-surface.mjs +24 -0
  16. package/scripts/fix-tool-exec-visible.mjs +42 -0
  17. package/scripts/generate-runtime-manifest.mjs +166 -0
  18. package/scripts/hook-bus-test.mjs +330 -0
  19. package/scripts/lead-workflow-smoke.mjs +33 -39
  20. package/scripts/live-worker-smoke.mjs +43 -37
  21. package/scripts/llm-trace-summary.mjs +315 -0
  22. package/scripts/memory-meta-concurrency-test.mjs +20 -0
  23. package/scripts/output-style-smoke.mjs +56 -15
  24. package/scripts/parent-abort-link-test.mjs +44 -0
  25. package/scripts/patch-agent-brief.mjs +48 -0
  26. package/scripts/patch-app.mjs +21 -0
  27. package/scripts/patch-app2.mjs +18 -0
  28. package/scripts/patch-dist-brief.mjs +96 -0
  29. package/scripts/patch-tool-exec.mjs +70 -0
  30. package/scripts/pretool-ask-runtime-test.mjs +54 -0
  31. package/scripts/provider-toolcall-test.mjs +376 -0
  32. package/scripts/reactive-compact-persist-smoke.mjs +124 -0
  33. package/scripts/sanitize-tool-pairs-test.mjs +260 -0
  34. package/scripts/session-context-bench.mjs +344 -0
  35. package/scripts/session-ingest-smoke.mjs +177 -0
  36. package/scripts/set-effort-config-test.mjs +41 -0
  37. package/scripts/smoke-runtime-negative.ps1 +106 -0
  38. package/scripts/smoke-runtime-negative.sh +97 -0
  39. package/scripts/smoke.mjs +25 -0
  40. package/scripts/tool-result-hook-test.mjs +48 -0
  41. package/scripts/tool-smoke.mjs +1223 -95
  42. package/scripts/toolcall-args-test.mjs +150 -0
  43. package/scripts/tui-background-failure-smoke.mjs +73 -0
  44. package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
  45. package/src/agents/debugger/AGENT.md +8 -0
  46. package/src/agents/explore/AGENT.md +4 -0
  47. package/src/agents/heavy-worker/AGENT.md +9 -3
  48. package/src/agents/maintainer/AGENT.md +4 -0
  49. package/src/agents/reviewer/AGENT.md +8 -0
  50. package/src/agents/scheduler-task/AGENT.md +12 -0
  51. package/src/agents/scheduler-task/agent.json +6 -0
  52. package/src/agents/webhook-handler/AGENT.md +12 -0
  53. package/src/agents/webhook-handler/agent.json +6 -0
  54. package/src/agents/worker/AGENT.md +9 -3
  55. package/src/app.mjs +77 -3
  56. package/src/defaults/hidden-roles.json +17 -12
  57. package/src/headless-role.mjs +117 -0
  58. package/src/help.mjs +30 -0
  59. package/src/hooks/lib/permission-evaluator.cjs +11 -475
  60. package/src/lib/keychain-cjs.cjs +9 -1
  61. package/src/lib/mixdog-debug.cjs +0 -7
  62. package/src/lib/rules-builder.cjs +240 -96
  63. package/src/lib/text-utils.cjs +1 -1
  64. package/src/mixdog-session-runtime.mjs +2325 -450
  65. package/src/output-styles/default.md +12 -28
  66. package/src/output-styles/extreme-simple.md +9 -6
  67. package/src/output-styles/simple.md +22 -9
  68. package/src/repl.mjs +118 -59
  69. package/src/rules/agent/00-common.md +15 -0
  70. package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
  71. package/src/rules/agent/30-explorer.md +22 -0
  72. package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
  73. package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
  74. package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
  75. package/src/rules/lead/01-general.md +9 -5
  76. package/src/rules/lead/04-workflow.md +51 -12
  77. package/src/rules/lead/lead-tool.md +6 -0
  78. package/src/rules/shared/01-tool.md +12 -1
  79. package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
  80. package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
  81. package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
  82. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
  83. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
  84. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
  85. package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
  86. package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
  87. package/src/runtime/agent/orchestrator/config.mjs +174 -55
  88. package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
  89. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
  90. package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
  91. package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
  92. package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
  93. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
  94. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +377 -243
  95. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -93
  96. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
  97. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
  98. package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
  99. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
  100. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
  101. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
  102. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
  103. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
  104. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
  106. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +221 -70
  107. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
  109. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
  110. package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
  111. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +31 -11
  112. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
  113. package/src/runtime/agent/orchestrator/session/cache/post-edit-marks.mjs +4 -4
  114. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +1 -1
  115. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  116. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  117. package/src/runtime/agent/orchestrator/session/loop.mjs +851 -674
  118. package/src/runtime/agent/orchestrator/session/manager.mjs +1593 -466
  119. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  120. package/src/runtime/agent/orchestrator/session/store.mjs +291 -46
  121. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  122. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  123. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  124. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +34 -7
  125. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
  126. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
  127. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
  128. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  129. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  130. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  131. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  132. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  133. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  137. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  138. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  139. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  140. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  141. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  142. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  143. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  144. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +1 -1
  145. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  146. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  147. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  148. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  149. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  150. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  151. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  152. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  153. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  154. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  155. package/src/runtime/channels/index.mjs +152 -24
  156. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  157. package/src/runtime/channels/lib/session-discovery.mjs +3 -2
  158. package/src/runtime/channels/lib/tool-format.mjs +0 -2
  159. package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
  160. package/src/runtime/channels/lib/webhook.mjs +1 -1
  161. package/src/runtime/channels/tool-defs.mjs +29 -29
  162. package/src/runtime/memory/index.mjs +635 -107
  163. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  164. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  165. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  166. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  167. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  168. package/src/runtime/memory/lib/memory-cycle1.mjs +38 -17
  169. package/src/runtime/memory/lib/memory-cycle2.mjs +6 -7
  170. package/src/runtime/memory/lib/memory-cycle3.mjs +4 -4
  171. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
  172. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  173. package/src/runtime/memory/lib/memory.mjs +88 -9
  174. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  175. package/src/runtime/memory/lib/pg/adapter.mjs +15 -1
  176. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  177. package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
  178. package/src/runtime/memory/lib/session-ingest.mjs +194 -0
  179. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  180. package/src/runtime/memory/tool-defs.mjs +46 -37
  181. package/src/runtime/search/index.mjs +102 -466
  182. package/src/runtime/search/lib/web-tools.mjs +45 -25
  183. package/src/runtime/search/tool-defs.mjs +16 -23
  184. package/src/runtime/shared/abort-controller.mjs +1 -1
  185. package/src/runtime/shared/atomic-file.mjs +4 -3
  186. package/src/runtime/shared/background-tasks.mjs +122 -11
  187. package/src/runtime/shared/child-spawn-gate.mjs +145 -0
  188. package/src/runtime/shared/config.mjs +7 -4
  189. package/src/runtime/shared/err-text.mjs +131 -4
  190. package/src/runtime/shared/llm/cost.mjs +2 -2
  191. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  192. package/src/runtime/shared/llm/index.mjs +34 -11
  193. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  194. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  195. package/src/runtime/shared/singleton-owner.mjs +104 -0
  196. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  197. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  198. package/src/runtime/shared/tool-surface.mjs +624 -98
  199. package/src/runtime/shared/user-data-guard.mjs +0 -2
  200. package/src/standalone/agent-task-status.mjs +203 -0
  201. package/src/standalone/agent-task-status.test.mjs +76 -0
  202. package/src/standalone/agent-tool.mjs +1913 -0
  203. package/src/standalone/channel-worker.mjs +370 -14
  204. package/src/standalone/explore-tool.mjs +165 -70
  205. package/src/standalone/folder-dialog.mjs +314 -0
  206. package/src/standalone/hook-bus.mjs +898 -22
  207. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  208. package/src/standalone/projects.mjs +226 -0
  209. package/src/standalone/provider-admin.mjs +41 -24
  210. package/src/standalone/seeds.mjs +2 -69
  211. package/src/standalone/usage-dashboard.mjs +96 -8
  212. package/src/tui/App.jsx +4798 -2153
  213. package/src/tui/components/AnsiText.jsx +39 -28
  214. package/src/tui/components/ContextPanel.jsx +87 -29
  215. package/src/tui/components/Markdown.jsx +43 -77
  216. package/src/tui/components/MarkdownTable.jsx +9 -184
  217. package/src/tui/components/Message.jsx +28 -11
  218. package/src/tui/components/Picker.jsx +95 -56
  219. package/src/tui/components/PromptInput.jsx +367 -239
  220. package/src/tui/components/QueuedCommands.jsx +1 -1
  221. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  222. package/src/tui/components/Spinner.jsx +67 -38
  223. package/src/tui/components/StatusLine.jsx +606 -38
  224. package/src/tui/components/TextEntryPanel.jsx +128 -9
  225. package/src/tui/components/ToolExecution.jsx +617 -368
  226. package/src/tui/components/TurnDone.jsx +3 -3
  227. package/src/tui/components/UsagePanel.jsx +3 -5
  228. package/src/tui/components/tool-output-format.mjs +365 -0
  229. package/src/tui/components/tool-output-format.test.mjs +220 -0
  230. package/src/tui/dist/index.mjs +8915 -2418
  231. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  232. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  233. package/src/tui/engine.mjs +1455 -279
  234. package/src/tui/figures.mjs +21 -40
  235. package/src/tui/index.jsx +75 -31
  236. package/src/tui/input-editing.mjs +25 -0
  237. package/src/tui/markdown/format-token.mjs +511 -68
  238. package/src/tui/markdown/format-token.test.mjs +216 -0
  239. package/src/tui/markdown/render-ansi.mjs +94 -0
  240. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  241. package/src/tui/markdown/stream-fence.mjs +34 -0
  242. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  243. package/src/tui/markdown/table-layout.mjs +250 -0
  244. package/src/tui/paste-attachments.mjs +0 -7
  245. package/src/tui/spinner-verbs.mjs +1 -2
  246. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  247. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  248. package/src/tui/theme.mjs +746 -24
  249. package/src/tui/time-format.mjs +1 -1
  250. package/src/tui/transcript-tool-failures.mjs +67 -0
  251. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  252. package/src/ui/ansi.mjs +1 -2
  253. package/src/ui/markdown.mjs +85 -26
  254. package/src/ui/markdown.test.mjs +70 -0
  255. package/src/ui/model-display.mjs +121 -0
  256. package/src/ui/session-stats.mjs +44 -0
  257. package/src/ui/statusline-context-label.test.mjs +15 -0
  258. package/src/ui/statusline.mjs +386 -178
  259. package/src/ui/tool-card.mjs +3 -16
  260. package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
  261. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  262. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  263. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  264. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  265. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  266. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  267. package/src/workflows/default/WORKFLOW.md +12 -5
  268. package/src/workflows/default/workflow.json +0 -1
  269. package/src/workflows/solo/WORKFLOW.md +15 -0
  270. package/src/workflows/solo/workflow.json +7 -0
  271. package/vendor/ink/build/output.js +6 -1
  272. package/src/agents/scheduler-task.md +0 -3
  273. package/src/agents/web-researcher/AGENT.md +0 -3
  274. package/src/agents/web-researcher/agent.json +0 -6
  275. package/src/agents/webhook-handler.md +0 -3
  276. package/src/rules/bridge/00-common.md +0 -5
  277. package/src/rules/bridge/30-explorer.md +0 -4
  278. package/src/rules/lead/00-tool-lead.md +0 -5
  279. package/src/rules/shared/00-language.md +0 -3
  280. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  281. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  282. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  283. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  284. package/src/standalone/bridge-tool.mjs +0 -1414
  285. package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
@@ -1,15 +1,105 @@
1
- import { spawn, execFileSync } from 'child_process';
1
+ import { spawn, execFile } from 'child_process';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
- import { startChildGuardian } from '../../../../shared/child-guardian.mjs';
4
+ import { promisify } from 'node:util';
5
+ import os from 'node:os';
6
+ import { acquire as acquireChildSpawnSlot } from '../../../../shared/child-spawn-gate.mjs';
7
+
8
+ const execFileAsync = promisify(execFile);
9
+
10
+ // ── rg orphan-reap policy (accepted risk) ────────────────────────────────
11
+ // The previous design started a 1:1 node child-guardian per rg spawn so a
12
+ // hard daemon kill (SIGKILL, no chance to run our own cleanup) could not leave
13
+ // an rg orphaned. That per-process guardian is exactly what over-saturated the
14
+ // box (each grep doubled the process count), so it has been REMOVED here and
15
+ // deliberately NOT replaced with another per-rg guardian.
16
+ //
17
+ // Accepted residual risk: on a hard daemon SIGKILL an in-flight rg can briefly
18
+ // outlive the daemon. This is judged acceptable because (a) on every NORMAL
19
+ // path rg is reaped by this module's own timeout → SIGTERM → grace → force-kill
20
+ // plus the force-settle backstop, so it is always collected when the daemon is
21
+ // alive to settle the promise; (b) rg is a short-lived, self-terminating
22
+ // (bounded by the 20s timeout) read-only process, not a long-running server; a
23
+ // stray instance exits on its own shortly after. A shared/OS-job-based reaper
24
+ // (one guardian for the whole daemon, or a Windows Job Object / POSIX process
25
+ // group kill on daemon teardown) is the right long-term fix but is out of scope
26
+ // for this change and intentionally not added per-spawn.
27
+
28
+ // ── rg --threads cap ─────────────────────────────────────────────────────
29
+ // Each rg process otherwise fans out across EVERY core; with several agents
30
+ // running grep at once the box over-saturates and the whole batch trips the
31
+ // 20s deadline together. Cap rg's worker threads to a fraction of the host so
32
+ // concurrent greps share the machine. Internal dynamic default; env override
33
+ // only — deliberately NOT exposed on any tool schema / parameter surface.
34
+ function _rgThreadCap() {
35
+ const override = Number(process.env.MIXDOG_RG_THREADS);
36
+ if (Number.isFinite(override) && override >= 1) return Math.floor(override);
37
+ let cpus = 0;
38
+ try { cpus = os.cpus()?.length || 0; } catch { cpus = 0; }
39
+ return Math.max(2, Math.ceil((cpus || 4) / 4));
40
+ }
41
+
42
+ // Single source of truth for "did the caller already pin rg's thread count?".
43
+ // Detects every form rg accepts: separated (`-j`, `--threads`) where the count
44
+ // is the NEXT arg, short-attached (`-j8`), and long-attached (`--threads=8`).
45
+ // Used by both the _withRgThreads injection guard and the EAGAIN retry guard
46
+ // so the two never disagree.
47
+ function _hasRgThreadArg(argsList) {
48
+ for (const a of argsList) {
49
+ if (typeof a !== 'string') continue;
50
+ if (a === '-j' || a === '--threads') return true;
51
+ if (/^-j\d+$/.test(a)) return true;
52
+ if (a.startsWith('--threads=')) return true;
53
+ }
54
+ return false;
55
+ }
56
+
57
+ // Inject `--threads N` unless the caller already pinned thread count
58
+ // (`-j`/`--threads`/`-jN`/`--threads=N`, e.g. the EAGAIN `-j 1` retry). Does
59
+ // not mutate; may return the ORIGINAL array unchanged when already
60
+ // thread-pinned (callers must not assume a fresh copy).
61
+ function _withRgThreads(argsList) {
62
+ if (_hasRgThreadArg(argsList)) return argsList;
63
+ return ['--threads', String(_rgThreadCap()), ...argsList];
64
+ }
65
+
66
+ // Build the EAGAIN single-thread retry args. If the caller already pinned a
67
+ // thread count we strip the existing flag(s) first, then prepend `-j 1`, so the
68
+ // retry is unambiguously single-threaded (never `-j 8 ... -j 1`). Drops both the
69
+ // separated form (`-j N` / `--threads N` → flag + following count) and the
70
+ // attached forms (`-jN` / `--threads=N`).
71
+ function _rgEagainRetryArgs(argsList) {
72
+ const out = [];
73
+ for (let i = 0; i < argsList.length; i++) {
74
+ const a = argsList[i];
75
+ if (a === '-j' || a === '--threads') { i++; continue; } // skip flag + its count
76
+ if (typeof a === 'string' && (/^-j\d+$/.test(a) || a.startsWith('--threads='))) continue;
77
+ out.push(a);
78
+ }
79
+ return ['-j', '1', ...out];
80
+ }
81
+
82
+ // True only when the args are ALREADY exactly single-threaded — i.e. an EAGAIN
83
+ // retry would change nothing. Matches the separated `-j 1` / `--threads 1` form
84
+ // and the attached `-j1` / `--threads=1` form. Any other thread pin (e.g. -j8)
85
+ // is NOT "single-thread pinned": the retry should still run and downshift it.
86
+ function _isRgSingleThreadPinned(argsList) {
87
+ for (let i = 0; i < argsList.length; i++) {
88
+ const a = argsList[i];
89
+ if (typeof a !== 'string') continue;
90
+ if ((a === '-j' || a === '--threads') && String(argsList[i + 1]).trim() === '1') return true;
91
+ if (a === '-j1' || a === '--threads=1') return true;
92
+ }
93
+ return false;
94
+ }
5
95
 
6
96
  let _rgExecutableResolved = null;
7
97
 
8
- function _resolveRgExecutable() {
98
+ async function _resolveRgExecutable() {
9
99
  const isWin = process.platform === 'win32';
10
100
  try {
11
101
  const cmd = isWin ? 'where' : 'which';
12
- const out = execFileSync(cmd, ['rg'], { encoding: 'utf8', windowsHide: true }).trim();
102
+ const { stdout: out } = await execFileAsync(cmd, ['rg'], { encoding: 'utf8', windowsHide: true });
13
103
  const first = out.split(/\r?\n/).map((l) => l.trim()).find(Boolean);
14
104
  if (first && existsSync(first)) return first;
15
105
  } catch { /* fall through to PATH scan */ }
@@ -33,11 +123,27 @@ function _resolveRgExecutable() {
33
123
  return 'rg';
34
124
  }
35
125
 
126
+ // Synchronous accessor for the spawn call sites — only valid AFTER
127
+ // ensureRgResolved() has run (runRg/runRgWindowedLines await it first). Falls
128
+ // back to the bare 'rg' if somehow called before resolution.
36
129
  function rgExecutable() {
37
- if (_rgExecutableResolved === null) {
38
- _rgExecutableResolved = _resolveRgExecutable();
130
+ return _rgExecutableResolved ?? 'rg';
131
+ }
132
+
133
+ // Async resolver: the `where`/`which` lookup used to run via execFileSync,
134
+ // blocking the event loop ~90ms on the first grep/glob/find of a session
135
+ // (freezing the TUI). Resolve asynchronously and cache the result; subsequent
136
+ // calls return immediately.
137
+ let _rgResolvePromise = null;
138
+ async function ensureRgResolved() {
139
+ if (_rgExecutableResolved !== null) return _rgExecutableResolved;
140
+ if (!_rgResolvePromise) {
141
+ _rgResolvePromise = _resolveRgExecutable().then((resolved) => {
142
+ _rgExecutableResolved = resolved;
143
+ return resolved;
144
+ });
39
145
  }
40
- return _rgExecutableResolved;
146
+ return _rgResolvePromise;
41
147
  }
42
148
 
43
149
  // When _resolveRgExecutable() exhausts `where`/`which` and the PATH scan and
@@ -48,13 +154,13 @@ function rgExecutable() {
48
154
  const RG_FALLBACK_FAIL_TTL_MS = 30000;
49
155
  let _rgFallbackUsable = null;
50
156
  let _rgFallbackFailAt = 0;
51
- function rgFallbackUsable() {
157
+ async function rgFallbackUsable() {
52
158
  if (_rgFallbackUsable === true) return true;
53
159
  if (_rgFallbackUsable === false && (Date.now() - _rgFallbackFailAt) < RG_FALLBACK_FAIL_TTL_MS) {
54
160
  return false;
55
161
  }
56
162
  try {
57
- execFileSync('rg', ['--version'], { stdio: 'ignore', windowsHide: true, timeout: 3000 });
163
+ await execFileAsync('rg', ['--version'], { windowsHide: true, timeout: 3000 });
58
164
  _rgFallbackUsable = true;
59
165
  } catch {
60
166
  _rgFallbackUsable = false;
@@ -65,10 +171,11 @@ function rgFallbackUsable() {
65
171
 
66
172
  // Throws a clear error only when the bare 'rg' fallback was reached AND it is
67
173
  // not actually runnable. A real resolved path short-circuits with no probe, so
68
- // behavior is unchanged whenever rg exists.
69
- function assertRgAvailable() {
70
- if (rgExecutable() !== 'rg') return;
71
- if (rgFallbackUsable()) return;
174
+ // behavior is unchanged whenever rg exists. Async so the underlying lookups
175
+ // (`where`/`which`, `rg --version`) never block the event loop.
176
+ async function assertRgAvailable() {
177
+ if (await ensureRgResolved() !== 'rg') return;
178
+ if (await rgFallbackUsable()) return;
72
179
  const e = new Error('ripgrep (rg) not found on PATH — install ripgrep or add it to PATH');
73
180
  e.code = 'ERG_NOT_FOUND';
74
181
  throw e;
@@ -168,14 +275,19 @@ function _armRgForceSettle({ timeoutMs, isSettled, timers, proc, onForceSettle }
168
275
  // surfaced as empty stdout so callers can render "(no matches)" uniformly.
169
276
  function spawnRg(argsList, execOptions) {
170
277
  const timeoutMs = Number(execOptions?.timeout ?? 20000);
171
- return new Promise((resolve, reject) => {
172
- const proc = spawn(rgExecutable(), argsList, {
278
+ const gateSignal = execOptions?.abortSignal || execOptions?.signal || null;
279
+ // Gate the spawn (not the whole call) so over-saturation queues here while
280
+ // the cap's worth of rg keep running. The rg timeout timer is armed AFTER
281
+ // spawn below, so queue-wait time is excluded from the 20s deadline. No
282
+ // node guardian is started: rg owns its full SIGTERM→grace→force-kill +
283
+ // force-settle teardown, so the 1:1 guardian process was pure overhead.
284
+ return acquireChildSpawnSlot(gateSignal).then((releaseSlot) => new Promise((resolve, reject) => {
285
+ const proc = spawn(rgExecutable(), _withRgThreads(argsList), {
173
286
  cwd: execOptions?.cwd,
174
287
  env: execOptions?.env || process.env,
175
288
  windowsHide: true,
176
289
  stdio: ['ignore', 'pipe', 'pipe'],
177
290
  });
178
- startChildGuardian({ childPid: proc.pid, label: 'rg-runner' });
179
291
  let stdout = '';
180
292
  let stderr = '';
181
293
  let timedOut = false;
@@ -282,17 +394,23 @@ function spawnRg(argsList, execOptions) {
282
394
  e.stderr = stderr;
283
395
  reject(e);
284
396
  });
285
- });
397
+ // Release the gate slot exactly once on ANY settle path (resolve/reject:
398
+ // close, error, or force-settle). releaseSlot is idempotent.
399
+ }).finally(() => releaseSlot()));
286
400
  }
287
401
 
288
402
  export async function runRg(argsList, execOptions = {}) {
289
- assertRgAvailable();
403
+ await assertRgAvailable();
290
404
  try {
291
405
  return await spawnRg(argsList, execOptions);
292
406
  } catch (err) {
293
407
  const msg = String(err?.message || err?.stderr || '');
294
- if (/EAGAIN/i.test(msg) && !argsList.includes('-j')) {
295
- return spawnRg(['-j', '1', ...argsList], execOptions);
408
+ // Retry single-threaded on EAGAIN. Skip only when the caller already
409
+ // pinned threads AND that pin is already -j 1 (nothing to gain); any
410
+ // other pin (-j8/--threads=N) is rewritten to a clean -j 1 by
411
+ // _rgEagainRetryArgs so we never emit conflicting thread flags.
412
+ if (/EAGAIN/i.test(msg) && !_isRgSingleThreadPinned(argsList)) {
413
+ return spawnRg(_rgEagainRetryArgs(argsList), execOptions);
296
414
  }
297
415
  throw err;
298
416
  }
@@ -304,14 +422,17 @@ function spawnRgWindowedLines(argsList, execOptions, opts = {}) {
304
422
  const lineLimit = Number.isFinite(opts.limit) ? Math.max(0, Math.floor(Number(opts.limit) || 0)) : Infinity;
305
423
  const summaryLimit = Math.max(0, Math.floor(Number(opts.summaryLimit) || 0));
306
424
  const collectLimit = lineLimit === Infinity ? Infinity : Math.max(lineLimit, summaryLimit);
307
- return new Promise((resolve, reject) => {
308
- const proc = spawn(rgExecutable(), argsList, {
425
+ const gateSignal = execOptions?.abortSignal || execOptions?.signal || null;
426
+ // Same gate + thread-cap + no-guardian treatment as spawnRg; queue-wait is
427
+ // excluded from the rg timeout (armed after spawn). releaseSlot is fired
428
+ // once via .finally on every settle path.
429
+ return acquireChildSpawnSlot(gateSignal).then((releaseSlot) => new Promise((resolve, reject) => {
430
+ const proc = spawn(rgExecutable(), _withRgThreads(argsList), {
309
431
  cwd: execOptions?.cwd,
310
432
  env: execOptions?.env || process.env,
311
433
  windowsHide: true,
312
434
  stdio: ['ignore', 'pipe', 'pipe'],
313
435
  });
314
- startChildGuardian({ childPid: proc.pid, label: 'rg-runner-windowed' });
315
436
  let buffer = '';
316
437
  let stderr = '';
317
438
  let skipped = 0;
@@ -448,17 +569,17 @@ function spawnRgWindowedLines(argsList, execOptions, opts = {}) {
448
569
  e.stderr = stderr;
449
570
  reject(e);
450
571
  });
451
- });
572
+ }).finally(() => releaseSlot()));
452
573
  }
453
574
 
454
575
  export async function runRgWindowedLines(argsList, execOptions = {}, opts = {}) {
455
- assertRgAvailable();
576
+ await assertRgAvailable();
456
577
  try {
457
578
  return await spawnRgWindowedLines(argsList, execOptions, opts);
458
579
  } catch (err) {
459
580
  const msg = String(err?.message || err?.stderr || '');
460
- if (/EAGAIN/i.test(msg) && !argsList.includes('-j')) {
461
- return spawnRgWindowedLines(['-j', '1', ...argsList], execOptions, opts);
581
+ if (/EAGAIN/i.test(msg) && !_isRgSingleThreadPinned(argsList)) {
582
+ return spawnRgWindowedLines(_rgEagainRetryArgs(argsList), execOptions, opts);
462
583
  }
463
584
  throw err;
464
585
  }
@@ -114,13 +114,13 @@ export function buildGrepRgArgs(parts) {
114
114
  return rgArgs;
115
115
  }
116
116
 
117
- export function buildGlobCacheKey({ patterns, basePath, headLimit, offset, extraIgnore }) {
117
+ export function buildGlobCacheKey({ patterns, basePath, headLimit, offset, extraIgnore, sort }) {
118
118
  // extraIgnore (rg ignore globs from _extraIgnoreDirs) alters which files
119
119
  // match, so it MUST partake in the key — otherwise calls that differ only
120
120
  // by extra ignores collide and return stale over-/under-filtered results.
121
121
  // Sorted so the same ignore set in any order maps to one key.
122
122
  const extra = Array.isArray(extraIgnore) && extraIgnore.length ? [...extraIgnore].sort().join('\x01') : '';
123
- return ['glob', patterns.join('\x01'), basePath, headLimit ?? '', offset ?? '', extra].join('|');
123
+ return ['glob', patterns.join('\x01'), basePath, headLimit ?? '', offset ?? '', sort || 'natural', extra].join('|');
124
124
  }
125
125
 
126
126
  export function buildListCacheKey(parts) {
@@ -30,11 +30,14 @@ import {
30
30
  import {
31
31
  cacheGet,
32
32
  cacheSet,
33
+ runResultCacheInFlight,
33
34
  statPathsForMtime,
34
35
  } from './cache-layers.mjs';
35
36
  import { recordReadSnapshot } from './read-snapshot-runtime.mjs';
36
37
  import { applyGrepContextLeadPolicy, GREP_CONTEXT_MAX } from './arg-guard.mjs';
37
38
 
39
+ const MIXDOG_GREP_CASE_HINT_PROBE = /^(1|true|yes|on)$/i.test(String(process.env.MIXDOG_GREP_CASE_HINT_PROBE || ''));
40
+
38
41
  function expandLegacyEscapedAlternationPattern(rawPattern) {
39
42
  if (typeof rawPattern !== 'string' || !rawPattern.includes('\\|')) return null;
40
43
  const parts = rawPattern.split('\\|').map((part) => part.trim()).filter(Boolean);
@@ -180,6 +183,14 @@ function splitGlobString(value) {
180
183
  return out;
181
184
  }
182
185
 
186
+ function isRedundantAllFilesGlob(value) {
187
+ const g = canonicalizeGlobSlashes(String(value || '').trim())
188
+ .replace(/^\.\//, '')
189
+ .replace(/^\/+/, '')
190
+ .replace(/\/+$/, '');
191
+ return g === '**/*' || g === '**';
192
+ }
193
+
183
194
  function resolveSearchScope(root, workDir) {
184
195
  return isAbsolute(root) ? resolve(root) : resolveAgainstCwd(root, workDir);
185
196
  }
@@ -226,46 +237,6 @@ function parseGrepCountLine(line) {
226
237
  return { path, count };
227
238
  }
228
239
 
229
- // Per-pattern file-count probe for array grby: runs ONE rg --files-with-matches
230
- // per pattern (reusing the same scope/glob/type/flags as the merged search) so
231
- // the summary line can surface patterns that matched zero files — otherwise the
232
- // merged result hides which member of the array contributed nothing.
233
- async function _perPatternFileCounts({ patterns, searchPath, globPatterns, caseInsensitive, multilineMode, fileType, workDir }) {
234
- const results = await Promise.all(patterns.map(async (pattern) => {
235
- try {
236
- const rgArgs = buildGrepRgArgs({
237
- patterns: [pattern],
238
- searchPath,
239
- globPatterns,
240
- outputMode: 'files_with_matches',
241
- caseInsensitive,
242
- showLineNumbers: false,
243
- beforeN: null,
244
- afterN: null,
245
- contextN: null,
246
- multilineMode,
247
- fileType,
248
- onlyMatching: false,
249
- });
250
- const stdout = await runRg(rgArgs, { cwd: workDir });
251
- // A boxed partial/truncated stdout means the count is not the true
252
- // total — render '?' rather than a misleadingly-low or zero number.
253
- if (stdout && typeof stdout === 'object' && (stdout.partial || stdout.truncated)) return null;
254
- return String(stdout).split('\n').filter(Boolean).length;
255
- } catch {
256
- // Probe threw → count unknown, NOT zero.
257
- return null;
258
- }
259
- }));
260
- // Quote first (so newline/control chars can't inject footer lines), then
261
- // truncate the quoted string to 40 chars — matches the no-match path.
262
- const trunc = (p) => {
263
- const q = JSON.stringify(p);
264
- return q.length > 40 ? `${q.slice(0, 40)}...` : q;
265
- };
266
- return `\n# per-pattern: ${patterns.map((p, i) => `${trunc(p)}=${results[i] === null ? '?' : `${results[i]} files`}`).join(', ')}`;
267
- }
268
-
269
240
  function formatGrepOutput({ windowed, totalWindowed, totalKnown, headLimit, offset, outputMode, patterns: _patterns, beforeN, afterN, contextN, searchPath, grepResolvedPath: _grepResolvedPath, workDir, globPatterns: _globPatterns, fileType: _fileType, filenameOmitted = false, prefix = '', broadAdvisory: _broadAdvisory = true }) {
270
241
  const lines = headLimit === Infinity ? windowed : windowed.slice(0, headLimit);
271
242
  const normalized = lines.map((line) => relativeGrepLine(line, workDir, outputMode === 'files_with_matches', outputMode, filenameOmitted));
@@ -378,7 +349,9 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
378
349
  // ripgrep `--glob` uses forward slashes on all platforms; canonicalize
379
350
  // `\`→`/` (win32 only) so a `**\*.ts` filter matches instead of being
380
351
  // parsed as an escape sequence.
381
- const normalizedGlobPatterns = uniqueStrings(globPatterns.map(canonicalizeGlobSlashes));
352
+ const normalizedGlobPatterns = uniqueStrings(globPatterns
353
+ .map(canonicalizeGlobSlashes)
354
+ .filter((g) => !isRedundantAllFilesGlob(g)));
382
355
 
383
356
  const ALLOWED_OUTPUT_MODES = new Set(['files_with_matches', 'content', 'count']);
384
357
  const rawOutputMode = typeof args.output_mode === 'string' ? args.output_mode.trim() : '';
@@ -484,6 +457,7 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
484
457
  // drift detection. So only the fresh-compute path (below) records a read.
485
458
  if (cached !== null) return cached;
486
459
 
460
+ return await runResultCacheInFlight(cacheKey, async () => {
487
461
  let grepStat;
488
462
  try { grepStat = statSync(grepResolvedPath); }
489
463
  catch (err) {
@@ -597,7 +571,7 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
597
571
  // pre-offset matches) just means the window skipped past real
598
572
  // case-sensitive hits, so the hint would be misleading.
599
573
  const trueZeroMatch = offset === 0 && totalWindowed === 0;
600
- if (trueZeroMatch && !caseInsensitive && patterns.length === 1 && /[A-Za-z]/.test(patterns[0])) {
574
+ if (MIXDOG_GREP_CASE_HINT_PROBE && trueZeroMatch && !caseInsensitive && patterns.length === 1 && /[A-Za-z]/.test(patterns[0])) {
601
575
  try {
602
576
  const probeArgs = buildGrepRgArgs({
603
577
  patterns,
@@ -620,21 +594,7 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
620
594
  } catch { /* best-effort hint */ }
621
595
  }
622
596
  }
623
- // Array-pattern visibility: append a per-pattern file-count summary so
624
- // zero-hit members of the merged result are not silently hidden.
625
- let perPatternSummary = '';
626
- if (patterns.length > 1) {
627
- perPatternSummary = await _perPatternFileCounts({
628
- patterns,
629
- searchPath,
630
- globPatterns: normalizedGlobPatterns,
631
- caseInsensitive,
632
- multilineMode,
633
- fileType,
634
- workDir: rgSpawnCwd,
635
- });
636
- }
637
- const out = body + rgPartialSuffix + perPatternSummary;
597
+ const out = body + rgPartialSuffix;
638
598
  const shownLines = headLimit === Infinity ? windowed : windowed.slice(0, headLimit);
639
599
  const remaining = Math.max(0, totalWindowed - shownLines.length);
640
600
  // Mirrors formatGrepOutput truncation / totalKnown semantics.
@@ -749,6 +709,7 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
749
709
  const msg = stderr || err?.message || String(err);
750
710
  return `Error: ${msg.slice(0, 500)}`;
751
711
  }
712
+ });
752
713
  }
753
714
 
754
715
  export async function executeGlobTool(args, workDir, options = {}) {
@@ -835,6 +796,8 @@ export async function executeGlobTool(args, workDir, options = {}) {
835
796
  return `Error: invalid offset ${JSON.stringify(args.offset)}; expected a non-negative integer`;
836
797
  }
837
798
  const offset = offsetCoerced === null || offsetCoerced === 0 ? 0 : offsetCoerced;
799
+ const rawSort = typeof args.sort === 'string' ? args.sort.trim() : '';
800
+ const sortMode = rawSort === 'mtime' ? 'mtime' : 'natural';
838
801
  // Internal-only ignore extension (see normalizeGlobArgs). Caller (e.g.
839
802
  // ai-wrapped-dispatch broad-cwd preflight) appends basename ignore globs
840
803
  // so head_limit bounds SOURCE entries rather than artifact noise.
@@ -860,10 +823,11 @@ export async function executeGlobTool(args, workDir, options = {}) {
860
823
  .map((root) => normalizeOutputPath(resolvedForSearchRoot(root)))
861
824
  .sort()
862
825
  .join('\x01');
863
- const cacheKey = buildGlobCacheKey({ patterns, basePath: cacheBasePath, headLimit, offset, extraIgnore: extraIgnoreGlobs });
826
+ const cacheKey = buildGlobCacheKey({ patterns, basePath: cacheBasePath, headLimit, offset, extraIgnore: extraIgnoreGlobs, sort: sortMode });
864
827
  const cached = cacheGet(cacheKey);
865
828
  if (cached !== null) return cached;
866
829
 
830
+ return await runResultCacheInFlight(cacheKey, async () => {
867
831
  const globGroups = [...groups.entries()];
868
832
 
869
833
  const allFiles = [];
@@ -929,23 +893,26 @@ export async function executeGlobTool(args, workDir, options = {}) {
929
893
  }
930
894
 
931
895
  const unique = Array.from(new Set(allFiles));
932
- // Bound the post-rg stat phase: a single hung stat (dead mount /
933
- // unresponsive network path) must not pin glob until the 600s bridge stall
934
- // watchdog. Per-stat 5s deadline a hung entry is treated as stat-failed
935
- // and dropped, while normal local stats (sub-ms) are unaffected.
936
- const withStatAll = await statPathsForMtime(unique, workDir, 64, { deadlineMs: 5000 });
937
- const withStat = withStatAll.filter((entry) => entry?.stat != null);
938
- withStat.sort((a, b) => {
939
- const dm = b.mtime - a.mtime;
940
- if (dm !== 0) return dm;
941
- return globMtimeTiePath(a).localeCompare(globMtimeTiePath(b));
942
- });
943
- const totalBeforeOffset = withStat.length;
944
- const windowed = offset > 0 ? withStat.slice(offset) : withStat;
945
- const capped = (headLimit === Infinity ? windowed : windowed.slice(0, headLimit)).map((entry) => {
946
- const abs = entry.full || resolveAgainstCwd(entry.path, workDir);
947
- return relativeSearchResultPath(abs, workDir);
948
- });
896
+ let orderedPaths;
897
+ if (sortMode === 'mtime') {
898
+ // Opt-in mtime sorting is intentionally slower: it stats every match.
899
+ // Bound the post-rg stat phase so a hung mount cannot pin glob until
900
+ // the agent stall watchdog fires.
901
+ const withStatAll = await statPathsForMtime(unique, workDir, 64, { deadlineMs: 5000 });
902
+ const withStat = withStatAll.filter((entry) => entry?.stat != null);
903
+ withStat.sort((a, b) => {
904
+ const dm = b.mtime - a.mtime;
905
+ if (dm !== 0) return dm;
906
+ return globMtimeTiePath(a).localeCompare(globMtimeTiePath(b));
907
+ });
908
+ orderedPaths = withStat.map((entry) => entry.full || resolveAgainstCwd(entry.path, workDir));
909
+ } else {
910
+ orderedPaths = unique.map((entry) => isAbsolute(entry) ? resolve(entry) : resolveAgainstCwd(entry, workDir));
911
+ }
912
+ const totalBeforeOffset = orderedPaths.length;
913
+ const windowed = offset > 0 ? orderedPaths.slice(offset) : orderedPaths;
914
+ const capped = (headLimit === Infinity ? windowed : windowed.slice(0, headLimit))
915
+ .map((abs) => relativeSearchResultPath(abs, workDir));
949
916
  const remaining = windowed.length - capped.length;
950
917
  const truncSuffix = accumTruncated
951
918
  ? '\n... [truncated at accumulation cap (50000)]'
@@ -979,4 +946,5 @@ export async function executeGlobTool(args, workDir, options = {}) {
979
946
  try { options.onProgress(`found ${totalBeforeOffset} files`); } catch { /* best-effort */ }
980
947
  }
981
948
  return out;
949
+ });
982
950
  }