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

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.
package/index.ts CHANGED
@@ -386,27 +386,29 @@ const memosLocalPlugin = {
386
386
  }),
387
387
  }) as { memoryId?: string; visibility?: "public" | "group" };
388
388
 
389
- const now = Date.now();
390
- const existing = store.getHubMemoryBySource(hubClient.userId, chunk.id);
391
- store.upsertHubMemory({
392
- id: response?.memoryId ?? existing?.id ?? `${chunk.id}-hub`,
393
- sourceChunkId: chunk.id,
394
- sourceUserId: hubClient.userId,
395
- role: chunk.role,
396
- content: chunk.content,
397
- summary: chunk.summary ?? "",
398
- kind: chunk.kind,
399
- groupId,
400
- visibility,
401
- createdAt: existing?.createdAt ?? now,
402
- updatedAt: now,
403
- });
389
+ const memoryId = response?.memoryId ?? `${chunk.id}-hub`;
390
+
391
+ // Only persist hub_memories locally in Hub mode where this DB owns the data.
392
+ // Client mode relies on the remote Hub for storage and search.
393
+ if (ctx.config.sharing?.role === "hub") {
394
+ const now = Date.now();
395
+ const existing = store.getHubMemoryBySource(hubClient.userId, chunk.id);
396
+ store.upsertHubMemory({
397
+ id: memoryId,
398
+ sourceChunkId: chunk.id,
399
+ sourceUserId: hubClient.userId,
400
+ role: chunk.role,
401
+ content: chunk.content,
402
+ summary: chunk.summary ?? "",
403
+ kind: chunk.kind,
404
+ groupId,
405
+ visibility,
406
+ createdAt: existing?.createdAt ?? now,
407
+ updatedAt: now,
408
+ });
409
+ }
404
410
 
405
- return {
406
- memoryId: response?.memoryId ?? existing?.id ?? `${chunk.id}-hub`,
407
- visibility,
408
- groupId,
409
- };
411
+ return { memoryId, visibility, groupId };
410
412
  };
411
413
 
412
414
  const unshareMemoryFromHub = async (
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memtensor/memos-local-openclaw-plugin",
3
- "version": "1.0.4-beta.11",
3
+ "version": "1.0.4-beta.12",
4
4
  "description": "MemOS Local memory plugin for OpenClaw — full-write, hybrid-recall, progressive retrieval",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -21,6 +21,7 @@ export interface HubStatusInfo {
21
21
  username: string;
22
22
  role: UserRole;
23
23
  status: UserStatus | string;
24
+ groups?: Array<{ id: string; name: string }>;
24
25
  };
25
26
  }
26
27
 
@@ -201,6 +202,7 @@ export async function getHubStatus(store: SqliteStore, config: MemosLocalConfig)
201
202
  username: latestUsername,
202
203
  role: latestRole,
203
204
  status: String(me.status ?? "active"),
205
+ groups: Array.isArray(me.groups) ? me.groups : [],
204
206
  },
205
207
  };
