@xdarkicex/openclaw-memory-libravdb 1.4.45 → 1.4.47
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/cli.js +1 -6
- package/dist/dream-promotion.js +1 -6
- package/dist/format-error.d.ts +9 -0
- package/dist/format-error.js +14 -0
- package/dist/index.js +1041 -907
- package/dist/ingest-queue.d.ts +39 -0
- package/dist/ingest-queue.js +123 -0
- package/dist/lifecycle-hooks.js +1 -6
- package/dist/markdown-ingest.js +15 -15
- package/dist/plugin-runtime.js +1 -6
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { LoggerLike } from "./types.js";
|
|
2
|
+
import { IngestMode } from "@xdarkicex/libravdb-contracts";
|
|
3
|
+
export interface IngestQueueOptions {
|
|
4
|
+
/** Max tokens per chunk. Infinity = chunking disabled (retry-only mode). */
|
|
5
|
+
chunkTokens: number;
|
|
6
|
+
/** Base delay for exponential backoff retry in ms. */
|
|
7
|
+
retryBaseDelayMs: number;
|
|
8
|
+
/** Max retries per chunk. */
|
|
9
|
+
maxRetries: number;
|
|
10
|
+
}
|
|
11
|
+
interface IngestMarkdownDocumentParams {
|
|
12
|
+
sourceDoc: string;
|
|
13
|
+
text: string;
|
|
14
|
+
tokenizerId: string;
|
|
15
|
+
coreDoc: boolean;
|
|
16
|
+
sourceMeta: {
|
|
17
|
+
sourceRoot: string;
|
|
18
|
+
sourcePath: string;
|
|
19
|
+
sourceKind: string;
|
|
20
|
+
fileHash: string;
|
|
21
|
+
sourceSize: number;
|
|
22
|
+
sourceMtimeMs: number;
|
|
23
|
+
ingestVersion: number;
|
|
24
|
+
hashBackend: string;
|
|
25
|
+
};
|
|
26
|
+
mode?: IngestMode;
|
|
27
|
+
}
|
|
28
|
+
export declare class IngestQueue {
|
|
29
|
+
private readonly queue;
|
|
30
|
+
private readonly rpcCall;
|
|
31
|
+
private readonly logger;
|
|
32
|
+
private readonly options;
|
|
33
|
+
private running;
|
|
34
|
+
constructor(rpcCall: <T>(method: string, params: unknown) => Promise<T>, logger: LoggerLike, options?: Partial<IngestQueueOptions>);
|
|
35
|
+
enqueueIngest(sourceDoc: string, text: string, baseParams: Omit<IngestMarkdownDocumentParams, "sourceDoc" | "text" | "mode">): Promise<void>;
|
|
36
|
+
private ingestWithRetry;
|
|
37
|
+
enqueueDelete(sourceDoc: string): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { IngestMode } from "@xdarkicex/libravdb-contracts";
|
|
2
|
+
const DEFAULT_OPTIONS = {
|
|
3
|
+
chunkTokens: 8192,
|
|
4
|
+
retryBaseDelayMs: 500,
|
|
5
|
+
maxRetries: 4,
|
|
6
|
+
};
|
|
7
|
+
export class IngestQueue {
|
|
8
|
+
queue = [];
|
|
9
|
+
rpcCall;
|
|
10
|
+
logger;
|
|
11
|
+
options;
|
|
12
|
+
running = false;
|
|
13
|
+
constructor(rpcCall, logger, options = {}) {
|
|
14
|
+
this.rpcCall = rpcCall;
|
|
15
|
+
this.logger = logger;
|
|
16
|
+
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
17
|
+
}
|
|
18
|
+
async enqueueIngest(sourceDoc, text, baseParams) {
|
|
19
|
+
if (this.options.chunkTokens === Infinity) {
|
|
20
|
+
// Retry-only mode: send full text as single chunk
|
|
21
|
+
return this.ingestWithRetry({
|
|
22
|
+
...baseParams,
|
|
23
|
+
sourceDoc,
|
|
24
|
+
text,
|
|
25
|
+
mode: IngestMode.REPLACE,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
const chunks = splitIntoChunks(text, this.options.chunkTokens);
|
|
29
|
+
if (chunks.length === 1) {
|
|
30
|
+
return this.ingestWithRetry({
|
|
31
|
+
...baseParams,
|
|
32
|
+
sourceDoc,
|
|
33
|
+
text: chunks[0].text,
|
|
34
|
+
mode: IngestMode.REPLACE,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
// Multiple chunks: use APPEND mode for all but last (which can be REPLACE)
|
|
38
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
39
|
+
const isLast = i === chunks.length - 1;
|
|
40
|
+
const chunkParams = {
|
|
41
|
+
...baseParams,
|
|
42
|
+
sourceDoc,
|
|
43
|
+
text: chunks[i].text,
|
|
44
|
+
mode: isLast ? IngestMode.REPLACE : IngestMode.APPEND,
|
|
45
|
+
};
|
|
46
|
+
await this.ingestWithRetry(chunkParams);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async ingestWithRetry(params) {
|
|
50
|
+
await withRetry(() => this.rpcCall("ingest_markdown_document", params), this.options.maxRetries, this.options.retryBaseDelayMs, this.logger, `ingest_markdown_document(${params.sourceDoc})`);
|
|
51
|
+
}
|
|
52
|
+
async enqueueDelete(sourceDoc) {
|
|
53
|
+
await withRetry(() => this.rpcCall("delete_authored_document", { sourceDoc }), this.options.maxRetries, this.options.retryBaseDelayMs, this.logger, `delete_authored_document(${sourceDoc})`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function splitIntoChunks(text, maxTokens) {
|
|
57
|
+
// Approximate: 4 chars per token for typical English text
|
|
58
|
+
const maxChars = maxTokens * 4;
|
|
59
|
+
if (text.length <= maxChars) {
|
|
60
|
+
return [{ text, ordinal: 0 }];
|
|
61
|
+
}
|
|
62
|
+
const chunks = [];
|
|
63
|
+
let offset = 0;
|
|
64
|
+
let ordinal = 0;
|
|
65
|
+
while (offset < text.length) {
|
|
66
|
+
let end = Math.min(offset + maxChars, text.length);
|
|
67
|
+
// Walk back up to 256 chars looking for a sentence boundary
|
|
68
|
+
const probeLimit = Math.min(256, end - offset);
|
|
69
|
+
let hardCut = end;
|
|
70
|
+
for (let i = 0; i < probeLimit; i++) {
|
|
71
|
+
const pos = end - i;
|
|
72
|
+
const ch = text.charAt(pos);
|
|
73
|
+
if (ch === "\n" && text.charAt(pos + 1) === "\n") {
|
|
74
|
+
hardCut = pos + 2;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (hardCut === end) {
|
|
79
|
+
for (let i = 0; i < probeLimit; i++) {
|
|
80
|
+
const pos = end - i;
|
|
81
|
+
if (text.charAt(pos) === "\n") {
|
|
82
|
+
hardCut = pos + 1;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (hardCut === end) {
|
|
88
|
+
for (let i = 0; i < probeLimit; i++) {
|
|
89
|
+
const pos = end - i;
|
|
90
|
+
if (text.charAt(pos) === " ") {
|
|
91
|
+
hardCut = pos;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
chunks.push({ text: text.slice(offset, hardCut), ordinal });
|
|
97
|
+
ordinal++;
|
|
98
|
+
offset = hardCut;
|
|
99
|
+
}
|
|
100
|
+
return chunks;
|
|
101
|
+
}
|
|
102
|
+
async function withRetry(fn, maxRetries, baseDelayMs, logger, label) {
|
|
103
|
+
let lastError;
|
|
104
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
105
|
+
try {
|
|
106
|
+
return await fn();
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
lastError = err;
|
|
110
|
+
if (attempt < maxRetries) {
|
|
111
|
+
// Full jitter: random * cap
|
|
112
|
+
const cap = baseDelayMs * Math.pow(2, attempt);
|
|
113
|
+
const delay = Math.random() * cap;
|
|
114
|
+
logger.warn?.(`[ingest-queue] ${label} failed (attempt ${attempt + 1}/${maxRetries + 1}), retrying in ${Math.round(delay)}ms: ${err}`);
|
|
115
|
+
await sleep(delay);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
120
|
+
}
|
|
121
|
+
function sleep(ms) {
|
|
122
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
123
|
+
}
|
package/dist/lifecycle-hooks.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { formatError } from "./format-error.js";
|
|
1
2
|
export function createBeforeResetHook(runtime, logger = console) {
|
|
2
3
|
return async (event, ctx) => {
|
|
3
4
|
const typedEvent = asBeforeResetEvent(event);
|
|
@@ -56,9 +57,3 @@ function asSessionEndEvent(value) {
|
|
|
56
57
|
function isRecord(value) {
|
|
57
58
|
return typeof value === "object" && value !== null;
|
|
58
59
|
}
|
|
59
|
-
function formatError(error) {
|
|
60
|
-
if (error instanceof Error && error.message.trim()) {
|
|
61
|
-
return error.message;
|
|
62
|
-
}
|
|
63
|
-
return String(error);
|
|
64
|
-
}
|
package/dist/markdown-ingest.js
CHANGED
|
@@ -2,6 +2,8 @@ import fs from "node:fs";
|
|
|
2
2
|
import fsp from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { hashBytes } from "./markdown-hash.js";
|
|
5
|
+
import { formatError } from "./format-error.js";
|
|
6
|
+
import { IngestQueue } from "./ingest-queue.js";
|
|
5
7
|
const DEFAULT_DEBOUNCE_MS = 150;
|
|
6
8
|
const DEFAULT_TOKENIZER_ID = "markdown-ingest:v1";
|
|
7
9
|
const MARKDOWN_INGEST_VERSION = 3;
|
|
@@ -77,6 +79,7 @@ class DirectoryMarkdownSourceAdapter {
|
|
|
77
79
|
tokenizerId;
|
|
78
80
|
coreDoc;
|
|
79
81
|
started = false;
|
|
82
|
+
ingestQueue = null;
|
|
80
83
|
stopping = false;
|
|
81
84
|
constructor(kind, config, getRpc, logger, fsApi) {
|
|
82
85
|
this.kind = kind;
|
|
@@ -342,10 +345,8 @@ class DirectoryMarkdownSourceAdapter {
|
|
|
342
345
|
});
|
|
343
346
|
}
|
|
344
347
|
async ingestMarkdownDocument(sourceDoc, text, sourceRoot, sourcePath, fileHash, sourceSize, sourceMtimeMs) {
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
sourceDoc,
|
|
348
|
-
text,
|
|
348
|
+
const queue = await this.getIngestQueue();
|
|
349
|
+
await queue.enqueueIngest(sourceDoc, text, {
|
|
349
350
|
tokenizerId: this.tokenizerId,
|
|
350
351
|
coreDoc: this.coreDoc,
|
|
351
352
|
sourceMeta: {
|
|
@@ -358,13 +359,18 @@ class DirectoryMarkdownSourceAdapter {
|
|
|
358
359
|
ingestVersion: MARKDOWN_INGEST_VERSION,
|
|
359
360
|
hashBackend: HASH_BACKEND,
|
|
360
361
|
},
|
|
361
|
-
};
|
|
362
|
-
await rpc.call("ingest_markdown_document", params);
|
|
362
|
+
});
|
|
363
363
|
}
|
|
364
364
|
async deleteSourceDocument(sourceDoc) {
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
365
|
+
const queue = await this.getIngestQueue();
|
|
366
|
+
await queue.enqueueDelete(sourceDoc);
|
|
367
|
+
}
|
|
368
|
+
async getIngestQueue() {
|
|
369
|
+
if (!this.ingestQueue) {
|
|
370
|
+
const rpc = await this.getRpc();
|
|
371
|
+
this.ingestQueue = new IngestQueue(rpc.call.bind(rpc), this.logger);
|
|
372
|
+
}
|
|
373
|
+
return this.ingestQueue;
|
|
368
374
|
}
|
|
369
375
|
async safeStat(filePath) {
|
|
370
376
|
try {
|
|
@@ -429,12 +435,6 @@ function matchesGlob(value, pattern) {
|
|
|
429
435
|
.join(".*");
|
|
430
436
|
return new RegExp(`^${escaped}$`).test(value);
|
|
431
437
|
}
|
|
432
|
-
function formatError(error) {
|
|
433
|
-
if (error instanceof Error) {
|
|
434
|
-
return error.message;
|
|
435
|
-
}
|
|
436
|
-
return String(error);
|
|
437
|
-
}
|
|
438
438
|
function looksLikeObsidianNote(filePath, text) {
|
|
439
439
|
if (!text.startsWith("---\n")) {
|
|
440
440
|
return hasInlineObsidianTag(text);
|
package/dist/plugin-runtime.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { RpcClient } from "./rpc.js";
|
|
2
2
|
import { GrpcKernelClient } from "./grpc-client.js";
|
|
3
3
|
import { daemonProvisioningHint, startSidecar } from "./sidecar.js";
|
|
4
|
+
import { formatError } from "./format-error.js";
|
|
4
5
|
import { readFileSync } from "node:fs";
|
|
5
6
|
export const DEFAULT_RPC_TIMEOUT_MS = 30000;
|
|
6
7
|
export const STARTUP_HEALTH_TIMEOUT_MS = 2000;
|
|
@@ -124,12 +125,6 @@ function loadSecretFromEnv() {
|
|
|
124
125
|
}
|
|
125
126
|
return undefined;
|
|
126
127
|
}
|
|
127
|
-
function formatError(error) {
|
|
128
|
-
if (error instanceof Error && error.message.trim()) {
|
|
129
|
-
return error.message;
|
|
130
|
-
}
|
|
131
|
-
return String(error);
|
|
132
|
-
}
|
|
133
128
|
export function enrichStartupError(error, healthMessage) {
|
|
134
129
|
const rawMessage = error instanceof Error ? error.message : String(error);
|
|
135
130
|
const message = rawMessage.trim() || "LibraVDB daemon startup failed";
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xdarkicex/openclaw-memory-libravdb",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.47",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"@grpc/proto-loader": "^0.8.0",
|
|
74
74
|
"@openclaw/plugin-inspector": "0.3.7",
|
|
75
75
|
"@types/node": "^20.11.0",
|
|
76
|
-
"@xdarkicex/libravdb-contracts": "^0.0.
|
|
76
|
+
"@xdarkicex/libravdb-contracts": "^0.0.6",
|
|
77
77
|
"esbuild": "^0.27.0",
|
|
78
78
|
"typescript": "^6.0.3"
|
|
79
79
|
},
|