opencode-codebase-index 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -724,6 +724,17 @@ var EMBEDDING_MODELS = {
724
724
  maxTokens: 2048,
725
725
  costPer1MTokens: 0.15,
726
726
  taskAble: true
727
+ },
728
+ "gemini-embedding-2": {
729
+ provider: "google",
730
+ model: "gemini-embedding-2",
731
+ // Keep a conservative, testable default embedding dimension. Gemini Embedding 2 supports
732
+ // flexible dimensions via outputDimensionality.
733
+ dimensions: 1536,
734
+ maxTokens: 8192,
735
+ costPer1MTokens: 0.15,
736
+ taskAble: false,
737
+ promptStyle: "embedding-2"
727
738
  }
728
739
  },
729
740
  "openai": {
@@ -757,26 +768,15 @@ var EMBEDDING_MODELS = {
757
768
  maxTokens: 512,
758
769
  costPer1MTokens: 0
759
770
  }
760
- },
761
- "github-copilot": {
762
- "text-embedding-3-small": {
763
- provider: "github-copilot",
764
- model: "text-embedding-3-small",
765
- dimensions: 1536,
766
- maxTokens: 8191,
767
- costPer1MTokens: 0
768
- }
769
771
  }
770
772
  };
771
773
  var DEFAULT_PROVIDER_MODELS = {
772
- "github-copilot": "text-embedding-3-small",
773
774
  "openai": "text-embedding-3-small",
774
775
  "google": "gemini-embedding-001",
775
776
  "ollama": "nomic-embed-text"
776
777
  };
777
778
  var AUTO_DETECT_PROVIDER_ORDER = [
778
779
  "ollama",
779
- "github-copilot",
780
780
  "openai",
781
781
  "google"
782
782
  ];
@@ -802,6 +802,9 @@ function getDefaultIndexingConfig() {
802
802
  maxDepth: 5,
803
803
  maxFilesPerDirectory: 100,
804
804
  fallbackToTextOnMaxChunks: true,
805
+ // Must stay in sync with DEFAULT_LINES_PER_CHUNK in native/src/lib.rs (the napi
806
+ // fallback used when a native caller omits the argument).
807
+ linesPerChunk: 30,
805
808
  gitBlame: { enabled: false }
806
809
  };
807
810
  }
