@memtensor/memos-local-openclaw-plugin 1.0.5 → 1.0.6-beta.10

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 (66) 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 +33 -5
  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 +2 -0
  11. package/dist/hub/server.d.ts.map +1 -1
  12. package/dist/hub/server.js +116 -54
  13. package/dist/hub/server.js.map +1 -1
  14. package/dist/ingest/providers/index.d.ts +4 -0
  15. package/dist/ingest/providers/index.d.ts.map +1 -1
  16. package/dist/ingest/providers/index.js +32 -86
  17. package/dist/ingest/providers/index.js.map +1 -1
  18. package/dist/ingest/providers/openai.d.ts.map +1 -1
  19. package/dist/ingest/providers/openai.js +29 -13
  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 +33 -32
  23. package/dist/recall/engine.js.map +1 -1
  24. package/dist/storage/sqlite.d.ts +43 -7
  25. package/dist/storage/sqlite.d.ts.map +1 -1
  26. package/dist/storage/sqlite.js +179 -58
  27. package/dist/storage/sqlite.js.map +1 -1
  28. package/dist/tools/memory-get.d.ts.map +1 -1
  29. package/dist/tools/memory-get.js +4 -1
  30. package/dist/tools/memory-get.js.map +1 -1
  31. package/dist/types.d.ts +1 -1
  32. package/dist/types.d.ts.map +1 -1
  33. package/dist/types.js.map +1 -1
  34. package/dist/update-check.d.ts.map +1 -1
  35. package/dist/update-check.js +2 -7
  36. package/dist/update-check.js.map +1 -1
  37. package/dist/viewer/html.d.ts.map +1 -1
  38. package/dist/viewer/html.js +115 -27
  39. package/dist/viewer/html.js.map +1 -1
  40. package/dist/viewer/server.d.ts +25 -0
  41. package/dist/viewer/server.d.ts.map +1 -1
  42. package/dist/viewer/server.js +503 -206
  43. package/dist/viewer/server.js.map +1 -1
  44. package/index.ts +273 -282
  45. package/openclaw.plugin.json +1 -1
  46. package/package.json +2 -1
  47. package/scripts/native-binding.cjs +32 -0
  48. package/scripts/postinstall.cjs +24 -11
  49. package/src/capture/index.ts +36 -0
  50. package/src/client/connector.ts +32 -5
  51. package/src/client/hub.ts +4 -0
  52. package/src/hub/server.ts +110 -50
  53. package/src/ingest/providers/index.ts +37 -92
  54. package/src/ingest/providers/openai.ts +31 -13
  55. package/src/recall/engine.ts +32 -30
  56. package/src/storage/sqlite.ts +196 -63
  57. package/src/tools/memory-get.ts +4 -1
  58. package/src/types.ts +2 -0
  59. package/src/update-check.ts +2 -7
  60. package/src/viewer/html.ts +115 -27
  61. package/src/viewer/server.ts +483 -172
  62. package/prebuilds/darwin-arm64/better_sqlite3.node +0 -0
  63. package/prebuilds/darwin-x64/better_sqlite3.node +0 -0
  64. package/prebuilds/linux-x64/better_sqlite3.node +0 -0
  65. package/prebuilds/win32-x64/better_sqlite3.node +0 -0
  66. package/telemetry.credentials.json +0 -5
@@ -37,6 +37,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.ViewerServer = void 0;
40
+ exports.computeMigrationSuccess = computeMigrationSuccess;
41
+ exports.createInitialMigrationState = createInitialMigrationState;
42
+ exports.applyMigrationItemToState = applyMigrationItemToState;
40
43
  const node_http_1 = __importDefault(require("node:http"));
41
44
  const node_os_1 = __importDefault(require("node:os"));
42
45
  const node_crypto_1 = __importDefault(require("node:crypto"));
@@ -57,10 +60,53 @@ const hub_1 = require("../client/hub");
57
60
  const skill_sync_1 = require("../client/skill-sync");
58
61
  const html_1 = require("./html");
59
62
  const uuid_1 = require("uuid");
