@memtensor/memos-local-openclaw-plugin 1.0.4 → 1.0.6-beta.1

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 (61) hide show
  1. package/dist/capture/index.d.ts.map +1 -1
  2. package/dist/capture/index.js +24 -0
  3. package/dist/capture/index.js.map +1 -1
  4. package/dist/client/connector.d.ts.map +1 -1
  5. package/dist/client/connector.js +23 -1
  6. package/dist/client/connector.js.map +1 -1
  7. package/dist/client/hub.d.ts.map +1 -1
  8. package/dist/client/hub.js +4 -0
  9. package/dist/client/hub.js.map +1 -1
  10. package/dist/hub/server.d.ts +1 -1
  11. package/dist/hub/server.d.ts.map +1 -1
  12. package/dist/hub/server.js +39 -31
  13. package/dist/hub/server.js.map +1 -1
  14. package/dist/ingest/providers/index.d.ts.map +1 -1
  15. package/dist/ingest/providers/index.js +16 -86
  16. package/dist/ingest/providers/index.js.map +1 -1
  17. package/dist/ingest/providers/openai.d.ts +3 -0
  18. package/dist/ingest/providers/openai.d.ts.map +1 -1
  19. package/dist/ingest/providers/openai.js +34 -19
  20. package/dist/ingest/providers/openai.js.map +1 -1
  21. package/dist/recall/engine.d.ts.map +1 -1
  22. package/dist/recall/engine.js +28 -19
  23. package/dist/recall/engine.js.map +1 -1
  24. package/dist/storage/sqlite.d.ts +30 -7
  25. package/dist/storage/sqlite.d.ts.map +1 -1
  26. package/dist/storage/sqlite.js +139 -60
  27. package/dist/storage/sqlite.js.map +1 -1
  28. package/dist/telemetry.d.ts +4 -1
  29. package/dist/telemetry.d.ts.map +1 -1
  30. package/dist/telemetry.js +26 -18
  31. package/dist/telemetry.js.map +1 -1
  32. package/dist/tools/memory-get.d.ts.map +1 -1
  33. package/dist/tools/memory-get.js +4 -1
  34. package/dist/tools/memory-get.js.map +1 -1
  35. package/dist/types.d.ts +1 -1
  36. package/dist/types.d.ts.map +1 -1
  37. package/dist/types.js.map +1 -1
  38. package/dist/viewer/server.d.ts +24 -0
  39. package/dist/viewer/server.d.ts.map +1 -1
  40. package/dist/viewer/server.js +332 -130
  41. package/dist/viewer/server.js.map +1 -1
  42. package/index.ts +66 -30
  43. package/package.json +1 -1
  44. package/scripts/postinstall.cjs +21 -5
  45. package/src/capture/index.ts +36 -0
  46. package/src/client/connector.ts +22 -1
  47. package/src/client/hub.ts +4 -0
  48. package/src/hub/server.ts +42 -26
  49. package/src/ingest/providers/index.ts +30 -93
  50. package/src/ingest/providers/openai.ts +32 -15
  51. package/src/recall/engine.ts +28 -19
  52. package/src/storage/sqlite.ts +156 -65
  53. package/src/telemetry.ts +25 -18
  54. package/src/tools/memory-get.ts +4 -1
  55. package/src/types.ts +2 -0
  56. package/src/viewer/server.ts +313 -125
  57. package/prebuilds/darwin-arm64/better_sqlite3.node +0 -0
  58. package/prebuilds/darwin-x64/better_sqlite3.node +0 -0
  59. package/prebuilds/linux-x64/better_sqlite3.node +0 -0
  60. package/prebuilds/win32-x64/better_sqlite3.node +0 -0
  61. package/telemetry.credentials.json +0 -5
@@ -1,7 +1,7 @@
1
1
  import http from "node:http";
2
2
  import os from "node:os";
3
3
  import crypto from "node:crypto";
4
- import { execSync, exec } from "node:child_process";
4
+ import { execSync, exec, execFile } from "node:child_process";
5
5
  import fs from "node:fs";
6
6
  import path from "node:path";
7
7
  import readline from "node:readline";
@@ -22,9 +22,89 @@ import type { Logger, Chunk, PluginContext, MemosLocalConfig } from "../types";
22
22
  import { viewerHTML } from "./html";
23
23
  import { v4 as uuid } from "uuid";
24
24
 
