mixdog 0.9.95 → 0.9.97

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (282) hide show
  1. package/LICENSES/Apache-2.0.txt +201 -0
  2. package/LICENSES/MIT.txt +56 -0
  3. package/LICENSES/codex-NOTICE.txt +6 -0
  4. package/NOTICE.md +46 -9
  5. package/README.md +0 -17
  6. package/package.json +16 -12
  7. package/scripts/.tmp-coverage-report.mjs +96 -0
  8. package/scripts/build-tui.mjs +17 -1
  9. package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
  10. package/scripts/fixtures/patch-replay-corpus.json +98 -0
  11. package/src/defaults/mixdog-config.template.json +5 -8
  12. package/src/defaults/skills/setup/SKILL.md +1 -1
  13. package/src/lib/keychain-cjs.cjs +1 -1
  14. package/src/lib/mixdog-debug.cjs +2 -3
  15. package/src/rules/agent/30-explorer.md +7 -4
  16. package/src/rules/lead/01-general.md +1 -2
  17. package/src/rules/lead/lead-tool.md +0 -3
  18. package/src/rules/shared/01-tool.md +21 -29
  19. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +18 -2
  20. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +31 -1
  21. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +6 -17
  22. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +12 -1
  23. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -13
  24. package/src/runtime/agent/orchestrator/config.mjs +246 -67
  25. package/src/runtime/agent/orchestrator/mcp/client.mjs +104 -41
  26. package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +23 -0
  27. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +113 -14
  28. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +5 -1
  29. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +32 -11
  30. package/src/runtime/agent/orchestrator/providers/gemini.mjs +89 -2
  31. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -1
  32. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +19 -1
  33. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +144 -39
  34. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -8
  35. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -1
  36. package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +1 -1
  37. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +3 -4
  38. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -6
  39. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -2
  40. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +67 -7
  41. package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +291 -0
  42. package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +21 -0
  43. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +36 -10
  44. package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
  45. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -33
  46. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +13 -0
  47. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +2 -2
  48. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +0 -24
  49. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +21 -29
  50. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +7 -10
  51. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +45 -25
  52. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
  53. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +142 -84
  54. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
  55. package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +4 -0
  56. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +31 -1
  57. package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +38 -2
  58. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +29 -9
  59. package/src/runtime/agent/orchestrator/session/manager.mjs +2 -0
  60. package/src/runtime/agent/orchestrator/session/result-classification.mjs +65 -0
  61. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +86 -8
  62. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +10 -3
  63. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +4 -0
  64. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +71 -42
  65. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +4 -33
  66. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +4 -5
  67. package/src/runtime/agent/orchestrator/stall-policy.mjs +1 -1
  68. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +2 -2
  69. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +35 -1
  70. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +116 -26
  71. package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
  72. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +8 -9
  73. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +2 -2
  74. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +2 -2
  75. package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +2 -2
  76. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +6 -15
  77. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +12 -26
  78. package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +5 -5
  79. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +83 -33
  80. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +42 -45
  81. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +27 -9
  82. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +7 -0
  83. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +17 -39
  84. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +73 -0
  85. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +1 -1
  86. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +0 -2
  87. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +3 -8
  88. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +30 -0
  89. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +5 -5
  90. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +142 -26
  91. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
  92. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +47 -19
  93. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -2
  94. package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -2
  95. package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +83 -13
  96. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +39 -1
  97. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +1 -1
  98. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +94 -20
  99. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +16 -0
  100. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +108 -12
  101. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +12 -11
  102. package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
  103. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +63 -9
  104. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +2 -2
  105. package/src/runtime/channels/lib/config.mjs +34 -34
  106. package/src/runtime/channels/lib/inbound-handler.mjs +8 -8
  107. package/src/runtime/channels/lib/interaction-handlers.mjs +6 -6
  108. package/src/runtime/channels/lib/memory-client.mjs +31 -157
  109. package/src/runtime/channels/lib/output-forwarder.mjs +16 -16
  110. package/src/runtime/channels/lib/owned-runtime.mjs +60 -60
  111. package/src/runtime/channels/lib/owner-heartbeat.mjs +1 -1
  112. package/src/runtime/channels/lib/{backend-dispatch.mjs → provider-dispatch.mjs} +6 -6
  113. package/src/runtime/channels/lib/runtime-paths.mjs +4 -30
  114. package/src/runtime/channels/lib/scheduler.mjs +2 -2
  115. package/src/runtime/channels/lib/status-snapshot.mjs +3 -3
  116. package/src/runtime/channels/lib/tool-dispatch.mjs +4 -4
  117. package/src/runtime/channels/lib/worker-bootstrap.mjs +1 -1
  118. package/src/runtime/channels/lib/worker-ipc.mjs +7 -7
  119. package/src/runtime/channels/lib/worker-main.mjs +38 -38
  120. package/src/runtime/channels/{backends → providers}/discord-access.mjs +1 -1
  121. package/src/runtime/channels/{backends → providers}/discord-gateway.mjs +1 -1
  122. package/src/runtime/channels/{backends → providers}/discord.mjs +3 -3
  123. package/src/runtime/channels/{backends → providers}/telegram.mjs +12 -12
  124. package/src/runtime/channels/tool-defs.mjs +1 -1
  125. package/src/runtime/memory/index.mjs +76 -248
  126. package/src/runtime/memory/lib/agent-ipc.mjs +146 -81
  127. package/src/runtime/memory/lib/compact-vector-cache.mjs +88 -0
  128. package/src/runtime/memory/lib/core-memory-store.mjs +18 -1
  129. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +13 -34
  130. package/src/runtime/memory/lib/embedding-provider.mjs +47 -26
  131. package/src/runtime/memory/lib/embedding-worker.mjs +30 -34
  132. package/src/runtime/memory/lib/http-router.mjs +9 -6
  133. package/src/runtime/memory/lib/ko-morph.mjs +3 -20
  134. package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -1
  135. package/src/runtime/memory/lib/memory-config-flags.mjs +1 -1
  136. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -0
  137. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -2
  138. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +1 -0
  139. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +4 -1
  140. package/src/runtime/memory/lib/memory-cycle3.mjs +3 -1
  141. package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +5 -0
  142. package/src/runtime/memory/lib/memory-log.mjs +1 -1
  143. package/src/runtime/memory/lib/memory-process-lock.mjs +5 -60
  144. package/src/runtime/memory/lib/memory.mjs +1 -1
  145. package/src/runtime/memory/lib/pg/process.mjs +32 -9
  146. package/src/runtime/memory/lib/session-ingest.mjs +1 -1
  147. package/src/runtime/memory/tool-defs.mjs +1 -5
  148. package/src/runtime/search/index.mjs +2 -2
  149. package/src/runtime/search/tool-defs.mjs +2 -2
  150. package/src/runtime/shared/abort-race.mjs +70 -0
  151. package/src/runtime/shared/agent-route-config.mjs +119 -0
  152. package/src/runtime/shared/atomic-file.mjs +80 -4
  153. package/src/runtime/shared/channel-notification-routing.mjs +2 -2
  154. package/src/runtime/shared/child-guardian.mjs +204 -108
  155. package/src/runtime/shared/child-spawn-gate.mjs +139 -72
  156. package/src/runtime/shared/config.mjs +120 -47
  157. package/src/runtime/shared/llm/index.mjs +1 -1
  158. package/src/runtime/shared/owner-fair-gate.mjs +134 -0
  159. package/src/runtime/shared/resource-admission.mjs +150 -100
  160. package/src/runtime/shared/stream-progress.mjs +6 -0
  161. package/src/runtime/shared/tool-execution-owner.mjs +12 -0
  162. package/src/runtime/shared/tool-result-summary.mjs +1 -1
  163. package/src/runtime/shared/tool-surface.mjs +91 -27
  164. package/src/runtime/shared/tool-workload-gates.mjs +29 -0
  165. package/src/runtime/shared/turn-snapshot.mjs +47 -6
  166. package/src/runtime/shared/turn-worktree-snapshot.mjs +14 -2
  167. package/src/session-runtime/channel-config-api.mjs +5 -5
  168. package/src/session-runtime/config-helpers.mjs +39 -37
  169. package/src/session-runtime/config-lifecycle.mjs +27 -27
  170. package/src/session-runtime/lifecycle-api.mjs +22 -3
  171. package/src/session-runtime/memory-runtime-prewarm.mjs +15 -0
  172. package/src/session-runtime/model-route-api.mjs +7 -5
  173. package/src/session-runtime/prewarm.mjs +1 -1
  174. package/src/session-runtime/provider-auth-api.mjs +0 -7
  175. package/src/session-runtime/provider-models.mjs +1 -1
  176. package/src/session-runtime/remote-control.mjs +6 -7
  177. package/src/session-runtime/runtime-core.mjs +63 -39
  178. package/src/session-runtime/runtime-tunables.mjs +4 -4
  179. package/src/session-runtime/self-update.mjs +1 -1
  180. package/src/session-runtime/session-lifecycle.mjs +45 -17
  181. package/src/session-runtime/session-title.mjs +1 -1
  182. package/src/session-runtime/session-turn-api.mjs +96 -24
  183. package/src/session-runtime/settings-api.mjs +12 -24
  184. package/src/session-runtime/tool-defs.mjs +3 -3
  185. package/src/session-runtime/workflow-agents-api.mjs +55 -81
  186. package/src/session-runtime/workflow.mjs +14 -24
  187. package/src/standalone/agent-dispatch-broker.mjs +198 -0
  188. package/src/standalone/agent-tool/helpers.mjs +2 -2
  189. package/src/standalone/agent-tool/spawn-flow.mjs +2 -0
  190. package/src/standalone/agent-tool/spawn-preset.mjs +11 -21
  191. package/src/standalone/agent-tool/tool-def.mjs +0 -14
  192. package/src/standalone/channel-admin.mjs +53 -64
  193. package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +33 -19
  194. package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +127 -46
  195. package/src/standalone/channel-worker.mjs +124 -647
  196. package/src/standalone/{backend-daemon.mjs → daemon.mjs} +270 -154
  197. package/src/standalone/explore-tool.mjs +187 -70
  198. package/src/standalone/fair-call-scheduler.mjs +264 -0
  199. package/src/standalone/memory-runtime-proxy.mjs +58 -1
  200. package/src/standalone/session-client.mjs +1134 -0
  201. package/src/standalone/session-protocol.mjs +192 -0
  202. package/src/standalone/session-runtime-pool.mjs +393 -0
  203. package/src/standalone/session-runtime-record.mjs +20 -0
  204. package/src/standalone/session-runtime-worker.mjs +203 -0
  205. package/src/standalone/{engine-daemon-service.mjs → session-service.mjs} +481 -224
  206. package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +390 -121
  207. package/src/standalone/session-wire.mjs +55 -0
  208. package/src/tui/App.jsx +34 -21
  209. package/src/tui/app/app-format.mjs +1 -1
  210. package/src/tui/app/app-view.jsx +5 -2
  211. package/src/tui/app/channel-pickers.mjs +41 -41
  212. package/src/tui/app/core-memory-picker.mjs +19 -19
  213. package/src/tui/app/doctor.mjs +5 -11
  214. package/src/tui/app/input-parsers.mjs +2 -2
  215. package/src/tui/app/message-selector.mjs +103 -0
  216. package/src/tui/app/onboarding-steps.mjs +3 -4
  217. package/src/tui/app/project-picker.mjs +80 -56
  218. package/src/tui/app/prompt-submit.mjs +48 -43
  219. package/src/tui/app/resume-picker.mjs +2 -2
  220. package/src/tui/app/route-pickers.mjs +3 -1
  221. package/src/tui/app/settings-picker.mjs +31 -33
  222. package/src/tui/app/transcript-window.mjs +38 -0
  223. package/src/tui/app/use-prompt-draft-flow.mjs +5 -7
  224. package/src/tui/app/use-prompt-handlers.mjs +86 -21
  225. package/src/tui/app/use-prompt-hint.mjs +3 -2
  226. package/src/tui/app/use-prompt-queue-history.mjs +10 -3
  227. package/src/tui/app/use-transcript-scroll.mjs +10 -3
  228. package/src/tui/app/use-transcript-window.mjs +50 -17
  229. package/src/tui/components/PromptInput.jsx +71 -38
  230. package/src/tui/components/Spinner.jsx +1 -1
  231. package/src/tui/components/ToolExecution.jsx +1 -1
  232. package/src/tui/components/prompt-input/edit-helpers.mjs +9 -11
  233. package/src/tui/components/prompt-input/escape-policy.mjs +42 -0
  234. package/src/tui/components/prompt-input/immediate-render.mjs +0 -10
  235. package/src/tui/components/prompt-input/interrupt-policy.mjs +10 -0
  236. package/src/tui/components/prompt-input/restore-policy.mjs +10 -0
  237. package/src/tui/components/tool-execution/surface-detail.mjs +1 -1
  238. package/src/tui/dist/index.mjs +1031 -1676
  239. package/src/tui/hooks/useSession.mjs +16 -0
  240. package/src/tui/index.jsx +12 -13
  241. package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
  242. package/src/tui/{engine → session}/agent-job-feed.mjs +4 -4
  243. package/src/tui/{engine → session}/boot-profile.mjs +2 -2
  244. package/src/tui/{engine → session}/context-state.mjs +3 -3
  245. package/src/tui/{engine → session}/labels.mjs +2 -2
  246. package/src/tui/{engine → session}/live-share.mjs +38 -4
  247. package/src/tui/{engine → session}/notice-text.mjs +2 -2
  248. package/src/tui/{engine → session}/notification-plan.mjs +3 -3
  249. package/src/tui/session/oauth-flows.mjs +140 -0
  250. package/src/tui/{engine → session}/prompt-history.mjs +3 -3
  251. package/src/tui/{engine → session}/queue-helpers.mjs +2 -2
  252. package/src/tui/{engine → session}/render-timing.mjs +2 -2
  253. package/src/tui/{engine → session}/session-api-ext.mjs +30 -20
  254. package/src/tui/{engine → session}/session-api.mjs +110 -93
  255. package/src/tui/{engine → session}/session-flow.mjs +8 -2
  256. package/src/tui/{engine → session}/session-stats.mjs +3 -3
  257. package/src/tui/{engine → session}/tool-approval.mjs +3 -3
  258. package/src/tui/{engine → session}/tool-call-fields.mjs +2 -2
  259. package/src/tui/{engine → session}/tool-card-results.mjs +8 -6
  260. package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
  261. package/src/tui/{engine → session}/tool-result-text.mjs +13 -3
  262. package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
  263. package/src/tui/{engine → session}/turn.mjs +38 -176
  264. package/src/tui/{engine-local-session.mjs → session-local.mjs} +103 -103
  265. package/src/tui/session.mjs +22 -0
  266. package/src/tui/spinner-meta.mjs +4 -4
  267. package/src/tui/spinner-verbs.mjs +51 -3
  268. package/src/ui/statusline-segments.mjs +1 -1
  269. package/src/ui/statusline.mjs +1 -1
  270. package/vendor/ink/build/components/App.js +10 -1
  271. package/src/session-runtime/memory-daemon-probe.mjs +0 -61
  272. package/src/standalone/engine-daemon-client.mjs +0 -894
  273. package/src/standalone/engine-daemon-local-bridge.mjs +0 -20
  274. package/src/standalone/engine-daemon-protocol.mjs +0 -33
  275. package/src/tui/engine/turn-watchdog.mjs +0 -92
  276. package/src/tui/engine.mjs +0 -22
  277. package/src/tui/hooks/useEngine.mjs +0 -16
  278. /package/src/runtime/agent/orchestrator/session/compact/{engine.mjs → runner.mjs} +0 -0
  279. /package/src/runtime/channels/{backends → providers}/discord-attachments.mjs +0 -0
  280. /package/src/tui/{engine → session}/agent-response-tail.mjs +0 -0
  281. /package/src/tui/{engine → session}/frame-batched-store.mjs +0 -0
  282. /package/src/tui/{engine → session}/tui-steering-persist.mjs +0 -0
