@xdarkicex/openclaw-memory-libravdb 1.10.10 → 1.10.14-beta.1

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
@@ -1,21 +1,69 @@
1
- # ♎ LibraVDB - Memory and Context Management
1
+ # ♎ LibraVDB Cognitive Memory Engine for OpenClaw
2
2
 
3
3
  <div align="center">
4
- <img src="./docs/assets/libravdb-logo.svg" alt="LibraVDB" width="640">
4
+ <img src="./docs/assets/libravdb-logo.svg" alt="LibraVDB" width="480">
5
+
6
+ <p><strong>Local-first memory kernel with causal graph reasoning, identity tracking, and hard PII enforcement.</strong><br/>
7
+ <em>768-dim vector search • 6-tier cognitive classification • Zero-cloud guarantee</em></p>
8
+
9
+ <p>
10
+ <a href="https://www.npmjs.com/package/@xdarkicex/openclaw-memory-libravdb"><img src="https://img.shields.io/npm/v/%40xdarkicex%2Fopenclaw-memory-libravdb?label=release&color=5B21B6" alt="npm"></a>
11
+ <a href="https://github.com/xDarkicex/openclaw-memory-libravdb"><img src="https://img.shields.io/github/stars/xDarkicex/openclaw-memory-libravdb?style=social" alt="stars"></a>
12
+ <a href="https://discord.gg/DWn4BpRQAS"><img src="https://img.shields.io/badge/Discord-LibraVDB-5865F2?logo=discord&logoColor=white" alt="Discord"></a>
13
+ <a href="./LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License"></a>
14
+ <br>
15
+ <a href="https://github.com/xDarkicex/libravdbd"><img src="https://img.shields.io/badge/daemon-v1.9.3-00ADD8?logo=go&logoColor=white" alt="Daemon"></a>
16
+ <img src="https://img.shields.io/badge/TypeScript-5.x-3178C6?logo=typescript&logoColor=white" alt="TS">
17
+ <img src="https://img.shields.io/badge/OpenClaw-2026.3%2B-111827" alt="OC">
18
+ <img src="https://img.shields.io/badge/K8s-ready-326CE5?logo=kubernetes&logoColor=white" alt="k8s">
19
+ </p>
5
20
  </div>
6
21
 
7
- <div align="center">
8
- <a href="https://github.com/xDarkicex/libravdbd"><img src="https://img.shields.io/badge/Go-1.25%2B-00ADD8?logo=go&logoColor=white" alt="Go 1.25+"></a>
9
- <a href="./package.json"><img src="https://img.shields.io/badge/TypeScript-5.x-3178C6?logo=typescript&logoColor=white" alt="TypeScript 5.x"></a>
10
- <a href="./openclaw.plugin.json"><img src="https://img.shields.io/badge/OpenClaw-memory%20plugin-111827" alt="OpenClaw memory plugin"></a>
11
- <a href="https://www.npmjs.com/package/@xdarkicex/openclaw-memory-libravdb"><img src="https://img.shields.io/npm/v/%40xdarkicex%2Fopenclaw-memory-libravdb?label=release&color=5B21B6" alt="Release"></a>
12
- <a href="https://discord.gg/DWn4BpRQAS"><img src="https://img.shields.io/badge/Discord-LibraVDB-5865F2?logo=discord&logoColor=white" alt="LibraVDB Discord"></a>
13
- </div>
22
+ ```mermaid
23
+ graph TB
24
+ OC[OpenClaw Agent] -->|gRPC| PLUGIN[Plugin: context-engine + tools]
25
+ PLUGIN -->|gRPC + mTLS| DAEMON[libravdbd Memory Kernel]
26
+ DAEMON --> EMBED[GGUF / ONNX / Remote]
27
+ DAEMON --> COG[Causal Graph: BOSS + TE/LK + SAT]
28
+ EMBED --> DB[(Vector DB)]
29
+ COG --> GRAPH[(Causal DAG)]
30
+ PLUGIN --> AGENT[LLM Agent]
31
+ ```
32
+
33
+ ## 🚀 Quick Start
14
34
 
