@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.
@@ -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.
@@ -3,7 +3,7 @@
3
3
  "name": "LibraVDB Memory",
4
4
  "description": "Persistent vector memory with three-tier hybrid scoring",
5
5
  "version": "1.3.11",
6
- "kind": "memory",
6
+ "kind": ["memory", "context-engine"],
7
7
  "configSchema": {
8
8
  "type": "object",
9
9
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xdarkicex/openclaw-memory-libravdb",
3
- "version": "1.3.11",
3
+ "version": "1.3.13",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -1,6 +1,16 @@
1
- import { scoreCandidates } from "./scoring.js";
2
- import { buildMemoryHeader, recentIds } from "./recall-utils.js";
3
- import { countTokens, fitPromptBudget } from "./tokens.js";
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: [`session:${sessionId}`, `turns:${userId}`, `user:${userId}`, "global"],
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 [sessionHits, userHits, globalHits] = await Promise.all([
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: cfg.topK ?? 8,
109
- excludeIds: excluded,
220
+ k: coarseTopK,
221
+ excludeIds: [...excluded, ...recentTailIDs],
110
222
  }),
111
223
  cached
112
- ? Promise.resolve({ results: cached.userHits })
113
- : rpc.call<{ results: SearchResult[] }>("search_text", {
114
- collection: `user:${userId}`,
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: Math.ceil((cfg.topK ?? 8) / 4),
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
- userHits: userHits.results,
132
- globalHits: globalHits.results,
237
+ durableVariantHits: durableHits.results,
133
238
  });
134
239
  }
135
240
 
136
- const ranked = scoreCandidates(
241
+ const ranked = rankSection7VariantCandidates(
137
242
  [
138
- ...sessionHits.results,
139
- ...userHits.results,
140
- ...globalHits.results,
243
+ ...annotateCollection(sessionHits.results, `session:${sessionId}`),
244
+ ...durableHits.results,
141
245
  ],
142
246
  {
143
- alpha: cfg.alpha,
144
- beta: cfg.beta,
145
- gamma: cfg.gamma,
146
- delta: cfg.compactionQualityWeight ?? 0.5,
147
- recencyLambdaSession: cfg.recencyLambdaSession,
148
- recencyLambdaUser: cfg.recencyLambdaUser,
149
- recencyLambdaGlobal: cfg.recencyLambdaGlobal,
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
- tokenBudget * (cfg.tokenBudgetFraction ?? 0.25),
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.text,
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
  };