@wrongstack/tools 0.236.0 → 0.250.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.
Files changed (46) hide show
  1. package/dist/audit.js +1 -0
  2. package/dist/audit.js.map +1 -1
  3. package/dist/background-indexer-DwJsyAB0.d.ts +373 -0
  4. package/dist/bash.js +5 -0
  5. package/dist/bash.js.map +1 -1
  6. package/dist/builtin.js +865 -327
  7. package/dist/builtin.js.map +1 -1
  8. package/dist/codebase-index/index.d.ts +53 -2
  9. package/dist/codebase-index/index.js +854 -364
  10. package/dist/codebase-index/index.js.map +1 -1
  11. package/dist/codebase-index/worker.d.ts +2 -0
  12. package/dist/codebase-index/worker.js +2321 -0
  13. package/dist/codebase-index/worker.js.map +1 -0
  14. package/dist/diff.js +2 -1
  15. package/dist/diff.js.map +1 -1
  16. package/dist/exec.js +1 -0
  17. package/dist/exec.js.map +1 -1
  18. package/dist/format.js +1 -0
  19. package/dist/format.js.map +1 -1
  20. package/dist/git.js +2 -1
  21. package/dist/git.js.map +1 -1
  22. package/dist/grep.js +2 -2
  23. package/dist/grep.js.map +1 -1
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.js +886 -386
  26. package/dist/index.js.map +1 -1
  27. package/dist/install.js +1 -0
  28. package/dist/install.js.map +1 -1
  29. package/dist/lint.js +1 -0
  30. package/dist/lint.js.map +1 -1
  31. package/dist/logs.js +1 -1
  32. package/dist/logs.js.map +1 -1
  33. package/dist/outdated.js +1 -1
  34. package/dist/outdated.js.map +1 -1
  35. package/dist/pack.js +865 -327
  36. package/dist/pack.js.map +1 -1
  37. package/dist/patch.js +1 -1
  38. package/dist/patch.js.map +1 -1
  39. package/dist/replace.js +3 -2
  40. package/dist/replace.js.map +1 -1
  41. package/dist/test.js +1 -0
  42. package/dist/test.js.map +1 -1
  43. package/dist/typecheck.js +1 -0
  44. package/dist/typecheck.js.map +1 -1
  45. package/package.json +2 -2
  46. package/dist/background-indexer-CtbgPExj.d.ts +0 -228
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import * as fs4 from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
3
  import { resolve, sep, dirname, join } from 'node:path';
4
4
  import * as Core from '@wrongstack/core';
5
- import { atomicWrite, unifiedDiff, detectNewlineStyle, normalizeToLf, toStyle, compileGlob, expectDefined, buildChildEnv, loadPlan, setPlanItemStatus, savePlan, loadTasks, saveTasks, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, emptyTaskFile, formatTaskList, formatPlan, recordPackageAction, detectPackageEcosystem, mutateTasks, emptyPlan, computeTaskItemProgress, resolveWstackPaths } from '@wrongstack/core';
5
+ import { atomicWrite, unifiedDiff, detectNewlineStyle, normalizeToLf, toStyle, compileGlob, expectDefined, buildChildEnv, loadPlan, setPlanItemStatus, savePlan, loadTasks, saveTasks, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, emptyTaskFile, formatTaskList, formatPlan, recordPackageAction, detectPackageEcosystem, mutateTasks, emptyPlan, computeTaskItemProgress, resolveWstackPaths, truncate } from '@wrongstack/core';
6
6
  import { spawn, execFileSync, spawnSync } from 'node:child_process';
7
7
  import * as os from 'node:os';
8
8
  import * as fs7 from 'node:fs';
@@ -11,6 +11,8 @@ import * as dns from 'node:dns/promises';
11
11
  import * as net from 'node:net';
12
12
  import { Agent } from 'undici';
13
13
  import { createRequire } from 'node:module';
14
+ import { fileURLToPath } from 'node:url';
15
+ import { Worker } from 'node:worker_threads';
14
16
  import * as ts from 'typescript';
15
17
  import { randomUUID } from 'node:crypto';
16
18
 
