@xdarkicex/openclaw-memory-libravdb 1.4.5 → 1.4.7
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/HOOK.md +14 -0
- package/README.md +32 -2
- package/dist/cli.d.ts +39 -0
- package/dist/cli.js +208 -0
- package/dist/context-engine.d.ts +56 -0
- package/dist/context-engine.js +125 -0
- package/dist/dream-promotion.d.ts +47 -0
- package/dist/dream-promotion.js +363 -0
- package/dist/dream-routing.d.ts +6 -0
- package/dist/dream-routing.js +31 -0
- package/dist/durable-namespace.d.ts +6 -0
- package/dist/durable-namespace.js +24 -0
- package/dist/grpc-client.d.ts +23 -0
- package/dist/grpc-client.js +104 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +40 -0
- package/dist/lifecycle-hooks.d.ts +4 -0
- package/dist/lifecycle-hooks.js +64 -0
- package/dist/markdown-hash.d.ts +3 -0
- package/dist/markdown-hash.js +82 -0
- package/dist/markdown-ingest.d.ts +43 -0
- package/dist/markdown-ingest.js +464 -0
- package/dist/memory-provider.d.ts +4 -0
- package/dist/memory-provider.js +13 -0
- package/dist/memory-runtime.d.ts +118 -0
- package/dist/memory-runtime.js +217 -0
- package/dist/plugin-runtime.d.ts +28 -0
- package/dist/plugin-runtime.js +127 -0
- package/dist/proto/intelligence_kernel/v1/kernel.proto +378 -0
- package/dist/recall-cache.d.ts +2 -0
- package/dist/recall-cache.js +30 -0
- package/dist/rpc-protobuf-codecs.d.ts +70 -0
- package/dist/rpc-protobuf-codecs.js +77 -0
- package/dist/rpc.d.ts +14 -0
- package/dist/rpc.js +121 -0
- package/dist/sidecar.d.ts +34 -0
- package/dist/sidecar.js +535 -0
- package/dist/types.d.ts +163 -0
- package/dist/types.js +1 -0
- package/docs/contributing.md +14 -13
- package/docs/install.md +7 -9
- package/docs/installation.md +23 -16
- package/docs/uninstall.md +1 -1
- package/index.js +2 -0
- package/openclaw.plugin.json +2 -2
- package/package.json +39 -16
- package/packaging/README.md +0 -71
- package/packaging/homebrew/libravdbd.rb.tmpl +0 -224
- package/packaging/launchd/com.xdarkicex.libravdbd.plist +0 -32
- package/packaging/systemd/libravdbd.service +0 -12
- package/src/cli.ts +0 -299
- package/src/comparison-experiments.ts +0 -128
- package/src/context-engine.ts +0 -1451
- package/src/continuity.ts +0 -93
- package/src/dream-promotion.ts +0 -492
- package/src/dream-routing.ts +0 -40
- package/src/durable-namespace.ts +0 -34
- package/src/index.ts +0 -47
- package/src/lifecycle-hooks.ts +0 -96
- package/src/markdown-hash.ts +0 -104
- package/src/markdown-ingest.ts +0 -627
- package/src/memory-provider.ts +0 -25
- package/src/memory-runtime.ts +0 -283
- package/src/openclaw-plugin-sdk.d.ts +0 -59
- package/src/plugin-runtime.ts +0 -116
- package/src/recall-cache.ts +0 -34
- package/src/recall-utils.ts +0 -131
- package/src/rpc.ts +0 -84
- package/src/scoring.ts +0 -632
- package/src/sidecar.ts +0 -486
- package/src/temporal.ts +0 -1010
- package/src/tokens.ts +0 -52
- package/src/types.ts +0 -277
- package/tsconfig.json +0 -20
- package/tsconfig.tests.json +0 -12
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const WASM_BYTES = new Uint8Array([
|
|
2
|
+
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x07, 0x01, 0x60,
|
|
3
|
+
0x02, 0x7f, 0x7f, 0x01, 0x7e, 0x03, 0x02, 0x01, 0x00, 0x04, 0x05, 0x01,
|
|
4
|
+
0x70, 0x01, 0x01, 0x01, 0x05, 0x03, 0x01, 0x00, 0x10, 0x06, 0x09, 0x01,
|
|
5
|
+
0x7f, 0x01, 0x41, 0x80, 0x80, 0xc0, 0x00, 0x0b, 0x07, 0x19, 0x02, 0x06,
|
|
6
|
+
0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x02, 0x00, 0x0c, 0x68, 0x61, 0x73,
|
|
7
|
+
0x68, 0x5f, 0x66, 0x6e, 0x76, 0x31, 0x61, 0x36, 0x34, 0x00, 0x00, 0x0a,
|
|
8
|
+
0x41, 0x01, 0x3f, 0x01, 0x01, 0x7e, 0x42, 0x83, 0x87, 0xf4, 0x9c, 0x87,
|
|
9
|
+
0xf6, 0xc3, 0xb2, 0x14, 0x21, 0x02, 0x02, 0x40, 0x20, 0x01, 0x45, 0x0d,
|
|
10
|
+
0x00, 0x03, 0x40, 0x20, 0x02, 0x20, 0x00, 0x31, 0x00, 0x00, 0x85, 0x42,
|
|
11
|
+
0xb3, 0x83, 0x80, 0x80, 0x80, 0x20, 0x7e, 0x21, 0x02, 0x20, 0x00, 0x41,
|
|
12
|
+
0x01, 0x6a, 0x21, 0x00, 0x20, 0x01, 0x41, 0x7f, 0x6a, 0x22, 0x01, 0x0d,
|
|
13
|
+
0x00, 0x0b, 0x0b, 0x20, 0x02, 0x0b,
|
|
14
|
+
]);
|
|
15
|
+
const textEncoder = new TextEncoder();
|
|
16
|
+
const FNV_OFFSET_BASIS = 0xcbf29ce484222325n;
|
|
17
|
+
const FNV_PRIME = 0x100000001b3n;
|
|
18
|
+
class Fnv64Fallback {
|
|
19
|
+
kind = "js-fnv1a64";
|
|
20
|
+
hash(bytes) {
|
|
21
|
+
let hash = FNV_OFFSET_BASIS;
|
|
22
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
23
|
+
hash ^= BigInt(bytes[i] ?? 0);
|
|
24
|
+
hash = BigInt.asUintN(64, hash * FNV_PRIME);
|
|
25
|
+
}
|
|
26
|
+
return hash.toString(16).padStart(16, "0");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
class WasmFnv64 {
|
|
30
|
+
kind = "wasm-fnv1a64";
|
|
31
|
+
memory;
|
|
32
|
+
hashFn;
|
|
33
|
+
view;
|
|
34
|
+
constructor() {
|
|
35
|
+
const module = new WebAssembly.Module(WASM_BYTES);
|
|
36
|
+
const instance = new WebAssembly.Instance(module, {});
|
|
37
|
+
const exports = instance.exports;
|
|
38
|
+
this.memory = exports.memory;
|
|
39
|
+
this.hashFn = exports.hash_fnv1a64;
|
|
40
|
+
this.view = new Uint8Array(this.memory.buffer);
|
|
41
|
+
}
|
|
42
|
+
hash(bytes) {
|
|
43
|
+
this.ensureCapacity(bytes.length);
|
|
44
|
+
this.view.set(bytes, 0);
|
|
45
|
+
const raw = this.hashFn(0, bytes.length);
|
|
46
|
+
return BigInt.asUintN(64, raw).toString(16).padStart(16, "0");
|
|
47
|
+
}
|
|
48
|
+
ensureCapacity(size) {
|
|
49
|
+
if (this.view.byteLength >= size) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const pageSize = 65536;
|
|
53
|
+
const requiredPages = Math.ceil(size / pageSize);
|
|
54
|
+
const currentPages = this.memory.buffer.byteLength / pageSize;
|
|
55
|
+
const deltaPages = requiredPages - currentPages;
|
|
56
|
+
if (deltaPages > 0) {
|
|
57
|
+
this.memory.grow(deltaPages);
|
|
58
|
+
this.view = new Uint8Array(this.memory.buffer);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
let backend = null;
|
|
63
|
+
function getBackend() {
|
|
64
|
+
if (!backend) {
|
|
65
|
+
try {
|
|
66
|
+
backend = new WasmFnv64();
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
backend = new Fnv64Fallback();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return backend;
|
|
73
|
+
}
|
|
74
|
+
export function hashBytes(bytes) {
|
|
75
|
+
return getBackend().hash(bytes);
|
|
76
|
+
}
|
|
77
|
+
export function hashText(text) {
|
|
78
|
+
return hashBytes(textEncoder.encode(text));
|
|
79
|
+
}
|
|
80
|
+
export function getHashBackendName() {
|
|
81
|
+
return getBackend().kind;
|
|
82
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { LoggerLike, PluginConfig } from "./types.js";
|
|
2
|
+
type Disposable = {
|
|
3
|
+
close(): void;
|
|
4
|
+
};
|
|
5
|
+
interface RpcLike {
|
|
6
|
+
call<T>(method: string, params: unknown): Promise<T>;
|
|
7
|
+
}
|
|
8
|
+
type RpcGetterLike = () => Promise<RpcLike>;
|
|
9
|
+
interface FsDirentLike {
|
|
10
|
+
name: string;
|
|
11
|
+
isDirectory(): boolean;
|
|
12
|
+
isFile(): boolean;
|
|
13
|
+
}
|
|
14
|
+
interface FsWatcherLike extends Disposable {
|
|
15
|
+
on(event: "error", handler: (error: Error) => void): void;
|
|
16
|
+
}
|
|
17
|
+
interface FsApi {
|
|
18
|
+
readdir(dir: string): Promise<FsDirentLike[]>;
|
|
19
|
+
readFile(file: string): Promise<Uint8Array>;
|
|
20
|
+
stat(file: string): Promise<{
|
|
21
|
+
size: number;
|
|
22
|
+
mtimeMs: number;
|
|
23
|
+
}>;
|
|
24
|
+
watch(dir: string, onChange: (event: string, filename: string | Buffer | null) => void): FsWatcherLike;
|
|
25
|
+
}
|
|
26
|
+
export interface MarkdownSourceAdapter {
|
|
27
|
+
kind: string;
|
|
28
|
+
start(): Promise<void>;
|
|
29
|
+
refresh(): Promise<void>;
|
|
30
|
+
stop(): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
export interface MarkdownIngestionHandle {
|
|
33
|
+
start(): Promise<void>;
|
|
34
|
+
refresh(): Promise<void>;
|
|
35
|
+
stop(): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
export interface MarkdownIngestionSnapshot {
|
|
38
|
+
fileHash: string;
|
|
39
|
+
size: number;
|
|
40
|
+
mtimeMs: number;
|
|
41
|
+
}
|
|
42
|
+
export declare function createMarkdownIngestionHandle(cfg: PluginConfig, getRpc: RpcGetterLike, logger?: LoggerLike, fsApi?: FsApi): MarkdownIngestionHandle;
|
|
43
|
+
export {};
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import fsp from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { hashBytes } from "./markdown-hash.js";
|
|
5
|
+
const DEFAULT_DEBOUNCE_MS = 150;
|
|
6
|
+
const DEFAULT_TOKENIZER_ID = "markdown-ingest:v1";
|
|
7
|
+
const MARKDOWN_INGEST_VERSION = 3;
|
|
8
|
+
const HASH_BACKEND = "wasm-fnv1a64";
|
|
9
|
+
export function createMarkdownIngestionHandle(cfg, getRpc, logger = console, fsApi = createRealFsApi()) {
|
|
10
|
+
const adapters = [];
|
|
11
|
+
const genericRoots = normalizeMarkdownRoots(cfg.markdownIngestionRoots);
|
|
12
|
+
if (isMarkdownIngestionEnabled(cfg, genericRoots)) {
|
|
13
|
+
adapters.push(new DirectoryMarkdownSourceAdapter("generic", {
|
|
14
|
+
roots: genericRoots,
|
|
15
|
+
include: cfg.markdownIngestionInclude,
|
|
16
|
+
exclude: cfg.markdownIngestionExclude,
|
|
17
|
+
debounceMs: cfg.markdownIngestionDebounceMs ?? DEFAULT_DEBOUNCE_MS,
|
|
18
|
+
}, getRpc, logger, fsApi));
|
|
19
|
+
}
|
|
20
|
+
const obsidianRoots = normalizeMarkdownRoots(cfg.markdownIngestionObsidianRoots);
|
|
21
|
+
if (cfg.markdownIngestionObsidianEnabled !== false && obsidianRoots.length > 0) {
|
|
22
|
+
adapters.push(new DirectoryMarkdownSourceAdapter("obsidian", {
|
|
23
|
+
roots: obsidianRoots,
|
|
24
|
+
include: cfg.markdownIngestionObsidianInclude,
|
|
25
|
+
exclude: cfg.markdownIngestionObsidianExclude,
|
|
26
|
+
debounceMs: cfg.markdownIngestionObsidianDebounceMs ?? cfg.markdownIngestionDebounceMs ?? DEFAULT_DEBOUNCE_MS,
|
|
27
|
+
}, getRpc, logger, fsApi));
|
|
28
|
+
}
|
|
29
|
+
if (adapters.length === 0) {
|
|
30
|
+
return {
|
|
31
|
+
async start() { },
|
|
32
|
+
async refresh() { },
|
|
33
|
+
async stop() { },
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const adapter = new CompositeMarkdownSourceAdapter(adapters);
|
|
37
|
+
return {
|
|
38
|
+
start: () => adapter.start(),
|
|
39
|
+
refresh: () => adapter.refresh(),
|
|
40
|
+
stop: () => adapter.stop(),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
class CompositeMarkdownSourceAdapter {
|
|
44
|
+
adapters;
|
|
45
|
+
kind = "composite";
|
|
46
|
+
constructor(adapters) {
|
|
47
|
+
this.adapters = adapters;
|
|
48
|
+
}
|
|
49
|
+
async start() {
|
|
50
|
+
for (const adapter of this.adapters) {
|
|
51
|
+
await adapter.start();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async refresh() {
|
|
55
|
+
for (const adapter of this.adapters) {
|
|
56
|
+
await adapter.refresh();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async stop() {
|
|
60
|
+
for (const adapter of this.adapters) {
|
|
61
|
+
await adapter.stop();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
class DirectoryMarkdownSourceAdapter {
|
|
66
|
+
kind;
|
|
67
|
+
roots;
|
|
68
|
+
includePatterns;
|
|
69
|
+
excludePatterns;
|
|
70
|
+
debounceMs;
|
|
71
|
+
fsApi;
|
|
72
|
+
getRpc;
|
|
73
|
+
logger;
|
|
74
|
+
states = new Map();
|
|
75
|
+
fileStates = new Map();
|
|
76
|
+
tokenizerId;
|
|
77
|
+
coreDoc;
|
|
78
|
+
started = false;
|
|
79
|
+
constructor(kind, config, getRpc, logger, fsApi) {
|
|
80
|
+
this.kind = kind;
|
|
81
|
+
this.roots = config.roots;
|
|
82
|
+
this.includePatterns = config.include?.length ? config.include : [];
|
|
83
|
+
this.excludePatterns = config.exclude?.length ? config.exclude : [];
|
|
84
|
+
this.debounceMs = config.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
85
|
+
this.fsApi = fsApi;
|
|
86
|
+
this.getRpc = getRpc;
|
|
87
|
+
this.logger = logger;
|
|
88
|
+
this.tokenizerId = DEFAULT_TOKENIZER_ID;
|
|
89
|
+
this.coreDoc = true;
|
|
90
|
+
}
|
|
91
|
+
async start() {
|
|
92
|
+
if (this.started) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
this.started = true;
|
|
96
|
+
await this.refresh();
|
|
97
|
+
}
|
|
98
|
+
async refresh() {
|
|
99
|
+
if (!this.started) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
for (const root of this.roots) {
|
|
103
|
+
await this.scanRoot(root);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async stop() {
|
|
107
|
+
for (const state of this.states.values()) {
|
|
108
|
+
if (state.scanState.timer) {
|
|
109
|
+
clearTimeout(state.scanState.timer);
|
|
110
|
+
state.scanState.timer = null;
|
|
111
|
+
}
|
|
112
|
+
for (const watcher of state.directoryWatchers.values()) {
|
|
113
|
+
watcher.close();
|
|
114
|
+
}
|
|
115
|
+
state.directoryWatchers.clear();
|
|
116
|
+
}
|
|
117
|
+
this.states.clear();
|
|
118
|
+
this.fileStates.clear();
|
|
119
|
+
this.started = false;
|
|
120
|
+
}
|
|
121
|
+
getRootState(root) {
|
|
122
|
+
const resolved = path.resolve(root);
|
|
123
|
+
const existing = this.states.get(resolved);
|
|
124
|
+
if (existing) {
|
|
125
|
+
return existing;
|
|
126
|
+
}
|
|
127
|
+
const created = {
|
|
128
|
+
root: resolved,
|
|
129
|
+
scanState: {
|
|
130
|
+
scanning: false,
|
|
131
|
+
dirty: false,
|
|
132
|
+
timer: null,
|
|
133
|
+
},
|
|
134
|
+
knownFiles: new Set(),
|
|
135
|
+
directoryWatchers: new Map(),
|
|
136
|
+
};
|
|
137
|
+
this.states.set(resolved, created);
|
|
138
|
+
return created;
|
|
139
|
+
}
|
|
140
|
+
async scanRoot(root) {
|
|
141
|
+
const rootState = this.getRootState(root);
|
|
142
|
+
if (rootState.scanState.scanning) {
|
|
143
|
+
rootState.scanState.dirty = true;
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
rootState.scanState.scanning = true;
|
|
147
|
+
try {
|
|
148
|
+
const currentFiles = new Set();
|
|
149
|
+
await this.walkDirectory(rootState, rootState.root, currentFiles);
|
|
150
|
+
await this.pruneDeletedFiles(rootState, currentFiles);
|
|
151
|
+
rootState.knownFiles = currentFiles;
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
rootState.scanState.scanning = false;
|
|
155
|
+
if (rootState.scanState.dirty) {
|
|
156
|
+
rootState.scanState.dirty = false;
|
|
157
|
+
this.scheduleRootScan(rootState);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
scheduleRootScan(rootState) {
|
|
162
|
+
if (rootState.scanState.scanning) {
|
|
163
|
+
rootState.scanState.dirty = true;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (rootState.scanState.timer) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
rootState.scanState.timer = setTimeout(() => {
|
|
170
|
+
rootState.scanState.timer = null;
|
|
171
|
+
void this.scanRoot(rootState.root).catch((error) => {
|
|
172
|
+
this.logger.warn?.(`[markdown-ingest] root scan failed for ${rootState.root}: ${formatError(error)}`);
|
|
173
|
+
});
|
|
174
|
+
}, this.debounceMs);
|
|
175
|
+
}
|
|
176
|
+
async walkDirectory(rootState, dir, currentFiles) {
|
|
177
|
+
await this.ensureDirectoryWatcher(rootState, dir);
|
|
178
|
+
let entries;
|
|
179
|
+
try {
|
|
180
|
+
entries = await this.fsApi.readdir(dir);
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
const message = formatError(error);
|
|
184
|
+
if (!message.includes("ENOENT")) {
|
|
185
|
+
this.logger.warn?.(`[markdown-ingest] readdir failed for ${dir}: ${message}`);
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
for (const entry of entries) {
|
|
190
|
+
const child = path.join(dir, entry.name);
|
|
191
|
+
if (entry.isDirectory()) {
|
|
192
|
+
await this.walkDirectory(rootState, child, currentFiles);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (!entry.isFile() || !isMarkdownFile(entry.name)) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (!this.shouldIncludeFile(rootState.root, child)) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
currentFiles.add(child);
|
|
202
|
+
try {
|
|
203
|
+
await this.syncMarkdownFile(rootState, child);
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
this.logger.warn?.(`[markdown-ingest] sync failed for ${child}: ${formatError(error)}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async ensureDirectoryWatcher(rootState, dir) {
|
|
211
|
+
if (rootState.directoryWatchers.has(dir)) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
const watcher = this.fsApi.watch(dir, () => {
|
|
216
|
+
this.scheduleRootScan(rootState);
|
|
217
|
+
});
|
|
218
|
+
watcher.on("error", (error) => {
|
|
219
|
+
this.logger.warn?.(`[markdown-ingest] watch error for ${dir}: ${formatError(error)}`);
|
|
220
|
+
});
|
|
221
|
+
rootState.directoryWatchers.set(dir, watcher);
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
this.logger.warn?.(`[markdown-ingest] watch unavailable for ${dir}: ${formatError(error)}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
shouldIncludeFile(root, filePath) {
|
|
228
|
+
if (isOpenClawMemoryFile(filePath)) {
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
const relative = toPosixPath(path.relative(root, filePath));
|
|
232
|
+
if (this.excludePatterns.length > 0) {
|
|
233
|
+
for (const pattern of this.excludePatterns) {
|
|
234
|
+
if (matchesGlob(relative, pattern)) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (this.includePatterns.length > 0) {
|
|
240
|
+
for (const pattern of this.includePatterns) {
|
|
241
|
+
if (matchesGlob(relative, pattern)) {
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
async pruneDeletedFiles(rootState, currentFiles) {
|
|
250
|
+
const removed = [];
|
|
251
|
+
for (const previous of rootState.knownFiles) {
|
|
252
|
+
if (!currentFiles.has(previous)) {
|
|
253
|
+
removed.push(previous);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (removed.length === 0) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
for (const filePath of removed) {
|
|
260
|
+
await this.deleteSourceDocument(filePath);
|
|
261
|
+
this.fileStates.delete(filePath);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
async syncMarkdownFile(rootState, filePath) {
|
|
265
|
+
const sourceDoc = filePath;
|
|
266
|
+
const relativePath = toPosixPath(path.relative(rootState.root, filePath));
|
|
267
|
+
const stat = await this.safeStat(filePath);
|
|
268
|
+
if (!stat) {
|
|
269
|
+
await this.deleteSourceDocument(sourceDoc);
|
|
270
|
+
this.fileStates.delete(sourceDoc);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const cached = this.fileStates.get(sourceDoc);
|
|
274
|
+
if (cached && cached.size === stat.size && cached.mtimeMs === stat.mtimeMs) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const bytes = await this.safeReadFile(filePath);
|
|
278
|
+
if (!bytes) {
|
|
279
|
+
await this.deleteSourceDocument(sourceDoc);
|
|
280
|
+
this.fileStates.delete(sourceDoc);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const fileHash = hashBytes(bytes);
|
|
284
|
+
if (cached && cached.fileHash === fileHash) {
|
|
285
|
+
this.fileStates.set(sourceDoc, {
|
|
286
|
+
root: rootState.root,
|
|
287
|
+
sourceDoc,
|
|
288
|
+
relativePath,
|
|
289
|
+
fileHash,
|
|
290
|
+
size: stat.size,
|
|
291
|
+
mtimeMs: stat.mtimeMs,
|
|
292
|
+
});
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const text = textDecoder.decode(bytes);
|
|
296
|
+
if (this.kind === "obsidian" && this.includePatterns.length === 0 && !looksLikeObsidianNote(filePath, text)) {
|
|
297
|
+
await this.deleteSourceDocument(sourceDoc);
|
|
298
|
+
this.fileStates.delete(sourceDoc);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
await this.ingestMarkdownDocument(sourceDoc, text, rootState.root, relativePath, fileHash, stat.size, stat.mtimeMs);
|
|
302
|
+
this.fileStates.set(sourceDoc, {
|
|
303
|
+
root: rootState.root,
|
|
304
|
+
sourceDoc,
|
|
305
|
+
relativePath,
|
|
306
|
+
fileHash,
|
|
307
|
+
size: stat.size,
|
|
308
|
+
mtimeMs: stat.mtimeMs,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
async ingestMarkdownDocument(sourceDoc, text, sourceRoot, sourcePath, fileHash, sourceSize, sourceMtimeMs) {
|
|
312
|
+
const rpc = await this.getRpc();
|
|
313
|
+
const params = {
|
|
314
|
+
sourceDoc,
|
|
315
|
+
text,
|
|
316
|
+
tokenizerId: this.tokenizerId,
|
|
317
|
+
coreDoc: this.coreDoc,
|
|
318
|
+
sourceMeta: {
|
|
319
|
+
sourceRoot,
|
|
320
|
+
sourcePath,
|
|
321
|
+
sourceKind: this.kind,
|
|
322
|
+
fileHash,
|
|
323
|
+
sourceSize,
|
|
324
|
+
sourceMtimeMs,
|
|
325
|
+
ingestVersion: MARKDOWN_INGEST_VERSION,
|
|
326
|
+
hashBackend: HASH_BACKEND,
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
await rpc.call("ingest_markdown_document", params);
|
|
330
|
+
}
|
|
331
|
+
async deleteSourceDocument(sourceDoc) {
|
|
332
|
+
const rpc = await this.getRpc();
|
|
333
|
+
const params = { sourceDoc };
|
|
334
|
+
await rpc.call("delete_authored_document", params);
|
|
335
|
+
}
|
|
336
|
+
async safeStat(filePath) {
|
|
337
|
+
try {
|
|
338
|
+
return await this.fsApi.stat(filePath);
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
async safeReadFile(filePath) {
|
|
345
|
+
try {
|
|
346
|
+
return await this.fsApi.readFile(filePath);
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
function toPosixPath(value) {
|
|
354
|
+
return value.split(path.sep).join("/");
|
|
355
|
+
}
|
|
356
|
+
const textDecoder = new TextDecoder();
|
|
357
|
+
function normalizeMarkdownRoots(roots) {
|
|
358
|
+
if (!roots?.length) {
|
|
359
|
+
return [];
|
|
360
|
+
}
|
|
361
|
+
const resolved = new Set();
|
|
362
|
+
for (const root of roots) {
|
|
363
|
+
const trimmed = root.trim();
|
|
364
|
+
if (!trimmed) {
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
resolved.add(path.resolve(trimmed));
|
|
368
|
+
}
|
|
369
|
+
return [...resolved];
|
|
370
|
+
}
|
|
371
|
+
function isMarkdownIngestionEnabled(cfg, roots) {
|
|
372
|
+
if (cfg.markdownIngestionEnabled === false) {
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
return roots.length > 0;
|
|
376
|
+
}
|
|
377
|
+
function createRealFsApi() {
|
|
378
|
+
return {
|
|
379
|
+
readdir: async (dir) => fsp.readdir(dir, { withFileTypes: true }),
|
|
380
|
+
readFile: async (file) => fsp.readFile(file),
|
|
381
|
+
stat: async (file) => {
|
|
382
|
+
const stat = await fsp.stat(file);
|
|
383
|
+
return { size: stat.size, mtimeMs: stat.mtimeMs };
|
|
384
|
+
},
|
|
385
|
+
watch: (dir, onChange) => fs.watch(dir, onChange),
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
function isMarkdownFile(fileName) {
|
|
389
|
+
const lower = fileName.toLowerCase();
|
|
390
|
+
return lower.endsWith(".md") || lower.endsWith(".markdown");
|
|
391
|
+
}
|
|
392
|
+
function matchesGlob(value, pattern) {
|
|
393
|
+
const escaped = pattern
|
|
394
|
+
.split("*")
|
|
395
|
+
.map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&"))
|
|
396
|
+
.join(".*");
|
|
397
|
+
return new RegExp(`^${escaped}$`).test(value);
|
|
398
|
+
}
|
|
399
|
+
function formatError(error) {
|
|
400
|
+
if (error instanceof Error) {
|
|
401
|
+
return error.message;
|
|
402
|
+
}
|
|
403
|
+
return String(error);
|
|
404
|
+
}
|
|
405
|
+
function looksLikeObsidianNote(filePath, text) {
|
|
406
|
+
if (!text.startsWith("---\n")) {
|
|
407
|
+
return hasInlineObsidianTag(text);
|
|
408
|
+
}
|
|
409
|
+
const frontmatterEnd = findFrontmatterEnd(text, 4);
|
|
410
|
+
if (frontmatterEnd < 0) {
|
|
411
|
+
return hasInlineObsidianTag(text);
|
|
412
|
+
}
|
|
413
|
+
const frontmatter = text.slice(4, frontmatterEnd);
|
|
414
|
+
const lines = frontmatter.split("\n");
|
|
415
|
+
for (const line of lines) {
|
|
416
|
+
const trimmed = line.trimStart();
|
|
417
|
+
if (trimmed.startsWith("tags:") ||
|
|
418
|
+
trimmed.startsWith("tag:") ||
|
|
419
|
+
trimmed.startsWith("openclaw:") ||
|
|
420
|
+
trimmed.startsWith("memory:")) {
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return hasInlineObsidianTag(text.slice(frontmatterEnd + 4));
|
|
425
|
+
}
|
|
426
|
+
function findFrontmatterEnd(text, offset) {
|
|
427
|
+
for (let i = offset; i < text.length - 3; i++) {
|
|
428
|
+
if (text.charCodeAt(i) !== 45 || text.charCodeAt(i + 1) !== 45 || text.charCodeAt(i + 2) !== 45) {
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
const next = text.charCodeAt(i + 3);
|
|
432
|
+
if (next === 10) {
|
|
433
|
+
return i;
|
|
434
|
+
}
|
|
435
|
+
if (next === 13 && text.charCodeAt(i + 4) === 10) {
|
|
436
|
+
return i;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return -1;
|
|
440
|
+
}
|
|
441
|
+
function hasInlineObsidianTag(text) {
|
|
442
|
+
let inFence = false;
|
|
443
|
+
const lines = text.split("\n");
|
|
444
|
+
for (const line of lines) {
|
|
445
|
+
const trimmed = line.trimStart();
|
|
446
|
+
if (trimmed.startsWith("```") || trimmed.startsWith("~~~")) {
|
|
447
|
+
inFence = !inFence;
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
if (inFence) {
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
if (trimmed.startsWith("#")) {
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
if (/(^|[^A-Za-z0-9_])#([A-Za-z][A-Za-z0-9/_-]*)\b/.test(line)) {
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
function isOpenClawMemoryFile(filePath) {
|
|
463
|
+
return path.basename(filePath).toLowerCase() === "memory.md";
|
|
464
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { MemoryPromptSectionBuilder } from "openclaw/plugin-sdk/plugin-entry";
|
|
2
|
+
import type { PluginConfig, RecallCache, SearchResult } from "./types.js";
|
|
3
|
+
import type { RpcGetter } from "./plugin-runtime.js";
|
|
4
|
+
export declare function buildMemoryPromptSection(_getRpc: RpcGetter, _cfg: PluginConfig, _recallCache: RecallCache<SearchResult>): MemoryPromptSectionBuilder;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const MEMORY_PROMPT_HEADER = [
|
|
2
|
+
"## Memory",
|
|
3
|
+
"LibraVDB persistent memory is configured. Recalled memories may appear",
|
|
4
|
+
"in context via the context-engine assembler when available and relevant.",
|
|
5
|
+
"",
|
|
6
|
+
];
|
|
7
|
+
export function buildMemoryPromptSection(_getRpc, _cfg, _recallCache) {
|
|
8
|
+
return function memoryPromptSection({ availableTools: _availableTools, citationsMode: _citationsMode, }) {
|
|
9
|
+
// OpenClaw builds the memory prompt section synchronously for embedded runs.
|
|
10
|
+
// Actual retrieval and ranking happen in the context engine during assemble().
|
|
11
|
+
return [...MEMORY_PROMPT_HEADER];
|
|
12
|
+
};
|
|
13
|
+
}
|