@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.
@@ -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
+ }
@@ -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
- }
@@ -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 rpc = await this.getRpc();
346
- const params = {
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 rpc = await this.getRpc();
366
- const params = { sourceDoc };
367
- 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;
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);
@@ -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";
@@ -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.45",
5
+ "version": "1.4.47",
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.45",
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.1",
76
+ "@xdarkicex/libravdb-contracts": "^0.0.6",
77
77
  "esbuild": "^0.27.0",
78
78
  "typescript": "^6.0.3"
79
79
  },