mixdog 0.9.96 → 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 (170) hide show
  1. package/README.md +0 -17
  2. package/package.json +8 -7
  3. package/scripts/build-tui.mjs +6 -6
  4. package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
  5. package/src/defaults/skills/setup/SKILL.md +1 -1
  6. package/src/lib/keychain-cjs.cjs +1 -1
  7. package/src/lib/mixdog-debug.cjs +2 -3
  8. package/src/rules/lead/01-general.md +1 -2
  9. package/src/rules/lead/lead-tool.md +0 -3
  10. package/src/rules/shared/01-tool.md +11 -10
  11. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -13
  12. package/src/runtime/agent/orchestrator/mcp/client.mjs +81 -33
  13. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -6
  14. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -2
  15. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +8 -6
  16. package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
  17. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -4
  18. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +20 -2
  19. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +4 -3
  20. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +40 -25
  21. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
  22. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
  23. package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +38 -2
  24. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +10 -3
  25. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +4 -0
  26. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +59 -42
  27. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +3 -2
  28. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +12 -22
  29. package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
  30. package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +2 -2
  31. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +2 -0
  32. package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +4 -4
  33. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +80 -30
  34. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +41 -22
  35. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +6 -2
  36. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +55 -11
  37. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
  38. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +55 -18
  39. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -0
  40. package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -2
  41. package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +2 -2
  42. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +1 -1
  43. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +67 -21
  44. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +2 -1
  45. package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
  46. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +7 -1
  47. package/src/runtime/channels/lib/config.mjs +33 -33
  48. package/src/runtime/channels/lib/inbound-handler.mjs +7 -7
  49. package/src/runtime/channels/lib/interaction-handlers.mjs +6 -6
  50. package/src/runtime/channels/lib/memory-client.mjs +31 -157
  51. package/src/runtime/channels/lib/output-forwarder.mjs +16 -16
  52. package/src/runtime/channels/lib/owned-runtime.mjs +60 -60
  53. package/src/runtime/channels/lib/owner-heartbeat.mjs +1 -1
  54. package/src/runtime/channels/lib/{backend-dispatch.mjs → provider-dispatch.mjs} +6 -6
  55. package/src/runtime/channels/lib/runtime-paths.mjs +4 -30
  56. package/src/runtime/channels/lib/status-snapshot.mjs +3 -3
  57. package/src/runtime/channels/lib/tool-dispatch.mjs +4 -4
  58. package/src/runtime/channels/lib/worker-bootstrap.mjs +1 -1
  59. package/src/runtime/channels/lib/worker-ipc.mjs +7 -7
  60. package/src/runtime/channels/lib/worker-main.mjs +38 -38
  61. package/src/runtime/channels/{backends → providers}/discord-access.mjs +1 -1
  62. package/src/runtime/channels/{backends → providers}/discord-gateway.mjs +1 -1
  63. package/src/runtime/channels/{backends → providers}/discord.mjs +3 -3
  64. package/src/runtime/channels/{backends → providers}/telegram.mjs +12 -12
  65. package/src/runtime/channels/tool-defs.mjs +1 -1
  66. package/src/runtime/memory/index.mjs +72 -211
  67. package/src/runtime/memory/lib/agent-ipc.mjs +9 -8
  68. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +2 -2
  69. package/src/runtime/memory/lib/http-router.mjs +1 -6
  70. package/src/runtime/memory/lib/memory-config-flags.mjs +1 -1
  71. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +4 -1
  72. package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +5 -0
  73. package/src/runtime/memory/lib/memory-log.mjs +1 -1
  74. package/src/runtime/memory/lib/memory-process-lock.mjs +5 -60
  75. package/src/runtime/memory/lib/memory.mjs +1 -1
  76. package/src/runtime/memory/lib/session-ingest.mjs +1 -1
  77. package/src/runtime/search/index.mjs +2 -2
  78. package/src/runtime/shared/abort-race.mjs +70 -0
  79. package/src/runtime/shared/child-spawn-gate.mjs +101 -63
  80. package/src/runtime/shared/config.mjs +11 -24
  81. package/src/runtime/shared/llm/index.mjs +1 -1
  82. package/src/runtime/shared/owner-fair-gate.mjs +134 -0
  83. package/src/runtime/shared/resource-admission.mjs +6 -5
  84. package/src/runtime/shared/tool-execution-owner.mjs +12 -0
  85. package/src/runtime/shared/tool-result-summary.mjs +1 -1
  86. package/src/runtime/shared/tool-surface.mjs +91 -22
  87. package/src/runtime/shared/tool-workload-gates.mjs +29 -0
  88. package/src/runtime/shared/turn-snapshot.mjs +17 -2
  89. package/src/session-runtime/channel-config-api.mjs +5 -5
  90. package/src/session-runtime/config-lifecycle.mjs +27 -27
  91. package/src/session-runtime/lifecycle-api.mjs +22 -3
  92. package/src/session-runtime/memory-runtime-prewarm.mjs +15 -0
  93. package/src/session-runtime/prewarm.mjs +1 -1
  94. package/src/session-runtime/provider-models.mjs +1 -1
  95. package/src/session-runtime/remote-control.mjs +6 -7
  96. package/src/session-runtime/runtime-core.mjs +52 -28
  97. package/src/session-runtime/self-update.mjs +1 -1
  98. package/src/session-runtime/session-lifecycle.mjs +36 -15
  99. package/src/session-runtime/session-title.mjs +1 -1
  100. package/src/session-runtime/session-turn-api.mjs +55 -15
  101. package/src/session-runtime/settings-api.mjs +7 -7
  102. package/src/standalone/agent-dispatch-broker.mjs +1 -1
  103. package/src/standalone/channel-admin.mjs +42 -53
  104. package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +16 -18
  105. package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +45 -46
  106. package/src/standalone/channel-worker.mjs +124 -647
  107. package/src/standalone/{backend-daemon.mjs → daemon.mjs} +187 -132
  108. package/src/standalone/memory-runtime-proxy.mjs +58 -1
  109. package/src/standalone/{engine-daemon-client.mjs → session-client.mjs} +151 -86
  110. package/src/standalone/session-protocol.mjs +8 -4
  111. package/src/standalone/session-runtime-pool.mjs +393 -0
  112. package/src/standalone/session-runtime-record.mjs +20 -0
  113. package/src/standalone/session-runtime-worker.mjs +203 -0
  114. package/src/standalone/{engine-daemon-service.mjs → session-service.mjs} +170 -157
  115. package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +139 -61
  116. package/src/standalone/session-wire.mjs +55 -0
  117. package/src/tui/App.jsx +4 -4
  118. package/src/tui/app/app-format.mjs +1 -1
  119. package/src/tui/app/channel-pickers.mjs +41 -41
  120. package/src/tui/app/input-parsers.mjs +2 -2
  121. package/src/tui/app/onboarding-steps.mjs +2 -2
  122. package/src/tui/app/project-picker.mjs +8 -8
  123. package/src/tui/app/prompt-submit.mjs +8 -8
  124. package/src/tui/app/settings-picker.mjs +25 -25
  125. package/src/tui/app/use-prompt-queue-history.mjs +1 -1
  126. package/src/tui/app/use-transcript-scroll.mjs +1 -1
  127. package/src/tui/components/ToolExecution.jsx +1 -1
  128. package/src/tui/components/prompt-input/restore-policy.mjs +1 -1
  129. package/src/tui/components/tool-execution/surface-detail.mjs +1 -1
  130. package/src/tui/dist/index.mjs +154 -127
  131. package/src/tui/hooks/useSession.mjs +16 -0
  132. package/src/tui/index.jsx +11 -11
  133. package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
  134. package/src/tui/{engine → session}/agent-job-feed.mjs +4 -4
  135. package/src/tui/{engine → session}/boot-profile.mjs +2 -2
  136. package/src/tui/{engine → session}/context-state.mjs +3 -3
  137. package/src/tui/{engine → session}/labels.mjs +2 -2
  138. package/src/tui/{engine → session}/live-share.mjs +7 -7
  139. package/src/tui/{engine → session}/notice-text.mjs +2 -2
  140. package/src/tui/{engine → session}/notification-plan.mjs +3 -3
  141. package/src/tui/session/oauth-flows.mjs +140 -0
  142. package/src/tui/{engine → session}/prompt-history.mjs +3 -3
  143. package/src/tui/{engine → session}/queue-helpers.mjs +2 -2
  144. package/src/tui/{engine → session}/render-timing.mjs +2 -2
  145. package/src/tui/{engine → session}/session-api-ext.mjs +30 -17
  146. package/src/tui/{engine → session}/session-api.mjs +7 -60
  147. package/src/tui/{engine → session}/session-flow.mjs +1 -1
  148. package/src/tui/{engine → session}/session-stats.mjs +3 -3
  149. package/src/tui/{engine → session}/tool-approval.mjs +3 -3
  150. package/src/tui/{engine → session}/tool-call-fields.mjs +2 -2
  151. package/src/tui/{engine → session}/tool-card-results.mjs +4 -4
  152. package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
  153. package/src/tui/{engine → session}/tool-result-text.mjs +3 -3
  154. package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
  155. package/src/tui/{engine → session}/turn.mjs +29 -107
  156. package/src/tui/{engine-local-session.mjs → session-local.mjs} +59 -75
  157. package/src/tui/session.mjs +22 -0
  158. package/src/ui/statusline-segments.mjs +1 -1
  159. package/src/ui/statusline.mjs +1 -1
  160. package/vendor/ink/build/components/App.js +10 -1
  161. package/src/session-runtime/memory-daemon-probe.mjs +0 -61
  162. package/src/standalone/engine-daemon-protocol.mjs +0 -32
  163. package/src/tui/engine/turn-watchdog.mjs +0 -92
  164. package/src/tui/engine.mjs +0 -22
  165. package/src/tui/hooks/useEngine.mjs +0 -16
  166. /package/src/runtime/agent/orchestrator/session/compact/{engine.mjs → runner.mjs} +0 -0
  167. /package/src/runtime/channels/{backends → providers}/discord-attachments.mjs +0 -0
  168. /package/src/tui/{engine → session}/agent-response-tail.mjs +0 -0
  169. /package/src/tui/{engine → session}/frame-batched-store.mjs +0 -0
  170. /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);
