mixdog 0.9.38 → 0.9.40

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 (124) hide show
  1. package/package.json +9 -4
  2. package/scripts/abort-recovery-test.mjs +43 -2
  3. package/scripts/agent-tag-reuse-smoke.mjs +146 -6
  4. package/scripts/agent-terminal-reap-test.mjs +127 -0
  5. package/scripts/agent-trace-io-test.mjs +69 -0
  6. package/scripts/code-graph-disk-hit-test.mjs +224 -0
  7. package/scripts/execution-completion-dedup-test.mjs +157 -0
  8. package/scripts/execution-pending-resume-kick-test.mjs +57 -2
  9. package/scripts/execution-resume-esc-integration-test.mjs +176 -0
  10. package/scripts/explore-bench.mjs +38 -2
  11. package/scripts/explore-prompt-policy-test.mjs +152 -11
  12. package/scripts/find-fuzzy-hidden-test.mjs +122 -0
  13. package/scripts/internal-comms-bench-test.mjs +226 -0
  14. package/scripts/internal-comms-bench.mjs +185 -58
  15. package/scripts/internal-comms-smoke.mjs +171 -23
  16. package/scripts/live-worker-smoke.mjs +38 -2
  17. package/scripts/memory-cycle-routing-test.mjs +111 -0
  18. package/scripts/memory-rule-contract-test.mjs +93 -0
  19. package/scripts/output-style-smoke.mjs +2 -2
  20. package/scripts/rg-runner-test.mjs +240 -0
  21. package/scripts/routing-corpus-test.mjs +349 -0
  22. package/scripts/routing-corpus.mjs +211 -32
  23. package/scripts/session-orphan-sweep-test.mjs +83 -0
  24. package/scripts/steering-drain-buckets-test.mjs +316 -2
  25. package/scripts/tool-smoke.mjs +21 -13
  26. package/scripts/tool-tui-presentation-test.mjs +202 -0
  27. package/scripts/tui-transcript-perf-test.mjs +279 -0
  28. package/src/agents/heavy-worker/AGENT.md +10 -7
  29. package/src/agents/reviewer/AGENT.md +6 -4
  30. package/src/agents/worker/AGENT.md +7 -5
  31. package/src/rules/agent/00-common.md +4 -4
  32. package/src/rules/agent/00-core.md +11 -14
  33. package/src/rules/agent/20-skip-protocol.md +3 -3
  34. package/src/rules/agent/30-explorer.md +50 -60
  35. package/src/rules/agent/40-cycle1-agent.md +15 -24
  36. package/src/rules/agent/41-cycle2-agent.md +33 -57
  37. package/src/rules/agent/42-cycle3-agent.md +28 -42
  38. package/src/rules/lead/01-general.md +7 -10
  39. package/src/rules/lead/lead-brief.md +11 -14
  40. package/src/rules/lead/lead-tool.md +6 -5
  41. package/src/rules/shared/01-tool.md +44 -45
  42. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +44 -1
  43. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +20 -5
  44. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +21 -1
  45. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +17 -38
  46. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +17 -8
  47. package/src/runtime/agent/orchestrator/session/context-utils.mjs +270 -78
  48. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +2 -1
  49. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +15 -0
  50. package/src/runtime/agent/orchestrator/session/manager/delivered-completions.mjs +123 -0
  51. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +15 -2
  52. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +67 -2
  53. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +1 -1
  54. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +2 -0
  55. package/src/runtime/agent/orchestrator/session/store.mjs +232 -43
  56. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +43 -1
  57. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -1
  58. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +27 -13
  59. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +25 -20
  60. package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +6 -2
  61. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +118 -53
  62. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +106 -29
  63. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +8 -1
  64. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +4 -2
  65. package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +3 -1
  66. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +4 -1
  67. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +33 -2
  68. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +151 -64
  69. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +37 -13
  70. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +84 -49
  71. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +172 -23
  72. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +3 -0
  73. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +68 -5
  74. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +17 -3
  75. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +24 -10
  76. package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +6 -3
  77. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -7
  78. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +1 -0
  79. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
  80. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -4
  81. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +4 -3
  82. package/src/runtime/memory/lib/memory-cycle3.mjs +12 -2
  83. package/src/runtime/shared/buffered-appender.mjs +13 -2
  84. package/src/runtime/shared/tool-primitives.mjs +4 -1
  85. package/src/runtime/shared/tool-status.mjs +27 -0
  86. package/src/runtime/shared/tool-surface.mjs +6 -3
  87. package/src/session-runtime/config-helpers.mjs +14 -0
  88. package/src/session-runtime/context-status.mjs +1 -0
  89. package/src/session-runtime/effort.mjs +6 -2
  90. package/src/session-runtime/lifecycle-api.mjs +4 -0
  91. package/src/session-runtime/model-recency.mjs +5 -2
  92. package/src/session-runtime/runtime-core.mjs +36 -2
  93. package/src/session-runtime/session-turn-api.mjs +12 -0
  94. package/src/session-runtime/tool-catalog.mjs +34 -0
  95. package/src/standalone/agent-tool/notify.mjs +13 -0
  96. package/src/standalone/agent-tool.mjs +53 -70
  97. package/src/standalone/explore-tool.mjs +6 -7
  98. package/src/tui/App.jsx +33 -1
  99. package/src/tui/app/model-options.mjs +5 -3
  100. package/src/tui/app/model-picker.mjs +12 -24
  101. package/src/tui/app/transcript-window.mjs +10 -10
  102. package/src/tui/app/use-transcript-window.mjs +38 -56
  103. package/src/tui/components/ToolExecution.jsx +11 -6
  104. package/src/tui/components/TranscriptItem.jsx +1 -1
  105. package/src/tui/components/tool-execution/surface-detail.mjs +24 -10
  106. package/src/tui/components/tool-execution/text-format.mjs +10 -19
  107. package/src/tui/dist/index.mjs +866 -300
  108. package/src/tui/engine/agent-job-feed.mjs +216 -19
  109. package/src/tui/engine/agent-response-tail.mjs +68 -0
  110. package/src/tui/engine/notification-plan.mjs +16 -0
  111. package/src/tui/engine/session-api.mjs +14 -2
  112. package/src/tui/engine/session-flow.mjs +22 -2
  113. package/src/tui/engine/tool-card-results.mjs +64 -37
  114. package/src/tui/engine/tool-result-status.mjs +75 -21
  115. package/src/tui/engine/turn.mjs +172 -77
  116. package/src/tui/engine.mjs +199 -39
  117. package/src/tui/index.jsx +2 -2
  118. package/src/workflows/bench/WORKFLOW.md +25 -35
  119. package/src/workflows/default/WORKFLOW.md +38 -32
  120. package/src/workflows/solo/WORKFLOW.md +19 -22
  121. package/scripts/_jitter-fuzz.mjs +0 -44410
  122. package/scripts/_jitter-fuzz2.mjs +0 -44400
  123. package/scripts/_jitter-probe.mjs +0 -44397
  124. package/scripts/_jp2.mjs +0 -45614
