@wrongstack/tools 0.289.0 → 0.291.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/dist/index.js CHANGED
@@ -8099,6 +8099,53 @@ function codebaseIndexDirOverride(ctx) {
8099
8099
  const v = ctx.meta?.["codebaseIndexDir"];
8100
8100
  return typeof v === "string" ? v : void 0;
8101
8101
  }
8102
+ var StorePool = class {
8103
+ stores = /* @__PURE__ */ new Map();
8104
+ key(projectRoot, indexDir) {
8105
+ return `${projectRoot}\0${indexDir ?? ""}`;
8106
+ }
8107
+ /** Borrow a store. Creates it on first access for this key. */
8108
+ acquire(projectRoot, opts) {
8109
+ const k = this.key(projectRoot, opts?.indexDir);
8110
+ let store = this.stores.get(k);
8111
+ if (!store) {
8112
+ store = new IndexStore(projectRoot, { indexDir: opts?.indexDir });
8113
+ this.stores.set(k, store);
8114
+ }
8115
+ return store;
8116
+ }
8117
+ /** Return the store to the pool. The connection stays warm for subsequent
8118
+ * operations on the same (projectRoot, indexDir). */
8119
+ release(_store) {
8120
+ }
8121
+ /** Close every pooled connection and drain the pool. Call on shutdown. */
8122
+ closeAll() {
8123
+ for (const store of this.stores.values()) {
8124
+ try {
8125
+ store.close();
8126
+ } catch {
8127
+ }
8128
+ }
8129
+ this.stores.clear();
8130
+ }
8131
+ /** Remove one store from the pool. Used by tests that need isolation. */
8132
+ evict(projectRoot, indexDir) {
8133
+ const k = this.key(projectRoot, indexDir);
8134
+ const store = this.stores.get(k);
8135
+ if (store) {
8136
+ try {
8137
+ store.close();
8138
+ } catch {
8139
+ }
8140
+ this.stores.delete(k);
8141
+ }
8142
+ }
8143
+ /** True when the pool holds a connection for the given key. */
8144
+ has(projectRoot, indexDir) {
8145
+ return this.stores.has(this.key(projectRoot, indexDir));
8146
+ }
8147
+ };
8148
+ var indexStorePool = new StorePool();
8102
8149
  var warningSilenced = false;
