mixdog 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (280) hide show
  1. package/README.md +47 -23
  2. package/package.json +33 -27
  3. package/scripts/_test-folder-dialog.mjs +30 -0
  4. package/scripts/agent-parallel-smoke.mjs +388 -0
  5. package/scripts/agent-tag-reuse-smoke.mjs +183 -0
  6. package/scripts/background-task-meta-smoke.mjs +38 -0
  7. package/scripts/boot-smoke.mjs +52 -9
  8. package/scripts/build-runtime-linux.sh +348 -0
  9. package/scripts/build-runtime-macos.sh +217 -0
  10. package/scripts/build-runtime-windows.ps1 +242 -0
  11. package/scripts/compact-active-turn-test.mjs +68 -0
  12. package/scripts/compact-smoke.mjs +859 -129
  13. package/scripts/compact-trigger-migration-smoke.mjs +187 -0
  14. package/scripts/fix-brief-fn.mjs +35 -0
  15. package/scripts/fix-format-tool-surface.mjs +24 -0
  16. package/scripts/fix-tool-exec-visible.mjs +42 -0
  17. package/scripts/generate-runtime-manifest.mjs +166 -0
  18. package/scripts/hook-bus-test.mjs +330 -0
  19. package/scripts/lead-workflow-smoke.mjs +33 -39
  20. package/scripts/live-worker-smoke.mjs +43 -37
  21. package/scripts/llm-trace-summary.mjs +315 -0
  22. package/scripts/memory-meta-concurrency-test.mjs +20 -0
  23. package/scripts/output-style-smoke.mjs +56 -15
  24. package/scripts/parent-abort-link-test.mjs +44 -0
  25. package/scripts/patch-agent-brief.mjs +48 -0
  26. package/scripts/patch-app.mjs +21 -0
  27. package/scripts/patch-app2.mjs +18 -0
  28. package/scripts/patch-dist-brief.mjs +96 -0
  29. package/scripts/patch-tool-exec.mjs +70 -0
  30. package/scripts/pretool-ask-runtime-test.mjs +54 -0
  31. package/scripts/provider-toolcall-test.mjs +376 -0
  32. package/scripts/reactive-compact-persist-smoke.mjs +124 -0
  33. package/scripts/sanitize-tool-pairs-test.mjs +260 -0
  34. package/scripts/session-context-bench.mjs +205 -33
  35. package/scripts/session-ingest-smoke.mjs +177 -0
  36. package/scripts/set-effort-config-test.mjs +41 -0
  37. package/scripts/smoke-runtime-negative.ps1 +106 -0
  38. package/scripts/smoke-runtime-negative.sh +97 -0
  39. package/scripts/smoke.mjs +25 -0
  40. package/scripts/tool-result-hook-test.mjs +48 -0
  41. package/scripts/tool-smoke.mjs +1222 -90
  42. package/scripts/toolcall-args-test.mjs +150 -0
  43. package/scripts/tui-background-failure-smoke.mjs +73 -0
  44. package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
  45. package/src/agents/debugger/AGENT.md +8 -0
  46. package/src/agents/explore/AGENT.md +4 -0
  47. package/src/agents/heavy-worker/AGENT.md +9 -3
  48. package/src/agents/maintainer/AGENT.md +4 -0
  49. package/src/agents/reviewer/AGENT.md +8 -0
  50. package/src/agents/scheduler-task/AGENT.md +12 -0
  51. package/src/agents/scheduler-task/agent.json +6 -0
  52. package/src/agents/webhook-handler/AGENT.md +12 -0
  53. package/src/agents/webhook-handler/agent.json +6 -0
  54. package/src/agents/worker/AGENT.md +9 -3
  55. package/src/app.mjs +77 -3
  56. package/src/defaults/hidden-roles.json +17 -12
  57. package/src/headless-role.mjs +117 -0
  58. package/src/help.mjs +30 -0
  59. package/src/hooks/lib/permission-evaluator.cjs +11 -475
  60. package/src/lib/keychain-cjs.cjs +9 -1
  61. package/src/lib/mixdog-debug.cjs +0 -7
  62. package/src/lib/rules-builder.cjs +240 -96
  63. package/src/lib/text-utils.cjs +1 -1
  64. package/src/mixdog-session-runtime.mjs +2212 -446
  65. package/src/output-styles/default.md +12 -28
  66. package/src/output-styles/extreme-simple.md +9 -6
  67. package/src/output-styles/simple.md +22 -9
  68. package/src/repl.mjs +110 -59
  69. package/src/rules/agent/00-common.md +15 -0
  70. package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
  71. package/src/rules/agent/30-explorer.md +22 -0
  72. package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
  73. package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
  74. package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
  75. package/src/rules/lead/01-general.md +9 -5
  76. package/src/rules/lead/04-workflow.md +51 -12
  77. package/src/rules/lead/lead-tool.md +6 -0
  78. package/src/rules/shared/01-tool.md +12 -1
  79. package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
  80. package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
  81. package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
  82. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
  83. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
  84. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
  85. package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
  86. package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
  87. package/src/runtime/agent/orchestrator/config.mjs +174 -55
  88. package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
  89. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
  90. package/src/runtime/agent/orchestrator/internal-roles.mjs +76 -28
  91. package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
  92. package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
  93. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
  94. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +377 -243
  95. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -93
  96. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
  97. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
  98. package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
  99. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
  100. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
  101. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
  102. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
  103. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
  104. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
  106. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +221 -70
  107. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
  109. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
  110. package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
  111. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +31 -11
  112. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
  113. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  114. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  115. package/src/runtime/agent/orchestrator/session/loop.mjs +840 -612
  116. package/src/runtime/agent/orchestrator/session/manager.mjs +1574 -459
  117. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  118. package/src/runtime/agent/orchestrator/session/store.mjs +67 -29
  119. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  120. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  121. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  122. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +34 -7
  123. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
  124. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
  125. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  126. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  127. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  128. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  129. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  130. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  131. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  132. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  133. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  137. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  138. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  139. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  140. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  141. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  142. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  143. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  144. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  145. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  146. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  147. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  148. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  149. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  150. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  151. package/src/runtime/channels/index.mjs +152 -24
  152. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  153. package/src/runtime/channels/lib/session-discovery.mjs +3 -2
  154. package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
  155. package/src/runtime/channels/lib/webhook.mjs +1 -1
  156. package/src/runtime/channels/tool-defs.mjs +29 -29
  157. package/src/runtime/memory/index.mjs +520 -98
  158. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  159. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  160. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  161. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  162. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  163. package/src/runtime/memory/lib/memory-cycle1.mjs +28 -7
  164. package/src/runtime/memory/lib/memory-cycle2.mjs +4 -5
  165. package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
  166. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
  167. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  168. package/src/runtime/memory/lib/memory.mjs +88 -9
  169. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  170. package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
  171. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  172. package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
  173. package/src/runtime/memory/lib/session-ingest.mjs +194 -0
  174. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  175. package/src/runtime/memory/tool-defs.mjs +46 -37
  176. package/src/runtime/search/index.mjs +102 -466
  177. package/src/runtime/search/lib/web-tools.mjs +45 -25
  178. package/src/runtime/search/tool-defs.mjs +16 -23
  179. package/src/runtime/shared/abort-controller.mjs +1 -1
  180. package/src/runtime/shared/atomic-file.mjs +4 -3
  181. package/src/runtime/shared/background-tasks.mjs +122 -11
  182. package/src/runtime/shared/child-spawn-gate.mjs +145 -0
  183. package/src/runtime/shared/config.mjs +7 -4
  184. package/src/runtime/shared/err-text.mjs +131 -4
  185. package/src/runtime/shared/llm/cost.mjs +2 -2
  186. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  187. package/src/runtime/shared/llm/index.mjs +34 -11
  188. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  189. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  190. package/src/runtime/shared/singleton-owner.mjs +104 -0
  191. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  192. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  193. package/src/runtime/shared/tool-surface.mjs +624 -95
  194. package/src/runtime/shared/user-data-guard.mjs +0 -2
  195. package/src/standalone/agent-task-status.mjs +203 -0
  196. package/src/standalone/agent-task-status.test.mjs +76 -0
  197. package/src/standalone/agent-tool.mjs +1913 -0
  198. package/src/standalone/channel-worker.mjs +370 -14
  199. package/src/standalone/explore-tool.mjs +165 -70
  200. package/src/standalone/folder-dialog.mjs +314 -0
  201. package/src/standalone/hook-bus.mjs +898 -22
  202. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  203. package/src/standalone/projects.mjs +226 -0
  204. package/src/standalone/provider-admin.mjs +41 -24
  205. package/src/standalone/seeds.mjs +2 -69
  206. package/src/standalone/usage-dashboard.mjs +96 -8
  207. package/src/tui/App.jsx +4788 -2151
  208. package/src/tui/components/AnsiText.jsx +39 -28
  209. package/src/tui/components/ContextPanel.jsx +14 -4
  210. package/src/tui/components/Markdown.jsx +43 -77
  211. package/src/tui/components/MarkdownTable.jsx +9 -184
  212. package/src/tui/components/Message.jsx +28 -11
  213. package/src/tui/components/Picker.jsx +95 -56
  214. package/src/tui/components/PromptInput.jsx +367 -239
  215. package/src/tui/components/QueuedCommands.jsx +1 -1
  216. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  217. package/src/tui/components/Spinner.jsx +67 -38
  218. package/src/tui/components/StatusLine.jsx +606 -38
  219. package/src/tui/components/TextEntryPanel.jsx +128 -9
  220. package/src/tui/components/ToolExecution.jsx +592 -362
  221. package/src/tui/components/TurnDone.jsx +3 -3
  222. package/src/tui/components/UsagePanel.jsx +3 -5
  223. package/src/tui/components/tool-output-format.mjs +365 -0
  224. package/src/tui/components/tool-output-format.test.mjs +220 -0
  225. package/src/tui/dist/index.mjs +8826 -2390
  226. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  227. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  228. package/src/tui/engine.mjs +1455 -279
  229. package/src/tui/figures.mjs +21 -40
  230. package/src/tui/index.jsx +75 -31
  231. package/src/tui/input-editing.mjs +25 -0
  232. package/src/tui/markdown/format-token.mjs +511 -68
  233. package/src/tui/markdown/format-token.test.mjs +216 -0
  234. package/src/tui/markdown/render-ansi.mjs +94 -0
  235. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  236. package/src/tui/markdown/stream-fence.mjs +34 -0
  237. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  238. package/src/tui/markdown/table-layout.mjs +250 -0
  239. package/src/tui/paste-attachments.mjs +0 -7
  240. package/src/tui/spinner-verbs.mjs +1 -2
  241. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  242. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  243. package/src/tui/theme.mjs +746 -24
  244. package/src/tui/time-format.mjs +1 -1
  245. package/src/tui/transcript-tool-failures.mjs +67 -0
  246. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  247. package/src/ui/ansi.mjs +1 -2
  248. package/src/ui/markdown.mjs +85 -26
  249. package/src/ui/markdown.test.mjs +70 -0
  250. package/src/ui/model-display.mjs +121 -0
  251. package/src/ui/session-stats.mjs +44 -0
  252. package/src/ui/statusline-context-label.test.mjs +15 -0
  253. package/src/ui/statusline.mjs +386 -178
  254. package/src/ui/tool-card.mjs +2 -13
  255. package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
  256. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  257. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  258. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  259. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  260. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  261. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  262. package/src/workflows/default/WORKFLOW.md +12 -5
  263. package/src/workflows/default/workflow.json +0 -1
  264. package/src/workflows/solo/WORKFLOW.md +15 -0
  265. package/src/workflows/solo/workflow.json +7 -0
  266. package/vendor/ink/build/output.js +6 -1
  267. package/src/agents/scheduler-task.md +0 -3
  268. package/src/agents/web-researcher/AGENT.md +0 -3
  269. package/src/agents/web-researcher/agent.json +0 -6
  270. package/src/agents/webhook-handler.md +0 -3
  271. package/src/rules/bridge/00-common.md +0 -5
  272. package/src/rules/bridge/30-explorer.md +0 -4
  273. package/src/rules/lead/00-tool-lead.md +0 -5
  274. package/src/rules/shared/00-language.md +0 -3
  275. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  276. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  277. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  278. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  279. package/src/standalone/bridge-tool.mjs +0 -1414
  280. package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
