opencode-codebase-index 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,
@@ -8080,13 +8995,22 @@ var Indexer = class {
8080
8995
  });
8081
8996
  const embeddingStartTime = performance2.now();
8082
8997
  const embeddingQuery = stripFilePathHint(query);
8083
- const embedding = await this.getQueryEmbedding(embeddingQuery, provider);
8998
+ let embedding;
8999
+ try {
9000
+ embedding = await this.getQueryEmbedding(embeddingQuery, provider);
9001
+ } catch (error) {
9002
+ this.logger.warn("Query embedding failed; falling back to keyword-only search", {
9003
+ query,
9004
+ error: getErrorMessage2(error),
9005
+ action: "Check the embedding provider configuration and retry search after restoring provider health."
9006
+ });
9007
+ }
8084
9008
  const embeddingMs = performance2.now() - embeddingStartTime;
8085
9009
  const vectorStartTime = performance2.now();
8086
- const semanticResults = store.search(embedding, maxResults * 4);
9010
+ const semanticResults = embedding ? store.search(embedding, maxResults * 4) : [];
8087
9011
  const vectorMs = performance2.now() - vectorStartTime;
8088
9012
  const keywordStartTime = performance2.now();
8089
- const keywordResults = await this.keywordSearch(query, maxResults * 4);
9013
+ const keywordResults = await this.keywordSearch(query, maxResults * 4, store, invertedIndex);
8090
9014
  const keywordMs = performance2.now() - keywordStartTime;
8091
9015
  let branchChunkIds = null;
8092
9016
  if (filterByBranch && (this.config.scope === "global" || this.currentBranch !== "default")) {
@@ -8118,12 +9042,13 @@ var Indexer = class {
8118
9042
  });
8119
9043
  }
8120
9044
  const fusionStartTime = performance2.now();
9045
+ const rankingHybridWeight = embedding === void 0 && fusionStrategy === "weighted" ? 1 : effectiveHybridWeight;
8121
9046
  const combined = rankHybridResults(query, semanticCandidates, keywordCandidates, {
8122
9047
  fusionStrategy,
8123
9048
  rrfK,
8124
9049
  rerankTopN,
8125
9050
  limit: maxResults,
8126
- hybridWeight,
9051
+ hybridWeight: rankingHybridWeight,
8127
9052
  prioritizeSourcePaths: sourceIntent
8128
9053
  });
8129
9054
  const rerankedCombined = await this.rerankCandidatesWithApi(query, combined, {
@@ -8223,8 +9148,7 @@ var Indexer = class {
8223
9148
  })
8224
9149
  );
8225
9150
  }
