@yeaft/webchat-agent 1.0.344 → 1.0.346

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.
@@ -235,6 +235,7 @@ db.exec(`
235
235
  user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
236
236
  catalog_key TEXT NOT NULL,
237
237
  pinned INTEGER NOT NULL DEFAULT 0,
238
+ is_hidden INTEGER NOT NULL DEFAULT 0,
238
239
  sort_rank INTEGER,
239
240
  updated_at INTEGER NOT NULL,
240
241
  PRIMARY KEY (user_id, catalog_key)
@@ -335,10 +336,18 @@ for (const migration of yeaftMigrations) {
335
336
  const yeaftProjectMigrations = [
336
337
  `ALTER TABLE yeaft_projects ADD COLUMN instruction TEXT NOT NULL DEFAULT ''`,
337
338
  ];
339
+
340
+ const sessionUiMetadataMigrations = [
341
+ `ALTER TABLE session_ui_metadata ADD COLUMN is_hidden INTEGER NOT NULL DEFAULT 0`,
342
+ ];
338
343
  for (const migration of yeaftProjectMigrations) {
339
344
  try { db.exec(migration); } catch (_) { /* column exists */ }
340
345
  }
341
346
 
347
+ for (const migration of sessionUiMetadataMigrations) {
348
+ try { db.exec(migration); } catch (_) { /* column exists */ }
349
+ }
350
+
342
351
  for (const migration of migrations) {
343
352
  try {
344
353
  db.exec(migration);
@@ -663,10 +672,11 @@ export const stmts = {
663
672
  `),
664
673
 
665
674
  upsertSessionUiMetadata: db.prepare(`
666
- INSERT INTO session_ui_metadata (user_id, catalog_key, pinned, sort_rank, updated_at)
667
- VALUES (?, ?, ?, ?, ?)
675
+ INSERT INTO session_ui_metadata (user_id, catalog_key, pinned, is_hidden, sort_rank, updated_at)
676
+ VALUES (?, ?, ?, ?, ?, ?)
668
677
  ON CONFLICT(user_id, catalog_key) DO UPDATE SET
669
678
  pinned = excluded.pinned,
679
+ is_hidden = excluded.is_hidden,
670
680
  sort_rank = excluded.sort_rank,
671
681
  updated_at = excluded.updated_at
672
682
  `),
@@ -1,36 +1,56 @@
1
1
  import { stmts, transaction } from './connection.js';
2
+ import { chatCatalogKey, yeaftCatalogKey } from '../session-catalog.js';
3
+
4
+ function normalizeMetadataUpdate(update) {
5
+ if (!update?.catalogKey) throw new Error('Catalog metadata update requires catalogKey');
6
+ if (!['yeaft', 'claude-code', 'copilot'].includes(update.runtimeProvider)) {
7
+ throw new Error('Unknown Session runtime provider during metadata update');
8
+ }
9
+ return {
10
+ ...update,
11
+ pinned: typeof update.pinned === 'boolean' ? update.pinned : null,
12
+ hidden: typeof update.hidden === 'boolean' ? update.hidden : null,
13
+ hasSortRank: Object.prototype.hasOwnProperty.call(update, 'sortRank'),
14
+ sortRank: Number.isFinite(update.sortRank) ? update.sortRank : null,
15
+ };
16
+ }
2
17
 
