@xdarkicex/openclaw-memory-libravdb 1.3.11 → 1.3.13
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 +51 -35
- package/docs/README.md +11 -1
- package/docs/ast-v2.md +125 -0
- package/docs/ast.md +70 -0
- package/docs/compaction-evaluation.md +182 -0
- package/docs/continuity.md +488 -0
- package/docs/contributing.md +1 -1
- package/docs/gating.md +53 -255
- package/docs/install.md +179 -0
- package/docs/installation.md +45 -9
- package/docs/mathematics-v2.md +1228 -0
- package/docs/uninstall.md +100 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/context-engine.ts +306 -35
- package/src/continuity.ts +93 -0
- package/src/index.ts +1 -1
- package/src/openclaw-plugin-sdk.d.ts +2 -2
- package/src/recall-utils.ts +100 -8
- package/src/scoring.ts +263 -9
- package/src/tokens.ts +1 -1
- package/src/types.ts +33 -2
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Uninstall Guide
|
|
2
|
+
|
|
3
|
+
This guide covers safe removal of the OpenClaw / OpenClaw.ai plugin and the
|
|
4
|
+
separately managed `libravdbd` daemon.
|
|
5
|
+
|
|
6
|
+
If you only want to disable the memory replacement temporarily, remove the
|
|
7
|
+
plugin slot assignment first and leave the daemon plus data in place.
|
|
8
|
+
|
|
9
|
+
## 1. Disable the Plugin
|
|
10
|
+
|
|
11
|
+
Remove the plugin from the active OpenClaw slot in `~/.openclaw/openclaw.json`:
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"plugins": {
|
|
16
|
+
"slots": {}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Treat that JSON as a minimal example only. If you assigned `libravdb-memory`
|
|
22
|
+
under `memory` or `contextEngine`, remove just that slot entry and leave any
|
|
23
|
+
other plugin slots intact.
|
|
24
|
+
|
|
25
|
+
If you installed the package through the OpenClaw.ai plugin UI, remove or
|
|
26
|
+
disable the same package there as well. If you use the CLI, remove it through
|
|
27
|
+
your standard OpenClaw plugin removal flow for
|
|
28
|
+
`@xdarkicex/openclaw-memory-libravdb`.
|
|
29
|
+
|
|
30
|
+
## 2. Stop the Daemon
|
|
31
|
+
|
|
32
|
+
Stop the sidecar before deleting binaries or stored data.
|
|
33
|
+
|
|
34
|
+
Homebrew:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
brew services stop libravdbd
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Linux user service:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
systemctl --user disable --now libravdbd.service
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
macOS LaunchAgent:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
launchctl bootout gui/$(id -u)/com.xdarkicex.libravdbd
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Foreground manual run:
|
|
53
|
+
|
|
54
|
+
- stop the `libravdbd serve` process in the terminal where it is running
|
|
55
|
+
|
|
56
|
+
## 3. Remove Installed Assets
|
|
57
|
+
|
|
58
|
+
### Plugin Package
|
|
59
|
+
|
|
60
|
+
Remove the published plugin package from OpenClaw or OpenClaw.ai after it is no
|
|
61
|
+
longer assigned to an active slot.
|
|
62
|
+
|
|
63
|
+
### Homebrew Daemon
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
brew uninstall libravdbd
|
|
67
|
+
brew untap xDarkicex/openclaw-libravdb-memory
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Manual Daemon Install
|
|
71
|
+
|
|
72
|
+
Delete the service file or launch agent you installed, along with the daemon
|
|
73
|
+
binary you copied into place.
|
|
74
|
+
|
|
75
|
+
Common locations:
|
|
76
|
+
|
|
77
|
+
- `~/.config/systemd/user/libravdbd.service`
|
|
78
|
+
- `~/Library/LaunchAgents/com.xdarkicex.libravdbd.plist`
|
|
79
|
+
- `~/.local/bin/libravdbd`
|
|
80
|
+
|
|
81
|
+
## 4. Optional Full Data Cleanup
|
|
82
|
+
|
|
83
|
+
Only do this if you want to permanently remove stored LibraVDB memory.
|
|
84
|
+
|
|
85
|
+
Common local state:
|
|
86
|
+
|
|
87
|
+
- socket directory: `~/.clawdb/run/`
|
|
88
|
+
- database file: `~/.clawdb/data.libravdb`
|
|
89
|
+
|
|
90
|
+
If you configured a custom Unix socket endpoint in `sidecarPath`, remove that
|
|
91
|
+
socket path or containing directory if applicable. If you configured `dbPath`,
|
|
92
|
+
remove that custom database location instead of the default path. TCP
|
|
93
|
+
`sidecarPath` endpoints are not filesystem paths and do not have anything to
|
|
94
|
+
delete during uninstall.
|
|
95
|
+
|
|
96
|
+
## 5. Post-Uninstall Check
|
|
97
|
+
|
|
98
|
+
After cleanup, `openclaw memory status` should no longer show this plugin as the
|
|
99
|
+
active memory provider, and the daemon endpoint should no longer be reachable
|
|
100
|
+
unless you intentionally kept it running for another workflow.
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/src/context-engine.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_CONTINUITY_MIN_TURNS,
|
|
3
|
+
DEFAULT_CONTINUITY_PRIOR_CONTEXT_TOKENS,
|
|
4
|
+
DEFAULT_CONTINUITY_TAIL_BUDGET_TOKENS,
|
|
5
|
+
selectRecentTail,
|
|
6
|
+
} from "./continuity.js";
|
|
7
|
+
import {
|
|
8
|
+
expandSection7HopCandidates,
|
|
9
|
+
mergeSection7VariantCandidates,
|
|
10
|
+
rankSection7VariantCandidates,
|
|
11
|
+
} from "./scoring.js";
|
|
12
|
+
import { buildInjectedMemoryMessageContent, buildMemoryHeader, recentIds } from "./recall-utils.js";
|
|
13
|
+
import { countTokens, estimateTokens, fitPromptBudget } from "./tokens.js";
|
|
4
14
|
import type { RpcGetter } from "./plugin-runtime.js";
|
|
5
15
|
import type {
|
|
6
16
|
ContextAssembleArgs,
|
|
@@ -13,18 +23,43 @@ import type {
|
|
|
13
23
|
SearchResult,
|
|
14
24
|
} from "./types.js";
|
|
15
25
|
|
|
26
|
+
const AUTHORED_HARD_COLLECTION = "authored:hard";
|
|
27
|
+
const AUTHORED_SOFT_COLLECTION = "authored:soft";
|
|
28
|
+
const AUTHORED_VARIANT_COLLECTION = "authored:variant";
|
|
29
|
+
|
|
16
30
|
export function buildContextEngineFactory(
|
|
17
31
|
getRpc: RpcGetter,
|
|
18
32
|
cfg: PluginConfig,
|
|
19
33
|
recallCache: RecallCache<SearchResult>,
|
|
20
34
|
) {
|
|
35
|
+
let authoredHardCache: SearchResult[] | null = null;
|
|
36
|
+
let authoredSoftCache: SearchResult[] | null = null;
|
|
37
|
+
let authoredVariantCache: SearchResult[] | null = null;
|
|
38
|
+
|
|
21
39
|
return {
|
|
22
40
|
ownsCompaction: true,
|
|
23
41
|
async bootstrap({ sessionId, userId }: ContextBootstrapArgs) {
|
|
24
42
|
const rpc = await getRpc();
|
|
25
43
|
await rpc.call("ensure_collections", {
|
|
26
|
-
collections: [
|
|
44
|
+
collections: [
|
|
45
|
+
`session:${sessionId}`,
|
|
46
|
+
`turns:${userId}`,
|
|
47
|
+
`user:${userId}`,
|
|
48
|
+
"global",
|
|
49
|
+
AUTHORED_HARD_COLLECTION,
|
|
50
|
+
AUTHORED_SOFT_COLLECTION,
|
|
51
|
+
AUTHORED_VARIANT_COLLECTION,
|
|
52
|
+
],
|
|
53
|
+
});
|
|
54
|
+
const [authoredHard, authoredSoft, authoredVariantRecords] = await loadAuthoredCollections(rpc, {
|
|
55
|
+
hard: authoredHardCache,
|
|
56
|
+
soft: authoredSoftCache,
|
|
57
|
+
variant: authoredVariantCache,
|
|
27
58
|
});
|
|
59
|
+
authoredHardCache = authoredHard;
|
|
60
|
+
authoredSoftCache = authoredSoft;
|
|
61
|
+
authoredVariantCache = authoredVariantRecords;
|
|
62
|
+
validateSection7StartupHardReserve(cfg, authoredHard);
|
|
28
63
|
return { ok: true };
|
|
29
64
|
},
|
|
30
65
|
async ingest({ sessionId, userId, message, isHeartbeat }: ContextIngestArgs) {
|
|
@@ -62,6 +97,7 @@ export function buildContextEngineFactory(
|
|
|
62
97
|
id: `${userId}:${ts}`,
|
|
63
98
|
text: message.content,
|
|
64
99
|
metadata: {
|
|
100
|
+
role: message.role,
|
|
65
101
|
ts,
|
|
66
102
|
sessionId,
|
|
67
103
|
type: "turn",
|
|
@@ -101,26 +137,96 @@ export function buildContextEngineFactory(
|
|
|
101
137
|
|
|
102
138
|
try {
|
|
103
139
|
const rpc = await getRpc();
|
|
104
|
-
const [
|
|
140
|
+
const [authoredHard, authoredSoft, authoredVariantRecords] = await loadAuthoredCollections(rpc, {
|
|
141
|
+
hard: authoredHardCache,
|
|
142
|
+
soft: authoredSoftCache,
|
|
143
|
+
variant: authoredVariantCache,
|
|
144
|
+
});
|
|
145
|
+
authoredHardCache = authoredHard;
|
|
146
|
+
authoredSoftCache = authoredSoft;
|
|
147
|
+
authoredVariantCache = authoredVariantRecords;
|
|
148
|
+
|
|
149
|
+
const memoryBudget = tokenBudget * (cfg.tokenBudgetFraction ?? 0.25);
|
|
150
|
+
const hardItems = authoredHard;
|
|
151
|
+
const hardUsed = tokenCostSum(hardItems);
|
|
152
|
+
const sessionRecords = await rpc.call<{ results: SearchResult[] }>("list_by_meta", {
|
|
153
|
+
collection: `session:${sessionId}`,
|
|
154
|
+
key: "sessionId",
|
|
155
|
+
value: sessionId,
|
|
156
|
+
});
|
|
157
|
+
const rawSessionTurns = sortChronological(
|
|
158
|
+
sessionRecords.results.filter((item) => item.metadata.type !== "summary"),
|
|
159
|
+
);
|
|
160
|
+
const minTurns = cfg.continuityMinTurns ?? DEFAULT_CONTINUITY_MIN_TURNS;
|
|
161
|
+
const tailTarget = cfg.continuityTailBudgetTokens ?? DEFAULT_CONTINUITY_TAIL_BUDGET_TOKENS;
|
|
162
|
+
const baseTail = selectRecentTail(rawSessionTurns, {
|
|
163
|
+
minTurns,
|
|
164
|
+
tailBudgetTokens: 0,
|
|
165
|
+
tokenCost,
|
|
166
|
+
sameBundle: isContinuityBundleCoupled,
|
|
167
|
+
});
|
|
168
|
+
const baseTailUsed = baseTail.baseTokens;
|
|
169
|
+
const configuredHardFraction = clampFraction(cfg.authoredHardBudgetFraction);
|
|
170
|
+
const hardBudget = configuredHardFraction > 0 ? memoryBudget * configuredHardFraction : hardUsed;
|
|
171
|
+
const degradedReasons: string[] = [];
|
|
172
|
+
if (hardUsed > hardBudget + 1e-9) {
|
|
173
|
+
degradedReasons.push("hard authored invariants exceed configured hard budget reserve");
|
|
174
|
+
}
|
|
175
|
+
if (hardUsed + baseTailUsed > memoryBudget + 1e-9) {
|
|
176
|
+
degradedReasons.push("hard authored invariants plus mandatory recent-tail base exceed available memory budget");
|
|
177
|
+
}
|
|
178
|
+
if (degradedReasons.length > 0) {
|
|
179
|
+
const degradedTail = markRecentTail(baseTail.base, baseTail.base.length);
|
|
180
|
+
const selected = [...hardItems, ...degradedTail];
|
|
181
|
+
const selectedMessages = selected.map((item) => ({
|
|
182
|
+
role: "system",
|
|
183
|
+
content: buildInjectedMemoryMessageContent(item),
|
|
184
|
+
}));
|
|
185
|
+
return {
|
|
186
|
+
messages: [...selectedMessages, ...messages],
|
|
187
|
+
estimatedTokens: countTokens(selectedMessages) + countTokens(messages),
|
|
188
|
+
systemPromptAddition: buildDegradedMemoryHeader(degradedReasons, selected),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
const authoredSoftTarget = Math.max(0, memoryBudget * (cfg.authoredSoftBudgetFraction ?? 0.3));
|
|
192
|
+
const softBudget = Math.max(0, Math.min(authoredSoftTarget, memoryBudget - hardUsed - baseTailUsed));
|
|
193
|
+
const softItems = fitPromptBudget(authoredSoft, softBudget);
|
|
194
|
+
const remainingAfterHardSoft = Math.max(0, memoryBudget - hardUsed - tokenCostSum(softItems));
|
|
195
|
+
const effectiveTailBudget = Math.min(
|
|
196
|
+
Math.max(tailTarget, baseTailUsed),
|
|
197
|
+
remainingAfterHardSoft,
|
|
198
|
+
);
|
|
199
|
+
const recentTailSelection = selectRecentTail(rawSessionTurns, {
|
|
200
|
+
minTurns,
|
|
201
|
+
tailBudgetTokens: effectiveTailBudget,
|
|
202
|
+
tokenCost,
|
|
203
|
+
sameBundle: isContinuityBundleCoupled,
|
|
204
|
+
});
|
|
205
|
+
const recentTail = markRecentTail(
|
|
206
|
+
recentTailSelection.recent,
|
|
207
|
+
recentTailSelection.base.length,
|
|
208
|
+
);
|
|
209
|
+
const tailBaseItems = recentTail.slice(-recentTailSelection.base.length);
|
|
210
|
+
const tailExtensionItems = recentTail.slice(0, Math.max(0, recentTail.length - recentTailSelection.base.length));
|
|
211
|
+
const retrievalBudget = Math.max(0, memoryBudget - hardUsed - tokenCostSum(softItems) - tokenCostSum(recentTail));
|
|
212
|
+
const recentTailIDs = recentTail.map((item) => item.id);
|
|
213
|
+
|
|
214
|
+
const coarseTopK = Math.max(cfg.section7CoarseTopK ?? Math.max((cfg.topK ?? 8) * 2, 8), 1);
|
|
215
|
+
const secondPassTopK = Math.max(cfg.section7SecondPassTopK ?? (cfg.topK ?? 8), 1);
|
|
216
|
+
const [sessionHits, durableHits] = await Promise.all([
|
|
105
217
|
rpc.call<{ results: SearchResult[] }>("search_text", {
|
|
106
218
|
collection: `session:${sessionId}`,
|
|
107
219
|
text: queryText,
|
|
108
|
-
k:
|
|
109
|
-
excludeIds: excluded,
|
|
220
|
+
k: coarseTopK,
|
|
221
|
+
excludeIds: [...excluded, ...recentTailIDs],
|
|
110
222
|
}),
|
|
111
223
|
cached
|
|
112
|
-
? Promise.resolve({ results: cached.
|
|
113
|
-
: rpc.call<{ results: SearchResult[] }>("
|
|
114
|
-
|
|
115
|
-
text: queryText,
|
|
116
|
-
k: Math.ceil((cfg.topK ?? 8) / 2),
|
|
117
|
-
}),
|
|
118
|
-
cached
|
|
119
|
-
? Promise.resolve({ results: cached.globalHits })
|
|
120
|
-
: rpc.call<{ results: SearchResult[] }>("search_text", {
|
|
121
|
-
collection: "global",
|
|
224
|
+
? Promise.resolve({ results: cached.durableVariantHits })
|
|
225
|
+
: rpc.call<{ results: SearchResult[] }>("search_text_collections", {
|
|
226
|
+
collections: [`user:${userId}`, "global", AUTHORED_VARIANT_COLLECTION],
|
|
122
227
|
text: queryText,
|
|
123
|
-
k:
|
|
228
|
+
k: coarseTopK,
|
|
229
|
+
excludeByCollection: {},
|
|
124
230
|
}),
|
|
125
231
|
]);
|
|
126
232
|
|
|
@@ -128,38 +234,56 @@ export function buildContextEngineFactory(
|
|
|
128
234
|
recallCache.put({
|
|
129
235
|
userId,
|
|
130
236
|
queryText,
|
|
131
|
-
|
|
132
|
-
globalHits: globalHits.results,
|
|
237
|
+
durableVariantHits: durableHits.results,
|
|
133
238
|
});
|
|
134
239
|
}
|
|
135
240
|
|
|
136
|
-
const ranked =
|
|
241
|
+
const ranked = rankSection7VariantCandidates(
|
|
137
242
|
[
|
|
138
|
-
...sessionHits.results,
|
|
139
|
-
...
|
|
140
|
-
...globalHits.results,
|
|
243
|
+
...annotateCollection(sessionHits.results, `session:${sessionId}`),
|
|
244
|
+
...durableHits.results,
|
|
141
245
|
],
|
|
142
246
|
{
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
247
|
+
queryText,
|
|
248
|
+
k1: coarseTopK,
|
|
249
|
+
k2: secondPassTopK,
|
|
250
|
+
theta1: cfg.section7Theta1,
|
|
251
|
+
kappa: cfg.section7Kappa,
|
|
252
|
+
authorityRecencyLambda: cfg.section7AuthorityRecencyLambda,
|
|
253
|
+
authorityRecencyWeight: cfg.section7AuthorityRecencyWeight,
|
|
254
|
+
authorityFrequencyWeight: cfg.section7AuthorityFrequencyWeight,
|
|
255
|
+
authorityAuthoredWeight: cfg.section7AuthorityAuthoredWeight,
|
|
150
256
|
sessionId,
|
|
151
257
|
userId,
|
|
152
258
|
},
|
|
153
259
|
);
|
|
154
|
-
|
|
155
|
-
const selected = fitPromptBudget(
|
|
260
|
+
const hopExpanded = expandSection7HopCandidates(
|
|
156
261
|
ranked,
|
|
157
|
-
|
|
262
|
+
annotateCollection(authoredVariantRecords, AUTHORED_VARIANT_COLLECTION),
|
|
263
|
+
{
|
|
264
|
+
etaHop: cfg.section7HopEta,
|
|
265
|
+
thetaHop: cfg.section7HopThreshold,
|
|
266
|
+
},
|
|
158
267
|
);
|
|
159
268
|
|
|
269
|
+
const variantItems = fitPromptBudget(
|
|
270
|
+
mergeSection7VariantCandidates(ranked, hopExpanded),
|
|
271
|
+
retrievalBudget,
|
|
272
|
+
);
|
|
273
|
+
const selected = [
|
|
274
|
+
...hardItems,
|
|
275
|
+
...tailBaseItems,
|
|
276
|
+
...softItems,
|
|
277
|
+
...tailExtensionItems,
|
|
278
|
+
...variantItems,
|
|
279
|
+
];
|
|
280
|
+
void rpc.call("bump_access_counts", {
|
|
281
|
+
updates: groupAccessCountUpdates(variantItems),
|
|
282
|
+
}).catch(() => {});
|
|
283
|
+
|
|
160
284
|
const selectedMessages = selected.map((item) => ({
|
|
161
285
|
role: "system",
|
|
162
|
-
content: item
|
|
286
|
+
content: buildInjectedMemoryMessageContent(item),
|
|
163
287
|
}));
|
|
164
288
|
|
|
165
289
|
return {
|
|
@@ -181,6 +305,9 @@ export function buildContextEngineFactory(
|
|
|
181
305
|
sessionId,
|
|
182
306
|
force,
|
|
183
307
|
targetSize: targetSize ?? cfg.compactThreshold,
|
|
308
|
+
continuityMinTurns: cfg.continuityMinTurns ?? DEFAULT_CONTINUITY_MIN_TURNS,
|
|
309
|
+
continuityTailBudgetTokens: cfg.continuityTailBudgetTokens ?? DEFAULT_CONTINUITY_TAIL_BUDGET_TOKENS,
|
|
310
|
+
continuityPriorContextTokens: cfg.continuityPriorContextTokens ?? DEFAULT_CONTINUITY_PRIOR_CONTEXT_TOKENS,
|
|
184
311
|
}).catch(() => ({ compacted: false }));
|
|
185
312
|
const compacted = "didCompact" in result
|
|
186
313
|
? (result.didCompact ?? result.compacted ?? false)
|
|
@@ -193,3 +320,147 @@ export function buildContextEngineFactory(
|
|
|
193
320
|
},
|
|
194
321
|
};
|
|
195
322
|
}
|
|
323
|
+
|
|
324
|
+
async function loadAuthoredCollections(
|
|
325
|
+
rpc: Awaited<ReturnType<RpcGetter>>,
|
|
326
|
+
cached: { hard: SearchResult[] | null; soft: SearchResult[] | null; variant: SearchResult[] | null },
|
|
327
|
+
): Promise<[SearchResult[], SearchResult[], SearchResult[]]> {
|
|
328
|
+
if (cached.hard && cached.soft && cached.variant) {
|
|
329
|
+
return [cached.hard, cached.soft, cached.variant];
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const [hard, soft, variant] = await Promise.all([
|
|
333
|
+
cached.hard
|
|
334
|
+
? Promise.resolve({ results: cached.hard })
|
|
335
|
+
: rpc.call<{ results: SearchResult[] }>("list_collection", { collection: AUTHORED_HARD_COLLECTION }),
|
|
336
|
+
cached.soft
|
|
337
|
+
? Promise.resolve({ results: cached.soft })
|
|
338
|
+
: rpc.call<{ results: SearchResult[] }>("list_collection", { collection: AUTHORED_SOFT_COLLECTION }),
|
|
339
|
+
cached.variant
|
|
340
|
+
? Promise.resolve({ results: cached.variant })
|
|
341
|
+
: rpc.call<{ results: SearchResult[] }>("list_collection", { collection: AUTHORED_VARIANT_COLLECTION }),
|
|
342
|
+
]);
|
|
343
|
+
|
|
344
|
+
return [hard.results, soft.results, variant.results];
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function tokenCostSum(items: SearchResult[]): number {
|
|
348
|
+
return items.reduce((sum, item) => sum + tokenCost(item), 0);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function tokenCost(item: SearchResult): number {
|
|
352
|
+
const estimate = item.metadata.token_estimate;
|
|
353
|
+
if (typeof estimate === "number" && estimate > 0) {
|
|
354
|
+
return estimate;
|
|
355
|
+
}
|
|
356
|
+
return estimateTokens(buildInjectedMemoryMessageContent(item));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function sortChronological(items: SearchResult[]): SearchResult[] {
|
|
360
|
+
return [...items].sort((left, right) => {
|
|
361
|
+
const leftTS = metadataTimestamp(left);
|
|
362
|
+
const rightTS = metadataTimestamp(right);
|
|
363
|
+
if (leftTS === rightTS) {
|
|
364
|
+
return left.id.localeCompare(right.id);
|
|
365
|
+
}
|
|
366
|
+
return leftTS - rightTS;
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function metadataTimestamp(item: SearchResult): number {
|
|
371
|
+
const raw = item.metadata.ts;
|
|
372
|
+
return typeof raw === "number" && Number.isFinite(raw) ? raw : 0;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function markRecentTail(items: SearchResult[], baseCount: number): SearchResult[] {
|
|
376
|
+
const baseStart = Math.max(0, items.length - baseCount);
|
|
377
|
+
return items.map((item, idx) => ({
|
|
378
|
+
...item,
|
|
379
|
+
metadata: {
|
|
380
|
+
...item.metadata,
|
|
381
|
+
continuity_tail: true,
|
|
382
|
+
continuity_base: idx >= baseStart,
|
|
383
|
+
},
|
|
384
|
+
}));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function annotateCollection(items: SearchResult[], collection: string): SearchResult[] {
|
|
388
|
+
return items.map((item) => ({
|
|
389
|
+
...item,
|
|
390
|
+
metadata: {
|
|
391
|
+
...item.metadata,
|
|
392
|
+
collection,
|
|
393
|
+
},
|
|
394
|
+
}));
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function groupAccessCountUpdates(items: SearchResult[]): Array<{ collection: string; ids: string[] }> {
|
|
398
|
+
const grouped = new Map<string, string[]>();
|
|
399
|
+
for (const item of items) {
|
|
400
|
+
const collection = typeof item.metadata.collection === "string" ? item.metadata.collection : "";
|
|
401
|
+
if (collection === "") {
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
const ids = grouped.get(collection) ?? [];
|
|
405
|
+
ids.push(item.id);
|
|
406
|
+
grouped.set(collection, ids);
|
|
407
|
+
}
|
|
408
|
+
return [...grouped.entries()].map(([collection, ids]) => ({ collection, ids }));
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function clampFraction(value: number | undefined): number {
|
|
412
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
413
|
+
return 0;
|
|
414
|
+
}
|
|
415
|
+
return Math.min(1, Math.max(0, value));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function validateSection7StartupHardReserve(cfg: PluginConfig, authoredHard: SearchResult[]): void {
|
|
419
|
+
if (authoredHard.length === 0) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
const hardFraction = clampFraction(cfg.authoredHardBudgetFraction);
|
|
423
|
+
if (hardFraction <= 0) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const startupTokenBudget = cfg.section7StartupTokenBudgetTokens;
|
|
427
|
+
if (typeof startupTokenBudget !== "number" || !Number.isFinite(startupTokenBudget) || startupTokenBudget <= 0) {
|
|
428
|
+
throw new Error(
|
|
429
|
+
"section7StartupTokenBudgetTokens is required to validate the authored hard reserve at bootstrap when authoredHardBudgetFraction is configured",
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
const memoryBudget = startupTokenBudget * (cfg.tokenBudgetFraction ?? 0.25);
|
|
433
|
+
const hardBudget = memoryBudget * hardFraction;
|
|
434
|
+
const hardUsed = tokenCostSum(authoredHard);
|
|
435
|
+
if (hardUsed > hardBudget + 1e-9) {
|
|
436
|
+
throw new Error(
|
|
437
|
+
`authored hard invariants require ${hardUsed} tokens but the configured startup reserve allows only ${hardBudget}`,
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function buildDegradedMemoryHeader(reasons: string[], selected: SearchResult[]): string {
|
|
443
|
+
const header = [
|
|
444
|
+
"<memory_degraded>",
|
|
445
|
+
"Memory assembly is in degraded mode.",
|
|
446
|
+
...reasons.map((reason, idx) => `[D${idx + 1}] ${reason}.`),
|
|
447
|
+
"Hard invariants and the mandatory recent-tail base were preserved without silent truncation.",
|
|
448
|
+
"</memory_degraded>",
|
|
449
|
+
].join("\n");
|
|
450
|
+
const body = buildMemoryHeader(selected);
|
|
451
|
+
return body === "" ? header : `${header}\n\n${body}`;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function isContinuityBundleCoupled(left: SearchResult, right: SearchResult): boolean {
|
|
455
|
+
const leftBundle = typeof left.metadata.continuity_bundle_id === "string" ? left.metadata.continuity_bundle_id : "";
|
|
456
|
+
const rightBundle = typeof right.metadata.continuity_bundle_id === "string" ? right.metadata.continuity_bundle_id : "";
|
|
457
|
+
if (leftBundle !== "" && leftBundle === rightBundle) {
|
|
458
|
+
return true;
|
|
459
|
+
}
|
|
460
|
+
const leftRole = typeof left.metadata.role === "string" ? left.metadata.role : "";
|
|
461
|
+
const rightRole = typeof right.metadata.role === "string" ? right.metadata.role : "";
|
|
462
|
+
return (
|
|
463
|
+
(leftRole === "user" && rightRole === "assistant") ||
|
|
464
|
+
(leftRole === "assistant" && rightRole === "user")
|
|
465
|
+
);
|
|
466
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export const DEFAULT_CONTINUITY_MIN_TURNS = 4;
|
|
2
|
+
export const DEFAULT_CONTINUITY_TAIL_BUDGET_TOKENS = 128;
|
|
3
|
+
export const DEFAULT_CONTINUITY_PRIOR_CONTEXT_TOKENS = 96;
|
|
4
|
+
|
|
5
|
+
export interface RecentTailSelection<T> {
|
|
6
|
+
older: T[];
|
|
7
|
+
base: T[];
|
|
8
|
+
recent: T[];
|
|
9
|
+
baseTokens: number;
|
|
10
|
+
recentTokens: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function selectRecentTail<T>(
|
|
14
|
+
items: T[],
|
|
15
|
+
{
|
|
16
|
+
minTurns = DEFAULT_CONTINUITY_MIN_TURNS,
|
|
17
|
+
tailBudgetTokens = DEFAULT_CONTINUITY_TAIL_BUDGET_TOKENS,
|
|
18
|
+
tokenCost,
|
|
19
|
+
sameBundle,
|
|
20
|
+
}: {
|
|
21
|
+
minTurns?: number;
|
|
22
|
+
tailBudgetTokens?: number;
|
|
23
|
+
tokenCost: (item: T) => number;
|
|
24
|
+
sameBundle?: (left: T, right: T) => boolean;
|
|
25
|
+
},
|
|
26
|
+
): RecentTailSelection<T> {
|
|
27
|
+
if (items.length === 0 || minTurns <= 0) {
|
|
28
|
+
return {
|
|
29
|
+
older: [...items],
|
|
30
|
+
base: [],
|
|
31
|
+
recent: [],
|
|
32
|
+
baseTokens: 0,
|
|
33
|
+
recentTokens: 0,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const normalizedMinTurns = Math.max(1, Math.floor(minTurns));
|
|
38
|
+
const normalizedTailBudget = Math.max(0, Math.floor(tailBudgetTokens));
|
|
39
|
+
const baseStart = Math.max(0, items.length - normalizedMinTurns);
|
|
40
|
+
const base = items.slice(baseStart);
|
|
41
|
+
const baseTokens = tokenCostSum(base, tokenCost);
|
|
42
|
+
|
|
43
|
+
if (baseTokens > normalizedTailBudget) {
|
|
44
|
+
const recentStart = extendBundleBoundary(items, baseStart, sameBundle);
|
|
45
|
+
const recent = items.slice(recentStart);
|
|
46
|
+
return {
|
|
47
|
+
older: items.slice(0, recentStart),
|
|
48
|
+
base,
|
|
49
|
+
recent,
|
|
50
|
+
baseTokens,
|
|
51
|
+
recentTokens: tokenCostSum(recent, tokenCost),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let start = baseStart;
|
|
56
|
+
let used = baseTokens;
|
|
57
|
+
for (let i = baseStart - 1; i >= 0; i -= 1) {
|
|
58
|
+
const nextCost = tokenCost(items[i]!);
|
|
59
|
+
if (used + nextCost > normalizedTailBudget) {
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
used += nextCost;
|
|
63
|
+
start = i;
|
|
64
|
+
}
|
|
65
|
+
start = extendBundleBoundary(items, start, sameBundle);
|
|
66
|
+
const recent = items.slice(start);
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
older: items.slice(0, start),
|
|
70
|
+
base,
|
|
71
|
+
recent,
|
|
72
|
+
baseTokens,
|
|
73
|
+
recentTokens: tokenCostSum(recent, tokenCost),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function tokenCostSum<T>(items: T[], tokenCost: (item: T) => number): number {
|
|
78
|
+
return items.reduce((sum, item) => sum + tokenCost(item), 0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function extendBundleBoundary<T>(
|
|
82
|
+
items: T[],
|
|
83
|
+
start: number,
|
|
84
|
+
sameBundle?: (left: T, right: T) => boolean,
|
|
85
|
+
): number {
|
|
86
|
+
if (!sameBundle) {
|
|
87
|
+
return start;
|
|
88
|
+
}
|
|
89
|
+
while (start > 0 && sameBundle(items[start - 1]!, items[start]!)) {
|
|
90
|
+
start -= 1;
|
|
91
|
+
}
|
|
92
|
+
return start;
|
|
93
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ export default definePluginEntry({
|
|
|
10
10
|
id: "libravdb-memory",
|
|
11
11
|
name: "LibraVDB Memory",
|
|
12
12
|
description: "Persistent vector memory with three-tier hybrid scoring",
|
|
13
|
-
kind: "memory",
|
|
13
|
+
kind: ["memory", "context-engine"],
|
|
14
14
|
|
|
15
15
|
register(api: OpenClawPluginApi) {
|
|
16
16
|
const cfg = api.pluginConfig as PluginConfig;
|
|
@@ -39,14 +39,14 @@ declare module "openclaw/plugin-sdk/plugin-entry" {
|
|
|
39
39
|
id: string;
|
|
40
40
|
name: string;
|
|
41
41
|
description: string;
|
|
42
|
-
kind?: "memory" | "context-engine"
|
|
42
|
+
kind?: "memory" | "context-engine" | Array<"memory" | "context-engine">;
|
|
43
43
|
configSchema?: unknown;
|
|
44
44
|
register(api: OpenClawPluginApi): void | Promise<void>;
|
|
45
45
|
}): {
|
|
46
46
|
id: string;
|
|
47
47
|
name: string;
|
|
48
48
|
description: string;
|
|
49
|
-
kind?: "memory" | "context-engine"
|
|
49
|
+
kind?: "memory" | "context-engine" | Array<"memory" | "context-engine">;
|
|
50
50
|
configSchema?: unknown;
|
|
51
51
|
register(api: OpenClawPluginApi): void | Promise<void>;
|
|
52
52
|
};
|