@xdarkicex/openclaw-memory-libravdb 1.3.19 → 1.3.21

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.
@@ -12,7 +12,9 @@ import {
12
12
  rankSection7VariantCandidates,
13
13
  } from "./scoring.js";
14
14
  import { buildInjectedMemoryMessageContent, buildMemoryHeader, recentIds } from "./recall-utils.js";
15
- import { countTokens, estimateTokens, fitPromptBudget } from "./tokens.js";
15
+ import { detectTemporalQuerySignal, rankTemporalRecoveryCandidates } from "./temporal.js";
16
+ import type { TemporalRecoveryRankingResult } from "./temporal.js";
17
+ import { countTokens, estimateTokens, fitPromptBudget, fitPromptBudgetFirstFit } from "./tokens.js";
16
18
  import type { RpcGetter } from "./plugin-runtime.js";
17
19
  import type {
18
20
  ContextAssembleArgs,
@@ -57,6 +59,7 @@ export function buildContextEngineFactory(
57
59
  }
58
60
 
59
61
  return {
62
+ info: { id: "libravdb-memory" },
60
63
  ownsCompaction: true,
61
64
  async bootstrap({ sessionId, userId }: ContextBootstrapArgs) {
62
65
  const rpc = await getRpc();
@@ -190,6 +193,7 @@ export function buildContextEngineFactory(
190
193
  systemPromptAddition: "",
191
194
  } satisfies ContextAssembleResult;
192
195
  }
196
+ const temporalQuery = detectTemporalQuerySignal(queryText);
193
197
 
194
198
  const excluded = recentIds(messages, 4);
195
199
  const cached = recallCache.take({ userId, queryText });
@@ -253,6 +257,7 @@ export function buildContextEngineFactory(
253
257
  cached,
254
258
  excluded,
255
259
  queryText,
260
+ temporalQuery,
256
261
  sessionId,
257
262
  userId,
258
263
  messages,
@@ -287,6 +292,7 @@ export function buildContextEngineFactory(
287
292
  cached,
288
293
  excluded,
289
294
  queryText,
295
+ temporalQuery,
290
296
  sessionId,
291
297
  userId,
292
298
  messages,
@@ -303,6 +309,7 @@ export function buildContextEngineFactory(
303
309
  cached: ReturnType<RecallCache<SearchResult>["take"]>;
304
310
  excluded: string[];
305
311
  queryText: string;
312
+ temporalQuery: ReturnType<typeof detectTemporalQuerySignal>;
306
313
  sessionId: string;
307
314
  userId: string;
308
315
  messages: Array<{ role: string; content: string }>;
@@ -562,6 +569,7 @@ export function buildContextEngineFactory(
562
569
  // it never modifies the C_total(q) output and does not spend from tau_V.
563
570
  let recoveryItems: SearchResult[] = [];
564
571
  let rawUserRecoveryDebug: NonNullable<NonNullable<ContextAssembleResult["_debug"]>["rawUserRecoveryCandidates"]> = [];
572
+ let temporalRecoveryResult: TemporalRecoveryRankingResult | null = null;
565
573
  if (recoveryTrigger.fire || crossSessionRawRecovery) {
566
574
  profiler?.mark("recovery_expand");
567
575
  const recoveryExcludeIDs = [...excluded, ...recentTailIDs, ...theoremSelectedIDs];
@@ -599,14 +607,44 @@ export function buildContextEngineFactory(
599
607
  k: Math.max((cfg.topK ?? 8) * 4, 8),
600
608
  excludeIds: recoveryExcludeIDs,
601
609
  });
602
- const reranked = rankRawUserRecoveryCandidates(
603
- annotateCollection(rawUserResults.results ?? [], `turns:${userId}`),
604
- { queryText },
605
- );
610
+ const annotatedUserResults = annotateCollection(rawUserResults.results ?? [], `turns:${userId}`);
611
+ temporalRecoveryResult = temporalQuery.active
612
+ ? rankTemporalRecoveryCandidates(annotatedUserResults, {
613
+ queryText,
614
+ maxSelected: 3,
615
+ nowMs: Date.now(),
616
+ recencyLambda: cfg.recencyLambdaUser ?? 0.00001,
617
+ })
618
+ : null;
619
+ const reranked = temporalRecoveryResult
620
+ ? temporalRecoveryResult
621
+ : rankRawUserRecoveryCandidates(annotatedUserResults, { queryText });
606
622
  if (debugRecovery) {
607
623
  rawUserRecoveryDebug = reranked.debug.slice(0, 8).map((item) => ({
608
- ...item,
624
+ id: item.id,
625
+ text: item.text,
609
626
  selected: false,
627
+ tokenEstimate: estimateTokens(item.text),
628
+ temporalAnchorDensity: "temporalAnchorDensity" in item && typeof item.temporalAnchorDensity === "number"
629
+ ? item.temporalAnchorDensity
630
+ : 0,
631
+ semanticScore: "semanticScore" in item && typeof item.semanticScore === "number"
632
+ ? item.semanticScore
633
+ : 0,
634
+ slotCoverage: "slotCoverage" in item && typeof item.slotCoverage === "number"
635
+ ? item.slotCoverage
636
+ : undefined,
637
+ slotMatches: "slotMatches" in item && Array.isArray(item.slotMatches)
638
+ ? item.slotMatches
639
+ : undefined,
640
+ lexicalCoverage: "lexicalCoverage" in item && typeof item.lexicalCoverage === "number"
641
+ ? item.lexicalCoverage
642
+ : ("slotCoverage" in item && typeof item.slotCoverage === "number" ? item.slotCoverage : 0),
643
+ recencyScore: "recencyScore" in item && typeof item.recencyScore === "number"
644
+ ? item.recencyScore
645
+ : 0,
646
+ finalScore: typeof item.finalScore === "number" ? item.finalScore : 0,
647
+ rationale: typeof item.rationale === "string" ? item.rationale : "",
610
648
  }));
611
649
  }
612
650
  recoveryCandidates.push(
@@ -622,7 +660,7 @@ export function buildContextEngineFactory(
622
660
  );
623
661
  }
624
662
 
625
- const fittedRecovery = fitPromptBudget(
663
+ const fittedRecovery = fitPromptBudgetFirstFit(
626
664
  dedupeRecoveryCandidates(recoveryCandidates),
627
665
  recoveryReserveTokens,
628
666
  );
@@ -667,6 +705,11 @@ export function buildContextEngineFactory(
667
705
  ? {
668
706
  recoveryTriggerFired: recoveryTrigger.fire,
669
707
  crossSessionRawRecovery,
708
+ recoveryReserveTokens,
709
+ temporalQueryIndicator: temporalQuery.indicator,
710
+ temporalQueryActive: temporalQuery.active,
711
+ temporalQueryPatterns: temporalQuery.matchedPatterns,
712
+ temporalRecoverySlots: temporalRecoveryResult?.slots,
670
713
  rawUserRecoveryCandidates: rawUserRecoveryDebug,
671
714
  }
672
715
  : undefined,
@@ -1,87 +1,25 @@
1
+ import type { MemoryPromptSectionBuilder } from "openclaw/plugin-sdk/plugin-entry";
1
2
  import type { PluginConfig, RecallCache, SearchResult } from "./types.js";
2
3
  import type { RpcGetter } from "./plugin-runtime.js";
3
- import { scoreCandidates } from "./scoring.js";
4
- import { fitPromptBudget } from "./tokens.js";
5
- import { buildMemoryHeader } from "./recall-utils.js";
6
4
 
7
- const MEMORY_PROMPT_BUDGET = 800;
5
+ const MEMORY_PROMPT_HEADER = [
6
+ "## Memory",
7
+ "LibraVDB persistent memory is configured. Recalled memories may appear",
8
+ "in context via the context-engine assembler when available and relevant.",
9
+ "",
10
+ ] as const;
8
11
 
9
12
  export function buildMemoryPromptSection(
10
- getRpc: RpcGetter,
11
- cfg: PluginConfig,
12
- recallCache: RecallCache<SearchResult>,
13
- ): (params: {
14
- availableTools: Set<string>;
15
- citationsMode?: string;
16
- messages?: Array<{ role: string; content: string }>;
17
- userId?: string;
18
- }) => Promise<string[]> {
19
- return async function memoryPromptSection(params: {
20
- availableTools: Set<string>;
21
- citationsMode?: string;
22
- messages?: Array<{ role: string; content: string }>;
23
- userId?: string;
24
- }): Promise<string[]> {
25
- const queryText = params.messages?.at(-1)?.content ?? "";
26
- const userId = params.userId ?? "default";
27
-
28
- if (!queryText) {
29
- return [
30
- "## Memory",
31
- "LibraVDB persistent memory is active. Recalled memories will appear",
32
- "in context via the context-engine assembler when relevant.",
33
- "",
34
- ];
35
- }
36
-
37
- const rpc = await getRpc();
38
-
39
- const [userHitsResult, globalHitsResult] = await Promise.all([
40
- rpc.call<{ results: SearchResult[] }>("search_text", {
41
- collection: `user:${userId}`,
42
- text: queryText,
43
- k: Math.ceil((cfg.topK ?? 8) / 2),
44
- }),
45
- rpc.call<{ results: SearchResult[] }>("search_text", {
46
- collection: "global",
47
- text: queryText,
48
- k: Math.ceil((cfg.topK ?? 8) / 4),
49
- }),
50
- ]);
51
-
52
- const userHits = userHitsResult.results;
53
- const globalHits = globalHitsResult.results;
54
-
55
- recallCache.put({
56
- userId,
57
- queryText,
58
- durableVariantHits: [],
59
- userHits,
60
- globalHits,
61
- });
62
-
63
- const ranked = scoreCandidates([...userHits, ...globalHits], {
64
- alpha: cfg.alpha,
65
- beta: cfg.beta,
66
- gamma: cfg.gamma,
67
- sessionId: "",
68
- userId,
69
- });
70
-
71
- const selected = fitPromptBudget(ranked, MEMORY_PROMPT_BUDGET);
72
- const recallHeader = buildMemoryHeader(selected);
73
-
74
- const lines: string[] = [
75
- "## Memory",
76
- "LibraVDB persistent memory is active. Recalled memories will appear",
77
- "in context via the context-engine assembler when relevant.",
78
- ];
79
-
80
- if (recallHeader) {
81
- lines.push(...recallHeader.split("\n"));
82
- }
83
-
84
- lines.push("");
85
- return lines;
13
+ _getRpc: RpcGetter,
14
+ _cfg: PluginConfig,
15
+ _recallCache: RecallCache<SearchResult>,
16
+ ): MemoryPromptSectionBuilder {
17
+ return function memoryPromptSection({
18
+ availableTools: _availableTools,
19
+ citationsMode: _citationsMode,
20
+ }): string[] {
21
+ // OpenClaw builds the memory prompt section synchronously for embedded runs.
22
+ // Actual retrieval and ranking happen in the context engine during assemble().
23
+ return [...MEMORY_PROMPT_HEADER];
86
24
  };
87
- }
25
+ }
@@ -1,4 +1,9 @@
1
1
  declare module "openclaw/plugin-sdk/plugin-entry" {
2
+ export type MemoryPromptSectionBuilder = (params: {
3
+ availableTools: Set<string>;
4
+ citationsMode?: string;
5
+ }) => string[];
6
+
2
7
  interface OpenClawCliCommand {
3
8
  commands?: OpenClawCliCommand[];
4
9
  command(name: string): OpenClawCliCommand;
@@ -18,7 +23,7 @@ declare module "openclaw/plugin-sdk/plugin-entry" {
18
23
  warn?(message: string): void;
19
24
  };
20
25
  registerContextEngine(id: string, factory: () => unknown): void;
21
- registerMemoryPromptSection(builder: unknown): void;
26
+ registerMemoryPromptSection(builder: MemoryPromptSectionBuilder): void;
22
27
  registerMemoryFlushPlan?(resolver: unknown): void;
23
28
  registerMemoryRuntime?(runtime: unknown): void;
24
29
  registerMemoryEmbeddingProvider?(provider: unknown): void;
package/src/scoring.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { SearchResult } from "./types.js";
2
+ import { getTemporalAnchorDensity } from "./temporal.js";
2
3
 
3
4
  interface HybridOptions {
4
5
  alpha?: number;
@@ -41,6 +42,7 @@ interface RawUserRecoveryOptions {
41
42
  export interface RawUserRecoveryDebugCandidate {
42
43
  id: string;
43
44
  text: string;
45
+ temporalAnchorDensity: number;
44
46
  semanticScore: number;
45
47
  lexicalCoverage: number;
46
48
  recencyScore: number;
@@ -319,17 +321,29 @@ export function rankRawUserRecoveryCandidates(
319
321
  const now = opts.nowMs ?? Date.now();
320
322
  const recencyLambda = Math.max(0, opts.recencyLambda ?? 0.00001);
321
323
  const keywords = extractKeywords(opts.queryText);
324
+ const intentPhrases = extractIntentPhrases(opts.queryText);
322
325
 
323
326
  const ranked = items
324
327
  .map((item) => {
325
328
  const semanticScore = clamp01(typeof item.score === "number" ? item.score : 0);
326
329
  const lexicalCoverage = normalizedKeywordCoverage(keywords, item.text);
327
330
  const recencyScore = computeRecencyScore(item, now, recencyLambda);
328
- const finalScore = clamp01((0.30 * semanticScore) + (0.60 * lexicalCoverage) + (0.10 * recencyScore));
331
+ const temporalAnchorDensity = getTemporalAnchorDensity(
332
+ `${typeof item.metadata.collection === "string" ? item.metadata.collection : "unknown"}::${item.id}`,
333
+ item.text,
334
+ );
335
+ const intentAlignmentBonus = computeIntentAlignmentBonus(item.text, intentPhrases);
336
+ const finalScore = clamp01(
337
+ (0.30 * semanticScore) +
338
+ (0.60 * lexicalCoverage) +
339
+ (0.10 * recencyScore) +
340
+ intentAlignmentBonus,
341
+ );
329
342
  const rationale = buildRawUserRecoveryRationale({
330
343
  semanticScore,
331
344
  lexicalCoverage,
332
345
  recencyScore,
346
+ intentAlignmentBonus,
333
347
  });
334
348
 
335
349
  return {
@@ -340,6 +354,7 @@ export function rankRawUserRecoveryCandidates(
340
354
  debug: {
341
355
  id: item.id,
342
356
  text: item.text,
357
+ temporalAnchorDensity,
343
358
  semanticScore,
344
359
  lexicalCoverage,
345
360
  recencyScore,
@@ -473,7 +488,11 @@ function buildRawUserRecoveryRationale(scores: {
473
488
  semanticScore: number;
474
489
  lexicalCoverage: number;
475
490
  recencyScore: number;
491
+ intentAlignmentBonus: number;
476
492
  }): string {
493
+ if (scores.intentAlignmentBonus >= 0.04) {
494
+ return "intent phrase overlap lifted this candidate toward the query's direct ask";
495
+ }
477
496
  const lexicalDelta = scores.lexicalCoverage - scores.semanticScore;
478
497
  if (lexicalDelta > 0.15) {
479
498
  return "lexical coverage lifted this candidate above its semantic score";
@@ -487,6 +506,79 @@ function buildRawUserRecoveryRationale(scores: {
487
506
  return "semantic and lexical scores were balanced";
488
507
  }
489
508
 
509
+ function computeIntentAlignmentBonus(text: string, intentPhrases: string[]): number {
510
+ if (intentPhrases.length === 0) {
511
+ return 0;
512
+ }
513
+ const normalized = normalizeTextForPhraseMatch(text);
514
+ const matched = intentPhrases.filter((phrase) => normalized.includes(phrase)).length;
515
+ if (matched === 0) {
516
+ return 0;
517
+ }
518
+ return Math.min(0.08, matched * 0.02);
519
+ }
520
+
521
+ function extractIntentPhrases(text: string): string[] {
522
+ const terms = normalizeTerms(text).filter((term) => !INTENT_STOPWORDS.has(term));
523
+ const phrases: string[] = [];
524
+ for (let size = 4; size >= 2; size -= 1) {
525
+ for (let i = 0; i <= terms.length - size; i += 1) {
526
+ const phraseTerms = terms.slice(i, i + size);
527
+ if (phraseTerms.some((term) => term.length < 3)) {
528
+ continue;
529
+ }
530
+ const phrase = phraseTerms.join(" ");
531
+ if (!phrases.includes(phrase)) {
532
+ phrases.push(phrase);
533
+ }
534
+ }
535
+ }
536
+ return phrases.slice(0, 12);
537
+ }
538
+
539
+ function normalizeTextForPhraseMatch(text: string): string {
540
+ return normalizeTerms(text).join(" ");
541
+ }
542
+
543
+ const INTENT_STOPWORDS = new Set([
544
+ "the",
545
+ "and",
546
+ "for",
547
+ "with",
548
+ "that",
549
+ "this",
550
+ "have",
551
+ "from",
552
+ "your",
553
+ "what",
554
+ "when",
555
+ "where",
556
+ "which",
557
+ "would",
558
+ "could",
559
+ "should",
560
+ "about",
561
+ "into",
562
+ "some",
563
+ "before",
564
+ "after",
565
+ "them",
566
+ "they",
567
+ "been",
568
+ "just",
569
+ "want",
570
+ "looking",
571
+ "look",
572
+ "help",
573
+ "need",
574
+ "recommend",
575
+ "suggestions",
576
+ "suggest",
577
+ "advice",
578
+ "think",
579
+ "also",
580
+ ]);
581
+
490
582
  function extractKeywords(text: string): string[] {
491
583
  const tokens = normalizeTerms(text);
492
584
  const seen = new Set<string>();
package/src/sidecar.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import fs from "node:fs";
1
2
  import net from "node:net";
2
3
  import os from "node:os";
3
4
  import path from "node:path";
@@ -268,13 +269,42 @@ export function daemonProvisioningHint(): string {
268
269
  }
269
270
 
270
271
  export function defaultEndpoint(platform = process.platform, homeDir = os.homedir()): string {
272
+ // Honour the daemon's own env var first (set by Homebrew LaunchAgent / systemd unit).
273
+ const envEndpoint = process.env.LIBRAVDB_RPC_ENDPOINT?.trim();
274
+ if (envEndpoint && isConfiguredEndpoint(envEndpoint)) {
275
+ return envEndpoint;
276
+ }
277
+
271
278
  if (platform === "win32") {
272
279
  return "tcp:127.0.0.1:37421";
273
280
  }
281
+
282
+ const sockName = "libravdb.sock";
283
+ const candidateDirs = [
284
+ // User-local (npm plugin convention)
285
+ homeDir?.trim() ? path.join(homeDir, ".clawdb", "run") : null,
286
+ // Homebrew (Apple Silicon) — matches the Homebrew formula LaunchAgent
287
+ "/opt/homebrew/var/clawdb/run",
288
+ // Homebrew (Intel Mac) / manual Linux installs
289
+ "/usr/local/var/clawdb/run",
290
+ ].filter((d): d is string => d !== null);
291
+
292
+ for (const dir of candidateDirs) {
293
+ const sockPath = path.join(dir, sockName);
294
+ try {
295
+ if (fs.existsSync(sockPath)) {
296
+ return `unix:${sockPath}`;
297
+ }
298
+ } catch {
299
+ // Permission error or similar — skip this candidate.
300
+ }
301
+ }
302
+
303
+ // Fallback to the original user-local path so error messages stay familiar.
274
304
  const baseDir = homeDir?.trim()
275
305
  ? path.join(homeDir, ".clawdb", "run")
276
306
  : path.join(".", ".clawdb", "run");
277
- return `unix:${path.join(baseDir, "libravdb.sock")}`;
307
+ return `unix:${path.join(baseDir, sockName)}`;
278
308
  }
279
309
 
280
310
  export function buildSidecarEnv(cfg: PluginConfig): Record<string, string> {