@yeaft/webchat-agent 1.0.190 → 1.0.192

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.
@@ -24,7 +24,7 @@ export function connect() {
24
24
  console.log(`Disallowed tools: ${ctx.CONFIG.disallowedTools.join(', ')}`);
25
25
  }
26
26
 
27
- ctx.ws = new WebSocket(url, {
27
+ const socket = new WebSocket(url, {
28
28
  // Match server's permessage-deflate config (bounded memory,
29
29
  // skip compression for small frames). The `ws` library handles
30
30
  // streaming compression so we no longer need the synchronous
@@ -35,15 +35,18 @@ export function connect() {
35
35
  threshold: 1024
36
36
  }
37
37
  });
38
+ ctx.ws = socket;
38
39
 
39
- ctx.ws.on('open', () => {
40
+ socket.on('open', () => {
41
+ if (socket !== ctx.ws) return;
40
42
  console.log('Connected to server, waiting for auth challenge...');
41
43
  clearTimeout(ctx.reconnectTimer);
42
44
  // 启动 agent 端心跳: 每 25 秒发一次 ping 帧
43
45
  startAgentHeartbeat();
44
46
  });
45
47
 
46
- ctx.ws.on('message', async (data) => {
48
+ socket.on('message', async (data) => {
49
+ if (socket !== ctx.ws) return;
47
50
  // 收到任何消息都说明连接活着
48
51
  ctx.lastPongAt = Date.now();
49
52
 
@@ -54,7 +57,7 @@ export function connect() {
54
57
  console.log('Received auth challenge, sending credentials...');
55
58
  ctx.pendingAuthTempId = msg.tempId;
56
59
  // Send authentication via WebSocket (not URL)
57
- ctx.ws.send(JSON.stringify({
60
+ socket.send(JSON.stringify({
58
61
  type: 'auth',
59
62
  tempId: msg.tempId,
60
63
  secret: ctx.CONFIG.agentSecret,
@@ -68,6 +71,9 @@ export function connect() {
68
71
  }
69
72
 
70
73
  const msg = await parseMessage(data);
74
+ // Decryption can yield after a reconnect replaces this socket. Fence again
75
+ // before a stale command can mutate Agent-local config or runtime state.
76
+ if (socket !== ctx.ws) return;
71
77
  if (msg) {
72
78
  handleMessage(msg).catch(err => {
73
79
  console.error('[WS] handleMessage error:', err.message || err);
@@ -75,7 +81,8 @@ export function connect() {
75
81
  }
76
82
  });
77
83
 
78
- ctx.ws.on('close', (code, reason) => {
84
+ socket.on('close', (code, reason) => {
85
+ if (socket !== ctx.ws) return;
79
86
  console.log(`Disconnected from server: ${code} ${reason}`);
80
87
  ctx.sessionKey = null;
81
88
  ctx.pendingAuthTempId = null;
@@ -89,7 +96,7 @@ export function connect() {
89
96
  scheduleReconnect(connect);
90
97
  });
91
98
 
92
- ctx.ws.on('error', (err) => {
99
+ socket.on('error', (err) => {
93
100
  console.error('WebSocket error:', err.message);
94
101
  });
95
102
  }
@@ -27,9 +27,10 @@ import { sendAgentMetricsSnapshot } from '../metrics.js';
27
27
  import { handleRestartAgent, handleUpgradeAgent } from './upgrade.js';
28
28
  import { loadMcpServers, updateMcpConfig } from '../mcp.js';
29
29
  import { getLlmConfig, updateLlmConfig, getYeaftSettings, updateYeaftSettings, getSearchSettings, updateSearchSettings, fetchTavilyUsage } from '../yeaft/config-api.js';
30
+ import { loadConfig } from '../yeaft/config.js';
30
31
  import { discoverLlmModels } from '../llm-model-discovery.js';
31
32
  import { fetchModelsDev } from '../yeaft/llm/models-dev.js';
32
- import { handleYeaftSessionSend, handleYeaftAskUserAnswer, handleYeaftSubAgentPrompt, handleYeaftTaskCancel, handleYeaftModeSwitch, handleYeaftModelSwitch, resetYeaftSession, handleYeaftLoadHistory, handleYeaftSearchHistory, handleYeaftLoadHistoryWindow, handleYeaftLoadMoreHistory, handleYeaftAbortThread, handleYeaftAbortAll, handleYeaftAbortTurn, handleYeaftVpSubscribe, handleYeaftVpCreate, handleYeaftVpUpdate, handleYeaftVpDelete, handleYeaftVpRead, handleYeaftListSessions, handleYeaftCreateSession, handleYeaftRenameSession, handleYeaftUpdateSession, handleYeaftUpdateSessionConfig, handleYeaftArchiveSession, handleYeaftDeleteSession, handleYeaftSessionAddMember, handleYeaftSessionRemoveMember, handleYeaftSessionSetDefaultVp, handleYeaftScanWorkdirSessions, handleYeaftRestoreSession, handleYeaftDreamTrigger, handleYeaftFetchToolStats, handleYeaftFetchDebugHistory, handleYeaftMcpList, handleYeaftMcpAdd, handleYeaftMcpRemove, handleYeaftMcpReload, broadcastLanguageChange, broadcastYeaftSessionSnapshotEager, preloadYeaftSkillSlashCommands } from '../yeaft/web-bridge.js';
33
+ import { handleYeaftSessionSend, handleYeaftAskUserAnswer, handleYeaftSubAgentPrompt, handleYeaftTaskCancel, handleYeaftModeSwitch, handleYeaftModelSwitch, resetYeaftSession, refreshLiveSessionConfig, handleYeaftLoadHistory, handleYeaftSearchHistory, handleYeaftLoadHistoryWindow, handleYeaftLoadMoreHistory, handleYeaftAbortThread, handleYeaftAbortAll, handleYeaftAbortTurn, handleYeaftVpSubscribe, handleYeaftVpCreate, handleYeaftVpUpdate, handleYeaftVpDelete, handleYeaftVpRead, handleYeaftListSessions, handleYeaftCreateSession, handleYeaftRenameSession, handleYeaftUpdateSession, handleYeaftUpdateSessionConfig, handleYeaftArchiveSession, handleYeaftDeleteSession, handleYeaftSessionAddMember, handleYeaftSessionRemoveMember, handleYeaftSessionSetDefaultVp, handleYeaftScanWorkdirSessions, handleYeaftRestoreSession, handleYeaftDreamTrigger, handleYeaftFetchToolStats, handleYeaftFetchDebugHistory, handleYeaftMcpList, handleYeaftMcpAdd, handleYeaftMcpRemove, handleYeaftMcpReload, broadcastLanguageChange, broadcastYeaftSessionSnapshotEager, preloadYeaftSkillSlashCommands } from '../yeaft/web-bridge.js';
33
34
  import { startYeaftStatusRefresh, forceRefreshYeaftStatus } from '../yeaft/status-cache.js';
34
35
  import { handleWorkCenterRequest } from '../yeaft/work-center/bridge.js';
35
36
 
@@ -37,25 +38,43 @@ export async function applyLlmConfigUpdate(msg, dependencies = {}) {
37
38
  const updateConfig = dependencies.updateLlmConfig || updateLlmConfig;
38
39
  const broadcastLanguage = dependencies.broadcastLanguageChange || broadcastLanguageChange;
39
40
  const forceStatusRefresh = dependencies.forceRefreshYeaftStatus || forceRefreshYeaftStatus;
41
+ const refreshRuntimeConfig = dependencies.refreshLiveSessionConfig || refreshLiveSessionConfig;
42
+ const readConfig = dependencies.loadConfig || loadConfig;
40
43
  const send = dependencies.sendToServer || sendToServer;
41
44
  const yeaftDir = dependencies.yeaftDir ?? ctx.CONFIG?.yeaftDir;
42
45
  const incomingLanguage = typeof msg.config?.language === 'string' && msg.config.language
43
46
  ? msg.config.language
44
47
  : null;
48
+ let previousDefaultModel = null;
49
+ try {
50
+ const previousConfig = readConfig({ dir: yeaftDir });
51
+ previousDefaultModel = previousConfig?.primaryModel || previousConfig?.model || null;
52
+ } catch { /* updateLlmConfig will report the actual config error */ }
45
53
  const result = updateConfig(msg.config || {}, yeaftDir);
46
54
  if (!result.error && incomingLanguage) broadcastLanguage(result.language);
47
55
 
48
56
  let statusRefreshError = null;
49
57
  if (!result.error) {
50
58
  try {
51
- const statusEvent = await forceStatusRefresh({ reason: 'llm_config_updated' });
52
- statusRefreshError = statusEvent?.refreshError || null;
59
+ await refreshRuntimeConfig({ previousDefaultModel });
53
60
  } catch (err) {
54
61
  statusRefreshError = err?.message || String(err);
55
62
  }
63
+ try {
64
+ const statusEvent = await forceStatusRefresh({ reason: 'llm_config_updated' });
65
+ statusRefreshError = statusRefreshError || statusEvent?.refreshError || null;
66
+ } catch (err) {
67
+ statusRefreshError = statusRefreshError || err?.message || String(err);
68
+ }
56
69
  }
57
70
 
58
- const response = { type: 'llm_config_updated', ...result, statusRefreshError };
71
+ const response = {
72
+ type: 'llm_config_updated',
73
+ ...result,
74
+ agentId: msg.agentId ?? null,
75
+ requestId: msg.requestId ?? null,
76
+ statusRefreshError,
77
+ };
59
78
  send(response);
60
79
  return response;
61
80
  }
@@ -614,6 +614,7 @@ export async function handleAgentOutput(agentId, agent, msg) {
614
614
  if (c.authenticated && (CONFIG.skipAuth || c.userId === agent.ownerId)) {
615
615
  await sendToWebClient(c, {
616
616
  type: 'yeaft_history_chunk',
617
+ agentId,
617
618
  conversationId: msg.conversationId,
618
619
  ...(msg.perfTraceId != null ? { perfTraceId: msg.perfTraceId } : {}),
619
620
  ...(msg.sessionId != null ? { sessionId: msg.sessionId } : {}),
@@ -301,12 +301,14 @@ export async function handleAgentSync(agentId, agent, msg) {
301
301
  await sendToWebClient(client, {
302
302
  type: 'llm_config_updated',
303
303
  agentId,
304
+ requestId: msg.requestId,
304
305
  providers: msg.providers,
305
306
  primaryModel: msg.primaryModel,
306
307
  fastModel: msg.fastModel,
307
308
  language: msg.language,
308
309
  agentConfig: msg.agentConfig,
309
310
  effectiveConfig: msg.effectiveConfig,
311
+ statusRefreshError: msg.statusRefreshError,
310
312
  error: msg.error
311
313
  });
312
314
  }
@@ -140,6 +140,8 @@ export async function handleClientMisc(clientId, client, msg, checkAgentAccess)
140
140
  if (!await checkAgentAccess(llmUpdateAgentId)) break;
141
141
  await forwardToAgent(llmUpdateAgentId, {
142
142
  type: 'update_llm_config',
143
+ agentId: llmUpdateAgentId,
144
+ requestId: msg.requestId,
143
145
  config: msg.config || {}
144
146
  });
145
147
  break;
@@ -39,8 +39,8 @@ export function handleAgentConnection(ws, url) {
39
39
 
40
40
  ws.on('message', async (data) => {
41
41
  const agent = agents.get(clientAgentId);
42
- if (!agent) {
43
- console.error(`[Agent] No agent found for id: ${clientAgentId}`);
42
+ if (!agent || agent.ws !== ws) {
43
+ if (!agent) console.error(`[Agent] No agent found for id: ${clientAgentId}`);
44
44
  return;
45
45
  }
46
46
  markAgentHeartbeatSeen(agent);
@@ -62,14 +62,14 @@ export function handleAgentConnection(ws, url) {
62
62
  bytes: data.length || 0,
63
63
  });
64
64
  }
65
- handleAgentMessage(clientAgentId, msg);
65
+ handleAgentMessage(clientAgentId, msg, ws);
66
66
  } else {
67
67
  console.error(`[Agent] Failed to parse message from ${clientAgentId}`);
68
68
  }
69
69
  });
70
70
 
71
71
  ws.on('close', () => {
72
- handleAgentDisconnect(clientAgentId, agentName);
72
+ handleAgentDisconnect(clientAgentId, agentName, ws);
73
73
  });
74
74
 
75
75
  ws.on('error', (err) => {
@@ -136,8 +136,8 @@ export function handleAgentConnection(ws, url) {
136
136
  // Already authenticated, handle normally
137
137
  if (!resolvedAgentId) return;
138
138
  const agent = agents.get(resolvedAgentId);
139
- if (!agent) {
140
- console.error(`[Agent] No agent found for id: ${resolvedAgentId}`);
139
+ if (!agent || agent.ws !== ws) {
140
+ if (!agent) console.error(`[Agent] No agent found for id: ${resolvedAgentId}`);
141
141
  return;
142
142
  }
143
143
  markAgentHeartbeatSeen(agent);
@@ -159,7 +159,7 @@ export function handleAgentConnection(ws, url) {
159
159
  bytes: data.length || 0,
160
160
  });
161
161
  }
162
- handleAgentMessage(resolvedAgentId, msg);
162
+ handleAgentMessage(resolvedAgentId, msg, ws);
163
163
  } else {
164
164
  console.error(`[Agent] Failed to parse message from ${resolvedAgentId}`);
165
165
  }
@@ -174,7 +174,7 @@ export function handleAgentConnection(ws, url) {
174
174
  }
175
175
  // Use resolvedAgentId if auth completed, otherwise nothing to clean
176
176
  if (resolvedAgentId) {
177
- handleAgentDisconnect(resolvedAgentId, agentName);
177
+ handleAgentDisconnect(resolvedAgentId, agentName, ws);
178
178
  }
179
179
  });
180
180
 
@@ -188,12 +188,13 @@ export function handleAgentConnection(ws, url) {
188
188
  * Conversations are persisted in DB and will be restored on reconnect via
189
189
  * get_agents (client-side recovery) and conversation_list (agent-side sync).
190
190
  */
191
- function handleAgentDisconnect(agentId, agentName) {
191
+ function handleAgentDisconnect(agentId, agentName, ws) {
192
192
  const agent = agents.get(agentId);
193
+ if (!agent || agent.ws !== ws) return;
193
194
  // Phase 4: 清理目录缓存
194
195
  clearAgentDirCache(agentId);
195
196
  // Phase 1: 清理同步超时
196
- if (agent?._syncTimeout) {
197
+ if (agent._syncTimeout) {
197
198
  clearTimeout(agent._syncTimeout);
198
199
  }
199
200
  // Remove agent entirely — eliminates zombie agents from broadcastAgentList
@@ -209,6 +210,7 @@ function completeAgentRegistration(ws, agentId, agentName, workDir, sessionKey,
209
210
  const proxyPorts = (existingAgent?.proxyPorts || []).map(p => ({ ...p, enabled: false }));
210
211
  const slashCommands = existingAgent?.slashCommands || [];
211
212
  const slashCommandDescriptions = existingAgent?.slashCommandDescriptions || {};
213
+ if (existingAgent?._syncTimeout) clearTimeout(existingAgent._syncTimeout);
212
214
 
213
215
  // 兼容旧版 agent:未上报 capabilities 时默认全部开启
214
216
  const effectiveCapabilities = capabilities.length > 0
@@ -244,7 +246,7 @@ function completeAgentRegistration(ws, agentId, agentName, workDir, sessionKey,
244
246
  // 同步超时保护:30 秒后强制 ready
245
247
  const syncTimeout = setTimeout(() => {
246
248
  const ag = agents.get(agentId);
247
- if (ag && ag.status === 'syncing') {
249
+ if (ag?.ws === ws && ag.status === 'syncing') {
248
250
  console.warn(`[Sync] Agent ${agentName} sync timeout, forcing ready`);
249
251
  ag.status = 'ready';
250
252
  broadcastAgentList();
@@ -252,10 +254,14 @@ function completeAgentRegistration(ws, agentId, agentName, workDir, sessionKey,
252
254
  }, 30000);
253
255
  agents.get(agentId)._syncTimeout = syncTimeout;
254
256
 
257
+ if (existingAgent?.ws && existingAgent.ws !== ws) {
258
+ existingAgent.ws.close(1008, 'Superseded by a newer Agent connection');
259
+ }
260
+
255
261
  // 心跳响应处理 + latency 测量
256
262
  ws.on('pong', () => {
257
263
  const agent = agents.get(agentId);
258
- if (agent) {
264
+ if (agent?.ws === ws) {
259
265
  markAgentHeartbeatSeen(agent);
260
266
  if (agent.pingSentAt) {
261
267
  agent.latency = Date.now() - agent.pingSentAt;
@@ -284,9 +290,9 @@ function completeAgentRegistration(ws, agentId, agentName, workDir, sessionKey,
284
290
  broadcastAgentList();
285
291
  }
286
292
 
287
- async function handleAgentMessage(agentId, msg) {
293
+ async function handleAgentMessage(agentId, msg, ws) {
288
294
  const agent = agents.get(agentId);
289
- if (!agent) return;
295
+ if (!agent || agent.ws !== ws) return;
290
296
 
291
297
  // Security: 需要 conversationId 的消息类型,验证该 conversation 属于此 agent.
292
298
  // The conversation-id check only authorizes Chat flows where the
@@ -1 +1 @@
1
- {"version":"1.0.190"}
1
+ {"version":"1.0.192"}