mixdog 0.9.87 → 0.9.89
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 +13 -7
- package/scripts/tool-failures.mjs +60 -24
- package/src/agents/heavy-worker/AGENT.md +3 -4
- package/src/agents/worker/AGENT.md +1 -2
- package/src/cli.mjs +8 -0
- package/src/defaults/cycle3-review-prompt.md +4 -4
- package/src/rules/agent/00-core.md +3 -0
- package/src/rules/agent/42-cycle3-agent.md +5 -6
- package/src/rules/lead/01-general.md +2 -0
- package/src/rules/shared/01-tool.md +29 -35
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +3 -51
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +59 -0
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -2
- package/src/runtime/agent/orchestrator/agent-runtime/title-completion.mjs +67 -0
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +57 -4
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +2 -3
- package/src/runtime/agent/orchestrator/config.mjs +29 -11
- package/src/runtime/agent/orchestrator/context/collect.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-agents.mjs +1 -1
- package/src/runtime/agent/orchestrator/mcp/client.mjs +0 -24
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +3 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +25 -17
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +321 -22
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +35 -22
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +28 -23
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +10 -2
- package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +0 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +0 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +2 -5
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-native-blocks.mjs +27 -0
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +23 -0
- package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +329 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +23 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +107 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +4 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +124 -12
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +19 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +110 -75
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +96 -113
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +160 -11
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +18 -1
- package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +147 -117
- package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +88 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +50 -8
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +25 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +47 -6
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +485 -24
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +100 -2
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +80 -14
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +142 -2
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +128 -32
- package/src/runtime/agent/orchestrator/session/manager.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +77 -4
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +70 -39
- package/src/runtime/agent/orchestrator/session/store/fs-probe.mjs +91 -0
- package/src/runtime/agent/orchestrator/session/store/listing.mjs +141 -64
- package/src/runtime/agent/orchestrator/session/store/live-state.mjs +253 -0
- package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +211 -28
- package/src/runtime/agent/orchestrator/session/store/save-fault.mjs +313 -0
- package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +622 -53
- package/src/runtime/agent/orchestrator/session/store/serialize.mjs +43 -10
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +53 -8
- package/src/runtime/agent/orchestrator/session/store/summary-rebuild-worker.mjs +20 -0
- package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +113 -9
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +476 -52
- package/src/runtime/agent/orchestrator/session/store.mjs +721 -196
- package/src/runtime/agent/orchestrator/session/token-native.mjs +2 -4
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +4 -6
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +146 -90
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +6 -0
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +93 -17
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +7 -7
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +17 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +85 -5
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +52 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +54 -51
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +14 -15
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +132 -8
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +86 -3
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +210 -38
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +23 -1
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +87 -19
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -8
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +12 -2
- package/src/runtime/media/lanes.mjs +2 -2
- package/src/runtime/media/renditions.mjs +101 -17
- package/src/runtime/media/renditions.test.mjs +54 -0
- package/src/runtime/media/store.mjs +111 -43
- package/src/runtime/media/store.test.mjs +53 -11
- package/src/runtime/memory/index.mjs +26 -50
- package/src/runtime/memory/lib/core-memory-candidates.mjs +2 -7
- package/src/runtime/memory/lib/core-memory-store.mjs +2 -9
- package/src/runtime/memory/lib/cycle-scheduler.mjs +10 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +3 -3
- package/src/runtime/memory/lib/embedding-worker.mjs +60 -15
- package/src/runtime/memory/lib/http-router.mjs +4 -0
- package/src/runtime/memory/lib/ko-morph.mjs +49 -6
- package/src/runtime/memory/lib/memory-action-handlers.mjs +21 -18
- package/src/runtime/memory/lib/memory-cycle2.mjs +3 -3
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -3
- package/src/runtime/memory/lib/memory-recall-store.mjs +24 -0
- package/src/runtime/memory/lib/query-handlers.mjs +26 -28
- package/src/runtime/memory/tool-defs.mjs +3 -3
- package/src/runtime/shared/atomic-file.mjs +5 -1
- package/src/runtime/shared/child-guardian.mjs +73 -2
- package/src/runtime/shared/provider-api-key.mjs +22 -5
- package/src/runtime/shared/tool-execution-contract.mjs +53 -0
- package/src/session-runtime/config-helpers.mjs +6 -5
- package/src/session-runtime/lifecycle-api.mjs +16 -4
- package/src/session-runtime/media-api.mjs +31 -16
- package/src/session-runtime/prewarm.mjs +2 -24
- package/src/session-runtime/runtime-core.mjs +33 -15
- package/src/session-runtime/runtime-tunables.mjs +0 -3
- package/src/session-runtime/session-lifecycle.mjs +0 -6
- package/src/session-runtime/session-title.mjs +228 -0
- package/src/session-runtime/session-turn-api.mjs +11 -7
- package/src/session-runtime/workflow-agents-api.mjs +16 -0
- package/src/session-runtime/workflow.mjs +7 -5
- package/src/standalone/agent-tool/job-views.mjs +90 -36
- package/src/standalone/agent-tool/spawn-flow.mjs +25 -216
- package/src/standalone/agent-tool/worker-index.mjs +8 -0
- package/src/standalone/agent-tool/worker-rows.mjs +2 -1
- package/src/standalone/agent-tool.mjs +17 -17
- package/src/standalone/agent-watchdog-registry.mjs +19 -2
- package/src/standalone/channel-daemon.mjs +8 -0
- package/src/standalone/explore-tool.mjs +1 -1
- package/src/standalone/memory-runtime-proxy.mjs +4 -7
- package/src/standalone/projects.mjs +4 -1
- package/src/standalone/usage-dashboard.mjs +25 -3
- package/src/tui/App.jsx +5 -3
- package/src/tui/app/resume-picker.mjs +7 -2
- package/src/tui/app/route-pickers.mjs +11 -117
- package/src/tui/app/slash-dispatch.mjs +13 -3
- package/src/tui/app/transcript-row-estimate.mjs +1 -1
- package/src/tui/app/transcript-window.mjs +17 -93
- package/src/tui/app/use-transcript-scroll.mjs +32 -2
- package/src/tui/app/use-transcript-window.mjs +64 -114
- package/src/tui/components/Markdown.jsx +19 -2
- package/src/tui/components/Message.jsx +8 -3
- package/src/tui/components/ToolExecution.jsx +7 -3
- package/src/tui/dist/index.mjs +243 -223
- package/src/tui/engine/live-share.mjs +15 -5
- package/src/tui/engine/render-timing.mjs +2 -2
- package/src/tui/engine/session-api-ext.mjs +74 -4
- package/src/tui/engine/session-flow.mjs +3 -1
- package/src/tui/engine.mjs +2 -2
- package/src/tui/hooks/useSharedTick.mjs +0 -2
- package/src/tui/index.jsx +5 -5
- package/src/ui/statusline-agents.mjs +44 -11
- package/src/vendor/statusline/bin/statusline-route.mjs +26 -3
- package/src/workflows/default/WORKFLOW.md +3 -3
- package/src/workflows/solo/WORKFLOW.md +5 -3
- package/src/runtime/media/index.mjs +0 -18
- package/src/runtime/memory/lib/embedding-warmup.mjs +0 -68
- package/src/standalone/agent-shard/shard-child.mjs +0 -300
- package/src/standalone/agent-shard/shard-pool.mjs +0 -443
|
@@ -19,7 +19,6 @@ import { existsSync } from 'node:fs';
|
|
|
19
19
|
import { spawn } from 'node:child_process';
|
|
20
20
|
import { fileURLToPath } from 'node:url';
|
|
21
21
|
import { resolve as pathResolve, dirname as pathDirname, join as pathJoin } from 'node:path';
|
|
22
|
-
import { startChildGuardian } from '../../../shared/child-guardian.mjs';
|
|
23
22
|
import { getPluginData } from '../config.mjs';
|
|
24
23
|
import { ensureTokenBinary, findCachedTokenBinary } from '../tools/token-binary-fetcher.mjs';
|
|
25
24
|
|
|
@@ -112,9 +111,8 @@ function _ensureServer() {
|
|
|
112
111
|
child.stdin?.unref?.();
|
|
113
112
|
child.stdout?.unref?.();
|
|
114
113
|
child.stdin?.on?.('error', () => { /* EPIPE on teardown */ });
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
} catch { /* guardian best-effort */ }
|
|
114
|
+
// The native server exits when its stdio owner closes. A second Electron
|
|
115
|
+
// guardian duplicated that same ownership signal while retaining ~100 MB.
|
|
118
116
|
server.ready = _request(server, OP_PING, null, READY_TIMEOUT_MS).then((count) => count !== null);
|
|
119
117
|
_server = server;
|
|
120
118
|
return server;
|
|
@@ -117,6 +117,12 @@ export async function processToolBatch(ctx) {
|
|
|
117
117
|
role: 'tool',
|
|
118
118
|
content: _stub,
|
|
119
119
|
toolCallId: call.id,
|
|
120
|
+
// Explicitly NOT a success: no tool executed for this call.
|
|
121
|
+
// 'skipped' keeps the transcript/UI non-error (a benign
|
|
122
|
+
// dedup, not a failure) while the unresolved-tool-failure
|
|
123
|
+
// stop hook refuses to count it as the executed success
|
|
124
|
+
// that would resolve a prior failure.
|
|
125
|
+
toolKind: 'skipped',
|
|
120
126
|
});
|
|
121
127
|
continue;
|
|
122
128
|
}
|
|
@@ -137,6 +143,10 @@ export async function processToolBatch(ctx) {
|
|
|
137
143
|
role: 'tool',
|
|
138
144
|
content: _stub,
|
|
139
145
|
toolCallId: call.id,
|
|
146
|
+
// Same rule as the intra-turn stub above: the earlier
|
|
147
|
+
// result is replayed from context, nothing executed, so
|
|
148
|
+
// this can never resolve a prior tool failure.
|
|
149
|
+
toolKind: 'skipped',
|
|
140
150
|
});
|
|
141
151
|
try {
|
|
142
152
|
appendAgentTrace({
|
|
@@ -172,6 +182,15 @@ export async function processToolBatch(ctx) {
|
|
|
172
182
|
role: 'tool',
|
|
173
183
|
content: `[repeat-failure-guard] This exact \`${call.name}\` call (identical arguments) has already failed ${_rfg.count} times in a row; not re-executing because the result will not change. Change approach: use different arguments, a different tool, or skip this step.`,
|
|
174
184
|
toolCallId: call.id,
|
|
185
|
+
// The call is still UNRESOLVED — the guard only skips the
|
|
186
|
+
// re-execution. Tagging it as an error keeps it a failure
|
|
187
|
+
// for every downstream consumer, notably the turn stop
|
|
188
|
+
// hook (a skip must never read as a success that resolves
|
|
189
|
+
// an earlier failure).
|
|
190
|
+
toolKind: 'error',
|
|
191
|
+
// …but nothing was dispatched, so this skip is not a real
|
|
192
|
+
// tool failure and must never ARM the stop hook either.
|
|
193
|
+
guardSkip: true,
|
|
175
194
|
});
|
|
176
195
|
continue;
|
|
177
196
|
}
|
|
@@ -151,11 +151,9 @@ function resolveBash() {
|
|
|
151
151
|
// Map<id, { proc, lastUsed, stdoutBuf, stderrBuf, busy }>
|
|
152
152
|
const _sessions = new Map();
|
|
153
153
|
let _reaperTimer = null;
|
|
154
|
-
// R17
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
// awaits when the host dies abruptly). Sync iteration of _sessions + direct
|
|
158
|
-
// _killProcessTree (sync taskkill on win, sync process.kill on posix).
|
|
154
|
+
// R17 terminating-signal hooks installed exactly once at first _spawnSession.
|
|
155
|
+
// The module-level synchronous exit drain below owns normal process teardown;
|
|
156
|
+
// these handlers cover signals that another listener may consume before exit.
|
|
159
157
|
let _parentExitInstalled = false;
|
|
160
158
|
function _installParentExitHook() {
|
|
161
159
|
if (_parentExitInstalled) return;
|
|
@@ -165,7 +163,6 @@ function _installParentExitHook() {
|
|
|
165
163
|
try { _killProcessTree(s.proc); } catch { /* ignore */ }
|
|
166
164
|
}
|
|
167
165
|
};
|
|
168
|
-
try { process.on('exit', sweep); } catch { /* ignore */ }
|
|
169
166
|
// For terminating signals, sweep children then restore default POSIX
|
|
170
167
|
// termination. A sweep-only handler swallows the signal and leaves the
|
|
171
168
|
// process alive, so after sweeping we remove our handler and re-raise the
|
|
@@ -367,6 +364,7 @@ function _spawnSession(id, initialCwd = process.cwd(), resourceLease = null) {
|
|
|
367
364
|
childPid: proc.pid,
|
|
368
365
|
childGroupPid: proc.pid,
|
|
369
366
|
label: 'bash-session',
|
|
367
|
+
protectHostMemory: true,
|
|
370
368
|
});
|
|
371
369
|
proc.stdout.setEncoding('utf-8');
|
|
372
370
|
proc.stderr.setEncoding('utf-8');
|
|
@@ -36,7 +36,7 @@ export const BUILTIN_TOOLS = [
|
|
|
36
36
|
name: 'read',
|
|
37
37
|
title: 'Mixdog Read',
|
|
38
38
|
annotations: { title: 'Mixdog Read', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: false },
|
|
39
|
-
description: 'Read file contents
|
|
39
|
+
description: 'Read file contents. Batch paths/regions as real arrays: path[] or {path,offset,limit}[] regions in one call. Not for directories.',
|
|
40
40
|
inputSchema: {
|
|
41
41
|
type: 'object',
|
|
42
42
|
properties: {
|
|
@@ -74,8 +74,8 @@ export const BUILTIN_TOOLS = [
|
|
|
74
74
|
name: 'shell',
|
|
75
75
|
title: 'Mixdog Shell',
|
|
76
76
|
annotations: { title: 'Mixdog Shell', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true, compressible: true },
|
|
77
|
-
description: 'Run programs/change state; not file inspection.
|
|
78
|
-
+ '
|
|
77
|
+
description: 'Run programs/change state; not file inspection. '
|
|
78
|
+
+ 'Combine order-dependent commands into one command. Use async for sleep/watch/dev loops.'
|
|
79
79
|
+ `${_shellSyntaxCheat} ${TOOL_ASYNC_EXECUTION_CONTRACT}`,
|
|
80
80
|
inputSchema: {
|
|
81
81
|
type: 'object',
|
|
@@ -123,7 +123,7 @@ export const BUILTIN_TOOLS = [
|
|
|
123
123
|
{ type: 'string' },
|
|
124
124
|
{ type: 'array', items: { type: 'string' }, minItems: 1 },
|
|
125
125
|
],
|
|
126
|
-
description: 'Text/regex; pattern[] batches variants in one call.
|
|
126
|
+
description: 'Text/regex; pattern[] batches variants in one call.',
|
|
127
127
|
},
|
|
128
128
|
path: {
|
|
129
129
|
anyOf: [
|
|
@@ -154,7 +154,7 @@ export const BUILTIN_TOOLS = [
|
|
|
154
154
|
name: 'glob',
|
|
155
155
|
title: 'Mixdog Glob',
|
|
156
156
|
annotations: { title: 'Mixdog Glob', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
|
|
157
|
-
description: 'Match exact glob patterns from verified base directories (project root is verified)
|
|
157
|
+
description: 'Match exact glob patterns from verified base directories (project root is verified). Batch pattern[]/path[].',
|
|
158
158
|
inputSchema: {
|
|
159
159
|
type: 'object',
|
|
160
160
|
properties: {
|
|
@@ -351,7 +351,7 @@ export async function executeTreeTool(args, workDir, options = {}) {
|
|
|
351
351
|
// plus a short TTL for serial reuse. Truncated/partial sweeps are
|
|
352
352
|
// known-incomplete and are NEVER cached.
|
|
353
353
|
const FIND_ENUM_CACHE = new Map(); // key -> { files, expiresAt, gen }
|
|
354
|
-
const FIND_ENUM_INFLIGHT = new Map(); // key ->
|
|
354
|
+
const FIND_ENUM_INFLIGHT = new Map(); // key -> { promise, controller, subscribers }
|
|
355
355
|
let FIND_ENUM_GEN = 0;
|
|
356
356
|
|
|
357
357
|
// The broad enumeration is a DERIVED cache the scope/path invalidation layer
|
|
@@ -388,13 +388,56 @@ function parseRgFileList(stdout) {
|
|
|
388
388
|
.map((p) => normalizeOutputPath(p.replace(/^\.[/\\]/, '')));
|
|
389
389
|
}
|
|
390
390
|
|
|
391
|
+
function findEnumerationAbortError(signal) {
|
|
392
|
+
const reason = signal?.reason;
|
|
393
|
+
return reason instanceof Error ? reason : new Error(String(reason || 'find canceled'));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function subscribeToFindEnumeration(key, entry, signal = null) {
|
|
397
|
+
const token = {};
|
|
398
|
+
entry.subscribers.add(token);
|
|
399
|
+
return new Promise((resolve, reject) => {
|
|
400
|
+
let done = false;
|
|
401
|
+
const detach = () => {
|
|
402
|
+
if (signal instanceof AbortSignal) {
|
|
403
|
+
try { signal.removeEventListener('abort', onAbort); } catch { /* ignore */ }
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
const settle = (fn, value) => {
|
|
407
|
+
if (done) return;
|
|
408
|
+
done = true;
|
|
409
|
+
entry.subscribers.delete(token);
|
|
410
|
+
detach();
|
|
411
|
+
fn(value);
|
|
412
|
+
};
|
|
413
|
+
function onAbort() {
|
|
414
|
+
const error = findEnumerationAbortError(signal);
|
|
415
|
+
settle(reject, error);
|
|
416
|
+
// A shared broad sweep survives one caller canceling, but when every
|
|
417
|
+
// subscriber is gone it is useless work and must release its rg.
|
|
418
|
+
if (entry.subscribers.size === 0) {
|
|
419
|
+
try { entry.controller.abort(error); } catch { /* ignore */ }
|
|
420
|
+
if (FIND_ENUM_INFLIGHT.get(key) === entry) FIND_ENUM_INFLIGHT.delete(key);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (signal instanceof AbortSignal) {
|
|
424
|
+
if (signal.aborted) { onAbort(); return; }
|
|
425
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
426
|
+
}
|
|
427
|
+
entry.promise.then(
|
|
428
|
+
(value) => settle(resolve, value),
|
|
429
|
+
(error) => settle(reject, error),
|
|
430
|
+
);
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
391
434
|
// Run (or reuse) the broad `rg --files` sweep for a root. Returns
|
|
392
435
|
// { files, truncated, partial }. The returned `files` array is SHARED — callers
|
|
393
436
|
// must treat it as read-only. `rgArgs` must be the broad-pass args (no per-query
|
|
394
437
|
// narrowing); the cache key includes every enumeration-affecting dimension, so
|
|
395
438
|
// any caller producing an
|
|
396
439
|
// equivalent sweep for the same dims reuses the result.
|
|
397
|
-
async function getBroadEnumeration({ root, hidden, depth, includeNoise, ignoreMode, rgArgs, cwd, runRgImpl = runRg, bestEffort = false }) {
|
|
440
|
+
async function getBroadEnumeration({ root, hidden, depth, includeNoise, ignoreMode, rgArgs, cwd, runRgImpl = runRg, bestEffort = false, signal = null }) {
|
|
398
441
|
const ttl = findEnumTtlMs();
|
|
399
442
|
const key = findEnumKey({ root, hidden, depth, includeNoise, ignoreMode });
|
|
400
443
|
if (ttl > 0) {
|
|
@@ -409,7 +452,7 @@ async function getBroadEnumeration({ root, hidden, depth, includeNoise, ignoreMo
|
|
|
409
452
|
// query[] fan-out should still share the one broad `rg --files` sweep
|
|
410
453
|
// instead of spawning N identical enumerations.
|
|
411
454
|
const inflight = FIND_ENUM_INFLIGHT.get(key);
|
|
412
|
-
if (inflight) return inflight;
|
|
455
|
+
if (inflight) return subscribeToFindEnumeration(key, inflight, signal);
|
|
413
456
|
// Non-competing prewarm: past the cache/single-flight fast paths this runs
|
|
414
457
|
// a fresh `rg` sweep (a child-spawn slot). Best-effort warmers skip that
|
|
415
458
|
// spawn when the gate has no spare capacity, returning a known-incomplete
|
|
@@ -419,8 +462,10 @@ async function getBroadEnumeration({ root, hidden, depth, includeNoise, ignoreMo
|
|
|
419
462
|
return { files: [], truncated: false, partial: true };
|
|
420
463
|
}
|
|
421
464
|
const genAtStart = FIND_ENUM_GEN;
|
|
422
|
-
const
|
|
423
|
-
|
|
465
|
+
const controller = new AbortController();
|
|
466
|
+
const entry = { promise: null, controller, subscribers: new Set() };
|
|
467
|
+
entry.promise = (async () => {
|
|
468
|
+
const stdout = await runRgImpl(rgArgs, { cwd, signal: controller.signal });
|
|
424
469
|
const truncated = Boolean(stdout && typeof stdout === 'object' && stdout.truncated);
|
|
425
470
|
const partial = Boolean(stdout && typeof stdout === 'object' && stdout.partial);
|
|
426
471
|
const files = parseRgFileList(stdout);
|
|
@@ -433,11 +478,12 @@ async function getBroadEnumeration({ root, hidden, depth, includeNoise, ignoreMo
|
|
|
433
478
|
}
|
|
434
479
|
return { files, truncated, partial };
|
|
435
480
|
})();
|
|
436
|
-
FIND_ENUM_INFLIGHT.set(key,
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
481
|
+
FIND_ENUM_INFLIGHT.set(key, entry);
|
|
482
|
+
const cleanup = () => {
|
|
483
|
+
if (FIND_ENUM_INFLIGHT.get(key) === entry) FIND_ENUM_INFLIGHT.delete(key);
|
|
484
|
+
};
|
|
485
|
+
entry.promise.then(cleanup, cleanup);
|
|
486
|
+
return subscribeToFindEnumeration(key, entry, signal);
|
|
441
487
|
}
|
|
442
488
|
|
|
443
489
|
// Best-effort warm of the broad enumeration for a root using the `find` tool's
|
|
@@ -458,6 +504,43 @@ export async function prewarmFindEnumeration(root) {
|
|
|
458
504
|
} catch { /* best-effort warm; never surface */ }
|
|
459
505
|
}
|
|
460
506
|
|
|
507
|
+
function escapeFindGlobLiteral(value) {
|
|
508
|
+
return String(value).replace(/[*?[\]{}]/g, (char) => `[${char}]`);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async function getTargetedFindEnumeration({
|
|
512
|
+
context,
|
|
513
|
+
queries,
|
|
514
|
+
root,
|
|
515
|
+
hidden,
|
|
516
|
+
depth,
|
|
517
|
+
includeNoise,
|
|
518
|
+
runRgImpl,
|
|
519
|
+
signal,
|
|
520
|
+
}) {
|
|
521
|
+
const terms = [...new Set((queries || []).map((query) => String(query || '').trim()).filter(Boolean))];
|
|
522
|
+
const key = JSON.stringify([root, hidden, depth ?? '', includeNoise, terms]);
|
|
523
|
+
const runs = context?.targetedRuns;
|
|
524
|
+
if (runs?.has(key)) return runs.get(key);
|
|
525
|
+
const run = (async () => {
|
|
526
|
+
const rgArgs = ['--files', '--no-ignore'];
|
|
527
|
+
if (hidden) rgArgs.push('--hidden');
|
|
528
|
+
if (depth != null) rgArgs.push('--max-depth', String(depth));
|
|
529
|
+
for (const query of terms) rgArgs.push('--iglob', `*${escapeFindGlobLiteral(query)}*`);
|
|
530
|
+
rgArgs.push('.');
|
|
531
|
+
const stdout = await runRgImpl(rgArgs, { cwd: root, signal });
|
|
532
|
+
const paths = parseRgFileList(stdout).filter((path) =>
|
|
533
|
+
includeNoise || !path.split('/').some((segment) => NOISE_DIR_NAMES.has(segment)));
|
|
534
|
+
return {
|
|
535
|
+
files: paths,
|
|
536
|
+
truncated: Boolean(stdout && typeof stdout === 'object' && stdout.truncated),
|
|
537
|
+
partial: Boolean(stdout && typeof stdout === 'object' && stdout.partial),
|
|
538
|
+
};
|
|
539
|
+
})();
|
|
540
|
+
if (runs) runs.set(key, run);
|
|
541
|
+
return run;
|
|
542
|
+
}
|
|
543
|
+
|
|
461
544
|
// Fuzzy filename search (nucleo-style): collect the file
|
|
462
545
|
// list via `rg --files`, then rank by subsequence score. `list.fuzzy` still
|
|
463
546
|
// routes here for hidden backward compatibility, but the model-facing tool is
|
|
@@ -468,30 +551,22 @@ export async function executeFuzzyFindTool(args, workDir, options = {}) {
|
|
|
468
551
|
const capped = list.length > 5;
|
|
469
552
|
const targets = capped ? list.slice(0, 5) : list;
|
|
470
553
|
if (targets.length > 1) {
|
|
471
|
-
//
|
|
472
|
-
//
|
|
473
|
-
//
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
const FIND_FANOUT_CONCURRENCY = 4;
|
|
479
|
-
const bodies = new Array(targets.length);
|
|
480
|
-
let cursor = 0;
|
|
481
|
-
const runWorker = async () => {
|
|
482
|
-
for (;;) {
|
|
483
|
-
const i = cursor++;
|
|
484
|
-
if (i >= targets.length) return;
|
|
485
|
-
try {
|
|
486
|
-
bodies[i] = await executeFuzzyFindTool({ ...args, query: targets[i] }, workDir, options);
|
|
487
|
-
} catch (err) {
|
|
488
|
-
bodies[i] = `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`;
|
|
489
|
-
}
|
|
490
|
-
}
|
|
554
|
+
// Full parallelism is intentional. All sections share one common
|
|
555
|
+
// broad enumeration and one unioned ignored/truncated targeted pass,
|
|
556
|
+
// but ranking and output remain isolated per query.
|
|
557
|
+
const batchContext = {
|
|
558
|
+
queries: targets,
|
|
559
|
+
targetedRuns: new Map(),
|
|
560
|
+
itemsByFiles: new WeakMap(),
|
|
491
561
|
};
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
562
|
+
const nestedOptions = { ...options, __findBatchContext: batchContext };
|
|
563
|
+
const bodies = await Promise.all(targets.map(async (query) => {
|
|
564
|
+
try {
|
|
565
|
+
return await executeFuzzyFindTool({ ...args, query }, workDir, nestedOptions);
|
|
566
|
+
} catch (err) {
|
|
567
|
+
return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`;
|
|
568
|
+
}
|
|
569
|
+
}));
|
|
495
570
|
const sections = targets.map((q, i) => `# find ${q}\n${bodies[i]}`);
|
|
496
571
|
if (capped) sections.push(`... [capped at 5 of ${list.length} queries]`);
|
|
497
572
|
return sections.join('\n\n');
|
|
@@ -551,7 +626,6 @@ export async function executeFuzzyFindTool(args, workDir, options = {}) {
|
|
|
551
626
|
// literal path separator and does NOT escape. So a query like "[slug].tsx"
|
|
552
627
|
// still produces the intended `*[[]slug[]].tsx*` include instead of a
|
|
553
628
|
// character-class that matches one of s/l/u/g.
|
|
554
|
-
const escapeGlobLiteral = (s) => s.replace(/[*?[\]{}]/g, (c) => `[${c}]`);
|
|
555
629
|
// Test-only seam: allow a caller to inject a runRg stand-in (e.g. to
|
|
556
630
|
// simulate a truncated broad pass) without touching the production path.
|
|
557
631
|
// Never set on the real tool-execution options object.
|
|
@@ -576,13 +650,19 @@ export async function executeFuzzyFindTool(args, workDir, options = {}) {
|
|
|
576
650
|
rgArgs: [...baseRgArgs, ...ignoreGlobs, '.'],
|
|
577
651
|
cwd: fullPath,
|
|
578
652
|
runRgImpl,
|
|
653
|
+
signal: options.signal,
|
|
579
654
|
});
|
|
580
655
|
} catch (err) {
|
|
581
656
|
return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`;
|
|
582
657
|
}
|
|
583
658
|
let rgTruncated = broadEnum.truncated;
|
|
584
659
|
let rgPartial = broadEnum.partial;
|
|
585
|
-
const
|
|
660
|
+
const batchContext = options?.__findBatchContext;
|
|
661
|
+
let passOneItems = batchContext?.itemsByFiles?.get(broadEnum.files);
|
|
662
|
+
if (!passOneItems) {
|
|
663
|
+
passOneItems = broadEnum.files.map((path) => ({ path }));
|
|
664
|
+
batchContext?.itemsByFiles?.set(broadEnum.files, passOneItems);
|
|
665
|
+
}
|
|
586
666
|
// Any single path segment is an exact-name candidate, including names with
|
|
587
667
|
// spaces, extensionless names, and literal glob characters. The comparison
|
|
588
668
|
// below is direct, so glob syntax has no special meaning in this decision.
|
|
@@ -590,57 +670,36 @@ export async function executeFuzzyFindTool(args, workDir, options = {}) {
|
|
|
590
670
|
const queryLower = query.toLowerCase();
|
|
591
671
|
const passOneHasExactBasename = !exactFilenameQuery || broadEnum.files.some((path) =>
|
|
592
672
|
path.slice(path.lastIndexOf('/') + 1).toLowerCase() === queryLower);
|
|
593
|
-
const
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
//
|
|
599
|
-
//
|
|
600
|
-
|
|
673
|
+
const rankLimit = headLimit > 0 ? headLimit + 1 : headLimit;
|
|
674
|
+
const passOneRanked = fuzzyRank(query, passOneItems, rankLimit);
|
|
675
|
+
const passOneHasCandidate = passOneRanked.length > 0;
|
|
676
|
+
let targetedProbeRan = false;
|
|
677
|
+
let targetedPaths = [];
|
|
678
|
+
// A full --no-ignore enumeration can exceed 20MB, then trigger one more
|
|
679
|
+
// whole-tree narrowed pass PER query. Probe only requested names instead,
|
|
680
|
+
// unioning every query in this find batch into one rg process.
|
|
681
|
+
if (rgTruncated || rgPartial || (!includeNoise && (!passOneHasCandidate || !passOneHasExactBasename))) {
|
|
601
682
|
try {
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
683
|
+
const targeted = await getTargetedFindEnumeration({
|
|
684
|
+
context: batchContext,
|
|
685
|
+
queries: batchContext?.queries || [query],
|
|
686
|
+
root: fullPath,
|
|
687
|
+
hidden,
|
|
688
|
+
depth,
|
|
689
|
+
includeNoise,
|
|
690
|
+
runRgImpl,
|
|
691
|
+
signal: options.signal,
|
|
607
692
|
});
|
|
608
|
-
rgTruncated ||=
|
|
609
|
-
rgPartial ||=
|
|
610
|
-
|
|
611
|
-
|
|
693
|
+
rgTruncated ||= targeted.truncated;
|
|
694
|
+
rgPartial ||= targeted.partial;
|
|
695
|
+
targetedPaths = targeted.files.filter((path) => path.toLowerCase().includes(queryLower));
|
|
696
|
+
targetedProbeRan = true;
|
|
612
697
|
} catch {
|
|
613
|
-
|
|
614
|
-
// existing partial warning and prevent the final output cache from
|
|
615
|
-
// preserving a pass-one-only answer.
|
|
698
|
+
if (options.signal?.aborted) throw findEnumerationAbortError(options.signal);
|
|
616
699
|
rgPartial = true;
|
|
617
700
|
}
|
|
618
701
|
}
|
|
619
|
-
//
|
|
620
|
-
// (case-insensitive substring glob). This output is tiny and effectively
|
|
621
|
-
// never truncated, so exact/substring hits are guaranteed to reach ranking
|
|
622
|
-
// regardless of whether the broad pass was cut at the cap. Best-effort:
|
|
623
|
-
// failures here never fail the tool — the broad pass still stands.
|
|
624
|
-
let narrowPaths = [];
|
|
625
|
-
// Only spawn the per-query narrowed rg pass when the broad enumeration was
|
|
626
|
-
// cut (cap/exit-2): an untruncated broad pass already contains every file,
|
|
627
|
-
// so every narrowed substring hit is a broad-set duplicate that the
|
|
628
|
-
// dedup below drops — running it would add zero items. Skipping it there
|
|
629
|
-
// removes a redundant full-tree walk per query with identical output.
|
|
630
|
-
if (rgTruncated || rgPartial) try {
|
|
631
|
-
// A positive --iglob whitelist makes ripgrep re-admit paths its own
|
|
632
|
-
// `!**/<noise>/**` negations would otherwise exclude (the whitelist
|
|
633
|
-
// wins regardless of glob order), so noise dirs are pruned in JS here
|
|
634
|
-
// instead — matching the broad pass's effective exclusion set.
|
|
635
|
-
const narrowBaseArgs = fallbackRan
|
|
636
|
-
? ['--files', '--no-ignore', ...baseRgArgs.slice(2)]
|
|
637
|
-
: baseRgArgs;
|
|
638
|
-
const narrowStdout = await runRgImpl([...narrowBaseArgs, '--iglob', `*${escapeGlobLiteral(query)}*`, '.'], { cwd: fullPath });
|
|
639
|
-
narrowPaths = parseRgFiles(narrowStdout).filter((p) =>
|
|
640
|
-
includeNoise || !p.split('/').some((seg) => NOISE_DIR_NAMES.has(seg)));
|
|
641
|
-
} catch { /* best-effort backstop; broad pass already collected */ }
|
|
642
|
-
// Merge pass one + fallback + narrowed, deduplicating by path. Pass-one
|
|
643
|
-
// order always wins so the fallback cannot perturb common-path ranking.
|
|
702
|
+
// Merge common + targeted results, preserving common-path order.
|
|
644
703
|
const seen = new Set();
|
|
645
704
|
const items = [];
|
|
646
705
|
for (const p of broadEnum.files) {
|
|
@@ -648,18 +707,14 @@ export async function executeFuzzyFindTool(args, workDir, options = {}) {
|
|
|
648
707
|
seen.add(p);
|
|
649
708
|
items.push({ path: p });
|
|
650
709
|
}
|
|
651
|
-
for (const p of
|
|
710
|
+
for (const p of targetedPaths) {
|
|
652
711
|
if (seen.has(p)) continue;
|
|
653
712
|
seen.add(p);
|
|
654
713
|
items.push({ path: p });
|
|
655
714
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
items.push({ path: p });
|
|
660
|
-
}
|
|
661
|
-
const rankLimit = headLimit > 0 ? headLimit + 1 : headLimit;
|
|
662
|
-
const rankedRaw = fuzzyRank(query, items, rankLimit);
|
|
715
|
+
const rankedRaw = targetedPaths.length > 0
|
|
716
|
+
? fuzzyRank(query, items, rankLimit)
|
|
717
|
+
: passOneRanked;
|
|
663
718
|
const hasMore = headLimit > 0 && rankedRaw.length > headLimit;
|
|
664
719
|
const ranked = hasMore ? rankedRaw.slice(0, headLimit) : rankedRaw;
|
|
665
720
|
// Build output lines uniformly for the hit and no-match cases so a
|
|
@@ -671,7 +726,7 @@ export async function executeFuzzyFindTool(args, workDir, options = {}) {
|
|
|
671
726
|
if (!noMatch && hasMore) lines.push(`... (top ${headLimit}; raise head_limit for more)`);
|
|
672
727
|
if (rgTruncated) lines.push('... [warning] rg stdout truncated at 20MB cap; broad ranking incomplete (exact-name hits still merged)');
|
|
673
728
|
if (rgPartial && !rgTruncated) lines.push('... [warning] rg exit 2 (partial results); broad ranking may be incomplete');
|
|
674
|
-
if (!
|
|
729
|
+
if (!targetedProbeRan && headLimit > 0 && passOneRanked.length >= headLimit) {
|
|
675
730
|
lines.push('[gitignored trees not searched; retry with include_noise:true]');
|
|
676
731
|
}
|
|
677
732
|
const result = lines.join('\n');
|
|
@@ -834,12 +889,13 @@ export async function executeFindFilesTool(args, workDir, options = {}) {
|
|
|
834
889
|
root: normalizeOutputPath(fullPath),
|
|
835
890
|
hidden, depth, includeNoise, ignoreMode: 'all',
|
|
836
891
|
rgArgs, cwd: fullPath,
|
|
892
|
+
signal: options.signal,
|
|
837
893
|
});
|
|
838
894
|
rgStdoutTruncated = enumRes.truncated;
|
|
839
895
|
rgStdoutPartial = enumRes.partial;
|
|
840
896
|
relPaths = enumRes.files;
|
|
841
897
|
} else {
|
|
842
|
-
const stdout = await runRg(rgArgs, { cwd: fullPath });
|
|
898
|
+
const stdout = await runRg(rgArgs, { cwd: fullPath, signal: options.signal });
|
|
843
899
|
rgStdoutTruncated = Boolean(stdout && typeof stdout === 'object' && stdout.truncated);
|
|
844
900
|
rgStdoutPartial = Boolean(stdout && typeof stdout === 'object' && stdout.partial);
|
|
845
901
|
relPaths = parseRgFileList(stdout);
|
|
@@ -5,6 +5,7 @@ import { extname } from 'path';
|
|
|
5
5
|
import { normalizeInputPath } from './path-utils.mjs';
|
|
6
6
|
import { buildNotFoundHint, finalizeReadFamilyEnoentTail, tryReadFamilyEnoentRedirect } from './search-path-diagnostics.mjs';
|
|
7
7
|
import { getReadSnapshot } from './read-snapshot-runtime.mjs';
|
|
8
|
+
import { recordReadPathRedirect } from './snapshot-store.mjs';
|
|
8
9
|
import { snapshotCoversFullFile, statMatchesSnapshot } from './snapshot-helpers.mjs';
|
|
9
10
|
import { formatBinaryReadPreview } from './binary-file.mjs';
|
|
10
11
|
|
|
@@ -247,6 +248,11 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
|
|
|
247
248
|
opts,
|
|
248
249
|
helpers,
|
|
249
250
|
),
|
|
251
|
+
onRedirect: (requested, target) => recordReadPathRedirect(
|
|
252
|
+
requested,
|
|
253
|
+
resolveAgainstCwd(normalizeInputPath(target), workDir),
|
|
254
|
+
readStateScope,
|
|
255
|
+
),
|
|
250
256
|
});
|
|
251
257
|
if (redirected) return redirected;
|
|
252
258
|
const similar = findSimilarFile(fullPath);
|