@yeaft/webchat-agent 1.0.298 → 1.0.300

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.
@@ -12,12 +12,14 @@ import { generateVerificationCode, maskEmail } from './utils.js';
12
12
  * Helper: complete login and return token + sessionKey + role
13
13
  */
14
14
  export function completeLogin(username, sessionKey, role) {
15
+ const user = getUserByUsername(username);
15
16
  const token = issueSessionToken(username);
16
17
  activeSessions.set(token, { username, sessionKey });
17
18
  return {
18
19
  success: true,
19
20
  token,
20
21
  sessionKey: encodeKey(sessionKey),
22
+ userId: user?.id || null,
21
23
  role: role === 'admin' ? 'admin' : 'pro',
22
24
  needTotpCode: false,
23
25
  needTotpSetup: false,
@@ -4,6 +4,7 @@ export { userDb } from './db/user-db.js';
4
4
  export { invitationDb } from './db/invitation-db.js';
5
5
  export { sessionDb } from './db/session-db.js';
6
6
  export { yeaftSessionDb } from './db/yeaft-session-db.js';
7
+ export { yeaftProjectDb, YeaftProjectDbError } from './db/yeaft-project-db.js';
7
8
  export { sessionUiMetadataDb } from './db/session-ui-metadata-db.js';
8
9
  export { messageDb } from './db/message-db.js';
9
10
  export { userStatsDb } from './db/user-stats-db.js';
@@ -241,6 +241,37 @@ db.exec(`
241
241
  );
242
242
  CREATE INDEX IF NOT EXISTS idx_session_ui_metadata_user_sort
243
243
  ON session_ui_metadata(user_id, pinned DESC, sort_rank ASC);
244
+
245
+ -- Projects are user-owned organization metadata. Membership keeps the full
246
+ -- Agent + Session identity because Session ids are only unique per Agent.
247
+ CREATE TABLE IF NOT EXISTS yeaft_projects (
248
+ id TEXT NOT NULL,
249
+ user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
250
+ name TEXT NOT NULL,
251
+ sort_order INTEGER NOT NULL,
252
+ created_at INTEGER NOT NULL,
253
+ updated_at INTEGER NOT NULL,
254
+ PRIMARY KEY (user_id, id)
255
+ );
256
+ CREATE TABLE IF NOT EXISTS yeaft_project_sessions (
257
+ user_id TEXT NOT NULL,
258
+ project_id TEXT NOT NULL,
259
+ agent_id TEXT NOT NULL,
260
+ session_id TEXT NOT NULL,
261
+ created_at INTEGER NOT NULL,
262
+ PRIMARY KEY (user_id, agent_id, session_id),
263
+ FOREIGN KEY (user_id, project_id) REFERENCES yeaft_projects(user_id, id) ON DELETE CASCADE
264
+ );
265
+ CREATE INDEX IF NOT EXISTS idx_yeaft_projects_user_sort
266
+ ON yeaft_projects(user_id, sort_order ASC, created_at ASC);
267
+ CREATE INDEX IF NOT EXISTS idx_yeaft_project_sessions_project
268
+ ON yeaft_project_sessions(user_id, project_id, agent_id);
269
+ CREATE TABLE IF NOT EXISTS yeaft_project_imports (
270
+ user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
271
+ agent_id TEXT NOT NULL,
272
+ imported_at INTEGER NOT NULL,
273
+ PRIMARY KEY (user_id, agent_id)
274
+ );
244
275
  `);
245
276
 
246
277
  try {
@@ -645,6 +676,61 @@ export const stmts = {
645
676
  DELETE FROM session_ui_metadata WHERE user_id = ? AND catalog_key = ?
646
677
  `),
647
678
 
679
+ // Project organization metadata. Projects are server-owned and may contain
680
+ // Sessions from multiple Agents; sharing still filters by agent_id.
681
+ insertYeaftProject: db.prepare(`
682
+ INSERT INTO yeaft_projects (id, user_id, name, sort_order, created_at, updated_at)
683
+ VALUES (?, ?, ?, ?, ?, ?)
684
+ `),
685
+ getYeaftProjectsByUser: db.prepare(`
686
+ SELECT * FROM yeaft_projects WHERE user_id = ? ORDER BY sort_order ASC, created_at ASC
687
+ `),
688
+ getYeaftProjectForUser: db.prepare(`
689
+ SELECT * FROM yeaft_projects WHERE user_id = ? AND id = ?
690
+ `),
691
+ updateYeaftProjectName: db.prepare(`
692
+ UPDATE yeaft_projects SET name = ?, updated_at = ? WHERE user_id = ? AND id = ?
693
+ `),
694
+ deleteYeaftProject: db.prepare(`
695
+ DELETE FROM yeaft_projects WHERE user_id = ? AND id = ?
696
+ `),
697
+ getYeaftProjectMembersByUser: db.prepare(`
698
+ SELECT * FROM yeaft_project_sessions
699
+ WHERE user_id = ?
700
+ ORDER BY created_at ASC, agent_id ASC, session_id ASC
701
+ `),
702
+ getYeaftProjectForSession: db.prepare(`
703
+ SELECT p.* FROM yeaft_projects p
704
+ JOIN yeaft_project_sessions m
705
+ ON m.user_id = p.user_id AND m.project_id = p.id
706
+ WHERE m.user_id = ? AND m.agent_id = ? AND m.session_id = ?
707
+ `),
708
+ getYeaftProjectMembersForAgent: db.prepare(`
709
+ SELECT agent_id, session_id FROM yeaft_project_sessions
710
+ WHERE user_id = ? AND project_id = ? AND agent_id = ?
711
+ ORDER BY created_at ASC, session_id ASC
712
+ `),
713
+ deleteYeaftProjectSessionMembership: db.prepare(`
714
+ DELETE FROM yeaft_project_sessions
715
+ WHERE user_id = ? AND agent_id = ? AND session_id = ?
716
+ `),
717
+ insertYeaftProjectSessionMembership: db.prepare(`
718
+ INSERT INTO yeaft_project_sessions
719
+ (user_id, project_id, agent_id, session_id, created_at)
720
+ VALUES (?, ?, ?, ?, ?)
721
+ `),
722
+ deleteYeaftProjectMembershipsForSession: db.prepare(`
723
+ DELETE FROM yeaft_project_sessions
724
+ WHERE user_id = ? AND agent_id = ? AND session_id = ?
725
+ `),
726
+ getYeaftProjectImport: db.prepare(`
727
+ SELECT imported_at FROM yeaft_project_imports WHERE user_id = ? AND agent_id = ?
728
+ `),
729
+ insertYeaftProjectImport: db.prepare(`
730
+ INSERT OR IGNORE INTO yeaft_project_imports (user_id, agent_id, imported_at)
731
+ VALUES (?, ?, ?)
732
+ `),
733
+
648
734
  // Message 操作
649
735
  insertMessage: db.prepare(`
650
736
  INSERT INTO messages (session_id, role, content, message_type, tool_name, tool_input, created_at, metadata)
@@ -0,0 +1,225 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { stmts, transaction } from './connection.js';
3
+
4
+ export class YeaftProjectDbError extends Error {
5
+ constructor(code, message) {
6
+ super(message || code);
7
+ this.name = 'YeaftProjectDbError';
8
+ this.code = code;
9
+ }
10
+ }
11
+
12
+ function requireUserId(value) {
13
+ const userId = typeof value === 'string' ? value.trim() : '';
14
+ if (!userId) throw new YeaftProjectDbError('missing_user', 'User identity is required');
15
+ return userId;
16
+ }
17
+
18
+ function requireName(value) {
19
+ const name = typeof value === 'string' ? value.trim() : '';
20
+ if (!name) throw new YeaftProjectDbError('invalid_name', 'Project name is required');
21
+ return name.slice(0, 120);
22
+ }
23
+
24
+ function requireId(value, code, label) {
25
+ const id = typeof value === 'string' ? value.trim() : '';
26
+ if (!id) throw new YeaftProjectDbError(code, `${label} is required`);
27
+ return id;
28
+ }
29
+
30
+ function mapProject(row, members = []) {
31
+ if (!row) return null;
32
+ return {
33
+ id: row.id,
34
+ name: row.name,
35
+ sortOrder: row.sort_order,
36
+ createdAt: row.created_at,
37
+ updatedAt: row.updated_at,
38
+ members: members.map(member => ({
39
+ agentId: member.agent_id,
40
+ sessionId: member.session_id,
41
+ })),
42
+ };
43
+ }
44
+
45
+ function requireProject(userId, projectId) {
46
+ const row = stmts.getYeaftProjectForUser.get(userId, projectId);
47
+ if (!row) throw new YeaftProjectDbError('not_found', 'Project not found');
48
+ return row;
49
+ }
50
+
51
+ export const yeaftProjectDb = {
52
+ list(userId) {
53
+ const ownerId = requireUserId(userId);
54
+ const membersByProject = new Map();
55
+ for (const member of stmts.getYeaftProjectMembersByUser.all(ownerId)) {
56
+ if (!membersByProject.has(member.project_id)) membersByProject.set(member.project_id, []);
57
+ membersByProject.get(member.project_id).push(member);
58
+ }
59
+ return stmts.getYeaftProjectsByUser.all(ownerId)
60
+ .map(row => mapProject(row, membersByProject.get(row.id) || []));
61
+ },
62
+
63
+ listForAgent(userId, agentId) {
64
+ const targetAgentId = typeof agentId === 'string' ? agentId.trim() : '';
65
+ return this.list(userId).map(project => ({
66
+ ...project,
67
+ sessionIds: targetAgentId
68
+ ? project.members
69
+ .filter(member => member.agentId === targetAgentId)
70
+ .map(member => member.sessionId)
71
+ : [],
72
+ }));
73
+ },
74
+
75
+ create(userId, name) {
76
+ const ownerId = requireUserId(userId);
77
+ const projects = this.list(ownerId);
78
+ const now = Date.now();
79
+ const project = {
80
+ id: `project-${randomUUID().slice(0, 8)}`,
81
+ name: requireName(name),
82
+ sortOrder: projects.reduce((max, row) => Math.max(max, Number(row.sortOrder) || 0), -1) + 1,
83
+ createdAt: now,
84
+ updatedAt: now,
85
+ members: [],
86
+ };
87
+ stmts.insertYeaftProject.run(
88
+ project.id,
89
+ ownerId,
90
+ project.name,
91
+ project.sortOrder,
92
+ now,
93
+ now,
94
+ );
95
+ return project;
96
+ },
97
+
98
+ rename(userId, projectId, name) {
99
+ const ownerId = requireUserId(userId);
100
+ const id = requireId(projectId, 'invalid_project_id', 'Project id');
101
+ requireProject(ownerId, id);
102
+ const nextName = requireName(name);
103
+ stmts.updateYeaftProjectName.run(nextName, Date.now(), ownerId, id);
104
+ return mapProject(stmts.getYeaftProjectForUser.get(ownerId, id),
105
+ stmts.getYeaftProjectMembersByUser.all(ownerId).filter(member => member.project_id === id));
106
+ },
107
+
108
+ delete(userId, projectId) {
109
+ const ownerId = requireUserId(userId);
110
+ const id = requireId(projectId, 'invalid_project_id', 'Project id');
111
+ requireProject(ownerId, id);
112
+ stmts.deleteYeaftProject.run(ownerId, id);
113
+ return { projectId: id };
114
+ },
115
+
116
+ moveSession(userId, { agentId, sessionId, projectId = null } = {}) {
117
+ const ownerId = requireUserId(userId);
118
+ const targetAgentId = requireId(agentId, 'invalid_agent_id', 'Agent id');
119
+ const targetSessionId = requireId(sessionId, 'invalid_session_id', 'Session id');
120
+ const targetProjectId = projectId == null || projectId === ''
121
+ ? null
122
+ : requireId(projectId, 'invalid_project_id', 'Project id');
123
+ if (targetProjectId) requireProject(ownerId, targetProjectId);
124
+
125
+ transaction(() => {
126
+ stmts.deleteYeaftProjectSessionMembership.run(ownerId, targetAgentId, targetSessionId);
127
+ if (targetProjectId) {
128
+ stmts.insertYeaftProjectSessionMembership.run(
129
+ ownerId,
130
+ targetProjectId,
131
+ targetAgentId,
132
+ targetSessionId,
133
+ Date.now(),
134
+ );
135
+ }
136
+ })();
137
+ return { agentId: targetAgentId, sessionId: targetSessionId, projectId: targetProjectId };
138
+ },
139
+
140
+ reconcileAgentSessions(userId, agentId, sessionIds) {
141
+ const ownerId = requireUserId(userId);
142
+ const targetAgentId = requireId(agentId, 'invalid_agent_id', 'Agent id');
143
+ const currentIds = new Set((Array.isArray(sessionIds) ? sessionIds : [])
144
+ .filter(id => typeof id === 'string' && id.trim())
145
+ .map(id => id.trim()));
146
+ let removed = 0;
147
+ for (const project of this.list(ownerId)) {
148
+ for (const member of project.members) {
149
+ if (member.agentId !== targetAgentId || currentIds.has(member.sessionId)) continue;
150
+ removed += stmts.deleteYeaftProjectMembershipsForSession.run(
151
+ ownerId,
152
+ targetAgentId,
153
+ member.sessionId,
154
+ ).changes;
155
+ }
156
+ }
157
+ return removed;
158
+ },
159
+
160
+ removeSession(userId, agentId, sessionId) {
161
+ const ownerId = requireUserId(userId);
162
+ const targetAgentId = requireId(agentId, 'invalid_agent_id', 'Agent id');
163
+ const targetSessionId = requireId(sessionId, 'invalid_session_id', 'Session id');
164
+ return stmts.deleteYeaftProjectMembershipsForSession.run(
165
+ ownerId,
166
+ targetAgentId,
167
+ targetSessionId,
168
+ ).changes > 0;
169
+ },
170
+
171
+ importLegacyProjects(userId, agentId, projects, sessionExists = () => true) {
172
+ const ownerId = requireUserId(userId);
173
+ const targetAgentId = requireId(agentId, 'invalid_agent_id', 'Agent id');
174
+ if (stmts.getYeaftProjectImport.get(ownerId, targetAgentId)) return false;
175
+ if (!Array.isArray(projects)) return false;
176
+ const rows = projects;
177
+ transaction(() => {
178
+ const existingProjects = this.list(ownerId);
179
+ const usedNames = new Set(existingProjects.map(project => project.name));
180
+ let sortOrder = existingProjects.length;
181
+ for (const row of rows) {
182
+ const baseName = typeof row?.name === 'string' ? row.name.trim() : '';
183
+ if (!baseName) continue;
184
+ let name = baseName;
185
+ for (let suffix = 2; usedNames.has(name); suffix += 1) name = `${baseName} ${suffix}`;
186
+ const projectId = `project-${randomUUID().slice(0, 8)}`;
187
+ const now = Date.now();
188
+ stmts.insertYeaftProject.run(projectId, ownerId, name.slice(0, 120), sortOrder, now, now);
189
+ sortOrder += 1;
190
+ usedNames.add(name);
191
+ for (const rawSessionId of Array.isArray(row.sessionIds) ? row.sessionIds : []) {
192
+ const sessionId = typeof rawSessionId === 'string' ? rawSessionId.trim() : '';
193
+ if (!sessionId || !sessionExists(sessionId)) continue;
194
+ stmts.deleteYeaftProjectSessionMembership.run(ownerId, targetAgentId, sessionId);
195
+ stmts.insertYeaftProjectSessionMembership.run(
196
+ ownerId,
197
+ projectId,
198
+ targetAgentId,
199
+ sessionId,
200
+ now,
201
+ );
202
+ }
203
+ }
204
+ stmts.insertYeaftProjectImport.run(ownerId, targetAgentId, Date.now());
205
+ })();
206
+ return true;
207
+ },
208
+
209
+ contextForSession(userId, agentId, sessionId) {
210
+ const ownerId = requireUserId(userId);
211
+ const targetAgentId = requireId(agentId, 'invalid_agent_id', 'Agent id');
212
+ const targetSessionId = requireId(sessionId, 'invalid_session_id', 'Session id');
213
+ const project = stmts.getYeaftProjectForSession.get(ownerId, targetAgentId, targetSessionId);
214
+ if (!project) return null;
215
+ const sameAgentMembers = stmts.getYeaftProjectMembersForAgent
216
+ .all(ownerId, project.id, targetAgentId)
217
+ .map(row => row.session_id)
218
+ .filter(id => id && id !== targetSessionId);
219
+ return {
220
+ projectId: project.id,
221
+ projectName: project.name,
222
+ sessionIds: sameAgentMembers,
223
+ };
224
+ },
225
+ };
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from 'crypto';
2
- import { messageDb, yeaftSessionDb } from '../database.js';
2
+ import { messageDb, yeaftProjectDb, yeaftSessionDb } from '../database.js';
3
+ import { transaction } from '../db/connection.js';
3
4
  import { broadcastAgentList, broadcastSessionCatalog, forwardToClients, sendToAgent, sendToWebClient } from '../ws-utils.js';
4
5
  import { webClients, previewFiles } from '../context.js';
5
6
  import { CONFIG } from '../config.js';
@@ -33,6 +34,19 @@ export function decorateYeaftSessionsWithPinned(agentId, sessions) {
33
34
  });
34
35
  }
35
36
 
37
+ function reconcileAuthoritativeSessionSnapshot(ownerId, agentId, sessions) {
38
+ const rows = Array.isArray(sessions) ? sessions : [];
39
+ transaction(() => {
40
+ yeaftSessionDb.reconcileFromSnapshot(ownerId, agentId, rows);
41
+ yeaftProjectDb.reconcileAgentSessions(
42
+ ownerId,
43
+ agentId,
44
+ rows.map(row => row?.id).filter(Boolean),
45
+ );
46
+ })();
47
+ return rows;
48
+ }
49
+
36
50
  function syncYeaftSessionMetadata(agentId, agent, event) {
37
51
  if (!event || typeof event !== 'object') return event;
38
52
  const ownerId = agent?.ownerId || null;
@@ -40,7 +54,15 @@ function syncYeaftSessionMetadata(agentId, agent, event) {
40
54
  if (event.type === 'session_list_updated') {
41
55
  const rows = Array.isArray(event.sessions) ? event.sessions : [];
42
56
  try {
43
- if (ownerId) yeaftSessionDb.reconcileFromSnapshot(ownerId, agentId, rows);
57
+ if (ownerId) {
58
+ reconcileAuthoritativeSessionSnapshot(ownerId, agentId, rows);
59
+ yeaftProjectDb.importLegacyProjects(
60
+ ownerId,
61
+ agentId,
62
+ event.projects,
63
+ sessionId => rows.some(row => row?.id === sessionId),
64
+ );
65
+ }
44
66
  } catch (e) {
45
67
  console.warn(`[Server] yeaft session persist failed for agent ${agentId}:`, e?.message || e);
46
68
  }
@@ -52,7 +74,7 @@ function syncYeaftSessionMetadata(agentId, agent, event) {
52
74
  const sessionId = event.sessionId;
53
75
  if (event.ok && op === 'list' && Array.isArray(event.sessions)) {
54
76
  try {
55
- if (ownerId) yeaftSessionDb.reconcileFromSnapshot(ownerId, agentId, event.sessions);
77
+ if (ownerId) reconcileAuthoritativeSessionSnapshot(ownerId, agentId, event.sessions);
56
78
  } catch (e) {
57
79
  console.warn(`[Server] yeaft session list persist failed for agent ${agentId}:`, e?.message || e);
58
80
  }
@@ -63,6 +85,7 @@ function syncYeaftSessionMetadata(agentId, agent, event) {
63
85
  if (op === 'archive') {
64
86
  try {
65
87
  yeaftSessionDb.setArchivedForAgent(ownerId, agentId, sessionId, true);
88
+ yeaftProjectDb.removeSession(ownerId, agentId, sessionId);
66
89
  } catch (e) {
67
90
  console.warn('[Server] Yeaft Session metadata archive failed:', e?.message || e);
68
91
  }
@@ -71,6 +94,7 @@ function syncYeaftSessionMetadata(agentId, agent, event) {
71
94
 
72
95
  try {
73
96
  yeaftSessionDb.deleteForAgent(ownerId, agentId, sessionId);
97
+ yeaftProjectDb.removeSession(ownerId, agentId, sessionId);
74
98
  } catch (e) {
75
99
  console.warn('[Server] Yeaft Session metadata cleanup failed:', e?.message || e);
76
100
  }
@@ -527,7 +551,14 @@ export async function handleAgentOutput(agentId, agent, msg) {
527
551
  case 'yeaft_session_output':
528
552
  case 'session_output': {
529
553
  const data = hydrateInlinePreviewData(msg.data);
530
- const event = syncYeaftSessionMetadata(agentId, agent, msg.event);
554
+ let event = syncYeaftSessionMetadata(agentId, agent, msg.event);
555
+ if ((event?.type === 'session_list_updated' || event?.type === 'session_crud_result') && agent.ownerId) {
556
+ event = {
557
+ ...event,
558
+ projects: yeaftProjectDb.listForAgent(agent.ownerId, agentId),
559
+ projectsAuthoritative: true,
560
+ };
561
+ }
531
562
  let catalogChanged = false;
532
563
  if (event?.type === 'yeaft_status') {
533
564
  agent.yeaftStatus = event;
@@ -850,6 +881,8 @@ export async function handleAgentOutput(agentId, agent, msg) {
850
881
  type: msg.type,
851
882
  agentId: agentId,
852
883
  sessions: decoratedSessions,
884
+ projects: agent.ownerId ? yeaftProjectDb.listForAgent(agent.ownerId, agentId) : [],
885
+ projectsAuthoritative: true,
853
886
  });
854
887
  }
855
888
  }
@@ -907,7 +940,10 @@ export async function handleAgentOutput(agentId, agent, msg) {
907
940
  // probes too: entering Yeaft asks each connected agent for its opened
908
941
  // sessions, and that response must carry persisted server-side pin
909
942
  // state before it hits the web store.
910
- const outboundMsg = syncYeaftSessionMetadata(agentId, agent, msg);
943
+ const syncedMsg = syncYeaftSessionMetadata(agentId, agent, msg);
944
+ const outboundMsg = agent.ownerId
945
+ ? { ...syncedMsg, projects: yeaftProjectDb.listForAgent(agent.ownerId, agentId), projectsAuthoritative: true }
946
+ : { ...syncedMsg, projects: [], projectsAuthoritative: true };
911
947
  // CRUD acknowledgements are not authoritative catalog snapshots. The
912
948
  // agent emits session_list_updated after mutations; broadcast only after
913
949
  // that reconciliation so create/rename cannot briefly project stale data.
@@ -4,6 +4,7 @@ import {
4
4
  sessionDb,
5
5
  messageDb,
6
6
  userDb,
7
+ yeaftProjectDb,
7
8
  yeaftSessionDb,
8
9
  sessionUiMetadataDb,
9
10
  } from '../database.js';
@@ -1277,6 +1278,65 @@ export async function handleClientConversation(clientId, client, msg, checkAgent
1277
1278
  break;
1278
1279
  }
1279
1280
 
1281
+ case 'yeaft_project_mutation': {
1282
+ const requestId = typeof msg.requestId === 'string' ? msg.requestId : null;
1283
+ const op = msg.op;
1284
+ const respond = async (payload) => {
1285
+ await sendToWebClient(client, {
1286
+ type: 'yeaft_output',
1287
+ agentId: msg.targetAgentId || msg.agentId || client.currentAgent || null,
1288
+ event: {
1289
+ type: 'project_mutation_result',
1290
+ requestId,
1291
+ op,
1292
+ projectsAuthoritative: true,
1293
+ ...payload,
1294
+ },
1295
+ });
1296
+ };
1297
+ try {
1298
+ if (!client.userId) throw new Error('User identity is required');
1299
+ let result = null;
1300
+ if (op === 'create') result = yeaftProjectDb.create(client.userId, msg.name);
1301
+ else if (op === 'rename') result = yeaftProjectDb.rename(client.userId, msg.projectId, msg.name);
1302
+ else if (op === 'delete') result = yeaftProjectDb.delete(client.userId, msg.projectId);
1303
+ else if (op === 'move_session') {
1304
+ const agentId = msg.targetAgentId || msg.agentId;
1305
+ const sessionId = typeof msg.sessionId === 'string' ? msg.sessionId.trim() : '';
1306
+ if (!agentId || !sessionId) throw new Error('Agent and Session identities are required');
1307
+ const targetSession = yeaftSessionDb.getForAgent(client.userId, agentId, sessionId);
1308
+ if (!targetSession) throw new Error('Session not found');
1309
+ if (targetSession.isArchived) {
1310
+ const error = new Error('Archived Sessions cannot be moved to Projects');
1311
+ error.code = 'session_archived';
1312
+ throw error;
1313
+ }
1314
+ result = yeaftProjectDb.moveSession(client.userId, {
1315
+ agentId,
1316
+ sessionId,
1317
+ projectId: msg.projectId || null,
1318
+ });
1319
+ } else {
1320
+ throw new Error('Unknown Project operation');
1321
+ }
1322
+ const responseAgentId = msg.targetAgentId || msg.agentId || client.currentAgent || null;
1323
+ const projects = responseAgentId
1324
+ ? yeaftProjectDb.listForAgent(client.userId, responseAgentId)
1325
+ : yeaftProjectDb.list(client.userId);
1326
+ await respond({ ok: true, result, projects });
1327
+ await broadcastSessionCatalog(client.userId);
1328
+ } catch (err) {
1329
+ await respond({
1330
+ ok: false,
1331
+ error: {
1332
+ code: err?.code || 'project_mutation_failed',
1333
+ message: err?.message || String(err),
1334
+ },
1335
+ });
1336
+ }
1337
+ break;
1338
+ }
1339
+
1280
1340
  case 'yeaft_merge_thread':
1281
1341
  case 'unify_merge_thread': {
1282
1342
  const mergeAgentId = msg.agentId || client.currentAgent;
@@ -1404,6 +1464,19 @@ export async function handleClientConversation(clientId, client, msg, checkAgent
1404
1464
  rest.type = relayType;
1405
1465
  if (rest.type === 'yeaft_session_send' || rest.type === 'yeaft_session_chat') {
1406
1466
  trackUserTurn(client.userId, Buffer.byteLength(JSON.stringify(msg)));
1467
+ const sessionId = typeof rest.sessionId === 'string' ? rest.sessionId.trim() : '';
1468
+ if (sessionId && client.userId) {
1469
+ const projectContext = yeaftProjectDb.contextForSession(
1470
+ client.userId,
1471
+ relayAgentId,
1472
+ sessionId,
1473
+ );
1474
+ rest.projectContext = projectContext || {
1475
+ projectId: null,
1476
+ projectName: null,
1477
+ sessionIds: [],
1478
+ };
1479
+ }
1407
1480
  }
1408
1481
 
1409
1482
  if (rest.type === 'yeaft_session_chat' && !rest.id) {
@@ -239,7 +239,13 @@ export function registerAuthRoutes(app, { requireAuth, checkRateLimit }) {
239
239
  if (!r) return res.json({ status: 'pending' });
240
240
  if (r.kind === 'login') {
241
241
  setSessionCookie(req, res, r.token);
242
- return res.json({ status: 'login', token: r.token, sessionKey: r.sessionKey, role: r.role });
242
+ return res.json({
243
+ status: 'login',
244
+ token: r.token,
245
+ sessionKey: r.sessionKey,
246
+ userId: r.userId,
247
+ role: r.role,
248
+ });
243
249
  }
244
250
  if (r.kind === 'bind') {
245
251
  return res.json({ status: 'bind', provider: r.provider });
@@ -278,6 +284,7 @@ export function registerAuthRoutes(app, { requireAuth, checkRateLimit }) {
278
284
  const params = new URLSearchParams({
279
285
  token: result.token,
280
286
  sessionKey: result.sessionKey,
287
+ userId: result.userId || '',
281
288
  role: result.role
282
289
  });
283
290
  return res.redirect(`/#/sso-complete?${params.toString()}`);
@@ -57,6 +57,7 @@ export function registerUserRoutes(app, { requireAuth, requireAdmin }) {
57
57
  return res.status(404).json({ error: 'User not found' });
58
58
  }
59
59
  res.json({
60
+ userId: user.id,
60
61
  username: user.username,
61
62
  displayName: user.display_name,
62
63
  email: user.email,
@@ -1,7 +1,7 @@
1
1
  import { WebSocket } from 'ws';
2
2
  import { CONFIG } from './config.js';
3
3
  import { encrypt, decrypt, isEncrypted, encodeKey } from './encryption.js';
4
- import { sessionDb, yeaftSessionDb, sessionUiMetadataDb } from './database.js';
4
+ import { sessionDb, yeaftProjectDb, yeaftSessionDb, sessionUiMetadataDb } from './database.js';
5
5
  import { projectSessionCatalog } from './session-catalog.js';
6
6
  import { agents, webClients, directoryCache, DIR_CACHE_TTL, DIR_CACHE_MAX_SIZE, trackMessageBytesSent } from './context.js';
7
7
 
@@ -126,6 +126,8 @@ export async function broadcastSessionCatalog(userId) {
126
126
  await sendToWebClient(client, {
127
127
  type: 'session_catalog_snapshot',
128
128
  catalog: buildSessionCatalog(userId, client.role),
129
+ projects: yeaftProjectDb.list(userId),
130
+ projectsAuthoritative: true,
129
131
  });
130
132
  } catch (e) {
131
133
  console.warn('[Server] session catalog projection failed:', e?.message || e);
@@ -187,6 +189,8 @@ export async function broadcastAgentList() {
187
189
  await sendToWebClient(client, {
188
190
  type: 'session_catalog_snapshot',
189
191
  catalog: buildSessionCatalog(client.userId, client.role),
192
+ projects: yeaftProjectDb.list(client.userId),
193
+ projectsAuthoritative: true,
190
194
  });
191
195
  } catch (e) {
192
196
  console.warn('[Server] session catalog projection failed:', e?.message || e);
@@ -1 +1 @@
1
- {"version":"1.0.298"}
1
+ {"version":"1.0.300"}