@xdarkicex/openclaw-memory-libravdb 1.4.2 → 1.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,128 @@
1
+ export const COMPARISON_ABLATION_MODES = [
2
+ "reserve_bump",
3
+ "protected_pair_pack",
4
+ "discriminative_affiliation",
5
+ "witness_position_pastness",
6
+ "contamination_penalty",
7
+ "pair_score_on_witness",
8
+ "comparison_blend",
9
+ ] as const;
10
+
11
+ export type ComparisonAblationMode = typeof COMPARISON_ABLATION_MODES[number];
12
+
13
+ export interface ComparisonProfileSummary {
14
+ ablationMode?: ComparisonAblationMode;
15
+ rankTotalMs: number;
16
+ decorateMs: number;
17
+ slotCoverageMs: number;
18
+ sideAffiliationMs: number;
19
+ specificityMs: number;
20
+ pairSelectionMs: number;
21
+ greedyFillMs: number;
22
+ recoveryPackingMs: number;
23
+ debugBuildMs: number;
24
+ rawCandidateCount: number;
25
+ comparisonCandidateCount: number;
26
+ side0AffiliatedCount: number;
27
+ side1AffiliatedCount: number;
28
+ normalizeTermsCalls: number;
29
+ normalizeContentTermsCalls: number;
30
+ estimateTokensCalls: number;
31
+ sortCalls: number;
32
+ totalSortedLength: number;
33
+ }
34
+
35
+ export interface ComparisonExperimentConfig {
36
+ profilingEnabled: boolean;
37
+ ablationMode: ComparisonAblationMode | null;
38
+ disableReserveBump: boolean;
39
+ disableProtectedPairPack: boolean;
40
+ disableDiscriminativeAffiliation: boolean;
41
+ disableWitnessPositionPastness: boolean;
42
+ disableContaminationPenalty: boolean;
43
+ disablePairScoreOnWitness: boolean;
44
+ disableComparisonBlend: boolean;
45
+ }
46
+
47
+ export function resolveComparisonExperimentConfig(
48
+ env: NodeJS.ProcessEnv = process.env,
49
+ ): ComparisonExperimentConfig {
50
+ const profilingEnabled = env.LONGMEMEVAL_PROFILE_COMPARISON === "1";
51
+ const rawMode = env.LONGMEMEVAL_COMPARISON_PROFILE_MODE?.trim() ?? "";
52
+ const ablationMode = COMPARISON_ABLATION_MODES.includes(rawMode as ComparisonAblationMode)
53
+ ? rawMode as ComparisonAblationMode
54
+ : null;
55
+ return {
56
+ profilingEnabled,
57
+ ablationMode,
58
+ disableReserveBump: ablationMode === "reserve_bump",
59
+ disableProtectedPairPack: ablationMode === "protected_pair_pack",
60
+ disableDiscriminativeAffiliation: ablationMode === "discriminative_affiliation",
61
+ disableWitnessPositionPastness: ablationMode === "witness_position_pastness",
62
+ disableContaminationPenalty: ablationMode === "contamination_penalty",
63
+ disablePairScoreOnWitness: ablationMode === "pair_score_on_witness",
64
+ disableComparisonBlend: ablationMode === "comparison_blend",
65
+ };
66
+ }
67
+
68
+ export function createComparisonProfileSummary(
69
+ ablationMode: ComparisonAblationMode | null,
70
+ ): ComparisonProfileSummary {
71
+ return {
72
+ ablationMode: ablationMode ?? undefined,
73
+ rankTotalMs: 0,
74
+ decorateMs: 0,
75
+ slotCoverageMs: 0,
76
+ sideAffiliationMs: 0,
77
+ specificityMs: 0,
78
+ pairSelectionMs: 0,
79
+ greedyFillMs: 0,
80
+ recoveryPackingMs: 0,
81
+ debugBuildMs: 0,
82
+ rawCandidateCount: 0,
83
+ comparisonCandidateCount: 0,
84
+ side0AffiliatedCount: 0,
85
+ side1AffiliatedCount: 0,
86
+ normalizeTermsCalls: 0,
87
+ normalizeContentTermsCalls: 0,
88
+ estimateTokensCalls: 0,
89
+ sortCalls: 0,
90
+ totalSortedLength: 0,
91
+ };
92
+ }
93
+
94
+ export function mergeComparisonProfileSummaries(
95
+ profiles: ComparisonProfileSummary[],
96
+ ): ComparisonProfileSummary | null {
97
+ if (profiles.length === 0) {
98
+ return null;
99
+ }
100
+
101
+ const merged = createComparisonProfileSummary(
102
+ profiles.every((profile) => profile.ablationMode === profiles[0]!.ablationMode)
103
+ ? (profiles[0]!.ablationMode ?? null)
104
+ : null,
105
+ );
106
+ for (const profile of profiles) {
107
+ merged.rankTotalMs += profile.rankTotalMs;
108
+ merged.decorateMs += profile.decorateMs;
109
+ merged.slotCoverageMs += profile.slotCoverageMs;
110
+ merged.sideAffiliationMs += profile.sideAffiliationMs;
111
+ merged.specificityMs += profile.specificityMs;
112
+ merged.pairSelectionMs += profile.pairSelectionMs;
113
+ merged.greedyFillMs += profile.greedyFillMs;
114
+ merged.recoveryPackingMs += profile.recoveryPackingMs;
115
+ merged.debugBuildMs += profile.debugBuildMs;
116
+ merged.rawCandidateCount += profile.rawCandidateCount;
117
+ merged.comparisonCandidateCount += profile.comparisonCandidateCount;
118
+ merged.side0AffiliatedCount += profile.side0AffiliatedCount;
119
+ merged.side1AffiliatedCount += profile.side1AffiliatedCount;
120
+ merged.normalizeTermsCalls += profile.normalizeTermsCalls;
121
+ merged.normalizeContentTermsCalls += profile.normalizeContentTermsCalls;
122
+ merged.estimateTokensCalls += profile.estimateTokensCalls;
123
+ merged.sortCalls += profile.sortCalls;
124
+ merged.totalSortedLength += profile.totalSortedLength;
125
+ }
126
+
127
+ return merged;
128
+ }
@@ -13,6 +13,8 @@ import {
13
13
  rankSection7VariantCandidates,
14
14
  } from "./scoring.js";
