opencode-rag-plugin 1.19.3 → 1.19.5
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/dist/api.d.ts +1 -1
- package/dist/api.js +2 -0
- package/dist/chunker/base.js +19 -5
- package/dist/chunker/factory.js +27 -9
- package/dist/chunker/grammar.d.ts +18 -1
- package/dist/chunker/grammar.js +48 -10
- package/dist/chunker/pdf.js +30 -14
- package/dist/cli/commands/init-helpers.js +15 -2
- package/dist/cli/commands/init.js +31 -21
- package/dist/cli/commands/query.js +2 -0
- package/dist/cli/commands/quirk.js +9 -3
- package/dist/cli/commands/setup.js +5 -2
- package/dist/cli/commands/status.js +14 -5
- package/dist/cli/commands/ui.js +23 -9
- package/dist/cli/commands/update.js +4 -5
- package/dist/cli/format.d.ts +5 -2
- package/dist/cli/format.js +14 -5
- package/dist/content/image.js +33 -11
- package/dist/content/reader.js +79 -17
- package/dist/core/bootstrap.js +10 -3
- package/dist/core/config.js +31 -0
- package/dist/core/desc-cache.d.ts +8 -2
- package/dist/core/desc-cache.js +10 -3
- package/dist/core/doc-progress.js +5 -2
- package/dist/core/interfaces.d.ts +8 -0
- package/dist/core/interfaces.js +8 -1
- package/dist/core/provider-defaults.d.ts +2 -0
- package/dist/core/provider-defaults.js +19 -4
- package/dist/core/runtime-overrides.d.ts +0 -6
- package/dist/core/version-check.d.ts +5 -0
- package/dist/core/version-check.js +8 -2
- package/dist/describer/anthropic.d.ts +2 -2
- package/dist/describer/anthropic.js +19 -5
- package/dist/describer/describer.js +15 -2
- package/dist/describer/gemini.js +25 -10
- package/dist/embedder/factory.d.ts +5 -3
- package/dist/embedder/factory.js +41 -8
- package/dist/embedder/health.js +19 -19
- package/dist/embedder/http.d.ts +14 -1
- package/dist/embedder/http.js +60 -6
- package/dist/eval/session-logger.js +7 -0
- package/dist/eval/storage.js +8 -0
- package/dist/indexer/git-diff.d.ts +1 -1
- package/dist/indexer/git-diff.js +5 -1
- package/dist/indexer/pipeline.js +421 -344
- package/dist/indexer/stats.d.ts +2 -0
- package/dist/indexer/stats.js +1 -0
- package/dist/indexer/watch.js +8 -1
- package/dist/indexer/worker.js +21 -0
- package/dist/mcp/cli.js +4 -0
- package/dist/mcp/handlers.d.ts +1 -1
- package/dist/mcp/handlers.js +23 -6
- package/dist/mcp/server.js +3 -0
- package/dist/opencode/create-read-tool.d.ts +1 -1
- package/dist/opencode/create-read-tool.js +17 -5
- package/dist/opencode/tool-args.js +23 -1
- package/dist/opencode/tools.d.ts +1 -1
- package/dist/opencode/tools.js +3 -1
- package/dist/plugin.d.ts +1 -1
- package/dist/plugin.js +69 -152
- package/dist/quirks/auto-capture.js +5 -0
- package/dist/quirks/quirk-store.d.ts +1 -1
- package/dist/quirks/quirk-store.js +56 -17
- package/dist/retriever/context-optimizer.js +18 -4
- package/dist/retriever/keyword-index.d.ts +2 -0
- package/dist/retriever/keyword-index.js +38 -4
- package/dist/retriever/retriever.js +6 -1
- package/dist/tui.js +41 -4
- package/dist/vectorstore/lancedb.d.ts +25 -1
- package/dist/vectorstore/lancedb.js +157 -11
- package/dist/vectorstore/memory.js +5 -1
- package/dist/watcher.js +30 -4
- package/dist/web/api.d.ts +6 -2
- package/dist/web/api.js +198 -70
- package/dist/web/server.d.ts +2 -0
- package/dist/web/server.js +66 -28
- package/dist/web/static.d.ts +5 -2
- package/dist/web/static.js +9 -5
- package/dist/web/ui/assets/index-BDPYdtA1.js +3 -0
- package/dist/web/ui/index.html +1 -1
- package/package.json +1 -1
- package/dist/web/ui/assets/index-CJBvt6e0.js +0 -3
package/dist/indexer/pipeline.js
CHANGED
|
@@ -66,9 +66,18 @@ export async function runIndexPass(options) {
|
|
|
66
66
|
if (lock.pid && !isPidAlive(lock.pid)) {
|
|
67
67
|
logger.debug(`Stale lock from dead process ${lock.pid} — continuing`);
|
|
68
68
|
}
|
|
69
|
+
else if (lock.pid) {
|
|
70
|
+
// A LIVE process owns the lock — always skip, regardless of age.
|
|
71
|
+
// (Long re-indexes routinely exceed LOCK_MAX_AGE_MS; allowing a second
|
|
72
|
+
// process through the age check defeats the lock entirely and causes
|
|
73
|
+
// concurrent LanceDB writes.)
|
|
74
|
+
logger.warn(`Another index pass is running (PID ${lock.pid}). Skipping.`);
|
|
75
|
+
return { ...createIndexStats(0, "missing"), skipped: true };
|
|
76
|
+
}
|
|
69
77
|
else if (lock.startedAt && age < LOCK_MAX_AGE_MS) {
|
|
70
|
-
|
|
71
|
-
|
|
78
|
+
// No readable PID but a fresh lock — treat as active.
|
|
79
|
+
logger.warn(`Another index pass is running (no PID). Skipping.`);
|
|
80
|
+
return { ...createIndexStats(0, "missing"), skipped: true };
|
|
72
81
|
}
|
|
73
82
|
}
|
|
74
83
|
catch {
|
|
@@ -77,8 +86,10 @@ export async function runIndexPass(options) {
|
|
|
77
86
|
try {
|
|
78
87
|
await fs.writeFile(lockPath, JSON.stringify({ pid: process.pid, startedAt: Date.now() }), "utf-8");
|
|
79
88
|
}
|
|
80
|
-
catch {
|
|
81
|
-
// Best-effort lock
|
|
89
|
+
catch (err) {
|
|
90
|
+
// Best-effort lock — but surface it: without the lock file, the
|
|
91
|
+
// concurrent-pass protection above silently stops working.
|
|
92
|
+
logger.debug(`Could not write index lock file: ${err.message} — concurrent-pass protection disabled`);
|
|
82
93
|
}
|
|
83
94
|
try {
|
|
84
95
|
return await runIndexPassInner(options, logger);
|
|
@@ -159,7 +170,24 @@ async function runIndexPassInner(options, logger) {
|
|
|
159
170
|
changedSet.add(f);
|
|
160
171
|
for (const f of untracked)
|
|
161
172
|
changedSet.add(f);
|
|
162
|
-
|
|
173
|
+
// Git paths are relative to the REPO ROOT. When the workspace is a
|
|
174
|
+
// subdirectory of the repo (monorepos), convert them to workspace-
|
|
175
|
+
// relative paths before passing them to the scan — resolving repo-root
|
|
176
|
+
// paths against `cwd` would silently miss every change.
|
|
177
|
+
const toCwdRelative = (p) => {
|
|
178
|
+
const abs = path.resolve(repoRoot, p);
|
|
179
|
+
const rel = path.relative(options.cwd, abs);
|
|
180
|
+
if (rel.startsWith("..") || path.isAbsolute(rel))
|
|
181
|
+
return null;
|
|
182
|
+
return rel;
|
|
183
|
+
};
|
|
184
|
+
const cwdRelative = [];
|
|
185
|
+
for (const p of changedSet) {
|
|
186
|
+
const rel = toCwdRelative(p);
|
|
187
|
+
if (rel !== null)
|
|
188
|
+
cwdRelative.push(rel);
|
|
189
|
+
}
|
|
190
|
+
filterPaths = cwdRelative;
|
|
163
191
|
gitDeletedPaths = diffResult.deletedFiles;
|
|
164
192
|
logger.debug(`Git incremental: ${filterPaths.length} changed/untracked, ${gitDeletedPaths.length} deleted since ${manifest.lastGitCommit.slice(0, 8)}`);
|
|
165
193
|
}
|
|
@@ -212,7 +240,6 @@ async function runIndexPassInner(options, logger) {
|
|
|
212
240
|
let effectiveStore = options.store;
|
|
213
241
|
let tempStorePath;
|
|
214
242
|
if (options.force || (manifestStatus !== "ok" && existingCount > 0)) {
|
|
215
|
-
options.keywordIndex?.clear();
|
|
216
243
|
for (const key of Object.keys(manifest.files)) {
|
|
217
244
|
delete manifest.files[key];
|
|
218
245
|
}
|
|
@@ -233,6 +260,10 @@ async function runIndexPassInner(options, logger) {
|
|
|
233
260
|
catch { /* may not exist */ }
|
|
234
261
|
effectiveStore = createVectorStore(options.config, tempStorePath, options.dimension);
|
|
235
262
|
logger.debug(`Rebuilding index in temporary store at ${tempStorePath}`);
|
|
263
|
+
// Only clear the shared in-memory keyword index once the rebuild is
|
|
264
|
+
// guaranteed to proceed — the guard below may still bail out, and
|
|
265
|
+
// clearing it there would degrade live hybrid search for nothing.
|
|
266
|
+
options.keywordIndex?.clear();
|
|
236
267
|
}
|
|
237
268
|
else if (existingCount > 0) {
|
|
238
269
|
// NEVER destroy existing data when we can't do an atomic rebuild.
|
|
@@ -244,6 +275,7 @@ async function runIndexPassInner(options, logger) {
|
|
|
244
275
|
}
|
|
245
276
|
else {
|
|
246
277
|
// No existing data — safe to proceed with in-place indexing (no clear needed)
|
|
278
|
+
options.keywordIndex?.clear();
|
|
247
279
|
logger.debug("No existing data; indexing from scratch.");
|
|
248
280
|
}
|
|
249
281
|
}
|
|
@@ -305,28 +337,30 @@ async function runIndexPassInner(options, logger) {
|
|
|
305
337
|
let chunkedDone = 0;
|
|
306
338
|
const chunkProgressInterval = Math.max(1, Math.floor(activeCount / 20));
|
|
307
339
|
const chunkPhaseStart = Date.now();
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
const
|
|
311
|
-
(
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
if (isActive) {
|
|
317
|
-
chunkedDone++;
|
|
318
|
-
if (chunkedDone % chunkProgressInterval === 0 || chunkedDone === activeCount) {
|
|
319
|
-
const pct = ((chunkedDone / activeCount) * 100).toFixed(1);
|
|
320
|
-
logger.info(` Chunking: ${chunkedDone}/${activeCount} files (${pct}%)`);
|
|
340
|
+
/** Chunk one window of files (used by the windowed pipeline loop below). */
|
|
341
|
+
async function prepareWindow(windowFiles) {
|
|
342
|
+
const prepared = await Promise.all(windowFiles.map((file) => limit(async () => {
|
|
343
|
+
const fileLabel = path.relative(options.cwd, file.normalizedPath).replace(/\\/g, "/");
|
|
344
|
+
const isActive = !file.isEmpty && !file.isTooSmall &&
|
|
345
|
+
(!manifest.files[file.normalizedPath] || manifest.files[file.normalizedPath].hash !== file.hash);
|
|
346
|
+
if (isActive) {
|
|
347
|
+
options.progress?.startFile(fileLabel);
|
|
321
348
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
349
|
+
const prep = await prepareFile(file, options.cwd, manifest.files[file.normalizedPath], options.config, options.keywordIndex, options.descriptionProvider, logger, deferDescriptions, descHash);
|
|
350
|
+
if (isActive) {
|
|
351
|
+
chunkedDone++;
|
|
352
|
+
if (chunkedDone % chunkProgressInterval === 0 || chunkedDone === activeCount) {
|
|
353
|
+
const pct = ((chunkedDone / activeCount) * 100).toFixed(1);
|
|
354
|
+
logger.info(` Chunking: ${chunkedDone}/${activeCount} files (${pct}%)`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (prep.earlyResult && isActive) {
|
|
358
|
+
options.progress?.finishFile(fileLabel);
|
|
359
|
+
}
|
|
360
|
+
return prep;
|
|
361
|
+
})));
|
|
362
|
+
return prepared;
|
|
363
|
+
}
|
|
330
364
|
const aborted = () => options.abortSignal?.aborted ?? false;
|
|
331
365
|
// Shared progress logger: "stage <file> (chunk i/n) — X/total remaining (P%)".
|
|
332
366
|
const logChunkProgress = (stage, fileLabel, index, count, completed, total) => {
|
|
@@ -334,356 +368,401 @@ async function runIndexPassInner(options, logger) {
|
|
|
334
368
|
const pct = total > 0 ? ((remaining / total) * 100).toFixed(1) : "0.0";
|
|
335
369
|
logger.info(`${stage} ${fileLabel} (chunk ${index}/${count}) — ${remaining}/${total} remaining (${pct}%)`);
|
|
336
370
|
};
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
371
|
+
// File metadata look-up for manifest entries
|
|
372
|
+
const fileMeta = new Map(workspaceFiles.map((f) => [f.normalizedPath, { mtime: f.mtime, size: f.size }]));
|
|
373
|
+
// Serialised manifest-save queue — prevents concurrent write races and acts
|
|
374
|
+
// as a checkpoint for Ctrl+C resilience. Each worker appends to this chain
|
|
375
|
+
// after a successful store, so previously completed files are never lost.
|
|
376
|
+
// During a temp-store rebuild, saves go to the temp path so the real
|
|
377
|
+
// manifest stays consistent with the real store if the process is aborted.
|
|
378
|
+
const manifestTargetPath = () => tempStorePath ?? options.storePath;
|
|
379
|
+
let manifestSaveChain = Promise.resolve(undefined);
|
|
380
|
+
function enqueueManifestSave() {
|
|
381
|
+
manifestSaveChain = manifestSaveChain.then(() => saveManifest(manifestTargetPath(), manifest).catch((err) => {
|
|
382
|
+
options.logger?.warn?.(`Failed to save manifest: ${err.message}`);
|
|
383
|
+
}));
|
|
384
|
+
}
|
|
385
|
+
// ── Windowed pipeline ────────────────────────────────────────────────────
|
|
386
|
+
// The prepare→describe→embed→store chain used to materialize ALL chunks,
|
|
387
|
+
// embed texts, and vectors of the whole workspace simultaneously (hundreds
|
|
388
|
+
// of MB on large repos). Processing bounded windows of files keeps peak
|
|
389
|
+
// memory proportional to the window size instead of the workspace size.
|
|
390
|
+
const WINDOW_SIZE = Math.max(50, (options.config.indexing.concurrency ?? 4) * 10);
|
|
391
|
+
const allFinalResults = [];
|
|
392
|
+
let abortedInWindow = false;
|
|
393
|
+
for (let windowStart = 0; windowStart < workspaceFiles.length; windowStart += WINDOW_SIZE) {
|
|
394
|
+
if (aborted()) {
|
|
395
|
+
abortedInWindow = true;
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
const windowFiles = workspaceFiles.slice(windowStart, windowStart + WINDOW_SIZE);
|
|
399
|
+
const prepared = await prepareWindow(windowFiles);
|
|
400
|
+
if (windowStart === 0) {
|
|
401
|
+
const chunkPhaseSec = ((Date.now() - chunkPhaseStart) / 1000).toFixed(1);
|
|
402
|
+
logger.info(`Chunking complete: ${activeCount} files in ${chunkPhaseSec}s`);
|
|
403
|
+
}
|
|
404
|
+
if (deferDescriptions) {
|
|
405
|
+
const deferredPreps = prepared.filter((p) => p.chunks && p.chunks.length > 0 && p.relPath !== undefined);
|
|
406
|
+
if (deferredPreps.length > 0) {
|
|
407
|
+
const allChunks = [];
|
|
408
|
+
const oversizedChunks = [];
|
|
409
|
+
const maxContentChars = options.config.description?.maxContentChars;
|
|
410
|
+
logger.info(`Description phase: ${deferredPreps.length} files with chunks to describe`);
|
|
411
|
+
for (const prep of deferredPreps) {
|
|
412
|
+
for (const chunk of prep.chunks) {
|
|
413
|
+
if (chunk.metadata.contentType !== "image") {
|
|
414
|
+
if (maxContentChars && chunk.content.length > maxContentChars) {
|
|
415
|
+
oversizedChunks.push(chunk);
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
allChunks.push(chunk);
|
|
419
|
+
}
|
|
352
420
|
}
|
|
353
421
|
}
|
|
354
422
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
chunk.description = buildFallbackDescription(chunk);
|
|
358
|
-
}
|
|
359
|
-
// Per-file chunk index + label for clear progress reporting, plus a shared
|
|
360
|
-
// progress logger emitting "stage <file> (chunk i/n) — X/total remaining (P%)".
|
|
361
|
-
const chunkToFileLabel = new Map();
|
|
362
|
-
for (const prep of deferredPreps) {
|
|
363
|
-
for (const chunk of prep.chunks ?? []) {
|
|
364
|
-
chunkToFileLabel.set(chunk.id, prep.fileLabel);
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
const chunkMeta = new Map();
|
|
368
|
-
{
|
|
369
|
-
const perFileCount = new Map();
|
|
370
|
-
for (const chunk of allChunks) {
|
|
371
|
-
perFileCount.set(chunk.metadata.filePath, (perFileCount.get(chunk.metadata.filePath) ?? 0) + 1);
|
|
372
|
-
}
|
|
373
|
-
const perFileSeen = new Map();
|
|
374
|
-
for (const chunk of allChunks) {
|
|
375
|
-
const seen = perFileSeen.get(chunk.metadata.filePath) ?? 0;
|
|
376
|
-
perFileSeen.set(chunk.metadata.filePath, seen + 1);
|
|
377
|
-
chunkMeta.set(chunk.id, {
|
|
378
|
-
fileLabel: chunkToFileLabel.get(chunk.id) ?? chunk.metadata.filePath,
|
|
379
|
-
index: seen + 1,
|
|
380
|
-
count: perFileCount.get(chunk.metadata.filePath) ?? 1,
|
|
381
|
-
});
|
|
423
|
+
for (const chunk of oversizedChunks) {
|
|
424
|
+
chunk.description = buildFallbackDescription(chunk);
|
|
382
425
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
// Check description cache first — reuse cached descriptions for unchanged chunks
|
|
390
|
-
const cacheHits = [];
|
|
391
|
-
const cacheMisses = [];
|
|
392
|
-
for (const chunk of allChunks) {
|
|
393
|
-
const cacheKey = DescriptionCache.codeKey(chunk.content, descHash);
|
|
394
|
-
const cached = descCache.get(cacheKey);
|
|
395
|
-
if (cached) {
|
|
396
|
-
cacheHits.push({ chunk, desc: cached });
|
|
397
|
-
}
|
|
398
|
-
else {
|
|
399
|
-
cacheMisses.push(chunk);
|
|
426
|
+
// Per-file chunk index + label for clear progress reporting, plus a shared
|
|
427
|
+
// progress logger emitting "stage <file> (chunk i/n) — X/total remaining (P%)".
|
|
428
|
+
const chunkToFileLabel = new Map();
|
|
429
|
+
for (const prep of deferredPreps) {
|
|
430
|
+
for (const chunk of prep.chunks ?? []) {
|
|
431
|
+
chunkToFileLabel.set(chunk.id, prep.fileLabel);
|
|
400
432
|
}
|
|
401
433
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
434
|
+
const chunkMeta = new Map();
|
|
435
|
+
{
|
|
436
|
+
const perFileCount = new Map();
|
|
437
|
+
for (const chunk of allChunks) {
|
|
438
|
+
perFileCount.set(chunk.metadata.filePath, (perFileCount.get(chunk.metadata.filePath) ?? 0) + 1);
|
|
439
|
+
}
|
|
440
|
+
const perFileSeen = new Map();
|
|
441
|
+
for (const chunk of allChunks) {
|
|
442
|
+
const seen = perFileSeen.get(chunk.metadata.filePath) ?? 0;
|
|
443
|
+
perFileSeen.set(chunk.metadata.filePath, seen + 1);
|
|
444
|
+
chunkMeta.set(chunk.id, {
|
|
445
|
+
fileLabel: chunkToFileLabel.get(chunk.id) ?? chunk.metadata.filePath,
|
|
446
|
+
index: seen + 1,
|
|
447
|
+
count: perFileCount.get(chunk.metadata.filePath) ?? 1,
|
|
448
|
+
});
|
|
406
449
|
}
|
|
407
450
|
}
|
|
408
|
-
//
|
|
409
|
-
const chunkToPrep = new Map();
|
|
451
|
+
// Advance progress to Description stage before descriptions start
|
|
410
452
|
for (const prep of deferredPreps) {
|
|
411
|
-
|
|
412
|
-
chunkToPrep.set(chunk.id, prep);
|
|
413
|
-
}
|
|
453
|
+
options.progress?.finishStage(prep.fileLabel);
|
|
414
454
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
const
|
|
420
|
-
const
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
455
|
+
if (allChunks.length > 0 && descHash) {
|
|
456
|
+
// Check description cache first — reuse cached descriptions for unchanged chunks.
|
|
457
|
+
// The key includes the chunk's file context: identical content in two
|
|
458
|
+
// files must not reuse a contextually wrong description.
|
|
459
|
+
const chunkContext = (c) => `${c.metadata.filePath}:${c.metadata.startLine}-${c.metadata.endLine}`;
|
|
460
|
+
const cacheHits = [];
|
|
461
|
+
const cacheMisses = [];
|
|
462
|
+
for (const chunk of allChunks) {
|
|
463
|
+
const cacheKey = DescriptionCache.codeKey(chunk.content, descHash, chunkContext(chunk));
|
|
464
|
+
const cached = descCache.get(cacheKey);
|
|
465
|
+
if (cached) {
|
|
466
|
+
cacheHits.push({ chunk, desc: cached });
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
cacheMisses.push(chunk);
|
|
470
|
+
}
|
|
429
471
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
472
|
+
if (cacheHits.length > 0) {
|
|
473
|
+
logger.debug(` Using ${cacheHits.length} cached descriptions`);
|
|
474
|
+
for (const { chunk, desc } of cacheHits) {
|
|
475
|
+
chunk.description = desc;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
// Build chunkId → prep map for tracking description failures per-file
|
|
479
|
+
const chunkToPrep = new Map();
|
|
480
|
+
for (const prep of deferredPreps) {
|
|
481
|
+
for (const chunk of prep.chunks ?? []) {
|
|
482
|
+
chunkToPrep.set(chunk.id, prep);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
// Process cache misses in parallel waves with throttled cache saves.
|
|
486
|
+
// Parallel sub-batches reduce serial LLM turnaround time.
|
|
487
|
+
// Cache saves are throttled to avoid O(n^2) full-JSON serialization I/O.
|
|
488
|
+
if (cacheMisses.length > 0) {
|
|
489
|
+
const totalMisses = cacheMisses.length;
|
|
490
|
+
const descConcurrency = options.config.indexing.descriptionConcurrency ?? 4;
|
|
491
|
+
const SUB_BATCH = 10;
|
|
492
|
+
let describedDone = 0;
|
|
493
|
+
let describedSinceLastSave = 0;
|
|
494
|
+
const SAVE_INTERVAL = 200;
|
|
495
|
+
const descLimit = pLimit(descConcurrency);
|
|
496
|
+
const subBatchTasks = [];
|
|
497
|
+
for (let i = 0; i < totalMisses; i += SUB_BATCH) {
|
|
498
|
+
subBatchTasks.push({ i, chunks: cacheMisses.slice(i, i + SUB_BATCH) });
|
|
499
|
+
}
|
|
500
|
+
await Promise.all(subBatchTasks.map(({ i, chunks: subBatch }) => descLimit(async () => {
|
|
501
|
+
if (aborted())
|
|
502
|
+
return;
|
|
503
|
+
try {
|
|
504
|
+
const batchResult = await options.descriptionProvider.generateBatchDescriptions(subBatch, logger, {
|
|
505
|
+
total: totalMisses,
|
|
506
|
+
onProgress: (chunk) => {
|
|
507
|
+
describedDone++;
|
|
508
|
+
const meta = chunkMeta.get(chunk.id);
|
|
509
|
+
if (meta)
|
|
510
|
+
logChunkProgress("Describing", meta.fileLabel, meta.index, meta.count, describedDone, totalMisses);
|
|
511
|
+
},
|
|
512
|
+
});
|
|
513
|
+
const newCacheEntries = [];
|
|
514
|
+
for (const chunk of subBatch) {
|
|
515
|
+
const desc = batchResult.get(chunk.id);
|
|
516
|
+
if (desc && desc.trim().length > 0) {
|
|
517
|
+
chunk.description = desc;
|
|
518
|
+
newCacheEntries.push([DescriptionCache.codeKey(chunk.content, descHash, chunkContext(chunk)), desc]);
|
|
519
|
+
}
|
|
449
520
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
521
|
+
if (newCacheEntries.length > 0) {
|
|
522
|
+
descCache.setMany(newCacheEntries);
|
|
523
|
+
describedSinceLastSave += newCacheEntries.length;
|
|
524
|
+
if (describedSinceLastSave >= SAVE_INTERVAL) {
|
|
525
|
+
await descCache.save();
|
|
526
|
+
describedSinceLastSave = 0;
|
|
527
|
+
}
|
|
457
528
|
}
|
|
458
529
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
530
|
+
catch (err) {
|
|
531
|
+
logger.warn(` Description sub-batch failed (${i}-${i + subBatch.length}): ${err.message}`);
|
|
532
|
+
const failedPreps = new Set();
|
|
533
|
+
for (const chunk of subBatch) {
|
|
534
|
+
const prep = chunkToPrep.get(chunk.id);
|
|
535
|
+
if (prep)
|
|
536
|
+
failedPreps.add(prep);
|
|
537
|
+
}
|
|
538
|
+
for (const prep of failedPreps) {
|
|
539
|
+
prep.descriptionFailed = true;
|
|
540
|
+
}
|
|
470
541
|
}
|
|
542
|
+
})));
|
|
543
|
+
if (describedSinceLastSave > 0) {
|
|
544
|
+
await descCache.save();
|
|
471
545
|
}
|
|
472
|
-
|
|
473
|
-
if (describedSinceLastSave > 0) {
|
|
474
|
-
await descCache.save();
|
|
546
|
+
logger.debug(`Descriptions generated for ${totalMisses} chunks (concurrency: ${descConcurrency})`);
|
|
475
547
|
}
|
|
476
|
-
logger.debug(`Descriptions generated for ${totalMisses} chunks (concurrency: ${descConcurrency})`);
|
|
477
548
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
549
|
+
else if (allChunks.length > 0) {
|
|
550
|
+
// No descHash available (no description provider) — still generate descriptions
|
|
551
|
+
let describedDone = 0;
|
|
552
|
+
try {
|
|
553
|
+
const batchResult = await options.descriptionProvider.generateBatchDescriptions(allChunks, logger, {
|
|
554
|
+
total: allChunks.length,
|
|
555
|
+
onProgress: (chunk) => {
|
|
556
|
+
describedDone++;
|
|
557
|
+
const meta = chunkMeta.get(chunk.id);
|
|
558
|
+
if (meta)
|
|
559
|
+
logChunkProgress("Describing", meta.fileLabel, meta.index, meta.count, describedDone, allChunks.length);
|
|
560
|
+
},
|
|
561
|
+
});
|
|
562
|
+
for (const chunk of allChunks) {
|
|
563
|
+
const desc = batchResult.get(chunk.id);
|
|
564
|
+
if (desc && desc.trim().length > 0) {
|
|
565
|
+
chunk.description = desc;
|
|
566
|
+
}
|
|
496
567
|
}
|
|
497
568
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
569
|
+
catch (err) {
|
|
570
|
+
logger.warn(` Global description generation failed: ${err.message}`);
|
|
571
|
+
for (const prep of deferredPreps) {
|
|
572
|
+
if (prep.chunks.some((c) => c.metadata.contentType !== "image")) {
|
|
573
|
+
prep.descriptionFailed = true;
|
|
574
|
+
}
|
|
504
575
|
}
|
|
505
576
|
}
|
|
506
577
|
}
|
|
578
|
+
for (const prep of deferredPreps) {
|
|
579
|
+
prep.textToEmbed = buildTextsToEmbed(prep.chunks, prep.relPath, prep.metaHeader ?? "", prep.docPrefix ?? "", prep.isImageFile ?? false);
|
|
580
|
+
}
|
|
581
|
+
const totalDescribedChunks = deferredPreps.reduce((s, p) => s + (p.chunks?.length ?? 0), 0);
|
|
582
|
+
logger.info(`Description phase complete: ${totalDescribedChunks} chunks across ${deferredPreps.length} files`);
|
|
507
583
|
}
|
|
508
|
-
for (const prep of deferredPreps) {
|
|
509
|
-
prep.textToEmbed = buildTextsToEmbed(prep.chunks, prep.relPath, prep.metaHeader ?? "", prep.docPrefix ?? "", prep.isImageFile ?? false);
|
|
510
|
-
}
|
|
511
|
-
const totalDescribedChunks = deferredPreps.reduce((s, p) => s + (p.chunks?.length ?? 0), 0);
|
|
512
|
-
logger.info(`Description phase complete: ${totalDescribedChunks} chunks across ${deferredPreps.length} files`);
|
|
513
584
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
// ── Phase 1: Collect embed queue + handle early results ────────────────
|
|
535
|
-
const embedQueue = [];
|
|
536
|
-
let totalEmbedChunks = 0;
|
|
537
|
-
const earlyWorkerResults = new Map();
|
|
538
|
-
for (let fi = 0; fi < prepared.length; fi++) {
|
|
539
|
-
const prep = prepared[fi];
|
|
540
|
-
if (prep.earlyResult) {
|
|
541
|
-
if (prep.earlyResult.isRemoved) {
|
|
542
|
-
await effectiveStore.deleteByFilePath(prep.normalizedPath);
|
|
543
|
-
options.keywordIndex?.removeByFilePath(prep.normalizedPath);
|
|
544
|
-
delete manifest.files[prep.normalizedPath];
|
|
545
|
-
enqueueManifestSave();
|
|
585
|
+
// Cross-file embedding batch: collect this window's texts into a single queue,
|
|
586
|
+
// embed in one batched call (with concurrency), then distribute back.
|
|
587
|
+
const isOllama = options.embedder.name === "ollama";
|
|
588
|
+
const defaultBatchSize = options.config.indexing.embedBatchSize;
|
|
589
|
+
const defaultConcurrency = options.config.indexing.embedConcurrency ?? 1;
|
|
590
|
+
// ── Phase 1: Collect embed queue + handle early results ────────────────
|
|
591
|
+
const embedQueue = [];
|
|
592
|
+
let totalEmbedChunks = 0;
|
|
593
|
+
const earlyWorkerResults = new Map();
|
|
594
|
+
for (let fi = 0; fi < prepared.length; fi++) {
|
|
595
|
+
const prep = prepared[fi];
|
|
596
|
+
if (prep.earlyResult) {
|
|
597
|
+
if (prep.earlyResult.isRemoved) {
|
|
598
|
+
await effectiveStore.deleteByFilePath(prep.normalizedPath);
|
|
599
|
+
options.keywordIndex?.removeByFilePath(prep.normalizedPath);
|
|
600
|
+
delete manifest.files[prep.normalizedPath];
|
|
601
|
+
enqueueManifestSave();
|
|
602
|
+
}
|
|
603
|
+
earlyWorkerResults.set(fi, prep.earlyResult);
|
|
604
|
+
continue;
|
|
546
605
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
earlyWorkerResults.set(fi, {
|
|
553
|
-
normalizedPath: prep.normalizedPath, hash: prep.hash, chunkCount: 0,
|
|
554
|
-
fileLabel: prep.fileLabel,
|
|
555
|
-
isNew: false, isModified: false, isUnchanged: false, isEmpty: false,
|
|
556
|
-
isTooSmall: false, isRemoved: true, hadChunks: false,
|
|
557
|
-
descriptionFailed: prep.descriptionFailed,
|
|
558
|
-
});
|
|
559
|
-
continue;
|
|
560
|
-
}
|
|
561
|
-
options.progress?.finishStage(prep.fileLabel);
|
|
562
|
-
for (let ci = 0; ci < prep.textToEmbed.length; ci++) {
|
|
563
|
-
embedQueue.push({ fileIdx: fi, chunkIdx: ci, text: prep.textToEmbed[ci] });
|
|
564
|
-
}
|
|
565
|
-
totalEmbedChunks += prep.textToEmbed.length;
|
|
566
|
-
}
|
|
567
|
-
// ── Phase 2: Embed all texts in a single batched call ──────────────────
|
|
568
|
-
const batchSize = isOllama
|
|
569
|
-
? Math.min(options.config.indexing.ollamaMaxBatchSize ?? 500, defaultBatchSize)
|
|
570
|
-
: defaultBatchSize;
|
|
571
|
-
let embeddedDone = 0;
|
|
572
|
-
const allTexts = embedQueue.map(item => item.text);
|
|
573
|
-
let allEmbeddings = [];
|
|
574
|
-
const embedPhaseStart = Date.now();
|
|
575
|
-
if (allTexts.length > 0) {
|
|
576
|
-
logger.info(`Embedding phase: ${allTexts.length} texts in batches of ${batchSize}...`);
|
|
577
|
-
try {
|
|
578
|
-
allEmbeddings = await embedBatch(options.embedder, allTexts, batchSize, "document", defaultConcurrency, (completed, total) => {
|
|
579
|
-
embeddedDone = completed;
|
|
580
|
-
logChunkProgress("Embedding", "", completed, total, embeddedDone, totalEmbedChunks);
|
|
581
|
-
});
|
|
582
|
-
logger.info(`Embedding complete: ${allTexts.length} texts in ${((Date.now() - embedPhaseStart) / 1000).toFixed(1)}s`);
|
|
583
|
-
}
|
|
584
|
-
catch (err) {
|
|
585
|
-
logger.warn(` Global embedding failed: ${err.message}`);
|
|
586
|
-
for (const { fileIdx } of embedQueue) {
|
|
587
|
-
options.progress?.failFile(prepared[fileIdx].fileLabel);
|
|
588
|
-
earlyWorkerResults.set(fileIdx, {
|
|
589
|
-
normalizedPath: prepared[fileIdx].normalizedPath,
|
|
590
|
-
hash: prepared[fileIdx].hash,
|
|
591
|
-
chunkCount: 0, fileLabel: prepared[fileIdx].fileLabel,
|
|
606
|
+
if (!prep.chunks || !prep.textToEmbed || prep.textToEmbed.length === 0) {
|
|
607
|
+
options.progress?.finishFile(prep.fileLabel);
|
|
608
|
+
earlyWorkerResults.set(fi, {
|
|
609
|
+
normalizedPath: prep.normalizedPath, hash: prep.hash, chunkCount: 0,
|
|
610
|
+
fileLabel: prep.fileLabel,
|
|
592
611
|
isNew: false, isModified: false, isUnchanged: false, isEmpty: false,
|
|
593
|
-
isTooSmall: false, isRemoved:
|
|
594
|
-
descriptionFailed:
|
|
612
|
+
isTooSmall: false, isRemoved: true, hadChunks: false,
|
|
613
|
+
descriptionFailed: prep.descriptionFailed,
|
|
614
|
+
});
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
options.progress?.finishStage(prep.fileLabel);
|
|
618
|
+
for (let ci = 0; ci < prep.textToEmbed.length; ci++) {
|
|
619
|
+
embedQueue.push({ fileIdx: fi, chunkIdx: ci, text: prep.textToEmbed[ci] });
|
|
620
|
+
}
|
|
621
|
+
totalEmbedChunks += prep.textToEmbed.length;
|
|
622
|
+
}
|
|
623
|
+
// ── Phase 2: Embed all texts in a single batched call ──────────────────
|
|
624
|
+
const batchSize = isOllama
|
|
625
|
+
? Math.min(options.config.indexing.ollamaMaxBatchSize ?? 500, defaultBatchSize)
|
|
626
|
+
: defaultBatchSize;
|
|
627
|
+
let embeddedDone = 0;
|
|
628
|
+
const allTexts = embedQueue.map(item => item.text);
|
|
629
|
+
let allEmbeddings = [];
|
|
630
|
+
const embedPhaseStart = Date.now();
|
|
631
|
+
if (allTexts.length > 0) {
|
|
632
|
+
logger.info(`Embedding phase: ${allTexts.length} texts in batches of ${batchSize}...`);
|
|
633
|
+
try {
|
|
634
|
+
allEmbeddings = await embedBatch(options.embedder, allTexts, batchSize, "document", defaultConcurrency, (completed, total) => {
|
|
635
|
+
embeddedDone = completed;
|
|
636
|
+
logChunkProgress("Embedding", "", completed, total, embeddedDone, totalEmbedChunks);
|
|
595
637
|
});
|
|
638
|
+
logger.info(`Embedding complete: ${allTexts.length} texts in ${((Date.now() - embedPhaseStart) / 1000).toFixed(1)}s`);
|
|
639
|
+
}
|
|
640
|
+
catch (err) {
|
|
641
|
+
logger.warn(` Global embedding failed: ${err.message}`);
|
|
642
|
+
for (const { fileIdx } of embedQueue) {
|
|
643
|
+
options.progress?.failFile(prepared[fileIdx].fileLabel);
|
|
644
|
+
earlyWorkerResults.set(fileIdx, {
|
|
645
|
+
normalizedPath: prepared[fileIdx].normalizedPath,
|
|
646
|
+
hash: prepared[fileIdx].hash,
|
|
647
|
+
chunkCount: 0, fileLabel: prepared[fileIdx].fileLabel,
|
|
648
|
+
isNew: false, isModified: false, isUnchanged: false, isEmpty: false,
|
|
649
|
+
isTooSmall: false, isRemoved: false, hadChunks: false,
|
|
650
|
+
descriptionFailed: prepared[fileIdx].descriptionFailed,
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
embedQueue.length = 0; // prevent double-processing in store phase
|
|
596
654
|
}
|
|
597
|
-
embedQueue.length = 0; // prevent double-processing in store phase
|
|
598
655
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
656
|
+
// ── Distribute embeddings back to per-file chunks ─────────────────────
|
|
657
|
+
for (let i = 0; i < embedQueue.length; i++) {
|
|
658
|
+
const { fileIdx, chunkIdx } = embedQueue[i];
|
|
659
|
+
const emb = allEmbeddings[i];
|
|
660
|
+
const prep = prepared[fileIdx];
|
|
661
|
+
if (prep.chunks && prep.chunks[chunkIdx] && Array.isArray(emb) && emb.length > 0 && typeof emb[0] === "number") {
|
|
662
|
+
prep.chunks[chunkIdx].embedding = emb;
|
|
663
|
+
}
|
|
607
664
|
}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
665
|
+
// ── Phase 3: Store + manifest update per file (parallel) ──────────────
|
|
666
|
+
const filesToStore = prepared.filter((p) => !earlyWorkerResults.has(prepared.indexOf(p)) && p.chunks && (p.textToEmbed?.length ?? 0) > 0).length;
|
|
667
|
+
const storePhaseStart = Date.now();
|
|
668
|
+
logger.info(`Store phase: storing ${filesToStore} file(s) into vector database...`);
|
|
669
|
+
let storedFiles = 0;
|
|
670
|
+
const storeLimit = pLimit(options.config.indexing.concurrency);
|
|
671
|
+
const storeResults = await Promise.all(prepared.map((prep, fi) => storeLimit(async () => {
|
|
672
|
+
if (aborted()) {
|
|
673
|
+
return { normalizedPath: prep.normalizedPath, skipped: true };
|
|
674
|
+
}
|
|
675
|
+
// Return early results from phase 1
|
|
676
|
+
const earlyResult = earlyWorkerResults.get(fi);
|
|
677
|
+
if (earlyResult)
|
|
678
|
+
return earlyResult;
|
|
679
|
+
// No-embed path (shouldn't reach here but guard anyway)
|
|
680
|
+
if (!prep.chunks || prep.textToEmbed?.length === 0) {
|
|
681
|
+
options.progress?.finishFile(prep.fileLabel);
|
|
682
|
+
return {
|
|
683
|
+
normalizedPath: prep.normalizedPath, hash: prep.hash, chunkCount: 0,
|
|
684
|
+
fileLabel: prep.fileLabel,
|
|
685
|
+
isNew: false, isModified: false, isUnchanged: false, isEmpty: false,
|
|
686
|
+
isTooSmall: false, isRemoved: true, hadChunks: false,
|
|
687
|
+
descriptionFailed: prep.descriptionFailed,
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
// Store chunks with pre-attached embeddings
|
|
691
|
+
const validChunks = (prep.chunks ?? []).filter((c) => c.embedding && c.embedding.length > 0);
|
|
692
|
+
// Partial embed failure: some chunks have no vector. Do NOT record the
|
|
693
|
+
// file as complete — keep the previous manifest entry (or none, for
|
|
694
|
+
// new files) so the next pass retries the missing chunks.
|
|
695
|
+
const allEmbedded = validChunks.length === (prep.chunks?.length ?? 0);
|
|
696
|
+
if (!allEmbedded && (prep.chunks?.length ?? 0) > 0) {
|
|
697
|
+
options.logger?.warn?.(` ${prep.fileLabel}: ${validChunks.length}/${prep.chunks?.length} chunks embedded — marking for retry on next pass`);
|
|
698
|
+
}
|
|
699
|
+
if (validChunks.length > 0) {
|
|
700
|
+
await effectiveStore.addChunks(validChunks);
|
|
701
|
+
}
|
|
702
|
+
const result = {
|
|
703
|
+
normalizedPath: prep.normalizedPath,
|
|
704
|
+
hash: prep.hash,
|
|
705
|
+
chunkCount: validChunks.length,
|
|
628
706
|
fileLabel: prep.fileLabel,
|
|
629
|
-
isNew:
|
|
630
|
-
|
|
707
|
+
isNew: !prep.isModified,
|
|
708
|
+
isModified: prep.isModified,
|
|
709
|
+
isUnchanged: false,
|
|
710
|
+
isEmpty: false,
|
|
711
|
+
isTooSmall: false,
|
|
712
|
+
isRemoved: (prep.chunks?.length ?? 0) > 0 && validChunks.length === 0 ? false : validChunks.length === 0,
|
|
713
|
+
hadChunks: (prep.chunks?.length ?? 0) > 0,
|
|
631
714
|
descriptionFailed: prep.descriptionFailed,
|
|
715
|
+
descHash: prep.descHash,
|
|
632
716
|
};
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
if (result.chunkCount > 0 && !result.isRemoved) {
|
|
656
|
-
const meta = fileMeta.get(result.normalizedPath);
|
|
657
|
-
const entry = {
|
|
658
|
-
hash: result.hash,
|
|
659
|
-
chunkCount: result.chunkCount,
|
|
660
|
-
indexedAt: Date.now(),
|
|
661
|
-
mtime: meta?.mtime,
|
|
662
|
-
size: meta?.size,
|
|
663
|
-
descriptionFailed: result.descriptionFailed,
|
|
664
|
-
};
|
|
665
|
-
if (result.descHash) {
|
|
666
|
-
entry.descHash = result.descHash;
|
|
717
|
+
// Update manifest — only when ALL chunks were embedded. A partial
|
|
718
|
+
// write must not bump the hash/chunkCount, otherwise the missing
|
|
719
|
+
// chunks would never be retried (hash match on the next pass).
|
|
720
|
+
if (result.chunkCount > 0 && !result.isRemoved && allEmbedded) {
|
|
721
|
+
const meta = fileMeta.get(result.normalizedPath);
|
|
722
|
+
const entry = {
|
|
723
|
+
hash: result.hash,
|
|
724
|
+
chunkCount: result.chunkCount,
|
|
725
|
+
indexedAt: Date.now(),
|
|
726
|
+
mtime: meta?.mtime,
|
|
727
|
+
size: meta?.size,
|
|
728
|
+
descriptionFailed: result.descriptionFailed,
|
|
729
|
+
};
|
|
730
|
+
if (result.descHash) {
|
|
731
|
+
entry.descHash = result.descHash;
|
|
732
|
+
}
|
|
733
|
+
manifest.files[result.normalizedPath] = entry;
|
|
734
|
+
enqueueManifestSave();
|
|
735
|
+
}
|
|
736
|
+
else if (result.isRemoved) {
|
|
737
|
+
delete manifest.files[result.normalizedPath];
|
|
738
|
+
enqueueManifestSave();
|
|
667
739
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
740
|
+
options.progress?.finishFile(prep.fileLabel);
|
|
741
|
+
storedFiles++;
|
|
742
|
+
logChunkProgress("Storing", prep.fileLabel, storedFiles, filesToStore, storedFiles, filesToStore);
|
|
743
|
+
return result;
|
|
744
|
+
})));
|
|
745
|
+
const workerResults = storeResults;
|
|
746
|
+
for (const r of workerResults) {
|
|
747
|
+
if (r.skipped) {
|
|
748
|
+
abortedInWindow = true;
|
|
749
|
+
break;
|
|
750
|
+
}
|
|
751
|
+
allFinalResults.push(r);
|
|
752
|
+
}
|
|
753
|
+
aggregateStats(stats, allFinalResults.slice(-workerResults.length));
|
|
754
|
+
const storePhaseSec = ((Date.now() - storePhaseStart) / 1000).toFixed(1);
|
|
755
|
+
logger.info(`Store phase: ${storedFiles} files stored in ${storePhaseSec}s (running total ${stats.totalChunks} chunks)`);
|
|
756
|
+
// Free this window's chunk payloads so GC can reclaim them before the
|
|
757
|
+
// next window is prepared.
|
|
758
|
+
for (const prep of prepared) {
|
|
759
|
+
prep.chunks = undefined;
|
|
760
|
+
prep.textToEmbed = undefined;
|
|
761
|
+
}
|
|
762
|
+
if (abortedInWindow)
|
|
684
763
|
break;
|
|
685
|
-
|
|
686
|
-
|
|
764
|
+
} // end windowed pipeline loop
|
|
765
|
+
const finalResults = allFinalResults;
|
|
687
766
|
// Drain any in-flight manifest saves so all file entries are durable
|
|
688
767
|
await manifestSaveChain;
|
|
689
768
|
// Update mtime/size for unchanged files (speeds up the next scan)
|
|
@@ -694,9 +773,7 @@ async function runIndexPassInner(options, logger) {
|
|
|
694
773
|
entry.size = size;
|
|
695
774
|
}
|
|
696
775
|
}
|
|
697
|
-
|
|
698
|
-
logger.info(`Store phase complete: ${storedFiles} files stored in ${storePhaseSec}s (${stats.totalChunks} total chunks)`);
|
|
699
|
-
aggregateStats(stats, finalResults);
|
|
776
|
+
logger.info(`Index pass complete: ${stats.totalChunks} total chunks (${finalResults.length} files processed)`);
|
|
700
777
|
// Update timestamps; advance lastGitCommit ONLY on a complete pass
|
|
701
778
|
manifest.lastIndexedAt = Date.now();
|
|
702
779
|
if (!aborted()) {
|