@xdarkicex/openclaw-memory-libravdb 1.10.13 → 1.10.14-beta.2

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;
@@ -79,6 +79,14 @@ function normalizeCompactResult(response, options = {}) {
79
79
  const tokenAccumulatorAfter = typeof response?.tokenAccumulatorAfter === "number" ? response.tokenAccumulatorAfter : undefined;
80
80
  const totalTurns = typeof response?.totalTurns === "bigint" ? response.totalTurns : undefined;
81
81
  const skippedNoNewTurns = typeof response?.skippedNoNewTurns === "boolean" ? response.skippedNoNewTurns : undefined;
82
+ // The daemon deliberately reports didCompact=false when the session has
83
+ // already been compacted and no new turns have arrived. That is still a
84
+ // successful compaction handoff for OpenClaw: the daemon-owned projection
85
+ // remains valid and must be activated so the host retries against it.
86
+ const alreadyCompacted = skippedNoNewTurns === true &&
87
+ lastCompactedTurn != null &&
88
+ lastCompactedTurn > 0n;
89
+ const effectiveCompacted = didCompact || alreadyCompacted;
82
90
  if (lastCompactedTurn != null ||
83
91
  tokenAccumulatorAfter != null ||
84
92
  totalTurns != null ||
@@ -106,19 +114,18 @@ function normalizeCompactResult(response, options = {}) {
106
114
  // instead of accepting a bloated session.
107
115
  const threshold = options.threshold;
108
116
  const overBudget = threshold != null && tokensBefore >= threshold;
109
- const engineRefused = !didCompact && overBudget;
110
- const tokensAfter = didCompact && typeof response?.tokensAfter === "number" && response.tokensAfter > 0
117
+ const engineRefused = !effectiveCompacted && overBudget;
118
+ const tokensAfter = didCompact && typeof response?.tokensAfter === "number" && response.tokensAfter >= 0
111
119
  ? response.tokensAfter
112
120
  : undefined;
113
121
  return {
114
122
  ok: !engineRefused,
115
123
  compacted: didCompact,
116
- ...(didCompact ? {} : { reason: engineRefused ? "overbudget_not_compacted" : "not_compacted" }),
124
+ ...(effectiveCompacted ? {} : { reason: engineRefused ? "overbudget_not_compacted" : "not_compacted" }),
117
125
  result: {
118
126
  tokensBefore,
119
127
  ...(tokensAfter != null ? { tokensAfter } : {}),
120
- ...(details.summaryMethod ? { summary: details.summaryMethod } : {}),
121
- ...(details.summaryText ? { summaryText: details.summaryText } : {}),
128
+ ...(details.summaryText ? { summary: details.summaryText } : {}),
122
129
  details: { ...details, ...(threshold != null ? { threshold } : {}) },
123
130
  },
124
131
  };
@@ -215,6 +222,27 @@ function findLastUserMessageIndex(messages) {
215
222
  }
216
223
  return -1;
217
224
  }
225
+ /**
226
+ * Selects one exact contiguous suffix from the OpenClaw transcript.
227
+ *
228
+ * The cut is moved backward to a user-message boundary so an assistant tool
229
+ * call and its tool results can never be separated by tail windowing. Message
230
+ * objects are returned untouched; daemon-normalized messages are never mapped
231
+ * back onto the provider transcript.
232
+ */
233
+ function selectTurnAlignedSourceSuffix(messages, maxMessages) {
234
+ if (messages.length === 0)
235
+ return [];
236
+ let start = Math.max(0, messages.length - Math.max(1, Math.floor(maxMessages)));
237
+ while (start > 0 && messages[start]?.role !== "user") {
238
+ start -= 1;
239
+ }
240
+ if (messages[start]?.role !== "user") {
241
+ const firstUserIndex = messages.findIndex((message) => message.role === "user");
242
+ start = firstUserIndex >= 0 ? firstUserIndex : 0;
243
+ }
244
+ return messages.slice(start);
245
+ }
218
246
  /**
219
247
  * Normalizes kernel content (string or block array) to a flat string.
220
248
  */
@@ -673,6 +701,43 @@ function trimMessagesToBudget(messages, tokenBudget) {
673
701
  }
674
702
  return [{ ...last, content: truncated }];
675
703
  }
704
+ /**
705
+ * Trims a compacted source projection only at user-turn boundaries.
706
+ *
707
+ * The newest user turn is mandatory, even if it alone exceeds the budget:
708
+ * dropping part of an in-flight tool-call/result sequence is more dangerous
709
+ * than surfacing a truthful overflow. Older turns are kept only as a
710
+ * contiguous suffix and only when their complete bundle fits.
711
+ */
712
+ function trimTurnAlignedSourceSuffixToBudget(messages, tokenBudget) {
713
+ if (messages.length === 0)
714
+ return [];
715
+ const userStarts = [];
716
+ for (let index = 0; index < messages.length; index += 1) {
717
+ if (messages[index]?.role === "user")
718
+ userStarts.push(index);
719
+ }
720
+ if (userStarts.length === 0) {
721
+ return messages;
722
+ }
723
+ const keptBundles = [];
724
+ let used = 0;
725
+ let bundleEnd = messages.length;
726
+ for (let index = userStarts.length - 1; index >= 0; index -= 1) {
727
+ const bundleStart = userStarts[index];
728
+ const bundle = messages.slice(bundleStart, bundleEnd);
729
+ const bundleCost = approximateMessagesTokens(bundle);
730
+ if (keptBundles.length > 0 && used + bundleCost > tokenBudget) {
731
+ break;
732
+ }
733
+ keptBundles.unshift(bundle);
734
+ used += bundleCost;
735
+ bundleEnd = bundleStart;
736
+ if (used >= tokenBudget)
737
+ break;
738
+ }
739
+ return keptBundles.flat();
740
+ }
676
741
  /**
677
742
  * Bounds after-turn messages for ingest, trimming if over max tokens.
678
743
  */
@@ -720,6 +785,39 @@ function enforceTokenBudgetInvariant(result, tokenBudget) {
720
785
  estimatedTokens: Math.min(effectiveBudget, systemPromptTokens + trimmedEstimate),
721
786
  };
722
787
  }
