@xdarkicex/openclaw-memory-libravdb 1.8.0 → 1.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -15
- package/dist/context-engine.js +38 -4
- package/dist/index.js +108 -9
- package/dist/memory-tools.js +35 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -144,19 +144,53 @@ If your service runs elsewhere, set `sidecarPath`:
|
|
|
144
144
|
|
|
145
145
|
## Highlights
|
|
146
146
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
- **
|
|
150
|
-
- **
|
|
151
|
-
- **
|
|
152
|
-
- **
|
|
153
|
-
- **
|
|
154
|
-
- **
|
|
155
|
-
- **
|
|
156
|
-
- **
|
|
157
|
-
- **
|
|
158
|
-
- **
|
|
159
|
-
- **
|
|
147
|
+
### Why LibraVDB over other memory plugins
|
|
148
|
+
|
|
149
|
+
- **Truly local.** All embedding, search, and compaction runs on your hardware through a dedicated vector service. No cloud API calls, no data leaving your machine, no subscription fees. Works offline.
|
|
150
|
+
- **Handles long conversations.** Sessions with hundreds of turns are automatically compacted into searchable summaries. The agent can recall what was discussed in turn 5 even when you're on turn 200 — without blowing the context window.
|
|
151
|
+
- **Never forgets a constraint.** Behavioral rules, preferences, and operating boundaries ("always use TLS", "prefers dark mode") are automatically detected and surfaced higher in recall than conversational noise. The agent can ask "what are my constraints?" and get a surgical answer.
|
|
152
|
+
- **Automatic contradiction detection.** When you say "my email changed to jeff@anthropic.com", the old email is automatically marked as outdated — no manual cleanup, no stale facts confusing the agent.
|
|
153
|
+
- **BM25 + vector hybrid search.** Lexical matching (exact identifiers, file paths, error codes) is fused with semantic similarity. A query for `docker-compose.yml` finds the file even if you described it as "the container config."
|
|
154
|
+
- **Summary recall with expansion tools.** Compacted conversation history can be explored without flooding context. `memory_describe` peeks at what a summary covers; `memory_expand` drills into specifics; `memory_grep` searches by pattern. The agent decides how deep to go.
|
|
155
|
+
- **Subagent-safe expansion.** When a summary is too large to expand directly, `memory_expand` enforces a token budget and delegates to a sub-agent — protecting the main agent's context window.
|
|
156
|
+
- **Predictive memory.** The vector service pre-computes what the agent is likely to ask next after each turn, injecting relevant context before the model even sees the prompt.
|
|
157
|
+
- **Three memory scopes.** Session memory (current conversation), user memory (everything you've ever told the agent), and global memory (shared across users) are kept separate. Searches can target specific scopes.
|
|
158
|
+
- **Cognitive kind and signal filters.** Memories are classified as identity, fact, preference, constraint, decision, or episode. `memory_search(kind="constraint")` returns only operating boundaries — no conversational noise.
|
|
159
|
+
- **True multi-tenancy.** Isolated per-agent vector databases within a single vector service process. Each agent sees only its own data.
|
|
160
|
+
- **Memory-mapped embedding cache.** Frequently embedded text is cached in a file-backed mmap region that survives daemon restarts. Cold starts are faster, repeat queries are instant.
|
|
161
|
+
- **Pluggable summarization backend.** The vector service's extractive summarization can replace LLM-based compaction — zero tokens burned on summarization.
|
|
162
|
+
- **Local-first inference.** GGUF, ONNX, or remote embedding backends. Hardware-native acceleration on Apple Silicon and NVIDIA. No cloud required.
|
|
163
|
+
- **Operational CLI.** `libravdbd status`, `health`, `search`, `tenant evict`, `migrate` — live observability and management without interrupting active sessions.
|
|
164
|
+
|
|
165
|
+
### Technical Architecture
|
|
166
|
+
|
|
167
|
+
- **Unified Cognitive Scoring** — mathematically blends cosine similarity with frequency, recency, authored salience, and cognitive authority composite weights (`ω(c)`).
|
|
168
|
+
- **Section 7 Two-Pass Retrieval** — coarse cascade search (coarse top-K) followed by precision reranking (second-pass top-K) with hop expansion and temporal comparison profiling.
|
|
169
|
+
- **BM25 + Vector RRF Fusion** — lexical BM25 scoring fused with vector similarity via Reciprocal Rank Fusion across all 11 recall paths.
|
|
170
|
+
- **Content-Addressed Summaries** — deterministic SHA256-based summary IDs: same inputs produce identical IDs across crashes and retries.
|
|
171
|
+
- **Structured Eviction Cues** — ~60-token deterministic metadata pointers on summary records (anchors, decisions, constraints, signal counts) — no LLM needed.
|
|
172
|
+
- **Topological Causal Graphs** — temporal memory chains via directed acyclic graphs (`WhyIDs`), injecting causal proximity into retrieval scoring.
|
|
173
|
+
- **Zero-GC Slab Allocation** — manages model tensor and inference data via a custom contiguous slab allocator (`slabby`), bypassing Go garbage collection pauses.
|
|
174
|
+
- **Deontic & Salience Retrieval** — structural authority weightings and deontic logic rules ensure critical behavioral constraints mathematically outrank conversational chatter.
|
|
175
|
+
- **Matryoshka Representation Learning** — dynamically tiered embedding dimensions (e.g., slicing 768d vectors down to 64d) for cascading coarse search followed by precision reranking.
|
|
176
|
+
- **Cognitive Routing Circuit Breakers** — stateful circuit breakers on remote endpoints, auto-disabling complex ML routing during outages while preserving foundational search.
|
|
177
|
+
- **Zero-ML Local Compaction** — purely localized session summarization and compaction cycles natively within the vector service. L1-L8 pipeline with deterministic state skeleton.
|
|
178
|
+
- **Anchor-Based Contradiction Detection** — regex anchor extraction with Jaccard dedup and automatic `MarkSuperseded` — zero LLM overhead.
|
|
179
|
+
- **Access Frequency in Omega** — `log2(accessCount+1)/10` term in the authority composite: frequently-retrieved memories surface higher without dominating relevance.
|
|
180
|
+
- **True multi-tenancy** — strictly isolated, per-agent vector databases within a single lightweight vector service process.
|
|
181
|
+
- **Zero-copy caching** — memory-mapped cross-tenant embedding cache across all active agents. Tenant-scoped keys prevent cross-tenant collision.
|
|
182
|
+
- **Three memory scopes** — active session, durable user, and global memory kept separate.
|
|
183
|
+
- **Local-first inference** — GGUF, ONNX, or remote embedding backends. Hardware-native acceleration on Apple Silicon and NVIDIA.
|
|
184
|
+
- **Pluggable compaction backend** — exposes the vector service's extractive summarization as an OpenClaw `CompactionProvider` — replaces LLM summarization.
|
|
185
|
+
- **Operational tooling** — dedicated CLI (`libravdbd status`, `health`, `search`, `migrate`, `tenant evict`) for live observability.
|
|
186
|
+
- **Half-Life Decay per Cognitive Kind** — each memory kind decays at its own rate: identity, constraint, and decision have infinite half-life (permanent); facts decay over 180 days; preferences over 365 days. Mathematical support accumulation prevents thrashing.
|
|
187
|
+
- **Deterministic State Skeleton (L8)** — extracts structured decisions, constraints, and next steps from raw turns using pure heuristics — no LLM call needed. Line-level scoring with commitment-verb and future-intent detection.
|
|
188
|
+
- **Deterministic Tool Output Compression** — 3-phase compression of tool outputs before summarization: JSON key sampling, log-line deduplication (FNV-64a), and fenced-block tagging. Reduces token pressure without losing deontic markers.
|
|
189
|
+
- **Seven Budget Channels** — waterfall token allocation across retrieval floor, mandatory continuity tail, hard-authored items, elevated guidance, soft-authored items, retrieval remainder, and recovery reserve. Each channel has its own budget fraction.
|
|
190
|
+
- **Temporal Comparison Profiling** — witness scoring with diachronicity detection for "how did this change?" queries. Slot decomposition, discriminative membership, and position-weighted specificity.
|
|
191
|
+
- **Merkle Chain Ingest** — content-hash-based session manifest with cursor reconciliation between plugin and vector service. Guarantees idempotent ingestion across crashes and retries.
|
|
192
|
+
- **Nonce-Chaining HMAC Auth** — per-request challenge-response authentication with single-use cryptographic nonces. Supports mTLS for secure multi-machine deployments.
|
|
193
|
+
- **Explicit service lifecycle** — the npm/OpenClaw package stays connect-only; `libravdbd` is installed and supervised separately over a secure gRPC transport.
|
|
160
194
|
|
|
161
195
|
## Embedding Backend Providers
|
|
162
196
|
|
|
@@ -193,11 +227,11 @@ openclaw memory journal --limit 50
|
|
|
193
227
|
openclaw memory dream-promote --user-id <userId> --dream-file ~/DREAMS.md
|
|
194
228
|
```
|
|
195
229
|
|
|
196
|
-
### Vector Service CLI (libravdbd v1.
|
|
230
|
+
### Vector Service CLI (libravdbd v1.6.0+)
|
|
197
231
|
|
|
198
232
|
```bash
|
|
199
233
|
# Service health and status
|
|
200
|
-
libravdbd status # tenants, cache, DB sizes
|
|
234
|
+
libravdbd status # tenants, cache, DB sizes, CPU load
|
|
201
235
|
libravdbd health # OK/UNHEALTHY
|
|
202
236
|
|
|
203
237
|
# Search tenant memory (same collections memory_search queries)
|
package/dist/context-engine.js
CHANGED
|
@@ -43,6 +43,19 @@ function requireSessionId(sessionId, operation) {
|
|
|
43
43
|
function normalizeCompactResult(response, options = {}) {
|
|
44
44
|
const didCompact = response?.didCompact === true;
|
|
45
45
|
const tokensBefore = normalizeCurrentTokenCount(options.tokensBefore) ?? 0;
|
|
46
|
+
const lastCompactedTurn = typeof response?.lastCompactedTurn === "bigint" ? response.lastCompactedTurn : undefined;
|
|
47
|
+
const tokenAccumulatorAfter = typeof response?.tokenAccumulatorAfter === "number" ? response.tokenAccumulatorAfter : undefined;
|
|
48
|
+
const totalTurns = typeof response?.totalTurns === "bigint" ? response.totalTurns : undefined;
|
|
49
|
+
const skippedNoNewTurns = typeof response?.skippedNoNewTurns === "boolean" ? response.skippedNoNewTurns : undefined;
|
|
50
|
+
if (lastCompactedTurn != null ||
|
|
51
|
+
tokenAccumulatorAfter != null ||
|
|
52
|
+
totalTurns != null ||
|
|
53
|
+
skippedNoNewTurns != null) {
|
|
54
|
+
options.logger?.info?.(`[compact:trace] daemon state lastCompactedTurn=${lastCompactedTurn?.toString() ?? "unknown"} ` +
|
|
55
|
+
`tokenAccumulatorAfter=${tokenAccumulatorAfter ?? "unknown"} ` +
|
|
56
|
+
`totalTurns=${totalTurns?.toString() ?? "unknown"} ` +
|
|
57
|
+
`skippedNoNewTurns=${skippedNoNewTurns ?? "unknown"}`);
|
|
58
|
+
}
|
|
46
59
|
const details = {
|
|
47
60
|
clustersFormed: typeof response?.clustersFormed === "number" ? response.clustersFormed : undefined,
|
|
48
61
|
clustersDeclined: typeof response?.clustersDeclined === "number" ? response.clustersDeclined : undefined,
|
|
@@ -54,6 +67,10 @@ function normalizeCompactResult(response, options = {}) {
|
|
|
54
67
|
summaryText: typeof response?.summaryText === "string" && response.summaryText.length > 0
|
|
55
68
|
? response.summaryText
|
|
56
69
|
: undefined,
|
|
70
|
+
...(lastCompactedTurn != null ? { lastCompactedTurn: lastCompactedTurn.toString() } : {}),
|
|
71
|
+
...(tokenAccumulatorAfter != null ? { tokenAccumulatorAfter } : {}),
|
|
72
|
+
...(totalTurns != null ? { totalTurns: totalTurns.toString() } : {}),
|
|
73
|
+
...(skippedNoNewTurns != null ? { skippedNoNewTurns } : {}),
|
|
57
74
|
};
|
|
58
75
|
// When the engine owns compaction but refuses to compact while the session
|
|
59
76
|
// exceeds the threshold, this is not a successful skip — it's a failure.
|
|
@@ -363,16 +380,32 @@ function normalizeThresholdFraction(fraction) {
|
|
|
363
380
|
/**
|
|
364
381
|
* Resolves the dynamic compaction threshold from budget and threshold params.
|
|
365
382
|
*/
|
|
366
|
-
function resolveDynamicCompactThreshold(tokenBudget, compactThreshold, compactionThresholdFraction) {
|
|
383
|
+
function resolveDynamicCompactThreshold(tokenBudget, compactThreshold, compactionThresholdFraction, compactSessionTokenBudget, logger) {
|
|
384
|
+
// Explicit compactThreshold always wins.
|
|
367
385
|
if (typeof compactThreshold === "number" && Number.isFinite(compactThreshold) && compactThreshold > 0) {
|
|
368
|
-
|
|
386
|
+
const val = Math.max(1, Math.floor(compactThreshold));
|
|
387
|
+
logger?.info?.(`[compact:trace] resolveDynamicCompactThreshold branch=explicit tokenBudget=${tokenBudget} compactThreshold=${compactThreshold} → ${val}`);
|
|
388
|
+
return val;
|
|
369
389
|
}
|
|
370
390
|
const normalizedBudget = normalizeTokenBudget(tokenBudget);
|
|
371
391
|
if (normalizedBudget == null) {
|
|
392
|
+
logger?.info?.(`[compact:trace] resolveDynamicCompactThreshold branch=null_budget tokenBudget=${tokenBudget} → undefined`);
|
|
372
393
|
return undefined;
|
|
373
394
|
}
|
|
374
395
|
const fraction = normalizeThresholdFraction(compactionThresholdFraction);
|
|
375
|
-
|
|
396
|
+
const derived = Math.max(1, Math.floor(normalizedBudget * fraction));
|
|
397
|
+
// Clamp to a safe range so the threshold is never absurdly low (not
|
|
398
|
+
// enough turns to compact) or absurdly high (Codex Runtime 1M tokens
|
|
399
|
+
// would produce an unreachable 800k threshold).
|
|
400
|
+
const withBounds = Math.max(2000, Math.min(16000, derived));
|
|
401
|
+
// User-configured compactSessionTokenBudget overrides the ceiling.
|
|
402
|
+
if (typeof compactSessionTokenBudget === "number" && compactSessionTokenBudget > 0) {
|
|
403
|
+
const capped = Math.min(withBounds, compactSessionTokenBudget);
|
|
404
|
+
logger?.info?.(`[compact:trace] resolveDynamicCompactThreshold branch=user_cap tokenBudget=${tokenBudget} normalizedBudget=${normalizedBudget} fraction=${fraction} derived=${derived} withBounds=${withBounds} cap=${compactSessionTokenBudget} → ${capped}`);
|
|
405
|
+
return capped;
|
|
406
|
+
}
|
|
407
|
+
logger?.info?.(`[compact:trace] resolveDynamicCompactThreshold branch=clamped tokenBudget=${tokenBudget} normalizedBudget=${normalizedBudget} fraction=${fraction} derived=${derived} withBounds=${withBounds} → ${withBounds}`);
|
|
408
|
+
return withBounds;
|
|
376
409
|
}
|
|
377
410
|
function resolvePredictiveCompactionTarget(params) {
|
|
378
411
|
const currentTokenCount = normalizeCurrentTokenCount(params.currentTokenCount);
|
|
@@ -1184,7 +1217,7 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
|
|
|
1184
1217
|
scored.sort((a, b) => b.score - a.score);
|
|
1185
1218
|
return scored.slice(0, maxItems).map((s) => s.prediction);
|
|
1186
1219
|
}
|
|
1187
|
-
const getDynamicCompactThreshold = (tokenBudget) => resolveDynamicCompactThreshold(tokenBudget, cfg.compactThreshold, cfg.compactionThresholdFraction);
|
|
1220
|
+
const getDynamicCompactThreshold = (tokenBudget) => resolveDynamicCompactThreshold(tokenBudget, cfg.compactThreshold, cfg.compactionThresholdFraction, cfg.compactSessionTokenBudget);
|
|
1188
1221
|
const buildAssemblyConfig = (tokenBudget) => ({
|
|
1189
1222
|
useSessionRecallProjection: cfg.useSessionRecallProjection,
|
|
1190
1223
|
useSessionSummarySearchExperiment: cfg.useSessionSummarySearchExperiment,
|
|
@@ -1331,6 +1364,7 @@ export function buildContextEngineFactory(runtime, cfg, logger = console) {
|
|
|
1331
1364
|
const threshold = getDynamicCompactThreshold(args.tokenBudget);
|
|
1332
1365
|
return normalizeCompactResult(await client.compactSession(request), {
|
|
1333
1366
|
tokensBefore: args.currentTokenCount,
|
|
1367
|
+
logger,
|
|
1334
1368
|
...(threshold != null ? { threshold } : {}),
|
|
1335
1369
|
});
|
|
1336
1370
|
}
|
package/dist/index.js
CHANGED
|
@@ -26813,13 +26813,26 @@ function requireSessionId(sessionId, operation) {
|
|
|
26813
26813
|
function normalizeCompactResult(response, options = {}) {
|
|
26814
26814
|
const didCompact = response?.didCompact === true;
|
|
26815
26815
|
const tokensBefore = normalizeCurrentTokenCount(options.tokensBefore) ?? 0;
|
|
26816
|
+
const lastCompactedTurn = typeof response?.lastCompactedTurn === "bigint" ? response.lastCompactedTurn : void 0;
|
|
26817
|
+
const tokenAccumulatorAfter = typeof response?.tokenAccumulatorAfter === "number" ? response.tokenAccumulatorAfter : void 0;
|
|
26818
|
+
const totalTurns = typeof response?.totalTurns === "bigint" ? response.totalTurns : void 0;
|
|
26819
|
+
const skippedNoNewTurns = typeof response?.skippedNoNewTurns === "boolean" ? response.skippedNoNewTurns : void 0;
|
|
26820
|
+
if (lastCompactedTurn != null || tokenAccumulatorAfter != null || totalTurns != null || skippedNoNewTurns != null) {
|
|
26821
|
+
options.logger?.info?.(
|
|
26822
|
+
`[compact:trace] daemon state lastCompactedTurn=${lastCompactedTurn?.toString() ?? "unknown"} tokenAccumulatorAfter=${tokenAccumulatorAfter ?? "unknown"} totalTurns=${totalTurns?.toString() ?? "unknown"} skippedNoNewTurns=${skippedNoNewTurns ?? "unknown"}`
|
|
26823
|
+
);
|
|
26824
|
+
}
|
|
26816
26825
|
const details = {
|
|
26817
26826
|
clustersFormed: typeof response?.clustersFormed === "number" ? response.clustersFormed : void 0,
|
|
26818
26827
|
clustersDeclined: typeof response?.clustersDeclined === "number" ? response.clustersDeclined : void 0,
|
|
26819
26828
|
turnsRemoved: typeof response?.turnsRemoved === "number" ? response.turnsRemoved : void 0,
|
|
26820
26829
|
summaryMethod: typeof response?.summaryMethod === "string" && response.summaryMethod.length > 0 ? response.summaryMethod : void 0,
|
|
26821
26830
|
meanConfidence: typeof response?.meanConfidence === "number" ? response.meanConfidence : void 0,
|
|
26822
|
-
summaryText: typeof response?.summaryText === "string" && response.summaryText.length > 0 ? response.summaryText : void 0
|
|
26831
|
+
summaryText: typeof response?.summaryText === "string" && response.summaryText.length > 0 ? response.summaryText : void 0,
|
|
26832
|
+
...lastCompactedTurn != null ? { lastCompactedTurn: lastCompactedTurn.toString() } : {},
|
|
26833
|
+
...tokenAccumulatorAfter != null ? { tokenAccumulatorAfter } : {},
|
|
26834
|
+
...totalTurns != null ? { totalTurns: totalTurns.toString() } : {},
|
|
26835
|
+
...skippedNoNewTurns != null ? { skippedNoNewTurns } : {}
|
|
26823
26836
|
};
|
|
26824
26837
|
const threshold = options.threshold;
|
|
26825
26838
|
const overBudget = threshold != null && tokensBefore >= threshold;
|
|
@@ -27082,16 +27095,27 @@ function normalizeThresholdFraction(fraction) {
|
|
|
27082
27095
|
}
|
|
27083
27096
|
return Math.min(0.99, Math.max(0.05, fraction));
|
|
27084
27097
|
}
|
|
27085
|
-
function resolveDynamicCompactThreshold(tokenBudget, compactThreshold, compactionThresholdFraction) {
|
|
27098
|
+
function resolveDynamicCompactThreshold(tokenBudget, compactThreshold, compactionThresholdFraction, compactSessionTokenBudget, logger) {
|
|
27086
27099
|
if (typeof compactThreshold === "number" && Number.isFinite(compactThreshold) && compactThreshold > 0) {
|
|
27087
|
-
|
|
27100
|
+
const val = Math.max(1, Math.floor(compactThreshold));
|
|
27101
|
+
logger?.info?.(`[compact:trace] resolveDynamicCompactThreshold branch=explicit tokenBudget=${tokenBudget} compactThreshold=${compactThreshold} \u2192 ${val}`);
|
|
27102
|
+
return val;
|
|
27088
27103
|
}
|
|
27089
27104
|
const normalizedBudget = normalizeTokenBudget(tokenBudget);
|
|
27090
27105
|
if (normalizedBudget == null) {
|
|
27106
|
+
logger?.info?.(`[compact:trace] resolveDynamicCompactThreshold branch=null_budget tokenBudget=${tokenBudget} \u2192 undefined`);
|
|
27091
27107
|
return void 0;
|
|
27092
27108
|
}
|
|
27093
27109
|
const fraction = normalizeThresholdFraction(compactionThresholdFraction);
|
|
27094
|
-
|
|
27110
|
+
const derived = Math.max(1, Math.floor(normalizedBudget * fraction));
|
|
27111
|
+
const withBounds = Math.max(2e3, Math.min(16e3, derived));
|
|
27112
|
+
if (typeof compactSessionTokenBudget === "number" && compactSessionTokenBudget > 0) {
|
|
27113
|
+
const capped = Math.min(withBounds, compactSessionTokenBudget);
|
|
27114
|
+
logger?.info?.(`[compact:trace] resolveDynamicCompactThreshold branch=user_cap tokenBudget=${tokenBudget} normalizedBudget=${normalizedBudget} fraction=${fraction} derived=${derived} withBounds=${withBounds} cap=${compactSessionTokenBudget} \u2192 ${capped}`);
|
|
27115
|
+
return capped;
|
|
27116
|
+
}
|
|
27117
|
+
logger?.info?.(`[compact:trace] resolveDynamicCompactThreshold branch=clamped tokenBudget=${tokenBudget} normalizedBudget=${normalizedBudget} fraction=${fraction} derived=${derived} withBounds=${withBounds} \u2192 ${withBounds}`);
|
|
27118
|
+
return withBounds;
|
|
27095
27119
|
}
|
|
27096
27120
|
function resolvePredictiveCompactionTarget(params) {
|
|
27097
27121
|
const currentTokenCount = normalizeCurrentTokenCount(params.currentTokenCount);
|
|
@@ -27761,7 +27785,8 @@ function buildContextEngineFactory(runtime, cfg, logger = console) {
|
|
|
27761
27785
|
const getDynamicCompactThreshold = (tokenBudget) => resolveDynamicCompactThreshold(
|
|
27762
27786
|
tokenBudget,
|
|
27763
27787
|
cfg.compactThreshold,
|
|
27764
|
-
cfg.compactionThresholdFraction
|
|
27788
|
+
cfg.compactionThresholdFraction,
|
|
27789
|
+
cfg.compactSessionTokenBudget
|
|
27765
27790
|
);
|
|
27766
27791
|
const buildAssemblyConfig = (tokenBudget) => ({
|
|
27767
27792
|
useSessionRecallProjection: cfg.useSessionRecallProjection,
|
|
@@ -27897,6 +27922,7 @@ function buildContextEngineFactory(runtime, cfg, logger = console) {
|
|
|
27897
27922
|
const threshold = getDynamicCompactThreshold(args.tokenBudget);
|
|
27898
27923
|
return normalizeCompactResult(await client.compactSession(request3), {
|
|
27899
27924
|
tokensBefore: args.currentTokenCount,
|
|
27925
|
+
logger,
|
|
27900
27926
|
...threshold != null ? { threshold } : {}
|
|
27901
27927
|
});
|
|
27902
27928
|
} catch (error2) {
|
|
@@ -28412,7 +28438,7 @@ import path3 from "node:path";
|
|
|
28412
28438
|
var proxy_exports = {};
|
|
28413
28439
|
__reExport(proxy_exports, __toESM(require_cjs(), 1));
|
|
28414
28440
|
|
|
28415
|
-
// node_modules/.pnpm/@xdarkicex+libravdb-contracts@2.0.
|
|
28441
|
+
// node_modules/.pnpm/@xdarkicex+libravdb-contracts@2.0.20/node_modules/@xdarkicex/libravdb-contracts/gen/js/libravdb/ipc/v1/rpc_pb.js
|
|
28416
28442
|
var IngestMode;
|
|
28417
28443
|
(function(IngestMode2) {
|
|
28418
28444
|
IngestMode2[IngestMode2["REPLACE"] = 0] = "REPLACE";
|
|
@@ -29820,11 +29846,28 @@ var CompactSessionResponse = class _CompactSessionResponse extends proxy_exports
|
|
|
29820
29846
|
*/
|
|
29821
29847
|
summaryText = "";
|
|
29822
29848
|
/**
|
|
29823
|
-
* estimated token count after compaction
|
|
29824
|
-
*
|
|
29825
29849
|
* @generated from field: int32 tokens_after = 8;
|
|
29826
29850
|
*/
|
|
29827
29851
|
tokensAfter = 0;
|
|
29852
|
+
/**
|
|
29853
|
+
* Engine-owned session state after the compaction attempt.
|
|
29854
|
+
* Zero / false when the daemon is older and does not populate these.
|
|
29855
|
+
*
|
|
29856
|
+
* @generated from field: int64 last_compacted_turn = 9;
|
|
29857
|
+
*/
|
|
29858
|
+
lastCompactedTurn = proxy_exports.protoInt64.zero;
|
|
29859
|
+
/**
|
|
29860
|
+
* @generated from field: int32 token_accumulator_after = 10;
|
|
29861
|
+
*/
|
|
29862
|
+
tokenAccumulatorAfter = 0;
|
|
29863
|
+
/**
|
|
29864
|
+
* @generated from field: int64 total_turns = 11;
|
|
29865
|
+
*/
|
|
29866
|
+
totalTurns = proxy_exports.protoInt64.zero;
|
|
29867
|
+
/**
|
|
29868
|
+
* @generated from field: bool skipped_no_new_turns = 12;
|
|
29869
|
+
*/
|
|
29870
|
+
skippedNoNewTurns = false;
|
|
29828
29871
|
constructor(data) {
|
|
29829
29872
|
super();
|
|
29830
29873
|
proxy_exports.proto3.util.initPartial(data, this);
|
|
@@ -29887,6 +29930,34 @@ var CompactSessionResponse = class _CompactSessionResponse extends proxy_exports
|
|
|
29887
29930
|
kind: "scalar",
|
|
29888
29931
|
T: 5
|
|
29889
29932
|
/* ScalarType.INT32 */
|
|
29933
|
+
},
|
|
29934
|
+
{
|
|
29935
|
+
no: 9,
|
|
29936
|
+
name: "last_compacted_turn",
|
|
29937
|
+
kind: "scalar",
|
|
29938
|
+
T: 3
|
|
29939
|
+
/* ScalarType.INT64 */
|
|
29940
|
+
},
|
|
29941
|
+
{
|
|
29942
|
+
no: 10,
|
|
29943
|
+
name: "token_accumulator_after",
|
|
29944
|
+
kind: "scalar",
|
|
29945
|
+
T: 5
|
|
29946
|
+
/* ScalarType.INT32 */
|
|
29947
|
+
},
|
|
29948
|
+
{
|
|
29949
|
+
no: 11,
|
|
29950
|
+
name: "total_turns",
|
|
29951
|
+
kind: "scalar",
|
|
29952
|
+
T: 3
|
|
29953
|
+
/* ScalarType.INT64 */
|
|
29954
|
+
},
|
|
29955
|
+
{
|
|
29956
|
+
no: 12,
|
|
29957
|
+
name: "skipped_no_new_turns",
|
|
29958
|
+
kind: "scalar",
|
|
29959
|
+
T: 8
|
|
29960
|
+
/* ScalarType.BOOL */
|
|
29890
29961
|
}
|
|
29891
29962
|
]);
|
|
29892
29963
|
static fromBinary(bytes, options) {
|
|
@@ -36020,6 +36091,27 @@ var MEMORY_GET_SCHEMA = {
|
|
|
36020
36091
|
function createLibraVdbMemoryTools(getClient, cfg, logger = console) {
|
|
36021
36092
|
const bridge = buildMemoryRuntimeBridge(getClient, cfg);
|
|
36022
36093
|
const managers = /* @__PURE__ */ new Map();
|
|
36094
|
+
const turnSearchKeys = /* @__PURE__ */ new Map();
|
|
36095
|
+
const TURN_SEARCH_MAX_KEYS = 500;
|
|
36096
|
+
function dedupKey(sessionKey, query) {
|
|
36097
|
+
return `${sessionKey}:${query.toLowerCase().replace(/\s+/g, " ").trim().slice(0, 80)}`;
|
|
36098
|
+
}
|
|
36099
|
+
function isDuplicateSearch(sessionKey, query) {
|
|
36100
|
+
if (!sessionKey) return false;
|
|
36101
|
+
const key = dedupKey(sessionKey, query);
|
|
36102
|
+
const keys = turnSearchKeys.get(sessionKey);
|
|
36103
|
+
if (!keys) {
|
|
36104
|
+
turnSearchKeys.set(sessionKey, /* @__PURE__ */ new Set([key]));
|
|
36105
|
+
if (turnSearchKeys.size > TURN_SEARCH_MAX_KEYS) {
|
|
36106
|
+
const oldest = turnSearchKeys.keys().next().value;
|
|
36107
|
+
if (oldest !== void 0) turnSearchKeys.delete(oldest);
|
|
36108
|
+
}
|
|
36109
|
+
return false;
|
|
36110
|
+
}
|
|
36111
|
+
if (keys.has(key)) return true;
|
|
36112
|
+
keys.add(key);
|
|
36113
|
+
return false;
|
|
36114
|
+
}
|
|
36023
36115
|
async function getManager(ctx, purpose) {
|
|
36024
36116
|
const key = managerCacheKey(ctx);
|
|
36025
36117
|
let manager = managers.get(key);
|
|
@@ -36045,6 +36137,13 @@ function createLibraVdbMemoryTools(getClient, cfg, logger = console) {
|
|
|
36045
36137
|
execute: async (_toolCallId, rawParams) => {
|
|
36046
36138
|
const params = asToolParamsRecord(rawParams);
|
|
36047
36139
|
const query = readRequiredStringParam(params, "query");
|
|
36140
|
+
const sessionKey = ctx.sessionKey ?? "";
|
|
36141
|
+
if (isDuplicateSearch(sessionKey, query)) {
|
|
36142
|
+
return jsonToolResult({
|
|
36143
|
+
results: [],
|
|
36144
|
+
error: `Duplicate search blocked. You already searched this turn \u2014 use the previous results. Do not call memory_search again.`
|
|
36145
|
+
});
|
|
36146
|
+
}
|
|
36048
36147
|
const corpus = readMemoryCorpus(params.corpus);
|
|
36049
36148
|
const kind = typeof params.kind === "string" ? params.kind : void 0;
|
|
36050
36149
|
const signals = Array.isArray(params.signals) ? params.signals.filter((s) => typeof s === "string") : void 0;
|
|
@@ -38639,7 +38738,7 @@ function createGrpcTransport(options) {
|
|
|
38639
38738
|
return createTransport(validateNodeTransportOptions(options));
|
|
38640
38739
|
}
|
|
38641
38740
|
|
|
38642
|
-
// node_modules/.pnpm/@xdarkicex+libravdb-contracts@2.0.
|
|
38741
|
+
// node_modules/.pnpm/@xdarkicex+libravdb-contracts@2.0.20/node_modules/@xdarkicex/libravdb-contracts/gen/js/libravdb/ipc/v1/rpc_connect.js
|
|
38643
38742
|
var LibravDB = {
|
|
38644
38743
|
typeName: "libravdb.ipc.v1.LibravDB",
|
|
38645
38744
|
methods: {
|
package/dist/memory-tools.js
CHANGED
|
@@ -67,6 +67,34 @@ const MEMORY_GET_SCHEMA = {
|
|
|
67
67
|
export function createLibraVdbMemoryTools(getClient, cfg, logger = console) {
|
|
68
68
|
const bridge = buildMemoryRuntimeBridge(getClient, cfg);
|
|
69
69
|
const managers = new Map();
|
|
70
|
+
// Turn-scoped search dedup: blocks repeated searches within the same turn.
|
|
71
|
+
// The model sometimes loops memory_search with slight query variations;
|
|
72
|
+
// this enforces "once per turn" at the tool level, not just the prompt.
|
|
73
|
+
const turnSearchKeys = new Map();
|
|
74
|
+
const TURN_SEARCH_MAX_KEYS = 500;
|
|
75
|
+
function dedupKey(sessionKey, query) {
|
|
76
|
+
return `${sessionKey}:${query.toLowerCase().replace(/\s+/g, " ").trim().slice(0, 80)}`;
|
|
77
|
+
}
|
|
78
|
+
function isDuplicateSearch(sessionKey, query) {
|
|
79
|
+
if (!sessionKey)
|
|
80
|
+
return false;
|
|
81
|
+
const key = dedupKey(sessionKey, query);
|
|
82
|
+
const keys = turnSearchKeys.get(sessionKey);
|
|
83
|
+
if (!keys) {
|
|
84
|
+
turnSearchKeys.set(sessionKey, new Set([key]));
|
|
85
|
+
// Prune stale entries.
|
|
86
|
+
if (turnSearchKeys.size > TURN_SEARCH_MAX_KEYS) {
|
|
87
|
+
const oldest = turnSearchKeys.keys().next().value;
|
|
88
|
+
if (oldest !== undefined)
|
|
89
|
+
turnSearchKeys.delete(oldest);
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
if (keys.has(key))
|
|
94
|
+
return true;
|
|
95
|
+
keys.add(key);
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
70
98
|
async function getManager(ctx, purpose) {
|
|
71
99
|
const key = managerCacheKey(ctx);
|
|
72
100
|
let manager = managers.get(key);
|
|
@@ -95,6 +123,13 @@ export function createLibraVdbMemoryTools(getClient, cfg, logger = console) {
|
|
|
95
123
|
execute: async (_toolCallId, rawParams) => {
|
|
96
124
|
const params = asToolParamsRecord(rawParams);
|
|
97
125
|
const query = readRequiredStringParam(params, "query");
|
|
126
|
+
const sessionKey = ctx.sessionKey ?? "";
|
|
127
|
+
if (isDuplicateSearch(sessionKey, query)) {
|
|
128
|
+
return jsonToolResult({
|
|
129
|
+
results: [],
|
|
130
|
+
error: `Duplicate search blocked. You already searched this turn — use the previous results. Do not call memory_search again.`,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
98
133
|
const corpus = readMemoryCorpus(params.corpus);
|
|
99
134
|
const kind = typeof params.kind === "string" ? params.kind : undefined;
|
|
100
135
|
const signals = Array.isArray(params.signals) ? params.signals.filter((s) => typeof s === "string") : undefined;
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xdarkicex/openclaw-memory-libravdb",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -79,6 +79,6 @@
|
|
|
79
79
|
"dependencies": {
|
|
80
80
|
"@connectrpc/connect": "^1.7.0",
|
|
81
81
|
"@connectrpc/connect-node": "^1.7.0",
|
|
82
|
-
"@xdarkicex/libravdb-contracts": "^2.0.
|
|
82
|
+
"@xdarkicex/libravdb-contracts": "^2.0.20"
|
|
83
83
|
}
|
|
84
84
|
}
|