25
- function normalizeTimestamp(ts: number): number {
26
- if (ts < 1e12) return ts * 1000;
27
- return ts;
25
+ export interface MigrationStepFailureCounts {
26
+ summarization: number;
27
+ dedup: number;
28
+ embedding: number;
29
+ }
30
+
31
+ export interface MigrationStateSnapshot {
32
+ phase: string;
33
+ stored: number;
34
+ skipped: number;
35
+ merged: number;
36
+ errors: number;
37
+ processed: number;
38
+ total: number;
39
+ lastItem: any;
40
+ done: boolean;
41
+ stopped: boolean;
42
+ stepFailures: MigrationStepFailureCounts;
43
+ success: boolean;
44
+ }
45
+
46
+ function createInitialStepFailures(): MigrationStepFailureCounts {
47
+ return { summarization: 0, dedup: 0, embedding: 0 };
48
+ }
49
+
50
+ export function computeMigrationSuccess(state: Pick<MigrationStateSnapshot, "errors" | "stepFailures">): boolean {
51
+ const sf = state.stepFailures;
52
+ return state.errors === 0 && sf.summarization === 0 && sf.dedup === 0 && sf.embedding === 0;
53
+ }
54
+
55
+ export function createInitialMigrationState(): MigrationStateSnapshot {
56
+ const stepFailures = createInitialStepFailures();
57
+ return {
58
+ phase: "",
59
+ stored: 0,
60
+ skipped: 0,
61
+ merged: 0,
62
+ errors: 0,
63
+ processed: 0,
64
+ total: 0,
65
+ lastItem: null,
66
+ done: false,
67
+ stopped: false,
68
+ stepFailures,
69
+ success: computeMigrationSuccess({ errors: 0, stepFailures }),
70
+ };
71
+ }
72
+
73
+ export function applyMigrationItemToState(state: MigrationStateSnapshot, d: any): void {
74
+ if (d.status === "stored") state.stored++;
75
+ else if (d.status === "skipped" || d.status === "duplicate") state.skipped++;
76
+ else if (d.status === "merged") state.merged++;
77
+ else if (d.status === "error") state.errors++;
78
+
79
+ if (Array.isArray(d.stepFailures)) {
80
+ for (const step of d.stepFailures) {
81
+ if (step === "summarization") state.stepFailures.summarization++;
82
+ else if (step === "dedup") state.stepFailures.dedup++;
83
+ else if (step === "embedding") state.stepFailures.embedding++;
84
+ }
85
+ }
86
+
87
+ state.processed = d.index ?? state.processed + 1;
88
+ state.total = d.total ?? state.total;
89
+ state.lastItem = d;
90
+ state.success = computeMigrationSuccess(state);
91
+ }
92
+
93
+ /** Epoch ms for Chunk; OpenClaw SQLite may store Unix seconds or ms. */
94
+ function normalizeTimestamp(value: unknown): number {
95
+ if (value == null) return Date.now();
96
+ if (typeof value === "string") {
97
+ const parsed = Date.parse(value.trim());
98
+ if (Number.isFinite(parsed)) return parsed;
99
+ const n = Number(value);
100
+ if (Number.isFinite(n)) return normalizeTimestamp(n);
101
+ return Date.now();
102
+ }
103
+ if (typeof value === "number" && Number.isFinite(value)) {
104
+ if (value > 0 && value < 5_000_000_000) return Math.round(value * 1000);
105
+ return Math.round(value);
106
+ }
107
+ return Date.now();
28
108
  }
29
109
 
30
110
  export interface ViewerServerOptions {
@@ -67,18 +147,7 @@ export class ViewerServer {
67
147
  private resetToken: string;
68
148
  private migrationRunning = false;
69
149
  private migrationAbort = false;
70
- private migrationState: {
71
- phase: string;
72
- stored: number;
73
- skipped: number;
74
- merged: number;
75
- errors: number;
76
- processed: number;
77
- total: number;
78
- lastItem: any;
79
- done: boolean;
80
- stopped: boolean;
81
- } = { phase: "", stored: 0, skipped: 0, merged: 0, errors: 0, processed: 0, total: 0, lastItem: null, done: false, stopped: false };
150
+ private migrationState: MigrationStateSnapshot = createInitialMigrationState();
82
151
  private migrationSSEClients: http.ServerResponse[] = [];
83
152
 
84
153
  private ppRunning = false;
@@ -491,13 +560,12 @@ export class ViewerServer {
491
560
  if (chunkIds.length > 0) {
492
561
  try {
493
562
  const placeholders = chunkIds.map(() => "?").join(",");
494
- const sharedRows = db.prepare(`SELECT source_chunk_id, visibility, group_id FROM hub_memories WHERE source_chunk_id IN (${placeholders})`).all(...chunkIds) as Array<{ source_chunk_id: string; visibility: string; group_id: string | null }>;
495
- for (const r of sharedRows) sharingMap.set(r.source_chunk_id, r);
496
- const teamMetaRows = db.prepare(`SELECT chunk_id, visibility, group_id FROM team_shared_chunks WHERE chunk_id IN (${placeholders})`).all(...chunkIds) as Array<{ chunk_id: string; visibility: string; group_id: string | null }>;
497
- for (const r of teamMetaRows) {
498
- if (!sharingMap.has(r.chunk_id)) {
499
- sharingMap.set(r.chunk_id, { visibility: r.visibility, group_id: r.group_id });
500
- }
563
+ if (this.sharingRole === "hub") {
564
+ const sharedRows = db.prepare(`SELECT source_chunk_id, visibility, group_id FROM hub_memories WHERE source_chunk_id IN (${placeholders})`).all(...chunkIds) as Array<{ source_chunk_id: string; visibility: string; group_id: string | null }>;
565
+ for (const r of sharedRows) sharingMap.set(r.source_chunk_id, r);
566
+ } else {
567
+ const teamMetaRows = db.prepare(`SELECT chunk_id, visibility, group_id FROM team_shared_chunks WHERE chunk_id IN (${placeholders})`).all(...chunkIds) as Array<{ chunk_id: string; visibility: string; group_id: string | null }>;
568
+ for (const r of teamMetaRows) sharingMap.set(r.chunk_id, { visibility: r.visibility, group_id: r.group_id });
501
569
  }
502
570
  const localRows = db.prepare(`SELECT chunk_id, original_owner, shared_at FROM local_shared_memories WHERE chunk_id IN (${placeholders})`).all(...chunkIds) as Array<{ chunk_id: string; original_owner: string; shared_at: number }>;
503
571
  for (const r of localRows) localShareMap.set(r.chunk_id, r);
@@ -564,7 +632,7 @@ export class ViewerServer {
564
632
  const db = (this.store as any).db;
565
633
  const items = tasks.map((t) => {
566
634
  const meta = db.prepare("SELECT skill_status, owner FROM tasks WHERE id = ?").get(t.id) as { skill_status: string | null; owner: string | null } | undefined;
567
- const sharedTask = db.prepare("SELECT visibility FROM hub_tasks WHERE source_task_id = ? ORDER BY updated_at DESC LIMIT 1").get(t.id) as { visibility: string } | undefined;
635
+ const hubTask = this.getHubTaskForLocal(t.id);
568
636
  return {
569
637
  id: t.id,
570
638
  sessionKey: t.sessionKey,
@@ -576,7 +644,7 @@ export class ViewerServer {
576
644
  chunkCount: this.store.countChunksByTask(t.id),
577
645
  skillStatus: meta?.skill_status ?? null,
578
646
  owner: meta?.owner ?? "agent:main",
579
- sharingVisibility: sharedTask?.visibility ?? null,
647
+ sharingVisibility: hubTask?.visibility ?? null,
580
648
  };
581
649
  });
582
650
 
@@ -611,7 +679,7 @@ export class ViewerServer {
611
679
  const db = (this.store as any).db;
612
680
  const meta = db.prepare("SELECT skill_status, skill_reason FROM tasks WHERE id = ?").get(taskId) as
613
681
  { skill_status: string | null; skill_reason: string | null } | undefined;
614
- const sharedTask = db.prepare("SELECT visibility, group_id FROM hub_tasks WHERE source_task_id = ? ORDER BY updated_at DESC LIMIT 1").get(taskId) as { visibility: string | null; group_id: string | null } | undefined;
682
+ const hubTask = this.getHubTaskForLocal(taskId);
615
683
 
616
684
  this.jsonResponse(res, {
617
685
  id: task.id,
@@ -626,9 +694,9 @@ export class ViewerServer {
626
694
  skillStatus: meta?.skill_status ?? null,
627
695
  skillReason: meta?.skill_reason ?? null,
628
696
  skillLinks,
629
- sharingVisibility: sharedTask?.visibility ?? null,
630
- sharingGroupId: sharedTask?.group_id ?? null,
631
- hubTaskId: sharedTask ? true : false,
697
+ sharingVisibility: hubTask?.visibility ?? null,
698
+ sharingGroupId: hubTask?.group_id ?? null,
699
+ hubTaskId: hubTask ? true : false,
632
700
  });
633
701
  }
634
702
 
@@ -818,10 +886,9 @@ export class ViewerServer {
818
886
  if (visibility) {
819
887
  skills = skills.filter(s => s.visibility === visibility);
820
888
  }
821
- const db = (this.store as any).db;
822
889
  const enriched = skills.map(s => {
823
- const hub = db.prepare("SELECT visibility FROM hub_skills WHERE source_skill_id = ? ORDER BY updated_at DESC LIMIT 1").get(s.id) as { visibility: string } | undefined;
824
- return { ...s, sharingVisibility: hub?.visibility ?? null };
890
+ const hubSkill = this.getHubSkillForLocal(s.id);
891
+ return { ...s, sharingVisibility: hubSkill?.visibility ?? null };
825
892
  });
826
893
  this.jsonResponse(res, { skills: enriched });
827
894
  }
@@ -839,11 +906,10 @@ export class ViewerServer {
839
906
  const relatedTasks = this.store.getTasksBySkill(skillId);
840
907
  const files = fs.existsSync(skill.dirPath) ? this.walkDir(skill.dirPath, skill.dirPath) : [];
841
908
 
842
- const db = (this.store as any).db;
843
- const sharedSkill = db.prepare("SELECT visibility, group_id FROM hub_skills WHERE source_skill_id = ? ORDER BY updated_at DESC LIMIT 1").get(skillId) as { visibility: string | null; group_id: string | null } | undefined;
909
+ const hubSkill = this.getHubSkillForLocal(skillId);
844
910
 
845
911
  this.jsonResponse(res, {
846
- skill: { ...skill, sharingVisibility: sharedSkill?.visibility ?? null, sharingGroupId: sharedSkill?.group_id ?? null },
912
+ skill: { ...skill, sharingVisibility: hubSkill?.visibility ?? null, sharingGroupId: hubSkill?.group_id ?? null },
847
913
  versions: versions.map(v => ({
848
914
  id: v.id,
849
915
  version: v.version,
@@ -982,7 +1048,7 @@ export class ViewerServer {
982
1048
  method: "POST",
983
1049
  body: JSON.stringify({ visibility: "public", groupId: null, metadata: bundle.metadata, bundle: bundle.bundle }),
984
1050
  }) as any;
985
- if (hubClient.userId) {
1051
+ if (this.sharingRole === "hub" && hubClient.userId) {
986
1052
  const existing = this.store.getHubSkillBySource(hubClient.userId, skillId);
987
1053
  this.store.upsertHubSkill({
988
1054
  id: response?.skillId ?? existing?.id ?? crypto.randomUUID(),
@@ -992,6 +1058,14 @@ export class ViewerServer {
992
1058
  bundle: JSON.stringify(bundle.bundle), qualityScore: skill.qualityScore,
993
1059
  createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
994
1060
  });
1061
+ } else {
1062
+ const conn = this.store.getClientHubConnection();
1063
+ this.store.upsertTeamSharedSkill(skillId, {
1064
+ hubSkillId: String(response?.skillId ?? ""),
1065
+ visibility: "public",
1066
+ groupId: null,
1067
+ hubInstanceId: conn?.hubInstanceId ?? "",
1068
+ });
995
1069
  }
996
1070
  hubSynced = true;
997
1071
  this.log.info(`Skill "${skill.name}" published to Hub`);
@@ -1000,7 +1074,8 @@ export class ViewerServer {
1000
1074
  method: "POST",
1001
1075
  body: JSON.stringify({ sourceSkillId: skillId }),
1002
1076
  });
1003
- if (hubClient.userId) this.store.deleteHubSkillBySource(hubClient.userId, skillId);
1077
+ if (this.sharingRole === "hub" && hubClient.userId) this.store.deleteHubSkillBySource(hubClient.userId, skillId);
1078
+ else this.store.deleteTeamSharedSkill(skillId);
1004
1079
  hubSynced = true;
1005
1080
  this.log.info(`Skill "${skill.name}" unpublished from Hub`);
1006
1081
  }
@@ -1271,7 +1346,8 @@ export class ViewerServer {
1271
1346
  createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
1272
1347
  });
1273
1348
  } else if (hubClient.userId) {
1274
- this.store.upsertTeamSharedChunk(chunkId, { hubMemoryId: memoryId, visibility: "public", groupId: null });
1349
+ const conn = this.store.getClientHubConnection();
1350
+ this.store.upsertTeamSharedChunk(chunkId, { hubMemoryId: memoryId, visibility: "public", groupId: null, hubInstanceId: conn?.hubInstanceId ?? "" });
1275
1351
  }
1276
1352
  hubSynced = true;
1277
1353
  } else {
@@ -1284,7 +1360,7 @@ export class ViewerServer {
1284
1360
  await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/unshare", {
1285
1361
  method: "POST", body: JSON.stringify({ sourceChunkId: chunkId }),
1286
1362
  });
1287
- if (hubClient.userId) this.store.deleteHubMemoryBySource(hubClient.userId, chunkId);
1363
+ if (this.sharingRole === "hub" && hubClient.userId) this.store.deleteHubMemoryBySource(hubClient.userId, chunkId);
1288
1364
  this.store.deleteTeamSharedChunk(chunkId);
1289
1365
  hubSynced = true;
1290
1366
  } catch (err) { this.log.warn(`Failed to unshare memory from team: ${err}`); }
@@ -1297,7 +1373,7 @@ export class ViewerServer {
1297
1373
  await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/unshare", {
1298
1374
  method: "POST", body: JSON.stringify({ sourceChunkId: chunkId }),
1299
1375
  });
1300
- if (hubClient.userId) this.store.deleteHubMemoryBySource(hubClient.userId, chunkId);
1376
+ if (this.sharingRole === "hub" && hubClient.userId) this.store.deleteHubMemoryBySource(hubClient.userId, chunkId);
1301
1377
  this.store.deleteTeamSharedChunk(chunkId);
1302
1378
  hubSynced = true;
1303
1379
  } catch (err) { this.log.warn(`Failed to unshare memory from team: ${err}`); }
@@ -1351,21 +1427,24 @@ export class ViewerServer {
1351
1427
  chunks: chunks.map((c) => ({ id: c.id, hubTaskId: refreshedTask.id, sourceTaskId: refreshedTask.id, sourceChunkId: c.id, role: c.role, content: c.content, summary: c.summary, kind: c.kind, groupId: null, visibility: "public", createdAt: c.createdAt ?? Date.now() })),
1352
1428
  }),
1353
1429
  });
1354
- if (hubClient.userId) {
1430
+ const hubTaskId = String((response as any)?.taskId ?? "");
1431
+ if (this.sharingRole === "hub" && hubClient.userId) {
1355
1432
  const existing = this.store.getHubTaskBySource(hubClient.userId, taskId);
1356
1433
  this.store.upsertHubTask({
1357
- id: (response as any)?.taskId ?? existing?.id ?? crypto.randomUUID(),
1434
+ id: hubTaskId || existing?.id || crypto.randomUUID(),
1358
1435
  sourceTaskId: taskId, sourceUserId: hubClient.userId, title: refreshedTask.title ?? "",
1359
1436
  summary: refreshedTask.summary ?? "", groupId: null, visibility: "public",
1360
1437
  createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
1361
1438
  });
1362
1439
  }
1440
+ const conn = this.store.getClientHubConnection();
1441
+ this.store.markTaskShared(taskId, hubTaskId, chunks.length, "public", null, conn?.hubInstanceId ?? "");
1363
1442
  hubSynced = true;
1364
1443
  }
1365
1444
  if (!isLocalShared) {
1366
1445
  const originalOwner = task.owner;
1367
1446
  const db = (this.store as any).db;
1368
- db.prepare("INSERT INTO local_shared_tasks (task_id, hub_task_id, original_owner, shared_at) VALUES (?, ?, ?, ?) ON CONFLICT(task_id) DO UPDATE SET original_owner = excluded.original_owner, shared_at = excluded.shared_at").run(taskId, "", originalOwner, Date.now());
1447
+ db.prepare("INSERT INTO local_shared_tasks (task_id, hub_task_id, original_owner, hub_instance_id, shared_at) VALUES (?, ?, ?, ?, ?) ON CONFLICT(task_id) DO UPDATE SET original_owner = excluded.original_owner, hub_instance_id = excluded.hub_instance_id, shared_at = excluded.shared_at").run(taskId, "", originalOwner, "", Date.now());
1369
1448
  db.prepare("UPDATE tasks SET owner = 'public' WHERE id = ?").run(taskId);
1370
1449
  }
1371
1450
  }
@@ -1385,7 +1464,8 @@ export class ViewerServer {
1385
1464
  await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/unshare", {
1386
1465
  method: "POST", body: JSON.stringify({ sourceTaskId: taskId }),
1387
1466
  });
1388
- if (hubClient.userId) this.store.deleteHubTaskBySource(hubClient.userId, taskId);
1467
+ if (this.sharingRole === "hub" && hubClient.userId) this.store.deleteHubTaskBySource(hubClient.userId, taskId);
1468
+ else this.store.downgradeTeamSharedTaskToLocal(taskId);
1389
1469
  hubSynced = true;
1390
1470
  } catch (err) { this.log.warn(`Failed to unshare task from team: ${err}`); }
1391
1471
  }
@@ -1397,7 +1477,8 @@ export class ViewerServer {
1397
1477
  await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/unshare", {
1398
1478
  method: "POST", body: JSON.stringify({ sourceTaskId: taskId }),
1399
1479
  });
1400
- if (hubClient.userId) this.store.deleteHubTaskBySource(hubClient.userId, taskId);
1480
+ if (this.sharingRole === "hub" && hubClient.userId) this.store.deleteHubTaskBySource(hubClient.userId, taskId);
1481
+ else if (!isLocalShared) this.store.unmarkTaskShared(taskId);
1401
1482
  hubSynced = true;
1402
1483
  } catch (err) { this.log.warn(`Failed to unshare task from team: ${err}`); }