60
- function normalizeTimestamp(ts) {
61
- if (ts < 1e12)
62
- return ts * 1000;
63
- return ts;
63
+ function createInitialStepFailures() {
64
+ return { summarization: 0, dedup: 0, embedding: 0 };
65
+ }
66
+ function computeMigrationSuccess(state) {
67
+ const sf = state.stepFailures;
68
+ return state.errors === 0 && sf.summarization === 0 && sf.dedup === 0 && sf.embedding === 0;
69
+ }
70
+ function createInitialMigrationState() {
71
+ const stepFailures = createInitialStepFailures();
72
+ return {
73
+ phase: "",
74
+ stored: 0,
75
+ skipped: 0,
76
+ merged: 0,
77
+ errors: 0,
78
+ processed: 0,
79
+ total: 0,
80
+ lastItem: null,
81
+ done: false,
82
+ stopped: false,
83
+ stepFailures,
84
+ success: computeMigrationSuccess({ errors: 0, stepFailures }),
85
+ };
86
+ }
87
+ function applyMigrationItemToState(state, d) {
88
+ if (d.status === "stored")
89
+ state.stored++;
90
+ else if (d.status === "skipped" || d.status === "duplicate")
91
+ state.skipped++;
92
+ else if (d.status === "merged")
93
+ state.merged++;
94
+ else if (d.status === "error")
95
+ state.errors++;
96
+ if (Array.isArray(d.stepFailures)) {
97
+ for (const step of d.stepFailures) {
98
+ if (step === "summarization")
99
+ state.stepFailures.summarization++;
100
+ else if (step === "dedup")
101
+ state.stepFailures.dedup++;
102
+ else if (step === "embedding")
103
+ state.stepFailures.embedding++;
104
+ }
105
+ }
106
+ state.processed = d.index ?? state.processed + 1;
107
+ state.total = d.total ?? state.total;
108
+ state.lastItem = d;
109
+ state.success = computeMigrationSuccess(state);
64
110
  }
65
111
  class ViewerServer {
66
112
  server = null;
@@ -87,7 +133,7 @@ class ViewerServer {
87
133
  resetToken;
88
134
  migrationRunning = false;
89
135
  migrationAbort = false;
90
- migrationState = { phase: "", stored: 0, skipped: 0, merged: 0, errors: 0, processed: 0, total: 0, lastItem: null, done: false, stopped: false };
136
+ migrationState = createInitialMigrationState();
91
137
  migrationSSEClients = [];
92
138
  ppRunning = false;
93
139
  ppAbort = false;
@@ -591,14 +637,15 @@ class ViewerServer {
591
637
  if (chunkIds.length > 0) {
592
638
  try {
593
639
  const placeholders = chunkIds.map(() => "?").join(",");
594
- const sharedRows = db.prepare(`SELECT source_chunk_id, visibility, group_id FROM hub_memories WHERE source_chunk_id IN (${placeholders})`).all(...chunkIds);
595
- for (const r of sharedRows)
596
- sharingMap.set(r.source_chunk_id, r);
597
- const teamMetaRows = db.prepare(`SELECT chunk_id, visibility, group_id FROM team_shared_chunks WHERE chunk_id IN (${placeholders})`).all(...chunkIds);
598
- for (const r of teamMetaRows) {
599
- if (!sharingMap.has(r.chunk_id)) {
640
+ if (this.sharingRole === "hub") {
641
+ const sharedRows = db.prepare(`SELECT source_chunk_id, visibility, group_id FROM hub_memories WHERE source_chunk_id IN (${placeholders})`).all(...chunkIds);
642
+ for (const r of sharedRows)
643
+ sharingMap.set(r.source_chunk_id, r);
644
+ }
645
+ else {
646
+ const teamMetaRows = db.prepare(`SELECT chunk_id, visibility, group_id FROM team_shared_chunks WHERE chunk_id IN (${placeholders})`).all(...chunkIds);
647
+ for (const r of teamMetaRows)
600
648
  sharingMap.set(r.chunk_id, { visibility: r.visibility, group_id: r.group_id });
601
- }
602
649
  }
603
650
  const localRows = db.prepare(`SELECT chunk_id, original_owner, shared_at FROM local_shared_memories WHERE chunk_id IN (${placeholders})`).all(...chunkIds);
604
651
  for (const r of localRows)
@@ -662,7 +709,7 @@ class ViewerServer {
662
709
  const db = this.store.db;
663
710
  const items = tasks.map((t) => {
664
711
  const meta = db.prepare("SELECT skill_status, owner FROM tasks WHERE id = ?").get(t.id);
665
- const sharedTask = db.prepare("SELECT visibility FROM hub_tasks WHERE source_task_id = ? ORDER BY updated_at DESC LIMIT 1").get(t.id);
712
+ const hubTask = this.getHubTaskForLocal(t.id);
666
713
  return {
667
714
  id: t.id,
668
715
  sessionKey: t.sessionKey,
@@ -674,7 +721,7 @@ class ViewerServer {
674
721
  chunkCount: this.store.countChunksByTask(t.id),
675
722
  skillStatus: meta?.skill_status ?? null,
676
723
  owner: meta?.owner ?? "agent:main",
677
- sharingVisibility: sharedTask?.visibility ?? null,
724
+ sharingVisibility: hubTask?.visibility ?? null,
678
725
  };
679
726
  });
680
727
  this.jsonResponse(res, { tasks: items, total, limit, offset });
@@ -703,7 +750,7 @@ class ViewerServer {
703
750
  }));
704
751
  const db = this.store.db;
705
752
  const meta = db.prepare("SELECT skill_status, skill_reason FROM tasks WHERE id = ?").get(taskId);
706
- const sharedTask = db.prepare("SELECT visibility, group_id FROM hub_tasks WHERE source_task_id = ? ORDER BY updated_at DESC LIMIT 1").get(taskId);
753
+ const hubTask = this.getHubTaskForLocal(taskId);
707
754
  this.jsonResponse(res, {
708
755
  id: task.id,
709
756
  sessionKey: task.sessionKey,
@@ -717,9 +764,9 @@ class ViewerServer {
717
764
  skillStatus: meta?.skill_status ?? null,
718
765
  skillReason: meta?.skill_reason ?? null,
719
766
  skillLinks,
720
- sharingVisibility: sharedTask?.visibility ?? null,
721
- sharingGroupId: sharedTask?.group_id ?? null,
722
- hubTaskId: sharedTask ? true : false,
767
+ sharingVisibility: hubTask?.visibility ?? null,
768
+ sharingGroupId: hubTask?.group_id ?? null,
769
+ hubTaskId: hubTask ? true : false,
723
770
  });
724
771
  }
725
772
  serveStats(res, url) {
@@ -924,10 +971,9 @@ class ViewerServer {
924
971
  if (visibility) {
925
972
  skills = skills.filter(s => s.visibility === visibility);
926
973
  }
927
- const db = this.store.db;
928
974
  const enriched = skills.map(s => {
929
- const hub = db.prepare("SELECT visibility FROM hub_skills WHERE source_skill_id = ? ORDER BY updated_at DESC LIMIT 1").get(s.id);
930
- return { ...s, sharingVisibility: hub?.visibility ?? null };
975
+ const hubSkill = this.getHubSkillForLocal(s.id);
976
+ return { ...s, sharingVisibility: hubSkill?.visibility ?? null };
931
977
  });
932
978
  this.jsonResponse(res, { skills: enriched });
933
979
  }
@@ -942,10 +988,9 @@ class ViewerServer {
942
988
  const versions = this.store.getSkillVersions(skillId);
943
989
  const relatedTasks = this.store.getTasksBySkill(skillId);
944
990
  const files = node_fs_1.default.existsSync(skill.dirPath) ? this.walkDir(skill.dirPath, skill.dirPath) : [];
945
- const db = this.store.db;
946
- const sharedSkill = db.prepare("SELECT visibility, group_id FROM hub_skills WHERE source_skill_id = ? ORDER BY updated_at DESC LIMIT 1").get(skillId);
991
+ const hubSkill = this.getHubSkillForLocal(skillId);
947
992
  this.jsonResponse(res, {
948
- skill: { ...skill, sharingVisibility: sharedSkill?.visibility ?? null, sharingGroupId: sharedSkill?.group_id ?? null },
993
+ skill: { ...skill, sharingVisibility: hubSkill?.visibility ?? null, sharingGroupId: hubSkill?.group_id ?? null },
949
994
  versions: versions.map(v => ({
950
995
  id: v.id,
951
996
  version: v.version,
@@ -1081,7 +1126,7 @@ class ViewerServer {
1081
1126
  method: "POST",
1082
1127
  body: JSON.stringify({ visibility: "public", groupId: null, metadata: bundle.metadata, bundle: bundle.bundle }),
1083
1128
  });
1084
- if (hubClient.userId) {
1129
+ if (this.sharingRole === "hub" && hubClient.userId) {
1085
1130
  const existing = this.store.getHubSkillBySource(hubClient.userId, skillId);
1086
1131
  this.store.upsertHubSkill({
1087
1132
  id: response?.skillId ?? existing?.id ?? node_crypto_1.default.randomUUID(),
@@ -1092,6 +1137,15 @@ class ViewerServer {
1092
1137
  createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
1093
1138
  });
1094
1139
  }
1140
+ else {
1141
+ const conn = this.store.getClientHubConnection();
1142
+ this.store.upsertTeamSharedSkill(skillId, {
1143
+ hubSkillId: String(response?.skillId ?? ""),
1144
+ visibility: "public",
1145
+ groupId: null,
1146
+ hubInstanceId: conn?.hubInstanceId ?? "",
1147
+ });
1148
+ }
1095
1149
  hubSynced = true;
1096
1150
  this.log.info(`Skill "${skill.name}" published to Hub`);
1097
1151
  }
@@ -1100,8 +1154,10 @@ class ViewerServer {
1100
1154
  method: "POST",
1101
1155
  body: JSON.stringify({ sourceSkillId: skillId }),
1102
1156
  });
1103
- if (hubClient.userId)
1157
+ if (this.sharingRole === "hub" && hubClient.userId)
1104
1158
  this.store.deleteHubSkillBySource(hubClient.userId, skillId);
1159
+ else
1160
+ this.store.deleteTeamSharedSkill(skillId);
1105
1161
  hubSynced = true;
1106
1162
  this.log.info(`Skill "${skill.name}" unpublished from Hub`);
1107
1163
  }
@@ -1399,7 +1455,8 @@ class ViewerServer {
1399
1455
  });
1400
1456
  }
1401
1457
  else if (hubClient.userId) {
1402
- this.store.upsertTeamSharedChunk(chunkId, { hubMemoryId: memoryId, visibility: "public", groupId: null });
1458
+ const conn = this.store.getClientHubConnection();
1459
+ this.store.upsertTeamSharedChunk(chunkId, { hubMemoryId: memoryId, visibility: "public", groupId: null, hubInstanceId: conn?.hubInstanceId ?? "" });
1403
1460
  }
1404
1461
  hubSynced = true;
1405
1462
  }
@@ -1415,7 +1472,7 @@ class ViewerServer {
1415
1472
  await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/unshare", {
1416
1473
  method: "POST", body: JSON.stringify({ sourceChunkId: chunkId }),
1417
1474
  });
1418
- if (hubClient.userId)
1475
+ if (this.sharingRole === "hub" && hubClient.userId)
1419
1476
  this.store.deleteHubMemoryBySource(hubClient.userId, chunkId);
1420
1477
  this.store.deleteTeamSharedChunk(chunkId);
1421
1478
  hubSynced = true;
@@ -1434,7 +1491,7 @@ class ViewerServer {
1434
1491
  await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/unshare", {
1435
1492
  method: "POST", body: JSON.stringify({ sourceChunkId: chunkId }),
1436
1493
  });
1437
- if (hubClient.userId)
1494
+ if (this.sharingRole === "hub" && hubClient.userId)
1438
1495
  this.store.deleteHubMemoryBySource(hubClient.userId, chunkId);
1439
1496
  this.store.deleteTeamSharedChunk(chunkId);
1440
1497
  hubSynced = true;
@@ -1488,21 +1545,24 @@ class ViewerServer {
1488
1545
  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() })),
1489
1546
  }),
1490
1547
  });
1491
- if (hubClient.userId) {
1548
+ const hubTaskId = String(response?.taskId ?? "");
1549
+ if (this.sharingRole === "hub" && hubClient.userId) {
1492
1550
  const existing = this.store.getHubTaskBySource(hubClient.userId, taskId);
1493
1551
  this.store.upsertHubTask({
1494
- id: response?.taskId ?? existing?.id ?? node_crypto_1.default.randomUUID(),
1552
+ id: hubTaskId || existing?.id || node_crypto_1.default.randomUUID(),
1495
1553
  sourceTaskId: taskId, sourceUserId: hubClient.userId, title: refreshedTask.title ?? "",
1496
1554
  summary: refreshedTask.summary ?? "", groupId: null, visibility: "public",
1497
1555
  createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
1498
1556
  });
1499
1557
  }
1558
+ const conn = this.store.getClientHubConnection();
1559
+ this.store.markTaskShared(taskId, hubTaskId, chunks.length, "public", null, conn?.hubInstanceId ?? "");
1500
1560
  hubSynced = true;
1501
1561
  }
1502
1562
  if (!isLocalShared) {
1503
1563
  const originalOwner = task.owner;
1504
1564
  const db = this.store.db;
1505
- 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());
1565
+ 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());
1506
1566
  db.prepare("UPDATE tasks SET owner = 'public' WHERE id = ?").run(taskId);
1507
1567
  }
1508
1568
  }
@@ -1520,8 +1580,10 @@ class ViewerServer {
1520
1580
  await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/unshare", {
1521
1581
  method: "POST", body: JSON.stringify({ sourceTaskId: taskId }),
1522
1582
  });
1523
- if (hubClient.userId)
1583
+ if (this.sharingRole === "hub" && hubClient.userId)
1524
1584
  this.store.deleteHubTaskBySource(hubClient.userId, taskId);
1585
+ else
1586
+ this.store.downgradeTeamSharedTaskToLocal(taskId);
1525
1587
  hubSynced = true;
1526
1588
  }
1527
1589
  catch (err) {
@@ -1535,8 +1597,10 @@ class ViewerServer {
1535
1597
  await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/unshare", {
1536
1598
  method: "POST", body: JSON.stringify({ sourceTaskId: taskId }),
1537
1599
  });
