@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
@@ -6,7 +6,7 @@ import path from "node:path";
6
6
  import readline from "node:readline";
7
7
  import type { SqliteStore } from "../storage/sqlite";
8
8
  import type { Embedder } from "../embedding";
9
- import { Summarizer } from "../ingest/providers";
9
+ import { Summarizer, modelHealth } from "../ingest/providers";
10
10
  import { findTopSimilar } from "../ingest/dedup";
11
11
  import { stripInboundMetadata } from "../capture";
12
12
  import { vectorSearch } from "../storage/vector";
@@ -17,6 +17,11 @@ import type { Logger, Chunk, PluginContext } from "../types";
17
17
  import { viewerHTML } from "./html";
18
18
  import { v4 as uuid } from "uuid";
19
19
 
20
+ function normalizeTimestamp(ts: number): number {
21
+ if (ts < 1e12) return ts * 1000;
22
+ return ts;
23
+ }
24
+
20
25
  export interface ViewerServerOptions {
21
26
  store: SqliteStore;
22
27
  embedder: Embedder;
@@ -43,6 +48,14 @@ export class ViewerServer {
43
48
  private readonly ctx?: PluginContext;
44
49
 
45
50
  private static readonly SESSION_TTL = 24 * 60 * 60 * 1000;
51
+ private static readonly PLUGIN_VERSION: string = (() => {
52
+ try {
53
+ const pkgPath = path.resolve(__dirname, "../../package.json");
54
+ return JSON.parse(fs.readFileSync(pkgPath, "utf-8")).version ?? "unknown";
55
+ } catch {
56
+ return "unknown";
57
+ }
58
+ })();
46
59
  private resetToken: string;
47
60
  private migrationRunning = false;
48
61
  private migrationAbort = false;
@@ -93,11 +106,28 @@ export class ViewerServer {
93
106
  this.server.listen(this.port, "127.0.0.1", () => {
94
107
  const addr = this.server!.address();
95
108
  const actualPort = typeof addr === "object" && addr ? addr.port : this.port;
109
+ this.autoCleanupPolluted();
96
110
  resolve(`http://127.0.0.1:${actualPort}`);
97
111
  });
98
112
  });
99
113
  }
100
114
 
