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

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 (90) hide show
  1. package/dist/capture/index.js +52 -8
  2. package/dist/capture/index.js.map +1 -1
  3. package/dist/embedding/index.d.ts.map +1 -1
  4. package/dist/embedding/index.js +4 -3
  5. package/dist/embedding/index.js.map +1 -1
  6. package/dist/ingest/chunker.d.ts +3 -4
  7. package/dist/ingest/chunker.d.ts.map +1 -1
  8. package/dist/ingest/chunker.js +19 -24
  9. package/dist/ingest/chunker.js.map +1 -1
  10. package/dist/ingest/providers/anthropic.d.ts +3 -1
  11. package/dist/ingest/providers/anthropic.d.ts.map +1 -1
  12. package/dist/ingest/providers/anthropic.js +79 -39
  13. package/dist/ingest/providers/anthropic.js.map +1 -1
  14. package/dist/ingest/providers/bedrock.d.ts +3 -1
  15. package/dist/ingest/providers/bedrock.d.ts.map +1 -1
  16. package/dist/ingest/providers/bedrock.js +79 -39
  17. package/dist/ingest/providers/bedrock.js.map +1 -1
  18. package/dist/ingest/providers/gemini.d.ts +3 -1
  19. package/dist/ingest/providers/gemini.d.ts.map +1 -1
  20. package/dist/ingest/providers/gemini.js +77 -39
  21. package/dist/ingest/providers/gemini.js.map +1 -1
  22. package/dist/ingest/providers/index.d.ts +3 -1
  23. package/dist/ingest/providers/index.d.ts.map +1 -1
  24. package/dist/ingest/providers/index.js +107 -30
  25. package/dist/ingest/providers/index.js.map +1 -1
  26. package/dist/ingest/providers/openai.d.ts +3 -1
  27. package/dist/ingest/providers/openai.d.ts.map +1 -1
  28. package/dist/ingest/providers/openai.js +80 -39
  29. package/dist/ingest/providers/openai.js.map +1 -1
  30. package/dist/ingest/task-processor.d.ts +1 -0
  31. package/dist/ingest/task-processor.d.ts.map +1 -1
  32. package/dist/ingest/task-processor.js +33 -9
  33. package/dist/ingest/task-processor.js.map +1 -1
  34. package/dist/ingest/worker.d.ts.map +1 -1
  35. package/dist/ingest/worker.js +29 -13
  36. package/dist/ingest/worker.js.map +1 -1
  37. package/dist/recall/engine.d.ts.map +1 -1
  38. package/dist/recall/engine.js +19 -14
  39. package/dist/recall/engine.js.map +1 -1
  40. package/dist/skill/bundled-memory-guide.d.ts +1 -5
  41. package/dist/skill/bundled-memory-guide.d.ts.map +1 -1
  42. package/dist/skill/bundled-memory-guide.js +38 -97
  43. package/dist/skill/bundled-memory-guide.js.map +1 -1
  44. package/dist/skill/evaluator.js +1 -1
  45. package/dist/storage/sqlite.d.ts +1 -2
  46. package/dist/storage/sqlite.d.ts.map +1 -1
  47. package/dist/storage/sqlite.js +90 -17
  48. package/dist/storage/sqlite.js.map +1 -1
  49. package/dist/tools/memory-get.d.ts.map +1 -1
  50. package/dist/tools/memory-get.js +1 -3
  51. package/dist/tools/memory-get.js.map +1 -1
  52. package/dist/types.d.ts +3 -3
  53. package/dist/types.d.ts.map +1 -1
  54. package/dist/types.js +1 -1
  55. package/dist/types.js.map +1 -1
  56. package/dist/update-check.d.ts +21 -0
  57. package/dist/update-check.d.ts.map +1 -0
  58. package/dist/update-check.js +110 -0
  59. package/dist/update-check.js.map +1 -0
  60. package/dist/viewer/html.d.ts.map +1 -1
  61. package/dist/viewer/html.js +487 -189
  62. package/dist/viewer/html.js.map +1 -1
  63. package/dist/viewer/server.d.ts +1 -1
  64. package/dist/viewer/server.d.ts.map +1 -1
  65. package/dist/viewer/server.js +240 -78
  66. package/dist/viewer/server.js.map +1 -1
  67. package/index.ts +205 -197
  68. package/openclaw.plugin.json +3 -0
  69. package/package.json +8 -3
  70. package/scripts/postinstall.cjs +69 -2
  71. package/skill/memos-memory-guide/SKILL.md +73 -36
  72. package/src/capture/index.ts +52 -8
  73. package/src/embedding/index.ts +4 -2
  74. package/src/ingest/chunker.ts +22 -30
  75. package/src/ingest/providers/anthropic.ts +89 -41
  76. package/src/ingest/providers/bedrock.ts +90 -41
  77. package/src/ingest/providers/gemini.ts +89 -41
  78. package/src/ingest/providers/index.ts +118 -35
  79. package/src/ingest/providers/openai.ts +90 -41
  80. package/src/ingest/task-processor.ts +29 -8
  81. package/src/ingest/worker.ts +31 -13
  82. package/src/recall/engine.ts +20 -13
  83. package/src/skill/bundled-memory-guide.ts +5 -96
  84. package/src/skill/evaluator.ts +1 -1
  85. package/src/storage/sqlite.ts +93 -21
  86. package/src/tools/memory-get.ts +1 -4
  87. package/src/types.ts +9 -10
  88. package/src/update-check.ts +95 -0
  89. package/src/viewer/html.ts +487 -189
  90. package/src/viewer/server.ts +187 -66
