@xdarkicex/openclaw-memory-libravdb 1.6.25 → 1.6.28

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.
@@ -10,6 +10,9 @@ export interface LibravDBClientOptions {
10
10
  tlsMode?: "auto" | "tls" | "insecure";
11
11
  tlsClientCertPath?: string;
12
12
  tlsClientKeyPath?: string;
13
+ /** Stable tenant key for multi-agent DB routing. Attached as the
14
+ * `libravdb-tenant-key` gRPC metadata header on every call. */
15
+ tenantKey?: string;
13
16
  }
14
17
  export declare function resolveClientEndpoint(configuredEndpoint?: string): string;
15
18
  export declare function isLegacyJsonRpcHealthResponse(payload: string): boolean;
@@ -19,6 +19,8 @@ export function resolveClientEndpoint(configuredEndpoint) {
19
19
  path.join(os.homedir(), ".libravdbd", "run"),
20
20
  "/opt/homebrew/var/libravdbd/run",
21
21
  "/usr/local/var/libravdbd/run",
22
+ "/var/run/libravdbd",
23
+ "/run/libravdbd",
22
24
  ];
23
25
  for (const dir of candidateDirs) {
24
26
  const fullPath = path.join(dir, sockName);
@@ -178,6 +180,15 @@ export class LibravDBClient {
178
180
  bootstrap: () => self.bootstrapHandshake(),
179
181
  rpcMutex,
180
182
  });
183
+ const interceptors = [];
184
+ if (options.tenantKey) {
185
+ const tenantKey = options.tenantKey;
186
+ interceptors.push((next) => async (req) => {
187
+ req.header.set("libravdb-tenant-key", tenantKey);
188
+ return next(req);
189
+ });
190
+ }
191
+ interceptors.push(authInterceptor);
181
192
  const transport = createGrpcTransport({
182
193
  baseUrl: targetUrl,
183
194
  httpVersion: "2",
@@ -190,7 +201,7 @@ export class LibravDBClient {
190
201
  ...(isInsecure ? { rejectUnauthorized: false } : {}),
191
202
  },
192
203
  defaultTimeoutMs: options.timeoutMs ?? 30000,
193
- interceptors: [authInterceptor],
204
+ interceptors,
194
205
  });
195
206
  this.client = createPromiseClient(LibravDB, transport);
196
207
  }