@@ -12,23 +12,6 @@ function snapshotBodyWasReturnedByRead(snapshot) {
12
12
  return String(snapshot?.source || '').startsWith('read');
13
13
  }
14
14
 
15
- // Optional context-budget for a whole-file read: `max_lines:N` requests a
16
- // TIGHTER head+tail elision than the default 600-line / 200+100 cap, to bound
17
- // lead-context cost when only a glance is needed. Returns null (= default) when
18
- // unset. No heuristic guessing — the budget is explicit and reuses the existing
19
- // smartReadTruncate head/tail invariant. (`budget:'compact'` is a SEPARATE,
20
- // pre-existing knob handled upstream in read-tool.mjs applyCompactReadBudget —
21
- // it remaps a whole-file read to mode:'count' stats, not head+tail content.)
22
- function resolveReadBudget(args) {
23
- const ml = Number(args?.max_lines);
24
- if (Number.isFinite(ml) && ml > 0) {
25
- const maxLines = Math.trunc(ml);
26
- const headLines = Math.max(1, Math.ceil(maxLines * 0.7));
27
- return { maxLines, headLines, tailLines: Math.max(0, maxLines - headLines) };
28
- }
29
- return null;
30
- }
31
-
32
15
  function withSymbolReadNote(text, args) {
33
16
  const note = typeof args?._symbolReadNote === 'string' ? args._symbolReadNote.trim() : '';
34
17
  if (!note || typeof text !== 'string') return text;
@@ -159,9 +142,6 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
159
142
  // emits a compact truncation marker when rendered bytes overflow
160
143
  // READ_MAX_OUTPUT_BYTES.
161
144
  const limit = parseLineLimitArg(args.limit, wantFull ? Infinity : 2000);
162
- // Context-budget (compact / max_lines) — only meaningful on a whole-file
163
- // read (no range, not full). Ignored otherwise.
164
- const _readBudget = (!hasRangeArgs && !wantFull) ? resolveReadBudget(args) : null;
165
145
  let st;
166
146
  let _statErr;
167
147
  try {
@@ -265,7 +245,7 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
265
245
  }
266
246
  return _ipynbOut;
267
247
  }
