mixdog 0.9.151 → 0.9.152
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/README.md +110 -68
- package/package.json +3 -2
- package/scripts/tool-stress.mjs +0 -2
- package/src/defaults/skills/setup/SKILL.md +18 -27
- package/src/headless-exec.mjs +3 -10
- package/src/headless-exec.test.mjs +2 -7
- package/src/runtime/agent/orchestrator/config.mjs +6 -66
- package/src/runtime/agent/orchestrator/context/collect-skills.test.mjs +153 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +49 -45
- package/src/runtime/agent/orchestrator/mcp/client.mjs +242 -15
- package/src/runtime/agent/orchestrator/mcp/features.test.mjs +66 -0
- package/src/runtime/agent/orchestrator/mcp/security.test.mjs +36 -20
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +54 -33
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +3 -8
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +10 -2
- package/src/runtime/agent/orchestrator/providers/lib/provider-replay.mjs +50 -0
- package/src/runtime/agent/orchestrator/providers/lib/sse-framing.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +3 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +8 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +25 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +35 -4
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +0 -2
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +13 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +4 -7
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/provider-replay.test.mjs +235 -0
- package/src/runtime/agent/orchestrator/providers/sse-framing.test.mjs +719 -0
- package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +399 -39
- package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +26 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +14 -4
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +24 -1
- package/src/runtime/agent/orchestrator/session/compaction-read-reset.test.mjs +161 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +14 -4
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +12 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +31 -18
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +6 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +1 -3
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +556 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.test.mjs +457 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +105 -186
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +115 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +23 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool-cwd-env.test.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -10
- package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +30 -14
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +15 -4
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +73 -15
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +76 -2
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +24 -683
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +64 -72
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +2 -5
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.mjs +75 -0
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.test.mjs +24 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +24 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.test.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +12 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +35 -21
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +28 -13
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.test.mjs +54 -0
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
- package/src/runtime/browser-bridge/client.mjs +103 -0
- package/src/runtime/browser-bridge/tool-defs.mjs +42 -0
- package/src/runtime/computer-bridge/client.mjs +101 -0
- package/src/runtime/computer-bridge/tool-defs.mjs +53 -0
- package/src/runtime/media/store.mjs +18 -8
- package/src/runtime/media/store.test.mjs +14 -0
- package/src/runtime/memory/index.mjs +1 -1
- package/src/runtime/memory/lib/transcript-ingest.mjs +8 -6
- package/src/runtime/memory/lib/transcript-ingest.test.mjs +52 -0
- package/src/runtime/shared/child-spawn-gate.mjs +17 -0
- package/src/runtime/shared/child-spawn-remote.mjs +69 -7
- package/src/runtime/shared/child-spawn-remote.test.mjs +124 -0
- package/src/runtime/shared/pristine-execution-contract.json +2 -1
- package/src/runtime/shared/provider-api-key.mjs +1 -0
- package/src/runtime/shared/session-runtime-health.mjs +91 -0
- package/src/runtime/shared/session-runtime-health.test.mjs +53 -0
- package/src/runtime/shared/skill-document.mjs +86 -0
- package/src/runtime/shared/skill-document.test.mjs +81 -0
- package/src/runtime/shared/tool-surface.mjs +18 -0
- package/src/runtime/shared/turn-snapshot-store.mjs +1 -0
- package/src/runtime/shared/turn-snapshot.mjs +32 -9
- package/src/runtime/shared/user-cwd.mjs +33 -14
- package/src/runtime/shared/user-cwd.test.mjs +43 -0
- package/src/session-runtime/cwd-plugins.mjs +26 -26
- package/src/session-runtime/cwd-plugins.test.mjs +75 -0
- package/src/session-runtime/cwd-tool-routing.test.mjs +5 -0
- package/src/session-runtime/global-extensions.mjs +26 -0
- package/src/session-runtime/global-extensions.test.mjs +28 -0
- package/src/session-runtime/goal-runtime.mjs +721 -0
- package/src/session-runtime/goal-runtime.test.mjs +108 -0
- package/src/session-runtime/lifecycle-api.mjs +18 -4
- package/src/session-runtime/lifecycle-api.test.mjs +21 -0
- package/src/session-runtime/mcp-glue.mjs +49 -47
- package/src/session-runtime/plugin-mcp-standard.test.mjs +177 -0
- package/src/session-runtime/plugin-mcp.mjs +96 -22
- package/src/session-runtime/prewarm.mjs +1 -10
- package/src/session-runtime/resource-api-global.test.mjs +69 -0
- package/src/session-runtime/resource-api.mjs +173 -58
- package/src/session-runtime/runtime-core.mjs +148 -20
- package/src/session-runtime/runtime-tunables.mjs +6 -8
- package/src/session-runtime/session-turn-api.mjs +22 -11
- package/src/session-runtime/skills-api.mjs +79 -25
- package/src/session-runtime/skills-api.test.mjs +99 -0
- package/src/session-runtime/tool-catalog-schema.mjs +0 -5
- package/src/session-runtime/tool-catalog.mjs +4 -13
- package/src/session-runtime/tool-policy-surface.test.mjs +33 -11
- package/src/session-runtime/tool-surface.mjs +12 -0
- package/src/standalone/agent-dispatch-broker.mjs +5 -9
- package/src/standalone/agent-tool/notify.mjs +6 -8
- package/src/standalone/agent-tool/notify.test.mjs +37 -0
- package/src/standalone/daemon.mjs +31 -15
- package/src/standalone/hook-bus/config.mjs +1 -0
- package/src/standalone/plugin-admin.mjs +17 -0
- package/src/standalone/plugin-admin.test.mjs +7 -0
- package/src/standalone/provider-admin.mjs +1 -0
- package/src/standalone/session-protocol.mjs +5 -0
- package/src/standalone/session-runtime-agent-control-client.mjs +133 -0
- package/src/standalone/session-runtime-agent-control-client.test.mjs +71 -0
- package/src/standalone/session-runtime-dispatch-cancel.test.mjs +139 -0
- package/src/standalone/session-runtime-host-factory.mjs +17 -0
- package/src/standalone/session-runtime-host-factory.test.mjs +31 -0
- package/src/standalone/session-runtime-host-health.test.mjs +5 -0
- package/src/standalone/session-runtime-host.mjs +696 -58
- package/src/standalone/session-runtime-inline-host.mjs +351 -0
- package/src/standalone/session-runtime-inline-host.test.mjs +83 -0
- package/src/standalone/session-runtime-provider-cooldown.mjs +78 -0
- package/src/standalone/session-runtime-provider-cooldown.test.mjs +80 -0
- package/src/standalone/session-runtime-shard-host.test.mjs +569 -0
- package/src/standalone/session-runtime-shard-router.mjs +149 -0
- package/src/standalone/session-runtime-shard-router.test.mjs +101 -0
- package/src/standalone/session-runtime-worker.mjs +476 -18
- package/src/standalone/session-service.mjs +81 -6
- package/src/tui/app/app-view.jsx +1 -0
- package/src/tui/app/core-memory-picker.mjs +2 -0
- package/src/tui/app/extension-pickers.mjs +1 -1
- package/src/tui/app/maintenance-pickers.mjs +3 -3
- package/src/tui/app/model-options.mjs +2 -0
- package/src/tui/app/model-picker.mjs +14 -4
- package/src/tui/app/panel-handoff.test.mjs +311 -0
- package/src/tui/app/project-picker.mjs +1 -0
- package/src/tui/app/route-pickers.mjs +21 -6
- package/src/tui/app/settings-picker.mjs +26 -3
- package/src/tui/app/slash-commands.mjs +1 -0
- package/src/tui/app/slash-dispatch.mjs +73 -22
- package/src/tui/app/theme-effort-pickers.mjs +7 -2
- package/src/tui/components/Picker.jsx +10 -1
- package/src/tui/dist/index.mjs +163 -44
- package/src/tui/session/context-state.mjs +1 -0
- package/src/tui/session/goal-continuation.mjs +110 -0
- package/src/tui/session/goal-continuation.test.mjs +84 -0
- package/src/tui/session/queue-helpers.mjs +5 -2
- package/src/tui/session/session-action-surface.test.mjs +108 -0
- package/src/tui/session/session-api-ext.mjs +39 -8
- package/src/tui/session/session-api.mjs +58 -0
- package/src/tui/session/session-flow.mjs +6 -0
- package/src/tui/session/turn.mjs +1 -0
- package/src/tui/session-local.mjs +15 -1
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +0 -301
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { watch } from 'fs';
|
|
2
1
|
import { readdir, stat } from 'fs/promises';
|
|
3
2
|
import { basename, isAbsolute, relative } from 'path';
|
|
4
3
|
import {
|
|
@@ -23,8 +22,6 @@ import {
|
|
|
23
22
|
lstatPathsForMtime,
|
|
24
23
|
statCacheSet,
|
|
25
24
|
statPathsForMtime,
|
|
26
|
-
registerCacheInvalidationListener,
|
|
27
|
-
invalidateBuiltinResultCache,
|
|
28
25
|
} from './cache-layers.mjs';
|
|
29
26
|
import { formatListSize, formatMtime } from './list-formatting.mjs';
|
|
30
27
|
import {
|
|
@@ -39,11 +36,9 @@ import {
|
|
|
39
36
|
} from './tool-output-limit.mjs';
|
|
40
37
|
import { runRg, runRgWindowedLines } from './native-search-runner.mjs';
|
|
41
38
|
import { tryServeFuzzySearch, tryServeListMetadata } from './native-search-client.mjs';
|
|
42
|
-
import { fuzzyRankMulti, prepareFuzzyItems, splitFuzzyQueryTokens } from './fuzzy-match.mjs';
|
|
43
39
|
import {
|
|
44
40
|
recordLocalSearchBackend,
|
|
45
41
|
recordLocalSearchCacheHit,
|
|
46
|
-
recordLocalSearchIndex,
|
|
47
42
|
recordNativeSearchTiming,
|
|
48
43
|
} from './local-search-telemetry.mjs';
|
|
49
44
|
import { assertPathReachable } from './fs-reachability.mjs';
|
|
@@ -64,10 +59,6 @@ const FIND_WALK_TIMEOUT_MS = 20_000;
|
|
|
64
59
|
// whole-filesystem probes across six benchmark tasks, each burning a turn for
|
|
65
60
|
// an error). At this cap the native search returns its ranked partial instead.
|
|
66
61
|
const FIND_FUZZY_TIMEOUT_MS = positiveTimeoutEnv('MIXDOG_FIND_FUZZY_TIMEOUT_MS', 1_500);
|
|
67
|
-
// If the fast fuzzy window sees no usable candidate, spend the remainder of
|
|
68
|
-
// the outer 20s read-only budget on a literal query-targeted path scan. This
|
|
69
|
-
// finds real paths without repeating the complete fuzzy inventory.
|
|
70
|
-
const FIND_TARGETED_TIMEOUT_MS = positiveTimeoutEnv('MIXDOG_FIND_TARGETED_TIMEOUT_MS', 17_000);
|
|
71
62
|
// Same contract for name/glob find: bound the walk and the metadata pass, then
|
|
72
63
|
// SAY the result is partial. The old 20s walk plus a 5s stat deadline silently
|
|
73
64
|
// dropped every path whose stat missed the deadline, so a slow scope reported
|
|
@@ -243,9 +234,6 @@ async function tryNativeDeepListRows({
|
|
|
243
234
|
}
|
|
244
235
|
|
|
245
236
|
export async function executeListTool(args, workDir, options = {}) {
|
|
246
|
-
if (typeof args.fuzzy === 'string' && args.fuzzy.length > 0) {
|
|
247
|
-
return executeFuzzyFindTool({ ...args, query: args.fuzzy }, workDir, options);
|
|
248
|
-
}
|
|
249
237
|
if (args.mode === 'tree') return executeTreeTool(args, workDir, options);
|
|
250
238
|
if (args.mode === 'find') return executeFindFilesTool(args, workDir, options);
|
|
251
239
|
args.path = normalizeInputPath(args.path);
|
|
@@ -597,482 +585,15 @@ export async function executeTreeTool(args, workDir, options = {}) {
|
|
|
597
585
|
return out;
|
|
598
586
|
}
|
|
599
587
|
|
|
600
|
-
//
|
|
601
|
-
//
|
|
602
|
-
//
|
|
603
|
-
// broad pass and the find_files broad fast path re-run that full sweep for
|
|
604
|
-
// every query item AND for every concurrent caller (measured 1-4s each when
|
|
605
|
-
// several locator calls hit the same root). Cache the PARSED file list per
|
|
606
|
-
// key with in-flight promise dedup (N concurrent callers share ONE sweep)
|
|
607
|
-
// plus a short TTL for serial reuse. Truncated/partial sweeps are
|
|
608
|
-
// known-incomplete and are NEVER cached.
|
|
609
|
-
const FIND_ENUM_CACHE = new Map(); // key -> { files, items, expiresAt, gen }
|
|
610
|
-
const FIND_ENUM_INFLIGHT = new Map(); // key -> { promise, controller, subscribers }
|
|
611
|
-
const FIND_TARGETED_BATCHES_BY_RUNNER = new WeakMap();
|
|
612
|
-
const FIND_ENUM_ROOT_GEN = new Map();
|
|
613
|
-
const FIND_ENUM_WATCHERS = new Map();
|
|
614
|
-
let FIND_ENUM_GEN = 0;
|
|
615
|
-
|
|
616
|
-
// The broad enumeration is a DERIVED cache the scope/path invalidation layer
|
|
617
|
-
// does not otherwise know about. Invalidate only inventories whose roots
|
|
618
|
-
// overlap the written paths; a patch in an isolated temp root must not force
|
|
619
|
-
// every active Project to rescan.
|
|
620
|
-
function findEnumerationPathsOverlap(left, right) {
|
|
621
|
-
const contains = (base, target) => {
|
|
622
|
-
const rel = relative(base, target);
|
|
623
|
-
return rel === '' || (!isAbsolute(rel) && !/^\.\.(?:[\\/]|$)/.test(rel));
|
|
624
|
-
};
|
|
625
|
-
return contains(left, right) || contains(right, left);
|
|
626
|
-
}
|
|
627
|
-
function findEnumerationRootFromKey(key) {
|
|
628
|
-
return String(key).split('\u0000', 1)[0];
|
|
629
|
-
}
|
|
630
|
-
function findEnumerationRootGeneration(root) {
|
|
631
|
-
return FIND_ENUM_ROOT_GEN.get(root) || 0;
|
|
632
|
-
}
|
|
633
|
-
function invalidateFindEnumerationRoot(root) {
|
|
634
|
-
FIND_ENUM_ROOT_GEN.set(root, findEnumerationRootGeneration(root) + 1);
|
|
635
|
-
for (const key of [...FIND_ENUM_CACHE.keys()]) {
|
|
636
|
-
if (findEnumerationRootFromKey(key) === root) FIND_ENUM_CACHE.delete(key);
|
|
637
|
-
}
|
|
638
|
-
for (const key of [...FIND_ENUM_INFLIGHT.keys()]) {
|
|
639
|
-
if (findEnumerationRootFromKey(key) === root) FIND_ENUM_INFLIGHT.delete(key);
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
// Trailing-edge prewarm throttle: sustained write bursts (builds, installs)
|
|
643
|
-
// fire the watcher continuously; re-running the two full sweeps on every
|
|
644
|
-
// flush kept the serve bulk queue permanently busy. Invalidation stays
|
|
645
|
-
// immediate — only the warm rebuild is coalesced to one run per interval
|
|
646
|
-
// per root, scheduled at the trailing edge so the cache re-arms after the
|
|
647
|
-
// burst settles.
|
|
648
|
-
const FIND_ENUM_PREWARM_STATE = new Map(); // root -> { lastAt, timer }
|
|
649
|
-
const FIND_ENUM_PREWARM_MIN_INTERVAL_MS = 15_000;
|
|
650
|
-
function scheduleFindEnumerationPrewarm(root, delayMs = 100) {
|
|
651
|
-
let state = FIND_ENUM_PREWARM_STATE.get(root);
|
|
652
|
-
if (!state) {
|
|
653
|
-
state = { lastAt: 0, timer: null };
|
|
654
|
-
FIND_ENUM_PREWARM_STATE.set(root, state);
|
|
655
|
-
}
|
|
656
|
-
if (state.timer) return;
|
|
657
|
-
const wait = Math.max(delayMs, state.lastAt + FIND_ENUM_PREWARM_MIN_INTERVAL_MS - Date.now());
|
|
658
|
-
state.timer = setTimeout(() => {
|
|
659
|
-
state.timer = null;
|
|
660
|
-
state.lastAt = Date.now();
|
|
661
|
-
void prewarmFindEnumeration(root);
|
|
662
|
-
}, wait);
|
|
663
|
-
state.timer.unref?.();
|
|
664
|
-
}
|
|
665
|
-
function ensureFindEnumerationWatcher(root) {
|
|
666
|
-
const existing = FIND_ENUM_WATCHERS.get(root);
|
|
667
|
-
if (existing) {
|
|
668
|
-
existing.touchedAt = Date.now();
|
|
669
|
-
return true;
|
|
670
|
-
}
|
|
671
|
-
try {
|
|
672
|
-
const pending = new Set();
|
|
673
|
-
const record = { watcher: null, timer: null, touchedAt: Date.now() };
|
|
674
|
-
const flush = () => {
|
|
675
|
-
record.timer = null;
|
|
676
|
-
const paths = [...pending];
|
|
677
|
-
pending.clear();
|
|
678
|
-
invalidateBuiltinResultCache(paths.length ? paths : [root]);
|
|
679
|
-
scheduleFindEnumerationPrewarm(root);
|
|
680
|
-
};
|
|
681
|
-
record.watcher = watch(root, { recursive: true, persistent: false }, (_event, filename) => {
|
|
682
|
-
const rel = filename == null ? '' : String(filename);
|
|
683
|
-
pending.add(rel ? resolveAgainstCwd(rel, root) : root);
|
|
684
|
-
if (record.timer) clearTimeout(record.timer);
|
|
685
|
-
record.timer = setTimeout(flush, 40);
|
|
686
|
-
record.timer.unref?.();
|
|
687
|
-
});
|
|
688
|
-
record.watcher.on('error', () => {
|
|
689
|
-
if (record.timer) clearTimeout(record.timer);
|
|
690
|
-
FIND_ENUM_WATCHERS.delete(root);
|
|
691
|
-
invalidateFindEnumerationRoot(root);
|
|
692
|
-
try { record.watcher.close(); } catch {}
|
|
693
|
-
});
|
|
694
|
-
record.watcher.unref?.();
|
|
695
|
-
FIND_ENUM_WATCHERS.set(root, record);
|
|
696
|
-
if (FIND_ENUM_WATCHERS.size > 16) {
|
|
697
|
-
const oldest = [...FIND_ENUM_WATCHERS.entries()]
|
|
698
|
-
.filter(([candidate]) => candidate !== root)
|
|
699
|
-
.sort((a, b) => a[1].touchedAt - b[1].touchedAt)[0];
|
|
700
|
-
if (oldest) {
|
|
701
|
-
FIND_ENUM_WATCHERS.delete(oldest[0]);
|
|
702
|
-
try { oldest[1].watcher.close(); } catch {}
|
|
703
|
-
invalidateFindEnumerationRoot(oldest[0]);
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
return true;
|
|
707
|
-
} catch {
|
|
708
|
-
return false;
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
registerCacheInvalidationListener((affectedPaths) => {
|
|
712
|
-
if (!Array.isArray(affectedPaths) || affectedPaths.length === 0) {
|
|
713
|
-
const roots = new Set([
|
|
714
|
-
...FIND_ENUM_CACHE.keys(),
|
|
715
|
-
...FIND_ENUM_INFLIGHT.keys(),
|
|
716
|
-
].map(findEnumerationRootFromKey));
|
|
717
|
-
for (const root of roots) {
|
|
718
|
-
if (!FIND_ENUM_WATCHERS.has(root)) invalidateFindEnumerationRoot(root);
|
|
719
|
-
}
|
|
720
|
-
return;
|
|
721
|
-
}
|
|
722
|
-
const keys = new Set([...FIND_ENUM_CACHE.keys(), ...FIND_ENUM_INFLIGHT.keys()]);
|
|
723
|
-
const affectedRoots = new Set();
|
|
724
|
-
for (const key of keys) {
|
|
725
|
-
const root = findEnumerationRootFromKey(key);
|
|
726
|
-
if (affectedPaths.some((affected) => findEnumerationPathsOverlap(root, affected))) {
|
|
727
|
-
affectedRoots.add(root);
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
for (const root of affectedRoots) {
|
|
731
|
-
invalidateFindEnumerationRoot(root);
|
|
732
|
-
}
|
|
733
|
-
});
|
|
734
|
-
|
|
735
|
-
function findEnumTtlMs() {
|
|
736
|
-
const raw = process.env.MIXDOG_FIND_ENUM_CACHE_TTL_MS;
|
|
737
|
-
if (raw == null || raw === '') return 30000;
|
|
738
|
-
const n = Number(raw);
|
|
739
|
-
if (!Number.isFinite(n) || n < 0) return 30000; // malformed → default
|
|
740
|
-
return Math.floor(n); // 0 = disabled
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
function findEnumKey({ root, hidden, depth, includeNoise, ignoreMode }) {
|
|
744
|
-
return `${root}\u0000${hidden ? 1 : 0}\u0000${depth ?? ''}\u0000${includeNoise ? 1 : 0}\u0000${ignoreMode}`;
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
// Parse `rg --files` stdout into the same normalized relative-path list both
|
|
748
|
-
// broad passes build (strip trailing CR, drop empties, strip leading `./`,
|
|
749
|
-
// forward-slash). Module-level so the cache and both call sites agree.
|
|
750
|
-
function parseRgFileList(stdout) {
|
|
751
|
-
return String(stdout)
|
|
752
|
-
.split('\n')
|
|
753
|
-
.map((p) => (p.endsWith('\r') ? p.slice(0, -1) : p))
|
|
754
|
-
.filter((p) => p.length > 0)
|
|
755
|
-
.map((p) => normalizeOutputPath(p.replace(/^\.[/\\]/, '')));
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
function findEnumerationAbortError(signal) {
|
|
759
|
-
const reason = signal?.reason;
|
|
760
|
-
return reason instanceof Error ? reason : new Error(String(reason || 'find canceled'));
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
function subscribeToFindEnumeration(key, entry, signal = null) {
|
|
764
|
-
const token = {};
|
|
765
|
-
entry.subscribers.add(token);
|
|
766
|
-
return new Promise((resolve, reject) => {
|
|
767
|
-
let done = false;
|
|
768
|
-
const detach = () => {
|
|
769
|
-
if (signal instanceof AbortSignal) {
|
|
770
|
-
try { signal.removeEventListener('abort', onAbort); } catch { /* ignore */ }
|
|
771
|
-
}
|
|
772
|
-
};
|
|
773
|
-
const settle = (fn, value) => {
|
|
774
|
-
if (done) return;
|
|
775
|
-
done = true;
|
|
776
|
-
entry.subscribers.delete(token);
|
|
777
|
-
detach();
|
|
778
|
-
fn(value);
|
|
779
|
-
};
|
|
780
|
-
function onAbort() {
|
|
781
|
-
const error = findEnumerationAbortError(signal);
|
|
782
|
-
settle(reject, error);
|
|
783
|
-
// A shared broad sweep survives one caller canceling, but when every
|
|
784
|
-
// subscriber is gone it is useless work and must release its rg.
|
|
785
|
-
if (entry.subscribers.size === 0) {
|
|
786
|
-
try { entry.controller.abort(error); } catch { /* ignore */ }
|
|
787
|
-
if (FIND_ENUM_INFLIGHT.get(key) === entry) FIND_ENUM_INFLIGHT.delete(key);
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
if (signal instanceof AbortSignal) {
|
|
791
|
-
if (signal.aborted) { onAbort(); return; }
|
|
792
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
793
|
-
}
|
|
794
|
-
entry.promise.then(
|
|
795
|
-
(value) => settle(resolve, value),
|
|
796
|
-
(error) => settle(reject, error),
|
|
797
|
-
);
|
|
798
|
-
});
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
// Run (or reuse) the broad `rg --files` sweep for a root. Returns
|
|
802
|
-
// { files, truncated, partial }. The returned `files` array is SHARED — callers
|
|
803
|
-
// must treat it as read-only. `rgArgs` must be the broad-pass args (no per-query
|
|
804
|
-
// narrowing); the cache key includes every enumeration-affecting dimension, so
|
|
805
|
-
// any caller producing an
|
|
806
|
-
// equivalent sweep for the same dims reuses the result.
|
|
807
|
-
async function getBroadEnumeration({ root, hidden, depth, includeNoise, ignoreMode, rgArgs, cwd, runRgImpl = runRg, bestEffort = false, signal = null }) {
|
|
808
|
-
const ttl = findEnumTtlMs();
|
|
809
|
-
const key = findEnumKey({ root, hidden, depth, includeNoise, ignoreMode });
|
|
810
|
-
const rootGen = findEnumerationRootGeneration(root);
|
|
811
|
-
if (ttl > 0) {
|
|
812
|
-
const hit = FIND_ENUM_CACHE.get(key);
|
|
813
|
-
if (hit && hit.gen === FIND_ENUM_GEN && hit.rootGen === rootGen && hit.expiresAt > Date.now()) {
|
|
814
|
-
recordLocalSearchIndex('hit', hit.files.length);
|
|
815
|
-
return { files: hit.files, items: hit.items, truncated: false, partial: false };
|
|
816
|
-
}
|
|
817
|
-
if (hit) FIND_ENUM_CACHE.delete(key); // expired
|
|
818
|
-
}
|
|
819
|
-
// Single-flight is independent of the persistent TTL cache. Even when
|
|
820
|
-
// MIXDOG_FIND_ENUM_CACHE_TTL_MS=0 disables reuse across calls, concurrent
|
|
821
|
-
// scalar find calls should still share the one broad `rg --files` sweep
|
|
822
|
-
// instead of spawning N identical enumerations.
|
|
823
|
-
const inflight = FIND_ENUM_INFLIGHT.get(key);
|
|
824
|
-
if (inflight) return subscribeToFindEnumeration(key, inflight, signal);
|
|
825
|
-
const genAtStart = FIND_ENUM_GEN;
|
|
826
|
-
const rootGenAtStart = rootGen;
|
|
827
|
-
const controller = new AbortController();
|
|
828
|
-
const entry = { promise: null, controller, subscribers: new Set() };
|
|
829
|
-
entry.promise = (async () => {
|
|
830
|
-
let truncated = false;
|
|
831
|
-
let partial = false;
|
|
832
|
-
let files;
|
|
833
|
-
if (runRgImpl === runRg) {
|
|
834
|
-
const served = await runRgWindowedLines(rgArgs, {
|
|
835
|
-
cwd, timeout: 10_000, signal: controller.signal,
|
|
836
|
-
}, {
|
|
837
|
-
offset: 0,
|
|
838
|
-
limit: 50_000,
|
|
839
|
-
nativeInventory: true,
|
|
840
|
-
// Only explicit boot/prewarm work may finish an inventory after
|
|
841
|
-
// its caller leaves. Normal find calls cancel on last waiter.
|
|
842
|
-
keepWarm: bestEffort,
|
|
843
|
-
});
|
|
844
|
-
files = parseRgFileList(served.lines.join('\n'));
|
|
845
|
-
truncated = served.complete !== true || served.truncated === true;
|
|
846
|
-
partial = served.partial === true || served.timeout === true;
|
|
847
|
-
} else {
|
|
848
|
-
const stdout = await runRgImpl(rgArgs, { cwd, signal: controller.signal });
|
|
849
|
-
truncated = Boolean(stdout && typeof stdout === 'object' && stdout.truncated);
|
|
850
|
-
partial = Boolean(stdout && typeof stdout === 'object' && stdout.partial);
|
|
851
|
-
files = parseRgFileList(stdout);
|
|
852
|
-
}
|
|
853
|
-
const items = prepareFuzzyItems(files);
|
|
854
|
-
recordLocalSearchIndex('build', files.length);
|
|
855
|
-
// Never cache a truncated/partial sweep — it is known-incomplete, so a
|
|
856
|
-
// later query with a larger head_limit must re-run the enumeration.
|
|
857
|
-
// Also never let an in-flight prewarm/real sweep repopulate after a
|
|
858
|
-
// write invalidation cleared the cache during the sweep.
|
|
859
|
-
if (ttl > 0 && !truncated && !partial
|
|
860
|
-
&& FIND_ENUM_GEN === genAtStart
|
|
861
|
-
&& findEnumerationRootGeneration(root) === rootGenAtStart) {
|
|
862
|
-
const watched = ensureFindEnumerationWatcher(root);
|
|
863
|
-
FIND_ENUM_CACHE.set(key, {
|
|
864
|
-
files,
|
|
865
|
-
items,
|
|
866
|
-
expiresAt: watched ? Number.MAX_SAFE_INTEGER : Date.now() + ttl,
|
|
867
|
-
gen: genAtStart,
|
|
868
|
-
rootGen: rootGenAtStart,
|
|
869
|
-
});
|
|
870
|
-
}
|
|
871
|
-
return { files, items, truncated, partial };
|
|
872
|
-
})();
|
|
873
|
-
FIND_ENUM_INFLIGHT.set(key, entry);
|
|
874
|
-
const cleanup = () => {
|
|
875
|
-
if (FIND_ENUM_INFLIGHT.get(key) === entry) FIND_ENUM_INFLIGHT.delete(key);
|
|
876
|
-
};
|
|
877
|
-
entry.promise.then(cleanup, cleanup);
|
|
878
|
-
return subscribeToFindEnumeration(key, entry, signal);
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
// Cache-only peek: returns the SHARED files array of a still-valid broad
|
|
882
|
-
// sweep for these dimensions, or null. Never spawns, joins, or waits — the
|
|
883
|
-
// targeted find path below must not block behind a full-tree enumeration.
|
|
884
|
-
function peekBroadEnumeration({ root, hidden, depth, includeNoise, ignoreMode }) {
|
|
885
|
-
if (findEnumTtlMs() <= 0) return null;
|
|
886
|
-
const key = findEnumKey({ root, hidden, depth, includeNoise, ignoreMode });
|
|
887
|
-
const hit = FIND_ENUM_CACHE.get(key);
|
|
888
|
-
if (hit && hit.gen === FIND_ENUM_GEN
|
|
889
|
-
&& hit.rootGen === findEnumerationRootGeneration(root)
|
|
890
|
-
&& hit.expiresAt > Date.now()) {
|
|
891
|
-
return hit.files;
|
|
892
|
-
}
|
|
893
|
-
return null;
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
// Best-effort warm of the broad enumeration for a root using the `find` tool's
|
|
897
|
-
// DEFAULT flags (hidden:true, includeNoise:false, depth:unbounded). Swallows
|
|
898
|
-
// all errors — a failed prewarm must never surface or block the caller.
|
|
899
|
-
export async function prewarmFindEnumeration(root) {
|
|
900
|
-
try {
|
|
901
|
-
if (!root || typeof root !== 'string') return;
|
|
902
|
-
const hidden = true, includeNoise = false, depth = null;
|
|
903
|
-
const common = ['--files', '--no-require-git', '--hidden'];
|
|
904
|
-
for (const ex of DEFAULT_IGNORE_GLOBS) {
|
|
905
|
-
common.push('--glob', ex);
|
|
906
|
-
}
|
|
907
|
-
for (const ex of rootScanIgnoreGlobs(normalizeOutputPath(root))) common.push('--glob', ex);
|
|
908
|
-
common.push('.');
|
|
909
|
-
await getBroadEnumeration({
|
|
910
|
-
root: normalizeOutputPath(root),
|
|
911
|
-
hidden, depth, includeNoise, ignoreMode: 'git',
|
|
912
|
-
rgArgs: common, cwd: root, bestEffort: true,
|
|
913
|
-
});
|
|
914
|
-
} catch { /* best-effort warm; never surface */ }
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
function escapeFindGlobLiteral(value) {
|
|
918
|
-
return String(value).replace(/[*?[\]{}]/g, (char) => `[${char}]`);
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
async function getTargetedFindEnumeration({
|
|
922
|
-
queries,
|
|
923
|
-
root,
|
|
924
|
-
hidden,
|
|
925
|
-
depth,
|
|
926
|
-
includeNoise,
|
|
927
|
-
runRgImpl,
|
|
928
|
-
signal,
|
|
929
|
-
timeoutMs = null,
|
|
930
|
-
}) {
|
|
931
|
-
const terms = [...new Set((queries || []).map((query) => String(query || '').trim()).filter(Boolean))];
|
|
932
|
-
// Reuse a WARM broad inventory only (cache peek). A cold cache goes
|
|
933
|
-
// straight to the cheap targeted --iglob batch below: front-running the
|
|
934
|
-
// full `--no-ignore` sweep made cold finds stall behind the serve bulk
|
|
935
|
-
// queue for tens of seconds.
|
|
936
|
-
const inventory = peekBroadEnumeration({
|
|
937
|
-
root: normalizeOutputPath(root),
|
|
938
|
-
hidden,
|
|
939
|
-
depth,
|
|
940
|
-
includeNoise,
|
|
941
|
-
ignoreMode: includeNoise ? 'all' : 'git',
|
|
942
|
-
});
|
|
943
|
-
if (inventory) {
|
|
944
|
-
const lowerTerms = terms.map((term) => term.toLowerCase());
|
|
945
|
-
return {
|
|
946
|
-
files: inventory.filter((path) => {
|
|
947
|
-
const lower = path.toLowerCase();
|
|
948
|
-
return lowerTerms.some((term) => lower.includes(term));
|
|
949
|
-
}),
|
|
950
|
-
truncated: false,
|
|
951
|
-
partial: false,
|
|
952
|
-
};
|
|
953
|
-
}
|
|
954
|
-
let batches = FIND_TARGETED_BATCHES_BY_RUNNER.get(runRgImpl);
|
|
955
|
-
if (!batches) {
|
|
956
|
-
batches = new Map();
|
|
957
|
-
FIND_TARGETED_BATCHES_BY_RUNNER.set(runRgImpl, batches);
|
|
958
|
-
}
|
|
959
|
-
const batchKey = JSON.stringify([root, hidden, depth ?? '', includeNoise]);
|
|
960
|
-
let batch = batches.get(batchKey);
|
|
961
|
-
if (!batch) {
|
|
962
|
-
batch = {
|
|
963
|
-
terms: new Set(),
|
|
964
|
-
waiters: new Set(),
|
|
965
|
-
controller: new AbortController(),
|
|
966
|
-
};
|
|
967
|
-
batches.set(batchKey, batch);
|
|
968
|
-
setImmediate(async () => {
|
|
969
|
-
if (batches.get(batchKey) === batch) batches.delete(batchKey);
|
|
970
|
-
if (batch.waiters.size === 0) return;
|
|
971
|
-
const rgArgs = ['--files', includeNoise ? '--no-ignore' : '--no-require-git'];
|
|
972
|
-
if (hidden) rgArgs.push('--hidden');
|
|
973
|
-
if (depth != null) rgArgs.push('--max-depth', String(depth));
|
|
974
|
-
for (const query of batch.terms) {
|
|
975
|
-
rgArgs.push('--iglob', `*${escapeFindGlobLiteral(query)}*`);
|
|
976
|
-
}
|
|
977
|
-
if (!includeNoise) {
|
|
978
|
-
for (const ex of DEFAULT_IGNORE_GLOBS) rgArgs.push('--glob', ex);
|
|
979
|
-
}
|
|
980
|
-
for (const ex of rootScanIgnoreGlobs(root)) rgArgs.push('--glob', ex);
|
|
981
|
-
rgArgs.push('.');
|
|
982
|
-
try {
|
|
983
|
-
const stdout = await runRgImpl(rgArgs, {
|
|
984
|
-
cwd: root,
|
|
985
|
-
signal: batch.controller.signal,
|
|
986
|
-
...(Number.isFinite(Number(timeoutMs)) && Number(timeoutMs) > 0
|
|
987
|
-
? { timeout: Math.floor(Number(timeoutMs)) }
|
|
988
|
-
: {}),
|
|
989
|
-
});
|
|
990
|
-
const paths = parseRgFileList(stdout).filter((path) =>
|
|
991
|
-
includeNoise || !path.split('/').some((segment) => NOISE_DIR_NAMES.has(segment)));
|
|
992
|
-
const result = {
|
|
993
|
-
files: paths,
|
|
994
|
-
truncated: Boolean(stdout && typeof stdout === 'object' && stdout.truncated),
|
|
995
|
-
partial: Boolean(stdout && typeof stdout === 'object' && stdout.partial),
|
|
996
|
-
};
|
|
997
|
-
for (const waiter of [...batch.waiters]) waiter.resolve(result);
|
|
998
|
-
} catch (error) {
|
|
999
|
-
for (const waiter of [...batch.waiters]) waiter.reject(error);
|
|
1000
|
-
}
|
|
1001
|
-
});
|
|
1002
|
-
}
|
|
1003
|
-
for (const term of terms) batch.terms.add(term);
|
|
1004
|
-
const run = new Promise((resolve, reject) => {
|
|
1005
|
-
let settled = false;
|
|
1006
|
-
const waiter = {
|
|
1007
|
-
resolve(value) {
|
|
1008
|
-
if (settled) return;
|
|
1009
|
-
settled = true;
|
|
1010
|
-
batch.waiters.delete(waiter);
|
|
1011
|
-
if (signal instanceof AbortSignal) signal.removeEventListener('abort', onAbort);
|
|
1012
|
-
resolve(value);
|
|
1013
|
-
},
|
|
1014
|
-
reject(error) {
|
|
1015
|
-
if (settled) return;
|
|
1016
|
-
settled = true;
|
|
1017
|
-
batch.waiters.delete(waiter);
|
|
1018
|
-
if (signal instanceof AbortSignal) signal.removeEventListener('abort', onAbort);
|
|
1019
|
-
reject(error);
|
|
1020
|
-
},
|
|
1021
|
-
};
|
|
1022
|
-
const onAbort = () => {
|
|
1023
|
-
waiter.reject(findEnumerationAbortError(signal));
|
|
1024
|
-
if (batch.waiters.size === 0) {
|
|
1025
|
-
try { batch.controller.abort(findEnumerationAbortError(signal)); } catch {}
|
|
1026
|
-
}
|
|
1027
|
-
};
|
|
1028
|
-
batch.waiters.add(waiter);
|
|
1029
|
-
if (signal instanceof AbortSignal) {
|
|
1030
|
-
if (signal.aborted) onAbort();
|
|
1031
|
-
else signal.addEventListener('abort', onAbort, { once: true });
|
|
1032
|
-
}
|
|
1033
|
-
});
|
|
1034
|
-
return run;
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
// Fuzzy filename search (nucleo-style): collect the file
|
|
1038
|
-
// list via `rg --files`, then rank by subsequence score. `list.fuzzy` still
|
|
1039
|
-
// routes here for hidden backward compatibility, but the model-facing tool is
|
|
1040
|
-
// `find`.
|
|
1041
|
-
// A pruned tree cannot report what it never enumerated. Dependency and cache
|
|
1042
|
-
// directories are skipped by DEFAULT, so a file that exists only inside one —
|
|
1043
|
-
// a __pycache__ artifact, a vendored source, a build cache — came back as
|
|
1044
|
-
// "(no fuzzy match for X)", which reads as PROVEN ABSENCE and ends the search.
|
|
1045
|
-
// A second, noise-including pass turns that into the truth, but the default
|
|
1046
|
-
// answer still may not list dependency paths (that is the point of the
|
|
1047
|
-
// default), so the miss keeps its body and gains the one fact it was missing:
|
|
1048
|
-
// a match exists, and which flag surfaces it. Only a CLEAN miss retries — a
|
|
1049
|
-
// partial or timed-out pass says "no fuzzy match YET" and is already telling
|
|
1050
|
-
// the caller to narrow, so it never pays for a second walk.
|
|
588
|
+
// Fuzzy filename search has one canonical implementation: the Rust search
|
|
589
|
+
// server's resident inventory. A server failure is surfaced instead of
|
|
590
|
+
// launching a second filesystem walk with different semantics.
|
|
1051
591
|
export async function executeFuzzyFindTool(args, workDir, options = {}) {
|
|
1052
|
-
|
|
1053
|
-
const result = await runFuzzyFindPass(args, workDir, options);
|
|
1054
|
-
const text = String(result ?? '');
|
|
1055
|
-
if (!/^\(no fuzzy match for /.test(text)) return result;
|
|
1056
|
-
if (args?.include_noise === true || options?._findNoiseWidened === true) return result;
|
|
1057
|
-
// The retry walks the same tree again. A miss that was already expensive
|
|
1058
|
-
// pays for itself twice, so only a cheap pass earns the second look.
|
|
1059
|
-
if (performance.now() - startedAt > FIND_NOISE_WIDEN_BUDGET_MS) return result;
|
|
1060
|
-
const widened = String(await runFuzzyFindPass(
|
|
1061
|
-
{ ...args, include_noise: true },
|
|
1062
|
-
workDir,
|
|
1063
|
-
{ ...options, _findNoiseWidened: true },
|
|
1064
|
-
) ?? '');
|
|
1065
|
-
if (!widened || /^\(no fuzzy match/.test(widened)) return result;
|
|
1066
|
-
const hits = widened.split('\n').filter((line) => line && !line.startsWith('... [') && !line.startsWith('[')).length;
|
|
1067
|
-
return `${text}\n[notice] ${hits} match${hits === 1 ? '' : 'es'} exist inside dependency/cache trees the default scan skips`
|
|
1068
|
-
+ ' — pass include_noise:true to list them.';
|
|
592
|
+
return runFuzzyFindPass(args, workDir, options);
|
|
1069
593
|
}
|
|
1070
594
|
|
|
1071
|
-
/** A miss cheaper than this earns the one widening retry below. */
|
|
1072
|
-
const FIND_NOISE_WIDEN_BUDGET_MS = 2_000;
|
|
1073
|
-
|
|
1074
595
|
async function runFuzzyFindPass(args, workDir, options = {}) {
|
|
1075
|
-
const query = String(args.query ??
|
|
596
|
+
const query = String(args.query ?? '').trim();
|
|
1076
597
|
if (!query) return 'Error: find requires query.';
|
|
1077
598
|
const inputPath = normalizeInputPath(args.path) || '.';
|
|
1078
599
|
const guard = listGuardPath(inputPath);
|
|
@@ -1115,24 +636,16 @@ async function runFuzzyFindPass(args, workDir, options = {}) {
|
|
|
1115
636
|
const nativeFuzzyImpl = typeof options?.__tryServeFuzzySearch === 'function'
|
|
1116
637
|
? options.__tryServeFuzzySearch
|
|
1117
638
|
: tryServeFuzzySearch;
|
|
1118
|
-
const runRgImpl = (options && typeof options.__runRg === 'function') ? options.__runRg : runRg;
|
|
1119
639
|
let nativeEmptyPartial = null;
|
|
1120
640
|
// Space-separated fragments AND-match one path. The native matcher handles
|
|
1121
641
|
// every token independently and sums exact per-token scores.
|
|
1122
|
-
|
|
1123
|
-
if (
|
|
1124
|
-
headLimit > 0
|
|
1125
|
-
&& (
|
|
1126
|
-
typeof options?.__runRg !== 'function'
|
|
1127
|
-
|| typeof options?.__tryServeFuzzySearch === 'function'
|
|
1128
|
-
)
|
|
1129
|
-
) {
|
|
642
|
+
{
|
|
1130
643
|
const nativeStartedAt = performance.now();
|
|
1131
644
|
try {
|
|
1132
645
|
const served = await nativeFuzzyImpl({
|
|
1133
646
|
query,
|
|
1134
647
|
cwd: fullPath,
|
|
1135
|
-
limit: headLimit + 1,
|
|
648
|
+
limit: headLimit > 0 ? headLimit + 1 : 1_000,
|
|
1136
649
|
hidden,
|
|
1137
650
|
includeNoise,
|
|
1138
651
|
maxDepth: depth,
|
|
@@ -1151,11 +664,11 @@ async function runFuzzyFindPass(args, workDir, options = {}) {
|
|
|
1151
664
|
if (served?.complete) {
|
|
1152
665
|
recordNativeSearchTiming(served);
|
|
1153
666
|
recordLocalSearchBackend('native_fuzzy', performance.now() - nativeStartedAt, 'hit');
|
|
1154
|
-
const hasMore = served.hasMore || served.matches.length > headLimit;
|
|
1155
|
-
const matches = hasMore ? served.matches.slice(0, headLimit) : served.matches;
|
|
667
|
+
const hasMore = served.hasMore || (headLimit > 0 && served.matches.length > headLimit);
|
|
668
|
+
const matches = headLimit > 0 && hasMore ? served.matches.slice(0, headLimit) : served.matches;
|
|
1156
669
|
const lines = matches.length === 0
|
|
1157
670
|
? [`(no fuzzy match for "${query}")`]
|
|
1158
|
-
: [...matches, ...(hasMore ? [`... (top ${headLimit}; raise limit for more)`] : [])];
|
|
671
|
+
: [...matches, ...(hasMore ? [`... (top ${headLimit || 1_000}; narrow query or raise limit for more)`] : [])];
|
|
1159
672
|
const result = lines.join('\n');
|
|
1160
673
|
if (served.cacheSafe !== false) {
|
|
1161
674
|
cacheSet(cacheKey, result, { scopes: [fullPath] });
|
|
@@ -1181,6 +694,7 @@ async function runFuzzyFindPass(args, workDir, options = {}) {
|
|
|
1181
694
|
nativeEmptyPartial = served;
|
|
1182
695
|
} else {
|
|
1183
696
|
recordLocalSearchBackend('native_fuzzy', performance.now() - nativeStartedAt, 'miss');
|
|
697
|
+
return capFindResult('Error: native fuzzy search returned an invalid response.');
|
|
1184
698
|
}
|
|
1185
699
|
} catch (error) {
|
|
1186
700
|
if (options.signal?.aborted) throw error;
|
|
@@ -1205,197 +719,24 @@ async function runFuzzyFindPass(args, workDir, options = {}) {
|
|
|
1205
719
|
};
|
|
1206
720
|
} else {
|
|
1207
721
|
recordLocalSearchBackend('native_fuzzy', performance.now() - nativeStartedAt, 'error');
|
|
1208
|
-
|
|
1209
|
-
return capFindResult(`Error: ${normalizeErrorMessage(error instanceof Error ? error.message : String(error))}`);
|
|
1210
|
-
}
|
|
722
|
+
return capFindResult(`Error: ${normalizeErrorMessage(error instanceof Error ? error.message : String(error))}`);
|
|
1211
723
|
}
|
|
1212
724
|
}
|
|
1213
725
|
}
|
|
1214
726
|
if (nativeEmptyPartial) {
|
|
1215
|
-
const targetedStartedAt = performance.now();
|
|
1216
|
-
let targeted;
|
|
1217
|
-
try {
|
|
1218
|
-
targeted = await getTargetedFindEnumeration({
|
|
1219
|
-
queries: queryTokens.length > 0 ? queryTokens : [query],
|
|
1220
|
-
root: fullPath,
|
|
1221
|
-
hidden,
|
|
1222
|
-
depth,
|
|
1223
|
-
includeNoise,
|
|
1224
|
-
runRgImpl,
|
|
1225
|
-
signal: options.signal,
|
|
1226
|
-
timeoutMs: FIND_TARGETED_TIMEOUT_MS,
|
|
1227
|
-
});
|
|
1228
|
-
} catch (error) {
|
|
1229
|
-
if (options.signal?.aborted) throw findEnumerationAbortError(options.signal);
|
|
1230
|
-
recordLocalSearchBackend('native_fuzzy_targeted', performance.now() - targetedStartedAt, 'error');
|
|
1231
|
-
if (options?.scopedCacheOutcome) markScopedCacheIncomplete(options.scopedCacheOutcome);
|
|
1232
|
-
return capFindResult([
|
|
1233
|
-
`(no fuzzy match yet for "${query}")`,
|
|
1234
|
-
'... [native fuzzy ranking was partial and the targeted path scan could not complete; narrow path and retry]',
|
|
1235
|
-
].join('\n'));
|
|
1236
|
-
}
|
|
1237
|
-
const queryTokensLower = queryTokens.map((token) => token.toLowerCase());
|
|
1238
|
-
const targetedPaths = targeted.files.filter((path) => {
|
|
1239
|
-
const lower = path.toLowerCase();
|
|
1240
|
-
return queryTokensLower.every((token) => lower.includes(token));
|
|
1241
|
-
});
|
|
1242
|
-
const rankLimit = headLimit > 0 ? headLimit + 1 : headLimit;
|
|
1243
|
-
const rankedRaw = fuzzyRankMulti(query, prepareFuzzyItems(targetedPaths), rankLimit);
|
|
1244
|
-
const hasMore = headLimit > 0 && rankedRaw.length > headLimit;
|
|
1245
|
-
const ranked = hasMore ? rankedRaw.slice(0, headLimit) : rankedRaw;
|
|
1246
|
-
recordLocalSearchBackend(
|
|
1247
|
-
'native_fuzzy_targeted',
|
|
1248
|
-
performance.now() - targetedStartedAt,
|
|
1249
|
-
ranked.length > 0 ? 'hit' : 'miss',
|
|
1250
|
-
);
|
|
1251
727
|
if (options?.scopedCacheOutcome) markScopedCacheIncomplete(options.scopedCacheOutcome);
|
|
1252
|
-
const
|
|
1253
|
-
?
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
if (typeof options?.onProgress === 'function') {
|
|
1265
|
-
try { options.onProgress(`${ranked.length} targeted candidates`); } catch {}
|
|
1266
|
-
}
|
|
1267
|
-
return capFindResult(lines.join('\n'));
|
|
1268
|
-
}
|
|
1269
|
-
// Common discovery respects .gitignore even outside a Git repository.
|
|
1270
|
-
// include_noise deliberately retains the old hardened --no-ignore behavior.
|
|
1271
|
-
// Noise dirs stay excluded via DEFAULT_IGNORE_GLOBS below.
|
|
1272
|
-
const baseRgArgs = ['--files', includeNoise ? '--no-ignore' : '--no-require-git'];
|
|
1273
|
-
if (hidden) baseRgArgs.push('--hidden');
|
|
1274
|
-
if (depth != null) baseRgArgs.push('--max-depth', String(depth));
|
|
1275
|
-
// Noise-exclusion globs are kept SEPARATE and always appended LAST (after
|
|
1276
|
-
// any positive --iglob). ripgrep's "last matching glob wins" rule means a
|
|
1277
|
-
// positive include placed after these negations would re-admit e.g.
|
|
1278
|
-
// `.git/<query>` — so the exclusions must trail the narrowed include.
|
|
1279
|
-
const ignoreGlobs = [];
|
|
1280
|
-
if (!includeNoise) {
|
|
1281
|
-
for (const ex of DEFAULT_IGNORE_GLOBS) ignoreGlobs.push('--glob', ex);
|
|
1282
|
-
}
|
|
1283
|
-
for (const ex of rootScanIgnoreGlobs(normalizeOutputPath(fullPath))) ignoreGlobs.push('--glob', ex);
|
|
1284
|
-
// The narrowed pass must treat `query` as a LITERAL filename substring, not
|
|
1285
|
-
// a glob. Wrap every globset metacharacter in a single-char character class
|
|
1286
|
-
// (`[` → `[[]`, `*` → `[*]`, …): character-class quoting is the only form
|
|
1287
|
-
// globset honors on Windows, where a backslash-escape (`\*`) is read as a
|
|
1288
|
-
// literal path separator and does NOT escape. So a query like "[slug].tsx"
|
|
1289
|
-
// still produces the intended `*[[]slug[]].tsx*` include instead of a
|
|
1290
|
-
// character-class that matches one of s/l/u/g.
|
|
1291
|
-
// Test-only seam: allow a caller to inject a runRg stand-in (e.g. to
|
|
1292
|
-
// simulate a truncated broad pass) without touching the production path.
|
|
1293
|
-
// Never set on the real tool-execution options object.
|
|
1294
|
-
const parseRgFiles = (stdout) => String(stdout)
|
|
1295
|
-
.split('\n')
|
|
1296
|
-
// Strip only the trailing CR from rg's line split — do NOT trim, or a
|
|
1297
|
-
// filename with leading/trailing spaces would be corrupted.
|
|
1298
|
-
.map((p) => (p.endsWith('\r') ? p.slice(0, -1) : p))
|
|
1299
|
-
.filter((p) => p.length > 0)
|
|
1300
|
-
.map((p) => normalizeOutputPath(p.replace(/^\.[/\\]/, '')));
|
|
1301
|
-
// Broad enumeration: every file under the scope, ranked by fuzzy score.
|
|
1302
|
-
// Subject to rg's 20MB/20s cap — an exact-name hit deep in a huge tree can
|
|
1303
|
-
// be dropped by cap lottery, so it is backstopped by the narrowed pass.
|
|
1304
|
-
// Shared across queries/concurrent callers via the broad-enumeration cache
|
|
1305
|
-
// (keyed on root+hidden+depth+includeNoise, i.e. exactly this pass's args).
|
|
1306
|
-
let broadEnum;
|
|
1307
|
-
try {
|
|
1308
|
-
broadEnum = await getBroadEnumeration({
|
|
1309
|
-
root: normalizeOutputPath(fullPath),
|
|
1310
|
-
hidden, depth, includeNoise, ignoreMode: includeNoise ? 'all' : 'git',
|
|
1311
|
-
rgArgs: [...baseRgArgs, ...ignoreGlobs, '.'],
|
|
1312
|
-
cwd: fullPath,
|
|
1313
|
-
runRgImpl,
|
|
1314
|
-
signal: options.signal,
|
|
1315
|
-
});
|
|
1316
|
-
} catch (err) {
|
|
1317
|
-
return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`;
|
|
1318
|
-
}
|
|
1319
|
-
let rgTruncated = broadEnum.truncated;
|
|
1320
|
-
let rgPartial = broadEnum.partial;
|
|
1321
|
-
const passOneItems = broadEnum.items || prepareFuzzyItems(broadEnum.files);
|
|
1322
|
-
const queryTokensLower = queryTokens.map((t) => t.toLowerCase());
|
|
1323
|
-
const rankLimit = headLimit > 0 ? headLimit + 1 : headLimit;
|
|
1324
|
-
const passOneRanked = fuzzyRankMulti(query, passOneItems, rankLimit);
|
|
1325
|
-
const passOneHasCandidate = passOneRanked.length > 0;
|
|
1326
|
-
let targetedProbeRan = false;
|
|
1327
|
-
let targetedPaths = [];
|
|
1328
|
-
// A full --no-ignore enumeration can exceed 20MB, then trigger one more
|
|
1329
|
-
// whole-tree narrowed pass PER query. Probe only requested names instead,
|
|
1330
|
-
// unioning every query in this find batch into one rg process.
|
|
1331
|
-
if (rgTruncated || rgPartial || !passOneHasCandidate) {
|
|
1332
|
-
try {
|
|
1333
|
-
const targeted = await getTargetedFindEnumeration({
|
|
1334
|
-
queries: queryTokens.length > 0 ? queryTokens : [query],
|
|
1335
|
-
root: fullPath,
|
|
1336
|
-
hidden,
|
|
1337
|
-
depth,
|
|
1338
|
-
includeNoise,
|
|
1339
|
-
runRgImpl,
|
|
1340
|
-
signal: options.signal,
|
|
1341
|
-
});
|
|
1342
|
-
rgTruncated ||= targeted.truncated;
|
|
1343
|
-
rgPartial ||= targeted.partial;
|
|
1344
|
-
// Every fragment must appear in the path (AND) — a single-fragment
|
|
1345
|
-
// query keeps the prior substring behavior.
|
|
1346
|
-
targetedPaths = targeted.files.filter((path) => {
|
|
1347
|
-
const lower = path.toLowerCase();
|
|
1348
|
-
return queryTokensLower.every((t) => lower.includes(t));
|
|
1349
|
-
});
|
|
1350
|
-
targetedProbeRan = true;
|
|
1351
|
-
} catch {
|
|
1352
|
-
if (options.signal?.aborted) throw findEnumerationAbortError(options.signal);
|
|
1353
|
-
rgPartial = true;
|
|
1354
|
-
}
|
|
1355
|
-
}
|
|
1356
|
-
// Merge common + targeted results, preserving common-path order.
|
|
1357
|
-
const seen = new Set();
|
|
1358
|
-
const items = [];
|
|
1359
|
-
for (const item of passOneItems) {
|
|
1360
|
-
const p = item.path;
|
|
1361
|
-
if (seen.has(p)) continue;
|
|
1362
|
-
seen.add(p);
|
|
1363
|
-
items.push(item);
|
|
1364
|
-
}
|
|
1365
|
-
for (const item of prepareFuzzyItems(targetedPaths)) {
|
|
1366
|
-
const p = item.path;
|
|
1367
|
-
if (seen.has(p)) continue;
|
|
1368
|
-
seen.add(p);
|
|
1369
|
-
items.push(item);
|
|
1370
|
-
}
|
|
1371
|
-
const rankedRaw = targetedPaths.length > 0
|
|
1372
|
-
? fuzzyRankMulti(query, items, rankLimit)
|
|
1373
|
-
: passOneRanked;
|
|
1374
|
-
const hasMore = headLimit > 0 && rankedRaw.length > headLimit;
|
|
1375
|
-
const ranked = hasMore ? rankedRaw.slice(0, headLimit) : rankedRaw;
|
|
1376
|
-
// Build output lines uniformly for the hit and no-match cases so a
|
|
1377
|
-
// truncated/partial broad pass ALWAYS surfaces its warning — otherwise a
|
|
1378
|
-
// cut-off enumeration that happened to drop the sole match would silently
|
|
1379
|
-
// report "(no fuzzy match …)" as if the tree were exhaustively searched.
|
|
1380
|
-
const noMatch = ranked.length === 0;
|
|
1381
|
-
const lines = noMatch ? [`(no fuzzy match for "${query}")`] : ranked.map((r) => r.item.path);
|
|
1382
|
-
if (!noMatch && hasMore) lines.push(`... (top ${headLimit}; raise limit for more)`);
|
|
1383
|
-
if (rgTruncated) lines.push('... [warning] rg stdout truncated at 20MB cap; broad ranking incomplete (exact-name hits still merged)');
|
|
1384
|
-
if (rgPartial && !rgTruncated) lines.push('... [warning] rg exit 2 (partial results); broad ranking may be incomplete');
|
|
1385
|
-
if (!targetedProbeRan && headLimit > 0 && passOneRanked.length >= headLimit) {
|
|
1386
|
-
lines.push('[gitignored trees not searched; include_noise:true is available only if those trees are required]');
|
|
1387
|
-
}
|
|
1388
|
-
const result = lines.join('\n');
|
|
1389
|
-
// Do not cache a truncated/partial enumeration — the broad ranking is
|
|
1390
|
-
// known-incomplete, so a later call with a larger head_limit must re-run.
|
|
1391
|
-
// A no-match result is also left uncached (mirrors the prior early return).
|
|
1392
|
-
if (!noMatch && !rgTruncated && !rgPartial) {
|
|
1393
|
-
cacheSet(cacheKey, result, { scopes: [fullPath] });
|
|
1394
|
-
}
|
|
1395
|
-
if (typeof options?.onProgress === 'function') {
|
|
1396
|
-
try { options.onProgress(`${ranked.length} candidates`); } catch { /* best-effort */ }
|
|
1397
|
-
}
|
|
1398
|
-
return capFindResult(result);
|
|
728
|
+
const scanErrorNote = nativeEmptyPartial.scanErrors > 0
|
|
729
|
+
? `; ${nativeEmptyPartial.scanErrors} path(s) could not be enumerated`
|
|
730
|
+
+ (nativeEmptyPartial.walkErrorDetails?.length
|
|
731
|
+
? `: ${nativeEmptyPartial.walkErrorDetails.join('; ')}`
|
|
732
|
+
: '')
|
|
733
|
+
: '';
|
|
734
|
+
return capFindResult([
|
|
735
|
+
`(no fuzzy match yet for "${query}")`,
|
|
736
|
+
`... [native inventory is still building${scanErrorNote}; retry the same scope for complete results]`,
|
|
737
|
+
].join('\n'));
|
|
738
|
+
}
|
|
739
|
+
return capFindResult('Error: native fuzzy search did not return a result.');
|
|
1399
740
|
}
|
|
1400
741
|
|
|
1401
742
|
export async function executeFindFilesTool(args, workDir, options = {}) {
|