mixdog 0.9.134 → 0.9.136
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 -1
- package/scripts/tool-stress.mjs +8 -1
- package/src/agents/maintainer/agent.json +1 -1
- package/src/defaults/skills/setup/SKILL.md +8 -43
- package/src/headless-exec.mjs +26 -9
- package/src/headless-exec.test.mjs +55 -2
- package/src/help.mjs +0 -2
- package/src/rules/lead/01-general.md +0 -4
- package/src/rules/shared/01-tool.md +12 -4
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +30 -4
- package/src/runtime/agent/orchestrator/agent-trace-timing.test.mjs +57 -0
- package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.test.mjs +40 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +7 -0
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +5 -2
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +15 -6
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +25 -4
- package/src/runtime/agent/orchestrator/tools/builtin/edit-sequential-occupation.test.mjs +40 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-pattern-fanout.mjs +30 -7
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +150 -41
- package/src/runtime/agent/orchestrator/tools/builtin/local-search-telemetry.mjs +4 -0
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +60 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +18 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +10 -9
- package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +138 -19
- package/src/runtime/agent/orchestrator/tools/builtin/search-glob-top-k.test.mjs +39 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +7 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +80 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +82 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +3 -19
- package/src/runtime/agent/orchestrator/tools/env-scrub.test.mjs +25 -0
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +165 -17
- package/src/runtime/agent/orchestrator/tools/lib/shell-warm-standby.mjs +162 -0
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +36 -8
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +87 -0
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
- package/src/runtime/channels/lib/config.mjs +14 -101
- package/src/runtime/channels/lib/index-drop-trace.mjs +2 -5
- package/src/runtime/channels/lib/owned-runtime.mjs +4 -80
- package/src/runtime/channels/lib/scheduler.mjs +75 -18
- package/src/runtime/channels/lib/status-snapshot.mjs +5 -68
- package/src/runtime/channels/lib/tool-dispatch.mjs +12 -55
- package/src/runtime/channels/lib/worker-ipc.mjs +1 -16
- package/src/runtime/channels/lib/worker-main.mjs +22 -307
- package/src/runtime/media/renditions.mjs +68 -5
- package/src/runtime/media/renditions.test.mjs +45 -0
- package/src/runtime/media/store.mjs +1 -0
- package/src/runtime/memory/index.mjs +4 -3
- package/src/runtime/memory/lib/cycle-scheduler.mjs +17 -6
- package/src/runtime/memory/lib/memory-cycle-packets.test.mjs +7 -1
- package/src/runtime/memory/lib/memory-cycle-requests.mjs +13 -0
- package/src/runtime/memory/lib/query-handlers.mjs +1 -0
- package/src/runtime/shared/config.mjs +8 -49
- package/src/runtime/shared/schedule-time.mjs +25 -0
- package/src/runtime/shared/schedule-time.test.mjs +29 -0
- package/src/runtime/shared/schedules-db.mjs +73 -1
- package/src/runtime/shared/tool-card-model.mjs +1 -1
- package/src/runtime/shared/transcript-writer.mjs +10 -17
- package/src/runtime/shared/update-checker.mjs +7 -2
- package/src/runtime/shared/update-checker.test.mjs +33 -0
- package/src/session-runtime/channel-config-api.mjs +3 -20
- package/src/session-runtime/config-lifecycle.mjs +1 -42
- package/src/session-runtime/lifecycle-api.mjs +10 -21
- package/src/session-runtime/model-route-api.mjs +0 -2
- package/src/session-runtime/model-settings-persist.test.mjs +0 -1
- package/src/session-runtime/prewarm.mjs +11 -7
- package/src/session-runtime/prewarm.test.mjs +0 -1
- package/src/session-runtime/runtime-core.mjs +20 -186
- package/src/session-runtime/session-lifecycle.mjs +4 -17
- package/src/session-runtime/session-transcript.mjs +54 -0
- package/src/session-runtime/session-turn-api.mjs +4 -44
- package/src/session-runtime/settings-api.mjs +0 -45
- package/src/session-runtime/tool-catalog-schema.mjs +1 -1
- package/src/session-runtime/tool-policy-surface.test.mjs +4 -0
- package/src/standalone/channel-admin.mjs +8 -150
- package/src/standalone/channel-transport.mjs +13 -0
- package/src/standalone/daemon.mjs +19 -49
- package/src/standalone/seeds.mjs +18 -1
- package/src/standalone/session-protocol.mjs +0 -8
- package/src/standalone/session-service.mjs +1 -3
- package/src/tui/App.jsx +0 -5
- package/src/tui/app/app-view.jsx +1 -2
- package/src/tui/app/create-app-pickers.mjs +0 -20
- package/src/tui/app/doctor.mjs +2 -6
- package/src/tui/app/prompt-submit.mjs +2 -49
- package/src/tui/app/settings-picker.mjs +22 -96
- package/src/tui/app/slash-commands.mjs +0 -2
- package/src/tui/app/slash-dispatch.mjs +0 -17
- package/src/tui/app/use-prompt-handlers.mjs +4 -1
- package/src/tui/components/StatusLine.jsx +4 -4
- package/src/tui/dist/index.mjs +1003 -1461
- package/src/tui/session/context-state.mjs +0 -1
- package/src/tui/session/queue-helpers.mjs +1 -1
- package/src/tui/session/queue-helpers.test.mjs +10 -1
- package/src/tui/session/session-api-ext.mjs +0 -89
- package/src/tui/session/session-api.mjs +0 -2
- package/src/tui/session/tool-card-results.mjs +1 -1
- package/src/tui/session/tool-result-status.mjs +3 -3
- package/src/tui/session/tool-result-status.test.mjs +3 -3
- package/src/tui/session-local.mjs +0 -26
- package/src/workflows/default/WORKFLOW.md +13 -15
- package/src/workflows/solo/WORKFLOW.md +6 -7
- package/src/runtime/channels/lib/config-lock.mjs +0 -13
- package/src/runtime/channels/lib/drop-trace.mjs +0 -71
- package/src/runtime/channels/lib/format.mjs +0 -190
- package/src/runtime/channels/lib/inbound-handler.mjs +0 -387
- package/src/runtime/channels/lib/inbound-routing.mjs +0 -81
- package/src/runtime/channels/lib/interaction-handlers.mjs +0 -70
- package/src/runtime/channels/lib/interaction-workflows.mjs +0 -76
- package/src/runtime/channels/lib/memory-client.mjs +0 -289
- package/src/runtime/channels/lib/output-forwarder.mjs +0 -934
- package/src/runtime/channels/lib/session-control.mjs +0 -36
- package/src/runtime/channels/lib/session-discovery.mjs +0 -217
- package/src/runtime/channels/lib/session-discovery.test.mjs +0 -32
- package/src/runtime/channels/lib/telegram-format.mjs +0 -280
- package/src/runtime/channels/lib/tool-format.mjs +0 -125
- package/src/runtime/channels/lib/transcript-binding.mjs +0 -336
- package/src/runtime/channels/lib/transcript-discovery.mjs +0 -208
- package/src/runtime/channels/providers/discord-access.mjs +0 -30
- package/src/runtime/channels/providers/discord-attachments.mjs +0 -69
- package/src/runtime/channels/providers/discord-gateway.mjs +0 -230
- package/src/runtime/channels/providers/discord.mjs +0 -730
- package/src/runtime/channels/providers/telegram.mjs +0 -598
- package/src/runtime/channels/providers/telegram.test.mjs +0 -75
- package/src/session-runtime/remote-control.mjs +0 -126
- package/src/session-runtime/remote-transcript.mjs +0 -134
- package/src/session-runtime/remote-transition-queue.mjs +0 -14
- package/src/tui/app/channel-pickers.mjs +0 -384
|
@@ -14,6 +14,13 @@ const _shellSyntaxCheat =
|
|
|
14
14
|
const _shellToolRouting = process.platform === 'win32'
|
|
15
15
|
? 'Use read, NOT cat/Get-Content/head/tail; list, NOT ls/dir; find/glob, NOT find; grep, NOT grep/rg/Select-String; edit/apply_patch, NOT sed/awk/heredocs/echo/Set-Content.'
|
|
16
16
|
: 'Use read, NOT cat/head/tail; list, NOT ls; find/glob, NOT find; grep, NOT grep/rg; edit/apply_patch, NOT sed/awk/heredocs/echo.';
|
|
17
|
+
// CC parity: when background tasks are disabled for this process, drop the
|
|
18
|
+
// run_in_background field from the schema entirely so the model cannot burn a
|
|
19
|
+
// failure turn attempting it. Mirrors bash-tool's runtime guard, which stays
|
|
20
|
+
// as defense in depth. Process-stable env, evaluated once at module load.
|
|
21
|
+
const _shellBackgroundDisabled = /^(1|true|yes|on)$/i.test(
|
|
22
|
+
String(process.env.MIXDOG_SHELL_DISABLE_BACKGROUND_TASKS || '').trim(),
|
|
23
|
+
);
|
|
17
24
|
|
|
18
25
|
export const BUILTIN_TOOLS = [
|
|
19
26
|
{
|
|
@@ -77,16 +84,18 @@ export const BUILTIN_TOOLS = [
|
|
|
77
84
|
name: 'shell',
|
|
78
85
|
title: 'Shell',
|
|
79
86
|
annotations: { title: 'Shell', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true, compressible: true },
|
|
80
|
-
description: `Run programs, runtime/state operations, calculations, transformations, file generation, and unsupported-format inspection. Avoid file operations covered by dedicated tools unless explicitly instructed or after verifying that a dedicated tool cannot do the job. ${_shellToolRouting} Commands start in the foreground; after 15s, a still-running command continues as a tracked task_id and completes by notification.`,
|
|
87
|
+
description: `Run programs, runtime/state operations, calculations, transformations, file generation, and unsupported-format inspection. Avoid file operations covered by dedicated tools unless explicitly instructed or after verifying that a dedicated tool cannot do the job. ${_shellToolRouting} Commands start in the foreground${_shellBackgroundDisabled ? '' : ' unless run_in_background is true'}; after 15s, a still-running foreground command continues as a tracked task_id and completes by notification.`,
|
|
81
88
|
inputSchema: {
|
|
82
89
|
type: 'object',
|
|
83
90
|
properties: {
|
|
84
91
|
command: { type: 'string', description: `Command.${_shellSyntaxCheat}` },
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
92
|
+
...(_shellBackgroundDisabled ? {} : {
|
|
93
|
+
run_in_background: {
|
|
94
|
+
type: 'boolean',
|
|
95
|
+
default: false,
|
|
96
|
+
description: 'Start as a tracked background task immediately; use only when the result is not needed for the next step — completion arrives by notification. Default false.',
|
|
97
|
+
},
|
|
98
|
+
}),
|
|
90
99
|
},
|
|
91
100
|
required: ['command'],
|
|
92
101
|
additionalProperties: false,
|
|
@@ -191,6 +191,7 @@ async function subscribeResultCacheInFlight(entry, signal) {
|
|
|
191
191
|
|
|
192
192
|
export async function runResultCacheInFlight(key, compute, options = {}) {
|
|
193
193
|
const subscriberSignal = options?.signal || options?.abortSignal || null;
|
|
194
|
+
let invalidationRetries = 0;
|
|
194
195
|
for (;;) {
|
|
195
196
|
const cached = cacheGet(key);
|
|
196
197
|
if (cached !== null) return cached;
|
|
@@ -233,6 +234,13 @@ export async function runResultCacheInFlight(key, compute, options = {}) {
|
|
|
233
234
|
// freshness retry, not a user-visible abort: start a new generation
|
|
234
235
|
// unless this subscriber itself was cancelled.
|
|
235
236
|
if (!entry.invalidated || subscriberSignal?.aborted) throw error;
|
|
237
|
+
// Continuous invalidation (watcher churn, eviction broadcasts)
|
|
238
|
+
// must not spin forever: after a few generations run the compute
|
|
239
|
+
// directly — uncached and no longer abortable by invalidation —
|
|
240
|
+
// mirroring the server-side MAX_WALK_RESTARTS cap.
|
|
241
|
+
if (++invalidationRetries >= 3) {
|
|
242
|
+
return await compute({ signal: subscriberSignal });
|
|
243
|
+
}
|
|
236
244
|
}
|
|
237
245
|
}
|
|
238
246
|
}
|
|
@@ -351,9 +359,8 @@ export function getCachedReadOnlyStat(fullPath, loader = statSync, now = Date.no
|
|
|
351
359
|
return stat;
|
|
352
360
|
}
|
|
353
361
|
|
|
354
|
-
|
|
362
|
+
async function visitPathStats(paths, workDir, concurrency, opts, visitor) {
|
|
355
363
|
const items = Array.isArray(paths) ? paths : [];
|
|
356
|
-
const out = new Array(items.length);
|
|
357
364
|
const now = Date.now();
|
|
358
365
|
const inflight = new Map();
|
|
359
366
|
let next = 0;
|
|
@@ -398,17 +405,31 @@ export async function statPathsForMtime(paths, workDir, concurrency = Infinity,
|
|
|
398
405
|
if (index >= items.length) return;
|
|
399
406
|
const p = items[index];
|
|
400
407
|
const full = isAbsolute(p) ? p : resolveAgainstCwd(p, workDir);
|
|
408
|
+
let entry;
|
|
401
409
|
try {
|
|
402
410
|
const stat = await resolveStat(full);
|
|
403
411
|
if (!stat) throw new Error('stat failed');
|
|
404
|
-
|
|
412
|
+
entry = { path: p, full, stat, size: stat.size, mtime: stat.mtimeMs, mtimeMs: stat.mtimeMs };
|
|
405
413
|
} catch {
|
|
406
|
-
|
|
414
|
+
entry = { path: p, full, stat: null, size: 0, mtime: 0, mtimeMs: 0 };
|
|
407
415
|
}
|
|
416
|
+
visitor(entry, index);
|
|
408
417
|
}
|
|
409
418
|
}
|
|
410
419
|
const workerCount = Math.min(Math.max(1, concurrency), Math.max(1, items.length));
|
|
411
420
|
await Promise.all(Array.from({ length: workerCount }, worker));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export async function visitPathsForMtime(paths, workDir, concurrency = Infinity, opts = {}, visitor = () => {}) {
|
|
424
|
+
await visitPathStats(paths, workDir, concurrency, opts, visitor);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export async function statPathsForMtime(paths, workDir, concurrency = Infinity, opts = {}) {
|
|
428
|
+
const items = Array.isArray(paths) ? paths : [];
|
|
429
|
+
const out = new Array(items.length);
|
|
430
|
+
await visitPathStats(items, workDir, concurrency, opts, (entry, index) => {
|
|
431
|
+
out[index] = entry;
|
|
432
|
+
});
|
|
412
433
|
return out;
|
|
413
434
|
}
|
|
414
435
|
|
|
@@ -8,7 +8,9 @@ import { mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node
|
|
|
8
8
|
import { tmpdir } from 'node:os';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
import { tryExecuteExternalToolAdapter } from './external-tool-adapters.mjs';
|
|
11
|
+
import { executeBuiltinTool } from '../builtin.mjs';
|
|
11
12
|
import { recordReadSnapshot } from './read-snapshot-runtime.mjs';
|
|
13
|
+
import { executePatchTool } from '../patch.mjs';
|
|
12
14
|
import { closeNativePatchServerForTests } from '../patch/native-server.mjs';
|
|
13
15
|
|
|
14
16
|
const ANCHOR = '#if 0\n"""\n#endif';
|
|
@@ -57,6 +59,44 @@ test('count drift falls through to the strict ambiguity reject', async (t) => {
|
|
|
57
59
|
assert.match(String(result), /current file excerpt lines/);
|
|
58
60
|
});
|
|
59
61
|
|
|
62
|
+
test('successful edit snapshot makes a follow-up read return unchanged', async (t) => {
|
|
63
|
+
const { dir, file } = makeTempFile('alpha\nkeep\n');
|
|
64
|
+
const sessionId = `edit-known-current-${process.pid}`;
|
|
65
|
+
t.after(() => { rmSync(dir, { recursive: true, force: true }); void closeNativePatchServerForTests?.(); });
|
|
66
|
+
|
|
67
|
+
const result = await tryExecuteExternalToolAdapter('edit', {
|
|
68
|
+
file_path: file,
|
|
69
|
+
old_string: 'alpha',
|
|
70
|
+
new_string: 'omega',
|
|
71
|
+
}, dir, { sessionId });
|
|
72
|
+
assert.match(String(result), /^Updated /);
|
|
73
|
+
|
|
74
|
+
const reread = await executeBuiltinTool('read', { path: file }, dir, { sessionId });
|
|
75
|
+
assert.equal(reread, `[file unchanged: ${file.replaceAll('\\', '/')}]`);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('successful apply_patch snapshot makes a follow-up read return unchanged', async (t) => {
|
|
79
|
+
const { dir, file } = makeTempFile('alpha\nkeep\n');
|
|
80
|
+
const sessionId = `patch-known-current-${process.pid}`;
|
|
81
|
+
t.after(() => { rmSync(dir, { recursive: true, force: true }); void closeNativePatchServerForTests?.(); });
|
|
82
|
+
|
|
83
|
+
const result = await executePatchTool('apply_patch', {
|
|
84
|
+
base_path: dir,
|
|
85
|
+
patch: `*** Begin Patch
|
|
86
|
+
*** Update File: poly.c
|
|
87
|
+
@@
|
|
88
|
+
-alpha
|
|
89
|
+
+omega
|
|
90
|
+
keep
|
|
91
|
+
*** End Patch
|
|
92
|
+
`,
|
|
93
|
+
}, dir, { sessionId });
|
|
94
|
+
assert.match(String(result), /^Applied 1 File \(Native\)/);
|
|
95
|
+
|
|
96
|
+
const reread = await executeBuiltinTool('read', { path: file }, dir, { sessionId });
|
|
97
|
+
assert.equal(reread, `[file unchanged: ${file.replaceAll('\\', '/')}]`);
|
|
98
|
+
});
|
|
99
|
+
|
|
60
100
|
test('stale read snapshot does not block a still-unique current old_string', async (t) => {
|
|
61
101
|
const { dir, file } = makeTempFile('alpha\nkeep\n');
|
|
62
102
|
const sessionId = `edit-stale-safe-${process.pid}`;
|
|
@@ -11,6 +11,7 @@ import { runRgWindowedLines } from '../native-search-runner.mjs';
|
|
|
11
11
|
import { statReachable } from '../fs-reachability.mjs';
|
|
12
12
|
import { dedupeFanoutMatchLines, formatGrepOutput } from './grep-output.mjs';
|
|
13
13
|
import { expandGrepAnchorContextOutput } from './grep-context-expander.mjs';
|
|
14
|
+
import { markScopedCacheIncomplete } from '../../../session/cache/scoped-cache-outcome.mjs';
|
|
14
15
|
|
|
15
16
|
export async function runGrepPatternFanout({
|
|
16
17
|
args,
|
|
@@ -77,7 +78,9 @@ export async function runGrepPatternFanout({
|
|
|
77
78
|
const pre = await runRgWindowedLines(
|
|
78
79
|
prefilterArgs,
|
|
79
80
|
{ cwd: preSpawnCwd, signal: options.signal },
|
|
80
|
-
|
|
81
|
+
// Speculative whole-scope pass: bulk lane keeps it from
|
|
82
|
+
// competing with interactive searches for disk bandwidth.
|
|
83
|
+
{ offset: 0, limit: GREP_FANOUT_PREFILTER_FILE_CAP, summaryLimit: 0, bulkHint: true },
|
|
81
84
|
);
|
|
82
85
|
return pre.complete && !pre.partial ? pre.lines : null;
|
|
83
86
|
} catch { return null; }
|
|
@@ -130,15 +133,34 @@ export async function runGrepPatternFanout({
|
|
|
130
133
|
});
|
|
131
134
|
const perPatternWindow = headLimit === Infinity ? 300 : (offset + headLimit + 4);
|
|
132
135
|
const combinedCap = Math.min(4000, Math.max(400, perPatternWindow * patterns.length));
|
|
136
|
+
// Unfiltered multi-pattern directory scans are the broad-scope shape
|
|
137
|
+
// that saturated the interactive pool; route them to the bulk lane.
|
|
138
|
+
const combinedBulkHint = normalizedGlobPatterns.length === 0 && !fileType;
|
|
133
139
|
let streamed;
|
|
134
140
|
try {
|
|
135
141
|
streamed = await runRgWindowedLines(
|
|
136
142
|
combinedArgs,
|
|
137
143
|
{ cwd: rgCwd, signal: options.signal },
|
|
138
|
-
{ offset: 0, limit: combinedCap, summaryLimit: 0 },
|
|
144
|
+
{ offset: 0, limit: combinedCap, summaryLimit: 0, bulkHint: combinedBulkHint },
|
|
139
145
|
);
|
|
140
146
|
} catch { break combined; }
|
|
141
|
-
|
|
147
|
+
// Cap overflow (complete:false without partial) still falls back: the
|
|
148
|
+
// per-pattern rescan restores correct per-pattern windows. Timeout and
|
|
149
|
+
// scan-error partials keep their collected lines instead — the legacy
|
|
150
|
+
// fallback would rescan the same scope from scratch and usually time
|
|
151
|
+
// out again, discarding everything the first pass already found.
|
|
152
|
+
if (streamed.partial ? streamed.lines.length === 0 : !streamed.complete) break combined;
|
|
153
|
+
const combinedPartial = streamed.partial === true;
|
|
154
|
+
const combinedPartialSuffix = !combinedPartial
|
|
155
|
+
? ''
|
|
156
|
+
: streamed.timeout
|
|
157
|
+
? '\n[warning] rg timed out; partial results shown. Narrow path/glob/pattern for a complete result.'
|
|
158
|
+
: streamed.rgStderr
|
|
159
|
+
? `\n[warning] rg exit 2 (partial results): ${String(streamed.rgStderr).trim().slice(0, 300)}`
|
|
160
|
+
: '\n[warning] rg exit 2 (partial results)';
|
|
161
|
+
if (combinedPartial && options?.scopedCacheOutcome) {
|
|
162
|
+
markScopedCacheIncomplete(options.scopedCacheOutcome);
|
|
163
|
+
}
|
|
142
164
|
const adaptive = contextN > 0 && !(beforeN > 0) && !(afterN > 0);
|
|
143
165
|
const byPattern = patterns.map(() => []);
|
|
144
166
|
const residual = [];
|
|
@@ -175,7 +197,7 @@ export async function runGrepPatternFanout({
|
|
|
175
197
|
filenameOmitted: false,
|
|
176
198
|
headLimit,
|
|
177
199
|
offset,
|
|
178
|
-
totalKnown:
|
|
200
|
+
totalKnown: !combinedPartial,
|
|
179
201
|
requestedContext: contextN,
|
|
180
202
|
maxContext: GREP_AUTO_CONTEXT_LINES,
|
|
181
203
|
patterns: [p],
|
|
@@ -190,7 +212,7 @@ export async function runGrepPatternFanout({
|
|
|
190
212
|
body = formatGrepOutput({
|
|
191
213
|
windowed: windowedLines,
|
|
192
214
|
totalWindowed: post.length,
|
|
193
|
-
totalKnown:
|
|
215
|
+
totalKnown: !combinedPartial,
|
|
194
216
|
headLimit,
|
|
195
217
|
offset,
|
|
196
218
|
outputMode,
|
|
@@ -211,14 +233,15 @@ export async function runGrepPatternFanout({
|
|
|
211
233
|
sections.push(`# grep pattern:${JSON.stringify(p)}\n${dedupeFanoutMatchLines(body, seenCombined)}`);
|
|
212
234
|
}
|
|
213
235
|
if (noMatchPatterns.length > 0) {
|
|
214
|
-
|
|
236
|
+
// Under a partial scan a zero-hit pattern is NOT a proven no-match.
|
|
237
|
+
sections.push(`(no matches${combinedPartial ? ' in partial results' : ''}) pattern=${JSON.stringify(noMatchPatterns)} path=${searchPath}${globStr}; path exists`);
|
|
215
238
|
}
|
|
216
239
|
if (residual.length > 0) {
|
|
217
240
|
// Rust/JS regex divergence or --max-columns truncation left
|
|
218
241
|
// matches no pattern claimed; surface them rather than drop.
|
|
219
242
|
sections.push(`# grep (unattributed matches)\n${residual.slice(0, 40).join('\n')}`);
|
|
220
243
|
}
|
|
221
|
-
return patternCapNote + sections.join('\n\n');
|
|
244
|
+
return patternCapNote + sections.join('\n\n') + combinedPartialSuffix;
|
|
222
245
|
}
|
|
223
246
|
// Prefilter result (started above, overlapped with the combined
|
|
224
247
|
// attempt): when it completed under the cap, each per-pattern grep
|
|
@@ -33,7 +33,7 @@ function dataDir() {
|
|
|
33
33
|
|| join(process.env.MIXDOG_HOME || join(homedir(), '.mixdog'), 'data');
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
function pidAlive(pid) {
|
|
36
|
+
export function pidAlive(pid) {
|
|
37
37
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
38
38
|
try { process.kill(pid, 0); return true; }
|
|
39
39
|
catch (error) { return error?.code === 'EPERM'; } // EPERM = alive, not ours
|
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
TOOL_OUTPUT_MAX_BYTES,
|
|
38
38
|
} from './tool-output-limit.mjs';
|
|
39
39
|
import { runRg, runRgWindowedLines } from './native-search-runner.mjs';
|
|
40
|
-
import { tryServeFuzzySearch } from './native-search-client.mjs';
|
|
40
|
+
import { tryServeFuzzySearch, tryServeListMetadata } from './native-search-client.mjs';
|
|
41
41
|
import { fuzzyRankMulti, prepareFuzzyItems, splitFuzzyQueryTokens } from './fuzzy-match.mjs';
|
|
42
42
|
import {
|
|
43
43
|
recordLocalSearchBackend,
|
|
@@ -129,6 +129,95 @@ function _metaModeOctal(mode) {
|
|
|
129
129
|
return (mode & 0o7777).toString(8).padStart(3, '0');
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
async function listMetadataForPaths(paths, workDir, options = {}) {
|
|
133
|
+
if (!Array.isArray(paths) || paths.length === 0) return [];
|
|
134
|
+
const nativeImpl = typeof options?.__tryServeListMetadata === 'function'
|
|
135
|
+
? options.__tryServeListMetadata
|
|
136
|
+
: tryServeListMetadata;
|
|
137
|
+
try {
|
|
138
|
+
const entries = await nativeImpl(paths, {
|
|
139
|
+
cwd: workDir,
|
|
140
|
+
signal: options?.signal || options?.abortSignal || null,
|
|
141
|
+
timeout: 5_000,
|
|
142
|
+
});
|
|
143
|
+
if (Array.isArray(entries) && entries.length === paths.length) {
|
|
144
|
+
return entries.map((entry, index) => ({
|
|
145
|
+
fullPath: paths[index],
|
|
146
|
+
type: entry && !entry.error ? String(entry.type || '') : '',
|
|
147
|
+
stat: entry && !entry.error ? { mode: Number(entry.mode) || 0 } : null,
|
|
148
|
+
size: entry && !entry.error ? Math.max(0, Number(entry.size) || 0) : 0,
|
|
149
|
+
mtimeMs: entry && !entry.error ? Math.max(0, Number(entry.mtimeMs) || 0) : 0,
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
} catch {
|
|
153
|
+
// Older/unavailable native helpers preserve the existing JS fallback.
|
|
154
|
+
}
|
|
155
|
+
return lstatPathsForMtime(paths, workDir, 64, { deadlineMs: 5000 });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function tryNativeDeepListRows({
|
|
159
|
+
fullPath,
|
|
160
|
+
workDir,
|
|
161
|
+
depth,
|
|
162
|
+
hidden,
|
|
163
|
+
includeNoise,
|
|
164
|
+
signal,
|
|
165
|
+
options,
|
|
166
|
+
}) {
|
|
167
|
+
if (depth <= 1 || typeof options?.readdirImpl === 'function') return null;
|
|
168
|
+
const rgArgs = ['--files', '--directories', '--no-ignore', '--no-require-git'];
|
|
169
|
+
if (hidden) rgArgs.push('--hidden');
|
|
170
|
+
rgArgs.push('--max-depth', String(depth));
|
|
171
|
+
if (!includeNoise) {
|
|
172
|
+
for (const glob of DEFAULT_IGNORE_GLOBS) rgArgs.push('--glob', glob);
|
|
173
|
+
}
|
|
174
|
+
rgArgs.push('--', fullPath);
|
|
175
|
+
try {
|
|
176
|
+
const served = await runRgWindowedLines(
|
|
177
|
+
rgArgs,
|
|
178
|
+
{ cwd: workDir, timeout: LIST_WALK_TIMEOUT_MS, signal },
|
|
179
|
+
{ offset: 0, limit: LIST_ABSOLUTE_CAP, nativeInventory: true },
|
|
180
|
+
);
|
|
181
|
+
if (served?.complete !== true || served?.partial === true || !Array.isArray(served?.lines)) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
const paths = Array.from(new Set(served.lines.map((line) => (
|
|
185
|
+
isAbsolute(line) ? line : resolveAgainstCwd(line, workDir)
|
|
186
|
+
)))).filter((path) => path !== fullPath);
|
|
187
|
+
const metadata = await listMetadataForPaths(paths, workDir, options);
|
|
188
|
+
if (!Array.isArray(metadata) || metadata.length !== paths.length) return null;
|
|
189
|
+
const rows = paths.map((path, index) => {
|
|
190
|
+
const item = metadata[index] || {};
|
|
191
|
+
const type = ['file', 'dir', 'symlink', 'other'].includes(item.type)
|
|
192
|
+
? item.type
|
|
193
|
+
: (item.stat?.isDirectory?.()
|
|
194
|
+
? 'dir'
|
|
195
|
+
: (item.stat?.isFile?.()
|
|
196
|
+
? 'file'
|
|
197
|
+
: (item.stat?.isSymbolicLink?.() ? 'symlink' : 'other')));
|
|
198
|
+
return {
|
|
199
|
+
path,
|
|
200
|
+
type,
|
|
201
|
+
size: Number(item.size) || 0,
|
|
202
|
+
mtimeMs: Number(item.mtimeMs) || 0,
|
|
203
|
+
mode: Number(item.mode) || 0,
|
|
204
|
+
fullPath: path,
|
|
205
|
+
};
|
|
206
|
+
});
|
|
207
|
+
return {
|
|
208
|
+
rows,
|
|
209
|
+
walkResult: {
|
|
210
|
+
entriesVisited: rows.length,
|
|
211
|
+
directoriesVisited: rows.filter((row) => row.type === 'dir').length,
|
|
212
|
+
aborted: false,
|
|
213
|
+
nativeInventory: true,
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
} catch {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
132
221
|
export async function executeListTool(args, workDir, options = {}) {
|
|
133
222
|
if (typeof args.fuzzy === 'string' && args.fuzzy.length > 0) {
|
|
134
223
|
return executeFuzzyFindTool({ ...args, query: args.fuzzy }, workDir, options);
|
|
@@ -216,37 +305,56 @@ export async function executeListTool(args, workDir, options = {}) {
|
|
|
216
305
|
let truncatedByCap = false;
|
|
217
306
|
const walkWarnings = [];
|
|
218
307
|
const walkDeadline = Date.now() + LIST_WALK_TIMEOUT_MS;
|
|
219
|
-
const
|
|
308
|
+
const nativeDeep = await tryNativeDeepListRows({
|
|
309
|
+
fullPath,
|
|
310
|
+
workDir,
|
|
311
|
+
depth,
|
|
220
312
|
hidden,
|
|
221
|
-
|
|
222
|
-
excludeDirNames: includeNoise ? null : NOISE_DIR_NAMES,
|
|
313
|
+
includeNoise,
|
|
223
314
|
signal: options.signal,
|
|
224
|
-
|
|
225
|
-
onWarn: (dir, error, context) => walkWarnings.push({ dir, error, root: Boolean(context?.root) }),
|
|
226
|
-
visit: (ent, entPath) => {
|
|
227
|
-
if (Date.now() > walkDeadline) { truncatedByCap = true; return false; }
|
|
228
|
-
const isDir = ent.isDirectory();
|
|
229
|
-
const isFile = ent.isFile();
|
|
230
|
-
if (typeFilter === 'file' && !isFile) return;
|
|
231
|
-
if (typeFilter === 'dir' && !isDir) return;
|
|
232
|
-
const entType = isDir ? 'dir' : (isFile ? 'file' : (ent.isSymbolicLink() ? 'symlink' : 'other'));
|
|
233
|
-
rows.push({
|
|
234
|
-
path: entPath,
|
|
235
|
-
type: entType,
|
|
236
|
-
size: 0,
|
|
237
|
-
mtimeMs: 0,
|
|
238
|
-
mode: 0,
|
|
239
|
-
fullPath: entPath,
|
|
240
|
-
});
|
|
241
|
-
if (rows.length >= LIST_ABSOLUTE_CAP) {
|
|
242
|
-
truncatedByCap = true;
|
|
243
|
-
return false;
|
|
244
|
-
}
|
|
245
|
-
// Pre-sort truncation removed: a global name sort needs all
|
|
246
|
-
// candidates collected before slicing, otherwise the visible
|
|
247
|
-
// window depends on traversal order rather than sort order.
|
|
248
|
-
},
|
|
315
|
+
options,
|
|
249
316
|
});
|
|
317
|
+
let walkResult;
|
|
318
|
+
if (nativeDeep) {
|
|
319
|
+
for (const row of nativeDeep.rows) {
|
|
320
|
+
if (typeFilter === 'file' && row.type !== 'file') continue;
|
|
321
|
+
if (typeFilter === 'dir' && row.type !== 'dir') continue;
|
|
322
|
+
rows.push(row);
|
|
323
|
+
}
|
|
324
|
+
walkResult = nativeDeep.walkResult;
|
|
325
|
+
} else {
|
|
326
|
+
walkResult = await walkDir(fullPath, {
|
|
327
|
+
hidden,
|
|
328
|
+
maxDepth: depth,
|
|
329
|
+
excludeDirNames: includeNoise ? null : NOISE_DIR_NAMES,
|
|
330
|
+
signal: options.signal,
|
|
331
|
+
readdirImpl: options.readdirImpl,
|
|
332
|
+
onWarn: (dir, error, context) => walkWarnings.push({ dir, error, root: Boolean(context?.root) }),
|
|
333
|
+
visit: (ent, entPath) => {
|
|
334
|
+
if (Date.now() > walkDeadline) { truncatedByCap = true; return false; }
|
|
335
|
+
const isDir = ent.isDirectory();
|
|
336
|
+
const isFile = ent.isFile();
|
|
337
|
+
if (typeFilter === 'file' && !isFile) return;
|
|
338
|
+
if (typeFilter === 'dir' && !isDir) return;
|
|
339
|
+
const entType = isDir ? 'dir' : (isFile ? 'file' : (ent.isSymbolicLink() ? 'symlink' : 'other'));
|
|
340
|
+
rows.push({
|
|
341
|
+
path: entPath,
|
|
342
|
+
type: entType,
|
|
343
|
+
size: 0,
|
|
344
|
+
mtimeMs: 0,
|
|
345
|
+
mode: 0,
|
|
346
|
+
fullPath: entPath,
|
|
347
|
+
});
|
|
348
|
+
if (rows.length >= LIST_ABSOLUTE_CAP) {
|
|
349
|
+
truncatedByCap = true;
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
// Pre-sort truncation removed: a global name sort needs all
|
|
353
|
+
// candidates collected before slicing, otherwise the visible
|
|
354
|
+
// window depends on traversal order rather than sort order.
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
}
|
|
250
358
|
throwIfDirectoryWalkAborted(options.signal, walkResult, 'list walk');
|
|
251
359
|
const rootFailure = walkWarnings.find((warning) => warning.root);
|
|
252
360
|
if (rootFailure) {
|
|
@@ -257,9 +365,9 @@ export async function executeListTool(args, workDir, options = {}) {
|
|
|
257
365
|
}
|
|
258
366
|
recordShardDirectoryReadSuccess();
|
|
259
367
|
|
|
260
|
-
if (needsGlobalStat && rows.length > 0) {
|
|
368
|
+
if (!nativeDeep && needsGlobalStat && rows.length > 0) {
|
|
261
369
|
// lstat: symlinks should report own metadata, not the target's.
|
|
262
|
-
const stats = await
|
|
370
|
+
const stats = await listMetadataForPaths(rows.map((row) => row.fullPath), workDir, options);
|
|
263
371
|
for (let i = 0; i < rows.length; i++) {
|
|
264
372
|
const item = stats[i];
|
|
265
373
|
if (!item?.stat) continue;
|
|
@@ -281,8 +389,8 @@ export async function executeListTool(args, workDir, options = {}) {
|
|
|
281
389
|
// stat just the visible window (a global stat already ran for
|
|
282
390
|
// mtime/size sorts); `sliced` shares row objects with `rows`, so the
|
|
283
391
|
// assignments land on the rendered entries.
|
|
284
|
-
if (meta && !needsGlobalStat && sliced.length > 0) {
|
|
285
|
-
const stats = await
|
|
392
|
+
if (!nativeDeep && meta && !needsGlobalStat && sliced.length > 0) {
|
|
393
|
+
const stats = await listMetadataForPaths(sliced.map((row) => row.fullPath), workDir, options);
|
|
286
394
|
for (let i = 0; i < sliced.length; i++) {
|
|
287
395
|
const item = stats[i];
|
|
288
396
|
if (!item?.stat) continue;
|
|
@@ -940,15 +1048,11 @@ export async function executeFuzzyFindTool(args, workDir, options = {}) {
|
|
|
940
1048
|
const nativeFuzzyImpl = typeof options?.__tryServeFuzzySearch === 'function'
|
|
941
1049
|
? options.__tryServeFuzzySearch
|
|
942
1050
|
: tryServeFuzzySearch;
|
|
943
|
-
// Space-separated fragments AND-match one path. The native
|
|
944
|
-
//
|
|
945
|
-
// always miss); multi-fragment queries go straight to the broad
|
|
946
|
-
// enumeration + fuzzyRankMulti path below.
|
|
1051
|
+
// Space-separated fragments AND-match one path. The native matcher handles
|
|
1052
|
+
// every token independently and sums exact per-token scores.
|
|
947
1053
|
const queryTokens = splitFuzzyQueryTokens(query);
|
|
948
|
-
const multiToken = queryTokens.length > 1;
|
|
949
1054
|
if (
|
|
950
1055
|
headLimit > 0
|
|
951
|
-
&& !multiToken
|
|
952
1056
|
&& (
|
|
953
1057
|
typeof options?.__runRg !== 'function'
|
|
954
1058
|
|| typeof options?.__tryServeFuzzySearch === 'function'
|
|
@@ -978,6 +1082,9 @@ export async function executeFuzzyFindTool(args, workDir, options = {}) {
|
|
|
978
1082
|
? [`(no fuzzy match for "${query}")`]
|
|
979
1083
|
: [...matches, ...(hasMore ? [`... (top ${headLimit}; raise limit for more)`] : [])];
|
|
980
1084
|
const result = lines.join('\n');
|
|
1085
|
+
if (served.cacheSafe !== false) {
|
|
1086
|
+
cacheSet(cacheKey, result, { scopes: [fullPath] });
|
|
1087
|
+
}
|
|
981
1088
|
if (typeof options?.onProgress === 'function') {
|
|
982
1089
|
try { options.onProgress(`${matches.length} candidates`); } catch {}
|
|
983
1090
|
}
|
|
@@ -1333,8 +1440,10 @@ export async function executeFindFilesTool(args, workDir, options = {}) {
|
|
|
1333
1440
|
if (options?.scopedCacheOutcome && (truncatedByCap || windowed.length > sliced.length)) {
|
|
1334
1441
|
markScopedCacheIncomplete(options.scopedCacheOutcome);
|
|
1335
1442
|
}
|
|
1336
|
-
|
|
1337
|
-
|
|
1443
|
+
// A bounded page is still an exact result because offset/limit are keyed
|
|
1444
|
+
// and watcher invalidation covers the scope. Only the absolute safety cap
|
|
1445
|
+
// makes the computation incomplete.
|
|
1446
|
+
if (!truncatedByCap) {
|
|
1338
1447
|
cacheSet(cacheKey, out, { scopes: [fullPath] });
|
|
1339
1448
|
}
|
|
1340
1449
|
return out;
|
|
@@ -37,6 +37,10 @@ export function recordNativeSearchTiming(served) {
|
|
|
37
37
|
target[`native_${requestClass}_requests`] = (Number(target[`native_${requestClass}_requests`]) || 0) + 1;
|
|
38
38
|
addNumber(target, `native_${requestClass}_queue_ms`, served.queueMs);
|
|
39
39
|
addNumber(target, `native_${requestClass}_handler_ms`, served.handlerMs);
|
|
40
|
+
if (requestClass === 'fuzzy') {
|
|
41
|
+
addNumber(target, 'native_fuzzy_inventory_ms', served.inventoryMs);
|
|
42
|
+
addNumber(target, 'native_fuzzy_rank_ms', served.rankMs);
|
|
43
|
+
}
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
export function recordLocalSearchCacheHit(layer) {
|
|
@@ -42,6 +42,7 @@ function codedError(code, message, cause = null) {
|
|
|
42
42
|
|
|
43
43
|
function requestKind(request) {
|
|
44
44
|
if (request?.processSnapshot) return 'process snapshot';
|
|
45
|
+
if (request?.listMetadata) return 'list metadata';
|
|
45
46
|
if (request?.fuzzy != null) return 'fuzzy';
|
|
46
47
|
return request?.args?.includes?.('--files') ? 'file inventory' : 'content';
|
|
47
48
|
}
|
|
@@ -194,6 +195,31 @@ function _teardown(error, { countFailure = true, detail = '' } = {}) {
|
|
|
194
195
|
try { server.child.kill(); } catch {}
|
|
195
196
|
}
|
|
196
197
|
|
|
198
|
+
export async function shutdownNativeSearchServer(reason = 'process-exit', timeoutMs = 1_000) {
|
|
199
|
+
const server = _server;
|
|
200
|
+
if (!server) return true;
|
|
201
|
+
const child = server.child;
|
|
202
|
+
const exited = new Promise((resolve) => {
|
|
203
|
+
child.once('exit', () => resolve(true));
|
|
204
|
+
child.once('error', () => resolve(true));
|
|
205
|
+
});
|
|
206
|
+
try { child.stdin?.end?.(); } catch {}
|
|
207
|
+
_teardown(new Error(`native search server shutdown (${reason})`), { countFailure: false });
|
|
208
|
+
_warmPromise = null;
|
|
209
|
+
let timer;
|
|
210
|
+
const stopped = await Promise.race([
|
|
211
|
+
exited,
|
|
212
|
+
new Promise((resolve) => {
|
|
213
|
+
timer = setTimeout(() => resolve(false), Math.max(10, Number(timeoutMs) || 1_000));
|
|
214
|
+
}),
|
|
215
|
+
]);
|
|
216
|
+
if (timer) clearTimeout(timer);
|
|
217
|
+
if (!stopped) {
|
|
218
|
+
try { child.kill('SIGKILL'); } catch {}
|
|
219
|
+
}
|
|
220
|
+
return stopped;
|
|
221
|
+
}
|
|
222
|
+
|
|
197
223
|
export function _bindNativeSearchServerLifecycle(child, { onError, onExit } = {}) {
|
|
198
224
|
if (!child?.on) return;
|
|
199
225
|
child.on('error', onError);
|
|
@@ -541,6 +567,8 @@ export async function tryServeSearch(argsList, execOptions = {}, opts = {}) {
|
|
|
541
567
|
: 0,
|
|
542
568
|
deadlineMs: softDeadlineMs(remaining),
|
|
543
569
|
keepWarm: opts.keepWarm === true,
|
|
570
|
+
...(opts.bulkHint === true ? { bulkHint: true } : {}),
|
|
571
|
+
...(opts.mtimeTopK === true ? { mtimeTopK: true } : {}),
|
|
544
572
|
});
|
|
545
573
|
let response = await requestNativeWithRestart(buildRequest, execOptions, deadlineMs);
|
|
546
574
|
// Deadline-swallow defense (binaries before the serve_search response-level
|
|
@@ -612,6 +640,7 @@ export async function tryServeSearch(argsList, execOptions = {}, opts = {}) {
|
|
|
612
640
|
totalSeen: Math.max(0, Math.floor(Number(response.totalSeen) || 0)),
|
|
613
641
|
partial: response.partial === true,
|
|
614
642
|
timeout: response.timeout === true,
|
|
643
|
+
filesScanned: Math.max(0, Math.floor(Number(response.filesScanned) || 0)),
|
|
615
644
|
// Unreadable/half-read files the native scanner skipped: route the count
|
|
616
645
|
// through the existing rg-exit-2 partial phrasing in search-tool.mjs so
|
|
617
646
|
// the model sees WHY the result may be missing matches.
|
|
@@ -687,11 +716,42 @@ export async function tryServeFuzzySearch(args, execOptions = {}) {
|
|
|
687
716
|
cacheSafe: response.cacheSafe !== false,
|
|
688
717
|
queueMs: Math.max(0, Number(response.queueMs) || 0),
|
|
689
718
|
handlerMs: Math.max(0, Number(response.handlerMs) || 0),
|
|
719
|
+
inventoryMs: Math.max(0, Number(response.inventoryMs) || 0),
|
|
720
|
+
rankMs: Math.max(0, Number(response.rankMs) || 0),
|
|
690
721
|
requestClass: response.class === 'fuzzy' ? 'fuzzy' : 'bulk',
|
|
691
722
|
served: true,
|
|
692
723
|
};
|
|
693
724
|
}
|
|
694
725
|
|
|
726
|
+
export async function tryServeListMetadata(paths, execOptions = {}) {
|
|
727
|
+
if (process.env.MIXDOG_SEARCH_SERVER === '0') return null;
|
|
728
|
+
const list = Array.isArray(paths) ? paths.map(String) : [];
|
|
729
|
+
if (list.length === 0) return [];
|
|
730
|
+
if (list.length > 50_000) {
|
|
731
|
+
throw codedError('NATIVE_SEARCH_UNSUPPORTED', 'list metadata request exceeds 50000 paths');
|
|
732
|
+
}
|
|
733
|
+
const callerTimeoutMs = Number(execOptions.timeout);
|
|
734
|
+
const deadlineMs = Number.isFinite(callerTimeoutMs) && callerTimeoutMs > 0
|
|
735
|
+
? Math.min(callerTimeoutMs, REQUEST_TIMEOUT_MS)
|
|
736
|
+
: Math.min(5_000, REQUEST_TIMEOUT_MS);
|
|
737
|
+
const response = await requestNativeWithRestart(
|
|
738
|
+
(server) => ({
|
|
739
|
+
id: ++server.sequence,
|
|
740
|
+
cwd: String(execOptions.cwd || process.cwd()),
|
|
741
|
+
listMetadata: list,
|
|
742
|
+
}),
|
|
743
|
+
execOptions,
|
|
744
|
+
deadlineMs,
|
|
745
|
+
);
|
|
746
|
+
if (response?.error) {
|
|
747
|
+
throw codedError('NATIVE_SEARCH_ERROR', String(response.error));
|
|
748
|
+
}
|
|
749
|
+
if (!Array.isArray(response?.entries) || response.entries.length !== list.length) {
|
|
750
|
+
throw unavailableError();
|
|
751
|
+
}
|
|
752
|
+
return response.entries;
|
|
753
|
+
}
|
|
754
|
+
|
|
695
755
|
export function _resetNativeSearchClientForTest() {
|
|
696
756
|
_teardown(new Error('test reset'), { countFailure: false });
|
|
697
757
|
_binaryPath = undefined;
|
|
@@ -14,7 +14,10 @@ import {
|
|
|
14
14
|
import { extractOoxmlText } from './read-office-files.mjs';
|
|
15
15
|
|
|
16
16
|
function snapshotBodyWasReturnedByRead(snapshot) {
|
|
17
|
-
|
|
17
|
+
const source = String(snapshot?.source || '');
|
|
18
|
+
return source.startsWith('read')
|
|
19
|
+
|| source === 'edit'
|
|
20
|
+
|| source.startsWith('apply_patch_');
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
// BOM-only read-encoding detection. Mirrors CC fileRead.ts:34
|
|
@@ -272,6 +275,20 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
|
|
|
272
275
|
const _safeMsg = normalizeErrorMessage(_rawMsg, workDir);
|
|
273
276
|
return `Error: ${_safeMsg}${hint}`;
|
|
274
277
|
}
|
|
278
|
+
// A successful edit/apply_patch leaves a session-scoped full-file snapshot.
|
|
279
|
+
// The model already knows the resulting body from the prior body plus its
|
|
280
|
+
// mutation, so a follow-up Read needs only the stat tuple to prove that no
|
|
281
|
+
// external write landed afterward. Do not reopen/hash the file body merely
|
|
282
|
+
// to return the unchanged stub.
|
|
283
|
+
const _mutationSnapshot = readStateScope ? getReadSnapshot(fullPath, readStateScope) : null;
|
|
284
|
+
const _mutationSource = String(_mutationSnapshot?.source || '');
|
|
285
|
+
if (
|
|
286
|
+
(_mutationSource === 'edit' || _mutationSource.startsWith('apply_patch_'))
|
|
287
|
+
&& statMatchesSnapshot(st, _mutationSnapshot)
|
|
288
|
+
&& snapshotCoversFullFile(_mutationSnapshot)
|
|
289
|
+
) {
|
|
290
|
+
return `[file unchanged: ${normalizeOutputPath(filePath)}]`;
|
|
291
|
+
}
|
|
275
292
|
// MEDIA-WINS: .pdf/.ipynb dispatch runs BEFORE any cache/snapshot fast
|
|
276
293
|
// path. A media file previously read as text can carry a stale result-
|
|
277
294
|
// cache or read-snapshot entry; returning that cached TEXT instead of the
|