@xdarkicex/openclaw-memory-libravdb 1.3.13 → 1.3.18

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.
@@ -5,6 +5,7 @@ import {
5
5
  selectRecentTail,
6
6
  } from "./continuity.js";
7
7
  import {
8
+ detectRetrievalFailure,
8
9
  expandSection7HopCandidates,
9
10
  mergeSection7VariantCandidates,
10
11
  rankSection7VariantCandidates,
@@ -14,6 +15,7 @@ import { countTokens, estimateTokens, fitPromptBudget } from "./tokens.js";
14
15
  import type { RpcGetter } from "./plugin-runtime.js";
15
16
  import type {
16
17
  ContextAssembleArgs,
18
+ ContextAssembleResult,
17
19
  ContextBootstrapArgs,
18
20
  ContextCompactArgs,
19
21
  ContextIngestArgs,
@@ -26,6 +28,13 @@ import type {
26
28
  const AUTHORED_HARD_COLLECTION = "authored:hard";
27
29
  const AUTHORED_SOFT_COLLECTION = "authored:soft";
28
30
  const AUTHORED_VARIANT_COLLECTION = "authored:variant";
31
+ const ELEVATED_USER_COLLECTION_PREFIX = "elevated:user:";
32
+ const ELEVATED_SESSION_COLLECTION_PREFIX = "elevated:session:";
33
+ const SESSION_RECALL_COLLECTION_PREFIX = "session_recall:";
34
+ const SESSION_RAW_COLLECTION_PREFIX = "session_raw:";
35
+ const SESSION_SUMMARY_COLLECTION_PREFIX = "session_summary:";
36
+ const SESSION_EDGE_COLLECTION_PREFIX = "session_edge:";
37
+ const SESSION_STATE_COLLECTION_PREFIX = "session_state:";
29
38
 
30
39
  export function buildContextEngineFactory(
31
40
  getRpc: RpcGetter,
@@ -35,6 +44,16 @@ export function buildContextEngineFactory(
35
44
  let authoredHardCache: SearchResult[] | null = null;
36
45
  let authoredSoftCache: SearchResult[] | null = null;
37
46
  let authoredVariantCache: SearchResult[] | null = null;
47
+ const authoredVariantRecallCache = new Map<string, SearchResult[]>();
48
+
49
+ // Session-scoped elevated-guidance cache keyed by sessionId + generation + userId + queryText
50
+ const elevatedRecallCache = new Map<string, SearchResult[]>();
51
+ const elevatedRecallGeneration = new Map<string, number>();
52
+
53
+ function clearElevatedCacheForSession(sessionId: string) {
54
+ const nextGeneration = (elevatedRecallGeneration.get(sessionId) ?? 0) + 1;
55
+ elevatedRecallGeneration.set(sessionId, nextGeneration);
56
+ }
38
57
 
39
58
  return {
40
59
  ownsCompaction: true,
@@ -43,6 +62,11 @@ export function buildContextEngineFactory(
43
62
  await rpc.call("ensure_collections", {
44
63
  collections: [
45
64
  `session:${sessionId}`,
65
+ sessionRawCollection(sessionId),
66
+ sessionSummaryCollection(sessionId),
67
+ sessionEdgeCollection(sessionId),
68
+ sessionStateCollection(sessionId),
69
+ ...(useSessionRecallProjection(cfg) ? [sessionRecallCollection(sessionId)] : []),
46
70
  `turns:${userId}`,
47
71
  `user:${userId}`,
48
72
  "global",
@@ -59,6 +83,10 @@ export function buildContextEngineFactory(
59
83
  authoredHardCache = authoredHard;
60
84
  authoredSoftCache = authoredSoft;
61
85
  authoredVariantCache = authoredVariantRecords;
86
+ authoredVariantRecallCache.clear();
87
+ if (useSessionRecallProjection(cfg)) {
88
+ await rebuildSessionRecallProjection(rpc, cfg, sessionId);
89
+ }
62
90
  validateSection7StartupHardReserve(cfg, authoredHard);
63
91
  return { ok: true };
64
92
  },
@@ -69,12 +97,34 @@ export function buildContextEngineFactory(
69
97
 
70
98
  const rpc = await getRpc();
71
99
  const ts = Date.now();
72
- void rpc.call("insert_text", {
73
- collection: `session:${sessionId}`,
74
- id: `${sessionId}:${ts}`,
100
+ const sessionMeta = {
101
+ role: message.role,
102
+ ts,
103
+ userId,
104
+ sessionId,
105
+ type: "turn",
106
+ provenance_class: "session_turn",
107
+ stability_weight: stabilityWeightForMessage(message.role),
108
+ };
109
+ // Elevated cache is session-scoped, so invalidate immediately on every ingest
110
+ clearElevatedCacheForSession(sessionId);
111
+ const rawSessionId = `${sessionId}:${ts}`;
112
+ const rawSessionInsert = rpc.call("insert_session_turn", {
113
+ sessionId,
114
+ id: rawSessionId,
75
115
  text: message.content,
76
- metadata: { role: message.role, ts, userId, sessionId, type: "turn" },
77
- }).catch(console.error);
116
+ metadata: sessionMeta,
117
+ });
118
+ if (useSessionRecallProjection(cfg)) {
119
+ try {
120
+ await rawSessionInsert;
121
+ await rebuildSessionRecallProjection(rpc, cfg, sessionId);
122
+ } catch (error) {
123
+ console.error(error);
124
+ }
125
+ } else {
126
+ void rawSessionInsert.catch(console.error);
127
+ }
78
128
 
79
129
  if (message.role === "user") {
80
130
  try {
@@ -83,7 +133,10 @@ export function buildContextEngineFactory(
83
133
  collection: `turns:${userId}`,
84
134
  id: `${userId}:${ts}`,
85
135
  text: message.content,
86
- metadata: { role: message.role, ts, userId, sessionId, type: "turn" },
136
+ metadata: {
137
+ ...sessionMeta,
138
+ provenance_class: "turn_index",
139
+ },
87
140
  });
88
141
 
89
142
  const gating = await rpc.call<GatingResult>("gating_scalar", {
@@ -102,6 +155,8 @@ export function buildContextEngineFactory(
102
155
  sessionId,
103
156
  type: "turn",
104
157
  userId,
158
+ provenance_class: "durable_user_memory",
159
+ stability_weight: Math.max(stabilityWeightForMessage(message.role), gating.g),
105
160
  gating_score: gating.g,
106
161
  gating_t: gating.t,
107
162
  gating_h: gating.h,
@@ -123,181 +178,427 @@ export function buildContextEngineFactory(
123
178
  return { ingested: true };
124
179
  },
125
180
  async assemble({ sessionId, userId, messages, tokenBudget }: ContextAssembleArgs) {
181
+ const PROFILE = process.env.OPENCLAW_PROFILE_ASSEMBLE === "1";
182
+
126
183
  const queryText = messages.at(-1)?.content ?? "";
127
184
  if (!queryText) {
128
185
  return {
129
186
  messages,
130
187
  estimatedTokens: countTokens(messages),
131
188
  systemPromptAddition: "",
132
- };
189
+ } satisfies ContextAssembleResult;
133
190
  }
134
191
 
135
192
  const excluded = recentIds(messages, 4);
136
- const cached = recallCache.get({ userId, queryText });
193
+ const cached = recallCache.take({ userId, queryText });
137
194
 
138
- try {
139
- const rpc = await getRpc();
140
- const [authoredHard, authoredSoft, authoredVariantRecords] = await loadAuthoredCollections(rpc, {
195
+ const rpc = await getRpc();
196
+
197
+ // Use cached authored collections directly if available (bootstrap-loaded and sorted)
198
+ // Only load as fallback if caches are unexpectedly null
199
+ let authoredHard = authoredHardCache;
200
+ let authoredSoft = authoredSoftCache;
201
+ let authoredVariantRecords = authoredVariantCache;
202
+ if (!authoredHard || !authoredSoft || !authoredVariantRecords) {
203
+ const [loadedHard, loadedSoft, loadedVariant] = await loadAuthoredCollections(rpc, {
141
204
  hard: authoredHardCache,
142
205
  soft: authoredSoftCache,
143
206
  variant: authoredVariantCache,
144
207
  });
145
- authoredHardCache = authoredHard;
146
- authoredSoftCache = authoredSoft;
147
- authoredVariantCache = authoredVariantRecords;
148
-
149
- const memoryBudget = tokenBudget * (cfg.tokenBudgetFraction ?? 0.25);
150
- const hardItems = authoredHard;
151
- const hardUsed = tokenCostSum(hardItems);
152
- const sessionRecords = await rpc.call<{ results: SearchResult[] }>("list_by_meta", {
153
- collection: `session:${sessionId}`,
154
- key: "sessionId",
155
- value: sessionId,
156
- });
157
- const rawSessionTurns = sortChronological(
158
- sessionRecords.results.filter((item) => item.metadata.type !== "summary"),
159
- );
160
- const minTurns = cfg.continuityMinTurns ?? DEFAULT_CONTINUITY_MIN_TURNS;
161
- const tailTarget = cfg.continuityTailBudgetTokens ?? DEFAULT_CONTINUITY_TAIL_BUDGET_TOKENS;
162
- const baseTail = selectRecentTail(rawSessionTurns, {
163
- minTurns,
164
- tailBudgetTokens: 0,
165
- tokenCost,
166
- sameBundle: isContinuityBundleCoupled,
167
- });
168
- const baseTailUsed = baseTail.baseTokens;
169
- const configuredHardFraction = clampFraction(cfg.authoredHardBudgetFraction);
170
- const hardBudget = configuredHardFraction > 0 ? memoryBudget * configuredHardFraction : hardUsed;
171
- const degradedReasons: string[] = [];
172
- if (hardUsed > hardBudget + 1e-9) {
173
- degradedReasons.push("hard authored invariants exceed configured hard budget reserve");
174
- }
175
- if (hardUsed + baseTailUsed > memoryBudget + 1e-9) {
176
- degradedReasons.push("hard authored invariants plus mandatory recent-tail base exceed available memory budget");
177
- }
178
- if (degradedReasons.length > 0) {
179
- const degradedTail = markRecentTail(baseTail.base, baseTail.base.length);
180
- const selected = [...hardItems, ...degradedTail];
181
- const selectedMessages = selected.map((item) => ({
182
- role: "system",
183
- content: buildInjectedMemoryMessageContent(item),
184
- }));
185
- return {
186
- messages: [...selectedMessages, ...messages],
187
- estimatedTokens: countTokens(selectedMessages) + countTokens(messages),
188
- systemPromptAddition: buildDegradedMemoryHeader(degradedReasons, selected),
189
- };
190
- }
191
- const authoredSoftTarget = Math.max(0, memoryBudget * (cfg.authoredSoftBudgetFraction ?? 0.3));
192
- const softBudget = Math.max(0, Math.min(authoredSoftTarget, memoryBudget - hardUsed - baseTailUsed));
193
- const softItems = fitPromptBudget(authoredSoft, softBudget);
194
- const remainingAfterHardSoft = Math.max(0, memoryBudget - hardUsed - tokenCostSum(softItems));
195
- const effectiveTailBudget = Math.min(
196
- Math.max(tailTarget, baseTailUsed),
197
- remainingAfterHardSoft,
198
- );
199
- const recentTailSelection = selectRecentTail(rawSessionTurns, {
200
- minTurns,
201
- tailBudgetTokens: effectiveTailBudget,
202
- tokenCost,
203
- sameBundle: isContinuityBundleCoupled,
204
- });
205
- const recentTail = markRecentTail(
206
- recentTailSelection.recent,
207
- recentTailSelection.base.length,
208
- );
209
- const tailBaseItems = recentTail.slice(-recentTailSelection.base.length);
210
- const tailExtensionItems = recentTail.slice(0, Math.max(0, recentTail.length - recentTailSelection.base.length));
211
- const retrievalBudget = Math.max(0, memoryBudget - hardUsed - tokenCostSum(softItems) - tokenCostSum(recentTail));
212
- const recentTailIDs = recentTail.map((item) => item.id);
213
-
214
- const coarseTopK = Math.max(cfg.section7CoarseTopK ?? Math.max((cfg.topK ?? 8) * 2, 8), 1);
215
- const secondPassTopK = Math.max(cfg.section7SecondPassTopK ?? (cfg.topK ?? 8), 1);
216
- const [sessionHits, durableHits] = await Promise.all([
217
- rpc.call<{ results: SearchResult[] }>("search_text", {
218
- collection: `session:${sessionId}`,
219
- text: queryText,
220
- k: coarseTopK,
221
- excludeIds: [...excluded, ...recentTailIDs],
222
- }),
223
- cached
224
- ? Promise.resolve({ results: cached.durableVariantHits })
225
- : rpc.call<{ results: SearchResult[] }>("search_text_collections", {
226
- collections: [`user:${userId}`, "global", AUTHORED_VARIANT_COLLECTION],
227
- text: queryText,
228
- k: coarseTopK,
229
- excludeByCollection: {},
230
- }),
231
- ]);
232
-
233
- if (!cached) {
234
- recallCache.put({
235
- userId,
236
- queryText,
237
- durableVariantHits: durableHits.results,
238
- });
239
- }
208
+ authoredHard = loadedHard;
209
+ authoredSoft = loadedSoft;
210
+ authoredVariantRecords = loadedVariant;
211
+ authoredHardCache = loadedHard;
212
+ authoredSoftCache = loadedSoft;
213
+ authoredVariantCache = loadedVariant;
214
+ }
240
215
 
241
- const ranked = rankSection7VariantCandidates(
242
- [
243
- ...annotateCollection(sessionHits.results, `session:${sessionId}`),
244
- ...durableHits.results,
245
- ],
246
- {
247
- queryText,
248
- k1: coarseTopK,
249
- k2: secondPassTopK,
250
- theta1: cfg.section7Theta1,
251
- kappa: cfg.section7Kappa,
252
- authorityRecencyLambda: cfg.section7AuthorityRecencyLambda,
253
- authorityRecencyWeight: cfg.section7AuthorityRecencyWeight,
254
- authorityFrequencyWeight: cfg.section7AuthorityFrequencyWeight,
255
- authorityAuthoredWeight: cfg.section7AuthorityAuthoredWeight,
256
- sessionId,
257
- userId,
258
- },
259
- );
260
- const hopExpanded = expandSection7HopCandidates(
261
- ranked,
262
- annotateCollection(authoredVariantRecords, AUTHORED_VARIANT_COLLECTION),
263
- {
264
- etaHop: cfg.section7HopEta,
265
- thetaHop: cfg.section7HopThreshold,
266
- },
267
- );
216
+ // Profiler: null when disabled (zero overhead), object when enabled
217
+ const profiler = PROFILE
218
+ ? (() => {
219
+ const marks: Array<[string, bigint]> = [];
220
+ return {
221
+ mark(label: string) {
222
+ marks.push([label, process.hrtime.bigint()]);
223
+ },
224
+ lines() {
225
+ const lines: string[] = [];
226
+ for (let i = 0; i < marks.length - 1; i++) {
227
+ const [name, start] = marks[i];
228
+ const [, end] = marks[i + 1];
229
+ const ms = Number(end - start) / 1_000_000;
230
+ lines.push(`assemble profile: ${name}=${ms.toFixed(2)}ms`);
231
+ }
232
+ return lines;
233
+ },
234
+ emit() {
235
+ for (const line of this.lines()) {
236
+ console.log(line);
237
+ }
238
+ },
239
+ };
240
+ })()
241
+ : null;
268
242
 
269
- const variantItems = fitPromptBudget(
270
- mergeSection7VariantCandidates(ranked, hopExpanded),
271
- retrievalBudget,
272
- );
273
- const selected = [
274
- ...hardItems,
275
- ...tailBaseItems,
276
- ...softItems,
277
- ...tailExtensionItems,
278
- ...variantItems,
279
- ];
280
- void rpc.call("bump_access_counts", {
281
- updates: groupAccessCountUpdates(variantItems),
282
- }).catch(() => {});
243
+ try {
244
+ const result = await this.assembleCore({
245
+ rpc,
246
+ cfg,
247
+ recallCache,
248
+ authoredHard,
249
+ authoredSoft,
250
+ authoredVariantRecords,
251
+ cached,
252
+ excluded,
253
+ queryText,
254
+ sessionId,
255
+ userId,
256
+ messages,
257
+ tokenBudget,
258
+ profiler,
259
+ });
260
+
261
+ const profileLines = profiler?.lines() ?? [];
262
+ if (profiler) {
263
+ profiler.emit();
264
+ }
283
265
 
266
+ return profileLines.length > 0
267
+ ? { ...result, _profile: profileLines }
268
+ : result;
269
+ } catch {
270
+ return {
271
+ messages,
272
+ estimatedTokens: countTokens(messages),
273
+ systemPromptAddition: "",
274
+ } satisfies ContextAssembleResult;
275
+ }
276
+ },
277
+ async assembleCore({
278
+ rpc,
279
+ cfg,
280
+ recallCache,
281
+ authoredHard,
282
+ authoredSoft,
283
+ authoredVariantRecords,
284
+ cached,
285
+ excluded,
286
+ queryText,
287
+ sessionId,
288
+ userId,
289
+ messages,
290
+ tokenBudget,
291
+ profiler,
292
+ }: {
293
+ rpc: Awaited<ReturnType<RpcGetter>>;
294
+ cfg: PluginConfig;
295
+ recallCache: RecallCache<SearchResult>;
296
+ authoredHard: SearchResult[];
297
+ authoredSoft: SearchResult[];
298
+ authoredVariantRecords: SearchResult[];
299
+ cached: ReturnType<RecallCache<SearchResult>["take"]>;
300
+ excluded: string[];
301
+ queryText: string;
302
+ sessionId: string;
303
+ userId: string;
304
+ messages: Array<{ role: string; content: string }>;
305
+ tokenBudget: number;
306
+ profiler: { mark(label: string): void; emit(): void } | null;
307
+ }): Promise<ContextAssembleResult> {
308
+ const memoryBudget = tokenBudget * (cfg.tokenBudgetFraction ?? 0.25);
309
+ const hardItems = authoredHard;
310
+ const hardUsed = tokenCostSum(hardItems);
311
+
312
+ profiler?.mark("session");
313
+ const sessionRecords = await rpc.call<{ results: SearchResult[] }>("list_by_meta", {
314
+ collection: `session:${sessionId}`,
315
+ key: "sessionId",
316
+ value: sessionId,
317
+ });
318
+ const rawSessionTurns = sortChronological(
319
+ sessionRecords.results.filter((item) =>
320
+ // cascade_tier is ranking metadata (cascade search tier); exclude from session history
321
+ item.metadata.type !== "summary" &&
322
+ item.metadata.type !== "guidance_shard" &&
323
+ typeof item.metadata.cascade_tier !== "number"
324
+ ),
325
+ );
326
+ const minTurns = cfg.continuityMinTurns ?? DEFAULT_CONTINUITY_MIN_TURNS;
327
+ const tailTarget = cfg.continuityTailBudgetTokens ?? DEFAULT_CONTINUITY_TAIL_BUDGET_TOKENS;
328
+ const baseTail = selectRecentTail(rawSessionTurns, {
329
+ minTurns,
330
+ tailBudgetTokens: 0,
331
+ tokenCost,
332
+ sameBundle: isContinuityBundleCoupled,
333
+ });
334
+ const baseTailUsed = baseTail.baseTokens;
335
+ const configuredHardFraction = clampFraction(cfg.authoredHardBudgetFraction);
336
+ const hardBudget = configuredHardFraction > 0 ? memoryBudget * configuredHardFraction : hardUsed;
337
+ const degradedReasons: string[] = [];
338
+ if (hardUsed > hardBudget + 1e-9) {
339
+ degradedReasons.push("hard authored invariants exceed configured hard budget reserve");
340
+ }
341
+ if (hardUsed + baseTailUsed > memoryBudget + 1e-9) {
342
+ degradedReasons.push("hard authored invariants plus mandatory recent-tail base exceed available memory budget");
343
+ }
344
+ if (degradedReasons.length > 0) {
345
+ const degradedTail = markRecentTail(baseTail.base, baseTail.base.length);
346
+ const selected = [...hardItems, ...degradedTail];
284
347
  const selectedMessages = selected.map((item) => ({
285
348
  role: "system",
286
349
  content: buildInjectedMemoryMessageContent(item),
287
350
  }));
288
-
289
351
  return {
290
352
  messages: [...selectedMessages, ...messages],
291
353
  estimatedTokens: countTokens(selectedMessages) + countTokens(messages),
292
- systemPromptAddition: buildMemoryHeader(selected),
293
- };
294
- } catch {
295
- return {
296
- messages,
297
- estimatedTokens: countTokens(messages),
298
- systemPromptAddition: "",
354
+ systemPromptAddition: buildDegradedMemoryHeader(degradedReasons, selected),
299
355
  };
300
356
  }
357
+ const authoredSoftTarget = Math.max(0, memoryBudget * (cfg.authoredSoftBudgetFraction ?? 0.3));
358
+ const softBudget = Math.max(0, Math.min(authoredSoftTarget, memoryBudget - hardUsed - baseTailUsed));
359
+ const softItems = fitPromptBudget(authoredSoft, softBudget);
360
+ const remainingAfterHardSoft = Math.max(0, memoryBudget - hardUsed - tokenCostSum(softItems));
361
+ const effectiveTailBudget = Math.min(
362
+ Math.max(tailTarget, baseTailUsed),
363
+ remainingAfterHardSoft,
364
+ );
365
+ const recentTailSelection = selectRecentTail(rawSessionTurns, {
366
+ minTurns,
367
+ tailBudgetTokens: effectiveTailBudget,
368
+ tokenCost,
369
+ sameBundle: isContinuityBundleCoupled,
370
+ });
371
+ const recentTail = markRecentTail(
372
+ recentTailSelection.recent,
373
+ recentTailSelection.base.length,
374
+ );
375
+ const tailBaseItems = recentTail.slice(-recentTailSelection.base.length);
376
+ const tailExtensionItems = recentTail.slice(0, Math.max(0, recentTail.length - recentTailSelection.base.length));
377
+ const retrievalBudget = Math.max(0, memoryBudget - hardUsed - tokenCostSum(softItems) - tokenCostSum(recentTail));
378
+ const recentTailIDs = recentTail.map((item) => item.id);
379
+
380
+ const coarseTopK = Math.max(cfg.section7CoarseTopK ?? Math.max((cfg.topK ?? 8) * 2, 8), 1);
381
+ const sessionSearchTopK = Math.max(cfg.topK ?? 8, 1);
382
+ const secondPassTopK = Math.max(cfg.section7SecondPassTopK ?? (cfg.topK ?? 8), 1);
383
+ const searchSessionRecall = useSessionRecallProjection(cfg);
384
+ const searchSessionSummary = useSessionSummarySearchExperiment(cfg);
385
+ let sessionSearchCollection = `session:${sessionId}`;
386
+ let sessionExcludeIds = [...excluded, ...recentTailIDs];
387
+ if (searchSessionSummary) {
388
+ const summaryCollection = sessionSummaryCollection(sessionId);
389
+ const summaryRecords = await rpc.call<{ results: SearchResult[] }>("list_collection", {
390
+ collection: summaryCollection,
391
+ });
392
+ if (summaryRecords.results.length > 0) {
393
+ sessionSearchCollection = summaryCollection;
394
+ sessionExcludeIds = [...excluded];
395
+ }
396
+ } else if (searchSessionRecall) {
397
+ sessionSearchCollection = sessionRecallCollection(sessionId);
398
+ sessionExcludeIds = [...excluded, ...recentTailIDs.map(sessionRecallId)];
399
+ }
400
+
401
+ profiler?.mark("session_search");
402
+ const [sessionHits] = await Promise.all([
403
+ rpc.call<{ results: SearchResult[] }>("search_text", {
404
+ collection: sessionSearchCollection,
405
+ text: queryText,
406
+ k: sessionSearchTopK,
407
+ excludeIds: sessionExcludeIds,
408
+ }),
409
+ ]);
410
+
411
+ profiler?.mark("recall_user_global");
412
+ const [userHits, globalHits] = await Promise.all([
413
+ cached?.userHits
414
+ ? Promise.resolve({ results: cached.userHits })
415
+ : rpc.call<{ results: SearchResult[] }>("search_text", {
416
+ collection: `user:${userId}`,
417
+ text: queryText,
418
+ k: Math.ceil((cfg.topK ?? 8) / 2),
419
+ }),
420
+ cached?.globalHits
421
+ ? Promise.resolve({ results: cached.globalHits })
422
+ : rpc.call<{ results: SearchResult[] }>("search_text", {
423
+ collection: "global",
424
+ text: queryText,
425
+ k: Math.ceil((cfg.topK ?? 8) / 4),
426
+ }),
427
+ ]);
428
+
429
+ if (!cached) {
430
+ recallCache.put({
431
+ userId,
432
+ queryText,
433
+ durableVariantHits: [],
434
+ userHits: userHits.results,
435
+ globalHits: globalHits.results,
436
+ });
437
+ }
438
+
439
+ profiler?.mark("recall_authored_variant");
440
+ const authoredVariantKey = `${queryText}\n${coarseTopK}`;
441
+ const cachedAuthoredVariantHits = authoredVariantRecallCache.get(authoredVariantKey);
442
+ const [authoredVariantHits] = await Promise.all([
443
+ cachedAuthoredVariantHits
444
+ ? Promise.resolve({ results: cachedAuthoredVariantHits })
445
+ : rpc.call<{ results: SearchResult[] }>("search_text", {
446
+ collection: AUTHORED_VARIANT_COLLECTION,
447
+ text: queryText,
448
+ k: coarseTopK,
449
+ }),
450
+ ]);
451
+ if (!cachedAuthoredVariantHits) {
452
+ authoredVariantRecallCache.set(authoredVariantKey, authoredVariantHits.results);
453
+ }
454
+
455
+ profiler?.mark("recall_elevated");
456
+ const elevatedGeneration = elevatedRecallGeneration.get(sessionId) ?? 0;
457
+ const elevatedKey = `${sessionId}\n${elevatedGeneration}\n${userId}\n${queryText}`;
458
+ const cachedElevated = elevatedRecallCache.get(elevatedKey);
459
+ const [elevatedHits] = await Promise.all([
460
+ cachedElevated
461
+ ? Promise.resolve({ results: cachedElevated })
462
+ : rpc.call<{ results: SearchResult[] }>("search_text_collections", {
463
+ collections: [
464
+ `${ELEVATED_USER_COLLECTION_PREFIX}${userId}`,
465
+ `${ELEVATED_SESSION_COLLECTION_PREFIX}${sessionId}`,
466
+ ],
467
+ text: queryText,
468
+ k: coarseTopK,
469
+ excludeByCollection: {},
470
+ }),
471
+ ]);
472
+ if (!cachedElevated) {
473
+ elevatedRecallCache.set(elevatedKey, elevatedHits.results);
474
+ }
475
+
476
+ profiler?.mark("rank");
477
+ const ranked = rankSection7VariantCandidates(
478
+ [
479
+ ...annotateCollection(sessionHits.results, `session:${sessionId}`),
480
+ ...elevatedHits.results,
481
+ ...userHits.results,
482
+ ...globalHits.results,
483
+ ...authoredVariantHits.results,
484
+ ],
485
+ {
486
+ queryText,
487
+ k1: coarseTopK,
488
+ k2: secondPassTopK,
489
+ theta1: cfg.section7Theta1,
490
+ kappa: cfg.section7Kappa,
491
+ authorityRecencyLambda: cfg.section7AuthorityRecencyLambda,
492
+ authorityRecencyWeight: cfg.section7AuthorityRecencyWeight,
493
+ authorityFrequencyWeight: cfg.section7AuthorityFrequencyWeight,
494
+ authorityAuthoredWeight: cfg.section7AuthorityAuthoredWeight,
495
+ sessionId,
496
+ userId,
497
+ },
498
+ );
499
+
500
+ profiler?.mark("hop");
501
+ const hopExpanded = expandSection7HopCandidates(
502
+ ranked,
503
+ annotateCollection(authoredVariantRecords, AUTHORED_VARIANT_COLLECTION),
504
+ {
505
+ etaHop: cfg.section7HopEta,
506
+ thetaHop: cfg.section7HopThreshold,
507
+ },
508
+ );
509
+
510
+ profiler?.mark("fit");
511
+ const mergedCandidates = mergeSection7VariantCandidates(ranked, hopExpanded);
512
+ // Recovery trigger is evaluated before variant fitting so healthy sessions
513
+ // do not lose recall budget to an unused recovery reserve.
514
+ profiler?.mark("recovery_trigger");
515
+ const recoveryTrigger = detectRetrievalFailure(mergedCandidates, {
516
+ floorScore: cfg.recoveryFloorScore ?? 0.15,
517
+ minTopK: cfg.recoveryMinTopK ?? 4,
518
+ meanConfidenceThresh: cfg.recoveryMinConfidenceMean ?? 0.5,
519
+ });
520
+ const recoveryReserveTokens = recoveryTrigger.fire
521
+ ? Math.min(memoryBudget, Math.max(Math.floor(memoryBudget * 0.10), 16), 128)
522
+ : 0;
523
+ const elevatedGuidanceBudget = Math.max(
524
+ 0,
525
+ Math.min(
526
+ memoryBudget * (cfg.elevatedGuidanceBudgetFraction ?? 0.15),
527
+ retrievalBudget,
528
+ ),
529
+ );
530
+ const elevatedItems = fitPromptBudget(
531
+ mergedCandidates.filter((item) => item.metadata.elevated_guidance === true),
532
+ elevatedGuidanceBudget,
533
+ );
534
+ const remainingAfterElevated = Math.max(0, retrievalBudget - tokenCostSum(elevatedItems));
535
+ const remainingForVariant = Math.max(0, remainingAfterElevated - recoveryReserveTokens);
536
+ const variantItems = fitPromptBudget(
537
+ mergedCandidates.filter((item) => item.metadata.elevated_guidance !== true),
538
+ remainingForVariant,
539
+ );
540
+
541
+ // Build set of theorem-selected IDs for recovery deduplication.
542
+ // Recovery should only append NEW raw evidence, not re-inject content already
543
+ // selected by the normal assembly path (hard/soft/tail/elevated/variant).
544
+ const theoremSelectedIDs = new Set([
545
+ ...hardItems.map((i) => i.id),
546
+ ...softItems.map((i) => i.id),
547
+ ...tailBaseItems.map((i) => i.id),
548
+ ...tailExtensionItems.map((i) => i.id),
549
+ ...elevatedItems.map((i) => i.id),
550
+ ...variantItems.map((i) => i.id),
551
+ ]);
552
+
553
+ // Recovery is a policy overlay — it appends raw content only when triggered,
554
+ // it never modifies the C_total(q) output and does not spend from tau_V.
555
+ let recoveryItems: SearchResult[] = [];
556
+ if (recoveryTrigger.fire) {
557
+ profiler?.mark("recovery_expand");
558
+ // Recovery searches immutable raw history directly — never the active view, elevated shards,
559
+ // or authored collections. Raw turns are immutable (storage axiom, unchanged).
560
+ const recoveryExcludeIDs = [...excluded, ...recentTailIDs, ...theoremSelectedIDs];
561
+ const rawResults = await rpc.call<{ results: SearchResult[] }>("query_raw_session", {
562
+ sessionId,
563
+ text: queryText,
564
+ k: Math.max(cfg.topK ?? 8, 4),
565
+ excludeIds: recoveryExcludeIDs,
566
+ });
567
+ // Fit recovered raw items to the reserved recovery budget — never exceed it.
568
+ const fittedRecovery = fitPromptBudget(rawResults.results ?? [], recoveryReserveTokens);
569
+ recoveryItems = fittedRecovery.map((item: SearchResult) => ({
570
+ ...item,
571
+ metadata: {
572
+ ...item.metadata,
573
+ recovery_fallback: true,
574
+ },
575
+ }));
576
+ }
577
+
578
+ const selected = [
579
+ ...hardItems,
580
+ ...tailBaseItems,
581
+ ...softItems,
582
+ ...tailExtensionItems,
583
+ ...elevatedItems,
584
+ ...variantItems,
585
+ ...recoveryItems,
586
+ ];
587
+ void rpc.call("bump_access_counts", {
588
+ updates: groupAccessCountUpdates([...elevatedItems, ...variantItems]),
589
+ }).catch(() => {});
590
+
591
+ profiler?.mark("render");
592
+ const selectedMessages = selected.map((item) => ({
593
+ role: "system",
594
+ content: buildInjectedMemoryMessageContent(item),
595
+ }));
596
+
597
+ return {
598
+ messages: [...selectedMessages, ...messages],
599
+ estimatedTokens: countTokens(selectedMessages) + countTokens(messages),
600
+ systemPromptAddition: buildMemoryHeader(selected),
601
+ };
301
602
  },
302
603
  async compact({ sessionId, force, targetSize }: ContextCompactArgs) {
303
604
  const rpc = await getRpc();
@@ -312,6 +613,9 @@ export function buildContextEngineFactory(
312
613
  const compacted = "didCompact" in result
313
614
  ? (result.didCompact ?? result.compacted ?? false)
314
615
  : (result.compacted ?? false);
616
+ if (compacted && useSessionRecallProjection(cfg)) {
617
+ await rebuildSessionRecallProjection(rpc, cfg, sessionId);
618
+ }
315
619
 
316
620
  return {
317
621
  ok: true,
@@ -321,12 +625,103 @@ export function buildContextEngineFactory(
321
625
  };
322
626
  }
323
627
 
628
+ function useSessionRecallProjection(cfg: PluginConfig): boolean {
629
+ return cfg.useSessionRecallProjection === true;
630
+ }
631
+
632
+ function useSessionSummarySearchExperiment(cfg: PluginConfig): boolean {
633
+ return cfg.useSessionSummarySearchExperiment === true;
634
+ }
635
+
636
+ function sessionRecallCollection(sessionId: string): string {
637
+ return `${SESSION_RECALL_COLLECTION_PREFIX}${sessionId}`;
638
+ }
639
+
640
+ function sessionRawCollection(sessionId: string): string {
641
+ return `${SESSION_RAW_COLLECTION_PREFIX}${sessionId}`;
642
+ }
643
+
644
+ function sessionSummaryCollection(sessionId: string): string {
645
+ return `${SESSION_SUMMARY_COLLECTION_PREFIX}${sessionId}`;
646
+ }
647
+
648
+ function sessionEdgeCollection(sessionId: string): string {
649
+ return `${SESSION_EDGE_COLLECTION_PREFIX}${sessionId}`;
650
+ }
651
+
652
+ function sessionStateCollection(sessionId: string): string {
653
+ return `${SESSION_STATE_COLLECTION_PREFIX}${sessionId}`;
654
+ }
655
+
656
+ function sessionRecallId(sourceId: string): string {
657
+ return `recall:${sourceId}`;
658
+ }
659
+
660
+ async function rebuildSessionRecallProjection(
661
+ rpc: Awaited<ReturnType<RpcGetter>>,
662
+ cfg: PluginConfig,
663
+ sessionId: string,
664
+ ): Promise<void> {
665
+ const rawCollection = `session:${sessionId}`;
666
+ const projectionCollection = sessionRecallCollection(sessionId);
667
+ const sessionRecords = await rpc.call<{ results: SearchResult[] }>("list_by_meta", {
668
+ collection: rawCollection,
669
+ key: "sessionId",
670
+ value: sessionId,
671
+ });
672
+ const rawSessionTurns = sortChronological(
673
+ sessionRecords.results.filter((item) =>
674
+ // cascade_tier is ranking metadata (cascade search tier); exclude from session history
675
+ item.metadata.type !== "summary" &&
676
+ item.metadata.type !== "guidance_shard" &&
677
+ typeof item.metadata.cascade_tier !== "number"
678
+ ),
679
+ );
680
+ const recentTail = selectRecentTail(rawSessionTurns, {
681
+ minTurns: cfg.continuityMinTurns ?? DEFAULT_CONTINUITY_MIN_TURNS,
682
+ tailBudgetTokens: cfg.continuityTailBudgetTokens ?? DEFAULT_CONTINUITY_TAIL_BUDGET_TOKENS,
683
+ tokenCost,
684
+ sameBundle: isContinuityBundleCoupled,
685
+ });
686
+ const projectionItems = recentTail.older;
687
+ const existingProjection = await rpc.call<{ results: SearchResult[] }>("list_collection", {
688
+ collection: projectionCollection,
689
+ });
690
+ const existingIds = existingProjection.results
691
+ .map((item) => item.id)
692
+ .filter((id): id is string => typeof id === "string" && id.length > 0);
693
+ if (existingIds.length > 0) {
694
+ await rpc.call("delete_batch", {
695
+ collection: projectionCollection,
696
+ ids: existingIds,
697
+ });
698
+ }
699
+ await Promise.all(projectionItems.map((item) =>
700
+ rpc.call("insert_text", {
701
+ collection: projectionCollection,
702
+ id: sessionRecallId(item.id),
703
+ score: item.score,
704
+ text: item.text,
705
+ metadata: {
706
+ ...item.metadata,
707
+ projection_class: "session_recall",
708
+ source_turn_id: item.id,
709
+ source_turn_ts: metadataTimestamp(item),
710
+ },
711
+ })
712
+ ));
713
+ }
714
+
324
715
  async function loadAuthoredCollections(
325
716
  rpc: Awaited<ReturnType<RpcGetter>>,
326
717
  cached: { hard: SearchResult[] | null; soft: SearchResult[] | null; variant: SearchResult[] | null },
327
718
  ): Promise<[SearchResult[], SearchResult[], SearchResult[]]> {
328
719
  if (cached.hard && cached.soft && cached.variant) {
329
- return [cached.hard, cached.soft, cached.variant];
720
+ return [
721
+ sortAuthoredItems(cached.hard),
722
+ sortAuthoredItems(cached.soft),
723
+ sortAuthoredItems(cached.variant),
724
+ ];
330
725
  }
331
726
 
332
727
  const [hard, soft, variant] = await Promise.all([
@@ -341,7 +736,11 @@ async function loadAuthoredCollections(
341
736
  : rpc.call<{ results: SearchResult[] }>("list_collection", { collection: AUTHORED_VARIANT_COLLECTION }),
342
737
  ]);
343
738
 
344
- return [hard.results, soft.results, variant.results];
739
+ return [
740
+ sortAuthoredItems(hard.results),
741
+ sortAuthoredItems(soft.results),
742
+ sortAuthoredItems(variant.results),
743
+ ];
345
744
  }
346
745
 
347
746
  function tokenCostSum(items: SearchResult[]): number {
@@ -372,6 +771,11 @@ function metadataTimestamp(item: SearchResult): number {
372
771
  return typeof raw === "number" && Number.isFinite(raw) ? raw : 0;
373
772
  }
374
773
 
774
+ function metadataNumber(item: SearchResult, key: string): number {
775
+ const raw = item.metadata[key];
776
+ return typeof raw === "number" && Number.isFinite(raw) ? raw : 0;
777
+ }
778
+
375
779
  function markRecentTail(items: SearchResult[], baseCount: number): SearchResult[] {
376
780
  const baseStart = Math.max(0, items.length - baseCount);
377
781
  return items.map((item, idx) => ({
@@ -394,6 +798,30 @@ function annotateCollection(items: SearchResult[], collection: string): SearchRe
394
798
  }));
395
799
  }
396
800
 
801
+ function sortAuthoredItems(items: SearchResult[]): SearchResult[] {
802
+ return [...items].sort((left, right) => {
803
+ const leftDoc = typeof left.metadata.source_doc === "string" ? left.metadata.source_doc : "";
804
+ const rightDoc = typeof right.metadata.source_doc === "string" ? right.metadata.source_doc : "";
805
+ if (leftDoc !== rightDoc) {
806
+ return leftDoc.localeCompare(rightDoc);
807
+ }
808
+
809
+ const leftPosition = metadataNumber(left, "position");
810
+ const rightPosition = metadataNumber(right, "position");
811
+ if (leftPosition !== rightPosition) {
812
+ return leftPosition - rightPosition;
813
+ }
814
+
815
+ const leftOrdinal = metadataNumber(left, "ordinal");
816
+ const rightOrdinal = metadataNumber(right, "ordinal");
817
+ if (leftOrdinal !== rightOrdinal) {
818
+ return leftOrdinal - rightOrdinal;
819
+ }
820
+
821
+ return left.id.localeCompare(right.id);
822
+ });
823
+ }
824
+
397
825
  function groupAccessCountUpdates(items: SearchResult[]): Array<{ collection: string; ids: string[] }> {
398
826
  const grouped = new Map<string, string[]>();
399
827
  for (const item of items) {
@@ -464,3 +892,14 @@ function isContinuityBundleCoupled(left: SearchResult, right: SearchResult): boo
464
892
  (leftRole === "assistant" && rightRole === "user")
465
893
  );
466
894
  }
895
+
896
+ function stabilityWeightForMessage(role: string): number {
897
+ switch (role) {
898
+ case "user":
899
+ return 0.5;
900
+ case "assistant":
901
+ return 0.25;
902
+ default:
903
+ return 0.2;
904
+ }
905
+ }