@xdarkicex/openclaw-memory-libravdb 1.4.67 → 1.4.69

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.
@@ -275,6 +275,17 @@ function resolvePredictiveCompactionTokenCount(args) {
275
275
  return (normalizeCurrentTokenCount(args.currentTokenCount) ??
276
276
  approximateMessagesTokens(args.messages) + approximateTokenCount(args.prompt ?? ""));
277
277
  }
278
+ function resolveAfterTurnPredictiveCompactionTokenCount(args) {
279
+ const currentTokenCount = normalizeCurrentTokenCount(args.currentTokenCount);
280
+ const forwardedMessageTokens = normalizeCurrentTokenCount(approximateMessagesTokens(args.messages));
281
+ if (currentTokenCount == null) {
282
+ return forwardedMessageTokens;
283
+ }
284
+ if (forwardedMessageTokens == null) {
285
+ return currentTokenCount;
286
+ }
287
+ return Math.max(currentTokenCount, forwardedMessageTokens);
288
+ }
278
289
  export function normalizeKernelMessage(message) {
279
290
  return {
280
291
  role: message.role,
@@ -564,11 +575,15 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
564
575
  }
565
576
  async function performAfterTurnPredictiveCompaction(args) {
566
577
  const dynamicCompactThreshold = getDynamicCompactThreshold(args.tokenBudget);
567
- const predictiveTargetSize = resolvePredictiveCompactionTarget({
578
+ const currentContextTokens = resolveAfterTurnPredictiveCompactionTokenCount({
568
579
  currentTokenCount: args.currentTokenCount,
580
+ messages: args.messages,
581
+ });
582
+ const predictiveTargetSize = resolvePredictiveCompactionTarget({
583
+ currentTokenCount: currentContextTokens,
569
584
  threshold: dynamicCompactThreshold,
570
585
  });
571
- if (args.currentTokenCount == null ||
586
+ if (currentContextTokens == null ||
572
587
  dynamicCompactThreshold == null ||
573
588
  predictiveTargetSize == null) {
574
589
  return;
@@ -577,7 +592,7 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
577
592
  logger,
578
593
  phase: "afterTurn",
579
594
  sessionId: args.sessionId,
580
- currentTokenCount: args.currentTokenCount,
595
+ currentTokenCount: currentContextTokens,
581
596
  threshold: dynamicCompactThreshold,
582
597
  targetSize: predictiveTargetSize,
583
598
  tokenBudget: args.tokenBudget,
@@ -587,13 +602,13 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
587
602
  targetSize: predictiveTargetSize,
588
603
  tokenBudget: args.tokenBudget,
589
604
  force: true,
590
- currentTokenCount: args.currentTokenCount,
605
+ currentTokenCount: currentContextTokens,
591
606
  });
592
607
  logPredictiveCompactionOutcome({
593
608
  logger,
594
609
  phase: "afterTurn",
595
610
  sessionId: args.sessionId,
596
- currentTokenCount: args.currentTokenCount,
611
+ currentTokenCount: currentContextTokens,
597
612
  threshold: dynamicCompactThreshold,
598
613
  targetSize: predictiveTargetSize,
599
614
  tokenBudget: args.tokenBudget,
@@ -803,6 +818,7 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
803
818
  });
804
819
  await performAfterTurnPredictiveCompaction({
805
820
  sessionId,
821
+ messages,
806
822
  tokenBudget: args.tokenBudget,
807
823
  currentTokenCount,
808
824
  });
@@ -818,6 +834,7 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
818
834
  });
819
835
  await performAfterTurnPredictiveCompaction({
820
836
  sessionId,
837
+ messages,
821
838
  tokenBudget: args.tokenBudget,
822
839
  currentTokenCount,
823
840
  });
package/dist/index.js CHANGED
@@ -9467,7 +9467,7 @@ var require_service_config = __commonJS({
9467
9467
  exports2.validateRetryThrottling = validateRetryThrottling;
9468
9468
  exports2.validateServiceConfig = validateServiceConfig;
9469
9469
  exports2.extractAndSelectServiceConfig = extractAndSelectServiceConfig;
9470
- var os3 = __require("os");
9470
+ var os4 = __require("os");
9471
9471
  var constants_1 = require_constants();
9472
9472
  var DURATION_REGEX = /^\d+(\.\d{1,9})?s$/;
9473
9473
  var CLIENT_LANGUAGE_STRING = "node";
@@ -9766,7 +9766,7 @@ var require_service_config = __commonJS({
9766
9766
  if (Array.isArray(validatedConfig.clientHostname)) {
9767
9767
  let hostnameMatched = false;
9768
9768
  for (const hostname2 of validatedConfig.clientHostname) {
9769
- if (hostname2 === os3.hostname()) {
9769
+ if (hostname2 === os4.hostname()) {
9770
9770
  hostnameMatched = true;
9771
9771
  }
9772
9772
  }
@@ -24066,7 +24066,7 @@ var require_subchannel_call = __commonJS({
24066
24066
  Object.defineProperty(exports2, "__esModule", { value: true });
24067
24067
  exports2.Http2SubchannelCall = void 0;
24068
24068
  var http2 = __require("http2");
24069
- var os3 = __require("os");
24069
+ var os4 = __require("os");
24070
24070
  var constants_1 = require_constants();
24071
24071
  var metadata_1 = require_metadata();
24072
24072
  var stream_decoder_1 = require_stream_decoder();
@@ -24074,7 +24074,7 @@ var require_subchannel_call = __commonJS({
24074
24074
  var constants_2 = require_constants();
24075
24075
  var TRACER_NAME = "subchannel_call";
24076
24076
  function getSystemErrorName(errno) {
24077
- for (const [name, num] of Object.entries(os3.constants.errno)) {
24077
+ for (const [name, num] of Object.entries(os4.constants.errno)) {
24078
24078
  if (num === errno) {
24079
24079
  return name;
24080
24080
  }
@@ -33927,6 +33927,19 @@ function buildBudgetFallbackContext(messages, tokenBudget) {
33927
33927
  function resolvePredictiveCompactionTokenCount(args) {
33928
33928
  return normalizeCurrentTokenCount(args.currentTokenCount) ?? approximateMessagesTokens(args.messages) + approximateTokenCount(args.prompt ?? "");
33929
33929
  }
33930
+ function resolveAfterTurnPredictiveCompactionTokenCount(args) {
33931
+ const currentTokenCount = normalizeCurrentTokenCount(args.currentTokenCount);
33932
+ const forwardedMessageTokens = normalizeCurrentTokenCount(
33933
+ approximateMessagesTokens(args.messages)
33934
+ );
33935
+ if (currentTokenCount == null) {
33936
+ return forwardedMessageTokens;
33937
+ }
33938
+ if (forwardedMessageTokens == null) {
33939
+ return currentTokenCount;
33940
+ }
33941
+ return Math.max(currentTokenCount, forwardedMessageTokens);
33942
+ }
33930
33943
  function normalizeKernelMessage(message) {
33931
33944
  return {
33932
33945
  role: message.role,
@@ -34184,18 +34197,22 @@ function buildContextEngineFactory(runtime, cfg, logger = console) {
34184
34197
  }
34185
34198
  async function performAfterTurnPredictiveCompaction(args) {
34186
34199
  const dynamicCompactThreshold = getDynamicCompactThreshold(args.tokenBudget);
34187
- const predictiveTargetSize = resolvePredictiveCompactionTarget({
34200
+ const currentContextTokens = resolveAfterTurnPredictiveCompactionTokenCount({
34188
34201
  currentTokenCount: args.currentTokenCount,
34202
+ messages: args.messages
34203
+ });
34204
+ const predictiveTargetSize = resolvePredictiveCompactionTarget({
34205
+ currentTokenCount: currentContextTokens,
34189
34206
  threshold: dynamicCompactThreshold
34190
34207
  });
34191
- if (args.currentTokenCount == null || dynamicCompactThreshold == null || predictiveTargetSize == null) {
34208
+ if (currentContextTokens == null || dynamicCompactThreshold == null || predictiveTargetSize == null) {
34192
34209
  return;
34193
34210
  }
34194
34211
  logPredictiveCompactionAttempt({
34195
34212
  logger,
34196
34213
  phase: "afterTurn",
34197
34214
  sessionId: args.sessionId,
34198
- currentTokenCount: args.currentTokenCount,
34215
+ currentTokenCount: currentContextTokens,
34199
34216
  threshold: dynamicCompactThreshold,
34200
34217
  targetSize: predictiveTargetSize,
34201
34218
  tokenBudget: args.tokenBudget
@@ -34205,13 +34222,13 @@ function buildContextEngineFactory(runtime, cfg, logger = console) {
34205
34222
  targetSize: predictiveTargetSize,
34206
34223
  tokenBudget: args.tokenBudget,
34207
34224
  force: true,
34208
- currentTokenCount: args.currentTokenCount
34225
+ currentTokenCount: currentContextTokens
34209
34226
  });
34210
34227
  logPredictiveCompactionOutcome({
34211
34228
  logger,
34212
34229
  phase: "afterTurn",
34213
34230
  sessionId: args.sessionId,
34214
- currentTokenCount: args.currentTokenCount,
34231
+ currentTokenCount: currentContextTokens,
34215
34232
  threshold: dynamicCompactThreshold,
34216
34233
  targetSize: predictiveTargetSize,
34217
34234
  tokenBudget: args.tokenBudget,
@@ -34428,6 +34445,7 @@ function buildContextEngineFactory(runtime, cfg, logger = console) {
34428
34445
  });
34429
34446
  await performAfterTurnPredictiveCompaction({
34430
34447
  sessionId,
34448
+ messages,
34431
34449
  tokenBudget: args.tokenBudget,
34432
34450
  currentTokenCount
34433
34451
  });
@@ -34443,6 +34461,7 @@ function buildContextEngineFactory(runtime, cfg, logger = console) {
34443
34461
  });
34444
34462
  await performAfterTurnPredictiveCompaction({
34445
34463
  sessionId,
34464
+ messages,
34446
34465
  tokenBudget: args.tokenBudget,
34447
34466
  currentTokenCount
34448
34467
  });
@@ -34523,6 +34542,7 @@ function isRecord(value) {
34523
34542
  // src/markdown-ingest.ts
34524
34543
  import fs2 from "node:fs";
34525
34544
  import fsp2 from "node:fs/promises";
34545
+ import os2 from "node:os";
34526
34546
  import path2 from "node:path";
34527
34547
 
34528
34548
  // node_modules/.pnpm/@bufbuild+protobuf@1.7.2/node_modules/@bufbuild/protobuf/dist/proxy/index.js
@@ -38484,7 +38504,8 @@ function createMarkdownIngestionHandle(cfg, getRpc, logger = console, fsApi = cr
38484
38504
  roots: genericRoots,
38485
38505
  include: cfg.markdownIngestionInclude,
38486
38506
  exclude: cfg.markdownIngestionExclude,
38487
- debounceMs: cfg.markdownIngestionDebounceMs ?? DEFAULT_DEBOUNCE_MS2
38507
+ debounceMs: cfg.markdownIngestionDebounceMs ?? DEFAULT_DEBOUNCE_MS2,
38508
+ snapshotPath: resolveMarkdownSnapshotPath("generic", cfg.markdownIngestionSnapshotPath)
38488
38509
  },
38489
38510
  getRpc,
38490
38511
  logger,
@@ -38493,7 +38514,7 @@ function createMarkdownIngestionHandle(cfg, getRpc, logger = console, fsApi = cr
38493
38514
  );
38494
38515
  }
38495
38516
  const obsidianRoots = normalizeMarkdownRoots(cfg.markdownIngestionObsidianRoots);
38496
- if (cfg.markdownIngestionObsidianEnabled !== false && obsidianRoots.length > 0) {
38517
+ if (cfg.markdownIngestionObsidianEnabled === true && obsidianRoots.length > 0) {
38497
38518
  adapters.push(
38498
38519
  new DirectoryMarkdownSourceAdapter(
38499
38520
  "obsidian",
@@ -38501,7 +38522,8 @@ function createMarkdownIngestionHandle(cfg, getRpc, logger = console, fsApi = cr
38501
38522
  roots: obsidianRoots,
38502
38523
  include: cfg.markdownIngestionObsidianInclude,
38503
38524
  exclude: cfg.markdownIngestionObsidianExclude,
38504
- debounceMs: cfg.markdownIngestionObsidianDebounceMs ?? cfg.markdownIngestionDebounceMs ?? DEFAULT_DEBOUNCE_MS2
38525
+ debounceMs: cfg.markdownIngestionObsidianDebounceMs ?? cfg.markdownIngestionDebounceMs ?? DEFAULT_DEBOUNCE_MS2,
38526
+ snapshotPath: resolveMarkdownSnapshotPath("obsidian", cfg.markdownIngestionObsidianSnapshotPath)
38505
38527
  },
38506
38528
  getRpc,
38507
38529
  logger,
@@ -38557,6 +38579,7 @@ var DirectoryMarkdownSourceAdapter = class {
38557
38579
  fsApi;
38558
38580
  getRpc;
38559
38581
  logger;
38582
+ snapshotPath;
38560
38583
  states = /* @__PURE__ */ new Map();
38561
38584
  fileStates = /* @__PURE__ */ new Map();
38562
38585
  activeScans = /* @__PURE__ */ new Set();
@@ -38565,6 +38588,8 @@ var DirectoryMarkdownSourceAdapter = class {
38565
38588
  started = false;
38566
38589
  ingestQueue = null;
38567
38590
  stopping = false;
38591
+ snapshotLoaded = false;
38592
+ snapshotDirty = false;
38568
38593
  constructor(kind, config, getRpc, logger, fsApi) {
38569
38594
  this.kind = kind;
38570
38595
  this.roots = config.roots;
@@ -38574,6 +38599,7 @@ var DirectoryMarkdownSourceAdapter = class {
38574
38599
  this.fsApi = fsApi;
38575
38600
  this.getRpc = getRpc;
38576
38601
  this.logger = logger;
38602
+ this.snapshotPath = config.snapshotPath ?? resolveMarkdownSnapshotPath(kind);
38577
38603
  this.tokenizerId = DEFAULT_TOKENIZER_ID;
38578
38604
  this.coreDoc = true;
38579
38605
  }
@@ -38581,6 +38607,7 @@ var DirectoryMarkdownSourceAdapter = class {
38581
38607
  if (this.started) {
38582
38608
  return;
38583
38609
  }
38610
+ await this.loadSnapshot();
38584
38611
  this.started = true;
38585
38612
  this.stopping = false;
38586
38613
  await this.refresh();
@@ -38608,8 +38635,10 @@ var DirectoryMarkdownSourceAdapter = class {
38608
38635
  if (this.activeScans.size > 0) {
38609
38636
  await Promise.allSettled([...this.activeScans]);
38610
38637
  }
38638
+ await this.saveSnapshotIfDirty();
38611
38639
  this.states.clear();
38612
38640
  this.fileStates.clear();
38641
+ this.snapshotLoaded = false;
38613
38642
  this.started = false;
38614
38643
  }
38615
38644
  getRootState(root) {
@@ -38625,7 +38654,7 @@ var DirectoryMarkdownSourceAdapter = class {
38625
38654
  dirty: false,
38626
38655
  timer: null
38627
38656
  },
38628
- knownFiles: /* @__PURE__ */ new Set(),
38657
+ knownFiles: this.snapshotFilesForRoot(resolved),
38629
38658
  directoryWatchers: /* @__PURE__ */ new Map()
38630
38659
  };
38631
38660
  this.states.set(resolved, created);
@@ -38642,12 +38671,16 @@ var DirectoryMarkdownSourceAdapter = class {
38642
38671
  }
38643
38672
  rootState.scanState.scanning = true;
38644
38673
  const scan = (async () => {
38674
+ const stats = createScanStats();
38675
+ const startedAt = Date.now();
38645
38676
  try {
38646
38677
  const currentFiles = /* @__PURE__ */ new Set();
38647
- await this.walkDirectory(rootState, rootState.root, currentFiles);
38678
+ await this.walkDirectory(rootState, rootState.root, currentFiles, stats);
38648
38679
  if (!this.stopping) {
38649
- await this.pruneDeletedFiles(rootState, currentFiles);
38680
+ await this.pruneDeletedFiles(rootState, currentFiles, stats);
38650
38681
  rootState.knownFiles = currentFiles;
38682
+ await this.saveSnapshotIfDirty();
38683
+ this.logScanStats(rootState.root, stats, Date.now() - startedAt);
38651
38684
  }
38652
38685
  } finally {
38653
38686
  rootState.scanState.scanning = false;
@@ -38684,7 +38717,12 @@ var DirectoryMarkdownSourceAdapter = class {
38684
38717
  });
38685
38718
  }, this.debounceMs);
38686
38719
  }
38687
- async walkDirectory(rootState, dir, currentFiles) {
38720
+ async walkDirectory(rootState, dir, currentFiles, stats) {
38721
+ if (this.shouldPruneDirectory(rootState.root, dir)) {
38722
+ stats.directoriesPruned++;
38723
+ return;
38724
+ }
38725
+ stats.directoriesScanned++;
38688
38726
  await this.ensureDirectoryWatcher(rootState, dir);
38689
38727
  let entries;
38690
38728
  try {
@@ -38702,25 +38740,42 @@ var DirectoryMarkdownSourceAdapter = class {
38702
38740
  }
38703
38741
  const child = path2.join(dir, entry.name);
38704
38742
  if (entry.isDirectory()) {
38705
- await this.walkDirectory(rootState, child, currentFiles);
38743
+ await this.walkDirectory(rootState, child, currentFiles, stats);
38706
38744
  continue;
38707
38745
  }
38708
38746
  if (!entry.isFile() || !isMarkdownFile(entry.name)) {
38709
38747
  continue;
38710
38748
  }
38749
+ stats.markdownFilesSeen++;
38711
38750
  if (!this.shouldIncludeFile(rootState.root, child)) {
38751
+ stats.filesSkipped++;
38712
38752
  continue;
38713
38753
  }
38754
+ stats.filesIncluded++;
38714
38755
  currentFiles.add(child);
38715
38756
  try {
38716
- await this.syncMarkdownFile(rootState, child);
38757
+ const result = await this.syncMarkdownFile(rootState, child);
38758
+ recordSyncResult(stats, result);
38717
38759
  } catch (error) {
38760
+ stats.syncErrors++;
38718
38761
  if (!this.stopping) {
38719
38762
  this.logger.warn?.(`[markdown-ingest] sync failed for ${child}: ${formatError(error)}`);
38720
38763
  }
38721
38764
  }
38722
38765
  }
38723
38766
  }
38767
+ shouldPruneDirectory(root, dir) {
38768
+ const relative = toPosixPath(path2.relative(root, dir));
38769
+ if (!relative || relative === "." || relative.startsWith("..")) {
38770
+ return false;
38771
+ }
38772
+ for (const pattern of this.excludePatterns) {
38773
+ if (matchesExcludedDirectory(relative, pattern)) {
38774
+ return true;
38775
+ }
38776
+ }
38777
+ return false;
38778
+ }
38724
38779
  async ensureDirectoryWatcher(rootState, dir) {
38725
38780
  if (rootState.directoryWatchers.has(dir)) {
38726
38781
  return;
@@ -38761,7 +38816,7 @@ var DirectoryMarkdownSourceAdapter = class {
38761
38816
  }
38762
38817
  return true;
38763
38818
  }
38764
- async pruneDeletedFiles(rootState, currentFiles) {
38819
+ async pruneDeletedFiles(rootState, currentFiles, stats) {
38765
38820
  const removed = [];
38766
38821
  for (const previous of rootState.knownFiles) {
38767
38822
  if (!currentFiles.has(previous)) {
@@ -38774,6 +38829,8 @@ var DirectoryMarkdownSourceAdapter = class {
38774
38829
  for (const filePath of removed) {
38775
38830
  await this.deleteSourceDocument(filePath);
38776
38831
  this.fileStates.delete(filePath);
38832
+ this.snapshotDirty = true;
38833
+ stats.filesDeleted++;
38777
38834
  }
38778
38835
  }
38779
38836
  async syncMarkdownFile(rootState, filePath) {
@@ -38783,21 +38840,23 @@ var DirectoryMarkdownSourceAdapter = class {
38783
38840
  if (!stat) {
38784
38841
  await this.deleteSourceDocument(sourceDoc);
38785
38842
  this.fileStates.delete(sourceDoc);
38786
- return;
38843
+ this.snapshotDirty = true;
38844
+ return "deleted";
38787
38845
  }
38788
38846
  const cached = this.fileStates.get(sourceDoc);
38789
38847
  if (cached && cached.size === stat.size && cached.mtimeMs === stat.mtimeMs) {
38790
- return;
38848
+ return "unchanged";
38791
38849
  }
38792
38850
  const bytes = await this.safeReadFile(filePath);
38793
38851
  if (!bytes) {
38794
38852
  await this.deleteSourceDocument(sourceDoc);
38795
38853
  this.fileStates.delete(sourceDoc);
38796
- return;
38854
+ this.snapshotDirty = true;
38855
+ return "deleted";
38797
38856
  }
38798
38857
  const fileHash = hashBytes(bytes);
38799
38858
  if (cached && cached.fileHash === fileHash) {
38800
- this.fileStates.set(sourceDoc, {
38859
+ this.setFileState(sourceDoc, {
38801
38860
  root: rootState.root,
38802
38861
  sourceDoc,
38803
38862
  relativePath,
@@ -38805,16 +38864,17 @@ var DirectoryMarkdownSourceAdapter = class {
38805
38864
  size: stat.size,
38806
38865
  mtimeMs: stat.mtimeMs
38807
38866
  });
38808
- return;
38867
+ return "unchanged";
38809
38868
  }
38810
38869
  const text = textDecoder2.decode(bytes);
38811
38870
  if (this.kind === "obsidian" && this.includePatterns.length === 0 && !looksLikeObsidianNote(filePath, text)) {
38812
38871
  await this.deleteSourceDocument(sourceDoc);
38813
38872
  this.fileStates.delete(sourceDoc);
38814
- return;
38873
+ this.snapshotDirty = true;
38874
+ return "skipped";
38815
38875
  }
38816
38876
  await this.ingestMarkdownDocument(sourceDoc, text, rootState.root, relativePath, fileHash, stat.size, stat.mtimeMs);
38817
- this.fileStates.set(sourceDoc, {
38877
+ this.setFileState(sourceDoc, {
38818
38878
  root: rootState.root,
38819
38879
  sourceDoc,
38820
38880
  relativePath,
@@ -38822,6 +38882,11 @@ var DirectoryMarkdownSourceAdapter = class {
38822
38882
  size: stat.size,
38823
38883
  mtimeMs: stat.mtimeMs
38824
38884
  });
38885
+ return "ingested";
38886
+ }
38887
+ setFileState(sourceDoc, state) {
38888
+ this.fileStates.set(sourceDoc, state);
38889
+ this.snapshotDirty = true;
38825
38890
  }
38826
38891
  async ingestMarkdownDocument(sourceDoc, text, sourceRoot, sourcePath, fileHash, sourceSize, sourceMtimeMs) {
38827
38892
  const queue = await this.getIngestQueue();
@@ -38869,7 +38934,96 @@ var DirectoryMarkdownSourceAdapter = class {
38869
38934
  return null;
38870
38935
  }
38871
38936
  }
38937
+ snapshotFilesForRoot(root) {
38938
+ const files = /* @__PURE__ */ new Set();
38939
+ for (const state of this.fileStates.values()) {
38940
+ if (state.root === root) {
38941
+ files.add(state.sourceDoc);
38942
+ }
38943
+ }
38944
+ return files;
38945
+ }
38946
+ async loadSnapshot() {
38947
+ if (this.snapshotLoaded) {
38948
+ return;
38949
+ }
38950
+ this.snapshotLoaded = true;
38951
+ let raw;
38952
+ try {
38953
+ raw = await fsp2.readFile(this.snapshotPath, "utf8");
38954
+ } catch (error) {
38955
+ if (!formatError(error).includes("ENOENT")) {
38956
+ this.logger.warn?.(`[markdown-ingest] failed to read snapshot ${this.snapshotPath}: ${formatError(error)}`);
38957
+ }
38958
+ return;
38959
+ }
38960
+ try {
38961
+ const parsed = JSON.parse(raw);
38962
+ if (parsed.ingestVersion !== MARKDOWN_INGEST_VERSION || parsed.hashBackend !== HASH_BACKEND || !parsed.files) {
38963
+ return;
38964
+ }
38965
+ const configuredRoots = new Set(this.roots.map((root) => path2.resolve(root)));
38966
+ for (const [sourceDoc, state] of Object.entries(parsed.files)) {
38967
+ if (isValidSnapshotState(sourceDoc, state) && configuredRoots.has(path2.resolve(state.root))) {
38968
+ this.fileStates.set(sourceDoc, state);
38969
+ }
38970
+ }
38971
+ this.logger.info?.(`[markdown-ingest] loaded ${this.fileStates.size} ${this.kind} file snapshots from ${this.snapshotPath}`);
38972
+ } catch (error) {
38973
+ this.logger.warn?.(`[markdown-ingest] failed to parse snapshot ${this.snapshotPath}: ${formatError(error)}`);
38974
+ }
38975
+ }
38976
+ async saveSnapshotIfDirty() {
38977
+ if (!this.snapshotDirty) {
38978
+ return;
38979
+ }
38980
+ const payload = {
38981
+ version: 1,
38982
+ ingestVersion: MARKDOWN_INGEST_VERSION,
38983
+ hashBackend: HASH_BACKEND,
38984
+ files: Object.fromEntries([...this.fileStates.entries()].sort(([left], [right]) => left.localeCompare(right)))
38985
+ };
38986
+ try {
38987
+ await fsp2.mkdir(path2.dirname(this.snapshotPath), { recursive: true });
38988
+ const tmp = `${this.snapshotPath}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
38989
+ await fsp2.writeFile(tmp, `${JSON.stringify(payload, null, 2)}
38990
+ `);
38991
+ await fsp2.rename(tmp, this.snapshotPath);
38992
+ this.snapshotDirty = false;
38993
+ } catch (error) {
38994
+ this.logger.warn?.(`[markdown-ingest] failed to write snapshot ${this.snapshotPath}: ${formatError(error)}`);
38995
+ }
38996
+ }
38997
+ logScanStats(root, stats, durationMs) {
38998
+ this.logger.info?.(
38999
+ `[markdown-ingest] ${this.kind} scan complete root=${root} dirs=${stats.directoriesScanned} prunedDirs=${stats.directoriesPruned} markdown=${stats.markdownFilesSeen} included=${stats.filesIncluded} skipped=${stats.filesSkipped} unchanged=${stats.filesUnchanged} ingested=${stats.filesIngested} deleted=${stats.filesDeleted} errors=${stats.syncErrors} durationMs=${durationMs}`
39000
+ );
39001
+ }
38872
39002
  };
39003
+ function createScanStats() {
39004
+ return {
39005
+ directoriesScanned: 0,
39006
+ directoriesPruned: 0,
39007
+ markdownFilesSeen: 0,
39008
+ filesIncluded: 0,
39009
+ filesSkipped: 0,
39010
+ filesUnchanged: 0,
39011
+ filesIngested: 0,
39012
+ filesDeleted: 0,
39013
+ syncErrors: 0
39014
+ };
39015
+ }
39016
+ function recordSyncResult(stats, result) {
39017
+ if (result === "ingested") {
39018
+ stats.filesIngested++;
39019
+ } else if (result === "unchanged") {
39020
+ stats.filesUnchanged++;
39021
+ } else if (result === "deleted") {
39022
+ stats.filesDeleted++;
39023
+ } else {
39024
+ stats.filesSkipped++;
39025
+ }
39026
+ }
38873
39027
  function toPosixPath(value) {
38874
39028
  return value.split(path2.sep).join("/");
38875
39029
  }
@@ -38888,11 +39042,16 @@ function normalizeMarkdownRoots(roots) {
38888
39042
  }
38889
39043
  return [...resolved];
38890
39044
  }
38891
- function isMarkdownIngestionEnabled(cfg, roots) {
38892
- if (cfg.markdownIngestionEnabled === false) {
38893
- return false;
39045
+ function resolveMarkdownSnapshotPath(kind, configuredPath) {
39046
+ const trimmed = configuredPath?.trim();
39047
+ if (trimmed) {
39048
+ return path2.resolve(trimmed);
38894
39049
  }
38895
- return roots.length > 0;
39050
+ const stateDir = process.env.OPENCLAW_STATE_DIR?.trim() || path2.join(os2.homedir(), ".openclaw");
39051
+ return path2.join(stateDir, `libravdb-markdown-ingest-${kind}.json`);
39052
+ }
39053
+ function isMarkdownIngestionEnabled(cfg, roots) {
39054
+ return cfg.markdownIngestionEnabled === true && roots.length > 0;
38896
39055
  }
38897
39056
  function createRealFsApi2() {
38898
39057
  return {
@@ -38913,6 +39072,17 @@ function matchesGlob(value, pattern) {
38913
39072
  const escaped = pattern.split("*").map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*");
38914
39073
  return new RegExp(`^${escaped}$`).test(value);
38915
39074
  }
39075
+ function matchesExcludedDirectory(relativeDir, pattern) {
39076
+ const normalized = relativeDir.replace(/\/+$/, "");
39077
+ return matchesGlob(normalized, pattern) || matchesGlob(`${normalized}/`, pattern) || matchesGlob(`${normalized}/.probe`, pattern);
39078
+ }
39079
+ function isValidSnapshotState(sourceDoc, value) {
39080
+ if (!value || typeof value !== "object") {
39081
+ return false;
39082
+ }
39083
+ const state = value;
39084
+ return state.sourceDoc === sourceDoc && typeof state.root === "string" && typeof state.relativePath === "string" && typeof state.fileHash === "string" && typeof state.size === "number" && Number.isFinite(state.size) && typeof state.mtimeMs === "number" && Number.isFinite(state.mtimeMs);
39085
+ }
38916
39086
  function looksLikeObsidianNote(filePath, text) {
38917
39087
  const frontmatterStart = parseFrontmatterStart(text);
38918
39088
  if (frontmatterStart == null) {
@@ -39439,7 +39609,7 @@ var GrpcKernelClient = class {
39439
39609
  // src/sidecar.ts
39440
39610
  import fs3 from "node:fs";
39441
39611
  import net from "node:net";
39442
- import os2 from "node:os";
39612
+ import os3 from "node:os";
39443
39613
  import path4 from "node:path";
39444
39614
  var STARTUP_CONNECT_MAX_RETRIES = 5;
39445
39615
  var STARTUP_CONNECT_BASE_DELAY_MS = 100;
@@ -39710,7 +39880,7 @@ function resolveConfiguredEndpoint(cfg) {
39710
39880
  function daemonProvisioningHint() {
39711
39881
  return "If you installed the npm package, install and start libravdbd separately; the package does not provision the daemon binary, ONNX Runtime, or model assets.";
39712
39882
  }
39713
- function defaultEndpoint(platform = process.platform, homeDir = os2.homedir(), pathExists = fs3.existsSync) {
39883
+ function defaultEndpoint(platform = process.platform, homeDir = os3.homedir(), pathExists = fs3.existsSync) {
39714
39884
  const envEndpoint = normalizeConfiguredEndpoint(process.env.LIBRAVDB_RPC_ENDPOINT);
39715
39885
  if (envEndpoint) {
39716
39886
  return envEndpoint;
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import fsp from "node:fs/promises";
3
+ import os from "node:os";
3
4
  import path from "node:path";
4
5
  import { hashBytes } from "./markdown-hash.js";
5
6
  import { formatError } from "./format-error.js";
@@ -17,15 +18,17 @@ export function createMarkdownIngestionHandle(cfg, getRpc, logger = console, fsA
17
18
  include: cfg.markdownIngestionInclude,
18
19
  exclude: cfg.markdownIngestionExclude,
19
20
  debounceMs: cfg.markdownIngestionDebounceMs ?? DEFAULT_DEBOUNCE_MS,
21
+ snapshotPath: resolveMarkdownSnapshotPath("generic", cfg.markdownIngestionSnapshotPath),
20
22
  }, getRpc, logger, fsApi));
21
23
  }
22
24
  const obsidianRoots = normalizeMarkdownRoots(cfg.markdownIngestionObsidianRoots);
23
- if (cfg.markdownIngestionObsidianEnabled !== false && obsidianRoots.length > 0) {
25
+ if (cfg.markdownIngestionObsidianEnabled === true && obsidianRoots.length > 0) {
24
26
  adapters.push(new DirectoryMarkdownSourceAdapter("obsidian", {
25
27
  roots: obsidianRoots,
26
28
  include: cfg.markdownIngestionObsidianInclude,
27
29
  exclude: cfg.markdownIngestionObsidianExclude,
28
30
  debounceMs: cfg.markdownIngestionObsidianDebounceMs ?? cfg.markdownIngestionDebounceMs ?? DEFAULT_DEBOUNCE_MS,
31
+ snapshotPath: resolveMarkdownSnapshotPath("obsidian", cfg.markdownIngestionObsidianSnapshotPath),
29
32
  }, getRpc, logger, fsApi));
30
33
  }
31
34
  if (adapters.length === 0) {
@@ -73,6 +76,7 @@ class DirectoryMarkdownSourceAdapter {
73
76
  fsApi;
74
77
  getRpc;
75
78
  logger;
79
+ snapshotPath;
76
80
  states = new Map();
77
81
  fileStates = new Map();
78
82
  activeScans = new Set();
@@ -81,6 +85,8 @@ class DirectoryMarkdownSourceAdapter {
81
85
  started = false;
82
86
  ingestQueue = null;
83
87
  stopping = false;
88
+ snapshotLoaded = false;
89
+ snapshotDirty = false;
84
90
  constructor(kind, config, getRpc, logger, fsApi) {
85
91
  this.kind = kind;
86
92
  this.roots = config.roots;
@@ -90,6 +96,7 @@ class DirectoryMarkdownSourceAdapter {
90
96
  this.fsApi = fsApi;
91
97
  this.getRpc = getRpc;
92
98
  this.logger = logger;
99
+ this.snapshotPath = config.snapshotPath ?? resolveMarkdownSnapshotPath(kind);
93
100
  this.tokenizerId = DEFAULT_TOKENIZER_ID;
94
101
  this.coreDoc = true;
95
102
  }
@@ -97,6 +104,7 @@ class DirectoryMarkdownSourceAdapter {
97
104
  if (this.started) {
98
105
  return;
99
106
  }
107
+ await this.loadSnapshot();
100
108
  this.started = true;
101
109
  this.stopping = false;
102
110
  await this.refresh();
@@ -124,8 +132,10 @@ class DirectoryMarkdownSourceAdapter {
124
132
  if (this.activeScans.size > 0) {
125
133
  await Promise.allSettled([...this.activeScans]);
126
134
  }
135
+ await this.saveSnapshotIfDirty();
127
136
  this.states.clear();
128
137
  this.fileStates.clear();
138
+ this.snapshotLoaded = false;
129
139
  this.started = false;
130
140
  }
131
141
  getRootState(root) {
@@ -141,7 +151,7 @@ class DirectoryMarkdownSourceAdapter {
141
151
  dirty: false,
142
152
  timer: null,
143
153
  },
144
- knownFiles: new Set(),
154
+ knownFiles: this.snapshotFilesForRoot(resolved),
145
155
  directoryWatchers: new Map(),
146
156
  };
147
157
  this.states.set(resolved, created);
@@ -158,12 +168,16 @@ class DirectoryMarkdownSourceAdapter {
158
168
  }
159
169
  rootState.scanState.scanning = true;
160
170
  const scan = (async () => {
171
+ const stats = createScanStats();
172
+ const startedAt = Date.now();
161
173
  try {
162
174
  const currentFiles = new Set();
163
- await this.walkDirectory(rootState, rootState.root, currentFiles);
175
+ await this.walkDirectory(rootState, rootState.root, currentFiles, stats);
164
176
  if (!this.stopping) {
165
- await this.pruneDeletedFiles(rootState, currentFiles);
177
+ await this.pruneDeletedFiles(rootState, currentFiles, stats);
166
178
  rootState.knownFiles = currentFiles;
179
+ await this.saveSnapshotIfDirty();
180
+ this.logScanStats(rootState.root, stats, Date.now() - startedAt);
167
181
  }
168
182
  }
169
183
  finally {
@@ -202,7 +216,12 @@ class DirectoryMarkdownSourceAdapter {
202
216
  });
203
217
  }, this.debounceMs);
204
218
  }
205
- async walkDirectory(rootState, dir, currentFiles) {
219
+ async walkDirectory(rootState, dir, currentFiles, stats) {
220
+ if (this.shouldPruneDirectory(rootState.root, dir)) {
221
+ stats.directoriesPruned++;
222
+ return;
223
+ }
224
+ stats.directoriesScanned++;
206
225
  await this.ensureDirectoryWatcher(rootState, dir);
207
226
  let entries;
208
227
  try {
@@ -221,26 +240,43 @@ class DirectoryMarkdownSourceAdapter {
221
240
  }
222
241
  const child = path.join(dir, entry.name);
223
242
  if (entry.isDirectory()) {
224
- await this.walkDirectory(rootState, child, currentFiles);
243
+ await this.walkDirectory(rootState, child, currentFiles, stats);
225
244
  continue;
226
245
  }
227
246
  if (!entry.isFile() || !isMarkdownFile(entry.name)) {
228
247
  continue;
229
248
  }
249
+ stats.markdownFilesSeen++;
230
250
  if (!this.shouldIncludeFile(rootState.root, child)) {
251
+ stats.filesSkipped++;
231
252
  continue;
232
253
  }
254
+ stats.filesIncluded++;
233
255
  currentFiles.add(child);
234
256
  try {
235
- await this.syncMarkdownFile(rootState, child);
257
+ const result = await this.syncMarkdownFile(rootState, child);
258
+ recordSyncResult(stats, result);
236
259
  }
237
260
  catch (error) {
261
+ stats.syncErrors++;
238
262
  if (!this.stopping) {
239
263
  this.logger.warn?.(`[markdown-ingest] sync failed for ${child}: ${formatError(error)}`);
240
264
  }
241
265
  }
242
266
  }
243
267
  }
268
+ shouldPruneDirectory(root, dir) {
269
+ const relative = toPosixPath(path.relative(root, dir));
270
+ if (!relative || relative === "." || relative.startsWith("..")) {
271
+ return false;
272
+ }
273
+ for (const pattern of this.excludePatterns) {
274
+ if (matchesExcludedDirectory(relative, pattern)) {
275
+ return true;
276
+ }
277
+ }
278
+ return false;
279
+ }
244
280
  async ensureDirectoryWatcher(rootState, dir) {
245
281
  if (rootState.directoryWatchers.has(dir)) {
246
282
  return;
@@ -282,7 +318,7 @@ class DirectoryMarkdownSourceAdapter {
282
318
  }
283
319
  return true;
284
320
  }
285
- async pruneDeletedFiles(rootState, currentFiles) {
321
+ async pruneDeletedFiles(rootState, currentFiles, stats) {
286
322
  const removed = [];
287
323
  for (const previous of rootState.knownFiles) {
288
324
  if (!currentFiles.has(previous)) {
@@ -295,6 +331,8 @@ class DirectoryMarkdownSourceAdapter {
295
331
  for (const filePath of removed) {
296
332
  await this.deleteSourceDocument(filePath);
297
333
  this.fileStates.delete(filePath);
334
+ this.snapshotDirty = true;
335
+ stats.filesDeleted++;
298
336
  }
299
337
  }
300
338
  async syncMarkdownFile(rootState, filePath) {
@@ -304,21 +342,23 @@ class DirectoryMarkdownSourceAdapter {
304
342
  if (!stat) {
305
343
  await this.deleteSourceDocument(sourceDoc);
306
344
  this.fileStates.delete(sourceDoc);
307
- return;
345
+ this.snapshotDirty = true;
346
+ return "deleted";
308
347
  }
309
348
  const cached = this.fileStates.get(sourceDoc);
310
349
  if (cached && cached.size === stat.size && cached.mtimeMs === stat.mtimeMs) {
311
- return;
350
+ return "unchanged";
312
351
  }
313
352
  const bytes = await this.safeReadFile(filePath);
314
353
  if (!bytes) {
315
354
  await this.deleteSourceDocument(sourceDoc);
316
355
  this.fileStates.delete(sourceDoc);
317
- return;
356
+ this.snapshotDirty = true;
357
+ return "deleted";
318
358
  }
319
359
  const fileHash = hashBytes(bytes);
320
360
  if (cached && cached.fileHash === fileHash) {
321
- this.fileStates.set(sourceDoc, {
361
+ this.setFileState(sourceDoc, {
322
362
  root: rootState.root,
323
363
  sourceDoc,
324
364
  relativePath,
@@ -326,16 +366,17 @@ class DirectoryMarkdownSourceAdapter {
326
366
  size: stat.size,
327
367
  mtimeMs: stat.mtimeMs,
328
368
  });
329
- return;
369
+ return "unchanged";
330
370
  }
331
371
  const text = textDecoder.decode(bytes);
332
372
  if (this.kind === "obsidian" && this.includePatterns.length === 0 && !looksLikeObsidianNote(filePath, text)) {
333
373
  await this.deleteSourceDocument(sourceDoc);
334
374
  this.fileStates.delete(sourceDoc);
335
- return;
375
+ this.snapshotDirty = true;
376
+ return "skipped";
336
377
  }
337
378
  await this.ingestMarkdownDocument(sourceDoc, text, rootState.root, relativePath, fileHash, stat.size, stat.mtimeMs);
338
- this.fileStates.set(sourceDoc, {
379
+ this.setFileState(sourceDoc, {
339
380
  root: rootState.root,
340
381
  sourceDoc,
341
382
  relativePath,
@@ -343,6 +384,11 @@ class DirectoryMarkdownSourceAdapter {
343
384
  size: stat.size,
344
385
  mtimeMs: stat.mtimeMs,
345
386
  });
387
+ return "ingested";
388
+ }
389
+ setFileState(sourceDoc, state) {
390
+ this.fileStates.set(sourceDoc, state);
391
+ this.snapshotDirty = true;
346
392
  }
347
393
  async ingestMarkdownDocument(sourceDoc, text, sourceRoot, sourcePath, fileHash, sourceSize, sourceMtimeMs) {
348
394
  const queue = await this.getIngestQueue();
@@ -388,6 +434,98 @@ class DirectoryMarkdownSourceAdapter {
388
434
  return null;
389
435
  }
390
436
  }
437
+ snapshotFilesForRoot(root) {
438
+ const files = new Set();
439
+ for (const state of this.fileStates.values()) {
440
+ if (state.root === root) {
441
+ files.add(state.sourceDoc);
442
+ }
443
+ }
444
+ return files;
445
+ }
446
+ async loadSnapshot() {
447
+ if (this.snapshotLoaded) {
448
+ return;
449
+ }
450
+ this.snapshotLoaded = true;
451
+ let raw;
452
+ try {
453
+ raw = await fsp.readFile(this.snapshotPath, "utf8");
454
+ }
455
+ catch (error) {
456
+ if (!formatError(error).includes("ENOENT")) {
457
+ this.logger.warn?.(`[markdown-ingest] failed to read snapshot ${this.snapshotPath}: ${formatError(error)}`);
458
+ }
459
+ return;
460
+ }
461
+ try {
462
+ const parsed = JSON.parse(raw);
463
+ if (parsed.ingestVersion !== MARKDOWN_INGEST_VERSION || parsed.hashBackend !== HASH_BACKEND || !parsed.files) {
464
+ return;
465
+ }
466
+ const configuredRoots = new Set(this.roots.map((root) => path.resolve(root)));
467
+ for (const [sourceDoc, state] of Object.entries(parsed.files)) {
468
+ if (isValidSnapshotState(sourceDoc, state) && configuredRoots.has(path.resolve(state.root))) {
469
+ this.fileStates.set(sourceDoc, state);
470
+ }
471
+ }
472
+ this.logger.info?.(`[markdown-ingest] loaded ${this.fileStates.size} ${this.kind} file snapshots from ${this.snapshotPath}`);
473
+ }
474
+ catch (error) {
475
+ this.logger.warn?.(`[markdown-ingest] failed to parse snapshot ${this.snapshotPath}: ${formatError(error)}`);
476
+ }
477
+ }
478
+ async saveSnapshotIfDirty() {
479
+ if (!this.snapshotDirty) {
480
+ return;
481
+ }
482
+ const payload = {
483
+ version: 1,
484
+ ingestVersion: MARKDOWN_INGEST_VERSION,
485
+ hashBackend: HASH_BACKEND,
486
+ files: Object.fromEntries([...this.fileStates.entries()].sort(([left], [right]) => left.localeCompare(right))),
487
+ };
488
+ try {
489
+ await fsp.mkdir(path.dirname(this.snapshotPath), { recursive: true });
490
+ const tmp = `${this.snapshotPath}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
491
+ await fsp.writeFile(tmp, `${JSON.stringify(payload, null, 2)}\n`);
492
+ await fsp.rename(tmp, this.snapshotPath);
493
+ this.snapshotDirty = false;
494
+ }
495
+ catch (error) {
496
+ this.logger.warn?.(`[markdown-ingest] failed to write snapshot ${this.snapshotPath}: ${formatError(error)}`);
497
+ }
498
+ }
499
+ logScanStats(root, stats, durationMs) {
500
+ this.logger.info?.(`[markdown-ingest] ${this.kind} scan complete root=${root} dirs=${stats.directoriesScanned} prunedDirs=${stats.directoriesPruned} markdown=${stats.markdownFilesSeen} included=${stats.filesIncluded} skipped=${stats.filesSkipped} unchanged=${stats.filesUnchanged} ingested=${stats.filesIngested} deleted=${stats.filesDeleted} errors=${stats.syncErrors} durationMs=${durationMs}`);
501
+ }
502
+ }
503
+ function createScanStats() {
504
+ return {
505
+ directoriesScanned: 0,
506
+ directoriesPruned: 0,
507
+ markdownFilesSeen: 0,
508
+ filesIncluded: 0,
509
+ filesSkipped: 0,
510
+ filesUnchanged: 0,
511
+ filesIngested: 0,
512
+ filesDeleted: 0,
513
+ syncErrors: 0,
514
+ };
515
+ }
516
+ function recordSyncResult(stats, result) {
517
+ if (result === "ingested") {
518
+ stats.filesIngested++;
519
+ }
520
+ else if (result === "unchanged") {
521
+ stats.filesUnchanged++;
522
+ }
523
+ else if (result === "deleted") {
524
+ stats.filesDeleted++;
525
+ }
526
+ else {
527
+ stats.filesSkipped++;
528
+ }
391
529
  }
392
530
  function toPosixPath(value) {
393
531
  return value.split(path.sep).join("/");
@@ -407,11 +545,16 @@ function normalizeMarkdownRoots(roots) {
407
545
  }
408
546
  return [...resolved];
409
547
  }
410
- function isMarkdownIngestionEnabled(cfg, roots) {
411
- if (cfg.markdownIngestionEnabled === false) {
412
- return false;
548
+ function resolveMarkdownSnapshotPath(kind, configuredPath) {
549
+ const trimmed = configuredPath?.trim();
550
+ if (trimmed) {
551
+ return path.resolve(trimmed);
413
552
  }
414
- return roots.length > 0;
553
+ const stateDir = process.env.OPENCLAW_STATE_DIR?.trim() || path.join(os.homedir(), ".openclaw");
554
+ return path.join(stateDir, `libravdb-markdown-ingest-${kind}.json`);
555
+ }
556
+ function isMarkdownIngestionEnabled(cfg, roots) {
557
+ return cfg.markdownIngestionEnabled === true && roots.length > 0;
415
558
  }
416
559
  function createRealFsApi() {
417
560
  return {
@@ -435,6 +578,24 @@ function matchesGlob(value, pattern) {
435
578
  .join(".*");
436
579
  return new RegExp(`^${escaped}$`).test(value);
437
580
  }
581
+ function matchesExcludedDirectory(relativeDir, pattern) {
582
+ const normalized = relativeDir.replace(/\/+$/, "");
583
+ return matchesGlob(normalized, pattern) || matchesGlob(`${normalized}/`, pattern) || matchesGlob(`${normalized}/.probe`, pattern);
584
+ }
585
+ function isValidSnapshotState(sourceDoc, value) {
586
+ if (!value || typeof value !== "object") {
587
+ return false;
588
+ }
589
+ const state = value;
590
+ return (state.sourceDoc === sourceDoc &&
591
+ typeof state.root === "string" &&
592
+ typeof state.relativePath === "string" &&
593
+ typeof state.fileHash === "string" &&
594
+ typeof state.size === "number" &&
595
+ Number.isFinite(state.size) &&
596
+ typeof state.mtimeMs === "number" &&
597
+ Number.isFinite(state.mtimeMs));
598
+ }
438
599
  function looksLikeObsidianNote(filePath, text) {
439
600
  const frontmatterStart = parseFrontmatterStart(text);
440
601
  if (frontmatterStart == null) {
package/dist/types.d.ts CHANGED
@@ -48,6 +48,8 @@ export interface PluginConfig {
48
48
  markdownIngestionInclude?: string[];
49
49
  markdownIngestionExclude?: string[];
50
50
  markdownIngestionDebounceMs?: number;
51
+ markdownIngestionSnapshotPath?: string;
52
+ markdownIngestionObsidianSnapshotPath?: string;
51
53
  dreamPromotionEnabled?: boolean;
52
54
  dreamPromotionDiaryPath?: string;
53
55
  dreamPromotionUserId?: string;
@@ -98,6 +98,10 @@ The plugin exposes `ingestionGateThreshold` for host-side gating decisions:
98
98
  | `markdownIngestionObsidianExclude` | string[] | — | Obsidian glob exclude patterns |
99
99
  | `markdownIngestionObsidianDebounceMs` | number | `150` | Obsidian debounce window |
100
100
 
101
+ Configured markdown roots are ignored unless the matching enable flag is set to
102
+ `true`. Set `markdownIngestionEnabled: true` for generic roots and
103
+ `markdownIngestionObsidianEnabled: true` for Obsidian vault roots.
104
+
101
105
  ## Dream promotion
102
106
 
103
107
  | Key | Type | Default | Notes |
@@ -2,7 +2,7 @@
2
2
  "id": "libravdb-memory",
3
3
  "name": "LibraVDB Memory",
4
4
  "description": "Persistent vector memory with three-tier hybrid scoring",
5
- "version": "1.4.67",
5
+ "version": "1.4.69",
6
6
  "kind": [
7
7
  "memory",
8
8
  "context-engine"
@@ -206,6 +206,12 @@
206
206
  "type": "number",
207
207
  "default": 150
208
208
  },
209
+ "markdownIngestionSnapshotPath": {
210
+ "type": "string"
211
+ },
212
+ "markdownIngestionObsidianSnapshotPath": {
213
+ "type": "string"
214
+ },
209
215
  "dreamPromotionEnabled": {
210
216
  "type": "boolean",
211
217
  "default": false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xdarkicex/openclaw-memory-libravdb",
3
- "version": "1.4.67",
3
+ "version": "1.4.69",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",