pi-context-engine 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -2
- package/package.json +1 -1
- package/src/engine.ts +95 -6
- package/src/extension.ts +82 -5
- package/src/guidance.ts +1 -0
- package/src/indexer/pipeline.ts +25 -11
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ Ten tools the agent can call, plus four slash commands.
|
|
|
56
56
|
- **`index_status`**: chunk count and measured savings.
|
|
57
57
|
- **`reindex`**: refresh a path or the whole project.
|
|
58
58
|
|
|
59
|
-
Commands: `/context-index [--full]`, `/context-status`, `/context-savings`,
|
|
59
|
+
Commands: `/context-index [--full] [--reset]`, `/context-status`, `/context-savings`,
|
|
60
60
|
`/context-warmup`.
|
|
61
61
|
|
|
62
62
|
**Shipped guidance.** The package also injects a ~900-token guidance section
|
|
@@ -109,13 +109,24 @@ unchanged repo took 237 ms.
|
|
|
109
109
|
not add a full directory scan. Periodic yields keep the check responsive.
|
|
110
110
|
This prevents an accidental parent-directory
|
|
111
111
|
session from starting an unrequested multi-minute workload. `/context-index`
|
|
112
|
-
remains available as an explicit manual build.
|
|
112
|
+
remains available as an explicit manual build. While the pause is active,
|
|
113
|
+
every `context_search` result carries a stale-note and `/context-status`
|
|
114
|
+
leads with the pause line, so outdated results are never served silently;
|
|
115
|
+
all messages name the config file that raises the limit.
|
|
113
116
|
- **Resume after interruption**: the manifest checkpoints completed batches,
|
|
114
117
|
starting with the first batch and then at most once every five seconds. An
|
|
115
118
|
interrupted build can skip completed files instead of starting from zero.
|
|
116
119
|
Migration runs (schema reset, embedding-dimension change) are exempt: their
|
|
117
120
|
manifest stays untouched on disk until the run completes, so an interrupted
|
|
118
121
|
migration resumes cleanly instead of stranding stale rows.
|
|
122
|
+
- **One-command reset**: `/context-index --reset` deletes the index through the
|
|
123
|
+
engine while the session is running: sqlite handles are closed first, so it
|
|
124
|
+
succeeds where manual deletion of the storage directory fails with locked
|
|
125
|
+
file errors (EPERM on Windows). The report shows how many files and bytes
|
|
126
|
+
were freed, and memory, sessions, and the savings ledger survive. The
|
|
127
|
+
embedding cache is deleted too, so a rebuild re-embeds from scratch. The
|
|
128
|
+
next search rebuilds via the lazy kick, guarded by the same file budget as
|
|
129
|
+
every automatic run.
|
|
119
130
|
- **Cold-start visibility**: the first build on an empty index announces its
|
|
120
131
|
start (`building the first index…`) and always closes with a result line
|
|
121
132
|
(`updated: N file(s)` or `no files to index`). This is the one background
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-context-engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Semantic code context for pi: tree-sitter index + hybrid vector/BM25 retrieval + cross-session memory, all local. Port of code-context-engine (MIT, elara-labs) as a native pi extension.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
package/src/engine.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { SessionCapture } from "./memory/session-capture.js";
|
|
|
20
20
|
import { drainOne, enqueueSessionRollup, enqueueTurn, type CompressionDeps } from "./memory/compressor.js";
|
|
21
21
|
import { compress, expand, compressWithCounts, DEFAULT_LEVEL } from "./memory/grammar.js";
|
|
22
22
|
import { cosine } from "./memory/extractive.js";
|
|
23
|
-
import { AutomaticIndexFileLimitError, runIndexing } from "./indexer/pipeline.js";
|
|
23
|
+
import { AutomaticIndexFileLimitError, runIndexing, withPipelineLock } from "./indexer/pipeline.js";
|
|
24
24
|
import { getModelPricing } from "./pricing.js";
|
|
25
25
|
import type { Chunk } from "./types.js";
|
|
26
26
|
|
|
@@ -125,8 +125,10 @@ export interface SessionStats {
|
|
|
125
125
|
export interface EngineOptions {
|
|
126
126
|
projectDir: string;
|
|
127
127
|
config?: Config;
|
|
128
|
-
/** Test seam: reuse an existing backend/embedder/reranker instead of
|
|
129
|
-
|
|
128
|
+
/** Test seam: reuse an existing backend/embedder/reranker instead of
|
|
129
|
+
* auto-detecting. `backend` may be nulled by resetIndex — the pipeline
|
|
130
|
+
* then constructs a fresh one. */
|
|
131
|
+
runtime?: { backend?: LocalBackend | null; embedder: Embedder; reranker?: LocalReranker };
|
|
130
132
|
}
|
|
131
133
|
|
|
132
134
|
/** Issue #11 (upstream warmup): what /context-warmup resolved + cached. */
|
|
@@ -161,8 +163,8 @@ export class ContextEngine {
|
|
|
161
163
|
readonly projectName: string;
|
|
162
164
|
readonly storageBase: string;
|
|
163
165
|
config: Config;
|
|
164
|
-
private
|
|
165
|
-
| { backend
|
|
166
|
+
private injectedRuntime:
|
|
167
|
+
| { backend?: LocalBackend | null; embedder: Embedder; reranker?: LocalReranker }
|
|
166
168
|
| undefined;
|
|
167
169
|
private backendInstance: LocalBackend | null = null;
|
|
168
170
|
private embedderInstance: Embedder | null = null;
|
|
@@ -188,7 +190,7 @@ export class ContextEngine {
|
|
|
188
190
|
this.config = options.config ?? loadConfigForProject(this.projectDir);
|
|
189
191
|
this.injectedRuntime = options.runtime;
|
|
190
192
|
if (options.runtime) {
|
|
191
|
-
this.backendInstance = options.runtime.backend;
|
|
193
|
+
this.backendInstance = options.runtime.backend ?? null;
|
|
192
194
|
this.embedderInstance = options.runtime.embedder;
|
|
193
195
|
}
|
|
194
196
|
this.storageBase = projectStorageDir(this.config.storagePath, this.projectDir);
|
|
@@ -358,6 +360,93 @@ export class ContextEngine {
|
|
|
358
360
|
return this.backend.countChunks();
|
|
359
361
|
}
|
|
360
362
|
|
|
363
|
+
/**
|
|
364
|
+
* Delete this project's index (manifest, vector/FTS/graph stores, embedding
|
|
365
|
+
* cache) while keeping memory, sessions and the savings ledger — those are
|
|
366
|
+
* user history, not index state. Handles are closed BEFORE deletion:
|
|
367
|
+
* Windows refuses to delete files with open sqlite handles, which is why
|
|
368
|
+
* manually deleting the storage dir fails while a session is running.
|
|
369
|
+
* Queues behind in-flight and queued pipeline runs, and holds the same lock
|
|
370
|
+
* during deletion so a later reindex cannot race the reset.
|
|
371
|
+
* Errors are reported per target (e.g. another session holding a handle)
|
|
372
|
+
* instead of failing the whole reset.
|
|
373
|
+
*/
|
|
374
|
+
async resetIndex(): Promise<{ deletedFiles: number; freedBytes: number; errors: string[] }> {
|
|
375
|
+
return withPipelineLock(this.storageBase, () => this.resetIndexLocked());
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private async resetIndexLocked(): Promise<{ deletedFiles: number; freedBytes: number; errors: string[] }> {
|
|
379
|
+
try {
|
|
380
|
+
this.backendInstance?.close();
|
|
381
|
+
} catch {
|
|
382
|
+
// already closed
|
|
383
|
+
}
|
|
384
|
+
this.backendInstance = null;
|
|
385
|
+
this.retrieverInstance = null;
|
|
386
|
+
// Mutate the runtime object rather than replacing it: reindex calls that
|
|
387
|
+
// were queued behind this reset captured the same object and must see the
|
|
388
|
+
// closed backend dropped before they execute. Embedder and reranker
|
|
389
|
+
// survive: they hold no files.
|
|
390
|
+
if (this.injectedRuntime) this.injectedRuntime.backend = null;
|
|
391
|
+
// A reset index may be rebuilt by the next automatic kick.
|
|
392
|
+
this.autoIndexDone = false;
|
|
393
|
+
let deletedFiles = 0;
|
|
394
|
+
let freedBytes = 0;
|
|
395
|
+
const errors: string[] = [];
|
|
396
|
+
const countAndDelete = (target: string): void => {
|
|
397
|
+
let stat: fs.Stats;
|
|
398
|
+
try {
|
|
399
|
+
stat = fs.statSync(target);
|
|
400
|
+
} catch {
|
|
401
|
+
return; // not there — nothing to free
|
|
402
|
+
}
|
|
403
|
+
let files = 0;
|
|
404
|
+
let bytes = 0;
|
|
405
|
+
const measure = (p: string, s: fs.Stats): void => {
|
|
406
|
+
if (s.isDirectory()) {
|
|
407
|
+
for (const entry of fs.readdirSync(p)) {
|
|
408
|
+
const child = path.join(p, entry);
|
|
409
|
+
try {
|
|
410
|
+
measure(child, fs.statSync(child));
|
|
411
|
+
} catch {
|
|
412
|
+
// raced away between readdir and stat
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
files++;
|
|
418
|
+
bytes += s.size;
|
|
419
|
+
};
|
|
420
|
+
try {
|
|
421
|
+
measure(target, stat);
|
|
422
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
423
|
+
// Only report bytes actually released; a locked target must not
|
|
424
|
+
// inflate the success report when rmSync fails.
|
|
425
|
+
deletedFiles += files;
|
|
426
|
+
freedBytes += bytes;
|
|
427
|
+
} catch (err) {
|
|
428
|
+
// Locked file (another session?) — report instead of failing the reset.
|
|
429
|
+
errors.push(`${path.relative(this.storageBase, target) || path.basename(target)}: ${(err as Error).message.slice(0, 80)}`);
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
const graphDb = path.join(this.storageBase, "graph.db");
|
|
433
|
+
const embeddingCache = path.join(this.storageBase, "embedding_cache.db");
|
|
434
|
+
for (const target of [
|
|
435
|
+
path.join(this.storageBase, "manifest.json"),
|
|
436
|
+
path.join(this.storageBase, "vectors"),
|
|
437
|
+
path.join(this.storageBase, "fts"),
|
|
438
|
+
graphDb,
|
|
439
|
+
`${graphDb}-shm`,
|
|
440
|
+
`${graphDb}-wal`,
|
|
441
|
+
embeddingCache,
|
|
442
|
+
`${embeddingCache}-shm`,
|
|
443
|
+
`${embeddingCache}-wal`,
|
|
444
|
+
]) {
|
|
445
|
+
countAndDelete(target);
|
|
446
|
+
}
|
|
447
|
+
return { deletedFiles, freedBytes, errors };
|
|
448
|
+
}
|
|
449
|
+
|
|
361
450
|
async reindex(options: { full?: boolean; targetPath?: string | null; automatic?: boolean; onProgress?: (msg: string) => void } = {}): Promise<{
|
|
362
451
|
indexed: number;
|
|
363
452
|
chunks: number;
|
package/src/extension.ts
CHANGED
|
@@ -26,6 +26,26 @@ const CAPTURED_TOOLS = new Set(["bash", "read", "edit", "write", "grep", "find",
|
|
|
26
26
|
const AUTO_INDEX_DELAY_MS = 2000;
|
|
27
27
|
|
|
28
28
|
export default function contextEngineExtension(pi: ExtensionAPI) {
|
|
29
|
+
const MAX_AUTO_FILES_HINT =
|
|
30
|
+
"raise indexer.max_auto_files (~/.pi-context-engine/config.yaml or the project's .context-engine.yaml)";
|
|
31
|
+
|
|
32
|
+
/** One-line pause notice for /context-status and the index_status tool: the
|
|
33
|
+
* most likely question there is "why is my index not updating?" */
|
|
34
|
+
const pauseNotice = (): string | null =>
|
|
35
|
+
automaticIndexBlock
|
|
36
|
+
? `Automatic indexing paused: workspace exceeds ${automaticIndexBlock.limit.toLocaleString()} indexable files — the index may be stale. ` +
|
|
37
|
+
`Run /context-index, or ${MAX_AUTO_FILES_HINT}.`
|
|
38
|
+
: null;
|
|
39
|
+
|
|
40
|
+
/** Bracket note appended to every context_search result while the pause is
|
|
41
|
+
* active and a (possibly outdated) index is served. Without it the agent
|
|
42
|
+
* cannot tell fresh results from stale ones. */
|
|
43
|
+
const staleIndexNote = (): string | null =>
|
|
44
|
+
automaticIndexBlock
|
|
45
|
+
? `[note: automatic indexing is paused — workspace exceeds ${automaticIndexBlock.limit.toLocaleString()} indexable files; these results may be stale. ` +
|
|
46
|
+
`Run /context-index, or ${MAX_AUTO_FILES_HINT} to refresh]`
|
|
47
|
+
: null;
|
|
48
|
+
|
|
29
49
|
let engine: ContextEngine | null = null;
|
|
30
50
|
let currentCwd = process.cwd();
|
|
31
51
|
let promptNumber = 0;
|
|
@@ -114,7 +134,7 @@ export default function contextEngineExtension(pi: ExtensionAPI) {
|
|
|
114
134
|
notify?.(
|
|
115
135
|
`Context index: automatic indexing${eng.config.indexerWatch ? " and watcher" : ""} paused. ` +
|
|
116
136
|
`Workspace exceeds ${err.limit.toLocaleString()} indexable files. ` +
|
|
117
|
-
|
|
137
|
+
`Run /context-index to index anyway, or ${MAX_AUTO_FILES_HINT}.`,
|
|
118
138
|
"warning",
|
|
119
139
|
);
|
|
120
140
|
};
|
|
@@ -225,6 +245,20 @@ export default function contextEngineExtension(pi: ExtensionAPI) {
|
|
|
225
245
|
return engine;
|
|
226
246
|
};
|
|
227
247
|
|
|
248
|
+
/** Human-readable byte count for the reset report (explicit user action —
|
|
249
|
+
* the automatic guard never measures bytes by design). */
|
|
250
|
+
const formatBytes = (n: number): string => {
|
|
251
|
+
if (n < 1024) return `${n} B`;
|
|
252
|
+
const units = ["KiB", "MiB", "GiB"];
|
|
253
|
+
let v = n / 1024;
|
|
254
|
+
let u = 0;
|
|
255
|
+
while (v >= 1024 && u < units.length - 1) {
|
|
256
|
+
v /= 1024;
|
|
257
|
+
u++;
|
|
258
|
+
}
|
|
259
|
+
return `${v.toFixed(v >= 10 ? 0 : 1)} ${units[u]}`;
|
|
260
|
+
};
|
|
261
|
+
|
|
228
262
|
const text = (body: string, details?: unknown) => ({
|
|
229
263
|
content: [{ type: "text" as const, text: body }],
|
|
230
264
|
details: details ?? {},
|
|
@@ -267,7 +301,7 @@ export default function contextEngineExtension(pi: ExtensionAPI) {
|
|
|
267
301
|
return text(
|
|
268
302
|
`Automatic indexing is paused for ${eng.projectName}: the workspace exceeds ` +
|
|
269
303
|
`${automaticIndexBlock.limit.toLocaleString()} indexable files. ` +
|
|
270
|
-
|
|
304
|
+
`Run /context-index to index manually, or ${MAX_AUTO_FILES_HINT}.`,
|
|
271
305
|
);
|
|
272
306
|
}
|
|
273
307
|
if (indexInFlight) {
|
|
@@ -293,6 +327,8 @@ export default function contextEngineExtension(pi: ExtensionAPI) {
|
|
|
293
327
|
let body = result.body;
|
|
294
328
|
const nudge = eng.buildNudge();
|
|
295
329
|
if (nudge) body += nudge;
|
|
330
|
+
const stale = staleIndexNote();
|
|
331
|
+
if (stale) body += `\n${stale}`;
|
|
296
332
|
if (result.droppedLowValue > 0) {
|
|
297
333
|
body +=
|
|
298
334
|
"\n[note: lower-confidence results omitted — lower retrieval.marginal_ratio or retrieval.confidence_threshold to include them]";
|
|
@@ -431,7 +467,9 @@ export default function contextEngineExtension(pi: ExtensionAPI) {
|
|
|
431
467
|
description: "Check index health, chunk count, and token savings for this project.",
|
|
432
468
|
parameters: Type.Object({}),
|
|
433
469
|
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
434
|
-
|
|
470
|
+
const status = await getEngine(ctx.cwd).statusText();
|
|
471
|
+
const pause = pauseNotice();
|
|
472
|
+
return text(pause ? `${pause}\n${status}` : status);
|
|
435
473
|
},
|
|
436
474
|
});
|
|
437
475
|
|
|
@@ -471,9 +509,46 @@ export default function contextEngineExtension(pi: ExtensionAPI) {
|
|
|
471
509
|
/* ── Commands ────────────────────────────────────────────────────────── */
|
|
472
510
|
|
|
473
511
|
pi.registerCommand("context-index", {
|
|
474
|
-
description: "Build or refresh the semantic code index: /context-index [--full]",
|
|
512
|
+
description: "Build or refresh the semantic code index: /context-index [--full] [--reset]",
|
|
475
513
|
handler: async (args, ctx) => {
|
|
476
514
|
const eng = getEngine(ctx.cwd);
|
|
515
|
+
// --reset: delete the index through the engine (handles closed first, so
|
|
516
|
+
// it works while THIS session runs — manual deletion hits locked-file
|
|
517
|
+
// errors). Memory, sessions and savings survive. No auto-rebuild: the
|
|
518
|
+
// next search or /context-index decides, so an oversized workspace is
|
|
519
|
+
// not ambushed with a multi-minute build it just escaped.
|
|
520
|
+
if (args.includes("--reset")) {
|
|
521
|
+
if (indexInFlight) {
|
|
522
|
+
ctx.ui.notify(
|
|
523
|
+
"Context index: indexing is in progress — run /context-index --reset again once it finishes.",
|
|
524
|
+
"warning",
|
|
525
|
+
);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
// Reset also cancels the delayed startup kick. Otherwise its timer
|
|
529
|
+
// would rebuild the index immediately after the explicit delete.
|
|
530
|
+
if (autoIndexTimer) {
|
|
531
|
+
clearTimeout(autoIndexTimer);
|
|
532
|
+
autoIndexTimer = null;
|
|
533
|
+
}
|
|
534
|
+
const report = await eng.resetIndex();
|
|
535
|
+
// Free the lazy kick so the next search can rebuild (guarded by the
|
|
536
|
+
// same file budget as every automatic run).
|
|
537
|
+
lazyIndexStarted = false;
|
|
538
|
+
if (report.errors.length > 0) {
|
|
539
|
+
ctx.ui.notify(
|
|
540
|
+
`⚠ Index reset incomplete: ${report.errors.length} target(s) locked (${report.errors[0]}). Close other sessions on this project and run /context-index --reset again.`,
|
|
541
|
+
"warning",
|
|
542
|
+
);
|
|
543
|
+
} else {
|
|
544
|
+
ctx.ui.notify(
|
|
545
|
+
`✓ Index deleted: ${report.deletedFiles} file(s), ${formatBytes(report.freedBytes)} freed — memory, sessions and savings kept. ` +
|
|
546
|
+
"Run /context-index to build a fresh index.",
|
|
547
|
+
"info",
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
477
552
|
const full = args.includes("--full");
|
|
478
553
|
ctx.ui.notify(full ? "Re-indexing everything…" : "Indexing (incremental)…", "info");
|
|
479
554
|
const result = await eng.reindex({ full });
|
|
@@ -488,7 +563,9 @@ export default function contextEngineExtension(pi: ExtensionAPI) {
|
|
|
488
563
|
pi.registerCommand("context-status", {
|
|
489
564
|
description: "Show index health and token savings for this project",
|
|
490
565
|
handler: async (_args, ctx) => {
|
|
491
|
-
|
|
566
|
+
const status = await getEngine(ctx.cwd).statusText();
|
|
567
|
+
const pause = pauseNotice();
|
|
568
|
+
ctx.ui.notify(pause ? `${pause}\n${status}` : status, "info");
|
|
492
569
|
},
|
|
493
570
|
});
|
|
494
571
|
|
package/src/guidance.ts
CHANGED
|
@@ -100,6 +100,7 @@ If the first search misses:
|
|
|
100
100
|
- If a search says the index is empty, a background index is already running - retry shortly, or run \`/context-index\` for a synchronous build
|
|
101
101
|
- After large refactors, \`reindex\` (or \`/context-index\`) keeps results current; \`index_status\` reports freshness and measured token savings
|
|
102
102
|
- Incremental re-index only re-reads changed files, so it is cheap
|
|
103
|
+
- To wipe the index (stale or corrupted results), \`/context-index --reset\` deletes it safely mid-session - memory and sessions survive; the next search rebuilds
|
|
103
104
|
`;
|
|
104
105
|
|
|
105
106
|
/**
|
package/src/indexer/pipeline.ts
CHANGED
|
@@ -407,6 +407,22 @@ export async function indexCommits(
|
|
|
407
407
|
*/
|
|
408
408
|
const pipelineLocks = new Map<string, Promise<unknown>>();
|
|
409
409
|
|
|
410
|
+
/**
|
|
411
|
+
* Queue work behind every indexing run already registered for this storage
|
|
412
|
+
* dir, and keep later runs queued until the work finishes. Reset uses this
|
|
413
|
+
* instead of a one-shot idle wait: a reindex queued during that wait must not
|
|
414
|
+
* open the stores while deletion is in progress.
|
|
415
|
+
*/
|
|
416
|
+
export async function withPipelineLock<T>(storageBase: string, work: () => T | Promise<T>): Promise<T> {
|
|
417
|
+
const previous = pipelineLocks.get(storageBase) ?? Promise.resolve();
|
|
418
|
+
const current = previous.then(() => work());
|
|
419
|
+
pipelineLocks.set(
|
|
420
|
+
storageBase,
|
|
421
|
+
current.catch(() => undefined),
|
|
422
|
+
);
|
|
423
|
+
return current;
|
|
424
|
+
}
|
|
425
|
+
|
|
410
426
|
export async function runIndexing(
|
|
411
427
|
config: Config,
|
|
412
428
|
projectDirInput: string,
|
|
@@ -416,23 +432,18 @@ export async function runIndexing(
|
|
|
416
432
|
/** Automatic full-project runs only. Stop before migrations when the workspace exceeds this budget. */
|
|
417
433
|
maxAutoFiles?: number;
|
|
418
434
|
callbacks?: IndexCallbacks;
|
|
419
|
-
/** Reuse an already-constructed backend/embedder (the long-lived server path)
|
|
420
|
-
|
|
435
|
+
/** Reuse an already-constructed backend/embedder (the long-lived server path);
|
|
436
|
+
* a null backend falls through to a fresh LocalBackend (used by resetIndex). */
|
|
437
|
+
runtime?: { backend?: LocalBackend | null; embedder: Embedder };
|
|
421
438
|
} = {},
|
|
422
439
|
): Promise<IndexResult> {
|
|
423
440
|
const projectDir = fs.realpathSync(path.resolve(projectDirInput));
|
|
424
441
|
const storageBase = projectStorageDir(config.storagePath, projectDir);
|
|
425
442
|
fs.mkdirSync(storageBase, { recursive: true });
|
|
426
443
|
|
|
427
|
-
|
|
428
|
-
const current = previous.then(() =>
|
|
444
|
+
return withPipelineLock(storageBase, () =>
|
|
429
445
|
runIndexingLocked(config, projectDir, storageBase, options, options.runtime),
|
|
430
446
|
);
|
|
431
|
-
pipelineLocks.set(
|
|
432
|
-
storageBase,
|
|
433
|
-
current.catch(() => undefined),
|
|
434
|
-
);
|
|
435
|
-
return current;
|
|
436
447
|
}
|
|
437
448
|
|
|
438
449
|
async function runIndexingLocked(
|
|
@@ -440,13 +451,16 @@ async function runIndexingLocked(
|
|
|
440
451
|
projectDir: string,
|
|
441
452
|
storageBase: string,
|
|
442
453
|
options: { full?: boolean; targetPath?: string | null; maxAutoFiles?: number; callbacks?: IndexCallbacks },
|
|
443
|
-
runtime?: { backend
|
|
454
|
+
runtime?: { backend?: LocalBackend | null; embedder: Embedder },
|
|
444
455
|
): Promise<IndexResult> {
|
|
445
456
|
let { full = false, targetPath = null } = options;
|
|
446
457
|
const { callbacks = {} } = options;
|
|
447
458
|
const { log, progress, phase } = callbacks;
|
|
448
459
|
|
|
449
|
-
|
|
460
|
+
// The caller only owns the backend it actually passed in; a null backend
|
|
461
|
+
// (resetIndex dropped its closed handle) means the pipeline built its own
|
|
462
|
+
// and must close it — otherwise its sqlite handles leak until exit.
|
|
463
|
+
const ownsBackend = !runtime?.backend;
|
|
450
464
|
const chunker = new Chunker();
|
|
451
465
|
const manifest = new Manifest(path.join(storageBase, "manifest.json"));
|
|
452
466
|
const result: IndexResult = {
|