15
- `@xdarkicex/openclaw-memory-libravdb` is a local-first OpenClaw memory plugin
16
- backed by the `libravdbd` memory kernel. It replaces the lightweight default memory
17
- path with scoped session, user, and global memory; continuity-aware prompt
18
- assembly; durable recall; and kernel-owned compaction.
35
+ ```bash
36
+ # 1. Install daemon (pick your OS)
37
+ brew tap xDarkicex/homebrew-openclaw-libravdb-memory && brew install libravdbd && brew services start libravdbd # macOS
38
+ # apt install libravdbd # Linux
39
+ # See Arch Linux instructions below # Arch (AUR postponed)
40
+
41
+ # 2. Install plugin
42
+ openclaw plugins install @xdarkicex/openclaw-memory-libravdb
43
+
44
+ # 3. Verify
45
+ openclaw memory status
46
+ ```
47
+
48
+ > ⚡ **Done.** Your agent has persistent vector memory, identity tracking, and causal graph reasoning.
49
+
50
+ <details>
51
+ <summary>🐳 Docker (30 seconds) · ☸️ Kubernetes</summary>
52
+
53
+ ```bash
54
+ # Docker
55
+ cd deploy && docker compose up -d
56
+
57
+ # K8s with Helm
58
+ helm install libravdbd ./deploy/helm/libravdbd
59
+
60
+ # K8s with mTLS
61
+ helm install libravdbd ./deploy/helm/libravdbd \
62
+ --set tls.enabled=true --set tls.cert=$(base64 -i server.crt) --set tls.key=$(base64 -i server.key)
63
+ ```
64
+
65
+ Terraform modules for EKS/GKE/AKS: [`deploy/terraform/`](deploy/terraform/)
66
+ </details>
19
67
 
20
68
  [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)
21
69
 
@@ -57,10 +105,18 @@ sudo apt install libravdbd
57
105
  systemctl --user enable --now libravdbd
58
106
  ```
59
107
 
60
- **Linux (AUR)**
108
+ **Linux (Arch — custom pacman repo)**
109
+
110
+ AUR distribution is temporarily postponed due to the June 2026 AUR supply chain incident (new account registrations frozen). Use the official pacman repository instead:
61
111
 
62
112
  ```bash
63
- yay -S libravdbd-bin
113
+ # Add the repo to /etc/pacman.conf:
114
+ # [libravdbd]
115
+ # SigLevel = Optional
116
+ # Server = https://raw.githubusercontent.com/xDarkicex/aur-libravdbd-bin/main/$arch
117
+
118
+ echo -e '\n[libravdbd]\nSigLevel = Optional\nServer = https://raw.githubusercontent.com/xDarkicex/aur-libravdbd-bin/main/$arch' | sudo tee -a /etc/pacman.conf
119
+ sudo pacman -Sy libravdbd-bin
64
120
  systemctl --user enable --now libravdbd
65
121
  ```
66
122
 
@@ -142,6 +198,41 @@ If your service runs elsewhere, set `sidecarPath`:
142
198
  }
143
199
  ```
144
200
 
