@memtensor/memos-local-openclaw-plugin 1.0.2-beta.3 → 1.0.2-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/capture/index.d.ts.map +1 -1
  2. package/dist/capture/index.js +41 -1
  3. package/dist/capture/index.js.map +1 -1
  4. package/dist/embedding/index.d.ts.map +1 -1
  5. package/dist/embedding/index.js +20 -7
  6. package/dist/embedding/index.js.map +1 -1
  7. package/dist/ingest/providers/anthropic.d.ts.map +1 -1
  8. package/dist/ingest/providers/anthropic.js +39 -25
  9. package/dist/ingest/providers/anthropic.js.map +1 -1
  10. package/dist/ingest/providers/bedrock.d.ts.map +1 -1
  11. package/dist/ingest/providers/bedrock.js +39 -25
  12. package/dist/ingest/providers/bedrock.js.map +1 -1
  13. package/dist/ingest/providers/gemini.d.ts.map +1 -1
  14. package/dist/ingest/providers/gemini.js +39 -25
  15. package/dist/ingest/providers/gemini.js.map +1 -1
  16. package/dist/ingest/providers/index.d.ts +19 -0
  17. package/dist/ingest/providers/index.d.ts.map +1 -1
  18. package/dist/ingest/providers/index.js +98 -10
  19. package/dist/ingest/providers/index.js.map +1 -1
  20. package/dist/ingest/providers/openai.d.ts.map +1 -1
  21. package/dist/ingest/providers/openai.js +39 -25
  22. package/dist/ingest/providers/openai.js.map +1 -1
  23. package/dist/ingest/worker.d.ts.map +1 -1
  24. package/dist/ingest/worker.js +8 -14
  25. package/dist/ingest/worker.js.map +1 -1
  26. package/dist/skill/bundled-memory-guide.d.ts +1 -1
  27. package/dist/skill/bundled-memory-guide.d.ts.map +1 -1
  28. package/dist/skill/bundled-memory-guide.js +9 -0
  29. package/dist/skill/bundled-memory-guide.js.map +1 -1
  30. package/dist/storage/sqlite.d.ts +14 -0
  31. package/dist/storage/sqlite.d.ts.map +1 -1
  32. package/dist/storage/sqlite.js +42 -0
  33. package/dist/storage/sqlite.js.map +1 -1
  34. package/dist/viewer/html.d.ts +1 -1
  35. package/dist/viewer/html.d.ts.map +1 -1
  36. package/dist/viewer/html.js +276 -51
  37. package/dist/viewer/html.js.map +1 -1
  38. package/dist/viewer/server.d.ts +4 -0
  39. package/dist/viewer/server.d.ts.map +1 -1
  40. package/dist/viewer/server.js +152 -27
  41. package/dist/viewer/server.js.map +1 -1
  42. package/index.ts +38 -85
  43. package/package.json +2 -1
  44. package/src/capture/index.ts +56 -1
  45. package/src/embedding/index.ts +13 -7
  46. package/src/ingest/providers/anthropic.ts +39 -25
  47. package/src/ingest/providers/bedrock.ts +39 -25
  48. package/src/ingest/providers/gemini.ts +39 -25
  49. package/src/ingest/providers/index.ts +112 -9
  50. package/src/ingest/providers/openai.ts +39 -25
  51. package/src/ingest/worker.ts +8 -15
  52. package/src/skill/bundled-memory-guide.ts +9 -0
  53. package/src/storage/sqlite.ts +49 -0
  54. package/src/viewer/html.ts +275 -50
  55. package/src/viewer/server.ts +143 -32
@@ -19,6 +19,11 @@ const engine_1 = require("../recall/engine");
19
19
  const evolver_1 = require("../skill/evolver");
20
20
  const html_1 = require("./html");
21
21
  const uuid_1 = require("uuid");