1403
1484
  }
@@ -1454,16 +1535,20 @@ export class ViewerServer {
1454
1535
  method: "POST",
1455
1536
  body: JSON.stringify({ visibility: "public", groupId: null, metadata: bundle.metadata, bundle: bundle.bundle }),
1456
1537
  });
1457
- if (hubClient.userId) {
1538
+ const hubSkillId = String((response as any)?.skillId ?? "");
1539
+ if (this.sharingRole === "hub" && hubClient.userId) {
1458
1540
  const existing = this.store.getHubSkillBySource(hubClient.userId, skillId);
1459
1541
  this.store.upsertHubSkill({
1460
- id: (response as any)?.skillId ?? existing?.id ?? crypto.randomUUID(),
1542
+ id: hubSkillId || existing?.id || crypto.randomUUID(),
1461
1543
  sourceSkillId: skillId, sourceUserId: hubClient.userId,
1462
1544
  name: skill.name, description: skill.description, version: skill.version,
1463
1545
  groupId: null, visibility: "public",
1464
1546
  bundle: JSON.stringify(bundle.bundle), qualityScore: skill.qualityScore,
1465
1547
  createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
1466
1548
  });
1549
+ } else {
1550
+ const conn = this.store.getClientHubConnection();
1551
+ this.store.upsertTeamSharedSkill(skillId, { hubSkillId, visibility: "public", groupId: null, hubInstanceId: conn?.hubInstanceId ?? "" });
1467
1552
  }
1468
1553
  hubSynced = true;
1469
1554
  }
