@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 +166 -15
- package/dist/context-engine.d.ts +2 -3
- package/dist/context-engine.js +196 -26
- package/dist/index.js +269 -47
- package/dist/manifest.js +4 -4
- package/dist/memory-provider.js +26 -11
- package/dist/tools/memory-recall.d.ts +41 -0
- package/dist/tools/memory-recall.js +66 -6
- package/openclaw.plugin.json +4 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,21 +1,69 @@
|
|
|
1
|
-
# ♎ LibraVDB
|
|
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="
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
+
[](https://star-history.com/#xDarkicex/openclaw-memory-libravdb&Date)
|
|
764
|
+
</div>
|
package/dist/context-engine.d.ts
CHANGED
|
@@ -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;
|
package/dist/context-engine.js
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
|
1175
|
-
//
|
|
1176
|
-
//
|
|
1177
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
1960
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
2108
|
-
//
|
|
2109
|
-
//
|
|
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(
|
|
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();
|