@xdarkicex/openclaw-memory-libravdb 1.6.25 → 1.6.27

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/README.md CHANGED
@@ -14,7 +14,7 @@
14
14
  `@xdarkicex/openclaw-memory-libravdb` is a local-first OpenClaw memory plugin
15
15
  backed by the `libravdbd` vector service. It replaces the lightweight default memory
16
16
  path with scoped session, user, and global memory; continuity-aware prompt
17
- assembly; durable recall; and daemon-owned compaction.
17
+ assembly; durable recall; and vector-service-owned compaction.
18
18
 
19
19
  [Install](./docs/install.md) · [Full installation reference](./docs/installation.md) · [Architecture](./docs/architecture.md) · [Security](./docs/security.md) · [Performance and tuning](./docs/performance-and-tuning.md) · [Contributing](./docs/contributing.md)
20
20
 
@@ -33,7 +33,7 @@ brew install libravdbd
33
33
  brew services start libravdbd
34
34
  ```
35
35
 
36
- > **After upgrades:** Always restart the daemon so the newly installed binary takes effect:
36
+ > **After upgrades:** Always restart the vector service so the newly installed binary takes effect:
37
37
  > ```bash
38
38
  > # macOS (Homebrew)
39
39
  > brew services restart libravdbd
@@ -69,25 +69,16 @@ systemctl --user enable --now libravdbd
69
69
  openclaw plugins install @xdarkicex/openclaw-memory-libravdb
70
70
  ```
71
71
 
72
- Then activate the plugin in `~/.openclaw/openclaw.json`:
72
+ This automatically configures `plugins.slots.memory` and `plugins.slots.contextEngine` to point to `libravdb-memory`, and sets up the plugin entry with defaults.
73
73
 
74
- ```json
75
- {
76
- "plugins": {
77
- "slots": {
78
- "memory": "libravdb-memory",
79
- "contextEngine": "libravdb-memory"
80
- },
81
- "entries": {
82
- "libravdb-memory": {
83
- "enabled": true,
84
- "config": {
85
- "sidecarPath": "auto"
86
- }
87
- }
88
- }
89
- }
90
- }
74
+ Then restart the gateway so the plugin loads:
75
+
76
+ ```bash
77
+ # macOS/Linux
78
+ openclaw daemon restart
79
+
80
+ # Verify the plugin is loaded
81
+ openclaw plugins list | grep libravdb
91
82
  ```
92
83
 
93
84
  Verify the service and plugin:
@@ -136,22 +127,33 @@ If your service runs elsewhere, set `sidecarPath`:
136
127
 
137
128
  ## Highlights
138
129
 
