mixdog 0.9.95 → 0.9.97

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 (282) hide show
  1. package/LICENSES/Apache-2.0.txt +201 -0
  2. package/LICENSES/MIT.txt +56 -0
  3. package/LICENSES/codex-NOTICE.txt +6 -0
  4. package/NOTICE.md +46 -9
  5. package/README.md +0 -17
  6. package/package.json +16 -12
  7. package/scripts/.tmp-coverage-report.mjs +96 -0
  8. package/scripts/build-tui.mjs +17 -1
  9. package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
  10. package/scripts/fixtures/patch-replay-corpus.json +98 -0
  11. package/src/defaults/mixdog-config.template.json +5 -8
  12. package/src/defaults/skills/setup/SKILL.md +1 -1
  13. package/src/lib/keychain-cjs.cjs +1 -1
  14. package/src/lib/mixdog-debug.cjs +2 -3
  15. package/src/rules/agent/30-explorer.md +7 -4
  16. package/src/rules/lead/01-general.md +1 -2
  17. package/src/rules/lead/lead-tool.md +0 -3
  18. package/src/rules/shared/01-tool.md +21 -29
  19. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +18 -2
  20. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +31 -1
  21. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +6 -17
  22. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +12 -1
  23. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -13
  24. package/src/runtime/agent/orchestrator/config.mjs +246 -67
  25. package/src/runtime/agent/orchestrator/mcp/client.mjs +104 -41
  26. package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +23 -0
  27. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +113 -14
  28. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +5 -1
  29. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +32 -11
  30. package/src/runtime/agent/orchestrator/providers/gemini.mjs +89 -2
  31. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -1
  32. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +19 -1
  33. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +144 -39
  34. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -8
  35. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -1
  36. package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +1 -1
  37. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +3 -4
  38. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -6
  39. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -2
  40. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +67 -7
  41. package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +291 -0
  42. package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +21 -0
  43. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +36 -10
  44. package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
  45. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -33
  46. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +13 -0
  47. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +2 -2
  48. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +0 -24
  49. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +21 -29
  50. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +7 -10
  51. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +45 -25
  52. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
  53. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +142 -84
  54. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
  55. package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +4 -0
  56. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +31 -1
  57. package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +38 -2
  58. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +29 -9
  59. package/src/runtime/agent/orchestrator/session/manager.mjs +2 -0
  60. package/src/runtime/agent/orchestrator/session/result-classification.mjs +65 -0
  61. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +86 -8
  62. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +10 -3
  63. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +4 -0
  64. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +71 -42
  65. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +4 -33
  66. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +4 -5
  67. package/src/runtime/agent/orchestrator/stall-policy.mjs +1 -1
  68. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +2 -2
  69. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +35 -1
  70. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +116 -26
  71. package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
  72. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +8 -9
  73. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +2 -2
  74. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +2 -2
  75. package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +2 -2
  76. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +6 -15
  77. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +12 -26
  78. package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +5 -5
  79. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +83 -33
  80. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +42 -45
  81. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +27 -9
  82. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +7 -0
  83. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +17 -39
  84. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +73 -0
  85. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +1 -1
  86. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +0 -2
  87. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +3 -8
  88. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +30 -0
  89. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +5 -5
  90. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +142 -26
  91. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
  92. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +47 -19
  93. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -2
  94. package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -2
  95. package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +83 -13
  96. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +39 -1
  97. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +1 -1
  98. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +94 -20
  99. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +16 -0
  100. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +108 -12
  101. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +12 -11
  102. package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
  103. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +63 -9
  104. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +2 -2
  105. package/src/runtime/channels/lib/config.mjs +34 -34
  106. package/src/runtime/channels/lib/inbound-handler.mjs +8 -8
  107. package/src/runtime/channels/lib/interaction-handlers.mjs +6 -6
  108. package/src/runtime/channels/lib/memory-client.mjs +31 -157
  109. package/src/runtime/channels/lib/output-forwarder.mjs +16 -16
  110. package/src/runtime/channels/lib/owned-runtime.mjs +60 -60
  111. package/src/runtime/channels/lib/owner-heartbeat.mjs +1 -1
  112. package/src/runtime/channels/lib/{backend-dispatch.mjs → provider-dispatch.mjs} +6 -6
  113. package/src/runtime/channels/lib/runtime-paths.mjs +4 -30
  114. package/src/runtime/channels/lib/scheduler.mjs +2 -2
  115. package/src/runtime/channels/lib/status-snapshot.mjs +3 -3
  116. package/src/runtime/channels/lib/tool-dispatch.mjs +4 -4
  117. package/src/runtime/channels/lib/worker-bootstrap.mjs +1 -1
  118. package/src/runtime/channels/lib/worker-ipc.mjs +7 -7
  119. package/src/runtime/channels/lib/worker-main.mjs +38 -38
  120. package/src/runtime/channels/{backends → providers}/discord-access.mjs +1 -1
  121. package/src/runtime/channels/{backends → providers}/discord-gateway.mjs +1 -1
  122. package/src/runtime/channels/{backends → providers}/discord.mjs +3 -3
  123. package/src/runtime/channels/{backends → providers}/telegram.mjs +12 -12
  124. package/src/runtime/channels/tool-defs.mjs +1 -1
  125. package/src/runtime/memory/index.mjs +76 -248
  126. package/src/runtime/memory/lib/agent-ipc.mjs +146 -81
  127. package/src/runtime/memory/lib/compact-vector-cache.mjs +88 -0
  128. package/src/runtime/memory/lib/core-memory-store.mjs +18 -1
  129. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +13 -34
  130. package/src/runtime/memory/lib/embedding-provider.mjs +47 -26
  131. package/src/runtime/memory/lib/embedding-worker.mjs +30 -34
  132. package/src/runtime/memory/lib/http-router.mjs +9 -6
  133. package/src/runtime/memory/lib/ko-morph.mjs +3 -20
  134. package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -1
  135. package/src/runtime/memory/lib/memory-config-flags.mjs +1 -1
  136. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -0
  137. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -2
  138. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +1 -0
  139. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +4 -1
  140. package/src/runtime/memory/lib/memory-cycle3.mjs +3 -1
  141. package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +5 -0
  142. package/src/runtime/memory/lib/memory-log.mjs +1 -1
  143. package/src/runtime/memory/lib/memory-process-lock.mjs +5 -60
  144. package/src/runtime/memory/lib/memory.mjs +1 -1
  145. package/src/runtime/memory/lib/pg/process.mjs +32 -9
  146. package/src/runtime/memory/lib/session-ingest.mjs +1 -1
  147. package/src/runtime/memory/tool-defs.mjs +1 -5
  148. package/src/runtime/search/index.mjs +2 -2
  149. package/src/runtime/search/tool-defs.mjs +2 -2
  150. package/src/runtime/shared/abort-race.mjs +70 -0
  151. package/src/runtime/shared/agent-route-config.mjs +119 -0
  152. package/src/runtime/shared/atomic-file.mjs +80 -4
  153. package/src/runtime/shared/channel-notification-routing.mjs +2 -2
  154. package/src/runtime/shared/child-guardian.mjs +204 -108
  155. package/src/runtime/shared/child-spawn-gate.mjs +139 -72
  156. package/src/runtime/shared/config.mjs +120 -47
  157. package/src/runtime/shared/llm/index.mjs +1 -1
  158. package/src/runtime/shared/owner-fair-gate.mjs +134 -0
  159. package/src/runtime/shared/resource-admission.mjs +150 -100
  160. package/src/runtime/shared/stream-progress.mjs +6 -0
  161. package/src/runtime/shared/tool-execution-owner.mjs +12 -0
  162. package/src/runtime/shared/tool-result-summary.mjs +1 -1
  163. package/src/runtime/shared/tool-surface.mjs +91 -27
  164. package/src/runtime/shared/tool-workload-gates.mjs +29 -0
  165. package/src/runtime/shared/turn-snapshot.mjs +47 -6
  166. package/src/runtime/shared/turn-worktree-snapshot.mjs +14 -2
  167. package/src/session-runtime/channel-config-api.mjs +5 -5
  168. package/src/session-runtime/config-helpers.mjs +39 -37
  169. package/src/session-runtime/config-lifecycle.mjs +27 -27
  170. package/src/session-runtime/lifecycle-api.mjs +22 -3
  171. package/src/session-runtime/memory-runtime-prewarm.mjs +15 -0
  172. package/src/session-runtime/model-route-api.mjs +7 -5
  173. package/src/session-runtime/prewarm.mjs +1 -1
  174. package/src/session-runtime/provider-auth-api.mjs +0 -7
  175. package/src/session-runtime/provider-models.mjs +1 -1
  176. package/src/session-runtime/remote-control.mjs +6 -7
  177. package/src/session-runtime/runtime-core.mjs +63 -39
  178. package/src/session-runtime/runtime-tunables.mjs +4 -4
  179. package/src/session-runtime/self-update.mjs +1 -1
  180. package/src/session-runtime/session-lifecycle.mjs +45 -17
  181. package/src/session-runtime/session-title.mjs +1 -1
  182. package/src/session-runtime/session-turn-api.mjs +96 -24
  183. package/src/session-runtime/settings-api.mjs +12 -24
  184. package/src/session-runtime/tool-defs.mjs +3 -3
  185. package/src/session-runtime/workflow-agents-api.mjs +55 -81
  186. package/src/session-runtime/workflow.mjs +14 -24
  187. package/src/standalone/agent-dispatch-broker.mjs +198 -0
  188. package/src/standalone/agent-tool/helpers.mjs +2 -2
  189. package/src/standalone/agent-tool/spawn-flow.mjs +2 -0
  190. package/src/standalone/agent-tool/spawn-preset.mjs +11 -21
  191. package/src/standalone/agent-tool/tool-def.mjs +0 -14
  192. package/src/standalone/channel-admin.mjs +53 -64
  193. package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +33 -19
  194. package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +127 -46
  195. package/src/standalone/channel-worker.mjs +124 -647
  196. package/src/standalone/{backend-daemon.mjs → daemon.mjs} +270 -154
  197. package/src/standalone/explore-tool.mjs +187 -70
  198. package/src/standalone/fair-call-scheduler.mjs +264 -0
  199. package/src/standalone/memory-runtime-proxy.mjs +58 -1
  200. package/src/standalone/session-client.mjs +1134 -0
  201. package/src/standalone/session-protocol.mjs +192 -0
  202. package/src/standalone/session-runtime-pool.mjs +393 -0
  203. package/src/standalone/session-runtime-record.mjs +20 -0
  204. package/src/standalone/session-runtime-worker.mjs +203 -0
  205. package/src/standalone/{engine-daemon-service.mjs → session-service.mjs} +481 -224
  206. package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +390 -121
  207. package/src/standalone/session-wire.mjs +55 -0
  208. package/src/tui/App.jsx +34 -21
  209. package/src/tui/app/app-format.mjs +1 -1
  210. package/src/tui/app/app-view.jsx +5 -2
  211. package/src/tui/app/channel-pickers.mjs +41 -41
  212. package/src/tui/app/core-memory-picker.mjs +19 -19
  213. package/src/tui/app/doctor.mjs +5 -11
  214. package/src/tui/app/input-parsers.mjs +2 -2
  215. package/src/tui/app/message-selector.mjs +103 -0
  216. package/src/tui/app/onboarding-steps.mjs +3 -4
  217. package/src/tui/app/project-picker.mjs +80 -56
  218. package/src/tui/app/prompt-submit.mjs +48 -43
  219. package/src/tui/app/resume-picker.mjs +2 -2
  220. package/src/tui/app/route-pickers.mjs +3 -1
  221. package/src/tui/app/settings-picker.mjs +31 -33
  222. package/src/tui/app/transcript-window.mjs +38 -0
  223. package/src/tui/app/use-prompt-draft-flow.mjs +5 -7
  224. package/src/tui/app/use-prompt-handlers.mjs +86 -21
  225. package/src/tui/app/use-prompt-hint.mjs +3 -2
  226. package/src/tui/app/use-prompt-queue-history.mjs +10 -3
  227. package/src/tui/app/use-transcript-scroll.mjs +10 -3
  228. package/src/tui/app/use-transcript-window.mjs +50 -17
  229. package/src/tui/components/PromptInput.jsx +71 -38
  230. package/src/tui/components/Spinner.jsx +1 -1
  231. package/src/tui/components/ToolExecution.jsx +1 -1
  232. package/src/tui/components/prompt-input/edit-helpers.mjs +9 -11
  233. package/src/tui/components/prompt-input/escape-policy.mjs +42 -0
  234. package/src/tui/components/prompt-input/immediate-render.mjs +0 -10
  235. package/src/tui/components/prompt-input/interrupt-policy.mjs +10 -0
  236. package/src/tui/components/prompt-input/restore-policy.mjs +10 -0
  237. package/src/tui/components/tool-execution/surface-detail.mjs +1 -1
  238. package/src/tui/dist/index.mjs +1031 -1676
  239. package/src/tui/hooks/useSession.mjs +16 -0
  240. package/src/tui/index.jsx +12 -13
  241. package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
  242. package/src/tui/{engine → session}/agent-job-feed.mjs +4 -4
  243. package/src/tui/{engine → session}/boot-profile.mjs +2 -2
  244. package/src/tui/{engine → session}/context-state.mjs +3 -3
  245. package/src/tui/{engine → session}/labels.mjs +2 -2
  246. package/src/tui/{engine → session}/live-share.mjs +38 -4
  247. package/src/tui/{engine → session}/notice-text.mjs +2 -2
  248. package/src/tui/{engine → session}/notification-plan.mjs +3 -3
  249. package/src/tui/session/oauth-flows.mjs +140 -0
  250. package/src/tui/{engine → session}/prompt-history.mjs +3 -3
  251. package/src/tui/{engine → session}/queue-helpers.mjs +2 -2
  252. package/src/tui/{engine → session}/render-timing.mjs +2 -2
  253. package/src/tui/{engine → session}/session-api-ext.mjs +30 -20
  254. package/src/tui/{engine → session}/session-api.mjs +110 -93
  255. package/src/tui/{engine → session}/session-flow.mjs +8 -2
  256. package/src/tui/{engine → session}/session-stats.mjs +3 -3
  257. package/src/tui/{engine → session}/tool-approval.mjs +3 -3
  258. package/src/tui/{engine → session}/tool-call-fields.mjs +2 -2
  259. package/src/tui/{engine → session}/tool-card-results.mjs +8 -6
  260. package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
  261. package/src/tui/{engine → session}/tool-result-text.mjs +13 -3
  262. package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
  263. package/src/tui/{engine → session}/turn.mjs +38 -176
  264. package/src/tui/{engine-local-session.mjs → session-local.mjs} +103 -103
  265. package/src/tui/session.mjs +22 -0
  266. package/src/tui/spinner-meta.mjs +4 -4
  267. package/src/tui/spinner-verbs.mjs +51 -3
  268. package/src/ui/statusline-segments.mjs +1 -1
  269. package/src/ui/statusline.mjs +1 -1
  270. package/vendor/ink/build/components/App.js +10 -1
  271. package/src/session-runtime/memory-daemon-probe.mjs +0 -61
  272. package/src/standalone/engine-daemon-client.mjs +0 -894
  273. package/src/standalone/engine-daemon-local-bridge.mjs +0 -20
  274. package/src/standalone/engine-daemon-protocol.mjs +0 -33
  275. package/src/tui/engine/turn-watchdog.mjs +0 -92
  276. package/src/tui/engine.mjs +0 -22
  277. package/src/tui/hooks/useEngine.mjs +0 -16
  278. /package/src/runtime/agent/orchestrator/session/compact/{engine.mjs → runner.mjs} +0 -0
  279. /package/src/runtime/channels/{backends → providers}/discord-attachments.mjs +0 -0
  280. /package/src/tui/{engine → session}/agent-response-tail.mjs +0 -0
  281. /package/src/tui/{engine → session}/frame-batched-store.mjs +0 -0
  282. /package/src/tui/{engine → session}/tui-steering-persist.mjs +0 -0
