opencode-codebase-index 0.14.0 → 0.16.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,
@@ -8001,13 +8902,22 @@ var Indexer = class {
8001
8902
  });
8002
8903
  const embeddingStartTime = import_perf_hooks.performance.now();
8003
8904
  const embeddingQuery = stripFilePathHint(query);
8004
- const embedding = await this.getQueryEmbedding(embeddingQuery, provider);
8905
+ let embedding;
8906
+ try {
8907
+ embedding = await this.getQueryEmbedding(embeddingQuery, provider);
8908
+ } catch (error) {
8909
+ this.logger.warn("Query embedding failed; falling back to keyword-only search", {
8910
+ query,
8911
+ error: getErrorMessage2(error),
8912
+ action: "Check the embedding provider configuration and retry search after restoring provider health."
8913
+ });
8914
+ }
8005
8915
  const embeddingMs = import_perf_hooks.performance.now() - embeddingStartTime;
8006
8916
  const vectorStartTime = import_perf_hooks.performance.now();
8007
- const semanticResults = store.search(embedding, maxResults * 4);
8917
+ const semanticResults = embedding ? store.search(embedding, maxResults * 4) : [];
8008
8918
  const vectorMs = import_perf_hooks.performance.now() - vectorStartTime;
8009
8919
  const keywordStartTime = import_perf_hooks.performance.now();
8010
- const keywordResults = await this.keywordSearch(query, maxResults * 4);
8920
+ const keywordResults = await this.keywordSearch(query, maxResults * 4, store, invertedIndex);
8011
8921
  const keywordMs = import_perf_hooks.performance.now() - keywordStartTime;
8012
8922
  let branchChunkIds = null;
8013
8923
  if (filterByBranch && (this.config.scope === "global" || this.currentBranch !== "default")) {
@@ -8039,12 +8949,13 @@ var Indexer = class {
8039
8949
  });
8040
8950
  }
8041
8951
  const fusionStartTime = import_perf_hooks.performance.now();
8952
+ const rankingHybridWeight = embedding === void 0 && fusionStrategy === "weighted" ? 1 : effectiveHybridWeight;
8042
8953
  const combined = rankHybridResults(query, semanticCandidates, keywordCandidates, {
8043
8954
  fusionStrategy,
8044
8955
  rrfK,
8045
8956
  rerankTopN,
8046
8957
  limit: maxResults,
8047
- hybridWeight,
8958
+ hybridWeight: rankingHybridWeight,
8048
8959
  prioritizeSourcePaths: sourceIntent
8049
8960
  });