@@ -61,7 +61,6 @@ import {
61
61
  runResultCacheInFlight,
62
62
  statPathsForMtime,
63
63
  } from './cache-layers.mjs';
64
- import { recordReadSnapshot } from './read-snapshot-runtime.mjs';
65
64
  import { applyGrepContextLeadPolicy, GREP_CONTEXT_MAX, hasUnsupportedRipgrepRegex } from './arg-guard.mjs';
66
65
 
67
66
  // Default surrounding-lines window applied by output_mode:'content_with_context'
@@ -502,34 +501,62 @@ function formatGrepOutput({ windowed, totalWindowed, totalKnown, headLimit, offs
502
501
  export async function executeGrepTool(args, workDir, executeChildBuiltinTool, readStateScope = null, options = {}) {
503
502
  args = normalizeGrepArgs(args);
504
503
  args.path = coerceReadFamilyPathArg(args.path, workDir);
505
- // Fan-out guard: batch multiple string paths sequentially, mirroring
506
- // code_graph files[] batching. Recursive calls pass a single string
507
- // path, so recursion bottoms out after one level.
504
+ // Fan-out guard: batch multiple string paths with bounded concurrency,
505
+ // mirroring code_graph files[] batching. Recursive calls pass a single
506
+ // string path, so recursion bottoms out after one level. Results are
507
+ // assembled below in input order, regardless of completion order.
508
508
  if (Array.isArray(args.path)) {
509
509
  const GREP_PATH_CAP = 10;
510
+ const GREP_PATH_CONCURRENCY = 4;
510
511
  const seen = new Set();
511
512
  const list = args.path
512
513
  .map(p => typeof p === 'string' ? p.trim() : '')
513
514
  .filter(p => p && !seen.has(p) && seen.add(p));
514
515
  if (list.length > 1) {
515
516
  const capped = list.slice(0, GREP_PATH_CAP);
516
- const parts = [];
517
- for (const p of capped) {
518
- let body;
519
- try {
520
- body = await executeGrepTool({ ...args, path: p }, workDir, executeChildBuiltinTool, readStateScope, options);
521
- } catch (err) {
522
- body = `Error: ${err && err.message ? err.message : err}`;
517
+ const nestedOptions = { ...options, _grepPathFanout: true };
518
+ const configuredOutputCap = Number(options?.toolOutputMaxBytes) > 0
519
+ ? Math.trunc(Number(options.toolOutputMaxBytes))
520
+ : Math.trunc(Number(process.env.MIXDOG_TOOL_OUTPUT_MAX_BYTES));
521
+ const bodies = new Array(capped.length);
522
+ let next = 0;
523
+ const runWorker = async () => {
524
+ while (next < capped.length) {
525
+ const index = next++;
526
+ const p = capped[index];
527
+ try {
528
+ bodies[index] = await executeGrepTool(
529
+ { ...args, path: p },
530
+ workDir,
531
+ executeChildBuiltinTool,
532
+ readStateScope,
533
+ nestedOptions,
534
+ );
535
+ } catch (err) {
536
+ bodies[index] = `Error: ${err && err.message ? err.message : err}`;
537
+ }
523
538
  }
524
- parts.push(`# grep ${p}\n${body}`);
525
- }
539
+ };
540
+ await Promise.all(
541
+ Array.from(
542
+ { length: Math.min(GREP_PATH_CONCURRENCY, capped.length) },
543
+ () => runWorker(),
544
+ ),
545
+ );
546
+ const parts = capped.map((p, index) => `# grep ${p}\n${bodies[index]}`);
526
547
  if (list.length > GREP_PATH_CAP) {
527
548
  parts.push(`[capped at ${GREP_PATH_CAP} of ${list.length} paths]`);
528
549
  // Omitted paths mean the returned result cannot cover the whole
529
550
  // requested path set — never let it be cached as complete.
530
551
  if (options?.scopedCacheOutcome) markScopedCacheIncomplete(options.scopedCacheOutcome);
531
552
  }
532
- return parts.join('\n\n');
553
+ const output = parts.join('\n\n');
554
+ if (configuredOutputCap > 0
555
+ && Buffer.byteLength(output, 'utf8') > configuredOutputCap
556
+ && options?.scopedCacheOutcome) {
557
+ markScopedCacheIncomplete(options.scopedCacheOutcome);
558
+ }
559
+ return output;
533
560
  }
534
561
  args.path = list[0];
535
562
  }
@@ -646,17 +673,11 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
646
673
  if (rawOutputMode && !ALLOWED_OUTPUT_MODES.has(rawOutputMode)) {
647
674
  return `Error: invalid output_mode ${JSON.stringify(args.output_mode)}; expected one of ${[...ALLOWED_OUTPUT_MODES].join(', ')}`;
648
675
  }
649
- // `content_with_context` is a convenience alias for `content` that auto-applies
650
- // a generous surrounding-lines window, so the model can read a match in place
651
- // without following up with `read`. It maps to content mode; the default
652
- // context is applied below unless the caller passed an explicit -A/-B/-C/context.
653
- const wantAutoContext = rawOutputMode === 'content_with_context';
654
- // Default to `content` when output_mode is omitted. A pattern is always
655
- // present here (the no-pattern case returned above), so this is a content
656
- // search — it should return the matching lines WITH line numbers, not just
657
- // filenames. Filename-only was forcing callers to re-grep for the actual
658
- // coordinates (the explorer over-iteration root cause). `files_with_matches`
659
- // is now opt-in; pure filename discovery belongs to `glob`.
676
+ // Omitted output_mode and `content_with_context` both return content with a
677
+ // generous surrounding-lines window. Explicit `content` remains bare.
678
+ const wantAutoContext = rawOutputMode === '' || rawOutputMode === 'content_with_context';
679
+ // Filename-only and count searches are explicit: callers must opt into
680
+ // `files_with_matches` or `count` when they only need existence/count data.
660
681
  const outputMode = (rawOutputMode === 'content_with_context') ? 'content' : (rawOutputMode || 'content');
661
682
  const headLimitRaw = args.head_limit;
662
683
  const headLimitCoerced = coerceNonNegInt(headLimitRaw);
@@ -740,13 +761,16 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
740
761
  // apply dedup/section assembly in the original pattern order so the
741
762
  // shared `seen` set and output text stay byte-identical to the
742
763
  // sequential version.
743
- const subs = await Promise.all(patterns.map(async (p) => {
764
+ const runPattern = async (p) => {
744
765
  try {
745
766
  return await executeGrepTool({ ...args, pattern: p }, workDir, executeChildBuiltinTool, readStateScope, subOptions);
746
767
  } catch (err) {
747
768
  return `Error: ${err && err.message ? err.message : err}`;
748
769
  }
749
- }));
770
+ };
771
+ const subs = options._grepPathFanout
772
+ ? await patterns.reduce(async (all, p) => [...await all, await runPattern(p)], Promise.resolve([]))
773
+ : await Promise.all(patterns.map(runPattern));
750
774
  const parts = patterns.map((p, i) => `# grep pattern:${JSON.stringify(p)}\n${dedupeFanoutMatchLines(subs[i], seen)}`);
751
775
  return patternCapNote + parts.join('\n\n');
752
776
  }
@@ -866,17 +890,12 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
866
890
  // (or a differently-capped one) in the internal result cache.
867
891
  patternCapTotal,
868
892
  });
869
- // Single-file grep registers a whole-file read snapshot (parity with
870
- // apply_patch), satisfying the read-before-edit guard while keeping drift
871
- // detection intact via the auto-computed contentHash. Directory/glob greps
872
- // do NOT record.
873
- const recordGrepReadSnapshot = (st) => {
874
- try {
875
- if (st && st.isFile()) {
876
- recordReadSnapshot(grepResolvedPath, st, readStateScope, { source: 'grep' });
877
- }
878
- } catch {}
879
- };
893
+ // Read-only search: grep no longer records a whole-file read snapshot.
894
+ // That snapshot existed only to satisfy the apply_patch read-before-edit
895
+ // guard, and computing it turned a single-file grep into rg PLUS a full
896
+ // whole-file read+hash (recordReadSnapshot hashes the whole file when the
897
+ // range covers it). Edit-safety is unaffected: the read and apply_patch
898
+ // paths still record their own snapshots.
880
899
 
881
900
  const cached = cacheGet(cacheKey);
882
901
  // Cache-hit returns a PRIOR grep's output; the file may have changed since
@@ -889,12 +908,14 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
889
908
  let grepStat;
890
909
  try { grepStat = statSync(grepResolvedPath); }
891
910
  catch (err) {
911
+ const enoentCache = {};
892
912
  const redirected = await tryReadFamilyEnoentRedirect({
893
913
  workDir,
894
914
  resolvedPath: grepResolvedPath,
895
915
  requestedPath: searchPath,
896
916
  errCode: err?.code,
897
917
  options,
918
+ cache: enoentCache,
898
919
  rerun: (target, opts) => executeGrepTool(
899
920
  { ...args, path: target },
900
921
  workDir,
@@ -905,7 +926,7 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
905
926
  });
906
927
  if (redirected) return redirected;
907
928
  const msg = `Error: path does not exist: ${normalizeOutputPath(grepResolvedPath)} (${err?.code || 'ENOENT'})`;
908
- let hint = buildNotFoundHint(workDir, grepResolvedPath, 'Search', err?.code);
929
+ let hint = buildNotFoundHint(workDir, grepResolvedPath, 'Search', err?.code, enoentCache);
909
930
  if (!hint) hint = await _suggestIndexedPaths(grepResolvedPath, executeChildBuiltinTool, workDir);
910
931
  return msg + finalizeReadFamilyEnoentTail(hint, searchPath, err?.code);
911
932
  }
@@ -995,7 +1016,6 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
995
1016
  if (options?.scopedCacheOutcome && (!ctxTotalKnown || ctx.omitted > 0)) {
996
1017
  markScopedCacheIncomplete(options.scopedCacheOutcome);
997
1018
  }
998
- recordGrepReadSnapshot(grepStat);
999
1019
  if (ctxTotalKnown && ctx.omitted === 0) {
1000
1020
  cacheSet(cacheKey, ctxOut, { scopes: [grepResolvedPath] });
1001
1021
  }
@@ -1107,7 +1127,6 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
1107
1127
  if (options?.scopedCacheOutcome && (!totalKnown || remaining > 0)) {
1108
1128
  markScopedCacheIncomplete(options.scopedCacheOutcome);
1109
1129
  }
1110
- recordGrepReadSnapshot(grepStat);
1111
1130
  if (totalKnown && remaining === 0) {
1112
1131
  cacheSet(cacheKey, out, { scopes: [grepResolvedPath] });
1113
1132
  }
@@ -1207,7 +1226,6 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
1207
1226
  filenameOmitted,
1208
1227
  prefix: patternCapNote + '[regex parse fallback: fixed-string terms]\n',
1209
1228
  }) || `(no matches) fixed_terms=${JSON.stringify(fixedPatterns)} path=${searchPath}`;