139
- - **Memory capability ownership** - owns the OpenClaw `memory` slot and
140
- registers the context engine capability at runtime.
141
- - **Memory runtime bridge** - routes built-in `memory_search` calls to the same
142
- libraVDB-backed daemon on hosts that expose the runtime API.
143
- - **Three memory scopes** - keeps active session, durable user, and global memory
144
- separate.
145
- - **Hybrid retrieval** - blends semantic similarity, scope, recency, and summary
146
- quality instead of relying on cosine similarity alone.
147
- - **Continuity-aware assembly** - preserves the recent working tail while fitting
148
- recalled memory into a bounded prompt budget.
149
- - **Sidecar compaction** - summarizes older session turns without flattening the
150
- newest working context.
151
- - **Local-first inference** - uses local embedding and compaction paths by
152
- default, with optional external summarizer configuration.
153
- - **Explicit service lifecycle** - the npm/OpenClaw package stays connect-only;
154
- `libravdbd` is installed and supervised separately.
130
+ - **Unified Cognitive Scoring** - moves beyond standard semantic search by mathematically blending cosine similarity with frequency, recency, authored salience, and cognitive authority composite weights (`ω(c)`).
131
+ - **Topological Causal Graphs** - internally models temporal memory chains via directed acyclic graphs (`WhyIDs`), injecting causal proximity into retrieval scoring to anticipate temporally connected context.
132
+ - **Zero-GC Slab Allocation** - manages model tensor and inference data via a custom contiguous slab allocator (`slabby`), completely bypassing Go garbage collection pauses during high-throughput memory assembly.
133
+ - **Deontic & Salience Retrieval** - applies structural authority weightings and deontic logic rules to ensure critical behavioral constraints mathematically outrank conversational chatter.
134
+ - **Matryoshka Representation Learning** - natively supports dynamically tiered embedding dimensions (e.g., slicing 768d vectors down to 64d) for ultra-fast cascading coarse search followed by precision reranking.
135
+ - **Cognitive Routing Circuit Breakers** - strictly monitors remote endpoint health via stateful circuit breakers that trip and safely auto-disable complex ML routing during downstream outages, preserving foundational search uptime.
136
+ - **Zero-ML Local Compaction** - evaluates contextual gating thresholds to execute purely localized session summarization and compaction cycles natively within the vector service.
137
+ - **True multi-tenancy** - routes multiple OpenClaw agents to strictly isolated, per-agent vector databases within a single lightweight vector service process.
138
+ - **Zero-copy caching** - shares a memory-mapped, cross-tenant embedding cache across all active agents to keep hardware utilization incredibly low.
139
+ - **Three memory scopes** - keeps active session, durable user, and global memory separate.
140
+ - **Local-first inference** - uses local embedding paths by default, with optional remote model configurations.
141
+ - **Operational tooling** - provides a dedicated CLI (`libravdbd status`, `migrate`, `tenant evict`) for live observability and safe health management without interrupting active sessions.
142
+ - **Explicit service lifecycle** - the npm/OpenClaw package stays connect-only; `libravdbd` is installed and supervised separately over a secure gRPC transport.
143
+
144
+ ## Embedding Backend Providers
145
+
146
+ The plugin supports multiple embedding backends. Set via `embeddingBackend` in plugin config:
147
+
148
+ | Backend | Description | Config required |
149
+ |---|---|---|
150
+ | `gguf` (recommended) | Hardware-native acceleration via llama.cpp. Apple Silicon gets Metal, NVIDIA gets CUDA, everything else falls back to CPU. No ONNX Runtime dependency. | None — just `embeddingBackend: "gguf"` |
151
+ | `bundled` | ONNX build of `nomic-embed-text-v1.5`. Full-featured fallback when GGUF is unavailable. | None |
152
+ | `onnx-local` | Custom ONNX model from local assets. Requires `embeddingModelPath` and `embeddingRuntimePath`. | `embeddingModelPath`, `embeddingRuntimePath` |
153
+ | `custom-local` | Custom ONNX variant with your own assets and runtime. | `embeddingModelPath`, `embeddingRuntimePath`, `embeddingProfile` |
154
+ | `remote` | HTTP API embedder (e.g. OpenAI-compatible). Requires `embeddingEndpoint` and `embeddingRemoteModel`. | `embeddingEndpoint`, `embeddingRemoteModel` |
155
+
156
+ GGUF is the recommended default. It delivers `nomic-embed-text-v1.5` embeddings with hardware-native acceleration and no ONNX Runtime dependency. See [Embedding profiles](./docs/embedding-profiles.md) for full details.
155
157
 
156
158
  ## Security Defaults
157
159
 