8050
8961
  const rerankedCombined = await this.rerankCandidatesWithApi(query, combined, {
@@ -8118,7 +9029,7 @@ var Indexer = class {
8118
9029
  let contextEndLine = r.metadata.endLine;
8119
9030
  if (!metadataOnly && this.config.search.includeContext) {
8120
9031
  try {
8121
- const fileContent = await import_fs7.promises.readFile(
9032
+ const fileContent = await import_fs8.promises.readFile(
8122
9033
  r.metadata.filePath,
8123
9034
  "utf-8"
8124
9035
  );
@@ -8144,8 +9055,7 @@ var Indexer = class {
8144
9055
  })
8145
9056
  );
8146
9057
  }
8147
- async keywordSearch(query, limit) {
8148
- const { store, invertedIndex } = await this.ensureInitialized();
9058
+ async keywordSearch(query, limit, store, invertedIndex) {
8149
9059
  const scores = invertedIndex.search(query);
8150
9060
  if (scores.size === 0) {
8151
9061
  return [];
@@ -8163,24 +9073,54 @@ var Indexer = class {
8163
9073
  return results.slice(0, limit);
8164
9074
  }
8165
9075
  async getStatus() {
8166
- const { store, configuredProviderInfo, database } = await this.ensureInitialized();
9076
+ const { store, configuredProviderInfo, database, readIssues, compatibility } = await this.ensureInitialized();
8167
9077
  const failedBatchesCount = this.getFailedBatchesCount();
9078
+ const vectorCount = store.count();
9079
+ const statusReadIssues = [...readIssues];
9080
+ let startupWarning = "";
9081
+ if (!statusReadIssues.some((issue) => issue.component === "database")) {
9082
+ try {
9083
+ startupWarning = database.getMetadata(STARTUP_WARNING_METADATA_KEY) ?? "";
9084
+ } catch (error) {
9085
+ const message = this.getDatabaseReadIssueMessage();
9086
+ statusReadIssues.push(this.createReadIssue("database", message));
9087
+ if (!this.readIssues.some((issue) => issue.component === "database")) {
9088
+ this.recordReadIssue("database", message, error);
9089
+ }
9090
+ }
9091
+ }
9092
+ const readWarning = statusReadIssues.map((issue) => issue.message).join(" ");
9093
+ const warning = [readWarning, startupWarning].filter((message) => message.length > 0).join(" ");
9094
+ const hasBlockingReadIssue = statusReadIssues.some((issue) => issue.blocking);
8168
9095
  return {
8169
- indexed: store.count() > 0,
8170
- vectorCount: store.count(),
9096
+ indexed: vectorCount > 0 && !hasBlockingReadIssue,
9097
+ vectorCount,
8171
9098
  provider: configuredProviderInfo.provider,
8172
9099
  model: configuredProviderInfo.modelInfo.model,
8173
9100
  indexPath: this.indexPath,
8174
9101
  currentBranch: this.currentBranch,
8175
9102
  baseBranch: this.baseBranch,
8176
- compatibility: this.indexCompatibility,
9103
+ compatibility,
8177
9104
  failedBatchesCount,
8178
9105
  failedBatchesPath: failedBatchesCount > 0 ? this.failedBatchesPath : void 0,
8179
- warning: database.getMetadata(STARTUP_WARNING_METADATA_KEY) ?? void 0
9106
+ warning: warning || void 0
8180
9107
  };
8181
9108
  }
9109
+ async forceIndex(onProgress) {
9110
+ return this.withIndexMutationLease("force-index", async (recoveredOwners) => {
9111
+ await this.ensureInitializedUnlocked(recoveredOwners);
9112
+ await this.clearIndexUnlocked();
9113
+ return this.indexUnlocked(onProgress, [], true);
9114
+ });
9115
+ }
8182
9116
  async clearIndex() {
8183
- const { store, invertedIndex, database } = await this.ensureInitialized();
9117
+ await this.withIndexMutationLease("clear", async (recoveredOwners) => {
9118
+ await this.ensureInitializedUnlocked(recoveredOwners);
9119
+ await this.clearIndexUnlocked();
9120
+ });
9121
+ }
9122
+ async clearIndexUnlocked() {
9123
+ const { store, invertedIndex, database } = this.requireLoadedIndexState();
8184
9124
  if (this.config.scope === "global") {
8185
9125
  store.load();
8186
9126
  invertedIndex.load();
@@ -8207,7 +9147,7 @@ var Indexer = class {
8207
9147
  store.clear();
8208
9148
  store.save();
8209
9149
  invertedIndex.clear();
8210
- invertedIndex.save();
9150
+ this.saveInvertedIndex(invertedIndex);
8211
9151
  this.fileHashCache.clear();
8212
9152
  this.saveFileHashCache();
8213
9153
  database.clearAllIndexedData();
@@ -8231,14 +9171,7 @@ var Indexer = class {
8231
9171
  this.indexCompatibility = compatibility;
8232
9172
  return;
8233
9173
  }
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) {
9174
+ if (!this.isLocalProjectIndexPath()) {
8242
9175
  throw new Error(
8243
9176
  "Project-scoped force rebuild is unsafe while using an inherited worktree index. Create a local project config boundary before clearing the index."
8244
9177
  );
@@ -8246,7 +9179,7 @@ var Indexer = class {
8246
9179
  store.clear();
8247
9180
  store.save();
8248
9181
  invertedIndex.clear();
8249
- invertedIndex.save();
9182
+ this.saveInvertedIndex(invertedIndex);
8250
9183
  this.fileHashCache.clear();
8251
9184
  this.saveFileHashCache();
8252
9185
  database.clearAllIndexedData();
@@ -8264,7 +9197,13 @@ var Indexer = class {
8264
9197
  this.indexCompatibility = this.validateIndexCompatibility(this.configuredProviderInfo);
8265
9198
  }
8266
9199
  async healthCheck() {
8267
- const { store, invertedIndex, database } = await this.ensureInitialized();
9200
+ return this.withIndexMutationLease("health-check", async (recoveredOwners) => {
9201
+ await this.ensureInitializedUnlocked(recoveredOwners);
9202
+ return this.healthCheckUnlocked();
9203
+ });
9204
+ }
9205
+ async healthCheckUnlocked() {
9206
+ const { store, invertedIndex, database } = this.requireLoadedIndexState();
8268
9207
  this.logger.gc("info", "Starting health check");
8269
9208
  const allMetadata = store.getAllMetadata();
8270
9209
  const filePathsToChunkKeys = /* @__PURE__ */ new Map();
@@ -8277,7 +9216,7 @@ var Indexer = class {
8277
9216
  const removedChunkKeys = [];
8278
9217
  const chunkKeysByRemovedFile = /* @__PURE__ */ new Map();
8279
9218
  for (const [filePath, chunkKeys] of filePathsToChunkKeys) {
8280
- if (!(0, import_fs7.existsSync)(filePath)) {
9219
+ if (!(0, import_fs8.existsSync)(filePath)) {
8281
9220
  chunkKeysByRemovedFile.set(filePath, chunkKeys);
8282
9221
  for (const key of chunkKeys) {
8283
9222
  removedChunkKeys.push(key);
@@ -8302,7 +9241,7 @@ var Indexer = class {
8302
9241
  const removedCount = removedChunkKeys.length;
8303
9242
  if (removedCount > 0) {
8304
9243
  store.save();
8305
- invertedIndex.save();
9244
+ this.saveInvertedIndex(invertedIndex);
8306
9245
  }
8307
9246
  let gcOrphanEmbeddings;
8308
9247
  let gcOrphanChunks;
@@ -8317,7 +9256,7 @@ var Indexer = class {
8317
9256
  if (!await this.tryResetCorruptedIndex("running index health check", error)) {
8318
9257
  throw error;
8319
9258
  }
8320
- await this.ensureInitialized();
9259
+ await this.initializeUnlocked("writer", [], { skipAutoGc: true });
8321
9260
  return {
8322
9261
  removed: 0,
8323
9262
  filePaths: [],
@@ -8326,7 +9265,7 @@ var Indexer = class {
8326
9265
  gcOrphanSymbols: 0,
8327
9266
  gcOrphanCallEdges: 0,
8328
9267
  resetCorruptedIndex: true,
8329
- warning: this.getCorruptedIndexWarning(path12.join(this.indexPath, "codebase.db"))
9268
+ warning: this.getCorruptedIndexWarning(path13.join(this.indexPath, "codebase.db"))
8330
9269
  };
8331
9270
  }
8332
9271
  this.logger.recordGc(removedCount, gcOrphanChunks, gcOrphanEmbeddings);
@@ -8339,7 +9278,13 @@ var Indexer = class {
8339
9278
  return { removed: removedCount, filePaths: removedFilePaths, gcOrphanEmbeddings, gcOrphanChunks, gcOrphanSymbols, gcOrphanCallEdges };
8340
9279
  }
8341
9280
  async retryFailedBatches() {
8342
- const { store, provider, invertedIndex, database, configuredProviderInfo } = await this.ensureInitialized();
9281
+ return this.withIndexMutationLease("retry-failed-batches", async (recoveredOwners) => {
9282
+ await this.ensureInitializedUnlocked(recoveredOwners);
9283
+ return this.retryFailedBatchesUnlocked();
9284
+ });
9285
+ }
9286
+ async retryFailedBatchesUnlocked() {
9287
+ const { store, provider, invertedIndex, database, configuredProviderInfo } = this.requireLoadedIndexState();
8343
9288
  const maxChunkTokens = getSafeEmbeddingChunkTokenLimit(configuredProviderInfo);
8344
9289
  const providerRateLimits = this.getProviderRateLimits(configuredProviderInfo.provider);
8345
9290
  const roots = this.config.scope === "global" ? this.getScopedRoots() : null;
@@ -8503,7 +9448,7 @@ var Indexer = class {
8503
9448
  }
8504
9449
  if (succeeded > 0) {
8505
9450
  store.save();
8506
- invertedIndex.save();
9451
+ this.saveInvertedIndex(invertedIndex);
8507
9452
  }
8508
9453
  if (roots && succeeded > 0 && persistedStillFailing.length === 0 && this.hasProjectForceReembedPending()) {
8509
9454
  database.deleteMetadata(this.getProjectForceReembedMetadataKey());
@@ -8531,15 +9476,16 @@ var Indexer = class {
8531
9476
  }
8532
9477
  }
8533
9478
  async getDatabaseStats() {
8534
- const { database } = await this.ensureInitialized();
9479
+ const { database, readIssues } = await this.ensureInitialized();
9480
+ this.requireReadableComponents(readIssues, "database");
8535
9481
  return database.getStats();
8536
9482
  }
8537
9483
  getLogger() {
8538
9484
  return this.logger;
8539
9485
  }
8540
9486
  async findSimilar(code, limit = this.config.search.maxResults, options) {
8541
- const { store, provider, database } = await this.ensureInitialized();
8542
- const compatibility = this.checkCompatibility();
9487
+ const { store, provider, database, readIssues, compatibility } = await this.ensureInitialized();
9488
+ this.requireReadableComponents(readIssues, "vectors", "database");
8543
9489
  if (!compatibility.compatible) {
8544
9490
  throw new Error(
8545
9491
  `${compatibility.reason ?? "Index is incompatible with current embedding provider."} Run index_codebase with force=true to rebuild the index.`
@@ -8629,7 +9575,7 @@ var Indexer = class {
8629
9575
  let content = "";
8630
9576
  if (this.config.search.includeContext) {
8631
9577
  try {
8632
- const fileContent = await import_fs7.promises.readFile(
9578
+ const fileContent = await import_fs8.promises.readFile(
8633
9579
  r.metadata.filePath,
8634
9580
  "utf-8"
8635
9581
  );
@@ -8653,7 +9599,8 @@ var Indexer = class {
8653
9599
  );
8654
9600
  }
8655
9601
  async getCallers(targetName, callTypeFilter) {
8656
- const { database } = await this.ensureInitialized();
9602
+ const { database, readIssues } = await this.ensureInitialized();
9603
+ this.requireReadableComponents(readIssues, "database");
8657
9604
  const seen = /* @__PURE__ */ new Set();
8658
9605
  const results = [];
8659
9606
  for (const branchKey of this.getBranchCatalogKeys()) {
@@ -8667,7 +9614,8 @@ var Indexer = class {
8667
9614
  return results;
8668
9615
  }
8669
9616
  async getCallees(symbolId, callTypeFilter) {
8670
- const { database } = await this.ensureInitialized();
9617
+ const { database, readIssues } = await this.ensureInitialized();
9618
+ this.requireReadableComponents(readIssues, "database");
8671
9619
  const seen = /* @__PURE__ */ new Set();
8672
9620
  const results = [];
8673
9621
  for (const branchKey of this.getBranchCatalogKeys()) {
@@ -8681,43 +9629,50 @@ var Indexer = class {
8681
9629
  return results;
8682
9630
  }
8683
9631
  async findCallPath(fromName, toName, maxDepth) {
8684
- const { database } = await this.ensureInitialized();
9632
+ const { database, readIssues } = await this.ensureInitialized();
9633
+ this.requireReadableComponents(readIssues, "database");
8685
9634
  let shortest = [];
8686
9635
  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;
9636
+ const path17 = database.findShortestPath(fromName, toName, branchKey, maxDepth);
9637
+ if (path17.length > 0 && (shortest.length === 0 || path17.length < shortest.length)) {
9638
+ shortest = path17;
8690
9639
  }
8691
9640
  }
8692
9641
  return shortest;
8693
9642
  }
8694
9643
  async getSymbolsForBranch(branch) {
8695
- const { database } = await this.ensureInitialized();
9644
+ const { database, readIssues } = await this.ensureInitialized();
9645
+ this.requireReadableComponents(readIssues, "database");
8696
9646
  const resolvedBranch = branch ?? this.getBranchCatalogKey();
8697
9647
  return database.getSymbolsForBranch(resolvedBranch);
8698
9648
  }
8699
9649
  async getSymbolsForFiles(filePaths, branch) {
8700
- const { database } = await this.ensureInitialized();
9650
+ const { database, readIssues } = await this.ensureInitialized();
9651
+ this.requireReadableComponents(readIssues, "database");
8701
9652
  const resolvedBranch = branch ?? this.getBranchCatalogKey();
8702
9653
  return database.getSymbolsForFiles(filePaths, resolvedBranch);
8703
9654
  }
8704
9655
  async getTransitiveReachability(rootSymbolIds, direction, maxDepth) {
8705
- const { database } = await this.ensureInitialized();
9656
+ const { database, readIssues } = await this.ensureInitialized();
9657
+ this.requireReadableComponents(readIssues, "database");
8706
9658
  const branch = this.getBranchCatalogKey();
8707
9659
  return database.getTransitiveReachability(rootSymbolIds, branch, direction, maxDepth);
8708
9660
  }
8709
9661
  async detectCommunities(branch, symbolIds) {
8710
- const { database } = await this.ensureInitialized();
9662
+ const { database, readIssues } = await this.ensureInitialized();
9663
+ this.requireReadableComponents(readIssues, "database");
8711
9664
  const resolvedBranch = branch ?? this.getBranchCatalogKey();
8712
9665
  return database.detectCommunities(resolvedBranch, symbolIds);
8713
9666
  }
8714
9667
  async computeCentrality(branch) {
8715
- const { database } = await this.ensureInitialized();
9668
+ const { database, readIssues } = await this.ensureInitialized();
9669
+ this.requireReadableComponents(readIssues, "database");
8716
9670
  const resolvedBranch = branch ?? this.getBranchCatalogKey();
8717
9671
  return database.computeCentrality(resolvedBranch);
8718
9672
  }
8719
9673
  async getPrImpact(opts) {
8720
- const { database } = await this.ensureInitialized();
9674
+ const { database, readIssues } = await this.ensureInitialized();
9675
+ this.requireReadableComponents(readIssues, "database");
8721
9676
  const execFileAsync3 = (0, import_util3.promisify)(import_child_process3.execFile);
8722
9677
  const changedFilesResult = await getChangedFiles({
8723
9678
  pr: opts.pr,
@@ -8740,7 +9695,7 @@ var Indexer = class {
8740
9695
  "Run index_codebase first to build the call graph and symbol index for this branch."
8741
9696
  );
8742
9697
  }
8743
- const absoluteChangedFiles = changedFiles.map((f) => path12.resolve(this.projectRoot, f));
9698
+ const absoluteChangedFiles = changedFiles.map((f) => path13.resolve(this.projectRoot, f));
8744
9699
  const directSymbols = database.getSymbolsForFiles(absoluteChangedFiles, branchKey);
8745
9700
  const directIds = directSymbols.map((s) => s.id);
8746
9701
  const direction = opts.direction ?? "both";
@@ -8823,7 +9778,7 @@ var Indexer = class {
8823
9778
  projectRoot: this.projectRoot,
8824
9779
  baseBranch: this.baseBranch
8825
9780
  });
8826
- const otherAbsolute = otherChanged.files.map((f) => path12.resolve(this.projectRoot, f));
9781
+ const otherAbsolute = otherChanged.files.map((f) => path13.resolve(this.projectRoot, f));
8827
9782
  const otherBranchKey = this.getBranchCatalogKeyFor(openPr.headRefName);
8828
9783
  const otherSymbols = database.getSymbolsForFiles(otherAbsolute, otherBranchKey);
8829
9784
  const otherLabels = /* @__PURE__ */ new Set();
@@ -8873,7 +9828,8 @@ var Indexer = class {
8873
9828
  };
8874
9829
  }
8875
9830
  async getVisualizationData(options) {
8876
- const { database, store } = await this.ensureInitialized();
9831
+ const { database, store, readIssues } = await this.ensureInitialized();
9832
+ this.requireReadableComponents(readIssues, "vectors", "database");
8877
9833
  const seenSymbols = /* @__PURE__ */ new Map();
8878
9834
  const seenEdges = /* @__PURE__ */ new Map();
8879
9835
  for (const branchKey of this.getBranchCatalogKeys()) {
@@ -8886,12 +9842,12 @@ var Indexer = class {
8886
9842
  if (meta.filePath) filePaths.add(meta.filePath);
8887
9843
  }
8888
9844
  const directory = options?.directory?.replace(/\/$/, "");
8889
- const absoluteDirectoryFilter = directory ? path12.resolve(this.projectRoot, directory) : void 0;
9845
+ const absoluteDirectoryFilter = directory ? path13.resolve(this.projectRoot, directory) : void 0;
8890
9846
  for (const filePath of filePaths) {
8891
9847
  if (directory) {
8892
- const absoluteFilePath = path12.resolve(filePath);
9848
+ const absoluteFilePath = path13.resolve(filePath);
8893
9849
  const matchesRelative = filePath === directory || filePath.startsWith(directory + "/");
8894
- const matchesProjectRelative = absoluteDirectoryFilter !== void 0 && (absoluteFilePath === absoluteDirectoryFilter || absoluteFilePath.startsWith(absoluteDirectoryFilter + path12.sep));
9850
+ const matchesProjectRelative = absoluteDirectoryFilter !== void 0 && (absoluteFilePath === absoluteDirectoryFilter || absoluteFilePath.startsWith(absoluteDirectoryFilter + path13.sep));
8895
9851
  if (!matchesRelative && !matchesProjectRelative) {
8896
9852
  continue;
8897
9853
  }
@@ -8913,53 +9869,64 @@ var Indexer = class {
8913
9869
  return { symbols: [...seenSymbols.values()], edges: [...seenEdges.values()] };
8914
9870
  }
8915
9871
  async close() {
8916
- await this.database?.close();
9872
+ this.database?.close();
9873
+ for (const database of this.retiredDatabases) {
9874
+ database.close();
9875
+ }
9876
+ this.retiredDatabases = [];
8917
9877
  this.database = null;
8918
9878
  this.store = null;
8919
9879
  this.invertedIndex = null;
8920
9880
  this.provider = null;
8921
9881
  this.reranker = null;
9882
+ this.configuredProviderInfo = null;
9883
+ this.indexCompatibility = null;
9884
+ this.initializationMode = "none";
9885
+ this.readIssues = [];
9886
+ this.readerArtifactFingerprint = null;
9887
+ this.writerArtifactFingerprint = null;
9888
+ this.readerArtifactRetryAfter.clear();
8922
9889
  }
8923
9890
  };
8924
9891
 
8925
9892
  // src/tools/knowledge-base-paths.ts
8926
- var path13 = __toESM(require("path"), 1);
9893
+ var path14 = __toESM(require("path"), 1);
8927
9894
  function resolveConfigPathValue(value, baseDir) {
8928
9895
  const trimmed = value.trim();
8929
9896
  if (!trimmed) {
8930
9897
  return trimmed;
8931
9898
  }
8932
- const absolutePath = path13.isAbsolute(trimmed) ? trimmed : path13.resolve(baseDir, trimmed);
8933
- return path13.normalize(absolutePath);
9899
+ const absolutePath = path14.isAbsolute(trimmed) ? trimmed : path14.resolve(baseDir, trimmed);
9900
+ return path14.normalize(absolutePath);
8934
9901
  }
8935
9902
  function serializeConfigPathValue(value, baseDir) {
8936
9903
  const trimmed = value.trim();
8937
9904
  if (!trimmed) {
8938
9905
  return trimmed;
8939
9906
  }
8940
- if (!path13.isAbsolute(trimmed)) {
8941
- return normalizePathSeparators(path13.normalize(trimmed));
9907
+ if (!path14.isAbsolute(trimmed)) {
9908
+ return normalizePathSeparators(path14.normalize(trimmed));
8942
9909
  }
8943
- const relativePath = path13.relative(baseDir, trimmed);
8944
- if (!relativePath || !relativePath.startsWith("..") && !path13.isAbsolute(relativePath)) {
8945
- return normalizePathSeparators(path13.normalize(relativePath || "."));
9910
+ const relativePath = path14.relative(baseDir, trimmed);
9911
+ if (!relativePath || !relativePath.startsWith("..") && !path14.isAbsolute(relativePath)) {
9912
+ return normalizePathSeparators(path14.normalize(relativePath || "."));
8946
9913
  }
8947
- return path13.normalize(trimmed);
9914
+ return path14.normalize(trimmed);
8948
9915
  }
8949
9916
  function resolveKnowledgeBasePath(value, projectRoot3) {
8950
- return path13.isAbsolute(value) ? value : path13.resolve(projectRoot3, value);
9917
+ return path14.isAbsolute(value) ? value : path14.resolve(projectRoot3, value);
8951
9918
  }
8952
9919
  function normalizeKnowledgeBasePath2(value, projectRoot3) {
8953
- return path13.normalize(resolveKnowledgeBasePath(value, projectRoot3));
9920
+ return path14.normalize(resolveKnowledgeBasePath(value, projectRoot3));
8954
9921
  }
8955
9922
  function hasMatchingKnowledgeBasePath(knowledgeBases, inputPath, projectRoot3) {
8956
- const normalizedInput = path13.normalize(inputPath);
9923
+ const normalizedInput = path14.normalize(inputPath);
8957
9924
  return knowledgeBases.some((kb) => normalizeKnowledgeBasePath2(kb, projectRoot3) === normalizedInput);
8958
9925
  }
8959
9926
  function findKnowledgeBasePathIndex(knowledgeBases, inputPath, projectRoot3) {
8960
- const normalizedInput = path13.normalize(inputPath);
9927
+ const normalizedInput = path14.normalize(inputPath);
8961
9928
  return knowledgeBases.findIndex(
8962
- (kb) => path13.normalize(kb) === normalizedInput || normalizeKnowledgeBasePath2(kb, projectRoot3) === normalizedInput
9929
+ (kb) => path14.normalize(kb) === normalizedInput || normalizeKnowledgeBasePath2(kb, projectRoot3) === normalizedInput
8963
9930
  );
8964
9931
  }
8965
9932
 
@@ -9059,6 +10026,10 @@ function formatStatus(status) {
9059
10026
  lines.push(`Failed batches: ${status.failedBatchesPath}`);
9060
10027
  }
9061
10028
  }
10029
+ if (status.warning) {
10030
+ lines.push("");
10031
+ lines.push(`INDEX WARNING: ${status.warning}`);
10032
+ }
9062
10033
  if (status.compatibility && !status.compatibility.compatible) {
9063
10034
  lines.push("");
9064
10035
  lines.push(`COMPATIBILITY WARNING: ${status.compatibility.reason}`);
@@ -9104,6 +10075,17 @@ function calculatePercentage(progress) {
9104
10075
  if (progress.phase === "storing") return 95;
9105
10076
  return 0;
9106
10077
  }
10078
+ function formatCodebasePeek(results) {
10079
+ if (results.length === 0) {
10080
+ return "No matching code found. Try a different query or run index_codebase first.";
10081
+ }
10082
+ const formatted = results.map((r, idx) => {
10083
+ const location = `${r.filePath}:${r.startLine}-${r.endLine}`;
10084
+ const name = r.name ? `"${r.name}"` : "(anonymous)";
10085
+ return `[${idx + 1}] ${r.chunkType} ${name} at ${location} (score: ${r.score.toFixed(2)})${formatBlame(r)}`;
10086
+ });
10087
+ return formatted.join("\n");
10088
+ }
9107
10089
  function formatHealthCheck(result) {
9108
10090
  if (result.resetCorruptedIndex) {
9109
10091
  return result.warning ?? "Detected a corrupted local index and reset it. Run index_codebase to rebuild search data.";
@@ -9153,16 +10135,16 @@ function formatCallGraphCallees(symbolId, callees, relationshipType) {
9153
10135
  return `[${index + 1}] \u2192 ${edge.targetName} (${edge.callType})${confidence} at line ${edge.line}${edge.isResolved ? ` [resolved: ${edge.toSymbolId}]` : " [unresolved]"}`;
9154
10136
  }).join("\n");
9155
10137
  }
9156
- function formatCallGraphPath(from, to, path16) {
9157
- if (path16.length === 0) {
10138
+ function formatCallGraphPath(from, to, path17) {
10139
+ if (path17.length === 0) {
9158
10140
  return `No path found between "${from}" and "${to}". They may be in disconnected components, or the call graph index needs updating.`;
9159
10141
  }
9160
- const formatted = path16.map((hop, index) => {
10142
+ const formatted = path17.map((hop, index) => {
9161
10143
  const prefix = index === 0 ? "[start]" : `--${hop.callType}-->`;
9162
10144
  const location = hop.filePath ? ` (${hop.filePath}:${hop.line})` : "";
9163
10145
  return `${prefix} ${hop.symbolName}${location}`;
9164
10146
  });
9165
- return `Path (${path16.length} hops):
10147
+ return `Path (${path17.length} hops):
9166
10148
  ${formatted.join("\n")}`;
9167
10149
  }
9168
10150
  function formatResultHeader(result, index) {
@@ -9202,8 +10184,8 @@ ${truncateContent(r.content)}
9202
10184
  }
9203
10185
 
9204
10186
  // src/tools/config-state.ts
9205
- var import_fs8 = require("fs");
9206
- var path14 = __toESM(require("path"), 1);
10187
+ var import_fs9 = require("fs");
10188
+ var path15 = __toESM(require("path"), 1);
9207
10189
  function normalizeKnowledgeBasePaths(config, projectRoot3) {
9208
10190
  const normalized = { ...config };
9209
10191
  if (Array.isArray(normalized.knowledgeBases)) {
@@ -9230,10 +10212,10 @@ function loadEditableConfig(projectRoot3, host = "opencode") {
9230
10212
  }
9231
10213
  function saveConfig(projectRoot3, config, host = "opencode") {
9232
10214
  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 });
10215
+ const configDir = path15.dirname(configPath);
10216
+ const configBaseDir = path15.dirname(configDir);
10217
+ if (!(0, import_fs9.existsSync)(configDir)) {
10218
+ (0, import_fs9.mkdirSync)(configDir, { recursive: true });
9237
10219
  }
9238
10220
  const serializableConfig = { ...config };
9239
10221
  if (Array.isArray(serializableConfig.knowledgeBases)) {
@@ -9241,13 +10223,31 @@ function saveConfig(projectRoot3, config, host = "opencode") {
9241
10223
  (kb) => serializeConfigPathValue(kb, configBaseDir)
9242
10224
  );
9243
10225
  }
9244
- (0, import_fs8.writeFileSync)(configPath, JSON.stringify(serializableConfig, null, 2) + "\n", "utf-8");
10226
+ (0, import_fs9.writeFileSync)(configPath, JSON.stringify(serializableConfig, null, 2) + "\n", "utf-8");
9245
10227
  }
9246
10228
 
9247
10229
  // src/tools/operations.ts
9248
10230
  var indexerCache = /* @__PURE__ */ new Map();
9249
10231
  var configCache = /* @__PURE__ */ new Map();
9250
10232
  var defaultProjectRoots = /* @__PURE__ */ new Map();
10233
+ function getIndexBusyResult(error) {
10234
+ if (!isIndexLockContentionError(error)) return null;
10235
+ const owner = error.owner;
10236
+ const ownerText = owner ? `PID ${owner.pid}, operation ${owner.operation}, since ${owner.startedAt}` : "unreadable owner";
10237
+ if (error.reason === "legacy-lock") {
10238
+ return {
10239
+ kind: "busy",
10240
+ text: `INDEX_BUSY: legacy lock format detected (${ownerText}). Verify the PID and remove this lock manually only if it is stale.`
10241
+ };
10242
+ }
10243
+ if (error.reason === "unknown-owner") {
10244
+ return {
10245
+ kind: "busy",
10246
+ text: `INDEX_BUSY: unreadable or remote lock owner (${ownerText}). Automatic recovery was refused; manual verification is required.`
10247
+ };
10248
+ }
10249
+ return { kind: "busy", text: `INDEX_BUSY: another index operation is already in progress (${ownerText}).` };
10250
+ }
9251
10251
  function getProjectRoot(projectRoot3, host) {
9252
10252
  if (projectRoot3) {
9253
10253
  return projectRoot3;
@@ -9285,8 +10285,8 @@ function refreshIndexerForDirectory(projectRoot3, host = "opencode", config = pa
9285
10285
  }
9286
10286
  function shouldForceLocalizeProjectIndex(projectRoot3, host = "opencode") {
9287
10287
  const root = getProjectRoot(projectRoot3, host);
9288
- const localIndexPath = path15.join(root, getHostProjectIndexRelativePath(host));
9289
- if ((0, import_fs9.existsSync)(localIndexPath)) {
10288
+ const localIndexPath = path16.join(root, getHostProjectIndexRelativePath(host));
10289
+ if ((0, import_fs10.existsSync)(localIndexPath)) {
9290
10290
  return false;
9291
10291
  }
9292
10292
  const inheritedIndexPath = resolveWorktreeFallbackProjectIndexPath(root, host);
@@ -9342,35 +10342,46 @@ async function getCallGraphPath(projectRoot3, host, from, to, maxDepth) {
9342
10342
  async function runIndexCodebase(projectRoot3, host, args, onProgress) {
9343
10343
  const root = getProjectRoot(projectRoot3, host);
9344
10344
  const key = getIndexerCacheKey(root, host);
9345
- const cachedConfig = configCache.get(key);
9346
10345
  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)
10346
+ const runtimeConfig = configCache.get(key);
10347
+ try {
10348
+ if (args.estimateOnly) {
10349
+ return { kind: "estimate", estimate: await indexer.estimateCost() };
10350
+ }
10351
+ const runIndex = async (target) => {
10352
+ const operation = args.force ? target.forceIndex.bind(target) : target.index.bind(target);
10353
+ return operation((progress) => {
10354
+ if (onProgress) {
10355
+ return onProgress(formatProgressTitle(progress), {
10356
+ phase: progress.phase,
10357
+ filesProcessed: progress.filesProcessed,
10358
+ totalFiles: progress.totalFiles,
10359
+ chunksProcessed: progress.chunksProcessed,
10360
+ totalChunks: progress.totalChunks,
10361
+ percentage: calculatePercentage(progress)
10362
+ });
10363
+ }
10364
+ return Promise.resolve();
9369
10365
  });
10366
+ };
10367
+ let stats;
10368
+ if (args.force && shouldForceLocalizeProjectIndex(root, host)) {
10369
+ const inheritedIndexPath = resolveProjectIndexPath(root, runtimeConfig.scope, host);
10370
+ stats = await withIndexLock(inheritedIndexPath, "force-index", async () => {
10371
+ materializeLocalProjectConfig(root, loadProjectConfigLayer(root, host), host);
10372
+ refreshIndexerForDirectory(root, host, runtimeConfig);
10373
+ indexer = getIndexerForProject(root, host);
10374
+ return runIndex(indexer);
10375
+ }, { completeRecoveries: false });
10376
+ } else {
10377
+ stats = await runIndex(indexer);
9370
10378
  }
9371
- return Promise.resolve();
9372
- });
9373
- return { kind: "stats", stats };
10379
+ return { kind: "stats", stats };
10380
+ } catch (error) {
10381
+ const busyResult = getIndexBusyResult(error);
10382
+ if (!busyResult) throw error;
10383
+ return busyResult;
10384
+ }
9374
10385
  }
9375
10386
  async function getIndexStatus(projectRoot3, host) {
9376
10387
  const indexer = getIndexerForProject(projectRoot3, host);
@@ -9380,6 +10391,15 @@ async function getIndexHealthCheck(projectRoot3, host) {
9380
10391
  const indexer = getIndexerForProject(projectRoot3, host);
9381
10392
  return indexer.healthCheck();
9382
10393
  }
10394
+ async function runIndexHealthCheck(projectRoot3, host) {
10395
+ try {
10396
+ return { kind: "health", health: await getIndexHealthCheck(projectRoot3, host) };
10397
+ } catch (error) {
10398
+ const busyResult = getIndexBusyResult(error);
10399
+ if (!busyResult) throw error;
10400
+ return busyResult;
10401
+ }
10402
+ }
9383
10403
  async function getPrImpact(projectRoot3, host, params) {
9384
10404
  const indexer = getIndexerForProject(projectRoot3, host);
9385
10405
  return indexer.getPrImpact({
@@ -9446,15 +10466,15 @@ async function getIndexLogs(projectRoot3, host, args) {
9446
10466
  function addKnowledgeBase(projectRoot3, host, knowledgeBasePath) {
9447
10467
  const root = getProjectRoot(projectRoot3, host);
9448
10468
  const inputPath = knowledgeBasePath.trim();
9449
- const normalizedPath = path15.resolve(
9450
- path15.isAbsolute(inputPath) ? inputPath : resolveKnowledgeBasePath(inputPath, root)
10469
+ const normalizedPath = path16.resolve(
10470
+ path16.isAbsolute(inputPath) ? inputPath : resolveKnowledgeBasePath(inputPath, root)
9451
10471
  );
9452
- if (!(0, import_fs9.existsSync)(normalizedPath)) {
10472
+ if (!(0, import_fs10.existsSync)(normalizedPath)) {
9453
10473
  return `Error: Directory does not exist: ${normalizedPath}`;
9454
10474
  }
9455
10475
  let realPath;
9456
10476
  try {
9457
- realPath = (0, import_fs9.realpathSync)(normalizedPath);
10477
+ realPath = (0, import_fs10.realpathSync)(normalizedPath);
9458
10478
  } catch {
9459
10479
  return `Error: Cannot resolve path: ${normalizedPath}`;
9460
10480
  }
@@ -9483,13 +10503,13 @@ function addKnowledgeBase(projectRoot3, host, knowledgeBasePath) {
9483
10503
  }
9484
10504
  }
9485
10505
  for (const dotDir of sensitiveDotDirs) {
9486
- const sensitiveDir = path15.join(homeDir, dotDir);
10506
+ const sensitiveDir = path16.join(homeDir, dotDir);
9487
10507
  if (sensitiveDir && (realPath === sensitiveDir || realPath.startsWith(`${sensitiveDir}/`))) {
9488
10508
  return `Error: Adding sensitive directory as knowledge base is not allowed: ${normalizedPath}`;
9489
10509
  }
9490
10510
  }
9491
10511
  try {
9492
- const stat = (0, import_fs9.statSync)(normalizedPath);
10512
+ const stat = (0, import_fs10.statSync)(normalizedPath);
9493
10513
  if (!stat.isDirectory()) {
9494
10514
  return `Error: Path is not a directory: ${normalizedPath}`;
9495
10515
  }
@@ -9529,7 +10549,7 @@ function listKnowledgeBases(projectRoot3, host) {
9529
10549
  for (let i = 0; i < knowledgeBases.length; i++) {
9530
10550
  const kb = knowledgeBases[i];
9531
10551
  const resolvedPath = resolveKnowledgeBasePath(kb, root);
9532
- const exists = (0, import_fs9.existsSync)(resolvedPath);
10552
+ const exists = (0, import_fs10.existsSync)(resolvedPath);
9533
10553
  result += `[${i + 1}] ${kb}
9534
10554
  `;
9535
10555
  result += ` Resolved: ${resolvedPath}
@@ -9538,7 +10558,7 @@ function listKnowledgeBases(projectRoot3, host) {
9538
10558
  `;
9539
10559
  if (exists) {
9540
10560
  try {
9541
- const stat = (0, import_fs9.statSync)(resolvedPath);
10561
+ const stat = (0, import_fs10.statSync)(resolvedPath);
9542
10562
  result += ` Type: ${stat.isDirectory() ? "Directory" : "File"}
9543
10563
  `;
9544
10564
  } catch {
@@ -9546,7 +10566,7 @@ function listKnowledgeBases(projectRoot3, host) {
9546
10566
  }
9547
10567
  result += "\n";
9548
10568
  }
9549
- const hasHostConfig = (0, import_fs9.existsSync)(path15.join(root, getHostProjectConfigRelativePath(host)));
10569
+ const hasHostConfig = (0, import_fs10.existsSync)(path16.join(root, getHostProjectConfigRelativePath(host)));
9550
10570
  if (hasHostConfig) {
9551
10571
  result += `
9552
10572
  Config sources: 1 file(s).`;
@@ -9652,10 +10672,67 @@ function projectRoot2(ctx) {
9652
10672
  return ctx?.cwd ?? process.cwd();
9653
10673
  }
9654
10674
  function codebaseIndexPiExtension(pi) {
10675
+ pi.registerTool({
10676
+ name: "codebase_context",
10677
+ label: "Codebase Context",
10678
+ description: "PREFERRED FIRST TOOL for any repository question. Check index_status when freshness is unknown, then use this tool for low-token location discovery and dependency flow.",
10679
+ parameters: import_typebox2.Type.Object({
10680
+ query: import_typebox2.Type.String({ description: "Natural language description of what code you're trying to locate" }),
10681
+ from: import_typebox2.Type.Optional(import_typebox2.Type.String({ description: "Source symbol when asking for a dependency path." })),
10682
+ to: import_typebox2.Type.Optional(import_typebox2.Type.String({ description: "Target symbol when asking for a dependency path." })),
10683
+ symbol: import_typebox2.Type.Optional(import_typebox2.Type.String({ description: "Exact symbol name for an authoritative definition lookup." })),
10684
+ limit: import_typebox2.Type.Optional(import_typebox2.Type.Number({ description: "Maximum results (default: 10)" })),
10685
+ maxDepth: import_typebox2.Type.Optional(import_typebox2.Type.Number({ description: "Maximum call-graph traversal depth for from/to paths" })),
10686
+ fileType: import_typebox2.Type.Optional(import_typebox2.Type.String({ description: "Filter by file extension, e.g., ts, py, rs" })),
10687
+ directory: import_typebox2.Type.Optional(import_typebox2.Type.String({ description: "Filter by directory path" }))
10688
+ }),
10689
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
10690
+ const root = projectRoot2(ctx);
10691
+ if (params.from && params.to) {
10692
+ const path17 = await getCallGraphPath(root, HOST2, params.from, params.to, params.maxDepth);
10693
+ if (path17.length > 0) {
10694
+ return text2(formatCallGraphPath(params.from, params.to, path17), path17);
10695
+ }
10696
+ const { callers } = await getCallGraphData(root, HOST2, {
10697
+ name: params.to,
10698
+ direction: "callers"
10699
+ });
10700
+ const directEdge = callers.find((edge) => edge.fromSymbolName === params.from);
10701
+ if (directEdge) {
10702
+ const location = directEdge.fromSymbolFilePath ? ` at ${directEdge.fromSymbolFilePath}:${directEdge.line}` : "";
10703
+ return text2(
10704
+ `Direct path: ${params.from} --${directEdge.callType}--> ${params.to}${location} (edge is ${directEdge.isResolved ? "resolved" : "unresolved"}).`,
10705
+ path17
10706
+ );
10707
+ }
10708
+ return text2(formatCallGraphPath(params.from, params.to, path17), path17);
10709
+ }
10710
+ if (params.symbol) {
10711
+ const results2 = await implementationLookup(root, HOST2, params.symbol, {
10712
+ limit: params.limit ?? 10,
10713
+ fileType: params.fileType,
10714
+ directory: params.directory
10715
+ });
10716
+ return text2(formatDefinitionLookup(results2, params.symbol), results2);
10717
+ }
10718
+ const results = await searchCodebase(root, HOST2, params.query, {
10719
+ limit: params.limit ?? 10,
10720
+ fileType: params.fileType,
10721
+ directory: params.directory,
10722
+ metadataOnly: true
10723
+ });
10724
+ if (results.length === 0) {
10725
+ return text2("No matching code found. Try a different query or run index_status/index_codebase first.");
10726
+ }
10727
+ return text2(`Found ${results.length} locations for "${params.query}":
10728
+
10729
+ ${formatCodebasePeek(results)}`, results);
10730
+ }
10731
+ });
9655
10732
  pi.registerTool({
9656
10733
  name: "codebase_search",
9657
10734
  label: "Codebase Search",
9658
- description: "Semantic search over the indexed codebase. Describe behavior, not syntax.",
10735
+ description: "Use this after codebase_context when you need semantic content, not just locations. Describe behavior, not syntax.",
9659
10736
  parameters: import_typebox2.Type.Object({
9660
10737
  query: import_typebox2.Type.String({ description: "Natural language description of what code you're looking for" }),
9661
10738
  limit: import_typebox2.Type.Optional(import_typebox2.Type.Number({ description: "Maximum results (default: 10)" })),
@@ -9675,7 +10752,7 @@ function codebaseIndexPiExtension(pi) {
9675
10752
  pi.registerTool({
9676
10753
  name: "codebase_peek",
9677
10754
  label: "Codebase Peek",
9678
- description: "Semantic search returning only metadata (file, lines, symbol) to save tokens.",
10755
+ description: "LOW-TOKEN location-first retrieval. Prefer codebase_context first, then use this for cheap conceptual lookup.",
9679
10756
  parameters: import_typebox2.Type.Object({
9680
10757
  query: import_typebox2.Type.String(),
9681
10758
  limit: import_typebox2.Type.Optional(import_typebox2.Type.Number()),
@@ -9694,7 +10771,7 @@ function codebaseIndexPiExtension(pi) {
9694
10771
  pi.registerTool({
9695
10772
  name: "find_similar",
9696
10773
  label: "Find Similar Code",
9697
- description: "Find code similar to a snippet for duplicate detection and pattern discovery.",
10774
+ description: "Find code similar to a snippet for duplicate detection, pattern discovery, and refactor planning.",
9698
10775
  parameters: import_typebox2.Type.Object({
9699
10776
  code: import_typebox2.Type.String({ description: "Code snippet to compare" }),
9700
10777
  limit: import_typebox2.Type.Optional(import_typebox2.Type.Number()),
@@ -9711,7 +10788,7 @@ function codebaseIndexPiExtension(pi) {
9711
10788
  pi.registerTool({
9712
10789
  name: "implementation_lookup",
9713
10790
  label: "Implementation Lookup",
9714
- description: "Find likely symbol definitions or implementations by name or natural language.",
10791
+ description: "Find likely symbol definitions or implementations after codebase_context identifies a symbol.",
9715
10792
  parameters: import_typebox2.Type.Object({
9716
10793
  query: import_typebox2.Type.String(),
9717
10794
  limit: import_typebox2.Type.Optional(import_typebox2.Type.Number()),
@@ -9726,7 +10803,7 @@ function codebaseIndexPiExtension(pi) {
9726
10803
  pi.registerTool({
9727
10804
  name: "index_codebase",
9728
10805
  label: "Index Codebase",
9729
- description: "Build or refresh the semantic codebase index.",
10806
+ description: "Build or refresh the semantic codebase index. Run index_status when freshness is unknown.",
9730
10807
  parameters: import_typebox2.Type.Object({
9731
10808
  force: import_typebox2.Type.Optional(import_typebox2.Type.Boolean({ default: false })),
9732
10809
  estimateOnly: import_typebox2.Type.Optional(import_typebox2.Type.Boolean({ default: false })),
@@ -9734,7 +10811,9 @@ function codebaseIndexPiExtension(pi) {
9734
10811
  }),
9735
10812
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
9736
10813
  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);
10814
+ if (result.kind === "estimate") return text2(formatCostEstimate(result.estimate), result.estimate);
10815
+ if (result.kind === "busy") return text2(result.text, { code: "INDEX_BUSY" });
10816
+ return text2(formatIndexStats(result.stats, params.verbose ?? false), result.stats);
9738
10817
  }
9739
10818
  });
9740
10819
  pi.registerTool({
@@ -9750,11 +10829,12 @@ function codebaseIndexPiExtension(pi) {
9750
10829
  pi.registerTool({
9751
10830
  name: "index_health_check",
9752
10831
  label: "Index Health Check",
9753
- description: "Garbage collect orphaned embeddings/chunks and report health.",
10832
+ description: "Garbage collect orphaned embeddings/chunks and report index health status.",
9754
10833
  parameters: import_typebox2.Type.Object({}),
9755
10834
  async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
9756
- const result = await getIndexHealthCheck(projectRoot2(ctx), HOST2);
9757
- return text2(formatHealthCheck(result), result);
10835
+ const result = await runIndexHealthCheck(projectRoot2(ctx), HOST2);
10836
+ if (result.kind === "busy") return text2(result.text, { code: "INDEX_BUSY" });
10837
+ return text2(formatHealthCheck(result.health), result.health);
9758
10838
  }
9759
10839
  });
9760
10840
  pi.registerTool({
@@ -9789,6 +10869,11 @@ function codebaseIndexPiExtension(pi) {
9789
10869
  }
9790
10870
  });
9791
10871
  registerPiCallGraphTools(pi);
10872
+ pi.on("before_agent_start", (event) => ({
10873
+ systemPrompt: `${event.systemPrompt}
10874
+
10875
+ Check index_status first when index readiness is unknown. For repository questions, call codebase_context before search/grep/bash/read-style broad reads. Use implementation_lookup for known symbols and call_graph/call_graph_path after endpoints are identified for dependency flow. Avoid broad tool calls until semantic locations are known.`
10876
+ }));
9792
10877
  pi.registerTool({
9793
10878
  name: "pr_impact",
9794
10879
  label: "PR Impact",