mixdog 0.9.37 → 0.9.39
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/dispatch-persist-recovery-test.mjs +141 -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 +174 -0
- package/scripts/explore-bench.mjs +101 -8
- package/scripts/explore-prompt-policy-test.mjs +156 -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/notify-completion-mirror-test.mjs +73 -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/session-sweep.mjs +266 -0
- package/scripts/steering-drain-buckets-test.mjs +179 -0
- package/scripts/tool-smoke.mjs +21 -13
- package/scripts/tool-tui-presentation-test.mjs +202 -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 +56 -48
- 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 -41
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +37 -1
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +20 -5
- package/src/runtime/agent/orchestrator/dispatch-persist.mjs +31 -8
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +13 -7
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +21 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +17 -38
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +5 -4
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +119 -3
- 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/ask-session.mjs +18 -52
- 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 +41 -2
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/store.mjs +339 -63
- package/src/runtime/agent/orchestrator/stall-policy.mjs +37 -0
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -1
- 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/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/model-recency.mjs +5 -2
- package/src/session-runtime/provider-models.mjs +3 -3
- package/src/session-runtime/runtime-core.mjs +78 -10
- package/src/session-runtime/tool-catalog.mjs +34 -0
- package/src/session-runtime/warmup-schedulers.mjs +7 -1
- package/src/standalone/agent-tool/notify.mjs +13 -0
- package/src/standalone/agent-tool.mjs +45 -69
- package/src/standalone/explore-tool.mjs +6 -7
- package/src/tui/App.jsx +31 -0
- 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 +1 -0
- 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 +533 -143
- package/src/tui/engine/agent-job-feed.mjs +153 -16
- package/src/tui/engine/agent-response-tail.mjs +68 -0
- package/src/tui/engine/notification-plan.mjs +16 -0
- package/src/tui/engine/queue-helpers.mjs +8 -0
- package/src/tui/engine/session-api.mjs +8 -2
- package/src/tui/engine/session-flow.mjs +34 -2
- package/src/tui/engine/tool-card-results.mjs +54 -32
- package/src/tui/engine/tool-result-status.mjs +75 -21
- package/src/tui/engine/turn.mjs +83 -43
- package/src/tui/engine.mjs +63 -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
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
cacheGet,
|
|
21
21
|
cacheSet,
|
|
22
22
|
getCachedReadOnlyStat,
|
|
23
|
+
statCacheSet,
|
|
23
24
|
statPathsForMtime,
|
|
24
25
|
lstatPathsForMtime,
|
|
25
26
|
registerCacheInvalidationListener,
|
|
@@ -92,23 +93,41 @@ export async function executeListTool(args, workDir, options = {}) {
|
|
|
92
93
|
// wall-clock cost changes. Concurrency is capped so a 10-path batch
|
|
93
94
|
// cannot exhaust the child-spawn / FS-handle budget.
|
|
94
95
|
const LIST_FANOUT_CONCURRENCY = 4;
|
|
95
|
-
|
|
96
|
+
// Collapse semantic-duplicate targets: two spellings that resolve to
|
|
97
|
+
// the same directory (e.g. `foo` and `./foo`) are stat'd/walked ONCE.
|
|
98
|
+
// Each caller label keeps its own `# list <p>` section reusing the
|
|
99
|
+
// shared body, so output stays byte-identical — only the duplicate
|
|
100
|
+
// concurrent walk is eliminated.
|
|
101
|
+
const resolveKey = (p) => {
|
|
102
|
+
try { return normalizeOutputPath(resolveAgainstCwd(normalizeInputPath(p), workDir)); }
|
|
103
|
+
catch { return p; }
|
|
104
|
+
};
|
|
105
|
+
const keyByIndex = new Array(targets.length);
|
|
106
|
+
const uniqueKeys = [];
|
|
107
|
+
const repByKey = new Map();
|
|
108
|
+
for (let i = 0; i < targets.length; i++) {
|
|
109
|
+
const k = resolveKey(targets[i]);
|
|
110
|
+
keyByIndex[i] = k;
|
|
111
|
+
if (!repByKey.has(k)) { repByKey.set(k, targets[i]); uniqueKeys.push(k); }
|
|
112
|
+
}
|
|
113
|
+
const bodyByKey = new Map();
|
|
96
114
|
let cursor = 0;
|
|
97
115
|
const runWorker = async () => {
|
|
98
116
|
for (;;) {
|
|
99
117
|
const i = cursor++;
|
|
100
|
-
if (i >=
|
|
118
|
+
if (i >= uniqueKeys.length) return;
|
|
119
|
+
const k = uniqueKeys[i];
|
|
101
120
|
try {
|
|
102
|
-
|
|
121
|
+
bodyByKey.set(k, await executeListTool({ ...args, path: repByKey.get(k) }, workDir, options));
|
|
103
122
|
} catch (err) {
|
|
104
|
-
|
|
123
|
+
bodyByKey.set(k, `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`);
|
|
105
124
|
}
|
|
106
125
|
}
|
|
107
126
|
};
|
|
108
127
|
await Promise.all(
|
|
109
|
-
Array.from({ length: Math.min(LIST_FANOUT_CONCURRENCY,
|
|
128
|
+
Array.from({ length: Math.min(LIST_FANOUT_CONCURRENCY, uniqueKeys.length) }, runWorker),
|
|
110
129
|
);
|
|
111
|
-
const sections = targets.map((p, i) => `# list ${p}\n${
|
|
130
|
+
const sections = targets.map((p, i) => `# list ${p}\n${bodyByKey.get(keyByIndex[i])}`);
|
|
112
131
|
if (capped) sections.push(`... [capped at 10 of ${list.length} paths]`);
|
|
113
132
|
return sections.join('\n\n');
|
|
114
133
|
}
|
|
@@ -150,8 +169,12 @@ export async function executeListTool(args, workDir, options = {}) {
|
|
|
150
169
|
});
|
|
151
170
|
const cached = cacheGet(cacheKey);
|
|
152
171
|
if (cached !== null) return cached;
|
|
153
|
-
|
|
172
|
+
let _preStat;
|
|
173
|
+
try { _preStat = await assertPathReachable(fullPath); }
|
|
154
174
|
catch (err) { return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`; }
|
|
175
|
+
// Feed the reachability preflight's stat into the cache so the root is
|
|
176
|
+
// stat'd once instead of immediately re-stat'd by getCachedReadOnlyStat.
|
|
177
|
+
if (_preStat) statCacheSet(fullPath, _preStat);
|
|
155
178
|
let st;
|
|
156
179
|
try { st = getCachedReadOnlyStat(fullPath); }
|
|
157
180
|
catch (err) {
|
|
@@ -283,8 +306,10 @@ export async function executeTreeTool(args, workDir, options = {}) {
|
|
|
283
306
|
});
|
|
284
307
|
const cached = cacheGet(cacheKey);
|
|
285
308
|
if (cached !== null) return cached;
|
|
286
|
-
|
|
309
|
+
let _preStat;
|
|
310
|
+
try { _preStat = await assertPathReachable(fullPath); }
|
|
287
311
|
catch (err) { return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`; }
|
|
312
|
+
if (_preStat) statCacheSet(fullPath, _preStat);
|
|
288
313
|
let st;
|
|
289
314
|
try { st = getCachedReadOnlyStat(fullPath); }
|
|
290
315
|
catch (err) {
|
|
@@ -357,7 +382,7 @@ export async function executeTreeTool(args, workDir, options = {}) {
|
|
|
357
382
|
|
|
358
383
|
// ── Broad-enumeration cache (shared `rg --files` sweep) ──────────────────
|
|
359
384
|
// A `rg --files` sweep of a root depends ONLY on (root, hidden, depth,
|
|
360
|
-
// includeNoise) — NOT on the per-query narrowing. Yet both the fuzzy-find
|
|
385
|
+
// includeNoise, ignoreMode) — NOT on the per-query narrowing. Yet both the fuzzy-find
|
|
361
386
|
// broad pass and the find_files broad fast path re-run that full sweep for
|
|
362
387
|
// every query item AND for every concurrent caller (measured 1-4s each when
|
|
363
388
|
// 8 explorer sub-sessions hit the same root). Cache the PARSED file list per
|
|
@@ -387,8 +412,8 @@ function findEnumTtlMs() {
|
|
|
387
412
|
return Math.floor(n); // 0 = disabled
|
|
388
413
|
}
|
|
389
414
|
|
|
390
|
-
function findEnumKey({ root, hidden, depth, includeNoise }) {
|
|
391
|
-
return `${root}\u0000${hidden ? 1 : 0}\u0000${depth ?? ''}\u0000${includeNoise ? 1 : 0}`;
|
|
415
|
+
function findEnumKey({ root, hidden, depth, includeNoise, ignoreMode }) {
|
|
416
|
+
return `${root}\u0000${hidden ? 1 : 0}\u0000${depth ?? ''}\u0000${includeNoise ? 1 : 0}\u0000${ignoreMode}`;
|
|
392
417
|
}
|
|
393
418
|
|
|
394
419
|
// Parse `rg --files` stdout into the same normalized relative-path list both
|
|
@@ -405,11 +430,12 @@ function parseRgFileList(stdout) {
|
|
|
405
430
|
// Run (or reuse) the broad `rg --files` sweep for a root. Returns
|
|
406
431
|
// { files, truncated, partial }. The returned `files` array is SHARED — callers
|
|
407
432
|
// must treat it as read-only. `rgArgs` must be the broad-pass args (no per-query
|
|
408
|
-
// narrowing); the cache key
|
|
433
|
+
// narrowing); the cache key includes every enumeration-affecting dimension, so
|
|
434
|
+
// any caller producing an
|
|
409
435
|
// equivalent sweep for the same dims reuses the result.
|
|
410
|
-
async function getBroadEnumeration({ root, hidden, depth, includeNoise, rgArgs, cwd, runRgImpl = runRg, bestEffort = false }) {
|
|
436
|
+
async function getBroadEnumeration({ root, hidden, depth, includeNoise, ignoreMode, rgArgs, cwd, runRgImpl = runRg, bestEffort = false }) {
|
|
411
437
|
const ttl = findEnumTtlMs();
|
|
412
|
-
const key = findEnumKey({ root, hidden, depth, includeNoise });
|
|
438
|
+
const key = findEnumKey({ root, hidden, depth, includeNoise, ignoreMode });
|
|
413
439
|
if (ttl > 0) {
|
|
414
440
|
const hit = FIND_ENUM_CACHE.get(key);
|
|
415
441
|
if (hit && hit.gen === FIND_ENUM_GEN && hit.expiresAt > Date.now()) {
|
|
@@ -460,12 +486,12 @@ export async function prewarmFindEnumeration(root) {
|
|
|
460
486
|
try {
|
|
461
487
|
if (!root || typeof root !== 'string') return;
|
|
462
488
|
const hidden = true, includeNoise = false, depth = null;
|
|
463
|
-
const rgArgs = ['--files', '--no-
|
|
489
|
+
const rgArgs = ['--files', '--no-require-git', '--hidden'];
|
|
464
490
|
for (const ex of DEFAULT_IGNORE_GLOBS) rgArgs.push('--glob', ex);
|
|
465
491
|
rgArgs.push('.');
|
|
466
492
|
await getBroadEnumeration({
|
|
467
493
|
root: normalizeOutputPath(root),
|
|
468
|
-
hidden, depth, includeNoise,
|
|
494
|
+
hidden, depth, includeNoise, ignoreMode: 'git',
|
|
469
495
|
rgArgs, cwd: root, bestEffort: true,
|
|
470
496
|
});
|
|
471
497
|
} catch { /* best-effort warm; never surface */ }
|
|
@@ -543,11 +569,10 @@ export async function executeFuzzyFindTool(args, workDir, options = {}) {
|
|
|
543
569
|
});
|
|
544
570
|
const cached = cacheGet(cacheKey);
|
|
545
571
|
if (cached !== null) return cached;
|
|
546
|
-
//
|
|
547
|
-
//
|
|
572
|
+
// Common discovery respects .gitignore even outside a Git repository.
|
|
573
|
+
// include_noise deliberately retains the old hardened --no-ignore behavior.
|
|
548
574
|
// Noise dirs stay excluded via DEFAULT_IGNORE_GLOBS below.
|
|
549
|
-
|
|
550
|
-
const baseRgArgs = ['--files', '--no-ignore'];
|
|
575
|
+
const baseRgArgs = ['--files', includeNoise ? '--no-ignore' : '--no-require-git'];
|
|
551
576
|
if (hidden) baseRgArgs.push('--hidden');
|
|
552
577
|
if (depth != null) baseRgArgs.push('--max-depth', String(depth));
|
|
553
578
|
// Noise-exclusion globs are kept SEPARATE and always appended LAST (after
|
|
@@ -586,7 +611,7 @@ export async function executeFuzzyFindTool(args, workDir, options = {}) {
|
|
|
586
611
|
try {
|
|
587
612
|
broadEnum = await getBroadEnumeration({
|
|
588
613
|
root: normalizeOutputPath(fullPath),
|
|
589
|
-
hidden, depth, includeNoise,
|
|
614
|
+
hidden, depth, includeNoise, ignoreMode: includeNoise ? 'all' : 'git',
|
|
590
615
|
rgArgs: [...baseRgArgs, ...ignoreGlobs, '.'],
|
|
591
616
|
cwd: fullPath,
|
|
592
617
|
runRgImpl,
|
|
@@ -594,25 +619,67 @@ export async function executeFuzzyFindTool(args, workDir, options = {}) {
|
|
|
594
619
|
} catch (err) {
|
|
595
620
|
return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`;
|
|
596
621
|
}
|
|
597
|
-
|
|
598
|
-
|
|
622
|
+
let rgTruncated = broadEnum.truncated;
|
|
623
|
+
let rgPartial = broadEnum.partial;
|
|
624
|
+
const passOneItems = broadEnum.files.map((path) => ({ path }));
|
|
625
|
+
// Any single path segment is an exact-name candidate, including names with
|
|
626
|
+
// spaces, extensionless names, and literal glob characters. The comparison
|
|
627
|
+
// below is direct, so glob syntax has no special meaning in this decision.
|
|
628
|
+
const exactFilenameQuery = !/[\\/]/.test(query);
|
|
629
|
+
const queryLower = query.toLowerCase();
|
|
630
|
+
const passOneHasExactBasename = !exactFilenameQuery || broadEnum.files.some((path) =>
|
|
631
|
+
path.slice(path.lastIndexOf('/') + 1).toLowerCase() === queryLower);
|
|
632
|
+
const passOneHasCandidate = fuzzyRank(query, passOneItems, 1).length > 0;
|
|
633
|
+
let fallbackRan = false;
|
|
634
|
+
let fallbackPaths = [];
|
|
635
|
+
// Only pay for the hardened tree walk when common discovery has no answer,
|
|
636
|
+
// or when an exact filename could be hidden by .gitignore. Reuse the
|
|
637
|
+
// hardened broad cache shared with find_files: its key distinguishes
|
|
638
|
+
// ignoreMode, so this cannot collide with the common pass.
|
|
639
|
+
if (!includeNoise && (!passOneHasCandidate || !passOneHasExactBasename)) {
|
|
640
|
+
try {
|
|
641
|
+
const fallbackArgs = ['--files', '--no-ignore', ...baseRgArgs.slice(2), ...ignoreGlobs, '.'];
|
|
642
|
+
const fallbackEnum = await getBroadEnumeration({
|
|
643
|
+
root: normalizeOutputPath(fullPath),
|
|
644
|
+
hidden, depth, includeNoise, ignoreMode: 'all',
|
|
645
|
+
rgArgs: fallbackArgs, cwd: fullPath, runRgImpl,
|
|
646
|
+
});
|
|
647
|
+
rgTruncated ||= fallbackEnum.truncated;
|
|
648
|
+
rgPartial ||= fallbackEnum.partial;
|
|
649
|
+
fallbackPaths = fallbackEnum.files;
|
|
650
|
+
fallbackRan = true;
|
|
651
|
+
} catch {
|
|
652
|
+
// This required fallback leaves the result incomplete. Surface the
|
|
653
|
+
// existing partial warning and prevent the final output cache from
|
|
654
|
+
// preserving a pass-one-only answer.
|
|
655
|
+
rgPartial = true;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
599
658
|
// Narrowed enumeration: only files whose NAME contains the query
|
|
600
659
|
// (case-insensitive substring glob). This output is tiny and effectively
|
|
601
660
|
// never truncated, so exact/substring hits are guaranteed to reach ranking
|
|
602
661
|
// regardless of whether the broad pass was cut at the cap. Best-effort:
|
|
603
662
|
// failures here never fail the tool — the broad pass still stands.
|
|
604
663
|
let narrowPaths = [];
|
|
605
|
-
|
|
664
|
+
// Only spawn the per-query narrowed rg pass when the broad enumeration was
|
|
665
|
+
// cut (cap/exit-2): an untruncated broad pass already contains every file,
|
|
666
|
+
// so every narrowed substring hit is a broad-set duplicate that the
|
|
667
|
+
// dedup below drops — running it would add zero items. Skipping it there
|
|
668
|
+
// removes a redundant full-tree walk per query with identical output.
|
|
669
|
+
if (rgTruncated || rgPartial) try {
|
|
606
670
|
// A positive --iglob whitelist makes ripgrep re-admit paths its own
|
|
607
671
|
// `!**/<noise>/**` negations would otherwise exclude (the whitelist
|
|
608
672
|
// wins regardless of glob order), so noise dirs are pruned in JS here
|
|
609
673
|
// instead — matching the broad pass's effective exclusion set.
|
|
610
|
-
const
|
|
674
|
+
const narrowBaseArgs = fallbackRan
|
|
675
|
+
? ['--files', '--no-ignore', ...baseRgArgs.slice(2)]
|
|
676
|
+
: baseRgArgs;
|
|
677
|
+
const narrowStdout = await runRgImpl([...narrowBaseArgs, '--iglob', `*${escapeGlobLiteral(query)}*`, '.'], { cwd: fullPath });
|
|
611
678
|
narrowPaths = parseRgFiles(narrowStdout).filter((p) =>
|
|
612
679
|
includeNoise || !p.split('/').some((seg) => NOISE_DIR_NAMES.has(seg)));
|
|
613
680
|
} catch { /* best-effort backstop; broad pass already collected */ }
|
|
614
|
-
// Merge
|
|
615
|
-
//
|
|
681
|
+
// Merge pass one + fallback + narrowed, deduplicating by path. Pass-one
|
|
682
|
+
// order always wins so the fallback cannot perturb common-path ranking.
|
|
616
683
|
const seen = new Set();
|
|
617
684
|
const items = [];
|
|
618
685
|
for (const p of broadEnum.files) {
|
|
@@ -620,6 +687,11 @@ export async function executeFuzzyFindTool(args, workDir, options = {}) {
|
|
|
620
687
|
seen.add(p);
|
|
621
688
|
items.push({ path: p });
|
|
622
689
|
}
|
|
690
|
+
for (const p of fallbackPaths) {
|
|
691
|
+
if (seen.has(p)) continue;
|
|
692
|
+
seen.add(p);
|
|
693
|
+
items.push({ path: p });
|
|
694
|
+
}
|
|
623
695
|
for (const p of narrowPaths) {
|
|
624
696
|
if (seen.has(p)) continue;
|
|
625
697
|
seen.add(p);
|
|
@@ -638,6 +710,9 @@ export async function executeFuzzyFindTool(args, workDir, options = {}) {
|
|
|
638
710
|
if (!noMatch && hasMore) lines.push(`... (top ${headLimit}; raise head_limit for more)`);
|
|
639
711
|
if (rgTruncated) lines.push('... [warning] rg stdout truncated at 20MB cap; broad ranking incomplete (exact-name hits still merged)');
|
|
640
712
|
if (rgPartial && !rgTruncated) lines.push('... [warning] rg exit 2 (partial results); broad ranking may be incomplete');
|
|
713
|
+
if (!fallbackRan && headLimit > 0 && fuzzyRank(query, passOneItems, headLimit).length >= headLimit) {
|
|
714
|
+
lines.push('[gitignored trees not searched; retry with include_noise:true]');
|
|
715
|
+
}
|
|
641
716
|
const result = lines.join('\n');
|
|
642
717
|
// Do not cache a truncated/partial enumeration — the broad ranking is
|
|
643
718
|
// known-incomplete, so a later call with a larger head_limit must re-run.
|
|
@@ -753,8 +828,10 @@ export async function executeFindFilesTool(args, workDir, options = {}) {
|
|
|
753
828
|
return subject.toLowerCase().includes(nameLower);
|
|
754
829
|
};
|
|
755
830
|
|
|
756
|
-
|
|
831
|
+
let _preStat;
|
|
832
|
+
try { _preStat = await assertPathReachable(fullPath); }
|
|
757
833
|
catch (err) { return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`; }
|
|
834
|
+
if (_preStat) statCacheSet(fullPath, _preStat);
|
|
758
835
|
let rootStat;
|
|
759
836
|
try { rootStat = getCachedReadOnlyStat(fullPath); }
|
|
760
837
|
catch (err) {
|
|
@@ -794,7 +871,7 @@ export async function executeFindFilesTool(args, workDir, options = {}) {
|
|
|
794
871
|
if (!namePattern) {
|
|
795
872
|
const enumRes = await getBroadEnumeration({
|
|
796
873
|
root: normalizeOutputPath(fullPath),
|
|
797
|
-
hidden, depth, includeNoise,
|
|
874
|
+
hidden, depth, includeNoise, ignoreMode: 'all',
|
|
798
875
|
rgArgs, cwd: fullPath,
|
|
799
876
|
});
|
|
800
877
|
rgStdoutTruncated = enumRes.truncated;
|
|
@@ -2,6 +2,7 @@ import { homedir } from 'os';
|
|
|
2
2
|
import { isAbsolute, relative, resolve } from 'path';
|
|
3
3
|
import { realpathSync, statSync } from 'node:fs';
|
|
4
4
|
import { isWSL } from '../../../../shared/wsl.mjs';
|
|
5
|
+
import { statCacheSet } from './cache-layers.mjs';
|
|
5
6
|
|
|
6
7
|
// Restore the on-disk casing of a path (win32 only). rg relativizes candidate
|
|
7
8
|
// paths against its process cwd with a CASE-SENSITIVE prefix strip before
|
|
@@ -248,7 +249,13 @@ export function coerceReadFamilyPathArg(path, workDir = null) {
|
|
|
248
249
|
if (/\s/.test(trimmed)) {
|
|
249
250
|
let fullPathExists = false;
|
|
250
251
|
try {
|
|
251
|
-
|
|
252
|
+
// Seed the stat cache with this existence probe so the caller's
|
|
253
|
+
// immediate getCachedReadOnlyStat on the same resolved path
|
|
254
|
+
// reuses it instead of re-stat'ing. Pure de-dup: a cache miss
|
|
255
|
+
// just falls back to the caller's own stat.
|
|
256
|
+
const _full = resolveAgainstCwd(normalizeInputPath(trimmed), workDir);
|
|
257
|
+
const _st = statSync(_full);
|
|
258
|
+
statCacheSet(_full, _st);
|
|
252
259
|
fullPathExists = true;
|
|
253
260
|
} catch { /* split only when the literal path is missing */ }
|
|
254
261
|
if (!fullPathExists) {
|
|
@@ -89,7 +89,7 @@ export function readEntryLineWindow(entry) {
|
|
|
89
89
|
};
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
export function coalesceObjectReadEntries(rawEntries) {
|
|
92
|
+
export function coalesceObjectReadEntries(rawEntries, resolvePath = null) {
|
|
93
93
|
const out = new Array(rawEntries.length);
|
|
94
94
|
const groups = new Map();
|
|
95
95
|
for (let i = 0; i < rawEntries.length; i++) {
|
|
@@ -99,7 +99,9 @@ export function coalesceObjectReadEntries(rawEntries) {
|
|
|
99
99
|
continue;
|
|
100
100
|
}
|
|
101
101
|
const win = readEntryLineWindow(entry);
|
|
102
|
-
|
|
102
|
+
// Group by RESOLVED path so two path strings that point at the same
|
|
103
|
+
// file share one coalesced disk window instead of each opening it.
|
|
104
|
+
const key = (typeof resolvePath === 'function' ? resolvePath(entry.path || '') : (entry.path || ''));
|
|
103
105
|
if (!groups.has(key)) groups.set(key, []);
|
|
104
106
|
groups.get(key).push({ index: i, entry, offset: win.offset, end: win.end });
|
|
105
107
|
}
|
|
@@ -101,7 +101,9 @@ function ensureReadRangeIndexDiskSwept() {
|
|
|
101
101
|
function loadReadRangeIndexFromDisk(fullPath, st) {
|
|
102
102
|
ensureReadRangeIndexDiskSwept();
|
|
103
103
|
const file = readRangeIndexFilePath(fullPath);
|
|
104
|
-
|
|
104
|
+
// No existsSync preflight: a missing file surfaces as an ENOENT from
|
|
105
|
+
// readFileSync below, caught by the same try/catch — one FS pass, not two.
|
|
106
|
+
if (!file || !st) return null;
|
|
105
107
|
try {
|
|
106
108
|
const row = JSON.parse(readFileSync(file, 'utf-8'));
|
|
107
109
|
if (!readRangeIndexMatches(row, fullPath, st)) return null;
|
|
@@ -124,7 +124,10 @@ export function recordReadSnapshot(fullPath, st, scope = null, meta = {}) {
|
|
|
124
124
|
}
|
|
125
125
|
if (!next.contentHash && snapshotCoversFullFile(next)) {
|
|
126
126
|
try {
|
|
127
|
-
|
|
127
|
+
// Reuse the raw-content cache (populated by the read that produced
|
|
128
|
+
// this snapshot) instead of a fresh readFileSync + decode purely to
|
|
129
|
+
// hash. Decodes via the same helper, so the hash is byte-identical.
|
|
130
|
+
const content = readTextForSnapshotCheck(fullPath, null, st);
|
|
128
131
|
next.contentHash = hashText(content);
|
|
129
132
|
} catch {}
|
|
130
133
|
}
|
|
@@ -225,7 +225,7 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
|
|
|
225
225
|
// range into one huge window. Far-apart reads stay separate,
|
|
226
226
|
// which avoids scanning and then slicing thousands of lines
|
|
227
227
|
// just to return two tiny windows.
|
|
228
|
-
const entries = coalesceObjectReadEntries(rawEntries);
|
|
228
|
+
const entries = coalesceObjectReadEntries(rawEntries, (p) => resolveAgainstCwd(p, workDir));
|
|
229
229
|
// Deduplicate so the same union-range is read only once per path.
|
|
230
230
|
const _seen = new Map(); // cacheKey → dedupedEntries index
|
|
231
231
|
const dedupedEntries = [];
|
|
@@ -282,11 +282,31 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
|
|
|
282
282
|
// re-order results to match the caller's original entry order.
|
|
283
283
|
const _origEntries2 = Array.isArray(args._readsOrigEntries) ? args._readsOrigEntries : null;
|
|
284
284
|
const _entryMap2 = Array.isArray(args._readsEntryToDeduped) ? args._readsEntryToDeduped : null;
|
|
285
|
+
// Dedup string-batch entries by RESOLVED path + window so a file that
|
|
286
|
+
// appears twice (incl. two path strings that resolve to the same file)
|
|
287
|
+
// is stat/opened/read ONCE, not per duplicate. Duplicates copy the
|
|
288
|
+
// primary's body, keeping the per-index render byte-identical. Skipped
|
|
289
|
+
// when `overrides` (reads[] coalesce path) is set — those entries were
|
|
290
|
+
// already deduped upstream and carry the union-slice bookkeeping.
|
|
291
|
+
const _readIndexFor = new Array(entries.length);
|
|
292
|
+
if (!overrides) {
|
|
293
|
+
const _dedup = new Map();
|
|
294
|
+
for (let i = 0; i < entries.length; i++) {
|
|
295
|
+
const e = entries[i];
|
|
296
|
+
if (!e || !e.path) { _readIndexFor[i] = i; continue; }
|
|
297
|
+
const rp = resolveAgainstCwd(e.path, workDir);
|
|
298
|
+
const k = `${rp}|${e.mode ?? ''}|${e.offset ?? ''}|${e.limit ?? ''}|${e.n ?? ''}|${e.full ?? ''}`;
|
|
299
|
+
if (_dedup.has(k)) { _readIndexFor[i] = _dedup.get(k); }
|
|
300
|
+
else { _dedup.set(k, i); _readIndexFor[i] = i; }
|
|
301
|
+
}
|
|
302
|
+
} else {
|
|
303
|
+
for (let i = 0; i < entries.length; i++) _readIndexFor[i] = i;
|
|
304
|
+
}
|
|
285
305
|
const tasks = entries.map((entry, index) => ({
|
|
286
306
|
entry,
|
|
287
307
|
index,
|
|
288
308
|
offset: _isFullModeReadEntry(entry) ? _readEntryLineWindow(entry).offset : 0,
|
|
289
|
-
})).sort((a, b) => {
|
|
309
|
+
})).filter((t) => _readIndexFor[t.index] === t.index).sort((a, b) => {
|
|
290
310
|
const ap = a.entry?.path || '';
|
|
291
311
|
const bp = b.entry?.path || '';
|
|
292
312
|
if (ap !== bp) return ap < bp ? -1 : 1;
|
|
@@ -320,6 +340,17 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
|
|
|
320
340
|
readChains.set(key, next.catch(() => {}));
|
|
321
341
|
return next;
|
|
322
342
|
}));
|
|
343
|
+
// Fan the primary read's result out to its duplicate indices so every
|
|
344
|
+
// caller slot is populated without a second disk window.
|
|
345
|
+
for (let i = 0; i < entries.length; i++) {
|
|
346
|
+
const src = _readIndexFor[i];
|
|
347
|
+
if (src === i) continue;
|
|
348
|
+
const e = entries[i];
|
|
349
|
+
const s = results[src];
|
|
350
|
+
results[i] = s
|
|
351
|
+
? { path: e.path, mode: e.mode || 'full', n: e.n, body: s.body }
|
|
352
|
+
: { path: e.path, mode: e.mode || 'full', n: e.n, body: 'Error: dedup mapping failed' };
|
|
353
|
+
}
|
|
323
354
|
const orderedResults = _origEntries2
|
|
324
355
|
? _origEntries2.map((orig, i) => {
|
|
325
356
|
const r = results[_entryMap2 ? _entryMap2[i] : i] || { path: orig.path, mode: orig.mode || 'full', body: 'Error: dedup mapping failed' };
|