22
+ function normalizeTimestamp(ts) {
23
+ if (ts < 1e12)
24
+ return ts * 1000;
25
+ return ts;
26
+ }
22
27
  class ViewerServer {
23
28
  server = null;
24
29
  store;
@@ -30,6 +35,15 @@ class ViewerServer {
30
35
  auth;
31
36
  ctx;
32
37
  static SESSION_TTL = 24 * 60 * 60 * 1000;
38
+ static PLUGIN_VERSION = (() => {
39
+ try {
40
+ const pkgPath = node_path_1.default.resolve(__dirname, "../../package.json");
41
+ return JSON.parse(node_fs_1.default.readFileSync(pkgPath, "utf-8")).version ?? "unknown";
42
+ }
43
+ catch {
44
+ return "unknown";
45
+ }
46
+ })();
33
47
  resetToken;
34
48
  migrationRunning = false;
35
49
  migrationAbort = false;
@@ -66,10 +80,28 @@ class ViewerServer {
66
80
  this.server.listen(this.port, "127.0.0.1", () => {
67
81
  const addr = this.server.address();
68
82
  const actualPort = typeof addr === "object" && addr ? addr.port : this.port;
83
+ this.autoCleanupPolluted();
69
84
  resolve(`http://127.0.0.1:${actualPort}`);
70
85
  });
71
86
  });
72
87
  }
