openclaw-cortex-memory 0.1.0-Alpha.2 → 0.1.0-Alpha.20
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 +115 -90
- package/SKILL.md +96 -32
- package/dist/index.d.ts +52 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +677 -1195
- package/dist/index.js.map +1 -1
- package/dist/openclaw.plugin.json +157 -5
- package/dist/src/dedup/three_stage_deduplicator.d.ts +25 -0
- package/dist/src/dedup/three_stage_deduplicator.d.ts.map +1 -0
- package/dist/src/dedup/three_stage_deduplicator.js +225 -0
- package/dist/src/dedup/three_stage_deduplicator.js.map +1 -0
- package/dist/src/engine/memory_engine.d.ts +2 -1
- package/dist/src/engine/memory_engine.d.ts.map +1 -1
- package/dist/src/engine/ts_engine.d.ts +95 -0
- package/dist/src/engine/ts_engine.d.ts.map +1 -1
- package/dist/src/engine/ts_engine.js +918 -38
- package/dist/src/engine/ts_engine.js.map +1 -1
- package/dist/src/engine/types.d.ts +11 -0
- package/dist/src/engine/types.d.ts.map +1 -1
- package/dist/src/graph/ontology.d.ts +53 -0
- package/dist/src/graph/ontology.d.ts.map +1 -0
- package/dist/src/graph/ontology.js +252 -0
- package/dist/src/graph/ontology.js.map +1 -0
- package/dist/src/reflect/reflector.d.ts +7 -0
- package/dist/src/reflect/reflector.d.ts.map +1 -1
- package/dist/src/reflect/reflector.js +75 -1
- package/dist/src/reflect/reflector.js.map +1 -1
- package/dist/src/session/session_end.d.ts +56 -0
- package/dist/src/session/session_end.d.ts.map +1 -1
- package/dist/src/session/session_end.js +270 -55
- package/dist/src/session/session_end.js.map +1 -1
- package/dist/src/store/archive_store.d.ts +115 -0
- package/dist/src/store/archive_store.d.ts.map +1 -0
- package/dist/src/store/archive_store.js +446 -0
- package/dist/src/store/archive_store.js.map +1 -0
- package/dist/src/store/embedding_utils.d.ts +32 -0
- package/dist/src/store/embedding_utils.d.ts.map +1 -0
- package/dist/src/store/embedding_utils.js +173 -0
- package/dist/src/store/embedding_utils.js.map +1 -0
- package/dist/src/store/read_store.d.ts +59 -0
- package/dist/src/store/read_store.d.ts.map +1 -1
- package/dist/src/store/read_store.js +1114 -17
- package/dist/src/store/read_store.js.map +1 -1
- package/dist/src/store/vector_store.d.ts +43 -0
- package/dist/src/store/vector_store.d.ts.map +1 -0
- package/dist/src/store/vector_store.js +200 -0
- package/dist/src/store/vector_store.js.map +1 -0
- package/dist/src/store/write_store.d.ts +45 -0
- package/dist/src/store/write_store.d.ts.map +1 -1
- package/dist/src/store/write_store.js +230 -0
- package/dist/src/store/write_store.js.map +1 -1
- package/dist/src/sync/session_sync.d.ts +52 -2
- package/dist/src/sync/session_sync.d.ts.map +1 -1
- package/dist/src/sync/session_sync.js +474 -22
- package/dist/src/sync/session_sync.js.map +1 -1
- package/dist/src/utils/runtime_env.d.ts +4 -0
- package/dist/src/utils/runtime_env.d.ts.map +1 -0
- package/dist/src/utils/runtime_env.js +51 -0
- package/dist/src/utils/runtime_env.js.map +1 -0
- package/openclaw.plugin.json +157 -5
- package/package.json +21 -6
- package/scripts/cli.js +19 -14
- package/scripts/uninstall.js +22 -5
- package/index.ts +0 -2092
- package/scripts/install.js +0 -27
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.createReadStore = createReadStore;
|
|
37
37
|
const fs = __importStar(require("fs"));
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
|
+
const module_1 = require("module");
|
|
39
40
|
function safeReadFile(filePath) {
|
|
40
41
|
try {
|
|
41
42
|
if (!fs.existsSync(filePath)) {
|
|
@@ -65,6 +66,55 @@ function scoreText(query, text) {
|
|
|
65
66
|
}
|
|
66
67
|
return score;
|
|
67
68
|
}
|
|
69
|
+
function tokenize(text) {
|
|
70
|
+
return text
|
|
71
|
+
.toLowerCase()
|
|
72
|
+
.split(/[^a-z0-9\u4e00-\u9fa5]+/i)
|
|
73
|
+
.map(token => token.trim())
|
|
74
|
+
.filter(Boolean);
|
|
75
|
+
}
|
|
76
|
+
function buildBm25Stats(docs, queryTerms, getTokens) {
|
|
77
|
+
const docFreq = new Map();
|
|
78
|
+
let totalLen = 0;
|
|
79
|
+
for (const doc of docs) {
|
|
80
|
+
const tokens = typeof getTokens === "function" ? getTokens(doc) : tokenize(doc.text);
|
|
81
|
+
totalLen += tokens.length;
|
|
82
|
+
if (queryTerms.length === 0) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const termSet = new Set(tokens);
|
|
86
|
+
for (const term of queryTerms) {
|
|
87
|
+
if (termSet.has(term)) {
|
|
88
|
+
docFreq.set(term, (docFreq.get(term) || 0) + 1);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const avgDocLen = docs.length > 0 ? Math.max(1, totalLen / docs.length) : 1;
|
|
93
|
+
return { avgDocLen, docFreq };
|
|
94
|
+
}
|
|
95
|
+
function bm25Score(args) {
|
|
96
|
+
const tokens = Array.isArray(args.docTokens) ? args.docTokens : tokenize(args.docText);
|
|
97
|
+
if (tokens.length === 0 || args.queryTerms.length === 0 || args.docCount <= 0) {
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
const termFreq = new Map();
|
|
101
|
+
for (const token of tokens) {
|
|
102
|
+
termFreq.set(token, (termFreq.get(token) || 0) + 1);
|
|
103
|
+
}
|
|
104
|
+
const k1 = 1.2;
|
|
105
|
+
const b = 0.75;
|
|
106
|
+
let score = 0;
|
|
107
|
+
for (const term of args.queryTerms) {
|
|
108
|
+
const tf = termFreq.get(term) || 0;
|
|
109
|
+
if (tf <= 0)
|
|
110
|
+
continue;
|
|
111
|
+
const df = args.docFreq.get(term) || 0;
|
|
112
|
+
const idf = Math.log(1 + ((args.docCount - df + 0.5) / (df + 0.5)));
|
|
113
|
+
const denominator = tf + k1 * (1 - b + b * (tokens.length / Math.max(1, args.avgDocLen)));
|
|
114
|
+
score += idf * (((k1 + 1) * tf) / Math.max(1e-6, denominator));
|
|
115
|
+
}
|
|
116
|
+
return score;
|
|
117
|
+
}
|
|
68
118
|
function normalizeRecordText(record) {
|
|
69
119
|
const direct = [record.content, record.summary, record.text, record.message]
|
|
70
120
|
.find(v => typeof v === "string" && v.trim());
|
|
@@ -114,11 +164,46 @@ function parseJsonlFile(filePath, sourceLabel, logger) {
|
|
|
114
164
|
}
|
|
115
165
|
const id = typeof parsed.id === "string" ? parsed.id : `${sourceLabel}:${docs.length + 1}`;
|
|
116
166
|
const timestampValue = typeof parsed.timestamp === "string" ? Date.parse(parsed.timestamp) : NaN;
|
|
167
|
+
const entities = Array.isArray(parsed.entities)
|
|
168
|
+
? parsed.entities.map(item => (typeof item === "string" ? item.trim() : "")).filter(Boolean)
|
|
169
|
+
: [];
|
|
170
|
+
const relations = Array.isArray(parsed.relations)
|
|
171
|
+
? parsed.relations
|
|
172
|
+
.map(item => {
|
|
173
|
+
if (typeof item !== "object" || item === null)
|
|
174
|
+
return null;
|
|
175
|
+
const relation = item;
|
|
176
|
+
const source = typeof relation.source === "string" ? relation.source.trim() : "";
|
|
177
|
+
const target = typeof relation.target === "string" ? relation.target.trim() : "";
|
|
178
|
+
const type = typeof relation.type === "string" ? relation.type.trim() : "related_to";
|
|
179
|
+
if (!source || !target)
|
|
180
|
+
return null;
|
|
181
|
+
return { source, target, type };
|
|
182
|
+
})
|
|
183
|
+
.filter((item) => Boolean(item))
|
|
184
|
+
: [];
|
|
117
185
|
docs.push({
|
|
118
186
|
id,
|
|
119
187
|
text,
|
|
120
188
|
source: sourceLabel,
|
|
121
189
|
timestamp: Number.isFinite(timestampValue) ? timestampValue : undefined,
|
|
190
|
+
layer: parsed.layer === "active" || parsed.layer === "archive"
|
|
191
|
+
? parsed.layer
|
|
192
|
+
: (sourceLabel === "sessions_active" ? "active" : (sourceLabel === "sessions_archive" ? "archive" : undefined)),
|
|
193
|
+
sourceMemoryId: typeof parsed.source_memory_id === "string"
|
|
194
|
+
? parsed.source_memory_id
|
|
195
|
+
: id,
|
|
196
|
+
sourceMemoryCanonicalId: typeof parsed.source_memory_canonical_id === "string"
|
|
197
|
+
? parsed.source_memory_canonical_id
|
|
198
|
+
: (typeof parsed.canonical_id === "string" ? parsed.canonical_id : undefined),
|
|
199
|
+
embedding: Array.isArray(parsed.embedding) ? parsed.embedding.filter(item => Number.isFinite(item)) : undefined,
|
|
200
|
+
eventType: typeof parsed.event_type === "string" ? parsed.event_type.trim() : undefined,
|
|
201
|
+
qualityScore: typeof parsed.quality_score === "number" ? parsed.quality_score : undefined,
|
|
202
|
+
charCount: typeof parsed.char_count === "number" ? parsed.char_count : undefined,
|
|
203
|
+
tokenCount: typeof parsed.token_count === "number" ? parsed.token_count : undefined,
|
|
204
|
+
sessionId: typeof parsed.session_id === "string" ? parsed.session_id : undefined,
|
|
205
|
+
entities,
|
|
206
|
+
relations,
|
|
122
207
|
});
|
|
123
208
|
}
|
|
124
209
|
catch (error) {
|
|
@@ -160,19 +245,647 @@ function withRecencyBoost(score, timestamp) {
|
|
|
160
245
|
}
|
|
161
246
|
return score;
|
|
162
247
|
}
|
|
248
|
+
function recencyScore(timestamp) {
|
|
249
|
+
if (!timestamp) {
|
|
250
|
+
return 0;
|
|
251
|
+
}
|
|
252
|
+
const ageHours = (Date.now() - timestamp) / (1000 * 60 * 60);
|
|
253
|
+
if (ageHours < 12)
|
|
254
|
+
return 1;
|
|
255
|
+
if (ageHours < 24)
|
|
256
|
+
return 0.8;
|
|
257
|
+
if (ageHours < 72)
|
|
258
|
+
return 0.6;
|
|
259
|
+
if (ageHours < 168)
|
|
260
|
+
return 0.4;
|
|
261
|
+
if (ageHours < 720)
|
|
262
|
+
return 0.2;
|
|
263
|
+
return 0.05;
|
|
264
|
+
}
|
|
265
|
+
function eventTypeHalfLifeDays(eventType, options) {
|
|
266
|
+
const fallback = typeof options?.defaultHalfLifeDays === "number" && options.defaultHalfLifeDays > 0
|
|
267
|
+
? options.defaultHalfLifeDays
|
|
268
|
+
: 90;
|
|
269
|
+
const type = (eventType || "").trim().toLowerCase();
|
|
270
|
+
if (!type)
|
|
271
|
+
return fallback;
|
|
272
|
+
const configured = options?.halfLifeByEventType || {};
|
|
273
|
+
if (typeof configured[type] === "number" && configured[type] > 0) {
|
|
274
|
+
return configured[type];
|
|
275
|
+
}
|
|
276
|
+
if (["issue", "fix", "action_item", "blocker"].includes(type))
|
|
277
|
+
return 30;
|
|
278
|
+
if (["plan", "milestone", "follow_up"].includes(type))
|
|
279
|
+
return 60;
|
|
280
|
+
if (["decision", "insight", "retrospective"].includes(type))
|
|
281
|
+
return 120;
|
|
282
|
+
if (["preference", "constraint", "requirement", "dependency", "assumption"].includes(type))
|
|
283
|
+
return 240;
|
|
284
|
+
return fallback;
|
|
285
|
+
}
|
|
286
|
+
function computeAntiDecayBoost(id, hitStats, options) {
|
|
287
|
+
const anti = options?.antiDecay;
|
|
288
|
+
if (anti?.enabled === false) {
|
|
289
|
+
return 1;
|
|
290
|
+
}
|
|
291
|
+
const item = hitStats.items[id];
|
|
292
|
+
if (!item) {
|
|
293
|
+
return 1;
|
|
294
|
+
}
|
|
295
|
+
const hitWeight = typeof anti?.hitWeight === "number" && anti.hitWeight > 0 ? anti.hitWeight : 0.08;
|
|
296
|
+
const maxBoost = typeof anti?.maxBoost === "number" && anti.maxBoost >= 1 ? anti.maxBoost : 1.6;
|
|
297
|
+
const recentWindowDays = typeof anti?.recentWindowDays === "number" && anti.recentWindowDays > 0 ? anti.recentWindowDays : 30;
|
|
298
|
+
const lastHitTs = Date.parse(item.lastHitAt || "");
|
|
299
|
+
const ageDays = Number.isFinite(lastHitTs) ? Math.max(0, (Date.now() - lastHitTs) / (1000 * 60 * 60 * 24)) : recentWindowDays * 2;
|
|
300
|
+
const freshness = ageDays <= recentWindowDays ? (1 - ageDays / recentWindowDays) : 0;
|
|
301
|
+
const countFactor = Math.log1p(Math.max(0, item.count));
|
|
302
|
+
const boost = 1 + countFactor * hitWeight * (0.5 + 0.5 * freshness);
|
|
303
|
+
return Math.min(maxBoost, Math.max(1, boost));
|
|
304
|
+
}
|
|
305
|
+
function computeDecayFactor(id, eventType, timestamp, options, hitStats) {
|
|
306
|
+
const enabled = options?.enabled !== false;
|
|
307
|
+
if (!enabled || !timestamp) {
|
|
308
|
+
return computeAntiDecayBoost(id, hitStats, options);
|
|
309
|
+
}
|
|
310
|
+
const ageDays = Math.max(0, (Date.now() - timestamp) / (1000 * 60 * 60 * 24));
|
|
311
|
+
const halfLife = eventTypeHalfLifeDays(eventType, options);
|
|
312
|
+
const base = Math.pow(2, -ageDays / Math.max(1, halfLife));
|
|
313
|
+
const floor = typeof options?.minFloor === "number"
|
|
314
|
+
? Math.max(0, Math.min(1, options.minFloor))
|
|
315
|
+
: 0.15;
|
|
316
|
+
const decay = Math.max(floor, base);
|
|
317
|
+
const boost = computeAntiDecayBoost(id, hitStats, options);
|
|
318
|
+
return Math.min(1, decay * boost);
|
|
319
|
+
}
|
|
320
|
+
function normalizeBaseUrl(value) {
|
|
321
|
+
if (!value)
|
|
322
|
+
return "";
|
|
323
|
+
return value.endsWith("/") ? value.slice(0, -1) : value;
|
|
324
|
+
}
|
|
325
|
+
const READ_FUSION_PROMPT_VERSION = "read-fusion.v1.1.0";
|
|
326
|
+
const READ_FUSION_REGRESSION_SAMPLES = [
|
|
327
|
+
"样例A: 同一 source_memory_id 同时出现在 archive 与 vector,输出中只保留一条主事实并在证据链保留两者关联。",
|
|
328
|
+
"样例B: 新旧决策冲突时,将冲突写入 conflicts,并在 canonical_answer 标注优先级依据(时间、质量、明确性)。",
|
|
329
|
+
];
|
|
330
|
+
function cosineSimilarity(left, right) {
|
|
331
|
+
if (left.length === 0 || right.length === 0) {
|
|
332
|
+
return 0;
|
|
333
|
+
}
|
|
334
|
+
const size = Math.min(left.length, right.length);
|
|
335
|
+
let dot = 0;
|
|
336
|
+
let leftNorm = 0;
|
|
337
|
+
let rightNorm = 0;
|
|
338
|
+
for (let i = 0; i < size; i += 1) {
|
|
339
|
+
const a = left[i];
|
|
340
|
+
const b = right[i];
|
|
341
|
+
dot += a * b;
|
|
342
|
+
leftNorm += a * a;
|
|
343
|
+
rightNorm += b * b;
|
|
344
|
+
}
|
|
345
|
+
if (leftNorm === 0 || rightNorm === 0) {
|
|
346
|
+
return 0;
|
|
347
|
+
}
|
|
348
|
+
return dot / (Math.sqrt(leftNorm) * Math.sqrt(rightNorm));
|
|
349
|
+
}
|
|
350
|
+
async function requestEmbedding(args) {
|
|
351
|
+
const endpoint = args.baseUrl.endsWith("/embeddings") ? args.baseUrl : `${args.baseUrl}/embeddings`;
|
|
352
|
+
const body = {
|
|
353
|
+
input: args.text,
|
|
354
|
+
model: args.model,
|
|
355
|
+
};
|
|
356
|
+
if (typeof args.dimensions === "number" && Number.isFinite(args.dimensions) && args.dimensions > 0) {
|
|
357
|
+
body.dimensions = args.dimensions;
|
|
358
|
+
}
|
|
359
|
+
let lastError = null;
|
|
360
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
361
|
+
const controller = new AbortController();
|
|
362
|
+
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
|
363
|
+
try {
|
|
364
|
+
const response = await fetch(endpoint, {
|
|
365
|
+
method: "POST",
|
|
366
|
+
headers: {
|
|
367
|
+
"content-type": "application/json",
|
|
368
|
+
authorization: `Bearer ${args.apiKey}`,
|
|
369
|
+
},
|
|
370
|
+
body: JSON.stringify(body),
|
|
371
|
+
signal: controller.signal,
|
|
372
|
+
});
|
|
373
|
+
clearTimeout(timeoutId);
|
|
374
|
+
if (!response.ok) {
|
|
375
|
+
lastError = new Error(`embedding_http_${response.status}`);
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
const json = await response.json();
|
|
379
|
+
const embedding = json?.data?.[0]?.embedding;
|
|
380
|
+
if (Array.isArray(embedding) && embedding.length > 0) {
|
|
381
|
+
return embedding.filter(item => Number.isFinite(item));
|
|
382
|
+
}
|
|
383
|
+
lastError = new Error("embedding_empty");
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
clearTimeout(timeoutId);
|
|
387
|
+
lastError = error;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (lastError) {
|
|
391
|
+
throw lastError;
|
|
392
|
+
}
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
async function requestRerank(args) {
|
|
396
|
+
const endpoint = args.baseUrl.endsWith("/rerank") ? args.baseUrl : `${args.baseUrl}/rerank`;
|
|
397
|
+
const documents = args.candidates.map(item => item.text);
|
|
398
|
+
const body = {
|
|
399
|
+
model: args.model,
|
|
400
|
+
query: args.query,
|
|
401
|
+
documents,
|
|
402
|
+
top_n: args.candidates.length,
|
|
403
|
+
};
|
|
404
|
+
let lastError = null;
|
|
405
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
406
|
+
const controller = new AbortController();
|
|
407
|
+
const timeoutId = setTimeout(() => controller.abort(), 12000);
|
|
408
|
+
try {
|
|
409
|
+
const response = await fetch(endpoint, {
|
|
410
|
+
method: "POST",
|
|
411
|
+
headers: {
|
|
412
|
+
"content-type": "application/json",
|
|
413
|
+
authorization: `Bearer ${args.apiKey}`,
|
|
414
|
+
},
|
|
415
|
+
body: JSON.stringify(body),
|
|
416
|
+
signal: controller.signal,
|
|
417
|
+
});
|
|
418
|
+
clearTimeout(timeoutId);
|
|
419
|
+
if (!response.ok) {
|
|
420
|
+
lastError = new Error(`rerank_http_${response.status}`);
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
const json = await response.json();
|
|
424
|
+
const list = Array.isArray(json.results) ? json.results : (Array.isArray(json.data) ? json.data : []);
|
|
425
|
+
if (!Array.isArray(list) || list.length === 0) {
|
|
426
|
+
lastError = new Error("rerank_empty");
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
const mapped = list
|
|
430
|
+
.map((item, rank) => {
|
|
431
|
+
const index = typeof item.index === "number" ? item.index : rank;
|
|
432
|
+
const hit = args.candidates[index];
|
|
433
|
+
if (!hit)
|
|
434
|
+
return null;
|
|
435
|
+
const score = typeof item.relevance_score === "number" ? item.relevance_score : (typeof item.score === "number" ? item.score : hit.score);
|
|
436
|
+
return { ...hit, score };
|
|
437
|
+
})
|
|
438
|
+
.filter((item) => Boolean(item));
|
|
439
|
+
if (mapped.length > 0) {
|
|
440
|
+
return mapped;
|
|
441
|
+
}
|
|
442
|
+
lastError = new Error("rerank_map_empty");
|
|
443
|
+
}
|
|
444
|
+
catch (error) {
|
|
445
|
+
clearTimeout(timeoutId);
|
|
446
|
+
lastError = error;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError || "rerank_failed"));
|
|
450
|
+
}
|
|
451
|
+
function classifyIntent(query) {
|
|
452
|
+
const text = query.toLowerCase();
|
|
453
|
+
const relationHints = /(关系|依赖|关联|上下游|graph|relation|entity|拓扑)/i;
|
|
454
|
+
if (relationHints.test(text))
|
|
455
|
+
return "RELATION_DISCOVERY";
|
|
456
|
+
const troubleHints = /(报错|错误|异常|失败|超时|无法|崩溃|error|failed|timeout|fix)/i;
|
|
457
|
+
if (troubleHints.test(text))
|
|
458
|
+
return "TROUBLESHOOTING";
|
|
459
|
+
const preferenceHints = /(偏好|习惯|口味|喜欢|不喜欢|偏向|preference)/i;
|
|
460
|
+
if (preferenceHints.test(text))
|
|
461
|
+
return "PREFERENCE_PROFILE";
|
|
462
|
+
const timelineHints = /(最近|上次|之前|时间线|timeline|history)/i;
|
|
463
|
+
if (timelineHints.test(text))
|
|
464
|
+
return "TIMELINE_REVIEW";
|
|
465
|
+
const decisionHints = /(方案|决策|选择|建议|取舍|tradeoff|plan)/i;
|
|
466
|
+
if (decisionHints.test(text))
|
|
467
|
+
return "DECISION_SUPPORT";
|
|
468
|
+
return "FACT_LOOKUP";
|
|
469
|
+
}
|
|
470
|
+
function preferredEventTypes(intent) {
|
|
471
|
+
if (intent === "TROUBLESHOOTING")
|
|
472
|
+
return ["issue", "fix", "risk", "blocker", "dependency", "retrospective"];
|
|
473
|
+
if (intent === "PREFERENCE_PROFILE")
|
|
474
|
+
return ["preference", "decision", "constraint", "requirement"];
|
|
475
|
+
if (intent === "DECISION_SUPPORT")
|
|
476
|
+
return ["decision", "plan", "insight", "assumption", "constraint", "requirement"];
|
|
477
|
+
if (intent === "TIMELINE_REVIEW")
|
|
478
|
+
return ["action_item", "follow_up", "milestone", "plan", "decision", "issue", "fix"];
|
|
479
|
+
return [];
|
|
480
|
+
}
|
|
481
|
+
function sourceWeight(source, intent) {
|
|
482
|
+
if (source === "rules") {
|
|
483
|
+
return intent === "DECISION_SUPPORT" || intent === "TROUBLESHOOTING" ? 1.15 : 0.9;
|
|
484
|
+
}
|
|
485
|
+
if (source === "graph") {
|
|
486
|
+
return intent === "RELATION_DISCOVERY" ? 1.25 : 0.85;
|
|
487
|
+
}
|
|
488
|
+
if (source === "vector") {
|
|
489
|
+
return 1.05;
|
|
490
|
+
}
|
|
491
|
+
return 1;
|
|
492
|
+
}
|
|
493
|
+
function mergeKeyFromDoc(doc) {
|
|
494
|
+
const canonical = typeof doc.sourceMemoryCanonicalId === "string" ? doc.sourceMemoryCanonicalId.trim() : "";
|
|
495
|
+
if (canonical) {
|
|
496
|
+
return `canonical:${canonical}`;
|
|
497
|
+
}
|
|
498
|
+
const sourceMemoryId = typeof doc.sourceMemoryId === "string" ? doc.sourceMemoryId.trim() : "";
|
|
499
|
+
if (sourceMemoryId) {
|
|
500
|
+
return `source:${sourceMemoryId}`;
|
|
501
|
+
}
|
|
502
|
+
return `id:${doc.id}`;
|
|
503
|
+
}
|
|
504
|
+
function customChannelWeight(source, options) {
|
|
505
|
+
const weights = options?.channelWeights;
|
|
506
|
+
if (!weights)
|
|
507
|
+
return 1;
|
|
508
|
+
const value = weights[source];
|
|
509
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
510
|
+
return 1;
|
|
511
|
+
}
|
|
512
|
+
return value;
|
|
513
|
+
}
|
|
514
|
+
function lengthNormalizeFactor(doc, options) {
|
|
515
|
+
const lengthNorm = options?.lengthNorm;
|
|
516
|
+
if (lengthNorm?.enabled === false) {
|
|
517
|
+
return 1;
|
|
518
|
+
}
|
|
519
|
+
const pivotChars = typeof lengthNorm?.pivotChars === "number" && lengthNorm.pivotChars > 0
|
|
520
|
+
? lengthNorm.pivotChars
|
|
521
|
+
: 1200;
|
|
522
|
+
const strength = typeof lengthNorm?.strength === "number" && lengthNorm.strength > 0
|
|
523
|
+
? lengthNorm.strength
|
|
524
|
+
: 0.75;
|
|
525
|
+
const minFactor = typeof lengthNorm?.minFactor === "number" && lengthNorm.minFactor > 0 && lengthNorm.minFactor <= 1
|
|
526
|
+
? lengthNorm.minFactor
|
|
527
|
+
: 0.45;
|
|
528
|
+
const charCount = typeof doc.charCount === "number" && Number.isFinite(doc.charCount)
|
|
529
|
+
? doc.charCount
|
|
530
|
+
: doc.text.length;
|
|
531
|
+
if (charCount <= pivotChars) {
|
|
532
|
+
return 1;
|
|
533
|
+
}
|
|
534
|
+
const over = (charCount - pivotChars) / pivotChars;
|
|
535
|
+
const factor = 1 / (1 + over * strength);
|
|
536
|
+
return Math.max(minFactor, Math.min(1, factor));
|
|
537
|
+
}
|
|
538
|
+
function channelQuota(source, topK, options) {
|
|
539
|
+
const configured = options?.channelTopK?.[source];
|
|
540
|
+
if (typeof configured === "number" && Number.isFinite(configured) && configured >= 1) {
|
|
541
|
+
return Math.floor(configured);
|
|
542
|
+
}
|
|
543
|
+
if (source === "rules")
|
|
544
|
+
return Math.max(6, topK * 2);
|
|
545
|
+
if (source === "graph")
|
|
546
|
+
return Math.max(8, topK * 3);
|
|
547
|
+
return Math.max(12, topK * 4);
|
|
548
|
+
}
|
|
549
|
+
async function searchLanceDb(args) {
|
|
550
|
+
try {
|
|
551
|
+
const require = (0, module_1.createRequire)(__filename);
|
|
552
|
+
const lancedbDir = path.join(args.memoryRoot, "vector", "lancedb");
|
|
553
|
+
if (!fs.existsSync(lancedbDir)) {
|
|
554
|
+
return [];
|
|
555
|
+
}
|
|
556
|
+
const moduleValue = require("@lancedb/lancedb");
|
|
557
|
+
const connect = moduleValue.connect;
|
|
558
|
+
if (typeof connect !== "function") {
|
|
559
|
+
return [];
|
|
560
|
+
}
|
|
561
|
+
const db = await connect(lancedbDir);
|
|
562
|
+
if (!db || typeof db.openTable !== "function") {
|
|
563
|
+
return [];
|
|
564
|
+
}
|
|
565
|
+
const table = await db.openTable("events");
|
|
566
|
+
if (!table || typeof table.search !== "function") {
|
|
567
|
+
return [];
|
|
568
|
+
}
|
|
569
|
+
const searchObj = table.search(args.queryEmbedding);
|
|
570
|
+
if (!searchObj || typeof searchObj.limit !== "function") {
|
|
571
|
+
return [];
|
|
572
|
+
}
|
|
573
|
+
const limited = searchObj.limit(args.limit);
|
|
574
|
+
if (!limited || typeof limited.toArray !== "function") {
|
|
575
|
+
return [];
|
|
576
|
+
}
|
|
577
|
+
const rows = await limited.toArray();
|
|
578
|
+
const docs = [];
|
|
579
|
+
for (const row of rows) {
|
|
580
|
+
if (typeof row !== "object" || row === null)
|
|
581
|
+
continue;
|
|
582
|
+
const record = row;
|
|
583
|
+
const id = typeof record.id === "string" ? record.id : "";
|
|
584
|
+
const summary = typeof record.summary === "string" ? record.summary : "";
|
|
585
|
+
if (!id || !summary)
|
|
586
|
+
continue;
|
|
587
|
+
const ts = typeof record.timestamp === "string" ? Date.parse(record.timestamp) : NaN;
|
|
588
|
+
const entities = typeof record.entities_json === "string"
|
|
589
|
+
? JSON.parse(record.entities_json).filter(item => typeof item === "string" && item.trim())
|
|
590
|
+
: [];
|
|
591
|
+
const relations = typeof record.relations_json === "string"
|
|
592
|
+
? JSON.parse(record.relations_json)
|
|
593
|
+
: [];
|
|
594
|
+
docs.push({
|
|
595
|
+
id,
|
|
596
|
+
text: summary,
|
|
597
|
+
source: "vector_lancedb",
|
|
598
|
+
timestamp: Number.isFinite(ts) ? ts : undefined,
|
|
599
|
+
layer: record.layer === "active" || record.layer === "archive" ? record.layer : undefined,
|
|
600
|
+
sourceMemoryId: typeof record.source_memory_id === "string" ? record.source_memory_id : undefined,
|
|
601
|
+
sourceMemoryCanonicalId: typeof record.source_memory_canonical_id === "string" ? record.source_memory_canonical_id : undefined,
|
|
602
|
+
embedding: Array.isArray(record.vector) ? record.vector.filter(item => Number.isFinite(item)) : undefined,
|
|
603
|
+
eventType: typeof record.event_type === "string" ? record.event_type : undefined,
|
|
604
|
+
qualityScore: typeof record.quality_score === "number" ? record.quality_score : undefined,
|
|
605
|
+
charCount: typeof record.char_count === "number" ? record.char_count : undefined,
|
|
606
|
+
tokenCount: typeof record.token_count === "number" ? record.token_count : undefined,
|
|
607
|
+
sessionId: typeof record.session_id === "string" ? record.session_id : undefined,
|
|
608
|
+
entities,
|
|
609
|
+
relations: Array.isArray(relations) ? relations : [],
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
return docs;
|
|
613
|
+
}
|
|
614
|
+
catch (error) {
|
|
615
|
+
args.logger.debug(`LanceDB search fallback: ${error}`);
|
|
616
|
+
return [];
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
function parseVectorFallback(filePath, logger) {
|
|
620
|
+
const content = safeReadFile(filePath);
|
|
621
|
+
if (!content) {
|
|
622
|
+
return [];
|
|
623
|
+
}
|
|
624
|
+
const docs = [];
|
|
625
|
+
for (const line of content.split(/\r?\n/)) {
|
|
626
|
+
const trimmed = line.trim();
|
|
627
|
+
if (!trimmed)
|
|
628
|
+
continue;
|
|
629
|
+
try {
|
|
630
|
+
const parsed = JSON.parse(trimmed);
|
|
631
|
+
const id = typeof parsed.id === "string" ? parsed.id : "";
|
|
632
|
+
const summary = typeof parsed.summary === "string" ? parsed.summary.trim() : "";
|
|
633
|
+
if (!id || !summary)
|
|
634
|
+
continue;
|
|
635
|
+
const ts = typeof parsed.timestamp === "string" ? Date.parse(parsed.timestamp) : NaN;
|
|
636
|
+
const entities = Array.isArray(parsed.entities)
|
|
637
|
+
? parsed.entities.map(item => (typeof item === "string" ? item.trim() : "")).filter(Boolean)
|
|
638
|
+
: [];
|
|
639
|
+
const relations = Array.isArray(parsed.relations)
|
|
640
|
+
? parsed.relations
|
|
641
|
+
.map(item => {
|
|
642
|
+
if (typeof item !== "object" || item === null)
|
|
643
|
+
return null;
|
|
644
|
+
const relation = item;
|
|
645
|
+
const source = typeof relation.source === "string" ? relation.source.trim() : "";
|
|
646
|
+
const target = typeof relation.target === "string" ? relation.target.trim() : "";
|
|
647
|
+
const type = typeof relation.type === "string" ? relation.type.trim() : "related_to";
|
|
648
|
+
if (!source || !target)
|
|
649
|
+
return null;
|
|
650
|
+
return { source, target, type };
|
|
651
|
+
})
|
|
652
|
+
.filter((item) => Boolean(item))
|
|
653
|
+
: [];
|
|
654
|
+
docs.push({
|
|
655
|
+
id,
|
|
656
|
+
text: summary,
|
|
657
|
+
source: "vector_jsonl",
|
|
658
|
+
timestamp: Number.isFinite(ts) ? ts : undefined,
|
|
659
|
+
layer: parsed.layer === "active" || parsed.layer === "archive" ? parsed.layer : undefined,
|
|
660
|
+
sourceMemoryId: typeof parsed.source_memory_id === "string" ? parsed.source_memory_id : undefined,
|
|
661
|
+
sourceMemoryCanonicalId: typeof parsed.source_memory_canonical_id === "string" ? parsed.source_memory_canonical_id : undefined,
|
|
662
|
+
embedding: Array.isArray(parsed.embedding) ? parsed.embedding.filter(item => Number.isFinite(item)) : undefined,
|
|
663
|
+
eventType: typeof parsed.event_type === "string" ? parsed.event_type.trim() : undefined,
|
|
664
|
+
qualityScore: typeof parsed.quality_score === "number" ? parsed.quality_score : undefined,
|
|
665
|
+
charCount: typeof parsed.char_count === "number" ? parsed.char_count : undefined,
|
|
666
|
+
tokenCount: typeof parsed.token_count === "number" ? parsed.token_count : undefined,
|
|
667
|
+
sessionId: typeof parsed.session_id === "string" ? parsed.session_id : undefined,
|
|
668
|
+
entities,
|
|
669
|
+
relations,
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
catch (error) {
|
|
673
|
+
logger.debug(`Skip invalid vector jsonl line: ${error}`);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
return docs;
|
|
677
|
+
}
|
|
678
|
+
async function requestFusion(args) {
|
|
679
|
+
const endpoint = args.llm.baseUrl.endsWith("/chat/completions")
|
|
680
|
+
? args.llm.baseUrl
|
|
681
|
+
: `${args.llm.baseUrl}/chat/completions`;
|
|
682
|
+
const evidenceText = args.candidates
|
|
683
|
+
.map((item, index) => `${index + 1}. [${item.id}] (${item.source}, score=${item.score.toFixed(4)}) ${item.text}`)
|
|
684
|
+
.join("\n")
|
|
685
|
+
.slice(0, 18000);
|
|
686
|
+
const prompt = [
|
|
687
|
+
`prompt_version=${READ_FUSION_PROMPT_VERSION}`,
|
|
688
|
+
"你是记忆检索融合器。请融合多路召回结果,产出可直接给 Agent 使用的完整记忆包,不要让 Agent 再去翻历史。",
|
|
689
|
+
"必须严格返回 JSON:",
|
|
690
|
+
"{\"canonical_answer\": string, \"coverage_note\": string, \"facts\": [{\"text\": string, \"evidence_ids\": string[]}], \"timeline\": [{\"when\": string, \"event\": string, \"evidence_ids\": string[]}], \"entities\": [{\"name\": string, \"role\": string}], \"decisions\": [{\"decision\": string, \"rationale\": string, \"evidence_ids\": string[]}], \"fixes\": [{\"issue\": string, \"fix\": string, \"evidence_ids\": string[]}], \"preferences\": [{\"subject\": string, \"preference\": string, \"evidence_ids\": string[]}], \"risks\": [{\"risk\": string, \"mitigation\": string, \"evidence_ids\": string[]}], \"action_items\": [{\"item\": string, \"owner\": string, \"status\": string, \"evidence_ids\": string[]}], \"conflicts\": [{\"topic\": string, \"details\": string}], \"evidence_ids\": string[], \"confidence\": number}",
|
|
691
|
+
"要求:",
|
|
692
|
+
"1) canonical_answer 是完整可执行答案,不要只写摘要",
|
|
693
|
+
"2) facts 3-12 条,优先高分证据",
|
|
694
|
+
"3) evidence_ids 必须来自输入候选 id",
|
|
695
|
+
"4) 若存在冲突写入 conflicts,否则返回空数组",
|
|
696
|
+
"5) confidence 0~1",
|
|
697
|
+
"6) 不确定信息必须在 coverage_note 标注",
|
|
698
|
+
"7) 同源记录合并:同 source_memory_id/source_memory_canonical_id 的候选只保留一条主结论,其余作为证据补充",
|
|
699
|
+
...READ_FUSION_REGRESSION_SAMPLES,
|
|
700
|
+
].join("\n");
|
|
701
|
+
const body = {
|
|
702
|
+
model: args.llm.model,
|
|
703
|
+
temperature: 0.1,
|
|
704
|
+
messages: [
|
|
705
|
+
{ role: "system", content: "你只输出 JSON,不要额外解释。" },
|
|
706
|
+
{ role: "user", content: `${prompt}\n\n问题:\n${args.query}\n\n候选证据:\n${evidenceText}` },
|
|
707
|
+
],
|
|
708
|
+
};
|
|
709
|
+
let lastError = null;
|
|
710
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
711
|
+
const controller = new AbortController();
|
|
712
|
+
const timeoutId = setTimeout(() => controller.abort(), 20000);
|
|
713
|
+
try {
|
|
714
|
+
const response = await fetch(endpoint, {
|
|
715
|
+
method: "POST",
|
|
716
|
+
headers: {
|
|
717
|
+
"content-type": "application/json",
|
|
718
|
+
authorization: `Bearer ${args.llm.apiKey}`,
|
|
719
|
+
},
|
|
720
|
+
body: JSON.stringify(body),
|
|
721
|
+
signal: controller.signal,
|
|
722
|
+
});
|
|
723
|
+
clearTimeout(timeoutId);
|
|
724
|
+
if (!response.ok) {
|
|
725
|
+
lastError = new Error(`fusion_http_${response.status}`);
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
const json = await response.json();
|
|
729
|
+
const content = json?.choices?.[0]?.message?.content?.trim() || "";
|
|
730
|
+
if (!content) {
|
|
731
|
+
lastError = new Error("fusion_empty");
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
const parsed = JSON.parse(content);
|
|
735
|
+
if (!parsed || typeof parsed.canonical_answer !== "string" || !parsed.canonical_answer.trim()) {
|
|
736
|
+
lastError = new Error("fusion_invalid");
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
const evidenceIds = Array.isArray(parsed.evidence_ids)
|
|
740
|
+
? parsed.evidence_ids.filter(item => typeof item === "string" && item.trim())
|
|
741
|
+
: [];
|
|
742
|
+
return {
|
|
743
|
+
canonical_answer: parsed.canonical_answer.trim().slice(0, 6000),
|
|
744
|
+
coverage_note: typeof parsed.coverage_note === "string" ? parsed.coverage_note.trim().slice(0, 1200) : "",
|
|
745
|
+
facts: Array.isArray(parsed.facts) ? parsed.facts : [],
|
|
746
|
+
timeline: Array.isArray(parsed.timeline) ? parsed.timeline : [],
|
|
747
|
+
entities: Array.isArray(parsed.entities) ? parsed.entities : [],
|
|
748
|
+
decisions: Array.isArray(parsed.decisions) ? parsed.decisions : [],
|
|
749
|
+
fixes: Array.isArray(parsed.fixes) ? parsed.fixes : [],
|
|
750
|
+
preferences: Array.isArray(parsed.preferences) ? parsed.preferences : [],
|
|
751
|
+
risks: Array.isArray(parsed.risks) ? parsed.risks : [],
|
|
752
|
+
action_items: Array.isArray(parsed.action_items) ? parsed.action_items : [],
|
|
753
|
+
conflicts: Array.isArray(parsed.conflicts) ? parsed.conflicts : [],
|
|
754
|
+
evidence_ids: evidenceIds,
|
|
755
|
+
confidence: typeof parsed.confidence === "number"
|
|
756
|
+
? Math.max(0, Math.min(1, parsed.confidence))
|
|
757
|
+
: 0.5,
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
catch (error) {
|
|
761
|
+
clearTimeout(timeoutId);
|
|
762
|
+
lastError = error;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError || "fusion_failed"));
|
|
766
|
+
}
|
|
163
767
|
function createReadStore(options) {
|
|
164
768
|
const memoryRoot = options.dbPath ? path.resolve(options.dbPath) : path.join(options.projectRoot, "data", "memory");
|
|
769
|
+
const vectorFallbackPath = path.join(memoryRoot, "vector", "lancedb_events.jsonl");
|
|
770
|
+
const hitStatsPath = path.join(memoryRoot, ".read_hit_stats.json");
|
|
771
|
+
let docsCache = null;
|
|
772
|
+
let vectorFallbackCache = null;
|
|
773
|
+
let bm25TokenCacheSignature = "";
|
|
774
|
+
let bm25TokenCache = new Map();
|
|
775
|
+
function fileSignature(filePath) {
|
|
776
|
+
try {
|
|
777
|
+
if (!fs.existsSync(filePath)) {
|
|
778
|
+
return `${filePath}:missing`;
|
|
779
|
+
}
|
|
780
|
+
const stat = fs.statSync(filePath);
|
|
781
|
+
return `${filePath}:${stat.size}:${Math.floor(stat.mtimeMs)}`;
|
|
782
|
+
}
|
|
783
|
+
catch {
|
|
784
|
+
return `${filePath}:error`;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
function loadHitStats() {
|
|
788
|
+
try {
|
|
789
|
+
if (!fs.existsSync(hitStatsPath)) {
|
|
790
|
+
return { items: {} };
|
|
791
|
+
}
|
|
792
|
+
const content = fs.readFileSync(hitStatsPath, "utf-8").trim();
|
|
793
|
+
if (!content) {
|
|
794
|
+
return { items: {} };
|
|
795
|
+
}
|
|
796
|
+
const parsed = JSON.parse(content);
|
|
797
|
+
if (!parsed || typeof parsed !== "object" || !parsed.items || typeof parsed.items !== "object") {
|
|
798
|
+
return { items: {} };
|
|
799
|
+
}
|
|
800
|
+
return parsed;
|
|
801
|
+
}
|
|
802
|
+
catch {
|
|
803
|
+
return { items: {} };
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
function saveHitStats(state) {
|
|
807
|
+
try {
|
|
808
|
+
const dir = path.dirname(hitStatsPath);
|
|
809
|
+
if (!fs.existsSync(dir)) {
|
|
810
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
811
|
+
}
|
|
812
|
+
fs.writeFileSync(hitStatsPath, JSON.stringify(state, null, 2), "utf-8");
|
|
813
|
+
}
|
|
814
|
+
catch (error) {
|
|
815
|
+
options.logger.warn(`Failed to persist read hit stats: ${error}`);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
function markHit(ids) {
|
|
819
|
+
if (!ids.length) {
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
const state = loadHitStats();
|
|
823
|
+
const now = new Date().toISOString();
|
|
824
|
+
for (const id of ids) {
|
|
825
|
+
const key = (id || "").trim();
|
|
826
|
+
if (!key)
|
|
827
|
+
continue;
|
|
828
|
+
const prev = state.items[key];
|
|
829
|
+
state.items[key] = {
|
|
830
|
+
count: (prev?.count || 0) + 1,
|
|
831
|
+
lastHitAt: now,
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
const entries = Object.entries(state.items)
|
|
835
|
+
.sort((a, b) => {
|
|
836
|
+
const ta = Date.parse(a[1].lastHitAt || "");
|
|
837
|
+
const tb = Date.parse(b[1].lastHitAt || "");
|
|
838
|
+
return (Number.isFinite(tb) ? tb : 0) - (Number.isFinite(ta) ? ta : 0);
|
|
839
|
+
})
|
|
840
|
+
.slice(0, 20000);
|
|
841
|
+
state.items = Object.fromEntries(entries);
|
|
842
|
+
saveHitStats(state);
|
|
843
|
+
}
|
|
165
844
|
function loadAllDocuments() {
|
|
166
845
|
const cortexRulesPath = path.join(memoryRoot, "CORTEX_RULES.md");
|
|
167
846
|
const memoryMdPath = path.join(memoryRoot, "MEMORY.md");
|
|
168
847
|
const activeSessionsPath = path.join(memoryRoot, "sessions", "active", "sessions.jsonl");
|
|
169
848
|
const archiveSessionsPath = path.join(memoryRoot, "sessions", "archive", "sessions.jsonl");
|
|
170
|
-
|
|
849
|
+
const signature = [
|
|
850
|
+
fileSignature(cortexRulesPath),
|
|
851
|
+
fileSignature(memoryMdPath),
|
|
852
|
+
fileSignature(activeSessionsPath),
|
|
853
|
+
fileSignature(archiveSessionsPath),
|
|
854
|
+
].join("|");
|
|
855
|
+
if (docsCache && docsCache.signature === signature) {
|
|
856
|
+
return docsCache.docs;
|
|
857
|
+
}
|
|
858
|
+
const docs = [
|
|
171
859
|
...parseMarkdownFile(cortexRulesPath, "CORTEX_RULES.md"),
|
|
172
860
|
...parseMarkdownFile(memoryMdPath, "MEMORY.md"),
|
|
173
861
|
...parseJsonlFile(activeSessionsPath, "sessions_active", options.logger),
|
|
174
862
|
...parseJsonlFile(archiveSessionsPath, "sessions_archive", options.logger),
|
|
175
863
|
];
|
|
864
|
+
docsCache = { signature, docs };
|
|
865
|
+
return docs;
|
|
866
|
+
}
|
|
867
|
+
function loadVectorFallbackCached() {
|
|
868
|
+
const signature = fileSignature(vectorFallbackPath);
|
|
869
|
+
if (vectorFallbackCache && vectorFallbackCache.signature === signature) {
|
|
870
|
+
return vectorFallbackCache.docs;
|
|
871
|
+
}
|
|
872
|
+
const docs = parseVectorFallback(vectorFallbackPath, options.logger);
|
|
873
|
+
vectorFallbackCache = { signature, docs };
|
|
874
|
+
return docs;
|
|
875
|
+
}
|
|
876
|
+
function getBm25Tokens(doc, signature) {
|
|
877
|
+
if (bm25TokenCacheSignature !== signature) {
|
|
878
|
+
bm25TokenCacheSignature = signature;
|
|
879
|
+
bm25TokenCache = new Map();
|
|
880
|
+
}
|
|
881
|
+
const key = `${doc.source}|${doc.id}|${doc.text.length}|${doc.text.slice(0, 64)}`;
|
|
882
|
+
const cached = bm25TokenCache.get(key);
|
|
883
|
+
if (cached) {
|
|
884
|
+
return cached;
|
|
885
|
+
}
|
|
886
|
+
const tokens = tokenize(doc.text);
|
|
887
|
+
bm25TokenCache.set(key, tokens);
|
|
888
|
+
return tokens;
|
|
176
889
|
}
|
|
177
890
|
async function searchMemory(args) {
|
|
178
891
|
const query = args.query?.trim();
|
|
@@ -180,36 +893,406 @@ function createReadStore(options) {
|
|
|
180
893
|
return { results: [] };
|
|
181
894
|
}
|
|
182
895
|
const docs = loadAllDocuments();
|
|
183
|
-
const
|
|
896
|
+
const hitStats = loadHitStats();
|
|
897
|
+
const intent = classifyIntent(query);
|
|
898
|
+
const preferredTypes = preferredEventTypes(intent);
|
|
899
|
+
let queryEmbedding = null;
|
|
900
|
+
const embeddingModel = options.embedding?.model || "";
|
|
901
|
+
const embeddingApiKey = options.embedding?.apiKey || "";
|
|
902
|
+
const embeddingBaseUrl = normalizeBaseUrl(options.embedding?.baseURL || options.embedding?.baseUrl);
|
|
903
|
+
if (embeddingModel && embeddingApiKey && embeddingBaseUrl) {
|
|
904
|
+
try {
|
|
905
|
+
queryEmbedding = await requestEmbedding({
|
|
906
|
+
text: query,
|
|
907
|
+
model: embeddingModel,
|
|
908
|
+
apiKey: embeddingApiKey,
|
|
909
|
+
baseUrl: embeddingBaseUrl,
|
|
910
|
+
dimensions: options.embedding?.dimensions,
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
catch (error) {
|
|
914
|
+
options.logger.warn(`Embedding query failed, fallback to lexical search: ${error}`);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
const vectorDocsFromLance = queryEmbedding && queryEmbedding.length > 0
|
|
918
|
+
? await searchLanceDb({ memoryRoot, queryEmbedding, limit: Math.max(20, args.topK * 8), logger: options.logger })
|
|
919
|
+
: [];
|
|
920
|
+
const vectorDocsFallback = vectorDocsFromLance.length > 0
|
|
921
|
+
? []
|
|
922
|
+
: loadVectorFallbackCached();
|
|
923
|
+
const vectorDocs = [...vectorDocsFromLance, ...vectorDocsFallback];
|
|
924
|
+
const graphDocs = docs
|
|
925
|
+
.filter(doc => Array.isArray(doc.relations) && doc.relations.length > 0)
|
|
184
926
|
.map(doc => {
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
927
|
+
const graphText = [
|
|
928
|
+
doc.text,
|
|
929
|
+
...(doc.relations || []).map(relation => `${relation.source} ${relation.type} ${relation.target}`),
|
|
930
|
+
].join(" | ");
|
|
931
|
+
return {
|
|
932
|
+
...doc,
|
|
933
|
+
text: graphText,
|
|
934
|
+
};
|
|
935
|
+
});
|
|
936
|
+
const rulesDocs = docs.filter(doc => doc.source === "CORTEX_RULES.md");
|
|
937
|
+
const archiveDocs = docs.filter(doc => doc.source.startsWith("sessions_"));
|
|
938
|
+
const bm25Terms = tokenize(query);
|
|
939
|
+
const bm25Corpus = [...rulesDocs, ...archiveDocs, ...vectorDocs, ...graphDocs];
|
|
940
|
+
const bm25Signature = `${docsCache?.signature || "na"}|vector:${vectorDocs.length}:${vectorDocs.slice(0, 40).map(item => `${item.id}:${item.text.length}`).join(",")}`;
|
|
941
|
+
const bm25Stats = buildBm25Stats(bm25Corpus, bm25Terms, doc => getBm25Tokens(doc, bm25Signature));
|
|
942
|
+
const combinedCandidates = [];
|
|
943
|
+
const channels = {
|
|
944
|
+
rules: [],
|
|
945
|
+
archive: [],
|
|
946
|
+
vector: [],
|
|
947
|
+
graph: [],
|
|
948
|
+
};
|
|
949
|
+
const evaluateDoc = (doc, source) => {
|
|
950
|
+
const lexical = scoreText(query, doc.text);
|
|
951
|
+
const bm25 = bm25Score({
|
|
952
|
+
queryTerms: bm25Terms,
|
|
953
|
+
docText: doc.text,
|
|
954
|
+
docTokens: getBm25Tokens(doc, bm25Signature),
|
|
955
|
+
docCount: bm25Corpus.length,
|
|
956
|
+
avgDocLen: bm25Stats.avgDocLen,
|
|
957
|
+
docFreq: bm25Stats.docFreq,
|
|
958
|
+
});
|
|
959
|
+
const lexicalCombined = lexical + bm25 * 2;
|
|
960
|
+
const semantic = queryEmbedding && Array.isArray(doc.embedding) && doc.embedding.length > 0
|
|
961
|
+
? Math.max(0, cosineSimilarity(queryEmbedding, doc.embedding) * 5)
|
|
962
|
+
: 0;
|
|
963
|
+
if (lexicalCombined <= 0 && semantic <= 0) {
|
|
964
|
+
return null;
|
|
965
|
+
}
|
|
966
|
+
const recency = recencyScore(doc.timestamp);
|
|
967
|
+
const quality = typeof doc.qualityScore === "number" ? Math.max(0, Math.min(1, doc.qualityScore)) : 0.5;
|
|
968
|
+
const typeMatch = preferredTypes.length > 0 && doc.eventType
|
|
969
|
+
? (preferredTypes.includes(doc.eventType) ? 1 : 0)
|
|
970
|
+
: 0.5;
|
|
971
|
+
const graphMatch = source === "graph" ? 1 : 0;
|
|
972
|
+
const sourceBaseWeight = sourceWeight(source, intent);
|
|
973
|
+
const sourceConfigWeight = customChannelWeight(source, options.fusion);
|
|
974
|
+
const lengthNorm = lengthNormalizeFactor(doc, options.fusion);
|
|
975
|
+
const baseWeighted = (0.2 * lexicalCombined +
|
|
976
|
+
0.3 * (semantic * lengthNorm) +
|
|
977
|
+
0.1 * recency +
|
|
978
|
+
0.15 * quality +
|
|
979
|
+
0.15 * typeMatch +
|
|
980
|
+
0.1 * graphMatch) * sourceBaseWeight * sourceConfigWeight;
|
|
981
|
+
const decayFactor = computeDecayFactor(doc.id, doc.eventType, doc.timestamp, options.memoryDecay, hitStats);
|
|
982
|
+
const weighted = baseWeighted * decayFactor;
|
|
983
|
+
return {
|
|
984
|
+
doc,
|
|
985
|
+
source,
|
|
986
|
+
lexical: lexicalCombined,
|
|
987
|
+
bm25,
|
|
988
|
+
semantic,
|
|
989
|
+
recency,
|
|
990
|
+
quality,
|
|
991
|
+
typeMatch,
|
|
992
|
+
graphMatch,
|
|
993
|
+
decayFactor,
|
|
994
|
+
weighted,
|
|
995
|
+
};
|
|
996
|
+
};
|
|
997
|
+
for (const doc of rulesDocs) {
|
|
998
|
+
const candidate = evaluateDoc(doc, "rules");
|
|
999
|
+
if (candidate)
|
|
1000
|
+
channels.rules.push(candidate);
|
|
1001
|
+
}
|
|
1002
|
+
for (const doc of archiveDocs) {
|
|
1003
|
+
const candidate = evaluateDoc(doc, "archive");
|
|
1004
|
+
if (candidate)
|
|
1005
|
+
channels.archive.push(candidate);
|
|
1006
|
+
}
|
|
1007
|
+
for (const doc of vectorDocs) {
|
|
1008
|
+
const candidate = evaluateDoc(doc, "vector");
|
|
1009
|
+
if (candidate)
|
|
1010
|
+
channels.vector.push(candidate);
|
|
1011
|
+
}
|
|
1012
|
+
for (const doc of graphDocs) {
|
|
1013
|
+
const candidate = evaluateDoc(doc, "graph");
|
|
1014
|
+
if (candidate)
|
|
1015
|
+
channels.graph.push(candidate);
|
|
1016
|
+
}
|
|
1017
|
+
for (const key of Object.keys(channels)) {
|
|
1018
|
+
channels[key].sort((a, b) => b.weighted - a.weighted);
|
|
1019
|
+
combinedCandidates.push(...channels[key].slice(0, channelQuota(key, args.topK, options.fusion)));
|
|
1020
|
+
}
|
|
1021
|
+
const rrfMap = new Map();
|
|
1022
|
+
const weightedMap = new Map();
|
|
1023
|
+
const rrfK = 60;
|
|
1024
|
+
for (const key of Object.keys(channels)) {
|
|
1025
|
+
const list = channels[key];
|
|
1026
|
+
for (let i = 0; i < list.length; i += 1) {
|
|
1027
|
+
const candidate = list[i];
|
|
1028
|
+
const rrf = 1 / (rrfK + i + 1);
|
|
1029
|
+
const mergeKey = mergeKeyFromDoc(candidate.doc);
|
|
1030
|
+
rrfMap.set(mergeKey, (rrfMap.get(mergeKey) || 0) + rrf);
|
|
1031
|
+
const current = weightedMap.get(mergeKey);
|
|
1032
|
+
if (!current || candidate.weighted > current.weighted) {
|
|
1033
|
+
weightedMap.set(mergeKey, candidate);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
const preRanked = [...weightedMap.entries()]
|
|
1038
|
+
.map(([mergeKey, candidate]) => ({
|
|
1039
|
+
id: candidate.doc.id,
|
|
1040
|
+
merge_key: mergeKey,
|
|
1041
|
+
source_memory_id: candidate.doc.sourceMemoryId || "",
|
|
1042
|
+
source_memory_canonical_id: candidate.doc.sourceMemoryCanonicalId || "",
|
|
1043
|
+
text: candidate.doc.text,
|
|
1044
|
+
source: candidate.doc.source,
|
|
1045
|
+
layer: candidate.doc.layer || "",
|
|
1046
|
+
event_type: candidate.doc.eventType || "",
|
|
1047
|
+
quality_score: candidate.quality,
|
|
1048
|
+
timestamp: candidate.doc.timestamp ? new Date(candidate.doc.timestamp).toISOString() : "",
|
|
1049
|
+
score: candidate.weighted + (rrfMap.get(mergeKey) || 0) * 1.5,
|
|
1050
|
+
score_breakdown: {
|
|
1051
|
+
lexical: Number(candidate.lexical.toFixed(4)),
|
|
1052
|
+
bm25: Number(candidate.bm25.toFixed(4)),
|
|
1053
|
+
semantic: Number(candidate.semantic.toFixed(4)),
|
|
1054
|
+
recency: Number(candidate.recency.toFixed(4)),
|
|
1055
|
+
quality: Number(candidate.quality.toFixed(4)),
|
|
1056
|
+
type: Number(candidate.typeMatch.toFixed(4)),
|
|
1057
|
+
graph: Number(candidate.graphMatch.toFixed(4)),
|
|
1058
|
+
decay: Number(candidate.decayFactor.toFixed(4)),
|
|
1059
|
+
rrf: Number(((rrfMap.get(mergeKey) || 0) * 1.5).toFixed(4)),
|
|
1060
|
+
weighted: Number(candidate.weighted.toFixed(4)),
|
|
1061
|
+
},
|
|
1062
|
+
reason_tags: [
|
|
1063
|
+
`intent:${intent.toLowerCase()}`,
|
|
1064
|
+
candidate.semantic > 0 ? "vector_hit" : "lexical_hit",
|
|
1065
|
+
candidate.typeMatch >= 1 ? "event_type_match" : "event_type_weak",
|
|
1066
|
+
candidate.recency >= 0.8 ? "recent" : "historical",
|
|
1067
|
+
candidate.quality >= 0.7 ? "high_quality" : "normal_quality",
|
|
1068
|
+
candidate.decayFactor < 1 ? `decay:${candidate.decayFactor.toFixed(3)}` : "decay:1.000",
|
|
1069
|
+
`source:${candidate.source}`,
|
|
1070
|
+
`merge_key:${mergeKey}`,
|
|
1071
|
+
],
|
|
1072
|
+
}))
|
|
190
1073
|
.sort((a, b) => b.score - a.score)
|
|
191
|
-
.slice(0, Math.max(1, args.topK))
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
1074
|
+
.slice(0, Math.max(1, Math.max(args.topK, 20)));
|
|
1075
|
+
const lexicalRanked = preRanked
|
|
1076
|
+
.map(doc => {
|
|
1077
|
+
const boost = withRecencyBoost(doc.score, doc.timestamp ? Date.parse(doc.timestamp) : undefined);
|
|
1078
|
+
return { ...doc, score: Number(boost.toFixed(4)) };
|
|
1079
|
+
});
|
|
1080
|
+
const rerankerModel = options.reranker?.model || "";
|
|
1081
|
+
const rerankerApiKey = options.reranker?.apiKey || "";
|
|
1082
|
+
const rerankerBaseUrl = normalizeBaseUrl(options.reranker?.baseURL || options.reranker?.baseUrl);
|
|
1083
|
+
const fusionEnabled = options.fusion?.enabled !== false;
|
|
1084
|
+
const llmModel = options.llm?.model || "";
|
|
1085
|
+
const llmApiKey = options.llm?.apiKey || "";
|
|
1086
|
+
const llmBaseUrl = normalizeBaseUrl(options.llm?.baseURL || options.llm?.baseUrl);
|
|
1087
|
+
const fusionAuthoritative = options.fusion?.authoritative !== false;
|
|
1088
|
+
const skipRerankerForFusion = fusionEnabled && fusionAuthoritative && llmModel && llmApiKey && llmBaseUrl;
|
|
1089
|
+
let rerankedSimple = lexicalRanked.map(item => ({
|
|
1090
|
+
id: item.id,
|
|
1091
|
+
merge_key: item.merge_key,
|
|
1092
|
+
text: item.text,
|
|
1093
|
+
source: item.source,
|
|
1094
|
+
score: item.score,
|
|
197
1095
|
}));
|
|
198
|
-
|
|
1096
|
+
if (rerankerModel && rerankerApiKey && rerankerBaseUrl && lexicalRanked.length > 1 && !skipRerankerForFusion) {
|
|
1097
|
+
try {
|
|
1098
|
+
rerankedSimple = await requestRerank({
|
|
1099
|
+
query,
|
|
1100
|
+
candidates: lexicalRanked.map(item => ({ id: item.id, text: item.text, source: item.source, score: item.score })),
|
|
1101
|
+
model: rerankerModel,
|
|
1102
|
+
apiKey: rerankerApiKey,
|
|
1103
|
+
baseUrl: rerankerBaseUrl,
|
|
1104
|
+
});
|
|
1105
|
+
rerankedSimple = rerankedSimple.map(item => {
|
|
1106
|
+
const found = lexicalRanked.find(entry => entry.id === item.id);
|
|
1107
|
+
return { ...item, merge_key: found?.merge_key || item.id };
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
catch (error) {
|
|
1111
|
+
options.logger.warn(`Reranker failed, keep hybrid ranking: ${error}`);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
const ranked = rerankedSimple.slice(0, Math.max(1, args.topK)).map(item => {
|
|
1115
|
+
const hit = lexicalRanked.find(entry => entry.id === item.id);
|
|
1116
|
+
return {
|
|
1117
|
+
id: item.id,
|
|
1118
|
+
merge_key: hit?.merge_key || item.merge_key || item.id,
|
|
1119
|
+
source_memory_id: hit?.source_memory_id || "",
|
|
1120
|
+
source_memory_canonical_id: hit?.source_memory_canonical_id || "",
|
|
1121
|
+
text: item.text,
|
|
1122
|
+
source: item.source,
|
|
1123
|
+
layer: hit?.layer || "",
|
|
1124
|
+
event_type: hit?.event_type || "",
|
|
1125
|
+
quality_score: hit?.quality_score ?? 0,
|
|
1126
|
+
timestamp: hit?.timestamp || "",
|
|
1127
|
+
score: Number(item.score.toFixed(4)),
|
|
1128
|
+
score_breakdown: hit?.score_breakdown || {},
|
|
1129
|
+
reason_tags: Array.isArray(hit?.reason_tags) ? hit?.reason_tags : [],
|
|
1130
|
+
explain: {
|
|
1131
|
+
merge_key: hit?.merge_key || item.merge_key || item.id,
|
|
1132
|
+
source_memory_id: hit?.source_memory_id || "",
|
|
1133
|
+
source_memory_canonical_id: hit?.source_memory_canonical_id || "",
|
|
1134
|
+
channel: item.source,
|
|
1135
|
+
layer: hit?.layer || "",
|
|
1136
|
+
score_breakdown: hit?.score_breakdown || {},
|
|
1137
|
+
reason_tags: Array.isArray(hit?.reason_tags) ? hit?.reason_tags : [],
|
|
1138
|
+
},
|
|
1139
|
+
};
|
|
1140
|
+
});
|
|
1141
|
+
const minLexicalHits = Math.max(0, Math.floor(options.fusion?.minLexicalHits ?? 1));
|
|
1142
|
+
const minSemanticHits = Math.max(0, Math.floor(options.fusion?.minSemanticHits ?? 1));
|
|
1143
|
+
const fallbackPool = lexicalRanked.filter(item => !ranked.some(existing => existing.id === item.id));
|
|
1144
|
+
const lexicalCount = ranked.filter(item => item.reason_tags.includes("lexical_hit")).length;
|
|
1145
|
+
const semanticCount = ranked.filter(item => item.reason_tags.includes("vector_hit")).length;
|
|
1146
|
+
if (semanticCount < minSemanticHits) {
|
|
1147
|
+
const needed = minSemanticHits - semanticCount;
|
|
1148
|
+
const supplement = fallbackPool.filter(item => item.reason_tags.includes("vector_hit")).slice(0, needed);
|
|
1149
|
+
for (const item of supplement) {
|
|
1150
|
+
ranked.push({
|
|
1151
|
+
id: item.id,
|
|
1152
|
+
merge_key: item.merge_key,
|
|
1153
|
+
source_memory_id: item.source_memory_id,
|
|
1154
|
+
source_memory_canonical_id: item.source_memory_canonical_id,
|
|
1155
|
+
text: item.text,
|
|
1156
|
+
source: item.source,
|
|
1157
|
+
layer: item.layer,
|
|
1158
|
+
event_type: item.event_type,
|
|
1159
|
+
quality_score: item.quality_score,
|
|
1160
|
+
timestamp: item.timestamp,
|
|
1161
|
+
score: Number(item.score.toFixed(4)),
|
|
1162
|
+
score_breakdown: item.score_breakdown || {},
|
|
1163
|
+
reason_tags: Array.isArray(item.reason_tags) ? item.reason_tags : [],
|
|
1164
|
+
explain: {
|
|
1165
|
+
merge_key: item.merge_key,
|
|
1166
|
+
source_memory_id: item.source_memory_id,
|
|
1167
|
+
source_memory_canonical_id: item.source_memory_canonical_id,
|
|
1168
|
+
channel: item.source,
|
|
1169
|
+
layer: item.layer,
|
|
1170
|
+
score_breakdown: item.score_breakdown || {},
|
|
1171
|
+
reason_tags: Array.isArray(item.reason_tags) ? item.reason_tags : [],
|
|
1172
|
+
},
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
if (lexicalCount < minLexicalHits) {
|
|
1177
|
+
const needed = minLexicalHits - lexicalCount;
|
|
1178
|
+
const supplement = fallbackPool.filter(item => item.reason_tags.includes("lexical_hit")).slice(0, needed);
|
|
1179
|
+
for (const item of supplement) {
|
|
1180
|
+
if (ranked.some(existing => existing.id === item.id)) {
|
|
1181
|
+
continue;
|
|
1182
|
+
}
|
|
1183
|
+
ranked.push({
|
|
1184
|
+
id: item.id,
|
|
1185
|
+
merge_key: item.merge_key,
|
|
1186
|
+
source_memory_id: item.source_memory_id,
|
|
1187
|
+
source_memory_canonical_id: item.source_memory_canonical_id,
|
|
1188
|
+
text: item.text,
|
|
1189
|
+
source: item.source,
|
|
1190
|
+
layer: item.layer,
|
|
1191
|
+
event_type: item.event_type,
|
|
1192
|
+
quality_score: item.quality_score,
|
|
1193
|
+
timestamp: item.timestamp,
|
|
1194
|
+
score: Number(item.score.toFixed(4)),
|
|
1195
|
+
score_breakdown: item.score_breakdown || {},
|
|
1196
|
+
reason_tags: Array.isArray(item.reason_tags) ? item.reason_tags : [],
|
|
1197
|
+
explain: {
|
|
1198
|
+
merge_key: item.merge_key,
|
|
1199
|
+
source_memory_id: item.source_memory_id,
|
|
1200
|
+
source_memory_canonical_id: item.source_memory_canonical_id,
|
|
1201
|
+
channel: item.source,
|
|
1202
|
+
layer: item.layer,
|
|
1203
|
+
score_breakdown: item.score_breakdown || {},
|
|
1204
|
+
reason_tags: Array.isArray(item.reason_tags) ? item.reason_tags : [],
|
|
1205
|
+
},
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
ranked.sort((a, b) => b.score - a.score);
|
|
1210
|
+
if (fusionEnabled && llmModel && llmApiKey && llmBaseUrl && ranked.length > 1) {
|
|
1211
|
+
try {
|
|
1212
|
+
const maxCandidates = Math.max(4, Math.min(20, options.fusion?.maxCandidates ?? 10));
|
|
1213
|
+
const fusion = await requestFusion({
|
|
1214
|
+
query,
|
|
1215
|
+
candidates: ranked.slice(0, maxCandidates).map(item => ({
|
|
1216
|
+
id: item.id,
|
|
1217
|
+
text: item.text,
|
|
1218
|
+
source: item.source,
|
|
1219
|
+
event_type: item.event_type,
|
|
1220
|
+
quality_score: item.quality_score,
|
|
1221
|
+
timestamp: item.timestamp,
|
|
1222
|
+
score: item.score,
|
|
1223
|
+
reason_tags: Array.isArray(item.reason_tags) ? item.reason_tags : [],
|
|
1224
|
+
})),
|
|
1225
|
+
llm: {
|
|
1226
|
+
model: llmModel,
|
|
1227
|
+
apiKey: llmApiKey,
|
|
1228
|
+
baseUrl: llmBaseUrl,
|
|
1229
|
+
},
|
|
1230
|
+
});
|
|
1231
|
+
if (fusion && fusion.canonical_answer) {
|
|
1232
|
+
const fusedItem = {
|
|
1233
|
+
id: `fusion_${Date.now().toString(36)}`,
|
|
1234
|
+
text: fusion.canonical_answer,
|
|
1235
|
+
source: "llm_fusion",
|
|
1236
|
+
event_type: "fusion",
|
|
1237
|
+
quality_score: Number(fusion.confidence.toFixed(4)),
|
|
1238
|
+
timestamp: new Date().toISOString(),
|
|
1239
|
+
score: Number((Math.max(...ranked.map(item => item.score)) + 1).toFixed(4)),
|
|
1240
|
+
reason_tags: ["llm_fused_authoritative", `evidence:${fusion.evidence_ids.length}`],
|
|
1241
|
+
explain: {
|
|
1242
|
+
channel: "llm_fusion",
|
|
1243
|
+
fused_from: ranked.slice(0, maxCandidates).map(item => item.id),
|
|
1244
|
+
reason_tags: ["llm_fused_authoritative", `evidence:${fusion.evidence_ids.length}`],
|
|
1245
|
+
},
|
|
1246
|
+
fused_coverage_note: fusion.coverage_note || "",
|
|
1247
|
+
fused_facts: fusion.facts,
|
|
1248
|
+
fused_timeline: fusion.timeline || [],
|
|
1249
|
+
fused_entities: fusion.entities || [],
|
|
1250
|
+
fused_decisions: fusion.decisions || [],
|
|
1251
|
+
fused_fixes: fusion.fixes || [],
|
|
1252
|
+
fused_preferences: fusion.preferences || [],
|
|
1253
|
+
fused_risks: fusion.risks || [],
|
|
1254
|
+
fused_action_items: fusion.action_items || [],
|
|
1255
|
+
fused_conflicts: fusion.conflicts,
|
|
1256
|
+
fused_evidence_ids: fusion.evidence_ids,
|
|
1257
|
+
};
|
|
1258
|
+
const authoritative = options.fusion?.authoritative !== false;
|
|
1259
|
+
if (authoritative) {
|
|
1260
|
+
markHit(Array.isArray(fusion.evidence_ids) ? fusion.evidence_ids : []);
|
|
1261
|
+
return { results: [fusedItem] };
|
|
1262
|
+
}
|
|
1263
|
+
const merged = [fusedItem, ...ranked];
|
|
1264
|
+
markHit([
|
|
1265
|
+
...(Array.isArray(fusion.evidence_ids) ? fusion.evidence_ids : []),
|
|
1266
|
+
...ranked.map(item => item.id),
|
|
1267
|
+
]);
|
|
1268
|
+
return { results: merged.slice(0, Math.max(1, args.topK)) };
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
catch (error) {
|
|
1272
|
+
options.logger.warn(`LLM fusion failed, fallback to reranked results: ${error}`);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
const finalRanked = ranked.slice(0, Math.max(1, args.topK));
|
|
1276
|
+
markHit(finalRanked.map(item => item.id));
|
|
1277
|
+
return { results: finalRanked };
|
|
199
1278
|
}
|
|
200
1279
|
async function getHotContext(args) {
|
|
201
1280
|
const limit = Math.max(1, args.limit);
|
|
202
1281
|
const docs = loadAllDocuments();
|
|
203
1282
|
const coreRules = docs.find(doc => doc.source === "CORTEX_RULES.md");
|
|
204
|
-
const
|
|
205
|
-
.filter(doc => doc.source
|
|
1283
|
+
const archiveDocs = docs
|
|
1284
|
+
.filter(doc => doc.source === "sessions_archive")
|
|
206
1285
|
.sort((a, b) => (b.timestamp ?? 0) - (a.timestamp ?? 0))
|
|
207
1286
|
.slice(0, limit);
|
|
1287
|
+
const issueFixPairs = docs
|
|
1288
|
+
.filter(doc => doc.source === "sessions_archive" && (doc.eventType === "issue" || doc.eventType === "fix"))
|
|
1289
|
+
.sort((a, b) => (b.timestamp ?? 0) - (a.timestamp ?? 0))
|
|
1290
|
+
.slice(0, 2);
|
|
208
1291
|
const result = [];
|
|
209
1292
|
if (coreRules) {
|
|
210
1293
|
result.push({ id: coreRules.id, text: coreRules.text, source: coreRules.source });
|
|
211
1294
|
}
|
|
212
|
-
for (const doc of
|
|
1295
|
+
for (const doc of [...issueFixPairs, ...archiveDocs]) {
|
|
213
1296
|
result.push({ id: doc.id, text: doc.text, source: doc.source });
|
|
214
1297
|
}
|
|
215
1298
|
return { context: result.slice(0, limit) };
|
|
@@ -223,6 +1306,20 @@ function createReadStore(options) {
|
|
|
223
1306
|
age_seconds: args.cachedAutoSearch.ageSeconds,
|
|
224
1307
|
};
|
|
225
1308
|
}
|
|
1309
|
+
if (!result.auto_search) {
|
|
1310
|
+
const docs = loadAllDocuments()
|
|
1311
|
+
.filter(doc => doc.source === "sessions_archive" && doc.sessionId === args.sessionId)
|
|
1312
|
+
.sort((a, b) => (b.timestamp ?? 0) - (a.timestamp ?? 0));
|
|
1313
|
+
const latest = docs[0];
|
|
1314
|
+
if (latest && latest.text.trim()) {
|
|
1315
|
+
const light = await searchMemory({ query: latest.text.slice(0, 80), topK: 3 });
|
|
1316
|
+
result.auto_search = {
|
|
1317
|
+
query: latest.text.slice(0, 80),
|
|
1318
|
+
results: light.results,
|
|
1319
|
+
age_seconds: 0,
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
226
1323
|
if (args.includeHot) {
|
|
227
1324
|
const hot = await getHotContext({ limit: 20 });
|
|
228
1325
|
result.hot_context = hot.context;
|