1210
- recordGrepReadSnapshot(grepStat);
1211
1229
  return body + rgPartialSuffix;
1212
1230
  } catch { /* fall through to the original rg error */ }
1213
1231
  }
@@ -1294,16 +1312,28 @@ export async function executeGlobTool(args, workDir, options = {}) {
1294
1312
  const basePaths = (Array.isArray(args.path) && args.path.length > 0)
1295
1313
  ? args.path
1296
1314
  : [args.path || '.'];
1315
+ // Call-scoped stat cache: the preflight below stats each root, the per-group
1316
+ // rg runs re-stat the same resolved cwd, and the empty-result diagnostic
1317
+ // stats it a third time. Memoize by resolved path so each root is stat'd once.
1318
+ const statCache = new Map();
1319
+ const statCached = (resolvedPath) => {
1320
+ if (statCache.has(resolvedPath)) return statCache.get(resolvedPath);
1321
+ let entry;
1322
+ try { entry = { st: statSync(resolvedPath), err: null }; }
1323
+ catch (err) { entry = { st: null, err }; }
1324
+ statCache.set(resolvedPath, entry);
1325
+ return entry;
1326
+ };
1297
1327
  if (!options._enoentRedirectFrom) {
1298
1328
  for (const only of basePaths) {
1299
1329
  const resolvedOnly = resolveSearchScope(only, workDir);
1300
- try { statSync(resolvedOnly); }
1301
- catch (err) {
1330
+ const pre = statCached(resolvedOnly);
1331
+ if (pre.err) {
1302
1332
  const redirected = await tryReadFamilyEnoentRedirect({
1303
1333
  workDir,
1304
1334
  resolvedPath: resolvedOnly,
1305
1335
  requestedPath: only,
1306
- errCode: err?.code,
1336
+ errCode: pre.err?.code,
1307
1337
  options,
1308
1338
  rerun: (target, opts) => executeGlobTool({ ...args, path: target }, workDir, opts),
1309
1339
  });
@@ -1407,14 +1437,19 @@ export async function executeGlobTool(args, workDir, options = {}) {
1407
1437
  for (const rel of rels) rgArgs.push('--glob', rel);
1408
1438
  const rgCwd = resolvedForSearchRoot(root);
1409
1439
  rgArgs.push('.');
1410
- try { statSync(rgCwd); }
1411
- catch (err) {
1440
+ const cwdStat = statCached(rgCwd);
1441
+ if (cwdStat.err) {
1442
+ const err = cwdStat.err;
1443
+ // One shared ENOENT scan cache for the redirect probe + not-found
1444
+ // hint (both resolve the same missing rgCwd).
1445
+ const groupEnoentCache = {};
1412
1446
  const redirected = await tryReadFamilyEnoentRedirect({
1413
1447
  workDir,
1414
1448
  resolvedPath: rgCwd,
1415
1449
  requestedPath: root,
1416
1450
  errCode: err?.code,
1417
1451
  options,
1452
+ cache: groupEnoentCache,
1418
1453
  rerun: (target, opts) => executeGlobTool({ ...args, path: target }, workDir, opts),
1419
1454
  });
1420
1455
  if (redirected) {
@@ -1425,7 +1460,7 @@ export async function executeGlobTool(args, workDir, options = {}) {
1425
1460
  redirected,
1426
1461
  };
1427
1462
  }
1428
- const hint = buildNotFoundHint(workDir, rgCwd, 'Search', err?.code);
1463
+ const hint = buildNotFoundHint(workDir, rgCwd, 'Search', err?.code, groupEnoentCache);
1429
1464
  return {
1430
1465
  error: `path does not exist: ${normalizeOutputPath(rgCwd)} (${err?.code || 'ENOENT'})${finalizeReadFamilyEnoentTail(hint, root, err?.code)}`,
1431
1466
  paths: [],
@@ -1508,7 +1543,7 @@ export async function executeGlobTool(args, workDir, options = {}) {
1508
1543
  if (totalBeforeOffset > 0 && offset >= totalBeforeOffset) {
1509
1544
  emptyDiag = `(no entries after offset=${offset}; total=${totalBeforeOffset}) pattern=${patternStr} path=${baseLabel}`;
1510
1545
  } else {
1511
- emptyDiag = `(no files found) pattern=${patternStr} path=${baseLabel}; ${basePathDiagnostic(baseEntries.map((e) => e.root), workDir)}`;
1546
+ emptyDiag = `(no files found) pattern=${patternStr} path=${baseLabel}; ${basePathDiagnostic(baseEntries.map((e) => e.root), workDir, statCache)}`;
1512
1547
  }
1513
1548
  }
1514
1549
  const body = capped.length > 0
@@ -23,6 +23,7 @@ import {
23
23
  import {
24
24
  CODE_GRAPH_TTL_MS,
25
25
  CODE_GRAPH_MAX_FILES,
26
+ CODE_GRAPH_FAST_PATH_MAX_BYTES,
26
27
  CODE_GRAPH_WORKER_TIMEOUT_MS,
27
28
  SYMBOL_SCHEMA_VERSION,
28
29
  } from './constants.mjs';
@@ -34,7 +35,10 @@ import {
34
35
  import { _touchCodeGraphCache, _setCodeGraphCache } from './memory-cache.mjs';
35
36
  import {
36
37
  ensureDiskCodeGraphLoaded,
38
+ drainCodeGraphCacheStrict,
37
39
  getDiskCodeGraphEntry,
40
+ hasLegacyDiskCodeGraphCache,
41
+ probeDiskCodeGraphEntry,
38
42
  _setDiskCodeGraphEntry,
39
43
  } from './disk-cache.mjs';
40
44
  import {
@@ -53,6 +57,104 @@ import { _findDirProjectRoot } from './project-root.mjs';
53
57
  // after a failure can retry.
54
58
  const _inflightAsyncBuilds = new Map();
55
59
 
60
+ export function _isCompatibleDiskCodeGraphEntry(entry, maxFiles = CODE_GRAPH_MAX_FILES) {
61
+ return Number.isFinite(entry?.maxFiles) && entry.maxFiles === maxFiles;
62
+ }
63
+
64
+ function _throwIfAborted(signal) {
65
+ if (signal?.aborted) throw new Error('aborted');
66
+ }
67
+
68
+ // Validate an already-loaded disk entry before paying Worker startup. The
69
+ // manifest process is async; its child-spawn slot is held by the caller until
70
+ // either this returns a hit or the Worker takes over the same slot on a miss.
71
+ // Exported for the focused deterministic cache-validation test.
72
+ export async function _validateDiskCodeGraphHit({
73
+ graphCwd,
74
+ diskEntry,
75
+ genAtStart,
76
+ now = Date.now(),
77
+ runManifest = _runGraphManifest,
78
+ computeSignature = _computeGraphSignature,
79
+ deserializeGraph = _deserializeGraph,
80
+ getGeneration = _getCodeGraphGen,
81
+ setMemoryCache = _setCodeGraphCache,
82
+ signal = null,
83
+ }) {
84
+ if (!_isCompatibleDiskCodeGraphEntry(diskEntry)) return { incompatible: true };
85
+ const manifest = await runManifest(graphCwd, signal);
86
+ const signature = computeSignature(manifest);
87
+ if (diskEntry.signature !== signature) return { graph: null, manifest, signature };
88
+ const graph = deserializeGraph(graphCwd, diskEntry);
89
+ if (!graph) return { graph: null, manifest, signature };
90
+ if (getGeneration(graphCwd) !== genAtStart) return { invalidated: true };
91
+ setMemoryCache(graphCwd, { ts: now, signature, graph });
92
+ return { graph, manifest, signature };
93
+ }
94
+
95
+ // Owns a pre-acquired slot until a validated miss hands it to spawnWorker.
96
+ // Keeping this seam dependency-injectable makes slot/abort behavior testable
97
+ // without spawning native processes or Worker threads.
98
+ export async function _runDiskCodeGraphFastPath({
99
+ graphCwd,
100
+ diskProbe,
101
+ genAtStart,
102
+ signal = null,
103
+ loadDiskEntry,
104
+ consumeDirty = () => {},
105
+ acquireSlot = acquireChildSpawnSlot,
106
+ validateDiskHit = _validateDiskCodeGraphHit,
107
+ spawnWorker,
108
+ maxFiles = CODE_GRAPH_MAX_FILES,
109
+ }) {
110
+ if (!diskProbe?.isFastPathEligible || diskProbe.maxFiles !== maxFiles) {
111
+ return spawnWorker(null, null, null);
112
+ }
113
+ const diskEntry = loadDiskEntry();
114
+ if (!_isCompatibleDiskCodeGraphEntry(diskEntry, maxFiles)) {
115
+ return spawnWorker(null, null, null);
116
+ }
117
+ consumeDirty();
118
+ const release = await acquireSlot(signal || null);
119
+ let handedToWorker = false;
120
+ try {
121
+ _throwIfAborted(signal);
122
+ const hit = await validateDiskHit({ graphCwd, diskEntry, genAtStart, signal });
123
+ _throwIfAborted(signal);
124
+ if (hit?.invalidated) throw new Error('code-graph build invalidated during prewarm');
125
+ if (hit?.graph) return hit.graph;
126
+ _throwIfAborted(signal);
127
+ const workerPromise = spawnWorker(release, hit?.manifest || null, hit?.signature || null);
128
+ handedToWorker = true;
129
+ return workerPromise;
130
+ } finally {
131
+ if (!handedToWorker) release();
132
+ }
133
+ }
134
+
135
+ // A legacy single-file cache can be very large. Its migration is deliberately
136
+ // performed by _buildCodeGraph in the Worker; probing it here would poison the
137
+ // parent disk-cache loaded state after a synchronous JSON.parse.
138
+ export function _prepareDiskCodeGraphFastPath({
139
+ graphCwd,
140
+ hasLegacyCache = hasLegacyDiskCodeGraphCache,
141
+ ensureDiskLoaded = ensureDiskCodeGraphLoaded,
142
+ probeDiskEntry = probeDiskCodeGraphEntry,
143
+ runFastPath,
144
+ }) {
145
+ if (hasLegacyCache()) return runFastPath(null);
146
+ ensureDiskLoaded();
147
+ return runFastPath(probeDiskEntry(graphCwd, CODE_GRAPH_FAST_PATH_MAX_BYTES));
148
+ }
149
+
150
+ // Worker disk writes are debounced with an unref'd timer. Drain before sending
151
+ // success so legacy migration persists every loaded cwd before this Worker can
152
+ // exit; a drain failure is intentionally thrown to the worker's failure path.
153
+ export function _postCodeGraphWorkerSuccess(graph, postMessage, drainCache = drainCodeGraphCacheStrict) {
154
+ drainCache();
155
+ postMessage({ ok: true, signature: graph.signature, graph });
156
+ }
157
+
56
158
  export function prewarmCodeGraph(cwd) {
57
159
  if (!cwd) return;
58
160
  // Reuse the buildCodeGraphAsync single-flight path. Fire-and-forget, and
@@ -109,16 +211,54 @@ export async function buildCodeGraphAsync(cwd, signal = null, { bestEffort = fal
109
211
  (e) => { cleanup(); throw e; },
110
212
  );
111
213
  }
112
- // Non-competing prewarm: once past the cache/single-flight fast paths a
113
- // fresh build must spawn the worker (a child-spawn slot). Best-effort warmers
114
- // skip that spawn when the gate has no spare capacity, returning null so the
115
- // caller no-ops — and skipping BEFORE registering the in-flight entry so a
116
- // concurrent REAL caller is never attached to (and thus never starved by) a
117
- // skipped warm.
214
+ // Non-competing prewarm: the signature-validation manifest also needs a
215
+ // child-spawn slot, so warmers skip before either it or a Worker can queue.
118
216
  if (bestEffort && !childSpawnHasSpareCapacity()) return null;
119
217
  const _genAtStart = _getCodeGraphGen(graphCwd);
218
+ const promise = (async () => {
219
+ // Loading the compact disk manifest/one candidate entry is synchronous but
220
+ // bounded I/O. Do not run a manifest at all when no disk candidate exists:
221
+ // cold/dirty misses remain entirely on the existing Worker path.
222
+ return _prepareDiskCodeGraphFastPath({
223
+ graphCwd,
224
+ runFastPath: (diskProbe) => _runDiskCodeGraphFastPath({
225
+ graphCwd,
226
+ diskProbe,
227
+ genAtStart: _genAtStart,
228
+ signal,
229
+ loadDiskEntry: () => getDiskCodeGraphEntry(graphCwd),
230
+ consumeDirty: () => _consumeCodeGraphDirtyPaths(graphCwd),
231
+ spawnWorker: (preAcquiredRelease, manifest, signature) => _spawnCodeGraphWorker(
232
+ cwd,
233
+ graphCwd,
234
+ _genAtStart,
235
+ signal,
236
+ preAcquiredRelease,
237
+ manifest,
238
+ signature,
239
+ ),
240
+ }),
241
+ });
242
+ })();
243
+ _inflightAsyncBuilds.set(graphCwd, promise);
244
+ try {
245
+ return await promise;
246
+ } finally {
247
+ _inflightAsyncBuilds.delete(graphCwd);
248
+ }
249
+ }
250
+
251
+ function _spawnCodeGraphWorker(
252
+ cwd,
253
+ graphCwd,
254
+ genAtStart,
255
+ signal,
256
+ preAcquiredRelease = null,
257
+ manifest = null,
258
+ signature = null,
259
+ ) {
120
260
  let _worker = null;
121
- const promise = new Promise((resolve, reject) => {
261
+ return new Promise((resolve, reject) => {
122
262
  let settled = false;
123
263
  let timeout = null;
124
264
  let _onSignalAbort = null;
@@ -132,18 +272,23 @@ export async function buildCodeGraphAsync(cwd, signal = null, { bestEffort = fal
132
272
  _onSignalAbort = null;
133
273
  }
134
274
  if (_releaseSlot) { try { _releaseSlot(); } catch {} _releaseSlot = null; }
135
- _inflightAsyncBuilds.delete(graphCwd);
136
275
  if (val instanceof Error) reject(val);
137
276
  else resolve(val);
138
277
  };
139
- acquireChildSpawnSlot(signal || null).then((release) => {
278
+ (preAcquiredRelease ? Promise.resolve(preAcquiredRelease) : acquireChildSpawnSlot(signal || null)).then((release) => {
140
279
  _releaseSlot = release;
141
280
  if (settled) { release(); _releaseSlot = null; return; }
281
+ if (signal?.aborted) { settle(new Error('aborted')); return; }
142
282
  const workerUrl = new URL('../code-graph-prewarm-worker.mjs', import.meta.url);
143
- _worker = new Worker(workerUrl, {
144
- workerData: { cwd },
145
- execArgv: [],
146
- });
283
+ try {
284
+ _worker = new Worker(workerUrl, {
285
+ workerData: { cwd, manifest, signature },
286
+ execArgv: [],
287
+ });
288
+ } catch (e) {
289
+ settle(e instanceof Error ? e : new Error(String(e)));
290
+ return;
291
+ }
147
292
  const w = _worker;
148
293
  timeout = setTimeout(() => {
149
294
  try { _worker?.terminate(); } catch {}
@@ -160,7 +305,7 @@ export async function buildCodeGraphAsync(cwd, signal = null, { bestEffort = fal
160
305
  w.once('message', (msg) => {
161
306
  try {
162
307
  if (msg && msg.ok && msg.graph && typeof msg.signature === 'string') {
163
- const genStillCurrent = _getCodeGraphGen(graphCwd) === _genAtStart;
308
+ const genStillCurrent = _getCodeGraphGen(graphCwd) === genAtStart;
164
309
  if (genStillCurrent) {
165
310
  _setCodeGraphCache(graphCwd, { ts: Date.now(), signature: msg.signature, graph: msg.graph });
166
311
  _setDiskCodeGraphEntry(graphCwd, msg.graph);
@@ -174,8 +319,6 @@ export async function buildCodeGraphAsync(cwd, signal = null, { bestEffort = fal
174
319
  w.once('error', (e) => settle(e instanceof Error ? e : new Error(String(e))));
175
320
  }, (e) => settle(e instanceof Error ? e : new Error(String(e))));
176
321
  });
177
- _inflightAsyncBuilds.set(graphCwd, promise);
178
- return promise;
179
322
  }
180
323
 
181
324
  /**
@@ -184,7 +327,7 @@ export async function buildCodeGraphAsync(cwd, signal = null, { bestEffort = fal
184
327
  * buildCodeGraphAsync (worker-thread isolated) or the code_graph / find_symbol
185
328
  * tools, never this synchronous form on the main event loop.
186
329
  */
187
- export async function _buildCodeGraph(cwd) {
330
+ export async function _buildCodeGraph(cwd, { manifest: suppliedManifest = null, signature: suppliedSignature = null } = {}) {
188
331
  const now = Date.now();
189
332
  let _tp = performance.now();
190
333
  const _trace = (label) => { if (process.env.MIXDOG_GRAPH_TRACE) { const n = performance.now(); process.stderr.write(`[cg-trace] ${label}=${(n - _tp).toFixed(0)}ms\n`); _tp = n; } };
@@ -195,10 +338,14 @@ export async function _buildCodeGraph(cwd) {
195
338
  let previousGraph = cached?.graph || null;
196
339
  _consumeCodeGraphDirtyPaths(graphCwd);
197
340
 
198
- // 1. Change-detect via Rust --manifest (fp/rel/size only, no parse).
199
- const manifest = await _runGraphManifest(absRoot);
200
- const signature = _computeGraphSignature(manifest);
201
- _trace('manifest+sig');
341
+ // 1. Change-detect via Rust --manifest (fp/rel/size only, no parse). A
342
+ // main-thread disk-cache validation may hand its just-computed manifest to
343
+ // this Worker after a miss, avoiding a duplicate native process.
344
+ const manifest = Array.isArray(suppliedManifest) ? suppliedManifest : await _runGraphManifest(absRoot);
345
+ const signature = typeof suppliedSignature === 'string'
346
+ ? suppliedSignature
347
+ : _computeGraphSignature(manifest);
348
+ if (!Array.isArray(suppliedManifest)) _trace('manifest+sig');
202
349
  const truncated = manifest.length > CODE_GRAPH_MAX_FILES;
203
350
  const indexed = truncated ? manifest.slice(0, CODE_GRAPH_MAX_FILES) : manifest;
204
351
 
@@ -211,7 +358,7 @@ export async function _buildCodeGraph(cwd) {
211
358
  // 3. Disk cache hit.
212
359
  ensureDiskCodeGraphLoaded(now);
213
360
  const diskEntry = getDiskCodeGraphEntry(graphCwd);
214
- if (diskEntry?.signature === signature) {
361
+ if (_isCompatibleDiskCodeGraphEntry(diskEntry) && diskEntry.signature === signature) {
215
362
  const graph = _deserializeGraph(graphCwd, diskEntry);
216
363
  if (graph) {
217
364
  if (_getCodeGraphGen(graphCwd) === _genAtStart) {
@@ -220,7 +367,9 @@ export async function _buildCodeGraph(cwd) {
220
367
  return graph;
221
368
  }
222
369
  }
223
- if (!previousGraph && diskEntry) previousGraph = _deserializeGraph(graphCwd, diskEntry);
370
+ if (!previousGraph && _isCompatibleDiskCodeGraphEntry(diskEntry)) {
371
+ previousGraph = _deserializeGraph(graphCwd, diskEntry);
372
+ }
224
373
  if (previousGraph && previousGraph.schemaVersion !== SYMBOL_SCHEMA_VERSION) {
225
374
  previousGraph = null;
226
375
  }
@@ -4,6 +4,9 @@
4
4
 
5
5
  export const CODE_GRAPH_TTL_MS = 30_000;
6
6
  export const CODE_GRAPH_MAX_FILES = 10_000;
7
+ // A cold-process disk hit may deserialize only this much JSON on the main
8
+ // thread. Larger entries still validate/rebuild inside the Worker.
9
+ export const CODE_GRAPH_FAST_PATH_MAX_BYTES = 8 * 1024 * 1024;
7
10
  export const CODE_GRAPH_WORKER_TIMEOUT_MS = 120_000;
8
11
  // Timeout for the native mixdog-graph binary child process (spawned per graph build).
9
12
  export const CODE_GRAPH_BINARY_TIMEOUT_MS = Math.max(1000, Number(process.env.MIXDOG_CODE_GRAPH_BINARY_TIMEOUT_MS) || 20000);