@@ -599,7 +601,7 @@ async function globFiles(pattern, base, extraGlob) {
599
601
  function checkRg() {
600
602
  return new Promise((resolve7) => {
601
603
  try {
602
- const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore" });
604
+ const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore", windowsHide: true });
603
605
  p.on("error", () => resolve7(false));
604
606
  p.on("close", (code) => resolve7(code === 0));
605
607
  } catch {
@@ -612,7 +614,8 @@ function spawnRgFind(pattern, base) {
612
614
  const child = spawn("rg", args, {
613
615
  signal: AbortSignal.timeout(3e4),
614
616
  env: buildChildEnv(),
615
- stdio: ["ignore", "pipe", "pipe"]
617
+ stdio: ["ignore", "pipe", "pipe"],
618
+ windowsHide: true
616
619
  });
617
620
  let buf = "";
618
621
  child.stdout?.on("data", (chunk) => {
@@ -828,7 +831,7 @@ var grepTool = {
828
831
  async function detectRg(signal) {
829
832
  return new Promise((resolve7) => {
830
833
  try {
831
- const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore", signal });
834
+ const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore", signal, windowsHide: true });
832
835
  p.on("error", () => resolve7(false));
833
836
  p.on("close", (code) => resolve7(code === 0));
834
837
  } catch {
@@ -858,7 +861,7 @@ async function* runRgStream(input, base, mode, limit, signal) {
858
861
  const FLUSH_AT = 16;
859
862
  const MAX_BUF_BYTES = 1e6;
860
863
  let bufOverflow = false;
861
- const child = spawn("rg", args, { signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"] });
864
+ const child = spawn("rg", args, { signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
862
865
  const queue = [];
863
866
  let waiter;
864
867
  const wake = () => {
@@ -1523,6 +1526,10 @@ var bashTool = {
1523
1526
  env,
1524
1527
  stdio: ["ignore", "pipe", "pipe"],
1525
1528
  detached: true,
1529
+ // Detached console children on Windows allocate their own VISIBLE
1530
+ // console window (one per background command — test suites flash
1531
+ // dozens). CREATE_NO_WINDOW suppresses it; no-op elsewhere.
1532
+ windowsHide: true,
1526
1533
  signal: opts.signal
1527
1534
  });
1528
1535
  const pid2 = child2.pid;
@@ -1575,6 +1582,7 @@ var bashTool = {
1575
1582
  env,
1576
1583
  stdio: ["ignore", "pipe", "pipe"],
1577
1584
  detached,
1585
+ windowsHide: true,
1578
1586
  ...isWin ? {} : { signal: opts.signal }
1579
1587
  });
1580
1588
  const pid = child.pid;
@@ -1589,7 +1597,7 @@ var bashTool = {
1589
1597
  });
1590
1598
  }
1591
1599
  let buf = "";
1592
- let pending = "";
1600
+ let pending2 = "";
1593
1601
  let timedOut = false;
1594
1602
  const timers = [];
1595
1603
  function killWithTimeout(child2, timeoutMs2) {
@@ -1671,9 +1679,9 @@ var bashTool = {
1671
1679
  });
1672
1680
  let lastFlush = Date.now();
1673
1681
  const flush = () => {
1674
- if (pending.length === 0) return null;
1675
- const text = pending;
1676
- pending = "";
1682
+ if (pending2.length === 0) return null;
1683
+ const text = pending2;
1684
+ pending2 = "";
1677
1685
  lastFlush = Date.now();
1678
1686
  return text;
1679
1687
  };
@@ -1697,7 +1705,7 @@ var bashTool = {
1697
1705
  if (buf.length < MAX_OUTPUT) {
1698
1706
  buf += text.slice(0, MAX_OUTPUT - buf.length);
1699
1707
  }
1700
- pending += text;
1708
+ pending2 += text;
1701
1709
  push({ kind: "data", text });
1702
1710
  pauseIfFlooded();
1703
1711
  };
@@ -1735,7 +1743,7 @@ var bashTool = {
1735
1743
  return;
1736
1744
  }
1737
1745
  const now = Date.now();
1738
- if (pending.length >= STREAM_FLUSH_BYTES || now - lastFlush >= STREAM_FLUSH_INTERVAL_MS) {
1746
+ if (pending2.length >= STREAM_FLUSH_BYTES || now - lastFlush >= STREAM_FLUSH_INTERVAL_MS) {
1739
1747
  const text = flush();
1740
1748
  if (text) yield { type: "partial_output", text };
1741
1749
  }
@@ -1989,6 +1997,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
1989
1997
  cwd,
1990
1998
  env: buildChildEnv(sessionId),
1991
1999
  stdio: ["ignore", "pipe", "pipe"],
2000
+ windowsHide: true,
1992
2001
  ...isWin ? {} : { signal },
1993
2002
  ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
1994
2003
  });
@@ -3151,7 +3160,8 @@ function runGit(args, cwd, signal) {
3151
3160
  cwd,
3152
3161
  signal,
3153
3162
  env: buildChildEnv(),
3154
- stdio: ["ignore", "pipe", "pipe"]
3163
+ stdio: ["ignore", "pipe", "pipe"],
3164
+ windowsHide: true
3155
3165
  });
3156
3166
  child.stdout?.on("data", (chunk) => {
3157
3167
  if (stdout.length < MAX_OUTPUT3) {
@@ -3274,7 +3284,7 @@ function runPatch(args, cwd, signal) {
3274
3284
  let stdout = "";
3275
3285
  let stderr = "";
3276
3286
  const env = { ...buildChildEnv(), LANG: "C", LC_ALL: "C" };
3277
- const child = spawn("patch", args, { cwd, signal, env, stdio: ["pipe", "pipe", "pipe"] });
3287
+ const child = spawn("patch", args, { cwd, signal, env, stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
3278
3288
  child.stdout?.on("data", (c) => {
3279
3289
  stdout += c.toString();
3280
3290
  });
@@ -3513,7 +3523,8 @@ function runGit2(args, cwd, signal) {
3513
3523
  cwd,
3514
3524
  signal,
3515
3525
  env: buildChildEnv(),
3516
- stdio: ["ignore", "pipe", "pipe"]
3526
+ stdio: ["ignore", "pipe", "pipe"],
3527
+ windowsHide: true
3517
3528
  });
3518
3529
  child.stdout?.on("data", (c) => {
3519
3530
  stdout += c.toString();
@@ -3741,7 +3752,7 @@ async function* spawnStream(opts) {
3741
3752
  const maxQueue = opts.maxQueueSize ?? 500;
3742
3753
  let stdout = "";
3743
3754
  let stderr = "";
3744
- let pending = "";
3755
+ let pending2 = "";
3745
3756
  let error;
3746
3757
  const cmd = resolveWin32Command(opts.cmd);
3747
3758
  const needsShell = process.platform === "win32" && (cmd.endsWith(".cmd") || cmd.endsWith(".bat"));
@@ -3750,6 +3761,7 @@ async function* spawnStream(opts) {
3750
3761
  signal: opts.signal,
3751
3762
  env: buildChildEnv(),
3752
3763
  stdio: ["ignore", "pipe", "pipe"],
3764
+ windowsHide: true,
3753
3765
  ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
3754
3766
  });
3755
3767
  const queue = [];
@@ -3819,14 +3831,14 @@ async function* spawnStream(opts) {
3819
3831
  exitCode = 1;
3820
3832
  continue;
3821
3833
  }
3822
- pending += chunk.data;
3823
- if (pending.length >= flushAt) {
3824
- yield { type: "partial_output", text: pending };
3825
- pending = "";
3834
+ pending2 += chunk.data;
3835
+ if (pending2.length >= flushAt) {
3836
+ yield { type: "partial_output", text: pending2 };
3837
+ pending2 = "";
3826
3838
  }
3827
3839
  }
3828
- if (pending.length > 0) {
3829
- yield { type: "partial_output", text: pending };
3840
+ if (pending2.length > 0) {
3841
+ yield { type: "partial_output", text: pending2 };
3830
3842
  }
3831
3843
  return {
3832
3844
  stdout,
@@ -4539,7 +4551,7 @@ function runOutdated(manager, args, cwd, signal) {
4539
4551
  const MAX = 1e5;
4540
4552
  const resolved = resolveWin32Command(manager);
4541
4553
  const needsShell = process.platform === "win32" && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
4542
- const child = spawn(resolved, args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {} });
4554
+ const child = spawn(resolved, args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true, ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {} });
4543
4555
  child.stdout?.on("data", (c) => {
4544
4556
  if (stdout.length < MAX) stdout += c.toString();
4545
4557
  });
@@ -4693,7 +4705,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
4693
4705
  clearTimeout(timer);
4694
4706
  resolve7(result);
4695
4707
  };
4696
- const child = spawn("docker", args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"] });
4708
+ const child = spawn("docker", args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
4697
4709
  const timer = setTimeout(() => {
4698
4710
  child.kill("SIGTERM");
4699
4711
  finish(empty());
@@ -5754,8 +5766,91 @@ ${mode.description}`
5754
5766
  };
5755
5767
  }
5756
5768
 
5769
+ // src/codebase-index/circuit-breaker.ts
5770
+ var CircuitOpenError = class extends Error {
5771
+ name = "CircuitOpenError";
5772
+ };
5773
+ var IndexTimeoutError = class extends Error {
5774
+ name = "IndexTimeoutError";
5775
+ };
5776
+ var LockError = class extends Error {
5777
+ name = "LockError";
5778
+ };
5779
+ var IndexCircuitBreaker = class {
5780
+ failureThreshold;
5781
+ cooldownMs;
5782
+ now;
5783
+ state = "closed";
5784
+ consecutiveFailures = 0;
5785
+ openedAt = 0;
5786
+ lastFailure = null;
5787
+ probeInFlight = false;
5788
+ constructor(opts = {}) {
5789
+ this.failureThreshold = opts.failureThreshold ?? 3;
5790
+ this.cooldownMs = opts.cooldownMs ?? 6e4;
5791
+ this.now = opts.now ?? Date.now;
5792
+ }
5793
+ /**
5794
+ * True when a run may proceed. An open circuit transitions to half-open once
5795
+ * the cooldown has elapsed, admitting exactly one probe; further requests
5796
+ * are rejected until that probe settles via recordSuccess/recordFailure.
5797
+ */
5798
+ allowRequest() {
5799
+ if (this.state === "closed") return true;
5800
+ if (this.state === "open") {
5801
+ if (this.now() - this.openedAt < this.cooldownMs) return false;
5802
+ this.state = "half-open";
5803
+ this.probeInFlight = true;
5804
+ return true;
5805
+ }
5806
+ if (this.probeInFlight) return false;
5807
+ this.probeInFlight = true;
5808
+ return true;
5809
+ }
5810
+ recordSuccess() {
5811
+ this.state = "closed";
5812
+ this.consecutiveFailures = 0;
5813
+ this.lastFailure = null;
5814
+ this.probeInFlight = false;
5815
+ }
5816
+ recordFailure(err) {
5817
+ if (err instanceof LockError) {
5818
+ this.lastFailure = `[transient/lock] ${err.message}`;
5819
+ this.probeInFlight = false;
5820
+ return;
5821
+ }
5822
+ this.lastFailure = err instanceof Error ? err.message : String(err);
5823
+ this.probeInFlight = false;
5824
+ this.consecutiveFailures++;
5825
+ if (this.state === "half-open" || this.consecutiveFailures >= this.failureThreshold) {
5826
+ this.state = "open";
5827
+ this.openedAt = this.now();
5828
+ }
5829
+ }
5830
+ /** Force-close the circuit (manual recovery: `/codebase-reindex`). */
5831
+ reset() {
5832
+ this.state = "closed";
5833
+ this.consecutiveFailures = 0;
5834
+ this.lastFailure = null;
5835
+ this.probeInFlight = false;
5836
+ this.openedAt = 0;
5837
+ }
5838
+ snapshot() {
5839
+ return {
5840
+ state: this.state,
5841
+ consecutiveFailures: this.consecutiveFailures,
5842
+ lastFailure: this.lastFailure,
5843
+ cooldownRemainingMs: this.state === "open" ? Math.max(0, this.cooldownMs - (this.now() - this.openedAt)) : 0
5844
+ };
5845
+ }
5846
+ };
5847
+ var indexCircuitBreaker = new IndexCircuitBreaker();
5848
+ function resetIndexCircuitBreaker() {
5849
+ indexCircuitBreaker.reset();
5850
+ }
5851
+
5757
5852
  // src/codebase-index/schema.ts
5758
- var SCHEMA_VERSION = 1;
5853
+ var SCHEMA_VERSION = 2;
5759
5854
 
5760
5855
  // src/codebase-index/lsp-kind.ts
5761
5856
  function lspKindToInternalKind(k) {
@@ -5790,6 +5885,94 @@ function lspKindToInternalKind(k) {
5790
5885
  }
5791
5886
  }
5792
5887
 
5888
+ // src/codebase-index/bm25.ts
5889
+ var K1 = 1.5;
5890
+ var B = 0.75;
5891
+ function tokenise(text) {
5892
+ const sanitised = text.replace(/[^\p{L}\p{N}$'_]/gu, " ").replace(/_/g, " ");
5893
+ return sanitised.toLowerCase().split(" ").filter(Boolean);
5894
+ }
5895
+ function splitName(name) {
5896
+ return name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
5897
+ }
5898
+ function buildIndexableText(name, signature, docComment) {
5899
+ return [splitName(name), name, signature, docComment].filter(Boolean).join(" ");
5900
+ }
5901
+ function buildBm25Index(docs) {
5902
+ const documents = docs.map((d) => {
5903
+ const tokens = tokenise(d.text);
5904
+ return { id: d.id, tokens, raw: d.text, len: tokens.length };
5905
+ });
5906
+ const df = {};
5907
+ for (const doc of documents) {
5908
+ const seen = /* @__PURE__ */ new Set();
5909
+ for (const t of doc.tokens) {
5910
+ if (!seen.has(t)) {
5911
+ df[t] = (df[t] ?? 0) + 1;
5912
+ seen.add(t);
5913
+ }
5914
+ }
5915
+ }
5916
+ const N = documents.length;
5917
+ const totalLen = documents.reduce((sum, d) => sum + d.len, 0);
5918
+ const avgLen = N === 0 ? 0 : totalLen / N;
5919
+ return new Bm25Index(documents, df, N, avgLen);
5920
+ }
5921
+ var Bm25Index = class {
5922
+ constructor(documents, df, N, avgLen) {
5923
+ this.documents = documents;
5924
+ this.df = df;
5925
+ this.N = N;
5926
+ this.safeAvgLen = avgLen === 0 ? 1 : avgLen;
5927
+ }
5928
+ documents;
5929
+ df;
5930
+ N;
5931
+ safeAvgLen;
5932
+ score(query2, filter) {
5933
+ const qTokens = tokenise(query2);
5934
+ if (qTokens.length === 0) return [];
5935
+ const results = [];
5936
+ for (const doc of this.documents) {
5937
+ if (filter && !filter(doc.id)) continue;
5938
+ let docScore = 0;
5939
+ for (const qTerm of qTokens) {
5940
+ let tf = 0;
5941
+ for (const t of doc.tokens) {
5942
+ if (t === qTerm) tf++;
5943
+ }
5944
+ if (tf === 0) continue;
5945
+ const dfVal = this.df[qTerm] ?? 0;
5946
+ if (dfVal === 0) continue;
5947
+ const idf = Math.log((this.N - dfVal + 0.5) / (dfVal + 0.5) + 1);
5948
+ const lenRatio = B * (doc.len / this.safeAvgLen);
5949
+ const tfComponent = tf * (K1 + 1) / (tf + K1 * (1 - B + lenRatio));
5950
+ docScore += idf * tfComponent;
5951
+ }
5952
+ if (docScore > 0) results.push({ id: doc.id, score: docScore });
5953
+ }
5954
+ return results;
5955
+ }
5956
+ getDoc(id) {
5957
+ return this.documents.find((d) => d.id === id);
5958
+ }
5959
+ extractSnippet(docId, queryTokens, radius = 40) {
5960
+ const doc = this.getDoc(docId);
5961
+ if (!doc) return "";
5962
+ for (const tok of queryTokens) {
5963
+ const idx = doc.raw.toLowerCase().indexOf(tok);
5964
+ if (idx !== -1) {
5965
+ const start = Math.max(0, idx - radius);
5966
+ const end = Math.min(doc.raw.length, idx + tok.length + radius);
5967
+ const excerpt = doc.raw.slice(start, end);
5968
+ const ellipsis = "\u2026";
5969
+ return (start > 0 ? ellipsis : "") + excerpt + (end < doc.raw.length ? ellipsis : "");
5970
+ }
5971
+ }
5972
+ return doc.raw.slice(0, radius * 2) + (doc.raw.length > radius * 2 ? "\u2026" : "");
5973
+ }
5974
+ };
5975
+
5793
5976
  // src/codebase-index/writer.ts
5794
5977
  var DB_FILE = "index.db";
5795
5978
  function resolveIndexDir(projectRoot, override) {
@@ -5825,15 +6008,79 @@ function loadDatabaseSync() {
5825
6008
  }
5826
6009
  return DatabaseSyncCtor;
5827
6010
  }
6011
+ var MAX_LOCK_RETRIES = 3;
6012
+ var LOCK_RETRY_BASE_DELAY_MS = 50;
6013
+ var LOCK_RETRY_MAX_DELAY_MS = 500;
6014
+ function isLockError(err) {
6015
+ if (!(err instanceof Error)) return false;
6016
+ const e = err;
6017
+ const code = e.code ?? e.sqliteCode;
6018
+ if (typeof code === "string" && /SQLITE_(BUSY|LOCKED)/.test(code)) return true;
6019
+ if (typeof code === "number" && (code === 5 || code === 6)) return true;
6020
+ if (/SQLITE_(BUSY|LOCKED)/.test(err.message)) return true;
6021
+ return false;
6022
+ }
6023
+ function sleepSync(ms) {
6024
+ try {
6025
+ const sab = new SharedArrayBuffer(4);
6026
+ const view = new Int32Array(sab);
6027
+ Atomics.wait(view, 0, 0, ms);
6028
+ } catch {
6029
+ }
6030
+ }
5828
6031
  var IndexStore = class {
5829
6032
  db;
5830
6033
  /** Absolute path to this project's index directory. */
5831
6034
  indexDir;
6035
+ /**
6036
+ * True when the SQLite build provides FTS5 (Node's bundled SQLite does).
6037
+ * When false, ranked search falls back to the LIKE + in-process BM25 path.
6038
+ */
6039
+ ftsAvailable = false;
6040
+ /**
6041
+ * Execute a SQLite write operation with automatic retry on lock conflicts.
6042
+ *
6043
+ * When another wstack process is holding the write lock the statement first
6044
+ * waits up to `busy_timeout` ms, then throws SQLITE_BUSY. This wrapper catches
6045
+ * that error and retries (up to MAX_LOCK_RETRIES) with exponential backoff,
6046
+ * giving the competing writer time to finish and release the lock.
6047
+ *
6048
+ * @param fn The write operation to execute. Can return a value which is
6049
+ * returned to the caller on success.
6050
+ * @throws {@link LockError} when all retries are exhausted on a lock conflict
6051
+ * (non-lock errors always propagate on the first attempt).
6052
+ */
6053
+ runWithRetry(fn) {
6054
+ let lastError;
6055
+ for (let attempt = 0; attempt <= MAX_LOCK_RETRIES; attempt++) {
6056
+ try {
6057
+ return fn();
6058
+ } catch (err) {
6059
+ lastError = err;
6060
+ if (!isLockError(err)) throw err;
6061
+ if (attempt === MAX_LOCK_RETRIES) {
6062
+ const msg = lastError instanceof Error ? lastError.message : String(lastError);
6063
+ throw new LockError(`SQLite lock conflict after ${MAX_LOCK_RETRIES} retries: ${msg}`);
6064
+ }
6065
+ const delay = Math.min(
6066
+ LOCK_RETRY_BASE_DELAY_MS * Math.pow(2, attempt),
6067
+ LOCK_RETRY_MAX_DELAY_MS
6068
+ );
6069
+ sleepSync(delay);
6070
+ }
6071
+ }
6072
+ throw lastError;
6073
+ }
5832
6074
  constructor(projectRoot, opts = {}) {
5833
6075
  this.indexDir = resolveIndexDir(projectRoot, opts.indexDir);
5834
6076
  fs7.mkdirSync(this.indexDir, { recursive: true });
5835
6077
  const Database = loadDatabaseSync();
5836
6078
  this.db = new Database(path.join(this.indexDir, DB_FILE));
6079
+ try {
6080
+ this.db.exec("PRAGMA journal_mode = WAL");
6081
+ this.db.exec("PRAGMA busy_timeout = 5000");
6082
+ } catch {
6083
+ }
5837
6084
  this.initSchema();
5838
6085
  }
5839
6086
  initSchema() {
@@ -5842,6 +6089,21 @@ var IndexStore = class {
5842
6089
  key TEXT PRIMARY KEY,
5843
6090
  value TEXT NOT NULL
5844
6091
  );
6092
+ `);
6093
+ const storedRows = this.db.prepare("SELECT value FROM metadata WHERE key = ?").all("version");
6094
+ const storedVersion = storedRows.length ? Number(storedRows[0]?.value) : null;
6095
+ if (storedVersion !== null && storedVersion !== SCHEMA_VERSION) {
6096
+ this.db.exec(`
6097
+ DROP TABLE IF EXISTS symbols;
6098
+ DROP TABLE IF EXISTS files;
6099
+ DROP TABLE IF EXISTS refs;
6100
+ `);
6101
+ this.db.exec("DROP TABLE IF EXISTS symbols_fts");
6102
+ this.db.prepare("UPDATE metadata SET value = ? WHERE key = ?").run(String(SCHEMA_VERSION), "version");
6103
+ } else if (storedVersion === null) {
6104
+ this.db.prepare("INSERT INTO metadata(key, value) VALUES (?, ?)").run("version", String(SCHEMA_VERSION));
6105
+ }
6106
+ this.db.exec(`
5845
6107
  CREATE TABLE IF NOT EXISTS files (
5846
6108
  file TEXT PRIMARY KEY,
5847
6109
  lang TEXT NOT NULL,
@@ -5882,53 +6144,76 @@ var IndexStore = class {
5882
6144
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_to_id ON refs(to_id)");
5883
6145
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)");
5884
6146
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)");
5885
- const versionRows = this.db.prepare("SELECT value FROM metadata WHERE key = ?").all("version");
5886
- if (!versionRows.length) {
5887
- this.db.prepare("INSERT INTO metadata(key, value) VALUES (?, ?)").run("version", String(SCHEMA_VERSION));
6147
+ try {
6148
+ this.db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')");
6149
+ this.ftsAvailable = true;
6150
+ } catch {
6151
+ this.ftsAvailable = false;
5888
6152
  }
5889
6153
  }
5890
6154
  // ─── Symbol CRUD ─────────────────────────────────────────────────────────────
5891
6155
  insertSymbols(symbols, nextId) {
5892
- const stmt = this.db.prepare(
5893
- `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
5894
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
5895
- );
5896
- let id = nextId;
5897
- for (const s of symbols) {
5898
- stmt.run(
5899
- id++,
5900
- s.lang,
5901
- s.kind,
5902
- s.name,
5903
- s.file,
5904
- s.line,
5905
- s.col,
5906
- s.signature,
5907
- s.docComment,
5908
- s.scope,
5909
- s.text,
5910
- s.file
6156
+ return this.runWithRetry(() => {
6157
+ const stmt = this.db.prepare(
6158
+ `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
6159
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
5911
6160
  );
5912
- }
5913
- return id;
6161
+ const ftsStmt = this.ftsAvailable ? this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)") : null;
6162
+ let id = nextId;
6163
+ for (const s of symbols) {
6164
+ stmt.run(
6165
+ id,
6166
+ s.lang,
6167
+ s.kind,
6168
+ s.name,
6169
+ s.file,
6170
+ s.line,
6171
+ s.col,
6172
+ s.signature,
6173
+ s.docComment,
6174
+ s.scope,
6175
+ s.text,
6176
+ s.file
6177
+ );
6178
+ ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
6179
+ id++;
6180
+ }
6181
+ return id;
6182
+ });
5914
6183
  }
5915
6184
  deleteSymbolsForFile(file) {
5916
- this.db.prepare("DELETE FROM symbols WHERE file_fk = ?").run(file);
6185
+ this.runWithRetry(() => {
6186
+ if (this.ftsAvailable) {
6187
+ this.db.prepare("DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
6188
+ }
6189
+ this.db.prepare("DELETE FROM symbols WHERE file_fk = ?").run(file);
6190
+ });
5917
6191
  }
6192
+ /**
6193
+ * Remove every trace of a file (refs, symbols, FTS rows, file meta). Used
6194
+ * when a source file disappears between index runs — previously this only
6195
+ * dropped the `files` row, leaving its symbols orphaned but still searchable.
6196
+ */
5918
6197
  deleteFile(file) {
5919
- this.db.prepare("DELETE FROM files WHERE file = ?").run(file);
6198
+ this.runWithRetry(() => {
6199
+ this.deleteRefsForFile(file);
6200
+ this.deleteSymbolsForFile(file);
6201
+ this.db.prepare("DELETE FROM files WHERE file = ?").run(file);
6202
+ });
5920
6203
  }
5921
6204
  // ─── File metadata ──────────────────────────────────────────────────────────
5922
6205
  upsertFile(meta) {
5923
- this.db.prepare(
5924
- `INSERT INTO files(file, lang, mtime_ms, symbol_count, last_indexed)
5925
- VALUES (?, ?, ?, ?, ?)
5926
- ON CONFLICT(file) DO UPDATE SET
5927
- lang = excluded.lang,
5928
- mtime_ms = excluded.mtime_ms,
5929
- symbol_count = excluded.symbol_count,
5930
- last_indexed = excluded.last_indexed`
5931
- ).run(meta.file, meta.lang, meta.mtimeMs, meta.symbolCount, meta.lastIndexed);
6206
+ this.runWithRetry(() => {
6207
+ this.db.prepare(
6208
+ `INSERT INTO files(file, lang, mtime_ms, symbol_count, last_indexed)
6209
+ VALUES (?, ?, ?, ?, ?)
6210
+ ON CONFLICT(file) DO UPDATE SET
6211
+ lang = excluded.lang,
6212
+ mtime_ms = excluded.mtime_ms,
6213
+ symbol_count = excluded.symbol_count,
6214
+ last_indexed = excluded.last_indexed`
6215
+ ).run(meta.file, meta.lang, meta.mtimeMs, meta.symbolCount, meta.lastIndexed);
6216
+ });
5932
6217
  }
5933
6218
  getFileMeta(file) {
5934
6219
  const rows = this.db.prepare(
@@ -5995,6 +6280,94 @@ var IndexStore = class {
5995
6280
  lspKind: filter?.lspKind
5996
6281
  }));
5997
6282
  }
6283
+ /**
6284
+ * Ranked search — the one-stop query the codebase-search tool and plug-lsp
6285
+ * use. With FTS5 this is a single indexed `MATCH` ranked by SQLite's native
6286
+ * `bm25()` with a built-in `snippet()`; without FTS5 it falls back to the
6287
+ * legacy LIKE scan + in-process BM25 (identical semantics, slower).
6288
+ *
6289
+ * Tokens are matched as prefixes (`"tok"*`), mirroring the old
6290
+ * `LIKE '%tok%'` recall for the common symbol-search shapes ("user" finds
6291
+ * "users", camelCase-split text makes "complex" find "complexOperation").
6292
+ */
6293
+ searchRanked(query2, filter, limit) {
6294
+ const tokens = tokenise(query2);
6295
+ if (tokens.length === 0 || !this.ftsAvailable) {
6296
+ return this.searchRankedFallback(query2, filter, limit);
6297
+ }
6298
+ let effectiveKind = filter?.kind;
6299
+ if (filter?.lspKind !== void 0) {
6300
+ const mapped = lspKindToInternalKind(filter.lspKind);
6301
+ if (mapped === null) return { results: [], total: 0 };
6302
+ effectiveKind = mapped;
6303
+ }
6304
+ const match = tokens.map((t) => `"${t.replaceAll('"', "")}"*`).join(" OR ");
6305
+ const conditions = ["symbols_fts MATCH ?"];
6306
+ const values = [match];
6307
+ if (effectiveKind) {
6308
+ conditions.push("s.kind = ?");
6309
+ values.push(effectiveKind);
6310
+ }
6311
+ if (filter?.lang) {
6312
+ conditions.push("s.lang = ?");
6313
+ values.push(filter.lang);
6314
+ }
6315
+ if (filter?.file) {
6316
+ conditions.push("s.file LIKE ?");
6317
+ values.push(`%${filter.file}%`);
6318
+ }
6319
+ const where = conditions.join(" AND ");
6320
+ const countRows = this.db.prepare(`SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`).all(...values);
6321
+ const total = countRows[0] ? Number(countRows[0].n) : 0;
6322
+ if (total === 0) return { results: [], total: 0 };
6323
+ const rows = this.db.prepare(
6324
+ `SELECT s.id, s.lang, s.kind, s.name, s.file, s.line, s.col, s.signature, s.doc_comment,
6325
+ -bm25(symbols_fts) AS score,
6326
+ snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet
6327
+ FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid
6328
+ WHERE ${where}
6329
+ ORDER BY bm25(symbols_fts)
6330
+ LIMIT ?`
6331
+ ).all(...values, limit);
6332
+ return {
6333
+ results: rows.map((r) => ({
6334
+ id: r.id,
6335
+ lang: r.lang,
6336
+ kind: r.kind,
6337
+ name: r.name,
6338
+ file: r.file,
6339
+ line: r.line,
6340
+ col: r.col,
6341
+ signature: r.signature,
6342
+ docComment: r.doc_comment,
6343
+ // bm25() is negative-is-better; negate so callers keep "higher is
6344
+ // better" and clamp so a match never reports a zero score.
6345
+ score: Math.max(1e-4, r.score),
6346
+ snippet: r.snippet,
6347
+ lspKind: filter?.lspKind
6348
+ })),
6349
+ total
6350
+ };
6351
+ }
6352
+ /** Legacy ranked path: LIKE candidates + in-process BM25 + JS snippets. */
6353
+ searchRankedFallback(query2, filter, limit) {
6354
+ const candidates = this.search(query2, filter);
6355
+ if (candidates.length === 0) return { results: [], total: 0 };
6356
+ if (!query2.trim()) {
6357
+ return { results: candidates.slice(0, limit), total: candidates.length };
6358
+ }
6359
+ const bm25 = buildBm25Index(
6360
+ candidates.map((c) => ({ id: c.id, text: buildIndexableText(c.name, c.signature, c.docComment) }))
6361
+ );
6362
+ const scored = bm25.score(query2, (id) => candidates.some((c) => c.id === id));
6363
+ scored.sort((a, b) => b.score - a.score);
6364
+ const qTokens = tokenise(query2);
6365
+ const results = scored.slice(0, limit).map(({ id, score }) => {
6366
+ const c = expectDefined(candidates.find((cand) => cand.id === id));
6367
+ return { ...c, score, snippet: bm25.extractSnippet(id, qTokens) };
6368
+ });
6369
+ return { results, total: candidates.length };
6370
+ }
5998
6371
  getAllIndexable() {
5999
6372
  return this.db.prepare("SELECT id, text FROM symbols").all().map(
6000
6373
  ({ id, text }) => ({ id, text })
@@ -6044,14 +6417,19 @@ var IndexStore = class {
6044
6417
  };
6045
6418
  }
6046
6419
  setLastIndexed(ts2) {
6047
- this.db.prepare(
6048
- "INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)"
6049
- ).run(String(ts2));
6420
+ this.runWithRetry(() => {
6421
+ this.db.prepare(
6422
+ "INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)"
6423
+ ).run(String(ts2));
6424
+ });
6050
6425
  }
6051
6426
  clearAll() {
6052
- this.db.exec("DELETE FROM symbols");
6053
- this.db.exec("DELETE FROM files");
6054
- this.db.exec("DELETE FROM refs");
6427
+ this.runWithRetry(() => {
6428
+ this.db.exec("DELETE FROM symbols");
6429
+ this.db.exec("DELETE FROM files");
6430
+ this.db.exec("DELETE FROM refs");
6431
+ if (this.ftsAvailable) this.db.exec("DELETE FROM symbols_fts");
6432
+ });
6055
6433
  }
6056
6434
  // ─── Ref CRUD ────────────────────────────────────────────────────────────────
6057
6435
  /**
@@ -6059,46 +6437,52 @@ var IndexStore = class {
6059
6437
  * Replaces any existing refs from the same source (idempotent on re-index).
6060
6438
  */
6061
6439
  insertRefs(fromId, refs) {
6062
- this.db.prepare("DELETE FROM refs WHERE from_id = ?").run(fromId);
6063
- if (refs.length === 0) return;
6064
- const stmt = this.db.prepare(
6065
- `INSERT INTO refs(from_id, to_name, to_id, call_type, line)
6066
- VALUES (?, ?, ?, ?, ?)`
6067
- );
6068
- for (const ref of refs) {
6069
- stmt.run(fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
6070
- }
6440
+ this.runWithRetry(() => {
6441
+ this.db.prepare("DELETE FROM refs WHERE from_id = ?").run(fromId);
6442
+ if (refs.length === 0) return;
6443
+ const stmt = this.db.prepare(
6444
+ `INSERT INTO refs(from_id, to_name, to_id, call_type, line)
6445
+ VALUES (?, ?, ?, ?, ?)`
6446
+ );
6447
+ for (const ref of refs) {
6448
+ stmt.run(fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
6449
+ }
6450
+ });
6071
6451
  }
6072
6452
  /**
6073
6453
  * Delete all refs whose source symbols are in a given file.
6074
6454
  * Used when re-indexing a file to clear stale refs.
6075
6455
  */
6076
6456
  deleteRefsForFile(file) {
6077
- const ids = this.db.prepare(
6078
- "SELECT id FROM symbols WHERE file = ?"
6079
- ).all(file);
6080
- if (!ids.length) return;
6081
- const placeholders = ids.map(() => "?").join(",");
6082
- this.db.prepare(`DELETE FROM refs WHERE from_id IN (${placeholders})`).run(...ids.map((r) => r.id));
6457
+ this.runWithRetry(() => {
6458
+ const ids = this.db.prepare(
6459
+ "SELECT id FROM symbols WHERE file = ?"
6460
+ ).all(file);
6461
+ if (!ids.length) return;
6462
+ const placeholders = ids.map(() => "?").join(",");
6463
+ this.db.prepare(`DELETE FROM refs WHERE from_id IN (${placeholders})`).run(...ids.map((r) => r.id));
6464
+ });
6083
6465
  }
6084
6466
  /**
6085
6467
  * Resolve `to_name` → `to_id` for all refs that have a name but no id.
6086
6468
  * Call this after all symbols have been inserted to fill in cross-references.
6087
6469
  */
6088
6470
  resolveRefs() {
6089
- const unresolved = this.db.prepare(
6090
- "SELECT id, to_name FROM refs WHERE to_id IS NULL AND to_name IS NOT NULL"
6091
- ).all();
6092
- let resolved = 0;
6093
- for (const row of unresolved) {
6094
- const target = this.db.prepare("SELECT id FROM symbols WHERE name = ? LIMIT 1").all(row.to_name);
6095
- const first = target[0];
6096
- if (first) {
6097
- this.db.prepare("UPDATE refs SET to_id = ? WHERE id = ?").run(first.id, row.id);
6098
- resolved++;
6471
+ return this.runWithRetry(() => {
6472
+ const unresolved = this.db.prepare(
6473
+ "SELECT id, to_name FROM refs WHERE to_id IS NULL AND to_name IS NOT NULL"
6474
+ ).all();
6475
+ let resolved = 0;
6476
+ for (const row of unresolved) {
6477
+ const target = this.db.prepare("SELECT id FROM symbols WHERE name = ? LIMIT 1").all(row.to_name);
6478
+ const first = target[0];
6479
+ if (first) {
6480
+ this.db.prepare("UPDATE refs SET to_id = ? WHERE id = ?").run(first.id, row.id);
6481
+ resolved++;
6482
+ }
6099
6483
  }
6100
- }
6101
- return resolved;
6484
+ return resolved;
6485
+ });
6102
6486
  }
6103
6487
  /**
6104
6488
  * Find all references TO a given symbol (who calls / uses this symbol?).
@@ -6859,7 +7243,7 @@ function parseSymbols4(opts) {
6859
7243
  }
6860
7244
  function checkNativeParser() {
6861
7245
  try {
6862
- execFileSync("rustc", ["--version"], { stdio: "pipe" });
7246
+ execFileSync("rustc", ["--version"], { stdio: "pipe", windowsHide: true });
6863
7247
  const toolsDir = path.join(process.cwd(), "tools");
6864
7248
  try {
6865
7249
  execFileSync(
@@ -6872,7 +7256,7 @@ function checkNativeParser() {
6872
7256
  "--manifest-path",
6873
7257
  path.join(toolsDir, "Cargo.toml")
6874
7258
  ],
6875
- { stdio: "pipe" }
7259
+ { stdio: "pipe", windowsHide: true }
6876
7260
  );
6877
7261
  return true;
6878
7262
  } catch {
@@ -6895,7 +7279,8 @@ function tryNativeParse(file, content) {
6895
7279
  cwd: process.cwd(),
6896
7280
  encoding: "utf8",
6897
7281
  timeout: 15e3,
6898
- stdio: ["pipe", "pipe", "pipe"]
7282
+ stdio: ["pipe", "pipe", "pipe"],
7283
+ windowsHide: true
6899
7284
  }
6900
7285
  );
6901
7286
  if (result.status === 0 && result.stdout) {
@@ -7309,10 +7694,6 @@ function isScalar(value) {
7309
7694
  if (/^'[^']*'$/.test(value) || /^"[^"]*"$/.test(value)) return true;
7310
7695
  return false;
7311
7696
  }
7312
- function truncate(s, max) {
7313
- if (s.length <= max) return s;
7314
- return s.slice(0, max) + "...";
7315
- }
7316
7697
  function makeSymbol2(opts) {
7317
7698
  return {
7318
7699
  id: 0,
@@ -7379,140 +7760,20 @@ async function loadGitignoreMatcher(projectRoot) {
7379
7760
  return compileGitignore(lines);
7380
7761
  }
7381
7762
 
7382
- // src/codebase-index/background-indexer.ts
7383
- var _ready = false;
7384
- var _indexing = false;
7385
- var _currentFile = 0;
7386
- var _totalFiles = 0;
7387
- var _lastError = null;
7388
- function isIndexReady() {
7389
- return _ready;
7390
- }
7391
- function setIndexReady() {
7392
- _ready = true;
7393
- }
7394
- function isIndexing() {
7395
- return _indexing;
7396
- }
7397
- function getIndexState() {
7398
- return {
7399
- ready: _ready,
7400
- indexing: _indexing,
7401
- currentFile: _currentFile,
7402
- totalFiles: _totalFiles,
7403
- lastError: _lastError
7404
- };
7405
- }
7406
- var _listeners = [];
7407
- function onIndexStateChange(listener) {
7408
- _listeners.push(listener);
7409
- return () => {
7410
- _listeners = _listeners.filter((l) => l !== listener);
7411
- };
7763
+ // src/codebase-index/indexer.ts
7764
+ var YIELD_EVERY_N = 50;
7765
+ function yieldEventLoop() {
7766
+ return new Promise((resolve7) => setImmediate(resolve7));
7412
7767
  }
7413
- function emitState() {
7414
- const state = getIndexState();
7415
- for (const l of _listeners) l(state);
7768
+ function throwIfAborted(signal) {
7769
+ if (!signal?.aborted) return;
7770
+ if (signal.reason instanceof Error) throw signal.reason;
7771
+ throw new Error(
7772
+ typeof signal.reason === "string" ? signal.reason : "Indexing cancelled"
7773
+ );
7416
7774
  }
7417
- function _setIndexProgress(current, total) {
7418
- _currentFile = current;
7419
- _totalFiles = total;
7420
- emitState();
7421
- }
7422
- function stubCtx(projectRoot) {
7423
- return {
7424
- projectRoot,
7425
- cwd: projectRoot,
7426
- messages: [],
7427
- todos: [],
7428
- readFiles: /* @__PURE__ */ new Set(),
7429
- fileMtimes: /* @__PURE__ */ new Map()
7430
- };
7431
- }
7432
- var chain = Promise.resolve();
7433
- function withMutex(job) {
7434
- const run = chain.then(job, job);
7435
- chain = run.then(
7436
- () => void 0,
7437
- () => void 0
7438
- );
7439
- return run;
7440
- }
7441
- var DEFAULT_DEBOUNCE_MS = 400;
7442
- var debounceTimers = /* @__PURE__ */ new Map();
7443
- function debounceKey(indexDir, file) {
7444
- return `${indexDir ?? ""}|${file}`;
7445
- }
7446
- function isIndexableFile(filePath) {
7447
- return detectLang(filePath) !== null;
7448
- }
7449
- async function runStartupIndex(opts) {
7450
- _indexing = true;
7451
- emitState();
7452
- try {
7453
- const result = await withMutex(() => {
7454
- _currentFile = 0;
7455
- _totalFiles = 0;
7456
- _lastError = null;
7457
- return runIndexer(stubCtx(opts.projectRoot), {
7458
- projectRoot: opts.projectRoot,
7459
- indexDir: opts.indexDir,
7460
- force: opts.force,
7461
- signal: opts.signal
7462
- });
7463
- });
7464
- _ready = true;
7465
- return result;
7466
- } catch (err) {
7467
- _lastError = err instanceof Error ? err.message : String(err);
7468
- _ready = true;
7469
- throw err;
7470
- } finally {
7471
- _indexing = false;
7472
- emitState();
7473
- }
7474
- }
7475
- function enqueueReindex(opts) {
7476
- const files = opts.files.filter(isIndexableFile);
7477
- if (files.length === 0) return;
7478
- const ms = opts.debounceMs ?? DEFAULT_DEBOUNCE_MS;
7479
- for (const file of files) {
7480
- const key = debounceKey(opts.indexDir, file);
7481
- const existing = debounceTimers.get(key);
7482
- if (existing) clearTimeout(existing);
7483
- const timer = setTimeout(() => {
7484
- debounceTimers.delete(key);
7485
- void withMutex(
7486
- () => runIndexer(stubCtx(opts.projectRoot), {
7487
- projectRoot: opts.projectRoot,
7488
- files: [file],
7489
- indexDir: opts.indexDir
7490
- })
7491
- ).catch((err) => opts.onError?.(err));
7492
- }, ms);
7493
- timer.unref?.();
7494
- debounceTimers.set(key, timer);
7495
- }
7496
- }
7497
- function cancelPendingReindexes() {
7498
- for (const t of debounceTimers.values()) clearTimeout(t);
7499
- debounceTimers.clear();
7500
- }
7501
-
7502
- // src/codebase-index/indexer.ts
7503
- var YIELD_EVERY_N = 50;
7504
- function yieldEventLoop() {
7505
- return new Promise((resolve7) => setImmediate(resolve7));
7506
- }
7507
- function throwIfAborted(signal) {
7508
- if (!signal?.aborted) return;
7509
- if (signal.reason instanceof Error) throw signal.reason;
7510
- throw new Error(
7511
- typeof signal.reason === "string" ? signal.reason : "Indexing cancelled"
7512
- );
7513
- }
7514
- function isAbortError(err) {
7515
- return err instanceof DOMException && err.name === "AbortError";
7775
+ function isAbortError(err) {
7776
+ return err instanceof DOMException && err.name === "AbortError";
7516
7777
  }
7517
7778
  var DEFAULT_IGNORE5 = [
7518
7779
  "node_modules",
@@ -7636,7 +7897,7 @@ async function runIndexerWithStore(store, opts) {
7636
7897
  }
7637
7898
  for (let fi = 0; fi < files.length; fi++) {
7638
7899
  const file = expectDefined(files[fi]);
7639
- _setIndexProgress(fi + 1, files.length);
7900
+ opts.onProgress?.(fi + 1, files.length);
7640
7901
  if (fi > 0 && fi % YIELD_EVERY_N === 0) {
7641
7902
  await yieldEventLoop();
7642
7903
  throwIfAborted(signal);
@@ -7731,6 +7992,349 @@ async function runIndexerWithStore(store, opts) {
7731
7992
  };
7732
7993
  }
7733
7994
 
7995
+ // src/codebase-index/index-service.ts
7996
+ function stubCtx(projectRoot) {
7997
+ return {
7998
+ projectRoot,
7999
+ cwd: projectRoot,
8000
+ messages: [],
8001
+ todos: [],
8002
+ readFiles: /* @__PURE__ */ new Set(),
8003
+ fileMtimes: /* @__PURE__ */ new Map()
8004
+ };
8005
+ }
8006
+ async function indexService(args, hooks = {}) {
8007
+ return runIndexer(stubCtx(args.projectRoot), {
8008
+ projectRoot: args.projectRoot,
8009
+ indexDir: args.indexDir,
8010
+ files: args.files,
8011
+ force: args.force,
8012
+ langs: args.langs,
8013
+ ignore: args.ignore,
8014
+ signal: hooks.signal,
8015
+ onProgress: hooks.onProgress
8016
+ });
8017
+ }
8018
+ function searchService(args) {
8019
+ const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
8020
+ try {
8021
+ return store.searchRanked(
8022
+ args.query,
8023
+ {
8024
+ kind: args.kind,
8025
+ lang: args.lang,
8026
+ file: args.file,
8027
+ lspKind: args.lspKind
8028
+ },
8029
+ args.limit
8030
+ );
8031
+ } finally {
8032
+ store.close();
8033
+ }
8034
+ }
8035
+ function statsService(args) {
8036
+ const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
8037
+ try {
8038
+ return store.getStats();
8039
+ } finally {
8040
+ store.close();
8041
+ }
8042
+ }
8043
+
8044
+ // src/codebase-index/background-indexer.ts
8045
+ var DEFAULT_FULL_INDEX_TIMEOUT_MS = 12e4;
8046
+ var DEFAULT_INCREMENTAL_TIMEOUT_MS = 3e4;
8047
+ var DEFAULT_QUERY_TIMEOUT_MS = 8e3;
8048
+ var _ready = false;
8049
+ var _indexing = false;
8050
+ var _currentFile = 0;
8051
+ var _totalFiles = 0;
8052
+ var _lastError = null;
8053
+ function isIndexReady() {
8054
+ return _ready;
8055
+ }
8056
+ function isIndexing() {
8057
+ return _indexing;
8058
+ }
8059
+ function getIndexState() {
8060
+ return {
8061
+ ready: _ready,
8062
+ indexing: _indexing,
8063
+ currentFile: _currentFile,
8064
+ totalFiles: _totalFiles,
8065
+ lastError: _lastError,
8066
+ circuit: indexCircuitBreaker.snapshot()
8067
+ };
8068
+ }
8069
+ var _listeners = [];
8070
+ function onIndexStateChange(listener) {
8071
+ _listeners.push(listener);
8072
+ return () => {
8073
+ _listeners = _listeners.filter((l) => l !== listener);
8074
+ };
8075
+ }
8076
+ function emitState() {
8077
+ const state = getIndexState();
8078
+ for (const l of _listeners) l(state);
8079
+ }
8080
+ function setIndexProgress(current, total) {
8081
+ _currentFile = current;
8082
+ _totalFiles = total;
8083
+ emitState();
8084
+ }
8085
+ var worker = null;
8086
+ var workerUnavailable = false;
8087
+ var nextRpcId = 1;
8088
+ var pending = /* @__PURE__ */ new Map();
8089
+ function resolveWorkerUrl() {
8090
+ if (process.env["WRONGSTACK_INDEX_INLINE"]) return null;
8091
+ for (const rel of ["./worker.js", "./codebase-index/worker.js"]) {
8092
+ try {
8093
+ const url = new URL(rel, import.meta.url);
8094
+ if (url.protocol === "file:" && fs7.existsSync(fileURLToPath(url))) return url;
8095
+ } catch {
8096
+ }
8097
+ }
8098
+ return null;
8099
+ }
8100
+ function failAllPending(err) {
8101
+ const entries = [...pending.values()];
8102
+ pending.clear();
8103
+ for (const p of entries) p.reject(err);
8104
+ }
8105
+ function ensureWorker() {
8106
+ if (worker) return worker;
8107
+ if (workerUnavailable) return null;
8108
+ const url = resolveWorkerUrl();
8109
+ if (!url) {
8110
+ workerUnavailable = true;
8111
+ return null;
8112
+ }
8113
+ try {
8114
+ const w = new Worker(url, { name: "wstack-codebase-index" });
8115
+ w.unref();
8116
+ w.on("message", (msg) => {
8117
+ if (msg.type === "progress") {
8118
+ pending.get(msg.id)?.onProgress?.(msg.current, msg.total);
8119
+ return;
8120
+ }
8121
+ const entry = pending.get(msg.id);
8122
+ if (!entry) return;
8123
+ pending.delete(msg.id);
8124
+ if (msg.ok) entry.resolve(msg.result);
8125
+ else entry.reject(new Error(msg.error));
8126
+ });
8127
+ w.on("error", (err) => {
8128
+ worker = null;
8129
+ failAllPending(err);
8130
+ });
8131
+ w.on("exit", () => {
8132
+ if (worker === w) worker = null;
8133
+ failAllPending(new Error("codebase-index worker exited"));
8134
+ });
8135
+ worker = w;
8136
+ return w;
8137
+ } catch {
8138
+ workerUnavailable = true;
8139
+ return null;
8140
+ }
8141
+ }
8142
+ function terminateWorker(reason) {
8143
+ const w = worker;
8144
+ worker = null;
8145
+ failAllPending(reason);
8146
+ if (w) void w.terminate().catch(() => {
8147
+ });
8148
+ }
8149
+ function shutdownCodebaseIndexHost() {
8150
+ cancelPendingReindexes();
8151
+ terminateWorker(new Error("codebase-index host shut down"));
8152
+ workerUnavailable = false;
8153
+ }
8154
+ function callIndexOp(op, args, opts) {
8155
+ const w = ensureWorker();
8156
+ if (!w) return callInline(op, args, opts);
8157
+ return new Promise((resolve7, reject) => {
8158
+ const id = nextRpcId++;
8159
+ const timer = setTimeout(() => {
8160
+ pending.delete(id);
8161
+ const err = new IndexTimeoutError(
8162
+ `Index ${op} exceeded its ${opts.timeoutMs}ms watchdog timeout`
8163
+ );
8164
+ terminateWorker(err);
8165
+ reject(err);
8166
+ }, opts.timeoutMs);
8167
+ timer.unref?.();
8168
+ const onAbort = () => {
8169
+ w.postMessage({ type: "cancel", id });
8170
+ };
8171
+ if (opts.signal?.aborted) onAbort();
8172
+ else opts.signal?.addEventListener("abort", onAbort, { once: true });
8173
+ const cleanup = () => {
8174
+ clearTimeout(timer);
8175
+ opts.signal?.removeEventListener("abort", onAbort);
8176
+ };
8177
+ pending.set(id, {
8178
+ resolve: (v) => {
8179
+ cleanup();
8180
+ resolve7(v);
8181
+ },
8182
+ reject: (e) => {
8183
+ cleanup();
8184
+ reject(e);
8185
+ },
8186
+ onProgress: opts.onProgress
8187
+ });
8188
+ w.postMessage({ type: "request", id, op, args });
8189
+ });
8190
+ }
8191
+ async function callInline(op, args, opts) {
8192
+ const ac = new AbortController();
8193
+ const onOuterAbort = () => ac.abort(opts.signal?.reason ?? new Error("Indexing cancelled"));
8194
+ if (opts.signal?.aborted) onOuterAbort();
8195
+ else opts.signal?.addEventListener("abort", onOuterAbort, { once: true });
8196
+ let timer;
8197
+ const watchdog = new Promise((_, reject) => {
8198
+ timer = setTimeout(() => {
8199
+ const err = new IndexTimeoutError(
8200
+ `Index ${op} exceeded its ${opts.timeoutMs}ms watchdog timeout`
8201
+ );
8202
+ ac.abort(err);
8203
+ reject(err);
8204
+ }, opts.timeoutMs);
8205
+ timer.unref?.();
8206
+ });
8207
+ const job = async () => {
8208
+ switch (op) {
8209
+ case "index":
8210
+ return await indexService(args, {
8211
+ signal: ac.signal,
8212
+ onProgress: opts.onProgress
8213
+ });
8214
+ case "search":
8215
+ return searchService(args);
8216
+ case "stats":
8217
+ return statsService(args);
8218
+ default:
8219
+ throw new Error(`unknown index op: ${String(op)}`);
8220
+ }
8221
+ };
8222
+ try {
8223
+ return await Promise.race([job(), watchdog]);
8224
+ } finally {
8225
+ if (timer) clearTimeout(timer);
8226
+ opts.signal?.removeEventListener("abort", onOuterAbort);
8227
+ }
8228
+ }
8229
+ var chain = Promise.resolve();
8230
+ function withMutex(job) {
8231
+ const run = chain.then(job, job);
8232
+ chain = run.then(
8233
+ () => void 0,
8234
+ () => void 0
8235
+ );
8236
+ return run;
8237
+ }
8238
+ function circuitOpenError() {
8239
+ const c = indexCircuitBreaker.snapshot();
8240
+ return new CircuitOpenError(
8241
+ "Codebase indexing is temporarily paused after repeated failures" + (c.lastFailure ? ` (last: ${c.lastFailure})` : "") + (c.cooldownRemainingMs > 0 ? `; auto-retry in ${Math.ceil(c.cooldownRemainingMs / 1e3)}s` : "") + ". Use /codebase-reindex to retry now."
8242
+ );
8243
+ }
8244
+ var DEFAULT_DEBOUNCE_MS = 400;
8245
+ var debounceTimers = /* @__PURE__ */ new Map();
8246
+ function debounceKey(indexDir, file) {
8247
+ return `${indexDir ?? ""}|${file}`;
8248
+ }
8249
+ function isIndexableFile(filePath) {
8250
+ return detectLang(filePath) !== null;
8251
+ }
8252
+ async function runStartupIndex(opts) {
8253
+ if (!indexCircuitBreaker.allowRequest()) throw circuitOpenError();
8254
+ _indexing = true;
8255
+ emitState();
8256
+ try {
8257
+ const result = await withMutex(() => {
8258
+ _currentFile = 0;
8259
+ _totalFiles = 0;
8260
+ _lastError = null;
8261
+ return callIndexOp(
8262
+ "index",
8263
+ {
8264
+ projectRoot: opts.projectRoot,
8265
+ indexDir: opts.indexDir,
8266
+ force: opts.force,
8267
+ langs: opts.langs
8268
+ },
8269
+ {
8270
+ timeoutMs: opts.timeoutMs ?? DEFAULT_FULL_INDEX_TIMEOUT_MS,
8271
+ signal: opts.signal,
8272
+ onProgress: setIndexProgress
8273
+ }
8274
+ );
8275
+ });
8276
+ _ready = true;
8277
+ indexCircuitBreaker.recordSuccess();
8278
+ return result;
8279
+ } catch (err) {
8280
+ _lastError = err instanceof Error ? err.message : String(err);
8281
+ _ready = true;
8282
+ if (!opts.signal?.aborted) indexCircuitBreaker.recordFailure(err);
8283
+ throw err;
8284
+ } finally {
8285
+ _indexing = false;
8286
+ emitState();
8287
+ }
8288
+ }
8289
+ function enqueueReindex(opts) {
8290
+ const files = opts.files.filter(isIndexableFile);
8291
+ if (files.length === 0) return;
8292
+ const ms = opts.debounceMs ?? DEFAULT_DEBOUNCE_MS;
8293
+ for (const file of files) {
8294
+ const key = debounceKey(opts.indexDir, file);
8295
+ const existing = debounceTimers.get(key);
8296
+ if (existing) clearTimeout(existing);
8297
+ const timer = setTimeout(() => {
8298
+ debounceTimers.delete(key);
8299
+ if (!indexCircuitBreaker.allowRequest()) {
8300
+ opts.onError?.(circuitOpenError());
8301
+ return;
8302
+ }
8303
+ void withMutex(
8304
+ () => callIndexOp(
8305
+ "index",
8306
+ { projectRoot: opts.projectRoot, files: [file], indexDir: opts.indexDir },
8307
+ { timeoutMs: opts.timeoutMs ?? DEFAULT_INCREMENTAL_TIMEOUT_MS }
8308
+ )
8309
+ ).then(
8310
+ () => indexCircuitBreaker.recordSuccess(),
8311
+ (err) => {
8312
+ indexCircuitBreaker.recordFailure(err);
8313
+ opts.onError?.(err);
8314
+ }
8315
+ );
8316
+ }, ms);
8317
+ timer.unref?.();
8318
+ debounceTimers.set(key, timer);
8319
+ }
8320
+ }
8321
+ function cancelPendingReindexes() {
8322
+ for (const t of debounceTimers.values()) clearTimeout(t);
8323
+ debounceTimers.clear();
8324
+ }
8325
+ async function searchCodebaseIndex(args, opts = {}) {
8326
+ return callIndexOp("search", args, {
8327
+ timeoutMs: opts.timeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS,
8328
+ signal: opts.signal
8329
+ });
8330
+ }
8331
+ async function codebaseIndexStats(args, opts = {}) {
8332
+ return callIndexOp("stats", args, {
8333
+ timeoutMs: opts.timeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS,
8334
+ signal: opts.signal
8335
+ });
8336
+ }
8337
+
7734
8338
  // src/codebase-index/codebase-index-tool.ts
7735
8339
  var codebaseIndexTool = {
7736
8340
  name: "codebase-index",
@@ -7766,103 +8370,24 @@ var codebaseIndexTool = {
7766
8370
  note: "A full index is already in progress. Retry codebase-index after it completes (check codebase-stats)."
7767
8371
  };
7768
8372
  }
7769
- const result = await runIndexer(ctx, {
8373
+ const circuit = indexCircuitBreaker.snapshot();
8374
+ if (circuit.state === "open" && circuit.cooldownRemainingMs > 0) {
8375
+ return {
8376
+ filesIndexed: 0,
8377
+ symbolsIndexed: 0,
8378
+ langStats: {},
8379
+ durationMs: 0,
8380
+ errors: [],
8381
+ note: `Codebase indexing is paused after repeated failures (last: ${circuit.lastFailure ?? "unknown"}). Auto-retry possible in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s; the user can run /codebase-reindex to retry immediately.`
8382
+ };
8383
+ }
8384
+ return await runStartupIndex({
7770
8385
  projectRoot: ctx.projectRoot,
7771
8386
  force: input.force ?? false,
7772
8387
  langs: input.langs,
7773
8388
  indexDir: codebaseIndexDirOverride(ctx),
7774
8389
  signal: execOpts?.signal
7775
8390
  });
7776
- setIndexReady();
7777
- return result;
7778
- }
7779
- };
7780
-
7781
- // src/codebase-index/bm25.ts
7782
- var K1 = 1.5;
7783
- var B = 0.75;
7784
- function tokenise(text) {
7785
- const sanitised = text.replace(/[^\p{L}\p{N}$'_]/gu, " ").replace(/_/g, " ");
7786
- return sanitised.toLowerCase().split(" ").filter(Boolean);
7787
- }
7788
- function splitName(name) {
7789
- return name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
7790
- }
7791
- function buildIndexableText(name, signature, docComment) {
7792
- return [splitName(name), name, signature, docComment].filter(Boolean).join(" ");
7793
- }
7794
- function buildBm25Index(docs) {
7795
- const documents = docs.map((d) => {
7796
- const tokens = tokenise(d.text);
7797
- return { id: d.id, tokens, raw: d.text, len: tokens.length };
7798
- });
7799
- const df = {};
7800
- for (const doc of documents) {
7801
- const seen = /* @__PURE__ */ new Set();
7802
- for (const t of doc.tokens) {
7803
- if (!seen.has(t)) {
7804
- df[t] = (df[t] ?? 0) + 1;
7805
- seen.add(t);
7806
- }
7807
- }
7808
- }
7809
- const N = documents.length;
7810
- const totalLen = documents.reduce((sum, d) => sum + d.len, 0);
7811
- const avgLen = N === 0 ? 0 : totalLen / N;
7812
- return new Bm25Index(documents, df, N, avgLen);
7813
- }
7814
- var Bm25Index = class {
7815
- constructor(documents, df, N, avgLen) {
7816
- this.documents = documents;
7817
- this.df = df;
7818
- this.N = N;
7819
- this.safeAvgLen = avgLen === 0 ? 1 : avgLen;
7820
- }
7821
- documents;
7822
- df;
7823
- N;
7824
- safeAvgLen;
7825
- score(query2, filter) {
7826
- const qTokens = tokenise(query2);
7827
- if (qTokens.length === 0) return [];
7828
- const results = [];
7829
- for (const doc of this.documents) {
7830
- if (filter && !filter(doc.id)) continue;
7831
- let docScore = 0;
7832
- for (const qTerm of qTokens) {
7833
- let tf = 0;
7834
- for (const t of doc.tokens) {
7835
- if (t === qTerm) tf++;
7836
- }
7837
- if (tf === 0) continue;
7838
- const dfVal = this.df[qTerm] ?? 0;
7839
- if (dfVal === 0) continue;
7840
- const idf = Math.log((this.N - dfVal + 0.5) / (dfVal + 0.5) + 1);
7841
- const lenRatio = B * (doc.len / this.safeAvgLen);
7842
- const tfComponent = tf * (K1 + 1) / (tf + K1 * (1 - B + lenRatio));
7843
- docScore += idf * tfComponent;
7844
- }
7845
- if (docScore > 0) results.push({ id: doc.id, score: docScore });
7846
- }
7847
- return results;
7848
- }
7849
- getDoc(id) {
7850
- return this.documents.find((d) => d.id === id);
7851
- }
7852
- extractSnippet(docId, queryTokens, radius = 40) {
7853
- const doc = this.getDoc(docId);
7854
- if (!doc) return "";
7855
- for (const tok of queryTokens) {
7856
- const idx = doc.raw.toLowerCase().indexOf(tok);
7857
- if (idx !== -1) {
7858
- const start = Math.max(0, idx - radius);
7859
- const end = Math.min(doc.raw.length, idx + tok.length + radius);
7860
- const excerpt = doc.raw.slice(start, end);
7861
- const ellipsis = "\u2026";
7862
- return (start > 0 ? ellipsis : "") + excerpt + (end < doc.raw.length ? ellipsis : "");
7863
- }
7864
- }
7865
- return doc.raw.slice(0, radius * 2) + (doc.raw.length > radius * 2 ? "\u2026" : "");
7866
8391
  }
7867
8392
  };
7868
8393
 
@@ -7908,7 +8433,7 @@ var codebaseSearchTool = {
7908
8433
  },
7909
8434
  required: ["query"]
7910
8435
  },
7911
- async execute(input, ctx) {
8436
+ async execute(input, ctx, execOpts) {
7912
8437
  const state = getIndexState();
7913
8438
  if (!state.ready) {
7914
8439
  return {
@@ -7927,51 +8452,30 @@ var codebaseSearchTool = {
7927
8452
  };
7928
8453
  }
7929
8454
  if (state.lastError) {
8455
+ const circuit = state.circuit;
8456
+ const retryHint = circuit.state === "open" ? `Indexing is paused (circuit open, retry in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s); the user can run /codebase-reindex to retry now.` : "Try /codebase-reindex.";
7930
8457
  return {
7931
8458
  results: [],
7932
8459
  total: 0,
7933
8460
  query: input.query,
7934
- indexStatus: `Index build failed: ${state.lastError}. Try /codebase-reindex.`
8461
+ indexStatus: `Index build failed: ${state.lastError}. ${retryHint}`
7935
8462
  };
7936
8463
  }
7937
- const store = new IndexStore(ctx.projectRoot, { indexDir: codebaseIndexDirOverride(ctx) });
7938
- try {
7939
- const limit = Math.min(input.limit ?? 20, 100);
7940
- const candidates = store.search(input.query, {
8464
+ const limit = Math.min(input.limit ?? 20, 100);
8465
+ const { results, total } = await searchCodebaseIndex(
8466
+ {
8467
+ projectRoot: ctx.projectRoot,
8468
+ indexDir: codebaseIndexDirOverride(ctx),
8469
+ query: input.query,
7941
8470
  kind: input.kind,
7942
8471
  lang: input.lang,
7943
8472
  file: input.file,
7944
- lspKind: input.lspKind
7945
- });
7946
- if (candidates.length === 0) {
7947
- return { results: [], total: 0, query: input.query };
7948
- }
7949
- const indexable = candidates.map((c) => ({
7950
- id: c.id,
7951
- text: buildIndexableText(c.name, c.signature, c.docComment)
7952
- }));
7953
- const bm25 = buildBm25Index(indexable);
7954
- const scored = bm25.score(input.query, (id) => candidates.some((c) => c.id === id));
7955
- scored.sort((a, b) => b.score - a.score);
7956
- const top = scored.slice(0, limit);
7957
- const qTokens = tokenise(input.query);
7958
- const results = top.map(({ id, score }) => {
7959
- const c = expectDefined(candidates.find((c2) => c2.id === id));
7960
- const snippet = bm25.extractSnippet(id, qTokens);
7961
- return {
7962
- ...c,
7963
- score,
7964
- snippet
7965
- };
7966
- });
7967
- return {
7968
- results,
7969
- total: candidates.length,
7970
- query: input.query
7971
- };
7972
- } finally {
7973
- store.close();
7974
- }
8473
+ lspKind: input.lspKind,
8474
+ limit
8475
+ },
8476
+ { signal: execOpts?.signal }
8477
+ );
8478
+ return { results, total, query: input.query };
7975
8479
  }
7976
8480
  };
7977
8481
 
@@ -7990,7 +8494,7 @@ var codebaseStatsTool = {
7990
8494
  properties: {},
7991
8495
  additionalProperties: false
7992
8496
  },
7993
- async execute(_input, ctx) {
8497
+ async execute(_input, ctx, execOpts) {
7994
8498
  const idxState = getIndexState();
7995
8499
  if (!idxState.ready) {
7996
8500
  return {
@@ -8005,34 +8509,30 @@ var codebaseStatsTool = {
8005
8509
  indexStatus: idxState.indexing ? `Indexing in progress (${idxState.currentFile}/${idxState.totalFiles} files).` : "Index not yet built."
8006
8510
  };
8007
8511
  }
8512
+ const stats = await codebaseIndexStats(
8513
+ { projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
8514
+ { signal: execOpts?.signal }
8515
+ );
8008
8516
  if (idxState.indexing) {
8009
- const store2 = new IndexStore(ctx.projectRoot, { indexDir: codebaseIndexDirOverride(ctx) });
8010
- try {
8011
- const stats = store2.getStats();
8012
- return {
8013
- ...stats,
8014
- indexStatus: `Index refresh in progress (${idxState.currentFile}/${idxState.totalFiles} files). Stats may be incomplete.`
8015
- };
8016
- } finally {
8017
- store2.close();
8018
- }
8019
- }
8020
- const store = new IndexStore(ctx.projectRoot, { indexDir: codebaseIndexDirOverride(ctx) });
8021
- try {
8022
- const stats = store.getStats();
8023
8517
  return {
8024
- totalSymbols: stats.totalSymbols,
8025
- totalFiles: stats.totalFiles,
8026
- byLang: stats.byLang,
8027
- byKind: stats.byKind,
8028
- lastIndexed: stats.lastIndexed,
8029
- sizeBytes: stats.sizeBytes,
8030
- indexPath: stats.indexPath,
8031
- version: stats.version
8518
+ ...stats,
8519
+ indexStatus: `Index refresh in progress (${idxState.currentFile}/${idxState.totalFiles} files). Stats may be incomplete.`
8032
8520
  };
8033
- } finally {
8034
- store.close();
8035
8521
  }
8522
+ const circuit = idxState.circuit;
8523
+ return {
8524
+ totalSymbols: stats.totalSymbols,
8525
+ totalFiles: stats.totalFiles,
8526
+ byLang: stats.byLang,
8527
+ byKind: stats.byKind,
8528
+ lastIndexed: stats.lastIndexed,
8529
+ sizeBytes: stats.sizeBytes,
8530
+ indexPath: stats.indexPath,
8531
+ version: stats.version,
8532
+ ...circuit.state === "open" ? {
8533
+ indexStatus: `Indexing is paused after repeated failures (last: ${circuit.lastFailure ?? "unknown"}); auto-retry in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s, or run /codebase-reindex. Stats reflect the last successful build.`
8534
+ } : {}
8535
+ };
8036
8536
  }
8037
8537
  };
8038
8538
  var setWorkingDirTool = {
@@ -8430,6 +8930,6 @@ var builtinToolsPack = {
8430
8930
  tools: builtinTools
8431
8931
  };
8432
8932
 
8433
- export { CircuitBreaker, _resetProcessRegistry, auditTool, bashTool, batchToolUseTool, builtinTools, builtinToolsPack, cancelPendingReindexes, codebaseIndexTool, codebaseSearchTool, codebaseStatsTool, createModeTool, diffTool, documentTool, editTool, enqueueReindex, execTool, fetchTool, forgetTool, formatTool, getIndexState, getProcessRegistry, gitTool, globTool, grepTool, installTool, isIndexReady, isIndexableFile, isIndexing, jsonTool, lintTool, logsTool, onIndexStateChange, outdatedTool, patchTool, planTool, readTool, relatedMemoryTool, rememberTool, replaceTool, runStartupIndex, scaffoldTool, searchMemoryTool, searchTool, testTool, todoTool, toolHelpTool, toolSearchTool, toolUseTool, treeTool, typecheckTool, writeTool };
8933
+ export { CircuitBreaker, CircuitOpenError, IndexCircuitBreaker, IndexTimeoutError, _resetProcessRegistry, auditTool, bashTool, batchToolUseTool, builtinTools, builtinToolsPack, cancelPendingReindexes, codebaseIndexStats, codebaseIndexTool, codebaseSearchTool, codebaseStatsTool, createModeTool, diffTool, documentTool, editTool, enqueueReindex, execTool, fetchTool, forgetTool, formatTool, getIndexState, getProcessRegistry, gitTool, globTool, grepTool, indexCircuitBreaker, installTool, isIndexReady, isIndexableFile, isIndexing, jsonTool, lintTool, logsTool, onIndexStateChange, outdatedTool, patchTool, planTool, readTool, relatedMemoryTool, rememberTool, replaceTool, resetIndexCircuitBreaker, runStartupIndex, scaffoldTool, searchCodebaseIndex, searchMemoryTool, searchTool, shutdownCodebaseIndexHost, testTool, todoTool, toolHelpTool, toolSearchTool, toolUseTool, treeTool, typecheckTool, writeTool };
8434
8934
  //# sourceMappingURL=index.js.map
8435
8935
  //# sourceMappingURL=index.js.map