mixdog 0.9.38 → 0.9.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -4
- package/scripts/abort-recovery-test.mjs +43 -2
- package/scripts/agent-tag-reuse-smoke.mjs +146 -6
- package/scripts/agent-terminal-reap-test.mjs +127 -0
- package/scripts/agent-trace-io-test.mjs +69 -0
- package/scripts/code-graph-disk-hit-test.mjs +224 -0
- package/scripts/execution-completion-dedup-test.mjs +157 -0
- package/scripts/execution-pending-resume-kick-test.mjs +57 -2
- package/scripts/execution-resume-esc-integration-test.mjs +174 -0
- package/scripts/explore-bench.mjs +38 -2
- package/scripts/explore-prompt-policy-test.mjs +152 -11
- package/scripts/find-fuzzy-hidden-test.mjs +122 -0
- package/scripts/internal-comms-bench-test.mjs +226 -0
- package/scripts/internal-comms-bench.mjs +185 -58
- package/scripts/internal-comms-smoke.mjs +171 -23
- package/scripts/live-worker-smoke.mjs +38 -2
- package/scripts/memory-cycle-routing-test.mjs +111 -0
- package/scripts/memory-rule-contract-test.mjs +93 -0
- package/scripts/output-style-smoke.mjs +2 -2
- package/scripts/rg-runner-test.mjs +240 -0
- package/scripts/routing-corpus-test.mjs +349 -0
- package/scripts/routing-corpus.mjs +211 -32
- package/scripts/session-orphan-sweep-test.mjs +83 -0
- package/scripts/steering-drain-buckets-test.mjs +179 -0
- package/scripts/tool-smoke.mjs +21 -13
- package/scripts/tool-tui-presentation-test.mjs +202 -0
- package/src/agents/heavy-worker/AGENT.md +10 -7
- package/src/agents/reviewer/AGENT.md +6 -4
- package/src/agents/worker/AGENT.md +7 -5
- package/src/rules/agent/00-common.md +4 -4
- package/src/rules/agent/00-core.md +11 -14
- package/src/rules/agent/20-skip-protocol.md +3 -3
- package/src/rules/agent/30-explorer.md +50 -60
- package/src/rules/agent/40-cycle1-agent.md +15 -24
- package/src/rules/agent/41-cycle2-agent.md +33 -57
- package/src/rules/agent/42-cycle3-agent.md +28 -42
- package/src/rules/lead/01-general.md +7 -10
- package/src/rules/lead/lead-brief.md +11 -14
- package/src/rules/lead/lead-tool.md +6 -5
- package/src/rules/shared/01-tool.md +44 -45
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +37 -1
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +20 -5
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +21 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +17 -38
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +17 -8
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +270 -78
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/manager/delivered-completions.mjs +123 -0
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +15 -2
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +41 -2
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/store.mjs +223 -42
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +25 -20
- package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +118 -53
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +106 -29
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +8 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +4 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +33 -2
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +151 -64
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +37 -13
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +84 -49
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +172 -23
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +3 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +68 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +17 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +24 -10
- package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +6 -3
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -7
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +1 -0
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -4
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +4 -3
- package/src/runtime/memory/lib/memory-cycle3.mjs +12 -2
- package/src/runtime/shared/tool-primitives.mjs +4 -1
- package/src/runtime/shared/tool-status.mjs +27 -0
- package/src/runtime/shared/tool-surface.mjs +6 -3
- package/src/session-runtime/config-helpers.mjs +14 -0
- package/src/session-runtime/context-status.mjs +1 -0
- package/src/session-runtime/effort.mjs +6 -2
- package/src/session-runtime/model-recency.mjs +5 -2
- package/src/session-runtime/runtime-core.mjs +35 -2
- package/src/session-runtime/tool-catalog.mjs +34 -0
- package/src/standalone/agent-tool/notify.mjs +13 -0
- package/src/standalone/agent-tool.mjs +45 -69
- package/src/standalone/explore-tool.mjs +6 -7
- package/src/tui/App.jsx +31 -0
- package/src/tui/app/model-options.mjs +5 -3
- package/src/tui/app/model-picker.mjs +12 -24
- package/src/tui/app/transcript-window.mjs +1 -0
- package/src/tui/components/ToolExecution.jsx +11 -6
- package/src/tui/components/TranscriptItem.jsx +1 -1
- package/src/tui/components/tool-execution/surface-detail.mjs +24 -10
- package/src/tui/components/tool-execution/text-format.mjs +10 -19
- package/src/tui/dist/index.mjs +517 -142
- package/src/tui/engine/agent-job-feed.mjs +144 -17
- package/src/tui/engine/agent-response-tail.mjs +68 -0
- package/src/tui/engine/notification-plan.mjs +16 -0
- package/src/tui/engine/session-api.mjs +8 -2
- package/src/tui/engine/session-flow.mjs +19 -1
- package/src/tui/engine/tool-card-results.mjs +54 -32
- package/src/tui/engine/tool-result-status.mjs +75 -21
- package/src/tui/engine/turn.mjs +77 -42
- package/src/tui/engine.mjs +63 -2
- package/src/workflows/bench/WORKFLOW.md +25 -35
- package/src/workflows/default/WORKFLOW.md +38 -32
- package/src/workflows/solo/WORKFLOW.md +19 -22
- package/scripts/_jitter-fuzz.mjs +0 -44410
- package/scripts/_jitter-fuzz2.mjs +0 -44400
- package/scripts/_jitter-probe.mjs +0 -44397
- package/scripts/_jp2.mjs +0 -45614
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { spawn, execFile } from 'child_process';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
|
-
import {
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { accessSync, constants, statSync } from 'node:fs';
|
|
4
5
|
import { promisify } from 'node:util';
|
|
5
6
|
import os from 'node:os';
|
|
6
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
acquire as acquireChildSpawnSlot,
|
|
9
|
+
hasSpareCapacity as hasRgSpareCapacity,
|
|
10
|
+
} from '../../../../shared/child-spawn-gate.mjs';
|
|
7
11
|
|
|
8
12
|
const execFileAsync = promisify(execFile);
|
|
9
13
|
|
|
@@ -28,15 +32,48 @@ const execFileAsync = promisify(execFile);
|
|
|
28
32
|
// ── rg --threads cap ─────────────────────────────────────────────────────
|
|
29
33
|
// Each rg process otherwise fans out across EVERY core; with several agents
|
|
30
34
|
// running grep at once the box over-saturates and the whole batch trips the
|
|
31
|
-
// 20s deadline together.
|
|
32
|
-
//
|
|
33
|
-
// only — deliberately NOT exposed on
|
|
34
|
-
|
|
35
|
+
// 20s deadline together. Use the larger half-CPU cap only when the spawn gate
|
|
36
|
+
// has spare capacity; otherwise retain the conservative quarter-CPU cap.
|
|
37
|
+
// Internal dynamic default; env override only — deliberately NOT exposed on
|
|
38
|
+
// any tool schema / parameter surface.
|
|
39
|
+
let _rgDefaultThreadCap = null;
|
|
40
|
+
export function _rgThreadCap(spareCapacity = false) {
|
|
35
41
|
const override = Number(process.env.MIXDOG_RG_THREADS);
|
|
36
42
|
if (Number.isFinite(override) && override >= 1) return Math.floor(override);
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
43
|
+
if (_rgDefaultThreadCap === null) {
|
|
44
|
+
let cpus = 0;
|
|
45
|
+
try { cpus = os.cpus()?.length || 0; } catch { cpus = 0; }
|
|
46
|
+
_rgDefaultThreadCap = cpus || 4;
|
|
47
|
+
}
|
|
48
|
+
const cpus = _rgDefaultThreadCap;
|
|
49
|
+
return spareCapacity
|
|
50
|
+
? Math.max(4, Math.ceil(cpus / 2))
|
|
51
|
+
: Math.max(2, Math.ceil(cpus / 4));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Walk rg options once for all thread-argument helpers. Values of these
|
|
55
|
+
// flags are opaque patterns/paths and must not be mistaken for options.
|
|
56
|
+
const _rgValueFlags = new Set([
|
|
57
|
+
'-e', '--glob', '--iglob', '--type', '-A', '-B', '-C',
|
|
58
|
+
'--max-depth', '--max-columns',
|
|
59
|
+
]);
|
|
60
|
+
function _walkRgArgs(argsList, visit) {
|
|
61
|
+
let options = true;
|
|
62
|
+
for (let i = 0; i < argsList.length; i++) {
|
|
63
|
+
const a = argsList[i];
|
|
64
|
+
if (options && a === '--') {
|
|
65
|
+
visit(a, i, null, true);
|
|
66
|
+
options = false;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (options && typeof a === 'string'
|
|
70
|
+
&& (_rgValueFlags.has(a) || a === '-j' || a === '--threads')) {
|
|
71
|
+
visit(a, i, i + 1, true);
|
|
72
|
+
i++;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
visit(a, i, null, options);
|
|
76
|
+
}
|
|
40
77
|
}
|
|
41
78
|
|
|
42
79
|
// Single source of truth for "did the caller already pin rg's thread count?".
|
|
@@ -44,23 +81,23 @@ function _rgThreadCap() {
|
|
|
44
81
|
// is the NEXT arg, short-attached (`-j8`), and long-attached (`--threads=8`).
|
|
45
82
|
// Used by both the _withRgThreads injection guard and the EAGAIN retry guard
|
|
46
83
|
// so the two never disagree.
|
|
47
|
-
function _hasRgThreadArg(argsList) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
return
|
|
84
|
+
export function _hasRgThreadArg(argsList) {
|
|
85
|
+
let found = false;
|
|
86
|
+
_walkRgArgs(argsList, (a, _index, _valueIndex, options) => {
|
|
87
|
+
if (!options || typeof a !== 'string') return;
|
|
88
|
+
if (a === '-j' || a === '--threads'
|
|
89
|
+
|| /^-j\d+$/.test(a) || a.startsWith('--threads=')) found = true;
|
|
90
|
+
});
|
|
91
|
+
return found;
|
|
55
92
|
}
|
|
56
93
|
|
|
57
94
|
// Inject `--threads N` unless the caller already pinned thread count
|
|
58
95
|
// (`-j`/`--threads`/`-jN`/`--threads=N`, e.g. the EAGAIN `-j 1` retry). Does
|
|
59
96
|
// not mutate; may return the ORIGINAL array unchanged when already
|
|
60
97
|
// thread-pinned (callers must not assume a fresh copy).
|
|
61
|
-
function _withRgThreads(argsList) {
|
|
98
|
+
export function _withRgThreads(argsList, spareCapacity = false) {
|
|
62
99
|
if (_hasRgThreadArg(argsList)) return argsList;
|
|
63
|
-
return ['--threads', String(_rgThreadCap()), ...argsList];
|
|
100
|
+
return ['--threads', String(_rgThreadCap(spareCapacity)), ...argsList];
|
|
64
101
|
}
|
|
65
102
|
|
|
66
103
|
// Build the EAGAIN single-thread retry args. If the caller already pinned a
|
|
@@ -68,14 +105,15 @@ function _withRgThreads(argsList) {
|
|
|
68
105
|
// retry is unambiguously single-threaded (never `-j 8 ... -j 1`). Drops both the
|
|
69
106
|
// separated form (`-j N` / `--threads N` → flag + following count) and the
|
|
70
107
|
// attached forms (`-jN` / `--threads=N`).
|
|
71
|
-
function _rgEagainRetryArgs(argsList) {
|
|
108
|
+
export function _rgEagainRetryArgs(argsList) {
|
|
72
109
|
const out = [];
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
110
|
+
_walkRgArgs(argsList, (a, index, valueIndex, options) => {
|
|
111
|
+
if (options && typeof a === 'string'
|
|
112
|
+
&& (a === '-j' || a === '--threads'
|
|
113
|
+
|| /^-j\d+$/.test(a) || a.startsWith('--threads='))) return;
|
|
77
114
|
out.push(a);
|
|
78
|
-
|
|
115
|
+
if (valueIndex !== null && valueIndex < argsList.length) out.push(argsList[valueIndex]);
|
|
116
|
+
});
|
|
79
117
|
return ['-j', '1', ...out];
|
|
80
118
|
}
|
|
81
119
|
|
|
@@ -83,43 +121,75 @@ function _rgEagainRetryArgs(argsList) {
|
|
|
83
121
|
// retry would change nothing. Matches the separated `-j 1` / `--threads 1` form
|
|
84
122
|
// and the attached `-j1` / `--threads=1` form. Any other thread pin (e.g. -j8)
|
|
85
123
|
// is NOT "single-thread pinned": the retry should still run and downshift it.
|
|
86
|
-
function _isRgSingleThreadPinned(argsList) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (typeof a !== 'string')
|
|
90
|
-
if ((a === '-j' || a === '--threads')
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
124
|
+
export function _isRgSingleThreadPinned(argsList) {
|
|
125
|
+
let found = false;
|
|
126
|
+
_walkRgArgs(argsList, (a, _index, valueIndex, options) => {
|
|
127
|
+
if (!options || typeof a !== 'string') return;
|
|
128
|
+
if ((a === '-j' || a === '--threads')
|
|
129
|
+
&& valueIndex !== null && String(argsList[valueIndex]).trim() === '1') found = true;
|
|
130
|
+
if (a === '-j1' || a === '--threads=1') found = true;
|
|
131
|
+
});
|
|
132
|
+
return found;
|
|
94
133
|
}
|
|
95
134
|
|
|
96
135
|
let _rgExecutableResolved = null;
|
|
136
|
+
let _rgExecutableResolutionKey = null;
|
|
137
|
+
let _rgResolvePromise = null;
|
|
138
|
+
let _rgResolvePromiseKey = null;
|
|
139
|
+
let _rgResolutionGeneration = 0;
|
|
97
140
|
|
|
98
|
-
|
|
99
|
-
|
|
141
|
+
function _rgResolutionKey() {
|
|
142
|
+
return [
|
|
143
|
+
process.platform,
|
|
144
|
+
process.cwd(),
|
|
145
|
+
String(process.env.PATH || ''),
|
|
146
|
+
String(process.env.PATHEXT || ''),
|
|
147
|
+
].join('\0');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function _usableRgCandidate(candidate, isWin = process.platform === 'win32') {
|
|
100
151
|
try {
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
152
|
+
const info = statSync(candidate);
|
|
153
|
+
if (!info.isFile()) return false;
|
|
154
|
+
if (isWin) {
|
|
155
|
+
const ext = candidate.slice(candidate.lastIndexOf('.')).toLowerCase();
|
|
156
|
+
return ext === '.exe' || ext === '.com';
|
|
157
|
+
}
|
|
158
|
+
accessSync(candidate, constants.X_OK);
|
|
159
|
+
return true;
|
|
160
|
+
} catch {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function _resolveRgExecutable() {
|
|
166
|
+
const isWin = process.platform === 'win32';
|
|
106
167
|
const pathSep = isWin ? ';' : ':';
|
|
107
|
-
const dirs = String(process.env.PATH || '').split(pathSep)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
168
|
+
const dirs = String(process.env.PATH || '').split(pathSep);
|
|
169
|
+
// Avoid `where`/`which` on the normal path: on Windows it costs tens of
|
|
170
|
+
// milliseconds before doing the same filesystem search. Windows command
|
|
171
|
+
// lookup checks the current directory before PATH and applies PATHEXT in
|
|
172
|
+
// order; POSIX lookup is just the PATH order.
|
|
173
|
+
const searchDirs = isWin ? [process.cwd(), ...dirs] : dirs;
|
|
174
|
+
const names = isWin
|
|
175
|
+
? String(process.env.PATHEXT || '.EXE;.CMD;.BAT').split(';')
|
|
176
|
+
.map((ext) => ext.trim().toLowerCase())
|
|
177
|
+
.filter((ext) => ext === '.exe' || ext === '.com')
|
|
178
|
+
.map((ext) => `rg${ext}`)
|
|
179
|
+
: ['rg'];
|
|
180
|
+
for (const dir of searchDirs) {
|
|
113
181
|
for (const name of names) {
|
|
114
|
-
const candidate =
|
|
115
|
-
if (
|
|
116
|
-
if (isWin) {
|
|
117
|
-
const ext = candidate.slice(candidate.lastIndexOf('.')).toLowerCase();
|
|
118
|
-
if (ext && !pathext.includes(ext)) continue;
|
|
119
|
-
}
|
|
120
|
-
return candidate;
|
|
182
|
+
const candidate = resolve(dir, name);
|
|
183
|
+
if (existsSync(candidate) && _usableRgCandidate(candidate, isWin)) return candidate;
|
|
121
184
|
}
|
|
122
185
|
}
|
|
186
|
+
try {
|
|
187
|
+
const cmd = isWin ? 'where' : 'which';
|
|
188
|
+
const { stdout: out } = await execFileAsync(cmd, ['rg'], { encoding: 'utf8', windowsHide: true });
|
|
189
|
+
const first = out.split(/\r?\n/).map((l) => l.trim()).find(Boolean);
|
|
190
|
+
const candidate = first ? resolve(first) : '';
|
|
191
|
+
if (candidate && _usableRgCandidate(candidate, isWin)) return candidate;
|
|
192
|
+
} catch { /* preserve bare-rg fallback */ }
|
|
123
193
|
return 'rg';
|
|
124
194
|
}
|
|
125
195
|
|
|
@@ -134,12 +204,23 @@ function rgExecutable() {
|
|
|
134
204
|
// blocking the event loop ~90ms on the first grep/glob/find of a session
|
|
135
205
|
// (freezing the TUI). Resolve asynchronously and cache the result; subsequent
|
|
136
206
|
// calls return immediately.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
207
|
+
export async function ensureRgResolved() {
|
|
208
|
+
const key = _rgResolutionKey();
|
|
209
|
+
const cachedUsable = _rgExecutableResolved !== null
|
|
210
|
+
&& _rgExecutableResolved !== 'rg'
|
|
211
|
+
&& _usableRgCandidate(_rgExecutableResolved);
|
|
212
|
+
if (_rgExecutableResolutionKey === key && cachedUsable) return _rgExecutableResolved;
|
|
213
|
+
if (_rgExecutableResolved !== null && (_rgExecutableResolutionKey !== key || !cachedUsable)) {
|
|
214
|
+
_rgResolutionGeneration++;
|
|
215
|
+
_rgExecutableResolved = null;
|
|
216
|
+
_rgExecutableResolutionKey = null;
|
|
217
|
+
_rgResolvePromise = null;
|
|
218
|
+
}
|
|
219
|
+
if (!_rgResolvePromise || _rgResolvePromiseKey !== key) {
|
|
220
|
+
_rgResolvePromiseKey = key;
|
|
141
221
|
_rgResolvePromise = _resolveRgExecutable().then((resolved) => {
|
|
142
222
|
_rgExecutableResolved = resolved;
|
|
223
|
+
_rgExecutableResolutionKey = key;
|
|
143
224
|
return resolved;
|
|
144
225
|
});
|
|
145
226
|
}
|
|
@@ -155,7 +236,6 @@ const RG_FALLBACK_FAIL_TTL_MS = 30000;
|
|
|
155
236
|
let _rgFallbackUsable = null;
|
|
156
237
|
let _rgFallbackFailAt = 0;
|
|
157
238
|
async function rgFallbackUsable() {
|
|
158
|
-
if (_rgFallbackUsable === true) return true;
|
|
159
239
|
if (_rgFallbackUsable === false && (Date.now() - _rgFallbackFailAt) < RG_FALLBACK_FAIL_TTL_MS) {
|
|
160
240
|
return false;
|
|
161
241
|
}
|
|
@@ -195,12 +275,16 @@ const RG_PCRE2_FAIL_TTL_MS = 30000;
|
|
|
195
275
|
let _rgPcre2Supported = null;
|
|
196
276
|
let _rgPcre2FailAt = 0;
|
|
197
277
|
export async function rgSupportsPcre2() {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
278
|
+
await ensureRgResolved();
|
|
279
|
+
const cacheKey = `${_rgResolutionGeneration}:${rgExecutable()}`;
|
|
280
|
+
if (rgSupportsPcre2._cacheKey !== cacheKey) {
|
|
281
|
+
_rgPcre2Supported = null;
|
|
282
|
+
_rgPcre2FailAt = 0;
|
|
283
|
+
rgSupportsPcre2._cacheKey = cacheKey;
|
|
201
284
|
}
|
|
285
|
+
if (_rgPcre2Supported === true) return true;
|
|
286
|
+
if (_rgPcre2Supported === false && (Date.now() - _rgPcre2FailAt) < RG_PCRE2_FAIL_TTL_MS) return false;
|
|
202
287
|
try {
|
|
203
|
-
await ensureRgResolved();
|
|
204
288
|
await execFileAsync(rgExecutable(), ['--pcre2-version'], { windowsHide: true, timeout: 3000 });
|
|
205
289
|
_rgPcre2Supported = true;
|
|
206
290
|
} catch {
|
|
@@ -311,7 +395,9 @@ function spawnRg(argsList, execOptions) {
|
|
|
311
395
|
// node guardian is started: rg owns its full SIGTERM→grace→force-kill +
|
|
312
396
|
// force-settle teardown, so the 1:1 guardian process was pure overhead.
|
|
313
397
|
return acquireChildSpawnSlot(gateSignal).then((releaseSlot) => new Promise((resolve, reject) => {
|
|
314
|
-
|
|
398
|
+
// Probe after acquisition: this spawn counts toward inflight. The
|
|
399
|
+
// answer is best-effort and only selects the per-process rg cap.
|
|
400
|
+
const proc = spawn(rgExecutable(), _withRgThreads(argsList, hasRgSpareCapacity()), {
|
|
315
401
|
cwd: execOptions?.cwd,
|
|
316
402
|
env: execOptions?.env || process.env,
|
|
317
403
|
windowsHide: true,
|
|
@@ -456,7 +542,8 @@ function spawnRgWindowedLines(argsList, execOptions, opts = {}) {
|
|
|
456
542
|
// excluded from the rg timeout (armed after spawn). releaseSlot is fired
|
|
457
543
|
// once via .finally on every settle path.
|
|
458
544
|
return acquireChildSpawnSlot(gateSignal).then((releaseSlot) => new Promise((resolve, reject) => {
|
|
459
|
-
|
|
545
|
+
// As above, probe only after this spawn has acquired its gate slot.
|
|
546
|
+
const proc = spawn(rgExecutable(), _withRgThreads(argsList, hasRgSpareCapacity()), {
|
|
460
547
|
cwd: execOptions?.cwd,
|
|
461
548
|
env: execOptions?.env || process.env,
|
|
462
549
|
windowsHide: true,
|
|
@@ -67,6 +67,24 @@ const NOT_FOUND_CODES = new Set(['ENOENT', 'ENOTDIR']);
|
|
|
67
67
|
|
|
68
68
|
export const ENOENT_FIND_NUDGE = 'Locate with find on the basename before retrying.';
|
|
69
69
|
|
|
70
|
+
// Per-invocation memo for the ENOENT recovery fs scans. A single grep/glob
|
|
71
|
+
// ENOENT surfaces the SAME missing path through tryReadFamilyEnoentRedirect
|
|
72
|
+
// (resolveUniqueEnoentRedirect) AND buildNotFoundHint (which re-runs
|
|
73
|
+
// resolveUniqueEnoentRedirect plus its own findFileByBasename). Threading an
|
|
74
|
+
// optional cache object keyed on that single path collapses the repeated
|
|
75
|
+
// suffix-strip / basename BFS into one scan. Absent cache → scan fresh
|
|
76
|
+
// (unchanged behavior for callers that don't pass one).
|
|
77
|
+
function cachedSuffixStrip(workDir, missingPath, cache) {
|
|
78
|
+
if (!cache) return findBySuffixStrip(workDir, missingPath);
|
|
79
|
+
if (!('suffixHit' in cache)) cache.suffixHit = findBySuffixStrip(workDir, missingPath);
|
|
80
|
+
return cache.suffixHit;
|
|
81
|
+
}
|
|
82
|
+
function cachedFileByBasename(workDir, missingPath, cache) {
|
|
83
|
+
if (!cache) return findFileByBasename(workDir, missingPath);
|
|
84
|
+
if (!('fileHits' in cache)) cache.fileHits = findFileByBasename(workDir, missingPath);
|
|
85
|
+
return cache.fileHits;
|
|
86
|
+
}
|
|
87
|
+
|
|
70
88
|
function isDirectoryPathGuess(missingPath) {
|
|
71
89
|
const base = basename(String(missingPath || '').replace(/\\/g, '/'));
|
|
72
90
|
if (!base || /[*?[\]{}]/.test(base)) return false;
|
|
@@ -112,11 +130,11 @@ export function finalizeReadFamilyEnoentTail(hint, requestedPath, errCode = 'ENO
|
|
|
112
130
|
return appendEnoentFindNudge(String(hint || '') + spaceJoinedPathHint(requestedPath));
|
|
113
131
|
}
|
|
114
132
|
|
|
115
|
-
export function resolveUniqueEnoentRedirect(workDir, missingPath, errCode = 'ENOENT') {
|
|
133
|
+
export function resolveUniqueEnoentRedirect(workDir, missingPath, errCode = 'ENOENT', cache = null) {
|
|
116
134
|
if (!NOT_FOUND_CODES.has(String(errCode || 'ENOENT'))) return null;
|
|
117
|
-
const suffixHit =
|
|
135
|
+
const suffixHit = cachedSuffixStrip(workDir, missingPath, cache);
|
|
118
136
|
if (suffixHit) return suffixHit;
|
|
119
|
-
const elsewhere =
|
|
137
|
+
const elsewhere = cachedFileByBasename(workDir, missingPath, cache);
|
|
120
138
|
if (elsewhere.length === 1) return elsewhere[0];
|
|
121
139
|
if (isDirectoryPathGuess(missingPath)) {
|
|
122
140
|
const dirHits = findDirectoryByBasename(workDir, missingPath, { limit: 3 });
|
|
@@ -136,19 +154,20 @@ export async function tryReadFamilyEnoentRedirect({
|
|
|
136
154
|
errCode,
|
|
137
155
|
options,
|
|
138
156
|
rerun,
|
|
157
|
+
cache = null,
|
|
139
158
|
}) {
|
|
140
159
|
if (options?._enoentRedirectFrom) return null;
|
|
141
|
-
const target = resolveUniqueEnoentRedirect(workDir, resolvedPath, errCode);
|
|
160
|
+
const target = resolveUniqueEnoentRedirect(workDir, resolvedPath, errCode, cache);
|
|
142
161
|
if (!target) return null;
|
|
143
162
|
const body = await rerun(target, { ...options, _enoentRedirectFrom: resolvedPath });
|
|
144
163
|
const shown = requestedPath ?? resolvedPath;
|
|
145
164
|
return redirectedFromPrefix(shown) + body;
|
|
146
165
|
}
|
|
147
166
|
|
|
148
|
-
export function buildNotFoundHint(workDir, missingPath, actionVerb, errCode = 'ENOENT') {
|
|
167
|
+
export function buildNotFoundHint(workDir, missingPath, actionVerb, errCode = 'ENOENT', cache = null) {
|
|
149
168
|
if (!NOT_FOUND_CODES.has(String(errCode || 'ENOENT'))) return '';
|
|
150
|
-
if (resolveUniqueEnoentRedirect(workDir, missingPath, errCode)) return '';
|
|
151
|
-
const elsewhere =
|
|
169
|
+
if (resolveUniqueEnoentRedirect(workDir, missingPath, errCode, cache)) return '';
|
|
170
|
+
const elsewhere = cachedFileByBasename(workDir, missingPath, cache);
|
|
152
171
|
if (elsewhere.length) {
|
|
153
172
|
return ` Not found at this path; the same filename exists at: ${elsewhere.map((p) => `"${normalizeOutputPath(p)}"`).join(', ')}. ${actionVerb} that path directly.`;
|
|
154
173
|
}
|
|
@@ -211,15 +230,20 @@ export function uncRefusalMessage(toolName, original, resolved) {
|
|
|
211
230
|
return `Error: ${toolName} refuses UNC/SMB path ${JSON.stringify(shown)}; remote share access is blocked to prevent NTLM credential leaks`;
|
|
212
231
|
}
|
|
213
232
|
|
|
214
|
-
export function basePathDiagnostic(basePaths, workDir) {
|
|
233
|
+
export function basePathDiagnostic(basePaths, workDir, statCache = null) {
|
|
215
234
|
return basePaths.map((basePath) => {
|
|
216
235
|
const resolved = resolveSearchScope(basePath, workDir);
|
|
217
|
-
|
|
218
|
-
|
|
236
|
+
// Reuse the caller's call-scoped stat cache (glob preflight + per-group
|
|
237
|
+
// rg runs already stat'd this same resolved root) instead of re-stating.
|
|
238
|
+
let st = null;
|
|
239
|
+
let err = null;
|
|
240
|
+
const cached = statCache && statCache.get(resolved);
|
|
241
|
+
if (cached) { st = cached.st; err = cached.err; }
|
|
242
|
+
else { try { st = statSync(resolved); } catch (e) { err = e; } }
|
|
243
|
+
if (!err) {
|
|
219
244
|
return `${normalizeOutputPath(basePath)}: ${st.isDirectory() ? 'path exists (dir)' : 'path exists (file)'}`;
|
|
220
|
-
} catch (err) {
|
|
221
|
-
return `${normalizeOutputPath(basePath)}: path does not exist (${err?.code || 'ENOENT'})`
|
|
222
|
-
+ finalizeReadFamilyEnoentTail(buildNotFoundHint(workDir, resolved, 'Search', err?.code), basePath, err?.code);
|
|
223
245
|
}
|
|
246
|
+
return `${normalizeOutputPath(basePath)}: path does not exist (${err?.code || 'ENOENT'})`
|
|
247
|
+
+ finalizeReadFamilyEnoentTail(buildNotFoundHint(workDir, resolved, 'Search', err?.code), basePath, err?.code);
|
|
224
248
|
}).join('; ');
|
|
225
249
|
}
|