mixdog 0.9.36 → 0.9.37
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 +3 -2
- package/scripts/abort-recovery-test.mjs +118 -0
- package/scripts/compact-smoke.mjs +7 -7
- package/scripts/explore-bench-tmp.mjs +19 -0
- package/scripts/explore-bench.mjs +36 -6
- package/scripts/explore-prompt-policy-test.mjs +27 -0
- package/scripts/ingest-pure-conversation-smoke.mjs +11 -11
- package/scripts/openai-ws-early-settle-test.mjs +176 -0
- package/scripts/output-style-smoke.mjs +17 -17
- package/scripts/provider-toolcall-test.mjs +333 -14
- package/scripts/recall-bench-cases.json +3 -3
- package/scripts/recall-bench.mjs +1 -1
- package/scripts/recall-quality-cases.json +4 -4
- package/scripts/recall-usecase-cases.json +2 -2
- package/scripts/session-bench.mjs +13 -13
- package/scripts/steering-drain-buckets-test.mjs +72 -11
- package/scripts/submit-commandbusy-race-test.mjs +114 -0
- package/scripts/tool-smoke.mjs +83 -10
- package/src/app.mjs +2 -1
- package/src/defaults/cycle3-review-prompt.md +9 -0
- package/src/defaults/skills/setup/SKILL.md +93 -293
- package/src/lib/rules-builder.cjs +3 -2
- package/src/output-styles/default.md +2 -2
- package/src/output-styles/extreme-minimal.md +1 -1
- package/src/output-styles/minimal.md +1 -1
- package/src/output-styles/simple.md +2 -3
- package/src/rules/agent/30-explorer.md +15 -7
- package/src/rules/lead/01-general.md +4 -0
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +15 -3
- package/src/runtime/agent/orchestrator/config.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +35 -5
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +27 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +36 -37
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +72 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +26 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +12 -28
- package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +18 -15
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +4 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +113 -2
- package/src/runtime/agent/orchestrator/providers/registry.mjs +44 -5
- package/src/runtime/agent/orchestrator/providers/tool-stream-state.mjs +78 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +57 -31
- package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +8 -6
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +28 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +1 -3
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +15 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +26 -17
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +24 -16
- package/src/runtime/agent/orchestrator/session/result-classification.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +5 -2
- package/src/runtime/agent/orchestrator/stall-policy.mjs +18 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +35 -20
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -7
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +77 -31
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +25 -3
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +9 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +73 -25
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +17 -7
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +14 -12
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +56 -6
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -0
- package/src/runtime/channels/lib/worker-main.mjs +5 -0
- package/src/runtime/memory/lib/core-memory-store.mjs +104 -0
- package/src/runtime/memory/lib/ko-morph.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle3.mjs +64 -4
- package/src/runtime/memory/lib/memory-text-utils.mjs +4 -4
- package/src/runtime/memory/lib/recall-format.mjs +3 -3
- package/src/runtime/shared/child-spawn-gate.mjs +15 -0
- package/src/runtime/shared/config.mjs +98 -12
- package/src/runtime/shared/process-listener-headroom.mjs +12 -0
- package/src/session-runtime/cwd-plugins.mjs +6 -3
- package/src/session-runtime/model-route-api.mjs +50 -2
- package/src/session-runtime/provider-auth-api.mjs +8 -1
- package/src/session-runtime/resource-api.mjs +22 -0
- package/src/session-runtime/runtime-core.mjs +13 -2
- package/src/session-runtime/settings-api.mjs +11 -2
- package/src/standalone/agent-tool.mjs +15 -9
- package/src/standalone/explore-tool.mjs +4 -1
- package/src/tui/App.jsx +6 -5
- package/src/tui/app/transcript-window.mjs +60 -10
- package/src/tui/app/use-transcript-window.mjs +2 -1
- package/src/tui/components/ContextPanel.jsx +4 -1
- package/src/tui/components/ToolExecution.jsx +15 -12
- package/src/tui/components/TranscriptItem.jsx +1 -1
- package/src/tui/components/tool-execution/surface-detail.mjs +8 -8
- package/src/tui/dist/index.mjs +467 -151
- package/src/tui/engine/agent-job-feed.mjs +26 -6
- package/src/tui/engine/notification-plan.mjs +3 -3
- package/src/tui/engine/render-timing.mjs +104 -8
- package/src/tui/engine/session-api.mjs +58 -3
- package/src/tui/engine/session-flow.mjs +78 -28
- package/src/tui/engine/tool-card-results.mjs +28 -13
- package/src/tui/engine/turn.mjs +232 -156
- package/src/tui/engine.mjs +17 -8
- package/src/tui/index.jsx +10 -1
- package/src/workflows/default/WORKFLOW.md +8 -4
- package/src/workflows/solo/WORKFLOW.md +8 -4
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
import { formatMtime, formatListSize } from './list-formatting.mjs';
|
|
33
33
|
import { TOOL_OUTPUT_MAX_BYTES } from './tool-output-limit.mjs';
|
|
34
34
|
import { runRg } from './rg-runner.mjs';
|
|
35
|
+
import { hasSpareCapacity as childSpawnHasSpareCapacity } from '../../../../shared/child-spawn-gate.mjs';
|
|
35
36
|
import { fuzzyRank } from './fuzzy-match.mjs';
|
|
36
37
|
import { assertPathReachable } from './fs-reachability.mjs';
|
|
37
38
|
|
|
@@ -84,16 +85,30 @@ export async function executeListTool(args, workDir, options = {}) {
|
|
|
84
85
|
const capped = list.length > 10;
|
|
85
86
|
const targets = capped ? list.slice(0, 10) : list;
|
|
86
87
|
if (targets.length > 1) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
88
|
+
// Bounded parallel fan-out: each target is an independent listing
|
|
89
|
+
// (own guard/cache/reachability), so run them concurrently instead
|
|
90
|
+
// of serially. Bodies land in a fixed-index array so the emitted
|
|
91
|
+
// section order still matches the caller's `path[]` order — only the
|
|
92
|
+
// wall-clock cost changes. Concurrency is capped so a 10-path batch
|
|
93
|
+
// cannot exhaust the child-spawn / FS-handle budget.
|
|
94
|
+
const LIST_FANOUT_CONCURRENCY = 4;
|
|
95
|
+
const bodies = new Array(targets.length);
|
|
96
|
+
let cursor = 0;
|
|
97
|
+
const runWorker = async () => {
|
|
98
|
+
for (;;) {
|
|
99
|
+
const i = cursor++;
|
|
100
|
+
if (i >= targets.length) return;
|
|
101
|
+
try {
|
|
102
|
+
bodies[i] = await executeListTool({ ...args, path: targets[i] }, workDir, options);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
bodies[i] = `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`;
|
|
105
|
+
}
|
|
94
106
|
}
|
|
95
|
-
|
|
96
|
-
|
|
107
|
+
};
|
|
108
|
+
await Promise.all(
|
|
109
|
+
Array.from({ length: Math.min(LIST_FANOUT_CONCURRENCY, targets.length) }, runWorker),
|
|
110
|
+
);
|
|
111
|
+
const sections = targets.map((p, i) => `# list ${p}\n${bodies[i]}`);
|
|
97
112
|
if (capped) sections.push(`... [capped at 10 of ${list.length} paths]`);
|
|
98
113
|
return sections.join('\n\n');
|
|
99
114
|
}
|
|
@@ -349,8 +364,9 @@ export async function executeTreeTool(args, workDir, options = {}) {
|
|
|
349
364
|
// key with in-flight promise dedup (N concurrent callers share ONE sweep)
|
|
350
365
|
// plus a short TTL for serial reuse. Truncated/partial sweeps are
|
|
351
366
|
// known-incomplete and are NEVER cached.
|
|
352
|
-
const FIND_ENUM_CACHE = new Map(); // key -> { files, expiresAt }
|
|
367
|
+
const FIND_ENUM_CACHE = new Map(); // key -> { files, expiresAt, gen }
|
|
353
368
|
const FIND_ENUM_INFLIGHT = new Map(); // key -> Promise<{files,truncated,partial}>
|
|
369
|
+
let FIND_ENUM_GEN = 0;
|
|
354
370
|
|
|
355
371
|
// The broad enumeration is a DERIVED cache the scope/path invalidation layer
|
|
356
372
|
// does not otherwise know about — a file created/renamed after a sweep would
|
|
@@ -358,6 +374,7 @@ const FIND_ENUM_INFLIGHT = new Map(); // key -> Promise<{files,truncated,partial
|
|
|
358
374
|
// write-invalidation event (TTL remains the secondary bound). Full clear is
|
|
359
375
|
// fine: entries are cheap to rebuild.
|
|
360
376
|
registerCacheInvalidationListener(() => {
|
|
377
|
+
FIND_ENUM_GEN += 1;
|
|
361
378
|
FIND_ENUM_CACHE.clear();
|
|
362
379
|
FIND_ENUM_INFLIGHT.clear();
|
|
363
380
|
});
|
|
@@ -390,18 +407,31 @@ function parseRgFileList(stdout) {
|
|
|
390
407
|
// must treat it as read-only. `rgArgs` must be the broad-pass args (no per-query
|
|
391
408
|
// narrowing); the cache key is the 4 dims only, so any caller producing an
|
|
392
409
|
// equivalent sweep for the same dims reuses the result.
|
|
393
|
-
async function getBroadEnumeration({ root, hidden, depth, includeNoise, rgArgs, cwd, runRgImpl = runRg }) {
|
|
410
|
+
async function getBroadEnumeration({ root, hidden, depth, includeNoise, rgArgs, cwd, runRgImpl = runRg, bestEffort = false }) {
|
|
394
411
|
const ttl = findEnumTtlMs();
|
|
395
412
|
const key = findEnumKey({ root, hidden, depth, includeNoise });
|
|
396
413
|
if (ttl > 0) {
|
|
397
414
|
const hit = FIND_ENUM_CACHE.get(key);
|
|
398
|
-
if (hit && hit.expiresAt > Date.now()) {
|
|
415
|
+
if (hit && hit.gen === FIND_ENUM_GEN && hit.expiresAt > Date.now()) {
|
|
399
416
|
return { files: hit.files, truncated: false, partial: false };
|
|
400
417
|
}
|
|
401
418
|
if (hit) FIND_ENUM_CACHE.delete(key); // expired
|
|
402
|
-
const inflight = FIND_ENUM_INFLIGHT.get(key);
|
|
403
|
-
if (inflight) return inflight;
|
|
404
419
|
}
|
|
420
|
+
// Single-flight is independent of the persistent TTL cache. Even when
|
|
421
|
+
// MIXDOG_FIND_ENUM_CACHE_TTL_MS=0 disables reuse across calls, concurrent
|
|
422
|
+
// query[] fan-out should still share the one broad `rg --files` sweep
|
|
423
|
+
// instead of spawning N identical enumerations.
|
|
424
|
+
const inflight = FIND_ENUM_INFLIGHT.get(key);
|
|
425
|
+
if (inflight) return inflight;
|
|
426
|
+
// Non-competing prewarm: past the cache/single-flight fast paths this runs
|
|
427
|
+
// a fresh `rg` sweep (a child-spawn slot). Best-effort warmers skip that
|
|
428
|
+
// spawn when the gate has no spare capacity, returning a known-incomplete
|
|
429
|
+
// result — and skipping BEFORE registering FIND_ENUM_INFLIGHT so a real
|
|
430
|
+
// caller is never attached to (or cache-poisoned by) a skipped warm.
|
|
431
|
+
if (bestEffort && !childSpawnHasSpareCapacity()) {
|
|
432
|
+
return { files: [], truncated: false, partial: true };
|
|
433
|
+
}
|
|
434
|
+
const genAtStart = FIND_ENUM_GEN;
|
|
405
435
|
const run = (async () => {
|
|
406
436
|
const stdout = await runRgImpl(rgArgs, { cwd });
|
|
407
437
|
const truncated = Boolean(stdout && typeof stdout === 'object' && stdout.truncated);
|
|
@@ -409,17 +439,18 @@ async function getBroadEnumeration({ root, hidden, depth, includeNoise, rgArgs,
|
|
|
409
439
|
const files = parseRgFileList(stdout);
|
|
410
440
|
// Never cache a truncated/partial sweep — it is known-incomplete, so a
|
|
411
441
|
// later query with a larger head_limit must re-run the enumeration.
|
|
412
|
-
|
|
413
|
-
|
|
442
|
+
// Also never let an in-flight prewarm/real sweep repopulate after a
|
|
443
|
+
// write invalidation cleared the cache during the sweep.
|
|
444
|
+
if (ttl > 0 && !truncated && !partial && FIND_ENUM_GEN === genAtStart) {
|
|
445
|
+
FIND_ENUM_CACHE.set(key, { files, expiresAt: Date.now() + ttl, gen: genAtStart });
|
|
414
446
|
}
|
|
415
447
|
return { files, truncated, partial };
|
|
416
448
|
})();
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
449
|
+
FIND_ENUM_INFLIGHT.set(key, run);
|
|
450
|
+
try { return await run; }
|
|
451
|
+
finally {
|
|
452
|
+
if (FIND_ENUM_INFLIGHT.get(key) === run) FIND_ENUM_INFLIGHT.delete(key);
|
|
421
453
|
}
|
|
422
|
-
return run;
|
|
423
454
|
}
|
|
424
455
|
|
|
425
456
|
// Best-effort warm of the broad enumeration for a root using the `find` tool's
|
|
@@ -435,7 +466,7 @@ export async function prewarmFindEnumeration(root) {
|
|
|
435
466
|
await getBroadEnumeration({
|
|
436
467
|
root: normalizeOutputPath(root),
|
|
437
468
|
hidden, depth, includeNoise,
|
|
438
|
-
rgArgs, cwd: root,
|
|
469
|
+
rgArgs, cwd: root, bestEffort: true,
|
|
439
470
|
});
|
|
440
471
|
} catch { /* best-effort warm; never surface */ }
|
|
441
472
|
}
|
|
@@ -450,16 +481,31 @@ export async function executeFuzzyFindTool(args, workDir, options = {}) {
|
|
|
450
481
|
const capped = list.length > 5;
|
|
451
482
|
const targets = capped ? list.slice(0, 5) : list;
|
|
452
483
|
if (targets.length > 1) {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
484
|
+
// Bounded parallel fan-out: every query shares ONE broad `rg --files`
|
|
485
|
+
// sweep via the enumeration cache's in-flight single-flight dedup, so
|
|
486
|
+
// running them concurrently collapses N broad sweeps into one and
|
|
487
|
+
// overlaps the tiny per-query narrowed passes. Bodies land at a fixed
|
|
488
|
+
// index so the emitted section order still matches the caller's
|
|
489
|
+
// query[] order — only wall-clock cost changes. Concurrency is capped
|
|
490
|
+
// so a 5-query batch cannot exhaust the child-spawn budget.
|
|
491
|
+
const FIND_FANOUT_CONCURRENCY = 4;
|
|
492
|
+
const bodies = new Array(targets.length);
|
|
493
|
+
let cursor = 0;
|
|
494
|
+
const runWorker = async () => {
|
|
495
|
+
for (;;) {
|
|
496
|
+
const i = cursor++;
|
|
497
|
+
if (i >= targets.length) return;
|
|
498
|
+
try {
|
|
499
|
+
bodies[i] = await executeFuzzyFindTool({ ...args, query: targets[i] }, workDir, options);
|
|
500
|
+
} catch (err) {
|
|
501
|
+
bodies[i] = `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`;
|
|
502
|
+
}
|
|
460
503
|
}
|
|
461
|
-
|
|
462
|
-
|
|
504
|
+
};
|
|
505
|
+
await Promise.all(
|
|
506
|
+
Array.from({ length: Math.min(FIND_FANOUT_CONCURRENCY, targets.length) }, runWorker),
|
|
507
|
+
);
|
|
508
|
+
const sections = targets.map((q, i) => `# find ${q}\n${bodies[i]}`);
|
|
463
509
|
if (capped) sections.push(`... [capped at 5 of ${list.length} queries]`);
|
|
464
510
|
return sections.join('\n\n');
|
|
465
511
|
}
|
|
@@ -24,11 +24,20 @@ function _stripLineCoordForReach(s) {
|
|
|
24
24
|
.replace(/:\d+(?:-\d+)?(?::.*)?$/, '');
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
// Batch fan-out cap: array/object read shapes read only the first N entries
|
|
28
|
+
// (the >N slice below truncates the rest with a `_batchCapNote`). The
|
|
29
|
+
// reachability preflight must honour the SAME cap so a path that will be
|
|
30
|
+
// capped away is never stat-probed and can never reject the batch via a guard.
|
|
31
|
+
const READ_BATCH_PATH_CAP = 10;
|
|
32
|
+
|
|
27
33
|
function _collectReachCandidates(p) {
|
|
28
34
|
const out = [];
|
|
29
35
|
const push = (s) => { if (typeof s === 'string' && s) out.push(s); };
|
|
30
36
|
if (typeof p === 'string') push(p);
|
|
31
|
-
|
|
37
|
+
// Only the first READ_BATCH_PATH_CAP entries survive the fan-out cap, so
|
|
38
|
+
// preflight only the paths that will actually be read. Capped-away paths
|
|
39
|
+
// (incl. UNC/device) must NOT be probed or allowed to fail the batch.
|
|
40
|
+
else if (Array.isArray(p)) for (const e of p.slice(0, READ_BATCH_PATH_CAP)) push((e && typeof e === 'object') ? (e.path ?? e.file_path) : e);
|
|
32
41
|
return out;
|
|
33
42
|
}
|
|
34
43
|
|
|
@@ -66,11 +75,17 @@ async function _readReachPreflight(rawPath, workDir, helpers) {
|
|
|
66
75
|
// line-coordinate strip only needs to land in the right directory — exact
|
|
67
76
|
// suffix parsing is not required for the stat to be representative.
|
|
68
77
|
const candidates = [];
|
|
78
|
+
const seenFull = new Set();
|
|
69
79
|
for (const raw of _collectReachCandidates(rawPath)) {
|
|
70
80
|
const stripped = _stripLineCoordForReach(normalizeInputPath(raw));
|
|
71
81
|
const full = resolveAgainstCwd(stripped, workDir);
|
|
72
82
|
const guardMsg = _guardedReadError(stripped, helpers) || _guardedReadError(full, helpers);
|
|
73
83
|
if (guardMsg) return guardMsg;
|
|
84
|
+
// Dedup by resolved path so a batch repeating the same file (or the
|
|
85
|
+
// same union window) issues one stat probe, not one per entry —
|
|
86
|
+
// bounding the preflight's FS work to the distinct target set.
|
|
87
|
+
if (seenFull.has(full)) continue;
|
|
88
|
+
seenFull.add(full);
|
|
74
89
|
candidates.push(full);
|
|
75
90
|
}
|
|
76
91
|
if (candidates.length === 0) return null;
|
|
@@ -113,7 +128,14 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
|
|
|
113
128
|
args.path = coerceReadFamilyPathArg(args.path, workDir);
|
|
114
129
|
// Reachability preflight up front (all shapes) — before readPathStringGuardError /
|
|
115
130
|
// image stat, all of which can touch sync FS.
|
|
116
|
-
|
|
131
|
+
// options._skipReachPreflight: set only by the batch dispatcher on its
|
|
132
|
+
// child reads (below). The parent batch call already ran this exact
|
|
133
|
+
// preflight over EVERY candidate path in the array (_collectReachCandidates
|
|
134
|
+
// covers array/object shapes), so re-running it per child re-stats the same
|
|
135
|
+
// mounts N times. The UNC/device/ADS string guards still run inside the
|
|
136
|
+
// child (readPathStringGuardError / image fast-path) — only the async
|
|
137
|
+
// reachability stat is skipped, never the security guards.
|
|
138
|
+
if (options?._skipReachPreflight !== true) {
|
|
117
139
|
const _reErr = await _readReachPreflight(args.path, workDir, helpers);
|
|
118
140
|
if (_reErr) return _reErr;
|
|
119
141
|
}
|
|
@@ -289,7 +311,7 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
|
|
|
289
311
|
// document/image content-block object that would stringify to
|
|
290
312
|
// "[object Object]" and drop the payload. Scalar reads (the
|
|
291
313
|
// single-file path below) keep the rich block shapes.
|
|
292
|
-
const body = await executeChildBuiltinTool('read', readEntry, workDir, { suppressReadUnchangedStub: true, mediaTextOnly: true });
|
|
314
|
+
const body = await executeChildBuiltinTool('read', readEntry, workDir, { suppressReadUnchangedStub: true, mediaTextOnly: true, _skipReachPreflight: true });
|
|
293
315
|
results[index] = { path: entry.path, mode: entry.mode || 'full', n: entry.n, body };
|
|
294
316
|
};
|
|
295
317
|
const key = entry.path || `#missing-${index}`;
|
|
@@ -31,6 +31,7 @@ export function buildGrepCacheKey(parts) {
|
|
|
31
31
|
onlyMatching,
|
|
32
32
|
pcre2,
|
|
33
33
|
withFilename,
|
|
34
|
+
patternCapTotal = 0,
|
|
34
35
|
} = parts;
|
|
35
36
|
return [
|
|
36
37
|
'grep',
|
|
@@ -50,6 +51,10 @@ export function buildGrepCacheKey(parts) {
|
|
|
50
51
|
Array.isArray(fileType) ? fileType.join('\x01') : (fileType || ''),
|
|
51
52
|
pcre2 ? 'p1' : 'p0',
|
|
52
53
|
withFilename ? 'H1' : 'H0',
|
|
54
|
+
// Cap total keeps a capped request (first-N of M patterns, carrying the
|
|
55
|
+
// "[capped at N of M]" notice) from colliding with an exact N-pattern
|
|
56
|
+
// request or with a differently-capped one (of 15 vs of 20).
|
|
57
|
+
'pc' + String(patternCapTotal || 0),
|
|
53
58
|
].join('|');
|
|
54
59
|
}
|
|
55
60
|
|
|
@@ -126,13 +131,15 @@ export function buildGrepRgArgs(parts) {
|
|
|
126
131
|
return rgArgs;
|
|
127
132
|
}
|
|
128
133
|
|
|
129
|
-
export function buildGlobCacheKey({ patterns, basePath, headLimit, offset, extraIgnore, sort }) {
|
|
134
|
+
export function buildGlobCacheKey({ patterns, basePath, headLimit, offset, extraIgnore, sort, patternCapTotal = 0 }) {
|
|
130
135
|
// extraIgnore (rg ignore globs from _extraIgnoreDirs) alters which files
|
|
131
136
|
// match, so it MUST partake in the key — otherwise calls that differ only
|
|
132
137
|
// by extra ignores collide and return stale over-/under-filtered results.
|
|
133
138
|
// Sorted so the same ignore set in any order maps to one key.
|
|
134
139
|
const extra = Array.isArray(extraIgnore) && extraIgnore.length ? [...extraIgnore].sort().join('\x01') : '';
|
|
135
|
-
|
|
140
|
+
// patternCapTotal: a capped pattern set (first-N of M) must not collide with
|
|
141
|
+
// an exact N-pattern request or a differently-capped one.
|
|
142
|
+
return ['glob', patterns.join('\x01'), basePath, headLimit ?? '', offset ?? '', sort || 'natural', extra, 'pc' + String(patternCapTotal || 0)].join('|');
|
|
136
143
|
}
|
|
137
144
|
|
|
138
145
|
export function buildListCacheKey(parts) {
|
|
@@ -523,7 +523,12 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
|
|
|
523
523
|
}
|
|
524
524
|
parts.push(`# grep ${p}\n${body}`);
|
|
525
525
|
}
|
|
526
|
-
if (list.length > GREP_PATH_CAP)
|
|
526
|
+
if (list.length > GREP_PATH_CAP) {
|
|
527
|
+
parts.push(`[capped at ${GREP_PATH_CAP} of ${list.length} paths]`);
|
|
528
|
+
// Omitted paths mean the returned result cannot cover the whole
|
|
529
|
+
// requested path set — never let it be cached as complete.
|
|
530
|
+
if (options?.scopedCacheOutcome) markScopedCacheIncomplete(options.scopedCacheOutcome);
|
|
531
|
+
}
|
|
527
532
|
return parts.join('\n\n');
|
|
528
533
|
}
|
|
529
534
|
args.path = list[0];
|
|
@@ -542,9 +547,15 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
|
|
|
542
547
|
let patterns = uniqueStrings(rawPatterns.map(normalizeSearchPattern));
|
|
543
548
|
const GREP_PATTERN_ARRAY_CAP = 10;
|
|
544
549
|
let patternCapNote = '';
|
|
550
|
+
let patternCapTotal = 0;
|
|
545
551
|
if (patterns.length > GREP_PATTERN_ARRAY_CAP) {
|
|
546
552
|
patternCapNote = `[capped at ${GREP_PATTERN_ARRAY_CAP} of ${patterns.length} patterns]\n`;
|
|
553
|
+
patternCapTotal = patterns.length;
|
|
547
554
|
patterns = patterns.slice(0, GREP_PATTERN_ARRAY_CAP);
|
|
555
|
+
// Dropping input patterns means the returned result cannot cover the
|
|
556
|
+
// full requested pattern set — never cache it as complete. Applies to
|
|
557
|
+
// every downstream path (fan-out, chunk-merge, single combined).
|
|
558
|
+
if (options?.scopedCacheOutcome) markScopedCacheIncomplete(options.scopedCacheOutcome);
|
|
548
559
|
}
|
|
549
560
|
if (patterns.length === 0) {
|
|
550
561
|
if (args.glob || hasGlobMagic(args.path)) {
|
|
@@ -725,16 +736,18 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
|
|
|
725
736
|
&& !options._grepPatternFanout) {
|
|
726
737
|
const seen = new Set();
|
|
727
738
|
const subOptions = { ...options, _grepPatternFanout: true };
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
739
|
+
// Each pattern is an INDEPENDENT grep; run them concurrently and then
|
|
740
|
+
// apply dedup/section assembly in the original pattern order so the
|
|
741
|
+
// shared `seen` set and output text stay byte-identical to the
|
|
742
|
+
// sequential version.
|
|
743
|
+
const subs = await Promise.all(patterns.map(async (p) => {
|
|
731
744
|
try {
|
|
732
|
-
|
|
745
|
+
return await executeGrepTool({ ...args, pattern: p }, workDir, executeChildBuiltinTool, readStateScope, subOptions);
|
|
733
746
|
} catch (err) {
|
|
734
|
-
|
|
747
|
+
return `Error: ${err && err.message ? err.message : err}`;
|
|
735
748
|
}
|
|
736
|
-
|
|
737
|
-
}
|
|
749
|
+
}));
|
|
750
|
+
const parts = patterns.map((p, i) => `# grep pattern:${JSON.stringify(p)}\n${dedupeFanoutMatchLines(subs[i], seen)}`);
|
|
738
751
|
return patternCapNote + parts.join('\n\n');
|
|
739
752
|
}
|
|
740
753
|
|
|
@@ -749,20 +762,27 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
|
|
|
749
762
|
...(outputMode === 'count' ? { output_mode: 'content' } : {}),
|
|
750
763
|
};
|
|
751
764
|
const chunkMergeOptions = { ...options, _grepChunkMerge: true };
|
|
765
|
+
// Fetch every chunk concurrently with the full aggregate budget, then
|
|
766
|
+
// replay the sequential room-based accumulation over the results in
|
|
767
|
+
// chunk order. rg output order is deterministic, so slicing this
|
|
768
|
+
// superset by the sequential `room` yields identical lines, and the
|
|
769
|
+
// re-extract with `room` reproduces the same truncation flags — the
|
|
770
|
+
// merged output and `truncatedAggregate` are byte-identical to the
|
|
771
|
+
// old serial loop while the grep calls overlap.
|
|
772
|
+
const chunkBodies = await Promise.all(patternChunks.map((chunk) => executeGrepTool(
|
|
773
|
+
{ ...chunkBaseArgs, pattern: chunk, head_limit: aggregateBudget },
|
|
774
|
+
workDir,
|
|
775
|
+
executeChildBuiltinTool,
|
|
776
|
+
readStateScope,
|
|
777
|
+
chunkMergeOptions,
|
|
778
|
+
)));
|
|
752
779
|
const mergedRaw = [];
|
|
753
|
-
for (const
|
|
780
|
+
for (const chunkBody of chunkBodies) {
|
|
754
781
|
if (mergedRaw.length >= aggregateBudget) {
|
|
755
782
|
truncatedAggregate = true;
|
|
756
783
|
break;
|
|
757
784
|
}
|
|
758
785
|
const room = aggregateBudget - mergedRaw.length;
|
|
759
|
-
const chunkBody = await executeGrepTool(
|
|
760
|
-
{ ...chunkBaseArgs, pattern: chunk, head_limit: room },
|
|
761
|
-
workDir,
|
|
762
|
-
executeChildBuiltinTool,
|
|
763
|
-
readStateScope,
|
|
764
|
-
chunkMergeOptions,
|
|
765
|
-
);
|
|
766
786
|
const extracted = extractGrepChunkResultLines(chunkBody, room);
|
|
767
787
|
if (extracted.error) return extracted.error.startsWith('Error:') ? extracted.error : `Error: ${extracted.error}`;
|
|
768
788
|
const slice = extracted.lines.slice(0, room);
|
|
@@ -784,6 +804,16 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
|
|
|
784
804
|
const sliced = offset > 0 ? merged.slice(offset) : merged;
|
|
785
805
|
const limit = headLimit === Infinity ? sliced.length : headLimit;
|
|
786
806
|
const windowed = limit === Infinity ? sliced : sliced.slice(0, limit);
|
|
807
|
+
// Cache-outcome fidelity: the concurrent chunk fetches run with the full
|
|
808
|
+
// aggregateBudget as head_limit, so a child may report itself complete
|
|
809
|
+
// even though the replay above trimmed its lines to a smaller `room`
|
|
810
|
+
// (truncatedAggregate), the final head_limit window dropped lines, or an
|
|
811
|
+
// offset paged past earlier matches. Mirror the other grep return paths
|
|
812
|
+
// and mark the scoped cache incomplete so a partial/paged chunk-merge
|
|
813
|
+
// result is never cached as whole.
|
|
814
|
+
if (options?.scopedCacheOutcome && (truncatedAggregate || offset > 0 || windowed.length < sliced.length)) {
|
|
815
|
+
markScopedCacheIncomplete(options.scopedCacheOutcome);
|
|
816
|
+
}
|
|
787
817
|
if (!windowed.length) {
|
|
788
818
|
const patternStr = patterns.length === 1 ? JSON.stringify(patterns[0]) : JSON.stringify(patterns);
|
|
789
819
|
const globStr = normalizedGlobPatterns.length > 0 ? ` glob=${JSON.stringify(normalizedGlobPatterns)}` : '';
|
|
@@ -791,7 +821,10 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
|
|
|
791
821
|
}
|
|
792
822
|
return formatGrepOutput({
|
|
793
823
|
windowed,
|
|
794
|
-
|
|
824
|
+
// POST-offset total so formatGrepOutput's remaining/total math (which
|
|
825
|
+
// re-adds `offset`) does not conjure phantom "more" results from the
|
|
826
|
+
// offset-skipped prefix.
|
|
827
|
+
totalWindowed: sliced.length,
|
|
795
828
|
totalKnown: !truncatedAggregate,
|
|
796
829
|
headLimit,
|
|
797
830
|
offset,
|
|
@@ -828,6 +861,10 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
|
|
|
828
861
|
fileType,
|
|
829
862
|
pcre2: pcre2Mode,
|
|
830
863
|
withFilename: forceGrepFilename,
|
|
864
|
+
// Capped requests carry the "[capped at N of M]" notice; key on the
|
|
865
|
+
// original count so they never collide with an exact N-pattern request
|
|
866
|
+
// (or a differently-capped one) in the internal result cache.
|
|
867
|
+
patternCapTotal,
|
|
831
868
|
});
|
|
832
869
|
// Single-file grep registers a whole-file read snapshot (parity with
|
|
833
870
|
// apply_patch), satisfying the read-before-edit guard while keeping drift
|
|
@@ -1193,17 +1230,22 @@ export async function executeGlobTool(args, workDir, options = {}) {
|
|
|
1193
1230
|
.filter((p) => p && !seen.has(p) && seen.add(p));
|
|
1194
1231
|
if (list.length > 1) {
|
|
1195
1232
|
const capped = list.slice(0, GLOB_PATH_CAP);
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1233
|
+
// Independent per-path globs run concurrently; assemble sections in
|
|
1234
|
+
// the original (capped) order so output stays identical.
|
|
1235
|
+
const bodies = await Promise.all(capped.map(async (p) => {
|
|
1199
1236
|
try {
|
|
1200
|
-
|
|
1237
|
+
return await executeGlobTool({ ...args, path: p }, workDir, options);
|
|
1201
1238
|
} catch (err) {
|
|
1202
|
-
|
|
1239
|
+
return `Error: ${err && err.message ? err.message : err}`;
|
|
1203
1240
|
}
|
|
1204
|
-
|
|
1241
|
+
}));
|
|
1242
|
+
const parts = capped.map((p, i) => `# glob ${p}\n${bodies[i]}`);
|
|
1243
|
+
if (list.length > GLOB_PATH_CAP) {
|
|
1244
|
+
parts.push(`[capped at ${GLOB_PATH_CAP} of ${list.length} paths]`);
|
|
1245
|
+
// Omitted paths mean the returned listing is not the whole
|
|
1246
|
+
// requested set — never cache it as complete.
|
|
1247
|
+
if (options?.scopedCacheOutcome) markScopedCacheIncomplete(options.scopedCacheOutcome);
|
|
1205
1248
|
}
|
|
1206
|
-
if (list.length > GLOB_PATH_CAP) parts.push(`[capped at ${GLOB_PATH_CAP} of ${list.length} paths]`);
|
|
1207
1249
|
return parts.join('\n\n');
|
|
1208
1250
|
}
|
|
1209
1251
|
args.path = list[0] ?? '.';
|
|
@@ -1238,9 +1280,15 @@ export async function executeGlobTool(args, workDir, options = {}) {
|
|
|
1238
1280
|
}
|
|
1239
1281
|
const GLOB_PATTERN_ARRAY_CAP = 10;
|
|
1240
1282
|
let globPatternCapNote = '';
|
|
1283
|
+
let globPatternCapTotal = 0;
|
|
1241
1284
|
if (patterns.length > GLOB_PATTERN_ARRAY_CAP) {
|
|
1242
1285
|
globPatternCapNote = `[capped at ${GLOB_PATTERN_ARRAY_CAP} of ${patterns.length} patterns]\n`;
|
|
1286
|
+
globPatternCapTotal = patterns.length;
|
|
1243
1287
|
patterns = patterns.slice(0, GLOB_PATTERN_ARRAY_CAP);
|
|
1288
|
+
// Omitted patterns: mark the scoped cache incomplete and (below) key the
|
|
1289
|
+
// internal cache on the original count so a capped glob never collides
|
|
1290
|
+
// with an exact N-pattern request or is served as the whole set.
|
|
1291
|
+
if (options?.scopedCacheOutcome) markScopedCacheIncomplete(options.scopedCacheOutcome);
|
|
1244
1292
|
}
|
|
1245
1293
|
|
|
1246
1294
|
const basePaths = (Array.isArray(args.path) && args.path.length > 0)
|
|
@@ -1339,7 +1387,7 @@ export async function executeGlobTool(args, workDir, options = {}) {
|
|
|
1339
1387
|
.map((root) => normalizeOutputPath(resolvedForSearchRoot(root)))
|
|
1340
1388
|
.sort()
|
|
1341
1389
|
.join('\x01');
|
|
1342
|
-
const cacheKey = buildGlobCacheKey({ patterns, basePath: cacheBasePath, headLimit, offset, extraIgnore: extraIgnoreGlobs, sort: sortMode });
|
|
1390
|
+
const cacheKey = buildGlobCacheKey({ patterns, basePath: cacheBasePath, headLimit, offset, extraIgnore: extraIgnoreGlobs, sort: sortMode, patternCapTotal: globPatternCapTotal });
|
|
1343
1391
|
const cached = cacheGet(cacheKey);
|
|
1344
1392
|
if (cached !== null) return cached;
|
|
1345
1393
|
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
import {
|
|
17
17
|
executeBashTool,
|
|
18
18
|
executeTaskTool,
|
|
19
|
+
formatShellToolFailure,
|
|
19
20
|
} from './builtin/bash-tool.mjs';
|
|
20
21
|
import {
|
|
21
22
|
executeFindFilesTool,
|
|
@@ -464,7 +465,7 @@ export async function executeBuiltinTool(name, args, cwd, options = {}) {
|
|
|
464
465
|
}
|
|
465
466
|
const toolName = canonicalizeBuiltinToolName(name);
|
|
466
467
|
const argError = validateBuiltinArgs(toolName, args);
|
|
467
|
-
if (argError) return argError;
|
|
468
|
+
if (argError) return toolName === 'shell' ? formatShellToolFailure(argError) : argError;
|
|
468
469
|
// Fallback live-progress emit for direct callers (in-process toolExecutor
|
|
469
470
|
// path). The MCP dispatch path already fired the central start message and
|
|
470
471
|
// sets progressStarted:true, so guard against a double-emit there. No-op
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// worker at tools/.
|
|
14
14
|
import { resolve as pathResolve } from 'node:path';
|
|
15
15
|
import { Worker } from 'node:worker_threads';
|
|
16
|
-
import { acquire as acquireChildSpawnSlot } from '../../../../shared/child-spawn-gate.mjs';
|
|
16
|
+
import { acquire as acquireChildSpawnSlot, hasSpareCapacity as childSpawnHasSpareCapacity } from '../../../../shared/child-spawn-gate.mjs';
|
|
17
17
|
import {
|
|
18
18
|
canonicalGraphCwd as _canonicalGraphCwd,
|
|
19
19
|
codeGraphCache as _codeGraphCache,
|
|
@@ -55,8 +55,10 @@ const _inflightAsyncBuilds = new Map();
|
|
|
55
55
|
|
|
56
56
|
export function prewarmCodeGraph(cwd) {
|
|
57
57
|
if (!cwd) return;
|
|
58
|
-
// Reuse the buildCodeGraphAsync single-flight path. Fire-and-forget
|
|
59
|
-
|
|
58
|
+
// Reuse the buildCodeGraphAsync single-flight path. Fire-and-forget, and
|
|
59
|
+
// best-effort: skip a fresh worker spawn when the child-spawn gate is busy
|
|
60
|
+
// so this warm never queues ahead of real code_graph/find queries.
|
|
61
|
+
buildCodeGraphAsync(cwd, null, { bestEffort: true }).catch(() => { /* best-effort */ });
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
export function prewarmCodeGraphSymbols(cwd, symbols, { language = null } = {}) {
|
|
@@ -64,7 +66,7 @@ export function prewarmCodeGraphSymbols(cwd, symbols, { language = null } = {})
|
|
|
64
66
|
const wanted = (Array.isArray(symbols) ? symbols : [symbols])
|
|
65
67
|
.map((s) => String(s || '').trim())
|
|
66
68
|
.filter(Boolean);
|
|
67
|
-
buildCodeGraphAsync(cwd).then((graph) => {
|
|
69
|
+
buildCodeGraphAsync(cwd, null, { bestEffort: true }).then((graph) => {
|
|
68
70
|
if (!graph) return;
|
|
69
71
|
for (const symbol of wanted) {
|
|
70
72
|
try { _lookupCandidateNodes(graph, symbol, language); } catch { /* best-effort */ }
|
|
@@ -80,7 +82,7 @@ export function prewarmCodeGraphIfProject(cwd) {
|
|
|
80
82
|
return true;
|
|
81
83
|
}
|
|
82
84
|
|
|
83
|
-
export async function buildCodeGraphAsync(cwd, signal = null) {
|
|
85
|
+
export async function buildCodeGraphAsync(cwd, signal = null, { bestEffort = false } = {}) {
|
|
84
86
|
if (signal?.aborted) throw new Error('aborted');
|
|
85
87
|
const graphCwd = _canonicalGraphCwd(cwd);
|
|
86
88
|
const cached = _codeGraphCache.get(graphCwd);
|
|
@@ -107,6 +109,13 @@ export async function buildCodeGraphAsync(cwd, signal = null) {
|
|
|
107
109
|
(e) => { cleanup(); throw e; },
|
|
108
110
|
);
|
|
109
111
|
}
|
|
112
|
+
// Non-competing prewarm: once past the cache/single-flight fast paths a
|
|
113
|
+
// fresh build must spawn the worker (a child-spawn slot). Best-effort warmers
|
|
114
|
+
// skip that spawn when the gate has no spare capacity, returning null so the
|
|
115
|
+
// caller no-ops — and skipping BEFORE registering the in-flight entry so a
|
|
116
|
+
// concurrent REAL caller is never attached to (and thus never starved by) a
|
|
117
|
+
// skipped warm.
|
|
118
|
+
if (bestEffort && !childSpawnHasSpareCapacity()) return null;
|
|
110
119
|
const _genAtStart = _getCodeGraphGen(graphCwd);
|
|
111
120
|
let _worker = null;
|
|
112
121
|
const promise = new Promise((resolve, reject) => {
|
|
@@ -151,11 +160,12 @@ export async function buildCodeGraphAsync(cwd, signal = null) {
|
|
|
151
160
|
w.once('message', (msg) => {
|
|
152
161
|
try {
|
|
153
162
|
if (msg && msg.ok && msg.graph && typeof msg.signature === 'string') {
|
|
154
|
-
|
|
163
|
+
const genStillCurrent = _getCodeGraphGen(graphCwd) === _genAtStart;
|
|
164
|
+
if (genStillCurrent) {
|
|
155
165
|
_setCodeGraphCache(graphCwd, { ts: Date.now(), signature: msg.signature, graph: msg.graph });
|
|
156
166
|
_setDiskCodeGraphEntry(graphCwd, msg.graph);
|
|
157
167
|
}
|
|
158
|
-
settle(msg.graph);
|
|
168
|
+
settle(genStillCurrent ? msg.graph : new Error('code-graph build invalidated during prewarm'));
|
|
159
169
|
} else {
|
|
160
170
|
settle(new Error('code-graph prewarm worker failed'));
|
|
161
171
|
}
|
|
@@ -360,8 +360,8 @@ async function codeGraph(args, cwd, signal = null, options = {}) {
|
|
|
360
360
|
const userLimit = Number.isFinite(rawLimit) && rawLimit > 0
|
|
361
361
|
? Math.min(500, Math.floor(rawLimit))
|
|
362
362
|
: null;
|
|
363
|
-
await _prewarmReferenceSourceText(graph, symbol, lang);
|
|
364
|
-
const refResult = _cheapReferenceSearch(graph, symbol, cwd, { language: lang, limit: userLimit, fileRel: rel, scopeRelPrefix });
|
|
363
|
+
const _refNodes = await _prewarmReferenceSourceText(graph, symbol, lang);
|
|
364
|
+
const refResult = _cheapReferenceSearch(graph, symbol, cwd, { language: lang, limit: userLimit, fileRel: rel, scopeRelPrefix, nodes: _refNodes });
|
|
365
365
|
return narrowedByCaller ? refResult : _augmentNoHitDiagnostic(refResult, '(no references)', graph, cwd, symbol);
|
|
366
366
|
}
|
|
367
367
|
|
|
@@ -390,12 +390,12 @@ async function codeGraph(args, cwd, signal = null, options = {}) {
|
|
|
390
390
|
const userLimit = Number.isFinite(rawLimit) && rawLimit > 0
|
|
391
391
|
? Math.min(500, Math.floor(rawLimit))
|
|
392
392
|
: null;
|
|
393
|
-
await _prewarmReferenceSourceText(graph, symbol, lang);
|
|
393
|
+
const _callerNodes = await _prewarmReferenceSourceText(graph, symbol, lang);
|
|
394
394
|
const depth = Math.max(1, Math.min(5, Math.floor(Number(args?.depth) || 1)));
|
|
395
395
|
if (depth > 1) {
|
|
396
396
|
return _formatTransitiveCallers(graph, symbol, cwd, { language: lang, depth, page: args?.page });
|
|
397
397
|
}
|
|
398
|
-
const refs = _cheapReferenceSearch(graph, symbol, cwd, { language: lang, limit: userLimit, fileRel: rel, scopeRelPrefix });
|
|
398
|
+
const refs = _cheapReferenceSearch(graph, symbol, cwd, { language: lang, limit: userLimit, fileRel: rel, scopeRelPrefix, nodes: _callerNodes });
|
|
399
399
|
const callerResult = _formatCallerReferences(graph, symbol, refs, userLimit ? { limit: userLimit } : undefined);
|
|
400
400
|
return narrowedByCaller ? callerResult : _augmentNoHitDiagnostic(callerResult, '(no callers)', graph, cwd, symbol);
|
|
401
401
|
}
|
|
@@ -575,13 +575,16 @@ export async function executeCodeGraphTool(name, args, cwd, signal = null, optio
|
|
|
575
575
|
const symbolList = _collectGraphSymbolList(args);
|
|
576
576
|
if (symbolList.length > 1) {
|
|
577
577
|
return (async () => {
|
|
578
|
-
|
|
579
|
-
|
|
578
|
+
// Concurrent fan-out: the underlying graph build is single-flight
|
|
579
|
+
// and cached per cwd (buildCodeGraphAsync), so parallel sections
|
|
580
|
+
// share one build instead of serializing on it. Order and
|
|
581
|
+
// per-section error isolation are preserved by the indexed map.
|
|
582
|
+
const sections = await Promise.all(symbolList.map(async (sym) => {
|
|
580
583
|
let body;
|
|
581
584
|
try { body = await dispatchOne({ ...args, symbol: sym, symbols: undefined }); }
|
|
582
585
|
catch (e) { body = `Error: ${e?.message || String(e)}`; }
|
|
583
|
-
|
|
584
|
-
}
|
|
586
|
+
return `# ${batchMode} ${sym}\n${body}`;
|
|
587
|
+
}));
|
|
585
588
|
return sections.join('\n\n');
|
|
586
589
|
})();
|
|
587
590
|
}
|
|
@@ -594,13 +597,12 @@ export async function executeCodeGraphTool(name, args, cwd, signal = null, optio
|
|
|
594
597
|
if (fileList.length > 1) {
|
|
595
598
|
const capped = fileList._capped;
|
|
596
599
|
return (async () => {
|
|
597
|
-
const sections =
|
|
598
|
-
for (const f of fileList) {
|
|
600
|
+
const sections = await Promise.all(fileList.map(async (f) => {
|
|
599
601
|
let body;
|
|
600
602
|
try { body = await dispatchOne({ ...args, file: f, files: undefined }); }
|
|
601
603
|
catch (e) { body = `Error: ${e?.message || String(e)}`; }
|
|
602
|
-
|
|
603
|
-
}
|
|
604
|
+
return `# ${batchMode} ${f}\n${body}`;
|
|
605
|
+
}));
|
|
604
606
|
if (capped) sections.push(`Note: file list capped at ${CODE_GRAPH_FILE_BATCH_CAP} entries.`);
|
|
605
607
|
return sections.join('\n\n');
|
|
606
608
|
})();
|
|
@@ -119,6 +119,7 @@ export function _deserializeGraph(cwd, payload) {
|
|
|
119
119
|
export function _attachGraphRuntimeCaches(graph) {
|
|
120
120
|
if (!graph || typeof graph !== 'object') return graph;
|
|
121
121
|
if (!graph._referenceSearchCache) graph._referenceSearchCache = new Map();
|
|
122
|
+
if (!graph._keywordSearchCache) graph._keywordSearchCache = new Map();
|
|
122
123
|
if (!graph._maskedLinesCache) graph._maskedLinesCache = new Map();
|
|
123
124
|
if (!graph._sourceLinesCache) graph._sourceLinesCache = new Map();
|
|
124
125
|
if (!graph._sourceTextCache) graph._sourceTextCache = new Map();
|
|
@@ -144,6 +145,9 @@ export function _estimateGraphRuntimeCacheBytes(graph) {
|
|
|
144
145
|
for (const memo of graph._referenceSearchCache?.values() || []) {
|
|
145
146
|
total += Buffer.byteLength(String(memo || ''), 'utf8');
|
|
146
147
|
}
|
|
148
|
+
for (const memo of graph._keywordSearchCache?.values() || []) {
|
|
149
|
+
total += Buffer.byteLength(String(memo || ''), 'utf8');
|
|
150
|
+
}
|
|
147
151
|
return total;
|
|
148
152
|
}
|
|
149
153
|
|
|
@@ -153,6 +157,7 @@ export function _clearGraphRuntimeCaches(graph) {
|
|
|
153
157
|
graph._maskedLinesCache?.clear();
|
|
154
158
|
graph._sourceLinesCache?.clear();
|
|
155
159
|
graph._referenceSearchCache?.clear();
|
|
160
|
+
graph._keywordSearchCache?.clear();
|
|
156
161
|
graph._symbolTokenIndex?.clear();
|
|
157
162
|
graph._symbolTokenIndexDirty = true;
|
|
158
163
|
}
|