@@ -184,21 +186,200 @@ All keys are optional. For the full reference, see [Configuration](./docs/config
184
186
  | Key | Type | Default | |
185
187
  |---|---|---|---|
186
188
  | `sidecarPath` | string | `auto` | `"auto"` probes standard paths; set `unix:/path` or `tcp:host:port` to override |
189
+ | `embeddingBackend` | string | `gguf` | Embedding backend: `gguf` (recommended), `bundled`, `onnx-local`, `custom-local`, `remote` |
187
190
  | `embeddingProfile` | string | `nomic-embed-text-v1.5` | Primary embedding model |
188
191
  | `fallbackProfile` | string | `bge-small-en-v1.5` | Fallback profile for dimension mismatches |
189
192
  | `embeddingRuntimePath` | string | — | Required with `embeddingBackend: "onnx-local"`; path to `libonnxruntime` visible to `libravdbd` |
190
193
  | `embeddingModelPath` | string | — | Required with `embeddingBackend: "onnx-local"`; directory containing `embedding.json`, `model.onnx`, and `tokenizer.json` |
191
- | `onnxDevice` | string | `auto` | ONNX execution provider; set `cpu` to bypass CoreML/MPS on Intel Macs |
194
+ | `onnxDevice` | string | `cpu` | ONNX execution provider; `cpu` is the default; `auto` lets libravdbd auto-detect |
192
195
  | `userId` | string | auto-derived | Stable identity for cross-session durable memory |
196
+ | `tenantId` | string | auto-derived | Multi-tenant identifier. Resolved as `cfg.tenantId` > `LIBRAVDB_AGENT_ID` env > `userId`. Isolates the agent to a dedicated `.libravdb` file. |
193
197
  | `crossSessionRecall` | boolean | `true` | When `false`, only session-scoped memories are retrieved |
194
198
  | `compactSessionTokenBudget` | number | `2000` | Auto-compaction token threshold; `0` disables |
195
199
 
200
+ ## Multi-Tenant Support
201
+
202
+ `libravdbd` supports true multi-tenancy, allowing you to run multiple OpenClaw agents on the same machine with completely isolated vector databases. By default, the plugin connects to a single-tenant database named after your `userId`.
203
+
204
+ If you want to run multiple distinct agents (e.g., a "research-agent" and a "coding-agent"), you can assign each a unique `tenantId` in the OpenClaw configuration:
205
+
206
+ ```json
207
+ {
208
+ "plugins": {
209
+ "entries": {
210
+ "libravdb-memory": {
211
+ "enabled": true,
212
+ "config": {
213
+ "tenantId": "research-agent"
214
+ }
215
+ }
216
+ }
217
+ }
218
+ }
219
+ ```
220
+
221
+ The vector service will seamlessly route the agent's requests to a dedicated, isolated vector database file. It manages all tenant instances efficiently within a single process and automatically shares a centralized, memory-mapped embedding cache to keep hardware usage incredibly low.
222
+
223
+ ### Directory Structure
224
+
225
+ When running in multi-tenant mode, the vector service automatically scaffolds an isolated directory structure inside your configured `agent_db_root` (or the default profile directory). It scopes databases to the specific embedding model in use:
226
+
227
+ ```text
228
+ ~/.libravdbd/data_nomic-embed-text-v1_5/
229
+ ├── _internal:dedupe.libravdb # Cross-session deduplication state
230
+ ├── _internal:registry.libravdb # Tenant registry and health logs
231
+ └── agents/
232
+ ├── research-agent.libravdb # Isolated database for research-agent
233
+ ├── coding-agent.libravdb # Isolated database for coding-agent
234
+ └── my-default-user.libravdb # Isolated database for default user
235
+ ```
236
+
237
+ ### Multi-Tenant Operations
238
+
239
+ The vector service exposes tenant-aware operational commands:
240
+
241
+ ```bash
242
+ # View global vector service health, cache stats, and all active tenant footprints
243
+ libravdbd status
244
+
245
+ # Evict a specific tenant from memory without shutting down the vector service
246
+ libravdbd tenant evict <tenantId>
247
+
248
+ # Safely migrate an old single-tenant DB to a named tenant
249
+ libravdbd migrate --from ~/.libravdbd/data.libravdb --tenant <tenantId>
250
+ ```
251
+
252
+ ## Vector Service Configuration (YAML) & Kubernetes
253
+
254
+ `libravdbd` is heavily configurable via environment variables or a YAML configuration file. The vector service looks for `config.yaml` in this order:
255
+ 1. `LIBRAVDB_CONFIG=/path/to/config.yaml`
256
+ 2. `/etc/libravdbd/config.yaml`
257
+ 3. `~/.libravdbd/config.yaml`
258
+
259
+ Example `config.yaml` for a Kubernetes StatefulSet deployment in multi-tenant mode:
260
+
261
+ ```yaml
262
+ # /etc/libravdbd/config.yaml
263
+ agent_db_root: "/var/lib/libravdbd/agents"
264
+ tenant_mode: "auto"
265
+ tenant_max_open: 128
266
+ grpc_endpoint: "tcp:0.0.0.0:9090"
267
+ embedding_backend: "gguf"
268
+ embedding_profile: "nomic-embed-text-v1.5"
269
+ drain_timeout: "25s" # Must be less than k8s terminationGracePeriodSeconds
270
+ ```
271
+
272
+ ## Securing gRPC with mTLS
273
+
274
+ For distributed deployments where `libravdbd` and OpenClaw run on different machines, you must secure the TCP transport using Mutual TLS (mTLS).
275
+
276
+ **1. Generate Local Certificates:**
277
+ ```bash
278
+ # 1. Generate Certificate Authority (CA)
279
+ openssl req -x509 -newkey rsa:4096 -days 3650 -nodes -keyout ca.key -out ca.crt -subj "/CN=LibraVDB-CA"
280
+
281
+ # 2. Generate Vector Service Server Certificate
282
+ openssl req -newkey rsa:2048 -nodes -keyout server.key -out server.csr -subj "/CN=libravdbd.local"
283
+ openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 365
284
+
285
+ # 3. Generate Client Certificate (For OpenClaw plugins)
286
+ openssl req -newkey rsa:2048 -nodes -keyout client.key -out client.csr -subj "/CN=openclaw-client"
287
+ openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 365
288
+ ```
289
+
290
+ **2. Configure the Vector Service:**
291
+ Add the generated TLS paths to your vector service's `config.yaml`:
292
+ ```yaml
293
+ grpc_endpoint: "tcp:0.0.0.0:9090"
294
+ grpc_tls_cert: "/etc/libravdbd/certs/server.crt"
295
+ grpc_tls_key: "/etc/libravdbd/certs/server.key"
296
+ grpc_tls_ca: "/etc/libravdbd/certs/ca.crt" # Enforces mTLS client verification
297
+ ```
298
+
299
+ **3. Connect Your Client:**
300
+ Add the TLS client certificate paths to your OpenClaw plugin config in `openclaw.json`:
301
+
302
+ ```json
303
+ {
304
+ "plugins": {
305
+ "entries": {
306
+ "libravdb-memory": {
307
+ "config": {
308
+ "grpcEndpoint": "tcp:libravdbd.local:9090",
309
+ "grpcEndpointTlsMode": "tls",
310
+ "grpcEndpointTlsClientCert": "/etc/libravdbd/certs/client.crt",
311
+ "grpcEndpointTlsClientKey": "/etc/libravdbd/certs/client.key",
312
+ "grpcEndpointTlsCa": "/etc/libravdbd/certs/ca.crt"
313
+ }
314
+ }
315
+ }
316
+ }
317
+ }
318
+ ```
319
+
320
+ The `grpcEndpointTlsCa` field is required for mTLS; the plugin will verify the server certificate against this CA. When `grpcEndpointTlsMode` is `"tls"`, plaintext and unauthenticated connections are rejected.
321
+
196
322
  ## Optional Features
197
323
 
198
324
  - **Markdown ingestion** watches OpenClaw-owned markdown roots or Obsidian vaults
199
325
  and syncs eligible notes into memory. See [Features](./docs/features.md).
200
326
  - **Dream promotion** promotes vetted dream diary bullets into an isolated
201
327
  `dream:{userId}` collection. See [Features](./docs/features.md).
328
+
329
+ ### Dream Promotion
330
+
331
+ OpenClaw's dreaming cron writes AI-generated memory reflections to a dream diary
332
+ markdown file. The plugin can watch this file and automatically promote vetted
333
+ entries into the `dream:{userId}` durable collection managed by the vector service.
334
+
335
+ Enable by adding these config keys:
336
+
337
+ ```json
338
+ {
339
+ "plugins": {
340
+ "entries": {
341
+ "libravdb-memory": {
342
+ "config": {
343
+ "dreamPromotionEnabled": true,
344
+ "dreamPromotionUserId": "<your-user-id>",
345
+ "dreamPromotionDiaryPath": "~/DREAMS.md"
346
+ }
347
+ }
348
+ }
349
+ }
350
+ }
351
+ ```
352
+
353
+ | Key | Type | Required | Description |
354
+ |---|---|---|---|
355
+ | `dreamPromotionEnabled` | boolean | yes | Enable the dream diary file watcher |
356
+ | `dreamPromotionUserId` | string | yes | User ID whose `dream:` collection receives promoted entries |
357
+ | `dreamPromotionDiaryPath` | string | yes | Path to the dream diary markdown file (supports `~`) |
358
+ | `dreamPromotionDebounceMs` | number | no | Debounce delay before scanning after a change (default: `150`) |
359
+
360
+ The diary file is standard markdown. Entries under a `## Deep Sleep` or
361
+ `## Dream Promotion` heading are parsed as bullet points with trailing metadata:
362
+
363
+ ```markdown
364
+ ## Deep Sleep
365
+ - A key insight about the user's workflow patterns {score=0.85, recall=4, unique=3}
366
+ - Another consolidated observation {score=0.72, recall=2, unique=2}
367
+
368
+ <!-- or equivalently: -->
369
+
370
+ ## Dream Promotion
371
+ - A key insight about the user's workflow patterns {score=0.85, recall=4, unique=3}
372
+ - Another consolidated observation {score=0.72, recall=2, unique=2}
373
+ ```
374
+
375
+ Entries are promoted to `dream:<userId>` and surfaced when the user asks
376
+ dream-related questions (e.g. "what did I dream about?").
377
+
378
+ You can also promote manually without enabling the watcher:
379
+
380
+ ```bash
381
+ openclaw memory dream-promote --user-id <userId> --dream-file ~/DREAMS.md
382
+ ```
202
383
  - **Embedding profiles** default to `nomic-embed-text-v1.5` with `bge-small-en-v1.5`
203
384
  fallback. See [Embedding profiles](./docs/embedding-profiles.md).
204
385
 
@@ -1,5 +1,7 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { resolveIdentity } from "./identity.js";
2
3
  import { resolveUserCollection } from "./memory-scopes.js";
4
+ import { manifestStore } from "./manifest.js";
3
5
  const APPROX_CHARS_PER_TOKEN = 4;
4
6
  const PROMPT_AUTHORITY_PREASSEMBLY_MAY_OVERFLOW = "preassembly_may_overflow";
5
7
  const ASSEMBLE_BUDGET_HEADROOM_TOKENS = 256;
@@ -377,7 +379,7 @@ export function normalizeKernelMessage(message) {
377
379
  return {
378
380
  role: message.role,
379
381
  content: normalizeKernelContent(message.content),
380
- ...(typeof message.id === "string" ? { id: message.id } : {}),
382
+ id: message.id || randomUUID(),
381
383
  };
382
384
  }
383
385
  /**
@@ -471,9 +473,12 @@ function escapeMemoryFactText(text) {
471
473
  .replaceAll("\t", "&#9;");
472
474
  }
473
475
  // Tool-call pattern detection for sanitization
474
- const TOOL_CALL_BRACKET_RE = /\[tool:([^\]]+)\]/gi;
475
- const TOOL_CALL_JSON_RE = /\{\s*"name"\s*:\s*"([^"]+)"[^}]*\}/g;
476
- const TOOL_RESULT_ANNOTATION_RE = /\[tool:[^\]]+\](?:\s*[^{\[]*)?/g;
476
+ // Matches [tool:name] followed by optional whitespace and any trailing JSON object {...}, array [...], or string "..."
477
+ const TOOL_CALL_BRACKET_RE = /\[tool:([^\]]+)\](?:\s*(?:\{[\s\S]*?\}|\[[\s\S]*?\]|".*?"))?/gi;
478
+ // Matches raw JSON tool-call objects targeting a "name\" field
479
+ const TOOL_CALL_JSON_RE = /\{\s*"name"\s*:\s*"([^"]+)"[\s\S]*?\}/g;
480
+ // Matches older annotations, aggressively consuming trailing characters on the same line
481
+ const TOOL_RESULT_ANNOTATION_RE = /\[tool:[^\]]+\][^\n]*/g;
477
482
  /**
478
483
  * Sanitizes text that may contain tool-call syntax to prevent loop-priming.
479
484
  * Replaces executable-looking patterns with neutral summaries rather than
@@ -697,9 +702,10 @@ export function normalizeAssembleResult(result, sourceMessages) {
697
702
  isRealTranscript = message.role === "user" || message.role === "assistant";
698
703
  }
699
704
  if (isRealTranscript) {
705
+ // BUG PATH A SEALED: Sanitize the content before pushing to the trajectory
700
706
  messages.push({
701
707
  role: message.role === "user" ? "user" : "assistant",
702
- content,
708
+ content: sanitizeToolCallPatterns(content),
703
709
  ...(typeof message.id === "string" ? { id: message.id } : {}),
704
710
  });
705
711
  }
@@ -724,6 +730,20 @@ export function normalizeAssembleResult(result, sourceMessages) {
724
730
  ...(result.debug != null ? { debug: result.debug } : {}),
725
731
  };
726
732
  }
733
+ function extractCursorFromResult(result) {
734
+ if (result && typeof result === "object" && "cursor" in result) {
735
+ const cursor = result.cursor;
736
+ if (cursor && typeof cursor === "object") {
737
+ const c = cursor;
738
+ if (typeof c.lastProcessedIndex === "number" &&
739
+ typeof c.sessionVersion === "number" &&
740
+ typeof c.manifestTailHash === "string") {
741
+ return c;
742
+ }
743
+ }
744
+ }
745
+ return undefined;
746
+ }
727
747
  /**
728
748
  * Builds the context engine factory with the given client getter.
729
749
  */
@@ -1140,12 +1160,24 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
1140
1160
  userIdOverride: args.userId,
1141
1161
  sessionKey: args.sessionKey,
1142
1162
  });
