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