1538
- if (hubClient.userId)
1600
+ if (this.sharingRole === "hub" && hubClient.userId)
1539
1601
  this.store.deleteHubTaskBySource(hubClient.userId, taskId);
1602
+ else if (!isLocalShared)
1603
+ this.store.unmarkTaskShared(taskId);
1540
1604
  hubSynced = true;
1541
1605
  }
1542
1606
  catch (err) {
@@ -1591,10 +1655,11 @@ class ViewerServer {
1591
1655
  method: "POST",
1592
1656
  body: JSON.stringify({ visibility: "public", groupId: null, metadata: bundle.metadata, bundle: bundle.bundle }),
1593
1657
  });
1594
- if (hubClient.userId) {
1658
+ const hubSkillId = String(response?.skillId ?? "");
1659
+ if (this.sharingRole === "hub" && hubClient.userId) {
1595
1660
  const existing = this.store.getHubSkillBySource(hubClient.userId, skillId);
1596
1661
  this.store.upsertHubSkill({
1597
- id: response?.skillId ?? existing?.id ?? node_crypto_1.default.randomUUID(),
1662
+ id: hubSkillId || existing?.id || node_crypto_1.default.randomUUID(),
1598
1663
  sourceSkillId: skillId, sourceUserId: hubClient.userId,
1599
1664
  name: skill.name, description: skill.description, version: skill.version,
1600
1665
  groupId: null, visibility: "public",
@@ -1602,6 +1667,10 @@ class ViewerServer {
1602
1667
  createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
1603
1668
  });
1604
1669
  }
1670
+ else {
1671
+ const conn = this.store.getClientHubConnection();
1672
+ this.store.upsertTeamSharedSkill(skillId, { hubSkillId, visibility: "public", groupId: null, hubInstanceId: conn?.hubInstanceId ?? "" });
1673
+ }
1605
1674
  hubSynced = true;
1606
1675
  }
1607
1676
  if (!isLocalShared)
@@ -1617,8 +1686,10 @@ class ViewerServer {
1617
1686
  await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/unpublish", {
1618
1687
  method: "POST", body: JSON.stringify({ sourceSkillId: skillId }),
1619
1688
  });
1620
- if (hubClient.userId)
1689
+ if (this.sharingRole === "hub" && hubClient.userId)
1621
1690
  this.store.deleteHubSkillBySource(hubClient.userId, skillId);
1691
+ else
1692
+ this.store.deleteTeamSharedSkill(skillId);
1622
1693
  hubSynced = true;
1623
1694
  }
1624
1695
  catch (err) {
@@ -1632,8 +1703,10 @@ class ViewerServer {
1632
1703
  await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/unpublish", {
1633
1704
  method: "POST", body: JSON.stringify({ sourceSkillId: skillId }),
1634
1705
  });
1635
- if (hubClient.userId)
1706
+ if (this.sharingRole === "hub" && hubClient.userId)
1636
1707
  this.store.deleteHubSkillBySource(hubClient.userId, skillId);
1708
+ else
1709
+ this.store.deleteTeamSharedSkill(skillId);
1637
1710
  hubSynced = true;
1638
1711
  }
1639
1712
  catch (err) {
@@ -1650,28 +1723,52 @@ class ViewerServer {
1650
1723
  }
1651
1724
  });
1652
1725
  }
1726
+ get sharingRole() {
1727
+ return this.ctx?.config?.sharing?.role;
1728
+ }
1729
+ isCurrentClientHubInstance(hubInstanceId) {
1730
+ if (this.sharingRole !== "client")
1731
+ return true;
1732
+ const scopedHubInstanceId = String(hubInstanceId ?? "");
1733
+ if (!scopedHubInstanceId)
1734
+ return true;
1735
+ const currentHubInstanceId = this.store.getClientHubConnection()?.hubInstanceId ?? "";
1736
+ if (!currentHubInstanceId)
1737
+ return true;
1738
+ return scopedHubInstanceId === currentHubInstanceId;
1739
+ }
1653
1740
  getHubMemoryForChunk(chunkId) {
1654
- const db = this.store.db;
1655
- const hub = db.prepare("SELECT * FROM hub_memories WHERE source_chunk_id = ? LIMIT 1").get(chunkId);
1656
- if (hub)
1657
- return hub;
1741
+ if (this.sharingRole === "hub") {
1742
+ const db = this.store.db;
1743
+ return db.prepare("SELECT * FROM hub_memories WHERE source_chunk_id = ? LIMIT 1").get(chunkId);
1744
+ }
1658
1745
  const ts = this.store.getTeamSharedChunk(chunkId);
1659
- if (ts) {
1660
- return {
1661
- source_chunk_id: chunkId,
1662
- visibility: ts.visibility,
1663
- group_id: ts.groupId,
1664
- };
1746
+ if (ts && this.isCurrentClientHubInstance(ts.hubInstanceId)) {
1747
+ return { source_chunk_id: chunkId, visibility: ts.visibility, group_id: ts.groupId };
1665
1748
  }
1666
1749
  return undefined;
1667
1750
  }
1668
1751
  getHubTaskForLocal(taskId) {
1669
- const db = this.store.db;
1670
- return db.prepare("SELECT * FROM hub_tasks WHERE source_task_id = ? LIMIT 1").get(taskId);
1752
+ if (this.sharingRole === "hub") {
1753
+ const db = this.store.db;
1754
+ return db.prepare("SELECT * FROM hub_tasks WHERE source_task_id = ? LIMIT 1").get(taskId);
1755
+ }
1756
+ const shared = this.store.getLocalSharedTask(taskId);
1757
+ if (shared && shared.hubTaskId && this.isCurrentClientHubInstance(shared.hubInstanceId)) {
1758
+ return { source_task_id: taskId, visibility: shared.visibility, group_id: shared.groupId };
1759
+ }
1760
+ return undefined;
1671
1761
  }
1672
1762
  getHubSkillForLocal(skillId) {
1673
- const db = this.store.db;
1674
- return db.prepare("SELECT * FROM hub_skills WHERE source_skill_id = ? LIMIT 1").get(skillId);
1763
+ if (this.sharingRole === "hub") {
1764
+ const db = this.store.db;
1765
+ return db.prepare("SELECT * FROM hub_skills WHERE source_skill_id = ? LIMIT 1").get(skillId);
1766
+ }
1767
+ const ts = this.store.getTeamSharedSkill(skillId);
1768
+ if (ts && this.isCurrentClientHubInstance(ts.hubInstanceId)) {
1769
+ return { source_skill_id: skillId, visibility: ts.visibility, group_id: ts.groupId };
1770
+ }
1771
+ return undefined;
1675
1772
  }
1676
1773
  handleDeleteSession(res, url) {
1677
1774
  const key = url.searchParams.get("key");
@@ -1973,18 +2070,24 @@ class ViewerServer {
1973
2070
  handleRetryJoin(req, res) {
1974
2071
  this.readBody(req, async (_body) => {
1975
2072
  if (!this.ctx)
1976
- return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
2073
+ return this.jsonResponse(res, { ok: false, error: "sharing_unavailable", errorCode: "sharing_unavailable" });
1977
2074
  const sharing = this.ctx.config.sharing;
1978
2075
  if (!sharing?.enabled || sharing.role !== "client") {
1979
- return this.jsonResponse(res, { ok: false, error: "not_in_client_mode" });
2076
+ return this.jsonResponse(res, { ok: false, error: "not_in_client_mode", errorCode: "not_in_client_mode" });
1980
2077
  }
1981
2078
  const hubAddress = sharing.client?.hubAddress ?? "";
1982
2079
  const teamToken = sharing.client?.teamToken ?? "";
1983
2080
  if (!hubAddress || !teamToken) {
1984
- return this.jsonResponse(res, { ok: false, error: "missing_hub_address_or_team_token" });
2081
+ return this.jsonResponse(res, { ok: false, error: "missing_hub_address_or_team_token", errorCode: "missing_config" });
2082
+ }
2083
+ const hubUrl = (0, hub_1.normalizeHubUrl)(hubAddress);
2084
+ try {
2085
+ await (0, hub_1.hubRequestJson)(hubUrl, "", "/api/v1/hub/info", { method: "GET" });
2086
+ }
2087
+ catch {
2088
+ return this.jsonResponse(res, { ok: false, error: "hub_unreachable", errorCode: "hub_unreachable" });
1985
2089
  }
1986
2090
  try {
1987
- const hubUrl = (0, hub_1.normalizeHubUrl)(hubAddress);
1988
2091
  const os = await Promise.resolve().then(() => __importStar(require("os")));
1989
2092
  const nickname = sharing.client?.nickname;
1990
2093
  const username = nickname || os.userInfo().username || "user";
@@ -1996,6 +2099,12 @@ class ViewerServer {
1996
2099
  body: JSON.stringify({ teamToken, username, deviceName: hostname, reapply: true, identityKey: existingIdentityKey }),
1997
2100
  });
1998
2101
  const returnedIdentityKey = String(result.identityKey || existingIdentityKey || "");
2102
+ let hubInstanceId = persisted?.hubInstanceId || "";
2103
+ try {
2104
+ const info = await (0, hub_1.hubRequestJson)(hubUrl, "", "/api/v1/hub/info", { method: "GET" });
2105
+ hubInstanceId = String(info?.hubInstanceId ?? hubInstanceId);
2106
+ }
2107
+ catch { /* best-effort */ }
1999
2108
  this.store.setClientHubConnection({
2000
2109
  hubUrl,
2001
2110
  userId: String(result.userId || ""),
@@ -2005,11 +2114,22 @@ class ViewerServer {
2005
2114
  connectedAt: Date.now(),
2006
2115
  identityKey: returnedIdentityKey,
2007
2116
  lastKnownStatus: result.status || "",
2117
+ hubInstanceId,
2008
2118
  });
2119
+ if (result.status === "blocked") {
2120
+ return this.jsonResponse(res, { ok: false, error: "blocked", errorCode: "blocked" });
2121
+ }
2009
2122
  this.jsonResponse(res, { ok: true, status: result.status || "pending" });
2010
2123
  }
2011
2124
  catch (err) {
2012
- this.jsonResponse(res, { ok: false, error: String(err) });
2125
+ const errStr = String(err);
2126
+ if (errStr.includes("(409)") || errStr.includes("username_taken")) {
2127
+ return this.jsonResponse(res, { ok: false, error: "username_taken", errorCode: "username_taken" });
2128
+ }
2129
+ if (errStr.includes("(403)") || errStr.includes("invalid_team_token")) {
2130
+ return this.jsonResponse(res, { ok: false, error: "invalid_team_token", errorCode: "invalid_team_token" });
2131
+ }
2132
+ this.jsonResponse(res, { ok: false, error: errStr, errorCode: "unknown" });
2013
2133
  }
2014
2134
  });
2015
2135
  }
@@ -2227,9 +2347,10 @@ class ViewerServer {
2227
2347
  }),
2228
2348
  });