@@ -1,4 +1,4 @@
1
- import { readFileSync } from 'fs';
1
+ import { readFile } from 'fs/promises';
2
2
  import { executeSingleReadTool } from './read-single-tool.mjs';
3
3
  import { imageMimeForPath, readImageAsContent } from './read-image.mjs';
4
4
  import { readEntryCoalescedDiskWindow } from './read-batch.mjs';
@@ -6,6 +6,8 @@ import { readPathStringGuardError } from './read-open.mjs';
6
6
  import { parseReadLineNumberArg } from './read-args.mjs';
7
7
  import { assertPathsReachable } from './fs-reachability.mjs';
8
8
  import { coerceReadFamilyPathArg } from './path-utils.mjs';
9
+ import { readIoAdmission } from '../../../../shared/tool-workload-gates.mjs';
10
+ import { currentToolExecutionOwner } from '../../../../shared/tool-execution-owner.mjs';
9
11
 
10
12
  function hasLineCoordinate(path) {
11
13
  return typeof path === 'string' && /(?:#L\d+|:\d+(?:-\d+)?(?::|$))/i.test(path);
@@ -24,20 +26,11 @@ function _stripLineCoordForReach(s) {
24
26
  .replace(/:\d+(?:-\d+)?(?::.*)?$/, '');
25
27
  }
26
28
 
27
- // Batch fan-out cap: array/object read shapes read only the first N entries
28
- // (the >N slice below truncates the rest with a `_batchCapNote`). The
29
- // reachability preflight must honour the SAME cap so a path that will be
30
- // capped away is never stat-probed and can never reject the batch via a guard.
31
- const READ_BATCH_PATH_CAP = 10;
32
-
33
29
  function _collectReachCandidates(p) {
34
30
  const out = [];
35
31
  const push = (s) => { if (typeof s === 'string' && s) out.push(s); };
36
32
  if (typeof p === 'string') push(p);
37
- // Only the first READ_BATCH_PATH_CAP entries survive the fan-out cap, so
38
- // preflight only the paths that will actually be read. Capped-away paths
39
- // (incl. UNC/device) must NOT be probed or allowed to fail the batch.
40
- else if (Array.isArray(p)) for (const e of p.slice(0, READ_BATCH_PATH_CAP)) push((e && typeof e === 'object') ? (e.path ?? e.file_path) : e);
33
+ else if (Array.isArray(p)) for (const e of p) push((e && typeof e === 'object') ? (e.path ?? e.file_path) : e);
41
34
  return out;
42
35
  }
43
36
 
@@ -59,15 +52,14 @@ function _guardedReadError(p, helpers) {
59
52
  }
60
53
 
61
54
  // Reachability preflight for EVERY read shape (scalar / array / reads[]). MUST
62
- // run before any sync FS — including path normalization and the image
63
- // stat/read. A dead mount would otherwise freeze the event loop, defeating even
64
- // the 630s dispatch ceiling.
55
+ // run before any filesystem access. Besides bounding a dead mount, its Stats
56
+ // objects seed the real read so the common path does not immediately re-stat.
65
57
  async function _readReachPreflight(rawPath, workDir, helpers) {
66
58
  const {
67
59
  normalizeInputPath, resolveAgainstCwd,
68
60
  } = helpers;
69
61
  // A guarded path (UNC/SMB, Windows device, ADS, /dev/* block) must be
70
- // REJECTED here, not skipped: skipping would let the later sync guard/open
62
+ // REJECTED here, not skipped: skipping would let the later guard/open
71
63
  // path touch it and trigger NTLM/raw-device access or hang. Reject up front
72
64
  // with the same message the inline guards emit.
73
65
  // normalizeInputPath FIRST (FS-pure) so we stat the same path the real read
@@ -80,7 +72,7 @@ async function _readReachPreflight(rawPath, workDir, helpers) {
80
72
  const stripped = _stripLineCoordForReach(normalizeInputPath(raw));
81
73
  const full = resolveAgainstCwd(stripped, workDir);
82
74
  const guardMsg = _guardedReadError(stripped, helpers) || _guardedReadError(full, helpers);
83
- if (guardMsg) return guardMsg;
75
+ if (guardMsg) return { error: guardMsg, statsByPath: null };
84
76
  // Dedup by resolved path so a batch repeating the same file (or the
85
77
  // same union window) issues one stat probe, not one per entry —
86
78
  // bounding the preflight's FS work to the distinct target set.
@@ -88,9 +80,17 @@ async function _readReachPreflight(rawPath, workDir, helpers) {
88
80
  seenFull.add(full);
89
81
  candidates.push(full);
90
82
  }
91
- if (candidates.length === 0) return null;
92
- try { await assertPathsReachable(candidates); return null; }
93
- catch (e) { return `Error: ${e?.message || e}`; }
83
+ if (candidates.length === 0) return { error: null, statsByPath: new Map() };
84
+ try {
85
+ const stats = await assertPathsReachable(candidates);
86
+ const statsByPath = new Map();
87
+ for (let i = 0; i < candidates.length; i++) {
88
+ if (stats[i]) statsByPath.set(candidates[i], stats[i]);
89
+ }
90
+ return { error: null, statsByPath };
91
+ } catch (e) {
92
+ return { error: `Error: ${e?.message || e}`, statsByPath: null };
93
+ }
94
94
  }
95
95
 
96
96
  export async function executeReadTool(args, workDir, readStateScope, executeChildBuiltinTool, options = {}, helpers = {}) {
@@ -127,8 +127,8 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
127
127
  }
128
128
  }
129
129
  args.path = coerceReadFamilyPathArg(args.path, workDir);
130
- // Reachability preflight up front (all shapes) — before readPathStringGuardError /
131
- // image stat, all of which can touch sync FS.
130
+ // Reachability preflight up front (all shapes) — before
131
+ // readPathStringGuardError / image stat.
132
132
  // options._skipReachPreflight: set only by the batch dispatcher on its
133
133
  // child reads (below). The parent batch call already ran this exact
134
134
  // preflight over EVERY candidate path in the array (_collectReachCandidates
@@ -137,8 +137,9 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
137
137
  // child (readPathStringGuardError / image fast-path) — only the async
138
138
  // reachability stat is skipped, never the security guards.
139
139
  if (options?._skipReachPreflight !== true) {
140
- const _reErr = await _readReachPreflight(args.path, workDir, helpers);
141
- if (_reErr) return _reErr;
140
+ const _preflight = await _readReachPreflight(args.path, workDir, helpers);
141
+ if (_preflight.error) return _preflight.error;
142
+ options = { ...options, _preflightStats: _preflight.statsByPath };
142
143
  }
143
144
  // Image files (png/jpg/jpeg/gif/webp): return an MCP image block so the
144
145
  // model can actually SEE the image. native Read does this, but mixdog's
@@ -149,7 +150,7 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
149
150
  if (options?.mediaTextOnly !== true && typeof args.path === 'string' && imageMimeForPath(args.path)) {
150
151
  const _imgNorm = normalizeInputPath(args.path);
151
152
  // W1 H: device-file / UNC / Windows-device / ADS guards must run
152
- // BEFORE the image fast-path so statSync/readFileSync of a
153
+ // BEFORE the image fast-path so stat/readFile of a
153
154
  // UNC/device path can't bypass the checks the normal read path
154
155
  // enforces (NTLM hash leak, raw-device access, ADS).
155
156
  if (typeof isUncPath === 'function' && isUncPath(_imgNorm))
@@ -167,7 +168,11 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
167
168
  return `Error: cannot read Windows device path (reserved name or raw-device namespace): ${normalizeOutputPath(_imgFull)}`;
168
169
  if (typeof hasUnsafeWin32Component === 'function' && hasUnsafeWin32Component(_imgFull))
169
170
  return `Error: cannot read Windows path with trailing dot/space or NTFS ADS suffix (bypasses device guard): ${normalizeOutputPath(_imgFull)}`;
170
- const _imgResult = await readImageAsContent(_imgFull, normalizeOutputPath(_imgNorm));
171
+ const _imgResult = await readImageAsContent(
172
+ _imgFull,
173
+ normalizeOutputPath(_imgNorm),
174
+ options?._preflightStats?.get?.(_imgFull) || null,
175
+ );
171
176
  if (_imgResult) return _imgResult;
172
177
  }
173
178
  // Unified-read dispatch (v0.6.283+):
@@ -181,14 +186,6 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
181
186
  // Single turn can touch many files or swap modes without
182
187
  // the agent iterating across multiple tool names.
183
188
  if (Array.isArray(args.path) && args.path.length > 0 && args.path[0] && typeof args.path[0] === 'object') {
184
- // Cap batch fan-out: never error on an oversized array, just
185
- // truncate and note it in the final output (mirrors list-tool.mjs
186
- // path[] cap pattern).
187
- if (args.path.length > 10) {
188
- const _origObjLen = args.path.length;
189
- args.path = args.path.slice(0, 10);
190
- args._batchCapNote = `... [capped at 10 of ${_origObjLen} paths]`;
191
- }
192
189
  // Per-file batch: each entry carries its own options.
193
190
  // Coalesce same-path entries: multiple chunks for the same
194
191
  // file are merged into a single wider read (min offset to max
@@ -244,11 +241,6 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
244
241
  args.mode = undefined; args.n = undefined; args.offset = undefined; args.limit = undefined; args.full = undefined;
245
242
  }
246
243
  if (Array.isArray(args.path)) {
247
- if (args.path.length > 10) {
248
- const _origStrLen = args.path.length;
249
- args.path = args.path.slice(0, 10);
250
- if (!args._batchCapNote) args._batchCapNote = `... [capped at 10 of ${_origStrLen} paths]`;
251
- }
252
244
  // Schema is `path: string | string[]` — array entries are
253
245
  // strings only. Top-level mode / n / offset / limit / full
254
246
  // apply uniformly to every entry in the batch (the only
@@ -421,7 +413,7 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
421
413
  let rangeHashes = rangeHashesByPath.get(fullPath) || [];
422
414
  if (rangeHashes.length === 0 && mergedRanges.length > 0) {
423
415
  try {
424
- const rawLines = readFileSync(fullPath, 'utf-8').split('\n');
416
+ const rawLines = (await readFile(fullPath, 'utf-8')).split('\n');
425
417
  rangeHashes = mergedRanges.map((range) => {
426
418
  const startIdx = Math.max(0, range.startLine - 1);
427
419
  const endIdx = Math.min(rawLines.length, range.endLine);
@@ -630,11 +622,16 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
630
622
  // Mode routing. A window already dropped any conflicting head/tail/summary
631
623
  // glance above (so the window is served by executeSingleReadTool); what
632
624
  // remains here is a mode-only read, or count/hex which are not text windows.
633
- if (args.mode === 'head') return executeChildBuiltinTool('head', { path: args.path, n: args.n }, workDir);
634
- if (args.mode === 'tail') return executeChildBuiltinTool('tail', { path: args.path, n: args.n }, workDir);
635
- if (args.mode === 'count') return executeChildBuiltinTool('wc', { path: args.path }, workDir);
636
- if (args.mode === 'summary') return executeChildBuiltinTool('summary', { path: args.path, n: args.n, limit: args.limit }, workDir);
637
- if (args.mode === 'hex') return executeChildBuiltinTool('hex', { path: args.path, n: args.n, offset: args.offset }, workDir);
638
- return executeSingleReadTool(args, workDir, readStateScope, options, helpers);
625
+ const runReadIo = (task) => readIoAdmission.run(
626
+ options?.callerSessionId || options?.sessionId || currentToolExecutionOwner(),
627
+ task,
628
+ { signal: options?.signal || options?.abortSignal || null },
629
+ );
630
+ if (args.mode === 'head') return runReadIo(() => executeChildBuiltinTool('head', { path: args.path, n: args.n }, workDir));
631
+ if (args.mode === 'tail') return runReadIo(() => executeChildBuiltinTool('tail', { path: args.path, n: args.n }, workDir));
632
+ if (args.mode === 'count') return runReadIo(() => executeChildBuiltinTool('wc', { path: args.path }, workDir));
633
+ if (args.mode === 'summary') return runReadIo(() => executeChildBuiltinTool('summary', { path: args.path, n: args.n, limit: args.limit }, workDir));
634
+ if (args.mode === 'hex') return runReadIo(() => executeChildBuiltinTool('hex', { path: args.path, n: args.n, offset: args.offset }, workDir));
635
+ return runReadIo(() => executeSingleReadTool(args, workDir, readStateScope, options, helpers));
639
636
 
640
637
  }
@@ -7,7 +7,7 @@ import { createRequire } from 'node:module';
7
7
  import os from 'node:os';
8
8
  import {
9
9
  acquire as acquireChildSpawnSlot,
10
- hasSpareCapacity as hasRgSpareCapacity,
10
+ snapshot as childSpawnSnapshot,
11
11
  } from '../../../../shared/child-spawn-gate.mjs';
12
12
 
13
13
  const execFileAsync = promisify(execFile);
@@ -38,7 +38,7 @@ const execFileAsync = promisify(execFile);
38
38
  // This does not queue or limit calls; it only prevents per-process
39
39
  // oversubscription. Env override remains available for operator tuning.
40
40
  let _rgDefaultThreadCap = null;
41
- export function _rgThreadCap(_spareCapacity = false) {
41
+ export function _rgThreadCap(activeSearches = 1) {
42
42
  const override = Number(process.env.MIXDOG_RG_THREADS);
43
43
  if (Number.isFinite(override) && override >= 1) return Math.floor(override);
44
44
  if (_rgDefaultThreadCap === null) {
@@ -47,7 +47,12 @@ export function _rgThreadCap(_spareCapacity = false) {
47
47
  _rgDefaultThreadCap = cpus || 4;
48
48
  }
49
49
  const cpus = _rgDefaultThreadCap;
50
- return Math.max(2, Math.min(4, Math.ceil(cpus / 4)));
50
+ const perProcessMax = Math.max(2, Math.min(4, Math.ceil(cpus / 4)));
51
+ const active = typeof activeSearches === 'boolean'
52
+ ? 1
53
+ : Math.max(1, Math.floor(Number(activeSearches) || 1));
54
+ const aggregateBudget = Math.max(perProcessMax, cpus - 1);
55
+ return Math.max(1, Math.min(perProcessMax, Math.floor(aggregateBudget / active)));
51
56
  }
52
57
 
53
58
  // Walk rg options once for all thread-argument helpers. Values of these
@@ -94,9 +99,18 @@ function _hasRgThreadArg(argsList) {
94
99
  // (`-j`/`--threads`/`-jN`/`--threads=N`, e.g. the EAGAIN `-j 1` retry). Does
95
100
  // not mutate; may return the ORIGINAL array unchanged when already
96
101
  // thread-pinned (callers must not assume a fresh copy).
97
- export function _withRgThreads(argsList, spareCapacity = false) {
102
+ export function _withRgThreads(argsList, activeSearches = 1) {
98
103
  if (_hasRgThreadArg(argsList)) return argsList;
99
- return ['--threads', String(_rgThreadCap(spareCapacity)), ...argsList];
104
+ return ['--threads', String(_rgThreadCap(activeSearches)), ...argsList];
105
+ }
106
+
107
+ function activeSearchSpawnCount() {
108
+ try {
109
+ const lane = childSpawnSnapshot().lanes.find((entry) => entry.name === 'search');
110
+ return Math.max(1, Number(lane?.inflight) || 1);
111
+ } catch {
112
+ return 1;
113
+ }
100
114
  }
101
115
 
102
116
  // Build the EAGAIN single-thread retry args. If the caller already pinned a
@@ -409,10 +423,12 @@ function spawnRg(argsList, execOptions) {
409
423
  // spawn below, so queue-wait time is excluded from the 20s deadline. No
410
424
  // node guardian is started: rg owns its full SIGTERM→grace→force-kill +
411
425
  // force-settle teardown, so the 1:1 guardian process was pure overhead.
412
- return acquireChildSpawnSlot(gateSignal).then((releaseSlot) => new Promise((resolve, reject) => {
426
+ return acquireChildSpawnSlot(gateSignal, 'search', {
427
+ ownerKey: execOptions?.ownerKey || execOptions?.callerSessionId || execOptions?.sessionId,
428
+ }).then((releaseSlot) => new Promise((resolve, reject) => {
413
429
  // Probe after acquisition: this spawn counts toward inflight. The
414
430
  // answer is best-effort and only selects the per-process rg cap.
415
- const proc = spawn(rgExecutable(), _withRgThreads(argsList, hasRgSpareCapacity()), {
431
+ const proc = spawn(rgExecutable(), _withRgThreads(argsList, activeSearchSpawnCount()), {
416
432
  cwd: execOptions?.cwd,
417
433
  env: execOptions?.env || process.env,
418
434
  windowsHide: true,
@@ -591,9 +607,11 @@ function spawnRgWindowedLines(argsList, execOptions, opts = {}) {
591
607
  // Same gate + thread-cap + no-guardian treatment as spawnRg; queue-wait is
592
608
  // excluded from the rg timeout (armed after spawn). releaseSlot is fired
593
609
  // once via .finally on every settle path.
594
- return acquireChildSpawnSlot(gateSignal).then((releaseSlot) => new Promise((resolve, reject) => {
610
+ return acquireChildSpawnSlot(gateSignal, 'search', {
611
+ ownerKey: execOptions?.ownerKey || execOptions?.callerSessionId || execOptions?.sessionId,
612
+ }).then((releaseSlot) => new Promise((resolve, reject) => {
595
613
  // As above, probe only after this spawn has acquired its gate slot.
596
- const proc = spawn(rgExecutable(), _withRgThreads(argsList, hasRgSpareCapacity()), {
614
+ const proc = spawn(rgExecutable(), _withRgThreads(argsList, activeSearchSpawnCount()), {
597
615
  cwd: execOptions?.cwd,
598
616
  env: execOptions?.env || process.env,
599
617
  windowsHide: true,
@@ -171,6 +171,13 @@ export async function tryReadFamilyEnoentRedirect({
171
171
 
172
172
  export function buildNotFoundHint(workDir, missingPath, actionVerb, errCode = 'ENOENT', cache = null) {
173
173
  if (!NOT_FOUND_CODES.has(String(errCode || 'ENOENT'))) return '';
174
+ // `C:tmpsmp` shape: a Windows path whose separators were eaten by JSON
175
+ // escaping (`"C:\\tmp\\smp"` written as `"C:\tmp\smp"`). Name the cause —
176
+ // the sibling/basename hints below can never resolve such a path.
177
+ const rawMissing = String(missingPath || '');
178
+ if (/^[A-Za-z]:(?![\\/])[^\\/]/.test(rawMissing.replace(/\\/g, '\\'))) {
179
+ return ` The path has a drive letter but no separator after it — the backslashes were probably lost in escaping. Re-issue with forward slashes (e.g. "C:/tmp/smp").`;
180
+ }
174
181
  if (resolveUniqueEnoentRedirect(workDir, missingPath, errCode, cache)) return '';
175
182
  const elsewhere = cachedFileByBasename(workDir, missingPath, cache);
176
183
  if (elsewhere.length) {
@@ -81,7 +81,7 @@ import {
81
81
  parseGrepCountLine,
82
82
  } from './lib/search-input-helpers.mjs';
83
83
 
84
- // CC parity (GrepTool.ts): a single glob string may pack multiple filters
84
+ // A single glob string may pack multiple filters
85
85
  // separated by whitespace or commas, e.g. "*.ts,*.tsx" or "*.ts *.tsx". Split
86
86
  // each into its own --glob. Brace patterns ("*.{ts,tsx}") are left intact so
87
87
  // their internal commas are not torn apart.
@@ -127,19 +127,16 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
127
127
  args = normalizeGrepArgs(args);
128
128
  args.path = coerceReadFamilyPathArg(args.path, workDir);
129
129
  const callContextCharBudget = _grepContextCharBudget(options);
130
- // Fan-out guard: batch multiple string paths with bounded concurrency,
131
- // mirroring code_graph files[] batching. Recursive calls pass a single
132
- // string path, so recursion bottoms out after one level. Results are
133
- // assembled below in input order, regardless of completion order.
130
+ // Batch multiple string paths concurrently. Recursive calls pass a single
131
+ // string path, so recursion bottoms out after one level. Results retain
132
+ // input order even though every path starts immediately.
134
133
  if (Array.isArray(args.path)) {
135
- const GREP_PATH_CAP = 10;
136
- const GREP_PATH_CONCURRENCY = 4;
137
134
  const seen = new Set();
138
135
  const list = args.path
139
136
  .map(p => typeof p === 'string' ? p.trim() : '')
140
137
  .filter(p => p && !seen.has(p) && seen.add(p));
141
138
  if (list.length > 1) {
142
- const capped = list.slice(0, GREP_PATH_CAP);
139
+ const capped = list;
143
140
  // Combined single-spawn path[] fan-out (mirrors the pattern[]
144
141
  // combined path below): ONE rg run with every path as a positional
145
142
  // operand, then normalized-prefix attribution rebuilds the
@@ -151,7 +148,6 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
151
148
  // per-path ENOENT/UNC diagnostics). MIXDOG_GREP_PATH_COMBINED=0
152
149
  // disables.
153
150
  combinedPaths: if (process.env.MIXDOG_GREP_PATH_COMBINED !== '0'
154
- && list.length <= GREP_PATH_CAP
155
151
  && !args.glob
156
152
  && !args.type
157
153
  && args.multiline !== true
@@ -340,38 +336,20 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
340
336
  const configuredOutputCap = Number(options?.toolOutputMaxBytes) > 0
341
337
  ? Math.trunc(Number(options.toolOutputMaxBytes))
342
338
  : Math.trunc(Number(process.env.MIXDOG_TOOL_OUTPUT_MAX_BYTES));
343
- const bodies = new Array(capped.length);
344
- let next = 0;
345
- const runWorker = async () => {
346
- while (next < capped.length) {
347
- const index = next++;
348
- const p = capped[index];
349
- try {
350
- bodies[index] = await executeGrepTool(
351
- { ...args, path: p },
352
- workDir,
353
- executeChildBuiltinTool,
354
- readStateScope,
355
- nestedOptions,
356
- );
357
- } catch (err) {
358
- bodies[index] = `Error: ${err && err.message ? err.message : err}`;
359
- }
339
+ const bodies = await Promise.all(capped.map(async (p) => {
340
+ try {
341
+ return await executeGrepTool(
342
+ { ...args, path: p },
343
+ workDir,
344
+ executeChildBuiltinTool,
345
+ readStateScope,
346
+ nestedOptions,
347
+ );
348
+ } catch (err) {
349
+ return `Error: ${err && err.message ? err.message : err}`;
360
350
  }
361
- };
362
- await Promise.all(
363
- Array.from(
364
- { length: Math.min(GREP_PATH_CONCURRENCY, capped.length) },
365
- () => runWorker(),
366
- ),
367
- );
351
+ }));
368
352
  const parts = capped.map((p, index) => `# grep ${p}\n${bodies[index]}`);
369
- if (list.length > GREP_PATH_CAP) {
370
- parts.push(`[capped at ${GREP_PATH_CAP} of ${list.length} paths]`);
371
- // Omitted paths mean the returned result cannot cover the whole
372
- // requested path set — never let it be cached as complete.
373
- if (options?.scopedCacheOutcome) markScopedCacheIncomplete(options.scopedCacheOutcome);
374
- }
375
353
  const output = parts.join('\n\n');
376
354
  if (configuredOutputCap > 0
377
355
  && Buffer.byteLength(output, 'utf8') > configuredOutputCap
@@ -560,6 +560,10 @@ export function preflightPowerShellHygiene(command, { shellType, shellName } = {
560
560
  const name = String(shellName || '').toLowerCase();
561
561
  const isLegacyPS = /powershell/.test(name) && !/pwsh/.test(name);
562
562
  const violations = [];
563
+ // Violation kinds drive the bash auto-rescue: a command blocked ONLY for
564
+ // bash syntax can be run in bash unchanged, while a PowerShell-specific
565
+ // violation ($PID reassignment) must stay a hard block.
566
+ const kinds = [];
563
567
  // `$PID=` reassignment and `&&` connectors are judged on a quote-masked
564
568
  // copy so quoted literals (`echo "a && b"`, `Write-Output '$PID=1'`) and
565
569
  // regex/search args are never mistaken for real syntax.
@@ -572,14 +576,17 @@ export function preflightPowerShellHygiene(command, { shellType, shellName } = {
572
576
  const first = String(tokens[0] || '').toLowerCase().replace(/^.*[\\/]/, '');
573
577
  if (POWERSHELL_BASHISM_HINTS[first]) {
574
578
  violations.push(`\`${first}\` is a Unix command: ${POWERSHELL_BASHISM_HINTS[first]}`);
579
+ kinds.push('bashism');
575
580
  }
576
581
  }
577
582
  }
578
583
  if (/\$PID\s*=(?!=)/i.test(masked)) {
579
584
  violations.push('`$PID` is a reserved PowerShell automatic variable — do not reassign it (use a different name).');
585
+ kinds.push('powershell-only');
580
586
  }
581
587
  if (isLegacyPS && /(?:^|[^&])&&(?:[^&]|$)/.test(masked)) {
582
588
  violations.push('`&&` is not supported in Windows PowerShell 5.1 — use `;` to sequence or issue separate calls.');
589
+ kinds.push('bashism');
583
590
  }
584
591
 
585
592
  if (violations.length > 0) {
@@ -587,12 +594,78 @@ export function preflightPowerShellHygiene(command, { shellType, shellName } = {
587
594
  return {
588
595
  command: rewritten,
589
596
  note,
597
+ // The MSYS path rewrite is a PowerShell-targeted normalisation, so
598
+ // a bash rescue must re-run the ORIGINAL text.
599
+ original,
600
+ bashOnly: kinds.length > 0 && kinds.every((kind) => kind === 'bashism'),
590
601
  block: `PowerShell preflight blocked this command (bash syntax on a PowerShell host). Fix and retry:\n- ${hints.join('\n- ')}`,
591
602
  };
592
603
  }
593
604
  return { command: rewritten, note, block: null };
594
605
  }
595
606
 
607
+ // PowerShell-only constructs. Used to decide whether a command the PowerShell
608
+ // preflight blocked for bash syntax can simply be RUN in bash: only a command
609
+ // with zero PowerShell-specific syntax qualifies, so a mixed script (bash
610
+ // pipes plus `$env:`/cmdlets/`2>$null`) stays a hard block instead of being
611
+ // silently rerouted into a shell that would mangle it. Deliberately
612
+ // over-inclusive — a false positive only declines the rescue.
613
+ const POWERSHELL_ONLY_SYNTAX = [
614
+ /\$(?:null|true|false|env:|_\b|args\b|host\b|profile\b|pwd\b|psversiontable\b|psscriptroot\b|lastexitcode\b|erroractionpreference\b)/i,
615
+ /\$[A-Za-z_]\w*\s*=/,
616
+ /\b(?:get|set|new|remove|select|where|foreach|invoke|out|write|start|stop|test|measure|sort|join|split|convert|import|export|add|copy|move|rename|resolve|compare|clear|push|pop)-[A-Za-z]+\b/i,
617
+ /\[[A-Za-z_][\w.]*\]::/,
618
+ /(?:^|\s)-(?:eq|ne|gt|lt|ge|le|match|notmatch|like|notlike|contains|replace|not|and|or|is|as|ErrorAction|Recurse|Force|Encoding|TotalCount|Tail|Pattern|Path|LiteralPath)\b/,
619
+ /2>\s*\$/,
620
+ /\|\s*[%?]\s*\{/,
621
+ /@['"][\s\S]*?['"]@/,
622
+ /`\s*$/m,
623
+ ];
624
+ export function hasPowerShellOnlySyntax(command) {
625
+ const text = String(command || '');
626
+ if (!text.trim()) return false;
627
+ return POWERSHELL_ONLY_SYNTAX.some((pattern) => pattern.test(text));
628
+ }
629
+
630
+ // ── Inline-script hoisting ──────────────────────────────────────────────────
631
+ // `node -e "<body>"` / `python -c "<body>"` hand the script through the host
632
+ // shell's quoting layer, where nested quotes and escapes are the measured top
633
+ // cause of inline-script failures. When the body is quote-LITERAL (the shell
634
+ // would pass it through byte for byte) the same run is expressible as a file
635
+ // invocation, which has no quoting layer at all — so the failure cannot occur.
636
+ //
637
+ // The transform is refused whenever file semantics would differ from `-e`:
638
+ // - a body with $, backtick or a backslash escape (the shell WOULD rewrite it);
639
+ // - anything resolved relative to the script (relative require/import,
640
+ // import.meta, __dirname/__filename);
641
+ // - a body that reads process.argv[1] (undefined under -e, a path in a file).
642
+ const INLINE_SCRIPT_RE = /\b(node(?:\.exe)?|python3?|py)((?:\s+--?[\w-]+(?:=[^\s"']+)?)*)\s+(-e|--eval|-c)\s+"((?:[^"\\]|\\.)*)"/;
643
+ const INLINE_UNSAFE_BODY = /[$`\\]/;
644
+ const INLINE_FILE_RELATIVE = /(?:require|import)\s*\(\s*['"]\.{1,2}\/|from\s+['"]\.{1,2}\/|import\.meta|__dirname|__filename|argv\s*\[\s*1\s*\]/;
645
+
646
+ export function planInlineScriptHoist(command) {
647
+ const text = String(command || '');
648
+ if (!text.trim()) return null;
649
+ const match = text.match(INLINE_SCRIPT_RE);
650
+ if (!match) return null;
651
+ const [whole, exe, rawFlags, , body] = match;
652
+ if (INLINE_UNSAFE_BODY.test(body)) return null;
653
+ if (INLINE_FILE_RELATIVE.test(body)) return null;
654
+ const flags = String(rawFlags || '');
655
+ const isNode = /^node/i.test(exe);
656
+ const esm = /--input-type=module/.test(flags);
657
+ const extension = isNode ? (esm ? '.mjs' : '.cjs') : '.py';
658
+ // `--input-type` only describes an inline body; the extension carries the
659
+ // module kind for a file, so the flag is dropped with the body.
660
+ const keptFlags = flags.replace(/\s+--input-type=\w+/g, '');
661
+ return {
662
+ exe,
663
+ extension,
664
+ body,
665
+ replace: (filePath) => text.replace(whole, `${exe}${keptFlags} "${filePath}"`),
666
+ };
667
+ }
668
+
596
669
  export async function preflightShellLargeFileProbe(command, cwd) {
597
670
  const text = String(command || '').trim();
598
671
  let localCwd = resolve(cwd || process.cwd());
@@ -213,7 +213,7 @@ export function shellJobDonePath(jobId) { return join(getShellJobsDir(), `${jobI
213
213
  // enforces unconditionally and records detail.timeoutEnforced:true.
214
214
  export function shellJobEnforcedPath(jobId) { return join(getShellJobsDir(), `${jobId}.enforced`); }
215
215
  // Guardian kill receipt: written by the detached child-guardian immediately
216
- // before it force-kills the job tree (host memory floor / orphaned parent).
216
+ // before it cleans up a job tree whose owning parent exited.
217
217
  // Its existence lets refreshShellJob attribute a marker-less death to the
218
218
  // guardian instead of reporting an unexplained unknown exit.
219
219
  export function shellJobGuardianReceiptPath(jobId) { return join(getShellJobsDir(), `${jobId}.guardian`); }
@@ -272,7 +272,6 @@ export async function _startBackgroundShellJobImpl({
272
272
  childPid: child.pid,
273
273
  childGroupPid: child.pid,
274
274
  label: 'shell-job',
275
- protectHostMemory: true,
276
275
  receiptPath: shellJobGuardianReceiptPath(jobId),
277
276
  });
278
277
  _installShellJobsExitHook();
@@ -523,7 +522,6 @@ async function startBackgroundPowerShellJob({
523
522
  childPid,
524
523
  childGroupPid: childPid,
525
524
  label: 'shell-job-powershell',
526
- protectHostMemory: true,
527
525
  receiptPath: shellJobGuardianReceiptPath(jobId),
528
526
  });
529
527
  _installShellJobsExitHook();
@@ -140,8 +140,8 @@ export function killShellJob(jobId) {
140
140
  // wrapper publishing the done marker. Evidence order:
141
141
  // 1. exit file — the wrapper writes exit BEFORE done, so a kill in that
142
142
  // window still left the real exit code; recover it instead of guessing.
143
- // 2. guardian receipt — the child-guardian records why it force-killed the
144
- // tree (host memory floor / orphaned parent) before killing.
143
+ // 2. guardian receipt — the child-guardian records an orphaned parent before
144
+ // cleaning up the owned tree.
145
145
  // 3. otherwise — honest unknown: terminal 'failed' with exitCode null and
146
146
  // an explicit "exit status unknown" error; spilled output is preserved
147
147
  // and the completion notification carries its tail.
@@ -165,12 +165,7 @@ function finalizeMarkerlessShellJob(detail) {
165
165
  } catch { /* no guardian receipt */ }
166
166
  detail.status = 'failed';
167
167
  detail.exitCode = 137;
168
- if (receipt?.reason === 'host-memory-floor') {
169
- const freeMb = Math.round(Number(receipt.freeBytes || 0) / (1024 * 1024));
170
- const floorMb = Math.round(Number(receipt.minFreeMemoryBytes || 0) / (1024 * 1024));
171
- detail.killedByGuardian = true;
172
- detail.error = `killed by child guardian: host free memory ${freeMb} MB fell below the ${floorMb} MB floor — job tree force-killed to protect the host; retry with lower concurrency or after memory recovers`;
173
- } else if (receipt?.reason === 'parent-exit') {
168
+ if (receipt?.reason === 'parent-exit') {
174
169
  detail.killedByGuardian = true;
175
170
  detail.error = 'killed by child guardian: owning runtime process exited (session cleanup)';
176
171
  } else {
@@ -66,10 +66,40 @@ function firstExistingPathFromPath(commandName, excludeRe = null) {
66
66
  .find((candidate) => !excludeRe || !excludeRe.test(candidate)) || null;
67
67
  }
68
68
 
69
+ // Well-known pwsh (PowerShell 7+) install roots, probed on the filesystem when
70
+ // PATH does not carry pwsh.exe. A long-lived daemon frequently inherits an env
71
+ // without the PowerShell 7 directory, which silently downgraded every command
72
+ // to Windows PowerShell 5.1 — the shell that lacks `&&` and drives the bashism
73
+ // preflight blocks. Newest major first, then the Store alias.
74
+ function pwshFromKnownWindowsRoots() {
75
+ const env = process.env;
76
+ const bases = [env.ProgramW6432, env.ProgramFiles, env['ProgramFiles(x86)'], 'C:\\Program Files', 'C:\\Program Files (x86)']
77
+ .filter(Boolean)
78
+ .map((base) => join(base, 'PowerShell'));
79
+ const versions = ['7', '7-preview', '8', '9'];
80
+ for (const base of bases) {
81
+ for (const version of versions) {
82
+ const candidate = join(base, version, 'pwsh.exe');
83
+ if (existsSync(candidate)) return candidate;
84
+ }
85
+ }
86
+ if (env.LOCALAPPDATA) {
87
+ const storeAlias = join(env.LOCALAPPDATA, 'Microsoft', 'WindowsApps', 'pwsh.exe');
88
+ if (existsSync(storeAlias)) return storeAlias;
89
+ }
90
+ return null;
91
+ }
92
+
69
93
  function resolveWindowsPowerShell() {
94
+ // Priority: pwsh on PATH → pwsh in a known install root → bundled 5.1 →
95
+ // any powershell.exe on PATH. PowerShell 7 is preferred wherever it exists
96
+ // because 5.1 rejects `&&` chains and several modern operators.
70
97
  const pwsh = firstExistingPathFromPath('pwsh.exe');
71
98
  if (pwsh) return shellSpec(pwsh, 'powershell');
72
99
 
100
+ const installed = pwshFromKnownWindowsRoots();
101
+ if (installed) return shellSpec(installed, 'powershell');
102
+
73
103
  const systemRoot = process.env.SystemRoot || process.env.WINDIR || 'C:\\Windows';
74
104
  const bundled = join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
75
105
  if (existsSync(bundled)) return shellSpec(bundled, 'powershell');
@@ -146,9 +146,9 @@ export async function _runDiskCodeGraphFastPath({
146
146
  signal = null,
147
147
  loadDiskEntry,
148
148
  consumeDirty = () => {},
149
- // Long-lived build slot: 'build' lane so a cold graph build never blocks
150
- // short rg spawns behind the win32 cap=1 default lane.
151
- acquireSlot = (signal) => acquireChildSpawnSlot(signal, 'build'),
149
+ // Long-lived code-graph lane so a cold graph build never blocks short rg
150
+ // processes in the independent search lane.
151
+ acquireSlot = (signal) => acquireChildSpawnSlot(signal, 'code-graph'),
152
152
  validateDiskHit = _validateDiskCodeGraphHit,
153
153
  spawnWorker,
154
154
  maxFiles = CODE_GRAPH_MAX_FILES,
@@ -344,7 +344,7 @@ export async function buildCodeGraphAsync(cwd, signal = null, {
344
344
  }
345
345
  // Non-competing prewarm: the signature-validation manifest also needs a
346
346
  // child-spawn slot, so warmers skip before either it or a Worker can queue.
347
- if (bestEffort && !childSpawnHasSpareCapacity()) return null;
347
+ if (bestEffort && !childSpawnHasSpareCapacity('code-graph')) return null;
348
348
  const _genAtStart = _getCodeGraphGen(graphCwd);
349
349
  const promise = (async () => {
350
350
  // Loading the compact disk manifest/one candidate entry is synchronous but
@@ -422,7 +422,7 @@ export function _spawnCodeGraphWorker(
422
422
  if (val instanceof Error) reject(val);
423
423
  else resolve(val);
424
424
  };
425
- (preAcquiredRelease ? Promise.resolve(preAcquiredRelease) : acquireChildSpawnSlot(signal || null, 'build')).then((release) => {
425
+ (preAcquiredRelease ? Promise.resolve(preAcquiredRelease) : acquireChildSpawnSlot(signal || null, 'code-graph')).then((release) => {
426
426
  _releaseSlot = release;
427
427
  if (settled) { release(); _releaseSlot = null; return; }
428
428
  if (signal?.aborted) { settle(new Error('aborted')); return; }