opencode-codebase-index 0.13.2 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -490,7 +490,7 @@ var require_ignore = __commonJS({
490
490
  // path matching.
491
491
  // - check `string` either `MODE_IGNORE` or `MODE_CHECK_IGNORE`
492
492
  // @returns {TestResult} true if a file is ignored
493
- test(path22, checkUnignored, mode) {
493
+ test(path24, checkUnignored, mode) {
494
494
  let ignored = false;
495
495
  let unignored = false;
496
496
  let matchedRule;
@@ -499,7 +499,7 @@ var require_ignore = __commonJS({
499
499
  if (unignored === negative && ignored !== unignored || negative && !ignored && !unignored && !checkUnignored) {
500
500
  return;
501
501
  }
502
- const matched = rule[mode].test(path22);
502
+ const matched = rule[mode].test(path24);
503
503
  if (!matched) {
504
504
  return;
505
505
  }
@@ -520,17 +520,17 @@ var require_ignore = __commonJS({
520
520
  var throwError = (message, Ctor) => {
521
521
  throw new Ctor(message);
522
522
  };
523
- var checkPath = (path22, originalPath, doThrow) => {
524
- if (!isString(path22)) {
523
+ var checkPath = (path24, originalPath, doThrow) => {
524
+ if (!isString(path24)) {
525
525
  return doThrow(
526
526
  `path must be a string, but got \`${originalPath}\``,
527
527
  TypeError
528
528
  );
529
529
  }
530
- if (!path22) {
530
+ if (!path24) {
531
531
  return doThrow(`path must not be empty`, TypeError);
532
532
  }
533
- if (checkPath.isNotRelative(path22)) {
533
+ if (checkPath.isNotRelative(path24)) {
534
534
  const r = "`path.relative()`d";
535
535
  return doThrow(
536
536
  `path should be a ${r} string, but got "${originalPath}"`,
@@ -539,7 +539,7 @@ var require_ignore = __commonJS({
539
539
  }
540
540
  return true;
541
541
  };
542
- var isNotRelative = (path22) => REGEX_TEST_INVALID_PATH.test(path22);
542
+ var isNotRelative = (path24) => REGEX_TEST_INVALID_PATH.test(path24);
543
543
  checkPath.isNotRelative = isNotRelative;
544
544
  checkPath.convert = (p) => p;
545
545
  var Ignore2 = class {
@@ -569,19 +569,19 @@ var require_ignore = __commonJS({
569
569
  }
570
570
  // @returns {TestResult}
571
571
  _test(originalPath, cache, checkUnignored, slices) {
572
- const path22 = originalPath && checkPath.convert(originalPath);
572
+ const path24 = originalPath && checkPath.convert(originalPath);
573
573
  checkPath(
574
- path22,
574
+ path24,
575
575
  originalPath,
576
576
  this._strictPathCheck ? throwError : RETURN_FALSE
577
577
  );
578
- return this._t(path22, cache, checkUnignored, slices);
578
+ return this._t(path24, cache, checkUnignored, slices);
579
579
  }
580
- checkIgnore(path22) {
581
- if (!REGEX_TEST_TRAILING_SLASH.test(path22)) {
582
- return this.test(path22);
580
+ checkIgnore(path24) {
581
+ if (!REGEX_TEST_TRAILING_SLASH.test(path24)) {
582
+ return this.test(path24);
583
583
  }
584
- const slices = path22.split(SLASH2).filter(Boolean);
584
+ const slices = path24.split(SLASH2).filter(Boolean);
585
585
  slices.pop();
586
586
  if (slices.length) {
587
587
  const parent = this._t(
@@ -594,18 +594,18 @@ var require_ignore = __commonJS({
594
594
  return parent;
595
595
  }
596
596
  }
597
- return this._rules.test(path22, false, MODE_CHECK_IGNORE);
597
+ return this._rules.test(path24, false, MODE_CHECK_IGNORE);
598
598
  }
599
- _t(path22, cache, checkUnignored, slices) {
600
- if (path22 in cache) {
601
- return cache[path22];
599
+ _t(path24, cache, checkUnignored, slices) {
600
+ if (path24 in cache) {
601
+ return cache[path24];
602
602
  }
603
603
  if (!slices) {
604
- slices = path22.split(SLASH2).filter(Boolean);
604
+ slices = path24.split(SLASH2).filter(Boolean);
605
605
  }
606
606
  slices.pop();
607
607
  if (!slices.length) {
608
- return cache[path22] = this._rules.test(path22, checkUnignored, MODE_IGNORE);
608
+ return cache[path24] = this._rules.test(path24, checkUnignored, MODE_IGNORE);
609
609
  }
610
610
  const parent = this._t(
611
611
  slices.join(SLASH2) + SLASH2,
@@ -613,29 +613,29 @@ var require_ignore = __commonJS({
613
613
  checkUnignored,
614
614
  slices
615
615
  );
616
- return cache[path22] = parent.ignored ? parent : this._rules.test(path22, checkUnignored, MODE_IGNORE);
616
+ return cache[path24] = parent.ignored ? parent : this._rules.test(path24, checkUnignored, MODE_IGNORE);
617
617
  }
618
- ignores(path22) {
619
- return this._test(path22, this._ignoreCache, false).ignored;
618
+ ignores(path24) {
619
+ return this._test(path24, this._ignoreCache, false).ignored;
620
620
  }
621
621
  createFilter() {
622
- return (path22) => !this.ignores(path22);
622
+ return (path24) => !this.ignores(path24);
623
623
  }
624
624
  filter(paths) {
625
625
  return makeArray(paths).filter(this.createFilter());
626
626
  }
627
627
  // @returns {TestResult}
628
- test(path22) {
629
- return this._test(path22, this._testCache, true);
628
+ test(path24) {
629
+ return this._test(path24, this._testCache, true);
630
630
  }
631
631
  };
632
632
  var factory = (options) => new Ignore2(options);
633
- var isPathValid = (path22) => checkPath(path22 && checkPath.convert(path22), path22, RETURN_FALSE);
633
+ var isPathValid = (path24) => checkPath(path24 && checkPath.convert(path24), path24, RETURN_FALSE);
634
634
  var setupWindows = () => {
635
635
  const makePosix = (str) => /^\\\\\?\\/.test(str) || /["<>|\u0000-\u001F]+/u.test(str) ? str : str.replace(/\\/g, "/");
636
636
  checkPath.convert = makePosix;
637
637
  const REGEX_TEST_WINDOWS_PATH_ABSOLUTE = /^[a-z]:\//i;
638
- checkPath.isNotRelative = (path22) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path22) || isNotRelative(path22);
638
+ checkPath.isNotRelative = (path24) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path24) || isNotRelative(path24);
639
639
  };
640
640
  if (
641
641
  // Detect `process` so that it can run in browsers.
@@ -651,16 +651,16 @@ var require_ignore = __commonJS({
651
651
  });
652
652
 
653
653
  // src/index.ts
654
- import * as os5 from "os";
655
- import * as path21 from "path";
654
+ import * as os6 from "os";
655
+ import * as path23 from "path";
656
656
  import { fileURLToPath as fileURLToPath2 } from "url";
657
657
 
658
658
  // src/config/constants.ts
659
659
  var DEFAULT_INCLUDE = [
660
- "**/*.{ts,tsx,js,jsx,mjs,cjs}",
660
+ "**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}",
661
661
  "**/*.{py,pyi}",
662
- "**/*.{go,rs,java,kt,scala}",
663
- "**/*.{c,cpp,cc,h,hpp}",
662
+ "**/*.{go,rs,java,cs,kt,scala}",
663
+ "**/*.{c,cpp,cc,cxx,h,hpp,hxx}",
664
664
  "**/*.{rb,php,inc,swift}",
665
665
  "**/*.{cls,trigger}",
666
666
  "**/*.{vue,svelte,astro}",
@@ -785,7 +785,8 @@ function getDefaultIndexingConfig() {
785
785
  requireProjectMarker: true,
786
786
  maxDepth: 5,
787
787
  maxFilesPerDirectory: 100,
788
- fallbackToTextOnMaxChunks: true
788
+ fallbackToTextOnMaxChunks: true,
789
+ gitBlame: { enabled: false }
789
790
  };
790
791
  }
791
792
  function getDefaultSearchConfig() {
@@ -909,7 +910,10 @@ function parseConfig(raw) {
909
910
  requireProjectMarker: typeof rawIndexing.requireProjectMarker === "boolean" ? rawIndexing.requireProjectMarker : defaultIndexing.requireProjectMarker,
910
911
  maxDepth: typeof rawIndexing.maxDepth === "number" ? rawIndexing.maxDepth < -1 ? -1 : rawIndexing.maxDepth : defaultIndexing.maxDepth,
911
912
  maxFilesPerDirectory: typeof rawIndexing.maxFilesPerDirectory === "number" ? Math.max(1, rawIndexing.maxFilesPerDirectory) : defaultIndexing.maxFilesPerDirectory,
912
- fallbackToTextOnMaxChunks: typeof rawIndexing.fallbackToTextOnMaxChunks === "boolean" ? rawIndexing.fallbackToTextOnMaxChunks : defaultIndexing.fallbackToTextOnMaxChunks
913
+ fallbackToTextOnMaxChunks: typeof rawIndexing.fallbackToTextOnMaxChunks === "boolean" ? rawIndexing.fallbackToTextOnMaxChunks : defaultIndexing.fallbackToTextOnMaxChunks,
914
+ gitBlame: {
915
+ enabled: rawIndexing.gitBlame && typeof rawIndexing.gitBlame === "object" && typeof rawIndexing.gitBlame.enabled === "boolean" ? rawIndexing.gitBlame.enabled : defaultIndexing.gitBlame.enabled
916
+ }
913
917
  };
914
918
  const rawSearch = input.search && typeof input.search === "object" ? input.search : {};
915
919
  const search = {
@@ -1240,6 +1244,9 @@ function getHostProjectIndexRelativePath(host) {
1240
1244
  function hasHostProjectConfig(projectRoot, host) {
1241
1245
  return existsSync3(path3.join(projectRoot, getProjectConfigRelativePath(host)));
1242
1246
  }
1247
+ function hasHostGlobalConfig(host) {
1248
+ return existsSync3(getGlobalConfigPath(host));
1249
+ }
1243
1250
  function getGlobalIndexPath(host = "opencode") {
1244
1251
  switch (host) {
1245
1252
  case "opencode":
@@ -1279,6 +1286,9 @@ function resolveGlobalIndexPath(host = "opencode") {
1279
1286
  return hostIndexPath;
1280
1287
  }
1281
1288
  if (host !== "opencode") {
1289
+ if (hasHostGlobalConfig(host)) {
1290
+ return hostIndexPath;
1291
+ }
1282
1292
  const legacyIndexPath = getGlobalIndexPath("opencode");
1283
1293
  if (existsSync3(legacyIndexPath)) {
1284
1294
  return legacyIndexPath;
@@ -1577,16 +1587,442 @@ function loadMergedConfig(projectRoot, host = "opencode") {
1577
1587
  return merged;
1578
1588
  }
1579
1589
 
1590
+ // src/indexer/index-lock.ts
1591
+ import { randomUUID } from "crypto";
1592
+ import {
1593
+ existsSync as existsSync5,
1594
+ lstatSync,
1595
+ mkdirSync as mkdirSync2,
1596
+ readFileSync as readFileSync4,
1597
+ readdirSync as readdirSync2,
1598
+ realpathSync,
1599
+ renameSync,
1600
+ rmSync,
1601
+ writeFileSync as writeFileSync2
1602
+ } from "fs";
1603
+ import * as os2 from "os";
1604
+ import * as path7 from "path";
1605
+ var OWNER_FILE_NAME = "owner.json";
1606
+ var RECLAIM_DIRECTORY_NAME = "reclaiming";
1607
+ var RECOVERY_MARKER_PREFIX = "indexing.lock.recovery.";
1608
+ 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;
1609
+ var VALID_OPERATIONS = /* @__PURE__ */ new Set([
1610
+ "initialize",
1611
+ "index",
1612
+ "force-index",
1613
+ "clear",
1614
+ "health-check",
1615
+ "retry-failed-batches",
1616
+ "recovery"
1617
+ ]);
1618
+ var temporaryCounter = 0;
1619
+ function getErrorCode(error) {
1620
+ return typeof error === "object" && error !== null && "code" in error ? String(error.code) : void 0;
1621
+ }
1622
+ function retryTransientFilesystemOperation(operation) {
1623
+ let lastError;
1624
+ for (let attempt = 0; attempt < 3; attempt += 1) {
1625
+ try {
1626
+ operation();
1627
+ return;
1628
+ } catch (error) {
1629
+ lastError = error;
1630
+ const code = getErrorCode(error);
1631
+ if (code !== "EBUSY" && code !== "EPERM") throw error;
1632
+ if (attempt < 2) {
1633
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, (attempt + 1) * 10);
1634
+ }
1635
+ }
1636
+ }
1637
+ throw lastError;
1638
+ }
1639
+ function parseOwner(value) {
1640
+ if (typeof value !== "object" || value === null) return null;
1641
+ const candidate = value;
1642
+ if (!Number.isInteger(candidate.pid) || (candidate.pid ?? 0) <= 0) return null;
1643
+ if (typeof candidate.hostname !== "string" || candidate.hostname.length === 0) return null;
1644
+ if (typeof candidate.startedAt !== "string" || Number.isNaN(Date.parse(candidate.startedAt))) return null;
1645
+ if (typeof candidate.operation !== "string" || !VALID_OPERATIONS.has(candidate.operation)) return null;
1646
+ if (typeof candidate.token !== "string" || !UUID_PATTERN.test(candidate.token)) return null;
1647
+ return candidate;
1648
+ }
1649
+ function parseReclaimOwner(value) {
1650
+ if (typeof value !== "object" || value === null) return null;
1651
+ const candidate = value;
1652
+ if (!Number.isInteger(candidate.pid) || (candidate.pid ?? 0) <= 0) return null;
1653
+ if (typeof candidate.hostname !== "string" || candidate.hostname.length === 0) return null;
1654
+ if (typeof candidate.startedAt !== "string" || Number.isNaN(Date.parse(candidate.startedAt))) return null;
1655
+ if (typeof candidate.token !== "string" || !UUID_PATTERN.test(candidate.token)) return null;
1656
+ if (typeof candidate.expectedOwnerToken !== "string" || !UUID_PATTERN.test(candidate.expectedOwnerToken)) return null;
1657
+ return candidate;
1658
+ }
1659
+ function readJsonDirectory(directoryPath, parser) {
1660
+ try {
1661
+ return parser(JSON.parse(readFileSync4(path7.join(directoryPath, OWNER_FILE_NAME), "utf-8")));
1662
+ } catch {
1663
+ return null;
1664
+ }
1665
+ }
1666
+ function readDirectoryOwner(lockPath) {
1667
+ return readJsonDirectory(lockPath, parseOwner);
1668
+ }
1669
+ function readReclaimOwner(markerPath) {
1670
+ return readJsonDirectory(markerPath, parseReclaimOwner);
1671
+ }
1672
+ function readRecoveryOwner(markerPath) {
1673
+ return readDirectoryOwner(markerPath);
1674
+ }
1675
+ function readLegacyOwner(lockPath) {
1676
+ try {
1677
+ const parsed = JSON.parse(readFileSync4(lockPath, "utf-8"));
1678
+ if (!Number.isInteger(parsed.pid) || Number(parsed.pid) <= 0) return null;
1679
+ if (typeof parsed.startedAt !== "string" || Number.isNaN(Date.parse(parsed.startedAt))) return null;
1680
+ return {
1681
+ pid: Number(parsed.pid),
1682
+ hostname: typeof parsed.hostname === "string" ? parsed.hostname : os2.hostname(),
1683
+ startedAt: parsed.startedAt,
1684
+ operation: typeof parsed.operation === "string" && VALID_OPERATIONS.has(parsed.operation) ? parsed.operation : "index",
1685
+ token: typeof parsed.token === "string" ? parsed.token : "legacy-v0.14.0"
1686
+ };
1687
+ } catch {
1688
+ return null;
1689
+ }
1690
+ }
1691
+ function getOwnerLiveness(owner) {
1692
+ if (owner.hostname !== os2.hostname()) return "unknown";
1693
+ try {
1694
+ process.kill(owner.pid, 0);
1695
+ return "alive";
1696
+ } catch (error) {
1697
+ const code = getErrorCode(error);
1698
+ if (code === "ESRCH") return "dead";
1699
+ if (code === "EPERM") return "alive";
1700
+ return "unknown";
1701
+ }
1702
+ }
1703
+ function sameOwner(left, right) {
1704
+ return left.pid === right.pid && left.hostname === right.hostname && left.token === right.token;
1705
+ }
1706
+ function sameReclaimOwner(left, right) {
1707
+ return left.pid === right.pid && left.hostname === right.hostname && left.token === right.token && left.expectedOwnerToken === right.expectedOwnerToken;
1708
+ }
1709
+ function publishJsonDirectory(finalPath, value) {
1710
+ const candidatePath = `${finalPath}.candidate.${process.pid}.${randomUUID()}`;
1711
+ mkdirSync2(candidatePath, { mode: 448 });
1712
+ try {
1713
+ writeFileSync2(path7.join(candidatePath, OWNER_FILE_NAME), JSON.stringify(value), {
1714
+ encoding: "utf-8",
1715
+ flag: "wx",
1716
+ mode: 384
1717
+ });
1718
+ if (existsSync5(finalPath)) return false;
1719
+ try {
1720
+ renameSync(candidatePath, finalPath);
1721
+ return true;
1722
+ } catch (error) {
1723
+ if (existsSync5(finalPath)) return false;
1724
+ throw error;
1725
+ }
1726
+ } finally {
1727
+ if (existsSync5(candidatePath)) rmSync(candidatePath, { recursive: true, force: true });
1728
+ }
1729
+ }
1730
+ function createOwner(operation) {
1731
+ return {
1732
+ pid: process.pid,
1733
+ hostname: os2.hostname(),
1734
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1735
+ operation,
1736
+ token: randomUUID()
1737
+ };
1738
+ }
1739
+ function recoveryMarkerPath(indexPath, owner) {
1740
+ return path7.join(indexPath, `${RECOVERY_MARKER_PREFIX}${owner.token}`);
1741
+ }
1742
+ function publishRecoveryMarker(indexPath, owner) {
1743
+ const markerPath = recoveryMarkerPath(indexPath, owner);
1744
+ if (!publishJsonDirectory(markerPath, owner)) {
1745
+ const markerOwner = readRecoveryOwner(markerPath);
1746
+ if (!markerOwner || !sameOwner(markerOwner, owner)) {
1747
+ throw new IndexLockContentionError(markerPath, markerOwner, "unknown-owner");
1748
+ }
1749
+ }
1750
+ return markerPath;
1751
+ }
1752
+ function getPendingRecoveries(indexPath) {
1753
+ const recoveries = [];
1754
+ const markerNames = readdirSync2(indexPath).filter((name) => {
1755
+ if (!name.startsWith(RECOVERY_MARKER_PREFIX)) return false;
1756
+ return UUID_PATTERN.test(name.slice(RECOVERY_MARKER_PREFIX.length));
1757
+ }).sort();
1758
+ for (const markerName of markerNames) {
1759
+ const markerPath = path7.join(indexPath, markerName);
1760
+ const markerToken = markerName.slice(RECOVERY_MARKER_PREFIX.length);
1761
+ let markerStats;
1762
+ try {
1763
+ markerStats = lstatSync(markerPath);
1764
+ } catch (error) {
1765
+ if (getErrorCode(error) === "ENOENT") continue;
1766
+ throw error;
1767
+ }
1768
+ if (!markerStats.isDirectory()) {
1769
+ throw new IndexLockContentionError(markerPath, null, "unknown-owner");
1770
+ }
1771
+ const owner = readRecoveryOwner(markerPath);
1772
+ if (!owner || owner.token !== markerToken || getOwnerLiveness(owner) !== "dead") {
1773
+ throw new IndexLockContentionError(markerPath, owner, "unknown-owner");
1774
+ }
1775
+ recoveries.push({ owner, markerPath });
1776
+ }
1777
+ recoveries.sort((left, right) => {
1778
+ const byTimestamp = left.owner.startedAt.localeCompare(right.owner.startedAt);
1779
+ return byTimestamp !== 0 ? byTimestamp : left.markerPath.localeCompare(right.markerPath);
1780
+ });
1781
+ return recoveries;
1782
+ }
1783
+ function cleanupDeadPublicationCandidates(indexPath) {
1784
+ const candidatePattern = /^indexing\.lock(?:\.recovery\.[0-9a-f-]{36})?\.candidate\.(\d+)\.[0-9a-f-]{36}$/i;
1785
+ for (const entry of readdirSync2(indexPath)) {
1786
+ const match = candidatePattern.exec(entry);
1787
+ if (!match) continue;
1788
+ const pid = Number(match[1]);
1789
+ if (!Number.isInteger(pid) || pid <= 0) continue;
1790
+ if (getOwnerLiveness({ pid, hostname: os2.hostname() }) !== "dead") continue;
1791
+ rmSync(path7.join(indexPath, entry), { recursive: true, force: true });
1792
+ }
1793
+ }
1794
+ function removeDeadReclaimMarker(lockPath, expectedOwner) {
1795
+ const markerPath = path7.join(lockPath, RECLAIM_DIRECTORY_NAME);
1796
+ const marker = readReclaimOwner(markerPath);
1797
+ if (!marker || marker.expectedOwnerToken !== expectedOwner.token || getOwnerLiveness(marker) !== "dead") {
1798
+ return false;
1799
+ }
1800
+ const currentOwner = readDirectoryOwner(lockPath);
1801
+ if (!currentOwner || !sameOwner(currentOwner, expectedOwner) || getOwnerLiveness(currentOwner) !== "dead") {
1802
+ return false;
1803
+ }
1804
+ const claimedMarkerPath = `${markerPath}.stale.${marker.pid}.${marker.token}.${randomUUID()}`;
1805
+ try {
1806
+ renameSync(markerPath, claimedMarkerPath);
1807
+ } catch (error) {
1808
+ if (getErrorCode(error) === "ENOENT") return false;
1809
+ throw error;
1810
+ }
1811
+ const claimedMarker = readReclaimOwner(claimedMarkerPath);
1812
+ const ownerAfterClaim = readDirectoryOwner(lockPath);
1813
+ if (!claimedMarker || !sameReclaimOwner(claimedMarker, marker) || !ownerAfterClaim || !sameOwner(ownerAfterClaim, expectedOwner) || getOwnerLiveness(ownerAfterClaim) !== "dead") {
1814
+ if (!existsSync5(markerPath) && existsSync5(claimedMarkerPath)) {
1815
+ renameSync(claimedMarkerPath, markerPath);
1816
+ }
1817
+ return false;
1818
+ }
1819
+ rmSync(claimedMarkerPath, { recursive: true, force: true });
1820
+ return true;
1821
+ }
1822
+ function reclaimDeadOwner(indexPath, lockPath, expectedOwner) {
1823
+ const reclaimPath = path7.join(lockPath, RECLAIM_DIRECTORY_NAME);
1824
+ const reclaimOwner = {
1825
+ pid: process.pid,
1826
+ hostname: os2.hostname(),
1827
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1828
+ token: randomUUID(),
1829
+ expectedOwnerToken: expectedOwner.token
1830
+ };
1831
+ for (let attempt = 0; attempt < 2; attempt += 1) {
1832
+ if (publishJsonDirectory(reclaimPath, reclaimOwner)) break;
1833
+ if (attempt === 0 && removeDeadReclaimMarker(lockPath, expectedOwner)) continue;
1834
+ return false;
1835
+ }
1836
+ try {
1837
+ const currentReclaimer = readReclaimOwner(reclaimPath);
1838
+ const currentOwner = readDirectoryOwner(lockPath);
1839
+ if (!currentReclaimer || !sameReclaimOwner(currentReclaimer, reclaimOwner) || !currentOwner || !sameOwner(currentOwner, expectedOwner) || getOwnerLiveness(currentOwner) !== "dead") {
1840
+ return false;
1841
+ }
1842
+ publishRecoveryMarker(indexPath, expectedOwner);
1843
+ const ownerBeforeQuarantine = readDirectoryOwner(lockPath);
1844
+ const reclaimerBeforeQuarantine = readReclaimOwner(reclaimPath);
1845
+ if (!ownerBeforeQuarantine || !sameOwner(ownerBeforeQuarantine, expectedOwner) || getOwnerLiveness(ownerBeforeQuarantine) !== "dead" || !reclaimerBeforeQuarantine || !sameReclaimOwner(reclaimerBeforeQuarantine, reclaimOwner)) {
1846
+ return false;
1847
+ }
1848
+ const quarantinePath = `${lockPath}.stale.${expectedOwner.token}.${reclaimOwner.token}`;
1849
+ renameSync(lockPath, quarantinePath);
1850
+ rmSync(quarantinePath, { recursive: true, force: true });
1851
+ return true;
1852
+ } catch (error) {
1853
+ if (getErrorCode(error) === "ENOENT") return false;
1854
+ throw error;
1855
+ }
1856
+ }
1857
+ var IndexLockContentionError = class extends Error {
1858
+ constructor(lockPath, owner, reason) {
1859
+ const ownerDescription = owner ? `PID ${owner.pid} on ${owner.hostname}, operation ${owner.operation}, since ${owner.startedAt}` : "an unreadable owner";
1860
+ super(`Index mutation already in progress: ${ownerDescription}`);
1861
+ this.lockPath = lockPath;
1862
+ this.owner = owner;
1863
+ this.reason = reason;
1864
+ this.name = "IndexLockContentionError";
1865
+ }
1866
+ lockPath;
1867
+ owner;
1868
+ reason;
1869
+ code = "INDEX_BUSY";
1870
+ };
1871
+ function isIndexLockContentionError(error) {
1872
+ return error instanceof IndexLockContentionError || typeof error === "object" && error !== null && "code" in error && error.code === "INDEX_BUSY";
1873
+ }
1874
+ function isTransientIndexLockContention(error) {
1875
+ if (!isIndexLockContentionError(error) || !("reason" in error)) return false;
1876
+ return error.reason === "active" || error.reason === "reclaiming";
1877
+ }
1878
+ function acquireIndexLock(indexPath, operation) {
1879
+ mkdirSync2(indexPath, { recursive: true });
1880
+ const canonicalIndexPath = realpathSync.native(indexPath);
1881
+ const lockPath = path7.join(canonicalIndexPath, "indexing.lock");
1882
+ cleanupDeadPublicationCandidates(canonicalIndexPath);
1883
+ for (let attempt = 0; attempt < 6; attempt += 1) {
1884
+ const owner = createOwner(operation);
1885
+ if (publishJsonDirectory(lockPath, owner)) {
1886
+ const lease = {
1887
+ canonicalIndexPath,
1888
+ lockPath,
1889
+ owner,
1890
+ recoveries: []
1891
+ };
1892
+ try {
1893
+ lease.recoveries = getPendingRecoveries(canonicalIndexPath);
1894
+ return lease;
1895
+ } catch (error) {
1896
+ releaseIndexLock(lease);
1897
+ throw error;
1898
+ }
1899
+ }
1900
+ let stats;
1901
+ try {
1902
+ stats = lstatSync(lockPath);
1903
+ } catch (error) {
1904
+ if (getErrorCode(error) === "ENOENT") continue;
1905
+ throw error;
1906
+ }
1907
+ if (!stats.isDirectory()) {
1908
+ const legacyOwner = readLegacyOwner(lockPath);
1909
+ throw new IndexLockContentionError(lockPath, legacyOwner, "legacy-lock");
1910
+ }
1911
+ const existingOwner = readDirectoryOwner(lockPath);
1912
+ if (!existingOwner) {
1913
+ throw new IndexLockContentionError(lockPath, null, "unknown-owner");
1914
+ }
1915
+ const liveness = getOwnerLiveness(existingOwner);
1916
+ if (liveness !== "dead") {
1917
+ throw new IndexLockContentionError(lockPath, existingOwner, liveness === "alive" ? "active" : "unknown-owner");
1918
+ }
1919
+ if (!reclaimDeadOwner(canonicalIndexPath, lockPath, existingOwner)) {
1920
+ throw new IndexLockContentionError(lockPath, existingOwner, "reclaiming");
1921
+ }
1922
+ }
1923
+ throw new IndexLockContentionError(lockPath, null, "reclaiming");
1924
+ }
1925
+ function releaseIndexLock(lease) {
1926
+ const currentOwner = readDirectoryOwner(lease.lockPath);
1927
+ if (!currentOwner || !sameOwner(currentOwner, lease.owner)) return false;
1928
+ const releasePath = `${lease.lockPath}.release.${lease.owner.pid}.${lease.owner.token}`;
1929
+ try {
1930
+ retryTransientFilesystemOperation(() => renameSync(lease.lockPath, releasePath));
1931
+ } catch (error) {
1932
+ if (getErrorCode(error) === "ENOENT") return false;
1933
+ throw error;
1934
+ }
1935
+ const claimedOwner = readDirectoryOwner(releasePath);
1936
+ if (!claimedOwner || !sameOwner(claimedOwner, lease.owner)) {
1937
+ if (!existsSync5(lease.lockPath) && existsSync5(releasePath)) {
1938
+ renameSync(releasePath, lease.lockPath);
1939
+ }
1940
+ return false;
1941
+ }
1942
+ try {
1943
+ retryTransientFilesystemOperation(() => rmSync(releasePath, { recursive: true, force: true }));
1944
+ } catch (error) {
1945
+ console.error(`[codebase-index] Lease released but tombstone cleanup failed: ${releasePath}`, error);
1946
+ }
1947
+ return true;
1948
+ }
1949
+ async function withIndexLock(indexPath, operation, callback, options = {}) {
1950
+ const lease = acquireIndexLock(indexPath, operation);
1951
+ let result;
1952
+ let callbackError;
1953
+ let callbackFailed = false;
1954
+ try {
1955
+ result = await callback(lease);
1956
+ } catch (error) {
1957
+ callbackFailed = true;
1958
+ callbackError = error;
1959
+ }
1960
+ if (!callbackFailed && options.completeRecoveries !== false) {
1961
+ try {
1962
+ completeLeaseRecovery(lease);
1963
+ } catch (error) {
1964
+ callbackFailed = true;
1965
+ callbackError = error;
1966
+ }
1967
+ }
1968
+ let releaseError;
1969
+ try {
1970
+ if (!releaseIndexLock(lease)) {
1971
+ releaseError = new Error(`Lost ownership of index mutation lease ${lease.owner.token}`);
1972
+ }
1973
+ } catch (error) {
1974
+ releaseError = error;
1975
+ }
1976
+ if (releaseError !== void 0) {
1977
+ if (callbackFailed) throw new AggregateError([callbackError, releaseError], "Index mutation and lease release both failed");
1978
+ throw releaseError;
1979
+ }
1980
+ if (callbackFailed) throw callbackError;
1981
+ return result;
1982
+ }
1983
+ function createLeaseTemporaryPath(targetPath, owner, kind = "tmp") {
1984
+ if (kind === "bak") return `${targetPath}.bak.${owner.pid}.${owner.token}`;
1985
+ temporaryCounter += 1;
1986
+ return `${targetPath}.tmp.${owner.pid}.${owner.token}.${temporaryCounter}`;
1987
+ }
1988
+ function removeLeaseTemporaryPath(temporaryPath) {
1989
+ if (existsSync5(temporaryPath)) rmSync(temporaryPath, { recursive: true, force: true });
1990
+ }
1991
+ function recoverLeaseArtifacts(indexPath, owner, backupTargets) {
1992
+ for (const targetPath of backupTargets) {
1993
+ const backupPath = createLeaseTemporaryPath(targetPath, owner, "bak");
1994
+ if (!existsSync5(backupPath)) continue;
1995
+ if (existsSync5(targetPath)) rmSync(targetPath, { recursive: true, force: true });
1996
+ renameSync(backupPath, targetPath);
1997
+ }
1998
+ const temporaryOwnerMarker = `.tmp.${owner.pid}.${owner.token}.`;
1999
+ for (const entry of readdirSync2(indexPath)) {
2000
+ if (!entry.includes(temporaryOwnerMarker)) continue;
2001
+ rmSync(path7.join(indexPath, entry), { recursive: true, force: true });
2002
+ }
2003
+ }
2004
+ function completeLeaseRecovery(lease) {
2005
+ for (const recovery of lease.recoveries) {
2006
+ const markerOwner = readRecoveryOwner(recovery.markerPath);
2007
+ if (!markerOwner || !sameOwner(markerOwner, recovery.owner)) {
2008
+ throw new Error(`Recovery marker ownership changed: ${recovery.markerPath}`);
2009
+ }
2010
+ }
2011
+ for (const recovery of lease.recoveries) {
2012
+ rmSync(recovery.markerPath, { recursive: true, force: true });
2013
+ }
2014
+ }
2015
+
1580
2016
  // src/tools/operations.ts
1581
- import { existsSync as existsSync9, realpathSync, statSync as statSync3 } from "fs";
1582
- import * as path14 from "path";
2017
+ import { existsSync as existsSync10, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
2018
+ import * as path16 from "path";
1583
2019
 
1584
2020
  // src/indexer/index.ts
1585
- import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync2, renameSync, unlinkSync, mkdirSync as mkdirSync2, promises as fsPromises2 } from "fs";
1586
- import * as path11 from "path";
2021
+ import { existsSync as existsSync8, readFileSync as readFileSync7, statSync as statSync3, writeFileSync as writeFileSync3, renameSync as renameSync2, unlinkSync, mkdirSync as mkdirSync3, promises as fsPromises2 } from "fs";
2022
+ import * as path13 from "path";
1587
2023
  import { performance as performance2 } from "perf_hooks";
1588
- import { execFile as execFile2 } from "child_process";
1589
- import { promisify as promisify2 } from "util";
2024
+ import { execFile as execFile3 } from "child_process";
2025
+ import { promisify as promisify3 } from "util";
1590
2026
 
1591
2027
  // node_modules/eventemitter3/index.mjs
1592
2028
  var import_index = __toESM(require_eventemitter3(), 1);
@@ -2633,17 +3069,17 @@ async function pRetry(input, options = {}) {
2633
3069
  }
2634
3070
 
2635
3071
  // src/embeddings/detector.ts
2636
- import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
2637
- import * as path7 from "path";
2638
- import * as os2 from "os";
3072
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
3073
+ import * as path8 from "path";
3074
+ import * as os3 from "os";
2639
3075
  function getOpenCodeAuthPath() {
2640
- return path7.join(os2.homedir(), ".local", "share", "opencode", "auth.json");
3076
+ return path8.join(os3.homedir(), ".local", "share", "opencode", "auth.json");
2641
3077
  }
2642
3078
  function loadOpenCodeAuth() {
2643
3079
  const authPath = getOpenCodeAuthPath();
2644
3080
  try {
2645
- if (existsSync5(authPath)) {
2646
- return JSON.parse(readFileSync4(authPath, "utf-8"));
3081
+ if (existsSync6(authPath)) {
3082
+ return JSON.parse(readFileSync5(authPath, "utf-8"));
2647
3083
  }
2648
3084
  } catch {
2649
3085
  }
@@ -2865,17 +3301,17 @@ function validateExternalUrl(urlString) {
2865
3301
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
2866
3302
  return { valid: false, reason: `Blocked protocol: ${parsed.protocol}` };
2867
3303
  }
2868
- const hostname = parsed.hostname.toLowerCase();
2869
- if (BLOCKED_HOSTNAMES.has(hostname)) {
2870
- return { valid: false, reason: `Blocked: cloud metadata service (${hostname})` };
3304
+ const hostname2 = parsed.hostname.toLowerCase();
3305
+ if (BLOCKED_HOSTNAMES.has(hostname2)) {
3306
+ return { valid: false, reason: `Blocked: cloud metadata service (${hostname2})` };
2871
3307
  }
2872
3308
  for (const pattern of BLOCKED_METADATA_IPS) {
2873
- if (pattern.test(hostname)) {
2874
- return { valid: false, reason: `Blocked: cloud metadata IP (${hostname})` };
3309
+ if (pattern.test(hostname2)) {
3310
+ return { valid: false, reason: `Blocked: cloud metadata IP (${hostname2})` };
2875
3311
  }
2876
3312
  }
2877
- if (/^169\.254\./.test(hostname)) {
2878
- return { valid: false, reason: `Blocked: link-local address (${hostname})` };
3313
+ if (/^169\.254\./.test(hostname2)) {
3314
+ return { valid: false, reason: `Blocked: link-local address (${hostname2})` };
2879
3315
  }
2880
3316
  return { valid: true };
2881
3317
  }
@@ -3346,8 +3782,8 @@ var SiliconFlowReranker = class {
3346
3782
 
3347
3783
  // src/utils/files.ts
3348
3784
  var import_ignore = __toESM(require_ignore(), 1);
3349
- import { existsSync as existsSync6, readFileSync as readFileSync5, promises as fsPromises } from "fs";
3350
- import * as path8 from "path";
3785
+ import { existsSync as existsSync7, readFileSync as readFileSync6, promises as fsPromises } from "fs";
3786
+ import * as path9 from "path";
3351
3787
  var PROJECT_MARKERS = [
3352
3788
  ".git",
3353
3789
  "package.json",
@@ -3367,7 +3803,7 @@ var PROJECT_MARKERS = [
3367
3803
  ];
3368
3804
  function hasProjectMarker(projectRoot) {
3369
3805
  for (const marker of PROJECT_MARKERS) {
3370
- if (existsSync6(path8.join(projectRoot, marker))) {
3806
+ if (existsSync7(path9.join(projectRoot, marker))) {
3371
3807
  return true;
3372
3808
  }
3373
3809
  }
@@ -3394,16 +3830,16 @@ function createIgnoreFilter(projectRoot) {
3394
3830
  "**/*build*/**"
3395
3831
  ];
3396
3832
  ig.add(defaultIgnores);
3397
- const gitignorePath = path8.join(projectRoot, ".gitignore");
3398
- if (existsSync6(gitignorePath)) {
3399
- const gitignoreContent = readFileSync5(gitignorePath, "utf-8");
3833
+ const gitignorePath = path9.join(projectRoot, ".gitignore");
3834
+ if (existsSync7(gitignorePath)) {
3835
+ const gitignoreContent = readFileSync6(gitignorePath, "utf-8");
3400
3836
  ig.add(gitignoreContent);
3401
3837
  }
3402
3838
  return ig;
3403
3839
  }
3404
3840
  function shouldIncludeFile(filePath, projectRoot, includePatterns, excludePatterns, ignoreFilter) {
3405
- const relativePath = path8.relative(projectRoot, filePath);
3406
- if (hasFilteredPathSegment(relativePath, path8.sep)) {
3841
+ const relativePath = path9.relative(projectRoot, filePath);
3842
+ if (hasFilteredPathSegment(relativePath, path9.sep)) {
3407
3843
  return false;
3408
3844
  }
3409
3845
  if (ignoreFilter.ignores(relativePath)) {
@@ -3441,8 +3877,8 @@ async function* walkDirectory(dir, projectRoot, includePatterns, excludePatterns
3441
3877
  const filesInDir = [];
3442
3878
  const subdirs = [];
3443
3879
  for (const entry of entries) {
3444
- const fullPath = path8.join(dir, entry.name);
3445
- const relativePath = path8.relative(projectRoot, fullPath);
3880
+ const fullPath = path9.join(dir, entry.name);
3881
+ const relativePath = path9.relative(projectRoot, fullPath);
3446
3882
  if (isHiddenPathSegment(entry.name)) {
3447
3883
  if (entry.isDirectory()) {
3448
3884
  skipped.push({ path: relativePath, reason: "excluded" });
@@ -3491,7 +3927,7 @@ async function* walkDirectory(dir, projectRoot, includePatterns, excludePatterns
3491
3927
  yield f;
3492
3928
  }
3493
3929
  for (let i = options.maxFilesPerDirectory; i < filesInDir.length; i++) {
3494
- skipped.push({ path: path8.relative(projectRoot, filesInDir[i].path), reason: "excluded" });
3930
+ skipped.push({ path: path9.relative(projectRoot, filesInDir[i].path), reason: "excluded" });
3495
3931
  }
3496
3932
  const canRecurse = options.maxDepth === -1 || currentDepth < options.maxDepth;
3497
3933
  if (canRecurse) {
@@ -3531,8 +3967,8 @@ async function collectFiles(projectRoot, includePatterns, excludePatterns, maxFi
3531
3967
  if (additionalRoots && additionalRoots.length > 0) {
3532
3968
  const normalizedRoots = /* @__PURE__ */ new Set();
3533
3969
  for (const kbRoot of additionalRoots) {
3534
- const resolved = path8.normalize(
3535
- path8.isAbsolute(kbRoot) ? kbRoot : path8.resolve(projectRoot, kbRoot)
3970
+ const resolved = path9.normalize(
3971
+ path9.isAbsolute(kbRoot) ? kbRoot : path9.resolve(projectRoot, kbRoot)
3536
3972
  );
3537
3973
  normalizedRoots.add(resolved);
3538
3974
  }
@@ -3939,13 +4375,13 @@ function initializeLogger(config) {
3939
4375
  }
3940
4376
 
3941
4377
  // src/native/index.ts
3942
- import * as path9 from "path";
3943
- import * as os3 from "os";
4378
+ import * as path10 from "path";
4379
+ import * as os4 from "os";
3944
4380
  import * as module from "module";
3945
4381
  import { fileURLToPath } from "url";
3946
4382
  function getNativeBinding() {
3947
- const platform2 = os3.platform();
3948
- const arch2 = os3.arch();
4383
+ const platform2 = os4.platform();
4384
+ const arch2 = os4.arch();
3949
4385
  let bindingName;
3950
4386
  if (platform2 === "darwin" && arch2 === "arm64") {
3951
4387
  bindingName = "codebase-index-native.darwin-arm64.node";
@@ -3963,19 +4399,19 @@ function getNativeBinding() {
3963
4399
  let currentDir;
3964
4400
  let requireTarget;
3965
4401
  if (typeof import.meta !== "undefined" && import.meta.url) {
3966
- currentDir = path9.dirname(fileURLToPath(import.meta.url));
4402
+ currentDir = path10.dirname(fileURLToPath(import.meta.url));
3967
4403
  requireTarget = import.meta.url;
3968
4404
  } else if (typeof __dirname !== "undefined") {
3969
4405
  currentDir = __dirname;
3970
4406
  requireTarget = __filename;
3971
4407
  } else {
3972
4408
  currentDir = process.cwd();
3973
- requireTarget = path9.join(currentDir, "index.js");
4409
+ requireTarget = path10.join(currentDir, "index.js");
3974
4410
  }
3975
4411
  const normalizedDir = currentDir.replace(/\\/g, "/");
3976
- const isDevMode = normalizedDir.includes("/src/native") || currentDir.includes(path9.join("src", "native"));
3977
- const packageRoot = isDevMode ? path9.resolve(currentDir, "../..") : path9.resolve(currentDir, "..");
3978
- const nativePath = path9.join(packageRoot, "native", bindingName);
4412
+ const isDevMode = normalizedDir.includes("/src/native") || currentDir.includes(path10.join("src", "native"));
4413
+ const packageRoot = isDevMode ? path10.resolve(currentDir, "../..") : path10.resolve(currentDir, "..");
4414
+ const nativePath = path10.join(packageRoot, "native", bindingName);
3979
4415
  const require2 = module.createRequire(requireTarget);
3980
4416
  return require2(nativePath);
3981
4417
  }
@@ -4017,6 +4453,12 @@ function createMockNativeBinding() {
4017
4453
  constructor() {
4018
4454
  throw error;
4019
4455
  }
4456
+ static openReadOnly() {
4457
+ throw error;
4458
+ }
4459
+ static createEmptyReadOnly() {
4460
+ throw error;
4461
+ }
4020
4462
  close() {
4021
4463
  throw error;
4022
4464
  }
@@ -4120,6 +4562,12 @@ var VectorStore = class {
4120
4562
  load() {
4121
4563
  this.inner.load();
4122
4564
  }
4565
+ loadStrict() {
4566
+ this.inner.loadStrict();
4567
+ }
4568
+ hasFingerprint() {
4569
+ return this.inner.hasFingerprint();
4570
+ }
4123
4571
  count() {
4124
4572
  return this.inner.count();
4125
4573
  }
@@ -4402,12 +4850,24 @@ var InvertedIndex = class {
4402
4850
  return this.inner.documentCount();
4403
4851
  }
4404
4852
  };
4405
- var Database = class {
4853
+ var Database = class _Database {
4406
4854
  inner;
4407
4855
  closed = false;
4408
4856
  constructor(dbPath) {
4409
4857
  this.inner = new native.Database(dbPath);
4410
4858
  }
4859
+ static fromNative(inner) {
4860
+ const database = Object.create(_Database.prototype);
4861
+ database.inner = inner;
4862
+ database.closed = false;
4863
+ return database;
4864
+ }
4865
+ static openReadOnly(dbPath) {
4866
+ return _Database.fromNative(native.Database.openReadOnly(dbPath));
4867
+ }
4868
+ static createEmptyReadOnly() {
4869
+ return _Database.fromNative(native.Database.createEmptyReadOnly());
4870
+ }
4411
4871
  throwIfClosed() {
4412
4872
  if (this.closed) {
4413
4873
  throw new Error("Database is closed");
@@ -4684,7 +5144,7 @@ var Database = class {
4684
5144
  };
4685
5145
 
4686
5146
  // src/tools/changed-files.ts
4687
- import * as path10 from "path";
5147
+ import * as path11 from "path";
4688
5148
  import { execFile } from "child_process";
4689
5149
  import { promisify } from "util";
4690
5150
  var execFileAsync = promisify(execFile);
@@ -4772,9 +5232,9 @@ function normalizeFiles(rawFiles, projectRoot) {
4772
5232
  for (const raw of rawFiles) {
4773
5233
  const trimmed = raw.trim();
4774
5234
  if (!trimmed) continue;
4775
- const absolute = path10.resolve(projectRoot, trimmed);
4776
- const relative10 = path10.relative(projectRoot, absolute);
4777
- const cleaned = relative10.startsWith("./") ? relative10.slice(2) : relative10;
5235
+ const absolute = path11.resolve(projectRoot, trimmed);
5236
+ const relative11 = path11.relative(projectRoot, absolute);
5237
+ const cleaned = relative11.startsWith("./") ? relative11.slice(2) : relative11;
4778
5238
  if (!seen.has(cleaned)) {
4779
5239
  seen.add(cleaned);
4780
5240
  result.push(cleaned);
@@ -4783,8 +5243,61 @@ function normalizeFiles(rawFiles, projectRoot) {
4783
5243
  return result;
4784
5244
  }
4785
5245
 
5246
+ // src/indexer/git-blame.ts
5247
+ import { execFile as execFile2 } from "child_process";
5248
+ import * as path12 from "path";
5249
+ import { promisify as promisify2 } from "util";
5250
+ var execFileAsync2 = promisify2(execFile2);
5251
+ function parseGitBlamePorcelain(output) {
5252
+ const commits = /* @__PURE__ */ new Map();
5253
+ let current;
5254
+ for (const line of output.split("\n")) {
5255
+ if (/^[0-9a-f]{40} /.test(line)) {
5256
+ const sha = line.slice(0, 40);
5257
+ current = commits.get(sha) ?? {
5258
+ sha,
5259
+ author: "",
5260
+ authorEmail: "",
5261
+ committedAt: 0,
5262
+ summary: "",
5263
+ lines: 0
5264
+ };
5265
+ commits.set(sha, current);
5266
+ continue;
5267
+ }
5268
+ if (!current) {
5269
+ continue;
5270
+ }
5271
+ if (line.startsWith("author ")) {
5272
+ current.author = line.slice("author ".length);
5273
+ } else if (line.startsWith("author-mail ")) {
5274
+ current.authorEmail = line.slice("author-mail ".length).replace(/^<|>$/g, "");
5275
+ } else if (line.startsWith("author-time ")) {
5276
+ current.committedAt = Number.parseInt(line.slice("author-time ".length), 10);
5277
+ } else if (line.startsWith("summary ")) {
5278
+ current.summary = line.slice("summary ".length);
5279
+ } else if (line.startsWith(" ")) {
5280
+ current.lines += 1;
5281
+ }
5282
+ }
5283
+ return Array.from(commits.values()).filter((commit) => commit.lines > 0).sort((a, b) => b.lines - a.lines || b.committedAt - a.committedAt)[0];
5284
+ }
5285
+ async function getChunkGitBlame(projectRoot, filePath, startLine, endLine) {
5286
+ const relativePath = path12.relative(projectRoot, filePath);
5287
+ try {
5288
+ const { stdout } = await execFileAsync2(
5289
+ "git",
5290
+ ["blame", "--line-porcelain", "-L", `${startLine},${endLine}`, "--", relativePath],
5291
+ { cwd: projectRoot, timeout: 3e4 }
5292
+ );
5293
+ return parseGitBlamePorcelain(stdout);
5294
+ } catch {
5295
+ return void 0;
5296
+ }
5297
+ }
5298
+
4786
5299
  // src/indexer/index.ts
4787
- var CALL_GRAPH_LANGUAGES = /* @__PURE__ */ new Set(["typescript", "tsx", "javascript", "jsx", "python", "go", "rust", "php", "apex", "zig", "gdscript", "matlab"]);
5300
+ var CALL_GRAPH_LANGUAGES = /* @__PURE__ */ new Set(["typescript", "tsx", "javascript", "jsx", "python", "go", "rust", "php", "apex", "zig", "gdscript", "matlab", "bash"]);
4788
5301
  var CASE_INSENSITIVE_LANGUAGES = /* @__PURE__ */ new Set(["apex"]);
4789
5302
  var CALL_GRAPH_SYMBOL_CHUNK_TYPES = /* @__PURE__ */ new Set([
4790
5303
  "function_declaration",
@@ -4864,6 +5377,46 @@ function isSqliteCorruptionError(error) {
4864
5377
  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");
4865
5378
  }
4866
5379
  var STARTUP_WARNING_METADATA_KEY = "index.startupWarning";
5380
+ var READER_ARTIFACT_RETRY_INTERVAL_MS = 1e3;
5381
+ function metadataFromBlame(blame) {
5382
+ if (!blame) {
5383
+ return {};
5384
+ }
5385
+ return {
5386
+ blameSha: blame.sha,
5387
+ blameAuthor: blame.author,
5388
+ blameAuthorEmail: blame.authorEmail,
5389
+ blameCommittedAt: blame.committedAt,
5390
+ blameSummary: blame.summary
5391
+ };
5392
+ }
5393
+ function blameFromChunkData(chunk) {
5394
+ if (!chunk?.blameSha || !chunk.blameAuthor || !chunk.blameAuthorEmail || chunk.blameCommittedAt === void 0 || !chunk.blameSummary) {
5395
+ return void 0;
5396
+ }
5397
+ return {
5398
+ sha: chunk.blameSha,
5399
+ author: chunk.blameAuthor,
5400
+ authorEmail: chunk.blameAuthorEmail,
5401
+ committedAt: chunk.blameCommittedAt,
5402
+ summary: chunk.blameSummary
5403
+ };
5404
+ }
5405
+ function blameFromMetadata(metadata) {
5406
+ if (!metadata.blameSha || !metadata.blameAuthor || !metadata.blameAuthorEmail || metadata.blameCommittedAt === void 0 || !metadata.blameSummary) {
5407
+ return void 0;
5408
+ }
5409
+ return {
5410
+ sha: metadata.blameSha,
5411
+ author: metadata.blameAuthor,
5412
+ authorEmail: metadata.blameAuthorEmail,
5413
+ committedAt: metadata.blameCommittedAt,
5414
+ summary: metadata.blameSummary
5415
+ };
5416
+ }
5417
+ function hasBlameMetadata(metadata) {
5418
+ return blameFromMetadata(metadata) !== void 0;
5419
+ }
4867
5420
  var INDEX_METADATA_VERSION = "1";
4868
5421
  var EMBEDDING_STRATEGY_VERSION = "2";
4869
5422
  var RANKING_TOKEN_CACHE_LIMIT = 4096;
@@ -5022,9 +5575,9 @@ function hasAllEmbeddingParts(parts, expectedPartCount) {
5022
5575
  return true;
5023
5576
  }
5024
5577
  function isPathWithinRoot(filePath, rootPath) {
5025
- const normalizedFilePath = path11.resolve(filePath);
5026
- const normalizedRoot = path11.resolve(rootPath);
5027
- return normalizedFilePath === normalizedRoot || normalizedFilePath.startsWith(`${normalizedRoot}${path11.sep}`);
5578
+ const normalizedFilePath = path13.resolve(filePath);
5579
+ const normalizedRoot = path13.resolve(rootPath);
5580
+ return normalizedFilePath === normalizedRoot || normalizedFilePath.startsWith(`${normalizedRoot}${path13.sep}`);
5028
5581
  }
5029
5582
  var rankingQueryTokenCache = /* @__PURE__ */ new Map();
5030
5583
  var rankingNameTokenCache = /* @__PURE__ */ new Map();
@@ -5783,7 +6336,8 @@ function promoteIdentifierMatches(query, combined, semanticCandidates, keywordCa
5783
6336
  chunkType,
5784
6337
  name: chunk.name ?? void 0,
5785
6338
  language: chunk.language,
5786
- hash: chunk.contentHash
6339
+ hash: chunk.contentHash,
6340
+ ...metadataFromBlame(blameFromChunkData(chunk))
5787
6341
  };
5788
6342
  const baselineScore = existing?.score ?? 0.5;
5789
6343
  candidateUnion.set(chunk.chunkId, {
@@ -5867,7 +6421,8 @@ function buildSymbolDefinitionLane(query, database, branchChunkIds, limit, fallb
5867
6421
  chunkType,
5868
6422
  name: chunk.name ?? void 0,
5869
6423
  language: chunk.language,
5870
- hash: chunk.contentHash
6424
+ hash: chunk.contentHash,
6425
+ ...metadataFromBlame(blameFromChunkData(chunk))
5871
6426
  }
5872
6427
  });
5873
6428
  }
@@ -6036,6 +6591,21 @@ function matchesSearchFilters(candidate, options, minScore) {
6036
6591
  if (options?.chunkType && candidate.metadata.chunkType !== options.chunkType) {
6037
6592
  return false;
6038
6593
  }
6594
+ if (options?.blameAuthor) {
6595
+ const author = options.blameAuthor.toLowerCase();
6596
+ const candidateAuthor = candidate.metadata.blameAuthor?.toLowerCase();
6597
+ const candidateEmail = candidate.metadata.blameAuthorEmail?.toLowerCase();
6598
+ if (candidateAuthor !== author && candidateEmail !== author) return false;
6599
+ }
6600
+ if (options?.blameSha && !candidate.metadata.blameSha?.toLowerCase().startsWith(options.blameSha.toLowerCase())) {
6601
+ return false;
6602
+ }
6603
+ if (options?.blameSince) {
6604
+ const sinceMs = Date.parse(options.blameSince);
6605
+ if (Number.isNaN(sinceMs)) return false;
6606
+ const committedAt = candidate.metadata.blameCommittedAt;
6607
+ if (committedAt === void 0 || committedAt < Math.floor(sinceMs / 1e3)) return false;
6608
+ }
6039
6609
  return true;
6040
6610
  }
6041
6611
  function unionCandidates(semanticCandidates, keywordCandidates) {
@@ -6073,26 +6643,133 @@ var Indexer = class {
6073
6643
  queryCacheTtlMs = 5 * 60 * 1e3;
6074
6644
  querySimilarityThreshold = 0.85;
6075
6645
  indexCompatibility = null;
6076
- indexingLockPath = "";
6646
+ activeIndexLease = null;
6647
+ initializationPromise = null;
6648
+ initializationMode = "none";
6649
+ readIssues = [];
6650
+ retiredDatabases = [];
6651
+ readerArtifactFingerprint = null;
6652
+ writerArtifactFingerprint = null;
6653
+ readerArtifactRetryAfter = /* @__PURE__ */ new Map();
6077
6654
  constructor(projectRoot, config, host = "opencode") {
6078
6655
  this.projectRoot = projectRoot;
6079
6656
  this.config = config;
6080
6657
  this.host = host;
6081
6658
  this.indexPath = this.getIndexPath();
6082
- this.fileHashCachePath = path11.join(this.indexPath, "file-hashes.json");
6083
- this.failedBatchesPath = path11.join(this.indexPath, "failed-batches.json");
6084
- this.indexingLockPath = path11.join(this.indexPath, "indexing.lock");
6659
+ this.fileHashCachePath = path13.join(this.indexPath, "file-hashes.json");
6660
+ this.failedBatchesPath = path13.join(this.indexPath, "failed-batches.json");
6085
6661
  this.logger = initializeLogger(config.debug);
6086
6662
  }
6087
6663
  getIndexPath() {
6088
6664
  return resolveProjectIndexPath(this.projectRoot, this.config.scope, this.host);
6089
6665
  }
6666
+ isLocalProjectIndexPath() {
6667
+ const localProjectIndexPaths = [path13.join(this.projectRoot, getHostProjectIndexRelativePath(this.host))];
6668
+ if (this.host !== "opencode") {
6669
+ localProjectIndexPaths.push(path13.join(this.projectRoot, getHostProjectIndexRelativePath("opencode")));
6670
+ }
6671
+ return localProjectIndexPaths.some((localPath) => {
6672
+ if (!existsSync8(localPath) || !existsSync8(this.indexPath)) {
6673
+ return path13.resolve(this.indexPath) === path13.resolve(localPath);
6674
+ }
6675
+ const indexStats = statSync3(this.indexPath);
6676
+ const localStats = statSync3(localPath);
6677
+ return indexStats.dev === localStats.dev && indexStats.ino === localStats.ino;
6678
+ });
6679
+ }
6680
+ resetLoadedIndexState(retireDatabase = false) {
6681
+ if (this.database) {
6682
+ if (retireDatabase) {
6683
+ this.retiredDatabases.push(this.database);
6684
+ } else {
6685
+ this.database.close();
6686
+ }
6687
+ }
6688
+ this.store = null;
6689
+ this.invertedIndex = null;
6690
+ this.database = null;
6691
+ this.provider = null;
6692
+ this.configuredProviderInfo = null;
6693
+ this.reranker = null;
6694
+ this.indexCompatibility = null;
6695
+ this.initializationMode = "none";
6696
+ this.readIssues = [];
6697
+ this.readerArtifactFingerprint = null;
6698
+ this.writerArtifactFingerprint = null;
6699
+ this.readerArtifactRetryAfter.clear();
6700
+ this.fileHashCache.clear();
6701
+ }
6702
+ refreshLoadedIndexState() {
6703
+ if (!this.store || !this.invertedIndex || !this.configuredProviderInfo) return;
6704
+ this.store.load();
6705
+ this.invertedIndex.load();
6706
+ this.fileHashCache.clear();
6707
+ this.loadFileHashCache();
6708
+ this.indexCompatibility = this.validateIndexCompatibility(this.configuredProviderInfo);
6709
+ this.readIssues = [];
6710
+ this.readerArtifactRetryAfter.clear();
6711
+ }
6712
+ async withIndexMutationLease(operation, callback) {
6713
+ const lease = acquireIndexLock(this.indexPath, operation);
6714
+ this.indexPath = lease.canonicalIndexPath;
6715
+ this.fileHashCachePath = path13.join(this.indexPath, "file-hashes.json");
6716
+ this.failedBatchesPath = path13.join(this.indexPath, "failed-batches.json");
6717
+ this.activeIndexLease = lease;
6718
+ let result;
6719
+ let callbackError;
6720
+ let callbackFailed = false;
6721
+ try {
6722
+ result = await callback(lease.recoveries.map(({ owner }) => owner));
6723
+ } catch (error) {
6724
+ callbackFailed = true;
6725
+ callbackError = error;
6726
+ }
6727
+ if (!callbackFailed) {
6728
+ try {
6729
+ completeLeaseRecovery(lease);
6730
+ this.writerArtifactFingerprint = this.captureReaderArtifactFingerprint();
6731
+ } catch (error) {
6732
+ callbackFailed = true;
6733
+ callbackError = error;
6734
+ }
6735
+ }
6736
+ let releaseError;
6737
+ try {
6738
+ if (!releaseIndexLock(lease)) {
6739
+ releaseError = new Error(`Lost ownership of index mutation lease ${lease.owner.token}`);
6740
+ this.writerArtifactFingerprint = null;
6741
+ if (this.activeIndexLease?.owner.token === lease.owner.token) {
6742
+ this.activeIndexLease = null;
6743
+ }
6744
+ } else if (this.activeIndexLease?.owner.token === lease.owner.token) {
6745
+ this.activeIndexLease = null;
6746
+ }
6747
+ } catch (error) {
6748
+ releaseError = error;
6749
+ this.writerArtifactFingerprint = null;
6750
+ if (!existsSync8(lease.lockPath) && this.activeIndexLease?.owner.token === lease.owner.token) {
6751
+ this.activeIndexLease = null;
6752
+ }
6753
+ }
6754
+ if (releaseError !== void 0) {
6755
+ if (callbackFailed) throw new AggregateError([callbackError, releaseError], "Index mutation and lease release both failed");
6756
+ throw releaseError;
6757
+ }
6758
+ if (callbackFailed) throw callbackError;
6759
+ return result;
6760
+ }
6761
+ requireActiveLease() {
6762
+ if (!this.activeIndexLease) {
6763
+ throw new Error("Index mutation attempted without an active interprocess lease");
6764
+ }
6765
+ return this.activeIndexLease;
6766
+ }
6090
6767
  loadFileHashCache() {
6091
- if (!existsSync7(this.fileHashCachePath)) {
6768
+ if (!existsSync8(this.fileHashCachePath)) {
6092
6769
  return;
6093
6770
  }
6094
6771
  try {
6095
- const data = readFileSync6(this.fileHashCachePath, "utf-8");
6772
+ const data = readFileSync7(this.fileHashCachePath, "utf-8");
6096
6773
  const parsed = JSON.parse(data);
6097
6774
  this.fileHashCache = new Map(Object.entries(parsed));
6098
6775
  } catch (error) {
@@ -6112,15 +6789,26 @@ var Indexer = class {
6112
6789
  this.atomicWriteSync(this.fileHashCachePath, JSON.stringify(obj));
6113
6790
  }
6114
6791
  atomicWriteSync(targetPath, data) {
6115
- const tempPath = `${targetPath}.tmp`;
6116
- mkdirSync2(path11.dirname(targetPath), { recursive: true });
6117
- writeFileSync2(tempPath, data);
6118
- renameSync(tempPath, targetPath);
6792
+ const lease = this.requireActiveLease();
6793
+ const tempPath = createLeaseTemporaryPath(targetPath, lease.owner, "tmp");
6794
+ mkdirSync3(path13.dirname(targetPath), { recursive: true });
6795
+ try {
6796
+ writeFileSync3(tempPath, data);
6797
+ renameSync2(tempPath, targetPath);
6798
+ } finally {
6799
+ removeLeaseTemporaryPath(tempPath);
6800
+ }
6801
+ }
6802
+ saveInvertedIndex(invertedIndex) {
6803
+ this.atomicWriteSync(
6804
+ path13.join(this.indexPath, "inverted-index.json"),
6805
+ invertedIndex.serialize()
6806
+ );
6119
6807
  }
6120
6808
  getScopedRoots() {
6121
- const roots = /* @__PURE__ */ new Set([path11.resolve(this.projectRoot)]);
6809
+ const roots = /* @__PURE__ */ new Set([path13.resolve(this.projectRoot)]);
6122
6810
  for (const kbRoot of this.config.knowledgeBases) {
6123
- roots.add(path11.resolve(this.projectRoot, kbRoot));
6811
+ roots.add(path13.resolve(this.projectRoot, kbRoot));
6124
6812
  }
6125
6813
  return Array.from(roots);
6126
6814
  }
@@ -6129,29 +6817,29 @@ var Indexer = class {
6129
6817
  if (this.config.scope !== "global") {
6130
6818
  return branchName;
6131
6819
  }
6132
- const projectHash = hashContent(path11.resolve(this.projectRoot)).slice(0, 16);
6820
+ const projectHash = hashContent(path13.resolve(this.projectRoot)).slice(0, 16);
6133
6821
  return `${projectHash}:${branchName}`;
6134
6822
  }
6135
6823
  getBranchCatalogKeyFor(branchName) {
6136
6824
  if (this.config.scope !== "global") {
6137
6825
  return branchName;
6138
6826
  }
6139
- const projectHash = hashContent(path11.resolve(this.projectRoot)).slice(0, 16);
6827
+ const projectHash = hashContent(path13.resolve(this.projectRoot)).slice(0, 16);
6140
6828
  return `${projectHash}:${branchName}`;
6141
6829
  }
6142
6830
  getLegacyBranchCatalogKey() {
6143
6831
  return this.currentBranch || "default";
6144
6832
  }
6145
6833
  getLegacyMigrationMetadataKey() {
6146
- const projectHash = hashContent(path11.resolve(this.projectRoot)).slice(0, 16);
6834
+ const projectHash = hashContent(path13.resolve(this.projectRoot)).slice(0, 16);
6147
6835
  return `index.globalBranchMigration.${projectHash}`;
6148
6836
  }
6149
6837
  getProjectEmbeddingStrategyMetadataKey() {
6150
- const projectHash = hashContent(path11.resolve(this.projectRoot)).slice(0, 16);
6838
+ const projectHash = hashContent(path13.resolve(this.projectRoot)).slice(0, 16);
6151
6839
  return `index.embeddingStrategyVersion.${projectHash}`;
6152
6840
  }
6153
6841
  getProjectForceReembedMetadataKey() {
6154
- const projectHash = hashContent(path11.resolve(this.projectRoot)).slice(0, 16);
6842
+ const projectHash = hashContent(path13.resolve(this.projectRoot)).slice(0, 16);
6155
6843
  return `index.forceReembed.${projectHash}`;
6156
6844
  }
6157
6845
  hasProjectForceReembedPending() {
@@ -6245,7 +6933,7 @@ var Indexer = class {
6245
6933
  if (!this.database) {
6246
6934
  return { chunkIds, symbolIds };
6247
6935
  }
6248
- const projectRootPath = path11.resolve(this.projectRoot);
6936
+ const projectRootPath = path13.resolve(this.projectRoot);
6249
6937
  const projectLocalFilePaths = /* @__PURE__ */ new Set([
6250
6938
  ...Array.from(this.fileHashCache.keys()).filter(
6251
6939
  (filePath) => this.isFileInCurrentScope(filePath, roots) && isPathWithinRoot(filePath, projectRootPath)
@@ -6268,7 +6956,7 @@ var Indexer = class {
6268
6956
  if (this.config.scope !== "global") {
6269
6957
  return this.getBranchCatalogCleanupKeys();
6270
6958
  }
6271
- const projectHash = hashContent(path11.resolve(this.projectRoot)).slice(0, 16);
6959
+ const projectHash = hashContent(path13.resolve(this.projectRoot)).slice(0, 16);
6272
6960
  const keys = /* @__PURE__ */ new Set();
6273
6961
  const projectChunkIdSet = new Set(projectChunkIds);
6274
6962
  const projectSymbolIdSet = new Set(projectSymbolIds);
@@ -6352,7 +7040,7 @@ var Indexer = class {
6352
7040
  if (!this.database || this.config.scope !== "global") {
6353
7041
  return false;
6354
7042
  }
6355
- const projectHash = hashContent(path11.resolve(this.projectRoot)).slice(0, 16);
7043
+ const projectHash = hashContent(path13.resolve(this.projectRoot)).slice(0, 16);
6356
7044
  const roots = this.getScopedRoots();
6357
7045
  const { chunkIds: projectLocalChunkIds, symbolIds: projectLocalSymbolIds } = this.getProjectLocalScopedOwnershipIds(roots);
6358
7046
  return this.database.getAllBranches().some(
@@ -6386,7 +7074,7 @@ var Indexer = class {
6386
7074
  ...Array.from(this.fileHashCache.keys()).filter((filePath) => this.isFileInCurrentScope(filePath, roots)),
6387
7075
  ...scopedEntries.map(({ metadata }) => metadata.filePath)
6388
7076
  ]);
6389
- const projectRootPath = path11.resolve(this.projectRoot);
7077
+ const projectRootPath = path13.resolve(this.projectRoot);
6390
7078
  const projectLocalFilePaths = new Set(
6391
7079
  Array.from(filePaths).filter((filePath) => isPathWithinRoot(filePath, projectRootPath))
6392
7080
  );
@@ -6448,34 +7136,27 @@ var Indexer = class {
6448
7136
  database.gcOrphanEmbeddings();
6449
7137
  database.gcOrphanChunks();
6450
7138
  store.save();
6451
- invertedIndex.save();
7139
+ this.saveInvertedIndex(invertedIndex);
6452
7140
  return {
6453
7141
  removedChunkIds: removedChunkIdList,
6454
7142
  hasForeignData: allMetadata.some(({ metadata }) => !this.isFileInCurrentScope(metadata.filePath, roots))
6455
7143
  };
6456
7144
  }
6457
- checkForInterruptedIndexing() {
6458
- return existsSync7(this.indexingLockPath);
6459
- }
6460
- acquireIndexingLock() {
6461
- const lockData = {
6462
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
6463
- pid: process.pid
6464
- };
6465
- writeFileSync2(this.indexingLockPath, JSON.stringify(lockData));
6466
- }
6467
- releaseIndexingLock() {
6468
- if (existsSync7(this.indexingLockPath)) {
6469
- unlinkSync(this.indexingLockPath);
7145
+ async recoverFromInterruptedIndexingUnlocked(owners) {
7146
+ for (const owner of owners) {
7147
+ this.logger.warn("Detected interrupted indexing session, recovering...", {
7148
+ pid: owner.pid,
7149
+ hostname: owner.hostname,
7150
+ operation: owner.operation,
7151
+ startedAt: owner.startedAt
7152
+ });
6470
7153
  }
6471
- }
6472
- async recoverFromInterruptedIndexing() {
6473
- this.logger.warn("Detected interrupted indexing session, recovering...");
6474
- if (existsSync7(this.fileHashCachePath)) {
6475
- unlinkSync(this.fileHashCachePath);
7154
+ if (this.config.scope === "global") {
7155
+ if (existsSync8(this.fileHashCachePath)) {
7156
+ unlinkSync(this.fileHashCachePath);
7157
+ }
7158
+ await this.healthCheckUnlocked();
6476
7159
  }
6477
- await this.healthCheck();
6478
- this.releaseIndexingLock();
6479
7160
  this.logger.info("Recovery complete, next index will re-process all files");
6480
7161
  }
6481
7162
  loadFailedBatches(maxChunkTokens) {
@@ -6491,10 +7172,10 @@ var Indexer = class {
6491
7172
  }
6492
7173
  }
6493
7174
  loadSerializedFailedBatches() {
6494
- if (!existsSync7(this.failedBatchesPath)) {
7175
+ if (!existsSync8(this.failedBatchesPath)) {
6495
7176
  return [];
6496
7177
  }
6497
- const data = readFileSync6(this.failedBatchesPath, "utf-8");
7178
+ const data = readFileSync7(this.failedBatchesPath, "utf-8");
6498
7179
  const parsed = JSON.parse(data);
6499
7180
  return parsed.map((batch) => {
6500
7181
  const chunks = Array.isArray(batch.chunks) ? batch.chunks : [];
@@ -6511,7 +7192,7 @@ var Indexer = class {
6511
7192
  }
6512
7193
  saveFailedBatches(batches) {
6513
7194
  if (batches.length === 0) {
6514
- if (existsSync7(this.failedBatchesPath)) {
7195
+ if (existsSync8(this.failedBatchesPath)) {
6515
7196
  try {
6516
7197
  unlinkSync(this.failedBatchesPath);
6517
7198
  } catch {
@@ -6519,7 +7200,7 @@ var Indexer = class {
6519
7200
  }
6520
7201
  return;
6521
7202
  }
6522
- writeFileSync2(this.failedBatchesPath, JSON.stringify(batches, null, 2));
7203
+ this.atomicWriteSync(this.failedBatchesPath, JSON.stringify(batches, null, 2));
6523
7204
  }
6524
7205
  collectRetryableFailedChunks(currentFileHashes, unchangedFilePaths, maxChunkTokens) {
6525
7206
  const retryableById = /* @__PURE__ */ new Map();
@@ -6713,15 +7394,231 @@ var Indexer = class {
6713
7394
  return parts.join("\n");
6714
7395
  }
6715
7396
  async initialize() {
6716
- if (this.config.embeddingProvider === "custom") {
6717
- if (!this.config.customProvider) {
6718
- throw new Error("embeddingProvider is 'custom' but customProvider config is missing.");
7397
+ if (this.initializationPromise) {
7398
+ await this.initializationPromise;
7399
+ }
7400
+ if (this.isInitializedFor("reader")) {
7401
+ return;
7402
+ }
7403
+ await this.initializeOnce("reader", [], { skipAutoGc: true });
7404
+ }
7405
+ async initializeOnce(mode, recoveredOwners, options) {
7406
+ if (this.initializationPromise) {
7407
+ await this.initializationPromise;
7408
+ if (this.isInitializedFor(mode)) {
7409
+ return;
6719
7410
  }
6720
- this.configuredProviderInfo = createCustomProviderInfo(this.config.customProvider);
6721
- } else if (this.config.embeddingProvider === "auto") {
6722
- this.configuredProviderInfo = await tryDetectProvider();
6723
- } else {
6724
- this.configuredProviderInfo = await detectEmbeddingProvider(this.config.embeddingProvider, this.config.embeddingModel);
7411
+ return this.initializeOnce(mode, recoveredOwners, options);
7412
+ }
7413
+ if (this.isInitializedFor(mode)) {
7414
+ return;
7415
+ }
7416
+ const initialization = this.initializeUnlocked(mode, recoveredOwners, options).catch((error) => {
7417
+ this.resetLoadedIndexState();
7418
+ throw error;
7419
+ }).finally(() => {
7420
+ if (this.initializationPromise === initialization) {
7421
+ this.initializationPromise = null;
7422
+ }
7423
+ });
7424
+ this.initializationPromise = initialization;
7425
+ await initialization;
7426
+ }
7427
+ isInitializedFor(mode) {
7428
+ const hasState = Boolean(
7429
+ this.store && this.provider && this.invertedIndex && this.configuredProviderInfo && this.database
7430
+ );
7431
+ if (!hasState) {
7432
+ return false;
7433
+ }
7434
+ return mode === "reader" ? this.initializationMode !== "none" : this.initializationMode === "writer";
7435
+ }
7436
+ recordReadIssue(component, message, error) {
7437
+ this.readIssues.push(this.createReadIssue(component, message));
7438
+ this.readerArtifactRetryAfter.set(component, Date.now() + READER_ARTIFACT_RETRY_INTERVAL_MS);
7439
+ this.logger.warn(message, error === void 0 ? void 0 : { error: getErrorMessage2(error) });
7440
+ }
7441
+ createReadIssue(component, message) {
7442
+ return {
7443
+ component,
7444
+ message,
7445
+ blocking: component !== "keyword"
7446
+ };
7447
+ }
7448
+ getVectorReadIssueMessage() {
7449
+ if (this.config.scope === "global") {
7450
+ return "Shared vector index could not be read. Restore or repair the complete fingerprinted shared vector artifacts; automatic reset is disabled for global scope.";
7451
+ }
7452
+ if (!this.isLocalProjectIndexPath()) {
7453
+ 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.";
7454
+ }
7455
+ 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.";
7456
+ }
7457
+ getKeywordReadIssueMessage() {
7458
+ if (this.config.scope === "global") {
7459
+ 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.";
7460
+ }
7461
+ if (!this.isLocalProjectIndexPath()) {
7462
+ 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.";
7463
+ }
7464
+ 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.";
7465
+ }
7466
+ getDatabaseReadIssueMessage() {
7467
+ if (this.config.scope === "global") {
7468
+ return "Shared index database could not be read. Restore or repair the shared SQLite database; automatic reset is disabled for global scope.";
7469
+ }
7470
+ if (!this.isLocalProjectIndexPath()) {
7471
+ 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.";
7472
+ }
7473
+ return "Index database could not be read. Run index_codebase after the active writer finishes to repair or migrate it under the writer lease.";
7474
+ }
7475
+ getReaderFileFingerprint(filePath, identityOnly = false) {
7476
+ try {
7477
+ const stats = statSync3(filePath);
7478
+ if (identityOnly) {
7479
+ return `${stats.dev}:${stats.ino}`;
7480
+ }
7481
+ return `${stats.dev}:${stats.ino}:${stats.size}:${stats.mtimeMs}:${stats.ctimeMs}`;
7482
+ } catch (error) {
7483
+ return `unavailable:${getErrorMessage2(error)}`;
7484
+ }
7485
+ }
7486
+ captureReaderArtifactFingerprint() {
7487
+ const storePath = path13.join(this.indexPath, "vectors");
7488
+ return {
7489
+ vectors: `${this.getReaderFileFingerprint(storePath)}|${this.getReaderFileFingerprint(`${storePath}.meta.json`)}`,
7490
+ keyword: this.getReaderFileFingerprint(path13.join(this.indexPath, "inverted-index.json")),
7491
+ database: this.getReaderFileFingerprint(path13.join(this.indexPath, "codebase.db")),
7492
+ databaseIdentity: this.getReaderFileFingerprint(path13.join(this.indexPath, "codebase.db"), true)
7493
+ };
7494
+ }
7495
+ refreshReaderArtifacts() {
7496
+ if (this.initializationMode !== "reader" || !this.configuredProviderInfo) {
7497
+ return;
7498
+ }
7499
+ const previousFingerprint = this.readerArtifactFingerprint;
7500
+ const currentFingerprint = this.captureReaderArtifactFingerprint();
7501
+ const issues = new Map(this.readIssues.map((issue) => [issue.component, issue]));
7502
+ const retryDue = (component) => issues.has(component) && Date.now() >= (this.readerArtifactRetryAfter.get(component) ?? 0);
7503
+ const vectorsChanged = !previousFingerprint || currentFingerprint.vectors !== previousFingerprint.vectors;
7504
+ const keywordChanged = !previousFingerprint || currentFingerprint.keyword !== previousFingerprint.keyword;
7505
+ const databaseChanged = !previousFingerprint || currentFingerprint.database !== previousFingerprint.database;
7506
+ const databaseReplaced = !previousFingerprint || currentFingerprint.databaseIdentity !== previousFingerprint.databaseIdentity;
7507
+ if (previousFingerprint && !vectorsChanged && !keywordChanged && !databaseChanged && !Array.from(issues.keys()).some(retryDue)) {
7508
+ return;
7509
+ }
7510
+ const setIssue = (component, message, error) => {
7511
+ if (!issues.has(component)) {
7512
+ this.logger.warn(message, error === void 0 ? void 0 : { error: getErrorMessage2(error) });
7513
+ }
7514
+ issues.set(component, this.createReadIssue(component, message));
7515
+ this.readerArtifactRetryAfter.set(component, Date.now() + READER_ARTIFACT_RETRY_INTERVAL_MS);
7516
+ };
7517
+ const storePath = path13.join(this.indexPath, "vectors");
7518
+ const vectorMetadataPath = `${storePath}.meta.json`;
7519
+ const invertedIndexPath = path13.join(this.indexPath, "inverted-index.json");
7520
+ const dbPath = path13.join(this.indexPath, "codebase.db");
7521
+ if (vectorsChanged || retryDue("vectors")) {
7522
+ const vectorStoreExists = existsSync8(storePath);
7523
+ const vectorMetadataExists = existsSync8(vectorMetadataPath);
7524
+ if (vectorStoreExists && vectorMetadataExists) {
7525
+ try {
7526
+ const store = new VectorStore(storePath, this.configuredProviderInfo.modelInfo.dimensions);
7527
+ store.loadStrict();
7528
+ this.store = store;
7529
+ issues.delete("vectors");
7530
+ this.readerArtifactRetryAfter.delete("vectors");
7531
+ } catch (error) {
7532
+ setIssue("vectors", this.getVectorReadIssueMessage(), error);
7533
+ }
7534
+ } else if (vectorStoreExists !== vectorMetadataExists || issues.has("vectors")) {
7535
+ setIssue("vectors", this.getVectorReadIssueMessage());
7536
+ }
7537
+ }
7538
+ if (keywordChanged || retryDue("keyword") || !existsSync8(invertedIndexPath) && (this.store?.count() ?? 0) > 0) {
7539
+ if (existsSync8(invertedIndexPath)) {
7540
+ try {
7541
+ const invertedIndex = new InvertedIndex(invertedIndexPath);
7542
+ invertedIndex.load();
7543
+ this.invertedIndex = invertedIndex;
7544
+ issues.delete("keyword");
7545
+ this.readerArtifactRetryAfter.delete("keyword");
7546
+ } catch (error) {
7547
+ setIssue("keyword", this.getKeywordReadIssueMessage(), error);
7548
+ }
7549
+ } else if ((this.store?.count() ?? 0) > 0 || issues.has("keyword")) {
7550
+ setIssue("keyword", this.getKeywordReadIssueMessage());
7551
+ }
7552
+ }
7553
+ if (databaseReplaced || databaseChanged && issues.has("database") || retryDue("database")) {
7554
+ if (existsSync8(dbPath)) {
7555
+ try {
7556
+ const database = Database.openReadOnly(dbPath);
7557
+ if (this.database) {
7558
+ this.retiredDatabases.push(this.database);
7559
+ }
7560
+ this.database = database;
7561
+ issues.delete("database");
7562
+ this.readerArtifactRetryAfter.delete("database");
7563
+ } catch (error) {
7564
+ setIssue("database", this.getDatabaseReadIssueMessage(), error);
7565
+ }
7566
+ } else if ((this.store?.count() ?? 0) > 0 || issues.has("database")) {
7567
+ setIssue("database", this.getDatabaseReadIssueMessage());
7568
+ }
7569
+ }
7570
+ if (!issues.has("database")) {
7571
+ try {
7572
+ this.indexCompatibility = this.validateIndexCompatibility(this.configuredProviderInfo);
7573
+ } catch (error) {
7574
+ setIssue("database", this.getDatabaseReadIssueMessage(), error);
7575
+ }
7576
+ }
7577
+ this.readIssues = Array.from(issues.values());
7578
+ this.readerArtifactFingerprint = currentFingerprint;
7579
+ }
7580
+ refreshInactiveWriterArtifacts() {
7581
+ if (this.initializationMode !== "writer" || this.activeIndexLease) {
7582
+ return true;
7583
+ }
7584
+ const previousFingerprint = this.writerArtifactFingerprint;
7585
+ const currentFingerprint = this.captureReaderArtifactFingerprint();
7586
+ const retryDue = this.readIssues.some(
7587
+ (issue) => Date.now() >= (this.readerArtifactRetryAfter.get(issue.component) ?? 0)
7588
+ );
7589
+ const artifactsChanged = !previousFingerprint || currentFingerprint.vectors !== previousFingerprint.vectors || currentFingerprint.keyword !== previousFingerprint.keyword || currentFingerprint.database !== previousFingerprint.database || currentFingerprint.databaseIdentity !== previousFingerprint.databaseIdentity;
7590
+ if (!artifactsChanged && !retryDue) {
7591
+ return true;
7592
+ }
7593
+ if (!previousFingerprint || currentFingerprint.databaseIdentity !== previousFingerprint.databaseIdentity) {
7594
+ return false;
7595
+ }
7596
+ this.initializationMode = "reader";
7597
+ this.readerArtifactFingerprint = previousFingerprint;
7598
+ try {
7599
+ this.refreshReaderArtifacts();
7600
+ this.writerArtifactFingerprint = this.readerArtifactFingerprint ?? currentFingerprint;
7601
+ } finally {
7602
+ this.readerArtifactFingerprint = null;
7603
+ this.initializationMode = "writer";
7604
+ }
7605
+ return true;
7606
+ }
7607
+ async initializeUnlocked(mode, recoveredOwners = [], options = {}) {
7608
+ if (mode === "writer") {
7609
+ this.requireActiveLease();
7610
+ }
7611
+ this.readIssues = [];
7612
+ this.readerArtifactRetryAfter.clear();
7613
+ if (this.config.embeddingProvider === "custom") {
7614
+ if (!this.config.customProvider) {
7615
+ throw new Error("embeddingProvider is 'custom' but customProvider config is missing.");
7616
+ }
7617
+ this.configuredProviderInfo = createCustomProviderInfo(this.config.customProvider);
7618
+ } else if (this.config.embeddingProvider === "auto") {
7619
+ this.configuredProviderInfo = await tryDetectProvider();
7620
+ } else {
7621
+ this.configuredProviderInfo = await detectEmbeddingProvider(this.config.embeddingProvider, this.config.embeddingModel);
6725
7622
  }
6726
7623
  if (!this.configuredProviderInfo) {
6727
7624
  throw new Error(
@@ -6744,36 +7641,103 @@ var Indexer = class {
6744
7641
  });
6745
7642
  }
6746
7643
  }
6747
- await fsPromises2.mkdir(this.indexPath, { recursive: true });
6748
7644
  const dimensions = this.configuredProviderInfo.modelInfo.dimensions;
6749
- const storePath = path11.join(this.indexPath, "vectors");
6750
- this.store = new VectorStore(storePath, dimensions);
6751
- const indexFilePath = path11.join(this.indexPath, "vectors.usearch");
6752
- if (existsSync7(indexFilePath)) {
6753
- this.store.load();
6754
- }
6755
- const invertedIndexPath = path11.join(this.indexPath, "inverted-index.json");
6756
- this.invertedIndex = new InvertedIndex(invertedIndexPath);
6757
- try {
6758
- this.invertedIndex.load();
6759
- } catch {
6760
- if (existsSync7(invertedIndexPath)) {
6761
- await fsPromises2.unlink(invertedIndexPath);
7645
+ const storePath = path13.join(this.indexPath, "vectors");
7646
+ const vectorMetadataPath = `${storePath}.meta.json`;
7647
+ const invertedIndexPath = path13.join(this.indexPath, "inverted-index.json");
7648
+ const dbPath = path13.join(this.indexPath, "codebase.db");
7649
+ let dbIsNew = !existsSync8(dbPath);
7650
+ const readerArtifactFingerprint = mode === "reader" ? this.captureReaderArtifactFingerprint() : null;
7651
+ if (mode === "writer") {
7652
+ await fsPromises2.mkdir(this.indexPath, { recursive: true });
7653
+ if (recoveredOwners.length > 0 && this.config.scope === "project" && !this.isLocalProjectIndexPath()) {
7654
+ throw new Error(
7655
+ "Interrupted indexing recovery is unsafe while using an inherited worktree index. Run index_codebase with force=true to create a local project index boundary."
7656
+ );
7657
+ }
7658
+ for (const recoveredOwner of recoveredOwners) {
7659
+ recoverLeaseArtifacts(this.indexPath, recoveredOwner, [
7660
+ storePath,
7661
+ `${storePath}.meta.json`
7662
+ ]);
7663
+ }
7664
+ if (recoveredOwners.length > 0 && this.config.scope === "project") {
7665
+ await this.resetLocalIndexArtifacts();
7666
+ }
7667
+ this.store = new VectorStore(storePath, dimensions);
7668
+ if (existsSync8(storePath) || existsSync8(vectorMetadataPath)) {
7669
+ this.store.load();
6762
7670
  }
6763
7671
  this.invertedIndex = new InvertedIndex(invertedIndexPath);
6764
- }
6765
- const dbPath = path11.join(this.indexPath, "codebase.db");
6766
- let dbIsNew = !existsSync7(dbPath);
6767
- try {
6768
- this.database = new Database(dbPath);
6769
- } catch (error) {
6770
- if (!await this.tryResetCorruptedIndex("initializing index database", error)) {
6771
- throw error;
7672
+ try {
7673
+ this.invertedIndex.load();
7674
+ } catch {
7675
+ if (existsSync8(invertedIndexPath)) {
7676
+ await fsPromises2.unlink(invertedIndexPath);
7677
+ }
7678
+ this.invertedIndex = new InvertedIndex(invertedIndexPath);
6772
7679
  }
7680
+ try {
7681
+ this.database = new Database(dbPath);
7682
+ } catch (error) {
7683
+ if (!await this.tryResetCorruptedIndex("initializing index database", error)) {
7684
+ throw error;
7685
+ }
7686
+ this.store = new VectorStore(storePath, dimensions);
7687
+ this.invertedIndex = new InvertedIndex(invertedIndexPath);
7688
+ this.database = new Database(dbPath);
7689
+ dbIsNew = true;
7690
+ }
7691
+ } else {
6773
7692
  this.store = new VectorStore(storePath, dimensions);
7693
+ const vectorStoreExists = existsSync8(storePath);
7694
+ const vectorMetadataExists = existsSync8(vectorMetadataPath);
7695
+ const vectorReadFailureMessage = this.getVectorReadIssueMessage();
7696
+ if (vectorStoreExists !== vectorMetadataExists) {
7697
+ this.recordReadIssue("vectors", vectorReadFailureMessage);
7698
+ } else if (vectorStoreExists) {
7699
+ try {
7700
+ this.store.loadStrict();
7701
+ } catch (error) {
7702
+ this.recordReadIssue("vectors", vectorReadFailureMessage, error);
7703
+ this.store = new VectorStore(storePath, dimensions);
7704
+ }
7705
+ }
6774
7706
  this.invertedIndex = new InvertedIndex(invertedIndexPath);
6775
- this.database = new Database(dbPath);
6776
- dbIsNew = true;
7707
+ if (existsSync8(invertedIndexPath)) {
7708
+ try {
7709
+ this.invertedIndex.load();
7710
+ } catch (error) {
7711
+ this.recordReadIssue(
7712
+ "keyword",
7713
+ this.getKeywordReadIssueMessage(),
7714
+ error
7715
+ );
7716
+ this.invertedIndex = new InvertedIndex(invertedIndexPath);
7717
+ }
7718
+ } else if (this.store.count() > 0) {
7719
+ this.recordReadIssue("keyword", this.getKeywordReadIssueMessage());
7720
+ }
7721
+ if (existsSync8(dbPath)) {
7722
+ try {
7723
+ this.database = Database.openReadOnly(dbPath);
7724
+ } catch (error) {
7725
+ this.recordReadIssue(
7726
+ "database",
7727
+ this.getDatabaseReadIssueMessage(),
7728
+ error
7729
+ );
7730
+ this.database = Database.createEmptyReadOnly();
7731
+ }
7732
+ } else {
7733
+ this.database = Database.createEmptyReadOnly();
7734
+ if (this.store.count() > 0) {
7735
+ this.recordReadIssue(
7736
+ "database",
7737
+ `Index database is missing for the published vectors. ${this.getDatabaseReadIssueMessage()}`
7738
+ );
7739
+ }
7740
+ }
6777
7741
  }
6778
7742
  if (isGitRepo(this.projectRoot)) {
6779
7743
  this.currentBranch = getBranchOrDefault(this.projectRoot);
@@ -6787,10 +7751,10 @@ var Indexer = class {
6787
7751
  this.baseBranch = "default";
6788
7752
  this.logger.branch("debug", "Not a git repository, using default branch");
6789
7753
  }
6790
- if (this.checkForInterruptedIndexing()) {
6791
- await this.recoverFromInterruptedIndexing();
7754
+ if (mode === "writer" && recoveredOwners.length > 0) {
7755
+ await this.recoverFromInterruptedIndexingUnlocked(recoveredOwners);
6792
7756
  }
6793
- if (dbIsNew && this.store.count() > 0) {
7757
+ if (mode === "writer" && dbIsNew && this.store.count() > 0) {
6794
7758
  this.migrateFromLegacyIndex();
6795
7759
  }
6796
7760
  this.loadFileHashCache();
@@ -6802,9 +7766,11 @@ var Indexer = class {
6802
7766
  configuredProviderInfo: this.configuredProviderInfo
6803
7767
  });
6804
7768
  }
6805
- if (this.config.indexing.autoGc) {
7769
+ if (mode === "writer" && this.config.indexing.autoGc && !options.skipAutoGc) {
6806
7770
  await this.maybeRunAutoGc();
6807
7771
  }
7772
+ this.initializationMode = mode;
7773
+ this.readerArtifactFingerprint = readerArtifactFingerprint;
6808
7774
  }
6809
7775
  async maybeRunAutoGc() {
6810
7776
  if (!this.database) return;
@@ -6821,7 +7787,7 @@ var Indexer = class {
6821
7787
  }
6822
7788
  }
6823
7789
  if (shouldRunGc) {
6824
- const result = await this.healthCheck();
7790
+ const result = await this.healthCheckUnlocked();
6825
7791
  if (result.warning) {
6826
7792
  this.database.setMetadata(STARTUP_WARNING_METADATA_KEY, result.warning);
6827
7793
  } else {
@@ -6843,7 +7809,7 @@ var Indexer = class {
6843
7809
  if (await this.tryResetCorruptedIndex("running automatic orphan garbage collection", error)) {
6844
7810
  return {
6845
7811
  resetCorruptedIndex: true,
6846
- warning: this.getCorruptedIndexWarning(path11.join(this.indexPath, "codebase.db"))
7812
+ warning: this.getCorruptedIndexWarning(path13.join(this.indexPath, "codebase.db"))
6847
7813
  };
6848
7814
  }
6849
7815
  throw error;
@@ -6858,28 +7824,29 @@ var Indexer = class {
6858
7824
  return;
6859
7825
  }
6860
7826
  const retainedEntries = store.getAllMetadata().filter(({ key }) => !excludedSet.has(key));
6861
- const storeBasePath = path11.join(this.indexPath, "vectors");
6862
- const storeIndexPath = `${storeBasePath}.usearch`;
7827
+ const storeBasePath = path13.join(this.indexPath, "vectors");
7828
+ const storeIndexPath = storeBasePath;
6863
7829
  const storeMetadataPath = `${storeBasePath}.meta.json`;
6864
- const backupIndexPath = `${storeIndexPath}.bak`;
6865
- const backupMetadataPath = `${storeMetadataPath}.bak`;
7830
+ const lease = this.requireActiveLease();
7831
+ const backupIndexPath = createLeaseTemporaryPath(storeIndexPath, lease.owner, "bak");
7832
+ const backupMetadataPath = createLeaseTemporaryPath(storeMetadataPath, lease.owner, "bak");
6866
7833
  let backedUpIndex = false;
6867
7834
  let backedUpMetadata = false;
6868
7835
  let rebuiltCount = 0;
6869
7836
  let skippedCount = 0;
6870
- if (existsSync7(backupIndexPath)) {
7837
+ if (existsSync8(backupIndexPath)) {
6871
7838
  unlinkSync(backupIndexPath);
6872
7839
  }
6873
- if (existsSync7(backupMetadataPath)) {
7840
+ if (existsSync8(backupMetadataPath)) {
6874
7841
  unlinkSync(backupMetadataPath);
6875
7842
  }
6876
7843
  try {
6877
- if (existsSync7(storeIndexPath)) {
6878
- renameSync(storeIndexPath, backupIndexPath);
7844
+ if (existsSync8(storeIndexPath)) {
7845
+ renameSync2(storeIndexPath, backupIndexPath);
6879
7846
  backedUpIndex = true;
6880
7847
  }
6881
- if (existsSync7(storeMetadataPath)) {
6882
- renameSync(storeMetadataPath, backupMetadataPath);
7848
+ if (existsSync8(storeMetadataPath)) {
7849
+ renameSync2(storeMetadataPath, backupMetadataPath);
6883
7850
  backedUpMetadata = true;
6884
7851
  }
6885
7852
  store.clear();
@@ -6899,10 +7866,10 @@ var Indexer = class {
6899
7866
  rebuiltCount += 1;
6900
7867
  }
6901
7868
  store.save();
6902
- if (backedUpIndex && existsSync7(backupIndexPath)) {
7869
+ if (backedUpIndex && existsSync8(backupIndexPath)) {
6903
7870
  unlinkSync(backupIndexPath);
6904
7871
  }
6905
- if (backedUpMetadata && existsSync7(backupMetadataPath)) {
7872
+ if (backedUpMetadata && existsSync8(backupMetadataPath)) {
6906
7873
  unlinkSync(backupMetadataPath);
6907
7874
  }
6908
7875
  this.logger.gc("info", "Rebuilt vector store to avoid native remove", {
@@ -6915,17 +7882,17 @@ var Indexer = class {
6915
7882
  store.clear();
6916
7883
  } catch {
6917
7884
  }
6918
- if (existsSync7(storeIndexPath)) {
7885
+ if (existsSync8(storeIndexPath)) {
6919
7886
  unlinkSync(storeIndexPath);
6920
7887
  }
6921
- if (existsSync7(storeMetadataPath)) {
7888
+ if (existsSync8(storeMetadataPath)) {
6922
7889
  unlinkSync(storeMetadataPath);
6923
7890
  }
6924
- if (backedUpIndex && existsSync7(backupIndexPath)) {
6925
- renameSync(backupIndexPath, storeIndexPath);
7891
+ if (backedUpIndex && existsSync8(backupIndexPath)) {
7892
+ renameSync2(backupIndexPath, storeIndexPath);
6926
7893
  }
6927
- if (backedUpMetadata && existsSync7(backupMetadataPath)) {
6928
- renameSync(backupMetadataPath, storeMetadataPath);
7894
+ if (backedUpMetadata && existsSync8(backupMetadataPath)) {
7895
+ renameSync2(backupMetadataPath, storeMetadataPath);
6929
7896
  }
6930
7897
  if (backedUpIndex || backedUpMetadata) {
6931
7898
  store.load();
@@ -6939,11 +7906,37 @@ var Indexer = class {
6939
7906
  }
6940
7907
  return `Detected a corrupted local SQLite index at ${dbPath} and reset the local index. Run index_codebase to rebuild search data.`;
6941
7908
  }
7909
+ async resetLocalIndexArtifacts() {
7910
+ this.store = null;
7911
+ this.invertedIndex = null;
7912
+ this.database?.close();
7913
+ this.database = null;
7914
+ this.indexCompatibility = null;
7915
+ this.initializationMode = "none";
7916
+ this.readIssues = [];
7917
+ this.readerArtifactFingerprint = null;
7918
+ this.writerArtifactFingerprint = null;
7919
+ this.readerArtifactRetryAfter.clear();
7920
+ this.fileHashCache.clear();
7921
+ const resetPaths = [
7922
+ path13.join(this.indexPath, "codebase.db"),
7923
+ path13.join(this.indexPath, "codebase.db-shm"),
7924
+ path13.join(this.indexPath, "codebase.db-wal"),
7925
+ path13.join(this.indexPath, "vectors"),
7926
+ path13.join(this.indexPath, "vectors.usearch"),
7927
+ path13.join(this.indexPath, "vectors.meta.json"),
7928
+ path13.join(this.indexPath, "inverted-index.json"),
7929
+ path13.join(this.indexPath, "file-hashes.json"),
7930
+ path13.join(this.indexPath, "failed-batches.json")
7931
+ ];
7932
+ await Promise.all(resetPaths.map((targetPath) => fsPromises2.rm(targetPath, { recursive: true, force: true })));
7933
+ await fsPromises2.mkdir(this.indexPath, { recursive: true });
7934
+ }
6942
7935
  async tryResetCorruptedIndex(stage, error) {
6943
7936
  if (!isSqliteCorruptionError(error)) {
6944
7937
  return false;
6945
7938
  }
6946
- const dbPath = path11.join(this.indexPath, "codebase.db");
7939
+ const dbPath = path13.join(this.indexPath, "codebase.db");
6947
7940
  const warning = this.getCorruptedIndexWarning(dbPath);
6948
7941
  const errorMessage = getErrorMessage2(error);
6949
7942
  if (this.config.scope === "global") {
@@ -6959,30 +7952,7 @@ var Indexer = class {
6959
7952
  dbPath,
6960
7953
  error: errorMessage
6961
7954
  });
6962
- this.store = null;
6963
- this.invertedIndex = null;
6964
- this.database?.close();
6965
- this.database = null;
6966
- this.indexCompatibility = null;
6967
- this.fileHashCache.clear();
6968
- const resetPaths = [
6969
- path11.join(this.indexPath, "codebase.db"),
6970
- path11.join(this.indexPath, "codebase.db-shm"),
6971
- path11.join(this.indexPath, "codebase.db-wal"),
6972
- path11.join(this.indexPath, "vectors.usearch"),
6973
- path11.join(this.indexPath, "inverted-index.json"),
6974
- path11.join(this.indexPath, "file-hashes.json"),
6975
- path11.join(this.indexPath, "failed-batches.json"),
6976
- path11.join(this.indexPath, "indexing.lock"),
6977
- path11.join(this.indexPath, "vectors")
6978
- ];
6979
- await Promise.all(resetPaths.map(async (targetPath) => {
6980
- try {
6981
- await fsPromises2.rm(targetPath, { recursive: true, force: true });
6982
- } catch {
6983
- }
6984
- }));
6985
- await fsPromises2.mkdir(this.indexPath, { recursive: true });
7955
+ await this.resetLocalIndexArtifacts();
6986
7956
  return true;
6987
7957
  }
6988
7958
  migrateFromLegacyIndex() {
@@ -7101,8 +8071,62 @@ var Indexer = class {
7101
8071
  return this.indexCompatibility;
7102
8072
  }
7103
8073
  async ensureInitialized() {
8074
+ let initializedReader = false;
8075
+ while (true) {
8076
+ if (this.initializationPromise) {
8077
+ await this.initializationPromise;
8078
+ }
8079
+ if (!this.isInitializedFor("reader")) {
8080
+ await this.initialize();
8081
+ initializedReader = true;
8082
+ continue;
8083
+ }
8084
+ if (this.initializationMode === "writer" && !this.activeIndexLease && !initializedReader) {
8085
+ if (!this.refreshInactiveWriterArtifacts()) {
8086
+ this.resetLoadedIndexState(true);
8087
+ await this.initialize();
8088
+ initializedReader = true;
8089
+ continue;
8090
+ }
8091
+ }
8092
+ if (this.initializationMode === "reader" && !initializedReader) {
8093
+ this.refreshReaderArtifacts();
8094
+ }
8095
+ const state = this.requireLoadedIndexState();
8096
+ return {
8097
+ ...state,
8098
+ readIssues: [...this.readIssues],
8099
+ compatibility: this.indexCompatibility ?? this.validateIndexCompatibility(state.configuredProviderInfo)
8100
+ };
8101
+ }
8102
+ }
8103
+ async ensureInitializedUnlocked(recoveredOwners = []) {
8104
+ this.requireActiveLease();
8105
+ if (this.initializationPromise) {
8106
+ await this.initializationPromise;
8107
+ }
8108
+ if (recoveredOwners.length > 0 || !this.isInitializedFor("writer")) {
8109
+ const retireReaderDatabase = this.initializationMode === "reader";
8110
+ this.resetLoadedIndexState(retireReaderDatabase);
8111
+ await this.initializeOnce("writer", recoveredOwners, { skipAutoGc: true });
8112
+ } else {
8113
+ this.refreshLoadedIndexState();
8114
+ }
8115
+ if (this.config.indexing.autoGc) {
8116
+ await this.maybeRunAutoGc();
8117
+ }
8118
+ return this.requireLoadedIndexState();
8119
+ }
8120
+ requireReadableComponents(readIssues, ...components) {
8121
+ const componentSet = new Set(components);
8122
+ const issues = readIssues.filter((issue) => issue.blocking && componentSet.has(issue.component));
8123
+ if (issues.length > 0) {
8124
+ throw new Error(issues.map((issue) => issue.message).join(" "));
8125
+ }
8126
+ }
8127
+ requireLoadedIndexState() {
7104
8128
  if (!this.store || !this.provider || !this.invertedIndex || !this.configuredProviderInfo || !this.database) {
7105
- await this.initialize();
8129
+ throw new Error("Index state is not initialized");
7106
8130
  }
7107
8131
  return {
7108
8132
  store: this.store,
@@ -7126,7 +8150,12 @@ var Indexer = class {
7126
8150
  return createCostEstimate(files, configuredProviderInfo);
7127
8151
  }
7128
8152
  async index(onProgress) {
7129
- const { store, provider, invertedIndex, database, configuredProviderInfo } = await this.ensureInitialized();
8153
+ return this.withIndexMutationLease("index", async (recoveredOwners) => {
8154
+ return this.indexUnlocked(onProgress, recoveredOwners);
8155
+ });
8156
+ }
8157
+ async indexUnlocked(onProgress, recoveredOwners = [], stateReady = false) {
8158
+ const { store, provider, invertedIndex, database, configuredProviderInfo } = stateReady ? this.requireLoadedIndexState() : await this.ensureInitializedUnlocked(recoveredOwners);
7130
8159
  const scopedRoots = this.config.scope === "global" ? this.getScopedRoots() : null;
7131
8160
  const branchCatalogKey = this.getBranchCatalogKey();
7132
8161
  const forceScopedReembed = scopedRoots !== null && database.getMetadata(this.getProjectForceReembedMetadataKey()) === "true";
@@ -7136,7 +8165,6 @@ var Indexer = class {
7136
8165
  `${this.indexCompatibility?.reason} Run index_codebase with force=true to rebuild the index.`
7137
8166
  );
7138
8167
  }
7139
- this.acquireIndexingLock();
7140
8168
  this.logger.recordIndexingStart();
7141
8169
  this.logger.info("Starting indexing", { projectRoot: this.projectRoot });
7142
8170
  const startTime = Date.now();
@@ -7211,6 +8239,7 @@ var Indexer = class {
7211
8239
  this.logger.debug("Parsed changed files", { parsedCount: parsedFiles.length, parseMs: parseMs.toFixed(2) });
7212
8240
  const existingChunks = /* @__PURE__ */ new Map();
7213
8241
  const existingChunksByFile = /* @__PURE__ */ new Map();
8242
+ const existingMetadataById = /* @__PURE__ */ new Map();
7214
8243
  for (const { key, metadata } of store.getAllMetadata()) {
7215
8244
  if (scopedRoots && !this.isFileInCurrentScope(metadata.filePath, scopedRoots)) {
7216
8245
  continue;
@@ -7219,6 +8248,7 @@ var Indexer = class {
7219
8248
  continue;
7220
8249
  }
7221
8250
  existingChunks.set(key, metadata.hash);
8251
+ existingMetadataById.set(key, metadata);
7222
8252
  const fileChunks = existingChunksByFile.get(metadata.filePath) || /* @__PURE__ */ new Set();
7223
8253
  fileChunks.add(key);
7224
8254
  existingChunksByFile.set(metadata.filePath, fileChunks);
@@ -7226,6 +8256,8 @@ var Indexer = class {
7226
8256
  const currentChunkIds = /* @__PURE__ */ new Set();
7227
8257
  const currentFilePaths = /* @__PURE__ */ new Set();
7228
8258
  const pendingChunks = [];
8259
+ const gitBlameEnabled = this.config.indexing.gitBlame.enabled && isGitRepo(this.projectRoot);
8260
+ let backfilledBlameMetadata = false;
7229
8261
  for (const filePath of unchangedFilePaths) {
7230
8262
  currentFilePaths.add(filePath);
7231
8263
  const fileChunks = existingChunksByFile.get(filePath);
@@ -7236,10 +8268,52 @@ var Indexer = class {
7236
8268
  }
7237
8269
  }
7238
8270
  const chunkDataBatch = [];
8271
+ if (gitBlameEnabled) {
8272
+ const backfillItems = [];
8273
+ for (const chunkId of currentChunkIds) {
8274
+ const metadata = existingMetadataById.get(chunkId);
8275
+ if (!metadata || hasBlameMetadata(metadata)) {
8276
+ continue;
8277
+ }
8278
+ const chunk = database.getChunk(chunkId);
8279
+ if (!chunk) {
8280
+ continue;
8281
+ }
8282
+ const blame = await getChunkGitBlame(this.projectRoot, chunk.filePath, chunk.startLine, chunk.endLine);
8283
+ const blameMetadata = metadataFromBlame(blame);
8284
+ if (!blameMetadata.blameSha) {
8285
+ continue;
8286
+ }
8287
+ chunkDataBatch.push({
8288
+ ...chunk,
8289
+ blameSha: blameMetadata.blameSha,
8290
+ blameAuthor: blameMetadata.blameAuthor,
8291
+ blameAuthorEmail: blameMetadata.blameAuthorEmail,
8292
+ blameCommittedAt: blameMetadata.blameCommittedAt,
8293
+ blameSummary: blameMetadata.blameSummary
8294
+ });
8295
+ const embeddingBuffer = database.getEmbedding(chunk.contentHash);
8296
+ if (!embeddingBuffer) {
8297
+ continue;
8298
+ }
8299
+ backfillItems.push({
8300
+ id: chunkId,
8301
+ vector: Array.from(bufferToFloat32Array(embeddingBuffer)),
8302
+ metadata: {
8303
+ ...metadata,
8304
+ ...blameMetadata
8305
+ }
8306
+ });
8307
+ }
8308
+ if (backfillItems.length > 0) {
8309
+ store.addBatch(backfillItems);
8310
+ backfilledBlameMetadata = true;
8311
+ }
8312
+ }
7239
8313
  for (const parsed of parsedFiles) {
7240
8314
  currentFilePaths.add(parsed.path);
7241
8315
  if (parsed.chunks.length === 0) {
7242
- const relativePath = path11.relative(this.projectRoot, parsed.path);
8316
+ const relativePath = path13.relative(this.projectRoot, parsed.path);
7243
8317
  stats.parseFailures.push(relativePath);
7244
8318
  }
7245
8319
  let fileChunkCount = 0;
@@ -7260,6 +8334,10 @@ var Indexer = class {
7260
8334
  }
7261
8335
  const id = generateChunkId(parsed.path, chunk);
7262
8336
  const contentHash = generateChunkHash(chunk);
8337
+ const existingContentHash = existingChunks.get(id);
8338
+ const existingChunk = gitBlameEnabled ? database.getChunk(id) : null;
8339
+ const blame = gitBlameEnabled && existingContentHash !== contentHash ? await getChunkGitBlame(this.projectRoot, parsed.path, chunk.startLine, chunk.endLine) : blameFromChunkData(existingChunk);
8340
+ const blameMetadata = metadataFromBlame(blame);
7263
8341
  currentChunkIds.add(id);
7264
8342
  chunkDataBatch.push({
7265
8343
  chunkId: id,
@@ -7269,9 +8347,14 @@ var Indexer = class {
7269
8347
  endLine: chunk.endLine,
7270
8348
  nodeType: chunk.chunkType,
7271
8349
  name: chunk.name,
7272
- language: chunk.language
8350
+ language: chunk.language,
8351
+ blameSha: blameMetadata.blameSha,
8352
+ blameAuthor: blameMetadata.blameAuthor,
8353
+ blameAuthorEmail: blameMetadata.blameAuthorEmail,
8354
+ blameCommittedAt: blameMetadata.blameCommittedAt,
8355
+ blameSummary: blameMetadata.blameSummary
7273
8356
  });
7274
- if (existingChunks.get(id) === contentHash) {
8357
+ if (existingContentHash === contentHash) {
7275
8358
  fileChunkCount++;
7276
8359
  continue;
7277
8360
  }
@@ -7286,7 +8369,8 @@ var Indexer = class {
7286
8369
  chunkType: chunk.chunkType,
7287
8370
  name: chunk.name,
7288
8371
  language: chunk.language,
7289
- hash: contentHash
8372
+ hash: contentHash,
8373
+ ...blameMetadata
7290
8374
  };
7291
8375
  pendingChunks.push({
7292
8376
  id,
@@ -7429,6 +8513,11 @@ var Indexer = class {
7429
8513
  database.addChunksToBranchBatch(branchCatalogKey, Array.from(currentChunkIds));
7430
8514
  database.clearBranchSymbols(branchCatalogKey);
7431
8515
  database.addSymbolsToBranchBatch(branchCatalogKey, Array.from(allSymbolIds));
8516
+ const vectorPath = path13.join(this.indexPath, "vectors");
8517
+ const shouldFingerprintLegacyPair = !store.hasFingerprint() && existsSync8(vectorPath) && existsSync8(`${vectorPath}.meta.json`);
8518
+ if (backfilledBlameMetadata || shouldFingerprintLegacyPair) {
8519
+ store.save();
8520
+ }
7432
8521
  if (scopedRoots) {
7433
8522
  this.replaceScopedFileHashCache(currentFileHashes, scopedRoots);
7434
8523
  this.clearScopedFailedBatches(scopedRoots);
@@ -7447,7 +8536,6 @@ var Indexer = class {
7447
8536
  chunksProcessed: 0,
7448
8537
  totalChunks: 0
7449
8538
  });
7450
- this.releaseIndexingLock();
7451
8539
  return stats;
7452
8540
  }
7453
8541
  if (pendingChunks.length === 0) {
@@ -7456,7 +8544,7 @@ var Indexer = class {
7456
8544
  database.clearBranchSymbols(branchCatalogKey);
7457
8545
  database.addSymbolsToBranchBatch(branchCatalogKey, Array.from(allSymbolIds));
7458
8546
  store.save();
7459
- invertedIndex.save();
8547
+ this.saveInvertedIndex(invertedIndex);
7460
8548
  if (scopedRoots) {
7461
8549
  this.replaceScopedFileHashCache(currentFileHashes, scopedRoots);
7462
8550
  this.clearScopedFailedBatches(scopedRoots);
@@ -7475,7 +8563,6 @@ var Indexer = class {
7475
8563
  chunksProcessed: 0,
7476
8564
  totalChunks: 0
7477
8565
  });
7478
- this.releaseIndexingLock();
7479
8566
  return stats;
7480
8567
  }
7481
8568
  onProgress?.({
@@ -7706,7 +8793,7 @@ var Indexer = class {
7706
8793
  database.clearBranchSymbols(branchCatalogKey);
7707
8794
  database.addSymbolsToBranchBatch(branchCatalogKey, Array.from(allSymbolIds));
7708
8795
  store.save();
7709
- invertedIndex.save();
8796
+ this.saveInvertedIndex(invertedIndex);
7710
8797
  if (scopedRoots) {
7711
8798
  this.replaceScopedFileHashCache(currentFileHashes, scopedRoots);
7712
8799
  } else {
@@ -7758,7 +8845,6 @@ var Indexer = class {
7758
8845
  chunksProcessed: stats.indexedChunks,
7759
8846
  totalChunks: pendingChunks.length
7760
8847
  });
7761
- this.releaseIndexingLock();
7762
8848
  return stats;
7763
8849
  }
7764
8850
  async getQueryEmbedding(query, provider) {
@@ -7824,8 +8910,8 @@ var Indexer = class {
7824
8910
  return intersection / union;
7825
8911
  }
7826
8912
  async search(query, limit, options) {
7827
- const { store, provider, database } = await this.ensureInitialized();
7828
- const compatibility = this.checkCompatibility();
8913
+ const { store, provider, invertedIndex, database, readIssues, compatibility } = await this.ensureInitialized();
8914
+ this.requireReadableComponents(readIssues, "vectors", "database");
7829
8915
  if (!compatibility.compatible) {
7830
8916
  throw new Error(
7831
8917
  `${compatibility.reason ?? "Index is incompatible with current embedding provider."} A possible solution is to run index_codebase with force=true to rebuild the index.`
@@ -7839,6 +8925,7 @@ var Indexer = class {
7839
8925
  const maxResults = limit ?? this.config.search.maxResults;
7840
8926
  const hybridWeight = options?.hybridWeight ?? this.config.search.hybridWeight;
7841
8927
  const fusionStrategy = this.config.search.fusionStrategy;
8928
+ const effectiveHybridWeight = fusionStrategy === "weighted" && readIssues.some((issue) => issue.component === "keyword") ? 0 : hybridWeight;
7842
8929
  const rrfK = this.config.search.rrfK;
7843
8930
  const rerankTopN = this.config.search.rerankTopN;
7844
8931
  const filterByBranch = options?.filterByBranch ?? true;
@@ -7847,7 +8934,7 @@ var Indexer = class {
7847
8934
  this.logger.search("debug", "Starting search", {
7848
8935
  query,
7849
8936
  maxResults,
7850
- hybridWeight,
8937
+ hybridWeight: effectiveHybridWeight,
7851
8938
  fusionStrategy,
7852
8939
  rrfK,
7853
8940
  rerankTopN,
@@ -7861,7 +8948,7 @@ var Indexer = class {
7861
8948
  const semanticResults = store.search(embedding, maxResults * 4);
7862
8949
  const vectorMs = performance2.now() - vectorStartTime;
7863
8950
  const keywordStartTime = performance2.now();
7864
- const keywordResults = await this.keywordSearch(query, maxResults * 4);
8951
+ const keywordResults = await this.keywordSearch(query, maxResults * 4, store, invertedIndex);
7865
8952
  const keywordMs = performance2.now() - keywordStartTime;
7866
8953
  let branchChunkIds = null;
7867
8954
  if (filterByBranch && (this.config.scope === "global" || this.currentBranch !== "default")) {
@@ -7898,7 +8985,7 @@ var Indexer = class {
7898
8985
  rrfK,
7899
8986
  rerankTopN,
7900
8987
  limit: maxResults,
7901
- hybridWeight,
8988
+ hybridWeight: effectiveHybridWeight,
7902
8989
  prioritizeSourcePaths: sourceIntent
7903
8990
  });
7904
8991
  const rerankedCombined = await this.rerankCandidatesWithApi(query, combined, {
@@ -7992,13 +9079,13 @@ var Indexer = class {
7992
9079
  content,
7993
9080
  score: r.score,
7994
9081
  chunkType: r.metadata.chunkType,
7995
- name: r.metadata.name
9082
+ name: r.metadata.name,
9083
+ blame: blameFromMetadata(r.metadata)
7996
9084
  };
7997
9085
  })
7998
9086
  );
7999
9087
  }
8000
- async keywordSearch(query, limit) {
8001
- const { store, invertedIndex } = await this.ensureInitialized();
9088
+ async keywordSearch(query, limit, store, invertedIndex) {
8002
9089
  const scores = invertedIndex.search(query);
8003
9090
  if (scores.size === 0) {
8004
9091
  return [];
@@ -8016,24 +9103,54 @@ var Indexer = class {
8016
9103
  return results.slice(0, limit);
8017
9104
  }
8018
9105
  async getStatus() {
8019
- const { store, configuredProviderInfo, database } = await this.ensureInitialized();
9106
+ const { store, configuredProviderInfo, database, readIssues, compatibility } = await this.ensureInitialized();
8020
9107
  const failedBatchesCount = this.getFailedBatchesCount();
9108
+ const vectorCount = store.count();
9109
+ const statusReadIssues = [...readIssues];
9110
+ let startupWarning = "";
9111
+ if (!statusReadIssues.some((issue) => issue.component === "database")) {
9112
+ try {
9113
+ startupWarning = database.getMetadata(STARTUP_WARNING_METADATA_KEY) ?? "";
9114
+ } catch (error) {
9115
+ const message = this.getDatabaseReadIssueMessage();
9116
+ statusReadIssues.push(this.createReadIssue("database", message));
9117
+ if (!this.readIssues.some((issue) => issue.component === "database")) {
9118
+ this.recordReadIssue("database", message, error);
9119
+ }
9120
+ }
9121
+ }
9122
+ const readWarning = statusReadIssues.map((issue) => issue.message).join(" ");
9123
+ const warning = [readWarning, startupWarning].filter((message) => message.length > 0).join(" ");
9124
+ const hasBlockingReadIssue = statusReadIssues.some((issue) => issue.blocking);
8021
9125
  return {
8022
- indexed: store.count() > 0,
8023
- vectorCount: store.count(),
9126
+ indexed: vectorCount > 0 && !hasBlockingReadIssue,
9127
+ vectorCount,
8024
9128
  provider: configuredProviderInfo.provider,
8025
9129
  model: configuredProviderInfo.modelInfo.model,
8026
9130
  indexPath: this.indexPath,
8027
9131
  currentBranch: this.currentBranch,
8028
9132
  baseBranch: this.baseBranch,
8029
- compatibility: this.indexCompatibility,
9133
+ compatibility,
8030
9134
  failedBatchesCount,
8031
9135
  failedBatchesPath: failedBatchesCount > 0 ? this.failedBatchesPath : void 0,
8032
- warning: database.getMetadata(STARTUP_WARNING_METADATA_KEY) ?? void 0
9136
+ warning: warning || void 0
8033
9137
  };
8034
9138
  }
9139
+ async forceIndex(onProgress) {
9140
+ return this.withIndexMutationLease("force-index", async (recoveredOwners) => {
9141
+ await this.ensureInitializedUnlocked(recoveredOwners);
9142
+ await this.clearIndexUnlocked();
9143
+ return this.indexUnlocked(onProgress, [], true);
9144
+ });
9145
+ }
8035
9146
  async clearIndex() {
8036
- const { store, invertedIndex, database } = await this.ensureInitialized();
9147
+ await this.withIndexMutationLease("clear", async (recoveredOwners) => {
9148
+ await this.ensureInitializedUnlocked(recoveredOwners);
9149
+ await this.clearIndexUnlocked();
9150
+ });
9151
+ }
9152
+ async clearIndexUnlocked() {
9153
+ const { store, invertedIndex, database } = this.requireLoadedIndexState();
8037
9154
  if (this.config.scope === "global") {
8038
9155
  store.load();
8039
9156
  invertedIndex.load();
@@ -8060,7 +9177,7 @@ var Indexer = class {
8060
9177
  store.clear();
8061
9178
  store.save();
8062
9179
  invertedIndex.clear();
8063
- invertedIndex.save();
9180
+ this.saveInvertedIndex(invertedIndex);
8064
9181
  this.fileHashCache.clear();
8065
9182
  this.saveFileHashCache();
8066
9183
  database.clearAllIndexedData();
@@ -8084,14 +9201,7 @@ var Indexer = class {
8084
9201
  this.indexCompatibility = compatibility;
8085
9202
  return;
8086
9203
  }
8087
- const localProjectIndexPaths = [path11.join(this.projectRoot, getHostProjectIndexRelativePath(this.host))];
8088
- if (this.host !== "opencode") {
8089
- localProjectIndexPaths.push(path11.join(this.projectRoot, getHostProjectIndexRelativePath("opencode")));
8090
- }
8091
- const isLocalProjectIndex = localProjectIndexPaths.some(
8092
- (localPath) => path11.resolve(this.indexPath) === path11.resolve(localPath)
8093
- );
8094
- if (!isLocalProjectIndex) {
9204
+ if (!this.isLocalProjectIndexPath()) {
8095
9205
  throw new Error(
8096
9206
  "Project-scoped force rebuild is unsafe while using an inherited worktree index. Create a local project config boundary before clearing the index."
8097
9207
  );
@@ -8099,7 +9209,7 @@ var Indexer = class {
8099
9209
  store.clear();
8100
9210
  store.save();
8101
9211
  invertedIndex.clear();
8102
- invertedIndex.save();
9212
+ this.saveInvertedIndex(invertedIndex);
8103
9213
  this.fileHashCache.clear();
8104
9214
  this.saveFileHashCache();
8105
9215
  database.clearAllIndexedData();
@@ -8117,7 +9227,13 @@ var Indexer = class {
8117
9227
  this.indexCompatibility = this.validateIndexCompatibility(this.configuredProviderInfo);
8118
9228
  }
8119
9229
  async healthCheck() {
8120
- const { store, invertedIndex, database } = await this.ensureInitialized();
9230
+ return this.withIndexMutationLease("health-check", async (recoveredOwners) => {
9231
+ await this.ensureInitializedUnlocked(recoveredOwners);
9232
+ return this.healthCheckUnlocked();
9233
+ });
9234
+ }
9235
+ async healthCheckUnlocked() {
9236
+ const { store, invertedIndex, database } = this.requireLoadedIndexState();
8121
9237
  this.logger.gc("info", "Starting health check");
8122
9238
  const allMetadata = store.getAllMetadata();
8123
9239
  const filePathsToChunkKeys = /* @__PURE__ */ new Map();
@@ -8130,7 +9246,7 @@ var Indexer = class {
8130
9246
  const removedChunkKeys = [];
8131
9247
  const chunkKeysByRemovedFile = /* @__PURE__ */ new Map();
8132
9248
  for (const [filePath, chunkKeys] of filePathsToChunkKeys) {
8133
- if (!existsSync7(filePath)) {
9249
+ if (!existsSync8(filePath)) {
8134
9250
  chunkKeysByRemovedFile.set(filePath, chunkKeys);
8135
9251
  for (const key of chunkKeys) {
8136
9252
  removedChunkKeys.push(key);
@@ -8155,7 +9271,7 @@ var Indexer = class {
8155
9271
  const removedCount = removedChunkKeys.length;
8156
9272
  if (removedCount > 0) {
8157
9273
  store.save();
8158
- invertedIndex.save();
9274
+ this.saveInvertedIndex(invertedIndex);
8159
9275
  }
8160
9276
  let gcOrphanEmbeddings;
8161
9277
  let gcOrphanChunks;
@@ -8170,7 +9286,7 @@ var Indexer = class {
8170
9286
  if (!await this.tryResetCorruptedIndex("running index health check", error)) {
8171
9287
  throw error;
8172
9288
  }
8173
- await this.ensureInitialized();
9289
+ await this.initializeUnlocked("writer", [], { skipAutoGc: true });
8174
9290
  return {
8175
9291
  removed: 0,
8176
9292
  filePaths: [],
@@ -8179,7 +9295,7 @@ var Indexer = class {
8179
9295
  gcOrphanSymbols: 0,
8180
9296
  gcOrphanCallEdges: 0,
8181
9297
  resetCorruptedIndex: true,
8182
- warning: this.getCorruptedIndexWarning(path11.join(this.indexPath, "codebase.db"))
9298
+ warning: this.getCorruptedIndexWarning(path13.join(this.indexPath, "codebase.db"))
8183
9299
  };
8184
9300
  }
8185
9301
  this.logger.recordGc(removedCount, gcOrphanChunks, gcOrphanEmbeddings);
@@ -8192,7 +9308,13 @@ var Indexer = class {
8192
9308
  return { removed: removedCount, filePaths: removedFilePaths, gcOrphanEmbeddings, gcOrphanChunks, gcOrphanSymbols, gcOrphanCallEdges };
8193
9309
  }
8194
9310
  async retryFailedBatches() {
8195
- const { store, provider, invertedIndex, database, configuredProviderInfo } = await this.ensureInitialized();
9311
+ return this.withIndexMutationLease("retry-failed-batches", async (recoveredOwners) => {
9312
+ await this.ensureInitializedUnlocked(recoveredOwners);
9313
+ return this.retryFailedBatchesUnlocked();
9314
+ });
9315
+ }
9316
+ async retryFailedBatchesUnlocked() {
9317
+ const { store, provider, invertedIndex, database, configuredProviderInfo } = this.requireLoadedIndexState();
8196
9318
  const maxChunkTokens = getSafeEmbeddingChunkTokenLimit(configuredProviderInfo);
8197
9319
  const providerRateLimits = this.getProviderRateLimits(configuredProviderInfo.provider);
8198
9320
  const roots = this.config.scope === "global" ? this.getScopedRoots() : null;
@@ -8356,7 +9478,7 @@ var Indexer = class {
8356
9478
  }
8357
9479
  if (succeeded > 0) {
8358
9480
  store.save();
8359
- invertedIndex.save();
9481
+ this.saveInvertedIndex(invertedIndex);
8360
9482
  }
8361
9483
  if (roots && succeeded > 0 && persistedStillFailing.length === 0 && this.hasProjectForceReembedPending()) {
8362
9484
  database.deleteMetadata(this.getProjectForceReembedMetadataKey());
@@ -8384,15 +9506,16 @@ var Indexer = class {
8384
9506
  }
8385
9507
  }
8386
9508
  async getDatabaseStats() {
8387
- const { database } = await this.ensureInitialized();
9509
+ const { database, readIssues } = await this.ensureInitialized();
9510
+ this.requireReadableComponents(readIssues, "database");
8388
9511
  return database.getStats();
8389
9512
  }
8390
9513
  getLogger() {
8391
9514
  return this.logger;
8392
9515
  }
8393
9516
  async findSimilar(code, limit = this.config.search.maxResults, options) {
8394
- const { store, provider, database } = await this.ensureInitialized();
8395
- const compatibility = this.checkCompatibility();
9517
+ const { store, provider, database, readIssues, compatibility } = await this.ensureInitialized();
9518
+ this.requireReadableComponents(readIssues, "vectors", "database");
8396
9519
  if (!compatibility.compatible) {
8397
9520
  throw new Error(
8398
9521
  `${compatibility.reason ?? "Index is incompatible with current embedding provider."} Run index_codebase with force=true to rebuild the index.`
@@ -8499,13 +9622,15 @@ var Indexer = class {
8499
9622
  content,
8500
9623
  score: r.score,
8501
9624
  chunkType: r.metadata.chunkType,
8502
- name: r.metadata.name
9625
+ name: r.metadata.name,
9626
+ blame: blameFromMetadata(r.metadata)
8503
9627
  };
8504
9628
  })
8505
9629
  );
8506
9630
  }
8507
9631
  async getCallers(targetName, callTypeFilter) {
8508
- const { database } = await this.ensureInitialized();
9632
+ const { database, readIssues } = await this.ensureInitialized();
9633
+ this.requireReadableComponents(readIssues, "database");
8509
9634
  const seen = /* @__PURE__ */ new Set();
8510
9635
  const results = [];
8511
9636
  for (const branchKey of this.getBranchCatalogKeys()) {
@@ -8519,7 +9644,8 @@ var Indexer = class {
8519
9644
  return results;
8520
9645
  }
8521
9646
  async getCallees(symbolId, callTypeFilter) {
8522
- const { database } = await this.ensureInitialized();
9647
+ const { database, readIssues } = await this.ensureInitialized();
9648
+ this.requireReadableComponents(readIssues, "database");
8523
9649
  const seen = /* @__PURE__ */ new Set();
8524
9650
  const results = [];
8525
9651
  for (const branchKey of this.getBranchCatalogKeys()) {
@@ -8533,44 +9659,51 @@ var Indexer = class {
8533
9659
  return results;
8534
9660
  }
8535
9661
  async findCallPath(fromName, toName, maxDepth) {
8536
- const { database } = await this.ensureInitialized();
9662
+ const { database, readIssues } = await this.ensureInitialized();
9663
+ this.requireReadableComponents(readIssues, "database");
8537
9664
  let shortest = [];
8538
9665
  for (const branchKey of this.getBranchCatalogKeys()) {
8539
- const path22 = database.findShortestPath(fromName, toName, branchKey, maxDepth);
8540
- if (path22.length > 0 && (shortest.length === 0 || path22.length < shortest.length)) {
8541
- shortest = path22;
9666
+ const path24 = database.findShortestPath(fromName, toName, branchKey, maxDepth);
9667
+ if (path24.length > 0 && (shortest.length === 0 || path24.length < shortest.length)) {
9668
+ shortest = path24;
8542
9669
  }
8543
9670
  }
8544
9671
  return shortest;
8545
9672
  }
8546
9673
  async getSymbolsForBranch(branch) {
8547
- const { database } = await this.ensureInitialized();
9674
+ const { database, readIssues } = await this.ensureInitialized();
9675
+ this.requireReadableComponents(readIssues, "database");
8548
9676
  const resolvedBranch = branch ?? this.getBranchCatalogKey();
8549
9677
  return database.getSymbolsForBranch(resolvedBranch);
8550
9678
  }
8551
9679
  async getSymbolsForFiles(filePaths, branch) {
8552
- const { database } = await this.ensureInitialized();
9680
+ const { database, readIssues } = await this.ensureInitialized();
9681
+ this.requireReadableComponents(readIssues, "database");
8553
9682
  const resolvedBranch = branch ?? this.getBranchCatalogKey();
8554
9683
  return database.getSymbolsForFiles(filePaths, resolvedBranch);
8555
9684
  }
8556
9685
  async getTransitiveReachability(rootSymbolIds, direction, maxDepth) {
8557
- const { database } = await this.ensureInitialized();
9686
+ const { database, readIssues } = await this.ensureInitialized();
9687
+ this.requireReadableComponents(readIssues, "database");
8558
9688
  const branch = this.getBranchCatalogKey();
8559
9689
  return database.getTransitiveReachability(rootSymbolIds, branch, direction, maxDepth);
8560
9690
  }
8561
9691
  async detectCommunities(branch, symbolIds) {
8562
- const { database } = await this.ensureInitialized();
9692
+ const { database, readIssues } = await this.ensureInitialized();
9693
+ this.requireReadableComponents(readIssues, "database");
8563
9694
  const resolvedBranch = branch ?? this.getBranchCatalogKey();
8564
9695
  return database.detectCommunities(resolvedBranch, symbolIds);
8565
9696
  }
8566
9697
  async computeCentrality(branch) {
8567
- const { database } = await this.ensureInitialized();
9698
+ const { database, readIssues } = await this.ensureInitialized();
9699
+ this.requireReadableComponents(readIssues, "database");
8568
9700
  const resolvedBranch = branch ?? this.getBranchCatalogKey();
8569
9701
  return database.computeCentrality(resolvedBranch);
8570
9702
  }
8571
9703
  async getPrImpact(opts) {
8572
- const { database } = await this.ensureInitialized();
8573
- const execFileAsync2 = promisify2(execFile2);
9704
+ const { database, readIssues } = await this.ensureInitialized();
9705
+ this.requireReadableComponents(readIssues, "database");
9706
+ const execFileAsync3 = promisify3(execFile3);
8574
9707
  const changedFilesResult = await getChangedFiles({
8575
9708
  pr: opts.pr,
8576
9709
  branch: opts.branch,
@@ -8592,7 +9725,7 @@ var Indexer = class {
8592
9725
  "Run index_codebase first to build the call graph and symbol index for this branch."
8593
9726
  );
8594
9727
  }
8595
- const absoluteChangedFiles = changedFiles.map((f) => path11.resolve(this.projectRoot, f));
9728
+ const absoluteChangedFiles = changedFiles.map((f) => path13.resolve(this.projectRoot, f));
8596
9729
  const directSymbols = database.getSymbolsForFiles(absoluteChangedFiles, branchKey);
8597
9730
  const directIds = directSymbols.map((s) => s.id);
8598
9731
  const direction = opts.direction ?? "both";
@@ -8654,7 +9787,7 @@ var Indexer = class {
8654
9787
  if (opts.checkConflicts) {
8655
9788
  conflictingPRs = [];
8656
9789
  try {
8657
- const { stdout } = await execFileAsync2(
9790
+ const { stdout } = await execFileAsync3(
8658
9791
  "gh",
8659
9792
  ["pr", "list", "--state", "open", "--json", "number,headRefName", "--limit", "10000"],
8660
9793
  { cwd: this.projectRoot, timeout: 3e4 }
@@ -8675,7 +9808,7 @@ var Indexer = class {
8675
9808
  projectRoot: this.projectRoot,
8676
9809
  baseBranch: this.baseBranch
8677
9810
  });
8678
- const otherAbsolute = otherChanged.files.map((f) => path11.resolve(this.projectRoot, f));
9811
+ const otherAbsolute = otherChanged.files.map((f) => path13.resolve(this.projectRoot, f));
8679
9812
  const otherBranchKey = this.getBranchCatalogKeyFor(openPr.headRefName);
8680
9813
  const otherSymbols = database.getSymbolsForFiles(otherAbsolute, otherBranchKey);
8681
9814
  const otherLabels = /* @__PURE__ */ new Set();
@@ -8725,7 +9858,8 @@ var Indexer = class {
8725
9858
  };
8726
9859
  }
8727
9860
  async getVisualizationData(options) {
8728
- const { database, store } = await this.ensureInitialized();
9861
+ const { database, store, readIssues } = await this.ensureInitialized();
9862
+ this.requireReadableComponents(readIssues, "vectors", "database");
8729
9863
  const seenSymbols = /* @__PURE__ */ new Map();
8730
9864
  const seenEdges = /* @__PURE__ */ new Map();
8731
9865
  for (const branchKey of this.getBranchCatalogKeys()) {
@@ -8738,12 +9872,12 @@ var Indexer = class {
8738
9872
  if (meta.filePath) filePaths.add(meta.filePath);
8739
9873
  }
8740
9874
  const directory = options?.directory?.replace(/\/$/, "");
8741
- const absoluteDirectoryFilter = directory ? path11.resolve(this.projectRoot, directory) : void 0;
9875
+ const absoluteDirectoryFilter = directory ? path13.resolve(this.projectRoot, directory) : void 0;
8742
9876
  for (const filePath of filePaths) {
8743
9877
  if (directory) {
8744
- const absoluteFilePath = path11.resolve(filePath);
9878
+ const absoluteFilePath = path13.resolve(filePath);
8745
9879
  const matchesRelative = filePath === directory || filePath.startsWith(directory + "/");
8746
- const matchesProjectRelative = absoluteDirectoryFilter !== void 0 && (absoluteFilePath === absoluteDirectoryFilter || absoluteFilePath.startsWith(absoluteDirectoryFilter + path11.sep));
9880
+ const matchesProjectRelative = absoluteDirectoryFilter !== void 0 && (absoluteFilePath === absoluteDirectoryFilter || absoluteFilePath.startsWith(absoluteDirectoryFilter + path13.sep));
8747
9881
  if (!matchesRelative && !matchesProjectRelative) {
8748
9882
  continue;
8749
9883
  }
@@ -8765,53 +9899,64 @@ var Indexer = class {
8765
9899
  return { symbols: [...seenSymbols.values()], edges: [...seenEdges.values()] };
8766
9900
  }
8767
9901
  async close() {
8768
- await this.database?.close();
9902
+ this.database?.close();
9903
+ for (const database of this.retiredDatabases) {
9904
+ database.close();
9905
+ }
9906
+ this.retiredDatabases = [];
8769
9907
  this.database = null;
8770
9908
  this.store = null;
8771
9909
  this.invertedIndex = null;
8772
9910
  this.provider = null;
8773
9911
  this.reranker = null;
9912
+ this.configuredProviderInfo = null;
9913
+ this.indexCompatibility = null;
9914
+ this.initializationMode = "none";
9915
+ this.readIssues = [];
9916
+ this.readerArtifactFingerprint = null;
9917
+ this.writerArtifactFingerprint = null;
9918
+ this.readerArtifactRetryAfter.clear();
8774
9919
  }
8775
9920
  };
8776
9921
 
8777
9922
  // src/tools/knowledge-base-paths.ts
8778
- import * as path12 from "path";
9923
+ import * as path14 from "path";
8779
9924
  function resolveConfigPathValue(value, baseDir) {
8780
9925
  const trimmed = value.trim();
8781
9926
  if (!trimmed) {
8782
9927
  return trimmed;
8783
9928
  }
8784
- const absolutePath = path12.isAbsolute(trimmed) ? trimmed : path12.resolve(baseDir, trimmed);
8785
- return path12.normalize(absolutePath);
9929
+ const absolutePath = path14.isAbsolute(trimmed) ? trimmed : path14.resolve(baseDir, trimmed);
9930
+ return path14.normalize(absolutePath);
8786
9931
  }
8787
9932
  function serializeConfigPathValue(value, baseDir) {
8788
9933
  const trimmed = value.trim();
8789
9934
  if (!trimmed) {
8790
9935
  return trimmed;
8791
9936
  }
8792
- if (!path12.isAbsolute(trimmed)) {
8793
- return normalizePathSeparators(path12.normalize(trimmed));
9937
+ if (!path14.isAbsolute(trimmed)) {
9938
+ return normalizePathSeparators(path14.normalize(trimmed));
8794
9939
  }
8795
- const relativePath = path12.relative(baseDir, trimmed);
8796
- if (!relativePath || !relativePath.startsWith("..") && !path12.isAbsolute(relativePath)) {
8797
- return normalizePathSeparators(path12.normalize(relativePath || "."));
9940
+ const relativePath = path14.relative(baseDir, trimmed);
9941
+ if (!relativePath || !relativePath.startsWith("..") && !path14.isAbsolute(relativePath)) {
9942
+ return normalizePathSeparators(path14.normalize(relativePath || "."));
8798
9943
  }
8799
- return path12.normalize(trimmed);
9944
+ return path14.normalize(trimmed);
8800
9945
  }
8801
9946
  function resolveKnowledgeBasePath(value, projectRoot) {
8802
- return path12.isAbsolute(value) ? value : path12.resolve(projectRoot, value);
9947
+ return path14.isAbsolute(value) ? value : path14.resolve(projectRoot, value);
8803
9948
  }
8804
9949
  function normalizeKnowledgeBasePath2(value, projectRoot) {
8805
- return path12.normalize(resolveKnowledgeBasePath(value, projectRoot));
9950
+ return path14.normalize(resolveKnowledgeBasePath(value, projectRoot));
8806
9951
  }
8807
9952
  function hasMatchingKnowledgeBasePath(knowledgeBases, inputPath, projectRoot) {
8808
- const normalizedInput = path12.normalize(inputPath);
9953
+ const normalizedInput = path14.normalize(inputPath);
8809
9954
  return knowledgeBases.some((kb) => normalizeKnowledgeBasePath2(kb, projectRoot) === normalizedInput);
8810
9955
  }
8811
9956
  function findKnowledgeBasePathIndex(knowledgeBases, inputPath, projectRoot) {
8812
- const normalizedInput = path12.normalize(inputPath);
9957
+ const normalizedInput = path14.normalize(inputPath);
8813
9958
  return knowledgeBases.findIndex(
8814
- (kb) => path12.normalize(kb) === normalizedInput || normalizeKnowledgeBasePath2(kb, projectRoot) === normalizedInput
9959
+ (kb) => path14.normalize(kb) === normalizedInput || normalizeKnowledgeBasePath2(kb, projectRoot) === normalizedInput
8815
9960
  );
8816
9961
  }
8817
9962
 
@@ -8911,6 +10056,10 @@ function formatStatus(status) {
8911
10056
  lines.push(`Failed batches: ${status.failedBatchesPath}`);
8912
10057
  }
8913
10058
  }
10059
+ if (status.warning) {
10060
+ lines.push("");
10061
+ lines.push(`INDEX WARNING: ${status.warning}`);
10062
+ }
8914
10063
  if (status.compatibility && !status.compatibility.compatible) {
8915
10064
  lines.push("");
8916
10065
  lines.push(`COMPATIBILITY WARNING: ${status.compatibility.reason}`);
@@ -8963,7 +10112,7 @@ function formatCodebasePeek(results) {
8963
10112
  const formatted = results.map((r, idx) => {
8964
10113
  const location = `${r.filePath}:${r.startLine}-${r.endLine}`;
8965
10114
  const name = r.name ? `"${r.name}"` : "(anonymous)";
8966
- return `[${idx + 1}] ${r.chunkType} ${name} at ${location} (score: ${r.score.toFixed(2)})`;
10115
+ return `[${idx + 1}] ${r.chunkType} ${name} at ${location} (score: ${r.score.toFixed(2)})${formatBlame(r)}`;
8967
10116
  });
8968
10117
  return formatted.join("\n");
8969
10118
  }
@@ -8995,16 +10144,57 @@ function formatHealthCheck(result) {
8995
10144
  }
8996
10145
  return lines.join("\n");
8997
10146
  }
10147
+ function formatCallGraphCallers(name, callers, relationshipType) {
10148
+ if (callers.length === 0) {
10149
+ return `No callers found for "${name}"${relationshipType ? ` with type ${relationshipType}` : ""}. It may not be called by any tracked function, or the index needs updating.`;
10150
+ }
10151
+ const formatted = callers.map((edge, index) => {
10152
+ const confidence = edge.confidence !== "Direct" ? ` [${edge.confidence.toLowerCase()}]` : "";
10153
+ return `[${index + 1}] \u2190 from ${edge.fromSymbolName ?? "<unknown>"} in ${edge.fromSymbolFilePath ?? "<unknown file>"} [${edge.fromSymbolId}] (${edge.callType})${confidence} at line ${edge.line}${edge.isResolved ? " [resolved]" : " [unresolved]"}`;
10154
+ });
10155
+ return `"${name}" is called by ${callers.length} function(s):
10156
+
10157
+ ${formatted.join("\n")}`;
10158
+ }
10159
+ function formatCallGraphCallees(symbolId, callees, relationshipType) {
10160
+ if (callees.length === 0) {
10161
+ return `No callees found for symbol ${symbolId}${relationshipType ? ` with type ${relationshipType}` : ""}. The function may not call any other tracked functions.`;
10162
+ }
10163
+ return callees.map((edge, index) => {
10164
+ const confidence = edge.confidence !== "Direct" ? ` [${edge.confidence.toLowerCase()}]` : "";
10165
+ return `[${index + 1}] \u2192 ${edge.targetName} (${edge.callType})${confidence} at line ${edge.line}${edge.isResolved ? ` [resolved: ${edge.toSymbolId}]` : " [unresolved]"}`;
10166
+ }).join("\n");
10167
+ }
10168
+ function formatCallGraphPath(from, to, path24) {
10169
+ if (path24.length === 0) {
10170
+ return `No path found between "${from}" and "${to}". They may be in disconnected components, or the call graph index needs updating.`;
10171
+ }
10172
+ const formatted = path24.map((hop, index) => {
10173
+ const prefix = index === 0 ? "[start]" : `--${hop.callType}-->`;
10174
+ const location = hop.filePath ? ` (${hop.filePath}:${hop.line})` : "";
10175
+ return `${prefix} ${hop.symbolName}${location}`;
10176
+ });
10177
+ return `Path (${path24.length} hops):
10178
+ ${formatted.join("\n")}`;
10179
+ }
8998
10180
  function formatResultHeader(result, index) {
8999
10181
  return result.name ? `[${index + 1}] ${result.chunkType} "${result.name}" in ${result.filePath}:${result.startLine}-${result.endLine}` : `[${index + 1}] ${result.chunkType} in ${result.filePath}:${result.startLine}-${result.endLine}`;
9000
10182
  }
10183
+ function formatBlame(result) {
10184
+ if (!result.blame) {
10185
+ return "";
10186
+ }
10187
+ const date = new Date(result.blame.committedAt * 1e3).toISOString().slice(0, 10);
10188
+ return `
10189
+ ${result.blame.sha.slice(0, 7)} | ${result.blame.author} | ${date} | ${result.blame.summary}`;
10190
+ }
9001
10191
  function formatDefinitionLookup(results, query) {
9002
10192
  if (results.length === 0) {
9003
10193
  return `No definition found for "${query}". Try codebase_search for broader discovery, or verify the symbol name.`;
9004
10194
  }
9005
10195
  const formatted = results.map((r, idx) => {
9006
10196
  const header = formatResultHeader(r, idx);
9007
- return `${header} (score: ${r.score.toFixed(2)})
10197
+ return `${header} (score: ${r.score.toFixed(2)})${formatBlame(r)}
9008
10198
  \`\`\`
9009
10199
  ${truncateContent(r.content)}
9010
10200
  \`\`\``;
@@ -9015,7 +10205,7 @@ function formatSearchResults(results, scoreFormat = "similarity") {
9015
10205
  const formatted = results.map((r, idx) => {
9016
10206
  const header = formatResultHeader(r, idx);
9017
10207
  const scoreLabel = scoreFormat === "similarity" ? `(similarity: ${(r.score * 100).toFixed(1)}%)` : `(score: ${r.score.toFixed(2)})`;
9018
- return `${header} ${scoreLabel}
10208
+ return `${header} ${scoreLabel}${formatBlame(r)}
9019
10209
  \`\`\`
9020
10210
  ${truncateContent(r.content)}
9021
10211
  \`\`\``;
@@ -9024,8 +10214,8 @@ ${truncateContent(r.content)}
9024
10214
  }
9025
10215
 
9026
10216
  // src/tools/config-state.ts
9027
- import { existsSync as existsSync8, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
9028
- import * as path13 from "path";
10217
+ import { existsSync as existsSync9, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
10218
+ import * as path15 from "path";
9029
10219
  function normalizeKnowledgeBasePaths(config, projectRoot) {
9030
10220
  const normalized = { ...config };
9031
10221
  if (Array.isArray(normalized.knowledgeBases)) {
@@ -9052,10 +10242,10 @@ function loadEditableConfig(projectRoot, host = "opencode") {
9052
10242
  }
9053
10243
  function saveConfig(projectRoot, config, host = "opencode") {
9054
10244
  const configPath = getConfigPath(projectRoot, host);
9055
- const configDir = path13.dirname(configPath);
9056
- const configBaseDir = path13.dirname(configDir);
9057
- if (!existsSync8(configDir)) {
9058
- mkdirSync3(configDir, { recursive: true });
10245
+ const configDir = path15.dirname(configPath);
10246
+ const configBaseDir = path15.dirname(configDir);
10247
+ if (!existsSync9(configDir)) {
10248
+ mkdirSync4(configDir, { recursive: true });
9059
10249
  }
9060
10250
  const serializableConfig = { ...config };
9061
10251
  if (Array.isArray(serializableConfig.knowledgeBases)) {
@@ -9063,13 +10253,31 @@ function saveConfig(projectRoot, config, host = "opencode") {
9063
10253
  (kb) => serializeConfigPathValue(kb, configBaseDir)
9064
10254
  );
9065
10255
  }
9066
- writeFileSync3(configPath, JSON.stringify(serializableConfig, null, 2) + "\n", "utf-8");
10256
+ writeFileSync4(configPath, JSON.stringify(serializableConfig, null, 2) + "\n", "utf-8");
9067
10257
  }
9068
10258
 
9069
10259
  // src/tools/operations.ts
9070
10260
  var indexerCache = /* @__PURE__ */ new Map();
9071
10261
  var configCache = /* @__PURE__ */ new Map();
9072
10262
  var defaultProjectRoots = /* @__PURE__ */ new Map();
10263
+ function getIndexBusyResult(error) {
10264
+ if (!isIndexLockContentionError(error)) return null;
10265
+ const owner = error.owner;
10266
+ const ownerText = owner ? `PID ${owner.pid}, operation ${owner.operation}, since ${owner.startedAt}` : "unreadable owner";
10267
+ if (error.reason === "legacy-lock") {
10268
+ return {
10269
+ kind: "busy",
10270
+ text: `INDEX_BUSY: legacy lock format detected (${ownerText}). Verify the PID and remove this lock manually only if it is stale.`
10271
+ };
10272
+ }
10273
+ if (error.reason === "unknown-owner") {
10274
+ return {
10275
+ kind: "busy",
10276
+ text: `INDEX_BUSY: unreadable or remote lock owner (${ownerText}). Automatic recovery was refused; manual verification is required.`
10277
+ };
10278
+ }
10279
+ return { kind: "busy", text: `INDEX_BUSY: another index operation is already in progress (${ownerText}).` };
10280
+ }
9073
10281
  function getProjectRoot(projectRoot, host) {
9074
10282
  if (projectRoot) {
9075
10283
  return projectRoot;
@@ -9114,8 +10322,8 @@ function refreshIndexerForDirectory(projectRoot, host = "opencode", config = par
9114
10322
  }
9115
10323
  function shouldForceLocalizeProjectIndex(projectRoot, host = "opencode") {
9116
10324
  const root = getProjectRoot(projectRoot, host);
9117
- const localIndexPath = path14.join(root, getHostProjectIndexRelativePath(host));
9118
- if (existsSync9(localIndexPath)) {
10325
+ const localIndexPath = path16.join(root, getHostProjectIndexRelativePath(host));
10326
+ if (existsSync10(localIndexPath)) {
9119
10327
  return false;
9120
10328
  }
9121
10329
  const inheritedIndexPath = resolveWorktreeFallbackProjectIndexPath(root, host);
@@ -9129,7 +10337,10 @@ async function searchCodebase(projectRoot, host, query, options = {}) {
9129
10337
  chunkType: options.chunkType,
9130
10338
  contextLines: options.contextLines,
9131
10339
  metadataOnly: options.metadataOnly,
9132
- definitionIntent: options.definitionIntent
10340
+ definitionIntent: options.definitionIntent,
10341
+ blameAuthor: options.blameAuthor,
10342
+ blameSha: options.blameSha,
10343
+ blameSince: options.blameSince
9133
10344
  });
9134
10345
  }
9135
10346
  async function findSimilarCode(projectRoot, host, code, options = {}) {
@@ -9168,35 +10379,46 @@ async function getCallGraphPath(projectRoot, host, from, to, maxDepth) {
9168
10379
  async function runIndexCodebase(projectRoot, host, args, onProgress) {
9169
10380
  const root = getProjectRoot(projectRoot, host);
9170
10381
  const key = getIndexerCacheKey(root, host);
9171
- const cachedConfig = configCache.get(key);
9172
10382
  let indexer = getIndexerForProject(root, host);
9173
- if (args.estimateOnly) {
9174
- return { kind: "estimate", estimate: await indexer.estimateCost() };
9175
- }
9176
- if (args.force) {
9177
- if (shouldForceLocalizeProjectIndex(root, host)) {
9178
- materializeLocalProjectConfig(root, loadProjectConfigLayer(root, host), host);
9179
- refreshIndexerForDirectory(root, host, cachedConfig);
9180
- indexer = getIndexerForProject(root, host);
9181
- }
9182
- await indexer.clearIndex();
9183
- refreshIndexerForDirectory(root, host, cachedConfig);
9184
- indexer = getIndexerForProject(root, host);
9185
- }
9186
- const stats = await indexer.index((progress) => {
9187
- if (onProgress) {
9188
- return onProgress(formatProgressTitle(progress), {
9189
- phase: progress.phase,
9190
- filesProcessed: progress.filesProcessed,
9191
- totalFiles: progress.totalFiles,
9192
- chunksProcessed: progress.chunksProcessed,
9193
- totalChunks: progress.totalChunks,
9194
- percentage: calculatePercentage(progress)
10383
+ const runtimeConfig = configCache.get(key);
10384
+ try {
10385
+ if (args.estimateOnly) {
10386
+ return { kind: "estimate", estimate: await indexer.estimateCost() };
10387
+ }
10388
+ const runIndex = async (target) => {
10389
+ const operation = args.force ? target.forceIndex.bind(target) : target.index.bind(target);
10390
+ return operation((progress) => {
10391
+ if (onProgress) {
10392
+ return onProgress(formatProgressTitle(progress), {
10393
+ phase: progress.phase,
10394
+ filesProcessed: progress.filesProcessed,
10395
+ totalFiles: progress.totalFiles,
10396
+ chunksProcessed: progress.chunksProcessed,
10397
+ totalChunks: progress.totalChunks,
10398
+ percentage: calculatePercentage(progress)
10399
+ });
10400
+ }
10401
+ return Promise.resolve();
9195
10402
  });
10403
+ };
10404
+ let stats;
10405
+ if (args.force && shouldForceLocalizeProjectIndex(root, host)) {
10406
+ const inheritedIndexPath = resolveProjectIndexPath(root, runtimeConfig.scope, host);
10407
+ stats = await withIndexLock(inheritedIndexPath, "force-index", async () => {
10408
+ materializeLocalProjectConfig(root, loadProjectConfigLayer(root, host), host);
10409
+ refreshIndexerForDirectory(root, host, runtimeConfig);
10410
+ indexer = getIndexerForProject(root, host);
10411
+ return runIndex(indexer);
10412
+ }, { completeRecoveries: false });
10413
+ } else {
10414
+ stats = await runIndex(indexer);
9196
10415
  }
9197
- return Promise.resolve();
9198
- });
9199
- return { kind: "stats", stats };
10416
+ return { kind: "stats", stats };
10417
+ } catch (error) {
10418
+ const busyResult = getIndexBusyResult(error);
10419
+ if (!busyResult) throw error;
10420
+ return busyResult;
10421
+ }
9200
10422
  }
9201
10423
  async function getIndexStatus(projectRoot, host) {
9202
10424
  const indexer = getIndexerForProject(projectRoot, host);
@@ -9206,6 +10428,15 @@ async function getIndexHealthCheck(projectRoot, host) {
9206
10428
  const indexer = getIndexerForProject(projectRoot, host);
9207
10429
  return indexer.healthCheck();
9208
10430
  }
10431
+ async function runIndexHealthCheck(projectRoot, host) {
10432
+ try {
10433
+ return { kind: "health", health: await getIndexHealthCheck(projectRoot, host) };
10434
+ } catch (error) {
10435
+ const busyResult = getIndexBusyResult(error);
10436
+ if (!busyResult) throw error;
10437
+ return busyResult;
10438
+ }
10439
+ }
9209
10440
  async function getIndexMetrics(projectRoot, host) {
9210
10441
  const indexer = getIndexerForProject(projectRoot, host);
9211
10442
  const logger = indexer.getLogger();
@@ -9261,15 +10492,15 @@ async function getIndexLogs(projectRoot, host, args) {
9261
10492
  function addKnowledgeBase(projectRoot, host, knowledgeBasePath) {
9262
10493
  const root = getProjectRoot(projectRoot, host);
9263
10494
  const inputPath = knowledgeBasePath.trim();
9264
- const normalizedPath = path14.resolve(
9265
- path14.isAbsolute(inputPath) ? inputPath : resolveKnowledgeBasePath(inputPath, root)
10495
+ const normalizedPath = path16.resolve(
10496
+ path16.isAbsolute(inputPath) ? inputPath : resolveKnowledgeBasePath(inputPath, root)
9266
10497
  );
9267
- if (!existsSync9(normalizedPath)) {
10498
+ if (!existsSync10(normalizedPath)) {
9268
10499
  return `Error: Directory does not exist: ${normalizedPath}`;
9269
10500
  }
9270
10501
  let realPath;
9271
10502
  try {
9272
- realPath = realpathSync(normalizedPath);
10503
+ realPath = realpathSync2(normalizedPath);
9273
10504
  } catch {
9274
10505
  return `Error: Cannot resolve path: ${normalizedPath}`;
9275
10506
  }
@@ -9298,13 +10529,13 @@ function addKnowledgeBase(projectRoot, host, knowledgeBasePath) {
9298
10529
  }
9299
10530
  }
9300
10531
  for (const dotDir of sensitiveDotDirs) {
9301
- const sensitiveDir = path14.join(homeDir, dotDir);
10532
+ const sensitiveDir = path16.join(homeDir, dotDir);
9302
10533
  if (sensitiveDir && (realPath === sensitiveDir || realPath.startsWith(`${sensitiveDir}/`))) {
9303
10534
  return `Error: Adding sensitive directory as knowledge base is not allowed: ${normalizedPath}`;
9304
10535
  }
9305
10536
  }
9306
10537
  try {
9307
- const stat4 = statSync3(normalizedPath);
10538
+ const stat4 = statSync4(normalizedPath);
9308
10539
  if (!stat4.isDirectory()) {
9309
10540
  return `Error: Path is not a directory: ${normalizedPath}`;
9310
10541
  }
@@ -9344,7 +10575,7 @@ function listKnowledgeBases(projectRoot, host) {
9344
10575
  for (let i = 0; i < knowledgeBases.length; i++) {
9345
10576
  const kb = knowledgeBases[i];
9346
10577
  const resolvedPath = resolveKnowledgeBasePath(kb, root);
9347
- const exists = existsSync9(resolvedPath);
10578
+ const exists = existsSync10(resolvedPath);
9348
10579
  result += `[${i + 1}] ${kb}
9349
10580
  `;
9350
10581
  result += ` Resolved: ${resolvedPath}
@@ -9353,7 +10584,7 @@ function listKnowledgeBases(projectRoot, host) {
9353
10584
  `;
9354
10585
  if (exists) {
9355
10586
  try {
9356
- const stat4 = statSync3(resolvedPath);
10587
+ const stat4 = statSync4(resolvedPath);
9357
10588
  result += ` Type: ${stat4.isDirectory() ? "Directory" : "File"}
9358
10589
  `;
9359
10590
  } catch {
@@ -9361,7 +10592,7 @@ function listKnowledgeBases(projectRoot, host) {
9361
10592
  }
9362
10593
  result += "\n";
9363
10594
  }
9364
- const hasHostConfig = existsSync9(path14.join(root, getHostProjectConfigRelativePath(host)));
10595
+ const hasHostConfig = existsSync10(path16.join(root, getHostProjectConfigRelativePath(host)));
9365
10596
  if (hasHostConfig) {
9366
10597
  result += `
9367
10598
  Config sources: 1 file(s).`;
@@ -9484,7 +10715,7 @@ var ReaddirpStream = class extends Readable {
9484
10715
  this._directoryFilter = normalizeFilter(opts.directoryFilter);
9485
10716
  const statMethod = opts.lstat ? lstat : stat;
9486
10717
  if (wantBigintFsStats) {
9487
- this._stat = (path22) => statMethod(path22, { bigint: true });
10718
+ this._stat = (path24) => statMethod(path24, { bigint: true });
9488
10719
  } else {
9489
10720
  this._stat = statMethod;
9490
10721
  }
@@ -9509,8 +10740,8 @@ var ReaddirpStream = class extends Readable {
9509
10740
  const par = this.parent;
9510
10741
  const fil = par && par.files;
9511
10742
  if (fil && fil.length > 0) {
9512
- const { path: path22, depth } = par;
9513
- const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path22));
10743
+ const { path: path24, depth } = par;
10744
+ const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path24));
9514
10745
  const awaited = await Promise.all(slice);
9515
10746
  for (const entry of awaited) {
9516
10747
  if (!entry)
@@ -9550,20 +10781,20 @@ var ReaddirpStream = class extends Readable {
9550
10781
  this.reading = false;
9551
10782
  }
9552
10783
  }
9553
- async _exploreDir(path22, depth) {
10784
+ async _exploreDir(path24, depth) {
9554
10785
  let files;
9555
10786
  try {
9556
- files = await readdir(path22, this._rdOptions);
10787
+ files = await readdir(path24, this._rdOptions);
9557
10788
  } catch (error) {
9558
10789
  this._onError(error);
9559
10790
  }
9560
- return { files, depth, path: path22 };
10791
+ return { files, depth, path: path24 };
9561
10792
  }
9562
- async _formatEntry(dirent, path22) {
10793
+ async _formatEntry(dirent, path24) {
9563
10794
  let entry;
9564
10795
  const basename5 = this._isDirent ? dirent.name : dirent;
9565
10796
  try {
9566
- const fullPath = presolve(pjoin(path22, basename5));
10797
+ const fullPath = presolve(pjoin(path24, basename5));
9567
10798
  entry = { path: prelative(this._root, fullPath), fullPath, basename: basename5 };
9568
10799
  entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
9569
10800
  } catch (err) {
@@ -9963,16 +11194,16 @@ var delFromSet = (main, prop, item) => {
9963
11194
  };
9964
11195
  var isEmptySet = (val) => val instanceof Set ? val.size === 0 : !val;
9965
11196
  var FsWatchInstances = /* @__PURE__ */ new Map();
9966
- function createFsWatchInstance(path22, options, listener, errHandler, emitRaw) {
11197
+ function createFsWatchInstance(path24, options, listener, errHandler, emitRaw) {
9967
11198
  const handleEvent = (rawEvent, evPath) => {
9968
- listener(path22);
9969
- emitRaw(rawEvent, evPath, { watchedPath: path22 });
9970
- if (evPath && path22 !== evPath) {
9971
- fsWatchBroadcast(sp.resolve(path22, evPath), KEY_LISTENERS, sp.join(path22, evPath));
11199
+ listener(path24);
11200
+ emitRaw(rawEvent, evPath, { watchedPath: path24 });
11201
+ if (evPath && path24 !== evPath) {
11202
+ fsWatchBroadcast(sp.resolve(path24, evPath), KEY_LISTENERS, sp.join(path24, evPath));
9972
11203
  }
9973
11204
  };
9974
11205
  try {
9975
- return fs_watch(path22, {
11206
+ return fs_watch(path24, {
9976
11207
  persistent: options.persistent
9977
11208
  }, handleEvent);
9978
11209
  } catch (error) {
@@ -9988,12 +11219,12 @@ var fsWatchBroadcast = (fullPath, listenerType, val1, val2, val3) => {
9988
11219
  listener(val1, val2, val3);
9989
11220
  });
9990
11221
  };
9991
- var setFsWatchListener = (path22, fullPath, options, handlers) => {
11222
+ var setFsWatchListener = (path24, fullPath, options, handlers) => {
9992
11223
  const { listener, errHandler, rawEmitter } = handlers;
9993
11224
  let cont = FsWatchInstances.get(fullPath);
9994
11225
  let watcher;
9995
11226
  if (!options.persistent) {
9996
- watcher = createFsWatchInstance(path22, options, listener, errHandler, rawEmitter);
11227
+ watcher = createFsWatchInstance(path24, options, listener, errHandler, rawEmitter);
9997
11228
  if (!watcher)
9998
11229
  return;
9999
11230
  return watcher.close.bind(watcher);
@@ -10004,7 +11235,7 @@ var setFsWatchListener = (path22, fullPath, options, handlers) => {
10004
11235
  addAndConvert(cont, KEY_RAW, rawEmitter);
10005
11236
  } else {
10006
11237
  watcher = createFsWatchInstance(
10007
- path22,
11238
+ path24,
10008
11239
  options,
10009
11240
  fsWatchBroadcast.bind(null, fullPath, KEY_LISTENERS),
10010
11241
  errHandler,
@@ -10019,7 +11250,7 @@ var setFsWatchListener = (path22, fullPath, options, handlers) => {
10019
11250
  cont.watcherUnusable = true;
10020
11251
  if (isWindows && error.code === "EPERM") {
10021
11252
  try {
10022
- const fd = await open(path22, "r");
11253
+ const fd = await open(path24, "r");
10023
11254
  await fd.close();
10024
11255
  broadcastErr(error);
10025
11256
  } catch (err) {
@@ -10050,7 +11281,7 @@ var setFsWatchListener = (path22, fullPath, options, handlers) => {
10050
11281
  };
10051
11282
  };
10052
11283
  var FsWatchFileInstances = /* @__PURE__ */ new Map();
10053
- var setFsWatchFileListener = (path22, fullPath, options, handlers) => {
11284
+ var setFsWatchFileListener = (path24, fullPath, options, handlers) => {
10054
11285
  const { listener, rawEmitter } = handlers;
10055
11286
  let cont = FsWatchFileInstances.get(fullPath);
10056
11287
  const copts = cont && cont.options;
@@ -10072,7 +11303,7 @@ var setFsWatchFileListener = (path22, fullPath, options, handlers) => {
10072
11303
  });
10073
11304
  const currmtime = curr.mtimeMs;
10074
11305
  if (curr.size !== prev.size || currmtime > prev.mtimeMs || currmtime === 0) {
10075
- foreach(cont.listeners, (listener2) => listener2(path22, curr));
11306
+ foreach(cont.listeners, (listener2) => listener2(path24, curr));
10076
11307
  }
10077
11308
  })
10078
11309
  };
@@ -10102,13 +11333,13 @@ var NodeFsHandler = class {
10102
11333
  * @param listener on fs change
10103
11334
  * @returns closer for the watcher instance
10104
11335
  */
10105
- _watchWithNodeFs(path22, listener) {
11336
+ _watchWithNodeFs(path24, listener) {
10106
11337
  const opts = this.fsw.options;
10107
- const directory = sp.dirname(path22);
10108
- const basename5 = sp.basename(path22);
11338
+ const directory = sp.dirname(path24);
11339
+ const basename5 = sp.basename(path24);
10109
11340
  const parent = this.fsw._getWatchedDir(directory);
10110
11341
  parent.add(basename5);
10111
- const absolutePath = sp.resolve(path22);
11342
+ const absolutePath = sp.resolve(path24);
10112
11343
  const options = {
10113
11344
  persistent: opts.persistent
10114
11345
  };
@@ -10118,12 +11349,12 @@ var NodeFsHandler = class {
10118
11349
  if (opts.usePolling) {
10119
11350
  const enableBin = opts.interval !== opts.binaryInterval;
10120
11351
  options.interval = enableBin && isBinaryPath(basename5) ? opts.binaryInterval : opts.interval;
10121
- closer = setFsWatchFileListener(path22, absolutePath, options, {
11352
+ closer = setFsWatchFileListener(path24, absolutePath, options, {
10122
11353
  listener,
10123
11354
  rawEmitter: this.fsw._emitRaw
10124
11355
  });
10125
11356
  } else {
10126
- closer = setFsWatchListener(path22, absolutePath, options, {
11357
+ closer = setFsWatchListener(path24, absolutePath, options, {
10127
11358
  listener,
10128
11359
  errHandler: this._boundHandleError,
10129
11360
  rawEmitter: this.fsw._emitRaw
@@ -10145,7 +11376,7 @@ var NodeFsHandler = class {
10145
11376
  let prevStats = stats;
10146
11377
  if (parent.has(basename5))
10147
11378
  return;
10148
- const listener = async (path22, newStats) => {
11379
+ const listener = async (path24, newStats) => {
10149
11380
  if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5))
10150
11381
  return;
10151
11382
  if (!newStats || newStats.mtimeMs === 0) {
@@ -10159,11 +11390,11 @@ var NodeFsHandler = class {
10159
11390
  this.fsw._emit(EV.CHANGE, file, newStats2);
10160
11391
  }
10161
11392
  if ((isMacos || isLinux || isFreeBSD) && prevStats.ino !== newStats2.ino) {
10162
- this.fsw._closeFile(path22);
11393
+ this.fsw._closeFile(path24);
10163
11394
  prevStats = newStats2;
10164
11395
  const closer2 = this._watchWithNodeFs(file, listener);
10165
11396
  if (closer2)
10166
- this.fsw._addPathCloser(path22, closer2);
11397
+ this.fsw._addPathCloser(path24, closer2);
10167
11398
  } else {
10168
11399
  prevStats = newStats2;
10169
11400
  }
@@ -10195,7 +11426,7 @@ var NodeFsHandler = class {
10195
11426
  * @param item basename of this item
10196
11427
  * @returns true if no more processing is needed for this entry.
10197
11428
  */
10198
- async _handleSymlink(entry, directory, path22, item) {
11429
+ async _handleSymlink(entry, directory, path24, item) {
10199
11430
  if (this.fsw.closed) {
10200
11431
  return;
10201
11432
  }
@@ -10205,7 +11436,7 @@ var NodeFsHandler = class {
10205
11436
  this.fsw._incrReadyCount();
10206
11437
  let linkPath;
10207
11438
  try {
10208
- linkPath = await fsrealpath(path22);
11439
+ linkPath = await fsrealpath(path24);
10209
11440
  } catch (e) {
10210
11441
  this.fsw._emitReady();
10211
11442
  return true;
@@ -10215,12 +11446,12 @@ var NodeFsHandler = class {
10215
11446
  if (dir.has(item)) {
10216
11447
  if (this.fsw._symlinkPaths.get(full) !== linkPath) {
10217
11448
  this.fsw._symlinkPaths.set(full, linkPath);
10218
- this.fsw._emit(EV.CHANGE, path22, entry.stats);
11449
+ this.fsw._emit(EV.CHANGE, path24, entry.stats);
10219
11450
  }
10220
11451
  } else {
10221
11452
  dir.add(item);
10222
11453
  this.fsw._symlinkPaths.set(full, linkPath);
10223
- this.fsw._emit(EV.ADD, path22, entry.stats);
11454
+ this.fsw._emit(EV.ADD, path24, entry.stats);
10224
11455
  }
10225
11456
  this.fsw._emitReady();
10226
11457
  return true;
@@ -10250,9 +11481,9 @@ var NodeFsHandler = class {
10250
11481
  return;
10251
11482
  }
10252
11483
  const item = entry.path;
10253
- let path22 = sp.join(directory, item);
11484
+ let path24 = sp.join(directory, item);
10254
11485
  current.add(item);
10255
- if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path22, item)) {
11486
+ if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path24, item)) {
10256
11487
  return;
10257
11488
  }
10258
11489
  if (this.fsw.closed) {
@@ -10261,8 +11492,8 @@ var NodeFsHandler = class {
10261
11492
  }
10262
11493
  if (item === target || !target && !previous.has(item)) {
10263
11494
  this.fsw._incrReadyCount();
10264
- path22 = sp.join(dir, sp.relative(dir, path22));
10265
- this._addToNodeFs(path22, initialAdd, wh, depth + 1);
11495
+ path24 = sp.join(dir, sp.relative(dir, path24));
11496
+ this._addToNodeFs(path24, initialAdd, wh, depth + 1);
10266
11497
  }
10267
11498
  }).on(EV.ERROR, this._boundHandleError);
10268
11499
  return new Promise((resolve13, reject) => {
@@ -10331,13 +11562,13 @@ var NodeFsHandler = class {
10331
11562
  * @param depth Child path actually targeted for watch
10332
11563
  * @param target Child path actually targeted for watch
10333
11564
  */
10334
- async _addToNodeFs(path22, initialAdd, priorWh, depth, target) {
11565
+ async _addToNodeFs(path24, initialAdd, priorWh, depth, target) {
10335
11566
  const ready = this.fsw._emitReady;
10336
- if (this.fsw._isIgnored(path22) || this.fsw.closed) {
11567
+ if (this.fsw._isIgnored(path24) || this.fsw.closed) {
10337
11568
  ready();
10338
11569
  return false;
10339
11570
  }
10340
- const wh = this.fsw._getWatchHelpers(path22);
11571
+ const wh = this.fsw._getWatchHelpers(path24);
10341
11572
  if (priorWh) {
10342
11573
  wh.filterPath = (entry) => priorWh.filterPath(entry);
10343
11574
  wh.filterDir = (entry) => priorWh.filterDir(entry);
@@ -10353,8 +11584,8 @@ var NodeFsHandler = class {
10353
11584
  const follow = this.fsw.options.followSymlinks;
10354
11585
  let closer;
10355
11586
  if (stats.isDirectory()) {
10356
- const absPath = sp.resolve(path22);
10357
- const targetPath = follow ? await fsrealpath(path22) : path22;
11587
+ const absPath = sp.resolve(path24);
11588
+ const targetPath = follow ? await fsrealpath(path24) : path24;
10358
11589
  if (this.fsw.closed)
10359
11590
  return;
10360
11591
  closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath);
@@ -10364,29 +11595,29 @@ var NodeFsHandler = class {
10364
11595
  this.fsw._symlinkPaths.set(absPath, targetPath);
10365
11596
  }
10366
11597
  } else if (stats.isSymbolicLink()) {
10367
- const targetPath = follow ? await fsrealpath(path22) : path22;
11598
+ const targetPath = follow ? await fsrealpath(path24) : path24;
10368
11599
  if (this.fsw.closed)
10369
11600
  return;
10370
11601
  const parent = sp.dirname(wh.watchPath);
10371
11602
  this.fsw._getWatchedDir(parent).add(wh.watchPath);
10372
11603
  this.fsw._emit(EV.ADD, wh.watchPath, stats);
10373
- closer = await this._handleDir(parent, stats, initialAdd, depth, path22, wh, targetPath);
11604
+ closer = await this._handleDir(parent, stats, initialAdd, depth, path24, wh, targetPath);
10374
11605
  if (this.fsw.closed)
10375
11606
  return;
10376
11607
  if (targetPath !== void 0) {
10377
- this.fsw._symlinkPaths.set(sp.resolve(path22), targetPath);
11608
+ this.fsw._symlinkPaths.set(sp.resolve(path24), targetPath);
10378
11609
  }
10379
11610
  } else {
10380
11611
  closer = this._handleFile(wh.watchPath, stats, initialAdd);
10381
11612
  }
10382
11613
  ready();
10383
11614
  if (closer)
10384
- this.fsw._addPathCloser(path22, closer);
11615
+ this.fsw._addPathCloser(path24, closer);
10385
11616
  return false;
10386
11617
  } catch (error) {
10387
11618
  if (this.fsw._handleError(error)) {
10388
11619
  ready();
10389
- return path22;
11620
+ return path24;
10390
11621
  }
10391
11622
  }
10392
11623
  }
@@ -10418,35 +11649,35 @@ function createPattern(matcher) {
10418
11649
  if (matcher.path === string)
10419
11650
  return true;
10420
11651
  if (matcher.recursive) {
10421
- const relative10 = sp2.relative(matcher.path, string);
10422
- if (!relative10) {
11652
+ const relative11 = sp2.relative(matcher.path, string);
11653
+ if (!relative11) {
10423
11654
  return false;
10424
11655
  }
10425
- return !relative10.startsWith("..") && !sp2.isAbsolute(relative10);
11656
+ return !relative11.startsWith("..") && !sp2.isAbsolute(relative11);
10426
11657
  }
10427
11658
  return false;
10428
11659
  };
10429
11660
  }
10430
11661
  return () => false;
10431
11662
  }
10432
- function normalizePath(path22) {
10433
- if (typeof path22 !== "string")
11663
+ function normalizePath(path24) {
11664
+ if (typeof path24 !== "string")
10434
11665
  throw new Error("string expected");
10435
- path22 = sp2.normalize(path22);
10436
- path22 = path22.replace(/\\/g, "/");
11666
+ path24 = sp2.normalize(path24);
11667
+ path24 = path24.replace(/\\/g, "/");
10437
11668
  let prepend = false;
10438
- if (path22.startsWith("//"))
11669
+ if (path24.startsWith("//"))
10439
11670
  prepend = true;
10440
- path22 = path22.replace(DOUBLE_SLASH_RE, "/");
11671
+ path24 = path24.replace(DOUBLE_SLASH_RE, "/");
10441
11672
  if (prepend)
10442
- path22 = "/" + path22;
10443
- return path22;
11673
+ path24 = "/" + path24;
11674
+ return path24;
10444
11675
  }
10445
11676
  function matchPatterns(patterns, testString, stats) {
10446
- const path22 = normalizePath(testString);
11677
+ const path24 = normalizePath(testString);
10447
11678
  for (let index = 0; index < patterns.length; index++) {
10448
11679
  const pattern = patterns[index];
10449
- if (pattern(path22, stats)) {
11680
+ if (pattern(path24, stats)) {
10450
11681
  return true;
10451
11682
  }
10452
11683
  }
@@ -10484,19 +11715,19 @@ var toUnix = (string) => {
10484
11715
  }
10485
11716
  return str;
10486
11717
  };
10487
- var normalizePathToUnix = (path22) => toUnix(sp2.normalize(toUnix(path22)));
10488
- var normalizeIgnored = (cwd = "") => (path22) => {
10489
- if (typeof path22 === "string") {
10490
- return normalizePathToUnix(sp2.isAbsolute(path22) ? path22 : sp2.join(cwd, path22));
11718
+ var normalizePathToUnix = (path24) => toUnix(sp2.normalize(toUnix(path24)));
11719
+ var normalizeIgnored = (cwd = "") => (path24) => {
11720
+ if (typeof path24 === "string") {
11721
+ return normalizePathToUnix(sp2.isAbsolute(path24) ? path24 : sp2.join(cwd, path24));
10491
11722
  } else {
10492
- return path22;
11723
+ return path24;
10493
11724
  }
10494
11725
  };
10495
- var getAbsolutePath = (path22, cwd) => {
10496
- if (sp2.isAbsolute(path22)) {
10497
- return path22;
11726
+ var getAbsolutePath = (path24, cwd) => {
11727
+ if (sp2.isAbsolute(path24)) {
11728
+ return path24;
10498
11729
  }
10499
- return sp2.join(cwd, path22);
11730
+ return sp2.join(cwd, path24);
10500
11731
  };
10501
11732
  var EMPTY_SET = Object.freeze(/* @__PURE__ */ new Set());
10502
11733
  var DirEntry = class {
@@ -10561,10 +11792,10 @@ var WatchHelper = class {
10561
11792
  dirParts;
10562
11793
  followSymlinks;
10563
11794
  statMethod;
10564
- constructor(path22, follow, fsw) {
11795
+ constructor(path24, follow, fsw) {
10565
11796
  this.fsw = fsw;
10566
- const watchPath = path22;
10567
- this.path = path22 = path22.replace(REPLACER_RE, "");
11797
+ const watchPath = path24;
11798
+ this.path = path24 = path24.replace(REPLACER_RE, "");
10568
11799
  this.watchPath = watchPath;
10569
11800
  this.fullWatchPath = sp2.resolve(watchPath);
10570
11801
  this.dirParts = [];
@@ -10704,20 +11935,20 @@ var FSWatcher = class extends EventEmitter2 {
10704
11935
  this._closePromise = void 0;
10705
11936
  let paths = unifyPaths(paths_);
10706
11937
  if (cwd) {
10707
- paths = paths.map((path22) => {
10708
- const absPath = getAbsolutePath(path22, cwd);
11938
+ paths = paths.map((path24) => {
11939
+ const absPath = getAbsolutePath(path24, cwd);
10709
11940
  return absPath;
10710
11941
  });
10711
11942
  }
10712
- paths.forEach((path22) => {
10713
- this._removeIgnoredPath(path22);
11943
+ paths.forEach((path24) => {
11944
+ this._removeIgnoredPath(path24);
10714
11945
  });
10715
11946
  this._userIgnored = void 0;
10716
11947
  if (!this._readyCount)
10717
11948
  this._readyCount = 0;
10718
11949
  this._readyCount += paths.length;
10719
- Promise.all(paths.map(async (path22) => {
10720
- const res = await this._nodeFsHandler._addToNodeFs(path22, !_internal, void 0, 0, _origAdd);
11950
+ Promise.all(paths.map(async (path24) => {
11951
+ const res = await this._nodeFsHandler._addToNodeFs(path24, !_internal, void 0, 0, _origAdd);
10721
11952
  if (res)
10722
11953
  this._emitReady();
10723
11954
  return res;
@@ -10739,17 +11970,17 @@ var FSWatcher = class extends EventEmitter2 {
10739
11970
  return this;
10740
11971
  const paths = unifyPaths(paths_);
10741
11972
  const { cwd } = this.options;
10742
- paths.forEach((path22) => {
10743
- if (!sp2.isAbsolute(path22) && !this._closers.has(path22)) {
11973
+ paths.forEach((path24) => {
11974
+ if (!sp2.isAbsolute(path24) && !this._closers.has(path24)) {
10744
11975
  if (cwd)
10745
- path22 = sp2.join(cwd, path22);
10746
- path22 = sp2.resolve(path22);
11976
+ path24 = sp2.join(cwd, path24);
11977
+ path24 = sp2.resolve(path24);
10747
11978
  }
10748
- this._closePath(path22);
10749
- this._addIgnoredPath(path22);
10750
- if (this._watched.has(path22)) {
11979
+ this._closePath(path24);
11980
+ this._addIgnoredPath(path24);
11981
+ if (this._watched.has(path24)) {
10751
11982
  this._addIgnoredPath({
10752
- path: path22,
11983
+ path: path24,
10753
11984
  recursive: true
10754
11985
  });
10755
11986
  }
@@ -10813,38 +12044,38 @@ var FSWatcher = class extends EventEmitter2 {
10813
12044
  * @param stats arguments to be passed with event
10814
12045
  * @returns the error if defined, otherwise the value of the FSWatcher instance's `closed` flag
10815
12046
  */
10816
- async _emit(event, path22, stats) {
12047
+ async _emit(event, path24, stats) {
10817
12048
  if (this.closed)
10818
12049
  return;
10819
12050
  const opts = this.options;
10820
12051
  if (isWindows)
10821
- path22 = sp2.normalize(path22);
12052
+ path24 = sp2.normalize(path24);
10822
12053
  if (opts.cwd)
10823
- path22 = sp2.relative(opts.cwd, path22);
10824
- const args = [path22];
12054
+ path24 = sp2.relative(opts.cwd, path24);
12055
+ const args = [path24];
10825
12056
  if (stats != null)
10826
12057
  args.push(stats);
10827
12058
  const awf = opts.awaitWriteFinish;
10828
12059
  let pw;
10829
- if (awf && (pw = this._pendingWrites.get(path22))) {
12060
+ if (awf && (pw = this._pendingWrites.get(path24))) {
10830
12061
  pw.lastChange = /* @__PURE__ */ new Date();
10831
12062
  return this;
10832
12063
  }
10833
12064
  if (opts.atomic) {
10834
12065
  if (event === EVENTS.UNLINK) {
10835
- this._pendingUnlinks.set(path22, [event, ...args]);
12066
+ this._pendingUnlinks.set(path24, [event, ...args]);
10836
12067
  setTimeout(() => {
10837
- this._pendingUnlinks.forEach((entry, path23) => {
12068
+ this._pendingUnlinks.forEach((entry, path25) => {
10838
12069
  this.emit(...entry);
10839
12070
  this.emit(EVENTS.ALL, ...entry);
10840
- this._pendingUnlinks.delete(path23);
12071
+ this._pendingUnlinks.delete(path25);
10841
12072
  });
10842
12073
  }, typeof opts.atomic === "number" ? opts.atomic : 100);
10843
12074
  return this;
10844
12075
  }
10845
- if (event === EVENTS.ADD && this._pendingUnlinks.has(path22)) {
12076
+ if (event === EVENTS.ADD && this._pendingUnlinks.has(path24)) {
10846
12077
  event = EVENTS.CHANGE;
10847
- this._pendingUnlinks.delete(path22);
12078
+ this._pendingUnlinks.delete(path24);
10848
12079
  }
10849
12080
  }
10850
12081
  if (awf && (event === EVENTS.ADD || event === EVENTS.CHANGE) && this._readyEmitted) {
@@ -10862,16 +12093,16 @@ var FSWatcher = class extends EventEmitter2 {
10862
12093
  this.emitWithAll(event, args);
10863
12094
  }
10864
12095
  };
10865
- this._awaitWriteFinish(path22, awf.stabilityThreshold, event, awfEmit);
12096
+ this._awaitWriteFinish(path24, awf.stabilityThreshold, event, awfEmit);
10866
12097
  return this;
10867
12098
  }
10868
12099
  if (event === EVENTS.CHANGE) {
10869
- const isThrottled = !this._throttle(EVENTS.CHANGE, path22, 50);
12100
+ const isThrottled = !this._throttle(EVENTS.CHANGE, path24, 50);
10870
12101
  if (isThrottled)
10871
12102
  return this;
10872
12103
  }
10873
12104
  if (opts.alwaysStat && stats === void 0 && (event === EVENTS.ADD || event === EVENTS.ADD_DIR || event === EVENTS.CHANGE)) {
10874
- const fullPath = opts.cwd ? sp2.join(opts.cwd, path22) : path22;
12105
+ const fullPath = opts.cwd ? sp2.join(opts.cwd, path24) : path24;
10875
12106
  let stats2;
10876
12107
  try {
10877
12108
  stats2 = await stat3(fullPath);
@@ -10902,23 +12133,23 @@ var FSWatcher = class extends EventEmitter2 {
10902
12133
  * @param timeout duration of time to suppress duplicate actions
10903
12134
  * @returns tracking object or false if action should be suppressed
10904
12135
  */
10905
- _throttle(actionType, path22, timeout) {
12136
+ _throttle(actionType, path24, timeout) {
10906
12137
  if (!this._throttled.has(actionType)) {
10907
12138
  this._throttled.set(actionType, /* @__PURE__ */ new Map());
10908
12139
  }
10909
12140
  const action = this._throttled.get(actionType);
10910
12141
  if (!action)
10911
12142
  throw new Error("invalid throttle");
10912
- const actionPath = action.get(path22);
12143
+ const actionPath = action.get(path24);
10913
12144
  if (actionPath) {
10914
12145
  actionPath.count++;
10915
12146
  return false;
10916
12147
  }
10917
12148
  let timeoutObject;
10918
12149
  const clear = () => {
10919
- const item = action.get(path22);
12150
+ const item = action.get(path24);
10920
12151
  const count = item ? item.count : 0;
10921
- action.delete(path22);
12152
+ action.delete(path24);
10922
12153
  clearTimeout(timeoutObject);
10923
12154
  if (item)
10924
12155
  clearTimeout(item.timeoutObject);
@@ -10926,7 +12157,7 @@ var FSWatcher = class extends EventEmitter2 {
10926
12157
  };
10927
12158
  timeoutObject = setTimeout(clear, timeout);
10928
12159
  const thr = { timeoutObject, clear, count: 0 };
10929
- action.set(path22, thr);
12160
+ action.set(path24, thr);
10930
12161
  return thr;
10931
12162
  }
10932
12163
  _incrReadyCount() {
@@ -10940,44 +12171,44 @@ var FSWatcher = class extends EventEmitter2 {
10940
12171
  * @param event
10941
12172
  * @param awfEmit Callback to be called when ready for event to be emitted.
10942
12173
  */
10943
- _awaitWriteFinish(path22, threshold, event, awfEmit) {
12174
+ _awaitWriteFinish(path24, threshold, event, awfEmit) {
10944
12175
  const awf = this.options.awaitWriteFinish;
10945
12176
  if (typeof awf !== "object")
10946
12177
  return;
10947
12178
  const pollInterval = awf.pollInterval;
10948
12179
  let timeoutHandler;
10949
- let fullPath = path22;
10950
- if (this.options.cwd && !sp2.isAbsolute(path22)) {
10951
- fullPath = sp2.join(this.options.cwd, path22);
12180
+ let fullPath = path24;
12181
+ if (this.options.cwd && !sp2.isAbsolute(path24)) {
12182
+ fullPath = sp2.join(this.options.cwd, path24);
10952
12183
  }
10953
12184
  const now = /* @__PURE__ */ new Date();
10954
12185
  const writes = this._pendingWrites;
10955
12186
  function awaitWriteFinishFn(prevStat) {
10956
12187
  statcb(fullPath, (err, curStat) => {
10957
- if (err || !writes.has(path22)) {
12188
+ if (err || !writes.has(path24)) {
10958
12189
  if (err && err.code !== "ENOENT")
10959
12190
  awfEmit(err);
10960
12191
  return;
10961
12192
  }
10962
12193
  const now2 = Number(/* @__PURE__ */ new Date());
10963
12194
  if (prevStat && curStat.size !== prevStat.size) {
10964
- writes.get(path22).lastChange = now2;
12195
+ writes.get(path24).lastChange = now2;
10965
12196
  }
10966
- const pw = writes.get(path22);
12197
+ const pw = writes.get(path24);
10967
12198
  const df = now2 - pw.lastChange;
10968
12199
  if (df >= threshold) {
10969
- writes.delete(path22);
12200
+ writes.delete(path24);
10970
12201
  awfEmit(void 0, curStat);
10971
12202
  } else {
10972
12203
  timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval, curStat);
10973
12204
  }
10974
12205
  });
10975
12206
  }
10976
- if (!writes.has(path22)) {
10977
- writes.set(path22, {
12207
+ if (!writes.has(path24)) {
12208
+ writes.set(path24, {
10978
12209
  lastChange: now,
10979
12210
  cancelWait: () => {
10980
- writes.delete(path22);
12211
+ writes.delete(path24);
10981
12212
  clearTimeout(timeoutHandler);
10982
12213
  return event;
10983
12214
  }
@@ -10988,8 +12219,8 @@ var FSWatcher = class extends EventEmitter2 {
10988
12219
  /**
10989
12220
  * Determines whether user has asked to ignore this path.
10990
12221
  */
10991
- _isIgnored(path22, stats) {
10992
- if (this.options.atomic && DOT_RE.test(path22))
12222
+ _isIgnored(path24, stats) {
12223
+ if (this.options.atomic && DOT_RE.test(path24))
10993
12224
  return true;
10994
12225
  if (!this._userIgnored) {
10995
12226
  const { cwd } = this.options;
@@ -10999,17 +12230,17 @@ var FSWatcher = class extends EventEmitter2 {
10999
12230
  const list = [...ignoredPaths.map(normalizeIgnored(cwd)), ...ignored];
11000
12231
  this._userIgnored = anymatch(list, void 0);
11001
12232
  }
11002
- return this._userIgnored(path22, stats);
12233
+ return this._userIgnored(path24, stats);
11003
12234
  }
11004
- _isntIgnored(path22, stat4) {
11005
- return !this._isIgnored(path22, stat4);
12235
+ _isntIgnored(path24, stat4) {
12236
+ return !this._isIgnored(path24, stat4);
11006
12237
  }
11007
12238
  /**
11008
12239
  * Provides a set of common helpers and properties relating to symlink handling.
11009
12240
  * @param path file or directory pattern being watched
11010
12241
  */
11011
- _getWatchHelpers(path22) {
11012
- return new WatchHelper(path22, this.options.followSymlinks, this);
12242
+ _getWatchHelpers(path24) {
12243
+ return new WatchHelper(path24, this.options.followSymlinks, this);
11013
12244
  }
11014
12245
  // Directory helpers
11015
12246
  // -----------------
@@ -11041,63 +12272,63 @@ var FSWatcher = class extends EventEmitter2 {
11041
12272
  * @param item base path of item/directory
11042
12273
  */
11043
12274
  _remove(directory, item, isDirectory) {
11044
- const path22 = sp2.join(directory, item);
11045
- const fullPath = sp2.resolve(path22);
11046
- isDirectory = isDirectory != null ? isDirectory : this._watched.has(path22) || this._watched.has(fullPath);
11047
- if (!this._throttle("remove", path22, 100))
12275
+ const path24 = sp2.join(directory, item);
12276
+ const fullPath = sp2.resolve(path24);
12277
+ isDirectory = isDirectory != null ? isDirectory : this._watched.has(path24) || this._watched.has(fullPath);
12278
+ if (!this._throttle("remove", path24, 100))
11048
12279
  return;
11049
12280
  if (!isDirectory && this._watched.size === 1) {
11050
12281
  this.add(directory, item, true);
11051
12282
  }
11052
- const wp = this._getWatchedDir(path22);
12283
+ const wp = this._getWatchedDir(path24);
11053
12284
  const nestedDirectoryChildren = wp.getChildren();
11054
- nestedDirectoryChildren.forEach((nested) => this._remove(path22, nested));
12285
+ nestedDirectoryChildren.forEach((nested) => this._remove(path24, nested));
11055
12286
  const parent = this._getWatchedDir(directory);
11056
12287
  const wasTracked = parent.has(item);
11057
12288
  parent.remove(item);
11058
12289
  if (this._symlinkPaths.has(fullPath)) {
11059
12290
  this._symlinkPaths.delete(fullPath);
11060
12291
  }
11061
- let relPath = path22;
12292
+ let relPath = path24;
11062
12293
  if (this.options.cwd)
11063
- relPath = sp2.relative(this.options.cwd, path22);
12294
+ relPath = sp2.relative(this.options.cwd, path24);
11064
12295
  if (this.options.awaitWriteFinish && this._pendingWrites.has(relPath)) {
11065
12296
  const event = this._pendingWrites.get(relPath).cancelWait();
11066
12297
  if (event === EVENTS.ADD)
11067
12298
  return;
11068
12299
  }
11069
- this._watched.delete(path22);
12300
+ this._watched.delete(path24);
11070
12301
  this._watched.delete(fullPath);
11071
12302
  const eventName = isDirectory ? EVENTS.UNLINK_DIR : EVENTS.UNLINK;
11072
- if (wasTracked && !this._isIgnored(path22))
11073
- this._emit(eventName, path22);
11074
- this._closePath(path22);
12303
+ if (wasTracked && !this._isIgnored(path24))
12304
+ this._emit(eventName, path24);
12305
+ this._closePath(path24);
11075
12306
  }
11076
12307
  /**
11077
12308
  * Closes all watchers for a path
11078
12309
  */
11079
- _closePath(path22) {
11080
- this._closeFile(path22);
11081
- const dir = sp2.dirname(path22);
11082
- this._getWatchedDir(dir).remove(sp2.basename(path22));
12310
+ _closePath(path24) {
12311
+ this._closeFile(path24);
12312
+ const dir = sp2.dirname(path24);
12313
+ this._getWatchedDir(dir).remove(sp2.basename(path24));
11083
12314
  }
11084
12315
  /**
11085
12316
  * Closes only file-specific watchers
11086
12317
  */
11087
- _closeFile(path22) {
11088
- const closers = this._closers.get(path22);
12318
+ _closeFile(path24) {
12319
+ const closers = this._closers.get(path24);
11089
12320
  if (!closers)
11090
12321
  return;
11091
12322
  closers.forEach((closer) => closer());
11092
- this._closers.delete(path22);
12323
+ this._closers.delete(path24);
11093
12324
  }
11094
- _addPathCloser(path22, closer) {
12325
+ _addPathCloser(path24, closer) {
11095
12326
  if (!closer)
11096
12327
  return;
11097
- let list = this._closers.get(path22);
12328
+ let list = this._closers.get(path24);
11098
12329
  if (!list) {
11099
12330
  list = [];
11100
- this._closers.set(path22, list);
12331
+ this._closers.set(path24, list);
11101
12332
  }
11102
12333
  list.push(closer);
11103
12334
  }
@@ -11127,7 +12358,7 @@ function watch(paths, options = {}) {
11127
12358
  var chokidar_default = { watch, FSWatcher };
11128
12359
 
11129
12360
  // src/watcher/file-watcher.ts
11130
- import * as path15 from "path";
12361
+ import * as path17 from "path";
11131
12362
  var FileWatcher = class {
11132
12363
  watcher = null;
11133
12364
  projectRoot;
@@ -11138,6 +12369,8 @@ var FileWatcher = class {
11138
12369
  debounceTimer = null;
11139
12370
  debounceMs = 1e3;
11140
12371
  onChanges = null;
12372
+ readyPromise = null;
12373
+ resolveReady = null;
11141
12374
  constructor(projectRoot, config, host = "opencode", options = {}) {
11142
12375
  this.projectRoot = projectRoot;
11143
12376
  this.config = config;
@@ -11153,15 +12386,15 @@ var FileWatcher = class {
11153
12386
  const watchTargets = this.configPath ? [this.projectRoot, this.configPath] : this.projectRoot;
11154
12387
  this.watcher = chokidar_default.watch(watchTargets, {
11155
12388
  ignored: (filePath) => {
11156
- const relativePath = path15.relative(this.projectRoot, filePath);
12389
+ const relativePath = path17.relative(this.projectRoot, filePath);
11157
12390
  if (!relativePath) return false;
11158
12391
  if (this.isProjectConfigPathOrAncestor(relativePath)) {
11159
12392
  return false;
11160
12393
  }
11161
- if (hasFilteredPathSegment(relativePath, path15.sep)) {
12394
+ if (hasFilteredPathSegment(relativePath, path17.sep)) {
11162
12395
  return true;
11163
12396
  }
11164
- if (isRestrictedDirectory(relativePath, path15.sep)) {
12397
+ if (isRestrictedDirectory(relativePath, path17.sep)) {
11165
12398
  return true;
11166
12399
  }
11167
12400
  if (ignoreFilter.ignores(relativePath)) {
@@ -11176,6 +12409,13 @@ var FileWatcher = class {
11176
12409
  pollInterval: 100
11177
12410
  }
11178
12411
  });
12412
+ this.readyPromise = new Promise((resolve13) => {
12413
+ this.resolveReady = resolve13;
12414
+ });
12415
+ this.watcher.once("ready", () => {
12416
+ this.resolveReady?.();
12417
+ this.resolveReady = null;
12418
+ });
11179
12419
  this.watcher.on("error", (error) => {
11180
12420
  const err = error instanceof Error ? error : null;
11181
12421
  if (err?.code === "EPERM" || err?.code === "EACCES") {
@@ -11207,24 +12447,24 @@ var FileWatcher = class {
11207
12447
  this.scheduleFlush();
11208
12448
  }
11209
12449
  isProjectConfigPath(filePath) {
11210
- const relativePath = path15.relative(this.projectRoot, filePath);
11211
- const normalizedRelativePath = path15.normalize(relativePath);
12450
+ const relativePath = path17.relative(this.projectRoot, filePath);
12451
+ const normalizedRelativePath = path17.normalize(relativePath);
11212
12452
  return this.getProjectConfigRelativePaths().some((configPath) => configPath === normalizedRelativePath);
11213
12453
  }
11214
12454
  isProjectConfigPathOrAncestor(relativePath) {
11215
- const normalizedRelativePath = path15.normalize(relativePath);
12455
+ const normalizedRelativePath = path17.normalize(relativePath);
11216
12456
  return this.getProjectConfigRelativePaths().some(
11217
- (configPath) => configPath === normalizedRelativePath || configPath.startsWith(`${normalizedRelativePath}${path15.sep}`)
12457
+ (configPath) => configPath === normalizedRelativePath || configPath.startsWith(`${normalizedRelativePath}${path17.sep}`)
11218
12458
  );
11219
12459
  }
11220
12460
  getProjectConfigRelativePaths() {
11221
12461
  if (this.configPath) {
11222
- return [path15.normalize(path15.relative(this.projectRoot, this.configPath))];
12462
+ return [path17.normalize(path17.relative(this.projectRoot, this.configPath))];
11223
12463
  }
11224
12464
  return [
11225
12465
  resolveProjectConfigPath(this.projectRoot, this.host),
11226
12466
  resolveWritableProjectConfigPath(this.projectRoot, this.host)
11227
- ].map((configPath) => path15.normalize(path15.relative(this.projectRoot, configPath)));
12467
+ ].map((configPath) => path17.normalize(path17.relative(this.projectRoot, configPath)));
11228
12468
  }
11229
12469
  scheduleFlush() {
11230
12470
  if (this.debounceTimer) {
@@ -11239,7 +12479,7 @@ var FileWatcher = class {
11239
12479
  return;
11240
12480
  }
11241
12481
  const changes = Array.from(this.pendingChanges.entries()).map(
11242
- ([path22, type]) => ({ path: path22, type })
12482
+ ([path24, type]) => ({ path: path24, type })
11243
12483
  );
11244
12484
  this.pendingChanges.clear();
11245
12485
  try {
@@ -11248,25 +12488,32 @@ var FileWatcher = class {
11248
12488
  console.error("Error handling file changes:", error);
11249
12489
  }
11250
12490
  }
11251
- stop() {
12491
+ async stop() {
11252
12492
  if (this.debounceTimer) {
11253
12493
  clearTimeout(this.debounceTimer);
11254
12494
  this.debounceTimer = null;
11255
12495
  }
11256
12496
  if (this.watcher) {
11257
- this.watcher.close();
12497
+ const watcher = this.watcher;
11258
12498
  this.watcher = null;
12499
+ await watcher.close();
11259
12500
  }
12501
+ this.resolveReady?.();
12502
+ this.resolveReady = null;
12503
+ this.readyPromise = null;
11260
12504
  this.pendingChanges.clear();
11261
12505
  this.onChanges = null;
11262
12506
  }
11263
12507
  isRunning() {
11264
12508
  return this.watcher !== null;
11265
12509
  }
12510
+ async waitUntilReady() {
12511
+ await (this.readyPromise ?? Promise.resolve());
12512
+ }
11266
12513
  };
11267
12514
 
11268
12515
  // src/watcher/git-head-watcher.ts
11269
- import * as path16 from "path";
12516
+ import * as path18 from "path";
11270
12517
  var GitHeadWatcher = class {
11271
12518
  watcher = null;
11272
12519
  projectRoot;
@@ -11288,7 +12535,7 @@ var GitHeadWatcher = class {
11288
12535
  this.onBranchChange = handler;
11289
12536
  this.currentBranch = getCurrentBranch(this.projectRoot);
11290
12537
  const headPath = getHeadPath(this.projectRoot);
11291
- const refsPath = path16.join(this.projectRoot, ".git", "refs", "heads");
12538
+ const refsPath = path18.join(this.projectRoot, ".git", "refs", "heads");
11292
12539
  this.watcher = chokidar_default.watch([headPath, refsPath], {
11293
12540
  persistent: true,
11294
12541
  ignoreInitial: true,
@@ -11325,14 +12572,15 @@ var GitHeadWatcher = class {
11325
12572
  getCurrentBranch() {
11326
12573
  return this.currentBranch;
11327
12574
  }
11328
- stop() {
12575
+ async stop() {
11329
12576
  if (this.debounceTimer) {
11330
12577
  clearTimeout(this.debounceTimer);
11331
12578
  this.debounceTimer = null;
11332
12579
  }
11333
12580
  if (this.watcher) {
11334
- this.watcher.close();
12581
+ const watcher = this.watcher;
11335
12582
  this.watcher = null;
12583
+ await watcher.close();
11336
12584
  }
11337
12585
  this.onBranchChange = null;
11338
12586
  }
@@ -11350,6 +12598,8 @@ var BackgroundReindexer = class {
11350
12598
  running = false;
11351
12599
  pending = false;
11352
12600
  stopped = false;
12601
+ retryTimer = null;
12602
+ retryDelayMs = 50;
11353
12603
  request() {
11354
12604
  if (this.stopped) {
11355
12605
  return;
@@ -11360,9 +12610,13 @@ var BackgroundReindexer = class {
11360
12610
  stop() {
11361
12611
  this.stopped = true;
11362
12612
  this.pending = false;
12613
+ if (this.retryTimer) {
12614
+ clearTimeout(this.retryTimer);
12615
+ this.retryTimer = null;
12616
+ }
11363
12617
  }
11364
12618
  drain() {
11365
- if (this.stopped || this.running || !this.pending) {
12619
+ if (this.stopped || this.running || this.retryTimer || !this.pending) {
11366
12620
  return;
11367
12621
  }
11368
12622
  this.pending = false;
@@ -11370,13 +12624,29 @@ var BackgroundReindexer = class {
11370
12624
  void this.run();
11371
12625
  }
11372
12626
  async run() {
12627
+ let retryAfterContention = false;
11373
12628
  try {
11374
12629
  await this.runIndex();
12630
+ this.retryDelayMs = 50;
11375
12631
  } catch (error) {
11376
- console.error("[codebase-index] Background reindex failed:", error);
12632
+ if (isTransientIndexLockContention(error)) {
12633
+ this.pending = true;
12634
+ retryAfterContention = true;
12635
+ } else {
12636
+ console.error("[codebase-index] Background reindex failed:", error);
12637
+ }
11377
12638
  } finally {
11378
12639
  this.running = false;
11379
- this.drain();
12640
+ if (retryAfterContention && !this.stopped) {
12641
+ const delay = this.retryDelayMs;
12642
+ this.retryDelayMs = Math.min(this.retryDelayMs * 2, 500);
12643
+ this.retryTimer = setTimeout(() => {
12644
+ this.retryTimer = null;
12645
+ this.drain();
12646
+ }, delay);
12647
+ } else {
12648
+ this.drain();
12649
+ }
11380
12650
  }
11381
12651
  }
11382
12652
  };
@@ -11410,10 +12680,12 @@ function createWatcherWithIndexer(getIndexer, projectRoot, config, host = "openc
11410
12680
  return {
11411
12681
  fileWatcher,
11412
12682
  gitWatcher,
11413
- stop() {
12683
+ whenReady() {
12684
+ return fileWatcher.waitUntilReady();
12685
+ },
12686
+ async stop() {
11414
12687
  backgroundReindexer.stop();
11415
- fileWatcher.stop();
11416
- gitWatcher?.stop();
12688
+ await Promise.all([fileWatcher.stop(), gitWatcher?.stop()]);
11417
12689
  }
11418
12690
  };
11419
12691
  }
@@ -11519,13 +12791,13 @@ var pr_impact = tool({
11519
12791
  });
11520
12792
 
11521
12793
  // src/tools/index.ts
11522
- import { writeFileSync as writeFileSync4 } from "fs";
11523
- import * as os4 from "os";
11524
- import * as path19 from "path";
12794
+ import { writeFileSync as writeFileSync5 } from "fs";
12795
+ import * as os5 from "os";
12796
+ import * as path21 from "path";
11525
12797
 
11526
12798
  // src/tools/visualize/activity.ts
11527
12799
  import { execFileSync } from "child_process";
11528
- import * as path17 from "path";
12800
+ import * as path19 from "path";
11529
12801
  function attachRecentActivity(data, projectRoot) {
11530
12802
  const activity = readGitActivity(projectRoot);
11531
12803
  const changes = activity.size > 0 ? buildGitChanges(data, activity, projectRoot) : buildGraphChanges(data);
@@ -11687,7 +12959,7 @@ function normalizePath2(filePath) {
11687
12959
  return filePath.replace(/\\/g, "/");
11688
12960
  }
11689
12961
  function toGitRelativePath(projectRoot, filePath) {
11690
- const relativePath = path17.isAbsolute(filePath) ? path17.relative(projectRoot, filePath) : filePath;
12962
+ const relativePath = path19.isAbsolute(filePath) ? path19.relative(projectRoot, filePath) : filePath;
11691
12963
  return normalizePath2(relativePath);
11692
12964
  }
11693
12965
 
@@ -11945,7 +13217,7 @@ render();
11945
13217
  }
11946
13218
 
11947
13219
  // src/tools/visualize/transform.ts
11948
- import * as path18 from "path";
13220
+ import * as path20 from "path";
11949
13221
 
11950
13222
  // src/tools/visualize/modules.ts
11951
13223
  var MAX_MODULES = 18;
@@ -12078,8 +13350,8 @@ function compactModules(prefixToNodes) {
12078
13350
  function deriveModules(nodes) {
12079
13351
  const initial = /* @__PURE__ */ new Map();
12080
13352
  for (const node of nodes) {
12081
- const relative10 = stripToProjectRelative(node.filePath);
12082
- const prefix = modulePrefixFromRelativePath(relative10);
13353
+ const relative11 = stripToProjectRelative(node.filePath);
13354
+ const prefix = modulePrefixFromRelativePath(relative11);
12083
13355
  if (!initial.has(prefix)) initial.set(prefix, []);
12084
13356
  initial.get(prefix)?.push(node);
12085
13357
  }
@@ -12205,7 +13477,7 @@ function transformForVisualization(symbols, edges, options = {}) {
12205
13477
  filePath: s.filePath,
12206
13478
  kind: s.kind,
12207
13479
  line: s.startLine,
12208
- directory: path18.dirname(s.filePath),
13480
+ directory: path20.dirname(s.filePath),
12209
13481
  moduleId: "",
12210
13482
  moduleLabel: ""
12211
13483
  }));
@@ -12256,7 +13528,10 @@ var codebase_peek = tool2({
12256
13528
  limit: z2.number().optional().default(10).describe("Maximum number of results to return"),
12257
13529
  fileType: z2.string().optional().describe("Filter by file extension (e.g., 'ts', 'py', 'rs')"),
12258
13530
  directory: z2.string().optional().describe("Filter by directory path (e.g., 'src/utils', 'lib')"),
12259
- chunkType: z2.enum(CHUNK_TYPE_VALUES).optional().describe("Filter by code chunk type")
13531
+ chunkType: z2.enum(CHUNK_TYPE_VALUES).optional().describe("Filter by code chunk type"),
13532
+ blameAuthor: z2.string().optional().describe("Filter by git blame author name or email"),
13533
+ blameSha: z2.string().optional().describe("Filter by git blame commit SHA or prefix"),
13534
+ blameSince: z2.string().optional().describe("Filter to chunks last changed on or after this date (e.g., 2025-01-01)")
12260
13535
  },
12261
13536
  async execute(args, context) {
12262
13537
  const results = await searchCodebase(context?.worktree, DEFAULT_HOST, args.query, {
@@ -12264,7 +13539,10 @@ var codebase_peek = tool2({
12264
13539
  fileType: args.fileType,
12265
13540
  directory: args.directory,
12266
13541
  chunkType: args.chunkType,
12267
- metadataOnly: true
13542
+ metadataOnly: true,
13543
+ blameAuthor: args.blameAuthor,
13544
+ blameSha: args.blameSha,
13545
+ blameSince: args.blameSince
12268
13546
  });
12269
13547
  return formatCodebasePeek(results);
12270
13548
  }
@@ -12280,7 +13558,9 @@ var index_codebase = tool2({
12280
13558
  const result = await runIndexCodebase(context?.worktree, DEFAULT_HOST, args, (title, metadata) => {
12281
13559
  context.metadata({ title, metadata });
12282
13560
  });
12283
- return result.kind === "estimate" ? formatCostEstimate(result.estimate) : formatIndexStats(result.stats, args.verbose ?? false);
13561
+ if (result.kind === "estimate") return formatCostEstimate(result.estimate);
13562
+ if (result.kind === "busy") return result.text;
13563
+ return formatIndexStats(result.stats, args.verbose ?? false);
12284
13564
  }
12285
13565
  });
12286
13566
  var index_status = tool2({
@@ -12294,7 +13574,9 @@ var index_health_check = tool2({
12294
13574
  description: "Check index health and remove stale entries from deleted files. Run this to clean up the index after files have been deleted.",
12295
13575
  args: {},
12296
13576
  async execute(_args, context) {
12297
- return formatHealthCheck(await getIndexHealthCheck(context?.worktree, DEFAULT_HOST));
13577
+ const result = await runIndexHealthCheck(context?.worktree, DEFAULT_HOST);
13578
+ if (result.kind === "busy") return result.text;
13579
+ return formatHealthCheck(result.health);
12298
13580
  }
12299
13581
  });
12300
13582
  var index_metrics = tool2({
@@ -12347,7 +13629,10 @@ var codebase_search = tool2({
12347
13629
  fileType: z2.string().optional().describe("Filter by file extension (e.g., 'ts', 'py', 'rs')"),
12348
13630
  directory: z2.string().optional().describe("Filter by directory path (e.g., 'src/utils', 'lib')"),
12349
13631
  chunkType: z2.enum(CHUNK_TYPE_VALUES).optional().describe("Filter by code chunk type"),
12350
- contextLines: z2.number().optional().describe("Number of extra lines to include before/after each match (default: 0)")
13632
+ contextLines: z2.number().optional().describe("Number of extra lines to include before/after each match (default: 0)"),
13633
+ blameAuthor: z2.string().optional().describe("Filter by git blame author name or email"),
13634
+ blameSha: z2.string().optional().describe("Filter by git blame commit SHA or prefix"),
13635
+ blameSince: z2.string().optional().describe("Filter to chunks last changed on or after this date (e.g., 2025-01-01)")
12351
13636
  },
12352
13637
  async execute(args, context) {
12353
13638
  const results = await searchCodebase(context?.worktree, DEFAULT_HOST, args.query, {
@@ -12355,7 +13640,10 @@ var codebase_search = tool2({
12355
13640
  fileType: args.fileType,
12356
13641
  directory: args.directory,
12357
13642
  chunkType: args.chunkType,
12358
- contextLines: args.contextLines
13643
+ contextLines: args.contextLines,
13644
+ blameAuthor: args.blameAuthor,
13645
+ blameSha: args.blameSha,
13646
+ blameSince: args.blameSince
12359
13647
  });
12360
13648
  if (results.length === 0) {
12361
13649
  return "No matching code found. Try a different query or run index_codebase first.";
@@ -12394,22 +13682,10 @@ var call_graph = tool2({
12394
13682
  return "Error: 'symbolId' is required when direction is 'callees'. First use direction='callers' to find the symbol ID.";
12395
13683
  }
12396
13684
  const { callees } = await getCallGraphData(context?.worktree, DEFAULT_HOST, args);
12397
- if (callees.length === 0) {
12398
- return `No callees found for symbol ${args.symbolId}${args.relationshipType ? ` with type ${args.relationshipType}` : ""}. The function may not call any other tracked functions.`;
12399
- }
12400
- return callees.map((edge, index) => {
12401
- const confidence = edge.confidence !== "Direct" ? ` [${edge.confidence.toLowerCase()}]` : "";
12402
- return `[${index + 1}] \u2192 ${edge.targetName} (${edge.callType})${confidence} at line ${edge.line}${edge.isResolved ? ` [resolved: ${edge.toSymbolId}]` : " [unresolved]"}`;
12403
- }).join("\n");
13685
+ return formatCallGraphCallees(args.symbolId, callees, args.relationshipType);
12404
13686
  }
12405
13687
  const { callers } = await getCallGraphData(context?.worktree, DEFAULT_HOST, args);
12406
- if (callers.length === 0) {
12407
- return `No callers found for "${args.name}"${args.relationshipType ? ` with type ${args.relationshipType}` : ""}. It may not be called by any tracked function, or the index needs updating.`;
12408
- }
12409
- return callers.map((edge, index) => {
12410
- const confidence = edge.confidence !== "Direct" ? ` [${edge.confidence.toLowerCase()}]` : "";
12411
- return `[${index + 1}] \u2190 from ${edge.fromSymbolName ?? "<unknown>"} in ${edge.fromSymbolFilePath ?? "<unknown file>"} [${edge.fromSymbolId}] (${edge.callType})${confidence} at line ${edge.line}${edge.isResolved ? " [resolved]" : " [unresolved]"}`;
12412
- }).join("\n");
13688
+ return formatCallGraphCallers(args.name, callers, args.relationshipType);
12413
13689
  }
12414
13690
  });
12415
13691
  var call_graph_path = tool2({
@@ -12420,17 +13696,8 @@ var call_graph_path = tool2({
12420
13696
  maxDepth: z2.number().optional().default(10).describe("Maximum traversal depth (default: 10)")
12421
13697
  },
12422
13698
  async execute(args, context) {
12423
- const path22 = await getCallGraphPath(context?.worktree, DEFAULT_HOST, args.from, args.to, args.maxDepth);
12424
- if (path22.length === 0) {
12425
- return `No path found between "${args.from}" and "${args.to}". They may be in disconnected components, or the call graph index needs updating.`;
12426
- }
12427
- const formatted = path22.map((hop, index) => {
12428
- const prefix = index === 0 ? "[start]" : `--${hop.callType}-->`;
12429
- const location = hop.filePath ? ` (${hop.filePath}:${hop.line})` : "";
12430
- return `${prefix} ${hop.symbolName}${location}`;
12431
- });
12432
- return `Path (${path22.length} hops):
12433
- ${formatted.join("\n")}`;
13699
+ const path24 = await getCallGraphPath(context?.worktree, DEFAULT_HOST, args.from, args.to, args.maxDepth);
13700
+ return formatCallGraphPath(args.from, args.to, path24);
12434
13701
  }
12435
13702
  });
12436
13703
  var add_knowledge_base = tool2({
@@ -12483,8 +13750,8 @@ var index_visualize = tool2({
12483
13750
  return "No connected symbols found for visualization. Try including orphans with includeOrphans=true, or check that the call graph has resolved edges.";
12484
13751
  }
12485
13752
  const html = generateVisualizationHtml(vizData);
12486
- const outputPath = path19.join(os4.tmpdir(), `call-graph-${Date.now()}.html`);
12487
- writeFileSync4(outputPath, html, "utf-8");
13753
+ const outputPath = path21.join(os5.tmpdir(), `call-graph-${Date.now()}.html`);
13754
+ writeFileSync5(outputPath, html, "utf-8");
12488
13755
  let result = `Temporal call graph visualization generated: ${outputPath}
12489
13756
 
12490
13757
  `;
@@ -12505,8 +13772,8 @@ var index_visualize = tool2({
12505
13772
  });
12506
13773
 
12507
13774
  // src/commands/loader.ts
12508
- import { existsSync as existsSync10, readdirSync as readdirSync2, readFileSync as readFileSync7 } from "fs";
12509
- import * as path20 from "path";
13775
+ import { existsSync as existsSync11, readdirSync as readdirSync3, readFileSync as readFileSync8 } from "fs";
13776
+ import * as path22 from "path";
12510
13777
  function parseFrontmatter(content) {
12511
13778
  const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
12512
13779
  const match = content.match(frontmatterRegex);
@@ -12527,21 +13794,21 @@ function parseFrontmatter(content) {
12527
13794
  }
12528
13795
  function loadCommandsFromDirectory(commandsDir) {
12529
13796
  const commands = /* @__PURE__ */ new Map();
12530
- if (!existsSync10(commandsDir)) {
13797
+ if (!existsSync11(commandsDir)) {
12531
13798
  return commands;
12532
13799
  }
12533
- const files = readdirSync2(commandsDir).filter((f) => f.endsWith(".md"));
13800
+ const files = readdirSync3(commandsDir).filter((f) => f.endsWith(".md"));
12534
13801
  for (const file of files) {
12535
- const filePath = path20.join(commandsDir, file);
13802
+ const filePath = path22.join(commandsDir, file);
12536
13803
  let content;
12537
13804
  try {
12538
- content = readFileSync7(filePath, "utf-8");
13805
+ content = readFileSync8(filePath, "utf-8");
12539
13806
  } catch (error) {
12540
13807
  const message = error instanceof Error ? error.message : String(error);
12541
13808
  throw new Error(`Failed to load command file ${filePath}: ${message}`);
12542
13809
  }
12543
13810
  const { frontmatter, body } = parseFrontmatter(content);
12544
- const name = path20.basename(file, ".md");
13811
+ const name = path22.basename(file, ".md");
12545
13812
  const description = frontmatter.description || `Run the ${name} command`;
12546
13813
  commands.set(name, {
12547
13814
  description,
@@ -12845,18 +14112,37 @@ var RoutingHintController = class {
12845
14112
  };
12846
14113
 
12847
14114
  // src/utils/auto-index.ts
14115
+ var INITIAL_RETRY_DELAY_MS = 50;
14116
+ var MAX_RETRY_DELAY_MS = 500;
14117
+ var autoIndexStates = /* @__PURE__ */ new WeakMap();
12848
14118
  function getErrorMessage3(error) {
12849
14119
  return error instanceof Error ? error.message : String(error);
12850
14120
  }
12851
- function startAutoIndex(indexer, projectRoot) {
12852
- indexer.initialize().then(() => {
12853
- indexer.index().catch((error) => {
12854
- console.error(`[codebase-index] Auto-index failed for "${projectRoot}": ${getErrorMessage3(error)}`);
12855
- });
14121
+ function runAutoIndex(indexer, projectRoot, state) {
14122
+ void indexer.index().then(() => {
14123
+ autoIndexStates.delete(indexer);
12856
14124
  }).catch((error) => {
12857
- console.error(`[codebase-index] Auto-index initialization failed for "${projectRoot}": ${getErrorMessage3(error)}`);
14125
+ if (!isTransientIndexLockContention(error)) {
14126
+ autoIndexStates.delete(indexer);
14127
+ console.error(`[codebase-index] Auto-index failed for "${projectRoot}": ${getErrorMessage3(error)}`);
14128
+ return;
14129
+ }
14130
+ const retryDelayMs = state.retryDelayMs;
14131
+ state.retryDelayMs = Math.min(retryDelayMs * 2, MAX_RETRY_DELAY_MS);
14132
+ const retryTimer = setTimeout(() => {
14133
+ runAutoIndex(indexer, projectRoot, state);
14134
+ }, retryDelayMs);
14135
+ retryTimer.unref?.();
12858
14136
  });
12859
14137
  }
14138
+ function startAutoIndex(indexer, projectRoot) {
14139
+ if (autoIndexStates.has(indexer)) return;
14140
+ const state = {
14141
+ retryDelayMs: INITIAL_RETRY_DELAY_MS
14142
+ };
14143
+ autoIndexStates.set(indexer, state);
14144
+ runAutoIndex(indexer, projectRoot, state);
14145
+ }
12860
14146
 
12861
14147
  // src/index.ts
12862
14148
  var activeWatchers = /* @__PURE__ */ new Map();
@@ -12873,9 +14159,9 @@ function replaceActiveWatcher(projectRoot, nextWatcher) {
12873
14159
  function getCommandsDir() {
12874
14160
  let currentDir = process.cwd();
12875
14161
  if (typeof import.meta !== "undefined" && import.meta.url) {
12876
- currentDir = path21.dirname(fileURLToPath2(import.meta.url));
14162
+ currentDir = path23.dirname(fileURLToPath2(import.meta.url));
12877
14163
  }
12878
- return path21.join(currentDir, "..", "commands");
14164
+ return path23.join(currentDir, "..", "commands");
12879
14165
  }
12880
14166
  function appendRoutingHints(output, hints, preferredRole) {
12881
14167
  const preferredBucket = preferredRole === "developer" ? output.developer : output.system;
@@ -12895,7 +14181,7 @@ var plugin = async ({ directory, worktree }) => {
12895
14181
  initializeTools2(projectRoot, config);
12896
14182
  const getProjectIndexer = () => getIndexerForProject2(projectRoot);
12897
14183
  const routingHints = config.search.routingHints ? new RoutingHintController(() => getProjectIndexer().getStatus(), 200, config.search.routingGraphHandoffHints) : null;
12898
- const isHomeDir = path21.resolve(projectRoot) === path21.resolve(os5.homedir());
14184
+ const isHomeDir = path23.resolve(projectRoot) === path23.resolve(os6.homedir());
12899
14185
  const isValidProject = !isHomeDir && (!config.indexing.requireProjectMarker || hasProjectMarker(projectRoot));
12900
14186
  if (isHomeDir) {
12901
14187
  console.warn(