@xdarkicex/openclaw-memory-libravdb 1.4.6 → 1.4.8

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.
Files changed (75) hide show
  1. package/HOOK.md +14 -0
  2. package/README.md +32 -2
  3. package/dist/cli.d.ts +39 -0
  4. package/dist/cli.js +208 -0
  5. package/dist/context-engine.d.ts +56 -0
  6. package/dist/context-engine.js +125 -0
  7. package/dist/dream-promotion.d.ts +47 -0
  8. package/dist/dream-promotion.js +363 -0
  9. package/dist/dream-routing.d.ts +6 -0
  10. package/dist/dream-routing.js +31 -0
  11. package/dist/durable-namespace.d.ts +6 -0
  12. package/dist/durable-namespace.js +24 -0
  13. package/dist/grpc-client.d.ts +23 -0
  14. package/dist/grpc-client.js +104 -0
  15. package/dist/index.d.ts +10 -0
  16. package/dist/index.js +40 -0
  17. package/dist/lifecycle-hooks.d.ts +4 -0
  18. package/dist/lifecycle-hooks.js +64 -0
  19. package/dist/markdown-hash.d.ts +3 -0
  20. package/dist/markdown-hash.js +82 -0
  21. package/dist/markdown-ingest.d.ts +43 -0
  22. package/dist/markdown-ingest.js +464 -0
  23. package/dist/memory-provider.d.ts +4 -0
  24. package/dist/memory-provider.js +13 -0
  25. package/dist/memory-runtime.d.ts +118 -0
  26. package/dist/memory-runtime.js +217 -0
  27. package/dist/plugin-runtime.d.ts +28 -0
  28. package/dist/plugin-runtime.js +127 -0
  29. package/dist/proto/intelligence_kernel/v1/kernel.proto +378 -0
  30. package/dist/recall-cache.d.ts +2 -0
  31. package/dist/recall-cache.js +30 -0
  32. package/dist/rpc-protobuf-codecs.d.ts +70 -0
  33. package/dist/rpc-protobuf-codecs.js +77 -0
  34. package/dist/rpc.d.ts +14 -0
  35. package/dist/rpc.js +121 -0
  36. package/dist/sidecar.d.ts +34 -0
  37. package/dist/sidecar.js +535 -0
  38. package/dist/types.d.ts +163 -0
  39. package/dist/types.js +1 -0
  40. package/docs/contributing.md +14 -13
  41. package/docs/install.md +7 -9
  42. package/docs/installation.md +23 -16
  43. package/docs/uninstall.md +1 -1
  44. package/index.js +2 -0
  45. package/openclaw.plugin.json +2 -2
  46. package/package.json +39 -16
  47. package/packaging/README.md +0 -71
  48. package/packaging/homebrew/libravdbd.rb.tmpl +0 -224
  49. package/packaging/launchd/com.xdarkicex.libravdbd.plist +0 -32
  50. package/packaging/systemd/libravdbd.service +0 -12
  51. package/src/cli.ts +0 -299
  52. package/src/comparison-experiments.ts +0 -128
  53. package/src/context-engine.ts +0 -1645
  54. package/src/continuity.ts +0 -93
  55. package/src/dream-promotion.ts +0 -492
  56. package/src/dream-routing.ts +0 -40
  57. package/src/durable-namespace.ts +0 -34
  58. package/src/index.ts +0 -47
  59. package/src/lifecycle-hooks.ts +0 -96
  60. package/src/markdown-hash.ts +0 -104
  61. package/src/markdown-ingest.ts +0 -627
  62. package/src/memory-provider.ts +0 -25
  63. package/src/memory-runtime.ts +0 -283
  64. package/src/openclaw-plugin-sdk.d.ts +0 -59
  65. package/src/plugin-runtime.ts +0 -119
  66. package/src/recall-cache.ts +0 -34
  67. package/src/recall-utils.ts +0 -131
  68. package/src/rpc.ts +0 -92
  69. package/src/scoring.ts +0 -632
  70. package/src/sidecar.ts +0 -583
  71. package/src/temporal.ts +0 -1031
  72. package/src/tokens.ts +0 -52
  73. package/src/types.ts +0 -278
  74. package/tsconfig.json +0 -20
  75. package/tsconfig.tests.json +0 -12
