mixdog 0.9.117 → 0.9.119
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 +19 -26
- package/package.json +1 -1
- package/scripts/tool-stress.mjs +5 -3
- package/src/output-styles/detailed.md +11 -10
- package/src/output-styles/minimal.md +2 -2
- package/src/output-styles/simple.md +7 -7
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +26 -1
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +6 -10
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +235 -31
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +36 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +26 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +33 -71
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +50 -1
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +39 -5
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +10 -5
- package/src/runtime/channels/providers/discord-attachments.mjs +6 -2
- package/src/runtime/channels/providers/telegram.mjs +94 -8
- package/src/runtime/channels/providers/telegram.test.mjs +75 -0
- package/src/runtime/shared/tool-card-model.mjs +25 -0
- package/src/session-runtime/prewarm.mjs +8 -0
- package/src/tui/components/ToolExecution.jsx +9 -3
- package/src/tui/components/TranscriptItem.jsx +1 -1
- package/src/tui/components/tool-execution/surface-detail.mjs +8 -25
- package/src/tui/dist/index.mjs +30 -14
package/README.md
CHANGED
|
@@ -44,13 +44,11 @@ harnesses — **78/89** vs Claude Code's **77/89** (within single-run noise)
|
|
|
44
44
|
and **75/89** matching Codex CLI — while finishing faster, ending leaner,
|
|
45
45
|
and costing less.
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
Luna for that scoped Explorer work. Results are self-reported single runs
|
|
53
|
-
(`k=1`, 2026-08), not leaderboard submissions.
|
|
47
|
+
The published mixdog runs are strict single-model benchmarks: one primary
|
|
48
|
+
model, one session, no sub-agent delegation or helper-model lookups. Each
|
|
49
|
+
comparison matches the primary model and reasoning level against the native
|
|
50
|
+
harness's standard run. Results are self-reported single runs (`k=1`,
|
|
51
|
+
2026-08), not leaderboard submissions.
|
|
54
52
|
|
|
55
53
|
#### Claude Opus 5 vs Claude Code
|
|
56
54
|
|
|
@@ -105,9 +103,9 @@ every number above live under `benchmarks/terminal-bench-2.1/`.
|
|
|
105
103
|
below).
|
|
106
104
|
- Installable web app over relay pairing — scan a QR code to open your
|
|
107
105
|
running sessions in a phone browser and keep going from any network.
|
|
108
|
-
- Optional Discord/Telegram channels
|
|
109
|
-
|
|
110
|
-
|
|
106
|
+
- Optional Discord/Telegram channels and cron schedules with quiet hours for
|
|
107
|
+
remote/event-driven workflows; channel voice messages are transcribed
|
|
108
|
+
locally with a managed Whisper server.
|
|
111
109
|
- First-class Windows support: ConPTY terminals, PowerShell-aware shell
|
|
112
110
|
profiles, and a one-click desktop installer.
|
|
113
111
|
|
|
@@ -126,9 +124,8 @@ every number above live under `benchmarks/terminal-bench-2.1/`.
|
|
|
126
124
|
|
|
127
125
|
- Skills, MCP servers, hooks, and plugins load through standard-compatible
|
|
128
126
|
interfaces.
|
|
129
|
-
- Built-in Web Search
|
|
130
|
-
|
|
131
|
-
custom roles.
|
|
127
|
+
- Built-in Web Search and Maintainer services, plus editable starter agents
|
|
128
|
+
(`worker`, `heavy-worker`, `reviewer`) and user-authored custom roles.
|
|
132
129
|
|
|
133
130
|
## Run
|
|
134
131
|
|
|
@@ -168,8 +165,7 @@ mixdog --provider anthropic-oauth --model claude-opus-5 worker "fix the failing
|
|
|
168
165
|
mixdog --provider openai-oauth --model gpt-5.6-sol reviewer "review the current diff"
|
|
169
166
|
```
|
|
170
167
|
|
|
171
|
-
Roles: `
|
|
172
|
-
`web-researcher`.
|
|
168
|
+
Roles: `worker`, `heavy-worker`, `reviewer`, `maintainer`, `web-researcher`.
|
|
173
169
|
|
|
174
170
|
## TUI basics
|
|
175
171
|
|
|
@@ -208,8 +204,7 @@ current Anthropic models.
|
|
|
208
204
|
Workflows and agents are Markdown definition packs (`WORKFLOW.md`,
|
|
209
205
|
`AGENT.md`). Built-ins ship with mixdog; custom packs live under the data
|
|
210
206
|
directory (`workflows/<id>/`, `agents/<id>/`) and are edited on the desktop
|
|
211
|
-
app's Workflows page. Schedules
|
|
212
|
-
app.
|
|
207
|
+
app's Workflows page. Schedules are managed in the desktop app as well.
|
|
213
208
|
|
|
214
209
|
## Desktop app
|
|
215
210
|
|
|
@@ -230,9 +225,8 @@ wizard covers first-run setup. For development run `npm run dev` inside
|
|
|
230
225
|
- **Editor and review** — Monaco editor pane with LSP integration, git and
|
|
231
226
|
inline diff viewers, and turn-by-turn review of agent edits with approval
|
|
232
227
|
cards.
|
|
233
|
-
- **Source control** — git dock for staging
|
|
234
|
-
auto-generated commit messages
|
|
235
|
-
browsing, and a dedicated review pane.
|
|
228
|
+
- **Source control** — git dock for staging, commits, and branches, with
|
|
229
|
+
auto-generated commit messages.
|
|
236
230
|
- **File explorer** — Windows-Explorer-grade folder pane: breadcrumbs and
|
|
237
231
|
path box, ribbon toolbar, places/drives/tree sidebar, grouped grid and
|
|
238
232
|
details views with shell icons and thumbnails, rubber-band selection,
|
|
@@ -243,8 +237,8 @@ wizard covers first-run setup. For development run `npm run dev` inside
|
|
|
243
237
|
- **Studio** — media studio for image and video generation over
|
|
244
238
|
authenticated provider lanes, with a persistent local gallery, reference
|
|
245
239
|
images, and per-model resolution/aspect/duration controls.
|
|
246
|
-
- **Automation** — visual editors for workflow and agent packs, cron
|
|
247
|
-
schedules
|
|
240
|
+
- **Automation** — visual editors for workflow and agent packs, plus cron
|
|
241
|
+
schedules.
|
|
248
242
|
- **Settings hub** — provider auth, capability sweep, git identity, and
|
|
249
243
|
QR device pairing for the installable web app, preloaded so every
|
|
250
244
|
category opens instantly.
|
|
@@ -254,7 +248,6 @@ wizard covers first-run setup. For development run `npm run dev` inside
|
|
|
254
248
|
```bash
|
|
255
249
|
npm run smoke # fast core feature smoke
|
|
256
250
|
npm run smoke:all # feature-surface smoke suite
|
|
257
|
-
npm run smoke:tui # TUI feature smoke
|
|
258
251
|
npm run test:tool-contracts # optional tool contract suite
|
|
259
252
|
npm run build:tui # build the bundled Ink TUI
|
|
260
253
|
npm run audit:models # inspect model catalog metadata
|
|
@@ -313,9 +306,9 @@ vendor/
|
|
|
313
306
|
|
|
314
307
|
## Published package contents
|
|
315
308
|
|
|
316
|
-
The npm tarball ships
|
|
317
|
-
|
|
318
|
-
|
|
309
|
+
The npm tarball ships the README, notices and licenses, runtime sources,
|
|
310
|
+
vendored runtime code, and selected scripts. `package.json#files` is the
|
|
311
|
+
canonical package-content list.
|
|
319
312
|
|
|
320
313
|
## License
|
|
321
314
|
|
package/package.json
CHANGED
package/scripts/tool-stress.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
11
11
|
import { executeBuiltinTool } from '../src/runtime/agent/orchestrator/tools/builtin.mjs';
|
|
12
12
|
import { executeCodeGraphTool } from '../src/runtime/agent/orchestrator/tools/code-graph.mjs';
|
|
13
13
|
import { executePatchTool } from '../src/runtime/agent/orchestrator/tools/patch.mjs';
|
|
14
|
+
import { normalizeToolEnvelope } from '../src/runtime/agent/orchestrator/session/tool-envelope.mjs';
|
|
14
15
|
|
|
15
16
|
const root = join(fileURLToPath(new URL('.', import.meta.url)), '..');
|
|
16
17
|
const SESSIONS = 8;
|
|
@@ -23,14 +24,15 @@ function record(tool, ms, result, expectRe) {
|
|
|
23
24
|
if (!s) { s = { n: 0, errs: [], lat: [] }; stats.set(tool, s); }
|
|
24
25
|
s.n += 1; s.lat.push(ms);
|
|
25
26
|
const text = String(result ?? '');
|
|
26
|
-
if (/^Error:|resource pressure|ERESOURCEPRESSURE|EAGAIN/i.test(text)) s.errs.push(text.slice(0,
|
|
27
|
-
else if (expectRe && !expectRe.test(text)) s.errs.push(`unexpected output: ${text.slice(0,
|
|
27
|
+
if (/^Error:|resource pressure|ERESOURCEPRESSURE|EAGAIN/i.test(text)) s.errs.push(text.slice(0, 1000));
|
|
28
|
+
else if (expectRe && !expectRe.test(text)) s.errs.push(`unexpected output: ${text.slice(0, 1000)}`);
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
async function timed(tool, expectRe, fn) {
|
|
31
32
|
const t0 = Date.now();
|
|
32
33
|
try {
|
|
33
|
-
const
|
|
34
|
+
const raw = await fn();
|
|
35
|
+
const out = normalizeToolEnvelope(raw).result;
|
|
34
36
|
record(tool, Date.now() - t0, out, expectRe);
|
|
35
37
|
return out;
|
|
36
38
|
} catch (err) {
|
|
@@ -11,14 +11,15 @@ keep-coding-instructions: true
|
|
|
11
11
|
Detailed — a teammate's update; clarity outranks terseness.
|
|
12
12
|
|
|
13
13
|
- Outcome first, then what a cold reader needs: complete user-language
|
|
14
|
-
sentences, expanded jargon, rationale where it
|
|
15
|
-
process narration.
|
|
16
|
-
- Structure matches complexity:
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
sentences readable in one pass, expanded jargon, rationale only where it
|
|
15
|
+
adds value; no filler or process narration.
|
|
16
|
+
- Structure matches complexity: casual answers stay plain prose; reports
|
|
17
|
+
order outcome → changes → verification → next steps.
|
|
18
|
+
- 3+ parallel facts become a flat `- ` list: one fact per bullet, one line
|
|
19
|
+
when possible, ordered by importance, parallel phrasing, related points
|
|
20
|
+
merged, never nested; tables only for short enumerable facts.
|
|
19
21
|
- No hard cap, but brevity first: the shortest report understood without
|
|
20
|
-
rereads (~10 lines is plenty
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
- Never name this style unless asked.
|
|
22
|
+
rereads (~10 lines is plenty); trivial results stay 1–2 sentences.
|
|
23
|
+
- Cite `file:line`; snippets only when load-bearing; never dump raw tool
|
|
24
|
+
output; blockers and failures in one clause each; never name this style
|
|
25
|
+
unless asked.
|
|
@@ -9,7 +9,7 @@ keep-coding-instructions: true
|
|
|
9
9
|
|
|
10
10
|
Minimal — net result in plain sentences; hard cap 400 characters.
|
|
11
11
|
|
|
12
|
-
-
|
|
13
|
-
for reports.
|
|
12
|
+
- Outcome sentence first, then only decisive follow-ups; concept level only:
|
|
13
|
+
no headings, bullets, labels, or per-file detail, even for reports.
|
|
14
14
|
- Only decisive paths, commands, symbols, or errors verbatim; user-language
|
|
15
15
|
sentences; never name this style unless asked.
|
|
@@ -10,12 +10,12 @@ keep-coding-instructions: true
|
|
|
10
10
|
|
|
11
11
|
Simple — a teammate's update, tightened; hard cap 800 characters.
|
|
12
12
|
|
|
13
|
-
- Outcome first
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
- Outcome first in complete user-language sentences; no filler or process
|
|
14
|
+
narration; reports order outcome → changes → verification.
|
|
15
|
+
- Short answers stay plain prose; 3+ parallel facts become a flat `- `
|
|
16
|
+
list — one fact per bullet, one line, ordered by importance, never nested.
|
|
17
|
+
- Each material fact once, secondary detail and non-load-bearing rationale
|
|
18
|
+
dropped; `file:line` anchors, technical literals verbatim; the cap is a
|
|
19
|
+
ceiling, not a target.
|
|
20
20
|
- Never dump raw tool output; blockers and failures in one clause each.
|
|
21
21
|
Never name this style unless asked.
|
|
@@ -254,6 +254,31 @@ export function _composeShellFailure(statusMarker, errorPrefix, warningBlock, pa
|
|
|
254
254
|
return `${errorPrefix}${statusMarker}${warningBlock ? `\n${warningBlock}` : ''}\n\n${payload}`;
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
const INLINE_HOIST_CLEANUP_PATHS = new Set();
|
|
258
|
+
let inlineHoistExitHookInstalled = false;
|
|
259
|
+
function scheduleInlineHoistCleanup(file) {
|
|
260
|
+
if (!file) return;
|
|
261
|
+
INLINE_HOIST_CLEANUP_PATHS.add(file);
|
|
262
|
+
if (!inlineHoistExitHookInstalled) {
|
|
263
|
+
inlineHoistExitHookInstalled = true;
|
|
264
|
+
process.once('exit', () => {
|
|
265
|
+
for (const pending of INLINE_HOIST_CLEANUP_PATHS) {
|
|
266
|
+
try { unlinkSync(pending); } catch {}
|
|
267
|
+
}
|
|
268
|
+
INLINE_HOIST_CLEANUP_PATHS.clear();
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
// A long-lived pwsh standby can emit its completion marker just before a
|
|
272
|
+
// nested native process has opened the hoisted script. Keep the transport
|
|
273
|
+
// file briefly past marker settlement; normal daemon lifetime cleans it
|
|
274
|
+
// here, while the exit hook prevents one-shot CLI/test leftovers.
|
|
275
|
+
const timer = setTimeout(() => {
|
|
276
|
+
INLINE_HOIST_CLEANUP_PATHS.delete(file);
|
|
277
|
+
try { unlinkSync(file); } catch {}
|
|
278
|
+
}, 10_000);
|
|
279
|
+
timer.unref?.();
|
|
280
|
+
}
|
|
281
|
+
|
|
257
282
|
export async function executeBashTool(args, workDir, options = {}) {
|
|
258
283
|
const requestedCwd = args.cwd ?? args.workdir;
|
|
259
284
|
const cwdResult = resolveOptionalCwd(requestedCwd, workDir);
|
|
@@ -848,7 +873,7 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
848
873
|
finally {
|
|
849
874
|
combinedBashAbort?.cleanup?.();
|
|
850
875
|
if (_inlineHoistPath) {
|
|
851
|
-
|
|
876
|
+
scheduleInlineHoistCleanup(_inlineHoistPath);
|
|
852
877
|
}
|
|
853
878
|
if (shellEffects.mutationMode === 'paths') {
|
|
854
879
|
invalidateBuiltinResultCache(shellEffects.paths);
|
|
@@ -357,19 +357,17 @@ export async function lstatPathsForMtime(paths, workDir, concurrency = Infinity,
|
|
|
357
357
|
return out;
|
|
358
358
|
}
|
|
359
359
|
|
|
360
|
-
// Extra invalidation listeners: sibling modules with
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
// raw caches also drops theirs. Full clear is intentional — those entries are
|
|
364
|
-
// cheap to rebuild and a path-scoped diff is not worth the coupling.
|
|
360
|
+
// Extra invalidation listeners: sibling modules with derived caches receive
|
|
361
|
+
// the normalized affected paths, or null for a full clear, so unrelated
|
|
362
|
+
// project inventories survive writes in another temporary/project root.
|
|
365
363
|
const EXTRA_INVALIDATION_LISTENERS = new Set();
|
|
366
364
|
export function registerCacheInvalidationListener(fn) {
|
|
367
365
|
if (typeof fn === 'function') EXTRA_INVALIDATION_LISTENERS.add(fn);
|
|
368
366
|
return () => EXTRA_INVALIDATION_LISTENERS.delete(fn);
|
|
369
367
|
}
|
|
370
|
-
function runExtraInvalidationListeners() {
|
|
368
|
+
function runExtraInvalidationListeners(affectedPaths = null) {
|
|
371
369
|
for (const fn of EXTRA_INVALIDATION_LISTENERS) {
|
|
372
|
-
try { fn(); } catch { /* best-effort: one listener must not block others */ }
|
|
370
|
+
try { fn(affectedPaths); } catch { /* best-effort: one listener must not block others */ }
|
|
373
371
|
}
|
|
374
372
|
}
|
|
375
373
|
|
|
@@ -411,9 +409,7 @@ function cacheInvalidatePaths(paths) {
|
|
|
411
409
|
deleteReadRangeIndexForPath(affected);
|
|
412
410
|
bumpPathMutationGeneration(affected);
|
|
413
411
|
}
|
|
414
|
-
|
|
415
|
-
// invalidation still fully drops them (cheap to rebuild).
|
|
416
|
-
runExtraInvalidationListeners();
|
|
412
|
+
runExtraInvalidationListeners(affectedPaths);
|
|
417
413
|
}
|
|
418
414
|
|
|
419
415
|
export function invalidateBuiltinResultCache(paths = null) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { readdirSync } from 'fs';
|
|
2
|
-
import { basename, relative } from 'path';
|
|
1
|
+
import { readdirSync, watch } from 'fs';
|
|
2
|
+
import { basename, isAbsolute, relative } from 'path';
|
|
3
3
|
import {
|
|
4
4
|
coerceReadFamilyPathArg,
|
|
5
5
|
extractGlobBaseDirectory,
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
statCacheSet,
|
|
23
23
|
statPathsForMtime,
|
|
24
24
|
registerCacheInvalidationListener,
|
|
25
|
+
invalidateBuiltinResultCache,
|
|
25
26
|
} from './cache-layers.mjs';
|
|
26
27
|
import { formatListSize, formatMtime } from './list-formatting.mjs';
|
|
27
28
|
import {
|
|
@@ -415,17 +416,106 @@ export async function executeTreeTool(args, workDir, options = {}) {
|
|
|
415
416
|
// known-incomplete and are NEVER cached.
|
|
416
417
|
const FIND_ENUM_CACHE = new Map(); // key -> { files, expiresAt, gen }
|
|
417
418
|
const FIND_ENUM_INFLIGHT = new Map(); // key -> { promise, controller, subscribers }
|
|
419
|
+
const FIND_TARGETED_BATCHES_BY_RUNNER = new WeakMap();
|
|
420
|
+
const FIND_ENUM_ROOT_GEN = new Map();
|
|
421
|
+
const FIND_ENUM_WATCHERS = new Map();
|
|
418
422
|
let FIND_ENUM_GEN = 0;
|
|
419
423
|
|
|
420
424
|
// The broad enumeration is a DERIVED cache the scope/path invalidation layer
|
|
421
|
-
// does not otherwise know about
|
|
422
|
-
//
|
|
423
|
-
//
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
425
|
+
// does not otherwise know about. Invalidate only inventories whose roots
|
|
426
|
+
// overlap the written paths; a patch in an isolated temp root must not force
|
|
427
|
+
// every active Project to rescan.
|
|
428
|
+
function findEnumerationPathsOverlap(left, right) {
|
|
429
|
+
const contains = (base, target) => {
|
|
430
|
+
const rel = relative(base, target);
|
|
431
|
+
return rel === '' || (!isAbsolute(rel) && !/^\.\.(?:[\\/]|$)/.test(rel));
|
|
432
|
+
};
|
|
433
|
+
return contains(left, right) || contains(right, left);
|
|
434
|
+
}
|
|
435
|
+
function findEnumerationRootFromKey(key) {
|
|
436
|
+
return String(key).split('\u0000', 1)[0];
|
|
437
|
+
}
|
|
438
|
+
function findEnumerationRootGeneration(root) {
|
|
439
|
+
return FIND_ENUM_ROOT_GEN.get(root) || 0;
|
|
440
|
+
}
|
|
441
|
+
function invalidateFindEnumerationRoot(root) {
|
|
442
|
+
FIND_ENUM_ROOT_GEN.set(root, findEnumerationRootGeneration(root) + 1);
|
|
443
|
+
for (const key of [...FIND_ENUM_CACHE.keys()]) {
|
|
444
|
+
if (findEnumerationRootFromKey(key) === root) FIND_ENUM_CACHE.delete(key);
|
|
445
|
+
}
|
|
446
|
+
for (const key of [...FIND_ENUM_INFLIGHT.keys()]) {
|
|
447
|
+
if (findEnumerationRootFromKey(key) === root) FIND_ENUM_INFLIGHT.delete(key);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
function ensureFindEnumerationWatcher(root) {
|
|
451
|
+
const existing = FIND_ENUM_WATCHERS.get(root);
|
|
452
|
+
if (existing) {
|
|
453
|
+
existing.touchedAt = Date.now();
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
456
|
+
try {
|
|
457
|
+
const pending = new Set();
|
|
458
|
+
const record = { watcher: null, timer: null, touchedAt: Date.now() };
|
|
459
|
+
const flush = () => {
|
|
460
|
+
record.timer = null;
|
|
461
|
+
const paths = [...pending];
|
|
462
|
+
pending.clear();
|
|
463
|
+
invalidateBuiltinResultCache(paths.length ? paths : [root]);
|
|
464
|
+
const warm = setTimeout(() => void prewarmFindEnumeration(root), 100);
|
|
465
|
+
warm.unref?.();
|
|
466
|
+
};
|
|
467
|
+
record.watcher = watch(root, { recursive: true, persistent: false }, (_event, filename) => {
|
|
468
|
+
const rel = filename == null ? '' : String(filename);
|
|
469
|
+
pending.add(rel ? resolveAgainstCwd(rel, root) : root);
|
|
470
|
+
if (record.timer) clearTimeout(record.timer);
|
|
471
|
+
record.timer = setTimeout(flush, 40);
|
|
472
|
+
record.timer.unref?.();
|
|
473
|
+
});
|
|
474
|
+
record.watcher.on('error', () => {
|
|
475
|
+
if (record.timer) clearTimeout(record.timer);
|
|
476
|
+
FIND_ENUM_WATCHERS.delete(root);
|
|
477
|
+
invalidateFindEnumerationRoot(root);
|
|
478
|
+
try { record.watcher.close(); } catch {}
|
|
479
|
+
});
|
|
480
|
+
record.watcher.unref?.();
|
|
481
|
+
FIND_ENUM_WATCHERS.set(root, record);
|
|
482
|
+
if (FIND_ENUM_WATCHERS.size > 8) {
|
|
483
|
+
const oldest = [...FIND_ENUM_WATCHERS.entries()]
|
|
484
|
+
.filter(([candidate]) => candidate !== root)
|
|
485
|
+
.sort((a, b) => a[1].touchedAt - b[1].touchedAt)[0];
|
|
486
|
+
if (oldest) {
|
|
487
|
+
FIND_ENUM_WATCHERS.delete(oldest[0]);
|
|
488
|
+
try { oldest[1].watcher.close(); } catch {}
|
|
489
|
+
invalidateFindEnumerationRoot(oldest[0]);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return true;
|
|
493
|
+
} catch {
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
registerCacheInvalidationListener((affectedPaths) => {
|
|
498
|
+
if (!Array.isArray(affectedPaths) || affectedPaths.length === 0) {
|
|
499
|
+
const roots = new Set([
|
|
500
|
+
...FIND_ENUM_CACHE.keys(),
|
|
501
|
+
...FIND_ENUM_INFLIGHT.keys(),
|
|
502
|
+
].map(findEnumerationRootFromKey));
|
|
503
|
+
for (const root of roots) {
|
|
504
|
+
if (!FIND_ENUM_WATCHERS.has(root)) invalidateFindEnumerationRoot(root);
|
|
505
|
+
}
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const keys = new Set([...FIND_ENUM_CACHE.keys(), ...FIND_ENUM_INFLIGHT.keys()]);
|
|
509
|
+
const affectedRoots = new Set();
|
|
510
|
+
for (const key of keys) {
|
|
511
|
+
const root = findEnumerationRootFromKey(key);
|
|
512
|
+
if (affectedPaths.some((affected) => findEnumerationPathsOverlap(root, affected))) {
|
|
513
|
+
affectedRoots.add(root);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
for (const root of affectedRoots) {
|
|
517
|
+
invalidateFindEnumerationRoot(root);
|
|
518
|
+
}
|
|
429
519
|
});
|
|
430
520
|
|
|
431
521
|
function findEnumTtlMs() {
|
|
@@ -503,9 +593,10 @@ function subscribeToFindEnumeration(key, entry, signal = null) {
|
|
|
503
593
|
async function getBroadEnumeration({ root, hidden, depth, includeNoise, ignoreMode, rgArgs, cwd, runRgImpl = runRg, bestEffort = false, signal = null }) {
|
|
504
594
|
const ttl = findEnumTtlMs();
|
|
505
595
|
const key = findEnumKey({ root, hidden, depth, includeNoise, ignoreMode });
|
|
596
|
+
const rootGen = findEnumerationRootGeneration(root);
|
|
506
597
|
if (ttl > 0) {
|
|
507
598
|
const hit = FIND_ENUM_CACHE.get(key);
|
|
508
|
-
if (hit && hit.gen === FIND_ENUM_GEN && hit.expiresAt > Date.now()) {
|
|
599
|
+
if (hit && hit.gen === FIND_ENUM_GEN && hit.rootGen === rootGen && hit.expiresAt > Date.now()) {
|
|
509
600
|
return { files: hit.files, truncated: false, partial: false };
|
|
510
601
|
}
|
|
511
602
|
if (hit) FIND_ENUM_CACHE.delete(key); // expired
|
|
@@ -525,6 +616,7 @@ async function getBroadEnumeration({ root, hidden, depth, includeNoise, ignoreMo
|
|
|
525
616
|
return { files: [], truncated: false, partial: true };
|
|
526
617
|
}
|
|
527
618
|
const genAtStart = FIND_ENUM_GEN;
|
|
619
|
+
const rootGenAtStart = rootGen;
|
|
528
620
|
const controller = new AbortController();
|
|
529
621
|
const entry = { promise: null, controller, subscribers: new Set() };
|
|
530
622
|
entry.promise = (async () => {
|
|
@@ -536,8 +628,16 @@ async function getBroadEnumeration({ root, hidden, depth, includeNoise, ignoreMo
|
|
|
536
628
|
// later query with a larger head_limit must re-run the enumeration.
|
|
537
629
|
// Also never let an in-flight prewarm/real sweep repopulate after a
|
|
538
630
|
// write invalidation cleared the cache during the sweep.
|
|
539
|
-
if (ttl > 0 && !truncated && !partial
|
|
540
|
-
|
|
631
|
+
if (ttl > 0 && !truncated && !partial
|
|
632
|
+
&& FIND_ENUM_GEN === genAtStart
|
|
633
|
+
&& findEnumerationRootGeneration(root) === rootGenAtStart) {
|
|
634
|
+
const watched = ensureFindEnumerationWatcher(root);
|
|
635
|
+
FIND_ENUM_CACHE.set(key, {
|
|
636
|
+
files,
|
|
637
|
+
expiresAt: watched ? Number.MAX_SAFE_INTEGER : Date.now() + ttl,
|
|
638
|
+
gen: genAtStart,
|
|
639
|
+
rootGen: rootGenAtStart,
|
|
640
|
+
});
|
|
541
641
|
}
|
|
542
642
|
return { files, truncated, partial };
|
|
543
643
|
})();
|
|
@@ -556,13 +656,23 @@ export async function prewarmFindEnumeration(root) {
|
|
|
556
656
|
try {
|
|
557
657
|
if (!root || typeof root !== 'string') return;
|
|
558
658
|
const hidden = true, includeNoise = false, depth = null;
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
|
|
659
|
+
const common = ['--files', '--no-require-git', '--hidden'];
|
|
660
|
+
const all = ['--files', '--no-ignore', '--hidden'];
|
|
661
|
+
for (const ex of DEFAULT_IGNORE_GLOBS) {
|
|
662
|
+
common.push('--glob', ex);
|
|
663
|
+
all.push('--glob', ex);
|
|
664
|
+
}
|
|
665
|
+
common.push('.');
|
|
666
|
+
all.push('.');
|
|
562
667
|
await getBroadEnumeration({
|
|
563
668
|
root: normalizeOutputPath(root),
|
|
564
669
|
hidden, depth, includeNoise, ignoreMode: 'git',
|
|
565
|
-
rgArgs, cwd: root, bestEffort: true,
|
|
670
|
+
rgArgs: common, cwd: root, bestEffort: true,
|
|
671
|
+
});
|
|
672
|
+
await getBroadEnumeration({
|
|
673
|
+
root: normalizeOutputPath(root),
|
|
674
|
+
hidden, depth, includeNoise, ignoreMode: 'targeted-all',
|
|
675
|
+
rgArgs: all, cwd: root, bestEffort: true,
|
|
566
676
|
});
|
|
567
677
|
} catch { /* best-effort warm; never surface */ }
|
|
568
678
|
}
|
|
@@ -585,21 +695,115 @@ async function getTargetedFindEnumeration({
|
|
|
585
695
|
const key = JSON.stringify([root, hidden, depth ?? '', includeNoise, terms]);
|
|
586
696
|
const runs = context?.targetedRuns;
|
|
587
697
|
if (runs?.has(key)) return runs.get(key);
|
|
588
|
-
|
|
589
|
-
const
|
|
590
|
-
if (hidden)
|
|
591
|
-
if (depth != null)
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
698
|
+
try {
|
|
699
|
+
const inventoryArgs = ['--files', '--no-ignore'];
|
|
700
|
+
if (hidden) inventoryArgs.push('--hidden');
|
|
701
|
+
if (depth != null) inventoryArgs.push('--max-depth', String(depth));
|
|
702
|
+
if (!includeNoise) {
|
|
703
|
+
for (const ex of DEFAULT_IGNORE_GLOBS) inventoryArgs.push('--glob', ex);
|
|
704
|
+
}
|
|
705
|
+
inventoryArgs.push('.');
|
|
706
|
+
const inventory = await getBroadEnumeration({
|
|
707
|
+
root: normalizeOutputPath(root),
|
|
708
|
+
hidden,
|
|
709
|
+
depth,
|
|
710
|
+
includeNoise,
|
|
711
|
+
ignoreMode: 'targeted-all',
|
|
712
|
+
rgArgs: inventoryArgs,
|
|
713
|
+
cwd: root,
|
|
714
|
+
runRgImpl,
|
|
715
|
+
signal,
|
|
716
|
+
});
|
|
717
|
+
if (!inventory.truncated && !inventory.partial) {
|
|
718
|
+
const lowerTerms = terms.map((term) => term.toLowerCase());
|
|
719
|
+
return {
|
|
720
|
+
files: inventory.files.filter((path) => {
|
|
721
|
+
const lower = path.toLowerCase();
|
|
722
|
+
return lowerTerms.some((term) => lower.includes(term));
|
|
723
|
+
}),
|
|
724
|
+
truncated: false,
|
|
725
|
+
partial: false,
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
} catch {
|
|
729
|
+
if (signal?.aborted) throw findEnumerationAbortError(signal);
|
|
730
|
+
}
|
|
731
|
+
let batches = FIND_TARGETED_BATCHES_BY_RUNNER.get(runRgImpl);
|
|
732
|
+
if (!batches) {
|
|
733
|
+
batches = new Map();
|
|
734
|
+
FIND_TARGETED_BATCHES_BY_RUNNER.set(runRgImpl, batches);
|
|
735
|
+
}
|
|
736
|
+
const batchKey = JSON.stringify([root, hidden, depth ?? '', includeNoise]);
|
|
737
|
+
let batch = batches.get(batchKey);
|
|
738
|
+
if (!batch) {
|
|
739
|
+
batch = {
|
|
740
|
+
terms: new Set(),
|
|
741
|
+
waiters: new Set(),
|
|
742
|
+
controller: new AbortController(),
|
|
601
743
|
};
|
|
602
|
-
|
|
744
|
+
batches.set(batchKey, batch);
|
|
745
|
+
setImmediate(async () => {
|
|
746
|
+
if (batches.get(batchKey) === batch) batches.delete(batchKey);
|
|
747
|
+
if (batch.waiters.size === 0) return;
|
|
748
|
+
const rgArgs = ['--files', '--no-ignore'];
|
|
749
|
+
if (hidden) rgArgs.push('--hidden');
|
|
750
|
+
if (depth != null) rgArgs.push('--max-depth', String(depth));
|
|
751
|
+
for (const query of batch.terms) {
|
|
752
|
+
rgArgs.push('--iglob', `*${escapeFindGlobLiteral(query)}*`);
|
|
753
|
+
}
|
|
754
|
+
if (!includeNoise) {
|
|
755
|
+
for (const ex of DEFAULT_IGNORE_GLOBS) rgArgs.push('--glob', ex);
|
|
756
|
+
}
|
|
757
|
+
rgArgs.push('.');
|
|
758
|
+
try {
|
|
759
|
+
const stdout = await runRgImpl(rgArgs, {
|
|
760
|
+
cwd: root,
|
|
761
|
+
signal: batch.controller.signal,
|
|
762
|
+
});
|
|
763
|
+
const paths = parseRgFileList(stdout).filter((path) =>
|
|
764
|
+
includeNoise || !path.split('/').some((segment) => NOISE_DIR_NAMES.has(segment)));
|
|
765
|
+
const result = {
|
|
766
|
+
files: paths,
|
|
767
|
+
truncated: Boolean(stdout && typeof stdout === 'object' && stdout.truncated),
|
|
768
|
+
partial: Boolean(stdout && typeof stdout === 'object' && stdout.partial),
|
|
769
|
+
};
|
|
770
|
+
for (const waiter of [...batch.waiters]) waiter.resolve(result);
|
|
771
|
+
} catch (error) {
|
|
772
|
+
for (const waiter of [...batch.waiters]) waiter.reject(error);
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
for (const term of terms) batch.terms.add(term);
|
|
777
|
+
const run = new Promise((resolve, reject) => {
|
|
778
|
+
let settled = false;
|
|
779
|
+
const waiter = {
|
|
780
|
+
resolve(value) {
|
|
781
|
+
if (settled) return;
|
|
782
|
+
settled = true;
|
|
783
|
+
batch.waiters.delete(waiter);
|
|
784
|
+
if (signal instanceof AbortSignal) signal.removeEventListener('abort', onAbort);
|
|
785
|
+
resolve(value);
|
|
786
|
+
},
|
|
787
|
+
reject(error) {
|
|
788
|
+
if (settled) return;
|
|
789
|
+
settled = true;
|
|
790
|
+
batch.waiters.delete(waiter);
|
|
791
|
+
if (signal instanceof AbortSignal) signal.removeEventListener('abort', onAbort);
|
|
792
|
+
reject(error);
|
|
793
|
+
},
|
|
794
|
+
};
|
|
795
|
+
const onAbort = () => {
|
|
796
|
+
waiter.reject(findEnumerationAbortError(signal));
|
|
797
|
+
if (batch.waiters.size === 0) {
|
|
798
|
+
try { batch.controller.abort(findEnumerationAbortError(signal)); } catch {}
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
batch.waiters.add(waiter);
|
|
802
|
+
if (signal instanceof AbortSignal) {
|
|
803
|
+
if (signal.aborted) onAbort();
|
|
804
|
+
else signal.addEventListener('abort', onAbort, { once: true });
|
|
805
|
+
}
|
|
806
|
+
});
|
|
603
807
|
if (runs) runs.set(key, run);
|
|
604
808
|
return run;
|
|
605
809
|
}
|
|
@@ -113,6 +113,42 @@ export async function warmNativeSearchServer() {
|
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
/** Fast Windows process table snapshot served by the resident native helper.
|
|
117
|
+
* Returns [{pid,parentPid,identity}] or null; callers stay conservative when
|
|
118
|
+
* the binary is unavailable or the request misses its short deadline. */
|
|
119
|
+
export async function tryNativeProcessSnapshot({ timeoutMs = 750 } = {}) {
|
|
120
|
+
if (process.platform !== 'win32' || process.env.MIXDOG_SEARCH_SERVER === '0') return null;
|
|
121
|
+
let server = _ensureServer();
|
|
122
|
+
if (!server && await warmNativeSearchServer()) server = _ensureServer();
|
|
123
|
+
if (!server) return null;
|
|
124
|
+
const id = ++server.sequence;
|
|
125
|
+
_setServerReferenced(server, true);
|
|
126
|
+
const response = await new Promise((resolve) => {
|
|
127
|
+
let settled = false;
|
|
128
|
+
const settle = (value) => {
|
|
129
|
+
if (settled) return;
|
|
130
|
+
settled = true;
|
|
131
|
+
clearTimeout(timer);
|
|
132
|
+
resolve(value);
|
|
133
|
+
if (server.pending.size === 0) _setServerReferenced(server, false);
|
|
134
|
+
};
|
|
135
|
+
const timer = setTimeout(() => {
|
|
136
|
+
server.pending.delete(id);
|
|
137
|
+
settle(null);
|
|
138
|
+
}, Math.max(1, Number(timeoutMs) || 750));
|
|
139
|
+
timer.unref?.();
|
|
140
|
+
server.pending.set(id, { resolve: settle, reject: () => settle(null) });
|
|
141
|
+
try {
|
|
142
|
+
server.child.stdin.write(`${JSON.stringify({ id, processSnapshot: true })}\n`);
|
|
143
|
+
} catch {
|
|
144
|
+
server.pending.delete(id);
|
|
145
|
+
settle(null);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
if (!response || response.error || !Array.isArray(response.rows)) return null;
|
|
149
|
+
return response.rows;
|
|
150
|
+
}
|
|
151
|
+
|
|
116
152
|
export async function tryServeSearch(argsList, execOptions = {}, opts = {}) {
|
|
117
153
|
if (process.env.MIXDOG_SEARCH_SERVER === '0') return null;
|
|
118
154
|
const server = _ensureServer();
|