268
- const cacheKey = `read|${fullPath}|${st.mtimeMs}|${st.size}|${hasOffsetArg ? offset : 'd'}|${hasLimitArg ? limit : 'd'}|${wantFull ? 'f' : 's'}|${_readBudget ? `b${_readBudget.maxLines}` : 'd'}`;
248
+ const cacheKey = `read|${fullPath}|${st.mtimeMs}|${st.size}|${hasOffsetArg ? offset : 'd'}|${hasLimitArg ? limit : 'd'}|${wantFull ? 'f' : 's'}`;
269
249
  // Race-guard helper: same-mtime same-size rapid rewrite (NTFS / exFAT 1 s
270
250
  // resolution) can pass mtimeMs+size yet differ in content. When the cache
271
251
  // entry stores a contentPrefixHash, recompute the current prefix and bail
@@ -358,7 +338,7 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
358
338
  _recordReadSnapshot(fullPath, st, readStateScope, cachedEntry.readSnapshotMeta || { source: 'read_cached' });
359
339
  // G6: file_unchanged stub. The full body is already in the
360
340
  // prior tool_result; resending it wastes cache_creation
361
- // tokens (Claude Code upstream measured ~18% on Read calls).
341
+ // tokens (reference upstream measured ~18% on Read calls).
362
342
  // The stub keeps the snapshot tracking intact (Edit
