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

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 (86) hide show
  1. package/dist/capture/index.js +52 -8
  2. package/dist/capture/index.js.map +1 -1
  3. package/dist/ingest/chunker.d.ts +3 -4
  4. package/dist/ingest/chunker.d.ts.map +1 -1
  5. package/dist/ingest/chunker.js +19 -24
  6. package/dist/ingest/chunker.js.map +1 -1
  7. package/dist/ingest/providers/anthropic.d.ts +3 -1
  8. package/dist/ingest/providers/anthropic.d.ts.map +1 -1
  9. package/dist/ingest/providers/anthropic.js +79 -39
  10. package/dist/ingest/providers/anthropic.js.map +1 -1
  11. package/dist/ingest/providers/bedrock.d.ts +3 -1
  12. package/dist/ingest/providers/bedrock.d.ts.map +1 -1
  13. package/dist/ingest/providers/bedrock.js +79 -39
  14. package/dist/ingest/providers/bedrock.js.map +1 -1
  15. package/dist/ingest/providers/gemini.d.ts +3 -1
  16. package/dist/ingest/providers/gemini.d.ts.map +1 -1
  17. package/dist/ingest/providers/gemini.js +77 -39
  18. package/dist/ingest/providers/gemini.js.map +1 -1
  19. package/dist/ingest/providers/index.d.ts +3 -1
  20. package/dist/ingest/providers/index.d.ts.map +1 -1
  21. package/dist/ingest/providers/index.js +70 -30
  22. package/dist/ingest/providers/index.js.map +1 -1
  23. package/dist/ingest/providers/openai.d.ts +3 -1
  24. package/dist/ingest/providers/openai.d.ts.map +1 -1
  25. package/dist/ingest/providers/openai.js +80 -39
  26. package/dist/ingest/providers/openai.js.map +1 -1
  27. package/dist/ingest/task-processor.d.ts +1 -0
  28. package/dist/ingest/task-processor.d.ts.map +1 -1
  29. package/dist/ingest/task-processor.js +33 -9
  30. package/dist/ingest/task-processor.js.map +1 -1
  31. package/dist/ingest/worker.d.ts.map +1 -1
  32. package/dist/ingest/worker.js +29 -13
  33. package/dist/ingest/worker.js.map +1 -1
  34. package/dist/recall/engine.d.ts.map +1 -1
  35. package/dist/recall/engine.js +19 -14
  36. package/dist/recall/engine.js.map +1 -1
  37. package/dist/skill/bundled-memory-guide.d.ts +1 -5
  38. package/dist/skill/bundled-memory-guide.d.ts.map +1 -1
  39. package/dist/skill/bundled-memory-guide.js +38 -97
  40. package/dist/skill/bundled-memory-guide.js.map +1 -1
  41. package/dist/skill/evaluator.js +1 -1
  42. package/dist/storage/sqlite.d.ts +1 -2
  43. package/dist/storage/sqlite.d.ts.map +1 -1
  44. package/dist/storage/sqlite.js +90 -17
  45. package/dist/storage/sqlite.js.map +1 -1
  46. package/dist/tools/memory-get.d.ts.map +1 -1
  47. package/dist/tools/memory-get.js +1 -3
  48. package/dist/tools/memory-get.js.map +1 -1
  49. package/dist/types.d.ts +2 -2
  50. package/dist/types.d.ts.map +1 -1
  51. package/dist/types.js +1 -1
  52. package/dist/types.js.map +1 -1
  53. package/dist/update-check.d.ts +21 -0
  54. package/dist/update-check.d.ts.map +1 -0
  55. package/dist/update-check.js +111 -0
  56. package/dist/update-check.js.map +1 -0
  57. package/dist/viewer/html.d.ts.map +1 -1
  58. package/dist/viewer/html.js +444 -182
  59. package/dist/viewer/html.js.map +1 -1
  60. package/dist/viewer/server.d.ts +1 -1
  61. package/dist/viewer/server.d.ts.map +1 -1
  62. package/dist/viewer/server.js +142 -78
  63. package/dist/viewer/server.js.map +1 -1
  64. package/index.ts +206 -198
  65. package/openclaw.plugin.json +3 -0
  66. package/package.json +5 -1
  67. package/scripts/postinstall.cjs +69 -2
  68. package/skill/memos-memory-guide/SKILL.md +73 -36
  69. package/src/capture/index.ts +52 -8
  70. package/src/ingest/chunker.ts +22 -30
  71. package/src/ingest/providers/anthropic.ts +89 -41
  72. package/src/ingest/providers/bedrock.ts +90 -41
  73. package/src/ingest/providers/gemini.ts +89 -41
  74. package/src/ingest/providers/index.ts +81 -35
  75. package/src/ingest/providers/openai.ts +90 -41
  76. package/src/ingest/task-processor.ts +29 -8
  77. package/src/ingest/worker.ts +31 -13
  78. package/src/recall/engine.ts +20 -13
  79. package/src/skill/bundled-memory-guide.ts +5 -96
  80. package/src/skill/evaluator.ts +1 -1
  81. package/src/storage/sqlite.ts +93 -21
  82. package/src/tools/memory-get.ts +1 -4
  83. package/src/types.ts +2 -9
  84. package/src/update-check.ts +96 -0
  85. package/src/viewer/html.ts +444 -182
  86. package/src/viewer/server.ts +101 -66