@@ -1,1645 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import {
3
- DEFAULT_CONTINUITY_MIN_TURNS,
4
- DEFAULT_CONTINUITY_PRIOR_CONTEXT_TOKENS,
5
- DEFAULT_CONTINUITY_TAIL_BUDGET_TOKENS,
6
- selectRecentTail,
7
- } from "./continuity.js";
8
- import {
9
- detectRetrievalFailure,
10
- expandSection7HopCandidates,
11
- rankRawUserRecoveryCandidates,
12
- mergeSection7VariantCandidates,
13
- rankSection7VariantCandidates,
14
- } from "./scoring.js";
15
- import { buildInjectedMemoryMessageContent, buildMemoryHeader, recentIds } from "./recall-utils.js";
16
- import { detectDreamQuerySignal, resolveDreamCollection } from "./dream-routing.js";
17
- import { resolveComparisonExperimentConfig } from "./comparison-experiments.js";
18
- import {
19
- decideTemporalSelectorGuard,
20
- detectTemporalQuerySignal,
21
- rankTemporalRecoveryCandidates,
22
- } from "./temporal.js";
23
- import type { TemporalRecoveryRankingResult } from "./temporal.js";
24
- import { countTokens, estimateTokens, fitPromptBudget, fitPromptBudgetFirstFit } from "./tokens.js";
25
- import { resolveDurableNamespace } from "./durable-namespace.js";
26
- import type { RpcGetter } from "./plugin-runtime.js";
27
- import type {
28
- ContextAssembleArgs,
29
- ContextAssembleResult,
30
- ContextBootstrapArgs,
31
- ContextCompactArgs,
32
- ContextIngestArgs,
33
- GatingResult,
34
- LoggerLike,
35
- MemoryMessage,
36
- PluginConfig,
37
- RecallCache,
38
- SearchResult,
39
- } from "./types.js";
40
-
41
- const AUTHORED_HARD_COLLECTION = "authored:hard";
42
- const AUTHORED_SOFT_COLLECTION = "authored:soft";
43
- const AUTHORED_VARIANT_COLLECTION = "authored:variant";
44
- const ELEVATED_USER_COLLECTION_PREFIX = "elevated:user:";
45
- const ELEVATED_SESSION_COLLECTION_PREFIX = "elevated:session:";
46
- const SESSION_RECALL_COLLECTION_PREFIX = "session_recall:";
47
- const SESSION_RAW_COLLECTION_PREFIX = "session_raw:";
48
- const SESSION_SUMMARY_COLLECTION_PREFIX = "session_summary:";
49
- const SESSION_EDGE_COLLECTION_PREFIX = "session_edge:";
50
- const SESSION_STATE_COLLECTION_PREFIX = "session_state:";
51
- const AFTER_TURN_DEDUPE_TTL_MS = 60 * 60 * 1000;
52
- const AFTER_TURN_DEDUPE_MAX_ENTRIES = 1024;
53
-
54
- function mapTemporalRecoveryDebugCandidates(
55
- ranking: TemporalRecoveryRankingResult,
56
- ): NonNullable<NonNullable<ContextAssembleResult["_debug"]>["rawUserRecoveryCandidates"]> {
57
- return ranking.debug.slice(0, 8).map((item) => ({
58
- id: item.id,
59
- text: item.text,
60
- selected: "selected" in item ? Boolean(item.selected) : false,
61
- tokenEstimate: estimateTokens(item.text),
62
- temporalAnchorDensity: "temporalAnchorDensity" in item && typeof item.temporalAnchorDensity === "number"
63
- ? item.temporalAnchorDensity
64
- : 0,
65
- semanticScore: "semanticScore" in item && typeof item.semanticScore === "number"
66
- ? item.semanticScore
67
- : 0,
68
- slotCoverage: "slotCoverage" in item && typeof item.slotCoverage === "number"
69
- ? item.slotCoverage
70
- : undefined,
71
- slotMatches: "slotMatches" in item && Array.isArray(item.slotMatches)
72
- ? item.slotMatches
73
- : undefined,
74
- lexicalCoverage: "lexicalCoverage" in item && typeof item.lexicalCoverage === "number"
75
- ? item.lexicalCoverage
76
- : ("slotCoverage" in item && typeof item.slotCoverage === "number" ? item.slotCoverage : 0),
77
- recencyScore: "recencyScore" in item && typeof item.recencyScore === "number"
78
- ? item.recencyScore
79
- : 0,
80
- finalScore: typeof item.finalScore === "number" ? item.finalScore : 0,
81
- rationale: typeof item.rationale === "string" ? item.rationale : "",
82
- comparisonSide: "comparisonSide" in item && (item.comparisonSide === 0 || item.comparisonSide === 1 || item.comparisonSide === null)
83
- ? item.comparisonSide
84
- : undefined,
85
- comparisonSlot: "comparisonSlot" in item && typeof item.comparisonSlot === "string"
86
- ? item.comparisonSlot
87
- : undefined,
88
- comparisonSlotRecall: "comparisonSlotRecall" in item && typeof item.comparisonSlotRecall === "number"
89
- ? item.comparisonSlotRecall
90
- : undefined,
91
- comparisonSlotPrecision: "comparisonSlotPrecision" in item && typeof item.comparisonSlotPrecision === "number"
92
- ? item.comparisonSlotPrecision
93
- : undefined,
94
- comparisonSlotSpecificity: "comparisonSlotSpecificity" in item && typeof item.comparisonSlotSpecificity === "number"
95
- ? item.comparisonSlotSpecificity
96
- : undefined,
97
- comparisonSlotPositionWeightedRecall: "comparisonSlotPositionWeightedRecall" in item && typeof item.comparisonSlotPositionWeightedRecall === "number"
98
- ? item.comparisonSlotPositionWeightedRecall
99
- : undefined,
100
- comparisonSlotPositionWeightedPrecision: "comparisonSlotPositionWeightedPrecision" in item && typeof item.comparisonSlotPositionWeightedPrecision === "number"
101
- ? item.comparisonSlotPositionWeightedPrecision
102
- : undefined,
103
- comparisonSlotPositionWeightedSpecificity: "comparisonSlotPositionWeightedSpecificity" in item && typeof item.comparisonSlotPositionWeightedSpecificity === "number"
104
- ? item.comparisonSlotPositionWeightedSpecificity
105
- : undefined,
106
- comparisonFirstPersonClauseCount: "comparisonFirstPersonClauseCount" in item && typeof item.comparisonFirstPersonClauseCount === "number"
107
- ? item.comparisonFirstPersonClauseCount
108
- : undefined,
109
- comparisonProspectivePersonalVerbCount: "comparisonProspectivePersonalVerbCount" in item && typeof item.comparisonProspectivePersonalVerbCount === "number"
110
- ? item.comparisonProspectivePersonalVerbCount
111
- : undefined,
112
- comparisonPlanningDensity: "comparisonPlanningDensity" in item && typeof item.comparisonPlanningDensity === "number"
113
- ? item.comparisonPlanningDensity
114
- : undefined,
115
- comparisonPastness: "comparisonPastness" in item && typeof item.comparisonPastness === "number"
116
- ? item.comparisonPastness
117
- : undefined,
118
- comparisonSideWitnessScore: "comparisonSideWitnessScore" in item && typeof item.comparisonSideWitnessScore === "number"
119
- ? item.comparisonSideWitnessScore
120
- : undefined,
121
- }));
122
- }
123
-
124
- export function buildContextEngineFactory(
125
- getRpc: RpcGetter,
126
- cfg: PluginConfig,
127
- recallCache: RecallCache<SearchResult>,
128
- logger: LoggerLike = console,
129
- ) {
130
- let authoredHardCache: SearchResult[] | null = null;
131
- let authoredSoftCache: SearchResult[] | null = null;
132
- let authoredVariantCache: SearchResult[] | null = null;
133
- const authoredVariantRecallCache = new Map<string, SearchResult[]>();
134
- const afterTurnIngestedKeys = new Map<string, number>();
135
- // Tracks accumulated uncompacted token count per session for auto-compaction
136
- const sessionTokenAccumulators = new Map<string, number>();
137
-
138
- // Session-scoped elevated-guidance cache keyed by sessionId + generation + durable namespace + queryText
139
- const elevatedRecallCache = new Map<string, SearchResult[]>();
140
- const elevatedRecallGeneration = new Map<string, number>();
141
-
142
- function clearElevatedCacheForSession(sessionId: string) {
143
- const nextGeneration = (elevatedRecallGeneration.get(sessionId) ?? 0) + 1;
144
- elevatedRecallGeneration.set(sessionId, nextGeneration);
145
- }
146
-
147
- return {
148
- info: { id: "libravdb-memory", name: "LibraVDB Memory", ownsCompaction: true },
149
- ownsCompaction: true,
150
- async bootstrap({ sessionId, sessionKey, userId }: ContextBootstrapArgs) {
151
- const durableNamespace = resolveDurableNamespace({ userId, sessionKey, fallback: `session:${sessionId}` });
152
- logger.info?.(
153
- `[libravdb] bootstrap sessionId=${sessionId} userId=${userId ?? "(none)"} sessionKey=${sessionKey ?? "(none)"} → durable=${durableNamespace}`,
154
- );
155
- const rpc = await getRpc();
156
- await rpc.call("ensure_collections", {
157
- collections: [
158
- `session:${sessionId}`,
159
- sessionRawCollection(sessionId),
160
- sessionSummaryCollection(sessionId),
161
- sessionEdgeCollection(sessionId),
162
- sessionStateCollection(sessionId),
163
- ...(useSessionRecallProjection(cfg) ? [sessionRecallCollection(sessionId)] : []),
164
- `turns:${durableNamespace}`,
165
- `user:${durableNamespace}`,
166
- "global",
167
- AUTHORED_HARD_COLLECTION,
168
- AUTHORED_SOFT_COLLECTION,
169
- AUTHORED_VARIANT_COLLECTION,
170
- ],
171
- });
172
- const [authoredHard, authoredSoft, authoredVariantRecords] = await loadAuthoredCollections(rpc, {
173
- hard: authoredHardCache,
174
- soft: authoredSoftCache,
175
- variant: authoredVariantCache,
176
- });
177
- authoredHardCache = authoredHard;
178
- authoredSoftCache = authoredSoft;
179
- authoredVariantCache = authoredVariantRecords;
180
- authoredVariantRecallCache.clear();
181
- if (useSessionRecallProjection(cfg)) {
182
- await rebuildSessionRecallProjection(rpc, cfg, sessionId);
183
- }
184
- validateSection7StartupHardReserve(cfg, authoredHard);
185
- return { ok: true };
186
- },
187
- async ingest({ sessionId, sessionKey, userId, message, isHeartbeat }: ContextIngestArgs) {
188
- if (isHeartbeat) {
189
- return { ingested: false };
190
- }
191
-
192
- const result = await ingestCanonicalMessage({
193
- getRpc,
194
- cfg,
195
- logger,
196
- recallCache,
197
- clearElevatedCacheForSession,
198
- sessionId,
199
- sessionKey,
200
- userId,
201
- message,
202
- });
203
- return { ingested: result.ingested };
204
- },
205
- async afterTurn({ sessionId, sessionKey, userId, messages, prePromptMessageCount, isHeartbeat }: {
206
- sessionId: string;
207
- sessionKey?: string;
208
- userId?: string;
209
- messages: Array<{ role: string; content: unknown }>;
210
- prePromptMessageCount: number;
211
- isHeartbeat?: boolean;
212
- }) {
213
- if (isHeartbeat) {
214
- return;
215
- }
216
-
217
- const startIndex = Math.max(0, prePromptMessageCount - 1);
218
- const turnMessages = messages.slice(startIndex);
219
- const normalizedTurnMessages = turnMessages.flatMap((turnMessage, offset) => {
220
- const normalized = normalizeHostMessage(turnMessage);
221
- if (!normalized) {
222
- return [];
223
- }
224
- return [{ index: startIndex + offset, normalized }] as const;
225
- });
226
- for (let offset = 0; offset < normalizedTurnMessages.length; offset++) {
227
- const { index, normalized } = normalizedTurnMessages[offset];
228
-
229
- const dedupeKey = `${sessionId}\n${index}\n${normalized.role}\n${hashMessageContent(normalized.content)}`;
230
- if (hasRecentAfterTurnIngest(afterTurnIngestedKeys, dedupeKey)) {
231
- continue;
232
- }
233
-
234
- const result = await ingestCanonicalMessage({
235
- getRpc,
236
- cfg,
237
- logger,
238
- recallCache,
239
- clearElevatedCacheForSession,
240
- sessionId,
241
- sessionKey,
242
- userId,
243
- message: {
244
- ...normalized,
245
- id: `after-turn:${index}`,
246
- },
247
- skipProjectionRebuild: offset !== normalizedTurnMessages.length - 1,
248
- });
249
- if (result.ingested) {
250
- rememberAfterTurnIngest(afterTurnIngestedKeys, dedupeKey);
251
- }
252
-
253
- // Auto-trigger compaction when the session's uncompacted token count exceeds
254
- // the budget. This keeps session size bounded without relying on the host
255
- // to call compact() explicitly.
256
- const sessionTokenBudget = cfg.compactSessionTokenBudget ?? 2000;
257
- if (sessionTokenBudget > 0) {
258
- const tokens = estimateTokens(normalized.content);
259
- const accumulated = (sessionTokenAccumulators.get(sessionId) ?? 0) + tokens;
260
- sessionTokenAccumulators.set(sessionId, accumulated);
261
- if (accumulated >= sessionTokenBudget) {
262
- sessionTokenAccumulators.set(sessionId, 0);
263
- void this.compact({ sessionId, force: false }).catch(() => {});
264
- }
265
- }
266
- }
267
- },
268
- async assemble({ sessionId, sessionKey, userId, messages, tokenBudget, ...rest }: ContextAssembleArgs & Record<string, unknown>) {
269
- const PROFILE = process.env.OPENCLAW_PROFILE_ASSEMBLE === "1";
270
- const DEBUG_RECOVERY = process.env.LONGMEMEVAL_DEBUG_RANKING === "1";
271
- const durableNamespace = resolveDurableNamespace({ userId, sessionKey, fallback: `session:${sessionId}` });
272
- const originalMessages = messages;
273
- const normalizedMessages = normalizeConversationMessages(messages as Array<{ role: string; content: unknown }>);
274
-
275
- const queryText =
276
- (typeof rest.prompt === "string" && rest.prompt.trim() ? rest.prompt : undefined) ??
277
- normalizedMessages.at(-1)?.content ?? "";
278
- if (!queryText) {
279
- return {
280
- messages: originalMessages,
281
- estimatedTokens: countTokens(originalMessages),
282
- systemPromptAddition: "",
283
- } satisfies ContextAssembleResult;
284
- }
285
- const dreamQuery = detectDreamQuerySignal(queryText);
286
- const temporalQuery = detectTemporalQuerySignal(queryText);
287
- const temporalSelectorGuard = decideTemporalSelectorGuard(queryText, temporalQuery);
288
- const comparisonExperiment = resolveComparisonExperimentConfig();
289
- const emitComparisonProfile = comparisonExperiment.profilingEnabled;
290
-
291
- const excluded = recentIds(normalizedMessages, 4);
292
- const cached = dreamQuery.active ? undefined : recallCache.take({ userId: durableNamespace, queryText });
293
-
294
- const rpc = await getRpc();
295
-
296
- // Use cached authored collections directly if available (bootstrap-loaded and sorted)
297
- // Only load as fallback if caches are unexpectedly null
298
- let authoredHard = authoredHardCache;
299
- let authoredSoft = authoredSoftCache;
300
- let authoredVariantRecords = authoredVariantCache;
301
- if (!authoredHard || !authoredSoft || !authoredVariantRecords) {
302
- const [loadedHard, loadedSoft, loadedVariant] = await loadAuthoredCollections(rpc, {
303
- hard: authoredHardCache,
304
- soft: authoredSoftCache,
305
- variant: authoredVariantCache,
306
- });
307
- authoredHard = loadedHard;
308
- authoredSoft = loadedSoft;
309
- authoredVariantRecords = loadedVariant;
310
- authoredHardCache = loadedHard;
311
- authoredSoftCache = loadedSoft;
312
- authoredVariantCache = loadedVariant;
313
- }
314
-
315
- // Profiler: null when disabled (zero overhead), object when enabled
316
- const profiler = PROFILE
317
- ? (() => {
318
- const marks: Array<[string, bigint]> = [];
319
- return {
320
- mark(label: string) {
321
- marks.push([label, process.hrtime.bigint()]);
322
- },
323
- lines() {
324
- const lines: string[] = [];
325
- for (let i = 0; i < marks.length - 1; i++) {
326
- const [name, start] = marks[i];
327
- const [, end] = marks[i + 1];
328
- const ms = Number(end - start) / 1_000_000;
329
- lines.push(`assemble profile: ${name}=${ms.toFixed(2)}ms`);
330
- }
331
- return lines;
332
- },
333
- emit() {
334
- for (const line of this.lines()) {
335
- logger.info?.(line);
336
- }
337
- },
338
- };
339
- })()
340
- : null;
341
-
342
- try {
343
- const result = await this.assembleCore({
344
- rpc,
345
- cfg,
346
- recallCache,
347
- authoredHard,
348
- authoredSoft,
349
- authoredVariantRecords,
350
- cached,
351
- excluded,
352
- queryText,
353
- dreamQuery,
354
- temporalQuery,
355
- temporalSelectorGuard,
356
- comparisonExperiment,
357
- emitComparisonProfile,
358
- sessionId,
359
- durableNamespace,
360
- visibleMessages: originalMessages,
361
- messages: normalizedMessages,
362
- tokenBudget,
363
- profiler,
364
- debugRecovery: DEBUG_RECOVERY,
365
- });
366
-
367
- const profileLines = profiler?.lines() ?? [];
368
- if (profiler) {
369
- profiler.emit();
370
- }
371
-
372
- return profileLines.length > 0
373
- ? { ...result, _profile: profileLines }
374
- : result;
375
- } catch {
376
- return {
377
- messages: originalMessages,
378
- estimatedTokens: countTokens(originalMessages),
379
- systemPromptAddition: "",
380
- } satisfies ContextAssembleResult;
381
- }
382
- },
383
- async assembleCore({
384
- rpc,
385
- cfg,
386
- recallCache,
387
- authoredHard,
388
- authoredSoft,
389
- authoredVariantRecords,
390
- cached,
391
- excluded,
392
- queryText,
393
- dreamQuery,
394
- temporalQuery,
395
- temporalSelectorGuard,
396
- comparisonExperiment,
397
- emitComparisonProfile,
398
- sessionId,
399
- durableNamespace,
400
- visibleMessages,
401
- messages,
402
- tokenBudget,
403
- profiler,
404
- debugRecovery,
405
- }: {
406
- rpc: Awaited<ReturnType<RpcGetter>>;
407
- cfg: PluginConfig;
408
- recallCache: RecallCache<SearchResult>;
409
- authoredHard: SearchResult[];
410
- authoredSoft: SearchResult[];
411
- authoredVariantRecords: SearchResult[];
412
- cached: ReturnType<RecallCache<SearchResult>["take"]>;
413
- excluded: string[];
414
- queryText: string;
415
- dreamQuery: ReturnType<typeof detectDreamQuerySignal>;
416
- temporalQuery: ReturnType<typeof detectTemporalQuerySignal>;
417
- temporalSelectorGuard: ReturnType<typeof decideTemporalSelectorGuard>;
418
- comparisonExperiment: ReturnType<typeof resolveComparisonExperimentConfig>;
419
- emitComparisonProfile: boolean;
420
- sessionId: string;
421
- durableNamespace: string;
422
- visibleMessages: MemoryMessage[];
423
- messages: Array<{ role: string; content: string }>;
424
- tokenBudget: number;
425
- profiler: { mark(label: string): void; emit(): void } | null;
426
- debugRecovery: boolean;
427
- }): Promise<ContextAssembleResult> {
428
- const memoryBudget = tokenBudget * (cfg.tokenBudgetFraction ?? 0.25);
429
- const hardItems = authoredHard;
430
- const hardUsed = tokenCostSum(hardItems);
431
- const dreamMode = dreamQuery.active;
432
- const dreamCollection = resolveDreamCollection(durableNamespace);
433
-
434
- if (dreamMode) {
435
- const authoredSoftTarget = Math.max(0, memoryBudget * (cfg.authoredSoftBudgetFraction ?? 0.3));
436
- const softBudget = Math.max(0, Math.min(authoredSoftTarget, memoryBudget - hardUsed));
437
- const softItems = fitPromptBudget(authoredSoft, softBudget);
438
- const remainingBudget = Math.max(0, memoryBudget - hardUsed - tokenCostSum(softItems));
439
-
440
- profiler?.mark("dream_search");
441
- const dreamTopK = Math.max(cfg.topK ?? 8, 1);
442
- const dreamHits = await rpc.call<{ results: SearchResult[] }>("search_text", {
443
- collection: dreamCollection,
444
- text: queryText,
445
- k: dreamTopK,
446
- });
447
-
448
- profiler?.mark("dream_rank");
449
- const rankedDream = rankSection7VariantCandidates(
450
- annotateCollection(dreamHits.results ?? [], dreamCollection),
451
- {
452
- queryText,
453
- k1: dreamTopK,
454
- k2: dreamTopK,
455
- theta1: cfg.section7Theta1,
456
- kappa: cfg.section7Kappa,
457
- authorityRecencyLambda: cfg.section7AuthorityRecencyLambda,
458
- authorityRecencyWeight: cfg.section7AuthorityRecencyWeight,
459
- authorityFrequencyWeight: cfg.section7AuthorityFrequencyWeight,
460
- authorityAuthoredWeight: cfg.section7AuthorityAuthoredWeight,
461
- sessionId,
462
- userId: durableNamespace,
463
- },
464
- );
465
- const dreamItems = fitPromptBudget(rankedDream, remainingBudget);
466
- const selected = [...hardItems, ...softItems, ...dreamItems];
467
- const selectedMessages = selected.map((item) => ({
468
- role: "system",
469
- content: buildInjectedMemoryMessageContent(item),
470
- }));
471
- return {
472
- messages: [...selectedMessages, ...visibleMessages],
473
- estimatedTokens: countTokens(selectedMessages) + countTokens(visibleMessages),
474
- systemPromptAddition: buildMemoryHeader(selected),
475
- };
476
- }
477
-
478
- profiler?.mark("session");
479
- const sessionRecords = await rpc.call<{ results: SearchResult[] }>("list_by_meta", {
480
- collection: `session:${sessionId}`,
481
- key: "sessionId",
482
- value: sessionId,
483
- });
484
- const rawSessionTurns = sortChronological(
485
- sessionRecords.results.filter((item) =>
486
- // cascade_tier is ranking metadata (cascade search tier); exclude from session history
487
- item.metadata.type !== "summary" &&
488
- item.metadata.type !== "guidance_shard" &&
489
- typeof item.metadata.cascade_tier !== "number"
490
- ),
491
- );
492
- const minTurns = cfg.continuityMinTurns ?? DEFAULT_CONTINUITY_MIN_TURNS;
493
- const tailTarget = cfg.continuityTailBudgetTokens ?? DEFAULT_CONTINUITY_TAIL_BUDGET_TOKENS;
494
- const baseTail = selectRecentTail(rawSessionTurns, {
495
- minTurns,
496
- tailBudgetTokens: 0,
497
- tokenCost,
498
- sameBundle: isContinuityBundleCoupled,
499
- });
500
- const baseTailUsed = baseTail.baseTokens;
501
- const configuredHardFraction = clampFraction(cfg.authoredHardBudgetFraction);
502
- const hardBudget = configuredHardFraction > 0 ? memoryBudget * configuredHardFraction : hardUsed;
503
- const degradedReasons: string[] = [];
504
- if (hardUsed > hardBudget + 1e-9) {
505
- degradedReasons.push("hard authored invariants exceed configured hard budget reserve");
506
- }
507
- if (hardUsed + baseTailUsed > memoryBudget + 1e-9) {
508
- degradedReasons.push("hard authored invariants plus mandatory recent-tail base exceed available memory budget");
509
- }
510
- if (degradedReasons.length > 0) {
511
- const degradedTail = markRecentTail(baseTail.base, baseTail.base.length);
512
- const selected = [...hardItems, ...degradedTail];
513
- const selectedMessages = selected.map((item) => ({
514
- role: "system",
515
- content: buildInjectedMemoryMessageContent(item),
516
- }));
517
- return {
518
- messages: [...selectedMessages, ...visibleMessages],
519
- estimatedTokens: countTokens(selectedMessages) + countTokens(visibleMessages),
520
- systemPromptAddition: buildDegradedMemoryHeader(degradedReasons, selected),
521
- };
522
- }
523
- const authoredSoftTarget = Math.max(0, memoryBudget * (cfg.authoredSoftBudgetFraction ?? 0.3));
524
- const softBudget = Math.max(0, Math.min(authoredSoftTarget, memoryBudget - hardUsed - baseTailUsed));
525
- const softItems = fitPromptBudget(authoredSoft, softBudget);
526
- const remainingAfterHardSoft = Math.max(0, memoryBudget - hardUsed - tokenCostSum(softItems));
527
- const effectiveTailBudget = Math.min(
528
- Math.max(tailTarget, baseTailUsed),
529
- remainingAfterHardSoft,
530
- );
531
- const recentTailSelection = selectRecentTail(rawSessionTurns, {
532
- minTurns,
533
- tailBudgetTokens: effectiveTailBudget,
534
- tokenCost,
535
- sameBundle: isContinuityBundleCoupled,
536
- });
537
- const recentTail = markRecentTail(
538
- recentTailSelection.recent,
539
- recentTailSelection.base.length,
540
- );
541
- const tailBaseItems = recentTail.slice(-recentTailSelection.base.length);
542
- const tailExtensionItems = recentTail.slice(0, Math.max(0, recentTail.length - recentTailSelection.base.length));
543
- const retrievalBudget = Math.max(0, memoryBudget - hardUsed - tokenCostSum(softItems) - tokenCostSum(recentTail));
544
- const recentTailIDs = recentTail.map((item) => item.id);
545
-
546
- const coarseTopK = Math.max(cfg.section7CoarseTopK ?? Math.max((cfg.topK ?? 8) * 2, 8), 1);
547
- const sessionSearchTopK = Math.max(cfg.topK ?? 8, 1);
548
- const secondPassTopK = Math.max(cfg.section7SecondPassTopK ?? (cfg.topK ?? 8), 1);
549
- const searchSessionRecall = useSessionRecallProjection(cfg);
550
- const searchSessionSummary = useSessionSummarySearchExperiment(cfg);
551
- let sessionSearchCollection = `session:${sessionId}`;
552
- let sessionExcludeIds = [...excluded, ...recentTailIDs];
553
- if (dreamMode) {
554
- sessionSearchCollection = dreamCollection;
555
- sessionExcludeIds = [...excluded];
556
- } else if (searchSessionSummary) {
557
- const summaryCollection = sessionSummaryCollection(sessionId);
558
- const summaryRecords = await rpc.call<{ results: SearchResult[] }>("list_collection", {
559
- collection: summaryCollection,
560
- });
561
- if (summaryRecords.results.length > 0) {
562
- sessionSearchCollection = summaryCollection;
563
- sessionExcludeIds = [...excluded];
564
- }
565
- } else if (searchSessionRecall) {
566
- sessionSearchCollection = sessionRecallCollection(sessionId);
567
- sessionExcludeIds = [...excluded, ...recentTailIDs.map(sessionRecallId)];
568
- }
569
-
570
- profiler?.mark("session_search");
571
- const [sessionHits] = await Promise.all([
572
- rpc.call<{ results: SearchResult[] }>("search_text", {
573
- collection: sessionSearchCollection,
574
- text: queryText,
575
- k: sessionSearchTopK,
576
- excludeIds: sessionExcludeIds,
577
- }),
578
- ]);
579
-
580
- profiler?.mark("recall_user_global");
581
- const [userHits, globalHits] = await Promise.all([
582
- dreamMode
583
- ? Promise.resolve({ results: [] as SearchResult[] })
584
- : cached?.userHits
585
- ? Promise.resolve({ results: cached.userHits })
586
- : rpc.call<{ results: SearchResult[] }>("search_text", {
587
- collection: `user:${durableNamespace}`,
588
- text: queryText,
589
- k: Math.ceil((cfg.topK ?? 8) / 2),
590
- }),
591
- dreamMode
592
- ? Promise.resolve({ results: [] as SearchResult[] })
593
- : cached?.globalHits
594
- ? Promise.resolve({ results: cached.globalHits })
595
- : rpc.call<{ results: SearchResult[] }>("search_text", {
596
- collection: "global",
597
- text: queryText,
598
- k: Math.ceil((cfg.topK ?? 8) / 4),
599
- }),
600
- ]);
601
-
602
- if (!dreamMode) {
603
- logger.info?.(
604
- `[libravdb] assemble recall durable=${durableNamespace} session=${sessionSearchCollection} sessionHits=${sessionHits.results.length} userHits=${userHits.results.length} globalHits=${globalHits.results.length}`,
605
- );
606
- }
607
-
608
- let temporalRecoveryPreviewDebug: NonNullable<NonNullable<ContextAssembleResult["_debug"]>["rawUserRecoveryCandidates"]> = [];
609
-
610
- if (!cached && !dreamMode) {
611
- recallCache.put({
612
- userId: durableNamespace,
613
- queryText,
614
- durableVariantHits: [],
615
- userHits: userHits.results,
616
- globalHits: globalHits.results,
617
- });
618
- }
619
-
620
- profiler?.mark("recall_authored_variant");
621
- const authoredVariantKey = `${queryText}\n${coarseTopK}`;
622
- const cachedAuthoredVariantHits = authoredVariantRecallCache.get(authoredVariantKey);
623
- const [authoredVariantHits] = await Promise.all([
624
- dreamMode
625
- ? Promise.resolve({ results: [] as SearchResult[] })
626
- : cachedAuthoredVariantHits
627
- ? Promise.resolve({ results: cachedAuthoredVariantHits })
628
- : rpc.call<{ results: SearchResult[] }>("search_text", {
629
- collection: AUTHORED_VARIANT_COLLECTION,
630
- text: queryText,
631
- k: coarseTopK,
632
- }),
633
- ]);
634
- if (!cachedAuthoredVariantHits) {
635
- authoredVariantRecallCache.set(authoredVariantKey, authoredVariantHits.results);
636
- }
637
-
638
- profiler?.mark("recall_elevated");
639
- const elevatedGeneration = elevatedRecallGeneration.get(sessionId) ?? 0;
640
- const elevatedKey = `${sessionId}\n${elevatedGeneration}\n${durableNamespace}\n${queryText}`;
641
- const cachedElevated = elevatedRecallCache.get(elevatedKey);
642
- const [elevatedHits] = await Promise.all([
643
- dreamMode
644
- ? Promise.resolve({ results: [] as SearchResult[] })
645
- : cachedElevated
646
- ? Promise.resolve({ results: cachedElevated })
647
- : rpc.call<{ results: SearchResult[] }>("search_text_collections", {
648
- collections: [
649
- `${ELEVATED_USER_COLLECTION_PREFIX}${durableNamespace}`,
650
- `${ELEVATED_SESSION_COLLECTION_PREFIX}${sessionId}`,
651
- ],
652
- text: queryText,
653
- k: coarseTopK,
654
- excludeByCollection: {},
655
- }),
656
- ]);
657
- if (!cachedElevated) {
658
- elevatedRecallCache.set(elevatedKey, elevatedHits.results);
659
- }
660
-
661
- if (process.env.LONGMEMEVAL_DEBUG_RANKING === "1" && temporalSelectorGuard.shouldApply && userHits.results.length > 0) {
662
- const previewRanking = rankTemporalRecoveryCandidates(annotateCollection(userHits.results, `user:${durableNamespace}`), {
663
- queryText,
664
- maxSelected: 3,
665
- nowMs: Date.now(),
666
- recencyLambda: cfg.recencyLambdaUser ?? 0.00001,
667
- selectionTokenBudget: Math.max(1, Math.min(memoryBudget, retrievalBudget)),
668
- });
669
- temporalRecoveryPreviewDebug = mapTemporalRecoveryDebugCandidates(previewRanking);
670
- }
671
-
672
- profiler?.mark("rank");
673
- const ranked = rankSection7VariantCandidates(
674
- [
675
- ...annotateCollection(sessionHits.results, sessionSearchCollection),
676
- ...elevatedHits.results,
677
- ...userHits.results,
678
- ...globalHits.results,
679
- ...authoredVariantHits.results,
680
- ],
681
- {
682
- queryText,
683
- k1: coarseTopK,
684
- k2: secondPassTopK,
685
- theta1: cfg.section7Theta1,
686
- kappa: cfg.section7Kappa,
687
- authorityRecencyLambda: cfg.section7AuthorityRecencyLambda,
688
- authorityRecencyWeight: cfg.section7AuthorityRecencyWeight,
689
- authorityFrequencyWeight: cfg.section7AuthorityFrequencyWeight,
690
- authorityAuthoredWeight: cfg.section7AuthorityAuthoredWeight,
691
- sessionId,
692
- userId: durableNamespace,
693
- },
694
- );
695
-
696
- profiler?.mark("hop");
697
- const hopExpanded = expandSection7HopCandidates(
698
- ranked,
699
- annotateCollection(authoredVariantRecords, AUTHORED_VARIANT_COLLECTION),
700
- {
701
- etaHop: cfg.section7HopEta,
702
- thetaHop: cfg.section7HopThreshold,
703
- },
704
- );
705
-
706
- profiler?.mark("fit");
707
- const mergedCandidates = mergeSection7VariantCandidates(ranked, hopExpanded);
708
- // Recovery trigger is evaluated before variant fitting so healthy sessions
709
- // do not lose recall budget to an unused recovery reserve.
710
- profiler?.mark("recovery_trigger");
711
- const recoveryTrigger = dreamMode
712
- ? {
713
- signal1CascadeTier3: false,
714
- signal2TopScoreBelowFloor: false,
715
- signal3AllSummariesLowConfidence: false,
716
- fire: false,
717
- }
718
- : detectRetrievalFailure(mergedCandidates, {
719
- floorScore: cfg.recoveryFloorScore ?? 0.15,
720
- minTopK: cfg.recoveryMinTopK ?? 4,
721
- meanConfidenceThresh: cfg.recoveryMinConfidenceMean ?? 0.5,
722
- });
723
- const crossSessionRawRecovery = !dreamMode &&
724
- rawSessionTurns.length === 0 &&
725
- sessionHits.results.length === 0;
726
- const baseRecoveryReserveTokens = (recoveryTrigger.fire || crossSessionRawRecovery)
727
- ? Math.min(memoryBudget, Math.max(Math.floor(memoryBudget * 0.10), 16), 128)
728
- : 0;
729
- const isComparisonTemporalRecovery = temporalSelectorGuard.shouldApply &&
730
- temporalQuery.matchedPatterns.includes("first or earlier");
731
- const recoveryReserveTokens = isComparisonTemporalRecovery &&
732
- !comparisonExperiment.disableReserveBump &&
733
- baseRecoveryReserveTokens > 0
734
- ? Math.min(memoryBudget, Math.max(baseRecoveryReserveTokens, Math.ceil(baseRecoveryReserveTokens * 1.8)))
735
- : baseRecoveryReserveTokens;
736
- const elevatedGuidanceBudget = Math.max(
737
- 0,
738
- Math.min(
739
- memoryBudget * (cfg.elevatedGuidanceBudgetFraction ?? 0.15),
740
- retrievalBudget,
741
- ),
742
- );
743
- const elevatedItems = fitPromptBudget(
744
- mergedCandidates.filter((item) => item.metadata.elevated_guidance === true),
745
- elevatedGuidanceBudget,
746
- );
747
- const remainingAfterElevated = Math.max(0, retrievalBudget - tokenCostSum(elevatedItems));
748
- const remainingForVariant = Math.max(0, remainingAfterElevated - recoveryReserveTokens);
749
- const variantItems = fitPromptBudget(
750
- mergedCandidates.filter((item) => item.metadata.elevated_guidance !== true),
751
- remainingForVariant,
752
- );
753
-
754
- // Build set of theorem-selected IDs for recovery deduplication.
755
- // Recovery should only append NEW raw evidence, not re-inject content already
756
- // selected by the normal assembly path (hard/soft/tail/elevated/variant).
757
- const theoremSelectedIDs = new Set([
758
- ...hardItems.map((i) => i.id),
759
- ...softItems.map((i) => i.id),
760
- ...tailBaseItems.map((i) => i.id),
761
- ...tailExtensionItems.map((i) => i.id),
762
- ...elevatedItems.map((i) => i.id),
763
- ...variantItems.map((i) => i.id),
764
- ]);
765
-
766
- // Recovery is a policy overlay — it appends raw content only when triggered,
767
- // it never modifies the C_total(q) output and does not spend from tau_V.
768
- let recoveryItems: SearchResult[] = [];
769
- let rawUserRecoveryDebug: NonNullable<NonNullable<ContextAssembleResult["_debug"]>["rawUserRecoveryCandidates"]> = [];
770
- let dedupedRecoveryDebug: NonNullable<NonNullable<ContextAssembleResult["_debug"]>["recoveryDedupedOrder"]> = [];
771
- let fittedRecoveryDebug: NonNullable<NonNullable<ContextAssembleResult["_debug"]>["recoveryFittedOrder"]> = [];
772
- let temporalRecoveryResult: TemporalRecoveryRankingResult | null = null;
773
- if (!dreamMode && (recoveryTrigger.fire || crossSessionRawRecovery)) {
774
- profiler?.mark("recovery_expand");
775
- const recoveryExcludeIDs = [...excluded, ...recentTailIDs, ...theoremSelectedIDs];
776
- const recoveryCandidates: SearchResult[] = [];
777
-
778
- if (recoveryTrigger.fire) {
779
- // Recovery searches immutable raw session history directly — never the active view,
780
- // elevated shards, or authored collections.
781
- const rawResults = await rpc.call<{ results: SearchResult[] }>("query_raw_session", {
782
- sessionId,
783
- text: queryText,
784
- k: Math.max(cfg.topK ?? 8, 4),
785
- excludeIds: recoveryExcludeIDs,
786
- });
787
- recoveryCandidates.push(
788
- ...(rawResults.results ?? []).map((item) => ({
789
- ...item,
790
- finalScore: typeof item.finalScore === "number" ? item.finalScore : item.score,
791
- metadata: {
792
- ...item.metadata,
793
- recovery_fallback: true,
794
- recovery_scope: "session_raw",
795
- },
796
- })),
797
- );
798
- }
799
-
800
- if (crossSessionRawRecovery) {
801
- // When a fresh query session has no searchable history yet, durable memory can be too
802
- // coarse for exact-turn recall. Search the immutable per-user raw turn index instead of
803
- // widening topK so precise historical turns still have a bounded path back into context.
804
- const rawUserResults = await rpc.call<{ results: SearchResult[] }>("search_text", {
805
- collection: `turns:${durableNamespace}`,
806
- text: queryText,
807
- k: Math.max((cfg.topK ?? 8) * 4, 8),
808
- excludeIds: recoveryExcludeIDs,
809
- });
810
- const annotatedUserResults = annotateCollection(rawUserResults.results ?? [], `turns:${durableNamespace}`);
811
- temporalRecoveryResult = temporalSelectorGuard.shouldApply
812
- ? rankTemporalRecoveryCandidates(annotatedUserResults, {
813
- queryText,
814
- maxSelected: 3,
815
- nowMs: Date.now(),
816
- recencyLambda: cfg.recencyLambdaUser ?? 0.00001,
817
- selectionTokenBudget: recoveryReserveTokens,
818
- })
819
- : null;
820
- const reranked = temporalRecoveryResult
821
- ? temporalRecoveryResult
822
- : rankRawUserRecoveryCandidates(annotatedUserResults, { queryText });
823
- if (debugRecovery) {
824
- rawUserRecoveryDebug = reranked.debug.slice(0, 8).map((item) => ({
825
- id: item.id,
826
- text: item.text,
827
- // This debug surface mirrors the recovery ranker itself. Packing and dedupe can
828
- // legitimately drop a ranked candidate later, but the host-flow temporal test needs
829
- // to see what the ranker selected before those downstream filters run.
830
- selected: "selected" in item ? Boolean(item.selected) : false,
831
- tokenEstimate: estimateTokens(item.text),
832
- temporalAnchorDensity: "temporalAnchorDensity" in item && typeof item.temporalAnchorDensity === "number"
833
- ? item.temporalAnchorDensity
834
- : 0,
835
- semanticScore: "semanticScore" in item && typeof item.semanticScore === "number"
836
- ? item.semanticScore
837
- : 0,
838
- slotCoverage: "slotCoverage" in item && typeof item.slotCoverage === "number"
839
- ? item.slotCoverage
840
- : undefined,
841
- slotMatches: "slotMatches" in item && Array.isArray(item.slotMatches)
842
- ? item.slotMatches
843
- : undefined,
844
- lexicalCoverage: "lexicalCoverage" in item && typeof item.lexicalCoverage === "number"
845
- ? item.lexicalCoverage
846
- : ("slotCoverage" in item && typeof item.slotCoverage === "number" ? item.slotCoverage : 0),
847
- recencyScore: "recencyScore" in item && typeof item.recencyScore === "number"
848
- ? item.recencyScore
849
- : 0,
850
- finalScore: typeof item.finalScore === "number" ? item.finalScore : 0,
851
- rationale: typeof item.rationale === "string" ? item.rationale : "",
852
- comparisonSide: "comparisonSide" in item && (item.comparisonSide === 0 || item.comparisonSide === 1 || item.comparisonSide === null)
853
- ? item.comparisonSide
854
- : undefined,
855
- comparisonSlot: "comparisonSlot" in item && typeof item.comparisonSlot === "string"
856
- ? item.comparisonSlot
857
- : undefined,
858
- comparisonSlotRecall: "comparisonSlotRecall" in item && typeof item.comparisonSlotRecall === "number"
859
- ? item.comparisonSlotRecall
860
- : undefined,
861
- comparisonSlotPrecision: "comparisonSlotPrecision" in item && typeof item.comparisonSlotPrecision === "number"
862
- ? item.comparisonSlotPrecision
863
- : undefined,
864
- comparisonSlotSpecificity: "comparisonSlotSpecificity" in item && typeof item.comparisonSlotSpecificity === "number"
865
- ? item.comparisonSlotSpecificity
866
- : undefined,
867
- comparisonSlotPositionWeightedRecall: "comparisonSlotPositionWeightedRecall" in item && typeof item.comparisonSlotPositionWeightedRecall === "number"
868
- ? item.comparisonSlotPositionWeightedRecall
869
- : undefined,
870
- comparisonSlotPositionWeightedPrecision: "comparisonSlotPositionWeightedPrecision" in item && typeof item.comparisonSlotPositionWeightedPrecision === "number"
871
- ? item.comparisonSlotPositionWeightedPrecision
872
- : undefined,
873
- comparisonSlotPositionWeightedSpecificity: "comparisonSlotPositionWeightedSpecificity" in item && typeof item.comparisonSlotPositionWeightedSpecificity === "number"
874
- ? item.comparisonSlotPositionWeightedSpecificity
875
- : undefined,
876
- comparisonFirstPersonClauseCount: "comparisonFirstPersonClauseCount" in item && typeof item.comparisonFirstPersonClauseCount === "number"
877
- ? item.comparisonFirstPersonClauseCount
878
- : undefined,
879
- comparisonProspectivePersonalVerbCount: "comparisonProspectivePersonalVerbCount" in item && typeof item.comparisonProspectivePersonalVerbCount === "number"
880
- ? item.comparisonProspectivePersonalVerbCount
881
- : undefined,
882
- comparisonPlanningDensity: "comparisonPlanningDensity" in item && typeof item.comparisonPlanningDensity === "number"
883
- ? item.comparisonPlanningDensity
884
- : undefined,
885
- comparisonPastness: "comparisonPastness" in item && typeof item.comparisonPastness === "number"
886
- ? item.comparisonPastness
887
- : undefined,
888
- comparisonSideWitnessScore: "comparisonSideWitnessScore" in item && typeof item.comparisonSideWitnessScore === "number"
889
- ? item.comparisonSideWitnessScore
890
- : undefined,
891
- }));
892
-
893
- // If the raw-turn recovery path gets fully deduped because the same turns were already
894
- // surfaced through the normal durable user collection, expose the ranker output from
895
- // that durable view as debug-only fallback. This keeps the host-flow temporal test
896
- // aligned with the actual ranking behavior without changing prompt assembly.
897
- if (rawUserRecoveryDebug.length === 0 && userHits.results.length > 0) {
898
- const debugUserRanking = temporalRecoveryResult
899
- ? temporalRecoveryResult
900
- : rankTemporalRecoveryCandidates(
901
- annotateCollection(userHits.results, `user:${durableNamespace}`),
902
- {
903
- queryText,
904
- maxSelected: 3,
905
- nowMs: Date.now(),
906
- recencyLambda: cfg.recencyLambdaUser ?? 0.00001,
907
- selectionTokenBudget: recoveryReserveTokens,
908
- },
909
- );
910
- rawUserRecoveryDebug = debugUserRanking.debug.slice(0, 8).map((item) => ({
911
- id: item.id,
912
- text: item.text,
913
- selected: "selected" in item ? Boolean(item.selected) : false,
914
- tokenEstimate: estimateTokens(item.text),
915
- temporalAnchorDensity: "temporalAnchorDensity" in item && typeof item.temporalAnchorDensity === "number"
916
- ? item.temporalAnchorDensity
917
- : 0,
918
- semanticScore: "semanticScore" in item && typeof item.semanticScore === "number"
919
- ? item.semanticScore
920
- : 0,
921
- slotCoverage: "slotCoverage" in item && typeof item.slotCoverage === "number"
922
- ? item.slotCoverage
923
- : undefined,
924
- slotMatches: "slotMatches" in item && Array.isArray(item.slotMatches)
925
- ? item.slotMatches
926
- : undefined,
927
- lexicalCoverage: "lexicalCoverage" in item && typeof item.lexicalCoverage === "number"
928
- ? item.lexicalCoverage
929
- : ("slotCoverage" in item && typeof item.slotCoverage === "number" ? item.slotCoverage : 0),
930
- recencyScore: "recencyScore" in item && typeof item.recencyScore === "number"
931
- ? item.recencyScore
932
- : 0,
933
- finalScore: typeof item.finalScore === "number" ? item.finalScore : 0,
934
- rationale: typeof item.rationale === "string" ? item.rationale : "",
935
- comparisonSide: "comparisonSide" in item && (item.comparisonSide === 0 || item.comparisonSide === 1 || item.comparisonSide === null)
936
- ? item.comparisonSide
937
- : undefined,
938
- comparisonSlot: "comparisonSlot" in item && typeof item.comparisonSlot === "string"
939
- ? item.comparisonSlot
940
- : undefined,
941
- comparisonSlotRecall: "comparisonSlotRecall" in item && typeof item.comparisonSlotRecall === "number"
942
- ? item.comparisonSlotRecall
943
- : undefined,
944
- comparisonSlotPrecision: "comparisonSlotPrecision" in item && typeof item.comparisonSlotPrecision === "number"
945
- ? item.comparisonSlotPrecision
946
- : undefined,
947
- comparisonSlotSpecificity: "comparisonSlotSpecificity" in item && typeof item.comparisonSlotSpecificity === "number"
948
- ? item.comparisonSlotSpecificity
949
- : undefined,
950
- comparisonSlotPositionWeightedRecall: "comparisonSlotPositionWeightedRecall" in item && typeof item.comparisonSlotPositionWeightedRecall === "number"
951
- ? item.comparisonSlotPositionWeightedRecall
952
- : undefined,
953
- comparisonSlotPositionWeightedPrecision: "comparisonSlotPositionWeightedPrecision" in item && typeof item.comparisonSlotPositionWeightedPrecision === "number"
954
- ? item.comparisonSlotPositionWeightedPrecision
955
- : undefined,
956
- comparisonSlotPositionWeightedSpecificity: "comparisonSlotPositionWeightedSpecificity" in item && typeof item.comparisonSlotPositionWeightedSpecificity === "number"
957
- ? item.comparisonSlotPositionWeightedSpecificity
958
- : undefined,
959
- comparisonFirstPersonClauseCount: "comparisonFirstPersonClauseCount" in item && typeof item.comparisonFirstPersonClauseCount === "number"
960
- ? item.comparisonFirstPersonClauseCount
961
- : undefined,
962
- comparisonProspectivePersonalVerbCount: "comparisonProspectivePersonalVerbCount" in item && typeof item.comparisonProspectivePersonalVerbCount === "number"
963
- ? item.comparisonProspectivePersonalVerbCount
964
- : undefined,
965
- comparisonPlanningDensity: "comparisonPlanningDensity" in item && typeof item.comparisonPlanningDensity === "number"
966
- ? item.comparisonPlanningDensity
967
- : undefined,
968
- comparisonPastness: "comparisonPastness" in item && typeof item.comparisonPastness === "number"
969
- ? item.comparisonPastness
970
- : undefined,
971
- comparisonSideWitnessScore: "comparisonSideWitnessScore" in item && typeof item.comparisonSideWitnessScore === "number"
972
- ? item.comparisonSideWitnessScore
973
- : undefined,
974
- }));
975
- }
976
- }
977
- recoveryCandidates.push(
978
- ...reranked.ranked.map((item) => {
979
- return {
980
- ...item,
981
- finalScore: typeof item.finalScore === "number" ? item.finalScore : item.score,
982
- metadata: {
983
- ...item.metadata,
984
- recovery_fallback: true,
985
- recovery_scope: "user_turns",
986
- },
987
- };
988
- }),
989
- );
990
- }
991
-
992
- const dedupedRecovery = dedupeRecoveryCandidates(recoveryCandidates);
993
- const packingStart = temporalRecoveryResult?.comparisonProfile ? process.hrtime.bigint() : 0n;
994
- const fittedRecovery = comparisonExperiment.disableProtectedPairPack
995
- ? fitPromptBudgetFirstFit(dedupedRecovery, recoveryReserveTokens)
996
- : fitProtectedComparisonRecovery(
997
- dedupedRecovery,
998
- recoveryReserveTokens,
999
- temporalRecoveryResult?.comparisonCoverageApplied === true
1000
- ? temporalRecoveryResult.comparisonWitnessIds
1001
- : undefined,
1002
- );
1003
- if (temporalRecoveryResult?.comparisonProfile) {
1004
- temporalRecoveryResult.comparisonProfile.recoveryPackingMs += Number(process.hrtime.bigint() - packingStart) / 1_000_000;
1005
- }
1006
- recoveryItems = fittedRecovery;
1007
- if (debugRecovery) {
1008
- dedupedRecoveryDebug = dedupedRecovery.map((item) => ({
1009
- id: item.id,
1010
- recoveryScope: typeof item.metadata.recovery_scope === "string" ? item.metadata.recovery_scope : "unknown",
1011
- finalScore: typeof item.finalScore === "number" ? item.finalScore : item.score,
1012
- tokenEstimate: estimateTokens(item.text),
1013
- }));
1014
- fittedRecoveryDebug = fittedRecovery.map((item) => ({
1015
- id: item.id,
1016
- recoveryScope: typeof item.metadata.recovery_scope === "string" ? item.metadata.recovery_scope : "unknown",
1017
- finalScore: typeof item.finalScore === "number" ? item.finalScore : item.score,
1018
- tokenEstimate: estimateTokens(item.text),
1019
- }));
1020
- }
1021
- if (debugRecovery && rawUserRecoveryDebug.length > 0) {
1022
- rawUserRecoveryDebug = rawUserRecoveryDebug.map((item) => ({
1023
- ...item,
1024
- }));
1025
- }
1026
- }
1027
-
1028
- if (debugRecovery && rawUserRecoveryDebug.length === 0 && temporalRecoveryPreviewDebug.length > 0) {
1029
- rawUserRecoveryDebug = temporalRecoveryPreviewDebug;
1030
- }
1031
-
1032
- const selected = [
1033
- ...hardItems,
1034
- ...tailBaseItems,
1035
- ...softItems,
1036
- ...tailExtensionItems,
1037
- ...elevatedItems,
1038
- ...variantItems,
1039
- ...recoveryItems,
1040
- ];
1041
- void rpc.call("bump_access_counts", {
1042
- updates: groupAccessCountUpdates([...elevatedItems, ...variantItems]),
1043
- }).catch(() => {});
1044
-
1045
- profiler?.mark("render");
1046
- const selectedMessages = selected.map((item) => ({
1047
- role: "system",
1048
- content: buildInjectedMemoryMessageContent(item),
1049
- }));
1050
-
1051
- return {
1052
- messages: [...selectedMessages, ...visibleMessages],
1053
- estimatedTokens: countTokens(selectedMessages) + countTokens(visibleMessages),
1054
- systemPromptAddition: buildMemoryHeader(selected),
1055
- _debug: (debugRecovery || emitComparisonProfile)
1056
- ? {
1057
- recoveryTriggerFired: recoveryTrigger.fire,
1058
- crossSessionRawRecovery,
1059
- recoveryReserveTokens,
1060
- temporalQueryIndicator: temporalQuery.indicator,
1061
- temporalQueryActive: temporalQuery.active,
1062
- temporalQueryPatterns: temporalQuery.matchedPatterns,
1063
- temporalSelectorApplied: temporalSelectorGuard.shouldApply,
1064
- temporalSelectorReason: temporalSelectorGuard.reason,
1065
- temporalRecoverySlots: temporalRecoveryResult?.slots,
1066
- temporalComparisonCoverageApplied: temporalRecoveryResult?.comparisonCoverageApplied,
1067
- temporalComparisonCoverageSlots: temporalRecoveryResult?.comparisonCoverageSlots,
1068
- temporalComparisonCoverageMinTokens: temporalRecoveryResult?.comparisonCoverageMinTokens,
1069
- temporalComparisonWitnessIds: temporalRecoveryResult?.comparisonWitnessIds,
1070
- comparisonProfile: temporalRecoveryResult?.comparisonProfile,
1071
- recoveryDedupedOrder: dedupedRecoveryDebug,
1072
- recoveryFittedOrder: fittedRecoveryDebug,
1073
- rawUserRecoveryCandidates: rawUserRecoveryDebug,
1074
- }
1075
- : undefined,
1076
- };
1077
- },
1078
- async compact({ sessionId, force, targetSize }: ContextCompactArgs) {
1079
- const rpc = await getRpc();
1080
- const result = await rpc.call<{ compacted?: boolean; didCompact?: boolean }>("compact_session", {
1081
- sessionId,
1082
- force,
1083
- targetSize: targetSize ?? cfg.compactThreshold,
1084
- continuityMinTurns: cfg.continuityMinTurns ?? DEFAULT_CONTINUITY_MIN_TURNS,
1085
- continuityTailBudgetTokens: cfg.continuityTailBudgetTokens ?? DEFAULT_CONTINUITY_TAIL_BUDGET_TOKENS,
1086
- continuityPriorContextTokens: cfg.continuityPriorContextTokens ?? DEFAULT_CONTINUITY_PRIOR_CONTEXT_TOKENS,
1087
- }).catch(() => ({ compacted: false }));
1088
- const compacted = "didCompact" in result
1089
- ? (result.didCompact ?? result.compacted ?? false)
1090
- : (result.compacted ?? false);
1091
- if (compacted && useSessionRecallProjection(cfg)) {
1092
- await rebuildSessionRecallProjection(rpc, cfg, sessionId);
1093
- }
1094
-
1095
- return {
1096
- ok: true,
1097
- compacted,
1098
- };
1099
- },
1100
- };
1101
- }
1102
-
1103
- function useSessionRecallProjection(cfg: PluginConfig): boolean {
1104
- return cfg.useSessionRecallProjection === true;
1105
- }
1106
-
1107
- function useSessionSummarySearchExperiment(cfg: PluginConfig): boolean {
1108
- return cfg.useSessionSummarySearchExperiment === true;
1109
- }
1110
-
1111
- function sessionRecallCollection(sessionId: string): string {
1112
- return `${SESSION_RECALL_COLLECTION_PREFIX}${sessionId}`;
1113
- }
1114
-
1115
- function sessionRawCollection(sessionId: string): string {
1116
- return `${SESSION_RAW_COLLECTION_PREFIX}${sessionId}`;
1117
- }
1118
-
1119
- function sessionSummaryCollection(sessionId: string): string {
1120
- return `${SESSION_SUMMARY_COLLECTION_PREFIX}${sessionId}`;
1121
- }
1122
-
1123
- function sessionEdgeCollection(sessionId: string): string {
1124
- return `${SESSION_EDGE_COLLECTION_PREFIX}${sessionId}`;
1125
- }
1126
-
1127
- function sessionStateCollection(sessionId: string): string {
1128
- return `${SESSION_STATE_COLLECTION_PREFIX}${sessionId}`;
1129
- }
1130
-
1131
- function sessionRecallId(sourceId: string): string {
1132
- return `recall:${sourceId}`;
1133
- }
1134
-
1135
- async function rebuildSessionRecallProjection(
1136
- rpc: Awaited<ReturnType<RpcGetter>>,
1137
- cfg: PluginConfig,
1138
- sessionId: string,
1139
- ): Promise<void> {
1140
- const rawCollection = `session:${sessionId}`;
1141
- const projectionCollection = sessionRecallCollection(sessionId);
1142
- const sessionRecords = await rpc.call<{ results: SearchResult[] }>("list_by_meta", {
1143
- collection: rawCollection,
1144
- key: "sessionId",
1145
- value: sessionId,
1146
- });
1147
- const rawSessionTurns = sortChronological(
1148
- sessionRecords.results.filter((item) =>
1149
- // cascade_tier is ranking metadata (cascade search tier); exclude from session history
1150
- item.metadata.type !== "summary" &&
1151
- item.metadata.type !== "guidance_shard" &&
1152
- typeof item.metadata.cascade_tier !== "number"
1153
- ),
1154
- );
1155
- const recentTail = selectRecentTail(rawSessionTurns, {
1156
- minTurns: cfg.continuityMinTurns ?? DEFAULT_CONTINUITY_MIN_TURNS,
1157
- tailBudgetTokens: cfg.continuityTailBudgetTokens ?? DEFAULT_CONTINUITY_TAIL_BUDGET_TOKENS,
1158
- tokenCost,
1159
- sameBundle: isContinuityBundleCoupled,
1160
- });
1161
- const projectionItems = recentTail.older;
1162
- const existingProjection = await rpc.call<{ results: SearchResult[] }>("list_collection", {
1163
- collection: projectionCollection,
1164
- });
1165
- const existingIds = existingProjection.results
1166
- .map((item) => item.id)
1167
- .filter((id): id is string => typeof id === "string" && id.length > 0);
1168
- if (existingIds.length > 0) {
1169
- await rpc.call("delete_batch", {
1170
- collection: projectionCollection,
1171
- ids: existingIds,
1172
- });
1173
- }
1174
- await Promise.all(projectionItems.map((item) =>
1175
- rpc.call("insert_text", {
1176
- collection: projectionCollection,
1177
- id: sessionRecallId(item.id),
1178
- score: item.score,
1179
- text: item.text,
1180
- metadata: {
1181
- ...item.metadata,
1182
- projection_class: "session_recall",
1183
- source_turn_id: item.id,
1184
- source_turn_ts: metadataTimestamp(item),
1185
- },
1186
- })
1187
- ));
1188
- }
1189
-
1190
- async function loadAuthoredCollections(
1191
- rpc: Awaited<ReturnType<RpcGetter>>,
1192
- cached: { hard: SearchResult[] | null; soft: SearchResult[] | null; variant: SearchResult[] | null },
1193
- ): Promise<[SearchResult[], SearchResult[], SearchResult[]]> {
1194
- if (cached.hard && cached.soft && cached.variant) {
1195
- return [
1196
- sortAuthoredItems(cached.hard),
1197
- sortAuthoredItems(cached.soft),
1198
- sortAuthoredItems(cached.variant),
1199
- ];
1200
- }
1201
-
1202
- const [hard, soft, variant] = await Promise.all([
1203
- cached.hard
1204
- ? Promise.resolve({ results: cached.hard })
1205
- : rpc.call<{ results: SearchResult[] }>("list_collection", { collection: AUTHORED_HARD_COLLECTION }),
1206
- cached.soft
1207
- ? Promise.resolve({ results: cached.soft })
1208
- : rpc.call<{ results: SearchResult[] }>("list_collection", { collection: AUTHORED_SOFT_COLLECTION }),
1209
- cached.variant
1210
- ? Promise.resolve({ results: cached.variant })
1211
- : rpc.call<{ results: SearchResult[] }>("list_collection", { collection: AUTHORED_VARIANT_COLLECTION }),
1212
- ]);
1213
-
1214
- return [
1215
- sortAuthoredItems(hard.results),
1216
- sortAuthoredItems(soft.results),
1217
- sortAuthoredItems(variant.results),
1218
- ];
1219
- }
1220
-
1221
- function tokenCostSum(items: SearchResult[]): number {
1222
- return items.reduce((sum, item) => sum + tokenCost(item), 0);
1223
- }
1224
-
1225
- function tokenCost(item: SearchResult): number {
1226
- const estimate = item.metadata.token_estimate;
1227
- if (typeof estimate === "number" && estimate > 0) {
1228
- return estimate;
1229
- }
1230
- return estimateTokens(buildInjectedMemoryMessageContent(item));
1231
- }
1232
-
1233
- function sortChronological(items: SearchResult[]): SearchResult[] {
1234
- return [...items].sort((left, right) => {
1235
- const leftTS = metadataTimestamp(left);
1236
- const rightTS = metadataTimestamp(right);
1237
- if (leftTS === rightTS) {
1238
- return left.id.localeCompare(right.id);
1239
- }
1240
- return leftTS - rightTS;
1241
- });
1242
- }
1243
-
1244
- function metadataTimestamp(item: SearchResult): number {
1245
- const raw = item.metadata.ts;
1246
- return typeof raw === "number" && Number.isFinite(raw) ? raw : 0;
1247
- }
1248
-
1249
- function metadataNumber(item: SearchResult, key: string): number {
1250
- const raw = item.metadata[key];
1251
- return typeof raw === "number" && Number.isFinite(raw) ? raw : 0;
1252
- }
1253
-
1254
- function markRecentTail(items: SearchResult[], baseCount: number): SearchResult[] {
1255
- const baseStart = Math.max(0, items.length - baseCount);
1256
- return items.map((item, idx) => ({
1257
- ...item,
1258
- metadata: {
1259
- ...item.metadata,
1260
- continuity_tail: true,
1261
- continuity_base: idx >= baseStart,
1262
- },
1263
- }));
1264
- }
1265
-
1266
- function annotateCollection(items: SearchResult[], collection: string): SearchResult[] {
1267
- return items.map((item) => ({
1268
- ...item,
1269
- metadata: {
1270
- ...item.metadata,
1271
- collection,
1272
- },
1273
- }));
1274
- }
1275
-
1276
- function sortAuthoredItems(items: SearchResult[]): SearchResult[] {
1277
- return [...items].sort((left, right) => {
1278
- const leftDoc = typeof left.metadata.source_doc === "string" ? left.metadata.source_doc : "";
1279
- const rightDoc = typeof right.metadata.source_doc === "string" ? right.metadata.source_doc : "";
1280
- if (leftDoc !== rightDoc) {
1281
- return leftDoc.localeCompare(rightDoc);
1282
- }
1283
-
1284
- const leftPosition = metadataNumber(left, "position");
1285
- const rightPosition = metadataNumber(right, "position");
1286
- if (leftPosition !== rightPosition) {
1287
- return leftPosition - rightPosition;
1288
- }
1289
-
1290
- const leftOrdinal = metadataNumber(left, "ordinal");
1291
- const rightOrdinal = metadataNumber(right, "ordinal");
1292
- if (leftOrdinal !== rightOrdinal) {
1293
- return leftOrdinal - rightOrdinal;
1294
- }
1295
-
1296
- return left.id.localeCompare(right.id);
1297
- });
1298
- }
1299
-
1300
- function groupAccessCountUpdates(items: SearchResult[]): Array<{ collection: string; ids: string[] }> {
1301
- const grouped = new Map<string, string[]>();
1302
- for (const item of items) {
1303
- const collection = typeof item.metadata.collection === "string" ? item.metadata.collection : "";
1304
- if (collection === "") {
1305
- continue;
1306
- }
1307
- const ids = grouped.get(collection) ?? [];
1308
- ids.push(item.id);
1309
- grouped.set(collection, ids);
1310
- }
1311
- return [...grouped.entries()].map(([collection, ids]) => ({ collection, ids }));
1312
- }
1313
-
1314
- function dedupeRecoveryCandidates(items: SearchResult[]): SearchResult[] {
1315
- const byKey = new Map<string, SearchResult>();
1316
- for (const item of items) {
1317
- const collection = typeof item.metadata.collection === "string" ? item.metadata.collection : "";
1318
- const key = `${collection}::${item.id}`;
1319
- const existing = byKey.get(key);
1320
- if (!existing || (item.finalScore ?? item.score) > (existing.finalScore ?? existing.score)) {
1321
- byKey.set(key, item);
1322
- }
1323
- }
1324
- return [...byKey.values()].sort((left, right) => (right.finalScore ?? right.score) - (left.finalScore ?? left.score));
1325
- }
1326
-
1327
- function fitProtectedComparisonRecovery(
1328
- items: SearchResult[],
1329
- tokenBudget: number,
1330
- protectedWitnessIds?: string[],
1331
- ): SearchResult[] {
1332
- if (!protectedWitnessIds || protectedWitnessIds.length === 0) {
1333
- return fitPromptBudgetFirstFit(items, tokenBudget);
1334
- }
1335
-
1336
- const protectedById = new Map<string, SearchResult>();
1337
- const remaining: SearchResult[] = [];
1338
-
1339
- for (const item of items) {
1340
- if (protectedWitnessIds.includes(item.id) && !protectedById.has(item.id)) {
1341
- protectedById.set(item.id, item);
1342
- continue;
1343
- }
1344
- remaining.push(item);
1345
- }
1346
-
1347
- const protectedItems = protectedWitnessIds
1348
- .map((id) => protectedById.get(id))
1349
- .filter((item): item is SearchResult => Boolean(item));
1350
- if (protectedItems.length !== protectedWitnessIds.length) {
1351
- return fitPromptBudgetFirstFit(items, tokenBudget);
1352
- }
1353
-
1354
- const protectedTokens = protectedItems.reduce((sum, item) => sum + estimateTokens(item.text), 0);
1355
- if (protectedTokens > tokenBudget) {
1356
- return fitPromptBudgetFirstFit(items, tokenBudget);
1357
- }
1358
-
1359
- const tail = fitPromptBudgetFirstFit(remaining, tokenBudget - protectedTokens);
1360
- return [...protectedItems, ...tail];
1361
- }
1362
-
1363
- function clampFraction(value: number | undefined): number {
1364
- if (typeof value !== "number" || !Number.isFinite(value)) {
1365
- return 0;
1366
- }
1367
- return Math.min(1, Math.max(0, value));
1368
- }
1369
-
1370
- async function ingestCanonicalMessage(params: {
1371
- getRpc: RpcGetter;
1372
- cfg: PluginConfig;
1373
- logger: LoggerLike;
1374
- recallCache: RecallCache<SearchResult>;
1375
- clearElevatedCacheForSession: (sessionId: string) => void;
1376
- sessionId: string;
1377
- sessionKey?: string;
1378
- userId?: string;
1379
- message: MemoryMessage;
1380
- skipProjectionRebuild?: boolean;
1381
- }): Promise<{ ingested: boolean }> {
1382
- const normalized = normalizeMemoryMessage(params.message);
1383
- if (!normalized) {
1384
- return { ingested: false };
1385
- }
1386
-
1387
- const rpc = await params.getRpc();
1388
- const ts = Date.now();
1389
- const durableNamespace = resolveDurableNamespace({
1390
- userId: params.userId,
1391
- sessionKey: params.sessionKey,
1392
- fallback: `session:${params.sessionId}`,
1393
- });
1394
- const turnId = normalized.id ?? `${ts}`;
1395
- const sessionMeta = {
1396
- role: normalized.role,
1397
- ts,
1398
- userId: durableNamespace,
1399
- sessionId: params.sessionId,
1400
- type: "turn",
1401
- provenance_class: "session_turn",
1402
- stability_weight: stabilityWeightForMessage(normalized.role),
1403
- source_turn_id: turnId,
1404
- };
1405
-
1406
- params.clearElevatedCacheForSession(params.sessionId);
1407
- const rawSessionInsert = rpc.call("insert_session_turn", {
1408
- sessionId: params.sessionId,
1409
- id: `${params.sessionId}:${turnId}`,
1410
- text: normalized.content,
1411
- metadata: sessionMeta,
1412
- });
1413
- try {
1414
- await rawSessionInsert;
1415
- if (useSessionRecallProjection(params.cfg) && !params.skipProjectionRebuild) {
1416
- await rebuildSessionRecallProjection(rpc, params.cfg, params.sessionId);
1417
- }
1418
- } catch (error) {
1419
- params.logger.error(
1420
- `[libravdb] session ingest failed for ${params.sessionId}: ${error instanceof Error ? error.message : String(error)}`,
1421
- );
1422
- return { ingested: false };
1423
- }
1424
-
1425
- if (normalized.role !== "user") {
1426
- return { ingested: true };
1427
- }
1428
-
1429
- try {
1430
- params.recallCache.clearUser(durableNamespace);
1431
- await rpc.call("insert_text", {
1432
- collection: `turns:${durableNamespace}`,
1433
- id: `${durableNamespace}:${turnId}`,
1434
- text: normalized.content,
1435
- metadata: {
1436
- ...sessionMeta,
1437
- provenance_class: "turn_index",
1438
- },
1439
- });
1440
-
1441
- const gating = await rpc.call<GatingResult>("gating_scalar", {
1442
- userId: durableNamespace,
1443
- text: normalized.content,
1444
- });
1445
-
1446
- // Gating is designed for markdown file deduplication — not conversational content.
1447
- // User turns must be stored durably regardless of gating score so the agent can recall
1448
- // friends, preferences, and context across sessions. Gating signals are still
1449
- // recorded in metadata for observability, but do not gate the insert.
1450
- //
1451
- // IMPORTANT: This insert MUST be awaited. A previous fire-and-forget pattern
1452
- // (void + catch) caused silent data loss when the daemon was slow/overloaded —
1453
- // the user-level collection stayed empty, breaking cross-session recall entirely.
1454
- await rpc.call("insert_text", {
1455
- collection: `user:${durableNamespace}`,
1456
- id: `${durableNamespace}:${turnId}`,
1457
- text: normalized.content,
1458
- metadata: {
1459
- role: normalized.role,
1460
- ts,
1461
- sessionId: params.sessionId,
1462
- type: "turn",
1463
- userId: durableNamespace,
1464
- source_turn_id: turnId,
1465
- provenance_class: "durable_user_memory",
1466
- stability_weight: Math.max(stabilityWeightForMessage(normalized.role), gating.g),
1467
- gating_score: gating.g,
1468
- gating_t: gating.t,
1469
- gating_h: gating.h,
1470
- gating_r: gating.r,
1471
- gating_d: gating.d,
1472
- gating_p: gating.p,
1473
- gating_a: gating.a,
1474
- gating_dtech: gating.dtech,
1475
- gating_gconv: gating.gconv,
1476
- gating_gtech: gating.gtech,
1477
- },
1478
- });
1479
- } catch (userInsertError) {
1480
- // Session storage already happened; log durable promotion failure visibly
1481
- // so it does not silently vanish in production.
1482
- params.logger.error(
1483
- `[libravdb] durable user insert failed for ${durableNamespace}: ${userInsertError instanceof Error ? userInsertError.message : String(userInsertError)}`,
1484
- );
1485
- return { ingested: false };
1486
- }
1487
-
1488
- return { ingested: true };
1489
- }
1490
-
1491
- function hashMessageContent(content: string): string {
1492
- return createHash("sha256").update(content).digest("hex");
1493
- }
1494
-
1495
- function pruneAfterTurnIngestKeys(cache: Map<string, number>, now: number): void {
1496
- for (const [key, seenAt] of cache) {
1497
- if (now - seenAt > AFTER_TURN_DEDUPE_TTL_MS) {
1498
- cache.delete(key);
1499
- }
1500
- }
1501
- while (cache.size > AFTER_TURN_DEDUPE_MAX_ENTRIES) {
1502
- const oldestKey = cache.keys().next().value;
1503
- if (!oldestKey) {
1504
- break;
1505
- }
1506
- cache.delete(oldestKey);
1507
- }
1508
- }
1509
-
1510
- function hasRecentAfterTurnIngest(cache: Map<string, number>, key: string): boolean {
1511
- const now = Date.now();
1512
- pruneAfterTurnIngestKeys(cache, now);
1513
- return cache.has(key);
1514
- }
1515
-
1516
- function rememberAfterTurnIngest(cache: Map<string, number>, key: string): void {
1517
- const now = Date.now();
1518
- cache.delete(key);
1519
- cache.set(key, now);
1520
- pruneAfterTurnIngestKeys(cache, now);
1521
- }
1522
-
1523
- function normalizeHostMessage(message: { role: string; content: unknown } | undefined): MemoryMessage | null {
1524
- if (!message || !shouldIngestRole(message.role)) {
1525
- return null;
1526
- }
1527
- const content = extractMessageText(message.content);
1528
- if (!content) {
1529
- return null;
1530
- }
1531
- return {
1532
- role: message.role,
1533
- content,
1534
- };
1535
- }
1536
-
1537
- function normalizeConversationMessages(messages: Array<{ role: string; content: unknown }>): MemoryMessage[] {
1538
- return messages
1539
- .map((message) => normalizeHostMessage(message))
1540
- .filter((message): message is MemoryMessage => message !== null);
1541
- }
1542
-
1543
- function normalizeMemoryMessage(message: MemoryMessage): MemoryMessage | null {
1544
- if (!shouldIngestRole(message.role)) {
1545
- return null;
1546
- }
1547
- const content = extractMessageText(message.content);
1548
- if (!content) {
1549
- return null;
1550
- }
1551
- return {
1552
- ...message,
1553
- content,
1554
- };
1555
- }
1556
-
1557
- function shouldIngestRole(role: string): boolean {
1558
- return role === "user" || role === "assistant" || role === "system";
1559
- }
1560
-
1561
- function extractMessageText(content: unknown): string {
1562
- if (typeof content === "string") {
1563
- return content;
1564
- }
1565
- if (!Array.isArray(content)) {
1566
- return "";
1567
- }
1568
- return content
1569
- .flatMap((part) => {
1570
- if (!part || typeof part !== "object") {
1571
- return [];
1572
- }
1573
- const type = (part as { type?: unknown }).type;
1574
- if (
1575
- (type === "text" || type === "input_text" || type === "output_text") &&
1576
- typeof (part as { text?: unknown }).text === "string"
1577
- ) {
1578
- return [(part as { text: string }).text];
1579
- }
1580
- return [];
1581
- })
1582
- .join("\n")
1583
- .trim();
1584
- }
1585
-
1586
- function validateSection7StartupHardReserve(cfg: PluginConfig, authoredHard: SearchResult[]): void {
1587
- if (authoredHard.length === 0) {
1588
- return;
1589
- }
1590
- const hardFraction = clampFraction(cfg.authoredHardBudgetFraction);
1591
- if (hardFraction <= 0) {
1592
- return;
1593
- }
1594
- const startupTokenBudget = cfg.section7StartupTokenBudgetTokens;
1595
- if (typeof startupTokenBudget !== "number" || !Number.isFinite(startupTokenBudget) || startupTokenBudget <= 0) {
1596
- throw new Error(
1597
- "section7StartupTokenBudgetTokens is required to validate the authored hard reserve at bootstrap when authoredHardBudgetFraction is configured",
1598
- );
1599
- }
1600
- const memoryBudget = startupTokenBudget * (cfg.tokenBudgetFraction ?? 0.25);
1601
- const hardBudget = memoryBudget * hardFraction;
1602
- const hardUsed = tokenCostSum(authoredHard);
1603
- if (hardUsed > hardBudget + 1e-9) {
1604
- throw new Error(
1605
- `authored hard invariants require ${hardUsed} tokens but the configured startup reserve allows only ${hardBudget}`,
1606
- );
1607
- }
1608
- }
1609
-
1610
- function buildDegradedMemoryHeader(reasons: string[], selected: SearchResult[]): string {
1611
- const header = [
1612
- "<memory_degraded>",
1613
- "Memory assembly is in degraded mode.",
1614
- ...reasons.map((reason, idx) => `[D${idx + 1}] ${reason}.`),
1615
- "Hard invariants and the mandatory recent-tail base were preserved without silent truncation.",
1616
- "</memory_degraded>",
1617
- ].join("\n");
1618
- const body = buildMemoryHeader(selected);
1619
- return body === "" ? header : `${header}\n\n${body}`;
1620
- }
1621
-
1622
- function isContinuityBundleCoupled(left: SearchResult, right: SearchResult): boolean {
1623
- const leftBundle = typeof left.metadata.continuity_bundle_id === "string" ? left.metadata.continuity_bundle_id : "";
1624
- const rightBundle = typeof right.metadata.continuity_bundle_id === "string" ? right.metadata.continuity_bundle_id : "";
1625
- if (leftBundle !== "" && leftBundle === rightBundle) {
1626
- return true;
1627
- }
1628
- const leftRole = typeof left.metadata.role === "string" ? left.metadata.role : "";
1629
- const rightRole = typeof right.metadata.role === "string" ? right.metadata.role : "";
1630
- return (
1631
- (leftRole === "user" && rightRole === "assistant") ||
1632
- (leftRole === "assistant" && rightRole === "user")
1633
- );
1634
- }
1635
-
1636
- function stabilityWeightForMessage(role: string): number {
1637
- switch (role) {
1638
- case "user":
1639
- return 0.5;
1640
- case "assistant":
1641
- return 0.25;
1642
- default:
1643
- return 0.2;
1644
- }
1645
- }