mixdog 0.9.135 → 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/help.mjs +0 -2
- package/src/rules/lead/01-general.md +0 -4
- package/src/rules/shared/01-tool.md +10 -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/builtin-tools.mjs +0 -5
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +17 -4
- package/src/runtime/agent/orchestrator/tools/builtin/edit-sequential-occupation.test.mjs +40 -0
- 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 +34 -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 +124 -18
- 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/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/graph-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +78 -12
- package/src/runtime/agent/orchestrator/tools/lib/shell-warm-standby.mjs +34 -3
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +12 -5
- 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/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 +1 -19
- 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 +2 -7
- package/src/session-runtime/prewarm.test.mjs +0 -1
- package/src/session-runtime/runtime-core.mjs +6 -185
- 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/standalone/channel-admin.mjs +8 -150
- package/src/standalone/daemon.mjs +5 -48
- 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
|
@@ -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
|
}
|
|
@@ -567,6 +568,7 @@ export async function tryServeSearch(argsList, execOptions = {}, opts = {}) {
|
|
|
567
568
|
deadlineMs: softDeadlineMs(remaining),
|
|
568
569
|
keepWarm: opts.keepWarm === true,
|
|
569
570
|
...(opts.bulkHint === true ? { bulkHint: true } : {}),
|
|
571
|
+
...(opts.mtimeTopK === true ? { mtimeTopK: true } : {}),
|
|
570
572
|
});
|
|
571
573
|
let response = await requestNativeWithRestart(buildRequest, execOptions, deadlineMs);
|
|
572
574
|
// Deadline-swallow defense (binaries before the serve_search response-level
|
|
@@ -638,6 +640,7 @@ export async function tryServeSearch(argsList, execOptions = {}, opts = {}) {
|
|
|
638
640
|
totalSeen: Math.max(0, Math.floor(Number(response.totalSeen) || 0)),
|
|
639
641
|
partial: response.partial === true,
|
|
640
642
|
timeout: response.timeout === true,
|
|
643
|
+
filesScanned: Math.max(0, Math.floor(Number(response.filesScanned) || 0)),
|
|
641
644
|
// Unreadable/half-read files the native scanner skipped: route the count
|
|
642
645
|
// through the existing rg-exit-2 partial phrasing in search-tool.mjs so
|
|
643
646
|
// the model sees WHY the result may be missing matches.
|
|
@@ -713,11 +716,42 @@ export async function tryServeFuzzySearch(args, execOptions = {}) {
|
|
|
713
716
|
cacheSafe: response.cacheSafe !== false,
|
|
714
717
|
queueMs: Math.max(0, Number(response.queueMs) || 0),
|
|
715
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),
|
|
716
721
|
requestClass: response.class === 'fuzzy' ? 'fuzzy' : 'bulk',
|
|
717
722
|
served: true,
|
|
718
723
|
};
|
|
719
724
|
}
|
|
720
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
|
+
|
|
721
755
|
export function _resetNativeSearchClientForTest() {
|
|
722
756
|
_teardown(new Error('test reset'), { countFailure: false });
|
|
723
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
|
|
@@ -476,6 +476,7 @@ export async function streamSmartReadSummary(fullPath, st, source = 'read_smart_
|
|
|
476
476
|
};
|
|
477
477
|
|
|
478
478
|
const deadline = Date.now() + READ_STREAM_TIMEOUT_MS;
|
|
479
|
+
let scanCompleted = false;
|
|
479
480
|
try {
|
|
480
481
|
while (position < st.size) {
|
|
481
482
|
if (Date.now() > deadline) throw new Error(`read timed out after ${READ_STREAM_TIMEOUT_MS}ms`);
|
|
@@ -513,10 +514,12 @@ export async function streamSmartReadSummary(fullPath, st, source = 'read_smart_
|
|
|
513
514
|
: Buffer.concat(pendingParts, pendingBytes);
|
|
514
515
|
finishLine(lineBuf, currentLineStartByte, st.size);
|
|
515
516
|
}
|
|
517
|
+
scanCompleted = true;
|
|
516
518
|
} finally {
|
|
517
|
-
await fh.close().catch(() => {});
|
|
519
|
+
if (!scanCompleted) await fh.close().catch(() => {});
|
|
518
520
|
}
|
|
519
521
|
|
|
522
|
+
try {
|
|
520
523
|
const tailLen = Math.min(tailCount, SMART_READ_TAIL_LINES);
|
|
521
524
|
const tailOffsets = [];
|
|
522
525
|
for (let i = tailCount - tailLen; i < tailCount; i++) {
|
|
@@ -530,14 +533,9 @@ export async function streamSmartReadSummary(fullPath, st, source = 'read_smart_
|
|
|
530
533
|
const byteLen = Math.max(0, lastEnd - firstStart);
|
|
531
534
|
let tailWindow = Buffer.alloc(0);
|
|
532
535
|
if (byteLen > 0) {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
const { bytesRead } = await tailFh.read(tailWindow, 0, byteLen, firstStart);
|
|
537
|
-
if (bytesRead < byteLen) tailWindow = tailWindow.subarray(0, bytesRead);
|
|
538
|
-
} finally {
|
|
539
|
-
await tailFh.close().catch(() => {});
|
|
540
|
-
}
|
|
536
|
+
tailWindow = Buffer.allocUnsafe(byteLen);
|
|
537
|
+
const { bytesRead } = await fh.read(tailWindow, 0, byteLen, firstStart);
|
|
538
|
+
if (bytesRead < byteLen) tailWindow = tailWindow.subarray(0, bytesRead);
|
|
541
539
|
}
|
|
542
540
|
for (const entry of tailOffsets) {
|
|
543
541
|
let rawBuf = tailWindow.subarray(entry.startByte - firstStart, entry.endByte - firstStart);
|
|
@@ -604,4 +602,7 @@ export async function streamSmartReadSummary(fullPath, st, source = 'read_smart_
|
|
|
604
602
|
rangeHashes,
|
|
605
603
|
},
|
|
606
604
|
};
|
|
605
|
+
} finally {
|
|
606
|
+
await fh.close().catch(() => {});
|
|
607
|
+
}
|
|
607
608
|
}
|
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
cacheSet,
|
|
61
61
|
runResultCacheInFlight,
|
|
62
62
|
statPathsForMtime,
|
|
63
|
+
visitPathsForMtime,
|
|
63
64
|
} from './cache-layers.mjs';
|
|
64
65
|
import { recordLocalSearchCacheHit } from './local-search-telemetry.mjs';
|
|
65
66
|
import { applyGrepContextLeadPolicy, GREP_CONTEXT_MAX, hasUnsupportedRipgrepRegex } from './arg-guard.mjs';
|
|
@@ -121,6 +122,58 @@ function _globDefaultHeadLimit() {
|
|
|
121
122
|
return parsed > 0 ? parsed : 100;
|
|
122
123
|
}
|
|
123
124
|
|
|
125
|
+
function compareGlobMtimeEntries(a, b) {
|
|
126
|
+
const dm = b.mtime - a.mtime;
|
|
127
|
+
if (dm !== 0) return dm;
|
|
128
|
+
return globMtimeTiePath(a).localeCompare(globMtimeTiePath(b));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function retainBestEntries(heap, candidate, limit, compare) {
|
|
132
|
+
if (heap.length < limit) {
|
|
133
|
+
heap.push(candidate);
|
|
134
|
+
for (let index = heap.length - 1; index > 0;) {
|
|
135
|
+
const parent = Math.floor((index - 1) / 2);
|
|
136
|
+
if (compare(heap[index], heap[parent]) <= 0) break;
|
|
137
|
+
[heap[index], heap[parent]] = [heap[parent], heap[index]];
|
|
138
|
+
index = parent;
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (compare(candidate, heap[0]) >= 0) return;
|
|
143
|
+
heap[0] = candidate;
|
|
144
|
+
for (let index = 0;;) {
|
|
145
|
+
const left = index * 2 + 1;
|
|
146
|
+
const right = left + 1;
|
|
147
|
+
let worst = index;
|
|
148
|
+
if (left < heap.length && compare(heap[left], heap[worst]) > 0) worst = left;
|
|
149
|
+
if (right < heap.length && compare(heap[right], heap[worst]) > 0) worst = right;
|
|
150
|
+
if (worst === index) break;
|
|
151
|
+
[heap[index], heap[worst]] = [heap[worst], heap[index]];
|
|
152
|
+
index = worst;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function _createGlobMtimeTopK(limit) {
|
|
157
|
+
const cap = Math.max(1, Math.floor(Number(limit) || 1));
|
|
158
|
+
const statted = [];
|
|
159
|
+
const unstatted = [];
|
|
160
|
+
return {
|
|
161
|
+
add(entry, index) {
|
|
162
|
+
if (entry?.stat != null) {
|
|
163
|
+
retainBestEntries(statted, entry, cap, compareGlobMtimeEntries);
|
|
164
|
+
} else {
|
|
165
|
+
retainBestEntries(unstatted, { ...entry, _walkIndex: index }, cap,
|
|
166
|
+
(a, b) => a._walkIndex - b._walkIndex);
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
values() {
|
|
170
|
+
statted.sort(compareGlobMtimeEntries);
|
|
171
|
+
unstatted.sort((a, b) => a._walkIndex - b._walkIndex);
|
|
172
|
+
return [...statted, ...unstatted].slice(0, cap);
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
124
177
|
function _grepContextCharBudget(options = {}) {
|
|
125
178
|
const explicit = Number(options?._grepContextCharBudget);
|
|
126
179
|
if (Number.isFinite(explicit) && explicit > 0) return Math.floor(explicit);
|
|
@@ -338,8 +391,11 @@ export async function executeGlobTool(args, workDir, options = {}) {
|
|
|
338
391
|
let rgStdoutPartial = false;
|
|
339
392
|
let rgWindowIncomplete = false;
|
|
340
393
|
let rgCacheUnsafe = false;
|
|
394
|
+
let nativeMtimeTotal = null;
|
|
341
395
|
const accumCap = 50000;
|
|
342
396
|
const canWindowNatural = sortMode === 'natural' && headLimit !== Infinity;
|
|
397
|
+
const canWindowMtime = sortMode === 'mtime'
|
|
398
|
+
&& headLimit !== Infinity;
|
|
343
399
|
const groupRuns = await Promise.all(globGroups.map(async ([root, rels]) => {
|
|
344
400
|
const rgArgs = ['--files', '--hidden'];
|
|
345
401
|
// Explicit literal basenames (no glob magic in the final segment)
|
|
@@ -391,6 +447,24 @@ export async function executeGlobTool(args, workDir, options = {}) {
|
|
|
391
447
|
};
|
|
392
448
|
}
|
|
393
449
|
try {
|
|
450
|
+
if (canWindowMtime) {
|
|
451
|
+
const served = await runRgWindowedLines(
|
|
452
|
+
rgArgs,
|
|
453
|
+
{ cwd: rgCwd, signal: sharedSignal },
|
|
454
|
+
{ offset: 0, limit: offset + headLimit + 1, mtimeTopK: true },
|
|
455
|
+
);
|
|
456
|
+
return {
|
|
457
|
+
error: null,
|
|
458
|
+
paths: served.lines.map((line) =>
|
|
459
|
+
isAbsolute(line) ? line : resolveAgainstCwd(line, rgCwd)),
|
|
460
|
+
stdoutTruncated: false,
|
|
461
|
+
stdoutPartial: served.partial === true,
|
|
462
|
+
windowIncomplete: false,
|
|
463
|
+
cacheSafe: served.cacheSafe !== false,
|
|
464
|
+
nativeMtime: true,
|
|
465
|
+
totalSeen: served.totalSeen,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
394
468
|
if (canWindowNatural) {
|
|
395
469
|
const served = await runRgWindowedLines(
|
|
396
470
|
rgArgs,
|
|
@@ -440,6 +514,9 @@ export async function executeGlobTool(args, workDir, options = {}) {
|
|
|
440
514
|
if (run.stdoutPartial) rgStdoutPartial = true;
|
|
441
515
|
if (run.windowIncomplete) rgWindowIncomplete = true;
|
|
442
516
|
if (run.cacheSafe === false) rgCacheUnsafe = true;
|
|
517
|
+
if (run.nativeMtime) {
|
|
518
|
+
nativeMtimeTotal = (nativeMtimeTotal ?? 0) + Math.max(0, Number(run.totalSeen) || 0);
|
|
519
|
+
}
|
|
443
520
|
for (const p of run.paths) {
|
|
444
521
|
allFiles.push(p);
|
|
445
522
|
if (allFiles.length >= accumCap) {
|
|
@@ -454,34 +531,59 @@ export async function executeGlobTool(args, workDir, options = {}) {
|
|
|
454
531
|
|
|
455
532
|
const unique = Array.from(new Set(allFiles));
|
|
456
533
|
let orderedPaths;
|
|
457
|
-
if (
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
const withStatAll = await statPathsForMtime(unique, workDir, 64, { deadlineMs: 5000 });
|
|
534
|
+
if (nativeMtimeTotal !== null && globGroups.length === 1) {
|
|
535
|
+
orderedPaths = unique;
|
|
536
|
+
} else if (nativeMtimeTotal !== null) {
|
|
537
|
+
const withStat = await statPathsForMtime(unique, workDir, 64, { deadlineMs: 5000 });
|
|
462
538
|
const statted = [];
|
|
463
539
|
const unstatted = [];
|
|
464
|
-
for (const entry of
|
|
540
|
+
for (const entry of withStat) {
|
|
465
541
|
if (!entry) continue;
|
|
466
542
|
if (entry.stat != null) statted.push(entry);
|
|
467
543
|
else unstatted.push(entry);
|
|
468
544
|
}
|
|
469
|
-
statted.sort(
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
//
|
|
475
|
-
//
|
|
476
|
-
|
|
545
|
+
statted.sort(compareGlobMtimeEntries);
|
|
546
|
+
orderedPaths = [...statted, ...unstatted]
|
|
547
|
+
.map((entry) => entry.full || resolveAgainstCwd(entry.path, workDir));
|
|
548
|
+
} else if (sortMode === 'mtime') {
|
|
549
|
+
// Default mtime sorting stats every match. Bound the post-rg stat
|
|
550
|
+
// phase so a hung mount cannot pin glob until the agent stall
|
|
551
|
+
// watchdog fires.
|
|
552
|
+
if (headLimit === Infinity) {
|
|
553
|
+
const withStatAll = await statPathsForMtime(unique, workDir, 64, { deadlineMs: 5000 });
|
|
554
|
+
const statted = [];
|
|
555
|
+
const unstatted = [];
|
|
556
|
+
for (const entry of withStatAll) {
|
|
557
|
+
if (!entry) continue;
|
|
558
|
+
if (entry.stat != null) statted.push(entry);
|
|
559
|
+
else unstatted.push(entry);
|
|
560
|
+
}
|
|
561
|
+
statted.sort(compareGlobMtimeEntries);
|
|
562
|
+
// A deadline-expired or failed stat degrades ORDERING only, never
|
|
563
|
+
// completeness: unsortable entries keep their walk order at the tail.
|
|
564
|
+
orderedPaths = [...statted, ...unstatted]
|
|
565
|
+
.map((entry) => entry.full || resolveAgainstCwd(entry.path, workDir));
|
|
566
|
+
} else {
|
|
567
|
+
// Scan and stat every match for exact global ordering, but retain
|
|
568
|
+
// only the requested page plus one lookahead row. This changes
|
|
569
|
+
// O(N log N) sorting and O(N) result retention to O(N log K) and
|
|
570
|
+
// O(K) without narrowing the searched candidate set.
|
|
571
|
+
const selector = _createGlobMtimeTopK(
|
|
572
|
+
Math.min(unique.length, offset + headLimit + 1),
|
|
573
|
+
);
|
|
574
|
+
await visitPathsForMtime(unique, workDir, 64, { deadlineMs: 5000 },
|
|
575
|
+
(entry, index) => selector.add(entry, index));
|
|
576
|
+
orderedPaths = selector.values()
|
|
577
|
+
.map((entry) => entry.full || resolveAgainstCwd(entry.path, workDir));
|
|
578
|
+
}
|
|
477
579
|
} else {
|
|
478
580
|
orderedPaths = unique.map((entry) => isAbsolute(entry) ? resolve(entry) : resolveAgainstCwd(entry, workDir));
|
|
479
581
|
}
|
|
480
|
-
const totalBeforeOffset =
|
|
582
|
+
const totalBeforeOffset = nativeMtimeTotal ?? unique.length;
|
|
481
583
|
const windowed = offset > 0 ? orderedPaths.slice(offset) : orderedPaths;
|
|
482
584
|
const capped = (headLimit === Infinity ? windowed : windowed.slice(0, headLimit))
|
|
483
585
|
.map((abs) => relativeSearchResultPath(abs, workDir));
|
|
484
|
-
const remaining =
|
|
586
|
+
const remaining = Math.max(0, totalBeforeOffset - offset - capped.length);
|
|
485
587
|
const truncSuffix = accumTruncated
|
|
486
588
|
? '\n... [truncated at accumulation cap (50000)]'
|
|
487
589
|
: (rgStdoutTruncated ? '\n... [truncated at rg stdout cap (20MB); results incomplete]' : '')
|
|
@@ -507,8 +609,12 @@ export async function executeGlobTool(args, workDir, options = {}) {
|
|
|
507
609
|
if (options?.scopedCacheOutcome && (accumTruncated || rgStdoutTruncated || rgStdoutPartial || rgWindowIncomplete || remaining > 0)) {
|
|
508
610
|
markScopedCacheIncomplete(options.scopedCacheOutcome);
|
|
509
611
|
}
|
|
510
|
-
|
|
511
|
-
|
|
612
|
+
// Pagination is not an incomplete computation: limit/offset are part of
|
|
613
|
+
// the cache key, and the native watcher invalidates the scoped page on any
|
|
614
|
+
// relevant namespace/mtime change. Cache exact pages even when more rows
|
|
615
|
+
// exist; only a genuinely partial scan must bypass reuse.
|
|
616
|
+
const globComputationIncomplete = accumTruncated || rgStdoutTruncated || rgStdoutPartial || rgErrors.length > 0;
|
|
617
|
+
if (!globComputationIncomplete && !rgCacheUnsafe) {
|
|
512
618
|
cacheSet(cacheKey, out, { scopes: [...groups.keys()].map((root) => resolvedForSearchRoot(root)) });
|
|
513
619
|
}
|
|
514
620
|
// ② completion progress (claude "Found N" parity). Best-effort, no-op
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
|
|
4
|
+
import { _createGlobMtimeTopK } from './search-glob-tool.mjs';
|
|
5
|
+
|
|
6
|
+
function entry(path, mtime, stat = {}) {
|
|
7
|
+
return { path, full: path, mtime, mtimeMs: mtime, stat };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
test('glob mtime top-K exactly matches full ordering and preserves failed-stat walk order', () => {
|
|
11
|
+
const entries = [
|
|
12
|
+
entry('z-old', 1),
|
|
13
|
+
entry('b-tie', 9),
|
|
14
|
+
entry('failed-first', 0, null),
|
|
15
|
+
entry('a-tie', 9),
|
|
16
|
+
entry('newest', 12),
|
|
17
|
+
entry('failed-second', 0, null),
|
|
18
|
+
entry('middle', 5),
|
|
19
|
+
];
|
|
20
|
+
const selector = _createGlobMtimeTopK(6);
|
|
21
|
+
const completionOrder = [5, 1, 6, 2, 4, 0, 3];
|
|
22
|
+
for (const index of completionOrder) selector.add(entries[index], index);
|
|
23
|
+
|
|
24
|
+
assert.deepEqual(
|
|
25
|
+
selector.values().map((item) => item.path),
|
|
26
|
+
['newest', 'a-tie', 'b-tie', 'middle', 'z-old', 'failed-first'],
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('glob mtime top-K retains only the requested bound', () => {
|
|
31
|
+
const selector = _createGlobMtimeTopK(3);
|
|
32
|
+
for (let index = 0; index < 100; index += 1) {
|
|
33
|
+
selector.add(entry(`file-${String(index).padStart(3, '0')}`, index), index);
|
|
34
|
+
}
|
|
35
|
+
assert.deepEqual(
|
|
36
|
+
selector.values().map((item) => item.path),
|
|
37
|
+
['file-099', 'file-098', 'file-097'],
|
|
38
|
+
);
|
|
39
|
+
});
|