@@ -1480,7 +1565,8 @@ export class ViewerServer {
1480
1565
  await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/unpublish", {
1481
1566
  method: "POST", body: JSON.stringify({ sourceSkillId: skillId }),
1482
1567
  });
1483
- if (hubClient.userId) this.store.deleteHubSkillBySource(hubClient.userId, skillId);
1568
+ if (this.sharingRole === "hub" && hubClient.userId) this.store.deleteHubSkillBySource(hubClient.userId, skillId);
1569
+ else this.store.deleteTeamSharedSkill(skillId);
1484
1570
  hubSynced = true;
1485
1571
  } catch (err) { this.log.warn(`Failed to unpublish skill from team: ${err}`); }
1486
1572
  }
@@ -1492,7 +1578,8 @@ export class ViewerServer {
1492
1578
  await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/unpublish", {
1493
1579
  method: "POST", body: JSON.stringify({ sourceSkillId: skillId }),
1494
1580
  });
1495
- if (hubClient.userId) this.store.deleteHubSkillBySource(hubClient.userId, skillId);
1581
+ if (this.sharingRole === "hub" && hubClient.userId) this.store.deleteHubSkillBySource(hubClient.userId, skillId);
1582
+ else this.store.deleteTeamSharedSkill(skillId);
1496
1583
  hubSynced = true;
1497
1584
  } catch (err) { this.log.warn(`Failed to unpublish skill from team: ${err}`); }
1498
1585
  }
@@ -1506,29 +1593,53 @@ export class ViewerServer {
1506
1593
  });
1507
1594
  }
1508
1595
 