201
+ ## Docker & Kubernetes Deployment
202
+
203
+ The daemon is K8-ready. Deployment files live in [`deploy/`](deploy/):
204
+
205
+ | File | Use |
206
+ |------|-----|
207
+ | [`Dockerfile`](deploy/Dockerfile) | Production image (Alpine) — pulls the binary from GitHub releases |
208
+ | [`docker-compose.yml`](deploy/docker-compose.yml) | Local dev / single-server: `docker compose up -d` |
209
+ | [`helm/libravdbd/`](deploy/helm/libravdbd) | Full Helm chart — ConfigMap, PVC, Service, Deployment, mTLS support |
210
+
211
+ ```bash
212
+ # Quick start with Docker
213
+ cd deploy && docker compose up -d
214
+
215
+ # K8s with Helm
216
+ helm install libravdbd ./deploy/helm/libravdbd
217
+
218
+ # K8s with mTLS
219
+ helm install libravdbd ./deploy/helm/libravdbd \
220
+ --set tls.enabled=true \
221
+ --set tls.cert=$(base64 -i server.crt) \
222
+ --set tls.key=$(base64 -i server.key)
223
+ ```
224
+
225
+ The image pulls pre-built binaries from [GitHub Releases](https://github.com/zephyr-systems/libravdbd/releases) — no build step needed. Supports `linux/amd64` and `linux/arm64`.
226
+
227
+ **Terraform** modules for cloud deployment live in [`deploy/terraform/`](deploy/terraform/):
228
+
229
+ | Provider | Directory | Use |
230
+ |----------|-----------|-----|
231
+ | AWS EKS | [`eks/`](deploy/terraform/eks) | `terraform apply -var cluster_name=my-cluster` |
232
+ | GCP GKE | [`gke/`](deploy/terraform/gke) | `terraform apply -var project=my-project -var cluster_name=my-cluster` |
233
+ | Azure AKS | [`aks/`](deploy/terraform/aks) | `terraform apply -var resource_group=my-rg -var cluster_name=my-cluster` |
234
+ | Standalone VM | [`vm/`](deploy/terraform/vm) | EC2 + Docker, no k8s required |
235
+
145
236
  ## Highlights
146
237
 
147
238
  ### Why LibraVDB over other memory plugins
@@ -181,6 +272,51 @@ The daemon tracks **who you are** — not just what you said. Every speaker the
181
272
  - Cards participate in the causal graph (`memory_kind: "identity"`) — identity patterns detected by the cognitive scheduler at macro scale
182
273
  - `PredictiveContext` seeds the card node — BFS surfaces causally connected memories alongside identity
183
274
 
275
+ ### Bot Persona
276
+
277
+ Define who the bot IS — personality, tone, boundaries, and behavior. The persona
278
+ is stored as a user card (`__bot_persona__`) and injected at the top of every
279
+ session as `<bot_persona>`.
280
+
281
+ **Agent tools:**
282
+ - `set_persona(persona)` — define or update the bot's personality. Prose format.
283
+ Empty string deletes the persona.
284
+ - `get_persona()` — read the current persona.
285
+
286
+ **Example personas:**
287
+
288
+ ```prose
289
+ # Professional
290
+ You are a senior software architect. You speak precisely, use correct
291
+ terminology, and default to TypeScript patterns. You never apologize for
292
+ being thorough. When asked a question, you give the answer plus the
293
+ reasoning behind it.
294
+ ```
295
+
296
+ ```prose
297
+ # Creative
298
+ You are a playful creative assistant. You speak casually, use emoji freely,
299
+ and default to brainstorming mode. You always offer 3-5 wild ideas before
300
+ narrowing down. You never say "I can't" — you say "here's a different approach."
301
+ ```
302
+
303
+ ```prose
304
+ # Minimalist
305
+ You are a terse, no-nonsense assistant. You answer in as few words as
306
+ possible. No greetings, no sign-offs, no fluff. Just the answer.
307
+ If you don't know, you say "I don't know" — nothing else.
308
+ ```
309
+
310
+ ```prose
311
+ # Character
312
+ You are a 1920s detective. You call everyone "pal" or "doll." You use
313
+ noir slang and describe things like a hardboiled novel. You're cynical
314
+ but secretly have a heart of gold.
315
+ ```
316
+
317
+ The persona is injected **before** the user card and rules at session start,
318
+ so it acts as the foundational identity layer for the agent.
319
+
184
320
  ### PII Scrubbing & Hard Constraint Rules
185
321
 
186
322
  Agent replies are scanned for forbidden keywords before dispatch. Rules are stored
@@ -289,6 +425,13 @@ libravdbd search --tenant <key> --session <id> -k 10 "query"
289
425
  # Tenant management
290
426
  libravdbd tenant evict <key> # force-close a tenant DB
291
427
  libravdbd migrate # run pending DB migrations
428
+
429
+ # Database export (libravdbd v1.10.0+)
430
+ libravdbd export # export all collections as JSON to stdout
431
+ libravdbd export --tenant <key> # export a specific tenant's data
432
+ libravdbd export --collection <name># export a single collection
433
+ libravdbd export --format jsonl # newline-delimited JSON (one record per line)
434
+ libravdbd export --output out.json # write to file instead of stdout
292
435
  ```
293
436
 
294
437
  Use [Install](./docs/install.md) for service lifecycle commands and
@@ -611,3 +754,11 @@ service checkout. For the full source workflow, read [Development](./docs/develo
611
754
  - default macOS/Linux endpoint: `unix:$HOME/.libravdbd/run/libravdb.sock`
612
755
  - default Windows endpoint: `tcp:127.0.0.1:37421`
613
756
 
757
+
758
+ ---
759
+
760
+ <div align="center">
761
+ <sub>Built with ♎ by <a href="https://github.com/xDarkicex">xDarkicex</a> — local-first, privacy-first, forever free.</sub>
762
+
763
+ [![Star History Chart](https://api.star-history.com/svg?repos=xDarkicex/openclaw-memory-libravdb&type=Date)](https://star-history.com/#xDarkicex/openclaw-memory-libravdb&Date)
764
+ </div>
@@ -12,7 +12,7 @@ type OpenClawCompatibleMessage = {
12
12
  id?: string;
13
13
  [key: string]: unknown;
14
14
  };
15
- type OpenClawCompatiblePromptAuthority = "preassembly_may_overflow";
15
+ type OpenClawCompatiblePromptAuthority = "assembled" | "preassembly_may_overflow";
16
16
  type OpenClawCompatibleAssembleResult = {
17
17
  messages: OpenClawCompatibleMessage[];
18
18
  estimatedTokens: number;
@@ -31,7 +31,6 @@ type OpenClawCompatibleCompactResult = {
31
31
  reason?: string;
32
32
  result?: {
33
33
  summary?: string;
34
- summaryText?: string;
35
34
  firstKeptEntryId?: string;
36
35
  tokensBefore: number;
37
36
  tokensAfter?: number;
@@ -83,7 +82,7 @@ export declare function normalizeAssembleResult(result: {
83
82
  estimatedTokens?: number;
84
83
  systemPromptAddition?: string;
85
84
  debug?: AssembleContextInternalResponse["debug"];
86
- }, sourceMessages?: OpenClawCompatibleMessage[]): OpenClawCompatibleAssembleResult;
85
+ }, sourceMessages?: OpenClawCompatibleMessage[], promptAuthority?: OpenClawCompatiblePromptAuthority): OpenClawCompatibleAssembleResult;
87
86
  export declare function setSessionTrigger(sessionId: string, trigger: string | undefined): void;
88
87
  export declare function clearSessionTrigger(sessionId: string): void;
89
88
  export declare function consumeSubagentBudget(sessionKey: string, tokens: number): number;
@@ -107,7 +107,7 @@ function normalizeCompactResult(response, options = {}) {
107
107
  const threshold = options.threshold;
108
108
  const overBudget = threshold != null && tokensBefore >= threshold;
109
109
  const engineRefused = !didCompact && overBudget;
110
- const tokensAfter = didCompact && typeof response?.tokensAfter === "number" && response.tokensAfter > 0
110
+ const tokensAfter = didCompact && typeof response?.tokensAfter === "number" && response.tokensAfter >= 0
111
111
  ? response.tokensAfter
112
112
  : undefined;
113
113
  return {
@@ -117,8 +117,7 @@ function normalizeCompactResult(response, options = {}) {
117
117
  result: {
118
118
  tokensBefore,
119
119
  ...(tokensAfter != null ? { tokensAfter } : {}),
120
- ...(details.summaryMethod ? { summary: details.summaryMethod } : {}),
121
- ...(details.summaryText ? { summaryText: details.summaryText } : {}),
120
+ ...(details.summaryText ? { summary: details.summaryText } : {}),
122
121
  details: { ...details, ...(threshold != null ? { threshold } : {}) },
123
122
  },
124
123
  };
@@ -215,6 +214,27 @@ function findLastUserMessageIndex(messages) {
215
214
  }
216
215
  return -1;
217
216
  }
217
+ /**
218
+ * Selects one exact contiguous suffix from the OpenClaw transcript.
219
+ *
220
+ * The cut is moved backward to a user-message boundary so an assistant tool
221
+ * call and its tool results can never be separated by tail windowing. Message
222
+ * objects are returned untouched; daemon-normalized messages are never mapped
223
+ * back onto the provider transcript.
224
+ */
225
+ function selectTurnAlignedSourceSuffix(messages, maxMessages) {
226
+ if (messages.length === 0)
227
+ return [];
228
+ let start = Math.max(0, messages.length - Math.max(1, Math.floor(maxMessages)));
229
+ while (start > 0 && messages[start]?.role !== "user") {
230
+ start -= 1;
231
+ }
232
+ if (messages[start]?.role !== "user") {
233
+ const firstUserIndex = messages.findIndex((message) => message.role === "user");
234
+ start = firstUserIndex >= 0 ? firstUserIndex : 0;
235
+ }
236
+ return messages.slice(start);
237
+ }
218
238
  /**
219
239
  * Normalizes kernel content (string or block array) to a flat string.
220
240
  */
@@ -673,6 +693,43 @@ function trimMessagesToBudget(messages, tokenBudget) {
673
693
  }
674
694
  return [{ ...last, content: truncated }];
675
695
  }
696
+ /**
697
+ * Trims a compacted source projection only at user-turn boundaries.
698
+ *
699
+ * The newest user turn is mandatory, even if it alone exceeds the budget:
700
+ * dropping part of an in-flight tool-call/result sequence is more dangerous
701
+ * than surfacing a truthful overflow. Older turns are kept only as a
702
+ * contiguous suffix and only when their complete bundle fits.
703
+ */
704
+ function trimTurnAlignedSourceSuffixToBudget(messages, tokenBudget) {
705
+ if (messages.length === 0)
706
+ return [];
707
+ const userStarts = [];
708
+ for (let index = 0; index < messages.length; index += 1) {
709
+ if (messages[index]?.role === "user")
710
+ userStarts.push(index);
711
+ }
712
+ if (userStarts.length === 0) {
713
+ return messages;
714
+ }
715
+ const keptBundles = [];
716
+ let used = 0;
717
+ let bundleEnd = messages.length;
718
+ for (let index = userStarts.length - 1; index >= 0; index -= 1) {
719
+ const bundleStart = userStarts[index];
720
+ const bundle = messages.slice(bundleStart, bundleEnd);
721
+ const bundleCost = approximateMessagesTokens(bundle);
722
+ if (keptBundles.length > 0 && used + bundleCost > tokenBudget) {
723
+ break;
724
+ }
725
+ keptBundles.unshift(bundle);
726
+ used += bundleCost;
727
+ bundleEnd = bundleStart;
728
+ if (used >= tokenBudget)
729
+ break;
730
+ }
731
+ return keptBundles.flat();
732
+ }
676
733
  /**
677
734
  * Bounds after-turn messages for ingest, trimming if over max tokens.
678
735
  */
@@ -720,6 +777,39 @@ function enforceTokenBudgetInvariant(result, tokenBudget) {
720
777
  estimatedTokens: Math.min(effectiveBudget, systemPromptTokens + trimmedEstimate),
721
778
  };
722
779
  }
780
+ /**
781
+ * Enforces the model budget for a daemon-compacted projection without ever
782
+ * splitting the newest user/tool turn. The compacted system context and older
783
+ * complete turns yield budget before the live turn does.
784
+ */
785
+ function enforceCompactedProjectionBudgetInvariant(result, tokenBudget) {
786
+ if (typeof tokenBudget !== "number" || !Number.isFinite(tokenBudget) || tokenBudget <= 0) {
787
+ return result;
788
+ }
789
+ const effectiveBudget = resolveEffectiveAssembleBudget(Math.max(1, Math.floor(tokenBudget)));
790
+ const approximateTotal = approximateTokenCount(result.systemPromptAddition) +
791
+ approximateMessagesTokens(result.messages);
792
+ if (result.estimatedTokens <= effectiveBudget && approximateTotal <= effectiveBudget) {
793
+ return result;
794
+ }
795
+ const lastUserIndex = findLastUserMessageIndex(result.messages);
796
+ const mandatoryMessages = lastUserIndex >= 0
797
+ ? result.messages.slice(lastUserIndex)
798
+ : result.messages;
799
+ const mandatoryTokens = approximateMessagesTokens(mandatoryMessages);
800
+ const systemPromptBudget = Math.max(0, effectiveBudget - mandatoryTokens);
801
+ const systemPromptAddition = truncateSystemPromptAdditionToTokenBudget(result.systemPromptAddition, systemPromptBudget);
802
+ const systemPromptTokens = approximateTokenCount(systemPromptAddition);
803
+ const messageBudget = Math.max(mandatoryTokens, effectiveBudget - systemPromptTokens);
804
+ const messages = trimTurnAlignedSourceSuffixToBudget(result.messages, messageBudget);
805
+ const estimatedTokens = systemPromptTokens + approximateMessagesTokens(messages);
806
+ return {
807
+ ...result,
808
+ messages,
809
+ systemPromptAddition,
810
+ estimatedTokens,
811
+ };
812
+ }
723
813
  function buildBudgetFallbackContext(messages, tokenBudget) {
724
814
  const effectiveBudget = resolveEffectiveAssembleBudget(tokenBudget);
725
815
  const fallbackMessages = trimMessagesToBudget(messages.map((message) => ({ ...message })), effectiveBudget);
@@ -733,7 +823,11 @@ function buildBudgetFallbackContext(messages, tokenBudget) {
733
823
  const DAEMON_AUTHORED_CONTEXT_RE = /<authored_context\b[^>]*>([\s\S]*?)<\/authored_context>/gi;
734
824
  const DAEMON_AUTHORED_CONTEXT_GUIDANCE_RE = /^\s*Treat the authored entries below as active project rules and identity context\.?\s*$/i;
735
825
  const COMPACTED_SESSION_CONTEXT_RE = /<compacted_session_context\b([^>]*)>([\s\S]*?)<\/compacted_session_context>/gi;
826
+ const HAS_COMPACTED_SESSION_CONTEXT_RE = /<compacted_session_context\b/i;
736
827
  const COMPACTED_SESSION_RENDER_LEDGER_RE = /(?:^|\n)(?:Artifacts:|Constraints:|Open Next Steps:|Extracted context anchors:)(?:\n|$)/;
828
+ function hasCompactedSessionContext(text) {
829
+ return HAS_COMPACTED_SESSION_CONTEXT_RE.test(text);
830
+ }
737
831
  function sanitizeDaemonSystemPromptAddition(text) {
738
832
  return demoteDaemonAuthoredContextBlocks(sanitizeToolCallPatterns(canonicalizeCompactedSessionContextBlocks(text)));
739
833
  }
@@ -1170,11 +1264,11 @@ function ensureReplaySafeUserTurn(assembled, sourceMessages, logger, tokenBudget
1170
1264
  /**
1171
1265
  * Normalizes a compact result into the OpenClaw-compatible assemble result format.
1172
1266
  */
1173
- export function normalizeAssembleResult(result, sourceMessages) {
1174
- // The daemon's visibleMsgs is a filtered echo of args.Messages (toolResult
1175
- // stripped). Use sourceMessages directly they carry the full transcript
1176
- // with tool protocol intact. The daemon contributes memory context via
1177
- // systemPromptAddition, not message manipulation.
1267
+ export function normalizeAssembleResult(result, sourceMessages, promptAuthority = PROMPT_AUTHORITY_PREASSEMBLY_MAY_OVERFLOW) {
1268
+ // The daemon's visibleMsgs is a normalized projection that cannot preserve
1269
+ // provider tool protocol. Use only an exact source transcript projection.
1270
+ // The caller decides whether that is the full pre-compaction transcript or
1271
+ // the user-turn-aligned suffix owned by daemon compaction.
1178
1272
  const systemPromptAddition = typeof result.systemPromptAddition === "string"
1179
1273
  ? sanitizeDaemonSystemPromptAddition(result.systemPromptAddition)
1180
1274
  : "";
@@ -1182,7 +1276,7 @@ export function normalizeAssembleResult(result, sourceMessages) {
1182
1276
  messages: sourceMessages ?? [],
1183
1277
  estimatedTokens: typeof result.estimatedTokens === "number" ? result.estimatedTokens : 0,
1184
1278
  systemPromptAddition,
1185
- promptAuthority: PROMPT_AUTHORITY_PREASSEMBLY_MAY_OVERFLOW,
1279
+ promptAuthority,
1186
1280
  ...(result.debug != null ? { debug: result.debug } : {}),
1187
1281
  };
1188
1282
  }
@@ -1277,6 +1371,7 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
1277
1371
  setOptimizationMemoCacheSize(cfg.optimizationMemoCacheSize);
1278
1372
  }
1279
1373
  const predictiveContextCache = new Map();
1374
+ const compactedProjectionSessions = new Set();
1280
1375
  const PREDICTIVE_CACHE_MAX_SIZE = 100;
1281
1376
  // BeforeTurnKernel state
1282
1377
  const turnCache = new TurnMemoryCache(100);
@@ -1285,6 +1380,21 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
1285
1380
  let lastUserMessageHash = null;
1286
1381
  let cachedIdentity = null;
1287
1382
  let cachedSessionKey;
1383
+ function activateCompactedProjection(sessionId, source) {
1384
+ const wasActive = compactedProjectionSessions.has(sessionId);
1385
+ compactedProjectionSessions.add(sessionId);
1386
+ predictiveContextCache.delete(sessionId);
1387
+ postToolRecallCache.delete(sessionId);
1388
+ turnCache.invalidateSession(sessionId);
1389
+ if (!wasActive) {
1390
+ logger.info?.(`LibraVDB activated daemon-owned compacted projection sessionId=${sessionId} source=${source}`);
1391
+ }
1392
+ }
1393
+ function enforceAssembleBudget(result, tokenBudget, compactionProjectionActive) {
1394
+ return compactionProjectionActive
1395
+ ? enforceCompactedProjectionBudgetInvariant(result, tokenBudget)
1396
+ : enforceTokenBudgetInvariant(result, tokenBudget);
1397
+ }
1288
1398
  function resolveUserId(args) {
1289
1399
  // Framework-provided userId takes priority (channels, future SDK compat).
1290
1400
  const fwUserId = args?.userIdOverride?.trim();
@@ -1725,16 +1835,40 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
1725
1835
  return null;
1726
1836
  }
1727
1837
  }
1838
+ async function injectPersonaContext(params) {
1839
+ try {
1840
+ const resp = await params.client.getUserCard({ userId: "__bot_persona__" });
1841
+ if (!resp.cardJson)
1842
+ return null;
1843
+ let card;
1844
+ try {
1845
+ card = JSON.parse(resp.cardJson).card ?? resp.cardJson;
1846
+ }
1847
+ catch {
1848
+ card = resp.cardJson;
1849
+ }
1850
+ if (!card || card.trim().length === 0)
1851
+ return null;
1852
+ return '<bot_persona>\n' + card + '\n</bot_persona>';
1853
+ }
1854
+ catch {
1855
+ return null;
1856
+ }
1857
+ }
1728
1858
  async function runCompaction(args) {
1729
1859
  const request = buildCompactSessionRequest(args);
1730
1860
  try {
1731
1861
  const client = await runtime.getClient();
1732
1862
  const threshold = getDynamicCompactThreshold(args.tokenBudget);
1733
- return normalizeCompactResult(await client.compactSession(request), {
1863
+ const result = normalizeCompactResult(await client.compactSession(request), {
1734
1864
  tokensBefore: args.currentTokenCount,
1735
1865
  logger,
1736
1866
  ...(threshold != null ? { threshold } : {}),
1737
1867
  });
1868
+ if (result.ok && result.compacted) {
1869
+ activateCompactedProjection(args.sessionId, "compact");
1870
+ }
1871
+ return result;
1738
1872
  }
1739
1873
  catch (error) {
1740
1874
  return {
@@ -1794,6 +1928,7 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
1794
1928
  const sessionId = requireSessionId(args.sessionId, "bootstrap");
1795
1929
  predictiveContextCache.delete(sessionId);
1796
1930
  postToolRecallCache.delete(sessionId);
1931
+ turnCache.invalidateSession(sessionId);
1797
1932
  asyncIngestionQueues.delete(sessionId);
1798
1933
  const userId = resolveUserId({
1799
1934
  userIdOverride: args.userId,
@@ -1836,6 +1971,7 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
1836
1971
  },
1837
1972
  async assemble(args) {
1838
1973
  const sessionId = requireSessionId(args.sessionId, "assemble");
1974
+ let compactionProjectionActive = compactedProjectionSessions.has(sessionId);
1839
1975
  const userId = resolveUserId({
1840
1976
  userIdOverride: args.userId,
1841
1977
  sessionKey: args.sessionKey,
@@ -1845,10 +1981,22 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
1845
1981
  // Processing the full history on every turn is O(N²) and the
1846
1982
  // primary source of growing turn latency.
1847
1983
  const normalizeWindow = 50;
1848
- const recentMessages = args.messages.length > normalizeWindow
1849
- ? args.messages.slice(-normalizeWindow)
1850
- : args.messages;
1984
+ const recentMessages = selectTurnAlignedSourceSuffix(args.messages, normalizeWindow);
1851
1985
  const messages = normalizeKernelMessages(recentMessages);
1986
+ const projectedSourceMessages = () => compactionProjectionActive
1987
+ ? recentMessages
1988
+ : args.messages;
1989
+ const buildAssembleFallback = () => {
1990
+ if (!compactionProjectionActive) {
1991
+ return buildBudgetFallbackContext(args.messages, args.tokenBudget);
1992
+ }
1993
+ return enforceCompactedProjectionBudgetInvariant({
1994
+ messages: projectedSourceMessages(),
1995
+ estimatedTokens: approximateMessagesTokens(projectedSourceMessages()),
1996
+ systemPromptAddition: "",
1997
+ promptAuthority: "assembled",
1998
+ }, args.tokenBudget);
1999
+ };
1852
2000
  const strippedPrompt = args.prompt
1853
2001
  ? normalizeKernelContent(args.prompt, { retainOpenClawContext: false })
1854
2002
  : "";
@@ -1899,10 +2047,15 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
1899
2047
  compacted: compactionResult.compacted,
1900
2048
  reason: compactionResult.reason,
1901
2049
  });
1902
- if (!compactionResult.ok) {
2050
+ compactionProjectionActive = compactedProjectionSessions.has(sessionId);
2051
+ if (!compactionResult.ok && !compactionProjectionActive) {
1903
2052
  logger.info?.(`LibraVDB predictive compaction blocked assemble path at ${currentContextTokens} tokens ` +
1904
2053
  `(threshold=${dynamicCompactThreshold}): ${compactionResult.reason ?? "compaction failed"}`);
1905
- return ensureReplaySafeUserTurn(buildBudgetFallbackContext(args.messages, args.tokenBudget), args.messages, logger, args.tokenBudget);
2054
+ return ensureReplaySafeUserTurn(buildAssembleFallback(), args.messages, logger, args.tokenBudget);
2055
+ }
2056
+ if (!compactionResult.ok) {
2057
+ logger.info?.(`LibraVDB predictive compaction made no additional progress at ${currentContextTokens} tokens; ` +
2058
+ "continuing with the existing daemon-owned compacted projection.");
1906
2059
  }
1907
2060
  }
1908
2061
  // BeforeTurnKernel: semantic memory retrieval against the current user query.
@@ -1956,8 +2109,13 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
1956
2109
  }
1957
2110
  }
1958
2111
  if (cachedSystemPrompt !== undefined) {
1959
- const mockResp = { messages: args.messages, systemPromptAddition: cachedSystemPrompt };
1960
- enforced = enforceTokenBudgetInvariant(normalizeAssembleResult(mockResp, args.messages), args.tokenBudget);
2112
+ if (hasCompactedSessionContext(cachedSystemPrompt)) {
2113
+ activateCompactedProjection(sessionId, "assemble");
2114
+ compactionProjectionActive = true;
2115
+ }
2116
+ const sourceProjection = projectedSourceMessages();
2117
+ const mockResp = { messages: sourceProjection, systemPromptAddition: cachedSystemPrompt };
2118
+ enforced = enforceAssembleBudget(normalizeAssembleResult(mockResp, sourceProjection, compactionProjectionActive ? "assembled" : PROMPT_AUTHORITY_PREASSEMBLY_MAY_OVERFLOW), args.tokenBudget, compactionProjectionActive);
1961
2119
  }
1962
2120
  else {
1963
2121
  // Drain pending async ingestion for this session so the daemon's
@@ -2018,7 +2176,13 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
2018
2176
  }),
2019
2177
  new Promise((_, reject) => setTimeout(() => reject(new Error(`AssembleContextInternal timed out after ${assembleTimeout}ms`)), assembleTimeout)),
2020
2178
  ]);
2021
- const assembled = normalizeAssembleResult(resp, args.messages);
2179
+ if (typeof resp.systemPromptAddition === "string" &&
2180
+ hasCompactedSessionContext(resp.systemPromptAddition)) {
2181
+ activateCompactedProjection(sessionId, "assemble");
2182
+ compactionProjectionActive = true;
2183
+ }
2184
+ const sourceProjection = projectedSourceMessages();
2185
+ const assembled = normalizeAssembleResult(resp, sourceProjection, compactionProjectionActive ? "assembled" : PROMPT_AUTHORITY_PREASSEMBLY_MAY_OVERFLOW);
2022
2186
  const continuityContext = await injectContinuityContext({
2023
2187
  client,
2024
2188
  userId,
@@ -2029,12 +2193,16 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
2029
2193
  systemPromptAddition: assembled.systemPromptAddition,
2030
2194
  });
2031
2195
  const userCardContext = await injectUserCardContext({ client, userId });
2196
+ const personaContext = await injectPersonaContext({ client });
2032
2197
  const rulesContext = buildRulesContext();
2033
- // Only inject continuity, user card, and rules on session bootstrap.
2034
- // After the first turn, predictive context handles it.
2198
+ // Only inject on session bootstrap.
2035
2199
  const isSessionBootstrap = messages.length <= 1;
2036
2200
  let withContext = assembled;
2037
2201
  if (isSessionBootstrap) {
2202
+ // Persona first — identity of the bot itself.
2203
+ if (personaContext) {
2204
+ withContext = { ...withContext, systemPromptAddition: appendSystemPromptAddition(withContext.systemPromptAddition, personaContext) };
2205
+ }
2038
2206
  if (userCardContext) {
2039
2207
  withContext = { ...withContext, systemPromptAddition: appendSystemPromptAddition(withContext.systemPromptAddition, userCardContext) };
2040
2208
  }
@@ -2046,13 +2214,13 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
2046
2214
  withContext = { ...withContext, systemPromptAddition: appendSystemPromptAddition(withContext.systemPromptAddition, rulesContext) };
2047
2215
  }
2048
2216
  }
2049
- enforced = enforceTokenBudgetInvariant(await augmentWithExactRecall(withContext, {
2217
+ enforced = enforceAssembleBudget(await augmentWithExactRecall(withContext, {
2050
2218
  queryText: retrievalQuery,
2051
2219
  userId,
2052
2220
  sessionId,
2053
2221
  tokenBudget: args.tokenBudget,
2054
2222
  reservedTokens: reservedCurrentTurnTokens,
2055
- }), args.tokenBudget);
2223
+ }), args.tokenBudget, compactionProjectionActive);
2056
2224
  const predictions = predictiveContextCache.get(sessionId) || [];
2057
2225
  predictiveContextCache.delete(sessionId);
2058
2226
  if (predictions.length > 0) {
@@ -2104,14 +2272,15 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
2104
2272
  systemPromptAddition: enforced.systemPromptAddition,
2105
2273
  });
2106
2274
  }
2107
- enforced = enforceTokenBudgetInvariant(enforced, args.tokenBudget);
2108
- // normalizeAssembleResult uses sourceMessages directly full transcript
2109
- // with tool protocol intact. No message re-classification needed.
2275
+ enforced = enforceAssembleBudget(enforced, args.tokenBudget, compactionProjectionActive);
2276
+ // The returned transcript is always an untouched source projection.
2277
+ // Once daemon compaction owns history, OpenClaw must precheck this
2278
+ // assembled suffix rather than the unwindowed session transcript.
2110
2279
  return ensureReplaySafeUserTurn(enforced, args.messages, logger, args.tokenBudget);
2111
2280
  }
2112
2281
  catch (error) {
2113
2282
  logger.warn?.(`LibraVDB assemble failed, using budget-clamped fallback context: ${error instanceof Error ? error.message : String(error)}`);
2114
- return ensureReplaySafeUserTurn(buildBudgetFallbackContext(args.messages, args.tokenBudget), args.messages, logger, args.tokenBudget);
2283
+ return ensureReplaySafeUserTurn(buildAssembleFallback(), args.messages, logger, args.tokenBudget);
2115
2284
  }
2116
2285
  },
2117
2286
  async compact(args) {
@@ -2319,6 +2488,7 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
2319
2488
  }
2320
2489
  }
2321
2490
  predictiveContextCache.clear();
2491
+ compactedProjectionSessions.clear();
2322
2492
  postToolRecallCache.clear();
2323
2493
  asyncIngestionQueues.clear();
2324
2494
  triggerCache.clear();