@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/pack.js CHANGED
@@ -7774,6 +7774,53 @@ function codebaseIndexDirOverride(ctx) {
7774
7774
  const v = ctx.meta?.["codebaseIndexDir"];
7775
7775
  return typeof v === "string" ? v : void 0;
7776
7776
  }
7777
+ var StorePool = class {
7778
+ stores = /* @__PURE__ */ new Map();
7779
+ key(projectRoot, indexDir) {
7780
+ return `${projectRoot}\0${indexDir ?? ""}`;
7781
+ }
7782
+ /** Borrow a store. Creates it on first access for this key. */
7783
+ acquire(projectRoot, opts) {
7784
+ const k = this.key(projectRoot, opts?.indexDir);
7785
+ let store = this.stores.get(k);
7786
+ if (!store) {
7787
+ store = new IndexStore(projectRoot, { indexDir: opts?.indexDir });
7788
+ this.stores.set(k, store);
7789
+ }
7790
+ return store;
7791
+ }
7792
+ /** Return the store to the pool. The connection stays warm for subsequent
7793
+ * operations on the same (projectRoot, indexDir). */
7794
+ release(_store) {
7795
+ }
7796
+ /** Close every pooled connection and drain the pool. Call on shutdown. */
7797
+ closeAll() {
7798
+ for (const store of this.stores.values()) {
7799
+ try {
7800
+ store.close();
7801
+ } catch {
7802
+ }
7803
+ }
7804
+ this.stores.clear();
7805
+ }
7806
+ /** Remove one store from the pool. Used by tests that need isolation. */
7807
+ evict(projectRoot, indexDir) {
7808
+ const k = this.key(projectRoot, indexDir);
7809
+ const store = this.stores.get(k);
7810
+ if (store) {
7811
+ try {
7812
+ store.close();
7813
+ } catch {
7814
+ }
7815
+ this.stores.delete(k);
7816
+ }
7817
+ }
7818
+ /** True when the pool holds a connection for the given key. */
7819
+ has(projectRoot, indexDir) {
7820
+ return this.stores.has(this.key(projectRoot, indexDir));
7821
+ }
7822
+ };
7823
+ var indexStorePool = new StorePool();
7777
7824
  var warningSilenced = false;