@@ -1,6 +1,7 @@
1
1
  import http from "node:http";
2
+ import os from "node:os";
2
3
  import crypto from "node:crypto";
3
- import { execSync } from "node:child_process";
4
+ import { execSync, exec } from "node:child_process";
4
5
  import fs from "node:fs";
5
6
  import path from "node:path";
6
7
  import readline from "node:readline";
@@ -75,8 +76,8 @@ export class ViewerServer {
75
76
 
76
77
  private ppRunning = false;
77
78
  private ppAbort = false;
78
- private ppState: { running: boolean; done: boolean; stopped: boolean; processed: number; total: number; tasksCreated: number; skillsCreated: number; errors: number } =
79
- { running: false, done: false, stopped: false, processed: 0, total: 0, tasksCreated: 0, skillsCreated: 0, errors: 0 };
79
+ private ppState: { running: boolean; done: boolean; stopped: boolean; processed: number; total: number; tasksCreated: number; skillsCreated: number; errors: number; skippedSessions: number; totalSessions: number } =
80
+ { running: false, done: false, stopped: false, processed: 0, total: 0, tasksCreated: 0, skillsCreated: 0, errors: 0, skippedSessions: 0, totalSessions: 0 };
80
81
  private ppSSEClients: http.ServerResponse[] = [];
81
82
 
82
83
  constructor(opts: ViewerServerOptions) {
@@ -235,7 +236,6 @@ export class ViewerServer {
235
236
  else if (p.startsWith("/api/skill/") && req.method === "DELETE") this.handleSkillDelete(res, p);
236
237
  else if (p.startsWith("/api/skill/") && req.method === "PUT") this.handleSkillUpdate(req, res, p);
237
238
  else if (p.startsWith("/api/skill/") && req.method === "GET") this.serveSkillDetail(res, p);
238
- else if (p === "/api/memory" && req.method === "POST") this.handleCreate(req, res);
239
239
  else if (p.startsWith("/api/memory/") && req.method === "GET") this.serveMemoryDetail(res, p);
240
240
  else if (p.startsWith("/api/memory/") && req.method === "PUT") this.handleUpdate(req, res, p);
241
241
  else if (p.startsWith("/api/memory/") && req.method === "DELETE") this.handleDelete(res, p);
@@ -249,6 +249,7 @@ export class ViewerServer {
249
249
  else if (p === "/api/model-health" && req.method === "GET") this.serveModelHealth(res);
250
250
  else if (p === "/api/fallback-model" && req.method === "GET") this.serveFallbackModel(res);
251
251
  else if (p === "/api/update-check" && req.method === "GET") this.handleUpdateCheck(res);
252
+ else if (p === "/api/update-install" && req.method === "POST") this.handleUpdateInstall(req, res);
252
253
  else if (p === "/api/auth/logout" && req.method === "POST") this.handleLogout(req, res);
253
254
  else if (p === "/api/cleanup-polluted" && req.method === "POST") this.handleCleanupPolluted(res);
254
255
  else if (p === "/api/migrate/scan" && req.method === "GET") this.handleMigrateScan(res);
@@ -382,7 +383,6 @@ export class ViewerServer {
382
383
  const offset = (page - 1) * limit;
383
384
  const session = url.searchParams.get("session") ?? undefined;
384
385
  const role = url.searchParams.get("role") ?? undefined;
385
- const kind = url.searchParams.get("kind") ?? undefined;
386
386
  const dateFrom = url.searchParams.get("dateFrom") ?? undefined;
387
387
  const dateTo = url.searchParams.get("dateTo") ?? undefined;
388
388
  const owner = url.searchParams.get("owner") ?? undefined;
@@ -393,7 +393,6 @@ export class ViewerServer {
393
393
  const params: any[] = [];
394
394
  if (session) { conditions.push("session_key = ?"); params.push(session); }
395
395
  if (role) { conditions.push("role = ?"); params.push(role); }
396
- if (kind) { conditions.push("kind = ?"); params.push(kind); }
397
396
  if (owner) { conditions.push("owner = ?"); params.push(owner); }
398
397
  if (dateFrom) { conditions.push("created_at >= ?"); params.push(new Date(dateFrom).getTime()); }
399
398
  if (dateTo) { conditions.push("created_at <= ?"); params.push(new Date(dateTo).getTime()); }
@@ -401,9 +400,14 @@ export class ViewerServer {
401
400
  const where = conditions.length > 0 ? " WHERE " + conditions.join(" AND ") : "";
402
401
  const totalRow = db.prepare("SELECT COUNT(*) as count FROM chunks" + where).get(...params) as any;
403
402
  const rawMemories = db.prepare("SELECT * FROM chunks" + where + ` ORDER BY created_at ${sortBy} LIMIT ? OFFSET ?`).all(...params, limit, offset);
403
+ const findMergeSources = db.prepare("SELECT id, summary, role FROM chunks WHERE dedup_target = ? AND (dedup_status = 'merged' OR dedup_status = 'duplicate')");
404
404
  const memories = rawMemories.map((m: any) => {
405
405
  if (m.role === "user" && m.content) {
406
- return { ...m, content: stripInboundMetadata(m.content) };
406
+ m = { ...m, content: stripInboundMetadata(m.content) };
407
+ }
408
+ if (m.merge_count > 0) {
409
+ const sources = findMergeSources.all(m.id) as Array<{ id: string; summary: string; role: string }>;
410
+ m.merge_sources = sources;
407
411
  }
408
412
  return m;
409
413
  });
@@ -441,7 +445,7 @@ export class ViewerServer {
441
445
  id: t.id,
442
446
  sessionKey: t.sessionKey,
443
447
  title: t.title,
444
- summary: t.summary ? (t.summary.length > 300 ? t.summary.slice(0, 297) + "..." : t.summary) : "",
448
+ summary: t.summary ?? "",
445
449
  status: t.status,
446
450
  startedAt: t.startedAt,
447
451
  endedAt: t.endedAt,
@@ -464,8 +468,7 @@ export class ViewerServer {
464
468
 
465
469
  const chunks = this.store.getChunksByTask(taskId);
466
470
  const chunkItems = chunks.map((c) => {
467
- let text = c.role === "user" ? stripInboundMetadata(c.content) : c.content;
468
- if (text.length > 500) text = text.slice(0, 497) + "...";
471
+ const text = c.role === "user" ? stripInboundMetadata(c.content) : c.content;
469
472
  return { id: c.id, role: c.role, content: text, summary: c.summary, createdAt: c.createdAt };
470
473
  });
471
474
 
@@ -502,7 +505,7 @@ export class ViewerServer {
502
505
  const emptyStats = {
503
506
  totalMemories: 0, totalSessions: 0, totalEmbeddings: 0, totalSkills: 0,
504
507
  embeddingProvider: this.embedder?.provider ?? "none",
505
- roleBreakdown: {}, kindBreakdown: {}, dedupBreakdown: {},
508
+ dedupBreakdown: {},
506
509
  timeRange: { earliest: null, latest: null },
507
510
  sessions: [],
508
511
  };
@@ -516,7 +519,6 @@ export class ViewerServer {
516
519
  const db = (this.store as any).db;
517
520
  const total = db.prepare("SELECT COUNT(*) as count FROM chunks").get() as any;
518
521
  const sessions = db.prepare("SELECT COUNT(DISTINCT session_key) as count FROM chunks").get() as any;
519
- const roles = db.prepare("SELECT role, COUNT(*) as count FROM chunks GROUP BY role").all() as any[];
520
522
  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
523
  const MIN_VALID_TS = 1704067200000; // 2024-01-01
522
524
  if (timeRange.earliest != null && timeRange.earliest < MIN_VALID_TS) {
@@ -528,7 +530,6 @@ export class ViewerServer {
528
530
  }
529
531
  let embCount = 0;
530
532
  try { embCount = (db.prepare("SELECT COUNT(*) as count FROM embeddings").get() as any).count; } catch { /* table may not exist */ }
531
- const kinds = db.prepare("SELECT kind, COUNT(*) as count FROM chunks GROUP BY kind").all() as any[];
532
533
  const sessionList = db.prepare(
533
534
  "SELECT session_key, COUNT(*) as count, MIN(created_at) as earliest, MAX(created_at) as latest FROM chunks GROUP BY session_key ORDER BY latest DESC",
534
535
  ).all() as any[];
@@ -552,8 +553,6 @@ export class ViewerServer {
552
553
  totalMemories: total.count, totalSessions: sessions.count, totalEmbeddings: embCount,
553
554
  totalSkills: skillCount,
554
555
  embeddingProvider: this.embedder.provider,
555
- roleBreakdown: Object.fromEntries(roles.map((r: any) => [r.role, r.count])),
556
- kindBreakdown: Object.fromEntries(kinds.map((k: any) => [k.kind, k.count])),
557
556
  dedupBreakdown,
558
557
  timeRange: { earliest: timeRange.earliest, latest: timeRange.latest },
559
558
  sessions: sessionList,
@@ -570,7 +569,6 @@ export class ViewerServer {
570
569
  if (!q.trim()) { this.jsonResponse(res, { results: [], query: q }); return; }
571
570
 
572
571
  const role = url.searchParams.get("role") ?? undefined;
573
- const kind = url.searchParams.get("kind") ?? undefined;
574
572
  const session = url.searchParams.get("session") ?? undefined;
575
573
  const owner = url.searchParams.get("owner") ?? undefined;
576
574
  const dateFrom = url.searchParams.get("dateFrom") ?? undefined;
@@ -578,7 +576,6 @@ export class ViewerServer {
578
576
 
579
577
  const passesFilter = (r: any): boolean => {
580
578
  if (role && r.role !== role) return false;
581
- if (kind && r.kind !== kind) return false;
582
579
  if (session && r.session_key !== session) return false;
583
580
  if (owner && r.owner !== owner) return false;
584
581
  if (dateFrom && r.created_at < new Date(dateFrom).getTime()) return false;
@@ -920,35 +917,6 @@ export class ViewerServer {
920
917
 
921
918
  // ─── CRUD ───
922
919
 
923
- private handleCreate(req: http.IncomingMessage, res: http.ServerResponse): void {
924
- this.readBody(req, (body) => {
925
- try {
926
- const data = JSON.parse(body);
927
- if (!data.content || typeof data.content !== "string" || !data.content.trim()) {
928
- res.writeHead(400, { "Content-Type": "application/json" });
929
- res.end(JSON.stringify({ error: "content is required and must be a non-empty string" }));
930
- return;
931
- }
932
- const { v4: uuidv4 } = require("uuid");
933
- const id = uuidv4();
934
- const now = Date.now();
935
- this.store.insertChunk({
936
- id, sessionKey: data.session_key || "manual", turnId: `manual-${now}`, seq: 0,
937
- role: data.role || "user", content: data.content, kind: data.kind || "paragraph",
938
- summary: data.summary || data.content.slice(0, 100),
939
- taskId: null, skillId: null, owner: data.owner || "agent:main",
940
- dedupStatus: "active", dedupTarget: null, dedupReason: null,
941
- mergeCount: 0, lastHitAt: null, mergeHistory: "[]",
942
- createdAt: now, updatedAt: now, embedding: null,
943
- });
944
- this.jsonResponse(res, { ok: true, id, message: "Memory created" });
945
- } catch (err) {
946
- res.writeHead(400, { "Content-Type": "application/json" });
947
- res.end(JSON.stringify({ error: String(err) }));
948
- }
949
- });
950
- }
951
-
952
920
  private serveMemoryDetail(res: http.ServerResponse, urlPath: string): void {
953
921
  const chunkId = urlPath.replace("/api/memory/", "");
954
922
  const chunk = this.store.getChunk(chunkId);
@@ -973,7 +941,7 @@ export class ViewerServer {
973
941
  res.end(JSON.stringify({ error: "content must be a non-empty string" }));
974
942
  return;
975
943
  }
976
- const ok = this.store.updateChunk(chunkId, { summary: data.summary, content: data.content, role: data.role, kind: data.kind, owner: data.owner });
944
+ const ok = this.store.updateChunk(chunkId, { summary: data.summary, content: data.content, role: data.role, owner: data.owner });
977
945
  if (ok) this.jsonResponse(res, { ok: true, message: "Memory updated" });
978
946
  else { res.writeHead(404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Not found" })); }
979
947
  } catch (err) {
@@ -1184,20 +1152,20 @@ export class ViewerServer {
1184
1152
  this.jsonResponse(res, { updateAvailable: false, current });
1185
1153
  return;
1186
1154
  }
1187
- const npmResp = await fetch(`https://registry.npmjs.org/${name}/latest`, {
1188
- signal: AbortSignal.timeout(6_000),
1189
- });
1190
- if (!npmResp.ok) {
1191
- this.jsonResponse(res, { updateAvailable: false, current });
1155
+ const { computeUpdateCheck } = await import("../update-check");
1156
+ const result = await computeUpdateCheck(name, current, fetch, 6_000);
1157
+ if (!result) {
1158
+ this.jsonResponse(res, { updateAvailable: false, current, packageName: name });
1192
1159
  return;
1193
1160
  }
1194
- const data = await npmResp.json() as { version?: string };
1195
- const latest = data.version ?? current;
1196
1161
  this.jsonResponse(res, {
1197
- updateAvailable: latest !== current,
1198
- current,
1199
- latest,
1200
- packageName: name,
1162
+ updateAvailable: result.updateAvailable,
1163
+ current: result.current,
1164
+ latest: result.latest,
1165
+ packageName: result.packageName,
1166
+ channel: result.channel,
1167
+ installCommand: result.installCommand,
1168
+ stableChannel: result.stableChannel,
1201
1169
  });
1202
1170
  } catch (e) {
1203
1171
  this.log.warn(`handleUpdateCheck error: ${e}`);
@@ -1205,6 +1173,132 @@ export class ViewerServer {
1205
1173
  }
1206
1174
  }
1207
1175
 
1176
+ private handleUpdateInstall(req: http.IncomingMessage, res: http.ServerResponse): void {
1177
+ let body = "";
1178
+ req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
1179
+ req.on("end", () => {
1180
+ try {
1181
+ const { packageSpec: rawSpec } = JSON.parse(body);
1182
+ if (!rawSpec || typeof rawSpec !== "string") {
1183
+ res.writeHead(400, { "Content-Type": "application/json" });
1184
+ res.end(JSON.stringify({ ok: false, error: "Missing packageSpec" }));
1185
+ return;
1186
+ }
1187
+ const packageSpec = rawSpec.trim().replace(/^(?:npx\s+)?openclaw\s+plugins\s+install\s+/i, "");
1188
+ const allowed = /^@[\w-]+\/[\w.-]+(@[\w.-]+)?$/;
1189
+ this.log.info(`update-install: received packageSpec="${packageSpec}" (len=${packageSpec.length})`);
1190
+ if (!allowed.test(packageSpec)) {
1191
+ this.log.warn(`update-install: rejected packageSpec="${packageSpec}" — does not match ${allowed}`);
1192
+ res.writeHead(400, { "Content-Type": "application/json" });
1193
+ res.end(JSON.stringify({ ok: false, error: `Invalid package spec: "${packageSpec}"` }));
1194
+ return;
1195
+ }
1196
+
1197
+ const pkgPath = this.findPluginPackageJson();
1198
+ const pluginName = pkgPath
1199
+ ? (() => { try { return JSON.parse(fs.readFileSync(pkgPath, "utf-8")).name; } catch { return null; } })()
1200
+ : null;
1201
+ const shortName = pluginName?.replace(/^@[\w-]+\//, "") ?? "memos-local-openclaw-plugin";
1202
+ const extDir = path.join(os.homedir(), ".openclaw", "extensions", shortName);
1203
+ const tmpDir = path.join(os.tmpdir(), `openclaw-update-${Date.now()}`);
1204
+
1205
+ // Download via npm pack, extract, and replace extension dir.
1206
+ // Does NOT touch openclaw.json → no config watcher SIGUSR1.
1207
+ this.log.info(`update-install: downloading ${packageSpec} via npm pack...`);
1208
+ fs.mkdirSync(tmpDir, { recursive: true });
1209
+ exec(`npm pack ${packageSpec} --pack-destination ${tmpDir}`, { timeout: 60_000 }, (packErr, packOut) => {
1210
+ if (packErr) {
1211
+ this.log.warn(`update-install: npm pack failed: ${packErr.message}`);
1212
+ this.jsonResponse(res, { ok: false, error: `Download failed: ${packErr.message}` });
1213
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
1214
+ return;
1215
+ }
1216
+ const tgzFile = packOut.trim().split("\n").pop()!;
1217
+ const tgzPath = path.join(tmpDir, tgzFile);
1218
+ this.log.info(`update-install: downloaded ${tgzFile}, extracting...`);
1219
+
1220
+ const extractDir = path.join(tmpDir, "extract");
1221
+ fs.mkdirSync(extractDir, { recursive: true });
1222
+ exec(`tar -xzf ${tgzPath} -C ${extractDir}`, { timeout: 30_000 }, (tarErr) => {
1223
+ if (tarErr) {
1224
+ this.log.warn(`update-install: tar extract failed: ${tarErr.message}`);
1225
+ this.jsonResponse(res, { ok: false, error: `Extract failed: ${tarErr.message}` });
1226
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
1227
+ return;
1228
+ }
1229
+
1230
+ // npm pack extracts to a "package" subdirectory
1231
+ const srcDir = path.join(extractDir, "package");
1232
+ if (!fs.existsSync(srcDir)) {
1233
+ this.jsonResponse(res, { ok: false, error: "Extracted package has no 'package' dir" });
1234
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
1235
+ return;
1236
+ }
1237
+
1238
+ // Replace extension directory
1239
+ this.log.info(`update-install: replacing ${extDir}...`);
1240
+ try { fs.rmSync(extDir, { recursive: true, force: true }); } catch {}
1241
+ fs.mkdirSync(path.dirname(extDir), { recursive: true });
1242
+ fs.renameSync(srcDir, extDir);
1243
+
1244
+ // Install dependencies
1245
+ this.log.info(`update-install: installing dependencies...`);
1246
+ exec(`cd ${extDir} && npm install --omit=dev --ignore-scripts`, { timeout: 120_000 }, (npmErr, npmOut, npmStderr) => {
1247
+ if (npmErr) {
1248
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
1249
+ this.log.warn(`update-install: npm install failed: ${npmErr.message}`);
1250
+ this.jsonResponse(res, { ok: false, error: `Dependency install failed: ${npmStderr || npmErr.message}` });
1251
+ return;
1252
+ }
1253
+
1254
+ // Rebuild native modules (do not swallow errors)
1255
+ exec(`cd ${extDir} && npm rebuild better-sqlite3`, { timeout: 60_000 }, (rebuildErr, rebuildOut, rebuildStderr) => {
1256
+ if (rebuildErr) {
1257
+ this.log.warn(`update-install: better-sqlite3 rebuild failed: ${rebuildErr.message}`);
1258
+ const stderr = String(rebuildStderr || "").trim();
1259
+ if (stderr) this.log.warn(`update-install: rebuild stderr: ${stderr.slice(0, 500)}`);
1260
+ // Continue so postinstall.cjs can run (it will try rebuild again and show user guidance)
1261
+ }
1262
+
1263
+ // Run postinstall.cjs: legacy cleanup, skill install, version marker, and optional sqlite re-check
1264
+ this.log.info(`update-install: running postinstall...`);
1265
+ exec(`cd ${extDir} && node scripts/postinstall.cjs`, { timeout: 180_000 }, (postErr, postOut, postStderr) => {
1266
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
1267
+
1268
+ if (postErr) {
1269
+ this.log.warn(`update-install: postinstall failed: ${postErr.message}`);
1270
+ const postStderrStr = String(postStderr || "").trim();
1271
+ if (postStderrStr) this.log.warn(`update-install: postinstall stderr: ${postStderrStr.slice(0, 500)}`);
1272
+ // Still report success; plugin is updated, user can run postinstall manually if needed
1273
+ }
1274
+
1275
+ // Read new version
1276
+ let newVersion = "unknown";
1277
+ try {
1278
+ const newPkg = JSON.parse(fs.readFileSync(path.join(extDir, "package.json"), "utf-8"));
1279
+ newVersion = newPkg.version ?? newVersion;
1280
+ } catch {}
1281
+
1282
+ this.log.info(`update-install: success! Updated to ${newVersion}`);
1283
+ this.jsonResponse(res, { ok: true, version: newVersion });
1284
+
1285
+ // Trigger Gateway restart after response is sent
1286
+ setTimeout(() => {
1287
+ this.log.info(`update-install: triggering gateway restart...`);
1288
+ process.kill(process.pid, "SIGUSR1");
1289
+ }, 500);
1290
+ });
1291
+ });
1292
+ });
1293
+ });
1294
+ });
1295
+ } catch (e) {
1296
+ res.writeHead(400, { "Content-Type": "application/json" });
1297
+ res.end(JSON.stringify({ ok: false, error: String(e) }));
1298
+ }
1299
+ });
1300
+ }
1301
+
1208
1302
  private async testEmbeddingModel(provider: string, model: string, endpoint: string, apiKey: string): Promise<number | undefined> {
1209
1303
  if (provider === "local") {
1210
1304
  return 384;
@@ -1438,10 +1532,18 @@ export class ViewerServer {
1438
1532
  }
1439
1533
 
1440
1534
  let importedSessions: string[] = [];
1535
+ let importedChunkCount = 0;
1441
1536
  try {
1442
1537
  if (this.store) {
1443
1538
  importedSessions = this.store.getDistinctSessionKeys()
1444
1539
  .filter((sk: string) => sk.startsWith("openclaw-import-") || sk.startsWith("openclaw-session-"));
1540
+ if (importedSessions.length > 0) {
1541
+ const placeholders = importedSessions.map(() => "?").join(",");
1542
+ const row = (this.store as any).db.prepare(
1543
+ `SELECT COUNT(*) as cnt FROM chunks WHERE session_key IN (${placeholders})`
1544
+ ).get(...importedSessions) as { cnt: number };
1545
+ importedChunkCount = row?.cnt ?? 0;
1546
+ }
1445
1547
  }
1446
1548
  } catch (storeErr) {
1447
1549
  this.log.warn(`migrate/scan: store query failed: ${storeErr}`);
@@ -1456,6 +1558,7 @@ export class ViewerServer {
1456
1558
  hasSummarizer,
1457
1559
  hasImportedData: importedSessions.length > 0,
1458
1560
  importedSessionCount: importedSessions.length,
1561
+ importedChunkCount,
1459
1562
  });
1460
1563
  } catch (e) {
1461
1564
  this.log.warn(`migrate/scan error: ${e}`);
@@ -1587,11 +1690,14 @@ export class ViewerServer {
1587
1690
  } else {
1588
1691
  this.broadcastSSE("done", { ok: true });
1589
1692
  }
1590
- for (const c of this.migrationSSEClients) {
1591
- try { c.end(); } catch { /* ignore */ }
1592
- }
1593
- this.migrationSSEClients = [];
1594
1693
  this.migrationAbort = false;
1694
+ const clientsToClose = [...this.migrationSSEClients];
1695
+ this.migrationSSEClients = [];
1696
+ setTimeout(() => {
1697
+ for (const c of clientsToClose) {
1698
+ try { c.end(); } catch { /* ignore */ }
1699
+ }
1700
+ }, 500);
1595
1701
  });
1596
1702
  });
1597
1703
  }
@@ -2022,7 +2128,7 @@ export class ViewerServer {
2022
2128
  res.on("close", () => { this.ppSSEClients = this.ppSSEClients.filter(c => c !== res); });
2023
2129
 
2024
2130
  this.ppAbort = false;
2025
- this.ppState = { running: true, done: false, stopped: false, processed: 0, total: 0, tasksCreated: 0, skillsCreated: 0, errors: 0 };
2131
+ this.ppState = { running: true, done: false, stopped: false, processed: 0, total: 0, tasksCreated: 0, skillsCreated: 0, errors: 0, skippedSessions: 0, totalSessions: 0 };
2026
2132
 
2027
2133
  const send = (event: string, data: unknown) => {
2028
2134
  this.broadcastPPSSE(event, data);
@@ -2039,9 +2145,12 @@ export class ViewerServer {
2039
2145
  } else {
2040
2146
  this.broadcastPPSSE("done", { ...this.ppState });
2041
2147
  }
2042
- for (const c of this.ppSSEClients) { try { c.end(); } catch { /* */ } }
2043
- this.ppSSEClients = [];
2044
2148
  this.ppAbort = false;
2149
+ const ppClientsToClose = [...this.ppSSEClients];
2150
+ this.ppSSEClients = [];
2151
+ setTimeout(() => {
2152
+ for (const c of ppClientsToClose) { try { c.end(); } catch { /* */ } }
2153
+ }, 500);
2045
2154
  });
2046
2155
  });
2047
2156
  }
@@ -2073,7 +2182,13 @@ export class ViewerServer {
2073
2182
  }
2074
2183
 
2075
2184
  private handlePostprocessStatus(res: http.ServerResponse): void {
2076
- this.jsonResponse(res, this.ppState);
2185
+ let existingTasks = 0;
2186
+ let existingSkills = 0;
2187
+ try {
2188
+ existingTasks = (this.store as any).db.prepare("SELECT COUNT(*) as c FROM tasks").get()?.c ?? 0;
2189
+ existingSkills = this.store.countSkills("active");
2190
+ } catch { /* */ }
2191
+ this.jsonResponse(res, { ...this.ppState, existingTasks, existingSkills });
2077
2192
  }
2078
2193
 
2079
2194
  private broadcastPPSSE(event: string, data: unknown): void {
@@ -2123,12 +2238,18 @@ export class ViewerServer {
2123
2238
  }
2124
2239
 
2125
2240
  this.ppState.total = pendingItems.length;
2241
+ this.ppState.skippedSessions = skippedCount;
2242
+ this.ppState.totalSessions = importSessions.length;
2243
+ const existingTaskCount = (this.store as any).db.prepare("SELECT COUNT(*) as c FROM tasks WHERE session_key IN (" + importSessions.map(() => "?").join(",") + ")").get(...importSessions)?.c ?? 0;
2244
+ const existingSkillCount = this.store.countSkills("active");
2126
2245
  send("info", {
2127
2246
  totalSessions: importSessions.length,
2128
2247
  alreadyProcessed: skippedCount,
2129
2248
  pending: pendingItems.length,
2130
2249
  agents: Array.from(agentGroups.keys()),
2131
2250
  concurrency,
2251
+ existingTasks: existingTaskCount,
2252
+ existingSkills: existingSkillCount,
2132
2253
  });
2133
2254
  send("progress", { processed: 0, total: pendingItems.length });
2134
2255