openclaw-cortex-memory 0.1.0-Alpha.3 → 0.1.0-Alpha.5
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 +101 -18
- package/SKILL.md +79 -9
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +159 -7
- package/dist/index.js.map +1 -1
- package/dist/openclaw.plugin.json +101 -1
- 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/ts_engine.d.ts +36 -0
- package/dist/src/engine/ts_engine.d.ts.map +1 -1
- package/dist/src/engine/ts_engine.js +197 -32
- package/dist/src/engine/ts_engine.js.map +1 -1
- package/dist/src/engine/types.d.ts +4 -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 +55 -0
- package/dist/src/session/session_end.d.ts.map +1 -1
- package/dist/src/session/session_end.js +237 -51
- package/dist/src/session/session_end.js.map +1 -1
- package/dist/src/store/archive_store.d.ts +89 -0
- package/dist/src/store/archive_store.d.ts.map +1 -0
- package/dist/src/store/archive_store.js +242 -0
- package/dist/src/store/archive_store.js.map +1 -0
- package/dist/src/store/read_store.d.ts +39 -0
- package/dist/src/store/read_store.d.ts.map +1 -1
- package/dist/src/store/read_store.js +796 -15
- package/dist/src/store/read_store.js.map +1 -1
- package/dist/src/store/vector_store.d.ts +30 -0
- package/dist/src/store/vector_store.d.ts.map +1 -0
- package/dist/src/store/vector_store.js +127 -0
- package/dist/src/store/vector_store.js.map +1 -0
- package/dist/src/store/write_store.d.ts +8 -0
- package/dist/src/store/write_store.d.ts.map +1 -1
- package/dist/src/store/write_store.js +70 -0
- package/dist/src/store/write_store.js.map +1 -1
- package/dist/src/sync/session_sync.d.ts +7 -0
- package/dist/src/sync/session_sync.d.ts.map +1 -1
- package/dist/src/sync/session_sync.js +120 -10
- package/dist/src/sync/session_sync.js.map +1 -1
- package/openclaw.plugin.json +101 -1
- package/package.json +16 -5
- package/scripts/cli.js +6 -1
- package/scripts/uninstall.js +15 -4
- package/index.ts +0 -2142
- package/scripts/install.js +0 -27
|
@@ -114,11 +114,35 @@ function parseJsonlFile(filePath, sourceLabel, logger) {
|
|
|
114
114
|
}
|
|
115
115
|
const id = typeof parsed.id === "string" ? parsed.id : `${sourceLabel}:${docs.length + 1}`;
|
|
116
116
|
const timestampValue = typeof parsed.timestamp === "string" ? Date.parse(parsed.timestamp) : NaN;
|
|
117
|
+
const entities = Array.isArray(parsed.entities)
|
|
118
|
+
? parsed.entities.map(item => (typeof item === "string" ? item.trim() : "")).filter(Boolean)
|
|
119
|
+
: [];
|
|
120
|
+
const relations = Array.isArray(parsed.relations)
|
|
121
|
+
? parsed.relations
|
|
122
|
+
.map(item => {
|
|
123
|
+
if (typeof item !== "object" || item === null)
|
|
124
|
+
return null;
|
|
125
|
+
const relation = item;
|
|
126
|
+
const source = typeof relation.source === "string" ? relation.source.trim() : "";
|
|
127
|
+
const target = typeof relation.target === "string" ? relation.target.trim() : "";
|
|
128
|
+
const type = typeof relation.type === "string" ? relation.type.trim() : "related_to";
|
|
129
|
+
if (!source || !target)
|
|
130
|
+
return null;
|
|
131
|
+
return { source, target, type };
|
|
132
|
+
})
|
|
133
|
+
.filter((item) => Boolean(item))
|
|
134
|
+
: [];
|
|
117
135
|
docs.push({
|
|
118
136
|
id,
|
|
119
137
|
text,
|
|
120
138
|
source: sourceLabel,
|
|
121
139
|
timestamp: Number.isFinite(timestampValue) ? timestampValue : undefined,
|
|
140
|
+
embedding: Array.isArray(parsed.embedding) ? parsed.embedding.filter(item => Number.isFinite(item)) : undefined,
|
|
141
|
+
eventType: typeof parsed.event_type === "string" ? parsed.event_type.trim() : undefined,
|
|
142
|
+
qualityScore: typeof parsed.quality_score === "number" ? parsed.quality_score : undefined,
|
|
143
|
+
sessionId: typeof parsed.session_id === "string" ? parsed.session_id : undefined,
|
|
144
|
+
entities,
|
|
145
|
+
relations,
|
|
122
146
|
});
|
|
123
147
|
}
|
|
124
148
|
catch (error) {
|
|
@@ -160,8 +184,512 @@ function withRecencyBoost(score, timestamp) {
|
|
|
160
184
|
}
|
|
161
185
|
return score;
|
|
162
186
|
}
|
|
187
|
+
function recencyScore(timestamp) {
|
|
188
|
+
if (!timestamp) {
|
|
189
|
+
return 0;
|
|
190
|
+
}
|
|
191
|
+
const ageHours = (Date.now() - timestamp) / (1000 * 60 * 60);
|
|
192
|
+
if (ageHours < 12)
|
|
193
|
+
return 1;
|
|
194
|
+
if (ageHours < 24)
|
|
195
|
+
return 0.8;
|
|
196
|
+
if (ageHours < 72)
|
|
197
|
+
return 0.6;
|
|
198
|
+
if (ageHours < 168)
|
|
199
|
+
return 0.4;
|
|
200
|
+
if (ageHours < 720)
|
|
201
|
+
return 0.2;
|
|
202
|
+
return 0.05;
|
|
203
|
+
}
|
|
204
|
+
function eventTypeHalfLifeDays(eventType, options) {
|
|
205
|
+
const fallback = typeof options?.defaultHalfLifeDays === "number" && options.defaultHalfLifeDays > 0
|
|
206
|
+
? options.defaultHalfLifeDays
|
|
207
|
+
: 90;
|
|
208
|
+
const type = (eventType || "").trim().toLowerCase();
|
|
209
|
+
if (!type)
|
|
210
|
+
return fallback;
|
|
211
|
+
const configured = options?.halfLifeByEventType || {};
|
|
212
|
+
if (typeof configured[type] === "number" && configured[type] > 0) {
|
|
213
|
+
return configured[type];
|
|
214
|
+
}
|
|
215
|
+
if (["issue", "fix", "action_item", "blocker"].includes(type))
|
|
216
|
+
return 30;
|
|
217
|
+
if (["plan", "milestone", "follow_up"].includes(type))
|
|
218
|
+
return 60;
|
|
219
|
+
if (["decision", "insight", "retrospective"].includes(type))
|
|
220
|
+
return 120;
|
|
221
|
+
if (["preference", "constraint", "requirement", "dependency", "assumption"].includes(type))
|
|
222
|
+
return 240;
|
|
223
|
+
return fallback;
|
|
224
|
+
}
|
|
225
|
+
function computeAntiDecayBoost(id, hitStats, options) {
|
|
226
|
+
const anti = options?.antiDecay;
|
|
227
|
+
if (anti?.enabled === false) {
|
|
228
|
+
return 1;
|
|
229
|
+
}
|
|
230
|
+
const item = hitStats.items[id];
|
|
231
|
+
if (!item) {
|
|
232
|
+
return 1;
|
|
233
|
+
}
|
|
234
|
+
const hitWeight = typeof anti?.hitWeight === "number" && anti.hitWeight > 0 ? anti.hitWeight : 0.08;
|
|
235
|
+
const maxBoost = typeof anti?.maxBoost === "number" && anti.maxBoost >= 1 ? anti.maxBoost : 1.6;
|
|
236
|
+
const recentWindowDays = typeof anti?.recentWindowDays === "number" && anti.recentWindowDays > 0 ? anti.recentWindowDays : 30;
|
|
237
|
+
const lastHitTs = Date.parse(item.lastHitAt || "");
|
|
238
|
+
const ageDays = Number.isFinite(lastHitTs) ? Math.max(0, (Date.now() - lastHitTs) / (1000 * 60 * 60 * 24)) : recentWindowDays * 2;
|
|
239
|
+
const freshness = ageDays <= recentWindowDays ? (1 - ageDays / recentWindowDays) : 0;
|
|
240
|
+
const countFactor = Math.log1p(Math.max(0, item.count));
|
|
241
|
+
const boost = 1 + countFactor * hitWeight * (0.5 + 0.5 * freshness);
|
|
242
|
+
return Math.min(maxBoost, Math.max(1, boost));
|
|
243
|
+
}
|
|
244
|
+
function computeDecayFactor(id, eventType, timestamp, options, hitStats) {
|
|
245
|
+
const enabled = options?.enabled !== false;
|
|
246
|
+
if (!enabled || !timestamp) {
|
|
247
|
+
return computeAntiDecayBoost(id, hitStats, options);
|
|
248
|
+
}
|
|
249
|
+
const ageDays = Math.max(0, (Date.now() - timestamp) / (1000 * 60 * 60 * 24));
|
|
250
|
+
const halfLife = eventTypeHalfLifeDays(eventType, options);
|
|
251
|
+
const base = Math.pow(2, -ageDays / Math.max(1, halfLife));
|
|
252
|
+
const floor = typeof options?.minFloor === "number"
|
|
253
|
+
? Math.max(0, Math.min(1, options.minFloor))
|
|
254
|
+
: 0.15;
|
|
255
|
+
const decay = Math.max(floor, base);
|
|
256
|
+
const boost = computeAntiDecayBoost(id, hitStats, options);
|
|
257
|
+
return Math.min(1, decay * boost);
|
|
258
|
+
}
|
|
259
|
+
function normalizeBaseUrl(value) {
|
|
260
|
+
if (!value)
|
|
261
|
+
return "";
|
|
262
|
+
return value.endsWith("/") ? value.slice(0, -1) : value;
|
|
263
|
+
}
|
|
264
|
+
function cosineSimilarity(left, right) {
|
|
265
|
+
if (left.length === 0 || right.length === 0) {
|
|
266
|
+
return 0;
|
|
267
|
+
}
|
|
268
|
+
const size = Math.min(left.length, right.length);
|
|
269
|
+
let dot = 0;
|
|
270
|
+
let leftNorm = 0;
|
|
271
|
+
let rightNorm = 0;
|
|
272
|
+
for (let i = 0; i < size; i += 1) {
|
|
273
|
+
const a = left[i];
|
|
274
|
+
const b = right[i];
|
|
275
|
+
dot += a * b;
|
|
276
|
+
leftNorm += a * a;
|
|
277
|
+
rightNorm += b * b;
|
|
278
|
+
}
|
|
279
|
+
if (leftNorm === 0 || rightNorm === 0) {
|
|
280
|
+
return 0;
|
|
281
|
+
}
|
|
282
|
+
return dot / (Math.sqrt(leftNorm) * Math.sqrt(rightNorm));
|
|
283
|
+
}
|
|
284
|
+
async function requestEmbedding(args) {
|
|
285
|
+
const endpoint = args.baseUrl.endsWith("/embeddings") ? args.baseUrl : `${args.baseUrl}/embeddings`;
|
|
286
|
+
const body = {
|
|
287
|
+
input: args.text,
|
|
288
|
+
model: args.model,
|
|
289
|
+
};
|
|
290
|
+
if (typeof args.dimensions === "number" && Number.isFinite(args.dimensions) && args.dimensions > 0) {
|
|
291
|
+
body.dimensions = args.dimensions;
|
|
292
|
+
}
|
|
293
|
+
let lastError = null;
|
|
294
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
295
|
+
const controller = new AbortController();
|
|
296
|
+
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
|
297
|
+
try {
|
|
298
|
+
const response = await fetch(endpoint, {
|
|
299
|
+
method: "POST",
|
|
300
|
+
headers: {
|
|
301
|
+
"content-type": "application/json",
|
|
302
|
+
authorization: `Bearer ${args.apiKey}`,
|
|
303
|
+
},
|
|
304
|
+
body: JSON.stringify(body),
|
|
305
|
+
signal: controller.signal,
|
|
306
|
+
});
|
|
307
|
+
clearTimeout(timeoutId);
|
|
308
|
+
if (!response.ok) {
|
|
309
|
+
lastError = new Error(`embedding_http_${response.status}`);
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
const json = await response.json();
|
|
313
|
+
const embedding = json?.data?.[0]?.embedding;
|
|
314
|
+
if (Array.isArray(embedding) && embedding.length > 0) {
|
|
315
|
+
return embedding.filter(item => Number.isFinite(item));
|
|
316
|
+
}
|
|
317
|
+
lastError = new Error("embedding_empty");
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
clearTimeout(timeoutId);
|
|
321
|
+
lastError = error;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (lastError) {
|
|
325
|
+
throw lastError;
|
|
326
|
+
}
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
async function requestRerank(args) {
|
|
330
|
+
const endpoint = args.baseUrl.endsWith("/rerank") ? args.baseUrl : `${args.baseUrl}/rerank`;
|
|
331
|
+
const documents = args.candidates.map(item => item.text);
|
|
332
|
+
const body = {
|
|
333
|
+
model: args.model,
|
|
334
|
+
query: args.query,
|
|
335
|
+
documents,
|
|
336
|
+
top_n: args.candidates.length,
|
|
337
|
+
};
|
|
338
|
+
let lastError = null;
|
|
339
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
340
|
+
const controller = new AbortController();
|
|
341
|
+
const timeoutId = setTimeout(() => controller.abort(), 12000);
|
|
342
|
+
try {
|
|
343
|
+
const response = await fetch(endpoint, {
|
|
344
|
+
method: "POST",
|
|
345
|
+
headers: {
|
|
346
|
+
"content-type": "application/json",
|
|
347
|
+
authorization: `Bearer ${args.apiKey}`,
|
|
348
|
+
},
|
|
349
|
+
body: JSON.stringify(body),
|
|
350
|
+
signal: controller.signal,
|
|
351
|
+
});
|
|
352
|
+
clearTimeout(timeoutId);
|
|
353
|
+
if (!response.ok) {
|
|
354
|
+
lastError = new Error(`rerank_http_${response.status}`);
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
const json = await response.json();
|
|
358
|
+
const list = Array.isArray(json.results) ? json.results : (Array.isArray(json.data) ? json.data : []);
|
|
359
|
+
if (!Array.isArray(list) || list.length === 0) {
|
|
360
|
+
lastError = new Error("rerank_empty");
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
const mapped = list
|
|
364
|
+
.map((item, rank) => {
|
|
365
|
+
const index = typeof item.index === "number" ? item.index : rank;
|
|
366
|
+
const hit = args.candidates[index];
|
|
367
|
+
if (!hit)
|
|
368
|
+
return null;
|
|
369
|
+
const score = typeof item.relevance_score === "number" ? item.relevance_score : (typeof item.score === "number" ? item.score : hit.score);
|
|
370
|
+
return { ...hit, score };
|
|
371
|
+
})
|
|
372
|
+
.filter((item) => Boolean(item));
|
|
373
|
+
if (mapped.length > 0) {
|
|
374
|
+
return mapped;
|
|
375
|
+
}
|
|
376
|
+
lastError = new Error("rerank_map_empty");
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
clearTimeout(timeoutId);
|
|
380
|
+
lastError = error;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError || "rerank_failed"));
|
|
384
|
+
}
|
|
385
|
+
function classifyIntent(query) {
|
|
386
|
+
const text = query.toLowerCase();
|
|
387
|
+
const relationHints = /(关系|依赖|关联|上下游|graph|relation|entity|拓扑)/i;
|
|
388
|
+
if (relationHints.test(text))
|
|
389
|
+
return "RELATION_DISCOVERY";
|
|
390
|
+
const troubleHints = /(报错|错误|异常|失败|超时|无法|崩溃|error|failed|timeout|fix)/i;
|
|
391
|
+
if (troubleHints.test(text))
|
|
392
|
+
return "TROUBLESHOOTING";
|
|
393
|
+
const preferenceHints = /(偏好|习惯|口味|喜欢|不喜欢|偏向|preference)/i;
|
|
394
|
+
if (preferenceHints.test(text))
|
|
395
|
+
return "PREFERENCE_PROFILE";
|
|
396
|
+
const timelineHints = /(最近|上次|之前|时间线|timeline|history)/i;
|
|
397
|
+
if (timelineHints.test(text))
|
|
398
|
+
return "TIMELINE_REVIEW";
|
|
399
|
+
const decisionHints = /(方案|决策|选择|建议|取舍|tradeoff|plan)/i;
|
|
400
|
+
if (decisionHints.test(text))
|
|
401
|
+
return "DECISION_SUPPORT";
|
|
402
|
+
return "FACT_LOOKUP";
|
|
403
|
+
}
|
|
404
|
+
function preferredEventTypes(intent) {
|
|
405
|
+
if (intent === "TROUBLESHOOTING")
|
|
406
|
+
return ["issue", "fix", "risk", "blocker", "dependency", "retrospective"];
|
|
407
|
+
if (intent === "PREFERENCE_PROFILE")
|
|
408
|
+
return ["preference", "decision", "constraint", "requirement"];
|
|
409
|
+
if (intent === "DECISION_SUPPORT")
|
|
410
|
+
return ["decision", "plan", "insight", "assumption", "constraint", "requirement"];
|
|
411
|
+
if (intent === "TIMELINE_REVIEW")
|
|
412
|
+
return ["action_item", "follow_up", "milestone", "plan", "decision", "issue", "fix"];
|
|
413
|
+
return [];
|
|
414
|
+
}
|
|
415
|
+
function sourceWeight(source, intent) {
|
|
416
|
+
if (source === "rules") {
|
|
417
|
+
return intent === "DECISION_SUPPORT" || intent === "TROUBLESHOOTING" ? 1.15 : 0.9;
|
|
418
|
+
}
|
|
419
|
+
if (source === "graph") {
|
|
420
|
+
return intent === "RELATION_DISCOVERY" ? 1.25 : 0.85;
|
|
421
|
+
}
|
|
422
|
+
if (source === "vector") {
|
|
423
|
+
return 1.05;
|
|
424
|
+
}
|
|
425
|
+
return 1;
|
|
426
|
+
}
|
|
427
|
+
async function searchLanceDb(args) {
|
|
428
|
+
try {
|
|
429
|
+
const lancedbDir = path.join(args.memoryRoot, "vector", "lancedb");
|
|
430
|
+
if (!fs.existsSync(lancedbDir)) {
|
|
431
|
+
return [];
|
|
432
|
+
}
|
|
433
|
+
const dynamicImport = new Function("specifier", "return import(specifier)");
|
|
434
|
+
const moduleValue = await dynamicImport("@lancedb/lancedb");
|
|
435
|
+
const connect = moduleValue.connect;
|
|
436
|
+
if (typeof connect !== "function") {
|
|
437
|
+
return [];
|
|
438
|
+
}
|
|
439
|
+
const db = await connect(lancedbDir);
|
|
440
|
+
if (!db || typeof db.openTable !== "function") {
|
|
441
|
+
return [];
|
|
442
|
+
}
|
|
443
|
+
const table = await db.openTable("events");
|
|
444
|
+
if (!table || typeof table.search !== "function") {
|
|
445
|
+
return [];
|
|
446
|
+
}
|
|
447
|
+
const searchObj = table.search(args.queryEmbedding);
|
|
448
|
+
if (!searchObj || typeof searchObj.limit !== "function") {
|
|
449
|
+
return [];
|
|
450
|
+
}
|
|
451
|
+
const limited = searchObj.limit(args.limit);
|
|
452
|
+
if (!limited || typeof limited.toArray !== "function") {
|
|
453
|
+
return [];
|
|
454
|
+
}
|
|
455
|
+
const rows = await limited.toArray();
|
|
456
|
+
const docs = [];
|
|
457
|
+
for (const row of rows) {
|
|
458
|
+
if (typeof row !== "object" || row === null)
|
|
459
|
+
continue;
|
|
460
|
+
const record = row;
|
|
461
|
+
const id = typeof record.id === "string" ? record.id : "";
|
|
462
|
+
const summary = typeof record.summary === "string" ? record.summary : "";
|
|
463
|
+
if (!id || !summary)
|
|
464
|
+
continue;
|
|
465
|
+
const ts = typeof record.timestamp === "string" ? Date.parse(record.timestamp) : NaN;
|
|
466
|
+
const entities = typeof record.entities_json === "string"
|
|
467
|
+
? JSON.parse(record.entities_json).filter(item => typeof item === "string" && item.trim())
|
|
468
|
+
: [];
|
|
469
|
+
const relations = typeof record.relations_json === "string"
|
|
470
|
+
? JSON.parse(record.relations_json)
|
|
471
|
+
: [];
|
|
472
|
+
docs.push({
|
|
473
|
+
id,
|
|
474
|
+
text: summary,
|
|
475
|
+
source: "vector_lancedb",
|
|
476
|
+
timestamp: Number.isFinite(ts) ? ts : undefined,
|
|
477
|
+
embedding: Array.isArray(record.vector) ? record.vector.filter(item => Number.isFinite(item)) : undefined,
|
|
478
|
+
eventType: typeof record.event_type === "string" ? record.event_type : undefined,
|
|
479
|
+
qualityScore: typeof record.quality_score === "number" ? record.quality_score : undefined,
|
|
480
|
+
sessionId: typeof record.session_id === "string" ? record.session_id : undefined,
|
|
481
|
+
entities,
|
|
482
|
+
relations: Array.isArray(relations) ? relations : [],
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
return docs;
|
|
486
|
+
}
|
|
487
|
+
catch (error) {
|
|
488
|
+
args.logger.debug(`LanceDB search fallback: ${error}`);
|
|
489
|
+
return [];
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
function parseVectorFallback(filePath, logger) {
|
|
493
|
+
const content = safeReadFile(filePath);
|
|
494
|
+
if (!content) {
|
|
495
|
+
return [];
|
|
496
|
+
}
|
|
497
|
+
const docs = [];
|
|
498
|
+
for (const line of content.split(/\r?\n/)) {
|
|
499
|
+
const trimmed = line.trim();
|
|
500
|
+
if (!trimmed)
|
|
501
|
+
continue;
|
|
502
|
+
try {
|
|
503
|
+
const parsed = JSON.parse(trimmed);
|
|
504
|
+
const id = typeof parsed.id === "string" ? parsed.id : "";
|
|
505
|
+
const summary = typeof parsed.summary === "string" ? parsed.summary.trim() : "";
|
|
506
|
+
if (!id || !summary)
|
|
507
|
+
continue;
|
|
508
|
+
const ts = typeof parsed.timestamp === "string" ? Date.parse(parsed.timestamp) : NaN;
|
|
509
|
+
const entities = Array.isArray(parsed.entities)
|
|
510
|
+
? parsed.entities.map(item => (typeof item === "string" ? item.trim() : "")).filter(Boolean)
|
|
511
|
+
: [];
|
|
512
|
+
const relations = Array.isArray(parsed.relations)
|
|
513
|
+
? parsed.relations
|
|
514
|
+
.map(item => {
|
|
515
|
+
if (typeof item !== "object" || item === null)
|
|
516
|
+
return null;
|
|
517
|
+
const relation = item;
|
|
518
|
+
const source = typeof relation.source === "string" ? relation.source.trim() : "";
|
|
519
|
+
const target = typeof relation.target === "string" ? relation.target.trim() : "";
|
|
520
|
+
const type = typeof relation.type === "string" ? relation.type.trim() : "related_to";
|
|
521
|
+
if (!source || !target)
|
|
522
|
+
return null;
|
|
523
|
+
return { source, target, type };
|
|
524
|
+
})
|
|
525
|
+
.filter((item) => Boolean(item))
|
|
526
|
+
: [];
|
|
527
|
+
docs.push({
|
|
528
|
+
id,
|
|
529
|
+
text: summary,
|
|
530
|
+
source: "vector_jsonl",
|
|
531
|
+
timestamp: Number.isFinite(ts) ? ts : undefined,
|
|
532
|
+
embedding: Array.isArray(parsed.embedding) ? parsed.embedding.filter(item => Number.isFinite(item)) : undefined,
|
|
533
|
+
eventType: typeof parsed.event_type === "string" ? parsed.event_type.trim() : undefined,
|
|
534
|
+
qualityScore: typeof parsed.quality_score === "number" ? parsed.quality_score : undefined,
|
|
535
|
+
sessionId: typeof parsed.session_id === "string" ? parsed.session_id : undefined,
|
|
536
|
+
entities,
|
|
537
|
+
relations,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
catch (error) {
|
|
541
|
+
logger.debug(`Skip invalid vector jsonl line: ${error}`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return docs;
|
|
545
|
+
}
|
|
546
|
+
async function requestFusion(args) {
|
|
547
|
+
const endpoint = args.llm.baseUrl.endsWith("/chat/completions")
|
|
548
|
+
? args.llm.baseUrl
|
|
549
|
+
: `${args.llm.baseUrl}/chat/completions`;
|
|
550
|
+
const evidenceText = args.candidates
|
|
551
|
+
.map((item, index) => `${index + 1}. [${item.id}] (${item.source}, score=${item.score.toFixed(4)}) ${item.text}`)
|
|
552
|
+
.join("\n")
|
|
553
|
+
.slice(0, 18000);
|
|
554
|
+
const prompt = [
|
|
555
|
+
"你是记忆检索融合器。请融合多路召回结果,产出可直接给 Agent 使用的完整记忆包,不要让 Agent 再去翻历史。",
|
|
556
|
+
"必须严格返回 JSON:",
|
|
557
|
+
"{\"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}",
|
|
558
|
+
"要求:",
|
|
559
|
+
"1) canonical_answer 是完整可执行答案,不要只写摘要",
|
|
560
|
+
"2) facts 3-12 条,优先高分证据",
|
|
561
|
+
"3) evidence_ids 必须来自输入候选 id",
|
|
562
|
+
"4) 若存在冲突写入 conflicts,否则返回空数组",
|
|
563
|
+
"5) confidence 0~1",
|
|
564
|
+
"6) 不确定信息必须在 coverage_note 标注",
|
|
565
|
+
].join("\n");
|
|
566
|
+
const body = {
|
|
567
|
+
model: args.llm.model,
|
|
568
|
+
temperature: 0.1,
|
|
569
|
+
messages: [
|
|
570
|
+
{ role: "system", content: "你只输出 JSON,不要额外解释。" },
|
|
571
|
+
{ role: "user", content: `${prompt}\n\n问题:\n${args.query}\n\n候选证据:\n${evidenceText}` },
|
|
572
|
+
],
|
|
573
|
+
};
|
|
574
|
+
let lastError = null;
|
|
575
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
576
|
+
const controller = new AbortController();
|
|
577
|
+
const timeoutId = setTimeout(() => controller.abort(), 20000);
|
|
578
|
+
try {
|
|
579
|
+
const response = await fetch(endpoint, {
|
|
580
|
+
method: "POST",
|
|
581
|
+
headers: {
|
|
582
|
+
"content-type": "application/json",
|
|
583
|
+
authorization: `Bearer ${args.llm.apiKey}`,
|
|
584
|
+
},
|
|
585
|
+
body: JSON.stringify(body),
|
|
586
|
+
signal: controller.signal,
|
|
587
|
+
});
|
|
588
|
+
clearTimeout(timeoutId);
|
|
589
|
+
if (!response.ok) {
|
|
590
|
+
lastError = new Error(`fusion_http_${response.status}`);
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
const json = await response.json();
|
|
594
|
+
const content = json?.choices?.[0]?.message?.content?.trim() || "";
|
|
595
|
+
if (!content) {
|
|
596
|
+
lastError = new Error("fusion_empty");
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
const parsed = JSON.parse(content);
|
|
600
|
+
if (!parsed || typeof parsed.canonical_answer !== "string" || !parsed.canonical_answer.trim()) {
|
|
601
|
+
lastError = new Error("fusion_invalid");
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
const evidenceIds = Array.isArray(parsed.evidence_ids)
|
|
605
|
+
? parsed.evidence_ids.filter(item => typeof item === "string" && item.trim())
|
|
606
|
+
: [];
|
|
607
|
+
return {
|
|
608
|
+
canonical_answer: parsed.canonical_answer.trim().slice(0, 6000),
|
|
609
|
+
coverage_note: typeof parsed.coverage_note === "string" ? parsed.coverage_note.trim().slice(0, 1200) : "",
|
|
610
|
+
facts: Array.isArray(parsed.facts) ? parsed.facts : [],
|
|
611
|
+
timeline: Array.isArray(parsed.timeline) ? parsed.timeline : [],
|
|
612
|
+
entities: Array.isArray(parsed.entities) ? parsed.entities : [],
|
|
613
|
+
decisions: Array.isArray(parsed.decisions) ? parsed.decisions : [],
|
|
614
|
+
fixes: Array.isArray(parsed.fixes) ? parsed.fixes : [],
|
|
615
|
+
preferences: Array.isArray(parsed.preferences) ? parsed.preferences : [],
|
|
616
|
+
risks: Array.isArray(parsed.risks) ? parsed.risks : [],
|
|
617
|
+
action_items: Array.isArray(parsed.action_items) ? parsed.action_items : [],
|
|
618
|
+
conflicts: Array.isArray(parsed.conflicts) ? parsed.conflicts : [],
|
|
619
|
+
evidence_ids: evidenceIds,
|
|
620
|
+
confidence: typeof parsed.confidence === "number"
|
|
621
|
+
? Math.max(0, Math.min(1, parsed.confidence))
|
|
622
|
+
: 0.5,
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
catch (error) {
|
|
626
|
+
clearTimeout(timeoutId);
|
|
627
|
+
lastError = error;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError || "fusion_failed"));
|
|
631
|
+
}
|
|
163
632
|
function createReadStore(options) {
|
|
164
633
|
const memoryRoot = options.dbPath ? path.resolve(options.dbPath) : path.join(options.projectRoot, "data", "memory");
|
|
634
|
+
const vectorFallbackPath = path.join(memoryRoot, "vector", "lancedb_events.jsonl");
|
|
635
|
+
const hitStatsPath = path.join(memoryRoot, ".read_hit_stats.json");
|
|
636
|
+
function loadHitStats() {
|
|
637
|
+
try {
|
|
638
|
+
if (!fs.existsSync(hitStatsPath)) {
|
|
639
|
+
return { items: {} };
|
|
640
|
+
}
|
|
641
|
+
const content = fs.readFileSync(hitStatsPath, "utf-8").trim();
|
|
642
|
+
if (!content) {
|
|
643
|
+
return { items: {} };
|
|
644
|
+
}
|
|
645
|
+
const parsed = JSON.parse(content);
|
|
646
|
+
if (!parsed || typeof parsed !== "object" || !parsed.items || typeof parsed.items !== "object") {
|
|
647
|
+
return { items: {} };
|
|
648
|
+
}
|
|
649
|
+
return parsed;
|
|
650
|
+
}
|
|
651
|
+
catch {
|
|
652
|
+
return { items: {} };
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
function saveHitStats(state) {
|
|
656
|
+
try {
|
|
657
|
+
const dir = path.dirname(hitStatsPath);
|
|
658
|
+
if (!fs.existsSync(dir)) {
|
|
659
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
660
|
+
}
|
|
661
|
+
fs.writeFileSync(hitStatsPath, JSON.stringify(state, null, 2), "utf-8");
|
|
662
|
+
}
|
|
663
|
+
catch (error) {
|
|
664
|
+
options.logger.warn(`Failed to persist read hit stats: ${error}`);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
function markHit(ids) {
|
|
668
|
+
if (!ids.length) {
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
const state = loadHitStats();
|
|
672
|
+
const now = new Date().toISOString();
|
|
673
|
+
for (const id of ids) {
|
|
674
|
+
const key = (id || "").trim();
|
|
675
|
+
if (!key)
|
|
676
|
+
continue;
|
|
677
|
+
const prev = state.items[key];
|
|
678
|
+
state.items[key] = {
|
|
679
|
+
count: (prev?.count || 0) + 1,
|
|
680
|
+
lastHitAt: now,
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
const entries = Object.entries(state.items)
|
|
684
|
+
.sort((a, b) => {
|
|
685
|
+
const ta = Date.parse(a[1].lastHitAt || "");
|
|
686
|
+
const tb = Date.parse(b[1].lastHitAt || "");
|
|
687
|
+
return (Number.isFinite(tb) ? tb : 0) - (Number.isFinite(ta) ? ta : 0);
|
|
688
|
+
})
|
|
689
|
+
.slice(0, 20000);
|
|
690
|
+
state.items = Object.fromEntries(entries);
|
|
691
|
+
saveHitStats(state);
|
|
692
|
+
}
|
|
165
693
|
function loadAllDocuments() {
|
|
166
694
|
const cortexRulesPath = path.join(memoryRoot, "CORTEX_RULES.md");
|
|
167
695
|
const memoryMdPath = path.join(memoryRoot, "MEMORY.md");
|
|
@@ -180,36 +708,275 @@ function createReadStore(options) {
|
|
|
180
708
|
return { results: [] };
|
|
181
709
|
}
|
|
182
710
|
const docs = loadAllDocuments();
|
|
183
|
-
const
|
|
711
|
+
const hitStats = loadHitStats();
|
|
712
|
+
const intent = classifyIntent(query);
|
|
713
|
+
const preferredTypes = preferredEventTypes(intent);
|
|
714
|
+
let queryEmbedding = null;
|
|
715
|
+
const embeddingModel = options.embedding?.model || "";
|
|
716
|
+
const embeddingApiKey = options.embedding?.apiKey || "";
|
|
717
|
+
const embeddingBaseUrl = normalizeBaseUrl(options.embedding?.baseURL || options.embedding?.baseUrl);
|
|
718
|
+
if (embeddingModel && embeddingApiKey && embeddingBaseUrl) {
|
|
719
|
+
try {
|
|
720
|
+
queryEmbedding = await requestEmbedding({
|
|
721
|
+
text: query,
|
|
722
|
+
model: embeddingModel,
|
|
723
|
+
apiKey: embeddingApiKey,
|
|
724
|
+
baseUrl: embeddingBaseUrl,
|
|
725
|
+
dimensions: options.embedding?.dimensions,
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
catch (error) {
|
|
729
|
+
options.logger.warn(`Embedding query failed, fallback to lexical search: ${error}`);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
const vectorDocsFromLance = queryEmbedding && queryEmbedding.length > 0
|
|
733
|
+
? await searchLanceDb({ memoryRoot, queryEmbedding, limit: Math.max(20, args.topK * 8), logger: options.logger })
|
|
734
|
+
: [];
|
|
735
|
+
const vectorDocsFallback = vectorDocsFromLance.length > 0
|
|
736
|
+
? []
|
|
737
|
+
: parseVectorFallback(vectorFallbackPath, options.logger);
|
|
738
|
+
const vectorDocs = [...vectorDocsFromLance, ...vectorDocsFallback];
|
|
739
|
+
const graphDocs = docs
|
|
740
|
+
.filter(doc => Array.isArray(doc.relations) && doc.relations.length > 0)
|
|
184
741
|
.map(doc => {
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
742
|
+
const graphText = [
|
|
743
|
+
doc.text,
|
|
744
|
+
...(doc.relations || []).map(relation => `${relation.source} ${relation.type} ${relation.target}`),
|
|
745
|
+
].join(" | ");
|
|
746
|
+
return {
|
|
747
|
+
...doc,
|
|
748
|
+
text: graphText,
|
|
749
|
+
};
|
|
750
|
+
});
|
|
751
|
+
const rulesDocs = docs.filter(doc => doc.source === "CORTEX_RULES.md");
|
|
752
|
+
const archiveDocs = docs.filter(doc => doc.source.startsWith("sessions_"));
|
|
753
|
+
const combinedCandidates = [];
|
|
754
|
+
const channels = {
|
|
755
|
+
rules: [],
|
|
756
|
+
archive: [],
|
|
757
|
+
vector: [],
|
|
758
|
+
graph: [],
|
|
759
|
+
};
|
|
760
|
+
const evaluateDoc = (doc, source) => {
|
|
761
|
+
const lexical = scoreText(query, doc.text);
|
|
762
|
+
const semantic = queryEmbedding && Array.isArray(doc.embedding) && doc.embedding.length > 0
|
|
763
|
+
? Math.max(0, cosineSimilarity(queryEmbedding, doc.embedding) * 5)
|
|
764
|
+
: 0;
|
|
765
|
+
if (lexical <= 0 && semantic <= 0) {
|
|
766
|
+
return null;
|
|
767
|
+
}
|
|
768
|
+
const recency = recencyScore(doc.timestamp);
|
|
769
|
+
const quality = typeof doc.qualityScore === "number" ? Math.max(0, Math.min(1, doc.qualityScore)) : 0.5;
|
|
770
|
+
const typeMatch = preferredTypes.length > 0 && doc.eventType
|
|
771
|
+
? (preferredTypes.includes(doc.eventType) ? 1 : 0)
|
|
772
|
+
: 0.5;
|
|
773
|
+
const graphMatch = source === "graph" ? 1 : 0;
|
|
774
|
+
const baseWeighted = (0.2 * lexical +
|
|
775
|
+
0.3 * semantic +
|
|
776
|
+
0.1 * recency +
|
|
777
|
+
0.15 * quality +
|
|
778
|
+
0.15 * typeMatch +
|
|
779
|
+
0.1 * graphMatch) * sourceWeight(source, intent);
|
|
780
|
+
const decayFactor = computeDecayFactor(doc.id, doc.eventType, doc.timestamp, options.memoryDecay, hitStats);
|
|
781
|
+
const weighted = baseWeighted * decayFactor;
|
|
782
|
+
return {
|
|
783
|
+
doc,
|
|
784
|
+
source,
|
|
785
|
+
lexical,
|
|
786
|
+
semantic,
|
|
787
|
+
recency,
|
|
788
|
+
quality,
|
|
789
|
+
typeMatch,
|
|
790
|
+
graphMatch,
|
|
791
|
+
decayFactor,
|
|
792
|
+
weighted,
|
|
793
|
+
};
|
|
794
|
+
};
|
|
795
|
+
for (const doc of rulesDocs) {
|
|
796
|
+
const candidate = evaluateDoc(doc, "rules");
|
|
797
|
+
if (candidate)
|
|
798
|
+
channels.rules.push(candidate);
|
|
799
|
+
}
|
|
800
|
+
for (const doc of archiveDocs) {
|
|
801
|
+
const candidate = evaluateDoc(doc, "archive");
|
|
802
|
+
if (candidate)
|
|
803
|
+
channels.archive.push(candidate);
|
|
804
|
+
}
|
|
805
|
+
for (const doc of vectorDocs) {
|
|
806
|
+
const candidate = evaluateDoc(doc, "vector");
|
|
807
|
+
if (candidate)
|
|
808
|
+
channels.vector.push(candidate);
|
|
809
|
+
}
|
|
810
|
+
for (const doc of graphDocs) {
|
|
811
|
+
const candidate = evaluateDoc(doc, "graph");
|
|
812
|
+
if (candidate)
|
|
813
|
+
channels.graph.push(candidate);
|
|
814
|
+
}
|
|
815
|
+
for (const key of Object.keys(channels)) {
|
|
816
|
+
channels[key].sort((a, b) => b.weighted - a.weighted);
|
|
817
|
+
combinedCandidates.push(...channels[key].slice(0, Math.max(20, args.topK * 5)));
|
|
818
|
+
}
|
|
819
|
+
const rrfMap = new Map();
|
|
820
|
+
const weightedMap = new Map();
|
|
821
|
+
const rrfK = 60;
|
|
822
|
+
for (const key of Object.keys(channels)) {
|
|
823
|
+
const list = channels[key];
|
|
824
|
+
for (let i = 0; i < list.length; i += 1) {
|
|
825
|
+
const candidate = list[i];
|
|
826
|
+
const rrf = 1 / (rrfK + i + 1);
|
|
827
|
+
rrfMap.set(candidate.doc.id, (rrfMap.get(candidate.doc.id) || 0) + rrf);
|
|
828
|
+
const current = weightedMap.get(candidate.doc.id);
|
|
829
|
+
if (!current || candidate.weighted > current.weighted) {
|
|
830
|
+
weightedMap.set(candidate.doc.id, candidate);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
const preRanked = [...weightedMap.values()]
|
|
835
|
+
.map(candidate => ({
|
|
836
|
+
id: candidate.doc.id,
|
|
837
|
+
text: candidate.doc.text,
|
|
838
|
+
source: candidate.doc.source,
|
|
839
|
+
event_type: candidate.doc.eventType || "",
|
|
840
|
+
quality_score: candidate.quality,
|
|
841
|
+
timestamp: candidate.doc.timestamp ? new Date(candidate.doc.timestamp).toISOString() : "",
|
|
842
|
+
score: candidate.weighted + (rrfMap.get(candidate.doc.id) || 0) * 1.5,
|
|
843
|
+
reason_tags: [
|
|
844
|
+
`intent:${intent.toLowerCase()}`,
|
|
845
|
+
candidate.semantic > 0 ? "vector_hit" : "lexical_hit",
|
|
846
|
+
candidate.typeMatch >= 1 ? "event_type_match" : "event_type_weak",
|
|
847
|
+
candidate.recency >= 0.8 ? "recent" : "historical",
|
|
848
|
+
candidate.quality >= 0.7 ? "high_quality" : "normal_quality",
|
|
849
|
+
candidate.decayFactor < 1 ? `decay:${candidate.decayFactor.toFixed(3)}` : "decay:1.000",
|
|
850
|
+
`source:${candidate.source}`,
|
|
851
|
+
],
|
|
852
|
+
}))
|
|
190
853
|
.sort((a, b) => b.score - a.score)
|
|
191
|
-
.slice(0, Math.max(1, args.topK))
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
854
|
+
.slice(0, Math.max(1, Math.max(args.topK, 20)));
|
|
855
|
+
const lexicalRanked = preRanked
|
|
856
|
+
.map(doc => {
|
|
857
|
+
const boost = withRecencyBoost(doc.score, doc.timestamp ? Date.parse(doc.timestamp) : undefined);
|
|
858
|
+
return { ...doc, score: Number(boost.toFixed(4)) };
|
|
859
|
+
});
|
|
860
|
+
const rerankerModel = options.reranker?.model || "";
|
|
861
|
+
const rerankerApiKey = options.reranker?.apiKey || "";
|
|
862
|
+
const rerankerBaseUrl = normalizeBaseUrl(options.reranker?.baseURL || options.reranker?.baseUrl);
|
|
863
|
+
let rerankedSimple = lexicalRanked.map(item => ({
|
|
864
|
+
id: item.id,
|
|
865
|
+
text: item.text,
|
|
866
|
+
source: item.source,
|
|
867
|
+
score: item.score,
|
|
197
868
|
}));
|
|
869
|
+
if (rerankerModel && rerankerApiKey && rerankerBaseUrl && lexicalRanked.length > 1) {
|
|
870
|
+
try {
|
|
871
|
+
rerankedSimple = await requestRerank({
|
|
872
|
+
query,
|
|
873
|
+
candidates: lexicalRanked.map(item => ({ id: item.id, text: item.text, source: item.source, score: item.score })),
|
|
874
|
+
model: rerankerModel,
|
|
875
|
+
apiKey: rerankerApiKey,
|
|
876
|
+
baseUrl: rerankerBaseUrl,
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
catch (error) {
|
|
880
|
+
options.logger.warn(`Reranker failed, keep hybrid ranking: ${error}`);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
const ranked = rerankedSimple.slice(0, Math.max(1, args.topK)).map(item => {
|
|
884
|
+
const hit = lexicalRanked.find(entry => entry.id === item.id);
|
|
885
|
+
return {
|
|
886
|
+
id: item.id,
|
|
887
|
+
text: item.text,
|
|
888
|
+
source: item.source,
|
|
889
|
+
event_type: hit?.event_type || "",
|
|
890
|
+
quality_score: hit?.quality_score ?? 0,
|
|
891
|
+
timestamp: hit?.timestamp || "",
|
|
892
|
+
score: Number(item.score.toFixed(4)),
|
|
893
|
+
reason_tags: Array.isArray(hit?.reason_tags) ? hit?.reason_tags : [],
|
|
894
|
+
};
|
|
895
|
+
});
|
|
896
|
+
const fusionEnabled = options.fusion?.enabled !== false;
|
|
897
|
+
const llmModel = options.llm?.model || "";
|
|
898
|
+
const llmApiKey = options.llm?.apiKey || "";
|
|
899
|
+
const llmBaseUrl = normalizeBaseUrl(options.llm?.baseURL || options.llm?.baseUrl);
|
|
900
|
+
if (fusionEnabled && llmModel && llmApiKey && llmBaseUrl && ranked.length > 1) {
|
|
901
|
+
try {
|
|
902
|
+
const maxCandidates = Math.max(4, Math.min(20, options.fusion?.maxCandidates ?? 10));
|
|
903
|
+
const fusion = await requestFusion({
|
|
904
|
+
query,
|
|
905
|
+
candidates: ranked.slice(0, maxCandidates).map(item => ({
|
|
906
|
+
id: item.id,
|
|
907
|
+
text: item.text,
|
|
908
|
+
source: item.source,
|
|
909
|
+
event_type: item.event_type,
|
|
910
|
+
quality_score: item.quality_score,
|
|
911
|
+
timestamp: item.timestamp,
|
|
912
|
+
score: item.score,
|
|
913
|
+
reason_tags: Array.isArray(item.reason_tags) ? item.reason_tags : [],
|
|
914
|
+
})),
|
|
915
|
+
llm: {
|
|
916
|
+
model: llmModel,
|
|
917
|
+
apiKey: llmApiKey,
|
|
918
|
+
baseUrl: llmBaseUrl,
|
|
919
|
+
},
|
|
920
|
+
});
|
|
921
|
+
if (fusion && fusion.canonical_answer) {
|
|
922
|
+
const fusedItem = {
|
|
923
|
+
id: `fusion_${Date.now().toString(36)}`,
|
|
924
|
+
text: fusion.canonical_answer,
|
|
925
|
+
source: "llm_fusion",
|
|
926
|
+
event_type: "fusion",
|
|
927
|
+
quality_score: Number(fusion.confidence.toFixed(4)),
|
|
928
|
+
timestamp: new Date().toISOString(),
|
|
929
|
+
score: Number((Math.max(...ranked.map(item => item.score)) + 1).toFixed(4)),
|
|
930
|
+
reason_tags: ["llm_fused_authoritative", `evidence:${fusion.evidence_ids.length}`],
|
|
931
|
+
fused_coverage_note: fusion.coverage_note || "",
|
|
932
|
+
fused_facts: fusion.facts,
|
|
933
|
+
fused_timeline: fusion.timeline || [],
|
|
934
|
+
fused_entities: fusion.entities || [],
|
|
935
|
+
fused_decisions: fusion.decisions || [],
|
|
936
|
+
fused_fixes: fusion.fixes || [],
|
|
937
|
+
fused_preferences: fusion.preferences || [],
|
|
938
|
+
fused_risks: fusion.risks || [],
|
|
939
|
+
fused_action_items: fusion.action_items || [],
|
|
940
|
+
fused_conflicts: fusion.conflicts,
|
|
941
|
+
fused_evidence_ids: fusion.evidence_ids,
|
|
942
|
+
};
|
|
943
|
+
const authoritative = options.fusion?.authoritative !== false;
|
|
944
|
+
if (authoritative) {
|
|
945
|
+
markHit(Array.isArray(fusion.evidence_ids) ? fusion.evidence_ids : []);
|
|
946
|
+
return { results: [fusedItem] };
|
|
947
|
+
}
|
|
948
|
+
const merged = [fusedItem, ...ranked];
|
|
949
|
+
markHit([
|
|
950
|
+
...(Array.isArray(fusion.evidence_ids) ? fusion.evidence_ids : []),
|
|
951
|
+
...ranked.map(item => item.id),
|
|
952
|
+
]);
|
|
953
|
+
return { results: merged.slice(0, Math.max(1, args.topK)) };
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
catch (error) {
|
|
957
|
+
options.logger.warn(`LLM fusion failed, fallback to reranked results: ${error}`);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
markHit(ranked.map(item => item.id));
|
|
198
961
|
return { results: ranked };
|
|
199
962
|
}
|
|
200
963
|
async function getHotContext(args) {
|
|
201
964
|
const limit = Math.max(1, args.limit);
|
|
202
965
|
const docs = loadAllDocuments();
|
|
203
966
|
const coreRules = docs.find(doc => doc.source === "CORTEX_RULES.md");
|
|
204
|
-
const
|
|
205
|
-
.filter(doc => doc.source
|
|
967
|
+
const archiveDocs = docs
|
|
968
|
+
.filter(doc => doc.source === "sessions_archive")
|
|
206
969
|
.sort((a, b) => (b.timestamp ?? 0) - (a.timestamp ?? 0))
|
|
207
970
|
.slice(0, limit);
|
|
971
|
+
const issueFixPairs = docs
|
|
972
|
+
.filter(doc => doc.source === "sessions_archive" && (doc.eventType === "issue" || doc.eventType === "fix"))
|
|
973
|
+
.sort((a, b) => (b.timestamp ?? 0) - (a.timestamp ?? 0))
|
|
974
|
+
.slice(0, 2);
|
|
208
975
|
const result = [];
|
|
209
976
|
if (coreRules) {
|
|
210
977
|
result.push({ id: coreRules.id, text: coreRules.text, source: coreRules.source });
|
|
211
978
|
}
|
|
212
|
-
for (const doc of
|
|
979
|
+
for (const doc of [...issueFixPairs, ...archiveDocs]) {
|
|
213
980
|
result.push({ id: doc.id, text: doc.text, source: doc.source });
|
|
214
981
|
}
|
|
215
982
|
return { context: result.slice(0, limit) };
|
|
@@ -223,6 +990,20 @@ function createReadStore(options) {
|
|
|
223
990
|
age_seconds: args.cachedAutoSearch.ageSeconds,
|
|
224
991
|
};
|
|
225
992
|
}
|
|
993
|
+
if (!result.auto_search) {
|
|
994
|
+
const docs = loadAllDocuments()
|
|
995
|
+
.filter(doc => doc.source === "sessions_archive" && doc.sessionId === args.sessionId)
|
|
996
|
+
.sort((a, b) => (b.timestamp ?? 0) - (a.timestamp ?? 0));
|
|
997
|
+
const latest = docs[0];
|
|
998
|
+
if (latest && latest.text.trim()) {
|
|
999
|
+
const light = await searchMemory({ query: latest.text.slice(0, 80), topK: 3 });
|
|
1000
|
+
result.auto_search = {
|
|
1001
|
+
query: latest.text.slice(0, 80),
|
|
1002
|
+
results: light.results,
|
|
1003
|
+
age_seconds: 0,
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
226
1007
|
if (args.includeHot) {
|
|
227
1008
|
const hot = await getHotContext({ limit: 20 });
|
|
228
1009
|
result.hot_context = hot.context;
|