@@ -935,6 +938,7 @@ function parseConfig(raw) {
935
938
  maxDepth: typeof rawIndexing.maxDepth === "number" ? rawIndexing.maxDepth < -1 ? -1 : rawIndexing.maxDepth : defaultIndexing.maxDepth,
936
939
  maxFilesPerDirectory: typeof rawIndexing.maxFilesPerDirectory === "number" ? Math.max(1, rawIndexing.maxFilesPerDirectory) : defaultIndexing.maxFilesPerDirectory,
937
940
  fallbackToTextOnMaxChunks: typeof rawIndexing.fallbackToTextOnMaxChunks === "boolean" ? rawIndexing.fallbackToTextOnMaxChunks : defaultIndexing.fallbackToTextOnMaxChunks,
941
+ linesPerChunk: typeof rawIndexing.linesPerChunk === "number" && Number.isFinite(rawIndexing.linesPerChunk) ? Math.min(Math.max(1, Math.floor(rawIndexing.linesPerChunk)), 4294967295) : defaultIndexing.linesPerChunk,
938
942
  gitBlame: {
939
943
  enabled: rawIndexing.gitBlame && typeof rawIndexing.gitBlame === "object" && typeof rawIndexing.gitBlame.enabled === "boolean" ? rawIndexing.gitBlame.enabled : defaultIndexing.gitBlame.enabled
940
944
  }
@@ -977,6 +981,7 @@ function parseConfig(raw) {
977
981
  let embeddingModel;
978
982
  let customProvider;
979
983
  let reranker;
984
+ const githubCopilotDeprecationMessage = '`embeddingProvider: "github-copilot"` is deprecated and no longer available. Migrate existing configs to `embeddingProvider: "google"` and select an explicit Google model. For existing indexes, run `index_codebase` with `force: true` after changing to `gemini-embedding-001` or `gemini-embedding-2` to rebuild embeddings. See docs/configuration.md for details.';
980
985
  if (embeddingProviderValue === "custom") {
981
986
  embeddingProvider = "custom";
982
987
  const rawCustom = input.customProvider && typeof input.customProvider === "object" ? input.customProvider : null;
@@ -1016,6 +1021,8 @@ function parseConfig(raw) {
1016
1021
  } else if (rawEmbeddingModel) {
1017
1022
  embeddingModel = DEFAULT_PROVIDER_MODELS[embeddingProvider];
1018
1023
  }
1024
+ } else if (embeddingProviderValue === "github-copilot") {
1025
+ throw new Error(githubCopilotDeprecationMessage);
1019
1026
  } else {
1020
1027
  embeddingProvider = "auto";
1021
1028
  }
@@ -1046,10 +1053,21 @@ function parseConfig(raw) {
1046
1053
  timeoutMs: typeof rawReranker.timeoutMs === "number" ? Math.max(1e3, Math.floor(rawReranker.timeoutMs)) : 1e4
1047
1054
  };
1048
1055
  }
1056
+ const rawEmbedding = input.embedding && typeof input.embedding === "object" ? input.embedding : {};
1057
+ const rawEmbeddingBatch = rawEmbedding.batch && typeof rawEmbedding.batch === "object" ? rawEmbedding.batch : null;
1058
+ const embeddingMaxBatchItems = typeof rawEmbeddingBatch?.maxBatchItems === "number" && Number.isFinite(rawEmbeddingBatch.maxBatchItems) ? Math.max(1, Math.floor(rawEmbeddingBatch.maxBatchItems)) : void 0;
1059
+ const embeddingMaxBatchTokens = typeof rawEmbeddingBatch?.maxBatchTokens === "number" && Number.isFinite(rawEmbeddingBatch.maxBatchTokens) ? Math.max(1, Math.floor(rawEmbeddingBatch.maxBatchTokens)) : void 0;
1060
+ const embedding = embeddingMaxBatchItems !== void 0 || embeddingMaxBatchTokens !== void 0 ? {
1061
+ batch: {
1062
+ ...embeddingMaxBatchItems !== void 0 ? { maxBatchItems: embeddingMaxBatchItems } : {},
1063
+ ...embeddingMaxBatchTokens !== void 0 ? { maxBatchTokens: embeddingMaxBatchTokens } : {}
1064
+ }
1065
+ } : {};
1049
1066
  return {
1050
1067
  embeddingProvider,
1051
1068
  embeddingModel,
1052
1069
  customProvider,
1070
+ embedding,
1053
1071
  scope: isValidScope(scopeValue) ? scopeValue : "project",
1054
1072
  include: includeValue ?? DEFAULT_INCLUDE,
1055
1073
  exclude: excludeValue ?? DEFAULT_EXCLUDE,
@@ -2316,6 +2334,10 @@ function scoreCandidate(query, intent, candidate, originalIndex) {
2316
2334
  let boost = 0;
2317
2335
  if (intent.primary === "conceptual") {
2318
2336
  boost += Math.min(0.14, overlap * 0.14);
2337
+ if (intent.preferSourcePaths) {
2338
+ boost += implementationPath ? 0.32 : 0;
2339
+ if (testPath || fixturePath || docsPath) boost -= 0.35;
2340
+ }
2319
2341
  if (generatedOrVendor) boost -= 0.18;
2320
2342
  if (importChunk || weakContainer) boost -= 0.04;
2321
2343
  } else if (intent.primary === "test") {
@@ -3283,6 +3305,19 @@ function parseOwner(value) {
3283
3305
  if (typeof candidate.startedAt !== "string" || Number.isNaN(Date.parse(candidate.startedAt))) return null;
3284
3306
  if (typeof candidate.operation !== "string" || !VALID_OPERATIONS.has(candidate.operation)) return null;
3285
3307
  if (typeof candidate.token !== "string" || !UUID_PATTERN.test(candidate.token)) return null;
3308
+ if (candidate.recoveryProtocolVersion !== void 0 && candidate.recoveryProtocolVersion !== 1) return null;
3309
+ if (candidate.projectRoot !== void 0 && typeof candidate.projectRoot !== "string") return null;
3310
+ if (candidate.scopedRoots !== void 0) {
3311
+ if (!Array.isArray(candidate.scopedRoots) || candidate.scopedRoots.some((root) => typeof root !== "string")) {
3312
+ return null;
3313
+ }
3314
+ }
3315
+ if (candidate.clearRecovery !== void 0) {
3316
+ const recovery = candidate.clearRecovery;
3317
+ if (typeof recovery !== "object" || recovery === null || recovery.phase !== "clearing" || typeof recovery.embeddingProvider !== "string" || recovery.embeddingProvider.length === 0 || typeof recovery.embeddingModel !== "string" || recovery.embeddingModel.length === 0 || !Number.isInteger(recovery.embeddingDimensions) || (recovery.embeddingDimensions ?? 0) <= 0 || typeof recovery.embeddingStrategyVersion !== "string" || recovery.embeddingStrategyVersion.length === 0 || recovery.compatibilityDecision !== "compatible" && recovery.compatibilityDecision !== "embedding-strategy-mismatch" && recovery.compatibilityDecision !== "incompatible" || candidate.operation !== "clear" && candidate.operation !== "force-index") {
3318
+ return null;
3319
+ }
3320
+ }
3286
3321
  return candidate;
3287
3322
  }
3288
3323
  function parseReclaimOwner(value) {
@@ -3523,13 +3558,18 @@ function isTransientIndexLockContention(error) {
3523
3558
  if (!isIndexLockContentionError(error) || !("reason" in error)) return false;
3524
3559
  return error.reason === "active" || error.reason === "reclaiming";
3525
3560
  }
3526
- function acquireIndexLock(indexPath, operation) {
3561
+ function acquireIndexLock(indexPath, operation, recoveryScope) {
3527
3562
  (0, import_fs5.mkdirSync)(indexPath, { recursive: true });
3528
3563
  const canonicalIndexPath = import_fs5.realpathSync.native(indexPath);
3529
3564
  const lockPath = path9.join(canonicalIndexPath, "indexing.lock");
3530
3565
  cleanupDeadPublicationCandidates(canonicalIndexPath);
3531
3566
  for (let attempt = 0; attempt < 6; attempt += 1) {
3532
- const owner = createOwner(operation);
3567
+ const owner = recoveryScope === void 0 ? createOwner(operation) : {
3568
+ ...createOwner(operation),
3569
+ recoveryProtocolVersion: 1,
3570
+ projectRoot: recoveryScope.projectRoot,
3571
+ scopedRoots: recoveryScope.scopedRoots
3572
+ };
3533
3573
  if (publishJsonDirectory(lockPath, owner)) {
3534
3574
  const lease = {
3535
3575
  canonicalIndexPath,
@@ -3594,6 +3634,33 @@ function releaseIndexLock(lease) {
3594
3634
  }
3595
3635
  return true;
3596
3636
  }
3637
+ function setIndexLockClearRecoveryState(lease, clearRecovery) {
3638
+ const currentOwner = readDirectoryOwner(lease.lockPath);
3639
+ if (!currentOwner || !sameOwner(currentOwner, lease.owner)) {
3640
+ throw new Error(`Lost ownership of index mutation lease ${lease.owner.token}`);
3641
+ }
3642
+ const nextOwner = { ...currentOwner };
3643
+ if (clearRecovery === null) {
3644
+ delete nextOwner.clearRecovery;
3645
+ } else {
3646
+ nextOwner.clearRecovery = clearRecovery;
3647
+ }
3648
+ const ownerPath = path9.join(lease.lockPath, OWNER_FILE_NAME);
3649
+ const temporaryPath = path9.join(
3650
+ lease.lockPath,
3651
+ `${OWNER_FILE_NAME}.tmp.${lease.owner.pid}.${lease.owner.token}.${(0, import_crypto.randomUUID)()}`
3652
+ );
3653
+ try {
3654
+ (0, import_fs5.writeFileSync)(temporaryPath, JSON.stringify(nextOwner), {
3655
+ encoding: "utf-8",
3656
+ flag: "wx",
3657
+ mode: 384
3658
+ });
3659
+ retryTransientFilesystemOperation(() => (0, import_fs5.renameSync)(temporaryPath, ownerPath));
3660
+ } finally {
3661
+ if ((0, import_fs5.existsSync)(temporaryPath)) (0, import_fs5.rmSync)(temporaryPath, { force: true });
3662
+ }
3663
+ }
3597
3664
  function createLeaseTemporaryPath(targetPath, owner, kind = "tmp") {
3598
3665
  if (kind === "bak") return `${targetPath}.bak.${owner.pid}.${owner.token}`;
3599
3666
  temporaryCounter += 1;
@@ -5737,8 +5804,6 @@ async function tryDetectProvider() {
5737
5804
  }
5738
5805
  async function getProviderCredentials(provider) {
5739
5806
  switch (provider) {
5740
- case "github-copilot":
5741
- return getGitHubCopilotCredentials();
5742
5807
  case "openai":
5743
5808
  return getOpenAICredentials();
5744
5809
  case "google":
@@ -5749,22 +5814,6 @@ async function getProviderCredentials(provider) {
5749
5814
  return null;
5750
5815
  }
5751
5816
  }
5752
- function getGitHubCopilotCredentials() {
5753
- const authData = loadOpenCodeAuth();
5754
- const copilotAuth = authData["github-copilot"] || authData["github-copilot-enterprise"];
5755
- if (!copilotAuth || copilotAuth.type !== "oauth") {
5756
- return null;
5757
- }
5758
- const auth = copilotAuth;
5759
- const baseUrl = auth.enterpriseUrl ? `https://copilot-api.${auth.enterpriseUrl.replace(/^https?:\/\//, "").replace(/\/$/, "")}` : "https://models.github.ai";
5760
- return {
5761
- provider: "github-copilot",
5762
- baseUrl,
5763
- refreshToken: copilotAuth.refresh,
5764
- accessToken: copilotAuth.access,
5765
- tokenExpires: copilotAuth.expires
5766
- };
5767
- }
5768
5817
  function getOpenAICredentials() {
5769
5818
  const authData = loadOpenCodeAuth();
5770
5819
  const openaiAuth = authData["openai"];
@@ -5890,8 +5939,6 @@ async function tryDetectOllamaProvider() {
5890
5939
  }
5891
5940
  function getProviderDisplayName(provider) {
5892
5941
  switch (provider) {
5893
- case "github-copilot":
5894
- return "GitHub Copilot";
5895
5942
  case "openai":
5896
5943
  return "OpenAI";
5897
5944
  case "google":
@@ -6116,44 +6163,6 @@ var CustomEmbeddingProvider = class extends BaseEmbeddingProvider {
6116
6163
  }
6117
6164
  };
6118
6165
 
6119
- // src/embeddings/providers/github-copilot.ts
6120
- var GitHubCopilotEmbeddingProvider = class extends BaseEmbeddingProvider {
6121
- constructor(credentials, modelInfo) {
6122
- super(credentials, modelInfo);
6123
- }
6124
- getToken() {
6125
- if (!this.credentials.refreshToken) {
6126
- throw new Error("No OAuth token available for GitHub");
6127
- }
6128
- return this.credentials.refreshToken;
6129
- }
6130
- async embedBatch(texts) {
6131
- const token = this.getToken();
6132
- const response = await fetch(`${this.credentials.baseUrl}/inference/embeddings`, {
6133
- method: "POST",
6134
- headers: {
6135
- Authorization: `Bearer ${token}`,
6136
- "Content-Type": "application/json",
6137
- Accept: "application/vnd.github+json",
6138
- "X-GitHub-Api-Version": "2022-11-28"
6139
- },
6140
- body: JSON.stringify({
6141
- model: `openai/${this.modelInfo.model}`,
6142
- input: texts
6143
- })
6144
- });
6145
- if (!response.ok) {
6146
- const error = (await response.text()).slice(0, 500);
6147
- throw new Error(`GitHub Copilot embedding API error: ${response.status} - ${error}`);
6148
- }
6149
- const data = await response.json();
6150
- return {
6151
- embeddings: data.data.map((d) => d.embedding),
6152
- totalTokensUsed: data.usage.total_tokens
6153
- };
6154
- }
6155
- };
6156
-
6157
6166
  // src/embeddings/providers/google.ts
6158
6167
  var GoogleEmbeddingProvider = class _GoogleEmbeddingProvider extends BaseEmbeddingProvider {
6159
6168
  static BATCH_SIZE = 20;
@@ -6161,24 +6170,30 @@ var GoogleEmbeddingProvider = class _GoogleEmbeddingProvider extends BaseEmbeddi
6161
6170
  super(credentials, modelInfo);
6162
6171
  }
6163
6172
  async embedQuery(query) {
6164
- const taskType = this.modelInfo.taskAble ? "CODE_RETRIEVAL_QUERY" : void 0;
6165
- const result = await this.embedWithTaskType([query], taskType);
6173
+ const taskType = this.modelInfo.model === "gemini-embedding-001" && this.modelInfo.taskAble ? "CODE_RETRIEVAL_QUERY" : void 0;
6174
+ const texts = [
6175
+ this.modelInfo.model === "gemini-embedding-2" ? `task: code retrieval | query: ${query}` : query
6176
+ ];
6177
+ const result = await this.embedWithTaskType(texts, taskType);
6166
6178
  return {
6167
6179
  embedding: result.embeddings[0],
6168
6180
  tokensUsed: result.totalTokensUsed
6169
6181
  };
6170
6182
  }
6171
6183
  async embedDocument(document) {
6172
- const taskType = this.modelInfo.taskAble ? "RETRIEVAL_DOCUMENT" : void 0;
6173
- const result = await this.embedWithTaskType([document], taskType);
6184
+ const taskType = this.modelInfo.model === "gemini-embedding-001" && this.modelInfo.taskAble ? "RETRIEVAL_DOCUMENT" : void 0;
6185
+ const result = await this.embedWithTaskType([
6186
+ this.modelInfo.model === "gemini-embedding-2" ? `title: none | text: ${document}` : document
6187
+ ], taskType);
6174
6188
  return {
6175
6189
  embedding: result.embeddings[0],
6176
6190
  tokensUsed: result.totalTokensUsed
6177
6191
  };
6178
6192
  }
6179
6193
  async embedBatch(texts) {
6180
- const taskType = this.modelInfo.taskAble ? "RETRIEVAL_DOCUMENT" : void 0;
6181
- return this.embedWithTaskType(texts, taskType);
6194
+ const taskType = this.modelInfo.model === "gemini-embedding-001" && this.modelInfo.taskAble ? "RETRIEVAL_DOCUMENT" : void 0;
6195
+ const formattedTexts = this.modelInfo.model === "gemini-embedding-2" ? texts.map((text) => `title: none | text: ${text}`) : texts;
6196
+ return this.embedWithTaskType(formattedTexts, taskType);
6182
6197
  }
6183
6198
  async embedWithTaskType(texts, taskType) {
6184
6199
  const batches = [];
@@ -6228,6 +6243,10 @@ var GoogleEmbeddingProvider = class _GoogleEmbeddingProvider extends BaseEmbeddi
6228
6243
  var OllamaEmbeddingProvider = class _OllamaEmbeddingProvider extends BaseEmbeddingProvider {
6229
6244
  static MIN_TRUNCATION_CHARS = 512;
6230
6245
  static REQUEST_TIMEOUT_MS = 12e4;
6246
+ // Set when /api/embed returns 404 so subsequent multi-text batches skip the
6247
+ // batched endpoint and go straight to the legacy per-text path (one probe per
6248
+ // old ollama install, not one probe per batch).
6249
+ batchEndpointUnavailable = false;
6231
6250
  constructor(credentials, modelInfo) {
6232
6251
  super(credentials, modelInfo);
6233
6252
  }
@@ -6245,6 +6264,21 @@ var OllamaEmbeddingProvider = class _OllamaEmbeddingProvider extends BaseEmbeddi
6245
6264
  const message = (error instanceof Error ? error.message : String(error)).toLowerCase();
6246
6265
  return message.includes("context length") && (message.includes("exceed") || message.includes("exceeded") || message.includes("too long")) || message.includes("input length exceeds the context length") || message.includes("context length exceeded");
6247
6266
  }
6267
+ // True for a 404 from the newer /api/embed endpoint, i.e. an ollama version that
6268
+ // does not provide it. embedBatch uses this to fall back to the legacy per-text
6269
+ // /api/embeddings path so old ollama installs do not regress.
6270
+ isBatchEndpointUnavailableError(error) {
6271
+ const message = error instanceof Error ? error.message : String(error);
6272
+ return message.includes("Ollama /api/embed not available");
6273
+ }
6274
+ // True for a malformed /api/embed response (wrong vector count or a bad vector).
6275
+ // embedBatch falls back to the per-text path on this so a bad batch response
6276
+ // re-embeds each text cleanly. A text that then fails per-text is not isolated
6277
+ // here; it is isolated on the recovery run, which re-embeds one text per request.
6278
+ isBatchValidationError(error) {
6279
+ const message = error instanceof Error ? error.message : String(error);
6280
+ return message.includes("invalid embedding batch");
6281
+ }
6248
6282
  buildTruncationCandidates(text) {
6249
6283
  const baseMaxChars = Math.max(1, this.modelInfo.maxTokens * 4);
6250
6284
  const candidateLimits = /* @__PURE__ */ new Set();
@@ -6346,7 +6380,74 @@ var OllamaEmbeddingProvider = class _OllamaEmbeddingProvider extends BaseEmbeddi
6346
6380
  tokensUsed: this.estimateTokens(text)
6347
6381
  };
6348
6382
  }
6349
- async embedBatch(texts) {
6383
+ // Embeds many texts in one POST /api/embed request (input: string[]). Ollama
6384
+ // encodes each input independently, so the model context length applies per input
6385
+ // (the upstream splitter already bounds each input), not over the batch. This
6386
+ // amortizes N HTTP round-trips into one.
6387
+ async embedMany(texts) {
6388
+ const controller = new AbortController();
6389
+ const timeout = setTimeout(
6390
+ () => controller.abort(),
6391
+ _OllamaEmbeddingProvider.REQUEST_TIMEOUT_MS
6392
+ );
6393
+ let response;
6394
+ try {
6395
+ response = await fetch(`${this.credentials.baseUrl}/api/embed`, {
6396
+ method: "POST",
6397
+ headers: {
6398
+ "Content-Type": "application/json"
6399
+ },
6400
+ body: JSON.stringify({
6401
+ model: this.modelInfo.model,
6402
+ input: texts,
6403
+ truncate: false
6404
+ }),
6405
+ signal: controller.signal
6406
+ });
6407
+ } catch (error) {
6408
+ if (error instanceof Error && error.name === "AbortError") {
6409
+ throw new Error(
6410
+ `Ollama embedding request timed out after ${_OllamaEmbeddingProvider.REQUEST_TIMEOUT_MS}ms`
6411
+ );
6412
+ }
6413
+ throw error;
6414
+ } finally {
6415
+ clearTimeout(timeout);
6416
+ }
6417
+ if (!response.ok) {
6418
+ const error = (await response.text()).slice(0, 500);
6419
+ if (response.status === 404) {
6420
+ throw new Error(`Ollama /api/embed not available: ${response.status} - ${error}`);
6421
+ }
6422
+ throw new Error(`Ollama embedding API error: ${response.status} - ${error}`);
6423
+ }
6424
+ let parsed;
6425
+ try {
6426
+ parsed = await response.json();
6427
+ } catch {
6428
+ throw new Error(
6429
+ `Ollama returned an invalid embedding batch; expected ${texts.length} vectors of ${this.modelInfo.dimensions} finite dimensions`
6430
+ );
6431
+ }
6432
+ const data = parsed && typeof parsed === "object" ? parsed : {};
6433
+ if (!Array.isArray(data.embeddings) || data.embeddings.length !== texts.length || data.embeddings.some(
6434
+ (value) => !Array.isArray(value) || value.length !== this.modelInfo.dimensions || value.some((v) => typeof v !== "number" || !Number.isFinite(v))
6435
+ )) {
6436
+ throw new Error(
6437
+ `Ollama returned an invalid embedding batch; expected ${texts.length} vectors of ${this.modelInfo.dimensions} finite dimensions`
6438
+ );
6439
+ }
6440
+ return {
6441
+ embeddings: data.embeddings,
6442
+ totalTokensUsed: texts.reduce((sum, text) => sum + this.estimateTokens(text), 0)
6443
+ };
6444
+ }
6445
+ // Per-text /api/embeddings path shared by the single-text fast path and the
6446
+ // batch fallback. Uses the legacy endpoint one text at a time, so each text gets
6447
+ // its own truncation safety net and a vector validated on its own. A text that
6448
+ // hard-fails per-text throws here and fails the whole request batch; the recovery
6449
+ // run re-embeds one text per request to isolate it.
6450
+ async embedOneByOne(texts) {
6350
6451
  const results = [];
6351
6452
  for (const text of texts) {
6352
6453
  results.push(await this.embedSingleWithFallback(text));
@@ -6356,6 +6457,26 @@ var OllamaEmbeddingProvider = class _OllamaEmbeddingProvider extends BaseEmbeddi
6356
6457
  totalTokensUsed: results.reduce((sum, r) => sum + r.tokensUsed, 0)
6357
6458
  };
6358
6459
  }
6460
+ async embedBatch(texts) {
6461
+ if (texts.length === 0) {
6462
+ return { embeddings: [], totalTokensUsed: 0 };
6463
+ }
6464
+ if (texts.length === 1 || this.batchEndpointUnavailable) {
6465
+ return this.embedOneByOne(texts);
6466
+ }
6467
+ try {
6468
+ return await this.embedMany(texts);
6469
+ } catch (error) {
6470
+ if (this.isBatchEndpointUnavailableError(error)) {
6471
+ this.batchEndpointUnavailable = true;
6472
+ return this.embedOneByOne(texts);
6473
+ }
6474
+ if (!this.isContextLengthError(error) && !this.isBatchValidationError(error)) {
6475
+ throw error;
6476
+ }
6477
+ return this.embedOneByOne(texts);
6478
+ }
6479
+ }
6359
6480
  };
6360
6481
 
6361
6482
  // src/embeddings/providers/openai.ts
@@ -6390,8 +6511,6 @@ var OpenAIEmbeddingProvider = class extends BaseEmbeddingProvider {
6390
6511
  // src/embeddings/provider.ts
6391
6512
  function createEmbeddingProvider(configuredProviderInfo) {
6392
6513
  switch (configuredProviderInfo.provider) {
6393
- case "github-copilot":
6394
- return new GitHubCopilotEmbeddingProvider(configuredProviderInfo.credentials, configuredProviderInfo.modelInfo);
6395
6514
  case "openai":
6396
6515
  return new OpenAIEmbeddingProvider(configuredProviderInfo.credentials, configuredProviderInfo.modelInfo);
6397
6516
  case "google":
@@ -7143,12 +7262,12 @@ try {
7143
7262
  }
7144
7263
 
7145
7264
  // src/native/parsing.ts
7146
- function parseFileAsText(filePath, content) {
7147
- const result = native.parseFileAsText(filePath, content);
7265
+ function parseFileAsText(filePath, content, linesPerChunk) {
7266
+ const result = native.parseFileAsText(filePath, content, linesPerChunk);
7148
7267
  return result.map(mapChunk);
7149
7268
  }
7150
- function parseFiles(files) {
7151
- const result = native.parseFiles(files);
7269
+ function parseFiles(files, linesPerChunk) {
7270
+ const result = native.parseFiles(files, linesPerChunk);
7152
7271
  return result.map((f) => ({
7153
7272
  path: f.path,
7154
7273
  chunks: f.chunks.map(mapChunk),
@@ -7225,13 +7344,13 @@ var VectorStore = class {
7225
7344
  const metadata = items.map((i) => JSON.stringify(i.metadata));
7226
7345
  this.inner.addBatch(ids, vectors, metadata);
7227
7346
  }
7228
- search(queryVector, limit = 10) {
7347
+ search(queryVector, limit = 10, allowedIds) {
7229
7348
  if (queryVector.length !== this.dimensions) {
7230
7349
  throw new Error(
7231
7350
  `Query vector dimension mismatch: expected ${this.dimensions}, got ${queryVector.length}`
7232
7351
  );
7233
7352
  }
7234
- const results = this.inner.search(queryVector, limit);
7353
+ const results = allowedIds === void 0 ? this.inner.search(queryVector, limit) : this.inner.searchFiltered(queryVector, limit, allowedIds);
7235
7354
  return results.map((r) => ({
7236
7355
  id: r.id,
7237
7356
  score: r.score,
@@ -7459,6 +7578,10 @@ var Database = class _Database {
7459
7578
  this.throwIfClosed();
7460
7579
  return this.inner.getBranchChunkIds(branch);
7461
7580
  }
7581
+ getChunkIdsByBlameDate(since, until) {
7582
+ this.throwIfClosed();
7583
+ return this.inner.getChunkIdsByBlameDate(since, until);
7584
+ }
7462
7585
  getBranchDelta(branch, baseBranch) {
7463
7586
  this.throwIfClosed();
7464
7587
  return this.inner.getBranchDelta(branch, baseBranch);
@@ -9347,6 +9470,18 @@ function createFailedBatchWriter(targetPath) {
9347
9470
  temporaryPath
9348
9471
  };
9349
9472
  }
9473
+ function writeFailedBatchRecords(targetPath, records) {
9474
+ const writer = createFailedBatchWriter(targetPath);
9475
+ try {
9476
+ for (const record of records) {
9477
+ writer.write(record);
9478
+ }
9479
+ writer.commit();
9480
+ } catch (error) {
9481
+ writer.cleanup();
9482
+ throw error;
9483
+ }
9484
+ }
9350
9485
  function* readLegacyFailedBatchRecords(filePath, options) {
9351
9486
  const rawData = fs2.readFileSync(filePath, "utf-8");
9352
9487
  const trimmed = stripLeadingBomAndWhitespace(rawData).trim();
@@ -9629,14 +9764,18 @@ function getSafeEmbeddingChunkTokenLimit(provider) {
9629
9764
  const maxChunkTokens = Math.max(256, Math.floor(providerMaxTokens * 0.75));
9630
9765
  return Math.min(2e3, maxChunkTokens);
9631
9766
  }
9632
- function getDynamicBatchOptions(provider) {
9633
- if (provider.provider === "ollama") {
9634
- return {
9635
- maxBatchTokens: provider.modelInfo.maxTokens,
9636
- maxBatchItems: 1
9637
- };
9767
+ var DEFAULT_OLLAMA_MAX_BATCH_ITEMS = 16;
9768
+ var DEFAULT_OLLAMA_MAX_BATCH_TOKENS = 65536;
9769
+ function getDynamicBatchOptions(provider, embeddingBatch) {
9770
+ if (provider.provider !== "ollama") {
9771
+ return {};
9638
9772
  }
9639
- return {};
9773
+ const base = { maxBatchTokens: DEFAULT_OLLAMA_MAX_BATCH_TOKENS, maxBatchItems: DEFAULT_OLLAMA_MAX_BATCH_ITEMS };
9774
+ return {
9775
+ ...base,
9776
+ ...typeof embeddingBatch?.maxBatchTokens === "number" && Number.isFinite(embeddingBatch.maxBatchTokens) ? { maxBatchTokens: embeddingBatch.maxBatchTokens } : {},
9777
+ ...typeof embeddingBatch?.maxBatchItems === "number" && Number.isFinite(embeddingBatch.maxBatchItems) ? { maxBatchItems: embeddingBatch.maxBatchItems } : {}
9778
+ };
9640
9779
  }
9641
9780
  function isSqliteCorruptionError(error) {
9642
9781
  const message = getErrorMessage4(error).toLowerCase();
@@ -9654,6 +9793,14 @@ function getPendingChunkId(rawChunk) {
9654
9793
  const id = rawChunk.id;
9655
9794
  return typeof id === "string" ? id : null;
9656
9795
  }
9796
+ function parseBlameTimestamp(value, endOfDay) {
9797
+ let timestampMs = Date.parse(value);
9798
+ if (Number.isNaN(timestampMs)) return null;
9799
+ if (endOfDay && /^\d{4}-\d{2}-\d{2}$/.test(value.trim())) {
9800
+ timestampMs += 24 * 60 * 60 * 1e3 - 1;
9801
+ }
9802
+ return Math.floor(timestampMs / 1e3);
9803
+ }
9657
9804
  function metadataFromBlame(blame) {
9658
9805
  if (!blame) {
9659
9806
  return {};
@@ -9800,7 +9947,7 @@ function promoteIdentifierMatches(query, combined, semanticCandidates, keywordCa
9800
9947
  const remainder = combined.filter((candidate) => !promotedIds.has(candidate.id));
9801
9948
  return [...promoted, ...remainder];
9802
9949
  }
9803
- function buildSymbolDefinitionLane(query, database, branchChunkIds, branchSymbolIds, limit, fallbackCandidates, prioritizeSourcePaths = classifyQueryIntentRaw(query) === "source") {
9950
+ function buildSymbolDefinitionLane(query, database, branchChunkIds, branchSymbolIds, limit, fallbackCandidates, prioritizeSourcePaths = classifyQueryIntentRaw(query) === "source", allowNonSourcePaths = false) {
9804
9951
  if (!prioritizeSourcePaths) {
9805
9952
  return [];
9806
9953
  }
@@ -9820,7 +9967,7 @@ function buildSymbolDefinitionLane(query, database, branchChunkIds, branchSymbol
9820
9967
  if (!isImplementationChunkType(chunkType)) {
9821
9968
  return false;
9822
9969
  }
9823
- if (!isLikelyImplementationPath2(chunk.filePath)) {
9970
+ if (!allowNonSourcePaths && !isLikelyImplementationPath2(chunk.filePath)) {
9824
9971
  return false;
9825
9972
  }
9826
9973
  const nameLower = (chunk.name ?? "").toLowerCase();
@@ -9884,7 +10031,7 @@ function buildSymbolDefinitionLane(query, database, branchChunkIds, branchSymbol
9884
10031
  }
9885
10032
  foundCoveringChunk = upsertChunkCandidate(chunk, identifier, normalizedIdentifier) || foundCoveringChunk;
9886
10033
  }
9887
- if (foundCoveringChunk || !isLikelyImplementationPath2(symbol.filePath)) {
10034
+ if (foundCoveringChunk || !allowNonSourcePaths && !isLikelyImplementationPath2(symbol.filePath)) {
9888
10035
  continue;
9889
10036
  }
9890
10037
  const symbolName = symbol.name.toLowerCase();
@@ -9938,7 +10085,7 @@ function buildSymbolDefinitionLane(query, database, branchChunkIds, branchSymbol
9938
10085
  const ranked = Array.from(symbolCandidates.values()).sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
9939
10086
  if (ranked.length === 0) {
9940
10087
  const implementationFallback = fallbackCandidates.filter(
9941
- (candidate) => isImplementationChunkType(candidate.metadata.chunkType) && isLikelyImplementationPath2(candidate.metadata.filePath)
10088
+ (candidate) => isImplementationChunkType(candidate.metadata.chunkType) && (allowNonSourcePaths || isLikelyImplementationPath2(candidate.metadata.filePath))
9942
10089
  );
9943
10090
  for (const candidate of implementationFallback) {
9944
10091
  const nameLower = (candidate.metadata.name ?? "").toLowerCase();
@@ -10054,10 +10201,16 @@ function matchesHardSearchFilters(candidate, options, projectRoot) {
10054
10201
  return false;
10055
10202
  }
10056
10203
  if (options?.blameSince) {
10057
- const sinceMs = Date.parse(options.blameSince);
10058
- if (Number.isNaN(sinceMs)) return false;
10204
+ const since = parseBlameTimestamp(options.blameSince, false);
10205
+ if (since === null) return false;
10206
+ const committedAt = candidate.metadata.blameCommittedAt;
10207
+ if (committedAt === void 0 || committedAt < since) return false;
10208
+ }
10209
+ if (options?.blameUntil) {
10210
+ const until = parseBlameTimestamp(options.blameUntil, true);
10211
+ if (until === null) return false;
10059
10212
  const committedAt = candidate.metadata.blameCommittedAt;
10060
- if (committedAt === void 0 || committedAt < Math.floor(sinceMs / 1e3)) return false;
10213
+ if (committedAt === void 0 || committedAt > until) return false;
10061
10214
  }
10062
10215
  return true;
10063
10216
  }
@@ -10113,9 +10266,10 @@ var Indexer = class _Indexer {
10113
10266
  writerArtifactFingerprint = null;
10114
10267
  readerArtifactRetryAfter = /* @__PURE__ */ new Map();
10115
10268
  fileBatchLimits;
10269
+ checkpointIntervalChunks;
10116
10270
  constructor(projectRoot, config, host, runtimeOptions = {}) {
10117
10271
  this.projectRoot = projectRoot;
10118
- this.projectIdentityHash = hashContent(this.getCanonicalPath(projectRoot)).slice(0, 16);
10272
+ this.projectIdentityHash = this.getProjectIdentityHash(projectRoot);
10119
10273
  this.materializedProjectRoot = runtimeOptions.materializedProjectRoot ?? projectRoot;
10120
10274
  this.branchNameOverride = runtimeOptions.branchName;
10121
10275
  this.catalogIdentityOverride = runtimeOptions.catalogIdentity;
@@ -10125,6 +10279,7 @@ var Indexer = class _Indexer {
10125
10279
  this.expectedCommitOverride = runtimeOptions.expectedCommit?.toLowerCase();
10126
10280
  this.indexPathOverride = runtimeOptions.indexPath;
10127
10281
  this.fileBatchLimits = runtimeOptions.fileBatchLimits;
10282
+ this.checkpointIntervalChunks = runtimeOptions.checkpointIntervalChunks;
10128
10283
  this.config = config;
10129
10284
  this.host = host;
10130
10285
  if (isGitRepo(this.materializedProjectRoot)) {
@@ -10236,6 +10391,9 @@ var Indexer = class _Indexer {
10236
10391
  return path19.resolve(targetPath);
10237
10392
  }
10238
10393
  }
10394
+ getProjectIdentityHash(projectRoot) {
10395
+ return hashContent(this.getCanonicalPath(projectRoot)).slice(0, 16);
10396
+ }
10239
10397
  isProjectOwnedIndexPath() {
10240
10398
  return isProjectIndexPathOwnedByProject(this.projectRoot, this.indexPath, this.host);
10241
10399
  }
@@ -10272,7 +10430,10 @@ var Indexer = class _Indexer {
10272
10430
  }
10273
10431
  async withIndexMutationLease(operation, callback) {
10274
10432
  this.refreshBranchInfo();
10275
- const lease = acquireIndexLock(this.indexPath, operation);
10433
+ const lease = acquireIndexLock(this.indexPath, operation, {
10434
+ projectRoot: this.projectRoot,
10435
+ scopedRoots: this.getScopedRoots()
10436
+ });
10276
10437
  this.indexPath = lease.canonicalIndexPath;
10277
10438
  this.refreshRuntimeArtifactPaths();
10278
10439
  this.activeIndexLease = lease;
@@ -10327,6 +10488,7 @@ var Indexer = class _Indexer {
10327
10488
  }
10328
10489
  loadFileHashCache() {
10329
10490
  if (!(0, import_fs12.existsSync)(this.fileHashCachePath)) {
10491
+ this.fileHashCache = /* @__PURE__ */ new Map();
10330
10492
  return;
10331
10493
  }
10332
10494
  try {
@@ -10366,10 +10528,10 @@ var Indexer = class _Indexer {
10366
10528
  invertedIndex.serialize()
10367
10529
  );
10368
10530
  }
10369
- getScopedRoots() {
10370
- const roots = /* @__PURE__ */ new Set([this.getCanonicalPath(this.projectRoot)]);
10531
+ getScopedRoots(projectRoot = this.projectRoot) {
10532
+ const roots = /* @__PURE__ */ new Set([this.getCanonicalPath(projectRoot)]);
10371
10533
  for (const kbRoot of this.config.knowledgeBases) {
10372
- roots.add(this.getCanonicalPath(path19.resolve(this.projectRoot, kbRoot)));
10534
+ roots.add(this.getCanonicalPath(path19.resolve(projectRoot, kbRoot)));
10373
10535
  }
10374
10536
  return Array.from(roots);
10375
10537
  }
@@ -10440,14 +10602,17 @@ var Indexer = class _Indexer {
10440
10602
  getLegacyBranchCatalogKey() {
10441
10603
  return this.currentBranch || "default";
10442
10604
  }
10443
- getLegacyMigrationMetadataKey() {
10444
- return `index.globalBranchMigration.${this.projectIdentityHash}`;
10605
+ getLegacyMigrationMetadataKey(projectIdentityHash = this.projectIdentityHash) {
10606
+ return `index.globalBranchMigration.${projectIdentityHash}`;
10607
+ }
10608
+ getProjectEmbeddingStrategyMetadataKey(projectIdentityHash = this.projectIdentityHash) {
10609
+ return `index.embeddingStrategyVersion.${projectIdentityHash}`;
10445
10610
  }
10446
- getProjectEmbeddingStrategyMetadataKey() {
10447
- return `index.embeddingStrategyVersion.${this.projectIdentityHash}`;
10611
+ getProjectForceReembedMetadataKey(projectIdentityHash = this.projectIdentityHash) {
10612
+ return `index.forceReembed.${projectIdentityHash}`;
10448
10613
  }
10449
- getProjectForceReembedMetadataKey() {
10450
- return `index.forceReembed.${this.projectIdentityHash}`;
10614
+ getProjectMigrationFinalizedMetadataKey(projectIdentityHash = this.projectIdentityHash) {
10615
+ return `index.migrationFinalized.${projectIdentityHash}`;
10451
10616
  }
10452
10617
  getBranchMigrationMetadataKey(prefix, catalogIdentity = this.getBranchCatalogIdentity()) {
10453
10618
  const branchKey = this.getBranchCatalogKeyFor(catalogIdentity);
@@ -10553,7 +10718,7 @@ var Indexer = class _Indexer {
10553
10718
  const legacy = this.getLegacyBranchCatalogKey();
10554
10719
  return primary === legacy ? [primary] : [primary, legacy];
10555
10720
  }
10556
- getProjectLocalScopedOwnershipIds(roots) {
10721
+ getProjectLocalScopedOwnershipIds(roots, projectRoot = this.projectRoot) {
10557
10722
  const chunkIds = /* @__PURE__ */ new Set();
10558
10723
  const symbolIds = /* @__PURE__ */ new Set();
10559
10724
  if (!this.database) {
@@ -10561,10 +10726,10 @@ var Indexer = class _Indexer {
10561
10726
  }
10562
10727
  const projectLocalFilePaths = /* @__PURE__ */ new Set([
10563
10728
  ...Array.from(this.fileHashCache.keys()).filter(
10564
- (filePath) => this.isFileInCurrentScope(filePath, roots) && this.isFileInProjectRoot(filePath)
10729
+ (filePath) => this.isFileInCurrentScope(filePath, roots) && this.isFileInProjectRoot(filePath, projectRoot)
10565
10730
  ),
10566
10731
  ...(this.store?.getAllMetadata() ?? []).map(({ metadata }) => metadata.filePath).filter(
10567
- (filePath) => this.isFileInCurrentScope(filePath, roots) && this.isFileInProjectRoot(filePath)
10732
+ (filePath) => this.isFileInCurrentScope(filePath, roots) && this.isFileInProjectRoot(filePath, projectRoot)
10568
10733
  )
10569
10734
  ]);
10570
10735
  for (const filePath of projectLocalFilePaths) {
@@ -10577,15 +10742,16 @@ var Indexer = class _Indexer {
10577
10742
  }
10578
10743
  return { chunkIds, symbolIds };
10579
10744
  }
10580
- getProjectScopedBranchCatalogCleanupKeys(projectChunkIds, projectSymbolIds) {
10745
+ getProjectScopedBranchCatalogCleanupKeys(projectChunkIds, projectSymbolIds, projectRoot = this.projectRoot) {
10581
10746
  if (this.config.scope !== "global") {
10582
10747
  return this.getBranchCatalogCleanupKeys();
10583
10748
  }
10584
10749
  const keys = /* @__PURE__ */ new Set();
10585
10750
  const projectChunkIdSet = new Set(projectChunkIds);
10586
10751
  const projectSymbolIdSet = new Set(projectSymbolIds);
10752
+ const projectIdentityHash = this.getProjectIdentityHash(projectRoot);
10587
10753
  for (const branchKey of this.database?.getAllBranches() ?? []) {
10588
- if (branchKey.startsWith(`${this.projectIdentityHash}:`)) {
10754
+ if (branchKey.startsWith(`${projectIdentityHash}:`)) {
10589
10755
  keys.add(branchKey);
10590
10756
  continue;
10591
10757
  }
@@ -10595,8 +10761,10 @@ var Indexer = class _Indexer {
10595
10761
  keys.add(branchKey);
10596
10762
  }
10597
10763
  }
10598
- for (const branchKey of this.getBranchCatalogCleanupKeys()) {
10599
- keys.add(branchKey);
10764
+ if (projectRoot === this.projectRoot) {
10765
+ for (const branchKey of this.getBranchCatalogCleanupKeys()) {
10766
+ keys.add(branchKey);
10767
+ }
10600
10768
  }
10601
10769
  return Array.from(keys);
10602
10770
  }
@@ -10604,10 +10772,10 @@ var Indexer = class _Indexer {
10604
10772
  const canonicalFilePath = this.getCanonicalStoredFilePath(filePath);
10605
10773
  return roots.some((root) => isPathWithinRoot2(canonicalFilePath, root));
10606
10774
  }
10607
- isFileInProjectRoot(filePath) {
10775
+ isFileInProjectRoot(filePath, projectRoot = this.projectRoot) {
10608
10776
  return isPathWithinRoot2(
10609
10777
  this.getCanonicalStoredFilePath(filePath),
10610
- this.getCanonicalPath(this.projectRoot)
10778
+ this.getCanonicalPath(projectRoot)
10611
10779
  );
10612
10780
  }
10613
10781
  clearScopedFileHashCache(roots) {
@@ -10649,12 +10817,12 @@ var Indexer = class _Indexer {
10649
10817
  }
10650
10818
  return false;
10651
10819
  }
10652
- hasForeignScopedBranchData() {
10820
+ hasForeignScopedBranchData(projectRoot = this.projectRoot, roots = this.getScopedRoots(projectRoot)) {
10653
10821
  if (!this.database || this.config.scope !== "global") {
10654
10822
  return false;
10655
10823
  }
10656
- const roots = this.getScopedRoots();
10657
- const { chunkIds: projectLocalChunkIds, symbolIds: projectLocalSymbolIds } = this.getProjectLocalScopedOwnershipIds(roots);
10824
+ const projectIdentityHash = this.getProjectIdentityHash(projectRoot);
10825
+ const { chunkIds: projectLocalChunkIds, symbolIds: projectLocalSymbolIds } = this.getProjectLocalScopedOwnershipIds(roots, projectRoot);
10658
10826
  return this.database.getAllBranches().some(
10659
10827
  (branchKey) => {
10660
10828
  const branchChunkIds = this.database.getBranchChunkIds(branchKey);
@@ -10663,7 +10831,7 @@ var Indexer = class _Indexer {
10663
10831
  if (!hasBranchData) {
10664
10832
  return false;
10665
10833
  }
10666
- if (branchKey.startsWith(`${this.projectIdentityHash}:`)) {
10834
+ if (branchKey.startsWith(`${projectIdentityHash}:`)) {
10667
10835
  return false;
10668
10836
  }
10669
10837
  const referencesCurrentProjectChunks = branchChunkIds.some((chunkId) => projectLocalChunkIds.has(chunkId));
@@ -10672,7 +10840,7 @@ var Indexer = class _Indexer {
10672
10840
  }
10673
10841
  );
10674
10842
  }
10675
- clearSharedIndexProjectData(store, invertedIndex, database, roots) {
10843
+ clearSharedIndexProjectData(store, invertedIndex, database, roots, projectRoot = this.projectRoot) {
10676
10844
  const allMetadata = store.getAllMetadata();
10677
10845
  const scopedEntries = allMetadata.filter(({ metadata }) => this.isFileInCurrentScope(metadata.filePath, roots));
10678
10846
  const filePaths = /* @__PURE__ */ new Set([
@@ -10680,7 +10848,7 @@ var Indexer = class _Indexer {
10680
10848
  ...scopedEntries.map(({ metadata }) => metadata.filePath)
10681
10849
  ]);
10682
10850
  const projectLocalFilePaths = new Set(
10683
- Array.from(filePaths).filter((filePath) => this.isFileInProjectRoot(filePath))
10851
+ Array.from(filePaths).filter((filePath) => this.isFileInProjectRoot(filePath, projectRoot))
10684
10852
  );
10685
10853
  const removedChunkIds = new Set(scopedEntries.map(({ key }) => key));
10686
10854
  for (const filePath of filePaths) {
@@ -10690,7 +10858,7 @@ var Indexer = class _Indexer {
10690
10858
  }
10691
10859
  const removedChunkIdList = Array.from(removedChunkIds);
10692
10860
  const projectLocalChunkIds = new Set(
10693
- scopedEntries.filter(({ metadata }) => this.isFileInProjectRoot(metadata.filePath)).map(({ key }) => key)
10861
+ scopedEntries.filter(({ metadata }) => this.isFileInProjectRoot(metadata.filePath, projectRoot)).map(({ key }) => key)
10694
10862
  );
10695
10863
  for (const filePath of projectLocalFilePaths) {
10696
10864
  for (const chunk of database.getChunksByFile(filePath)) {
@@ -10709,7 +10877,8 @@ var Indexer = class _Indexer {
10709
10877
  }
10710
10878
  const branchCleanupKeys = this.getProjectScopedBranchCatalogCleanupKeys(
10711
10879
  Array.from(projectLocalChunkIds),
10712
- Array.from(projectLocalSymbolIds)
10880
+ Array.from(projectLocalSymbolIds),
10881
+ projectRoot
10713
10882
  );
10714
10883
  for (const branchKey of branchCleanupKeys) {
10715
10884
  database.deleteBranchChunksForBranch(branchKey, removedChunkIdList);
@@ -10744,29 +10913,96 @@ var Indexer = class _Indexer {
10744
10913
  database.gcOrphanSymbols();
10745
10914
  database.gcOrphanEmbeddings();
10746
10915
  database.gcOrphanChunks();
10747
- store.save();
10748
10916
  this.saveInvertedIndex(invertedIndex);
10917
+ store.save();
10749
10918
  return {
10750
10919
  removedChunkIds: removedChunkIdList,
10751
10920
  hasForeignData: allMetadata.some(({ metadata }) => !this.isFileInCurrentScope(metadata.filePath, roots))
10752
10921
  };
10753
10922
  }
10923
+ getCurrentClearRecoveryState() {
10924
+ if (!this.configuredProviderInfo) {
10925
+ throw new Error("Cannot persist clear recovery state before the embedding provider is initialized");
10926
+ }
10927
+ const compatibility = this.checkCompatibility();
10928
+ const compatibilityDecision = compatibility.compatible ? "compatible" : compatibility.code === "EMBEDDING_STRATEGY_MISMATCH" /* EMBEDDING_STRATEGY_MISMATCH */ ? "embedding-strategy-mismatch" : "incompatible";
10929
+ return {
10930
+ phase: "clearing",
10931
+ embeddingProvider: this.configuredProviderInfo.provider,
10932
+ embeddingModel: this.configuredProviderInfo.modelInfo.model,
10933
+ embeddingDimensions: this.configuredProviderInfo.modelInfo.dimensions,
10934
+ embeddingStrategyVersion: EMBEDDING_STRATEGY_VERSION,
10935
+ compatibilityDecision
10936
+ };
10937
+ }
10938
+ beginClearRecoveryState() {
10939
+ const recovery = this.getCurrentClearRecoveryState();
10940
+ setIndexLockClearRecoveryState(this.requireActiveLease(), recovery);
10941
+ return recovery;
10942
+ }
10943
+ finishClearRecoveryState() {
10944
+ setIndexLockClearRecoveryState(this.requireActiveLease(), null);
10945
+ }
10946
+ matchesCurrentClearRecoveryConfiguration(recovery) {
10947
+ const configuredProviderInfo = this.configuredProviderInfo;
10948
+ return configuredProviderInfo !== null && recovery.embeddingProvider === configuredProviderInfo.provider && recovery.embeddingModel === configuredProviderInfo.modelInfo.model && recovery.embeddingDimensions === configuredProviderInfo.modelInfo.dimensions && recovery.embeddingStrategyVersion === EMBEDDING_STRATEGY_VERSION;
10949
+ }
10950
+ hasUnknownLegacyForceIndexClear(owner) {
10951
+ return owner.operation === "force-index" && owner.clearRecovery === void 0 && owner.recoveryProtocolVersion !== 1 && (0, import_fs12.existsSync)(path19.join(this.indexPath, "force-index-phase"));
10952
+ }
10754
10953
  async recoverFromInterruptedIndexingUnlocked(owners) {
10755
10954
  for (const owner of owners) {
10756
10955
  this.logger.warn("Detected interrupted indexing session, recovering...", {
10757
10956
  pid: owner.pid,
10758
10957
  hostname: owner.hostname,
10759
10958
  operation: owner.operation,
10760
- startedAt: owner.startedAt
10959
+ startedAt: owner.startedAt,
10960
+ projectRoot: owner.projectRoot
10761
10961
  });
10762
10962
  }
10763
10963
  if (this.config.scope === "global") {
10764
- if ((0, import_fs12.existsSync)(this.fileHashCachePath)) {
10765
- (0, import_fs12.unlinkSync)(this.fileHashCachePath);
10964
+ const clearScopes = [];
10965
+ for (const owner of owners) {
10966
+ if (this.hasUnknownLegacyForceIndexClear(owner)) {
10967
+ throw new Error(
10968
+ `Cannot automatically recover interrupted force-index ${owner.token}: the legacy clearing phase ownership is unknown. The recovery marker was retained for manual inspection.`
10969
+ );
10970
+ }
10971
+ if (owner.operation === "clear" && owner.clearRecovery === void 0 && owner.recoveryProtocolVersion !== 1) {
10972
+ throw new Error(
10973
+ `Cannot automatically recover interrupted global clear ${owner.token}: the originating recovery state is unknown. The recovery marker was retained for manual inspection.`
10974
+ );
10975
+ }
10976
+ if (owner.clearRecovery === void 0) continue;
10977
+ if (!owner.projectRoot || !owner.scopedRoots || owner.scopedRoots.length === 0) {
10978
+ throw new Error(
10979
+ `Cannot automatically recover interrupted global clear ${owner.token}: the originating project scope is unknown. The recovery marker was retained for manual inspection.`
10980
+ );
10981
+ }
10982
+ if (!this.matchesCurrentClearRecoveryConfiguration(owner.clearRecovery)) {
10983
+ throw new Error(
10984
+ `Cannot automatically recover interrupted global clear ${owner.token}: the current embedding configuration does not match the originating lease. The recovery marker was retained; retry from the originating project with matching settings.`
10985
+ );
10986
+ }
10987
+ clearScopes.push({
10988
+ projectRoot: owner.projectRoot,
10989
+ scopedRoots: owner.scopedRoots,
10990
+ compatibilityDecision: owner.clearRecovery.compatibilityDecision
10991
+ });
10992
+ }
10993
+ if (clearScopes.length > 0) {
10994
+ this.loadFileHashCache();
10995
+ }
10996
+ for (const { projectRoot, scopedRoots, compatibilityDecision } of clearScopes) {
10997
+ this.clearGlobalIndexUnlocked(projectRoot, scopedRoots, compatibilityDecision);
10766
10998
  }
10767
10999
  await this.healthCheckUnlocked();
11000
+ this.logger.info(
11001
+ clearScopes.length > 0 ? "Recovery complete, next index will rebuild all files" : "Recovery complete, next index will resume from the last checkpoint"
11002
+ );
11003
+ return;
10768
11004
  }
10769
- this.logger.info("Recovery complete, next index will re-process all files");
11005
+ this.logger.info("Recovery complete, next index will resume from the last checkpoint");
10770
11006
  }
10771
11007
  *loadSerializedFailedBatches() {
10772
11008
  let warned = false;
@@ -10804,14 +11040,99 @@ var Indexer = class _Indexer {
10804
11040
  state.writer.write(record);
10805
11041
  state.recordsWritten += record.chunks.length;
10806
11042
  }
10807
- finalizeFailedBatchWriteState(state) {
11043
+ finalizeFailedBatchWriteState(state, resolvedChunkIds = /* @__PURE__ */ new Set()) {
10808
11044
  if (state.recordsWritten > 0) {
10809
- state.writer.commit();
11045
+ const seenChunkIds = /* @__PURE__ */ new Set();
11046
+ const retained = [];
11047
+ const records = Array.from(readFailedBatchRecords(state.writer.temporaryPath));
11048
+ for (let i = records.length - 1; i >= 0; i--) {
11049
+ const chunks = records[i].chunks.filter((rawChunk) => {
11050
+ const chunkId = getPendingChunkId(rawChunk);
11051
+ if (chunkId !== null) {
11052
+ if (resolvedChunkIds.has(chunkId)) return false;
11053
+ if (seenChunkIds.has(chunkId)) return false;
11054
+ seenChunkIds.add(chunkId);
11055
+ }
11056
+ return true;
11057
+ });
11058
+ if (chunks.length > 0) {
11059
+ retained.unshift({ ...records[i], chunks });
11060
+ }
11061
+ }
11062
+ state.writer.cleanup();
11063
+ if (retained.length > 0) {
11064
+ writeFailedBatchRecords(this.failedBatchesPath, retained);
11065
+ } else {
11066
+ writeFailedBatchRecords(this.failedBatchesPath, []);
11067
+ this.clearFailedBatchState();
11068
+ }
10810
11069
  return;
10811
11070
  }
10812
- state.writer.cleanup();
11071
+ state.writer.commit();
10813
11072
  this.clearFailedBatchState();
10814
11073
  }
11074
+ getCheckpointIntervalChunks(totalChunks) {
11075
+ return Math.max(
11076
+ this.checkpointIntervalChunks ?? 2e3,
11077
+ Math.floor(totalChunks / 10)
11078
+ );
11079
+ }
11080
+ checkpointIndexRun(database, store, invertedIndex, failedProcessing, resolvedRetryChunkIds, currentFileHashes, committedFilePaths, scopedRoots, configuredProviderInfo) {
11081
+ if (!this.hasProjectForceReembedPending()) {
11082
+ this.saveIndexMetadata(configuredProviderInfo);
11083
+ this.indexCompatibility = { compatible: true };
11084
+ }
11085
+ database.commitWriteTransaction();
11086
+ database.beginWriteTransaction();
11087
+ this.saveInvertedIndex(invertedIndex);
11088
+ store.save();
11089
+ if (failedProcessing.state.recordsWritten > 0 || failedProcessing.latestById.size > 0 || failedProcessing.discardedExistingRecords) {
11090
+ for (const metadata of failedProcessing.latestById.values()) {
11091
+ const alreadyMaterialized = metadata.chunks.some((rawChunk) => {
11092
+ const chunkId = getPendingChunkId(rawChunk);
11093
+ return chunkId !== null && failedProcessing.materializedRetryIds.has(chunkId);
11094
+ });
11095
+ if (alreadyMaterialized) continue;
11096
+ this.writeFailedBatchRecord(failedProcessing.state, {
11097
+ chunks: metadata.chunks,
11098
+ attemptCount: metadata.attemptCount,
11099
+ error: metadata.error,
11100
+ lastAttempt: metadata.lastAttempt
11101
+ });
11102
+ for (const rawChunk of metadata.chunks) {
11103
+ const chunkId = getPendingChunkId(rawChunk);
11104
+ if (chunkId !== null) {
11105
+ failedProcessing.materializedRetryIds.add(chunkId);
11106
+ }
11107
+ }
11108
+ }
11109
+ this.finalizeFailedBatchWriteState(failedProcessing.state, resolvedRetryChunkIds);
11110
+ failedProcessing.state = this.createFailedBatchWriteState();
11111
+ failedProcessing.discardedExistingRecords = false;
11112
+ for (const record of this.loadSerializedFailedBatches()) {
11113
+ for (const rawChunk of record.chunks) {
11114
+ const chunkId = getPendingChunkId(rawChunk);
11115
+ this.writeFailedBatchRecord(failedProcessing.state, { ...record, chunks: [rawChunk] });
11116
+ if (chunkId !== null) {
11117
+ failedProcessing.materializedRetryIds.add(chunkId);
11118
+ }
11119
+ }
11120
+ }
11121
+ }
11122
+ const partialHashes = /* @__PURE__ */ new Map();
11123
+ for (const filePath of committedFilePaths) {
11124
+ const hash = currentFileHashes.get(filePath);
11125
+ if (hash !== void 0) {
11126
+ partialHashes.set(filePath, hash);
11127
+ }
11128
+ }
11129
+ if (scopedRoots) {
11130
+ this.replaceScopedFileHashCache(partialHashes, scopedRoots);
11131
+ } else {
11132
+ this.fileHashCache = partialHashes;
11133
+ this.saveFileHashCache();
11134
+ }
11135
+ }
10815
11136
  clearFailedBatchState() {
10816
11137
  if ((0, import_fs12.existsSync)(this.failedBatchesPath)) {
10817
11138
  try {
@@ -10838,6 +11159,7 @@ var Indexer = class _Indexer {
10838
11159
  prepareFailedBatchProcessing(roots, shouldProcess) {
10839
11160
  const state = this.createFailedBatchWriteState();
10840
11161
  const latestById = /* @__PURE__ */ new Map();
11162
+ let discardedExistingRecords = false;
10841
11163
  try {
10842
11164
  for (const batch of this.loadSerializedFailedBatches()) {
10843
11165
  for (const rawChunk of batch.chunks) {
@@ -10848,10 +11170,12 @@ var Indexer = class _Indexer {
10848
11170
  continue;
10849
11171
  }
10850
11172
  if (!shouldProcess(filePath)) {
11173
+ discardedExistingRecords = true;
10851
11174
  continue;
10852
11175
  }
10853
11176
  const chunkId = getPendingChunkId(rawChunk);
10854
11177
  if (!chunkId) {
11178
+ discardedExistingRecords = true;
10855
11179
  continue;
10856
11180
  }
10857
11181
  const existing = latestById.get(chunkId);
@@ -10859,12 +11183,18 @@ var Indexer = class _Indexer {
10859
11183
  latestById.set(chunkId, {
10860
11184
  attemptCount: batch.attemptCount,
10861
11185
  error: batch.error,
10862
- lastAttempt: batch.lastAttempt
11186
+ lastAttempt: batch.lastAttempt,
11187
+ chunks: [rawChunk]
10863
11188
  });
10864
11189
  }
10865
11190
  }
10866
11191
  }
10867
- return { state, latestById };
11192
+ return {
11193
+ state,
11194
+ latestById,
11195
+ materializedRetryIds: /* @__PURE__ */ new Set(),
11196
+ discardedExistingRecords
11197
+ };
10868
11198
  } catch (error) {
10869
11199
  state.writer.cleanup();
10870
11200
  throw error;
@@ -10900,10 +11230,34 @@ var Indexer = class _Indexer {
10900
11230
  }
10901
11231
  }
10902
11232
  }
11233
+ restoreMissingChunkRows(database, chunks) {
11234
+ const missing = [];
11235
+ for (const chunk of chunks) {
11236
+ if (database.getChunk(chunk.id)) {
11237
+ continue;
11238
+ }
11239
+ missing.push({
11240
+ chunkId: chunk.id,
11241
+ contentHash: chunk.contentHash,
11242
+ filePath: chunk.metadata.filePath,
11243
+ startLine: chunk.metadata.startLine,
11244
+ endLine: chunk.metadata.endLine,
11245
+ nodeType: chunk.metadata.chunkType,
11246
+ name: chunk.metadata.name,
11247
+ language: chunk.metadata.language,
11248
+ blameSha: chunk.metadata.blameSha,
11249
+ blameAuthor: chunk.metadata.blameAuthor,
11250
+ blameAuthorEmail: chunk.metadata.blameAuthorEmail,
11251
+ blameCommittedAt: chunk.metadata.blameCommittedAt,
11252
+ blameSummary: chunk.metadata.blameSummary
11253
+ });
11254
+ }
11255
+ if (missing.length > 0) {
11256
+ database.upsertChunksBatch(missing);
11257
+ }
11258
+ }
10903
11259
  getProviderRateLimits(provider) {
10904
11260
  switch (provider) {
10905
- case "github-copilot":
10906
- return { concurrency: 1, intervalMs: 4e3, minRetryMs: 5e3, maxRetryMs: 6e4 };
10907
11261
  case "openai":
10908
11262
  return { concurrency: 3, intervalMs: 500, minRetryMs: 1e3, maxRetryMs: 3e4 };
10909
11263
  case "google":
@@ -10972,10 +11326,11 @@ var Indexer = class _Indexer {
10972
11326
  const embeddingPartsByChunk = /* @__PURE__ */ new Map();
10973
11327
  const completedVectorsByChunkId = /* @__PURE__ */ new Map();
10974
11328
  const completedChunkIds = /* @__PURE__ */ new Set();
10975
- const requestBatches = createPendingEmbeddingRequestBatches(
10976
- chunksNeedingEmbedding,
10977
- getDynamicBatchOptions(options.configuredProviderInfo)
10978
- );
11329
+ const batchOptions = getDynamicBatchOptions(options.configuredProviderInfo, this.config.embedding?.batch);
11330
+ if (options.forceSingleItemBatches && options.configuredProviderInfo.provider === "ollama") {
11331
+ batchOptions.maxBatchItems = 1;
11332
+ }
11333
+ const requestBatches = createPendingEmbeddingRequestBatches(chunksNeedingEmbedding, batchOptions);
10979
11334
  let fatalError;
10980
11335
  for (const requestBatch of requestBatches) {
10981
11336
  await options.queue.onSizeLessThan(Math.max(1, options.providerRateLimits.concurrency));
@@ -11538,7 +11893,7 @@ var Indexer = class _Indexer {
11538
11893
  }
11539
11894
  if (!this.configuredProviderInfo) {
11540
11895
  throw new Error(
11541
- "No embedding provider available. Configure GitHub Copilot, OpenAI, Google, Ollama, or a custom OpenAI-compatible endpoint."
11896
+ "No embedding provider available. Configure OpenAI, Google, Ollama, or a custom OpenAI-compatible endpoint."
11542
11897
  );
11543
11898
  }
11544
11899
  this.logger.info("Initializing indexer", {
@@ -11569,7 +11924,20 @@ var Indexer = class _Indexer {
11569
11924
  ]);
11570
11925
  }
11571
11926
  if (recoveredOwners.length > 0 && this.config.scope === "project") {
11572
- await this.resetLocalIndexArtifacts();
11927
+ const unknownLegacyForceIndex = recoveredOwners.find(
11928
+ (owner) => this.hasUnknownLegacyForceIndexClear(owner)
11929
+ );
11930
+ if (unknownLegacyForceIndex) {
11931
+ throw new Error(
11932
+ `Cannot automatically recover interrupted force-index ${unknownLegacyForceIndex.token}: the legacy clearing phase ownership is unknown. The recovery marker was retained for manual inspection.`
11933
+ );
11934
+ }
11935
+ const shouldReset = recoveredOwners.some(
11936
+ (owner) => owner.clearRecovery !== void 0 || owner.operation === "clear" && owner.recoveryProtocolVersion !== 1
11937
+ );
11938
+ if (shouldReset) {
11939
+ await this.resetLocalIndexArtifacts();
11940
+ }
11573
11941
  }
11574
11942
  this.store = new VectorStore(storePath, dimensions);
11575
11943
  if ((0, import_fs12.existsSync)(storePath) || (0, import_fs12.existsSync)(vectorMetadataPath)) {
@@ -12205,7 +12573,17 @@ var Indexer = class _Indexer {
12205
12573
  const needsCallGraphResolutionMigration = database.getMetadata(this.getCallGraphResolutionMetadataKey()) !== CALL_GRAPH_RESOLUTION_VERSION;
12206
12574
  for (const file of files) {
12207
12575
  const storedPath = this.toStoredFilePath(file.path);
12208
- const currentHash = hashFile(file.path);
12576
+ let currentHash;
12577
+ try {
12578
+ currentHash = hashFile(file.path);
12579
+ } catch (error) {
12580
+ stats.skippedFiles.push({ path: this.toCanonicalFilePath(file.path), reason: "unreadable" });
12581
+ this.logger.warn("Skipped unreadable file during indexing", {
12582
+ path: file.path,
12583
+ error: getErrorMessage4(error)
12584
+ });
12585
+ continue;
12586
+ }
12209
12587
  currentFileHashes.set(storedPath, currentHash);
12210
12588
  const cachedHashMatches = this.fileHashCache.get(storedPath) === currentHash;
12211
12589
  const needsCallGraphRefresh = cachedHashMatches && needsCallGraphResolutionMigration && database.getChunksByFile(storedPath).some(
@@ -12213,7 +12591,8 @@ var Indexer = class _Indexer {
12213
12591
  );
12214
12592
  const requiresSwiftParserUpgrade = reparseCachedSwiftFiles && path19.extname(storedPath).toLowerCase() === ".swift";
12215
12593
  const requiresMetalParserUpgrade = reparseCachedMetalFiles && path19.extname(storedPath).toLowerCase() === ".metal";
12216
- if (cachedHashMatches && !needsCallGraphRefresh && !requiresSwiftParserUpgrade && !requiresMetalParserUpgrade && !refreshCachedSymbols) {
12594
+ const inMigrationScope = forceScopedReembed && scopedRoots !== null && this.isFileInCurrentScope(storedPath, scopedRoots);
12595
+ if (cachedHashMatches && !inMigrationScope && !needsCallGraphRefresh && !requiresSwiftParserUpgrade && !requiresMetalParserUpgrade && !refreshCachedSymbols) {
12217
12596
  unchangedFilePaths.add(storedPath);
12218
12597
  this.logger.recordCacheHit();
12219
12598
  } else {
@@ -12339,6 +12718,9 @@ var Indexer = class _Indexer {
12339
12718
  }
12340
12719
  }
12341
12720
  let processedChangedFiles = 0;
12721
+ let lastCheckpointChunks = 0;
12722
+ const committedFilePaths = new Set(unchangedFilePaths);
12723
+ const resolvedRetryChunkIds = /* @__PURE__ */ new Set();
12342
12724
  for (const descriptorBatch of iterateOrderedFileBatches(
12343
12725
  changedFileDescriptors,
12344
12726
  (descriptor) => descriptor.sourceBytes,
@@ -12352,7 +12734,7 @@ var Indexer = class _Indexer {
12352
12734
  const loadedByPath = new Map(loadedFiles.map((file) => [file.path, file]));
12353
12735
  const descriptorByPath = new Map(descriptorBatch.map((descriptor) => [descriptor.storedPath, descriptor]));
12354
12736
  const parseStartTime = import_perf_hooks.performance.now();
12355
- const parsedFiles = parseFiles(loadedFiles);
12737
+ const parsedFiles = parseFiles(loadedFiles, this.config.indexing.linesPerChunk);
12356
12738
  const parseMs = import_perf_hooks.performance.now() - parseStartTime;
12357
12739
  this.logger.recordFilesParsed(parsedFiles.length);
12358
12740
  this.logger.recordParseDuration(parseMs);
@@ -12375,7 +12757,7 @@ var Indexer = class _Indexer {
12375
12757
  }
12376
12758
  let chunksToProcess = parsed.chunks;
12377
12759
  if (this.config.indexing.fallbackToTextOnMaxChunks && chunksToProcess.length > this.config.indexing.maxChunksPerFile) {
12378
- chunksToProcess = parseFileAsText(parsed.path, loadedFile.content);
12760
+ chunksToProcess = parseFileAsText(parsed.path, loadedFile.content, this.config.indexing.linesPerChunk);
12379
12761
  }
12380
12762
  chunksToProcess = selectIndexableChunks(
12381
12763
  chunksToProcess,
@@ -12509,6 +12891,10 @@ var Indexer = class _Indexer {
12509
12891
  }
12510
12892
  if (symbolBatch.length > 0) {
12511
12893
  database.upsertSymbolsBatch(symbolBatch);
12894
+ database.addSymbolsToBranchBatch(
12895
+ this.getBranchCatalogKey(),
12896
+ symbolBatch.map((symbol) => symbol.id)
12897
+ );
12512
12898
  }
12513
12899
  if (edgeBatch.length > 0) {
12514
12900
  database.upsertCallEdgesBatch(edgeBatch);
@@ -12544,6 +12930,12 @@ var Indexer = class _Indexer {
12544
12930
  forceReembed: forceScopedReembed,
12545
12931
  reuseCachedEmbeddings: true,
12546
12932
  incrementRepeatedFailures: true,
12933
+ onSucceeded: (succeededChunks) => {
12934
+ database.addChunksToBranchBatch(
12935
+ this.getBranchCatalogKey(),
12936
+ succeededChunks.map((chunk) => chunk.id)
12937
+ );
12938
+ },
12547
12939
  onProgress: (batchProgress) => onProgress?.({
12548
12940
  phase: "embedding",
12549
12941
  filesProcessed: unchangedFilePaths.size + processedChangedFiles,
@@ -12562,6 +12954,27 @@ var Indexer = class _Indexer {
12562
12954
  }
12563
12955
  }
12564
12956
  }
12957
+ for (const descriptor of descriptorBatch) {
12958
+ const existingFileChunks = existingChunksByFile.get(descriptor.storedPath);
12959
+ if (!existingFileChunks || existingFileChunks.size === 0) {
12960
+ committedFilePaths.add(descriptor.storedPath);
12961
+ }
12962
+ }
12963
+ const checkpointInterval = this.getCheckpointIntervalChunks(stats.totalChunks);
12964
+ if (stats.totalChunks - lastCheckpointChunks >= checkpointInterval) {
12965
+ lastCheckpointChunks = stats.totalChunks;
12966
+ this.checkpointIndexRun(
12967
+ database,
12968
+ store,
12969
+ invertedIndex,
12970
+ failedProcessing,
12971
+ resolvedRetryChunkIds,
12972
+ currentFileHashes,
12973
+ committedFilePaths,
12974
+ scopedRoots,
12975
+ configuredProviderInfo
12976
+ );
12977
+ }
12565
12978
  }
12566
12979
  const retryableFailedChunks = this.iterateLatestFailedChunks(
12567
12980
  failedProcessing.latestById,
@@ -12582,6 +12995,7 @@ var Indexer = class _Indexer {
12582
12995
  retryableChunksWithExistingData.add(chunk.id);
12583
12996
  }
12584
12997
  }
12998
+ this.restoreMissingChunkRows(database, pendingChunks);
12585
12999
  stats.totalChunks += pendingChunks.length;
12586
13000
  onProgress?.({
12587
13001
  phase: "embedding",
@@ -12604,6 +13018,17 @@ var Indexer = class _Indexer {
12604
13018
  forceReembed: forceScopedReembed,
12605
13019
  reuseCachedEmbeddings: true,
12606
13020
  incrementRepeatedFailures: true,
13021
+ forceSingleItemBatches: true,
13022
+ onSucceeded: (succeededChunks) => {
13023
+ database.addChunksToBranchBatch(
13024
+ this.getBranchCatalogKey(),
13025
+ succeededChunks.map((chunk) => chunk.id)
13026
+ );
13027
+ for (const chunk of succeededChunks) {
13028
+ failedProcessing.latestById.delete(chunk.id);
13029
+ resolvedRetryChunkIds.add(chunk.id);
13030
+ }
13031
+ },
12607
13032
  onProgress: (batchProgress) => onProgress?.({
12608
13033
  phase: "embedding",
12609
13034
  filesProcessed: files.length,
@@ -12621,6 +13046,20 @@ var Indexer = class _Indexer {
12621
13046
  failedForcedChunkIds.add(chunkId);
12622
13047
  }
12623
13048
  }
13049
+ if (stats.totalChunks - lastCheckpointChunks >= this.getCheckpointIntervalChunks(stats.totalChunks)) {
13050
+ lastCheckpointChunks = stats.totalChunks;
13051
+ this.checkpointIndexRun(
13052
+ database,
13053
+ store,
13054
+ invertedIndex,
13055
+ failedProcessing,
13056
+ resolvedRetryChunkIds,
13057
+ currentFileHashes,
13058
+ committedFilePaths,
13059
+ scopedRoots,
13060
+ configuredProviderInfo
13061
+ );
13062
+ }
12624
13063
  }
12625
13064
  const removedChunkIds = [];
12626
13065
  for (const [chunkId] of existingChunks) {
@@ -12657,13 +13096,6 @@ var Indexer = class _Indexer {
12657
13096
  if (removedStoredChunks) {
12658
13097
  this.saveInvertedIndex(invertedIndex);
12659
13098
  }
12660
- if (scopedRoots) {
12661
- this.replaceScopedFileHashCache(currentFileHashes, scopedRoots);
12662
- } else {
12663
- this.fileHashCache = currentFileHashes;
12664
- this.saveFileHashCache();
12665
- }
12666
- this.finalizeFailedBatchWriteState(failedProcessing.state);
12667
13099
  database.setMetadata(swiftParserMetadataKey, SWIFT_PARSER_VERSION);
12668
13100
  database.setMetadata(metalParserMetadataKey, METAL_PARSER_VERSION);
12669
13101
  database.setMetadata(symbolExtractorMetadataKey, SYMBOL_EXTRACTOR_VERSION);
@@ -12672,6 +13104,13 @@ var Indexer = class _Indexer {
12672
13104
  this.indexCompatibility = { compatible: true };
12673
13105
  database.commitWriteTransaction();
12674
13106
  writeTransactionActive = false;
13107
+ this.finalizeFailedBatchWriteState(failedProcessing.state, resolvedRetryChunkIds);
13108
+ if (scopedRoots) {
13109
+ this.replaceScopedFileHashCache(currentFileHashes, scopedRoots);
13110
+ } else {
13111
+ this.fileHashCache = currentFileHashes;
13112
+ this.saveFileHashCache();
13113
+ }
12675
13114
  stats.durationMs = Date.now() - startTime;
12676
13115
  onProgress?.({
12677
13116
  phase: "complete",
@@ -12695,13 +13134,6 @@ var Indexer = class _Indexer {
12695
13134
  );
12696
13135
  store.save();
12697
13136
  this.saveInvertedIndex(invertedIndex);
12698
- if (scopedRoots) {
12699
- this.replaceScopedFileHashCache(currentFileHashes, scopedRoots);
12700
- } else {
12701
- this.fileHashCache = currentFileHashes;
12702
- this.saveFileHashCache();
12703
- }
12704
- this.finalizeFailedBatchWriteState(failedProcessing.state);
12705
13137
  database.setMetadata(swiftParserMetadataKey, SWIFT_PARSER_VERSION);
12706
13138
  database.setMetadata(metalParserMetadataKey, METAL_PARSER_VERSION);
12707
13139
  database.setMetadata(symbolExtractorMetadataKey, SYMBOL_EXTRACTOR_VERSION);
@@ -12710,6 +13142,13 @@ var Indexer = class _Indexer {
12710
13142
  this.indexCompatibility = { compatible: true };
12711
13143
  database.commitWriteTransaction();
12712
13144
  writeTransactionActive = false;
13145
+ this.finalizeFailedBatchWriteState(failedProcessing.state, resolvedRetryChunkIds);
13146
+ if (scopedRoots) {
13147
+ this.replaceScopedFileHashCache(currentFileHashes, scopedRoots);
13148
+ } else {
13149
+ this.fileHashCache = currentFileHashes;
13150
+ this.saveFileHashCache();
13151
+ }
12713
13152
  stats.durationMs = Date.now() - startTime;
12714
13153
  onProgress?.({
12715
13154
  phase: "complete",
@@ -12744,15 +13183,15 @@ var Indexer = class _Indexer {
12744
13183
  );
12745
13184
  store.save();
12746
13185
  this.saveInvertedIndex(invertedIndex);
13186
+ database.commitWriteTransaction();
13187
+ writeTransactionActive = false;
13188
+ this.finalizeFailedBatchWriteState(failedProcessing.state, resolvedRetryChunkIds);
12747
13189
  if (scopedRoots) {
12748
13190
  this.replaceScopedFileHashCache(currentFileHashes, scopedRoots);
12749
13191
  } else {
12750
13192
  this.fileHashCache = currentFileHashes;
12751
13193
  this.saveFileHashCache();
12752
13194
  }
12753
- this.finalizeFailedBatchWriteState(failedProcessing.state);
12754
- database.commitWriteTransaction();
12755
- writeTransactionActive = false;
12756
13195
  if (this.config.indexing.autoGc && stats.removedChunks > 0) {
12757
13196
  const gcReset = await this.maybeRunOrphanGc();
12758
13197
  if (gcReset) {
@@ -12776,6 +13215,9 @@ var Indexer = class _Indexer {
12776
13215
  if (forceScopedReembed && failedForcedChunkIds.size === 0) {
12777
13216
  database.deleteMetadata(this.getProjectForceReembedMetadataKey());
12778
13217
  }
13218
+ if (forceScopedReembed) {
13219
+ database.setMetadata(this.getProjectMigrationFinalizedMetadataKey(), "true");
13220
+ }
12779
13221
  database.setMetadata(swiftParserMetadataKey, SWIFT_PARSER_VERSION);
12780
13222
  database.setMetadata(metalParserMetadataKey, METAL_PARSER_VERSION);
12781
13223
  database.setMetadata(symbolExtractorMetadataKey, SYMBOL_EXTRACTOR_VERSION);
@@ -12886,26 +13328,41 @@ var Indexer = class _Indexer {
12886
13328
  shouldPrefilterByBranch: branchChunkIds !== null && (this.config.scope === "global" || hasInitializedBranchCatalog)
12887
13329
  };
12888
13330
  }
12889
- searchCandidatesWithBranchPrefilter(initialLimit, totalCount, branchChunkIds, shouldPrefilterByBranch, search, getChunkId) {
13331
+ searchCandidatesWithAllowedIds(initialLimit, totalCount, allowedChunkIds, shouldPrefilter, search, getChunkId) {
12890
13332
  const normalizedLimit = Math.max(0, Math.floor(initialLimit));
12891
13333
  if (normalizedLimit === 0) return [];
12892
- if (!shouldPrefilterByBranch || !branchChunkIds) {
13334
+ if (!shouldPrefilter || !allowedChunkIds) {
12893
13335
  return search(normalizedLimit);
12894
13336
  }
12895
- const targetCount = Math.min(normalizedLimit, branchChunkIds.size);
13337
+ const targetCount = Math.min(normalizedLimit, allowedChunkIds.size);
12896
13338
  if (targetCount === 0 || totalCount === 0) return [];
12897
13339
  let requestedLimit = Math.min(normalizedLimit, totalCount);
12898
13340
  while (true) {
12899
13341
  const results = search(requestedLimit);
12900
- const branchResults = results.filter((candidate) => branchChunkIds.has(getChunkId(candidate)));
12901
- if (branchResults.length >= targetCount || results.length < requestedLimit || requestedLimit >= totalCount) {
12902
- return branchResults;
13342
+ const allowedResults = results.filter((candidate) => allowedChunkIds.has(getChunkId(candidate)));
13343
+ if (allowedResults.length >= targetCount || results.length < requestedLimit || requestedLimit >= totalCount) {
13344
+ return allowedResults;
12903
13345
  }
12904
13346
  const nextLimit = Math.min(totalCount, Math.max(requestedLimit + 1, requestedLimit * 2));
12905
- if (nextLimit === requestedLimit) return branchResults;
13347
+ if (nextLimit === requestedLimit) return allowedResults;
12906
13348
  requestedLimit = nextLimit;
12907
13349
  }
12908
13350
  }
13351
+ getTemporalChunkIds(database, options) {
13352
+ if (!options?.blameSince && !options?.blameUntil) return null;
13353
+ const since = options.blameSince ? parseBlameTimestamp(options.blameSince, false) : void 0;
13354
+ const until = options.blameUntil ? parseBlameTimestamp(options.blameUntil, true) : void 0;
13355
+ if (since === null || until === null) {
13356
+ return /* @__PURE__ */ new Set();
13357
+ }
13358
+ return new Set(database.getChunkIdsByBlameDate(since, until));
13359
+ }
13360
+ intersectChunkIdSets(first, second) {
13361
+ if (first === null) return second;
13362
+ if (second === null) return first;
13363
+ const [smaller, larger] = first.size <= second.size ? [first, second] : [second, first];
13364
+ return new Set(Array.from(smaller).filter((chunkId) => larger.has(chunkId)));
13365
+ }
12909
13366
  buildCandidateSnapshot(candidate) {
12910
13367
  return {
12911
13368
  id: candidate.id,
@@ -12920,13 +13377,16 @@ var Indexer = class _Indexer {
12920
13377
  buildCandidateSnapshotList(candidates) {
12921
13378
  return candidates.map((candidate) => this.buildCandidateSnapshot(candidate));
12922
13379
  }
12923
- searchSemanticCandidates(store, embedding, initialLimit, branchChunkIds, shouldPrefilterByBranch) {
12924
- return this.searchCandidatesWithBranchPrefilter(
12925
- initialLimit,
12926
- store.count(),
13380
+ searchSemanticCandidates(store, embedding, initialLimit, branchChunkIds, shouldPrefilterByBranch, temporalChunkIds) {
13381
+ const availableCount = temporalChunkIds?.size ?? store.count();
13382
+ if (availableCount === 0) return [];
13383
+ const allowedIds = temporalChunkIds === null ? void 0 : Array.from(temporalChunkIds);
13384
+ return this.searchCandidatesWithAllowedIds(
13385
+ Math.min(initialLimit, availableCount),
13386
+ availableCount,
12927
13387
  branchChunkIds,
12928
13388
  shouldPrefilterByBranch,
12929
- (requestedLimit) => store.search(embedding, requestedLimit),
13389
+ (requestedLimit) => store.search(embedding, requestedLimit, allowedIds),
12930
13390
  (candidate) => candidate.id
12931
13391
  );
12932
13392
  }
@@ -12951,8 +13411,9 @@ var Indexer = class _Indexer {
12951
13411
  const rerankTopN = this.config.search.rerankTopN;
12952
13412
  const filterByBranch = options?.filterByBranch ?? true;
12953
13413
  const sourceIntent = options?.definitionIntent === true || classifyQueryIntentRaw(query) === "source";
13414
+ const prioritizeSourcePaths = sourceIntent || options?.prioritizeSourcePaths === true;
12954
13415
  const identifierHints = extractIdentifierHints(query);
12955
- const candidateLimit = maxResults * (sourceIntent ? 12 : 4);
13416
+ const candidateLimit = maxResults * (prioritizeSourcePaths ? 12 : 4);
12956
13417
  this.logger.search("debug", "Starting search", {
12957
13418
  query,
12958
13419
  maxResults,
@@ -12983,6 +13444,7 @@ var Indexer = class _Indexer {
12983
13444
  branchChunkIds = new Set(branchCatalogKeys.flatMap((branchKey) => database.getBranchChunkIds(branchKey)));
12984
13445
  branchSymbolIds = new Set(branchCatalogKeys.flatMap((branchKey) => database.getBranchSymbolIds(branchKey)));
12985
13446
  }
13447
+ const temporalChunkIds = this.getTemporalChunkIds(database, options);
12986
13448
  const { hasInitializedBranchCatalog, shouldPrefilterByBranch } = this.getBranchPrefilterState(database, branchChunkIds);
12987
13449
  const prefilterMs = import_perf_hooks.performance.now() - prefilterStartTime;
12988
13450
  const vectorStartTime = import_perf_hooks.performance.now();
@@ -12991,7 +13453,8 @@ var Indexer = class _Indexer {
12991
13453
  embedding,
12992
13454
  candidateLimit,
12993
13455
  branchChunkIds,
12994
- shouldPrefilterByBranch
13456
+ shouldPrefilterByBranch,
13457
+ temporalChunkIds
12995
13458
  ) : [];
12996
13459
  const vectorMs = import_perf_hooks.performance.now() - vectorStartTime;
12997
13460
  const keywordStartTime = import_perf_hooks.performance.now();
@@ -13001,7 +13464,8 @@ var Indexer = class _Indexer {
13001
13464
  store,
13002
13465
  invertedIndex,
13003
13466
  branchChunkIds,
13004
- shouldPrefilterByBranch
13467
+ shouldPrefilterByBranch,
13468
+ temporalChunkIds
13005
13469
  );
13006
13470
  const keywordMs = import_perf_hooks.performance.now() - keywordStartTime;
13007
13471
  const scopedSemanticCandidates = semanticCandidates.filter(
@@ -13023,7 +13487,7 @@ var Indexer = class _Indexer {
13023
13487
  rerankTopN,
13024
13488
  limit: maxResults,
13025
13489
  hybridWeight: rankingHybridWeight,
13026
- prioritizeSourcePaths: sourceIntent
13490
+ prioritizeSourcePaths
13027
13491
  });
13028
13492
  const rerankedCombined = await this.rerankCandidatesWithApi(query, combined, {
13029
13493
  definitionIntent: options?.definitionIntent === true,
@@ -13059,10 +13523,11 @@ var Indexer = class _Indexer {
13059
13523
  branchSymbolIds,
13060
13524
  maxResults,
13061
13525
  union,
13062
- sourceIntent
13526
+ sourceIntent,
13527
+ options?.definitionIntent === true && ((options.directory?.trim().length ?? 0) > 0 || (options.fileType?.trim().length ?? 0) > 0)
13063
13528
  );
13064
13529
  const prePrimaryLane = mergeTieredResults(deterministicIdentifierLane, identifierLane, maxResults * 4);
13065
- const primaryLane = mergeTieredResults(prePrimaryLane, symbolLane, maxResults * 4);
13530
+ const primaryLane = options?.definitionIntent === true ? mergeTieredResults(symbolLane, prePrimaryLane, maxResults * 4) : mergeTieredResults(prePrimaryLane, symbolLane, maxResults * 4);
13066
13531
  const tiered = mergeTieredResults(primaryLane, rescued, maxResults * 4);
13067
13532
  const hasCodeHints = extractCodeTermHints(query).length > 0 || identifierHints.length > 0;
13068
13533
  const baseFiltered = tiered.filter(
@@ -13157,14 +13622,18 @@ var Indexer = class _Indexer {
13157
13622
  })
13158
13623
  );
13159
13624
  }
13160
- async keywordSearch(query, limit, store, invertedIndex, branchChunkIds = null, shouldPrefilterByBranch = false) {
13625
+ async keywordSearch(query, limit, store, invertedIndex, branchChunkIds = null, shouldPrefilterByBranch = false, temporalChunkIds = null) {
13161
13626
  const normalizedLimit = Math.max(0, Math.floor(limit));
13162
13627
  if (normalizedLimit === 0) return [];
13163
- const scoreEntries = this.searchCandidatesWithBranchPrefilter(
13628
+ const allowedChunkIds = this.intersectChunkIdSets(
13629
+ shouldPrefilterByBranch ? branchChunkIds : null,
13630
+ temporalChunkIds
13631
+ );
13632
+ const scoreEntries = this.searchCandidatesWithAllowedIds(
13164
13633
  normalizedLimit,
13165
13634
  invertedIndex.getDocumentCount(),
13166
- branchChunkIds,
13167
- shouldPrefilterByBranch,
13635
+ allowedChunkIds,
13636
+ allowedChunkIds !== null,
13168
13637
  (requestedLimit) => Array.from(invertedIndex.search(query, requestedLimit)),
13169
13638
  ([chunkId]) => chunkId
13170
13639
  );
@@ -13249,7 +13718,17 @@ var Indexer = class _Indexer {
13249
13718
  );
13250
13719
  const currentFileHashes = /* @__PURE__ */ new Map();
13251
13720
  for (const file of files) {
13252
- currentFileHashes.set(this.toStoredFilePath(file.path), hashFile(file.path));
13721
+ let hash;
13722
+ try {
13723
+ hash = hashFile(file.path);
13724
+ } catch (error) {
13725
+ this.logger.warn("Skipped unreadable file during freshness check", {
13726
+ path: file.path,
13727
+ error: getErrorMessage4(error)
13728
+ });
13729
+ return { readable: false, current: false, reason: "unreadable" };
13730
+ }
13731
+ currentFileHashes.set(this.toStoredFilePath(file.path), hash);
13253
13732
  }
13254
13733
  const scopedRoots = this.config.scope === "global" ? this.getScopedRoots() : null;
13255
13734
  const cachedFileHashes = scopedRoots ? new Map(Array.from(this.fileHashCache).filter(([filePath]) => this.isFileInCurrentScope(filePath, scopedRoots))) : this.fileHashCache;
@@ -13275,69 +13754,87 @@ var Indexer = class _Indexer {
13275
13754
  async forceIndex(onProgress) {
13276
13755
  return this.withIndexMutationLease("force-index", async (recoveredOwners) => {
13277
13756
  await this.ensureInitializedUnlocked(recoveredOwners);
13278
- await this.clearIndexUnlocked();
13757
+ const recovery = this.beginClearRecoveryState();
13758
+ await this.clearIndexUnlocked(recovery.compatibilityDecision);
13759
+ this.finishClearRecoveryState();
13279
13760
  return this.indexUnlocked(onProgress, [], true);
13280
13761
  });
13281
13762
  }
13282
13763
  async clearIndex() {
13283
13764
  await this.withIndexMutationLease("clear", async (recoveredOwners) => {
13284
13765
  await this.ensureInitializedUnlocked(recoveredOwners);
13285
- await this.clearIndexUnlocked();
13766
+ const recovery = this.beginClearRecoveryState();
13767
+ await this.clearIndexUnlocked(recovery.compatibilityDecision);
13286
13768
  });
13287
13769
  }
13288
- async clearIndexUnlocked() {
13770
+ clearGlobalIndexDataUnlocked(projectRoot = this.projectRoot) {
13289
13771
  const { store, invertedIndex, database } = this.requireLoadedIndexState();
13290
- if (this.config.scope === "global") {
13291
- store.load();
13292
- invertedIndex.load();
13293
- this.loadFileHashCache();
13294
- const roots = this.getScopedRoots();
13295
- const compatibility = this.checkCompatibility();
13296
- const allMetadata = store.getAllMetadata();
13297
- const hasForeignData = allMetadata.some(({ metadata }) => !this.isFileInCurrentScope(metadata.filePath, roots)) || this.hasForeignScopedBranchData() || this.hasForeignScopedFileHashData(roots) || this.hasForeignScopedFailedBatches(roots);
13298
- if (!compatibility.compatible && hasForeignData) {
13299
- if (compatibility.code === "EMBEDDING_STRATEGY_MISMATCH" /* EMBEDDING_STRATEGY_MISMATCH */) {
13300
- this.clearSharedIndexProjectData(store, invertedIndex, database, roots);
13301
- this.clearScopedFileHashCache(roots);
13302
- this.clearScopedFailedBatches(roots);
13303
- database.setMetadata(this.getProjectForceReembedMetadataKey(), "true");
13304
- database.deleteMetadata(this.getProjectEmbeddingStrategyMetadataKey());
13772
+ const clearedBranchKeys = database.getAllBranches();
13773
+ store.clear();
13774
+ store.save();
13775
+ invertedIndex.clear();
13776
+ this.saveInvertedIndex(invertedIndex);
13777
+ this.fileHashCache.clear();
13778
+ this.saveFileHashCache();
13779
+ database.clearAllIndexedData();
13780
+ this.deleteBranchCommitMetadata(database, clearedBranchKeys);
13781
+ this.clearFailedBatchState();
13782
+ database.deleteMetadata("index.version");
13783
+ database.deleteMetadata("index.pathStorageVersion");
13784
+ database.deleteMetadata("index.embeddingProvider");
13785
+ database.deleteMetadata("index.embeddingModel");
13786
+ database.deleteMetadata("index.embeddingDimensions");
13787
+ database.deleteMetadata("index.embeddingStrategyVersion");
13788
+ const projectIdentityHash = this.getProjectIdentityHash(projectRoot);
13789
+ database.deleteMetadata(this.getProjectEmbeddingStrategyMetadataKey(projectIdentityHash));
13790
+ database.deleteMetadata(this.getProjectForceReembedMetadataKey(projectIdentityHash));
13791
+ database.deleteMetadata(this.getLegacyMigrationMetadataKey(projectIdentityHash));
13792
+ database.deleteMetadata("index.createdAt");
13793
+ database.deleteMetadata("index.updatedAt");
13794
+ this.indexCompatibility = this.validateIndexCompatibility(this.configuredProviderInfo);
13795
+ }
13796
+ clearGlobalIndexUnlocked(projectRoot = this.projectRoot, roots = this.getScopedRoots(), recoveryDecision) {
13797
+ const { store, invertedIndex, database } = this.requireLoadedIndexState();
13798
+ store.load();
13799
+ invertedIndex.load();
13800
+ this.loadFileHashCache();
13801
+ const compatibility = this.checkCompatibility();
13802
+ const compatibilityDecision = recoveryDecision ?? (compatibility.compatible ? "compatible" : compatibility.code === "EMBEDDING_STRATEGY_MISMATCH" /* EMBEDDING_STRATEGY_MISMATCH */ ? "embedding-strategy-mismatch" : "incompatible");
13803
+ const allMetadata = store.getAllMetadata();
13804
+ const hasForeignData = allMetadata.some(({ metadata }) => !this.isFileInCurrentScope(metadata.filePath, roots)) || this.hasForeignScopedBranchData(projectRoot, roots) || this.hasForeignScopedFileHashData(roots) || this.hasForeignScopedFailedBatches(roots);
13805
+ if (compatibilityDecision !== "compatible" && hasForeignData) {
13806
+ if (compatibilityDecision === "embedding-strategy-mismatch") {
13807
+ this.clearSharedIndexProjectData(store, invertedIndex, database, roots, projectRoot);
13808
+ this.clearScopedFileHashCache(roots);
13809
+ this.clearScopedFailedBatches(roots);
13810
+ const projectIdentityHash = this.getProjectIdentityHash(projectRoot);
13811
+ database.setMetadata(this.getProjectForceReembedMetadataKey(projectIdentityHash), "true");
13812
+ database.deleteMetadata(this.getProjectEmbeddingStrategyMetadataKey(projectIdentityHash));
13813
+ database.deleteMetadata(this.getProjectMigrationFinalizedMetadataKey(projectIdentityHash));
13814
+ if (projectRoot === this.projectRoot) {
13305
13815
  this.indexCompatibility = { compatible: true };
13306
- return;
13307
13816
  }
13308
- throw new Error(
13309
- `Global index compatibility reset is unsafe because the shared index contains files from other projects. The current global index cannot be force-rebuilt for ${this.projectRoot} without deleting other repositories' indexed data. Use scope="project" for isolated rebuilds, or manually delete the shared global index if you intend to rebuild all projects.`
13310
- );
13311
- }
13312
- if (!hasForeignData) {
13313
- const clearedBranchKeys2 = database.getAllBranches();
13314
- store.clear();
13315
- store.save();
13316
- invertedIndex.clear();
13317
- this.saveInvertedIndex(invertedIndex);
13318
- this.fileHashCache.clear();
13319
- this.saveFileHashCache();
13320
- database.clearAllIndexedData();
13321
- this.deleteBranchCommitMetadata(database, clearedBranchKeys2);
13322
- this.clearFailedBatchState();
13323
- database.deleteMetadata("index.version");
13324
- database.deleteMetadata("index.pathStorageVersion");
13325
- database.deleteMetadata("index.embeddingProvider");
13326
- database.deleteMetadata("index.embeddingModel");
13327
- database.deleteMetadata("index.embeddingDimensions");
13328
- database.deleteMetadata("index.embeddingStrategyVersion");
13329
- database.deleteMetadata(this.getProjectEmbeddingStrategyMetadataKey());
13330
- database.deleteMetadata(this.getProjectForceReembedMetadataKey());
13331
- database.deleteMetadata(this.getLegacyMigrationMetadataKey());
13332
- database.deleteMetadata("index.createdAt");
13333
- database.deleteMetadata("index.updatedAt");
13334
- this.indexCompatibility = this.validateIndexCompatibility(this.configuredProviderInfo);
13335
13817
  return;
13336
13818
  }
13337
- this.clearSharedIndexProjectData(store, invertedIndex, database, roots);
13338
- this.clearScopedFileHashCache(roots);
13339
- this.clearScopedFailedBatches(roots);
13819
+ throw new Error(
13820
+ `Global index compatibility reset is unsafe because the shared index contains files from other projects. The current global index cannot be force-rebuilt for ${projectRoot} without deleting other repositories' indexed data. Use scope="project" for isolated rebuilds, or manually delete the shared global index if you intend to rebuild all projects.`
13821
+ );
13822
+ }
13823
+ if (!hasForeignData) {
13824
+ this.clearGlobalIndexDataUnlocked(projectRoot);
13825
+ return;
13826
+ }
13827
+ this.clearSharedIndexProjectData(store, invertedIndex, database, roots, projectRoot);
13828
+ this.clearScopedFileHashCache(roots);
13829
+ this.clearScopedFailedBatches(roots);
13830
+ if (projectRoot === this.projectRoot) {
13340
13831
  this.indexCompatibility = compatibility;
13832
+ }
13833
+ }
13834
+ async clearIndexUnlocked(recoveryDecision) {
13835
+ const { store, invertedIndex, database } = this.requireLoadedIndexState();
13836
+ if (this.config.scope === "global") {
13837
+ this.clearGlobalIndexUnlocked(this.projectRoot, this.getScopedRoots(), recoveryDecision);
13341
13838
  return;
13342
13839
  }
13343
13840
  if (!this.isProjectOwnedIndexPath()) {
@@ -13503,6 +14000,7 @@ var Indexer = class _Indexer {
13503
14000
  )) {
13504
14001
  const chunks = retryBatch.map(({ chunk }) => chunk);
13505
14002
  const attemptCounts = new Map(retryBatch.map(({ chunk, attemptCount }) => [chunk.id, attemptCount]));
14003
+ this.restoreMissingChunkRows(database, chunks);
13506
14004
  const batchResult = await this.processPendingChunkBatch(chunks, {
13507
14005
  store,
13508
14006
  provider,
@@ -13517,6 +14015,7 @@ var Indexer = class _Indexer {
13517
14015
  forceReembed: false,
13518
14016
  reuseCachedEmbeddings: false,
13519
14017
  incrementRepeatedFailures: false,
14018
+ forceSingleItemBatches: true,
13520
14019
  onSucceeded: (succeededChunks) => {
13521
14020
  database.addChunksToBranchBatch(
13522
14021
  this.getBranchCatalogKey(),
@@ -13538,9 +14037,12 @@ var Indexer = class _Indexer {
13538
14037
  this.saveInvertedIndex(invertedIndex);
13539
14038
  }
13540
14039
  if (roots && succeeded > 0 && remaining === 0 && this.hasProjectForceReembedPending()) {
13541
- database.deleteMetadata(this.getProjectForceReembedMetadataKey());
13542
- this.saveIndexMetadata(configuredProviderInfo);
13543
- this.indexCompatibility = { compatible: true };
14040
+ const migrationFinalized = database.getMetadata(this.getProjectMigrationFinalizedMetadataKey()) === "true";
14041
+ if (migrationFinalized) {
14042
+ database.deleteMetadata(this.getProjectForceReembedMetadataKey());
14043
+ this.saveIndexMetadata(configuredProviderInfo);
14044
+ this.indexCompatibility = { compatible: true };
14045
+ }
13544
14046
  }
13545
14047
  return { succeeded, failed, remaining };
13546
14048
  }
@@ -13562,7 +14064,8 @@ var Indexer = class _Indexer {
13562
14064
  latestById.set(chunkId, {
13563
14065
  attemptCount: batch.attemptCount,
13564
14066
  error: batch.error,
13565
- lastAttempt: batch.lastAttempt
14067
+ lastAttempt: batch.lastAttempt,
14068
+ chunks: [rawChunk]
13566
14069
  });
13567
14070
  }
13568
14071
  }
@@ -13629,6 +14132,7 @@ var Indexer = class _Indexer {
13629
14132
  this.getBranchCatalogKeys().flatMap((branchKey) => database.getBranchChunkIds(branchKey))
13630
14133
  );
13631
14134
  }
14135
+ const temporalChunkIds = this.getTemporalChunkIds(database, options);
13632
14136
  const { hasInitializedBranchCatalog, shouldPrefilterByBranch } = this.getBranchPrefilterState(database, branchChunkIds);
13633
14137
  const prefilterMs = import_perf_hooks.performance.now() - prefilterStartTime;
13634
14138
  const vectorStartTime = import_perf_hooks.performance.now();
@@ -13637,7 +14141,8 @@ var Indexer = class _Indexer {
13637
14141
  embedding,
13638
14142
  limit * 2,
13639
14143
  branchChunkIds,
13640
- shouldPrefilterByBranch
14144
+ shouldPrefilterByBranch,
14145
+ temporalChunkIds
13641
14146
  );
13642
14147
  const vectorMs = import_perf_hooks.performance.now() - vectorStartTime;
13643
14148
  if (this.config.scope !== "global" && branchChunkIds && !hasInitializedBranchCatalog) {
@@ -14386,9 +14891,11 @@ async function searchCodebase(projectRoot, host, query, options = {}) {
14386
14891
  contextLines: options.contextLines,
14387
14892
  metadataOnly: options.metadataOnly,
14388
14893
  definitionIntent: options.definitionIntent,
14894
+ prioritizeSourcePaths: options.prioritizeSourcePaths,
14389
14895
  blameAuthor: options.blameAuthor,
14390
14896
  blameSha: options.blameSha,
14391
14897
  blameSince: options.blameSince,
14898
+ blameUntil: options.blameUntil,
14392
14899
  trace: options.trace
14393
14900
  });
14394
14901
  }
@@ -14434,7 +14941,9 @@ async function findSimilarCode(projectRoot, host, code, options = {}) {
14434
14941
  fileType: options.fileType,
14435
14942
  directory: options.directory,
14436
14943
  chunkType: options.chunkType,
14437
- excludeFile: options.excludeFile
14944
+ excludeFile: options.excludeFile,
14945
+ blameSince: options.blameSince,
14946
+ blameUntil: options.blameUntil
14438
14947
  });
14439
14948
  }
14440
14949
  async function implementationLookup(projectRoot, host, query, options = {}) {
@@ -17973,13 +18482,19 @@ async function resolveSearchContext(input, operations) {
17973
18482
  (trace) => operations.lookup(symbol, MAX_CONTEXT_RESULT_LIMIT, scope, input.diagnostic ? trace : void 0)
17974
18483
  );
17975
18484
  };
17976
- const tryConceptualSearch = async (searchQuery, scope, relaxedFieldsForAttempt) => {
18485
+ const tryConceptualSearch = async (searchQuery, scope, relaxedFieldsForAttempt, prioritizeSourcePaths) => {
17977
18486
  return recordAttempt(
17978
18487
  "conceptual",
17979
18488
  searchQuery,
17980
18489
  scope,
17981
18490
  relaxedFieldsForAttempt,
17982
- (trace) => operations.search(searchQuery, MAX_CONTEXT_RESULT_LIMIT, scope, input.diagnostic ? trace : void 0)
18491
+ (trace) => operations.search(
18492
+ searchQuery,
18493
+ MAX_CONTEXT_RESULT_LIMIT,
18494
+ scope,
18495
+ input.diagnostic ? trace : void 0,
18496
+ { prioritizeSourcePaths }
18497
+ )
17983
18498
  );
17984
18499
  };
17985
18500
  const findSuccessfulAttemptState = (route) => {
@@ -18107,10 +18622,12 @@ Explicit symbol lookup only; conceptual search was not attempted.`
18107
18622
  }
18108
18623
  }
18109
18624
  for (const attempt of conceptualAttemptPlan) {
18625
+ const attemptIntent = analyzeQueryIntent(attempt.queryText);
18626
+ const prioritizeSourcePaths = attemptIntent.primary !== "docs" && attemptIntent.primary !== "test";
18110
18627
  if (inferredSymbol && attempt.queryText === inferredSymbol && attempt.queryText !== query) {
18111
18628
  decisions.fallbackFromOriginalConceptualToInferred = true;
18112
18629
  }
18113
- const results = await tryConceptualSearch(attempt.queryText, attempt.scope, attempt.relaxed);
18630
+ const results = await tryConceptualSearch(attempt.queryText, attempt.scope, attempt.relaxed, prioritizeSourcePaths);
18114
18631
  if (results.length > 0) {
18115
18632
  const heading = buildPackHeading("conceptual", decisions);
18116
18633
  const intent = analyzeQueryIntent(attempt.queryText);
@@ -18247,12 +18764,13 @@ async function resolveCodebaseContextUnmeasured(projectRoot, host, input) {
18247
18764
  directory: scope.directory,
18248
18765
  trace
18249
18766
  }),
18250
- search: (queryText, retrievalLimit, scope, trace) => searchCodebase(projectRoot, host, queryText, {
18767
+ search: (queryText, retrievalLimit, scope, trace, searchOptions) => searchCodebase(projectRoot, host, queryText, {
18251
18768
  limit: retrievalLimit,
18252
18769
  fileType: scope.fileType,
18253
18770
  directory: scope.directory,
18254
18771
  metadataOnly: true,
18255
- trace
18772
+ trace,
18773
+ prioritizeSourcePaths: searchOptions?.prioritizeSourcePaths
18256
18774
  })
18257
18775
  });
18258
18776
  }
@@ -19167,7 +19685,8 @@ var codebase_peek = tool({
19167
19685
  chunkType: z3.enum(CHUNK_TYPE_VALUES).optional().describe("Filter by code chunk type"),
19168
19686
  blameAuthor: z3.string().optional().describe("Filter by git blame author name or email"),
19169
19687
  blameSha: z3.string().optional().describe("Filter by git blame commit SHA or prefix"),
19170
- blameSince: z3.string().optional().describe("Filter to chunks last changed on or after this date (e.g., 2025-01-01)")
19688
+ blameSince: z3.string().optional().describe("Filter to chunks last changed on or after this date (e.g., 2025-01-01)"),
19689
+ blameUntil: z3.string().optional().describe("Filter to chunks last changed on or before this date (e.g., 2025-01-31)")
19171
19690
  },
19172
19691
  async execute(args, context) {
19173
19692
  return searchCodebaseWithEffectiveness(context?.worktree, DEFAULT_HOST, "peek", args.query, {
@@ -19178,7 +19697,8 @@ var codebase_peek = tool({
19178
19697
  metadataOnly: true,
19179
19698
  blameAuthor: args.blameAuthor,
19180
19699
  blameSha: args.blameSha,
19181
- blameSince: args.blameSince
19700
+ blameSince: args.blameSince,
19701
+ blameUntil: args.blameUntil
19182
19702
  }, (results) => {
19183
19703
  const text = formatCodebasePeek(results);
19184
19704
  return { output: text, text };
@@ -19240,7 +19760,9 @@ var find_similar = tool({
19240
19760
  fileType: z3.string().optional().describe("Filter by file extension (e.g., 'ts', 'py', 'rs')"),
19241
19761
  directory: z3.string().optional().describe("Filter by directory path (e.g., 'src/utils', 'lib')"),
19242
19762
  chunkType: z3.enum(CHUNK_TYPE_VALUES).optional().describe("Filter by code chunk type"),
19243
- excludeFile: z3.string().optional().describe("Exclude results from this file path (useful when searching for duplicates of code from a specific file)")
19763
+ excludeFile: z3.string().optional().describe("Exclude results from this file path (useful when searching for duplicates of code from a specific file)"),
19764
+ blameSince: z3.string().optional().describe("Filter to chunks last changed on or after this date (e.g., 2025-01-01)"),
19765
+ blameUntil: z3.string().optional().describe("Filter to chunks last changed on or before this date (e.g., 2025-01-31)")
19244
19766
  },
19245
19767
  async execute(args, context) {
19246
19768
  const results = await findSimilarCode(context?.worktree, DEFAULT_HOST, args.code, {
@@ -19248,7 +19770,9 @@ var find_similar = tool({
19248
19770
  fileType: args.fileType,
19249
19771
  directory: args.directory,
19250
19772
  chunkType: args.chunkType,
19251
- excludeFile: args.excludeFile
19773
+ excludeFile: args.excludeFile,
19774
+ blameSince: args.blameSince,
19775
+ blameUntil: args.blameUntil
19252
19776
  });
19253
19777
  if (results.length === 0) {
19254
19778
  return "No similar code found. Try a different snippet or run index_codebase first.";
@@ -19267,7 +19791,8 @@ var codebase_search = tool({
19267
19791
  contextLines: z3.number().optional().describe("Number of extra lines to include before/after each match (default: 0)"),
19268
19792
  blameAuthor: z3.string().optional().describe("Filter by git blame author name or email"),
19269
19793
  blameSha: z3.string().optional().describe("Filter by git blame commit SHA or prefix"),
19270
- blameSince: z3.string().optional().describe("Filter to chunks last changed on or after this date (e.g., 2025-01-01)")
19794
+ blameSince: z3.string().optional().describe("Filter to chunks last changed on or after this date (e.g., 2025-01-01)"),
19795
+ blameUntil: z3.string().optional().describe("Filter to chunks last changed on or before this date (e.g., 2025-01-31)")
19271
19796
  },
19272
19797
  async execute(args, context) {
19273
19798
  return searchCodebaseWithEffectiveness(context?.worktree, DEFAULT_HOST, "search", args.query, {
@@ -19278,7 +19803,8 @@ var codebase_search = tool({
19278
19803
  contextLines: args.contextLines,
19279
19804
  blameAuthor: args.blameAuthor,
19280
19805
  blameSha: args.blameSha,
19281
- blameSince: args.blameSince
19806
+ blameSince: args.blameSince,
19807
+ blameUntil: args.blameUntil
19282
19808
  }, (results) => {
19283
19809
  const text = results.length === 0 ? "No matching code found. Try a different query or run index_codebase first." : formatSearchResults(results, "score");
19284
19810
  return { output: text, text };
@@ -19491,6 +20017,12 @@ var PI_TOOL_NAMES = [
19491
20017
  TOOL_NAME.PI_KNOWLEDGE_BASE_ADD,
19492
20018
  TOOL_NAME.PI_KNOWLEDGE_BASE_REMOVE
19493
20019
  ];
20020
+ var MCP_TOOL_NAMES = [
20021
+ ...PORTABLE_TOOL_NAMES,
20022
+ TOOL_NAME.ADD_KNOWLEDGE_BASE,
20023
+ TOOL_NAME.LIST_KNOWLEDGE_BASES,
20024
+ TOOL_NAME.REMOVE_KNOWLEDGE_BASE
20025
+ ];
19494
20026
 
19495
20027
  // src/commands/loader.ts
19496
20028
  var import_fs16 = require("fs");