mixdog 0.9.95 → 0.9.96

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 (178) 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/package.json +12 -9
  6. package/scripts/.tmp-coverage-report.mjs +96 -0
  7. package/scripts/build-tui.mjs +17 -1
  8. package/scripts/fixtures/patch-replay-corpus.json +98 -0
  9. package/src/defaults/mixdog-config.template.json +5 -8
  10. package/src/rules/agent/30-explorer.md +7 -4
  11. package/src/rules/shared/01-tool.md +20 -29
  12. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +18 -2
  13. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +31 -1
  14. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +6 -17
  15. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +12 -1
  16. package/src/runtime/agent/orchestrator/config.mjs +246 -67
  17. package/src/runtime/agent/orchestrator/mcp/client.mjs +26 -11
  18. package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +23 -0
  19. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +113 -14
  20. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +5 -1
  21. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +32 -11
  22. package/src/runtime/agent/orchestrator/providers/gemini.mjs +89 -2
  23. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -1
  24. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +19 -1
  25. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +144 -39
  26. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -8
  27. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -1
  28. package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +1 -1
  29. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +3 -4
  30. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +67 -7
  31. package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +291 -0
  32. package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +21 -0
  33. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +28 -4
  34. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +4 -33
  35. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +13 -0
  36. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +2 -2
  37. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +0 -24
  38. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +1 -27
  39. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +6 -10
  40. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +5 -0
  41. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +142 -84
  42. package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +4 -0
  43. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +31 -1
  44. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +29 -9
  45. package/src/runtime/agent/orchestrator/session/manager.mjs +2 -0
  46. package/src/runtime/agent/orchestrator/session/result-classification.mjs +65 -0
  47. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +86 -8
  48. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +12 -0
  49. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +3 -33
  50. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +4 -5
  51. package/src/runtime/agent/orchestrator/stall-policy.mjs +1 -1
  52. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +2 -2
  53. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +35 -1
  54. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +118 -18
  55. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +8 -9
  56. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +2 -2
  57. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +2 -2
  58. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +6 -15
  59. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +10 -26
  60. package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +1 -1
  61. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +3 -3
  62. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +1 -23
  63. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +23 -9
  64. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +7 -0
  65. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +17 -39
  66. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +73 -0
  67. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +1 -1
  68. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +0 -2
  69. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +3 -8
  70. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +30 -0
  71. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +5 -5
  72. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +87 -15
  73. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +6 -15
  74. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +2 -2
  75. package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +83 -13
  76. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +39 -1
  77. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +29 -1
  78. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +16 -0
  79. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +108 -12
  80. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +10 -10
  81. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +56 -8
  82. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +2 -2
  83. package/src/runtime/channels/lib/config.mjs +2 -2
  84. package/src/runtime/channels/lib/inbound-handler.mjs +1 -1
  85. package/src/runtime/channels/lib/scheduler.mjs +2 -2
  86. package/src/runtime/memory/index.mjs +5 -38
  87. package/src/runtime/memory/lib/agent-ipc.mjs +145 -81
  88. package/src/runtime/memory/lib/compact-vector-cache.mjs +88 -0
  89. package/src/runtime/memory/lib/core-memory-store.mjs +18 -1
  90. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +12 -33
  91. package/src/runtime/memory/lib/embedding-provider.mjs +47 -26
  92. package/src/runtime/memory/lib/embedding-worker.mjs +30 -34
  93. package/src/runtime/memory/lib/http-router.mjs +8 -0
  94. package/src/runtime/memory/lib/ko-morph.mjs +3 -20
  95. package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -1
  96. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -0
  97. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -2
  98. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +1 -0
  99. package/src/runtime/memory/lib/memory-cycle3.mjs +3 -1
  100. package/src/runtime/memory/lib/pg/process.mjs +32 -9
  101. package/src/runtime/memory/tool-defs.mjs +1 -5
  102. package/src/runtime/search/tool-defs.mjs +2 -2
  103. package/src/runtime/shared/agent-route-config.mjs +119 -0
  104. package/src/runtime/shared/atomic-file.mjs +80 -4
  105. package/src/runtime/shared/channel-notification-routing.mjs +2 -2
  106. package/src/runtime/shared/child-guardian.mjs +204 -108
  107. package/src/runtime/shared/child-spawn-gate.mjs +60 -31
  108. package/src/runtime/shared/config.mjs +130 -44
  109. package/src/runtime/shared/resource-admission.mjs +146 -97
  110. package/src/runtime/shared/stream-progress.mjs +6 -0
  111. package/src/runtime/shared/tool-surface.mjs +0 -5
  112. package/src/runtime/shared/turn-snapshot.mjs +31 -5
  113. package/src/runtime/shared/turn-worktree-snapshot.mjs +14 -2
  114. package/src/session-runtime/config-helpers.mjs +39 -37
  115. package/src/session-runtime/model-route-api.mjs +7 -5
  116. package/src/session-runtime/provider-auth-api.mjs +0 -7
  117. package/src/session-runtime/runtime-core.mjs +11 -11
  118. package/src/session-runtime/runtime-tunables.mjs +4 -4
  119. package/src/session-runtime/session-lifecycle.mjs +9 -2
  120. package/src/session-runtime/session-turn-api.mjs +41 -9
  121. package/src/session-runtime/settings-api.mjs +5 -17
  122. package/src/session-runtime/tool-defs.mjs +3 -3
  123. package/src/session-runtime/workflow-agents-api.mjs +55 -81
  124. package/src/session-runtime/workflow.mjs +14 -24
  125. package/src/standalone/agent-dispatch-broker.mjs +198 -0
  126. package/src/standalone/agent-tool/helpers.mjs +2 -2
  127. package/src/standalone/agent-tool/spawn-flow.mjs +2 -0
  128. package/src/standalone/agent-tool/spawn-preset.mjs +11 -21
  129. package/src/standalone/agent-tool/tool-def.mjs +0 -14
  130. package/src/standalone/backend-daemon.mjs +97 -36
  131. package/src/standalone/channel-admin.mjs +12 -12
  132. package/src/standalone/channel-daemon-client.mjs +17 -1
  133. package/src/standalone/channel-daemon-transport.mjs +90 -8
  134. package/src/standalone/engine-daemon-client.mjs +380 -205
  135. package/src/standalone/engine-daemon-protocol.mjs +3 -4
  136. package/src/standalone/engine-daemon-service.mjs +378 -134
  137. package/src/standalone/engine-daemon-transport.mjs +268 -77
  138. package/src/standalone/explore-tool.mjs +187 -70
  139. package/src/standalone/fair-call-scheduler.mjs +264 -0
  140. package/src/standalone/session-protocol.mjs +188 -0
  141. package/src/tui/App.jsx +30 -17
  142. package/src/tui/app/app-view.jsx +5 -2
  143. package/src/tui/app/core-memory-picker.mjs +19 -19
  144. package/src/tui/app/doctor.mjs +5 -11
  145. package/src/tui/app/message-selector.mjs +103 -0
  146. package/src/tui/app/onboarding-steps.mjs +1 -2
  147. package/src/tui/app/project-picker.mjs +80 -56
  148. package/src/tui/app/prompt-submit.mjs +46 -41
  149. package/src/tui/app/resume-picker.mjs +2 -2
  150. package/src/tui/app/route-pickers.mjs +3 -1
  151. package/src/tui/app/settings-picker.mjs +6 -8
  152. package/src/tui/app/transcript-window.mjs +38 -0
  153. package/src/tui/app/use-prompt-draft-flow.mjs +5 -7
  154. package/src/tui/app/use-prompt-handlers.mjs +86 -21
  155. package/src/tui/app/use-prompt-hint.mjs +3 -2
  156. package/src/tui/app/use-prompt-queue-history.mjs +10 -3
  157. package/src/tui/app/use-transcript-scroll.mjs +9 -2
  158. package/src/tui/app/use-transcript-window.mjs +50 -17
  159. package/src/tui/components/PromptInput.jsx +71 -38
  160. package/src/tui/components/Spinner.jsx +1 -1
  161. package/src/tui/components/prompt-input/edit-helpers.mjs +9 -11
  162. package/src/tui/components/prompt-input/escape-policy.mjs +42 -0
  163. package/src/tui/components/prompt-input/immediate-render.mjs +0 -10
  164. package/src/tui/components/prompt-input/interrupt-policy.mjs +10 -0
  165. package/src/tui/components/prompt-input/restore-policy.mjs +10 -0
  166. package/src/tui/dist/index.mjs +920 -1592
  167. package/src/tui/engine/live-share.mjs +34 -0
  168. package/src/tui/engine/session-api-ext.mjs +1 -4
  169. package/src/tui/engine/session-api.mjs +103 -33
  170. package/src/tui/engine/session-flow.mjs +7 -1
  171. package/src/tui/engine/tool-card-results.mjs +4 -2
  172. package/src/tui/engine/tool-result-text.mjs +10 -0
  173. package/src/tui/engine/turn.mjs +9 -69
  174. package/src/tui/engine-local-session.mjs +47 -31
  175. package/src/tui/index.jsx +1 -2
  176. package/src/tui/spinner-meta.mjs +4 -4
  177. package/src/tui/spinner-verbs.mjs +51 -3
  178. package/src/standalone/engine-daemon-local-bridge.mjs +0 -20