363
343
  // validation still works) while collapsing the response
364
344
  // payload. Falls back to the full body when the cached
@@ -496,19 +476,7 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
496
476
  ? await streamSmartReadSummary(fullPath, st, 'read_smart_stream')
497
477
  : null;
498
478
  if (_streamSmart?.text) {
499
- let out = _streamSmart.text;
500
- // Honor a compact/max_lines budget on a large file: the stream
501
- // already elided to head 200/tail 100; re-apply the tighter
502
- // head+tail so the lead sees only the requested glance.
503
- if (_readBudget) {
504
- // Use the file's REAL line count from the stream pass
505
- // (snapshotMeta.fileLineCount — the result has no top-level
506
- // totalLines), not the already-elided output's row count: the
507
- // re-budget marker otherwise reports "[TRUNCATED - 301 lines]"
508
- // for a 3800-line file.
509
- const _rebud = smartReadTruncate(out, _streamSmart.snapshotMeta?.fileLineCount || out.split('\n').length, st.size, filePath, _readBudget);
510
- if (_rebud?.truncated) out = _rebud.text;
511
- }
479
+ const out = _streamSmart.text;
512
480
  const snapshotMeta = _streamSmart.snapshotMeta || {
513
481
  source: 'read_smart_stream',
514
482
  ranges: [],
@@ -625,7 +593,7 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
625
593
  // green-light an overwrite against bytes the read never returned.
626
594
  let _byteCapTruncated = false;
627
595
  const smart = (!hasRangeArgs && !wantFull && typeof smartReadTruncate === 'function')
628
- ? smartReadTruncate(rendered, lineCount, st.size, filePath, _readBudget)
596
+ ? smartReadTruncate(rendered, lineCount, st.size, filePath)
629
597
  : null;
630
598
  let _smartTruncated = false;
631
599
  let _smartVisibleRanges = null;
@@ -79,34 +79,6 @@ async function _readReachPreflight(rawPath, workDir, helpers) {
79
79
  catch (e) { return `Error: ${e?.message || e}`; }
80
80
  }
81
81
 
82
- function capPositiveNumber(value, max, fallback = max) {
83
- const n = Number(value);
84
- if (!Number.isFinite(n) || n <= 0) return fallback;
85
- return Math.min(max, Math.max(1, Math.trunc(n)));
86
- }
87
-
88
- function applyCompactReadBudget(entry) {
89
- if (!entry || String(entry.budget || '').toLowerCase() !== 'compact') return entry;
90
- const next = { ...entry };
91
- const isFullMode = !next.mode || next.mode === 'full';
92
- const hasLine = next.line !== undefined && next.line !== null;
93
- const hasRange = next.offset !== undefined || next.limit !== undefined;
94
- const hasPathLine = hasLineCoordinate(next.path);
95
- if (hasLine && (next.context === undefined || next.context === null || Number(next.context) > 20)) {
96
- next.context = 20;
97
- }
98
- if (hasRange && (next.limit === undefined || next.limit === null || Number(next.limit) > 120)) {
99
- next.limit = 120;
100
- }
101
- if ((next.mode === 'head' || next.mode === 'tail') && (next.n === undefined || next.n === null || Number(next.n) > 80)) {
102
- next.n = capPositiveNumber(next.n, 80, 80);
103
- }
104
- if (isFullMode && !hasLine && !hasRange && !hasPathLine && next.full !== true) {
105
- next.mode = 'count';
106
- }
107
- return next;
108
- }
109
-
110
82
  export async function executeReadTool(args, workDir, readStateScope, executeChildBuiltinTool, options = {}, helpers = {}) {
111
83
  const {
112
84
  classifyResultKind,
@@ -217,10 +189,7 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
217
189
  if (r?.line !== undefined) entry.line = r.line;
218
190
  if (r?.context !== undefined) entry.context = r.context;
219
191
  if (r?.full !== undefined) entry.full = r.full;
220
- if (r?.budget !== undefined) entry.budget = r.budget;
221
- entry = applyCompactReadBudget(entry);
222
192
  entry = normaliseReadLineWindowArgs(entry, workDir);
223
- entry = applyCompactReadBudget(entry);
224
193
  return entry;
225
194
  });
226
195
  const _invertedRawEntry = rawEntries.find((e) => e && e._invertedRangeError);
@@ -269,10 +238,7 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
269
238
  if (args.line !== undefined) entry.line = args.line;
270
239
  if (args.context !== undefined) entry.context = args.context;
271
240
  if (args.full !== undefined) entry.full = args.full;
272
- if (args.budget !== undefined) entry.budget = args.budget;
273
- entry = applyCompactReadBudget(entry);
274
241
  entry = normaliseReadLineWindowArgs(entry, workDir);
275
- entry = applyCompactReadBudget(entry);
276
242
  return entry;
277
243
  });
