@autorag/librarian 2.13.5 → 2.13.6

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/mcp/index.js CHANGED
@@ -10374,195 +10374,225 @@ async function syncWithRegistry(options, registry, isolated) {
10374
10374
  });
10375
10375
  const pending = new Set(plan.decisions.filter((decision) => decision.action === "process" || decision.action === "retry").map((decision) => decision.key));
10376
10376
  const deferred = new Map(plan.decisions.filter((decision) => decision.deferred === true).map((decision) => [decision.key, decision]));
10377
- for (const decision of plan.decisions) {
10378
- if (decision.action !== "delete")
10379
- continue;
10380
- const path = pathsByKey.get(decision.key);
10381
- await session.delete(decision.key);
10382
- if (path === undefined)
10383
- continue;
10384
- if (previous.entries[path])
10385
- deleted += 1;
10386
- if (!currentPaths.has(path))
10387
- diagnostics.push({
10388
- code: "deleted-mirror",
10389
- severity: "info",
10390
- message: "A previously indexed document is gone; its parsed mirror was removed.",
10391
- source: path
10392
- });
10393
- }
10394
- await gate.classify(current.filter((entry) => pending.has(mirrorStageKey(entry.virtualPath))).map((entry) => entry.sourcePath));
10395
- for (const [position, entry] of current.entries()) {
10396
- options.onProgress?.({
10397
- processed: position,
10398
- total: current.length,
10399
- written,
10400
- deleted,
10401
- skipped,
10402
- currentVirtualPath: entry.virtualPath
10403
- });
10404
- if (options.signal?.aborted)
10405
- throw abortReason(options.signal);
10406
- const omission = omitted.get(entry.virtualPath);
10407
- if (omission !== undefined) {
10408
- if (pending.has(mirrorStageKey(entry.virtualPath))) {
10409
- if (previous.entries[entry.virtualPath])
10410
- deleted += 1;
10411
- await session.skip(mirrorStageKey(entry.virtualPath), versions.get(entry.virtualPath), omission.reason);
10412
- }
10413
- skipped += 1;
10414
- if (omission.diagnostic)
10415
- diagnostics.push({ ...omission.diagnostic, source: entry.virtualPath });
10416
- continue;
10417
- }
10418
- const deferredFailure = deferred.get(mirrorStageKey(entry.virtualPath));
10419
- if (deferredFailure !== undefined) {
10420
- skipped += 1;
10421
- const code = deferredFailure.error === "source-unreadable" ? "source-unreadable" : "parser-failed";
10422
- diagnostics.push({
10423
- code,
10424
- severity: "warning",
10425
- source: entry.virtualPath,
10426
- message: code === "parser-failed" ? PARSER_FAILURE_MESSAGE : "The source could not be read; retry is deferred until the source or settings change, or an explicit retry is requested."
10427
- });
10428
- continue;
10429
- }
10430
- const parser = registry.getForVirtualPath(entry.virtualPath);
10431
- const outputPath = parsedOutputPath(options.root, entry.virtualPath);
10432
- const cloudDecision = pending.has(mirrorStageKey(entry.virtualPath)) ? await gate.decide(entry.sourcePath, entry.sizeBytes, dirname12(entry.sourcePath)) : { kind: "local" };
10433
- if (cloudDecision.kind === "skip") {
10434
- skipped += 1;
10435
- diagnostics.push({
10436
- code: "cloud-placeholder-skipped",
10437
- severity: "info",
10438
- message: `The file is not downloaded and its provider has no verified release path, so it was not read; its last indexed copy was kept. Make it available offline to index it.`,
10439
- source: entry.virtualPath,
10440
- ...cloudDecision.provider === undefined ? {} : { reason: cloudDecision.provider }
10441
- });
10442
- continue;
10443
- }
10444
- if (cloudDecision.kind === "defer") {
10445
- skipped += 1;
10446
- diagnostics.push({
10447
- code: "cloud-download-deferred",
10448
- severity: "info",
10449
- message: "The download budget for this refresh is used up; the file stays queued for the next refresh.",
10450
- source: entry.virtualPath
10451
- });
10452
- continue;
10453
- }
10454
- if (pending.has(mirrorStageKey(entry.virtualPath))) {
10455
- let bytes;
10456
- try {
10457
- bytes = await withTransientRetry(() => readFile(entry.sourcePath), transientRetries, transientBackoffMs);
10458
- } catch (error) {
10459
- const failure = sourceReadFailure(error);
10460
- await skipFile(entry, failure, failure.code === "source-unreadable" ? "source-unreadable" : undefined);
10377
+ const applyDeleteDecisions = async () => {
10378
+ for (const decision of plan.decisions) {
10379
+ if (decision.action !== "delete")
10461
10380
  continue;
10462
- }
10463
- const tooSmallImage = standaloneImageTooSmallMessage(entry.virtualPath, bytes);
10464
- if (tooSmallImage !== undefined) {
10465
- await skipFile(entry, { code: "parser-skipped", severity: "warning", message: tooSmallImage }, "parser-skipped");
10381
+ const path = pathsByKey.get(decision.key);
10382
+ await session.delete(decision.key);
10383
+ if (path === undefined)
10466
10384
  continue;
10467
- }
10468
- let parsed;
10469
- try {
10470
- const input = { virtualPath: entry.virtualPath, sourcePath: entry.sourcePath, bytes };
10471
- parsed = isolated !== undefined && ISOLATED_PARSER_NAMES.has(parser.name) ? await isolated.parse(parser.name, input, options.signal) : await parser.parse(input);
10472
- } catch (error) {
10473
- if (options.signal?.aborted)
10474
- throw abortReason(options.signal);
10475
- if (error instanceof ParseError)
10476
- await skipFile(entry, {
10477
- code: "parser-failed",
10478
- severity: "warning",
10479
- message: error.cause instanceof ParseIsolationError ? `${PARSER_FAILURE_MESSAGE} ${error.cause.message}.` : PARSER_FAILURE_MESSAGE
10480
- }, "parser-failed");
10481
- else
10482
- await skipFile(entry, {
10483
- code: "parser-crashed",
10484
- severity: "warning",
10485
- message: PARSER_CRASH_MESSAGE,
10486
- ...error instanceof Error ? { reason: error.name } : {}
10487
- });
10385
+ if (previous.entries[path])
10386
+ deleted += 1;
10387
+ if (!currentPaths.has(path))
10388
+ diagnostics.push({
10389
+ code: "deleted-mirror",
10390
+ severity: "info",
10391
+ message: "A previously indexed document is gone; its parsed mirror was removed.",
10392
+ source: path
10393
+ });
10394
+ }
10395
+ };
10396
+ const deferDeletions = options.onBatch !== undefined;
10397
+ const batchSize = Math.max(1, Math.floor(options.batchSize ?? DEFAULT_MIRROR_BATCH_SIZE));
10398
+ const batchCount = Math.max(1, Math.ceil(current.length / batchSize));
10399
+ let batchIndex = 0;
10400
+ let batchStopped = false;
10401
+ const runBatch = async (virtualPaths, final) => {
10402
+ if (options.onBatch === undefined)
10403
+ return;
10404
+ batchIndex += 1;
10405
+ const outcome = await options.onBatch({ index: batchIndex, total: batchCount, virtualPaths, final });
10406
+ if (outcome?.stop === true)
10407
+ batchStopped = true;
10408
+ };
10409
+ if (!deferDeletions)
10410
+ await applyDeleteDecisions();
10411
+ await gate.classify(current.filter((entry) => pending.has(mirrorStageKey(entry.virtualPath))).map((entry) => entry.sourcePath));
10412
+ for (let batchStart = 0;batchStart < Math.max(current.length, 1); batchStart += batchSize) {
10413
+ const batchEnd = Math.min(batchStart + batchSize, current.length);
10414
+ const finalBatch = batchEnd >= current.length;
10415
+ const changedBefore = written + deleted;
10416
+ for (let position = batchStart;position < batchEnd; position += 1) {
10417
+ const entry = current[position];
10418
+ options.onProgress?.({
10419
+ processed: position,
10420
+ total: current.length,
10421
+ written,
10422
+ deleted,
10423
+ skipped,
10424
+ currentVirtualPath: entry.virtualPath
10425
+ });
10426
+ if (options.signal?.aborted)
10427
+ throw abortReason(options.signal);
10428
+ const omission = omitted.get(entry.virtualPath);
10429
+ if (omission !== undefined) {
10430
+ if (pending.has(mirrorStageKey(entry.virtualPath))) {
10431
+ if (previous.entries[entry.virtualPath])
10432
+ deleted += 1;
10433
+ await session.skip(mirrorStageKey(entry.virtualPath), versions.get(entry.virtualPath), omission.reason);
10434
+ }
10435
+ skipped += 1;
10436
+ if (omission.diagnostic)
10437
+ diagnostics.push({ ...omission.diagnostic, source: entry.virtualPath });
10488
10438
  continue;
10489
10439
  }
10490
- let info;
10491
- try {
10492
- info = await stat3(entry.sourcePath, { bigint: true });
10493
- } catch (error) {
10494
- await skipFile(entry, sourceReadFailure(error));
10440
+ const deferredFailure = deferred.get(mirrorStageKey(entry.virtualPath));
10441
+ if (deferredFailure !== undefined) {
10442
+ skipped += 1;
10443
+ const code = deferredFailure.error === "source-unreadable" ? "source-unreadable" : "parser-failed";
10444
+ diagnostics.push({
10445
+ code,
10446
+ severity: "warning",
10447
+ source: entry.virtualPath,
10448
+ message: code === "parser-failed" ? PARSER_FAILURE_MESSAGE : "The source could not be read; retry is deferred until the source or settings change, or an explicit retry is requested."
10449
+ });
10495
10450
  continue;
10496
10451
  }
10497
- const unchanged = cloudDecision.kind === "download" ? contentIdentity(info) === entry.contentIdentity : statIdentity(info) === entry.statIdentity;
10498
- if (!unchanged) {
10452
+ const parser = registry.getForVirtualPath(entry.virtualPath);
10453
+ const outputPath = parsedOutputPath(options.root, entry.virtualPath);
10454
+ const cloudDecision = pending.has(mirrorStageKey(entry.virtualPath)) ? await gate.decide(entry.sourcePath, entry.sizeBytes, dirname12(entry.sourcePath)) : { kind: "local" };
10455
+ if (cloudDecision.kind === "skip") {
10499
10456
  skipped += 1;
10500
10457
  diagnostics.push({
10501
- code: "source-changed",
10502
- severity: "warning",
10503
- message: "The source changed while parsing; its last good mirror was kept and it will be retried on the next refresh.",
10504
- source: entry.virtualPath
10458
+ code: "cloud-placeholder-skipped",
10459
+ severity: "info",
10460
+ message: `The file is not downloaded and its provider has no verified release path, so it was not read; its last indexed copy was kept. Make it available offline to index it.`,
10461
+ source: entry.virtualPath,
10462
+ ...cloudDecision.provider === undefined ? {} : { reason: cloudDecision.provider }
10505
10463
  });
10506
- await session.fail(mirrorStageKey(entry.virtualPath), versions.get(entry.virtualPath), "source-changed");
10507
10464
  continue;
10508
10465
  }
10509
- const markdown = normalizeMarkdown(parsed.markdown);
10510
- try {
10511
- await withTransientRetry(() => writeFileAtomic(outputPath, markdown), transientRetries, transientBackoffMs);
10512
- } catch (error) {
10513
- const code = errorCode(error);
10514
- if (code !== undefined && Object.hasOwn(WORKSPACE_ERROR_CODES, code))
10515
- throw new MirrorWorkspaceError(options.root, error);
10516
- await skipFile(entry, {
10517
- code: "mirror-write-failed",
10518
- severity: "warning",
10519
- message: error instanceof Error ? error.message : String(error),
10520
- ...code !== undefined ? { reason: code } : {}
10466
+ if (cloudDecision.kind === "defer") {
10467
+ skipped += 1;
10468
+ diagnostics.push({
10469
+ code: "cloud-download-deferred",
10470
+ severity: "info",
10471
+ message: "The download budget for this refresh is used up; the file stays queued for the next refresh.",
10472
+ source: entry.virtualPath
10521
10473
  });
10522
10474
  continue;
10523
10475
  }
10524
- await session.commit(mirrorStageKey(entry.virtualPath), versions.get(entry.virtualPath), {
10525
- path: basename10(outputPath),
10526
- size: Buffer.byteLength(markdown)
10527
- });
10528
- for (const diagnostic of parsed.diagnostics ?? [])
10529
- diagnostics.push({ ...diagnostic, source: entry.virtualPath });
10530
- written += 1;
10531
- if (fingerprintCloud && cloudFilePaths.has(entry.sourcePath)) {
10532
- const existing = cloudFingerprintEntries[entry.sourcePath];
10533
- if (existing === undefined || existing.sourceVersion !== entry.sourceVersion) {
10534
- try {
10535
- const fingerprintResult = await fingerprintWithDupey(entry.sourcePath, options.cloudFingerprints?.dupey ?? {});
10536
- cloudFingerprintEntries[entry.sourcePath] = {
10537
- sourceVersion: entry.sourceVersion,
10538
- exact: fingerprintResult.exact,
10539
- chars: fingerprintResult.chars,
10540
- modified: fingerprintResult.modified,
10541
- revision: fingerprintResult.revision
10542
- };
10543
- cloudFingerprintsDirty = true;
10544
- } catch (error) {
10545
- diagnostics.push({
10546
- code: "cloud-fingerprint-failed",
10476
+ if (pending.has(mirrorStageKey(entry.virtualPath))) {
10477
+ let bytes;
10478
+ try {
10479
+ bytes = await withTransientRetry(() => readFile(entry.sourcePath), transientRetries, transientBackoffMs);
10480
+ } catch (error) {
10481
+ const failure = sourceReadFailure(error);
10482
+ await skipFile(entry, failure, failure.code === "source-unreadable" ? "source-unreadable" : undefined);
10483
+ continue;
10484
+ }
10485
+ const tooSmallImage = standaloneImageTooSmallMessage(entry.virtualPath, bytes);
10486
+ if (tooSmallImage !== undefined) {
10487
+ await skipFile(entry, { code: "parser-skipped", severity: "warning", message: tooSmallImage }, "parser-skipped");
10488
+ continue;
10489
+ }
10490
+ let parsed;
10491
+ try {
10492
+ const input = { virtualPath: entry.virtualPath, sourcePath: entry.sourcePath, bytes };
10493
+ parsed = isolated !== undefined && ISOLATED_PARSER_NAMES.has(parser.name) ? await isolated.parse(parser.name, input, options.signal) : await parser.parse(input);
10494
+ } catch (error) {
10495
+ if (options.signal?.aborted)
10496
+ throw abortReason(options.signal);
10497
+ if (error instanceof ParseError)
10498
+ await skipFile(entry, {
10499
+ code: "parser-failed",
10547
10500
  severity: "warning",
10548
- message: `The cloud or network file was indexed but its duplicate fingerprint could not be read, so it will not take part in exact-duplicate exclusion: ${error instanceof Error ? error.message : String(error)}`,
10549
- source: entry.virtualPath
10501
+ message: error.cause instanceof ParseIsolationError ? `${PARSER_FAILURE_MESSAGE} ${error.cause.message}.` : PARSER_FAILURE_MESSAGE
10502
+ }, "parser-failed");
10503
+ else
10504
+ await skipFile(entry, {
10505
+ code: "parser-crashed",
10506
+ severity: "warning",
10507
+ message: PARSER_CRASH_MESSAGE,
10508
+ ...error instanceof Error ? { reason: error.name } : {}
10550
10509
  });
10551
- }
10510
+ continue;
10552
10511
  }
10553
- }
10554
- if (cloudDecision.kind === "download" && gate.evictAfterParse) {
10555
- const released = await gate.release(entry.sourcePath);
10556
- if (!released.ok) {
10512
+ let info;
10513
+ try {
10514
+ info = await stat3(entry.sourcePath, { bigint: true });
10515
+ } catch (error) {
10516
+ await skipFile(entry, sourceReadFailure(error));
10517
+ continue;
10518
+ }
10519
+ const unchanged = cloudDecision.kind === "download" ? contentIdentity(info) === entry.contentIdentity : statIdentity(info) === entry.statIdentity;
10520
+ if (!unchanged) {
10521
+ skipped += 1;
10557
10522
  diagnostics.push({
10558
- code: "cloud-evict-failed",
10523
+ code: "source-changed",
10559
10524
  severity: "warning",
10560
- message: `The file was indexed but its local copy could not be released: ${released.message}`,
10525
+ message: "The source changed while parsing; its last good mirror was kept and it will be retried on the next refresh.",
10561
10526
  source: entry.virtualPath
10562
10527
  });
10528
+ await session.fail(mirrorStageKey(entry.virtualPath), versions.get(entry.virtualPath), "source-changed");
10529
+ continue;
10530
+ }
10531
+ const markdown = normalizeMarkdown(parsed.markdown);
10532
+ try {
10533
+ await withTransientRetry(() => writeFileAtomic(outputPath, markdown), transientRetries, transientBackoffMs);
10534
+ } catch (error) {
10535
+ const code = errorCode(error);
10536
+ if (code !== undefined && Object.hasOwn(WORKSPACE_ERROR_CODES, code))
10537
+ throw new MirrorWorkspaceError(options.root, error);
10538
+ await skipFile(entry, {
10539
+ code: "mirror-write-failed",
10540
+ severity: "warning",
10541
+ message: error instanceof Error ? error.message : String(error),
10542
+ ...code !== undefined ? { reason: code } : {}
10543
+ });
10544
+ continue;
10545
+ }
10546
+ await session.commit(mirrorStageKey(entry.virtualPath), versions.get(entry.virtualPath), {
10547
+ path: basename10(outputPath),
10548
+ size: Buffer.byteLength(markdown)
10549
+ });
10550
+ for (const diagnostic of parsed.diagnostics ?? [])
10551
+ diagnostics.push({ ...diagnostic, source: entry.virtualPath });
10552
+ written += 1;
10553
+ if (fingerprintCloud && cloudFilePaths.has(entry.sourcePath)) {
10554
+ const existing = cloudFingerprintEntries[entry.sourcePath];
10555
+ if (existing === undefined || existing.sourceVersion !== entry.sourceVersion) {
10556
+ try {
10557
+ const fingerprintResult = await fingerprintWithDupey(entry.sourcePath, options.cloudFingerprints?.dupey ?? {});
10558
+ cloudFingerprintEntries[entry.sourcePath] = {
10559
+ sourceVersion: entry.sourceVersion,
10560
+ exact: fingerprintResult.exact,
10561
+ chars: fingerprintResult.chars,
10562
+ modified: fingerprintResult.modified,
10563
+ revision: fingerprintResult.revision
10564
+ };
10565
+ cloudFingerprintsDirty = true;
10566
+ } catch (error) {
10567
+ diagnostics.push({
10568
+ code: "cloud-fingerprint-failed",
10569
+ severity: "warning",
10570
+ message: `The cloud or network file was indexed but its duplicate fingerprint could not be read, so it will not take part in exact-duplicate exclusion: ${error instanceof Error ? error.message : String(error)}`,
10571
+ source: entry.virtualPath
10572
+ });
10573
+ }
10574
+ }
10575
+ }
10576
+ if (cloudDecision.kind === "download" && gate.evictAfterParse) {
10577
+ const released = await gate.release(entry.sourcePath);
10578
+ if (!released.ok) {
10579
+ diagnostics.push({
10580
+ code: "cloud-evict-failed",
10581
+ severity: "warning",
10582
+ message: `The file was indexed but its local copy could not be released: ${released.message}`,
10583
+ source: entry.virtualPath
10584
+ });
10585
+ }
10563
10586
  }
10564
10587
  }
10565
10588
  }
10589
+ if (finalBatch && deferDeletions)
10590
+ await applyDeleteDecisions();
10591
+ if (finalBatch || written + deleted !== changedBefore) {
10592
+ await runBatch(current.slice(batchStart, batchEnd).map((item) => item.virtualPath), finalBatch);
10593
+ }
10594
+ if (batchStopped)
10595
+ break;
10566
10596
  }
10567
10597
  await session.finish();
10568
10598
  if (fingerprintCloud) {
@@ -10854,7 +10884,7 @@ async function collectFiles(state, directory, depth) {
10854
10884
  warnDirectoryUnreadable(state, directory, code);
10855
10885
  }
10856
10886
  }
10857
- var DEFAULT_MAX_SOURCE_BYTES, DEFAULT_MAX_DELETE_RATIO = 0.3, MASS_DELETE_FLOOR = 100, DEFAULT_MAX_SCANNED_ENTRIES = 2000000, DEFAULT_MAX_WALK_DEPTH = 64, PRODUCT_ARTIFACT_FILE_NAMES, SKIP_DIR_NAMES, PARSER_FAILURE_MESSAGE = "The registered parser failed on this file; it was skipped during indexing.", PARSER_CRASH_MESSAGE = "The registered parser crashed unexpectedly on this file; it was skipped during indexing.", DEFAULT_TRANSIENT_RETRIES = 3, DEFAULT_TRANSIENT_BACKOFF_MS = 50, TRANSIENT_ERROR_CODES, WORKSPACE_ERROR_CODES, MirrorWorkspaceError, STANDALONE_OCR_IMAGE_EXTENSIONS, SEARCH_ROOT_UNAVAILABLE_MESSAGES;
10887
+ var DEFAULT_MAX_SOURCE_BYTES, DEFAULT_MAX_DELETE_RATIO = 0.3, MASS_DELETE_FLOOR = 100, DEFAULT_MAX_SCANNED_ENTRIES = 2000000, DEFAULT_MAX_WALK_DEPTH = 64, DEFAULT_MIRROR_BATCH_SIZE = 25, PRODUCT_ARTIFACT_FILE_NAMES, SKIP_DIR_NAMES, PARSER_FAILURE_MESSAGE = "The registered parser failed on this file; it was skipped during indexing.", PARSER_CRASH_MESSAGE = "The registered parser crashed unexpectedly on this file; it was skipped during indexing.", DEFAULT_TRANSIENT_RETRIES = 3, DEFAULT_TRANSIENT_BACKOFF_MS = 50, TRANSIENT_ERROR_CODES, WORKSPACE_ERROR_CODES, MirrorWorkspaceError, STANDALONE_OCR_IMAGE_EXTENSIONS, SEARCH_ROOT_UNAVAILABLE_MESSAGES;
10858
10888
  var init_sync = __esm(() => {
10859
10889
  init_cli2();
10860
10890
  init_cloud_placeholder();
@@ -38693,11 +38723,52 @@ ${this.discoveryHint((tools) => `When exploring local files and folders, activel
38693
38723
  };
38694
38724
  try {
38695
38725
  writeRefreshProgress(this.workspaceProjectRoot, progress);
38726
+ const batchedMinSync = needsParsed && wants("minsync");
38727
+ const minSyncBatches = [];
38728
+ const minSyncBatchFailures = [];
38729
+ let minSyncSyncedBase = 0;
38730
+ let minSyncTotalBase = 0;
38731
+ const minSyncBatch = async (batch) => {
38732
+ signal?.throwIfAborted();
38733
+ progress = updateRefreshProgress(progress, { phase: "minsync" });
38734
+ writeRefreshProgress(this.workspaceProjectRoot, progress);
38735
+ let batchTotal = minSyncTotalBase;
38736
+ const result = await this.syncMinSync(force && batch.index === 1, signal, (minsyncProgress) => {
38737
+ batchTotal = minSyncTotalBase + minsyncProgress.total;
38738
+ progress = updateRefreshProgress(progress, {
38739
+ minsync: { synced: minSyncSyncedBase + minsyncProgress.processed, total: batchTotal }
38740
+ });
38741
+ writeRefreshProgress(this.workspaceProjectRoot, progress);
38742
+ emitProgress({
38743
+ phase: "minsync",
38744
+ processed: minSyncSyncedBase + minsyncProgress.processed,
38745
+ total: batchTotal
38746
+ });
38747
+ });
38748
+ minSyncBatches.push(result);
38749
+ if (result.ok)
38750
+ minSyncBatchFailures.length = 0;
38751
+ else if (result.reason !== "lock-held") {
38752
+ for (const virtualPath of batch.virtualPaths) {
38753
+ minSyncBatchFailures.push({
38754
+ code: "minsync-sync-failed",
38755
+ severity: "warning",
38756
+ message: `This file stayed at its previous index entry: its batch could not be indexed and the next sync retries it. ${sanitizeDiagnosticMessage(result.diagnostic?.message ?? result.reason ?? "MinSync sync failed.")}`,
38757
+ source: virtualPath
38758
+ });
38759
+ }
38760
+ }
38761
+ minSyncSyncedBase = batchTotal;
38762
+ minSyncTotalBase = batchTotal;
38763
+ return !result.ok && result.reason === "lock-held" ? { stop: true } : undefined;
38764
+ };
38696
38765
  const summary = needsParsed ? await this.syncParsedMirrors(force, {
38697
38766
  ...opts?.retrySkipped === true ? { retrySkipped: true } : {},
38698
38767
  ...signal === undefined ? {} : { signal },
38768
+ ...batchedMinSync ? { onBatch: minSyncBatch } : {},
38699
38769
  onProgress: (parsedProgress) => {
38700
38770
  progress = updateRefreshProgress(progress, {
38771
+ phase: "parsed",
38701
38772
  sourceFiles: { total: parsedProgress.total },
38702
38773
  parsedCounts: {
38703
38774
  scanned: parsedProgress.processed,
@@ -38719,7 +38790,7 @@ ${this.discoveryHint((tools) => `When exploring local files and folders, activel
38719
38790
  }) : await this.scanMirrorStaleness();
38720
38791
  signal?.throwIfAborted();
38721
38792
  progress = updateRefreshProgress(progress, {
38722
- phase: wants("minsync") ? "minsync" : wants("datasources") ? "datasources" : wants("jikji") ? "jikji" : "finalizing",
38793
+ phase: wants("datasources") ? "datasources" : wants("jikji") ? "jikji" : "finalizing",
38723
38794
  sourceFiles: { total: summary.scanned },
38724
38795
  parsedCounts: {
38725
38796
  scanned: summary.scanned,
@@ -38729,24 +38800,12 @@ ${this.discoveryHint((tools) => `When exploring local files and folders, activel
38729
38800
  }
38730
38801
  });
38731
38802
  writeRefreshProgress(this.workspaceProjectRoot, progress);
38732
- const minsync = wants("minsync") ? await this.syncMinSync(force, signal, (minsyncProgress) => {
38733
- progress = updateRefreshProgress(progress, {
38734
- minsync: { synced: minsyncProgress.processed, total: minsyncProgress.total }
38735
- });
38736
- writeRefreshProgress(this.workspaceProjectRoot, progress);
38737
- emitProgress({
38738
- phase: "minsync",
38739
- processed: minsyncProgress.processed,
38740
- total: minsyncProgress.total
38741
- });
38742
- }) : undefined;
38743
- signal?.throwIfAborted();
38803
+ const minsync = batchedMinSync ? aggregateMinSyncBatches(minSyncBatches) : undefined;
38744
38804
  if (minsync !== undefined) {
38745
38805
  progress = updateRefreshProgress(progress, {
38746
- phase: wants("datasources") ? "datasources" : wants("jikji") ? "jikji" : "finalizing",
38747
38806
  minsync: {
38748
- ...progress.minsync,
38749
38807
  synced: minsync.synced,
38808
+ total: Math.max(minSyncTotalBase, minsync.synced),
38750
38809
  ...minsync.filesFailed !== undefined && minsync.filesFailed.length > 0 ? { filesSkipped: minsync.filesFailed.length } : {}
38751
38810
  }
38752
38811
  });
@@ -38808,7 +38867,7 @@ ${this.discoveryHint((tools) => `When exploring local files and folders, activel
38808
38867
  finishedAt: new Date().toISOString()
38809
38868
  });
38810
38869
  writeRefreshProgress(this.workspaceProjectRoot, progress);
38811
- const minsyncDiagnostics = minSyncRefreshDiagnostics(minsync);
38870
+ const minsyncDiagnostics = [...minSyncRefreshDiagnostics(minsync), ...minSyncBatchFailures];
38812
38871
  const publicMinsync = minsync ? {
38813
38872
  ok: minsync.ok,
38814
38873
  synced: minsync.synced,
@@ -39613,6 +39672,12 @@ function fsearchRefreshDiagnostics(fsearch) {
39613
39672
  }
39614
39673
  ];
39615
39674
  }
39675
+ function aggregateMinSyncBatches(batches) {
39676
+ const last = batches.at(-1);
39677
+ if (last === undefined)
39678
+ return;
39679
+ return { ...last, synced: batches.reduce((total, batch) => total + batch.synced, 0) };
39680
+ }
39616
39681
  function minSyncRefreshDiagnostics(minsync) {
39617
39682
  if (!minsync)
39618
39683
  return [];
@@ -10,6 +10,12 @@ export declare const DEFAULT_MAX_DELETE_RATIO = 0.3;
10
10
  export declare const DEFAULT_MAX_SCANNED_ENTRIES = 2000000;
11
11
  /** Deepest directory level (search root = 0) the walk descends into. */
12
12
  export declare const DEFAULT_MAX_WALK_DEPTH = 64;
13
+ /**
14
+ * Sources parsed and committed per batch before {@link ParsedMirrorSyncOptions.onBatch}
15
+ * runs. Small enough that a corpus becomes searchable after its first slice,
16
+ * large enough to amortize one index sync's startup.
17
+ */
18
+ export declare const DEFAULT_MIRROR_BATCH_SIZE = 25;
13
19
  export interface ParsedMirrorSyncOptions {
14
20
  readonly root: string;
15
21
  readonly searchPaths: readonly string[];
@@ -36,6 +42,20 @@ export interface ParsedMirrorSyncOptions {
36
42
  readonly signal?: AbortSignal;
37
43
  /** Called before each file and once more with `processed === total` when the parse phase ends. */
38
44
  readonly onProgress?: (progress: ParsedMirrorProgress) => void;
45
+ /**
46
+ * Sources parsed and committed per batch before {@link ParsedMirrorSyncOptions.onBatch}
47
+ * runs. Default {@link DEFAULT_MIRROR_BATCH_SIZE}.
48
+ */
49
+ readonly batchSize?: number;
50
+ /**
51
+ * Runs after each batch of sources is parsed and committed to the mirror, and
52
+ * before the next batch is read, so a caller can make the batch's results
53
+ * searchable (stage them and sync the index). A thrown error aborts the
54
+ * refresh; returning `stop` ends the loop after this batch. Deletions of
55
+ * genuinely-removed sources wait for the final batch, so a mid-refresh call
56
+ * never drops a not-yet-reached document from the index.
57
+ */
58
+ readonly onBatch?: (batch: ParsedMirrorBatch) => Promise<ParsedMirrorBatchOutcome | undefined>;
39
59
  /**
40
60
  * Largest share of previously indexed documents one refresh may delete because their source
41
61
  * disappeared. A larger deletion (above 100 documents) is suppressed with `mass-delete-suppressed`
@@ -115,6 +135,32 @@ export interface ParsedMirrorSyncResult {
115
135
  readonly indexPath: string;
116
136
  readonly diagnostics: readonly ParsedMirrorDiagnostic[];
117
137
  }
138
+ /**
139
+ * One batch of the source listing, parsed and committed to the mirror before
140
+ * {@link ParsedMirrorSyncOptions.onBatch} runs. The last batch is the only one
141
+ * whose run reflects deletions of genuinely-removed sources.
142
+ */
143
+ export interface ParsedMirrorBatch {
144
+ /** 1-based index of this batch among the batches handed to `onBatch`. */
145
+ readonly index: number;
146
+ /**
147
+ * Batches the listing was divided into. A batch that wrote and deleted no
148
+ * mirror output is not handed to `onBatch`; the final batch always is.
149
+ */
150
+ readonly total: number;
151
+ /** Virtual paths the refresh attempted in this batch. */
152
+ readonly virtualPaths: readonly string[];
153
+ /** The last batch: genuine deletions have by now been applied to the mirror. */
154
+ readonly final: boolean;
155
+ }
156
+ /**
157
+ * What {@link ParsedMirrorSyncOptions.onBatch} asks the refresh to do next.
158
+ * A thrown error aborts the refresh; `stop` ends the loop after this batch, so a
159
+ * systemic index failure never reads sources it knows it cannot index.
160
+ */
161
+ export interface ParsedMirrorBatchOutcome {
162
+ readonly stop?: boolean;
163
+ }
118
164
  /** Default retries for a transient per-file I/O error. Mirrors the `refresh.transientRetries` config default. */
119
165
  export declare const DEFAULT_TRANSIENT_RETRIES = 3;
120
166
  /**