1163
+ // Load manifest and normalize messages in parallel
1164
+ const manifest = manifestStore.load(sessionId, logger);
1143
1165
  const afterTurnMessages = selectAfterTurnMessages(args.messages, args.prePromptMessageCount, logger);
1144
1166
  const messages = normalizeKernelMessages(afterTurnMessages);
1145
- const ingestMessages = boundAfterTurnMessagesForIngest(messages, logger, sessionId);
1146
- const msgCount = messages.length;
1167
+ // Find overlap: messages already in our manifest
1168
+ const overlapIndex = manifestStore.findOverlapIndex(manifest, messages);
1169
+ const newMessages = messages.slice(overlapIndex);
1170
+ // Apply token budget cap only to new messages
1171
+ const ingestMessages = boundAfterTurnMessagesForIngest(newMessages, logger, sessionId);
1172
+ const startIndex = manifestStore.deriveStartingIndex(manifest, args.prePromptMessageCount);
1173
+ const cursor = {
1174
+ lastProcessedIndex: startIndex > 0 ? startIndex - 1 : 0,
1175
+ sessionVersion: manifest.version,
1176
+ manifestTailHash: manifest.tailHash,
1177
+ };
1147
1178
  logger.info?.(`LibraVDB afterTurn sessionId=${sessionId} userId=${userId} ` +
1148
- `messageCount=${msgCount} totalMessages=${args.messages.length} ` +
1179
+ `messageCount=${messages.length} newMessages=${newMessages.length} ` +
1180
+ `overlapIndex=${overlapIndex} startIndex=${startIndex} ` +
1149
1181
  `prePromptMessageCount=${args.prePromptMessageCount ?? "unknown"} ` +
1150
1182
  `heartbeat=${args.isHeartbeat ?? false}`);
