@yeaft/webchat-agent 1.0.458 → 1.0.460

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.
@@ -350,7 +350,7 @@ export async function handleMessage(msg) {
350
350
  break;
351
351
 
352
352
  case 'restart_agent':
353
- await handleRestartAgent();
353
+ await handleRestartAgent({ requestId: msg.requestId, clientId: msg.clientId });
354
354
  break;
355
355
 
356
356
  case 'set_dream_enabled': {
@@ -363,15 +363,15 @@ export async function handleMessage(msg) {
363
363
  const runtime = await ensureSessionLoaded();
364
364
  runtime.config.dream = { ...(runtime.config.dream || {}), enabled };
365
365
  runtime.dreamScheduler?.setEnabled?.(enabled);
366
- sendToServer({ type: 'dream_enabled_changed', enabled });
366
+ sendToServer({ type: 'dream_enabled_changed', enabled, requestId: msg.requestId, clientId: msg.clientId });
367
367
  } catch (error) {
368
- sendToServer({ type: 'dream_enabled_changed', enabled: !enabled, error: error?.message || String(error) });
368
+ sendToServer({ type: 'dream_enabled_changed', enabled: !enabled, error: error?.message || String(error), requestId: msg.requestId, clientId: msg.clientId });
369
369
  }
370
370
  break;
371
371
  }
372
372
 
373
373
  case 'upgrade_agent':
374
- await handleUpgradeAgent();
374
+ await handleUpgradeAgent({ requestId: msg.requestId, clientId: msg.clientId });
375
375
  break;
376
376
 
377
377
  // MCP configuration
@@ -495,7 +495,7 @@ export async function handleMessage(msg) {
495
495
  // section of config.json. This does not expose trace payloads.
496
496
  case 'get_telemetry_settings': {
497
497
  const settings = getTelemetrySettings(ctx.CONFIG?.yeaftDir);
498
- sendToServer({ type: 'telemetry_settings', ...settings });
498
+ sendToServer({ type: 'telemetry_settings', requestId: msg.requestId, clientId: msg.clientId, ...settings });
499
499
  break;
500
500
  }
501
501
 
@@ -515,7 +515,7 @@ export async function handleMessage(msg) {
515
515
  result.runtimeRefreshError = error?.message || String(error);
516
516
  }
517
517
  }
518
- sendToServer({ type: 'telemetry_settings_updated', ...result });
518
+ sendToServer({ type: 'telemetry_settings_updated', requestId: msg.requestId, clientId: msg.clientId, ...result });
519
519
  break;
520
520
  }
521
521
 
@@ -92,9 +92,9 @@ function cleanupAndExit(exitCode) {
92
92
  }, 500);
93
93
  }
94
94
 
95
- export async function handleRestartAgent() {
95
+ export async function handleRestartAgent(correlation = {}) {
96
96
  console.log('[Agent] Restart requested, shutting down for PM2/systemd restart...');
97
- await sendToServer({ type: 'restart_agent_ack' });
97
+ await sendToServer({ type: 'restart_agent_ack', requestId: correlation.requestId, clientId: correlation.clientId });
98
98
  cleanupAndExit(1);
99
99
  }
100
100
 
@@ -178,13 +178,18 @@ function compareCmp(cur, cmp) {
178
178
  }
179
179
  }
180
180
 