@@ -50,15 +52,14 @@ function _guardedReadError(p, helpers) {
50
52
  }
51
53
 
52
54
  // Reachability preflight for EVERY read shape (scalar / array / reads[]). MUST
53
- // run before any sync FS — including path normalization and the image
54
- // stat/read. A dead mount would otherwise freeze the event loop, defeating even
55
- // 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.
56
57
  async function _readReachPreflight(rawPath, workDir, helpers) {
57
58
  const {
58
59
  normalizeInputPath, resolveAgainstCwd,
59
60
  } = helpers;
60
61
  // A guarded path (UNC/SMB, Windows device, ADS, /dev/* block) must be
61
- // REJECTED here, not skipped: skipping would let the later sync guard/open
62
+ // REJECTED here, not skipped: skipping would let the later guard/open
62
63
  // path touch it and trigger NTLM/raw-device access or hang. Reject up front
63
64
  // with the same message the inline guards emit.
64
65
  // normalizeInputPath FIRST (FS-pure) so we stat the same path the real read
@@ -71,7 +72,7 @@ async function _readReachPreflight(rawPath, workDir, helpers) {
71
72
  const stripped = _stripLineCoordForReach(normalizeInputPath(raw));
72
73
  const full = resolveAgainstCwd(stripped, workDir);
73
74
  const guardMsg = _guardedReadError(stripped, helpers) || _guardedReadError(full, helpers);
74
- if (guardMsg) return guardMsg;
75
+ if (guardMsg) return { error: guardMsg, statsByPath: null };
75
76
  // Dedup by resolved path so a batch repeating the same file (or the
76
77
  // same union window) issues one stat probe, not one per entry —
77
78
  // bounding the preflight's FS work to the distinct target set.
@@ -79,9 +80,17 @@ async function _readReachPreflight(rawPath, workDir, helpers) {
79
80
  seenFull.add(full);
80
81
  candidates.push(full);
81
82
  }
82
- if (candidates.length === 0) return null;
83
- try { await assertPathsReachable(candidates); return null; }
84
- 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
+ }
85
94
  }