@@ -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,
@@ -51,7 +53,8 @@ import {
51
53
 
52
54
  const CODE_GRAPH_BATCHABLE_MODES = new Set(['symbol', 'find_symbol', 'symbol_search', 'callers', 'callees', 'references']);
53
55
  const CODE_GRAPH_FILE_BATCHABLE_MODES = new Set(['imports', 'dependents', 'related', 'impact', 'symbols', 'overview']);
54
- const CODE_GRAPH_SYMBOL_BATCH_CAP = 20;
56
+ const CODE_GRAPH_MAX_BATCH_SYMBOLS = 20;
57
+ const CODE_GRAPH_FILE_BATCH_CAP = 20;
55
58
 
56
59
  function _collectGraphSymbolList(args) {
57
60
  const split = (s) => String(s || '').split(/[,\s]+/).map((t) => t.trim()).filter(Boolean);
@@ -60,18 +63,21 @@ function _collectGraphSymbolList(args) {
60
63
  ...(typeof args?.symbols === 'string' ? split(args.symbols) : []),
61
64
  ...(typeof args?.symbol === 'string' ? split(args.symbol) : []),
62
65
  ])];
63
- if (list.length <= CODE_GRAPH_SYMBOL_BATCH_CAP) return list;
64
- const capped = list.slice(0, CODE_GRAPH_SYMBOL_BATCH_CAP);
65
- capped._omitted = list.slice(CODE_GRAPH_SYMBOL_BATCH_CAP);
66
- return capped;
66
+ return list;
67
67
  }