15
15
  import { buildInjectedMemoryMessageContent, buildMemoryHeader, recentIds } from "./recall-utils.js";
16
+ import { detectDreamQuerySignal, resolveDreamCollection } from "./dream-routing.js";
17
+ import { resolveComparisonExperimentConfig } from "./comparison-experiments.js";
16
18
  import {
17
19
  decideTemporalSelectorGuard,
18
20
  detectTemporalQuerySignal,
@@ -58,6 +60,8 @@ export function buildContextEngineFactory(
58
60
  let authoredVariantCache: SearchResult[] | null = null;
59
61
  const authoredVariantRecallCache = new Map<string, SearchResult[]>();
60
62
  const afterTurnIngestedKeys = new Map<string, number>();
63
+ // Tracks accumulated uncompacted token count per session for auto-compaction
64
+ const sessionTokenAccumulators = new Map<string, number>();
61
65
 
62
66
  // Session-scoped elevated-guidance cache keyed by sessionId + generation + durable namespace + queryText
63
67
  const elevatedRecallCache = new Map<string, SearchResult[]>();
@@ -168,6 +172,20 @@ export function buildContextEngineFactory(
168
172
  if (result.ingested) {
169
173
  rememberAfterTurnIngest(afterTurnIngestedKeys, dedupeKey);
170
174
  }
175
+
176
+ // Auto-trigger compaction when the session's uncompacted token count exceeds
177
+ // the budget. This keeps session size bounded without relying on the host
178
+ // to call compact() explicitly.
179
+ const sessionTokenBudget = cfg.compactSessionTokenBudget ?? 2000;
180
+ if (sessionTokenBudget > 0) {
181
+ const tokens = estimateTokens(normalized.content);
182
+ const accumulated = (sessionTokenAccumulators.get(sessionId) ?? 0) + tokens;
183
+ sessionTokenAccumulators.set(sessionId, accumulated);
184
+ if (accumulated >= sessionTokenBudget) {
185
+ sessionTokenAccumulators.set(sessionId, 0);
186
+ void this.compact({ sessionId, force: false }).catch(() => {});
187
+ }
188
+ }
171
189
  }
172
190
  },
173
191
  async assemble({ sessionId, sessionKey, userId, messages, tokenBudget, ...rest }: ContextAssembleArgs & Record<string, unknown>) {
@@ -187,11 +205,14 @@ export function buildContextEngineFactory(
187
205
  systemPromptAddition: "",
188
206
  } satisfies ContextAssembleResult;
189
207
  }
208
+ const dreamQuery = detectDreamQuerySignal(queryText);
190
209
  const temporalQuery = detectTemporalQuerySignal(queryText);
191
210
  const temporalSelectorGuard = decideTemporalSelectorGuard(queryText, temporalQuery);
211
+ const comparisonExperiment = resolveComparisonExperimentConfig();
212
+ const emitComparisonProfile = comparisonExperiment.profilingEnabled;
192
213
 
193
214
  const excluded = recentIds(normalizedMessages, 4);
194
- const cached = recallCache.take({ userId: durableNamespace, queryText });
215
+ const cached = dreamQuery.active ? undefined : recallCache.take({ userId: durableNamespace, queryText });
195
216
 
196
217
  const rpc = await getRpc();
197
218
 
@@ -252,8 +273,11 @@ export function buildContextEngineFactory(
252
273
  cached,
253
274
  excluded,
254
275
  queryText,
276
+ dreamQuery,
255
277
  temporalQuery,
256
278
  temporalSelectorGuard,
279
+ comparisonExperiment,
280
+ emitComparisonProfile,
257
281
  sessionId,
258
282
  userId: durableNamespace,
259
283
  visibleMessages: originalMessages,
@@ -289,8 +313,11 @@ export function buildContextEngineFactory(
289
313
  cached,
290
314
  excluded,
291
315
  queryText,
316
+ dreamQuery,
292
317
  temporalQuery,
293
318
  temporalSelectorGuard,
319
+ comparisonExperiment,
320
+ emitComparisonProfile,
294
321
  sessionId,
295
322
  userId,
296
323
  visibleMessages,
@@ -308,8 +335,11 @@ export function buildContextEngineFactory(
308
335
  cached: ReturnType<RecallCache<SearchResult>["take"]>;
309
336
  excluded: string[];
310
337
  queryText: string;
338
+ dreamQuery: ReturnType<typeof detectDreamQuerySignal>;
311
339
  temporalQuery: ReturnType<typeof detectTemporalQuerySignal>;
312
340
  temporalSelectorGuard: ReturnType<typeof decideTemporalSelectorGuard>;
341
+ comparisonExperiment: ReturnType<typeof resolveComparisonExperimentConfig>;
342
+ emitComparisonProfile: boolean;
313
343
  sessionId: string;
314
344
  userId: string;
315
345
  visibleMessages: MemoryMessage[];
@@ -321,6 +351,52 @@ export function buildContextEngineFactory(
321
351
  const memoryBudget = tokenBudget * (cfg.tokenBudgetFraction ?? 0.25);
322
352
  const hardItems = authoredHard;
323
353
  const hardUsed = tokenCostSum(hardItems);
354
+ const dreamMode = dreamQuery.active;
355
+ const dreamCollection = resolveDreamCollection(userId);
356
+
357
+ if (dreamMode) {
358
+ const authoredSoftTarget = Math.max(0, memoryBudget * (cfg.authoredSoftBudgetFraction ?? 0.3));
359
+ const softBudget = Math.max(0, Math.min(authoredSoftTarget, memoryBudget - hardUsed));
360
+ const softItems = fitPromptBudget(authoredSoft, softBudget);
361
+ const remainingBudget = Math.max(0, memoryBudget - hardUsed - tokenCostSum(softItems));
362
+
363
+ profiler?.mark("dream_search");
364
+ const dreamTopK = Math.max(cfg.topK ?? 8, 1);
365
+ const dreamHits = await rpc.call<{ results: SearchResult[] }>("search_text", {
366
+ collection: dreamCollection,
367
+ text: queryText,
368
+ k: dreamTopK,
369
+ });
370
+
371
+ profiler?.mark("dream_rank");
372
+ const rankedDream = rankSection7VariantCandidates(
373
+ annotateCollection(dreamHits.results ?? [], dreamCollection),
374
+ {
375
+ queryText,
376
+ k1: dreamTopK,
377
+ k2: dreamTopK,
378
+ theta1: cfg.section7Theta1,
379
+ kappa: cfg.section7Kappa,
380
+ authorityRecencyLambda: cfg.section7AuthorityRecencyLambda,
381
+ authorityRecencyWeight: cfg.section7AuthorityRecencyWeight,
382
+ authorityFrequencyWeight: cfg.section7AuthorityFrequencyWeight,
383
+ authorityAuthoredWeight: cfg.section7AuthorityAuthoredWeight,
384
+ sessionId,
385
+ userId,
386
+ },
387
+ );
388
+ const dreamItems = fitPromptBudget(rankedDream, remainingBudget);
389
+ const selected = [...hardItems, ...softItems, ...dreamItems];
390
+ const selectedMessages = selected.map((item) => ({
391
+ role: "system",
392
+ content: buildInjectedMemoryMessageContent(item),
393
+ }));
394
+ return {
395
+ messages: [...selectedMessages, ...visibleMessages],
396
+ estimatedTokens: countTokens(selectedMessages) + countTokens(visibleMessages),
397
+ systemPromptAddition: buildMemoryHeader(selected),
398
+ };
399
+ }
324
400
 
325
401
  profiler?.mark("session");
326
402
  const sessionRecords = await rpc.call<{ results: SearchResult[] }>("list_by_meta", {
@@ -397,7 +473,10 @@ export function buildContextEngineFactory(
397
473
  const searchSessionSummary = useSessionSummarySearchExperiment(cfg);
398
474
  let sessionSearchCollection = `session:${sessionId}`;
399
475
  let sessionExcludeIds = [...excluded, ...recentTailIDs];
400
- if (searchSessionSummary) {
476
+ if (dreamMode) {
477
+ sessionSearchCollection = dreamCollection;
478
+ sessionExcludeIds = [...excluded];
479
+ } else if (searchSessionSummary) {
401
480
  const summaryCollection = sessionSummaryCollection(sessionId);
402
481
  const summaryRecords = await rpc.call<{ results: SearchResult[] }>("list_collection", {
403
482
  collection: summaryCollection,
@@ -423,14 +502,18 @@ export function buildContextEngineFactory(
423
502
 
424
503
  profiler?.mark("recall_user_global");
425
504
  const [userHits, globalHits] = await Promise.all([
426
- cached?.userHits
505
+ dreamMode
506
+ ? Promise.resolve({ results: [] as SearchResult[] })
507
+ : cached?.userHits
427
508
  ? Promise.resolve({ results: cached.userHits })
428
509
  : rpc.call<{ results: SearchResult[] }>("search_text", {
429
510
  collection: `user:${userId}`,
430
511
  text: queryText,
431
512
  k: Math.ceil((cfg.topK ?? 8) / 2),
432
513
  }),
433
- cached?.globalHits
514
+ dreamMode
515
+ ? Promise.resolve({ results: [] as SearchResult[] })
516
+ : cached?.globalHits
434
517
  ? Promise.resolve({ results: cached.globalHits })
435
518
  : rpc.call<{ results: SearchResult[] }>("search_text", {
436
519
  collection: "global",
@@ -439,7 +522,7 @@ export function buildContextEngineFactory(
439
522
  }),
440
523
  ]);
441
524
 
442
- if (!cached) {
525
+ if (!cached && !dreamMode) {
443
526
  recallCache.put({
444
527
  userId,
445
528
  queryText,
@@ -453,7 +536,9 @@ export function buildContextEngineFactory(
453
536
  const authoredVariantKey = `${queryText}\n${coarseTopK}`;
454
537
  const cachedAuthoredVariantHits = authoredVariantRecallCache.get(authoredVariantKey);
455
538
  const [authoredVariantHits] = await Promise.all([
456
- cachedAuthoredVariantHits
539
+ dreamMode
540
+ ? Promise.resolve({ results: [] as SearchResult[] })
541
+ : cachedAuthoredVariantHits
457
542
  ? Promise.resolve({ results: cachedAuthoredVariantHits })
458
543
  : rpc.call<{ results: SearchResult[] }>("search_text", {
459
544
  collection: AUTHORED_VARIANT_COLLECTION,
@@ -470,7 +555,9 @@ export function buildContextEngineFactory(
470
555
  const elevatedKey = `${sessionId}\n${elevatedGeneration}\n${userId}\n${queryText}`;
471
556
  const cachedElevated = elevatedRecallCache.get(elevatedKey);
472
557
  const [elevatedHits] = await Promise.all([
473
- cachedElevated
558
+ dreamMode
559
+ ? Promise.resolve({ results: [] as SearchResult[] })
560
+ : cachedElevated
474
561
  ? Promise.resolve({ results: cachedElevated })
475
562
  : rpc.call<{ results: SearchResult[] }>("search_text_collections", {
476
563
  collections: [
@@ -489,7 +576,7 @@ export function buildContextEngineFactory(
489
576
  profiler?.mark("rank");
490
577
  const ranked = rankSection7VariantCandidates(
491
578
  [
492
- ...annotateCollection(sessionHits.results, `session:${sessionId}`),
579
+ ...annotateCollection(sessionHits.results, sessionSearchCollection),
493
580
  ...elevatedHits.results,
494
581
  ...userHits.results,
495
582
  ...globalHits.results,
@@ -525,17 +612,31 @@ export function buildContextEngineFactory(
525
612
  // Recovery trigger is evaluated before variant fitting so healthy sessions
526
613
  // do not lose recall budget to an unused recovery reserve.
527
614
  profiler?.mark("recovery_trigger");
528
- const recoveryTrigger = detectRetrievalFailure(mergedCandidates, {
529
- floorScore: cfg.recoveryFloorScore ?? 0.15,
530
- minTopK: cfg.recoveryMinTopK ?? 4,
531
- meanConfidenceThresh: cfg.recoveryMinConfidenceMean ?? 0.5,
532
- });
533
- const crossSessionRawRecovery =
615
+ const recoveryTrigger = dreamMode
616
+ ? {
617
+ signal1CascadeTier3: false,
618
+ signal2TopScoreBelowFloor: false,
619
+ signal3AllSummariesLowConfidence: false,
620
+ fire: false,
621
+ }
622
+ : detectRetrievalFailure(mergedCandidates, {
623
+ floorScore: cfg.recoveryFloorScore ?? 0.15,
624
+ minTopK: cfg.recoveryMinTopK ?? 4,
625
+ meanConfidenceThresh: cfg.recoveryMinConfidenceMean ?? 0.5,
626
+ });
627
+ const crossSessionRawRecovery = !dreamMode &&
534
628
  rawSessionTurns.length === 0 &&
535
629
  sessionHits.results.length === 0;
536
- const recoveryReserveTokens = (recoveryTrigger.fire || crossSessionRawRecovery)
630
+ const baseRecoveryReserveTokens = (recoveryTrigger.fire || crossSessionRawRecovery)
537
631
  ? Math.min(memoryBudget, Math.max(Math.floor(memoryBudget * 0.10), 16), 128)
538
632
  : 0;
633
+ const isComparisonTemporalRecovery = temporalSelectorGuard.shouldApply &&
634
+ temporalQuery.matchedPatterns.includes("first or earlier");
635
+ const recoveryReserveTokens = isComparisonTemporalRecovery &&
636
+ !comparisonExperiment.disableReserveBump &&
637
+ baseRecoveryReserveTokens > 0
638
+ ? Math.min(memoryBudget, Math.max(baseRecoveryReserveTokens, Math.ceil(baseRecoveryReserveTokens * 1.8)))
639
+ : baseRecoveryReserveTokens;
539
640
  const elevatedGuidanceBudget = Math.max(
540
641
  0,
541
642
  Math.min(
@@ -570,8 +671,10 @@ export function buildContextEngineFactory(
570
671
  // it never modifies the C_total(q) output and does not spend from tau_V.
571
672
  let recoveryItems: SearchResult[] = [];
572
673
  let rawUserRecoveryDebug: NonNullable<NonNullable<ContextAssembleResult["_debug"]>["rawUserRecoveryCandidates"]> = [];
674
+ let dedupedRecoveryDebug: NonNullable<NonNullable<ContextAssembleResult["_debug"]>["recoveryDedupedOrder"]> = [];
675
+ let fittedRecoveryDebug: NonNullable<NonNullable<ContextAssembleResult["_debug"]>["recoveryFittedOrder"]> = [];
573
676
  let temporalRecoveryResult: TemporalRecoveryRankingResult | null = null;
574
- if (recoveryTrigger.fire || crossSessionRawRecovery) {
677
+ if (!dreamMode && (recoveryTrigger.fire || crossSessionRawRecovery)) {
575
678
  profiler?.mark("recovery_expand");
576
679
  const recoveryExcludeIDs = [...excluded, ...recentTailIDs, ...theoremSelectedIDs];
577
680
  const recoveryCandidates: SearchResult[] = [];
@@ -615,6 +718,7 @@ export function buildContextEngineFactory(
615
718
  maxSelected: 3,
616
719
  nowMs: Date.now(),
617
720
  recencyLambda: cfg.recencyLambdaUser ?? 0.00001,
721
+ selectionTokenBudget: recoveryReserveTokens,
618
722
  })
619
723
  : null;
620
724
  const reranked = temporalRecoveryResult
@@ -646,26 +750,91 @@ export function buildContextEngineFactory(
646
750
  : 0,
647
751
  finalScore: typeof item.finalScore === "number" ? item.finalScore : 0,
648
752
  rationale: typeof item.rationale === "string" ? item.rationale : "",
753
+ comparisonSide: "comparisonSide" in item && (item.comparisonSide === 0 || item.comparisonSide === 1 || item.comparisonSide === null)
754
+ ? item.comparisonSide
755
+ : undefined,
756
+ comparisonSlot: "comparisonSlot" in item && typeof item.comparisonSlot === "string"
757
+ ? item.comparisonSlot
758
+ : undefined,
759
+ comparisonSlotRecall: "comparisonSlotRecall" in item && typeof item.comparisonSlotRecall === "number"
760
+ ? item.comparisonSlotRecall
761
+ : undefined,
762
+ comparisonSlotPrecision: "comparisonSlotPrecision" in item && typeof item.comparisonSlotPrecision === "number"
763
+ ? item.comparisonSlotPrecision
764
+ : undefined,
765
+ comparisonSlotSpecificity: "comparisonSlotSpecificity" in item && typeof item.comparisonSlotSpecificity === "number"
766
+ ? item.comparisonSlotSpecificity
767
+ : undefined,
768
+ comparisonSlotPositionWeightedRecall: "comparisonSlotPositionWeightedRecall" in item && typeof item.comparisonSlotPositionWeightedRecall === "number"
769
+ ? item.comparisonSlotPositionWeightedRecall
770
+ : undefined,
771
+ comparisonSlotPositionWeightedPrecision: "comparisonSlotPositionWeightedPrecision" in item && typeof item.comparisonSlotPositionWeightedPrecision === "number"
772
+ ? item.comparisonSlotPositionWeightedPrecision
773
+ : undefined,
774
+ comparisonSlotPositionWeightedSpecificity: "comparisonSlotPositionWeightedSpecificity" in item && typeof item.comparisonSlotPositionWeightedSpecificity === "number"
775
+ ? item.comparisonSlotPositionWeightedSpecificity
776
+ : undefined,
777
+ comparisonFirstPersonClauseCount: "comparisonFirstPersonClauseCount" in item && typeof item.comparisonFirstPersonClauseCount === "number"
778
+ ? item.comparisonFirstPersonClauseCount
779
+ : undefined,
780
+ comparisonProspectivePersonalVerbCount: "comparisonProspectivePersonalVerbCount" in item && typeof item.comparisonProspectivePersonalVerbCount === "number"
781
+ ? item.comparisonProspectivePersonalVerbCount
782
+ : undefined,
783
+ comparisonPlanningDensity: "comparisonPlanningDensity" in item && typeof item.comparisonPlanningDensity === "number"
784
+ ? item.comparisonPlanningDensity
785
+ : undefined,
786
+ comparisonPastness: "comparisonPastness" in item && typeof item.comparisonPastness === "number"
787
+ ? item.comparisonPastness
788
+ : undefined,
789
+ comparisonSideWitnessScore: "comparisonSideWitnessScore" in item && typeof item.comparisonSideWitnessScore === "number"
790
+ ? item.comparisonSideWitnessScore
791
+ : undefined,
649
792
  }));
650
793
  }
651
794
  recoveryCandidates.push(
652
- ...reranked.ranked.map((item) => ({
653
- ...item,
654
- finalScore: typeof item.finalScore === "number" ? item.finalScore : item.score,
655
- metadata: {
656
- ...item.metadata,
657
- recovery_fallback: true,
658
- recovery_scope: "user_turns",
659
- },
660
- })),
795
+ ...reranked.ranked.map((item) => {
796
+ return {
797
+ ...item,
798
+ finalScore: typeof item.finalScore === "number" ? item.finalScore : item.score,
799
+ metadata: {
800
+ ...item.metadata,
801
+ recovery_fallback: true,
802
+ recovery_scope: "user_turns",
803
+ },
804
+ };
805
+ }),
661
806
  );
662
807
  }
663
808
 
664
- const fittedRecovery = fitPromptBudgetFirstFit(
665
- dedupeRecoveryCandidates(recoveryCandidates),
666
- recoveryReserveTokens,
667
- );
809
+ const dedupedRecovery = dedupeRecoveryCandidates(recoveryCandidates);
810
+ const packingStart = temporalRecoveryResult?.comparisonProfile ? process.hrtime.bigint() : 0n;
811
+ const fittedRecovery = comparisonExperiment.disableProtectedPairPack
812
+ ? fitPromptBudgetFirstFit(dedupedRecovery, recoveryReserveTokens)
813
+ : fitProtectedComparisonRecovery(
814
+ dedupedRecovery,
815
+ recoveryReserveTokens,
816
+ temporalRecoveryResult?.comparisonCoverageApplied === true
817
+ ? temporalRecoveryResult.comparisonWitnessIds
818
+ : undefined,
819
+ );
820
+ if (temporalRecoveryResult?.comparisonProfile) {
821
+ temporalRecoveryResult.comparisonProfile.recoveryPackingMs += Number(process.hrtime.bigint() - packingStart) / 1_000_000;
822
+ }
668
823
  recoveryItems = fittedRecovery;
824
+ if (debugRecovery) {
825
+ dedupedRecoveryDebug = dedupedRecovery.map((item) => ({
826
+ id: item.id,
827
+ recoveryScope: typeof item.metadata.recovery_scope === "string" ? item.metadata.recovery_scope : "unknown",
828
+ finalScore: typeof item.finalScore === "number" ? item.finalScore : item.score,
829
+ tokenEstimate: estimateTokens(item.text),
830
+ }));
831
+ fittedRecoveryDebug = fittedRecovery.map((item) => ({
832
+ id: item.id,
833
+ recoveryScope: typeof item.metadata.recovery_scope === "string" ? item.metadata.recovery_scope : "unknown",
834
+ finalScore: typeof item.finalScore === "number" ? item.finalScore : item.score,
835
+ tokenEstimate: estimateTokens(item.text),
836
+ }));
837
+ }
669
838
  if (debugRecovery && rawUserRecoveryDebug.length > 0) {
670
839
  const selectedIDs = new Set(
671
840
  fittedRecovery
@@ -698,11 +867,11 @@ export function buildContextEngineFactory(
698
867
  content: buildInjectedMemoryMessageContent(item),
699
868
  }));
700
869
 
701
- return {
702
- messages: [...selectedMessages, ...visibleMessages],
703
- estimatedTokens: countTokens(selectedMessages) + countTokens(visibleMessages),
704
- systemPromptAddition: buildMemoryHeader(selected),
705
- _debug: debugRecovery
870
+ return {
871
+ messages: [...selectedMessages, ...visibleMessages],
872
+ estimatedTokens: countTokens(selectedMessages) + countTokens(visibleMessages),
873
+ systemPromptAddition: buildMemoryHeader(selected),
874
+ _debug: (debugRecovery || emitComparisonProfile)
706
875
  ? {
707
876
  recoveryTriggerFired: recoveryTrigger.fire,
708
877
  crossSessionRawRecovery,
@@ -713,10 +882,17 @@ export function buildContextEngineFactory(
713
882
  temporalSelectorApplied: temporalSelectorGuard.shouldApply,
714
883
  temporalSelectorReason: temporalSelectorGuard.reason,
715
884
  temporalRecoverySlots: temporalRecoveryResult?.slots,
885
+ temporalComparisonCoverageApplied: temporalRecoveryResult?.comparisonCoverageApplied,
886
+ temporalComparisonCoverageSlots: temporalRecoveryResult?.comparisonCoverageSlots,
887
+ temporalComparisonCoverageMinTokens: temporalRecoveryResult?.comparisonCoverageMinTokens,
888
+ temporalComparisonWitnessIds: temporalRecoveryResult?.comparisonWitnessIds,
889
+ comparisonProfile: temporalRecoveryResult?.comparisonProfile,
890
+ recoveryDedupedOrder: dedupedRecoveryDebug,
891
+ recoveryFittedOrder: fittedRecoveryDebug,
716
892
  rawUserRecoveryCandidates: rawUserRecoveryDebug,
717
893
  }
718
894
  : undefined,
719
- };
895
+ };
720
896
  },
721
897
  async compact({ sessionId, force, targetSize }: ContextCompactArgs) {
722
898
  const rpc = await getRpc();
@@ -967,6 +1143,42 @@ function dedupeRecoveryCandidates(items: SearchResult[]): SearchResult[] {
967
1143
  return [...byKey.values()].sort((left, right) => (right.finalScore ?? right.score) - (left.finalScore ?? left.score));
968
1144
  }
969
1145
 
1146
+ function fitProtectedComparisonRecovery(
1147
+ items: SearchResult[],
1148
+ tokenBudget: number,
1149
+ protectedWitnessIds?: string[],
1150
+ ): SearchResult[] {
1151
+ if (!protectedWitnessIds || protectedWitnessIds.length === 0) {
1152
+ return fitPromptBudgetFirstFit(items, tokenBudget);
1153
+ }
1154
+
1155
+ const protectedById = new Map<string, SearchResult>();
1156
+ const remaining: SearchResult[] = [];
1157
+
1158
+ for (const item of items) {
1159
+ if (protectedWitnessIds.includes(item.id) && !protectedById.has(item.id)) {
1160
+ protectedById.set(item.id, item);
1161
+ continue;
1162
+ }
1163
+ remaining.push(item);
1164
+ }
1165
+
1166
+ const protectedItems = protectedWitnessIds
1167
+ .map((id) => protectedById.get(id))
1168
+ .filter((item): item is SearchResult => Boolean(item));
1169
+ if (protectedItems.length !== protectedWitnessIds.length) {
1170
+ return fitPromptBudgetFirstFit(items, tokenBudget);
1171
+ }
1172
+
1173
+ const protectedTokens = protectedItems.reduce((sum, item) => sum + estimateTokens(item.text), 0);
1174
+ if (protectedTokens > tokenBudget) {
1175
+ return fitPromptBudgetFirstFit(items, tokenBudget);
1176
+ }
1177
+
1178
+ const tail = fitPromptBudgetFirstFit(remaining, tokenBudget - protectedTokens);
1179
+ return [...protectedItems, ...tail];
1180
+ }
1181
+
970
1182
  function clampFraction(value: number | undefined): number {
971
1183
  if (typeof value !== "number" || !Number.isFinite(value)) {
972
1184
  return 0;
@@ -1046,33 +1258,35 @@ async function ingestCanonicalMessage(params: {
1046
1258
  text: normalized.content,
1047
1259
  });
1048
1260
 
1049
- if (gating.g >= (params.cfg.ingestionGateThreshold ?? 0.35)) {
1050
- void rpc.call("insert_text", {
1051
- collection: `user:${durableNamespace}`,
1052
- id: `${durableNamespace}:${turnId}`,
1053
- text: normalized.content,
1054
- metadata: {
1055
- role: normalized.role,
1056
- ts,
1057
- sessionId: params.sessionId,
1058
- type: "turn",
1059
- userId: durableNamespace,
1060
- source_turn_id: turnId,
1061
- provenance_class: "durable_user_memory",
1062
- stability_weight: Math.max(stabilityWeightForMessage(normalized.role), gating.g),
1063
- gating_score: gating.g,
1064
- gating_t: gating.t,
1065
- gating_h: gating.h,
1066
- gating_r: gating.r,
1067
- gating_d: gating.d,
1068
- gating_p: gating.p,
1069
- gating_a: gating.a,
1070
- gating_dtech: gating.dtech,
1071
- gating_gconv: gating.gconv,
1072
- gating_gtech: gating.gtech,
1073
- },
1074
- }).catch(console.error);
1075
- }
1261
+ // Gating is designed for markdown file deduplication — not conversational content.
1262
+ // User turns must be stored durably regardless of gating score so the agent can recall
1263
+ // friends, preferences, and context across sessions. Gating signals are still
1264
+ // recorded in metadata for observability, but do not gate the insert.
1265
+ void rpc.call("insert_text", {
1266
+ collection: `user:${durableNamespace}`,
1267
+ id: `${durableNamespace}:${turnId}`,
1268
+ text: normalized.content,
1269
+ metadata: {
1270
+ role: normalized.role,
1271
+ ts,
1272
+ sessionId: params.sessionId,
1273
+ type: "turn",
1274
+ userId: durableNamespace,
1275
+ source_turn_id: turnId,
1276
+ provenance_class: "durable_user_memory",
1277
+ stability_weight: Math.max(stabilityWeightForMessage(normalized.role), gating.g),
1278
+ gating_score: gating.g,
1279
+ gating_t: gating.t,
1280
+ gating_h: gating.h,
1281
+ gating_r: gating.r,
1282
+ gating_d: gating.d,
1283
+ gating_p: gating.p,
1284
+ gating_a: gating.a,
1285
+ gating_dtech: gating.dtech,
1286
+ gating_gconv: gating.gconv,
1287
+ gating_gtech: gating.gtech,
1288
+ },
1289
+ }).catch(console.error);
1076
1290
  } catch {
1077
1291
  // Session storage already happened; skip durable promotion on gating failure.
1078
1292
  }