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.
- package/package.json +9 -4
- package/scripts/abort-recovery-test.mjs +43 -2
- package/scripts/agent-tag-reuse-smoke.mjs +146 -6
- package/scripts/agent-terminal-reap-test.mjs +127 -0
- package/scripts/agent-trace-io-test.mjs +69 -0
- package/scripts/code-graph-disk-hit-test.mjs +224 -0
- package/scripts/execution-completion-dedup-test.mjs +157 -0
- package/scripts/execution-pending-resume-kick-test.mjs +57 -2
- package/scripts/execution-resume-esc-integration-test.mjs +176 -0
- package/scripts/explore-bench.mjs +38 -2
- package/scripts/explore-prompt-policy-test.mjs +152 -11
- package/scripts/find-fuzzy-hidden-test.mjs +122 -0
- package/scripts/internal-comms-bench-test.mjs +226 -0
- package/scripts/internal-comms-bench.mjs +185 -58
- package/scripts/internal-comms-smoke.mjs +171 -23
- package/scripts/live-worker-smoke.mjs +38 -2
- package/scripts/memory-cycle-routing-test.mjs +111 -0
- package/scripts/memory-rule-contract-test.mjs +93 -0
- package/scripts/output-style-smoke.mjs +2 -2
- package/scripts/rg-runner-test.mjs +240 -0
- package/scripts/routing-corpus-test.mjs +349 -0
- package/scripts/routing-corpus.mjs +211 -32
- package/scripts/session-orphan-sweep-test.mjs +83 -0
- package/scripts/steering-drain-buckets-test.mjs +316 -2
- package/scripts/tool-smoke.mjs +21 -13
- package/scripts/tool-tui-presentation-test.mjs +202 -0
- package/scripts/tui-transcript-perf-test.mjs +279 -0
- package/src/agents/heavy-worker/AGENT.md +10 -7
- package/src/agents/reviewer/AGENT.md +6 -4
- package/src/agents/worker/AGENT.md +7 -5
- package/src/rules/agent/00-common.md +4 -4
- package/src/rules/agent/00-core.md +11 -14
- package/src/rules/agent/20-skip-protocol.md +3 -3
- package/src/rules/agent/30-explorer.md +50 -60
- package/src/rules/agent/40-cycle1-agent.md +15 -24
- package/src/rules/agent/41-cycle2-agent.md +33 -57
- package/src/rules/agent/42-cycle3-agent.md +28 -42
- package/src/rules/lead/01-general.md +7 -10
- package/src/rules/lead/lead-brief.md +11 -14
- package/src/rules/lead/lead-tool.md +6 -5
- package/src/rules/shared/01-tool.md +44 -45
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +44 -1
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +20 -5
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +21 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +17 -38
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +17 -8
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +270 -78
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +15 -0
- package/src/runtime/agent/orchestrator/session/manager/delivered-completions.mjs +123 -0
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +15 -2
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +67 -2
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +232 -43
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +43 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +27 -13
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +25 -20
- package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +118 -53
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +106 -29
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +8 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +4 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +33 -2
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +151 -64
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +37 -13
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +84 -49
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +172 -23
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +3 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +68 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +17 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +24 -10
- package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +6 -3
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -7
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +1 -0
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -4
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +4 -3
- package/src/runtime/memory/lib/memory-cycle3.mjs +12 -2
- package/src/runtime/shared/buffered-appender.mjs +13 -2
- package/src/runtime/shared/tool-primitives.mjs +4 -1
- package/src/runtime/shared/tool-status.mjs +27 -0
- package/src/runtime/shared/tool-surface.mjs +6 -3
- package/src/session-runtime/config-helpers.mjs +14 -0
- package/src/session-runtime/context-status.mjs +1 -0
- package/src/session-runtime/effort.mjs +6 -2
- package/src/session-runtime/lifecycle-api.mjs +4 -0
- package/src/session-runtime/model-recency.mjs +5 -2
- package/src/session-runtime/runtime-core.mjs +36 -2
- package/src/session-runtime/session-turn-api.mjs +12 -0
- package/src/session-runtime/tool-catalog.mjs +34 -0
- package/src/standalone/agent-tool/notify.mjs +13 -0
- package/src/standalone/agent-tool.mjs +53 -70
- package/src/standalone/explore-tool.mjs +6 -7
- package/src/tui/App.jsx +33 -1
- package/src/tui/app/model-options.mjs +5 -3
- package/src/tui/app/model-picker.mjs +12 -24
- package/src/tui/app/transcript-window.mjs +10 -10
- package/src/tui/app/use-transcript-window.mjs +38 -56
- package/src/tui/components/ToolExecution.jsx +11 -6
- package/src/tui/components/TranscriptItem.jsx +1 -1
- package/src/tui/components/tool-execution/surface-detail.mjs +24 -10
- package/src/tui/components/tool-execution/text-format.mjs +10 -19
- package/src/tui/dist/index.mjs +866 -300
- package/src/tui/engine/agent-job-feed.mjs +216 -19
- package/src/tui/engine/agent-response-tail.mjs +68 -0
- package/src/tui/engine/notification-plan.mjs +16 -0
- package/src/tui/engine/session-api.mjs +14 -2
- package/src/tui/engine/session-flow.mjs +22 -2
- package/src/tui/engine/tool-card-results.mjs +64 -37
- package/src/tui/engine/tool-result-status.mjs +75 -21
- package/src/tui/engine/turn.mjs +172 -77
- package/src/tui/engine.mjs +199 -39
- package/src/tui/index.jsx +2 -2
- package/src/workflows/bench/WORKFLOW.md +25 -35
- package/src/workflows/default/WORKFLOW.md +38 -32
- package/src/workflows/solo/WORKFLOW.md +19 -22
- package/scripts/_jitter-fuzz.mjs +0 -44410
- package/scripts/_jitter-fuzz2.mjs +0 -44400
- package/scripts/_jitter-probe.mjs +0 -44397
- 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
|
|
506
|
-
// code_graph files[] batching. Recursive calls pass a single
|
|
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
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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`
|
|
650
|
-
//
|
|
651
|
-
|
|
652
|
-
//
|
|
653
|
-
|
|
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
|
|
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
|
-
//
|
|
870
|
-
//
|
|
871
|
-
//
|
|
872
|
-
//
|
|
873
|
-
|
|
874
|
-
|
|
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
|
-
|
|
1301
|
-
|
|
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
|
-
|
|
1411
|
-
|
|
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:
|
|
113
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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) ===
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
|
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)
|
|
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);
|