@xdarkicex/openclaw-memory-libravdb 1.4.46 → 1.4.48

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.
@@ -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,124 @@
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: clear the source once, then append the remaining chunks.
38
+ // Sending REPLACE last deletes the earlier chunks from the same source_doc.
39
+ for (let i = 0; i < chunks.length; i++) {
40
+ const isFirst = i === 0;
41
+ const chunkParams = {
42
+ ...baseParams,
43
+ sourceDoc,
44
+ text: chunks[i].text,
45
+ mode: isFirst ? IngestMode.REPLACE : IngestMode.APPEND,
46
+ };
47
+ await this.ingestWithRetry(chunkParams);
48
+ }
49
+ }
50
+ async ingestWithRetry(params) {
51
+ await withRetry(() => this.rpcCall("ingest_markdown_document", params), this.options.maxRetries, this.options.retryBaseDelayMs, this.logger, `ingest_markdown_document(${params.sourceDoc})`);
52
+ }
53
+ async enqueueDelete(sourceDoc) {
54
+ await withRetry(() => this.rpcCall("delete_authored_document", { sourceDoc }), this.options.maxRetries, this.options.retryBaseDelayMs, this.logger, `delete_authored_document(${sourceDoc})`);
55
+ }
56
+ }
57
+ function splitIntoChunks(text, maxTokens) {
58
+ // Approximate: 4 chars per token for typical English text
59
+ const maxChars = maxTokens * 4;
60
+ if (text.length <= maxChars) {
61
+ return [{ text, ordinal: 0 }];
62
+ }
63
+ const chunks = [];
64
+ let offset = 0;
65
+ let ordinal = 0;
66
+ while (offset < text.length) {
67
+ let end = Math.min(offset + maxChars, text.length);
68
+ // Walk back up to 256 chars looking for a sentence boundary
69
+ const probeLimit = Math.min(256, end - offset);
70
+ let hardCut = end;
71
+ for (let i = 0; i < probeLimit; i++) {
72
+ const pos = end - i;
73
+ const ch = text.charAt(pos);
74
+ if (ch === "\n" && text.charAt(pos + 1) === "\n") {
75
+ hardCut = pos + 2;
76
+ break;
77
+ }
78
+ }
79
+ if (hardCut === end) {
80
+ for (let i = 0; i < probeLimit; i++) {
81
+ const pos = end - i;
82
+ if (text.charAt(pos) === "\n") {
83
+ hardCut = pos + 1;
84
+ break;
85
+ }
86
+ }
87
+ }
88
+ if (hardCut === end) {
89
+ for (let i = 0; i < probeLimit; i++) {
90
+ const pos = end - i;
91
+ if (text.charAt(pos) === " ") {
92
+ hardCut = pos;
93
+ break;
94
+ }
95
+ }
96
+ }
97
+ chunks.push({ text: text.slice(offset, hardCut), ordinal });
98
+ ordinal++;
99
+ offset = hardCut;
100
+ }
101
+ return chunks;
102
+ }
103
+ async function withRetry(fn, maxRetries, baseDelayMs, logger, label) {
104
+ let lastError;
105
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
106
+ try {
107
+ return await fn();
108
+ }
109
+ catch (err) {
110
+ lastError = err;
111
+ if (attempt < maxRetries) {
112
+ // Full jitter: random * cap
113
+ const cap = baseDelayMs * Math.pow(2, attempt);
114
+ const delay = Math.random() * cap;
115
+ logger.warn?.(`[ingest-queue] ${label} failed (attempt ${attempt + 1}/${maxRetries + 1}), retrying in ${Math.round(delay)}ms: ${err}`);
116
+ await sleep(delay);
117
+ }
118
+ }
119
+ }
120
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
121
+ }
122
+ function sleep(ms) {
123
+ return new Promise((resolve) => setTimeout(resolve, ms));
124
+ }
@@ -3,6 +3,7 @@ import fsp from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { hashBytes } from "./markdown-hash.js";
5
5
  import { formatError } from "./format-error.js";
6
+ import { IngestQueue } from "./ingest-queue.js";
6
7
  const DEFAULT_DEBOUNCE_MS = 150;
7
8
  const DEFAULT_TOKENIZER_ID = "markdown-ingest:v1";
8
9
  const MARKDOWN_INGEST_VERSION = 3;
@@ -78,6 +79,7 @@ class DirectoryMarkdownSourceAdapter {
78
79
  tokenizerId;
79
80
  coreDoc;
80
81
  started = false;
82
+ ingestQueue = null;
81
83
  stopping = false;
82
84
  constructor(kind, config, getRpc, logger, fsApi) {
83
85
  this.kind = kind;
@@ -343,10 +345,8 @@ class DirectoryMarkdownSourceAdapter {
343
345
  });
344
346
  }
345
347
  async ingestMarkdownDocument(sourceDoc, text, sourceRoot, sourcePath, fileHash, sourceSize, sourceMtimeMs) {
346
- const rpc = await this.getRpc();
347
- const params = {
348
- sourceDoc,
349
- text,
348
+ const queue = await this.getIngestQueue();
349
+ await queue.enqueueIngest(sourceDoc, text, {
350
350
  tokenizerId: this.tokenizerId,
351
351
  coreDoc: this.coreDoc,
352
352
  sourceMeta: {
@@ -359,13 +359,18 @@ class DirectoryMarkdownSourceAdapter {
359
359
  ingestVersion: MARKDOWN_INGEST_VERSION,
360
360
  hashBackend: HASH_BACKEND,
361
361
  },
362
- };
363
- await rpc.call("ingest_markdown_document", params);
362
+ });
364
363
  }
365
364
  async deleteSourceDocument(sourceDoc) {
366
- const rpc = await this.getRpc();
367
- const params = { sourceDoc };
368
- await rpc.call("delete_authored_document", params);
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;
369
374
  }
370
375
  async safeStat(filePath) {
371
376
  try {
@@ -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.46",
5
+ "version": "1.4.48",
6
6
  "kind": [
7
7
  "memory",
8
8
  "context-engine"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xdarkicex/openclaw-memory-libravdb",
3
- "version": "1.4.46",
3
+ "version": "1.4.48",
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.1",
76
+ "@xdarkicex/libravdb-contracts": "^0.0.6",
77
77
  "esbuild": "^0.27.0",
78
78
  "typescript": "^6.0.3"
79
79
  },