opencode-codebase-index 0.13.2 → 0.15.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/cli.cjs CHANGED
@@ -496,7 +496,7 @@ var require_ignore = __commonJS({
496
496
  // path matching.
497
497
  // - check `string` either `MODE_IGNORE` or `MODE_CHECK_IGNORE`
498
498
  // @returns {TestResult} true if a file is ignored
499
- test(path25, checkUnignored, mode) {
499
+ test(path27, checkUnignored, mode) {
500
500
  let ignored = false;
501
501
  let unignored = false;
502
502
  let matchedRule;
@@ -505,7 +505,7 @@ var require_ignore = __commonJS({
505
505
  if (unignored === negative && ignored !== unignored || negative && !ignored && !unignored && !checkUnignored) {
506
506
  return;
507
507
  }
508
- const matched = rule[mode].test(path25);
508
+ const matched = rule[mode].test(path27);
509
509
  if (!matched) {
510
510
  return;
511
511
  }
@@ -526,17 +526,17 @@ var require_ignore = __commonJS({
526
526
  var throwError = (message, Ctor) => {
527
527
  throw new Ctor(message);
528
528
  };
529
- var checkPath = (path25, originalPath, doThrow) => {
530
- if (!isString(path25)) {
529
+ var checkPath = (path27, originalPath, doThrow) => {
530
+ if (!isString(path27)) {
531
531
  return doThrow(
532
532
  `path must be a string, but got \`${originalPath}\``,
533
533
  TypeError
534
534
  );
535
535
  }
536
- if (!path25) {
536
+ if (!path27) {
537
537
  return doThrow(`path must not be empty`, TypeError);
538
538
  }
539
- if (checkPath.isNotRelative(path25)) {
539
+ if (checkPath.isNotRelative(path27)) {
540
540
  const r = "`path.relative()`d";
541
541
  return doThrow(
542
542
  `path should be a ${r} string, but got "${originalPath}"`,
@@ -545,7 +545,7 @@ var require_ignore = __commonJS({
545
545
  }
546
546
  return true;
547
547
  };
548
- var isNotRelative = (path25) => REGEX_TEST_INVALID_PATH.test(path25);
548
+ var isNotRelative = (path27) => REGEX_TEST_INVALID_PATH.test(path27);
549
549
  checkPath.isNotRelative = isNotRelative;
550
550
  checkPath.convert = (p) => p;
551
551
  var Ignore2 = class {
@@ -575,19 +575,19 @@ var require_ignore = __commonJS({
575
575
  }
576
576
  // @returns {TestResult}
577
577
  _test(originalPath, cache, checkUnignored, slices) {
578
- const path25 = originalPath && checkPath.convert(originalPath);
578
+ const path27 = originalPath && checkPath.convert(originalPath);
579
579
  checkPath(
580
- path25,
580
+ path27,
581
581
  originalPath,
582
582
  this._strictPathCheck ? throwError : RETURN_FALSE
583
583
  );
584
- return this._t(path25, cache, checkUnignored, slices);
584
+ return this._t(path27, cache, checkUnignored, slices);
585
585
  }
586
- checkIgnore(path25) {
587
- if (!REGEX_TEST_TRAILING_SLASH.test(path25)) {
588
- return this.test(path25);
586
+ checkIgnore(path27) {
587
+ if (!REGEX_TEST_TRAILING_SLASH.test(path27)) {
588
+ return this.test(path27);
589
589
  }
590
- const slices = path25.split(SLASH2).filter(Boolean);
590
+ const slices = path27.split(SLASH2).filter(Boolean);
591
591
  slices.pop();
592
592
  if (slices.length) {
593
593
  const parent = this._t(
@@ -600,18 +600,18 @@ var require_ignore = __commonJS({
600
600
  return parent;
601
601
  }
602
602
  }
603
- return this._rules.test(path25, false, MODE_CHECK_IGNORE);
603
+ return this._rules.test(path27, false, MODE_CHECK_IGNORE);
604
604
  }
605
- _t(path25, cache, checkUnignored, slices) {
606
- if (path25 in cache) {
607
- return cache[path25];
605
+ _t(path27, cache, checkUnignored, slices) {
606
+ if (path27 in cache) {
607
+ return cache[path27];
608
608
  }
609
609
  if (!slices) {
610
- slices = path25.split(SLASH2).filter(Boolean);
610
+ slices = path27.split(SLASH2).filter(Boolean);
611
611
  }
612
612
  slices.pop();
613
613
  if (!slices.length) {
614
- return cache[path25] = this._rules.test(path25, checkUnignored, MODE_IGNORE);
614
+ return cache[path27] = this._rules.test(path27, checkUnignored, MODE_IGNORE);
615
615
  }
616
616
  const parent = this._t(
617
617
  slices.join(SLASH2) + SLASH2,
@@ -619,29 +619,29 @@ var require_ignore = __commonJS({
619
619
  checkUnignored,
620
620
  slices
621
621
  );
622
- return cache[path25] = parent.ignored ? parent : this._rules.test(path25, checkUnignored, MODE_IGNORE);
622
+ return cache[path27] = parent.ignored ? parent : this._rules.test(path27, checkUnignored, MODE_IGNORE);
623
623
  }
624
- ignores(path25) {
625
- return this._test(path25, this._ignoreCache, false).ignored;
624
+ ignores(path27) {
625
+ return this._test(path27, this._ignoreCache, false).ignored;
626
626
  }
627
627
  createFilter() {
628
- return (path25) => !this.ignores(path25);
628
+ return (path27) => !this.ignores(path27);
629
629
  }
630
630
  filter(paths) {
631
631
  return makeArray(paths).filter(this.createFilter());
632
632
  }
633
633
  // @returns {TestResult}
634
- test(path25) {
635
- return this._test(path25, this._testCache, true);
634
+ test(path27) {
635
+ return this._test(path27, this._testCache, true);
636
636
  }
637
637
  };
638
638
  var factory = (options) => new Ignore2(options);
639
- var isPathValid = (path25) => checkPath(path25 && checkPath.convert(path25), path25, RETURN_FALSE);
639
+ var isPathValid = (path27) => checkPath(path27 && checkPath.convert(path27), path27, RETURN_FALSE);
640
640
  var setupWindows = () => {
641
641
  const makePosix = (str) => /^\\\\\?\\/.test(str) || /["<>|\u0000-\u001F]+/u.test(str) ? str : str.replace(/\\/g, "/");
642
642
  checkPath.convert = makePosix;
643
643
  const REGEX_TEST_WINDOWS_PATH_ABSOLUTE = /^[a-z]:\//i;
644
- checkPath.isNotRelative = (path25) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path25) || isNotRelative(path25);
644
+ checkPath.isNotRelative = (path27) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path27) || isNotRelative(path27);
645
645
  };
646
646
  if (
647
647
  // Detect `process` so that it can run in browsers.
@@ -665,17 +665,17 @@ __export(cli_exports, {
665
665
  });
666
666
  module.exports = __toCommonJS(cli_exports);
667
667
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
668
- var import_fs15 = require("fs");
669
- var os5 = __toESM(require("os"), 1);
670
- var path24 = __toESM(require("path"), 1);
668
+ var import_fs16 = require("fs");
669
+ var os6 = __toESM(require("os"), 1);
670
+ var path26 = __toESM(require("path"), 1);
671
671
  var import_url2 = require("url");
672
672
 
673
673
  // src/config/constants.ts
674
674
  var DEFAULT_INCLUDE = [
675
- "**/*.{ts,tsx,js,jsx,mjs,cjs}",
675
+ "**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}",
676
676
  "**/*.{py,pyi}",
677
- "**/*.{go,rs,java,kt,scala}",
678
- "**/*.{c,cpp,cc,h,hpp}",
677
+ "**/*.{go,rs,java,cs,kt,scala}",
678
+ "**/*.{c,cpp,cc,cxx,h,hpp,hxx}",
679
679
  "**/*.{rb,php,inc,swift}",
680
680
  "**/*.{cls,trigger}",
681
681
  "**/*.{vue,svelte,astro}",
@@ -800,7 +800,8 @@ function getDefaultIndexingConfig() {
800
800
  requireProjectMarker: true,
801
801
  maxDepth: 5,
802
802
  maxFilesPerDirectory: 100,
803
- fallbackToTextOnMaxChunks: true
803
+ fallbackToTextOnMaxChunks: true,
804
+ gitBlame: { enabled: false }
804
805
  };
805
806
  }
806
807
  function getDefaultSearchConfig() {
@@ -924,7 +925,10 @@ function parseConfig(raw) {
924
925
  requireProjectMarker: typeof rawIndexing.requireProjectMarker === "boolean" ? rawIndexing.requireProjectMarker : defaultIndexing.requireProjectMarker,
925
926
  maxDepth: typeof rawIndexing.maxDepth === "number" ? rawIndexing.maxDepth < -1 ? -1 : rawIndexing.maxDepth : defaultIndexing.maxDepth,
926
927
  maxFilesPerDirectory: typeof rawIndexing.maxFilesPerDirectory === "number" ? Math.max(1, rawIndexing.maxFilesPerDirectory) : defaultIndexing.maxFilesPerDirectory,
927
- fallbackToTextOnMaxChunks: typeof rawIndexing.fallbackToTextOnMaxChunks === "boolean" ? rawIndexing.fallbackToTextOnMaxChunks : defaultIndexing.fallbackToTextOnMaxChunks
928
+ fallbackToTextOnMaxChunks: typeof rawIndexing.fallbackToTextOnMaxChunks === "boolean" ? rawIndexing.fallbackToTextOnMaxChunks : defaultIndexing.fallbackToTextOnMaxChunks,
929
+ gitBlame: {
930
+ enabled: rawIndexing.gitBlame && typeof rawIndexing.gitBlame === "object" && typeof rawIndexing.gitBlame.enabled === "boolean" ? rawIndexing.gitBlame.enabled : defaultIndexing.gitBlame.enabled
931
+ }
928
932
  };
929
933
  const rawSearch = input.search && typeof input.search === "object" ? input.search : {};
930
934
  const search = {
@@ -1054,7 +1058,7 @@ var autoDetectProviders = AUTO_DETECT_PROVIDER_ORDER.filter(
1054
1058
  );
1055
1059
 
1056
1060
  // src/config/host.ts
1057
- var HOST_MODES = ["opencode", "codex", "claude", "pi"];
1061
+ var HOST_MODES = ["opencode", "codex", "claude", "pi", "jcode"];
1058
1062
  function isSupportedHostMode(value) {
1059
1063
  return HOST_MODES.includes(value);
1060
1064
  }
@@ -1109,9 +1113,9 @@ var import_fs = require("fs");
1109
1113
  var path = __toESM(require("path"), 1);
1110
1114
 
1111
1115
  // src/eval/report-formatters.ts
1112
- function assertFiniteNumber(value, path25) {
1116
+ function assertFiniteNumber(value, path27) {
1113
1117
  if (typeof value !== "number" || Number.isNaN(value) || !Number.isFinite(value)) {
1114
- throw new Error(`${path25} must be a finite number`);
1118
+ throw new Error(`${path27} must be a finite number`);
1115
1119
  }
1116
1120
  return value;
1117
1121
  }
@@ -1299,16 +1303,16 @@ function buildPerQueryArtifact(perQuery) {
1299
1303
  }
1300
1304
 
1301
1305
  // src/eval/runner.ts
1302
- var import_fs10 = require("fs");
1303
- var path13 = __toESM(require("path"), 1);
1306
+ var import_fs11 = require("fs");
1307
+ var path15 = __toESM(require("path"), 1);
1304
1308
  var import_perf_hooks2 = require("perf_hooks");
1305
1309
 
1306
1310
  // src/indexer/index.ts
1307
- var import_fs7 = require("fs");
1308
- var path10 = __toESM(require("path"), 1);
1311
+ var import_fs8 = require("fs");
1312
+ var path12 = __toESM(require("path"), 1);
1309
1313
  var import_perf_hooks = require("perf_hooks");
1310
- var import_child_process2 = require("child_process");
1311
- var import_util2 = require("util");
1314
+ var import_child_process3 = require("child_process");
1315
+ var import_util3 = require("util");
1312
1316
 
1313
1317
  // node_modules/eventemitter3/index.mjs
1314
1318
  var import_index = __toESM(require_eventemitter3(), 1);
@@ -2587,17 +2591,17 @@ function validateExternalUrl(urlString) {
2587
2591
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
2588
2592
  return { valid: false, reason: `Blocked protocol: ${parsed.protocol}` };
2589
2593
  }
2590
- const hostname = parsed.hostname.toLowerCase();
2591
- if (BLOCKED_HOSTNAMES.has(hostname)) {
2592
- return { valid: false, reason: `Blocked: cloud metadata service (${hostname})` };
2594
+ const hostname2 = parsed.hostname.toLowerCase();
2595
+ if (BLOCKED_HOSTNAMES.has(hostname2)) {
2596
+ return { valid: false, reason: `Blocked: cloud metadata service (${hostname2})` };
2593
2597
  }
2594
2598
  for (const pattern of BLOCKED_METADATA_IPS) {
2595
- if (pattern.test(hostname)) {
2596
- return { valid: false, reason: `Blocked: cloud metadata IP (${hostname})` };
2599
+ if (pattern.test(hostname2)) {
2600
+ return { valid: false, reason: `Blocked: cloud metadata IP (${hostname2})` };
2597
2601
  }
2598
2602
  }
2599
- if (/^169\.254\./.test(hostname)) {
2600
- return { valid: false, reason: `Blocked: link-local address (${hostname})` };
2603
+ if (/^169\.254\./.test(hostname2)) {
2604
+ return { valid: false, reason: `Blocked: link-local address (${hostname2})` };
2601
2605
  }
2602
2606
  return { valid: true };
2603
2607
  }
@@ -3788,6 +3792,12 @@ function createMockNativeBinding() {
3788
3792
  constructor() {
3789
3793
  throw error;
3790
3794
  }
3795
+ static openReadOnly() {
3796
+ throw error;
3797
+ }
3798
+ static createEmptyReadOnly() {
3799
+ throw error;
3800
+ }
3791
3801
  close() {
3792
3802
  throw error;
3793
3803
  }
@@ -3891,6 +3901,12 @@ var VectorStore = class {
3891
3901
  load() {
3892
3902
  this.inner.load();
3893
3903
  }
3904
+ loadStrict() {
3905
+ this.inner.loadStrict();
3906
+ }
3907
+ hasFingerprint() {
3908
+ return this.inner.hasFingerprint();
3909
+ }
3894
3910
  count() {
3895
3911
  return this.inner.count();
3896
3912
  }
@@ -4173,12 +4189,24 @@ var InvertedIndex = class {
4173
4189
  return this.inner.documentCount();
4174
4190
  }
4175
4191
  };
4176
- var Database = class {
4192
+ var Database = class _Database {
4177
4193
  inner;
4178
4194
  closed = false;
4179
4195
  constructor(dbPath) {
4180
4196
  this.inner = new native.Database(dbPath);
4181
4197
  }
4198
+ static fromNative(inner) {
4199
+ const database = Object.create(_Database.prototype);
4200
+ database.inner = inner;
4201
+ database.closed = false;
4202
+ return database;
4203
+ }
4204
+ static openReadOnly(dbPath) {
4205
+ return _Database.fromNative(native.Database.openReadOnly(dbPath));
4206
+ }
4207
+ static createEmptyReadOnly() {
4208
+ return _Database.fromNative(native.Database.createEmptyReadOnly());
4209
+ }
4182
4210
  throwIfClosed() {
4183
4211
  if (this.closed) {
4184
4212
  throw new Error("Database is closed");
@@ -4647,6 +4675,9 @@ function getHostProjectIndexRelativePath(host) {
4647
4675
  function hasHostProjectConfig(projectRoot, host) {
4648
4676
  return (0, import_fs6.existsSync)(path8.join(projectRoot, getProjectConfigRelativePath(host)));
4649
4677
  }
4678
+ function hasHostGlobalConfig(host) {
4679
+ return (0, import_fs6.existsSync)(getGlobalConfigPath(host));
4680
+ }
4650
4681
  function getGlobalIndexPath(host = "opencode") {
4651
4682
  switch (host) {
4652
4683
  case "opencode":
@@ -4686,6 +4717,9 @@ function resolveGlobalIndexPath(host = "opencode") {
4686
4717
  return hostIndexPath;
4687
4718
  }
4688
4719
  if (host !== "opencode") {
4720
+ if (hasHostGlobalConfig(host)) {
4721
+ return hostIndexPath;
4722
+ }
4689
4723
  const legacyIndexPath = getGlobalIndexPath("opencode");
4690
4724
  if ((0, import_fs6.existsSync)(legacyIndexPath)) {
4691
4725
  return legacyIndexPath;
@@ -4839,8 +4873,8 @@ function normalizeFiles(rawFiles, projectRoot) {
4839
4873
  const trimmed = raw.trim();
4840
4874
  if (!trimmed) continue;
4841
4875
  const absolute = path9.resolve(projectRoot, trimmed);
4842
- const relative10 = path9.relative(projectRoot, absolute);
4843
- const cleaned = relative10.startsWith("./") ? relative10.slice(2) : relative10;
4876
+ const relative11 = path9.relative(projectRoot, absolute);
4877
+ const cleaned = relative11.startsWith("./") ? relative11.slice(2) : relative11;
4844
4878
  if (!seen.has(cleaned)) {
4845
4879
  seen.add(cleaned);
4846
4880
  result.push(cleaned);
@@ -4849,8 +4883,477 @@ function normalizeFiles(rawFiles, projectRoot) {
4849
4883
  return result;
4850
4884
  }
4851
4885
 
4886
+ // src/indexer/git-blame.ts
4887
+ var import_child_process2 = require("child_process");
4888
+ var path10 = __toESM(require("path"), 1);
4889
+ var import_util2 = require("util");
4890
+ var execFileAsync2 = (0, import_util2.promisify)(import_child_process2.execFile);
4891
+ function parseGitBlamePorcelain(output) {
4892
+ const commits = /* @__PURE__ */ new Map();
4893
+ let current;
4894
+ for (const line of output.split("\n")) {
4895
+ if (/^[0-9a-f]{40} /.test(line)) {
4896
+ const sha = line.slice(0, 40);
4897
+ current = commits.get(sha) ?? {
4898
+ sha,
4899
+ author: "",
4900
+ authorEmail: "",
4901
+ committedAt: 0,
4902
+ summary: "",
4903
+ lines: 0
4904
+ };
4905
+ commits.set(sha, current);
4906
+ continue;
4907
+ }
4908
+ if (!current) {
4909
+ continue;
4910
+ }
4911
+ if (line.startsWith("author ")) {
4912
+ current.author = line.slice("author ".length);
4913
+ } else if (line.startsWith("author-mail ")) {
4914
+ current.authorEmail = line.slice("author-mail ".length).replace(/^<|>$/g, "");
4915
+ } else if (line.startsWith("author-time ")) {
4916
+ current.committedAt = Number.parseInt(line.slice("author-time ".length), 10);
4917
+ } else if (line.startsWith("summary ")) {
4918
+ current.summary = line.slice("summary ".length);
4919
+ } else if (line.startsWith(" ")) {
4920
+ current.lines += 1;
4921
+ }
4922
+ }
4923
+ return Array.from(commits.values()).filter((commit) => commit.lines > 0).sort((a, b) => b.lines - a.lines || b.committedAt - a.committedAt)[0];
4924
+ }
4925
+ async function getChunkGitBlame(projectRoot, filePath, startLine, endLine) {
4926
+ const relativePath = path10.relative(projectRoot, filePath);
4927
+ try {
4928
+ const { stdout } = await execFileAsync2(
4929
+ "git",
4930
+ ["blame", "--line-porcelain", "-L", `${startLine},${endLine}`, "--", relativePath],
4931
+ { cwd: projectRoot, timeout: 3e4 }
4932
+ );
4933
+ return parseGitBlamePorcelain(stdout);
4934
+ } catch {
4935
+ return void 0;
4936
+ }
4937
+ }
4938
+
4939
+ // src/indexer/index-lock.ts
4940
+ var import_crypto = require("crypto");
4941
+ var import_fs7 = require("fs");
4942
+ var os4 = __toESM(require("os"), 1);
4943
+ var path11 = __toESM(require("path"), 1);
4944
+ var OWNER_FILE_NAME = "owner.json";
4945
+ var RECLAIM_DIRECTORY_NAME = "reclaiming";
4946
+ var RECOVERY_MARKER_PREFIX = "indexing.lock.recovery.";
4947
+ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
4948
+ var VALID_OPERATIONS = /* @__PURE__ */ new Set([
4949
+ "initialize",
4950
+ "index",
4951
+ "force-index",
4952
+ "clear",
4953
+ "health-check",
4954
+ "retry-failed-batches",
4955
+ "recovery"
4956
+ ]);
4957
+ var temporaryCounter = 0;
4958
+ function getErrorCode(error) {
4959
+ return typeof error === "object" && error !== null && "code" in error ? String(error.code) : void 0;
4960
+ }
4961
+ function retryTransientFilesystemOperation(operation) {
4962
+ let lastError;
4963
+ for (let attempt = 0; attempt < 3; attempt += 1) {
4964
+ try {
4965
+ operation();
4966
+ return;
4967
+ } catch (error) {
4968
+ lastError = error;
4969
+ const code = getErrorCode(error);
4970
+ if (code !== "EBUSY" && code !== "EPERM") throw error;
4971
+ if (attempt < 2) {
4972
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, (attempt + 1) * 10);
4973
+ }
4974
+ }
4975
+ }
4976
+ throw lastError;
4977
+ }
4978
+ function parseOwner(value) {
4979
+ if (typeof value !== "object" || value === null) return null;
4980
+ const candidate = value;
4981
+ if (!Number.isInteger(candidate.pid) || (candidate.pid ?? 0) <= 0) return null;
4982
+ if (typeof candidate.hostname !== "string" || candidate.hostname.length === 0) return null;
4983
+ if (typeof candidate.startedAt !== "string" || Number.isNaN(Date.parse(candidate.startedAt))) return null;
4984
+ if (typeof candidate.operation !== "string" || !VALID_OPERATIONS.has(candidate.operation)) return null;
4985
+ if (typeof candidate.token !== "string" || !UUID_PATTERN.test(candidate.token)) return null;
4986
+ return candidate;
4987
+ }
4988
+ function parseReclaimOwner(value) {
4989
+ if (typeof value !== "object" || value === null) return null;
4990
+ const candidate = value;
4991
+ if (!Number.isInteger(candidate.pid) || (candidate.pid ?? 0) <= 0) return null;
4992
+ if (typeof candidate.hostname !== "string" || candidate.hostname.length === 0) return null;
4993
+ if (typeof candidate.startedAt !== "string" || Number.isNaN(Date.parse(candidate.startedAt))) return null;
4994
+ if (typeof candidate.token !== "string" || !UUID_PATTERN.test(candidate.token)) return null;
4995
+ if (typeof candidate.expectedOwnerToken !== "string" || !UUID_PATTERN.test(candidate.expectedOwnerToken)) return null;
4996
+ return candidate;
4997
+ }
4998
+ function readJsonDirectory(directoryPath, parser) {
4999
+ try {
5000
+ return parser(JSON.parse((0, import_fs7.readFileSync)(path11.join(directoryPath, OWNER_FILE_NAME), "utf-8")));
5001
+ } catch {
5002
+ return null;
5003
+ }
5004
+ }
5005
+ function readDirectoryOwner(lockPath) {
5006
+ return readJsonDirectory(lockPath, parseOwner);
5007
+ }
5008
+ function readReclaimOwner(markerPath) {
5009
+ return readJsonDirectory(markerPath, parseReclaimOwner);
5010
+ }
5011
+ function readRecoveryOwner(markerPath) {
5012
+ return readDirectoryOwner(markerPath);
5013
+ }
5014
+ function readLegacyOwner(lockPath) {
5015
+ try {
5016
+ const parsed = JSON.parse((0, import_fs7.readFileSync)(lockPath, "utf-8"));
5017
+ if (!Number.isInteger(parsed.pid) || Number(parsed.pid) <= 0) return null;
5018
+ if (typeof parsed.startedAt !== "string" || Number.isNaN(Date.parse(parsed.startedAt))) return null;
5019
+ return {
5020
+ pid: Number(parsed.pid),
5021
+ hostname: typeof parsed.hostname === "string" ? parsed.hostname : os4.hostname(),
5022
+ startedAt: parsed.startedAt,
5023
+ operation: typeof parsed.operation === "string" && VALID_OPERATIONS.has(parsed.operation) ? parsed.operation : "index",
5024
+ token: typeof parsed.token === "string" ? parsed.token : "legacy-v0.14.0"
5025
+ };
5026
+ } catch {
5027
+ return null;
5028
+ }
5029
+ }
5030
+ function getOwnerLiveness(owner) {
5031
+ if (owner.hostname !== os4.hostname()) return "unknown";
5032
+ try {
5033
+ process.kill(owner.pid, 0);
5034
+ return "alive";
5035
+ } catch (error) {
5036
+ const code = getErrorCode(error);
5037
+ if (code === "ESRCH") return "dead";
5038
+ if (code === "EPERM") return "alive";
5039
+ return "unknown";
5040
+ }
5041
+ }
5042
+ function sameOwner(left, right) {
5043
+ return left.pid === right.pid && left.hostname === right.hostname && left.token === right.token;
5044
+ }
5045
+ function sameReclaimOwner(left, right) {
5046
+ return left.pid === right.pid && left.hostname === right.hostname && left.token === right.token && left.expectedOwnerToken === right.expectedOwnerToken;
5047
+ }
5048
+ function publishJsonDirectory(finalPath, value) {
5049
+ const candidatePath = `${finalPath}.candidate.${process.pid}.${(0, import_crypto.randomUUID)()}`;
5050
+ (0, import_fs7.mkdirSync)(candidatePath, { mode: 448 });
5051
+ try {
5052
+ (0, import_fs7.writeFileSync)(path11.join(candidatePath, OWNER_FILE_NAME), JSON.stringify(value), {
5053
+ encoding: "utf-8",
5054
+ flag: "wx",
5055
+ mode: 384
5056
+ });
5057
+ if ((0, import_fs7.existsSync)(finalPath)) return false;
5058
+ try {
5059
+ (0, import_fs7.renameSync)(candidatePath, finalPath);
5060
+ return true;
5061
+ } catch (error) {
5062
+ if ((0, import_fs7.existsSync)(finalPath)) return false;
5063
+ throw error;
5064
+ }
5065
+ } finally {
5066
+ if ((0, import_fs7.existsSync)(candidatePath)) (0, import_fs7.rmSync)(candidatePath, { recursive: true, force: true });
5067
+ }
5068
+ }
5069
+ function createOwner(operation) {
5070
+ return {
5071
+ pid: process.pid,
5072
+ hostname: os4.hostname(),
5073
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
5074
+ operation,
5075
+ token: (0, import_crypto.randomUUID)()
5076
+ };
5077
+ }
5078
+ function recoveryMarkerPath(indexPath, owner) {
5079
+ return path11.join(indexPath, `${RECOVERY_MARKER_PREFIX}${owner.token}`);
5080
+ }
5081
+ function publishRecoveryMarker(indexPath, owner) {
5082
+ const markerPath = recoveryMarkerPath(indexPath, owner);
5083
+ if (!publishJsonDirectory(markerPath, owner)) {
5084
+ const markerOwner = readRecoveryOwner(markerPath);
5085
+ if (!markerOwner || !sameOwner(markerOwner, owner)) {
5086
+ throw new IndexLockContentionError(markerPath, markerOwner, "unknown-owner");
5087
+ }
5088
+ }
5089
+ return markerPath;
5090
+ }
5091
+ function getPendingRecoveries(indexPath) {
5092
+ const recoveries = [];
5093
+ const markerNames = (0, import_fs7.readdirSync)(indexPath).filter((name) => {
5094
+ if (!name.startsWith(RECOVERY_MARKER_PREFIX)) return false;
5095
+ return UUID_PATTERN.test(name.slice(RECOVERY_MARKER_PREFIX.length));
5096
+ }).sort();
5097
+ for (const markerName of markerNames) {
5098
+ const markerPath = path11.join(indexPath, markerName);
5099
+ const markerToken = markerName.slice(RECOVERY_MARKER_PREFIX.length);
5100
+ let markerStats;
5101
+ try {
5102
+ markerStats = (0, import_fs7.lstatSync)(markerPath);
5103
+ } catch (error) {
5104
+ if (getErrorCode(error) === "ENOENT") continue;
5105
+ throw error;
5106
+ }
5107
+ if (!markerStats.isDirectory()) {
5108
+ throw new IndexLockContentionError(markerPath, null, "unknown-owner");
5109
+ }
5110
+ const owner = readRecoveryOwner(markerPath);
5111
+ if (!owner || owner.token !== markerToken || getOwnerLiveness(owner) !== "dead") {
5112
+ throw new IndexLockContentionError(markerPath, owner, "unknown-owner");
5113
+ }
5114
+ recoveries.push({ owner, markerPath });
5115
+ }
5116
+ recoveries.sort((left, right) => {
5117
+ const byTimestamp = left.owner.startedAt.localeCompare(right.owner.startedAt);
5118
+ return byTimestamp !== 0 ? byTimestamp : left.markerPath.localeCompare(right.markerPath);
5119
+ });
5120
+ return recoveries;
5121
+ }
5122
+ function cleanupDeadPublicationCandidates(indexPath) {
5123
+ const candidatePattern = /^indexing\.lock(?:\.recovery\.[0-9a-f-]{36})?\.candidate\.(\d+)\.[0-9a-f-]{36}$/i;
5124
+ for (const entry of (0, import_fs7.readdirSync)(indexPath)) {
5125
+ const match = candidatePattern.exec(entry);
5126
+ if (!match) continue;
5127
+ const pid = Number(match[1]);
5128
+ if (!Number.isInteger(pid) || pid <= 0) continue;
5129
+ if (getOwnerLiveness({ pid, hostname: os4.hostname() }) !== "dead") continue;
5130
+ (0, import_fs7.rmSync)(path11.join(indexPath, entry), { recursive: true, force: true });
5131
+ }
5132
+ }
5133
+ function removeDeadReclaimMarker(lockPath, expectedOwner) {
5134
+ const markerPath = path11.join(lockPath, RECLAIM_DIRECTORY_NAME);
5135
+ const marker = readReclaimOwner(markerPath);
5136
+ if (!marker || marker.expectedOwnerToken !== expectedOwner.token || getOwnerLiveness(marker) !== "dead") {
5137
+ return false;
5138
+ }
5139
+ const currentOwner = readDirectoryOwner(lockPath);
5140
+ if (!currentOwner || !sameOwner(currentOwner, expectedOwner) || getOwnerLiveness(currentOwner) !== "dead") {
5141
+ return false;
5142
+ }
5143
+ const claimedMarkerPath = `${markerPath}.stale.${marker.pid}.${marker.token}.${(0, import_crypto.randomUUID)()}`;
5144
+ try {
5145
+ (0, import_fs7.renameSync)(markerPath, claimedMarkerPath);
5146
+ } catch (error) {
5147
+ if (getErrorCode(error) === "ENOENT") return false;
5148
+ throw error;
5149
+ }
5150
+ const claimedMarker = readReclaimOwner(claimedMarkerPath);
5151
+ const ownerAfterClaim = readDirectoryOwner(lockPath);
5152
+ if (!claimedMarker || !sameReclaimOwner(claimedMarker, marker) || !ownerAfterClaim || !sameOwner(ownerAfterClaim, expectedOwner) || getOwnerLiveness(ownerAfterClaim) !== "dead") {
5153
+ if (!(0, import_fs7.existsSync)(markerPath) && (0, import_fs7.existsSync)(claimedMarkerPath)) {
5154
+ (0, import_fs7.renameSync)(claimedMarkerPath, markerPath);
5155
+ }
5156
+ return false;
5157
+ }
5158
+ (0, import_fs7.rmSync)(claimedMarkerPath, { recursive: true, force: true });
5159
+ return true;
5160
+ }
5161
+ function reclaimDeadOwner(indexPath, lockPath, expectedOwner) {
5162
+ const reclaimPath = path11.join(lockPath, RECLAIM_DIRECTORY_NAME);
5163
+ const reclaimOwner = {
5164
+ pid: process.pid,
5165
+ hostname: os4.hostname(),
5166
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
5167
+ token: (0, import_crypto.randomUUID)(),
5168
+ expectedOwnerToken: expectedOwner.token
5169
+ };
5170
+ for (let attempt = 0; attempt < 2; attempt += 1) {
5171
+ if (publishJsonDirectory(reclaimPath, reclaimOwner)) break;
5172
+ if (attempt === 0 && removeDeadReclaimMarker(lockPath, expectedOwner)) continue;
5173
+ return false;
5174
+ }
5175
+ try {
5176
+ const currentReclaimer = readReclaimOwner(reclaimPath);
5177
+ const currentOwner = readDirectoryOwner(lockPath);
5178
+ if (!currentReclaimer || !sameReclaimOwner(currentReclaimer, reclaimOwner) || !currentOwner || !sameOwner(currentOwner, expectedOwner) || getOwnerLiveness(currentOwner) !== "dead") {
5179
+ return false;
5180
+ }
5181
+ publishRecoveryMarker(indexPath, expectedOwner);
5182
+ const ownerBeforeQuarantine = readDirectoryOwner(lockPath);
5183
+ const reclaimerBeforeQuarantine = readReclaimOwner(reclaimPath);
5184
+ if (!ownerBeforeQuarantine || !sameOwner(ownerBeforeQuarantine, expectedOwner) || getOwnerLiveness(ownerBeforeQuarantine) !== "dead" || !reclaimerBeforeQuarantine || !sameReclaimOwner(reclaimerBeforeQuarantine, reclaimOwner)) {
5185
+ return false;
5186
+ }
5187
+ const quarantinePath = `${lockPath}.stale.${expectedOwner.token}.${reclaimOwner.token}`;
5188
+ (0, import_fs7.renameSync)(lockPath, quarantinePath);
5189
+ (0, import_fs7.rmSync)(quarantinePath, { recursive: true, force: true });
5190
+ return true;
5191
+ } catch (error) {
5192
+ if (getErrorCode(error) === "ENOENT") return false;
5193
+ throw error;
5194
+ }
5195
+ }
5196
+ var IndexLockContentionError = class extends Error {
5197
+ constructor(lockPath, owner, reason) {
5198
+ const ownerDescription = owner ? `PID ${owner.pid} on ${owner.hostname}, operation ${owner.operation}, since ${owner.startedAt}` : "an unreadable owner";
5199
+ super(`Index mutation already in progress: ${ownerDescription}`);
5200
+ this.lockPath = lockPath;
5201
+ this.owner = owner;
5202
+ this.reason = reason;
5203
+ this.name = "IndexLockContentionError";
5204
+ }
5205
+ lockPath;
5206
+ owner;
5207
+ reason;
5208
+ code = "INDEX_BUSY";
5209
+ };
5210
+ function isIndexLockContentionError(error) {
5211
+ return error instanceof IndexLockContentionError || typeof error === "object" && error !== null && "code" in error && error.code === "INDEX_BUSY";
5212
+ }
5213
+ function isTransientIndexLockContention(error) {
5214
+ if (!isIndexLockContentionError(error) || !("reason" in error)) return false;
5215
+ return error.reason === "active" || error.reason === "reclaiming";
5216
+ }
5217
+ function acquireIndexLock(indexPath, operation) {
5218
+ (0, import_fs7.mkdirSync)(indexPath, { recursive: true });
5219
+ const canonicalIndexPath = import_fs7.realpathSync.native(indexPath);
5220
+ const lockPath = path11.join(canonicalIndexPath, "indexing.lock");
5221
+ cleanupDeadPublicationCandidates(canonicalIndexPath);
5222
+ for (let attempt = 0; attempt < 6; attempt += 1) {
5223
+ const owner = createOwner(operation);
5224
+ if (publishJsonDirectory(lockPath, owner)) {
5225
+ const lease = {
5226
+ canonicalIndexPath,
5227
+ lockPath,
5228
+ owner,
5229
+ recoveries: []
5230
+ };
5231
+ try {
5232
+ lease.recoveries = getPendingRecoveries(canonicalIndexPath);
5233
+ return lease;
5234
+ } catch (error) {
5235
+ releaseIndexLock(lease);
5236
+ throw error;
5237
+ }
5238
+ }
5239
+ let stats;
5240
+ try {
5241
+ stats = (0, import_fs7.lstatSync)(lockPath);
5242
+ } catch (error) {
5243
+ if (getErrorCode(error) === "ENOENT") continue;
5244
+ throw error;
5245
+ }
5246
+ if (!stats.isDirectory()) {
5247
+ const legacyOwner = readLegacyOwner(lockPath);
5248
+ throw new IndexLockContentionError(lockPath, legacyOwner, "legacy-lock");
5249
+ }
5250
+ const existingOwner = readDirectoryOwner(lockPath);
5251
+ if (!existingOwner) {
5252
+ throw new IndexLockContentionError(lockPath, null, "unknown-owner");
5253
+ }
5254
+ const liveness = getOwnerLiveness(existingOwner);
5255
+ if (liveness !== "dead") {
5256
+ throw new IndexLockContentionError(lockPath, existingOwner, liveness === "alive" ? "active" : "unknown-owner");
5257
+ }
5258
+ if (!reclaimDeadOwner(canonicalIndexPath, lockPath, existingOwner)) {
5259
+ throw new IndexLockContentionError(lockPath, existingOwner, "reclaiming");
5260
+ }
5261
+ }
5262
+ throw new IndexLockContentionError(lockPath, null, "reclaiming");
5263
+ }
5264
+ function releaseIndexLock(lease) {
5265
+ const currentOwner = readDirectoryOwner(lease.lockPath);
5266
+ if (!currentOwner || !sameOwner(currentOwner, lease.owner)) return false;
5267
+ const releasePath = `${lease.lockPath}.release.${lease.owner.pid}.${lease.owner.token}`;
5268
+ try {
5269
+ retryTransientFilesystemOperation(() => (0, import_fs7.renameSync)(lease.lockPath, releasePath));
5270
+ } catch (error) {
5271
+ if (getErrorCode(error) === "ENOENT") return false;
5272
+ throw error;
5273
+ }
5274
+ const claimedOwner = readDirectoryOwner(releasePath);
5275
+ if (!claimedOwner || !sameOwner(claimedOwner, lease.owner)) {
5276
+ if (!(0, import_fs7.existsSync)(lease.lockPath) && (0, import_fs7.existsSync)(releasePath)) {
5277
+ (0, import_fs7.renameSync)(releasePath, lease.lockPath);
5278
+ }
5279
+ return false;
5280
+ }
5281
+ try {
5282
+ retryTransientFilesystemOperation(() => (0, import_fs7.rmSync)(releasePath, { recursive: true, force: true }));
5283
+ } catch (error) {
5284
+ console.error(`[codebase-index] Lease released but tombstone cleanup failed: ${releasePath}`, error);
5285
+ }
5286
+ return true;
5287
+ }
5288
+ async function withIndexLock(indexPath, operation, callback, options = {}) {
5289
+ const lease = acquireIndexLock(indexPath, operation);
5290
+ let result;
5291
+ let callbackError;
5292
+ let callbackFailed = false;
5293
+ try {
5294
+ result = await callback(lease);
5295
+ } catch (error) {
5296
+ callbackFailed = true;
5297
+ callbackError = error;
5298
+ }
5299
+ if (!callbackFailed && options.completeRecoveries !== false) {
5300
+ try {
5301
+ completeLeaseRecovery(lease);
5302
+ } catch (error) {
5303
+ callbackFailed = true;
5304
+ callbackError = error;
5305
+ }
5306
+ }
5307
+ let releaseError;
5308
+ try {
5309
+ if (!releaseIndexLock(lease)) {
5310
+ releaseError = new Error(`Lost ownership of index mutation lease ${lease.owner.token}`);
5311
+ }
5312
+ } catch (error) {
5313
+ releaseError = error;
5314
+ }
5315
+ if (releaseError !== void 0) {
5316
+ if (callbackFailed) throw new AggregateError([callbackError, releaseError], "Index mutation and lease release both failed");
5317
+ throw releaseError;
5318
+ }
5319
+ if (callbackFailed) throw callbackError;
5320
+ return result;
5321
+ }
5322
+ function createLeaseTemporaryPath(targetPath, owner, kind = "tmp") {
5323
+ if (kind === "bak") return `${targetPath}.bak.${owner.pid}.${owner.token}`;
5324
+ temporaryCounter += 1;
5325
+ return `${targetPath}.tmp.${owner.pid}.${owner.token}.${temporaryCounter}`;
5326
+ }
5327
+ function removeLeaseTemporaryPath(temporaryPath) {
5328
+ if ((0, import_fs7.existsSync)(temporaryPath)) (0, import_fs7.rmSync)(temporaryPath, { recursive: true, force: true });
5329
+ }
5330
+ function recoverLeaseArtifacts(indexPath, owner, backupTargets) {
5331
+ for (const targetPath of backupTargets) {
5332
+ const backupPath = createLeaseTemporaryPath(targetPath, owner, "bak");
5333
+ if (!(0, import_fs7.existsSync)(backupPath)) continue;
5334
+ if ((0, import_fs7.existsSync)(targetPath)) (0, import_fs7.rmSync)(targetPath, { recursive: true, force: true });
5335
+ (0, import_fs7.renameSync)(backupPath, targetPath);
5336
+ }
5337
+ const temporaryOwnerMarker = `.tmp.${owner.pid}.${owner.token}.`;
5338
+ for (const entry of (0, import_fs7.readdirSync)(indexPath)) {
5339
+ if (!entry.includes(temporaryOwnerMarker)) continue;
5340
+ (0, import_fs7.rmSync)(path11.join(indexPath, entry), { recursive: true, force: true });
5341
+ }
5342
+ }
5343
+ function completeLeaseRecovery(lease) {
5344
+ for (const recovery of lease.recoveries) {
5345
+ const markerOwner = readRecoveryOwner(recovery.markerPath);
5346
+ if (!markerOwner || !sameOwner(markerOwner, recovery.owner)) {
5347
+ throw new Error(`Recovery marker ownership changed: ${recovery.markerPath}`);
5348
+ }
5349
+ }
5350
+ for (const recovery of lease.recoveries) {
5351
+ (0, import_fs7.rmSync)(recovery.markerPath, { recursive: true, force: true });
5352
+ }
5353
+ }
5354
+
4852
5355
  // src/indexer/index.ts
4853
- var CALL_GRAPH_LANGUAGES = /* @__PURE__ */ new Set(["typescript", "tsx", "javascript", "jsx", "python", "go", "rust", "php", "apex", "zig", "gdscript", "matlab"]);
5356
+ var CALL_GRAPH_LANGUAGES = /* @__PURE__ */ new Set(["typescript", "tsx", "javascript", "jsx", "python", "go", "rust", "php", "apex", "zig", "gdscript", "matlab", "bash"]);
4854
5357
  var CASE_INSENSITIVE_LANGUAGES = /* @__PURE__ */ new Set(["apex"]);
4855
5358
  var CALL_GRAPH_SYMBOL_CHUNK_TYPES = /* @__PURE__ */ new Set([
4856
5359
  "function_declaration",
@@ -4930,6 +5433,46 @@ function isSqliteCorruptionError(error) {
4930
5433
  return message.includes("database disk image is malformed") || message.includes("file is not a database") || message.includes("database schema is corrupt") || message.includes("sqlite_corrupt");
4931
5434
  }
4932
5435
  var STARTUP_WARNING_METADATA_KEY = "index.startupWarning";
5436
+ var READER_ARTIFACT_RETRY_INTERVAL_MS = 1e3;
5437
+ function metadataFromBlame(blame) {
5438
+ if (!blame) {
5439
+ return {};
5440
+ }
5441
+ return {
5442
+ blameSha: blame.sha,
5443
+ blameAuthor: blame.author,
5444
+ blameAuthorEmail: blame.authorEmail,
5445
+ blameCommittedAt: blame.committedAt,
5446
+ blameSummary: blame.summary
5447
+ };
5448
+ }
5449
+ function blameFromChunkData(chunk) {
5450
+ if (!chunk?.blameSha || !chunk.blameAuthor || !chunk.blameAuthorEmail || chunk.blameCommittedAt === void 0 || !chunk.blameSummary) {
5451
+ return void 0;
5452
+ }
5453
+ return {
5454
+ sha: chunk.blameSha,
5455
+ author: chunk.blameAuthor,
5456
+ authorEmail: chunk.blameAuthorEmail,
5457
+ committedAt: chunk.blameCommittedAt,
5458
+ summary: chunk.blameSummary
5459
+ };
5460
+ }
5461
+ function blameFromMetadata(metadata) {
5462
+ if (!metadata.blameSha || !metadata.blameAuthor || !metadata.blameAuthorEmail || metadata.blameCommittedAt === void 0 || !metadata.blameSummary) {
5463
+ return void 0;
5464
+ }
5465
+ return {
5466
+ sha: metadata.blameSha,
5467
+ author: metadata.blameAuthor,
5468
+ authorEmail: metadata.blameAuthorEmail,
5469
+ committedAt: metadata.blameCommittedAt,
5470
+ summary: metadata.blameSummary
5471
+ };
5472
+ }
5473
+ function hasBlameMetadata(metadata) {
5474
+ return blameFromMetadata(metadata) !== void 0;
5475
+ }
4933
5476
  var INDEX_METADATA_VERSION = "1";
4934
5477
  var EMBEDDING_STRATEGY_VERSION = "2";
4935
5478
  var RANKING_TOKEN_CACHE_LIMIT = 4096;
@@ -5088,9 +5631,9 @@ function hasAllEmbeddingParts(parts, expectedPartCount) {
5088
5631
  return true;
5089
5632
  }
5090
5633
  function isPathWithinRoot(filePath, rootPath) {
5091
- const normalizedFilePath = path10.resolve(filePath);
5092
- const normalizedRoot = path10.resolve(rootPath);
5093
- return normalizedFilePath === normalizedRoot || normalizedFilePath.startsWith(`${normalizedRoot}${path10.sep}`);
5634
+ const normalizedFilePath = path12.resolve(filePath);
5635
+ const normalizedRoot = path12.resolve(rootPath);
5636
+ return normalizedFilePath === normalizedRoot || normalizedFilePath.startsWith(`${normalizedRoot}${path12.sep}`);
5094
5637
  }
5095
5638
  var rankingQueryTokenCache = /* @__PURE__ */ new Map();
5096
5639
  var rankingNameTokenCache = /* @__PURE__ */ new Map();
@@ -5849,7 +6392,8 @@ function promoteIdentifierMatches(query, combined, semanticCandidates, keywordCa
5849
6392
  chunkType,
5850
6393
  name: chunk.name ?? void 0,
5851
6394
  language: chunk.language,
5852
- hash: chunk.contentHash
6395
+ hash: chunk.contentHash,
6396
+ ...metadataFromBlame(blameFromChunkData(chunk))
5853
6397
  };
5854
6398
  const baselineScore = existing?.score ?? 0.5;
5855
6399
  candidateUnion.set(chunk.chunkId, {
@@ -5933,7 +6477,8 @@ function buildSymbolDefinitionLane(query, database, branchChunkIds, limit, fallb
5933
6477
  chunkType,
5934
6478
  name: chunk.name ?? void 0,
5935
6479
  language: chunk.language,
5936
- hash: chunk.contentHash
6480
+ hash: chunk.contentHash,
6481
+ ...metadataFromBlame(blameFromChunkData(chunk))
5937
6482
  }
5938
6483
  });
5939
6484
  }
@@ -6102,6 +6647,21 @@ function matchesSearchFilters(candidate, options, minScore) {
6102
6647
  if (options?.chunkType && candidate.metadata.chunkType !== options.chunkType) {
6103
6648
  return false;
6104
6649
  }
6650
+ if (options?.blameAuthor) {
6651
+ const author = options.blameAuthor.toLowerCase();
6652
+ const candidateAuthor = candidate.metadata.blameAuthor?.toLowerCase();
6653
+ const candidateEmail = candidate.metadata.blameAuthorEmail?.toLowerCase();
6654
+ if (candidateAuthor !== author && candidateEmail !== author) return false;
6655
+ }
6656
+ if (options?.blameSha && !candidate.metadata.blameSha?.toLowerCase().startsWith(options.blameSha.toLowerCase())) {
6657
+ return false;
6658
+ }
6659
+ if (options?.blameSince) {
6660
+ const sinceMs = Date.parse(options.blameSince);
6661
+ if (Number.isNaN(sinceMs)) return false;
6662
+ const committedAt = candidate.metadata.blameCommittedAt;
6663
+ if (committedAt === void 0 || committedAt < Math.floor(sinceMs / 1e3)) return false;
6664
+ }
6105
6665
  return true;
6106
6666
  }
6107
6667
  function unionCandidates(semanticCandidates, keywordCandidates) {
@@ -6139,26 +6699,133 @@ var Indexer = class {
6139
6699
  queryCacheTtlMs = 5 * 60 * 1e3;
6140
6700
  querySimilarityThreshold = 0.85;
6141
6701
  indexCompatibility = null;
6142
- indexingLockPath = "";
6702
+ activeIndexLease = null;
6703
+ initializationPromise = null;
6704
+ initializationMode = "none";
6705
+ readIssues = [];
6706
+ retiredDatabases = [];
6707
+ readerArtifactFingerprint = null;
6708
+ writerArtifactFingerprint = null;
6709
+ readerArtifactRetryAfter = /* @__PURE__ */ new Map();
6143
6710
  constructor(projectRoot, config, host = "opencode") {
6144
6711
  this.projectRoot = projectRoot;
6145
6712
  this.config = config;
6146
6713
  this.host = host;
6147
6714
  this.indexPath = this.getIndexPath();
6148
- this.fileHashCachePath = path10.join(this.indexPath, "file-hashes.json");
6149
- this.failedBatchesPath = path10.join(this.indexPath, "failed-batches.json");
6150
- this.indexingLockPath = path10.join(this.indexPath, "indexing.lock");
6715
+ this.fileHashCachePath = path12.join(this.indexPath, "file-hashes.json");
6716
+ this.failedBatchesPath = path12.join(this.indexPath, "failed-batches.json");
6151
6717
  this.logger = initializeLogger(config.debug);
6152
6718
  }
6153
6719
  getIndexPath() {
6154
6720
  return resolveProjectIndexPath(this.projectRoot, this.config.scope, this.host);
6155
6721
  }
6722
+ isLocalProjectIndexPath() {
6723
+ const localProjectIndexPaths = [path12.join(this.projectRoot, getHostProjectIndexRelativePath(this.host))];
6724
+ if (this.host !== "opencode") {
6725
+ localProjectIndexPaths.push(path12.join(this.projectRoot, getHostProjectIndexRelativePath("opencode")));
6726
+ }
6727
+ return localProjectIndexPaths.some((localPath) => {
6728
+ if (!(0, import_fs8.existsSync)(localPath) || !(0, import_fs8.existsSync)(this.indexPath)) {
6729
+ return path12.resolve(this.indexPath) === path12.resolve(localPath);
6730
+ }
6731
+ const indexStats = (0, import_fs8.statSync)(this.indexPath);
6732
+ const localStats = (0, import_fs8.statSync)(localPath);
6733
+ return indexStats.dev === localStats.dev && indexStats.ino === localStats.ino;
6734
+ });
6735
+ }
6736
+ resetLoadedIndexState(retireDatabase = false) {
6737
+ if (this.database) {
6738
+ if (retireDatabase) {
6739
+ this.retiredDatabases.push(this.database);
6740
+ } else {
6741
+ this.database.close();
6742
+ }
6743
+ }
6744
+ this.store = null;
6745
+ this.invertedIndex = null;
6746
+ this.database = null;
6747
+ this.provider = null;
6748
+ this.configuredProviderInfo = null;
6749
+ this.reranker = null;
6750
+ this.indexCompatibility = null;
6751
+ this.initializationMode = "none";
6752
+ this.readIssues = [];
6753
+ this.readerArtifactFingerprint = null;
6754
+ this.writerArtifactFingerprint = null;
6755
+ this.readerArtifactRetryAfter.clear();
6756
+ this.fileHashCache.clear();
6757
+ }
6758
+ refreshLoadedIndexState() {
6759
+ if (!this.store || !this.invertedIndex || !this.configuredProviderInfo) return;
6760
+ this.store.load();
6761
+ this.invertedIndex.load();
6762
+ this.fileHashCache.clear();
6763
+ this.loadFileHashCache();
6764
+ this.indexCompatibility = this.validateIndexCompatibility(this.configuredProviderInfo);
6765
+ this.readIssues = [];
6766
+ this.readerArtifactRetryAfter.clear();
6767
+ }
6768
+ async withIndexMutationLease(operation, callback) {
6769
+ const lease = acquireIndexLock(this.indexPath, operation);
6770
+ this.indexPath = lease.canonicalIndexPath;
6771
+ this.fileHashCachePath = path12.join(this.indexPath, "file-hashes.json");
6772
+ this.failedBatchesPath = path12.join(this.indexPath, "failed-batches.json");
6773
+ this.activeIndexLease = lease;
6774
+ let result;
6775
+ let callbackError;
6776
+ let callbackFailed = false;
6777
+ try {
6778
+ result = await callback(lease.recoveries.map(({ owner }) => owner));
6779
+ } catch (error) {
6780
+ callbackFailed = true;
6781
+ callbackError = error;
6782
+ }
6783
+ if (!callbackFailed) {
6784
+ try {
6785
+ completeLeaseRecovery(lease);
6786
+ this.writerArtifactFingerprint = this.captureReaderArtifactFingerprint();
6787
+ } catch (error) {
6788
+ callbackFailed = true;
6789
+ callbackError = error;
6790
+ }
6791
+ }
6792
+ let releaseError;
6793
+ try {
6794
+ if (!releaseIndexLock(lease)) {
6795
+ releaseError = new Error(`Lost ownership of index mutation lease ${lease.owner.token}`);
6796
+ this.writerArtifactFingerprint = null;
6797
+ if (this.activeIndexLease?.owner.token === lease.owner.token) {
6798
+ this.activeIndexLease = null;
6799
+ }
6800
+ } else if (this.activeIndexLease?.owner.token === lease.owner.token) {
6801
+ this.activeIndexLease = null;
6802
+ }
6803
+ } catch (error) {
6804
+ releaseError = error;
6805
+ this.writerArtifactFingerprint = null;
6806
+ if (!(0, import_fs8.existsSync)(lease.lockPath) && this.activeIndexLease?.owner.token === lease.owner.token) {
6807
+ this.activeIndexLease = null;
6808
+ }
6809
+ }
6810
+ if (releaseError !== void 0) {
6811
+ if (callbackFailed) throw new AggregateError([callbackError, releaseError], "Index mutation and lease release both failed");
6812
+ throw releaseError;
6813
+ }
6814
+ if (callbackFailed) throw callbackError;
6815
+ return result;
6816
+ }
6817
+ requireActiveLease() {
6818
+ if (!this.activeIndexLease) {
6819
+ throw new Error("Index mutation attempted without an active interprocess lease");
6820
+ }
6821
+ return this.activeIndexLease;
6822
+ }
6156
6823
  loadFileHashCache() {
6157
- if (!(0, import_fs7.existsSync)(this.fileHashCachePath)) {
6824
+ if (!(0, import_fs8.existsSync)(this.fileHashCachePath)) {
6158
6825
  return;
6159
6826
  }
6160
6827
  try {
6161
- const data = (0, import_fs7.readFileSync)(this.fileHashCachePath, "utf-8");
6828
+ const data = (0, import_fs8.readFileSync)(this.fileHashCachePath, "utf-8");
6162
6829
  const parsed = JSON.parse(data);
6163
6830
  this.fileHashCache = new Map(Object.entries(parsed));
6164
6831
  } catch (error) {
@@ -6178,15 +6845,26 @@ var Indexer = class {
6178
6845
  this.atomicWriteSync(this.fileHashCachePath, JSON.stringify(obj));
6179
6846
  }
6180
6847
  atomicWriteSync(targetPath, data) {
6181
- const tempPath = `${targetPath}.tmp`;
6182
- (0, import_fs7.mkdirSync)(path10.dirname(targetPath), { recursive: true });
6183
- (0, import_fs7.writeFileSync)(tempPath, data);
6184
- (0, import_fs7.renameSync)(tempPath, targetPath);
6848
+ const lease = this.requireActiveLease();
6849
+ const tempPath = createLeaseTemporaryPath(targetPath, lease.owner, "tmp");
6850
+ (0, import_fs8.mkdirSync)(path12.dirname(targetPath), { recursive: true });
6851
+ try {
6852
+ (0, import_fs8.writeFileSync)(tempPath, data);
6853
+ (0, import_fs8.renameSync)(tempPath, targetPath);
6854
+ } finally {
6855
+ removeLeaseTemporaryPath(tempPath);
6856
+ }
6857
+ }
6858
+ saveInvertedIndex(invertedIndex) {
6859
+ this.atomicWriteSync(
6860
+ path12.join(this.indexPath, "inverted-index.json"),
6861
+ invertedIndex.serialize()
6862
+ );
6185
6863
  }
6186
6864
  getScopedRoots() {
6187
- const roots = /* @__PURE__ */ new Set([path10.resolve(this.projectRoot)]);
6865
+ const roots = /* @__PURE__ */ new Set([path12.resolve(this.projectRoot)]);
6188
6866
  for (const kbRoot of this.config.knowledgeBases) {
6189
- roots.add(path10.resolve(this.projectRoot, kbRoot));
6867
+ roots.add(path12.resolve(this.projectRoot, kbRoot));
6190
6868
  }
6191
6869
  return Array.from(roots);
6192
6870
  }
@@ -6195,29 +6873,29 @@ var Indexer = class {
6195
6873
  if (this.config.scope !== "global") {
6196
6874
  return branchName;
6197
6875
  }
6198
- const projectHash = hashContent(path10.resolve(this.projectRoot)).slice(0, 16);
6876
+ const projectHash = hashContent(path12.resolve(this.projectRoot)).slice(0, 16);
6199
6877
  return `${projectHash}:${branchName}`;
6200
6878
  }
6201
6879
  getBranchCatalogKeyFor(branchName) {
6202
6880
  if (this.config.scope !== "global") {
6203
6881
  return branchName;
6204
6882
  }
6205
- const projectHash = hashContent(path10.resolve(this.projectRoot)).slice(0, 16);
6883
+ const projectHash = hashContent(path12.resolve(this.projectRoot)).slice(0, 16);
6206
6884
  return `${projectHash}:${branchName}`;
6207
6885
  }
6208
6886
  getLegacyBranchCatalogKey() {
6209
6887
  return this.currentBranch || "default";
6210
6888
  }
6211
6889
  getLegacyMigrationMetadataKey() {
6212
- const projectHash = hashContent(path10.resolve(this.projectRoot)).slice(0, 16);
6890
+ const projectHash = hashContent(path12.resolve(this.projectRoot)).slice(0, 16);
6213
6891
  return `index.globalBranchMigration.${projectHash}`;
6214
6892
  }
6215
6893
  getProjectEmbeddingStrategyMetadataKey() {
6216
- const projectHash = hashContent(path10.resolve(this.projectRoot)).slice(0, 16);
6894
+ const projectHash = hashContent(path12.resolve(this.projectRoot)).slice(0, 16);
6217
6895
  return `index.embeddingStrategyVersion.${projectHash}`;
6218
6896
  }
6219
6897
  getProjectForceReembedMetadataKey() {
6220
- const projectHash = hashContent(path10.resolve(this.projectRoot)).slice(0, 16);
6898
+ const projectHash = hashContent(path12.resolve(this.projectRoot)).slice(0, 16);
6221
6899
  return `index.forceReembed.${projectHash}`;
6222
6900
  }
6223
6901
  hasProjectForceReembedPending() {
@@ -6311,7 +6989,7 @@ var Indexer = class {
6311
6989
  if (!this.database) {
6312
6990
  return { chunkIds, symbolIds };
6313
6991
  }
6314
- const projectRootPath = path10.resolve(this.projectRoot);
6992
+ const projectRootPath = path12.resolve(this.projectRoot);
6315
6993
  const projectLocalFilePaths = /* @__PURE__ */ new Set([
6316
6994
  ...Array.from(this.fileHashCache.keys()).filter(
6317
6995
  (filePath) => this.isFileInCurrentScope(filePath, roots) && isPathWithinRoot(filePath, projectRootPath)
@@ -6334,7 +7012,7 @@ var Indexer = class {
6334
7012
  if (this.config.scope !== "global") {
6335
7013
  return this.getBranchCatalogCleanupKeys();
6336
7014
  }
6337
- const projectHash = hashContent(path10.resolve(this.projectRoot)).slice(0, 16);
7015
+ const projectHash = hashContent(path12.resolve(this.projectRoot)).slice(0, 16);
6338
7016
  const keys = /* @__PURE__ */ new Set();
6339
7017
  const projectChunkIdSet = new Set(projectChunkIds);
6340
7018
  const projectSymbolIdSet = new Set(projectSymbolIds);
@@ -6418,7 +7096,7 @@ var Indexer = class {
6418
7096
  if (!this.database || this.config.scope !== "global") {
6419
7097
  return false;
6420
7098
  }
6421
- const projectHash = hashContent(path10.resolve(this.projectRoot)).slice(0, 16);
7099
+ const projectHash = hashContent(path12.resolve(this.projectRoot)).slice(0, 16);
6422
7100
  const roots = this.getScopedRoots();
6423
7101
  const { chunkIds: projectLocalChunkIds, symbolIds: projectLocalSymbolIds } = this.getProjectLocalScopedOwnershipIds(roots);
6424
7102
  return this.database.getAllBranches().some(
@@ -6452,7 +7130,7 @@ var Indexer = class {
6452
7130
  ...Array.from(this.fileHashCache.keys()).filter((filePath) => this.isFileInCurrentScope(filePath, roots)),
6453
7131
  ...scopedEntries.map(({ metadata }) => metadata.filePath)
6454
7132
  ]);
6455
- const projectRootPath = path10.resolve(this.projectRoot);
7133
+ const projectRootPath = path12.resolve(this.projectRoot);
6456
7134
  const projectLocalFilePaths = new Set(
6457
7135
  Array.from(filePaths).filter((filePath) => isPathWithinRoot(filePath, projectRootPath))
6458
7136
  );
@@ -6514,34 +7192,27 @@ var Indexer = class {
6514
7192
  database.gcOrphanEmbeddings();
6515
7193
  database.gcOrphanChunks();
6516
7194
  store.save();
6517
- invertedIndex.save();
7195
+ this.saveInvertedIndex(invertedIndex);
6518
7196
  return {
6519
7197
  removedChunkIds: removedChunkIdList,
6520
7198
  hasForeignData: allMetadata.some(({ metadata }) => !this.isFileInCurrentScope(metadata.filePath, roots))
6521
7199
  };
6522
7200
  }
6523
- checkForInterruptedIndexing() {
6524
- return (0, import_fs7.existsSync)(this.indexingLockPath);
6525
- }
6526
- acquireIndexingLock() {
6527
- const lockData = {
6528
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
6529
- pid: process.pid
6530
- };
6531
- (0, import_fs7.writeFileSync)(this.indexingLockPath, JSON.stringify(lockData));
6532
- }
6533
- releaseIndexingLock() {
6534
- if ((0, import_fs7.existsSync)(this.indexingLockPath)) {
6535
- (0, import_fs7.unlinkSync)(this.indexingLockPath);
7201
+ async recoverFromInterruptedIndexingUnlocked(owners) {
7202
+ for (const owner of owners) {
7203
+ this.logger.warn("Detected interrupted indexing session, recovering...", {
7204
+ pid: owner.pid,
7205
+ hostname: owner.hostname,
7206
+ operation: owner.operation,
7207
+ startedAt: owner.startedAt
7208
+ });
6536
7209
  }
6537
- }
6538
- async recoverFromInterruptedIndexing() {
6539
- this.logger.warn("Detected interrupted indexing session, recovering...");
6540
- if ((0, import_fs7.existsSync)(this.fileHashCachePath)) {
6541
- (0, import_fs7.unlinkSync)(this.fileHashCachePath);
7210
+ if (this.config.scope === "global") {
7211
+ if ((0, import_fs8.existsSync)(this.fileHashCachePath)) {
7212
+ (0, import_fs8.unlinkSync)(this.fileHashCachePath);
7213
+ }
7214
+ await this.healthCheckUnlocked();
6542
7215
  }
6543
- await this.healthCheck();
6544
- this.releaseIndexingLock();
6545
7216
  this.logger.info("Recovery complete, next index will re-process all files");
6546
7217
  }
6547
7218
  loadFailedBatches(maxChunkTokens) {
@@ -6557,10 +7228,10 @@ var Indexer = class {
6557
7228
  }
6558
7229
  }
6559
7230
  loadSerializedFailedBatches() {
6560
- if (!(0, import_fs7.existsSync)(this.failedBatchesPath)) {
7231
+ if (!(0, import_fs8.existsSync)(this.failedBatchesPath)) {
6561
7232
  return [];
6562
7233
  }
6563
- const data = (0, import_fs7.readFileSync)(this.failedBatchesPath, "utf-8");
7234
+ const data = (0, import_fs8.readFileSync)(this.failedBatchesPath, "utf-8");
6564
7235
  const parsed = JSON.parse(data);
6565
7236
  return parsed.map((batch) => {
6566
7237
  const chunks = Array.isArray(batch.chunks) ? batch.chunks : [];
@@ -6577,15 +7248,15 @@ var Indexer = class {
6577
7248
  }
6578
7249
  saveFailedBatches(batches) {
6579
7250
  if (batches.length === 0) {
6580
- if ((0, import_fs7.existsSync)(this.failedBatchesPath)) {
7251
+ if ((0, import_fs8.existsSync)(this.failedBatchesPath)) {
6581
7252
  try {
6582
- (0, import_fs7.unlinkSync)(this.failedBatchesPath);
7253
+ (0, import_fs8.unlinkSync)(this.failedBatchesPath);
6583
7254
  } catch {
6584
7255
  }
6585
7256
  }
6586
7257
  return;
6587
7258
  }
6588
- (0, import_fs7.writeFileSync)(this.failedBatchesPath, JSON.stringify(batches, null, 2));
7259
+ this.atomicWriteSync(this.failedBatchesPath, JSON.stringify(batches, null, 2));
6589
7260
  }
6590
7261
  collectRetryableFailedChunks(currentFileHashes, unchangedFilePaths, maxChunkTokens) {
6591
7262
  const retryableById = /* @__PURE__ */ new Map();
@@ -6765,7 +7436,7 @@ var Indexer = class {
6765
7436
  const intent = isLikelyImplementationPath(candidate.metadata.filePath) ? "implementation" : "doc_or_test";
6766
7437
  parts.push(`intent_hint: ${intent}`);
6767
7438
  try {
6768
- const fileContent = await import_fs7.promises.readFile(candidate.metadata.filePath, "utf-8");
7439
+ const fileContent = await import_fs8.promises.readFile(candidate.metadata.filePath, "utf-8");
6769
7440
  const lines = fileContent.split("\n");
6770
7441
  const snippetStartLine = Math.max(1, candidate.metadata.startLine - 2);
6771
7442
  const snippetEndLine = Math.min(lines.length, candidate.metadata.endLine + 2);
@@ -6778,7 +7449,223 @@ var Indexer = class {
6778
7449
  }
6779
7450
  return parts.join("\n");
6780
7451
  }
6781
- async initialize() {
7452
+ async initialize() {
7453
+ if (this.initializationPromise) {
7454
+ await this.initializationPromise;
7455
+ }
7456
+ if (this.isInitializedFor("reader")) {
7457
+ return;
7458
+ }
7459
+ await this.initializeOnce("reader", [], { skipAutoGc: true });
7460
+ }
7461
+ async initializeOnce(mode, recoveredOwners, options) {
7462
+ if (this.initializationPromise) {
7463
+ await this.initializationPromise;
7464
+ if (this.isInitializedFor(mode)) {
7465
+ return;
7466
+ }
7467
+ return this.initializeOnce(mode, recoveredOwners, options);
7468
+ }
7469
+ if (this.isInitializedFor(mode)) {
7470
+ return;
7471
+ }
7472
+ const initialization = this.initializeUnlocked(mode, recoveredOwners, options).catch((error) => {
7473
+ this.resetLoadedIndexState();
7474
+ throw error;
7475
+ }).finally(() => {
7476
+ if (this.initializationPromise === initialization) {
7477
+ this.initializationPromise = null;
7478
+ }
7479
+ });
7480
+ this.initializationPromise = initialization;
7481
+ await initialization;
7482
+ }
7483
+ isInitializedFor(mode) {
7484
+ const hasState = Boolean(
7485
+ this.store && this.provider && this.invertedIndex && this.configuredProviderInfo && this.database
7486
+ );
7487
+ if (!hasState) {
7488
+ return false;
7489
+ }
7490
+ return mode === "reader" ? this.initializationMode !== "none" : this.initializationMode === "writer";
7491
+ }
7492
+ recordReadIssue(component, message, error) {
7493
+ this.readIssues.push(this.createReadIssue(component, message));
7494
+ this.readerArtifactRetryAfter.set(component, Date.now() + READER_ARTIFACT_RETRY_INTERVAL_MS);
7495
+ this.logger.warn(message, error === void 0 ? void 0 : { error: getErrorMessage2(error) });
7496
+ }
7497
+ createReadIssue(component, message) {
7498
+ return {
7499
+ component,
7500
+ message,
7501
+ blocking: component !== "keyword"
7502
+ };
7503
+ }
7504
+ getVectorReadIssueMessage() {
7505
+ if (this.config.scope === "global") {
7506
+ return "Shared vector index could not be read. Restore or repair the complete fingerprinted shared vector artifacts; automatic reset is disabled for global scope.";
7507
+ }
7508
+ if (!this.isLocalProjectIndexPath()) {
7509
+ return "Vector index could not be read from an inherited project index. Restore or fingerprint it from the checkout that owns the index; do not remove or rebuild it from this worktree.";
7510
+ }
7511
+ return "Vector index could not be read. Run index_codebase after the active writer finishes to fingerprint a structurally valid legacy pair, or remove this checkout's local index directory and run index_codebase to rebuild it.";
7512
+ }
7513
+ getKeywordReadIssueMessage() {
7514
+ if (this.config.scope === "global") {
7515
+ return "Shared keyword index could not be read; semantic search remains available. Restore or repair the shared keyword artifact; automatic reset is disabled for global scope.";
7516
+ }
7517
+ if (!this.isLocalProjectIndexPath()) {
7518
+ return "Keyword index could not be read from an inherited project index; semantic search remains available. Restore or repair it from the checkout that owns the index; do not rebuild it from this worktree.";
7519
+ }
7520
+ return "Keyword index could not be read; semantic search remains available. Restore a readable published keyword index, or run index_codebase with force=true after the active writer finishes.";
7521
+ }
7522
+ getDatabaseReadIssueMessage() {
7523
+ if (this.config.scope === "global") {
7524
+ return "Shared index database could not be read. Restore or repair the shared SQLite database; automatic reset is disabled for global scope.";
7525
+ }
7526
+ if (!this.isLocalProjectIndexPath()) {
7527
+ return "Index database could not be read from an inherited project index. Restore or repair it from the checkout that owns the index; do not migrate or rebuild it from this worktree.";
7528
+ }
7529
+ return "Index database could not be read. Run index_codebase after the active writer finishes to repair or migrate it under the writer lease.";
7530
+ }
7531
+ getReaderFileFingerprint(filePath, identityOnly = false) {
7532
+ try {
7533
+ const stats = (0, import_fs8.statSync)(filePath);
7534
+ if (identityOnly) {
7535
+ return `${stats.dev}:${stats.ino}`;
7536
+ }
7537
+ return `${stats.dev}:${stats.ino}:${stats.size}:${stats.mtimeMs}:${stats.ctimeMs}`;
7538
+ } catch (error) {
7539
+ return `unavailable:${getErrorMessage2(error)}`;
7540
+ }
7541
+ }
7542
+ captureReaderArtifactFingerprint() {
7543
+ const storePath = path12.join(this.indexPath, "vectors");
7544
+ return {
7545
+ vectors: `${this.getReaderFileFingerprint(storePath)}|${this.getReaderFileFingerprint(`${storePath}.meta.json`)}`,
7546
+ keyword: this.getReaderFileFingerprint(path12.join(this.indexPath, "inverted-index.json")),
7547
+ database: this.getReaderFileFingerprint(path12.join(this.indexPath, "codebase.db")),
7548
+ databaseIdentity: this.getReaderFileFingerprint(path12.join(this.indexPath, "codebase.db"), true)
7549
+ };
7550
+ }
7551
+ refreshReaderArtifacts() {
7552
+ if (this.initializationMode !== "reader" || !this.configuredProviderInfo) {
7553
+ return;
7554
+ }
7555
+ const previousFingerprint = this.readerArtifactFingerprint;
7556
+ const currentFingerprint = this.captureReaderArtifactFingerprint();
7557
+ const issues = new Map(this.readIssues.map((issue) => [issue.component, issue]));
7558
+ const retryDue = (component) => issues.has(component) && Date.now() >= (this.readerArtifactRetryAfter.get(component) ?? 0);
7559
+ const vectorsChanged = !previousFingerprint || currentFingerprint.vectors !== previousFingerprint.vectors;
7560
+ const keywordChanged = !previousFingerprint || currentFingerprint.keyword !== previousFingerprint.keyword;
7561
+ const databaseChanged = !previousFingerprint || currentFingerprint.database !== previousFingerprint.database;
7562
+ const databaseReplaced = !previousFingerprint || currentFingerprint.databaseIdentity !== previousFingerprint.databaseIdentity;
7563
+ if (previousFingerprint && !vectorsChanged && !keywordChanged && !databaseChanged && !Array.from(issues.keys()).some(retryDue)) {
7564
+ return;
7565
+ }
7566
+ const setIssue = (component, message, error) => {
7567
+ if (!issues.has(component)) {
7568
+ this.logger.warn(message, error === void 0 ? void 0 : { error: getErrorMessage2(error) });
7569
+ }
7570
+ issues.set(component, this.createReadIssue(component, message));
7571
+ this.readerArtifactRetryAfter.set(component, Date.now() + READER_ARTIFACT_RETRY_INTERVAL_MS);
7572
+ };
7573
+ const storePath = path12.join(this.indexPath, "vectors");
7574
+ const vectorMetadataPath = `${storePath}.meta.json`;
7575
+ const invertedIndexPath = path12.join(this.indexPath, "inverted-index.json");
7576
+ const dbPath = path12.join(this.indexPath, "codebase.db");
7577
+ if (vectorsChanged || retryDue("vectors")) {
7578
+ const vectorStoreExists = (0, import_fs8.existsSync)(storePath);
7579
+ const vectorMetadataExists = (0, import_fs8.existsSync)(vectorMetadataPath);
7580
+ if (vectorStoreExists && vectorMetadataExists) {
7581
+ try {
7582
+ const store = new VectorStore(storePath, this.configuredProviderInfo.modelInfo.dimensions);
7583
+ store.loadStrict();
7584
+ this.store = store;
7585
+ issues.delete("vectors");
7586
+ this.readerArtifactRetryAfter.delete("vectors");
7587
+ } catch (error) {
7588
+ setIssue("vectors", this.getVectorReadIssueMessage(), error);
7589
+ }
7590
+ } else if (vectorStoreExists !== vectorMetadataExists || issues.has("vectors")) {
7591
+ setIssue("vectors", this.getVectorReadIssueMessage());
7592
+ }
7593
+ }
7594
+ if (keywordChanged || retryDue("keyword") || !(0, import_fs8.existsSync)(invertedIndexPath) && (this.store?.count() ?? 0) > 0) {
7595
+ if ((0, import_fs8.existsSync)(invertedIndexPath)) {
7596
+ try {
7597
+ const invertedIndex = new InvertedIndex(invertedIndexPath);
7598
+ invertedIndex.load();
7599
+ this.invertedIndex = invertedIndex;
7600
+ issues.delete("keyword");
7601
+ this.readerArtifactRetryAfter.delete("keyword");
7602
+ } catch (error) {
7603
+ setIssue("keyword", this.getKeywordReadIssueMessage(), error);
7604
+ }
7605
+ } else if ((this.store?.count() ?? 0) > 0 || issues.has("keyword")) {
7606
+ setIssue("keyword", this.getKeywordReadIssueMessage());
7607
+ }
7608
+ }
7609
+ if (databaseReplaced || databaseChanged && issues.has("database") || retryDue("database")) {
7610
+ if ((0, import_fs8.existsSync)(dbPath)) {
7611
+ try {
7612
+ const database = Database.openReadOnly(dbPath);
7613
+ if (this.database) {
7614
+ this.retiredDatabases.push(this.database);
7615
+ }
7616
+ this.database = database;
7617
+ issues.delete("database");
7618
+ this.readerArtifactRetryAfter.delete("database");
7619
+ } catch (error) {
7620
+ setIssue("database", this.getDatabaseReadIssueMessage(), error);
7621
+ }
7622
+ } else if ((this.store?.count() ?? 0) > 0 || issues.has("database")) {
7623
+ setIssue("database", this.getDatabaseReadIssueMessage());
7624
+ }
7625
+ }
7626
+ if (!issues.has("database")) {
7627
+ try {
7628
+ this.indexCompatibility = this.validateIndexCompatibility(this.configuredProviderInfo);
7629
+ } catch (error) {
7630
+ setIssue("database", this.getDatabaseReadIssueMessage(), error);
7631
+ }
7632
+ }
7633
+ this.readIssues = Array.from(issues.values());
7634
+ this.readerArtifactFingerprint = currentFingerprint;
7635
+ }
7636
+ refreshInactiveWriterArtifacts() {
7637
+ if (this.initializationMode !== "writer" || this.activeIndexLease) {
7638
+ return true;
7639
+ }
7640
+ const previousFingerprint = this.writerArtifactFingerprint;
7641
+ const currentFingerprint = this.captureReaderArtifactFingerprint();
7642
+ const retryDue = this.readIssues.some(
7643
+ (issue) => Date.now() >= (this.readerArtifactRetryAfter.get(issue.component) ?? 0)
7644
+ );
7645
+ const artifactsChanged = !previousFingerprint || currentFingerprint.vectors !== previousFingerprint.vectors || currentFingerprint.keyword !== previousFingerprint.keyword || currentFingerprint.database !== previousFingerprint.database || currentFingerprint.databaseIdentity !== previousFingerprint.databaseIdentity;
7646
+ if (!artifactsChanged && !retryDue) {
7647
+ return true;
7648
+ }
7649
+ if (!previousFingerprint || currentFingerprint.databaseIdentity !== previousFingerprint.databaseIdentity) {
7650
+ return false;
7651
+ }
7652
+ this.initializationMode = "reader";
7653
+ this.readerArtifactFingerprint = previousFingerprint;
7654
+ try {
7655
+ this.refreshReaderArtifacts();
7656
+ this.writerArtifactFingerprint = this.readerArtifactFingerprint ?? currentFingerprint;
7657
+ } finally {
7658
+ this.readerArtifactFingerprint = null;
7659
+ this.initializationMode = "writer";
7660
+ }
7661
+ return true;
7662
+ }
7663
+ async initializeUnlocked(mode, recoveredOwners = [], options = {}) {
7664
+ if (mode === "writer") {
7665
+ this.requireActiveLease();
7666
+ }
7667
+ this.readIssues = [];
7668
+ this.readerArtifactRetryAfter.clear();
6782
7669
  if (this.config.embeddingProvider === "custom") {
6783
7670
  if (!this.config.customProvider) {
6784
7671
  throw new Error("embeddingProvider is 'custom' but customProvider config is missing.");
@@ -6810,36 +7697,103 @@ var Indexer = class {
6810
7697
  });
6811
7698
  }
6812
7699
  }
6813
- await import_fs7.promises.mkdir(this.indexPath, { recursive: true });
6814
7700
  const dimensions = this.configuredProviderInfo.modelInfo.dimensions;
6815
- const storePath = path10.join(this.indexPath, "vectors");
6816
- this.store = new VectorStore(storePath, dimensions);
6817
- const indexFilePath = path10.join(this.indexPath, "vectors.usearch");
6818
- if ((0, import_fs7.existsSync)(indexFilePath)) {
6819
- this.store.load();
6820
- }
6821
- const invertedIndexPath = path10.join(this.indexPath, "inverted-index.json");
6822
- this.invertedIndex = new InvertedIndex(invertedIndexPath);
6823
- try {
6824
- this.invertedIndex.load();
6825
- } catch {
6826
- if ((0, import_fs7.existsSync)(invertedIndexPath)) {
6827
- await import_fs7.promises.unlink(invertedIndexPath);
7701
+ const storePath = path12.join(this.indexPath, "vectors");
7702
+ const vectorMetadataPath = `${storePath}.meta.json`;
7703
+ const invertedIndexPath = path12.join(this.indexPath, "inverted-index.json");
7704
+ const dbPath = path12.join(this.indexPath, "codebase.db");
7705
+ let dbIsNew = !(0, import_fs8.existsSync)(dbPath);
7706
+ const readerArtifactFingerprint = mode === "reader" ? this.captureReaderArtifactFingerprint() : null;
7707
+ if (mode === "writer") {
7708
+ await import_fs8.promises.mkdir(this.indexPath, { recursive: true });
7709
+ if (recoveredOwners.length > 0 && this.config.scope === "project" && !this.isLocalProjectIndexPath()) {
7710
+ throw new Error(
7711
+ "Interrupted indexing recovery is unsafe while using an inherited worktree index. Run index_codebase with force=true to create a local project index boundary."
7712
+ );
7713
+ }
7714
+ for (const recoveredOwner of recoveredOwners) {
7715
+ recoverLeaseArtifacts(this.indexPath, recoveredOwner, [
7716
+ storePath,
7717
+ `${storePath}.meta.json`
7718
+ ]);
7719
+ }
7720
+ if (recoveredOwners.length > 0 && this.config.scope === "project") {
7721
+ await this.resetLocalIndexArtifacts();
7722
+ }
7723
+ this.store = new VectorStore(storePath, dimensions);
7724
+ if ((0, import_fs8.existsSync)(storePath) || (0, import_fs8.existsSync)(vectorMetadataPath)) {
7725
+ this.store.load();
6828
7726
  }
6829
7727
  this.invertedIndex = new InvertedIndex(invertedIndexPath);
6830
- }
6831
- const dbPath = path10.join(this.indexPath, "codebase.db");
6832
- let dbIsNew = !(0, import_fs7.existsSync)(dbPath);
6833
- try {
6834
- this.database = new Database(dbPath);
6835
- } catch (error) {
6836
- if (!await this.tryResetCorruptedIndex("initializing index database", error)) {
6837
- throw error;
7728
+ try {
7729
+ this.invertedIndex.load();
7730
+ } catch {
7731
+ if ((0, import_fs8.existsSync)(invertedIndexPath)) {
7732
+ await import_fs8.promises.unlink(invertedIndexPath);
7733
+ }
7734
+ this.invertedIndex = new InvertedIndex(invertedIndexPath);
7735
+ }
7736
+ try {
7737
+ this.database = new Database(dbPath);
7738
+ } catch (error) {
7739
+ if (!await this.tryResetCorruptedIndex("initializing index database", error)) {
7740
+ throw error;
7741
+ }
7742
+ this.store = new VectorStore(storePath, dimensions);
7743
+ this.invertedIndex = new InvertedIndex(invertedIndexPath);
7744
+ this.database = new Database(dbPath);
7745
+ dbIsNew = true;
6838
7746
  }
7747
+ } else {
6839
7748
  this.store = new VectorStore(storePath, dimensions);
7749
+ const vectorStoreExists = (0, import_fs8.existsSync)(storePath);
7750
+ const vectorMetadataExists = (0, import_fs8.existsSync)(vectorMetadataPath);
7751
+ const vectorReadFailureMessage = this.getVectorReadIssueMessage();
7752
+ if (vectorStoreExists !== vectorMetadataExists) {
7753
+ this.recordReadIssue("vectors", vectorReadFailureMessage);
7754
+ } else if (vectorStoreExists) {
7755
+ try {
7756
+ this.store.loadStrict();
7757
+ } catch (error) {
7758
+ this.recordReadIssue("vectors", vectorReadFailureMessage, error);
7759
+ this.store = new VectorStore(storePath, dimensions);
7760
+ }
7761
+ }
6840
7762
  this.invertedIndex = new InvertedIndex(invertedIndexPath);
6841
- this.database = new Database(dbPath);
6842
- dbIsNew = true;
7763
+ if ((0, import_fs8.existsSync)(invertedIndexPath)) {
7764
+ try {
7765
+ this.invertedIndex.load();
7766
+ } catch (error) {
7767
+ this.recordReadIssue(
7768
+ "keyword",
7769
+ this.getKeywordReadIssueMessage(),
7770
+ error
7771
+ );
7772
+ this.invertedIndex = new InvertedIndex(invertedIndexPath);
7773
+ }
7774
+ } else if (this.store.count() > 0) {
7775
+ this.recordReadIssue("keyword", this.getKeywordReadIssueMessage());
7776
+ }
7777
+ if ((0, import_fs8.existsSync)(dbPath)) {
7778
+ try {
7779
+ this.database = Database.openReadOnly(dbPath);
7780
+ } catch (error) {
7781
+ this.recordReadIssue(
7782
+ "database",
7783
+ this.getDatabaseReadIssueMessage(),
7784
+ error
7785
+ );
7786
+ this.database = Database.createEmptyReadOnly();
7787
+ }
7788
+ } else {
7789
+ this.database = Database.createEmptyReadOnly();
7790
+ if (this.store.count() > 0) {
7791
+ this.recordReadIssue(
7792
+ "database",
7793
+ `Index database is missing for the published vectors. ${this.getDatabaseReadIssueMessage()}`
7794
+ );
7795
+ }
7796
+ }
6843
7797
  }
6844
7798
  if (isGitRepo(this.projectRoot)) {
6845
7799
  this.currentBranch = getBranchOrDefault(this.projectRoot);
@@ -6853,10 +7807,10 @@ var Indexer = class {
6853
7807
  this.baseBranch = "default";
6854
7808
  this.logger.branch("debug", "Not a git repository, using default branch");
6855
7809
  }
6856
- if (this.checkForInterruptedIndexing()) {
6857
- await this.recoverFromInterruptedIndexing();
7810
+ if (mode === "writer" && recoveredOwners.length > 0) {
7811
+ await this.recoverFromInterruptedIndexingUnlocked(recoveredOwners);
6858
7812
  }
6859
- if (dbIsNew && this.store.count() > 0) {
7813
+ if (mode === "writer" && dbIsNew && this.store.count() > 0) {
6860
7814
  this.migrateFromLegacyIndex();
6861
7815
  }
6862
7816
  this.loadFileHashCache();
@@ -6868,9 +7822,11 @@ var Indexer = class {
6868
7822
  configuredProviderInfo: this.configuredProviderInfo
6869
7823
  });
6870
7824
  }
6871
- if (this.config.indexing.autoGc) {
7825
+ if (mode === "writer" && this.config.indexing.autoGc && !options.skipAutoGc) {
6872
7826
  await this.maybeRunAutoGc();
6873
7827
  }
7828
+ this.initializationMode = mode;
7829
+ this.readerArtifactFingerprint = readerArtifactFingerprint;
6874
7830
  }
6875
7831
  async maybeRunAutoGc() {
6876
7832
  if (!this.database) return;
@@ -6887,7 +7843,7 @@ var Indexer = class {
6887
7843
  }
6888
7844
  }
6889
7845
  if (shouldRunGc) {
6890
- const result = await this.healthCheck();
7846
+ const result = await this.healthCheckUnlocked();
6891
7847
  if (result.warning) {
6892
7848
  this.database.setMetadata(STARTUP_WARNING_METADATA_KEY, result.warning);
6893
7849
  } else {
@@ -6909,7 +7865,7 @@ var Indexer = class {
6909
7865
  if (await this.tryResetCorruptedIndex("running automatic orphan garbage collection", error)) {
6910
7866
  return {
6911
7867
  resetCorruptedIndex: true,
6912
- warning: this.getCorruptedIndexWarning(path10.join(this.indexPath, "codebase.db"))
7868
+ warning: this.getCorruptedIndexWarning(path12.join(this.indexPath, "codebase.db"))
6913
7869
  };
6914
7870
  }
6915
7871
  throw error;
@@ -6924,28 +7880,29 @@ var Indexer = class {
6924
7880
  return;
6925
7881
  }
6926
7882
  const retainedEntries = store.getAllMetadata().filter(({ key }) => !excludedSet.has(key));
6927
- const storeBasePath = path10.join(this.indexPath, "vectors");
6928
- const storeIndexPath = `${storeBasePath}.usearch`;
7883
+ const storeBasePath = path12.join(this.indexPath, "vectors");
7884
+ const storeIndexPath = storeBasePath;
6929
7885
  const storeMetadataPath = `${storeBasePath}.meta.json`;
6930
- const backupIndexPath = `${storeIndexPath}.bak`;
6931
- const backupMetadataPath = `${storeMetadataPath}.bak`;
7886
+ const lease = this.requireActiveLease();
7887
+ const backupIndexPath = createLeaseTemporaryPath(storeIndexPath, lease.owner, "bak");
7888
+ const backupMetadataPath = createLeaseTemporaryPath(storeMetadataPath, lease.owner, "bak");
6932
7889
  let backedUpIndex = false;
6933
7890
  let backedUpMetadata = false;
6934
7891
  let rebuiltCount = 0;
6935
7892
  let skippedCount = 0;
6936
- if ((0, import_fs7.existsSync)(backupIndexPath)) {
6937
- (0, import_fs7.unlinkSync)(backupIndexPath);
7893
+ if ((0, import_fs8.existsSync)(backupIndexPath)) {
7894
+ (0, import_fs8.unlinkSync)(backupIndexPath);
6938
7895
  }
6939
- if ((0, import_fs7.existsSync)(backupMetadataPath)) {
6940
- (0, import_fs7.unlinkSync)(backupMetadataPath);
7896
+ if ((0, import_fs8.existsSync)(backupMetadataPath)) {
7897
+ (0, import_fs8.unlinkSync)(backupMetadataPath);
6941
7898
  }
6942
7899
  try {
6943
- if ((0, import_fs7.existsSync)(storeIndexPath)) {
6944
- (0, import_fs7.renameSync)(storeIndexPath, backupIndexPath);
7900
+ if ((0, import_fs8.existsSync)(storeIndexPath)) {
7901
+ (0, import_fs8.renameSync)(storeIndexPath, backupIndexPath);
6945
7902
  backedUpIndex = true;
6946
7903
  }
6947
- if ((0, import_fs7.existsSync)(storeMetadataPath)) {
6948
- (0, import_fs7.renameSync)(storeMetadataPath, backupMetadataPath);
7904
+ if ((0, import_fs8.existsSync)(storeMetadataPath)) {
7905
+ (0, import_fs8.renameSync)(storeMetadataPath, backupMetadataPath);
6949
7906
  backedUpMetadata = true;
6950
7907
  }
6951
7908
  store.clear();
@@ -6965,11 +7922,11 @@ var Indexer = class {
6965
7922
  rebuiltCount += 1;
6966
7923
  }
6967
7924
  store.save();
6968
- if (backedUpIndex && (0, import_fs7.existsSync)(backupIndexPath)) {
6969
- (0, import_fs7.unlinkSync)(backupIndexPath);
7925
+ if (backedUpIndex && (0, import_fs8.existsSync)(backupIndexPath)) {
7926
+ (0, import_fs8.unlinkSync)(backupIndexPath);
6970
7927
  }
6971
- if (backedUpMetadata && (0, import_fs7.existsSync)(backupMetadataPath)) {
6972
- (0, import_fs7.unlinkSync)(backupMetadataPath);
7928
+ if (backedUpMetadata && (0, import_fs8.existsSync)(backupMetadataPath)) {
7929
+ (0, import_fs8.unlinkSync)(backupMetadataPath);
6973
7930
  }
6974
7931
  this.logger.gc("info", "Rebuilt vector store to avoid native remove", {
6975
7932
  excludedChunks: excludedSet.size,
@@ -6981,17 +7938,17 @@ var Indexer = class {
6981
7938
  store.clear();
6982
7939
  } catch {
6983
7940
  }
6984
- if ((0, import_fs7.existsSync)(storeIndexPath)) {
6985
- (0, import_fs7.unlinkSync)(storeIndexPath);
7941
+ if ((0, import_fs8.existsSync)(storeIndexPath)) {
7942
+ (0, import_fs8.unlinkSync)(storeIndexPath);
6986
7943
  }
6987
- if ((0, import_fs7.existsSync)(storeMetadataPath)) {
6988
- (0, import_fs7.unlinkSync)(storeMetadataPath);
7944
+ if ((0, import_fs8.existsSync)(storeMetadataPath)) {
7945
+ (0, import_fs8.unlinkSync)(storeMetadataPath);
6989
7946
  }
6990
- if (backedUpIndex && (0, import_fs7.existsSync)(backupIndexPath)) {
6991
- (0, import_fs7.renameSync)(backupIndexPath, storeIndexPath);
7947
+ if (backedUpIndex && (0, import_fs8.existsSync)(backupIndexPath)) {
7948
+ (0, import_fs8.renameSync)(backupIndexPath, storeIndexPath);
6992
7949
  }
6993
- if (backedUpMetadata && (0, import_fs7.existsSync)(backupMetadataPath)) {
6994
- (0, import_fs7.renameSync)(backupMetadataPath, storeMetadataPath);
7950
+ if (backedUpMetadata && (0, import_fs8.existsSync)(backupMetadataPath)) {
7951
+ (0, import_fs8.renameSync)(backupMetadataPath, storeMetadataPath);
6995
7952
  }
6996
7953
  if (backedUpIndex || backedUpMetadata) {
6997
7954
  store.load();
@@ -7005,11 +7962,37 @@ var Indexer = class {
7005
7962
  }
7006
7963
  return `Detected a corrupted local SQLite index at ${dbPath} and reset the local index. Run index_codebase to rebuild search data.`;
7007
7964
  }
7965
+ async resetLocalIndexArtifacts() {
7966
+ this.store = null;
7967
+ this.invertedIndex = null;
7968
+ this.database?.close();
7969
+ this.database = null;
7970
+ this.indexCompatibility = null;
7971
+ this.initializationMode = "none";
7972
+ this.readIssues = [];
7973
+ this.readerArtifactFingerprint = null;
7974
+ this.writerArtifactFingerprint = null;
7975
+ this.readerArtifactRetryAfter.clear();
7976
+ this.fileHashCache.clear();
7977
+ const resetPaths = [
7978
+ path12.join(this.indexPath, "codebase.db"),
7979
+ path12.join(this.indexPath, "codebase.db-shm"),
7980
+ path12.join(this.indexPath, "codebase.db-wal"),
7981
+ path12.join(this.indexPath, "vectors"),
7982
+ path12.join(this.indexPath, "vectors.usearch"),
7983
+ path12.join(this.indexPath, "vectors.meta.json"),
7984
+ path12.join(this.indexPath, "inverted-index.json"),
7985
+ path12.join(this.indexPath, "file-hashes.json"),
7986
+ path12.join(this.indexPath, "failed-batches.json")
7987
+ ];
7988
+ await Promise.all(resetPaths.map((targetPath) => import_fs8.promises.rm(targetPath, { recursive: true, force: true })));
7989
+ await import_fs8.promises.mkdir(this.indexPath, { recursive: true });
7990
+ }
7008
7991
  async tryResetCorruptedIndex(stage, error) {
7009
7992
  if (!isSqliteCorruptionError(error)) {
7010
7993
  return false;
7011
7994
  }
7012
- const dbPath = path10.join(this.indexPath, "codebase.db");
7995
+ const dbPath = path12.join(this.indexPath, "codebase.db");
7013
7996
  const warning = this.getCorruptedIndexWarning(dbPath);
7014
7997
  const errorMessage = getErrorMessage2(error);
7015
7998
  if (this.config.scope === "global") {
@@ -7025,30 +8008,7 @@ var Indexer = class {
7025
8008
  dbPath,
7026
8009
  error: errorMessage
7027
8010
  });
7028
- this.store = null;
7029
- this.invertedIndex = null;
7030
- this.database?.close();
7031
- this.database = null;
7032
- this.indexCompatibility = null;
7033
- this.fileHashCache.clear();
7034
- const resetPaths = [
7035
- path10.join(this.indexPath, "codebase.db"),
7036
- path10.join(this.indexPath, "codebase.db-shm"),
7037
- path10.join(this.indexPath, "codebase.db-wal"),
7038
- path10.join(this.indexPath, "vectors.usearch"),
7039
- path10.join(this.indexPath, "inverted-index.json"),
7040
- path10.join(this.indexPath, "file-hashes.json"),
7041
- path10.join(this.indexPath, "failed-batches.json"),
7042
- path10.join(this.indexPath, "indexing.lock"),
7043
- path10.join(this.indexPath, "vectors")
7044
- ];
7045
- await Promise.all(resetPaths.map(async (targetPath) => {
7046
- try {
7047
- await import_fs7.promises.rm(targetPath, { recursive: true, force: true });
7048
- } catch {
7049
- }
7050
- }));
7051
- await import_fs7.promises.mkdir(this.indexPath, { recursive: true });
8011
+ await this.resetLocalIndexArtifacts();
7052
8012
  return true;
7053
8013
  }
7054
8014
  migrateFromLegacyIndex() {
@@ -7167,8 +8127,62 @@ var Indexer = class {
7167
8127
  return this.indexCompatibility;
7168
8128
  }
7169
8129
  async ensureInitialized() {
8130
+ let initializedReader = false;
8131
+ while (true) {
8132
+ if (this.initializationPromise) {
8133
+ await this.initializationPromise;
8134
+ }
8135
+ if (!this.isInitializedFor("reader")) {
8136
+ await this.initialize();
8137
+ initializedReader = true;
8138
+ continue;
8139
+ }
8140
+ if (this.initializationMode === "writer" && !this.activeIndexLease && !initializedReader) {
8141
+ if (!this.refreshInactiveWriterArtifacts()) {
8142
+ this.resetLoadedIndexState(true);
8143
+ await this.initialize();
8144
+ initializedReader = true;
8145
+ continue;
8146
+ }
8147
+ }
8148
+ if (this.initializationMode === "reader" && !initializedReader) {
8149
+ this.refreshReaderArtifacts();
8150
+ }
8151
+ const state = this.requireLoadedIndexState();
8152
+ return {
8153
+ ...state,
8154
+ readIssues: [...this.readIssues],
8155
+ compatibility: this.indexCompatibility ?? this.validateIndexCompatibility(state.configuredProviderInfo)
8156
+ };
8157
+ }
8158
+ }
8159
+ async ensureInitializedUnlocked(recoveredOwners = []) {
8160
+ this.requireActiveLease();
8161
+ if (this.initializationPromise) {
8162
+ await this.initializationPromise;
8163
+ }
8164
+ if (recoveredOwners.length > 0 || !this.isInitializedFor("writer")) {
8165
+ const retireReaderDatabase = this.initializationMode === "reader";
8166
+ this.resetLoadedIndexState(retireReaderDatabase);
8167
+ await this.initializeOnce("writer", recoveredOwners, { skipAutoGc: true });
8168
+ } else {
8169
+ this.refreshLoadedIndexState();
8170
+ }
8171
+ if (this.config.indexing.autoGc) {
8172
+ await this.maybeRunAutoGc();
8173
+ }
8174
+ return this.requireLoadedIndexState();
8175
+ }
8176
+ requireReadableComponents(readIssues, ...components) {
8177
+ const componentSet = new Set(components);
8178
+ const issues = readIssues.filter((issue) => issue.blocking && componentSet.has(issue.component));
8179
+ if (issues.length > 0) {
8180
+ throw new Error(issues.map((issue) => issue.message).join(" "));
8181
+ }
8182
+ }
8183
+ requireLoadedIndexState() {
7170
8184
  if (!this.store || !this.provider || !this.invertedIndex || !this.configuredProviderInfo || !this.database) {
7171
- await this.initialize();
8185
+ throw new Error("Index state is not initialized");
7172
8186
  }
7173
8187
  return {
7174
8188
  store: this.store,
@@ -7192,7 +8206,12 @@ var Indexer = class {
7192
8206
  return createCostEstimate(files, configuredProviderInfo);
7193
8207
  }
7194
8208
  async index(onProgress) {
7195
- const { store, provider, invertedIndex, database, configuredProviderInfo } = await this.ensureInitialized();
8209
+ return this.withIndexMutationLease("index", async (recoveredOwners) => {
8210
+ return this.indexUnlocked(onProgress, recoveredOwners);
8211
+ });
8212
+ }
8213
+ async indexUnlocked(onProgress, recoveredOwners = [], stateReady = false) {
8214
+ const { store, provider, invertedIndex, database, configuredProviderInfo } = stateReady ? this.requireLoadedIndexState() : await this.ensureInitializedUnlocked(recoveredOwners);
7196
8215
  const scopedRoots = this.config.scope === "global" ? this.getScopedRoots() : null;
7197
8216
  const branchCatalogKey = this.getBranchCatalogKey();
7198
8217
  const forceScopedReembed = scopedRoots !== null && database.getMetadata(this.getProjectForceReembedMetadataKey()) === "true";
@@ -7202,7 +8221,6 @@ var Indexer = class {
7202
8221
  `${this.indexCompatibility?.reason} Run index_codebase with force=true to rebuild the index.`
7203
8222
  );
7204
8223
  }
7205
- this.acquireIndexingLock();
7206
8224
  this.logger.recordIndexingStart();
7207
8225
  this.logger.info("Starting indexing", { projectRoot: this.projectRoot });
7208
8226
  const startTime = Date.now();
@@ -7253,7 +8271,7 @@ var Indexer = class {
7253
8271
  unchangedFilePaths.add(f.path);
7254
8272
  this.logger.recordCacheHit();
7255
8273
  } else {
7256
- const content = await import_fs7.promises.readFile(f.path, "utf-8");
8274
+ const content = await import_fs8.promises.readFile(f.path, "utf-8");
7257
8275
  changedFiles.push({ path: f.path, content, hash: currentHash });
7258
8276
  this.logger.recordCacheMiss();
7259
8277
  }
@@ -7277,6 +8295,7 @@ var Indexer = class {
7277
8295
  this.logger.debug("Parsed changed files", { parsedCount: parsedFiles.length, parseMs: parseMs.toFixed(2) });
7278
8296
  const existingChunks = /* @__PURE__ */ new Map();
7279
8297
  const existingChunksByFile = /* @__PURE__ */ new Map();
8298
+ const existingMetadataById = /* @__PURE__ */ new Map();
7280
8299
  for (const { key, metadata } of store.getAllMetadata()) {
7281
8300
  if (scopedRoots && !this.isFileInCurrentScope(metadata.filePath, scopedRoots)) {
7282
8301
  continue;
@@ -7285,6 +8304,7 @@ var Indexer = class {
7285
8304
  continue;
7286
8305
  }
7287
8306
  existingChunks.set(key, metadata.hash);
8307
+ existingMetadataById.set(key, metadata);
7288
8308
  const fileChunks = existingChunksByFile.get(metadata.filePath) || /* @__PURE__ */ new Set();
7289
8309
  fileChunks.add(key);
7290
8310
  existingChunksByFile.set(metadata.filePath, fileChunks);
@@ -7292,6 +8312,8 @@ var Indexer = class {
7292
8312
  const currentChunkIds = /* @__PURE__ */ new Set();
7293
8313
  const currentFilePaths = /* @__PURE__ */ new Set();
7294
8314
  const pendingChunks = [];
8315
+ const gitBlameEnabled = this.config.indexing.gitBlame.enabled && isGitRepo(this.projectRoot);
8316
+ let backfilledBlameMetadata = false;
7295
8317
  for (const filePath of unchangedFilePaths) {
7296
8318
  currentFilePaths.add(filePath);
7297
8319
  const fileChunks = existingChunksByFile.get(filePath);
@@ -7302,10 +8324,52 @@ var Indexer = class {
7302
8324
  }
7303
8325
  }
7304
8326
  const chunkDataBatch = [];
8327
+ if (gitBlameEnabled) {
8328
+ const backfillItems = [];
8329
+ for (const chunkId of currentChunkIds) {
8330
+ const metadata = existingMetadataById.get(chunkId);
8331
+ if (!metadata || hasBlameMetadata(metadata)) {
8332
+ continue;
8333
+ }
8334
+ const chunk = database.getChunk(chunkId);
8335
+ if (!chunk) {
8336
+ continue;
8337
+ }
8338
+ const blame = await getChunkGitBlame(this.projectRoot, chunk.filePath, chunk.startLine, chunk.endLine);
8339
+ const blameMetadata = metadataFromBlame(blame);
8340
+ if (!blameMetadata.blameSha) {
8341
+ continue;
8342
+ }
8343
+ chunkDataBatch.push({
8344
+ ...chunk,
8345
+ blameSha: blameMetadata.blameSha,
8346
+ blameAuthor: blameMetadata.blameAuthor,
8347
+ blameAuthorEmail: blameMetadata.blameAuthorEmail,
8348
+ blameCommittedAt: blameMetadata.blameCommittedAt,
8349
+ blameSummary: blameMetadata.blameSummary
8350
+ });
8351
+ const embeddingBuffer = database.getEmbedding(chunk.contentHash);
8352
+ if (!embeddingBuffer) {
8353
+ continue;
8354
+ }
8355
+ backfillItems.push({
8356
+ id: chunkId,
8357
+ vector: Array.from(bufferToFloat32Array(embeddingBuffer)),
8358
+ metadata: {
8359
+ ...metadata,
8360
+ ...blameMetadata
8361
+ }
8362
+ });
8363
+ }
8364
+ if (backfillItems.length > 0) {
8365
+ store.addBatch(backfillItems);
8366
+ backfilledBlameMetadata = true;
8367
+ }
8368
+ }
7305
8369
  for (const parsed of parsedFiles) {
7306
8370
  currentFilePaths.add(parsed.path);
7307
8371
  if (parsed.chunks.length === 0) {
7308
- const relativePath = path10.relative(this.projectRoot, parsed.path);
8372
+ const relativePath = path12.relative(this.projectRoot, parsed.path);
7309
8373
  stats.parseFailures.push(relativePath);
7310
8374
  }
7311
8375
  let fileChunkCount = 0;
@@ -7326,6 +8390,10 @@ var Indexer = class {
7326
8390
  }
7327
8391
  const id = generateChunkId(parsed.path, chunk);
7328
8392
  const contentHash = generateChunkHash(chunk);
8393
+ const existingContentHash = existingChunks.get(id);
8394
+ const existingChunk = gitBlameEnabled ? database.getChunk(id) : null;
8395
+ const blame = gitBlameEnabled && existingContentHash !== contentHash ? await getChunkGitBlame(this.projectRoot, parsed.path, chunk.startLine, chunk.endLine) : blameFromChunkData(existingChunk);
8396
+ const blameMetadata = metadataFromBlame(blame);
7329
8397
  currentChunkIds.add(id);
7330
8398
  chunkDataBatch.push({
7331
8399
  chunkId: id,
@@ -7335,9 +8403,14 @@ var Indexer = class {
7335
8403
  endLine: chunk.endLine,
7336
8404
  nodeType: chunk.chunkType,
7337
8405
  name: chunk.name,
7338
- language: chunk.language
8406
+ language: chunk.language,
8407
+ blameSha: blameMetadata.blameSha,
8408
+ blameAuthor: blameMetadata.blameAuthor,
8409
+ blameAuthorEmail: blameMetadata.blameAuthorEmail,
8410
+ blameCommittedAt: blameMetadata.blameCommittedAt,
8411
+ blameSummary: blameMetadata.blameSummary
7339
8412
  });
7340
- if (existingChunks.get(id) === contentHash) {
8413
+ if (existingContentHash === contentHash) {
7341
8414
  fileChunkCount++;
7342
8415
  continue;
7343
8416
  }
@@ -7352,7 +8425,8 @@ var Indexer = class {
7352
8425
  chunkType: chunk.chunkType,
7353
8426
  name: chunk.name,
7354
8427
  language: chunk.language,
7355
- hash: contentHash
8428
+ hash: contentHash,
8429
+ ...blameMetadata
7356
8430
  };
7357
8431
  pendingChunks.push({
7358
8432
  id,
@@ -7495,6 +8569,11 @@ var Indexer = class {
7495
8569
  database.addChunksToBranchBatch(branchCatalogKey, Array.from(currentChunkIds));
7496
8570
  database.clearBranchSymbols(branchCatalogKey);
7497
8571
  database.addSymbolsToBranchBatch(branchCatalogKey, Array.from(allSymbolIds));
8572
+ const vectorPath = path12.join(this.indexPath, "vectors");
8573
+ const shouldFingerprintLegacyPair = !store.hasFingerprint() && (0, import_fs8.existsSync)(vectorPath) && (0, import_fs8.existsSync)(`${vectorPath}.meta.json`);
8574
+ if (backfilledBlameMetadata || shouldFingerprintLegacyPair) {
8575
+ store.save();
8576
+ }
7498
8577
  if (scopedRoots) {
7499
8578
  this.replaceScopedFileHashCache(currentFileHashes, scopedRoots);
7500
8579
  this.clearScopedFailedBatches(scopedRoots);
@@ -7513,7 +8592,6 @@ var Indexer = class {
7513
8592
  chunksProcessed: 0,
7514
8593
  totalChunks: 0
7515
8594
  });
7516
- this.releaseIndexingLock();
7517
8595
  return stats;
7518
8596
  }
7519
8597
  if (pendingChunks.length === 0) {
@@ -7522,7 +8600,7 @@ var Indexer = class {
7522
8600
  database.clearBranchSymbols(branchCatalogKey);
7523
8601
  database.addSymbolsToBranchBatch(branchCatalogKey, Array.from(allSymbolIds));
7524
8602
  store.save();
7525
- invertedIndex.save();
8603
+ this.saveInvertedIndex(invertedIndex);
7526
8604
  if (scopedRoots) {
7527
8605
  this.replaceScopedFileHashCache(currentFileHashes, scopedRoots);
7528
8606
  this.clearScopedFailedBatches(scopedRoots);
@@ -7541,7 +8619,6 @@ var Indexer = class {
7541
8619
  chunksProcessed: 0,
7542
8620
  totalChunks: 0
7543
8621
  });
7544
- this.releaseIndexingLock();
7545
8622
  return stats;
7546
8623
  }
7547
8624
  onProgress?.({
@@ -7772,7 +8849,7 @@ var Indexer = class {
7772
8849
  database.clearBranchSymbols(branchCatalogKey);
7773
8850
  database.addSymbolsToBranchBatch(branchCatalogKey, Array.from(allSymbolIds));
7774
8851
  store.save();
7775
- invertedIndex.save();
8852
+ this.saveInvertedIndex(invertedIndex);
7776
8853
  if (scopedRoots) {
7777
8854
  this.replaceScopedFileHashCache(currentFileHashes, scopedRoots);
7778
8855
  } else {
@@ -7824,7 +8901,6 @@ var Indexer = class {
7824
8901
  chunksProcessed: stats.indexedChunks,
7825
8902
  totalChunks: pendingChunks.length
7826
8903
  });
7827
- this.releaseIndexingLock();
7828
8904
  return stats;
7829
8905
  }
7830
8906
  async getQueryEmbedding(query, provider) {
@@ -7890,8 +8966,8 @@ var Indexer = class {
7890
8966
  return intersection / union;
7891
8967
  }
7892
8968
  async search(query, limit, options) {
7893
- const { store, provider, database } = await this.ensureInitialized();
7894
- const compatibility = this.checkCompatibility();
8969
+ const { store, provider, invertedIndex, database, readIssues, compatibility } = await this.ensureInitialized();
8970
+ this.requireReadableComponents(readIssues, "vectors", "database");
7895
8971
  if (!compatibility.compatible) {
7896
8972
  throw new Error(
7897
8973
  `${compatibility.reason ?? "Index is incompatible with current embedding provider."} A possible solution is to run index_codebase with force=true to rebuild the index.`
@@ -7905,6 +8981,7 @@ var Indexer = class {
7905
8981
  const maxResults = limit ?? this.config.search.maxResults;
7906
8982
  const hybridWeight = options?.hybridWeight ?? this.config.search.hybridWeight;
7907
8983
  const fusionStrategy = this.config.search.fusionStrategy;
8984
+ const effectiveHybridWeight = fusionStrategy === "weighted" && readIssues.some((issue) => issue.component === "keyword") ? 0 : hybridWeight;
7908
8985
  const rrfK = this.config.search.rrfK;
7909
8986
  const rerankTopN = this.config.search.rerankTopN;
7910
8987
  const filterByBranch = options?.filterByBranch ?? true;
@@ -7913,7 +8990,7 @@ var Indexer = class {
7913
8990
  this.logger.search("debug", "Starting search", {
7914
8991
  query,
7915
8992
  maxResults,
7916
- hybridWeight,
8993
+ hybridWeight: effectiveHybridWeight,
7917
8994
  fusionStrategy,
7918
8995
  rrfK,
7919
8996
  rerankTopN,
@@ -7927,7 +9004,7 @@ var Indexer = class {
7927
9004
  const semanticResults = store.search(embedding, maxResults * 4);
7928
9005
  const vectorMs = import_perf_hooks.performance.now() - vectorStartTime;
7929
9006
  const keywordStartTime = import_perf_hooks.performance.now();
7930
- const keywordResults = await this.keywordSearch(query, maxResults * 4);
9007
+ const keywordResults = await this.keywordSearch(query, maxResults * 4, store, invertedIndex);
7931
9008
  const keywordMs = import_perf_hooks.performance.now() - keywordStartTime;
7932
9009
  let branchChunkIds = null;
7933
9010
  if (filterByBranch && (this.config.scope === "global" || this.currentBranch !== "default")) {
@@ -7964,7 +9041,7 @@ var Indexer = class {
7964
9041
  rrfK,
7965
9042
  rerankTopN,
7966
9043
  limit: maxResults,
7967
- hybridWeight,
9044
+ hybridWeight: effectiveHybridWeight,
7968
9045
  prioritizeSourcePaths: sourceIntent
7969
9046
  });
7970
9047
  const rerankedCombined = await this.rerankCandidatesWithApi(query, combined, {
@@ -8038,7 +9115,7 @@ var Indexer = class {
8038
9115
  let contextEndLine = r.metadata.endLine;
8039
9116
  if (!metadataOnly && this.config.search.includeContext) {
8040
9117
  try {
8041
- const fileContent = await import_fs7.promises.readFile(
9118
+ const fileContent = await import_fs8.promises.readFile(
8042
9119
  r.metadata.filePath,
8043
9120
  "utf-8"
8044
9121
  );
@@ -8058,13 +9135,13 @@ var Indexer = class {
8058
9135
  content,
8059
9136
  score: r.score,
8060
9137
  chunkType: r.metadata.chunkType,
8061
- name: r.metadata.name
9138
+ name: r.metadata.name,
9139
+ blame: blameFromMetadata(r.metadata)
8062
9140
  };
8063
9141
  })
8064
9142
  );
8065
9143
  }
8066
- async keywordSearch(query, limit) {
8067
- const { store, invertedIndex } = await this.ensureInitialized();
9144
+ async keywordSearch(query, limit, store, invertedIndex) {
8068
9145
  const scores = invertedIndex.search(query);
8069
9146
  if (scores.size === 0) {
8070
9147
  return [];
@@ -8082,24 +9159,54 @@ var Indexer = class {
8082
9159
  return results.slice(0, limit);
8083
9160
  }
8084
9161
  async getStatus() {
8085
- const { store, configuredProviderInfo, database } = await this.ensureInitialized();
9162
+ const { store, configuredProviderInfo, database, readIssues, compatibility } = await this.ensureInitialized();
8086
9163
  const failedBatchesCount = this.getFailedBatchesCount();
9164
+ const vectorCount = store.count();
9165
+ const statusReadIssues = [...readIssues];
9166
+ let startupWarning = "";
9167
+ if (!statusReadIssues.some((issue) => issue.component === "database")) {
9168
+ try {
9169
+ startupWarning = database.getMetadata(STARTUP_WARNING_METADATA_KEY) ?? "";
9170
+ } catch (error) {
9171
+ const message = this.getDatabaseReadIssueMessage();
9172
+ statusReadIssues.push(this.createReadIssue("database", message));
9173
+ if (!this.readIssues.some((issue) => issue.component === "database")) {
9174
+ this.recordReadIssue("database", message, error);
9175
+ }
9176
+ }
9177
+ }
9178
+ const readWarning = statusReadIssues.map((issue) => issue.message).join(" ");
9179
+ const warning = [readWarning, startupWarning].filter((message) => message.length > 0).join(" ");
9180
+ const hasBlockingReadIssue = statusReadIssues.some((issue) => issue.blocking);
8087
9181
  return {
8088
- indexed: store.count() > 0,
8089
- vectorCount: store.count(),
9182
+ indexed: vectorCount > 0 && !hasBlockingReadIssue,
9183
+ vectorCount,
8090
9184
  provider: configuredProviderInfo.provider,
8091
9185
  model: configuredProviderInfo.modelInfo.model,
8092
9186
  indexPath: this.indexPath,
8093
9187
  currentBranch: this.currentBranch,
8094
9188
  baseBranch: this.baseBranch,
8095
- compatibility: this.indexCompatibility,
9189
+ compatibility,
8096
9190
  failedBatchesCount,
8097
9191
  failedBatchesPath: failedBatchesCount > 0 ? this.failedBatchesPath : void 0,
8098
- warning: database.getMetadata(STARTUP_WARNING_METADATA_KEY) ?? void 0
9192
+ warning: warning || void 0
8099
9193
  };
8100
9194
  }
9195
+ async forceIndex(onProgress) {
9196
+ return this.withIndexMutationLease("force-index", async (recoveredOwners) => {
9197
+ await this.ensureInitializedUnlocked(recoveredOwners);
9198
+ await this.clearIndexUnlocked();
9199
+ return this.indexUnlocked(onProgress, [], true);
9200
+ });
9201
+ }
8101
9202
  async clearIndex() {
8102
- const { store, invertedIndex, database } = await this.ensureInitialized();
9203
+ await this.withIndexMutationLease("clear", async (recoveredOwners) => {
9204
+ await this.ensureInitializedUnlocked(recoveredOwners);
9205
+ await this.clearIndexUnlocked();
9206
+ });
9207
+ }
9208
+ async clearIndexUnlocked() {
9209
+ const { store, invertedIndex, database } = this.requireLoadedIndexState();
8103
9210
  if (this.config.scope === "global") {
8104
9211
  store.load();
8105
9212
  invertedIndex.load();
@@ -8126,7 +9233,7 @@ var Indexer = class {
8126
9233
  store.clear();
8127
9234
  store.save();
8128
9235
  invertedIndex.clear();
8129
- invertedIndex.save();
9236
+ this.saveInvertedIndex(invertedIndex);
8130
9237
  this.fileHashCache.clear();
8131
9238
  this.saveFileHashCache();
8132
9239
  database.clearAllIndexedData();
@@ -8150,14 +9257,7 @@ var Indexer = class {
8150
9257
  this.indexCompatibility = compatibility;
8151
9258
  return;
8152
9259
  }
8153
- const localProjectIndexPaths = [path10.join(this.projectRoot, getHostProjectIndexRelativePath(this.host))];
8154
- if (this.host !== "opencode") {
8155
- localProjectIndexPaths.push(path10.join(this.projectRoot, getHostProjectIndexRelativePath("opencode")));
8156
- }
8157
- const isLocalProjectIndex = localProjectIndexPaths.some(
8158
- (localPath) => path10.resolve(this.indexPath) === path10.resolve(localPath)
8159
- );
8160
- if (!isLocalProjectIndex) {
9260
+ if (!this.isLocalProjectIndexPath()) {
8161
9261
  throw new Error(
8162
9262
  "Project-scoped force rebuild is unsafe while using an inherited worktree index. Create a local project config boundary before clearing the index."
8163
9263
  );
@@ -8165,7 +9265,7 @@ var Indexer = class {
8165
9265
  store.clear();
8166
9266
  store.save();
8167
9267
  invertedIndex.clear();
8168
- invertedIndex.save();
9268
+ this.saveInvertedIndex(invertedIndex);
8169
9269
  this.fileHashCache.clear();
8170
9270
  this.saveFileHashCache();
8171
9271
  database.clearAllIndexedData();
@@ -8183,7 +9283,13 @@ var Indexer = class {
8183
9283
  this.indexCompatibility = this.validateIndexCompatibility(this.configuredProviderInfo);
8184
9284
  }
8185
9285
  async healthCheck() {
8186
- const { store, invertedIndex, database } = await this.ensureInitialized();
9286
+ return this.withIndexMutationLease("health-check", async (recoveredOwners) => {
9287
+ await this.ensureInitializedUnlocked(recoveredOwners);
9288
+ return this.healthCheckUnlocked();
9289
+ });
9290
+ }
9291
+ async healthCheckUnlocked() {
9292
+ const { store, invertedIndex, database } = this.requireLoadedIndexState();
8187
9293
  this.logger.gc("info", "Starting health check");
8188
9294
  const allMetadata = store.getAllMetadata();
8189
9295
  const filePathsToChunkKeys = /* @__PURE__ */ new Map();
@@ -8196,7 +9302,7 @@ var Indexer = class {
8196
9302
  const removedChunkKeys = [];
8197
9303
  const chunkKeysByRemovedFile = /* @__PURE__ */ new Map();
8198
9304
  for (const [filePath, chunkKeys] of filePathsToChunkKeys) {
8199
- if (!(0, import_fs7.existsSync)(filePath)) {
9305
+ if (!(0, import_fs8.existsSync)(filePath)) {
8200
9306
  chunkKeysByRemovedFile.set(filePath, chunkKeys);
8201
9307
  for (const key of chunkKeys) {
8202
9308
  removedChunkKeys.push(key);
@@ -8221,7 +9327,7 @@ var Indexer = class {
8221
9327
  const removedCount = removedChunkKeys.length;
8222
9328
  if (removedCount > 0) {
8223
9329
  store.save();
8224
- invertedIndex.save();
9330
+ this.saveInvertedIndex(invertedIndex);
8225
9331
  }
8226
9332
  let gcOrphanEmbeddings;
8227
9333
  let gcOrphanChunks;
@@ -8236,7 +9342,7 @@ var Indexer = class {
8236
9342
  if (!await this.tryResetCorruptedIndex("running index health check", error)) {
8237
9343
  throw error;
8238
9344
  }
8239
- await this.ensureInitialized();
9345
+ await this.initializeUnlocked("writer", [], { skipAutoGc: true });
8240
9346
  return {
8241
9347
  removed: 0,
8242
9348
  filePaths: [],
@@ -8245,7 +9351,7 @@ var Indexer = class {
8245
9351
  gcOrphanSymbols: 0,
8246
9352
  gcOrphanCallEdges: 0,
8247
9353
  resetCorruptedIndex: true,
8248
- warning: this.getCorruptedIndexWarning(path10.join(this.indexPath, "codebase.db"))
9354
+ warning: this.getCorruptedIndexWarning(path12.join(this.indexPath, "codebase.db"))
8249
9355
  };
8250
9356
  }
8251
9357
  this.logger.recordGc(removedCount, gcOrphanChunks, gcOrphanEmbeddings);
@@ -8258,7 +9364,13 @@ var Indexer = class {
8258
9364
  return { removed: removedCount, filePaths: removedFilePaths, gcOrphanEmbeddings, gcOrphanChunks, gcOrphanSymbols, gcOrphanCallEdges };
8259
9365
  }
8260
9366
  async retryFailedBatches() {
8261
- const { store, provider, invertedIndex, database, configuredProviderInfo } = await this.ensureInitialized();
9367
+ return this.withIndexMutationLease("retry-failed-batches", async (recoveredOwners) => {
9368
+ await this.ensureInitializedUnlocked(recoveredOwners);
9369
+ return this.retryFailedBatchesUnlocked();
9370
+ });
9371
+ }
9372
+ async retryFailedBatchesUnlocked() {
9373
+ const { store, provider, invertedIndex, database, configuredProviderInfo } = this.requireLoadedIndexState();
8262
9374
  const maxChunkTokens = getSafeEmbeddingChunkTokenLimit(configuredProviderInfo);
8263
9375
  const providerRateLimits = this.getProviderRateLimits(configuredProviderInfo.provider);
8264
9376
  const roots = this.config.scope === "global" ? this.getScopedRoots() : null;
@@ -8422,7 +9534,7 @@ var Indexer = class {
8422
9534
  }
8423
9535
  if (succeeded > 0) {
8424
9536
  store.save();
8425
- invertedIndex.save();
9537
+ this.saveInvertedIndex(invertedIndex);
8426
9538
  }
8427
9539
  if (roots && succeeded > 0 && persistedStillFailing.length === 0 && this.hasProjectForceReembedPending()) {
8428
9540
  database.deleteMetadata(this.getProjectForceReembedMetadataKey());
@@ -8450,15 +9562,16 @@ var Indexer = class {
8450
9562
  }
8451
9563
  }
8452
9564
  async getDatabaseStats() {
8453
- const { database } = await this.ensureInitialized();
9565
+ const { database, readIssues } = await this.ensureInitialized();
9566
+ this.requireReadableComponents(readIssues, "database");
8454
9567
  return database.getStats();
8455
9568
  }
8456
9569
  getLogger() {
8457
9570
  return this.logger;
8458
9571
  }
8459
9572
  async findSimilar(code, limit = this.config.search.maxResults, options) {
8460
- const { store, provider, database } = await this.ensureInitialized();
8461
- const compatibility = this.checkCompatibility();
9573
+ const { store, provider, database, readIssues, compatibility } = await this.ensureInitialized();
9574
+ this.requireReadableComponents(readIssues, "vectors", "database");
8462
9575
  if (!compatibility.compatible) {
8463
9576
  throw new Error(
8464
9577
  `${compatibility.reason ?? "Index is incompatible with current embedding provider."} Run index_codebase with force=true to rebuild the index.`
@@ -8548,7 +9661,7 @@ var Indexer = class {
8548
9661
  let content = "";
8549
9662
  if (this.config.search.includeContext) {
8550
9663
  try {
8551
- const fileContent = await import_fs7.promises.readFile(
9664
+ const fileContent = await import_fs8.promises.readFile(
8552
9665
  r.metadata.filePath,
8553
9666
  "utf-8"
8554
9667
  );
@@ -8565,13 +9678,15 @@ var Indexer = class {
8565
9678
  content,
8566
9679
  score: r.score,
8567
9680
  chunkType: r.metadata.chunkType,
8568
- name: r.metadata.name
9681
+ name: r.metadata.name,
9682
+ blame: blameFromMetadata(r.metadata)
8569
9683
  };
8570
9684
  })
8571
9685
  );
8572
9686
  }
8573
9687
  async getCallers(targetName, callTypeFilter) {
8574
- const { database } = await this.ensureInitialized();
9688
+ const { database, readIssues } = await this.ensureInitialized();
9689
+ this.requireReadableComponents(readIssues, "database");
8575
9690
  const seen = /* @__PURE__ */ new Set();
8576
9691
  const results = [];
8577
9692
  for (const branchKey of this.getBranchCatalogKeys()) {
@@ -8585,7 +9700,8 @@ var Indexer = class {
8585
9700
  return results;
8586
9701
  }
8587
9702
  async getCallees(symbolId, callTypeFilter) {
8588
- const { database } = await this.ensureInitialized();
9703
+ const { database, readIssues } = await this.ensureInitialized();
9704
+ this.requireReadableComponents(readIssues, "database");
8589
9705
  const seen = /* @__PURE__ */ new Set();
8590
9706
  const results = [];
8591
9707
  for (const branchKey of this.getBranchCatalogKeys()) {
@@ -8599,44 +9715,51 @@ var Indexer = class {
8599
9715
  return results;
8600
9716
  }
8601
9717
  async findCallPath(fromName, toName, maxDepth) {
8602
- const { database } = await this.ensureInitialized();
9718
+ const { database, readIssues } = await this.ensureInitialized();
9719
+ this.requireReadableComponents(readIssues, "database");
8603
9720
  let shortest = [];
8604
9721
  for (const branchKey of this.getBranchCatalogKeys()) {
8605
- const path25 = database.findShortestPath(fromName, toName, branchKey, maxDepth);
8606
- if (path25.length > 0 && (shortest.length === 0 || path25.length < shortest.length)) {
8607
- shortest = path25;
9722
+ const path27 = database.findShortestPath(fromName, toName, branchKey, maxDepth);
9723
+ if (path27.length > 0 && (shortest.length === 0 || path27.length < shortest.length)) {
9724
+ shortest = path27;
8608
9725
  }
8609
9726
  }
8610
9727
  return shortest;
8611
9728
  }
8612
9729
  async getSymbolsForBranch(branch) {
8613
- const { database } = await this.ensureInitialized();
9730
+ const { database, readIssues } = await this.ensureInitialized();
9731
+ this.requireReadableComponents(readIssues, "database");
8614
9732
  const resolvedBranch = branch ?? this.getBranchCatalogKey();
8615
9733
  return database.getSymbolsForBranch(resolvedBranch);
8616
9734
  }
8617
9735
  async getSymbolsForFiles(filePaths, branch) {
8618
- const { database } = await this.ensureInitialized();
9736
+ const { database, readIssues } = await this.ensureInitialized();
9737
+ this.requireReadableComponents(readIssues, "database");
8619
9738
  const resolvedBranch = branch ?? this.getBranchCatalogKey();
8620
9739
  return database.getSymbolsForFiles(filePaths, resolvedBranch);
8621
9740
  }
8622
9741
  async getTransitiveReachability(rootSymbolIds, direction, maxDepth) {
8623
- const { database } = await this.ensureInitialized();
9742
+ const { database, readIssues } = await this.ensureInitialized();
9743
+ this.requireReadableComponents(readIssues, "database");
8624
9744
  const branch = this.getBranchCatalogKey();
8625
9745
  return database.getTransitiveReachability(rootSymbolIds, branch, direction, maxDepth);
8626
9746
  }
8627
9747
  async detectCommunities(branch, symbolIds) {
8628
- const { database } = await this.ensureInitialized();
9748
+ const { database, readIssues } = await this.ensureInitialized();
9749
+ this.requireReadableComponents(readIssues, "database");
8629
9750
  const resolvedBranch = branch ?? this.getBranchCatalogKey();
8630
9751
  return database.detectCommunities(resolvedBranch, symbolIds);
8631
9752
  }
8632
9753
  async computeCentrality(branch) {
8633
- const { database } = await this.ensureInitialized();
9754
+ const { database, readIssues } = await this.ensureInitialized();
9755
+ this.requireReadableComponents(readIssues, "database");
8634
9756
  const resolvedBranch = branch ?? this.getBranchCatalogKey();
8635
9757
  return database.computeCentrality(resolvedBranch);
8636
9758
  }
8637
9759
  async getPrImpact(opts) {
8638
- const { database } = await this.ensureInitialized();
8639
- const execFileAsync2 = (0, import_util2.promisify)(import_child_process2.execFile);
9760
+ const { database, readIssues } = await this.ensureInitialized();
9761
+ this.requireReadableComponents(readIssues, "database");
9762
+ const execFileAsync3 = (0, import_util3.promisify)(import_child_process3.execFile);
8640
9763
  const changedFilesResult = await getChangedFiles({
8641
9764
  pr: opts.pr,
8642
9765
  branch: opts.branch,
@@ -8658,7 +9781,7 @@ var Indexer = class {
8658
9781
  "Run index_codebase first to build the call graph and symbol index for this branch."
8659
9782
  );
8660
9783
  }
8661
- const absoluteChangedFiles = changedFiles.map((f) => path10.resolve(this.projectRoot, f));
9784
+ const absoluteChangedFiles = changedFiles.map((f) => path12.resolve(this.projectRoot, f));
8662
9785
  const directSymbols = database.getSymbolsForFiles(absoluteChangedFiles, branchKey);
8663
9786
  const directIds = directSymbols.map((s) => s.id);
8664
9787
  const direction = opts.direction ?? "both";
@@ -8720,7 +9843,7 @@ var Indexer = class {
8720
9843
  if (opts.checkConflicts) {
8721
9844
  conflictingPRs = [];
8722
9845
  try {
8723
- const { stdout } = await execFileAsync2(
9846
+ const { stdout } = await execFileAsync3(
8724
9847
  "gh",
8725
9848
  ["pr", "list", "--state", "open", "--json", "number,headRefName", "--limit", "10000"],
8726
9849
  { cwd: this.projectRoot, timeout: 3e4 }
@@ -8741,7 +9864,7 @@ var Indexer = class {
8741
9864
  projectRoot: this.projectRoot,
8742
9865
  baseBranch: this.baseBranch
8743
9866
  });
8744
- const otherAbsolute = otherChanged.files.map((f) => path10.resolve(this.projectRoot, f));
9867
+ const otherAbsolute = otherChanged.files.map((f) => path12.resolve(this.projectRoot, f));
8745
9868
  const otherBranchKey = this.getBranchCatalogKeyFor(openPr.headRefName);
8746
9869
  const otherSymbols = database.getSymbolsForFiles(otherAbsolute, otherBranchKey);
8747
9870
  const otherLabels = /* @__PURE__ */ new Set();
@@ -8791,7 +9914,8 @@ var Indexer = class {
8791
9914
  };
8792
9915
  }
8793
9916
  async getVisualizationData(options) {
8794
- const { database, store } = await this.ensureInitialized();
9917
+ const { database, store, readIssues } = await this.ensureInitialized();
9918
+ this.requireReadableComponents(readIssues, "vectors", "database");
8795
9919
  const seenSymbols = /* @__PURE__ */ new Map();
8796
9920
  const seenEdges = /* @__PURE__ */ new Map();
8797
9921
  for (const branchKey of this.getBranchCatalogKeys()) {
@@ -8804,12 +9928,12 @@ var Indexer = class {
8804
9928
  if (meta.filePath) filePaths.add(meta.filePath);
8805
9929
  }
8806
9930
  const directory = options?.directory?.replace(/\/$/, "");
8807
- const absoluteDirectoryFilter = directory ? path10.resolve(this.projectRoot, directory) : void 0;
9931
+ const absoluteDirectoryFilter = directory ? path12.resolve(this.projectRoot, directory) : void 0;
8808
9932
  for (const filePath of filePaths) {
8809
9933
  if (directory) {
8810
- const absoluteFilePath = path10.resolve(filePath);
9934
+ const absoluteFilePath = path12.resolve(filePath);
8811
9935
  const matchesRelative = filePath === directory || filePath.startsWith(directory + "/");
8812
- const matchesProjectRelative = absoluteDirectoryFilter !== void 0 && (absoluteFilePath === absoluteDirectoryFilter || absoluteFilePath.startsWith(absoluteDirectoryFilter + path10.sep));
9936
+ const matchesProjectRelative = absoluteDirectoryFilter !== void 0 && (absoluteFilePath === absoluteDirectoryFilter || absoluteFilePath.startsWith(absoluteDirectoryFilter + path12.sep));
8813
9937
  if (!matchesRelative && !matchesProjectRelative) {
8814
9938
  continue;
8815
9939
  }
@@ -8831,12 +9955,23 @@ var Indexer = class {
8831
9955
  return { symbols: [...seenSymbols.values()], edges: [...seenEdges.values()] };
8832
9956
  }
8833
9957
  async close() {
8834
- await this.database?.close();
9958
+ this.database?.close();
9959
+ for (const database of this.retiredDatabases) {
9960
+ database.close();
9961
+ }
9962
+ this.retiredDatabases = [];
8835
9963
  this.database = null;
8836
9964
  this.store = null;
8837
9965
  this.invertedIndex = null;
8838
9966
  this.provider = null;
8839
9967
  this.reranker = null;
9968
+ this.configuredProviderInfo = null;
9969
+ this.indexCompatibility = null;
9970
+ this.initializationMode = "none";
9971
+ this.readIssues = [];
9972
+ this.readerArtifactFingerprint = null;
9973
+ this.writerArtifactFingerprint = null;
9974
+ this.readerArtifactRetryAfter.clear();
8840
9975
  }
8841
9976
  };
8842
9977
 
@@ -9087,15 +10222,15 @@ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingToke
9087
10222
  }
9088
10223
 
9089
10224
  // src/eval/runner-config.ts
9090
- var import_fs8 = require("fs");
9091
- var os4 = __toESM(require("os"), 1);
9092
- var path12 = __toESM(require("path"), 1);
10225
+ var import_fs9 = require("fs");
10226
+ var os5 = __toESM(require("os"), 1);
10227
+ var path14 = __toESM(require("path"), 1);
9093
10228
 
9094
10229
  // src/config/rebase.ts
9095
- var path11 = __toESM(require("path"), 1);
10230
+ var path13 = __toESM(require("path"), 1);
9096
10231
  function isWithinRoot(rootDir, targetPath) {
9097
- const relativePath = path11.relative(rootDir, targetPath);
9098
- return relativePath === "" || !relativePath.startsWith("..") && !path11.isAbsolute(relativePath);
10232
+ const relativePath = path13.relative(rootDir, targetPath);
10233
+ return relativePath === "" || !relativePath.startsWith("..") && !path13.isAbsolute(relativePath);
9099
10234
  }
9100
10235
  function rebasePathEntries(values, fromDir, toDir) {
9101
10236
  if (!Array.isArray(values)) {
@@ -9103,10 +10238,10 @@ function rebasePathEntries(values, fromDir, toDir) {
9103
10238
  }
9104
10239
  return values.filter((value) => typeof value === "string").map((value) => {
9105
10240
  const trimmed = value.trim();
9106
- if (!trimmed || path11.isAbsolute(trimmed)) {
10241
+ if (!trimmed || path13.isAbsolute(trimmed)) {
9107
10242
  return trimmed;
9108
10243
  }
9109
- return normalizePathSeparators(path11.normalize(path11.relative(toDir, path11.resolve(fromDir, trimmed))));
10244
+ return normalizePathSeparators(path13.normalize(path13.relative(toDir, path13.resolve(fromDir, trimmed))));
9110
10245
  }).filter(Boolean);
9111
10246
  }
9112
10247
  function resolveInheritedKnowledgeBaseEntries(values, sourceRoot, targetRoot) {
@@ -9118,17 +10253,17 @@ function resolveInheritedKnowledgeBaseEntries(values, sourceRoot, targetRoot) {
9118
10253
  if (!trimmed) {
9119
10254
  return trimmed;
9120
10255
  }
9121
- if (path11.isAbsolute(trimmed)) {
10256
+ if (path13.isAbsolute(trimmed)) {
9122
10257
  if (isWithinRoot(sourceRoot, trimmed)) {
9123
- return normalizePathSeparators(path11.normalize(path11.relative(sourceRoot, trimmed) || "."));
10258
+ return normalizePathSeparators(path13.normalize(path13.relative(sourceRoot, trimmed) || "."));
9124
10259
  }
9125
- return path11.normalize(trimmed);
10260
+ return path13.normalize(trimmed);
9126
10261
  }
9127
- const resolvedFromSource = path11.resolve(sourceRoot, trimmed);
10262
+ const resolvedFromSource = path13.resolve(sourceRoot, trimmed);
9128
10263
  if (isWithinRoot(sourceRoot, resolvedFromSource)) {
9129
- return normalizePathSeparators(path11.normalize(trimmed));
10264
+ return normalizePathSeparators(path13.normalize(trimmed));
9130
10265
  }
9131
- return normalizePathSeparators(path11.normalize(path11.relative(targetRoot, resolvedFromSource)));
10266
+ return normalizePathSeparators(path13.normalize(path13.relative(targetRoot, resolvedFromSource)));
9132
10267
  }).filter(Boolean);
9133
10268
  }
9134
10269
 
@@ -9166,7 +10301,7 @@ function validateEvalConfigShape(rawConfig, filePath) {
9166
10301
  }
9167
10302
  function parseJsonConfigFile(filePath) {
9168
10303
  try {
9169
- return validateEvalConfigShape(JSON.parse((0, import_fs8.readFileSync)(filePath, "utf-8")), filePath);
10304
+ return validateEvalConfigShape(JSON.parse((0, import_fs9.readFileSync)(filePath, "utf-8")), filePath);
9170
10305
  } catch (error) {
9171
10306
  if (error instanceof Error && error.message.startsWith("Eval config at ")) {
9172
10307
  throw error;
@@ -9176,20 +10311,20 @@ function parseJsonConfigFile(filePath) {
9176
10311
  }
9177
10312
  }
9178
10313
  function toAbsolute(projectRoot, maybeRelative) {
9179
- return path12.isAbsolute(maybeRelative) ? maybeRelative : path12.join(projectRoot, maybeRelative);
10314
+ return path14.isAbsolute(maybeRelative) ? maybeRelative : path14.join(projectRoot, maybeRelative);
9180
10315
  }
9181
10316
  function isProjectScopedConfigPath(configPath) {
9182
- return path12.basename(configPath) === "codebase-index.json" && path12.basename(path12.dirname(configPath)) === ".opencode";
10317
+ return path14.basename(configPath) === "codebase-index.json" && path14.basename(path14.dirname(configPath)) === ".opencode";
9183
10318
  }
9184
10319
  function normalizeEvalConfigKnowledgeBases(rawConfig, projectRoot, resolvedConfigPath) {
9185
10320
  const config = rawConfig && typeof rawConfig === "object" ? { ...rawConfig } : {};
9186
10321
  const rebaseEntries = (values) => isProjectScopedConfigPath(resolvedConfigPath) ? resolveInheritedKnowledgeBaseEntries(
9187
10322
  values,
9188
- path12.dirname(path12.dirname(resolvedConfigPath)),
10323
+ path14.dirname(path14.dirname(resolvedConfigPath)),
9189
10324
  projectRoot
9190
10325
  ) : rebasePathEntries(
9191
10326
  values,
9192
- path12.dirname(resolvedConfigPath),
10327
+ path14.dirname(resolvedConfigPath),
9193
10328
  projectRoot
9194
10329
  );
9195
10330
  if (Array.isArray(config.knowledgeBases)) {
@@ -9202,7 +10337,7 @@ function normalizeEvalConfigKnowledgeBases(rawConfig, projectRoot, resolvedConfi
9202
10337
  }
9203
10338
  function loadRawConfig(projectRoot, configPath) {
9204
10339
  const fromPath = configPath ? toAbsolute(projectRoot, configPath) : null;
9205
- if (fromPath && (0, import_fs8.existsSync)(fromPath)) {
10340
+ if (fromPath && (0, import_fs9.existsSync)(fromPath)) {
9206
10341
  return normalizeEvalConfigKnowledgeBases(
9207
10342
  parseJsonConfigFile(fromPath),
9208
10343
  projectRoot,
@@ -9210,15 +10345,15 @@ function loadRawConfig(projectRoot, configPath) {
9210
10345
  );
9211
10346
  }
9212
10347
  const projectConfig = resolveProjectConfigPath(projectRoot);
9213
- if ((0, import_fs8.existsSync)(projectConfig)) {
10348
+ if ((0, import_fs9.existsSync)(projectConfig)) {
9214
10349
  return normalizeEvalConfigKnowledgeBases(
9215
10350
  parseJsonConfigFile(projectConfig),
9216
10351
  projectRoot,
9217
10352
  projectConfig
9218
10353
  );
9219
10354
  }
9220
- const globalConfig = path12.join(os4.homedir(), ".config", "opencode", "codebase-index.json");
9221
- if ((0, import_fs8.existsSync)(globalConfig)) {
10355
+ const globalConfig = path14.join(os5.homedir(), ".config", "opencode", "codebase-index.json");
10356
+ if ((0, import_fs9.existsSync)(globalConfig)) {
9222
10357
  return parseJsonConfigFile(globalConfig);
9223
10358
  }
9224
10359
  return {};
@@ -9227,24 +10362,24 @@ function getIndexRootPath(projectRoot, scope) {
9227
10362
  return scope === "global" ? getGlobalIndexPath() : resolveProjectIndexPath(projectRoot, scope);
9228
10363
  }
9229
10364
  function getLocalProjectIndexRoot(projectRoot) {
9230
- return path12.join(projectRoot, ".opencode", "index");
10365
+ return path14.join(projectRoot, ".opencode", "index");
9231
10366
  }
9232
10367
  function getLocalProjectConfigPath(projectRoot) {
9233
- return path12.join(projectRoot, ".opencode", "codebase-index.json");
10368
+ return path14.join(projectRoot, ".opencode", "codebase-index.json");
9234
10369
  }
9235
10370
  function clearIndexRoot(projectRoot, scope) {
9236
10371
  const indexRoot = scope === "global" ? getIndexRootPath(projectRoot, scope) : getLocalProjectIndexRoot(projectRoot);
9237
- if ((0, import_fs8.existsSync)(indexRoot)) {
9238
- (0, import_fs8.rmSync)(indexRoot, { recursive: true, force: true });
10372
+ if ((0, import_fs9.existsSync)(indexRoot)) {
10373
+ (0, import_fs9.rmSync)(indexRoot, { recursive: true, force: true });
9239
10374
  }
9240
10375
  }
9241
10376
  function ensureLocalEvalProjectConfig(projectRoot, configPath) {
9242
10377
  const localConfigPath = getLocalProjectConfigPath(projectRoot);
9243
10378
  const resolvedConfigPath = configPath ? toAbsolute(projectRoot, configPath) : resolveProjectConfigPath(projectRoot);
9244
- if (!configPath && (0, import_fs8.existsSync)(localConfigPath)) {
10379
+ if (!configPath && (0, import_fs9.existsSync)(localConfigPath)) {
9245
10380
  return localConfigPath;
9246
10381
  }
9247
- if (!(0, import_fs8.existsSync)(resolvedConfigPath) || resolvedConfigPath === localConfigPath) {
10382
+ if (!(0, import_fs9.existsSync)(resolvedConfigPath) || resolvedConfigPath === localConfigPath) {
9248
10383
  return resolvedConfigPath;
9249
10384
  }
9250
10385
  const sourceConfig = normalizeEvalConfigKnowledgeBases(
@@ -9252,8 +10387,8 @@ function ensureLocalEvalProjectConfig(projectRoot, configPath) {
9252
10387
  projectRoot,
9253
10388
  resolvedConfigPath
9254
10389
  );
9255
- (0, import_fs8.mkdirSync)(path12.dirname(localConfigPath), { recursive: true });
9256
- (0, import_fs8.writeFileSync)(localConfigPath, JSON.stringify(sourceConfig, null, 2), "utf-8");
10390
+ (0, import_fs9.mkdirSync)(path14.dirname(localConfigPath), { recursive: true });
10391
+ (0, import_fs9.writeFileSync)(localConfigPath, JSON.stringify(sourceConfig, null, 2), "utf-8");
9257
10392
  return localConfigPath;
9258
10393
  }
9259
10394
  function loadParsedConfig(projectRoot, configPath) {
@@ -9286,9 +10421,9 @@ function getEmbeddingCostPer1MTokens(embeddingProvider) {
9286
10421
  }
9287
10422
 
9288
10423
  // src/eval/schema.ts
9289
- var import_fs9 = require("fs");
10424
+ var import_fs10 = require("fs");
9290
10425
  function parseJsonFile(filePath) {
9291
- const content = (0, import_fs9.readFileSync)(filePath, "utf-8");
10426
+ const content = (0, import_fs10.readFileSync)(filePath, "utf-8");
9292
10427
  try {
9293
10428
  return JSON.parse(content);
9294
10429
  } catch (error) {
@@ -9302,23 +10437,23 @@ function isRecord2(value) {
9302
10437
  function isStringArray3(value) {
9303
10438
  return Array.isArray(value) && value.every((item) => typeof item === "string");
9304
10439
  }
9305
- function asPositiveNumber(value, path25) {
10440
+ function asPositiveNumber(value, path27) {
9306
10441
  if (typeof value !== "number" || Number.isNaN(value) || value < 0) {
9307
- throw new Error(`${path25} must be a non-negative number`);
10442
+ throw new Error(`${path27} must be a non-negative number`);
9308
10443
  }
9309
10444
  return value;
9310
10445
  }
9311
- function parseQueryType(value, path25) {
10446
+ function parseQueryType(value, path27) {
9312
10447
  if (value === "definition" || value === "implementation-intent" || value === "similarity" || value === "keyword-heavy") {
9313
10448
  return value;
9314
10449
  }
9315
10450
  throw new Error(
9316
- `${path25} must be one of: definition, implementation-intent, similarity, keyword-heavy`
10451
+ `${path27} must be one of: definition, implementation-intent, similarity, keyword-heavy`
9317
10452
  );
9318
10453
  }
9319
- function parseExpected(input, path25) {
10454
+ function parseExpected(input, path27) {
9320
10455
  if (!isRecord2(input)) {
9321
- throw new Error(`${path25} must be an object`);
10456
+ throw new Error(`${path27} must be an object`);
9322
10457
  }
9323
10458
  const filePathRaw = input.filePath;
9324
10459
  const acceptableFilesRaw = input.acceptableFiles;
@@ -9327,16 +10462,16 @@ function parseExpected(input, path25) {
9327
10462
  const filePath = typeof filePathRaw === "string" ? filePathRaw : void 0;
9328
10463
  const acceptableFiles = isStringArray3(acceptableFilesRaw) ? acceptableFilesRaw : void 0;
9329
10464
  if (!filePath && (!acceptableFiles || acceptableFiles.length === 0)) {
9330
- throw new Error(`${path25} must include either expected.filePath or expected.acceptableFiles`);
10465
+ throw new Error(`${path27} must include either expected.filePath or expected.acceptableFiles`);
9331
10466
  }
9332
10467
  if (acceptableFilesRaw !== void 0 && !isStringArray3(acceptableFilesRaw)) {
9333
- throw new Error(`${path25}.acceptableFiles must be an array of strings`);
10468
+ throw new Error(`${path27}.acceptableFiles must be an array of strings`);
9334
10469
  }
9335
10470
  if (symbolRaw !== void 0 && typeof symbolRaw !== "string") {
9336
- throw new Error(`${path25}.symbol must be a string when provided`);
10471
+ throw new Error(`${path27}.symbol must be a string when provided`);
9337
10472
  }
9338
10473
  if (branchRaw !== void 0 && typeof branchRaw !== "string") {
9339
- throw new Error(`${path25}.branch must be a string when provided`);
10474
+ throw new Error(`${path27}.branch must be a string when provided`);
9340
10475
  }
9341
10476
  return {
9342
10477
  filePath,
@@ -9346,25 +10481,25 @@ function parseExpected(input, path25) {
9346
10481
  };
9347
10482
  }
9348
10483
  function parseQuery(input, index) {
9349
- const path25 = `queries[${index}]`;
10484
+ const path27 = `queries[${index}]`;
9350
10485
  if (!isRecord2(input)) {
9351
- throw new Error(`${path25} must be an object`);
10486
+ throw new Error(`${path27} must be an object`);
9352
10487
  }
9353
10488
  const id = input.id;
9354
10489
  const query = input.query;
9355
10490
  const queryType = input.queryType;
9356
10491
  const expected = input.expected;
9357
10492
  if (typeof id !== "string" || id.trim().length === 0) {
9358
- throw new Error(`${path25}.id must be a non-empty string`);
10493
+ throw new Error(`${path27}.id must be a non-empty string`);
9359
10494
  }
9360
10495
  if (typeof query !== "string" || query.trim().length === 0) {
9361
- throw new Error(`${path25}.query must be a non-empty string`);
10496
+ throw new Error(`${path27}.query must be a non-empty string`);
9362
10497
  }
9363
10498
  return {
9364
10499
  id,
9365
10500
  query,
9366
- queryType: parseQueryType(queryType, `${path25}.queryType`),
9367
- expected: parseExpected(expected, `${path25}.expected`)
10501
+ queryType: parseQueryType(queryType, `${path27}.queryType`),
10502
+ expected: parseExpected(expected, `${path27}.expected`)
9368
10503
  };
9369
10504
  }
9370
10505
  function parseGoldenDataset(raw, sourceLabel) {
@@ -9547,13 +10682,13 @@ async function runEvaluation(options) {
9547
10682
  };
9548
10683
  const outputDir = createRunDirectory(toAbsolute(options.projectRoot, options.outputRoot));
9549
10684
  const perQueryArtifact = buildPerQueryArtifact(perQuery);
9550
- writeJson(path13.join(outputDir, "summary.json"), summary);
9551
- writeJson(path13.join(outputDir, "per-query.json"), perQueryArtifact);
10685
+ writeJson(path15.join(outputDir, "summary.json"), summary);
10686
+ writeJson(path15.join(outputDir, "per-query.json"), perQueryArtifact);
9552
10687
  let comparison;
9553
10688
  if (againstPath) {
9554
10689
  const baseline = loadSummary(againstPath);
9555
10690
  comparison = compareSummaries(summary, baseline, againstPath);
9556
- writeJson(path13.join(outputDir, "compare.json"), comparison);
10691
+ writeJson(path15.join(outputDir, "compare.json"), comparison);
9557
10692
  }
9558
10693
  let gate;
9559
10694
  if (options.ciMode) {
@@ -9563,10 +10698,10 @@ async function runEvaluation(options) {
9563
10698
  const budget = loadBudget(budgetPath);
9564
10699
  if (!comparison && budget.baselinePath) {
9565
10700
  const resolvedBaseline = toAbsolute(options.projectRoot, budget.baselinePath);
9566
- if ((0, import_fs10.existsSync)(resolvedBaseline)) {
10701
+ if ((0, import_fs11.existsSync)(resolvedBaseline)) {
9567
10702
  const baselineSummary = loadSummary(resolvedBaseline);
9568
10703
  comparison = compareSummaries(summary, baselineSummary, resolvedBaseline);
9569
- writeJson(path13.join(outputDir, "compare.json"), comparison);
10704
+ writeJson(path15.join(outputDir, "compare.json"), comparison);
9570
10705
  } else if (budget.failOnMissingBaseline) {
9571
10706
  throw new Error(
9572
10707
  `Budget baseline is missing: ${resolvedBaseline}. Set failOnMissingBaseline=false to allow CI run without baseline.`
@@ -9576,7 +10711,7 @@ async function runEvaluation(options) {
9576
10711
  gate = evaluateBudgetGate(budget, summary, comparison);
9577
10712
  }
9578
10713
  const markdown = createSummaryMarkdown(summary, comparison, gate);
9579
- writeText(path13.join(outputDir, "summary.md"), markdown);
10714
+ writeText(path15.join(outputDir, "summary.md"), markdown);
9580
10715
  return { outputDir, summary, perQuery, comparison, gate };
9581
10716
  } finally {
9582
10717
  await indexer.close();
@@ -9634,23 +10769,23 @@ async function runSweep(options, sweep) {
9634
10769
  bestByMrrAt10,
9635
10770
  bestByP95Latency
9636
10771
  };
9637
- writeJson(path13.join(outputDir, "compare.json"), aggregate);
10772
+ writeJson(path15.join(outputDir, "compare.json"), aggregate);
9638
10773
  const md = createSummaryMarkdown(
9639
10774
  bestByHitAt5?.summary ?? runs[0].summary,
9640
10775
  bestByHitAt5?.comparison,
9641
10776
  void 0,
9642
10777
  aggregate
9643
10778
  );
9644
- writeText(path13.join(outputDir, "summary.md"), md);
9645
- writeJson(path13.join(outputDir, "summary.json"), bestByHitAt5?.summary ?? runs[0].summary);
10779
+ writeText(path15.join(outputDir, "summary.md"), md);
10780
+ writeJson(path15.join(outputDir, "summary.json"), bestByHitAt5?.summary ?? runs[0].summary);
9646
10781
  return { outputDir, aggregate };
9647
10782
  }
9648
10783
 
9649
10784
  // src/eval/cli.ts
9650
- var path15 = __toESM(require("path"), 1);
10785
+ var path17 = __toESM(require("path"), 1);
9651
10786
 
9652
10787
  // src/eval/cli-parser.ts
9653
- var path14 = __toESM(require("path"), 1);
10788
+ var path16 = __toESM(require("path"), 1);
9654
10789
  function printUsage() {
9655
10790
  console.log(`
9656
10791
  Usage:
@@ -9722,12 +10857,12 @@ function parseEvalArgs(argv, cwd) {
9722
10857
  const arg = argv[i];
9723
10858
  const next = argv[i + 1];
9724
10859
  if (arg === "--project" && next) {
9725
- parsed.projectRoot = path14.resolve(cwd, next);
10860
+ parsed.projectRoot = path16.resolve(cwd, next);
9726
10861
  i += 1;
9727
10862
  continue;
9728
10863
  }
9729
10864
  if (arg === "--config" && next) {
9730
- parsed.configPath = path14.resolve(cwd, next);
10865
+ parsed.configPath = path16.resolve(cwd, next);
9731
10866
  i += 1;
9732
10867
  continue;
9733
10868
  }
@@ -9923,22 +11058,22 @@ async function handleEvalCommand(args, cwd) {
9923
11058
  if (!parsed.againstPath.endsWith(".json")) {
9924
11059
  throw new Error("eval diff --against must point to a summary JSON file");
9925
11060
  }
9926
- const currentSummary = loadSummary(path15.resolve(parsed.projectRoot, currentPath), {
11061
+ const currentSummary = loadSummary(path17.resolve(parsed.projectRoot, currentPath), {
9927
11062
  allowLegacyDiversityMetrics: true
9928
11063
  });
9929
- const baselineSummary = loadSummary(path15.resolve(parsed.projectRoot, parsed.againstPath), {
11064
+ const baselineSummary = loadSummary(path17.resolve(parsed.projectRoot, parsed.againstPath), {
9930
11065
  allowLegacyDiversityMetrics: true
9931
11066
  });
9932
11067
  const comparison = compareSummaries(
9933
11068
  currentSummary,
9934
11069
  baselineSummary,
9935
- path15.resolve(parsed.projectRoot, parsed.againstPath)
11070
+ path17.resolve(parsed.projectRoot, parsed.againstPath)
9936
11071
  );
9937
- const outputDir = createRunDirectory(path15.resolve(parsed.projectRoot, parsed.outputRoot));
11072
+ const outputDir = createRunDirectory(path17.resolve(parsed.projectRoot, parsed.outputRoot));
9938
11073
  const summaryMd = createSummaryMarkdown(currentSummary, comparison);
9939
- writeJson(path15.join(outputDir, "compare.json"), comparison);
9940
- writeText(path15.join(outputDir, "summary.md"), summaryMd);
9941
- writeJson(path15.join(outputDir, "summary.json"), currentSummary);
11074
+ writeJson(path17.join(outputDir, "compare.json"), comparison);
11075
+ writeText(path17.join(outputDir, "summary.md"), summaryMd);
11076
+ writeJson(path17.join(outputDir, "summary.json"), currentSummary);
9942
11077
  console.log(`Eval diff complete. Artifacts: ${outputDir}`);
9943
11078
  return 0;
9944
11079
  }
@@ -9947,7 +11082,7 @@ async function handleEvalCommand(args, cwd) {
9947
11082
 
9948
11083
  // src/mcp-server.ts
9949
11084
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
9950
- var import_fs14 = require("fs");
11085
+ var import_fs15 = require("fs");
9951
11086
 
9952
11087
  // src/mcp-server/register-prompts.ts
9953
11088
  var import_zod = require("zod");
@@ -10138,6 +11273,10 @@ function formatStatus(status) {
10138
11273
  lines.push(`Failed batches: ${status.failedBatchesPath}`);
10139
11274
  }
10140
11275
  }
11276
+ if (status.warning) {
11277
+ lines.push("");
11278
+ lines.push(`INDEX WARNING: ${status.warning}`);
11279
+ }
10141
11280
  if (status.compatibility && !status.compatibility.compatible) {
10142
11281
  lines.push("");
10143
11282
  lines.push(`COMPATIBILITY WARNING: ${status.compatibility.reason}`);
@@ -10183,6 +11322,17 @@ function calculatePercentage(progress) {
10183
11322
  if (progress.phase === "storing") return 95;
10184
11323
  return 0;
10185
11324
  }
11325
+ function formatCodebasePeek(results) {
11326
+ if (results.length === 0) {
11327
+ return "No matching code found. Try a different query or run index_codebase first.";
11328
+ }
11329
+ const formatted = results.map((r, idx) => {
11330
+ const location = `${r.filePath}:${r.startLine}-${r.endLine}`;
11331
+ const name = r.name ? `"${r.name}"` : "(anonymous)";
11332
+ return `[${idx + 1}] ${r.chunkType} ${name} at ${location} (score: ${r.score.toFixed(2)})${formatBlame(r)}`;
11333
+ });
11334
+ return formatted.join("\n");
11335
+ }
10186
11336
  function formatHealthCheck(result) {
10187
11337
  if (result.resetCorruptedIndex) {
10188
11338
  return result.warning ?? "Detected a corrupted local index and reset it. Run index_codebase to rebuild search data.";
@@ -10211,16 +11361,68 @@ function formatHealthCheck(result) {
10211
11361
  }
10212
11362
  return lines.join("\n");
10213
11363
  }
11364
+ function formatCallGraphCallers(name, callers, relationshipType) {
11365
+ if (callers.length === 0) {
11366
+ return `No callers found for "${name}"${relationshipType ? ` with type ${relationshipType}` : ""}. It may not be called by any tracked function, or the index needs updating.`;
11367
+ }
11368
+ const formatted = callers.map((edge, index) => {
11369
+ const confidence = edge.confidence !== "Direct" ? ` [${edge.confidence.toLowerCase()}]` : "";
11370
+ return `[${index + 1}] \u2190 from ${edge.fromSymbolName ?? "<unknown>"} in ${edge.fromSymbolFilePath ?? "<unknown file>"} [${edge.fromSymbolId}] (${edge.callType})${confidence} at line ${edge.line}${edge.isResolved ? " [resolved]" : " [unresolved]"}`;
11371
+ });
11372
+ return `"${name}" is called by ${callers.length} function(s):
11373
+
11374
+ ${formatted.join("\n")}`;
11375
+ }
11376
+ function formatCallGraphCallees(symbolId, callees, relationshipType) {
11377
+ if (callees.length === 0) {
11378
+ return `No callees found for symbol ${symbolId}${relationshipType ? ` with type ${relationshipType}` : ""}. The function may not call any other tracked functions.`;
11379
+ }
11380
+ return callees.map((edge, index) => {
11381
+ const confidence = edge.confidence !== "Direct" ? ` [${edge.confidence.toLowerCase()}]` : "";
11382
+ return `[${index + 1}] \u2192 ${edge.targetName} (${edge.callType})${confidence} at line ${edge.line}${edge.isResolved ? ` [resolved: ${edge.toSymbolId}]` : " [unresolved]"}`;
11383
+ }).join("\n");
11384
+ }
11385
+ function formatCallGraphPath(from, to, path27) {
11386
+ if (path27.length === 0) {
11387
+ return `No path found between "${from}" and "${to}". They may be in disconnected components, or the call graph index needs updating.`;
11388
+ }
11389
+ const formatted = path27.map((hop, index) => {
11390
+ const prefix = index === 0 ? "[start]" : `--${hop.callType}-->`;
11391
+ const location = hop.filePath ? ` (${hop.filePath}:${hop.line})` : "";
11392
+ return `${prefix} ${hop.symbolName}${location}`;
11393
+ });
11394
+ return `Path (${path27.length} hops):
11395
+ ${formatted.join("\n")}`;
11396
+ }
10214
11397
  function formatResultHeader(result, index) {
10215
11398
  return result.name ? `[${index + 1}] ${result.chunkType} "${result.name}" in ${result.filePath}:${result.startLine}-${result.endLine}` : `[${index + 1}] ${result.chunkType} in ${result.filePath}:${result.startLine}-${result.endLine}`;
10216
11399
  }
11400
+ function formatBlame(result) {
11401
+ if (!result.blame) {
11402
+ return "";
11403
+ }
11404
+ const date = new Date(result.blame.committedAt * 1e3).toISOString().slice(0, 10);
11405
+ return `
11406
+ ${result.blame.sha.slice(0, 7)} | ${result.blame.author} | ${date} | ${result.blame.summary}`;
11407
+ }
10217
11408
  function formatDefinitionLookup(results, query) {
10218
11409
  if (results.length === 0) {
10219
11410
  return `No definition found for "${query}". Try codebase_search for broader discovery, or verify the symbol name.`;
10220
11411
  }
10221
11412
  const formatted = results.map((r, idx) => {
10222
11413
  const header = formatResultHeader(r, idx);
10223
- return `${header} (score: ${r.score.toFixed(2)})
11414
+ return `${header} (score: ${r.score.toFixed(2)})${formatBlame(r)}
11415
+ \`\`\`
11416
+ ${truncateContent(r.content)}
11417
+ \`\`\``;
11418
+ });
11419
+ return formatted.join("\n\n");
11420
+ }
11421
+ function formatSearchResults(results, scoreFormat = "similarity") {
11422
+ const formatted = results.map((r, idx) => {
11423
+ const header = formatResultHeader(r, idx);
11424
+ const scoreLabel = scoreFormat === "similarity" ? `(similarity: ${(r.score * 100).toFixed(1)}%)` : `(score: ${r.score.toFixed(2)})`;
11425
+ return `${header} ${scoreLabel}${formatBlame(r)}
10224
11426
  \`\`\`
10225
11427
  ${truncateContent(r.content)}
10226
11428
  \`\`\``;
@@ -10280,12 +11482,12 @@ function formatPrImpact(result) {
10280
11482
  }
10281
11483
 
10282
11484
  // src/tools/operations.ts
10283
- var import_fs13 = require("fs");
10284
- var path19 = __toESM(require("path"), 1);
11485
+ var import_fs14 = require("fs");
11486
+ var path21 = __toESM(require("path"), 1);
10285
11487
 
10286
11488
  // src/config/merger.ts
10287
- var import_fs11 = require("fs");
10288
- var path16 = __toESM(require("path"), 1);
11489
+ var import_fs12 = require("fs");
11490
+ var path18 = __toESM(require("path"), 1);
10289
11491
  var PROJECT_OVERRIDE_KEYS = [
10290
11492
  "embeddingProvider",
10291
11493
  "customProvider",
@@ -10318,8 +11520,8 @@ function mergeUniqueStringArray(values) {
10318
11520
  return [...new Set(values.map((value) => String(value).trim()))];
10319
11521
  }
10320
11522
  function normalizeKnowledgeBasePath(value) {
10321
- let normalized = path16.normalize(String(value).trim());
10322
- const root = path16.parse(normalized).root;
11523
+ let normalized = path18.normalize(String(value).trim());
11524
+ const root = path18.parse(normalized).root;
10323
11525
  while (normalized.length > root.length && /[\\/]$/.test(normalized)) {
10324
11526
  normalized = normalized.slice(0, -1);
10325
11527
  }
@@ -10353,11 +11555,11 @@ function validateConfigLayerShape(rawConfig, filePath) {
10353
11555
  return rawConfig;
10354
11556
  }
10355
11557
  function loadJsonFile(filePath) {
10356
- if (!(0, import_fs11.existsSync)(filePath)) {
11558
+ if (!(0, import_fs12.existsSync)(filePath)) {
10357
11559
  return null;
10358
11560
  }
10359
11561
  try {
10360
- const content = (0, import_fs11.readFileSync)(filePath, "utf-8");
11562
+ const content = (0, import_fs12.readFileSync)(filePath, "utf-8");
10361
11563
  return validateConfigLayerShape(JSON.parse(content), filePath);
10362
11564
  } catch (error) {
10363
11565
  if (error instanceof Error && error.message.startsWith("Config file ")) {
@@ -10372,8 +11574,8 @@ function loadConfigFile(filePath) {
10372
11574
  }
10373
11575
  function materializeLocalProjectConfig(projectRoot, config, host = "opencode") {
10374
11576
  const localConfigPath = resolveWritableProjectConfigPath(projectRoot, host);
10375
- (0, import_fs11.mkdirSync)(path16.dirname(localConfigPath), { recursive: true });
10376
- (0, import_fs11.writeFileSync)(localConfigPath, JSON.stringify(config, null, 2), "utf-8");
11577
+ (0, import_fs12.mkdirSync)(path18.dirname(localConfigPath), { recursive: true });
11578
+ (0, import_fs12.writeFileSync)(localConfigPath, JSON.stringify(config, null, 2), "utf-8");
10377
11579
  return localConfigPath;
10378
11580
  }
10379
11581
  function loadProjectConfigLayer(projectRoot, host = "opencode") {
@@ -10383,7 +11585,7 @@ function loadProjectConfigLayer(projectRoot, host = "opencode") {
10383
11585
  return {};
10384
11586
  }
10385
11587
  const normalizedConfig = { ...projectConfig };
10386
- const projectConfigBaseDir = path16.dirname(path16.dirname(projectConfigPath));
11588
+ const projectConfigBaseDir = path18.dirname(path18.dirname(projectConfigPath));
10387
11589
  if (Array.isArray(normalizedConfig.knowledgeBases)) {
10388
11590
  normalizedConfig.knowledgeBases = resolveInheritedKnowledgeBaseEntries(
10389
11591
  normalizedConfig.knowledgeBases,
@@ -10447,19 +11649,19 @@ function loadMergedConfig(projectRoot, host = "opencode") {
10447
11649
  }
10448
11650
 
10449
11651
  // src/tools/knowledge-base-paths.ts
10450
- var path17 = __toESM(require("path"), 1);
11652
+ var path19 = __toESM(require("path"), 1);
10451
11653
  function resolveConfigPathValue(value, baseDir) {
10452
11654
  const trimmed = value.trim();
10453
11655
  if (!trimmed) {
10454
11656
  return trimmed;
10455
11657
  }
10456
- const absolutePath = path17.isAbsolute(trimmed) ? trimmed : path17.resolve(baseDir, trimmed);
10457
- return path17.normalize(absolutePath);
11658
+ const absolutePath = path19.isAbsolute(trimmed) ? trimmed : path19.resolve(baseDir, trimmed);
11659
+ return path19.normalize(absolutePath);
10458
11660
  }
10459
11661
 
10460
11662
  // src/tools/config-state.ts
10461
- var import_fs12 = require("fs");
10462
- var path18 = __toESM(require("path"), 1);
11663
+ var import_fs13 = require("fs");
11664
+ var path20 = __toESM(require("path"), 1);
10463
11665
  function normalizeKnowledgeBasePaths(config, projectRoot) {
10464
11666
  const normalized = { ...config };
10465
11667
  if (Array.isArray(normalized.knowledgeBases)) {
@@ -10483,6 +11685,24 @@ function loadRuntimeConfig(projectRoot, host = "opencode") {
10483
11685
  var indexerCache = /* @__PURE__ */ new Map();
10484
11686
  var configCache = /* @__PURE__ */ new Map();
10485
11687
  var defaultProjectRoots = /* @__PURE__ */ new Map();
11688
+ function getIndexBusyResult(error) {
11689
+ if (!isIndexLockContentionError(error)) return null;
11690
+ const owner = error.owner;
11691
+ const ownerText = owner ? `PID ${owner.pid}, operation ${owner.operation}, since ${owner.startedAt}` : "unreadable owner";
11692
+ if (error.reason === "legacy-lock") {
11693
+ return {
11694
+ kind: "busy",
11695
+ text: `INDEX_BUSY: legacy lock format detected (${ownerText}). Verify the PID and remove this lock manually only if it is stale.`
11696
+ };
11697
+ }
11698
+ if (error.reason === "unknown-owner") {
11699
+ return {
11700
+ kind: "busy",
11701
+ text: `INDEX_BUSY: unreadable or remote lock owner (${ownerText}). Automatic recovery was refused; manual verification is required.`
11702
+ };
11703
+ }
11704
+ return { kind: "busy", text: `INDEX_BUSY: another index operation is already in progress (${ownerText}).` };
11705
+ }
10486
11706
  function getProjectRoot(projectRoot, host) {
10487
11707
  if (projectRoot) {
10488
11708
  return projectRoot;
@@ -10527,8 +11747,8 @@ function refreshIndexerForDirectory(projectRoot, host = "opencode", config = par
10527
11747
  }
10528
11748
  function shouldForceLocalizeProjectIndex(projectRoot, host = "opencode") {
10529
11749
  const root = getProjectRoot(projectRoot, host);
10530
- const localIndexPath = path19.join(root, getHostProjectIndexRelativePath(host));
10531
- if ((0, import_fs13.existsSync)(localIndexPath)) {
11750
+ const localIndexPath = path21.join(root, getHostProjectIndexRelativePath(host));
11751
+ if ((0, import_fs14.existsSync)(localIndexPath)) {
10532
11752
  return false;
10533
11753
  }
10534
11754
  const inheritedIndexPath = resolveWorktreeFallbackProjectIndexPath(root, host);
@@ -10542,7 +11762,10 @@ async function searchCodebase(projectRoot, host, query, options = {}) {
10542
11762
  chunkType: options.chunkType,
10543
11763
  contextLines: options.contextLines,
10544
11764
  metadataOnly: options.metadataOnly,
10545
- definitionIntent: options.definitionIntent
11765
+ definitionIntent: options.definitionIntent,
11766
+ blameAuthor: options.blameAuthor,
11767
+ blameSha: options.blameSha,
11768
+ blameSince: options.blameSince
10546
11769
  });
10547
11770
  }
10548
11771
  async function findSimilarCode(projectRoot, host, code, options = {}) {
@@ -10581,35 +11804,46 @@ async function getCallGraphPath(projectRoot, host, from, to, maxDepth) {
10581
11804
  async function runIndexCodebase(projectRoot, host, args, onProgress) {
10582
11805
  const root = getProjectRoot(projectRoot, host);
10583
11806
  const key = getIndexerCacheKey(root, host);
10584
- const cachedConfig = configCache.get(key);
10585
11807
  let indexer = getIndexerForProject(root, host);
10586
- if (args.estimateOnly) {
10587
- return { kind: "estimate", estimate: await indexer.estimateCost() };
10588
- }
10589
- if (args.force) {
10590
- if (shouldForceLocalizeProjectIndex(root, host)) {
10591
- materializeLocalProjectConfig(root, loadProjectConfigLayer(root, host), host);
10592
- refreshIndexerForDirectory(root, host, cachedConfig);
10593
- indexer = getIndexerForProject(root, host);
10594
- }
10595
- await indexer.clearIndex();
10596
- refreshIndexerForDirectory(root, host, cachedConfig);
10597
- indexer = getIndexerForProject(root, host);
10598
- }
10599
- const stats = await indexer.index((progress) => {
10600
- if (onProgress) {
10601
- return onProgress(formatProgressTitle(progress), {
10602
- phase: progress.phase,
10603
- filesProcessed: progress.filesProcessed,
10604
- totalFiles: progress.totalFiles,
10605
- chunksProcessed: progress.chunksProcessed,
10606
- totalChunks: progress.totalChunks,
10607
- percentage: calculatePercentage(progress)
11808
+ const runtimeConfig = configCache.get(key);
11809
+ try {
11810
+ if (args.estimateOnly) {
11811
+ return { kind: "estimate", estimate: await indexer.estimateCost() };
11812
+ }
11813
+ const runIndex = async (target) => {
11814
+ const operation = args.force ? target.forceIndex.bind(target) : target.index.bind(target);
11815
+ return operation((progress) => {
11816
+ if (onProgress) {
11817
+ return onProgress(formatProgressTitle(progress), {
11818
+ phase: progress.phase,
11819
+ filesProcessed: progress.filesProcessed,
11820
+ totalFiles: progress.totalFiles,
11821
+ chunksProcessed: progress.chunksProcessed,
11822
+ totalChunks: progress.totalChunks,
11823
+ percentage: calculatePercentage(progress)
11824
+ });
11825
+ }
11826
+ return Promise.resolve();
10608
11827
  });
11828
+ };
11829
+ let stats;
11830
+ if (args.force && shouldForceLocalizeProjectIndex(root, host)) {
11831
+ const inheritedIndexPath = resolveProjectIndexPath(root, runtimeConfig.scope, host);
11832
+ stats = await withIndexLock(inheritedIndexPath, "force-index", async () => {
11833
+ materializeLocalProjectConfig(root, loadProjectConfigLayer(root, host), host);
11834
+ refreshIndexerForDirectory(root, host, runtimeConfig);
11835
+ indexer = getIndexerForProject(root, host);
11836
+ return runIndex(indexer);
11837
+ }, { completeRecoveries: false });
11838
+ } else {
11839
+ stats = await runIndex(indexer);
10609
11840
  }
10610
- return Promise.resolve();
10611
- });
10612
- return { kind: "stats", stats };
11841
+ return { kind: "stats", stats };
11842
+ } catch (error) {
11843
+ const busyResult = getIndexBusyResult(error);
11844
+ if (!busyResult) throw error;
11845
+ return busyResult;
11846
+ }
10613
11847
  }
10614
11848
  async function getIndexStatus(projectRoot, host) {
10615
11849
  const indexer = getIndexerForProject(projectRoot, host);
@@ -10619,6 +11853,15 @@ async function getIndexHealthCheck(projectRoot, host) {
10619
11853
  const indexer = getIndexerForProject(projectRoot, host);
10620
11854
  return indexer.healthCheck();
10621
11855
  }
11856
+ async function runIndexHealthCheck(projectRoot, host) {
11857
+ try {
11858
+ return { kind: "health", health: await getIndexHealthCheck(projectRoot, host) };
11859
+ } catch (error) {
11860
+ const busyResult = getIndexBusyResult(error);
11861
+ if (!busyResult) throw error;
11862
+ return busyResult;
11863
+ }
11864
+ }
10622
11865
  async function getPrImpact(projectRoot, host, params) {
10623
11866
  const indexer = getIndexerForProject(projectRoot, host);
10624
11867
  return indexer.getPrImpact({
@@ -10684,13 +11927,6 @@ async function getIndexLogs(projectRoot, host, args) {
10684
11927
  }
10685
11928
 
10686
11929
  // src/mcp-server/shared.ts
10687
- var MAX_CONTENT_LINES2 = 30;
10688
- function truncateContent2(content) {
10689
- const lines = content.split("\n");
10690
- if (lines.length <= MAX_CONTENT_LINES2) return content;
10691
- return lines.slice(0, MAX_CONTENT_LINES2).join("\n") + `
10692
- // ... (${lines.length - MAX_CONTENT_LINES2} more lines)`;
10693
- }
10694
11930
  var CHUNK_TYPE_ENUM = [
10695
11931
  "function",
10696
11932
  "class",
@@ -10716,7 +11952,10 @@ function registerMcpTools(server, runtime) {
10716
11952
  fileType: import_zod2.z.string().optional().describe("Filter by file extension (e.g., 'ts', 'py', 'rs')"),
10717
11953
  directory: import_zod2.z.string().optional().describe("Filter by directory path (e.g., 'src/utils', 'lib')"),
10718
11954
  chunkType: import_zod2.z.enum(CHUNK_TYPE_ENUM).optional().describe("Filter by code chunk type"),
10719
- contextLines: import_zod2.z.number().optional().describe("Number of extra lines to include before/after each match (default: 0)")
11955
+ contextLines: import_zod2.z.number().optional().describe("Number of extra lines to include before/after each match (default: 0)"),
11956
+ blameAuthor: import_zod2.z.string().optional().describe("Filter by git blame author name or email"),
11957
+ blameSha: import_zod2.z.string().optional().describe("Filter by git blame commit SHA or prefix"),
11958
+ blameSince: import_zod2.z.string().optional().describe("Filter to chunks last changed on or after this date")
10720
11959
  },
10721
11960
  async (args) => {
10722
11961
  const results = await searchCodebase(runtime.projectRoot, runtime.host, args.query, {
@@ -10724,21 +11963,17 @@ function registerMcpTools(server, runtime) {
10724
11963
  fileType: args.fileType,
10725
11964
  directory: args.directory,
10726
11965
  chunkType: args.chunkType,
10727
- contextLines: args.contextLines
11966
+ contextLines: args.contextLines,
11967
+ blameAuthor: args.blameAuthor,
11968
+ blameSha: args.blameSha,
11969
+ blameSince: args.blameSince
10728
11970
  });
10729
11971
  if (results.length === 0) {
10730
11972
  return { content: [{ type: "text", text: "No matching code found. Try a different query or run index_codebase first." }] };
10731
11973
  }
10732
- const formatted = results.map((r, idx) => {
10733
- const header = r.name ? `[${idx + 1}] ${r.chunkType} "${r.name}" in ${r.filePath}:${r.startLine}-${r.endLine}` : `[${idx + 1}] ${r.chunkType} in ${r.filePath}:${r.startLine}-${r.endLine}`;
10734
- return `${header} (score: ${r.score.toFixed(2)})
10735
- \`\`\`
10736
- ${truncateContent2(r.content)}
10737
- \`\`\``;
10738
- });
10739
11974
  return { content: [{ type: "text", text: `Found ${results.length} results for "${args.query}":
10740
11975
 
10741
- ${formatted.join("\n\n")}` }] };
11976
+ ${formatSearchResults(results, "score")}` }] };
10742
11977
  }
10743
11978
  );
10744
11979
  server.tool(
@@ -10749,7 +11984,10 @@ ${formatted.join("\n\n")}` }] };
10749
11984
  limit: import_zod2.z.number().optional().default(10).describe("Maximum number of results to return"),
10750
11985
  fileType: import_zod2.z.string().optional().describe("Filter by file extension (e.g., 'ts', 'py', 'rs')"),
10751
11986
  directory: import_zod2.z.string().optional().describe("Filter by directory path (e.g., 'src/utils', 'lib')"),
10752
- chunkType: import_zod2.z.enum(CHUNK_TYPE_ENUM).optional().describe("Filter by code chunk type")
11987
+ chunkType: import_zod2.z.enum(CHUNK_TYPE_ENUM).optional().describe("Filter by code chunk type"),
11988
+ blameAuthor: import_zod2.z.string().optional().describe("Filter by git blame author name or email"),
11989
+ blameSha: import_zod2.z.string().optional().describe("Filter by git blame commit SHA or prefix"),
11990
+ blameSince: import_zod2.z.string().optional().describe("Filter to chunks last changed on or after this date")
10753
11991
  },
10754
11992
  async (args) => {
10755
11993
  const results = await searchCodebase(runtime.projectRoot, runtime.host, args.query, {
@@ -10757,19 +11995,17 @@ ${formatted.join("\n\n")}` }] };
10757
11995
  fileType: args.fileType,
10758
11996
  directory: args.directory,
10759
11997
  chunkType: args.chunkType,
10760
- metadataOnly: true
11998
+ metadataOnly: true,
11999
+ blameAuthor: args.blameAuthor,
12000
+ blameSha: args.blameSha,
12001
+ blameSince: args.blameSince
10761
12002
  });
10762
12003
  if (results.length === 0) {
10763
12004
  return { content: [{ type: "text", text: "No matching code found. Try a different query or run index_codebase first." }] };
10764
12005
  }
10765
- const formatted = results.map((r, idx) => {
10766
- const location = `${r.filePath}:${r.startLine}-${r.endLine}`;
10767
- const name = r.name ? `"${r.name}"` : "(anonymous)";
10768
- return `[${idx + 1}] ${r.chunkType} ${name} at ${location} (score: ${r.score.toFixed(2)})`;
10769
- });
10770
12006
  return { content: [{ type: "text", text: `Found ${results.length} locations for "${args.query}":
10771
12007
 
10772
- ${formatted.join("\n")}
12008
+ ${formatCodebasePeek(results)}
10773
12009
 
10774
12010
  Use Read tool to examine specific files.` }] };
10775
12011
  }
@@ -10784,8 +12020,13 @@ Use Read tool to examine specific files.` }] };
10784
12020
  },
10785
12021
  async (args) => {
10786
12022
  const result = await runIndexCodebase(runtime.projectRoot, runtime.host, args);
10787
- const text = result.kind === "estimate" ? formatCostEstimate(result.estimate) : formatIndexStats(result.stats, args.verbose ?? false);
10788
- return { content: [{ type: "text", text }] };
12023
+ if (result.kind === "estimate") {
12024
+ return { content: [{ type: "text", text: formatCostEstimate(result.estimate) }] };
12025
+ }
12026
+ if (result.kind === "busy") {
12027
+ return { content: [{ type: "text", text: result.text }], isError: true };
12028
+ }
12029
+ return { content: [{ type: "text", text: formatIndexStats(result.stats, args.verbose ?? false) }] };
10789
12030
  }
10790
12031
  );
10791
12032
  server.tool(
@@ -10802,8 +12043,11 @@ Use Read tool to examine specific files.` }] };
10802
12043
  "Check index health and remove stale entries from deleted files. Run this to clean up the index after files have been deleted.",
10803
12044
  {},
10804
12045
  async () => {
10805
- const result = await getIndexHealthCheck(runtime.projectRoot, runtime.host);
10806
- return { content: [{ type: "text", text: formatHealthCheck(result) }] };
12046
+ const result = await runIndexHealthCheck(runtime.projectRoot, runtime.host);
12047
+ if (result.kind === "busy") {
12048
+ return { content: [{ type: "text", text: result.text }], isError: true };
12049
+ }
12050
+ return { content: [{ type: "text", text: formatHealthCheck(result.health) }] };
10807
12051
  }
10808
12052
  );
10809
12053
  server.tool(
@@ -10850,16 +12094,9 @@ Use Read tool to examine specific files.` }] };
10850
12094
  if (results.length === 0) {
10851
12095
  return { content: [{ type: "text", text: "No similar code found. Try a different snippet or run index_codebase first." }] };
10852
12096
  }
10853
- const formatted = results.map((r, idx) => {
10854
- const header = r.name ? `[${idx + 1}] ${r.chunkType} "${r.name}" in ${r.filePath}:${r.startLine}-${r.endLine}` : `[${idx + 1}] ${r.chunkType} in ${r.filePath}:${r.startLine}-${r.endLine}`;
10855
- return `${header} (similarity: ${(r.score * 100).toFixed(1)}%)
10856
- \`\`\`
10857
- ${truncateContent2(r.content)}
10858
- \`\`\``;
10859
- });
10860
12097
  return { content: [{ type: "text", text: `Found ${results.length} similar code blocks:
10861
12098
 
10862
- ${formatted.join("\n\n")}` }] };
12099
+ ${formatSearchResults(results)}` }] };
10863
12100
  }
10864
12101
  );
10865
12102
  server.tool(
@@ -10895,28 +12132,10 @@ ${formatted.join("\n\n")}` }] };
10895
12132
  return { content: [{ type: "text", text: "Error: 'symbolId' is required when direction is 'callees'." }] };
10896
12133
  }
10897
12134
  const { callees } = await getCallGraphData(runtime.projectRoot, runtime.host, args);
10898
- if (callees.length === 0) {
10899
- return { content: [{ type: "text", text: `No callees found for symbol ${args.symbolId}${args.relationshipType ? ` with type ${args.relationshipType}` : ""}.` }] };
10900
- }
10901
- const formatted2 = callees.map((e, i) => {
10902
- const conf = e.confidence !== "Direct" ? ` [${e.confidence.toLowerCase()}]` : "";
10903
- return `[${i + 1}] \u2192 ${e.targetName} (${e.callType})${conf} at line ${e.line}${e.isResolved ? ` [resolved: ${e.toSymbolId}]` : " [unresolved]"}`;
10904
- });
10905
- return { content: [{ type: "text", text: `Callees (${callees.length}):
10906
-
10907
- ${formatted2.join("\n")}` }] };
12135
+ return { content: [{ type: "text", text: formatCallGraphCallees(args.symbolId, callees, args.relationshipType) }] };
10908
12136
  }
10909
12137
  const { callers } = await getCallGraphData(runtime.projectRoot, runtime.host, args);
10910
- if (callers.length === 0) {
10911
- return { content: [{ type: "text", text: `No callers found for "${args.name}"${args.relationshipType ? ` with type ${args.relationshipType}` : ""}.` }] };
10912
- }
10913
- const formatted = callers.map((e, i) => {
10914
- const conf = e.confidence !== "Direct" ? ` [${e.confidence.toLowerCase()}]` : "";
10915
- return `[${i + 1}] \u2190 from ${e.fromSymbolName ?? "<unknown>"} in ${e.fromSymbolFilePath ?? "<unknown file>"} [${e.fromSymbolId}] (${e.callType})${conf} at line ${e.line}${e.isResolved ? " [resolved]" : " [unresolved]"}`;
10916
- });
10917
- return { content: [{ type: "text", text: `"${args.name}" is called by ${callers.length} function(s):
10918
-
10919
- ${formatted.join("\n")}` }] };
12138
+ return { content: [{ type: "text", text: formatCallGraphCallers(args.name, callers, args.relationshipType) }] };
10920
12139
  }
10921
12140
  );
10922
12141
  server.tool(
@@ -10928,17 +12147,8 @@ ${formatted.join("\n")}` }] };
10928
12147
  maxDepth: import_zod2.z.number().optional().default(10).describe("Maximum traversal depth (default: 10)")
10929
12148
  },
10930
12149
  async (args) => {
10931
- const path25 = await getCallGraphPath(runtime.projectRoot, runtime.host, args.from, args.to, args.maxDepth);
10932
- if (path25.length === 0) {
10933
- return { content: [{ type: "text", text: `No path found between "${args.from}" and "${args.to}". They may be in disconnected components, or the call graph index needs updating.` }] };
10934
- }
10935
- const formatted = path25.map((hop, i) => {
10936
- const prefix = i === 0 ? "[start]" : `--${hop.callType}-->`;
10937
- const location = hop.filePath ? ` (${hop.filePath}:${hop.line})` : "";
10938
- return `${prefix} ${hop.symbolName}${location}`;
10939
- });
10940
- return { content: [{ type: "text", text: `Path (${path25.length} hops):
10941
- ${formatted.join("\n")}` }] };
12150
+ const path27 = await getCallGraphPath(runtime.projectRoot, runtime.host, args.from, args.to, args.maxDepth);
12151
+ return { content: [{ type: "text", text: formatCallGraphPath(args.from, args.to, path27) }] };
10942
12152
  }
10943
12153
  );
10944
12154
  server.tool(
@@ -10974,7 +12184,7 @@ ${formatted.join("\n")}` }] };
10974
12184
  // src/mcp-server.ts
10975
12185
  var import_meta2 = {};
10976
12186
  function getPackageVersion() {
10977
- const raw = JSON.parse((0, import_fs14.readFileSync)(new URL("../package.json", import_meta2.url), "utf-8"));
12187
+ const raw = JSON.parse((0, import_fs15.readFileSync)(new URL("../package.json", import_meta2.url), "utf-8"));
10978
12188
  if (raw && typeof raw === "object" && "version" in raw && typeof raw.version === "string") {
10979
12189
  return raw.version;
10980
12190
  }
@@ -10995,18 +12205,37 @@ function createMcpServer(projectRoot, config, host = "opencode") {
10995
12205
  }
10996
12206
 
10997
12207
  // src/utils/auto-index.ts
12208
+ var INITIAL_RETRY_DELAY_MS = 50;
12209
+ var MAX_RETRY_DELAY_MS = 500;
12210
+ var autoIndexStates = /* @__PURE__ */ new WeakMap();
10998
12211
  function getErrorMessage3(error) {
10999
12212
  return error instanceof Error ? error.message : String(error);
11000
12213
  }
11001
- function startAutoIndex(indexer, projectRoot) {
11002
- indexer.initialize().then(() => {
11003
- indexer.index().catch((error) => {
11004
- console.error(`[codebase-index] Auto-index failed for "${projectRoot}": ${getErrorMessage3(error)}`);
11005
- });
12214
+ function runAutoIndex(indexer, projectRoot, state) {
12215
+ void indexer.index().then(() => {
12216
+ autoIndexStates.delete(indexer);
11006
12217
  }).catch((error) => {
11007
- console.error(`[codebase-index] Auto-index initialization failed for "${projectRoot}": ${getErrorMessage3(error)}`);
12218
+ if (!isTransientIndexLockContention(error)) {
12219
+ autoIndexStates.delete(indexer);
12220
+ console.error(`[codebase-index] Auto-index failed for "${projectRoot}": ${getErrorMessage3(error)}`);
12221
+ return;
12222
+ }
12223
+ const retryDelayMs = state.retryDelayMs;
12224
+ state.retryDelayMs = Math.min(retryDelayMs * 2, MAX_RETRY_DELAY_MS);
12225
+ const retryTimer = setTimeout(() => {
12226
+ runAutoIndex(indexer, projectRoot, state);
12227
+ }, retryDelayMs);
12228
+ retryTimer.unref?.();
11008
12229
  });
11009
12230
  }
12231
+ function startAutoIndex(indexer, projectRoot) {
12232
+ if (autoIndexStates.has(indexer)) return;
12233
+ const state = {
12234
+ retryDelayMs: INITIAL_RETRY_DELAY_MS
12235
+ };
12236
+ autoIndexStates.set(indexer, state);
12237
+ runAutoIndex(indexer, projectRoot, state);
12238
+ }
11010
12239
 
11011
12240
  // node_modules/chokidar/index.js
11012
12241
  var import_node_events = require("events");
@@ -11098,7 +12327,7 @@ var ReaddirpStream = class extends import_node_stream.Readable {
11098
12327
  this._directoryFilter = normalizeFilter(opts.directoryFilter);
11099
12328
  const statMethod = opts.lstat ? import_promises.lstat : import_promises.stat;
11100
12329
  if (wantBigintFsStats) {
11101
- this._stat = (path25) => statMethod(path25, { bigint: true });
12330
+ this._stat = (path27) => statMethod(path27, { bigint: true });
11102
12331
  } else {
11103
12332
  this._stat = statMethod;
11104
12333
  }
@@ -11123,8 +12352,8 @@ var ReaddirpStream = class extends import_node_stream.Readable {
11123
12352
  const par = this.parent;
11124
12353
  const fil = par && par.files;
11125
12354
  if (fil && fil.length > 0) {
11126
- const { path: path25, depth } = par;
11127
- const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path25));
12355
+ const { path: path27, depth } = par;
12356
+ const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path27));
11128
12357
  const awaited = await Promise.all(slice);
11129
12358
  for (const entry of awaited) {
11130
12359
  if (!entry)
@@ -11164,20 +12393,20 @@ var ReaddirpStream = class extends import_node_stream.Readable {
11164
12393
  this.reading = false;
11165
12394
  }
11166
12395
  }
11167
- async _exploreDir(path25, depth) {
12396
+ async _exploreDir(path27, depth) {
11168
12397
  let files;
11169
12398
  try {
11170
- files = await (0, import_promises.readdir)(path25, this._rdOptions);
12399
+ files = await (0, import_promises.readdir)(path27, this._rdOptions);
11171
12400
  } catch (error) {
11172
12401
  this._onError(error);
11173
12402
  }
11174
- return { files, depth, path: path25 };
12403
+ return { files, depth, path: path27 };
11175
12404
  }
11176
- async _formatEntry(dirent, path25) {
12405
+ async _formatEntry(dirent, path27) {
11177
12406
  let entry;
11178
12407
  const basename5 = this._isDirent ? dirent.name : dirent;
11179
12408
  try {
11180
- const fullPath = (0, import_node_path.resolve)((0, import_node_path.join)(path25, basename5));
12409
+ const fullPath = (0, import_node_path.resolve)((0, import_node_path.join)(path27, basename5));
11181
12410
  entry = { path: (0, import_node_path.relative)(this._root, fullPath), fullPath, basename: basename5 };
11182
12411
  entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
11183
12412
  } catch (err) {
@@ -11577,16 +12806,16 @@ var delFromSet = (main2, prop, item) => {
11577
12806
  };
11578
12807
  var isEmptySet = (val) => val instanceof Set ? val.size === 0 : !val;
11579
12808
  var FsWatchInstances = /* @__PURE__ */ new Map();
11580
- function createFsWatchInstance(path25, options, listener, errHandler, emitRaw) {
12809
+ function createFsWatchInstance(path27, options, listener, errHandler, emitRaw) {
11581
12810
  const handleEvent = (rawEvent, evPath) => {
11582
- listener(path25);
11583
- emitRaw(rawEvent, evPath, { watchedPath: path25 });
11584
- if (evPath && path25 !== evPath) {
11585
- fsWatchBroadcast(sp.resolve(path25, evPath), KEY_LISTENERS, sp.join(path25, evPath));
12811
+ listener(path27);
12812
+ emitRaw(rawEvent, evPath, { watchedPath: path27 });
12813
+ if (evPath && path27 !== evPath) {
12814
+ fsWatchBroadcast(sp.resolve(path27, evPath), KEY_LISTENERS, sp.join(path27, evPath));
11586
12815
  }
11587
12816
  };
11588
12817
  try {
11589
- return (0, import_node_fs.watch)(path25, {
12818
+ return (0, import_node_fs.watch)(path27, {
11590
12819
  persistent: options.persistent
11591
12820
  }, handleEvent);
11592
12821
  } catch (error) {
@@ -11602,12 +12831,12 @@ var fsWatchBroadcast = (fullPath, listenerType, val1, val2, val3) => {
11602
12831
  listener(val1, val2, val3);
11603
12832
  });
11604
12833
  };
11605
- var setFsWatchListener = (path25, fullPath, options, handlers) => {
12834
+ var setFsWatchListener = (path27, fullPath, options, handlers) => {
11606
12835
  const { listener, errHandler, rawEmitter } = handlers;
11607
12836
  let cont = FsWatchInstances.get(fullPath);
11608
12837
  let watcher;
11609
12838
  if (!options.persistent) {
11610
- watcher = createFsWatchInstance(path25, options, listener, errHandler, rawEmitter);
12839
+ watcher = createFsWatchInstance(path27, options, listener, errHandler, rawEmitter);
11611
12840
  if (!watcher)
11612
12841
  return;
11613
12842
  return watcher.close.bind(watcher);
@@ -11618,7 +12847,7 @@ var setFsWatchListener = (path25, fullPath, options, handlers) => {
11618
12847
  addAndConvert(cont, KEY_RAW, rawEmitter);
11619
12848
  } else {
11620
12849
  watcher = createFsWatchInstance(
11621
- path25,
12850
+ path27,
11622
12851
  options,
11623
12852
  fsWatchBroadcast.bind(null, fullPath, KEY_LISTENERS),
11624
12853
  errHandler,
@@ -11633,7 +12862,7 @@ var setFsWatchListener = (path25, fullPath, options, handlers) => {
11633
12862
  cont.watcherUnusable = true;
11634
12863
  if (isWindows && error.code === "EPERM") {
11635
12864
  try {
11636
- const fd = await (0, import_promises2.open)(path25, "r");
12865
+ const fd = await (0, import_promises2.open)(path27, "r");
11637
12866
  await fd.close();
11638
12867
  broadcastErr(error);
11639
12868
  } catch (err) {
@@ -11664,7 +12893,7 @@ var setFsWatchListener = (path25, fullPath, options, handlers) => {
11664
12893
  };
11665
12894
  };
11666
12895
  var FsWatchFileInstances = /* @__PURE__ */ new Map();
11667
- var setFsWatchFileListener = (path25, fullPath, options, handlers) => {
12896
+ var setFsWatchFileListener = (path27, fullPath, options, handlers) => {
11668
12897
  const { listener, rawEmitter } = handlers;
11669
12898
  let cont = FsWatchFileInstances.get(fullPath);
11670
12899
  const copts = cont && cont.options;
@@ -11686,7 +12915,7 @@ var setFsWatchFileListener = (path25, fullPath, options, handlers) => {
11686
12915
  });
11687
12916
  const currmtime = curr.mtimeMs;
11688
12917
  if (curr.size !== prev.size || currmtime > prev.mtimeMs || currmtime === 0) {
11689
- foreach(cont.listeners, (listener2) => listener2(path25, curr));
12918
+ foreach(cont.listeners, (listener2) => listener2(path27, curr));
11690
12919
  }
11691
12920
  })
11692
12921
  };
@@ -11716,13 +12945,13 @@ var NodeFsHandler = class {
11716
12945
  * @param listener on fs change
11717
12946
  * @returns closer for the watcher instance
11718
12947
  */
11719
- _watchWithNodeFs(path25, listener) {
12948
+ _watchWithNodeFs(path27, listener) {
11720
12949
  const opts = this.fsw.options;
11721
- const directory = sp.dirname(path25);
11722
- const basename5 = sp.basename(path25);
12950
+ const directory = sp.dirname(path27);
12951
+ const basename5 = sp.basename(path27);
11723
12952
  const parent = this.fsw._getWatchedDir(directory);
11724
12953
  parent.add(basename5);
11725
- const absolutePath = sp.resolve(path25);
12954
+ const absolutePath = sp.resolve(path27);
11726
12955
  const options = {
11727
12956
  persistent: opts.persistent
11728
12957
  };
@@ -11732,12 +12961,12 @@ var NodeFsHandler = class {
11732
12961
  if (opts.usePolling) {
11733
12962
  const enableBin = opts.interval !== opts.binaryInterval;
11734
12963
  options.interval = enableBin && isBinaryPath(basename5) ? opts.binaryInterval : opts.interval;
11735
- closer = setFsWatchFileListener(path25, absolutePath, options, {
12964
+ closer = setFsWatchFileListener(path27, absolutePath, options, {
11736
12965
  listener,
11737
12966
  rawEmitter: this.fsw._emitRaw
11738
12967
  });
11739
12968
  } else {
11740
- closer = setFsWatchListener(path25, absolutePath, options, {
12969
+ closer = setFsWatchListener(path27, absolutePath, options, {
11741
12970
  listener,
11742
12971
  errHandler: this._boundHandleError,
11743
12972
  rawEmitter: this.fsw._emitRaw
@@ -11759,7 +12988,7 @@ var NodeFsHandler = class {
11759
12988
  let prevStats = stats;
11760
12989
  if (parent.has(basename5))
11761
12990
  return;
11762
- const listener = async (path25, newStats) => {
12991
+ const listener = async (path27, newStats) => {
11763
12992
  if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5))
11764
12993
  return;
11765
12994
  if (!newStats || newStats.mtimeMs === 0) {
@@ -11773,11 +13002,11 @@ var NodeFsHandler = class {
11773
13002
  this.fsw._emit(EV.CHANGE, file, newStats2);
11774
13003
  }
11775
13004
  if ((isMacos || isLinux || isFreeBSD) && prevStats.ino !== newStats2.ino) {
11776
- this.fsw._closeFile(path25);
13005
+ this.fsw._closeFile(path27);
11777
13006
  prevStats = newStats2;
11778
13007
  const closer2 = this._watchWithNodeFs(file, listener);
11779
13008
  if (closer2)
11780
- this.fsw._addPathCloser(path25, closer2);
13009
+ this.fsw._addPathCloser(path27, closer2);
11781
13010
  } else {
11782
13011
  prevStats = newStats2;
11783
13012
  }
@@ -11809,7 +13038,7 @@ var NodeFsHandler = class {
11809
13038
  * @param item basename of this item
11810
13039
  * @returns true if no more processing is needed for this entry.
11811
13040
  */
11812
- async _handleSymlink(entry, directory, path25, item) {
13041
+ async _handleSymlink(entry, directory, path27, item) {
11813
13042
  if (this.fsw.closed) {
11814
13043
  return;
11815
13044
  }
@@ -11819,7 +13048,7 @@ var NodeFsHandler = class {
11819
13048
  this.fsw._incrReadyCount();
11820
13049
  let linkPath;
11821
13050
  try {
11822
- linkPath = await (0, import_promises2.realpath)(path25);
13051
+ linkPath = await (0, import_promises2.realpath)(path27);
11823
13052
  } catch (e) {
11824
13053
  this.fsw._emitReady();
11825
13054
  return true;
@@ -11829,12 +13058,12 @@ var NodeFsHandler = class {
11829
13058
  if (dir.has(item)) {
11830
13059
  if (this.fsw._symlinkPaths.get(full) !== linkPath) {
11831
13060
  this.fsw._symlinkPaths.set(full, linkPath);
11832
- this.fsw._emit(EV.CHANGE, path25, entry.stats);
13061
+ this.fsw._emit(EV.CHANGE, path27, entry.stats);
11833
13062
  }
11834
13063
  } else {
11835
13064
  dir.add(item);
11836
13065
  this.fsw._symlinkPaths.set(full, linkPath);
11837
- this.fsw._emit(EV.ADD, path25, entry.stats);
13066
+ this.fsw._emit(EV.ADD, path27, entry.stats);
11838
13067
  }
11839
13068
  this.fsw._emitReady();
11840
13069
  return true;
@@ -11864,9 +13093,9 @@ var NodeFsHandler = class {
11864
13093
  return;
11865
13094
  }
11866
13095
  const item = entry.path;
11867
- let path25 = sp.join(directory, item);
13096
+ let path27 = sp.join(directory, item);
11868
13097
  current.add(item);
11869
- if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path25, item)) {
13098
+ if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path27, item)) {
11870
13099
  return;
11871
13100
  }
11872
13101
  if (this.fsw.closed) {
@@ -11875,8 +13104,8 @@ var NodeFsHandler = class {
11875
13104
  }
11876
13105
  if (item === target || !target && !previous.has(item)) {
11877
13106
  this.fsw._incrReadyCount();
11878
- path25 = sp.join(dir, sp.relative(dir, path25));
11879
- this._addToNodeFs(path25, initialAdd, wh, depth + 1);
13107
+ path27 = sp.join(dir, sp.relative(dir, path27));
13108
+ this._addToNodeFs(path27, initialAdd, wh, depth + 1);
11880
13109
  }
11881
13110
  }).on(EV.ERROR, this._boundHandleError);
11882
13111
  return new Promise((resolve15, reject) => {
@@ -11945,13 +13174,13 @@ var NodeFsHandler = class {
11945
13174
  * @param depth Child path actually targeted for watch
11946
13175
  * @param target Child path actually targeted for watch
11947
13176
  */
11948
- async _addToNodeFs(path25, initialAdd, priorWh, depth, target) {
13177
+ async _addToNodeFs(path27, initialAdd, priorWh, depth, target) {
11949
13178
  const ready = this.fsw._emitReady;
11950
- if (this.fsw._isIgnored(path25) || this.fsw.closed) {
13179
+ if (this.fsw._isIgnored(path27) || this.fsw.closed) {
11951
13180
  ready();
11952
13181
  return false;
11953
13182
  }
11954
- const wh = this.fsw._getWatchHelpers(path25);
13183
+ const wh = this.fsw._getWatchHelpers(path27);
11955
13184
  if (priorWh) {
11956
13185
  wh.filterPath = (entry) => priorWh.filterPath(entry);
11957
13186
  wh.filterDir = (entry) => priorWh.filterDir(entry);
@@ -11967,8 +13196,8 @@ var NodeFsHandler = class {
11967
13196
  const follow = this.fsw.options.followSymlinks;
11968
13197
  let closer;
11969
13198
  if (stats.isDirectory()) {
11970
- const absPath = sp.resolve(path25);
11971
- const targetPath = follow ? await (0, import_promises2.realpath)(path25) : path25;
13199
+ const absPath = sp.resolve(path27);
13200
+ const targetPath = follow ? await (0, import_promises2.realpath)(path27) : path27;
11972
13201
  if (this.fsw.closed)
11973
13202
  return;
11974
13203
  closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath);
@@ -11978,29 +13207,29 @@ var NodeFsHandler = class {
11978
13207
  this.fsw._symlinkPaths.set(absPath, targetPath);
11979
13208
  }
11980
13209
  } else if (stats.isSymbolicLink()) {
11981
- const targetPath = follow ? await (0, import_promises2.realpath)(path25) : path25;
13210
+ const targetPath = follow ? await (0, import_promises2.realpath)(path27) : path27;
11982
13211
  if (this.fsw.closed)
11983
13212
  return;
11984
13213
  const parent = sp.dirname(wh.watchPath);
11985
13214
  this.fsw._getWatchedDir(parent).add(wh.watchPath);
11986
13215
  this.fsw._emit(EV.ADD, wh.watchPath, stats);
11987
- closer = await this._handleDir(parent, stats, initialAdd, depth, path25, wh, targetPath);
13216
+ closer = await this._handleDir(parent, stats, initialAdd, depth, path27, wh, targetPath);
11988
13217
  if (this.fsw.closed)
11989
13218
  return;
11990
13219
  if (targetPath !== void 0) {
11991
- this.fsw._symlinkPaths.set(sp.resolve(path25), targetPath);
13220
+ this.fsw._symlinkPaths.set(sp.resolve(path27), targetPath);
11992
13221
  }
11993
13222
  } else {
11994
13223
  closer = this._handleFile(wh.watchPath, stats, initialAdd);
11995
13224
  }
11996
13225
  ready();
11997
13226
  if (closer)
11998
- this.fsw._addPathCloser(path25, closer);
13227
+ this.fsw._addPathCloser(path27, closer);
11999
13228
  return false;
12000
13229
  } catch (error) {
12001
13230
  if (this.fsw._handleError(error)) {
12002
13231
  ready();
12003
- return path25;
13232
+ return path27;
12004
13233
  }
12005
13234
  }
12006
13235
  }
@@ -12032,35 +13261,35 @@ function createPattern(matcher) {
12032
13261
  if (matcher.path === string)
12033
13262
  return true;
12034
13263
  if (matcher.recursive) {
12035
- const relative10 = sp2.relative(matcher.path, string);
12036
- if (!relative10) {
13264
+ const relative11 = sp2.relative(matcher.path, string);
13265
+ if (!relative11) {
12037
13266
  return false;
12038
13267
  }
12039
- return !relative10.startsWith("..") && !sp2.isAbsolute(relative10);
13268
+ return !relative11.startsWith("..") && !sp2.isAbsolute(relative11);
12040
13269
  }
12041
13270
  return false;
12042
13271
  };
12043
13272
  }
12044
13273
  return () => false;
12045
13274
  }
12046
- function normalizePath2(path25) {
12047
- if (typeof path25 !== "string")
13275
+ function normalizePath2(path27) {
13276
+ if (typeof path27 !== "string")
12048
13277
  throw new Error("string expected");
12049
- path25 = sp2.normalize(path25);
12050
- path25 = path25.replace(/\\/g, "/");
13278
+ path27 = sp2.normalize(path27);
13279
+ path27 = path27.replace(/\\/g, "/");
12051
13280
  let prepend = false;
12052
- if (path25.startsWith("//"))
13281
+ if (path27.startsWith("//"))
12053
13282
  prepend = true;
12054
- path25 = path25.replace(DOUBLE_SLASH_RE, "/");
13283
+ path27 = path27.replace(DOUBLE_SLASH_RE, "/");
12055
13284
  if (prepend)
12056
- path25 = "/" + path25;
12057
- return path25;
13285
+ path27 = "/" + path27;
13286
+ return path27;
12058
13287
  }
12059
13288
  function matchPatterns(patterns, testString, stats) {
12060
- const path25 = normalizePath2(testString);
13289
+ const path27 = normalizePath2(testString);
12061
13290
  for (let index = 0; index < patterns.length; index++) {
12062
13291
  const pattern = patterns[index];
12063
- if (pattern(path25, stats)) {
13292
+ if (pattern(path27, stats)) {
12064
13293
  return true;
12065
13294
  }
12066
13295
  }
@@ -12098,19 +13327,19 @@ var toUnix = (string) => {
12098
13327
  }
12099
13328
  return str;
12100
13329
  };
12101
- var normalizePathToUnix = (path25) => toUnix(sp2.normalize(toUnix(path25)));
12102
- var normalizeIgnored = (cwd = "") => (path25) => {
12103
- if (typeof path25 === "string") {
12104
- return normalizePathToUnix(sp2.isAbsolute(path25) ? path25 : sp2.join(cwd, path25));
13330
+ var normalizePathToUnix = (path27) => toUnix(sp2.normalize(toUnix(path27)));
13331
+ var normalizeIgnored = (cwd = "") => (path27) => {
13332
+ if (typeof path27 === "string") {
13333
+ return normalizePathToUnix(sp2.isAbsolute(path27) ? path27 : sp2.join(cwd, path27));
12105
13334
  } else {
12106
- return path25;
13335
+ return path27;
12107
13336
  }
12108
13337
  };
12109
- var getAbsolutePath = (path25, cwd) => {
12110
- if (sp2.isAbsolute(path25)) {
12111
- return path25;
13338
+ var getAbsolutePath = (path27, cwd) => {
13339
+ if (sp2.isAbsolute(path27)) {
13340
+ return path27;
12112
13341
  }
12113
- return sp2.join(cwd, path25);
13342
+ return sp2.join(cwd, path27);
12114
13343
  };
12115
13344
  var EMPTY_SET = Object.freeze(/* @__PURE__ */ new Set());
12116
13345
  var DirEntry = class {
@@ -12175,10 +13404,10 @@ var WatchHelper = class {
12175
13404
  dirParts;
12176
13405
  followSymlinks;
12177
13406
  statMethod;
12178
- constructor(path25, follow, fsw) {
13407
+ constructor(path27, follow, fsw) {
12179
13408
  this.fsw = fsw;
12180
- const watchPath = path25;
12181
- this.path = path25 = path25.replace(REPLACER_RE, "");
13409
+ const watchPath = path27;
13410
+ this.path = path27 = path27.replace(REPLACER_RE, "");
12182
13411
  this.watchPath = watchPath;
12183
13412
  this.fullWatchPath = sp2.resolve(watchPath);
12184
13413
  this.dirParts = [];
@@ -12318,20 +13547,20 @@ var FSWatcher = class extends import_node_events.EventEmitter {
12318
13547
  this._closePromise = void 0;
12319
13548
  let paths = unifyPaths(paths_);
12320
13549
  if (cwd) {
12321
- paths = paths.map((path25) => {
12322
- const absPath = getAbsolutePath(path25, cwd);
13550
+ paths = paths.map((path27) => {
13551
+ const absPath = getAbsolutePath(path27, cwd);
12323
13552
  return absPath;
12324
13553
  });
12325
13554
  }
12326
- paths.forEach((path25) => {
12327
- this._removeIgnoredPath(path25);
13555
+ paths.forEach((path27) => {
13556
+ this._removeIgnoredPath(path27);
12328
13557
  });
12329
13558
  this._userIgnored = void 0;
12330
13559
  if (!this._readyCount)
12331
13560
  this._readyCount = 0;
12332
13561
  this._readyCount += paths.length;
12333
- Promise.all(paths.map(async (path25) => {
12334
- const res = await this._nodeFsHandler._addToNodeFs(path25, !_internal, void 0, 0, _origAdd);
13562
+ Promise.all(paths.map(async (path27) => {
13563
+ const res = await this._nodeFsHandler._addToNodeFs(path27, !_internal, void 0, 0, _origAdd);
12335
13564
  if (res)
12336
13565
  this._emitReady();
12337
13566
  return res;
@@ -12353,17 +13582,17 @@ var FSWatcher = class extends import_node_events.EventEmitter {
12353
13582
  return this;
12354
13583
  const paths = unifyPaths(paths_);
12355
13584
  const { cwd } = this.options;
12356
- paths.forEach((path25) => {
12357
- if (!sp2.isAbsolute(path25) && !this._closers.has(path25)) {
13585
+ paths.forEach((path27) => {
13586
+ if (!sp2.isAbsolute(path27) && !this._closers.has(path27)) {
12358
13587
  if (cwd)
12359
- path25 = sp2.join(cwd, path25);
12360
- path25 = sp2.resolve(path25);
13588
+ path27 = sp2.join(cwd, path27);
13589
+ path27 = sp2.resolve(path27);
12361
13590
  }
12362
- this._closePath(path25);
12363
- this._addIgnoredPath(path25);
12364
- if (this._watched.has(path25)) {
13591
+ this._closePath(path27);
13592
+ this._addIgnoredPath(path27);
13593
+ if (this._watched.has(path27)) {
12365
13594
  this._addIgnoredPath({
12366
- path: path25,
13595
+ path: path27,
12367
13596
  recursive: true
12368
13597
  });
12369
13598
  }
@@ -12427,38 +13656,38 @@ var FSWatcher = class extends import_node_events.EventEmitter {
12427
13656
  * @param stats arguments to be passed with event
12428
13657
  * @returns the error if defined, otherwise the value of the FSWatcher instance's `closed` flag
12429
13658
  */
12430
- async _emit(event, path25, stats) {
13659
+ async _emit(event, path27, stats) {
12431
13660
  if (this.closed)
12432
13661
  return;
12433
13662
  const opts = this.options;
12434
13663
  if (isWindows)
12435
- path25 = sp2.normalize(path25);
13664
+ path27 = sp2.normalize(path27);
12436
13665
  if (opts.cwd)
12437
- path25 = sp2.relative(opts.cwd, path25);
12438
- const args = [path25];
13666
+ path27 = sp2.relative(opts.cwd, path27);
13667
+ const args = [path27];
12439
13668
  if (stats != null)
12440
13669
  args.push(stats);
12441
13670
  const awf = opts.awaitWriteFinish;
12442
13671
  let pw;
12443
- if (awf && (pw = this._pendingWrites.get(path25))) {
13672
+ if (awf && (pw = this._pendingWrites.get(path27))) {
12444
13673
  pw.lastChange = /* @__PURE__ */ new Date();
12445
13674
  return this;
12446
13675
  }
12447
13676
  if (opts.atomic) {
12448
13677
  if (event === EVENTS.UNLINK) {
12449
- this._pendingUnlinks.set(path25, [event, ...args]);
13678
+ this._pendingUnlinks.set(path27, [event, ...args]);
12450
13679
  setTimeout(() => {
12451
- this._pendingUnlinks.forEach((entry, path26) => {
13680
+ this._pendingUnlinks.forEach((entry, path28) => {
12452
13681
  this.emit(...entry);
12453
13682
  this.emit(EVENTS.ALL, ...entry);
12454
- this._pendingUnlinks.delete(path26);
13683
+ this._pendingUnlinks.delete(path28);
12455
13684
  });
12456
13685
  }, typeof opts.atomic === "number" ? opts.atomic : 100);
12457
13686
  return this;
12458
13687
  }
12459
- if (event === EVENTS.ADD && this._pendingUnlinks.has(path25)) {
13688
+ if (event === EVENTS.ADD && this._pendingUnlinks.has(path27)) {
12460
13689
  event = EVENTS.CHANGE;
12461
- this._pendingUnlinks.delete(path25);
13690
+ this._pendingUnlinks.delete(path27);
12462
13691
  }
12463
13692
  }
12464
13693
  if (awf && (event === EVENTS.ADD || event === EVENTS.CHANGE) && this._readyEmitted) {
@@ -12476,16 +13705,16 @@ var FSWatcher = class extends import_node_events.EventEmitter {
12476
13705
  this.emitWithAll(event, args);
12477
13706
  }
12478
13707
  };
12479
- this._awaitWriteFinish(path25, awf.stabilityThreshold, event, awfEmit);
13708
+ this._awaitWriteFinish(path27, awf.stabilityThreshold, event, awfEmit);
12480
13709
  return this;
12481
13710
  }
12482
13711
  if (event === EVENTS.CHANGE) {
12483
- const isThrottled = !this._throttle(EVENTS.CHANGE, path25, 50);
13712
+ const isThrottled = !this._throttle(EVENTS.CHANGE, path27, 50);
12484
13713
  if (isThrottled)
12485
13714
  return this;
12486
13715
  }
12487
13716
  if (opts.alwaysStat && stats === void 0 && (event === EVENTS.ADD || event === EVENTS.ADD_DIR || event === EVENTS.CHANGE)) {
12488
- const fullPath = opts.cwd ? sp2.join(opts.cwd, path25) : path25;
13717
+ const fullPath = opts.cwd ? sp2.join(opts.cwd, path27) : path27;
12489
13718
  let stats2;
12490
13719
  try {
12491
13720
  stats2 = await (0, import_promises3.stat)(fullPath);
@@ -12516,23 +13745,23 @@ var FSWatcher = class extends import_node_events.EventEmitter {
12516
13745
  * @param timeout duration of time to suppress duplicate actions
12517
13746
  * @returns tracking object or false if action should be suppressed
12518
13747
  */
12519
- _throttle(actionType, path25, timeout) {
13748
+ _throttle(actionType, path27, timeout) {
12520
13749
  if (!this._throttled.has(actionType)) {
12521
13750
  this._throttled.set(actionType, /* @__PURE__ */ new Map());
12522
13751
  }
12523
13752
  const action = this._throttled.get(actionType);
12524
13753
  if (!action)
12525
13754
  throw new Error("invalid throttle");
12526
- const actionPath = action.get(path25);
13755
+ const actionPath = action.get(path27);
12527
13756
  if (actionPath) {
12528
13757
  actionPath.count++;
12529
13758
  return false;
12530
13759
  }
12531
13760
  let timeoutObject;
12532
13761
  const clear = () => {
12533
- const item = action.get(path25);
13762
+ const item = action.get(path27);
12534
13763
  const count = item ? item.count : 0;
12535
- action.delete(path25);
13764
+ action.delete(path27);
12536
13765
  clearTimeout(timeoutObject);
12537
13766
  if (item)
12538
13767
  clearTimeout(item.timeoutObject);
@@ -12540,7 +13769,7 @@ var FSWatcher = class extends import_node_events.EventEmitter {
12540
13769
  };
12541
13770
  timeoutObject = setTimeout(clear, timeout);
12542
13771
  const thr = { timeoutObject, clear, count: 0 };
12543
- action.set(path25, thr);
13772
+ action.set(path27, thr);
12544
13773
  return thr;
12545
13774
  }
12546
13775
  _incrReadyCount() {
@@ -12554,44 +13783,44 @@ var FSWatcher = class extends import_node_events.EventEmitter {
12554
13783
  * @param event
12555
13784
  * @param awfEmit Callback to be called when ready for event to be emitted.
12556
13785
  */
12557
- _awaitWriteFinish(path25, threshold, event, awfEmit) {
13786
+ _awaitWriteFinish(path27, threshold, event, awfEmit) {
12558
13787
  const awf = this.options.awaitWriteFinish;
12559
13788
  if (typeof awf !== "object")
12560
13789
  return;
12561
13790
  const pollInterval = awf.pollInterval;
12562
13791
  let timeoutHandler;
12563
- let fullPath = path25;
12564
- if (this.options.cwd && !sp2.isAbsolute(path25)) {
12565
- fullPath = sp2.join(this.options.cwd, path25);
13792
+ let fullPath = path27;
13793
+ if (this.options.cwd && !sp2.isAbsolute(path27)) {
13794
+ fullPath = sp2.join(this.options.cwd, path27);
12566
13795
  }
12567
13796
  const now = /* @__PURE__ */ new Date();
12568
13797
  const writes = this._pendingWrites;
12569
13798
  function awaitWriteFinishFn(prevStat) {
12570
13799
  (0, import_node_fs2.stat)(fullPath, (err, curStat) => {
12571
- if (err || !writes.has(path25)) {
13800
+ if (err || !writes.has(path27)) {
12572
13801
  if (err && err.code !== "ENOENT")
12573
13802
  awfEmit(err);
12574
13803
  return;
12575
13804
  }
12576
13805
  const now2 = Number(/* @__PURE__ */ new Date());
12577
13806
  if (prevStat && curStat.size !== prevStat.size) {
12578
- writes.get(path25).lastChange = now2;
13807
+ writes.get(path27).lastChange = now2;
12579
13808
  }
12580
- const pw = writes.get(path25);
13809
+ const pw = writes.get(path27);
12581
13810
  const df = now2 - pw.lastChange;
12582
13811
  if (df >= threshold) {
12583
- writes.delete(path25);
13812
+ writes.delete(path27);
12584
13813
  awfEmit(void 0, curStat);
12585
13814
  } else {
12586
13815
  timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval, curStat);
12587
13816
  }
12588
13817
  });
12589
13818
  }
12590
- if (!writes.has(path25)) {
12591
- writes.set(path25, {
13819
+ if (!writes.has(path27)) {
13820
+ writes.set(path27, {
12592
13821
  lastChange: now,
12593
13822
  cancelWait: () => {
12594
- writes.delete(path25);
13823
+ writes.delete(path27);
12595
13824
  clearTimeout(timeoutHandler);
12596
13825
  return event;
12597
13826
  }
@@ -12602,8 +13831,8 @@ var FSWatcher = class extends import_node_events.EventEmitter {
12602
13831
  /**
12603
13832
  * Determines whether user has asked to ignore this path.
12604
13833
  */
12605
- _isIgnored(path25, stats) {
12606
- if (this.options.atomic && DOT_RE.test(path25))
13834
+ _isIgnored(path27, stats) {
13835
+ if (this.options.atomic && DOT_RE.test(path27))
12607
13836
  return true;
12608
13837
  if (!this._userIgnored) {
12609
13838
  const { cwd } = this.options;
@@ -12613,17 +13842,17 @@ var FSWatcher = class extends import_node_events.EventEmitter {
12613
13842
  const list = [...ignoredPaths.map(normalizeIgnored(cwd)), ...ignored];
12614
13843
  this._userIgnored = anymatch(list, void 0);
12615
13844
  }
12616
- return this._userIgnored(path25, stats);
13845
+ return this._userIgnored(path27, stats);
12617
13846
  }
12618
- _isntIgnored(path25, stat4) {
12619
- return !this._isIgnored(path25, stat4);
13847
+ _isntIgnored(path27, stat4) {
13848
+ return !this._isIgnored(path27, stat4);
12620
13849
  }
12621
13850
  /**
12622
13851
  * Provides a set of common helpers and properties relating to symlink handling.
12623
13852
  * @param path file or directory pattern being watched
12624
13853
  */
12625
- _getWatchHelpers(path25) {
12626
- return new WatchHelper(path25, this.options.followSymlinks, this);
13854
+ _getWatchHelpers(path27) {
13855
+ return new WatchHelper(path27, this.options.followSymlinks, this);
12627
13856
  }
12628
13857
  // Directory helpers
12629
13858
  // -----------------
@@ -12655,63 +13884,63 @@ var FSWatcher = class extends import_node_events.EventEmitter {
12655
13884
  * @param item base path of item/directory
12656
13885
  */
12657
13886
  _remove(directory, item, isDirectory) {
12658
- const path25 = sp2.join(directory, item);
12659
- const fullPath = sp2.resolve(path25);
12660
- isDirectory = isDirectory != null ? isDirectory : this._watched.has(path25) || this._watched.has(fullPath);
12661
- if (!this._throttle("remove", path25, 100))
13887
+ const path27 = sp2.join(directory, item);
13888
+ const fullPath = sp2.resolve(path27);
13889
+ isDirectory = isDirectory != null ? isDirectory : this._watched.has(path27) || this._watched.has(fullPath);
13890
+ if (!this._throttle("remove", path27, 100))
12662
13891
  return;
12663
13892
  if (!isDirectory && this._watched.size === 1) {
12664
13893
  this.add(directory, item, true);
12665
13894
  }
12666
- const wp = this._getWatchedDir(path25);
13895
+ const wp = this._getWatchedDir(path27);
12667
13896
  const nestedDirectoryChildren = wp.getChildren();
12668
- nestedDirectoryChildren.forEach((nested) => this._remove(path25, nested));
13897
+ nestedDirectoryChildren.forEach((nested) => this._remove(path27, nested));
12669
13898
  const parent = this._getWatchedDir(directory);
12670
13899
  const wasTracked = parent.has(item);
12671
13900
  parent.remove(item);
12672
13901
  if (this._symlinkPaths.has(fullPath)) {
12673
13902
  this._symlinkPaths.delete(fullPath);
12674
13903
  }
12675
- let relPath = path25;
13904
+ let relPath = path27;
12676
13905
  if (this.options.cwd)
12677
- relPath = sp2.relative(this.options.cwd, path25);
13906
+ relPath = sp2.relative(this.options.cwd, path27);
12678
13907
  if (this.options.awaitWriteFinish && this._pendingWrites.has(relPath)) {
12679
13908
  const event = this._pendingWrites.get(relPath).cancelWait();
12680
13909
  if (event === EVENTS.ADD)
12681
13910
  return;
12682
13911
  }
12683
- this._watched.delete(path25);
13912
+ this._watched.delete(path27);
12684
13913
  this._watched.delete(fullPath);
12685
13914
  const eventName = isDirectory ? EVENTS.UNLINK_DIR : EVENTS.UNLINK;
12686
- if (wasTracked && !this._isIgnored(path25))
12687
- this._emit(eventName, path25);
12688
- this._closePath(path25);
13915
+ if (wasTracked && !this._isIgnored(path27))
13916
+ this._emit(eventName, path27);
13917
+ this._closePath(path27);
12689
13918
  }
12690
13919
  /**
12691
13920
  * Closes all watchers for a path
12692
13921
  */
12693
- _closePath(path25) {
12694
- this._closeFile(path25);
12695
- const dir = sp2.dirname(path25);
12696
- this._getWatchedDir(dir).remove(sp2.basename(path25));
13922
+ _closePath(path27) {
13923
+ this._closeFile(path27);
13924
+ const dir = sp2.dirname(path27);
13925
+ this._getWatchedDir(dir).remove(sp2.basename(path27));
12697
13926
  }
12698
13927
  /**
12699
13928
  * Closes only file-specific watchers
12700
13929
  */
12701
- _closeFile(path25) {
12702
- const closers = this._closers.get(path25);
13930
+ _closeFile(path27) {
13931
+ const closers = this._closers.get(path27);
12703
13932
  if (!closers)
12704
13933
  return;
12705
13934
  closers.forEach((closer) => closer());
12706
- this._closers.delete(path25);
13935
+ this._closers.delete(path27);
12707
13936
  }
12708
- _addPathCloser(path25, closer) {
13937
+ _addPathCloser(path27, closer) {
12709
13938
  if (!closer)
12710
13939
  return;
12711
- let list = this._closers.get(path25);
13940
+ let list = this._closers.get(path27);
12712
13941
  if (!list) {
12713
13942
  list = [];
12714
- this._closers.set(path25, list);
13943
+ this._closers.set(path27, list);
12715
13944
  }
12716
13945
  list.push(closer);
12717
13946
  }
@@ -12741,7 +13970,7 @@ function watch(paths, options = {}) {
12741
13970
  var chokidar_default = { watch, FSWatcher };
12742
13971
 
12743
13972
  // src/watcher/file-watcher.ts
12744
- var path20 = __toESM(require("path"), 1);
13973
+ var path22 = __toESM(require("path"), 1);
12745
13974
  var FileWatcher = class {
12746
13975
  watcher = null;
12747
13976
  projectRoot;
@@ -12752,6 +13981,8 @@ var FileWatcher = class {
12752
13981
  debounceTimer = null;
12753
13982
  debounceMs = 1e3;
12754
13983
  onChanges = null;
13984
+ readyPromise = null;
13985
+ resolveReady = null;
12755
13986
  constructor(projectRoot, config, host = "opencode", options = {}) {
12756
13987
  this.projectRoot = projectRoot;
12757
13988
  this.config = config;
@@ -12767,15 +13998,15 @@ var FileWatcher = class {
12767
13998
  const watchTargets = this.configPath ? [this.projectRoot, this.configPath] : this.projectRoot;
12768
13999
  this.watcher = chokidar_default.watch(watchTargets, {
12769
14000
  ignored: (filePath) => {
12770
- const relativePath = path20.relative(this.projectRoot, filePath);
14001
+ const relativePath = path22.relative(this.projectRoot, filePath);
12771
14002
  if (!relativePath) return false;
12772
14003
  if (this.isProjectConfigPathOrAncestor(relativePath)) {
12773
14004
  return false;
12774
14005
  }
12775
- if (hasFilteredPathSegment(relativePath, path20.sep)) {
14006
+ if (hasFilteredPathSegment(relativePath, path22.sep)) {
12776
14007
  return true;
12777
14008
  }
12778
- if (isRestrictedDirectory(relativePath, path20.sep)) {
14009
+ if (isRestrictedDirectory(relativePath, path22.sep)) {
12779
14010
  return true;
12780
14011
  }
12781
14012
  if (ignoreFilter.ignores(relativePath)) {
@@ -12790,6 +14021,13 @@ var FileWatcher = class {
12790
14021
  pollInterval: 100
12791
14022
  }
12792
14023
  });
14024
+ this.readyPromise = new Promise((resolve15) => {
14025
+ this.resolveReady = resolve15;
14026
+ });
14027
+ this.watcher.once("ready", () => {
14028
+ this.resolveReady?.();
14029
+ this.resolveReady = null;
14030
+ });
12793
14031
  this.watcher.on("error", (error) => {
12794
14032
  const err = error instanceof Error ? error : null;
12795
14033
  if (err?.code === "EPERM" || err?.code === "EACCES") {
@@ -12821,24 +14059,24 @@ var FileWatcher = class {
12821
14059
  this.scheduleFlush();
12822
14060
  }
12823
14061
  isProjectConfigPath(filePath) {
12824
- const relativePath = path20.relative(this.projectRoot, filePath);
12825
- const normalizedRelativePath = path20.normalize(relativePath);
14062
+ const relativePath = path22.relative(this.projectRoot, filePath);
14063
+ const normalizedRelativePath = path22.normalize(relativePath);
12826
14064
  return this.getProjectConfigRelativePaths().some((configPath) => configPath === normalizedRelativePath);
12827
14065
  }
12828
14066
  isProjectConfigPathOrAncestor(relativePath) {
12829
- const normalizedRelativePath = path20.normalize(relativePath);
14067
+ const normalizedRelativePath = path22.normalize(relativePath);
12830
14068
  return this.getProjectConfigRelativePaths().some(
12831
- (configPath) => configPath === normalizedRelativePath || configPath.startsWith(`${normalizedRelativePath}${path20.sep}`)
14069
+ (configPath) => configPath === normalizedRelativePath || configPath.startsWith(`${normalizedRelativePath}${path22.sep}`)
12832
14070
  );
12833
14071
  }
12834
14072
  getProjectConfigRelativePaths() {
12835
14073
  if (this.configPath) {
12836
- return [path20.normalize(path20.relative(this.projectRoot, this.configPath))];
14074
+ return [path22.normalize(path22.relative(this.projectRoot, this.configPath))];
12837
14075
  }
12838
14076
  return [
12839
14077
  resolveProjectConfigPath(this.projectRoot, this.host),
12840
14078
  resolveWritableProjectConfigPath(this.projectRoot, this.host)
12841
- ].map((configPath) => path20.normalize(path20.relative(this.projectRoot, configPath)));
14079
+ ].map((configPath) => path22.normalize(path22.relative(this.projectRoot, configPath)));
12842
14080
  }
12843
14081
  scheduleFlush() {
12844
14082
  if (this.debounceTimer) {
@@ -12853,7 +14091,7 @@ var FileWatcher = class {
12853
14091
  return;
12854
14092
  }
12855
14093
  const changes = Array.from(this.pendingChanges.entries()).map(
12856
- ([path25, type]) => ({ path: path25, type })
14094
+ ([path27, type]) => ({ path: path27, type })
12857
14095
  );
12858
14096
  this.pendingChanges.clear();
12859
14097
  try {
@@ -12862,25 +14100,32 @@ var FileWatcher = class {
12862
14100
  console.error("Error handling file changes:", error);
12863
14101
  }
12864
14102
  }
12865
- stop() {
14103
+ async stop() {
12866
14104
  if (this.debounceTimer) {
12867
14105
  clearTimeout(this.debounceTimer);
12868
14106
  this.debounceTimer = null;
12869
14107
  }
12870
14108
  if (this.watcher) {
12871
- this.watcher.close();
14109
+ const watcher = this.watcher;
12872
14110
  this.watcher = null;
14111
+ await watcher.close();
12873
14112
  }
14113
+ this.resolveReady?.();
14114
+ this.resolveReady = null;
14115
+ this.readyPromise = null;
12874
14116
  this.pendingChanges.clear();
12875
14117
  this.onChanges = null;
12876
14118
  }
12877
14119
  isRunning() {
12878
14120
  return this.watcher !== null;
12879
14121
  }
14122
+ async waitUntilReady() {
14123
+ await (this.readyPromise ?? Promise.resolve());
14124
+ }
12880
14125
  };
12881
14126
 
12882
14127
  // src/watcher/git-head-watcher.ts
12883
- var path21 = __toESM(require("path"), 1);
14128
+ var path23 = __toESM(require("path"), 1);
12884
14129
  var GitHeadWatcher = class {
12885
14130
  watcher = null;
12886
14131
  projectRoot;
@@ -12902,7 +14147,7 @@ var GitHeadWatcher = class {
12902
14147
  this.onBranchChange = handler;
12903
14148
  this.currentBranch = getCurrentBranch(this.projectRoot);
12904
14149
  const headPath = getHeadPath(this.projectRoot);
12905
- const refsPath = path21.join(this.projectRoot, ".git", "refs", "heads");
14150
+ const refsPath = path23.join(this.projectRoot, ".git", "refs", "heads");
12906
14151
  this.watcher = chokidar_default.watch([headPath, refsPath], {
12907
14152
  persistent: true,
12908
14153
  ignoreInitial: true,
@@ -12939,14 +14184,15 @@ var GitHeadWatcher = class {
12939
14184
  getCurrentBranch() {
12940
14185
  return this.currentBranch;
12941
14186
  }
12942
- stop() {
14187
+ async stop() {
12943
14188
  if (this.debounceTimer) {
12944
14189
  clearTimeout(this.debounceTimer);
12945
14190
  this.debounceTimer = null;
12946
14191
  }
12947
14192
  if (this.watcher) {
12948
- this.watcher.close();
14193
+ const watcher = this.watcher;
12949
14194
  this.watcher = null;
14195
+ await watcher.close();
12950
14196
  }
12951
14197
  this.onBranchChange = null;
12952
14198
  }
@@ -12964,6 +14210,8 @@ var BackgroundReindexer = class {
12964
14210
  running = false;
12965
14211
  pending = false;
12966
14212
  stopped = false;
14213
+ retryTimer = null;
14214
+ retryDelayMs = 50;
12967
14215
  request() {
12968
14216
  if (this.stopped) {
12969
14217
  return;
@@ -12974,9 +14222,13 @@ var BackgroundReindexer = class {
12974
14222
  stop() {
12975
14223
  this.stopped = true;
12976
14224
  this.pending = false;
14225
+ if (this.retryTimer) {
14226
+ clearTimeout(this.retryTimer);
14227
+ this.retryTimer = null;
14228
+ }
12977
14229
  }
12978
14230
  drain() {
12979
- if (this.stopped || this.running || !this.pending) {
14231
+ if (this.stopped || this.running || this.retryTimer || !this.pending) {
12980
14232
  return;
12981
14233
  }
12982
14234
  this.pending = false;
@@ -12984,13 +14236,29 @@ var BackgroundReindexer = class {
12984
14236
  void this.run();
12985
14237
  }
12986
14238
  async run() {
14239
+ let retryAfterContention = false;
12987
14240
  try {
12988
14241
  await this.runIndex();
14242
+ this.retryDelayMs = 50;
12989
14243
  } catch (error) {
12990
- console.error("[codebase-index] Background reindex failed:", error);
14244
+ if (isTransientIndexLockContention(error)) {
14245
+ this.pending = true;
14246
+ retryAfterContention = true;
14247
+ } else {
14248
+ console.error("[codebase-index] Background reindex failed:", error);
14249
+ }
12991
14250
  } finally {
12992
14251
  this.running = false;
12993
- this.drain();
14252
+ if (retryAfterContention && !this.stopped) {
14253
+ const delay = this.retryDelayMs;
14254
+ this.retryDelayMs = Math.min(this.retryDelayMs * 2, 500);
14255
+ this.retryTimer = setTimeout(() => {
14256
+ this.retryTimer = null;
14257
+ this.drain();
14258
+ }, delay);
14259
+ } else {
14260
+ this.drain();
14261
+ }
12994
14262
  }
12995
14263
  }
12996
14264
  };
@@ -13024,10 +14292,12 @@ function createWatcherWithIndexer(getIndexer, projectRoot, config, host = "openc
13024
14292
  return {
13025
14293
  fileWatcher,
13026
14294
  gitWatcher,
13027
- stop() {
14295
+ whenReady() {
14296
+ return fileWatcher.waitUntilReady();
14297
+ },
14298
+ async stop() {
13028
14299
  backgroundReindexer.stop();
13029
- fileWatcher.stop();
13030
- gitWatcher?.stop();
14300
+ await Promise.all([fileWatcher.stop(), gitWatcher?.stop()]);
13031
14301
  }
13032
14302
  };
13033
14303
  }
@@ -13045,8 +14315,8 @@ function getConfigPaths(projectRoot, host, options) {
13045
14315
  }
13046
14316
 
13047
14317
  // src/tools/visualize/activity.ts
13048
- var import_child_process3 = require("child_process");
13049
- var path22 = __toESM(require("path"), 1);
14318
+ var import_child_process4 = require("child_process");
14319
+ var path24 = __toESM(require("path"), 1);
13050
14320
  function attachRecentActivity(data, projectRoot) {
13051
14321
  const activity = readGitActivity(projectRoot);
13052
14322
  const changes = activity.size > 0 ? buildGitChanges(data, activity, projectRoot) : buildGraphChanges(data);
@@ -13057,7 +14327,7 @@ function attachRecentActivity(data, projectRoot) {
13057
14327
  }
13058
14328
  function readGitActivity(projectRoot) {
13059
14329
  try {
13060
- const output = (0, import_child_process3.execFileSync)(
14330
+ const output = (0, import_child_process4.execFileSync)(
13061
14331
  "git",
13062
14332
  ["-C", projectRoot, "log", "--since=90.days", "--numstat", "--date=short", "--pretty=format:__COMMIT__%x09%h%x09%ad%x09%s"],
13063
14333
  { encoding: "utf8", maxBuffer: 8 * 1024 * 1024, stdio: ["ignore", "pipe", "ignore"] }
@@ -13208,7 +14478,7 @@ function normalizePath3(filePath) {
13208
14478
  return filePath.replace(/\\/g, "/");
13209
14479
  }
13210
14480
  function toGitRelativePath(projectRoot, filePath) {
13211
- const relativePath = path22.isAbsolute(filePath) ? path22.relative(projectRoot, filePath) : filePath;
14481
+ const relativePath = path24.isAbsolute(filePath) ? path24.relative(projectRoot, filePath) : filePath;
13212
14482
  return normalizePath3(relativePath);
13213
14483
  }
13214
14484
 
@@ -13466,7 +14736,7 @@ render();
13466
14736
  }
13467
14737
 
13468
14738
  // src/tools/visualize/transform.ts
13469
- var path23 = __toESM(require("path"), 1);
14739
+ var path25 = __toESM(require("path"), 1);
13470
14740
 
13471
14741
  // src/tools/visualize/modules.ts
13472
14742
  var MAX_MODULES = 18;
@@ -13599,8 +14869,8 @@ function compactModules(prefixToNodes) {
13599
14869
  function deriveModules(nodes) {
13600
14870
  const initial = /* @__PURE__ */ new Map();
13601
14871
  for (const node of nodes) {
13602
- const relative10 = stripToProjectRelative(node.filePath);
13603
- const prefix = modulePrefixFromRelativePath(relative10);
14872
+ const relative11 = stripToProjectRelative(node.filePath);
14873
+ const prefix = modulePrefixFromRelativePath(relative11);
13604
14874
  if (!initial.has(prefix)) initial.set(prefix, []);
13605
14875
  initial.get(prefix)?.push(node);
13606
14876
  }
@@ -13726,7 +14996,7 @@ function transformForVisualization(symbols, edges, options = {}) {
13726
14996
  filePath: s.filePath,
13727
14997
  kind: s.kind,
13728
14998
  line: s.startLine,
13729
- directory: path23.dirname(s.filePath),
14999
+ directory: path25.dirname(s.filePath),
13730
15000
  moduleId: "",
13731
15001
  moduleLabel: ""
13732
15002
  }));
@@ -13755,9 +15025,9 @@ function parseArgs(argv) {
13755
15025
  let host = "opencode";
13756
15026
  for (let i = 2; i < argv.length; i++) {
13757
15027
  if (argv[i] === "--project" && argv[i + 1]) {
13758
- project = path24.resolve(argv[++i]);
15028
+ project = path26.resolve(argv[++i]);
13759
15029
  } else if (argv[i] === "--config" && argv[i + 1]) {
13760
- config = path24.resolve(argv[++i]);
15030
+ config = path26.resolve(argv[++i]);
13761
15031
  } else if (argv[i] === "--host" && argv[i + 1]) {
13762
15032
  host = parseHostMode(argv[++i]);
13763
15033
  } else if (argv[i] === "--host") {
@@ -13770,7 +15040,7 @@ function loadCliRawConfig(args) {
13770
15040
  return args.config ? loadConfigFile(args.config) : loadMergedConfig(args.project, args.host);
13771
15041
  }
13772
15042
  function isCliEntrypoint(moduleUrl, argvPath) {
13773
- return argvPath !== void 0 && (0, import_fs15.realpathSync)((0, import_url2.fileURLToPath)(moduleUrl)) === (0, import_fs15.realpathSync)(argvPath);
15043
+ return argvPath !== void 0 && (0, import_fs16.realpathSync)((0, import_url2.fileURLToPath)(moduleUrl)) === (0, import_fs16.realpathSync)(argvPath);
13774
15044
  }
13775
15045
  function parseVisualizeArgs(argv, cwd) {
13776
15046
  let project = cwd;
@@ -13780,7 +15050,7 @@ function parseVisualizeArgs(argv, cwd) {
13780
15050
  for (let i = 0; i < argv.length; i++) {
13781
15051
  const arg = argv[i];
13782
15052
  if (arg === "--project" && argv[i + 1]) {
13783
- project = path24.resolve(argv[++i]);
15053
+ project = path26.resolve(argv[++i]);
13784
15054
  } else if (arg === "--max" && argv[i + 1]) {
13785
15055
  maxNodes = Number(argv[++i]);
13786
15056
  } else if (arg.startsWith("--max=") || arg.startsWith("max=")) {
@@ -13817,8 +15087,8 @@ async function handleVisualizeCommand(argv, cwd) {
13817
15087
  console.error("No connected symbols found. Retry with: npm run visualize -- orphans");
13818
15088
  return 1;
13819
15089
  }
13820
- const outputPath = path24.join(os5.tmpdir(), `call-graph-${Date.now()}.html`);
13821
- (0, import_fs15.writeFileSync)(outputPath, generateVisualizationHtml(vizData), "utf-8");
15090
+ const outputPath = path26.join(os6.tmpdir(), `call-graph-${Date.now()}.html`);
15091
+ (0, import_fs16.writeFileSync)(outputPath, generateVisualizationHtml(vizData), "utf-8");
13822
15092
  console.log(`Temporal call graph visualization generated: ${outputPath}`);
13823
15093
  console.log(`Nodes: ${vizData.nodes.length} | Edges: ${vizData.edges.length}`);
13824
15094
  console.log(`Recent change lenses: ${vizData.changes?.length ?? 0}`);
@@ -13847,7 +15117,7 @@ async function main() {
13847
15117
  const transport = new import_stdio.StdioServerTransport();
13848
15118
  await server.connect(transport);
13849
15119
  let watcher = null;
13850
- const isHomeDir = path24.resolve(args.project) === path24.resolve(os5.homedir());
15120
+ const isHomeDir = path26.resolve(args.project) === path26.resolve(os6.homedir());
13851
15121
  const isValidProject = !isHomeDir && (!config.indexing.requireProjectMarker || hasProjectMarker(args.project));
13852
15122
  if (config.indexing.autoIndex && isValidProject) {
13853
15123
  const indexer = getIndexerForProject(args.project, args.host);
@@ -13862,14 +15132,25 @@ async function main() {
13862
15132
  args.config ? { configPath: args.config } : {}
13863
15133
  );
13864
15134
  }
13865
- const shutdown = () => {
13866
- watcher?.stop();
13867
- server.close().catch(() => {
13868
- });
13869
- process.exit(0);
15135
+ let shuttingDown = false;
15136
+ const shutdown = async () => {
15137
+ if (shuttingDown) return;
15138
+ shuttingDown = true;
15139
+ try {
15140
+ await watcher?.stop();
15141
+ await server.close();
15142
+ process.exit(0);
15143
+ } catch (error) {
15144
+ console.error("Failed to stop MCP server cleanly:", error);
15145
+ process.exit(1);
15146
+ }
13870
15147
  };
13871
- process.on("SIGINT", shutdown);
13872
- process.on("SIGTERM", shutdown);
15148
+ process.on("SIGINT", () => {
15149
+ void shutdown();
15150
+ });
15151
+ process.on("SIGTERM", () => {
15152
+ void shutdown();
15153
+ });
13873
15154
  }
13874
15155
  function handleMainError(error) {
13875
15156
  if (error instanceof Error && error.message.startsWith("Invalid host mode")) {