788
+ /**
789
+ * Enforces the model budget for a daemon-compacted projection without ever
790
+ * splitting the newest user/tool turn. The compacted system context and older
791
+ * complete turns yield budget before the live turn does.
792
+ */
793
+ function enforceCompactedProjectionBudgetInvariant(result, tokenBudget) {
794
+ if (typeof tokenBudget !== "number" || !Number.isFinite(tokenBudget) || tokenBudget <= 0) {
795
+ return result;
796
+ }
797
+ const effectiveBudget = resolveEffectiveAssembleBudget(Math.max(1, Math.floor(tokenBudget)));
798
+ const approximateTotal = approximateTokenCount(result.systemPromptAddition) +
799
+ approximateMessagesTokens(result.messages);
800
+ if (result.estimatedTokens <= effectiveBudget && approximateTotal <= effectiveBudget) {
801
+ return result;
802
+ }
803
+ const lastUserIndex = findLastUserMessageIndex(result.messages);
804
+ const mandatoryMessages = lastUserIndex >= 0
805
+ ? result.messages.slice(lastUserIndex)
806
+ : result.messages;
807
+ const mandatoryTokens = approximateMessagesTokens(mandatoryMessages);
808
+ const systemPromptBudget = Math.max(0, effectiveBudget - mandatoryTokens);
809
+ const systemPromptAddition = truncateSystemPromptAdditionToTokenBudget(result.systemPromptAddition, systemPromptBudget);
810
+ const systemPromptTokens = approximateTokenCount(systemPromptAddition);
811
+ const messageBudget = Math.max(mandatoryTokens, effectiveBudget - systemPromptTokens);
812
+ const messages = trimTurnAlignedSourceSuffixToBudget(result.messages, messageBudget);
813
+ const estimatedTokens = systemPromptTokens + approximateMessagesTokens(messages);
814
+ return {
815
+ ...result,
816
+ messages,
817
+ systemPromptAddition,
818
+ estimatedTokens,
819
+ };
820
+ }
723
821
  function buildBudgetFallbackContext(messages, tokenBudget) {
724
822
  const effectiveBudget = resolveEffectiveAssembleBudget(tokenBudget);
725
823
  const fallbackMessages = trimMessagesToBudget(messages.map((message) => ({ ...message })), effectiveBudget);
@@ -733,7 +831,11 @@ function buildBudgetFallbackContext(messages, tokenBudget) {
733
831
  const DAEMON_AUTHORED_CONTEXT_RE = /<authored_context\b[^>]*>([\s\S]*?)<\/authored_context>/gi;
734
832
  const DAEMON_AUTHORED_CONTEXT_GUIDANCE_RE = /^\s*Treat the authored entries below as active project rules and identity context\.?\s*$/i;
735
833
  const COMPACTED_SESSION_CONTEXT_RE = /<compacted_session_context\b([^>]*)>([\s\S]*?)<\/compacted_session_context>/gi;
834
+ const HAS_COMPACTED_SESSION_CONTEXT_RE = /<compacted_session_context\b/i;
736
835
  const COMPACTED_SESSION_RENDER_LEDGER_RE = /(?:^|\n)(?:Artifacts:|Constraints:|Open Next Steps:|Extracted context anchors:)(?:\n|$)/;
836
+ function hasCompactedSessionContext(text) {
837
+ return HAS_COMPACTED_SESSION_CONTEXT_RE.test(text);
838
+ }
737
839
  function sanitizeDaemonSystemPromptAddition(text) {
738
840
  return demoteDaemonAuthoredContextBlocks(sanitizeToolCallPatterns(canonicalizeCompactedSessionContextBlocks(text)));
739
841
  }
@@ -1170,11 +1272,11 @@ function ensureReplaySafeUserTurn(assembled, sourceMessages, logger, tokenBudget
1170
1272
  /**
1171
1273
  * Normalizes a compact result into the OpenClaw-compatible assemble result format.
1172
1274
  */
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.
1275
+ export function normalizeAssembleResult(result, sourceMessages, promptAuthority = PROMPT_AUTHORITY_PREASSEMBLY_MAY_OVERFLOW) {
1276
+ // The daemon's visibleMsgs is a normalized projection that cannot preserve
1277
+ // provider tool protocol. Use only an exact source transcript projection.
1278
+ // The caller decides whether that is the full pre-compaction transcript or
1279
+ // the user-turn-aligned suffix owned by daemon compaction.
1178
1280
  const systemPromptAddition = typeof result.systemPromptAddition === "string"
1179
1281
  ? sanitizeDaemonSystemPromptAddition(result.systemPromptAddition)
1180
1282
  : "";
@@ -1182,7 +1284,7 @@ export function normalizeAssembleResult(result, sourceMessages) {
1182
1284
  messages: sourceMessages ?? [],
1183
1285
  estimatedTokens: typeof result.estimatedTokens === "number" ? result.estimatedTokens : 0,
1184
1286
  systemPromptAddition,
1185
- promptAuthority: PROMPT_AUTHORITY_PREASSEMBLY_MAY_OVERFLOW,
1287
+ promptAuthority,
1186
1288
  ...(result.debug != null ? { debug: result.debug } : {}),
1187
1289
  };
1188
1290
  }
@@ -1277,6 +1379,7 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
1277
1379
  setOptimizationMemoCacheSize(cfg.optimizationMemoCacheSize);
1278
1380
  }
1279
1381
  const predictiveContextCache = new Map();
1382
+ const compactedProjectionSessions = new Set();
1280
1383
  const PREDICTIVE_CACHE_MAX_SIZE = 100;
1281
1384
  // BeforeTurnKernel state
1282
1385
  const turnCache = new TurnMemoryCache(100);
@@ -1285,6 +1388,21 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
1285
1388
  let lastUserMessageHash = null;
1286
1389
  let cachedIdentity = null;
1287
1390
  let cachedSessionKey;
1391
+ function activateCompactedProjection(sessionId, source) {
1392
+ const wasActive = compactedProjectionSessions.has(sessionId);
1393
+ compactedProjectionSessions.add(sessionId);
1394
+ predictiveContextCache.delete(sessionId);
1395
+ postToolRecallCache.delete(sessionId);
1396
+ turnCache.invalidateSession(sessionId);
1397
+ if (!wasActive) {
1398
+ logger.info?.(`LibraVDB activated daemon-owned compacted projection sessionId=${sessionId} source=${source}`);
1399
+ }
1400
+ }
1401
+ function enforceAssembleBudget(result, tokenBudget, compactionProjectionActive) {
1402
+ return compactionProjectionActive
1403
+ ? enforceCompactedProjectionBudgetInvariant(result, tokenBudget)
1404
+ : enforceTokenBudgetInvariant(result, tokenBudget);
1405
+ }
1288
1406
  function resolveUserId(args) {
1289
1407
  // Framework-provided userId takes priority (channels, future SDK compat).
1290
1408
  const fwUserId = args?.userIdOverride?.trim();
@@ -1750,11 +1868,15 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
1750
1868
  try {
1751
1869
  const client = await runtime.getClient();
1752
1870
  const threshold = getDynamicCompactThreshold(args.tokenBudget);
1753
- return normalizeCompactResult(await client.compactSession(request), {
1871
+ const result = normalizeCompactResult(await client.compactSession(request), {
1754
1872
  tokensBefore: args.currentTokenCount,
1755
1873
  logger,
1756
1874
  ...(threshold != null ? { threshold } : {}),
1757
1875
  });
1876
+ if (result.ok && result.compacted) {
1877
+ activateCompactedProjection(args.sessionId, "compact");
1878
+ }
1879
+ return result;
1758
1880
  }
1759
1881
  catch (error) {
1760
1882
  return {
@@ -1814,6 +1936,7 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
1814
1936
  const sessionId = requireSessionId(args.sessionId, "bootstrap");
1815
1937
  predictiveContextCache.delete(sessionId);
1816
1938
  postToolRecallCache.delete(sessionId);
1939
+ turnCache.invalidateSession(sessionId);
1817
1940
  asyncIngestionQueues.delete(sessionId);
1818
1941
  const userId = resolveUserId({
1819
1942
  userIdOverride: args.userId,
@@ -1856,6 +1979,7 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
1856
1979
  },
1857
1980
  async assemble(args) {
1858
1981
  const sessionId = requireSessionId(args.sessionId, "assemble");
1982
+ let compactionProjectionActive = compactedProjectionSessions.has(sessionId);
1859
1983
  const userId = resolveUserId({
1860
1984
  userIdOverride: args.userId,
1861
1985
  sessionKey: args.sessionKey,
@@ -1865,10 +1989,22 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
1865
1989
  // Processing the full history on every turn is O(N²) and the
1866
1990
  // primary source of growing turn latency.
1867
1991
  const normalizeWindow = 50;
1868
- const recentMessages = args.messages.length > normalizeWindow
1869
- ? args.messages.slice(-normalizeWindow)
1870
- : args.messages;
1992
+ const recentMessages = selectTurnAlignedSourceSuffix(args.messages, normalizeWindow);
1871
1993
  const messages = normalizeKernelMessages(recentMessages);
1994
+ const projectedSourceMessages = () => compactionProjectionActive
1995
+ ? recentMessages
1996
+ : args.messages;
1997
+ const buildAssembleFallback = () => {
1998
+ if (!compactionProjectionActive) {
1999
+ return buildBudgetFallbackContext(args.messages, args.tokenBudget);
2000
+ }
2001
+ return enforceCompactedProjectionBudgetInvariant({
2002
+ messages: projectedSourceMessages(),
2003
+ estimatedTokens: approximateMessagesTokens(projectedSourceMessages()),
2004
+ systemPromptAddition: "",
2005
+ promptAuthority: "assembled",
2006
+ }, args.tokenBudget);
2007
+ };
1872
2008
  const strippedPrompt = args.prompt
1873
2009
  ? normalizeKernelContent(args.prompt, { retainOpenClawContext: false })
1874
2010
  : "";
@@ -1919,10 +2055,15 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
1919
2055
  compacted: compactionResult.compacted,
1920
2056
  reason: compactionResult.reason,
1921
2057
  });
1922
- if (!compactionResult.ok) {
2058
+ compactionProjectionActive = compactedProjectionSessions.has(sessionId);
2059
+ if (!compactionResult.ok && !compactionProjectionActive) {
1923
2060
  logger.info?.(`LibraVDB predictive compaction blocked assemble path at ${currentContextTokens} tokens ` +
1924
2061
  `(threshold=${dynamicCompactThreshold}): ${compactionResult.reason ?? "compaction failed"}`);
1925
- return ensureReplaySafeUserTurn(buildBudgetFallbackContext(args.messages, args.tokenBudget), args.messages, logger, args.tokenBudget);
2062
+ return ensureReplaySafeUserTurn(buildAssembleFallback(), args.messages, logger, args.tokenBudget);
2063
+ }
2064
+ if (!compactionResult.ok) {
2065
+ logger.info?.(`LibraVDB predictive compaction made no additional progress at ${currentContextTokens} tokens; ` +
2066
+ "continuing with the existing daemon-owned compacted projection.");
1926
2067
  }
1927
2068
  }
1928
2069
  // BeforeTurnKernel: semantic memory retrieval against the current user query.
@@ -1976,8 +2117,13 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
1976
2117
  }
1977
2118
  }
1978
2119
  if (cachedSystemPrompt !== undefined) {
1979
- const mockResp = { messages: args.messages, systemPromptAddition: cachedSystemPrompt };
1980
- enforced = enforceTokenBudgetInvariant(normalizeAssembleResult(mockResp, args.messages), args.tokenBudget);
2120
+ if (hasCompactedSessionContext(cachedSystemPrompt)) {
2121
+ activateCompactedProjection(sessionId, "assemble");
2122
+ compactionProjectionActive = true;
2123
+ }
2124
+ const sourceProjection = projectedSourceMessages();
2125
+ const mockResp = { messages: sourceProjection, systemPromptAddition: cachedSystemPrompt };
2126
+ enforced = enforceAssembleBudget(normalizeAssembleResult(mockResp, sourceProjection, compactionProjectionActive ? "assembled" : PROMPT_AUTHORITY_PREASSEMBLY_MAY_OVERFLOW), args.tokenBudget, compactionProjectionActive);
1981
2127
  }
1982
2128
  else {
1983
2129
  // Drain pending async ingestion for this session so the daemon's
@@ -2038,7 +2184,13 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
2038
2184
  }),
2039
2185
  new Promise((_, reject) => setTimeout(() => reject(new Error(`AssembleContextInternal timed out after ${assembleTimeout}ms`)), assembleTimeout)),
2040
2186
  ]);
2041
- const assembled = normalizeAssembleResult(resp, args.messages);
2187
+ if (typeof resp.systemPromptAddition === "string" &&
2188
+ hasCompactedSessionContext(resp.systemPromptAddition)) {
2189
+ activateCompactedProjection(sessionId, "assemble");
2190
+ compactionProjectionActive = true;
2191
+ }
2192
+ const sourceProjection = projectedSourceMessages();
2193
+ const assembled = normalizeAssembleResult(resp, sourceProjection, compactionProjectionActive ? "assembled" : PROMPT_AUTHORITY_PREASSEMBLY_MAY_OVERFLOW);
2042
2194
  const continuityContext = await injectContinuityContext({
2043
2195
  client,
2044
2196
  userId,
@@ -2070,13 +2222,13 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
2070
2222
  withContext = { ...withContext, systemPromptAddition: appendSystemPromptAddition(withContext.systemPromptAddition, rulesContext) };
2071
2223
  }
2072
2224
  }
2073
- enforced = enforceTokenBudgetInvariant(await augmentWithExactRecall(withContext, {
2225
+ enforced = enforceAssembleBudget(await augmentWithExactRecall(withContext, {
2074
2226
  queryText: retrievalQuery,
2075
2227
  userId,
2076
2228
  sessionId,
2077
2229
  tokenBudget: args.tokenBudget,
2078
2230
  reservedTokens: reservedCurrentTurnTokens,
2079
- }), args.tokenBudget);
2231
+ }), args.tokenBudget, compactionProjectionActive);
2080
2232
  const predictions = predictiveContextCache.get(sessionId) || [];
2081
2233
  predictiveContextCache.delete(sessionId);
2082
2234
  if (predictions.length > 0) {
@@ -2128,14 +2280,15 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
2128
2280
  systemPromptAddition: enforced.systemPromptAddition,
2129
2281
  });
2130
2282
  }
2131
- enforced = enforceTokenBudgetInvariant(enforced, args.tokenBudget);
2132
- // normalizeAssembleResult uses sourceMessages directly full transcript
2133
- // with tool protocol intact. No message re-classification needed.
2283
+ enforced = enforceAssembleBudget(enforced, args.tokenBudget, compactionProjectionActive);
2284
+ // The returned transcript is always an untouched source projection.
2285
+ // Once daemon compaction owns history, OpenClaw must precheck this
2286
+ // assembled suffix rather than the unwindowed session transcript.
2134
2287
  return ensureReplaySafeUserTurn(enforced, args.messages, logger, args.tokenBudget);
2135
2288
  }
2136
2289
  catch (error) {
2137
2290
  logger.warn?.(`LibraVDB assemble failed, using budget-clamped fallback context: ${error instanceof Error ? error.message : String(error)}`);
2138
- return ensureReplaySafeUserTurn(buildBudgetFallbackContext(args.messages, args.tokenBudget), args.messages, logger, args.tokenBudget);
2291
+ return ensureReplaySafeUserTurn(buildAssembleFallback(), args.messages, logger, args.tokenBudget);
2139
2292
  }
2140
2293
  },