86
95
 
87
96
  export async function executeReadTool(args, workDir, readStateScope, executeChildBuiltinTool, options = {}, helpers = {}) {
@@ -118,8 +127,8 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
118
127
  }
119
128
  }
120
129
  args.path = coerceReadFamilyPathArg(args.path, workDir);
121
- // Reachability preflight up front (all shapes) — before readPathStringGuardError /
122
- // image stat, all of which can touch sync FS.
130
+ // Reachability preflight up front (all shapes) — before
131
+ // readPathStringGuardError / image stat.
123
132
  // options._skipReachPreflight: set only by the batch dispatcher on its
124
133
  // child reads (below). The parent batch call already ran this exact
125
134
  // preflight over EVERY candidate path in the array (_collectReachCandidates
@@ -128,8 +137,9 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
128
137
  // child (readPathStringGuardError / image fast-path) — only the async
129
138
  // reachability stat is skipped, never the security guards.
130
139
  if (options?._skipReachPreflight !== true) {
131
- const _reErr = await _readReachPreflight(args.path, workDir, helpers);
132
- 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 };
133
143
  }
134
144
  // Image files (png/jpg/jpeg/gif/webp): return an MCP image block so the
135
145
  // model can actually SEE the image. native Read does this, but mixdog's
@@ -140,7 +150,7 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
140
150
  if (options?.mediaTextOnly !== true && typeof args.path === 'string' && imageMimeForPath(args.path)) {
141
151
  const _imgNorm = normalizeInputPath(args.path);
142
152
  // W1 H: device-file / UNC / Windows-device / ADS guards must run
143
- // BEFORE the image fast-path so statSync/readFileSync of a
153
+ // BEFORE the image fast-path so stat/readFile of a
144
154
  // UNC/device path can't bypass the checks the normal read path
145
155
  // enforces (NTLM hash leak, raw-device access, ADS).
146
156
  if (typeof isUncPath === 'function' && isUncPath(_imgNorm))
@@ -158,7 +168,11 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
158
168
  return `Error: cannot read Windows device path (reserved name or raw-device namespace): ${normalizeOutputPath(_imgFull)}`;
159
169
  if (typeof hasUnsafeWin32Component === 'function' && hasUnsafeWin32Component(_imgFull))
160
170
  return `Error: cannot read Windows path with trailing dot/space or NTFS ADS suffix (bypasses device guard): ${normalizeOutputPath(_imgFull)}`;
161
- const _imgResult = await readImageAsContent(_imgFull, normalizeOutputPath(_imgNorm));
171
+ const _imgResult = await readImageAsContent(
172
+ _imgFull,
173
+ normalizeOutputPath(_imgNorm),
174
+ options?._preflightStats?.get?.(_imgFull) || null,
175
+ );
162
176
  if (_imgResult) return _imgResult;
163
177
  }