115
+ private autoCleanupPolluted(): void {
116
+ try {
117
+ const polluted = this.store.findPollutedUserChunks();
118
+ let deleted = 0;
119
+ for (const { id } of polluted) {
120
+ if (this.store.deleteChunk(id)) deleted++;
121
+ }
122
+ const fixed = this.store.fixMixedUserChunks();
123
+ if (deleted > 0 || fixed > 0) {
124
+ this.log.info(`Auto-cleanup: removed ${deleted} polluted chunks, fixed ${fixed} mixed user+assistant chunks`);
125
+ }
126
+ } catch (err) {
127
+ this.log.warn(`Auto-cleanup failed: ${err}`);
128
+ }
129
+ }
130
+
101
131
  stop(): void {
102
132
  this.server?.close();
103
133
  this.server = null;
@@ -216,9 +246,11 @@ export class ViewerServer {
216
246
  else if (p === "/api/config" && req.method === "GET") this.serveConfig(res);
217
247
  else if (p === "/api/config" && req.method === "PUT") this.handleSaveConfig(req, res);
218
248
  else if (p === "/api/test-model" && req.method === "POST") this.handleTestModel(req, res);
249
+ else if (p === "/api/model-health" && req.method === "GET") this.serveModelHealth(res);
219
250
  else if (p === "/api/fallback-model" && req.method === "GET") this.serveFallbackModel(res);
220
251
  else if (p === "/api/update-check" && req.method === "GET") this.handleUpdateCheck(res);
221
252
  else if (p === "/api/auth/logout" && req.method === "POST") this.handleLogout(req, res);
253
+ else if (p === "/api/cleanup-polluted" && req.method === "POST") this.handleCleanupPolluted(res);
222
254
  else if (p === "/api/migrate/scan" && req.method === "GET") this.handleMigrateScan(res);
223
255
  else if (p === "/api/migrate/start" && req.method === "POST") this.handleMigrateStart(req, res);
224
256
  else if (p === "/api/migrate/status" && req.method === "GET") this.handleMigrateStatus(res);
@@ -339,7 +371,7 @@ export class ViewerServer {
339
371
 
340
372
  private serveViewer(res: http.ServerResponse): void {
341
373
  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" });
342
- res.end(viewerHTML);
374
+ res.end(viewerHTML(ViewerServer.PLUGIN_VERSION));
343
375
  }
344
376
 
345
377
  // ─── Data APIs ───
@@ -485,7 +517,15 @@ export class ViewerServer {
485
517
  const total = db.prepare("SELECT COUNT(*) as count FROM chunks").get() as any;
486
518
  const sessions = db.prepare("SELECT COUNT(DISTINCT session_key) as count FROM chunks").get() as any;
487
519
  const roles = db.prepare("SELECT role, COUNT(*) as count FROM chunks GROUP BY role").all() as any[];
488
- const timeRange = db.prepare("SELECT MIN(created_at) as earliest, MAX(created_at) as latest FROM chunks").get() as any;
520
+ const timeRange = db.prepare("SELECT MIN(created_at) as earliest, MAX(created_at) as latest FROM chunks WHERE dedup_status = 'active'").get() as any;
521
+ const MIN_VALID_TS = 1704067200000; // 2024-01-01
522
+ if (timeRange.earliest != null && timeRange.earliest < MIN_VALID_TS) {
523
+ timeRange.earliest = db.prepare("SELECT MIN(created_at) as v FROM chunks WHERE dedup_status = 'active' AND created_at >= ?").get(MIN_VALID_TS) as any;
524
+ timeRange.earliest = timeRange.earliest?.v ?? null;
525
+ }
526
+ if (timeRange.latest != null && timeRange.latest < MIN_VALID_TS) {
527
+ timeRange.latest = null;
528
+ }
489
529
  let embCount = 0;
490
530
  try { embCount = (db.prepare("SELECT COUNT(*) as count FROM embeddings").get() as any).count; } catch { /* table may not exist */ }
491
531
  const kinds = db.prepare("SELECT kind, COUNT(*) as count FROM chunks GROUP BY kind").all() as any[];
@@ -531,44 +571,71 @@ export class ViewerServer {
531
571
 
532
572
  const role = url.searchParams.get("role") ?? undefined;
533
573
  const kind = url.searchParams.get("kind") ?? undefined;
574
+ const session = url.searchParams.get("session") ?? undefined;
575
+ const owner = url.searchParams.get("owner") ?? undefined;
534
576
  const dateFrom = url.searchParams.get("dateFrom") ?? undefined;
535
577
  const dateTo = url.searchParams.get("dateTo") ?? undefined;
536
578
 
537
579
  const passesFilter = (r: any): boolean => {
538
580
  if (role && r.role !== role) return false;
539
581
  if (kind && r.kind !== kind) return false;
582
+ if (session && r.session_key !== session) return false;
583
+ if (owner && r.owner !== owner) return false;
540
584
  if (dateFrom && r.created_at < new Date(dateFrom).getTime()) return false;
541
585
  if (dateTo && r.created_at > new Date(dateTo).getTime()) return false;
542
586
  return true;
543
587
  };
544
588
 
589
+ const ftsFilters: string[] = [];
590
+ const likeFilters: string[] = [];
591
+ const sqlParams: any[] = [];
592
+ if (session) { ftsFilters.push("c.session_key = ?"); likeFilters.push("session_key = ?"); sqlParams.push(session); }
593
+ if (owner) { ftsFilters.push("c.owner = ?"); likeFilters.push("owner = ?"); sqlParams.push(owner); }
594
+ const ftsWhere = ftsFilters.length > 0 ? " AND " + ftsFilters.join(" AND ") : "";
595
+ const likeWhere = likeFilters.length > 0 ? " AND " + likeFilters.join(" AND ") : "";
596
+
545
597
  const db = (this.store as any).db;
546
598
  let ftsResults: any[] = [];
547
599
  try {
548
600
  ftsResults = db.prepare(
549
- "SELECT c.* FROM chunks_fts f JOIN chunks c ON f.rowid = c.rowid WHERE chunks_fts MATCH ? ORDER BY rank LIMIT 100",
550
- ).all(q).filter(passesFilter);
601
+ `SELECT c.* FROM chunks_fts f JOIN chunks c ON f.rowid = c.rowid WHERE chunks_fts MATCH ?${ftsWhere} ORDER BY rank LIMIT 100`,
602
+ ).all(q, ...sqlParams).filter(passesFilter);
551
603
  } catch { /* FTS syntax error, fall through */ }
552
604
  if (ftsResults.length === 0) {
553
- ftsResults = db.prepare(
554
- "SELECT * FROM chunks WHERE content LIKE ? OR summary LIKE ? ORDER BY created_at DESC LIMIT 100",
555
- ).all(`%${q}%`, `%${q}%`).filter(passesFilter);
605
+ try {
606
+ ftsResults = db.prepare(
607
+ `SELECT * FROM chunks WHERE (content LIKE ? OR summary LIKE ?)${likeWhere} ORDER BY created_at DESC LIMIT 100`,
608
+ ).all(`%${q}%`, `%${q}%`, ...sqlParams).filter(passesFilter);
609
+ } catch (err) {
610
+ this.log.warn(`LIKE search failed: ${err}`);
611
+ }
556
612
  }
557
613
 
558
614
  const SEMANTIC_THRESHOLD = 0.64;
615
+ const VECTOR_TIMEOUT_MS = 8000;
559
616
  let vectorResults: any[] = [];
560
617
  let scoreMap = new Map<string, number>();
561
618
  try {
562
- const queryVec = await this.embedder.embedQuery(q);
563
- const hits = vectorSearch(this.store, queryVec, 40);
564
- scoreMap = new Map(hits.map(h => [h.chunkId, h.score]));
565
- const hitIds = new Set(hits.filter(h => h.score >= SEMANTIC_THRESHOLD).map(h => h.chunkId));
566
- if (hitIds.size > 0) {
567
- const placeholders = [...hitIds].map(() => "?").join(",");
568
- const rows = db.prepare(`SELECT * FROM chunks WHERE id IN (${placeholders})`).all(...hitIds).filter(passesFilter);
569
- rows.forEach((r: any) => { r._vscore = scoreMap.get(r.id) ?? 0; });
570
- rows.sort((a: any, b: any) => (b._vscore ?? 0) - (a._vscore ?? 0));
571
- vectorResults = rows;
619
+ const vecPromise = (async () => {
620
+ const queryVec = await this.embedder.embedQuery(q);
621
+ return vectorSearch(this.store, queryVec, 40);
622
+ })();
623
+ const hits = await Promise.race([
624
+ vecPromise,
625
+ new Promise<null>((resolve) => setTimeout(() => resolve(null), VECTOR_TIMEOUT_MS)),
626
+ ]);
627
+ if (hits) {
628
+ scoreMap = new Map(hits.map(h => [h.chunkId, h.score]));
629
+ const hitIds = new Set(hits.filter(h => h.score >= SEMANTIC_THRESHOLD).map(h => h.chunkId));
630
+ if (hitIds.size > 0) {
631
+ const placeholders = [...hitIds].map(() => "?").join(",");
632
+ const rows = db.prepare(`SELECT * FROM chunks WHERE id IN (${placeholders})${likeWhere}`).all(...hitIds, ...sqlParams).filter(passesFilter);
633
+ rows.forEach((r: any) => { r._vscore = scoreMap.get(r.id) ?? 0; });
634
+ rows.sort((a: any, b: any) => (b._vscore ?? 0) - (a._vscore ?? 0));
635
+ vectorResults = rows;
636
+ }
637
+ } else {
638
+ this.log.warn("Vector search timed out, returning FTS results only");
572
639
  }
573
640
  } catch (err) {
574
641
  this.log.warn(`Vector search failed (falling back to FTS only): ${err}`);
@@ -1041,8 +1108,8 @@ export class ViewerServer {
1041
1108
  return;
1042
1109
  }
1043
1110
  if (type === "embedding") {
1044
- await this.testEmbeddingModel(provider, model, endpoint, apiKey);
1045
- this.jsonResponse(res, { ok: true, detail: `${provider}/${model}` });
1111
+ const dims = await this.testEmbeddingModel(provider, model, endpoint, apiKey);
1112
+ this.jsonResponse(res, { ok: true, detail: `${provider}/${model}`, dimensions: dims });
1046
1113
  } else {
1047
1114
  await this.testChatModel(provider, model, endpoint, apiKey);
1048
1115
  this.jsonResponse(res, { ok: true, detail: `${provider}/${model}` });
@@ -1055,6 +1122,10 @@ export class ViewerServer {
1055
1122
  });
1056
1123
  }
1057
1124
 
1125
+ private serveModelHealth(res: http.ServerResponse): void {
1126
+ this.jsonResponse(res, { models: modelHealth.getAll() });
1127
+ }
1128
+
1058
1129
  private serveFallbackModel(res: http.ServerResponse): void {
1059
1130
  try {
1060
1131
  const cfgPath = this.getOpenClawConfigPath();
@@ -1134,9 +1205,9 @@ export class ViewerServer {
1134
1205
  }
1135
1206
  }
1136
1207
 
1137
- private async testEmbeddingModel(provider: string, model: string, endpoint: string, apiKey: string): Promise<void> {
1208
+ private async testEmbeddingModel(provider: string, model: string, endpoint: string, apiKey: string): Promise<number | undefined> {
1138
1209
  if (provider === "local") {
1139
- return;
1210
+ return 384;
1140
1211
  }
1141
1212
  const baseUrl = (endpoint || "https://api.openai.com/v1").replace(/\/+$/, "");
1142
1213
  const embUrl = baseUrl.endsWith("/embeddings") ? baseUrl : `${baseUrl}/embeddings`;
@@ -1149,39 +1220,59 @@ export class ViewerServer {
1149
1220
  const resp = await fetch(baseUrl.replace(/\/v\d+.*/, "/v2/embed"), {
1150
1221
  method: "POST",
1151
1222
  headers,
1152
- body: JSON.stringify({ texts: ["test"], model: model || "embed-english-v3.0", input_type: "search_query", embedding_types: ["float"] }),
1223
+ body: JSON.stringify({ texts: ["test embedding vector"], model: model || "embed-english-v3.0", input_type: "search_query", embedding_types: ["float"] }),
1153
1224
  signal: AbortSignal.timeout(15_000),
1154
1225
  });
1155
1226
  if (!resp.ok) {
1156
1227
  const txt = await resp.text();
1157
1228
  throw new Error(`Cohere embed ${resp.status}: ${txt}`);
1158
1229
  }
1159
- return;
1230
+ const json = await resp.json() as any;
1231
+ const vecs = json?.embeddings?.float;
1232
+ if (!Array.isArray(vecs) || vecs.length === 0 || !Array.isArray(vecs[0]) || vecs[0].length === 0) {
1233
+ throw new Error("Cohere returned empty embedding vector");
1234
+ }
1235
+ return vecs[0].length;
1160
1236
  }
1161
1237
  if (provider === "gemini") {
1162
1238
  const url = `https://generativelanguage.googleapis.com/v1/models/${model || "text-embedding-004"}:embedContent?key=${apiKey}`;
1163
1239
  const resp = await fetch(url, {
1164
1240
  method: "POST",
1165
1241
  headers: { "Content-Type": "application/json" },
1166
- body: JSON.stringify({ content: { parts: [{ text: "test" }] } }),
1242
+ body: JSON.stringify({ content: { parts: [{ text: "test embedding vector" }] } }),
1167
1243
  signal: AbortSignal.timeout(15_000),
1168
1244
  });
1169
1245
  if (!resp.ok) {
1170
1246
  const txt = await resp.text();
1171
1247
  throw new Error(`Gemini embed ${resp.status}: ${txt}`);
1172
1248
  }
1173
- return;
1249
+ const json = await resp.json() as any;
1250
+ const vec = json?.embedding?.values;
1251
+ if (!Array.isArray(vec) || vec.length === 0) {
1252
+ throw new Error("Gemini returned empty embedding vector");
1253
+ }
1254
+ return vec.length;
1174
1255
  }
1175
1256
  const resp = await fetch(embUrl, {
1176
1257
  method: "POST",
1177
1258
  headers,
1178
- body: JSON.stringify({ input: ["test"], model: model || "text-embedding-3-small" }),
1259
+ body: JSON.stringify({ input: ["test embedding vector"], model: model || "text-embedding-3-small" }),
1179
1260
  signal: AbortSignal.timeout(15_000),
1180
1261
  });
1181
1262
  if (!resp.ok) {
1182
1263
  const txt = await resp.text();
1183
1264
  throw new Error(`${resp.status}: ${txt}`);
1184
1265
  }
1266
+ const json = await resp.json() as any;
1267
+ const data = json?.data;
1268
+ if (!Array.isArray(data) || data.length === 0) {
1269
+ throw new Error("API returned no embedding data");
1270
+ }
1271
+ const vec = data[0]?.embedding;
1272
+ if (!Array.isArray(vec) || vec.length === 0) {
1273
+ throw new Error(`API returned empty embedding vector (got ${JSON.stringify(vec)?.slice(0, 100)})`);
1274
+ }
1275
+ return vec.length;
1185
1276
  }
1186
1277
 
1187
1278
  private async testChatModel(provider: string, model: string, endpoint: string, apiKey: string): Promise<void> {
@@ -1256,6 +1347,28 @@ export class ViewerServer {
1256
1347
  return path.join(home, ".openclaw");
1257
1348
  }
1258
1349
 
1350
+ private handleCleanupPolluted(res: http.ServerResponse): void {
1351
+ try {
1352
+ const polluted = this.store.findPollutedUserChunks();
1353
+ let deleted = 0;
1354
+ for (const { id, reason } of polluted) {
1355
+ if (this.store.deleteChunk(id)) {
1356
+ deleted++;
1357
+ this.log.info(`Cleaned polluted chunk ${id}: ${reason}`);
1358
+ }
1359
+ }
1360
+ const fixed = this.store.fixMixedUserChunks();
1361
+ this.log.info(`Cleanup: removed ${deleted} polluted, fixed ${fixed} mixed chunks`);
1362
+ res.writeHead(200, { "Content-Type": "application/json" });
1363
+ res.end(JSON.stringify({ deleted, fixed, total: polluted.length }));
1364
+ } catch (err) {
1365
+ const msg = err instanceof Error ? err.message : String(err);
1366
+ this.log.error(`handleCleanupPolluted error: ${msg}`);
1367
+ res.writeHead(500, { "Content-Type": "application/json" });
1368
+ res.end(JSON.stringify({ error: msg }));
1369
+ }
1370
+ }
1371
+
1259
1372
  private handleMigrateScan(res: http.ServerResponse): void {
1260
1373
  try {
1261
1374
  const ocHome = this.getOpenClawHome();
@@ -1499,7 +1612,6 @@ export class ViewerServer {
1499
1612
 
1500
1613
  const cfgPath = this.getOpenClawConfigPath();
1501
1614
  let summarizerCfg: any;
1502
- let strongCfg: any;
1503
1615
  try {
1504
1616
  const raw = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
1505
1617
  const pluginCfg = raw?.plugins?.entries?.["memos-local-openclaw-plugin"]?.config ??
@@ -1507,10 +1619,9 @@ export class ViewerServer {
1507
1619
  raw?.plugins?.entries?.["memos-lite-openclaw-plugin"]?.config ??
1508
1620
  raw?.plugins?.entries?.["memos-lite"]?.config ?? {};
1509
1621
  summarizerCfg = pluginCfg.summarizer;
1510
- strongCfg = pluginCfg.skillEvolution?.summarizer;
1511
1622
  } catch { /* no config */ }
1512
1623
 
1513
- const summarizer = new Summarizer(summarizerCfg, this.log, strongCfg);
1624
+ const summarizer = new Summarizer(summarizerCfg, this.log);
1514
1625
 
1515
1626
  // Phase 1: Import SQLite memory chunks
1516
1627
  if (importSqlite) {
@@ -1636,8 +1747,8 @@ export class ViewerServer {
1636
1747
  mergeCount: 0,
1637
1748
  lastHitAt: null,
1638
1749
  mergeHistory: "[]",
1639
- createdAt: row.updated_at * 1000,
1640
- updatedAt: row.updated_at * 1000,
1750
+ createdAt: normalizeTimestamp(row.updated_at),
1751
+ updatedAt: normalizeTimestamp(row.updated_at),
1641
1752
  };
1642
1753
 
1643
1754
  this.store.insertChunk(chunk);