@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 +166 -15
- package/dist/context-engine.d.ts +2 -3
- package/dist/context-engine.js +180 -26
- package/dist/index.js +196 -49
- package/dist/manifest.js +4 -4
- package/dist/memory-provider.js +26 -11
- package/dist/tools/memory-recall.d.ts +7 -0
- package/dist/tools/memory-recall.js +10 -6
- package/openclaw.plugin.json +1 -1
- 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
|
@@ -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 = !
|
|
110
|
-
const tokensAfter = didCompact && typeof response?.tokensAfter === "number" && response.tokensAfter
|
|
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
|
-
...(
|
|
124
|
+
...(effectiveCompacted ? {} : { reason: engineRefused ? "overbudget_not_compacted" : "not_compacted" }),
|
|
117
125
|
result: {
|
|
118
126
|
tokensBefore,
|
|
119
127
|
...(tokensAfter != null ? { tokensAfter } : {}),
|
|
120
|
-
...(details.
|
|
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
|
|
1175
|
-
//
|
|
1176
|
-
//
|
|
1177
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
1980
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
2132
|
-
//
|
|
2133
|
-
//
|
|
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(
|
|
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 = !
|
|
18926
|
-
const tokensAfter = didCompact && typeof response?.tokensAfter === "number" && response.tokensAfter
|
|
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
|
-
...
|
|
18932
|
+
...effectiveCompacted ? {} : { reason: engineRefused ? "overbudget_not_compacted" : "not_compacted" },
|
|
18931
18933
|
result: {
|
|
18932
18934
|
tokensBefore,
|
|
18933
18935
|
...tokensAfter != null ? { tokensAfter } : {},
|
|
18934
|
-
...details.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20519
|
-
|
|
20520
|
-
|
|
20521
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
31216
|
-
|
|
31217
|
-
|
|
31218
|
-
|
|
31219
|
-
|
|
31220
|
-
|
|
31221
|
-
|
|
31222
|
-
|
|
31223
|
-
|
|
31224
|
-
|
|
31225
|
-
|
|
31226
|
-
|
|
31227
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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() {
|
package/dist/memory-provider.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
223
|
-
text
|
|
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
|
|
234
|
+
details: { summaryId: recordId, depth: maxDepth, text: text || "", truncated: false, exceededBudget: false, parentCount: connected.length, connected },
|
|
231
235
|
};
|
|
232
236
|
}
|
|
233
237
|
catch (error) {
|
package/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
5
|
+
"version": "1.10.14-beta.2",
|
|
6
6
|
"kind": [
|
|
7
7
|
"memory",
|
|
8
8
|
"context-engine"
|