2141
2294
  async compact(args) {
@@ -2343,6 +2496,7 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
2343
2496
  }
2344
2497
  }
2345
2498
  predictiveContextCache.clear();
2499
+ compactedProjectionSessions.clear();
2346
2500
  postToolRecallCache.clear();
2347
2501
  asyncIngestionQueues.clear();
2348
2502
  triggerCache.clear();
package/dist/index.js CHANGED
@@ -18615,10 +18615,10 @@ function buildRulesContext() {
18615
18615
  }
18616
18616
 
18617
18617
  // src/manifest.ts
18618
- import * as fs2 from "fs";
18619
- import * as path2 from "path";
18620
- import * as crypto from "crypto";
18621
- import * as os2 from "os";
18618
+ import * as fs2 from "node:fs";
18619
+ import * as path2 from "node:path";
18620
+ import * as crypto from "node:crypto";
18621
+ import * as os2 from "node:os";
18622
18622
  var TurnManifestStore = class {
18623
18623
  manifestDir;
18624
18624
  constructor() {
@@ -18903,6 +18903,8 @@ function normalizeCompactResult(response, options = {}) {
18903
18903
  const tokenAccumulatorAfter = typeof response?.tokenAccumulatorAfter === "number" ? response.tokenAccumulatorAfter : void 0;
18904
18904
  const totalTurns = typeof response?.totalTurns === "bigint" ? response.totalTurns : void 0;
18905
18905
  const skippedNoNewTurns = typeof response?.skippedNoNewTurns === "boolean" ? response.skippedNoNewTurns : void 0;
18906
+ const alreadyCompacted = skippedNoNewTurns === true && lastCompactedTurn != null && lastCompactedTurn > 0n;
18907
+ const effectiveCompacted = didCompact || alreadyCompacted;
18906
18908
  if (lastCompactedTurn != null || tokenAccumulatorAfter != null || totalTurns != null || skippedNoNewTurns != null) {
18907
18909
  options.logger?.info?.(
18908
18910
  `[compact:trace] daemon state lastCompactedTurn=${lastCompactedTurn?.toString() ?? "unknown"} tokenAccumulatorAfter=${tokenAccumulatorAfter ?? "unknown"} totalTurns=${totalTurns?.toString() ?? "unknown"} skippedNoNewTurns=${skippedNoNewTurns ?? "unknown"}`
@@ -18922,17 +18924,16 @@ function normalizeCompactResult(response, options = {}) {
18922
18924
  };
18923
18925
  const threshold = options.threshold;
18924
18926
  const overBudget = threshold != null && tokensBefore >= threshold;
18925
- const engineRefused = !didCompact && overBudget;
18926
- const tokensAfter = didCompact && typeof response?.tokensAfter === "number" && response.tokensAfter > 0 ? response.tokensAfter : void 0;
18927
+ const engineRefused = !effectiveCompacted && overBudget;
18928
+ const tokensAfter = didCompact && typeof response?.tokensAfter === "number" && response.tokensAfter >= 0 ? response.tokensAfter : void 0;
18927
18929
  return {
18928
18930
  ok: !engineRefused,
18929
18931
  compacted: didCompact,
18930
- ...didCompact ? {} : { reason: engineRefused ? "overbudget_not_compacted" : "not_compacted" },
18932
+ ...effectiveCompacted ? {} : { reason: engineRefused ? "overbudget_not_compacted" : "not_compacted" },
18931
18933
  result: {
18932
18934
  tokensBefore,
18933
18935
  ...tokensAfter != null ? { tokensAfter } : {},
18934
- ...details.summaryMethod ? { summary: details.summaryMethod } : {},
18935
- ...details.summaryText ? { summaryText: details.summaryText } : {},
18936
+ ...details.summaryText ? { summary: details.summaryText } : {},
18936
18937
  details: { ...details, ...threshold != null ? { threshold } : {} }
18937
18938
  }
18938
18939
  };
@@ -19011,6 +19012,18 @@ function findLastUserMessageIndex(messages) {
19011
19012
  }
19012
19013
  return -1;
19013
19014
  }
19015
+ function selectTurnAlignedSourceSuffix(messages, maxMessages) {
19016
+ if (messages.length === 0) return [];
19017
+ let start = Math.max(0, messages.length - Math.max(1, Math.floor(maxMessages)));
19018
+ while (start > 0 && messages[start]?.role !== "user") {
19019
+ start -= 1;
19020
+ }
19021
+ if (messages[start]?.role !== "user") {
19022
+ const firstUserIndex = messages.findIndex((message) => message.role === "user");
19023
+ start = firstUserIndex >= 0 ? firstUserIndex : 0;
19024
+ }
19025
+ return messages.slice(start);
19026
+ }
19014
19027
  function normalizeKernelContent(content, options = {}) {
19015
19028
  const text = typeof content === "string" ? content : Array.isArray(content) ? content.map(stringifyKernelBlock).filter((part) => part.length > 0).join("\n") : "";
19016
19029
  return stripOpenClawUntrustedMetadataEnvelope(text, {
@@ -19374,6 +19387,32 @@ function trimMessagesToBudget(messages, tokenBudget) {
19374
19387
  }
19375
19388
  return [{ ...last, content: truncated }];
19376
19389
  }
19390
+ function trimTurnAlignedSourceSuffixToBudget(messages, tokenBudget) {
19391
+ if (messages.length === 0) return [];
19392
+ const userStarts = [];
19393
+ for (let index = 0; index < messages.length; index += 1) {
19394
+ if (messages[index]?.role === "user") userStarts.push(index);
19395
+ }
19396
+ if (userStarts.length === 0) {
19397
+ return messages;
19398
+ }
19399
+ const keptBundles = [];
19400
+ let used = 0;
19401
+ let bundleEnd = messages.length;
19402
+ for (let index = userStarts.length - 1; index >= 0; index -= 1) {
19403
+ const bundleStart = userStarts[index];
19404
+ const bundle = messages.slice(bundleStart, bundleEnd);
19405
+ const bundleCost = approximateMessagesTokens(bundle);
19406
+ if (keptBundles.length > 0 && used + bundleCost > tokenBudget) {
19407
+ break;
19408
+ }
19409
+ keptBundles.unshift(bundle);
19410
+ used += bundleCost;
19411
+ bundleEnd = bundleStart;
19412
+ if (used >= tokenBudget) break;
19413
+ }
19414
+ return keptBundles.flat();
19415
+ }
19377
19416
  function boundAfterTurnMessagesForIngest(messages, logger, sessionId) {
19378
19417
  const estimatedTokens = approximateMessagesTokens(messages);
19379
19418
  if (estimatedTokens <= AFTER_TURN_INGEST_MAX_TOKENS) {
@@ -19414,6 +19453,34 @@ function enforceTokenBudgetInvariant(result, tokenBudget) {
19414
19453
  estimatedTokens: Math.min(effectiveBudget, systemPromptTokens + trimmedEstimate)
19415
19454
  };
19416
19455
  }
19456
+ function enforceCompactedProjectionBudgetInvariant(result, tokenBudget) {
19457
+ if (typeof tokenBudget !== "number" || !Number.isFinite(tokenBudget) || tokenBudget <= 0) {
19458
+ return result;
19459
+ }
19460
+ const effectiveBudget = resolveEffectiveAssembleBudget(Math.max(1, Math.floor(tokenBudget)));
19461
+ const approximateTotal = approximateTokenCount(result.systemPromptAddition) + approximateMessagesTokens(result.messages);
19462
+ if (result.estimatedTokens <= effectiveBudget && approximateTotal <= effectiveBudget) {
19463
+ return result;
19464
+ }
19465
+ const lastUserIndex = findLastUserMessageIndex(result.messages);
19466
+ const mandatoryMessages = lastUserIndex >= 0 ? result.messages.slice(lastUserIndex) : result.messages;
19467
+ const mandatoryTokens = approximateMessagesTokens(mandatoryMessages);
19468
+ const systemPromptBudget = Math.max(0, effectiveBudget - mandatoryTokens);
19469
+ const systemPromptAddition = truncateSystemPromptAdditionToTokenBudget(
19470
+ result.systemPromptAddition,
19471
+ systemPromptBudget
19472
+ );
19473
+ const systemPromptTokens = approximateTokenCount(systemPromptAddition);
19474
+ const messageBudget = Math.max(mandatoryTokens, effectiveBudget - systemPromptTokens);
19475
+ const messages = trimTurnAlignedSourceSuffixToBudget(result.messages, messageBudget);
19476
+ const estimatedTokens = systemPromptTokens + approximateMessagesTokens(messages);
19477
+ return {
19478
+ ...result,
19479
+ messages,
19480
+ systemPromptAddition,
19481
+ estimatedTokens
19482
+ };
19483
+ }
19417
19484
  function buildBudgetFallbackContext(messages, tokenBudget) {
19418
19485
  const effectiveBudget = resolveEffectiveAssembleBudget(tokenBudget);
19419
19486
  const fallbackMessages = trimMessagesToBudget(
@@ -19430,7 +19497,11 @@ function buildBudgetFallbackContext(messages, tokenBudget) {
19430
19497
  var DAEMON_AUTHORED_CONTEXT_RE = /<authored_context\b[^>]*>([\s\S]*?)<\/authored_context>/gi;
19431
19498
  var DAEMON_AUTHORED_CONTEXT_GUIDANCE_RE = /^\s*Treat the authored entries below as active project rules and identity context\.?\s*$/i;
19432
19499
  var COMPACTED_SESSION_CONTEXT_RE = /<compacted_session_context\b([^>]*)>([\s\S]*?)<\/compacted_session_context>/gi;
19500
+ var HAS_COMPACTED_SESSION_CONTEXT_RE = /<compacted_session_context\b/i;
19433
19501
  var COMPACTED_SESSION_RENDER_LEDGER_RE = /(?:^|\n)(?:Artifacts:|Constraints:|Open Next Steps:|Extracted context anchors:)(?:\n|$)/;
19502
+ function hasCompactedSessionContext(text) {
19503
+ return HAS_COMPACTED_SESSION_CONTEXT_RE.test(text);
19504
+ }
19434
19505
  function sanitizeDaemonSystemPromptAddition(text) {
19435
19506
  return demoteDaemonAuthoredContextBlocks(
19436
19507
  sanitizeToolCallPatterns(canonicalizeCompactedSessionContextBlocks(text))
@@ -19781,13 +19852,13 @@ function ensureReplaySafeUserTurn(assembled, sourceMessages, logger, tokenBudget
19781
19852
  estimatedTokens: baseEstimatedTokens + approximateMessageTokens(fallbackUser)
19782
19853
  };
19783
19854
  }
19784
- function normalizeAssembleResult(result, sourceMessages) {
19855
+ function normalizeAssembleResult(result, sourceMessages, promptAuthority = PROMPT_AUTHORITY_PREASSEMBLY_MAY_OVERFLOW) {
19785
19856
  const systemPromptAddition = typeof result.systemPromptAddition === "string" ? sanitizeDaemonSystemPromptAddition(result.systemPromptAddition) : "";
19786
19857
  return {
19787
19858
  messages: sourceMessages ?? [],
19788
19859
  estimatedTokens: typeof result.estimatedTokens === "number" ? result.estimatedTokens : 0,
19789
19860
  systemPromptAddition,
19790
- promptAuthority: PROMPT_AUTHORITY_PREASSEMBLY_MAY_OVERFLOW,
19861
+ promptAuthority,
19791
19862
  ...result.debug != null ? { debug: result.debug } : {}
19792
19863
  };
19793
19864
  }
@@ -19858,6 +19929,7 @@ function buildContextEngineFactory(runtime, cfg, logger = console) {
19858
19929
  setOptimizationMemoCacheSize(cfg.optimizationMemoCacheSize);
19859
19930
  }
19860
19931
  const predictiveContextCache = /* @__PURE__ */ new Map();
19932
+ const compactedProjectionSessions = /* @__PURE__ */ new Set();
19861
19933
  const PREDICTIVE_CACHE_MAX_SIZE = 100;
19862
19934
  const turnCache = new TurnMemoryCache(100);
19863
19935
  const circuitBreakers = /* @__PURE__ */ new Map();
@@ -19865,6 +19937,21 @@ function buildContextEngineFactory(runtime, cfg, logger = console) {
19865
19937
  let lastUserMessageHash = null;
19866
19938
  let cachedIdentity = null;
19867
19939
  let cachedSessionKey;
19940
+ function activateCompactedProjection(sessionId, source) {
19941
+ const wasActive = compactedProjectionSessions.has(sessionId);
19942
+ compactedProjectionSessions.add(sessionId);
19943
+ predictiveContextCache.delete(sessionId);
19944
+ postToolRecallCache.delete(sessionId);
19945
+ turnCache.invalidateSession(sessionId);
19946
+ if (!wasActive) {
19947
+ logger.info?.(
19948
+ `LibraVDB activated daemon-owned compacted projection sessionId=${sessionId} source=${source}`
19949
+ );
19950
+ }
19951
+ }
19952
+ function enforceAssembleBudget(result, tokenBudget, compactionProjectionActive) {
19953
+ return compactionProjectionActive ? enforceCompactedProjectionBudgetInvariant(result, tokenBudget) : enforceTokenBudgetInvariant(result, tokenBudget);
19954
+ }
19868
19955
  function resolveUserId(args) {
19869
19956
  const fwUserId = args?.userIdOverride?.trim();
19870
19957
  if (fwUserId) return fwUserId;
@@ -20302,11 +20389,15 @@ ${cached}
20302
20389
  try {
20303
20390
  const client = await runtime.getClient();
20304
20391
  const threshold = getDynamicCompactThreshold(args.tokenBudget);
20305
- return normalizeCompactResult(await client.compactSession(request3), {
20392
+ const result = normalizeCompactResult(await client.compactSession(request3), {
20306
20393
  tokensBefore: args.currentTokenCount,
20307
20394
  logger,
20308
20395
  ...threshold != null ? { threshold } : {}
20309
20396
  });
20397
+ if (result.ok && result.compacted) {
20398
+ activateCompactedProjection(args.sessionId, "compact");
20399
+ }
20400
+ return result;
20310
20401
  } catch (error2) {
20311
20402
  return {
20312
20403
  ok: false,
@@ -20363,6 +20454,7 @@ ${cached}
20363
20454
  const sessionId = requireSessionId(args.sessionId, "bootstrap");
20364
20455
  predictiveContextCache.delete(sessionId);
20365
20456
  postToolRecallCache.delete(sessionId);
20457
+ turnCache.invalidateSession(sessionId);
20366
20458
  asyncIngestionQueues.delete(sessionId);
20367
20459
  const userId = resolveUserId({
20368
20460
  userIdOverride: args.userId,
@@ -20406,13 +20498,26 @@ ${cached}
20406
20498
  },
20407
20499
  async assemble(args) {
20408
20500
  const sessionId = requireSessionId(args.sessionId, "assemble");
20501
+ let compactionProjectionActive = compactedProjectionSessions.has(sessionId);
20409
20502
  const userId = resolveUserId({
20410
20503
  userIdOverride: args.userId,
20411
20504
  sessionKey: args.sessionKey
20412
20505
  });
20413
20506
  const normalizeWindow = 50;
20414
- const recentMessages = args.messages.length > normalizeWindow ? args.messages.slice(-normalizeWindow) : args.messages;
20507
+ const recentMessages = selectTurnAlignedSourceSuffix(args.messages, normalizeWindow);
20415
20508
  const messages = normalizeKernelMessages(recentMessages);
20509
+ const projectedSourceMessages = () => compactionProjectionActive ? recentMessages : args.messages;
20510
+ const buildAssembleFallback = () => {
20511
+ if (!compactionProjectionActive) {
20512
+ return buildBudgetFallbackContext(args.messages, args.tokenBudget);
20513
+ }
20514
+ return enforceCompactedProjectionBudgetInvariant({
20515
+ messages: projectedSourceMessages(),
20516
+ estimatedTokens: approximateMessagesTokens(projectedSourceMessages()),
20517
+ systemPromptAddition: "",
20518
+ promptAuthority: "assembled"
20519
+ }, args.tokenBudget);
20520
+ };
20416
20521
  const strippedPrompt = args.prompt ? normalizeKernelContent(args.prompt, { retainOpenClawContext: false }) : "";
20417
20522
  const lastUserIndex = findLastUserMessageIndex(messages);
20418
20523
  const isPostToolContinuation = lastUserIndex >= 0 && lastUserIndex < messages.length - 1 && hasLiveToolProtocolAfterLastUser(messages, lastUserIndex);
@@ -20458,17 +20563,23 @@ ${cached}
20458
20563
  compacted: compactionResult.compacted,
20459
20564
  reason: compactionResult.reason
20460
20565
  });
20461
- if (!compactionResult.ok) {
20566
+ compactionProjectionActive = compactedProjectionSessions.has(sessionId);
20567
+ if (!compactionResult.ok && !compactionProjectionActive) {
20462
20568
  logger.info?.(
20463
20569
  `LibraVDB predictive compaction blocked assemble path at ${currentContextTokens} tokens (threshold=${dynamicCompactThreshold}): ${compactionResult.reason ?? "compaction failed"}`
20464
20570
  );
20465
20571
  return ensureReplaySafeUserTurn(
20466
- buildBudgetFallbackContext(args.messages, args.tokenBudget),
20572
+ buildAssembleFallback(),
20467
20573
  args.messages,
20468
20574
  logger,
20469
20575
  args.tokenBudget
20470
20576
  );
20471
20577
  }
20578
+ if (!compactionResult.ok) {
20579
+ logger.info?.(
20580
+ `LibraVDB predictive compaction made no additional progress at ${currentContextTokens} tokens; continuing with the existing daemon-owned compacted projection.`
20581
+ );
20582
+ }
20472
20583
  }
20473
20584
  const btLog = cfg.beforeTurnDebug ? (msg) => logger.info?.(msg) : (_msg) => {
20474
20585
  };
@@ -20515,10 +20626,20 @@ ${cached}
20515
20626
  }
20516
20627
  }
20517
20628
  if (cachedSystemPrompt !== void 0) {
20518
- const mockResp = { messages: args.messages, systemPromptAddition: cachedSystemPrompt };
20519
- enforced = enforceTokenBudgetInvariant(
20520
- normalizeAssembleResult(mockResp, args.messages),
20521
- args.tokenBudget
20629
+ if (hasCompactedSessionContext(cachedSystemPrompt)) {
20630
+ activateCompactedProjection(sessionId, "assemble");
20631
+ compactionProjectionActive = true;
20632
+ }
20633
+ const sourceProjection = projectedSourceMessages();
20634
+ const mockResp = { messages: sourceProjection, systemPromptAddition: cachedSystemPrompt };
20635
+ enforced = enforceAssembleBudget(
20636
+ normalizeAssembleResult(
20637
+ mockResp,
20638
+ sourceProjection,
20639
+ compactionProjectionActive ? "assembled" : PROMPT_AUTHORITY_PREASSEMBLY_MAY_OVERFLOW
20640
+ ),
20641
+ args.tokenBudget,
20642
+ compactionProjectionActive
20522
20643
  );
20523
20644
  } else {
20524
20645
  const pending = asyncIngestionQueues.get(sessionId);
@@ -20580,7 +20701,16 @@ ${cached}
20580
20701
  (_, reject) => setTimeout(() => reject(new Error(`AssembleContextInternal timed out after ${assembleTimeout}ms`)), assembleTimeout)
20581
20702
  )
20582
20703
  ]);
20583
- const assembled = normalizeAssembleResult(resp, args.messages);
20704
+ if (typeof resp.systemPromptAddition === "string" && hasCompactedSessionContext(resp.systemPromptAddition)) {
20705
+ activateCompactedProjection(sessionId, "assemble");
20706
+ compactionProjectionActive = true;
20707
+ }
20708
+ const sourceProjection = projectedSourceMessages();
20709
+ const assembled = normalizeAssembleResult(
20710
+ resp,
20711
+ sourceProjection,
20712
+ compactionProjectionActive ? "assembled" : PROMPT_AUTHORITY_PREASSEMBLY_MAY_OVERFLOW
20713
+ );
20584
20714
  const continuityContext = await injectContinuityContext({
20585
20715
  client,
20586
20716
  userId,
@@ -20609,7 +20739,7 @@ ${cached}
20609
20739
  withContext = { ...withContext, systemPromptAddition: appendSystemPromptAddition(withContext.systemPromptAddition, rulesContext) };
20610
20740
  }
20611
20741
  }
20612
- enforced = enforceTokenBudgetInvariant(
20742
+ enforced = enforceAssembleBudget(
20613
20743
  await augmentWithExactRecall(withContext, {
20614
20744
  queryText: retrievalQuery,
20615
20745
  userId,
@@ -20617,7 +20747,8 @@ ${cached}
20617
20747
  tokenBudget: args.tokenBudget,
20618
20748
  reservedTokens: reservedCurrentTurnTokens
20619
20749
  }),
20620
- args.tokenBudget
20750
+ args.tokenBudget,
20751
+ compactionProjectionActive
20621
20752
  );
20622
20753
  const predictions = predictiveContextCache.get(sessionId) || [];
20623
20754
  predictiveContextCache.delete(sessionId);
@@ -20674,9 +20805,10 @@ ${cached}
20674
20805
  systemPromptAddition: enforced.systemPromptAddition
20675
20806
  });
20676
20807
  }
20677
- enforced = enforceTokenBudgetInvariant(
20808
+ enforced = enforceAssembleBudget(
20678
20809
  enforced,
20679
- args.tokenBudget
20810
+ args.tokenBudget,
20811
+ compactionProjectionActive
20680
20812
  );
20681
20813
  return ensureReplaySafeUserTurn(enforced, args.messages, logger, args.tokenBudget);
20682
20814
  } catch (error2) {
@@ -20684,7 +20816,7 @@ ${cached}
20684
20816
  `LibraVDB assemble failed, using budget-clamped fallback context: ${error2 instanceof Error ? error2.message : String(error2)}`
20685
20817
  );
20686
20818
  return ensureReplaySafeUserTurn(
20687
- buildBudgetFallbackContext(args.messages, args.tokenBudget),
20819
+ buildAssembleFallback(),
20688
20820
  args.messages,
20689
20821
  logger,
20690
20822
  args.tokenBudget
@@ -20872,6 +21004,7 @@ ${cached}
20872
21004
  }
20873
21005
  }
20874
21006
  predictiveContextCache.clear();
21007
+ compactedProjectionSessions.clear();
20875
21008
  postToolRecallCache.clear();
20876
21009
  asyncIngestionQueues.clear();
20877
21010
  triggerCache.clear();
@@ -31194,6 +31327,12 @@ function buildToolGuidance(availableTools) {
31194
31327
  return [];
31195
31328
  }
31196
31329
  const lines = [];
31330
+ const hasSearch = availableTools.has("memory_search");
31331
+ const hasGet = availableTools.has("memory_get");
31332
+ const hasDescribe = availableTools.has("memory_describe");
31333
+ const hasExpand = availableTools.has("memory_expand");
31334
+ const hasGrep = availableTools.has("memory_grep");
31335
+ const hasUserCard = availableTools.has("get_user_card");
31197
31336
  const hasGetCard = availableTools.has("get_user_card");
31198
31337
  const hasListCards = availableTools.has("list_user_cards");
31199
31338
  if (hasGetCard || hasListCards) {
@@ -31204,7 +31343,7 @@ function buildToolGuidance(availableTools) {
31204
31343
  hasListCards ? "- `list_user_cards()` \u2014 MANDATORY roster check. Call if unsure whether a card exists." : "",
31205
31344
  "Cards are the canonical record. You MUST call these tools. Do NOT answer from",
31206
31345
  "memory, context, or training data without checking the card first.",
31207
- "Only use memory_search if the card is empty or missing.",
31346
+ hasSearch ? "Only use memory_search if the card is empty or missing." : "Use the card result as the available identity record.",
31208
31347
  "",
31209
31348
  "**Autonomous card maintenance:**",
31210
31349
  hasGetCard ? "- When ANY speaker is mentioned with new or changed information (status, relationships, jobs, life events, feelings), call `update_user_card` BEFORE responding. Update the card first, then reply. Do NOT wait to be asked. Build the world picture proactively. Every person the user mentions matters." : "",
@@ -31212,20 +31351,19 @@ function buildToolGuidance(availableTools) {
31212
31351
  ""
31213
31352
  );
31214
31353
  }
31215
- lines.push(
31216
- "Call `memory_search` once per user question for prior turns, remembered",
31217
- "facts, earliest interactions, and channel history. Do not answer memory",
31218
- "questions from prior transcript claims \u2014 perform a search every time.",
31219
- "After receiving results, use them directly; do not re-call in the same turn.",
31220
- ...availableTools.has("memory_get") ? [
31221
- "After a `memory_search` hit, call `memory_get` when exact wording or more context is needed.",
31222
- "IMPORTANT: If a search snippet is cluttered with metadata, do NOT claim nothing was found. Call `memory_get` on the hit's path to read the full record first. The data is there \u2014 expand before giving up."
31223
- ] : [],
31224
- ""
31225
- );
31226
- const hasDescribe = availableTools.has("memory_describe");
31227
- const hasExpand = availableTools.has("memory_expand");
31228
- const hasGrep = availableTools.has("memory_grep");
31354
+ if (hasSearch) {
31355
+ lines.push(
31356
+ "Call `memory_search` once per user question for prior turns, remembered",
31357
+ "facts, earliest interactions, and channel history. Do not answer memory",
31358
+ "questions from prior transcript claims \u2014 perform a search every time.",
31359
+ "After receiving results, use them directly; do not re-call in the same turn.",
31360
+ ...hasGet ? [
31361
+ "After a `memory_search` hit, call `memory_get` when exact wording or more context is needed.",
31362
+ "IMPORTANT: If a search snippet is cluttered with metadata, do NOT claim nothing was found. Call `memory_get` on the hit's path to read the full record first. The data is there \u2014 expand before giving up."
31363
+ ] : [],
31364
+ ""
31365
+ );
31366
+ }
31229
31367
  if (hasDescribe || hasExpand || hasGrep) {
31230
31368
  lines.push(
31231
31369
  "**Compacted summaries \u2014 recall hierarchy (cheap \u2192 expensive):**",
@@ -31272,13 +31410,16 @@ function buildToolGuidance(availableTools) {
31272
31410
  ""
31273
31411
  );
31274
31412
  if (hasExpand) {
31413
+ const traversalSteps = [
31414
+ hasSearch ? "1. `memory_search` for the people/events in question to get record IDs" : "1. Start from a record ID already present in context or another tool result",
31415
+ "2. `memory_expand` with the most relevant `record_id` to walk typed causal and 6W1H hop edges",
31416
+ hasGet ? "3. Follow interesting edges; use `memory_get` for exact detail on connected records" : "3. Use the typed edge snippets to decide whether the connected records answer the question",
31417
+ ...hasUserCard ? ["4. Use `get_user_card` to cross-reference identity context"] : []
31418
+ ];
31275
31419
  lines.push(
31276
31420
  "### Causal Graph Traversal",
31277
31421
  "When the user asks about causes, patterns, or relationships:",
31278
- "1. `memory_search` for the people/events in question to get record IDs",
31279
- "2. `memory_expand` with the most relevant `record_id` to walk causal edges",
31280
- "3. Follow interesting edges \u2014 use `memory_get` for full detail on connected records",
31281
- "4. Use `get_user_card` to cross-reference identity context",
31422
+ ...traversalSteps,
31282
31423
  ""
31283
31424
  );
31284
31425
  }
@@ -31466,7 +31607,7 @@ function createMemoryExpandTool(getClient, getSessionKey, logger = console, getS
31466
31607
  return {
31467
31608
  name: "memory_expand",
31468
31609
  label: "Memory Expand",
31469
- description: "Expand compacted summaries OR walk causal graph edges from ANY record. Summary mode (summaryIds): walk the summary tree up to maxDepth levels. Graph mode (record_id): walk causal edges (why_ids/how_ids/hop_targets) from a record ID. Use exact IDs from memory_search or memory_get results \u2014 any ingested turn, memory, or summary has graph edges. After get_user_card for context, search for related people/events then expand the most relevant hit. For large expansions, spawns a sub-agent. Use memory_describe first to check if expansion is warranted.",
31610
+ description: "Expand compacted summaries OR walk causal graph edges from ANY record. Summary mode (summaryIds): walk the summary tree up to maxDepth levels. Graph mode (record_id): walk causal edges (why_ids/how_ids/hop_targets) from a record ID. Use exact IDs from memory_search or memory_get results \u2014 any ingested turn, memory, or summary has graph edges. For large expansions, spawns a sub-agent. Use memory_describe first to check if expansion is warranted.",
31470
31611
  parameters: MEMORY_EXPAND_SCHEMA,
31471
31612
  execute: async (_toolCallId, rawParams) => {
31472
31613
  const params = asParams(rawParams);
@@ -31481,10 +31622,16 @@ function createMemoryExpandTool(getClient, getSessionKey, logger = console, getS
31481
31622
  const client = await getClient();
31482
31623
  const resp = await client.expandSummary({ recordId, maxDepth });
31483
31624
  let text = resp.text ?? "";
31484
- const connected = resp.connected;
31485
- if (connected && connected.length > 0) {
31625
+ const connected = (resp.connected ?? []).map((c) => ({
31626
+ recordId: c.recordId,
31627
+ text: c.text ?? "",
31628
+ depth: c.depth,
31629
+ edgeType: c.edgeType || "unknown",
31630
+ edgeWeight: c.edgeWeight ?? 0
31631
+ }));
31632
+ if (connected.length > 0) {
31486
31633
  text = connected.map(
31487
- (c) => `[depth=${c.depth}] ${c.recordId}: ${c.text || ""}`
31634
+ (c) => `[depth=${c.depth} edge=${c.edgeType} weight=${c.edgeWeight}] ${c.recordId}: ${c.text}`
31488
31635
  ).join("\n\n");
31489
31636
  }
31490
31637
  if (!text && resp.whyIds?.length) {
@@ -31494,7 +31641,7 @@ hop_targets: ${resp.hopTargets?.join(", ") ?? "none"}`;
31494
31641
  }
31495
31642
  return {
31496
31643
  content: [{ type: "text", text: text || "(no graph edges found)" }],
31497
- details: { summaryId: recordId, depth: maxDepth, text: text || "", truncated: false, exceededBudget: false, parentCount: connected?.length ?? 0 }
31644
+ details: { summaryId: recordId, depth: maxDepth, text: text || "", truncated: false, exceededBudget: false, parentCount: connected.length, connected }
31498
31645
  };
31499
31646
  } catch (error2) {
31500
31647
  logger.warn?.(`memory_expand graph mode failed: ${formatError(error2)}`);
package/dist/manifest.js CHANGED
@@ -1,7 +1,7 @@
1
- import * as fs from "fs";
2
- import * as path from "path";
3
- import * as crypto from "crypto";
4
- import * as os from "os";
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import * as crypto from "node:crypto";
4
+ import * as os from "node:os";
5
5
  export class TurnManifestStore {
6
6
  manifestDir;
7
7
  constructor() {
@@ -25,22 +25,27 @@ function buildToolGuidance(availableTools) {
25
25
  return [];
26
26
  }
27
27
  const lines = [];
28
+ const hasSearch = availableTools.has("memory_search");
29
+ const hasGet = availableTools.has("memory_get");
30
+ const hasDescribe = availableTools.has("memory_describe");
31
+ const hasExpand = availableTools.has("memory_expand");
32
+ const hasGrep = availableTools.has("memory_grep");
33
+ const hasUserCard = availableTools.has("get_user_card");
28
34
  // ── User card tools (identity-first override) ──
29
35
  const hasGetCard = availableTools.has("get_user_card");
30
36
  const hasListCards = availableTools.has("list_user_cards");
31
37
  if (hasGetCard || hasListCards) {
32
- lines.push("**Identity/Entity questions — MANDATORY card lookup:**", "BEFORE answering any question about a person, pet, place, or named thing:", hasGetCard ? "- `get_user_card(user_id)` — MANDATORY lookup for a specific entity." : "", hasListCards ? "- `list_user_cards()` — MANDATORY roster check. Call if unsure whether a card exists." : "", "Cards are the canonical record. You MUST call these tools. Do NOT answer from", "memory, context, or training data without checking the card first.", "Only use memory_search if the card is empty or missing.", "", "**Autonomous card maintenance:**", hasGetCard ? "- When ANY speaker is mentioned with new or changed information (status, relationships, jobs, life events, feelings), call `update_user_card` BEFORE responding. Update the card first, then reply. Do NOT wait to be asked. Build the world picture proactively. Every person the user mentions matters." : "", hasGetCard ? "- If a card for the speaker doesn't exist yet, CREATE one with `update_user_card`. Better to have a stub card than no card at all." : "", "");
38
+ lines.push("**Identity/Entity questions — MANDATORY card lookup:**", "BEFORE answering any question about a person, pet, place, or named thing:", hasGetCard ? "- `get_user_card(user_id)` — MANDATORY lookup for a specific entity." : "", hasListCards ? "- `list_user_cards()` — MANDATORY roster check. Call if unsure whether a card exists." : "", "Cards are the canonical record. You MUST call these tools. Do NOT answer from", "memory, context, or training data without checking the card first.", hasSearch ? "Only use memory_search if the card is empty or missing." : "Use the card result as the available identity record.", "", "**Autonomous card maintenance:**", hasGetCard ? "- When ANY speaker is mentioned with new or changed information (status, relationships, jobs, life events, feelings), call `update_user_card` BEFORE responding. Update the card first, then reply. Do NOT wait to be asked. Build the world picture proactively. Every person the user mentions matters." : "", hasGetCard ? "- If a card for the speaker doesn't exist yet, CREATE one with `update_user_card`. Better to have a stub card than no card at all." : "", "");
39
+ }
40
+ if (hasSearch) {
41
+ lines.push("Call `memory_search` once per user question for prior turns, remembered", "facts, earliest interactions, and channel history. Do not answer memory", "questions from prior transcript claims — perform a search every time.", "After receiving results, use them directly; do not re-call in the same turn.", ...(hasGet
42
+ ? [
43
+ "After a `memory_search` hit, call `memory_get` when exact wording or more context is needed.",
44
+ "IMPORTANT: If a search snippet is cluttered with metadata, do NOT claim nothing was found. Call `memory_get` on the hit's path to read the full record first. The data is there — expand before giving up."
45
+ ]
46
+ : []), "");
33
47
  }
34
- lines.push("Call `memory_search` once per user question for prior turns, remembered", "facts, earliest interactions, and channel history. Do not answer memory", "questions from prior transcript claims — perform a search every time.", "After receiving results, use them directly; do not re-call in the same turn.", ...(availableTools.has("memory_get")
35
- ? [
36
- "After a `memory_search` hit, call `memory_get` when exact wording or more context is needed.",
37
- "IMPORTANT: If a search snippet is cluttered with metadata, do NOT claim nothing was found. Call `memory_get` on the hit's path to read the full record first. The data is there — expand before giving up."
38
- ]
39
- : []), "");
40
48
  // ── Summaries / recall (when available) ──
41
- const hasDescribe = availableTools.has("memory_describe");
42
- const hasExpand = availableTools.has("memory_expand");
43
- const hasGrep = availableTools.has("memory_grep");
44
49
  if (hasDescribe || hasExpand || hasGrep) {
45
50
  lines.push("**Compacted summaries — recall hierarchy (cheap → expensive):**", "", "Summaries in search results show `[Summary sum_xxx]: [eviction cue]`.", "The cue lists what the summary covers — anchors (files, tools, versions),", "decisions, constraints, and signal counts. Many questions can be answered", "from the cue alone without expanding.", "");
46
51
  if (hasDescribe) {
@@ -58,7 +63,17 @@ function buildToolGuidance(availableTools) {
58
63
  lines.push("### Hard Constraint Rules", "Rules are injected at session start as `<hard_constraints>`. They are non-negotiable.", "Use `set_rule` to create one (max 20), `list_rules` to see current rules,", "`delete_rule` to remove one. Rules override all other instructions.", "Never reason around a rule, find loopholes, or deprioritize it.", "");
59
64
  // ── Causal graph traversal (when expand supports record_id) ──
60
65
  if (hasExpand) {
61
- lines.push("### Causal Graph Traversal", "When the user asks about causes, patterns, or relationships:", "1. `memory_search` for the people/events in question to get record IDs", "2. `memory_expand` with the most relevant `record_id` to walk causal edges", "3. Follow interesting edges — use `memory_get` for full detail on connected records", "4. Use `get_user_card` to cross-reference identity context", "");
66
+ const traversalSteps = [
67
+ hasSearch
68
+ ? "1. `memory_search` for the people/events in question to get record IDs"
69
+ : "1. Start from a record ID already present in context or another tool result",
70
+ "2. `memory_expand` with the most relevant `record_id` to walk typed causal and 6W1H hop edges",
71
+ hasGet
72
+ ? "3. Follow interesting edges; use `memory_get` for exact detail on connected records"
73
+ : "3. Use the typed edge snippets to decide whether the connected records answer the question",
74
+ ...(hasUserCard ? ["4. Use `get_user_card` to cross-reference identity context"] : []),
75
+ ];
76
+ lines.push("### Causal Graph Traversal", "When the user asks about causes, patterns, or relationships:", ...traversalSteps, "");
62
77
  }
63
78
  lines.push("LibraVDB memory is vector-backed and retrieved through tools, not files.", "");
64
79
  return lines;
@@ -26,6 +26,13 @@ type MemoryExpandDetails = {
26
26
  truncated: boolean;
27
27
  exceededBudget: boolean;
28
28
  parentCount: number;
29
+ connected?: Array<{
30
+ recordId: string;
31
+ text: string;
32
+ depth: number;
33
+ edgeType: string;
34
+ edgeWeight: number;
35
+ }>;
29
36
  error?: string;
30
37
  };
31
38
  type MemoryGrepDetails = {
@@ -199,8 +199,6 @@ export function createMemoryExpandTool(getClient, getSessionKey, logger = consol
199
199
  "Graph mode (record_id): walk causal edges (why_ids/how_ids/hop_targets) " +
200
200
  "from a record ID. Use exact IDs from memory_search or memory_get results — " +
201
201
  "any ingested turn, memory, or summary has graph edges. " +
202
- "After get_user_card for context, search for related people/events then " +
203
- "expand the most relevant hit. " +
204
202
  "For large expansions, spawns a sub-agent. " +
205
203
  "Use memory_describe first to check if expansion is warranted.",
206
204
  parameters: MEMORY_EXPAND_SCHEMA,
@@ -218,16 +216,22 @@ export function createMemoryExpandTool(getClient, getSessionKey, logger = consol
218
216
  const client = await getClient();
219
217
  const resp = await client.expandSummary({ recordId, maxDepth });
220
218
  let text = resp.text ?? "";
221
- const connected = resp.connected;
222
- if (connected && connected.length > 0) {
223
- text = connected.map((c) => `[depth=${c.depth}] ${c.recordId}: ${c.text || ""}`).join("\n\n");
219
+ const connected = (resp.connected ?? []).map((c) => ({
220
+ recordId: c.recordId,
221
+ text: c.text ?? "",
222
+ depth: c.depth,
223
+ edgeType: c.edgeType || "unknown",
224
+ edgeWeight: c.edgeWeight ?? 0,
225
+ }));
226
+ if (connected.length > 0) {
227
+ text = connected.map((c) => `[depth=${c.depth} edge=${c.edgeType} weight=${c.edgeWeight}] ${c.recordId}: ${c.text}`).join("\n\n");
224
228
  }
225
229
  if (!text && resp.whyIds?.length) {
226
230
  text = `why_ids: ${resp.whyIds.join(", ")}\nhow_ids: ${resp.howIds?.join(", ") ?? "none"}\nhop_targets: ${resp.hopTargets?.join(", ") ?? "none"}`;
227
231
  }
228
232
  return {
229
233
  content: [{ type: "text", text: text || "(no graph edges found)" }],
230
- details: { summaryId: recordId, depth: maxDepth, text: text || "", truncated: false, exceededBudget: false, parentCount: connected?.length ?? 0 },
234
+ details: { summaryId: recordId, depth: maxDepth, text: text || "", truncated: false, exceededBudget: false, parentCount: connected.length, connected },
231
235
  };
232
236
  }
233
237
  catch (error) {
@@ -2,7 +2,7 @@
2
2
  "id": "libravdb-memory",
3
3
  "name": "LibraVDB Memory",
4
4
  "description": "Cognitive memory engine — causal graph reasoning, predictive context, identity tracking, and hybrid vector recall with back-door adjustment scoring",
5
- "version": "1.10.13",
5
+ "version": "1.10.14-beta.2",
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.10.13",
3
+ "version": "1.10.14-beta.2",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",