1596
+ private get sharingRole(): string | undefined {
1597
+ return this.ctx?.config?.sharing?.role;
1598
+ }
1599
+
1600
+ private isCurrentClientHubInstance(hubInstanceId?: string): boolean {
1601
+ if (this.sharingRole !== "client") return true;
1602
+ const scopedHubInstanceId = String(hubInstanceId ?? "");
1603
+ if (!scopedHubInstanceId) return true;
1604
+ const currentHubInstanceId = this.store.getClientHubConnection()?.hubInstanceId ?? "";
1605
+ if (!currentHubInstanceId) return true;
1606
+ return scopedHubInstanceId === currentHubInstanceId;
1607
+ }
1608
+
1509
1609
  private getHubMemoryForChunk(chunkId: string): any {
1510
- const db = (this.store as any).db;
1511
- const hub = db.prepare("SELECT * FROM hub_memories WHERE source_chunk_id = ? LIMIT 1").get(chunkId);
1512
- if (hub) return hub;
1610
+ if (this.sharingRole === "hub") {
1611
+ const db = (this.store as any).db;
1612
+ return db.prepare("SELECT * FROM hub_memories WHERE source_chunk_id = ? LIMIT 1").get(chunkId);
1613
+ }
1513
1614
  const ts = this.store.getTeamSharedChunk(chunkId);
1514
- if (ts) {
1515
- return {
1516
- source_chunk_id: chunkId,
1517
- visibility: ts.visibility,
1518
- group_id: ts.groupId,
1519
- };
1615
+ if (ts && this.isCurrentClientHubInstance(ts.hubInstanceId)) {
1616
+ return { source_chunk_id: chunkId, visibility: ts.visibility, group_id: ts.groupId };
1520
1617
  }
1521
1618
  return undefined;
1522
1619
  }
1523
1620
 
1524
1621
  private getHubTaskForLocal(taskId: string): any {
1525
- const db = (this.store as any).db;
1526
- return db.prepare("SELECT * FROM hub_tasks WHERE source_task_id = ? LIMIT 1").get(taskId);
1622
+ if (this.sharingRole === "hub") {
1623
+ const db = (this.store as any).db;
1624
+ return db.prepare("SELECT * FROM hub_tasks WHERE source_task_id = ? LIMIT 1").get(taskId);
1625
+ }
1626
+ const shared = this.store.getLocalSharedTask(taskId);
1627
+ if (shared && shared.hubTaskId && this.isCurrentClientHubInstance(shared.hubInstanceId)) {
1628
+ return { source_task_id: taskId, visibility: shared.visibility, group_id: shared.groupId };
1629
+ }
1630
+ return undefined;
1527
1631
  }
1528
1632
 
1529
1633
  private getHubSkillForLocal(skillId: string): any {
1530
- const db = (this.store as any).db;
1531
- return db.prepare("SELECT * FROM hub_skills WHERE source_skill_id = ? LIMIT 1").get(skillId);
1634
+ if (this.sharingRole === "hub") {
1635
+ const db = (this.store as any).db;
1636
+ return db.prepare("SELECT * FROM hub_skills WHERE source_skill_id = ? LIMIT 1").get(skillId);
1637
+ }
1638
+ const ts = this.store.getTeamSharedSkill(skillId);
1639
+ if (ts && this.isCurrentClientHubInstance(ts.hubInstanceId)) {
1640
+ return { source_skill_id: skillId, visibility: ts.visibility, group_id: ts.groupId };
1641
+ }
1642
+ return undefined;
1532
1643
  }
1533
1644
 
1534
1645
  private handleDeleteSession(res: http.ServerResponse, url: URL): void {
@@ -1844,6 +1955,11 @@ export class ViewerServer {
1844
1955
  body: JSON.stringify({ teamToken, username, deviceName: hostname, reapply: true, identityKey: existingIdentityKey }),
1845
1956
  }) as any;
1846
1957
  const returnedIdentityKey = String(result.identityKey || existingIdentityKey || "");
1958
+ let hubInstanceId = persisted?.hubInstanceId || "";
1959
+ try {
1960
+ const info = await hubRequestJson(hubUrl, "", "/api/v1/hub/info", { method: "GET" }) as any;
1961
+ hubInstanceId = String(info?.hubInstanceId ?? hubInstanceId);
1962
+ } catch { /* best-effort */ }
1847
1963
  this.store.setClientHubConnection({
1848
1964
  hubUrl,
1849
1965
  userId: String(result.userId || ""),
@@ -1853,6 +1969,7 @@ export class ViewerServer {
1853
1969
  connectedAt: Date.now(),
1854
1970
  identityKey: returnedIdentityKey,
1855
1971
  lastKnownStatus: result.status || "",
1972
+ hubInstanceId,
1856
1973
  });
1857
1974
  this.jsonResponse(res, { ok: true, status: result.status || "pending" });
1858
1975
  } catch (err) {
@@ -2060,9 +2177,10 @@ export class ViewerServer {
2060
2177
  }),
2061
2178
  });
2062
2179
  const hubUserId = hubClient.userId;
2063
- if (hubUserId) {
2180
+ const hubTaskId = String((response as any)?.taskId ?? task.id);
2181
+ if (this.sharingRole === "hub" && hubUserId) {
2064
2182
  this.store.upsertHubTask({
2065
- id: task.id,
2183
+ id: hubTaskId,
2066
2184
  sourceTaskId: task.id,
2067
2185
  sourceUserId: hubUserId,
2068
2186
  title: task.title,
@@ -2072,6 +2190,9 @@ export class ViewerServer {
2072
2190
  createdAt: task.startedAt ?? Date.now(),
2073
2191
  updatedAt: task.updatedAt ?? Date.now(),
2074
2192
  });
2193
+ } else {
2194
+ const conn = this.store.getClientHubConnection();
2195
+ this.store.markTaskShared(task.id, hubTaskId, chunks.length, visibility, groupId, conn?.hubInstanceId ?? "");
2075
2196
  }
2076
2197
  this.jsonResponse(res, { ok: true, taskId, visibility, response });
2077
2198
  } catch (err) {
@@ -2094,7 +2215,9 @@ export class ViewerServer {
2094
2215
  body: JSON.stringify({ sourceTaskId: task.id }),
2095
2216
  });
2096
2217
  const hubUserId = hubClient.userId;
2097
- if (hubUserId) this.store.deleteHubTaskBySource(hubUserId, task.id);
2218
+ if (this.sharingRole === "hub" && hubUserId) this.store.deleteHubTaskBySource(hubUserId, task.id);
2219
+ else if (task.owner === "public") this.store.downgradeTeamSharedTaskToLocal(task.id);
2220
+ else this.store.unmarkTaskShared(task.id);
2098
2221
  this.jsonResponse(res, { ok: true, taskId });
2099
2222
  } catch (err) {
2100
2223
  this.jsonResponse(res, { ok: false, error: String(err) });
@@ -2146,7 +2269,8 @@ export class ViewerServer {
2146
2269
  updatedAt: now,
2147
2270
  });
2148
2271
  } else if (hubClient.userId) {
2149
- this.store.upsertTeamSharedChunk(chunk.id, { hubMemoryId: mid, visibility, groupId });
2272
+ const conn = this.store.getClientHubConnection();
2273
+ this.store.upsertTeamSharedChunk(chunk.id, { hubMemoryId: mid, visibility, groupId, hubInstanceId: conn?.hubInstanceId ?? "" });
2150
2274
  }
2151
2275
  this.jsonResponse(res, { ok: true, chunkId, visibility, response });
2152
2276
  } catch (err) {
@@ -2167,8 +2291,8 @@ export class ViewerServer {
2167
2291
  body: JSON.stringify({ sourceChunkId: chunkId }),
2168
2292
  });
2169
2293
  const hubUserId = hubClient.userId;
2170
- if (hubUserId) this.store.deleteHubMemoryBySource(hubUserId, chunkId);
2171
- this.store.deleteTeamSharedChunk(chunkId);
2294
+ if (this.sharingRole === "hub" && hubUserId) this.store.deleteHubMemoryBySource(hubUserId, chunkId);
2295
+ else this.store.deleteTeamSharedChunk(chunkId);
2172
2296
  this.jsonResponse(res, { ok: true, chunkId });
2173
2297
  } catch (err) {
2174
2298
  this.jsonResponse(res, { ok: false, error: String(err) });
@@ -2213,7 +2337,7 @@ export class ViewerServer {
2213
2337
  }),
2214
2338
  });
2215
2339
  const hubUserId = hubClient.userId;
