@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
@@ -3,7 +3,7 @@ import { createHash } from "crypto";
3
3
  import * as fs from "fs";
4
4
  import * as path from "path";
5
5
  import type { Chunk, ChunkRef, DedupStatus, Task, TaskStatus, Skill, SkillStatus, SkillVisibility, SkillVersion, TaskSkillLink, TaskSkillRelation, Logger } from "../types";
6
- import type { GroupInfo, SharedVisibility, UserInfo, UserRole, UserStatus } from "../sharing/types";
6
+ import type { SharedVisibility, UserInfo, UserRole, UserStatus } from "../sharing/types";
7
7
 
8
8
  export class SqliteStore {
9
9
  private db: Database.Database;
@@ -112,6 +112,10 @@ export class SqliteStore {
112
112
  this.migrateSkillEmbeddingsAndFts();
113
113
  this.migrateFtsToTrigram();
114
114
  this.migrateHubTables();
115
+ this.migrateHubFtsToTrigram();
116
+ this.migrateLocalSharedTasksOwner();
117
+ this.migrateHubUserIdentityFields();
118
+ this.migrateClientHubConnectionIdentityFields();
115
119
  this.log.debug("Database schema initialized");
116
120
  }
117
121
 
@@ -119,6 +123,59 @@ export class SqliteStore {
119
123
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_chunks_dedup_created ON chunks(dedup_status, created_at DESC)");
120
124
  }
121
125
 
126
+ private migrateLocalSharedTasksOwner(): void {
127
+ try {
128
+ const cols = this.db.prepare("PRAGMA table_info(local_shared_tasks)").all() as Array<{ name: string }>;
129
+ if (cols.length > 0 && !cols.some((c) => c.name === "original_owner")) {
130
+ this.db.exec("ALTER TABLE local_shared_tasks ADD COLUMN original_owner TEXT NOT NULL DEFAULT 'agent:main'");
131
+ this.log.info("Migrated: added original_owner column to local_shared_tasks");
132
+ }
133
+ } catch { /* table may not exist yet */ }
134
+ }
135
+
136
+ private migrateHubUserIdentityFields(): void {
137
+ try {
138
+ const cols = this.db.prepare("PRAGMA table_info(hub_users)").all() as Array<{ name: string }>;
139
+ if (cols.length === 0) return;
140
+ if (!cols.some(c => c.name === "identity_key")) {
141
+ this.db.exec("ALTER TABLE hub_users ADD COLUMN identity_key TEXT NOT NULL DEFAULT ''");
142
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_hub_users_identity_key ON hub_users(identity_key)");
143
+ this.log.info("Migrated: added identity_key to hub_users");
144
+ }
145
+ if (!cols.some(c => c.name === "left_at")) {
146
+ this.db.exec("ALTER TABLE hub_users ADD COLUMN left_at INTEGER");
147
+ this.log.info("Migrated: added left_at to hub_users");
148
+ }
149
+ if (!cols.some(c => c.name === "removed_at")) {
150
+ this.db.exec("ALTER TABLE hub_users ADD COLUMN removed_at INTEGER");
151
+ this.log.info("Migrated: added removed_at to hub_users");
152
+ }
153
+ if (!cols.some(c => c.name === "rejected_at")) {
154
+ this.db.exec("ALTER TABLE hub_users ADD COLUMN rejected_at INTEGER");
155
+ this.log.info("Migrated: added rejected_at to hub_users");
156
+ }
157
+ if (!cols.some(c => c.name === "rejoin_requested_at")) {
158
+ this.db.exec("ALTER TABLE hub_users ADD COLUMN rejoin_requested_at INTEGER");
159
+ this.log.info("Migrated: added rejoin_requested_at to hub_users");
160
+ }
161
+ } catch { /* table may not exist yet */ }
162
+ }
163
+
164
+ private migrateClientHubConnectionIdentityFields(): void {
165
+ try {
166
+ const cols = this.db.prepare("PRAGMA table_info(client_hub_connection)").all() as Array<{ name: string }>;
167
+ if (cols.length === 0) return;
168
+ if (!cols.some(c => c.name === "identity_key")) {
169
+ this.db.exec("ALTER TABLE client_hub_connection ADD COLUMN identity_key TEXT NOT NULL DEFAULT ''");
170
+ this.log.info("Migrated: added identity_key to client_hub_connection");
171
+ }
172
+ if (!cols.some(c => c.name === "last_known_status")) {
173
+ this.db.exec("ALTER TABLE client_hub_connection ADD COLUMN last_known_status TEXT NOT NULL DEFAULT ''");
174
+ this.log.info("Migrated: added last_known_status to client_hub_connection");
175
+ }
176
+ } catch { /* table may not exist yet */ }
177
+ }
178
+
122
179
  private migrateOwnerFields(): void {
123
180
  const chunkCols = this.db.prepare("PRAGMA table_info(chunks)").all() as Array<{ name: string }>;
124
181
  if (!chunkCols.some((c) => c.name === "owner")) {
@@ -273,6 +330,51 @@ export class SqliteStore {
273
330
  }
274
331
  }
275
332
 
333
+ private migrateHubFtsToTrigram(): void {
334
+ const tables: Array<{ fts: string; source: string; columns: string; triggers: string[] }> = [
335
+ {
336
+ fts: "hub_chunks_fts", source: "hub_chunks", columns: "summary, content",
337
+ triggers: ["hub_chunks_ai", "hub_chunks_ad", "hub_chunks_au"],
338
+ },
339
+ {
340
+ fts: "hub_skills_fts", source: "hub_skills", columns: "name, description",
341
+ triggers: ["hub_skills_ai", "hub_skills_ad", "hub_skills_au"],
342
+ },
343
+ {
344
+ fts: "hub_memories_fts", source: "hub_memories", columns: "summary, content",
345
+ triggers: ["hub_memories_ai", "hub_memories_ad", "hub_memories_au"],
346
+ },
347
+ ];
348
+ for (const t of tables) {
349
+ try {
350
+ const row = this.db.prepare(`SELECT sql FROM sqlite_master WHERE name='${t.fts}'`).get() as { sql: string } | undefined;
351
+ if (!row || !row.sql) continue;
352
+ if (row.sql.includes("trigram")) continue;
353
+ this.log.info(`Migrating ${t.fts} to trigram tokenizer...`);
354
+ for (const tr of t.triggers) this.db.exec(`DROP TRIGGER IF EXISTS ${tr}`);
355
+ this.db.exec(`DROP TABLE IF EXISTS ${t.fts}`);
356
+ this.db.exec(`CREATE VIRTUAL TABLE ${t.fts} USING fts5(${t.columns}, content='${t.source}', content_rowid='rowid', tokenize='trigram')`);
357
+ this.db.exec(`
358
+ CREATE TRIGGER ${t.triggers[0]} AFTER INSERT ON ${t.source} BEGIN
359
+ INSERT INTO ${t.fts}(rowid, ${t.columns}) VALUES (new.rowid, ${t.columns.split(", ").map(c => "new." + c).join(", ")});
360
+ END;
361
+ CREATE TRIGGER ${t.triggers[1]} AFTER DELETE ON ${t.source} BEGIN
362
+ INSERT INTO ${t.fts}(${t.fts}, rowid, ${t.columns}) VALUES ('delete', old.rowid, ${t.columns.split(", ").map(c => "old." + c).join(", ")});
363
+ END;
364
+ CREATE TRIGGER ${t.triggers[2]} AFTER UPDATE ON ${t.source} BEGIN
365
+ INSERT INTO ${t.fts}(${t.fts}, rowid, ${t.columns}) VALUES ('delete', old.rowid, ${t.columns.split(", ").map(c => "old." + c).join(", ")});
366
+ INSERT INTO ${t.fts}(rowid, ${t.columns}) VALUES (new.rowid, ${t.columns.split(", ").map(c => "new." + c).join(", ")});
367
+ END
368
+ `);
369
+ this.db.exec(`INSERT INTO ${t.fts}(rowid, ${t.columns}) SELECT rowid, ${t.columns} FROM ${t.source}`);
370
+ const cnt = (this.db.prepare(`SELECT COUNT(*) as c FROM ${t.fts}`).get() as { c: number }).c;
371
+ this.log.info(`Migrated ${t.fts} to trigram: ${cnt} rows indexed`);
372
+ } catch (err) {
373
+ this.log.warn(`Failed to migrate ${t.fts} to trigram: ${err}`);
374
+ }
375
+ }
376
+ }
377
+
276
378
  private migrateTaskId(): void {
277
379
  const cols = this.db.prepare("PRAGMA table_info(chunks)").all() as Array<{ name: string }>;
278
380
  if (!cols.some((c) => c.name === "task_id")) {
@@ -516,12 +618,13 @@ export class SqliteStore {
516
618
  ).run(toolName, Math.round(durationMs), success ? 1 : 0, Date.now());
517
619
  }
518
620
 
519
- getToolMetrics(minutes: number): {
621
+ getToolMetrics(minutes: number, fromMs?: number, toMs?: number): {
520
622
  tools: string[];
521
623
  series: Array<{ minute: string; [tool: string]: number | string }>;
522
624
  aggregated: Array<{ tool: string; totalCalls: number; avgMs: number; p95Ms: number; errorCount: number }>;
523
625
  } {
524
- const since = Date.now() - minutes * 60 * 1000;
626
+ const since = fromMs ?? (Date.now() - minutes * 60 * 1000);
627
+ const until = toMs ?? Date.now();
525
628
 
526
629
  const rows = this.db.prepare(
527
630
  `SELECT tool_name,
@@ -529,9 +632,9 @@ export class SqliteStore {
529
632
  success,
530
633
  strftime('%Y-%m-%d %H:%M', called_at/1000, 'unixepoch', 'localtime') as minute_key
531
634
  FROM tool_calls
532
- WHERE called_at >= ?
635
+ WHERE called_at >= ? AND called_at <= ?
533
636
  ORDER BY called_at`,
534
- ).all(since) as Array<{ tool_name: string; duration_ms: number; success: number; minute_key: string }>;
637
+ ).all(since, until) as Array<{ tool_name: string; duration_ms: number; success: number; minute_key: string }>;
535
638
 
536
639
  const toolSet = new Set<string>();
537
640
  const minuteMap = new Map<string, Map<string, { total: number; count: number }>>();
@@ -683,34 +786,27 @@ export class SqliteStore {
683
786
  shared_at INTEGER NOT NULL
684
787
  );
685
788
 
789
+ CREATE TABLE IF NOT EXISTS local_shared_memories (
790
+ chunk_id TEXT PRIMARY KEY REFERENCES chunks(id) ON DELETE CASCADE,
791
+ original_owner TEXT NOT NULL,
792
+ shared_at INTEGER NOT NULL
793
+ );
794
+
686
795
  CREATE TABLE IF NOT EXISTS hub_users (
687
- id TEXT PRIMARY KEY,
688
- username TEXT NOT NULL UNIQUE,
689
- device_name TEXT NOT NULL DEFAULT '',
690
- role TEXT NOT NULL,
691
- status TEXT NOT NULL,
692
- token_hash TEXT NOT NULL DEFAULT '',
693
- created_at INTEGER NOT NULL,
694
- approved_at INTEGER
796
+ id TEXT PRIMARY KEY,
797
+ username TEXT NOT NULL UNIQUE,
798
+ device_name TEXT NOT NULL DEFAULT '',
799
+ role TEXT NOT NULL,
800
+ status TEXT NOT NULL,
801
+ token_hash TEXT NOT NULL DEFAULT '',
802
+ created_at INTEGER NOT NULL,
803
+ approved_at INTEGER,
804
+ last_ip TEXT NOT NULL DEFAULT '',
805
+ last_active_at INTEGER
695
806
  );
696
807
  CREATE INDEX IF NOT EXISTS idx_hub_users_status ON hub_users(status);
697
808
  CREATE INDEX IF NOT EXISTS idx_hub_users_role ON hub_users(role);
698
809
 
699
- CREATE TABLE IF NOT EXISTS hub_groups (
700
- id TEXT PRIMARY KEY,
701
- name TEXT NOT NULL UNIQUE,
702
- description TEXT NOT NULL DEFAULT '',
703
- created_at INTEGER NOT NULL
704
- );
705
-
706
- CREATE TABLE IF NOT EXISTS hub_group_members (
707
- group_id TEXT NOT NULL REFERENCES hub_groups(id) ON DELETE CASCADE,
708
- user_id TEXT NOT NULL REFERENCES hub_users(id) ON DELETE CASCADE,
709
- joined_at INTEGER NOT NULL,
710
- PRIMARY KEY (group_id, user_id)
711
- );
712
- CREATE INDEX IF NOT EXISTS idx_hub_group_members_user ON hub_group_members(user_id);
713
-
714
810
  CREATE TABLE IF NOT EXISTS hub_tasks (
715
811
  id TEXT PRIMARY KEY,
716
812
  source_task_id TEXT NOT NULL,
@@ -752,7 +848,7 @@ export class SqliteStore {
752
848
  content,
753
849
  content='hub_chunks',
754
850
  content_rowid='rowid',
755
- tokenize='porter unicode61'
851
+ tokenize='trigram'
756
852
  );
757
853
 
758
854
  CREATE TRIGGER IF NOT EXISTS hub_chunks_ai AFTER INSERT ON hub_chunks BEGIN
@@ -802,7 +898,7 @@ export class SqliteStore {
802
898
  description,
803
899
  content='hub_skills',
804
900
  content_rowid='rowid',
805
- tokenize='porter unicode61'
901
+ tokenize='trigram'
806
902
  );
807
903
 
808
904
  CREATE TRIGGER IF NOT EXISTS hub_skills_ai AFTER INSERT ON hub_skills BEGIN
@@ -852,7 +948,7 @@ export class SqliteStore {
852
948
  content,
853
949
  content='hub_memories',
854
950
  content_rowid='rowid',
855
- tokenize='porter unicode61'
951
+ tokenize='trigram'
856
952
  );
857
953
 
858
954
  CREATE TRIGGER IF NOT EXISTS hub_memories_ai AFTER INSERT ON hub_memories BEGIN
@@ -872,6 +968,32 @@ export class SqliteStore {
872
968
  VALUES (new.rowid, new.summary, new.content);
873
969
  END;
874
970
  `);
971
+
972
+ this.db.exec(`
973
+ CREATE TABLE IF NOT EXISTS hub_notifications (
974
+ id TEXT PRIMARY KEY,
975
+ user_id TEXT NOT NULL,
976
+ type TEXT NOT NULL,
977
+ resource TEXT NOT NULL,
978
+ title TEXT NOT NULL,
979
+ message TEXT NOT NULL DEFAULT '',
980
+ read INTEGER NOT NULL DEFAULT 0,
981
+ created_at INTEGER NOT NULL
982
+ );
983
+ CREATE INDEX IF NOT EXISTS idx_hub_notif_user ON hub_notifications(user_id, read, created_at DESC);
984
+ `);
985
+
986
+ try {
987
+ const cols = this.db.prepare("PRAGMA table_info(hub_users)").all() as Array<{ name: string }>;
988
+ if (cols.length > 0 && !cols.some(c => c.name === "last_ip")) {
989
+ this.db.exec("ALTER TABLE hub_users ADD COLUMN last_ip TEXT NOT NULL DEFAULT ''");
990
+ this.log.info("Migrated: added last_ip column to hub_users");
991
+ }
992
+ if (cols.length > 0 && !cols.some(c => c.name === "last_active_at")) {
993
+ this.db.exec("ALTER TABLE hub_users ADD COLUMN last_active_at INTEGER");
994
+ this.log.info("Migrated: added last_active_at column to hub_users");
995
+ }
996
+ } catch { /* table may not exist yet */ }
875
997
  }
876
998
 
877
999
  // ─── Write ───
@@ -1047,6 +1169,25 @@ export class SqliteStore {
1047
1169
  }
1048
1170
  }
1049
1171
 
1172
+ hubMemoryPatternSearch(patterns: string[], opts: { limit?: number } = {}): Array<{ memoryId: string; content: string; role: string; createdAt: number }> {
1173
+ if (patterns.length === 0) return [];
1174
+ const limit = opts.limit ?? 10;
1175
+ const conditions = patterns.map(() => "(hm.content LIKE ? OR hm.summary LIKE ?)");
1176
+ const params: (string | number)[] = [];
1177
+ for (const p of patterns) { params.push(`%${p}%`, `%${p}%`); }
1178
+ params.push(limit);
1179
+ try {
1180
+ const rows = this.db.prepare(`
1181
+ SELECT hm.id as memory_id, hm.content, hm.role, hm.created_at
1182
+ FROM hub_memories hm
1183
+ WHERE ${conditions.join(" OR ")}
1184
+ ORDER BY hm.created_at DESC
1185
+ LIMIT ?
1186
+ `).all(...params) as Array<{ memory_id: string; content: string; role: string; created_at: number }>;
1187
+ return rows.map(r => ({ memoryId: r.memory_id, content: r.content, role: r.role, createdAt: r.created_at }));
1188
+ } catch { return []; }
1189
+ }
1190
+
1050
1191
  // ─── Vector Search ───
1051
1192
 
1052
1193
  getAllEmbeddings(ownerFilter?: string[]): Array<{ chunkId: string; vector: number[] }> {
@@ -1213,6 +1354,8 @@ export class SqliteStore {
1213
1354
  "skill_embeddings",
1214
1355
  "skill_versions",
1215
1356
  "skills",
1357
+ "local_shared_memories",
1358
+ "local_shared_tasks",
1216
1359
  "embeddings",
1217
1360
  "chunks",
1218
1361
  "tasks",
@@ -1633,16 +1776,18 @@ export class SqliteStore {
1633
1776
 
1634
1777
  setClientHubConnection(conn: ClientHubConnection): void {
1635
1778
  this.db.prepare(`
1636
- INSERT INTO client_hub_connection (id, hub_url, user_id, username, user_token, role, connected_at)
1637
- VALUES (1, ?, ?, ?, ?, ?, ?)
1779
+ INSERT INTO client_hub_connection (id, hub_url, user_id, username, user_token, role, connected_at, identity_key, last_known_status)
1780
+ VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?)
1638
1781
  ON CONFLICT(id) DO UPDATE SET
1639
1782
  hub_url = excluded.hub_url,
1640
1783
  user_id = excluded.user_id,
1641
1784
  username = excluded.username,
1642
1785
  user_token = excluded.user_token,
1643
1786
  role = excluded.role,
1644
- connected_at = excluded.connected_at
1645
- `).run(conn.hubUrl, conn.userId, conn.username, conn.userToken, conn.role, conn.connectedAt);
1787
+ connected_at = excluded.connected_at,
1788
+ identity_key = excluded.identity_key,
1789
+ last_known_status = excluded.last_known_status
1790
+ `).run(conn.hubUrl, conn.userId, conn.username, conn.userToken, conn.role, conn.connectedAt, conn.identityKey ?? "", conn.lastKnownStatus ?? "");
1646
1791
  }
1647
1792
 
1648
1793
  getClientHubConnection(): ClientHubConnection | null {
@@ -1684,12 +1829,73 @@ export class SqliteStore {
1684
1829
  return rows.map(r => ({ taskId: r.task_id, hubTaskId: r.hub_task_id, visibility: r.visibility, groupId: r.group_id, syncedChunks: r.synced_chunks }));
1685
1830
  }
1686
1831
 
1832
+ // ─── Local Shared Memories (client-side tracking) ───
1833
+
1834
+ markMemorySharedLocally(chunkId: string): { ok: boolean; owner?: string; originalOwner?: string; sharedAt?: number; reason?: string } {
1835
+ const chunk = this.getChunk(chunkId);
1836
+ if (!chunk) return { ok: false, reason: "not_found" };
1837
+ if (chunk.owner === "public") {
1838
+ const existing = this.getLocalSharedMemory(chunkId);
1839
+ return {
1840
+ ok: true,
1841
+ owner: "public",
1842
+ originalOwner: existing?.originalOwner ?? undefined,
1843
+ sharedAt: existing?.sharedAt ?? undefined,
1844
+ };
1845
+ }
1846
+
1847
+ const sharedAt = Date.now();
1848
+ this.db.transaction(() => {
1849
+ this.db.prepare(`
1850
+ INSERT INTO local_shared_memories (chunk_id, original_owner, shared_at)
1851
+ VALUES (?, ?, ?)
1852
+ ON CONFLICT(chunk_id) DO UPDATE SET
1853
+ original_owner = excluded.original_owner,
1854
+ shared_at = excluded.shared_at
1855
+ `).run(chunkId, chunk.owner, sharedAt);
1856
+ this.updateChunk(chunkId, { owner: "public" });
1857
+ })();
1858
+
1859
+ return { ok: true, owner: "public", originalOwner: chunk.owner, sharedAt };
1860
+ }
1861
+
1862
+ unmarkMemorySharedLocally(chunkId: string, fallbackOwner?: string): { ok: boolean; owner?: string; originalOwner?: string; reason?: string } {
1863
+ const chunk = this.getChunk(chunkId);
1864
+ if (!chunk) return { ok: false, reason: "not_found" };
1865
+ if (chunk.owner !== "public") {
1866
+ return { ok: true, owner: chunk.owner };
1867
+ }
1868
+
1869
+ const existing = this.getLocalSharedMemory(chunkId);
1870
+ const restoreOwner = existing?.originalOwner ?? fallbackOwner;
1871
+ if (!restoreOwner || restoreOwner === "public") {
1872
+ return { ok: false, reason: "original_owner_missing" };
1873
+ }
1874
+
1875
+ this.db.transaction(() => {
1876
+ this.updateChunk(chunkId, { owner: restoreOwner });
1877
+ this.db.prepare("DELETE FROM local_shared_memories WHERE chunk_id = ?").run(chunkId);
1878
+ })();
1879
+
1880
+ return { ok: true, owner: restoreOwner, originalOwner: restoreOwner };
1881
+ }
1882
+
1883
+ getLocalSharedMemory(chunkId: string): { chunkId: string; originalOwner: string; sharedAt: number } | null {
1884
+ const row = this.db.prepare("SELECT chunk_id, original_owner, shared_at FROM local_shared_memories WHERE chunk_id = ?").get(chunkId) as any;
1885
+ if (!row) return null;
1886
+ return {
1887
+ chunkId: row.chunk_id,
1888
+ originalOwner: row.original_owner,
1889
+ sharedAt: row.shared_at,
1890
+ };
1891
+ }
1892
+
1687
1893
  // ─── Hub Users / Groups ───
1688
1894
 
1689
1895
  upsertHubUser(user: HubUserRecord): void {
1690
1896
  this.db.prepare(`
1691
- INSERT INTO hub_users (id, username, device_name, role, status, token_hash, created_at, approved_at)
1692
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1897
+ 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)
1898
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1693
1899
  ON CONFLICT(id) DO UPDATE SET
1694
1900
  username = excluded.username,
1695
1901
  device_name = excluded.device_name,
@@ -1697,81 +1903,64 @@ export class SqliteStore {
1697
1903
  status = excluded.status,
1698
1904
  token_hash = excluded.token_hash,
1699
1905
  created_at = excluded.created_at,
1700
- approved_at = excluded.approved_at
1701
- `).run(user.id, user.username, user.deviceName ?? "", user.role, user.status, user.tokenHash, user.createdAt, user.approvedAt);
1906
+ approved_at = excluded.approved_at,
1907
+ identity_key = excluded.identity_key,
1908
+ left_at = excluded.left_at,
1909
+ removed_at = excluded.removed_at,
1910
+ rejected_at = excluded.rejected_at,
1911
+ rejoin_requested_at = excluded.rejoin_requested_at
1912
+ `).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);
1702
1913
  }
1703
1914
 
1704
1915
  getHubUser(userId: string): HubUserRecord | null {
1705
1916
  const row = this.db.prepare('SELECT * FROM hub_users WHERE id = ?').get(userId) as HubUserRow | undefined;
1706
1917
  if (!row) return null;
1707
- return this.attachGroupsToHubUser(rowToHubUser(row));
1918
+ return rowToHubUser(row);
1708
1919
  }
1709
1920
 
1710
1921
  listHubUsers(status?: UserStatus): HubUserRecord[] {
1711
1922
  const rows = status
1712
1923
  ? this.db.prepare('SELECT * FROM hub_users WHERE status = ? ORDER BY created_at').all(status) as HubUserRow[]
1713
1924
  : this.db.prepare('SELECT * FROM hub_users ORDER BY created_at').all() as HubUserRow[];
1714
- return rows.map((row) => this.attachGroupsToHubUser(rowToHubUser(row)));
1925
+ return rows.map(rowToHubUser);
1715
1926
  }
1716
1927
 
1717
- upsertHubGroup(group: HubGroupRecord): void {
1718
- this.db.prepare(`
1719
- INSERT INTO hub_groups (id, name, description, created_at)
1720
- VALUES (?, ?, ?, ?)
1721
- ON CONFLICT(id) DO UPDATE SET
1722
- name = excluded.name,
1723
- description = excluded.description,
1724
- created_at = excluded.created_at
1725
- `).run(group.id, group.name, group.description, group.createdAt);
1726
- }
1727
-
1728
- listHubGroups(): HubGroupRecord[] {
1729
- const rows = this.db.prepare('SELECT * FROM hub_groups ORDER BY name').all() as HubGroupRow[];
1730
- return rows.map(rowToHubGroup);
1731
- }
1732
-
1733
- addHubGroupMember(groupId: string, userId: string, joinedAt = Date.now()): void {
1734
- this.db.prepare(`
1735
- INSERT INTO hub_group_members (group_id, user_id, joined_at)
1736
- VALUES (?, ?, ?)
1737
- ON CONFLICT(group_id, user_id) DO UPDATE SET joined_at = excluded.joined_at
1738
- `).run(groupId, userId, joinedAt);
1928
+ deleteHubUser(userId: string, cleanResources = false): boolean {
1929
+ if (cleanResources) {
1930
+ this.db.prepare('DELETE FROM hub_tasks WHERE source_user_id = ?').run(userId);
1931
+ this.db.prepare('DELETE FROM hub_skills WHERE source_user_id = ?').run(userId);
1932
+ this.db.prepare('DELETE FROM hub_memories WHERE source_user_id = ?').run(userId);
1933
+ const result = this.db.prepare('DELETE FROM hub_users WHERE id = ?').run(userId);
1934
+ return result.changes > 0;
1935
+ }
1936
+ const result = this.db.prepare("UPDATE hub_users SET status = 'removed', token_hash = '', removed_at = ? WHERE id = ?").run(Date.now(), userId);
1937
+ return result.changes > 0;
1739
1938
  }
1740
1939
 
1741
- getHubGroupById(groupId: string): HubGroupRecord | undefined {
1742
- const row = this.db.prepare('SELECT * FROM hub_groups WHERE id = ?').get(groupId) as HubGroupRow | undefined;
1743
- return row ? rowToHubGroup(row) : undefined;
1940
+ findHubUserByIdentityKey(identityKey: string): HubUserRecord | null {
1941
+ if (!identityKey) return null;
1942
+ const row = this.db.prepare('SELECT * FROM hub_users WHERE identity_key = ?').get(identityKey) as HubUserRow | undefined;
1943
+ return row ? rowToHubUser(row) : null;
1744
1944
  }
1745
1945
 
1746
- deleteHubGroup(groupId: string): boolean {
1747
- const result = this.db.prepare('DELETE FROM hub_groups WHERE id = ?').run(groupId);
1946
+ markHubUserLeft(userId: string): boolean {
1947
+ const result = this.db.prepare("UPDATE hub_users SET status = 'left', token_hash = '', left_at = ? WHERE id = ?").run(Date.now(), userId);
1748
1948
  return result.changes > 0;
1749
1949
  }
1750
1950
 
1751
- listHubGroupMembers(groupId: string): Array<{ userId: string; username: string; joinedAt: number }> {
1752
- const rows = this.db.prepare(`
1753
- SELECT gm.user_id, hu.username, gm.joined_at
1754
- FROM hub_group_members gm
1755
- JOIN hub_users hu ON hu.id = gm.user_id
1756
- WHERE gm.group_id = ?
1757
- ORDER BY gm.joined_at
1758
- `).all(groupId) as Array<{ user_id: string; username: string; joined_at: number }>;
1759
- return rows.map(r => ({ userId: r.user_id, username: r.username, joinedAt: r.joined_at }));
1951
+ updateHubUserActivity(userId: string, ip: string, timestamp?: number): void {
1952
+ this.db.prepare('UPDATE hub_users SET last_ip = ?, last_active_at = ? WHERE id = ?').run(ip, timestamp ?? Date.now(), userId);
1760
1953
  }
1761
1954
 
1762
- removeHubGroupMember(groupId: string, userId: string): void {
1763
- this.db.prepare('DELETE FROM hub_group_members WHERE group_id = ? AND user_id = ?').run(groupId, userId);
1764
- }
1765
-
1766
- getGroupsForHubUser(userId: string): GroupInfo[] {
1767
- const rows = this.db.prepare(`
1768
- SELECT g.*
1769
- FROM hub_group_members gm
1770
- JOIN hub_groups g ON g.id = gm.group_id
1771
- WHERE gm.user_id = ?
1772
- ORDER BY g.name
1773
- `).all(userId) as HubGroupRow[];
1774
- return rows.map((row) => ({ id: row.id, name: row.name, description: row.description || undefined }));
1955
+ getHubUserContributions(): Record<string, { memoryCount: number; taskCount: number; skillCount: number }> {
1956
+ const result: Record<string, { memoryCount: number; taskCount: number; skillCount: number }> = {};
1957
+ const memRows = this.db.prepare('SELECT source_user_id, COUNT(*) as cnt FROM hub_memories GROUP BY source_user_id').all() as Array<{ source_user_id: string; cnt: number }>;
1958
+ const taskRows = this.db.prepare('SELECT source_user_id, COUNT(*) as cnt FROM hub_tasks GROUP BY source_user_id').all() as Array<{ source_user_id: string; cnt: number }>;
1959
+ const skillRows = this.db.prepare('SELECT source_user_id, COUNT(*) as cnt FROM hub_skills GROUP BY source_user_id').all() as Array<{ source_user_id: string; cnt: number }>;
1960
+ for (const r of memRows) { if (!result[r.source_user_id]) result[r.source_user_id] = { memoryCount: 0, taskCount: 0, skillCount: 0 }; result[r.source_user_id].memoryCount = r.cnt; }
1961
+ for (const r of taskRows) { if (!result[r.source_user_id]) result[r.source_user_id] = { memoryCount: 0, taskCount: 0, skillCount: 0 }; result[r.source_user_id].taskCount = r.cnt; }
1962
+ for (const r of skillRows) { if (!result[r.source_user_id]) result[r.source_user_id] = { memoryCount: 0, taskCount: 0, skillCount: 0 }; result[r.source_user_id].skillCount = r.cnt; }
1963
+ return result;
1775
1964
  }
1776
1965
 
1777
1966
  // ─── Hub Shared Data ───
@@ -1795,6 +1984,11 @@ export class SqliteStore {
1795
1984
  return row ? rowToHubTask(row) : null;
1796
1985
  }
1797
1986
 
1987
+ getHubTaskById(taskId: string): HubTaskRecord | null {
1988
+ const row = this.db.prepare('SELECT * FROM hub_tasks WHERE id = ?').get(taskId) as HubTaskRow | undefined;
1989
+ return row ? rowToHubTask(row) : null;
1990
+ }
1991
+
1798
1992
  upsertHubChunk(chunk: HubChunkUpsertInput): void {
1799
1993
  if (!chunk.sourceTaskId) throw new Error("sourceTaskId is required for hub chunk upserts");
1800
1994
  const taskId = this.resolveCanonicalHubTaskId(chunk.hubTaskId, chunk.sourceUserId, chunk.sourceTaskId);
@@ -1870,6 +2064,18 @@ export class SqliteStore {
1870
2064
  return out;
1871
2065
  }
1872
2066
 
2067
+ getVisibleHubSkillEmbeddings(): Array<{ skillId: string; vector: Float32Array }> {
2068
+ const rows = this.db.prepare(`
2069
+ SELECT hse.skill_id, hse.vector, hse.dimensions
2070
+ FROM hub_skill_embeddings hse
2071
+ JOIN hub_skills hs ON hs.id = hse.skill_id
2072
+ `).all() as Array<{ skill_id: string; vector: Buffer; dimensions: number }>;
2073
+ return rows.map(r => ({
2074
+ skillId: r.skill_id,
2075
+ vector: new Float32Array(r.vector.buffer, r.vector.byteOffset, r.dimensions),
2076
+ }));
2077
+ }
2078
+
1873
2079
  searchHubChunks(query: string, options?: { userId?: string; maxResults?: number }): Array<{ hit: HubSearchRow; rank: number }> {
1874
2080
  const limit = options?.maxResults ?? 10;
1875
2081
  const userId = options?.userId ?? "";
@@ -1940,7 +2146,7 @@ export class SqliteStore {
1940
2146
  let rows: HubSkillSearchRow[];
1941
2147
  if (sanitized) {
1942
2148
  rows = this.db.prepare(`
1943
- SELECT hs.id, hs.name, hs.description, hs.version, hs.visibility, '' AS group_name, hu.username AS owner_name, hs.quality_score,
2149
+ 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,
1944
2150
  bm25(hub_skills_fts) as rank
1945
2151
  FROM hub_skills_fts f
1946
2152
  JOIN hub_skills hs ON hs.rowid = f.rowid
@@ -1951,7 +2157,7 @@ export class SqliteStore {
1951
2157
  `).all(sanitized, limit) as HubSkillSearchRow[];
1952
2158
  } else {
1953
2159
  rows = this.db.prepare(`
1954
- SELECT hs.id, hs.name, hs.description, hs.version, hs.visibility, '' AS group_name, hu.username AS owner_name, hs.quality_score,
2160
+ 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,
1955
2161
  0 as rank
1956
2162
  FROM hub_skills hs
1957
2163
  LEFT JOIN hub_users hu ON hu.id = hs.source_user_id
@@ -1966,9 +2172,9 @@ export class SqliteStore {
1966
2172
  this.db.prepare('DELETE FROM hub_skills WHERE source_user_id = ? AND source_skill_id = ?').run(sourceUserId, sourceSkillId);
1967
2173
  }
1968
2174
 
1969
- listVisibleHubTasks(userId: string, limit = 40): Array<{ id: string; sourceTaskId: string; sourceUserId: string; title: string; summary: string; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; chunkCount: number; createdAt: number; updatedAt: number }> {
2175
+ listVisibleHubTasks(userId: string, limit = 40): Array<{ id: string; sourceTaskId: string; sourceUserId: string; title: string; summary: string; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; ownerStatus: string; chunkCount: number; createdAt: number; updatedAt: number }> {
1970
2176
  const rows = this.db.prepare(`
1971
- SELECT t.*, u.username AS owner_name, NULL AS group_name,
2177
+ SELECT t.*, u.username AS owner_name, u.status AS owner_status, NULL AS group_name,
1972
2178
  (SELECT COUNT(*) FROM hub_chunks c WHERE c.hub_task_id = t.id) AS chunk_count
1973
2179
  FROM hub_tasks t
1974
2180
  LEFT JOIN hub_users u ON u.id = t.source_user_id
@@ -1978,36 +2184,40 @@ export class SqliteStore {
1978
2184
  return rows.map(r => ({
1979
2185
  id: r.id, sourceTaskId: r.source_task_id, sourceUserId: r.source_user_id,
1980
2186
  title: r.title, summary: r.summary, groupId: r.group_id, groupName: r.group_name ?? null,
1981
- visibility: r.visibility, ownerName: r.owner_name ?? "unknown", chunkCount: r.chunk_count ?? 0,
2187
+ visibility: r.visibility, ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", chunkCount: r.chunk_count ?? 0,
1982
2188
  createdAt: r.created_at, updatedAt: r.updated_at,
1983
2189
  }));
1984
2190
  }
1985
2191
 
1986
- listAllHubTasks(): Array<{ id: string; sourceTaskId: string; sourceUserId: string; title: string; summary: string; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; chunkCount: number; createdAt: number; updatedAt: number }> {
2192
+ listAllHubTasks(): Array<{ id: string; sourceTaskId: string; sourceUserId: string; title: string; summary: string; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; ownerStatus: string; chunkCount: number; createdAt: number; updatedAt: number }> {
1987
2193
  const rows = this.db.prepare(`
1988
- SELECT t.*, u.username AS owner_name, g.name AS group_name,
2194
+ SELECT t.*, u.username AS owner_name, u.status AS owner_status,
1989
2195
  (SELECT COUNT(*) FROM hub_chunks c WHERE c.hub_task_id = t.id) AS chunk_count
1990
2196
  FROM hub_tasks t
1991
2197
  LEFT JOIN hub_users u ON u.id = t.source_user_id
1992
- LEFT JOIN hub_groups g ON g.id = t.group_id
1993
2198
  ORDER BY t.updated_at DESC
1994
2199
  `).all() as any[];
1995
2200
  return rows.map(r => ({
1996
2201
  id: r.id, sourceTaskId: r.source_task_id, sourceUserId: r.source_user_id,
1997
- title: r.title, summary: r.summary, groupId: r.group_id, groupName: r.group_name ?? null,
1998
- visibility: r.visibility, ownerName: r.owner_name ?? "unknown", chunkCount: r.chunk_count ?? 0,
2202
+ title: r.title, summary: r.summary, groupId: r.group_id, groupName: null as string | null,
2203
+ visibility: r.visibility, ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", chunkCount: r.chunk_count ?? 0,
1999
2204
  createdAt: r.created_at, updatedAt: r.updated_at,
2000
2205
  }));
2001
2206
  }
2002
2207
 
2208
+ listHubChunksByTaskId(hubTaskId: string): HubChunkRecord[] {
2209
+ const rows = this.db.prepare('SELECT * FROM hub_chunks WHERE hub_task_id = ? ORDER BY created_at ASC').all(hubTaskId) as HubChunkRow[];
2210
+ return rows.map(rowToHubChunk);
2211
+ }
2212
+
2003
2213
  deleteHubTaskById(taskId: string): boolean {
2004
2214
  const info = this.db.prepare('DELETE FROM hub_tasks WHERE id = ?').run(taskId);
2005
2215
  return info.changes > 0;
2006
2216
  }
2007
2217
 
2008
- listVisibleHubSkills(userId: string, limit = 40): Array<{ id: string; sourceSkillId: string; sourceUserId: string; name: string; description: string; version: number; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; qualityScore: number | null; createdAt: number; updatedAt: number }> {
2218
+ listVisibleHubSkills(userId: string, limit = 40): Array<{ id: string; sourceSkillId: string; sourceUserId: string; name: string; description: string; version: number; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; ownerStatus: string; qualityScore: number | null; createdAt: number; updatedAt: number }> {
2009
2219
  const rows = this.db.prepare(`
2010
- SELECT s.*, u.username AS owner_name, NULL AS group_name
2220
+ SELECT s.*, u.username AS owner_name, u.status AS owner_status, NULL AS group_name
2011
2221
  FROM hub_skills s
2012
2222
  LEFT JOIN hub_users u ON u.id = s.source_user_id
2013
2223
  ORDER BY s.updated_at DESC
@@ -2017,24 +2227,23 @@ export class SqliteStore {
2017
2227
  id: r.id, sourceSkillId: r.source_skill_id, sourceUserId: r.source_user_id,
2018
2228
  name: r.name, description: r.description, version: r.version,
2019
2229
  groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
2020
- ownerName: r.owner_name ?? "unknown", qualityScore: r.quality_score,
2230
+ ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", qualityScore: r.quality_score,
2021
2231
  createdAt: r.created_at, updatedAt: r.updated_at,
2022
2232
  }));
2023
2233
  }
2024
2234
 
2025
- listAllHubSkills(): Array<{ id: string; sourceSkillId: string; sourceUserId: string; name: string; description: string; version: number; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; qualityScore: number | null; createdAt: number; updatedAt: number }> {
2235
+ listAllHubSkills(): Array<{ id: string; sourceSkillId: string; sourceUserId: string; name: string; description: string; version: number; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; ownerStatus: string; qualityScore: number | null; createdAt: number; updatedAt: number }> {
2026
2236
  const rows = this.db.prepare(`
2027
- SELECT s.*, u.username AS owner_name, g.name AS group_name
2237
+ SELECT s.*, u.username AS owner_name, u.status AS owner_status
2028
2238
  FROM hub_skills s
2029
2239
  LEFT JOIN hub_users u ON u.id = s.source_user_id
2030
- LEFT JOIN hub_groups g ON g.id = s.group_id
2031
2240
  ORDER BY s.updated_at DESC
2032
2241
  `).all() as any[];
2033
2242
  return rows.map(r => ({
2034
2243
  id: r.id, sourceSkillId: r.source_skill_id, sourceUserId: r.source_user_id,
2035
2244
  name: r.name, description: r.description, version: r.version,
2036
- groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
2037
- ownerName: r.owner_name ?? "unknown", qualityScore: r.quality_score,
2245
+ groupId: r.group_id, groupName: null as string | null, visibility: r.visibility,
2246
+ ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", qualityScore: r.quality_score,
2038
2247
  createdAt: r.created_at, updatedAt: r.updated_at,
2039
2248
  }));
2040
2249
  }
@@ -2081,6 +2290,47 @@ export class SqliteStore {
2081
2290
  return info.changes > 0;
2082
2291
  }
2083
2292
 
2293
+ // ─── Hub Notifications ───
2294
+
2295
+ insertHubNotification(n: { id: string; userId: string; type: string; resource: string; title: string; message?: string }): void {
2296
+ this.db.prepare(
2297
+ 'INSERT INTO hub_notifications (id, user_id, type, resource, title, message, read, created_at) VALUES (?, ?, ?, ?, ?, ?, 0, ?)'
2298
+ ).run(n.id, n.userId, n.type, n.resource, n.title, n.message ?? '', Date.now());
2299
+ }
2300
+
2301
+ hasRecentHubNotification(userId: string, type: string, resource: string, windowMs: number = 300_000): boolean {
2302
+ const since = Date.now() - windowMs;
2303
+ const row = this.db.prepare(
2304
+ 'SELECT COUNT(*) AS cnt FROM hub_notifications WHERE user_id = ? AND type = ? AND resource = ? AND created_at > ?'
2305
+ ).get(userId, type, resource, since) as { cnt: number };
2306
+ return row.cnt > 0;
2307
+ }
2308
+
2309
+ listHubNotifications(userId: string, opts?: { unreadOnly?: boolean; limit?: number }): Array<{ id: string; userId: string; type: string; resource: string; title: string; message: string; read: boolean; createdAt: number }> {
2310
+ const where = opts?.unreadOnly ? 'WHERE user_id = ? AND read = 0' : 'WHERE user_id = ?';
2311
+ const limit = opts?.limit ?? 50;
2312
+ const rows = this.db.prepare(`SELECT * FROM hub_notifications ${where} ORDER BY created_at DESC LIMIT ?`).all(userId, limit) as any[];
2313
+ 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 }));
2314
+ }
2315
+
2316
+ countUnreadHubNotifications(userId: string): number {
2317
+ const row = this.db.prepare('SELECT COUNT(*) AS cnt FROM hub_notifications WHERE user_id = ? AND read = 0').get(userId) as { cnt: number };
2318
+ return row.cnt;
2319
+ }
2320
+
2321
+ markHubNotificationsRead(userId: string, ids?: string[]): void {
2322
+ if (ids && ids.length > 0) {
2323
+ const placeholders = ids.map(() => '?').join(',');
2324
+ this.db.prepare(`UPDATE hub_notifications SET read = 1 WHERE user_id = ? AND id IN (${placeholders})`).run(userId, ...ids);
2325
+ } else {
2326
+ this.db.prepare('UPDATE hub_notifications SET read = 1 WHERE user_id = ?').run(userId);
2327
+ }
2328
+ }
2329
+
2330
+ clearHubNotifications(userId: string): void {
2331
+ this.db.prepare('DELETE FROM hub_notifications WHERE user_id = ?').run(userId);
2332
+ }
2333
+
2084
2334
  upsertHubMemoryEmbedding(memoryId: string, vector: Float32Array): void {
2085
2335
  const buf = Buffer.from(vector.buffer, vector.byteOffset, vector.byteLength);
2086
2336
  this.db.prepare(`
@@ -2138,9 +2388,9 @@ export class SqliteStore {
2138
2388
  return row ?? null;
2139
2389
  }
2140
2390
 
2141
- listVisibleHubMemories(userId: string, limit = 40): Array<{ id: string; sourceChunkId: string; sourceUserId: string; role: string; summary: string; kind: string; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; createdAt: number; updatedAt: number }> {
2391
+ listVisibleHubMemories(userId: string, limit = 40): Array<{ id: string; sourceChunkId: string; sourceUserId: string; role: string; content: string; summary: string; kind: string; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; ownerStatus: string; createdAt: number; updatedAt: number }> {
2142
2392
  const rows = this.db.prepare(`
2143
- SELECT m.*, u.username AS owner_name, NULL AS group_name
2393
+ SELECT m.*, u.username AS owner_name, u.status AS owner_status, NULL AS group_name
2144
2394
  FROM hub_memories m
2145
2395
  LEFT JOIN hub_users u ON u.id = m.source_user_id
2146
2396
  ORDER BY m.updated_at DESC
@@ -2148,25 +2398,24 @@ export class SqliteStore {
2148
2398
  `).all(limit) as any[];
2149
2399
  return rows.map(r => ({
2150
2400
  id: r.id, sourceChunkId: r.source_chunk_id, sourceUserId: r.source_user_id,
2151
- role: r.role, summary: r.summary, kind: r.kind,
2401
+ role: r.role, content: r.content ?? "", summary: r.summary, kind: r.kind,
2152
2402
  groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
2153
- ownerName: r.owner_name ?? "unknown", createdAt: r.created_at, updatedAt: r.updated_at,
2403
+ ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", createdAt: r.created_at, updatedAt: r.updated_at,
2154
2404
  }));
2155
2405
  }
2156
2406
 
2157
- listAllHubMemories(): Array<{ id: string; sourceChunkId: string; sourceUserId: string; role: string; summary: string; kind: string; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; createdAt: number; updatedAt: number }> {
2407
+ listAllHubMemories(): Array<{ id: string; sourceChunkId: string; sourceUserId: string; role: string; content: string; summary: string; kind: string; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; ownerStatus: string; createdAt: number; updatedAt: number }> {
2158
2408
  const rows = this.db.prepare(`
2159
- SELECT m.*, u.username AS owner_name, g.name AS group_name
2409
+ SELECT m.*, u.username AS owner_name, u.status AS owner_status
2160
2410
  FROM hub_memories m
2161
2411
  LEFT JOIN hub_users u ON u.id = m.source_user_id
2162
- LEFT JOIN hub_groups g ON g.id = m.group_id
2163
2412
  ORDER BY m.updated_at DESC
2164
2413
  `).all() as any[];
2165
2414
  return rows.map(r => ({
2166
2415
  id: r.id, sourceChunkId: r.source_chunk_id, sourceUserId: r.source_user_id,
2167
- role: r.role, summary: r.summary, kind: r.kind,
2168
- groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
2169
- ownerName: r.owner_name ?? "unknown", createdAt: r.created_at, updatedAt: r.updated_at,
2416
+ role: r.role, content: r.content ?? "", summary: r.summary, kind: r.kind,
2417
+ groupId: r.group_id, groupName: null as string | null, visibility: r.visibility,
2418
+ ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", createdAt: r.created_at, updatedAt: r.updated_at,
2170
2419
  }));
2171
2420
  }
2172
2421
 
@@ -2190,13 +2439,6 @@ export class SqliteStore {
2190
2439
  throw new Error(`source skill not found for skillId=${skillId}`);
2191
2440
  }
2192
2441
 
2193
- private attachGroupsToHubUser(user: HubUserRecord): HubUserRecord {
2194
- return {
2195
- ...user,
2196
- groups: this.getGroupsForHubUser(user.id),
2197
- };
2198
- }
2199
-
2200
2442
  getSessionOwnerMap(sessionKeys: string[]): Map<string, string> {
2201
2443
  const result = new Map<string, string>();
2202
2444
  if (sessionKeys.length === 0) return result;
@@ -2382,6 +2624,8 @@ interface ClientHubConnection {
2382
2624
  userToken: string;
2383
2625
  role: UserRole;
2384
2626
  connectedAt: number;
2627
+ identityKey?: string;
2628
+ lastKnownStatus?: string;
2385
2629
  }
2386
2630
 
2387
2631
  interface ClientHubConnectionRow {
@@ -2391,6 +2635,8 @@ interface ClientHubConnectionRow {
2391
2635
  user_token: string;
2392
2636
  role: string;
2393
2637
  connected_at: number;
2638
+ identity_key?: string;
2639
+ last_known_status?: string;
2394
2640
  }
2395
2641
 
2396
2642
  function rowToClientHubConnection(row: ClientHubConnectionRow): ClientHubConnection {
@@ -2401,6 +2647,8 @@ function rowToClientHubConnection(row: ClientHubConnectionRow): ClientHubConnect
2401
2647
  userToken: row.user_token,
2402
2648
  role: row.role as UserRole,
2403
2649
  connectedAt: row.connected_at,
2650
+ identityKey: row.identity_key || "",
2651
+ lastKnownStatus: row.last_known_status || "",
2404
2652
  };
2405
2653
  }
2406
2654
 
@@ -2408,6 +2656,13 @@ interface HubUserRecord extends UserInfo {
2408
2656
  tokenHash: string;
2409
2657
  createdAt: number;
2410
2658
  approvedAt: number | null;
2659
+ lastIp: string;
2660
+ lastActiveAt: number | null;
2661
+ identityKey?: string;
2662
+ leftAt?: number | null;
2663
+ removedAt?: number | null;
2664
+ rejectedAt?: number | null;
2665
+ rejoinRequestedAt?: number | null;
2411
2666
  }
2412
2667
 
2413
2668
  interface HubUserRow {
@@ -2419,6 +2674,13 @@ interface HubUserRow {
2419
2674
  token_hash: string;
2420
2675
  created_at: number;
2421
2676
  approved_at: number | null;
2677
+ last_ip: string;
2678
+ last_active_at: number | null;
2679
+ identity_key?: string;
2680
+ left_at?: number | null;
2681
+ removed_at?: number | null;
2682
+ rejected_at?: number | null;
2683
+ rejoin_requested_at?: number | null;
2422
2684
  }
2423
2685
 
2424
2686
  function rowToHubUser(row: HubUserRow): HubUserRecord {
@@ -2432,29 +2694,13 @@ function rowToHubUser(row: HubUserRow): HubUserRecord {
2432
2694
  tokenHash: row.token_hash,
2433
2695
  createdAt: row.created_at,
2434
2696
  approvedAt: row.approved_at,
2435
- };
2436
- }
2437
-
2438
- interface HubGroupRecord {
2439
- id: string;
2440
- name: string;
2441
- description: string;
2442
- createdAt: number;
2443
- }
2444
-
2445
- interface HubGroupRow {
2446
- id: string;
2447
- name: string;
2448
- description: string;
2449
- created_at: number;
2450
- }
2451
-
2452
- function rowToHubGroup(row: HubGroupRow): HubGroupRecord {
2453
- return {
2454
- id: row.id,
2455
- name: row.name,
2456
- description: row.description,
2457
- createdAt: row.created_at,
2697
+ lastIp: row.last_ip || "",
2698
+ lastActiveAt: row.last_active_at ?? null,
2699
+ identityKey: row.identity_key || "",
2700
+ leftAt: row.left_at ?? null,
2701
+ removedAt: row.removed_at ?? null,
2702
+ rejectedAt: row.rejected_at ?? null,
2703
+ rejoinRequestedAt: row.rejoin_requested_at ?? null,
2458
2704
  };
2459
2705
  }
2460
2706
 
@@ -2603,6 +2849,7 @@ interface HubSkillSearchRow {
2603
2849
  visibility: string;
2604
2850
  group_name: string | null;
2605
2851
  owner_name: string | null;
2852
+ owner_status: string | null;
2606
2853
  quality_score: number | null;
2607
2854
  }
2608
2855