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

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 (119) hide show
  1. package/.env.example +7 -0
  2. package/README.md +24 -24
  3. package/dist/capture/index.d.ts +1 -1
  4. package/dist/capture/index.d.ts.map +1 -1
  5. package/dist/capture/index.js +34 -2
  6. package/dist/capture/index.js.map +1 -1
  7. package/dist/client/connector.d.ts +2 -2
  8. package/dist/client/connector.d.ts.map +1 -1
  9. package/dist/client/connector.js +122 -26
  10. package/dist/client/connector.js.map +1 -1
  11. package/dist/client/hub.d.ts.map +1 -1
  12. package/dist/client/hub.js +22 -0
  13. package/dist/client/hub.js.map +1 -1
  14. package/dist/client/skill-sync.d.ts +7 -0
  15. package/dist/client/skill-sync.d.ts.map +1 -1
  16. package/dist/client/skill-sync.js +10 -0
  17. package/dist/client/skill-sync.js.map +1 -1
  18. package/dist/config.d.ts.map +1 -1
  19. package/dist/config.js +0 -2
  20. package/dist/config.js.map +1 -1
  21. package/dist/hub/server.d.ts +8 -0
  22. package/dist/hub/server.d.ts.map +1 -1
  23. package/dist/hub/server.js +390 -106
  24. package/dist/hub/server.js.map +1 -1
  25. package/dist/hub/user-manager.d.ts +11 -0
  26. package/dist/hub/user-manager.d.ts.map +1 -1
  27. package/dist/hub/user-manager.js +31 -3
  28. package/dist/hub/user-manager.js.map +1 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +7 -2
  31. package/dist/index.js.map +1 -1
  32. package/dist/ingest/providers/index.d.ts.map +1 -1
  33. package/dist/ingest/providers/index.js +37 -6
  34. package/dist/ingest/providers/index.js.map +1 -1
  35. package/dist/recall/engine.d.ts.map +1 -1
  36. package/dist/recall/engine.js +93 -1
  37. package/dist/recall/engine.js.map +1 -1
  38. package/dist/shared/llm-call.d.ts +1 -0
  39. package/dist/shared/llm-call.d.ts.map +1 -1
  40. package/dist/shared/llm-call.js +82 -8
  41. package/dist/shared/llm-call.js.map +1 -1
  42. package/dist/sharing/types.d.ts +1 -1
  43. package/dist/sharing/types.d.ts.map +1 -1
  44. package/dist/skill/evolver.d.ts +4 -0
  45. package/dist/skill/evolver.d.ts.map +1 -1
  46. package/dist/skill/evolver.js +59 -5
  47. package/dist/skill/evolver.js.map +1 -1
  48. package/dist/skill/generator.d.ts +2 -0
  49. package/dist/skill/generator.d.ts.map +1 -1
  50. package/dist/skill/generator.js +45 -3
  51. package/dist/skill/generator.js.map +1 -1
  52. package/dist/skill/installer.d.ts +26 -0
  53. package/dist/skill/installer.d.ts.map +1 -1
  54. package/dist/skill/installer.js +80 -4
  55. package/dist/skill/installer.js.map +1 -1
  56. package/dist/skill/upgrader.d.ts +2 -0
  57. package/dist/skill/upgrader.d.ts.map +1 -1
  58. package/dist/skill/upgrader.js +139 -1
  59. package/dist/skill/upgrader.js.map +1 -1
  60. package/dist/skill/validator.d.ts +3 -0
  61. package/dist/skill/validator.d.ts.map +1 -1
  62. package/dist/skill/validator.js +75 -0
  63. package/dist/skill/validator.js.map +1 -1
  64. package/dist/storage/ensure-binding.d.ts +12 -0
  65. package/dist/storage/ensure-binding.d.ts.map +1 -0
  66. package/dist/storage/ensure-binding.js +53 -0
  67. package/dist/storage/ensure-binding.js.map +1 -0
  68. package/dist/storage/sqlite.d.ts +89 -20
  69. package/dist/storage/sqlite.d.ts.map +1 -1
  70. package/dist/storage/sqlite.js +374 -124
  71. package/dist/storage/sqlite.js.map +1 -1
  72. package/dist/telemetry.d.ts +12 -5
  73. package/dist/telemetry.d.ts.map +1 -1
  74. package/dist/telemetry.js +156 -40
  75. package/dist/telemetry.js.map +1 -1
  76. package/dist/tools/memory-search.d.ts +3 -1
  77. package/dist/tools/memory-search.d.ts.map +1 -1
  78. package/dist/tools/memory-search.js +3 -1
  79. package/dist/tools/memory-search.js.map +1 -1
  80. package/dist/types.d.ts +11 -2
  81. package/dist/types.d.ts.map +1 -1
  82. package/dist/types.js +4 -0
  83. package/dist/types.js.map +1 -1
  84. package/dist/viewer/html.d.ts.map +1 -1
  85. package/dist/viewer/html.js +2671 -879
  86. package/dist/viewer/html.js.map +1 -1
  87. package/dist/viewer/server.d.ts +30 -8
  88. package/dist/viewer/server.d.ts.map +1 -1
  89. package/dist/viewer/server.js +990 -198
  90. package/dist/viewer/server.js.map +1 -1
  91. package/index.ts +700 -56
  92. package/openclaw.plugin.json +1 -1
  93. package/package.json +3 -2
  94. package/scripts/postinstall.cjs +1 -1
  95. package/skill/memos-memory-guide/SKILL.md +64 -26
  96. package/src/capture/index.ts +37 -1
  97. package/src/client/connector.ts +124 -28
  98. package/src/client/hub.ts +18 -0
  99. package/src/client/skill-sync.ts +14 -0
  100. package/src/config.ts +0 -2
  101. package/src/hub/server.ts +374 -97
  102. package/src/hub/user-manager.ts +48 -8
  103. package/src/index.ts +10 -2
  104. package/src/ingest/providers/index.ts +41 -7
  105. package/src/recall/engine.ts +86 -1
  106. package/src/shared/llm-call.ts +97 -9
  107. package/src/sharing/types.ts +1 -1
  108. package/src/skill/evolver.ts +63 -6
  109. package/src/skill/generator.ts +44 -5
  110. package/src/skill/installer.ts +107 -4
  111. package/src/skill/upgrader.ts +139 -1
  112. package/src/skill/validator.ts +79 -0
  113. package/src/storage/ensure-binding.ts +52 -0
  114. package/src/storage/sqlite.ts +395 -148
  115. package/src/telemetry.ts +172 -41
  116. package/src/tools/memory-search.ts +2 -1
  117. package/src/types.ts +12 -2
  118. package/src/viewer/html.ts +2671 -879
  119. package/src/viewer/server.ts +913 -182