206
208
  } catch (err: any) {
package/src/hub/server.ts CHANGED
@@ -377,18 +377,33 @@ export class HubServer {
377
377
  if (req.method === "POST" && routePath === "/api/v1/hub/admin/approve-user") {
378
378
  if (auth.role !== "admin") return this.json(res, 403, { error: "forbidden" });
379
379
  const body = await this.readJson(req);
380
- const token = issueUserToken({ userId: String(body.userId), username: String(body.username || ""), role: "member", status: "active" }, this.authSecret);
381
- const approved = this.userManager.approveUser(String(body.userId), token);
380
+ const userId = String(body.userId);
381
+ const username = String(body.username || "");
382
+ const token = issueUserToken({ userId, username, role: "member", status: "active" }, this.authSecret);
383
+ const approved = this.userManager.approveUser(userId, token);
382
384
  if (!approved) return this.json(res, 404, { error: "not_found" });
383
- try { this.opts.store.updateHubUserActivity(String(body.userId), ""); } catch { /* best-effort */ }
385
+ try { this.opts.store.updateHubUserActivity(userId, ""); } catch { /* best-effort */ }
386
+ try {
387
+ this.opts.store.insertHubNotification({
388
+ id: randomUUID(), userId, type: "membership_approved",
389
+ resource: "user", title: `Your request to join team "${this.teamName}" has been approved. Welcome!`,
390
+ });
391
+ } catch { /* best-effort */ }
384
392
  return this.json(res, 200, { status: "active", token });
385
393
  }
386
394
 
387
395
  if (req.method === "POST" && routePath === "/api/v1/hub/admin/reject-user") {
388
396
  if (auth.role !== "admin") return this.json(res, 403, { error: "forbidden" });
389
397
  const body = await this.readJson(req);
390
- const rejected = this.userManager.rejectUser(String(body.userId));
398
+ const userId = String(body.userId);
399
+ const rejected = this.userManager.rejectUser(userId);
391
400
  if (!rejected) return this.json(res, 404, { error: "not_found" });
401
+ try {
402
+ this.opts.store.insertHubNotification({
403
+ id: randomUUID(), userId, type: "membership_rejected",
404
+ resource: "user", title: `Your request to join team "${this.teamName}" has been declined.`,
405
+ });
406
+ } catch { /* best-effort */ }
392
407
  return this.json(res, 200, { status: "rejected" });
393
408
  }
394
409
 
@@ -1,6 +1,8 @@
1
+ export type ChunkKind = "paragraph" | "code_block" | "error_stack" | "list" | "command";
2
+
1
3
  export interface RawChunk {
2
4
  content: string;
3
- kind: "paragraph";
5
+ kind: ChunkKind;
4
6
  }
5
7
 
6
8
  const MAX_CHUNK_CHARS = 3000;
@@ -28,21 +30,25 @@ const COMMAND_LINE_RE = /^(?:\$|>|#)\s+.+$/gm;
28
30
  */
29
31
  export function chunkText(text: string): RawChunk[] {
30
32
  let remaining = text;
31
- const slots: Array<{ placeholder: string; content: string }> = [];
33
+ const slots: Array<{ placeholder: string; content: string; kind: ChunkKind }> = [];
32
34
  let counter = 0;
33
35
 
34
- function ph(content: string): string {
36
+ function ph(content: string, kind: ChunkKind = "paragraph"): string {
35
37
  const tag = `\x00SLOT_${counter++}\x00`;
36
- slots.push({ placeholder: tag, content: content.trim() });
38
+ slots.push({ placeholder: tag, content: content.trim(), kind });
37
39
  return tag;
38
40
  }
39
41
 
40
- remaining = remaining.replace(FENCED_CODE_RE, (m) => ph(m));
42
+ remaining = remaining.replace(FENCED_CODE_RE, (m) => ph(m, "code_block"));
41
43
  remaining = extractBraceBlocks(remaining, ph);
42
44
 
43
- const structural: RegExp[] = [ERROR_STACK_RE, LIST_BLOCK_RE, COMMAND_LINE_RE];
44
- for (const re of structural) {
45
- remaining = remaining.replace(re, (m) => ph(m));
45
+ const structuralKinds: Array<[RegExp, ChunkKind]> = [
46
+ [ERROR_STACK_RE, "error_stack"],
47
+ [LIST_BLOCK_RE, "list"],
48
+ [COMMAND_LINE_RE, "command"],
49
+ ];
50
+ for (const [re, kind] of structuralKinds) {
51
+ remaining = remaining.replace(re, (m) => ph(m, kind));
46
52
  }
47
53
 
48
54
  const raw: RawChunk[] = [];
@@ -57,7 +63,7 @@ export function chunkText(text: string): RawChunk[] {
57
63
  for (const part of parts) {
58
64
  const slot = slots.find((s) => s.placeholder === part);
59
65
  if (slot) {
60
- raw.push({ content: slot.content, kind: "paragraph" });
66
+ raw.push({ content: slot.content, kind: slot.kind });
61
67
  } else if (part.trim().length >= MIN_CHUNK_CHARS) {
62
68
  raw.push({ content: part.trim(), kind: "paragraph" });
63
69
  }
@@ -69,7 +75,7 @@ export function chunkText(text: string): RawChunk[] {
69
75
 
70
76
  for (const s of slots) {
71
77
  if (!raw.some((c) => c.content === s.content)) {
72
- raw.push({ content: s.content, kind: "paragraph" });
78
+ raw.push({ content: s.content, kind: s.kind });
73
79
  }
74
80
  }
75
81
 
@@ -85,7 +91,7 @@ export function chunkText(text: string): RawChunk[] {
85
91
  */
86
92
  function extractBraceBlocks(
87
93
  text: string,
88
- ph: (content: string) => string,
94
+ ph: (content: string, kind?: ChunkKind) => string,
89
95
  ): string {
90
96
  const lines = text.split("\n");
91
97
  const result: string[] = [];
@@ -119,7 +125,7 @@ function extractBraceBlocks(
119
125
  if (depth <= 0 || (BLOCK_CLOSE_RE.test(line) && depth <= 0)) {
120
126
  const block = blockLines.join("\n");
121
127
  if (block.trim().length >= MIN_CHUNK_CHARS) {
122
- result.push(ph(block));
128
+ result.push(ph(block, "code_block"));
123
129
  } else {
124
130
  result.push(block);
125
131
  }
@@ -135,7 +141,7 @@ function extractBraceBlocks(
135
141
  if (blockLines.length > 0) {
136
142
  const block = blockLines.join("\n");
137
143
  if (block.trim().length >= MIN_CHUNK_CHARS) {
138
- result.push(ph(block));
144
+ result.push(ph(block, "code_block"));
139
145
  } else {
140
146
  result.push(block);
141
147
  }
@@ -74,11 +74,14 @@ export class RecallEngine {
74
74
  score: 1 / (i + 1),
75
75
  }));
76
76
 
77
- // Step 1c: Hub memories search (when sharing is enabled and hub_memories exist)
77
+ // Step 1c: Hub memories search only in Hub mode where local DB owns the
78
+ // hub_memories data and embeddings were generated by the same Embedder.
79
+ // Client mode must use remote API (hubSearchMemories) to avoid cross-model
80
+ // embedding mismatch.
78
81
  let hubMemFtsRanked: Array<{ id: string; score: number }> = [];
79
82
  let hubMemVecRanked: Array<{ id: string; score: number }> = [];
80
83
  let hubMemPatternRanked: Array<{ id: string; score: number }> = [];
81
- if (query && this.ctx.config.sharing?.enabled) {
84
+ if (query && this.ctx.config.sharing?.enabled && this.ctx.config.sharing.role === "hub") {
82
85
  try {
83
86
  const hubFtsHits = this.store.searchHubMemories(query, { maxResults: candidatePool });
84
87
  hubMemFtsRanked = hubFtsHits.map(({ hit }, i) => ({
@@ -807,6 +807,20 @@ export class SqliteStore {
807
807
  CREATE INDEX IF NOT EXISTS idx_hub_users_status ON hub_users(status);
808
808
  CREATE INDEX IF NOT EXISTS idx_hub_users_role ON hub_users(role);
809
809
 
810
+ CREATE TABLE IF NOT EXISTS hub_groups (
811
+ id TEXT PRIMARY KEY,
812
+ name TEXT NOT NULL,
813
+ description TEXT NOT NULL DEFAULT '',
814
+ created_at INTEGER NOT NULL
815
+ );
816
+
817
+ CREATE TABLE IF NOT EXISTS hub_group_members (
818
+ group_id TEXT NOT NULL REFERENCES hub_groups(id) ON DELETE CASCADE,
819
+ user_id TEXT NOT NULL REFERENCES hub_users(id) ON DELETE CASCADE,
820
+ joined_at INTEGER NOT NULL,
821
+ PRIMARY KEY (group_id, user_id)
822
+ );
823
+
810
824
  CREATE TABLE IF NOT EXISTS hub_tasks (
811
825
  id TEXT PRIMARY KEY,
812
826
  source_task_id TEXT NOT NULL,
@@ -1915,14 +1929,20 @@ export class SqliteStore {
1915
1929
  getHubUser(userId: string): HubUserRecord | null {
1916
1930
  const row = this.db.prepare('SELECT * FROM hub_users WHERE id = ?').get(userId) as HubUserRow | undefined;
1917
1931
  if (!row) return null;
1918
- return rowToHubUser(row);
1932
+ const user = rowToHubUser(row);
1933
+ user.groups = this.getGroupsForHubUser(userId);
1934
+ return user;
1919
1935
  }
1920
1936
 
1921
1937
  listHubUsers(status?: UserStatus): HubUserRecord[] {
1922
1938
  const rows = status
1923
1939
  ? this.db.prepare('SELECT * FROM hub_users WHERE status = ? ORDER BY created_at').all(status) as HubUserRow[]
1924
1940
  : this.db.prepare('SELECT * FROM hub_users ORDER BY created_at').all() as HubUserRow[];
1925
- return rows.map(rowToHubUser);
1941
+ return rows.map(r => {
1942
+ const user = rowToHubUser(r);
1943
+ user.groups = this.getGroupsForHubUser(r.id);
1944
+ return user;
1945
+ });
1926
1946
  }
1927
1947
 
1928
1948
  deleteHubUser(userId: string, cleanResources = false): boolean {
@@ -1952,6 +1972,35 @@ export class SqliteStore {
1952
1972
  this.db.prepare('UPDATE hub_users SET last_ip = ?, last_active_at = ? WHERE id = ?').run(ip, timestamp ?? Date.now(), userId);
1953
1973
  }
1954
1974
 
1975
+ // ─── Hub Groups ───
1976
+
1977
+ upsertHubGroup(group: { id: string; name: string; description?: string; createdAt: number }): void {
1978
+ this.db.prepare(`
1979
+ INSERT INTO hub_groups (id, name, description, created_at)
1980
+ VALUES (?, ?, ?, ?)
1981
+ ON CONFLICT(id) DO UPDATE SET name = excluded.name, description = excluded.description
1982
+ `).run(group.id, group.name, group.description ?? "", group.createdAt);
1983
+ }
1984
+
1985
+ addHubGroupMember(groupId: string, userId: string, joinedAt: number): void {
1986
+ this.db.prepare(`
1987
+ INSERT OR IGNORE INTO hub_group_members (group_id, user_id, joined_at)
1988
+ VALUES (?, ?, ?)
1989
+ `).run(groupId, userId, joinedAt);
1990
+ }
1991
+
1992
+ removeHubGroupMember(groupId: string, userId: string): void {
1993
+ this.db.prepare('DELETE FROM hub_group_members WHERE group_id = ? AND user_id = ?').run(groupId, userId);
1994
+ }
1995
+
1996
+ getGroupsForHubUser(userId: string): Array<{ id: string; name: string; description: string }> {
1997
+ return this.db.prepare(`
1998
+ SELECT g.id, g.name, g.description FROM hub_groups g
1999
+ JOIN hub_group_members m ON m.group_id = g.id
2000
+ WHERE m.user_id = ?
2001
+ `).all(userId) as Array<{ id: string; name: string; description: string }>;
2002
+ }
2003
+
1955
2004
  getHubUserContributions(): Record<string, { memoryCount: number; taskCount: number; skillCount: number }> {
1956
2005
  const result: Record<string, { memoryCount: number; taskCount: number; skillCount: number }> = {};
1957
2006
  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 }>;
@@ -2080,16 +2129,21 @@ export class SqliteStore {
2080
2129
  const limit = options?.maxResults ?? 10;
2081
2130
  const userId = options?.userId ?? "";
2082
2131
  const rows = this.db.prepare(`
2083
- SELECT hc.id, hc.content, hc.summary, hc.role, hc.created_at, ht.title as task_title, ht.visibility, '' as group_name, hu.username as owner_name,
2132
+ SELECT hc.id, hc.content, hc.summary, hc.role, hc.created_at, ht.title as task_title, ht.visibility,
2133
+ COALESCE(hg.name, '') as group_name, hu.username as owner_name,
2084
2134
  bm25(hub_chunks_fts) as rank
2085
2135
  FROM hub_chunks_fts f
2086
2136
  JOIN hub_chunks hc ON hc.rowid = f.rowid
2087
2137
  JOIN hub_tasks ht ON ht.id = hc.hub_task_id
2088
2138
  LEFT JOIN hub_users hu ON hu.id = ht.source_user_id
2139
+ LEFT JOIN hub_groups hg ON hg.id = ht.group_id
2089
2140
  WHERE hub_chunks_fts MATCH ?
2141
+ AND (ht.visibility = 'public'
2142
+ OR ht.source_user_id = ?
2143
+ OR EXISTS (SELECT 1 FROM hub_group_members gm WHERE gm.group_id = ht.group_id AND gm.user_id = ?))
2090
2144
  ORDER BY rank
2091
2145
  LIMIT ?
2092
- `).all(sanitizeFtsQuery(query), limit) as HubSearchRow[];
2146
+ `).all(sanitizeFtsQuery(query), userId, userId, limit) as HubSearchRow[];
2093
2147
  return rows.map((row, idx) => ({ hit: row, rank: idx + 1 }));
2094
2148
  }
2095
2149
 
@@ -2114,7 +2168,10 @@ export class SqliteStore {
2114
2168
  FROM hub_embeddings he
2115
2169
  JOIN hub_chunks hc ON hc.id = he.chunk_id
2116
2170
  JOIN hub_tasks ht ON ht.id = hc.hub_task_id
2117
- `).all() as Array<{ chunk_id: string; vector: Buffer; dimensions: number }>;
2171
+ WHERE ht.visibility = 'public'
2172
+ OR ht.source_user_id = ?
2173
+ OR EXISTS (SELECT 1 FROM hub_group_members gm WHERE gm.group_id = ht.group_id AND gm.user_id = ?)
2174
+ `).all(userId, userId) as Array<{ chunk_id: string; vector: Buffer; dimensions: number }>;
2118
2175
  return rows.map(r => ({
2119
2176
  chunkId: r.chunk_id,
2120
2177
  vector: new Float32Array(r.vector.buffer, r.vector.byteOffset, r.dimensions),
@@ -2123,14 +2180,19 @@ export class SqliteStore {
2123
2180
 
2124
2181
  getVisibleHubSearchHitByChunkId(chunkId: string, userId: string): HubSearchRow | null {
2125
2182
  const row = this.db.prepare(`
2126
- SELECT hc.id, hc.content, hc.summary, hc.role, hc.created_at, ht.title as task_title, ht.visibility, '' as group_name, hu.username as owner_name,
2183
+ SELECT hc.id, hc.content, hc.summary, hc.role, hc.created_at, ht.title as task_title, ht.visibility,
2184
+ COALESCE(hg.name, '') as group_name, hu.username as owner_name,
2127
2185
  0 as rank
2128
2186
  FROM hub_chunks hc
2129
2187
  JOIN hub_tasks ht ON ht.id = hc.hub_task_id
2130
2188
  LEFT JOIN hub_users hu ON hu.id = ht.source_user_id
2189
+ LEFT JOIN hub_groups hg ON hg.id = ht.group_id
2131
2190
  WHERE hc.id = ?
2191
+ AND (ht.visibility = 'public'
2192
+ OR ht.source_user_id = ?
2193
+ OR EXISTS (SELECT 1 FROM hub_group_members gm WHERE gm.group_id = ht.group_id AND gm.user_id = ?))
2132
2194
  LIMIT 1
2133
- `).get(chunkId) as HubSearchRow | undefined;
2195
+ `).get(chunkId, userId, userId) as HubSearchRow | undefined;
2134
2196
  return row ?? null;
2135
2197
  }
2136
2198
 
@@ -3696,7 +3696,14 @@ function switchView(view){
3696
3696
  else if(view==='skills') loadSkills();
3697
3697
  else if(view==='analytics') loadMetrics();
3698
3698
  else if(view==='logs') loadLogs();
3699
- else if(view==='settings'){loadConfig();loadModelHealth();}
3699
+ else if(view==='settings'){loadConfig().then(function(){
3700
+ var notDismissed=localStorage.getItem('memos-team-guide-dismissed')!=='1';
3701
+ var sharingOn=document.getElementById('cfgSharingEnabled');
3702
+ var sharingNotEnabled=!sharingOn||!sharingOn.checked;
3703
+ if(notDismissed&&sharingNotEnabled){
3704
+ switchSettingsTab('hub',document.querySelector('.settings-tab-btn[data-tab="hub"]'));
3705
+ }
3706
+ });loadModelHealth();}
3700
3707
  else if(view==='import'){if(!window._migrateRunning) migrateScan(false);}
3701
3708
  else if(view==='admin'){loadAdminData();}
3702
3709
  }
@@ -3736,6 +3743,13 @@ function onTaskScopeChange(){
3736
3743
 
3737
3744
  var _clientPendingPollTimer=null;
3738
3745
  var _lastSharingConnStatus='';
3746
+ function _updateScopeSelectorsVisibility(hubAvailable){
3747
+ var ids=['memorySearchScope','taskSearchScope','skillSearchScope'];
3748
+ for(var i=0;i<ids.length;i++){
3749
+ var el=document.getElementById(ids[i]);
3750
+ if(el) el.style.display=hubAvailable?'':'none';
3751
+ }
3752
+ }
3739
3753
  async function loadSharingStatus(forcePending){
3740
3754
  try{
3741
3755
  const r=await fetch('/api/sharing/status');
@@ -3748,15 +3762,19 @@ async function loadSharingStatus(forcePending){
3748
3762
  if(!d||!d.enabled){
3749
3763
  if(_clientPendingPollTimer){clearInterval(_clientPendingPollTimer);_clientPendingPollTimer=null;}
3750
3764
  _lastSharingConnStatus='';
3765
+ _updateScopeSelectorsVisibility(false);
3751
3766
  return;
3752
3767
  }
3753
3768
  var conn=d.connection||{};
3754
3769
  var curStatus=conn.rejected?'rejected':conn.pendingApproval?'pending':conn.connected?'connected':'none';
3770
+ var hubActive=d.role==='hub'||curStatus==='connected';
3771
+ _updateScopeSelectorsVisibility(hubActive);
3755
3772
  if(_lastSharingConnStatus==='pending'&&curStatus==='rejected'){
3756
3773
  toast(t('sharing.rejected.toast'),'error');
3757
3774
  }
3758
3775
  if(_lastSharingConnStatus==='pending'&&curStatus==='connected'){
3759
3776
  toast(t('sharing.approved.toast'),'success');
3777
+ loadMemories();loadTasks();loadSkills();
3760
3778
  }
3761
3779
  _lastSharingConnStatus=curStatus;
3762
3780
  if(curStatus==='pending'&&!_clientPendingPollTimer){
@@ -3770,6 +3788,7 @@ async function loadSharingStatus(forcePending){
3770
3788
  renderSharingSidebar(null);
3771
3789
  renderSharingSettings(null);
3772
3790
  updateTeamGuide(null);
3791
+ _updateScopeSelectorsVisibility(false);
3773
3792
  }
3774
3793
  }
3775
3794
 
@@ -1232,7 +1232,7 @@ export class ViewerServer {
1232
1232
  body: JSON.stringify({ memory: { sourceChunkId: refreshedChunk.id, role: refreshedChunk.role, content: refreshedChunk.content, summary: refreshedChunk.summary, kind: refreshedChunk.kind, groupId: null, visibility: "public" } }),
1233
1233
  });
1234
1234
  if (!isLocalShared) this.store.markMemorySharedLocally(chunkId);
1235
- if (hubClient.userId) {
1235
+ if (hubClient.userId && this.ctx?.config?.sharing?.role === "hub") {
1236
1236
  const existing = this.store.getHubMemoryBySource(hubClient.userId, chunkId);
1237
1237
  this.store.upsertHubMemory({
1238
1238
  id: (response as any)?.memoryId ?? existing?.id ?? crypto.randomUUID(),
@@ -2080,14 +2080,13 @@ export class ViewerServer {
2080
2080
  },
2081
2081
  }),
2082
2082
  });
2083
- const hubUserId = hubClient.userId;
2084
- if (hubUserId) {
2083
+ if (hubClient.userId && this.ctx?.config?.sharing?.role === "hub") {
2085
2084
  const now = Date.now();
2086
- const existing = this.store.getHubMemoryBySource(hubUserId, chunk.id);
2085
+ const existing = this.store.getHubMemoryBySource(hubClient.userId, chunk.id);
2087
2086
  this.store.upsertHubMemory({
2088
2087
  id: (response as any)?.memoryId ?? existing?.id ?? crypto.randomUUID(),
2089
2088
  sourceChunkId: chunk.id,
2090
- sourceUserId: hubUserId,
2089
+ sourceUserId: hubClient.userId,
2091
2090
  role: chunk.role,
2092
2091
  content: chunk.content,
2093
2092
  summary: chunk.summary ?? "",