278
244
  const _invertedStrEntry = entries.find((e) => e && e._invertedRangeError);
@@ -450,9 +416,9 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
450
416
  args.path = normalizeInputPath(args.path);
451
417
  // Symbol reads are span-driven. Models (notably gpt-5.5) co-send the whole
452
418
  // schema filled with placeholder/zero values (offset:0, limit:0, line:0,
453
- // context:0, pages:'', max_lines:0, mode, budget) alongside symbol, which
419
+ // context:0, pages:'', mode) alongside symbol, which
454
420
  // otherwise makes the symbol branch think a window was requested and falls
455
- // back to a 0-window read. Treat those zero/empty/mode/budget params as
421
+ // back to a 0-window read. Treat those zero/empty/mode params as
456
422
  // absent so the whole symbol body returns in ONE call; a MEANINGFUL window
457
423
  // (offset>0 / limit>0 / non-empty pages / real line) is kept and overrides.
458
424
  if (typeof args.symbol === 'string' && args.symbol.trim()) {
@@ -460,11 +426,8 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
460
426
  if (args.limit === 0) delete args.limit;
461
427
  if (args.line === 0) delete args.line;
462
428
  if (args.pages === '') delete args.pages;
463
- if (args.max_lines === 0) delete args.max_lines;
464
- if (args.budget !== undefined) delete args.budget;
465
429
  if (args.mode !== undefined) delete args.mode;
466
430
  }
467
- args = applyCompactReadBudget(args);
468
431
  const sym = typeof args.symbol === 'string' ? args.symbol.trim() : '';
469
432
  if (sym) {
470
433
  const hasOffset = args.offset !== undefined && args.offset !== null;
@@ -515,7 +478,6 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
515
478
  }
516
479
  args = normaliseReadLineWindowArgs(args, workDir);
517
480
  if (args._invertedRangeError) return args._invertedRangeError;
518
- args = applyCompactReadBudget(args);
519
481
  }
520
482
  // Mode routing. A window already dropped any conflicting head/tail/summary
521
483
  // glance above (so the window is served by executeSingleReadTool); what