164
178
  // Unified-read dispatch (v0.6.283+):
@@ -399,7 +413,7 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
399
413
  let rangeHashes = rangeHashesByPath.get(fullPath) || [];
400
414
  if (rangeHashes.length === 0 && mergedRanges.length > 0) {
401
415
  try {
402
- const rawLines = readFileSync(fullPath, 'utf-8').split('\n');
416
+ const rawLines = (await readFile(fullPath, 'utf-8')).split('\n');
403
417
  rangeHashes = mergedRanges.map((range) => {
404
418
  const startIdx = Math.max(0, range.startLine - 1);
405
419
  const endIdx = Math.min(rawLines.length, range.endLine);
@@ -608,11 +622,16 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
608
622
  // Mode routing. A window already dropped any conflicting head/tail/summary
609
623
  // glance above (so the window is served by executeSingleReadTool); what
610
624
  // remains here is a mode-only read, or count/hex which are not text windows.
611
- if (args.mode === 'head') return executeChildBuiltinTool('head', { path: args.path, n: args.n }, workDir);
612
- if (args.mode === 'tail') return executeChildBuiltinTool('tail', { path: args.path, n: args.n }, workDir);
613
- if (args.mode === 'count') return executeChildBuiltinTool('wc', { path: args.path }, workDir);
614
- if (args.mode === 'summary') return executeChildBuiltinTool('summary', { path: args.path, n: args.n, limit: args.limit }, workDir);
615
- if (args.mode === 'hex') return executeChildBuiltinTool('hex', { path: args.path, n: args.n, offset: args.offset }, workDir);
616
- 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));
617
636
 
618
637
  }
@@ -423,7 +423,9 @@ function spawnRg(argsList, execOptions) {
423
423
  // spawn below, so queue-wait time is excluded from the 20s deadline. No
424
424
  // node guardian is started: rg owns its full SIGTERM→grace→force-kill +
425
425
  // force-settle teardown, so the 1:1 guardian process was pure overhead.
426
- return acquireChildSpawnSlot(gateSignal, 'search').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) => {
427
429
  // Probe after acquisition: this spawn counts toward inflight. The
428
430
  // answer is best-effort and only selects the per-process rg cap.
429
431
  const proc = spawn(rgExecutable(), _withRgThreads(argsList, activeSearchSpawnCount()), {
@@ -605,7 +607,9 @@ function spawnRgWindowedLines(argsList, execOptions, opts = {}) {
605
607
  // Same gate + thread-cap + no-guardian treatment as spawnRg; queue-wait is
606
608
  // excluded from the rg timeout (armed after spawn). releaseSlot is fired
607
609
  // once via .finally on every settle path.
608
- return acquireChildSpawnSlot(gateSignal, 'search').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) => {
609
613
  // As above, probe only after this spawn has acquired its gate slot.
610
614
  const proc = spawn(rgExecutable(), _withRgThreads(argsList, activeSearchSpawnCount()), {
611
615
  cwd: execOptions?.cwd,
@@ -4,7 +4,7 @@
4
4
  // code-graph.mjs.
5
5
  import { resolve as pathResolve, isAbsolute, relative as pathRelative, basename as pathBasename } from 'node:path';
6
6
  import { homedir as osHomedir } from 'node:os';
7
- import { readFileSync, existsSync, statSync } from 'node:fs';
7
+ import { existsSync, statSync } from 'node:fs';
8
8
  import { normalizeInputPath, toDisplayPath } from '../builtin.mjs';
9
9
  import { findFileByBasename } from '../builtin/path-diagnostics.mjs';
10
10
  import { markScopedCacheIncomplete } from '../../session/cache/scoped-cache-outcome.mjs';
@@ -40,7 +40,9 @@ import {
40
40
  _CALLEES_BRACE_LANGS,
41
41
  _formatRelated,
42
42
  _formatImpact,
43
+ _impactSourceNodes,
43
44
  _resolveReferenceLanguageNode,
45
+ _prewarmSourceTextNodes,
44
46
  _prewarmReferenceSourceText,
45
47
  _cheapReferenceSearch,
46
48
  _formatCallerReferences,
@@ -375,13 +377,19 @@ async function codeGraph(args, cwd, signal = null, options = {}) {
375
377
  const depsAll = [...(graph.reverse.get(depRel) || [])].sort();
376
378
  if (!depsAll.length) return '(no dependents)';
377
379
  const deps = depsAll.slice(0, GRAPH_LIST_CAP);
380
+ await _prewarmSourceTextNodes(
381
+ graph,
382
+ deps.map((dep) => graph.nodes.get(dep)).filter(Boolean),
383
+ { signal },
384
+ );
378
385
  const basename = depRel.split('/').pop();
379
386
  const stem = basename.replace(/\.[^/.]+$/, '');
380
387
  const enriched = deps.map((dep) => {
381
388
  const depNode = graph.nodes.get(dep);
382
389
  if (!depNode) return dep;
383
- let text;
384
- try { text = readFileSync(depNode.abs, 'utf8'); } catch { return dep; }
390
+ const cached = graph._sourceTextCache?.get(depNode.rel);
391
+ if (!cached || cached.fingerprint !== (depNode.fingerprint || '')) return dep;
392
+ const text = cached.text;
385
393
  const linesArr = text.split(/\r?\n/);
386
394
  for (let i = 0; i < linesArr.length; i++) {
387
395
  const ln = linesArr[i];
@@ -407,6 +415,8 @@ async function codeGraph(args, cwd, signal = null, options = {}) {
407
415
  if (mode === 'impact') {
408
416
  if (!node) return _appendSameBasenameHint(`Error: code_graph impact: file not found in graph: ${normFile || '(missing file)'}`, normFile, graph);
409
417
  const targetSymbol = String(args?.symbol || '').trim();
418
+ await _prewarmSourceTextNodes(graph, [node], { signal });
419
+ await _prewarmSourceTextNodes(graph, _impactSourceNodes(node, graph, targetSymbol), { signal });
410
420
  return _formatImpact(node, graph, cwd, targetSymbol);
411
421
  }
412
422
 
@@ -425,6 +435,7 @@ async function codeGraph(args, cwd, signal = null, options = {}) {
425
435
  if (!_CALLEES_BRACE_LANGS.has(declHit.lang)) {
426
436
  return `(callees unsupported for ${declHit.lang})`;
427
437
  }
438
+ await _prewarmSourceTextNodes(graph, [graph.nodes.get(declHit.rel)].filter(Boolean), { signal });
428
439
  const rows = _extractCallees(graph, declHit, cwd, {
429
440
  cap: 200,
430
441
  callerSymbol: symbol,
@@ -438,9 +449,11 @@ async function codeGraph(args, cwd, signal = null, options = {}) {
438
449
 
439
450
  if (mode === 'symbols') {
440
451
  if (!node) return _appendSameBasenameHint(`Error: code_graph symbols: file not found in graph: ${normFile || '(missing file)'}`, normFile, graph);
441
- let text = '';
442
- try { text = readFileSync(node.abs, 'utf8'); } catch { return '(no symbols)'; }
443
- return _extractSymbolsCheap(text, node.lang);
452
+ await _prewarmSourceTextNodes(graph, [node], { signal });
453
+ const cached = graph._sourceTextCache?.get(node.rel);
454
+ return cached && cached.fingerprint === (node.fingerprint || '')
455
+ ? _extractSymbolsCheap(cached.text, node.lang)
456
+ : '(no symbols)';
444
457
  }
445
458
 
446
459
  if (mode === 'find_symbol') {
@@ -449,6 +462,15 @@ async function codeGraph(args, cwd, signal = null, options = {}) {
449
462
  const language = String(args?.language || '').trim() || null;
450
463
  const limit = Math.max(1, Math.min(50, Number(args?.limit || 20)));
451
464
  if (rel && !node) return _appendSameBasenameHint(`Error: code_graph find_symbol: file not found in graph: ${normFile || '(missing file)'}`, normFile, graph);
465
+ if (args?.body !== false) {
466
+ const hits = _findSymbolHits(graph, symbol, { language });
467
+ const primary = hits.find((hit) => hit.declarationLike) || hits[0];
468
+ await _prewarmSourceTextNodes(
469
+ graph,
470
+ [primary?.rel ? graph.nodes.get(primary.rel) : null].filter(Boolean),
471
+ { signal },
472
+ );
473
+ }
452
474
  return _findSymbolAcrossGraph(graph, symbol, cwd, { language, limit, fileRel: rel, body: args?.body !== false });
453
475
  }
454
476
 
@@ -461,6 +483,14 @@ async function codeGraph(args, cwd, signal = null, options = {}) {
461
483
  const keyword = String(args?.symbol || '').trim();
462
484
  const keywords = symbolsList.length ? symbolsList : (keyword ? [keyword] : []);
463
485
  if (!keywords.length) throw new Error('code_graph symbol_search: "symbol" (or "symbols[]") is required.');
486
+ // Native graph symbols answer without source text. Nodes lacking native
487
+ // symbols fall back to cheap text extraction, so warm exactly that subset
488
+ // asynchronously before the synchronous formatter scans it.
489
+ await _prewarmSourceTextNodes(
490
+ graph,
491
+ [...graph.nodes.values()].filter((candidate) => !Array.isArray(candidate?.symbols) || candidate.symbols.length === 0),
492
+ { signal },
493
+ );
464
494
  if (keywords.length === 1) {
465
495
  return _searchSymbolsByKeyword(graph, keywords[0], cwd, { language, limit });
466
496
  }
@@ -487,6 +517,7 @@ async function codeGraph(args, cwd, signal = null, options = {}) {
487
517
  }
488
518
  }
489
519
  const narrowedByCaller = Boolean(rel || scopeRelPrefix || explicitLanguage);
520
+ if (node) await _prewarmSourceTextNodes(graph, [node], { signal });
490
521
  const resolved = _resolveReferenceLanguageNode(graph, symbol, rel, cwd, explicitLanguage);
491
522
  if (rel && resolved.kind === 'file-not-found') {
492
523
  return _appendSameBasenameHint(`Error: code_graph references: file not found in graph: ${normFile || '(missing file)'}`, normFile, graph);
@@ -501,7 +532,7 @@ async function codeGraph(args, cwd, signal = null, options = {}) {
501
532
  const userLimit = Number.isFinite(rawLimit) && rawLimit > 0
502
533
  ? Math.min(500, Math.floor(rawLimit))
503
534
  : null;
504
- const _refNodes = await _prewarmReferenceSourceText(graph, symbol, lang);
535
+ const _refNodes = await _prewarmReferenceSourceText(graph, symbol, lang, { signal });
505
536
  const refResult = _cheapReferenceSearch(graph, symbol, cwd, { language: lang, limit: userLimit, fileRel: rel, scopeRelPrefix, nodes: _refNodes });
506
537
  return narrowedByCaller ? refResult : _augmentNoHitDiagnostic(refResult, '(no references)', graph, cwd, symbol);
507
538
  }
@@ -517,6 +548,7 @@ async function codeGraph(args, cwd, signal = null, options = {}) {
517
548
  }
518
549
  }
519
550
  const narrowedByCaller = Boolean(rel || scopeRelPrefix || explicitLanguage);
551
+ if (node) await _prewarmSourceTextNodes(graph, [node], { signal });
520
552
  const resolved = _resolveReferenceLanguageNode(graph, symbol, rel, cwd, explicitLanguage);
521
553
  if (rel && resolved.kind === 'file-not-found') {
522
554
  return _appendSameBasenameHint(`Error: code_graph callers: file not found in graph: ${normFile || '(missing file)'}`, normFile, graph);
@@ -531,7 +563,7 @@ async function codeGraph(args, cwd, signal = null, options = {}) {
531
563
  const userLimit = Number.isFinite(rawLimit) && rawLimit > 0
532
564
  ? Math.min(500, Math.floor(rawLimit))
533
565
  : null;
534
- const _callerNodes = await _prewarmReferenceSourceText(graph, symbol, lang);
566
+ const _callerNodes = await _prewarmReferenceSourceText(graph, symbol, lang, { signal });
535
567
  const depth = Math.max(1, Math.min(5, Math.floor(Number(args?.depth) || 1)));
536
568
  if (depth > 1) {
537
569
  return _formatTransitiveCallers(graph, symbol, cwd, { language: lang, depth, page: args?.page });
@@ -576,12 +608,23 @@ async function findSymbolTool(args, cwd, signal = null, options = {}) {
576
608
  if (!symbol) {
577
609
  if (fileRel) {
578
610
  const node = graph.nodes.get(fileRel);
579
- let text = '';
580
- try { text = readFileSync(node.abs, 'utf8'); } catch { return '(no symbols)'; }
581
- return _extractSymbolsCheap(text, node.lang);
611
+ await _prewarmSourceTextNodes(graph, [node], { signal });
612
+ const cached = graph._sourceTextCache?.get(node.rel);
613
+ return cached && cached.fingerprint === (node.fingerprint || '')
614
+ ? _extractSymbolsCheap(cached.text, node.lang)
615
+ : '(no symbols)';
582
616
  }
583
617
  throw new Error('find_symbol: provide "symbol" (to locate) or "file" (to list its symbols).');
584
618
  }
619
+ if (args?.body !== false) {
620
+ const hits = _findSymbolHits(graph, symbol, { language });
621
+ const primary = hits.find((hit) => hit.declarationLike) || hits[0];
622
+ await _prewarmSourceTextNodes(
623
+ graph,
624
+ [primary?.rel ? graph.nodes.get(primary.rel) : null].filter(Boolean),
625
+ { signal },
626
+ );
627
+ }
585
628
  return _findSymbolAcrossGraph(graph, symbol, cwd, { language, limit, fileRel, body: args?.body !== false });
586
629
  }
587
630
 
@@ -632,6 +675,7 @@ export async function resolveSymbolReadSpan(cwd, { symbol, path = null, language
632
675
  }
633
676
  if (!Number.isFinite(endLine) || endLine < startLine) {
634
677
  const node = graph.nodes.get(primary.rel);
678
+ await _prewarmSourceTextNodes(graph, [node].filter(Boolean));
635
679
  const srcText = node ? _getSourceTextForNode(graph, node) : null;
636
680
  const inferred = srcText ? _inferSpanEndByIndent(srcText.split('\n'), startLine) : null;
637
681
  if (inferred) {
@@ -2,7 +2,6 @@
2
2
  // Symbol search / callers / callees / references / impact query layer over a
3
3
  // built graph. Pure over {graph,cwd,args}; owns no cache state. Extracted
4
4
  // verbatim from code-graph.mjs.
5
- import { readFileSync } from 'node:fs';
6
5
  import { _isJsLike } from './lang-predicates.mjs';
7
6
  import { _maskNonCodeText } from './text-mask.mjs';
8
7
  import {
@@ -61,6 +60,18 @@ function _collectImpactSymbols(node, graph) {
61
60
  return [...names];
62
61
  }
63
62
 
63
+ export function _impactSourceNodes(node, graph, targetSymbol = '') {
64
+ const symbols = targetSymbol ? [targetSymbol] : _collectImpactSymbols(node, graph).slice(0, 8);
65
+ const out = [];
66
+ const seen = new Set();
67
+ for (const candidate of [node, ...symbols.flatMap((symbol) => _lookupCandidateNodes(graph, symbol, node?.lang || null))]) {
68
+ if (!candidate?.rel || seen.has(candidate.rel)) continue;
69
+ seen.add(candidate.rel);
70
+ out.push(candidate);
71
+ }
72
+ return out;
73
+ }
74
+
64
75
  function _buildImpactSummary(node, graph, cwd, targetSymbol = '') {
65
76
  const imports = node.resolvedImports.map((p) => _graphRel(p, cwd));
66
77
  const dependents = [...(graph.reverse.get(node.rel) || [])].sort();
@@ -1,7 +1,8 @@
1
1
  // Symbol search / callers / callees / references / impact query layer over a
2
2
  // built graph. Pure over {graph,cwd,args}; owns no cache state. Extracted
3
3
  // verbatim from code-graph.mjs.
4
- import { readFileSync } from 'node:fs';
4
+ import { readFile } from 'node:fs/promises';
5
+ import { codeGraphSourceIoAdmission } from '../../../../shared/tool-workload-gates.mjs';
5
6
  import { _isJsLike } from './lang-predicates.mjs';
6
7
  import { _maskNonCodeText } from './text-mask.mjs';
7
8
  import {
@@ -30,7 +31,7 @@ import {
30
31
  _keywordMatchesSymbolName,
31
32
  } from './keyword-match.mjs';
32
33
 
33
- export { _formatRelated, _formatImpact, _findSymbolAcrossGraph, _resolveReferenceLanguageNode, _formatCallerReferences, _formatTransitiveCallers } from './search-references.mjs';
34
+ export { _formatRelated, _formatImpact, _impactSourceNodes, _findSymbolAcrossGraph, _resolveReferenceLanguageNode, _formatCallerReferences, _formatTransitiveCallers } from './search-references.mjs';
34
35
  export function _extractCallees(graph, declHit, _cwd, { cap = 200, callerSymbol = null, language = null } = {}) {
35
36
  if (!declHit || !_CALLEES_BRACE_LANGS.has(declHit.lang)) return [];
36
37
  const declNode = graph.nodes.get(declHit.rel);
@@ -253,28 +254,64 @@ export function _formatCalleeRow(row) {
253
254
  const enclosing = row.enclosing ? `(in ${row.enclosing})` : '(in ?)';
254
255
  return `${row.name}\t${callsite}\t${decl}\t${enclosing}`;
255
256
  }
256
- export async function _prewarmReferenceSourceText(graph, symbol, language) {
257
- const candidateNodes = _lookupCandidateNodes(graph, symbol, language);
258
- // Return the resolved candidate set so the immediately-following
259
- // _cheapReferenceSearch (references/callers dispatch) can reuse it instead
260
- // of recomputing _lookupCandidateNodes for the same (symbol, language) —
261
- // which on a token-index miss is a full-graph scan run twice per symbol.
262
- if (!candidateNodes.length) return candidateNodes;
257
+ const CODE_GRAPH_SOURCE_READ_CONCURRENCY = Math.max(
258
+ 1,
259
+ Math.min(32, Math.floor(Number(process.env.MIXDOG_CODE_GRAPH_SOURCE_READ_CONCURRENCY) || 8)),
260
+ );
261
+
262
+ export async function _prewarmSourceTextNodes(graph, nodes, {
263
+ concurrency = CODE_GRAPH_SOURCE_READ_CONCURRENCY,
264
+ readFileImpl = readFile,
265
+ signal = null,
266
+ ownerKey = null,
267
+ } = {}) {
268
+ const sourceNodes = [];
269
+ const seen = new Set();
270
+ for (const node of Array.isArray(nodes) ? nodes : []) {
271
+ if (!node?.rel || !node?.abs || seen.has(node.rel)) continue;
272
+ seen.add(node.rel);
273
+ sourceNodes.push(node);
274
+ }
263
275
  const uncached = [];
264
- for (const node of candidateNodes) {
276
+ for (const node of sourceNodes) {
265
277
  const cached = graph._sourceTextCache?.get(node.rel);
266
278
  if (!cached || cached.fingerprint !== (node.fingerprint || '')) {
267
279
  uncached.push(node);
268
280
  }
269
281
  }
270
- if (uncached.length === 0) return candidateNodes;
271
- const { readFile } = await import('fs/promises');
272
- await Promise.all(uncached.map(async (node) => {
273
- try {
274
- const text = await readFile(node.abs, 'utf8');
275
- graph._sourceTextCache?.set(node.rel, { fingerprint: node.fingerprint || '', text });
276
- } catch { /* skip unreadable file */ }
277
- }));
282
+ let next = 0;
283
+ const worker = async () => {
284
+ while (!signal?.aborted) {
285
+ const index = next++;
286
+ if (index >= uncached.length) return;
287
+ const node = uncached[index];
288
+ try {
289
+ const text = await codeGraphSourceIoAdmission.run(
290
+ ownerKey,
291
+ () => readFileImpl(node.abs, 'utf8'),
292
+ { signal },
293
+ );
294
+ graph._sourceTextCache?.set(node.rel, { fingerprint: node.fingerprint || '', text });
295
+ } catch { /* skip unreadable/aborted file */ }
296
+ }
297
+ };
298
+ const workerCount = Math.min(
299
+ Math.max(1, Math.floor(Number(concurrency) || CODE_GRAPH_SOURCE_READ_CONCURRENCY)),
300
+ Math.max(1, uncached.length),
301
+ );
302
+ if (uncached.length > 0) {
303
+ await Promise.all(Array.from({ length: workerCount }, worker));
304
+ }
305
+ return sourceNodes;
306
+ }
307
+
308
+ export async function _prewarmReferenceSourceText(graph, symbol, language, options = {}) {
309
+ const candidateNodes = _lookupCandidateNodes(graph, symbol, language);
310
+ // Return the resolved candidate set so the immediately-following
311
+ // _cheapReferenceSearch (references/callers dispatch) can reuse it instead
312
+ // of recomputing _lookupCandidateNodes for the same (symbol, language) —
313
+ // which on a token-index miss is a full-graph scan run twice per symbol.
314
+ await _prewarmSourceTextNodes(graph, candidateNodes, options);
278
315
  return candidateNodes;
279
316
  }
280
317
 
@@ -14,6 +14,7 @@ export const CODE_GRAPH_TOOL_DEFS = [
14
14
  limit: { type: 'number', minimum: 1, description: 'Max results.' },
15
15
  depth: { type: 'number', minimum: 1, maximum: 5, description: 'Caller depth.' },
16
16
  page: { type: 'number', minimum: 1, description: 'Caller page.' },
17
+ cwd: { type: 'string', description: 'Explicit root.' },
17
18
  },
18
19
  required: ['mode'],
19
20
  additionalProperties: false,
@@ -120,8 +120,7 @@ export function scrubRuntimeRootVars(env) {
120
120
  // Daemon IDENTITY leak: user-facing children must not inherit host/worker
121
121
  // ownership or the daemon's terminal-lead PID. Internal daemon/worker
122
122
  // spawns set every identity value explicitly after this boundary.
123
- delete env.MIXDOG_ENGINE_DAEMON_HOST;
124
- delete env.MIXDOG_CHANNEL_DAEMON;
123
+ delete env.MIXDOG_DAEMON_HOST;
125
124
  delete env.MIXDOG_WORKER_MODE;
126
125
  delete env.MIXDOG_DAEMON_SPAWNED_FOR;
127
126
  delete env.MIXDOG_SUPERVISOR_PID;
@@ -43,11 +43,11 @@ export function _resolvePwshPoolTarget({
43
43
  if (Number.isFinite(n) && n >= 0) return Math.min(8, n);
44
44
  }
45
45
  if (platform !== 'win32') return 0;
46
- // Tools now execute in the machine-global backend daemon. Keep client/test
46
+ // Tools now execute in the machine-global daemon. Keep client/test
47
47
  // processes at zero by default, but let that ONE host maintain a short-lived
48
48
  // warm burst sized to the machine instead of making every shell call pay the
49
49
  // ~300ms pwsh startup cost.
50
- if (env?.MIXDOG_ENGINE_DAEMON_HOST !== '1' && env?.MIXDOG_CHANNEL_DAEMON !== '1') return 0;
50
+ if (env?.MIXDOG_DAEMON_HOST !== '1') return 0;
51
51
  let cores = Number(parallelism);
52
52
  if (!Number.isFinite(cores) || cores < 1) {
53
53
  try { cores = availableParallelism(); } catch { cores = 1; }
@@ -2,7 +2,7 @@
2
2
  // mode gating, binary resolution, prewarm/idle lifecycle, and the char-indexed
3
3
  // EDIT client. Split out of patch.mjs; behavior is identical.
4
4
  //
5
- // Backend: NATIVE-ONLY. Every supported apply/edit case is dispatched to the
5
+ // Executor: NATIVE-ONLY. Every supported apply/edit case is dispatched to the
6
6
  // mixdog-patch Rust engine via the persistent stdio server. There is NO JS
7
7
  // apply fallback: unsupported / unsafe input returns a clean Error string.
8
8