8226
- async keywordSearch(query, limit) {
8227
- const { store, invertedIndex } = await this.ensureInitialized();
9151
+ async keywordSearch(query, limit, store, invertedIndex) {
8228
9152
  const scores = invertedIndex.search(query);
8229
9153
  if (scores.size === 0) {
8230
9154
  return [];
@@ -8242,24 +9166,54 @@ var Indexer = class {
8242
9166
  return results.slice(0, limit);
8243
9167
  }
8244
9168
  async getStatus() {
8245
- const { store, configuredProviderInfo, database } = await this.ensureInitialized();
9169
+ const { store, configuredProviderInfo, database, readIssues, compatibility } = await this.ensureInitialized();
8246
9170
  const failedBatchesCount = this.getFailedBatchesCount();
9171
+ const vectorCount = store.count();
9172
+ const statusReadIssues = [...readIssues];
9173
+ let startupWarning = "";
9174
+ if (!statusReadIssues.some((issue) => issue.component === "database")) {
9175
+ try {
9176
+ startupWarning = database.getMetadata(STARTUP_WARNING_METADATA_KEY) ?? "";
9177
+ } catch (error) {
9178
+ const message = this.getDatabaseReadIssueMessage();
9179
+ statusReadIssues.push(this.createReadIssue("database", message));
9180
+ if (!this.readIssues.some((issue) => issue.component === "database")) {
9181
+ this.recordReadIssue("database", message, error);
9182
+ }
9183
+ }
9184
+ }
9185
+ const readWarning = statusReadIssues.map((issue) => issue.message).join(" ");
9186
+ const warning = [readWarning, startupWarning].filter((message) => message.length > 0).join(" ");
9187
+ const hasBlockingReadIssue = statusReadIssues.some((issue) => issue.blocking);
8247
9188
  return {
8248
- indexed: store.count() > 0,
8249
- vectorCount: store.count(),
9189
+ indexed: vectorCount > 0 && !hasBlockingReadIssue,
9190
+ vectorCount,
8250
9191
  provider: configuredProviderInfo.provider,
8251
9192
  model: configuredProviderInfo.modelInfo.model,
8252
9193
  indexPath: this.indexPath,
8253
9194
  currentBranch: this.currentBranch,
8254
9195
  baseBranch: this.baseBranch,
8255
- compatibility: this.indexCompatibility,
9196
+ compatibility,
8256
9197
  failedBatchesCount,
8257
9198
  failedBatchesPath: failedBatchesCount > 0 ? this.failedBatchesPath : void 0,
8258
- warning: database.getMetadata(STARTUP_WARNING_METADATA_KEY) ?? void 0
9199
+ warning: warning || void 0
8259
9200
  };
8260
9201
  }
9202
+ async forceIndex(onProgress) {
9203
+ return this.withIndexMutationLease("force-index", async (recoveredOwners) => {
9204
+ await this.ensureInitializedUnlocked(recoveredOwners);
9205
+ await this.clearIndexUnlocked();
9206
+ return this.indexUnlocked(onProgress, [], true);
9207
+ });
9208
+ }
8261
9209
  async clearIndex() {
8262
- const { store, invertedIndex, database } = await this.ensureInitialized();
9210
+ await this.withIndexMutationLease("clear", async (recoveredOwners) => {
9211
+ await this.ensureInitializedUnlocked(recoveredOwners);
9212
+ await this.clearIndexUnlocked();
9213
+ });
9214
+ }
9215
+ async clearIndexUnlocked() {
9216
+ const { store, invertedIndex, database } = this.requireLoadedIndexState();
8263
9217
  if (this.config.scope === "global") {
8264
9218
  store.load();
8265
9219
  invertedIndex.load();
@@ -8286,7 +9240,7 @@ var Indexer = class {
8286
9240
  store.clear();
8287
9241
  store.save();
8288
9242
  invertedIndex.clear();
8289
- invertedIndex.save();
9243
+ this.saveInvertedIndex(invertedIndex);
8290
9244
  this.fileHashCache.clear();
8291
9245
  this.saveFileHashCache();
8292
9246
  database.clearAllIndexedData();
@@ -8310,14 +9264,7 @@ var Indexer = class {
8310
9264
  this.indexCompatibility = compatibility;
8311
9265
  return;
8312
9266
  }
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) {
9267
+ if (!this.isLocalProjectIndexPath()) {
8321
9268
  throw new Error(
8322
9269
  "Project-scoped force rebuild is unsafe while using an inherited worktree index. Create a local project config boundary before clearing the index."
8323
9270
  );
@@ -8325,7 +9272,7 @@ var Indexer = class {
8325
9272
  store.clear();
8326
9273
  store.save();
8327
9274
  invertedIndex.clear();
8328
- invertedIndex.save();
9275
+ this.saveInvertedIndex(invertedIndex);
8329
9276
  this.fileHashCache.clear();
8330
9277
  this.saveFileHashCache();
8331
9278
  database.clearAllIndexedData();
@@ -8343,7 +9290,13 @@ var Indexer = class {
8343
9290
  this.indexCompatibility = this.validateIndexCompatibility(this.configuredProviderInfo);
8344
9291
  }
8345
9292
  async healthCheck() {
8346
- const { store, invertedIndex, database } = await this.ensureInitialized();
9293
+ return this.withIndexMutationLease("health-check", async (recoveredOwners) => {
9294
+ await this.ensureInitializedUnlocked(recoveredOwners);
9295
+ return this.healthCheckUnlocked();
9296
+ });
9297
+ }
9298
+ async healthCheckUnlocked() {
9299
+ const { store, invertedIndex, database } = this.requireLoadedIndexState();
8347
9300
  this.logger.gc("info", "Starting health check");
8348
9301
  const allMetadata = store.getAllMetadata();
8349
9302
  const filePathsToChunkKeys = /* @__PURE__ */ new Map();
@@ -8356,7 +9309,7 @@ var Indexer = class {
8356
9309
  const removedChunkKeys = [];
8357
9310
  const chunkKeysByRemovedFile = /* @__PURE__ */ new Map();
8358
9311
  for (const [filePath, chunkKeys] of filePathsToChunkKeys) {
8359
- if (!existsSync6(filePath)) {
9312
+ if (!existsSync7(filePath)) {
8360
9313
  chunkKeysByRemovedFile.set(filePath, chunkKeys);
8361
9314
  for (const key of chunkKeys) {
8362
9315
  removedChunkKeys.push(key);
@@ -8381,7 +9334,7 @@ var Indexer = class {
8381
9334
  const removedCount = removedChunkKeys.length;
8382
9335
  if (removedCount > 0) {
8383
9336
  store.save();
8384
- invertedIndex.save();
9337
+ this.saveInvertedIndex(invertedIndex);
8385
9338
  }
8386
9339
  let gcOrphanEmbeddings;
8387
9340
  let gcOrphanChunks;
@@ -8396,7 +9349,7 @@ var Indexer = class {
8396
9349
  if (!await this.tryResetCorruptedIndex("running index health check", error)) {
8397
9350
  throw error;
8398
9351
  }
8399
- await this.ensureInitialized();
9352
+ await this.initializeUnlocked("writer", [], { skipAutoGc: true });
8400
9353
  return {
8401
9354
  removed: 0,
8402
9355
  filePaths: [],
@@ -8405,7 +9358,7 @@ var Indexer = class {
8405
9358
  gcOrphanSymbols: 0,
8406
9359
  gcOrphanCallEdges: 0,
8407
9360
  resetCorruptedIndex: true,
8408
- warning: this.getCorruptedIndexWarning(path11.join(this.indexPath, "codebase.db"))
9361
+ warning: this.getCorruptedIndexWarning(path12.join(this.indexPath, "codebase.db"))
8409
9362
  };
8410
9363
  }
8411
9364
  this.logger.recordGc(removedCount, gcOrphanChunks, gcOrphanEmbeddings);
@@ -8418,7 +9371,13 @@ var Indexer = class {
8418
9371
  return { removed: removedCount, filePaths: removedFilePaths, gcOrphanEmbeddings, gcOrphanChunks, gcOrphanSymbols, gcOrphanCallEdges };
8419
9372
  }
8420
9373
  async retryFailedBatches() {
8421
- const { store, provider, invertedIndex, database, configuredProviderInfo } = await this.ensureInitialized();
9374
+ return this.withIndexMutationLease("retry-failed-batches", async (recoveredOwners) => {
9375
+ await this.ensureInitializedUnlocked(recoveredOwners);
9376
+ return this.retryFailedBatchesUnlocked();
9377
+ });
9378
+ }
9379
+ async retryFailedBatchesUnlocked() {
9380
+ const { store, provider, invertedIndex, database, configuredProviderInfo } = this.requireLoadedIndexState();
8422
9381
  const maxChunkTokens = getSafeEmbeddingChunkTokenLimit(configuredProviderInfo);
8423
9382
  const providerRateLimits = this.getProviderRateLimits(configuredProviderInfo.provider);
8424
9383
  const roots = this.config.scope === "global" ? this.getScopedRoots() : null;
@@ -8582,7 +9541,7 @@ var Indexer = class {
8582
9541
  }
8583
9542
  if (succeeded > 0) {
8584
9543
  store.save();
8585
- invertedIndex.save();
9544
+ this.saveInvertedIndex(invertedIndex);
8586
9545
  }
8587
9546
  if (roots && succeeded > 0 && persistedStillFailing.length === 0 && this.hasProjectForceReembedPending()) {
8588
9547
  database.deleteMetadata(this.getProjectForceReembedMetadataKey());
@@ -8610,15 +9569,16 @@ var Indexer = class {
8610
9569
  }
8611
9570
  }
8612
9571
  async getDatabaseStats() {
8613
- const { database } = await this.ensureInitialized();
9572
+ const { database, readIssues } = await this.ensureInitialized();
9573
+ this.requireReadableComponents(readIssues, "database");
8614
9574
  return database.getStats();
8615
9575
  }
8616
9576
  getLogger() {
8617
9577
  return this.logger;
8618
9578
  }
8619
9579
  async findSimilar(code, limit = this.config.search.maxResults, options) {
8620
- const { store, provider, database } = await this.ensureInitialized();
8621
- const compatibility = this.checkCompatibility();
9580
+ const { store, provider, database, readIssues, compatibility } = await this.ensureInitialized();
9581
+ this.requireReadableComponents(readIssues, "vectors", "database");
8622
9582
  if (!compatibility.compatible) {
8623
9583
  throw new Error(
8624
9584
  `${compatibility.reason ?? "Index is incompatible with current embedding provider."} Run index_codebase with force=true to rebuild the index.`
@@ -8732,7 +9692,8 @@ var Indexer = class {
8732
9692
  );
8733
9693
  }
8734
9694
  async getCallers(targetName, callTypeFilter) {
8735
- const { database } = await this.ensureInitialized();
9695
+ const { database, readIssues } = await this.ensureInitialized();
9696
+ this.requireReadableComponents(readIssues, "database");
8736
9697
  const seen = /* @__PURE__ */ new Set();
8737
9698
  const results = [];
8738
9699
  for (const branchKey of this.getBranchCatalogKeys()) {
@@ -8746,7 +9707,8 @@ var Indexer = class {
8746
9707
  return results;
8747
9708
  }
8748
9709
  async getCallees(symbolId, callTypeFilter) {
8749
- const { database } = await this.ensureInitialized();
9710
+ const { database, readIssues } = await this.ensureInitialized();
9711
+ this.requireReadableComponents(readIssues, "database");
8750
9712
  const seen = /* @__PURE__ */ new Set();
8751
9713
  const results = [];
8752
9714
  for (const branchKey of this.getBranchCatalogKeys()) {
@@ -8760,43 +9722,50 @@ var Indexer = class {
8760
9722
  return results;
8761
9723
  }
8762
9724
  async findCallPath(fromName, toName, maxDepth) {
8763
- const { database } = await this.ensureInitialized();
9725
+ const { database, readIssues } = await this.ensureInitialized();
9726
+ this.requireReadableComponents(readIssues, "database");
8764
9727
  let shortest = [];
8765
9728
  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;
9729
+ const path27 = database.findShortestPath(fromName, toName, branchKey, maxDepth);
9730
+ if (path27.length > 0 && (shortest.length === 0 || path27.length < shortest.length)) {
9731
+ shortest = path27;
8769
9732
  }
8770
9733
  }
8771
9734
  return shortest;
8772
9735
  }
8773
9736
  async getSymbolsForBranch(branch) {
8774
- const { database } = await this.ensureInitialized();
9737
+ const { database, readIssues } = await this.ensureInitialized();
9738
+ this.requireReadableComponents(readIssues, "database");
8775
9739
  const resolvedBranch = branch ?? this.getBranchCatalogKey();
8776
9740
  return database.getSymbolsForBranch(resolvedBranch);
8777
9741
  }
8778
9742
  async getSymbolsForFiles(filePaths, branch) {
8779
- const { database } = await this.ensureInitialized();
9743
+ const { database, readIssues } = await this.ensureInitialized();
9744
+ this.requireReadableComponents(readIssues, "database");
8780
9745
  const resolvedBranch = branch ?? this.getBranchCatalogKey();
8781
9746
  return database.getSymbolsForFiles(filePaths, resolvedBranch);
8782
9747
  }
8783
9748
  async getTransitiveReachability(rootSymbolIds, direction, maxDepth) {
8784
- const { database } = await this.ensureInitialized();
9749
+ const { database, readIssues } = await this.ensureInitialized();
9750
+ this.requireReadableComponents(readIssues, "database");
8785
9751
  const branch = this.getBranchCatalogKey();
8786
9752
  return database.getTransitiveReachability(rootSymbolIds, branch, direction, maxDepth);
8787
9753
  }
8788
9754
  async detectCommunities(branch, symbolIds) {
8789
- const { database } = await this.ensureInitialized();
9755
+ const { database, readIssues } = await this.ensureInitialized();
9756
+ this.requireReadableComponents(readIssues, "database");
8790
9757
  const resolvedBranch = branch ?? this.getBranchCatalogKey();
8791
9758
  return database.detectCommunities(resolvedBranch, symbolIds);
8792
9759
  }
8793
9760
  async computeCentrality(branch) {
8794
- const { database } = await this.ensureInitialized();
9761
+ const { database, readIssues } = await this.ensureInitialized();
9762
+ this.requireReadableComponents(readIssues, "database");
8795
9763
  const resolvedBranch = branch ?? this.getBranchCatalogKey();
8796
9764
  return database.computeCentrality(resolvedBranch);
8797
9765
  }
8798
9766
  async getPrImpact(opts) {
8799
- const { database } = await this.ensureInitialized();
9767
+ const { database, readIssues } = await this.ensureInitialized();
9768
+ this.requireReadableComponents(readIssues, "database");
8800
9769
  const execFileAsync3 = promisify3(execFile3);
8801
9770
  const changedFilesResult = await getChangedFiles({
8802
9771
  pr: opts.pr,
@@ -8819,7 +9788,7 @@ var Indexer = class {
8819
9788
  "Run index_codebase first to build the call graph and symbol index for this branch."
8820
9789
  );
8821
9790
  }
8822
- const absoluteChangedFiles = changedFiles.map((f) => path11.resolve(this.projectRoot, f));
9791
+ const absoluteChangedFiles = changedFiles.map((f) => path12.resolve(this.projectRoot, f));
8823
9792
  const directSymbols = database.getSymbolsForFiles(absoluteChangedFiles, branchKey);
8824
9793
  const directIds = directSymbols.map((s) => s.id);
8825
9794
  const direction = opts.direction ?? "both";
@@ -8902,7 +9871,7 @@ var Indexer = class {
8902
9871
  projectRoot: this.projectRoot,
8903
9872
  baseBranch: this.baseBranch
8904
9873
  });
8905
- const otherAbsolute = otherChanged.files.map((f) => path11.resolve(this.projectRoot, f));
9874
+ const otherAbsolute = otherChanged.files.map((f) => path12.resolve(this.projectRoot, f));
8906
9875
  const otherBranchKey = this.getBranchCatalogKeyFor(openPr.headRefName);
8907
9876
  const otherSymbols = database.getSymbolsForFiles(otherAbsolute, otherBranchKey);
8908
9877
  const otherLabels = /* @__PURE__ */ new Set();
@@ -8952,7 +9921,8 @@ var Indexer = class {
8952
9921
  };
8953
9922
  }
8954
9923
  async getVisualizationData(options) {
8955
- const { database, store } = await this.ensureInitialized();
9924
+ const { database, store, readIssues } = await this.ensureInitialized();
9925
+ this.requireReadableComponents(readIssues, "vectors", "database");
8956
9926
  const seenSymbols = /* @__PURE__ */ new Map();
8957
9927
  const seenEdges = /* @__PURE__ */ new Map();
8958
9928
  for (const branchKey of this.getBranchCatalogKeys()) {
@@ -8965,12 +9935,12 @@ var Indexer = class {
8965
9935
  if (meta.filePath) filePaths.add(meta.filePath);
8966
9936
  }
8967
9937
  const directory = options?.directory?.replace(/\/$/, "");
8968
- const absoluteDirectoryFilter = directory ? path11.resolve(this.projectRoot, directory) : void 0;
9938
+ const absoluteDirectoryFilter = directory ? path12.resolve(this.projectRoot, directory) : void 0;
8969
9939
  for (const filePath of filePaths) {
8970
9940
  if (directory) {
8971
- const absoluteFilePath = path11.resolve(filePath);
9941
+ const absoluteFilePath = path12.resolve(filePath);
8972
9942
  const matchesRelative = filePath === directory || filePath.startsWith(directory + "/");
8973
- const matchesProjectRelative = absoluteDirectoryFilter !== void 0 && (absoluteFilePath === absoluteDirectoryFilter || absoluteFilePath.startsWith(absoluteDirectoryFilter + path11.sep));
9943
+ const matchesProjectRelative = absoluteDirectoryFilter !== void 0 && (absoluteFilePath === absoluteDirectoryFilter || absoluteFilePath.startsWith(absoluteDirectoryFilter + path12.sep));
8974
9944
  if (!matchesRelative && !matchesProjectRelative) {
8975
9945
  continue;
8976
9946
  }
@@ -8992,12 +9962,23 @@ var Indexer = class {
8992
9962
  return { symbols: [...seenSymbols.values()], edges: [...seenEdges.values()] };
8993
9963
  }
8994
9964
  async close() {
8995
- await this.database?.close();
9965
+ this.database?.close();
9966
+ for (const database of this.retiredDatabases) {
9967
+ database.close();
9968
+ }
9969
+ this.retiredDatabases = [];
8996
9970
  this.database = null;
8997
9971
  this.store = null;
8998
9972
  this.invertedIndex = null;
8999
9973
  this.provider = null;
9000
9974
  this.reranker = null;
9975
+ this.configuredProviderInfo = null;
9976
+ this.indexCompatibility = null;
9977
+ this.initializationMode = "none";
9978
+ this.readIssues = [];
9979
+ this.readerArtifactFingerprint = null;
9980
+ this.writerArtifactFingerprint = null;
9981
+ this.readerArtifactRetryAfter.clear();
9001
9982
  }
9002
9983
  };
9003
9984
 
@@ -9248,15 +10229,15 @@ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingToke
9248
10229
  }
9249
10230
 
9250
10231
  // 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";
10232
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync8, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
10233
+ import * as os5 from "os";
10234
+ import * as path14 from "path";
9254
10235
 
9255
10236
  // src/config/rebase.ts
9256
- import * as path12 from "path";
10237
+ import * as path13 from "path";
9257
10238
  function isWithinRoot(rootDir, targetPath) {
9258
- const relativePath = path12.relative(rootDir, targetPath);
9259
- return relativePath === "" || !relativePath.startsWith("..") && !path12.isAbsolute(relativePath);
10239
+ const relativePath = path13.relative(rootDir, targetPath);
10240
+ return relativePath === "" || !relativePath.startsWith("..") && !path13.isAbsolute(relativePath);
9260
10241
  }
9261
10242
  function rebasePathEntries(values, fromDir, toDir) {
9262
10243
  if (!Array.isArray(values)) {
@@ -9264,10 +10245,10 @@ function rebasePathEntries(values, fromDir, toDir) {
9264
10245
  }
9265
10246
  return values.filter((value) => typeof value === "string").map((value) => {
9266
10247
  const trimmed = value.trim();
9267
- if (!trimmed || path12.isAbsolute(trimmed)) {
10248
+ if (!trimmed || path13.isAbsolute(trimmed)) {
9268
10249
  return trimmed;
9269
10250
  }
9270
- return normalizePathSeparators(path12.normalize(path12.relative(toDir, path12.resolve(fromDir, trimmed))));
10251
+ return normalizePathSeparators(path13.normalize(path13.relative(toDir, path13.resolve(fromDir, trimmed))));
9271
10252
  }).filter(Boolean);
9272
10253
  }
9273
10254
  function resolveInheritedKnowledgeBaseEntries(values, sourceRoot, targetRoot) {
@@ -9279,17 +10260,17 @@ function resolveInheritedKnowledgeBaseEntries(values, sourceRoot, targetRoot) {
9279
10260
  if (!trimmed) {
9280
10261
  return trimmed;
9281
10262
  }
9282
- if (path12.isAbsolute(trimmed)) {
10263
+ if (path13.isAbsolute(trimmed)) {
9283
10264
  if (isWithinRoot(sourceRoot, trimmed)) {
9284
- return normalizePathSeparators(path12.normalize(path12.relative(sourceRoot, trimmed) || "."));
10265
+ return normalizePathSeparators(path13.normalize(path13.relative(sourceRoot, trimmed) || "."));
9285
10266
  }
9286
- return path12.normalize(trimmed);
10267
+ return path13.normalize(trimmed);
9287
10268
  }
9288
- const resolvedFromSource = path12.resolve(sourceRoot, trimmed);
10269
+ const resolvedFromSource = path13.resolve(sourceRoot, trimmed);
9289
10270
  if (isWithinRoot(sourceRoot, resolvedFromSource)) {
9290
- return normalizePathSeparators(path12.normalize(trimmed));
10271
+ return normalizePathSeparators(path13.normalize(trimmed));
9291
10272
  }
9292
- return normalizePathSeparators(path12.normalize(path12.relative(targetRoot, resolvedFromSource)));
10273
+ return normalizePathSeparators(path13.normalize(path13.relative(targetRoot, resolvedFromSource)));
9293
10274
  }).filter(Boolean);
9294
10275
  }
9295
10276
 
@@ -9327,7 +10308,7 @@ function validateEvalConfigShape(rawConfig, filePath) {
9327
10308
  }
9328
10309
  function parseJsonConfigFile(filePath) {
9329
10310
  try {
9330
- return validateEvalConfigShape(JSON.parse(readFileSync7(filePath, "utf-8")), filePath);
10311
+ return validateEvalConfigShape(JSON.parse(readFileSync8(filePath, "utf-8")), filePath);
9331
10312
  } catch (error) {
9332
10313
  if (error instanceof Error && error.message.startsWith("Eval config at ")) {
9333
10314
  throw error;
@@ -9337,20 +10318,20 @@ function parseJsonConfigFile(filePath) {
9337
10318
  }
9338
10319
  }
9339
10320
  function toAbsolute(projectRoot, maybeRelative) {
9340
- return path13.isAbsolute(maybeRelative) ? maybeRelative : path13.join(projectRoot, maybeRelative);
10321
+ return path14.isAbsolute(maybeRelative) ? maybeRelative : path14.join(projectRoot, maybeRelative);
9341
10322
  }
9342
10323
  function isProjectScopedConfigPath(configPath) {
9343
- return path13.basename(configPath) === "codebase-index.json" && path13.basename(path13.dirname(configPath)) === ".opencode";
10324
+ return path14.basename(configPath) === "codebase-index.json" && path14.basename(path14.dirname(configPath)) === ".opencode";
9344
10325
  }
9345
10326
  function normalizeEvalConfigKnowledgeBases(rawConfig, projectRoot, resolvedConfigPath) {
9346
10327
  const config = rawConfig && typeof rawConfig === "object" ? { ...rawConfig } : {};
9347
10328
  const rebaseEntries = (values) => isProjectScopedConfigPath(resolvedConfigPath) ? resolveInheritedKnowledgeBaseEntries(
9348
10329
  values,
9349
- path13.dirname(path13.dirname(resolvedConfigPath)),
10330
+ path14.dirname(path14.dirname(resolvedConfigPath)),
9350
10331
  projectRoot
9351
10332
  ) : rebasePathEntries(
9352
10333
  values,
9353
- path13.dirname(resolvedConfigPath),
10334
+ path14.dirname(resolvedConfigPath),
9354
10335
  projectRoot
9355
10336
  );
9356
10337
  if (Array.isArray(config.knowledgeBases)) {
@@ -9363,7 +10344,7 @@ function normalizeEvalConfigKnowledgeBases(rawConfig, projectRoot, resolvedConfi
9363
10344
  }
9364
10345
  function loadRawConfig(projectRoot, configPath) {
9365
10346
  const fromPath = configPath ? toAbsolute(projectRoot, configPath) : null;
9366
- if (fromPath && existsSync7(fromPath)) {
10347
+ if (fromPath && existsSync8(fromPath)) {
9367
10348
  return normalizeEvalConfigKnowledgeBases(
9368
10349
  parseJsonConfigFile(fromPath),
9369
10350
  projectRoot,
@@ -9371,15 +10352,15 @@ function loadRawConfig(projectRoot, configPath) {
9371
10352
  );
9372
10353
  }
9373
10354
  const projectConfig = resolveProjectConfigPath(projectRoot);
9374
- if (existsSync7(projectConfig)) {
10355
+ if (existsSync8(projectConfig)) {
9375
10356
  return normalizeEvalConfigKnowledgeBases(
9376
10357
  parseJsonConfigFile(projectConfig),
9377
10358
  projectRoot,
9378
10359
  projectConfig
9379
10360
  );
9380
10361
  }
9381
- const globalConfig = path13.join(os4.homedir(), ".config", "opencode", "codebase-index.json");
9382
- if (existsSync7(globalConfig)) {
10362
+ const globalConfig = path14.join(os5.homedir(), ".config", "opencode", "codebase-index.json");
10363
+ if (existsSync8(globalConfig)) {
9383
10364
  return parseJsonConfigFile(globalConfig);
9384
10365
  }
9385
10366
  return {};
@@ -9388,24 +10369,24 @@ function getIndexRootPath(projectRoot, scope) {
9388
10369
  return scope === "global" ? getGlobalIndexPath() : resolveProjectIndexPath(projectRoot, scope);
9389
10370
  }
9390
10371
  function getLocalProjectIndexRoot(projectRoot) {
9391
- return path13.join(projectRoot, ".opencode", "index");
10372
+ return path14.join(projectRoot, ".opencode", "index");
9392
10373
  }
9393
10374
  function getLocalProjectConfigPath(projectRoot) {
9394
- return path13.join(projectRoot, ".opencode", "codebase-index.json");
10375
+ return path14.join(projectRoot, ".opencode", "codebase-index.json");
9395
10376
  }
9396
10377
  function clearIndexRoot(projectRoot, scope) {
9397
10378
  const indexRoot = scope === "global" ? getIndexRootPath(projectRoot, scope) : getLocalProjectIndexRoot(projectRoot);
9398
- if (existsSync7(indexRoot)) {
9399
- rmSync(indexRoot, { recursive: true, force: true });
10379
+ if (existsSync8(indexRoot)) {
10380
+ rmSync2(indexRoot, { recursive: true, force: true });
9400
10381
  }
9401
10382
  }
9402
10383
  function ensureLocalEvalProjectConfig(projectRoot, configPath) {
9403
10384
  const localConfigPath = getLocalProjectConfigPath(projectRoot);
9404
10385
  const resolvedConfigPath = configPath ? toAbsolute(projectRoot, configPath) : resolveProjectConfigPath(projectRoot);
9405
- if (!configPath && existsSync7(localConfigPath)) {
10386
+ if (!configPath && existsSync8(localConfigPath)) {
9406
10387
  return localConfigPath;
9407
10388
  }
9408
- if (!existsSync7(resolvedConfigPath) || resolvedConfigPath === localConfigPath) {
10389
+ if (!existsSync8(resolvedConfigPath) || resolvedConfigPath === localConfigPath) {
9409
10390
  return resolvedConfigPath;
9410
10391
  }
9411
10392
  const sourceConfig = normalizeEvalConfigKnowledgeBases(
@@ -9413,8 +10394,8 @@ function ensureLocalEvalProjectConfig(projectRoot, configPath) {
9413
10394
  projectRoot,
9414
10395
  resolvedConfigPath
9415
10396
  );
9416
- mkdirSync3(path13.dirname(localConfigPath), { recursive: true });
9417
- writeFileSync3(localConfigPath, JSON.stringify(sourceConfig, null, 2), "utf-8");
10397
+ mkdirSync4(path14.dirname(localConfigPath), { recursive: true });
10398
+ writeFileSync4(localConfigPath, JSON.stringify(sourceConfig, null, 2), "utf-8");
9418
10399
  return localConfigPath;
9419
10400
  }
9420
10401
  function loadParsedConfig(projectRoot, configPath) {
@@ -9447,9 +10428,9 @@ function getEmbeddingCostPer1MTokens(embeddingProvider) {
9447
10428
  }
9448
10429
 
9449
10430
  // src/eval/schema.ts
9450
- import { readFileSync as readFileSync8 } from "fs";
10431
+ import { readFileSync as readFileSync9 } from "fs";
9451
10432
  function parseJsonFile(filePath) {
9452
- const content = readFileSync8(filePath, "utf-8");
10433
+ const content = readFileSync9(filePath, "utf-8");
9453
10434
  try {
9454
10435
  return JSON.parse(content);
9455
10436
  } catch (error) {
@@ -9463,23 +10444,23 @@ function isRecord2(value) {
9463
10444
  function isStringArray3(value) {
9464
10445
  return Array.isArray(value) && value.every((item) => typeof item === "string");
9465
10446
  }
9466
- function asPositiveNumber(value, path26) {
10447
+ function asPositiveNumber(value, path27) {
9467
10448
  if (typeof value !== "number" || Number.isNaN(value) || value < 0) {
9468
- throw new Error(`${path26} must be a non-negative number`);
10449
+ throw new Error(`${path27} must be a non-negative number`);
9469
10450
  }
9470
10451
  return value;
9471
10452
  }
9472
- function parseQueryType(value, path26) {
10453
+ function parseQueryType(value, path27) {
9473
10454
  if (value === "definition" || value === "implementation-intent" || value === "similarity" || value === "keyword-heavy") {
9474
10455
  return value;
9475
10456
  }
9476
10457
  throw new Error(
9477
- `${path26} must be one of: definition, implementation-intent, similarity, keyword-heavy`
10458
+ `${path27} must be one of: definition, implementation-intent, similarity, keyword-heavy`
9478
10459
  );
9479
10460
  }
9480
- function parseExpected(input, path26) {
10461
+ function parseExpected(input, path27) {
9481
10462
  if (!isRecord2(input)) {
9482
- throw new Error(`${path26} must be an object`);
10463
+ throw new Error(`${path27} must be an object`);
9483
10464
  }
9484
10465
  const filePathRaw = input.filePath;
9485
10466
  const acceptableFilesRaw = input.acceptableFiles;
@@ -9488,16 +10469,16 @@ function parseExpected(input, path26) {
9488
10469
  const filePath = typeof filePathRaw === "string" ? filePathRaw : void 0;
9489
10470
  const acceptableFiles = isStringArray3(acceptableFilesRaw) ? acceptableFilesRaw : void 0;
9490
10471
  if (!filePath && (!acceptableFiles || acceptableFiles.length === 0)) {
9491
- throw new Error(`${path26} must include either expected.filePath or expected.acceptableFiles`);
10472
+ throw new Error(`${path27} must include either expected.filePath or expected.acceptableFiles`);
9492
10473
  }
9493
10474
  if (acceptableFilesRaw !== void 0 && !isStringArray3(acceptableFilesRaw)) {
9494
- throw new Error(`${path26}.acceptableFiles must be an array of strings`);
10475
+ throw new Error(`${path27}.acceptableFiles must be an array of strings`);
9495
10476
  }
9496
10477
  if (symbolRaw !== void 0 && typeof symbolRaw !== "string") {
9497
- throw new Error(`${path26}.symbol must be a string when provided`);
10478
+ throw new Error(`${path27}.symbol must be a string when provided`);
9498
10479
  }
9499
10480
  if (branchRaw !== void 0 && typeof branchRaw !== "string") {
9500
- throw new Error(`${path26}.branch must be a string when provided`);
10481
+ throw new Error(`${path27}.branch must be a string when provided`);
9501
10482
  }
9502
10483
  return {
9503
10484
  filePath,
@@ -9507,25 +10488,25 @@ function parseExpected(input, path26) {
9507
10488
  };
9508
10489
  }
9509
10490
  function parseQuery(input, index) {
9510
- const path26 = `queries[${index}]`;
10491
+ const path27 = `queries[${index}]`;
9511
10492
  if (!isRecord2(input)) {
9512
- throw new Error(`${path26} must be an object`);
10493
+ throw new Error(`${path27} must be an object`);
9513
10494
  }
9514
10495
  const id = input.id;
9515
10496
  const query = input.query;
9516
10497
  const queryType = input.queryType;
9517
10498
  const expected = input.expected;
9518
10499
  if (typeof id !== "string" || id.trim().length === 0) {
9519
- throw new Error(`${path26}.id must be a non-empty string`);
10500
+ throw new Error(`${path27}.id must be a non-empty string`);
9520
10501
  }
9521
10502
  if (typeof query !== "string" || query.trim().length === 0) {
9522
- throw new Error(`${path26}.query must be a non-empty string`);
10503
+ throw new Error(`${path27}.query must be a non-empty string`);
9523
10504
  }
9524
10505
  return {
9525
10506
  id,
9526
10507
  query,
9527
- queryType: parseQueryType(queryType, `${path26}.queryType`),
9528
- expected: parseExpected(expected, `${path26}.expected`)
10508
+ queryType: parseQueryType(queryType, `${path27}.queryType`),
10509
+ expected: parseExpected(expected, `${path27}.expected`)
9529
10510
  };
9530
10511
  }
9531
10512
  function parseGoldenDataset(raw, sourceLabel) {
@@ -9708,13 +10689,13 @@ async function runEvaluation(options) {
9708
10689
  };
9709
10690
  const outputDir = createRunDirectory(toAbsolute(options.projectRoot, options.outputRoot));
9710
10691
  const perQueryArtifact = buildPerQueryArtifact(perQuery);
9711
- writeJson(path14.join(outputDir, "summary.json"), summary);
9712
- writeJson(path14.join(outputDir, "per-query.json"), perQueryArtifact);
10692
+ writeJson(path15.join(outputDir, "summary.json"), summary);
10693
+ writeJson(path15.join(outputDir, "per-query.json"), perQueryArtifact);
9713
10694
  let comparison;
9714
10695
  if (againstPath) {
9715
10696
  const baseline = loadSummary(againstPath);
9716
10697
  comparison = compareSummaries(summary, baseline, againstPath);
9717
- writeJson(path14.join(outputDir, "compare.json"), comparison);
10698
+ writeJson(path15.join(outputDir, "compare.json"), comparison);
9718
10699
  }
9719
10700
  let gate;
9720
10701
  if (options.ciMode) {
@@ -9724,10 +10705,10 @@ async function runEvaluation(options) {
9724
10705
  const budget = loadBudget(budgetPath);
9725
10706
  if (!comparison && budget.baselinePath) {
9726
10707
  const resolvedBaseline = toAbsolute(options.projectRoot, budget.baselinePath);
9727
- if (existsSync8(resolvedBaseline)) {
10708
+ if (existsSync9(resolvedBaseline)) {
9728
10709
  const baselineSummary = loadSummary(resolvedBaseline);
9729
10710
  comparison = compareSummaries(summary, baselineSummary, resolvedBaseline);
9730
- writeJson(path14.join(outputDir, "compare.json"), comparison);
10711
+ writeJson(path15.join(outputDir, "compare.json"), comparison);
9731
10712
  } else if (budget.failOnMissingBaseline) {
9732
10713
  throw new Error(
9733
10714
  `Budget baseline is missing: ${resolvedBaseline}. Set failOnMissingBaseline=false to allow CI run without baseline.`
@@ -9737,7 +10718,7 @@ async function runEvaluation(options) {
9737
10718
  gate = evaluateBudgetGate(budget, summary, comparison);
9738
10719
  }
9739
10720
  const markdown = createSummaryMarkdown(summary, comparison, gate);
9740
- writeText(path14.join(outputDir, "summary.md"), markdown);
10721
+ writeText(path15.join(outputDir, "summary.md"), markdown);
9741
10722
  return { outputDir, summary, perQuery, comparison, gate };
9742
10723
  } finally {
9743
10724
  await indexer.close();
@@ -9795,23 +10776,23 @@ async function runSweep(options, sweep) {
9795
10776
  bestByMrrAt10,
9796
10777
  bestByP95Latency
9797
10778
  };
9798
- writeJson(path14.join(outputDir, "compare.json"), aggregate);
10779
+ writeJson(path15.join(outputDir, "compare.json"), aggregate);
9799
10780
  const md = createSummaryMarkdown(
9800
10781
  bestByHitAt5?.summary ?? runs[0].summary,
9801
10782
  bestByHitAt5?.comparison,
9802
10783
  void 0,
9803
10784
  aggregate
9804
10785
  );
9805
- writeText(path14.join(outputDir, "summary.md"), md);
9806
- writeJson(path14.join(outputDir, "summary.json"), bestByHitAt5?.summary ?? runs[0].summary);
10786
+ writeText(path15.join(outputDir, "summary.md"), md);
10787
+ writeJson(path15.join(outputDir, "summary.json"), bestByHitAt5?.summary ?? runs[0].summary);
9807
10788
  return { outputDir, aggregate };
9808
10789
  }
9809
10790
 
9810
10791
  // src/eval/cli.ts
9811
- import * as path16 from "path";
10792
+ import * as path17 from "path";
9812
10793
 
9813
10794
  // src/eval/cli-parser.ts
9814
- import * as path15 from "path";
10795
+ import * as path16 from "path";
9815
10796
  function printUsage() {
9816
10797
  console.log(`
9817
10798
  Usage:
@@ -9883,12 +10864,12 @@ function parseEvalArgs(argv, cwd) {
9883
10864
  const arg = argv[i];
9884
10865
  const next = argv[i + 1];
9885
10866
  if (arg === "--project" && next) {
9886
- parsed.projectRoot = path15.resolve(cwd, next);
10867
+ parsed.projectRoot = path16.resolve(cwd, next);
9887
10868
  i += 1;
9888
10869
  continue;
9889
10870
  }
9890
10871
  if (arg === "--config" && next) {
9891
- parsed.configPath = path15.resolve(cwd, next);
10872
+ parsed.configPath = path16.resolve(cwd, next);
9892
10873
  i += 1;
9893
10874
  continue;
9894
10875
  }
@@ -10084,22 +11065,22 @@ async function handleEvalCommand(args, cwd) {
10084
11065
  if (!parsed.againstPath.endsWith(".json")) {
10085
11066
  throw new Error("eval diff --against must point to a summary JSON file");
10086
11067
  }
10087
- const currentSummary = loadSummary(path16.resolve(parsed.projectRoot, currentPath), {
11068
+ const currentSummary = loadSummary(path17.resolve(parsed.projectRoot, currentPath), {
10088
11069
  allowLegacyDiversityMetrics: true
10089
11070
  });
10090
- const baselineSummary = loadSummary(path16.resolve(parsed.projectRoot, parsed.againstPath), {
11071
+ const baselineSummary = loadSummary(path17.resolve(parsed.projectRoot, parsed.againstPath), {
10091
11072
  allowLegacyDiversityMetrics: true
10092
11073
  });
10093
11074
  const comparison = compareSummaries(
10094
11075
  currentSummary,
10095
11076
  baselineSummary,
10096
- path16.resolve(parsed.projectRoot, parsed.againstPath)
11077
+ path17.resolve(parsed.projectRoot, parsed.againstPath)
10097
11078
  );
10098
- const outputDir = createRunDirectory(path16.resolve(parsed.projectRoot, parsed.outputRoot));
11079
+ const outputDir = createRunDirectory(path17.resolve(parsed.projectRoot, parsed.outputRoot));
10099
11080
  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);
11081
+ writeJson(path17.join(outputDir, "compare.json"), comparison);
11082
+ writeText(path17.join(outputDir, "summary.md"), summaryMd);
11083
+ writeJson(path17.join(outputDir, "summary.json"), currentSummary);
10103
11084
  console.log(`Eval diff complete. Artifacts: ${outputDir}`);
10104
11085
  return 0;
10105
11086
  }
@@ -10108,7 +11089,7 @@ async function handleEvalCommand(args, cwd) {
10108
11089
 
10109
11090
  // src/mcp-server.ts
10110
11091
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10111
- import { readFileSync as readFileSync10 } from "fs";
11092
+ import { readFileSync as readFileSync11 } from "fs";
10112
11093
 
10113
11094
  // src/mcp-server/register-prompts.ts
10114
11095
  import { z } from "zod";
@@ -10299,6 +11280,10 @@ function formatStatus(status) {
10299
11280
  lines.push(`Failed batches: ${status.failedBatchesPath}`);
10300
11281
  }
10301
11282
  }
11283
+ if (status.warning) {
11284
+ lines.push("");
11285
+ lines.push(`INDEX WARNING: ${status.warning}`);
11286
+ }
10302
11287
  if (status.compatibility && !status.compatibility.compatible) {
10303
11288
  lines.push("");
10304
11289
  lines.push(`COMPATIBILITY WARNING: ${status.compatibility.reason}`);
@@ -10404,16 +11389,16 @@ function formatCallGraphCallees(symbolId, callees, relationshipType) {
10404
11389
  return `[${index + 1}] \u2192 ${edge.targetName} (${edge.callType})${confidence} at line ${edge.line}${edge.isResolved ? ` [resolved: ${edge.toSymbolId}]` : " [unresolved]"}`;
10405
11390
  }).join("\n");
10406
11391
  }
10407
- function formatCallGraphPath(from, to, path26) {
10408
- if (path26.length === 0) {
11392
+ function formatCallGraphPath(from, to, path27) {
11393
+ if (path27.length === 0) {
10409
11394
  return `No path found between "${from}" and "${to}". They may be in disconnected components, or the call graph index needs updating.`;
10410
11395
  }
10411
- const formatted = path26.map((hop, index) => {
11396
+ const formatted = path27.map((hop, index) => {
10412
11397
  const prefix = index === 0 ? "[start]" : `--${hop.callType}-->`;
10413
11398
  const location = hop.filePath ? ` (${hop.filePath}:${hop.line})` : "";
10414
11399
  return `${prefix} ${hop.symbolName}${location}`;
10415
11400
  });
10416
- return `Path (${path26.length} hops):
11401
+ return `Path (${path27.length} hops):
10417
11402
  ${formatted.join("\n")}`;
10418
11403
  }
10419
11404
  function formatResultHeader(result, index) {
@@ -10504,12 +11489,12 @@ function formatPrImpact(result) {
10504
11489
  }
10505
11490
 
10506
11491
  // src/tools/operations.ts
10507
- import { existsSync as existsSync11, realpathSync, statSync as statSync3 } from "fs";
10508
- import * as path20 from "path";
11492
+ import { existsSync as existsSync12, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
11493
+ import * as path21 from "path";
10509
11494
 
10510
11495
  // 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";
11496
+ import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
11497
+ import * as path18 from "path";
10513
11498
  var PROJECT_OVERRIDE_KEYS = [
10514
11499
  "embeddingProvider",
10515
11500
  "customProvider",
@@ -10542,8 +11527,8 @@ function mergeUniqueStringArray(values) {
10542
11527
  return [...new Set(values.map((value) => String(value).trim()))];
10543
11528
  }
10544
11529
  function normalizeKnowledgeBasePath(value) {
10545
- let normalized = path17.normalize(String(value).trim());
10546
- const root = path17.parse(normalized).root;
11530
+ let normalized = path18.normalize(String(value).trim());
11531
+ const root = path18.parse(normalized).root;
10547
11532
  while (normalized.length > root.length && /[\\/]$/.test(normalized)) {
10548
11533
  normalized = normalized.slice(0, -1);
10549
11534
  }
@@ -10577,11 +11562,11 @@ function validateConfigLayerShape(rawConfig, filePath) {
10577
11562
  return rawConfig;
10578
11563
  }
10579
11564
  function loadJsonFile(filePath) {
10580
- if (!existsSync9(filePath)) {
11565
+ if (!existsSync10(filePath)) {
10581
11566
  return null;
10582
11567
  }
10583
11568
  try {
10584
- const content = readFileSync9(filePath, "utf-8");
11569
+ const content = readFileSync10(filePath, "utf-8");
10585
11570
  return validateConfigLayerShape(JSON.parse(content), filePath);
10586
11571
  } catch (error) {
10587
11572
  if (error instanceof Error && error.message.startsWith("Config file ")) {
@@ -10596,8 +11581,8 @@ function loadConfigFile(filePath) {
10596
11581
  }
10597
11582
  function materializeLocalProjectConfig(projectRoot, config, host = "opencode") {
10598
11583
  const localConfigPath = resolveWritableProjectConfigPath(projectRoot, host);
10599
- mkdirSync4(path17.dirname(localConfigPath), { recursive: true });
10600
- writeFileSync4(localConfigPath, JSON.stringify(config, null, 2), "utf-8");
11584
+ mkdirSync5(path18.dirname(localConfigPath), { recursive: true });
11585
+ writeFileSync5(localConfigPath, JSON.stringify(config, null, 2), "utf-8");
10601
11586
  return localConfigPath;
10602
11587
  }
10603
11588
  function loadProjectConfigLayer(projectRoot, host = "opencode") {
@@ -10607,7 +11592,7 @@ function loadProjectConfigLayer(projectRoot, host = "opencode") {
10607
11592
  return {};
10608
11593
  }
10609
11594
  const normalizedConfig = { ...projectConfig };
10610
- const projectConfigBaseDir = path17.dirname(path17.dirname(projectConfigPath));
11595
+ const projectConfigBaseDir = path18.dirname(path18.dirname(projectConfigPath));
10611
11596
  if (Array.isArray(normalizedConfig.knowledgeBases)) {
10612
11597
  normalizedConfig.knowledgeBases = resolveInheritedKnowledgeBaseEntries(
10613
11598
  normalizedConfig.knowledgeBases,
@@ -10671,19 +11656,19 @@ function loadMergedConfig(projectRoot, host = "opencode") {
10671
11656
  }
10672
11657
 
10673
11658
  // src/tools/knowledge-base-paths.ts
10674
- import * as path18 from "path";
11659
+ import * as path19 from "path";
10675
11660
  function resolveConfigPathValue(value, baseDir) {
10676
11661
  const trimmed = value.trim();
10677
11662
  if (!trimmed) {
10678
11663
  return trimmed;
10679
11664
  }
10680
- const absolutePath = path18.isAbsolute(trimmed) ? trimmed : path18.resolve(baseDir, trimmed);
10681
- return path18.normalize(absolutePath);
11665
+ const absolutePath = path19.isAbsolute(trimmed) ? trimmed : path19.resolve(baseDir, trimmed);
11666
+ return path19.normalize(absolutePath);
10682
11667
  }
10683
11668
 
10684
11669
  // src/tools/config-state.ts
10685
- import { existsSync as existsSync10, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
10686
- import * as path19 from "path";
11670
+ import { existsSync as existsSync11, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
11671
+ import * as path20 from "path";
10687
11672
  function normalizeKnowledgeBasePaths(config, projectRoot) {
10688
11673
  const normalized = { ...config };
10689
11674
  if (Array.isArray(normalized.knowledgeBases)) {
@@ -10707,6 +11692,24 @@ function loadRuntimeConfig(projectRoot, host = "opencode") {
10707
11692
  var indexerCache = /* @__PURE__ */ new Map();
10708
11693
  var configCache = /* @__PURE__ */ new Map();
10709
11694
  var defaultProjectRoots = /* @__PURE__ */ new Map();
11695
+ function getIndexBusyResult(error) {
11696
+ if (!isIndexLockContentionError(error)) return null;
11697
+ const owner = error.owner;
11698
+ const ownerText = owner ? `PID ${owner.pid}, operation ${owner.operation}, since ${owner.startedAt}` : "unreadable owner";
11699
+ if (error.reason === "legacy-lock") {
11700
+ return {
11701
+ kind: "busy",
11702
+ text: `INDEX_BUSY: legacy lock format detected (${ownerText}). Verify the PID and remove this lock manually only if it is stale.`
11703
+ };
11704
+ }
11705
+ if (error.reason === "unknown-owner") {
11706
+ return {
11707
+ kind: "busy",
11708
+ text: `INDEX_BUSY: unreadable or remote lock owner (${ownerText}). Automatic recovery was refused; manual verification is required.`
11709
+ };
11710
+ }
11711
+ return { kind: "busy", text: `INDEX_BUSY: another index operation is already in progress (${ownerText}).` };
11712
+ }
10710
11713
  function getProjectRoot(projectRoot, host) {
10711
11714
  if (projectRoot) {
10712
11715
  return projectRoot;
@@ -10751,8 +11754,8 @@ function refreshIndexerForDirectory(projectRoot, host = "opencode", config = par
10751
11754
  }
10752
11755
  function shouldForceLocalizeProjectIndex(projectRoot, host = "opencode") {
10753
11756
  const root = getProjectRoot(projectRoot, host);
10754
- const localIndexPath = path20.join(root, getHostProjectIndexRelativePath(host));
10755
- if (existsSync11(localIndexPath)) {
11757
+ const localIndexPath = path21.join(root, getHostProjectIndexRelativePath(host));
11758
+ if (existsSync12(localIndexPath)) {
10756
11759
  return false;
10757
11760
  }
10758
11761
  const inheritedIndexPath = resolveWorktreeFallbackProjectIndexPath(root, host);
@@ -10808,35 +11811,46 @@ async function getCallGraphPath(projectRoot, host, from, to, maxDepth) {
10808
11811
  async function runIndexCodebase(projectRoot, host, args, onProgress) {
10809
11812
  const root = getProjectRoot(projectRoot, host);
10810
11813
  const key = getIndexerCacheKey(root, host);
10811
- const cachedConfig = configCache.get(key);
10812
11814
  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)
11815
+ const runtimeConfig = configCache.get(key);
11816
+ try {
11817
+ if (args.estimateOnly) {
11818
+ return { kind: "estimate", estimate: await indexer.estimateCost() };
11819
+ }
11820
+ const runIndex = async (target) => {
11821
+ const operation = args.force ? target.forceIndex.bind(target) : target.index.bind(target);
11822
+ return operation((progress) => {
11823
+ if (onProgress) {
11824
+ return onProgress(formatProgressTitle(progress), {
11825
+ phase: progress.phase,
11826
+ filesProcessed: progress.filesProcessed,
11827
+ totalFiles: progress.totalFiles,
11828
+ chunksProcessed: progress.chunksProcessed,
11829
+ totalChunks: progress.totalChunks,
11830
+ percentage: calculatePercentage(progress)
11831
+ });
11832
+ }
11833
+ return Promise.resolve();
10835
11834
  });
11835
+ };
11836
+ let stats;
11837
+ if (args.force && shouldForceLocalizeProjectIndex(root, host)) {
11838
+ const inheritedIndexPath = resolveProjectIndexPath(root, runtimeConfig.scope, host);
11839
+ stats = await withIndexLock(inheritedIndexPath, "force-index", async () => {
11840
+ materializeLocalProjectConfig(root, loadProjectConfigLayer(root, host), host);
11841
+ refreshIndexerForDirectory(root, host, runtimeConfig);
11842
+ indexer = getIndexerForProject(root, host);
11843
+ return runIndex(indexer);
11844
+ }, { completeRecoveries: false });
11845
+ } else {
11846
+ stats = await runIndex(indexer);
10836
11847
  }
10837
- return Promise.resolve();
10838
- });
10839
- return { kind: "stats", stats };
11848
+ return { kind: "stats", stats };
11849
+ } catch (error) {
11850
+ const busyResult = getIndexBusyResult(error);
11851
+ if (!busyResult) throw error;
11852
+ return busyResult;
11853
+ }
10840
11854
  }
10841
11855
  async function getIndexStatus(projectRoot, host) {
10842
11856
  const indexer = getIndexerForProject(projectRoot, host);
@@ -10846,6 +11860,15 @@ async function getIndexHealthCheck(projectRoot, host) {
10846
11860
  const indexer = getIndexerForProject(projectRoot, host);
10847
11861
  return indexer.healthCheck();
10848
11862
  }
11863
+ async function runIndexHealthCheck(projectRoot, host) {
11864
+ try {
11865
+ return { kind: "health", health: await getIndexHealthCheck(projectRoot, host) };
11866
+ } catch (error) {
11867
+ const busyResult = getIndexBusyResult(error);
11868
+ if (!busyResult) throw error;
11869
+ return busyResult;
11870
+ }
11871
+ }
10849
11872
  async function getPrImpact(projectRoot, host, params) {
10850
11873
  const indexer = getIndexerForProject(projectRoot, host);
10851
11874
  return indexer.getPrImpact({
@@ -10926,20 +11949,87 @@ var CHUNK_TYPE_ENUM = [
10926
11949
  ];
10927
11950
 
10928
11951
  // src/mcp-server/register-tools.ts
11952
+ function allowNullAsUndefined(schema) {
11953
+ return z2.preprocess((value) => value === null ? void 0 : value, schema);
11954
+ }
10929
11955
  function registerMcpTools(server, runtime) {
11956
+ server.tool(
11957
+ "codebase_context",
11958
+ "PREFERRED FIRST TOOL for any question about this repository. Use before built-in code search, grep, shell search, or broad file reads. Provide from+to for a dependency path, symbol for a definition, or only query for low-token conceptual discovery. Use call_graph directly for callers or callees.",
11959
+ {
11960
+ query: z2.string().describe("The codebase question or behavior to locate. Always provide the user's repository question here."),
11961
+ from: allowNullAsUndefined(z2.string().optional()).describe("Source symbol. For dependency-path questions, extract the first endpoint and provide it here."),
11962
+ to: allowNullAsUndefined(z2.string().optional()).describe("Target symbol. For dependency-path questions, extract the second endpoint and provide it here."),
11963
+ symbol: allowNullAsUndefined(z2.string().optional()).describe("Exact symbol for an authoritative definition lookup. Omit when from and to are supplied."),
11964
+ limit: allowNullAsUndefined(z2.number().optional().default(10)).describe("Maximum number of search or definition results"),
11965
+ maxDepth: allowNullAsUndefined(z2.number().optional().default(10)).describe("Maximum call-graph traversal depth for from/to path lookup"),
11966
+ fileType: allowNullAsUndefined(z2.string().optional()).describe("Filter by file extension (e.g., 'ts', 'py', 'rs')"),
11967
+ directory: allowNullAsUndefined(z2.string().optional()).describe("Filter by directory path (e.g., 'src/utils', 'lib')")
11968
+ },
11969
+ async (args) => {
11970
+ if (args.from && args.to) {
11971
+ const path27 = await getCallGraphPath(runtime.projectRoot, runtime.host, args.from, args.to, args.maxDepth);
11972
+ if (path27.length > 0) {
11973
+ return { content: [{ type: "text", text: formatCallGraphPath(args.from, args.to, path27) }] };
11974
+ }
11975
+ const { callers } = await getCallGraphData(runtime.projectRoot, runtime.host, {
11976
+ name: args.to,
11977
+ direction: "callers"
11978
+ });
11979
+ const directEdge = callers.find((edge) => edge.fromSymbolName === args.from);
11980
+ if (directEdge) {
11981
+ const location = directEdge.fromSymbolFilePath ? ` at ${directEdge.fromSymbolFilePath}:${directEdge.line}` : "";
11982
+ return {
11983
+ content: [{
11984
+ type: "text",
11985
+ text: `Direct path: ${args.from} --${directEdge.callType}--> ${args.to}${location} (edge is ${directEdge.isResolved ? "resolved" : "unresolved"}).`
11986
+ }]
11987
+ };
11988
+ }
11989
+ return { content: [{ type: "text", text: formatCallGraphPath(args.from, args.to, path27) }] };
11990
+ }
11991
+ if (args.symbol) {
11992
+ const results2 = await implementationLookup(runtime.projectRoot, runtime.host, args.symbol, {
11993
+ limit: args.limit ?? 10,
11994
+ fileType: args.fileType,
11995
+ directory: args.directory
11996
+ });
11997
+ return { content: [{ type: "text", text: formatDefinitionLookup(results2, args.symbol) }] };
11998
+ }
11999
+ const results = await searchCodebase(runtime.projectRoot, runtime.host, args.query, {
12000
+ limit: args.limit ?? 10,
12001
+ fileType: args.fileType,
12002
+ directory: args.directory,
12003
+ metadataOnly: true
12004
+ });
12005
+ if (results.length === 0) {
12006
+ return { content: [{ type: "text", text: "No matching code found. Try a different query or run index_codebase first." }] };
12007
+ }
12008
+ return {
12009
+ content: [{
12010
+ type: "text",
12011
+ text: `Found ${results.length} locations for "${args.query}":
12012
+
12013
+ ${formatCodebasePeek(results)}
12014
+
12015
+ Use implementation_lookup for an authoritative definition, or call call_graph/call_graph_path once you have a symbol.`
12016
+ }]
12017
+ };
12018
+ }
12019
+ );
10930
12020
  server.tool(
10931
12021
  "codebase_search",
10932
- "Search codebase by MEANING, not keywords. Returns full code content. For just finding WHERE code is (saves ~90% tokens), use codebase_peek instead.",
12022
+ "FULL-CONTENT semantic retrieval. Use after codebase_peek when you need implementation text, not as the default first step. For exact identifiers or exhaustive matches use grep instead.",
10933
12023
  {
10934
12024
  query: z2.string().describe("Natural language description of what code you're looking for. Describe behavior, not syntax."),
10935
- limit: z2.number().optional().default(5).describe("Maximum number of results to return"),
10936
- fileType: z2.string().optional().describe("Filter by file extension (e.g., 'ts', 'py', 'rs')"),
10937
- directory: z2.string().optional().describe("Filter by directory path (e.g., 'src/utils', 'lib')"),
10938
- chunkType: z2.enum(CHUNK_TYPE_ENUM).optional().describe("Filter by code chunk type"),
10939
- contextLines: z2.number().optional().describe("Number of extra lines to include before/after each match (default: 0)"),
10940
- blameAuthor: z2.string().optional().describe("Filter by git blame author name or email"),
10941
- blameSha: z2.string().optional().describe("Filter by git blame commit SHA or prefix"),
10942
- blameSince: z2.string().optional().describe("Filter to chunks last changed on or after this date")
12025
+ limit: allowNullAsUndefined(z2.number().optional().default(5)).describe("Maximum number of results to return"),
12026
+ fileType: allowNullAsUndefined(z2.string().optional()).describe("Filter by file extension (e.g., 'ts', 'py', 'rs')"),
12027
+ directory: allowNullAsUndefined(z2.string().optional()).describe("Filter by directory path (e.g., 'src/utils', 'lib')"),
12028
+ chunkType: allowNullAsUndefined(z2.enum(CHUNK_TYPE_ENUM).optional()).describe("Filter by code chunk type"),
12029
+ contextLines: allowNullAsUndefined(z2.number().optional()).describe("Number of extra lines to include before/after each match (default: 0)"),
12030
+ blameAuthor: allowNullAsUndefined(z2.string().optional()).describe("Filter by git blame author name or email"),
12031
+ blameSha: allowNullAsUndefined(z2.string().optional()).describe("Filter by git blame commit SHA or prefix"),
12032
+ blameSince: allowNullAsUndefined(z2.string().optional()).describe("Filter to chunks last changed on or after this date")
10943
12033
  },
10944
12034
  async (args) => {
10945
12035
  const results = await searchCodebase(runtime.projectRoot, runtime.host, args.query, {
@@ -10962,16 +12052,16 @@ ${formatSearchResults(results, "score")}` }] };
10962
12052
  );
10963
12053
  server.tool(
10964
12054
  "codebase_peek",
10965
- "Quick lookup of code locations by meaning. Returns only metadata (file, line, name, type) WITHOUT code content. Saves ~90% tokens vs codebase_search.",
12055
+ "DIRECT LOW-TOKEN semantic location lookup for unfamiliar-code discovery. Prefer codebase_context when the request may involve definitions or graph navigation; use this specialized tool when you only need conceptual locations.",
10966
12056
  {
10967
12057
  query: z2.string().describe("Natural language description of what code you're looking for."),
10968
- limit: z2.number().optional().default(10).describe("Maximum number of results to return"),
10969
- fileType: z2.string().optional().describe("Filter by file extension (e.g., 'ts', 'py', 'rs')"),
10970
- directory: z2.string().optional().describe("Filter by directory path (e.g., 'src/utils', 'lib')"),
10971
- chunkType: z2.enum(CHUNK_TYPE_ENUM).optional().describe("Filter by code chunk type"),
10972
- blameAuthor: z2.string().optional().describe("Filter by git blame author name or email"),
10973
- blameSha: z2.string().optional().describe("Filter by git blame commit SHA or prefix"),
10974
- blameSince: z2.string().optional().describe("Filter to chunks last changed on or after this date")
12058
+ limit: allowNullAsUndefined(z2.number().optional().default(10)).describe("Maximum number of results to return"),
12059
+ fileType: allowNullAsUndefined(z2.string().optional()).describe("Filter by file extension (e.g., 'ts', 'py', 'rs')"),
12060
+ directory: allowNullAsUndefined(z2.string().optional()).describe("Filter by directory path (e.g., 'src/utils', 'lib')"),
12061
+ chunkType: allowNullAsUndefined(z2.enum(CHUNK_TYPE_ENUM).optional()).describe("Filter by code chunk type"),
12062
+ blameAuthor: allowNullAsUndefined(z2.string().optional()).describe("Filter by git blame author name or email"),
12063
+ blameSha: allowNullAsUndefined(z2.string().optional()).describe("Filter by git blame commit SHA or prefix"),
12064
+ blameSince: allowNullAsUndefined(z2.string().optional()).describe("Filter to chunks last changed on or after this date")
10975
12065
  },
10976
12066
  async (args) => {
10977
12067
  const results = await searchCodebase(runtime.projectRoot, runtime.host, args.query, {
@@ -10996,21 +12086,26 @@ Use Read tool to examine specific files.` }] };
10996
12086
  );
10997
12087
  server.tool(
10998
12088
  "index_codebase",
10999
- "Index the codebase for semantic search. Creates vector embeddings of code chunks. Incremental - only re-indexes changed files. Run before first codebase_search.",
12089
+ "Create or refresh the semantic index. Call index_status first when readiness is unknown, then use this tool only if the index is missing, stale, or incompatible. Incremental by default; force=true rebuilds everything.",
11000
12090
  {
11001
- force: z2.boolean().optional().default(false).describe("Force reindex even if already indexed"),
11002
- estimateOnly: z2.boolean().optional().default(false).describe("Only show cost estimate without indexing"),
11003
- verbose: z2.boolean().optional().default(false).describe("Show detailed info about skipped files and parsing failures")
12091
+ force: allowNullAsUndefined(z2.boolean().optional().default(false)).describe("Force reindex even if already indexed"),
12092
+ estimateOnly: allowNullAsUndefined(z2.boolean().optional().default(false)).describe("Only show cost estimate without indexing"),
12093
+ verbose: allowNullAsUndefined(z2.boolean().optional().default(false)).describe("Show detailed info about skipped files and parsing failures")
11004
12094
  },
11005
12095
  async (args) => {
11006
12096
  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 }] };
12097
+ if (result.kind === "estimate") {
12098
+ return { content: [{ type: "text", text: formatCostEstimate(result.estimate) }] };
12099
+ }
12100
+ if (result.kind === "busy") {
12101
+ return { content: [{ type: "text", text: result.text }], isError: true };
12102
+ }
12103
+ return { content: [{ type: "text", text: formatIndexStats(result.stats, args.verbose ?? false) }] };
11009
12104
  }
11010
12105
  );
11011
12106
  server.tool(
11012
12107
  "index_status",
11013
- "Check the status of the codebase index. Shows whether the codebase is indexed, how many chunks are stored, and the embedding provider being used.",
12108
+ "START HERE once per repository task when index readiness or freshness is unknown. Reports whether semantic retrieval is ready, chunk counts, compatibility, and the embedding provider. If ready, continue with codebase_peek or implementation_lookup; otherwise run index_codebase.",
11014
12109
  {},
11015
12110
  async () => {
11016
12111
  const status = await getIndexStatus(runtime.projectRoot, runtime.host);
@@ -11022,8 +12117,11 @@ Use Read tool to examine specific files.` }] };
11022
12117
  "Check index health and remove stale entries from deleted files. Run this to clean up the index after files have been deleted.",
11023
12118
  {},
11024
12119
  async () => {
11025
- const result = await getIndexHealthCheck(runtime.projectRoot, runtime.host);
11026
- return { content: [{ type: "text", text: formatHealthCheck(result) }] };
12120
+ const result = await runIndexHealthCheck(runtime.projectRoot, runtime.host);
12121
+ if (result.kind === "busy") {
12122
+ return { content: [{ type: "text", text: result.text }], isError: true };
12123
+ }
12124
+ return { content: [{ type: "text", text: formatHealthCheck(result.health) }] };
11027
12125
  }
11028
12126
  );
11029
12127
  server.tool(
@@ -11039,9 +12137,13 @@ Use Read tool to examine specific files.` }] };
11039
12137
  "index_logs",
11040
12138
  "Get recent debug logs from the codebase indexer. Requires debug.enabled=true in config.",
11041
12139
  {
11042
- limit: z2.number().optional().default(20).describe("Maximum number of log entries to return"),
11043
- category: z2.enum(["search", "embedding", "cache", "gc", "branch", "general"]).optional().describe("Filter by log category"),
11044
- level: z2.enum(["error", "warn", "info", "debug"]).optional().describe("Filter by minimum log level")
12140
+ limit: allowNullAsUndefined(z2.number().optional().default(20)).describe("Maximum number of log entries to return"),
12141
+ category: allowNullAsUndefined(
12142
+ z2.enum(["search", "embedding", "cache", "gc", "branch", "general"]).optional()
12143
+ ).describe("Filter by log category"),
12144
+ level: allowNullAsUndefined(
12145
+ z2.enum(["error", "warn", "info", "debug"]).optional()
12146
+ ).describe("Filter by minimum log level")
11045
12147
  },
11046
12148
  async (args) => {
11047
12149
  const result = await getIndexLogs(runtime.projectRoot, runtime.host, args);
@@ -11050,14 +12152,14 @@ Use Read tool to examine specific files.` }] };
11050
12152
  );
11051
12153
  server.tool(
11052
12154
  "find_similar",
11053
- "Find code similar to a given snippet. Use for duplicate detection, pattern discovery, or refactoring prep.",
12155
+ "Use when you already have a code snippet and need analogous implementations, duplicates, patterns, or refactoring candidates. For a natural-language concept without example code, start with codebase_peek instead.",
11054
12156
  {
11055
12157
  code: z2.string().describe("The code snippet to find similar code for"),
11056
- limit: z2.number().optional().default(10).describe("Maximum number of results to return"),
11057
- fileType: z2.string().optional().describe("Filter by file extension (e.g., 'ts', 'py', 'rs')"),
11058
- directory: z2.string().optional().describe("Filter by directory path (e.g., 'src/utils', 'lib')"),
11059
- chunkType: z2.enum(CHUNK_TYPE_ENUM).optional().describe("Filter by code chunk type"),
11060
- excludeFile: z2.string().optional().describe("Exclude results from this file path")
12158
+ limit: allowNullAsUndefined(z2.number().optional().default(10)).describe("Maximum number of results to return"),
12159
+ fileType: allowNullAsUndefined(z2.string().optional()).describe("Filter by file extension (e.g., 'ts', 'py', 'rs')"),
12160
+ directory: allowNullAsUndefined(z2.string().optional()).describe("Filter by directory path (e.g., 'src/utils', 'lib')"),
12161
+ chunkType: allowNullAsUndefined(z2.enum(CHUNK_TYPE_ENUM).optional()).describe("Filter by code chunk type"),
12162
+ excludeFile: allowNullAsUndefined(z2.string().optional()).describe("Exclude results from this file path")
11061
12163
  },
11062
12164
  async (args) => {
11063
12165
  const results = await findSimilarCode(runtime.projectRoot, runtime.host, args.code, {
@@ -11077,12 +12179,12 @@ ${formatSearchResults(results)}` }] };
11077
12179
  );
11078
12180
  server.tool(
11079
12181
  "implementation_lookup",
11080
- "Jump to symbol definition. Find WHERE something is defined. Returns the authoritative source location(s). Prefers real implementation files over tests, docs, examples, and fixtures.",
12182
+ "FIRST TOOL only for known-symbol definition questions. Returns authoritative source locations and prefers implementations over tests, docs, examples, and fixtures. Do not use for callers, callees, dependency paths, or code flow; use codebase_context with direction or from/to for those questions.",
11081
12183
  {
11082
12184
  query: z2.string().describe("Symbol name or natural language description (e.g., 'validateToken', 'where is the payment handler defined')"),
11083
- limit: z2.number().optional().default(5).describe("Maximum number of results"),
11084
- fileType: z2.string().optional().describe("Filter by file extension (e.g., 'ts', 'py')"),
11085
- directory: z2.string().optional().describe("Filter by directory path (e.g., 'src/utils')")
12185
+ limit: allowNullAsUndefined(z2.number().optional().default(5)).describe("Maximum number of results"),
12186
+ fileType: allowNullAsUndefined(z2.string().optional()).describe("Filter by file extension (e.g., 'ts', 'py')"),
12187
+ directory: allowNullAsUndefined(z2.string().optional()).describe("Filter by directory path (e.g., 'src/utils')")
11086
12188
  },
11087
12189
  async (args) => {
11088
12190
  const results = await implementationLookup(runtime.projectRoot, runtime.host, args.query, {
@@ -11095,12 +12197,16 @@ ${formatSearchResults(results)}` }] };
11095
12197
  );
11096
12198
  server.tool(
11097
12199
  "call_graph",
11098
- "Query the call graph to find callers or callees of a function/method. Use to understand code flow and dependencies. Supports relationship types: Call, MethodCall, Constructor, Import, Inherits, Implements.",
12200
+ "Use after identifying a symbol to find its direct callers or callees and understand code flow. Use implementation_lookup first if the symbol or definition is still ambiguous. Supports relationship types: Call, MethodCall, Constructor, Import, Inherits, Implements.",
11099
12201
  {
11100
12202
  name: z2.string().describe("Function or method name to query"),
11101
- direction: z2.enum(["callers", "callees"]).default("callers").describe("Direction: 'callers' finds who calls this function, 'callees' finds what this function calls"),
11102
- symbolId: z2.string().optional().describe("Symbol ID (required for 'callees' direction)"),
11103
- relationshipType: z2.enum(["Call", "MethodCall", "Constructor", "Import", "Inherits", "Implements"]).optional().describe("Filter by relationship type. Omit to show all.")
12203
+ direction: allowNullAsUndefined(
12204
+ z2.enum(["callers", "callees"]).default("callers")
12205
+ ).describe("Direction: 'callers' finds who calls this function, 'callees' finds what this function calls"),
12206
+ symbolId: allowNullAsUndefined(z2.string().optional()).describe("Symbol ID (required for 'callees' direction)"),
12207
+ relationshipType: allowNullAsUndefined(
12208
+ z2.enum(["Call", "MethodCall", "Constructor", "Import", "Inherits", "Implements"]).optional()
12209
+ ).describe("Filter by relationship type. Omit to show all.")
11104
12210
  },
11105
12211
  async (args) => {
11106
12212
  if (args.direction === "callees") {
@@ -11116,27 +12222,29 @@ ${formatSearchResults(results)}` }] };
11116
12222
  );
11117
12223
  server.tool(
11118
12224
  "call_graph_path",
11119
- "Find the shortest connection path between two symbols in the call graph. Returns the chain of calls connecting them.",
12225
+ "Use after identifying both endpoint symbols to find the shortest known call path between them. Use codebase_peek or implementation_lookup first when either endpoint is unknown.",
11120
12226
  {
11121
12227
  from: z2.string().describe("Source function/method name (starting point)"),
11122
12228
  to: z2.string().describe("Target function/method name (destination)"),
11123
- maxDepth: z2.number().optional().default(10).describe("Maximum traversal depth (default: 10)")
12229
+ maxDepth: allowNullAsUndefined(z2.number().optional().default(10)).describe("Maximum traversal depth (default: 10)")
11124
12230
  },
11125
12231
  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) }] };
12232
+ const path27 = await getCallGraphPath(runtime.projectRoot, runtime.host, args.from, args.to, args.maxDepth);
12233
+ return { content: [{ type: "text", text: formatCallGraphPath(args.from, args.to, path27) }] };
11128
12234
  }
11129
12235
  );
11130
12236
  server.tool(
11131
12237
  "pr_impact",
11132
- "Analyze the impact of a pull request or branch by examining changed files, affected symbols, transitive callers, communities touched, hub nodes, and risk level. Use to understand blast radius before merging.",
12238
+ "FIRST TOOL for pull-request or branch blast-radius questions. Analyzes changed files, affected symbols, transitive dependencies, communities, hub nodes, conflicts, and risk before merging.",
11133
12239
  {
11134
- pr: z2.number().optional().describe("Pull request number to analyze"),
11135
- branch: z2.string().optional().describe("Branch name to analyze (defaults to current branch)"),
11136
- maxDepth: z2.number().optional().default(5).describe("Maximum traversal depth for transitive callers (default: 5)"),
11137
- hubThreshold: z2.number().optional().default(10).describe("Minimum caller count to flag a symbol as a hub node (default: 10)"),
11138
- checkConflicts: z2.boolean().optional().default(false).describe("Check for conflicting open PRs touching the same communities (default: false)"),
11139
- direction: z2.enum(["callers", "callees", "both"]).optional().default("both").describe("Call-graph traversal direction: 'callers' for upstream, 'callees' for downstream, 'both' for union (default: both)")
12240
+ pr: allowNullAsUndefined(z2.number().optional()).describe("Pull request number to analyze"),
12241
+ branch: allowNullAsUndefined(z2.string().optional()).describe("Branch name to analyze (defaults to current branch)"),
12242
+ maxDepth: allowNullAsUndefined(z2.number().optional().default(5)).describe("Maximum traversal depth for transitive callers (default: 5)"),
12243
+ hubThreshold: allowNullAsUndefined(z2.number().optional().default(10)).describe("Minimum caller count to flag a symbol as a hub node (default: 10)"),
12244
+ checkConflicts: allowNullAsUndefined(z2.boolean().optional().default(false)).describe("Check for conflicting open PRs touching the same communities (default: false)"),
12245
+ direction: allowNullAsUndefined(
12246
+ z2.enum(["callers", "callees", "both"]).optional().default("both")
12247
+ ).describe("Call-graph traversal direction: 'callers' for upstream, 'callees' for downstream, 'both' for union (default: both)")
11140
12248
  },
11141
12249
  async (args) => {
11142
12250
  try {
@@ -11158,8 +12266,12 @@ ${formatSearchResults(results)}` }] };
11158
12266
  }
11159
12267
 
11160
12268
  // src/mcp-server.ts
12269
+ function getServerInstructions(host) {
12270
+ const hostText = `host ${host}`;
12271
+ return `This MCP server is the preferred codebase-understanding path for ${hostText}. Start a repository task with index_status when index readiness or freshness is unknown. Use codebase_context as the preferred first entry point because it returns low-token locations first and routes to definitions or call-graph helpers when symbol intent is present. Use codebase_peek for direct conceptual location lookup, implementation_lookup for known-symbol definition questions, and codebase_search only when full semantic code content is needed. For exact identifiers or exhaustive matches, use grep. After identifying symbols, use call_graph or call_graph_path to trace dependencies. If the index is unavailable, run index_codebase, then retry the retrieval tool.`;
12272
+ }
11161
12273
  function getPackageVersion() {
11162
- const raw = JSON.parse(readFileSync10(new URL("../package.json", import.meta.url), "utf-8"));
12274
+ const raw = JSON.parse(readFileSync11(new URL("../package.json", import.meta.url), "utf-8"));
11163
12275
  if (raw && typeof raw === "object" && "version" in raw && typeof raw.version === "string") {
11164
12276
  return raw.version;
11165
12277
  }
@@ -11169,6 +12281,8 @@ function createMcpServer(projectRoot, config, host = "opencode") {
11169
12281
  const server = new McpServer({
11170
12282
  name: "opencode-codebase-index",
11171
12283
  version: getPackageVersion()
12284
+ }, {
12285
+ instructions: getServerInstructions(host)
11172
12286
  });
11173
12287
  initializeTools(projectRoot, config, host);
11174
12288
  registerMcpTools(server, {
@@ -11180,18 +12294,37 @@ function createMcpServer(projectRoot, config, host = "opencode") {
11180
12294
  }
11181
12295
 
11182
12296
  // src/utils/auto-index.ts
12297
+ var INITIAL_RETRY_DELAY_MS = 50;
12298
+ var MAX_RETRY_DELAY_MS = 500;
12299
+ var autoIndexStates = /* @__PURE__ */ new WeakMap();
11183
12300
  function getErrorMessage3(error) {
11184
12301
  return error instanceof Error ? error.message : String(error);
11185
12302
  }
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
- });
12303
+ function runAutoIndex(indexer, projectRoot, state) {
12304
+ void indexer.index().then(() => {
12305
+ autoIndexStates.delete(indexer);
11191
12306
  }).catch((error) => {
11192
- console.error(`[codebase-index] Auto-index initialization failed for "${projectRoot}": ${getErrorMessage3(error)}`);
12307
+ if (!isTransientIndexLockContention(error)) {
12308
+ autoIndexStates.delete(indexer);
12309
+ console.error(`[codebase-index] Auto-index failed for "${projectRoot}": ${getErrorMessage3(error)}`);
12310
+ return;
12311
+ }
12312
+ const retryDelayMs = state.retryDelayMs;
12313
+ state.retryDelayMs = Math.min(retryDelayMs * 2, MAX_RETRY_DELAY_MS);
12314
+ const retryTimer = setTimeout(() => {
12315
+ runAutoIndex(indexer, projectRoot, state);
12316
+ }, retryDelayMs);
12317
+ retryTimer.unref?.();
11193
12318
  });
11194
12319
  }
12320
+ function startAutoIndex(indexer, projectRoot) {
12321
+ if (autoIndexStates.has(indexer)) return;
12322
+ const state = {
12323
+ retryDelayMs: INITIAL_RETRY_DELAY_MS
12324
+ };
12325
+ autoIndexStates.set(indexer, state);
12326
+ runAutoIndex(indexer, projectRoot, state);
12327
+ }
11195
12328
 
11196
12329
  // node_modules/chokidar/index.js
11197
12330
  import { EventEmitter as EventEmitter2 } from "events";
@@ -11283,7 +12416,7 @@ var ReaddirpStream = class extends Readable {
11283
12416
  this._directoryFilter = normalizeFilter(opts.directoryFilter);
11284
12417
  const statMethod = opts.lstat ? lstat : stat;
11285
12418
  if (wantBigintFsStats) {
11286
- this._stat = (path26) => statMethod(path26, { bigint: true });
12419
+ this._stat = (path27) => statMethod(path27, { bigint: true });
11287
12420
  } else {
11288
12421
  this._stat = statMethod;
11289
12422
  }
@@ -11308,8 +12441,8 @@ var ReaddirpStream = class extends Readable {
11308
12441
  const par = this.parent;
11309
12442
  const fil = par && par.files;
11310
12443
  if (fil && fil.length > 0) {
11311
- const { path: path26, depth } = par;
11312
- const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path26));
12444
+ const { path: path27, depth } = par;
12445
+ const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path27));
11313
12446
  const awaited = await Promise.all(slice);
11314
12447
  for (const entry of awaited) {
11315
12448
  if (!entry)
@@ -11349,20 +12482,20 @@ var ReaddirpStream = class extends Readable {
11349
12482
  this.reading = false;
11350
12483
  }
11351
12484
  }
11352
- async _exploreDir(path26, depth) {
12485
+ async _exploreDir(path27, depth) {
11353
12486
  let files;
11354
12487
  try {
11355
- files = await readdir(path26, this._rdOptions);
12488
+ files = await readdir(path27, this._rdOptions);
11356
12489
  } catch (error) {
11357
12490
  this._onError(error);
11358
12491
  }
11359
- return { files, depth, path: path26 };
12492
+ return { files, depth, path: path27 };
11360
12493
  }
11361
- async _formatEntry(dirent, path26) {
12494
+ async _formatEntry(dirent, path27) {
11362
12495
  let entry;
11363
12496
  const basename5 = this._isDirent ? dirent.name : dirent;
11364
12497
  try {
11365
- const fullPath = presolve(pjoin(path26, basename5));
12498
+ const fullPath = presolve(pjoin(path27, basename5));
11366
12499
  entry = { path: prelative(this._root, fullPath), fullPath, basename: basename5 };
11367
12500
  entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
11368
12501
  } catch (err) {
@@ -11762,16 +12895,16 @@ var delFromSet = (main2, prop, item) => {
11762
12895
  };
11763
12896
  var isEmptySet = (val) => val instanceof Set ? val.size === 0 : !val;
11764
12897
  var FsWatchInstances = /* @__PURE__ */ new Map();
11765
- function createFsWatchInstance(path26, options, listener, errHandler, emitRaw) {
12898
+ function createFsWatchInstance(path27, options, listener, errHandler, emitRaw) {
11766
12899
  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));
12900
+ listener(path27);
12901
+ emitRaw(rawEvent, evPath, { watchedPath: path27 });
12902
+ if (evPath && path27 !== evPath) {
12903
+ fsWatchBroadcast(sp.resolve(path27, evPath), KEY_LISTENERS, sp.join(path27, evPath));
11771
12904
  }
11772
12905
  };
11773
12906
  try {
11774
- return fs_watch(path26, {
12907
+ return fs_watch(path27, {
11775
12908
  persistent: options.persistent
11776
12909
  }, handleEvent);
11777
12910
  } catch (error) {
@@ -11787,12 +12920,12 @@ var fsWatchBroadcast = (fullPath, listenerType, val1, val2, val3) => {
11787
12920
  listener(val1, val2, val3);
11788
12921
  });
11789
12922
  };
11790
- var setFsWatchListener = (path26, fullPath, options, handlers) => {
12923
+ var setFsWatchListener = (path27, fullPath, options, handlers) => {
11791
12924
  const { listener, errHandler, rawEmitter } = handlers;
11792
12925
  let cont = FsWatchInstances.get(fullPath);
11793
12926
  let watcher;
11794
12927
  if (!options.persistent) {
11795
- watcher = createFsWatchInstance(path26, options, listener, errHandler, rawEmitter);
12928
+ watcher = createFsWatchInstance(path27, options, listener, errHandler, rawEmitter);
11796
12929
  if (!watcher)
11797
12930
  return;
11798
12931
  return watcher.close.bind(watcher);
@@ -11803,7 +12936,7 @@ var setFsWatchListener = (path26, fullPath, options, handlers) => {
11803
12936
  addAndConvert(cont, KEY_RAW, rawEmitter);
11804
12937
  } else {
11805
12938
  watcher = createFsWatchInstance(
11806
- path26,
12939
+ path27,
11807
12940
  options,
11808
12941
  fsWatchBroadcast.bind(null, fullPath, KEY_LISTENERS),
11809
12942
  errHandler,
@@ -11818,7 +12951,7 @@ var setFsWatchListener = (path26, fullPath, options, handlers) => {
11818
12951
  cont.watcherUnusable = true;
11819
12952
  if (isWindows && error.code === "EPERM") {
11820
12953
  try {
11821
- const fd = await open(path26, "r");
12954
+ const fd = await open(path27, "r");
11822
12955
  await fd.close();
11823
12956
  broadcastErr(error);
11824
12957
  } catch (err) {
@@ -11849,7 +12982,7 @@ var setFsWatchListener = (path26, fullPath, options, handlers) => {
11849
12982
  };
11850
12983
  };
11851
12984
  var FsWatchFileInstances = /* @__PURE__ */ new Map();
11852
- var setFsWatchFileListener = (path26, fullPath, options, handlers) => {
12985
+ var setFsWatchFileListener = (path27, fullPath, options, handlers) => {
11853
12986
  const { listener, rawEmitter } = handlers;
11854
12987
  let cont = FsWatchFileInstances.get(fullPath);
11855
12988
  const copts = cont && cont.options;
@@ -11871,7 +13004,7 @@ var setFsWatchFileListener = (path26, fullPath, options, handlers) => {
11871
13004
  });
11872
13005
  const currmtime = curr.mtimeMs;
11873
13006
  if (curr.size !== prev.size || currmtime > prev.mtimeMs || currmtime === 0) {
11874
- foreach(cont.listeners, (listener2) => listener2(path26, curr));
13007
+ foreach(cont.listeners, (listener2) => listener2(path27, curr));
11875
13008
  }
11876
13009
  })
11877
13010
  };
@@ -11901,13 +13034,13 @@ var NodeFsHandler = class {
11901
13034
  * @param listener on fs change
11902
13035
  * @returns closer for the watcher instance
11903
13036
  */
11904
- _watchWithNodeFs(path26, listener) {
13037
+ _watchWithNodeFs(path27, listener) {
11905
13038
  const opts = this.fsw.options;
11906
- const directory = sp.dirname(path26);
11907
- const basename5 = sp.basename(path26);
13039
+ const directory = sp.dirname(path27);
13040
+ const basename5 = sp.basename(path27);
11908
13041
  const parent = this.fsw._getWatchedDir(directory);
11909
13042
  parent.add(basename5);
11910
- const absolutePath = sp.resolve(path26);
13043
+ const absolutePath = sp.resolve(path27);
11911
13044
  const options = {
11912
13045
  persistent: opts.persistent
11913
13046
  };
@@ -11917,12 +13050,12 @@ var NodeFsHandler = class {
11917
13050
  if (opts.usePolling) {
11918
13051
  const enableBin = opts.interval !== opts.binaryInterval;
11919
13052
  options.interval = enableBin && isBinaryPath(basename5) ? opts.binaryInterval : opts.interval;
11920
- closer = setFsWatchFileListener(path26, absolutePath, options, {
13053
+ closer = setFsWatchFileListener(path27, absolutePath, options, {
11921
13054
  listener,
11922
13055
  rawEmitter: this.fsw._emitRaw
11923
13056
  });
11924
13057
  } else {
11925
- closer = setFsWatchListener(path26, absolutePath, options, {
13058
+ closer = setFsWatchListener(path27, absolutePath, options, {
11926
13059
  listener,
11927
13060
  errHandler: this._boundHandleError,
11928
13061
  rawEmitter: this.fsw._emitRaw
@@ -11944,7 +13077,7 @@ var NodeFsHandler = class {
11944
13077
  let prevStats = stats;
11945
13078
  if (parent.has(basename5))
11946
13079
  return;
11947
- const listener = async (path26, newStats) => {
13080
+ const listener = async (path27, newStats) => {
11948
13081
  if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5))
11949
13082
  return;
11950
13083
  if (!newStats || newStats.mtimeMs === 0) {
@@ -11958,11 +13091,11 @@ var NodeFsHandler = class {
11958
13091
  this.fsw._emit(EV.CHANGE, file, newStats2);
11959
13092
  }
11960
13093
  if ((isMacos || isLinux || isFreeBSD) && prevStats.ino !== newStats2.ino) {
11961
- this.fsw._closeFile(path26);
13094
+ this.fsw._closeFile(path27);
11962
13095
  prevStats = newStats2;
11963
13096
  const closer2 = this._watchWithNodeFs(file, listener);
11964
13097
  if (closer2)
11965
- this.fsw._addPathCloser(path26, closer2);
13098
+ this.fsw._addPathCloser(path27, closer2);
11966
13099
  } else {
11967
13100
  prevStats = newStats2;
11968
13101
  }
@@ -11994,7 +13127,7 @@ var NodeFsHandler = class {
11994
13127
  * @param item basename of this item
11995
13128
  * @returns true if no more processing is needed for this entry.
11996
13129
  */
11997
- async _handleSymlink(entry, directory, path26, item) {
13130
+ async _handleSymlink(entry, directory, path27, item) {
11998
13131
  if (this.fsw.closed) {
11999
13132
  return;
12000
13133
  }
@@ -12004,7 +13137,7 @@ var NodeFsHandler = class {
12004
13137
  this.fsw._incrReadyCount();
12005
13138
  let linkPath;
12006
13139
  try {
12007
- linkPath = await fsrealpath(path26);
13140
+ linkPath = await fsrealpath(path27);
12008
13141
  } catch (e) {
12009
13142
  this.fsw._emitReady();
12010
13143
  return true;
@@ -12014,12 +13147,12 @@ var NodeFsHandler = class {
12014
13147
  if (dir.has(item)) {
12015
13148
  if (this.fsw._symlinkPaths.get(full) !== linkPath) {
12016
13149
  this.fsw._symlinkPaths.set(full, linkPath);
12017
- this.fsw._emit(EV.CHANGE, path26, entry.stats);
13150
+ this.fsw._emit(EV.CHANGE, path27, entry.stats);
12018
13151
  }
12019
13152
  } else {
12020
13153
  dir.add(item);
12021
13154
  this.fsw._symlinkPaths.set(full, linkPath);
12022
- this.fsw._emit(EV.ADD, path26, entry.stats);
13155
+ this.fsw._emit(EV.ADD, path27, entry.stats);
12023
13156
  }
12024
13157
  this.fsw._emitReady();
12025
13158
  return true;
@@ -12049,9 +13182,9 @@ var NodeFsHandler = class {
12049
13182
  return;
12050
13183
  }
12051
13184
  const item = entry.path;
12052
- let path26 = sp.join(directory, item);
13185
+ let path27 = sp.join(directory, item);
12053
13186
  current.add(item);
12054
- if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path26, item)) {
13187
+ if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path27, item)) {
12055
13188
  return;
12056
13189
  }
12057
13190
  if (this.fsw.closed) {
@@ -12060,8 +13193,8 @@ var NodeFsHandler = class {
12060
13193
  }
12061
13194
  if (item === target || !target && !previous.has(item)) {
12062
13195
  this.fsw._incrReadyCount();
12063
- path26 = sp.join(dir, sp.relative(dir, path26));
12064
- this._addToNodeFs(path26, initialAdd, wh, depth + 1);
13196
+ path27 = sp.join(dir, sp.relative(dir, path27));
13197
+ this._addToNodeFs(path27, initialAdd, wh, depth + 1);
12065
13198
  }
12066
13199
  }).on(EV.ERROR, this._boundHandleError);
12067
13200
  return new Promise((resolve15, reject) => {
@@ -12130,13 +13263,13 @@ var NodeFsHandler = class {
12130
13263
  * @param depth Child path actually targeted for watch
12131
13264
  * @param target Child path actually targeted for watch
12132
13265
  */
12133
- async _addToNodeFs(path26, initialAdd, priorWh, depth, target) {
13266
+ async _addToNodeFs(path27, initialAdd, priorWh, depth, target) {
12134
13267
  const ready = this.fsw._emitReady;
12135
- if (this.fsw._isIgnored(path26) || this.fsw.closed) {
13268
+ if (this.fsw._isIgnored(path27) || this.fsw.closed) {
12136
13269
  ready();
12137
13270
  return false;
12138
13271
  }
12139
- const wh = this.fsw._getWatchHelpers(path26);
13272
+ const wh = this.fsw._getWatchHelpers(path27);
12140
13273
  if (priorWh) {
12141
13274
  wh.filterPath = (entry) => priorWh.filterPath(entry);
12142
13275
  wh.filterDir = (entry) => priorWh.filterDir(entry);
@@ -12152,8 +13285,8 @@ var NodeFsHandler = class {
12152
13285
  const follow = this.fsw.options.followSymlinks;
12153
13286
  let closer;
12154
13287
  if (stats.isDirectory()) {
12155
- const absPath = sp.resolve(path26);
12156
- const targetPath = follow ? await fsrealpath(path26) : path26;
13288
+ const absPath = sp.resolve(path27);
13289
+ const targetPath = follow ? await fsrealpath(path27) : path27;
12157
13290
  if (this.fsw.closed)
12158
13291
  return;
12159
13292
  closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath);
@@ -12163,29 +13296,29 @@ var NodeFsHandler = class {
12163
13296
  this.fsw._symlinkPaths.set(absPath, targetPath);
12164
13297
  }
12165
13298
  } else if (stats.isSymbolicLink()) {
12166
- const targetPath = follow ? await fsrealpath(path26) : path26;
13299
+ const targetPath = follow ? await fsrealpath(path27) : path27;
12167
13300
  if (this.fsw.closed)
12168
13301
  return;
12169
13302
  const parent = sp.dirname(wh.watchPath);
12170
13303
  this.fsw._getWatchedDir(parent).add(wh.watchPath);
12171
13304
  this.fsw._emit(EV.ADD, wh.watchPath, stats);
12172
- closer = await this._handleDir(parent, stats, initialAdd, depth, path26, wh, targetPath);
13305
+ closer = await this._handleDir(parent, stats, initialAdd, depth, path27, wh, targetPath);
12173
13306
  if (this.fsw.closed)
12174
13307
  return;
12175
13308
  if (targetPath !== void 0) {
12176
- this.fsw._symlinkPaths.set(sp.resolve(path26), targetPath);
13309
+ this.fsw._symlinkPaths.set(sp.resolve(path27), targetPath);
12177
13310
  }
12178
13311
  } else {
12179
13312
  closer = this._handleFile(wh.watchPath, stats, initialAdd);
12180
13313
  }
12181
13314
  ready();
12182
13315
  if (closer)
12183
- this.fsw._addPathCloser(path26, closer);
13316
+ this.fsw._addPathCloser(path27, closer);
12184
13317
  return false;
12185
13318
  } catch (error) {
12186
13319
  if (this.fsw._handleError(error)) {
12187
13320
  ready();
12188
- return path26;
13321
+ return path27;
12189
13322
  }
12190
13323
  }
12191
13324
  }
@@ -12228,24 +13361,24 @@ function createPattern(matcher) {
12228
13361
  }
12229
13362
  return () => false;
12230
13363
  }
12231
- function normalizePath2(path26) {
12232
- if (typeof path26 !== "string")
13364
+ function normalizePath2(path27) {
13365
+ if (typeof path27 !== "string")
12233
13366
  throw new Error("string expected");
12234
- path26 = sp2.normalize(path26);
12235
- path26 = path26.replace(/\\/g, "/");
13367
+ path27 = sp2.normalize(path27);
13368
+ path27 = path27.replace(/\\/g, "/");
12236
13369
  let prepend = false;
12237
- if (path26.startsWith("//"))
13370
+ if (path27.startsWith("//"))
12238
13371
  prepend = true;
12239
- path26 = path26.replace(DOUBLE_SLASH_RE, "/");
13372
+ path27 = path27.replace(DOUBLE_SLASH_RE, "/");
12240
13373
  if (prepend)
12241
- path26 = "/" + path26;
12242
- return path26;
13374
+ path27 = "/" + path27;
13375
+ return path27;
12243
13376
  }
12244
13377
  function matchPatterns(patterns, testString, stats) {
12245
- const path26 = normalizePath2(testString);
13378
+ const path27 = normalizePath2(testString);
12246
13379
  for (let index = 0; index < patterns.length; index++) {
12247
13380
  const pattern = patterns[index];
12248
- if (pattern(path26, stats)) {
13381
+ if (pattern(path27, stats)) {
12249
13382
  return true;
12250
13383
  }
12251
13384
  }
@@ -12283,19 +13416,19 @@ var toUnix = (string) => {
12283
13416
  }
12284
13417
  return str;
12285
13418
  };
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));
13419
+ var normalizePathToUnix = (path27) => toUnix(sp2.normalize(toUnix(path27)));
13420
+ var normalizeIgnored = (cwd = "") => (path27) => {
13421
+ if (typeof path27 === "string") {
13422
+ return normalizePathToUnix(sp2.isAbsolute(path27) ? path27 : sp2.join(cwd, path27));
12290
13423
  } else {
12291
- return path26;
13424
+ return path27;
12292
13425
  }
12293
13426
  };
12294
- var getAbsolutePath = (path26, cwd) => {
12295
- if (sp2.isAbsolute(path26)) {
12296
- return path26;
13427
+ var getAbsolutePath = (path27, cwd) => {
13428
+ if (sp2.isAbsolute(path27)) {
13429
+ return path27;
12297
13430
  }
12298
- return sp2.join(cwd, path26);
13431
+ return sp2.join(cwd, path27);
12299
13432
  };
12300
13433
  var EMPTY_SET = Object.freeze(/* @__PURE__ */ new Set());
12301
13434
  var DirEntry = class {
@@ -12360,10 +13493,10 @@ var WatchHelper = class {
12360
13493
  dirParts;
12361
13494
  followSymlinks;
12362
13495
  statMethod;
12363
- constructor(path26, follow, fsw) {
13496
+ constructor(path27, follow, fsw) {
12364
13497
  this.fsw = fsw;
12365
- const watchPath = path26;
12366
- this.path = path26 = path26.replace(REPLACER_RE, "");
13498
+ const watchPath = path27;
13499
+ this.path = path27 = path27.replace(REPLACER_RE, "");
12367
13500
  this.watchPath = watchPath;
12368
13501
  this.fullWatchPath = sp2.resolve(watchPath);
12369
13502
  this.dirParts = [];
@@ -12503,20 +13636,20 @@ var FSWatcher = class extends EventEmitter2 {
12503
13636
  this._closePromise = void 0;
12504
13637
  let paths = unifyPaths(paths_);
12505
13638
  if (cwd) {
12506
- paths = paths.map((path26) => {
12507
- const absPath = getAbsolutePath(path26, cwd);
13639
+ paths = paths.map((path27) => {
13640
+ const absPath = getAbsolutePath(path27, cwd);
12508
13641
  return absPath;
12509
13642
  });
12510
13643
  }
12511
- paths.forEach((path26) => {
12512
- this._removeIgnoredPath(path26);
13644
+ paths.forEach((path27) => {
13645
+ this._removeIgnoredPath(path27);
12513
13646
  });
12514
13647
  this._userIgnored = void 0;
12515
13648
  if (!this._readyCount)
12516
13649
  this._readyCount = 0;
12517
13650
  this._readyCount += paths.length;
12518
- Promise.all(paths.map(async (path26) => {
12519
- const res = await this._nodeFsHandler._addToNodeFs(path26, !_internal, void 0, 0, _origAdd);
13651
+ Promise.all(paths.map(async (path27) => {
13652
+ const res = await this._nodeFsHandler._addToNodeFs(path27, !_internal, void 0, 0, _origAdd);
12520
13653
  if (res)
12521
13654
  this._emitReady();
12522
13655
  return res;
@@ -12538,17 +13671,17 @@ var FSWatcher = class extends EventEmitter2 {
12538
13671
  return this;
12539
13672
  const paths = unifyPaths(paths_);
12540
13673
  const { cwd } = this.options;
12541
- paths.forEach((path26) => {
12542
- if (!sp2.isAbsolute(path26) && !this._closers.has(path26)) {
13674
+ paths.forEach((path27) => {
13675
+ if (!sp2.isAbsolute(path27) && !this._closers.has(path27)) {
12543
13676
  if (cwd)
12544
- path26 = sp2.join(cwd, path26);
12545
- path26 = sp2.resolve(path26);
13677
+ path27 = sp2.join(cwd, path27);
13678
+ path27 = sp2.resolve(path27);
12546
13679
  }
12547
- this._closePath(path26);
12548
- this._addIgnoredPath(path26);
12549
- if (this._watched.has(path26)) {
13680
+ this._closePath(path27);
13681
+ this._addIgnoredPath(path27);
13682
+ if (this._watched.has(path27)) {
12550
13683
  this._addIgnoredPath({
12551
- path: path26,
13684
+ path: path27,
12552
13685
  recursive: true
12553
13686
  });
12554
13687
  }
@@ -12612,38 +13745,38 @@ var FSWatcher = class extends EventEmitter2 {
12612
13745
  * @param stats arguments to be passed with event
12613
13746
  * @returns the error if defined, otherwise the value of the FSWatcher instance's `closed` flag
12614
13747
  */
12615
- async _emit(event, path26, stats) {
13748
+ async _emit(event, path27, stats) {
12616
13749
  if (this.closed)
12617
13750
  return;
12618
13751
  const opts = this.options;
12619
13752
  if (isWindows)
12620
- path26 = sp2.normalize(path26);
13753
+ path27 = sp2.normalize(path27);
12621
13754
  if (opts.cwd)
12622
- path26 = sp2.relative(opts.cwd, path26);
12623
- const args = [path26];
13755
+ path27 = sp2.relative(opts.cwd, path27);
13756
+ const args = [path27];
12624
13757
  if (stats != null)
12625
13758
  args.push(stats);
12626
13759
  const awf = opts.awaitWriteFinish;
12627
13760
  let pw;
12628
- if (awf && (pw = this._pendingWrites.get(path26))) {
13761
+ if (awf && (pw = this._pendingWrites.get(path27))) {
12629
13762
  pw.lastChange = /* @__PURE__ */ new Date();
12630
13763
  return this;
12631
13764
  }
12632
13765
  if (opts.atomic) {
12633
13766
  if (event === EVENTS.UNLINK) {
12634
- this._pendingUnlinks.set(path26, [event, ...args]);
13767
+ this._pendingUnlinks.set(path27, [event, ...args]);
12635
13768
  setTimeout(() => {
12636
- this._pendingUnlinks.forEach((entry, path27) => {
13769
+ this._pendingUnlinks.forEach((entry, path28) => {
12637
13770
  this.emit(...entry);
12638
13771
  this.emit(EVENTS.ALL, ...entry);
12639
- this._pendingUnlinks.delete(path27);
13772
+ this._pendingUnlinks.delete(path28);
12640
13773
  });
12641
13774
  }, typeof opts.atomic === "number" ? opts.atomic : 100);
12642
13775
  return this;
12643
13776
  }
12644
- if (event === EVENTS.ADD && this._pendingUnlinks.has(path26)) {
13777
+ if (event === EVENTS.ADD && this._pendingUnlinks.has(path27)) {
12645
13778
  event = EVENTS.CHANGE;
12646
- this._pendingUnlinks.delete(path26);
13779
+ this._pendingUnlinks.delete(path27);
12647
13780
  }
12648
13781
  }
12649
13782
  if (awf && (event === EVENTS.ADD || event === EVENTS.CHANGE) && this._readyEmitted) {
@@ -12661,16 +13794,16 @@ var FSWatcher = class extends EventEmitter2 {
12661
13794
  this.emitWithAll(event, args);
12662
13795
  }
12663
13796
  };
12664
- this._awaitWriteFinish(path26, awf.stabilityThreshold, event, awfEmit);
13797
+ this._awaitWriteFinish(path27, awf.stabilityThreshold, event, awfEmit);
12665
13798
  return this;
12666
13799
  }
12667
13800
  if (event === EVENTS.CHANGE) {
12668
- const isThrottled = !this._throttle(EVENTS.CHANGE, path26, 50);
13801
+ const isThrottled = !this._throttle(EVENTS.CHANGE, path27, 50);
12669
13802
  if (isThrottled)
12670
13803
  return this;
12671
13804
  }
12672
13805
  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;
13806
+ const fullPath = opts.cwd ? sp2.join(opts.cwd, path27) : path27;
12674
13807
  let stats2;
12675
13808
  try {
12676
13809
  stats2 = await stat3(fullPath);
@@ -12701,23 +13834,23 @@ var FSWatcher = class extends EventEmitter2 {
12701
13834
  * @param timeout duration of time to suppress duplicate actions
12702
13835
  * @returns tracking object or false if action should be suppressed
12703
13836
  */
12704
- _throttle(actionType, path26, timeout) {
13837
+ _throttle(actionType, path27, timeout) {
12705
13838
  if (!this._throttled.has(actionType)) {
12706
13839
  this._throttled.set(actionType, /* @__PURE__ */ new Map());
12707
13840
  }
12708
13841
  const action = this._throttled.get(actionType);
12709
13842
  if (!action)
12710
13843
  throw new Error("invalid throttle");
12711
- const actionPath = action.get(path26);
13844
+ const actionPath = action.get(path27);
12712
13845
  if (actionPath) {
12713
13846
  actionPath.count++;
12714
13847
  return false;
12715
13848
  }
12716
13849
  let timeoutObject;
12717
13850
  const clear = () => {
12718
- const item = action.get(path26);
13851
+ const item = action.get(path27);
12719
13852
  const count = item ? item.count : 0;
12720
- action.delete(path26);
13853
+ action.delete(path27);
12721
13854
  clearTimeout(timeoutObject);
12722
13855
  if (item)
12723
13856
  clearTimeout(item.timeoutObject);
@@ -12725,7 +13858,7 @@ var FSWatcher = class extends EventEmitter2 {
12725
13858
  };
12726
13859
  timeoutObject = setTimeout(clear, timeout);
12727
13860
  const thr = { timeoutObject, clear, count: 0 };
12728
- action.set(path26, thr);
13861
+ action.set(path27, thr);
12729
13862
  return thr;
12730
13863
  }
12731
13864
  _incrReadyCount() {
@@ -12739,44 +13872,44 @@ var FSWatcher = class extends EventEmitter2 {
12739
13872
  * @param event
12740
13873
  * @param awfEmit Callback to be called when ready for event to be emitted.
12741
13874
  */
12742
- _awaitWriteFinish(path26, threshold, event, awfEmit) {
13875
+ _awaitWriteFinish(path27, threshold, event, awfEmit) {
12743
13876
  const awf = this.options.awaitWriteFinish;
12744
13877
  if (typeof awf !== "object")
12745
13878
  return;
12746
13879
  const pollInterval = awf.pollInterval;
12747
13880
  let timeoutHandler;
12748
- let fullPath = path26;
12749
- if (this.options.cwd && !sp2.isAbsolute(path26)) {
12750
- fullPath = sp2.join(this.options.cwd, path26);
13881
+ let fullPath = path27;
13882
+ if (this.options.cwd && !sp2.isAbsolute(path27)) {
13883
+ fullPath = sp2.join(this.options.cwd, path27);
12751
13884
  }
12752
13885
  const now = /* @__PURE__ */ new Date();
12753
13886
  const writes = this._pendingWrites;
12754
13887
  function awaitWriteFinishFn(prevStat) {
12755
13888
  statcb(fullPath, (err, curStat) => {
12756
- if (err || !writes.has(path26)) {
13889
+ if (err || !writes.has(path27)) {
12757
13890
  if (err && err.code !== "ENOENT")
12758
13891
  awfEmit(err);
12759
13892
  return;
12760
13893
  }
12761
13894
  const now2 = Number(/* @__PURE__ */ new Date());
12762
13895
  if (prevStat && curStat.size !== prevStat.size) {
12763
- writes.get(path26).lastChange = now2;
13896
+ writes.get(path27).lastChange = now2;
12764
13897
  }
12765
- const pw = writes.get(path26);
13898
+ const pw = writes.get(path27);
12766
13899
  const df = now2 - pw.lastChange;
12767
13900
  if (df >= threshold) {
12768
- writes.delete(path26);
13901
+ writes.delete(path27);
12769
13902
  awfEmit(void 0, curStat);
12770
13903
  } else {
12771
13904
  timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval, curStat);
12772
13905
  }
12773
13906
  });
12774
13907
  }
12775
- if (!writes.has(path26)) {
12776
- writes.set(path26, {
13908
+ if (!writes.has(path27)) {
13909
+ writes.set(path27, {
12777
13910
  lastChange: now,
12778
13911
  cancelWait: () => {
12779
- writes.delete(path26);
13912
+ writes.delete(path27);
12780
13913
  clearTimeout(timeoutHandler);
12781
13914
  return event;
12782
13915
  }
@@ -12787,8 +13920,8 @@ var FSWatcher = class extends EventEmitter2 {
12787
13920
  /**
12788
13921
  * Determines whether user has asked to ignore this path.
12789
13922
  */
12790
- _isIgnored(path26, stats) {
12791
- if (this.options.atomic && DOT_RE.test(path26))
13923
+ _isIgnored(path27, stats) {
13924
+ if (this.options.atomic && DOT_RE.test(path27))
12792
13925
  return true;
12793
13926
  if (!this._userIgnored) {
12794
13927
  const { cwd } = this.options;
@@ -12798,17 +13931,17 @@ var FSWatcher = class extends EventEmitter2 {
12798
13931
  const list = [...ignoredPaths.map(normalizeIgnored(cwd)), ...ignored];
12799
13932
  this._userIgnored = anymatch(list, void 0);
12800
13933
  }
12801
- return this._userIgnored(path26, stats);
13934
+ return this._userIgnored(path27, stats);
12802
13935
  }
12803
- _isntIgnored(path26, stat4) {
12804
- return !this._isIgnored(path26, stat4);
13936
+ _isntIgnored(path27, stat4) {
13937
+ return !this._isIgnored(path27, stat4);
12805
13938
  }
12806
13939
  /**
12807
13940
  * Provides a set of common helpers and properties relating to symlink handling.
12808
13941
  * @param path file or directory pattern being watched
12809
13942
  */
12810
- _getWatchHelpers(path26) {
12811
- return new WatchHelper(path26, this.options.followSymlinks, this);
13943
+ _getWatchHelpers(path27) {
13944
+ return new WatchHelper(path27, this.options.followSymlinks, this);
12812
13945
  }
12813
13946
  // Directory helpers
12814
13947
  // -----------------
@@ -12840,63 +13973,63 @@ var FSWatcher = class extends EventEmitter2 {
12840
13973
  * @param item base path of item/directory
12841
13974
  */
12842
13975
  _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))
13976
+ const path27 = sp2.join(directory, item);
13977
+ const fullPath = sp2.resolve(path27);
13978
+ isDirectory = isDirectory != null ? isDirectory : this._watched.has(path27) || this._watched.has(fullPath);
13979
+ if (!this._throttle("remove", path27, 100))
12847
13980
  return;
12848
13981
  if (!isDirectory && this._watched.size === 1) {
12849
13982
  this.add(directory, item, true);
12850
13983
  }
12851
- const wp = this._getWatchedDir(path26);
13984
+ const wp = this._getWatchedDir(path27);
12852
13985
  const nestedDirectoryChildren = wp.getChildren();
12853
- nestedDirectoryChildren.forEach((nested) => this._remove(path26, nested));
13986
+ nestedDirectoryChildren.forEach((nested) => this._remove(path27, nested));
12854
13987
  const parent = this._getWatchedDir(directory);
12855
13988
  const wasTracked = parent.has(item);
12856
13989
  parent.remove(item);
12857
13990
  if (this._symlinkPaths.has(fullPath)) {
12858
13991
  this._symlinkPaths.delete(fullPath);
12859
13992
  }
12860
- let relPath = path26;
13993
+ let relPath = path27;
12861
13994
  if (this.options.cwd)
12862
- relPath = sp2.relative(this.options.cwd, path26);
13995
+ relPath = sp2.relative(this.options.cwd, path27);
12863
13996
  if (this.options.awaitWriteFinish && this._pendingWrites.has(relPath)) {
12864
13997
  const event = this._pendingWrites.get(relPath).cancelWait();
12865
13998
  if (event === EVENTS.ADD)
12866
13999
  return;
12867
14000
  }
12868
- this._watched.delete(path26);
14001
+ this._watched.delete(path27);
12869
14002
  this._watched.delete(fullPath);
12870
14003
  const eventName = isDirectory ? EVENTS.UNLINK_DIR : EVENTS.UNLINK;
12871
- if (wasTracked && !this._isIgnored(path26))
12872
- this._emit(eventName, path26);
12873
- this._closePath(path26);
14004
+ if (wasTracked && !this._isIgnored(path27))
14005
+ this._emit(eventName, path27);
14006
+ this._closePath(path27);
12874
14007
  }
12875
14008
  /**
12876
14009
  * Closes all watchers for a path
12877
14010
  */
12878
- _closePath(path26) {
12879
- this._closeFile(path26);
12880
- const dir = sp2.dirname(path26);
12881
- this._getWatchedDir(dir).remove(sp2.basename(path26));
14011
+ _closePath(path27) {
14012
+ this._closeFile(path27);
14013
+ const dir = sp2.dirname(path27);
14014
+ this._getWatchedDir(dir).remove(sp2.basename(path27));
12882
14015
  }
12883
14016
  /**
12884
14017
  * Closes only file-specific watchers
12885
14018
  */
12886
- _closeFile(path26) {
12887
- const closers = this._closers.get(path26);
14019
+ _closeFile(path27) {
14020
+ const closers = this._closers.get(path27);
12888
14021
  if (!closers)
12889
14022
  return;
12890
14023
  closers.forEach((closer) => closer());
12891
- this._closers.delete(path26);
14024
+ this._closers.delete(path27);
12892
14025
  }
12893
- _addPathCloser(path26, closer) {
14026
+ _addPathCloser(path27, closer) {
12894
14027
  if (!closer)
12895
14028
  return;
12896
- let list = this._closers.get(path26);
14029
+ let list = this._closers.get(path27);
12897
14030
  if (!list) {
12898
14031
  list = [];
12899
- this._closers.set(path26, list);
14032
+ this._closers.set(path27, list);
12900
14033
  }
12901
14034
  list.push(closer);
12902
14035
  }
@@ -12926,7 +14059,7 @@ function watch(paths, options = {}) {
12926
14059
  var chokidar_default = { watch, FSWatcher };
12927
14060
 
12928
14061
  // src/watcher/file-watcher.ts
12929
- import * as path21 from "path";
14062
+ import * as path22 from "path";
12930
14063
  var FileWatcher = class {
12931
14064
  watcher = null;
12932
14065
  projectRoot;
@@ -12937,6 +14070,8 @@ var FileWatcher = class {
12937
14070
  debounceTimer = null;
12938
14071
  debounceMs = 1e3;
12939
14072
  onChanges = null;
14073
+ readyPromise = null;
14074
+ resolveReady = null;
12940
14075
  constructor(projectRoot, config, host = "opencode", options = {}) {
12941
14076
  this.projectRoot = projectRoot;
12942
14077
  this.config = config;
@@ -12952,15 +14087,15 @@ var FileWatcher = class {
12952
14087
  const watchTargets = this.configPath ? [this.projectRoot, this.configPath] : this.projectRoot;
12953
14088
  this.watcher = chokidar_default.watch(watchTargets, {
12954
14089
  ignored: (filePath) => {
12955
- const relativePath = path21.relative(this.projectRoot, filePath);
14090
+ const relativePath = path22.relative(this.projectRoot, filePath);
12956
14091
  if (!relativePath) return false;
12957
14092
  if (this.isProjectConfigPathOrAncestor(relativePath)) {
12958
14093
  return false;
12959
14094
  }
12960
- if (hasFilteredPathSegment(relativePath, path21.sep)) {
14095
+ if (hasFilteredPathSegment(relativePath, path22.sep)) {
12961
14096
  return true;
12962
14097
  }
12963
- if (isRestrictedDirectory(relativePath, path21.sep)) {
14098
+ if (isRestrictedDirectory(relativePath, path22.sep)) {
12964
14099
  return true;
12965
14100
  }
12966
14101
  if (ignoreFilter.ignores(relativePath)) {
@@ -12975,6 +14110,13 @@ var FileWatcher = class {
12975
14110
  pollInterval: 100
12976
14111
  }
12977
14112
  });
14113
+ this.readyPromise = new Promise((resolve15) => {
14114
+ this.resolveReady = resolve15;
14115
+ });
14116
+ this.watcher.once("ready", () => {
14117
+ this.resolveReady?.();
14118
+ this.resolveReady = null;
14119
+ });
12978
14120
  this.watcher.on("error", (error) => {
12979
14121
  const err = error instanceof Error ? error : null;
12980
14122
  if (err?.code === "EPERM" || err?.code === "EACCES") {
@@ -13006,24 +14148,24 @@ var FileWatcher = class {
13006
14148
  this.scheduleFlush();
13007
14149
  }
13008
14150
  isProjectConfigPath(filePath) {
13009
- const relativePath = path21.relative(this.projectRoot, filePath);
13010
- const normalizedRelativePath = path21.normalize(relativePath);
14151
+ const relativePath = path22.relative(this.projectRoot, filePath);
14152
+ const normalizedRelativePath = path22.normalize(relativePath);
13011
14153
  return this.getProjectConfigRelativePaths().some((configPath) => configPath === normalizedRelativePath);
13012
14154
  }
13013
14155
  isProjectConfigPathOrAncestor(relativePath) {
13014
- const normalizedRelativePath = path21.normalize(relativePath);
14156
+ const normalizedRelativePath = path22.normalize(relativePath);
13015
14157
  return this.getProjectConfigRelativePaths().some(
13016
- (configPath) => configPath === normalizedRelativePath || configPath.startsWith(`${normalizedRelativePath}${path21.sep}`)
14158
+ (configPath) => configPath === normalizedRelativePath || configPath.startsWith(`${normalizedRelativePath}${path22.sep}`)
13017
14159
  );
13018
14160
  }
13019
14161
  getProjectConfigRelativePaths() {
13020
14162
  if (this.configPath) {
13021
- return [path21.normalize(path21.relative(this.projectRoot, this.configPath))];
14163
+ return [path22.normalize(path22.relative(this.projectRoot, this.configPath))];
13022
14164
  }
13023
14165
  return [
13024
14166
  resolveProjectConfigPath(this.projectRoot, this.host),
13025
14167
  resolveWritableProjectConfigPath(this.projectRoot, this.host)
13026
- ].map((configPath) => path21.normalize(path21.relative(this.projectRoot, configPath)));
14168
+ ].map((configPath) => path22.normalize(path22.relative(this.projectRoot, configPath)));
13027
14169
  }
13028
14170
  scheduleFlush() {
13029
14171
  if (this.debounceTimer) {
@@ -13038,7 +14180,7 @@ var FileWatcher = class {
13038
14180
  return;
13039
14181
  }
13040
14182
  const changes = Array.from(this.pendingChanges.entries()).map(
13041
- ([path26, type]) => ({ path: path26, type })
14183
+ ([path27, type]) => ({ path: path27, type })
13042
14184
  );
13043
14185
  this.pendingChanges.clear();
13044
14186
  try {
@@ -13047,25 +14189,32 @@ var FileWatcher = class {
13047
14189
  console.error("Error handling file changes:", error);
13048
14190
  }
13049
14191
  }
13050
- stop() {
14192
+ async stop() {
13051
14193
  if (this.debounceTimer) {
13052
14194
  clearTimeout(this.debounceTimer);
13053
14195
  this.debounceTimer = null;
13054
14196
  }
13055
14197
  if (this.watcher) {
13056
- this.watcher.close();
14198
+ const watcher = this.watcher;
13057
14199
  this.watcher = null;
14200
+ await watcher.close();
13058
14201
  }
14202
+ this.resolveReady?.();
14203
+ this.resolveReady = null;
14204
+ this.readyPromise = null;
13059
14205
  this.pendingChanges.clear();
13060
14206
  this.onChanges = null;
13061
14207
  }
13062
14208
  isRunning() {
13063
14209
  return this.watcher !== null;
13064
14210
  }
14211
+ async waitUntilReady() {
14212
+ await (this.readyPromise ?? Promise.resolve());
14213
+ }
13065
14214
  };
13066
14215
 
13067
14216
  // src/watcher/git-head-watcher.ts
13068
- import * as path22 from "path";
14217
+ import * as path23 from "path";
13069
14218
  var GitHeadWatcher = class {
13070
14219
  watcher = null;
13071
14220
  projectRoot;
@@ -13087,7 +14236,7 @@ var GitHeadWatcher = class {
13087
14236
  this.onBranchChange = handler;
13088
14237
  this.currentBranch = getCurrentBranch(this.projectRoot);
13089
14238
  const headPath = getHeadPath(this.projectRoot);
13090
- const refsPath = path22.join(this.projectRoot, ".git", "refs", "heads");
14239
+ const refsPath = path23.join(this.projectRoot, ".git", "refs", "heads");
13091
14240
  this.watcher = chokidar_default.watch([headPath, refsPath], {
13092
14241
  persistent: true,
13093
14242
  ignoreInitial: true,
@@ -13124,14 +14273,15 @@ var GitHeadWatcher = class {
13124
14273
  getCurrentBranch() {
13125
14274
  return this.currentBranch;
13126
14275
  }
13127
- stop() {
14276
+ async stop() {
13128
14277
  if (this.debounceTimer) {
13129
14278
  clearTimeout(this.debounceTimer);
13130
14279
  this.debounceTimer = null;
13131
14280
  }
13132
14281
  if (this.watcher) {
13133
- this.watcher.close();
14282
+ const watcher = this.watcher;
13134
14283
  this.watcher = null;
14284
+ await watcher.close();
13135
14285
  }
13136
14286
  this.onBranchChange = null;
13137
14287
  }
@@ -13149,6 +14299,8 @@ var BackgroundReindexer = class {
13149
14299
  running = false;
13150
14300
  pending = false;
13151
14301
  stopped = false;
14302
+ retryTimer = null;
14303
+ retryDelayMs = 50;
13152
14304
  request() {
13153
14305
  if (this.stopped) {
13154
14306
  return;
@@ -13159,9 +14311,13 @@ var BackgroundReindexer = class {
13159
14311
  stop() {
13160
14312
  this.stopped = true;
13161
14313
  this.pending = false;
14314
+ if (this.retryTimer) {
14315
+ clearTimeout(this.retryTimer);
14316
+ this.retryTimer = null;
14317
+ }
13162
14318
  }
13163
14319
  drain() {
13164
- if (this.stopped || this.running || !this.pending) {
14320
+ if (this.stopped || this.running || this.retryTimer || !this.pending) {
13165
14321
  return;
13166
14322
  }
13167
14323
  this.pending = false;
@@ -13169,13 +14325,29 @@ var BackgroundReindexer = class {
13169
14325
  void this.run();
13170
14326
  }
13171
14327
  async run() {
14328
+ let retryAfterContention = false;
13172
14329
  try {
13173
14330
  await this.runIndex();
14331
+ this.retryDelayMs = 50;
13174
14332
  } catch (error) {
13175
- console.error("[codebase-index] Background reindex failed:", error);
14333
+ if (isTransientIndexLockContention(error)) {
14334
+ this.pending = true;
14335
+ retryAfterContention = true;
14336
+ } else {
14337
+ console.error("[codebase-index] Background reindex failed:", error);
14338
+ }
13176
14339
  } finally {
13177
14340
  this.running = false;
13178
- this.drain();
14341
+ if (retryAfterContention && !this.stopped) {
14342
+ const delay = this.retryDelayMs;
14343
+ this.retryDelayMs = Math.min(this.retryDelayMs * 2, 500);
14344
+ this.retryTimer = setTimeout(() => {
14345
+ this.retryTimer = null;
14346
+ this.drain();
14347
+ }, delay);
14348
+ } else {
14349
+ this.drain();
14350
+ }
13179
14351
  }
13180
14352
  }
13181
14353
  };
@@ -13209,10 +14381,12 @@ function createWatcherWithIndexer(getIndexer, projectRoot, config, host = "openc
13209
14381
  return {
13210
14382
  fileWatcher,
13211
14383
  gitWatcher,
13212
- stop() {
14384
+ whenReady() {
14385
+ return fileWatcher.waitUntilReady();
14386
+ },
14387
+ async stop() {
13213
14388
  backgroundReindexer.stop();
13214
- fileWatcher.stop();
13215
- gitWatcher?.stop();
14389
+ await Promise.all([fileWatcher.stop(), gitWatcher?.stop()]);
13216
14390
  }
13217
14391
  };
13218
14392
  }
@@ -13231,7 +14405,7 @@ function getConfigPaths(projectRoot, host, options) {
13231
14405
 
13232
14406
  // src/tools/visualize/activity.ts
13233
14407
  import { execFileSync } from "child_process";
13234
- import * as path23 from "path";
14408
+ import * as path24 from "path";
13235
14409
  function attachRecentActivity(data, projectRoot) {
13236
14410
  const activity = readGitActivity(projectRoot);
13237
14411
  const changes = activity.size > 0 ? buildGitChanges(data, activity, projectRoot) : buildGraphChanges(data);
@@ -13393,7 +14567,7 @@ function normalizePath3(filePath) {
13393
14567
  return filePath.replace(/\\/g, "/");
13394
14568
  }
13395
14569
  function toGitRelativePath(projectRoot, filePath) {
13396
- const relativePath = path23.isAbsolute(filePath) ? path23.relative(projectRoot, filePath) : filePath;
14570
+ const relativePath = path24.isAbsolute(filePath) ? path24.relative(projectRoot, filePath) : filePath;
13397
14571
  return normalizePath3(relativePath);
13398
14572
  }
13399
14573
 
@@ -13651,7 +14825,7 @@ render();
13651
14825
  }
13652
14826
 
13653
14827
  // src/tools/visualize/transform.ts
13654
- import * as path24 from "path";
14828
+ import * as path25 from "path";
13655
14829
 
13656
14830
  // src/tools/visualize/modules.ts
13657
14831
  var MAX_MODULES = 18;
@@ -13911,7 +15085,7 @@ function transformForVisualization(symbols, edges, options = {}) {
13911
15085
  filePath: s.filePath,
13912
15086
  kind: s.kind,
13913
15087
  line: s.startLine,
13914
- directory: path24.dirname(s.filePath),
15088
+ directory: path25.dirname(s.filePath),
13915
15089
  moduleId: "",
13916
15090
  moduleLabel: ""
13917
15091
  }));
@@ -13939,9 +15113,9 @@ function parseArgs(argv) {
13939
15113
  let host = "opencode";
13940
15114
  for (let i = 2; i < argv.length; i++) {
13941
15115
  if (argv[i] === "--project" && argv[i + 1]) {
13942
- project = path25.resolve(argv[++i]);
15116
+ project = path26.resolve(argv[++i]);
13943
15117
  } else if (argv[i] === "--config" && argv[i + 1]) {
13944
- config = path25.resolve(argv[++i]);
15118
+ config = path26.resolve(argv[++i]);
13945
15119
  } else if (argv[i] === "--host" && argv[i + 1]) {
13946
15120
  host = parseHostMode(argv[++i]);
13947
15121
  } else if (argv[i] === "--host") {
@@ -13954,7 +15128,7 @@ function loadCliRawConfig(args) {
13954
15128
  return args.config ? loadConfigFile(args.config) : loadMergedConfig(args.project, args.host);
13955
15129
  }
13956
15130
  function isCliEntrypoint(moduleUrl, argvPath) {
13957
- return argvPath !== void 0 && realpathSync2(fileURLToPath2(moduleUrl)) === realpathSync2(argvPath);
15131
+ return argvPath !== void 0 && realpathSync3(fileURLToPath2(moduleUrl)) === realpathSync3(argvPath);
13958
15132
  }
13959
15133
  function parseVisualizeArgs(argv, cwd) {
13960
15134
  let project = cwd;
@@ -13964,7 +15138,7 @@ function parseVisualizeArgs(argv, cwd) {
13964
15138
  for (let i = 0; i < argv.length; i++) {
13965
15139
  const arg = argv[i];
13966
15140
  if (arg === "--project" && argv[i + 1]) {
13967
- project = path25.resolve(argv[++i]);
15141
+ project = path26.resolve(argv[++i]);
13968
15142
  } else if (arg === "--max" && argv[i + 1]) {
13969
15143
  maxNodes = Number(argv[++i]);
13970
15144
  } else if (arg.startsWith("--max=") || arg.startsWith("max=")) {
@@ -14001,8 +15175,8 @@ async function handleVisualizeCommand(argv, cwd) {
14001
15175
  console.error("No connected symbols found. Retry with: npm run visualize -- orphans");
14002
15176
  return 1;
14003
15177
  }
14004
- const outputPath = path25.join(os5.tmpdir(), `call-graph-${Date.now()}.html`);
14005
- writeFileSync6(outputPath, generateVisualizationHtml(vizData), "utf-8");
15178
+ const outputPath = path26.join(os6.tmpdir(), `call-graph-${Date.now()}.html`);
15179
+ writeFileSync7(outputPath, generateVisualizationHtml(vizData), "utf-8");
14006
15180
  console.log(`Temporal call graph visualization generated: ${outputPath}`);
14007
15181
  console.log(`Nodes: ${vizData.nodes.length} | Edges: ${vizData.edges.length}`);
14008
15182
  console.log(`Recent change lenses: ${vizData.changes?.length ?? 0}`);
@@ -14031,7 +15205,7 @@ async function main() {
14031
15205
  const transport = new StdioServerTransport();
14032
15206
  await server.connect(transport);
14033
15207
  let watcher = null;
14034
- const isHomeDir = path25.resolve(args.project) === path25.resolve(os5.homedir());
15208
+ const isHomeDir = path26.resolve(args.project) === path26.resolve(os6.homedir());
14035
15209
  const isValidProject = !isHomeDir && (!config.indexing.requireProjectMarker || hasProjectMarker(args.project));
14036
15210
  if (config.indexing.autoIndex && isValidProject) {
14037
15211
  const indexer = getIndexerForProject(args.project, args.host);
@@ -14046,14 +15220,25 @@ async function main() {
14046
15220
  args.config ? { configPath: args.config } : {}
14047
15221
  );
14048
15222
  }
14049
- const shutdown = () => {
14050
- watcher?.stop();
14051
- server.close().catch(() => {
14052
- });
14053
- process.exit(0);
15223
+ let shuttingDown = false;
15224
+ const shutdown = async () => {
15225
+ if (shuttingDown) return;
15226
+ shuttingDown = true;
15227
+ try {
15228
+ await watcher?.stop();
15229
+ await server.close();
15230
+ process.exit(0);
15231
+ } catch (error) {
15232
+ console.error("Failed to stop MCP server cleanly:", error);
15233
+ process.exit(1);
15234
+ }
14054
15235
  };
14055
- process.on("SIGINT", shutdown);
14056
- process.on("SIGTERM", shutdown);
15236
+ process.on("SIGINT", () => {
15237
+ void shutdown();
15238
+ });
15239
+ process.on("SIGTERM", () => {
15240
+ void shutdown();
15241
+ });
14057
15242
  }
14058
15243
  function handleMainError(error) {
14059
15244
  if (error instanceof Error && error.message.startsWith("Invalid host mode")) {