@@ -1,15 +1,105 @@
1
- import { spawn, execFileSync } from 'child_process';
1
+ import { spawn, execFile } from 'child_process';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
- import { startChildGuardian } from '../../../../shared/child-guardian.mjs';
4
+ import { promisify } from 'node:util';
5
+ import os from 'node:os';
6
+ import { acquire as acquireChildSpawnSlot } from '../../../../shared/child-spawn-gate.mjs';
7
+
8
+ const execFileAsync = promisify(execFile);
9
+
10
+ // ── rg orphan-reap policy (accepted risk) ────────────────────────────────
11
+ // The previous design started a 1:1 node child-guardian per rg spawn so a
12
+ // hard daemon kill (SIGKILL, no chance to run our own cleanup) could not leave
13
+ // an rg orphaned. That per-process guardian is exactly what over-saturated the
14
+ // box (each grep doubled the process count), so it has been REMOVED here and
15
+ // deliberately NOT replaced with another per-rg guardian.
16
+ //
17
+ // Accepted residual risk: on a hard daemon SIGKILL an in-flight rg can briefly
18
+ // outlive the daemon. This is judged acceptable because (a) on every NORMAL
19
+ // path rg is reaped by this module's own timeout → SIGTERM → grace → force-kill
20
+ // plus the force-settle backstop, so it is always collected when the daemon is
21
+ // alive to settle the promise; (b) rg is a short-lived, self-terminating
22
+ // (bounded by the 20s timeout) read-only process, not a long-running server; a
23
+ // stray instance exits on its own shortly after. A shared/OS-job-based reaper
24
+ // (one guardian for the whole daemon, or a Windows Job Object / POSIX process
25
+ // group kill on daemon teardown) is the right long-term fix but is out of scope
26
+ // for this change and intentionally not added per-spawn.
27
+
28
+ // ── rg --threads cap ─────────────────────────────────────────────────────
29
+ // Each rg process otherwise fans out across EVERY core; with several agents
30
+ // running grep at once the box over-saturates and the whole batch trips the
31
+ // 20s deadline together. Cap rg's worker threads to a fraction of the host so
32
+ // concurrent greps share the machine. Internal dynamic default; env override
33
+ // only — deliberately NOT exposed on any tool schema / parameter surface.
34
+ function _rgThreadCap() {
35
+ const override = Number(process.env.MIXDOG_RG_THREADS);
36
+ if (Number.isFinite(override) && override >= 1) return Math.floor(override);
37
+ let cpus = 0;
38
+ try { cpus = os.cpus()?.length || 0; } catch { cpus = 0; }
39
+ return Math.max(2, Math.ceil((cpus || 4) / 4));
40
+ }
41
+
42
+ // Single source of truth for "did the caller already pin rg's thread count?".
43
+ // Detects every form rg accepts: separated (`-j`, `--threads`) where the count
44
+ // is the NEXT arg, short-attached (`-j8`), and long-attached (`--threads=8`).
45
+ // Used by both the _withRgThreads injection guard and the EAGAIN retry guard
46
+ // so the two never disagree.
47
+ function _hasRgThreadArg(argsList) {
48
+ for (const a of argsList) {
49
+ if (typeof a !== 'string') continue;
50
+ if (a === '-j' || a === '--threads') return true;
51
+ if (/^-j\d+$/.test(a)) return true;
52
+ if (a.startsWith('--threads=')) return true;
53
+ }
54
+ return false;
55
+ }
56
+
57
+ // Inject `--threads N` unless the caller already pinned thread count
58
+ // (`-j`/`--threads`/`-jN`/`--threads=N`, e.g. the EAGAIN `-j 1` retry). Does
59
+ // not mutate; may return the ORIGINAL array unchanged when already
60
+ // thread-pinned (callers must not assume a fresh copy).
61
+ function _withRgThreads(argsList) {
62
+ if (_hasRgThreadArg(argsList)) return argsList;
63
+ return ['--threads', String(_rgThreadCap()), ...argsList];
64
+ }
65
+
66
+ // Build the EAGAIN single-thread retry args. If the caller already pinned a
67
+ // thread count we strip the existing flag(s) first, then prepend `-j 1`, so the
68
+ // retry is unambiguously single-threaded (never `-j 8 ... -j 1`). Drops both the
69
+ // separated form (`-j N` / `--threads N` → flag + following count) and the
70
+ // attached forms (`-jN` / `--threads=N`).
71
+ function _rgEagainRetryArgs(argsList) {
72
+ const out = [];
73
+ for (let i = 0; i < argsList.length; i++) {
74
+ const a = argsList[i];
75
+ if (a === '-j' || a === '--threads') { i++; continue; } // skip flag + its count
76
+ if (typeof a === 'string' && (/^-j\d+$/.test(a) || a.startsWith('--threads='))) continue;
77
+ out.push(a);
78
+ }
79
+ return ['-j', '1', ...out];
80
+ }
81
+
82
+ // True only when the args are ALREADY exactly single-threaded — i.e. an EAGAIN
83
+ // retry would change nothing. Matches the separated `-j 1` / `--threads 1` form
84
+ // and the attached `-j1` / `--threads=1` form. Any other thread pin (e.g. -j8)
85
+ // is NOT "single-thread pinned": the retry should still run and downshift it.
86
+ function _isRgSingleThreadPinned(argsList) {
87
+ for (let i = 0; i < argsList.length; i++) {
88
+ const a = argsList[i];
89
+ if (typeof a !== 'string') continue;
90
+ if ((a === '-j' || a === '--threads') && String(argsList[i + 1]).trim() === '1') return true;
91
+ if (a === '-j1' || a === '--threads=1') return true;
92
+ }
93
+ return false;
94
+ }
5
95
 
6
96
  let _rgExecutableResolved = null;
7
97
 