7778
7825
  function silenceSqliteExperimentalWarning() {
7779
7826
  if (warningSilenced) return;
@@ -7991,7 +8038,7 @@ var IndexStore = class _IndexStore {
7991
8038
  return this.runWithRetry(() => {
7992
8039
  this.db.exec("BEGIN IMMEDIATE");
7993
8040
  try {
7994
- const maxRows = this.db.prepare("SELECT MAX(id) AS m FROM symbols").all();
8041
+ const maxRows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
7995
8042
  let nextId = (maxRows[0]?.m ?? 0) + 1;
7996
8043
  const stmt = this.db.prepare(
7997
8044
  `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
@@ -8263,7 +8310,7 @@ var IndexStore = class _IndexStore {
8263
8310
  return { results, total: candidates.length };
8264
8311
  }
8265
8312
  getAllIndexable() {
8266
- return this.db.prepare("SELECT id, text FROM symbols").all().map(({ id, text }) => ({ id, text }));
8313
+ return this.stmt("SELECT id, text FROM symbols").all().map(({ id, text }) => ({ id, text }));
8267
8314
  }
8268
8315
  /**
8269
8316
  * Largest symbol id currently in the table (0 when empty). New ids must be
@@ -8273,7 +8320,7 @@ var IndexStore = class _IndexStore {
8273
8320
  * `symbols.id`). Ids may have gaps — that is fine.
8274
8321
  */
8275
8322
  getMaxSymbolId() {
8276
- const rows = this.db.prepare("SELECT MAX(id) AS m FROM symbols").all();
8323
+ const rows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
8277
8324
  return rows[0]?.m ?? 0;
8278
8325
  }
8279
8326
  // ─── Stats ───────────────────────────────────────────────────────────────────
@@ -8281,14 +8328,14 @@ var IndexStore = class _IndexStore {
8281
8328
  const sizeBytes = this.sizeBytes();
8282
8329
  const lastRows = this.db.prepare("SELECT value FROM metadata WHERE key = 'last_indexed'").all();
8283
8330
  const lastIndexed = lastRows.length ? Number(lastRows[0]?.value) : null;
8284
- const totalRows = this.db.prepare("SELECT COUNT(*) FROM symbols").all();
8331
+ const totalRows = this.stmt("SELECT COUNT(*) FROM symbols").all();
8285
8332
  const totalSymbols = totalRows[0] ? Number(totalRows[0]["COUNT(*)"]) : 0;
8286
- const fileRows = this.db.prepare("SELECT COUNT(*) FROM files").all();
8333
+ const fileRows = this.stmt("SELECT COUNT(*) FROM files").all();
8287
8334
  const totalFiles = fileRows[0] ? Number(fileRows[0]["COUNT(*)"]) : 0;
8288
- const langRows = this.db.prepare("SELECT lang, COUNT(*) FROM symbols GROUP BY lang").all();
8335
+ const langRows = this.stmt("SELECT lang, COUNT(*) FROM symbols GROUP BY lang").all();
8289
8336
  const byLang = {};
8290
8337
  for (const row of langRows) byLang[row.lang] = Number(row["COUNT(*)"]);
8291
- const kindRows = this.db.prepare("SELECT kind, COUNT(*) FROM symbols GROUP BY kind").all();
8338
+ const kindRows = this.stmt("SELECT kind, COUNT(*) FROM symbols GROUP BY kind").all();
8292
8339
  const byKind = {};
8293
8340
  for (const row of kindRows) byKind[row.kind] = Number(row["COUNT(*)"]);
8294
8341
  return {
@@ -8320,11 +8367,14 @@ var IndexStore = class _IndexStore {
8320
8367
  this.runWithRetry(() => {
8321
8368
  this.db.exec("BEGIN IMMEDIATE");
8322
8369
  try {
8323
- this.db.exec("DELETE FROM refs");
8324
- this.db.exec("DELETE FROM symbols");
8325
- this.db.exec("DELETE FROM files");
8326
- if (this.ftsAvailable) this.db.exec("DELETE FROM symbols_fts");
8370
+ this.db.exec("DROP TABLE IF EXISTS refs");
8371
+ this.db.exec("DROP TABLE IF EXISTS symbols");
8372
+ this.db.exec("DROP TABLE IF EXISTS files");
8373
+ this.db.exec("DROP TABLE IF EXISTS metadata");
8374
+ if (this.ftsAvailable) this.db.exec("DROP TABLE IF EXISTS symbols_fts");
8327
8375
  this.db.exec("COMMIT");
8376
+ this.stmtCache.clear();
8377
+ this.initSchema();
8328
8378
  } catch (err) {
8329
8379
  this.db.exec("ROLLBACK");
8330
8380
  throw err;
@@ -8409,7 +8459,7 @@ var IndexStore = class _IndexStore {
8409
8459
  ).run(...options.deleteForFiles);
8410
8460
  this.db.prepare(`DELETE FROM symbols WHERE file IN (${placeholders})`).run(...options.deleteForFiles);
8411
8461
  }
8412
- const maxRows = this.db.prepare("SELECT MAX(id) AS m FROM symbols").all();
8462
+ const maxRows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
8413
8463
  let nextId = (maxRows[0]?.m ?? 0) + 1;
8414
8464
  const symStmt = this.db.prepare(
8415
8465
  `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
@@ -8593,7 +8643,7 @@ var IndexStore = class _IndexStore {
8593
8643
  * symbol resolved in package B). Node metadata includes symbol/file counts.
8594
8644
  */
8595
8645
  getPackageGraph() {
8596
- const symbols = this.db.prepare("SELECT file, id, name, kind, lang, line FROM symbols ORDER BY id").all();
8646
+ const symbols = this.db.prepare("SELECT file, id FROM symbols ORDER BY id").all();
8597
8647
  const pkgNodes = /* @__PURE__ */ new Map();
8598
8648
  const fileToPkg = /* @__PURE__ */ new Map();
8599
8649
  const symbolToPkg = /* @__PURE__ */ new Map();
@@ -8684,15 +8734,18 @@ var IndexStore = class _IndexStore {
8684
8734
  * derived from cross-file symbol references within the package.
8685
8735
  */
8686
8736
  getFileGraph(packageFilter) {
8687
- const allSymbols = this.db.prepare("SELECT file, id, name, kind, lang, line FROM symbols ORDER BY id").all();
8688
- const pkgSyms = allSymbols.filter(
8689
- (s) => (_IndexStore.derivePackage(s.file) ?? "(root)") === packageFilter
8690
- );
8691
- if (pkgSyms.length === 0) return { nodes: [], edges: [] };
8737
+ const allFiles = this.db.prepare("SELECT DISTINCT file FROM symbols").all();
8738
+ const pkgFilePaths = allFiles.filter((f) => (_IndexStore.derivePackage(f.file) ?? "(root)") === packageFilter).map((f) => f.file);
8739
+ const localFiles = new Set(pkgFilePaths);
8740
+ if (localFiles.size === 0) return { nodes: [], edges: [] };
8741
+ const filePlaceholders = [...localFiles].map(() => "?").join(",");
8742
+ const pkgSyms = this.db.prepare(
8743
+ `SELECT file, id, name, kind, lang, line FROM symbols WHERE file IN (${filePlaceholders}) ORDER BY id`
8744
+ ).all(...pkgFilePaths);
8692
8745
  const fileNodes = /* @__PURE__ */ new Map();
8693
8746
  const symToFile = /* @__PURE__ */ new Map();
8694
8747
  const fileStats = /* @__PURE__ */ new Map();
8695
- for (const s of allSymbols) {
8748
+ for (const s of pkgSyms) {
8696
8749
  symToFile.set(s.id, s.file);
8697
8750
  const current = fileStats.get(s.file);
8698
8751
  fileStats.set(s.file, {
@@ -8700,8 +8753,6 @@ var IndexStore = class _IndexStore {
8700
8753
  lang: current?.lang ?? s.lang
8701
8754
  });
8702
8755
  }
8703
- const localFiles = new Set(pkgSyms.map((s) => s.file));
8704
- const indexedFiles = new Set(allSymbols.map((s) => s.file));
8705
8756
  const ensureFileNode = (file) => {
8706
8757
  if (fileNodes.has(file)) return;
8707
8758
  const stats = fileStats.get(file);
@@ -8719,11 +8770,32 @@ var IndexStore = class _IndexStore {
8719
8770
  for (const file of localFiles) {
8720
8771
  ensureFileNode(file);
8721
8772
  }
8773
+ const indexedFiles = new Set(allFiles.map((f) => f.file));
8722
8774
  const refRows = this.db.prepare(
8723
8775
  `SELECT r.from_id, r.to_id, r.call_type
8724
8776
  FROM refs r
8725
- WHERE r.to_id IS NOT NULL`
8726
- ).all();
8777
+ WHERE (r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))
8778
+ OR r.to_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders})))
8779
+ AND r.to_id IS NOT NULL`
8780
+ ).all(...pkgFilePaths, ...pkgFilePaths);
8781
+ const knownSymIds = new Set(pkgSyms.map((s) => s.id));
8782
+ const crossRefIds = /* @__PURE__ */ new Set();
8783
+ for (const r of refRows) {
8784
+ if (!knownSymIds.has(r.from_id)) crossRefIds.add(r.from_id);
8785
+ if (!knownSymIds.has(r.to_id)) crossRefIds.add(r.to_id);
8786
+ }
8787
+ if (crossRefIds.size > 0) {
8788
+ const crossPlaceholders = [...crossRefIds].map(() => "?").join(",");
8789
+ const extras = this.db.prepare(
8790
+ `SELECT id, file FROM symbols WHERE id IN (${crossPlaceholders})`
8791
+ ).all(...crossRefIds);
8792
+ for (const x of extras) {
8793
+ symToFile.set(x.id, x.file);
8794
+ if (!fileStats.has(x.file)) {
8795
+ fileStats.set(x.file, { count: 0, lang: "ts" });
8796
+ }
8797
+ }
8798
+ }
8727
8799
  const edgeMap = /* @__PURE__ */ new Map();
8728
8800
  for (const r of refRows) {
8729
8801
  if (r.call_type === "import") continue;
@@ -8745,8 +8817,9 @@ var IndexStore = class _IndexStore {
8745
8817
  const importRows = this.db.prepare(
8746
8818
  `SELECT r.from_id, r.to_name
8747
8819
  FROM refs r
8748
- WHERE r.call_type = 'import'`
8749
- ).all();
8820
+ WHERE r.call_type = 'import'
8821
+ AND r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))`
8822
+ ).all(...pkgFilePaths);
8750
8823
  for (const r of importRows) {
8751
8824
  const fromFile = symToFile.get(r.from_id);
8752
8825
  if (!fromFile || !localFiles.has(fromFile)) continue;
@@ -8788,12 +8861,11 @@ var IndexStore = class _IndexStore {
8788
8861
  * derived from intra-file and cross-file symbol references (who calls whom).
8789
8862
  */
8790
8863
  getSymbolGraph(fileFilter) {
8791
- const allSymbols = this.db.prepare(
8792
- "SELECT id, name, kind, lang, file, line, signature, scope FROM symbols ORDER BY file, line, id"
8793
- ).all();
8794
- const syms = allSymbols.filter((symbol) => symbol.file === fileFilter);
8864
+ const syms = this.db.prepare(
8865
+ "SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE file = ? ORDER BY line, id"
8866
+ ).all(fileFilter);
8795
8867
  if (syms.length === 0) return { nodes: [], edges: [] };
8796
- const symById = new Map(allSymbols.map((symbol) => [symbol.id, symbol]));
8868
+ const symById = new Map(syms.map((symbol) => [symbol.id, symbol]));
8797
8869
  const relatedIds = new Set(syms.map((symbol) => symbol.id));
8798
8870
  const toGraphNode = (s) => ({
8799
8871
  id: `sym:${s.id}`,
@@ -8849,6 +8921,15 @@ var IndexStore = class _IndexStore {
8849
8921
  refType: bestType
8850
8922
  });
8851
8923
  }
8924
+ const loadedIds = new Set(syms.map((s) => s.id));
8925
+ const missingIds = [...relatedIds].filter((id) => !loadedIds.has(id));
8926
+ if (missingIds.length > 0) {
8927
+ const placeholders = missingIds.map(() => "?").join(",");
8928
+ const extras = this.db.prepare(
8929
+ `SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE id IN (${placeholders})`
8930
+ ).all(...missingIds);
8931
+ for (const s of extras) symById.set(s.id, s);
8932
+ }
8852
8933
  const nodes = [...relatedIds].map((id) => symById.get(id)).filter((symbol) => symbol !== void 0).sort((a, b) => {
8853
8934
  const aExternal = a.file === fileFilter ? 0 : 1;
8854
8935
  const bExternal = b.file === fileFilter ? 0 : 1;
@@ -8874,6 +8955,7 @@ import { Worker } from "node:worker_threads";
8874
8955
  import { expectDefined as expectDefined6 } from "@wrongstack/core";
8875
8956
  import * as fs14 from "node:fs/promises";
8876
8957
  import * as path19 from "node:path";
8958
+ import { availableParallelism } from "node:os";
8877
8959
  import { compileGlob as compileGlob2 } from "@wrongstack/core";
8878
8960
 
8879
8961
  // src/codebase-index/ts-parser.ts
@@ -8930,8 +9012,7 @@ function extToLang(ext) {
8930
9012
  return null;
8931
9013
  }
8932
9014
  }
8933
- function getSignature(node, sourceFile) {
8934
- const printer = ts.createPrinter({});
9015
+ function getSignature(printer, node, sourceFile) {
8935
9016
  const raw = printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
8936
9017
  return raw.replace(/\s+/g, " ").slice(0, 500);
8937
9018
  }
@@ -8950,28 +9031,14 @@ function getJsDoc(node, sourceFile) {
8950
9031
  }
8951
9032
  return "";
8952
9033
  }
8953
- function hasFunctionLikeAncestor(node) {
8954
- let current = node.parent;
8955
- while (current) {
8956
- if (ts.isFunctionLike(current)) return true;
8957
- current = current.parent;
8958
- }
8959
- return false;
8960
- }
8961
- function buildScope(node) {
8962
- const parts = [];
8963
- let current = node.parent;
8964
- while (current) {
8965
- if (ts.isClassDeclaration(current) || ts.isInterfaceDeclaration(current) || ts.isEnumDeclaration(current) || ts.isTypeAliasDeclaration(current)) {
8966
- parts.unshift(current.name?.text ?? "Anon");
8967
- } else if (ts.isMethodDeclaration(current) || ts.isGetAccessor(current) || ts.isSetAccessor(current) || ts.isPropertyDeclaration(current) || ts.isFunctionDeclaration(current)) {
8968
- if (current.name && ts.isIdentifier(current.name)) {
8969
- parts.unshift(current.name.text);
8970
- }
9034
+ function pushScopeName(node, parts) {
9035
+ if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isEnumDeclaration(node) || ts.isTypeAliasDeclaration(node)) {
9036
+ parts.push(node.name?.text ?? "Anon");
9037
+ } else if (ts.isMethodDeclaration(node) || ts.isGetAccessor(node) || ts.isSetAccessor(node) || ts.isPropertyDeclaration(node) || ts.isFunctionDeclaration(node)) {
9038
+ if (node.name && ts.isIdentifier(node.name)) {
9039
+ parts.push(node.name.text);
8971
9040
  }
8972
- current = current.parent;
8973
9041
  }
8974
- return parts.join(".");
8975
9042
  }
8976
9043
  function parseSymbols(opts) {
8977
9044
  const { file, content, lang } = opts;
@@ -8982,45 +9049,39 @@ function parseSymbols(opts) {
8982
9049
  return { file, lang, symbols: [], mtimeMs: Date.now() };
8983
9050
  }
8984
9051
  const symbols = [];
8985
- function visit(node) {
9052
+ const refs = [];
9053
+ const printer = ts.createPrinter({});
9054
+ function visit(node, funcDepth, scopeParts) {
8986
9055
  const kind = kindOf(node);
8987
9056
  if (kind) {
8988
- if ((kind === "const" || kind === "let" || kind === "var" || kind === "parameter") && hasFunctionLikeAncestor(node)) {
8989
- ts.forEachChild(node, visit);
8990
- return;
9057
+ if ((kind === "const" || kind === "let" || kind === "var" || kind === "parameter") && funcDepth > 0) {
9058
+ } else {
9059
+ const nameNode = node.name;
9060
+ if (!nameNode || !ts.isIdentifier(nameNode)) {
9061
+ return;
9062
+ }
9063
+ const name = nameNode.text;
9064
+ const pos2 = nameNode.getStart(sourceFile);
9065
+ const { line: line2, character } = sourceFile.getLineAndCharacterOfPosition(pos2);
9066
+ const scope = scopeParts.join(".");
9067
+ const signature = getSignature(printer, node, sourceFile);
9068
+ const docComment = getJsDoc(node, sourceFile);
9069
+ const text = [name, signature, docComment].filter(Boolean).join(" | ");
9070
+ symbols.push({
9071
+ id: 0,
9072
+ lang,
9073
+ kind,
9074
+ name,
9075
+ file,
9076
+ line: line2 + 1,
9077
+ col: character,
9078
+ signature,
9079
+ docComment,
9080
+ scope,
9081
+ text
9082
+ });
8991
9083
  }
8992
- const nameNode = node.name;
8993
- if (!nameNode || !ts.isIdentifier(nameNode)) return;
8994
- const name = nameNode.text;
8995
- const pos = nameNode.getStart(sourceFile);
8996
- const { line, character } = sourceFile.getLineAndCharacterOfPosition(pos);
8997
- const scope = buildScope(node);
8998
- const signature = getSignature(node, sourceFile);
8999
- const docComment = getJsDoc(node, sourceFile);
9000
- const text = [name, signature, docComment].filter(Boolean).join(" | ");
9001
- symbols.push({
9002
- id: 0,
9003
- lang,
9004
- kind,
9005
- name,
9006
- file,
9007
- line: line + 1,
9008
- col: character,
9009
- signature,
9010
- docComment,
9011
- scope,
9012
- text
9013
- });
9014
9084
  }
9015
- ts.forEachChild(node, visit);
9016
- }
9017
- visit(sourceFile);
9018
- const refs = extractRefs(sourceFile);
9019
- return { file, lang, symbols, refs, mtimeMs: Date.now() };
9020
- }
9021
- function extractRefs(sourceFile) {
9022
- const refs = [];
9023
- function visit(node) {
9024
9085
  const pos = node.getStart(sourceFile);
9025
9086
  const { line } = sourceFile.getLineAndCharacterOfPosition(pos);
9026
9087
  const lineNum = line + 1;
@@ -9045,10 +9106,14 @@ function extractRefs(sourceFile) {
9045
9106
  const moduleName = getModuleName(node);
9046
9107
  if (moduleName) refs.push({ fromId: 0, toName: moduleName, callType: "import", line: lineNum });
9047
9108
  }
9048
- ts.forEachChild(node, visit);
9109
+ const scopeIdx = scopeParts.length;
9110
+ pushScopeName(node, scopeParts);
9111
+ const childFuncDepth = ts.isFunctionLike(node) ? funcDepth + 1 : funcDepth;
9112
+ ts.forEachChild(node, (child) => visit(child, childFuncDepth, scopeParts));
9113
+ scopeParts.length = scopeIdx;
9049
9114
  }
9050
- visit(sourceFile);
9051
- return deduplicateRefs(refs);
9115
+ visit(sourceFile, 0, []);
9116
+ return { file, lang, symbols, refs: deduplicateRefs(refs), mtimeMs: Date.now() };
9052
9117
  }
9053
9118
  function getTypeName(name) {
9054
9119
  if (ts.isIdentifier(name)) return name.text;
@@ -10330,7 +10395,7 @@ async function loadGitignoreMatcher(projectRoot) {
10330
10395
 
10331
10396
  // src/codebase-index/indexer.ts
10332
10397
  var YIELD_EVERY_N = 50;
10333
- var PARALLEL_BATCH = 20;
10398
+ var PARALLEL_BATCH = Math.min(availableParallelism() * 4, 40);
10334
10399
  function yieldEventLoop() {
10335
10400
  return new Promise((resolve14) => setImmediate(resolve14));
10336
10401
  }
@@ -10508,11 +10573,14 @@ async function runIndexerWithStore(store, opts) {
10508
10573
  if (!force) {
10509
10574
  for (const meta of store.getAllFileMetas()) existingMeta.set(meta.file, meta);
10510
10575
  }
10576
+ let filesSinceLastYield = 0;
10511
10577
  for (let batchStart = 0; batchStart < files.length; batchStart += PARALLEL_BATCH) {
10512
10578
  const batchEnd = Math.min(batchStart + PARALLEL_BATCH, files.length);
10513
10579
  const batchFiles = files.slice(batchStart, batchEnd);
10514
10580
  opts.onProgress?.(batchEnd, files.length);
10515
- if (batchStart > 0 && batchStart % YIELD_EVERY_N === 0) {
10581
+ filesSinceLastYield += batchFiles.length;
10582
+ if (filesSinceLastYield >= YIELD_EVERY_N) {
10583
+ filesSinceLastYield = 0;
10516
10584
  await yieldEventLoop();
10517
10585
  throwIfAborted(signal);
10518
10586
  }
@@ -10723,7 +10791,7 @@ async function indexService(args, hooks = {}) {
10723
10791
  });
10724
10792
  }
10725
10793
  function searchService(args) {
10726
- const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
10794
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
10727
10795
  try {
10728
10796
  return store.searchRanked(
10729
10797
  args.query,
@@ -10736,15 +10804,15 @@ function searchService(args) {
10736
10804
  args.limit
10737
10805
  );
10738
10806
  } finally {
10739
- store.close();
10807
+ indexStorePool.release(store);
10740
10808
  }
10741
10809
  }
10742
10810
  function statsService(args) {
10743
- const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
10811
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
10744
10812
  try {
10745
10813
  return store.getStats();
10746
10814
  } finally {
10747
- store.close();
10815
+ indexStorePool.release(store);
10748
10816
  }
10749
10817
  }
10750
10818
 
@@ -16098,7 +16166,7 @@ import {
16098
16166
  duplicateBoard,
16099
16167
  exportBoardAsMarkdown,
16100
16168
  exportBoardToTaskGraph,
16101
- generateBoardFromDescription,
16169
+ createBoardFromText,
16102
16170
  getBoard as getBoard2,
16103
16171
  getKanbanOrchestrationSnapshot,
16104
16172
  getKanbanQueueHealth,
@@ -16622,7 +16690,7 @@ var kanbanTool = {
16622
16690
  }
16623
16691
  case "generate_board": {
16624
16692
  if (!input.description) return fail("generate_board requires description.");
16625
- const boardInput = generateBoardFromDescription({
16693
+ const boardInput = createBoardFromText({
16626
16694
  description: input.description,
16627
16695
  ...input.title !== void 0 ? { title: input.title } : {},
16628
16696
  ...input.context !== void 0 ? { context: input.context } : {},
@@ -17007,20 +17075,31 @@ var kanbanTool = {
17007
17075
  if (!input.boardId || !input.taskId)
17008
17076
  return fail("mark_assignment requires boardId and taskId.");
17009
17077
  const assignmentStatus = input.assignmentStatus ?? (input.status === "completed" ? "completed" : input.error ? "failed" : void 0);
17010
- const board = await updateTaskAssignment(projectRoot, input.boardId, input.taskId, {
17011
- ...assignmentStatus !== void 0 ? { status: assignmentStatus } : {},
17012
- ...input.subagentId !== void 0 ? { subagentId: input.subagentId } : {},
17013
- ...input.runTaskId !== void 0 ? { runTaskId: input.runTaskId } : {},
17014
- ...input.lastResult !== void 0 ? { lastResult: input.lastResult } : {},
17015
- ...input.error !== void 0 ? { error: input.error } : {},
17016
- ...input.agentId !== void 0 ? { agentId: input.agentId } : {},
17017
- ...input.leaseId !== void 0 ? { leaseId: input.leaseId } : {},
17018
- ...input.claimedAt !== void 0 ? { claimedAt: input.claimedAt } : {},
17019
- ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
17020
- ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
17021
- ...input.attempt !== void 0 ? { attempt: input.attempt } : {},
17022
- ...input.maxAttempts !== void 0 ? { maxAttempts: input.maxAttempts } : {}
17023
- });
17078
+ const board = await updateTaskAssignment(
17079
+ projectRoot,
17080
+ input.boardId,
17081
+ input.taskId,
17082
+ {
17083
+ ...assignmentStatus !== void 0 ? { status: assignmentStatus } : {},
17084
+ ...input.subagentId !== void 0 ? { subagentId: input.subagentId } : {},
17085
+ ...input.runTaskId !== void 0 ? { runTaskId: input.runTaskId } : {},
17086
+ ...input.lastResult !== void 0 ? { lastResult: input.lastResult } : {},
17087
+ ...input.error !== void 0 ? { error: input.error } : {},
17088
+ ...input.agentId !== void 0 ? { agentId: input.agentId } : {},
17089
+ ...input.leaseId !== void 0 ? { leaseId: input.leaseId } : {},
17090
+ ...input.claimedAt !== void 0 ? { claimedAt: input.claimedAt } : {},
17091
+ ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
17092
+ ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
17093
+ ...input.attempt !== void 0 ? { attempt: input.attempt } : {},
17094
+ ...input.maxAttempts !== void 0 ? { maxAttempts: input.maxAttempts } : {}
17095
+ },
17096
+ // Ownership fence: when expectedLeaseId is supplied, the write is
17097
+ // applied only if the current assignment still holds this lease.
17098
+ // This prevents a recovered+reassigned stale worker's terminal
17099
+ // mark_assignment from overwriting the successor's state. The check
17100
+ // is atomic inside updateTaskAssignment's mutateBoard lock.
17101
+ input.expectedLeaseId !== void 0 ? { expectedLeaseId: input.expectedLeaseId } : {}
17102
+ );
17024
17103
  return board ? okBoard(board, "Assignment updated.") : fail("Task not found.");
17025
17104
  }
17026
17105
  case "heartbeat_assignment": {
@@ -17029,7 +17108,13 @@ var kanbanTool = {
17029
17108
  }
17030
17109
  const board = await heartbeatTaskAssignment(projectRoot, input.boardId, input.taskId, {
17031
17110
  ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
17032
- ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {}
17111
+ ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
17112
+ // Ownership fence: when expectedLeaseId is supplied, the renewal
17113
+ // is applied only if the current assignment still holds this lease.
17114
+ // This prevents a recovered+reassigned stale worker's heartbeat
17115
+ // from renewing the successor's lease. The check is atomic inside
17116
+ // heartbeatTaskAssignment's mutateBoard lock.
17117
+ ...input.expectedLeaseId !== void 0 ? { expectedLeaseId: input.expectedLeaseId } : {}
17033
17118
  });
17034
17119
  return board ? okBoard(board, "Assignment heartbeat updated.") : fail("Task assignment not found.");
17035
17120
  }