opencode-codebase-index 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -491,7 +491,7 @@ var require_ignore = __commonJS({
491
491
  // path matching.
492
492
  // - check `string` either `MODE_IGNORE` or `MODE_CHECK_IGNORE`
493
493
  // @returns {TestResult} true if a file is ignored
494
- test(path26, checkUnignored, mode) {
494
+ test(path27, checkUnignored, mode) {
495
495
  let ignored = false;
496
496
  let unignored = false;
497
497
  let matchedRule;
@@ -500,7 +500,7 @@ var require_ignore = __commonJS({
500
500
  if (unignored === negative && ignored !== unignored || negative && !ignored && !unignored && !checkUnignored) {
501
501
  return;
502
502
  }
503
- const matched = rule[mode].test(path26);
503
+ const matched = rule[mode].test(path27);
504
504
  if (!matched) {
505
505
  return;
506
506
  }
@@ -521,17 +521,17 @@ var require_ignore = __commonJS({
521
521
  var throwError = (message, Ctor) => {
522
522
  throw new Ctor(message);
523
523
  };
524
- var checkPath = (path26, originalPath, doThrow) => {
525
- if (!isString(path26)) {
524
+ var checkPath = (path27, originalPath, doThrow) => {
525
+ if (!isString(path27)) {
526
526
  return doThrow(
527
527
  `path must be a string, but got \`${originalPath}\``,
528
528
  TypeError
529
529
  );
530
530
  }
531
- if (!path26) {
531
+ if (!path27) {
532
532
  return doThrow(`path must not be empty`, TypeError);
533
533
  }
534
- if (checkPath.isNotRelative(path26)) {
534
+ if (checkPath.isNotRelative(path27)) {
535
535
  const r = "`path.relative()`d";
536
536
  return doThrow(
537
537
  `path should be a ${r} string, but got "${originalPath}"`,
@@ -540,7 +540,7 @@ var require_ignore = __commonJS({
540
540
  }
541
541
  return true;
542
542
  };
543
- var isNotRelative = (path26) => REGEX_TEST_INVALID_PATH.test(path26);
543
+ var isNotRelative = (path27) => REGEX_TEST_INVALID_PATH.test(path27);
544
544
  checkPath.isNotRelative = isNotRelative;
545
545
  checkPath.convert = (p) => p;
546
546
  var Ignore2 = class {
@@ -570,19 +570,19 @@ var require_ignore = __commonJS({
570
570
  }
571
571
  // @returns {TestResult}
572
572
  _test(originalPath, cache, checkUnignored, slices) {
573
- const path26 = originalPath && checkPath.convert(originalPath);
573
+ const path27 = originalPath && checkPath.convert(originalPath);
574
574
  checkPath(
575
- path26,
575
+ path27,
576
576
  originalPath,
577
577
  this._strictPathCheck ? throwError : RETURN_FALSE
578
578
  );
579
- return this._t(path26, cache, checkUnignored, slices);
579
+ return this._t(path27, cache, checkUnignored, slices);
580
580
  }
581
- checkIgnore(path26) {
582
- if (!REGEX_TEST_TRAILING_SLASH.test(path26)) {
583
- return this.test(path26);
581
+ checkIgnore(path27) {
582
+ if (!REGEX_TEST_TRAILING_SLASH.test(path27)) {
583
+ return this.test(path27);
584
584
  }
585
- const slices = path26.split(SLASH2).filter(Boolean);
585
+ const slices = path27.split(SLASH2).filter(Boolean);
586
586
  slices.pop();
587
587
  if (slices.length) {
588
588
  const parent = this._t(
@@ -595,18 +595,18 @@ var require_ignore = __commonJS({
595
595
  return parent;
596
596
  }
597
597
  }
598
- return this._rules.test(path26, false, MODE_CHECK_IGNORE);
598
+ return this._rules.test(path27, false, MODE_CHECK_IGNORE);
599
599
  }
600
- _t(path26, cache, checkUnignored, slices) {
601
- if (path26 in cache) {
602
- return cache[path26];
600
+ _t(path27, cache, checkUnignored, slices) {
601
+ if (path27 in cache) {
602
+ return cache[path27];
603
603
  }
604
604
  if (!slices) {
605
- slices = path26.split(SLASH2).filter(Boolean);
605
+ slices = path27.split(SLASH2).filter(Boolean);
606
606
  }
607
607
  slices.pop();
608
608
  if (!slices.length) {
609
- return cache[path26] = this._rules.test(path26, checkUnignored, MODE_IGNORE);
609
+ return cache[path27] = this._rules.test(path27, checkUnignored, MODE_IGNORE);
610
610
  }
611
611
  const parent = this._t(
612
612
  slices.join(SLASH2) + SLASH2,
@@ -614,29 +614,29 @@ var require_ignore = __commonJS({
614
614
  checkUnignored,
615
615
  slices
616
616
  );
617
- return cache[path26] = parent.ignored ? parent : this._rules.test(path26, checkUnignored, MODE_IGNORE);
617
+ return cache[path27] = parent.ignored ? parent : this._rules.test(path27, checkUnignored, MODE_IGNORE);
618
618
  }
619
- ignores(path26) {
620
- return this._test(path26, this._ignoreCache, false).ignored;
619
+ ignores(path27) {
620
+ return this._test(path27, this._ignoreCache, false).ignored;
621
621
  }
622
622
  createFilter() {
623
- return (path26) => !this.ignores(path26);
623
+ return (path27) => !this.ignores(path27);
624
624
  }
625
625
  filter(paths) {
626
626
  return makeArray(paths).filter(this.createFilter());
627
627
  }
628
628
  // @returns {TestResult}
629
- test(path26) {
630
- return this._test(path26, this._testCache, true);
629
+ test(path27) {
630
+ return this._test(path27, this._testCache, true);
631
631
  }
632
632
  };
633
633
  var factory = (options) => new Ignore2(options);
634
- var isPathValid = (path26) => checkPath(path26 && checkPath.convert(path26), path26, RETURN_FALSE);
634
+ var isPathValid = (path27) => checkPath(path27 && checkPath.convert(path27), path27, RETURN_FALSE);
635
635
  var setupWindows = () => {
636
636
  const makePosix = (str) => /^\\\\\?\\/.test(str) || /["<>|\u0000-\u001F]+/u.test(str) ? str : str.replace(/\\/g, "/");
637
637
  checkPath.convert = makePosix;
638
638
  const REGEX_TEST_WINDOWS_PATH_ABSOLUTE = /^[a-z]:\//i;
639
- checkPath.isNotRelative = (path26) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path26) || isNotRelative(path26);
639
+ checkPath.isNotRelative = (path27) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path27) || isNotRelative(path27);
640
640
  };
641
641
  if (
642
642
  // Detect `process` so that it can run in browsers.
@@ -653,17 +653,17 @@ var require_ignore = __commonJS({
653
653
 
654
654
  // src/cli.ts
655
655
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
656
- import { realpathSync as realpathSync2, writeFileSync as writeFileSync6 } from "fs";
657
- import * as os5 from "os";
658
- import * as path25 from "path";
656
+ import { realpathSync as realpathSync3, writeFileSync as writeFileSync7 } from "fs";
657
+ import * as os6 from "os";
658
+ import * as path26 from "path";
659
659
  import { fileURLToPath as fileURLToPath2 } from "url";
660
660
 
661
661
  // src/config/constants.ts
662
662
  var DEFAULT_INCLUDE = [
663
- "**/*.{ts,tsx,js,jsx,mjs,cjs}",
663
+ "**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}",
664
664
  "**/*.{py,pyi}",
665
- "**/*.{go,rs,java,kt,scala}",
666
- "**/*.{c,cpp,cc,h,hpp}",
665
+ "**/*.{go,rs,java,cs,kt,scala}",
666
+ "**/*.{c,cpp,cc,cxx,h,hpp,hxx}",
667
667
  "**/*.{rb,php,inc,swift}",
668
668
  "**/*.{cls,trigger}",
669
669
  "**/*.{vue,svelte,astro}",
@@ -1046,7 +1046,7 @@ var autoDetectProviders = AUTO_DETECT_PROVIDER_ORDER.filter(
1046
1046
  );
1047
1047
 
1048
1048
  // src/config/host.ts
1049
- var HOST_MODES = ["opencode", "codex", "claude", "pi"];
1049
+ var HOST_MODES = ["opencode", "codex", "claude", "pi", "jcode"];
1050
1050
  function isSupportedHostMode(value) {
1051
1051
  return HOST_MODES.includes(value);
1052
1052
  }
@@ -1101,9 +1101,9 @@ import { mkdirSync, readFileSync, writeFileSync } from "fs";
1101
1101
  import * as path from "path";
1102
1102
 
1103
1103
  // src/eval/report-formatters.ts
1104
- function assertFiniteNumber(value, path26) {
1104
+ function assertFiniteNumber(value, path27) {
1105
1105
  if (typeof value !== "number" || Number.isNaN(value) || !Number.isFinite(value)) {
1106
- throw new Error(`${path26} must be a finite number`);
1106
+ throw new Error(`${path27} must be a finite number`);
1107
1107
  }
1108
1108
  return value;
1109
1109
  }
@@ -1291,13 +1291,13 @@ function buildPerQueryArtifact(perQuery) {
1291
1291
  }
1292
1292
 
1293
1293
  // src/eval/runner.ts
1294
- import { existsSync as existsSync8 } from "fs";
1295
- import * as path14 from "path";
1294
+ import { existsSync as existsSync9 } from "fs";
1295
+ import * as path15 from "path";
1296
1296
  import { performance as performance3 } from "perf_hooks";
1297
1297
 
1298
1298
  // src/indexer/index.ts
1299
- import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync2, renameSync, unlinkSync, mkdirSync as mkdirSync2, promises as fsPromises2 } from "fs";
1300
- import * as path11 from "path";
1299
+ import { existsSync as existsSync7, readFileSync as readFileSync7, statSync as statSync3, writeFileSync as writeFileSync3, renameSync as renameSync2, unlinkSync, mkdirSync as mkdirSync3, promises as fsPromises2 } from "fs";
1300
+ import * as path12 from "path";
1301
1301
  import { performance as performance2 } from "perf_hooks";
1302
1302
  import { execFile as execFile3 } from "child_process";
1303
1303
  import { promisify as promisify3 } from "util";
@@ -2579,17 +2579,17 @@ function validateExternalUrl(urlString) {
2579
2579
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
2580
2580
  return { valid: false, reason: `Blocked protocol: ${parsed.protocol}` };
2581
2581
  }
2582
- const hostname = parsed.hostname.toLowerCase();
2583
- if (BLOCKED_HOSTNAMES.has(hostname)) {
2584
- return { valid: false, reason: `Blocked: cloud metadata service (${hostname})` };
2582
+ const hostname2 = parsed.hostname.toLowerCase();
2583
+ if (BLOCKED_HOSTNAMES.has(hostname2)) {
2584
+ return { valid: false, reason: `Blocked: cloud metadata service (${hostname2})` };
2585
2585
  }
2586
2586
  for (const pattern of BLOCKED_METADATA_IPS) {
2587
- if (pattern.test(hostname)) {
2588
- return { valid: false, reason: `Blocked: cloud metadata IP (${hostname})` };
2587
+ if (pattern.test(hostname2)) {
2588
+ return { valid: false, reason: `Blocked: cloud metadata IP (${hostname2})` };
2589
2589
  }
2590
2590
  }
2591
- if (/^169\.254\./.test(hostname)) {
2592
- return { valid: false, reason: `Blocked: link-local address (${hostname})` };
2591
+ if (/^169\.254\./.test(hostname2)) {
2592
+ return { valid: false, reason: `Blocked: link-local address (${hostname2})` };
2593
2593
  }
2594
2594
  return { valid: true };
2595
2595
  }
@@ -3779,6 +3779,12 @@ function createMockNativeBinding() {
3779
3779
  constructor() {
3780
3780
  throw error;
3781
3781
  }
3782
+ static openReadOnly() {
3783
+ throw error;
3784
+ }
3785
+ static createEmptyReadOnly() {
3786
+ throw error;
3787
+ }
3782
3788
  close() {
3783
3789
  throw error;
3784
3790
  }
@@ -3882,6 +3888,12 @@ var VectorStore = class {
3882
3888
  load() {
3883
3889
  this.inner.load();
3884
3890
  }
3891
+ loadStrict() {
3892
+ this.inner.loadStrict();
3893
+ }
3894
+ hasFingerprint() {
3895
+ return this.inner.hasFingerprint();
3896
+ }
3885
3897
  count() {
3886
3898
  return this.inner.count();
3887
3899
  }
@@ -4164,12 +4176,24 @@ var InvertedIndex = class {
4164
4176
  return this.inner.documentCount();
4165
4177
  }
4166
4178
  };
4167
- var Database = class {
4179
+ var Database = class _Database {
4168
4180
  inner;
4169
4181
  closed = false;
4170
4182
  constructor(dbPath) {
4171
4183
  this.inner = new native.Database(dbPath);
4172
4184
  }
4185
+ static fromNative(inner) {
4186
+ const database = Object.create(_Database.prototype);
4187
+ database.inner = inner;
4188
+ database.closed = false;
4189
+ return database;
4190
+ }
4191
+ static openReadOnly(dbPath) {
4192
+ return _Database.fromNative(native.Database.openReadOnly(dbPath));
4193
+ }
4194
+ static createEmptyReadOnly() {
4195
+ return _Database.fromNative(native.Database.createEmptyReadOnly());
4196
+ }
4173
4197
  throwIfClosed() {
4174
4198
  if (this.closed) {
4175
4199
  throw new Error("Database is closed");
@@ -4638,6 +4662,9 @@ function getHostProjectIndexRelativePath(host) {
4638
4662
  function hasHostProjectConfig(projectRoot, host) {
4639
4663
  return existsSync5(path8.join(projectRoot, getProjectConfigRelativePath(host)));
4640
4664
  }
4665
+ function hasHostGlobalConfig(host) {
4666
+ return existsSync5(getGlobalConfigPath(host));
4667
+ }
4641
4668
  function getGlobalIndexPath(host = "opencode") {
4642
4669
  switch (host) {
4643
4670
  case "opencode":
@@ -4677,6 +4704,9 @@ function resolveGlobalIndexPath(host = "opencode") {
4677
4704
  return hostIndexPath;
4678
4705
  }
4679
4706
  if (host !== "opencode") {
4707
+ if (hasHostGlobalConfig(host)) {
4708
+ return hostIndexPath;
4709
+ }
4680
4710
  const legacyIndexPath = getGlobalIndexPath("opencode");
4681
4711
  if (existsSync5(legacyIndexPath)) {
4682
4712
  return legacyIndexPath;
@@ -4893,6 +4923,432 @@ async function getChunkGitBlame(projectRoot, filePath, startLine, endLine) {
4893
4923
  }
4894
4924
  }
4895
4925
 
4926
+ // src/indexer/index-lock.ts
4927
+ import { randomUUID } from "crypto";
4928
+ import {
4929
+ existsSync as existsSync6,
4930
+ lstatSync,
4931
+ mkdirSync as mkdirSync2,
4932
+ readFileSync as readFileSync6,
4933
+ readdirSync as readdirSync2,
4934
+ realpathSync,
4935
+ renameSync,
4936
+ rmSync,
4937
+ writeFileSync as writeFileSync2
4938
+ } from "fs";
4939
+ import * as os4 from "os";
4940
+ import * as path11 from "path";
4941
+ var OWNER_FILE_NAME = "owner.json";
4942
+ var RECLAIM_DIRECTORY_NAME = "reclaiming";
4943
+ var RECOVERY_MARKER_PREFIX = "indexing.lock.recovery.";
4944
+ 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;
4945
+ var VALID_OPERATIONS = /* @__PURE__ */ new Set([
4946
+ "initialize",
4947
+ "index",
4948
+ "force-index",
4949
+ "clear",
4950
+ "health-check",
4951
+ "retry-failed-batches",
4952
+ "recovery"
4953
+ ]);
4954
+ var temporaryCounter = 0;
4955
+ function getErrorCode(error) {
4956
+ return typeof error === "object" && error !== null && "code" in error ? String(error.code) : void 0;
4957
+ }
4958
+ function retryTransientFilesystemOperation(operation) {
4959
+ let lastError;
4960
+ for (let attempt = 0; attempt < 3; attempt += 1) {
4961
+ try {
4962
+ operation();
4963
+ return;
4964
+ } catch (error) {
4965
+ lastError = error;
4966
+ const code = getErrorCode(error);
4967
+ if (code !== "EBUSY" && code !== "EPERM") throw error;
4968
+ if (attempt < 2) {
4969
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, (attempt + 1) * 10);
4970
+ }
4971
+ }
4972
+ }
4973
+ throw lastError;
4974
+ }
4975
+ function parseOwner(value) {
4976
+ if (typeof value !== "object" || value === null) return null;
4977
+ const candidate = value;
4978
+ if (!Number.isInteger(candidate.pid) || (candidate.pid ?? 0) <= 0) return null;
4979
+ if (typeof candidate.hostname !== "string" || candidate.hostname.length === 0) return null;
4980
+ if (typeof candidate.startedAt !== "string" || Number.isNaN(Date.parse(candidate.startedAt))) return null;
4981
+ if (typeof candidate.operation !== "string" || !VALID_OPERATIONS.has(candidate.operation)) return null;
4982
+ if (typeof candidate.token !== "string" || !UUID_PATTERN.test(candidate.token)) return null;
4983
+ return candidate;
4984
+ }
4985
+ function parseReclaimOwner(value) {
4986
+ if (typeof value !== "object" || value === null) return null;
4987
+ const candidate = value;
4988
+ if (!Number.isInteger(candidate.pid) || (candidate.pid ?? 0) <= 0) return null;
4989
+ if (typeof candidate.hostname !== "string" || candidate.hostname.length === 0) return null;
4990
+ if (typeof candidate.startedAt !== "string" || Number.isNaN(Date.parse(candidate.startedAt))) return null;
4991
+ if (typeof candidate.token !== "string" || !UUID_PATTERN.test(candidate.token)) return null;
4992
+ if (typeof candidate.expectedOwnerToken !== "string" || !UUID_PATTERN.test(candidate.expectedOwnerToken)) return null;
4993
+ return candidate;
4994
+ }
4995
+ function readJsonDirectory(directoryPath, parser) {
4996
+ try {
4997
+ return parser(JSON.parse(readFileSync6(path11.join(directoryPath, OWNER_FILE_NAME), "utf-8")));
4998
+ } catch {
4999
+ return null;
5000
+ }
5001
+ }
5002
+ function readDirectoryOwner(lockPath) {
5003
+ return readJsonDirectory(lockPath, parseOwner);
5004
+ }
5005
+ function readReclaimOwner(markerPath) {
5006
+ return readJsonDirectory(markerPath, parseReclaimOwner);
5007
+ }
5008
+ function readRecoveryOwner(markerPath) {
5009
+ return readDirectoryOwner(markerPath);
5010
+ }
5011
+ function readLegacyOwner(lockPath) {
5012
+ try {
5013
+ const parsed = JSON.parse(readFileSync6(lockPath, "utf-8"));
5014
+ if (!Number.isInteger(parsed.pid) || Number(parsed.pid) <= 0) return null;
5015
+ if (typeof parsed.startedAt !== "string" || Number.isNaN(Date.parse(parsed.startedAt))) return null;
5016
+ return {
5017
+ pid: Number(parsed.pid),
5018
+ hostname: typeof parsed.hostname === "string" ? parsed.hostname : os4.hostname(),
5019
+ startedAt: parsed.startedAt,
5020
+ operation: typeof parsed.operation === "string" && VALID_OPERATIONS.has(parsed.operation) ? parsed.operation : "index",
5021
+ token: typeof parsed.token === "string" ? parsed.token : "legacy-v0.14.0"
5022
+ };
5023
+ } catch {
5024
+ return null;
5025
+ }
5026
+ }
5027
+ function getOwnerLiveness(owner) {
5028
+ if (owner.hostname !== os4.hostname()) return "unknown";
5029
+ try {
5030
+ process.kill(owner.pid, 0);
5031
+ return "alive";
5032
+ } catch (error) {
5033
+ const code = getErrorCode(error);
5034
+ if (code === "ESRCH") return "dead";
5035
+ if (code === "EPERM") return "alive";
5036
+ return "unknown";
5037
+ }
5038
+ }
5039
+ function sameOwner(left, right) {
5040
+ return left.pid === right.pid && left.hostname === right.hostname && left.token === right.token;
5041
+ }
5042
+ function sameReclaimOwner(left, right) {
5043
+ return left.pid === right.pid && left.hostname === right.hostname && left.token === right.token && left.expectedOwnerToken === right.expectedOwnerToken;
5044
+ }
5045
+ function publishJsonDirectory(finalPath, value) {
5046
+ const candidatePath = `${finalPath}.candidate.${process.pid}.${randomUUID()}`;
5047
+ mkdirSync2(candidatePath, { mode: 448 });
5048
+ try {
5049
+ writeFileSync2(path11.join(candidatePath, OWNER_FILE_NAME), JSON.stringify(value), {
5050
+ encoding: "utf-8",
5051
+ flag: "wx",
5052
+ mode: 384
5053
+ });
5054
+ if (existsSync6(finalPath)) return false;
5055
+ try {
5056
+ renameSync(candidatePath, finalPath);
5057
+ return true;
5058
+ } catch (error) {
5059
+ if (existsSync6(finalPath)) return false;
5060
+ throw error;
5061
+ }
5062
+ } finally {
5063
+ if (existsSync6(candidatePath)) rmSync(candidatePath, { recursive: true, force: true });
5064
+ }
5065
+ }
5066
+ function createOwner(operation) {
5067
+ return {
5068
+ pid: process.pid,
5069
+ hostname: os4.hostname(),
5070
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
5071
+ operation,
5072
+ token: randomUUID()
5073
+ };
5074
+ }
5075
+ function recoveryMarkerPath(indexPath, owner) {
5076
+ return path11.join(indexPath, `${RECOVERY_MARKER_PREFIX}${owner.token}`);
5077
+ }
5078
+ function publishRecoveryMarker(indexPath, owner) {
5079
+ const markerPath = recoveryMarkerPath(indexPath, owner);
5080
+ if (!publishJsonDirectory(markerPath, owner)) {
5081
+ const markerOwner = readRecoveryOwner(markerPath);
5082
+ if (!markerOwner || !sameOwner(markerOwner, owner)) {
5083
+ throw new IndexLockContentionError(markerPath, markerOwner, "unknown-owner");
5084
+ }
5085
+ }
5086
+ return markerPath;
5087
+ }
5088
+ function getPendingRecoveries(indexPath) {
5089
+ const recoveries = [];
5090
+ const markerNames = readdirSync2(indexPath).filter((name) => {
5091
+ if (!name.startsWith(RECOVERY_MARKER_PREFIX)) return false;
5092
+ return UUID_PATTERN.test(name.slice(RECOVERY_MARKER_PREFIX.length));
5093
+ }).sort();
5094
+ for (const markerName of markerNames) {
5095
+ const markerPath = path11.join(indexPath, markerName);
5096
+ const markerToken = markerName.slice(RECOVERY_MARKER_PREFIX.length);
5097
+ let markerStats;
5098
+ try {
5099
+ markerStats = lstatSync(markerPath);
5100
+ } catch (error) {
5101
+ if (getErrorCode(error) === "ENOENT") continue;
5102
+ throw error;
5103
+ }
5104
+ if (!markerStats.isDirectory()) {
5105
+ throw new IndexLockContentionError(markerPath, null, "unknown-owner");
5106
+ }
5107
+ const owner = readRecoveryOwner(markerPath);
5108
+ if (!owner || owner.token !== markerToken || getOwnerLiveness(owner) !== "dead") {
5109
+ throw new IndexLockContentionError(markerPath, owner, "unknown-owner");
5110
+ }
5111
+ recoveries.push({ owner, markerPath });
5112
+ }
5113
+ recoveries.sort((left, right) => {
5114
+ const byTimestamp = left.owner.startedAt.localeCompare(right.owner.startedAt);
5115
+ return byTimestamp !== 0 ? byTimestamp : left.markerPath.localeCompare(right.markerPath);
5116
+ });
5117
+ return recoveries;
5118
+ }
5119
+ function cleanupDeadPublicationCandidates(indexPath) {
5120
+ const candidatePattern = /^indexing\.lock(?:\.recovery\.[0-9a-f-]{36})?\.candidate\.(\d+)\.[0-9a-f-]{36}$/i;
5121
+ for (const entry of readdirSync2(indexPath)) {
5122
+ const match = candidatePattern.exec(entry);
5123
+ if (!match) continue;
5124
+ const pid = Number(match[1]);
5125
+ if (!Number.isInteger(pid) || pid <= 0) continue;
5126
+ if (getOwnerLiveness({ pid, hostname: os4.hostname() }) !== "dead") continue;
5127
+ rmSync(path11.join(indexPath, entry), { recursive: true, force: true });
5128
+ }
5129
+ }
5130
+ function removeDeadReclaimMarker(lockPath, expectedOwner) {
5131
+ const markerPath = path11.join(lockPath, RECLAIM_DIRECTORY_NAME);
5132
+ const marker = readReclaimOwner(markerPath);
5133
+ if (!marker || marker.expectedOwnerToken !== expectedOwner.token || getOwnerLiveness(marker) !== "dead") {
5134
+ return false;
5135
+ }
5136
+ const currentOwner = readDirectoryOwner(lockPath);
5137
+ if (!currentOwner || !sameOwner(currentOwner, expectedOwner) || getOwnerLiveness(currentOwner) !== "dead") {
5138
+ return false;
5139
+ }
5140
+ const claimedMarkerPath = `${markerPath}.stale.${marker.pid}.${marker.token}.${randomUUID()}`;
5141
+ try {
5142
+ renameSync(markerPath, claimedMarkerPath);
5143
+ } catch (error) {
5144
+ if (getErrorCode(error) === "ENOENT") return false;
5145
+ throw error;
5146
+ }
5147
+ const claimedMarker = readReclaimOwner(claimedMarkerPath);
5148
+ const ownerAfterClaim = readDirectoryOwner(lockPath);
5149
+ if (!claimedMarker || !sameReclaimOwner(claimedMarker, marker) || !ownerAfterClaim || !sameOwner(ownerAfterClaim, expectedOwner) || getOwnerLiveness(ownerAfterClaim) !== "dead") {
5150
+ if (!existsSync6(markerPath) && existsSync6(claimedMarkerPath)) {
5151
+ renameSync(claimedMarkerPath, markerPath);
5152
+ }
5153
+ return false;
5154
+ }
5155
+ rmSync(claimedMarkerPath, { recursive: true, force: true });
5156
+ return true;
5157
+ }
5158
+ function reclaimDeadOwner(indexPath, lockPath, expectedOwner) {
5159
+ const reclaimPath = path11.join(lockPath, RECLAIM_DIRECTORY_NAME);
5160
+ const reclaimOwner = {
5161
+ pid: process.pid,
5162
+ hostname: os4.hostname(),
5163
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
5164
+ token: randomUUID(),
5165
+ expectedOwnerToken: expectedOwner.token
5166
+ };
5167
+ for (let attempt = 0; attempt < 2; attempt += 1) {
5168
+ if (publishJsonDirectory(reclaimPath, reclaimOwner)) break;
5169
+ if (attempt === 0 && removeDeadReclaimMarker(lockPath, expectedOwner)) continue;
5170
+ return false;
5171
+ }
5172
+ try {
5173
+ const currentReclaimer = readReclaimOwner(reclaimPath);
5174
+ const currentOwner = readDirectoryOwner(lockPath);
5175
+ if (!currentReclaimer || !sameReclaimOwner(currentReclaimer, reclaimOwner) || !currentOwner || !sameOwner(currentOwner, expectedOwner) || getOwnerLiveness(currentOwner) !== "dead") {
5176
+ return false;
5177
+ }
5178
+ publishRecoveryMarker(indexPath, expectedOwner);
5179
+ const ownerBeforeQuarantine = readDirectoryOwner(lockPath);
5180
+ const reclaimerBeforeQuarantine = readReclaimOwner(reclaimPath);
5181
+ if (!ownerBeforeQuarantine || !sameOwner(ownerBeforeQuarantine, expectedOwner) || getOwnerLiveness(ownerBeforeQuarantine) !== "dead" || !reclaimerBeforeQuarantine || !sameReclaimOwner(reclaimerBeforeQuarantine, reclaimOwner)) {
5182
+ return false;
5183
+ }
5184
+ const quarantinePath = `${lockPath}.stale.${expectedOwner.token}.${reclaimOwner.token}`;
5185
+ renameSync(lockPath, quarantinePath);
5186
+ rmSync(quarantinePath, { recursive: true, force: true });
5187
+ return true;
5188
+ } catch (error) {
5189
+ if (getErrorCode(error) === "ENOENT") return false;
5190
+ throw error;
5191
+ }
5192
+ }
5193
+ var IndexLockContentionError = class extends Error {
5194
+ constructor(lockPath, owner, reason) {
5195
+ const ownerDescription = owner ? `PID ${owner.pid} on ${owner.hostname}, operation ${owner.operation}, since ${owner.startedAt}` : "an unreadable owner";
5196
+ super(`Index mutation already in progress: ${ownerDescription}`);
5197
+ this.lockPath = lockPath;
5198
+ this.owner = owner;
5199
+ this.reason = reason;
5200
+ this.name = "IndexLockContentionError";
5201
+ }
5202
+ lockPath;
5203
+ owner;
5204
+ reason;
5205
+ code = "INDEX_BUSY";
5206
+ };
5207
+ function isIndexLockContentionError(error) {
5208
+ return error instanceof IndexLockContentionError || typeof error === "object" && error !== null && "code" in error && error.code === "INDEX_BUSY";
5209
+ }
5210
+ function isTransientIndexLockContention(error) {
5211
+ if (!isIndexLockContentionError(error) || !("reason" in error)) return false;
5212
+ return error.reason === "active" || error.reason === "reclaiming";
5213
+ }
5214
+ function acquireIndexLock(indexPath, operation) {
5215
+ mkdirSync2(indexPath, { recursive: true });
5216
+ const canonicalIndexPath = realpathSync.native(indexPath);
5217
+ const lockPath = path11.join(canonicalIndexPath, "indexing.lock");
5218
+ cleanupDeadPublicationCandidates(canonicalIndexPath);
5219
+ for (let attempt = 0; attempt < 6; attempt += 1) {
5220
+ const owner = createOwner(operation);
5221
+ if (publishJsonDirectory(lockPath, owner)) {
5222
+ const lease = {
5223
+ canonicalIndexPath,
5224
+ lockPath,
5225
+ owner,
5226
+ recoveries: []
5227
+ };
5228
+ try {
5229
+ lease.recoveries = getPendingRecoveries(canonicalIndexPath);
5230
+ return lease;
5231
+ } catch (error) {
5232
+ releaseIndexLock(lease);
5233
+ throw error;
5234
+ }
5235
+ }
5236
+ let stats;
5237
+ try {
5238
+ stats = lstatSync(lockPath);
5239
+ } catch (error) {
5240
+ if (getErrorCode(error) === "ENOENT") continue;
5241
+ throw error;
5242
+ }
5243
+ if (!stats.isDirectory()) {
5244
+ const legacyOwner = readLegacyOwner(lockPath);
5245
+ throw new IndexLockContentionError(lockPath, legacyOwner, "legacy-lock");
5246
+ }
5247
+ const existingOwner = readDirectoryOwner(lockPath);
5248
+ if (!existingOwner) {
5249
+ throw new IndexLockContentionError(lockPath, null, "unknown-owner");
5250
+ }
5251
+ const liveness = getOwnerLiveness(existingOwner);
5252
+ if (liveness !== "dead") {
5253
+ throw new IndexLockContentionError(lockPath, existingOwner, liveness === "alive" ? "active" : "unknown-owner");
5254
+ }
5255
+ if (!reclaimDeadOwner(canonicalIndexPath, lockPath, existingOwner)) {
5256
+ throw new IndexLockContentionError(lockPath, existingOwner, "reclaiming");
5257
+ }
5258
+ }
5259
+ throw new IndexLockContentionError(lockPath, null, "reclaiming");
5260
+ }
5261
+ function releaseIndexLock(lease) {
5262
+ const currentOwner = readDirectoryOwner(lease.lockPath);
5263
+ if (!currentOwner || !sameOwner(currentOwner, lease.owner)) return false;
5264
+ const releasePath = `${lease.lockPath}.release.${lease.owner.pid}.${lease.owner.token}`;
5265
+ try {
5266
+ retryTransientFilesystemOperation(() => renameSync(lease.lockPath, releasePath));
5267
+ } catch (error) {
5268
+ if (getErrorCode(error) === "ENOENT") return false;
5269
+ throw error;
5270
+ }
5271
+ const claimedOwner = readDirectoryOwner(releasePath);
5272
+ if (!claimedOwner || !sameOwner(claimedOwner, lease.owner)) {
5273
+ if (!existsSync6(lease.lockPath) && existsSync6(releasePath)) {
5274
+ renameSync(releasePath, lease.lockPath);
5275
+ }
5276
+ return false;
5277
+ }
5278
+ try {
5279
+ retryTransientFilesystemOperation(() => rmSync(releasePath, { recursive: true, force: true }));
5280
+ } catch (error) {
5281
+ console.error(`[codebase-index] Lease released but tombstone cleanup failed: ${releasePath}`, error);
5282
+ }
5283
+ return true;
5284
+ }
5285
+ async function withIndexLock(indexPath, operation, callback, options = {}) {
5286
+ const lease = acquireIndexLock(indexPath, operation);
5287
+ let result;
5288
+ let callbackError;
5289
+ let callbackFailed = false;
5290
+ try {
5291
+ result = await callback(lease);
5292
+ } catch (error) {
5293
+ callbackFailed = true;
5294
+ callbackError = error;
5295
+ }
5296
+ if (!callbackFailed && options.completeRecoveries !== false) {
5297
+ try {
5298
+ completeLeaseRecovery(lease);
5299
+ } catch (error) {
5300
+ callbackFailed = true;
5301
+ callbackError = error;
5302
+ }
5303
+ }
5304
+ let releaseError;
5305
+ try {
5306
+ if (!releaseIndexLock(lease)) {
5307
+ releaseError = new Error(`Lost ownership of index mutation lease ${lease.owner.token}`);
5308
+ }
5309
+ } catch (error) {
5310
+ releaseError = error;
5311
+ }
5312
+ if (releaseError !== void 0) {
5313
+ if (callbackFailed) throw new AggregateError([callbackError, releaseError], "Index mutation and lease release both failed");
5314
+ throw releaseError;
5315
+ }
5316
+ if (callbackFailed) throw callbackError;
5317
+ return result;
5318
+ }
5319
+ function createLeaseTemporaryPath(targetPath, owner, kind = "tmp") {
5320
+ if (kind === "bak") return `${targetPath}.bak.${owner.pid}.${owner.token}`;
5321
+ temporaryCounter += 1;
5322
+ return `${targetPath}.tmp.${owner.pid}.${owner.token}.${temporaryCounter}`;
5323
+ }
5324
+ function removeLeaseTemporaryPath(temporaryPath) {
5325
+ if (existsSync6(temporaryPath)) rmSync(temporaryPath, { recursive: true, force: true });
5326
+ }
5327
+ function recoverLeaseArtifacts(indexPath, owner, backupTargets) {
5328
+ for (const targetPath of backupTargets) {
5329
+ const backupPath = createLeaseTemporaryPath(targetPath, owner, "bak");
5330
+ if (!existsSync6(backupPath)) continue;
5331
+ if (existsSync6(targetPath)) rmSync(targetPath, { recursive: true, force: true });
5332
+ renameSync(backupPath, targetPath);
5333
+ }
5334
+ const temporaryOwnerMarker = `.tmp.${owner.pid}.${owner.token}.`;
5335
+ for (const entry of readdirSync2(indexPath)) {
5336
+ if (!entry.includes(temporaryOwnerMarker)) continue;
5337
+ rmSync(path11.join(indexPath, entry), { recursive: true, force: true });
5338
+ }
5339
+ }
5340
+ function completeLeaseRecovery(lease) {
5341
+ for (const recovery of lease.recoveries) {
5342
+ const markerOwner = readRecoveryOwner(recovery.markerPath);
5343
+ if (!markerOwner || !sameOwner(markerOwner, recovery.owner)) {
5344
+ throw new Error(`Recovery marker ownership changed: ${recovery.markerPath}`);
5345
+ }
5346
+ }
5347
+ for (const recovery of lease.recoveries) {
5348
+ rmSync(recovery.markerPath, { recursive: true, force: true });
5349
+ }
5350
+ }
5351
+
4896
5352
  // src/indexer/index.ts
4897
5353
  var CALL_GRAPH_LANGUAGES = /* @__PURE__ */ new Set(["typescript", "tsx", "javascript", "jsx", "python", "go", "rust", "php", "apex", "zig", "gdscript", "matlab", "bash"]);
4898
5354
  var CASE_INSENSITIVE_LANGUAGES = /* @__PURE__ */ new Set(["apex"]);
@@ -4974,6 +5430,7 @@ function isSqliteCorruptionError(error) {
4974
5430
  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");
4975
5431
  }
4976
5432
  var STARTUP_WARNING_METADATA_KEY = "index.startupWarning";
5433
+ var READER_ARTIFACT_RETRY_INTERVAL_MS = 1e3;
4977
5434
  function metadataFromBlame(blame) {
4978
5435
  if (!blame) {
4979
5436
  return {};
@@ -5171,9 +5628,9 @@ function hasAllEmbeddingParts(parts, expectedPartCount) {
5171
5628
  return true;
5172
5629
  }
5173
5630
  function isPathWithinRoot(filePath, rootPath) {
5174
- const normalizedFilePath = path11.resolve(filePath);
5175
- const normalizedRoot = path11.resolve(rootPath);
5176
- return normalizedFilePath === normalizedRoot || normalizedFilePath.startsWith(`${normalizedRoot}${path11.sep}`);
5631
+ const normalizedFilePath = path12.resolve(filePath);
5632
+ const normalizedRoot = path12.resolve(rootPath);
5633
+ return normalizedFilePath === normalizedRoot || normalizedFilePath.startsWith(`${normalizedRoot}${path12.sep}`);
5177
5634
  }
5178
5635
  var rankingQueryTokenCache = /* @__PURE__ */ new Map();
5179
5636
  var rankingNameTokenCache = /* @__PURE__ */ new Map();
@@ -6239,26 +6696,133 @@ var Indexer = class {
6239
6696
  queryCacheTtlMs = 5 * 60 * 1e3;
6240
6697
  querySimilarityThreshold = 0.85;
6241
6698
  indexCompatibility = null;
6242
- indexingLockPath = "";
6699
+ activeIndexLease = null;
6700
+ initializationPromise = null;
6701
+ initializationMode = "none";
6702
+ readIssues = [];
6703
+ retiredDatabases = [];
6704
+ readerArtifactFingerprint = null;
6705
+ writerArtifactFingerprint = null;
6706
+ readerArtifactRetryAfter = /* @__PURE__ */ new Map();
6243
6707
  constructor(projectRoot, config, host = "opencode") {
6244
6708
  this.projectRoot = projectRoot;
6245
6709
  this.config = config;
6246
6710
  this.host = host;
6247
6711
  this.indexPath = this.getIndexPath();
6248
- this.fileHashCachePath = path11.join(this.indexPath, "file-hashes.json");
6249
- this.failedBatchesPath = path11.join(this.indexPath, "failed-batches.json");
6250
- this.indexingLockPath = path11.join(this.indexPath, "indexing.lock");
6712
+ this.fileHashCachePath = path12.join(this.indexPath, "file-hashes.json");
6713
+ this.failedBatchesPath = path12.join(this.indexPath, "failed-batches.json");
6251
6714
  this.logger = initializeLogger(config.debug);
6252
6715
  }
6253
6716
  getIndexPath() {
6254
6717
  return resolveProjectIndexPath(this.projectRoot, this.config.scope, this.host);
6255
6718
  }
6719
+ isLocalProjectIndexPath() {
6720
+ const localProjectIndexPaths = [path12.join(this.projectRoot, getHostProjectIndexRelativePath(this.host))];
6721
+ if (this.host !== "opencode") {
6722
+ localProjectIndexPaths.push(path12.join(this.projectRoot, getHostProjectIndexRelativePath("opencode")));
6723
+ }
6724
+ return localProjectIndexPaths.some((localPath) => {
6725
+ if (!existsSync7(localPath) || !existsSync7(this.indexPath)) {
6726
+ return path12.resolve(this.indexPath) === path12.resolve(localPath);
6727
+ }
6728
+ const indexStats = statSync3(this.indexPath);
6729
+ const localStats = statSync3(localPath);
6730
+ return indexStats.dev === localStats.dev && indexStats.ino === localStats.ino;
6731
+ });
6732
+ }
6733
+ resetLoadedIndexState(retireDatabase = false) {
6734
+ if (this.database) {
6735
+ if (retireDatabase) {
6736
+ this.retiredDatabases.push(this.database);
6737
+ } else {
6738
+ this.database.close();
6739
+ }
6740
+ }
6741
+ this.store = null;
6742
+ this.invertedIndex = null;
6743
+ this.database = null;
6744
+ this.provider = null;
6745
+ this.configuredProviderInfo = null;
6746
+ this.reranker = null;
6747
+ this.indexCompatibility = null;
6748
+ this.initializationMode = "none";
6749
+ this.readIssues = [];
6750
+ this.readerArtifactFingerprint = null;
6751
+ this.writerArtifactFingerprint = null;
6752
+ this.readerArtifactRetryAfter.clear();
6753
+ this.fileHashCache.clear();
6754
+ }
6755
+ refreshLoadedIndexState() {
6756
+ if (!this.store || !this.invertedIndex || !this.configuredProviderInfo) return;
6757
+ this.store.load();
6758
+ this.invertedIndex.load();
6759
+ this.fileHashCache.clear();
6760
+ this.loadFileHashCache();
6761
+ this.indexCompatibility = this.validateIndexCompatibility(this.configuredProviderInfo);
6762
+ this.readIssues = [];
6763
+ this.readerArtifactRetryAfter.clear();
6764
+ }
6765
+ async withIndexMutationLease(operation, callback) {
6766
+ const lease = acquireIndexLock(this.indexPath, operation);
6767
+ this.indexPath = lease.canonicalIndexPath;
6768
+ this.fileHashCachePath = path12.join(this.indexPath, "file-hashes.json");
6769
+ this.failedBatchesPath = path12.join(this.indexPath, "failed-batches.json");
6770
+ this.activeIndexLease = lease;
6771
+ let result;
6772
+ let callbackError;
6773
+ let callbackFailed = false;
6774
+ try {
6775
+ result = await callback(lease.recoveries.map(({ owner }) => owner));
6776
+ } catch (error) {
6777
+ callbackFailed = true;
6778
+ callbackError = error;
6779
+ }
6780
+ if (!callbackFailed) {
6781
+ try {
6782
+ completeLeaseRecovery(lease);
6783
+ this.writerArtifactFingerprint = this.captureReaderArtifactFingerprint();
6784
+ } catch (error) {
6785
+ callbackFailed = true;
6786
+ callbackError = error;
6787
+ }
6788
+ }
6789
+ let releaseError;
6790
+ try {
6791
+ if (!releaseIndexLock(lease)) {
6792
+ releaseError = new Error(`Lost ownership of index mutation lease ${lease.owner.token}`);
6793
+ this.writerArtifactFingerprint = null;
6794
+ if (this.activeIndexLease?.owner.token === lease.owner.token) {
6795
+ this.activeIndexLease = null;
6796
+ }
6797
+ } else if (this.activeIndexLease?.owner.token === lease.owner.token) {
6798
+ this.activeIndexLease = null;
6799
+ }
6800
+ } catch (error) {
6801
+ releaseError = error;
6802
+ this.writerArtifactFingerprint = null;
6803
+ if (!existsSync7(lease.lockPath) && this.activeIndexLease?.owner.token === lease.owner.token) {
6804
+ this.activeIndexLease = null;
6805
+ }
6806
+ }
6807
+ if (releaseError !== void 0) {
6808
+ if (callbackFailed) throw new AggregateError([callbackError, releaseError], "Index mutation and lease release both failed");
6809
+ throw releaseError;
6810
+ }
6811
+ if (callbackFailed) throw callbackError;
6812
+ return result;
6813
+ }
6814
+ requireActiveLease() {
6815
+ if (!this.activeIndexLease) {
6816
+ throw new Error("Index mutation attempted without an active interprocess lease");
6817
+ }
6818
+ return this.activeIndexLease;
6819
+ }
6256
6820
  loadFileHashCache() {
6257
- if (!existsSync6(this.fileHashCachePath)) {
6821
+ if (!existsSync7(this.fileHashCachePath)) {
6258
6822
  return;
6259
6823
  }
6260
6824
  try {
6261
- const data = readFileSync6(this.fileHashCachePath, "utf-8");
6825
+ const data = readFileSync7(this.fileHashCachePath, "utf-8");
6262
6826
  const parsed = JSON.parse(data);
6263
6827
  this.fileHashCache = new Map(Object.entries(parsed));
6264
6828
  } catch (error) {
@@ -6278,15 +6842,26 @@ var Indexer = class {
6278
6842
  this.atomicWriteSync(this.fileHashCachePath, JSON.stringify(obj));
6279
6843
  }
6280
6844
  atomicWriteSync(targetPath, data) {
6281
- const tempPath = `${targetPath}.tmp`;
6282
- mkdirSync2(path11.dirname(targetPath), { recursive: true });
6283
- writeFileSync2(tempPath, data);
6284
- renameSync(tempPath, targetPath);
6845
+ const lease = this.requireActiveLease();
6846
+ const tempPath = createLeaseTemporaryPath(targetPath, lease.owner, "tmp");
6847
+ mkdirSync3(path12.dirname(targetPath), { recursive: true });
6848
+ try {
6849
+ writeFileSync3(tempPath, data);
6850
+ renameSync2(tempPath, targetPath);
6851
+ } finally {
6852
+ removeLeaseTemporaryPath(tempPath);
6853
+ }
6854
+ }
6855
+ saveInvertedIndex(invertedIndex) {
6856
+ this.atomicWriteSync(
6857
+ path12.join(this.indexPath, "inverted-index.json"),
6858
+ invertedIndex.serialize()
6859
+ );
6285
6860
  }
6286
6861
  getScopedRoots() {
6287
- const roots = /* @__PURE__ */ new Set([path11.resolve(this.projectRoot)]);
6862
+ const roots = /* @__PURE__ */ new Set([path12.resolve(this.projectRoot)]);
6288
6863
  for (const kbRoot of this.config.knowledgeBases) {
6289
- roots.add(path11.resolve(this.projectRoot, kbRoot));
6864
+ roots.add(path12.resolve(this.projectRoot, kbRoot));
6290
6865
  }
6291
6866
  return Array.from(roots);
6292
6867
  }
@@ -6295,29 +6870,29 @@ var Indexer = class {
6295
6870
  if (this.config.scope !== "global") {
6296
6871
  return branchName;
6297
6872
  }
6298
- const projectHash = hashContent(path11.resolve(this.projectRoot)).slice(0, 16);
6873
+ const projectHash = hashContent(path12.resolve(this.projectRoot)).slice(0, 16);
6299
6874
  return `${projectHash}:${branchName}`;
6300
6875
  }
6301
6876
  getBranchCatalogKeyFor(branchName) {
6302
6877
  if (this.config.scope !== "global") {
6303
6878
  return branchName;
6304
6879
  }
6305
- const projectHash = hashContent(path11.resolve(this.projectRoot)).slice(0, 16);
6880
+ const projectHash = hashContent(path12.resolve(this.projectRoot)).slice(0, 16);
6306
6881
  return `${projectHash}:${branchName}`;
6307
6882
  }
6308
6883
  getLegacyBranchCatalogKey() {
6309
6884
  return this.currentBranch || "default";
6310
6885
  }
6311
6886
  getLegacyMigrationMetadataKey() {
6312
- const projectHash = hashContent(path11.resolve(this.projectRoot)).slice(0, 16);
6887
+ const projectHash = hashContent(path12.resolve(this.projectRoot)).slice(0, 16);
6313
6888
  return `index.globalBranchMigration.${projectHash}`;
6314
6889
  }
6315
6890
  getProjectEmbeddingStrategyMetadataKey() {
6316
- const projectHash = hashContent(path11.resolve(this.projectRoot)).slice(0, 16);
6891
+ const projectHash = hashContent(path12.resolve(this.projectRoot)).slice(0, 16);
6317
6892
  return `index.embeddingStrategyVersion.${projectHash}`;
6318
6893
  }
6319
6894
  getProjectForceReembedMetadataKey() {
6320
- const projectHash = hashContent(path11.resolve(this.projectRoot)).slice(0, 16);
6895
+ const projectHash = hashContent(path12.resolve(this.projectRoot)).slice(0, 16);
6321
6896
  return `index.forceReembed.${projectHash}`;
6322
6897
  }
6323
6898
  hasProjectForceReembedPending() {
@@ -6411,7 +6986,7 @@ var Indexer = class {
6411
6986
  if (!this.database) {
6412
6987
  return { chunkIds, symbolIds };
6413
6988
  }
6414
- const projectRootPath = path11.resolve(this.projectRoot);
6989
+ const projectRootPath = path12.resolve(this.projectRoot);
6415
6990
  const projectLocalFilePaths = /* @__PURE__ */ new Set([
6416
6991
  ...Array.from(this.fileHashCache.keys()).filter(
6417
6992
  (filePath) => this.isFileInCurrentScope(filePath, roots) && isPathWithinRoot(filePath, projectRootPath)
@@ -6434,7 +7009,7 @@ var Indexer = class {
6434
7009
  if (this.config.scope !== "global") {
6435
7010
  return this.getBranchCatalogCleanupKeys();
6436
7011
  }
6437
- const projectHash = hashContent(path11.resolve(this.projectRoot)).slice(0, 16);
7012
+ const projectHash = hashContent(path12.resolve(this.projectRoot)).slice(0, 16);
6438
7013
  const keys = /* @__PURE__ */ new Set();
6439
7014
  const projectChunkIdSet = new Set(projectChunkIds);
6440
7015
  const projectSymbolIdSet = new Set(projectSymbolIds);
@@ -6518,7 +7093,7 @@ var Indexer = class {
6518
7093
  if (!this.database || this.config.scope !== "global") {
6519
7094
  return false;
6520
7095
  }
6521
- const projectHash = hashContent(path11.resolve(this.projectRoot)).slice(0, 16);
7096
+ const projectHash = hashContent(path12.resolve(this.projectRoot)).slice(0, 16);
6522
7097
  const roots = this.getScopedRoots();
6523
7098
  const { chunkIds: projectLocalChunkIds, symbolIds: projectLocalSymbolIds } = this.getProjectLocalScopedOwnershipIds(roots);
6524
7099
  return this.database.getAllBranches().some(
@@ -6552,7 +7127,7 @@ var Indexer = class {
6552
7127
  ...Array.from(this.fileHashCache.keys()).filter((filePath) => this.isFileInCurrentScope(filePath, roots)),
6553
7128
  ...scopedEntries.map(({ metadata }) => metadata.filePath)
6554
7129
  ]);
6555
- const projectRootPath = path11.resolve(this.projectRoot);
7130
+ const projectRootPath = path12.resolve(this.projectRoot);
6556
7131
  const projectLocalFilePaths = new Set(
6557
7132
  Array.from(filePaths).filter((filePath) => isPathWithinRoot(filePath, projectRootPath))
6558
7133
  );
@@ -6614,34 +7189,27 @@ var Indexer = class {
6614
7189
  database.gcOrphanEmbeddings();
6615
7190
  database.gcOrphanChunks();
6616
7191
  store.save();
6617
- invertedIndex.save();
7192
+ this.saveInvertedIndex(invertedIndex);
6618
7193
  return {
6619
7194
  removedChunkIds: removedChunkIdList,
6620
7195
  hasForeignData: allMetadata.some(({ metadata }) => !this.isFileInCurrentScope(metadata.filePath, roots))
6621
7196
  };
6622
7197
  }
6623
- checkForInterruptedIndexing() {
6624
- return existsSync6(this.indexingLockPath);
6625
- }
6626
- acquireIndexingLock() {
6627
- const lockData = {
6628
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
6629
- pid: process.pid
6630
- };
6631
- writeFileSync2(this.indexingLockPath, JSON.stringify(lockData));
6632
- }
6633
- releaseIndexingLock() {
6634
- if (existsSync6(this.indexingLockPath)) {
6635
- unlinkSync(this.indexingLockPath);
7198
+ async recoverFromInterruptedIndexingUnlocked(owners) {
7199
+ for (const owner of owners) {
7200
+ this.logger.warn("Detected interrupted indexing session, recovering...", {
7201
+ pid: owner.pid,
7202
+ hostname: owner.hostname,
7203
+ operation: owner.operation,
7204
+ startedAt: owner.startedAt
7205
+ });
6636
7206
  }
6637
- }
6638
- async recoverFromInterruptedIndexing() {
6639
- this.logger.warn("Detected interrupted indexing session, recovering...");
6640
- if (existsSync6(this.fileHashCachePath)) {
6641
- unlinkSync(this.fileHashCachePath);
7207
+ if (this.config.scope === "global") {
7208
+ if (existsSync7(this.fileHashCachePath)) {
7209
+ unlinkSync(this.fileHashCachePath);
7210
+ }
7211
+ await this.healthCheckUnlocked();
6642
7212
  }
6643
- await this.healthCheck();
6644
- this.releaseIndexingLock();
6645
7213
  this.logger.info("Recovery complete, next index will re-process all files");
6646
7214
  }
6647
7215
  loadFailedBatches(maxChunkTokens) {
@@ -6657,10 +7225,10 @@ var Indexer = class {
6657
7225
  }
6658
7226
  }
6659
7227
  loadSerializedFailedBatches() {
6660
- if (!existsSync6(this.failedBatchesPath)) {
7228
+ if (!existsSync7(this.failedBatchesPath)) {
6661
7229
  return [];
6662
7230
  }
6663
- const data = readFileSync6(this.failedBatchesPath, "utf-8");
7231
+ const data = readFileSync7(this.failedBatchesPath, "utf-8");
6664
7232
  const parsed = JSON.parse(data);
6665
7233
  return parsed.map((batch) => {
6666
7234
  const chunks = Array.isArray(batch.chunks) ? batch.chunks : [];
@@ -6677,7 +7245,7 @@ var Indexer = class {
6677
7245
  }
6678
7246
  saveFailedBatches(batches) {
6679
7247
  if (batches.length === 0) {
6680
- if (existsSync6(this.failedBatchesPath)) {
7248
+ if (existsSync7(this.failedBatchesPath)) {
6681
7249
  try {
6682
7250
  unlinkSync(this.failedBatchesPath);
6683
7251
  } catch {
@@ -6685,7 +7253,7 @@ var Indexer = class {
6685
7253
  }
6686
7254
  return;
6687
7255
  }
6688
- writeFileSync2(this.failedBatchesPath, JSON.stringify(batches, null, 2));
7256
+ this.atomicWriteSync(this.failedBatchesPath, JSON.stringify(batches, null, 2));
6689
7257
  }
6690
7258
  collectRetryableFailedChunks(currentFileHashes, unchangedFilePaths, maxChunkTokens) {
6691
7259
  const retryableById = /* @__PURE__ */ new Map();
@@ -6879,6 +7447,222 @@ var Indexer = class {
6879
7447
  return parts.join("\n");
6880
7448
  }
6881
7449
  async initialize() {
7450
+ if (this.initializationPromise) {
7451
+ await this.initializationPromise;
7452
+ }
7453
+ if (this.isInitializedFor("reader")) {
7454
+ return;
7455
+ }
7456
+ await this.initializeOnce("reader", [], { skipAutoGc: true });
7457
+ }
7458
+ async initializeOnce(mode, recoveredOwners, options) {
7459
+ if (this.initializationPromise) {
7460
+ await this.initializationPromise;
7461
+ if (this.isInitializedFor(mode)) {
7462
+ return;
7463
+ }
7464
+ return this.initializeOnce(mode, recoveredOwners, options);
7465
+ }
7466
+ if (this.isInitializedFor(mode)) {
7467
+ return;
7468
+ }
7469
+ const initialization = this.initializeUnlocked(mode, recoveredOwners, options).catch((error) => {
7470
+ this.resetLoadedIndexState();
7471
+ throw error;
7472
+ }).finally(() => {
7473
+ if (this.initializationPromise === initialization) {
7474
+ this.initializationPromise = null;
7475
+ }
7476
+ });
7477
+ this.initializationPromise = initialization;
7478
+ await initialization;
7479
+ }
7480
+ isInitializedFor(mode) {
7481
+ const hasState = Boolean(
7482
+ this.store && this.provider && this.invertedIndex && this.configuredProviderInfo && this.database
7483
+ );
7484
+ if (!hasState) {
7485
+ return false;
7486
+ }
7487
+ return mode === "reader" ? this.initializationMode !== "none" : this.initializationMode === "writer";
7488
+ }
7489
+ recordReadIssue(component, message, error) {
7490
+ this.readIssues.push(this.createReadIssue(component, message));
7491
+ this.readerArtifactRetryAfter.set(component, Date.now() + READER_ARTIFACT_RETRY_INTERVAL_MS);
7492
+ this.logger.warn(message, error === void 0 ? void 0 : { error: getErrorMessage2(error) });
7493
+ }
7494
+ createReadIssue(component, message) {
7495
+ return {
7496
+ component,
7497
+ message,
7498
+ blocking: component !== "keyword"
7499
+ };
7500
+ }
7501
+ getVectorReadIssueMessage() {
7502
+ if (this.config.scope === "global") {
7503
+ return "Shared vector index could not be read. Restore or repair the complete fingerprinted shared vector artifacts; automatic reset is disabled for global scope.";
7504
+ }
7505
+ if (!this.isLocalProjectIndexPath()) {
7506
+ 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.";
7507
+ }
7508
+ 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.";
7509
+ }
7510
+ getKeywordReadIssueMessage() {
7511
+ if (this.config.scope === "global") {
7512
+ 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.";
7513
+ }
7514
+ if (!this.isLocalProjectIndexPath()) {
7515
+ 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.";
7516
+ }
7517
+ 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.";
7518
+ }
7519
+ getDatabaseReadIssueMessage() {
7520
+ if (this.config.scope === "global") {
7521
+ return "Shared index database could not be read. Restore or repair the shared SQLite database; automatic reset is disabled for global scope.";
7522
+ }
7523
+ if (!this.isLocalProjectIndexPath()) {
7524
+ 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.";
7525
+ }
7526
+ return "Index database could not be read. Run index_codebase after the active writer finishes to repair or migrate it under the writer lease.";
7527
+ }
7528
+ getReaderFileFingerprint(filePath, identityOnly = false) {
7529
+ try {
7530
+ const stats = statSync3(filePath);
7531
+ if (identityOnly) {
7532
+ return `${stats.dev}:${stats.ino}`;
7533
+ }
7534
+ return `${stats.dev}:${stats.ino}:${stats.size}:${stats.mtimeMs}:${stats.ctimeMs}`;
7535
+ } catch (error) {
7536
+ return `unavailable:${getErrorMessage2(error)}`;
7537
+ }
7538
+ }
7539
+ captureReaderArtifactFingerprint() {
7540
+ const storePath = path12.join(this.indexPath, "vectors");
7541
+ return {
7542
+ vectors: `${this.getReaderFileFingerprint(storePath)}|${this.getReaderFileFingerprint(`${storePath}.meta.json`)}`,
7543
+ keyword: this.getReaderFileFingerprint(path12.join(this.indexPath, "inverted-index.json")),
7544
+ database: this.getReaderFileFingerprint(path12.join(this.indexPath, "codebase.db")),
7545
+ databaseIdentity: this.getReaderFileFingerprint(path12.join(this.indexPath, "codebase.db"), true)
7546
+ };
7547
+ }
7548
+ refreshReaderArtifacts() {
7549
+ if (this.initializationMode !== "reader" || !this.configuredProviderInfo) {
7550
+ return;
7551
+ }
7552
+ const previousFingerprint = this.readerArtifactFingerprint;
7553
+ const currentFingerprint = this.captureReaderArtifactFingerprint();
7554
+ const issues = new Map(this.readIssues.map((issue) => [issue.component, issue]));
7555
+ const retryDue = (component) => issues.has(component) && Date.now() >= (this.readerArtifactRetryAfter.get(component) ?? 0);
7556
+ const vectorsChanged = !previousFingerprint || currentFingerprint.vectors !== previousFingerprint.vectors;
7557
+ const keywordChanged = !previousFingerprint || currentFingerprint.keyword !== previousFingerprint.keyword;
7558
+ const databaseChanged = !previousFingerprint || currentFingerprint.database !== previousFingerprint.database;
7559
+ const databaseReplaced = !previousFingerprint || currentFingerprint.databaseIdentity !== previousFingerprint.databaseIdentity;
7560
+ if (previousFingerprint && !vectorsChanged && !keywordChanged && !databaseChanged && !Array.from(issues.keys()).some(retryDue)) {
7561
+ return;
7562
+ }
7563
+ const setIssue = (component, message, error) => {
7564
+ if (!issues.has(component)) {
7565
+ this.logger.warn(message, error === void 0 ? void 0 : { error: getErrorMessage2(error) });
7566
+ }
7567
+ issues.set(component, this.createReadIssue(component, message));
7568
+ this.readerArtifactRetryAfter.set(component, Date.now() + READER_ARTIFACT_RETRY_INTERVAL_MS);
7569
+ };
7570
+ const storePath = path12.join(this.indexPath, "vectors");
7571
+ const vectorMetadataPath = `${storePath}.meta.json`;
7572
+ const invertedIndexPath = path12.join(this.indexPath, "inverted-index.json");
7573
+ const dbPath = path12.join(this.indexPath, "codebase.db");
7574
+ if (vectorsChanged || retryDue("vectors")) {
7575
+ const vectorStoreExists = existsSync7(storePath);
7576
+ const vectorMetadataExists = existsSync7(vectorMetadataPath);
7577
+ if (vectorStoreExists && vectorMetadataExists) {
7578
+ try {
7579
+ const store = new VectorStore(storePath, this.configuredProviderInfo.modelInfo.dimensions);
7580
+ store.loadStrict();
7581
+ this.store = store;
7582
+ issues.delete("vectors");
7583
+ this.readerArtifactRetryAfter.delete("vectors");
7584
+ } catch (error) {
7585
+ setIssue("vectors", this.getVectorReadIssueMessage(), error);
7586
+ }
7587
+ } else if (vectorStoreExists !== vectorMetadataExists || issues.has("vectors")) {
7588
+ setIssue("vectors", this.getVectorReadIssueMessage());
7589
+ }
7590
+ }
7591
+ if (keywordChanged || retryDue("keyword") || !existsSync7(invertedIndexPath) && (this.store?.count() ?? 0) > 0) {
7592
+ if (existsSync7(invertedIndexPath)) {
7593
+ try {
7594
+ const invertedIndex = new InvertedIndex(invertedIndexPath);
7595
+ invertedIndex.load();
7596
+ this.invertedIndex = invertedIndex;
7597
+ issues.delete("keyword");
7598
+ this.readerArtifactRetryAfter.delete("keyword");
7599
+ } catch (error) {
7600
+ setIssue("keyword", this.getKeywordReadIssueMessage(), error);
7601
+ }
7602
+ } else if ((this.store?.count() ?? 0) > 0 || issues.has("keyword")) {
7603
+ setIssue("keyword", this.getKeywordReadIssueMessage());
7604
+ }
7605
+ }
7606
+ if (databaseReplaced || databaseChanged && issues.has("database") || retryDue("database")) {
7607
+ if (existsSync7(dbPath)) {
7608
+ try {
7609
+ const database = Database.openReadOnly(dbPath);
7610
+ if (this.database) {
7611
+ this.retiredDatabases.push(this.database);
7612
+ }
7613
+ this.database = database;
7614
+ issues.delete("database");
7615
+ this.readerArtifactRetryAfter.delete("database");
7616
+ } catch (error) {
7617
+ setIssue("database", this.getDatabaseReadIssueMessage(), error);
7618
+ }
7619
+ } else if ((this.store?.count() ?? 0) > 0 || issues.has("database")) {
7620
+ setIssue("database", this.getDatabaseReadIssueMessage());
7621
+ }
7622
+ }
7623
+ if (!issues.has("database")) {
7624
+ try {
7625
+ this.indexCompatibility = this.validateIndexCompatibility(this.configuredProviderInfo);
7626
+ } catch (error) {
7627
+ setIssue("database", this.getDatabaseReadIssueMessage(), error);
7628
+ }
7629
+ }
7630
+ this.readIssues = Array.from(issues.values());
7631
+ this.readerArtifactFingerprint = currentFingerprint;
7632
+ }
7633
+ refreshInactiveWriterArtifacts() {
7634
+ if (this.initializationMode !== "writer" || this.activeIndexLease) {
7635
+ return true;
7636
+ }
7637
+ const previousFingerprint = this.writerArtifactFingerprint;
7638
+ const currentFingerprint = this.captureReaderArtifactFingerprint();
7639
+ const retryDue = this.readIssues.some(
7640
+ (issue) => Date.now() >= (this.readerArtifactRetryAfter.get(issue.component) ?? 0)
7641
+ );
7642
+ const artifactsChanged = !previousFingerprint || currentFingerprint.vectors !== previousFingerprint.vectors || currentFingerprint.keyword !== previousFingerprint.keyword || currentFingerprint.database !== previousFingerprint.database || currentFingerprint.databaseIdentity !== previousFingerprint.databaseIdentity;
7643
+ if (!artifactsChanged && !retryDue) {
7644
+ return true;
7645
+ }
7646
+ if (!previousFingerprint || currentFingerprint.databaseIdentity !== previousFingerprint.databaseIdentity) {
7647
+ return false;
7648
+ }
7649
+ this.initializationMode = "reader";
7650
+ this.readerArtifactFingerprint = previousFingerprint;
7651
+ try {
7652
+ this.refreshReaderArtifacts();
7653
+ this.writerArtifactFingerprint = this.readerArtifactFingerprint ?? currentFingerprint;
7654
+ } finally {
7655
+ this.readerArtifactFingerprint = null;
7656
+ this.initializationMode = "writer";
7657
+ }
7658
+ return true;
7659
+ }
7660
+ async initializeUnlocked(mode, recoveredOwners = [], options = {}) {
7661
+ if (mode === "writer") {
7662
+ this.requireActiveLease();
7663
+ }
7664
+ this.readIssues = [];
7665
+ this.readerArtifactRetryAfter.clear();
6882
7666
  if (this.config.embeddingProvider === "custom") {
6883
7667
  if (!this.config.customProvider) {
6884
7668
  throw new Error("embeddingProvider is 'custom' but customProvider config is missing.");
@@ -6910,36 +7694,103 @@ var Indexer = class {
6910
7694
  });
6911
7695
  }
6912
7696
  }
6913
- await fsPromises2.mkdir(this.indexPath, { recursive: true });
6914
7697
  const dimensions = this.configuredProviderInfo.modelInfo.dimensions;
6915
- const storePath = path11.join(this.indexPath, "vectors");
6916
- this.store = new VectorStore(storePath, dimensions);
6917
- const indexFilePath = path11.join(this.indexPath, "vectors.usearch");
6918
- if (existsSync6(indexFilePath)) {
6919
- this.store.load();
6920
- }
6921
- const invertedIndexPath = path11.join(this.indexPath, "inverted-index.json");
6922
- this.invertedIndex = new InvertedIndex(invertedIndexPath);
6923
- try {
6924
- this.invertedIndex.load();
6925
- } catch {
6926
- if (existsSync6(invertedIndexPath)) {
6927
- await fsPromises2.unlink(invertedIndexPath);
7698
+ const storePath = path12.join(this.indexPath, "vectors");
7699
+ const vectorMetadataPath = `${storePath}.meta.json`;
7700
+ const invertedIndexPath = path12.join(this.indexPath, "inverted-index.json");
7701
+ const dbPath = path12.join(this.indexPath, "codebase.db");
7702
+ let dbIsNew = !existsSync7(dbPath);
7703
+ const readerArtifactFingerprint = mode === "reader" ? this.captureReaderArtifactFingerprint() : null;
7704
+ if (mode === "writer") {
7705
+ await fsPromises2.mkdir(this.indexPath, { recursive: true });
7706
+ if (recoveredOwners.length > 0 && this.config.scope === "project" && !this.isLocalProjectIndexPath()) {
7707
+ throw new Error(
7708
+ "Interrupted indexing recovery is unsafe while using an inherited worktree index. Run index_codebase with force=true to create a local project index boundary."
7709
+ );
7710
+ }
7711
+ for (const recoveredOwner of recoveredOwners) {
7712
+ recoverLeaseArtifacts(this.indexPath, recoveredOwner, [
7713
+ storePath,
7714
+ `${storePath}.meta.json`
7715
+ ]);
7716
+ }
7717
+ if (recoveredOwners.length > 0 && this.config.scope === "project") {
7718
+ await this.resetLocalIndexArtifacts();
7719
+ }
7720
+ this.store = new VectorStore(storePath, dimensions);
7721
+ if (existsSync7(storePath) || existsSync7(vectorMetadataPath)) {
7722
+ this.store.load();
6928
7723
  }
6929
7724
  this.invertedIndex = new InvertedIndex(invertedIndexPath);
6930
- }
6931
- const dbPath = path11.join(this.indexPath, "codebase.db");
6932
- let dbIsNew = !existsSync6(dbPath);
6933
- try {
6934
- this.database = new Database(dbPath);
6935
- } catch (error) {
6936
- if (!await this.tryResetCorruptedIndex("initializing index database", error)) {
6937
- throw error;
7725
+ try {
7726
+ this.invertedIndex.load();
7727
+ } catch {
7728
+ if (existsSync7(invertedIndexPath)) {
7729
+ await fsPromises2.unlink(invertedIndexPath);
7730
+ }
7731
+ this.invertedIndex = new InvertedIndex(invertedIndexPath);
7732
+ }
7733
+ try {
7734
+ this.database = new Database(dbPath);
7735
+ } catch (error) {
7736
+ if (!await this.tryResetCorruptedIndex("initializing index database", error)) {
7737
+ throw error;
7738
+ }
7739
+ this.store = new VectorStore(storePath, dimensions);
7740
+ this.invertedIndex = new InvertedIndex(invertedIndexPath);
7741
+ this.database = new Database(dbPath);
7742
+ dbIsNew = true;
6938
7743
  }
7744
+ } else {
6939
7745
  this.store = new VectorStore(storePath, dimensions);
7746
+ const vectorStoreExists = existsSync7(storePath);
7747
+ const vectorMetadataExists = existsSync7(vectorMetadataPath);
7748
+ const vectorReadFailureMessage = this.getVectorReadIssueMessage();
7749
+ if (vectorStoreExists !== vectorMetadataExists) {
7750
+ this.recordReadIssue("vectors", vectorReadFailureMessage);
7751
+ } else if (vectorStoreExists) {
7752
+ try {
7753
+ this.store.loadStrict();
7754
+ } catch (error) {
7755
+ this.recordReadIssue("vectors", vectorReadFailureMessage, error);
7756
+ this.store = new VectorStore(storePath, dimensions);
7757
+ }
7758
+ }
6940
7759
  this.invertedIndex = new InvertedIndex(invertedIndexPath);
6941
- this.database = new Database(dbPath);
6942
- dbIsNew = true;
7760
+ if (existsSync7(invertedIndexPath)) {
7761
+ try {
7762
+ this.invertedIndex.load();
7763
+ } catch (error) {
7764
+ this.recordReadIssue(
7765
+ "keyword",
7766
+ this.getKeywordReadIssueMessage(),
7767
+ error
7768
+ );
7769
+ this.invertedIndex = new InvertedIndex(invertedIndexPath);
7770
+ }
7771
+ } else if (this.store.count() > 0) {
7772
+ this.recordReadIssue("keyword", this.getKeywordReadIssueMessage());
7773
+ }
7774
+ if (existsSync7(dbPath)) {
7775
+ try {
7776
+ this.database = Database.openReadOnly(dbPath);
7777
+ } catch (error) {
7778
+ this.recordReadIssue(
7779
+ "database",
7780
+ this.getDatabaseReadIssueMessage(),
7781
+ error
7782
+ );
7783
+ this.database = Database.createEmptyReadOnly();
7784
+ }
7785
+ } else {
7786
+ this.database = Database.createEmptyReadOnly();
7787
+ if (this.store.count() > 0) {
7788
+ this.recordReadIssue(
7789
+ "database",
7790
+ `Index database is missing for the published vectors. ${this.getDatabaseReadIssueMessage()}`
7791
+ );
7792
+ }
7793
+ }
6943
7794
  }
6944
7795
  if (isGitRepo(this.projectRoot)) {
6945
7796
  this.currentBranch = getBranchOrDefault(this.projectRoot);
@@ -6953,10 +7804,10 @@ var Indexer = class {
6953
7804
  this.baseBranch = "default";
6954
7805
  this.logger.branch("debug", "Not a git repository, using default branch");
6955
7806
  }
6956
- if (this.checkForInterruptedIndexing()) {
6957
- await this.recoverFromInterruptedIndexing();
7807
+ if (mode === "writer" && recoveredOwners.length > 0) {
7808
+ await this.recoverFromInterruptedIndexingUnlocked(recoveredOwners);
6958
7809
  }
6959
- if (dbIsNew && this.store.count() > 0) {
7810
+ if (mode === "writer" && dbIsNew && this.store.count() > 0) {
6960
7811
  this.migrateFromLegacyIndex();
6961
7812
  }
6962
7813
  this.loadFileHashCache();
@@ -6968,9 +7819,11 @@ var Indexer = class {
6968
7819
  configuredProviderInfo: this.configuredProviderInfo
6969
7820
  });
6970
7821
  }
6971
- if (this.config.indexing.autoGc) {
7822
+ if (mode === "writer" && this.config.indexing.autoGc && !options.skipAutoGc) {
6972
7823
  await this.maybeRunAutoGc();
6973
7824
  }
7825
+ this.initializationMode = mode;
7826
+ this.readerArtifactFingerprint = readerArtifactFingerprint;
6974
7827
  }
6975
7828
  async maybeRunAutoGc() {
6976
7829
  if (!this.database) return;
@@ -6987,7 +7840,7 @@ var Indexer = class {
6987
7840
  }
6988
7841
  }
6989
7842
  if (shouldRunGc) {
6990
- const result = await this.healthCheck();
7843
+ const result = await this.healthCheckUnlocked();
6991
7844
  if (result.warning) {
6992
7845
  this.database.setMetadata(STARTUP_WARNING_METADATA_KEY, result.warning);
6993
7846
  } else {
@@ -7009,7 +7862,7 @@ var Indexer = class {
7009
7862
  if (await this.tryResetCorruptedIndex("running automatic orphan garbage collection", error)) {
7010
7863
  return {
7011
7864
  resetCorruptedIndex: true,
7012
- warning: this.getCorruptedIndexWarning(path11.join(this.indexPath, "codebase.db"))
7865
+ warning: this.getCorruptedIndexWarning(path12.join(this.indexPath, "codebase.db"))
7013
7866
  };
7014
7867
  }
7015
7868
  throw error;
@@ -7024,28 +7877,29 @@ var Indexer = class {
7024
7877
  return;
7025
7878
  }
7026
7879
  const retainedEntries = store.getAllMetadata().filter(({ key }) => !excludedSet.has(key));
7027
- const storeBasePath = path11.join(this.indexPath, "vectors");
7028
- const storeIndexPath = `${storeBasePath}.usearch`;
7880
+ const storeBasePath = path12.join(this.indexPath, "vectors");
7881
+ const storeIndexPath = storeBasePath;
7029
7882
  const storeMetadataPath = `${storeBasePath}.meta.json`;
7030
- const backupIndexPath = `${storeIndexPath}.bak`;
7031
- const backupMetadataPath = `${storeMetadataPath}.bak`;
7883
+ const lease = this.requireActiveLease();
7884
+ const backupIndexPath = createLeaseTemporaryPath(storeIndexPath, lease.owner, "bak");
7885
+ const backupMetadataPath = createLeaseTemporaryPath(storeMetadataPath, lease.owner, "bak");
7032
7886
  let backedUpIndex = false;
7033
7887
  let backedUpMetadata = false;
7034
7888
  let rebuiltCount = 0;
7035
7889
  let skippedCount = 0;
7036
- if (existsSync6(backupIndexPath)) {
7890
+ if (existsSync7(backupIndexPath)) {
7037
7891
  unlinkSync(backupIndexPath);
7038
7892
  }
7039
- if (existsSync6(backupMetadataPath)) {
7893
+ if (existsSync7(backupMetadataPath)) {
7040
7894
  unlinkSync(backupMetadataPath);
7041
7895
  }
7042
7896
  try {
7043
- if (existsSync6(storeIndexPath)) {
7044
- renameSync(storeIndexPath, backupIndexPath);
7897
+ if (existsSync7(storeIndexPath)) {
7898
+ renameSync2(storeIndexPath, backupIndexPath);
7045
7899
  backedUpIndex = true;
7046
7900
  }
7047
- if (existsSync6(storeMetadataPath)) {
7048
- renameSync(storeMetadataPath, backupMetadataPath);
7901
+ if (existsSync7(storeMetadataPath)) {
7902
+ renameSync2(storeMetadataPath, backupMetadataPath);
7049
7903
  backedUpMetadata = true;
7050
7904
  }
7051
7905
  store.clear();
@@ -7065,10 +7919,10 @@ var Indexer = class {
7065
7919
  rebuiltCount += 1;
7066
7920
  }
7067
7921
  store.save();
7068
- if (backedUpIndex && existsSync6(backupIndexPath)) {
7922
+ if (backedUpIndex && existsSync7(backupIndexPath)) {
7069
7923
  unlinkSync(backupIndexPath);
7070
7924
  }
7071
- if (backedUpMetadata && existsSync6(backupMetadataPath)) {
7925
+ if (backedUpMetadata && existsSync7(backupMetadataPath)) {
7072
7926
  unlinkSync(backupMetadataPath);
7073
7927
  }
7074
7928
  this.logger.gc("info", "Rebuilt vector store to avoid native remove", {
@@ -7081,17 +7935,17 @@ var Indexer = class {
7081
7935
  store.clear();
7082
7936
  } catch {
7083
7937
  }
7084
- if (existsSync6(storeIndexPath)) {
7938
+ if (existsSync7(storeIndexPath)) {
7085
7939
  unlinkSync(storeIndexPath);
7086
7940
  }
7087
- if (existsSync6(storeMetadataPath)) {
7941
+ if (existsSync7(storeMetadataPath)) {
7088
7942
  unlinkSync(storeMetadataPath);
7089
7943
  }
7090
- if (backedUpIndex && existsSync6(backupIndexPath)) {
7091
- renameSync(backupIndexPath, storeIndexPath);
7944
+ if (backedUpIndex && existsSync7(backupIndexPath)) {
7945
+ renameSync2(backupIndexPath, storeIndexPath);
7092
7946
  }
7093
- if (backedUpMetadata && existsSync6(backupMetadataPath)) {
7094
- renameSync(backupMetadataPath, storeMetadataPath);
7947
+ if (backedUpMetadata && existsSync7(backupMetadataPath)) {
7948
+ renameSync2(backupMetadataPath, storeMetadataPath);
7095
7949
  }
7096
7950
  if (backedUpIndex || backedUpMetadata) {
7097
7951
  store.load();
@@ -7105,11 +7959,37 @@ var Indexer = class {
7105
7959
  }
7106
7960
  return `Detected a corrupted local SQLite index at ${dbPath} and reset the local index. Run index_codebase to rebuild search data.`;
7107
7961
  }
7962
+ async resetLocalIndexArtifacts() {
7963
+ this.store = null;
7964
+ this.invertedIndex = null;
7965
+ this.database?.close();
7966
+ this.database = null;
7967
+ this.indexCompatibility = null;
7968
+ this.initializationMode = "none";
7969
+ this.readIssues = [];
7970
+ this.readerArtifactFingerprint = null;
7971
+ this.writerArtifactFingerprint = null;
7972
+ this.readerArtifactRetryAfter.clear();
7973
+ this.fileHashCache.clear();
7974
+ const resetPaths = [
7975
+ path12.join(this.indexPath, "codebase.db"),
7976
+ path12.join(this.indexPath, "codebase.db-shm"),
7977
+ path12.join(this.indexPath, "codebase.db-wal"),
7978
+ path12.join(this.indexPath, "vectors"),
7979
+ path12.join(this.indexPath, "vectors.usearch"),
7980
+ path12.join(this.indexPath, "vectors.meta.json"),
7981
+ path12.join(this.indexPath, "inverted-index.json"),
7982
+ path12.join(this.indexPath, "file-hashes.json"),
7983
+ path12.join(this.indexPath, "failed-batches.json")
7984
+ ];
7985
+ await Promise.all(resetPaths.map((targetPath) => fsPromises2.rm(targetPath, { recursive: true, force: true })));
7986
+ await fsPromises2.mkdir(this.indexPath, { recursive: true });
7987
+ }
7108
7988
  async tryResetCorruptedIndex(stage, error) {
7109
7989
  if (!isSqliteCorruptionError(error)) {
7110
7990
  return false;
7111
7991
  }
7112
- const dbPath = path11.join(this.indexPath, "codebase.db");
7992
+ const dbPath = path12.join(this.indexPath, "codebase.db");
7113
7993
  const warning = this.getCorruptedIndexWarning(dbPath);
7114
7994
  const errorMessage = getErrorMessage2(error);
7115
7995
  if (this.config.scope === "global") {
@@ -7125,30 +8005,7 @@ var Indexer = class {
7125
8005
  dbPath,
7126
8006
  error: errorMessage
7127
8007
  });
7128
- this.store = null;
7129
- this.invertedIndex = null;
7130
- this.database?.close();
7131
- this.database = null;
7132
- this.indexCompatibility = null;
7133
- this.fileHashCache.clear();
7134
- const resetPaths = [
7135
- path11.join(this.indexPath, "codebase.db"),
7136
- path11.join(this.indexPath, "codebase.db-shm"),
7137
- path11.join(this.indexPath, "codebase.db-wal"),
7138
- path11.join(this.indexPath, "vectors.usearch"),
7139
- path11.join(this.indexPath, "inverted-index.json"),
7140
- path11.join(this.indexPath, "file-hashes.json"),
7141
- path11.join(this.indexPath, "failed-batches.json"),
7142
- path11.join(this.indexPath, "indexing.lock"),
7143
- path11.join(this.indexPath, "vectors")
7144
- ];
7145
- await Promise.all(resetPaths.map(async (targetPath) => {
7146
- try {
7147
- await fsPromises2.rm(targetPath, { recursive: true, force: true });
7148
- } catch {
7149
- }
7150
- }));
7151
- await fsPromises2.mkdir(this.indexPath, { recursive: true });
8008
+ await this.resetLocalIndexArtifacts();
7152
8009
  return true;
7153
8010
  }
7154
8011
  migrateFromLegacyIndex() {
@@ -7267,8 +8124,62 @@ var Indexer = class {
7267
8124
  return this.indexCompatibility;
7268
8125
  }
7269
8126
  async ensureInitialized() {
8127
+ let initializedReader = false;
8128
+ while (true) {
8129
+ if (this.initializationPromise) {
8130
+ await this.initializationPromise;
8131
+ }
8132
+ if (!this.isInitializedFor("reader")) {
8133
+ await this.initialize();
8134
+ initializedReader = true;
8135
+ continue;
8136
+ }
8137
+ if (this.initializationMode === "writer" && !this.activeIndexLease && !initializedReader) {
8138
+ if (!this.refreshInactiveWriterArtifacts()) {
8139
+ this.resetLoadedIndexState(true);
8140
+ await this.initialize();
8141
+ initializedReader = true;
8142
+ continue;
8143
+ }
8144
+ }
8145
+ if (this.initializationMode === "reader" && !initializedReader) {
8146
+ this.refreshReaderArtifacts();
8147
+ }
8148
+ const state = this.requireLoadedIndexState();
8149
+ return {
8150
+ ...state,
8151
+ readIssues: [...this.readIssues],
8152
+ compatibility: this.indexCompatibility ?? this.validateIndexCompatibility(state.configuredProviderInfo)
8153
+ };
8154
+ }
8155
+ }
8156
+ async ensureInitializedUnlocked(recoveredOwners = []) {
8157
+ this.requireActiveLease();
8158
+ if (this.initializationPromise) {
8159
+ await this.initializationPromise;
8160
+ }
8161
+ if (recoveredOwners.length > 0 || !this.isInitializedFor("writer")) {
8162
+ const retireReaderDatabase = this.initializationMode === "reader";
8163
+ this.resetLoadedIndexState(retireReaderDatabase);
8164
+ await this.initializeOnce("writer", recoveredOwners, { skipAutoGc: true });
8165
+ } else {
8166
+ this.refreshLoadedIndexState();
8167
+ }
8168
+ if (this.config.indexing.autoGc) {
8169
+ await this.maybeRunAutoGc();
8170
+ }
8171
+ return this.requireLoadedIndexState();
8172
+ }
8173
+ requireReadableComponents(readIssues, ...components) {
8174
+ const componentSet = new Set(components);
8175
+ const issues = readIssues.filter((issue) => issue.blocking && componentSet.has(issue.component));
8176
+ if (issues.length > 0) {
8177
+ throw new Error(issues.map((issue) => issue.message).join(" "));
8178
+ }
8179
+ }
8180
+ requireLoadedIndexState() {
7270
8181
  if (!this.store || !this.provider || !this.invertedIndex || !this.configuredProviderInfo || !this.database) {
7271
- await this.initialize();
8182
+ throw new Error("Index state is not initialized");
7272
8183
  }
7273
8184
  return {
7274
8185
  store: this.store,
@@ -7292,7 +8203,12 @@ var Indexer = class {
7292
8203
  return createCostEstimate(files, configuredProviderInfo);
7293
8204
  }
7294
8205
  async index(onProgress) {
7295
- const { store, provider, invertedIndex, database, configuredProviderInfo } = await this.ensureInitialized();
8206
+ return this.withIndexMutationLease("index", async (recoveredOwners) => {
8207
+ return this.indexUnlocked(onProgress, recoveredOwners);
8208
+ });
8209
+ }
8210
+ async indexUnlocked(onProgress, recoveredOwners = [], stateReady = false) {
8211
+ const { store, provider, invertedIndex, database, configuredProviderInfo } = stateReady ? this.requireLoadedIndexState() : await this.ensureInitializedUnlocked(recoveredOwners);
7296
8212
  const scopedRoots = this.config.scope === "global" ? this.getScopedRoots() : null;
7297
8213
  const branchCatalogKey = this.getBranchCatalogKey();
7298
8214
  const forceScopedReembed = scopedRoots !== null && database.getMetadata(this.getProjectForceReembedMetadataKey()) === "true";
@@ -7302,7 +8218,6 @@ var Indexer = class {
7302
8218
  `${this.indexCompatibility?.reason} Run index_codebase with force=true to rebuild the index.`
7303
8219
  );
7304
8220
  }
7305
- this.acquireIndexingLock();
7306
8221
  this.logger.recordIndexingStart();
7307
8222
  this.logger.info("Starting indexing", { projectRoot: this.projectRoot });
7308
8223
  const startTime = Date.now();
@@ -7451,7 +8366,7 @@ var Indexer = class {
7451
8366
  for (const parsed of parsedFiles) {
7452
8367
  currentFilePaths.add(parsed.path);
7453
8368
  if (parsed.chunks.length === 0) {
7454
- const relativePath = path11.relative(this.projectRoot, parsed.path);
8369
+ const relativePath = path12.relative(this.projectRoot, parsed.path);
7455
8370
  stats.parseFailures.push(relativePath);
7456
8371
  }
7457
8372
  let fileChunkCount = 0;
@@ -7651,7 +8566,9 @@ var Indexer = class {
7651
8566
  database.addChunksToBranchBatch(branchCatalogKey, Array.from(currentChunkIds));
7652
8567
  database.clearBranchSymbols(branchCatalogKey);
7653
8568
  database.addSymbolsToBranchBatch(branchCatalogKey, Array.from(allSymbolIds));
7654
- if (backfilledBlameMetadata) {
8569
+ const vectorPath = path12.join(this.indexPath, "vectors");
8570
+ const shouldFingerprintLegacyPair = !store.hasFingerprint() && existsSync7(vectorPath) && existsSync7(`${vectorPath}.meta.json`);
8571
+ if (backfilledBlameMetadata || shouldFingerprintLegacyPair) {
7655
8572
  store.save();
7656
8573
  }
7657
8574
  if (scopedRoots) {
@@ -7672,7 +8589,6 @@ var Indexer = class {
7672
8589
  chunksProcessed: 0,
7673
8590
  totalChunks: 0
7674
8591
  });
7675
- this.releaseIndexingLock();
7676
8592
  return stats;
7677
8593
  }
7678
8594
  if (pendingChunks.length === 0) {
@@ -7681,7 +8597,7 @@ var Indexer = class {
7681
8597
  database.clearBranchSymbols(branchCatalogKey);
7682
8598
  database.addSymbolsToBranchBatch(branchCatalogKey, Array.from(allSymbolIds));
7683
8599
  store.save();
7684
- invertedIndex.save();
8600
+ this.saveInvertedIndex(invertedIndex);
7685
8601
  if (scopedRoots) {
7686
8602
  this.replaceScopedFileHashCache(currentFileHashes, scopedRoots);
7687
8603
  this.clearScopedFailedBatches(scopedRoots);
@@ -7700,7 +8616,6 @@ var Indexer = class {
7700
8616
  chunksProcessed: 0,
7701
8617
  totalChunks: 0
7702
8618
  });
7703
- this.releaseIndexingLock();
7704
8619
  return stats;
7705
8620
  }
7706
8621
  onProgress?.({
@@ -7931,7 +8846,7 @@ var Indexer = class {
7931
8846
  database.clearBranchSymbols(branchCatalogKey);
7932
8847
  database.addSymbolsToBranchBatch(branchCatalogKey, Array.from(allSymbolIds));
7933
8848
  store.save();
7934
- invertedIndex.save();
8849
+ this.saveInvertedIndex(invertedIndex);
7935
8850
  if (scopedRoots) {
7936
8851
  this.replaceScopedFileHashCache(currentFileHashes, scopedRoots);
7937
8852
  } else {
@@ -7983,7 +8898,6 @@ var Indexer = class {
7983
8898
  chunksProcessed: stats.indexedChunks,
7984
8899
  totalChunks: pendingChunks.length
7985
8900
  });
7986
- this.releaseIndexingLock();
7987
8901
  return stats;
7988
8902
  }
7989
8903
  async getQueryEmbedding(query, provider) {
@@ -8049,8 +8963,8 @@ var Indexer = class {
8049
8963
  return intersection / union;
8050
8964
  }
8051
8965
  async search(query, limit, options) {
8052
- const { store, provider, database } = await this.ensureInitialized();
8053
- const compatibility = this.checkCompatibility();
8966
+ const { store, provider, invertedIndex, database, readIssues, compatibility } = await this.ensureInitialized();
8967
+ this.requireReadableComponents(readIssues, "vectors", "database");
8054
8968
  if (!compatibility.compatible) {
8055
8969
  throw new Error(
8056
8970
  `${compatibility.reason ?? "Index is incompatible with current embedding provider."} A possible solution is to run index_codebase with force=true to rebuild the index.`
@@ -8064,6 +8978,7 @@ var Indexer = class {
8064
8978
  const maxResults = limit ?? this.config.search.maxResults;
8065
8979
  const hybridWeight = options?.hybridWeight ?? this.config.search.hybridWeight;
8066
8980
  const fusionStrategy = this.config.search.fusionStrategy;
8981
+ const effectiveHybridWeight = fusionStrategy === "weighted" && readIssues.some((issue) => issue.component === "keyword") ? 0 : hybridWeight;
8067
8982
  const rrfK = this.config.search.rrfK;
8068
8983
  const rerankTopN = this.config.search.rerankTopN;
8069
8984
  const filterByBranch = options?.filterByBranch ?? true;
@@ -8072,7 +8987,7 @@ var Indexer = class {
8072
8987
  this.logger.search("debug", "Starting search", {
8073
8988
  query,
8074
8989
  maxResults,
8075
- hybridWeight,
8990
+ hybridWeight: effectiveHybridWeight,
8076
8991
  fusionStrategy,
8077
8992
  rrfK,
8078
8993
  rerankTopN,
@@ -8086,7 +9001,7 @@ var Indexer = class {
8086
9001
  const semanticResults = store.search(embedding, maxResults * 4);
8087
9002
  const vectorMs = performance2.now() - vectorStartTime;
8088
9003
  const keywordStartTime = performance2.now();
8089
- const keywordResults = await this.keywordSearch(query, maxResults * 4);
9004
+ const keywordResults = await this.keywordSearch(query, maxResults * 4, store, invertedIndex);
8090
9005
  const keywordMs = performance2.now() - keywordStartTime;
8091
9006
  let branchChunkIds = null;
8092
9007
  if (filterByBranch && (this.config.scope === "global" || this.currentBranch !== "default")) {
@@ -8123,7 +9038,7 @@ var Indexer = class {
8123
9038
  rrfK,
8124
9039
  rerankTopN,
8125
9040
  limit: maxResults,
8126
- hybridWeight,
9041
+ hybridWeight: effectiveHybridWeight,
8127
9042
  prioritizeSourcePaths: sourceIntent
8128
9043
  });
8129
9044
  const rerankedCombined = await this.rerankCandidatesWithApi(query, combined, {
@@ -8223,8 +9138,7 @@ var Indexer = class {
8223
9138
  })
8224
9139
  );
8225
9140
  }
8226
- async keywordSearch(query, limit) {
8227
- const { store, invertedIndex } = await this.ensureInitialized();
9141
+ async keywordSearch(query, limit, store, invertedIndex) {
8228
9142
  const scores = invertedIndex.search(query);
8229
9143
  if (scores.size === 0) {
8230
9144
  return [];
@@ -8242,24 +9156,54 @@ var Indexer = class {
8242
9156
  return results.slice(0, limit);
8243
9157
  }
8244
9158
  async getStatus() {
8245
- const { store, configuredProviderInfo, database } = await this.ensureInitialized();
9159
+ const { store, configuredProviderInfo, database, readIssues, compatibility } = await this.ensureInitialized();
8246
9160
  const failedBatchesCount = this.getFailedBatchesCount();
9161
+ const vectorCount = store.count();
9162
+ const statusReadIssues = [...readIssues];
9163
+ let startupWarning = "";
9164
+ if (!statusReadIssues.some((issue) => issue.component === "database")) {
9165
+ try {
9166
+ startupWarning = database.getMetadata(STARTUP_WARNING_METADATA_KEY) ?? "";
9167
+ } catch (error) {
9168
+ const message = this.getDatabaseReadIssueMessage();
9169
+ statusReadIssues.push(this.createReadIssue("database", message));
9170
+ if (!this.readIssues.some((issue) => issue.component === "database")) {
9171
+ this.recordReadIssue("database", message, error);
9172
+ }
9173
+ }
9174
+ }
9175
+ const readWarning = statusReadIssues.map((issue) => issue.message).join(" ");
9176
+ const warning = [readWarning, startupWarning].filter((message) => message.length > 0).join(" ");
9177
+ const hasBlockingReadIssue = statusReadIssues.some((issue) => issue.blocking);
8247
9178
  return {
8248
- indexed: store.count() > 0,
8249
- vectorCount: store.count(),
9179
+ indexed: vectorCount > 0 && !hasBlockingReadIssue,
9180
+ vectorCount,
8250
9181
  provider: configuredProviderInfo.provider,
8251
9182
  model: configuredProviderInfo.modelInfo.model,
8252
9183
  indexPath: this.indexPath,
8253
9184
  currentBranch: this.currentBranch,
8254
9185
  baseBranch: this.baseBranch,
8255
- compatibility: this.indexCompatibility,
9186
+ compatibility,
8256
9187
  failedBatchesCount,
8257
9188
  failedBatchesPath: failedBatchesCount > 0 ? this.failedBatchesPath : void 0,
8258
- warning: database.getMetadata(STARTUP_WARNING_METADATA_KEY) ?? void 0
9189
+ warning: warning || void 0
8259
9190
  };
8260
9191
  }
9192
+ async forceIndex(onProgress) {
9193
+ return this.withIndexMutationLease("force-index", async (recoveredOwners) => {
9194
+ await this.ensureInitializedUnlocked(recoveredOwners);
9195
+ await this.clearIndexUnlocked();
9196
+ return this.indexUnlocked(onProgress, [], true);
9197
+ });
9198
+ }
8261
9199
  async clearIndex() {
8262
- const { store, invertedIndex, database } = await this.ensureInitialized();
9200
+ await this.withIndexMutationLease("clear", async (recoveredOwners) => {
9201
+ await this.ensureInitializedUnlocked(recoveredOwners);
9202
+ await this.clearIndexUnlocked();
9203
+ });
9204
+ }
9205
+ async clearIndexUnlocked() {
9206
+ const { store, invertedIndex, database } = this.requireLoadedIndexState();
8263
9207
  if (this.config.scope === "global") {
8264
9208
  store.load();
8265
9209
  invertedIndex.load();
@@ -8286,7 +9230,7 @@ var Indexer = class {
8286
9230
  store.clear();
8287
9231
  store.save();
8288
9232
  invertedIndex.clear();
8289
- invertedIndex.save();
9233
+ this.saveInvertedIndex(invertedIndex);
8290
9234
  this.fileHashCache.clear();
8291
9235
  this.saveFileHashCache();
8292
9236
  database.clearAllIndexedData();
@@ -8310,14 +9254,7 @@ var Indexer = class {
8310
9254
  this.indexCompatibility = compatibility;
8311
9255
  return;
8312
9256
  }
8313
- const localProjectIndexPaths = [path11.join(this.projectRoot, getHostProjectIndexRelativePath(this.host))];
8314
- if (this.host !== "opencode") {
8315
- localProjectIndexPaths.push(path11.join(this.projectRoot, getHostProjectIndexRelativePath("opencode")));
8316
- }
8317
- const isLocalProjectIndex = localProjectIndexPaths.some(
8318
- (localPath) => path11.resolve(this.indexPath) === path11.resolve(localPath)
8319
- );
8320
- if (!isLocalProjectIndex) {
9257
+ if (!this.isLocalProjectIndexPath()) {
8321
9258
  throw new Error(
8322
9259
  "Project-scoped force rebuild is unsafe while using an inherited worktree index. Create a local project config boundary before clearing the index."
8323
9260
  );
@@ -8325,7 +9262,7 @@ var Indexer = class {
8325
9262
  store.clear();
8326
9263
  store.save();
8327
9264
  invertedIndex.clear();
8328
- invertedIndex.save();
9265
+ this.saveInvertedIndex(invertedIndex);
8329
9266
  this.fileHashCache.clear();
8330
9267
  this.saveFileHashCache();
8331
9268
  database.clearAllIndexedData();
@@ -8343,7 +9280,13 @@ var Indexer = class {
8343
9280
  this.indexCompatibility = this.validateIndexCompatibility(this.configuredProviderInfo);
8344
9281
  }
8345
9282
  async healthCheck() {
8346
- const { store, invertedIndex, database } = await this.ensureInitialized();
9283
+ return this.withIndexMutationLease("health-check", async (recoveredOwners) => {
9284
+ await this.ensureInitializedUnlocked(recoveredOwners);
9285
+ return this.healthCheckUnlocked();
9286
+ });
9287
+ }
9288
+ async healthCheckUnlocked() {
9289
+ const { store, invertedIndex, database } = this.requireLoadedIndexState();
8347
9290
  this.logger.gc("info", "Starting health check");
8348
9291
  const allMetadata = store.getAllMetadata();
8349
9292
  const filePathsToChunkKeys = /* @__PURE__ */ new Map();
@@ -8356,7 +9299,7 @@ var Indexer = class {
8356
9299
  const removedChunkKeys = [];
8357
9300
  const chunkKeysByRemovedFile = /* @__PURE__ */ new Map();
8358
9301
  for (const [filePath, chunkKeys] of filePathsToChunkKeys) {
8359
- if (!existsSync6(filePath)) {
9302
+ if (!existsSync7(filePath)) {
8360
9303
  chunkKeysByRemovedFile.set(filePath, chunkKeys);
8361
9304
  for (const key of chunkKeys) {
8362
9305
  removedChunkKeys.push(key);
@@ -8381,7 +9324,7 @@ var Indexer = class {
8381
9324
  const removedCount = removedChunkKeys.length;
8382
9325
  if (removedCount > 0) {
8383
9326
  store.save();
8384
- invertedIndex.save();
9327
+ this.saveInvertedIndex(invertedIndex);
8385
9328
  }
8386
9329
  let gcOrphanEmbeddings;
8387
9330
  let gcOrphanChunks;
@@ -8396,7 +9339,7 @@ var Indexer = class {
8396
9339
  if (!await this.tryResetCorruptedIndex("running index health check", error)) {
8397
9340
  throw error;
8398
9341
  }
8399
- await this.ensureInitialized();
9342
+ await this.initializeUnlocked("writer", [], { skipAutoGc: true });
8400
9343
  return {
8401
9344
  removed: 0,
8402
9345
  filePaths: [],
@@ -8405,7 +9348,7 @@ var Indexer = class {
8405
9348
  gcOrphanSymbols: 0,
8406
9349
  gcOrphanCallEdges: 0,
8407
9350
  resetCorruptedIndex: true,
8408
- warning: this.getCorruptedIndexWarning(path11.join(this.indexPath, "codebase.db"))
9351
+ warning: this.getCorruptedIndexWarning(path12.join(this.indexPath, "codebase.db"))
8409
9352
  };
8410
9353
  }
8411
9354
  this.logger.recordGc(removedCount, gcOrphanChunks, gcOrphanEmbeddings);
@@ -8418,7 +9361,13 @@ var Indexer = class {
8418
9361
  return { removed: removedCount, filePaths: removedFilePaths, gcOrphanEmbeddings, gcOrphanChunks, gcOrphanSymbols, gcOrphanCallEdges };
8419
9362
  }
8420
9363
  async retryFailedBatches() {
8421
- const { store, provider, invertedIndex, database, configuredProviderInfo } = await this.ensureInitialized();
9364
+ return this.withIndexMutationLease("retry-failed-batches", async (recoveredOwners) => {
9365
+ await this.ensureInitializedUnlocked(recoveredOwners);
9366
+ return this.retryFailedBatchesUnlocked();
9367
+ });
9368
+ }
9369
+ async retryFailedBatchesUnlocked() {
9370
+ const { store, provider, invertedIndex, database, configuredProviderInfo } = this.requireLoadedIndexState();
8422
9371
  const maxChunkTokens = getSafeEmbeddingChunkTokenLimit(configuredProviderInfo);
8423
9372
  const providerRateLimits = this.getProviderRateLimits(configuredProviderInfo.provider);
8424
9373
  const roots = this.config.scope === "global" ? this.getScopedRoots() : null;
@@ -8582,7 +9531,7 @@ var Indexer = class {
8582
9531
  }
8583
9532
  if (succeeded > 0) {
8584
9533
  store.save();
8585
- invertedIndex.save();
9534
+ this.saveInvertedIndex(invertedIndex);
8586
9535
  }
8587
9536
  if (roots && succeeded > 0 && persistedStillFailing.length === 0 && this.hasProjectForceReembedPending()) {
8588
9537
  database.deleteMetadata(this.getProjectForceReembedMetadataKey());
@@ -8610,15 +9559,16 @@ var Indexer = class {
8610
9559
  }
8611
9560
  }
8612
9561
  async getDatabaseStats() {
8613
- const { database } = await this.ensureInitialized();
9562
+ const { database, readIssues } = await this.ensureInitialized();
9563
+ this.requireReadableComponents(readIssues, "database");
8614
9564
  return database.getStats();
8615
9565
  }
8616
9566
  getLogger() {
8617
9567
  return this.logger;
8618
9568
  }
8619
9569
  async findSimilar(code, limit = this.config.search.maxResults, options) {
8620
- const { store, provider, database } = await this.ensureInitialized();
8621
- const compatibility = this.checkCompatibility();
9570
+ const { store, provider, database, readIssues, compatibility } = await this.ensureInitialized();
9571
+ this.requireReadableComponents(readIssues, "vectors", "database");
8622
9572
  if (!compatibility.compatible) {
8623
9573
  throw new Error(
8624
9574
  `${compatibility.reason ?? "Index is incompatible with current embedding provider."} Run index_codebase with force=true to rebuild the index.`
@@ -8732,7 +9682,8 @@ var Indexer = class {
8732
9682
  );
8733
9683
  }
8734
9684
  async getCallers(targetName, callTypeFilter) {
8735
- const { database } = await this.ensureInitialized();
9685
+ const { database, readIssues } = await this.ensureInitialized();
9686
+ this.requireReadableComponents(readIssues, "database");
8736
9687
  const seen = /* @__PURE__ */ new Set();
8737
9688
  const results = [];
8738
9689
  for (const branchKey of this.getBranchCatalogKeys()) {
@@ -8746,7 +9697,8 @@ var Indexer = class {
8746
9697
  return results;
8747
9698
  }
8748
9699
  async getCallees(symbolId, callTypeFilter) {
8749
- const { database } = await this.ensureInitialized();
9700
+ const { database, readIssues } = await this.ensureInitialized();
9701
+ this.requireReadableComponents(readIssues, "database");
8750
9702
  const seen = /* @__PURE__ */ new Set();
8751
9703
  const results = [];
8752
9704
  for (const branchKey of this.getBranchCatalogKeys()) {
@@ -8760,43 +9712,50 @@ var Indexer = class {
8760
9712
  return results;
8761
9713
  }
8762
9714
  async findCallPath(fromName, toName, maxDepth) {
8763
- const { database } = await this.ensureInitialized();
9715
+ const { database, readIssues } = await this.ensureInitialized();
9716
+ this.requireReadableComponents(readIssues, "database");
8764
9717
  let shortest = [];
8765
9718
  for (const branchKey of this.getBranchCatalogKeys()) {
8766
- const path26 = database.findShortestPath(fromName, toName, branchKey, maxDepth);
8767
- if (path26.length > 0 && (shortest.length === 0 || path26.length < shortest.length)) {
8768
- shortest = path26;
9719
+ const path27 = database.findShortestPath(fromName, toName, branchKey, maxDepth);
9720
+ if (path27.length > 0 && (shortest.length === 0 || path27.length < shortest.length)) {
9721
+ shortest = path27;
8769
9722
  }
8770
9723
  }
8771
9724
  return shortest;
8772
9725
  }
8773
9726
  async getSymbolsForBranch(branch) {
8774
- const { database } = await this.ensureInitialized();
9727
+ const { database, readIssues } = await this.ensureInitialized();
9728
+ this.requireReadableComponents(readIssues, "database");
8775
9729
  const resolvedBranch = branch ?? this.getBranchCatalogKey();
8776
9730
  return database.getSymbolsForBranch(resolvedBranch);
8777
9731
  }
8778
9732
  async getSymbolsForFiles(filePaths, branch) {
8779
- const { database } = await this.ensureInitialized();
9733
+ const { database, readIssues } = await this.ensureInitialized();
9734
+ this.requireReadableComponents(readIssues, "database");
8780
9735
  const resolvedBranch = branch ?? this.getBranchCatalogKey();
8781
9736
  return database.getSymbolsForFiles(filePaths, resolvedBranch);
8782
9737
  }
8783
9738
  async getTransitiveReachability(rootSymbolIds, direction, maxDepth) {
8784
- const { database } = await this.ensureInitialized();
9739
+ const { database, readIssues } = await this.ensureInitialized();
9740
+ this.requireReadableComponents(readIssues, "database");
8785
9741
  const branch = this.getBranchCatalogKey();
8786
9742
  return database.getTransitiveReachability(rootSymbolIds, branch, direction, maxDepth);
8787
9743
  }
8788
9744
  async detectCommunities(branch, symbolIds) {
8789
- const { database } = await this.ensureInitialized();
9745
+ const { database, readIssues } = await this.ensureInitialized();
9746
+ this.requireReadableComponents(readIssues, "database");
8790
9747
  const resolvedBranch = branch ?? this.getBranchCatalogKey();
8791
9748
  return database.detectCommunities(resolvedBranch, symbolIds);
8792
9749
  }
8793
9750
  async computeCentrality(branch) {
8794
- const { database } = await this.ensureInitialized();
9751
+ const { database, readIssues } = await this.ensureInitialized();
9752
+ this.requireReadableComponents(readIssues, "database");
8795
9753
  const resolvedBranch = branch ?? this.getBranchCatalogKey();
8796
9754
  return database.computeCentrality(resolvedBranch);
8797
9755
  }
8798
9756
  async getPrImpact(opts) {
8799
- const { database } = await this.ensureInitialized();
9757
+ const { database, readIssues } = await this.ensureInitialized();
9758
+ this.requireReadableComponents(readIssues, "database");
8800
9759
  const execFileAsync3 = promisify3(execFile3);
8801
9760
  const changedFilesResult = await getChangedFiles({
8802
9761
  pr: opts.pr,
@@ -8819,7 +9778,7 @@ var Indexer = class {
8819
9778
  "Run index_codebase first to build the call graph and symbol index for this branch."
8820
9779
  );
8821
9780
  }
8822
- const absoluteChangedFiles = changedFiles.map((f) => path11.resolve(this.projectRoot, f));
9781
+ const absoluteChangedFiles = changedFiles.map((f) => path12.resolve(this.projectRoot, f));
8823
9782
  const directSymbols = database.getSymbolsForFiles(absoluteChangedFiles, branchKey);
8824
9783
  const directIds = directSymbols.map((s) => s.id);
8825
9784
  const direction = opts.direction ?? "both";
@@ -8902,7 +9861,7 @@ var Indexer = class {
8902
9861
  projectRoot: this.projectRoot,
8903
9862
  baseBranch: this.baseBranch
8904
9863
  });
8905
- const otherAbsolute = otherChanged.files.map((f) => path11.resolve(this.projectRoot, f));
9864
+ const otherAbsolute = otherChanged.files.map((f) => path12.resolve(this.projectRoot, f));
8906
9865
  const otherBranchKey = this.getBranchCatalogKeyFor(openPr.headRefName);
8907
9866
  const otherSymbols = database.getSymbolsForFiles(otherAbsolute, otherBranchKey);
8908
9867
  const otherLabels = /* @__PURE__ */ new Set();
@@ -8952,7 +9911,8 @@ var Indexer = class {
8952
9911
  };
8953
9912
  }
8954
9913
  async getVisualizationData(options) {
8955
- const { database, store } = await this.ensureInitialized();
9914
+ const { database, store, readIssues } = await this.ensureInitialized();
9915
+ this.requireReadableComponents(readIssues, "vectors", "database");
8956
9916
  const seenSymbols = /* @__PURE__ */ new Map();
8957
9917
  const seenEdges = /* @__PURE__ */ new Map();
8958
9918
  for (const branchKey of this.getBranchCatalogKeys()) {
@@ -8965,12 +9925,12 @@ var Indexer = class {
8965
9925
  if (meta.filePath) filePaths.add(meta.filePath);
8966
9926
  }
8967
9927
  const directory = options?.directory?.replace(/\/$/, "");
8968
- const absoluteDirectoryFilter = directory ? path11.resolve(this.projectRoot, directory) : void 0;
9928
+ const absoluteDirectoryFilter = directory ? path12.resolve(this.projectRoot, directory) : void 0;
8969
9929
  for (const filePath of filePaths) {
8970
9930
  if (directory) {
8971
- const absoluteFilePath = path11.resolve(filePath);
9931
+ const absoluteFilePath = path12.resolve(filePath);
8972
9932
  const matchesRelative = filePath === directory || filePath.startsWith(directory + "/");
8973
- const matchesProjectRelative = absoluteDirectoryFilter !== void 0 && (absoluteFilePath === absoluteDirectoryFilter || absoluteFilePath.startsWith(absoluteDirectoryFilter + path11.sep));
9933
+ const matchesProjectRelative = absoluteDirectoryFilter !== void 0 && (absoluteFilePath === absoluteDirectoryFilter || absoluteFilePath.startsWith(absoluteDirectoryFilter + path12.sep));
8974
9934
  if (!matchesRelative && !matchesProjectRelative) {
8975
9935
  continue;
8976
9936
  }
@@ -8992,12 +9952,23 @@ var Indexer = class {
8992
9952
  return { symbols: [...seenSymbols.values()], edges: [...seenEdges.values()] };
8993
9953
  }
8994
9954
  async close() {
8995
- await this.database?.close();
9955
+ this.database?.close();
9956
+ for (const database of this.retiredDatabases) {
9957
+ database.close();
9958
+ }
9959
+ this.retiredDatabases = [];
8996
9960
  this.database = null;
8997
9961
  this.store = null;
8998
9962
  this.invertedIndex = null;
8999
9963
  this.provider = null;
9000
9964
  this.reranker = null;
9965
+ this.configuredProviderInfo = null;
9966
+ this.indexCompatibility = null;
9967
+ this.initializationMode = "none";
9968
+ this.readIssues = [];
9969
+ this.readerArtifactFingerprint = null;
9970
+ this.writerArtifactFingerprint = null;
9971
+ this.readerArtifactRetryAfter.clear();
9001
9972
  }
9002
9973
  };
9003
9974
 
@@ -9248,15 +10219,15 @@ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingToke
9248
10219
  }
9249
10220
 
9250
10221
  // src/eval/runner-config.ts
9251
- import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7, rmSync, writeFileSync as writeFileSync3 } from "fs";
9252
- import * as os4 from "os";
9253
- import * as path13 from "path";
10222
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync8, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
10223
+ import * as os5 from "os";
10224
+ import * as path14 from "path";
9254
10225
 
9255
10226
  // src/config/rebase.ts
9256
- import * as path12 from "path";
10227
+ import * as path13 from "path";
9257
10228
  function isWithinRoot(rootDir, targetPath) {
9258
- const relativePath = path12.relative(rootDir, targetPath);
9259
- return relativePath === "" || !relativePath.startsWith("..") && !path12.isAbsolute(relativePath);
10229
+ const relativePath = path13.relative(rootDir, targetPath);
10230
+ return relativePath === "" || !relativePath.startsWith("..") && !path13.isAbsolute(relativePath);
9260
10231
  }
9261
10232
  function rebasePathEntries(values, fromDir, toDir) {
9262
10233
  if (!Array.isArray(values)) {
@@ -9264,10 +10235,10 @@ function rebasePathEntries(values, fromDir, toDir) {
9264
10235
  }
9265
10236
  return values.filter((value) => typeof value === "string").map((value) => {
9266
10237
  const trimmed = value.trim();
9267
- if (!trimmed || path12.isAbsolute(trimmed)) {
10238
+ if (!trimmed || path13.isAbsolute(trimmed)) {
9268
10239
  return trimmed;
9269
10240
  }
9270
- return normalizePathSeparators(path12.normalize(path12.relative(toDir, path12.resolve(fromDir, trimmed))));
10241
+ return normalizePathSeparators(path13.normalize(path13.relative(toDir, path13.resolve(fromDir, trimmed))));
9271
10242
  }).filter(Boolean);
9272
10243
  }
9273
10244
  function resolveInheritedKnowledgeBaseEntries(values, sourceRoot, targetRoot) {
@@ -9279,17 +10250,17 @@ function resolveInheritedKnowledgeBaseEntries(values, sourceRoot, targetRoot) {
9279
10250
  if (!trimmed) {
9280
10251
  return trimmed;
9281
10252
  }
9282
- if (path12.isAbsolute(trimmed)) {
10253
+ if (path13.isAbsolute(trimmed)) {
9283
10254
  if (isWithinRoot(sourceRoot, trimmed)) {
9284
- return normalizePathSeparators(path12.normalize(path12.relative(sourceRoot, trimmed) || "."));
10255
+ return normalizePathSeparators(path13.normalize(path13.relative(sourceRoot, trimmed) || "."));
9285
10256
  }
9286
- return path12.normalize(trimmed);
10257
+ return path13.normalize(trimmed);
9287
10258
  }
9288
- const resolvedFromSource = path12.resolve(sourceRoot, trimmed);
10259
+ const resolvedFromSource = path13.resolve(sourceRoot, trimmed);
9289
10260
  if (isWithinRoot(sourceRoot, resolvedFromSource)) {
9290
- return normalizePathSeparators(path12.normalize(trimmed));
10261
+ return normalizePathSeparators(path13.normalize(trimmed));
9291
10262
  }
9292
- return normalizePathSeparators(path12.normalize(path12.relative(targetRoot, resolvedFromSource)));
10263
+ return normalizePathSeparators(path13.normalize(path13.relative(targetRoot, resolvedFromSource)));
9293
10264
  }).filter(Boolean);
9294
10265
  }
9295
10266
 
@@ -9327,7 +10298,7 @@ function validateEvalConfigShape(rawConfig, filePath) {
9327
10298
  }
9328
10299
  function parseJsonConfigFile(filePath) {
9329
10300
  try {
9330
- return validateEvalConfigShape(JSON.parse(readFileSync7(filePath, "utf-8")), filePath);
10301
+ return validateEvalConfigShape(JSON.parse(readFileSync8(filePath, "utf-8")), filePath);
9331
10302
  } catch (error) {
9332
10303
  if (error instanceof Error && error.message.startsWith("Eval config at ")) {
9333
10304
  throw error;
@@ -9337,20 +10308,20 @@ function parseJsonConfigFile(filePath) {
9337
10308
  }
9338
10309
  }
9339
10310
  function toAbsolute(projectRoot, maybeRelative) {
9340
- return path13.isAbsolute(maybeRelative) ? maybeRelative : path13.join(projectRoot, maybeRelative);
10311
+ return path14.isAbsolute(maybeRelative) ? maybeRelative : path14.join(projectRoot, maybeRelative);
9341
10312
  }
9342
10313
  function isProjectScopedConfigPath(configPath) {
9343
- return path13.basename(configPath) === "codebase-index.json" && path13.basename(path13.dirname(configPath)) === ".opencode";
10314
+ return path14.basename(configPath) === "codebase-index.json" && path14.basename(path14.dirname(configPath)) === ".opencode";
9344
10315
  }
9345
10316
  function normalizeEvalConfigKnowledgeBases(rawConfig, projectRoot, resolvedConfigPath) {
9346
10317
  const config = rawConfig && typeof rawConfig === "object" ? { ...rawConfig } : {};
9347
10318
  const rebaseEntries = (values) => isProjectScopedConfigPath(resolvedConfigPath) ? resolveInheritedKnowledgeBaseEntries(
9348
10319
  values,
9349
- path13.dirname(path13.dirname(resolvedConfigPath)),
10320
+ path14.dirname(path14.dirname(resolvedConfigPath)),
9350
10321
  projectRoot
9351
10322
  ) : rebasePathEntries(
9352
10323
  values,
9353
- path13.dirname(resolvedConfigPath),
10324
+ path14.dirname(resolvedConfigPath),
9354
10325
  projectRoot
9355
10326
  );
9356
10327
  if (Array.isArray(config.knowledgeBases)) {
@@ -9363,7 +10334,7 @@ function normalizeEvalConfigKnowledgeBases(rawConfig, projectRoot, resolvedConfi
9363
10334
  }
9364
10335
  function loadRawConfig(projectRoot, configPath) {
9365
10336
  const fromPath = configPath ? toAbsolute(projectRoot, configPath) : null;
9366
- if (fromPath && existsSync7(fromPath)) {
10337
+ if (fromPath && existsSync8(fromPath)) {
9367
10338
  return normalizeEvalConfigKnowledgeBases(
9368
10339
  parseJsonConfigFile(fromPath),
9369
10340
  projectRoot,
@@ -9371,15 +10342,15 @@ function loadRawConfig(projectRoot, configPath) {
9371
10342
  );
9372
10343
  }
9373
10344
  const projectConfig = resolveProjectConfigPath(projectRoot);
9374
- if (existsSync7(projectConfig)) {
10345
+ if (existsSync8(projectConfig)) {
9375
10346
  return normalizeEvalConfigKnowledgeBases(
9376
10347
  parseJsonConfigFile(projectConfig),
9377
10348
  projectRoot,
9378
10349
  projectConfig
9379
10350
  );
9380
10351
  }
9381
- const globalConfig = path13.join(os4.homedir(), ".config", "opencode", "codebase-index.json");
9382
- if (existsSync7(globalConfig)) {
10352
+ const globalConfig = path14.join(os5.homedir(), ".config", "opencode", "codebase-index.json");
10353
+ if (existsSync8(globalConfig)) {
9383
10354
  return parseJsonConfigFile(globalConfig);
9384
10355
  }
9385
10356
  return {};
@@ -9388,24 +10359,24 @@ function getIndexRootPath(projectRoot, scope) {
9388
10359
  return scope === "global" ? getGlobalIndexPath() : resolveProjectIndexPath(projectRoot, scope);
9389
10360
  }
9390
10361
  function getLocalProjectIndexRoot(projectRoot) {
9391
- return path13.join(projectRoot, ".opencode", "index");
10362
+ return path14.join(projectRoot, ".opencode", "index");
9392
10363
  }
9393
10364
  function getLocalProjectConfigPath(projectRoot) {
9394
- return path13.join(projectRoot, ".opencode", "codebase-index.json");
10365
+ return path14.join(projectRoot, ".opencode", "codebase-index.json");
9395
10366
  }
9396
10367
  function clearIndexRoot(projectRoot, scope) {
9397
10368
  const indexRoot = scope === "global" ? getIndexRootPath(projectRoot, scope) : getLocalProjectIndexRoot(projectRoot);
9398
- if (existsSync7(indexRoot)) {
9399
- rmSync(indexRoot, { recursive: true, force: true });
10369
+ if (existsSync8(indexRoot)) {
10370
+ rmSync2(indexRoot, { recursive: true, force: true });
9400
10371
  }
9401
10372
  }
9402
10373
  function ensureLocalEvalProjectConfig(projectRoot, configPath) {
9403
10374
  const localConfigPath = getLocalProjectConfigPath(projectRoot);
9404
10375
  const resolvedConfigPath = configPath ? toAbsolute(projectRoot, configPath) : resolveProjectConfigPath(projectRoot);
9405
- if (!configPath && existsSync7(localConfigPath)) {
10376
+ if (!configPath && existsSync8(localConfigPath)) {
9406
10377
  return localConfigPath;
9407
10378
  }
9408
- if (!existsSync7(resolvedConfigPath) || resolvedConfigPath === localConfigPath) {
10379
+ if (!existsSync8(resolvedConfigPath) || resolvedConfigPath === localConfigPath) {
9409
10380
  return resolvedConfigPath;
9410
10381
  }
9411
10382
  const sourceConfig = normalizeEvalConfigKnowledgeBases(
@@ -9413,8 +10384,8 @@ function ensureLocalEvalProjectConfig(projectRoot, configPath) {
9413
10384
  projectRoot,
9414
10385
  resolvedConfigPath
9415
10386
  );
9416
- mkdirSync3(path13.dirname(localConfigPath), { recursive: true });
9417
- writeFileSync3(localConfigPath, JSON.stringify(sourceConfig, null, 2), "utf-8");
10387
+ mkdirSync4(path14.dirname(localConfigPath), { recursive: true });
10388
+ writeFileSync4(localConfigPath, JSON.stringify(sourceConfig, null, 2), "utf-8");
9418
10389
  return localConfigPath;
9419
10390
  }
9420
10391
  function loadParsedConfig(projectRoot, configPath) {
@@ -9447,9 +10418,9 @@ function getEmbeddingCostPer1MTokens(embeddingProvider) {
9447
10418
  }
9448
10419
 
9449
10420
  // src/eval/schema.ts
9450
- import { readFileSync as readFileSync8 } from "fs";
10421
+ import { readFileSync as readFileSync9 } from "fs";
9451
10422
  function parseJsonFile(filePath) {
9452
- const content = readFileSync8(filePath, "utf-8");
10423
+ const content = readFileSync9(filePath, "utf-8");
9453
10424
  try {
9454
10425
  return JSON.parse(content);
9455
10426
  } catch (error) {
@@ -9463,23 +10434,23 @@ function isRecord2(value) {
9463
10434
  function isStringArray3(value) {
9464
10435
  return Array.isArray(value) && value.every((item) => typeof item === "string");
9465
10436
  }
9466
- function asPositiveNumber(value, path26) {
10437
+ function asPositiveNumber(value, path27) {
9467
10438
  if (typeof value !== "number" || Number.isNaN(value) || value < 0) {
9468
- throw new Error(`${path26} must be a non-negative number`);
10439
+ throw new Error(`${path27} must be a non-negative number`);
9469
10440
  }
9470
10441
  return value;
9471
10442
  }
9472
- function parseQueryType(value, path26) {
10443
+ function parseQueryType(value, path27) {
9473
10444
  if (value === "definition" || value === "implementation-intent" || value === "similarity" || value === "keyword-heavy") {
9474
10445
  return value;
9475
10446
  }
9476
10447
  throw new Error(
9477
- `${path26} must be one of: definition, implementation-intent, similarity, keyword-heavy`
10448
+ `${path27} must be one of: definition, implementation-intent, similarity, keyword-heavy`
9478
10449
  );
9479
10450
  }
9480
- function parseExpected(input, path26) {
10451
+ function parseExpected(input, path27) {
9481
10452
  if (!isRecord2(input)) {
9482
- throw new Error(`${path26} must be an object`);
10453
+ throw new Error(`${path27} must be an object`);
9483
10454
  }
9484
10455
  const filePathRaw = input.filePath;
9485
10456
  const acceptableFilesRaw = input.acceptableFiles;
@@ -9488,16 +10459,16 @@ function parseExpected(input, path26) {
9488
10459
  const filePath = typeof filePathRaw === "string" ? filePathRaw : void 0;
9489
10460
  const acceptableFiles = isStringArray3(acceptableFilesRaw) ? acceptableFilesRaw : void 0;
9490
10461
  if (!filePath && (!acceptableFiles || acceptableFiles.length === 0)) {
9491
- throw new Error(`${path26} must include either expected.filePath or expected.acceptableFiles`);
10462
+ throw new Error(`${path27} must include either expected.filePath or expected.acceptableFiles`);
9492
10463
  }
9493
10464
  if (acceptableFilesRaw !== void 0 && !isStringArray3(acceptableFilesRaw)) {
9494
- throw new Error(`${path26}.acceptableFiles must be an array of strings`);
10465
+ throw new Error(`${path27}.acceptableFiles must be an array of strings`);
9495
10466
  }
9496
10467
  if (symbolRaw !== void 0 && typeof symbolRaw !== "string") {
9497
- throw new Error(`${path26}.symbol must be a string when provided`);
10468
+ throw new Error(`${path27}.symbol must be a string when provided`);
9498
10469
  }
9499
10470
  if (branchRaw !== void 0 && typeof branchRaw !== "string") {
9500
- throw new Error(`${path26}.branch must be a string when provided`);
10471
+ throw new Error(`${path27}.branch must be a string when provided`);
9501
10472
  }
9502
10473
  return {
9503
10474
  filePath,
@@ -9507,25 +10478,25 @@ function parseExpected(input, path26) {
9507
10478
  };
9508
10479
  }
9509
10480
  function parseQuery(input, index) {
9510
- const path26 = `queries[${index}]`;
10481
+ const path27 = `queries[${index}]`;
9511
10482
  if (!isRecord2(input)) {
9512
- throw new Error(`${path26} must be an object`);
10483
+ throw new Error(`${path27} must be an object`);
9513
10484
  }
9514
10485
  const id = input.id;
9515
10486
  const query = input.query;
9516
10487
  const queryType = input.queryType;
9517
10488
  const expected = input.expected;
9518
10489
  if (typeof id !== "string" || id.trim().length === 0) {
9519
- throw new Error(`${path26}.id must be a non-empty string`);
10490
+ throw new Error(`${path27}.id must be a non-empty string`);
9520
10491
  }
9521
10492
  if (typeof query !== "string" || query.trim().length === 0) {
9522
- throw new Error(`${path26}.query must be a non-empty string`);
10493
+ throw new Error(`${path27}.query must be a non-empty string`);
9523
10494
  }
9524
10495
  return {
9525
10496
  id,
9526
10497
  query,
9527
- queryType: parseQueryType(queryType, `${path26}.queryType`),
9528
- expected: parseExpected(expected, `${path26}.expected`)
10498
+ queryType: parseQueryType(queryType, `${path27}.queryType`),
10499
+ expected: parseExpected(expected, `${path27}.expected`)
9529
10500
  };
9530
10501
  }
9531
10502
  function parseGoldenDataset(raw, sourceLabel) {
@@ -9708,13 +10679,13 @@ async function runEvaluation(options) {
9708
10679
  };
9709
10680
  const outputDir = createRunDirectory(toAbsolute(options.projectRoot, options.outputRoot));
9710
10681
  const perQueryArtifact = buildPerQueryArtifact(perQuery);
9711
- writeJson(path14.join(outputDir, "summary.json"), summary);
9712
- writeJson(path14.join(outputDir, "per-query.json"), perQueryArtifact);
10682
+ writeJson(path15.join(outputDir, "summary.json"), summary);
10683
+ writeJson(path15.join(outputDir, "per-query.json"), perQueryArtifact);
9713
10684
  let comparison;
9714
10685
  if (againstPath) {
9715
10686
  const baseline = loadSummary(againstPath);
9716
10687
  comparison = compareSummaries(summary, baseline, againstPath);
9717
- writeJson(path14.join(outputDir, "compare.json"), comparison);
10688
+ writeJson(path15.join(outputDir, "compare.json"), comparison);
9718
10689
  }
9719
10690
  let gate;
9720
10691
  if (options.ciMode) {
@@ -9724,10 +10695,10 @@ async function runEvaluation(options) {
9724
10695
  const budget = loadBudget(budgetPath);
9725
10696
  if (!comparison && budget.baselinePath) {
9726
10697
  const resolvedBaseline = toAbsolute(options.projectRoot, budget.baselinePath);
9727
- if (existsSync8(resolvedBaseline)) {
10698
+ if (existsSync9(resolvedBaseline)) {
9728
10699
  const baselineSummary = loadSummary(resolvedBaseline);
9729
10700
  comparison = compareSummaries(summary, baselineSummary, resolvedBaseline);
9730
- writeJson(path14.join(outputDir, "compare.json"), comparison);
10701
+ writeJson(path15.join(outputDir, "compare.json"), comparison);
9731
10702
  } else if (budget.failOnMissingBaseline) {
9732
10703
  throw new Error(
9733
10704
  `Budget baseline is missing: ${resolvedBaseline}. Set failOnMissingBaseline=false to allow CI run without baseline.`
@@ -9737,7 +10708,7 @@ async function runEvaluation(options) {
9737
10708
  gate = evaluateBudgetGate(budget, summary, comparison);
9738
10709
  }
9739
10710
  const markdown = createSummaryMarkdown(summary, comparison, gate);
9740
- writeText(path14.join(outputDir, "summary.md"), markdown);
10711
+ writeText(path15.join(outputDir, "summary.md"), markdown);
9741
10712
  return { outputDir, summary, perQuery, comparison, gate };
9742
10713
  } finally {
9743
10714
  await indexer.close();
@@ -9795,23 +10766,23 @@ async function runSweep(options, sweep) {
9795
10766
  bestByMrrAt10,
9796
10767
  bestByP95Latency
9797
10768
  };
9798
- writeJson(path14.join(outputDir, "compare.json"), aggregate);
10769
+ writeJson(path15.join(outputDir, "compare.json"), aggregate);
9799
10770
  const md = createSummaryMarkdown(
9800
10771
  bestByHitAt5?.summary ?? runs[0].summary,
9801
10772
  bestByHitAt5?.comparison,
9802
10773
  void 0,
9803
10774
  aggregate
9804
10775
  );
9805
- writeText(path14.join(outputDir, "summary.md"), md);
9806
- writeJson(path14.join(outputDir, "summary.json"), bestByHitAt5?.summary ?? runs[0].summary);
10776
+ writeText(path15.join(outputDir, "summary.md"), md);
10777
+ writeJson(path15.join(outputDir, "summary.json"), bestByHitAt5?.summary ?? runs[0].summary);
9807
10778
  return { outputDir, aggregate };
9808
10779
  }
9809
10780
 
9810
10781
  // src/eval/cli.ts
9811
- import * as path16 from "path";
10782
+ import * as path17 from "path";
9812
10783
 
9813
10784
  // src/eval/cli-parser.ts
9814
- import * as path15 from "path";
10785
+ import * as path16 from "path";
9815
10786
  function printUsage() {
9816
10787
  console.log(`
9817
10788
  Usage:
@@ -9883,12 +10854,12 @@ function parseEvalArgs(argv, cwd) {
9883
10854
  const arg = argv[i];
9884
10855
  const next = argv[i + 1];
9885
10856
  if (arg === "--project" && next) {
9886
- parsed.projectRoot = path15.resolve(cwd, next);
10857
+ parsed.projectRoot = path16.resolve(cwd, next);
9887
10858
  i += 1;
9888
10859
  continue;
9889
10860
  }
9890
10861
  if (arg === "--config" && next) {
9891
- parsed.configPath = path15.resolve(cwd, next);
10862
+ parsed.configPath = path16.resolve(cwd, next);
9892
10863
  i += 1;
9893
10864
  continue;
9894
10865
  }
@@ -10084,22 +11055,22 @@ async function handleEvalCommand(args, cwd) {
10084
11055
  if (!parsed.againstPath.endsWith(".json")) {
10085
11056
  throw new Error("eval diff --against must point to a summary JSON file");
10086
11057
  }
10087
- const currentSummary = loadSummary(path16.resolve(parsed.projectRoot, currentPath), {
11058
+ const currentSummary = loadSummary(path17.resolve(parsed.projectRoot, currentPath), {
10088
11059
  allowLegacyDiversityMetrics: true
10089
11060
  });
10090
- const baselineSummary = loadSummary(path16.resolve(parsed.projectRoot, parsed.againstPath), {
11061
+ const baselineSummary = loadSummary(path17.resolve(parsed.projectRoot, parsed.againstPath), {
10091
11062
  allowLegacyDiversityMetrics: true
10092
11063
  });
10093
11064
  const comparison = compareSummaries(
10094
11065
  currentSummary,
10095
11066
  baselineSummary,
10096
- path16.resolve(parsed.projectRoot, parsed.againstPath)
11067
+ path17.resolve(parsed.projectRoot, parsed.againstPath)
10097
11068
  );
10098
- const outputDir = createRunDirectory(path16.resolve(parsed.projectRoot, parsed.outputRoot));
11069
+ const outputDir = createRunDirectory(path17.resolve(parsed.projectRoot, parsed.outputRoot));
10099
11070
  const summaryMd = createSummaryMarkdown(currentSummary, comparison);
10100
- writeJson(path16.join(outputDir, "compare.json"), comparison);
10101
- writeText(path16.join(outputDir, "summary.md"), summaryMd);
10102
- writeJson(path16.join(outputDir, "summary.json"), currentSummary);
11071
+ writeJson(path17.join(outputDir, "compare.json"), comparison);
11072
+ writeText(path17.join(outputDir, "summary.md"), summaryMd);
11073
+ writeJson(path17.join(outputDir, "summary.json"), currentSummary);
10103
11074
  console.log(`Eval diff complete. Artifacts: ${outputDir}`);
10104
11075
  return 0;
10105
11076
  }
@@ -10108,7 +11079,7 @@ async function handleEvalCommand(args, cwd) {
10108
11079
 
10109
11080
  // src/mcp-server.ts
10110
11081
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10111
- import { readFileSync as readFileSync10 } from "fs";
11082
+ import { readFileSync as readFileSync11 } from "fs";
10112
11083
 
10113
11084
  // src/mcp-server/register-prompts.ts
10114
11085
  import { z } from "zod";
@@ -10299,6 +11270,10 @@ function formatStatus(status) {
10299
11270
  lines.push(`Failed batches: ${status.failedBatchesPath}`);
10300
11271
  }
10301
11272
  }
11273
+ if (status.warning) {
11274
+ lines.push("");
11275
+ lines.push(`INDEX WARNING: ${status.warning}`);
11276
+ }
10302
11277
  if (status.compatibility && !status.compatibility.compatible) {
10303
11278
  lines.push("");
10304
11279
  lines.push(`COMPATIBILITY WARNING: ${status.compatibility.reason}`);
@@ -10404,16 +11379,16 @@ function formatCallGraphCallees(symbolId, callees, relationshipType) {
10404
11379
  return `[${index + 1}] \u2192 ${edge.targetName} (${edge.callType})${confidence} at line ${edge.line}${edge.isResolved ? ` [resolved: ${edge.toSymbolId}]` : " [unresolved]"}`;
10405
11380
  }).join("\n");
10406
11381
  }
10407
- function formatCallGraphPath(from, to, path26) {
10408
- if (path26.length === 0) {
11382
+ function formatCallGraphPath(from, to, path27) {
11383
+ if (path27.length === 0) {
10409
11384
  return `No path found between "${from}" and "${to}". They may be in disconnected components, or the call graph index needs updating.`;
10410
11385
  }
10411
- const formatted = path26.map((hop, index) => {
11386
+ const formatted = path27.map((hop, index) => {
10412
11387
  const prefix = index === 0 ? "[start]" : `--${hop.callType}-->`;
10413
11388
  const location = hop.filePath ? ` (${hop.filePath}:${hop.line})` : "";
10414
11389
  return `${prefix} ${hop.symbolName}${location}`;
10415
11390
  });
10416
- return `Path (${path26.length} hops):
11391
+ return `Path (${path27.length} hops):
10417
11392
  ${formatted.join("\n")}`;
10418
11393
  }
10419
11394
  function formatResultHeader(result, index) {
@@ -10504,12 +11479,12 @@ function formatPrImpact(result) {
10504
11479
  }
10505
11480
 
10506
11481
  // src/tools/operations.ts
10507
- import { existsSync as existsSync11, realpathSync, statSync as statSync3 } from "fs";
10508
- import * as path20 from "path";
11482
+ import { existsSync as existsSync12, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
11483
+ import * as path21 from "path";
10509
11484
 
10510
11485
  // src/config/merger.ts
10511
- import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
10512
- import * as path17 from "path";
11486
+ import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
11487
+ import * as path18 from "path";
10513
11488
  var PROJECT_OVERRIDE_KEYS = [
10514
11489
  "embeddingProvider",
10515
11490
  "customProvider",
@@ -10542,8 +11517,8 @@ function mergeUniqueStringArray(values) {
10542
11517
  return [...new Set(values.map((value) => String(value).trim()))];
10543
11518
  }
10544
11519
  function normalizeKnowledgeBasePath(value) {
10545
- let normalized = path17.normalize(String(value).trim());
10546
- const root = path17.parse(normalized).root;
11520
+ let normalized = path18.normalize(String(value).trim());
11521
+ const root = path18.parse(normalized).root;
10547
11522
  while (normalized.length > root.length && /[\\/]$/.test(normalized)) {
10548
11523
  normalized = normalized.slice(0, -1);
10549
11524
  }
@@ -10577,11 +11552,11 @@ function validateConfigLayerShape(rawConfig, filePath) {
10577
11552
  return rawConfig;
10578
11553
  }
10579
11554
  function loadJsonFile(filePath) {
10580
- if (!existsSync9(filePath)) {
11555
+ if (!existsSync10(filePath)) {
10581
11556
  return null;
10582
11557
  }
10583
11558
  try {
10584
- const content = readFileSync9(filePath, "utf-8");
11559
+ const content = readFileSync10(filePath, "utf-8");
10585
11560
  return validateConfigLayerShape(JSON.parse(content), filePath);
10586
11561
  } catch (error) {
10587
11562
  if (error instanceof Error && error.message.startsWith("Config file ")) {
@@ -10596,8 +11571,8 @@ function loadConfigFile(filePath) {
10596
11571
  }
10597
11572
  function materializeLocalProjectConfig(projectRoot, config, host = "opencode") {
10598
11573
  const localConfigPath = resolveWritableProjectConfigPath(projectRoot, host);
10599
- mkdirSync4(path17.dirname(localConfigPath), { recursive: true });
10600
- writeFileSync4(localConfigPath, JSON.stringify(config, null, 2), "utf-8");
11574
+ mkdirSync5(path18.dirname(localConfigPath), { recursive: true });
11575
+ writeFileSync5(localConfigPath, JSON.stringify(config, null, 2), "utf-8");
10601
11576
  return localConfigPath;
10602
11577
  }
10603
11578
  function loadProjectConfigLayer(projectRoot, host = "opencode") {
@@ -10607,7 +11582,7 @@ function loadProjectConfigLayer(projectRoot, host = "opencode") {
10607
11582
  return {};
10608
11583
  }
10609
11584
  const normalizedConfig = { ...projectConfig };
10610
- const projectConfigBaseDir = path17.dirname(path17.dirname(projectConfigPath));
11585
+ const projectConfigBaseDir = path18.dirname(path18.dirname(projectConfigPath));
10611
11586
  if (Array.isArray(normalizedConfig.knowledgeBases)) {
10612
11587
  normalizedConfig.knowledgeBases = resolveInheritedKnowledgeBaseEntries(
10613
11588
  normalizedConfig.knowledgeBases,
@@ -10671,19 +11646,19 @@ function loadMergedConfig(projectRoot, host = "opencode") {
10671
11646
  }
10672
11647
 
10673
11648
  // src/tools/knowledge-base-paths.ts
10674
- import * as path18 from "path";
11649
+ import * as path19 from "path";
10675
11650
  function resolveConfigPathValue(value, baseDir) {
10676
11651
  const trimmed = value.trim();
10677
11652
  if (!trimmed) {
10678
11653
  return trimmed;
10679
11654
  }
10680
- const absolutePath = path18.isAbsolute(trimmed) ? trimmed : path18.resolve(baseDir, trimmed);
10681
- return path18.normalize(absolutePath);
11655
+ const absolutePath = path19.isAbsolute(trimmed) ? trimmed : path19.resolve(baseDir, trimmed);
11656
+ return path19.normalize(absolutePath);
10682
11657
  }
10683
11658
 
10684
11659
  // src/tools/config-state.ts
10685
- import { existsSync as existsSync10, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
10686
- import * as path19 from "path";
11660
+ import { existsSync as existsSync11, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
11661
+ import * as path20 from "path";
10687
11662
  function normalizeKnowledgeBasePaths(config, projectRoot) {
10688
11663
  const normalized = { ...config };
10689
11664
  if (Array.isArray(normalized.knowledgeBases)) {
@@ -10707,6 +11682,24 @@ function loadRuntimeConfig(projectRoot, host = "opencode") {
10707
11682
  var indexerCache = /* @__PURE__ */ new Map();
10708
11683
  var configCache = /* @__PURE__ */ new Map();
10709
11684
  var defaultProjectRoots = /* @__PURE__ */ new Map();
11685
+ function getIndexBusyResult(error) {
11686
+ if (!isIndexLockContentionError(error)) return null;
11687
+ const owner = error.owner;
11688
+ const ownerText = owner ? `PID ${owner.pid}, operation ${owner.operation}, since ${owner.startedAt}` : "unreadable owner";
11689
+ if (error.reason === "legacy-lock") {
11690
+ return {
11691
+ kind: "busy",
11692
+ text: `INDEX_BUSY: legacy lock format detected (${ownerText}). Verify the PID and remove this lock manually only if it is stale.`
11693
+ };
11694
+ }
11695
+ if (error.reason === "unknown-owner") {
11696
+ return {
11697
+ kind: "busy",
11698
+ text: `INDEX_BUSY: unreadable or remote lock owner (${ownerText}). Automatic recovery was refused; manual verification is required.`
11699
+ };
11700
+ }
11701
+ return { kind: "busy", text: `INDEX_BUSY: another index operation is already in progress (${ownerText}).` };
11702
+ }
10710
11703
  function getProjectRoot(projectRoot, host) {
10711
11704
  if (projectRoot) {
10712
11705
  return projectRoot;
@@ -10751,8 +11744,8 @@ function refreshIndexerForDirectory(projectRoot, host = "opencode", config = par
10751
11744
  }
10752
11745
  function shouldForceLocalizeProjectIndex(projectRoot, host = "opencode") {
10753
11746
  const root = getProjectRoot(projectRoot, host);
10754
- const localIndexPath = path20.join(root, getHostProjectIndexRelativePath(host));
10755
- if (existsSync11(localIndexPath)) {
11747
+ const localIndexPath = path21.join(root, getHostProjectIndexRelativePath(host));
11748
+ if (existsSync12(localIndexPath)) {
10756
11749
  return false;
10757
11750
  }
10758
11751
  const inheritedIndexPath = resolveWorktreeFallbackProjectIndexPath(root, host);
@@ -10808,35 +11801,46 @@ async function getCallGraphPath(projectRoot, host, from, to, maxDepth) {
10808
11801
  async function runIndexCodebase(projectRoot, host, args, onProgress) {
10809
11802
  const root = getProjectRoot(projectRoot, host);
10810
11803
  const key = getIndexerCacheKey(root, host);
10811
- const cachedConfig = configCache.get(key);
10812
11804
  let indexer = getIndexerForProject(root, host);
10813
- if (args.estimateOnly) {
10814
- return { kind: "estimate", estimate: await indexer.estimateCost() };
10815
- }
10816
- if (args.force) {
10817
- if (shouldForceLocalizeProjectIndex(root, host)) {
10818
- materializeLocalProjectConfig(root, loadProjectConfigLayer(root, host), host);
10819
- refreshIndexerForDirectory(root, host, cachedConfig);
10820
- indexer = getIndexerForProject(root, host);
10821
- }
10822
- await indexer.clearIndex();
10823
- refreshIndexerForDirectory(root, host, cachedConfig);
10824
- indexer = getIndexerForProject(root, host);
10825
- }
10826
- const stats = await indexer.index((progress) => {
10827
- if (onProgress) {
10828
- return onProgress(formatProgressTitle(progress), {
10829
- phase: progress.phase,
10830
- filesProcessed: progress.filesProcessed,
10831
- totalFiles: progress.totalFiles,
10832
- chunksProcessed: progress.chunksProcessed,
10833
- totalChunks: progress.totalChunks,
10834
- percentage: calculatePercentage(progress)
11805
+ const runtimeConfig = configCache.get(key);
11806
+ try {
11807
+ if (args.estimateOnly) {
11808
+ return { kind: "estimate", estimate: await indexer.estimateCost() };
11809
+ }
11810
+ const runIndex = async (target) => {
11811
+ const operation = args.force ? target.forceIndex.bind(target) : target.index.bind(target);
11812
+ return operation((progress) => {
11813
+ if (onProgress) {
11814
+ return onProgress(formatProgressTitle(progress), {
11815
+ phase: progress.phase,
11816
+ filesProcessed: progress.filesProcessed,
11817
+ totalFiles: progress.totalFiles,
11818
+ chunksProcessed: progress.chunksProcessed,
11819
+ totalChunks: progress.totalChunks,
11820
+ percentage: calculatePercentage(progress)
11821
+ });
11822
+ }
11823
+ return Promise.resolve();
10835
11824
  });
11825
+ };
11826
+ let stats;
11827
+ if (args.force && shouldForceLocalizeProjectIndex(root, host)) {
11828
+ const inheritedIndexPath = resolveProjectIndexPath(root, runtimeConfig.scope, host);
11829
+ stats = await withIndexLock(inheritedIndexPath, "force-index", async () => {
11830
+ materializeLocalProjectConfig(root, loadProjectConfigLayer(root, host), host);
11831
+ refreshIndexerForDirectory(root, host, runtimeConfig);
11832
+ indexer = getIndexerForProject(root, host);
11833
+ return runIndex(indexer);
11834
+ }, { completeRecoveries: false });
11835
+ } else {
11836
+ stats = await runIndex(indexer);
10836
11837
  }
10837
- return Promise.resolve();
10838
- });
10839
- return { kind: "stats", stats };
11838
+ return { kind: "stats", stats };
11839
+ } catch (error) {
11840
+ const busyResult = getIndexBusyResult(error);
11841
+ if (!busyResult) throw error;
11842
+ return busyResult;
11843
+ }
10840
11844
  }
10841
11845
  async function getIndexStatus(projectRoot, host) {
10842
11846
  const indexer = getIndexerForProject(projectRoot, host);
@@ -10846,6 +11850,15 @@ async function getIndexHealthCheck(projectRoot, host) {
10846
11850
  const indexer = getIndexerForProject(projectRoot, host);
10847
11851
  return indexer.healthCheck();
10848
11852
  }
11853
+ async function runIndexHealthCheck(projectRoot, host) {
11854
+ try {
11855
+ return { kind: "health", health: await getIndexHealthCheck(projectRoot, host) };
11856
+ } catch (error) {
11857
+ const busyResult = getIndexBusyResult(error);
11858
+ if (!busyResult) throw error;
11859
+ return busyResult;
11860
+ }
11861
+ }
10849
11862
  async function getPrImpact(projectRoot, host, params) {
10850
11863
  const indexer = getIndexerForProject(projectRoot, host);
10851
11864
  return indexer.getPrImpact({
@@ -11004,8 +12017,13 @@ Use Read tool to examine specific files.` }] };
11004
12017
  },
11005
12018
  async (args) => {
11006
12019
  const result = await runIndexCodebase(runtime.projectRoot, runtime.host, args);
11007
- const text = result.kind === "estimate" ? formatCostEstimate(result.estimate) : formatIndexStats(result.stats, args.verbose ?? false);
11008
- return { content: [{ type: "text", text }] };
12020
+ if (result.kind === "estimate") {
12021
+ return { content: [{ type: "text", text: formatCostEstimate(result.estimate) }] };
12022
+ }
12023
+ if (result.kind === "busy") {
12024
+ return { content: [{ type: "text", text: result.text }], isError: true };
12025
+ }
12026
+ return { content: [{ type: "text", text: formatIndexStats(result.stats, args.verbose ?? false) }] };
11009
12027
  }
11010
12028
  );
11011
12029
  server.tool(
@@ -11022,8 +12040,11 @@ Use Read tool to examine specific files.` }] };
11022
12040
  "Check index health and remove stale entries from deleted files. Run this to clean up the index after files have been deleted.",
11023
12041
  {},
11024
12042
  async () => {
11025
- const result = await getIndexHealthCheck(runtime.projectRoot, runtime.host);
11026
- return { content: [{ type: "text", text: formatHealthCheck(result) }] };
12043
+ const result = await runIndexHealthCheck(runtime.projectRoot, runtime.host);
12044
+ if (result.kind === "busy") {
12045
+ return { content: [{ type: "text", text: result.text }], isError: true };
12046
+ }
12047
+ return { content: [{ type: "text", text: formatHealthCheck(result.health) }] };
11027
12048
  }
11028
12049
  );
11029
12050
  server.tool(
@@ -11123,8 +12144,8 @@ ${formatSearchResults(results)}` }] };
11123
12144
  maxDepth: z2.number().optional().default(10).describe("Maximum traversal depth (default: 10)")
11124
12145
  },
11125
12146
  async (args) => {
11126
- const path26 = await getCallGraphPath(runtime.projectRoot, runtime.host, args.from, args.to, args.maxDepth);
11127
- return { content: [{ type: "text", text: formatCallGraphPath(args.from, args.to, path26) }] };
12147
+ const path27 = await getCallGraphPath(runtime.projectRoot, runtime.host, args.from, args.to, args.maxDepth);
12148
+ return { content: [{ type: "text", text: formatCallGraphPath(args.from, args.to, path27) }] };
11128
12149
  }
11129
12150
  );
11130
12151
  server.tool(
@@ -11159,7 +12180,7 @@ ${formatSearchResults(results)}` }] };
11159
12180
 
11160
12181
  // src/mcp-server.ts
11161
12182
  function getPackageVersion() {
11162
- const raw = JSON.parse(readFileSync10(new URL("../package.json", import.meta.url), "utf-8"));
12183
+ const raw = JSON.parse(readFileSync11(new URL("../package.json", import.meta.url), "utf-8"));
11163
12184
  if (raw && typeof raw === "object" && "version" in raw && typeof raw.version === "string") {
11164
12185
  return raw.version;
11165
12186
  }
@@ -11180,18 +12201,37 @@ function createMcpServer(projectRoot, config, host = "opencode") {
11180
12201
  }
11181
12202
 
11182
12203
  // src/utils/auto-index.ts
12204
+ var INITIAL_RETRY_DELAY_MS = 50;
12205
+ var MAX_RETRY_DELAY_MS = 500;
12206
+ var autoIndexStates = /* @__PURE__ */ new WeakMap();
11183
12207
  function getErrorMessage3(error) {
11184
12208
  return error instanceof Error ? error.message : String(error);
11185
12209
  }
11186
- function startAutoIndex(indexer, projectRoot) {
11187
- indexer.initialize().then(() => {
11188
- indexer.index().catch((error) => {
11189
- console.error(`[codebase-index] Auto-index failed for "${projectRoot}": ${getErrorMessage3(error)}`);
11190
- });
12210
+ function runAutoIndex(indexer, projectRoot, state) {
12211
+ void indexer.index().then(() => {
12212
+ autoIndexStates.delete(indexer);
11191
12213
  }).catch((error) => {
11192
- console.error(`[codebase-index] Auto-index initialization failed for "${projectRoot}": ${getErrorMessage3(error)}`);
12214
+ if (!isTransientIndexLockContention(error)) {
12215
+ autoIndexStates.delete(indexer);
12216
+ console.error(`[codebase-index] Auto-index failed for "${projectRoot}": ${getErrorMessage3(error)}`);
12217
+ return;
12218
+ }
12219
+ const retryDelayMs = state.retryDelayMs;
12220
+ state.retryDelayMs = Math.min(retryDelayMs * 2, MAX_RETRY_DELAY_MS);
12221
+ const retryTimer = setTimeout(() => {
12222
+ runAutoIndex(indexer, projectRoot, state);
12223
+ }, retryDelayMs);
12224
+ retryTimer.unref?.();
11193
12225
  });
11194
12226
  }
12227
+ function startAutoIndex(indexer, projectRoot) {
12228
+ if (autoIndexStates.has(indexer)) return;
12229
+ const state = {
12230
+ retryDelayMs: INITIAL_RETRY_DELAY_MS
12231
+ };
12232
+ autoIndexStates.set(indexer, state);
12233
+ runAutoIndex(indexer, projectRoot, state);
12234
+ }
11195
12235
 
11196
12236
  // node_modules/chokidar/index.js
11197
12237
  import { EventEmitter as EventEmitter2 } from "events";
@@ -11283,7 +12323,7 @@ var ReaddirpStream = class extends Readable {
11283
12323
  this._directoryFilter = normalizeFilter(opts.directoryFilter);
11284
12324
  const statMethod = opts.lstat ? lstat : stat;
11285
12325
  if (wantBigintFsStats) {
11286
- this._stat = (path26) => statMethod(path26, { bigint: true });
12326
+ this._stat = (path27) => statMethod(path27, { bigint: true });
11287
12327
  } else {
11288
12328
  this._stat = statMethod;
11289
12329
  }
@@ -11308,8 +12348,8 @@ var ReaddirpStream = class extends Readable {
11308
12348
  const par = this.parent;
11309
12349
  const fil = par && par.files;
11310
12350
  if (fil && fil.length > 0) {
11311
- const { path: path26, depth } = par;
11312
- const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path26));
12351
+ const { path: path27, depth } = par;
12352
+ const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path27));
11313
12353
  const awaited = await Promise.all(slice);
11314
12354
  for (const entry of awaited) {
11315
12355
  if (!entry)
@@ -11349,20 +12389,20 @@ var ReaddirpStream = class extends Readable {
11349
12389
  this.reading = false;
11350
12390
  }
11351
12391
  }
11352
- async _exploreDir(path26, depth) {
12392
+ async _exploreDir(path27, depth) {
11353
12393
  let files;
11354
12394
  try {
11355
- files = await readdir(path26, this._rdOptions);
12395
+ files = await readdir(path27, this._rdOptions);
11356
12396
  } catch (error) {
11357
12397
  this._onError(error);
11358
12398
  }
11359
- return { files, depth, path: path26 };
12399
+ return { files, depth, path: path27 };
11360
12400
  }
11361
- async _formatEntry(dirent, path26) {
12401
+ async _formatEntry(dirent, path27) {
11362
12402
  let entry;
11363
12403
  const basename5 = this._isDirent ? dirent.name : dirent;
11364
12404
  try {
11365
- const fullPath = presolve(pjoin(path26, basename5));
12405
+ const fullPath = presolve(pjoin(path27, basename5));
11366
12406
  entry = { path: prelative(this._root, fullPath), fullPath, basename: basename5 };
11367
12407
  entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
11368
12408
  } catch (err) {
@@ -11762,16 +12802,16 @@ var delFromSet = (main2, prop, item) => {
11762
12802
  };
11763
12803
  var isEmptySet = (val) => val instanceof Set ? val.size === 0 : !val;
11764
12804
  var FsWatchInstances = /* @__PURE__ */ new Map();
11765
- function createFsWatchInstance(path26, options, listener, errHandler, emitRaw) {
12805
+ function createFsWatchInstance(path27, options, listener, errHandler, emitRaw) {
11766
12806
  const handleEvent = (rawEvent, evPath) => {
11767
- listener(path26);
11768
- emitRaw(rawEvent, evPath, { watchedPath: path26 });
11769
- if (evPath && path26 !== evPath) {
11770
- fsWatchBroadcast(sp.resolve(path26, evPath), KEY_LISTENERS, sp.join(path26, evPath));
12807
+ listener(path27);
12808
+ emitRaw(rawEvent, evPath, { watchedPath: path27 });
12809
+ if (evPath && path27 !== evPath) {
12810
+ fsWatchBroadcast(sp.resolve(path27, evPath), KEY_LISTENERS, sp.join(path27, evPath));
11771
12811
  }
11772
12812
  };
11773
12813
  try {
11774
- return fs_watch(path26, {
12814
+ return fs_watch(path27, {
11775
12815
  persistent: options.persistent
11776
12816
  }, handleEvent);
11777
12817
  } catch (error) {
@@ -11787,12 +12827,12 @@ var fsWatchBroadcast = (fullPath, listenerType, val1, val2, val3) => {
11787
12827
  listener(val1, val2, val3);
11788
12828
  });
11789
12829
  };
11790
- var setFsWatchListener = (path26, fullPath, options, handlers) => {
12830
+ var setFsWatchListener = (path27, fullPath, options, handlers) => {
11791
12831
  const { listener, errHandler, rawEmitter } = handlers;
11792
12832
  let cont = FsWatchInstances.get(fullPath);
11793
12833
  let watcher;
11794
12834
  if (!options.persistent) {
11795
- watcher = createFsWatchInstance(path26, options, listener, errHandler, rawEmitter);
12835
+ watcher = createFsWatchInstance(path27, options, listener, errHandler, rawEmitter);
11796
12836
  if (!watcher)
11797
12837
  return;
11798
12838
  return watcher.close.bind(watcher);
@@ -11803,7 +12843,7 @@ var setFsWatchListener = (path26, fullPath, options, handlers) => {
11803
12843
  addAndConvert(cont, KEY_RAW, rawEmitter);
11804
12844
  } else {
11805
12845
  watcher = createFsWatchInstance(
11806
- path26,
12846
+ path27,
11807
12847
  options,
11808
12848
  fsWatchBroadcast.bind(null, fullPath, KEY_LISTENERS),
11809
12849
  errHandler,
@@ -11818,7 +12858,7 @@ var setFsWatchListener = (path26, fullPath, options, handlers) => {
11818
12858
  cont.watcherUnusable = true;
11819
12859
  if (isWindows && error.code === "EPERM") {
11820
12860
  try {
11821
- const fd = await open(path26, "r");
12861
+ const fd = await open(path27, "r");
11822
12862
  await fd.close();
11823
12863
  broadcastErr(error);
11824
12864
  } catch (err) {
@@ -11849,7 +12889,7 @@ var setFsWatchListener = (path26, fullPath, options, handlers) => {
11849
12889
  };
11850
12890
  };
11851
12891
  var FsWatchFileInstances = /* @__PURE__ */ new Map();
11852
- var setFsWatchFileListener = (path26, fullPath, options, handlers) => {
12892
+ var setFsWatchFileListener = (path27, fullPath, options, handlers) => {
11853
12893
  const { listener, rawEmitter } = handlers;
11854
12894
  let cont = FsWatchFileInstances.get(fullPath);
11855
12895
  const copts = cont && cont.options;
@@ -11871,7 +12911,7 @@ var setFsWatchFileListener = (path26, fullPath, options, handlers) => {
11871
12911
  });
11872
12912
  const currmtime = curr.mtimeMs;
11873
12913
  if (curr.size !== prev.size || currmtime > prev.mtimeMs || currmtime === 0) {
11874
- foreach(cont.listeners, (listener2) => listener2(path26, curr));
12914
+ foreach(cont.listeners, (listener2) => listener2(path27, curr));
11875
12915
  }
11876
12916
  })
11877
12917
  };
@@ -11901,13 +12941,13 @@ var NodeFsHandler = class {
11901
12941
  * @param listener on fs change
11902
12942
  * @returns closer for the watcher instance
11903
12943
  */
11904
- _watchWithNodeFs(path26, listener) {
12944
+ _watchWithNodeFs(path27, listener) {
11905
12945
  const opts = this.fsw.options;
11906
- const directory = sp.dirname(path26);
11907
- const basename5 = sp.basename(path26);
12946
+ const directory = sp.dirname(path27);
12947
+ const basename5 = sp.basename(path27);
11908
12948
  const parent = this.fsw._getWatchedDir(directory);
11909
12949
  parent.add(basename5);
11910
- const absolutePath = sp.resolve(path26);
12950
+ const absolutePath = sp.resolve(path27);
11911
12951
  const options = {
11912
12952
  persistent: opts.persistent
11913
12953
  };
@@ -11917,12 +12957,12 @@ var NodeFsHandler = class {
11917
12957
  if (opts.usePolling) {
11918
12958
  const enableBin = opts.interval !== opts.binaryInterval;
11919
12959
  options.interval = enableBin && isBinaryPath(basename5) ? opts.binaryInterval : opts.interval;
11920
- closer = setFsWatchFileListener(path26, absolutePath, options, {
12960
+ closer = setFsWatchFileListener(path27, absolutePath, options, {
11921
12961
  listener,
11922
12962
  rawEmitter: this.fsw._emitRaw
11923
12963
  });
11924
12964
  } else {
11925
- closer = setFsWatchListener(path26, absolutePath, options, {
12965
+ closer = setFsWatchListener(path27, absolutePath, options, {
11926
12966
  listener,
11927
12967
  errHandler: this._boundHandleError,
11928
12968
  rawEmitter: this.fsw._emitRaw
@@ -11944,7 +12984,7 @@ var NodeFsHandler = class {
11944
12984
  let prevStats = stats;
11945
12985
  if (parent.has(basename5))
11946
12986
  return;
11947
- const listener = async (path26, newStats) => {
12987
+ const listener = async (path27, newStats) => {
11948
12988
  if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5))
11949
12989
  return;
11950
12990
  if (!newStats || newStats.mtimeMs === 0) {
@@ -11958,11 +12998,11 @@ var NodeFsHandler = class {
11958
12998
  this.fsw._emit(EV.CHANGE, file, newStats2);
11959
12999
  }
11960
13000
  if ((isMacos || isLinux || isFreeBSD) && prevStats.ino !== newStats2.ino) {
11961
- this.fsw._closeFile(path26);
13001
+ this.fsw._closeFile(path27);
11962
13002
  prevStats = newStats2;
11963
13003
  const closer2 = this._watchWithNodeFs(file, listener);
11964
13004
  if (closer2)
11965
- this.fsw._addPathCloser(path26, closer2);
13005
+ this.fsw._addPathCloser(path27, closer2);
11966
13006
  } else {
11967
13007
  prevStats = newStats2;
11968
13008
  }
@@ -11994,7 +13034,7 @@ var NodeFsHandler = class {
11994
13034
  * @param item basename of this item
11995
13035
  * @returns true if no more processing is needed for this entry.
11996
13036
  */
11997
- async _handleSymlink(entry, directory, path26, item) {
13037
+ async _handleSymlink(entry, directory, path27, item) {
11998
13038
  if (this.fsw.closed) {
11999
13039
  return;
12000
13040
  }
@@ -12004,7 +13044,7 @@ var NodeFsHandler = class {
12004
13044
  this.fsw._incrReadyCount();
12005
13045
  let linkPath;
12006
13046
  try {
12007
- linkPath = await fsrealpath(path26);
13047
+ linkPath = await fsrealpath(path27);
12008
13048
  } catch (e) {
12009
13049
  this.fsw._emitReady();
12010
13050
  return true;
@@ -12014,12 +13054,12 @@ var NodeFsHandler = class {
12014
13054
  if (dir.has(item)) {
12015
13055
  if (this.fsw._symlinkPaths.get(full) !== linkPath) {
12016
13056
  this.fsw._symlinkPaths.set(full, linkPath);
12017
- this.fsw._emit(EV.CHANGE, path26, entry.stats);
13057
+ this.fsw._emit(EV.CHANGE, path27, entry.stats);
12018
13058
  }
12019
13059
  } else {
12020
13060
  dir.add(item);
12021
13061
  this.fsw._symlinkPaths.set(full, linkPath);
12022
- this.fsw._emit(EV.ADD, path26, entry.stats);
13062
+ this.fsw._emit(EV.ADD, path27, entry.stats);
12023
13063
  }
12024
13064
  this.fsw._emitReady();
12025
13065
  return true;
@@ -12049,9 +13089,9 @@ var NodeFsHandler = class {
12049
13089
  return;
12050
13090
  }
12051
13091
  const item = entry.path;
12052
- let path26 = sp.join(directory, item);
13092
+ let path27 = sp.join(directory, item);
12053
13093
  current.add(item);
12054
- if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path26, item)) {
13094
+ if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path27, item)) {
12055
13095
  return;
12056
13096
  }
12057
13097
  if (this.fsw.closed) {
@@ -12060,8 +13100,8 @@ var NodeFsHandler = class {
12060
13100
  }
12061
13101
  if (item === target || !target && !previous.has(item)) {
12062
13102
  this.fsw._incrReadyCount();
12063
- path26 = sp.join(dir, sp.relative(dir, path26));
12064
- this._addToNodeFs(path26, initialAdd, wh, depth + 1);
13103
+ path27 = sp.join(dir, sp.relative(dir, path27));
13104
+ this._addToNodeFs(path27, initialAdd, wh, depth + 1);
12065
13105
  }
12066
13106
  }).on(EV.ERROR, this._boundHandleError);
12067
13107
  return new Promise((resolve15, reject) => {
@@ -12130,13 +13170,13 @@ var NodeFsHandler = class {
12130
13170
  * @param depth Child path actually targeted for watch
12131
13171
  * @param target Child path actually targeted for watch
12132
13172
  */
12133
- async _addToNodeFs(path26, initialAdd, priorWh, depth, target) {
13173
+ async _addToNodeFs(path27, initialAdd, priorWh, depth, target) {
12134
13174
  const ready = this.fsw._emitReady;
12135
- if (this.fsw._isIgnored(path26) || this.fsw.closed) {
13175
+ if (this.fsw._isIgnored(path27) || this.fsw.closed) {
12136
13176
  ready();
12137
13177
  return false;
12138
13178
  }
12139
- const wh = this.fsw._getWatchHelpers(path26);
13179
+ const wh = this.fsw._getWatchHelpers(path27);
12140
13180
  if (priorWh) {
12141
13181
  wh.filterPath = (entry) => priorWh.filterPath(entry);
12142
13182
  wh.filterDir = (entry) => priorWh.filterDir(entry);
@@ -12152,8 +13192,8 @@ var NodeFsHandler = class {
12152
13192
  const follow = this.fsw.options.followSymlinks;
12153
13193
  let closer;
12154
13194
  if (stats.isDirectory()) {
12155
- const absPath = sp.resolve(path26);
12156
- const targetPath = follow ? await fsrealpath(path26) : path26;
13195
+ const absPath = sp.resolve(path27);
13196
+ const targetPath = follow ? await fsrealpath(path27) : path27;
12157
13197
  if (this.fsw.closed)
12158
13198
  return;
12159
13199
  closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath);
@@ -12163,29 +13203,29 @@ var NodeFsHandler = class {
12163
13203
  this.fsw._symlinkPaths.set(absPath, targetPath);
12164
13204
  }
12165
13205
  } else if (stats.isSymbolicLink()) {
12166
- const targetPath = follow ? await fsrealpath(path26) : path26;
13206
+ const targetPath = follow ? await fsrealpath(path27) : path27;
12167
13207
  if (this.fsw.closed)
12168
13208
  return;
12169
13209
  const parent = sp.dirname(wh.watchPath);
12170
13210
  this.fsw._getWatchedDir(parent).add(wh.watchPath);
12171
13211
  this.fsw._emit(EV.ADD, wh.watchPath, stats);
12172
- closer = await this._handleDir(parent, stats, initialAdd, depth, path26, wh, targetPath);
13212
+ closer = await this._handleDir(parent, stats, initialAdd, depth, path27, wh, targetPath);
12173
13213
  if (this.fsw.closed)
12174
13214
  return;
12175
13215
  if (targetPath !== void 0) {
12176
- this.fsw._symlinkPaths.set(sp.resolve(path26), targetPath);
13216
+ this.fsw._symlinkPaths.set(sp.resolve(path27), targetPath);
12177
13217
  }
12178
13218
  } else {
12179
13219
  closer = this._handleFile(wh.watchPath, stats, initialAdd);
12180
13220
  }
12181
13221
  ready();
12182
13222
  if (closer)
12183
- this.fsw._addPathCloser(path26, closer);
13223
+ this.fsw._addPathCloser(path27, closer);
12184
13224
  return false;
12185
13225
  } catch (error) {
12186
13226
  if (this.fsw._handleError(error)) {
12187
13227
  ready();
12188
- return path26;
13228
+ return path27;
12189
13229
  }
12190
13230
  }
12191
13231
  }
@@ -12228,24 +13268,24 @@ function createPattern(matcher) {
12228
13268
  }
12229
13269
  return () => false;
12230
13270
  }
12231
- function normalizePath2(path26) {
12232
- if (typeof path26 !== "string")
13271
+ function normalizePath2(path27) {
13272
+ if (typeof path27 !== "string")
12233
13273
  throw new Error("string expected");
12234
- path26 = sp2.normalize(path26);
12235
- path26 = path26.replace(/\\/g, "/");
13274
+ path27 = sp2.normalize(path27);
13275
+ path27 = path27.replace(/\\/g, "/");
12236
13276
  let prepend = false;
12237
- if (path26.startsWith("//"))
13277
+ if (path27.startsWith("//"))
12238
13278
  prepend = true;
12239
- path26 = path26.replace(DOUBLE_SLASH_RE, "/");
13279
+ path27 = path27.replace(DOUBLE_SLASH_RE, "/");
12240
13280
  if (prepend)
12241
- path26 = "/" + path26;
12242
- return path26;
13281
+ path27 = "/" + path27;
13282
+ return path27;
12243
13283
  }
12244
13284
  function matchPatterns(patterns, testString, stats) {
12245
- const path26 = normalizePath2(testString);
13285
+ const path27 = normalizePath2(testString);
12246
13286
  for (let index = 0; index < patterns.length; index++) {
12247
13287
  const pattern = patterns[index];
12248
- if (pattern(path26, stats)) {
13288
+ if (pattern(path27, stats)) {
12249
13289
  return true;
12250
13290
  }
12251
13291
  }
@@ -12283,19 +13323,19 @@ var toUnix = (string) => {
12283
13323
  }
12284
13324
  return str;
12285
13325
  };
12286
- var normalizePathToUnix = (path26) => toUnix(sp2.normalize(toUnix(path26)));
12287
- var normalizeIgnored = (cwd = "") => (path26) => {
12288
- if (typeof path26 === "string") {
12289
- return normalizePathToUnix(sp2.isAbsolute(path26) ? path26 : sp2.join(cwd, path26));
13326
+ var normalizePathToUnix = (path27) => toUnix(sp2.normalize(toUnix(path27)));
13327
+ var normalizeIgnored = (cwd = "") => (path27) => {
13328
+ if (typeof path27 === "string") {
13329
+ return normalizePathToUnix(sp2.isAbsolute(path27) ? path27 : sp2.join(cwd, path27));
12290
13330
  } else {
12291
- return path26;
13331
+ return path27;
12292
13332
  }
12293
13333
  };
12294
- var getAbsolutePath = (path26, cwd) => {
12295
- if (sp2.isAbsolute(path26)) {
12296
- return path26;
13334
+ var getAbsolutePath = (path27, cwd) => {
13335
+ if (sp2.isAbsolute(path27)) {
13336
+ return path27;
12297
13337
  }
12298
- return sp2.join(cwd, path26);
13338
+ return sp2.join(cwd, path27);
12299
13339
  };
12300
13340
  var EMPTY_SET = Object.freeze(/* @__PURE__ */ new Set());
12301
13341
  var DirEntry = class {
@@ -12360,10 +13400,10 @@ var WatchHelper = class {
12360
13400
  dirParts;
12361
13401
  followSymlinks;
12362
13402
  statMethod;
12363
- constructor(path26, follow, fsw) {
13403
+ constructor(path27, follow, fsw) {
12364
13404
  this.fsw = fsw;
12365
- const watchPath = path26;
12366
- this.path = path26 = path26.replace(REPLACER_RE, "");
13405
+ const watchPath = path27;
13406
+ this.path = path27 = path27.replace(REPLACER_RE, "");
12367
13407
  this.watchPath = watchPath;
12368
13408
  this.fullWatchPath = sp2.resolve(watchPath);
12369
13409
  this.dirParts = [];
@@ -12503,20 +13543,20 @@ var FSWatcher = class extends EventEmitter2 {
12503
13543
  this._closePromise = void 0;
12504
13544
  let paths = unifyPaths(paths_);
12505
13545
  if (cwd) {
12506
- paths = paths.map((path26) => {
12507
- const absPath = getAbsolutePath(path26, cwd);
13546
+ paths = paths.map((path27) => {
13547
+ const absPath = getAbsolutePath(path27, cwd);
12508
13548
  return absPath;
12509
13549
  });
12510
13550
  }
12511
- paths.forEach((path26) => {
12512
- this._removeIgnoredPath(path26);
13551
+ paths.forEach((path27) => {
13552
+ this._removeIgnoredPath(path27);
12513
13553
  });
12514
13554
  this._userIgnored = void 0;
12515
13555
  if (!this._readyCount)
12516
13556
  this._readyCount = 0;
12517
13557
  this._readyCount += paths.length;
12518
- Promise.all(paths.map(async (path26) => {
12519
- const res = await this._nodeFsHandler._addToNodeFs(path26, !_internal, void 0, 0, _origAdd);
13558
+ Promise.all(paths.map(async (path27) => {
13559
+ const res = await this._nodeFsHandler._addToNodeFs(path27, !_internal, void 0, 0, _origAdd);
12520
13560
  if (res)
12521
13561
  this._emitReady();
12522
13562
  return res;
@@ -12538,17 +13578,17 @@ var FSWatcher = class extends EventEmitter2 {
12538
13578
  return this;
12539
13579
  const paths = unifyPaths(paths_);
12540
13580
  const { cwd } = this.options;
12541
- paths.forEach((path26) => {
12542
- if (!sp2.isAbsolute(path26) && !this._closers.has(path26)) {
13581
+ paths.forEach((path27) => {
13582
+ if (!sp2.isAbsolute(path27) && !this._closers.has(path27)) {
12543
13583
  if (cwd)
12544
- path26 = sp2.join(cwd, path26);
12545
- path26 = sp2.resolve(path26);
13584
+ path27 = sp2.join(cwd, path27);
13585
+ path27 = sp2.resolve(path27);
12546
13586
  }
12547
- this._closePath(path26);
12548
- this._addIgnoredPath(path26);
12549
- if (this._watched.has(path26)) {
13587
+ this._closePath(path27);
13588
+ this._addIgnoredPath(path27);
13589
+ if (this._watched.has(path27)) {
12550
13590
  this._addIgnoredPath({
12551
- path: path26,
13591
+ path: path27,
12552
13592
  recursive: true
12553
13593
  });
12554
13594
  }
@@ -12612,38 +13652,38 @@ var FSWatcher = class extends EventEmitter2 {
12612
13652
  * @param stats arguments to be passed with event
12613
13653
  * @returns the error if defined, otherwise the value of the FSWatcher instance's `closed` flag
12614
13654
  */
12615
- async _emit(event, path26, stats) {
13655
+ async _emit(event, path27, stats) {
12616
13656
  if (this.closed)
12617
13657
  return;
12618
13658
  const opts = this.options;
12619
13659
  if (isWindows)
12620
- path26 = sp2.normalize(path26);
13660
+ path27 = sp2.normalize(path27);
12621
13661
  if (opts.cwd)
12622
- path26 = sp2.relative(opts.cwd, path26);
12623
- const args = [path26];
13662
+ path27 = sp2.relative(opts.cwd, path27);
13663
+ const args = [path27];
12624
13664
  if (stats != null)
12625
13665
  args.push(stats);
12626
13666
  const awf = opts.awaitWriteFinish;
12627
13667
  let pw;
12628
- if (awf && (pw = this._pendingWrites.get(path26))) {
13668
+ if (awf && (pw = this._pendingWrites.get(path27))) {
12629
13669
  pw.lastChange = /* @__PURE__ */ new Date();
12630
13670
  return this;
12631
13671
  }
12632
13672
  if (opts.atomic) {
12633
13673
  if (event === EVENTS.UNLINK) {
12634
- this._pendingUnlinks.set(path26, [event, ...args]);
13674
+ this._pendingUnlinks.set(path27, [event, ...args]);
12635
13675
  setTimeout(() => {
12636
- this._pendingUnlinks.forEach((entry, path27) => {
13676
+ this._pendingUnlinks.forEach((entry, path28) => {
12637
13677
  this.emit(...entry);
12638
13678
  this.emit(EVENTS.ALL, ...entry);
12639
- this._pendingUnlinks.delete(path27);
13679
+ this._pendingUnlinks.delete(path28);
12640
13680
  });
12641
13681
  }, typeof opts.atomic === "number" ? opts.atomic : 100);
12642
13682
  return this;
12643
13683
  }
12644
- if (event === EVENTS.ADD && this._pendingUnlinks.has(path26)) {
13684
+ if (event === EVENTS.ADD && this._pendingUnlinks.has(path27)) {
12645
13685
  event = EVENTS.CHANGE;
12646
- this._pendingUnlinks.delete(path26);
13686
+ this._pendingUnlinks.delete(path27);
12647
13687
  }
12648
13688
  }
12649
13689
  if (awf && (event === EVENTS.ADD || event === EVENTS.CHANGE) && this._readyEmitted) {
@@ -12661,16 +13701,16 @@ var FSWatcher = class extends EventEmitter2 {
12661
13701
  this.emitWithAll(event, args);
12662
13702
  }
12663
13703
  };
12664
- this._awaitWriteFinish(path26, awf.stabilityThreshold, event, awfEmit);
13704
+ this._awaitWriteFinish(path27, awf.stabilityThreshold, event, awfEmit);
12665
13705
  return this;
12666
13706
  }
12667
13707
  if (event === EVENTS.CHANGE) {
12668
- const isThrottled = !this._throttle(EVENTS.CHANGE, path26, 50);
13708
+ const isThrottled = !this._throttle(EVENTS.CHANGE, path27, 50);
12669
13709
  if (isThrottled)
12670
13710
  return this;
12671
13711
  }
12672
13712
  if (opts.alwaysStat && stats === void 0 && (event === EVENTS.ADD || event === EVENTS.ADD_DIR || event === EVENTS.CHANGE)) {
12673
- const fullPath = opts.cwd ? sp2.join(opts.cwd, path26) : path26;
13713
+ const fullPath = opts.cwd ? sp2.join(opts.cwd, path27) : path27;
12674
13714
  let stats2;
12675
13715
  try {
12676
13716
  stats2 = await stat3(fullPath);
@@ -12701,23 +13741,23 @@ var FSWatcher = class extends EventEmitter2 {
12701
13741
  * @param timeout duration of time to suppress duplicate actions
12702
13742
  * @returns tracking object or false if action should be suppressed
12703
13743
  */
12704
- _throttle(actionType, path26, timeout) {
13744
+ _throttle(actionType, path27, timeout) {
12705
13745
  if (!this._throttled.has(actionType)) {
12706
13746
  this._throttled.set(actionType, /* @__PURE__ */ new Map());
12707
13747
  }
12708
13748
  const action = this._throttled.get(actionType);
12709
13749
  if (!action)
12710
13750
  throw new Error("invalid throttle");
12711
- const actionPath = action.get(path26);
13751
+ const actionPath = action.get(path27);
12712
13752
  if (actionPath) {
12713
13753
  actionPath.count++;
12714
13754
  return false;
12715
13755
  }
12716
13756
  let timeoutObject;
12717
13757
  const clear = () => {
12718
- const item = action.get(path26);
13758
+ const item = action.get(path27);
12719
13759
  const count = item ? item.count : 0;
12720
- action.delete(path26);
13760
+ action.delete(path27);
12721
13761
  clearTimeout(timeoutObject);
12722
13762
  if (item)
12723
13763
  clearTimeout(item.timeoutObject);
@@ -12725,7 +13765,7 @@ var FSWatcher = class extends EventEmitter2 {
12725
13765
  };
12726
13766
  timeoutObject = setTimeout(clear, timeout);
12727
13767
  const thr = { timeoutObject, clear, count: 0 };
12728
- action.set(path26, thr);
13768
+ action.set(path27, thr);
12729
13769
  return thr;
12730
13770
  }
12731
13771
  _incrReadyCount() {
@@ -12739,44 +13779,44 @@ var FSWatcher = class extends EventEmitter2 {
12739
13779
  * @param event
12740
13780
  * @param awfEmit Callback to be called when ready for event to be emitted.
12741
13781
  */
12742
- _awaitWriteFinish(path26, threshold, event, awfEmit) {
13782
+ _awaitWriteFinish(path27, threshold, event, awfEmit) {
12743
13783
  const awf = this.options.awaitWriteFinish;
12744
13784
  if (typeof awf !== "object")
12745
13785
  return;
12746
13786
  const pollInterval = awf.pollInterval;
12747
13787
  let timeoutHandler;
12748
- let fullPath = path26;
12749
- if (this.options.cwd && !sp2.isAbsolute(path26)) {
12750
- fullPath = sp2.join(this.options.cwd, path26);
13788
+ let fullPath = path27;
13789
+ if (this.options.cwd && !sp2.isAbsolute(path27)) {
13790
+ fullPath = sp2.join(this.options.cwd, path27);
12751
13791
  }
12752
13792
  const now = /* @__PURE__ */ new Date();
12753
13793
  const writes = this._pendingWrites;
12754
13794
  function awaitWriteFinishFn(prevStat) {
12755
13795
  statcb(fullPath, (err, curStat) => {
12756
- if (err || !writes.has(path26)) {
13796
+ if (err || !writes.has(path27)) {
12757
13797
  if (err && err.code !== "ENOENT")
12758
13798
  awfEmit(err);
12759
13799
  return;
12760
13800
  }
12761
13801
  const now2 = Number(/* @__PURE__ */ new Date());
12762
13802
  if (prevStat && curStat.size !== prevStat.size) {
12763
- writes.get(path26).lastChange = now2;
13803
+ writes.get(path27).lastChange = now2;
12764
13804
  }
12765
- const pw = writes.get(path26);
13805
+ const pw = writes.get(path27);
12766
13806
  const df = now2 - pw.lastChange;
12767
13807
  if (df >= threshold) {
12768
- writes.delete(path26);
13808
+ writes.delete(path27);
12769
13809
  awfEmit(void 0, curStat);
12770
13810
  } else {
12771
13811
  timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval, curStat);
12772
13812
  }
12773
13813
  });
12774
13814
  }
12775
- if (!writes.has(path26)) {
12776
- writes.set(path26, {
13815
+ if (!writes.has(path27)) {
13816
+ writes.set(path27, {
12777
13817
  lastChange: now,
12778
13818
  cancelWait: () => {
12779
- writes.delete(path26);
13819
+ writes.delete(path27);
12780
13820
  clearTimeout(timeoutHandler);
12781
13821
  return event;
12782
13822
  }
@@ -12787,8 +13827,8 @@ var FSWatcher = class extends EventEmitter2 {
12787
13827
  /**
12788
13828
  * Determines whether user has asked to ignore this path.
12789
13829
  */
12790
- _isIgnored(path26, stats) {
12791
- if (this.options.atomic && DOT_RE.test(path26))
13830
+ _isIgnored(path27, stats) {
13831
+ if (this.options.atomic && DOT_RE.test(path27))
12792
13832
  return true;
12793
13833
  if (!this._userIgnored) {
12794
13834
  const { cwd } = this.options;
@@ -12798,17 +13838,17 @@ var FSWatcher = class extends EventEmitter2 {
12798
13838
  const list = [...ignoredPaths.map(normalizeIgnored(cwd)), ...ignored];
12799
13839
  this._userIgnored = anymatch(list, void 0);
12800
13840
  }
12801
- return this._userIgnored(path26, stats);
13841
+ return this._userIgnored(path27, stats);
12802
13842
  }
12803
- _isntIgnored(path26, stat4) {
12804
- return !this._isIgnored(path26, stat4);
13843
+ _isntIgnored(path27, stat4) {
13844
+ return !this._isIgnored(path27, stat4);
12805
13845
  }
12806
13846
  /**
12807
13847
  * Provides a set of common helpers and properties relating to symlink handling.
12808
13848
  * @param path file or directory pattern being watched
12809
13849
  */
12810
- _getWatchHelpers(path26) {
12811
- return new WatchHelper(path26, this.options.followSymlinks, this);
13850
+ _getWatchHelpers(path27) {
13851
+ return new WatchHelper(path27, this.options.followSymlinks, this);
12812
13852
  }
12813
13853
  // Directory helpers
12814
13854
  // -----------------
@@ -12840,63 +13880,63 @@ var FSWatcher = class extends EventEmitter2 {
12840
13880
  * @param item base path of item/directory
12841
13881
  */
12842
13882
  _remove(directory, item, isDirectory) {
12843
- const path26 = sp2.join(directory, item);
12844
- const fullPath = sp2.resolve(path26);
12845
- isDirectory = isDirectory != null ? isDirectory : this._watched.has(path26) || this._watched.has(fullPath);
12846
- if (!this._throttle("remove", path26, 100))
13883
+ const path27 = sp2.join(directory, item);
13884
+ const fullPath = sp2.resolve(path27);
13885
+ isDirectory = isDirectory != null ? isDirectory : this._watched.has(path27) || this._watched.has(fullPath);
13886
+ if (!this._throttle("remove", path27, 100))
12847
13887
  return;
12848
13888
  if (!isDirectory && this._watched.size === 1) {
12849
13889
  this.add(directory, item, true);
12850
13890
  }
12851
- const wp = this._getWatchedDir(path26);
13891
+ const wp = this._getWatchedDir(path27);
12852
13892
  const nestedDirectoryChildren = wp.getChildren();
12853
- nestedDirectoryChildren.forEach((nested) => this._remove(path26, nested));
13893
+ nestedDirectoryChildren.forEach((nested) => this._remove(path27, nested));
12854
13894
  const parent = this._getWatchedDir(directory);
12855
13895
  const wasTracked = parent.has(item);
12856
13896
  parent.remove(item);
12857
13897
  if (this._symlinkPaths.has(fullPath)) {
12858
13898
  this._symlinkPaths.delete(fullPath);
12859
13899
  }
12860
- let relPath = path26;
13900
+ let relPath = path27;
12861
13901
  if (this.options.cwd)
12862
- relPath = sp2.relative(this.options.cwd, path26);
13902
+ relPath = sp2.relative(this.options.cwd, path27);
12863
13903
  if (this.options.awaitWriteFinish && this._pendingWrites.has(relPath)) {
12864
13904
  const event = this._pendingWrites.get(relPath).cancelWait();
12865
13905
  if (event === EVENTS.ADD)
12866
13906
  return;
12867
13907
  }
12868
- this._watched.delete(path26);
13908
+ this._watched.delete(path27);
12869
13909
  this._watched.delete(fullPath);
12870
13910
  const eventName = isDirectory ? EVENTS.UNLINK_DIR : EVENTS.UNLINK;
12871
- if (wasTracked && !this._isIgnored(path26))
12872
- this._emit(eventName, path26);
12873
- this._closePath(path26);
13911
+ if (wasTracked && !this._isIgnored(path27))
13912
+ this._emit(eventName, path27);
13913
+ this._closePath(path27);
12874
13914
  }
12875
13915
  /**
12876
13916
  * Closes all watchers for a path
12877
13917
  */
12878
- _closePath(path26) {
12879
- this._closeFile(path26);
12880
- const dir = sp2.dirname(path26);
12881
- this._getWatchedDir(dir).remove(sp2.basename(path26));
13918
+ _closePath(path27) {
13919
+ this._closeFile(path27);
13920
+ const dir = sp2.dirname(path27);
13921
+ this._getWatchedDir(dir).remove(sp2.basename(path27));
12882
13922
  }
12883
13923
  /**
12884
13924
  * Closes only file-specific watchers
12885
13925
  */
12886
- _closeFile(path26) {
12887
- const closers = this._closers.get(path26);
13926
+ _closeFile(path27) {
13927
+ const closers = this._closers.get(path27);
12888
13928
  if (!closers)
12889
13929
  return;
12890
13930
  closers.forEach((closer) => closer());
12891
- this._closers.delete(path26);
13931
+ this._closers.delete(path27);
12892
13932
  }
12893
- _addPathCloser(path26, closer) {
13933
+ _addPathCloser(path27, closer) {
12894
13934
  if (!closer)
12895
13935
  return;
12896
- let list = this._closers.get(path26);
13936
+ let list = this._closers.get(path27);
12897
13937
  if (!list) {
12898
13938
  list = [];
12899
- this._closers.set(path26, list);
13939
+ this._closers.set(path27, list);
12900
13940
  }
12901
13941
  list.push(closer);
12902
13942
  }
@@ -12926,7 +13966,7 @@ function watch(paths, options = {}) {
12926
13966
  var chokidar_default = { watch, FSWatcher };
12927
13967
 
12928
13968
  // src/watcher/file-watcher.ts
12929
- import * as path21 from "path";
13969
+ import * as path22 from "path";
12930
13970
  var FileWatcher = class {
12931
13971
  watcher = null;
12932
13972
  projectRoot;
@@ -12937,6 +13977,8 @@ var FileWatcher = class {
12937
13977
  debounceTimer = null;
12938
13978
  debounceMs = 1e3;
12939
13979
  onChanges = null;
13980
+ readyPromise = null;
13981
+ resolveReady = null;
12940
13982
  constructor(projectRoot, config, host = "opencode", options = {}) {
12941
13983
  this.projectRoot = projectRoot;
12942
13984
  this.config = config;
@@ -12952,15 +13994,15 @@ var FileWatcher = class {
12952
13994
  const watchTargets = this.configPath ? [this.projectRoot, this.configPath] : this.projectRoot;
12953
13995
  this.watcher = chokidar_default.watch(watchTargets, {
12954
13996
  ignored: (filePath) => {
12955
- const relativePath = path21.relative(this.projectRoot, filePath);
13997
+ const relativePath = path22.relative(this.projectRoot, filePath);
12956
13998
  if (!relativePath) return false;
12957
13999
  if (this.isProjectConfigPathOrAncestor(relativePath)) {
12958
14000
  return false;
12959
14001
  }
12960
- if (hasFilteredPathSegment(relativePath, path21.sep)) {
14002
+ if (hasFilteredPathSegment(relativePath, path22.sep)) {
12961
14003
  return true;
12962
14004
  }
12963
- if (isRestrictedDirectory(relativePath, path21.sep)) {
14005
+ if (isRestrictedDirectory(relativePath, path22.sep)) {
12964
14006
  return true;
12965
14007
  }
12966
14008
  if (ignoreFilter.ignores(relativePath)) {
@@ -12975,6 +14017,13 @@ var FileWatcher = class {
12975
14017
  pollInterval: 100
12976
14018
  }
12977
14019
  });
14020
+ this.readyPromise = new Promise((resolve15) => {
14021
+ this.resolveReady = resolve15;
14022
+ });
14023
+ this.watcher.once("ready", () => {
14024
+ this.resolveReady?.();
14025
+ this.resolveReady = null;
14026
+ });
12978
14027
  this.watcher.on("error", (error) => {
12979
14028
  const err = error instanceof Error ? error : null;
12980
14029
  if (err?.code === "EPERM" || err?.code === "EACCES") {
@@ -13006,24 +14055,24 @@ var FileWatcher = class {
13006
14055
  this.scheduleFlush();
13007
14056
  }
13008
14057
  isProjectConfigPath(filePath) {
13009
- const relativePath = path21.relative(this.projectRoot, filePath);
13010
- const normalizedRelativePath = path21.normalize(relativePath);
14058
+ const relativePath = path22.relative(this.projectRoot, filePath);
14059
+ const normalizedRelativePath = path22.normalize(relativePath);
13011
14060
  return this.getProjectConfigRelativePaths().some((configPath) => configPath === normalizedRelativePath);
13012
14061
  }
13013
14062
  isProjectConfigPathOrAncestor(relativePath) {
13014
- const normalizedRelativePath = path21.normalize(relativePath);
14063
+ const normalizedRelativePath = path22.normalize(relativePath);
13015
14064
  return this.getProjectConfigRelativePaths().some(
13016
- (configPath) => configPath === normalizedRelativePath || configPath.startsWith(`${normalizedRelativePath}${path21.sep}`)
14065
+ (configPath) => configPath === normalizedRelativePath || configPath.startsWith(`${normalizedRelativePath}${path22.sep}`)
13017
14066
  );
13018
14067
  }
13019
14068
  getProjectConfigRelativePaths() {
13020
14069
  if (this.configPath) {
13021
- return [path21.normalize(path21.relative(this.projectRoot, this.configPath))];
14070
+ return [path22.normalize(path22.relative(this.projectRoot, this.configPath))];
13022
14071
  }
13023
14072
  return [
13024
14073
  resolveProjectConfigPath(this.projectRoot, this.host),
13025
14074
  resolveWritableProjectConfigPath(this.projectRoot, this.host)
13026
- ].map((configPath) => path21.normalize(path21.relative(this.projectRoot, configPath)));
14075
+ ].map((configPath) => path22.normalize(path22.relative(this.projectRoot, configPath)));
13027
14076
  }
13028
14077
  scheduleFlush() {
13029
14078
  if (this.debounceTimer) {
@@ -13038,7 +14087,7 @@ var FileWatcher = class {
13038
14087
  return;
13039
14088
  }
13040
14089
  const changes = Array.from(this.pendingChanges.entries()).map(
13041
- ([path26, type]) => ({ path: path26, type })
14090
+ ([path27, type]) => ({ path: path27, type })
13042
14091
  );
13043
14092
  this.pendingChanges.clear();
13044
14093
  try {
@@ -13047,25 +14096,32 @@ var FileWatcher = class {
13047
14096
  console.error("Error handling file changes:", error);
13048
14097
  }
13049
14098
  }
13050
- stop() {
14099
+ async stop() {
13051
14100
  if (this.debounceTimer) {
13052
14101
  clearTimeout(this.debounceTimer);
13053
14102
  this.debounceTimer = null;
13054
14103
  }
13055
14104
  if (this.watcher) {
13056
- this.watcher.close();
14105
+ const watcher = this.watcher;
13057
14106
  this.watcher = null;
14107
+ await watcher.close();
13058
14108
  }
14109
+ this.resolveReady?.();
14110
+ this.resolveReady = null;
14111
+ this.readyPromise = null;
13059
14112
  this.pendingChanges.clear();
13060
14113
  this.onChanges = null;
13061
14114
  }
13062
14115
  isRunning() {
13063
14116
  return this.watcher !== null;
13064
14117
  }
14118
+ async waitUntilReady() {
14119
+ await (this.readyPromise ?? Promise.resolve());
14120
+ }
13065
14121
  };
13066
14122
 
13067
14123
  // src/watcher/git-head-watcher.ts
13068
- import * as path22 from "path";
14124
+ import * as path23 from "path";
13069
14125
  var GitHeadWatcher = class {
13070
14126
  watcher = null;
13071
14127
  projectRoot;
@@ -13087,7 +14143,7 @@ var GitHeadWatcher = class {
13087
14143
  this.onBranchChange = handler;
13088
14144
  this.currentBranch = getCurrentBranch(this.projectRoot);
13089
14145
  const headPath = getHeadPath(this.projectRoot);
13090
- const refsPath = path22.join(this.projectRoot, ".git", "refs", "heads");
14146
+ const refsPath = path23.join(this.projectRoot, ".git", "refs", "heads");
13091
14147
  this.watcher = chokidar_default.watch([headPath, refsPath], {
13092
14148
  persistent: true,
13093
14149
  ignoreInitial: true,
@@ -13124,14 +14180,15 @@ var GitHeadWatcher = class {
13124
14180
  getCurrentBranch() {
13125
14181
  return this.currentBranch;
13126
14182
  }
13127
- stop() {
14183
+ async stop() {
13128
14184
  if (this.debounceTimer) {
13129
14185
  clearTimeout(this.debounceTimer);
13130
14186
  this.debounceTimer = null;
13131
14187
  }
13132
14188
  if (this.watcher) {
13133
- this.watcher.close();
14189
+ const watcher = this.watcher;
13134
14190
  this.watcher = null;
14191
+ await watcher.close();
13135
14192
  }
13136
14193
  this.onBranchChange = null;
13137
14194
  }
@@ -13149,6 +14206,8 @@ var BackgroundReindexer = class {
13149
14206
  running = false;
13150
14207
  pending = false;
13151
14208
  stopped = false;
14209
+ retryTimer = null;
14210
+ retryDelayMs = 50;
13152
14211
  request() {
13153
14212
  if (this.stopped) {
13154
14213
  return;
@@ -13159,9 +14218,13 @@ var BackgroundReindexer = class {
13159
14218
  stop() {
13160
14219
  this.stopped = true;
13161
14220
  this.pending = false;
14221
+ if (this.retryTimer) {
14222
+ clearTimeout(this.retryTimer);
14223
+ this.retryTimer = null;
14224
+ }
13162
14225
  }
13163
14226
  drain() {
13164
- if (this.stopped || this.running || !this.pending) {
14227
+ if (this.stopped || this.running || this.retryTimer || !this.pending) {
13165
14228
  return;
13166
14229
  }
13167
14230
  this.pending = false;
@@ -13169,13 +14232,29 @@ var BackgroundReindexer = class {
13169
14232
  void this.run();
13170
14233
  }
13171
14234
  async run() {
14235
+ let retryAfterContention = false;
13172
14236
  try {
13173
14237
  await this.runIndex();
14238
+ this.retryDelayMs = 50;
13174
14239
  } catch (error) {
13175
- console.error("[codebase-index] Background reindex failed:", error);
14240
+ if (isTransientIndexLockContention(error)) {
14241
+ this.pending = true;
14242
+ retryAfterContention = true;
14243
+ } else {
14244
+ console.error("[codebase-index] Background reindex failed:", error);
14245
+ }
13176
14246
  } finally {
13177
14247
  this.running = false;
13178
- this.drain();
14248
+ if (retryAfterContention && !this.stopped) {
14249
+ const delay = this.retryDelayMs;
14250
+ this.retryDelayMs = Math.min(this.retryDelayMs * 2, 500);
14251
+ this.retryTimer = setTimeout(() => {
14252
+ this.retryTimer = null;
14253
+ this.drain();
14254
+ }, delay);
14255
+ } else {
14256
+ this.drain();
14257
+ }
13179
14258
  }
13180
14259
  }
13181
14260
  };
@@ -13209,10 +14288,12 @@ function createWatcherWithIndexer(getIndexer, projectRoot, config, host = "openc
13209
14288
  return {
13210
14289
  fileWatcher,
13211
14290
  gitWatcher,
13212
- stop() {
14291
+ whenReady() {
14292
+ return fileWatcher.waitUntilReady();
14293
+ },
14294
+ async stop() {
13213
14295
  backgroundReindexer.stop();
13214
- fileWatcher.stop();
13215
- gitWatcher?.stop();
14296
+ await Promise.all([fileWatcher.stop(), gitWatcher?.stop()]);
13216
14297
  }
13217
14298
  };
13218
14299
  }
@@ -13231,7 +14312,7 @@ function getConfigPaths(projectRoot, host, options) {
13231
14312
 
13232
14313
  // src/tools/visualize/activity.ts
13233
14314
  import { execFileSync } from "child_process";
13234
- import * as path23 from "path";
14315
+ import * as path24 from "path";
13235
14316
  function attachRecentActivity(data, projectRoot) {
13236
14317
  const activity = readGitActivity(projectRoot);
13237
14318
  const changes = activity.size > 0 ? buildGitChanges(data, activity, projectRoot) : buildGraphChanges(data);
@@ -13393,7 +14474,7 @@ function normalizePath3(filePath) {
13393
14474
  return filePath.replace(/\\/g, "/");
13394
14475
  }
13395
14476
  function toGitRelativePath(projectRoot, filePath) {
13396
- const relativePath = path23.isAbsolute(filePath) ? path23.relative(projectRoot, filePath) : filePath;
14477
+ const relativePath = path24.isAbsolute(filePath) ? path24.relative(projectRoot, filePath) : filePath;
13397
14478
  return normalizePath3(relativePath);
13398
14479
  }
13399
14480
 
@@ -13651,7 +14732,7 @@ render();
13651
14732
  }
13652
14733
 
13653
14734
  // src/tools/visualize/transform.ts
13654
- import * as path24 from "path";
14735
+ import * as path25 from "path";
13655
14736
 
13656
14737
  // src/tools/visualize/modules.ts
13657
14738
  var MAX_MODULES = 18;
@@ -13911,7 +14992,7 @@ function transformForVisualization(symbols, edges, options = {}) {
13911
14992
  filePath: s.filePath,
13912
14993
  kind: s.kind,
13913
14994
  line: s.startLine,
13914
- directory: path24.dirname(s.filePath),
14995
+ directory: path25.dirname(s.filePath),
13915
14996
  moduleId: "",
13916
14997
  moduleLabel: ""
13917
14998
  }));
@@ -13939,9 +15020,9 @@ function parseArgs(argv) {
13939
15020
  let host = "opencode";
13940
15021
  for (let i = 2; i < argv.length; i++) {
13941
15022
  if (argv[i] === "--project" && argv[i + 1]) {
13942
- project = path25.resolve(argv[++i]);
15023
+ project = path26.resolve(argv[++i]);
13943
15024
  } else if (argv[i] === "--config" && argv[i + 1]) {
13944
- config = path25.resolve(argv[++i]);
15025
+ config = path26.resolve(argv[++i]);
13945
15026
  } else if (argv[i] === "--host" && argv[i + 1]) {
13946
15027
  host = parseHostMode(argv[++i]);
13947
15028
  } else if (argv[i] === "--host") {
@@ -13954,7 +15035,7 @@ function loadCliRawConfig(args) {
13954
15035
  return args.config ? loadConfigFile(args.config) : loadMergedConfig(args.project, args.host);
13955
15036
  }
13956
15037
  function isCliEntrypoint(moduleUrl, argvPath) {
13957
- return argvPath !== void 0 && realpathSync2(fileURLToPath2(moduleUrl)) === realpathSync2(argvPath);
15038
+ return argvPath !== void 0 && realpathSync3(fileURLToPath2(moduleUrl)) === realpathSync3(argvPath);
13958
15039
  }
13959
15040
  function parseVisualizeArgs(argv, cwd) {
13960
15041
  let project = cwd;
@@ -13964,7 +15045,7 @@ function parseVisualizeArgs(argv, cwd) {
13964
15045
  for (let i = 0; i < argv.length; i++) {
13965
15046
  const arg = argv[i];
13966
15047
  if (arg === "--project" && argv[i + 1]) {
13967
- project = path25.resolve(argv[++i]);
15048
+ project = path26.resolve(argv[++i]);
13968
15049
  } else if (arg === "--max" && argv[i + 1]) {
13969
15050
  maxNodes = Number(argv[++i]);
13970
15051
  } else if (arg.startsWith("--max=") || arg.startsWith("max=")) {
@@ -14001,8 +15082,8 @@ async function handleVisualizeCommand(argv, cwd) {
14001
15082
  console.error("No connected symbols found. Retry with: npm run visualize -- orphans");
14002
15083
  return 1;
14003
15084
  }
14004
- const outputPath = path25.join(os5.tmpdir(), `call-graph-${Date.now()}.html`);
14005
- writeFileSync6(outputPath, generateVisualizationHtml(vizData), "utf-8");
15085
+ const outputPath = path26.join(os6.tmpdir(), `call-graph-${Date.now()}.html`);
15086
+ writeFileSync7(outputPath, generateVisualizationHtml(vizData), "utf-8");
14006
15087
  console.log(`Temporal call graph visualization generated: ${outputPath}`);
14007
15088
  console.log(`Nodes: ${vizData.nodes.length} | Edges: ${vizData.edges.length}`);
14008
15089
  console.log(`Recent change lenses: ${vizData.changes?.length ?? 0}`);
@@ -14031,7 +15112,7 @@ async function main() {
14031
15112
  const transport = new StdioServerTransport();
14032
15113
  await server.connect(transport);
14033
15114
  let watcher = null;
14034
- const isHomeDir = path25.resolve(args.project) === path25.resolve(os5.homedir());
15115
+ const isHomeDir = path26.resolve(args.project) === path26.resolve(os6.homedir());
14035
15116
  const isValidProject = !isHomeDir && (!config.indexing.requireProjectMarker || hasProjectMarker(args.project));
14036
15117
  if (config.indexing.autoIndex && isValidProject) {
14037
15118
  const indexer = getIndexerForProject(args.project, args.host);
@@ -14046,14 +15127,25 @@ async function main() {
14046
15127
  args.config ? { configPath: args.config } : {}
14047
15128
  );
14048
15129
  }
14049
- const shutdown = () => {
14050
- watcher?.stop();
14051
- server.close().catch(() => {
14052
- });
14053
- process.exit(0);
15130
+ let shuttingDown = false;
15131
+ const shutdown = async () => {
15132
+ if (shuttingDown) return;
15133
+ shuttingDown = true;
15134
+ try {
15135
+ await watcher?.stop();
15136
+ await server.close();
15137
+ process.exit(0);
15138
+ } catch (error) {
15139
+ console.error("Failed to stop MCP server cleanly:", error);
15140
+ process.exit(1);
15141
+ }
14054
15142
  };
14055
- process.on("SIGINT", shutdown);
14056
- process.on("SIGTERM", shutdown);
15143
+ process.on("SIGINT", () => {
15144
+ void shutdown();
15145
+ });
15146
+ process.on("SIGTERM", () => {
15147
+ void shutdown();
15148
+ });
14057
15149
  }
14058
15150
  function handleMainError(error) {
14059
15151
  if (error instanceof Error && error.message.startsWith("Invalid host mode")) {