2229
2349
  const hubUserId = hubClient.userId;
2230
- if (hubUserId) {
2350
+ const hubTaskId = String(response?.taskId ?? task.id);
2351
+ if (this.sharingRole === "hub" && hubUserId) {
2231
2352
  this.store.upsertHubTask({
2232
- id: task.id,
2353
+ id: hubTaskId,
2233
2354
  sourceTaskId: task.id,
2234
2355
  sourceUserId: hubUserId,
2235
2356
  title: task.title,
@@ -2240,6 +2361,10 @@ class ViewerServer {
2240
2361
  updatedAt: task.updatedAt ?? Date.now(),
2241
2362
  });
2242
2363
  }
2364
+ else {
2365
+ const conn = this.store.getClientHubConnection();
2366
+ this.store.markTaskShared(task.id, hubTaskId, chunks.length, visibility, groupId, conn?.hubInstanceId ?? "");
2367
+ }
2243
2368
  this.jsonResponse(res, { ok: true, taskId, visibility, response });
2244
2369
  }
2245
2370
  catch (err) {
@@ -2263,8 +2388,12 @@ class ViewerServer {
2263
2388
  body: JSON.stringify({ sourceTaskId: task.id }),
2264
2389
  });
2265
2390
  const hubUserId = hubClient.userId;
2266
- if (hubUserId)
2391
+ if (this.sharingRole === "hub" && hubUserId)
2267
2392
  this.store.deleteHubTaskBySource(hubUserId, task.id);
2393
+ else if (task.owner === "public")
2394
+ this.store.downgradeTeamSharedTaskToLocal(task.id);
2395
+ else
2396
+ this.store.unmarkTaskShared(task.id);
2268
2397
  this.jsonResponse(res, { ok: true, taskId });
2269
2398
  }
2270
2399
  catch (err) {
@@ -2319,7 +2448,8 @@ class ViewerServer {
2319
2448
  });
2320
2449
  }
2321
2450
  else if (hubClient.userId) {
2322
- this.store.upsertTeamSharedChunk(chunk.id, { hubMemoryId: mid, visibility, groupId });
2451
+ const conn = this.store.getClientHubConnection();
2452
+ this.store.upsertTeamSharedChunk(chunk.id, { hubMemoryId: mid, visibility, groupId, hubInstanceId: conn?.hubInstanceId ?? "" });
2323
2453
  }
2324
2454
  this.jsonResponse(res, { ok: true, chunkId, visibility, response });
2325
2455
  }
@@ -2341,9 +2471,10 @@ class ViewerServer {
2341
2471
  body: JSON.stringify({ sourceChunkId: chunkId }),
2342
2472
  });
2343
2473
  const hubUserId = hubClient.userId;
2344
- if (hubUserId)
2474
+ if (this.sharingRole === "hub" && hubUserId)
2345
2475
  this.store.deleteHubMemoryBySource(hubUserId, chunkId);
2346
- this.store.deleteTeamSharedChunk(chunkId);
2476
+ else
2477
+ this.store.deleteTeamSharedChunk(chunkId);
2347
2478
  this.jsonResponse(res, { ok: true, chunkId });
2348
2479
  }
2349
2480
  catch (err) {
@@ -2391,7 +2522,7 @@ class ViewerServer {
2391
2522
  }),
2392
2523
  });
2393
2524
  const hubUserId = hubClient.userId;
2394
- if (hubUserId) {
2525
+ if (this.sharingRole === "hub" && hubUserId) {
2395
2526
  const existing = this.store.getHubSkillBySource(hubUserId, skillId);
2396
2527
  this.store.upsertHubSkill({
2397
2528
  id: response?.skillId ?? existing?.id ?? node_crypto_1.default.randomUUID(),
@@ -2408,6 +2539,15 @@ class ViewerServer {
2408
2539
  updatedAt: Date.now(),
2409
2540
  });
2410
2541
  }
2542
+ else {
2543
+ const conn = this.store.getClientHubConnection();
2544
+ this.store.upsertTeamSharedSkill(skillId, {
2545
+ hubSkillId: String(response?.skillId ?? ""),
2546
+ visibility,
2547
+ groupId,
2548
+ hubInstanceId: conn?.hubInstanceId ?? "",
2549
+ });
2550
+ }
2411
2551
  this.jsonResponse(res, { ok: true, skillId, visibility, response });
2412
2552
  }
2413
2553
  catch (err) {
@@ -2431,8 +2571,10 @@ class ViewerServer {
2431
2571
  body: JSON.stringify({ sourceSkillId: skill.id }),
2432
2572
  });
2433
2573
  const hubUserId = hubClient.userId;
2434
- if (hubUserId)
2574
+ if (this.sharingRole === "hub" && hubUserId)
2435
2575
  this.store.deleteHubSkillBySource(hubUserId, skill.id);
2576
+ else
2577
+ this.store.deleteTeamSharedSkill(skill.id);
2436
2578
  this.jsonResponse(res, { ok: true, skillId });
2437
2579
  }