181
- export async function handleUpgradeAgent() {
181
+ export async function handleUpgradeAgent(correlation = {}) {
182
+ const sendUpgradeAck = (payload) => sendToServer({
183
+ type: 'upgrade_agent_ack',
184
+ ...payload,
185
+ requestId: correlation.requestId,
186
+ clientId: correlation.clientId,
187
+ });
182
188
  if (isContainerAgentRuntime() || ctx.agentCapabilities?.includes(CONTAINER_AGENT_CAPABILITY)) {
183
189
  const version = ctx.agentVersion || null;
184
190
  const error = buildContainerImageUpgradeMessage(version);
185
191
  console.warn(`[Agent] Upgrade rejected (${CONTAINER_IMAGE_UPGRADE_REASON}): ${error}`);
186
- await sendToServer({
187
- type: 'upgrade_agent_ack',
192
+ await sendUpgradeAck({
188
193
  success: false,
189
194
  reason: CONTAINER_IMAGE_UPGRADE_REASON,
190
195
  error,
@@ -205,7 +210,7 @@ export async function handleUpgradeAgent() {
205
210
  });
206
211
  if (!isUpgradeAvailable(ctx.agentVersion, latestVersion)) {
207
212
  console.log(`[Agent] No newer version than ${ctx.agentVersion} is available (registry: ${latestVersion}), skipping upgrade.`);
208
- await sendToServer({ type: 'upgrade_agent_ack', success: true, alreadyLatest: true, version: ctx.agentVersion });
213
+ await sendUpgradeAck({ success: true, alreadyLatest: true, version: ctx.agentVersion });
209
214
  return;
210
215
  }
211
216
 
@@ -218,8 +223,7 @@ export async function handleUpgradeAgent() {
218
223
  if (requiredNode && !nodeRangeSatisfied(currentNode, requiredNode)) {
219
224
  const msg = `Node ${currentNode} does not satisfy required ${requiredNode} for ${pkgName}@${latestVersion}`;
220
225
  console.warn(`[Agent] Upgrade aborted: ${msg}`);
221
- await sendToServer({
222
- type: 'upgrade_agent_ack',
226
+ await sendUpgradeAck({
223
227
  success: false,
224
228
  reason: 'node_incompatible',
225
229
  error: msg,
@@ -240,7 +244,7 @@ export async function handleUpgradeAgent() {
240
244
  if (!isNpmInstall) {
241
245
  // 源码运行不支持远程升级(代码在 git repo 中,需要手动 git pull)
242
246
  console.log('[Agent] Source-based install detected, remote upgrade not supported.');
243
- await sendToServer({ type: 'upgrade_agent_ack', success: false, error: 'Source-based install: please use git pull to upgrade' });
247
+ await sendUpgradeAck({ success: false, error: 'Source-based install: please use git pull to upgrade' });
244
248
  return;
245
249
  }
246
250
 
@@ -260,9 +264,9 @@ export async function handleUpgradeAgent() {
260
264
  const instanceId = resolveInstanceId();
261
265
 
262
266
  if (isWindows) {
263
- await spawnWindowsUpgradeScript(pkgName, installDir, isGlobalInstall, latestVersion, instanceId);
267
+ await spawnWindowsUpgradeScript(pkgName, installDir, isGlobalInstall, latestVersion, instanceId, sendUpgradeAck);
264
268
  } else {
265
- await spawnUnixUpgradeScript(pkgName, installDir, isGlobalInstall, latestVersion, instanceId);
269
+ await spawnUnixUpgradeScript(pkgName, installDir, isGlobalInstall, latestVersion, instanceId, sendUpgradeAck);
266
270
  }
267
271
 
268
272
  // On Windows the detached runner removes the PM2 app only after this
@@ -281,11 +285,11 @@ export async function handleUpgradeAgent() {
281
285
  cleanupAndExit(0);
282
286
  } catch (e) {
283
287
  console.error('[Agent] Upgrade failed:', e.message);
284
- await sendToServer({ type: 'upgrade_agent_ack', success: false, error: e.message });
288
+ await sendUpgradeAck({ success: false, error: e.message });
285
289
  }
286
290
  }
287
291
 
288
- async function spawnWindowsUpgradeScript(pkgName, installDir, isGlobalInstall, latestVersion, instanceId = DEFAULT_INSTANCE_ID) {
292
+ async function spawnWindowsUpgradeScript(pkgName, installDir, isGlobalInstall, latestVersion, instanceId = DEFAULT_INSTANCE_ID, sendUpgradeAck = (payload) => sendToServer({ type: 'upgrade_agent_ack', ...payload })) {
289
293
  const configDir = getConfigDir(instanceId);
290
294
  const upgradeDir = join(configDir, 'upgrade-runtime');
291
295
  const logDir = join(configDir, 'logs');
@@ -364,7 +368,7 @@ async function spawnWindowsUpgradeScript(pkgName, installDir, isGlobalInstall, l
364
368
  spawnProcess: spawn,
365
369
  });
366
370
  console.log(`[Agent] Spawned Windows upgrade runner via ${launcher} (pm2=${isPm2}, dir=${installDir})`);
367
- await sendToServer({ type: 'upgrade_agent_ack', success: true, version: latestVersion, pendingRestart: true });
371
+ await sendUpgradeAck({ success: true, version: latestVersion, pendingRestart: true });
368
372
  }
369
373
 
370
374
  /**
@@ -529,7 +533,7 @@ export function buildUnixUpgradeScript({
529
533
  return shLines.join('\n');
530
534
  }
531
535
 
532
- async function spawnUnixUpgradeScript(pkgName, installDir, isGlobalInstall, latestVersion, instanceId = DEFAULT_INSTANCE_ID) {
536
+ async function spawnUnixUpgradeScript(pkgName, installDir, isGlobalInstall, latestVersion, instanceId = DEFAULT_INSTANCE_ID, sendUpgradeAck = (payload) => sendToServer({ type: 'upgrade_agent_ack', ...payload })) {
533
537
  const configDir = getConfigDir(instanceId);
534
538
  // Create logs/ too: the generated script's `exec > "$LOGFILE" 2>&1` aborts
535
539
  // the whole upgrade silently if that directory is missing.
@@ -555,5 +559,5 @@ async function spawnUnixUpgradeScript(pkgName, installDir, isGlobalInstall, late
555
559
  });
556
560
  child.unref();
557
561
  console.log(`[Agent] Spawned upgrade script: ${shPath}`);
558
- await sendToServer({ type: 'upgrade_agent_ack', success: true, version: latestVersion, pendingRestart: true });
562
+ await sendUpgradeAck({ success: true, version: latestVersion, pendingRestart: true });
559
563
  }
package/index.js CHANGED
@@ -175,7 +175,7 @@ async function detectCapabilities() {
175
175
  // agent build can speak plaintext WS frames. New servers see this and
176
176
  // flip `agent.encryptOutbound = false`, stopping outbound encryption
177
177
  // to this peer. Old servers ignore the unknown capability token.
178
- const capabilities = ['background_tasks', 'file_editor', 'ping_session', 'plaintext-ok', 'workbench_session_routes', 'work_center', 'work_center_message_v2', 'session_history_search', 'session_history_outline', 'session_history_window_prefetch', 'file_reference_resolution', 'yeaft_plugins', 'yeaft_managed_skills'];
178
+ const capabilities = ['background_tasks', 'file_editor', 'ping_session', 'plaintext-ok', 'workbench_session_routes', 'work_center', 'work_center_message_v2', 'session_history_search', 'session_history_outline', 'session_history_window_prefetch', 'file_reference_resolution', 'yeaft_plugins', 'yeaft_managed_skills', 'settings_request_correlation'];
179
179
  capabilities.push(getAgentUpgradeCapability());
180
180
  if (process.platform === 'linux') capabilities.push('work_item_attachments');
181
181
  const pty = await loadNodePty();
@@ -43,8 +43,104 @@ export const previewFiles = new Map();
43
43
  // original Agent/Session ownership check happened, and consume it exactly once.
44
44
  const YEAFT_DEBUG_REQUEST_TTL_MS = 30_000;
45
45
  const YEAFT_DEBUG_REQUEST_MAX_PENDING = 2048;
46
+ const YEAFT_DEBUG_REQUEST_MAX_PENDING_PER_CLIENT = 256;
46
47
  export const pendingYeaftDebugRequests = new Map();
47
48
 
49
+ // Agent settings replies share one Agent connection across browser clients.
50
+ // Correlation-capable Agents require exact requestId matching. Legacy Agents are
51
+ // allowed one in-flight request per Agent/operation so an identity-less reply has
52
+ // exactly one possible owner.
53
+ const AGENT_SETTINGS_REQUEST_TTL_MS = 120_000;
54
+ const AGENT_SETTINGS_REQUEST_MAX_PENDING = 2048;
55
+ const AGENT_SETTINGS_REQUEST_MAX_PENDING_PER_CLIENT = 256;
56
+ export const pendingAgentSettingsRequests = new Map();
57
+
58
+ function agentSettingsRequestKey(agentId, requestId) {
59
+ return `${String(agentId || '')}\u0000${String(requestId || '')}`;
60
+ }
61
+
62
+ function pruneAgentSettingsRequests(now = Date.now()) {
63
+ for (const [key, pending] of pendingAgentSettingsRequests) {
64
+ if (!pending || pending.expiresAt <= now || !webClients.has(pending.clientId)) {
65
+ pendingAgentSettingsRequests.delete(key);
66
+ }
67
+ }
68
+ }
69
+
70
+ /** Register an owner-checked settings request before it is sent to the Agent. */
71
+ export function registerAgentSettingsRequest({ agentId, operation, requestId, clientId, allowLegacyReply = false }) {
72
+ if (!agentId || !operation || !requestId || !clientId) return false;
73
+ const now = Date.now();
74
+ pruneAgentSettingsRequests(now);
75
+ const key = agentSettingsRequestKey(agentId, requestId);
76
+ if (pendingAgentSettingsRequests.has(key)) return false;
77
+ let clientPending = 0;
78
+ for (const pending of pendingAgentSettingsRequests.values()) {
79
+ if (pending?.clientId === clientId) clientPending++;
80
+ if (allowLegacyReply && pending?.agentId === agentId && pending?.operation === operation) return false;
81
+ }
82
+ if (clientPending >= AGENT_SETTINGS_REQUEST_MAX_PENDING_PER_CLIENT
83
+ || pendingAgentSettingsRequests.size >= AGENT_SETTINGS_REQUEST_MAX_PENDING) return false;
84
+ pendingAgentSettingsRequests.set(key, {
85
+ agentId,
86
+ operation,
87
+ requestId,
88
+ clientId,
89
+ allowLegacyReply,
90
+ expiresAt: now + AGENT_SETTINGS_REQUEST_TTL_MS,
91
+ });
92
+ return true;
93
+ }
94
+
95
+ /** Consume an exact response, or the sole explicitly legacy-compatible request. */
96
+ export function consumeAgentSettingsRequest({ agentId, operation, requestId }) {
97
+ if (!agentId || !operation) return null;
98
+ pruneAgentSettingsRequests();
99
+ if (requestId) {
100
+ const key = agentSettingsRequestKey(agentId, requestId);
101
+ const pending = pendingAgentSettingsRequests.get(key);
102
+ if (!pending || pending.operation !== operation) return null;
103
+ pendingAgentSettingsRequests.delete(key);
104
+ return pending;
105
+ }
106
+ let matchKey = null;
107
+ let match = null;
108
+ for (const [key, pending] of pendingAgentSettingsRequests) {
109
+ if (pending?.agentId !== agentId || pending?.operation !== operation || !pending.allowLegacyReply) continue;
110
+ if (match) return null;
111
+ matchKey = key;
112
+ match = pending;
113
+ }
114
+ if (!match) return null;
115
+ pendingAgentSettingsRequests.delete(matchKey);
116
+ return match;
117
+ }
118
+
119
+ export function deleteAgentSettingsRequest({ agentId, requestId, clientId = null }) {
120
+ const key = agentSettingsRequestKey(agentId, requestId);
121
+ const pending = pendingAgentSettingsRequests.get(key);
122
+ if (!pending || (clientId && pending.clientId !== clientId)) return false;
123
+ return pendingAgentSettingsRequests.delete(key);
124
+ }
125
+
126
+ export function clearAgentSettingsRequestsForClient(clientId) {
127
+ if (!clientId) return;
128
+ for (const [key, pending] of pendingAgentSettingsRequests) {
129
+ if (pending?.clientId === clientId) pendingAgentSettingsRequests.delete(key);
130
+ }
131
+ }
132
+
133
+ export function takeAgentSettingsRequestsForAgent(agentId) {
134
+ const removed = [];
135
+ if (!agentId) return removed;
136
+ for (const [key, pending] of pendingAgentSettingsRequests) {
137
+ if (pending?.agentId !== agentId) continue;
138
+ pendingAgentSettingsRequests.delete(key);
139
+ removed.push(pending);
140
+ }
141
+ return removed;
142
+ }
143
+
48
144
  function yeaftDebugRequestKey(agentId, requestId) {
49
145
  return `${String(agentId || '')}\u0000${String(requestId || '')}`;
50
146
  }
@@ -68,10 +164,12 @@ export function registerYeaftDebugRequest({ agentId, requestId, sessionId, clien
68
164
  pruneYeaftDebugRequests(now);
69
165
  const key = yeaftDebugRequestKey(agentId, requestId);
70
166
  if (pendingYeaftDebugRequests.has(key)) return false;
71
- if (pendingYeaftDebugRequests.size >= YEAFT_DEBUG_REQUEST_MAX_PENDING) {
72
- const oldestKey = pendingYeaftDebugRequests.keys().next().value;
73
- if (oldestKey != null) pendingYeaftDebugRequests.delete(oldestKey);
167
+ let clientPending = 0;
168
+ for (const pending of pendingYeaftDebugRequests.values()) {
169
+ if (pending?.clientId === clientId) clientPending++;
74
170
  }
171
+ if (clientPending >= YEAFT_DEBUG_REQUEST_MAX_PENDING_PER_CLIENT
172
+ || pendingYeaftDebugRequests.size >= YEAFT_DEBUG_REQUEST_MAX_PENDING) return false;
75
173
  pendingYeaftDebugRequests.set(key, {
76
174
  agentId,
77
175
  requestId,
@@ -1,6 +1,10 @@
1
1
  import { CONFIG } from '../config.js';
2
2
  import { agentInventoryDb, sessionDb, userStatsDb } from '../database.js';
3
- import { agents, webClients } from '../context.js';
3
+ import {
4
+ agents,
5
+ consumeAgentSettingsRequest,
6
+ webClients,
7
+ } from '../context.js';
4
8
  import { sendToWebClient, broadcastAgentList } from '../ws-utils.js';
5
9
  import {
6
10
  handleProxyResponse, handleProxyResponseChunk, handleProxyResponseEnd,
@@ -12,6 +16,18 @@ const numberMetric = (value) => {
12
16
  return Number.isFinite(n) && n >= 0 ? n : 0;
13
17
  };
14
18
 
19
+ async function sendAgentSettingsReply(agentId, agent, operation, msg, payload) {
20
+ const pending = consumeAgentSettingsRequest({ agentId, operation, requestId: msg.requestId });
21
+ if (!pending) return false;
22
+ if (!msg.requestId) payload.requestId = pending.requestId;
23
+ const client = webClients.get(pending.clientId);
24
+ if (!client?.authenticated || (!CONFIG.skipAuth
25
+ && !((agent.ownerId && client.userId === agent.ownerId)
26
+ || (!agent.ownerId && client.role === 'admin')))) return false;
27
+ await sendToWebClient(client, payload);
28
+ return true;
29
+ }
30
+
15
31
  function normalizeAgentMetrics(metrics = {}) {
16
32
  const chatTurns = numberMetric(metrics.chatTurns);
17
33
  const yeaftTurns = numberMetric(metrics.yeaftTurns);
@@ -80,16 +96,14 @@ export async function handleAgentSync(agentId, agent, msg) {
80
96
  agent.dreamEnabled = msg.enabled !== false;
81
97
  await broadcastAgentList();
82
98
  }
83
- for (const [, client] of webClients) {
84
- if (!client.authenticated) continue;
85
- if (!CONFIG.skipAuth && agent.ownerId !== client.userId && !(!agent.ownerId && client.role === 'admin')) continue;
86
- await sendToWebClient(client, {
87
- type: 'dream_enabled_changed',
88
- agentId,
89
- enabled: msg.error ? agent.dreamEnabled !== false : msg.enabled !== false,
90
- ...(msg.error ? { error: msg.error } : {}),
91
- });
92
- }
99
+ const payload = {
100
+ type: 'dream_enabled_changed',
101
+ agentId,
102
+ requestId: msg.requestId,
103
+ enabled: msg.error ? agent.dreamEnabled !== false : msg.enabled !== false,
104
+ ...(msg.error ? { error: msg.error } : {}),
105
+ };
106
+ await sendAgentSettingsReply(agentId, agent, 'dream', msg, payload);
93
107
  break;
94
108
  }
95
109
 
@@ -202,27 +216,14 @@ export async function handleAgentSync(agentId, agent, msg) {
202
216
  }
203
217
 
204
218
  case 'restart_agent_ack': {
205
- // 只通知该 Agent 的 owner
206
- for (const [, client] of webClients) {
207
- if (client.authenticated && (CONFIG.skipAuth ||
208
- (agent.ownerId && client.userId === agent.ownerId) ||
209
- (!agent.ownerId && client.role === 'admin')
210
- )) {
211
- await sendToWebClient(client, { type: 'restart_agent_ack', agentId });
212
- }
213
- }
219
+ const payload = { type: 'restart_agent_ack', agentId, requestId: msg.requestId };
220
+ await sendAgentSettingsReply(agentId, agent, 'restart', msg, payload);
214
221
  break;
215
222
  }
216
223
 
217
224
  case 'upgrade_agent_ack': {
218
- for (const [, client] of webClients) {
219
- if (client.authenticated && (CONFIG.skipAuth ||
220
- (agent.ownerId && client.userId === agent.ownerId) ||
221
- (!agent.ownerId && client.role === 'admin')
222
- )) {
223
- await sendToWebClient(client, { type: 'upgrade_agent_ack', agentId, success: msg.success, error: msg.error, alreadyLatest: msg.alreadyLatest, version: msg.version, reason: msg.reason, currentNode: msg.currentNode, requiredNode: msg.requiredNode, requiredCapability: msg.requiredCapability });
224
- }
225
- }
225
+ const payload = { type: 'upgrade_agent_ack', agentId, requestId: msg.requestId, success: msg.success, error: msg.error, alreadyLatest: msg.alreadyLatest, version: msg.version, reason: msg.reason, currentNode: msg.currentNode, requiredNode: msg.requiredNode, requiredCapability: msg.requiredCapability };
226
+ await sendAgentSettingsReply(agentId, agent, 'upgrade', msg, payload);
226
227
  break;
227
228
  }
228
229
 
@@ -408,14 +409,14 @@ export async function handleAgentSync(agentId, agent, msg) {
408
409
  // trace payloads stay on the Agent.
409
410
  case 'telemetry_settings':
410
411
  case 'telemetry_settings_updated': {
411
- for (const [, client] of webClients) {
412
- if (client.authenticated && (CONFIG.skipAuth ||
413
- (agent.ownerId && client.userId === agent.ownerId) ||
414
- (!agent.ownerId && client.role === 'admin')
415
- )) {
416
- await sendToWebClient(client, { ...msg, agentId });
417
- }
418
- }
412
+ const operation = msg.type === 'yeaft_plugins_updated'
413
+ ? 'plugins:update'
414
+ : msg.type === 'yeaft_plugins'
415
+ ? 'plugins:load'
416
+ : msg.type === 'telemetry_settings_updated'
417
+ ? 'telemetry:update'
418
+ : 'telemetry:load';
419
+ await sendAgentSettingsReply(agentId, agent, operation, msg, { ...msg, agentId });
419
420
  break;
420
421
  }
421
422
 
@@ -1339,9 +1339,12 @@ export async function handleClientConversation(clientId, client, msg, checkAgent
1339
1339
  clientId,
1340
1340
  userId: client.userId,
1341
1341
  });
1342
- if (!registered) return;
1342
+ if (!registered) {
1343
+ await sendToWebClient(client, { type: 'yeaft_debug_history', agentId: debugAgentId, sessionId: debugSessionId, requestId: debugRequestId, error: 'Request rejected: too many pending requests or duplicate requestId.' });
1344
+ return;
1345
+ }
1343
1346
  try {
1344
- await forwardToAgent(debugAgentId, {
1347
+ const dispatched = await forwardToAgent(debugAgentId, {
1345
1348
  type: 'yeaft_fetch_debug_history',
1346
1349
  sessionId: debugSessionId,
1347
1350
  requestId: debugRequestId,
@@ -1353,9 +1356,13 @@ export async function handleClientConversation(clientId, client, msg, checkAgent
1353
1356
  search: typeof msg.search === 'string' ? msg.search.slice(0, 500) : '',
1354
1357
  _requestClientId: clientId,
1355
1358
  });
1359
+ if (!dispatched) {
1360
+ deleteYeaftDebugRequest({ agentId: debugAgentId, requestId: debugRequestId, clientId });
1361
+ await sendToWebClient(client, { type: 'yeaft_debug_history', agentId: debugAgentId, sessionId: debugSessionId, requestId: debugRequestId, error: 'Agent is unavailable.' });
1362
+ }
1356
1363
  } catch (err) {
1357
1364
  deleteYeaftDebugRequest({ agentId: debugAgentId, requestId: debugRequestId, clientId });
1358
- throw err;
1365
+ await sendToWebClient(client, { type: 'yeaft_debug_history', agentId: debugAgentId, sessionId: debugSessionId, requestId: debugRequestId, error: `Failed to send request to Agent: ${err.message}` });
1359
1366
  }
1360
1367
  break;
1361
1368
  }
@@ -1,4 +1,9 @@
1
- import { agents, userFileTabs } from '../context.js';
1
+ import {
2
+ agents,
3
+ deleteAgentSettingsRequest,
4
+ registerAgentSettingsRequest,
5
+ userFileTabs,
6
+ } from '../context.js';
2
7
  import {
3
8
  sendToWebClient, forwardToAgent, broadcastAgentList
4
9
  } from '../ws-utils.js';
@@ -27,6 +32,26 @@ async function rejectUnsupportedYeaftPlugins(client, msg, agentId) {
27
32
  });
28
33
  }
29
34
 
35
+ async function forwardRegisteredAgentRequest({ client, clientId, agentId, operation, requestId, message, responseType }) {
36
+ const agent = agents.get(agentId);
37
+ const allowLegacyReply = !Array.isArray(agent?.capabilities)
38
+ || !agent.capabilities.includes('settings_request_correlation');
39
+ if (!registerAgentSettingsRequest({ agentId, operation, requestId, clientId, allowLegacyReply })) {
40
+ await sendToWebClient(client, { type: responseType, agentId, requestId, error: 'Request rejected: too many pending requests or duplicate requestId.' });
41
+ return false;
42
+ }
43
+ try {
44
+ if (await forwardToAgent(agentId, message)) return true;
45
+ } catch (error) {
46
+ deleteAgentSettingsRequest({ agentId, requestId, clientId });
47
+ await sendToWebClient(client, { type: responseType, agentId, requestId, error: `Failed to send request to Agent: ${error.message}` });
48
+ return false;
49
+ }
50
+ deleteAgentSettingsRequest({ agentId, requestId, clientId });
51
+ await sendToWebClient(client, { type: responseType, agentId, requestId, error: 'Agent is unavailable.' });
52
+ return false;
53
+ }
54
+
30
55
  export function requiresManualUpgradeBridge(capabilities, platform = null) {
31
56
  if (Array.isArray(capabilities) && capabilities.includes(SAFE_REMOTE_UPGRADE_CAPABILITY)) return false;
32
57
  const normalizedPlatform = typeof platform === 'string' ? platform.trim().toLowerCase() : '';
@@ -52,7 +77,12 @@ export async function handleClientMisc(clientId, client, msg, checkAgentAccess)
52
77
  const restartAgentId = msg.agentId;
53
78
  if (!restartAgentId) break;
54
79
  if (!await checkAgentAccess(restartAgentId)) break;
55
- await forwardToAgent(restartAgentId, { type: 'restart_agent' });
80
+ if (msg.requestId) {
81
+ await forwardRegisteredAgentRequest({ client, clientId, agentId: restartAgentId, operation: 'restart', requestId: msg.requestId,
82
+ message: { type: 'restart_agent', requestId: msg.requestId, clientId }, responseType: 'restart_agent_ack' });
83
+ } else {
84
+ await forwardToAgent(restartAgentId, { type: 'restart_agent' });
85
+ }
56
86
  break;
57
87
  }
58
88
 
@@ -60,7 +90,12 @@ export async function handleClientMisc(clientId, client, msg, checkAgentAccess)
60
90
  const agentId = msg.agentId;
61
91
  if (!agentId) break;
62
92
  if (!await checkAgentAccess(agentId)) break;
63
- await forwardToAgent(agentId, { type: 'set_dream_enabled', enabled: msg.enabled !== false });
93
+ if (msg.requestId) {
94
+ await forwardRegisteredAgentRequest({ client, clientId, agentId, operation: 'dream', requestId: msg.requestId,
95
+ message: { type: 'set_dream_enabled', enabled: msg.enabled !== false, requestId: msg.requestId, clientId }, responseType: 'dream_enabled_changed' });
96
+ } else {
97
+ await forwardToAgent(agentId, { type: 'set_dream_enabled', enabled: msg.enabled !== false });
98
+ }
64
99
  break;
65
100
  }
66
101
 
@@ -79,6 +114,7 @@ export async function handleClientMisc(clientId, client, msg, checkAgentAccess)
79
114
  version: upgradeAgent?.version || null,
80
115
  requiredCapability: CONTAINER_AGENT_CAPABILITY,
81
116
  error: 'Container Agent is managed as a Docker image. Pull the configured image and recreate the same container through the Server/Sandbox lifecycle while keeping its persistent volumes and host-side agent-secret file.',
117
+ requestId: msg.requestId,
82
118
  });
83
119
  break;
84
120
  }
@@ -91,10 +127,16 @@ export async function handleClientMisc(clientId, client, msg, checkAgentAccess)
91
127
  version: upgradeAgent?.version || null,
92
128
  requiredCapability: SAFE_REMOTE_UPGRADE_CAPABILITY,
93
129
  error: `Agent ${upgradeAgent?.version || 'unknown'} does not advertise the safe remote-upgrade contract. First stop the selected Agent/service on that machine: if it runs under PM2 or another service manager, stop that exact instance there; if it runs in a foreground terminal, terminate that process. Confirm that process has exited, then run "npm install -g @yeaft/webchat-agent@latest --registry=https://pkg.yeaft.com/". Finally, restart the same Agent instance with its original configuration.`,
130
+ requestId: msg.requestId,
94
131
  });
95
132
  break;
96
133
  }
97
- await forwardToAgent(upgradeAgentId, { type: 'upgrade_agent' });
134
+ if (msg.requestId) {
135
+ await forwardRegisteredAgentRequest({ client, clientId, agentId: upgradeAgentId, operation: 'upgrade', requestId: msg.requestId,
136
+ message: { type: 'upgrade_agent', requestId: msg.requestId, clientId }, responseType: 'upgrade_agent_ack' });
137
+ } else {
138
+ await forwardToAgent(upgradeAgentId, { type: 'upgrade_agent' });
139
+ }
98
140
  break;
99
141
  }
100
142
 
@@ -247,10 +289,8 @@ export async function handleClientMisc(clientId, client, msg, checkAgentAccess)
247
289
  await rejectUnsupportedYeaftPlugins(client, msg, targetAgentId);
248
290
  break;
249
291
  }
250
- await forwardToAgent(targetAgentId, {
251
- type: 'get_yeaft_plugins',
252
- requestId: msg.requestId || null,
253
- });
292
+ await forwardRegisteredAgentRequest({ client, clientId, agentId: targetAgentId, operation: 'plugins:load', requestId: msg.requestId,
293
+ message: { type: 'get_yeaft_plugins', requestId: msg.requestId }, responseType: 'yeaft_plugins' });
254
294
  break;
255
295
  }
256
296
 
@@ -264,12 +304,15 @@ export async function handleClientMisc(clientId, client, msg, checkAgentAccess)
264
304
  }
265
305
  const hasPlugins = Object.prototype.hasOwnProperty.call(msg, 'plugins');
266
306
  const hasConfig = Object.prototype.hasOwnProperty.call(msg, 'config');
267
- await forwardToAgent(targetAgentId, {
268
- type: 'update_yeaft_plugins',
269
- requestId: msg.requestId || null,
270
- // Preserve explicit falsy values so Agent-side schema validation can
271
- // reject them. Only an absent payload is the legacy empty selection.
272
- plugins: hasPlugins ? msg.plugins : (hasConfig ? msg.config : {}),
307
+ await forwardRegisteredAgentRequest({ client, clientId, agentId: targetAgentId, operation: 'plugins:update', requestId: msg.requestId,
308
+ message: {
309
+ type: 'update_yeaft_plugins',
310
+ requestId: msg.requestId,
311
+ // Preserve explicit falsy values so Agent-side schema validation can
312
+ // reject them. Only an absent payload is the legacy empty selection.
313
+ plugins: hasPlugins ? msg.plugins : (hasConfig ? msg.config : {}),
314
+ },
315
+ responseType: 'yeaft_plugins_updated',
273
316
  });
274
317
  break;
275
318
  }
@@ -291,7 +334,8 @@ export async function handleClientMisc(clientId, client, msg, checkAgentAccess)
291
334
  const a = msg.agentId || client.currentAgent;
292
335
  if (!a) break;
293
336
  if (!await checkAgentAccess(a)) break;
294
- await forwardToAgent(a, { type: 'get_telemetry_settings' });
337
+ await forwardRegisteredAgentRequest({ client, clientId, agentId: a, operation: 'telemetry:load', requestId: msg.requestId,
338
+ message: { type: 'get_telemetry_settings', requestId: msg.requestId, clientId }, responseType: 'telemetry_settings' });
295
339
  break;
296
340
  }
297
341
 
@@ -299,10 +343,9 @@ export async function handleClientMisc(clientId, client, msg, checkAgentAccess)
299
343
  const a = msg.agentId || client.currentAgent;
300
344
  if (!a) break;
301
345
  if (!await checkAgentAccess(a)) break;
302
- await forwardToAgent(a, {
303
- type: 'update_telemetry_settings',
304
- settings: msg.settings || msg.config || {},
305
- });
346
+ await forwardRegisteredAgentRequest({ client, clientId, agentId: a, operation: 'telemetry:update', requestId: msg.requestId,
347
+ message: { type: 'update_telemetry_settings', requestId: msg.requestId, clientId, settings: msg.settings || msg.config || {} },
348
+ responseType: 'telemetry_settings_updated' });
306
349
  break;
307
350
  }
308
351