openclaw-cortex-memory 0.1.0-Alpha.3 → 0.1.0-Alpha.31
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/LICENSE +21 -0
- package/README.md +296 -203
- package/SIGNATURE.md +7 -0
- package/SKILL.md +92 -268
- package/dist/index.d.ts +100 -22
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1249 -1252
- package/dist/index.js.map +1 -1
- package/dist/openclaw.plugin.json +501 -16
- 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 +224 -0
- package/dist/src/dedup/three_stage_deduplicator.js.map +1 -0
- package/dist/src/engine/memory_engine.d.ts +6 -1
- package/dist/src/engine/memory_engine.d.ts.map +1 -1
- package/dist/src/engine/ts_engine.d.ts +242 -0
- package/dist/src/engine/ts_engine.d.ts.map +1 -1
- package/dist/src/engine/ts_engine.js +1468 -52
- package/dist/src/engine/ts_engine.js.map +1 -1
- package/dist/src/engine/types.d.ts +29 -0
- package/dist/src/engine/types.d.ts.map +1 -1
- package/dist/src/graph/ontology.d.ts +125 -0
- package/dist/src/graph/ontology.d.ts.map +1 -0
- package/dist/src/graph/ontology.js +1237 -0
- package/dist/src/graph/ontology.js.map +1 -0
- package/dist/src/net/http_post.d.ts +17 -0
- package/dist/src/net/http_post.d.ts.map +1 -0
- package/dist/src/net/http_post.js +56 -0
- package/dist/src/net/http_post.js.map +1 -0
- package/dist/src/quality/llm_output_validator.d.ts +66 -0
- package/dist/src/quality/llm_output_validator.d.ts.map +1 -0
- package/dist/src/quality/llm_output_validator.js +659 -0
- package/dist/src/quality/llm_output_validator.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 +352 -8
- package/dist/src/reflect/reflector.js.map +1 -1
- package/dist/src/rules/rule_store.d.ts.map +1 -1
- package/dist/src/rules/rule_store.js +75 -16
- package/dist/src/rules/rule_store.js.map +1 -1
- package/dist/src/session/session_end.d.ts +33 -0
- package/dist/src/session/session_end.d.ts.map +1 -1
- package/dist/src/session/session_end.js +67 -64
- package/dist/src/session/session_end.js.map +1 -1
- package/dist/src/store/archive_store.d.ts +136 -0
- package/dist/src/store/archive_store.d.ts.map +1 -0
- package/dist/src/store/archive_store.js +635 -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/graph_memory_store.d.ts +114 -0
- package/dist/src/store/graph_memory_store.d.ts.map +1 -0
- package/dist/src/store/graph_memory_store.js +841 -0
- package/dist/src/store/graph_memory_store.js.map +1 -0
- package/dist/src/store/read_store.d.ts +89 -0
- package/dist/src/store/read_store.d.ts.map +1 -1
- package/dist/src/store/read_store.js +2459 -28
- package/dist/src/store/read_store.js.map +1 -1
- package/dist/src/store/vector_store.d.ts +45 -0
- package/dist/src/store/vector_store.d.ts.map +1 -0
- package/dist/src/store/vector_store.js +202 -0
- package/dist/src/store/vector_store.js.map +1 -0
- package/dist/src/store/write_store.d.ts +54 -0
- package/dist/src/store/write_store.d.ts.map +1 -1
- package/dist/src/store/write_store.js +284 -6
- package/dist/src/store/write_store.js.map +1 -1
- package/dist/src/sync/session_sync.d.ts +119 -2
- package/dist/src/sync/session_sync.d.ts.map +1 -1
- package/dist/src/sync/session_sync.js +2377 -31
- 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/dist/src/wiki/wiki_linter.d.ts +25 -0
- package/dist/src/wiki/wiki_linter.d.ts.map +1 -0
- package/dist/src/wiki/wiki_linter.js +268 -0
- package/dist/src/wiki/wiki_linter.js.map +1 -0
- package/dist/src/wiki/wiki_logger.d.ts +10 -0
- package/dist/src/wiki/wiki_logger.d.ts.map +1 -0
- package/dist/src/wiki/wiki_logger.js +78 -0
- package/dist/src/wiki/wiki_logger.js.map +1 -0
- package/dist/src/wiki/wiki_maintainer.d.ts +36 -0
- package/dist/src/wiki/wiki_maintainer.d.ts.map +1 -0
- package/dist/src/wiki/wiki_maintainer.js +38 -0
- package/dist/src/wiki/wiki_maintainer.js.map +1 -0
- package/dist/src/wiki/wiki_projector.d.ts +33 -0
- package/dist/src/wiki/wiki_projector.d.ts.map +1 -0
- package/dist/src/wiki/wiki_projector.js +633 -0
- package/dist/src/wiki/wiki_projector.js.map +1 -0
- package/dist/src/wiki/wiki_queue.d.ts +29 -0
- package/dist/src/wiki/wiki_queue.d.ts.map +1 -0
- package/dist/src/wiki/wiki_queue.js +137 -0
- package/dist/src/wiki/wiki_queue.js.map +1 -0
- package/openclaw.plugin.json +501 -16
- package/package.json +58 -7
- package/schema/graph.schema.yaml +330 -0
- package/scripts/cli.js +19 -14
- package/scripts/repair-memory.js +321 -0
- package/scripts/uninstall.js +22 -5
- package/skills/cortex-memory/SKILL.md +49 -0
- package/skills/cortex-memory/references/agent-manual.md +115 -0
- package/skills/cortex-memory/references/configuration.md +92 -0
- package/skills/cortex-memory/references/publish-checklist.md +46 -0
- package/skills/cortex-memory/references/system-prompt-template.md +27 -0
- package/skills/cortex-memory/references/tools.md +181 -0
- package/skills/cortex-memory/scripts/smoke-check.ps1 +56 -0
- package/index.ts +0 -2142
|
@@ -36,6 +36,185 @@ 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");
|
|
40
|
+
const http_post_1 = require("../net/http_post");
|
|
41
|
+
function graphRelationKey(relation) {
|
|
42
|
+
const source = (relation.source || "").trim().toLowerCase();
|
|
43
|
+
const type = (relation.type || "related_to").trim().toLowerCase();
|
|
44
|
+
const target = (relation.target || "").trim().toLowerCase();
|
|
45
|
+
return `${source}|${type}|${target}`;
|
|
46
|
+
}
|
|
47
|
+
function buildEntityGraphSummaryDocs(graphDocs) {
|
|
48
|
+
const entityEdges = new Map();
|
|
49
|
+
const entityLatestTs = new Map();
|
|
50
|
+
const entitySession = new Map();
|
|
51
|
+
for (const doc of graphDocs) {
|
|
52
|
+
const ts = typeof doc.timestamp === "number" ? doc.timestamp : 0;
|
|
53
|
+
const sessionId = typeof doc.sessionId === "string" ? doc.sessionId : "";
|
|
54
|
+
const relations = Array.isArray(doc.relations) ? doc.relations : [];
|
|
55
|
+
for (const relation of relations) {
|
|
56
|
+
const source = (relation.source || "").trim();
|
|
57
|
+
const target = (relation.target || "").trim();
|
|
58
|
+
const type = (relation.type || "").trim();
|
|
59
|
+
if (!source || !target || !type)
|
|
60
|
+
continue;
|
|
61
|
+
if (!entityEdges.has(source))
|
|
62
|
+
entityEdges.set(source, []);
|
|
63
|
+
if (!entityEdges.has(target))
|
|
64
|
+
entityEdges.set(target, []);
|
|
65
|
+
entityEdges.get(source)?.push({ source, target, type });
|
|
66
|
+
entityEdges.get(target)?.push({ source, target, type });
|
|
67
|
+
entityLatestTs.set(source, Math.max(entityLatestTs.get(source) || 0, ts));
|
|
68
|
+
entityLatestTs.set(target, Math.max(entityLatestTs.get(target) || 0, ts));
|
|
69
|
+
if (sessionId) {
|
|
70
|
+
if (!entitySession.has(source))
|
|
71
|
+
entitySession.set(source, sessionId);
|
|
72
|
+
if (!entitySession.has(target))
|
|
73
|
+
entitySession.set(target, sessionId);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const output = [];
|
|
78
|
+
for (const [entity, edges] of entityEdges.entries()) {
|
|
79
|
+
if (!edges.length)
|
|
80
|
+
continue;
|
|
81
|
+
const outgoing = edges.filter(edge => edge.source === entity);
|
|
82
|
+
const incoming = edges.filter(edge => edge.target === entity);
|
|
83
|
+
const typeCounter = new Map();
|
|
84
|
+
for (const edge of edges) {
|
|
85
|
+
typeCounter.set(edge.type, (typeCounter.get(edge.type) || 0) + 1);
|
|
86
|
+
}
|
|
87
|
+
const typeSummary = [...typeCounter.entries()]
|
|
88
|
+
.sort((a, b) => b[1] - a[1])
|
|
89
|
+
.map(([type, count]) => `${type}:${count}`)
|
|
90
|
+
.join(", ");
|
|
91
|
+
const sortedOutgoing = [...outgoing].sort((a, b) => a.type.localeCompare(b.type));
|
|
92
|
+
const sortedIncoming = [...incoming].sort((a, b) => a.type.localeCompare(b.type));
|
|
93
|
+
const cappedOutgoing = sortedOutgoing.slice(0, 20);
|
|
94
|
+
const cappedIncoming = sortedIncoming.slice(0, 20);
|
|
95
|
+
const relationFacts = edges
|
|
96
|
+
.slice(0, 40)
|
|
97
|
+
.map(edge => `${edge.source} ${edge.type} ${edge.target}`)
|
|
98
|
+
.join(" | ");
|
|
99
|
+
const outgoingBlock = cappedOutgoing.length > 0
|
|
100
|
+
? cappedOutgoing.map((edge, index) => `${index + 1}. ${edge.source} -[${edge.type}]-> ${edge.target}`).join("\n")
|
|
101
|
+
: "none";
|
|
102
|
+
const incomingBlock = cappedIncoming.length > 0
|
|
103
|
+
? cappedIncoming.map((edge, index) => `${index + 1}. ${edge.source} -[${edge.type}]-> ${edge.target}`).join("\n")
|
|
104
|
+
: "none";
|
|
105
|
+
const summaryText = [
|
|
106
|
+
`# Graph Entity Summary`,
|
|
107
|
+
`entity: ${entity}`,
|
|
108
|
+
``,
|
|
109
|
+
`## Stats`,
|
|
110
|
+
`relation_total: ${edges.length}`,
|
|
111
|
+
`outgoing_total: ${outgoing.length}`,
|
|
112
|
+
`incoming_total: ${incoming.length}`,
|
|
113
|
+
typeSummary ? `relation_type_distribution: ${typeSummary}` : "relation_type_distribution: none",
|
|
114
|
+
``,
|
|
115
|
+
`## Outgoing Relations`,
|
|
116
|
+
outgoingBlock,
|
|
117
|
+
outgoing.length > cappedOutgoing.length ? `...truncated_outgoing: ${outgoing.length - cappedOutgoing.length}` : "",
|
|
118
|
+
``,
|
|
119
|
+
`## Incoming Relations`,
|
|
120
|
+
incomingBlock,
|
|
121
|
+
incoming.length > cappedIncoming.length ? `...truncated_incoming: ${incoming.length - cappedIncoming.length}` : "",
|
|
122
|
+
``,
|
|
123
|
+
`## Relation Facts`,
|
|
124
|
+
relationFacts || "none",
|
|
125
|
+
].filter(Boolean).join("\n");
|
|
126
|
+
output.push({
|
|
127
|
+
id: `gph_entity_${entity.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fa5]+/gi, "_")}`,
|
|
128
|
+
text: summaryText,
|
|
129
|
+
source: "sessions_graph_entity",
|
|
130
|
+
timestamp: (entityLatestTs.get(entity) || 0) > 0 ? entityLatestTs.get(entity) : undefined,
|
|
131
|
+
layer: "archive",
|
|
132
|
+
sourceMemoryId: entity,
|
|
133
|
+
sessionId: entitySession.get(entity) || undefined,
|
|
134
|
+
entities: [entity],
|
|
135
|
+
relations: edges,
|
|
136
|
+
eventType: "graph_summary",
|
|
137
|
+
qualityScore: 1,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return output;
|
|
141
|
+
}
|
|
142
|
+
const DEFAULT_READ_TUNING = {
|
|
143
|
+
scoring: {
|
|
144
|
+
lexicalWeight: 0.2,
|
|
145
|
+
bm25Scale: 2,
|
|
146
|
+
semanticWeight: 0.3,
|
|
147
|
+
recencyWeight: 0.1,
|
|
148
|
+
qualityWeight: 0.15,
|
|
149
|
+
typeMatchWeight: 0.15,
|
|
150
|
+
graphMatchWeight: 0.1,
|
|
151
|
+
},
|
|
152
|
+
rrf: {
|
|
153
|
+
k: 60,
|
|
154
|
+
weight: 1.5,
|
|
155
|
+
},
|
|
156
|
+
recency: {
|
|
157
|
+
buckets: [
|
|
158
|
+
{ maxAgeHours: 12, score: 1, bonus: 0.6 },
|
|
159
|
+
{ maxAgeHours: 24, score: 0.8, bonus: 0.6 },
|
|
160
|
+
{ maxAgeHours: 72, score: 0.6, bonus: 0.3 },
|
|
161
|
+
{ maxAgeHours: 168, score: 0.4, bonus: 0.3 },
|
|
162
|
+
{ maxAgeHours: 720, score: 0.2, bonus: 0 },
|
|
163
|
+
{ maxAgeHours: Number.POSITIVE_INFINITY, score: 0.05, bonus: 0 },
|
|
164
|
+
],
|
|
165
|
+
},
|
|
166
|
+
autoContext: {
|
|
167
|
+
queryMaxChars: 80,
|
|
168
|
+
lightweightSearch: true,
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
function resolveReadTuning(options) {
|
|
172
|
+
const configuredBuckets = Array.isArray(options?.recency?.buckets)
|
|
173
|
+
? options?.recency?.buckets
|
|
174
|
+
.filter(item => item &&
|
|
175
|
+
Number.isFinite(item.maxAgeHours) &&
|
|
176
|
+
item.maxAgeHours > 0 &&
|
|
177
|
+
Number.isFinite(item.score) &&
|
|
178
|
+
item.score >= 0 &&
|
|
179
|
+
Number.isFinite(item.bonus))
|
|
180
|
+
.map(item => ({
|
|
181
|
+
maxAgeHours: item.maxAgeHours,
|
|
182
|
+
score: Math.max(0, item.score),
|
|
183
|
+
bonus: Math.max(0, item.bonus),
|
|
184
|
+
}))
|
|
185
|
+
: [];
|
|
186
|
+
const sortedBuckets = configuredBuckets
|
|
187
|
+
.sort((a, b) => a.maxAgeHours - b.maxAgeHours);
|
|
188
|
+
const buckets = sortedBuckets.length > 0 ? sortedBuckets : DEFAULT_READ_TUNING.recency.buckets;
|
|
189
|
+
const numberOr = (value, fallback, min) => {
|
|
190
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < min) {
|
|
191
|
+
return fallback;
|
|
192
|
+
}
|
|
193
|
+
return value;
|
|
194
|
+
};
|
|
195
|
+
return {
|
|
196
|
+
scoring: {
|
|
197
|
+
lexicalWeight: numberOr(options?.scoring?.lexicalWeight, DEFAULT_READ_TUNING.scoring.lexicalWeight, 0),
|
|
198
|
+
bm25Scale: numberOr(options?.scoring?.bm25Scale, DEFAULT_READ_TUNING.scoring.bm25Scale, 0),
|
|
199
|
+
semanticWeight: numberOr(options?.scoring?.semanticWeight, DEFAULT_READ_TUNING.scoring.semanticWeight, 0),
|
|
200
|
+
recencyWeight: numberOr(options?.scoring?.recencyWeight, DEFAULT_READ_TUNING.scoring.recencyWeight, 0),
|
|
201
|
+
qualityWeight: numberOr(options?.scoring?.qualityWeight, DEFAULT_READ_TUNING.scoring.qualityWeight, 0),
|
|
202
|
+
typeMatchWeight: numberOr(options?.scoring?.typeMatchWeight, DEFAULT_READ_TUNING.scoring.typeMatchWeight, 0),
|
|
203
|
+
graphMatchWeight: numberOr(options?.scoring?.graphMatchWeight, DEFAULT_READ_TUNING.scoring.graphMatchWeight, 0),
|
|
204
|
+
},
|
|
205
|
+
rrf: {
|
|
206
|
+
k: Math.floor(numberOr(options?.rrf?.k, DEFAULT_READ_TUNING.rrf.k, 1)),
|
|
207
|
+
weight: numberOr(options?.rrf?.weight, DEFAULT_READ_TUNING.rrf.weight, 0),
|
|
208
|
+
},
|
|
209
|
+
recency: {
|
|
210
|
+
buckets,
|
|
211
|
+
},
|
|
212
|
+
autoContext: {
|
|
213
|
+
queryMaxChars: Math.floor(numberOr(options?.autoContext?.queryMaxChars, DEFAULT_READ_TUNING.autoContext.queryMaxChars, 20)),
|
|
214
|
+
lightweightSearch: options?.autoContext?.lightweightSearch !== false,
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
39
218
|
function safeReadFile(filePath) {
|
|
40
219
|
try {
|
|
41
220
|
if (!fs.existsSync(filePath)) {
|
|
@@ -65,7 +244,64 @@ function scoreText(query, text) {
|
|
|
65
244
|
}
|
|
66
245
|
return score;
|
|
67
246
|
}
|
|
247
|
+
function tokenize(text) {
|
|
248
|
+
return text
|
|
249
|
+
.toLowerCase()
|
|
250
|
+
.split(/[^a-z0-9\u4e00-\u9fa5]+/i)
|
|
251
|
+
.map(token => token.trim())
|
|
252
|
+
.filter(Boolean);
|
|
253
|
+
}
|
|
254
|
+
function buildBm25Stats(docs, queryTerms, getTokens) {
|
|
255
|
+
const docFreq = new Map();
|
|
256
|
+
let totalLen = 0;
|
|
257
|
+
for (const doc of docs) {
|
|
258
|
+
const tokens = typeof getTokens === "function" ? getTokens(doc) : tokenize(doc.text);
|
|
259
|
+
totalLen += tokens.length;
|
|
260
|
+
if (queryTerms.length === 0) {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
const termSet = new Set(tokens);
|
|
264
|
+
for (const term of queryTerms) {
|
|
265
|
+
if (termSet.has(term)) {
|
|
266
|
+
docFreq.set(term, (docFreq.get(term) || 0) + 1);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const avgDocLen = docs.length > 0 ? Math.max(1, totalLen / docs.length) : 1;
|
|
271
|
+
return { avgDocLen, docFreq };
|
|
272
|
+
}
|
|
273
|
+
function bm25Score(args) {
|
|
274
|
+
const tokens = Array.isArray(args.docTokens) ? args.docTokens : tokenize(args.docText);
|
|
275
|
+
if (tokens.length === 0 || args.queryTerms.length === 0 || args.docCount <= 0) {
|
|
276
|
+
return 0;
|
|
277
|
+
}
|
|
278
|
+
const termFreq = new Map();
|
|
279
|
+
for (const token of tokens) {
|
|
280
|
+
termFreq.set(token, (termFreq.get(token) || 0) + 1);
|
|
281
|
+
}
|
|
282
|
+
const k1 = 1.2;
|
|
283
|
+
const b = 0.75;
|
|
284
|
+
let score = 0;
|
|
285
|
+
for (const term of args.queryTerms) {
|
|
286
|
+
const tf = termFreq.get(term) || 0;
|
|
287
|
+
if (tf <= 0)
|
|
288
|
+
continue;
|
|
289
|
+
const df = args.docFreq.get(term) || 0;
|
|
290
|
+
const idf = Math.log(1 + ((args.docCount - df + 0.5) / (df + 0.5)));
|
|
291
|
+
const denominator = tf + k1 * (1 - b + b * (tokens.length / Math.max(1, args.avgDocLen)));
|
|
292
|
+
score += idf * (((k1 + 1) * tf) / Math.max(1e-6, denominator));
|
|
293
|
+
}
|
|
294
|
+
return score;
|
|
295
|
+
}
|
|
68
296
|
function normalizeRecordText(record) {
|
|
297
|
+
const summary = typeof record.summary === "string" ? record.summary.trim() : "";
|
|
298
|
+
const sourceText = typeof record.source_text === "string" ? record.source_text.trim() : "";
|
|
299
|
+
if (summary && sourceText) {
|
|
300
|
+
return [
|
|
301
|
+
`summary: ${summary}`,
|
|
302
|
+
`source_text: ${sourceText}`,
|
|
303
|
+
].join("\n");
|
|
304
|
+
}
|
|
69
305
|
const direct = [record.content, record.summary, record.text, record.message]
|
|
70
306
|
.find(v => typeof v === "string" && v.trim());
|
|
71
307
|
if (direct) {
|
|
@@ -108,7 +344,18 @@ function parseJsonlFile(filePath, sourceLabel, logger) {
|
|
|
108
344
|
}
|
|
109
345
|
try {
|
|
110
346
|
const parsed = JSON.parse(trimmed);
|
|
111
|
-
const
|
|
347
|
+
const summaryText = typeof parsed.summary === "string" ? parsed.summary.trim() : "";
|
|
348
|
+
const causeText = typeof parsed.cause === "string" ? parsed.cause.trim() : "";
|
|
349
|
+
const processText = typeof parsed.process === "string" ? parsed.process.trim() : "";
|
|
350
|
+
const resultText = typeof parsed.result === "string" ? parsed.result.trim() : "";
|
|
351
|
+
const sourceText = typeof parsed.source_text === "string" ? parsed.source_text.trim() : "";
|
|
352
|
+
const activeContent = typeof parsed.content === "string" ? parsed.content.trim() : "";
|
|
353
|
+
const text = [
|
|
354
|
+
summaryText,
|
|
355
|
+
causeText ? `cause: ${causeText}` : "",
|
|
356
|
+
processText ? `process: ${processText}` : "",
|
|
357
|
+
resultText ? `result: ${resultText}` : "",
|
|
358
|
+
].filter(Boolean).join("\n") || normalizeRecordText(parsed);
|
|
112
359
|
if (!text.trim()) {
|
|
113
360
|
continue;
|
|
114
361
|
}
|
|
@@ -117,8 +364,29 @@ function parseJsonlFile(filePath, sourceLabel, logger) {
|
|
|
117
364
|
docs.push({
|
|
118
365
|
id,
|
|
119
366
|
text,
|
|
367
|
+
summaryText: summaryText || text,
|
|
368
|
+
sourceText: sourceText || activeContent || undefined,
|
|
369
|
+
sourceEventId: typeof parsed.source_event_id === "string" ? parsed.source_event_id : undefined,
|
|
370
|
+
sourceFile: typeof parsed.source_file === "string" ? parsed.source_file : undefined,
|
|
120
371
|
source: sourceLabel,
|
|
121
372
|
timestamp: Number.isFinite(timestampValue) ? timestampValue : undefined,
|
|
373
|
+
layer: parsed.layer === "active" || parsed.layer === "archive"
|
|
374
|
+
? parsed.layer
|
|
375
|
+
: (sourceLabel === "sessions_active" ? "active" : (sourceLabel === "sessions_archive" ? "archive" : undefined)),
|
|
376
|
+
sourceMemoryId: typeof parsed.source_memory_id === "string"
|
|
377
|
+
? parsed.source_memory_id
|
|
378
|
+
: id,
|
|
379
|
+
sourceMemoryCanonicalId: typeof parsed.source_memory_canonical_id === "string"
|
|
380
|
+
? parsed.source_memory_canonical_id
|
|
381
|
+
: (typeof parsed.canonical_id === "string" ? parsed.canonical_id : undefined),
|
|
382
|
+
embedding: Array.isArray(parsed.embedding) ? parsed.embedding.filter(item => Number.isFinite(item)) : undefined,
|
|
383
|
+
eventType: typeof parsed.event_type === "string" ? parsed.event_type.trim() : undefined,
|
|
384
|
+
qualityScore: typeof parsed.quality_score === "number" ? parsed.quality_score : undefined,
|
|
385
|
+
charCount: typeof parsed.char_count === "number" ? parsed.char_count : undefined,
|
|
386
|
+
tokenCount: typeof parsed.token_count === "number" ? parsed.token_count : undefined,
|
|
387
|
+
sessionId: typeof parsed.session_id === "string" ? parsed.session_id : undefined,
|
|
388
|
+
entities: [],
|
|
389
|
+
relations: [],
|
|
122
390
|
});
|
|
123
391
|
}
|
|
124
392
|
catch (error) {
|
|
@@ -147,69 +415,2213 @@ function parseMarkdownFile(filePath, sourceLabel) {
|
|
|
147
415
|
},
|
|
148
416
|
];
|
|
149
417
|
}
|
|
150
|
-
function
|
|
418
|
+
function normalizeFactStatus(value) {
|
|
419
|
+
const token = (value || "").trim().toLowerCase();
|
|
420
|
+
if (!token)
|
|
421
|
+
return null;
|
|
422
|
+
if (token === "active")
|
|
423
|
+
return "active";
|
|
424
|
+
if (token === "pending" || token === "pending_conflict")
|
|
425
|
+
return "pending_conflict";
|
|
426
|
+
if (token === "superseded")
|
|
427
|
+
return "superseded";
|
|
428
|
+
if (token === "rejected")
|
|
429
|
+
return "rejected";
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
function uniqueStrings(values) {
|
|
433
|
+
const output = [];
|
|
434
|
+
const seen = new Set();
|
|
435
|
+
for (const value of values) {
|
|
436
|
+
const key = (value || "").trim();
|
|
437
|
+
if (!key || seen.has(key))
|
|
438
|
+
continue;
|
|
439
|
+
seen.add(key);
|
|
440
|
+
output.push(key);
|
|
441
|
+
}
|
|
442
|
+
return output;
|
|
443
|
+
}
|
|
444
|
+
function buildGraphEvidenceIds(args) {
|
|
445
|
+
const relationKey = graphRelationKey({
|
|
446
|
+
source: args.source,
|
|
447
|
+
target: args.target,
|
|
448
|
+
type: args.type,
|
|
449
|
+
});
|
|
450
|
+
const evidenceIds = [
|
|
451
|
+
relationKey ? `graph:relation:${relationKey}` : "",
|
|
452
|
+
args.sourceEventId ? `graph:event:${args.sourceEventId}` : "",
|
|
453
|
+
args.evidenceSpan
|
|
454
|
+
? `graph:evidence:${args.sourceEventId || relationKey}`
|
|
455
|
+
: "",
|
|
456
|
+
args.wikiRef
|
|
457
|
+
? `wiki:${args.wikiRef}${args.wikiAnchor ? `#${args.wikiAnchor}` : ""}`
|
|
458
|
+
: "",
|
|
459
|
+
];
|
|
460
|
+
return uniqueStrings(evidenceIds);
|
|
461
|
+
}
|
|
462
|
+
function toMemoryRelativePath(memoryRoot, filePath) {
|
|
463
|
+
return path.relative(memoryRoot, filePath).replace(/\\/g, "/");
|
|
464
|
+
}
|
|
465
|
+
function toAnchorToken(value) {
|
|
466
|
+
const token = (value || "")
|
|
467
|
+
.trim()
|
|
468
|
+
.toLowerCase()
|
|
469
|
+
.replace(/[^a-z0-9\u4e00-\u9fa5]+/gi, "-")
|
|
470
|
+
.replace(/^-+|-+$/g, "");
|
|
471
|
+
return token || "facts";
|
|
472
|
+
}
|
|
473
|
+
function parseWikiRelationLine(line) {
|
|
474
|
+
const text = line.trim();
|
|
475
|
+
if (!text.startsWith("- "))
|
|
476
|
+
return null;
|
|
477
|
+
if (text === "- (none)")
|
|
478
|
+
return null;
|
|
479
|
+
const body = text.replace(/^-+\s*/, "");
|
|
480
|
+
const matched = body.match(/^(.+?)\s+--([^\/]+)\/([^-\s]+)-->\s+(.+?)\s*(?:\((.*)\))?$/);
|
|
481
|
+
if (!matched) {
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
const source = matched[1].trim();
|
|
485
|
+
const type = matched[2].trim();
|
|
486
|
+
const status = normalizeFactStatus(matched[3]);
|
|
487
|
+
const target = matched[4].trim();
|
|
488
|
+
const attrs = (matched[5] || "").trim();
|
|
489
|
+
if (!source || !target || !type) {
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
const attributeMap = new Map();
|
|
493
|
+
if (attrs) {
|
|
494
|
+
const parts = attrs.split(",").map(item => item.trim()).filter(Boolean);
|
|
495
|
+
for (const part of parts) {
|
|
496
|
+
const eq = part.indexOf("=");
|
|
497
|
+
if (eq <= 0)
|
|
498
|
+
continue;
|
|
499
|
+
const key = part.slice(0, eq).trim().toLowerCase();
|
|
500
|
+
const value = part.slice(eq + 1).trim();
|
|
501
|
+
if (!key)
|
|
502
|
+
continue;
|
|
503
|
+
attributeMap.set(key, value);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
const evidenceSpanRaw = attributeMap.get("evidence") || "";
|
|
507
|
+
const confidenceRaw = attributeMap.get("confidence") || "";
|
|
508
|
+
const sourceEventIdRaw = attributeMap.get("source_event_id") || "";
|
|
509
|
+
const conflictIdRaw = attributeMap.get("conflict_id") || "";
|
|
510
|
+
const confidenceNum = confidenceRaw ? Number(confidenceRaw) : NaN;
|
|
511
|
+
const relationKey = graphRelationKey({ source, target, type });
|
|
512
|
+
return {
|
|
513
|
+
relation: {
|
|
514
|
+
source,
|
|
515
|
+
target,
|
|
516
|
+
type,
|
|
517
|
+
evidence_span: evidenceSpanRaw && evidenceSpanRaw.toLowerCase() !== "n/a" ? evidenceSpanRaw : undefined,
|
|
518
|
+
confidence: Number.isFinite(confidenceNum) ? confidenceNum : undefined,
|
|
519
|
+
fact_status: status || undefined,
|
|
520
|
+
source_event_id: sourceEventIdRaw && sourceEventIdRaw.toLowerCase() !== "n/a" ? sourceEventIdRaw : undefined,
|
|
521
|
+
conflict_id: conflictIdRaw && conflictIdRaw.toLowerCase() !== "n/a" ? conflictIdRaw : undefined,
|
|
522
|
+
relation_key: relationKey,
|
|
523
|
+
},
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
function parseWikiProjectionDocuments(memoryRoot, logger) {
|
|
527
|
+
const wikiRoot = path.join(memoryRoot, "wiki");
|
|
528
|
+
const folders = [
|
|
529
|
+
{ dir: path.join(wikiRoot, "entities"), kind: "entity" },
|
|
530
|
+
{ dir: path.join(wikiRoot, "topics"), kind: "topic" },
|
|
531
|
+
{ dir: path.join(wikiRoot, "timelines"), kind: "timeline" },
|
|
532
|
+
];
|
|
533
|
+
const docs = [];
|
|
534
|
+
for (const { dir, kind } of folders) {
|
|
535
|
+
if (!fs.existsSync(dir))
|
|
536
|
+
continue;
|
|
537
|
+
let files = [];
|
|
538
|
+
try {
|
|
539
|
+
files = fs.readdirSync(dir)
|
|
540
|
+
.filter(file => file.toLowerCase().endsWith(".md"))
|
|
541
|
+
.sort((a, b) => a.localeCompare(b));
|
|
542
|
+
}
|
|
543
|
+
catch (error) {
|
|
544
|
+
logger.debug(`Skipping wiki projection directory ${dir}: ${error}`);
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
for (const file of files) {
|
|
548
|
+
const filePath = path.join(dir, file);
|
|
549
|
+
const markdown = safeReadFile(filePath);
|
|
550
|
+
if (!markdown.trim())
|
|
551
|
+
continue;
|
|
552
|
+
const relativePath = toMemoryRelativePath(memoryRoot, filePath);
|
|
553
|
+
const mtime = (() => {
|
|
554
|
+
try {
|
|
555
|
+
return fs.statSync(filePath).mtimeMs;
|
|
556
|
+
}
|
|
557
|
+
catch {
|
|
558
|
+
return NaN;
|
|
559
|
+
}
|
|
560
|
+
})();
|
|
561
|
+
let section = "Facts";
|
|
562
|
+
const sectionCounters = new Map();
|
|
563
|
+
const lines = markdown.split(/\r?\n/);
|
|
564
|
+
for (const rawLine of lines) {
|
|
565
|
+
const line = rawLine.trim();
|
|
566
|
+
if (!line)
|
|
567
|
+
continue;
|
|
568
|
+
if (line.startsWith("## ")) {
|
|
569
|
+
section = line.replace(/^##\s+/, "").trim() || "Facts";
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
const parsed = parseWikiRelationLine(line);
|
|
573
|
+
if (!parsed)
|
|
574
|
+
continue;
|
|
575
|
+
const sectionToken = toAnchorToken(section);
|
|
576
|
+
const index = (sectionCounters.get(sectionToken) || 0) + 1;
|
|
577
|
+
sectionCounters.set(sectionToken, index);
|
|
578
|
+
const anchor = `${sectionToken}-${index}`;
|
|
579
|
+
const relation = parsed.relation;
|
|
580
|
+
const factStatus = relation.fact_status || "active";
|
|
581
|
+
const evidenceIds = buildGraphEvidenceIds({
|
|
582
|
+
source: relation.source,
|
|
583
|
+
target: relation.target,
|
|
584
|
+
type: relation.type,
|
|
585
|
+
sourceEventId: relation.source_event_id,
|
|
586
|
+
evidenceSpan: relation.evidence_span,
|
|
587
|
+
wikiRef: relativePath,
|
|
588
|
+
wikiAnchor: anchor,
|
|
589
|
+
});
|
|
590
|
+
docs.push({
|
|
591
|
+
id: `wiki:${relativePath}#${anchor}`,
|
|
592
|
+
text: [
|
|
593
|
+
`# Wiki Projection`,
|
|
594
|
+
`wiki_kind: ${kind}`,
|
|
595
|
+
`wiki_path: ${relativePath}`,
|
|
596
|
+
`wiki_section: ${section}`,
|
|
597
|
+
`fact_status: ${factStatus}`,
|
|
598
|
+
`${relation.source} ${relation.type} ${relation.target}`,
|
|
599
|
+
line,
|
|
600
|
+
].join("\n"),
|
|
601
|
+
source: "sessions_graph_wiki",
|
|
602
|
+
timestamp: Number.isFinite(mtime) ? Math.floor(mtime) : undefined,
|
|
603
|
+
layer: "archive",
|
|
604
|
+
sourceFile: relativePath,
|
|
605
|
+
sourceMemoryId: relation.relation_key || `wiki:${relativePath}`,
|
|
606
|
+
sourceEventId: relation.source_event_id,
|
|
607
|
+
eventType: "graph_wiki_projection",
|
|
608
|
+
qualityScore: 0.95,
|
|
609
|
+
entities: uniqueStrings([relation.source, relation.target]),
|
|
610
|
+
relations: [relation],
|
|
611
|
+
factStatus,
|
|
612
|
+
wikiRef: relativePath,
|
|
613
|
+
wikiAnchor: anchor,
|
|
614
|
+
evidenceIds,
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return docs;
|
|
620
|
+
}
|
|
621
|
+
function extractPrioritizedRuleLines(text, maxRules) {
|
|
622
|
+
if (!text.trim() || maxRules <= 0) {
|
|
623
|
+
return [];
|
|
624
|
+
}
|
|
625
|
+
const lines = text
|
|
626
|
+
.split(/\r?\n/)
|
|
627
|
+
.map(line => line.trim())
|
|
628
|
+
.filter(Boolean)
|
|
629
|
+
.filter(line => !/^core rules and knowledge extracted/i.test(line))
|
|
630
|
+
.filter(line => !/^core rules\b/i.test(line));
|
|
631
|
+
if (lines.length === 0) {
|
|
632
|
+
return [];
|
|
633
|
+
}
|
|
634
|
+
const dedupedFromTail = [];
|
|
635
|
+
const seen = new Set();
|
|
636
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
637
|
+
const line = lines[i];
|
|
638
|
+
const key = line.toLowerCase().replace(/\s+/g, " ").trim();
|
|
639
|
+
if (!key || seen.has(key)) {
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
seen.add(key);
|
|
643
|
+
dedupedFromTail.push(line);
|
|
644
|
+
}
|
|
645
|
+
const scored = dedupedFromTail.map((line, indexFromTail) => {
|
|
646
|
+
let score = 0;
|
|
647
|
+
if (/(must|should|ensure|avoid|prefer|always|never|fallback|verify|validate|retry|sanitize)/i.test(line)) {
|
|
648
|
+
score += 3;
|
|
649
|
+
}
|
|
650
|
+
if (/(fix|resolved|success|stable|deploy|release|incident|rollback|constraint|decision)/i.test(line)) {
|
|
651
|
+
score += 2;
|
|
652
|
+
}
|
|
653
|
+
if (/(确保|避免|优先|必须|建议|回退|重试|校验|稳定|发布|决策)/.test(line)) {
|
|
654
|
+
score += 2;
|
|
655
|
+
}
|
|
656
|
+
if (line.length >= 30 && line.length <= 220) {
|
|
657
|
+
score += 2;
|
|
658
|
+
}
|
|
659
|
+
else if (line.length > 220) {
|
|
660
|
+
score -= 1;
|
|
661
|
+
}
|
|
662
|
+
if (/[.!?]$/.test(line)) {
|
|
663
|
+
score += 1;
|
|
664
|
+
}
|
|
665
|
+
score += Math.max(0, 2 - indexFromTail * 0.08);
|
|
666
|
+
return { line, score, indexFromTail };
|
|
667
|
+
});
|
|
668
|
+
const selected = scored
|
|
669
|
+
.sort((a, b) => (b.score - a.score) || (a.indexFromTail - b.indexFromTail))
|
|
670
|
+
.slice(0, maxRules)
|
|
671
|
+
.sort((a, b) => a.indexFromTail - b.indexFromTail)
|
|
672
|
+
.map(item => item.line);
|
|
673
|
+
return selected;
|
|
674
|
+
}
|
|
675
|
+
function withRecencyBoost(score, timestamp, buckets) {
|
|
151
676
|
if (!timestamp) {
|
|
152
677
|
return score;
|
|
153
678
|
}
|
|
154
679
|
const ageHours = (Date.now() - timestamp) / (1000 * 60 * 60);
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
return score + 0.3;
|
|
680
|
+
for (const bucket of buckets) {
|
|
681
|
+
if (ageHours <= bucket.maxAgeHours) {
|
|
682
|
+
return score + bucket.bonus;
|
|
683
|
+
}
|
|
160
684
|
}
|
|
161
685
|
return score;
|
|
162
686
|
}
|
|
687
|
+
function recencyScore(timestamp, buckets) {
|
|
688
|
+
if (!timestamp) {
|
|
689
|
+
return 0;
|
|
690
|
+
}
|
|
691
|
+
const ageHours = (Date.now() - timestamp) / (1000 * 60 * 60);
|
|
692
|
+
for (const bucket of buckets) {
|
|
693
|
+
if (ageHours <= bucket.maxAgeHours) {
|
|
694
|
+
return bucket.score;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return 0;
|
|
698
|
+
}
|
|
699
|
+
function eventTypeHalfLifeDays(eventType, options) {
|
|
700
|
+
const fallback = typeof options?.defaultHalfLifeDays === "number" && options.defaultHalfLifeDays > 0
|
|
701
|
+
? options.defaultHalfLifeDays
|
|
702
|
+
: 90;
|
|
703
|
+
const type = (eventType || "").trim().toLowerCase();
|
|
704
|
+
if (!type)
|
|
705
|
+
return fallback;
|
|
706
|
+
const configured = options?.halfLifeByEventType || {};
|
|
707
|
+
if (typeof configured[type] === "number" && configured[type] > 0) {
|
|
708
|
+
return configured[type];
|
|
709
|
+
}
|
|
710
|
+
if (["issue", "fix", "action_item", "blocker"].includes(type))
|
|
711
|
+
return 30;
|
|
712
|
+
if (["plan", "milestone", "follow_up"].includes(type))
|
|
713
|
+
return 60;
|
|
714
|
+
if (["decision", "insight", "retrospective"].includes(type))
|
|
715
|
+
return 120;
|
|
716
|
+
if (["preference", "constraint", "requirement", "dependency", "assumption"].includes(type))
|
|
717
|
+
return 240;
|
|
718
|
+
return fallback;
|
|
719
|
+
}
|
|
720
|
+
function computeAntiDecayBoost(id, hitStats, options) {
|
|
721
|
+
const anti = options?.antiDecay;
|
|
722
|
+
if (anti?.enabled === false) {
|
|
723
|
+
return 1;
|
|
724
|
+
}
|
|
725
|
+
const item = hitStats.items[id];
|
|
726
|
+
if (!item) {
|
|
727
|
+
return 1;
|
|
728
|
+
}
|
|
729
|
+
const hitWeight = typeof anti?.hitWeight === "number" && anti.hitWeight > 0 ? anti.hitWeight : 0.08;
|
|
730
|
+
const maxBoost = typeof anti?.maxBoost === "number" && anti.maxBoost >= 1 ? anti.maxBoost : 1.6;
|
|
731
|
+
const recentWindowDays = typeof anti?.recentWindowDays === "number" && anti.recentWindowDays > 0 ? anti.recentWindowDays : 30;
|
|
732
|
+
const lastHitTs = Date.parse(item.lastHitAt || "");
|
|
733
|
+
const ageDays = Number.isFinite(lastHitTs) ? Math.max(0, (Date.now() - lastHitTs) / (1000 * 60 * 60 * 24)) : recentWindowDays * 2;
|
|
734
|
+
const freshness = ageDays <= recentWindowDays ? (1 - ageDays / recentWindowDays) : 0;
|
|
735
|
+
const countFactor = Math.log1p(Math.max(0, item.count));
|
|
736
|
+
const boost = 1 + countFactor * hitWeight * (0.5 + 0.5 * freshness);
|
|
737
|
+
return Math.min(maxBoost, Math.max(1, boost));
|
|
738
|
+
}
|
|
739
|
+
function computeDecayFactor(id, eventType, timestamp, options, hitStats) {
|
|
740
|
+
const enabled = options?.enabled !== false;
|
|
741
|
+
if (!enabled || !timestamp) {
|
|
742
|
+
return computeAntiDecayBoost(id, hitStats, options);
|
|
743
|
+
}
|
|
744
|
+
const ageDays = Math.max(0, (Date.now() - timestamp) / (1000 * 60 * 60 * 24));
|
|
745
|
+
const halfLife = eventTypeHalfLifeDays(eventType, options);
|
|
746
|
+
const base = Math.pow(2, -ageDays / Math.max(1, halfLife));
|
|
747
|
+
const floor = typeof options?.minFloor === "number"
|
|
748
|
+
? Math.max(0, Math.min(1, options.minFloor))
|
|
749
|
+
: 0.15;
|
|
750
|
+
const decay = Math.max(floor, base);
|
|
751
|
+
const boost = computeAntiDecayBoost(id, hitStats, options);
|
|
752
|
+
return Math.min(1, decay * boost);
|
|
753
|
+
}
|
|
754
|
+
function normalizeBaseUrl(value) {
|
|
755
|
+
if (!value)
|
|
756
|
+
return "";
|
|
757
|
+
return value.endsWith("/") ? value.slice(0, -1) : value;
|
|
758
|
+
}
|
|
759
|
+
const READ_FUSION_PROMPT_VERSION = "read-fusion.v1.2.0";
|
|
760
|
+
const READ_FUSION_REGRESSION_SAMPLES = [
|
|
761
|
+
"Example A: if archive and vector refer to the same source_memory_id, keep one main conclusion and keep the rest as supporting evidence.",
|
|
762
|
+
"Example B: if conclusions conflict, write conflicts and explain prioritization in canonical_answer (time, quality, explicitness).",
|
|
763
|
+
"Example C: if summary/excerpt is insufficient, return event ids in need_fulltext_event_ids for full-text lookup.",
|
|
764
|
+
];
|
|
765
|
+
function cosineSimilarity(left, right) {
|
|
766
|
+
if (left.length === 0 || right.length === 0) {
|
|
767
|
+
return 0;
|
|
768
|
+
}
|
|
769
|
+
const size = Math.min(left.length, right.length);
|
|
770
|
+
let dot = 0;
|
|
771
|
+
let leftNorm = 0;
|
|
772
|
+
let rightNorm = 0;
|
|
773
|
+
for (let i = 0; i < size; i += 1) {
|
|
774
|
+
const a = left[i];
|
|
775
|
+
const b = right[i];
|
|
776
|
+
dot += a * b;
|
|
777
|
+
leftNorm += a * a;
|
|
778
|
+
rightNorm += b * b;
|
|
779
|
+
}
|
|
780
|
+
if (leftNorm === 0 || rightNorm === 0) {
|
|
781
|
+
return 0;
|
|
782
|
+
}
|
|
783
|
+
return dot / (Math.sqrt(leftNorm) * Math.sqrt(rightNorm));
|
|
784
|
+
}
|
|
785
|
+
async function requestEmbedding(args) {
|
|
786
|
+
const endpoint = args.baseUrl.endsWith("/embeddings") ? args.baseUrl : `${args.baseUrl}/embeddings`;
|
|
787
|
+
const body = {
|
|
788
|
+
input: args.text,
|
|
789
|
+
model: args.model,
|
|
790
|
+
};
|
|
791
|
+
if (typeof args.dimensions === "number" && Number.isFinite(args.dimensions) && args.dimensions > 0) {
|
|
792
|
+
body.dimensions = args.dimensions;
|
|
793
|
+
}
|
|
794
|
+
let lastError = null;
|
|
795
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
796
|
+
const response = await (0, http_post_1.postJsonWithTimeout)({
|
|
797
|
+
endpoint,
|
|
798
|
+
apiKey: args.apiKey,
|
|
799
|
+
body,
|
|
800
|
+
timeoutMs: 10000,
|
|
801
|
+
});
|
|
802
|
+
if (!response.ok) {
|
|
803
|
+
lastError = new Error(response.status > 0 ? `embedding_http_${response.status}` : (response.error || "embedding_network_error"));
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
try {
|
|
807
|
+
const json = (response.json || {});
|
|
808
|
+
const embedding = json?.data?.[0]?.embedding;
|
|
809
|
+
if (Array.isArray(embedding) && embedding.length > 0) {
|
|
810
|
+
return embedding.filter(item => Number.isFinite(item));
|
|
811
|
+
}
|
|
812
|
+
lastError = new Error("embedding_empty");
|
|
813
|
+
}
|
|
814
|
+
catch (error) {
|
|
815
|
+
lastError = error;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
if (lastError) {
|
|
819
|
+
throw lastError;
|
|
820
|
+
}
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
async function requestRerank(args) {
|
|
824
|
+
const endpoint = args.baseUrl.endsWith("/rerank") ? args.baseUrl : `${args.baseUrl}/rerank`;
|
|
825
|
+
const documents = args.candidates.map(item => item.text);
|
|
826
|
+
const body = {
|
|
827
|
+
model: args.model,
|
|
828
|
+
query: args.query,
|
|
829
|
+
documents,
|
|
830
|
+
top_n: args.candidates.length,
|
|
831
|
+
};
|
|
832
|
+
let lastError = null;
|
|
833
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
834
|
+
const response = await (0, http_post_1.postJsonWithTimeout)({
|
|
835
|
+
endpoint,
|
|
836
|
+
apiKey: args.apiKey,
|
|
837
|
+
body,
|
|
838
|
+
timeoutMs: 12000,
|
|
839
|
+
});
|
|
840
|
+
if (!response.ok) {
|
|
841
|
+
lastError = new Error(response.status > 0 ? `rerank_http_${response.status}` : (response.error || "rerank_network_error"));
|
|
842
|
+
continue;
|
|
843
|
+
}
|
|
844
|
+
try {
|
|
845
|
+
const json = (response.json || {});
|
|
846
|
+
const list = Array.isArray(json.results) ? json.results : (Array.isArray(json.data) ? json.data : []);
|
|
847
|
+
if (!Array.isArray(list) || list.length === 0) {
|
|
848
|
+
lastError = new Error("rerank_empty");
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
const mapped = list
|
|
852
|
+
.map((item, rank) => {
|
|
853
|
+
const index = typeof item.index === "number" ? item.index : rank;
|
|
854
|
+
const hit = args.candidates[index];
|
|
855
|
+
if (!hit)
|
|
856
|
+
return null;
|
|
857
|
+
const score = typeof item.relevance_score === "number" ? item.relevance_score : (typeof item.score === "number" ? item.score : hit.score);
|
|
858
|
+
return { ...hit, score };
|
|
859
|
+
})
|
|
860
|
+
.filter((item) => Boolean(item));
|
|
861
|
+
if (mapped.length > 0) {
|
|
862
|
+
return mapped;
|
|
863
|
+
}
|
|
864
|
+
lastError = new Error("rerank_map_empty");
|
|
865
|
+
}
|
|
866
|
+
catch (error) {
|
|
867
|
+
lastError = error;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError || "rerank_failed"));
|
|
871
|
+
}
|
|
872
|
+
function classifyIntent(query) {
|
|
873
|
+
const text = query.toLowerCase();
|
|
874
|
+
const relationHints = /(关系|依赖|关联|上下游|图谱|拓扑|graph|relation|entity|dependency)/i;
|
|
875
|
+
if (relationHints.test(text))
|
|
876
|
+
return "RELATION_DISCOVERY";
|
|
877
|
+
const troubleHints = /(报错|错误|异常|失败|超时|无法|崩溃|故障|修复|bug|error|failed|timeout|fix)/i;
|
|
878
|
+
if (troubleHints.test(text))
|
|
879
|
+
return "TROUBLESHOOTING";
|
|
880
|
+
const preferenceHints = /(偏好|习惯|口味|喜欢|不喜欢|偏向|preference)/i;
|
|
881
|
+
if (preferenceHints.test(text))
|
|
882
|
+
return "PREFERENCE_PROFILE";
|
|
883
|
+
const timelineHints = /(最近|上次|之前|时间线|历史|timeline|history)/i;
|
|
884
|
+
if (timelineHints.test(text))
|
|
885
|
+
return "TIMELINE_REVIEW";
|
|
886
|
+
const decisionHints = /(方案|决策|选择|建议|取舍|权衡|tradeoff|plan)/i;
|
|
887
|
+
if (decisionHints.test(text))
|
|
888
|
+
return "DECISION_SUPPORT";
|
|
889
|
+
return "FACT_LOOKUP";
|
|
890
|
+
}
|
|
891
|
+
function preferredEventTypes(intent) {
|
|
892
|
+
if (intent === "TROUBLESHOOTING")
|
|
893
|
+
return ["issue", "fix", "risk", "blocker", "dependency", "retrospective"];
|
|
894
|
+
if (intent === "PREFERENCE_PROFILE")
|
|
895
|
+
return ["preference", "decision", "constraint", "requirement"];
|
|
896
|
+
if (intent === "DECISION_SUPPORT")
|
|
897
|
+
return ["decision", "plan", "insight", "assumption", "constraint", "requirement"];
|
|
898
|
+
if (intent === "TIMELINE_REVIEW")
|
|
899
|
+
return ["action_item", "follow_up", "milestone", "plan", "decision", "issue", "fix"];
|
|
900
|
+
return [];
|
|
901
|
+
}
|
|
902
|
+
function sourceWeight(source, intent) {
|
|
903
|
+
if (source === "rules") {
|
|
904
|
+
return intent === "DECISION_SUPPORT" || intent === "TROUBLESHOOTING" ? 1.15 : 0.9;
|
|
905
|
+
}
|
|
906
|
+
if (source === "graph") {
|
|
907
|
+
return intent === "RELATION_DISCOVERY" ? 1.25 : 0.85;
|
|
908
|
+
}
|
|
909
|
+
if (source === "vector") {
|
|
910
|
+
return 1.05;
|
|
911
|
+
}
|
|
912
|
+
return 1;
|
|
913
|
+
}
|
|
914
|
+
const QUERY_PLAN_STOPWORDS = new Set([
|
|
915
|
+
"what",
|
|
916
|
+
"is",
|
|
917
|
+
"the",
|
|
918
|
+
"a",
|
|
919
|
+
"an",
|
|
920
|
+
"of",
|
|
921
|
+
"for",
|
|
922
|
+
"to",
|
|
923
|
+
"in",
|
|
924
|
+
"on",
|
|
925
|
+
"and",
|
|
926
|
+
"or",
|
|
927
|
+
"with",
|
|
928
|
+
"about",
|
|
929
|
+
"this",
|
|
930
|
+
"that",
|
|
931
|
+
"这些",
|
|
932
|
+
"那些",
|
|
933
|
+
"这个",
|
|
934
|
+
"那个",
|
|
935
|
+
"之前",
|
|
936
|
+
"提到",
|
|
937
|
+
"提过",
|
|
938
|
+
"一下",
|
|
939
|
+
"请问",
|
|
940
|
+
"关于",
|
|
941
|
+
"相关",
|
|
942
|
+
"什么",
|
|
943
|
+
"怎么",
|
|
944
|
+
"如何",
|
|
945
|
+
"是否",
|
|
946
|
+
"有没有",
|
|
947
|
+
"是什么",
|
|
948
|
+
"哪些",
|
|
949
|
+
"哪个",
|
|
950
|
+
]);
|
|
951
|
+
function planQueryKeywords(query) {
|
|
952
|
+
const normalized = (query || "").trim().replace(/\s+/g, " ");
|
|
953
|
+
if (!normalized)
|
|
954
|
+
return [];
|
|
955
|
+
const output = [];
|
|
956
|
+
const seen = new Set();
|
|
957
|
+
const push = (value) => {
|
|
958
|
+
const item = (value || "").trim();
|
|
959
|
+
if (!item)
|
|
960
|
+
return;
|
|
961
|
+
const key = item.toLowerCase();
|
|
962
|
+
if (seen.has(key))
|
|
963
|
+
return;
|
|
964
|
+
seen.add(key);
|
|
965
|
+
output.push(item);
|
|
966
|
+
};
|
|
967
|
+
push(normalized);
|
|
968
|
+
for (const match of normalized.matchAll(/["“”'‘’]([^"“”'‘’]{2,64})["“”'‘’]/g)) {
|
|
969
|
+
push(match[1]);
|
|
970
|
+
}
|
|
971
|
+
for (const match of normalized.match(/https?:\/\/[^\s]+/gi) || []) {
|
|
972
|
+
push(match);
|
|
973
|
+
}
|
|
974
|
+
for (const match of normalized.matchAll(/\b[A-Za-z0-9][A-Za-z0-9._/-]*(?:\s+[A-Za-z0-9][A-Za-z0-9._/-]*){0,3}\b/g)) {
|
|
975
|
+
const phrase = match[0].trim();
|
|
976
|
+
const words = phrase.split(/\s+/).filter(Boolean);
|
|
977
|
+
const hasStrongSignal = words.length >= 2 || /[A-Z]{2,}/.test(phrase);
|
|
978
|
+
if (hasStrongSignal && !QUERY_PLAN_STOPWORDS.has(phrase.toLowerCase())) {
|
|
979
|
+
push(phrase);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
const normalizedForSplit = normalized
|
|
983
|
+
.replace(/[(){}\[\]<>]/g, " ")
|
|
984
|
+
.replace(/[,。!?;:,.!?;:、]/g, " ")
|
|
985
|
+
.replace(/(之前提到的|之前提到|提到的|提到|请问|一下|是什么|什么|哪个|哪篇|哪里|如何|怎么|有关|关于|以及|还有|并且|然后|能否|可以|帮我|给我)/g, " ");
|
|
986
|
+
for (const raw of normalizedForSplit.split(/[\/\-\s]+/)) {
|
|
987
|
+
const token = raw.trim();
|
|
988
|
+
if (!token || token.length < 2)
|
|
989
|
+
continue;
|
|
990
|
+
if (QUERY_PLAN_STOPWORDS.has(token.toLowerCase()))
|
|
991
|
+
continue;
|
|
992
|
+
push(token);
|
|
993
|
+
}
|
|
994
|
+
return output.slice(0, 5);
|
|
995
|
+
}
|
|
996
|
+
function shouldTriggerFulltextFallback(ranked, topK) {
|
|
997
|
+
const scoped = ranked.slice(0, Math.max(1, topK));
|
|
998
|
+
if (scoped.length === 0)
|
|
999
|
+
return true;
|
|
1000
|
+
const summaryHits = scoped.filter(item => Array.isArray(item.reason_tags) && item.reason_tags.includes("summary_hit")).length;
|
|
1001
|
+
return summaryHits === 0;
|
|
1002
|
+
}
|
|
1003
|
+
function mergeKeyFromDoc(doc) {
|
|
1004
|
+
const canonical = typeof doc.sourceMemoryCanonicalId === "string" ? doc.sourceMemoryCanonicalId.trim() : "";
|
|
1005
|
+
if (canonical) {
|
|
1006
|
+
return `canonical:${canonical}`;
|
|
1007
|
+
}
|
|
1008
|
+
const sourceMemoryId = typeof doc.sourceMemoryId === "string" ? doc.sourceMemoryId.trim() : "";
|
|
1009
|
+
if (sourceMemoryId) {
|
|
1010
|
+
return `source:${sourceMemoryId}`;
|
|
1011
|
+
}
|
|
1012
|
+
return `id:${doc.id}`;
|
|
1013
|
+
}
|
|
1014
|
+
function docFactStatus(doc) {
|
|
1015
|
+
const direct = normalizeFactStatus(typeof doc.factStatus === "string" ? doc.factStatus : "");
|
|
1016
|
+
if (direct)
|
|
1017
|
+
return direct;
|
|
1018
|
+
const relation = Array.isArray(doc.relations) && doc.relations.length > 0 ? doc.relations[0] : null;
|
|
1019
|
+
const relationStatus = normalizeFactStatus(typeof relation?.fact_status === "string" ? relation.fact_status : "");
|
|
1020
|
+
if (relationStatus)
|
|
1021
|
+
return relationStatus;
|
|
1022
|
+
return "active";
|
|
1023
|
+
}
|
|
1024
|
+
function docEvidenceIds(doc) {
|
|
1025
|
+
if (Array.isArray(doc.evidenceIds) && doc.evidenceIds.length > 0) {
|
|
1026
|
+
return uniqueStrings(doc.evidenceIds);
|
|
1027
|
+
}
|
|
1028
|
+
const relation = Array.isArray(doc.relations) && doc.relations.length > 0 ? doc.relations[0] : null;
|
|
1029
|
+
const source = relation?.source || "";
|
|
1030
|
+
const target = relation?.target || "";
|
|
1031
|
+
const type = relation?.type || "";
|
|
1032
|
+
const evidence = source && target && type
|
|
1033
|
+
? buildGraphEvidenceIds({
|
|
1034
|
+
source,
|
|
1035
|
+
target,
|
|
1036
|
+
type,
|
|
1037
|
+
sourceEventId: relation?.source_event_id || doc.sourceEventId,
|
|
1038
|
+
evidenceSpan: relation?.evidence_span,
|
|
1039
|
+
wikiRef: doc.wikiRef,
|
|
1040
|
+
wikiAnchor: doc.wikiAnchor,
|
|
1041
|
+
})
|
|
1042
|
+
: [];
|
|
1043
|
+
return uniqueStrings([
|
|
1044
|
+
...evidence,
|
|
1045
|
+
doc.sourceEventId ? `graph:event:${doc.sourceEventId}` : "",
|
|
1046
|
+
doc.wikiRef ? `wiki:${doc.wikiRef}${doc.wikiAnchor ? `#${doc.wikiAnchor}` : ""}` : "",
|
|
1047
|
+
`doc:${doc.id}`,
|
|
1048
|
+
]);
|
|
1049
|
+
}
|
|
1050
|
+
function customChannelWeight(source, options) {
|
|
1051
|
+
const weights = options?.channelWeights;
|
|
1052
|
+
if (!weights)
|
|
1053
|
+
return 1;
|
|
1054
|
+
const value = weights[source];
|
|
1055
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
1056
|
+
return 1;
|
|
1057
|
+
}
|
|
1058
|
+
return value;
|
|
1059
|
+
}
|
|
1060
|
+
function lengthNormalizeFactor(doc, options) {
|
|
1061
|
+
const lengthNorm = options?.lengthNorm;
|
|
1062
|
+
if (lengthNorm?.enabled === false) {
|
|
1063
|
+
return 1;
|
|
1064
|
+
}
|
|
1065
|
+
const pivotChars = typeof lengthNorm?.pivotChars === "number" && lengthNorm.pivotChars > 0
|
|
1066
|
+
? lengthNorm.pivotChars
|
|
1067
|
+
: 1200;
|
|
1068
|
+
const strength = typeof lengthNorm?.strength === "number" && lengthNorm.strength > 0
|
|
1069
|
+
? lengthNorm.strength
|
|
1070
|
+
: 0.75;
|
|
1071
|
+
const minFactor = typeof lengthNorm?.minFactor === "number" && lengthNorm.minFactor > 0 && lengthNorm.minFactor <= 1
|
|
1072
|
+
? lengthNorm.minFactor
|
|
1073
|
+
: 0.45;
|
|
1074
|
+
const charCount = typeof doc.charCount === "number" && Number.isFinite(doc.charCount)
|
|
1075
|
+
? doc.charCount
|
|
1076
|
+
: doc.text.length;
|
|
1077
|
+
if (charCount <= pivotChars) {
|
|
1078
|
+
return 1;
|
|
1079
|
+
}
|
|
1080
|
+
const over = (charCount - pivotChars) / pivotChars;
|
|
1081
|
+
const factor = 1 / (1 + over * strength);
|
|
1082
|
+
return Math.max(minFactor, Math.min(1, factor));
|
|
1083
|
+
}
|
|
1084
|
+
function channelQuota(source, topK, options) {
|
|
1085
|
+
const configured = options?.channelTopK?.[source];
|
|
1086
|
+
if (typeof configured === "number" && Number.isFinite(configured) && configured >= 1) {
|
|
1087
|
+
return Math.floor(configured);
|
|
1088
|
+
}
|
|
1089
|
+
if (source === "rules")
|
|
1090
|
+
return Math.max(6, topK * 2);
|
|
1091
|
+
if (source === "graph")
|
|
1092
|
+
return Math.max(8, topK * 3);
|
|
1093
|
+
return Math.max(12, topK * 4);
|
|
1094
|
+
}
|
|
1095
|
+
function parseJsonStringArray(value) {
|
|
1096
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
1097
|
+
return [];
|
|
1098
|
+
}
|
|
1099
|
+
try {
|
|
1100
|
+
const parsed = JSON.parse(value);
|
|
1101
|
+
if (!Array.isArray(parsed)) {
|
|
1102
|
+
return [];
|
|
1103
|
+
}
|
|
1104
|
+
return parsed
|
|
1105
|
+
.map(item => (typeof item === "string" ? item.trim() : ""))
|
|
1106
|
+
.filter(Boolean);
|
|
1107
|
+
}
|
|
1108
|
+
catch {
|
|
1109
|
+
return [];
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
function parseJsonRelations(value) {
|
|
1113
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
1114
|
+
return [];
|
|
1115
|
+
}
|
|
1116
|
+
try {
|
|
1117
|
+
const parsed = JSON.parse(value);
|
|
1118
|
+
if (!Array.isArray(parsed)) {
|
|
1119
|
+
return [];
|
|
1120
|
+
}
|
|
1121
|
+
return parsed
|
|
1122
|
+
.map(item => {
|
|
1123
|
+
if (typeof item !== "object" || item === null)
|
|
1124
|
+
return null;
|
|
1125
|
+
const relation = item;
|
|
1126
|
+
const source = typeof relation.source === "string" ? relation.source.trim() : "";
|
|
1127
|
+
const target = typeof relation.target === "string" ? relation.target.trim() : "";
|
|
1128
|
+
const type = typeof relation.type === "string" && relation.type.trim() ? relation.type.trim() : "related_to";
|
|
1129
|
+
if (!source || !target)
|
|
1130
|
+
return null;
|
|
1131
|
+
return { source, target, type };
|
|
1132
|
+
})
|
|
1133
|
+
.filter((item) => Boolean(item));
|
|
1134
|
+
}
|
|
1135
|
+
catch {
|
|
1136
|
+
return [];
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
async function searchLanceDb(args) {
|
|
1140
|
+
try {
|
|
1141
|
+
const require = (0, module_1.createRequire)(__filename);
|
|
1142
|
+
const lancedbDir = path.join(args.memoryRoot, "vector", "lancedb");
|
|
1143
|
+
if (!fs.existsSync(lancedbDir)) {
|
|
1144
|
+
return [];
|
|
1145
|
+
}
|
|
1146
|
+
const moduleValue = require("@lancedb/lancedb");
|
|
1147
|
+
const connect = moduleValue.connect;
|
|
1148
|
+
if (typeof connect !== "function") {
|
|
1149
|
+
return [];
|
|
1150
|
+
}
|
|
1151
|
+
const db = await connect(lancedbDir);
|
|
1152
|
+
if (!db || typeof db.openTable !== "function") {
|
|
1153
|
+
return [];
|
|
1154
|
+
}
|
|
1155
|
+
const table = await db.openTable("events");
|
|
1156
|
+
if (!table || typeof table.search !== "function") {
|
|
1157
|
+
return [];
|
|
1158
|
+
}
|
|
1159
|
+
const searchObj = table.search(args.queryEmbedding);
|
|
1160
|
+
if (!searchObj || typeof searchObj.limit !== "function") {
|
|
1161
|
+
return [];
|
|
1162
|
+
}
|
|
1163
|
+
const limited = searchObj.limit(args.limit);
|
|
1164
|
+
if (!limited || typeof limited.toArray !== "function") {
|
|
1165
|
+
return [];
|
|
1166
|
+
}
|
|
1167
|
+
const rows = await limited.toArray();
|
|
1168
|
+
const docs = [];
|
|
1169
|
+
for (const row of rows) {
|
|
1170
|
+
if (typeof row !== "object" || row === null)
|
|
1171
|
+
continue;
|
|
1172
|
+
const record = row;
|
|
1173
|
+
const id = typeof record.id === "string" ? record.id : "";
|
|
1174
|
+
const summary = typeof record.summary === "string" ? record.summary : "";
|
|
1175
|
+
if (!id || !summary)
|
|
1176
|
+
continue;
|
|
1177
|
+
const ts = typeof record.timestamp === "string" ? Date.parse(record.timestamp) : NaN;
|
|
1178
|
+
const entities = parseJsonStringArray(record.entities_json);
|
|
1179
|
+
const relations = parseJsonRelations(record.relations_json);
|
|
1180
|
+
docs.push({
|
|
1181
|
+
id,
|
|
1182
|
+
text: summary,
|
|
1183
|
+
source: "vector_lancedb",
|
|
1184
|
+
timestamp: Number.isFinite(ts) ? ts : undefined,
|
|
1185
|
+
layer: record.layer === "active" || record.layer === "archive" ? record.layer : undefined,
|
|
1186
|
+
sourceMemoryId: typeof record.source_memory_id === "string" ? record.source_memory_id : undefined,
|
|
1187
|
+
sourceMemoryCanonicalId: typeof record.source_memory_canonical_id === "string" ? record.source_memory_canonical_id : undefined,
|
|
1188
|
+
sourceEventId: typeof record.source_event_id === "string" ? record.source_event_id : undefined,
|
|
1189
|
+
sourceField: record.source_field === "summary" || record.source_field === "evidence"
|
|
1190
|
+
? record.source_field
|
|
1191
|
+
: undefined,
|
|
1192
|
+
embedding: Array.isArray(record.vector) ? record.vector.filter(item => Number.isFinite(item)) : undefined,
|
|
1193
|
+
eventType: typeof record.event_type === "string" ? record.event_type : undefined,
|
|
1194
|
+
qualityScore: typeof record.quality_score === "number" ? record.quality_score : undefined,
|
|
1195
|
+
charCount: typeof record.char_count === "number" ? record.char_count : undefined,
|
|
1196
|
+
tokenCount: typeof record.token_count === "number" ? record.token_count : undefined,
|
|
1197
|
+
sessionId: typeof record.session_id === "string" ? record.session_id : undefined,
|
|
1198
|
+
entities,
|
|
1199
|
+
relations: Array.isArray(relations) ? relations : [],
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
return docs;
|
|
1203
|
+
}
|
|
1204
|
+
catch (error) {
|
|
1205
|
+
args.logger.debug(`LanceDB search fallback: ${error}`);
|
|
1206
|
+
return [];
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
function parseVectorFallback(filePath, logger) {
|
|
1210
|
+
const content = safeReadFile(filePath);
|
|
1211
|
+
if (!content) {
|
|
1212
|
+
return [];
|
|
1213
|
+
}
|
|
1214
|
+
const docs = [];
|
|
1215
|
+
for (const line of content.split(/\r?\n/)) {
|
|
1216
|
+
const trimmed = line.trim();
|
|
1217
|
+
if (!trimmed)
|
|
1218
|
+
continue;
|
|
1219
|
+
try {
|
|
1220
|
+
const parsed = JSON.parse(trimmed);
|
|
1221
|
+
const id = typeof parsed.id === "string" ? parsed.id : "";
|
|
1222
|
+
const summary = typeof parsed.summary === "string" ? parsed.summary.trim() : "";
|
|
1223
|
+
if (!id || !summary)
|
|
1224
|
+
continue;
|
|
1225
|
+
const ts = typeof parsed.timestamp === "string" ? Date.parse(parsed.timestamp) : NaN;
|
|
1226
|
+
const entities = Array.isArray(parsed.entities)
|
|
1227
|
+
? parsed.entities.map(item => (typeof item === "string" ? item.trim() : "")).filter(Boolean)
|
|
1228
|
+
: [];
|
|
1229
|
+
const relations = Array.isArray(parsed.relations)
|
|
1230
|
+
? parsed.relations
|
|
1231
|
+
.map(item => {
|
|
1232
|
+
if (typeof item !== "object" || item === null)
|
|
1233
|
+
return null;
|
|
1234
|
+
const relation = item;
|
|
1235
|
+
const source = typeof relation.source === "string" ? relation.source.trim() : "";
|
|
1236
|
+
const target = typeof relation.target === "string" ? relation.target.trim() : "";
|
|
1237
|
+
const type = typeof relation.type === "string" ? relation.type.trim() : "related_to";
|
|
1238
|
+
if (!source || !target)
|
|
1239
|
+
return null;
|
|
1240
|
+
return { source, target, type };
|
|
1241
|
+
})
|
|
1242
|
+
.filter((item) => Boolean(item))
|
|
1243
|
+
: [];
|
|
1244
|
+
docs.push({
|
|
1245
|
+
id,
|
|
1246
|
+
text: summary,
|
|
1247
|
+
source: "vector_jsonl",
|
|
1248
|
+
timestamp: Number.isFinite(ts) ? ts : undefined,
|
|
1249
|
+
layer: parsed.layer === "active" || parsed.layer === "archive" ? parsed.layer : undefined,
|
|
1250
|
+
sourceMemoryId: typeof parsed.source_memory_id === "string" ? parsed.source_memory_id : undefined,
|
|
1251
|
+
sourceMemoryCanonicalId: typeof parsed.source_memory_canonical_id === "string" ? parsed.source_memory_canonical_id : undefined,
|
|
1252
|
+
sourceEventId: typeof parsed.source_event_id === "string" ? parsed.source_event_id : undefined,
|
|
1253
|
+
sourceField: parsed.source_field === "summary" || parsed.source_field === "evidence"
|
|
1254
|
+
? parsed.source_field
|
|
1255
|
+
: undefined,
|
|
1256
|
+
embedding: Array.isArray(parsed.embedding) ? parsed.embedding.filter(item => Number.isFinite(item)) : undefined,
|
|
1257
|
+
eventType: typeof parsed.event_type === "string" ? parsed.event_type.trim() : undefined,
|
|
1258
|
+
qualityScore: typeof parsed.quality_score === "number" ? parsed.quality_score : undefined,
|
|
1259
|
+
charCount: typeof parsed.char_count === "number" ? parsed.char_count : undefined,
|
|
1260
|
+
tokenCount: typeof parsed.token_count === "number" ? parsed.token_count : undefined,
|
|
1261
|
+
sessionId: typeof parsed.session_id === "string" ? parsed.session_id : undefined,
|
|
1262
|
+
entities,
|
|
1263
|
+
relations,
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
catch (error) {
|
|
1267
|
+
logger.debug(`Skip invalid vector jsonl line: ${error}`);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
return docs;
|
|
1271
|
+
}
|
|
1272
|
+
async function requestFusion(args) {
|
|
1273
|
+
const candidateIdSet = new Set(args.candidates.map(item => item.id));
|
|
1274
|
+
const endpoint = args.llm.baseUrl.endsWith("/chat/completions")
|
|
1275
|
+
? args.llm.baseUrl
|
|
1276
|
+
: `${args.llm.baseUrl}/chat/completions`;
|
|
1277
|
+
const evidenceText = args.candidates
|
|
1278
|
+
.map((item, index) => {
|
|
1279
|
+
const excerpt = (item.source_excerpt || "").trim();
|
|
1280
|
+
const sourceFile = (item.source_file || "").trim();
|
|
1281
|
+
const sourceMemoryId = (item.source_memory_id || "").trim();
|
|
1282
|
+
const sourceMemoryCanonicalId = (item.source_memory_canonical_id || "").trim();
|
|
1283
|
+
const sourceLayer = (item.source_layer || "").trim();
|
|
1284
|
+
const sourceEventId = (item.source_event_id || "").trim();
|
|
1285
|
+
const sourceField = (item.source_field || "").trim();
|
|
1286
|
+
const extraParts = [];
|
|
1287
|
+
if (sourceMemoryId)
|
|
1288
|
+
extraParts.push(`source_memory_id=${sourceMemoryId}`);
|
|
1289
|
+
if (sourceMemoryCanonicalId)
|
|
1290
|
+
extraParts.push(`source_memory_canonical_id=${sourceMemoryCanonicalId}`);
|
|
1291
|
+
if (sourceLayer)
|
|
1292
|
+
extraParts.push(`source_layer=${sourceLayer}`);
|
|
1293
|
+
if (sourceEventId)
|
|
1294
|
+
extraParts.push(`source_event_id=${sourceEventId}`);
|
|
1295
|
+
if (sourceField)
|
|
1296
|
+
extraParts.push(`source_field=${sourceField}`);
|
|
1297
|
+
if (sourceFile)
|
|
1298
|
+
extraParts.push(`source_file=${sourceFile}`);
|
|
1299
|
+
if (excerpt)
|
|
1300
|
+
extraParts.push(`source_excerpt=${excerpt}`);
|
|
1301
|
+
const extra = extraParts.length > 0 ? `\n ${extraParts.join("\n ")}` : "";
|
|
1302
|
+
return `${index + 1}. [${item.id}] (${item.source}, score=${item.score.toFixed(4)}) ${item.text}${extra}`;
|
|
1303
|
+
})
|
|
1304
|
+
.join("\n")
|
|
1305
|
+
.slice(0, 18000);
|
|
1306
|
+
const prompt = [
|
|
1307
|
+
`prompt_version=${READ_FUSION_PROMPT_VERSION}`,
|
|
1308
|
+
"You are a memory retrieval fusion engine. Fuse multi-channel evidence into a structured answer package for the agent.",
|
|
1309
|
+
"Core values and principles:",
|
|
1310
|
+
"A) Truthfulness first: do not fabricate; do not infer beyond evidence.",
|
|
1311
|
+
"B) Evidence first: every key conclusion must be traceable via evidence_ids.",
|
|
1312
|
+
"C) Make conflicts explicit: write conflicts instead of silently overriding.",
|
|
1313
|
+
"D) Be transparent about uncertainty: put uncertain parts in coverage_note.",
|
|
1314
|
+
"E) Summary-first: prefer summary evidence for conclusions; source_excerpt is supporting evidence.",
|
|
1315
|
+
"F) Same-source dedup: merge duplicate evidence from the same source_memory_id/source_memory_canonical_id.",
|
|
1316
|
+
"G) Full-text recall: if summary/excerpt is insufficient, return event ids in need_fulltext_event_ids.",
|
|
1317
|
+
"Source channel semantics:",
|
|
1318
|
+
"- rules: policy/constraints; use for what should be done.",
|
|
1319
|
+
"- archive: event-level stable facts (summary-first).",
|
|
1320
|
+
"- vector: semantic neighbors for recall; source_field=summary/evidence indicates chunk role.",
|
|
1321
|
+
"- graph: entity-relation structure; prefer for dependency/relationship questions.",
|
|
1322
|
+
"Query alignment:",
|
|
1323
|
+
"- answer the user query first; ignore evidence unrelated to the query.",
|
|
1324
|
+
"- when evidence conflicts, prioritize recency + quality + explicitness and record the conflict.",
|
|
1325
|
+
"Return strict JSON only:",
|
|
1326
|
+
"{\"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[], \"need_fulltext_event_ids\": string[], \"confidence\": number}",
|
|
1327
|
+
"Output constraints:",
|
|
1328
|
+
"1) canonical_answer must be directly usable.",
|
|
1329
|
+
"2) facts: usually 3-12 items; prefer high-quality evidence.",
|
|
1330
|
+
"3) evidence_ids must come from input candidate ids.",
|
|
1331
|
+
"4) conflicts must be [] when no conflict exists.",
|
|
1332
|
+
"5) confidence must be within [0, 1].",
|
|
1333
|
+
"6) uncertain parts must be explicitly stated in coverage_note.",
|
|
1334
|
+
...READ_FUSION_REGRESSION_SAMPLES,
|
|
1335
|
+
].join("\n");
|
|
1336
|
+
const body = {
|
|
1337
|
+
model: args.llm.model,
|
|
1338
|
+
temperature: 0.1,
|
|
1339
|
+
messages: [
|
|
1340
|
+
{ role: "system", content: "Output JSON only. No extra text." },
|
|
1341
|
+
{ role: "user", content: `${prompt}\n\nQuery:\n${args.query}\n\nCandidate Evidence:\n${evidenceText}` },
|
|
1342
|
+
],
|
|
1343
|
+
};
|
|
1344
|
+
let lastError = null;
|
|
1345
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1346
|
+
const response = await (0, http_post_1.postJsonWithTimeout)({
|
|
1347
|
+
endpoint,
|
|
1348
|
+
apiKey: args.llm.apiKey,
|
|
1349
|
+
body,
|
|
1350
|
+
timeoutMs: 20000,
|
|
1351
|
+
});
|
|
1352
|
+
if (!response.ok) {
|
|
1353
|
+
lastError = new Error(response.status > 0 ? `fusion_http_${response.status}` : (response.error || "fusion_network_error"));
|
|
1354
|
+
continue;
|
|
1355
|
+
}
|
|
1356
|
+
try {
|
|
1357
|
+
const json = (response.json || {});
|
|
1358
|
+
const content = json?.choices?.[0]?.message?.content?.trim() || "";
|
|
1359
|
+
if (!content) {
|
|
1360
|
+
lastError = new Error("fusion_empty");
|
|
1361
|
+
continue;
|
|
1362
|
+
}
|
|
1363
|
+
const parsed = JSON.parse(content);
|
|
1364
|
+
if (!parsed || typeof parsed.canonical_answer !== "string" || !parsed.canonical_answer.trim()) {
|
|
1365
|
+
lastError = new Error("fusion_invalid");
|
|
1366
|
+
continue;
|
|
1367
|
+
}
|
|
1368
|
+
const evidenceIds = Array.isArray(parsed.evidence_ids)
|
|
1369
|
+
? parsed.evidence_ids.filter(item => typeof item === "string" && item.trim())
|
|
1370
|
+
: [];
|
|
1371
|
+
const whitelistedEvidenceIds = [...new Set(evidenceIds.filter(id => candidateIdSet.has(id)))];
|
|
1372
|
+
const needFulltextEventIds = Array.isArray(parsed.need_fulltext_event_ids)
|
|
1373
|
+
? [...new Set(parsed.need_fulltext_event_ids
|
|
1374
|
+
.filter(item => typeof item === "string")
|
|
1375
|
+
.map(item => item.trim())
|
|
1376
|
+
.filter(Boolean))]
|
|
1377
|
+
: [];
|
|
1378
|
+
return {
|
|
1379
|
+
canonical_answer: parsed.canonical_answer.trim().slice(0, 6000),
|
|
1380
|
+
coverage_note: typeof parsed.coverage_note === "string" ? parsed.coverage_note.trim().slice(0, 1200) : "",
|
|
1381
|
+
facts: Array.isArray(parsed.facts) ? parsed.facts : [],
|
|
1382
|
+
timeline: Array.isArray(parsed.timeline) ? parsed.timeline : [],
|
|
1383
|
+
entities: Array.isArray(parsed.entities) ? parsed.entities : [],
|
|
1384
|
+
decisions: Array.isArray(parsed.decisions) ? parsed.decisions : [],
|
|
1385
|
+
fixes: Array.isArray(parsed.fixes) ? parsed.fixes : [],
|
|
1386
|
+
preferences: Array.isArray(parsed.preferences) ? parsed.preferences : [],
|
|
1387
|
+
risks: Array.isArray(parsed.risks) ? parsed.risks : [],
|
|
1388
|
+
action_items: Array.isArray(parsed.action_items) ? parsed.action_items : [],
|
|
1389
|
+
conflicts: Array.isArray(parsed.conflicts) ? parsed.conflicts : [],
|
|
1390
|
+
evidence_ids: whitelistedEvidenceIds,
|
|
1391
|
+
need_fulltext_event_ids: needFulltextEventIds,
|
|
1392
|
+
confidence: typeof parsed.confidence === "number"
|
|
1393
|
+
? Math.max(0, Math.min(1, parsed.confidence))
|
|
1394
|
+
: 0.5,
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
catch (error) {
|
|
1398
|
+
lastError = error;
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError || "fusion_failed"));
|
|
1402
|
+
}
|
|
163
1403
|
function createReadStore(options) {
|
|
164
1404
|
const memoryRoot = options.dbPath ? path.resolve(options.dbPath) : path.join(options.projectRoot, "data", "memory");
|
|
1405
|
+
const vectorFallbackPath = path.join(memoryRoot, "vector", "lancedb_events.jsonl");
|
|
1406
|
+
const hitStatsPath = path.join(memoryRoot, ".read_hit_stats.json");
|
|
1407
|
+
let docsCache = null;
|
|
1408
|
+
let vectorFallbackCache = null;
|
|
1409
|
+
let bm25TokenCacheSignature = "";
|
|
1410
|
+
let bm25TokenCache = new Map();
|
|
1411
|
+
let hitStatsCache = null;
|
|
1412
|
+
let hitStatsDirty = false;
|
|
1413
|
+
let hitStatsPendingMutations = 0;
|
|
1414
|
+
let lastHitStatsFlushAt = 0;
|
|
1415
|
+
const hitStatsFlushIntervalMs = 5000;
|
|
1416
|
+
const hitStatsFlushBatch = 24;
|
|
1417
|
+
const readTuning = resolveReadTuning(options.readTuning);
|
|
1418
|
+
function fileSignature(filePath) {
|
|
1419
|
+
try {
|
|
1420
|
+
if (!fs.existsSync(filePath)) {
|
|
1421
|
+
return `${filePath}:missing`;
|
|
1422
|
+
}
|
|
1423
|
+
const stat = fs.statSync(filePath);
|
|
1424
|
+
return `${filePath}:${stat.size}:${Math.floor(stat.mtimeMs)}`;
|
|
1425
|
+
}
|
|
1426
|
+
catch {
|
|
1427
|
+
return `${filePath}:error`;
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
function loadHitStats() {
|
|
1431
|
+
if (hitStatsCache) {
|
|
1432
|
+
return hitStatsCache;
|
|
1433
|
+
}
|
|
1434
|
+
try {
|
|
1435
|
+
if (!fs.existsSync(hitStatsPath)) {
|
|
1436
|
+
hitStatsCache = { items: {} };
|
|
1437
|
+
return hitStatsCache;
|
|
1438
|
+
}
|
|
1439
|
+
const content = fs.readFileSync(hitStatsPath, "utf-8").trim();
|
|
1440
|
+
if (!content) {
|
|
1441
|
+
hitStatsCache = { items: {} };
|
|
1442
|
+
return hitStatsCache;
|
|
1443
|
+
}
|
|
1444
|
+
const parsed = JSON.parse(content);
|
|
1445
|
+
if (!parsed || typeof parsed !== "object" || !parsed.items || typeof parsed.items !== "object") {
|
|
1446
|
+
hitStatsCache = { items: {} };
|
|
1447
|
+
return hitStatsCache;
|
|
1448
|
+
}
|
|
1449
|
+
hitStatsCache = parsed;
|
|
1450
|
+
return hitStatsCache;
|
|
1451
|
+
}
|
|
1452
|
+
catch {
|
|
1453
|
+
hitStatsCache = { items: {} };
|
|
1454
|
+
return hitStatsCache;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
function directorySignature(dirPath, extension) {
|
|
1458
|
+
try {
|
|
1459
|
+
if (!fs.existsSync(dirPath)) {
|
|
1460
|
+
return `${dirPath}:missing`;
|
|
1461
|
+
}
|
|
1462
|
+
const files = fs.readdirSync(dirPath)
|
|
1463
|
+
.filter(file => file.toLowerCase().endsWith(extension.toLowerCase()))
|
|
1464
|
+
.sort((a, b) => a.localeCompare(b));
|
|
1465
|
+
if (files.length === 0) {
|
|
1466
|
+
return `${dirPath}:empty`;
|
|
1467
|
+
}
|
|
1468
|
+
const signatures = files.map(file => fileSignature(path.join(dirPath, file)));
|
|
1469
|
+
return `${dirPath}:${signatures.join("|")}`;
|
|
1470
|
+
}
|
|
1471
|
+
catch {
|
|
1472
|
+
return `${dirPath}:error`;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
function saveHitStats(state) {
|
|
1476
|
+
try {
|
|
1477
|
+
const dir = path.dirname(hitStatsPath);
|
|
1478
|
+
if (!fs.existsSync(dir)) {
|
|
1479
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1480
|
+
}
|
|
1481
|
+
fs.writeFileSync(hitStatsPath, JSON.stringify(state, null, 2), "utf-8");
|
|
1482
|
+
}
|
|
1483
|
+
catch (error) {
|
|
1484
|
+
options.logger.warn(`Failed to persist read hit stats: ${error}`);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
function maybeFlushHitStats(force) {
|
|
1488
|
+
if (!hitStatsDirty || !hitStatsCache) {
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
const now = Date.now();
|
|
1492
|
+
if (!force &&
|
|
1493
|
+
hitStatsPendingMutations < hitStatsFlushBatch &&
|
|
1494
|
+
(now - lastHitStatsFlushAt) < hitStatsFlushIntervalMs) {
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
saveHitStats(hitStatsCache);
|
|
1498
|
+
hitStatsDirty = false;
|
|
1499
|
+
hitStatsPendingMutations = 0;
|
|
1500
|
+
lastHitStatsFlushAt = now;
|
|
1501
|
+
}
|
|
1502
|
+
function markHit(ids) {
|
|
1503
|
+
if (!ids.length) {
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
const state = loadHitStats();
|
|
1507
|
+
const now = new Date().toISOString();
|
|
1508
|
+
for (const id of ids) {
|
|
1509
|
+
const key = (id || "").trim();
|
|
1510
|
+
if (!key)
|
|
1511
|
+
continue;
|
|
1512
|
+
const prev = state.items[key];
|
|
1513
|
+
state.items[key] = {
|
|
1514
|
+
count: (prev?.count || 0) + 1,
|
|
1515
|
+
lastHitAt: now,
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
const entries = Object.entries(state.items)
|
|
1519
|
+
.sort((a, b) => {
|
|
1520
|
+
const ta = Date.parse(a[1].lastHitAt || "");
|
|
1521
|
+
const tb = Date.parse(b[1].lastHitAt || "");
|
|
1522
|
+
return (Number.isFinite(tb) ? tb : 0) - (Number.isFinite(ta) ? ta : 0);
|
|
1523
|
+
})
|
|
1524
|
+
.slice(0, 20000);
|
|
1525
|
+
state.items = Object.fromEntries(entries);
|
|
1526
|
+
hitStatsCache = state;
|
|
1527
|
+
hitStatsDirty = true;
|
|
1528
|
+
hitStatsPendingMutations += ids.length;
|
|
1529
|
+
maybeFlushHitStats(false);
|
|
1530
|
+
}
|
|
165
1531
|
function loadAllDocuments() {
|
|
166
1532
|
const cortexRulesPath = path.join(memoryRoot, "CORTEX_RULES.md");
|
|
167
|
-
const memoryMdPath = path.join(memoryRoot, "MEMORY.md");
|
|
168
1533
|
const activeSessionsPath = path.join(memoryRoot, "sessions", "active", "sessions.jsonl");
|
|
169
1534
|
const archiveSessionsPath = path.join(memoryRoot, "sessions", "archive", "sessions.jsonl");
|
|
170
|
-
|
|
1535
|
+
const graphMemoryPath = path.join(memoryRoot, "graph", "memory.jsonl");
|
|
1536
|
+
const supersededRelationPath = path.join(memoryRoot, "graph", "superseded_relations.jsonl");
|
|
1537
|
+
const conflictQueuePath = path.join(memoryRoot, "graph", "conflict_queue.jsonl");
|
|
1538
|
+
const wikiEntitiesDir = path.join(memoryRoot, "wiki", "entities");
|
|
1539
|
+
const wikiTopicsDir = path.join(memoryRoot, "wiki", "topics");
|
|
1540
|
+
const wikiTimelinesDir = path.join(memoryRoot, "wiki", "timelines");
|
|
1541
|
+
const wikiProjectionIndexPath = path.join(memoryRoot, "wiki", ".projection_index.json");
|
|
1542
|
+
const signature = [
|
|
1543
|
+
fileSignature(cortexRulesPath),
|
|
1544
|
+
fileSignature(activeSessionsPath),
|
|
1545
|
+
fileSignature(archiveSessionsPath),
|
|
1546
|
+
fileSignature(graphMemoryPath),
|
|
1547
|
+
fileSignature(supersededRelationPath),
|
|
1548
|
+
fileSignature(conflictQueuePath),
|
|
1549
|
+
directorySignature(wikiEntitiesDir, ".md"),
|
|
1550
|
+
directorySignature(wikiTopicsDir, ".md"),
|
|
1551
|
+
directorySignature(wikiTimelinesDir, ".md"),
|
|
1552
|
+
fileSignature(wikiProjectionIndexPath),
|
|
1553
|
+
].join("|");
|
|
1554
|
+
if (docsCache && docsCache.signature === signature) {
|
|
1555
|
+
return docsCache.docs;
|
|
1556
|
+
}
|
|
1557
|
+
const archiveEventTypeById = new Map();
|
|
1558
|
+
if (fs.existsSync(archiveSessionsPath)) {
|
|
1559
|
+
const archiveContent = safeReadFile(archiveSessionsPath);
|
|
1560
|
+
for (const line of archiveContent.split(/\r?\n/)) {
|
|
1561
|
+
const trimmed = line.trim();
|
|
1562
|
+
if (!trimmed)
|
|
1563
|
+
continue;
|
|
1564
|
+
try {
|
|
1565
|
+
const parsed = JSON.parse(trimmed);
|
|
1566
|
+
const id = typeof parsed.id === "string" ? parsed.id.trim() : "";
|
|
1567
|
+
const eventType = typeof parsed.event_type === "string" ? parsed.event_type.trim() : "";
|
|
1568
|
+
if (id && eventType) {
|
|
1569
|
+
archiveEventTypeById.set(id, eventType);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
catch { }
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
const graphDocs = [];
|
|
1576
|
+
const supersededRelationKeys = new Set();
|
|
1577
|
+
if (fs.existsSync(supersededRelationPath)) {
|
|
1578
|
+
const supersededContent = safeReadFile(supersededRelationPath);
|
|
1579
|
+
for (const line of supersededContent.split(/\r?\n/)) {
|
|
1580
|
+
const trimmed = line.trim();
|
|
1581
|
+
if (!trimmed)
|
|
1582
|
+
continue;
|
|
1583
|
+
try {
|
|
1584
|
+
const parsed = JSON.parse(trimmed);
|
|
1585
|
+
const relationKey = typeof parsed.relation_key === "string" ? parsed.relation_key.trim().toLowerCase() : "";
|
|
1586
|
+
if (relationKey) {
|
|
1587
|
+
supersededRelationKeys.add(relationKey);
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
catch (error) {
|
|
1591
|
+
options.logger.debug(`Skipping invalid superseded relation line: ${error}`);
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
if (fs.existsSync(graphMemoryPath)) {
|
|
1596
|
+
const graphContent = safeReadFile(graphMemoryPath);
|
|
1597
|
+
for (const line of graphContent.split(/\r?\n/)) {
|
|
1598
|
+
const trimmed = line.trim();
|
|
1599
|
+
if (!trimmed)
|
|
1600
|
+
continue;
|
|
1601
|
+
try {
|
|
1602
|
+
const parsed = JSON.parse(trimmed);
|
|
1603
|
+
const id = typeof parsed.id === "string" ? parsed.id : "";
|
|
1604
|
+
const summary = typeof parsed.summary === "string" ? parsed.summary.trim() : "";
|
|
1605
|
+
const sourceTextNav = typeof parsed.source_text_nav === "object" && parsed.source_text_nav !== null && !Array.isArray(parsed.source_text_nav)
|
|
1606
|
+
? parsed.source_text_nav
|
|
1607
|
+
: undefined;
|
|
1608
|
+
const sourceEventId = typeof parsed.source_event_id === "string" ? parsed.source_event_id : "";
|
|
1609
|
+
const archiveEventId = typeof parsed.archive_event_id === "string" ? parsed.archive_event_id : "";
|
|
1610
|
+
const navSourceEventId = typeof sourceTextNav?.source_event_id === "string" ? sourceTextNav.source_event_id.trim() : "";
|
|
1611
|
+
const navSourceMemoryId = typeof sourceTextNav?.source_memory_id === "string" ? sourceTextNav.source_memory_id.trim() : "";
|
|
1612
|
+
const navSessionId = typeof sourceTextNav?.session_id === "string" ? sourceTextNav.session_id.trim() : "";
|
|
1613
|
+
const navSourceLayer = typeof sourceTextNav?.layer === "string" ? sourceTextNav.layer.trim() : "";
|
|
1614
|
+
const navSourceFile = typeof sourceTextNav?.source_file === "string" ? sourceTextNav.source_file.trim() : "";
|
|
1615
|
+
const eventRefId = navSourceMemoryId || archiveEventId || navSourceEventId || sourceEventId;
|
|
1616
|
+
const sessionId = navSessionId || (typeof parsed.session_id === "string" ? parsed.session_id : "");
|
|
1617
|
+
const sourceLayer = navSourceLayer || (typeof parsed.source_layer === "string" ? parsed.source_layer : "");
|
|
1618
|
+
const sourceFile = navSourceFile || (typeof parsed.source_file === "string" ? parsed.source_file : "");
|
|
1619
|
+
const timestamp = typeof parsed.timestamp === "string" ? Date.parse(parsed.timestamp) : NaN;
|
|
1620
|
+
const entities = Array.isArray(parsed.entities)
|
|
1621
|
+
? parsed.entities.map((item) => (typeof item === "string" ? item.trim() : "")).filter(Boolean)
|
|
1622
|
+
: [];
|
|
1623
|
+
const entityTypes = typeof parsed.entity_types === "object" && parsed.entity_types !== null
|
|
1624
|
+
? parsed.entity_types
|
|
1625
|
+
: {};
|
|
1626
|
+
const relations = Array.isArray(parsed.relations) ? parsed.relations : [];
|
|
1627
|
+
const eventType = (typeof parsed.event_type === "string" ? parsed.event_type : "") || archiveEventTypeById.get(eventRefId) || "";
|
|
1628
|
+
let relationCount = 0;
|
|
1629
|
+
for (const relationRaw of relations) {
|
|
1630
|
+
if (typeof relationRaw !== "object" || relationRaw === null)
|
|
1631
|
+
continue;
|
|
1632
|
+
const relationRecord = relationRaw;
|
|
1633
|
+
const source = typeof relationRecord.source === "string" ? relationRecord.source.trim() : "";
|
|
1634
|
+
const target = typeof relationRecord.target === "string" ? relationRecord.target.trim() : "";
|
|
1635
|
+
const type = typeof relationRecord.type === "string" ? relationRecord.type.trim() : "related_to";
|
|
1636
|
+
if (!source || !target)
|
|
1637
|
+
continue;
|
|
1638
|
+
relationCount += 1;
|
|
1639
|
+
const relationKey = graphRelationKey({ source, target, type });
|
|
1640
|
+
const factStatus = supersededRelationKeys.has(relationKey) ? "superseded" : "active";
|
|
1641
|
+
const evidenceSpan = typeof relationRecord.evidence_span === "string" && relationRecord.evidence_span.trim()
|
|
1642
|
+
? relationRecord.evidence_span.trim()
|
|
1643
|
+
: undefined;
|
|
1644
|
+
const confidenceValue = typeof relationRecord.confidence === "number" && Number.isFinite(relationRecord.confidence)
|
|
1645
|
+
? relationRecord.confidence
|
|
1646
|
+
: undefined;
|
|
1647
|
+
const relationOrigin = typeof relationRecord.relation_origin === "string" && relationRecord.relation_origin.trim()
|
|
1648
|
+
? relationRecord.relation_origin.trim()
|
|
1649
|
+
: undefined;
|
|
1650
|
+
const relationDefinition = typeof relationRecord.relation_definition === "string" && relationRecord.relation_definition.trim()
|
|
1651
|
+
? relationRecord.relation_definition.trim()
|
|
1652
|
+
: undefined;
|
|
1653
|
+
const contextChunk = typeof relationRecord.context_chunk === "string" && relationRecord.context_chunk.trim()
|
|
1654
|
+
? relationRecord.context_chunk.trim()
|
|
1655
|
+
: undefined;
|
|
1656
|
+
const relation = {
|
|
1657
|
+
source,
|
|
1658
|
+
target,
|
|
1659
|
+
type,
|
|
1660
|
+
relation_origin: relationOrigin,
|
|
1661
|
+
relation_definition: relationDefinition,
|
|
1662
|
+
evidence_span: evidenceSpan,
|
|
1663
|
+
context_chunk: contextChunk,
|
|
1664
|
+
confidence: confidenceValue,
|
|
1665
|
+
fact_status: factStatus,
|
|
1666
|
+
source_event_id: sourceEventId || archiveEventId || undefined,
|
|
1667
|
+
relation_key: relationKey,
|
|
1668
|
+
};
|
|
1669
|
+
const relationEntities = uniqueStrings([...entities, source, target]);
|
|
1670
|
+
const entityLines = relationEntities.length > 0
|
|
1671
|
+
? relationEntities.map((entity, index) => {
|
|
1672
|
+
const entityType = entityTypes[entity];
|
|
1673
|
+
return `${index + 1}. ${entity}${entityType ? ` (${entityType})` : ""}`;
|
|
1674
|
+
}).join("\n")
|
|
1675
|
+
: "none";
|
|
1676
|
+
const text = [
|
|
1677
|
+
`# Graph Relation`,
|
|
1678
|
+
`record_id: ${id}`,
|
|
1679
|
+
`relation_index: ${relationCount}`,
|
|
1680
|
+
`relation_key: ${relationKey}`,
|
|
1681
|
+
`fact_status: ${factStatus}`,
|
|
1682
|
+
`source_event_id: ${sourceEventId || archiveEventId || "unknown"}`,
|
|
1683
|
+
`source_layer: ${sourceLayer || "unknown"}`,
|
|
1684
|
+
`archive_event_id: ${archiveEventId || "n/a"}`,
|
|
1685
|
+
`event_type: ${eventType || "unknown"}`,
|
|
1686
|
+
`session_id: ${sessionId || "unknown"}`,
|
|
1687
|
+
`source_file: ${sourceFile || "unknown"}`,
|
|
1688
|
+
`evidence_span: ${evidenceSpan || "n/a"}`,
|
|
1689
|
+
`context_chunk: ${contextChunk || "n/a"}`,
|
|
1690
|
+
`relation_origin: ${relationOrigin || "n/a"}`,
|
|
1691
|
+
`relation_definition: ${relationDefinition || "n/a"}`,
|
|
1692
|
+
`confidence: ${typeof confidenceValue === "number" ? confidenceValue : "n/a"}`,
|
|
1693
|
+
``,
|
|
1694
|
+
`## Summary`,
|
|
1695
|
+
summary || "n/a",
|
|
1696
|
+
``,
|
|
1697
|
+
`## Source References`,
|
|
1698
|
+
`source_event_id: ${navSourceEventId || sourceEventId || archiveEventId || "unknown"}`,
|
|
1699
|
+
`source_memory_id: ${navSourceMemoryId || eventRefId || "unknown"}`,
|
|
1700
|
+
`source_layer: ${sourceLayer || "unknown"}`,
|
|
1701
|
+
`source_file: ${sourceFile || "unknown"}`,
|
|
1702
|
+
`session_id: ${sessionId || "unknown"}`,
|
|
1703
|
+
``,
|
|
1704
|
+
`## Entities`,
|
|
1705
|
+
entityLines,
|
|
1706
|
+
``,
|
|
1707
|
+
`## Relation`,
|
|
1708
|
+
`${source} -[${type}/${factStatus}]-> ${target}`,
|
|
1709
|
+
].join("\n");
|
|
1710
|
+
graphDocs.push({
|
|
1711
|
+
id: `${id || "graph"}:rel:${relationCount}`,
|
|
1712
|
+
text,
|
|
1713
|
+
source: "sessions_graph",
|
|
1714
|
+
timestamp: Number.isFinite(timestamp) ? timestamp : undefined,
|
|
1715
|
+
layer: sourceLayer === "active_only" ? "active" : "archive",
|
|
1716
|
+
summaryText: summary || undefined,
|
|
1717
|
+
sourceText: contextChunk || undefined,
|
|
1718
|
+
sourceMemoryId: eventRefId || id,
|
|
1719
|
+
sourceEventId: navSourceEventId || sourceEventId || archiveEventId || undefined,
|
|
1720
|
+
sourceFile: sourceFile || undefined,
|
|
1721
|
+
sessionId,
|
|
1722
|
+
entities: relationEntities,
|
|
1723
|
+
relations: [relation],
|
|
1724
|
+
eventType: eventType || undefined,
|
|
1725
|
+
qualityScore: typeof parsed.confidence === "number" && Number.isFinite(parsed.confidence) ? parsed.confidence : 1,
|
|
1726
|
+
factStatus,
|
|
1727
|
+
evidenceIds: buildGraphEvidenceIds({
|
|
1728
|
+
source,
|
|
1729
|
+
target,
|
|
1730
|
+
type,
|
|
1731
|
+
sourceEventId: relation.source_event_id,
|
|
1732
|
+
evidenceSpan,
|
|
1733
|
+
}),
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
catch (error) {
|
|
1738
|
+
options.logger.debug(`Skipping invalid graph memory line: ${error}`);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
if (fs.existsSync(conflictQueuePath)) {
|
|
1743
|
+
const queueContent = safeReadFile(conflictQueuePath);
|
|
1744
|
+
for (const line of queueContent.split(/\r?\n/)) {
|
|
1745
|
+
const trimmed = line.trim();
|
|
1746
|
+
if (!trimmed)
|
|
1747
|
+
continue;
|
|
1748
|
+
try {
|
|
1749
|
+
const parsed = JSON.parse(trimmed);
|
|
1750
|
+
const status = normalizeFactStatus(typeof parsed.status === "string" ? parsed.status : "");
|
|
1751
|
+
if (status !== "pending_conflict" && status !== "rejected") {
|
|
1752
|
+
continue;
|
|
1753
|
+
}
|
|
1754
|
+
const conflictId = typeof parsed.conflict_id === "string" ? parsed.conflict_id.trim() : "";
|
|
1755
|
+
const sourceEventId = typeof parsed.source_event_id === "string" ? parsed.source_event_id.trim() : "";
|
|
1756
|
+
const sessionId = typeof parsed.session_id === "string" ? parsed.session_id.trim() : "";
|
|
1757
|
+
const sourceFile = typeof parsed.source_file === "string" ? parsed.source_file.trim() : "";
|
|
1758
|
+
const sourceLayer = typeof parsed.source_layer === "string" ? parsed.source_layer.trim() : "";
|
|
1759
|
+
const updatedAt = typeof parsed.updated_at === "string" ? Date.parse(parsed.updated_at) : NaN;
|
|
1760
|
+
const candidate = typeof parsed.candidate === "object" && parsed.candidate !== null
|
|
1761
|
+
? parsed.candidate
|
|
1762
|
+
: {};
|
|
1763
|
+
const candidateSummary = typeof candidate.summary === "string" ? candidate.summary.trim() : "";
|
|
1764
|
+
const candidateSourceTextNav = typeof candidate.source_text_nav === "object" && candidate.source_text_nav !== null && !Array.isArray(candidate.source_text_nav)
|
|
1765
|
+
? candidate.source_text_nav
|
|
1766
|
+
: undefined;
|
|
1767
|
+
const navSourceEventId = typeof candidateSourceTextNav?.source_event_id === "string" ? candidateSourceTextNav.source_event_id.trim() : "";
|
|
1768
|
+
const navSourceMemoryId = typeof candidateSourceTextNav?.source_memory_id === "string" ? candidateSourceTextNav.source_memory_id.trim() : "";
|
|
1769
|
+
const navSourceLayer = typeof candidateSourceTextNav?.layer === "string" ? candidateSourceTextNav.layer.trim() : "";
|
|
1770
|
+
const navSourceFile = typeof candidateSourceTextNav?.source_file === "string" ? candidateSourceTextNav.source_file.trim() : "";
|
|
1771
|
+
const navSessionId = typeof candidateSourceTextNav?.session_id === "string" ? candidateSourceTextNav.session_id.trim() : "";
|
|
1772
|
+
const candidateEventType = typeof candidate.event_type === "string" ? candidate.event_type.trim() : "";
|
|
1773
|
+
const candidateRelations = Array.isArray(candidate.relations) ? candidate.relations : [];
|
|
1774
|
+
let relationIndex = 0;
|
|
1775
|
+
for (const relationRaw of candidateRelations) {
|
|
1776
|
+
if (typeof relationRaw !== "object" || relationRaw === null)
|
|
1777
|
+
continue;
|
|
1778
|
+
const relationRecord = relationRaw;
|
|
1779
|
+
const source = typeof relationRecord.source === "string" ? relationRecord.source.trim() : "";
|
|
1780
|
+
const target = typeof relationRecord.target === "string" ? relationRecord.target.trim() : "";
|
|
1781
|
+
const type = typeof relationRecord.type === "string" ? relationRecord.type.trim() : "related_to";
|
|
1782
|
+
if (!source || !target)
|
|
1783
|
+
continue;
|
|
1784
|
+
relationIndex += 1;
|
|
1785
|
+
const relationKey = graphRelationKey({ source, target, type });
|
|
1786
|
+
const evidenceSpan = typeof relationRecord.evidence_span === "string" && relationRecord.evidence_span.trim()
|
|
1787
|
+
? relationRecord.evidence_span.trim()
|
|
1788
|
+
: undefined;
|
|
1789
|
+
const confidenceValue = typeof relationRecord.confidence === "number" && Number.isFinite(relationRecord.confidence)
|
|
1790
|
+
? relationRecord.confidence
|
|
1791
|
+
: undefined;
|
|
1792
|
+
const relationOrigin = typeof relationRecord.relation_origin === "string" && relationRecord.relation_origin.trim()
|
|
1793
|
+
? relationRecord.relation_origin.trim()
|
|
1794
|
+
: undefined;
|
|
1795
|
+
const relationDefinition = typeof relationRecord.relation_definition === "string" && relationRecord.relation_definition.trim()
|
|
1796
|
+
? relationRecord.relation_definition.trim()
|
|
1797
|
+
: undefined;
|
|
1798
|
+
const contextChunk = typeof relationRecord.context_chunk === "string" && relationRecord.context_chunk.trim()
|
|
1799
|
+
? relationRecord.context_chunk.trim()
|
|
1800
|
+
: undefined;
|
|
1801
|
+
const relation = {
|
|
1802
|
+
source,
|
|
1803
|
+
target,
|
|
1804
|
+
type,
|
|
1805
|
+
relation_origin: relationOrigin,
|
|
1806
|
+
relation_definition: relationDefinition,
|
|
1807
|
+
evidence_span: evidenceSpan,
|
|
1808
|
+
context_chunk: contextChunk,
|
|
1809
|
+
confidence: confidenceValue,
|
|
1810
|
+
fact_status: status,
|
|
1811
|
+
source_event_id: sourceEventId || undefined,
|
|
1812
|
+
conflict_id: conflictId || undefined,
|
|
1813
|
+
relation_key: relationKey,
|
|
1814
|
+
};
|
|
1815
|
+
const evidenceIds = uniqueStrings([
|
|
1816
|
+
...buildGraphEvidenceIds({
|
|
1817
|
+
source,
|
|
1818
|
+
target,
|
|
1819
|
+
type,
|
|
1820
|
+
sourceEventId: sourceEventId || undefined,
|
|
1821
|
+
evidenceSpan,
|
|
1822
|
+
}),
|
|
1823
|
+
conflictId ? `graph:conflict:${conflictId}` : "",
|
|
1824
|
+
]);
|
|
1825
|
+
const text = [
|
|
1826
|
+
`# Graph Conflict Candidate`,
|
|
1827
|
+
`conflict_id: ${conflictId || "unknown"}`,
|
|
1828
|
+
`fact_status: ${status}`,
|
|
1829
|
+
`source_event_id: ${sourceEventId || "unknown"}`,
|
|
1830
|
+
`source_layer: ${sourceLayer || "unknown"}`,
|
|
1831
|
+
`event_type: ${candidateEventType || "unknown"}`,
|
|
1832
|
+
`session_id: ${sessionId || "unknown"}`,
|
|
1833
|
+
`source_file: ${sourceFile || "unknown"}`,
|
|
1834
|
+
`relation_key: ${relationKey}`,
|
|
1835
|
+
`evidence_span: ${evidenceSpan || "n/a"}`,
|
|
1836
|
+
`context_chunk: ${contextChunk || "n/a"}`,
|
|
1837
|
+
`relation_origin: ${relationOrigin || "n/a"}`,
|
|
1838
|
+
`relation_definition: ${relationDefinition || "n/a"}`,
|
|
1839
|
+
`confidence: ${typeof confidenceValue === "number" ? confidenceValue : "n/a"}`,
|
|
1840
|
+
``,
|
|
1841
|
+
`## Summary`,
|
|
1842
|
+
candidateSummary || "n/a",
|
|
1843
|
+
``,
|
|
1844
|
+
`## Source References`,
|
|
1845
|
+
`source_event_id: ${navSourceEventId || sourceEventId || "unknown"}`,
|
|
1846
|
+
`source_memory_id: ${navSourceMemoryId || sourceEventId || "unknown"}`,
|
|
1847
|
+
`source_layer: ${navSourceLayer || sourceLayer || "unknown"}`,
|
|
1848
|
+
`source_file: ${navSourceFile || sourceFile || "unknown"}`,
|
|
1849
|
+
`session_id: ${navSessionId || sessionId || "unknown"}`,
|
|
1850
|
+
``,
|
|
1851
|
+
`${source} -[${type}/${status}]-> ${target}`,
|
|
1852
|
+
].join("\n");
|
|
1853
|
+
graphDocs.push({
|
|
1854
|
+
id: `gcf:${conflictId || "unknown"}:rel:${relationIndex}`,
|
|
1855
|
+
text,
|
|
1856
|
+
source: "sessions_graph_conflict",
|
|
1857
|
+
timestamp: Number.isFinite(updatedAt) ? updatedAt : undefined,
|
|
1858
|
+
layer: (navSourceLayer || sourceLayer) === "active_only" ? "active" : "archive",
|
|
1859
|
+
summaryText: candidateSummary || undefined,
|
|
1860
|
+
sourceText: contextChunk || undefined,
|
|
1861
|
+
sourceMemoryId: navSourceMemoryId || relationKey,
|
|
1862
|
+
sourceEventId: navSourceEventId || sourceEventId || undefined,
|
|
1863
|
+
sourceFile: navSourceFile || sourceFile || undefined,
|
|
1864
|
+
sessionId: navSessionId || sessionId || undefined,
|
|
1865
|
+
entities: uniqueStrings([source, target]),
|
|
1866
|
+
relations: [relation],
|
|
1867
|
+
eventType: candidateEventType || "graph_conflict",
|
|
1868
|
+
qualityScore: typeof confidenceValue === "number" ? confidenceValue : 0.8,
|
|
1869
|
+
factStatus: status,
|
|
1870
|
+
evidenceIds,
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
catch (error) {
|
|
1875
|
+
options.logger.debug(`Skipping invalid conflict queue line: ${error}`);
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
const entitySummaryDocs = buildEntityGraphSummaryDocs(graphDocs.filter(item => item.factStatus === "active"));
|
|
1880
|
+
const wikiProjectionDocs = parseWikiProjectionDocuments(memoryRoot, options.logger);
|
|
1881
|
+
const docs = [
|
|
171
1882
|
...parseMarkdownFile(cortexRulesPath, "CORTEX_RULES.md"),
|
|
172
|
-
...parseMarkdownFile(memoryMdPath, "MEMORY.md"),
|
|
173
1883
|
...parseJsonlFile(activeSessionsPath, "sessions_active", options.logger),
|
|
174
1884
|
...parseJsonlFile(archiveSessionsPath, "sessions_archive", options.logger),
|
|
1885
|
+
...graphDocs,
|
|
1886
|
+
...entitySummaryDocs,
|
|
1887
|
+
...wikiProjectionDocs,
|
|
175
1888
|
];
|
|
1889
|
+
docsCache = { signature, docs };
|
|
1890
|
+
return docs;
|
|
1891
|
+
}
|
|
1892
|
+
function loadVectorFallbackCached() {
|
|
1893
|
+
const signature = fileSignature(vectorFallbackPath);
|
|
1894
|
+
if (vectorFallbackCache && vectorFallbackCache.signature === signature) {
|
|
1895
|
+
return vectorFallbackCache.docs;
|
|
1896
|
+
}
|
|
1897
|
+
const docs = parseVectorFallback(vectorFallbackPath, options.logger);
|
|
1898
|
+
vectorFallbackCache = { signature, docs };
|
|
1899
|
+
return docs;
|
|
1900
|
+
}
|
|
1901
|
+
function getBm25Tokens(doc, signature, channel) {
|
|
1902
|
+
if (bm25TokenCacheSignature !== signature) {
|
|
1903
|
+
bm25TokenCacheSignature = signature;
|
|
1904
|
+
bm25TokenCache = new Map();
|
|
1905
|
+
}
|
|
1906
|
+
const channelText = channel === "fulltext"
|
|
1907
|
+
? ((doc.sourceText || "").trim() || (doc.summaryText || doc.text || ""))
|
|
1908
|
+
: ((doc.summaryText || doc.text || "").trim());
|
|
1909
|
+
const key = `${channel}:${doc.source}|${doc.id}|${channelText.length}|${channelText.slice(0, 64)}`;
|
|
1910
|
+
const cached = bm25TokenCache.get(key);
|
|
1911
|
+
if (cached) {
|
|
1912
|
+
return cached;
|
|
1913
|
+
}
|
|
1914
|
+
const tokens = tokenize(channelText);
|
|
1915
|
+
bm25TokenCache.set(key, tokens);
|
|
1916
|
+
return tokens;
|
|
176
1917
|
}
|
|
177
1918
|
async function searchMemory(args) {
|
|
178
1919
|
const query = args.query?.trim();
|
|
179
1920
|
if (!query) {
|
|
180
|
-
return {
|
|
1921
|
+
return {
|
|
1922
|
+
results: [],
|
|
1923
|
+
semantic_results: [],
|
|
1924
|
+
keyword_results: [],
|
|
1925
|
+
strategy: "vector_sentence_and_keyword_parallel",
|
|
1926
|
+
};
|
|
181
1927
|
}
|
|
1928
|
+
const mode = args.mode === "lightweight" ? "lightweight" : "default";
|
|
1929
|
+
const lightweightMode = mode === "lightweight";
|
|
182
1930
|
const docs = loadAllDocuments();
|
|
183
|
-
const
|
|
1931
|
+
const hitStats = loadHitStats();
|
|
1932
|
+
const intent = classifyIntent(query);
|
|
1933
|
+
const preferredTypes = preferredEventTypes(intent);
|
|
1934
|
+
const plannedQueriesRaw = lightweightMode ? [query] : planQueryKeywords(query);
|
|
1935
|
+
const plannedQueries = plannedQueriesRaw.length > 0 ? plannedQueriesRaw : [query];
|
|
1936
|
+
const summaryChannelWeight = 1;
|
|
1937
|
+
const fulltextChannelWeight = 0.35;
|
|
1938
|
+
const maxCandidatePool = Math.max(1, Math.max(args.topK, 20));
|
|
1939
|
+
let queryEmbedding = null;
|
|
1940
|
+
const embeddingModel = options.embedding?.model || "";
|
|
1941
|
+
const embeddingApiKey = options.embedding?.apiKey || "";
|
|
1942
|
+
const embeddingBaseUrl = normalizeBaseUrl(options.embedding?.baseURL || options.embedding?.baseUrl);
|
|
1943
|
+
if (!lightweightMode && embeddingModel && embeddingApiKey && embeddingBaseUrl) {
|
|
1944
|
+
try {
|
|
1945
|
+
queryEmbedding = await requestEmbedding({
|
|
1946
|
+
text: query,
|
|
1947
|
+
model: embeddingModel,
|
|
1948
|
+
apiKey: embeddingApiKey,
|
|
1949
|
+
baseUrl: embeddingBaseUrl,
|
|
1950
|
+
dimensions: options.embedding?.dimensions,
|
|
1951
|
+
});
|
|
1952
|
+
}
|
|
1953
|
+
catch (error) {
|
|
1954
|
+
options.logger.warn(`Embedding query failed, fallback to lexical search: ${error}`);
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
const vectorDocsFromLance = queryEmbedding && queryEmbedding.length > 0
|
|
1958
|
+
? await searchLanceDb({ memoryRoot, queryEmbedding, limit: Math.max(20, args.topK * 8), logger: options.logger })
|
|
1959
|
+
: [];
|
|
1960
|
+
const vectorDocsFallback = vectorDocsFromLance.length > 0
|
|
1961
|
+
? []
|
|
1962
|
+
: loadVectorFallbackCached();
|
|
1963
|
+
const vectorDocs = [...vectorDocsFromLance, ...vectorDocsFallback];
|
|
1964
|
+
const archiveSourceById = new Map();
|
|
1965
|
+
for (const doc of docs) {
|
|
1966
|
+
if (doc.source !== "sessions_archive")
|
|
1967
|
+
continue;
|
|
1968
|
+
const key = (doc.sourceMemoryId || doc.id || "").trim();
|
|
1969
|
+
if (!key)
|
|
1970
|
+
continue;
|
|
1971
|
+
archiveSourceById.set(key, {
|
|
1972
|
+
sourceText: doc.sourceText,
|
|
1973
|
+
summaryText: doc.summaryText || doc.text,
|
|
1974
|
+
sourceFile: doc.sourceFile,
|
|
1975
|
+
});
|
|
1976
|
+
}
|
|
1977
|
+
for (const doc of vectorDocs) {
|
|
1978
|
+
const key = (doc.sourceMemoryId || "").trim();
|
|
1979
|
+
if (!key)
|
|
1980
|
+
continue;
|
|
1981
|
+
const linked = archiveSourceById.get(key);
|
|
1982
|
+
if (!linked)
|
|
1983
|
+
continue;
|
|
1984
|
+
if (!doc.sourceText && linked.sourceText) {
|
|
1985
|
+
doc.sourceText = linked.sourceText;
|
|
1986
|
+
}
|
|
1987
|
+
if (!doc.summaryText && linked.summaryText) {
|
|
1988
|
+
doc.summaryText = linked.summaryText;
|
|
1989
|
+
}
|
|
1990
|
+
if (!doc.sourceFile && linked.sourceFile) {
|
|
1991
|
+
doc.sourceFile = linked.sourceFile;
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
const graphDocs = docs
|
|
1995
|
+
.filter(doc => doc.source.startsWith("sessions_graph"))
|
|
184
1996
|
.map(doc => {
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
1997
|
+
const graphText = [
|
|
1998
|
+
doc.text,
|
|
1999
|
+
...(doc.relations || []).map(relation => `${relation.source} ${relation.type} ${relation.target} ${relation.fact_status || doc.factStatus || "active"}`),
|
|
2000
|
+
].join(" | ");
|
|
2001
|
+
return {
|
|
2002
|
+
...doc,
|
|
2003
|
+
text: graphText,
|
|
2004
|
+
};
|
|
2005
|
+
});
|
|
2006
|
+
const rulesDocs = docs.filter(doc => doc.source === "CORTEX_RULES.md");
|
|
2007
|
+
const archiveDocs = docs.filter(doc => doc.source === "sessions_active" || doc.source === "sessions_archive");
|
|
2008
|
+
const bm25Corpus = [...rulesDocs, ...archiveDocs, ...vectorDocs, ...graphDocs];
|
|
2009
|
+
const bm25Signature = `${docsCache?.signature || "na"}|vector:${vectorDocs.length}:${vectorDocs.slice(0, 40).map(item => `${item.id}:${item.text.length}`).join(",")}`;
|
|
2010
|
+
const rankByQuery = (plannedQuery, includeFulltext) => {
|
|
2011
|
+
const bm25Terms = tokenize(plannedQuery);
|
|
2012
|
+
const bm25StatsSummary = buildBm25Stats(bm25Corpus, bm25Terms, doc => getBm25Tokens(doc, bm25Signature, "summary"));
|
|
2013
|
+
const bm25StatsFulltext = includeFulltext
|
|
2014
|
+
? buildBm25Stats(bm25Corpus, bm25Terms, doc => getBm25Tokens(doc, bm25Signature, "fulltext"))
|
|
2015
|
+
: { avgDocLen: 1, docFreq: new Map() };
|
|
2016
|
+
const channels = {
|
|
2017
|
+
rules: [],
|
|
2018
|
+
archive: [],
|
|
2019
|
+
vector: [],
|
|
2020
|
+
graph: [],
|
|
2021
|
+
};
|
|
2022
|
+
const evaluateDoc = (doc, source) => {
|
|
2023
|
+
const summaryText = (doc.summaryText || doc.text || "").trim();
|
|
2024
|
+
const fulltextText = (doc.sourceText || "").trim();
|
|
2025
|
+
const summaryBm25 = bm25Score({
|
|
2026
|
+
queryTerms: bm25Terms,
|
|
2027
|
+
docText: summaryText,
|
|
2028
|
+
docTokens: getBm25Tokens(doc, bm25Signature, "summary"),
|
|
2029
|
+
docCount: bm25Corpus.length,
|
|
2030
|
+
avgDocLen: bm25StatsSummary.avgDocLen,
|
|
2031
|
+
docFreq: bm25StatsSummary.docFreq,
|
|
2032
|
+
});
|
|
2033
|
+
const fulltextBm25 = includeFulltext
|
|
2034
|
+
? bm25Score({
|
|
2035
|
+
queryTerms: bm25Terms,
|
|
2036
|
+
docText: fulltextText,
|
|
2037
|
+
docTokens: getBm25Tokens(doc, bm25Signature, "fulltext"),
|
|
2038
|
+
docCount: bm25Corpus.length,
|
|
2039
|
+
avgDocLen: bm25StatsFulltext.avgDocLen,
|
|
2040
|
+
docFreq: bm25StatsFulltext.docFreq,
|
|
2041
|
+
})
|
|
2042
|
+
: 0;
|
|
2043
|
+
const summaryCombined = scoreText(plannedQuery, summaryText) + summaryBm25 * readTuning.scoring.bm25Scale;
|
|
2044
|
+
const fulltextCombined = includeFulltext
|
|
2045
|
+
? scoreText(plannedQuery, fulltextText) + fulltextBm25 * readTuning.scoring.bm25Scale
|
|
2046
|
+
: 0;
|
|
2047
|
+
const lexicalCombined = summaryCombined * summaryChannelWeight + fulltextCombined * fulltextChannelWeight;
|
|
2048
|
+
const semantic = plannedQuery === query && queryEmbedding && Array.isArray(doc.embedding) && doc.embedding.length > 0
|
|
2049
|
+
? Math.max(0, cosineSimilarity(queryEmbedding, doc.embedding) * 5)
|
|
2050
|
+
: 0;
|
|
2051
|
+
if (lexicalCombined <= 0 && semantic <= 0) {
|
|
2052
|
+
return null;
|
|
2053
|
+
}
|
|
2054
|
+
if (source === "graph") {
|
|
2055
|
+
const status = docFactStatus(doc);
|
|
2056
|
+
if (status === "superseded" || status === "rejected") {
|
|
2057
|
+
return null;
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
const recency = recencyScore(doc.timestamp, readTuning.recency.buckets);
|
|
2061
|
+
const quality = typeof doc.qualityScore === "number" ? Math.max(0, Math.min(1, doc.qualityScore)) : 0.5;
|
|
2062
|
+
const typeMatch = preferredTypes.length > 0 && doc.eventType
|
|
2063
|
+
? (preferredTypes.includes(doc.eventType) ? 1 : 0)
|
|
2064
|
+
: 0.5;
|
|
2065
|
+
const graphMatch = source === "graph" ? 1 : 0;
|
|
2066
|
+
const sourceBaseWeight = sourceWeight(source, intent);
|
|
2067
|
+
const sourceConfigWeight = customChannelWeight(source, options.fusion);
|
|
2068
|
+
const lengthNorm = lengthNormalizeFactor(doc, options.fusion);
|
|
2069
|
+
const baseWeighted = (readTuning.scoring.lexicalWeight * lexicalCombined +
|
|
2070
|
+
readTuning.scoring.semanticWeight * (semantic * lengthNorm) +
|
|
2071
|
+
readTuning.scoring.recencyWeight * recency +
|
|
2072
|
+
readTuning.scoring.qualityWeight * quality +
|
|
2073
|
+
readTuning.scoring.typeMatchWeight * typeMatch +
|
|
2074
|
+
readTuning.scoring.graphMatchWeight * graphMatch) * sourceBaseWeight * sourceConfigWeight;
|
|
2075
|
+
const decayFactor = computeDecayFactor(doc.id, doc.eventType, doc.timestamp, options.memoryDecay, hitStats);
|
|
2076
|
+
const weighted = baseWeighted * decayFactor;
|
|
2077
|
+
return {
|
|
2078
|
+
doc,
|
|
2079
|
+
source,
|
|
2080
|
+
lexical: lexicalCombined,
|
|
2081
|
+
bm25: summaryBm25 + fulltextBm25,
|
|
2082
|
+
semantic,
|
|
2083
|
+
recency,
|
|
2084
|
+
quality,
|
|
2085
|
+
typeMatch,
|
|
2086
|
+
graphMatch,
|
|
2087
|
+
decayFactor,
|
|
2088
|
+
weighted,
|
|
2089
|
+
summaryCombined,
|
|
2090
|
+
fulltextCombined,
|
|
2091
|
+
};
|
|
2092
|
+
};
|
|
2093
|
+
for (const doc of rulesDocs) {
|
|
2094
|
+
const candidate = evaluateDoc(doc, "rules");
|
|
2095
|
+
if (candidate)
|
|
2096
|
+
channels.rules.push(candidate);
|
|
2097
|
+
}
|
|
2098
|
+
for (const doc of archiveDocs) {
|
|
2099
|
+
const candidate = evaluateDoc(doc, "archive");
|
|
2100
|
+
if (candidate)
|
|
2101
|
+
channels.archive.push(candidate);
|
|
2102
|
+
}
|
|
2103
|
+
for (const doc of vectorDocs) {
|
|
2104
|
+
const candidate = evaluateDoc(doc, "vector");
|
|
2105
|
+
if (candidate)
|
|
2106
|
+
channels.vector.push(candidate);
|
|
2107
|
+
}
|
|
2108
|
+
for (const doc of graphDocs) {
|
|
2109
|
+
const candidate = evaluateDoc(doc, "graph");
|
|
2110
|
+
if (candidate)
|
|
2111
|
+
channels.graph.push(candidate);
|
|
2112
|
+
}
|
|
2113
|
+
const rrfMap = new Map();
|
|
2114
|
+
const weightedMap = new Map();
|
|
2115
|
+
const rrfK = readTuning.rrf.k;
|
|
2116
|
+
for (const key of Object.keys(channels)) {
|
|
2117
|
+
const list = channels[key].sort((a, b) => b.weighted - a.weighted);
|
|
2118
|
+
const capped = list.slice(0, channelQuota(key, args.topK, options.fusion));
|
|
2119
|
+
for (let i = 0; i < capped.length; i += 1) {
|
|
2120
|
+
const candidate = capped[i];
|
|
2121
|
+
const rrf = 1 / (rrfK + i + 1);
|
|
2122
|
+
const mergeKey = mergeKeyFromDoc(candidate.doc);
|
|
2123
|
+
rrfMap.set(mergeKey, (rrfMap.get(mergeKey) || 0) + rrf);
|
|
2124
|
+
const current = weightedMap.get(mergeKey);
|
|
2125
|
+
if (!current || candidate.weighted > current.weighted) {
|
|
2126
|
+
weightedMap.set(mergeKey, candidate);
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
return [...weightedMap.entries()]
|
|
2131
|
+
.map(([mergeKey, candidate]) => ({
|
|
2132
|
+
id: candidate.doc.id,
|
|
2133
|
+
merge_key: mergeKey,
|
|
2134
|
+
source_memory_id: candidate.doc.sourceMemoryId || "",
|
|
2135
|
+
source_memory_canonical_id: candidate.doc.sourceMemoryCanonicalId || "",
|
|
2136
|
+
source_event_id: candidate.doc.sourceEventId || "",
|
|
2137
|
+
source_field: candidate.doc.sourceField || "",
|
|
2138
|
+
text: candidate.doc.summaryText || candidate.doc.text,
|
|
2139
|
+
source_text: candidate.doc.sourceText ? candidate.doc.sourceText.slice(0, 4000) : "",
|
|
2140
|
+
source_excerpt: candidate.doc.sourceText ? candidate.doc.sourceText.slice(0, 360) : "",
|
|
2141
|
+
source_file: candidate.doc.sourceFile || "",
|
|
2142
|
+
source: candidate.doc.source,
|
|
2143
|
+
layer: candidate.doc.layer || "",
|
|
2144
|
+
event_type: candidate.doc.eventType || "",
|
|
2145
|
+
fact_status: docFactStatus(candidate.doc),
|
|
2146
|
+
wiki_ref: candidate.doc.wikiRef || "",
|
|
2147
|
+
quality_score: candidate.quality,
|
|
2148
|
+
timestamp: candidate.doc.timestamp ? new Date(candidate.doc.timestamp).toISOString() : "",
|
|
2149
|
+
evidence_ids: docEvidenceIds(candidate.doc),
|
|
2150
|
+
score: candidate.weighted + (rrfMap.get(mergeKey) || 0) * readTuning.rrf.weight,
|
|
2151
|
+
score_breakdown: {
|
|
2152
|
+
lexical: Number(candidate.lexical.toFixed(4)),
|
|
2153
|
+
bm25: Number(candidate.bm25.toFixed(4)),
|
|
2154
|
+
semantic: Number(candidate.semantic.toFixed(4)),
|
|
2155
|
+
recency: Number(candidate.recency.toFixed(4)),
|
|
2156
|
+
quality: Number(candidate.quality.toFixed(4)),
|
|
2157
|
+
type: Number(candidate.typeMatch.toFixed(4)),
|
|
2158
|
+
graph: Number(candidate.graphMatch.toFixed(4)),
|
|
2159
|
+
summary: Number(candidate.summaryCombined.toFixed(4)),
|
|
2160
|
+
fulltext: Number(candidate.fulltextCombined.toFixed(4)),
|
|
2161
|
+
decay: Number(candidate.decayFactor.toFixed(4)),
|
|
2162
|
+
rrf: Number(((rrfMap.get(mergeKey) || 0) * readTuning.rrf.weight).toFixed(4)),
|
|
2163
|
+
weighted: Number(candidate.weighted.toFixed(4)),
|
|
2164
|
+
},
|
|
2165
|
+
reason_tags: [
|
|
2166
|
+
`intent:${intent.toLowerCase()}`,
|
|
2167
|
+
candidate.summaryCombined > 0 ? "summary_hit" : "",
|
|
2168
|
+
candidate.fulltextCombined > 0 ? "fulltext_hit" : "",
|
|
2169
|
+
candidate.semantic > 0 ? "vector_hit" : "",
|
|
2170
|
+
candidate.lexical > 0 ? "lexical_hit" : "",
|
|
2171
|
+
candidate.typeMatch >= 1 ? "event_type_match" : "event_type_weak",
|
|
2172
|
+
candidate.recency >= 0.8 ? "recent" : "historical",
|
|
2173
|
+
candidate.quality >= 0.7 ? "high_quality" : "normal_quality",
|
|
2174
|
+
candidate.decayFactor < 1 ? `decay:${candidate.decayFactor.toFixed(3)}` : "decay:1.000",
|
|
2175
|
+
`source:${candidate.source}`,
|
|
2176
|
+
`merge_key:${mergeKey}`,
|
|
2177
|
+
`query_term:${plannedQuery}`,
|
|
2178
|
+
].filter(Boolean),
|
|
2179
|
+
matched_keywords: [plannedQuery],
|
|
2180
|
+
}))
|
|
2181
|
+
.sort((a, b) => b.score - a.score)
|
|
2182
|
+
.slice(0, maxCandidatePool);
|
|
2183
|
+
};
|
|
2184
|
+
const queryRuns = await Promise.all(plannedQueries.map(async (plannedQuery) => {
|
|
2185
|
+
const stage1 = rankByQuery(plannedQuery, false);
|
|
2186
|
+
const needFallback = shouldTriggerFulltextFallback(stage1, args.topK);
|
|
2187
|
+
if (!needFallback || lightweightMode) {
|
|
2188
|
+
return { plannedQuery, ranked: stage1, fulltextFallback: false };
|
|
2189
|
+
}
|
|
2190
|
+
const stage2 = rankByQuery(plannedQuery, true);
|
|
2191
|
+
const merged = new Map();
|
|
2192
|
+
for (const item of [...stage1, ...stage2]) {
|
|
2193
|
+
const mergeKey = item.merge_key || item.id || "";
|
|
2194
|
+
const existing = merged.get(mergeKey);
|
|
2195
|
+
if (!existing) {
|
|
2196
|
+
merged.set(mergeKey, { ...item });
|
|
2197
|
+
continue;
|
|
2198
|
+
}
|
|
2199
|
+
const best = Number(item.score || 0) > Number(existing.score || 0)
|
|
2200
|
+
? { ...existing, ...item }
|
|
2201
|
+
: { ...item, ...existing };
|
|
2202
|
+
const mergedReasonTags = uniqueStrings([
|
|
2203
|
+
...(Array.isArray(existing.reason_tags) ? existing.reason_tags.map(v => String(v)) : []),
|
|
2204
|
+
...(Array.isArray(item.reason_tags) ? item.reason_tags.map(v => String(v)) : []),
|
|
2205
|
+
]);
|
|
2206
|
+
const mergedEvidenceIds = uniqueStrings([
|
|
2207
|
+
...(Array.isArray(existing.evidence_ids) ? existing.evidence_ids.map(v => String(v)) : []),
|
|
2208
|
+
...(Array.isArray(item.evidence_ids) ? item.evidence_ids.map(v => String(v)) : []),
|
|
2209
|
+
]);
|
|
2210
|
+
const mergedKeywords = uniqueStrings([
|
|
2211
|
+
...(Array.isArray(existing.matched_keywords) ? existing.matched_keywords.map(v => String(v)) : []),
|
|
2212
|
+
...(Array.isArray(item.matched_keywords) ? item.matched_keywords.map(v => String(v)) : []),
|
|
2213
|
+
]);
|
|
2214
|
+
merged.set(mergeKey, {
|
|
2215
|
+
...best,
|
|
2216
|
+
score: Math.max(existing.score || 0, item.score || 0),
|
|
2217
|
+
reason_tags: mergedReasonTags,
|
|
2218
|
+
evidence_ids: mergedEvidenceIds,
|
|
2219
|
+
matched_keywords: mergedKeywords,
|
|
2220
|
+
});
|
|
2221
|
+
}
|
|
2222
|
+
return {
|
|
2223
|
+
plannedQuery,
|
|
2224
|
+
ranked: [...merged.values()]
|
|
2225
|
+
.sort((a, b) => Number(b.score || 0) - Number(a.score || 0))
|
|
2226
|
+
.slice(0, maxCandidatePool)
|
|
2227
|
+
.map((item) => ({
|
|
2228
|
+
...item,
|
|
2229
|
+
reason_tags: uniqueStrings([
|
|
2230
|
+
...(Array.isArray(item.reason_tags) ? item.reason_tags.map((v) => String(v)) : []),
|
|
2231
|
+
"fulltext_fallback",
|
|
2232
|
+
]),
|
|
2233
|
+
})),
|
|
2234
|
+
fulltextFallback: true,
|
|
2235
|
+
};
|
|
2236
|
+
}));
|
|
2237
|
+
const mergedByQuery = new Map();
|
|
2238
|
+
for (const run of queryRuns) {
|
|
2239
|
+
for (const item of run.ranked) {
|
|
2240
|
+
const mergeKey = item.merge_key || item.id || "";
|
|
2241
|
+
const existing = mergedByQuery.get(mergeKey);
|
|
2242
|
+
if (!existing) {
|
|
2243
|
+
mergedByQuery.set(mergeKey, {
|
|
2244
|
+
...item,
|
|
2245
|
+
matched_keywords: uniqueStrings([run.plannedQuery]),
|
|
2246
|
+
fulltext_fallback_used: run.fulltextFallback,
|
|
2247
|
+
});
|
|
2248
|
+
continue;
|
|
2249
|
+
}
|
|
2250
|
+
const mergedReasonTags = uniqueStrings([
|
|
2251
|
+
...(Array.isArray(existing.reason_tags) ? existing.reason_tags.map(v => String(v)) : []),
|
|
2252
|
+
...(Array.isArray(item.reason_tags) ? item.reason_tags.map(v => String(v)) : []),
|
|
2253
|
+
]);
|
|
2254
|
+
const mergedEvidenceIds = uniqueStrings([
|
|
2255
|
+
...(Array.isArray(existing.evidence_ids) ? existing.evidence_ids.map(v => String(v)) : []),
|
|
2256
|
+
...(Array.isArray(item.evidence_ids) ? item.evidence_ids.map(v => String(v)) : []),
|
|
2257
|
+
]);
|
|
2258
|
+
const matchedKeywords = uniqueStrings([
|
|
2259
|
+
...(Array.isArray(existing.matched_keywords) ? existing.matched_keywords.map(v => String(v)) : []),
|
|
2260
|
+
run.plannedQuery,
|
|
2261
|
+
]);
|
|
2262
|
+
const preferred = Number(item.score || 0) > Number(existing.score || 0)
|
|
2263
|
+
? { ...existing, ...item }
|
|
2264
|
+
: { ...item, ...existing };
|
|
2265
|
+
mergedByQuery.set(mergeKey, {
|
|
2266
|
+
...preferred,
|
|
2267
|
+
score: Math.max(existing.score || 0, item.score || 0),
|
|
2268
|
+
reason_tags: mergedReasonTags,
|
|
2269
|
+
evidence_ids: mergedEvidenceIds,
|
|
2270
|
+
matched_keywords: matchedKeywords,
|
|
2271
|
+
fulltext_fallback_used: Boolean(existing.fulltext_fallback_used) || run.fulltextFallback,
|
|
2272
|
+
});
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
const lexicalRanked = [...mergedByQuery.values()]
|
|
2276
|
+
.map((item) => {
|
|
2277
|
+
const matchedKeywords = Array.isArray(item.matched_keywords)
|
|
2278
|
+
? uniqueStrings(item.matched_keywords.map(v => String(v)))
|
|
2279
|
+
: [query];
|
|
2280
|
+
const keywordBonus = Math.max(0, matchedKeywords.length - 1) * 0.12;
|
|
2281
|
+
const boosted = withRecencyBoost(Number(item.score || 0) + keywordBonus, typeof item.timestamp === "string" ? Date.parse(item.timestamp) : undefined, readTuning.recency.buckets);
|
|
2282
|
+
return {
|
|
2283
|
+
...item,
|
|
2284
|
+
matched_keywords: matchedKeywords,
|
|
2285
|
+
query_plan_keywords: plannedQueries,
|
|
2286
|
+
score: Number(boosted.toFixed(4)),
|
|
2287
|
+
reason_tags: uniqueStrings([
|
|
2288
|
+
...(Array.isArray(item.reason_tags) ? item.reason_tags.map((v) => String(v)) : []),
|
|
2289
|
+
`keyword_hits:${matchedKeywords.length}`,
|
|
2290
|
+
`query_plan:${plannedQueries.length}`,
|
|
2291
|
+
Boolean(item.fulltext_fallback_used) ? "fulltext_fallback_used" : "",
|
|
2292
|
+
]),
|
|
2293
|
+
};
|
|
188
2294
|
})
|
|
189
|
-
.
|
|
190
|
-
.
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
2295
|
+
.sort((a, b) => Number(b.score || 0) - Number(a.score || 0))
|
|
2296
|
+
.slice(0, maxCandidatePool);
|
|
2297
|
+
const isVectorSource = (value) => value.startsWith("vector_");
|
|
2298
|
+
const semanticResults = lexicalRanked
|
|
2299
|
+
.filter(item => isVectorSource(item.source) && Array.isArray(item.reason_tags) && item.reason_tags.includes("vector_hit"))
|
|
2300
|
+
.slice(0, Math.max(1, args.topK));
|
|
2301
|
+
const keywordResults = lexicalRanked
|
|
2302
|
+
.filter(item => isVectorSource(item.source) && Array.isArray(item.reason_tags) && item.reason_tags.includes("lexical_hit"))
|
|
2303
|
+
.slice(0, Math.max(1, args.topK));
|
|
2304
|
+
const rerankerModel = options.reranker?.model || "";
|
|
2305
|
+
const rerankerApiKey = options.reranker?.apiKey || "";
|
|
2306
|
+
const rerankerBaseUrl = normalizeBaseUrl(options.reranker?.baseURL || options.reranker?.baseUrl);
|
|
2307
|
+
const fusionEnabled = !lightweightMode && options.fusion?.enabled !== false;
|
|
2308
|
+
const llmModel = options.llm?.model || "";
|
|
2309
|
+
const llmApiKey = options.llm?.apiKey || "";
|
|
2310
|
+
const llmBaseUrl = normalizeBaseUrl(options.llm?.baseURL || options.llm?.baseUrl);
|
|
2311
|
+
const fusionAuthoritative = options.fusion?.authoritative !== false;
|
|
2312
|
+
const skipRerankerForFusion = fusionEnabled && fusionAuthoritative && llmModel && llmApiKey && llmBaseUrl;
|
|
2313
|
+
let rerankedSimple = lexicalRanked.map(item => ({
|
|
2314
|
+
id: item.id,
|
|
2315
|
+
merge_key: item.merge_key,
|
|
2316
|
+
text: item.text,
|
|
2317
|
+
source: item.source,
|
|
2318
|
+
score: item.score,
|
|
197
2319
|
}));
|
|
198
|
-
|
|
2320
|
+
if (!lightweightMode && rerankerModel && rerankerApiKey && rerankerBaseUrl && lexicalRanked.length > 1 && !skipRerankerForFusion) {
|
|
2321
|
+
try {
|
|
2322
|
+
rerankedSimple = await requestRerank({
|
|
2323
|
+
query,
|
|
2324
|
+
candidates: lexicalRanked.map(item => ({ id: item.id, text: item.text, source: item.source, score: item.score })),
|
|
2325
|
+
model: rerankerModel,
|
|
2326
|
+
apiKey: rerankerApiKey,
|
|
2327
|
+
baseUrl: rerankerBaseUrl,
|
|
2328
|
+
});
|
|
2329
|
+
rerankedSimple = rerankedSimple.map(item => {
|
|
2330
|
+
const found = lexicalRanked.find(entry => entry.id === item.id);
|
|
2331
|
+
return { ...item, merge_key: found?.merge_key || item.id };
|
|
2332
|
+
});
|
|
2333
|
+
}
|
|
2334
|
+
catch (error) {
|
|
2335
|
+
options.logger.warn(`Reranker failed, keep hybrid ranking: ${error}`);
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
const ranked = rerankedSimple.slice(0, Math.max(1, args.topK)).map(item => {
|
|
2339
|
+
const hit = lexicalRanked.find(entry => entry.id === item.id);
|
|
2340
|
+
return {
|
|
2341
|
+
id: item.id,
|
|
2342
|
+
merge_key: hit?.merge_key || item.merge_key || item.id,
|
|
2343
|
+
source_memory_id: hit?.source_memory_id || "",
|
|
2344
|
+
source_memory_canonical_id: hit?.source_memory_canonical_id || "",
|
|
2345
|
+
source_event_id: hit?.source_event_id || "",
|
|
2346
|
+
source_field: hit?.source_field || "",
|
|
2347
|
+
fulltext_event_id: (hit?.source_event_id || hit?.source_memory_id || item.id || ""),
|
|
2348
|
+
text: item.text,
|
|
2349
|
+
source_text: hit?.source_text || "",
|
|
2350
|
+
source_excerpt: hit?.source_excerpt || "",
|
|
2351
|
+
source_file: hit?.source_file || "",
|
|
2352
|
+
source: item.source,
|
|
2353
|
+
layer: hit?.layer || "",
|
|
2354
|
+
event_type: hit?.event_type || "",
|
|
2355
|
+
fact_status: hit?.fact_status || "active",
|
|
2356
|
+
wiki_ref: hit?.wiki_ref || "",
|
|
2357
|
+
quality_score: hit?.quality_score ?? 0,
|
|
2358
|
+
timestamp: hit?.timestamp || "",
|
|
2359
|
+
evidence_ids: Array.isArray(hit?.evidence_ids) ? hit?.evidence_ids : [],
|
|
2360
|
+
score: Number(item.score.toFixed(4)),
|
|
2361
|
+
score_breakdown: hit?.score_breakdown || {},
|
|
2362
|
+
reason_tags: Array.isArray(hit?.reason_tags) ? hit?.reason_tags : [],
|
|
2363
|
+
explain: {
|
|
2364
|
+
merge_key: hit?.merge_key || item.merge_key || item.id,
|
|
2365
|
+
source_memory_id: hit?.source_memory_id || "",
|
|
2366
|
+
source_memory_canonical_id: hit?.source_memory_canonical_id || "",
|
|
2367
|
+
source_event_id: hit?.source_event_id || "",
|
|
2368
|
+
source_field: hit?.source_field || "",
|
|
2369
|
+
channel: item.source,
|
|
2370
|
+
source_file: hit?.source_file || "",
|
|
2371
|
+
layer: hit?.layer || "",
|
|
2372
|
+
fact_status: hit?.fact_status || "active",
|
|
2373
|
+
wiki_ref: hit?.wiki_ref || "",
|
|
2374
|
+
evidence_ids: Array.isArray(hit?.evidence_ids) ? hit?.evidence_ids : [],
|
|
2375
|
+
score_breakdown: hit?.score_breakdown || {},
|
|
2376
|
+
reason_tags: Array.isArray(hit?.reason_tags) ? hit?.reason_tags : [],
|
|
2377
|
+
},
|
|
2378
|
+
};
|
|
2379
|
+
});
|
|
2380
|
+
const minLexicalHits = Math.max(0, Math.floor(options.fusion?.minLexicalHits ?? 1));
|
|
2381
|
+
const minSemanticHits = Math.max(0, Math.floor(options.fusion?.minSemanticHits ?? 1));
|
|
2382
|
+
const fallbackPool = lexicalRanked.filter(item => !ranked.some(existing => existing.id === item.id));
|
|
2383
|
+
const lexicalCount = ranked.filter(item => item.reason_tags.includes("lexical_hit")).length;
|
|
2384
|
+
const semanticCount = ranked.filter(item => item.reason_tags.includes("vector_hit")).length;
|
|
2385
|
+
if (semanticCount < minSemanticHits) {
|
|
2386
|
+
const needed = minSemanticHits - semanticCount;
|
|
2387
|
+
const supplement = fallbackPool.filter(item => item.reason_tags.includes("vector_hit")).slice(0, needed);
|
|
2388
|
+
for (const item of supplement) {
|
|
2389
|
+
ranked.push({
|
|
2390
|
+
id: item.id,
|
|
2391
|
+
merge_key: item.merge_key,
|
|
2392
|
+
source_memory_id: item.source_memory_id,
|
|
2393
|
+
source_memory_canonical_id: item.source_memory_canonical_id,
|
|
2394
|
+
source_event_id: item.source_event_id || "",
|
|
2395
|
+
source_field: item.source_field || "",
|
|
2396
|
+
fulltext_event_id: item.source_event_id || item.source_memory_id || item.id,
|
|
2397
|
+
text: item.text,
|
|
2398
|
+
source_text: item.source_text || "",
|
|
2399
|
+
source_excerpt: item.source_excerpt || "",
|
|
2400
|
+
source_file: item.source_file || "",
|
|
2401
|
+
source: item.source,
|
|
2402
|
+
layer: item.layer,
|
|
2403
|
+
event_type: item.event_type,
|
|
2404
|
+
fact_status: item.fact_status || "active",
|
|
2405
|
+
wiki_ref: item.wiki_ref || "",
|
|
2406
|
+
quality_score: item.quality_score,
|
|
2407
|
+
timestamp: item.timestamp,
|
|
2408
|
+
evidence_ids: Array.isArray(item.evidence_ids) ? item.evidence_ids : [],
|
|
2409
|
+
score: Number(item.score.toFixed(4)),
|
|
2410
|
+
score_breakdown: item.score_breakdown || {},
|
|
2411
|
+
reason_tags: Array.isArray(item.reason_tags) ? item.reason_tags : [],
|
|
2412
|
+
explain: {
|
|
2413
|
+
merge_key: item.merge_key,
|
|
2414
|
+
source_memory_id: item.source_memory_id,
|
|
2415
|
+
source_memory_canonical_id: item.source_memory_canonical_id,
|
|
2416
|
+
source_event_id: item.source_event_id || "",
|
|
2417
|
+
source_field: item.source_field || "",
|
|
2418
|
+
channel: item.source,
|
|
2419
|
+
source_file: item.source_file || "",
|
|
2420
|
+
layer: item.layer,
|
|
2421
|
+
fact_status: item.fact_status || "active",
|
|
2422
|
+
wiki_ref: item.wiki_ref || "",
|
|
2423
|
+
evidence_ids: Array.isArray(item.evidence_ids) ? item.evidence_ids : [],
|
|
2424
|
+
score_breakdown: item.score_breakdown || {},
|
|
2425
|
+
reason_tags: Array.isArray(item.reason_tags) ? item.reason_tags : [],
|
|
2426
|
+
},
|
|
2427
|
+
});
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
if (lexicalCount < minLexicalHits) {
|
|
2431
|
+
const needed = minLexicalHits - lexicalCount;
|
|
2432
|
+
const supplement = fallbackPool.filter(item => item.reason_tags.includes("lexical_hit")).slice(0, needed);
|
|
2433
|
+
for (const item of supplement) {
|
|
2434
|
+
if (ranked.some(existing => existing.id === item.id)) {
|
|
2435
|
+
continue;
|
|
2436
|
+
}
|
|
2437
|
+
ranked.push({
|
|
2438
|
+
id: item.id,
|
|
2439
|
+
merge_key: item.merge_key,
|
|
2440
|
+
source_memory_id: item.source_memory_id,
|
|
2441
|
+
source_memory_canonical_id: item.source_memory_canonical_id,
|
|
2442
|
+
source_event_id: item.source_event_id || "",
|
|
2443
|
+
source_field: item.source_field || "",
|
|
2444
|
+
fulltext_event_id: item.source_event_id || item.source_memory_id || item.id,
|
|
2445
|
+
text: item.text,
|
|
2446
|
+
source_text: item.source_text || "",
|
|
2447
|
+
source_excerpt: item.source_excerpt || "",
|
|
2448
|
+
source_file: item.source_file || "",
|
|
2449
|
+
source: item.source,
|
|
2450
|
+
layer: item.layer,
|
|
2451
|
+
event_type: item.event_type,
|
|
2452
|
+
fact_status: item.fact_status || "active",
|
|
2453
|
+
wiki_ref: item.wiki_ref || "",
|
|
2454
|
+
quality_score: item.quality_score,
|
|
2455
|
+
timestamp: item.timestamp,
|
|
2456
|
+
evidence_ids: Array.isArray(item.evidence_ids) ? item.evidence_ids : [],
|
|
2457
|
+
score: Number(item.score.toFixed(4)),
|
|
2458
|
+
score_breakdown: item.score_breakdown || {},
|
|
2459
|
+
reason_tags: Array.isArray(item.reason_tags) ? item.reason_tags : [],
|
|
2460
|
+
explain: {
|
|
2461
|
+
merge_key: item.merge_key,
|
|
2462
|
+
source_memory_id: item.source_memory_id,
|
|
2463
|
+
source_memory_canonical_id: item.source_memory_canonical_id,
|
|
2464
|
+
source_event_id: item.source_event_id || "",
|
|
2465
|
+
source_field: item.source_field || "",
|
|
2466
|
+
channel: item.source,
|
|
2467
|
+
source_file: item.source_file || "",
|
|
2468
|
+
layer: item.layer,
|
|
2469
|
+
fact_status: item.fact_status || "active",
|
|
2470
|
+
wiki_ref: item.wiki_ref || "",
|
|
2471
|
+
evidence_ids: Array.isArray(item.evidence_ids) ? item.evidence_ids : [],
|
|
2472
|
+
score_breakdown: item.score_breakdown || {},
|
|
2473
|
+
reason_tags: Array.isArray(item.reason_tags) ? item.reason_tags : [],
|
|
2474
|
+
},
|
|
2475
|
+
});
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
ranked.sort((a, b) => b.score - a.score);
|
|
2479
|
+
if (fusionEnabled && llmModel && llmApiKey && llmBaseUrl && ranked.length > 1) {
|
|
2480
|
+
try {
|
|
2481
|
+
const maxCandidates = Math.max(4, Math.min(20, options.fusion?.maxCandidates ?? 10));
|
|
2482
|
+
const fusion = await requestFusion({
|
|
2483
|
+
query,
|
|
2484
|
+
candidates: ranked.slice(0, maxCandidates).map(item => ({
|
|
2485
|
+
id: item.id,
|
|
2486
|
+
text: item.text,
|
|
2487
|
+
source_excerpt: typeof item.source_excerpt === "string" ? item.source_excerpt : "",
|
|
2488
|
+
source_file: typeof item.source_file === "string" ? item.source_file : "",
|
|
2489
|
+
source_memory_id: typeof item.source_memory_id === "string" ? item.source_memory_id : "",
|
|
2490
|
+
source_memory_canonical_id: typeof item.source_memory_canonical_id === "string" ? item.source_memory_canonical_id : "",
|
|
2491
|
+
source_layer: typeof item.layer === "string" ? item.layer : "",
|
|
2492
|
+
source_event_id: typeof item.source_event_id === "string" ? item.source_event_id : "",
|
|
2493
|
+
source_field: item.source_field === "summary" || item.source_field === "evidence" ? item.source_field : "",
|
|
2494
|
+
source: item.source,
|
|
2495
|
+
event_type: item.event_type,
|
|
2496
|
+
quality_score: item.quality_score,
|
|
2497
|
+
timestamp: item.timestamp,
|
|
2498
|
+
score: item.score,
|
|
2499
|
+
reason_tags: Array.isArray(item.reason_tags) ? item.reason_tags : [],
|
|
2500
|
+
})),
|
|
2501
|
+
llm: {
|
|
2502
|
+
model: llmModel,
|
|
2503
|
+
apiKey: llmApiKey,
|
|
2504
|
+
baseUrl: llmBaseUrl,
|
|
2505
|
+
},
|
|
2506
|
+
});
|
|
2507
|
+
if (fusion && fusion.canonical_answer) {
|
|
2508
|
+
if (!Array.isArray(fusion.evidence_ids) || fusion.evidence_ids.length === 0) {
|
|
2509
|
+
throw new Error("fusion_missing_whitelisted_evidence");
|
|
2510
|
+
}
|
|
2511
|
+
const fusedEvidenceIds = uniqueStrings(fusion.evidence_ids.flatMap(item => {
|
|
2512
|
+
const linked = ranked.find(candidate => candidate.id === item);
|
|
2513
|
+
if (!linked)
|
|
2514
|
+
return [item];
|
|
2515
|
+
const linkedEvidence = Array.isArray(linked.evidence_ids) ? linked.evidence_ids : [];
|
|
2516
|
+
return linkedEvidence.length > 0 ? linkedEvidence : [item];
|
|
2517
|
+
}));
|
|
2518
|
+
const wikiRefs = uniqueStrings(fusion.evidence_ids.flatMap(item => {
|
|
2519
|
+
const linked = ranked.find(candidate => candidate.id === item);
|
|
2520
|
+
const wikiRef = typeof linked?.wiki_ref === "string" ? linked.wiki_ref : "";
|
|
2521
|
+
return wikiRef ? [wikiRef] : [];
|
|
2522
|
+
}));
|
|
2523
|
+
const fulltextFetchHints = (Array.isArray(fusion.need_fulltext_event_ids) ? fusion.need_fulltext_event_ids : [])
|
|
2524
|
+
.map(eventId => {
|
|
2525
|
+
const linked = ranked.find(item => item.source_memory_id === eventId ||
|
|
2526
|
+
item.source_memory_canonical_id === eventId ||
|
|
2527
|
+
item.id === eventId);
|
|
2528
|
+
return {
|
|
2529
|
+
event_id: eventId,
|
|
2530
|
+
source_file: linked?.source_file || "",
|
|
2531
|
+
source_excerpt: linked?.source_excerpt || "",
|
|
2532
|
+
};
|
|
2533
|
+
});
|
|
2534
|
+
const fusedItem = {
|
|
2535
|
+
id: `fusion_${Date.now().toString(36)}`,
|
|
2536
|
+
text: fusion.canonical_answer,
|
|
2537
|
+
source: "llm_fusion",
|
|
2538
|
+
event_type: "fusion",
|
|
2539
|
+
quality_score: Number(fusion.confidence.toFixed(4)),
|
|
2540
|
+
timestamp: new Date().toISOString(),
|
|
2541
|
+
score: Number((Math.max(...ranked.map(item => item.score)) + 1).toFixed(4)),
|
|
2542
|
+
reason_tags: ["llm_fused_authoritative", `evidence:${fusion.evidence_ids.length}`],
|
|
2543
|
+
explain: {
|
|
2544
|
+
channel: "llm_fusion",
|
|
2545
|
+
fused_from: ranked.slice(0, maxCandidates).map(item => item.id),
|
|
2546
|
+
reason_tags: ["llm_fused_authoritative", `evidence:${fusion.evidence_ids.length}`],
|
|
2547
|
+
},
|
|
2548
|
+
fused_coverage_note: fusion.coverage_note || "",
|
|
2549
|
+
fused_facts: fusion.facts,
|
|
2550
|
+
fused_timeline: fusion.timeline || [],
|
|
2551
|
+
fused_entities: fusion.entities || [],
|
|
2552
|
+
fused_decisions: fusion.decisions || [],
|
|
2553
|
+
fused_fixes: fusion.fixes || [],
|
|
2554
|
+
fused_preferences: fusion.preferences || [],
|
|
2555
|
+
fused_risks: fusion.risks || [],
|
|
2556
|
+
fused_action_items: fusion.action_items || [],
|
|
2557
|
+
fused_conflicts: fusion.conflicts,
|
|
2558
|
+
evidence_ids: fusedEvidenceIds,
|
|
2559
|
+
wiki_refs: wikiRefs,
|
|
2560
|
+
fused_evidence_ids: fusedEvidenceIds,
|
|
2561
|
+
fused_need_fulltext_event_ids: fusion.need_fulltext_event_ids || [],
|
|
2562
|
+
fulltext_fetch_hints: fulltextFetchHints,
|
|
2563
|
+
};
|
|
2564
|
+
const authoritative = options.fusion?.authoritative !== false;
|
|
2565
|
+
if (authoritative) {
|
|
2566
|
+
markHit(Array.isArray(fusion.evidence_ids) ? fusion.evidence_ids : []);
|
|
2567
|
+
return {
|
|
2568
|
+
results: [fusedItem],
|
|
2569
|
+
semantic_results: semanticResults,
|
|
2570
|
+
keyword_results: keywordResults,
|
|
2571
|
+
strategy: "vector_sentence_and_keyword_parallel",
|
|
2572
|
+
};
|
|
2573
|
+
}
|
|
2574
|
+
const merged = [fusedItem, ...ranked];
|
|
2575
|
+
markHit([
|
|
2576
|
+
...(Array.isArray(fusion.evidence_ids) ? fusion.evidence_ids : []),
|
|
2577
|
+
...ranked.map(item => item.id),
|
|
2578
|
+
]);
|
|
2579
|
+
return {
|
|
2580
|
+
results: merged.slice(0, Math.max(1, args.topK)),
|
|
2581
|
+
semantic_results: semanticResults,
|
|
2582
|
+
keyword_results: keywordResults,
|
|
2583
|
+
strategy: "vector_sentence_and_keyword_parallel",
|
|
2584
|
+
};
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
catch (error) {
|
|
2588
|
+
options.logger.warn(`LLM fusion failed, fallback to reranked results: ${error}`);
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
const finalRanked = ranked.slice(0, Math.max(1, args.topK));
|
|
2592
|
+
markHit(finalRanked.map(item => item.id));
|
|
2593
|
+
return {
|
|
2594
|
+
results: finalRanked,
|
|
2595
|
+
semantic_results: semanticResults,
|
|
2596
|
+
keyword_results: keywordResults,
|
|
2597
|
+
strategy: "vector_sentence_and_keyword_parallel",
|
|
2598
|
+
};
|
|
199
2599
|
}
|
|
200
2600
|
async function getHotContext(args) {
|
|
201
2601
|
const limit = Math.max(1, args.limit);
|
|
202
2602
|
const docs = loadAllDocuments();
|
|
203
2603
|
const coreRules = docs.find(doc => doc.source === "CORTEX_RULES.md");
|
|
204
|
-
const
|
|
205
|
-
|
|
2604
|
+
const ruleBudget = Math.max(1, Math.min(6, Math.floor(limit / 3)));
|
|
2605
|
+
const archiveDocs = docs
|
|
2606
|
+
.filter(doc => doc.source === "sessions_archive")
|
|
206
2607
|
.sort((a, b) => (b.timestamp ?? 0) - (a.timestamp ?? 0))
|
|
207
2608
|
.slice(0, limit);
|
|
2609
|
+
const issueFixPairs = docs
|
|
2610
|
+
.filter(doc => doc.source === "sessions_archive" && (doc.eventType === "issue" || doc.eventType === "fix"))
|
|
2611
|
+
.sort((a, b) => (b.timestamp ?? 0) - (a.timestamp ?? 0))
|
|
2612
|
+
.slice(0, 2);
|
|
208
2613
|
const result = [];
|
|
209
2614
|
if (coreRules) {
|
|
210
|
-
|
|
2615
|
+
const selectedRules = extractPrioritizedRuleLines(coreRules.text, ruleBudget);
|
|
2616
|
+
if (selectedRules.length > 0) {
|
|
2617
|
+
result.push({
|
|
2618
|
+
id: `${coreRules.id}.hot`,
|
|
2619
|
+
text: `# Hot Rules\n${selectedRules.map((line, index) => `${index + 1}. ${line}`).join("\n")}`,
|
|
2620
|
+
source: coreRules.source,
|
|
2621
|
+
});
|
|
2622
|
+
}
|
|
211
2623
|
}
|
|
212
|
-
for (const doc of
|
|
2624
|
+
for (const doc of [...issueFixPairs, ...archiveDocs]) {
|
|
213
2625
|
result.push({ id: doc.id, text: doc.text, source: doc.source });
|
|
214
2626
|
}
|
|
215
2627
|
return { context: result.slice(0, limit) };
|
|
@@ -223,6 +2635,25 @@ function createReadStore(options) {
|
|
|
223
2635
|
age_seconds: args.cachedAutoSearch.ageSeconds,
|
|
224
2636
|
};
|
|
225
2637
|
}
|
|
2638
|
+
if (!result.auto_search) {
|
|
2639
|
+
const docs = loadAllDocuments()
|
|
2640
|
+
.filter(doc => doc.source === "sessions_archive" && doc.sessionId === args.sessionId)
|
|
2641
|
+
.sort((a, b) => (b.timestamp ?? 0) - (a.timestamp ?? 0));
|
|
2642
|
+
const latest = docs[0];
|
|
2643
|
+
if (latest && latest.text.trim()) {
|
|
2644
|
+
const autoQuery = latest.text.slice(0, Math.max(20, readTuning.autoContext.queryMaxChars));
|
|
2645
|
+
const light = await searchMemory({
|
|
2646
|
+
query: autoQuery,
|
|
2647
|
+
topK: 3,
|
|
2648
|
+
mode: readTuning.autoContext.lightweightSearch ? "lightweight" : "default",
|
|
2649
|
+
});
|
|
2650
|
+
result.auto_search = {
|
|
2651
|
+
query: autoQuery,
|
|
2652
|
+
results: light.results,
|
|
2653
|
+
age_seconds: 0,
|
|
2654
|
+
};
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
226
2657
|
if (args.includeHot) {
|
|
227
2658
|
const hot = await getHotContext({ limit: 20 });
|
|
228
2659
|
result.hot_context = hot.context;
|