3
18
  export function applySessionUiMetadataUpdates(userId, updates, now = Date.now()) {
4
19
  if (!userId || !Array.isArray(updates) || updates.length === 0) return false;
5
- for (const update of updates) {
6
- if (!update?.catalogKey) throw new Error('Catalog metadata update requires catalogKey');
20
+ for (const rawUpdate of updates) {
21
+ const update = normalizeMetadataUpdate(rawUpdate);
22
+ const existing = stmts.getSessionUiMetadata.get(userId, update.catalogKey);
23
+ const pinned = update.pinned ?? (existing?.pinned === 1);
24
+ const hidden = update.hidden ?? (existing?.is_hidden === 1);
25
+ const sortRank = update.hasSortRank
26
+ ? update.sortRank
27
+ : (Number.isFinite(existing?.sort_rank) ? existing.sort_rank : null);
7
28
  stmts.upsertSessionUiMetadata.run(
8
29
  userId,
9
30
  update.catalogKey,
10
- update.pinned === true ? 1 : 0,
11
- Number.isFinite(update.sortRank) ? update.sortRank : null,
31
+ pinned ? 1 : 0,
32
+ hidden ? 1 : 0,
33
+ sortRank,
12
34
  now,
13
35
  );
14
36
  if (update.runtimeProvider === 'yeaft') {
15
37
  const result = stmts.setYeaftSessionPinnedForAgent.run(
16
- update.pinned === true ? 1 : 0,
38
+ pinned ? 1 : 0,
17
39
  now,
18
40
  update.sessionId,
19
41
  userId,
20
42
  update.agentId,
21
43
  );
22
44
  if (result.changes !== 1) throw new Error('Yeaft Session identity changed during metadata update');
23
- } else if (update.runtimeProvider === 'claude-code' || update.runtimeProvider === 'copilot') {
45
+ } else {
24
46
  const result = stmts.updateSessionPinnedForRoute.run(
25
- update.pinned === true ? 1 : 0,
47
+ pinned ? 1 : 0,
26
48
  now,
27
49
  update.sessionId,
28
50
  update.agentId,
29
51
  userId,
30
52
  );
31
53
  if (result.changes !== 1) throw new Error('Chat Session identity changed during metadata update');
32
- } else {
33
- throw new Error('Unknown Session runtime provider during metadata update');
34
54
  }
35
55
  }
36
56
  return true;
@@ -42,24 +62,13 @@ function mapRow(row) {
42
62
  userId: row.user_id,
43
63
  catalogKey: row.catalog_key,
44
64
  pinned: row.pinned === 1,
65
+ hidden: row.is_hidden === 1,
45
66
  sortRank: Number.isFinite(row.sort_rank) ? row.sort_rank : null,
46
67
  updatedAt: row.updated_at,
47
68
  };
48
69
  }
49
70
 
50
71
  export const sessionUiMetadataDb = {
51
- upsert(userId, catalogKey, { pinned = false, sortRank = null } = {}) {
52
- if (!userId || !catalogKey) return false;
53
- stmts.upsertSessionUiMetadata.run(
54
- userId,
55
- catalogKey,
56
- pinned ? 1 : 0,
57
- Number.isFinite(sortRank) ? sortRank : null,
58
- Date.now(),
59
- );
60
- return true;
61
- },
62
-
63
72
  get(userId, catalogKey) {
64
73
  if (!userId || !catalogKey) return null;
65
74
  return mapRow(stmts.getSessionUiMetadata.get(userId, catalogKey));
@@ -79,4 +88,12 @@ export const sessionUiMetadataDb = {
79
88
  if (!userId || !catalogKey) return false;
80
89
  return stmts.deleteSessionUiMetadata.run(userId, catalogKey).changes > 0;
81
90
  },
91
+
92
+ deleteForRoute(userId, { runtimeProvider, agentId, sessionId } = {}) {
93
+ if (!userId || !agentId || !sessionId) return false;
94
+ const catalogKey = runtimeProvider === 'yeaft'
95
+ ? yeaftCatalogKey(agentId, sessionId)
96
+ : chatCatalogKey(sessionId);
97
+ return this.delete(userId, catalogKey);
98
+ },
82
99
  };
@@ -1,4 +1,4 @@
1
- import { sessionDb, messageDb } from '../database.js';
1
+ import { sessionDb, messageDb, sessionUiMetadataDb } from '../database.js';
2
2
  import {
3
3
  broadcastAgentList, notifyConversationUpdate, forwardToClients
4
4
  } from '../ws-utils.js';
@@ -313,16 +313,36 @@ export async function handleAgentConversation(agentId, agent, msg) {
313
313
  }
314
314
  break;
315
315
 
316
- case 'conversation_deleted':
316
+ case 'conversation_deleted': {
317
+ // The event came from an authenticated Agent, but the conversation id
318
+ // alone is not an ownership proof: a stale Agent can still hold an id
319
+ // after the Session was moved. Always preserve the established delete
320
+ // lifecycle (deactivate it), but only clear sidebar metadata when the
321
+ // exact persisted owner-scoped Agent/user route matches.
322
+ const persistedSession = sessionDb.get(msg.conversationId);
323
+ const metadataOwnerId = persistedSession?.user_id || null;
324
+ const canClearMetadata = !!persistedSession
325
+ && persistedSession.agent_id === agentId
326
+ && !!agent.ownerId
327
+ && metadataOwnerId === agent.ownerId;
328
+
317
329
  agent.conversations.delete(msg.conversationId);
318
330
  try {
319
331
  sessionDb.setActive(msg.conversationId, false);
332
+ if (canClearMetadata) {
333
+ sessionUiMetadataDb.deleteForRoute(metadataOwnerId, {
334
+ runtimeProvider: persistedSession.provider || 'claude-code',
335
+ agentId,
336
+ sessionId: msg.conversationId,
337
+ });
338
+ }
320
339
  } catch (e) {
321
- console.error('Failed to update session in database:', e.message);
340
+ console.error('Failed to update deleted conversation metadata:', e.message);
322
341
  }
323
342
  await notifyConversationUpdate(agentId, msg);
324
343
  await broadcastAgentList();
325
344
  break;
345
+ }
326
346
 
327
347
  case 'history_sessions_list':
328
348
  case 'folders_list':
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from 'crypto';
2
- import { messageDb, yeaftProjectDb, yeaftSessionDb } from '../database.js';
2
+ import { messageDb, sessionUiMetadataDb, yeaftProjectDb, yeaftSessionDb } from '../database.js';
3
3
  import { transaction } from '../db/connection.js';
4
4
  import { broadcastAgentList, broadcastSessionCatalog, forwardToClients, sendToAgent, sendToWebClient } from '../ws-utils.js';
5
5
  import { webClients, previewFiles } from '../context.js';
@@ -95,6 +95,11 @@ function syncYeaftSessionMetadata(agentId, agent, event) {
95
95
  try {
96
96
  yeaftSessionDb.deleteForAgent(ownerId, agentId, sessionId);
97
97
  yeaftProjectDb.removeSession(ownerId, agentId, sessionId);
98
+ sessionUiMetadataDb.deleteForRoute(ownerId, {
99
+ runtimeProvider: 'yeaft',
100
+ agentId,
101
+ sessionId,
102
+ });
98
103
  } catch (e) {
99
104
  console.warn('[Server] Yeaft Session metadata cleanup failed:', e?.message || e);
100
105
  }
@@ -11,7 +11,7 @@ import {
11
11
  import { agents, pendingFiles, trackUserTurn, webClients } from '../context.js';
12
12
  import {
13
13
  sendToWebClient, forwardToAgent,
14
- broadcastAgentList, broadcastSessionCatalog, buildSessionCatalog,
14
+ broadcastAgentList, broadcastSessionCatalog, buildSessionCatalog, buildHiddenSessionCatalog,
15
15
  verifyConversationOwnership, verifyAgentOwnership
16
16
  } from '../ws-utils.js';
17
17
  import { routeSessionPin } from './session-pin-router.js';
@@ -100,16 +100,38 @@ function persistSessionPin(userId, routeRef, pinned) {
100
100
  }]);
101
101
  }
102
102
 
103
+ function catalogMetadataUpdates(rows) {
104
+ const seen = new Set();
105
+ const updates = [];
106
+ for (const row of rows) {
107
+ const routeRef = row?.routeRef;
108
+ if (!row?.catalogKey || seen.has(row.catalogKey)
109
+ || !routeRef?.runtimeProvider || !routeRef?.agentId || !routeRef?.sessionId) return null;
110
+ seen.add(row.catalogKey);
111
+ updates.push({
112
+ catalogKey: row.catalogKey,
113
+ runtimeProvider: routeRef.runtimeProvider,
114
+ agentId: routeRef.agentId,
115
+ sessionId: routeRef.sessionId,
116
+ pinned: row.pinned === true,
117
+ hidden: row.hidden === true,
118
+ sortRank: updates.length,
119
+ });
120
+ }
121
+ return updates;
122
+ }
123
+
103
124
  function catalogOrderUpdates(client, items) {
104
125
  if (!client?.userId || !Array.isArray(items) || items.length === 0) return null;
105
126
  const canonical = buildSessionCatalog(client.userId, client.role);
127
+ const hidden = buildHiddenSessionCatalog(client.userId, client.role);
106
128
  if (canonical.length !== items.length) return null;
107
129
  const canonicalByKey = new Map(canonical.map(row => [row.catalogKey, row]));
108
130
  if (canonicalByKey.size !== canonical.length) return null;
109
131
 
110
132
  const seen = new Set();
111
- const updates = [];
112
- for (const [sortRank, item] of items.entries()) {
133
+ const orderedVisible = [];
134
+ for (const item of items) {
113
135
  const row = canonicalByKey.get(item?.catalogKey);
114
136
  const routeRef = item?.routeRef;
115
137
  if (!row || seen.has(item.catalogKey)
@@ -117,16 +139,44 @@ function catalogOrderUpdates(client, items) {
117
139
  || routeRef?.agentId !== row.routeRef?.agentId
118
140
  || routeRef?.sessionId !== row.routeRef?.sessionId) return null;
119
141
  seen.add(item.catalogKey);
120
- updates.push({
121
- catalogKey: row.catalogKey,
122
- runtimeProvider: row.routeRef.runtimeProvider,
123
- agentId: row.routeRef.agentId,
124
- sessionId: row.routeRef.sessionId,
125
- pinned: row.pinned === true,
126
- sortRank,
127
- });
142
+ orderedVisible.push(row);
128
143
  }
129
- return seen.size === canonical.length ? updates : null;
144
+ if (seen.size !== canonical.length) return null;
145
+
146
+ // Hidden rows are deliberately absent from a drag payload, but their stale
147
+ // ranks must not collide with the newly ordered visible catalog when they
148
+ // are restored. Normalize both sets in the same transaction, preserving the
149
+ // visible order chosen by the client and the current hidden-row order.
150
+ return catalogMetadataUpdates([...orderedVisible, ...hidden]);
151
+ }
152
+
153
+ function catalogVisibilityUpdates(client, row, hidden) {
154
+ if (!client?.userId || !row?.catalogKey || !row?.routeRef) return null;
155
+ const visible = buildSessionCatalog(client.userId, client.role);
156
+ const hiddenRows = buildHiddenSessionCatalog(client.userId, client.role);
157
+ const visibleByKey = new Map(visible.map(item => [item.catalogKey, item]));
158
+ const hiddenByKey = new Map(hiddenRows.map(item => [item.catalogKey, item]));
159
+ if (visibleByKey.size !== visible.length || hiddenByKey.size !== hiddenRows.length) return null;
160
+
161
+ if (hidden) {
162
+ const visibleRow = visibleByKey.get(row.catalogKey);
163
+ if (!visibleRow || hiddenByKey.has(row.catalogKey)) return null;
164
+ return catalogMetadataUpdates([
165
+ ...visible.filter(item => item.catalogKey !== row.catalogKey),
166
+ { ...visibleRow, hidden: true },
167
+ ...hiddenRows,
168
+ ]);
169
+ }
170
+
171
+ const hiddenRow = hiddenByKey.get(row.catalogKey);
172
+ if (!hiddenRow || visibleByKey.has(row.catalogKey)) return null;
173
+
174
+ // The UI adds a restored Session after the visible list. Keep that policy on
175
+ // the server and also normalize any still-hidden rows, so no stale rank can
176
+ // collide with this restored row during a later refresh or restore.
177
+ const restored = { ...hiddenRow, hidden: false };
178
+ const remainingHidden = hiddenRows.filter(item => item.catalogKey !== restored.catalogKey);
179
+ return catalogMetadataUpdates([...visible, restored, ...remainingHidden]);
130
180
  }
131
181
 
132
182
  export function groupOnlineYeaftSessions(rows, agentRegistry = agents) {
@@ -472,6 +522,11 @@ export async function handleClientConversation(clientId, client, msg, checkAgent
472
522
 
473
523
  try {
474
524
  sessionDb.setActive(msg.conversationId, false);
525
+ sessionUiMetadataDb.deleteForRoute(client.userId, {
526
+ runtimeProvider: persisted?.provider || 'claude-code',
527
+ agentId: deleteAgentId,
528
+ sessionId: msg.conversationId,
529
+ });
475
530
  const deleteAgent = agents.get(deleteAgentId);
476
531
  deleteAgent?.conversations.delete(msg.conversationId);
477
532
  await broadcastAgentList();
@@ -574,31 +629,67 @@ export async function handleClientConversation(clientId, client, msg, checkAgent
574
629
  if (!client.userId || !msg.catalogKey || !msg.routeRef?.runtimeProvider) break;
575
630
  const { runtimeProvider, agentId, sessionId } = msg.routeRef;
576
631
  let expectedCatalogKey = null;
632
+ let sessionRow = null;
577
633
  if (runtimeProvider === 'yeaft') {
578
- if (agentId && sessionId && yeaftSessionDb.getForAgent(client.userId, agentId, sessionId)) {
579
- expectedCatalogKey = yeaftCatalogKey(agentId, sessionId);
580
- }
634
+ sessionRow = agentId && sessionId
635
+ ? yeaftSessionDb.getForAgent(client.userId, agentId, sessionId)
636
+ : null;
637
+ if (sessionRow) expectedCatalogKey = yeaftCatalogKey(agentId, sessionId);
581
638
  } else if (runtimeProvider === 'claude-code' || runtimeProvider === 'copilot') {
582
- const row = sessionId ? sessionDb.get(sessionId) : null;
639
+ sessionRow = sessionId ? sessionDb.get(sessionId) : null;
583
640
  if (agentId && sessionId
584
641
  && (CONFIG.skipAuth || verifyConversationOwnership(sessionId, client.userId, client.role))
585
- && row?.agent_id === agentId
586
- && (row.provider || 'claude-code') === runtimeProvider) {
642
+ && sessionRow?.agent_id === agentId
643
+ && (sessionRow.provider || 'claude-code') === runtimeProvider) {
587
644
  expectedCatalogKey = chatCatalogKey(sessionId);
588
645
  }
589
646
  }
590
647
  const authorized = expectedCatalogKey === msg.catalogKey;
648
+ let currentMetadata = null;
649
+ if (authorized) {
650
+ try {
651
+ currentMetadata = sessionUiMetadataDb.get(client.userId, expectedCatalogKey);
652
+ } catch (e) {
653
+ console.warn('[Server] Session metadata read failed:', e?.message || e);
654
+ }
655
+ }
656
+ const persistedPinned = runtimeProvider === 'yeaft'
657
+ ? (sessionRow?.pinned === true || sessionRow?.isPinned === true)
658
+ : sessionRow?.is_pinned === 1;
659
+ const nextPinned = typeof msg.pinned === 'boolean'
660
+ ? msg.pinned
661
+ : (currentMetadata?.pinned ?? !!persistedPinned);
662
+ const hasHidden = typeof msg.hidden === 'boolean';
663
+ const nextHidden = hasHidden
664
+ ? msg.hidden
665
+ : currentMetadata?.hidden === true;
666
+ const nextSortRank = Number.isFinite(msg.sortRank)
667
+ ? msg.sortRank
668
+ : (currentMetadata?.sortRank ?? null);
669
+ const hasSortRank = Object.prototype.hasOwnProperty.call(msg, 'sortRank');
591
670
  let persisted = false;
592
671
  if (authorized) {
593
672
  try {
594
- persisted = sessionUiMetadataDb.applyBatch(client.userId, [{
595
- catalogKey: expectedCatalogKey,
596
- runtimeProvider,
597
- agentId,
598
- sessionId,
599
- pinned: msg.pinned === true,
600
- sortRank: Number.isFinite(msg.sortRank) ? msg.sortRank : null,
601
- }]);
673
+ // Only an explicit hidden mutation changes catalog membership. A
674
+ // pinned-only first update has no metadata row yet, so comparing an
675
+ // implicit undefined state to false would incorrectly treat it as a
676
+ // restore and reject the valid visible route.
677
+ const visibilityChanged = hasHidden && (currentMetadata?.hidden === true) !== nextHidden;
678
+ const updates = visibilityChanged
679
+ ? catalogVisibilityUpdates(client, {
680
+ catalogKey: expectedCatalogKey,
681
+ routeRef: { runtimeProvider, agentId, sessionId },
682
+ }, nextHidden)
683
+ : [{
684
+ catalogKey: expectedCatalogKey,
685
+ runtimeProvider,
686
+ agentId,
687
+ sessionId,
688
+ pinned: nextPinned,
689
+ hidden: nextHidden,
690
+ ...(hasSortRank ? { sortRank: nextSortRank } : {}),
691
+ }];
692
+ persisted = updates ? sessionUiMetadataDb.applyBatch(client.userId, updates) : false;
602
693
  if (persisted) await broadcastSessionCatalog(client.userId);
603
694
  } catch (e) {
604
695
  console.warn('[Server] Session metadata update failed:', e?.message || e);
@@ -611,8 +702,9 @@ export async function handleClientConversation(clientId, client, msg, checkAgent
611
702
  catalogKey: msg.catalogKey,
612
703
  routeRef: msg.routeRef,
613
704
  ...(persisted ? {
614
- pinned: msg.pinned === true,
615
- sortRank: Number.isFinite(msg.sortRank) ? msg.sortRank : null,
705
+ pinned: nextPinned,
706
+ hidden: nextHidden,
707
+ sortRank: nextSortRank,
616
708
  } : { error: 'Permission denied or stale Session route' }),
617
709
  });
618
710
  break;
@@ -35,11 +35,24 @@ function hasCompleteSortRanks(rows) {
35
35
  return ranks.every(Number.isFinite) && new Set(ranks).size === rows.length;
36
36
  }
37
37
 
38
+ function catalogSort(left, right, ranked = false) {
39
+ if (left.pinned !== right.pinned) return left.pinned ? -1 : 1;
40
+ if (ranked) {
41
+ const leftRank = Number.isFinite(left.sortRank) ? left.sortRank : Number.MAX_SAFE_INTEGER;
42
+ const rightRank = Number.isFinite(right.sortRank) ? right.sortRank : Number.MAX_SAFE_INTEGER;
43
+ if (leftRank !== rightRank) return leftRank - rightRank;
44
+ }
45
+ const metadataDelta = timestampValue(right.metadataUpdatedAt) - timestampValue(left.metadataUpdatedAt);
46
+ if (metadataDelta !== 0) return metadataDelta;
47
+ return left.catalogKey.localeCompare(right.catalogKey);
48
+ }
49
+
38
50
  export function projectSessionCatalog({
39
51
  chatSessions = [],
40
52
  yeaftSessions = [],
41
53
  metadata = [],
42
54
  onlineAgentIds = new Set(),
55
+ includeHidden = false,
43
56
  } = {}) {
44
57
  const metadataByKey = new Map(metadata.map(row => [row.catalogKey, row]));
45
58
  const onlineAgents = onlineAgentIds instanceof Set ? onlineAgentIds : new Set(onlineAgentIds);
@@ -50,6 +63,7 @@ export function projectSessionCatalog({
50
63
  const catalogKey = chatCatalogKey(session.id);
51
64
  const runtimeProvider = normalizeChatRuntimeProvider(session.provider);
52
65
  const meta = metadataByKey.get(catalogKey) || {};
66
+ if (!includeHidden && meta.hidden === true) continue;
53
67
  rows.push({
54
68
  catalogKey,
55
69
  runtimeProvider,
@@ -60,6 +74,7 @@ export function projectSessionCatalog({
60
74
  agentName: session.agent_name || '',
61
75
  availability: onlineAgents.has(session.agent_id) ? 'online' : 'offline',
62
76
  pinned: meta.pinned ?? session.is_pinned === 1,
77
+ hidden: meta.hidden === true,
63
78
  sortRank: meta.sortRank ?? null,
64
79
  createdAt: session.created_at || null,
65
80
  metadataUpdatedAt: session.metadata_updated_at ?? session.created_at ?? null,
@@ -69,6 +84,7 @@ export function projectSessionCatalog({
69
84
  for (const session of yeaftSessions) {
70
85
  const catalogKey = yeaftCatalogKey(session.agentId, session.id);
71
86
  const meta = metadataByKey.get(catalogKey) || {};
87
+ if (!includeHidden && meta.hidden === true) continue;
72
88
  rows.push({
73
89
  catalogKey,
74
90
  runtimeProvider: 'yeaft',
@@ -79,6 +95,7 @@ export function projectSessionCatalog({
79
95
  agentName: session.agentName || '',
80
96
  availability: onlineAgents.has(session.agentId) ? 'online' : 'offline',
81
97
  pinned: meta.pinned ?? !!session.pinned,
98
+ hidden: meta.hidden === true,
82
99
  sortRank: meta.sortRank ?? null,
83
100
  createdAt: session.createdAt || null,
84
101
  metadataUpdatedAt: session.metadataUpdatedAt ?? session.createdAt ?? null,
@@ -86,15 +103,5 @@ export function projectSessionCatalog({
86
103
  }
87
104
 
88
105
  const ranked = hasCompleteSortRanks(rows);
89
- return rows.sort((left, right) => {
90
- if (left.pinned !== right.pinned) return left.pinned ? -1 : 1;
91
- if (ranked) {
92
- const leftRank = Number.isFinite(left.sortRank) ? left.sortRank : Number.MAX_SAFE_INTEGER;
93
- const rightRank = Number.isFinite(right.sortRank) ? right.sortRank : Number.MAX_SAFE_INTEGER;
94
- if (leftRank !== rightRank) return leftRank - rightRank;
95
- }
96
- const metadataDelta = timestampValue(right.metadataUpdatedAt) - timestampValue(left.metadataUpdatedAt);
97
- if (metadataDelta !== 0) return metadataDelta;
98
- return left.catalogKey.localeCompare(right.catalogKey);
99
- });
106
+ return rows.sort((left, right) => catalogSort(left, right, ranked));
100
107
  }
@@ -100,7 +100,7 @@ export function verifyPersistedAgentOwnership(agentId, userId, role = null) {
100
100
  || yeaftSessionDb.getByUser(userId).some(row => row.agentId === agentId);
101
101
  }
102
102
 
103
- export function buildSessionCatalog(userId, role = null) {
103
+ function catalogProjectionInputs(userId, role = null) {
104
104
  const chatSessions = sessionDb.getActiveByUser(userId).filter((session) => {
105
105
  if (CONFIG.skipAuth || session.user_id === userId) return true;
106
106
  if (session.user_id != null) return false;
@@ -108,12 +108,25 @@ export function buildSessionCatalog(userId, role = null) {
108
108
  // offline. This same predicate protects catalog mutations.
109
109
  return verifyPersistedAgentOwnership(session.agent_id, userId, role);
110
110
  });
111
- return projectSessionCatalog({
111
+ const yeaftSessions = yeaftSessionDb.getByUser(userId);
112
+ const metadata = sessionUiMetadataDb.getByUser(userId);
113
+ return {
112
114
  chatSessions,
113
- yeaftSessions: yeaftSessionDb.getByUser(userId),
114
- metadata: sessionUiMetadataDb.getByUser(userId),
115
+ yeaftSessions,
116
+ metadata,
115
117
  onlineAgentIds: onlineAgentIdsForUser(userId, role),
116
- });
118
+ };
119
+ }
120
+
121
+ export function buildSessionCatalog(userId, role = null) {
122
+ return projectSessionCatalog(catalogProjectionInputs(userId, role));
123
+ }
124
+
125
+ export function buildHiddenSessionCatalog(userId, role = null) {
126
+ return projectSessionCatalog({
127
+ ...catalogProjectionInputs(userId, role),
128
+ includeHidden: true,
129
+ }).filter(row => row.hidden === true);
117
130
  }
118
131
 
119
132
  // Broadcast the owner-scoped read model after canonical Session state changes.
@@ -126,6 +139,7 @@ export async function broadcastSessionCatalog(userId) {
126
139
  await sendToWebClient(client, {
127
140
  type: 'session_catalog_snapshot',
128
141
  catalog: buildSessionCatalog(userId, client.role),
142
+ hiddenCatalog: buildHiddenSessionCatalog(userId, client.role),
129
143
  projects: yeaftProjectDb.list(userId),
130
144
  projectsAuthoritative: true,
131
145
  });
@@ -189,6 +203,7 @@ export async function broadcastAgentList() {
189
203
  await sendToWebClient(client, {
190
204
  type: 'session_catalog_snapshot',
191
205
  catalog: buildSessionCatalog(client.userId, client.role),
206
+ hiddenCatalog: buildHiddenSessionCatalog(client.userId, client.role),
192
207
  projects: yeaftProjectDb.list(client.userId),
193
208
  projectsAuthoritative: true,
194
209
  });
@@ -1 +1 @@
1
- {"version":"1.0.344"}
1
+ {"version":"1.0.346"}