@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.
- package/dist/context-engine.js +22 -5
- package/dist/index.js +202 -32
- package/dist/markdown-ingest.js +180 -19
- package/dist/types.d.ts +2 -0
- package/docs/configuration.md +4 -0
- package/openclaw.plugin.json +7 -1
- package/package.json +1 -1
package/dist/context-engine.js
CHANGED
|
@@ -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
|
|
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 (
|
|
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:
|
|
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:
|
|
605
|
+
currentTokenCount: currentContextTokens,
|
|
591
606
|
});
|
|
592
607
|
logPredictiveCompactionOutcome({
|
|
593
608
|
logger,
|
|
594
609
|
phase: "afterTurn",
|
|
595
610
|
sessionId: args.sessionId,
|
|
596
|
-
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
|
|
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 ===
|
|
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
|
|
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(
|
|
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
|
|
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 (
|
|
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:
|
|
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:
|
|
34225
|
+
currentTokenCount: currentContextTokens
|
|
34209
34226
|
});
|
|
34210
34227
|
logPredictiveCompactionOutcome({
|
|
34211
34228
|
logger,
|
|
34212
34229
|
phase: "afterTurn",
|
|
34213
34230
|
sessionId: args.sessionId,
|
|
34214
|
-
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
38854
|
+
this.snapshotDirty = true;
|
|
38855
|
+
return "deleted";
|
|
38797
38856
|
}
|
|
38798
38857
|
const fileHash = hashBytes(bytes);
|
|
38799
38858
|
if (cached && cached.fileHash === fileHash) {
|
|
38800
|
-
this.
|
|
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
|
-
|
|
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.
|
|
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
|
|
38892
|
-
|
|
38893
|
-
|
|
39045
|
+
function resolveMarkdownSnapshotPath(kind, configuredPath) {
|
|
39046
|
+
const trimmed = configuredPath?.trim();
|
|
39047
|
+
if (trimmed) {
|
|
39048
|
+
return path2.resolve(trimmed);
|
|
38894
39049
|
}
|
|
38895
|
-
|
|
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
|
|
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 =
|
|
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;
|
package/dist/markdown-ingest.js
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
356
|
+
this.snapshotDirty = true;
|
|
357
|
+
return "deleted";
|
|
318
358
|
}
|
|
319
359
|
const fileHash = hashBytes(bytes);
|
|
320
360
|
if (cached && cached.fileHash === fileHash) {
|
|
321
|
-
this.
|
|
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
|
-
|
|
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.
|
|
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
|
|
411
|
-
|
|
412
|
-
|
|
548
|
+
function resolveMarkdownSnapshotPath(kind, configuredPath) {
|
|
549
|
+
const trimmed = configuredPath?.trim();
|
|
550
|
+
if (trimmed) {
|
|
551
|
+
return path.resolve(trimmed);
|
|
413
552
|
}
|
|
414
|
-
|
|
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;
|
package/docs/configuration.md
CHANGED
|
@@ -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 |
|
package/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
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
|