68
68
 
69
- const CODE_GRAPH_FILE_BATCH_CAP = 20;
70
69
  const _AGGREGATE_FILE_WILDCARD_RE = /[*?[\]{}]/;
71
70
  const ROOT_FEDERATED_MODES = new Set([
72
71
  'overview', 'symbol', 'find_symbol', 'symbol_search', 'search',
73
72
  'references', 'callers', 'callees', 'symbols', 'prewarm',
74
73
  ]);
74
+ // Fan-out cap when federation targets are DISCOVERED (immediate child project
75
+ // roots of a sentinel-free cwd) rather than registered. Bounds the cost of
76
+ // answering a query at a multi-repo parent; registered roots are unaffected.
77
+ const CODE_GRAPH_DISCOVERED_FEDERATION_CAP = (() => {
78
+ const raw = parseInt(process.env.MIXDOG_CODE_GRAPH_FEDERATION_CAP ?? '', 10);
79
+ return Number.isFinite(raw) && raw > 0 ? raw : 8;
80
+ })();
75
81
 
76
82
  export async function _runCodeGraphFederation(roots, runOne, projectArgs) {
77
83
  return Promise.all((roots || []).map(async (root) => {
@@ -132,6 +138,25 @@ function _hasAggregateFileArgs(args) {
132
138
  || (typeof args?.files === 'string' && args.files.trim());
133
139
  }
134
140
 
141
+ // Aggregate anchors that ALL resolve to the cwd itself ('.', './', the cwd
142
+ // path) add no scope — they mean "search here". Detected so the call can take
143
+ // the plain-cwd route, which adopts a sentinel-free single tree (a vendored
144
+ // reference checkout) as its own root while still refusing an unbounded or
145
+ // multi-project parent. Without this, `files:"."` turned every reference tree
146
+ // into a hard "not inside a project" refusal.
147
+ function _aggregateAnchorsAreCwd(args, baseCwd) {
148
+ if (!_hasAggregateFileArgs(args)) return false;
149
+ const files = _collectGraphFileList(args);
150
+ if (files.length === 0) return false;
151
+ return files.every((file) => {
152
+ const trimmed = String(file || '').trim();
153
+ if (!trimmed || _AGGREGATE_FILE_WILDCARD_RE.test(trimmed)) return false;
154
+ try {
155
+ return pathResolve(isAbsolute(trimmed) ? trimmed : pathResolve(baseCwd, trimmed)) === pathResolve(baseCwd);
156
+ } catch { return false; }
157
+ });
158
+ }
159
+
135
160
  // An invalid caller cwd may be recovered for an explicit files aggregate only
136
161
  // when every supplied anchor points at the same detectable project. Do not use
137
162
  // the batch cap here: an omitted anchor could belong to another project.
@@ -156,9 +181,35 @@ function _resolveAggregateFileProjectRoot(args, baseCwd) {
156
181
  const root = isDirectory ? _findDirProjectRoot(abs) : _resolveFileProjectRoot(abs);
157
182
  if (!root) return null;
158
183
  roots.add(pathResolve(root));
159
- if (roots.size > 1) return null;
160
184
  }
161
- return roots.size === 1 ? [...roots][0] : null;
185
+ if (roots.size === 0) return null;
186
+ if (roots.size === 1) return [...roots][0];
187
+ // Monorepo anchors legitimately resolve to DIFFERENT sentinels: a workspace
188
+ // package (apps/desktop/package.json) is nearer than the repo root, so one
189
+ // call spanning `apps/desktop/...` and `src/...` yields two roots even though
190
+ // both live in exactly one project. When one candidate contains every other,
191
+ // that outermost root IS the single detectable project — adopt it instead of
192
+ // refusing. Genuinely unrelated trees share no such candidate and still fail.
193
+ return _outermostContainingRoot([...roots]);
194
+ }
195
+
196
+ // The candidate that contains (or equals) every other candidate, else null.
197
+ // Only an EXISTING candidate can win: a bare common ancestor that has no
198
+ // sentinel of its own is never adopted as a project root.
199
+ function _outermostContainingRoot(roots) {
200
+ for (const candidate of roots) {
201
+ if (roots.every((root) => _isSameOrInside(root, candidate))) return candidate;
202
+ }
203
+ return null;
204
+ }
205
+
206
+ function _isSameOrInside(child, parent) {
207
+ try {
208
+ const rel = pathRelative(pathResolve(parent), pathResolve(child));
209
+ return rel === '' || (!!rel && !rel.startsWith('..') && !isAbsolute(rel));
210
+ } catch {
211
+ return false;
212
+ }
162
213
  }
163
214
 
164
215
  // Aggregate recovery resolves relative anchors against the caller's original
@@ -326,13 +377,19 @@ async function codeGraph(args, cwd, signal = null, options = {}) {
326
377
  const depsAll = [...(graph.reverse.get(depRel) || [])].sort();
327
378
  if (!depsAll.length) return '(no dependents)';
328
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
+ );
329
385
  const basename = depRel.split('/').pop();
330
386
  const stem = basename.replace(/\.[^/.]+$/, '');
331
387
  const enriched = deps.map((dep) => {
332
388
  const depNode = graph.nodes.get(dep);
333
389
  if (!depNode) return dep;
334
- let text;
335
- 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;
336
393
  const linesArr = text.split(/\r?\n/);
337
394
  for (let i = 0; i < linesArr.length; i++) {
338
395
  const ln = linesArr[i];
@@ -358,6 +415,8 @@ async function codeGraph(args, cwd, signal = null, options = {}) {
358
415
  if (mode === 'impact') {
359
416
  if (!node) return _appendSameBasenameHint(`Error: code_graph impact: file not found in graph: ${normFile || '(missing file)'}`, normFile, graph);
360
417
  const targetSymbol = String(args?.symbol || '').trim();
418
+ await _prewarmSourceTextNodes(graph, [node], { signal });
419
+ await _prewarmSourceTextNodes(graph, _impactSourceNodes(node, graph, targetSymbol), { signal });
361
420
  return _formatImpact(node, graph, cwd, targetSymbol);
362
421
  }
363
422
 
@@ -376,6 +435,7 @@ async function codeGraph(args, cwd, signal = null, options = {}) {
376
435
  if (!_CALLEES_BRACE_LANGS.has(declHit.lang)) {
377
436
  return `(callees unsupported for ${declHit.lang})`;
378
437
  }
438
+ await _prewarmSourceTextNodes(graph, [graph.nodes.get(declHit.rel)].filter(Boolean), { signal });
379
439
  const rows = _extractCallees(graph, declHit, cwd, {
380
440
  cap: 200,
381
441
  callerSymbol: symbol,
@@ -389,9 +449,11 @@ async function codeGraph(args, cwd, signal = null, options = {}) {
389
449
 
390
450
  if (mode === 'symbols') {
391
451
  if (!node) return _appendSameBasenameHint(`Error: code_graph symbols: file not found in graph: ${normFile || '(missing file)'}`, normFile, graph);
392
- let text = '';
393
- try { text = readFileSync(node.abs, 'utf8'); } catch { return '(no symbols)'; }
394
- 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)';
395
457
  }
396
458
 
397
459
  if (mode === 'find_symbol') {
@@ -400,6 +462,15 @@ async function codeGraph(args, cwd, signal = null, options = {}) {
400
462
  const language = String(args?.language || '').trim() || null;
401
463
  const limit = Math.max(1, Math.min(50, Number(args?.limit || 20)));
402
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
+ }
403
474
  return _findSymbolAcrossGraph(graph, symbol, cwd, { language, limit, fileRel: rel, body: args?.body !== false });
404
475
  }
405
476
 
@@ -412,6 +483,14 @@ async function codeGraph(args, cwd, signal = null, options = {}) {
412
483
  const keyword = String(args?.symbol || '').trim();
413
484
  const keywords = symbolsList.length ? symbolsList : (keyword ? [keyword] : []);
414
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
+ );
415
494
  if (keywords.length === 1) {
416
495
  return _searchSymbolsByKeyword(graph, keywords[0], cwd, { language, limit });
417
496
  }
@@ -438,6 +517,7 @@ async function codeGraph(args, cwd, signal = null, options = {}) {
438
517
  }
439
518
  }
440
519
  const narrowedByCaller = Boolean(rel || scopeRelPrefix || explicitLanguage);
520
+ if (node) await _prewarmSourceTextNodes(graph, [node], { signal });
441
521
  const resolved = _resolveReferenceLanguageNode(graph, symbol, rel, cwd, explicitLanguage);
442
522
  if (rel && resolved.kind === 'file-not-found') {
443
523
  return _appendSameBasenameHint(`Error: code_graph references: file not found in graph: ${normFile || '(missing file)'}`, normFile, graph);
@@ -452,7 +532,7 @@ async function codeGraph(args, cwd, signal = null, options = {}) {
452
532
  const userLimit = Number.isFinite(rawLimit) && rawLimit > 0
453
533
  ? Math.min(500, Math.floor(rawLimit))
454
534
  : null;
455
- const _refNodes = await _prewarmReferenceSourceText(graph, symbol, lang);
535
+ const _refNodes = await _prewarmReferenceSourceText(graph, symbol, lang, { signal });
456
536
  const refResult = _cheapReferenceSearch(graph, symbol, cwd, { language: lang, limit: userLimit, fileRel: rel, scopeRelPrefix, nodes: _refNodes });
457
537
  return narrowedByCaller ? refResult : _augmentNoHitDiagnostic(refResult, '(no references)', graph, cwd, symbol);
458
538
  }
@@ -468,6 +548,7 @@ async function codeGraph(args, cwd, signal = null, options = {}) {
468
548
  }
469
549
  }
470
550
  const narrowedByCaller = Boolean(rel || scopeRelPrefix || explicitLanguage);
551
+ if (node) await _prewarmSourceTextNodes(graph, [node], { signal });
471
552
  const resolved = _resolveReferenceLanguageNode(graph, symbol, rel, cwd, explicitLanguage);
472
553
  if (rel && resolved.kind === 'file-not-found') {
473
554
  return _appendSameBasenameHint(`Error: code_graph callers: file not found in graph: ${normFile || '(missing file)'}`, normFile, graph);
@@ -482,7 +563,7 @@ async function codeGraph(args, cwd, signal = null, options = {}) {
482
563
  const userLimit = Number.isFinite(rawLimit) && rawLimit > 0
483
564
  ? Math.min(500, Math.floor(rawLimit))
484
565
  : null;
485
- const _callerNodes = await _prewarmReferenceSourceText(graph, symbol, lang);
566
+ const _callerNodes = await _prewarmReferenceSourceText(graph, symbol, lang, { signal });
486
567
  const depth = Math.max(1, Math.min(5, Math.floor(Number(args?.depth) || 1)));
487
568
  if (depth > 1) {
488
569
  return _formatTransitiveCallers(graph, symbol, cwd, { language: lang, depth, page: args?.page });
@@ -527,12 +608,23 @@ async function findSymbolTool(args, cwd, signal = null, options = {}) {
527
608
  if (!symbol) {
528
609
  if (fileRel) {
529
610
  const node = graph.nodes.get(fileRel);
530
- let text = '';
531
- try { text = readFileSync(node.abs, 'utf8'); } catch { return '(no symbols)'; }
532
- 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)';
533
616
  }
534
617
  throw new Error('find_symbol: provide "symbol" (to locate) or "file" (to list its symbols).');
535
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
+ }
536
628
  return _findSymbolAcrossGraph(graph, symbol, cwd, { language, limit, fileRel, body: args?.body !== false });
537
629
  }
538
630
 
@@ -583,6 +675,7 @@ export async function resolveSymbolReadSpan(cwd, { symbol, path = null, language
583
675
  }
584
676
  if (!Number.isFinite(endLine) || endLine < startLine) {
585
677
  const node = graph.nodes.get(primary.rel);
678
+ await _prewarmSourceTextNodes(graph, [node].filter(Boolean));
586
679
  const srcText = node ? _getSourceTextForNode(graph, node) : null;
587
680
  const inferred = srcText ? _inferSpanEndByIndent(srcText.split('\n'), startLine) : null;
588
681
  if (inferred) {
@@ -624,6 +717,11 @@ async function executeCodeGraphToolRaw(name, args, cwd, signal = null, options =
624
717
  args = { ...args };
625
718
  delete args.file;
626
719
  }
720
+ if (symbolMode && _aggregateAnchorsAreCwd(args, baseCwd)) {
721
+ args = { ...args };
722
+ delete args.files;
723
+ delete args.file;
724
+ }
627
725
  const fileArg = (args && typeof args.file === 'string' && args.file.trim()) ? args.file.trim() : '';
628
726
  const hasAggregateFileArgs = _hasAggregateFileArgs(args);
629
727
  let effectiveCwd = baseCwd;
@@ -647,6 +745,20 @@ async function executeCodeGraphToolRaw(name, args, cwd, signal = null, options =
647
745
  ? []
648
746
  : collectTrustedCodeGraphRoots(baseCwd)
649
747
  .filter((root) => pathResolve(root) !== pathResolve(baseCwd));
748
+ // Trust registration is a ROUTING PREFERENCE, not an admission gate. A
749
+ // sentinel-free cwd whose children are obvious project roots (a refs/ folder
750
+ // of checkouts, a multi-repo parent) is answerable: federate over those
751
+ // children instead of refusing the call. Cost stays bounded by the existing
752
+ // per-project graph timeout and the federation fan-out cap — the same way the
753
+ // reference CLIs bound a wide search (time/output caps, never a scope
754
+ // refusal). Registered roots keep priority; discovered children only fill in
755
+ // when registration yields nothing.
756
+ const federationRootsAtBase = trustedRootsAtBase.length
757
+ ? trustedRootsAtBase
758
+ : (baseProjectRoot
759
+ ? []
760
+ : _childProjectRoots(baseCwd, { cap: CODE_GRAPH_DISCOVERED_FEDERATION_CAP })
761
+ .filter((root) => pathResolve(root) !== pathResolve(baseCwd)));
650
762
  // A sentinel-free cwd (multi-repo parent, vendored reference tree) is still
651
763
  // routable when trusted project roots live under it — federate over those
652
764
  // instead of refusing the call outright.
@@ -656,9 +768,12 @@ async function executeCodeGraphToolRaw(name, args, cwd, signal = null, options =
656
768
  && !fileArg
657
769
  && !hasAggregateFileArgs
658
770
  && ROOT_FEDERATED_MODES.has(rawModeAtBase)
659
- && trustedRootsAtBase.length > 0;
771
+ && federationRootsAtBase.length > 0;
660
772
  if (filesystemRootCwd || parentDotFederation || sentinelFreeFederation) {
661
- const trustedRoots = trustedRootsAtBase;
773
+ // A filesystem root stays on the REGISTERED set only: fanning out over
774
+ // every project directory on a whole drive is a different cost class than
775
+ // fanning out over one folder's children.
776
+ const trustedRoots = filesystemRootCwd ? trustedRootsAtBase : federationRootsAtBase;
662
777
  const files = exactDotFederation ? [] : aggregateFilesAtBase;
663
778
  const rawMode = rawModeAtBase;
664
779
  const canFederate = files.length > 0 || ROOT_FEDERATED_MODES.has(rawMode);
@@ -818,7 +933,8 @@ async function executeCodeGraphToolRaw(name, args, cwd, signal = null, options =
818
933
  ? findSymbolTool(_stripEmptyArgs(a), effectiveCwd, signal, options)
819
934
  : codeGraph(a, effectiveCwd, signal, options));
820
935
  if (CODE_GRAPH_BATCHABLE_MODES.has(batchMode)) {
821
- const symbolList = _collectGraphSymbolList(args);
936
+ const collectedSymbols = _collectGraphSymbolList(args);
937
+ const symbolList = collectedSymbols.slice(0, CODE_GRAPH_MAX_BATCH_SYMBOLS);
822
938
  if (symbolList.length > 1) {
823
939
  return (async () => {
824
940
  // Concurrent fan-out: the underlying graph build is single-flight
@@ -831,10 +947,10 @@ async function executeCodeGraphToolRaw(name, args, cwd, signal = null, options =
831
947
  catch (e) { body = `Error: ${e?.message || String(e)}`; }
832
948
  return `# ${batchMode} ${sym}\n${body}`;
833
949
  }));
834
- if (symbolList._omitted?.length) {
950
+ if (collectedSymbols.length > symbolList.length) {
835
951
  sections.push(
836
- `Note: symbol list capped at ${CODE_GRAPH_SYMBOL_BATCH_CAP}; `
837
- + `retry symbols=${JSON.stringify(symbolList._omitted)}.`,
952
+ `[arg-guard] notice: symbol list capped at ${CODE_GRAPH_MAX_BATCH_SYMBOLS} `
953
+ + `(${collectedSymbols.length - symbolList.length} omitted)`,
838
954
  );
839
955
  }
840
956
  return sections.join('\n\n');
@@ -847,8 +963,8 @@ async function executeCodeGraphToolRaw(name, args, cwd, signal = null, options =
847
963
  if (CODE_GRAPH_FILE_BATCHABLE_MODES.has(batchMode)) {
848
964
  const fileList = _collectGraphFileList(args);
849
965
  if (fileList.length > 1) {
850
- const capped = fileList._capped;
851
966
  return (async () => {
967
+ const capped = fileList._capped;
852
968
  const sections = await Promise.all(fileList.map(async (f) => {
853
969
  let body;
854
970
  try { body = await dispatchOne({ ...args, file: f, files: undefined }); }
@@ -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,37 +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
- const concurrency = 64;
273
282
  let next = 0;
274
- async function worker() {
275
- while (true) {
283
+ const worker = async () => {
284
+ while (!signal?.aborted) {
276
285
  const index = next++;
277
286
  if (index >= uncached.length) return;
278
287
  const node = uncached[index];
279
288
  try {
280
- const text = await readFile(node.abs, 'utf8');
289
+ const text = await codeGraphSourceIoAdmission.run(
290
+ ownerKey,
291
+ () => readFileImpl(node.abs, 'utf8'),
292
+ { signal },
293
+ );
281
294
  graph._sourceTextCache?.set(node.rel, { fingerprint: node.fingerprint || '', text });
282
- } catch { /* skip unreadable file */ }
295
+ } catch { /* skip unreadable/aborted file */ }
283
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));
284
304
  }
285
- const workerCount = Math.min(Math.max(1, concurrency), uncached.length);
286
- await Promise.all(Array.from({ length: workerCount }, () => worker()));
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);
287
315
  return candidateNodes;
288
316
  }
289
317
 
@@ -3,17 +3,18 @@ export const CODE_GRAPH_TOOL_DEFS = [
3
3
  name: 'code_graph',
4
4
  title: 'Code Graph',
5
5
  annotations: { title: 'Code Graph', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: false, compressibleLossless: true },
6
- description: 'Repo code structure/flow over source files. File modes take files[]; symbol modes take symbols[] — exact identifiers via find_symbol/references/callers/callees, keywords via symbol_search/search. Unsupported target arrays are omitted, never mixed.',
6
+ description: 'Source-file structure, symbol relations, and flow. File modes take files[]; symbol modes take symbols[] — exact identifiers via find_symbol/references/callers/callees, keywords via symbol_search/search (symbol-index terms). Unsupported target arrays are omitted, never mixed.',
7
7
  inputSchema: {
8
8
  type: 'object',
9
9
  properties: {
10
10
  mode: { type: 'string', enum: ['overview', 'imports', 'dependents', 'related', 'impact', 'symbols', 'find_symbol', 'symbol_search', 'search', 'references', 'callers', 'callees'], description: 'File modes={overview,imports,dependents,related,impact}; symbols with files[]=file outline; the rest are symbol modes.' },
11
11
  files: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], description: 'Source file path(s); supported targets only.' },
12
- symbols: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], description: 'Exact identifiers or keywords; batch multiple in one symbols[].' },
12
+ symbols: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], description: 'Exact identifiers or keywords; keywords are symbol-index terms; batch multiple in one symbols[].' },
13
13
  body: { type: 'boolean', description: 'Include body.' },
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;