@xdarkicex/openclaw-memory-libravdb 1.10.13 → 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 +170 -24
- package/dist/index.js +192 -47
- 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
|
@@ -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();
|
|
@@ -1750,11 +1860,15 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
|
|
|
1750
1860
|
try {
|
|
1751
1861
|
const client = await runtime.getClient();
|
|
1752
1862
|
const threshold = getDynamicCompactThreshold(args.tokenBudget);
|
|
1753
|
-
|
|
1863
|
+
const result = normalizeCompactResult(await client.compactSession(request), {
|
|
1754
1864
|
tokensBefore: args.currentTokenCount,
|
|
1755
1865
|
logger,
|
|
1756
1866
|
...(threshold != null ? { threshold } : {}),
|
|
1757
1867
|
});
|
|
1868
|
+
if (result.ok && result.compacted) {
|
|
1869
|
+
activateCompactedProjection(args.sessionId, "compact");
|
|
1870
|
+
}
|
|
1871
|
+
return result;
|
|
1758
1872
|
}
|
|
1759
1873
|
catch (error) {
|
|
1760
1874
|
return {
|
|
@@ -1814,6 +1928,7 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
|
|
|
1814
1928
|
const sessionId = requireSessionId(args.sessionId, "bootstrap");
|
|
1815
1929
|
predictiveContextCache.delete(sessionId);
|
|
1816
1930
|
postToolRecallCache.delete(sessionId);
|
|
1931
|
+
turnCache.invalidateSession(sessionId);
|
|
1817
1932
|
asyncIngestionQueues.delete(sessionId);
|
|
1818
1933
|
const userId = resolveUserId({
|
|
1819
1934
|
userIdOverride: args.userId,
|
|
@@ -1856,6 +1971,7 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
|
|
|
1856
1971
|
},
|
|
1857
1972
|
async assemble(args) {
|
|
1858
1973
|
const sessionId = requireSessionId(args.sessionId, "assemble");
|
|
1974
|
+
let compactionProjectionActive = compactedProjectionSessions.has(sessionId);
|
|
1859
1975
|
const userId = resolveUserId({
|
|
1860
1976
|
userIdOverride: args.userId,
|
|
1861
1977
|
sessionKey: args.sessionKey,
|
|
@@ -1865,10 +1981,22 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
|
|
|
1865
1981
|
// Processing the full history on every turn is O(N²) and the
|
|
1866
1982
|
// primary source of growing turn latency.
|
|
1867
1983
|
const normalizeWindow = 50;
|
|
1868
|
-
const recentMessages = args.messages
|
|
1869
|
-
? args.messages.slice(-normalizeWindow)
|
|
1870
|
-
: args.messages;
|
|
1984
|
+
const recentMessages = selectTurnAlignedSourceSuffix(args.messages, normalizeWindow);
|
|
1871
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
|
+
};
|
|
1872
2000
|
const strippedPrompt = args.prompt
|
|
1873
2001
|
? normalizeKernelContent(args.prompt, { retainOpenClawContext: false })
|
|
1874
2002
|
: "";
|
|
@@ -1919,10 +2047,15 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
|
|
|
1919
2047
|
compacted: compactionResult.compacted,
|
|
1920
2048
|
reason: compactionResult.reason,
|
|
1921
2049
|
});
|
|
1922
|
-
|
|
2050
|
+
compactionProjectionActive = compactedProjectionSessions.has(sessionId);
|
|
2051
|
+
if (!compactionResult.ok && !compactionProjectionActive) {
|
|
1923
2052
|
logger.info?.(`LibraVDB predictive compaction blocked assemble path at ${currentContextTokens} tokens ` +
|
|
1924
2053
|
`(threshold=${dynamicCompactThreshold}): ${compactionResult.reason ?? "compaction failed"}`);
|
|
1925
|
-
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.");
|
|
1926
2059
|
}
|
|
1927
2060
|
}
|
|
1928
2061
|
// BeforeTurnKernel: semantic memory retrieval against the current user query.
|
|
@@ -1976,8 +2109,13 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
|
|
|
1976
2109
|
}
|
|
1977
2110
|
}
|
|
1978
2111
|
if (cachedSystemPrompt !== undefined) {
|
|
1979
|
-
|
|
1980
|
-
|
|
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);
|
|
1981
2119
|
}
|
|
1982
2120
|
else {
|
|
1983
2121
|
// Drain pending async ingestion for this session so the daemon's
|
|
@@ -2038,7 +2176,13 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
|
|
|
2038
2176
|
}),
|
|
2039
2177
|
new Promise((_, reject) => setTimeout(() => reject(new Error(`AssembleContextInternal timed out after ${assembleTimeout}ms`)), assembleTimeout)),
|
|
2040
2178
|
]);
|
|
2041
|
-
|
|
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);
|
|
2042
2186
|
const continuityContext = await injectContinuityContext({
|
|
2043
2187
|
client,
|
|
2044
2188
|
userId,
|
|
@@ -2070,13 +2214,13 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
|
|
|
2070
2214
|
withContext = { ...withContext, systemPromptAddition: appendSystemPromptAddition(withContext.systemPromptAddition, rulesContext) };
|
|
2071
2215
|
}
|
|
2072
2216
|
}
|
|
2073
|
-
enforced =
|
|
2217
|
+
enforced = enforceAssembleBudget(await augmentWithExactRecall(withContext, {
|
|
2074
2218
|
queryText: retrievalQuery,
|
|
2075
2219
|
userId,
|
|
2076
2220
|
sessionId,
|
|
2077
2221
|
tokenBudget: args.tokenBudget,
|
|
2078
2222
|
reservedTokens: reservedCurrentTurnTokens,
|
|
2079
|
-
}), args.tokenBudget);
|
|
2223
|
+
}), args.tokenBudget, compactionProjectionActive);
|
|
2080
2224
|
const predictions = predictiveContextCache.get(sessionId) || [];
|
|
2081
2225
|
predictiveContextCache.delete(sessionId);
|
|
2082
2226
|
if (predictions.length > 0) {
|
|
@@ -2128,14 +2272,15 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
|
|
|
2128
2272
|
systemPromptAddition: enforced.systemPromptAddition,
|
|
2129
2273
|
});
|
|
2130
2274
|
}
|
|
2131
|
-
enforced =
|
|
2132
|
-
//
|
|
2133
|
-
//
|
|
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.
|
|
2134
2279
|
return ensureReplaySafeUserTurn(enforced, args.messages, logger, args.tokenBudget);
|
|
2135
2280
|
}
|
|
2136
2281
|
catch (error) {
|
|
2137
2282
|
logger.warn?.(`LibraVDB assemble failed, using budget-clamped fallback context: ${error instanceof Error ? error.message : String(error)}`);
|
|
2138
|
-
return ensureReplaySafeUserTurn(
|
|
2283
|
+
return ensureReplaySafeUserTurn(buildAssembleFallback(), args.messages, logger, args.tokenBudget);
|
|
2139
2284
|
}
|
|
2140
2285
|
},
|
|
2141
2286
|
async compact(args) {
|
|
@@ -2343,6 +2488,7 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
|
|
|
2343
2488
|
}
|
|
2344
2489
|
}
|
|
2345
2490
|
predictiveContextCache.clear();
|
|
2491
|
+
compactedProjectionSessions.clear();
|
|
2346
2492
|
postToolRecallCache.clear();
|
|
2347
2493
|
asyncIngestionQueues.clear();
|
|
2348
2494
|
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() {
|
|
@@ -18923,7 +18923,7 @@ function normalizeCompactResult(response, options = {}) {
|
|
|
18923
18923
|
const threshold = options.threshold;
|
|
18924
18924
|
const overBudget = threshold != null && tokensBefore >= threshold;
|
|
18925
18925
|
const engineRefused = !didCompact && overBudget;
|
|
18926
|
-
const tokensAfter = didCompact && typeof response?.tokensAfter === "number" && response.tokensAfter
|
|
18926
|
+
const tokensAfter = didCompact && typeof response?.tokensAfter === "number" && response.tokensAfter >= 0 ? response.tokensAfter : void 0;
|
|
18927
18927
|
return {
|
|
18928
18928
|
ok: !engineRefused,
|
|
18929
18929
|
compacted: didCompact,
|
|
@@ -18931,8 +18931,7 @@ function normalizeCompactResult(response, options = {}) {
|
|
|
18931
18931
|
result: {
|
|
18932
18932
|
tokensBefore,
|
|
18933
18933
|
...tokensAfter != null ? { tokensAfter } : {},
|
|
18934
|
-
...details.
|
|
18935
|
-
...details.summaryText ? { summaryText: details.summaryText } : {},
|
|
18934
|
+
...details.summaryText ? { summary: details.summaryText } : {},
|
|
18936
18935
|
details: { ...details, ...threshold != null ? { threshold } : {} }
|
|
18937
18936
|
}
|
|
18938
18937
|
};
|
|
@@ -19011,6 +19010,18 @@ function findLastUserMessageIndex(messages) {
|
|
|
19011
19010
|
}
|
|
19012
19011
|
return -1;
|
|
19013
19012
|
}
|
|
19013
|
+
function selectTurnAlignedSourceSuffix(messages, maxMessages) {
|
|
19014
|
+
if (messages.length === 0) return [];
|
|
19015
|
+
let start = Math.max(0, messages.length - Math.max(1, Math.floor(maxMessages)));
|
|
19016
|
+
while (start > 0 && messages[start]?.role !== "user") {
|
|
19017
|
+
start -= 1;
|
|
19018
|
+
}
|
|
19019
|
+
if (messages[start]?.role !== "user") {
|
|
19020
|
+
const firstUserIndex = messages.findIndex((message) => message.role === "user");
|
|
19021
|
+
start = firstUserIndex >= 0 ? firstUserIndex : 0;
|
|
19022
|
+
}
|
|
19023
|
+
return messages.slice(start);
|
|
19024
|
+
}
|
|
19014
19025
|
function normalizeKernelContent(content, options = {}) {
|
|
19015
19026
|
const text = typeof content === "string" ? content : Array.isArray(content) ? content.map(stringifyKernelBlock).filter((part) => part.length > 0).join("\n") : "";
|
|
19016
19027
|
return stripOpenClawUntrustedMetadataEnvelope(text, {
|
|
@@ -19374,6 +19385,32 @@ function trimMessagesToBudget(messages, tokenBudget) {
|
|
|
19374
19385
|
}
|
|
19375
19386
|
return [{ ...last, content: truncated }];
|
|
19376
19387
|
}
|
|
19388
|
+
function trimTurnAlignedSourceSuffixToBudget(messages, tokenBudget) {
|
|
19389
|
+
if (messages.length === 0) return [];
|
|
19390
|
+
const userStarts = [];
|
|
19391
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
19392
|
+
if (messages[index]?.role === "user") userStarts.push(index);
|
|
19393
|
+
}
|
|
19394
|
+
if (userStarts.length === 0) {
|
|
19395
|
+
return messages;
|
|
19396
|
+
}
|
|
19397
|
+
const keptBundles = [];
|
|
19398
|
+
let used = 0;
|
|
19399
|
+
let bundleEnd = messages.length;
|
|
19400
|
+
for (let index = userStarts.length - 1; index >= 0; index -= 1) {
|
|
19401
|
+
const bundleStart = userStarts[index];
|
|
19402
|
+
const bundle = messages.slice(bundleStart, bundleEnd);
|
|
19403
|
+
const bundleCost = approximateMessagesTokens(bundle);
|
|
19404
|
+
if (keptBundles.length > 0 && used + bundleCost > tokenBudget) {
|
|
19405
|
+
break;
|
|
19406
|
+
}
|
|
19407
|
+
keptBundles.unshift(bundle);
|
|
19408
|
+
used += bundleCost;
|
|
19409
|
+
bundleEnd = bundleStart;
|
|
19410
|
+
if (used >= tokenBudget) break;
|
|
19411
|
+
}
|
|
19412
|
+
return keptBundles.flat();
|
|
19413
|
+
}
|
|
19377
19414
|
function boundAfterTurnMessagesForIngest(messages, logger, sessionId) {
|
|
19378
19415
|
const estimatedTokens = approximateMessagesTokens(messages);
|
|
19379
19416
|
if (estimatedTokens <= AFTER_TURN_INGEST_MAX_TOKENS) {
|
|
@@ -19414,6 +19451,34 @@ function enforceTokenBudgetInvariant(result, tokenBudget) {
|
|
|
19414
19451
|
estimatedTokens: Math.min(effectiveBudget, systemPromptTokens + trimmedEstimate)
|
|
19415
19452
|
};
|
|
19416
19453
|
}
|
|
19454
|
+
function enforceCompactedProjectionBudgetInvariant(result, tokenBudget) {
|
|
19455
|
+
if (typeof tokenBudget !== "number" || !Number.isFinite(tokenBudget) || tokenBudget <= 0) {
|
|
19456
|
+
return result;
|
|
19457
|
+
}
|
|
19458
|
+
const effectiveBudget = resolveEffectiveAssembleBudget(Math.max(1, Math.floor(tokenBudget)));
|
|
19459
|
+
const approximateTotal = approximateTokenCount(result.systemPromptAddition) + approximateMessagesTokens(result.messages);
|
|
19460
|
+
if (result.estimatedTokens <= effectiveBudget && approximateTotal <= effectiveBudget) {
|
|
19461
|
+
return result;
|
|
19462
|
+
}
|
|
19463
|
+
const lastUserIndex = findLastUserMessageIndex(result.messages);
|
|
19464
|
+
const mandatoryMessages = lastUserIndex >= 0 ? result.messages.slice(lastUserIndex) : result.messages;
|
|
19465
|
+
const mandatoryTokens = approximateMessagesTokens(mandatoryMessages);
|
|
19466
|
+
const systemPromptBudget = Math.max(0, effectiveBudget - mandatoryTokens);
|
|
19467
|
+
const systemPromptAddition = truncateSystemPromptAdditionToTokenBudget(
|
|
19468
|
+
result.systemPromptAddition,
|
|
19469
|
+
systemPromptBudget
|
|
19470
|
+
);
|
|
19471
|
+
const systemPromptTokens = approximateTokenCount(systemPromptAddition);
|
|
19472
|
+
const messageBudget = Math.max(mandatoryTokens, effectiveBudget - systemPromptTokens);
|
|
19473
|
+
const messages = trimTurnAlignedSourceSuffixToBudget(result.messages, messageBudget);
|
|
19474
|
+
const estimatedTokens = systemPromptTokens + approximateMessagesTokens(messages);
|
|
19475
|
+
return {
|
|
19476
|
+
...result,
|
|
19477
|
+
messages,
|
|
19478
|
+
systemPromptAddition,
|
|
19479
|
+
estimatedTokens
|
|
19480
|
+
};
|
|
19481
|
+
}
|
|
19417
19482
|
function buildBudgetFallbackContext(messages, tokenBudget) {
|
|
19418
19483
|
const effectiveBudget = resolveEffectiveAssembleBudget(tokenBudget);
|
|
19419
19484
|
const fallbackMessages = trimMessagesToBudget(
|
|
@@ -19430,7 +19495,11 @@ function buildBudgetFallbackContext(messages, tokenBudget) {
|
|
|
19430
19495
|
var DAEMON_AUTHORED_CONTEXT_RE = /<authored_context\b[^>]*>([\s\S]*?)<\/authored_context>/gi;
|
|
19431
19496
|
var DAEMON_AUTHORED_CONTEXT_GUIDANCE_RE = /^\s*Treat the authored entries below as active project rules and identity context\.?\s*$/i;
|
|
19432
19497
|
var COMPACTED_SESSION_CONTEXT_RE = /<compacted_session_context\b([^>]*)>([\s\S]*?)<\/compacted_session_context>/gi;
|
|
19498
|
+
var HAS_COMPACTED_SESSION_CONTEXT_RE = /<compacted_session_context\b/i;
|
|
19433
19499
|
var COMPACTED_SESSION_RENDER_LEDGER_RE = /(?:^|\n)(?:Artifacts:|Constraints:|Open Next Steps:|Extracted context anchors:)(?:\n|$)/;
|
|
19500
|
+
function hasCompactedSessionContext(text) {
|
|
19501
|
+
return HAS_COMPACTED_SESSION_CONTEXT_RE.test(text);
|
|
19502
|
+
}
|
|
19434
19503
|
function sanitizeDaemonSystemPromptAddition(text) {
|
|
19435
19504
|
return demoteDaemonAuthoredContextBlocks(
|
|
19436
19505
|
sanitizeToolCallPatterns(canonicalizeCompactedSessionContextBlocks(text))
|
|
@@ -19781,13 +19850,13 @@ function ensureReplaySafeUserTurn(assembled, sourceMessages, logger, tokenBudget
|
|
|
19781
19850
|
estimatedTokens: baseEstimatedTokens + approximateMessageTokens(fallbackUser)
|
|
19782
19851
|
};
|
|
19783
19852
|
}
|
|
19784
|
-
function normalizeAssembleResult(result, sourceMessages) {
|
|
19853
|
+
function normalizeAssembleResult(result, sourceMessages, promptAuthority = PROMPT_AUTHORITY_PREASSEMBLY_MAY_OVERFLOW) {
|
|
19785
19854
|
const systemPromptAddition = typeof result.systemPromptAddition === "string" ? sanitizeDaemonSystemPromptAddition(result.systemPromptAddition) : "";
|
|
19786
19855
|
return {
|
|
19787
19856
|
messages: sourceMessages ?? [],
|
|
19788
19857
|
estimatedTokens: typeof result.estimatedTokens === "number" ? result.estimatedTokens : 0,
|
|
19789
19858
|
systemPromptAddition,
|
|
19790
|
-
promptAuthority
|
|
19859
|
+
promptAuthority,
|
|
19791
19860
|
...result.debug != null ? { debug: result.debug } : {}
|
|
19792
19861
|
};
|
|
19793
19862
|
}
|
|
@@ -19858,6 +19927,7 @@ function buildContextEngineFactory(runtime, cfg, logger = console) {
|
|
|
19858
19927
|
setOptimizationMemoCacheSize(cfg.optimizationMemoCacheSize);
|
|
19859
19928
|
}
|
|
19860
19929
|
const predictiveContextCache = /* @__PURE__ */ new Map();
|
|
19930
|
+
const compactedProjectionSessions = /* @__PURE__ */ new Set();
|
|
19861
19931
|
const PREDICTIVE_CACHE_MAX_SIZE = 100;
|
|
19862
19932
|
const turnCache = new TurnMemoryCache(100);
|
|
19863
19933
|
const circuitBreakers = /* @__PURE__ */ new Map();
|
|
@@ -19865,6 +19935,21 @@ function buildContextEngineFactory(runtime, cfg, logger = console) {
|
|
|
19865
19935
|
let lastUserMessageHash = null;
|
|
19866
19936
|
let cachedIdentity = null;
|
|
19867
19937
|
let cachedSessionKey;
|
|
19938
|
+
function activateCompactedProjection(sessionId, source) {
|
|
19939
|
+
const wasActive = compactedProjectionSessions.has(sessionId);
|
|
19940
|
+
compactedProjectionSessions.add(sessionId);
|
|
19941
|
+
predictiveContextCache.delete(sessionId);
|
|
19942
|
+
postToolRecallCache.delete(sessionId);
|
|
19943
|
+
turnCache.invalidateSession(sessionId);
|
|
19944
|
+
if (!wasActive) {
|
|
19945
|
+
logger.info?.(
|
|
19946
|
+
`LibraVDB activated daemon-owned compacted projection sessionId=${sessionId} source=${source}`
|
|
19947
|
+
);
|
|
19948
|
+
}
|
|
19949
|
+
}
|
|
19950
|
+
function enforceAssembleBudget(result, tokenBudget, compactionProjectionActive) {
|
|
19951
|
+
return compactionProjectionActive ? enforceCompactedProjectionBudgetInvariant(result, tokenBudget) : enforceTokenBudgetInvariant(result, tokenBudget);
|
|
19952
|
+
}
|
|
19868
19953
|
function resolveUserId(args) {
|
|
19869
19954
|
const fwUserId = args?.userIdOverride?.trim();
|
|
19870
19955
|
if (fwUserId) return fwUserId;
|
|
@@ -20302,11 +20387,15 @@ ${cached}
|
|
|
20302
20387
|
try {
|
|
20303
20388
|
const client = await runtime.getClient();
|
|
20304
20389
|
const threshold = getDynamicCompactThreshold(args.tokenBudget);
|
|
20305
|
-
|
|
20390
|
+
const result = normalizeCompactResult(await client.compactSession(request3), {
|
|
20306
20391
|
tokensBefore: args.currentTokenCount,
|
|
20307
20392
|
logger,
|
|
20308
20393
|
...threshold != null ? { threshold } : {}
|
|
20309
20394
|
});
|
|
20395
|
+
if (result.ok && result.compacted) {
|
|
20396
|
+
activateCompactedProjection(args.sessionId, "compact");
|
|
20397
|
+
}
|
|
20398
|
+
return result;
|
|
20310
20399
|
} catch (error2) {
|
|
20311
20400
|
return {
|
|
20312
20401
|
ok: false,
|
|
@@ -20363,6 +20452,7 @@ ${cached}
|
|
|
20363
20452
|
const sessionId = requireSessionId(args.sessionId, "bootstrap");
|
|
20364
20453
|
predictiveContextCache.delete(sessionId);
|
|
20365
20454
|
postToolRecallCache.delete(sessionId);
|
|
20455
|
+
turnCache.invalidateSession(sessionId);
|
|
20366
20456
|
asyncIngestionQueues.delete(sessionId);
|
|
20367
20457
|
const userId = resolveUserId({
|
|
20368
20458
|
userIdOverride: args.userId,
|
|
@@ -20406,13 +20496,26 @@ ${cached}
|
|
|
20406
20496
|
},
|
|
20407
20497
|
async assemble(args) {
|
|
20408
20498
|
const sessionId = requireSessionId(args.sessionId, "assemble");
|
|
20499
|
+
let compactionProjectionActive = compactedProjectionSessions.has(sessionId);
|
|
20409
20500
|
const userId = resolveUserId({
|
|
20410
20501
|
userIdOverride: args.userId,
|
|
20411
20502
|
sessionKey: args.sessionKey
|
|
20412
20503
|
});
|
|
20413
20504
|
const normalizeWindow = 50;
|
|
20414
|
-
const recentMessages = args.messages
|
|
20505
|
+
const recentMessages = selectTurnAlignedSourceSuffix(args.messages, normalizeWindow);
|
|
20415
20506
|
const messages = normalizeKernelMessages(recentMessages);
|
|
20507
|
+
const projectedSourceMessages = () => compactionProjectionActive ? recentMessages : args.messages;
|
|
20508
|
+
const buildAssembleFallback = () => {
|
|
20509
|
+
if (!compactionProjectionActive) {
|
|
20510
|
+
return buildBudgetFallbackContext(args.messages, args.tokenBudget);
|
|
20511
|
+
}
|
|
20512
|
+
return enforceCompactedProjectionBudgetInvariant({
|
|
20513
|
+
messages: projectedSourceMessages(),
|
|
20514
|
+
estimatedTokens: approximateMessagesTokens(projectedSourceMessages()),
|
|
20515
|
+
systemPromptAddition: "",
|
|
20516
|
+
promptAuthority: "assembled"
|
|
20517
|
+
}, args.tokenBudget);
|
|
20518
|
+
};
|
|
20416
20519
|
const strippedPrompt = args.prompt ? normalizeKernelContent(args.prompt, { retainOpenClawContext: false }) : "";
|
|
20417
20520
|
const lastUserIndex = findLastUserMessageIndex(messages);
|
|
20418
20521
|
const isPostToolContinuation = lastUserIndex >= 0 && lastUserIndex < messages.length - 1 && hasLiveToolProtocolAfterLastUser(messages, lastUserIndex);
|
|
@@ -20458,17 +20561,23 @@ ${cached}
|
|
|
20458
20561
|
compacted: compactionResult.compacted,
|
|
20459
20562
|
reason: compactionResult.reason
|
|
20460
20563
|
});
|
|
20461
|
-
|
|
20564
|
+
compactionProjectionActive = compactedProjectionSessions.has(sessionId);
|
|
20565
|
+
if (!compactionResult.ok && !compactionProjectionActive) {
|
|
20462
20566
|
logger.info?.(
|
|
20463
20567
|
`LibraVDB predictive compaction blocked assemble path at ${currentContextTokens} tokens (threshold=${dynamicCompactThreshold}): ${compactionResult.reason ?? "compaction failed"}`
|
|
20464
20568
|
);
|
|
20465
20569
|
return ensureReplaySafeUserTurn(
|
|
20466
|
-
|
|
20570
|
+
buildAssembleFallback(),
|
|
20467
20571
|
args.messages,
|
|
20468
20572
|
logger,
|
|
20469
20573
|
args.tokenBudget
|
|
20470
20574
|
);
|
|
20471
20575
|
}
|
|
20576
|
+
if (!compactionResult.ok) {
|
|
20577
|
+
logger.info?.(
|
|
20578
|
+
`LibraVDB predictive compaction made no additional progress at ${currentContextTokens} tokens; continuing with the existing daemon-owned compacted projection.`
|
|
20579
|
+
);
|
|
20580
|
+
}
|
|
20472
20581
|
}
|
|
20473
20582
|
const btLog = cfg.beforeTurnDebug ? (msg) => logger.info?.(msg) : (_msg) => {
|
|
20474
20583
|
};
|
|
@@ -20515,10 +20624,20 @@ ${cached}
|
|
|
20515
20624
|
}
|
|
20516
20625
|
}
|
|
20517
20626
|
if (cachedSystemPrompt !== void 0) {
|
|
20518
|
-
|
|
20519
|
-
|
|
20520
|
-
|
|
20521
|
-
|
|
20627
|
+
if (hasCompactedSessionContext(cachedSystemPrompt)) {
|
|
20628
|
+
activateCompactedProjection(sessionId, "assemble");
|
|
20629
|
+
compactionProjectionActive = true;
|
|
20630
|
+
}
|
|
20631
|
+
const sourceProjection = projectedSourceMessages();
|
|
20632
|
+
const mockResp = { messages: sourceProjection, systemPromptAddition: cachedSystemPrompt };
|
|
20633
|
+
enforced = enforceAssembleBudget(
|
|
20634
|
+
normalizeAssembleResult(
|
|
20635
|
+
mockResp,
|
|
20636
|
+
sourceProjection,
|
|
20637
|
+
compactionProjectionActive ? "assembled" : PROMPT_AUTHORITY_PREASSEMBLY_MAY_OVERFLOW
|
|
20638
|
+
),
|
|
20639
|
+
args.tokenBudget,
|
|
20640
|
+
compactionProjectionActive
|
|
20522
20641
|
);
|
|
20523
20642
|
} else {
|
|
20524
20643
|
const pending = asyncIngestionQueues.get(sessionId);
|
|
@@ -20580,7 +20699,16 @@ ${cached}
|
|
|
20580
20699
|
(_, reject) => setTimeout(() => reject(new Error(`AssembleContextInternal timed out after ${assembleTimeout}ms`)), assembleTimeout)
|
|
20581
20700
|
)
|
|
20582
20701
|
]);
|
|
20583
|
-
|
|
20702
|
+
if (typeof resp.systemPromptAddition === "string" && hasCompactedSessionContext(resp.systemPromptAddition)) {
|
|
20703
|
+
activateCompactedProjection(sessionId, "assemble");
|
|
20704
|
+
compactionProjectionActive = true;
|
|
20705
|
+
}
|
|
20706
|
+
const sourceProjection = projectedSourceMessages();
|
|
20707
|
+
const assembled = normalizeAssembleResult(
|
|
20708
|
+
resp,
|
|
20709
|
+
sourceProjection,
|
|
20710
|
+
compactionProjectionActive ? "assembled" : PROMPT_AUTHORITY_PREASSEMBLY_MAY_OVERFLOW
|
|
20711
|
+
);
|
|
20584
20712
|
const continuityContext = await injectContinuityContext({
|
|
20585
20713
|
client,
|
|
20586
20714
|
userId,
|
|
@@ -20609,7 +20737,7 @@ ${cached}
|
|
|
20609
20737
|
withContext = { ...withContext, systemPromptAddition: appendSystemPromptAddition(withContext.systemPromptAddition, rulesContext) };
|
|
20610
20738
|
}
|
|
20611
20739
|
}
|
|
20612
|
-
enforced =
|
|
20740
|
+
enforced = enforceAssembleBudget(
|
|
20613
20741
|
await augmentWithExactRecall(withContext, {
|
|
20614
20742
|
queryText: retrievalQuery,
|
|
20615
20743
|
userId,
|
|
@@ -20617,7 +20745,8 @@ ${cached}
|
|
|
20617
20745
|
tokenBudget: args.tokenBudget,
|
|
20618
20746
|
reservedTokens: reservedCurrentTurnTokens
|
|
20619
20747
|
}),
|
|
20620
|
-
args.tokenBudget
|
|
20748
|
+
args.tokenBudget,
|
|
20749
|
+
compactionProjectionActive
|
|
20621
20750
|
);
|
|
20622
20751
|
const predictions = predictiveContextCache.get(sessionId) || [];
|
|
20623
20752
|
predictiveContextCache.delete(sessionId);
|
|
@@ -20674,9 +20803,10 @@ ${cached}
|
|
|
20674
20803
|
systemPromptAddition: enforced.systemPromptAddition
|
|
20675
20804
|
});
|
|
20676
20805
|
}
|
|
20677
|
-
enforced =
|
|
20806
|
+
enforced = enforceAssembleBudget(
|
|
20678
20807
|
enforced,
|
|
20679
|
-
args.tokenBudget
|
|
20808
|
+
args.tokenBudget,
|
|
20809
|
+
compactionProjectionActive
|
|
20680
20810
|
);
|
|
20681
20811
|
return ensureReplaySafeUserTurn(enforced, args.messages, logger, args.tokenBudget);
|
|
20682
20812
|
} catch (error2) {
|
|
@@ -20684,7 +20814,7 @@ ${cached}
|
|
|
20684
20814
|
`LibraVDB assemble failed, using budget-clamped fallback context: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
20685
20815
|
);
|
|
20686
20816
|
return ensureReplaySafeUserTurn(
|
|
20687
|
-
|
|
20817
|
+
buildAssembleFallback(),
|
|
20688
20818
|
args.messages,
|
|
20689
20819
|
logger,
|
|
20690
20820
|
args.tokenBudget
|
|
@@ -20872,6 +21002,7 @@ ${cached}
|
|
|
20872
21002
|
}
|
|
20873
21003
|
}
|
|
20874
21004
|
predictiveContextCache.clear();
|
|
21005
|
+
compactedProjectionSessions.clear();
|
|
20875
21006
|
postToolRecallCache.clear();
|
|
20876
21007
|
asyncIngestionQueues.clear();
|
|
20877
21008
|
triggerCache.clear();
|
|
@@ -31194,6 +31325,12 @@ function buildToolGuidance(availableTools) {
|
|
|
31194
31325
|
return [];
|
|
31195
31326
|
}
|
|
31196
31327
|
const lines = [];
|
|
31328
|
+
const hasSearch = availableTools.has("memory_search");
|
|
31329
|
+
const hasGet = availableTools.has("memory_get");
|
|
31330
|
+
const hasDescribe = availableTools.has("memory_describe");
|
|
31331
|
+
const hasExpand = availableTools.has("memory_expand");
|
|
31332
|
+
const hasGrep = availableTools.has("memory_grep");
|
|
31333
|
+
const hasUserCard = availableTools.has("get_user_card");
|
|
31197
31334
|
const hasGetCard = availableTools.has("get_user_card");
|
|
31198
31335
|
const hasListCards = availableTools.has("list_user_cards");
|
|
31199
31336
|
if (hasGetCard || hasListCards) {
|
|
@@ -31204,7 +31341,7 @@ function buildToolGuidance(availableTools) {
|
|
|
31204
31341
|
hasListCards ? "- `list_user_cards()` \u2014 MANDATORY roster check. Call if unsure whether a card exists." : "",
|
|
31205
31342
|
"Cards are the canonical record. You MUST call these tools. Do NOT answer from",
|
|
31206
31343
|
"memory, context, or training data without checking the card first.",
|
|
31207
|
-
"Only use memory_search if the card is empty or missing.",
|
|
31344
|
+
hasSearch ? "Only use memory_search if the card is empty or missing." : "Use the card result as the available identity record.",
|
|
31208
31345
|
"",
|
|
31209
31346
|
"**Autonomous card maintenance:**",
|
|
31210
31347
|
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 +31349,19 @@ function buildToolGuidance(availableTools) {
|
|
|
31212
31349
|
""
|
|
31213
31350
|
);
|
|
31214
31351
|
}
|
|
31215
|
-
|
|
31216
|
-
|
|
31217
|
-
|
|
31218
|
-
|
|
31219
|
-
|
|
31220
|
-
|
|
31221
|
-
|
|
31222
|
-
|
|
31223
|
-
|
|
31224
|
-
|
|
31225
|
-
|
|
31226
|
-
|
|
31227
|
-
|
|
31228
|
-
const hasGrep = availableTools.has("memory_grep");
|
|
31352
|
+
if (hasSearch) {
|
|
31353
|
+
lines.push(
|
|
31354
|
+
"Call `memory_search` once per user question for prior turns, remembered",
|
|
31355
|
+
"facts, earliest interactions, and channel history. Do not answer memory",
|
|
31356
|
+
"questions from prior transcript claims \u2014 perform a search every time.",
|
|
31357
|
+
"After receiving results, use them directly; do not re-call in the same turn.",
|
|
31358
|
+
...hasGet ? [
|
|
31359
|
+
"After a `memory_search` hit, call `memory_get` when exact wording or more context is needed.",
|
|
31360
|
+
"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."
|
|
31361
|
+
] : [],
|
|
31362
|
+
""
|
|
31363
|
+
);
|
|
31364
|
+
}
|
|
31229
31365
|
if (hasDescribe || hasExpand || hasGrep) {
|
|
31230
31366
|
lines.push(
|
|
31231
31367
|
"**Compacted summaries \u2014 recall hierarchy (cheap \u2192 expensive):**",
|
|
@@ -31272,13 +31408,16 @@ function buildToolGuidance(availableTools) {
|
|
|
31272
31408
|
""
|
|
31273
31409
|
);
|
|
31274
31410
|
if (hasExpand) {
|
|
31411
|
+
const traversalSteps = [
|
|
31412
|
+
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",
|
|
31413
|
+
"2. `memory_expand` with the most relevant `record_id` to walk typed causal and 6W1H hop edges",
|
|
31414
|
+
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",
|
|
31415
|
+
...hasUserCard ? ["4. Use `get_user_card` to cross-reference identity context"] : []
|
|
31416
|
+
];
|
|
31275
31417
|
lines.push(
|
|
31276
31418
|
"### Causal Graph Traversal",
|
|
31277
31419
|
"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",
|
|
31420
|
+
...traversalSteps,
|
|
31282
31421
|
""
|
|
31283
31422
|
);
|
|
31284
31423
|
}
|
|
@@ -31466,7 +31605,7 @@ function createMemoryExpandTool(getClient, getSessionKey, logger = console, getS
|
|
|
31466
31605
|
return {
|
|
31467
31606
|
name: "memory_expand",
|
|
31468
31607
|
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.
|
|
31608
|
+
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
31609
|
parameters: MEMORY_EXPAND_SCHEMA,
|
|
31471
31610
|
execute: async (_toolCallId, rawParams) => {
|
|
31472
31611
|
const params = asParams(rawParams);
|
|
@@ -31481,10 +31620,16 @@ function createMemoryExpandTool(getClient, getSessionKey, logger = console, getS
|
|
|
31481
31620
|
const client = await getClient();
|
|
31482
31621
|
const resp = await client.expandSummary({ recordId, maxDepth });
|
|
31483
31622
|
let text = resp.text ?? "";
|
|
31484
|
-
const connected = resp.connected
|
|
31485
|
-
|
|
31623
|
+
const connected = (resp.connected ?? []).map((c) => ({
|
|
31624
|
+
recordId: c.recordId,
|
|
31625
|
+
text: c.text ?? "",
|
|
31626
|
+
depth: c.depth,
|
|
31627
|
+
edgeType: c.edgeType || "unknown",
|
|
31628
|
+
edgeWeight: c.edgeWeight ?? 0
|
|
31629
|
+
}));
|
|
31630
|
+
if (connected.length > 0) {
|
|
31486
31631
|
text = connected.map(
|
|
31487
|
-
(c) => `[depth=${c.depth}] ${c.recordId}: ${c.text
|
|
31632
|
+
(c) => `[depth=${c.depth} edge=${c.edgeType} weight=${c.edgeWeight}] ${c.recordId}: ${c.text}`
|
|
31488
31633
|
).join("\n\n");
|
|
31489
31634
|
}
|
|
31490
31635
|
if (!text && resp.whyIds?.length) {
|
|
@@ -31494,7 +31639,7 @@ hop_targets: ${resp.hopTargets?.join(", ") ?? "none"}`;
|
|
|
31494
31639
|
}
|
|
31495
31640
|
return {
|
|
31496
31641
|
content: [{ type: "text", text: text || "(no graph edges found)" }],
|
|
31497
|
-
details: { summaryId: recordId, depth: maxDepth, text: text || "", truncated: false, exceededBudget: false, parentCount: connected
|
|
31642
|
+
details: { summaryId: recordId, depth: maxDepth, text: text || "", truncated: false, exceededBudget: false, parentCount: connected.length, connected }
|
|
31498
31643
|
};
|
|
31499
31644
|
} catch (error2) {
|
|
31500
31645
|
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.1",
|
|
6
6
|
"kind": [
|
|
7
7
|
"memory",
|
|
8
8
|
"context-engine"
|