@yeaft/webchat-agent 1.0.186 → 1.0.189
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/cli.js +12 -0
- package/index.js +10 -2
- package/local-run.js +218 -0
- package/local-runtime/server/.env.example +54 -0
- package/local-runtime/server/api.js +111 -0
- package/local-runtime/server/auth/aad.js +235 -0
- package/local-runtime/server/auth/login.js +156 -0
- package/local-runtime/server/auth/oauth-flow.js +277 -0
- package/local-runtime/server/auth/password-reset.js +134 -0
- package/local-runtime/server/auth/providers/alipay.js +125 -0
- package/local-runtime/server/auth/providers/github.js +82 -0
- package/local-runtime/server/auth/providers/google.js +60 -0
- package/local-runtime/server/auth/providers/microsoft.js +71 -0
- package/local-runtime/server/auth/providers/types.js +57 -0
- package/local-runtime/server/auth/providers/wechat.js +68 -0
- package/local-runtime/server/auth/register.js +91 -0
- package/local-runtime/server/auth/session-store.js +57 -0
- package/local-runtime/server/auth/token.js +85 -0
- package/local-runtime/server/auth/totp-auth.js +133 -0
- package/local-runtime/server/auth/utils.js +42 -0
- package/local-runtime/server/auth.js +8 -0
- package/local-runtime/server/check-node-version.js +74 -0
- package/local-runtime/server/config.js +298 -0
- package/local-runtime/server/context.js +140 -0
- package/local-runtime/server/create-user.js +59 -0
- package/local-runtime/server/database.js +12 -0
- package/local-runtime/server/db/connection.js +963 -0
- package/local-runtime/server/db/expert-db.js +171 -0
- package/local-runtime/server/db/identity-db.js +92 -0
- package/local-runtime/server/db/invitation-db.js +38 -0
- package/local-runtime/server/db/message-db.js +257 -0
- package/local-runtime/server/db/session-db.js +118 -0
- package/local-runtime/server/db/user-db.js +165 -0
- package/local-runtime/server/db/user-stats-db.js +185 -0
- package/local-runtime/server/db/yeaft-session-db.js +258 -0
- package/local-runtime/server/email.js +96 -0
- package/local-runtime/server/encryption.js +105 -0
- package/local-runtime/server/handlers/agent-conversation.js +347 -0
- package/local-runtime/server/handlers/agent-file-terminal.js +99 -0
- package/local-runtime/server/handlers/agent-output.js +854 -0
- package/local-runtime/server/handlers/agent-sync.js +399 -0
- package/local-runtime/server/handlers/agent-work-center.js +27 -0
- package/local-runtime/server/handlers/client-conversation.js +1182 -0
- package/local-runtime/server/handlers/client-misc.js +254 -0
- package/local-runtime/server/handlers/client-work-center.js +269 -0
- package/local-runtime/server/handlers/client-workbench.js +146 -0
- package/local-runtime/server/handlers/session-pin-router.js +61 -0
- package/local-runtime/server/heartbeat-policy.js +46 -0
- package/local-runtime/server/index.js +275 -0
- package/local-runtime/server/package.json +55 -0
- package/local-runtime/server/perf-trace.js +154 -0
- package/local-runtime/server/proxy.js +273 -0
- package/local-runtime/server/routes/admin-routes.js +207 -0
- package/local-runtime/server/routes/auth-routes.js +322 -0
- package/local-runtime/server/routes/expert-routes.js +117 -0
- package/local-runtime/server/routes/invitation-routes.js +60 -0
- package/local-runtime/server/routes/session-routes.js +112 -0
- package/local-runtime/server/routes/upload-routes.js +109 -0
- package/local-runtime/server/routes/user-routes.js +241 -0
- package/local-runtime/server/totp.js +74 -0
- package/local-runtime/server/work-item-attachment-policy.js +56 -0
- package/local-runtime/server/ws-agent.js +319 -0
- package/local-runtime/server/ws-client.js +214 -0
- package/local-runtime/server/ws-utils.js +394 -0
- package/local-runtime/server/yeaft-asset-store.js +339 -0
- package/local-runtime/version.json +1 -0
- package/local-runtime/web/app.bundle.js +7673 -0
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/assets/avatars/README.md +34 -0
- package/local-runtime/web/assets/avatars/ada.svg +1 -0
- package/local-runtime/web/assets/avatars/alan.svg +1 -0
- package/local-runtime/web/assets/avatars/alice.svg +1 -0
- package/local-runtime/web/assets/avatars/anders.svg +1 -0
- package/local-runtime/web/assets/avatars/bezos.svg +1 -0
- package/local-runtime/web/assets/avatars/borges.svg +1 -0
- package/local-runtime/web/assets/avatars/buffett.svg +1 -0
- package/local-runtime/web/assets/avatars/clausewitz.svg +1 -0
- package/local-runtime/web/assets/avatars/dalio.svg +1 -0
- package/local-runtime/web/assets/avatars/dieter.svg +1 -0
- package/local-runtime/web/assets/avatars/drucker.svg +1 -0
- package/local-runtime/web/assets/avatars/einstein.svg +1 -0
- package/local-runtime/web/assets/avatars/grace.svg +1 -0
- package/local-runtime/web/assets/avatars/harari.svg +1 -0
- package/local-runtime/web/assets/avatars/jung.svg +1 -0
- package/local-runtime/web/assets/avatars/kahneman.svg +1 -0
- package/local-runtime/web/assets/avatars/ken.svg +1 -0
- package/local-runtime/web/assets/avatars/kongzi.svg +1 -0
- package/local-runtime/web/assets/avatars/kubrick.svg +1 -0
- package/local-runtime/web/assets/avatars/linus.svg +1 -0
- package/local-runtime/web/assets/avatars/luxun.svg +1 -0
- package/local-runtime/web/assets/avatars/margaret.svg +1 -0
- package/local-runtime/web/assets/avatars/martin.svg +1 -0
- package/local-runtime/web/assets/avatars/miyazaki.svg +1 -0
- package/local-runtime/web/assets/avatars/munger.svg +1 -0
- package/local-runtime/web/assets/avatars/nietzsche.svg +1 -0
- package/local-runtime/web/assets/avatars/norman.svg +1 -0
- package/local-runtime/web/assets/avatars/shannon.svg +1 -0
- package/local-runtime/web/assets/avatars/simaqian.svg +1 -0
- package/local-runtime/web/assets/avatars/socrates.svg +1 -0
- package/local-runtime/web/assets/avatars/steve.svg +1 -0
- package/local-runtime/web/assets/avatars/sudongpo.svg +1 -0
- package/local-runtime/web/assets/avatars/sunzi.svg +1 -0
- package/local-runtime/web/docx-preview.min.js +2 -0
- package/local-runtime/web/docx-preview.min.js.gz +0 -0
- package/local-runtime/web/html-to-image.min.js +2 -0
- package/local-runtime/web/html-to-image.min.js.gz +0 -0
- package/local-runtime/web/index.html +21 -0
- package/local-runtime/web/jszip.min.js +13 -0
- package/local-runtime/web/jszip.min.js.gz +0 -0
- package/local-runtime/web/mermaid.min.js +2843 -0
- package/local-runtime/web/mermaid.min.js.gz +0 -0
- package/local-runtime/web/msal-browser.min.js +69 -0
- package/local-runtime/web/msal-browser.min.js.gz +0 -0
- package/local-runtime/web/style.bundle.css +10 -0
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/local-runtime/web/vendor.bundle.js +1522 -0
- package/local-runtime/web/vendor.bundle.js.gz +0 -0
- package/local-runtime/web/xlsx.min.js +24 -0
- package/local-runtime/web/xlsx.min.js.gz +0 -0
- package/package.json +13 -3
- package/scripts/prepare-local-runtime.js +25 -0
|
@@ -0,0 +1,854 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { messageDb, yeaftSessionDb } from '../database.js';
|
|
3
|
+
import { broadcastAgentList, forwardToClients, sendToAgent, sendToWebClient } from '../ws-utils.js';
|
|
4
|
+
import { webClients, previewFiles } from '../context.js';
|
|
5
|
+
import { CONFIG } from '../config.js';
|
|
6
|
+
import { yeaftAssetStore } from '../yeaft-asset-store.js';
|
|
7
|
+
import { recordPerfTraceEvent } from '../perf-trace.js';
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
export function decorateYeaftSessionsWithPinned(agentId, sessions) {
|
|
11
|
+
const rawRows = Array.isArray(sessions) ? sessions : [];
|
|
12
|
+
const rowsById = new Map();
|
|
13
|
+
let authoritative = true;
|
|
14
|
+
try {
|
|
15
|
+
for (const row of yeaftSessionDb.getByAgent(agentId)) {
|
|
16
|
+
if (row && row.id) rowsById.set(row.id, row);
|
|
17
|
+
}
|
|
18
|
+
} catch (e) {
|
|
19
|
+
authoritative = false;
|
|
20
|
+
console.warn(`[Server] yeaft session metadata decorate read failed for agent ${agentId}:`, e?.message || e);
|
|
21
|
+
}
|
|
22
|
+
if (!authoritative) return rawRows;
|
|
23
|
+
return rawRows.map(s => {
|
|
24
|
+
if (!s || !s.id) return s;
|
|
25
|
+
const row = rowsById.get(s.id);
|
|
26
|
+
const pinned = !!row?.isPinned;
|
|
27
|
+
return {
|
|
28
|
+
...s,
|
|
29
|
+
pinned,
|
|
30
|
+
isPinned: pinned,
|
|
31
|
+
...(Number.isFinite(row?.sortOrder) ? { sortOrder: row.sortOrder } : {}),
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function hydrateMessagePreviewData(message) {
|
|
37
|
+
const attachments = message?.attachments;
|
|
38
|
+
if (!Array.isArray(attachments) || attachments.length === 0) return message;
|
|
39
|
+
let changed = false;
|
|
40
|
+
const hydrated = attachments.map((att) => {
|
|
41
|
+
const previewData = att?.previewData;
|
|
42
|
+
if (!previewData?.data || att.preview) return att;
|
|
43
|
+
const fileId = randomUUID();
|
|
44
|
+
const token = randomUUID();
|
|
45
|
+
previewFiles.set(fileId, {
|
|
46
|
+
buffer: Buffer.from(previewData.data, 'base64'),
|
|
47
|
+
mimeType: previewData.mimeType || att.mimeType || 'application/octet-stream',
|
|
48
|
+
filename: previewData.filename || att.name || `attachment-${Date.now()}`,
|
|
49
|
+
createdAt: Date.now(),
|
|
50
|
+
token,
|
|
51
|
+
});
|
|
52
|
+
changed = true;
|
|
53
|
+
const { previewData: _previewData, ...rest } = att;
|
|
54
|
+
return {
|
|
55
|
+
...rest,
|
|
56
|
+
preview: `/api/preview/${fileId}?token=${encodeURIComponent(token)}`,
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
return changed ? { ...message, attachments: hydrated } : message;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function hydrateInlinePreviewData(data) {
|
|
63
|
+
if (!data?.message) return data;
|
|
64
|
+
const message = hydrateMessagePreviewData(data.message);
|
|
65
|
+
return message === data.message ? data : { ...data, message };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function projectConfirmedAssetImages(messages, { ownerId, agentId, sessionId }) {
|
|
69
|
+
const turnIds = messages
|
|
70
|
+
.filter(message => message?.role === 'assistant' && message.imageAssetAnchor === true)
|
|
71
|
+
.map(message => message.turnId || message.threadId || null)
|
|
72
|
+
.filter(Boolean);
|
|
73
|
+
const imagesByTurn = yeaftAssetStore.describeTurns({ ownerId, agentId, sessionId, turnIds });
|
|
74
|
+
return messages.map((message) => {
|
|
75
|
+
if (!message || message.role !== 'assistant') return message;
|
|
76
|
+
const { images: _pendingImages, ...rest } = message;
|
|
77
|
+
if (message.imageAssetAnchor !== true) return rest;
|
|
78
|
+
const turnId = message.turnId || message.threadId || null;
|
|
79
|
+
const images = turnId ? imagesByTurn.get(turnId) || [] : [];
|
|
80
|
+
return images.length > 0 ? { ...rest, images } : rest;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Handle CLI/Yeaft output and interaction messages from agent.
|
|
86
|
+
* Types: claude_output (legacy Claude/Copilot CLI output frame),
|
|
87
|
+
* yeaft_output / yeaft_session_output / session_output,
|
|
88
|
+
* chat_image, context_usage, execution_cancelled,
|
|
89
|
+
* background_task_started, background_task_output,
|
|
90
|
+
* slash_commands_update, compact_status, ask_user_question,
|
|
91
|
+
* conversation_mcp_update, error,
|
|
92
|
+
* btw_stream, btw_done, btw_error
|
|
93
|
+
*/
|
|
94
|
+
export async function handleAgentOutput(agentId, agent, msg) {
|
|
95
|
+
switch (msg.type) {
|
|
96
|
+
case 'claude_output':
|
|
97
|
+
// 保存消息到数据库
|
|
98
|
+
try {
|
|
99
|
+
const data = msg.data;
|
|
100
|
+
if (data && msg.conversationId) {
|
|
101
|
+
if (data.type === 'user' && data.message?.content) {
|
|
102
|
+
const rawContent = data.message.content;
|
|
103
|
+
const content = typeof rawContent === 'string'
|
|
104
|
+
? rawContent
|
|
105
|
+
: (Array.isArray(rawContent) ? rawContent.map(b => b.text || '').join('') : JSON.stringify(rawContent));
|
|
106
|
+
// Skip empty content — tool_result arrays produce '' which shouldn't be stored as user messages
|
|
107
|
+
if (content) {
|
|
108
|
+
// 检查 convInfo 上暂存的 expertSelections,保存为 metadata
|
|
109
|
+
const conv = agent.conversations.get(msg.conversationId);
|
|
110
|
+
// fix-usermsg-dup: pull the client-stamped id stashed by the
|
|
111
|
+
// `chat` handler so the echo carries it back to the web for
|
|
112
|
+
// optimistic-dedup, AND so the DB row preserves it for
|
|
113
|
+
// post-refresh `sync_messages_result` replay. Without the DB
|
|
114
|
+
// half, refresh → resend would lose the dedup key and fall
|
|
115
|
+
// back to the unreliable content-equality path.
|
|
116
|
+
const clientMessageId = conv?._pendingClientMessageId || null;
|
|
117
|
+
// Review M2 (Torvalds): build the payload first, then
|
|
118
|
+
// clear the stash. Mixing mutation with build made the
|
|
119
|
+
// delete look conditional on the JSON construction.
|
|
120
|
+
const metaParts = {};
|
|
121
|
+
if (conv?._pendingExperts) metaParts.experts = conv._pendingExperts;
|
|
122
|
+
if (clientMessageId) metaParts.clientMessageId = clientMessageId;
|
|
123
|
+
if (conv) {
|
|
124
|
+
delete conv._pendingExperts;
|
|
125
|
+
delete conv._pendingClientMessageId;
|
|
126
|
+
}
|
|
127
|
+
const metadata = Object.keys(metaParts).length > 0 ? JSON.stringify(metaParts) : null;
|
|
128
|
+
const dbId = messageDb.add(msg.conversationId, 'user', content, 'user', null, null, metadata);
|
|
129
|
+
msg.data.dbMessageId = dbId;
|
|
130
|
+
if (clientMessageId) {
|
|
131
|
+
msg.data.clientMessageId = clientMessageId;
|
|
132
|
+
}
|
|
133
|
+
// User-turn stats are recorded at the web send boundary so
|
|
134
|
+
// inbound payload bytes are counted exactly once.
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (data.type === 'assistant' && data.message?.content) {
|
|
138
|
+
let content;
|
|
139
|
+
if (typeof data.message.content === 'string') {
|
|
140
|
+
content = data.message.content;
|
|
141
|
+
} else if (Array.isArray(data.message.content)) {
|
|
142
|
+
content = data.message.content
|
|
143
|
+
.filter(block => block.type === 'text' && block.text)
|
|
144
|
+
.map(block => block.text)
|
|
145
|
+
.join('');
|
|
146
|
+
} else {
|
|
147
|
+
content = JSON.stringify(data.message.content);
|
|
148
|
+
}
|
|
149
|
+
if (content) {
|
|
150
|
+
const dbId = messageDb.add(msg.conversationId, 'assistant', content, 'assistant');
|
|
151
|
+
msg.data.dbMessageId = dbId;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (data.type === 'assistant' && data.message?.content) {
|
|
155
|
+
const contents = Array.isArray(data.message.content)
|
|
156
|
+
? data.message.content
|
|
157
|
+
: [data.message.content];
|
|
158
|
+
for (const item of contents) {
|
|
159
|
+
if (item.type === 'tool_use') {
|
|
160
|
+
messageDb.add(
|
|
161
|
+
msg.conversationId,
|
|
162
|
+
'assistant',
|
|
163
|
+
JSON.stringify(item.input || {}),
|
|
164
|
+
'tool_use',
|
|
165
|
+
item.name,
|
|
166
|
+
JSON.stringify(item.input || {})
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (data.type === 'result') {
|
|
172
|
+
const resultContent = typeof data.result === 'string'
|
|
173
|
+
? data.result
|
|
174
|
+
: JSON.stringify(data.result);
|
|
175
|
+
const truncated = resultContent.length > 10000
|
|
176
|
+
? resultContent.slice(0, 10000) + '...[truncated]'
|
|
177
|
+
: resultContent;
|
|
178
|
+
messageDb.add(msg.conversationId, 'tool', truncated, 'tool_result');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} catch (e) {
|
|
182
|
+
// 静默处理保存错误,不影响正常流程
|
|
183
|
+
}
|
|
184
|
+
await forwardToClients(agentId, msg.conversationId, {
|
|
185
|
+
type: 'claude_output',
|
|
186
|
+
conversationId: msg.conversationId,
|
|
187
|
+
data: msg.data
|
|
188
|
+
});
|
|
189
|
+
break;
|
|
190
|
+
|
|
191
|
+
case 'context_usage':
|
|
192
|
+
await forwardToClients(agentId, msg.conversationId, {
|
|
193
|
+
type: 'context_usage',
|
|
194
|
+
conversationId: msg.conversationId,
|
|
195
|
+
inputTokens: msg.inputTokens,
|
|
196
|
+
maxTokens: msg.maxTokens,
|
|
197
|
+
percentage: msg.percentage
|
|
198
|
+
});
|
|
199
|
+
break;
|
|
200
|
+
|
|
201
|
+
case 'execution_cancelled': {
|
|
202
|
+
const cancelledConv = agent.conversations.get(msg.conversationId);
|
|
203
|
+
if (cancelledConv) {
|
|
204
|
+
cancelledConv.processing = false;
|
|
205
|
+
}
|
|
206
|
+
await forwardToClients(agentId, msg.conversationId, {
|
|
207
|
+
type: 'execution_cancelled',
|
|
208
|
+
conversationId: msg.conversationId
|
|
209
|
+
});
|
|
210
|
+
await broadcastAgentList();
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
case 'background_task_started':
|
|
215
|
+
console.log(`[Background] Task started: ${msg.task?.id} in conversation ${msg.conversationId}`);
|
|
216
|
+
await forwardToClients(agentId, msg.conversationId, {
|
|
217
|
+
type: 'background_task_started',
|
|
218
|
+
conversationId: msg.conversationId,
|
|
219
|
+
task: msg.task
|
|
220
|
+
});
|
|
221
|
+
break;
|
|
222
|
+
|
|
223
|
+
case 'background_task_output':
|
|
224
|
+
await forwardToClients(agentId, msg.conversationId, {
|
|
225
|
+
type: 'background_task_output',
|
|
226
|
+
conversationId: msg.conversationId,
|
|
227
|
+
taskId: msg.taskId,
|
|
228
|
+
task: msg.task,
|
|
229
|
+
newOutput: msg.newOutput
|
|
230
|
+
});
|
|
231
|
+
break;
|
|
232
|
+
|
|
233
|
+
case 'subagent_started':
|
|
234
|
+
case 'subagent_message':
|
|
235
|
+
case 'subagent_completed':
|
|
236
|
+
await forwardToClients(agentId, msg.conversationId, msg);
|
|
237
|
+
break;
|
|
238
|
+
|
|
239
|
+
case 'slash_commands_update':
|
|
240
|
+
// 缓存到 agent 对象上,供 web 端选择 agent 时立即获取
|
|
241
|
+
agent.slashCommands = msg.slashCommands || [];
|
|
242
|
+
if (msg.slashCommandDescriptions) {
|
|
243
|
+
agent.slashCommandDescriptions = { ...agent.slashCommandDescriptions, ...msg.slashCommandDescriptions };
|
|
244
|
+
}
|
|
245
|
+
if (msg.conversationId === '__preload__') {
|
|
246
|
+
// Agent-level preload: broadcast to all owner clients with agentId
|
|
247
|
+
for (const [, client] of webClients) {
|
|
248
|
+
if (client.authenticated && (CONFIG.skipAuth || agent.ownerId === client.userId)) {
|
|
249
|
+
await sendToWebClient(client, {
|
|
250
|
+
type: 'slash_commands_update',
|
|
251
|
+
agentId,
|
|
252
|
+
slashCommands: msg.slashCommands,
|
|
253
|
+
slashCommandDescriptions: agent.slashCommandDescriptions || {}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
// Per-conversation update. Stamp agentId too so
|
|
259
|
+
// Yeaft's temporary/local conversation id cannot hide the agent-level
|
|
260
|
+
// fallback command list in the web store.
|
|
261
|
+
await forwardToClients(agentId, msg.conversationId, {
|
|
262
|
+
type: 'slash_commands_update',
|
|
263
|
+
agentId,
|
|
264
|
+
conversationId: msg.conversationId,
|
|
265
|
+
slashCommands: msg.slashCommands,
|
|
266
|
+
slashCommandDescriptions: agent.slashCommandDescriptions || {}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
break;
|
|
270
|
+
|
|
271
|
+
case 'compact_status':
|
|
272
|
+
console.log(`[Compact] Status: ${msg.status} for conversation ${msg.conversationId}`);
|
|
273
|
+
await forwardToClients(agentId, msg.conversationId, {
|
|
274
|
+
type: 'compact_status',
|
|
275
|
+
conversationId: msg.conversationId,
|
|
276
|
+
status: msg.status,
|
|
277
|
+
message: msg.message
|
|
278
|
+
});
|
|
279
|
+
break;
|
|
280
|
+
|
|
281
|
+
case 'ask_user_question':
|
|
282
|
+
console.log(`[AskUser] Question for conversation ${msg.conversationId}, requestId: ${msg.requestId}`);
|
|
283
|
+
// Persist requestId + questions into the AskUserQuestion tool_use DB record
|
|
284
|
+
try {
|
|
285
|
+
if (msg.conversationId && msg.requestId) {
|
|
286
|
+
// Search enough messages to find the tool_use record (may be far from latest
|
|
287
|
+
// if there was a lot of streaming output between tool_use and ask_user_question)
|
|
288
|
+
const recent = messageDb.getRecent(msg.conversationId, 100);
|
|
289
|
+
// Find the LATEST unlinked AskUserQuestion (search from newest to oldest)
|
|
290
|
+
let askMsg = null;
|
|
291
|
+
for (let i = recent.length - 1; i >= 0; i--) {
|
|
292
|
+
const m = recent[i];
|
|
293
|
+
if (m.message_type === 'tool_use' && m.tool_name === 'AskUserQuestion') {
|
|
294
|
+
// Check if already linked to a different requestId
|
|
295
|
+
if (m.metadata) {
|
|
296
|
+
try {
|
|
297
|
+
const existing = JSON.parse(m.metadata);
|
|
298
|
+
if (existing.askRequestId && existing.askRequestId !== msg.requestId) continue;
|
|
299
|
+
} catch { /* proceed */ }
|
|
300
|
+
}
|
|
301
|
+
askMsg = m;
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (askMsg) {
|
|
306
|
+
messageDb.updateMetadata(askMsg.id, JSON.stringify({
|
|
307
|
+
askRequestId: msg.requestId,
|
|
308
|
+
askQuestions: msg.questions
|
|
309
|
+
}));
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
} catch (e) {
|
|
313
|
+
// Silent — don't block the main flow
|
|
314
|
+
}
|
|
315
|
+
await forwardToClients(agentId, msg.conversationId, {
|
|
316
|
+
type: 'ask_user_question',
|
|
317
|
+
conversationId: msg.conversationId,
|
|
318
|
+
requestId: msg.requestId,
|
|
319
|
+
questions: msg.questions
|
|
320
|
+
});
|
|
321
|
+
break;
|
|
322
|
+
|
|
323
|
+
case 'conversation_mcp_update':
|
|
324
|
+
await forwardToClients(agentId, msg.conversationId, {
|
|
325
|
+
type: 'conversation_mcp_update',
|
|
326
|
+
conversationId: msg.conversationId,
|
|
327
|
+
servers: msg.servers,
|
|
328
|
+
serverTools: msg.serverTools
|
|
329
|
+
});
|
|
330
|
+
break;
|
|
331
|
+
|
|
332
|
+
case 'error':
|
|
333
|
+
await forwardToClients(agentId, msg.conversationId, {
|
|
334
|
+
type: 'error',
|
|
335
|
+
conversationId: msg.conversationId,
|
|
336
|
+
message: msg.message
|
|
337
|
+
});
|
|
338
|
+
break;
|
|
339
|
+
|
|
340
|
+
case 'btw_stream':
|
|
341
|
+
await forwardToClients(agentId, msg.conversationId, {
|
|
342
|
+
type: 'btw_stream',
|
|
343
|
+
conversationId: msg.conversationId,
|
|
344
|
+
delta: msg.delta
|
|
345
|
+
});
|
|
346
|
+
break;
|
|
347
|
+
|
|
348
|
+
case 'btw_done':
|
|
349
|
+
await forwardToClients(agentId, msg.conversationId, {
|
|
350
|
+
type: 'btw_done',
|
|
351
|
+
conversationId: msg.conversationId
|
|
352
|
+
});
|
|
353
|
+
break;
|
|
354
|
+
|
|
355
|
+
case 'btw_error':
|
|
356
|
+
await forwardToClients(agentId, msg.conversationId, {
|
|
357
|
+
type: 'btw_error',
|
|
358
|
+
conversationId: msg.conversationId,
|
|
359
|
+
error: msg.error
|
|
360
|
+
});
|
|
361
|
+
break;
|
|
362
|
+
|
|
363
|
+
case 'pong_session': {
|
|
364
|
+
// Forward pong to specific client (if clientId provided) or broadcast
|
|
365
|
+
if (msg.clientId) {
|
|
366
|
+
const targetClient = webClients.get(msg.clientId);
|
|
367
|
+
if (targetClient) {
|
|
368
|
+
await sendToWebClient(targetClient, {
|
|
369
|
+
type: 'pong_session',
|
|
370
|
+
conversationId: msg.conversationId,
|
|
371
|
+
status: msg.status,
|
|
372
|
+
isProcessing: msg.isProcessing,
|
|
373
|
+
currentTool: msg.currentTool
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
} else {
|
|
377
|
+
await forwardToClients(agentId, msg.conversationId, {
|
|
378
|
+
type: 'pong_session',
|
|
379
|
+
conversationId: msg.conversationId,
|
|
380
|
+
status: msg.status,
|
|
381
|
+
isProcessing: msg.isProcessing,
|
|
382
|
+
currentTool: msg.currentTool
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
case 'chat_image': {
|
|
389
|
+
// Image from Claude response (tool screenshots, etc.) — persisted on agent side,
|
|
390
|
+
// cached on server in previewFiles for web client serving via /api/preview/:fileId
|
|
391
|
+
const dataSize = msg.data ? Buffer.byteLength(msg.data, 'base64') : 0;
|
|
392
|
+
if (dataSize > 10 * 1024 * 1024) {
|
|
393
|
+
console.warn(`[Server] Chat image too large: ${dataSize} bytes, skipping`);
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
const fileId = randomUUID();
|
|
397
|
+
const token = randomUUID();
|
|
398
|
+
previewFiles.set(fileId, {
|
|
399
|
+
buffer: Buffer.from(msg.data, 'base64'),
|
|
400
|
+
mimeType: msg.mimeType,
|
|
401
|
+
filename: msg.filename || `chat-${Date.now()}.png`,
|
|
402
|
+
createdAt: Date.now(),
|
|
403
|
+
token
|
|
404
|
+
});
|
|
405
|
+
await forwardToClients(agentId, msg.conversationId, {
|
|
406
|
+
type: 'chat_image',
|
|
407
|
+
conversationId: msg.conversationId,
|
|
408
|
+
fileId,
|
|
409
|
+
previewToken: token,
|
|
410
|
+
mimeType: msg.mimeType
|
|
411
|
+
});
|
|
412
|
+
console.log(`[Server] Cached chat image: fileId=${fileId}, conv=${msg.conversationId}, mime=${msg.mimeType}`);
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
case 'yeaft_asset_put': {
|
|
417
|
+
let ok = false;
|
|
418
|
+
let stored = false;
|
|
419
|
+
let error = null;
|
|
420
|
+
try {
|
|
421
|
+
if (!agent.ownerId || !msg.sessionId || !msg.image?.previewData?.data) {
|
|
422
|
+
throw new Error('Incomplete Yeaft asset payload');
|
|
423
|
+
}
|
|
424
|
+
const image = yeaftAssetStore.put({
|
|
425
|
+
ownerId: agent.ownerId, agentId, sessionId: msg.sessionId,
|
|
426
|
+
assetId: msg.metadata?.assetId || msg.image.assetId, data: msg.image.previewData.data,
|
|
427
|
+
mimeType: msg.metadata?.mimeType || msg.image.mimeType || msg.image.previewData.mimeType,
|
|
428
|
+
filename: msg.metadata?.filename || msg.image.filename || msg.image.previewData.filename,
|
|
429
|
+
width: msg.metadata?.width ?? msg.image.width,
|
|
430
|
+
height: msg.metadata?.height ?? msg.image.height,
|
|
431
|
+
turnId: msg.turnId || null,
|
|
432
|
+
vpId: msg.vpId || null,
|
|
433
|
+
threadId: msg.threadId || null,
|
|
434
|
+
});
|
|
435
|
+
stored = true;
|
|
436
|
+
await forwardToClients(agentId, msg.conversationId, {
|
|
437
|
+
type: 'yeaft_asset_ready', conversationId: msg.conversationId,
|
|
438
|
+
agentId, sessionId: msg.sessionId,
|
|
439
|
+
...(msg.vpId ? { vpId: msg.vpId } : {}),
|
|
440
|
+
...(msg.turnId ? { turnId: msg.turnId } : {}),
|
|
441
|
+
...(msg.threadId ? { threadId: msg.threadId } : {}),
|
|
442
|
+
image, _requestUserId: agent.ownerId,
|
|
443
|
+
});
|
|
444
|
+
ok = true;
|
|
445
|
+
} catch (err) {
|
|
446
|
+
error = err?.message || String(err);
|
|
447
|
+
console.warn(`[Server] Failed to store Yeaft asset: ${error}`);
|
|
448
|
+
}
|
|
449
|
+
if (msg.deliveryId) {
|
|
450
|
+
await sendToAgent(agent, {
|
|
451
|
+
type: 'yeaft_asset_ack',
|
|
452
|
+
deliveryId: msg.deliveryId,
|
|
453
|
+
ok: ok || stored,
|
|
454
|
+
...(!stored && /quota exceeded|invalid|unsupported|MIME|must be between|does not match|incomplete/i.test(error || '') ? { permanent: true } : {}),
|
|
455
|
+
...(error ? { error } : {}),
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
case 'yeaft_output':
|
|
462
|
+
case 'yeaft_session_output':
|
|
463
|
+
case 'session_output': {
|
|
464
|
+
const data = hydrateInlinePreviewData(msg.data);
|
|
465
|
+
if (msg.event?.type === 'yeaft_status') {
|
|
466
|
+
agent.yeaftStatus = msg.event;
|
|
467
|
+
await broadcastAgentList();
|
|
468
|
+
}
|
|
469
|
+
if (msg.perfTraceId) {
|
|
470
|
+
recordPerfTraceEvent({
|
|
471
|
+
traceId: msg.perfTraceId,
|
|
472
|
+
source: 'server',
|
|
473
|
+
phase: 'relay.agent_output_received',
|
|
474
|
+
at: Date.now(),
|
|
475
|
+
userId: agent.ownerId || null,
|
|
476
|
+
agentId,
|
|
477
|
+
sessionId: msg.sessionId || null,
|
|
478
|
+
vpId: msg.vpId || null,
|
|
479
|
+
turnId: msg.turnId || null,
|
|
480
|
+
threadId: msg.threadId || null,
|
|
481
|
+
messageType: data?.type || msg.event?.type || msg.type,
|
|
482
|
+
bytes: Buffer.byteLength(JSON.stringify(msg)),
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
// Forward Yeaft Session output to all authenticated clients of this agent's owner.
|
|
486
|
+
// Payload carries { conversationId, data } (assistant output frame) or { event } (metadata).
|
|
487
|
+
//
|
|
488
|
+
// Compatibility: accept neutral aliases from newer agents, but always
|
|
489
|
+
// relay the legacy `yeaft_output` envelope so older web bundles continue
|
|
490
|
+
// to render during rolling upgrades.
|
|
491
|
+
//
|
|
492
|
+
// Envelope passthrough — every field the agent stamped on the envelope
|
|
493
|
+
// (groupId, vpId, turnId) MUST be forwarded verbatim. The
|
|
494
|
+
// frontend uses vpId + turnId in `handleYeaftOutput` to set
|
|
495
|
+
// `_currentYeaftVpId` / `_currentYeaftTurnId`, which in turn drive
|
|
496
|
+
// `stampSpeakerOnVpMessage` so streaming assistant deltas get a
|
|
497
|
+
// `speakerVpId`. Without that, MessageList falls through from
|
|
498
|
+
// VpTurnBlock (with avatar) to a plain AssistantTurn (no avatar) —
|
|
499
|
+
// the bug v0.1.756 fixed. Keep this spread in sync with the agent
|
|
500
|
+
// side `sendSessionOutputFrame` / `sendSessionEvent` in agent/yeaft/web-bridge.js.
|
|
501
|
+
//
|
|
502
|
+
// (2026-05-13: `featureId` field dropped from the envelope along
|
|
503
|
+
// with the rest of the Feature system.)
|
|
504
|
+
//
|
|
505
|
+
// Forward semantics: `!= null` (not truthy) — a relay should
|
|
506
|
+
// pass through whatever was stamped, including empty strings. IDs
|
|
507
|
+
// are non-empty in practice, but we don't want the relay silently
|
|
508
|
+
// eating a legitimate "" or 0 if one ever shows up.
|
|
509
|
+
for (const [cId, c] of webClients) {
|
|
510
|
+
if (c.authenticated && (CONFIG.skipAuth || c.userId === agent.ownerId)) {
|
|
511
|
+
await sendToWebClient(c, {
|
|
512
|
+
type: 'yeaft_output',
|
|
513
|
+
conversationId: msg.conversationId,
|
|
514
|
+
...(msg.perfTraceId != null ? { perfTraceId: msg.perfTraceId } : {}),
|
|
515
|
+
// Stamp the source agent so the web sessions store can keep
|
|
516
|
+
// per-agent rosters (cross-agent listing in the unified
|
|
517
|
+
// sidebar). Older web bundles ignore the extra field.
|
|
518
|
+
agentId: agentId,
|
|
519
|
+
...(msg.sessionId != null ? { sessionId: msg.sessionId } : {}),
|
|
520
|
+
...(msg.vpId != null ? { vpId: msg.vpId } : {}),
|
|
521
|
+
...(msg.turnId != null ? { turnId: msg.turnId } : {}),
|
|
522
|
+
...(msg.threadId != null ? { threadId: msg.threadId } : {}),
|
|
523
|
+
data,
|
|
524
|
+
event: msg.event,
|
|
525
|
+
});
|
|
526
|
+
if (msg.perfTraceId) {
|
|
527
|
+
recordPerfTraceEvent({
|
|
528
|
+
traceId: msg.perfTraceId,
|
|
529
|
+
source: 'server',
|
|
530
|
+
phase: 'relay.forward_to_web',
|
|
531
|
+
at: Date.now(),
|
|
532
|
+
userId: c.userId || null,
|
|
533
|
+
agentId,
|
|
534
|
+
sessionId: msg.sessionId || null,
|
|
535
|
+
vpId: msg.vpId || null,
|
|
536
|
+
turnId: msg.turnId || null,
|
|
537
|
+
threadId: msg.threadId || null,
|
|
538
|
+
messageType: data?.type || msg.event?.type || msg.type,
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
case 'yeaft_history_search_result': {
|
|
547
|
+
const targetClient = msg._requestClientId ? webClients.get(msg._requestClientId) : null;
|
|
548
|
+
if (targetClient?.authenticated && (CONFIG.skipAuth || targetClient.userId === agent.ownerId)) {
|
|
549
|
+
await sendToWebClient(targetClient, {
|
|
550
|
+
type: 'yeaft_history_search_result',
|
|
551
|
+
agentId,
|
|
552
|
+
requestId: msg.requestId ?? null,
|
|
553
|
+
sessionId: msg.sessionId ?? null,
|
|
554
|
+
query: msg.query || '',
|
|
555
|
+
results: Array.isArray(msg.results) ? msg.results : [],
|
|
556
|
+
hasMore: !!msg.hasMore,
|
|
557
|
+
nextBeforeSeq: msg.nextBeforeSeq ?? null,
|
|
558
|
+
...(msg.error ? { error: msg.error } : {}),
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
break;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
case 'yeaft_history_window': {
|
|
565
|
+
const targetClient = msg._requestClientId ? webClients.get(msg._requestClientId) : null;
|
|
566
|
+
if (targetClient?.authenticated && (CONFIG.skipAuth || targetClient.userId === agent.ownerId)) {
|
|
567
|
+
const messages = Array.isArray(msg.messages) ? projectConfirmedAssetImages(
|
|
568
|
+
msg.messages.map(hydrateMessagePreviewData),
|
|
569
|
+
{ ownerId: agent.ownerId, agentId, sessionId: msg.sessionId },
|
|
570
|
+
) : [];
|
|
571
|
+
await sendToWebClient(targetClient, {
|
|
572
|
+
type: 'yeaft_history_window',
|
|
573
|
+
agentId,
|
|
574
|
+
requestId: msg.requestId ?? null,
|
|
575
|
+
conversationId: msg.conversationId ?? null,
|
|
576
|
+
sessionId: msg.sessionId ?? null,
|
|
577
|
+
anchorMessageId: msg.anchorMessageId ?? null,
|
|
578
|
+
anchorSeq: msg.anchorSeq ?? null,
|
|
579
|
+
messages,
|
|
580
|
+
oldestSeq: msg.oldestSeq ?? null,
|
|
581
|
+
hasMoreBefore: !!msg.hasMoreBefore,
|
|
582
|
+
...(msg.error ? { error: msg.error } : {}),
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
case 'yeaft_history_chunk': {
|
|
589
|
+
const messages = Array.isArray(msg.messages)
|
|
590
|
+
? projectConfirmedAssetImages(
|
|
591
|
+
msg.messages.map(hydrateMessagePreviewData),
|
|
592
|
+
{ ownerId: agent.ownerId, agentId, sessionId: msg.sessionId },
|
|
593
|
+
)
|
|
594
|
+
: [];
|
|
595
|
+
// Forward a "load older messages" pagination chunk to the same
|
|
596
|
+
// authenticated clients. Distinct from `yeaft_output` because the
|
|
597
|
+
// frontend needs to PREPEND these older messages above the current
|
|
598
|
+
// history (whereas yeaft_output flows through an append pipeline).
|
|
599
|
+
if (msg.perfTraceId) {
|
|
600
|
+
recordPerfTraceEvent({
|
|
601
|
+
traceId: msg.perfTraceId,
|
|
602
|
+
source: 'server',
|
|
603
|
+
phase: 'relay.agent_output_received',
|
|
604
|
+
at: Date.now(),
|
|
605
|
+
userId: agent.ownerId || null,
|
|
606
|
+
agentId,
|
|
607
|
+
sessionId: msg.sessionId || null,
|
|
608
|
+
messageType: msg.type,
|
|
609
|
+
bytes: Buffer.byteLength(JSON.stringify(msg)),
|
|
610
|
+
detail: { mode: msg.mode || 'older', count: messages.length },
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
for (const [cId, c] of webClients) {
|
|
614
|
+
if (c.authenticated && (CONFIG.skipAuth || c.userId === agent.ownerId)) {
|
|
615
|
+
await sendToWebClient(c, {
|
|
616
|
+
type: 'yeaft_history_chunk',
|
|
617
|
+
conversationId: msg.conversationId,
|
|
618
|
+
...(msg.perfTraceId != null ? { perfTraceId: msg.perfTraceId } : {}),
|
|
619
|
+
...(msg.sessionId != null ? { sessionId: msg.sessionId } : {}),
|
|
620
|
+
messages,
|
|
621
|
+
mode: msg.mode || 'older',
|
|
622
|
+
oldestSeq: msg.oldestSeq ?? null,
|
|
623
|
+
hasMore: !!msg.hasMore,
|
|
624
|
+
latestSeq: msg.latestSeq ?? null,
|
|
625
|
+
afterSeq: msg.afterSeq ?? null,
|
|
626
|
+
turns: msg.turns ?? null,
|
|
627
|
+
});
|
|
628
|
+
if (msg.perfTraceId) {
|
|
629
|
+
recordPerfTraceEvent({
|
|
630
|
+
traceId: msg.perfTraceId,
|
|
631
|
+
source: 'server',
|
|
632
|
+
phase: 'relay.forward_to_web',
|
|
633
|
+
at: Date.now(),
|
|
634
|
+
userId: c.userId || null,
|
|
635
|
+
agentId,
|
|
636
|
+
sessionId: msg.sessionId || null,
|
|
637
|
+
messageType: msg.type,
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
break;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
case 'yeaft_dream_status':
|
|
646
|
+
case 'yeaft_dream_result':
|
|
647
|
+
// Forward Dream lifecycle envelopes to the web client.
|
|
648
|
+
//
|
|
649
|
+
// The bug this fixes: `handleYeaftDreamTrigger` in
|
|
650
|
+
// `agent/yeaft/web-bridge.js` emits these as BARE top-level
|
|
651
|
+
// messages when the user clicks "Run dream now" (in the topbar or
|
|
652
|
+
// group settings). Without a case here, the switch hit
|
|
653
|
+
// `default: return false` and silently dropped the message,
|
|
654
|
+
// leaving the UI stuck on "Running…" even after the dream pass
|
|
655
|
+
// completed.
|
|
656
|
+
//
|
|
657
|
+
// Whitelist spread matches the pattern used by sibling cases
|
|
658
|
+
// (`yeaft_output`, `yeaft_history_chunk`) so a future agent-side
|
|
659
|
+
// bug that tags an internal field onto a dream envelope can't
|
|
660
|
+
// leak it to the web client. Keep in sync with the agent emit at
|
|
661
|
+
// `handleYeaftDreamTrigger`.
|
|
662
|
+
for (const [, c] of webClients) {
|
|
663
|
+
if (c.authenticated && (CONFIG.skipAuth || c.userId === agent.ownerId)) {
|
|
664
|
+
await sendToWebClient(c, {
|
|
665
|
+
type: msg.type,
|
|
666
|
+
...(msg.sessionId != null ? { sessionId: msg.sessionId } : {}),
|
|
667
|
+
...(msg.vpId != null ? { vpId: msg.vpId } : {}),
|
|
668
|
+
...(msg.status != null ? { status: msg.status } : {}),
|
|
669
|
+
...(typeof msg.success === 'boolean' ? { success: msg.success } : {}),
|
|
670
|
+
...(typeof msg.entriesCreated === 'number' ? { entriesCreated: msg.entriesCreated } : {}),
|
|
671
|
+
...(msg.lastDreamAt != null ? { lastDreamAt: msg.lastDreamAt } : {}),
|
|
672
|
+
...(msg.skipped ? { skipped: true } : {}),
|
|
673
|
+
...(msg.error != null ? { error: msg.error } : {}),
|
|
674
|
+
...(Array.isArray(msg.groups) ? { groups: msg.groups } : {}),
|
|
675
|
+
...(Array.isArray(msg.targets) ? { targets: msg.targets } : {}),
|
|
676
|
+
...(msg.startedAt != null ? { startedAt: msg.startedAt } : {}),
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
break;
|
|
681
|
+
|
|
682
|
+
case 'yeaft_tool_stats':
|
|
683
|
+
// 2026-05-13: relay tool-call counters from the agent to the web
|
|
684
|
+
// client that requested them. Whitelist `snapshot`/`registered`/
|
|
685
|
+
// `unused` so a future schema bump can't leak unrelated fields,
|
|
686
|
+
// and tolerate a `error` field for graceful failure replies.
|
|
687
|
+
for (const [, c] of webClients) {
|
|
688
|
+
if (c.authenticated && (CONFIG.skipAuth || c.userId === agent.ownerId)) {
|
|
689
|
+
await sendToWebClient(c, {
|
|
690
|
+
type: 'yeaft_tool_stats',
|
|
691
|
+
...(msg.snapshot && typeof msg.snapshot === 'object' ? { snapshot: msg.snapshot } : { snapshot: {} }),
|
|
692
|
+
...(Array.isArray(msg.registered) ? { registered: msg.registered } : { registered: [] }),
|
|
693
|
+
...(Array.isArray(msg.unused) ? { unused: msg.unused } : { unused: [] }),
|
|
694
|
+
...(msg.error != null ? { error: msg.error } : {}),
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
break;
|
|
699
|
+
|
|
700
|
+
case 'session_list_updated': {
|
|
701
|
+
// Server-side persistence for yeaft sessions. Mirror the agent's
|
|
702
|
+
// authoritative snapshot into the `yeaft_sessions` table so the
|
|
703
|
+
// unified sidebar can show this user's yeaft sessions across all
|
|
704
|
+
// their agents (online or not) and survive reload. Pure shadow
|
|
705
|
+
// store — agent's on-disk `~/.yeaft/sessions/` remains canonical.
|
|
706
|
+
// Forwarding to the web continues unchanged below.
|
|
707
|
+
try {
|
|
708
|
+
const rows = Array.isArray(msg.sessions) ? msg.sessions : [];
|
|
709
|
+
if (agent.ownerId) {
|
|
710
|
+
yeaftSessionDb.reconcileFromSnapshot(agent.ownerId, agentId, rows);
|
|
711
|
+
}
|
|
712
|
+
} catch (e) {
|
|
713
|
+
console.warn(`[Server] yeaft session persist failed for agent ${agentId}:`, e?.message || e);
|
|
714
|
+
}
|
|
715
|
+
// fix-yeaft-session-list-and-menu: decorate the outgoing snapshot
|
|
716
|
+
// with the server-side pin state. The agent has no notion of
|
|
717
|
+
// pinning (pin is UI metadata that lives in the `yeaft_sessions`
|
|
718
|
+
// table); the web sessions store reads `pinned: true` off each
|
|
719
|
+
// row in applySnapshot and mirrors it into chatStore.pinnedSessions
|
|
720
|
+
// so chat + yeaft share a single pin registry for sort logic.
|
|
721
|
+
//
|
|
722
|
+
// Use one batch read (`getByAgent`) and a Set lookup instead of an
|
|
723
|
+
// N+1 `get(id)` per row — relays this size hot path on every
|
|
724
|
+
// snapshot per connected client.
|
|
725
|
+
const decoratedSessions = decorateYeaftSessionsWithPinned(agentId, msg.sessions);
|
|
726
|
+
// Relay verbatim to web (agentId stamped so the web sessions store
|
|
727
|
+
// can merge per-agent rosters).
|
|
728
|
+
for (const [, c] of webClients) {
|
|
729
|
+
if (c.authenticated && (CONFIG.skipAuth || c.userId === agent.ownerId)) {
|
|
730
|
+
await sendToWebClient(c, {
|
|
731
|
+
type: msg.type,
|
|
732
|
+
agentId: agentId,
|
|
733
|
+
sessions: decoratedSessions,
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
break;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
case 'session_roster_changed': {
|
|
741
|
+
// Delta event — update only the affected session row. Same as
|
|
742
|
+
// above: keep the DB shadow + forward to web.
|
|
743
|
+
try {
|
|
744
|
+
if (agent.ownerId && msg) {
|
|
745
|
+
const sessionId = msg.sessionId;
|
|
746
|
+
if (sessionId) {
|
|
747
|
+
const existing = yeaftSessionDb.getForAgent(agent.ownerId, agentId, sessionId);
|
|
748
|
+
// Roster-delta path is a cache update, not authoritative
|
|
749
|
+
// creation. If we've never seen this row before, skip and
|
|
750
|
+
// wait for the next full snapshot (which carries truthful
|
|
751
|
+
// createdAt / workDir / config).
|
|
752
|
+
if (!existing) break;
|
|
753
|
+
const merged = {
|
|
754
|
+
id: sessionId,
|
|
755
|
+
name: msg.name != null ? msg.name : (existing?.name || sessionId),
|
|
756
|
+
roster: Array.isArray(msg.roster)
|
|
757
|
+
? msg.roster
|
|
758
|
+
: (existing?.roster || []),
|
|
759
|
+
defaultVpId: msg.defaultVpId != null
|
|
760
|
+
? msg.defaultVpId
|
|
761
|
+
: (existing?.defaultVpId || null),
|
|
762
|
+
workDir: existing?.workDir || '',
|
|
763
|
+
config: existing?.config || {},
|
|
764
|
+
announcement: typeof msg.announcement === 'string'
|
|
765
|
+
? msg.announcement
|
|
766
|
+
: (existing?.announcement || ''),
|
|
767
|
+
createdAt: existing?.createdAt || Date.now(),
|
|
768
|
+
};
|
|
769
|
+
yeaftSessionDb.upsertFromSnapshot(agent.ownerId, agentId, merged);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
} catch (e) {
|
|
773
|
+
console.warn(`[Server] yeaft roster persist failed for agent ${agentId}:`, e?.message || e);
|
|
774
|
+
}
|
|
775
|
+
for (const [, c] of webClients) {
|
|
776
|
+
if (c.authenticated && (CONFIG.skipAuth || c.userId === agent.ownerId)) {
|
|
777
|
+
await sendToWebClient(c, { ...msg, agentId: agentId });
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
break;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
case 'session_crud_result': {
|
|
784
|
+
// Surface op result to the web (the only client that cares about
|
|
785
|
+
// requestId routing). Keep the server shadow table in sync for list
|
|
786
|
+
// probes too: entering Yeaft asks each connected agent for its opened
|
|
787
|
+
// sessions, and that response must carry persisted server-side pin
|
|
788
|
+
// state before it hits the web store.
|
|
789
|
+
let outboundMsg = msg;
|
|
790
|
+
try {
|
|
791
|
+
const op = msg.op;
|
|
792
|
+
const sessionId = msg.sessionId;
|
|
793
|
+
if (msg.ok && op === 'list' && Array.isArray(msg.sessions)) {
|
|
794
|
+
if (agent.ownerId) yeaftSessionDb.reconcileFromSnapshot(agent.ownerId, agentId, msg.sessions);
|
|
795
|
+
outboundMsg = { ...msg, sessions: decorateYeaftSessionsWithPinned(agentId, msg.sessions) };
|
|
796
|
+
} else if (msg.ok && sessionId && (op === 'delete' || op === 'archive')) {
|
|
797
|
+
if (op === 'archive') {
|
|
798
|
+
yeaftSessionDb.setArchivedForAgent(agent.ownerId, agentId, sessionId, true);
|
|
799
|
+
} else {
|
|
800
|
+
try {
|
|
801
|
+
yeaftSessionDb.deleteForAgent(agent.ownerId, agentId, sessionId);
|
|
802
|
+
} catch (e) {
|
|
803
|
+
console.warn('[Server] Yeaft Session metadata cleanup failed:', e?.message || e);
|
|
804
|
+
}
|
|
805
|
+
try {
|
|
806
|
+
yeaftAssetStore.deleteScope({ ownerId: agent.ownerId, agentId, sessionId });
|
|
807
|
+
} catch (e) {
|
|
808
|
+
console.warn('[Server] Yeaft asset cleanup failed:', e?.message || e);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
} catch (e) {
|
|
813
|
+
console.warn(`[Server] yeaft crud-result persist failed:`, e?.message || e);
|
|
814
|
+
}
|
|
815
|
+
for (const [, c] of webClients) {
|
|
816
|
+
if (c.authenticated && (CONFIG.skipAuth || c.userId === agent.ownerId)) {
|
|
817
|
+
await sendToWebClient(c, { ...outboundMsg, agentId: agentId });
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
break;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
case 'yeaft_debug_history':
|
|
824
|
+
// Relay the file-backed debug trace snapshot from the agent to the web
|
|
825
|
+
// client that requested it via `yeaft_fetch_debug_history`. Keep the
|
|
826
|
+
// paging/detail flags intact; otherwise the store cannot distinguish a
|
|
827
|
+
// bounded index refresh from a single-request detail hydration.
|
|
828
|
+
for (const [, c] of webClients) {
|
|
829
|
+
if (c.authenticated && (CONFIG.skipAuth || c.userId === agent.ownerId)) {
|
|
830
|
+
await sendToWebClient(c, {
|
|
831
|
+
type: 'yeaft_debug_history',
|
|
832
|
+
loops: Array.isArray(msg.loops) ? msg.loops : [],
|
|
833
|
+
turns: Array.isArray(msg.turns) ? msg.turns : [],
|
|
834
|
+
dreamEvents: Array.isArray(msg.dreamEvents) ? msg.dreamEvents : [],
|
|
835
|
+
...(msg.sessionId != null ? { sessionId: msg.sessionId } : {}),
|
|
836
|
+
...(msg.threadId != null ? { threadId: msg.threadId } : {}),
|
|
837
|
+
...(msg.requestId != null ? { requestId: msg.requestId } : {}),
|
|
838
|
+
...(msg.requestKind != null ? { requestKind: msg.requestKind } : {}),
|
|
839
|
+
...(msg.search != null ? { search: msg.search } : {}),
|
|
840
|
+
...(msg.hasMore != null ? { hasMore: !!msg.hasMore } : {}),
|
|
841
|
+
...(msg.limit != null ? { limit: msg.limit } : {}),
|
|
842
|
+
...(msg.indexOnly != null ? { indexOnly: !!msg.indexOnly } : {}),
|
|
843
|
+
...(msg.detailTurnId != null ? { detailTurnId: msg.detailTurnId } : {}),
|
|
844
|
+
...(msg.error != null ? { error: msg.error } : {}),
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
break;
|
|
849
|
+
|
|
850
|
+
default:
|
|
851
|
+
return false; // Not handled
|
|
852
|
+
}
|
|
853
|
+
return true; // Handled
|
|
854
|
+
}
|