opencode-codebase-index 0.14.0 → 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.
@@ -495,7 +495,7 @@ var require_ignore = __commonJS({
495
495
  // path matching.
496
496
  // - check `string` either `MODE_IGNORE` or `MODE_CHECK_IGNORE`
497
497
  // @returns {TestResult} true if a file is ignored
498
- test(path16, checkUnignored, mode) {
498
+ test(path17, checkUnignored, mode) {
499
499
  let ignored = false;
500
500
  let unignored = false;
501
501
  let matchedRule;
@@ -504,7 +504,7 @@ var require_ignore = __commonJS({
504
504
  if (unignored === negative && ignored !== unignored || negative && !ignored && !unignored && !checkUnignored) {
505
505
  return;
506
506
  }
507
- const matched = rule[mode].test(path16);
507
+ const matched = rule[mode].test(path17);
508
508
  if (!matched) {
509
509
  return;
510
510
  }
@@ -525,17 +525,17 @@ var require_ignore = __commonJS({
525
525
  var throwError = (message, Ctor) => {
526
526
  throw new Ctor(message);
527
527
  };
528
- var checkPath = (path16, originalPath, doThrow) => {
529
- if (!isString(path16)) {
528
+ var checkPath = (path17, originalPath, doThrow) => {
529
+ if (!isString(path17)) {
530
530
  return doThrow(
531
531
  `path must be a string, but got \`${originalPath}\``,
532
532
  TypeError
533
533
  );
534
534
  }
535
- if (!path16) {
535
+ if (!path17) {
536
536
  return doThrow(`path must not be empty`, TypeError);
537
537
  }
538
- if (checkPath.isNotRelative(path16)) {
538
+ if (checkPath.isNotRelative(path17)) {
539
539
  const r = "`path.relative()`d";
540
540
  return doThrow(
541
541
  `path should be a ${r} string, but got "${originalPath}"`,
@@ -544,7 +544,7 @@ var require_ignore = __commonJS({
544
544
  }
545
545
  return true;
546
546
  };
547
- var isNotRelative = (path16) => REGEX_TEST_INVALID_PATH.test(path16);
547
+ var isNotRelative = (path17) => REGEX_TEST_INVALID_PATH.test(path17);
548
548
  checkPath.isNotRelative = isNotRelative;
549
549
  checkPath.convert = (p) => p;
550
550
  var Ignore2 = class {
@@ -574,19 +574,19 @@ var require_ignore = __commonJS({
574
574
  }
575
575
  // @returns {TestResult}
576
576
  _test(originalPath, cache, checkUnignored, slices) {
577
- const path16 = originalPath && checkPath.convert(originalPath);
577
+ const path17 = originalPath && checkPath.convert(originalPath);
578
578
  checkPath(
579
- path16,
579
+ path17,
580
580
  originalPath,
581
581
  this._strictPathCheck ? throwError : RETURN_FALSE
582
582
  );
583
- return this._t(path16, cache, checkUnignored, slices);
583
+ return this._t(path17, cache, checkUnignored, slices);
584
584
  }
585
- checkIgnore(path16) {
586
- if (!REGEX_TEST_TRAILING_SLASH.test(path16)) {
587
- return this.test(path16);
585
+ checkIgnore(path17) {
586
+ if (!REGEX_TEST_TRAILING_SLASH.test(path17)) {
587
+ return this.test(path17);
588
588
  }
589
- const slices = path16.split(SLASH).filter(Boolean);
589
+ const slices = path17.split(SLASH).filter(Boolean);
590
590
  slices.pop();
591
591
  if (slices.length) {
592
592
  const parent = this._t(
@@ -599,18 +599,18 @@ var require_ignore = __commonJS({
599
599
  return parent;
600
600
  }
601
601
  }
602
- return this._rules.test(path16, false, MODE_CHECK_IGNORE);
602
+ return this._rules.test(path17, false, MODE_CHECK_IGNORE);
603
603
  }
604
- _t(path16, cache, checkUnignored, slices) {
605
- if (path16 in cache) {
606
- return cache[path16];
604
+ _t(path17, cache, checkUnignored, slices) {
605
+ if (path17 in cache) {
606
+ return cache[path17];
607
607
  }
608
608
  if (!slices) {
609
- slices = path16.split(SLASH).filter(Boolean);
609
+ slices = path17.split(SLASH).filter(Boolean);
610
610
  }
611
611
  slices.pop();
612
612
  if (!slices.length) {
613
- return cache[path16] = this._rules.test(path16, checkUnignored, MODE_IGNORE);
613
+ return cache[path17] = this._rules.test(path17, checkUnignored, MODE_IGNORE);
614
614
  }
615
615
  const parent = this._t(
616
616
  slices.join(SLASH) + SLASH,
@@ -618,29 +618,29 @@ var require_ignore = __commonJS({
618
618
  checkUnignored,
619
619
  slices
620
620
  );
621
- return cache[path16] = parent.ignored ? parent : this._rules.test(path16, checkUnignored, MODE_IGNORE);
621
+ return cache[path17] = parent.ignored ? parent : this._rules.test(path17, checkUnignored, MODE_IGNORE);
622
622
  }
623
- ignores(path16) {
624
- return this._test(path16, this._ignoreCache, false).ignored;
623
+ ignores(path17) {
624
+ return this._test(path17, this._ignoreCache, false).ignored;
625
625
  }
626
626
  createFilter() {
627
- return (path16) => !this.ignores(path16);
627
+ return (path17) => !this.ignores(path17);
628
628
  }
629
629
  filter(paths) {
630
630
  return makeArray(paths).filter(this.createFilter());
631
631
  }
632
632
  // @returns {TestResult}
633
- test(path16) {
634
- return this._test(path16, this._testCache, true);
633
+ test(path17) {
634
+ return this._test(path17, this._testCache, true);
635
635
  }
636
636
  };
637
637
  var factory = (options) => new Ignore2(options);
638
- var isPathValid = (path16) => checkPath(path16 && checkPath.convert(path16), path16, RETURN_FALSE);
638
+ var isPathValid = (path17) => checkPath(path17 && checkPath.convert(path17), path17, RETURN_FALSE);
639
639
  var setupWindows = () => {
640
640
  const makePosix = (str) => /^\\\\\?\\/.test(str) || /["<>|\u0000-\u001F]+/u.test(str) ? str : str.replace(/\\/g, "/");
641
641
  checkPath.convert = makePosix;
642
642
  const REGEX_TEST_WINDOWS_PATH_ABSOLUTE = /^[a-z]:\//i;
643
- checkPath.isNotRelative = (path16) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path16) || isNotRelative(path16);
643
+ checkPath.isNotRelative = (path17) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path17) || isNotRelative(path17);
644
644
  };
645
645
  if (
646
646
  // Detect `process` so that it can run in browsers.
@@ -665,10 +665,10 @@ var import_typebox2 = require("typebox");
665
665
 
666
666
  // src/config/constants.ts
667
667
  var DEFAULT_INCLUDE = [
668
- "**/*.{ts,tsx,js,jsx,mjs,cjs}",
668
+ "**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}",
669
669
  "**/*.{py,pyi}",
670
- "**/*.{go,rs,java,kt,scala}",
671
- "**/*.{c,cpp,cc,h,hpp}",
670
+ "**/*.{go,rs,java,cs,kt,scala}",
671
+ "**/*.{c,cpp,cc,cxx,h,hpp,hxx}",
672
672
  "**/*.{rb,php,inc,swift}",
673
673
  "**/*.{cls,trigger}",
674
674
  "**/*.{vue,svelte,astro}",
@@ -1337,8 +1337,8 @@ function formatPrImpact(result) {
1337
1337
  }
1338
1338
 
1339
1339
  // src/tools/operations.ts
1340
- var import_fs9 = require("fs");
1341
- var path15 = __toESM(require("path"), 1);
1340
+ var import_fs10 = require("fs");
1341
+ var path16 = __toESM(require("path"), 1);
1342
1342
 
1343
1343
  // src/config/merger.ts
1344
1344
  var import_fs5 = require("fs");
@@ -1535,6 +1535,9 @@ function getHostProjectIndexRelativePath(host) {
1535
1535
  function hasHostProjectConfig(projectRoot3, host) {
1536
1536
  return (0, import_fs4.existsSync)(path4.join(projectRoot3, getProjectConfigRelativePath(host)));
1537
1537
  }
1538
+ function hasHostGlobalConfig(host) {
1539
+ return (0, import_fs4.existsSync)(getGlobalConfigPath(host));
1540
+ }
1538
1541
  function getGlobalIndexPath(host = "opencode") {
1539
1542
  switch (host) {
1540
1543
  case "opencode":
@@ -1574,6 +1577,9 @@ function resolveGlobalIndexPath(host = "opencode") {
1574
1577
  return hostIndexPath;
1575
1578
  }
1576
1579
  if (host !== "opencode") {
1580
+ if (hasHostGlobalConfig(host)) {
1581
+ return hostIndexPath;
1582
+ }
1577
1583
  const legacyIndexPath = getGlobalIndexPath("opencode");
1578
1584
  if ((0, import_fs4.existsSync)(legacyIndexPath)) {
1579
1585
  return legacyIndexPath;
@@ -1839,8 +1845,8 @@ function loadMergedConfig(projectRoot3, host = "opencode") {
1839
1845
  }
1840
1846
 
1841
1847
  // src/indexer/index.ts
1842
- var import_fs7 = require("fs");
1843
- var path12 = __toESM(require("path"), 1);
1848
+ var import_fs8 = require("fs");
1849
+ var path13 = __toESM(require("path"), 1);
1844
1850
  var import_perf_hooks = require("perf_hooks");
1845
1851
  var import_child_process3 = require("child_process");
1846
1852
  var import_util3 = require("util");
@@ -2947,17 +2953,17 @@ function validateExternalUrl(urlString) {
2947
2953
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
2948
2954
  return { valid: false, reason: `Blocked protocol: ${parsed.protocol}` };
2949
2955
  }
2950
- const hostname = parsed.hostname.toLowerCase();
2951
- if (BLOCKED_HOSTNAMES.has(hostname)) {
2952
- return { valid: false, reason: `Blocked: cloud metadata service (${hostname})` };
2956
+ const hostname2 = parsed.hostname.toLowerCase();
2957
+ if (BLOCKED_HOSTNAMES.has(hostname2)) {
2958
+ return { valid: false, reason: `Blocked: cloud metadata service (${hostname2})` };
2953
2959
  }
2954
2960
  for (const pattern of BLOCKED_METADATA_IPS) {
2955
- if (pattern.test(hostname)) {
2956
- return { valid: false, reason: `Blocked: cloud metadata IP (${hostname})` };
2961
+ if (pattern.test(hostname2)) {
2962
+ return { valid: false, reason: `Blocked: cloud metadata IP (${hostname2})` };
2957
2963
  }
2958
2964
  }
2959
- if (/^169\.254\./.test(hostname)) {
2960
- return { valid: false, reason: `Blocked: link-local address (${hostname})` };
2965
+ if (/^169\.254\./.test(hostname2)) {
2966
+ return { valid: false, reason: `Blocked: link-local address (${hostname2})` };
2961
2967
  }
2962
2968
  return { valid: true };
2963
2969
  }
@@ -3995,6 +4001,12 @@ function createMockNativeBinding() {
3995
4001
  constructor() {
3996
4002
  throw error;
3997
4003
  }
4004
+ static openReadOnly() {
4005
+ throw error;
4006
+ }
4007
+ static createEmptyReadOnly() {
4008
+ throw error;
4009
+ }
3998
4010
  close() {
3999
4011
  throw error;
4000
4012
  }
@@ -4098,6 +4110,12 @@ var VectorStore = class {
4098
4110
  load() {
4099
4111
  this.inner.load();
4100
4112
  }
4113
+ loadStrict() {
4114
+ this.inner.loadStrict();
4115
+ }
4116
+ hasFingerprint() {
4117
+ return this.inner.hasFingerprint();
4118
+ }
4101
4119
  count() {
4102
4120
  return this.inner.count();
4103
4121
  }
@@ -4380,12 +4398,24 @@ var InvertedIndex = class {
4380
4398
  return this.inner.documentCount();
4381
4399
  }
4382
4400
  };
4383
- var Database = class {
4401
+ var Database = class _Database {
4384
4402
  inner;
4385
4403
  closed = false;
4386
4404
  constructor(dbPath) {
4387
4405
  this.inner = new native.Database(dbPath);
4388
4406
  }
4407
+ static fromNative(inner) {
4408
+ const database = Object.create(_Database.prototype);
4409
+ database.inner = inner;
4410
+ database.closed = false;
4411
+ return database;
4412
+ }
4413
+ static openReadOnly(dbPath) {
4414
+ return _Database.fromNative(native.Database.openReadOnly(dbPath));
4415
+ }
4416
+ static createEmptyReadOnly() {
4417
+ return _Database.fromNative(native.Database.createEmptyReadOnly());
4418
+ }
4389
4419
  throwIfClosed() {
4390
4420
  if (this.closed) {
4391
4421
  throw new Error("Database is closed");
@@ -4814,6 +4844,418 @@ async function getChunkGitBlame(projectRoot3, filePath, startLine, endLine) {
4814
4844
  }
4815
4845
  }
4816
4846
 
4847
+ // src/indexer/index-lock.ts
4848
+ var import_crypto = require("crypto");
4849
+ var import_fs7 = require("fs");
4850
+ var os4 = __toESM(require("os"), 1);
4851
+ var path12 = __toESM(require("path"), 1);
4852
+ var OWNER_FILE_NAME = "owner.json";
4853
+ var RECLAIM_DIRECTORY_NAME = "reclaiming";
4854
+ var RECOVERY_MARKER_PREFIX = "indexing.lock.recovery.";
4855
+ 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;
4856
+ var VALID_OPERATIONS = /* @__PURE__ */ new Set([
4857
+ "initialize",
4858
+ "index",
4859
+ "force-index",
4860
+ "clear",
4861
+ "health-check",
4862
+ "retry-failed-batches",
4863
+ "recovery"
4864
+ ]);
4865
+ var temporaryCounter = 0;
4866
+ function getErrorCode(error) {
4867
+ return typeof error === "object" && error !== null && "code" in error ? String(error.code) : void 0;
4868
+ }
4869
+ function retryTransientFilesystemOperation(operation) {
4870
+ let lastError;
4871
+ for (let attempt = 0; attempt < 3; attempt += 1) {
4872
+ try {
4873
+ operation();
4874
+ return;
4875
+ } catch (error) {
4876
+ lastError = error;
4877
+ const code = getErrorCode(error);
4878
+ if (code !== "EBUSY" && code !== "EPERM") throw error;
4879
+ if (attempt < 2) {
4880
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, (attempt + 1) * 10);
4881
+ }
4882
+ }
4883
+ }
4884
+ throw lastError;
4885
+ }
4886
+ function parseOwner(value) {
4887
+ if (typeof value !== "object" || value === null) return null;
4888
+ const candidate = value;
4889
+ if (!Number.isInteger(candidate.pid) || (candidate.pid ?? 0) <= 0) return null;
4890
+ if (typeof candidate.hostname !== "string" || candidate.hostname.length === 0) return null;
4891
+ if (typeof candidate.startedAt !== "string" || Number.isNaN(Date.parse(candidate.startedAt))) return null;
4892
+ if (typeof candidate.operation !== "string" || !VALID_OPERATIONS.has(candidate.operation)) return null;
4893
+ if (typeof candidate.token !== "string" || !UUID_PATTERN.test(candidate.token)) return null;
4894
+ return candidate;
4895
+ }
4896
+ function parseReclaimOwner(value) {
4897
+ if (typeof value !== "object" || value === null) return null;
4898
+ const candidate = value;
4899
+ if (!Number.isInteger(candidate.pid) || (candidate.pid ?? 0) <= 0) return null;
4900
+ if (typeof candidate.hostname !== "string" || candidate.hostname.length === 0) return null;
4901
+ if (typeof candidate.startedAt !== "string" || Number.isNaN(Date.parse(candidate.startedAt))) return null;
4902
+ if (typeof candidate.token !== "string" || !UUID_PATTERN.test(candidate.token)) return null;
4903
+ if (typeof candidate.expectedOwnerToken !== "string" || !UUID_PATTERN.test(candidate.expectedOwnerToken)) return null;
4904
+ return candidate;
4905
+ }
4906
+ function readJsonDirectory(directoryPath, parser) {
4907
+ try {
4908
+ return parser(JSON.parse((0, import_fs7.readFileSync)(path12.join(directoryPath, OWNER_FILE_NAME), "utf-8")));
4909
+ } catch {
4910
+ return null;
4911
+ }
4912
+ }
4913
+ function readDirectoryOwner(lockPath) {
4914
+ return readJsonDirectory(lockPath, parseOwner);
4915
+ }
4916
+ function readReclaimOwner(markerPath) {
4917
+ return readJsonDirectory(markerPath, parseReclaimOwner);
4918
+ }
4919
+ function readRecoveryOwner(markerPath) {
4920
+ return readDirectoryOwner(markerPath);
4921
+ }
4922
+ function readLegacyOwner(lockPath) {
4923
+ try {
4924
+ const parsed = JSON.parse((0, import_fs7.readFileSync)(lockPath, "utf-8"));
4925
+ if (!Number.isInteger(parsed.pid) || Number(parsed.pid) <= 0) return null;
4926
+ if (typeof parsed.startedAt !== "string" || Number.isNaN(Date.parse(parsed.startedAt))) return null;
4927
+ return {
4928
+ pid: Number(parsed.pid),
4929
+ hostname: typeof parsed.hostname === "string" ? parsed.hostname : os4.hostname(),
4930
+ startedAt: parsed.startedAt,
4931
+ operation: typeof parsed.operation === "string" && VALID_OPERATIONS.has(parsed.operation) ? parsed.operation : "index",
4932
+ token: typeof parsed.token === "string" ? parsed.token : "legacy-v0.14.0"
4933
+ };
4934
+ } catch {
4935
+ return null;
4936
+ }
4937
+ }
4938
+ function getOwnerLiveness(owner) {
4939
+ if (owner.hostname !== os4.hostname()) return "unknown";
4940
+ try {
4941
+ process.kill(owner.pid, 0);
4942
+ return "alive";
4943
+ } catch (error) {
4944
+ const code = getErrorCode(error);
4945
+ if (code === "ESRCH") return "dead";
4946
+ if (code === "EPERM") return "alive";
4947
+ return "unknown";
4948
+ }
4949
+ }
4950
+ function sameOwner(left, right) {
4951
+ return left.pid === right.pid && left.hostname === right.hostname && left.token === right.token;
4952
+ }
4953
+ function sameReclaimOwner(left, right) {
4954
+ return left.pid === right.pid && left.hostname === right.hostname && left.token === right.token && left.expectedOwnerToken === right.expectedOwnerToken;
4955
+ }
4956
+ function publishJsonDirectory(finalPath, value) {
4957
+ const candidatePath = `${finalPath}.candidate.${process.pid}.${(0, import_crypto.randomUUID)()}`;
4958
+ (0, import_fs7.mkdirSync)(candidatePath, { mode: 448 });
4959
+ try {
4960
+ (0, import_fs7.writeFileSync)(path12.join(candidatePath, OWNER_FILE_NAME), JSON.stringify(value), {
4961
+ encoding: "utf-8",
4962
+ flag: "wx",
4963
+ mode: 384
4964
+ });
4965
+ if ((0, import_fs7.existsSync)(finalPath)) return false;
4966
+ try {
4967
+ (0, import_fs7.renameSync)(candidatePath, finalPath);
4968
+ return true;
4969
+ } catch (error) {
4970
+ if ((0, import_fs7.existsSync)(finalPath)) return false;
4971
+ throw error;
4972
+ }
4973
+ } finally {
4974
+ if ((0, import_fs7.existsSync)(candidatePath)) (0, import_fs7.rmSync)(candidatePath, { recursive: true, force: true });
4975
+ }
4976
+ }
4977
+ function createOwner(operation) {
4978
+ return {
4979
+ pid: process.pid,
4980
+ hostname: os4.hostname(),
4981
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
4982
+ operation,
4983
+ token: (0, import_crypto.randomUUID)()
4984
+ };
4985
+ }
4986
+ function recoveryMarkerPath(indexPath, owner) {
4987
+ return path12.join(indexPath, `${RECOVERY_MARKER_PREFIX}${owner.token}`);
4988
+ }
4989
+ function publishRecoveryMarker(indexPath, owner) {
4990
+ const markerPath = recoveryMarkerPath(indexPath, owner);
4991
+ if (!publishJsonDirectory(markerPath, owner)) {
4992
+ const markerOwner = readRecoveryOwner(markerPath);
4993
+ if (!markerOwner || !sameOwner(markerOwner, owner)) {
4994
+ throw new IndexLockContentionError(markerPath, markerOwner, "unknown-owner");
4995
+ }
4996
+ }
4997
+ return markerPath;
4998
+ }
4999
+ function getPendingRecoveries(indexPath) {
5000
+ const recoveries = [];
5001
+ const markerNames = (0, import_fs7.readdirSync)(indexPath).filter((name) => {
5002
+ if (!name.startsWith(RECOVERY_MARKER_PREFIX)) return false;
5003
+ return UUID_PATTERN.test(name.slice(RECOVERY_MARKER_PREFIX.length));
5004
+ }).sort();
5005
+ for (const markerName of markerNames) {
5006
+ const markerPath = path12.join(indexPath, markerName);
5007
+ const markerToken = markerName.slice(RECOVERY_MARKER_PREFIX.length);
5008
+ let markerStats;
5009
+ try {
5010
+ markerStats = (0, import_fs7.lstatSync)(markerPath);
5011
+ } catch (error) {
5012
+ if (getErrorCode(error) === "ENOENT") continue;
5013
+ throw error;
5014
+ }
5015
+ if (!markerStats.isDirectory()) {
5016
+ throw new IndexLockContentionError(markerPath, null, "unknown-owner");
5017
+ }
5018
+ const owner = readRecoveryOwner(markerPath);
5019
+ if (!owner || owner.token !== markerToken || getOwnerLiveness(owner) !== "dead") {
5020
+ throw new IndexLockContentionError(markerPath, owner, "unknown-owner");
5021
+ }
5022
+ recoveries.push({ owner, markerPath });
5023
+ }
5024
+ recoveries.sort((left, right) => {
5025
+ const byTimestamp = left.owner.startedAt.localeCompare(right.owner.startedAt);
5026
+ return byTimestamp !== 0 ? byTimestamp : left.markerPath.localeCompare(right.markerPath);
5027
+ });
5028
+ return recoveries;
5029
+ }
5030
+ function cleanupDeadPublicationCandidates(indexPath) {
5031
+ const candidatePattern = /^indexing\.lock(?:\.recovery\.[0-9a-f-]{36})?\.candidate\.(\d+)\.[0-9a-f-]{36}$/i;
5032
+ for (const entry of (0, import_fs7.readdirSync)(indexPath)) {
5033
+ const match = candidatePattern.exec(entry);
5034
+ if (!match) continue;
5035
+ const pid = Number(match[1]);
5036
+ if (!Number.isInteger(pid) || pid <= 0) continue;
5037
+ if (getOwnerLiveness({ pid, hostname: os4.hostname() }) !== "dead") continue;
5038
+ (0, import_fs7.rmSync)(path12.join(indexPath, entry), { recursive: true, force: true });
5039
+ }
5040
+ }
5041
+ function removeDeadReclaimMarker(lockPath, expectedOwner) {
5042
+ const markerPath = path12.join(lockPath, RECLAIM_DIRECTORY_NAME);
5043
+ const marker = readReclaimOwner(markerPath);
5044
+ if (!marker || marker.expectedOwnerToken !== expectedOwner.token || getOwnerLiveness(marker) !== "dead") {
5045
+ return false;
5046
+ }
5047
+ const currentOwner = readDirectoryOwner(lockPath);
5048
+ if (!currentOwner || !sameOwner(currentOwner, expectedOwner) || getOwnerLiveness(currentOwner) !== "dead") {
5049
+ return false;
5050
+ }
5051
+ const claimedMarkerPath = `${markerPath}.stale.${marker.pid}.${marker.token}.${(0, import_crypto.randomUUID)()}`;
5052
+ try {
5053
+ (0, import_fs7.renameSync)(markerPath, claimedMarkerPath);
5054
+ } catch (error) {
5055
+ if (getErrorCode(error) === "ENOENT") return false;
5056
+ throw error;
5057
+ }
5058
+ const claimedMarker = readReclaimOwner(claimedMarkerPath);
5059
+ const ownerAfterClaim = readDirectoryOwner(lockPath);
5060
+ if (!claimedMarker || !sameReclaimOwner(claimedMarker, marker) || !ownerAfterClaim || !sameOwner(ownerAfterClaim, expectedOwner) || getOwnerLiveness(ownerAfterClaim) !== "dead") {
5061
+ if (!(0, import_fs7.existsSync)(markerPath) && (0, import_fs7.existsSync)(claimedMarkerPath)) {
5062
+ (0, import_fs7.renameSync)(claimedMarkerPath, markerPath);
5063
+ }
5064
+ return false;
5065
+ }
5066
+ (0, import_fs7.rmSync)(claimedMarkerPath, { recursive: true, force: true });
5067
+ return true;
5068
+ }
5069
+ function reclaimDeadOwner(indexPath, lockPath, expectedOwner) {
5070
+ const reclaimPath = path12.join(lockPath, RECLAIM_DIRECTORY_NAME);
5071
+ const reclaimOwner = {
5072
+ pid: process.pid,
5073
+ hostname: os4.hostname(),
5074
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
5075
+ token: (0, import_crypto.randomUUID)(),
5076
+ expectedOwnerToken: expectedOwner.token
5077
+ };
5078
+ for (let attempt = 0; attempt < 2; attempt += 1) {
5079
+ if (publishJsonDirectory(reclaimPath, reclaimOwner)) break;
5080
+ if (attempt === 0 && removeDeadReclaimMarker(lockPath, expectedOwner)) continue;
5081
+ return false;
5082
+ }
5083
+ try {
5084
+ const currentReclaimer = readReclaimOwner(reclaimPath);
5085
+ const currentOwner = readDirectoryOwner(lockPath);
5086
+ if (!currentReclaimer || !sameReclaimOwner(currentReclaimer, reclaimOwner) || !currentOwner || !sameOwner(currentOwner, expectedOwner) || getOwnerLiveness(currentOwner) !== "dead") {
5087
+ return false;
5088
+ }
5089
+ publishRecoveryMarker(indexPath, expectedOwner);
5090
+ const ownerBeforeQuarantine = readDirectoryOwner(lockPath);
5091
+ const reclaimerBeforeQuarantine = readReclaimOwner(reclaimPath);
5092
+ if (!ownerBeforeQuarantine || !sameOwner(ownerBeforeQuarantine, expectedOwner) || getOwnerLiveness(ownerBeforeQuarantine) !== "dead" || !reclaimerBeforeQuarantine || !sameReclaimOwner(reclaimerBeforeQuarantine, reclaimOwner)) {
5093
+ return false;
5094
+ }
5095
+ const quarantinePath = `${lockPath}.stale.${expectedOwner.token}.${reclaimOwner.token}`;
5096
+ (0, import_fs7.renameSync)(lockPath, quarantinePath);
5097
+ (0, import_fs7.rmSync)(quarantinePath, { recursive: true, force: true });
5098
+ return true;
5099
+ } catch (error) {
5100
+ if (getErrorCode(error) === "ENOENT") return false;
5101
+ throw error;
5102
+ }
5103
+ }
5104
+ var IndexLockContentionError = class extends Error {
5105
+ constructor(lockPath, owner, reason) {
5106
+ const ownerDescription = owner ? `PID ${owner.pid} on ${owner.hostname}, operation ${owner.operation}, since ${owner.startedAt}` : "an unreadable owner";
5107
+ super(`Index mutation already in progress: ${ownerDescription}`);
5108
+ this.lockPath = lockPath;
5109
+ this.owner = owner;
5110
+ this.reason = reason;
5111
+ this.name = "IndexLockContentionError";
5112
+ }
5113
+ lockPath;
5114
+ owner;
5115
+ reason;
5116
+ code = "INDEX_BUSY";
5117
+ };
5118
+ function isIndexLockContentionError(error) {
5119
+ return error instanceof IndexLockContentionError || typeof error === "object" && error !== null && "code" in error && error.code === "INDEX_BUSY";
5120
+ }
5121
+ function acquireIndexLock(indexPath, operation) {
5122
+ (0, import_fs7.mkdirSync)(indexPath, { recursive: true });
5123
+ const canonicalIndexPath = import_fs7.realpathSync.native(indexPath);
5124
+ const lockPath = path12.join(canonicalIndexPath, "indexing.lock");
5125
+ cleanupDeadPublicationCandidates(canonicalIndexPath);
5126
+ for (let attempt = 0; attempt < 6; attempt += 1) {
5127
+ const owner = createOwner(operation);
5128
+ if (publishJsonDirectory(lockPath, owner)) {
5129
+ const lease = {
5130
+ canonicalIndexPath,
5131
+ lockPath,
5132
+ owner,
5133
+ recoveries: []
5134
+ };
5135
+ try {
5136
+ lease.recoveries = getPendingRecoveries(canonicalIndexPath);
5137
+ return lease;
5138
+ } catch (error) {
5139
+ releaseIndexLock(lease);
5140
+ throw error;
5141
+ }
5142
+ }
5143
+ let stats;
5144
+ try {
5145
+ stats = (0, import_fs7.lstatSync)(lockPath);
5146
+ } catch (error) {
5147
+ if (getErrorCode(error) === "ENOENT") continue;
5148
+ throw error;
5149
+ }
5150
+ if (!stats.isDirectory()) {
5151
+ const legacyOwner = readLegacyOwner(lockPath);
5152
+ throw new IndexLockContentionError(lockPath, legacyOwner, "legacy-lock");
5153
+ }
5154
+ const existingOwner = readDirectoryOwner(lockPath);
5155
+ if (!existingOwner) {
5156
+ throw new IndexLockContentionError(lockPath, null, "unknown-owner");
5157
+ }
5158
+ const liveness = getOwnerLiveness(existingOwner);
5159
+ if (liveness !== "dead") {
5160
+ throw new IndexLockContentionError(lockPath, existingOwner, liveness === "alive" ? "active" : "unknown-owner");
5161
+ }
5162
+ if (!reclaimDeadOwner(canonicalIndexPath, lockPath, existingOwner)) {
5163
+ throw new IndexLockContentionError(lockPath, existingOwner, "reclaiming");
5164
+ }
5165
+ }
5166
+ throw new IndexLockContentionError(lockPath, null, "reclaiming");
5167
+ }
5168
+ function releaseIndexLock(lease) {
5169
+ const currentOwner = readDirectoryOwner(lease.lockPath);
5170
+ if (!currentOwner || !sameOwner(currentOwner, lease.owner)) return false;
5171
+ const releasePath = `${lease.lockPath}.release.${lease.owner.pid}.${lease.owner.token}`;
5172
+ try {
5173
+ retryTransientFilesystemOperation(() => (0, import_fs7.renameSync)(lease.lockPath, releasePath));
5174
+ } catch (error) {
5175
+ if (getErrorCode(error) === "ENOENT") return false;
5176
+ throw error;
5177
+ }
5178
+ const claimedOwner = readDirectoryOwner(releasePath);
5179
+ if (!claimedOwner || !sameOwner(claimedOwner, lease.owner)) {
5180
+ if (!(0, import_fs7.existsSync)(lease.lockPath) && (0, import_fs7.existsSync)(releasePath)) {
5181
+ (0, import_fs7.renameSync)(releasePath, lease.lockPath);
5182
+ }
5183
+ return false;
5184
+ }
5185
+ try {
5186
+ retryTransientFilesystemOperation(() => (0, import_fs7.rmSync)(releasePath, { recursive: true, force: true }));
5187
+ } catch (error) {
5188
+ console.error(`[codebase-index] Lease released but tombstone cleanup failed: ${releasePath}`, error);
5189
+ }
5190
+ return true;
5191
+ }
5192
+ async function withIndexLock(indexPath, operation, callback, options = {}) {
5193
+ const lease = acquireIndexLock(indexPath, operation);
5194
+ let result;
5195
+ let callbackError;
5196
+ let callbackFailed = false;
5197
+ try {
5198
+ result = await callback(lease);
5199
+ } catch (error) {
5200
+ callbackFailed = true;
5201
+ callbackError = error;
5202
+ }
5203
+ if (!callbackFailed && options.completeRecoveries !== false) {
5204
+ try {
5205
+ completeLeaseRecovery(lease);
5206
+ } catch (error) {
5207
+ callbackFailed = true;
5208
+ callbackError = error;
5209
+ }
5210
+ }
5211
+ let releaseError;
5212
+ try {
5213
+ if (!releaseIndexLock(lease)) {
5214
+ releaseError = new Error(`Lost ownership of index mutation lease ${lease.owner.token}`);
5215
+ }
5216
+ } catch (error) {
5217
+ releaseError = error;
5218
+ }
5219
+ if (releaseError !== void 0) {
5220
+ if (callbackFailed) throw new AggregateError([callbackError, releaseError], "Index mutation and lease release both failed");
5221
+ throw releaseError;
5222
+ }
5223
+ if (callbackFailed) throw callbackError;
5224
+ return result;
5225
+ }
5226
+ function createLeaseTemporaryPath(targetPath, owner, kind = "tmp") {
5227
+ if (kind === "bak") return `${targetPath}.bak.${owner.pid}.${owner.token}`;
5228
+ temporaryCounter += 1;
5229
+ return `${targetPath}.tmp.${owner.pid}.${owner.token}.${temporaryCounter}`;
5230
+ }
5231
+ function removeLeaseTemporaryPath(temporaryPath) {
5232
+ if ((0, import_fs7.existsSync)(temporaryPath)) (0, import_fs7.rmSync)(temporaryPath, { recursive: true, force: true });
5233
+ }
5234
+ function recoverLeaseArtifacts(indexPath, owner, backupTargets) {
5235
+ for (const targetPath of backupTargets) {
5236
+ const backupPath = createLeaseTemporaryPath(targetPath, owner, "bak");
5237
+ if (!(0, import_fs7.existsSync)(backupPath)) continue;
5238
+ if ((0, import_fs7.existsSync)(targetPath)) (0, import_fs7.rmSync)(targetPath, { recursive: true, force: true });
5239
+ (0, import_fs7.renameSync)(backupPath, targetPath);
5240
+ }
5241
+ const temporaryOwnerMarker = `.tmp.${owner.pid}.${owner.token}.`;
5242
+ for (const entry of (0, import_fs7.readdirSync)(indexPath)) {
5243
+ if (!entry.includes(temporaryOwnerMarker)) continue;
5244
+ (0, import_fs7.rmSync)(path12.join(indexPath, entry), { recursive: true, force: true });
5245
+ }
5246
+ }
5247
+ function completeLeaseRecovery(lease) {
5248
+ for (const recovery of lease.recoveries) {
5249
+ const markerOwner = readRecoveryOwner(recovery.markerPath);
5250
+ if (!markerOwner || !sameOwner(markerOwner, recovery.owner)) {
5251
+ throw new Error(`Recovery marker ownership changed: ${recovery.markerPath}`);
5252
+ }
5253
+ }
5254
+ for (const recovery of lease.recoveries) {
5255
+ (0, import_fs7.rmSync)(recovery.markerPath, { recursive: true, force: true });
5256
+ }
5257
+ }
5258
+
4817
5259
  // src/indexer/index.ts
4818
5260
  var CALL_GRAPH_LANGUAGES = /* @__PURE__ */ new Set(["typescript", "tsx", "javascript", "jsx", "python", "go", "rust", "php", "apex", "zig", "gdscript", "matlab", "bash"]);
4819
5261
  var CASE_INSENSITIVE_LANGUAGES = /* @__PURE__ */ new Set(["apex"]);
@@ -4895,6 +5337,7 @@ function isSqliteCorruptionError(error) {
4895
5337
  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");
4896
5338
  }
4897
5339
  var STARTUP_WARNING_METADATA_KEY = "index.startupWarning";
5340
+ var READER_ARTIFACT_RETRY_INTERVAL_MS = 1e3;
4898
5341
  function metadataFromBlame(blame) {
4899
5342
  if (!blame) {
4900
5343
  return {};
@@ -5092,9 +5535,9 @@ function hasAllEmbeddingParts(parts, expectedPartCount) {
5092
5535
  return true;
5093
5536
  }
5094
5537
  function isPathWithinRoot(filePath, rootPath) {
5095
- const normalizedFilePath = path12.resolve(filePath);
5096
- const normalizedRoot = path12.resolve(rootPath);
5097
- return normalizedFilePath === normalizedRoot || normalizedFilePath.startsWith(`${normalizedRoot}${path12.sep}`);
5538
+ const normalizedFilePath = path13.resolve(filePath);
5539
+ const normalizedRoot = path13.resolve(rootPath);
5540
+ return normalizedFilePath === normalizedRoot || normalizedFilePath.startsWith(`${normalizedRoot}${path13.sep}`);
5098
5541
  }
5099
5542
  var rankingQueryTokenCache = /* @__PURE__ */ new Map();
5100
5543
  var rankingNameTokenCache = /* @__PURE__ */ new Map();
@@ -6160,26 +6603,133 @@ var Indexer = class {
6160
6603
  queryCacheTtlMs = 5 * 60 * 1e3;
6161
6604
  querySimilarityThreshold = 0.85;
6162
6605
  indexCompatibility = null;
6163
- indexingLockPath = "";
6606
+ activeIndexLease = null;
6607
+ initializationPromise = null;
6608
+ initializationMode = "none";
6609
+ readIssues = [];
6610
+ retiredDatabases = [];
6611
+ readerArtifactFingerprint = null;
6612
+ writerArtifactFingerprint = null;
6613
+ readerArtifactRetryAfter = /* @__PURE__ */ new Map();
6164
6614
  constructor(projectRoot3, config, host = "opencode") {
6165
6615
  this.projectRoot = projectRoot3;
6166
6616
  this.config = config;
6167
6617
  this.host = host;
6168
6618
  this.indexPath = this.getIndexPath();
6169
- this.fileHashCachePath = path12.join(this.indexPath, "file-hashes.json");
6170
- this.failedBatchesPath = path12.join(this.indexPath, "failed-batches.json");
6171
- this.indexingLockPath = path12.join(this.indexPath, "indexing.lock");
6619
+ this.fileHashCachePath = path13.join(this.indexPath, "file-hashes.json");
6620
+ this.failedBatchesPath = path13.join(this.indexPath, "failed-batches.json");
6172
6621
  this.logger = initializeLogger(config.debug);
6173
6622
  }
6174
6623
  getIndexPath() {
6175
6624
  return resolveProjectIndexPath(this.projectRoot, this.config.scope, this.host);
6176
6625
  }
6626
+ isLocalProjectIndexPath() {
6627
+ const localProjectIndexPaths = [path13.join(this.projectRoot, getHostProjectIndexRelativePath(this.host))];
6628
+ if (this.host !== "opencode") {
6629
+ localProjectIndexPaths.push(path13.join(this.projectRoot, getHostProjectIndexRelativePath("opencode")));
6630
+ }
6631
+ return localProjectIndexPaths.some((localPath) => {
6632
+ if (!(0, import_fs8.existsSync)(localPath) || !(0, import_fs8.existsSync)(this.indexPath)) {
6633
+ return path13.resolve(this.indexPath) === path13.resolve(localPath);
6634
+ }
6635
+ const indexStats = (0, import_fs8.statSync)(this.indexPath);
6636
+ const localStats = (0, import_fs8.statSync)(localPath);
6637
+ return indexStats.dev === localStats.dev && indexStats.ino === localStats.ino;
6638
+ });
6639
+ }
6640
+ resetLoadedIndexState(retireDatabase = false) {
6641
+ if (this.database) {
6642
+ if (retireDatabase) {
6643
+ this.retiredDatabases.push(this.database);
6644
+ } else {
6645
+ this.database.close();
6646
+ }
6647
+ }
6648
+ this.store = null;
6649
+ this.invertedIndex = null;
6650
+ this.database = null;
6651
+ this.provider = null;
6652
+ this.configuredProviderInfo = null;
6653
+ this.reranker = null;
6654
+ this.indexCompatibility = null;
6655
+ this.initializationMode = "none";
6656
+ this.readIssues = [];
6657
+ this.readerArtifactFingerprint = null;
6658
+ this.writerArtifactFingerprint = null;
6659
+ this.readerArtifactRetryAfter.clear();
6660
+ this.fileHashCache.clear();
6661
+ }
6662
+ refreshLoadedIndexState() {
6663
+ if (!this.store || !this.invertedIndex || !this.configuredProviderInfo) return;
6664
+ this.store.load();
6665
+ this.invertedIndex.load();
6666
+ this.fileHashCache.clear();
6667
+ this.loadFileHashCache();
6668
+ this.indexCompatibility = this.validateIndexCompatibility(this.configuredProviderInfo);
6669
+ this.readIssues = [];
6670
+ this.readerArtifactRetryAfter.clear();
6671
+ }
6672
+ async withIndexMutationLease(operation, callback) {
6673
+ const lease = acquireIndexLock(this.indexPath, operation);
6674
+ this.indexPath = lease.canonicalIndexPath;
6675
+ this.fileHashCachePath = path13.join(this.indexPath, "file-hashes.json");
6676
+ this.failedBatchesPath = path13.join(this.indexPath, "failed-batches.json");
6677
+ this.activeIndexLease = lease;
6678
+ let result;
6679
+ let callbackError;
6680
+ let callbackFailed = false;
6681
+ try {
6682
+ result = await callback(lease.recoveries.map(({ owner }) => owner));
6683
+ } catch (error) {
6684
+ callbackFailed = true;
6685
+ callbackError = error;
6686
+ }
6687
+ if (!callbackFailed) {
6688
+ try {
6689
+ completeLeaseRecovery(lease);
6690
+ this.writerArtifactFingerprint = this.captureReaderArtifactFingerprint();
6691
+ } catch (error) {
6692
+ callbackFailed = true;
6693
+ callbackError = error;
6694
+ }
6695
+ }
6696
+ let releaseError;
6697
+ try {
6698
+ if (!releaseIndexLock(lease)) {
6699
+ releaseError = new Error(`Lost ownership of index mutation lease ${lease.owner.token}`);
6700
+ this.writerArtifactFingerprint = null;
6701
+ if (this.activeIndexLease?.owner.token === lease.owner.token) {
6702
+ this.activeIndexLease = null;
6703
+ }
6704
+ } else if (this.activeIndexLease?.owner.token === lease.owner.token) {
6705
+ this.activeIndexLease = null;
6706
+ }
6707
+ } catch (error) {
6708
+ releaseError = error;
6709
+ this.writerArtifactFingerprint = null;
6710
+ if (!(0, import_fs8.existsSync)(lease.lockPath) && this.activeIndexLease?.owner.token === lease.owner.token) {
6711
+ this.activeIndexLease = null;
6712
+ }
6713
+ }
6714
+ if (releaseError !== void 0) {
6715
+ if (callbackFailed) throw new AggregateError([callbackError, releaseError], "Index mutation and lease release both failed");
6716
+ throw releaseError;
6717
+ }
6718
+ if (callbackFailed) throw callbackError;
6719
+ return result;
6720
+ }
6721
+ requireActiveLease() {
6722
+ if (!this.activeIndexLease) {
6723
+ throw new Error("Index mutation attempted without an active interprocess lease");
6724
+ }
6725
+ return this.activeIndexLease;
6726
+ }
6177
6727
  loadFileHashCache() {
6178
- if (!(0, import_fs7.existsSync)(this.fileHashCachePath)) {
6728
+ if (!(0, import_fs8.existsSync)(this.fileHashCachePath)) {
6179
6729
  return;
6180
6730
  }
6181
6731
  try {
6182
- const data = (0, import_fs7.readFileSync)(this.fileHashCachePath, "utf-8");
6732
+ const data = (0, import_fs8.readFileSync)(this.fileHashCachePath, "utf-8");
6183
6733
  const parsed = JSON.parse(data);
6184
6734
  this.fileHashCache = new Map(Object.entries(parsed));
6185
6735
  } catch (error) {
@@ -6199,15 +6749,26 @@ var Indexer = class {
6199
6749
  this.atomicWriteSync(this.fileHashCachePath, JSON.stringify(obj));
6200
6750
  }
6201
6751
  atomicWriteSync(targetPath, data) {
6202
- const tempPath = `${targetPath}.tmp`;
6203
- (0, import_fs7.mkdirSync)(path12.dirname(targetPath), { recursive: true });
6204
- (0, import_fs7.writeFileSync)(tempPath, data);
6205
- (0, import_fs7.renameSync)(tempPath, targetPath);
6752
+ const lease = this.requireActiveLease();
6753
+ const tempPath = createLeaseTemporaryPath(targetPath, lease.owner, "tmp");
6754
+ (0, import_fs8.mkdirSync)(path13.dirname(targetPath), { recursive: true });
6755
+ try {
6756
+ (0, import_fs8.writeFileSync)(tempPath, data);
6757
+ (0, import_fs8.renameSync)(tempPath, targetPath);
6758
+ } finally {
6759
+ removeLeaseTemporaryPath(tempPath);
6760
+ }
6761
+ }
6762
+ saveInvertedIndex(invertedIndex) {
6763
+ this.atomicWriteSync(
6764
+ path13.join(this.indexPath, "inverted-index.json"),
6765
+ invertedIndex.serialize()
6766
+ );
6206
6767
  }
6207
6768
  getScopedRoots() {
6208
- const roots = /* @__PURE__ */ new Set([path12.resolve(this.projectRoot)]);
6769
+ const roots = /* @__PURE__ */ new Set([path13.resolve(this.projectRoot)]);
6209
6770
  for (const kbRoot of this.config.knowledgeBases) {
6210
- roots.add(path12.resolve(this.projectRoot, kbRoot));
6771
+ roots.add(path13.resolve(this.projectRoot, kbRoot));
6211
6772
  }
6212
6773
  return Array.from(roots);
6213
6774
  }
@@ -6216,29 +6777,29 @@ var Indexer = class {
6216
6777
  if (this.config.scope !== "global") {
6217
6778
  return branchName;
6218
6779
  }
6219
- const projectHash = hashContent(path12.resolve(this.projectRoot)).slice(0, 16);
6780
+ const projectHash = hashContent(path13.resolve(this.projectRoot)).slice(0, 16);
6220
6781
  return `${projectHash}:${branchName}`;
6221
6782
  }
6222
6783
  getBranchCatalogKeyFor(branchName) {
6223
6784
  if (this.config.scope !== "global") {
6224
6785
  return branchName;
6225
6786
  }
6226
- const projectHash = hashContent(path12.resolve(this.projectRoot)).slice(0, 16);
6787
+ const projectHash = hashContent(path13.resolve(this.projectRoot)).slice(0, 16);
6227
6788
  return `${projectHash}:${branchName}`;
6228
6789
  }
6229
6790
  getLegacyBranchCatalogKey() {
6230
6791
  return this.currentBranch || "default";
6231
6792
  }
6232
6793
  getLegacyMigrationMetadataKey() {
6233
- const projectHash = hashContent(path12.resolve(this.projectRoot)).slice(0, 16);
6794
+ const projectHash = hashContent(path13.resolve(this.projectRoot)).slice(0, 16);
6234
6795
  return `index.globalBranchMigration.${projectHash}`;
6235
6796
  }
6236
6797
  getProjectEmbeddingStrategyMetadataKey() {
6237
- const projectHash = hashContent(path12.resolve(this.projectRoot)).slice(0, 16);
6798
+ const projectHash = hashContent(path13.resolve(this.projectRoot)).slice(0, 16);
6238
6799
  return `index.embeddingStrategyVersion.${projectHash}`;
6239
6800
  }
6240
6801
  getProjectForceReembedMetadataKey() {
6241
- const projectHash = hashContent(path12.resolve(this.projectRoot)).slice(0, 16);
6802
+ const projectHash = hashContent(path13.resolve(this.projectRoot)).slice(0, 16);
6242
6803
  return `index.forceReembed.${projectHash}`;
6243
6804
  }
6244
6805
  hasProjectForceReembedPending() {
@@ -6332,7 +6893,7 @@ var Indexer = class {
6332
6893
  if (!this.database) {
6333
6894
  return { chunkIds, symbolIds };
6334
6895
  }
6335
- const projectRootPath = path12.resolve(this.projectRoot);
6896
+ const projectRootPath = path13.resolve(this.projectRoot);
6336
6897
  const projectLocalFilePaths = /* @__PURE__ */ new Set([
6337
6898
  ...Array.from(this.fileHashCache.keys()).filter(
6338
6899
  (filePath) => this.isFileInCurrentScope(filePath, roots) && isPathWithinRoot(filePath, projectRootPath)
@@ -6355,7 +6916,7 @@ var Indexer = class {
6355
6916
  if (this.config.scope !== "global") {
6356
6917
  return this.getBranchCatalogCleanupKeys();
6357
6918
  }
6358
- const projectHash = hashContent(path12.resolve(this.projectRoot)).slice(0, 16);
6919
+ const projectHash = hashContent(path13.resolve(this.projectRoot)).slice(0, 16);
6359
6920
  const keys = /* @__PURE__ */ new Set();
6360
6921
  const projectChunkIdSet = new Set(projectChunkIds);
6361
6922
  const projectSymbolIdSet = new Set(projectSymbolIds);
@@ -6439,7 +7000,7 @@ var Indexer = class {
6439
7000
  if (!this.database || this.config.scope !== "global") {
6440
7001
  return false;
6441
7002
  }
6442
- const projectHash = hashContent(path12.resolve(this.projectRoot)).slice(0, 16);
7003
+ const projectHash = hashContent(path13.resolve(this.projectRoot)).slice(0, 16);
6443
7004
  const roots = this.getScopedRoots();
6444
7005
  const { chunkIds: projectLocalChunkIds, symbolIds: projectLocalSymbolIds } = this.getProjectLocalScopedOwnershipIds(roots);
6445
7006
  return this.database.getAllBranches().some(
@@ -6473,7 +7034,7 @@ var Indexer = class {
6473
7034
  ...Array.from(this.fileHashCache.keys()).filter((filePath) => this.isFileInCurrentScope(filePath, roots)),
6474
7035
  ...scopedEntries.map(({ metadata }) => metadata.filePath)
6475
7036
  ]);
6476
- const projectRootPath = path12.resolve(this.projectRoot);
7037
+ const projectRootPath = path13.resolve(this.projectRoot);
6477
7038
  const projectLocalFilePaths = new Set(
6478
7039
  Array.from(filePaths).filter((filePath) => isPathWithinRoot(filePath, projectRootPath))
6479
7040
  );
@@ -6535,34 +7096,27 @@ var Indexer = class {
6535
7096
  database.gcOrphanEmbeddings();
6536
7097
  database.gcOrphanChunks();
6537
7098
  store.save();
6538
- invertedIndex.save();
7099
+ this.saveInvertedIndex(invertedIndex);
6539
7100
  return {
6540
7101
  removedChunkIds: removedChunkIdList,
6541
7102
  hasForeignData: allMetadata.some(({ metadata }) => !this.isFileInCurrentScope(metadata.filePath, roots))
6542
7103
  };
6543
7104
  }
6544
- checkForInterruptedIndexing() {
6545
- return (0, import_fs7.existsSync)(this.indexingLockPath);
6546
- }
6547
- acquireIndexingLock() {
6548
- const lockData = {
6549
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
6550
- pid: process.pid
6551
- };
6552
- (0, import_fs7.writeFileSync)(this.indexingLockPath, JSON.stringify(lockData));
6553
- }
6554
- releaseIndexingLock() {
6555
- if ((0, import_fs7.existsSync)(this.indexingLockPath)) {
6556
- (0, import_fs7.unlinkSync)(this.indexingLockPath);
7105
+ async recoverFromInterruptedIndexingUnlocked(owners) {
7106
+ for (const owner of owners) {
7107
+ this.logger.warn("Detected interrupted indexing session, recovering...", {
7108
+ pid: owner.pid,
7109
+ hostname: owner.hostname,
7110
+ operation: owner.operation,
7111
+ startedAt: owner.startedAt
7112
+ });
6557
7113
  }
6558
- }
6559
- async recoverFromInterruptedIndexing() {
6560
- this.logger.warn("Detected interrupted indexing session, recovering...");
6561
- if ((0, import_fs7.existsSync)(this.fileHashCachePath)) {
6562
- (0, import_fs7.unlinkSync)(this.fileHashCachePath);
7114
+ if (this.config.scope === "global") {
7115
+ if ((0, import_fs8.existsSync)(this.fileHashCachePath)) {
7116
+ (0, import_fs8.unlinkSync)(this.fileHashCachePath);
7117
+ }
7118
+ await this.healthCheckUnlocked();
6563
7119
  }
6564
- await this.healthCheck();
6565
- this.releaseIndexingLock();
6566
7120
  this.logger.info("Recovery complete, next index will re-process all files");
6567
7121
  }
6568
7122
  loadFailedBatches(maxChunkTokens) {
@@ -6578,10 +7132,10 @@ var Indexer = class {
6578
7132
  }
6579
7133
  }
6580
7134
  loadSerializedFailedBatches() {
6581
- if (!(0, import_fs7.existsSync)(this.failedBatchesPath)) {
7135
+ if (!(0, import_fs8.existsSync)(this.failedBatchesPath)) {
6582
7136
  return [];
6583
7137
  }
6584
- const data = (0, import_fs7.readFileSync)(this.failedBatchesPath, "utf-8");
7138
+ const data = (0, import_fs8.readFileSync)(this.failedBatchesPath, "utf-8");
6585
7139
  const parsed = JSON.parse(data);
6586
7140
  return parsed.map((batch) => {
6587
7141
  const chunks = Array.isArray(batch.chunks) ? batch.chunks : [];
@@ -6598,15 +7152,15 @@ var Indexer = class {
6598
7152
  }
6599
7153
  saveFailedBatches(batches) {
6600
7154
  if (batches.length === 0) {
6601
- if ((0, import_fs7.existsSync)(this.failedBatchesPath)) {
7155
+ if ((0, import_fs8.existsSync)(this.failedBatchesPath)) {
6602
7156
  try {
6603
- (0, import_fs7.unlinkSync)(this.failedBatchesPath);
7157
+ (0, import_fs8.unlinkSync)(this.failedBatchesPath);
6604
7158
  } catch {
6605
7159
  }
6606
7160
  }
6607
7161
  return;
6608
7162
  }
6609
- (0, import_fs7.writeFileSync)(this.failedBatchesPath, JSON.stringify(batches, null, 2));
7163
+ this.atomicWriteSync(this.failedBatchesPath, JSON.stringify(batches, null, 2));
6610
7164
  }
6611
7165
  collectRetryableFailedChunks(currentFileHashes, unchangedFilePaths, maxChunkTokens) {
6612
7166
  const retryableById = /* @__PURE__ */ new Map();
@@ -6786,7 +7340,7 @@ var Indexer = class {
6786
7340
  const intent = isLikelyImplementationPath(candidate.metadata.filePath) ? "implementation" : "doc_or_test";
6787
7341
  parts.push(`intent_hint: ${intent}`);
6788
7342
  try {
6789
- const fileContent = await import_fs7.promises.readFile(candidate.metadata.filePath, "utf-8");
7343
+ const fileContent = await import_fs8.promises.readFile(candidate.metadata.filePath, "utf-8");
6790
7344
  const lines = fileContent.split("\n");
6791
7345
  const snippetStartLine = Math.max(1, candidate.metadata.startLine - 2);
6792
7346
  const snippetEndLine = Math.min(lines.length, candidate.metadata.endLine + 2);
@@ -6800,6 +7354,222 @@ var Indexer = class {
6800
7354
  return parts.join("\n");
6801
7355
  }
6802
7356
  async initialize() {
7357
+ if (this.initializationPromise) {
7358
+ await this.initializationPromise;
7359
+ }
7360
+ if (this.isInitializedFor("reader")) {
7361
+ return;
7362
+ }
7363
+ await this.initializeOnce("reader", [], { skipAutoGc: true });
7364
+ }
7365
+ async initializeOnce(mode, recoveredOwners, options) {
7366
+ if (this.initializationPromise) {
7367
+ await this.initializationPromise;
7368
+ if (this.isInitializedFor(mode)) {
7369
+ return;
7370
+ }
7371
+ return this.initializeOnce(mode, recoveredOwners, options);
7372
+ }
7373
+ if (this.isInitializedFor(mode)) {
7374
+ return;
7375
+ }
7376
+ const initialization = this.initializeUnlocked(mode, recoveredOwners, options).catch((error) => {
7377
+ this.resetLoadedIndexState();
7378
+ throw error;
7379
+ }).finally(() => {
7380
+ if (this.initializationPromise === initialization) {
7381
+ this.initializationPromise = null;
7382
+ }
7383
+ });
7384
+ this.initializationPromise = initialization;
7385
+ await initialization;
7386
+ }
7387
+ isInitializedFor(mode) {
7388
+ const hasState = Boolean(
7389
+ this.store && this.provider && this.invertedIndex && this.configuredProviderInfo && this.database
7390
+ );
7391
+ if (!hasState) {
7392
+ return false;
7393
+ }
7394
+ return mode === "reader" ? this.initializationMode !== "none" : this.initializationMode === "writer";
7395
+ }
7396
+ recordReadIssue(component, message, error) {
7397
+ this.readIssues.push(this.createReadIssue(component, message));
7398
+ this.readerArtifactRetryAfter.set(component, Date.now() + READER_ARTIFACT_RETRY_INTERVAL_MS);
7399
+ this.logger.warn(message, error === void 0 ? void 0 : { error: getErrorMessage2(error) });
7400
+ }
7401
+ createReadIssue(component, message) {
7402
+ return {
7403
+ component,
7404
+ message,
7405
+ blocking: component !== "keyword"
7406
+ };
7407
+ }
7408
+ getVectorReadIssueMessage() {
7409
+ if (this.config.scope === "global") {
7410
+ return "Shared vector index could not be read. Restore or repair the complete fingerprinted shared vector artifacts; automatic reset is disabled for global scope.";
7411
+ }
7412
+ if (!this.isLocalProjectIndexPath()) {
7413
+ 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.";
7414
+ }
7415
+ 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.";
7416
+ }
7417
+ getKeywordReadIssueMessage() {
7418
+ if (this.config.scope === "global") {
7419
+ 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.";
7420
+ }
7421
+ if (!this.isLocalProjectIndexPath()) {
7422
+ 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.";
7423
+ }
7424
+ 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.";
7425
+ }
7426
+ getDatabaseReadIssueMessage() {
7427
+ if (this.config.scope === "global") {
7428
+ return "Shared index database could not be read. Restore or repair the shared SQLite database; automatic reset is disabled for global scope.";
7429
+ }
7430
+ if (!this.isLocalProjectIndexPath()) {
7431
+ 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.";
7432
+ }
7433
+ return "Index database could not be read. Run index_codebase after the active writer finishes to repair or migrate it under the writer lease.";
7434
+ }
7435
+ getReaderFileFingerprint(filePath, identityOnly = false) {
7436
+ try {
7437
+ const stats = (0, import_fs8.statSync)(filePath);
7438
+ if (identityOnly) {
7439
+ return `${stats.dev}:${stats.ino}`;
7440
+ }
7441
+ return `${stats.dev}:${stats.ino}:${stats.size}:${stats.mtimeMs}:${stats.ctimeMs}`;
7442
+ } catch (error) {
7443
+ return `unavailable:${getErrorMessage2(error)}`;
7444
+ }
7445
+ }
7446
+ captureReaderArtifactFingerprint() {
7447
+ const storePath = path13.join(this.indexPath, "vectors");
7448
+ return {
7449
+ vectors: `${this.getReaderFileFingerprint(storePath)}|${this.getReaderFileFingerprint(`${storePath}.meta.json`)}`,
7450
+ keyword: this.getReaderFileFingerprint(path13.join(this.indexPath, "inverted-index.json")),
7451
+ database: this.getReaderFileFingerprint(path13.join(this.indexPath, "codebase.db")),
7452
+ databaseIdentity: this.getReaderFileFingerprint(path13.join(this.indexPath, "codebase.db"), true)
7453
+ };
7454
+ }
7455
+ refreshReaderArtifacts() {
7456
+ if (this.initializationMode !== "reader" || !this.configuredProviderInfo) {
7457
+ return;
7458
+ }
7459
+ const previousFingerprint = this.readerArtifactFingerprint;
7460
+ const currentFingerprint = this.captureReaderArtifactFingerprint();
7461
+ const issues = new Map(this.readIssues.map((issue) => [issue.component, issue]));
7462
+ const retryDue = (component) => issues.has(component) && Date.now() >= (this.readerArtifactRetryAfter.get(component) ?? 0);
7463
+ const vectorsChanged = !previousFingerprint || currentFingerprint.vectors !== previousFingerprint.vectors;
7464
+ const keywordChanged = !previousFingerprint || currentFingerprint.keyword !== previousFingerprint.keyword;
7465
+ const databaseChanged = !previousFingerprint || currentFingerprint.database !== previousFingerprint.database;
7466
+ const databaseReplaced = !previousFingerprint || currentFingerprint.databaseIdentity !== previousFingerprint.databaseIdentity;
7467
+ if (previousFingerprint && !vectorsChanged && !keywordChanged && !databaseChanged && !Array.from(issues.keys()).some(retryDue)) {
7468
+ return;
7469
+ }
7470
+ const setIssue = (component, message, error) => {
7471
+ if (!issues.has(component)) {
7472
+ this.logger.warn(message, error === void 0 ? void 0 : { error: getErrorMessage2(error) });
7473
+ }
7474
+ issues.set(component, this.createReadIssue(component, message));
7475
+ this.readerArtifactRetryAfter.set(component, Date.now() + READER_ARTIFACT_RETRY_INTERVAL_MS);
7476
+ };
7477
+ const storePath = path13.join(this.indexPath, "vectors");
7478
+ const vectorMetadataPath = `${storePath}.meta.json`;
7479
+ const invertedIndexPath = path13.join(this.indexPath, "inverted-index.json");
7480
+ const dbPath = path13.join(this.indexPath, "codebase.db");
7481
+ if (vectorsChanged || retryDue("vectors")) {
7482
+ const vectorStoreExists = (0, import_fs8.existsSync)(storePath);
7483
+ const vectorMetadataExists = (0, import_fs8.existsSync)(vectorMetadataPath);
7484
+ if (vectorStoreExists && vectorMetadataExists) {
7485
+ try {
7486
+ const store = new VectorStore(storePath, this.configuredProviderInfo.modelInfo.dimensions);
7487
+ store.loadStrict();
7488
+ this.store = store;
7489
+ issues.delete("vectors");
7490
+ this.readerArtifactRetryAfter.delete("vectors");
7491
+ } catch (error) {
7492
+ setIssue("vectors", this.getVectorReadIssueMessage(), error);
7493
+ }
7494
+ } else if (vectorStoreExists !== vectorMetadataExists || issues.has("vectors")) {
7495
+ setIssue("vectors", this.getVectorReadIssueMessage());
7496
+ }
7497
+ }
7498
+ if (keywordChanged || retryDue("keyword") || !(0, import_fs8.existsSync)(invertedIndexPath) && (this.store?.count() ?? 0) > 0) {
7499
+ if ((0, import_fs8.existsSync)(invertedIndexPath)) {
7500
+ try {
7501
+ const invertedIndex = new InvertedIndex(invertedIndexPath);
7502
+ invertedIndex.load();
7503
+ this.invertedIndex = invertedIndex;
7504
+ issues.delete("keyword");
7505
+ this.readerArtifactRetryAfter.delete("keyword");
7506
+ } catch (error) {
7507
+ setIssue("keyword", this.getKeywordReadIssueMessage(), error);
7508
+ }
7509
+ } else if ((this.store?.count() ?? 0) > 0 || issues.has("keyword")) {
7510
+ setIssue("keyword", this.getKeywordReadIssueMessage());
7511
+ }
7512
+ }
7513
+ if (databaseReplaced || databaseChanged && issues.has("database") || retryDue("database")) {
7514
+ if ((0, import_fs8.existsSync)(dbPath)) {
7515
+ try {
7516
+ const database = Database.openReadOnly(dbPath);
7517
+ if (this.database) {
7518
+ this.retiredDatabases.push(this.database);
7519
+ }
7520
+ this.database = database;
7521
+ issues.delete("database");
7522
+ this.readerArtifactRetryAfter.delete("database");
7523
+ } catch (error) {
7524
+ setIssue("database", this.getDatabaseReadIssueMessage(), error);
7525
+ }
7526
+ } else if ((this.store?.count() ?? 0) > 0 || issues.has("database")) {
7527
+ setIssue("database", this.getDatabaseReadIssueMessage());
7528
+ }
7529
+ }
7530
+ if (!issues.has("database")) {
7531
+ try {
7532
+ this.indexCompatibility = this.validateIndexCompatibility(this.configuredProviderInfo);
7533
+ } catch (error) {
7534
+ setIssue("database", this.getDatabaseReadIssueMessage(), error);
7535
+ }
7536
+ }
7537
+ this.readIssues = Array.from(issues.values());
7538
+ this.readerArtifactFingerprint = currentFingerprint;
7539
+ }
7540
+ refreshInactiveWriterArtifacts() {
7541
+ if (this.initializationMode !== "writer" || this.activeIndexLease) {
7542
+ return true;
7543
+ }
7544
+ const previousFingerprint = this.writerArtifactFingerprint;
7545
+ const currentFingerprint = this.captureReaderArtifactFingerprint();
7546
+ const retryDue = this.readIssues.some(
7547
+ (issue) => Date.now() >= (this.readerArtifactRetryAfter.get(issue.component) ?? 0)
7548
+ );
7549
+ const artifactsChanged = !previousFingerprint || currentFingerprint.vectors !== previousFingerprint.vectors || currentFingerprint.keyword !== previousFingerprint.keyword || currentFingerprint.database !== previousFingerprint.database || currentFingerprint.databaseIdentity !== previousFingerprint.databaseIdentity;
7550
+ if (!artifactsChanged && !retryDue) {
7551
+ return true;
7552
+ }
7553
+ if (!previousFingerprint || currentFingerprint.databaseIdentity !== previousFingerprint.databaseIdentity) {
7554
+ return false;
7555
+ }
7556
+ this.initializationMode = "reader";
7557
+ this.readerArtifactFingerprint = previousFingerprint;
7558
+ try {
7559
+ this.refreshReaderArtifacts();
7560
+ this.writerArtifactFingerprint = this.readerArtifactFingerprint ?? currentFingerprint;
7561
+ } finally {
7562
+ this.readerArtifactFingerprint = null;
7563
+ this.initializationMode = "writer";
7564
+ }
7565
+ return true;
7566
+ }
7567
+ async initializeUnlocked(mode, recoveredOwners = [], options = {}) {
7568
+ if (mode === "writer") {
7569
+ this.requireActiveLease();
7570
+ }
7571
+ this.readIssues = [];
7572
+ this.readerArtifactRetryAfter.clear();
6803
7573
  if (this.config.embeddingProvider === "custom") {
6804
7574
  if (!this.config.customProvider) {
6805
7575
  throw new Error("embeddingProvider is 'custom' but customProvider config is missing.");
@@ -6831,36 +7601,103 @@ var Indexer = class {
6831
7601
  });
6832
7602
  }
6833
7603
  }
6834
- await import_fs7.promises.mkdir(this.indexPath, { recursive: true });
6835
7604
  const dimensions = this.configuredProviderInfo.modelInfo.dimensions;
6836
- const storePath = path12.join(this.indexPath, "vectors");
6837
- this.store = new VectorStore(storePath, dimensions);
6838
- const indexFilePath = path12.join(this.indexPath, "vectors.usearch");
6839
- if ((0, import_fs7.existsSync)(indexFilePath)) {
6840
- this.store.load();
6841
- }
6842
- const invertedIndexPath = path12.join(this.indexPath, "inverted-index.json");
6843
- this.invertedIndex = new InvertedIndex(invertedIndexPath);
6844
- try {
6845
- this.invertedIndex.load();
6846
- } catch {
6847
- if ((0, import_fs7.existsSync)(invertedIndexPath)) {
6848
- await import_fs7.promises.unlink(invertedIndexPath);
7605
+ const storePath = path13.join(this.indexPath, "vectors");
7606
+ const vectorMetadataPath = `${storePath}.meta.json`;
7607
+ const invertedIndexPath = path13.join(this.indexPath, "inverted-index.json");
7608
+ const dbPath = path13.join(this.indexPath, "codebase.db");
7609
+ let dbIsNew = !(0, import_fs8.existsSync)(dbPath);
7610
+ const readerArtifactFingerprint = mode === "reader" ? this.captureReaderArtifactFingerprint() : null;
7611
+ if (mode === "writer") {
7612
+ await import_fs8.promises.mkdir(this.indexPath, { recursive: true });
7613
+ if (recoveredOwners.length > 0 && this.config.scope === "project" && !this.isLocalProjectIndexPath()) {
7614
+ throw new Error(
7615
+ "Interrupted indexing recovery is unsafe while using an inherited worktree index. Run index_codebase with force=true to create a local project index boundary."
7616
+ );
7617
+ }
7618
+ for (const recoveredOwner of recoveredOwners) {
7619
+ recoverLeaseArtifacts(this.indexPath, recoveredOwner, [
7620
+ storePath,
7621
+ `${storePath}.meta.json`
7622
+ ]);
7623
+ }
7624
+ if (recoveredOwners.length > 0 && this.config.scope === "project") {
7625
+ await this.resetLocalIndexArtifacts();
7626
+ }
7627
+ this.store = new VectorStore(storePath, dimensions);
7628
+ if ((0, import_fs8.existsSync)(storePath) || (0, import_fs8.existsSync)(vectorMetadataPath)) {
7629
+ this.store.load();
6849
7630
  }
6850
7631
  this.invertedIndex = new InvertedIndex(invertedIndexPath);
6851
- }
6852
- const dbPath = path12.join(this.indexPath, "codebase.db");
6853
- let dbIsNew = !(0, import_fs7.existsSync)(dbPath);
6854
- try {
6855
- this.database = new Database(dbPath);
6856
- } catch (error) {
6857
- if (!await this.tryResetCorruptedIndex("initializing index database", error)) {
6858
- throw error;
7632
+ try {
7633
+ this.invertedIndex.load();
7634
+ } catch {
7635
+ if ((0, import_fs8.existsSync)(invertedIndexPath)) {
7636
+ await import_fs8.promises.unlink(invertedIndexPath);
7637
+ }
7638
+ this.invertedIndex = new InvertedIndex(invertedIndexPath);
6859
7639
  }
7640
+ try {
7641
+ this.database = new Database(dbPath);
7642
+ } catch (error) {
7643
+ if (!await this.tryResetCorruptedIndex("initializing index database", error)) {
7644
+ throw error;
7645
+ }
7646
+ this.store = new VectorStore(storePath, dimensions);
7647
+ this.invertedIndex = new InvertedIndex(invertedIndexPath);
7648
+ this.database = new Database(dbPath);
7649
+ dbIsNew = true;
7650
+ }
7651
+ } else {
6860
7652
  this.store = new VectorStore(storePath, dimensions);
7653
+ const vectorStoreExists = (0, import_fs8.existsSync)(storePath);
7654
+ const vectorMetadataExists = (0, import_fs8.existsSync)(vectorMetadataPath);
7655
+ const vectorReadFailureMessage = this.getVectorReadIssueMessage();
7656
+ if (vectorStoreExists !== vectorMetadataExists) {
7657
+ this.recordReadIssue("vectors", vectorReadFailureMessage);
7658
+ } else if (vectorStoreExists) {
7659
+ try {
7660
+ this.store.loadStrict();
7661
+ } catch (error) {
7662
+ this.recordReadIssue("vectors", vectorReadFailureMessage, error);
7663
+ this.store = new VectorStore(storePath, dimensions);
7664
+ }
7665
+ }
6861
7666
  this.invertedIndex = new InvertedIndex(invertedIndexPath);
6862
- this.database = new Database(dbPath);
6863
- dbIsNew = true;
7667
+ if ((0, import_fs8.existsSync)(invertedIndexPath)) {
7668
+ try {
7669
+ this.invertedIndex.load();
7670
+ } catch (error) {
7671
+ this.recordReadIssue(
7672
+ "keyword",
7673
+ this.getKeywordReadIssueMessage(),
7674
+ error
7675
+ );
7676
+ this.invertedIndex = new InvertedIndex(invertedIndexPath);
7677
+ }
7678
+ } else if (this.store.count() > 0) {
7679
+ this.recordReadIssue("keyword", this.getKeywordReadIssueMessage());
7680
+ }
7681
+ if ((0, import_fs8.existsSync)(dbPath)) {
7682
+ try {
7683
+ this.database = Database.openReadOnly(dbPath);
7684
+ } catch (error) {
7685
+ this.recordReadIssue(
7686
+ "database",
7687
+ this.getDatabaseReadIssueMessage(),
7688
+ error
7689
+ );
7690
+ this.database = Database.createEmptyReadOnly();
7691
+ }
7692
+ } else {
7693
+ this.database = Database.createEmptyReadOnly();
7694
+ if (this.store.count() > 0) {
7695
+ this.recordReadIssue(
7696
+ "database",
7697
+ `Index database is missing for the published vectors. ${this.getDatabaseReadIssueMessage()}`
7698
+ );
7699
+ }
7700
+ }
6864
7701
  }
6865
7702
  if (isGitRepo(this.projectRoot)) {
6866
7703
  this.currentBranch = getBranchOrDefault(this.projectRoot);
@@ -6874,10 +7711,10 @@ var Indexer = class {
6874
7711
  this.baseBranch = "default";
6875
7712
  this.logger.branch("debug", "Not a git repository, using default branch");
6876
7713
  }
6877
- if (this.checkForInterruptedIndexing()) {
6878
- await this.recoverFromInterruptedIndexing();
7714
+ if (mode === "writer" && recoveredOwners.length > 0) {
7715
+ await this.recoverFromInterruptedIndexingUnlocked(recoveredOwners);
6879
7716
  }
6880
- if (dbIsNew && this.store.count() > 0) {
7717
+ if (mode === "writer" && dbIsNew && this.store.count() > 0) {
6881
7718
  this.migrateFromLegacyIndex();
6882
7719
  }
6883
7720
  this.loadFileHashCache();
@@ -6889,9 +7726,11 @@ var Indexer = class {
6889
7726
  configuredProviderInfo: this.configuredProviderInfo
6890
7727
  });
6891
7728
  }
6892
- if (this.config.indexing.autoGc) {
7729
+ if (mode === "writer" && this.config.indexing.autoGc && !options.skipAutoGc) {
6893
7730
  await this.maybeRunAutoGc();
6894
7731
  }
7732
+ this.initializationMode = mode;
7733
+ this.readerArtifactFingerprint = readerArtifactFingerprint;
6895
7734
  }
6896
7735
  async maybeRunAutoGc() {
6897
7736
  if (!this.database) return;
@@ -6908,7 +7747,7 @@ var Indexer = class {
6908
7747
  }
6909
7748
  }
6910
7749
  if (shouldRunGc) {
6911
- const result = await this.healthCheck();
7750
+ const result = await this.healthCheckUnlocked();
6912
7751
  if (result.warning) {
6913
7752
  this.database.setMetadata(STARTUP_WARNING_METADATA_KEY, result.warning);
6914
7753
  } else {
@@ -6930,7 +7769,7 @@ var Indexer = class {
6930
7769
  if (await this.tryResetCorruptedIndex("running automatic orphan garbage collection", error)) {
6931
7770
  return {
6932
7771
  resetCorruptedIndex: true,
6933
- warning: this.getCorruptedIndexWarning(path12.join(this.indexPath, "codebase.db"))
7772
+ warning: this.getCorruptedIndexWarning(path13.join(this.indexPath, "codebase.db"))
6934
7773
  };
6935
7774
  }
6936
7775
  throw error;
@@ -6945,28 +7784,29 @@ var Indexer = class {
6945
7784
  return;
6946
7785
  }
6947
7786
  const retainedEntries = store.getAllMetadata().filter(({ key }) => !excludedSet.has(key));
6948
- const storeBasePath = path12.join(this.indexPath, "vectors");
6949
- const storeIndexPath = `${storeBasePath}.usearch`;
7787
+ const storeBasePath = path13.join(this.indexPath, "vectors");
7788
+ const storeIndexPath = storeBasePath;
6950
7789
  const storeMetadataPath = `${storeBasePath}.meta.json`;
6951
- const backupIndexPath = `${storeIndexPath}.bak`;
6952
- const backupMetadataPath = `${storeMetadataPath}.bak`;
7790
+ const lease = this.requireActiveLease();
7791
+ const backupIndexPath = createLeaseTemporaryPath(storeIndexPath, lease.owner, "bak");
7792
+ const backupMetadataPath = createLeaseTemporaryPath(storeMetadataPath, lease.owner, "bak");
6953
7793
  let backedUpIndex = false;
6954
7794
  let backedUpMetadata = false;
6955
7795
  let rebuiltCount = 0;
6956
7796
  let skippedCount = 0;
6957
- if ((0, import_fs7.existsSync)(backupIndexPath)) {
6958
- (0, import_fs7.unlinkSync)(backupIndexPath);
7797
+ if ((0, import_fs8.existsSync)(backupIndexPath)) {
7798
+ (0, import_fs8.unlinkSync)(backupIndexPath);
6959
7799
  }
6960
- if ((0, import_fs7.existsSync)(backupMetadataPath)) {
6961
- (0, import_fs7.unlinkSync)(backupMetadataPath);
7800
+ if ((0, import_fs8.existsSync)(backupMetadataPath)) {
7801
+ (0, import_fs8.unlinkSync)(backupMetadataPath);
6962
7802
  }
6963
7803
  try {
6964
- if ((0, import_fs7.existsSync)(storeIndexPath)) {
6965
- (0, import_fs7.renameSync)(storeIndexPath, backupIndexPath);
7804
+ if ((0, import_fs8.existsSync)(storeIndexPath)) {
7805
+ (0, import_fs8.renameSync)(storeIndexPath, backupIndexPath);
6966
7806
  backedUpIndex = true;
6967
7807
  }
6968
- if ((0, import_fs7.existsSync)(storeMetadataPath)) {
6969
- (0, import_fs7.renameSync)(storeMetadataPath, backupMetadataPath);
7808
+ if ((0, import_fs8.existsSync)(storeMetadataPath)) {
7809
+ (0, import_fs8.renameSync)(storeMetadataPath, backupMetadataPath);
6970
7810
  backedUpMetadata = true;
6971
7811
  }
6972
7812
  store.clear();
@@ -6986,11 +7826,11 @@ var Indexer = class {
6986
7826
  rebuiltCount += 1;
6987
7827
  }
6988
7828
  store.save();
6989
- if (backedUpIndex && (0, import_fs7.existsSync)(backupIndexPath)) {
6990
- (0, import_fs7.unlinkSync)(backupIndexPath);
7829
+ if (backedUpIndex && (0, import_fs8.existsSync)(backupIndexPath)) {
7830
+ (0, import_fs8.unlinkSync)(backupIndexPath);
6991
7831
  }
6992
- if (backedUpMetadata && (0, import_fs7.existsSync)(backupMetadataPath)) {
6993
- (0, import_fs7.unlinkSync)(backupMetadataPath);
7832
+ if (backedUpMetadata && (0, import_fs8.existsSync)(backupMetadataPath)) {
7833
+ (0, import_fs8.unlinkSync)(backupMetadataPath);
6994
7834
  }
6995
7835
  this.logger.gc("info", "Rebuilt vector store to avoid native remove", {
6996
7836
  excludedChunks: excludedSet.size,
@@ -7002,17 +7842,17 @@ var Indexer = class {
7002
7842
  store.clear();
7003
7843
  } catch {
7004
7844
  }
7005
- if ((0, import_fs7.existsSync)(storeIndexPath)) {
7006
- (0, import_fs7.unlinkSync)(storeIndexPath);
7845
+ if ((0, import_fs8.existsSync)(storeIndexPath)) {
7846
+ (0, import_fs8.unlinkSync)(storeIndexPath);
7007
7847
  }
7008
- if ((0, import_fs7.existsSync)(storeMetadataPath)) {
7009
- (0, import_fs7.unlinkSync)(storeMetadataPath);
7848
+ if ((0, import_fs8.existsSync)(storeMetadataPath)) {
7849
+ (0, import_fs8.unlinkSync)(storeMetadataPath);
7010
7850
  }
7011
- if (backedUpIndex && (0, import_fs7.existsSync)(backupIndexPath)) {
7012
- (0, import_fs7.renameSync)(backupIndexPath, storeIndexPath);
7851
+ if (backedUpIndex && (0, import_fs8.existsSync)(backupIndexPath)) {
7852
+ (0, import_fs8.renameSync)(backupIndexPath, storeIndexPath);
7013
7853
  }
7014
- if (backedUpMetadata && (0, import_fs7.existsSync)(backupMetadataPath)) {
7015
- (0, import_fs7.renameSync)(backupMetadataPath, storeMetadataPath);
7854
+ if (backedUpMetadata && (0, import_fs8.existsSync)(backupMetadataPath)) {
7855
+ (0, import_fs8.renameSync)(backupMetadataPath, storeMetadataPath);
7016
7856
  }
7017
7857
  if (backedUpIndex || backedUpMetadata) {
7018
7858
  store.load();
@@ -7026,11 +7866,37 @@ var Indexer = class {
7026
7866
  }
7027
7867
  return `Detected a corrupted local SQLite index at ${dbPath} and reset the local index. Run index_codebase to rebuild search data.`;
7028
7868
  }
7869
+ async resetLocalIndexArtifacts() {
7870
+ this.store = null;
7871
+ this.invertedIndex = null;
7872
+ this.database?.close();
7873
+ this.database = null;
7874
+ this.indexCompatibility = null;
7875
+ this.initializationMode = "none";
7876
+ this.readIssues = [];
7877
+ this.readerArtifactFingerprint = null;
7878
+ this.writerArtifactFingerprint = null;
7879
+ this.readerArtifactRetryAfter.clear();
7880
+ this.fileHashCache.clear();
7881
+ const resetPaths = [
7882
+ path13.join(this.indexPath, "codebase.db"),
7883
+ path13.join(this.indexPath, "codebase.db-shm"),
7884
+ path13.join(this.indexPath, "codebase.db-wal"),
7885
+ path13.join(this.indexPath, "vectors"),
7886
+ path13.join(this.indexPath, "vectors.usearch"),
7887
+ path13.join(this.indexPath, "vectors.meta.json"),
7888
+ path13.join(this.indexPath, "inverted-index.json"),
7889
+ path13.join(this.indexPath, "file-hashes.json"),
7890
+ path13.join(this.indexPath, "failed-batches.json")
7891
+ ];
7892
+ await Promise.all(resetPaths.map((targetPath) => import_fs8.promises.rm(targetPath, { recursive: true, force: true })));
7893
+ await import_fs8.promises.mkdir(this.indexPath, { recursive: true });
7894
+ }
7029
7895
  async tryResetCorruptedIndex(stage, error) {
7030
7896
  if (!isSqliteCorruptionError(error)) {
7031
7897
  return false;
7032
7898
  }
7033
- const dbPath = path12.join(this.indexPath, "codebase.db");
7899
+ const dbPath = path13.join(this.indexPath, "codebase.db");
7034
7900
  const warning = this.getCorruptedIndexWarning(dbPath);
7035
7901
  const errorMessage = getErrorMessage2(error);
7036
7902
  if (this.config.scope === "global") {
@@ -7046,30 +7912,7 @@ var Indexer = class {
7046
7912
  dbPath,
7047
7913
  error: errorMessage
7048
7914
  });
7049
- this.store = null;
7050
- this.invertedIndex = null;
7051
- this.database?.close();
7052
- this.database = null;
7053
- this.indexCompatibility = null;
7054
- this.fileHashCache.clear();
7055
- const resetPaths = [
7056
- path12.join(this.indexPath, "codebase.db"),
7057
- path12.join(this.indexPath, "codebase.db-shm"),
7058
- path12.join(this.indexPath, "codebase.db-wal"),
7059
- path12.join(this.indexPath, "vectors.usearch"),
7060
- path12.join(this.indexPath, "inverted-index.json"),
7061
- path12.join(this.indexPath, "file-hashes.json"),
7062
- path12.join(this.indexPath, "failed-batches.json"),
7063
- path12.join(this.indexPath, "indexing.lock"),
7064
- path12.join(this.indexPath, "vectors")
7065
- ];
7066
- await Promise.all(resetPaths.map(async (targetPath) => {
7067
- try {
7068
- await import_fs7.promises.rm(targetPath, { recursive: true, force: true });
7069
- } catch {
7070
- }
7071
- }));
7072
- await import_fs7.promises.mkdir(this.indexPath, { recursive: true });
7915
+ await this.resetLocalIndexArtifacts();
7073
7916
  return true;
7074
7917
  }
7075
7918
  migrateFromLegacyIndex() {
@@ -7188,8 +8031,62 @@ var Indexer = class {
7188
8031
  return this.indexCompatibility;
7189
8032
  }
7190
8033
  async ensureInitialized() {
8034
+ let initializedReader = false;
8035
+ while (true) {
8036
+ if (this.initializationPromise) {
8037
+ await this.initializationPromise;
8038
+ }
8039
+ if (!this.isInitializedFor("reader")) {
8040
+ await this.initialize();
8041
+ initializedReader = true;
8042
+ continue;
8043
+ }
8044
+ if (this.initializationMode === "writer" && !this.activeIndexLease && !initializedReader) {
8045
+ if (!this.refreshInactiveWriterArtifacts()) {
8046
+ this.resetLoadedIndexState(true);
8047
+ await this.initialize();
8048
+ initializedReader = true;
8049
+ continue;
8050
+ }
8051
+ }
8052
+ if (this.initializationMode === "reader" && !initializedReader) {
8053
+ this.refreshReaderArtifacts();
8054
+ }
8055
+ const state = this.requireLoadedIndexState();
8056
+ return {
8057
+ ...state,
8058
+ readIssues: [...this.readIssues],
8059
+ compatibility: this.indexCompatibility ?? this.validateIndexCompatibility(state.configuredProviderInfo)
8060
+ };
8061
+ }
8062
+ }
8063
+ async ensureInitializedUnlocked(recoveredOwners = []) {
8064
+ this.requireActiveLease();
8065
+ if (this.initializationPromise) {
8066
+ await this.initializationPromise;
8067
+ }
8068
+ if (recoveredOwners.length > 0 || !this.isInitializedFor("writer")) {
8069
+ const retireReaderDatabase = this.initializationMode === "reader";
8070
+ this.resetLoadedIndexState(retireReaderDatabase);
8071
+ await this.initializeOnce("writer", recoveredOwners, { skipAutoGc: true });
8072
+ } else {
8073
+ this.refreshLoadedIndexState();
8074
+ }
8075
+ if (this.config.indexing.autoGc) {
8076
+ await this.maybeRunAutoGc();
8077
+ }
8078
+ return this.requireLoadedIndexState();
8079
+ }
8080
+ requireReadableComponents(readIssues, ...components) {
8081
+ const componentSet = new Set(components);
8082
+ const issues = readIssues.filter((issue) => issue.blocking && componentSet.has(issue.component));
8083
+ if (issues.length > 0) {
8084
+ throw new Error(issues.map((issue) => issue.message).join(" "));
8085
+ }
8086
+ }
8087
+ requireLoadedIndexState() {
7191
8088
  if (!this.store || !this.provider || !this.invertedIndex || !this.configuredProviderInfo || !this.database) {
7192
- await this.initialize();
8089
+ throw new Error("Index state is not initialized");
7193
8090
  }
7194
8091
  return {
7195
8092
  store: this.store,
@@ -7213,7 +8110,12 @@ var Indexer = class {
7213
8110
  return createCostEstimate(files, configuredProviderInfo);
7214
8111
  }
7215
8112
  async index(onProgress) {
7216
- const { store, provider, invertedIndex, database, configuredProviderInfo } = await this.ensureInitialized();
8113
+ return this.withIndexMutationLease("index", async (recoveredOwners) => {
8114
+ return this.indexUnlocked(onProgress, recoveredOwners);
8115
+ });
8116
+ }
8117
+ async indexUnlocked(onProgress, recoveredOwners = [], stateReady = false) {
8118
+ const { store, provider, invertedIndex, database, configuredProviderInfo } = stateReady ? this.requireLoadedIndexState() : await this.ensureInitializedUnlocked(recoveredOwners);
7217
8119
  const scopedRoots = this.config.scope === "global" ? this.getScopedRoots() : null;
7218
8120
  const branchCatalogKey = this.getBranchCatalogKey();
7219
8121
  const forceScopedReembed = scopedRoots !== null && database.getMetadata(this.getProjectForceReembedMetadataKey()) === "true";
@@ -7223,7 +8125,6 @@ var Indexer = class {
7223
8125
  `${this.indexCompatibility?.reason} Run index_codebase with force=true to rebuild the index.`
7224
8126
  );
7225
8127
  }
7226
- this.acquireIndexingLock();
7227
8128
  this.logger.recordIndexingStart();
7228
8129
  this.logger.info("Starting indexing", { projectRoot: this.projectRoot });
7229
8130
  const startTime = Date.now();
@@ -7274,7 +8175,7 @@ var Indexer = class {
7274
8175
  unchangedFilePaths.add(f.path);
7275
8176
  this.logger.recordCacheHit();
7276
8177
  } else {
7277
- const content = await import_fs7.promises.readFile(f.path, "utf-8");
8178
+ const content = await import_fs8.promises.readFile(f.path, "utf-8");
7278
8179
  changedFiles.push({ path: f.path, content, hash: currentHash });
7279
8180
  this.logger.recordCacheMiss();
7280
8181
  }
@@ -7372,7 +8273,7 @@ var Indexer = class {
7372
8273
  for (const parsed of parsedFiles) {
7373
8274
  currentFilePaths.add(parsed.path);
7374
8275
  if (parsed.chunks.length === 0) {
7375
- const relativePath = path12.relative(this.projectRoot, parsed.path);
8276
+ const relativePath = path13.relative(this.projectRoot, parsed.path);
7376
8277
  stats.parseFailures.push(relativePath);
7377
8278
  }
7378
8279
  let fileChunkCount = 0;
@@ -7572,7 +8473,9 @@ var Indexer = class {
7572
8473
  database.addChunksToBranchBatch(branchCatalogKey, Array.from(currentChunkIds));
7573
8474
  database.clearBranchSymbols(branchCatalogKey);
7574
8475
  database.addSymbolsToBranchBatch(branchCatalogKey, Array.from(allSymbolIds));
7575
- if (backfilledBlameMetadata) {
8476
+ const vectorPath = path13.join(this.indexPath, "vectors");
8477
+ const shouldFingerprintLegacyPair = !store.hasFingerprint() && (0, import_fs8.existsSync)(vectorPath) && (0, import_fs8.existsSync)(`${vectorPath}.meta.json`);
8478
+ if (backfilledBlameMetadata || shouldFingerprintLegacyPair) {
7576
8479
  store.save();
7577
8480
  }
7578
8481
  if (scopedRoots) {
@@ -7593,7 +8496,6 @@ var Indexer = class {
7593
8496
  chunksProcessed: 0,
7594
8497
  totalChunks: 0
7595
8498
  });
7596
- this.releaseIndexingLock();
7597
8499
  return stats;
7598
8500
  }
7599
8501
  if (pendingChunks.length === 0) {
@@ -7602,7 +8504,7 @@ var Indexer = class {
7602
8504
  database.clearBranchSymbols(branchCatalogKey);
7603
8505
  database.addSymbolsToBranchBatch(branchCatalogKey, Array.from(allSymbolIds));
7604
8506
  store.save();
7605
- invertedIndex.save();
8507
+ this.saveInvertedIndex(invertedIndex);
7606
8508
  if (scopedRoots) {
7607
8509
  this.replaceScopedFileHashCache(currentFileHashes, scopedRoots);
7608
8510
  this.clearScopedFailedBatches(scopedRoots);
@@ -7621,7 +8523,6 @@ var Indexer = class {
7621
8523
  chunksProcessed: 0,
7622
8524
  totalChunks: 0
7623
8525
  });
7624
- this.releaseIndexingLock();
7625
8526
  return stats;
7626
8527
  }
7627
8528
  onProgress?.({
@@ -7852,7 +8753,7 @@ var Indexer = class {
7852
8753
  database.clearBranchSymbols(branchCatalogKey);
7853
8754
  database.addSymbolsToBranchBatch(branchCatalogKey, Array.from(allSymbolIds));
7854
8755
  store.save();
7855
- invertedIndex.save();
8756
+ this.saveInvertedIndex(invertedIndex);
7856
8757
  if (scopedRoots) {
7857
8758
  this.replaceScopedFileHashCache(currentFileHashes, scopedRoots);
7858
8759
  } else {
@@ -7904,7 +8805,6 @@ var Indexer = class {
7904
8805
  chunksProcessed: stats.indexedChunks,
7905
8806
  totalChunks: pendingChunks.length
7906
8807
  });
7907
- this.releaseIndexingLock();
7908
8808
  return stats;
7909
8809
  }
7910
8810
  async getQueryEmbedding(query, provider) {
@@ -7970,8 +8870,8 @@ var Indexer = class {
7970
8870
  return intersection / union;
7971
8871
  }
7972
8872
  async search(query, limit, options) {
7973
- const { store, provider, database } = await this.ensureInitialized();
7974
- const compatibility = this.checkCompatibility();
8873
+ const { store, provider, invertedIndex, database, readIssues, compatibility } = await this.ensureInitialized();
8874
+ this.requireReadableComponents(readIssues, "vectors", "database");
7975
8875
  if (!compatibility.compatible) {
7976
8876
  throw new Error(
7977
8877
  `${compatibility.reason ?? "Index is incompatible with current embedding provider."} A possible solution is to run index_codebase with force=true to rebuild the index.`
@@ -7985,6 +8885,7 @@ var Indexer = class {
7985
8885
  const maxResults = limit ?? this.config.search.maxResults;
7986
8886
  const hybridWeight = options?.hybridWeight ?? this.config.search.hybridWeight;
7987
8887
  const fusionStrategy = this.config.search.fusionStrategy;
8888
+ const effectiveHybridWeight = fusionStrategy === "weighted" && readIssues.some((issue) => issue.component === "keyword") ? 0 : hybridWeight;
7988
8889
  const rrfK = this.config.search.rrfK;
7989
8890
  const rerankTopN = this.config.search.rerankTopN;
7990
8891
  const filterByBranch = options?.filterByBranch ?? true;
@@ -7993,7 +8894,7 @@ var Indexer = class {
7993
8894
  this.logger.search("debug", "Starting search", {
7994
8895
  query,
7995
8896
  maxResults,
7996
- hybridWeight,
8897
+ hybridWeight: effectiveHybridWeight,
7997
8898
  fusionStrategy,
7998
8899
  rrfK,
7999
8900
  rerankTopN,
@@ -8007,7 +8908,7 @@ var Indexer = class {
8007
8908
  const semanticResults = store.search(embedding, maxResults * 4);
8008
8909
  const vectorMs = import_perf_hooks.performance.now() - vectorStartTime;
8009
8910
  const keywordStartTime = import_perf_hooks.performance.now();
8010
- const keywordResults = await this.keywordSearch(query, maxResults * 4);
8911
+ const keywordResults = await this.keywordSearch(query, maxResults * 4, store, invertedIndex);
8011
8912
  const keywordMs = import_perf_hooks.performance.now() - keywordStartTime;
8012
8913
  let branchChunkIds = null;
8013
8914
  if (filterByBranch && (this.config.scope === "global" || this.currentBranch !== "default")) {
@@ -8044,7 +8945,7 @@ var Indexer = class {
8044
8945
  rrfK,
8045
8946
  rerankTopN,
8046
8947
  limit: maxResults,
8047
- hybridWeight,
8948
+ hybridWeight: effectiveHybridWeight,
8048
8949
  prioritizeSourcePaths: sourceIntent
8049
8950
  });
8050
8951
  const rerankedCombined = await this.rerankCandidatesWithApi(query, combined, {
@@ -8118,7 +9019,7 @@ var Indexer = class {
8118
9019
  let contextEndLine = r.metadata.endLine;
8119
9020
  if (!metadataOnly && this.config.search.includeContext) {
8120
9021
  try {
8121
- const fileContent = await import_fs7.promises.readFile(
9022
+ const fileContent = await import_fs8.promises.readFile(
8122
9023
  r.metadata.filePath,
8123
9024
  "utf-8"
8124
9025
  );
@@ -8144,8 +9045,7 @@ var Indexer = class {
8144
9045
  })
8145
9046
  );
8146
9047
  }
8147
- async keywordSearch(query, limit) {
8148
- const { store, invertedIndex } = await this.ensureInitialized();
9048
+ async keywordSearch(query, limit, store, invertedIndex) {
8149
9049
  const scores = invertedIndex.search(query);
8150
9050
  if (scores.size === 0) {
8151
9051
  return [];
@@ -8163,24 +9063,54 @@ var Indexer = class {
8163
9063
  return results.slice(0, limit);
8164
9064
  }
8165
9065
  async getStatus() {
8166
- const { store, configuredProviderInfo, database } = await this.ensureInitialized();
9066
+ const { store, configuredProviderInfo, database, readIssues, compatibility } = await this.ensureInitialized();
8167
9067
  const failedBatchesCount = this.getFailedBatchesCount();
9068
+ const vectorCount = store.count();
9069
+ const statusReadIssues = [...readIssues];
9070
+ let startupWarning = "";
9071
+ if (!statusReadIssues.some((issue) => issue.component === "database")) {
9072
+ try {
9073
+ startupWarning = database.getMetadata(STARTUP_WARNING_METADATA_KEY) ?? "";
9074
+ } catch (error) {
9075
+ const message = this.getDatabaseReadIssueMessage();
9076
+ statusReadIssues.push(this.createReadIssue("database", message));
9077
+ if (!this.readIssues.some((issue) => issue.component === "database")) {
9078
+ this.recordReadIssue("database", message, error);
9079
+ }
9080
+ }
9081
+ }
9082
+ const readWarning = statusReadIssues.map((issue) => issue.message).join(" ");
9083
+ const warning = [readWarning, startupWarning].filter((message) => message.length > 0).join(" ");
9084
+ const hasBlockingReadIssue = statusReadIssues.some((issue) => issue.blocking);
8168
9085
  return {
8169
- indexed: store.count() > 0,
8170
- vectorCount: store.count(),
9086
+ indexed: vectorCount > 0 && !hasBlockingReadIssue,
9087
+ vectorCount,
8171
9088
  provider: configuredProviderInfo.provider,
8172
9089
  model: configuredProviderInfo.modelInfo.model,
8173
9090
  indexPath: this.indexPath,
8174
9091
  currentBranch: this.currentBranch,
8175
9092
  baseBranch: this.baseBranch,
8176
- compatibility: this.indexCompatibility,
9093
+ compatibility,
8177
9094
  failedBatchesCount,
8178
9095
  failedBatchesPath: failedBatchesCount > 0 ? this.failedBatchesPath : void 0,
8179
- warning: database.getMetadata(STARTUP_WARNING_METADATA_KEY) ?? void 0
9096
+ warning: warning || void 0
8180
9097
  };
8181
9098
  }
9099
+ async forceIndex(onProgress) {
9100
+ return this.withIndexMutationLease("force-index", async (recoveredOwners) => {
9101
+ await this.ensureInitializedUnlocked(recoveredOwners);
9102
+ await this.clearIndexUnlocked();
9103
+ return this.indexUnlocked(onProgress, [], true);
9104
+ });
9105
+ }
8182
9106
  async clearIndex() {
8183
- const { store, invertedIndex, database } = await this.ensureInitialized();
9107
+ await this.withIndexMutationLease("clear", async (recoveredOwners) => {
9108
+ await this.ensureInitializedUnlocked(recoveredOwners);
9109
+ await this.clearIndexUnlocked();
9110
+ });
9111
+ }
9112
+ async clearIndexUnlocked() {
9113
+ const { store, invertedIndex, database } = this.requireLoadedIndexState();
8184
9114
  if (this.config.scope === "global") {
8185
9115
  store.load();
8186
9116
  invertedIndex.load();
@@ -8207,7 +9137,7 @@ var Indexer = class {
8207
9137
  store.clear();
8208
9138
  store.save();
8209
9139
  invertedIndex.clear();
8210
- invertedIndex.save();
9140
+ this.saveInvertedIndex(invertedIndex);
8211
9141
  this.fileHashCache.clear();
8212
9142
  this.saveFileHashCache();
8213
9143
  database.clearAllIndexedData();
@@ -8231,14 +9161,7 @@ var Indexer = class {
8231
9161
  this.indexCompatibility = compatibility;
8232
9162
  return;
8233
9163
  }
8234
- const localProjectIndexPaths = [path12.join(this.projectRoot, getHostProjectIndexRelativePath(this.host))];
8235
- if (this.host !== "opencode") {
8236
- localProjectIndexPaths.push(path12.join(this.projectRoot, getHostProjectIndexRelativePath("opencode")));
8237
- }
8238
- const isLocalProjectIndex = localProjectIndexPaths.some(
8239
- (localPath) => path12.resolve(this.indexPath) === path12.resolve(localPath)
8240
- );
8241
- if (!isLocalProjectIndex) {
9164
+ if (!this.isLocalProjectIndexPath()) {
8242
9165
  throw new Error(
8243
9166
  "Project-scoped force rebuild is unsafe while using an inherited worktree index. Create a local project config boundary before clearing the index."
8244
9167
  );
@@ -8246,7 +9169,7 @@ var Indexer = class {
8246
9169
  store.clear();
8247
9170
  store.save();
8248
9171
  invertedIndex.clear();
8249
- invertedIndex.save();
9172
+ this.saveInvertedIndex(invertedIndex);
8250
9173
  this.fileHashCache.clear();
8251
9174
  this.saveFileHashCache();
8252
9175
  database.clearAllIndexedData();
@@ -8264,7 +9187,13 @@ var Indexer = class {
8264
9187
  this.indexCompatibility = this.validateIndexCompatibility(this.configuredProviderInfo);
8265
9188
  }
8266
9189
  async healthCheck() {
8267
- const { store, invertedIndex, database } = await this.ensureInitialized();
9190
+ return this.withIndexMutationLease("health-check", async (recoveredOwners) => {
9191
+ await this.ensureInitializedUnlocked(recoveredOwners);
9192
+ return this.healthCheckUnlocked();
9193
+ });
9194
+ }
9195
+ async healthCheckUnlocked() {
9196
+ const { store, invertedIndex, database } = this.requireLoadedIndexState();
8268
9197
  this.logger.gc("info", "Starting health check");
8269
9198
  const allMetadata = store.getAllMetadata();
8270
9199
  const filePathsToChunkKeys = /* @__PURE__ */ new Map();
@@ -8277,7 +9206,7 @@ var Indexer = class {
8277
9206
  const removedChunkKeys = [];
8278
9207
  const chunkKeysByRemovedFile = /* @__PURE__ */ new Map();
8279
9208
  for (const [filePath, chunkKeys] of filePathsToChunkKeys) {
8280
- if (!(0, import_fs7.existsSync)(filePath)) {
9209
+ if (!(0, import_fs8.existsSync)(filePath)) {
8281
9210
  chunkKeysByRemovedFile.set(filePath, chunkKeys);
8282
9211
  for (const key of chunkKeys) {
8283
9212
  removedChunkKeys.push(key);
@@ -8302,7 +9231,7 @@ var Indexer = class {
8302
9231
  const removedCount = removedChunkKeys.length;
8303
9232
  if (removedCount > 0) {
8304
9233
  store.save();
8305
- invertedIndex.save();
9234
+ this.saveInvertedIndex(invertedIndex);
8306
9235
  }
8307
9236
  let gcOrphanEmbeddings;
8308
9237
  let gcOrphanChunks;
@@ -8317,7 +9246,7 @@ var Indexer = class {
8317
9246
  if (!await this.tryResetCorruptedIndex("running index health check", error)) {
8318
9247
  throw error;
8319
9248
  }
8320
- await this.ensureInitialized();
9249
+ await this.initializeUnlocked("writer", [], { skipAutoGc: true });
8321
9250
  return {
8322
9251
  removed: 0,
8323
9252
  filePaths: [],
@@ -8326,7 +9255,7 @@ var Indexer = class {
8326
9255
  gcOrphanSymbols: 0,
8327
9256
  gcOrphanCallEdges: 0,
8328
9257
  resetCorruptedIndex: true,
8329
- warning: this.getCorruptedIndexWarning(path12.join(this.indexPath, "codebase.db"))
9258
+ warning: this.getCorruptedIndexWarning(path13.join(this.indexPath, "codebase.db"))
8330
9259
  };
8331
9260
  }
8332
9261
  this.logger.recordGc(removedCount, gcOrphanChunks, gcOrphanEmbeddings);
@@ -8339,7 +9268,13 @@ var Indexer = class {
8339
9268
  return { removed: removedCount, filePaths: removedFilePaths, gcOrphanEmbeddings, gcOrphanChunks, gcOrphanSymbols, gcOrphanCallEdges };
8340
9269
  }
8341
9270
  async retryFailedBatches() {
8342
- const { store, provider, invertedIndex, database, configuredProviderInfo } = await this.ensureInitialized();
9271
+ return this.withIndexMutationLease("retry-failed-batches", async (recoveredOwners) => {
9272
+ await this.ensureInitializedUnlocked(recoveredOwners);
9273
+ return this.retryFailedBatchesUnlocked();
9274
+ });
9275
+ }
9276
+ async retryFailedBatchesUnlocked() {
9277
+ const { store, provider, invertedIndex, database, configuredProviderInfo } = this.requireLoadedIndexState();
8343
9278
  const maxChunkTokens = getSafeEmbeddingChunkTokenLimit(configuredProviderInfo);
8344
9279
  const providerRateLimits = this.getProviderRateLimits(configuredProviderInfo.provider);
8345
9280
  const roots = this.config.scope === "global" ? this.getScopedRoots() : null;
@@ -8503,7 +9438,7 @@ var Indexer = class {
8503
9438
  }
8504
9439
  if (succeeded > 0) {
8505
9440
  store.save();
8506
- invertedIndex.save();
9441
+ this.saveInvertedIndex(invertedIndex);
8507
9442
  }
8508
9443
  if (roots && succeeded > 0 && persistedStillFailing.length === 0 && this.hasProjectForceReembedPending()) {
8509
9444
  database.deleteMetadata(this.getProjectForceReembedMetadataKey());
@@ -8531,15 +9466,16 @@ var Indexer = class {
8531
9466
  }
8532
9467
  }
8533
9468
  async getDatabaseStats() {
8534
- const { database } = await this.ensureInitialized();
9469
+ const { database, readIssues } = await this.ensureInitialized();
9470
+ this.requireReadableComponents(readIssues, "database");
8535
9471
  return database.getStats();
8536
9472
  }
8537
9473
  getLogger() {
8538
9474
  return this.logger;
8539
9475
  }
8540
9476
  async findSimilar(code, limit = this.config.search.maxResults, options) {
8541
- const { store, provider, database } = await this.ensureInitialized();
8542
- const compatibility = this.checkCompatibility();
9477
+ const { store, provider, database, readIssues, compatibility } = await this.ensureInitialized();
9478
+ this.requireReadableComponents(readIssues, "vectors", "database");
8543
9479
  if (!compatibility.compatible) {
8544
9480
  throw new Error(
8545
9481
  `${compatibility.reason ?? "Index is incompatible with current embedding provider."} Run index_codebase with force=true to rebuild the index.`
@@ -8629,7 +9565,7 @@ var Indexer = class {
8629
9565
  let content = "";
8630
9566
  if (this.config.search.includeContext) {
8631
9567
  try {
8632
- const fileContent = await import_fs7.promises.readFile(
9568
+ const fileContent = await import_fs8.promises.readFile(
8633
9569
  r.metadata.filePath,
8634
9570
  "utf-8"
8635
9571
  );
@@ -8653,7 +9589,8 @@ var Indexer = class {
8653
9589
  );
8654
9590
  }
8655
9591
  async getCallers(targetName, callTypeFilter) {
8656
- const { database } = await this.ensureInitialized();
9592
+ const { database, readIssues } = await this.ensureInitialized();
9593
+ this.requireReadableComponents(readIssues, "database");
8657
9594
  const seen = /* @__PURE__ */ new Set();
8658
9595
  const results = [];
8659
9596
  for (const branchKey of this.getBranchCatalogKeys()) {
@@ -8667,7 +9604,8 @@ var Indexer = class {
8667
9604
  return results;
8668
9605
  }
8669
9606
  async getCallees(symbolId, callTypeFilter) {
8670
- const { database } = await this.ensureInitialized();
9607
+ const { database, readIssues } = await this.ensureInitialized();
9608
+ this.requireReadableComponents(readIssues, "database");
8671
9609
  const seen = /* @__PURE__ */ new Set();
8672
9610
  const results = [];
8673
9611
  for (const branchKey of this.getBranchCatalogKeys()) {
@@ -8681,43 +9619,50 @@ var Indexer = class {
8681
9619
  return results;
8682
9620
  }
8683
9621
  async findCallPath(fromName, toName, maxDepth) {
8684
- const { database } = await this.ensureInitialized();
9622
+ const { database, readIssues } = await this.ensureInitialized();
9623
+ this.requireReadableComponents(readIssues, "database");
8685
9624
  let shortest = [];
8686
9625
  for (const branchKey of this.getBranchCatalogKeys()) {
8687
- const path16 = database.findShortestPath(fromName, toName, branchKey, maxDepth);
8688
- if (path16.length > 0 && (shortest.length === 0 || path16.length < shortest.length)) {
8689
- shortest = path16;
9626
+ const path17 = database.findShortestPath(fromName, toName, branchKey, maxDepth);
9627
+ if (path17.length > 0 && (shortest.length === 0 || path17.length < shortest.length)) {
9628
+ shortest = path17;
8690
9629
  }
8691
9630
  }
8692
9631
  return shortest;
8693
9632
  }
8694
9633
  async getSymbolsForBranch(branch) {
8695
- const { database } = await this.ensureInitialized();
9634
+ const { database, readIssues } = await this.ensureInitialized();
9635
+ this.requireReadableComponents(readIssues, "database");
8696
9636
  const resolvedBranch = branch ?? this.getBranchCatalogKey();
8697
9637
  return database.getSymbolsForBranch(resolvedBranch);
8698
9638
  }
8699
9639
  async getSymbolsForFiles(filePaths, branch) {
8700
- const { database } = await this.ensureInitialized();
9640
+ const { database, readIssues } = await this.ensureInitialized();
9641
+ this.requireReadableComponents(readIssues, "database");
8701
9642
  const resolvedBranch = branch ?? this.getBranchCatalogKey();
8702
9643
  return database.getSymbolsForFiles(filePaths, resolvedBranch);
8703
9644
  }
8704
9645
  async getTransitiveReachability(rootSymbolIds, direction, maxDepth) {
8705
- const { database } = await this.ensureInitialized();
9646
+ const { database, readIssues } = await this.ensureInitialized();
9647
+ this.requireReadableComponents(readIssues, "database");
8706
9648
  const branch = this.getBranchCatalogKey();
8707
9649
  return database.getTransitiveReachability(rootSymbolIds, branch, direction, maxDepth);
8708
9650
  }
8709
9651
  async detectCommunities(branch, symbolIds) {
8710
- const { database } = await this.ensureInitialized();
9652
+ const { database, readIssues } = await this.ensureInitialized();
9653
+ this.requireReadableComponents(readIssues, "database");
8711
9654
  const resolvedBranch = branch ?? this.getBranchCatalogKey();
8712
9655
  return database.detectCommunities(resolvedBranch, symbolIds);
8713
9656
  }
8714
9657
  async computeCentrality(branch) {
8715
- const { database } = await this.ensureInitialized();
9658
+ const { database, readIssues } = await this.ensureInitialized();
9659
+ this.requireReadableComponents(readIssues, "database");
8716
9660
  const resolvedBranch = branch ?? this.getBranchCatalogKey();
8717
9661
  return database.computeCentrality(resolvedBranch);
8718
9662
  }
8719
9663
  async getPrImpact(opts) {
8720
- const { database } = await this.ensureInitialized();
9664
+ const { database, readIssues } = await this.ensureInitialized();
9665
+ this.requireReadableComponents(readIssues, "database");
8721
9666
  const execFileAsync3 = (0, import_util3.promisify)(import_child_process3.execFile);
8722
9667
  const changedFilesResult = await getChangedFiles({
8723
9668
  pr: opts.pr,
@@ -8740,7 +9685,7 @@ var Indexer = class {
8740
9685
  "Run index_codebase first to build the call graph and symbol index for this branch."
8741
9686
  );
8742
9687
  }
8743
- const absoluteChangedFiles = changedFiles.map((f) => path12.resolve(this.projectRoot, f));
9688
+ const absoluteChangedFiles = changedFiles.map((f) => path13.resolve(this.projectRoot, f));
8744
9689
  const directSymbols = database.getSymbolsForFiles(absoluteChangedFiles, branchKey);
8745
9690
  const directIds = directSymbols.map((s) => s.id);
8746
9691
  const direction = opts.direction ?? "both";
@@ -8823,7 +9768,7 @@ var Indexer = class {
8823
9768
  projectRoot: this.projectRoot,
8824
9769
  baseBranch: this.baseBranch
8825
9770
  });
8826
- const otherAbsolute = otherChanged.files.map((f) => path12.resolve(this.projectRoot, f));
9771
+ const otherAbsolute = otherChanged.files.map((f) => path13.resolve(this.projectRoot, f));
8827
9772
  const otherBranchKey = this.getBranchCatalogKeyFor(openPr.headRefName);
8828
9773
  const otherSymbols = database.getSymbolsForFiles(otherAbsolute, otherBranchKey);
8829
9774
  const otherLabels = /* @__PURE__ */ new Set();
@@ -8873,7 +9818,8 @@ var Indexer = class {
8873
9818
  };
8874
9819
  }
8875
9820
  async getVisualizationData(options) {
8876
- const { database, store } = await this.ensureInitialized();
9821
+ const { database, store, readIssues } = await this.ensureInitialized();
9822
+ this.requireReadableComponents(readIssues, "vectors", "database");
8877
9823
  const seenSymbols = /* @__PURE__ */ new Map();
8878
9824
  const seenEdges = /* @__PURE__ */ new Map();
8879
9825
  for (const branchKey of this.getBranchCatalogKeys()) {
@@ -8886,12 +9832,12 @@ var Indexer = class {
8886
9832
  if (meta.filePath) filePaths.add(meta.filePath);
8887
9833
  }
8888
9834
  const directory = options?.directory?.replace(/\/$/, "");
8889
- const absoluteDirectoryFilter = directory ? path12.resolve(this.projectRoot, directory) : void 0;
9835
+ const absoluteDirectoryFilter = directory ? path13.resolve(this.projectRoot, directory) : void 0;
8890
9836
  for (const filePath of filePaths) {
8891
9837
  if (directory) {
8892
- const absoluteFilePath = path12.resolve(filePath);
9838
+ const absoluteFilePath = path13.resolve(filePath);
8893
9839
  const matchesRelative = filePath === directory || filePath.startsWith(directory + "/");
8894
- const matchesProjectRelative = absoluteDirectoryFilter !== void 0 && (absoluteFilePath === absoluteDirectoryFilter || absoluteFilePath.startsWith(absoluteDirectoryFilter + path12.sep));
9840
+ const matchesProjectRelative = absoluteDirectoryFilter !== void 0 && (absoluteFilePath === absoluteDirectoryFilter || absoluteFilePath.startsWith(absoluteDirectoryFilter + path13.sep));
8895
9841
  if (!matchesRelative && !matchesProjectRelative) {
8896
9842
  continue;
8897
9843
  }
@@ -8913,53 +9859,64 @@ var Indexer = class {
8913
9859
  return { symbols: [...seenSymbols.values()], edges: [...seenEdges.values()] };
8914
9860
  }
8915
9861
  async close() {
8916
- await this.database?.close();
9862
+ this.database?.close();
9863
+ for (const database of this.retiredDatabases) {
9864
+ database.close();
9865
+ }
9866
+ this.retiredDatabases = [];
8917
9867
  this.database = null;
8918
9868
  this.store = null;
8919
9869
  this.invertedIndex = null;
8920
9870
  this.provider = null;
8921
9871
  this.reranker = null;
9872
+ this.configuredProviderInfo = null;
9873
+ this.indexCompatibility = null;
9874
+ this.initializationMode = "none";
9875
+ this.readIssues = [];
9876
+ this.readerArtifactFingerprint = null;
9877
+ this.writerArtifactFingerprint = null;
9878
+ this.readerArtifactRetryAfter.clear();
8922
9879
  }
8923
9880
  };
8924
9881
 
8925
9882
  // src/tools/knowledge-base-paths.ts
8926
- var path13 = __toESM(require("path"), 1);
9883
+ var path14 = __toESM(require("path"), 1);
8927
9884
  function resolveConfigPathValue(value, baseDir) {
8928
9885
  const trimmed = value.trim();
8929
9886
  if (!trimmed) {
8930
9887
  return trimmed;
8931
9888
  }
8932
- const absolutePath = path13.isAbsolute(trimmed) ? trimmed : path13.resolve(baseDir, trimmed);
8933
- return path13.normalize(absolutePath);
9889
+ const absolutePath = path14.isAbsolute(trimmed) ? trimmed : path14.resolve(baseDir, trimmed);
9890
+ return path14.normalize(absolutePath);
8934
9891
  }
8935
9892
  function serializeConfigPathValue(value, baseDir) {
8936
9893
  const trimmed = value.trim();
8937
9894
  if (!trimmed) {
8938
9895
  return trimmed;
8939
9896
  }
8940
- if (!path13.isAbsolute(trimmed)) {
8941
- return normalizePathSeparators(path13.normalize(trimmed));
9897
+ if (!path14.isAbsolute(trimmed)) {
9898
+ return normalizePathSeparators(path14.normalize(trimmed));
8942
9899
  }
8943
- const relativePath = path13.relative(baseDir, trimmed);
8944
- if (!relativePath || !relativePath.startsWith("..") && !path13.isAbsolute(relativePath)) {
8945
- return normalizePathSeparators(path13.normalize(relativePath || "."));
9900
+ const relativePath = path14.relative(baseDir, trimmed);
9901
+ if (!relativePath || !relativePath.startsWith("..") && !path14.isAbsolute(relativePath)) {
9902
+ return normalizePathSeparators(path14.normalize(relativePath || "."));
8946
9903
  }
8947
- return path13.normalize(trimmed);
9904
+ return path14.normalize(trimmed);
8948
9905
  }
8949
9906
  function resolveKnowledgeBasePath(value, projectRoot3) {
8950
- return path13.isAbsolute(value) ? value : path13.resolve(projectRoot3, value);
9907
+ return path14.isAbsolute(value) ? value : path14.resolve(projectRoot3, value);
8951
9908
  }
8952
9909
  function normalizeKnowledgeBasePath2(value, projectRoot3) {
8953
- return path13.normalize(resolveKnowledgeBasePath(value, projectRoot3));
9910
+ return path14.normalize(resolveKnowledgeBasePath(value, projectRoot3));
8954
9911
  }
8955
9912
  function hasMatchingKnowledgeBasePath(knowledgeBases, inputPath, projectRoot3) {
8956
- const normalizedInput = path13.normalize(inputPath);
9913
+ const normalizedInput = path14.normalize(inputPath);
8957
9914
  return knowledgeBases.some((kb) => normalizeKnowledgeBasePath2(kb, projectRoot3) === normalizedInput);
8958
9915
  }
8959
9916
  function findKnowledgeBasePathIndex(knowledgeBases, inputPath, projectRoot3) {
8960
- const normalizedInput = path13.normalize(inputPath);
9917
+ const normalizedInput = path14.normalize(inputPath);
8961
9918
  return knowledgeBases.findIndex(
8962
- (kb) => path13.normalize(kb) === normalizedInput || normalizeKnowledgeBasePath2(kb, projectRoot3) === normalizedInput
9919
+ (kb) => path14.normalize(kb) === normalizedInput || normalizeKnowledgeBasePath2(kb, projectRoot3) === normalizedInput
8963
9920
  );
8964
9921
  }
8965
9922
 
@@ -9059,6 +10016,10 @@ function formatStatus(status) {
9059
10016
  lines.push(`Failed batches: ${status.failedBatchesPath}`);
9060
10017
  }
9061
10018
  }
10019
+ if (status.warning) {
10020
+ lines.push("");
10021
+ lines.push(`INDEX WARNING: ${status.warning}`);
10022
+ }
9062
10023
  if (status.compatibility && !status.compatibility.compatible) {
9063
10024
  lines.push("");
9064
10025
  lines.push(`COMPATIBILITY WARNING: ${status.compatibility.reason}`);
@@ -9153,16 +10114,16 @@ function formatCallGraphCallees(symbolId, callees, relationshipType) {
9153
10114
  return `[${index + 1}] \u2192 ${edge.targetName} (${edge.callType})${confidence} at line ${edge.line}${edge.isResolved ? ` [resolved: ${edge.toSymbolId}]` : " [unresolved]"}`;
9154
10115
  }).join("\n");
9155
10116
  }
9156
- function formatCallGraphPath(from, to, path16) {
9157
- if (path16.length === 0) {
10117
+ function formatCallGraphPath(from, to, path17) {
10118
+ if (path17.length === 0) {
9158
10119
  return `No path found between "${from}" and "${to}". They may be in disconnected components, or the call graph index needs updating.`;
9159
10120
  }
9160
- const formatted = path16.map((hop, index) => {
10121
+ const formatted = path17.map((hop, index) => {
9161
10122
  const prefix = index === 0 ? "[start]" : `--${hop.callType}-->`;
9162
10123
  const location = hop.filePath ? ` (${hop.filePath}:${hop.line})` : "";
9163
10124
  return `${prefix} ${hop.symbolName}${location}`;
9164
10125
  });
9165
- return `Path (${path16.length} hops):
10126
+ return `Path (${path17.length} hops):
9166
10127
  ${formatted.join("\n")}`;
9167
10128
  }
9168
10129
  function formatResultHeader(result, index) {
@@ -9202,8 +10163,8 @@ ${truncateContent(r.content)}
9202
10163
  }
9203
10164
 
9204
10165
  // src/tools/config-state.ts
9205
- var import_fs8 = require("fs");
9206
- var path14 = __toESM(require("path"), 1);
10166
+ var import_fs9 = require("fs");
10167
+ var path15 = __toESM(require("path"), 1);
9207
10168
  function normalizeKnowledgeBasePaths(config, projectRoot3) {
9208
10169
  const normalized = { ...config };
9209
10170
  if (Array.isArray(normalized.knowledgeBases)) {
@@ -9230,10 +10191,10 @@ function loadEditableConfig(projectRoot3, host = "opencode") {
9230
10191
  }
9231
10192
  function saveConfig(projectRoot3, config, host = "opencode") {
9232
10193
  const configPath = getConfigPath(projectRoot3, host);
9233
- const configDir = path14.dirname(configPath);
9234
- const configBaseDir = path14.dirname(configDir);
9235
- if (!(0, import_fs8.existsSync)(configDir)) {
9236
- (0, import_fs8.mkdirSync)(configDir, { recursive: true });
10194
+ const configDir = path15.dirname(configPath);
10195
+ const configBaseDir = path15.dirname(configDir);
10196
+ if (!(0, import_fs9.existsSync)(configDir)) {
10197
+ (0, import_fs9.mkdirSync)(configDir, { recursive: true });
9237
10198
  }
9238
10199
  const serializableConfig = { ...config };
9239
10200
  if (Array.isArray(serializableConfig.knowledgeBases)) {
@@ -9241,13 +10202,31 @@ function saveConfig(projectRoot3, config, host = "opencode") {
9241
10202
  (kb) => serializeConfigPathValue(kb, configBaseDir)
9242
10203
  );
9243
10204
  }
9244
- (0, import_fs8.writeFileSync)(configPath, JSON.stringify(serializableConfig, null, 2) + "\n", "utf-8");
10205
+ (0, import_fs9.writeFileSync)(configPath, JSON.stringify(serializableConfig, null, 2) + "\n", "utf-8");
9245
10206
  }
9246
10207
 
9247
10208
  // src/tools/operations.ts
9248
10209
  var indexerCache = /* @__PURE__ */ new Map();
9249
10210
  var configCache = /* @__PURE__ */ new Map();
9250
10211
  var defaultProjectRoots = /* @__PURE__ */ new Map();
10212
+ function getIndexBusyResult(error) {
10213
+ if (!isIndexLockContentionError(error)) return null;
10214
+ const owner = error.owner;
10215
+ const ownerText = owner ? `PID ${owner.pid}, operation ${owner.operation}, since ${owner.startedAt}` : "unreadable owner";
10216
+ if (error.reason === "legacy-lock") {
10217
+ return {
10218
+ kind: "busy",
10219
+ text: `INDEX_BUSY: legacy lock format detected (${ownerText}). Verify the PID and remove this lock manually only if it is stale.`
10220
+ };
10221
+ }
10222
+ if (error.reason === "unknown-owner") {
10223
+ return {
10224
+ kind: "busy",
10225
+ text: `INDEX_BUSY: unreadable or remote lock owner (${ownerText}). Automatic recovery was refused; manual verification is required.`
10226
+ };
10227
+ }
10228
+ return { kind: "busy", text: `INDEX_BUSY: another index operation is already in progress (${ownerText}).` };
10229
+ }
9251
10230
  function getProjectRoot(projectRoot3, host) {
9252
10231
  if (projectRoot3) {
9253
10232
  return projectRoot3;
@@ -9285,8 +10264,8 @@ function refreshIndexerForDirectory(projectRoot3, host = "opencode", config = pa
9285
10264
  }
9286
10265
  function shouldForceLocalizeProjectIndex(projectRoot3, host = "opencode") {
9287
10266
  const root = getProjectRoot(projectRoot3, host);
9288
- const localIndexPath = path15.join(root, getHostProjectIndexRelativePath(host));
9289
- if ((0, import_fs9.existsSync)(localIndexPath)) {
10267
+ const localIndexPath = path16.join(root, getHostProjectIndexRelativePath(host));
10268
+ if ((0, import_fs10.existsSync)(localIndexPath)) {
9290
10269
  return false;
9291
10270
  }
9292
10271
  const inheritedIndexPath = resolveWorktreeFallbackProjectIndexPath(root, host);
@@ -9342,35 +10321,46 @@ async function getCallGraphPath(projectRoot3, host, from, to, maxDepth) {
9342
10321
  async function runIndexCodebase(projectRoot3, host, args, onProgress) {
9343
10322
  const root = getProjectRoot(projectRoot3, host);
9344
10323
  const key = getIndexerCacheKey(root, host);
9345
- const cachedConfig = configCache.get(key);
9346
10324
  let indexer = getIndexerForProject(root, host);
9347
- if (args.estimateOnly) {
9348
- return { kind: "estimate", estimate: await indexer.estimateCost() };
9349
- }
9350
- if (args.force) {
9351
- if (shouldForceLocalizeProjectIndex(root, host)) {
9352
- materializeLocalProjectConfig(root, loadProjectConfigLayer(root, host), host);
9353
- refreshIndexerForDirectory(root, host, cachedConfig);
9354
- indexer = getIndexerForProject(root, host);
9355
- }
9356
- await indexer.clearIndex();
9357
- refreshIndexerForDirectory(root, host, cachedConfig);
9358
- indexer = getIndexerForProject(root, host);
9359
- }
9360
- const stats = await indexer.index((progress) => {
9361
- if (onProgress) {
9362
- return onProgress(formatProgressTitle(progress), {
9363
- phase: progress.phase,
9364
- filesProcessed: progress.filesProcessed,
9365
- totalFiles: progress.totalFiles,
9366
- chunksProcessed: progress.chunksProcessed,
9367
- totalChunks: progress.totalChunks,
9368
- percentage: calculatePercentage(progress)
10325
+ const runtimeConfig = configCache.get(key);
10326
+ try {
10327
+ if (args.estimateOnly) {
10328
+ return { kind: "estimate", estimate: await indexer.estimateCost() };
10329
+ }
10330
+ const runIndex = async (target) => {
10331
+ const operation = args.force ? target.forceIndex.bind(target) : target.index.bind(target);
10332
+ return operation((progress) => {
10333
+ if (onProgress) {
10334
+ return onProgress(formatProgressTitle(progress), {
10335
+ phase: progress.phase,
10336
+ filesProcessed: progress.filesProcessed,
10337
+ totalFiles: progress.totalFiles,
10338
+ chunksProcessed: progress.chunksProcessed,
10339
+ totalChunks: progress.totalChunks,
10340
+ percentage: calculatePercentage(progress)
10341
+ });
10342
+ }
10343
+ return Promise.resolve();
9369
10344
  });
10345
+ };
10346
+ let stats;
10347
+ if (args.force && shouldForceLocalizeProjectIndex(root, host)) {
10348
+ const inheritedIndexPath = resolveProjectIndexPath(root, runtimeConfig.scope, host);
10349
+ stats = await withIndexLock(inheritedIndexPath, "force-index", async () => {
10350
+ materializeLocalProjectConfig(root, loadProjectConfigLayer(root, host), host);
10351
+ refreshIndexerForDirectory(root, host, runtimeConfig);
10352
+ indexer = getIndexerForProject(root, host);
10353
+ return runIndex(indexer);
10354
+ }, { completeRecoveries: false });
10355
+ } else {
10356
+ stats = await runIndex(indexer);
9370
10357
  }
9371
- return Promise.resolve();
9372
- });
9373
- return { kind: "stats", stats };
10358
+ return { kind: "stats", stats };
10359
+ } catch (error) {
10360
+ const busyResult = getIndexBusyResult(error);
10361
+ if (!busyResult) throw error;
10362
+ return busyResult;
10363
+ }
9374
10364
  }
9375
10365
  async function getIndexStatus(projectRoot3, host) {
9376
10366
  const indexer = getIndexerForProject(projectRoot3, host);
@@ -9380,6 +10370,15 @@ async function getIndexHealthCheck(projectRoot3, host) {
9380
10370
  const indexer = getIndexerForProject(projectRoot3, host);
9381
10371
  return indexer.healthCheck();
9382
10372
  }
10373
+ async function runIndexHealthCheck(projectRoot3, host) {
10374
+ try {
10375
+ return { kind: "health", health: await getIndexHealthCheck(projectRoot3, host) };
10376
+ } catch (error) {
10377
+ const busyResult = getIndexBusyResult(error);
10378
+ if (!busyResult) throw error;
10379
+ return busyResult;
10380
+ }
10381
+ }
9383
10382
  async function getPrImpact(projectRoot3, host, params) {
9384
10383
  const indexer = getIndexerForProject(projectRoot3, host);
9385
10384
  return indexer.getPrImpact({
@@ -9446,15 +10445,15 @@ async function getIndexLogs(projectRoot3, host, args) {
9446
10445
  function addKnowledgeBase(projectRoot3, host, knowledgeBasePath) {
9447
10446
  const root = getProjectRoot(projectRoot3, host);
9448
10447
  const inputPath = knowledgeBasePath.trim();
9449
- const normalizedPath = path15.resolve(
9450
- path15.isAbsolute(inputPath) ? inputPath : resolveKnowledgeBasePath(inputPath, root)
10448
+ const normalizedPath = path16.resolve(
10449
+ path16.isAbsolute(inputPath) ? inputPath : resolveKnowledgeBasePath(inputPath, root)
9451
10450
  );
9452
- if (!(0, import_fs9.existsSync)(normalizedPath)) {
10451
+ if (!(0, import_fs10.existsSync)(normalizedPath)) {
9453
10452
  return `Error: Directory does not exist: ${normalizedPath}`;
9454
10453
  }
9455
10454
  let realPath;
9456
10455
  try {
9457
- realPath = (0, import_fs9.realpathSync)(normalizedPath);
10456
+ realPath = (0, import_fs10.realpathSync)(normalizedPath);
9458
10457
  } catch {
9459
10458
  return `Error: Cannot resolve path: ${normalizedPath}`;
9460
10459
  }
@@ -9483,13 +10482,13 @@ function addKnowledgeBase(projectRoot3, host, knowledgeBasePath) {
9483
10482
  }
9484
10483
  }
9485
10484
  for (const dotDir of sensitiveDotDirs) {
9486
- const sensitiveDir = path15.join(homeDir, dotDir);
10485
+ const sensitiveDir = path16.join(homeDir, dotDir);
9487
10486
  if (sensitiveDir && (realPath === sensitiveDir || realPath.startsWith(`${sensitiveDir}/`))) {
9488
10487
  return `Error: Adding sensitive directory as knowledge base is not allowed: ${normalizedPath}`;
9489
10488
  }
9490
10489
  }
9491
10490
  try {
9492
- const stat = (0, import_fs9.statSync)(normalizedPath);
10491
+ const stat = (0, import_fs10.statSync)(normalizedPath);
9493
10492
  if (!stat.isDirectory()) {
9494
10493
  return `Error: Path is not a directory: ${normalizedPath}`;
9495
10494
  }
@@ -9529,7 +10528,7 @@ function listKnowledgeBases(projectRoot3, host) {
9529
10528
  for (let i = 0; i < knowledgeBases.length; i++) {
9530
10529
  const kb = knowledgeBases[i];
9531
10530
  const resolvedPath = resolveKnowledgeBasePath(kb, root);
9532
- const exists = (0, import_fs9.existsSync)(resolvedPath);
10531
+ const exists = (0, import_fs10.existsSync)(resolvedPath);
9533
10532
  result += `[${i + 1}] ${kb}
9534
10533
  `;
9535
10534
  result += ` Resolved: ${resolvedPath}
@@ -9538,7 +10537,7 @@ function listKnowledgeBases(projectRoot3, host) {
9538
10537
  `;
9539
10538
  if (exists) {
9540
10539
  try {
9541
- const stat = (0, import_fs9.statSync)(resolvedPath);
10540
+ const stat = (0, import_fs10.statSync)(resolvedPath);
9542
10541
  result += ` Type: ${stat.isDirectory() ? "Directory" : "File"}
9543
10542
  `;
9544
10543
  } catch {
@@ -9546,7 +10545,7 @@ function listKnowledgeBases(projectRoot3, host) {
9546
10545
  }
9547
10546
  result += "\n";
9548
10547
  }
9549
- const hasHostConfig = (0, import_fs9.existsSync)(path15.join(root, getHostProjectConfigRelativePath(host)));
10548
+ const hasHostConfig = (0, import_fs10.existsSync)(path16.join(root, getHostProjectConfigRelativePath(host)));
9550
10549
  if (hasHostConfig) {
9551
10550
  result += `
9552
10551
  Config sources: 1 file(s).`;
@@ -9734,7 +10733,9 @@ function codebaseIndexPiExtension(pi) {
9734
10733
  }),
9735
10734
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
9736
10735
  const result = await runIndexCodebase(projectRoot2(ctx), HOST2, params);
9737
- return result.kind === "estimate" ? text2(formatCostEstimate(result.estimate), result.estimate) : text2(formatIndexStats(result.stats, params.verbose ?? false), result.stats);
10736
+ if (result.kind === "estimate") return text2(formatCostEstimate(result.estimate), result.estimate);
10737
+ if (result.kind === "busy") return text2(result.text, { code: "INDEX_BUSY" });
10738
+ return text2(formatIndexStats(result.stats, params.verbose ?? false), result.stats);
9738
10739
  }
9739
10740
  });
9740
10741
  pi.registerTool({
@@ -9753,8 +10754,9 @@ function codebaseIndexPiExtension(pi) {
9753
10754
  description: "Garbage collect orphaned embeddings/chunks and report health.",
9754
10755
  parameters: import_typebox2.Type.Object({}),
9755
10756
  async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
9756
- const result = await getIndexHealthCheck(projectRoot2(ctx), HOST2);
9757
- return text2(formatHealthCheck(result), result);
10757
+ const result = await runIndexHealthCheck(projectRoot2(ctx), HOST2);
10758
+ if (result.kind === "busy") return text2(result.text, { code: "INDEX_BUSY" });
10759
+ return text2(formatHealthCheck(result.health), result.health);
9758
10760
  }
9759
10761
  });
9760
10762
  pi.registerTool({