2216
- if (hubUserId) {
2340
+ if (this.sharingRole === "hub" && hubUserId) {
2217
2341
  const existing = this.store.getHubSkillBySource(hubUserId, skillId);
2218
2342
  this.store.upsertHubSkill({
2219
2343
  id: (response as any)?.skillId ?? existing?.id ?? crypto.randomUUID(),
@@ -2229,6 +2353,14 @@ export class ViewerServer {
2229
2353
  createdAt: existing?.createdAt ?? Date.now(),
2230
2354
  updatedAt: Date.now(),
2231
2355
  });
2356
+ } else {
2357
+ const conn = this.store.getClientHubConnection();
2358
+ this.store.upsertTeamSharedSkill(skillId, {
2359
+ hubSkillId: String((response as any)?.skillId ?? ""),
2360
+ visibility,
2361
+ groupId,
2362
+ hubInstanceId: conn?.hubInstanceId ?? "",
2363
+ });
2232
2364
  }
2233
2365
  this.jsonResponse(res, { ok: true, skillId, visibility, response });
2234
2366
  } catch (err) {
@@ -2251,7 +2383,8 @@ export class ViewerServer {
2251
2383
  body: JSON.stringify({ sourceSkillId: skill.id }),
2252
2384
  });
2253
2385
  const hubUserId = hubClient.userId;
2254
- if (hubUserId) this.store.deleteHubSkillBySource(hubUserId, skill.id);
2386
+ if (this.sharingRole === "hub" && hubUserId) this.store.deleteHubSkillBySource(hubUserId, skill.id);
2387
+ else this.store.deleteTeamSharedSkill(skill.id);
2255
2388
  this.jsonResponse(res, { ok: true, skillId });
2256
2389
  } catch (err) {
2257
2390
  this.jsonResponse(res, { ok: false, error: String(err) });
@@ -2717,19 +2850,21 @@ export class ViewerServer {
2717
2850
  const isClient = newEnabled && newRole === "client";
2718
2851
  if (wasClient && !isClient) {
2719
2852
  await this.withdrawOrLeaveHub();
2853
+ this.store.clearAllTeamSharingState();
2720
2854
  this.store.clearClientHubConnection();
2721
- this.log.info("Client hub connection cleared (sharing disabled or role changed)");
2855
+ this.log.info("Client hub connection and team sharing state cleared (sharing disabled or role changed)");
2722
2856
  }
2723
2857
 
2724
2858
  if (wasClient && isClient) {
2725
2859
  const newClientAddr = String((merged.client as Record<string, unknown>)?.hubAddress || "");
2726
2860
  if (newClientAddr && oldClientHubAddress && normalizeHubUrl(newClientAddr) !== normalizeHubUrl(oldClientHubAddress)) {
2727
2861
  this.notifyHubLeave();
2862
+ this.store.clearAllTeamSharingState();
2728
2863
  const oldConn = this.store.getClientHubConnection();
2729
2864
  if (oldConn) {
2730
- this.store.setClientHubConnection({ ...oldConn, hubUrl: normalizeHubUrl(newClientAddr), userToken: "", lastKnownStatus: "hub_changed" });
2865
+ this.store.setClientHubConnection({ ...oldConn, hubUrl: normalizeHubUrl(newClientAddr), userToken: "", hubInstanceId: "", lastKnownStatus: "hub_changed" });
2731
2866
  }
2732
- this.log.info("Client hub connection token cleared (switched to different Hub), identity preserved");
2867
+ this.log.info("Client hub connection and team sharing state cleared (switched to different Hub)");
2733
2868
  }
2734
2869
  }
2735
2870
 
@@ -2790,6 +2925,11 @@ export class ViewerServer {
2790
2925
  body: JSON.stringify({ teamToken, username, deviceName: hostname, identityKey: existingIdentityKey }),
2791
2926
  }) as any;
2792
2927
  const returnedIdentityKey = String(result.identityKey || existingIdentityKey || "");
2928
+ let hubInstanceId = persisted?.hubInstanceId || "";
2929
+ try {
2930
+ const info = await hubRequestJson(hubUrl, "", "/api/v1/hub/info", { method: "GET" }) as any;
2931
+ hubInstanceId = String(info?.hubInstanceId ?? hubInstanceId);
2932
+ } catch { /* best-effort */ }
2793
2933
  this.store.setClientHubConnection({
2794
2934
  hubUrl,
2795
2935
  userId: String(result.userId || ""),
@@ -2799,6 +2939,7 @@ export class ViewerServer {
2799
2939
  connectedAt: Date.now(),
2800
2940
  identityKey: returnedIdentityKey,
2801
2941
  lastKnownStatus: result.status || "",
2942
+ hubInstanceId,
2802
2943
  });
2803
2944
  this.log.info(`Auto-join on save: status=${result.status}, userId=${result.userId}`);
2804
2945
  if (result.userToken) {
@@ -2811,6 +2952,7 @@ export class ViewerServer {
2811
2952
  this.readBody(_req, async () => {
2812
2953
  try {
2813
2954
  await this.withdrawOrLeaveHub();
2955
+ this.store.clearAllTeamSharingState();
2814
2956
  this.store.clearClientHubConnection();
2815
2957
 
2816
2958
  const configPath = this.getOpenClawConfigPath();
@@ -3196,7 +3338,8 @@ export class ViewerServer {
3196
3338
 
3197
3339
  // Install dependencies
3198
3340
  this.log.info(`update-install: installing dependencies...`);
3199
- exec(`cd ${extDir} && npm install --omit=dev --ignore-scripts`, { timeout: 120_000 }, (npmErr, npmOut, npmStderr) => {
3341
+ const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
3342
+ execFile(npmCmd, ["install", "--omit=dev", "--ignore-scripts"], { cwd: extDir, timeout: 120_000 }, (npmErr, npmOut, npmStderr) => {
3200
3343
  if (npmErr) {
3201
3344
  try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
3202
3345
  this.log.warn(`update-install: npm install failed: ${npmErr.message}`);
@@ -3204,25 +3347,21 @@ export class ViewerServer {
3204
3347
  return;
3205
3348
  }
3206
3349
 
3207
- // Rebuild native modules (do not swallow errors)
3208
- exec(`cd ${extDir} && npm rebuild better-sqlite3`, { timeout: 60_000 }, (rebuildErr, rebuildOut, rebuildStderr) => {
3350
+ execFile(npmCmd, ["rebuild", "better-sqlite3"], { cwd: extDir, timeout: 60_000 }, (rebuildErr, rebuildOut, rebuildStderr) => {
3209
3351
  if (rebuildErr) {
3210
3352
  this.log.warn(`update-install: better-sqlite3 rebuild failed: ${rebuildErr.message}`);
3211
3353
  const stderr = String(rebuildStderr || "").trim();
3212
3354
  if (stderr) this.log.warn(`update-install: rebuild stderr: ${stderr.slice(0, 500)}`);
3213
- // Continue so postinstall.cjs can run (it will try rebuild again and show user guidance)
3214
3355
  }
3215
3356
 
3216
- // Run postinstall.cjs: legacy cleanup, skill install, version marker, and optional sqlite re-check
3217
3357
  this.log.info(`update-install: running postinstall...`);
3218
- exec(`cd ${extDir} && node scripts/postinstall.cjs`, { timeout: 180_000 }, (postErr, postOut, postStderr) => {
3358
+ execFile(process.execPath, ["scripts/postinstall.cjs"], { cwd: extDir, timeout: 180_000 }, (postErr, postOut, postStderr) => {
3219
3359
  try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
3220
3360
 
3221
3361
  if (postErr) {
3222
3362
  this.log.warn(`update-install: postinstall failed: ${postErr.message}`);
3223
3363
  const postStderrStr = String(postStderr || "").trim();
3224
3364
  if (postStderrStr) this.log.warn(`update-install: postinstall stderr: ${postStderrStr.slice(0, 500)}`);
3225
- // Still report success; plugin is updated, user can run postinstall manually if needed
3226
3365
  }
3227
3366
 
3228
3367
  // Read new version
@@ -3575,7 +3714,7 @@ export class ViewerServer {
3575
3714
  } else if (this.migrationState.done) {
3576
3715
  const evtName = this.migrationState.stopped ? "stopped" : "done";
3577
3716
  res.write(`event: state\ndata: ${JSON.stringify(this.migrationState)}\n\n`);
3578
- res.write(`event: ${evtName}\ndata: ${JSON.stringify({ ok: true })}\n\n`);
3717
+ res.write(`event: ${evtName}\ndata: ${JSON.stringify({ ok: this.migrationState.success, ...this.migrationState })}\n\n`);
3579
3718
  res.end();
3580
3719
  } else {
3581
3720
  res.end();
@@ -3616,19 +3755,12 @@ export class ViewerServer {
3616
3755
  this.migrationSSEClients = this.migrationSSEClients.filter(c => c !== res);
3617
3756
  });
3618
3757
 
3619
- this.migrationAbort = false;
3620
- this.migrationState = { phase: "", stored: 0, skipped: 0, merged: 0, errors: 0, processed: 0, total: 0, lastItem: null, done: false, stopped: false };
3758
+ this.migrationState = createInitialMigrationState();
3621
3759
 
3622
3760
  const send = (event: string, data: unknown) => {
3623
3761
  if (event === "item") {
3624
3762
  const d = data as any;
3625
- if (d.status === "stored") this.migrationState.stored++;
3626
- else if (d.status === "skipped" || d.status === "duplicate") this.migrationState.skipped++;
3627
- else if (d.status === "merged") this.migrationState.merged++;
3628
- else if (d.status === "error") this.migrationState.errors++;
3629
- this.migrationState.processed = d.index ?? this.migrationState.processed + 1;
3630
- this.migrationState.total = d.total ?? this.migrationState.total;
3631
- this.migrationState.lastItem = d;
3763
+ applyMigrationItemToState(this.migrationState, d);
3632
3764
  } else if (event === "phase") {
3633
3765
  this.migrationState.phase = (data as any).phase;
3634
3766
  } else if (event === "progress") {
@@ -3641,11 +3773,13 @@ export class ViewerServer {
3641
3773
  this.runMigration(send, opts.sources, concurrency).finally(() => {
3642
3774
  this.migrationRunning = false;
3643
3775
  this.migrationState.done = true;
3776
+ this.migrationState.success = computeMigrationSuccess(this.migrationState);
3777
+ const donePayload = { ok: this.migrationState.success, ...this.migrationState };
3644
3778
  if (this.migrationAbort) {
3645
3779
  this.migrationState.stopped = true;
3646
- this.broadcastSSE("stopped", { ok: true, ...this.migrationState });
3780
+ this.broadcastSSE("stopped", donePayload);
3647
3781
  } else {
3648
- this.broadcastSSE("done", { ok: true });
3782
+ this.broadcastSSE("done", donePayload);
3649
3783
  }
3650
3784
  this.migrationAbort = false;
3651
3785
  const clientsToClose = [...this.migrationSSEClients];
@@ -3742,11 +3876,24 @@ export class ViewerServer {
3742
3876
  }
3743
3877
 
3744
3878
  try {
3745
- const summary = await summarizer.summarize(row.text);
3879
+ const stepFailures: Array<"summarization" | "dedup" | "embedding"> = [];
3880
+ let summary = "";
3881
+ try {
3882
+ summary = await summarizer.summarize(row.text);
3883
+ } catch (err) {
3884
+ stepFailures.push("summarization");
3885
+ this.log.warn(`Migration summarization failed: ${err}`);
3886
+ }
3887
+ if (!summary) {
3888
+ stepFailures.push("summarization");
3889
+ summary = row.text.slice(0, 200);
3890
+ }
3891
+
3746
3892
  let embedding: number[] | null = null;
3747
3893
  try {
3748
3894
  [embedding] = await this.embedder.embed([summary]);
3749
3895
  } catch (err) {
3896
+ stepFailures.push("embedding");
3750
3897
  this.log.warn(`Migration embed failed: ${err}`);
3751
3898
  }
3752
3899
 
@@ -3765,26 +3912,31 @@ export class ViewerServer {
3765
3912
  }).filter(c => c.summary);
3766
3913
 
3767
3914
  if (candidates.length > 0) {
3768
- const dedupResult = await summarizer.judgeDedup(summary, candidates);
3769
- if (dedupResult?.action === "DUPLICATE" && dedupResult.targetIndex) {
3770
- const targetId = candidates[dedupResult.targetIndex - 1]?.chunkId;
3771
- if (targetId) {
3772
- dedupStatus = "duplicate";
3773
- dedupTarget = targetId;
3774
- dedupReason = dedupResult.reason;
3775
- }
3776
- } else if (dedupResult?.action === "UPDATE" && dedupResult.targetIndex && dedupResult.mergedSummary) {
3777
- const targetId = candidates[dedupResult.targetIndex - 1]?.chunkId;
3778
- if (targetId) {
3779
- this.store.updateChunkSummaryAndContent(targetId, dedupResult.mergedSummary, row.text);
3780
- try {
3781
- const [newEmb] = await this.embedder.embed([dedupResult.mergedSummary]);
3782
- if (newEmb) this.store.upsertEmbedding(targetId, newEmb);
3783
- } catch { /* best-effort */ }
3784
- dedupStatus = "merged";
3785
- dedupTarget = targetId;
3786
- dedupReason = dedupResult.reason;
3915
+ try {
3916
+ const dedupResult = await summarizer.judgeDedup(summary, candidates);
3917
+ if (dedupResult?.action === "DUPLICATE" && dedupResult.targetIndex) {
3918
+ const targetId = candidates[dedupResult.targetIndex - 1]?.chunkId;
3919
+ if (targetId) {
3920
+ dedupStatus = "duplicate";
3921
+ dedupTarget = targetId;
3922
+ dedupReason = dedupResult.reason;
3923
+ }
3924
+ } else if (dedupResult?.action === "UPDATE" && dedupResult.targetIndex && dedupResult.mergedSummary) {
3925
+ const targetId = candidates[dedupResult.targetIndex - 1]?.chunkId;
3926
+ if (targetId) {
3927
+ this.store.updateChunkSummaryAndContent(targetId, dedupResult.mergedSummary, row.text);
3928
+ try {
3929
+ const [newEmb] = await this.embedder.embed([dedupResult.mergedSummary]);
3930
+ if (newEmb) this.store.upsertEmbedding(targetId, newEmb);
3931
+ } catch { /* best-effort */ }
3932
+ dedupStatus = "merged";
3933
+ dedupTarget = targetId;
3934
+ dedupReason = dedupResult.reason;
3935
+ }
3787
3936
  }
3937
+ } catch (err) {
3938
+ stepFailures.push("dedup");
3939
+ this.log.warn(`Migration dedup judgment failed: ${err}`);
3788
3940
  }
3789
3941
  }
3790
3942
  }
@@ -3827,7 +3979,13 @@ export class ViewerServer {
3827
3979
  preview: row.text.slice(0, 120),
3828
3980
  summary: summary.slice(0, 80),
3829
3981
  source: file,
3982
+ stepFailures,
3830
3983
  });
3984
+ if (stepFailures.length > 0) {
3985
+ this.log.warn(`[MIGRATION] sqlite item imported with step failures: ${stepFailures.join(",")}`);
3986
+ } else {
3987
+ this.log.info("[MIGRATION] sqlite item imported successfully (all steps)");
3988
+ }
3831
3989
  } catch (err) {
3832
3990
  totalErrors++;
3833
3991
  send("item", {
@@ -3957,11 +4115,24 @@ export class ViewerServer {
3957
4115
  }
3958
4116
 
3959
4117
  try {
3960
- const summary = await summarizer.summarize(content);
4118
+ const stepFailures: Array<"summarization" | "dedup" | "embedding"> = [];
4119
+ let summary = "";
4120
+ try {
4121
+ summary = await summarizer.summarize(content);
4122
+ } catch (err) {
4123
+ stepFailures.push("summarization");
4124
+ this.log.warn(`Migration summarization failed: ${err}`);
4125
+ }
4126
+ if (!summary) {
4127
+ stepFailures.push("summarization");
4128
+ summary = content.slice(0, 200);
4129
+ }
4130
+
3961
4131
  let embedding: number[] | null = null;
3962
4132
  try {
3963
4133
  [embedding] = await this.embedder.embed([summary]);
3964
4134
  } catch (err) {
4135
+ stepFailures.push("embedding");
3965
4136
  this.log.warn(`Migration embed failed: ${err}`);
3966
4137
  }
3967
4138
 
@@ -3980,17 +4151,22 @@ export class ViewerServer {
3980
4151
  }).filter(c => c.summary);
3981
4152
 
3982
4153
  if (candidates.length > 0) {
3983
- const dedupResult = await summarizer.judgeDedup(summary, candidates);
3984
- if (dedupResult?.action === "DUPLICATE" && dedupResult.targetIndex) {
3985
- const targetId = candidates[dedupResult.targetIndex - 1]?.chunkId;
3986
- if (targetId) { dedupStatus = "duplicate"; dedupTarget = targetId; dedupReason = dedupResult.reason; }
3987
- } else if (dedupResult?.action === "UPDATE" && dedupResult.targetIndex && dedupResult.mergedSummary) {
3988
- const targetId = candidates[dedupResult.targetIndex - 1]?.chunkId;
3989
- if (targetId) {
3990
- this.store.updateChunkSummaryAndContent(targetId, dedupResult.mergedSummary, content);
3991
- try { const [newEmb] = await this.embedder.embed([dedupResult.mergedSummary]); if (newEmb) this.store.upsertEmbedding(targetId, newEmb); } catch { /* best-effort */ }
3992
- dedupStatus = "merged"; dedupTarget = targetId; dedupReason = dedupResult.reason;
4154
+ try {
4155
+ const dedupResult = await summarizer.judgeDedup(summary, candidates);
4156
+ if (dedupResult?.action === "DUPLICATE" && dedupResult.targetIndex) {
4157
+ const targetId = candidates[dedupResult.targetIndex - 1]?.chunkId;
4158
+ if (targetId) { dedupStatus = "duplicate"; dedupTarget = targetId; dedupReason = dedupResult.reason; }
4159
+ } else if (dedupResult?.action === "UPDATE" && dedupResult.targetIndex && dedupResult.mergedSummary) {
4160
+ const targetId = candidates[dedupResult.targetIndex - 1]?.chunkId;
4161
+ if (targetId) {
4162
+ this.store.updateChunkSummaryAndContent(targetId, dedupResult.mergedSummary, content);
4163
+ try { const [newEmb] = await this.embedder.embed([dedupResult.mergedSummary]); if (newEmb) this.store.upsertEmbedding(targetId, newEmb); } catch { /* best-effort */ }
4164
+ dedupStatus = "merged"; dedupTarget = targetId; dedupReason = dedupResult.reason;
4165
+ }
3993
4166
  }
4167
+ } catch (err) {
4168
+ stepFailures.push("dedup");
4169
+ this.log.warn(`Migration dedup judgment failed: ${err}`);
3994
4170
  }
3995
4171
  }
3996
4172
  }
@@ -4010,7 +4186,12 @@ export class ViewerServer {
4010
4186
  if (embedding && dedupStatus === "active") this.store.upsertEmbedding(chunkId, embedding);
4011
4187
 
4012
4188
  totalStored++;
4013
- send("item", { index: idx, total: totalMsgs, status: dedupStatus === "active" ? "stored" : dedupStatus, preview: content.slice(0, 120), summary: summary.slice(0, 80), source: file, agent: agentId, role: msgRole });
4189
+ send("item", { index: idx, total: totalMsgs, status: dedupStatus === "active" ? "stored" : dedupStatus, preview: content.slice(0, 120), summary: summary.slice(0, 80), source: file, agent: agentId, role: msgRole, stepFailures });
4190
+ if (stepFailures.length > 0) {
4191
+ this.log.warn(`[MIGRATION] session item imported with step failures: ${stepFailures.join(",")}`);
4192
+ } else {
4193
+ this.log.info("[MIGRATION] session item imported successfully (all steps)");
4194
+ }
4014
4195
  } catch (err) {
4015
4196
  totalErrors++;
4016
4197
  send("item", { index: idx, total: totalMsgs, status: "error", preview: content.slice(0, 120), source: file, agent: agentId, error: String(err).slice(0, 200) });
@@ -4051,7 +4232,14 @@ export class ViewerServer {
4051
4232
  }
4052
4233
 
4053
4234
  send("progress", { total: totalProcessed, processed: totalProcessed, phase: "done" });
4054
- send("summary", { totalProcessed, totalStored, totalSkipped, totalErrors });
4235
+ send("summary", {
4236
+ totalProcessed,
4237
+ totalStored,
4238
+ totalSkipped,
4239
+ totalErrors,
4240
+ success: computeMigrationSuccess(this.migrationState),
4241
+ stepFailures: this.migrationState.stepFailures,
4242
+ });
4055
4243
  }
4056
4244
 
4057
4245
  // ─── Post-processing: independent task/skill generation ───