2438
2580
  catch (err) {
@@ -2939,18 +3081,20 @@ class ViewerServer {
2939
3081
  const isClient = newEnabled && newRole === "client";
2940
3082
  if (wasClient && !isClient) {
2941
3083
  await this.withdrawOrLeaveHub();
3084
+ this.store.clearAllTeamSharingState();
2942
3085
  this.store.clearClientHubConnection();
2943
- this.log.info("Client hub connection cleared (sharing disabled or role changed)");
3086
+ this.log.info("Client hub connection and team sharing state cleared (sharing disabled or role changed)");
2944
3087
  }
2945
3088
  if (wasClient && isClient) {
2946
3089
  const newClientAddr = String(merged.client?.hubAddress || "");
2947
3090
  if (newClientAddr && oldClientHubAddress && (0, hub_1.normalizeHubUrl)(newClientAddr) !== (0, hub_1.normalizeHubUrl)(oldClientHubAddress)) {
2948
3091
  this.notifyHubLeave();
3092
+ this.store.clearAllTeamSharingState();
2949
3093
  const oldConn = this.store.getClientHubConnection();
2950
3094
  if (oldConn) {
2951
- this.store.setClientHubConnection({ ...oldConn, hubUrl: (0, hub_1.normalizeHubUrl)(newClientAddr), userToken: "", lastKnownStatus: "hub_changed" });
3095
+ this.store.setClientHubConnection({ ...oldConn, hubUrl: (0, hub_1.normalizeHubUrl)(newClientAddr), userToken: "", hubInstanceId: "", lastKnownStatus: "hub_changed" });
2952
3096
  }
2953
- this.log.info("Client hub connection token cleared (switched to different Hub), identity preserved");
3097
+ this.log.info("Client hub connection and team sharing state cleared (switched to different Hub)");
2954
3098
  }
2955
3099
  }
2956
3100
  if (merged.role === "hub") {
@@ -2970,24 +3114,24 @@ class ViewerServer {
2970
3114
  const nowClient = Boolean(finalSharing?.enabled) && finalSharing?.role === "client";
2971
3115
  const previouslyClient = oldSharingEnabled && oldSharingRole === "client";
2972
3116
  let joinStatus;
3117
+ let joinError;
2973
3118
  if (nowClient && !previouslyClient) {
2974
3119
  try {
2975
3120
  joinStatus = await this.autoJoinOnSave(finalSharing);
2976
3121
  }
2977
3122
  catch (e) {
2978
- this.log.warn(`Auto-join on save failed: ${e}`);
3123
+ const msg = String(e instanceof Error ? e.message : e);
3124
+ this.log.warn(`Auto-join on save failed: ${msg}`);
3125
+ if (msg === "hub_unreachable" || msg === "username_taken" || msg === "invalid_team_token") {
3126
+ joinError = msg;
3127
+ }
2979
3128
  }
2980
3129
  }
2981
- this.jsonResponse(res, { ok: true, joinStatus, restart: true });
2982
- setTimeout(() => {
2983
- this.log.info("config-save: triggering gateway restart via SIGUSR1...");
2984
- try {
2985
- process.kill(process.pid, "SIGUSR1");
2986
- }
2987
- catch (sig) {
2988
- this.log.warn(`SIGUSR1 failed: ${sig}`);
2989
- }
2990
- }, 500);
3130
+ if (joinError) {
3131
+ this.jsonResponse(res, { ok: true, joinError, restart: false });
3132
+ return;
3133
+ }
3134
+ this.jsonResponseAndRestart(res, { ok: true, joinStatus, restart: true }, "config-save");
2991
3135
  }
2992
3136
  catch (e) {
2993
3137
  this.log.warn(`handleSaveConfig error: ${e}`);
@@ -3003,17 +3147,42 @@ class ViewerServer {
3003
3147
  if (!hubAddress || !teamToken)
3004
3148
  return undefined;
3005
3149
  const hubUrl = (0, hub_1.normalizeHubUrl)(hubAddress);
3150
+ try {
3151
+ await (0, hub_1.hubRequestJson)(hubUrl, "", "/api/v1/hub/info", { method: "GET" });
3152
+ }
3153
+ catch {
3154
+ throw new Error("hub_unreachable");
3155
+ }
3006
3156
  const os = await Promise.resolve().then(() => __importStar(require("os")));
3007
3157
  const nickname = String(clientCfg?.nickname || "");
3008
3158
  const username = nickname || os.userInfo().username || "user";
3009
3159
  const hostname = os.hostname() || "unknown";
3010
3160
  const persisted = this.store.getClientHubConnection();
3011
3161
  const existingIdentityKey = persisted?.identityKey || "";
3012
- const result = await (0, hub_1.hubRequestJson)(hubUrl, "", "/api/v1/hub/join", {
3013
- method: "POST",
3014
- body: JSON.stringify({ teamToken, username, deviceName: hostname, identityKey: existingIdentityKey }),
3015
- });
3162
+ let result;
3163
+ try {
3164
+ result = await (0, hub_1.hubRequestJson)(hubUrl, "", "/api/v1/hub/join", {
3165
+ method: "POST",
3166
+ body: JSON.stringify({ teamToken, username, deviceName: hostname, identityKey: existingIdentityKey }),
3167
+ });
3168
+ }
3169
+ catch (err) {
3170
+ const errStr = String(err);
3171
+ if (errStr.includes("(409)") || errStr.includes("username_taken")) {
3172
+ throw new Error("username_taken");
3173
+ }
3174
+ if (errStr.includes("(403)") || errStr.includes("invalid_team_token")) {
3175
+ throw new Error("invalid_team_token");
3176
+ }
3177
+ throw err;
3178
+ }
3016
3179
  const returnedIdentityKey = String(result.identityKey || existingIdentityKey || "");
3180
+ let hubInstanceId = persisted?.hubInstanceId || "";
3181
+ try {
3182
+ const info = await (0, hub_1.hubRequestJson)(hubUrl, "", "/api/v1/hub/info", { method: "GET" });
3183
+ hubInstanceId = String(info?.hubInstanceId ?? hubInstanceId);
3184
+ }
3185
+ catch { /* best-effort */ }
3017
3186
  this.store.setClientHubConnection({
3018
3187
  hubUrl,
3019
3188
  userId: String(result.userId || ""),
@@ -3023,6 +3192,7 @@ class ViewerServer {
3023
3192
  connectedAt: Date.now(),
3024
3193
  identityKey: returnedIdentityKey,
3025
3194
  lastKnownStatus: result.status || "",
3195
+ hubInstanceId,
3026
3196
  });
3027
3197
  this.log.info(`Auto-join on save: status=${result.status}, userId=${result.userId}`);
3028
3198
  if (result.userToken) {
@@ -3034,6 +3204,7 @@ class ViewerServer {
3034
3204
  this.readBody(_req, async () => {
3035
3205
  try {
3036
3206
  await this.withdrawOrLeaveHub();
3207
+ this.store.clearAllTeamSharingState();
3037
3208
  this.store.clearClientHubConnection();
3038
3209
  const configPath = this.getOpenClawConfigPath();
3039
3210
  if (configPath && node_fs_1.default.existsSync(configPath)) {
@@ -3050,16 +3221,7 @@ class ViewerServer {
3050
3221
  this.log.info("handleLeaveTeam: config updated, sharing disabled");
3051
3222
  }
3052
3223
  }
3053
- this.jsonResponse(res, { ok: true, restart: true });
3054
- setTimeout(() => {
3055
- this.log.info("handleLeaveTeam: triggering gateway restart via SIGUSR1...");
3056
- try {
3057
- process.kill(process.pid, "SIGUSR1");
3058
- }
3059
- catch (sig) {
3060
- this.log.warn(`SIGUSR1 failed: ${sig}`);
3061
- }
3062
- }, 500);
3224
+ this.jsonResponseAndRestart(res, { ok: true, restart: true }, "handleLeaveTeam");
3063
3225
  }
3064
3226
  catch (e) {
3065
3227
  this.log.warn(`handleLeaveTeam error: ${e}`);
@@ -3233,17 +3395,45 @@ class ViewerServer {
3233
3395
  }
3234
3396
  }
3235
3397
  catch { }
3236
- const url = hubUrl.replace(/\/+$/, "") + "/api/v1/hub/info";
3398
+ const baseUrl = hubUrl.replace(/\/+$/, "");
3399
+ const infoUrl = baseUrl + "/api/v1/hub/info";
3237
3400
  const ctrl = new AbortController();
3238
3401
  const timeout = setTimeout(() => ctrl.abort(), 8000);
3239
3402
  try {
3240
- const r = await fetch(url, { signal: ctrl.signal });
3403
+ const r = await fetch(infoUrl, { signal: ctrl.signal });
3241
3404
  clearTimeout(timeout);
3242
3405
  if (!r.ok) {
3243
3406
  this.jsonResponse(res, { ok: false, error: `HTTP ${r.status}` });
3244
3407
  return;
3245
3408
  }
3246
3409
  const info = await r.json();
3410
+ const { teamToken, nickname } = JSON.parse(body);
3411
+ if (teamToken) {
3412
+ const username = (typeof nickname === "string" && nickname.trim()) || node_os_1.default.userInfo().username || "user";
3413
+ const persisted = this.store.getClientHubConnection();
3414
+ const identityKey = persisted?.identityKey || "";
3415
+ try {
3416
+ const joinR = await fetch(baseUrl + "/api/v1/hub/join", {
3417
+ method: "POST",
3418
+ headers: { "content-type": "application/json" },
3419
+ body: JSON.stringify({ teamToken, username, identityKey, deviceName: node_os_1.default.hostname(), dryRun: true }),
3420
+ });
3421
+ const joinData = await joinR.json();
3422
+ if (!joinR.ok && joinData.error === "username_taken") {
3423
+ this.jsonResponse(res, { ok: false, error: "username_taken", teamName: info.teamName || "" });
3424
+ return;
3425
+ }
3426
+ if (!joinR.ok && joinData.error === "invalid_team_token") {
3427
+ this.jsonResponse(res, { ok: false, error: "invalid_team_token", teamName: info.teamName || "" });
3428
+ return;
3429
+ }
3430
+ if (joinR.ok && joinData.status === "blocked") {
3431
+ this.jsonResponse(res, { ok: false, error: "blocked", teamName: info.teamName || "" });
3432
+ return;
3433
+ }
3434
+ }
3435
+ catch { /* join check is best-effort; connection itself is OK */ }
3436
+ }
3247
3437
  this.jsonResponse(res, { ok: true, teamName: info.teamName || "", apiVersion: info.apiVersion || "" });
3248
3438
  }
3249
3439
  catch (e) {
@@ -3330,26 +3520,35 @@ class ViewerServer {
3330
3520
  return null;
3331
3521
  }
3332
3522
  async handleUpdateCheck(res) {
3523
+ const sendNoStore = (data, statusCode = 200) => {
3524
+ res.writeHead(statusCode, {
3525
+ "Content-Type": "application/json; charset=utf-8",
3526
+ "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
3527
+ "Pragma": "no-cache",
3528
+ "Expires": "0",
3529
+ });
3530
+ res.end(JSON.stringify(data));
3531
+ };
3333
3532
  try {
3334
3533
  const pkgPath = this.findPluginPackageJson();
3335
3534
  if (!pkgPath) {
3336
- this.jsonResponse(res, { updateAvailable: false, error: "package.json not found" });
3535
+ sendNoStore({ updateAvailable: false, error: "package.json not found" });
3337
3536
  return;
3338
3537
  }
3339
3538
  const pkg = JSON.parse(node_fs_1.default.readFileSync(pkgPath, "utf-8"));
3340
3539
  const current = pkg.version;
3341
3540
  const name = pkg.name;
3342
3541
  if (!current || !name) {
3343
- this.jsonResponse(res, { updateAvailable: false, current });
3542
+ sendNoStore({ updateAvailable: false, current });
3344
3543
  return;
3345
3544
  }
3346
3545
  const { computeUpdateCheck } = await Promise.resolve().then(() => __importStar(require("../update-check")));
3347
3546
  const result = await computeUpdateCheck(name, current, fetch, 6_000);
3348
3547
  if (!result) {
3349
- this.jsonResponse(res, { updateAvailable: false, current, packageName: name });
3548
+ sendNoStore({ updateAvailable: false, current, packageName: name });
3350
3549
  return;
3351
3550
  }
3352
- this.jsonResponse(res, {
3551
+ sendNoStore({
3353
3552
  updateAvailable: result.updateAvailable,
3354
3553
  current: result.current,
3355
3554
  latest: result.latest,
@@ -3361,7 +3560,7 @@ class ViewerServer {
3361
3560
  }
3362
3561
  catch (e) {
3363
3562
  this.log.warn(`handleUpdateCheck error: ${e}`);
3364
- this.jsonResponse(res, { updateAvailable: false, error: String(e) });
3563
+ sendNoStore({ updateAvailable: false, error: String(e) });
3365
3564
  }
3366
3565
  }
3367
3566
  handleUpdateInstall(req, res) {
@@ -3369,13 +3568,14 @@ class ViewerServer {
3369
3568
  req.on("data", (chunk) => { body += chunk.toString(); });
3370
3569
  req.on("end", () => {
3371
3570
  try {
3372
- const { packageSpec: rawSpec } = JSON.parse(body);
3571
+ const { packageSpec: rawSpec, targetVersion: rawTargetVersion } = JSON.parse(body);
3373
3572
  if (!rawSpec || typeof rawSpec !== "string") {
3374
3573
  res.writeHead(400, { "Content-Type": "application/json" });
3375
3574
  res.end(JSON.stringify({ ok: false, error: "Missing packageSpec" }));
3376
3575
  return;
3377
3576
  }
3378
3577
  const packageSpec = rawSpec.trim().replace(/^(?:npx\s+)?openclaw\s+plugins\s+install\s+/i, "");
3578
+ const targetVersion = typeof rawTargetVersion === "string" ? rawTargetVersion.trim() : "";
3379
3579
  const allowed = /^@[\w-]+\/[\w.-]+(@[\w.-]+)?$/;
3380
3580
  this.log.info(`update-install: received packageSpec="${packageSpec}" (len=${packageSpec.length})`);
3381
3581
  if (!allowed.test(packageSpec)) {
@@ -3396,18 +3596,50 @@ class ViewerServer {
3396
3596
  const shortName = pluginName?.replace(/^@[\w-]+\//, "") ?? "memos-local-openclaw-plugin";
3397
3597
  const extDir = node_path_1.default.join(node_os_1.default.homedir(), ".openclaw", "extensions", shortName);
3398
3598
  const tmpDir = node_path_1.default.join(node_os_1.default.tmpdir(), `openclaw-update-${Date.now()}`);
3599
+ const backupDir = node_path_1.default.join(node_path_1.default.dirname(extDir), `${shortName}.backup-${Date.now()}`);
3600
+ let backupReady = false;
3601
+ const cleanupTmpDir = () => {
3602
+ try {
3603
+ node_fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
3604
+ }
3605
+ catch { }
3606
+ };
3607
+ const rollbackInstall = () => {
3608
+ try {
3609
+ node_fs_1.default.rmSync(extDir, { recursive: true, force: true });
3610
+ }
3611
+ catch { }
3612
+ if (!backupReady)
3613
+ return;
3614
+ try {
3615
+ node_fs_1.default.renameSync(backupDir, extDir);
3616
+ backupReady = false;
3617
+ this.log.info(`update-install: restored previous version from ${backupDir}`);
3618
+ }
3619
+ catch (restoreErr) {
3620
+ this.log.warn(`update-install: failed to restore previous version: ${restoreErr?.message ?? restoreErr}`);
3621
+ }
3622
+ };
3623
+ const discardBackup = () => {
3624
+ if (!backupReady)
3625
+ return;
3626
+ try {
3627
+ node_fs_1.default.rmSync(backupDir, { recursive: true, force: true });
3628
+ backupReady = false;
3629
+ }
3630
+ catch (cleanupErr) {
3631
+ this.log.warn(`update-install: failed to remove backup dir ${backupDir}: ${cleanupErr?.message ?? cleanupErr}`);
3632
+ }
3633
+ };
3399
3634
  // Download via npm pack, extract, and replace extension dir.
3400
3635
  // Does NOT touch openclaw.json → no config watcher SIGUSR1.
3401
3636
  this.log.info(`update-install: downloading ${packageSpec} via npm pack...`);
3402
3637
  node_fs_1.default.mkdirSync(tmpDir, { recursive: true });
3403
- (0, node_child_process_1.exec)(`npm pack ${packageSpec} --pack-destination ${tmpDir}`, { timeout: 60_000 }, (packErr, packOut) => {
3638
+ (0, node_child_process_1.exec)(`npm pack ${packageSpec} --pack-destination ${tmpDir} --prefer-online`, { timeout: 60_000 }, (packErr, packOut) => {
3404
3639
  if (packErr) {
3405
3640
  this.log.warn(`update-install: npm pack failed: ${packErr.message}`);
3406
3641
  this.jsonResponse(res, { ok: false, error: `Download failed: ${packErr.message}` });
3407
- try {
3408
- node_fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
3409
- }
3410
- catch { }
3642
+ cleanupTmpDir();
3411
3643
  return;
3412
3644
  }
3413
3645
  const tgzFile = packOut.trim().split("\n").pop();
@@ -3419,83 +3651,85 @@ class ViewerServer {
3419
3651
  if (tarErr) {
3420
3652
  this.log.warn(`update-install: tar extract failed: ${tarErr.message}`);
3421
3653
  this.jsonResponse(res, { ok: false, error: `Extract failed: ${tarErr.message}` });
3422
- try {
3423
- node_fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
3424
- }
3425
- catch { }
3654
+ cleanupTmpDir();
3426
3655
  return;
3427
3656
  }
3428
3657
  // npm pack extracts to a "package" subdirectory
3429
3658
  const srcDir = node_path_1.default.join(extractDir, "package");
3430
3659
  if (!node_fs_1.default.existsSync(srcDir)) {
3431
3660
  this.jsonResponse(res, { ok: false, error: "Extracted package has no 'package' dir" });
3432
- try {
3433
- node_fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
3434
- }
3435
- catch { }
3661
+ cleanupTmpDir();
3436
3662
  return;
3437
3663
  }
3438
3664
  // Replace extension directory
3439
3665
  this.log.info(`update-install: replacing ${extDir}...`);
3440
3666
  try {
3441
- node_fs_1.default.rmSync(extDir, { recursive: true, force: true });
3667
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(extDir), { recursive: true });
3668
+ try {
3669
+ node_fs_1.default.rmSync(backupDir, { recursive: true, force: true });
3670
+ }
3671
+ catch { }
3672
+ if (node_fs_1.default.existsSync(extDir)) {
3673
+ node_fs_1.default.renameSync(extDir, backupDir);
3674
+ backupReady = true;
3675
+ }
3676
+ node_fs_1.default.renameSync(srcDir, extDir);
3677
+ }
3678
+ catch (replaceErr) {
3679
+ this.log.warn(`update-install: replace failed: ${replaceErr?.message ?? replaceErr}`);
3680
+ cleanupTmpDir();
3681
+ rollbackInstall();
3682
+ this.jsonResponse(res, { ok: false, error: `Replace failed: ${replaceErr?.message ?? replaceErr}` });
3683
+ return;
3442
3684
  }
3443
- catch { }
3444
- node_fs_1.default.mkdirSync(node_path_1.default.dirname(extDir), { recursive: true });
3445
- node_fs_1.default.renameSync(srcDir, extDir);
3446
3685
  // Install dependencies
3447
3686
  this.log.info(`update-install: installing dependencies...`);
3448
- (0, node_child_process_1.exec)(`cd ${extDir} && npm install --omit=dev --ignore-scripts`, { timeout: 120_000 }, (npmErr, npmOut, npmStderr) => {
3687
+ const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
3688
+ (0, node_child_process_1.execFile)(npmCmd, ["install", "--omit=dev", "--ignore-scripts"], { cwd: extDir, timeout: 120_000 }, (npmErr, npmOut, npmStderr) => {
3449
3689
  if (npmErr) {
3450
- try {
3451
- node_fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
3452
- }
3453
- catch { }
3454
3690
  this.log.warn(`update-install: npm install failed: ${npmErr.message}`);
3691
+ cleanupTmpDir();
3692
+ rollbackInstall();
3455
3693
  this.jsonResponse(res, { ok: false, error: `Dependency install failed: ${npmStderr || npmErr.message}` });
3456
3694
  return;
3457
3695
  }
3458
- // Rebuild native modules (do not swallow errors)
3459
- (0, node_child_process_1.exec)(`cd ${extDir} && npm rebuild better-sqlite3`, { timeout: 60_000 }, (rebuildErr, rebuildOut, rebuildStderr) => {
3696
+ (0, node_child_process_1.execFile)(npmCmd, ["rebuild", "better-sqlite3"], { cwd: extDir, timeout: 60_000 }, (rebuildErr, rebuildOut, rebuildStderr) => {
3460
3697
  if (rebuildErr) {
3461
3698
  this.log.warn(`update-install: better-sqlite3 rebuild failed: ${rebuildErr.message}`);
3462
3699
  const stderr = String(rebuildStderr || "").trim();
3463
3700
  if (stderr)
3464
3701
  this.log.warn(`update-install: rebuild stderr: ${stderr.slice(0, 500)}`);
3465
- // Continue so postinstall.cjs can run (it will try rebuild again and show user guidance)
3466
3702
  }
3467
- // Run postinstall.cjs: legacy cleanup, skill install, version marker, and optional sqlite re-check
3468
3703
  this.log.info(`update-install: running postinstall...`);
3469
- (0, node_child_process_1.exec)(`cd ${extDir} && node scripts/postinstall.cjs`, { timeout: 180_000 }, (postErr, postOut, postStderr) => {
3470
- try {
3471
- node_fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
3472
- }
3473
- catch { }
3704
+ (0, node_child_process_1.execFile)(process.execPath, ["scripts/postinstall.cjs"], { cwd: extDir, timeout: 180_000 }, (postErr, postOut, postStderr) => {
3705
+ cleanupTmpDir();
3474
3706
  if (postErr) {
3475
3707
  this.log.warn(`update-install: postinstall failed: ${postErr.message}`);
3476
3708
  const postStderrStr = String(postStderr || "").trim();
3477
3709
  if (postStderrStr)
3478
3710
  this.log.warn(`update-install: postinstall stderr: ${postStderrStr.slice(0, 500)}`);
3479
- // Still report success; plugin is updated, user can run postinstall manually if needed
3711
+ rollbackInstall();
3712
+ this.jsonResponse(res, { ok: false, error: `Postinstall failed: ${postStderrStr || postErr.message}` });
3713
+ return;
3480
3714
  }
3481
- // Read new version
3482
3715
  let newVersion = "unknown";
3483
3716
  try {
3484
3717
  const newPkg = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(extDir, "package.json"), "utf-8"));
3485
3718
  newVersion = newPkg.version ?? newVersion;
3486
3719
  }
3487
3720
  catch { }
3721
+ if (targetVersion && newVersion !== targetVersion) {
3722
+ this.log.warn(`update-install: version mismatch! expected=${targetVersion}, got=${newVersion} — rolling back`);
3723
+ rollbackInstall();
3724
+ this.jsonResponse(res, {
3725
+ ok: false,
3726
+ error: `Version mismatch: expected ${targetVersion} but downloaded ${newVersion}. npm cache may be stale — please try again.`,
3727
+ });
3728
+ return;
3729
+ }
3730
+ discardBackup();
3488
3731
  this.log.info(`update-install: success! Updated to ${newVersion}`);
3489
- this.jsonResponse(res, { ok: true, version: newVersion });
3490
- setTimeout(() => {
3491
- this.log.info(`update-install: triggering gateway restart via SIGUSR1...`);
3492
- try {
3493
- process.kill(process.pid, "SIGUSR1");
3494
- }
3495
- catch (sig) {
3496
- this.log.warn(`SIGUSR1 failed: ${sig}`);
3497
- }
3498
- }, 500);
3732
+ this.jsonResponseAndRestart(res, { ok: true, version: newVersion }, "update-install");
3499
3733
  });
3500
3734
  });
3501
3735
  });
@@ -3833,7 +4067,7 @@ class ViewerServer {
3833
4067
  else if (this.migrationState.done) {
3834
4068
  const evtName = this.migrationState.stopped ? "stopped" : "done";
3835
4069
  res.write(`event: state\ndata: ${JSON.stringify(this.migrationState)}\n\n`);
3836
- res.write(`event: ${evtName}\ndata: ${JSON.stringify({ ok: true })}\n\n`);
4070
+ res.write(`event: ${evtName}\ndata: ${JSON.stringify({ ok: this.migrationState.success, ...this.migrationState })}\n\n`);
3837
4071
  res.end();
3838
4072
  }
3839
4073
  else {
@@ -3872,22 +4106,11 @@ class ViewerServer {
3872
4106
  res.on("close", () => {
3873
4107
  this.migrationSSEClients = this.migrationSSEClients.filter(c => c !== res);
3874
4108
  });
3875
- this.migrationAbort = false;
3876
- this.migrationState = { phase: "", stored: 0, skipped: 0, merged: 0, errors: 0, processed: 0, total: 0, lastItem: null, done: false, stopped: false };
4109
+ this.migrationState = createInitialMigrationState();
3877
4110
  const send = (event, data) => {
3878
4111
  if (event === "item") {
3879
4112
  const d = data;
3880
- if (d.status === "stored")
3881
- this.migrationState.stored++;
3882
- else if (d.status === "skipped" || d.status === "duplicate")
3883
- this.migrationState.skipped++;
3884
- else if (d.status === "merged")
3885
- this.migrationState.merged++;
3886
- else if (d.status === "error")
3887
- this.migrationState.errors++;
3888
- this.migrationState.processed = d.index ?? this.migrationState.processed + 1;
3889
- this.migrationState.total = d.total ?? this.migrationState.total;
3890
- this.migrationState.lastItem = d;
4113
+ applyMigrationItemToState(this.migrationState, d);
3891
4114
  }
3892
4115
  else if (event === "phase") {
3893
4116
  this.migrationState.phase = data.phase;
@@ -3901,12 +4124,14 @@ class ViewerServer {
3901
4124
  this.runMigration(send, opts.sources, concurrency).finally(() => {
3902
4125
  this.migrationRunning = false;
3903
4126
  this.migrationState.done = true;
4127
+ this.migrationState.success = computeMigrationSuccess(this.migrationState);
4128
+ const donePayload = { ok: this.migrationState.success, ...this.migrationState };
3904
4129
  if (this.migrationAbort) {
3905
4130
  this.migrationState.stopped = true;
3906
- this.broadcastSSE("stopped", { ok: true, ...this.migrationState });
4131
+ this.broadcastSSE("stopped", donePayload);
3907
4132
  }
3908
4133
  else {
3909
- this.broadcastSSE("done", { ok: true });
4134
+ this.broadcastSSE("done", donePayload);
3910
4135
  }
3911
4136
  this.migrationAbort = false;
3912
4137
  const clientsToClose = [...this.migrationSSEClients];
@@ -3992,12 +4217,25 @@ class ViewerServer {
3992
4217
  continue;
3993
4218
  }
3994
4219
  try {
3995
- const summary = await summarizer.summarize(row.text);
4220
+ const stepFailures = [];
4221
+ let summary = "";
4222
+ try {
4223
+ summary = await summarizer.summarize(row.text);
4224
+ }
4225
+ catch (err) {
4226
+ stepFailures.push("summarization");
4227
+ this.log.warn(`Migration summarization failed: ${err}`);
4228
+ }
4229
+ if (!summary) {
4230
+ stepFailures.push("summarization");
4231
+ summary = row.text.slice(0, 200);
4232
+ }
3996
4233
  let embedding = null;
3997
4234
  try {
3998
4235
  [embedding] = await this.embedder.embed([summary]);
3999
4236
  }
4000
4237
  catch (err) {
4238
+ stepFailures.push("embedding");
4001
4239
  this.log.warn(`Migration embed failed: ${err}`);
4002
4240
  }
4003
4241
  let dedupStatus = "active";
@@ -4013,30 +4251,36 @@ class ViewerServer {
4013
4251
  return { index: idx + 1, summary: chunk?.summary ?? "", chunkId: s.chunkId };
4014
4252
  }).filter(c => c.summary);
4015
4253
  if (candidates.length > 0) {
4016
- const dedupResult = await summarizer.judgeDedup(summary, candidates);
4017
- if (dedupResult?.action === "DUPLICATE" && dedupResult.targetIndex) {
4018
- const targetId = candidates[dedupResult.targetIndex - 1]?.chunkId;
4019
- if (targetId) {
4020
- dedupStatus = "duplicate";
4021
- dedupTarget = targetId;
4022
- dedupReason = dedupResult.reason;
4254
+ try {
4255
+ const dedupResult = await summarizer.judgeDedup(summary, candidates);
4256
+ if (dedupResult?.action === "DUPLICATE" && dedupResult.targetIndex) {
4257
+ const targetId = candidates[dedupResult.targetIndex - 1]?.chunkId;
4258
+ if (targetId) {
4259
+ dedupStatus = "duplicate";
4260
+ dedupTarget = targetId;
4261
+ dedupReason = dedupResult.reason;
4262
+ }
4023
4263
  }
4024
- }
4025
- else if (dedupResult?.action === "UPDATE" && dedupResult.targetIndex && dedupResult.mergedSummary) {
4026
- const targetId = candidates[dedupResult.targetIndex - 1]?.chunkId;
4027
- if (targetId) {
4028
- this.store.updateChunkSummaryAndContent(targetId, dedupResult.mergedSummary, row.text);
4029
- try {
4030
- const [newEmb] = await this.embedder.embed([dedupResult.mergedSummary]);
4031
- if (newEmb)
4032
- this.store.upsertEmbedding(targetId, newEmb);
4264
+ else if (dedupResult?.action === "UPDATE" && dedupResult.targetIndex && dedupResult.mergedSummary) {
4265
+ const targetId = candidates[dedupResult.targetIndex - 1]?.chunkId;
4266
+ if (targetId) {
4267
+ this.store.updateChunkSummaryAndContent(targetId, dedupResult.mergedSummary, row.text);
4268
+ try {
4269
+ const [newEmb] = await this.embedder.embed([dedupResult.mergedSummary]);
4270
+ if (newEmb)
4271
+ this.store.upsertEmbedding(targetId, newEmb);
4272
+ }
4273
+ catch { /* best-effort */ }
4274
+ dedupStatus = "merged";
4275
+ dedupTarget = targetId;
4276
+ dedupReason = dedupResult.reason;
4033
4277
  }
4034
- catch { /* best-effort */ }
4035
- dedupStatus = "merged";
4036
- dedupTarget = targetId;
4037
- dedupReason = dedupResult.reason;
4038
4278
  }
4039
4279
  }
4280
+ catch (err) {
4281
+ stepFailures.push("dedup");
4282
+ this.log.warn(`Migration dedup judgment failed: ${err}`);
4283
+ }
4040
4284
  }
4041
4285
  }
4042
4286
  }
@@ -4060,8 +4304,8 @@ class ViewerServer {
4060
4304
  mergeCount: 0,
4061
4305
  lastHitAt: null,
4062
4306
  mergeHistory: "[]",
4063
- createdAt: normalizeTimestamp(row.updated_at),
4064
- updatedAt: normalizeTimestamp(row.updated_at),
4307
+ createdAt: Number(row.updated_at) < 1e12 ? Number(row.updated_at) * 1000 : Number(row.updated_at),
4308
+ updatedAt: Number(row.updated_at) < 1e12 ? Number(row.updated_at) * 1000 : Number(row.updated_at),
4065
4309
  };
4066
4310
  this.store.insertChunk(chunk);
4067
4311
  if (embedding && dedupStatus === "active") {
@@ -4075,7 +4319,14 @@ class ViewerServer {
4075
4319
  preview: row.text.slice(0, 120),
4076
4320
  summary: summary.slice(0, 80),
4077
4321
  source: file,
4322
+ stepFailures,
4078
4323
  });
4324
+ if (stepFailures.length > 0) {
4325
+ this.log.warn(`[MIGRATION] sqlite item imported with step failures: ${stepFailures.join(",")}`);
4326
+ }
4327
+ else {
4328
+ this.log.info("[MIGRATION] sqlite item imported successfully (all steps)");
4329
+ }
4079
4330
  }
4080
4331
  catch (err) {
4081
4332
  totalErrors++;
@@ -4216,12 +4467,25 @@ class ViewerServer {
4216
4467
  continue;
4217
4468
  }
4218
4469
  try {
4219
- const summary = await summarizer.summarize(content);
4470
+ const stepFailures = [];
4471
+ let summary = "";
4472
+ try {
4473
+ summary = await summarizer.summarize(content);
4474
+ }
4475
+ catch (err) {
4476
+ stepFailures.push("summarization");
4477
+ this.log.warn(`Migration summarization failed: ${err}`);
4478
+ }
4479
+ if (!summary) {
4480
+ stepFailures.push("summarization");
4481
+ summary = content.slice(0, 200);
4482
+ }
4220
4483
  let embedding = null;
4221
4484
  try {
4222
4485
  [embedding] = await this.embedder.embed([summary]);
4223
4486
  }
4224
4487
  catch (err) {
4488
+ stepFailures.push("embedding");
4225
4489
  this.log.warn(`Migration embed failed: ${err}`);
4226
4490
  }
4227
4491
  let dedupStatus = "active";
@@ -4237,30 +4501,36 @@ class ViewerServer {
4237
4501
  return { index: i + 1, summary: chunk?.summary ?? "", chunkId: s.chunkId };
4238
4502
  }).filter(c => c.summary);
4239
4503
  if (candidates.length > 0) {
4240
- const dedupResult = await summarizer.judgeDedup(summary, candidates);
4241
- if (dedupResult?.action === "DUPLICATE" && dedupResult.targetIndex) {
4242
- const targetId = candidates[dedupResult.targetIndex - 1]?.chunkId;
4243
- if (targetId) {
4244
- dedupStatus = "duplicate";
4245
- dedupTarget = targetId;
4246
- dedupReason = dedupResult.reason;
4504
+ try {
4505
+ const dedupResult = await summarizer.judgeDedup(summary, candidates);
4506
+ if (dedupResult?.action === "DUPLICATE" && dedupResult.targetIndex) {
4507
+ const targetId = candidates[dedupResult.targetIndex - 1]?.chunkId;
4508
+ if (targetId) {
4509
+ dedupStatus = "duplicate";
4510
+ dedupTarget = targetId;
4511
+ dedupReason = dedupResult.reason;
4512
+ }
4247
4513
  }
4248
- }
4249
- else if (dedupResult?.action === "UPDATE" && dedupResult.targetIndex && dedupResult.mergedSummary) {
4250
- const targetId = candidates[dedupResult.targetIndex - 1]?.chunkId;
4251
- if (targetId) {
4252
- this.store.updateChunkSummaryAndContent(targetId, dedupResult.mergedSummary, content);
4253
- try {
4254
- const [newEmb] = await this.embedder.embed([dedupResult.mergedSummary]);
4255
- if (newEmb)
4256
- this.store.upsertEmbedding(targetId, newEmb);
4514
+ else if (dedupResult?.action === "UPDATE" && dedupResult.targetIndex && dedupResult.mergedSummary) {
4515
+ const targetId = candidates[dedupResult.targetIndex - 1]?.chunkId;
4516
+ if (targetId) {
4517
+ this.store.updateChunkSummaryAndContent(targetId, dedupResult.mergedSummary, content);
4518
+ try {
4519
+ const [newEmb] = await this.embedder.embed([dedupResult.mergedSummary]);
4520
+ if (newEmb)
4521
+ this.store.upsertEmbedding(targetId, newEmb);
4522
+ }
4523
+ catch { /* best-effort */ }
4524
+ dedupStatus = "merged";
4525
+ dedupTarget = targetId;
4526
+ dedupReason = dedupResult.reason;
4257
4527
  }
4258
- catch { /* best-effort */ }
4259
- dedupStatus = "merged";
4260
- dedupTarget = targetId;
4261
- dedupReason = dedupResult.reason;
4262
4528
  }
4263
4529
  }
4530
+ catch (err) {
4531
+ stepFailures.push("dedup");
4532
+ this.log.warn(`Migration dedup judgment failed: ${err}`);
4533
+ }
4264
4534
  }
4265
4535
  }
4266
4536
  }
@@ -4277,7 +4547,13 @@ class ViewerServer {
4277
4547
  if (embedding && dedupStatus === "active")
4278
4548
  this.store.upsertEmbedding(chunkId, embedding);
4279
4549
  totalStored++;
4280
- 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 });
4550
+ 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 });
4551
+ if (stepFailures.length > 0) {
4552
+ this.log.warn(`[MIGRATION] session item imported with step failures: ${stepFailures.join(",")}`);
4553
+ }
4554
+ else {
4555
+ this.log.info("[MIGRATION] session item imported successfully (all steps)");
4556
+ }
4281
4557
  }
4282
4558
  catch (err) {
4283
4559
  totalErrors++;
@@ -4320,7 +4596,14 @@ class ViewerServer {
4320
4596
  }
4321
4597
  }
4322
4598
  send("progress", { total: totalProcessed, processed: totalProcessed, phase: "done" });
4323
- send("summary", { totalProcessed, totalStored, totalSkipped, totalErrors });
4599
+ send("summary", {
4600
+ totalProcessed,
4601
+ totalStored,
4602
+ totalSkipped,
4603
+ totalErrors,
4604
+ success: computeMigrationSuccess(this.migrationState),
4605
+ stepFailures: this.migrationState.stepFailures,
4606
+ });
4324
4607
  }
4325
4608
  // ─── Post-processing: independent task/skill generation ───
4326
4609
  handlePostprocess(req, res) {
@@ -4579,6 +4862,20 @@ class ViewerServer {
4579
4862
  req.on("data", (chunk) => { body += chunk.toString(); });
4580
4863
  req.on("end", () => cb(body));
4581
4864
  }
4865
+ jsonResponseAndRestart(res, data, source, delayMs = 1500, statusCode = 200) {
4866
+ res.writeHead(statusCode, { "Content-Type": "application/json; charset=utf-8" });
4867
+ res.end(JSON.stringify(data), () => {
4868
+ setTimeout(() => {
4869
+ this.log.info(`${source}: triggering gateway restart via SIGUSR1...`);
4870
+ try {
4871
+ process.kill(process.pid, "SIGUSR1");
4872
+ }
4873
+ catch (sig) {
4874
+ this.log.warn(`SIGUSR1 failed: ${sig}`);
4875
+ }
4876
+ }, delayMs);
4877
+ });
4878
+ }
4582
4879
  jsonResponse(res, data, statusCode = 200) {
4583
4880
  res.writeHead(statusCode, { "Content-Type": "application/json; charset=utf-8" });
4584
4881
  res.end(JSON.stringify(data));