1151
1183
  try {
@@ -1158,8 +1190,40 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
1158
1190
  sessionKey: args.sessionKey,
1159
1191
  userId,
1160
1192
  messages: ingestMessages,
1193
+ prePromptMessageCount: args.prePromptMessageCount,
1161
1194
  isHeartbeat: args.isHeartbeat,
1195
+ cursor,
1162
1196
  });
1197
+ // Reconcile manifest with daemon-confirmed cursor.
1198
+ // The daemon returns a cursor even when it ingests zero messages
1199
+ // (e.g. gap detected, all messages deduped). Trust its
1200
+ // lastProcessedIndex over our optimistic startIndex math.
1201
+ const daemonCursor = extractCursorFromResult(result);
1202
+ if (daemonCursor) {
1203
+ if (!daemonCursor.manifestTailHash) {
1204
+ // Daemon detected a gap: its DB is behind our manifest.
1205
+ // It did NOT ingest our messages. Reset the manifest so the
1206
+ // next turn does a full re-sync.
1207
+ logger.warn?.(`[LibraVDB] Daemon reported cursor gap for session ${sessionId}. ` +
1208
+ `Resetting manifest for full re-sync next turn.`);
1209
+ manifestStore.save(manifestStore.createEmpty(sessionId));
1210
+ }
1211
+ else if (ingestMessages.length > 0) {
1212
+ // Normal path: reconcile to what the daemon actually confirmed.
1213
+ const confirmedIndex = daemonCursor.lastProcessedIndex;
1214
+ const ackCount = Math.max(0, confirmedIndex - startIndex + 1);
1215
+ if (ackCount > 0) {
1216
+ const ackedMessages = ingestMessages.slice(0, ackCount);
1217
+ const updatedManifest = manifestStore.appendACKedMessages(manifest, ackedMessages, startIndex);
1218
+ manifestStore.save(updatedManifest);
1219
+ }
1220
+ }
1221
+ }
1222
+ else if (ingestMessages.length > 0) {
1223
+ // Legacy daemon (no cursor in response): optimistic ACK.
1224
+ const updatedManifest = manifestStore.appendACKedMessages(manifest, ingestMessages, startIndex);
1225
+ manifestStore.save(updatedManifest);
1226
+ }
1163
1227
  await performAfterTurnPredictiveCompaction({
1164
1228
  sessionId,
1165
1229
  messages,
@@ -1,4 +1,4 @@
1
- import type { LoggerLike } from "./types.js";
1
+ import type { LoggerLike, PluginConfig } from "./types.js";
2
2
  export type IdentitySource = "config" | "file" | "auto" | "session-key" | "default";
3
3
  export type ResolvedIdentity = {
4
4
  userId: string;
@@ -13,3 +13,12 @@ export declare function resolveIdentity(params: {
13
13
  * read-only commands (e.g. status --deep) that should not mutate disk. */
14
14
  noAutoPersist?: boolean;
15
15
  }): ResolvedIdentity;
16
+ /**
17
+ * Resolves a stable tenant key for multi-agent DB routing.
18
+ *
19
+ * Priority chain:
20
+ * 1. cfg.tenantId (explicit config, highest priority)
21
+ * 2. LIBRAVDB_AGENT_ID env var (container/CI override)
22
+ * 3. Fall back to resolved userId (existing identity system)
23
+ */
24
+ export declare function resolveTenantKey(cfg: PluginConfig): string;
package/dist/identity.js CHANGED
@@ -118,3 +118,23 @@ export function resolveIdentity(params) {
118
118
  }
119
119
  return { userId: autoId, source: "auto" };
120
120
  }
121
+ /**
122
+ * Resolves a stable tenant key for multi-agent DB routing.
123
+ *
124
+ * Priority chain:
125
+ * 1. cfg.tenantId (explicit config, highest priority)
126
+ * 2. LIBRAVDB_AGENT_ID env var (container/CI override)
127
+ * 3. Fall back to resolved userId (existing identity system)
128
+ */
129
+ export function resolveTenantKey(cfg) {
130
+ const explicit = cfg.tenantId?.trim();
131
+ if (explicit)
132
+ return explicit;
133
+ const envId = process.env.LIBRAVDB_AGENT_ID?.trim();
134
+ if (envId)
135
+ return envId;
136
+ return resolveIdentity({
137
+ configUserId: cfg.userId,
138
+ identityPath: cfg.identityPath,
139
+ }).userId;
140
+ }