@@ -0,0 +1,46 @@
1
+ export interface TurnEntry {
2
+ index: number;
3
+ role: string;
4
+ contentHash: string;
5
+ turnHash: string;
6
+ ingestedAt: number;
7
+ }
8
+ export interface TurnManifest {
9
+ sessionId: string;
10
+ version: number;
11
+ turns: TurnEntry[];
12
+ tailHash: string;
13
+ }
14
+ export interface KernelCompatibleMessage {
15
+ role: string;
16
+ content: string;
17
+ id?: string;
18
+ }
19
+ export declare class TurnManifestStore {
20
+ private manifestDir;
21
+ constructor();
22
+ private getManifestPath;
23
+ hashString(data: string): string;
24
+ createEmpty(sessionId: string): TurnManifest;
25
+ load(sessionId: string, logger?: {
26
+ warn?: (msg: string) => void;
27
+ error?: (msg: string, e: unknown) => void;
28
+ }): TurnManifest;
29
+ save(manifest: TurnManifest): void;
30
+ verifyChain(manifest: TurnManifest): boolean;
31
+ /**
32
+ * Finds the overlap point between incoming messages and our stored history.
33
+ * Returns the index into incomingMessages where new (un-ACKed) messages begin.
34
+ * Returns 0 if no overlap (full re-sync).
35
+ */
36
+ findOverlapIndex(manifest: TurnManifest, incomingMessages: KernelCompatibleMessage[]): number;
37
+ appendACKedMessages(manifest: TurnManifest, newMessages: KernelCompatibleMessage[], startingIndex: number): TurnManifest;
38
+ /**
39
+ * Determines the absolute starting index for a set of new messages.
40
+ * If we have stored turns, the next message's index is last_turn.index + 1.
41
+ * If the manifest is empty, we infer from OpenClaw's prePromptMessageCount signal
42
+ * (caller must provide this as a hint when available).
43
+ */
44
+ deriveStartingIndex(manifest: TurnManifest, prePromptMessageCountHint?: number): number;
45
+ }
46
+ export declare const manifestStore: TurnManifestStore;
@@ -0,0 +1,127 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import * as crypto from "crypto";
4
+ import * as os from "os";
5
+ export class TurnManifestStore {
6
+ manifestDir;
7
+ constructor() {
8
+ this.manifestDir = path.join(os.homedir(), ".openclaw", "libravdb-manifests");
9
+ if (!fs.existsSync(this.manifestDir)) {
10
+ fs.mkdirSync(this.manifestDir, { recursive: true });
11
+ }
12
+ }
13
+ getManifestPath(sessionId) {
14
+ const safe = sessionId.replace(/[^A-Za-z0-9._-]/g, "_");
15
+ return path.join(this.manifestDir, `${safe}.manifest.json`);
16
+ }
17
+ hashString(data) {
18
+ return crypto.createHash("sha256").update(data).digest("hex");
19
+ }
20
+ createEmpty(sessionId) {
21
+ return {
22
+ sessionId,
23
+ version: 0,
24
+ turns: [],
25
+ tailHash: "0000000000000000000000000000000000000000000000000000000000000000",
26
+ };
27
+ }
28
+ load(sessionId, logger) {
29
+ const filePath = this.getManifestPath(sessionId);
30
+ if (!fs.existsSync(filePath)) {
31
+ return this.createEmpty(sessionId);
32
+ }
33
+ try {
34
+ const raw = fs.readFileSync(filePath, "utf8");
35
+ const manifest = JSON.parse(raw);
36
+ if (!this.verifyChain(manifest)) {
37
+ logger?.warn?.(`[LibraVDB] Manifest chain broken for session ${sessionId}. Forcing re-sync.`);
38
+ return this.createEmpty(sessionId);
39
+ }
40
+ return manifest;
41
+ }
42
+ catch (e) {
43
+ logger?.error?.(`[LibraVDB] Failed to read manifest for ${sessionId}:`, e);
44
+ return this.createEmpty(sessionId);
45
+ }
46
+ }
47
+ save(manifest) {
48
+ const filePath = this.getManifestPath(manifest.sessionId);
49
+ const tempPath = `${filePath}.${process.pid}.tmp`;
50
+ fs.writeFileSync(tempPath, JSON.stringify(manifest, null, 2), "utf8");
51
+ fs.renameSync(tempPath, filePath);
52
+ }
53
+ verifyChain(manifest) {
54
+ let currentHash = "0000000000000000000000000000000000000000000000000000000000000000";
55
+ for (const turn of manifest.turns) {
56
+ const expectedHash = this.hashString(`${turn.index}${turn.role}${turn.contentHash}${currentHash}`);
57
+ if (turn.turnHash !== expectedHash) {
58
+ return false;
59
+ }
60
+ currentHash = expectedHash;
61
+ }
62
+ return manifest.tailHash === currentHash;
63
+ }
64
+ /**
65
+ * Finds the overlap point between incoming messages and our stored history.
66
+ * Returns the index into incomingMessages where new (un-ACKed) messages begin.
67
+ * Returns 0 if no overlap (full re-sync).
68
+ */
69
+ findOverlapIndex(manifest, incomingMessages) {
70
+ if (manifest.turns.length === 0) {
71
+ return 0;
72
+ }
73
+ // Build a map of contentHash → index in our manifest
74
+ const known = new Map();
75
+ for (const turn of manifest.turns) {
76
+ known.set(turn.contentHash, turn.index);
77
+ }
78
+ // Scan incoming messages from newest to oldest to find the last match
79
+ for (let i = incomingMessages.length - 1; i >= 0; i--) {
80
+ const contentHash = this.hashString(incomingMessages[i].content);
81
+ if (known.has(contentHash)) {
82
+ return i + 1; // everything at and after this index is new
83
+ }
84
+ }
85
+ // No overlap found — OpenClaw trimmed too much or session diverged
86
+ return 0;
87
+ }
88
+ appendACKedMessages(manifest, newMessages, startingIndex) {
89
+ let currentHash = manifest.tailHash;
90
+ const newTurns = [];
91
+ for (let i = 0; i < newMessages.length; i++) {
92
+ const msg = newMessages[i];
93
+ const absoluteIndex = startingIndex + i;
94
+ const contentHash = this.hashString(msg.content);
95
+ currentHash = this.hashString(`${absoluteIndex}${msg.role}${contentHash}${currentHash}`);
96
+ newTurns.push({
97
+ index: absoluteIndex,
98
+ role: msg.role,
99
+ contentHash,
100
+ turnHash: currentHash,
101
+ ingestedAt: Date.now(),
102
+ });
103
+ }
104
+ return {
105
+ sessionId: manifest.sessionId,
106
+ version: manifest.version + 1,
107
+ turns: [...manifest.turns, ...newTurns],
108
+ tailHash: currentHash,
109
+ };
110
+ }
111
+ /**
112
+ * Determines the absolute starting index for a set of new messages.
113
+ * If we have stored turns, the next message's index is last_turn.index + 1.
114
+ * If the manifest is empty, we infer from OpenClaw's prePromptMessageCount signal
115
+ * (caller must provide this as a hint when available).
116
+ */
117
+ deriveStartingIndex(manifest, prePromptMessageCountHint) {
118
+ if (manifest.turns.length > 0) {
119
+ return manifest.turns[manifest.turns.length - 1].index + 1;
120
+ }
121
+ // Empty manifest — use OpenClaw's signal if provided, else assume 0
122
+ return typeof prePromptMessageCountHint === "number" && prePromptMessageCountHint >= 0
123
+ ? prePromptMessageCountHint
124
+ : 0;
125
+ }
126
+ }
127
+ export const manifestStore = new TurnManifestStore();
@@ -74,18 +74,22 @@ function createMemorySearchManager(getClient, cfg, defaults, initialStatus) {
74
74
  const filteredResults = minScore === undefined
75
75
  ? result.results
76
76
  : result.results.filter((item) => item.score >= minScore);
77
- const legacyResults = filteredResults.map((item) => ({
78
- ...item,
79
- content: item.text,
80
- }));
77
+ const legacyResults = filteredResults.map((item) => {
78
+ const meta = parseMetadataJson(item);
79
+ return {
80
+ ...item,
81
+ content: item.text || (typeof meta.text === "string" ? meta.text : ""),
82
+ };
83
+ });
81
84
  if (legacyCall) {
82
85
  return { results: legacyResults };
83
86
  }
84
87
  const memoryResults = filteredResults.map((item) => {
85
88
  const meta = parseMetadataJson(item);
86
89
  const collection = typeof meta.collection === "string" ? meta.collection : "memory";
90
+ const effectiveText = item.text || (typeof meta.text === "string" ? meta.text : "") || "";
87
91
  const relPath = encodeSearchResultPath(collection, item.id);
88
- returnedSearchPaths.set(relPath, item.text);
92
+ returnedSearchPaths.set(relPath, effectiveText);
89
93
  return toMemorySearchResult(item);
90
94
  });
91
95
  return memoryResults;
@@ -194,12 +198,13 @@ function parseMetadataJson(item) {
194
198
  function toMemorySearchResult(item) {
195
199
  const meta = parseMetadataJson(item);
196
200
  const collection = typeof meta.collection === "string" ? meta.collection : "memory";
201
+ const effectiveText = item.text || (typeof meta.text === "string" ? meta.text : "") || "";
197
202
  return {
198
203
  path: encodeSearchResultPath(collection, item.id),
199
204
  startLine: 1,
200
- endLine: Math.max(1, item.text.split("\n").length),
205
+ endLine: Math.max(1, effectiveText.split("\n").length),
201
206
  score: item.score,
202
- snippet: item.text,
207
+ snippet: effectiveText,
203
208
  source: collection.startsWith("session:") || collection.startsWith("session_") ? "sessions" : "memory",
204
209
  citation: `${collection}:${item.id}`,
205
210
  };
@@ -1,7 +1,7 @@
1
1
  import { LibravDBClient } from "./libravdb-client.js";
2
2
  import type { LoggerLike, PluginConfig } from "./types.js";
3
3
  export type ClientGetter = () => Promise<LibravDBClient>;
4
- export declare const DEFAULT_RPC_TIMEOUT_MS = 30000;
4
+ export declare const DEFAULT_RPC_TIMEOUT_MS = 120000;
5
5
  export declare const STARTUP_HEALTH_TIMEOUT_MS = 2000;
6
6
  export declare const VALID_TLS_MODES: readonly ["auto", "tls", "insecure"];
7
7
  export type ValidTlsMode = typeof VALID_TLS_MODES[number];
@@ -1,13 +1,19 @@
1
1
  import { LibravDBClient, resolveClientEndpoint } from "./libravdb-client.js";
2
2
  import { formatError } from "./format-error.js";
3
+ import { resolveTenantKey } from "./identity.js";
3
4
  import { existsSync, statSync } from "node:fs";
4
5
  import path from "node:path";
5
- export const DEFAULT_RPC_TIMEOUT_MS = 30000;
6
+ export const DEFAULT_RPC_TIMEOUT_MS = 120_000;
6
7
  export const STARTUP_HEALTH_TIMEOUT_MS = 2000;
8
+ const ENV_RPC_TIMEOUT_MS = (() => {
9
+ const raw = Number(process.env.LIBRAVDB_RPC_TIMEOUT_MS);
10
+ return Number.isFinite(raw) && raw > 0 ? raw : 0;
11
+ })();
7
12
  export const VALID_TLS_MODES = ["auto", "tls", "insecure"];
8
13
  const isTlsModeValid = (m) => VALID_TLS_MODES.includes(m);
9
14
  export function resolveStartupHealthTimeoutMs(cfg) {
10
- return Math.max(STARTUP_HEALTH_TIMEOUT_MS, cfg.rpcTimeoutMs ?? DEFAULT_RPC_TIMEOUT_MS);
15
+ const timeout = cfg.rpcTimeoutMs ?? (ENV_RPC_TIMEOUT_MS || DEFAULT_RPC_TIMEOUT_MS);
16
+ return Math.max(STARTUP_HEALTH_TIMEOUT_MS, timeout);
11
17
  }
12
18
  export function daemonProvisioningHint() {
13
19
  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.";
@@ -48,11 +54,12 @@ export function createPluginRuntime(cfg, logger = console) {
48
54
  validateTlsConfig(cfg, logger);
49
55
  client = new LibravDBClient({
50
56
  endpoint: cfg.grpcEndpoint || cfg.sidecarPath,
51
- timeoutMs: cfg.rpcTimeoutMs ?? DEFAULT_RPC_TIMEOUT_MS,
57
+ timeoutMs: cfg.rpcTimeoutMs ?? (ENV_RPC_TIMEOUT_MS || DEFAULT_RPC_TIMEOUT_MS),
52
58
  tlsCaPath: cfg.grpcEndpointTlsCa,
53
59
  tlsMode: cfg.grpcEndpointTlsMode,
54
60
  tlsClientCertPath: cfg.grpcEndpointTlsClientCert,
55
61
  tlsClientKeyPath: cfg.grpcEndpointTlsClientKey,
62
+ tenantKey: resolveTenantKey(cfg),
56
63
  });
57
64
  await client.bootstrapHandshake();
58
65
  return client;
package/dist/types.d.ts CHANGED
@@ -2,6 +2,11 @@ export interface PluginConfig {
2
2
  dbPath?: string;
3
3
  /** Legacy fallback alias for grpcEndpoint. */
4
4
  sidecarPath?: string;
5
+ /** Stable tenant identifier for multi-agent deployments. When set, the daemon
6
+ * routes this plugin instance to an isolated vector database. When unset,
7
+ * the plugin falls back to the auto-derived userId. Set different values per
8
+ * agent to isolate memory storage. */
9
+ tenantId?: string;
5
10
  /** Stable identity for cross-session durable memory. When set, all sessions
6
11
  * share memories under user:{userId}. When unset, the plugin auto-derives
7
12
  * identity from the OS and persists it to the identity file. */
@@ -87,8 +87,8 @@ address, explicitly set `grpcEndpointTlsMode: "tls"` to match:
87
87
  |---|---|---|---|
88
88
  | `embeddingProfile` | string | `nomic-embed-text-v1.5` | Primary embedding model |
89
89
  | `fallbackProfile` | string | `bge-small-en-v1.5` | Fallback when primary model fails dimension checks |
90
- | `embeddingBackend` | string | — | `bundled`, `onnx-local`, `custom-local`, or `remote` |
91
- | `onnxDevice` | string | `auto` | ONNX execution provider: `auto`, `cpu`, `coreml` (macOS), `cuda` (Linux/Windows), `directml` (Windows), `openvino` (Linux) |
90
+ | `embeddingBackend` | string | — | `gguf` (recommended default), `bundled`, `onnx-local`, `custom-local`, or `remote` |
91
+ | `onnxDevice` | string | `cpu` | ONNX execution provider: `auto`, `cpu`, `coreml` (macOS), `cuda` (Linux/Windows), `directml` (Windows), `openvino` (Linux) |
92
92
  | `embeddingRuntimePath` | string | — | Path to ONNX runtime library visible to the vector service (maps to `LIBRAVDB_ONNX_RUNTIME`; required with `embeddingBackend: "onnx-local"`) |
93
93
  | `embeddingModelPath` | string | — | Path to the model directory containing `embedding.json`, `model.onnx`, and `tokenizer.json` (maps to `LIBRAVDB_EMBEDDING_MODEL`; required with `embeddingBackend: "onnx-local"`) |
94
94
  | `embeddingTokenizerPath` | string | — | Path to custom tokenizer file |
@@ -9,7 +9,8 @@ Default selection baseline:
9
9
 
10
10
  Why:
11
11
 
12
- - Nomic is the default because its Matryoshka-trained embeddings deliver significantly higher retrieval accuracy than MiniLM, with principled dimensionality tiering (`64d 256d 768d`) that lets the vector service trade memory for precision without re-embedding.
12
+ - GGUF is the recommended default and preferred backend for local embedding. It delivers Matryoshka-trained `nomic-embed-text-v1.5` embeddings with no ONNX Runtime dependency and hardware-native acceleration on Apple Silicon (Metal), NVIDIA (CUDA), and CPU.
13
+ - `bundled` uses the ONNX build of `nomic-embed-text-v1.5` and is the full-featured fallback when GGUF is unavailable.
13
14
  - bge-small-en-v1.5 is the fallback for resource-constrained systems and is automatically selected when the primary model's dimensions do not match the active collection.
14
15
  - Intel Macs without reliable Metal/MPS support should set `onnxDevice: "cpu"` to force CPU ONNX execution and bypass CoreML.
15
16
 
@@ -33,7 +34,7 @@ How it works:
33
34
  - `onnx-local` still requires local model assets through `embeddingModelPath`, typically a directory containing `embedding.json`.
34
35
  - The manifest may override or refine the profile, but explicit dimension mismatches fail closed.
35
36
  - The vector service store persists an embedding fingerprint, so reopening an existing store with a different effective model profile will fail instead of silently mixing vector spaces.
36
- - `onnxDevice` is passed through as `LIBRAVDB_ONNX_DEVICE` for vector service versions that support execution-provider selection (`auto`, `cpu`, `cuda`, `coreml`, `directml`, `openvino`).
37
+ - `onnxDevice` is passed through as `LIBRAVDB_ONNX_DEVICE` for vector service versions that support execution-provider selection (`auto`, `cpu` (default), `cuda`, `coreml`, `directml`, `openvino`).
37
38
 
38
39
  ## Store Compatibility and Upgrades
39
40
 
@@ -65,6 +66,7 @@ Do not delete the old store until the replacement has been verified.
65
66
 
66
67
  Recommended usage:
67
68
 
68
- - `bundled` for the shipped default path, which uses `nomic-embed-text-v1.5`.
69
+ - `gguf` for the recommended local embedding path, using `nomic-embed-text-v1.5` with hardware-native acceleration and no ONNX Runtime dependency.
70
+ - `bundled` for the ONNX build of `nomic-embed-text-v1.5` when GGUF is unavailable.
69
71
  - `onnx-local` plus `embeddingProfile` when a power user wants a known model family with local assets.
70
72
  - treat remote/Ollama providers as future separate backend types, not as overloads of `custom-local`.
@@ -231,12 +231,20 @@ libravdbd serve
231
231
 
232
232
  ### Deterministic fallback embeddings
233
233
 
234
- If vector service logs mention deterministic fallback mode, the vector service did not find the
235
- configured ONNX runtime or model manifest. Stop the vector service, set
236
- `LIBRAVDB_ONNX_RUNTIME` and `LIBRAVDB_EMBEDDING_MODEL`, confirm the model
237
- directory contains `embedding.json`, then restart. If a database was created
238
- while fallback mode was active, move that `.libravdb` file and its adjacent
239
- `.embedding.json` aside before starting with ONNX assets.
234
+ If the vector service falls back to deterministic embeddings, it could not locate
235
+ the configured ONNX runtime or model assets. The service will still run but all
236
+ embedding operations will fail until resolved.
237
+
238
+ To resolve: stop the vector service, then set `LIBRAVDB_ONNX_RUNTIME` to the
239
+ full path of the ONNX runtime library (e.g. `libonnxruntime.so` or
240
+ `libonnxruntime.dylib`) and `LIBRAVDB_EMBEDDING_MODEL` to the model directory
241
+ containing `embedding.json`. Verify the model directory also contains the
242
+ corresponding `.onnx` model file and `tokenizer.json`.
243
+
244
+ If a database was created while the service was in fallback mode, move that
245
+ `.libravdb` file and its adjacent `.embedding.json` aside before restarting —
246
+ a store initialized with deterministic embeddings is incompatible with ONNX-backed
247
+ operation.
240
248
 
241
249
  ### Incompatible database or embedding profile
242
250
 
@@ -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.6.25",
5
+ "version": "1.6.28",
6
6
  "kind": [
7
7
  "memory",
8
8
  "context-engine"
@@ -63,6 +63,10 @@
63
63
  "sidecarPath": {
64
64
  "type": "string"
65
65
  },
66
+ "tenantId": {
67
+ "type": "string",
68
+ "description": "Stable tenant identifier for multi-agent deployments. When set, the daemon routes this plugin instance to an isolated vector database. When unset, the plugin falls back to the auto-derived userId. Set different values per agent to isolate memory storage."
69
+ },
66
70
  "userId": {
67
71
  "type": "string",
68
72
  "description": "Stable identity for cross-session durable memory. When set, sessions share memories under user:{userId}."
@@ -440,7 +444,8 @@
440
444
  },
441
445
  "rpcTimeoutMs": {
442
446
  "type": "number",
443
- "default": 30000
447
+ "default": 120000,
448
+ "description": "gRPC timeout in milliseconds for daemon calls. Default: 120000 (2 min). Can also be set via LIBRAVDB_RPC_TIMEOUT_MS env var."
444
449
  },
445
450
  "maxRetries": {
446
451
  "type": "number"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xdarkicex/openclaw-memory-libravdb",
3
- "version": "1.6.25",
3
+ "version": "1.6.28",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -79,6 +79,6 @@
79
79
  "dependencies": {
80
80
  "@connectrpc/connect": "^1.7.0",
81
81
  "@connectrpc/connect-node": "^1.7.0",
82
- "@xdarkicex/libravdb-contracts": "^2.0.8"
82
+ "@xdarkicex/libravdb-contracts": "^2.0.12"
83
83
  }
84
84
  }