@@ -5,7 +5,6 @@
5
5
  // only (behavior + argument shapes); usage policy lives in rules/shared/01-tool.md.
6
6
  import {
7
7
  TOOL_ASYNC_EXECUTION_CONTRACT,
8
- TOOL_MANUAL_CONTROL_CONTRACT,
9
8
  executionModeSchemaDescription,
10
9
  } from '../../../../shared/background-tasks.mjs';
11
10
 
@@ -28,7 +27,7 @@ function _shellMaxTimeoutMs() {
28
27
  // process lifetime, so this is evaluated once at module load.
29
28
  const _shellSyntaxCheat =
30
29
  process.platform === 'win32'
31
- ? ' PowerShell: grep→Select-String, tail→Get-Content -Tail, head→Get-Content -TotalCount, /c/→C:\\, if && is unsupported use ;, $PID is reserved.'
30
+ ? ' PowerShell: use ; between commands, /c/→C:\\, and note that $PID is reserved.'
32
31
  : '';
33
32
 
34
33
  export const BUILTIN_TOOLS = [
@@ -36,7 +35,7 @@ export const BUILTIN_TOOLS = [
36
35
  name: 'read',
37
36
  title: 'Mixdog Read',
38
37
  annotations: { title: 'Mixdog Read', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: false },
39
- description: 'Read file contents. Batch paths/regions as real arrays: path[] or {path,offset,limit}[] regions in one call. Not for directories.',
38
+ description: 'Read file contents or line ranges; not directories.',
40
39
  inputSchema: {
41
40
  type: 'object',
42
41
  properties: {
@@ -99,12 +98,12 @@ export const BUILTIN_TOOLS = [
99
98
  name: 'task',
100
99
  title: 'Background Task Control',
101
100
  annotations: { title: 'Background Task Control', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
102
- description: `Control a shell async task_id: actions list/status/read/wait/cancel. ${TOOL_MANUAL_CONTROL_CONTRACT} Not sess_* or agent ids.`,
101
+ description: 'Control a shell background task_id; not session or agent ids.',
103
102
  inputSchema: {
104
103
  type: 'object',
105
104
  properties: {
106
105
  task_id: { type: 'string', description: 'shell async task_id.' },
107
- action: { type: 'string', enum: ['list', 'status', 'read', 'wait', 'cancel'], description: 'Defaults to list, or wait when task_id is set; avoid polling loops.' },
106
+ action: { type: 'string', enum: ['list', 'status', 'read', 'wait', 'cancel'], description: 'Default list; with task_id, default wait.' },
108
107
  timeout_ms: { type: 'number', description: 'Wait timeout ms.' },
109
108
  },
110
109
  required: [],
@@ -115,7 +114,7 @@ export const BUILTIN_TOOLS = [
115
114
  name: 'grep',
116
115
  title: 'Mixdog Grep',
117
116
  annotations: { title: 'Mixdog Grep', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
118
- description: 'Content search by literal or regex over file/dir scopes; matches return copy-ready raw source spans.',
117
+ description: 'Literal/regex search over file/dir scopes; returns source blocks with context.',
119
118
  inputSchema: {
120
119
  type: 'object',
121
120
  properties: {
@@ -156,7 +155,7 @@ export const BUILTIN_TOOLS = [
156
155
  name: 'glob',
157
156
  title: 'Mixdog Glob',
158
157
  annotations: { title: 'Mixdog Glob', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
159
- description: 'Match exact glob patterns from base directories; batch pattern[]/path[].',
158
+ description: 'Match glob patterns under base directories.',
160
159
  inputSchema: {
161
160
  type: 'object',
162
161
  properties: {
@@ -185,7 +184,7 @@ export const BUILTIN_TOOLS = [
185
184
  name: 'find',
186
185
  title: 'Mixdog Find Files',
187
186
  annotations: { title: 'Mixdog Find Files', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
188
- description: 'Fuzzy lookup for partial paths/names (dot dirs included). Returns matching paths; not file contents.',
187
+ description: 'Fuzzy partial path/name lookup; returns paths only.',
189
188
  inputSchema: {
190
189
  type: 'object',
191
190
  properties: {
@@ -207,7 +206,7 @@ export const BUILTIN_TOOLS = [
207
206
  name: 'list',
208
207
  title: 'Mixdog List Directory',
209
208
  annotations: { title: 'Mixdog List Directory', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
210
- description: 'List directory entries (path + type); batch independent dirs as path[].',
209
+ description: 'List directory entries (path + type).',
211
210
  inputSchema: {
212
211
  type: 'object',
213
212
  properties: {
@@ -238,7 +238,7 @@ export function getCachedReadOnlyStat(fullPath, loader = statSync, now = Date.no
238
238
  return stat;
239
239
  }
240
240
 
241
- export async function statPathsForMtime(paths, workDir, concurrency = 64, opts = {}) {
241
+ export async function statPathsForMtime(paths, workDir, concurrency = Infinity, opts = {}) {
242
242
  const items = Array.isArray(paths) ? paths : [];
243
243
  const out = new Array(items.length);
244
244
  const now = Date.now();
@@ -302,7 +302,7 @@ export async function statPathsForMtime(paths, workDir, concurrency = 64, opts =
302
302
  // lstat variant — does NOT follow symlinks. Use for directory-listing
303
303
  // surfaces where a symlink to a 200 GB file should report as a symlink,
304
304
  // not as the target's size/mtime.
305
- export async function lstatPathsForMtime(paths, workDir, concurrency = 64, opts = {}) {
305
+ export async function lstatPathsForMtime(paths, workDir, concurrency = Infinity, opts = {}) {
306
306
  const items = Array.isArray(paths) ? paths : [];
307
307
  const out = new Array(items.length);
308
308
  const inflight = new Map();
@@ -19,8 +19,8 @@ export function isBlockedDevicePath(p) {
19
19
 
20
20
  // R1: UNC / SMB share paths (\\server\share or //server/share). Reading these
21
21
  // causes Windows to auto-authenticate to the remote host, leaking the NTLM
22
- // hash of the current user to any attacker-controlled SMB target. CC parity:
23
- // FileReadTool.ts:461 rejects the same prefix before stat. Accepts both
22
+ // hash of the current user to any attacker-controlled SMB target. The prefix
23
+ // is rejected before stat. Accepts both
24
24
  // backslash and forward-slash leaders so a normalize that picked either form
25
25
  // is caught.
26
26
  export function isUncPath(p) {
@@ -17,7 +17,6 @@ import {
17
17
  } from './search-input-helpers.mjs';
18
18
 
19
19
  export const GREP_CONTEXT_CHAR_BUDGET_DEFAULT = 10_000;
20
- const GREP_CONTEXT_READ_CONCURRENCY = 4;
21
20
  const GREP_FOCUSED_CONTEXT_RADIUS = 12;
22
21
  const GREP_FOCUSED_RAW_BLOCKS = 3;
23
22
  // Anchors must stay usable as evidence without a follow-up read: keep the
@@ -229,21 +228,13 @@ async function readAnchorSources(anchors, radius, signal) {
229
228
  groups.get(anchor.absolutePath).anchors.push(anchor);
230
229
  }
231
230
  const entries = [...groups.values()];
232
- let next = 0;
233
- const worker = async () => {
234
- while (next < entries.length) {
235
- const entry = entries[next++];
236
- try {
237
- entry.lines = await readFileWindows(entry, radius, signal);
238
- } catch (err) {
239
- entry.error = err;
240
- }
231
+ await Promise.all(entries.map(async (entry) => {
232
+ try {
233
+ entry.lines = await readFileWindows(entry, radius, signal);
234
+ } catch (err) {
235
+ entry.error = err;
241
236
  }
242
- };
243
- await Promise.all(Array.from(
244
- { length: Math.min(GREP_CONTEXT_READ_CONCURRENCY, entries.length) },
245
- () => worker(),
246
- ));
237
+ }));
247
238
  return groups;
248
239
  }
249
240
 
@@ -88,16 +88,12 @@ export async function executeListTool(args, workDir, options = {}) {
88
88
  args.path = coerceReadFamilyPathArg(args.path, workDir);
89
89
  if (Array.isArray(args.path)) {
90
90
  const list = [...new Set(args.path.map((p) => (typeof p === 'string' ? p.trim() : '')).filter((p) => p.length > 0))];
91
- const capped = list.length > 10;
92
- const targets = capped ? list.slice(0, 10) : list;
91
+ const targets = list;
93
92
  if (targets.length > 1) {
94
- // Bounded parallel fan-out: each target is an independent listing
93
+ // Full parallel fan-out: each target is an independent listing
95
94
  // (own guard/cache/reachability), so run them concurrently instead
96
95
  // of serially. Bodies land in a fixed-index array so the emitted
97
- // section order still matches the caller's `path[]` order — only the
98
- // wall-clock cost changes. Concurrency is capped so a 10-path batch
99
- // cannot exhaust the child-spawn / FS-handle budget.
100
- const LIST_FANOUT_CONCURRENCY = 4;
96
+ // section order still matches the caller's `path[]` order.
101
97
  // Collapse semantic-duplicate targets: two spellings that resolve to
102
98
  // the same directory (e.g. `foo` and `./foo`) are stat'd/walked ONCE.
103
99
  // Each caller label keeps its own `# list <p>` section reusing the
@@ -116,24 +112,14 @@ export async function executeListTool(args, workDir, options = {}) {
116
112
  if (!repByKey.has(k)) { repByKey.set(k, targets[i]); uniqueKeys.push(k); }
117
113
  }
118
114
  const bodyByKey = new Map();
119
- let cursor = 0;
120
- const runWorker = async () => {
121
- for (;;) {
122
- const i = cursor++;
123
- if (i >= uniqueKeys.length) return;
124
- const k = uniqueKeys[i];
125
- try {
126
- bodyByKey.set(k, await executeListTool({ ...args, path: repByKey.get(k) }, workDir, options));
127
- } catch (err) {
128
- bodyByKey.set(k, `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`);
129
- }
115
+ await Promise.all(uniqueKeys.map(async (k) => {
116
+ try {
117
+ bodyByKey.set(k, await executeListTool({ ...args, path: repByKey.get(k) }, workDir, options));
118
+ } catch (err) {
119
+ bodyByKey.set(k, `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`);
130
120
  }
131
- };
132
- await Promise.all(
133
- Array.from({ length: Math.min(LIST_FANOUT_CONCURRENCY, uniqueKeys.length) }, runWorker),
134
- );
121
+ }));
135
122
  const sections = targets.map((p, i) => `# list ${p}\n${bodyByKey.get(keyByIndex[i])}`);
136
- if (capped) sections.push(`... [capped at 10 of ${list.length} paths]`);
137
123
  return sections.join('\n\n');
138
124
  }
139
125
  args.path = targets[0];
@@ -582,8 +568,7 @@ async function getTargetedFindEnumeration({
582
568
  export async function executeFuzzyFindTool(args, workDir, options = {}) {
583
569
  if (Array.isArray(args.query)) {
584
570
  const list = [...new Set(args.query.map((q) => (typeof q === 'string' ? q.trim() : '')).filter((q) => q.length > 0))];
585
- const capped = list.length > 5;
586
- const targets = capped ? list.slice(0, 5) : list;
571
+ const targets = list;
587
572
  if (targets.length > 1) {
588
573
  // head_limit is one call-level result budget, not a multiplier per
589
574
  // query. Split it deterministically in caller order; if it cannot
@@ -601,7 +586,6 @@ export async function executeFuzzyFindTool(args, workDir, options = {}) {
601
586
  + `retry query=${JSON.stringify(omittedByHeadLimit)}]`,
602
587
  );
603
588
  }
604
- if (capped) notes.push(`... [capped at 5 of ${list.length} queries]`);
605
589
  const sectionHeaders = activeTargets.map((query) => `# find ${query}\n`);
606
590
  const separatorBytes = Math.max(0, activeTargets.length + notes.length - 1) * 2;
607
591
  const fixedBytes = sectionHeaders.reduce(
@@ -6,7 +6,7 @@ import {
6
6
  resizeImageBuffer,
7
7
  } from './read-image-resize.mjs';
8
8
 
9
- // Image extensions native Read renders as image blocks (FileReadTool.ts).
9
+ // Image extensions that Read renders as image blocks.
10
10
  const IMAGE_MIME = {
11
11
  '.png': 'image/png',
12
12
  '.jpg': 'image/jpeg',
@@ -112,7 +112,7 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
112
112
  // R1: UNC / SMB share reject (\\server\share, //server/share). Reading
113
113
  // these on Windows auto-authenticates to the remote host and leaks the
114
114
  // NTLM hash of the current user to any attacker-controlled SMB target.
115
- // CC parity: FileReadTool.ts:461 rejects the same prefix before stat.
115
+ // The prefix is rejected before stat.
116
116
  // Must run before resolveAgainstCwd so a relative path can't be coerced
117
117
  // into a UNC share by the cwd resolution.
118
118
  if (typeof isUncPath === 'function' && isUncPath(filePath))
@@ -142,7 +142,7 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
142
142
  return `Error: cannot read Windows device path (reserved name or raw-device namespace): ${normalizeOutputPath(fullPath)}`;
143
143
  if (typeof hasUnsafeWin32Component === 'function' && hasUnsafeWin32Component(fullPath))
144
144
  return `Error: cannot read Windows path with trailing dot/space or NTFS ADS suffix (bypasses device guard): ${normalizeOutputPath(fullPath)}`;
145
- // Pre-read size cap (Anthropic FileReadTool/limits.ts pattern):
145
+ // Pre-read size cap:
146
146
  // throw a small error response when the file is too big rather
147
147
  // than truncating to 25K tokens of content. Throw is decisively
148
148
  // more token-efficient (Anthropic #21841 reverted truncation).
@@ -712,7 +712,7 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
712
712
  out = appendReadContextAdvisory(out, { filePath, lineCount, bytes: st.size });
713
713
  }
714
714
  }
715
- // CC parity: empty file gets a system-reminder instead of
715
+ // An empty file gets a system-reminder instead of
716
716
  // a bare `1│` line. The reminder makes the empty-state
717
717
  // explicit so the agent doesn't assume content was elided.
718
718
  if (content.length === 0) {
@@ -24,20 +24,11 @@ function _stripLineCoordForReach(s) {
24
24
  .replace(/:\d+(?:-\d+)?(?::.*)?$/, '');
25
25
  }
26
26
 
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
27
  function _collectReachCandidates(p) {
34
28
  const out = [];
35
29
  const push = (s) => { if (typeof s === 'string' && s) out.push(s); };
36
30
  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);
31
+ else if (Array.isArray(p)) for (const e of p) push((e && typeof e === 'object') ? (e.path ?? e.file_path) : e);
41
32
  return out;
42
33
  }
43
34
 
@@ -181,14 +172,6 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
181
172
  // Single turn can touch many files or swap modes without
182
173
  // the agent iterating across multiple tool names.
183
174
  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
175
  // Per-file batch: each entry carries its own options.
193
176
  // Coalesce same-path entries: multiple chunks for the same
194
177
  // file are merged into a single wider read (min offset to max
@@ -244,11 +227,6 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
244
227
  args.mode = undefined; args.n = undefined; args.offset = undefined; args.limit = undefined; args.full = undefined;
245
228
  }
246
229
  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
230
  // Schema is `path: string | string[]` — array entries are
253
231
  // strings only. Top-level mode / n / offset / limit / full
254
232
  // apply uniformly to every entry in the batch (the only
@@ -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,10 @@ 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').then((releaseSlot) => new Promise((resolve, reject) => {
413
427
  // Probe after acquisition: this spawn counts toward inflight. The
414
428
  // answer is best-effort and only selects the per-process rg cap.
415
- const proc = spawn(rgExecutable(), _withRgThreads(argsList, hasRgSpareCapacity()), {
429
+ const proc = spawn(rgExecutable(), _withRgThreads(argsList, activeSearchSpawnCount()), {
416
430
  cwd: execOptions?.cwd,
417
431
  env: execOptions?.env || process.env,
418
432
  windowsHide: true,
@@ -591,9 +605,9 @@ function spawnRgWindowedLines(argsList, execOptions, opts = {}) {
591
605
  // Same gate + thread-cap + no-guardian treatment as spawnRg; queue-wait is
592
606
  // excluded from the rg timeout (armed after spawn). releaseSlot is fired
593
607
  // once via .finally on every settle path.
594
- return acquireChildSpawnSlot(gateSignal).then((releaseSlot) => new Promise((resolve, reject) => {
608
+ return acquireChildSpawnSlot(gateSignal, 'search').then((releaseSlot) => new Promise((resolve, reject) => {
595
609
  // As above, probe only after this spawn has acquired its gate slot.
596
- const proc = spawn(rgExecutable(), _withRgThreads(argsList, hasRgSpareCapacity()), {
610
+ const proc = spawn(rgExecutable(), _withRgThreads(argsList, activeSearchSpawnCount()), {
597
611
  cwd: execOptions?.cwd,
598
612
  env: execOptions?.env || process.env,
599
613
  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();