@@ -146,11 +146,70 @@ class SqliteStore {
146
146
  this.migrateSkillEmbeddingsAndFts();
147
147
  this.migrateFtsToTrigram();
148
148
  this.migrateHubTables();
149
+ this.migrateHubFtsToTrigram();
150
+ this.migrateLocalSharedTasksOwner();
151
+ this.migrateHubUserIdentityFields();
152
+ this.migrateClientHubConnectionIdentityFields();
149
153
  this.log.debug("Database schema initialized");
150
154
  }
151
155
  migrateChunksIndexesForRecall() {
152
156
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_chunks_dedup_created ON chunks(dedup_status, created_at DESC)");
153
157
  }
158
+ migrateLocalSharedTasksOwner() {
159
+ try {
160
+ const cols = this.db.prepare("PRAGMA table_info(local_shared_tasks)").all();
161
+ if (cols.length > 0 && !cols.some((c) => c.name === "original_owner")) {
162
+ this.db.exec("ALTER TABLE local_shared_tasks ADD COLUMN original_owner TEXT NOT NULL DEFAULT 'agent:main'");
163
+ this.log.info("Migrated: added original_owner column to local_shared_tasks");
164
+ }
165
+ }
166
+ catch { /* table may not exist yet */ }
167
+ }
168
+ migrateHubUserIdentityFields() {
169
+ try {
170
+ const cols = this.db.prepare("PRAGMA table_info(hub_users)").all();
171
+ if (cols.length === 0)
172
+ return;
173
+ if (!cols.some(c => c.name === "identity_key")) {
174
+ this.db.exec("ALTER TABLE hub_users ADD COLUMN identity_key TEXT NOT NULL DEFAULT ''");
175
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_hub_users_identity_key ON hub_users(identity_key)");
176
+ this.log.info("Migrated: added identity_key to hub_users");
177
+ }
178
+ if (!cols.some(c => c.name === "left_at")) {
179
+ this.db.exec("ALTER TABLE hub_users ADD COLUMN left_at INTEGER");
180
+ this.log.info("Migrated: added left_at to hub_users");
181
+ }
182
+ if (!cols.some(c => c.name === "removed_at")) {
183
+ this.db.exec("ALTER TABLE hub_users ADD COLUMN removed_at INTEGER");
184
+ this.log.info("Migrated: added removed_at to hub_users");
185
+ }
186
+ if (!cols.some(c => c.name === "rejected_at")) {
187
+ this.db.exec("ALTER TABLE hub_users ADD COLUMN rejected_at INTEGER");
188
+ this.log.info("Migrated: added rejected_at to hub_users");
189
+ }
190
+ if (!cols.some(c => c.name === "rejoin_requested_at")) {
191
+ this.db.exec("ALTER TABLE hub_users ADD COLUMN rejoin_requested_at INTEGER");
192
+ this.log.info("Migrated: added rejoin_requested_at to hub_users");
193
+ }
194
+ }
195
+ catch { /* table may not exist yet */ }
196
+ }
197
+ migrateClientHubConnectionIdentityFields() {
198
+ try {
199
+ const cols = this.db.prepare("PRAGMA table_info(client_hub_connection)").all();
200
+ if (cols.length === 0)
201
+ return;
202
+ if (!cols.some(c => c.name === "identity_key")) {
203
+ this.db.exec("ALTER TABLE client_hub_connection ADD COLUMN identity_key TEXT NOT NULL DEFAULT ''");
204
+ this.log.info("Migrated: added identity_key to client_hub_connection");
205
+ }
206
+ if (!cols.some(c => c.name === "last_known_status")) {
207
+ this.db.exec("ALTER TABLE client_hub_connection ADD COLUMN last_known_status TEXT NOT NULL DEFAULT ''");
208
+ this.log.info("Migrated: added last_known_status to client_hub_connection");
209
+ }
210
+ }
211
+ catch { /* table may not exist yet */ }
212
+ }
154
213
  migrateOwnerFields() {
155
214
  const chunkCols = this.db.prepare("PRAGMA table_info(chunks)").all();
156
215
  if (!chunkCols.some((c) => c.name === "owner")) {
@@ -298,6 +357,54 @@ class SqliteStore {
298
357
  this.log.warn(`Failed to migrate skills_fts to trigram: ${err}`);
299
358
  }
300
359
  }
360
+ migrateHubFtsToTrigram() {
361
+ const tables = [
362
+ {
363
+ fts: "hub_chunks_fts", source: "hub_chunks", columns: "summary, content",
364
+ triggers: ["hub_chunks_ai", "hub_chunks_ad", "hub_chunks_au"],
365
+ },
366
+ {
367
+ fts: "hub_skills_fts", source: "hub_skills", columns: "name, description",
368
+ triggers: ["hub_skills_ai", "hub_skills_ad", "hub_skills_au"],
369
+ },
370
+ {
371
+ fts: "hub_memories_fts", source: "hub_memories", columns: "summary, content",
372
+ triggers: ["hub_memories_ai", "hub_memories_ad", "hub_memories_au"],
373
+ },
374
+ ];
375
+ for (const t of tables) {
376
+ try {
377
+ const row = this.db.prepare(`SELECT sql FROM sqlite_master WHERE name='${t.fts}'`).get();
378
+ if (!row || !row.sql)
379
+ continue;
380
+ if (row.sql.includes("trigram"))
381
+ continue;
382
+ this.log.info(`Migrating ${t.fts} to trigram tokenizer...`);
383
+ for (const tr of t.triggers)
384
+ this.db.exec(`DROP TRIGGER IF EXISTS ${tr}`);
385
+ this.db.exec(`DROP TABLE IF EXISTS ${t.fts}`);
386
+ this.db.exec(`CREATE VIRTUAL TABLE ${t.fts} USING fts5(${t.columns}, content='${t.source}', content_rowid='rowid', tokenize='trigram')`);
387
+ this.db.exec(`
388
+ CREATE TRIGGER ${t.triggers[0]} AFTER INSERT ON ${t.source} BEGIN
389
+ INSERT INTO ${t.fts}(rowid, ${t.columns}) VALUES (new.rowid, ${t.columns.split(", ").map(c => "new." + c).join(", ")});
390
+ END;
391
+ CREATE TRIGGER ${t.triggers[1]} AFTER DELETE ON ${t.source} BEGIN
392
+ INSERT INTO ${t.fts}(${t.fts}, rowid, ${t.columns}) VALUES ('delete', old.rowid, ${t.columns.split(", ").map(c => "old." + c).join(", ")});
393
+ END;
394
+ CREATE TRIGGER ${t.triggers[2]} AFTER UPDATE ON ${t.source} BEGIN
395
+ INSERT INTO ${t.fts}(${t.fts}, rowid, ${t.columns}) VALUES ('delete', old.rowid, ${t.columns.split(", ").map(c => "old." + c).join(", ")});
396
+ INSERT INTO ${t.fts}(rowid, ${t.columns}) VALUES (new.rowid, ${t.columns.split(", ").map(c => "new." + c).join(", ")});
397
+ END
398
+ `);
399
+ this.db.exec(`INSERT INTO ${t.fts}(rowid, ${t.columns}) SELECT rowid, ${t.columns} FROM ${t.source}`);
400
+ const cnt = this.db.prepare(`SELECT COUNT(*) as c FROM ${t.fts}`).get().c;
401
+ this.log.info(`Migrated ${t.fts} to trigram: ${cnt} rows indexed`);
402
+ }
403
+ catch (err) {
404
+ this.log.warn(`Failed to migrate ${t.fts} to trigram: ${err}`);
405
+ }
406
+ }
407
+ }
301
408
  migrateTaskId() {
302
409
  const cols = this.db.prepare("PRAGMA table_info(chunks)").all();
303
410
  if (!cols.some((c) => c.name === "task_id")) {
@@ -503,15 +610,16 @@ class SqliteStore {
503
610
  recordToolCall(toolName, durationMs, success) {
504
611
  this.db.prepare("INSERT INTO tool_calls (tool_name, duration_ms, success, called_at) VALUES (?, ?, ?, ?)").run(toolName, Math.round(durationMs), success ? 1 : 0, Date.now());
505
612
  }
506
- getToolMetrics(minutes) {
507
- const since = Date.now() - minutes * 60 * 1000;
613
+ getToolMetrics(minutes, fromMs, toMs) {
614
+ const since = fromMs ?? (Date.now() - minutes * 60 * 1000);
615
+ const until = toMs ?? Date.now();
508
616
  const rows = this.db.prepare(`SELECT tool_name,
509
617
  duration_ms,
510
618
  success,
511
619
  strftime('%Y-%m-%d %H:%M', called_at/1000, 'unixepoch', 'localtime') as minute_key
512
620
  FROM tool_calls
513
- WHERE called_at >= ?
514
- ORDER BY called_at`).all(since);
621
+ WHERE called_at >= ? AND called_at <= ?
622
+ ORDER BY called_at`).all(since, until);
515
623
  const toolSet = new Set();
516
624
  const minuteMap = new Map();
517
625
  const aggMap = new Map();
@@ -644,34 +752,27 @@ class SqliteStore {
644
752
  shared_at INTEGER NOT NULL
645
753
  );
646
754
 
755
+ CREATE TABLE IF NOT EXISTS local_shared_memories (
756
+ chunk_id TEXT PRIMARY KEY REFERENCES chunks(id) ON DELETE CASCADE,
757
+ original_owner TEXT NOT NULL,
758
+ shared_at INTEGER NOT NULL
759
+ );
760
+
647
761
  CREATE TABLE IF NOT EXISTS hub_users (
648
- id TEXT PRIMARY KEY,
649
- username TEXT NOT NULL UNIQUE,
650
- device_name TEXT NOT NULL DEFAULT '',
651
- role TEXT NOT NULL,
652
- status TEXT NOT NULL,
653
- token_hash TEXT NOT NULL DEFAULT '',
654
- created_at INTEGER NOT NULL,
655
- approved_at INTEGER
762
+ id TEXT PRIMARY KEY,
763
+ username TEXT NOT NULL UNIQUE,
764
+ device_name TEXT NOT NULL DEFAULT '',
765
+ role TEXT NOT NULL,
766
+ status TEXT NOT NULL,
767
+ token_hash TEXT NOT NULL DEFAULT '',
768
+ created_at INTEGER NOT NULL,
769
+ approved_at INTEGER,
770
+ last_ip TEXT NOT NULL DEFAULT '',
771
+ last_active_at INTEGER
656
772
  );
657
773
  CREATE INDEX IF NOT EXISTS idx_hub_users_status ON hub_users(status);
658
774
  CREATE INDEX IF NOT EXISTS idx_hub_users_role ON hub_users(role);
659
775
 
660
- CREATE TABLE IF NOT EXISTS hub_groups (
661
- id TEXT PRIMARY KEY,
662
- name TEXT NOT NULL UNIQUE,
663
- description TEXT NOT NULL DEFAULT '',
664
- created_at INTEGER NOT NULL
665
- );
666
-
667
- CREATE TABLE IF NOT EXISTS hub_group_members (
668
- group_id TEXT NOT NULL REFERENCES hub_groups(id) ON DELETE CASCADE,
669
- user_id TEXT NOT NULL REFERENCES hub_users(id) ON DELETE CASCADE,
670
- joined_at INTEGER NOT NULL,
671
- PRIMARY KEY (group_id, user_id)
672
- );
673
- CREATE INDEX IF NOT EXISTS idx_hub_group_members_user ON hub_group_members(user_id);
674
-
675
776
  CREATE TABLE IF NOT EXISTS hub_tasks (
676
777
  id TEXT PRIMARY KEY,
677
778
  source_task_id TEXT NOT NULL,
@@ -713,7 +814,7 @@ class SqliteStore {
713
814
  content,
714
815
  content='hub_chunks',
715
816
  content_rowid='rowid',
716
- tokenize='porter unicode61'
817
+ tokenize='trigram'
717
818
  );
718
819
 
719
820
  CREATE TRIGGER IF NOT EXISTS hub_chunks_ai AFTER INSERT ON hub_chunks BEGIN
@@ -763,7 +864,7 @@ class SqliteStore {
763
864
  description,
764
865
  content='hub_skills',
765
866
  content_rowid='rowid',
766
- tokenize='porter unicode61'
867
+ tokenize='trigram'
767
868
  );
768
869
 
769
870
  CREATE TRIGGER IF NOT EXISTS hub_skills_ai AFTER INSERT ON hub_skills BEGIN
@@ -813,7 +914,7 @@ class SqliteStore {
813
914
  content,
814
915
  content='hub_memories',
815
916
  content_rowid='rowid',
816
- tokenize='porter unicode61'
917
+ tokenize='trigram'
817
918
  );
818
919
 
819
920
  CREATE TRIGGER IF NOT EXISTS hub_memories_ai AFTER INSERT ON hub_memories BEGIN
@@ -833,6 +934,31 @@ class SqliteStore {
833
934
  VALUES (new.rowid, new.summary, new.content);
834
935
  END;
835
936
  `);
937
+ this.db.exec(`
938
+ CREATE TABLE IF NOT EXISTS hub_notifications (
939
+ id TEXT PRIMARY KEY,
940
+ user_id TEXT NOT NULL,
941
+ type TEXT NOT NULL,
942
+ resource TEXT NOT NULL,
943
+ title TEXT NOT NULL,
944
+ message TEXT NOT NULL DEFAULT '',
945
+ read INTEGER NOT NULL DEFAULT 0,
946
+ created_at INTEGER NOT NULL
947
+ );
948
+ CREATE INDEX IF NOT EXISTS idx_hub_notif_user ON hub_notifications(user_id, read, created_at DESC);
949
+ `);
950
+ try {
951
+ const cols = this.db.prepare("PRAGMA table_info(hub_users)").all();
952
+ if (cols.length > 0 && !cols.some(c => c.name === "last_ip")) {
953
+ this.db.exec("ALTER TABLE hub_users ADD COLUMN last_ip TEXT NOT NULL DEFAULT ''");
954
+ this.log.info("Migrated: added last_ip column to hub_users");
955
+ }
956
+ if (cols.length > 0 && !cols.some(c => c.name === "last_active_at")) {
957
+ this.db.exec("ALTER TABLE hub_users ADD COLUMN last_active_at INTEGER");
958
+ this.log.info("Migrated: added last_active_at column to hub_users");
959
+ }
960
+ }
961
+ catch { /* table may not exist yet */ }
836
962
  }
837
963
  // ─── Write ───
838
964
  insertChunk(chunk) {
@@ -959,6 +1085,30 @@ class SqliteStore {
959
1085
  return [];
960
1086
  }
961
1087
  }
1088
+ hubMemoryPatternSearch(patterns, opts = {}) {
1089
+ if (patterns.length === 0)
1090
+ return [];
1091
+ const limit = opts.limit ?? 10;
1092
+ const conditions = patterns.map(() => "(hm.content LIKE ? OR hm.summary LIKE ?)");
1093
+ const params = [];
1094
+ for (const p of patterns) {
1095
+ params.push(`%${p}%`, `%${p}%`);
1096
+ }
1097
+ params.push(limit);
1098
+ try {
1099
+ const rows = this.db.prepare(`
1100
+ SELECT hm.id as memory_id, hm.content, hm.role, hm.created_at
1101
+ FROM hub_memories hm
1102
+ WHERE ${conditions.join(" OR ")}
1103
+ ORDER BY hm.created_at DESC
1104
+ LIMIT ?
1105
+ `).all(...params);
1106
+ return rows.map(r => ({ memoryId: r.memory_id, content: r.content, role: r.role, createdAt: r.created_at }));
1107
+ }
1108
+ catch {
1109
+ return [];
1110
+ }
1111
+ }
962
1112
  // ─── Vector Search ───
963
1113
  getAllEmbeddings(ownerFilter) {
964
1114
  let sql = `SELECT e.chunk_id, e.vector, e.dimensions FROM embeddings e
@@ -1097,6 +1247,8 @@ class SqliteStore {
1097
1247
  "skill_embeddings",
1098
1248
  "skill_versions",
1099
1249
  "skills",
1250
+ "local_shared_memories",
1251
+ "local_shared_tasks",
1100
1252
  "embeddings",
1101
1253
  "chunks",
1102
1254
  "tasks",
@@ -1475,16 +1627,18 @@ class SqliteStore {
1475
1627
  // ─── Hub / Client connection ───
1476
1628
  setClientHubConnection(conn) {
1477
1629
  this.db.prepare(`
1478
- INSERT INTO client_hub_connection (id, hub_url, user_id, username, user_token, role, connected_at)
1479
- VALUES (1, ?, ?, ?, ?, ?, ?)
1630
+ INSERT INTO client_hub_connection (id, hub_url, user_id, username, user_token, role, connected_at, identity_key, last_known_status)
1631
+ VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?)
1480
1632
  ON CONFLICT(id) DO UPDATE SET
1481
1633
  hub_url = excluded.hub_url,
1482
1634
  user_id = excluded.user_id,
1483
1635
  username = excluded.username,
1484
1636
  user_token = excluded.user_token,
1485
1637
  role = excluded.role,
1486
- connected_at = excluded.connected_at
1487
- `).run(conn.hubUrl, conn.userId, conn.username, conn.userToken, conn.role, conn.connectedAt);
1638
+ connected_at = excluded.connected_at,
1639
+ identity_key = excluded.identity_key,
1640
+ last_known_status = excluded.last_known_status
1641
+ `).run(conn.hubUrl, conn.userId, conn.username, conn.userToken, conn.role, conn.connectedAt, conn.identityKey ?? "", conn.lastKnownStatus ?? "");
1488
1642
  }
1489
1643
  getClientHubConnection() {
1490
1644
  const row = this.db.prepare('SELECT * FROM client_hub_connection WHERE id = 1').get();
@@ -1519,11 +1673,66 @@ class SqliteStore {
1519
1673
  const rows = this.db.prepare('SELECT task_id, hub_task_id, visibility, group_id, synced_chunks FROM local_shared_tasks').all();
1520
1674
  return rows.map(r => ({ taskId: r.task_id, hubTaskId: r.hub_task_id, visibility: r.visibility, groupId: r.group_id, syncedChunks: r.synced_chunks }));
1521
1675
  }
1676
+ // ─── Local Shared Memories (client-side tracking) ───
1677
+ markMemorySharedLocally(chunkId) {
1678
+ const chunk = this.getChunk(chunkId);
1679
+ if (!chunk)
1680
+ return { ok: false, reason: "not_found" };
1681
+ if (chunk.owner === "public") {
1682
+ const existing = this.getLocalSharedMemory(chunkId);
1683
+ return {
1684
+ ok: true,
1685
+ owner: "public",
1686
+ originalOwner: existing?.originalOwner ?? undefined,
1687
+ sharedAt: existing?.sharedAt ?? undefined,
1688
+ };
1689
+ }
1690
+ const sharedAt = Date.now();
1691
+ this.db.transaction(() => {
1692
+ this.db.prepare(`
1693
+ INSERT INTO local_shared_memories (chunk_id, original_owner, shared_at)
1694
+ VALUES (?, ?, ?)
1695
+ ON CONFLICT(chunk_id) DO UPDATE SET
1696
+ original_owner = excluded.original_owner,
1697
+ shared_at = excluded.shared_at
1698
+ `).run(chunkId, chunk.owner, sharedAt);
1699
+ this.updateChunk(chunkId, { owner: "public" });
1700
+ })();
1701
+ return { ok: true, owner: "public", originalOwner: chunk.owner, sharedAt };
1702
+ }
1703
+ unmarkMemorySharedLocally(chunkId, fallbackOwner) {
1704
+ const chunk = this.getChunk(chunkId);
1705
+ if (!chunk)
1706
+ return { ok: false, reason: "not_found" };
1707
+ if (chunk.owner !== "public") {
1708
+ return { ok: true, owner: chunk.owner };
1709
+ }
1710
+ const existing = this.getLocalSharedMemory(chunkId);
1711
+ const restoreOwner = existing?.originalOwner ?? fallbackOwner;
1712
+ if (!restoreOwner || restoreOwner === "public") {
1713
+ return { ok: false, reason: "original_owner_missing" };
1714
+ }
1715
+ this.db.transaction(() => {
1716
+ this.updateChunk(chunkId, { owner: restoreOwner });
1717
+ this.db.prepare("DELETE FROM local_shared_memories WHERE chunk_id = ?").run(chunkId);
1718
+ })();
1719
+ return { ok: true, owner: restoreOwner, originalOwner: restoreOwner };
1720
+ }
1721
+ getLocalSharedMemory(chunkId) {
1722
+ const row = this.db.prepare("SELECT chunk_id, original_owner, shared_at FROM local_shared_memories WHERE chunk_id = ?").get(chunkId);
1723
+ if (!row)
1724
+ return null;
1725
+ return {
1726
+ chunkId: row.chunk_id,
1727
+ originalOwner: row.original_owner,
1728
+ sharedAt: row.shared_at,
1729
+ };
1730
+ }
1522
1731
  // ─── Hub Users / Groups ───
1523
1732
  upsertHubUser(user) {
1524
1733
  this.db.prepare(`
1525
- INSERT INTO hub_users (id, username, device_name, role, status, token_hash, created_at, approved_at)
1526
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1734
+ INSERT INTO hub_users (id, username, device_name, role, status, token_hash, created_at, approved_at, identity_key, left_at, removed_at, rejected_at, rejoin_requested_at)
1735
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1527
1736
  ON CONFLICT(id) DO UPDATE SET
1528
1737
  username = excluded.username,
1529
1738
  device_name = excluded.device_name,
@@ -1531,72 +1740,71 @@ class SqliteStore {
1531
1740
  status = excluded.status,
1532
1741
  token_hash = excluded.token_hash,
1533
1742
  created_at = excluded.created_at,
1534
- approved_at = excluded.approved_at
1535
- `).run(user.id, user.username, user.deviceName ?? "", user.role, user.status, user.tokenHash, user.createdAt, user.approvedAt);
1743
+ approved_at = excluded.approved_at,
1744
+ identity_key = excluded.identity_key,
1745
+ left_at = excluded.left_at,
1746
+ removed_at = excluded.removed_at,
1747
+ rejected_at = excluded.rejected_at,
1748
+ rejoin_requested_at = excluded.rejoin_requested_at
1749
+ `).run(user.id, user.username, user.deviceName ?? "", user.role, user.status, user.tokenHash, user.createdAt, user.approvedAt, user.identityKey ?? "", user.leftAt ?? null, user.removedAt ?? null, user.rejectedAt ?? null, user.rejoinRequestedAt ?? null);
1536
1750
  }
1537
1751
  getHubUser(userId) {
1538
1752
  const row = this.db.prepare('SELECT * FROM hub_users WHERE id = ?').get(userId);
1539
1753
  if (!row)
1540
1754
  return null;
1541
- return this.attachGroupsToHubUser(rowToHubUser(row));
1755
+ return rowToHubUser(row);
1542
1756
  }
1543
1757
  listHubUsers(status) {
1544
1758
  const rows = status
1545
1759
  ? this.db.prepare('SELECT * FROM hub_users WHERE status = ? ORDER BY created_at').all(status)
1546
1760
  : this.db.prepare('SELECT * FROM hub_users ORDER BY created_at').all();
1547
- return rows.map((row) => this.attachGroupsToHubUser(rowToHubUser(row)));
1548
- }
1549
- upsertHubGroup(group) {
1550
- this.db.prepare(`
1551
- INSERT INTO hub_groups (id, name, description, created_at)
1552
- VALUES (?, ?, ?, ?)
1553
- ON CONFLICT(id) DO UPDATE SET
1554
- name = excluded.name,
1555
- description = excluded.description,
1556
- created_at = excluded.created_at
1557
- `).run(group.id, group.name, group.description, group.createdAt);
1558
- }
1559
- listHubGroups() {
1560
- const rows = this.db.prepare('SELECT * FROM hub_groups ORDER BY name').all();
1561
- return rows.map(rowToHubGroup);
1562
- }
1563
- addHubGroupMember(groupId, userId, joinedAt = Date.now()) {
1564
- this.db.prepare(`
1565
- INSERT INTO hub_group_members (group_id, user_id, joined_at)
1566
- VALUES (?, ?, ?)
1567
- ON CONFLICT(group_id, user_id) DO UPDATE SET joined_at = excluded.joined_at
1568
- `).run(groupId, userId, joinedAt);
1761
+ return rows.map(rowToHubUser);
1762
+ }
1763
+ deleteHubUser(userId, cleanResources = false) {
1764
+ if (cleanResources) {
1765
+ this.db.prepare('DELETE FROM hub_tasks WHERE source_user_id = ?').run(userId);
1766
+ this.db.prepare('DELETE FROM hub_skills WHERE source_user_id = ?').run(userId);
1767
+ this.db.prepare('DELETE FROM hub_memories WHERE source_user_id = ?').run(userId);
1768
+ const result = this.db.prepare('DELETE FROM hub_users WHERE id = ?').run(userId);
1769
+ return result.changes > 0;
1770
+ }
1771
+ const result = this.db.prepare("UPDATE hub_users SET status = 'removed', token_hash = '', removed_at = ? WHERE id = ?").run(Date.now(), userId);
1772
+ return result.changes > 0;
1569
1773
  }
1570
- getHubGroupById(groupId) {
1571
- const row = this.db.prepare('SELECT * FROM hub_groups WHERE id = ?').get(groupId);
1572
- return row ? rowToHubGroup(row) : undefined;
1774
+ findHubUserByIdentityKey(identityKey) {
1775
+ if (!identityKey)
1776
+ return null;
1777
+ const row = this.db.prepare('SELECT * FROM hub_users WHERE identity_key = ?').get(identityKey);
1778
+ return row ? rowToHubUser(row) : null;
1573
1779
  }
1574
- deleteHubGroup(groupId) {
1575
- const result = this.db.prepare('DELETE FROM hub_groups WHERE id = ?').run(groupId);
1780
+ markHubUserLeft(userId) {
1781
+ const result = this.db.prepare("UPDATE hub_users SET status = 'left', token_hash = '', left_at = ? WHERE id = ?").run(Date.now(), userId);
1576
1782
  return result.changes > 0;
1577
1783
  }
1578
- listHubGroupMembers(groupId) {
1579
- const rows = this.db.prepare(`
1580
- SELECT gm.user_id, hu.username, gm.joined_at
1581
- FROM hub_group_members gm
1582
- JOIN hub_users hu ON hu.id = gm.user_id
1583
- WHERE gm.group_id = ?
1584
- ORDER BY gm.joined_at
1585
- `).all(groupId);
1586
- return rows.map(r => ({ userId: r.user_id, username: r.username, joinedAt: r.joined_at }));
1587
- }
1588
- removeHubGroupMember(groupId, userId) {
1589
- this.db.prepare('DELETE FROM hub_group_members WHERE group_id = ? AND user_id = ?').run(groupId, userId);
1590
- }
1591
- getGroupsForHubUser(userId) {
1592
- const rows = this.db.prepare(`
1593
- SELECT g.*
1594
- FROM hub_group_members gm
1595
- JOIN hub_groups g ON g.id = gm.group_id
1596
- WHERE gm.user_id = ?
1597
- ORDER BY g.name
1598
- `).all(userId);
1599
- return rows.map((row) => ({ id: row.id, name: row.name, description: row.description || undefined }));
1784
+ updateHubUserActivity(userId, ip, timestamp) {
1785
+ this.db.prepare('UPDATE hub_users SET last_ip = ?, last_active_at = ? WHERE id = ?').run(ip, timestamp ?? Date.now(), userId);
1786
+ }
1787
+ getHubUserContributions() {
1788
+ const result = {};
1789
+ const memRows = this.db.prepare('SELECT source_user_id, COUNT(*) as cnt FROM hub_memories GROUP BY source_user_id').all();
1790
+ const taskRows = this.db.prepare('SELECT source_user_id, COUNT(*) as cnt FROM hub_tasks GROUP BY source_user_id').all();
1791
+ const skillRows = this.db.prepare('SELECT source_user_id, COUNT(*) as cnt FROM hub_skills GROUP BY source_user_id').all();
1792
+ for (const r of memRows) {
1793
+ if (!result[r.source_user_id])
1794
+ result[r.source_user_id] = { memoryCount: 0, taskCount: 0, skillCount: 0 };
1795
+ result[r.source_user_id].memoryCount = r.cnt;
1796
+ }
1797
+ for (const r of taskRows) {
1798
+ if (!result[r.source_user_id])
1799
+ result[r.source_user_id] = { memoryCount: 0, taskCount: 0, skillCount: 0 };
1800
+ result[r.source_user_id].taskCount = r.cnt;
1801
+ }
1802
+ for (const r of skillRows) {
1803
+ if (!result[r.source_user_id])
1804
+ result[r.source_user_id] = { memoryCount: 0, taskCount: 0, skillCount: 0 };
1805
+ result[r.source_user_id].skillCount = r.cnt;
1806
+ }
1807
+ return result;
1600
1808
  }
1601
1809
  // ─── Hub Shared Data ───
1602
1810
  upsertHubTask(task) {
@@ -1616,6 +1824,10 @@ class SqliteStore {
1616
1824
  const row = this.db.prepare('SELECT * FROM hub_tasks WHERE source_user_id = ? AND source_task_id = ?').get(sourceUserId, sourceTaskId);
1617
1825
  return row ? rowToHubTask(row) : null;
1618
1826
  }
1827
+ getHubTaskById(taskId) {
1828
+ const row = this.db.prepare('SELECT * FROM hub_tasks WHERE id = ?').get(taskId);
1829
+ return row ? rowToHubTask(row) : null;
1830
+ }
1619
1831
  upsertHubChunk(chunk) {
1620
1832
  if (!chunk.sourceTaskId)
1621
1833
  throw new Error("sourceTaskId is required for hub chunk upserts");
@@ -1688,6 +1900,17 @@ class SqliteStore {
1688
1900
  out.push(row.vector.readFloatLE(i * 4));
1689
1901
  return out;
1690
1902
  }
1903
+ getVisibleHubSkillEmbeddings() {
1904
+ const rows = this.db.prepare(`
1905
+ SELECT hse.skill_id, hse.vector, hse.dimensions
1906
+ FROM hub_skill_embeddings hse
1907
+ JOIN hub_skills hs ON hs.id = hse.skill_id
1908
+ `).all();
1909
+ return rows.map(r => ({
1910
+ skillId: r.skill_id,
1911
+ vector: new Float32Array(r.vector.buffer, r.vector.byteOffset, r.dimensions),
1912
+ }));
1913
+ }
1691
1914
  searchHubChunks(query, options) {
1692
1915
  const limit = options?.maxResults ?? 10;
1693
1916
  const userId = options?.userId ?? "";
@@ -1753,7 +1976,7 @@ class SqliteStore {
1753
1976
  let rows;
1754
1977
  if (sanitized) {
1755
1978
  rows = this.db.prepare(`
1756
- SELECT hs.id, hs.name, hs.description, hs.version, hs.visibility, '' AS group_name, hu.username AS owner_name, hs.quality_score,
1979
+ SELECT hs.id, hs.name, hs.description, hs.version, hs.visibility, '' AS group_name, hu.username AS owner_name, hu.status AS owner_status, hs.quality_score,
1757
1980
  bm25(hub_skills_fts) as rank
1758
1981
  FROM hub_skills_fts f
1759
1982
  JOIN hub_skills hs ON hs.rowid = f.rowid
@@ -1765,7 +1988,7 @@ class SqliteStore {
1765
1988
  }
1766
1989
  else {
1767
1990
  rows = this.db.prepare(`
1768
- SELECT hs.id, hs.name, hs.description, hs.version, hs.visibility, '' AS group_name, hu.username AS owner_name, hs.quality_score,
1991
+ SELECT hs.id, hs.name, hs.description, hs.version, hs.visibility, '' AS group_name, hu.username AS owner_name, hu.status AS owner_status, hs.quality_score,
1769
1992
  0 as rank
1770
1993
  FROM hub_skills hs
1771
1994
  LEFT JOIN hub_users hu ON hu.id = hs.source_user_id
@@ -1780,7 +2003,7 @@ class SqliteStore {
1780
2003
  }
1781
2004
  listVisibleHubTasks(userId, limit = 40) {
1782
2005
  const rows = this.db.prepare(`
1783
- SELECT t.*, u.username AS owner_name, NULL AS group_name,
2006
+ SELECT t.*, u.username AS owner_name, u.status AS owner_status, NULL AS group_name,
1784
2007
  (SELECT COUNT(*) FROM hub_chunks c WHERE c.hub_task_id = t.id) AS chunk_count
1785
2008
  FROM hub_tasks t
1786
2009
  LEFT JOIN hub_users u ON u.id = t.source_user_id
@@ -1790,33 +2013,36 @@ class SqliteStore {
1790
2013
  return rows.map(r => ({
1791
2014
  id: r.id, sourceTaskId: r.source_task_id, sourceUserId: r.source_user_id,
1792
2015
  title: r.title, summary: r.summary, groupId: r.group_id, groupName: r.group_name ?? null,
1793
- visibility: r.visibility, ownerName: r.owner_name ?? "unknown", chunkCount: r.chunk_count ?? 0,
2016
+ visibility: r.visibility, ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", chunkCount: r.chunk_count ?? 0,
1794
2017
  createdAt: r.created_at, updatedAt: r.updated_at,
1795
2018
  }));
1796
2019
  }
1797
2020
  listAllHubTasks() {
1798
2021
  const rows = this.db.prepare(`
1799
- SELECT t.*, u.username AS owner_name, g.name AS group_name,
2022
+ SELECT t.*, u.username AS owner_name, u.status AS owner_status,
1800
2023
  (SELECT COUNT(*) FROM hub_chunks c WHERE c.hub_task_id = t.id) AS chunk_count
1801
2024
  FROM hub_tasks t
1802
2025
  LEFT JOIN hub_users u ON u.id = t.source_user_id
1803
- LEFT JOIN hub_groups g ON g.id = t.group_id
1804
2026
  ORDER BY t.updated_at DESC
1805
2027
  `).all();
1806
2028
  return rows.map(r => ({
1807
2029
  id: r.id, sourceTaskId: r.source_task_id, sourceUserId: r.source_user_id,
1808
- title: r.title, summary: r.summary, groupId: r.group_id, groupName: r.group_name ?? null,
1809
- visibility: r.visibility, ownerName: r.owner_name ?? "unknown", chunkCount: r.chunk_count ?? 0,
2030
+ title: r.title, summary: r.summary, groupId: r.group_id, groupName: null,
2031
+ visibility: r.visibility, ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", chunkCount: r.chunk_count ?? 0,
1810
2032
  createdAt: r.created_at, updatedAt: r.updated_at,
1811
2033
  }));
1812
2034
  }
2035
+ listHubChunksByTaskId(hubTaskId) {
2036
+ const rows = this.db.prepare('SELECT * FROM hub_chunks WHERE hub_task_id = ? ORDER BY created_at ASC').all(hubTaskId);
2037
+ return rows.map(rowToHubChunk);
2038
+ }
1813
2039
  deleteHubTaskById(taskId) {
1814
2040
  const info = this.db.prepare('DELETE FROM hub_tasks WHERE id = ?').run(taskId);
1815
2041
  return info.changes > 0;
1816
2042
  }
1817
2043
  listVisibleHubSkills(userId, limit = 40) {
1818
2044
  const rows = this.db.prepare(`
1819
- SELECT s.*, u.username AS owner_name, NULL AS group_name
2045
+ SELECT s.*, u.username AS owner_name, u.status AS owner_status, NULL AS group_name
1820
2046
  FROM hub_skills s
1821
2047
  LEFT JOIN hub_users u ON u.id = s.source_user_id
1822
2048
  ORDER BY s.updated_at DESC
@@ -1826,23 +2052,22 @@ class SqliteStore {
1826
2052
  id: r.id, sourceSkillId: r.source_skill_id, sourceUserId: r.source_user_id,
1827
2053
  name: r.name, description: r.description, version: r.version,
1828
2054
  groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
1829
- ownerName: r.owner_name ?? "unknown", qualityScore: r.quality_score,
2055
+ ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", qualityScore: r.quality_score,
1830
2056
  createdAt: r.created_at, updatedAt: r.updated_at,
1831
2057
  }));
1832
2058
  }
1833
2059
  listAllHubSkills() {
1834
2060
  const rows = this.db.prepare(`
1835
- SELECT s.*, u.username AS owner_name, g.name AS group_name
2061
+ SELECT s.*, u.username AS owner_name, u.status AS owner_status
1836
2062
  FROM hub_skills s
1837
2063
  LEFT JOIN hub_users u ON u.id = s.source_user_id
1838
- LEFT JOIN hub_groups g ON g.id = s.group_id
1839
2064
  ORDER BY s.updated_at DESC
1840
2065
  `).all();
1841
2066
  return rows.map(r => ({
1842
2067
  id: r.id, sourceSkillId: r.source_skill_id, sourceUserId: r.source_user_id,
1843
2068
  name: r.name, description: r.description, version: r.version,
1844
- groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
1845
- ownerName: r.owner_name ?? "unknown", qualityScore: r.quality_score,
2069
+ groupId: r.group_id, groupName: null, visibility: r.visibility,
2070
+ ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", qualityScore: r.quality_score,
1846
2071
  createdAt: r.created_at, updatedAt: r.updated_at,
1847
2072
  }));
1848
2073
  }
@@ -1881,6 +2106,37 @@ class SqliteStore {
1881
2106
  const info = this.db.prepare('DELETE FROM hub_memories WHERE id = ?').run(memoryId);
1882
2107
  return info.changes > 0;
1883
2108
  }
2109
+ // ─── Hub Notifications ───
2110
+ insertHubNotification(n) {
2111
+ this.db.prepare('INSERT INTO hub_notifications (id, user_id, type, resource, title, message, read, created_at) VALUES (?, ?, ?, ?, ?, ?, 0, ?)').run(n.id, n.userId, n.type, n.resource, n.title, n.message ?? '', Date.now());
2112
+ }
2113
+ hasRecentHubNotification(userId, type, resource, windowMs = 300_000) {
2114
+ const since = Date.now() - windowMs;
2115
+ const row = this.db.prepare('SELECT COUNT(*) AS cnt FROM hub_notifications WHERE user_id = ? AND type = ? AND resource = ? AND created_at > ?').get(userId, type, resource, since);
2116
+ return row.cnt > 0;
2117
+ }
2118
+ listHubNotifications(userId, opts) {
2119
+ const where = opts?.unreadOnly ? 'WHERE user_id = ? AND read = 0' : 'WHERE user_id = ?';
2120
+ const limit = opts?.limit ?? 50;
2121
+ const rows = this.db.prepare(`SELECT * FROM hub_notifications ${where} ORDER BY created_at DESC LIMIT ?`).all(userId, limit);
2122
+ return rows.map(r => ({ id: r.id, userId: r.user_id, type: r.type, resource: r.resource, title: r.title, message: r.message, read: !!r.read, createdAt: r.created_at }));
2123
+ }
2124
+ countUnreadHubNotifications(userId) {
2125
+ const row = this.db.prepare('SELECT COUNT(*) AS cnt FROM hub_notifications WHERE user_id = ? AND read = 0').get(userId);
2126
+ return row.cnt;
2127
+ }
2128
+ markHubNotificationsRead(userId, ids) {
2129
+ if (ids && ids.length > 0) {
2130
+ const placeholders = ids.map(() => '?').join(',');
2131
+ this.db.prepare(`UPDATE hub_notifications SET read = 1 WHERE user_id = ? AND id IN (${placeholders})`).run(userId, ...ids);
2132
+ }
2133
+ else {
2134
+ this.db.prepare('UPDATE hub_notifications SET read = 1 WHERE user_id = ?').run(userId);
2135
+ }
2136
+ }
2137
+ clearHubNotifications(userId) {
2138
+ this.db.prepare('DELETE FROM hub_notifications WHERE user_id = ?').run(userId);
2139
+ }
1884
2140
  upsertHubMemoryEmbedding(memoryId, vector) {
1885
2141
  const buf = Buffer.from(vector.buffer, vector.byteOffset, vector.byteLength);
1886
2142
  this.db.prepare(`
@@ -1937,7 +2193,7 @@ class SqliteStore {
1937
2193
  }
1938
2194
  listVisibleHubMemories(userId, limit = 40) {
1939
2195
  const rows = this.db.prepare(`
1940
- SELECT m.*, u.username AS owner_name, NULL AS group_name
2196
+ SELECT m.*, u.username AS owner_name, u.status AS owner_status, NULL AS group_name
1941
2197
  FROM hub_memories m
1942
2198
  LEFT JOIN hub_users u ON u.id = m.source_user_id
1943
2199
  ORDER BY m.updated_at DESC
@@ -1945,24 +2201,23 @@ class SqliteStore {
1945
2201
  `).all(limit);
1946
2202
  return rows.map(r => ({
1947
2203
  id: r.id, sourceChunkId: r.source_chunk_id, sourceUserId: r.source_user_id,
1948
- role: r.role, summary: r.summary, kind: r.kind,
2204
+ role: r.role, content: r.content ?? "", summary: r.summary, kind: r.kind,
1949
2205
  groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
1950
- ownerName: r.owner_name ?? "unknown", createdAt: r.created_at, updatedAt: r.updated_at,
2206
+ ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", createdAt: r.created_at, updatedAt: r.updated_at,
1951
2207
  }));
1952
2208
  }
1953
2209
  listAllHubMemories() {
1954
2210
  const rows = this.db.prepare(`
1955
- SELECT m.*, u.username AS owner_name, g.name AS group_name
2211
+ SELECT m.*, u.username AS owner_name, u.status AS owner_status
1956
2212
  FROM hub_memories m
1957
2213
  LEFT JOIN hub_users u ON u.id = m.source_user_id
1958
- LEFT JOIN hub_groups g ON g.id = m.group_id
1959
2214
  ORDER BY m.updated_at DESC
1960
2215
  `).all();
1961
2216
  return rows.map(r => ({
1962
2217
  id: r.id, sourceChunkId: r.source_chunk_id, sourceUserId: r.source_user_id,
1963
- role: r.role, summary: r.summary, kind: r.kind,
1964
- groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
1965
- ownerName: r.owner_name ?? "unknown", createdAt: r.created_at, updatedAt: r.updated_at,
2218
+ role: r.role, content: r.content ?? "", summary: r.summary, kind: r.kind,
2219
+ groupId: r.group_id, groupName: null, visibility: r.visibility,
2220
+ ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", createdAt: r.created_at, updatedAt: r.updated_at,
1966
2221
  }));
1967
2222
  }
1968
2223
  resolveCanonicalHubTaskId(taskId, sourceUserId, sourceTaskId) {
@@ -1987,12 +2242,6 @@ class SqliteStore {
1987
2242
  }
1988
2243
  throw new Error(`source skill not found for skillId=${skillId}`);
1989
2244
  }
1990
- attachGroupsToHubUser(user) {
1991
- return {
1992
- ...user,
1993
- groups: this.getGroupsForHubUser(user.id),
1994
- };
1995
- }
1996
2245
  getSessionOwnerMap(sessionKeys) {
1997
2246
  const result = new Map();
1998
2247
  if (sessionKeys.length === 0)
@@ -2102,6 +2351,8 @@ function rowToClientHubConnection(row) {
2102
2351
  userToken: row.user_token,
2103
2352
  role: row.role,
2104
2353
  connectedAt: row.connected_at,
2354
+ identityKey: row.identity_key || "",
2355
+ lastKnownStatus: row.last_known_status || "",
2105
2356
  };
2106
2357
  }
2107
2358
  function rowToHubUser(row) {
@@ -2115,14 +2366,13 @@ function rowToHubUser(row) {
2115
2366
  tokenHash: row.token_hash,
2116
2367
  createdAt: row.created_at,
2117
2368
  approvedAt: row.approved_at,
2118
- };
2119
- }
2120
- function rowToHubGroup(row) {
2121
- return {
2122
- id: row.id,
2123
- name: row.name,
2124
- description: row.description,
2125
- createdAt: row.created_at,
2369
+ lastIp: row.last_ip || "",
2370
+ lastActiveAt: row.last_active_at ?? null,
2371
+ identityKey: row.identity_key || "",
2372
+ leftAt: row.left_at ?? null,
2373
+ removedAt: row.removed_at ?? null,
2374
+ rejectedAt: row.rejected_at ?? null,
2375
+ rejoinRequestedAt: row.rejoin_requested_at ?? null,
2126
2376
  };
2127
2377
  }
2128
2378
  function rowToHubTask(row) {