@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.
- package/HOOK.md +14 -0
- package/README.md +32 -2
- package/dist/cli.d.ts +39 -0
- package/dist/cli.js +208 -0
- package/dist/context-engine.d.ts +56 -0
- package/dist/context-engine.js +125 -0
- package/dist/dream-promotion.d.ts +47 -0
- package/dist/dream-promotion.js +363 -0
- package/dist/dream-routing.d.ts +6 -0
- package/dist/dream-routing.js +31 -0
- package/dist/durable-namespace.d.ts +6 -0
- package/dist/durable-namespace.js +24 -0
- package/dist/grpc-client.d.ts +23 -0
- package/dist/grpc-client.js +104 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +40 -0
- package/dist/lifecycle-hooks.d.ts +4 -0
- package/dist/lifecycle-hooks.js +64 -0
- package/dist/markdown-hash.d.ts +3 -0
- package/dist/markdown-hash.js +82 -0
- package/dist/markdown-ingest.d.ts +43 -0
- package/dist/markdown-ingest.js +464 -0
- package/dist/memory-provider.d.ts +4 -0
- package/dist/memory-provider.js +13 -0
- package/dist/memory-runtime.d.ts +118 -0
- package/dist/memory-runtime.js +217 -0
- package/dist/plugin-runtime.d.ts +28 -0
- package/dist/plugin-runtime.js +127 -0
- package/dist/proto/intelligence_kernel/v1/kernel.proto +378 -0
- package/dist/recall-cache.d.ts +2 -0
- package/dist/recall-cache.js +30 -0
- package/dist/rpc-protobuf-codecs.d.ts +70 -0
- package/dist/rpc-protobuf-codecs.js +77 -0
- package/dist/rpc.d.ts +14 -0
- package/dist/rpc.js +121 -0
- package/dist/sidecar.d.ts +34 -0
- package/dist/sidecar.js +535 -0
- package/dist/types.d.ts +163 -0
- package/dist/types.js +1 -0
- package/docs/contributing.md +14 -13
- package/docs/install.md +7 -9
- package/docs/installation.md +23 -16
- package/docs/uninstall.md +1 -1
- package/index.js +2 -0
- package/openclaw.plugin.json +2 -2
- package/package.json +39 -16
- package/packaging/README.md +0 -71
- package/packaging/homebrew/libravdbd.rb.tmpl +0 -224
- package/packaging/launchd/com.xdarkicex.libravdbd.plist +0 -32
- package/packaging/systemd/libravdbd.service +0 -12
- package/src/cli.ts +0 -299
- package/src/comparison-experiments.ts +0 -128
- package/src/context-engine.ts +0 -1645
- package/src/continuity.ts +0 -93
- package/src/dream-promotion.ts +0 -492
- package/src/dream-routing.ts +0 -40
- package/src/durable-namespace.ts +0 -34
- package/src/index.ts +0 -47
- package/src/lifecycle-hooks.ts +0 -96
- package/src/markdown-hash.ts +0 -104
- package/src/markdown-ingest.ts +0 -627
- package/src/memory-provider.ts +0 -25
- package/src/memory-runtime.ts +0 -283
- package/src/openclaw-plugin-sdk.d.ts +0 -59
- package/src/plugin-runtime.ts +0 -119
- package/src/recall-cache.ts +0 -34
- package/src/recall-utils.ts +0 -131
- package/src/rpc.ts +0 -92
- package/src/scoring.ts +0 -632
- package/src/sidecar.ts +0 -583
- package/src/temporal.ts +0 -1031
- package/src/tokens.ts +0 -52
- package/src/types.ts +0 -278
- package/tsconfig.json +0 -20
- package/tsconfig.tests.json +0 -12
package/src/context-engine.ts
DELETED
|
@@ -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
|
-
}
|