@@ -1,6 +1,6 @@
1
1
  import http from "node:http";
2
2
  import crypto from "node:crypto";
3
- import { execSync } from "node:child_process";
3
+ import { execSync, exec } from "node:child_process";
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import readline from "node:readline";
@@ -75,8 +75,8 @@ export class ViewerServer {
75
75
 
76
76
  private ppRunning = false;
77
77
  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 };
78
+ private ppState: { running: boolean; done: boolean; stopped: boolean; processed: number; total: number; tasksCreated: number; skillsCreated: number; errors: number; skippedSessions: number; totalSessions: number } =
79
+ { running: false, done: false, stopped: false, processed: 0, total: 0, tasksCreated: 0, skillsCreated: 0, errors: 0, skippedSessions: 0, totalSessions: 0 };
80
80
  private ppSSEClients: http.ServerResponse[] = [];
81
81
 
82
82
  constructor(opts: ViewerServerOptions) {
@@ -235,7 +235,6 @@ export class ViewerServer {
235
235
  else if (p.startsWith("/api/skill/") && req.method === "DELETE") this.handleSkillDelete(res, p);
236
236
  else if (p.startsWith("/api/skill/") && req.method === "PUT") this.handleSkillUpdate(req, res, p);
237
237
  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
238
  else if (p.startsWith("/api/memory/") && req.method === "GET") this.serveMemoryDetail(res, p);
240
239
  else if (p.startsWith("/api/memory/") && req.method === "PUT") this.handleUpdate(req, res, p);
241
240
  else if (p.startsWith("/api/memory/") && req.method === "DELETE") this.handleDelete(res, p);
@@ -249,6 +248,7 @@ export class ViewerServer {
249
248
  else if (p === "/api/model-health" && req.method === "GET") this.serveModelHealth(res);
250
249
  else if (p === "/api/fallback-model" && req.method === "GET") this.serveFallbackModel(res);
251
250
  else if (p === "/api/update-check" && req.method === "GET") this.handleUpdateCheck(res);
251
+ else if (p === "/api/update-install" && req.method === "POST") this.handleUpdateInstall(req, res);
252
252
  else if (p === "/api/auth/logout" && req.method === "POST") this.handleLogout(req, res);
253
253
  else if (p === "/api/cleanup-polluted" && req.method === "POST") this.handleCleanupPolluted(res);
254
254
  else if (p === "/api/migrate/scan" && req.method === "GET") this.handleMigrateScan(res);
@@ -382,7 +382,6 @@ export class ViewerServer {
382
382
  const offset = (page - 1) * limit;
383
383
  const session = url.searchParams.get("session") ?? undefined;
384
384
  const role = url.searchParams.get("role") ?? undefined;
385
- const kind = url.searchParams.get("kind") ?? undefined;
386
385
  const dateFrom = url.searchParams.get("dateFrom") ?? undefined;
387
386
  const dateTo = url.searchParams.get("dateTo") ?? undefined;
388
387
  const owner = url.searchParams.get("owner") ?? undefined;
@@ -393,7 +392,6 @@ export class ViewerServer {
393
392
  const params: any[] = [];
394
393
  if (session) { conditions.push("session_key = ?"); params.push(session); }
395
394
  if (role) { conditions.push("role = ?"); params.push(role); }
396
- if (kind) { conditions.push("kind = ?"); params.push(kind); }
397
395
  if (owner) { conditions.push("owner = ?"); params.push(owner); }
398
396
  if (dateFrom) { conditions.push("created_at >= ?"); params.push(new Date(dateFrom).getTime()); }
399
397
  if (dateTo) { conditions.push("created_at <= ?"); params.push(new Date(dateTo).getTime()); }
@@ -401,9 +399,14 @@ export class ViewerServer {
401
399
  const where = conditions.length > 0 ? " WHERE " + conditions.join(" AND ") : "";
402
400
  const totalRow = db.prepare("SELECT COUNT(*) as count FROM chunks" + where).get(...params) as any;
403
401
  const rawMemories = db.prepare("SELECT * FROM chunks" + where + ` ORDER BY created_at ${sortBy} LIMIT ? OFFSET ?`).all(...params, limit, offset);
402
+ const findMergeSources = db.prepare("SELECT id, summary, role FROM chunks WHERE dedup_target = ? AND (dedup_status = 'merged' OR dedup_status = 'duplicate')");
404
403
  const memories = rawMemories.map((m: any) => {
405
404
  if (m.role === "user" && m.content) {
406
- return { ...m, content: stripInboundMetadata(m.content) };
405
+ m = { ...m, content: stripInboundMetadata(m.content) };
406
+ }
407
+ if (m.merge_count > 0) {
408
+ const sources = findMergeSources.all(m.id) as Array<{ id: string; summary: string; role: string }>;
409
+ m.merge_sources = sources;
407
410
  }
408
411
  return m;
409
412
  });
@@ -441,7 +444,7 @@ export class ViewerServer {
441
444
  id: t.id,
442
445
  sessionKey: t.sessionKey,
443
446
  title: t.title,
444
- summary: t.summary ? (t.summary.length > 300 ? t.summary.slice(0, 297) + "..." : t.summary) : "",
447
+ summary: t.summary ?? "",
445
448
  status: t.status,
446
449
  startedAt: t.startedAt,
447
450
  endedAt: t.endedAt,
@@ -464,8 +467,7 @@ export class ViewerServer {
464
467
 
465
468
  const chunks = this.store.getChunksByTask(taskId);
466
469
  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) + "...";
470
+ const text = c.role === "user" ? stripInboundMetadata(c.content) : c.content;
469
471
  return { id: c.id, role: c.role, content: text, summary: c.summary, createdAt: c.createdAt };
470
472
  });
471
473
 
@@ -502,7 +504,7 @@ export class ViewerServer {
502
504
  const emptyStats = {
503
505
  totalMemories: 0, totalSessions: 0, totalEmbeddings: 0, totalSkills: 0,
504
506
  embeddingProvider: this.embedder?.provider ?? "none",
505
- roleBreakdown: {}, kindBreakdown: {}, dedupBreakdown: {},
507
+ dedupBreakdown: {},
506
508
  timeRange: { earliest: null, latest: null },
507
509
  sessions: [],
508
510
  };
@@ -516,7 +518,6 @@ export class ViewerServer {
516
518
  const db = (this.store as any).db;
517
519
  const total = db.prepare("SELECT COUNT(*) as count FROM chunks").get() as any;
518
520
  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
521
  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
522
  const MIN_VALID_TS = 1704067200000; // 2024-01-01
522
523
  if (timeRange.earliest != null && timeRange.earliest < MIN_VALID_TS) {
@@ -528,7 +529,6 @@ export class ViewerServer {
528
529
  }
529
530
  let embCount = 0;
530
531
  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
532
  const sessionList = db.prepare(
533
533
  "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
534
  ).all() as any[];
@@ -552,8 +552,6 @@ export class ViewerServer {
552
552
  totalMemories: total.count, totalSessions: sessions.count, totalEmbeddings: embCount,
553
553
  totalSkills: skillCount,
554
554
  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
555
  dedupBreakdown,
558
556
  timeRange: { earliest: timeRange.earliest, latest: timeRange.latest },
559
557
  sessions: sessionList,
@@ -570,7 +568,6 @@ export class ViewerServer {
570
568
  if (!q.trim()) { this.jsonResponse(res, { results: [], query: q }); return; }
571
569
 
572
570
  const role = url.searchParams.get("role") ?? undefined;
573
- const kind = url.searchParams.get("kind") ?? undefined;
574
571
  const session = url.searchParams.get("session") ?? undefined;
575
572
  const owner = url.searchParams.get("owner") ?? undefined;
576
573
  const dateFrom = url.searchParams.get("dateFrom") ?? undefined;
@@ -578,7 +575,6 @@ export class ViewerServer {
578
575
 
579
576
  const passesFilter = (r: any): boolean => {
580
577
  if (role && r.role !== role) return false;
581
- if (kind && r.kind !== kind) return false;
582
578
  if (session && r.session_key !== session) return false;
583
579
  if (owner && r.owner !== owner) return false;
584
580
  if (dateFrom && r.created_at < new Date(dateFrom).getTime()) return false;
@@ -920,35 +916,6 @@ export class ViewerServer {
920
916
 
921
917
  // ─── CRUD ───
922
918
 
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
919
  private serveMemoryDetail(res: http.ServerResponse, urlPath: string): void {
953
920
  const chunkId = urlPath.replace("/api/memory/", "");
954
921
  const chunk = this.store.getChunk(chunkId);
@@ -973,7 +940,7 @@ export class ViewerServer {
973
940
  res.end(JSON.stringify({ error: "content must be a non-empty string" }));
974
941
  return;
975
942
  }
976
- const ok = this.store.updateChunk(chunkId, { summary: data.summary, content: data.content, role: data.role, kind: data.kind, owner: data.owner });
943
+ const ok = this.store.updateChunk(chunkId, { summary: data.summary, content: data.content, role: data.role, owner: data.owner });
977
944
  if (ok) this.jsonResponse(res, { ok: true, message: "Memory updated" });
978
945
  else { res.writeHead(404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Not found" })); }
979
946
  } catch (err) {
@@ -1184,20 +1151,20 @@ export class ViewerServer {
1184
1151
  this.jsonResponse(res, { updateAvailable: false, current });
1185
1152
  return;
1186
1153
  }
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 });
1154
+ const { computeUpdateCheck } = await import("../update-check");
1155
+ const result = await computeUpdateCheck(name, current, fetch, 6_000);
1156
+ if (!result) {
1157
+ this.jsonResponse(res, { updateAvailable: false, current, packageName: name });
1192
1158
  return;
1193
1159
  }
1194
- const data = await npmResp.json() as { version?: string };
1195
- const latest = data.version ?? current;
1196
1160
  this.jsonResponse(res, {
1197
- updateAvailable: latest !== current,
1198
- current,
1199
- latest,
1200
- packageName: name,
1161
+ updateAvailable: result.updateAvailable,
1162
+ current: result.current,
1163
+ latest: result.latest,
1164
+ packageName: result.packageName,
1165
+ channel: result.channel,
1166
+ installCommand: result.installCommand,
1167
+ stableChannel: result.stableChannel,
1201
1168
  });
1202
1169
  } catch (e) {
1203
1170
  this.log.warn(`handleUpdateCheck error: ${e}`);
@@ -1205,6 +1172,47 @@ export class ViewerServer {
1205
1172
  }
1206
1173
  }
1207
1174
 
1175
+ private handleUpdateInstall(req: http.IncomingMessage, res: http.ServerResponse): void {
1176
+ let body = "";
1177
+ req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
1178
+ req.on("end", () => {
1179
+ try {
1180
+ const { packageSpec } = JSON.parse(body);
1181
+ if (!packageSpec || typeof packageSpec !== "string") {
1182
+ res.writeHead(400, { "Content-Type": "application/json" });
1183
+ res.end(JSON.stringify({ ok: false, error: "Missing packageSpec" }));
1184
+ return;
1185
+ }
1186
+ const allowed = /^@[\w-]+\/[\w.-]+(@[\w.-]+)?$/;
1187
+ if (!allowed.test(packageSpec)) {
1188
+ res.writeHead(400, { "Content-Type": "application/json" });
1189
+ res.end(JSON.stringify({ ok: false, error: "Invalid package spec" }));
1190
+ return;
1191
+ }
1192
+ this.log.info(`update-install: installing ${packageSpec}...`);
1193
+ exec(`npx openclaw plugins install ${packageSpec}`, { timeout: 120_000 }, (err, stdout, stderr) => {
1194
+ if (err) {
1195
+ this.log.warn(`update-install failed: ${err.message}\n${stderr}`);
1196
+ this.jsonResponse(res, { ok: false, error: stderr || err.message });
1197
+ return;
1198
+ }
1199
+ this.log.info(`update-install success: ${stdout}`);
1200
+ this.jsonResponse(res, { ok: true, output: stdout });
1201
+ this.log.info(`update-install: restarting gateway...`);
1202
+ setTimeout(() => {
1203
+ exec("npx openclaw gateway restart", { timeout: 30_000 }, (restartErr) => {
1204
+ if (restartErr) this.log.warn(`gateway restart failed: ${restartErr.message}`);
1205
+ else this.log.info("gateway restart initiated");
1206
+ });
1207
+ }, 1000);
1208
+ });
1209
+ } catch (e) {
1210
+ res.writeHead(400, { "Content-Type": "application/json" });
1211
+ res.end(JSON.stringify({ ok: false, error: String(e) }));
1212
+ }
1213
+ });
1214
+ }
1215
+
1208
1216
  private async testEmbeddingModel(provider: string, model: string, endpoint: string, apiKey: string): Promise<number | undefined> {
1209
1217
  if (provider === "local") {
1210
1218
  return 384;
@@ -1438,10 +1446,18 @@ export class ViewerServer {
1438
1446
  }
1439
1447
 
1440
1448
  let importedSessions: string[] = [];
1449
+ let importedChunkCount = 0;
1441
1450
  try {
1442
1451
  if (this.store) {
1443
1452
  importedSessions = this.store.getDistinctSessionKeys()
1444
1453
  .filter((sk: string) => sk.startsWith("openclaw-import-") || sk.startsWith("openclaw-session-"));
1454
+ if (importedSessions.length > 0) {
1455
+ const placeholders = importedSessions.map(() => "?").join(",");
1456
+ const row = (this.store as any).db.prepare(
1457
+ `SELECT COUNT(*) as cnt FROM chunks WHERE session_key IN (${placeholders})`
1458
+ ).get(...importedSessions) as { cnt: number };
1459
+ importedChunkCount = row?.cnt ?? 0;
1460
+ }
1445
1461
  }
1446
1462
  } catch (storeErr) {
1447
1463
  this.log.warn(`migrate/scan: store query failed: ${storeErr}`);
@@ -1456,6 +1472,7 @@ export class ViewerServer {
1456
1472
  hasSummarizer,
1457
1473
  hasImportedData: importedSessions.length > 0,
1458
1474
  importedSessionCount: importedSessions.length,
1475
+ importedChunkCount,
1459
1476
  });
1460
1477
  } catch (e) {
1461
1478
  this.log.warn(`migrate/scan error: ${e}`);
@@ -1587,11 +1604,14 @@ export class ViewerServer {
1587
1604
  } else {
1588
1605
  this.broadcastSSE("done", { ok: true });
1589
1606
  }
1590
- for (const c of this.migrationSSEClients) {
1591
- try { c.end(); } catch { /* ignore */ }
1592
- }
1593
- this.migrationSSEClients = [];
1594
1607
  this.migrationAbort = false;
1608
+ const clientsToClose = [...this.migrationSSEClients];
1609
+ this.migrationSSEClients = [];
1610
+ setTimeout(() => {
1611
+ for (const c of clientsToClose) {
1612
+ try { c.end(); } catch { /* ignore */ }
1613
+ }
1614
+ }, 500);
1595
1615
  });
1596
1616
  });
1597
1617
  }
@@ -2022,7 +2042,7 @@ export class ViewerServer {
2022
2042
  res.on("close", () => { this.ppSSEClients = this.ppSSEClients.filter(c => c !== res); });
2023
2043
 
2024
2044
  this.ppAbort = false;
2025
- this.ppState = { running: true, done: false, stopped: false, processed: 0, total: 0, tasksCreated: 0, skillsCreated: 0, errors: 0 };
2045
+ this.ppState = { running: true, done: false, stopped: false, processed: 0, total: 0, tasksCreated: 0, skillsCreated: 0, errors: 0, skippedSessions: 0, totalSessions: 0 };
2026
2046
 
2027
2047
  const send = (event: string, data: unknown) => {
2028
2048
  this.broadcastPPSSE(event, data);
@@ -2039,9 +2059,12 @@ export class ViewerServer {
2039
2059
  } else {
2040
2060
  this.broadcastPPSSE("done", { ...this.ppState });
2041
2061
  }
2042
- for (const c of this.ppSSEClients) { try { c.end(); } catch { /* */ } }
2043
- this.ppSSEClients = [];
2044
2062
  this.ppAbort = false;
2063
+ const ppClientsToClose = [...this.ppSSEClients];
2064
+ this.ppSSEClients = [];
2065
+ setTimeout(() => {
2066
+ for (const c of ppClientsToClose) { try { c.end(); } catch { /* */ } }
2067
+ }, 500);
2045
2068
  });
2046
2069
  });
2047
2070
  }
@@ -2073,7 +2096,13 @@ export class ViewerServer {
2073
2096
  }
2074
2097
 
2075
2098
  private handlePostprocessStatus(res: http.ServerResponse): void {
2076
- this.jsonResponse(res, this.ppState);
2099
+ let existingTasks = 0;
2100
+ let existingSkills = 0;
2101
+ try {
2102
+ existingTasks = (this.store as any).db.prepare("SELECT COUNT(*) as c FROM tasks").get()?.c ?? 0;
2103
+ existingSkills = this.store.countSkills("active");
2104
+ } catch { /* */ }
2105
+ this.jsonResponse(res, { ...this.ppState, existingTasks, existingSkills });
2077
2106
  }
2078
2107
 
2079
2108
  private broadcastPPSSE(event: string, data: unknown): void {
@@ -2123,12 +2152,18 @@ export class ViewerServer {
2123
2152
  }
2124
2153
 
2125
2154
  this.ppState.total = pendingItems.length;
2155
+ this.ppState.skippedSessions = skippedCount;
2156
+ this.ppState.totalSessions = importSessions.length;
2157
+ 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;
2158
+ const existingSkillCount = this.store.countSkills("active");
2126
2159
  send("info", {
2127
2160
  totalSessions: importSessions.length,
2128
2161
  alreadyProcessed: skippedCount,
2129
2162
  pending: pendingItems.length,
2130
2163
  agents: Array.from(agentGroups.keys()),
2131
2164
  concurrency,
2165
+ existingTasks: existingTaskCount,
2166
+ existingSkills: existingSkillCount,
2132
2167
  });
2133
2168
  send("progress", { processed: 0, total: pendingItems.length });
2134
2169