88
+ autoCleanupPolluted() {
89
+ try {
90
+ const polluted = this.store.findPollutedUserChunks();
91
+ let deleted = 0;
92
+ for (const { id } of polluted) {
93
+ if (this.store.deleteChunk(id))
94
+ deleted++;
95
+ }
96
+ const fixed = this.store.fixMixedUserChunks();
97
+ if (deleted > 0 || fixed > 0) {
98
+ this.log.info(`Auto-cleanup: removed ${deleted} polluted chunks, fixed ${fixed} mixed user+assistant chunks`);
99
+ }
100
+ }
101
+ catch (err) {
102
+ this.log.warn(`Auto-cleanup failed: ${err}`);
103
+ }
104
+ }
73
105
  stop() {
74
106
  this.server?.close();
75
107
  this.server = null;
@@ -212,12 +244,16 @@ class ViewerServer {
212
244
  this.handleSaveConfig(req, res);
213
245
  else if (p === "/api/test-model" && req.method === "POST")
214
246
  this.handleTestModel(req, res);
247
+ else if (p === "/api/model-health" && req.method === "GET")
248
+ this.serveModelHealth(res);
215
249
  else if (p === "/api/fallback-model" && req.method === "GET")
216
250
  this.serveFallbackModel(res);
217
251
  else if (p === "/api/update-check" && req.method === "GET")
218
252
  this.handleUpdateCheck(res);
219
253
  else if (p === "/api/auth/logout" && req.method === "POST")
220
254
  this.handleLogout(req, res);
255
+ else if (p === "/api/cleanup-polluted" && req.method === "POST")
256
+ this.handleCleanupPolluted(res);
221
257
  else if (p === "/api/migrate/scan" && req.method === "GET")
222
258
  this.handleMigrateScan(res);
223
259
  else if (p === "/api/migrate/start" && req.method === "POST")
@@ -345,7 +381,7 @@ class ViewerServer {
345
381
  // ─── Pages ───
346
382
  serveViewer(res) {
347
383
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0", "Pragma": "no-cache", "Expires": "0" });
348
- res.end(html_1.viewerHTML);
384
+ res.end((0, html_1.viewerHTML)(ViewerServer.PLUGIN_VERSION));
349
385
  }
350
386
  // ─── Data APIs ───
351
387
  serveMemories(res, url) {
@@ -491,7 +527,15 @@ class ViewerServer {
491
527
  const total = db.prepare("SELECT COUNT(*) as count FROM chunks").get();
492
528
  const sessions = db.prepare("SELECT COUNT(DISTINCT session_key) as count FROM chunks").get();
493
529
  const roles = db.prepare("SELECT role, COUNT(*) as count FROM chunks GROUP BY role").all();
494
- const timeRange = db.prepare("SELECT MIN(created_at) as earliest, MAX(created_at) as latest FROM chunks").get();
530
+ const timeRange = db.prepare("SELECT MIN(created_at) as earliest, MAX(created_at) as latest FROM chunks WHERE dedup_status = 'active'").get();
531
+ const MIN_VALID_TS = 1704067200000; // 2024-01-01
532
+ if (timeRange.earliest != null && timeRange.earliest < MIN_VALID_TS) {
533
+ timeRange.earliest = db.prepare("SELECT MIN(created_at) as v FROM chunks WHERE dedup_status = 'active' AND created_at >= ?").get(MIN_VALID_TS);
534
+ timeRange.earliest = timeRange.earliest?.v ?? null;
535
+ }
536
+ if (timeRange.latest != null && timeRange.latest < MIN_VALID_TS) {
537
+ timeRange.latest = null;
538
+ }
495
539
  let embCount = 0;
496
540
  try {
497
541
  embCount = db.prepare("SELECT COUNT(*) as count FROM embeddings").get().count;
@@ -541,6 +585,8 @@ class ViewerServer {
541
585
  }
542
586
  const role = url.searchParams.get("role") ?? undefined;
543
587
  const kind = url.searchParams.get("kind") ?? undefined;
588
+ const session = url.searchParams.get("session") ?? undefined;
589
+ const owner = url.searchParams.get("owner") ?? undefined;
544
590
  const dateFrom = url.searchParams.get("dateFrom") ?? undefined;
545
591
  const dateTo = url.searchParams.get("dateTo") ?? undefined;
546
592
  const passesFilter = (r) => {
@@ -548,35 +594,71 @@ class ViewerServer {
548
594
  return false;
549
595
  if (kind && r.kind !== kind)
550
596
  return false;
597
+ if (session && r.session_key !== session)
598
+ return false;
599
+ if (owner && r.owner !== owner)
600
+ return false;
551
601
  if (dateFrom && r.created_at < new Date(dateFrom).getTime())
552
602
  return false;
553
603
  if (dateTo && r.created_at > new Date(dateTo).getTime())
554
604
  return false;
555
605
  return true;
556
606
  };
607
+ const ftsFilters = [];
608
+ const likeFilters = [];
609
+ const sqlParams = [];
610
+ if (session) {
611
+ ftsFilters.push("c.session_key = ?");
612
+ likeFilters.push("session_key = ?");
613
+ sqlParams.push(session);
614
+ }
615
+ if (owner) {
616
+ ftsFilters.push("c.owner = ?");
617
+ likeFilters.push("owner = ?");
618
+ sqlParams.push(owner);
619
+ }
620
+ const ftsWhere = ftsFilters.length > 0 ? " AND " + ftsFilters.join(" AND ") : "";
621
+ const likeWhere = likeFilters.length > 0 ? " AND " + likeFilters.join(" AND ") : "";
557
622
  const db = this.store.db;
558
623
  let ftsResults = [];
559
624
  try {
560
- ftsResults = db.prepare("SELECT c.* FROM chunks_fts f JOIN chunks c ON f.rowid = c.rowid WHERE chunks_fts MATCH ? ORDER BY rank LIMIT 100").all(q).filter(passesFilter);
625
+ ftsResults = db.prepare(`SELECT c.* FROM chunks_fts f JOIN chunks c ON f.rowid = c.rowid WHERE chunks_fts MATCH ?${ftsWhere} ORDER BY rank LIMIT 100`).all(q, ...sqlParams).filter(passesFilter);
561
626
  }
562
627
  catch { /* FTS syntax error, fall through */ }
563
628
  if (ftsResults.length === 0) {
564
- ftsResults = db.prepare("SELECT * FROM chunks WHERE content LIKE ? OR summary LIKE ? ORDER BY created_at DESC LIMIT 100").all(`%${q}%`, `%${q}%`).filter(passesFilter);
629
+ try {
630
+ ftsResults = db.prepare(`SELECT * FROM chunks WHERE (content LIKE ? OR summary LIKE ?)${likeWhere} ORDER BY created_at DESC LIMIT 100`).all(`%${q}%`, `%${q}%`, ...sqlParams).filter(passesFilter);
631
+ }
632
+ catch (err) {
633
+ this.log.warn(`LIKE search failed: ${err}`);
634
+ }
565
635
  }
566
636
  const SEMANTIC_THRESHOLD = 0.64;
637
+ const VECTOR_TIMEOUT_MS = 8000;
567
638
  let vectorResults = [];
568
639
  let scoreMap = new Map();
569
640
  try {
570
- const queryVec = await this.embedder.embedQuery(q);
571
- const hits = (0, vector_1.vectorSearch)(this.store, queryVec, 40);
572
- scoreMap = new Map(hits.map(h => [h.chunkId, h.score]));
573
- const hitIds = new Set(hits.filter(h => h.score >= SEMANTIC_THRESHOLD).map(h => h.chunkId));
574
- if (hitIds.size > 0) {
575
- const placeholders = [...hitIds].map(() => "?").join(",");
576
- const rows = db.prepare(`SELECT * FROM chunks WHERE id IN (${placeholders})`).all(...hitIds).filter(passesFilter);
577
- rows.forEach((r) => { r._vscore = scoreMap.get(r.id) ?? 0; });
578
- rows.sort((a, b) => (b._vscore ?? 0) - (a._vscore ?? 0));
579
- vectorResults = rows;
641
+ const vecPromise = (async () => {
642
+ const queryVec = await this.embedder.embedQuery(q);
643
+ return (0, vector_1.vectorSearch)(this.store, queryVec, 40);
644
+ })();
645
+ const hits = await Promise.race([
646
+ vecPromise,
647
+ new Promise((resolve) => setTimeout(() => resolve(null), VECTOR_TIMEOUT_MS)),
648
+ ]);
649
+ if (hits) {
650
+ scoreMap = new Map(hits.map(h => [h.chunkId, h.score]));
651
+ const hitIds = new Set(hits.filter(h => h.score >= SEMANTIC_THRESHOLD).map(h => h.chunkId));
652
+ if (hitIds.size > 0) {
653
+ const placeholders = [...hitIds].map(() => "?").join(",");
654
+ const rows = db.prepare(`SELECT * FROM chunks WHERE id IN (${placeholders})${likeWhere}`).all(...hitIds, ...sqlParams).filter(passesFilter);
655
+ rows.forEach((r) => { r._vscore = scoreMap.get(r.id) ?? 0; });
656
+ rows.sort((a, b) => (b._vscore ?? 0) - (a._vscore ?? 0));
657
+ vectorResults = rows;
658
+ }
659
+ }
660
+ else {
661
+ this.log.warn("Vector search timed out, returning FTS results only");
580
662
  }
581
663
  }
582
664
  catch (err) {
@@ -1080,8 +1162,8 @@ class ViewerServer {
1080
1162
  return;
1081
1163
  }
1082
1164
  if (type === "embedding") {
1083
- await this.testEmbeddingModel(provider, model, endpoint, apiKey);
1084
- this.jsonResponse(res, { ok: true, detail: `${provider}/${model}` });
1165
+ const dims = await this.testEmbeddingModel(provider, model, endpoint, apiKey);
1166
+ this.jsonResponse(res, { ok: true, detail: `${provider}/${model}`, dimensions: dims });
1085
1167
  }
1086
1168
  else {
1087
1169
  await this.testChatModel(provider, model, endpoint, apiKey);
@@ -1095,6 +1177,9 @@ class ViewerServer {
1095
1177
  }
1096
1178
  });
1097
1179
  }
1180
+ serveModelHealth(res) {
1181
+ this.jsonResponse(res, { models: providers_1.modelHealth.getAll() });
1182
+ }
1098
1183
  serveFallbackModel(res) {
1099
1184
  try {
1100
1185
  const cfgPath = this.getOpenClawConfigPath();
@@ -1177,7 +1262,7 @@ class ViewerServer {
1177
1262
  }
1178
1263
  async testEmbeddingModel(provider, model, endpoint, apiKey) {
1179
1264
  if (provider === "local") {
1180
- return;
1265
+ return 384;
1181
1266
  }
1182
1267
  const baseUrl = (endpoint || "https://api.openai.com/v1").replace(/\/+$/, "");
1183
1268
  const embUrl = baseUrl.endsWith("/embeddings") ? baseUrl : `${baseUrl}/embeddings`;
@@ -1190,39 +1275,59 @@ class ViewerServer {
1190
1275
  const resp = await fetch(baseUrl.replace(/\/v\d+.*/, "/v2/embed"), {
1191
1276
  method: "POST",
1192
1277
  headers,
1193
- body: JSON.stringify({ texts: ["test"], model: model || "embed-english-v3.0", input_type: "search_query", embedding_types: ["float"] }),
1278
+ body: JSON.stringify({ texts: ["test embedding vector"], model: model || "embed-english-v3.0", input_type: "search_query", embedding_types: ["float"] }),
1194
1279
  signal: AbortSignal.timeout(15_000),
1195
1280
  });
1196
1281
  if (!resp.ok) {
1197
1282
  const txt = await resp.text();
1198
1283
  throw new Error(`Cohere embed ${resp.status}: ${txt}`);
1199
1284
  }
1200
- return;
1285
+ const json = await resp.json();
1286
+ const vecs = json?.embeddings?.float;
1287
+ if (!Array.isArray(vecs) || vecs.length === 0 || !Array.isArray(vecs[0]) || vecs[0].length === 0) {
1288
+ throw new Error("Cohere returned empty embedding vector");
1289
+ }
1290
+ return vecs[0].length;
1201
1291
  }
1202
1292
  if (provider === "gemini") {
1203
1293
  const url = `https://generativelanguage.googleapis.com/v1/models/${model || "text-embedding-004"}:embedContent?key=${apiKey}`;
1204
1294
  const resp = await fetch(url, {
1205
1295
  method: "POST",
1206
1296
  headers: { "Content-Type": "application/json" },
1207
- body: JSON.stringify({ content: { parts: [{ text: "test" }] } }),
1297
+ body: JSON.stringify({ content: { parts: [{ text: "test embedding vector" }] } }),
1208
1298
  signal: AbortSignal.timeout(15_000),
1209
1299
  });
1210
1300
  if (!resp.ok) {
1211
1301
  const txt = await resp.text();
1212
1302
  throw new Error(`Gemini embed ${resp.status}: ${txt}`);
1213
1303
  }
1214
- return;
1304
+ const json = await resp.json();
1305
+ const vec = json?.embedding?.values;
1306
+ if (!Array.isArray(vec) || vec.length === 0) {
1307
+ throw new Error("Gemini returned empty embedding vector");
1308
+ }
1309
+ return vec.length;
1215
1310
  }
1216
1311
  const resp = await fetch(embUrl, {
1217
1312
  method: "POST",
1218
1313
  headers,
1219
- body: JSON.stringify({ input: ["test"], model: model || "text-embedding-3-small" }),
1314
+ body: JSON.stringify({ input: ["test embedding vector"], model: model || "text-embedding-3-small" }),
1220
1315
  signal: AbortSignal.timeout(15_000),
1221
1316
  });
1222
1317
  if (!resp.ok) {
1223
1318
  const txt = await resp.text();
1224
1319
  throw new Error(`${resp.status}: ${txt}`);
1225
1320
  }
1321
+ const json = await resp.json();
1322
+ const data = json?.data;
1323
+ if (!Array.isArray(data) || data.length === 0) {
1324
+ throw new Error("API returned no embedding data");
1325
+ }
1326
+ const vec = data[0]?.embedding;
1327
+ if (!Array.isArray(vec) || vec.length === 0) {
1328
+ throw new Error(`API returned empty embedding vector (got ${JSON.stringify(vec)?.slice(0, 100)})`);
1329
+ }
1330
+ return vec.length;
1226
1331
  }
1227
1332
  async testChatModel(provider, model, endpoint, apiKey) {
1228
1333
  const baseUrl = (endpoint || "https://api.openai.com/v1").replace(/\/+$/, "");
@@ -1291,6 +1396,28 @@ class ViewerServer {
1291
1396
  const home = process.env.HOME || process.env.USERPROFILE || "";
1292
1397
  return node_path_1.default.join(home, ".openclaw");
1293
1398
  }
1399
+ handleCleanupPolluted(res) {
1400
+ try {
1401
+ const polluted = this.store.findPollutedUserChunks();
1402
+ let deleted = 0;
1403
+ for (const { id, reason } of polluted) {
1404
+ if (this.store.deleteChunk(id)) {
1405
+ deleted++;
1406
+ this.log.info(`Cleaned polluted chunk ${id}: ${reason}`);
1407
+ }
1408
+ }
1409
+ const fixed = this.store.fixMixedUserChunks();
1410
+ this.log.info(`Cleanup: removed ${deleted} polluted, fixed ${fixed} mixed chunks`);
1411
+ res.writeHead(200, { "Content-Type": "application/json" });
1412
+ res.end(JSON.stringify({ deleted, fixed, total: polluted.length }));
1413
+ }
1414
+ catch (err) {
1415
+ const msg = err instanceof Error ? err.message : String(err);
1416
+ this.log.error(`handleCleanupPolluted error: ${msg}`);
1417
+ res.writeHead(500, { "Content-Type": "application/json" });
1418
+ res.end(JSON.stringify({ error: msg }));
1419
+ }
1420
+ }
1294
1421
  handleMigrateScan(res) {
1295
1422
  try {
1296
1423
  const ocHome = this.getOpenClawHome();
@@ -1540,7 +1667,6 @@ class ViewerServer {
1540
1667
  let totalErrors = 0;
1541
1668
  const cfgPath = this.getOpenClawConfigPath();
1542
1669
  let summarizerCfg;
1543
- let strongCfg;
1544
1670
  try {
1545
1671
  const raw = JSON.parse(node_fs_1.default.readFileSync(cfgPath, "utf-8"));
1546
1672
  const pluginCfg = raw?.plugins?.entries?.["memos-local-openclaw-plugin"]?.config ??
@@ -1548,10 +1674,9 @@ class ViewerServer {
1548
1674
  raw?.plugins?.entries?.["memos-lite-openclaw-plugin"]?.config ??
1549
1675
  raw?.plugins?.entries?.["memos-lite"]?.config ?? {};
1550
1676
  summarizerCfg = pluginCfg.summarizer;
1551
- strongCfg = pluginCfg.skillEvolution?.summarizer;
1552
1677
  }
1553
1678
  catch { /* no config */ }
1554
- const summarizer = new providers_1.Summarizer(summarizerCfg, this.log, strongCfg);
1679
+ const summarizer = new providers_1.Summarizer(summarizerCfg, this.log);
1555
1680
  // Phase 1: Import SQLite memory chunks
1556
1681
  if (importSqlite) {
1557
1682
  const memoryDir = node_path_1.default.join(ocHome, "memory");
@@ -1670,8 +1795,8 @@ class ViewerServer {
1670
1795
  mergeCount: 0,
1671
1796
  lastHitAt: null,
1672
1797
  mergeHistory: "[]",
1673
- createdAt: row.updated_at * 1000,
1674
- updatedAt: row.updated_at * 1000,
1798
+ createdAt: normalizeTimestamp(row.updated_at),
1799
+ updatedAt: normalizeTimestamp(row.updated_at),
1675
1800
  };
1676
1801
  this.store.insertChunk(chunk);
1677
1802
  if (embedding && dedupStatus === "active") {