openclaw-cortex-memory 0.1.0-Alpha.7 → 0.1.0-Alpha.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -45
- package/SKILL.md +46 -51
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +127 -11
- package/dist/index.js.map +1 -1
- package/dist/openclaw.plugin.json +1 -2
- package/dist/src/engine/memory_engine.d.ts +2 -1
- package/dist/src/engine/memory_engine.d.ts.map +1 -1
- package/dist/src/engine/ts_engine.d.ts +59 -0
- package/dist/src/engine/ts_engine.d.ts.map +1 -1
- package/dist/src/engine/ts_engine.js +609 -0
- package/dist/src/engine/ts_engine.js.map +1 -1
- package/dist/src/engine/types.d.ts +7 -0
- package/dist/src/engine/types.d.ts.map +1 -1
- package/dist/src/session/session_end.d.ts.map +1 -1
- package/dist/src/session/session_end.js +18 -4
- package/dist/src/session/session_end.js.map +1 -1
- package/dist/src/store/archive_store.d.ts +24 -0
- package/dist/src/store/archive_store.d.ts.map +1 -1
- package/dist/src/store/archive_store.js +217 -24
- package/dist/src/store/archive_store.js.map +1 -1
- package/dist/src/store/embedding_utils.d.ts +32 -0
- package/dist/src/store/embedding_utils.d.ts.map +1 -0
- package/dist/src/store/embedding_utils.js +173 -0
- package/dist/src/store/embedding_utils.js.map +1 -0
- package/dist/src/store/read_store.d.ts +20 -0
- package/dist/src/store/read_store.d.ts.map +1 -1
- package/dist/src/store/read_store.js +336 -21
- package/dist/src/store/read_store.js.map +1 -1
- package/dist/src/store/vector_store.d.ts +13 -0
- package/dist/src/store/vector_store.d.ts.map +1 -1
- package/dist/src/store/vector_store.js +59 -1
- package/dist/src/store/vector_store.js.map +1 -1
- package/dist/src/store/write_store.d.ts +35 -0
- package/dist/src/store/write_store.d.ts.map +1 -1
- package/dist/src/store/write_store.js +163 -14
- package/dist/src/store/write_store.js.map +1 -1
- package/dist/src/sync/session_sync.d.ts +44 -2
- package/dist/src/sync/session_sync.d.ts.map +1 -1
- package/dist/src/sync/session_sync.js +364 -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/openclaw.plugin.json +1 -2
- package/package.json +3 -2
- package/scripts/cli.js +13 -13
- package/scripts/uninstall.js +7 -1
|
@@ -37,6 +37,11 @@ exports.createTsEngine = createTsEngine;
|
|
|
37
37
|
const fs = __importStar(require("fs"));
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
39
|
const ontology_1 = require("../graph/ontology");
|
|
40
|
+
const PROMPT_VERSIONS = {
|
|
41
|
+
write_gate: "write-gate.v1.1.0",
|
|
42
|
+
session_end_write: "session-end-write.v1.1.0",
|
|
43
|
+
read_fusion: "read-fusion.v1.1.0",
|
|
44
|
+
};
|
|
40
45
|
function createTsEngine(deps) {
|
|
41
46
|
const graphSchema = (0, ontology_1.loadGraphSchema)(deps.projectRoot);
|
|
42
47
|
const sessionMessageBuffer = new Map();
|
|
@@ -98,6 +103,287 @@ function createTsEngine(deps) {
|
|
|
98
103
|
archivePath: path.join(deps.memoryRoot, "sessions", "archive", "sessions.jsonl"),
|
|
99
104
|
};
|
|
100
105
|
}
|
|
106
|
+
function parseJsonFile(filePath) {
|
|
107
|
+
try {
|
|
108
|
+
if (!fs.existsSync(filePath)) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const raw = fs.readFileSync(filePath, "utf-8").trim();
|
|
112
|
+
if (!raw) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
return JSON.parse(raw);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function embeddingStats(records) {
|
|
122
|
+
let ok = 0;
|
|
123
|
+
let failed = 0;
|
|
124
|
+
let pending = 0;
|
|
125
|
+
for (const record of records) {
|
|
126
|
+
const explicit = typeof record.embedding_status === "string" ? record.embedding_status.trim() : "";
|
|
127
|
+
const hasEmbedding = Array.isArray(record.embedding) && record.embedding.length > 0;
|
|
128
|
+
if (explicit === "ok" || hasEmbedding) {
|
|
129
|
+
ok += 1;
|
|
130
|
+
}
|
|
131
|
+
else if (explicit === "failed") {
|
|
132
|
+
failed += 1;
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
pending += 1;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const total = records.length;
|
|
139
|
+
const coverage = total > 0 ? Number((ok / total).toFixed(4)) : 0;
|
|
140
|
+
return { total, ok, failed, pending, coverage };
|
|
141
|
+
}
|
|
142
|
+
function normalizeBaseUrl(value) {
|
|
143
|
+
if (!value)
|
|
144
|
+
return "";
|
|
145
|
+
return value.endsWith("/") ? value.slice(0, -1) : value;
|
|
146
|
+
}
|
|
147
|
+
function estimateTokenCount(text) {
|
|
148
|
+
const parts = text
|
|
149
|
+
.split(/[\s,.;:!?,。;:!?、()()[\]{}"'`~]+/)
|
|
150
|
+
.map(part => part.trim())
|
|
151
|
+
.filter(Boolean);
|
|
152
|
+
return parts.length;
|
|
153
|
+
}
|
|
154
|
+
function buildVectorSourceText(record, layer) {
|
|
155
|
+
if (layer === "active") {
|
|
156
|
+
const content = typeof record.content === "string" && record.content.trim()
|
|
157
|
+
? record.content.trim()
|
|
158
|
+
: (typeof record.text === "string" ? record.text.trim() : "");
|
|
159
|
+
return content;
|
|
160
|
+
}
|
|
161
|
+
const summary = typeof record.summary === "string" ? record.summary.trim() : "";
|
|
162
|
+
const eventType = typeof record.event_type === "string" ? record.event_type.trim() : "insight";
|
|
163
|
+
const outcome = typeof record.outcome === "string" ? record.outcome.trim() : "";
|
|
164
|
+
const sourceFile = typeof record.source_file === "string" ? record.source_file.trim() : "";
|
|
165
|
+
const actor = typeof record.actor === "string" ? record.actor.trim() : "";
|
|
166
|
+
const entities = Array.isArray(record.entities)
|
|
167
|
+
? record.entities.filter(v => typeof v === "string").map(v => String(v).trim()).filter(Boolean)
|
|
168
|
+
: [];
|
|
169
|
+
const relations = Array.isArray(record.relations)
|
|
170
|
+
? record.relations
|
|
171
|
+
.map(v => {
|
|
172
|
+
if (!v || typeof v !== "object")
|
|
173
|
+
return "";
|
|
174
|
+
const relation = v;
|
|
175
|
+
const source = typeof relation.source === "string" ? relation.source.trim() : "";
|
|
176
|
+
const target = typeof relation.target === "string" ? relation.target.trim() : "";
|
|
177
|
+
const type = typeof relation.type === "string" ? relation.type.trim() : "related_to";
|
|
178
|
+
if (!source || !target)
|
|
179
|
+
return "";
|
|
180
|
+
return `${source} -[${type}]-> ${target}`;
|
|
181
|
+
})
|
|
182
|
+
.filter(Boolean)
|
|
183
|
+
: [];
|
|
184
|
+
const lines = [
|
|
185
|
+
`event_type: ${eventType}`,
|
|
186
|
+
`summary: ${summary}`,
|
|
187
|
+
`outcome: ${outcome}`,
|
|
188
|
+
`entities: ${entities.join(", ")}`,
|
|
189
|
+
`source_file: ${sourceFile}`,
|
|
190
|
+
`actor: ${actor}`,
|
|
191
|
+
relations.length > 0 ? `relations: ${relations.join(" ; ")}` : "",
|
|
192
|
+
].filter(Boolean);
|
|
193
|
+
return lines.join("\n").trim();
|
|
194
|
+
}
|
|
195
|
+
function splitTextChunks(text, chunkSize, chunkOverlap) {
|
|
196
|
+
const normalizedSize = Number.isFinite(chunkSize) && chunkSize >= 200 ? Math.floor(chunkSize) : 600;
|
|
197
|
+
const normalizedOverlap = Number.isFinite(chunkOverlap) && chunkOverlap >= 0
|
|
198
|
+
? Math.floor(chunkOverlap)
|
|
199
|
+
: 100;
|
|
200
|
+
const overlap = Math.min(normalizedOverlap, Math.max(0, normalizedSize - 50));
|
|
201
|
+
const output = [];
|
|
202
|
+
let cursor = 0;
|
|
203
|
+
let index = 0;
|
|
204
|
+
const punctuationSet = new Set(["。", "!", "?", ".", "!", "?", "\n", ";", ";"]);
|
|
205
|
+
while (cursor < text.length) {
|
|
206
|
+
const rawEnd = Math.min(text.length, cursor + normalizedSize);
|
|
207
|
+
let end = rawEnd;
|
|
208
|
+
if (rawEnd < text.length) {
|
|
209
|
+
const backwardStart = Math.max(cursor + Math.floor(normalizedSize * 0.45), cursor + 1);
|
|
210
|
+
let found = -1;
|
|
211
|
+
for (let i = rawEnd - 1; i >= backwardStart; i -= 1) {
|
|
212
|
+
if (punctuationSet.has(text[i])) {
|
|
213
|
+
found = i + 1;
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (found < 0) {
|
|
218
|
+
const forwardEnd = Math.min(text.length, rawEnd + Math.floor(normalizedSize * 0.2));
|
|
219
|
+
for (let i = rawEnd; i < forwardEnd; i += 1) {
|
|
220
|
+
if (punctuationSet.has(text[i])) {
|
|
221
|
+
found = i + 1;
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (found > cursor) {
|
|
227
|
+
end = found;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (end <= cursor) {
|
|
231
|
+
end = Math.min(text.length, cursor + normalizedSize);
|
|
232
|
+
}
|
|
233
|
+
const chunkText = text.slice(cursor, end).trim();
|
|
234
|
+
if (chunkText) {
|
|
235
|
+
output.push({ index, start: cursor, end, text: chunkText });
|
|
236
|
+
index += 1;
|
|
237
|
+
}
|
|
238
|
+
if (end >= text.length) {
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
const nextCursor = Math.max(cursor + 1, end - overlap);
|
|
242
|
+
cursor = nextCursor <= cursor ? end : nextCursor;
|
|
243
|
+
}
|
|
244
|
+
return output;
|
|
245
|
+
}
|
|
246
|
+
function upsertJsonFile(filePath, patch) {
|
|
247
|
+
const current = parseJsonFile(filePath) || {};
|
|
248
|
+
const next = { ...current, ...patch };
|
|
249
|
+
const dir = path.dirname(filePath);
|
|
250
|
+
if (!fs.existsSync(dir)) {
|
|
251
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
252
|
+
}
|
|
253
|
+
fs.writeFileSync(filePath, JSON.stringify(next, null, 2), "utf-8");
|
|
254
|
+
}
|
|
255
|
+
async function probeModelConnection(args) {
|
|
256
|
+
const timeoutMs = typeof args.timeoutMs === "number" && Number.isFinite(args.timeoutMs) && args.timeoutMs >= 1000
|
|
257
|
+
? Math.floor(args.timeoutMs)
|
|
258
|
+
: 8000;
|
|
259
|
+
if (!args.model || !args.apiKey || !args.baseUrl) {
|
|
260
|
+
return {
|
|
261
|
+
configured: false,
|
|
262
|
+
connected: false,
|
|
263
|
+
model: args.model || "",
|
|
264
|
+
base_url: args.baseUrl || "",
|
|
265
|
+
error: "not_configured",
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
const controller = new AbortController();
|
|
269
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
270
|
+
try {
|
|
271
|
+
let endpoint = args.baseUrl;
|
|
272
|
+
let payload = {};
|
|
273
|
+
if (args.kind === "embedding") {
|
|
274
|
+
endpoint = args.baseUrl.endsWith("/embeddings") ? args.baseUrl : `${args.baseUrl}/embeddings`;
|
|
275
|
+
payload = {
|
|
276
|
+
model: args.model,
|
|
277
|
+
input: "diagnostics connectivity probe",
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
else if (args.kind === "llm") {
|
|
281
|
+
endpoint = args.baseUrl.endsWith("/chat/completions") ? args.baseUrl : `${args.baseUrl}/chat/completions`;
|
|
282
|
+
payload = {
|
|
283
|
+
model: args.model,
|
|
284
|
+
messages: [{ role: "user", content: "ping" }],
|
|
285
|
+
max_tokens: 1,
|
|
286
|
+
temperature: 0,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
endpoint = args.baseUrl.endsWith("/rerank") ? args.baseUrl : `${args.baseUrl}/rerank`;
|
|
291
|
+
payload = {
|
|
292
|
+
model: args.model,
|
|
293
|
+
query: "diagnostics",
|
|
294
|
+
documents: ["diagnostics connectivity probe"],
|
|
295
|
+
top_n: 1,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
const response = await fetch(endpoint, {
|
|
299
|
+
method: "POST",
|
|
300
|
+
headers: {
|
|
301
|
+
"content-type": "application/json",
|
|
302
|
+
authorization: `Bearer ${args.apiKey}`,
|
|
303
|
+
},
|
|
304
|
+
body: JSON.stringify(payload),
|
|
305
|
+
signal: controller.signal,
|
|
306
|
+
});
|
|
307
|
+
clearTimeout(timeoutId);
|
|
308
|
+
if (!response.ok) {
|
|
309
|
+
return {
|
|
310
|
+
configured: true,
|
|
311
|
+
connected: false,
|
|
312
|
+
model: args.model,
|
|
313
|
+
base_url: args.baseUrl,
|
|
314
|
+
error: `http_${response.status}`,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
configured: true,
|
|
319
|
+
connected: true,
|
|
320
|
+
model: args.model,
|
|
321
|
+
base_url: args.baseUrl,
|
|
322
|
+
error: "",
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
clearTimeout(timeoutId);
|
|
327
|
+
return {
|
|
328
|
+
configured: true,
|
|
329
|
+
connected: false,
|
|
330
|
+
model: args.model,
|
|
331
|
+
base_url: args.baseUrl,
|
|
332
|
+
error: error instanceof Error ? error.message : String(error),
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
async function requestEmbedding(args) {
|
|
337
|
+
const endpoint = args.baseUrl.endsWith("/embeddings") ? args.baseUrl : `${args.baseUrl}/embeddings`;
|
|
338
|
+
const body = {
|
|
339
|
+
input: args.text,
|
|
340
|
+
model: args.model,
|
|
341
|
+
};
|
|
342
|
+
if (typeof args.dimensions === "number" && Number.isFinite(args.dimensions) && args.dimensions > 0) {
|
|
343
|
+
body.dimensions = args.dimensions;
|
|
344
|
+
}
|
|
345
|
+
const timeoutMs = typeof args.timeoutMs === "number" && Number.isFinite(args.timeoutMs) && args.timeoutMs >= 1000
|
|
346
|
+
? Math.floor(args.timeoutMs)
|
|
347
|
+
: 20000;
|
|
348
|
+
const maxRetries = typeof args.maxRetries === "number" && Number.isFinite(args.maxRetries) && args.maxRetries >= 1
|
|
349
|
+
? Math.min(8, Math.floor(args.maxRetries))
|
|
350
|
+
: 4;
|
|
351
|
+
let lastError = null;
|
|
352
|
+
for (let attempt = 0; attempt < maxRetries; attempt += 1) {
|
|
353
|
+
const controller = new AbortController();
|
|
354
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
355
|
+
try {
|
|
356
|
+
const response = await fetch(endpoint, {
|
|
357
|
+
method: "POST",
|
|
358
|
+
headers: {
|
|
359
|
+
"content-type": "application/json",
|
|
360
|
+
authorization: `Bearer ${args.apiKey}`,
|
|
361
|
+
},
|
|
362
|
+
body: JSON.stringify(body),
|
|
363
|
+
signal: controller.signal,
|
|
364
|
+
});
|
|
365
|
+
clearTimeout(timeoutId);
|
|
366
|
+
if (!response.ok) {
|
|
367
|
+
lastError = new Error(`embedding_http_${response.status}`);
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
const json = await response.json();
|
|
371
|
+
const embedding = json?.data?.[0]?.embedding;
|
|
372
|
+
if (Array.isArray(embedding) && embedding.length > 0) {
|
|
373
|
+
return embedding.filter(item => Number.isFinite(item));
|
|
374
|
+
}
|
|
375
|
+
lastError = new Error("embedding_empty");
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
clearTimeout(timeoutId);
|
|
379
|
+
lastError = error;
|
|
380
|
+
}
|
|
381
|
+
if (attempt < maxRetries - 1) {
|
|
382
|
+
await new Promise(resolve => setTimeout(resolve, 300 * Math.pow(2, attempt)));
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError || "embedding_failed"));
|
|
386
|
+
}
|
|
101
387
|
async function storeEvent(args, _context) {
|
|
102
388
|
try {
|
|
103
389
|
const rawArgs = args;
|
|
@@ -435,19 +721,341 @@ function createTsEngine(deps) {
|
|
|
435
721
|
}
|
|
436
722
|
return { success: true, data: { deletedCount } };
|
|
437
723
|
}
|
|
724
|
+
async function backfillEmbeddings(args, _context) {
|
|
725
|
+
const layer = args.layer === "active" || args.layer === "archive" || args.layer === "all" ? args.layer : "all";
|
|
726
|
+
const rebuildMode = args.rebuild_mode === "vector_only" || args.rebuild_mode === "full"
|
|
727
|
+
? args.rebuild_mode
|
|
728
|
+
: "incremental";
|
|
729
|
+
const batchSize = typeof args.batch_size === "number" && Number.isFinite(args.batch_size) && args.batch_size > 0
|
|
730
|
+
? Math.min(500, Math.floor(args.batch_size))
|
|
731
|
+
: 100;
|
|
732
|
+
const maxRetries = typeof args.max_retries === "number" && Number.isFinite(args.max_retries) && args.max_retries >= 1
|
|
733
|
+
? Math.min(10, Math.floor(args.max_retries))
|
|
734
|
+
: 3;
|
|
735
|
+
const retryFailedOnly = args.retry_failed_only === true;
|
|
736
|
+
const forceRebuild = rebuildMode === "vector_only" || rebuildMode === "full";
|
|
737
|
+
const model = deps.embedding?.model || "";
|
|
738
|
+
const apiKey = deps.embedding?.apiKey || "";
|
|
739
|
+
const baseUrl = normalizeBaseUrl(deps.embedding?.baseURL || deps.embedding?.baseUrl);
|
|
740
|
+
if (!model || !apiKey || !baseUrl) {
|
|
741
|
+
return { success: false, error: "Embedding config missing for backfill tool." };
|
|
742
|
+
}
|
|
743
|
+
const statePath = path.join(deps.memoryRoot, ".vector_backfill_state.json");
|
|
744
|
+
const syncStatePath = path.join(deps.memoryRoot, ".sync_state.json");
|
|
745
|
+
const previousState = parseJsonFile(statePath) || {};
|
|
746
|
+
const failureCountState = (typeof previousState.failureCounts === "object" && previousState.failureCounts !== null)
|
|
747
|
+
? previousState.failureCounts
|
|
748
|
+
: {};
|
|
749
|
+
let fullSyncResult = null;
|
|
750
|
+
if (rebuildMode === "full") {
|
|
751
|
+
try {
|
|
752
|
+
fullSyncResult = await deps.sessionSync.syncMemory();
|
|
753
|
+
}
|
|
754
|
+
catch (error) {
|
|
755
|
+
deps.logger.warn(`backfill_full_rebuild_sync_failed error=${error}`);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
const { activePath, archivePath } = memoryFiles();
|
|
759
|
+
const targetFiles = [];
|
|
760
|
+
if (layer === "all" || layer === "active") {
|
|
761
|
+
targetFiles.push({ layer: "active", filePath: activePath });
|
|
762
|
+
}
|
|
763
|
+
if (layer === "all" || layer === "archive") {
|
|
764
|
+
targetFiles.push({ layer: "archive", filePath: archivePath });
|
|
765
|
+
}
|
|
766
|
+
const queue = [];
|
|
767
|
+
const recordsByFile = new Map();
|
|
768
|
+
for (const target of targetFiles) {
|
|
769
|
+
const records = readJsonl(target.filePath);
|
|
770
|
+
recordsByFile.set(target.filePath, records);
|
|
771
|
+
for (let i = 0; i < records.length; i += 1) {
|
|
772
|
+
const record = records[i];
|
|
773
|
+
const id = typeof record.id === "string" ? record.id : "";
|
|
774
|
+
if (!id) {
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
const status = typeof record.embedding_status === "string" ? record.embedding_status.trim() : "";
|
|
778
|
+
const hasEmbedding = Array.isArray(record.embedding) && record.embedding.length > 0;
|
|
779
|
+
if (forceRebuild) {
|
|
780
|
+
queue.push({ layer: target.layer, filePath: target.filePath, index: i });
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
if (retryFailedOnly) {
|
|
784
|
+
if (status !== "failed") {
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
else if (status === "ok" || hasEmbedding) {
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
const failCountRaw = failureCountState[id];
|
|
792
|
+
const failCount = typeof failCountRaw === "number" ? failCountRaw : 0;
|
|
793
|
+
if (failCount >= maxRetries && status === "failed") {
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
queue.push({ layer: target.layer, filePath: target.filePath, index: i });
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
const totalCandidates = queue.length;
|
|
800
|
+
let success = 0;
|
|
801
|
+
let failed = 0;
|
|
802
|
+
let skipped = 0;
|
|
803
|
+
let processed = 0;
|
|
804
|
+
const failureCounts = {};
|
|
805
|
+
for (const [key, value] of Object.entries(failureCountState)) {
|
|
806
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
807
|
+
failureCounts[key] = value;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
for (let start = 0; start < queue.length; start += batchSize) {
|
|
811
|
+
const batch = queue.slice(start, start + batchSize);
|
|
812
|
+
for (const item of batch) {
|
|
813
|
+
processed += 1;
|
|
814
|
+
const records = recordsByFile.get(item.filePath) || [];
|
|
815
|
+
const record = records[item.index];
|
|
816
|
+
if (!record) {
|
|
817
|
+
skipped += 1;
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
const id = typeof record.id === "string" ? record.id : "";
|
|
821
|
+
if (!id) {
|
|
822
|
+
skipped += 1;
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
const text = buildVectorSourceText(record, item.layer);
|
|
826
|
+
if (!text) {
|
|
827
|
+
record.embedding_status = "failed";
|
|
828
|
+
failed += 1;
|
|
829
|
+
failureCounts[id] = (failureCounts[id] || 0) + 1;
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
const chunkSize = deps.vectorChunking?.chunkSize ?? 600;
|
|
833
|
+
const chunkOverlap = deps.vectorChunking?.chunkOverlap ?? 100;
|
|
834
|
+
const chunks = splitTextChunks(text, chunkSize, chunkOverlap);
|
|
835
|
+
if (chunks.length === 0) {
|
|
836
|
+
record.embedding_status = "failed";
|
|
837
|
+
failed += 1;
|
|
838
|
+
failureCounts[id] = (failureCounts[id] || 0) + 1;
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
try {
|
|
842
|
+
if (forceRebuild) {
|
|
843
|
+
record.embedding_status = "pending";
|
|
844
|
+
}
|
|
845
|
+
await deps.vectorStore.deleteBySourceMemory({ layer: item.layer, sourceMemoryId: id });
|
|
846
|
+
let chunkOk = 0;
|
|
847
|
+
for (const chunk of chunks) {
|
|
848
|
+
const embedding = await requestEmbedding({
|
|
849
|
+
text: chunk.text,
|
|
850
|
+
model,
|
|
851
|
+
apiKey,
|
|
852
|
+
baseUrl,
|
|
853
|
+
dimensions: deps.embedding?.dimensions,
|
|
854
|
+
timeoutMs: deps.embedding?.timeoutMs,
|
|
855
|
+
maxRetries: deps.embedding?.maxRetries,
|
|
856
|
+
});
|
|
857
|
+
if (!embedding || embedding.length === 0) {
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
if (!record.embedding) {
|
|
861
|
+
record.embedding = embedding;
|
|
862
|
+
}
|
|
863
|
+
await deps.vectorStore.upsert({
|
|
864
|
+
id: `${id}_c${chunk.index}`,
|
|
865
|
+
session_id: typeof record.session_id === "string" ? record.session_id : "unknown",
|
|
866
|
+
event_type: typeof record.event_type === "string" ? record.event_type : (item.layer === "active" ? "message" : "insight"),
|
|
867
|
+
summary: chunk.text,
|
|
868
|
+
timestamp: typeof record.timestamp === "string" ? record.timestamp : new Date().toISOString(),
|
|
869
|
+
layer: item.layer,
|
|
870
|
+
source_memory_id: id,
|
|
871
|
+
source_memory_canonical_id: typeof record.canonical_id === "string" ? record.canonical_id : id,
|
|
872
|
+
outcome: typeof record.outcome === "string" ? record.outcome : "",
|
|
873
|
+
entities: Array.isArray(record.entities) ? record.entities.filter(v => typeof v === "string") : [],
|
|
874
|
+
relations: Array.isArray(record.relations)
|
|
875
|
+
? record.relations
|
|
876
|
+
.map(v => {
|
|
877
|
+
if (!v || typeof v !== "object")
|
|
878
|
+
return null;
|
|
879
|
+
const relation = v;
|
|
880
|
+
const source = typeof relation.source === "string" ? relation.source : "";
|
|
881
|
+
const target = typeof relation.target === "string" ? relation.target : "";
|
|
882
|
+
const type = typeof relation.type === "string" ? relation.type : "related_to";
|
|
883
|
+
if (!source || !target)
|
|
884
|
+
return null;
|
|
885
|
+
return { source, target, type };
|
|
886
|
+
})
|
|
887
|
+
.filter((v) => Boolean(v))
|
|
888
|
+
: [],
|
|
889
|
+
embedding,
|
|
890
|
+
quality_score: typeof record.quality_score === "number" ? record.quality_score : 0.5,
|
|
891
|
+
char_count: chunk.text.length,
|
|
892
|
+
token_count: estimateTokenCount(chunk.text),
|
|
893
|
+
chunk_index: chunk.index,
|
|
894
|
+
chunk_total: chunks.length,
|
|
895
|
+
chunk_start: chunk.start,
|
|
896
|
+
chunk_end: chunk.end,
|
|
897
|
+
});
|
|
898
|
+
chunkOk += 1;
|
|
899
|
+
}
|
|
900
|
+
record.vector_chunks_total = chunks.length;
|
|
901
|
+
record.vector_chunks_ok = chunkOk;
|
|
902
|
+
record.embedding_status = chunkOk === chunks.length ? "ok" : "failed";
|
|
903
|
+
if (!record.layer) {
|
|
904
|
+
record.layer = item.layer;
|
|
905
|
+
}
|
|
906
|
+
if (typeof record.char_count !== "number") {
|
|
907
|
+
record.char_count = text.length;
|
|
908
|
+
}
|
|
909
|
+
if (typeof record.token_count !== "number") {
|
|
910
|
+
record.token_count = estimateTokenCount(text);
|
|
911
|
+
}
|
|
912
|
+
if (chunkOk === chunks.length) {
|
|
913
|
+
success += 1;
|
|
914
|
+
failureCounts[id] = 0;
|
|
915
|
+
}
|
|
916
|
+
else {
|
|
917
|
+
failed += 1;
|
|
918
|
+
failureCounts[id] = (failureCounts[id] || 0) + 1;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
catch (error) {
|
|
922
|
+
record.embedding_status = "failed";
|
|
923
|
+
failed += 1;
|
|
924
|
+
failureCounts[id] = (failureCounts[id] || 0) + 1;
|
|
925
|
+
deps.logger.warn(`backfill_embedding_failed id=${id} layer=${item.layer} error=${error}`);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
deps.logger.info(`backfill_progress processed=${processed}/${totalCandidates} success=${success} failed=${failed} skipped=${skipped}`);
|
|
929
|
+
}
|
|
930
|
+
for (const target of targetFiles) {
|
|
931
|
+
const records = recordsByFile.get(target.filePath);
|
|
932
|
+
if (records) {
|
|
933
|
+
writeJsonl(target.filePath, records);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
const summary = {
|
|
937
|
+
runAt: new Date().toISOString(),
|
|
938
|
+
layer,
|
|
939
|
+
rebuild_mode: rebuildMode,
|
|
940
|
+
candidates: totalCandidates,
|
|
941
|
+
success,
|
|
942
|
+
failed,
|
|
943
|
+
skipped,
|
|
944
|
+
batch_size: batchSize,
|
|
945
|
+
max_retries: maxRetries,
|
|
946
|
+
retry_failed_only: retryFailedOnly,
|
|
947
|
+
full_sync_result: fullSyncResult,
|
|
948
|
+
};
|
|
949
|
+
upsertJsonFile(statePath, {
|
|
950
|
+
version: "1",
|
|
951
|
+
lastRun: summary,
|
|
952
|
+
failureCounts,
|
|
953
|
+
});
|
|
954
|
+
upsertJsonFile(syncStatePath, {
|
|
955
|
+
version: "2",
|
|
956
|
+
lastVectorBackfill: {
|
|
957
|
+
runAt: summary.runAt,
|
|
958
|
+
success,
|
|
959
|
+
failed,
|
|
960
|
+
skipped,
|
|
961
|
+
},
|
|
962
|
+
});
|
|
963
|
+
return { success: true, data: summary };
|
|
964
|
+
}
|
|
438
965
|
async function runDiagnostics(_args, _context) {
|
|
439
966
|
const { activePath, archivePath } = memoryFiles();
|
|
967
|
+
const activeRecords = readJsonl(activePath);
|
|
968
|
+
const archiveRecords = readJsonl(archivePath);
|
|
969
|
+
const activeVector = embeddingStats(activeRecords);
|
|
970
|
+
const archiveVector = embeddingStats(archiveRecords);
|
|
971
|
+
const vectorJsonlPath = path.join(deps.memoryRoot, "vector", "lancedb_events.jsonl");
|
|
972
|
+
const vectorJsonlRecords = readJsonl(vectorJsonlPath);
|
|
973
|
+
const activeVectorRecords = vectorJsonlRecords.filter(record => (record.layer === "active"));
|
|
974
|
+
const archiveVectorRecords = vectorJsonlRecords.filter(record => (record.layer === "archive"));
|
|
975
|
+
const syncState = parseJsonFile(path.join(deps.memoryRoot, ".sync_state.json"));
|
|
976
|
+
const backfillState = parseJsonFile(path.join(deps.memoryRoot, ".vector_backfill_state.json"));
|
|
977
|
+
const failureCounts = backfillState && typeof backfillState.failureCounts === "object" && backfillState.failureCounts !== null
|
|
978
|
+
? backfillState.failureCounts
|
|
979
|
+
: {};
|
|
980
|
+
const pendingRetry = Object.values(failureCounts).filter(value => typeof value === "number" && Number.isFinite(value) && value > 0).length;
|
|
981
|
+
const lastVectorBackfill = syncState && typeof syncState.lastVectorBackfill === "object" && syncState.lastVectorBackfill !== null
|
|
982
|
+
? syncState.lastVectorBackfill
|
|
983
|
+
: null;
|
|
984
|
+
const embeddingConnectivity = await probeModelConnection({
|
|
985
|
+
kind: "embedding",
|
|
986
|
+
model: deps.embedding?.model || "",
|
|
987
|
+
apiKey: deps.embedding?.apiKey || "",
|
|
988
|
+
baseUrl: normalizeBaseUrl(deps.embedding?.baseURL || deps.embedding?.baseUrl),
|
|
989
|
+
timeoutMs: deps.embedding?.timeoutMs,
|
|
990
|
+
});
|
|
991
|
+
const llmConnectivity = await probeModelConnection({
|
|
992
|
+
kind: "llm",
|
|
993
|
+
model: deps.llm?.model || "",
|
|
994
|
+
apiKey: deps.llm?.apiKey || "",
|
|
995
|
+
baseUrl: normalizeBaseUrl(deps.llm?.baseURL || deps.llm?.baseUrl),
|
|
996
|
+
timeoutMs: 8000,
|
|
997
|
+
});
|
|
998
|
+
const rerankerConnectivity = await probeModelConnection({
|
|
999
|
+
kind: "reranker",
|
|
1000
|
+
model: deps.reranker?.model || "",
|
|
1001
|
+
apiKey: deps.reranker?.apiKey || "",
|
|
1002
|
+
baseUrl: normalizeBaseUrl(deps.reranker?.baseURL || deps.reranker?.baseUrl),
|
|
1003
|
+
timeoutMs: 8000,
|
|
1004
|
+
});
|
|
440
1005
|
const checks = [
|
|
441
1006
|
{ name: "Engine mode", passed: true, message: "TS engine active" },
|
|
442
1007
|
{ name: "Active sessions store", passed: fs.existsSync(activePath), message: activePath },
|
|
443
1008
|
{ name: "Archive sessions store", passed: fs.existsSync(archivePath), message: archivePath },
|
|
444
1009
|
{ name: "Core rules store", passed: fs.existsSync(path.join(deps.memoryRoot, "CORTEX_RULES.md")), message: "CORTEX_RULES.md checked" },
|
|
1010
|
+
{ name: "Embedding model connectivity", passed: embeddingConnectivity.connected, message: embeddingConnectivity.error || "ok" },
|
|
1011
|
+
{ name: "LLM model connectivity", passed: llmConnectivity.connected, message: llmConnectivity.error || "ok" },
|
|
1012
|
+
{ name: "Reranker model connectivity", passed: rerankerConnectivity.connected, message: rerankerConnectivity.error || "ok" },
|
|
445
1013
|
];
|
|
446
1014
|
return {
|
|
447
1015
|
success: true,
|
|
448
1016
|
data: {
|
|
449
1017
|
status: "ok",
|
|
1018
|
+
prompt_versions: PROMPT_VERSIONS,
|
|
450
1019
|
checks,
|
|
1020
|
+
layers: {
|
|
1021
|
+
active: {
|
|
1022
|
+
records: activeRecords.length,
|
|
1023
|
+
path: activePath,
|
|
1024
|
+
},
|
|
1025
|
+
archive: {
|
|
1026
|
+
records: archiveRecords.length,
|
|
1027
|
+
path: archivePath,
|
|
1028
|
+
},
|
|
1029
|
+
vector: {
|
|
1030
|
+
active_coverage: activeVector.coverage,
|
|
1031
|
+
archive_coverage: archiveVector.coverage,
|
|
1032
|
+
active_unembedded: activeVector.pending + activeVector.failed,
|
|
1033
|
+
archive_unembedded: archiveVector.pending + archiveVector.failed,
|
|
1034
|
+
chunking: {
|
|
1035
|
+
chunk_size: deps.vectorChunking?.chunkSize ?? 600,
|
|
1036
|
+
chunk_overlap: deps.vectorChunking?.chunkOverlap ?? 100,
|
|
1037
|
+
},
|
|
1038
|
+
vector_jsonl_records: vectorJsonlRecords.length,
|
|
1039
|
+
vector_jsonl_by_layer: {
|
|
1040
|
+
active: activeVectorRecords.length,
|
|
1041
|
+
archive: archiveVectorRecords.length,
|
|
1042
|
+
},
|
|
1043
|
+
last_backfill_summary: lastVectorBackfill,
|
|
1044
|
+
backfill_state: {
|
|
1045
|
+
pending_retry_records: pendingRetry,
|
|
1046
|
+
has_state_file: fs.existsSync(path.join(deps.memoryRoot, ".vector_backfill_state.json")),
|
|
1047
|
+
},
|
|
1048
|
+
},
|
|
1049
|
+
graph_rules: {
|
|
1050
|
+
graph_mutation_log_exists: fs.existsSync(path.join(deps.memoryRoot, "graph", "mutation_log.jsonl")),
|
|
1051
|
+
rules_exists: fs.existsSync(path.join(deps.memoryRoot, "CORTEX_RULES.md")),
|
|
1052
|
+
},
|
|
1053
|
+
},
|
|
1054
|
+
model_connectivity: {
|
|
1055
|
+
embedding: embeddingConnectivity,
|
|
1056
|
+
llm: llmConnectivity,
|
|
1057
|
+
reranker: rerankerConnectivity,
|
|
1058
|
+
},
|
|
451
1059
|
recommendations: [],
|
|
452
1060
|
},
|
|
453
1061
|
};
|
|
@@ -588,6 +1196,7 @@ function createTsEngine(deps) {
|
|
|
588
1196
|
deleteMemory,
|
|
589
1197
|
updateMemory,
|
|
590
1198
|
cleanupMemories,
|
|
1199
|
+
backfillEmbeddings,
|
|
591
1200
|
runDiagnostics,
|
|
592
1201
|
onMessage,
|
|
593
1202
|
onSessionEnd,
|