8103
8150
  function silenceSqliteExperimentalWarning() {
8104
8151
  if (warningSilenced) return;
@@ -8316,7 +8363,7 @@ var IndexStore = class _IndexStore {
8316
8363
  return this.runWithRetry(() => {
8317
8364
  this.db.exec("BEGIN IMMEDIATE");
8318
8365
  try {
8319
- const maxRows = this.db.prepare("SELECT MAX(id) AS m FROM symbols").all();
8366
+ const maxRows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
8320
8367
  let nextId = (maxRows[0]?.m ?? 0) + 1;
8321
8368
  const stmt = this.db.prepare(
8322
8369
  `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
@@ -8588,7 +8635,7 @@ var IndexStore = class _IndexStore {
8588
8635
  return { results, total: candidates.length };
8589
8636
  }
8590
8637
  getAllIndexable() {
8591
- return this.db.prepare("SELECT id, text FROM symbols").all().map(({ id, text }) => ({ id, text }));
8638
+ return this.stmt("SELECT id, text FROM symbols").all().map(({ id, text }) => ({ id, text }));
8592
8639
  }
8593
8640
  /**
8594
8641
  * Largest symbol id currently in the table (0 when empty). New ids must be
@@ -8598,7 +8645,7 @@ var IndexStore = class _IndexStore {
8598
8645
  * `symbols.id`). Ids may have gaps — that is fine.
8599
8646
  */
8600
8647
  getMaxSymbolId() {
8601
- const rows = this.db.prepare("SELECT MAX(id) AS m FROM symbols").all();
8648
+ const rows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
8602
8649
  return rows[0]?.m ?? 0;
8603
8650
  }
8604
8651
  // ─── Stats ───────────────────────────────────────────────────────────────────
@@ -8606,14 +8653,14 @@ var IndexStore = class _IndexStore {
8606
8653
  const sizeBytes = this.sizeBytes();
8607
8654
  const lastRows = this.db.prepare("SELECT value FROM metadata WHERE key = 'last_indexed'").all();
8608
8655
  const lastIndexed = lastRows.length ? Number(lastRows[0]?.value) : null;
8609
- const totalRows = this.db.prepare("SELECT COUNT(*) FROM symbols").all();
8656
+ const totalRows = this.stmt("SELECT COUNT(*) FROM symbols").all();
8610
8657
  const totalSymbols = totalRows[0] ? Number(totalRows[0]["COUNT(*)"]) : 0;
8611
- const fileRows = this.db.prepare("SELECT COUNT(*) FROM files").all();
8658
+ const fileRows = this.stmt("SELECT COUNT(*) FROM files").all();
8612
8659
  const totalFiles = fileRows[0] ? Number(fileRows[0]["COUNT(*)"]) : 0;
8613
- const langRows = this.db.prepare("SELECT lang, COUNT(*) FROM symbols GROUP BY lang").all();
8660
+ const langRows = this.stmt("SELECT lang, COUNT(*) FROM symbols GROUP BY lang").all();
8614
8661
  const byLang = {};
8615
8662
  for (const row of langRows) byLang[row.lang] = Number(row["COUNT(*)"]);
8616
- const kindRows = this.db.prepare("SELECT kind, COUNT(*) FROM symbols GROUP BY kind").all();
8663
+ const kindRows = this.stmt("SELECT kind, COUNT(*) FROM symbols GROUP BY kind").all();
8617
8664
  const byKind = {};
8618
8665
  for (const row of kindRows) byKind[row.kind] = Number(row["COUNT(*)"]);
8619
8666
  return {
@@ -8645,11 +8692,14 @@ var IndexStore = class _IndexStore {
8645
8692
  this.runWithRetry(() => {
8646
8693
  this.db.exec("BEGIN IMMEDIATE");
8647
8694
  try {
8648
- this.db.exec("DELETE FROM refs");
8649
- this.db.exec("DELETE FROM symbols");
8650
- this.db.exec("DELETE FROM files");
8651
- if (this.ftsAvailable) this.db.exec("DELETE FROM symbols_fts");
8695
+ this.db.exec("DROP TABLE IF EXISTS refs");
8696
+ this.db.exec("DROP TABLE IF EXISTS symbols");
8697
+ this.db.exec("DROP TABLE IF EXISTS files");
8698
+ this.db.exec("DROP TABLE IF EXISTS metadata");
8699
+ if (this.ftsAvailable) this.db.exec("DROP TABLE IF EXISTS symbols_fts");
8652
8700
  this.db.exec("COMMIT");
8701
+ this.stmtCache.clear();
8702
+ this.initSchema();
8653
8703
  } catch (err) {
8654
8704
  this.db.exec("ROLLBACK");
8655
8705
  throw err;
@@ -8734,7 +8784,7 @@ var IndexStore = class _IndexStore {
8734
8784
  ).run(...options.deleteForFiles);
8735
8785
  this.db.prepare(`DELETE FROM symbols WHERE file IN (${placeholders})`).run(...options.deleteForFiles);
8736
8786
  }
8737
- const maxRows = this.db.prepare("SELECT MAX(id) AS m FROM symbols").all();
8787
+ const maxRows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
8738
8788
  let nextId = (maxRows[0]?.m ?? 0) + 1;
8739
8789
  const symStmt = this.db.prepare(
8740
8790
  `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
@@ -8918,7 +8968,7 @@ var IndexStore = class _IndexStore {
8918
8968
  * symbol resolved in package B). Node metadata includes symbol/file counts.
8919
8969
  */
8920
8970
  getPackageGraph() {
8921
- const symbols = this.db.prepare("SELECT file, id, name, kind, lang, line FROM symbols ORDER BY id").all();
8971
+ const symbols = this.db.prepare("SELECT file, id FROM symbols ORDER BY id").all();
8922
8972
  const pkgNodes = /* @__PURE__ */ new Map();
8923
8973
  const fileToPkg = /* @__PURE__ */ new Map();
8924
8974
  const symbolToPkg = /* @__PURE__ */ new Map();
@@ -9009,15 +9059,18 @@ var IndexStore = class _IndexStore {
9009
9059
  * derived from cross-file symbol references within the package.
9010
9060
  */
9011
9061
  getFileGraph(packageFilter) {
9012
- const allSymbols = this.db.prepare("SELECT file, id, name, kind, lang, line FROM symbols ORDER BY id").all();
9013
- const pkgSyms = allSymbols.filter(
9014
- (s) => (_IndexStore.derivePackage(s.file) ?? "(root)") === packageFilter
9015
- );
9016
- if (pkgSyms.length === 0) return { nodes: [], edges: [] };
9062
+ const allFiles = this.db.prepare("SELECT DISTINCT file FROM symbols").all();
9063
+ const pkgFilePaths = allFiles.filter((f) => (_IndexStore.derivePackage(f.file) ?? "(root)") === packageFilter).map((f) => f.file);
9064
+ const localFiles = new Set(pkgFilePaths);
9065
+ if (localFiles.size === 0) return { nodes: [], edges: [] };
9066
+ const filePlaceholders = [...localFiles].map(() => "?").join(",");
9067
+ const pkgSyms = this.db.prepare(
9068
+ `SELECT file, id, name, kind, lang, line FROM symbols WHERE file IN (${filePlaceholders}) ORDER BY id`
9069
+ ).all(...pkgFilePaths);
9017
9070
  const fileNodes = /* @__PURE__ */ new Map();
9018
9071
  const symToFile = /* @__PURE__ */ new Map();
9019
9072
  const fileStats = /* @__PURE__ */ new Map();
9020
- for (const s of allSymbols) {
9073
+ for (const s of pkgSyms) {
9021
9074
  symToFile.set(s.id, s.file);
9022
9075
  const current = fileStats.get(s.file);
9023
9076
  fileStats.set(s.file, {
@@ -9025,8 +9078,6 @@ var IndexStore = class _IndexStore {
9025
9078
  lang: current?.lang ?? s.lang
9026
9079
  });
9027
9080
  }
9028
- const localFiles = new Set(pkgSyms.map((s) => s.file));
9029
- const indexedFiles = new Set(allSymbols.map((s) => s.file));
9030
9081
  const ensureFileNode = (file) => {
9031
9082
  if (fileNodes.has(file)) return;
9032
9083
  const stats = fileStats.get(file);
@@ -9044,11 +9095,32 @@ var IndexStore = class _IndexStore {
9044
9095
  for (const file of localFiles) {
9045
9096
  ensureFileNode(file);
9046
9097
  }
9098
+ const indexedFiles = new Set(allFiles.map((f) => f.file));
9047
9099
  const refRows = this.db.prepare(
9048
9100
  `SELECT r.from_id, r.to_id, r.call_type
9049
9101
  FROM refs r
9050
- WHERE r.to_id IS NOT NULL`
9051
- ).all();
9102
+ WHERE (r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))
9103
+ OR r.to_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders})))
9104
+ AND r.to_id IS NOT NULL`
9105
+ ).all(...pkgFilePaths, ...pkgFilePaths);
9106
+ const knownSymIds = new Set(pkgSyms.map((s) => s.id));
9107
+ const crossRefIds = /* @__PURE__ */ new Set();
9108
+ for (const r of refRows) {
9109
+ if (!knownSymIds.has(r.from_id)) crossRefIds.add(r.from_id);
9110
+ if (!knownSymIds.has(r.to_id)) crossRefIds.add(r.to_id);
9111
+ }
9112
+ if (crossRefIds.size > 0) {
9113
+ const crossPlaceholders = [...crossRefIds].map(() => "?").join(",");
9114
+ const extras = this.db.prepare(
9115
+ `SELECT id, file FROM symbols WHERE id IN (${crossPlaceholders})`
9116
+ ).all(...crossRefIds);
9117
+ for (const x of extras) {
9118
+ symToFile.set(x.id, x.file);
9119
+ if (!fileStats.has(x.file)) {
9120
+ fileStats.set(x.file, { count: 0, lang: "ts" });
9121
+ }
9122
+ }
9123
+ }
9052
9124
  const edgeMap = /* @__PURE__ */ new Map();
9053
9125
  for (const r of refRows) {
9054
9126
  if (r.call_type === "import") continue;
@@ -9070,8 +9142,9 @@ var IndexStore = class _IndexStore {
9070
9142
  const importRows = this.db.prepare(
9071
9143
  `SELECT r.from_id, r.to_name
9072
9144
  FROM refs r
9073
- WHERE r.call_type = 'import'`
9074
- ).all();
9145
+ WHERE r.call_type = 'import'
9146
+ AND r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))`
9147
+ ).all(...pkgFilePaths);
9075
9148
  for (const r of importRows) {
9076
9149
  const fromFile = symToFile.get(r.from_id);
9077
9150
  if (!fromFile || !localFiles.has(fromFile)) continue;
@@ -9113,12 +9186,11 @@ var IndexStore = class _IndexStore {
9113
9186
  * derived from intra-file and cross-file symbol references (who calls whom).
9114
9187
  */
9115
9188
  getSymbolGraph(fileFilter) {
9116
- const allSymbols = this.db.prepare(
9117
- "SELECT id, name, kind, lang, file, line, signature, scope FROM symbols ORDER BY file, line, id"
9118
- ).all();
9119
- const syms = allSymbols.filter((symbol) => symbol.file === fileFilter);
9189
+ const syms = this.db.prepare(
9190
+ "SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE file = ? ORDER BY line, id"
9191
+ ).all(fileFilter);
9120
9192
  if (syms.length === 0) return { nodes: [], edges: [] };
9121
- const symById = new Map(allSymbols.map((symbol) => [symbol.id, symbol]));
9193
+ const symById = new Map(syms.map((symbol) => [symbol.id, symbol]));
9122
9194
  const relatedIds = new Set(syms.map((symbol) => symbol.id));
9123
9195
  const toGraphNode = (s) => ({
9124
9196
  id: `sym:${s.id}`,
@@ -9174,6 +9246,15 @@ var IndexStore = class _IndexStore {
9174
9246
  refType: bestType
9175
9247
  });
9176
9248
  }
9249
+ const loadedIds = new Set(syms.map((s) => s.id));
9250
+ const missingIds = [...relatedIds].filter((id) => !loadedIds.has(id));
9251
+ if (missingIds.length > 0) {
9252
+ const placeholders = missingIds.map(() => "?").join(",");
9253
+ const extras = this.db.prepare(
9254
+ `SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE id IN (${placeholders})`
9255
+ ).all(...missingIds);
9256
+ for (const s of extras) symById.set(s.id, s);
9257
+ }
9177
9258
  const nodes = [...relatedIds].map((id) => symById.get(id)).filter((symbol) => symbol !== void 0).sort((a, b) => {
9178
9259
  const aExternal = a.file === fileFilter ? 0 : 1;
9179
9260
  const bExternal = b.file === fileFilter ? 0 : 1;
@@ -9199,6 +9280,7 @@ import { Worker } from "node:worker_threads";
9199
9280
  import { expectDefined as expectDefined6 } from "@wrongstack/core";
9200
9281
  import * as fs14 from "node:fs/promises";
9201
9282
  import * as path19 from "node:path";
9283
+ import { availableParallelism } from "node:os";
9202
9284
  import { compileGlob as compileGlob2 } from "@wrongstack/core";
9203
9285
 
9204
9286
  // src/codebase-index/ts-parser.ts
@@ -9255,8 +9337,7 @@ function extToLang(ext) {
9255
9337
  return null;
9256
9338
  }
9257
9339
  }
9258
- function getSignature(node, sourceFile) {
9259
- const printer = ts.createPrinter({});
9340
+ function getSignature(printer, node, sourceFile) {
9260
9341
  const raw = printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
9261
9342
  return raw.replace(/\s+/g, " ").slice(0, 500);
9262
9343
  }
@@ -9275,28 +9356,14 @@ function getJsDoc(node, sourceFile) {
9275
9356
  }
9276
9357
  return "";
9277
9358
  }
9278
- function hasFunctionLikeAncestor(node) {
9279
- let current = node.parent;
9280
- while (current) {
9281
- if (ts.isFunctionLike(current)) return true;
9282
- current = current.parent;
9283
- }
9284
- return false;
9285
- }
9286
- function buildScope(node) {
9287
- const parts = [];
9288
- let current = node.parent;
9289
- while (current) {
9290
- if (ts.isClassDeclaration(current) || ts.isInterfaceDeclaration(current) || ts.isEnumDeclaration(current) || ts.isTypeAliasDeclaration(current)) {
9291
- parts.unshift(current.name?.text ?? "Anon");
9292
- } else if (ts.isMethodDeclaration(current) || ts.isGetAccessor(current) || ts.isSetAccessor(current) || ts.isPropertyDeclaration(current) || ts.isFunctionDeclaration(current)) {
9293
- if (current.name && ts.isIdentifier(current.name)) {
9294
- parts.unshift(current.name.text);
9295
- }
9359
+ function pushScopeName(node, parts) {
9360
+ if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isEnumDeclaration(node) || ts.isTypeAliasDeclaration(node)) {
9361
+ parts.push(node.name?.text ?? "Anon");
9362
+ } else if (ts.isMethodDeclaration(node) || ts.isGetAccessor(node) || ts.isSetAccessor(node) || ts.isPropertyDeclaration(node) || ts.isFunctionDeclaration(node)) {
9363
+ if (node.name && ts.isIdentifier(node.name)) {
9364
+ parts.push(node.name.text);
9296
9365
  }
9297
- current = current.parent;
9298
9366
  }
9299
- return parts.join(".");
9300
9367
  }
9301
9368
  function parseSymbols(opts) {
9302
9369
  const { file, content, lang } = opts;
@@ -9307,45 +9374,39 @@ function parseSymbols(opts) {
9307
9374
  return { file, lang, symbols: [], mtimeMs: Date.now() };
9308
9375
  }
9309
9376
  const symbols = [];
9310
- function visit(node) {
9377
+ const refs = [];
9378
+ const printer = ts.createPrinter({});
9379
+ function visit(node, funcDepth, scopeParts) {
9311
9380
  const kind = kindOf(node);
9312
9381
  if (kind) {
9313
- if ((kind === "const" || kind === "let" || kind === "var" || kind === "parameter") && hasFunctionLikeAncestor(node)) {
9314
- ts.forEachChild(node, visit);
9315
- return;
9382
+ if ((kind === "const" || kind === "let" || kind === "var" || kind === "parameter") && funcDepth > 0) {
9383
+ } else {
9384
+ const nameNode = node.name;
9385
+ if (!nameNode || !ts.isIdentifier(nameNode)) {
9386
+ return;
9387
+ }
9388
+ const name = nameNode.text;
9389
+ const pos2 = nameNode.getStart(sourceFile);
9390
+ const { line: line2, character } = sourceFile.getLineAndCharacterOfPosition(pos2);
9391
+ const scope = scopeParts.join(".");
9392
+ const signature = getSignature(printer, node, sourceFile);
9393
+ const docComment = getJsDoc(node, sourceFile);
9394
+ const text = [name, signature, docComment].filter(Boolean).join(" | ");
9395
+ symbols.push({
9396
+ id: 0,
9397
+ lang,
9398
+ kind,
9399
+ name,
9400
+ file,
9401
+ line: line2 + 1,
9402
+ col: character,
9403
+ signature,
9404
+ docComment,
9405
+ scope,
9406
+ text
9407
+ });
9316
9408
  }
9317
- const nameNode = node.name;
9318
- if (!nameNode || !ts.isIdentifier(nameNode)) return;
9319
- const name = nameNode.text;
9320
- const pos = nameNode.getStart(sourceFile);
9321
- const { line, character } = sourceFile.getLineAndCharacterOfPosition(pos);
9322
- const scope = buildScope(node);
9323
- const signature = getSignature(node, sourceFile);
9324
- const docComment = getJsDoc(node, sourceFile);
9325
- const text = [name, signature, docComment].filter(Boolean).join(" | ");
9326
- symbols.push({
9327
- id: 0,
9328
- lang,
9329
- kind,
9330
- name,
9331
- file,
9332
- line: line + 1,
9333
- col: character,
9334
- signature,
9335
- docComment,
9336
- scope,
9337
- text
9338
- });
9339
9409
  }
9340
- ts.forEachChild(node, visit);
9341
- }
9342
- visit(sourceFile);
9343
- const refs = extractRefs(sourceFile);
9344
- return { file, lang, symbols, refs, mtimeMs: Date.now() };
9345
- }
9346
- function extractRefs(sourceFile) {
9347
- const refs = [];
9348
- function visit(node) {
9349
9410
  const pos = node.getStart(sourceFile);
9350
9411
  const { line } = sourceFile.getLineAndCharacterOfPosition(pos);
9351
9412
  const lineNum = line + 1;
@@ -9370,10 +9431,14 @@ function extractRefs(sourceFile) {
9370
9431
  const moduleName = getModuleName(node);
9371
9432
  if (moduleName) refs.push({ fromId: 0, toName: moduleName, callType: "import", line: lineNum });
9372
9433
  }
9373
- ts.forEachChild(node, visit);
9434
+ const scopeIdx = scopeParts.length;
9435
+ pushScopeName(node, scopeParts);
9436
+ const childFuncDepth = ts.isFunctionLike(node) ? funcDepth + 1 : funcDepth;
9437
+ ts.forEachChild(node, (child) => visit(child, childFuncDepth, scopeParts));
9438
+ scopeParts.length = scopeIdx;
9374
9439
  }
9375
- visit(sourceFile);
9376
- return deduplicateRefs(refs);
9440
+ visit(sourceFile, 0, []);
9441
+ return { file, lang, symbols, refs: deduplicateRefs(refs), mtimeMs: Date.now() };
9377
9442
  }
9378
9443
  function getTypeName(name) {
9379
9444
  if (ts.isIdentifier(name)) return name.text;
@@ -10655,7 +10720,7 @@ async function loadGitignoreMatcher(projectRoot) {
10655
10720
 
10656
10721
  // src/codebase-index/indexer.ts
10657
10722
  var YIELD_EVERY_N = 50;
10658
- var PARALLEL_BATCH = 20;
10723
+ var PARALLEL_BATCH = Math.min(availableParallelism() * 4, 40);
10659
10724
  function yieldEventLoop() {
10660
10725
  return new Promise((resolve15) => setImmediate(resolve15));
10661
10726
  }
@@ -10833,11 +10898,14 @@ async function runIndexerWithStore(store, opts) {
10833
10898
  if (!force) {
10834
10899
  for (const meta of store.getAllFileMetas()) existingMeta.set(meta.file, meta);
10835
10900
  }
10901
+ let filesSinceLastYield = 0;
10836
10902
  for (let batchStart = 0; batchStart < files.length; batchStart += PARALLEL_BATCH) {
10837
10903
  const batchEnd = Math.min(batchStart + PARALLEL_BATCH, files.length);
10838
10904
  const batchFiles = files.slice(batchStart, batchEnd);
10839
10905
  opts.onProgress?.(batchEnd, files.length);
10840
- if (batchStart > 0 && batchStart % YIELD_EVERY_N === 0) {
10906
+ filesSinceLastYield += batchFiles.length;
10907
+ if (filesSinceLastYield >= YIELD_EVERY_N) {
10908
+ filesSinceLastYield = 0;
10841
10909
  await yieldEventLoop();
10842
10910
  throwIfAborted(signal);
10843
10911
  }
@@ -11048,7 +11116,7 @@ async function indexService(args, hooks = {}) {
11048
11116
  });
11049
11117
  }
11050
11118
  function searchService(args) {
11051
- const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
11119
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
11052
11120
  try {
11053
11121
  return store.searchRanked(
11054
11122
  args.query,
@@ -11061,39 +11129,39 @@ function searchService(args) {
11061
11129
  args.limit
11062
11130
  );
11063
11131
  } finally {
11064
- store.close();
11132
+ indexStorePool.release(store);
11065
11133
  }
11066
11134
  }
11067
11135
  function statsService(args) {
11068
- const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
11136
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
11069
11137
  try {
11070
11138
  return store.getStats();
11071
11139
  } finally {
11072
- store.close();
11140
+ indexStorePool.release(store);
11073
11141
  }
11074
11142
  }
11075
11143
  function packageGraphService(args) {
11076
- const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
11144
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
11077
11145
  try {
11078
11146
  return store.getPackageGraph();
11079
11147
  } finally {
11080
- store.close();
11148
+ indexStorePool.release(store);
11081
11149
  }
11082
11150
  }
11083
11151
  function fileGraphService(args) {
11084
- const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
11152
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
11085
11153
  try {
11086
11154
  return store.getFileGraph(args.packageFilter);
11087
11155
  } finally {
11088
- store.close();
11156
+ indexStorePool.release(store);
11089
11157
  }
11090
11158
  }
11091
11159
  function symbolGraphService(args) {
11092
- const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
11160
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
11093
11161
  try {
11094
11162
  return store.getSymbolGraph(args.fileFilter);
11095
11163
  } finally {
11096
- store.close();
11164
+ indexStorePool.release(store);
11097
11165
  }
11098
11166
  }
11099
11167
 
@@ -11210,10 +11278,19 @@ function terminateWorker(reason) {
11210
11278
  if (w) void w.terminate().catch(() => {
11211
11279
  });
11212
11280
  }
11213
- function shutdownCodebaseIndexHost() {
11281
+ async function shutdownCodebaseIndexHost() {
11214
11282
  cancelPendingReindexes();
11215
- terminateWorker(new Error("codebase-index host shut down"));
11283
+ indexStorePool.closeAll();
11284
+ const w = worker;
11285
+ worker = null;
11286
+ failAllPending(new Error("codebase-index host shut down"));
11216
11287
  workerUnavailable = false;
11288
+ if (w) {
11289
+ try {
11290
+ await w.terminate();
11291
+ } catch {
11292
+ }
11293
+ }
11217
11294
  }
11218
11295
  function callIndexOp(op, args, opts) {
11219
11296
  const w = ensureWorker();
@@ -16252,7 +16329,7 @@ import {
16252
16329
  duplicateBoard,
16253
16330
  exportBoardAsMarkdown,
16254
16331
  exportBoardToTaskGraph,
16255
- generateBoardFromDescription,
16332
+ createBoardFromText,
16256
16333
  getBoard as getBoard2,
16257
16334
  getKanbanOrchestrationSnapshot,
16258
16335
  getKanbanQueueHealth,
@@ -16317,12 +16394,17 @@ var SESSION_KANBAN_COLUMNS = [
16317
16394
  ];
16318
16395
  var boardQueue = /* @__PURE__ */ new Map();
16319
16396
  var boardEnsures = /* @__PURE__ */ new Map();
16397
+ var pendingMirrors = /* @__PURE__ */ new Map();
16398
+ var activeMirrors = /* @__PURE__ */ new Set();
16320
16399
  var bindings = /* @__PURE__ */ new WeakMap();
16321
16400
  var suppressedTodoMirrors = /* @__PURE__ */ new WeakSet();
16322
16401
  var activeSessionBoards = /* @__PURE__ */ new Map();
16323
16402
  function boardKey(projectRoot, sessionId) {
16324
16403
  return `${projectRoot}\0${sessionId}`;
16325
16404
  }
16405
+ function mirrorKey(projectRoot, sessionId, sourceSystem) {
16406
+ return `${boardKey(projectRoot, sessionId)}\0${sourceSystem}`;
16407
+ }
16326
16408
  function sessionTag(sessionId) {
16327
16409
  return `session:${sessionId}`;
16328
16410
  }
@@ -16458,6 +16540,43 @@ async function projectGraph(projectRoot, sessionId, graph, sourceSystem) {
16458
16540
  return result?.board ?? null;
16459
16541
  });
16460
16542
  }
16543
+ function queueLatestMirror(projectRoot, sessionId, graph, sourceSystem) {
16544
+ if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return;
16545
+ const key = mirrorKey(projectRoot, sessionId, sourceSystem);
16546
+ pendingMirrors.set(key, { projectRoot, sessionId, graph, sourceSystem });
16547
+ if (activeMirrors.has(key)) return;
16548
+ activeMirrors.add(key);
16549
+ void (async () => {
16550
+ try {
16551
+ for (; ; ) {
16552
+ const pending2 = pendingMirrors.get(key);
16553
+ if (!pending2) break;
16554
+ pendingMirrors.delete(key);
16555
+ try {
16556
+ await projectGraph(
16557
+ pending2.projectRoot,
16558
+ pending2.sessionId,
16559
+ pending2.graph,
16560
+ pending2.sourceSystem
16561
+ );
16562
+ } catch {
16563
+ }
16564
+ }
16565
+ } finally {
16566
+ activeMirrors.delete(key);
16567
+ const pending2 = pendingMirrors.get(key);
16568
+ if (pending2) {
16569
+ pendingMirrors.delete(key);
16570
+ queueLatestMirror(
16571
+ pending2.projectRoot,
16572
+ pending2.sessionId,
16573
+ pending2.graph,
16574
+ pending2.sourceSystem
16575
+ );
16576
+ }
16577
+ }
16578
+ })();
16579
+ }
16461
16580
  function todoListToSerializedGraph(todos, sessionId) {
16462
16581
  const nodes = todos.map((todo, index) => ({
16463
16582
  id: todo.id,
@@ -16608,13 +16727,28 @@ Reassess your current plan before continuing; do not rely on the initial todo sn
16608
16727
  }
16609
16728
  }
16610
16729
  function mirrorSessionTodosToKanban(projectRoot, todos, sessionId) {
16611
- fireAndForget(projectSessionTodosToKanban(projectRoot, todos, sessionId));
16730
+ queueLatestMirror(
16731
+ projectRoot,
16732
+ sessionId,
16733
+ todoListToSerializedGraph(todos, sessionId),
16734
+ "session-todo"
16735
+ );
16612
16736
  }
16613
16737
  function mirrorSessionTasksToKanban(projectRoot, tasks, sessionId) {
16614
- fireAndForget(projectSessionTasksToKanban(projectRoot, tasks, sessionId));
16738
+ queueLatestMirror(
16739
+ projectRoot,
16740
+ sessionId,
16741
+ taskFileToSerializedGraph(tasks, sessionId),
16742
+ "session-task"
16743
+ );
16615
16744
  }
16616
16745
  function mirrorSessionPlanToKanban(projectRoot, items, sessionId) {
16617
- fireAndForget(projectSessionPlanToKanban(projectRoot, items, sessionId));
16746
+ queueLatestMirror(
16747
+ projectRoot,
16748
+ sessionId,
16749
+ planFileToSerializedGraph(items, sessionId),
16750
+ "session-plan"
16751
+ );
16618
16752
  }
16619
16753
  function attachSessionKanbanMirror(context) {
16620
16754
  const existing = bindings.get(context);
@@ -17158,7 +17292,7 @@ var kanbanTool = {
17158
17292
  }
17159
17293
  case "generate_board": {
17160
17294
  if (!input.description) return fail("generate_board requires description.");
17161
- const boardInput = generateBoardFromDescription({
17295
+ const boardInput = createBoardFromText({
17162
17296
  description: input.description,
17163
17297
  ...input.title !== void 0 ? { title: input.title } : {},
17164
17298
  ...input.context !== void 0 ? { context: input.context } : {},
@@ -17543,20 +17677,31 @@ var kanbanTool = {
17543
17677
  if (!input.boardId || !input.taskId)
17544
17678
  return fail("mark_assignment requires boardId and taskId.");
17545
17679
  const assignmentStatus = input.assignmentStatus ?? (input.status === "completed" ? "completed" : input.error ? "failed" : void 0);
17546
- const board = await updateTaskAssignment(projectRoot, input.boardId, input.taskId, {
17547
- ...assignmentStatus !== void 0 ? { status: assignmentStatus } : {},
17548
- ...input.subagentId !== void 0 ? { subagentId: input.subagentId } : {},
17549
- ...input.runTaskId !== void 0 ? { runTaskId: input.runTaskId } : {},
17550
- ...input.lastResult !== void 0 ? { lastResult: input.lastResult } : {},
17551
- ...input.error !== void 0 ? { error: input.error } : {},
17552
- ...input.agentId !== void 0 ? { agentId: input.agentId } : {},
17553
- ...input.leaseId !== void 0 ? { leaseId: input.leaseId } : {},
17554
- ...input.claimedAt !== void 0 ? { claimedAt: input.claimedAt } : {},
17555
- ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
17556
- ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
17557
- ...input.attempt !== void 0 ? { attempt: input.attempt } : {},
17558
- ...input.maxAttempts !== void 0 ? { maxAttempts: input.maxAttempts } : {}
17559
- });
17680
+ const board = await updateTaskAssignment(
17681
+ projectRoot,
17682
+ input.boardId,
17683
+ input.taskId,
17684
+ {
17685
+ ...assignmentStatus !== void 0 ? { status: assignmentStatus } : {},
17686
+ ...input.subagentId !== void 0 ? { subagentId: input.subagentId } : {},
17687
+ ...input.runTaskId !== void 0 ? { runTaskId: input.runTaskId } : {},
17688
+ ...input.lastResult !== void 0 ? { lastResult: input.lastResult } : {},
17689
+ ...input.error !== void 0 ? { error: input.error } : {},
17690
+ ...input.agentId !== void 0 ? { agentId: input.agentId } : {},
17691
+ ...input.leaseId !== void 0 ? { leaseId: input.leaseId } : {},
17692
+ ...input.claimedAt !== void 0 ? { claimedAt: input.claimedAt } : {},
17693
+ ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
17694
+ ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
17695
+ ...input.attempt !== void 0 ? { attempt: input.attempt } : {},
17696
+ ...input.maxAttempts !== void 0 ? { maxAttempts: input.maxAttempts } : {}
17697
+ },
17698
+ // Ownership fence: when expectedLeaseId is supplied, the write is
17699
+ // applied only if the current assignment still holds this lease.
17700
+ // This prevents a recovered+reassigned stale worker's terminal
17701
+ // mark_assignment from overwriting the successor's state. The check
17702
+ // is atomic inside updateTaskAssignment's mutateBoard lock.
17703
+ input.expectedLeaseId !== void 0 ? { expectedLeaseId: input.expectedLeaseId } : {}
17704
+ );
17560
17705
  return board ? okBoard(board, "Assignment updated.") : fail("Task not found.");
17561
17706
  }
17562
17707
  case "heartbeat_assignment": {
@@ -17565,7 +17710,13 @@ var kanbanTool = {
17565
17710
  }
17566
17711
  const board = await heartbeatTaskAssignment(projectRoot, input.boardId, input.taskId, {
17567
17712
  ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
17568
- ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {}
17713
+ ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
17714
+ // Ownership fence: when expectedLeaseId is supplied, the renewal
17715
+ // is applied only if the current assignment still holds this lease.
17716
+ // This prevents a recovered+reassigned stale worker's heartbeat
17717
+ // from renewing the successor's lease. The check is atomic inside
17718
+ // heartbeatTaskAssignment's mutateBoard lock.
17719
+ ...input.expectedLeaseId !== void 0 ? { expectedLeaseId: input.expectedLeaseId } : {}
17569
17720
  });
17570
17721
  return board ? okBoard(board, "Assignment heartbeat updated.") : fail("Task assignment not found.");
17571
17722
  }