8
- function _resolveRgExecutable() {
98
+ async function _resolveRgExecutable() {
9
99
  const isWin = process.platform === 'win32';
10
100
  try {
11
101
  const cmd = isWin ? 'where' : 'which';
12
- const out = execFileSync(cmd, ['rg'], { encoding: 'utf8', windowsHide: true }).trim();
102
+ const { stdout: out } = await execFileAsync(cmd, ['rg'], { encoding: 'utf8', windowsHide: true });
13
103
  const first = out.split(/\r?\n/).map((l) => l.trim()).find(Boolean);
14
104
  if (first && existsSync(first)) return first;
15
105
  } catch { /* fall through to PATH scan */ }
@@ -33,11 +123,27 @@ function _resolveRgExecutable() {
33
123
  return 'rg';
34
124
  }
35
125
 
126
+ // Synchronous accessor for the spawn call sites — only valid AFTER
127
+ // ensureRgResolved() has run (runRg/runRgWindowedLines await it first). Falls
128
+ // back to the bare 'rg' if somehow called before resolution.
36
129
  function rgExecutable() {
37
- if (_rgExecutableResolved === null) {
38
- _rgExecutableResolved = _resolveRgExecutable();
130
+ return _rgExecutableResolved ?? 'rg';
131
+ }
132
+
133
+ // Async resolver: the `where`/`which` lookup used to run via execFileSync,
134
+ // blocking the event loop ~90ms on the first grep/glob/find of a session
135
+ // (freezing the TUI). Resolve asynchronously and cache the result; subsequent
136
+ // calls return immediately.
137
+ let _rgResolvePromise = null;
138
+ async function ensureRgResolved() {
139
+ if (_rgExecutableResolved !== null) return _rgExecutableResolved;
140
+ if (!_rgResolvePromise) {
141
+ _rgResolvePromise = _resolveRgExecutable().then((resolved) => {
142
+ _rgExecutableResolved = resolved;
143
+ return resolved;
144
+ });
39
145
  }
40
- return _rgExecutableResolved;
146
+ return _rgResolvePromise;
41
147
  }
42
148
 
43
149
  // When _resolveRgExecutable() exhausts `where`/`which` and the PATH scan and
@@ -48,13 +154,13 @@ function rgExecutable() {
48
154
  const RG_FALLBACK_FAIL_TTL_MS = 30000;
49
155
  let _rgFallbackUsable = null;
50
156
  let _rgFallbackFailAt = 0;
51
- function rgFallbackUsable() {
157
+ async function rgFallbackUsable() {
52
158
  if (_rgFallbackUsable === true) return true;
53
159
  if (_rgFallbackUsable === false && (Date.now() - _rgFallbackFailAt) < RG_FALLBACK_FAIL_TTL_MS) {
54
160
  return false;
55
161
  }
56
162
  try {
57
- execFileSync('rg', ['--version'], { stdio: 'ignore', windowsHide: true, timeout: 3000 });
163
+ await execFileAsync('rg', ['--version'], { windowsHide: true, timeout: 3000 });
58
164
  _rgFallbackUsable = true;
59
165
  } catch {
60
166
  _rgFallbackUsable = false;
@@ -65,10 +171,11 @@ function rgFallbackUsable() {
65
171
 
66
172
  // Throws a clear error only when the bare 'rg' fallback was reached AND it is
67
173
  // not actually runnable. A real resolved path short-circuits with no probe, so
68
- // behavior is unchanged whenever rg exists.
69
- function assertRgAvailable() {
70
- if (rgExecutable() !== 'rg') return;
71
- if (rgFallbackUsable()) return;
174
+ // behavior is unchanged whenever rg exists. Async so the underlying lookups
175
+ // (`where`/`which`, `rg --version`) never block the event loop.
176
+ async function assertRgAvailable() {
177
+ if (await ensureRgResolved() !== 'rg') return;
178
+ if (await rgFallbackUsable()) return;
72
179
  const e = new Error('ripgrep (rg) not found on PATH — install ripgrep or add it to PATH');
73
180
  e.code = 'ERG_NOT_FOUND';
74
181
  throw e;
@@ -168,14 +275,19 @@ function _armRgForceSettle({ timeoutMs, isSettled, timers, proc, onForceSettle }
168
275
  // surfaced as empty stdout so callers can render "(no matches)" uniformly.
169
276
  function spawnRg(argsList, execOptions) {
170
277
  const timeoutMs = Number(execOptions?.timeout ?? 20000);
171
- return new Promise((resolve, reject) => {
172
- const proc = spawn(rgExecutable(), argsList, {
278
+ const gateSignal = execOptions?.abortSignal || execOptions?.signal || null;
279
+ // Gate the spawn (not the whole call) so over-saturation queues here while
280
+ // the cap's worth of rg keep running. The rg timeout timer is armed AFTER
281
+ // spawn below, so queue-wait time is excluded from the 20s deadline. No
282
+ // node guardian is started: rg owns its full SIGTERM→grace→force-kill +
283
+ // force-settle teardown, so the 1:1 guardian process was pure overhead.
284
+ return acquireChildSpawnSlot(gateSignal).then((releaseSlot) => new Promise((resolve, reject) => {
285
+ const proc = spawn(rgExecutable(), _withRgThreads(argsList), {
173
286
  cwd: execOptions?.cwd,
174
287
  env: execOptions?.env || process.env,
175
288
  windowsHide: true,
176
289
  stdio: ['ignore', 'pipe', 'pipe'],
177
290
  });
178
- startChildGuardian({ childPid: proc.pid, label: 'rg-runner' });
179
291
  let stdout = '';
180
292
  let stderr = '';
181
293
  let timedOut = false;
@@ -282,17 +394,23 @@ function spawnRg(argsList, execOptions) {
282
394
  e.stderr = stderr;
283
395
  reject(e);
284
396
  });
285
- });
397
+ // Release the gate slot exactly once on ANY settle path (resolve/reject:
398
+ // close, error, or force-settle). releaseSlot is idempotent.
399
+ }).finally(() => releaseSlot()));
286
400
  }
287
401
 
288
402
  export async function runRg(argsList, execOptions = {}) {
289
- assertRgAvailable();
403
+ await assertRgAvailable();
290
404
  try {
291
405
  return await spawnRg(argsList, execOptions);
292
406
  } catch (err) {
293
407
  const msg = String(err?.message || err?.stderr || '');
294
- if (/EAGAIN/i.test(msg) && !argsList.includes('-j')) {
295
- return spawnRg(['-j', '1', ...argsList], execOptions);
408
+ // Retry single-threaded on EAGAIN. Skip only when the caller already
409
+ // pinned threads AND that pin is already -j 1 (nothing to gain); any
410
+ // other pin (-j8/--threads=N) is rewritten to a clean -j 1 by
411
+ // _rgEagainRetryArgs so we never emit conflicting thread flags.
412
+ if (/EAGAIN/i.test(msg) && !_isRgSingleThreadPinned(argsList)) {
413
+ return spawnRg(_rgEagainRetryArgs(argsList), execOptions);
296
414
  }
297
415
  throw err;
298
416
  }
@@ -304,14 +422,17 @@ function spawnRgWindowedLines(argsList, execOptions, opts = {}) {
304
422
  const lineLimit = Number.isFinite(opts.limit) ? Math.max(0, Math.floor(Number(opts.limit) || 0)) : Infinity;
305
423
  const summaryLimit = Math.max(0, Math.floor(Number(opts.summaryLimit) || 0));
306
424
  const collectLimit = lineLimit === Infinity ? Infinity : Math.max(lineLimit, summaryLimit);
307
- return new Promise((resolve, reject) => {
308
- const proc = spawn(rgExecutable(), argsList, {
425
+ const gateSignal = execOptions?.abortSignal || execOptions?.signal || null;
426
+ // Same gate + thread-cap + no-guardian treatment as spawnRg; queue-wait is
427
+ // excluded from the rg timeout (armed after spawn). releaseSlot is fired
428
+ // once via .finally on every settle path.
429
+ return acquireChildSpawnSlot(gateSignal).then((releaseSlot) => new Promise((resolve, reject) => {
430
+ const proc = spawn(rgExecutable(), _withRgThreads(argsList), {
309
431
  cwd: execOptions?.cwd,
310
432
  env: execOptions?.env || process.env,
311
433
  windowsHide: true,
312
434
  stdio: ['ignore', 'pipe', 'pipe'],
313
435
  });
314
- startChildGuardian({ childPid: proc.pid, label: 'rg-runner-windowed' });
315
436
  let buffer = '';
316
437
  let stderr = '';
317
438
  let skipped = 0;
@@ -448,17 +569,17 @@ function spawnRgWindowedLines(argsList, execOptions, opts = {}) {
448
569
  e.stderr = stderr;
449
570
  reject(e);
450
571
  });
451
- });
572
+ }).finally(() => releaseSlot()));
452
573
  }
453
574
 
454
575
  export async function runRgWindowedLines(argsList, execOptions = {}, opts = {}) {
455
- assertRgAvailable();
576
+ await assertRgAvailable();
456
577
  try {
457
578
  return await spawnRgWindowedLines(argsList, execOptions, opts);
458
579
  } catch (err) {
459
580
  const msg = String(err?.message || err?.stderr || '');
460
- if (/EAGAIN/i.test(msg) && !argsList.includes('-j')) {
461
- return spawnRgWindowedLines(['-j', '1', ...argsList], execOptions, opts);
581
+ if (/EAGAIN/i.test(msg) && !_isRgSingleThreadPinned(argsList)) {
582
+ return spawnRgWindowedLines(_rgEagainRetryArgs(argsList), execOptions, opts);
462
583
  }
463
584
  throw err;
464
585
  }
@@ -114,13 +114,13 @@ export function buildGrepRgArgs(parts) {
114
114
  return rgArgs;
115
115
  }
116
116
 
117
- export function buildGlobCacheKey({ patterns, basePath, headLimit, offset, extraIgnore }) {
117
+ export function buildGlobCacheKey({ patterns, basePath, headLimit, offset, extraIgnore, sort }) {
118
118
  // extraIgnore (rg ignore globs from _extraIgnoreDirs) alters which files
119
119
  // match, so it MUST partake in the key — otherwise calls that differ only
120
120
  // by extra ignores collide and return stale over-/under-filtered results.
121
121
  // Sorted so the same ignore set in any order maps to one key.
122
122
  const extra = Array.isArray(extraIgnore) && extraIgnore.length ? [...extraIgnore].sort().join('\x01') : '';
123
- return ['glob', patterns.join('\x01'), basePath, headLimit ?? '', offset ?? '', extra].join('|');
123
+ return ['glob', patterns.join('\x01'), basePath, headLimit ?? '', offset ?? '', sort || 'natural', extra].join('|');
124
124
  }
125
125
 
126
126
  export function buildListCacheKey(parts) {