@yeaft/webchat-agent 1.0.351 → 1.0.353

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.
Files changed (47) hide show
  1. package/connection/message-router.js +29 -1
  2. package/index.js +2 -0
  3. package/local-runtime/server/context.js +1 -1
  4. package/local-runtime/server/handlers/agent-sync.js +14 -0
  5. package/local-runtime/server/handlers/client-misc.js +53 -0
  6. package/local-runtime/server/ws-agent.js +55 -53
  7. package/local-runtime/version.json +1 -1
  8. package/local-runtime/web/app.bundle.js +27 -8
  9. package/local-runtime/web/app.bundle.js.gz +0 -0
  10. package/local-runtime/web/index.html +2 -2
  11. package/local-runtime/web/style.bundle.css +1 -1
  12. package/local-runtime/web/style.bundle.css.gz +0 -0
  13. package/package.json +1 -1
  14. package/yeaft/config-api.js +53 -1
  15. package/yeaft/config.js +54 -0
  16. package/yeaft/conversation/history-index-worker.js +13 -10
  17. package/yeaft/conversation/internal-control.js +1 -0
  18. package/yeaft/debug-trace.js +164 -47
  19. package/yeaft/engine.js +318 -28
  20. package/yeaft/llm/adapter.js +38 -0
  21. package/yeaft/llm/anthropic.js +11 -8
  22. package/yeaft/llm/openai-responses.js +11 -8
  23. package/yeaft/llm/router.js +1 -1
  24. package/yeaft/perf-trace.js +156 -24
  25. package/yeaft/session.js +7 -0
  26. package/yeaft/sessions/session-crud.js +19 -4
  27. package/yeaft/sub-agent/runner.js +4 -0
  28. package/yeaft/tools/agent.js +4 -0
  29. package/yeaft/tools/ask-user.js +1 -0
  30. package/yeaft/tools/bash.js +4 -0
  31. package/yeaft/tools/create-work-item.js +3 -0
  32. package/yeaft/tools/file-read.js +1 -0
  33. package/yeaft/tools/glob.js +1 -0
  34. package/yeaft/tools/grep.js +1 -0
  35. package/yeaft/tools/history-search.js +74 -20
  36. package/yeaft/tools/js-repl.js +1 -0
  37. package/yeaft/tools/list-agents.js +1 -0
  38. package/yeaft/tools/list-dir.js +1 -0
  39. package/yeaft/tools/list-tasks.js +1 -0
  40. package/yeaft/tools/read-task-log.js +1 -0
  41. package/yeaft/tools/route-forward.js +4 -0
  42. package/yeaft/tools/send-message.js +3 -0
  43. package/yeaft/tools/types.js +8 -0
  44. package/yeaft/tools/wait-agent.js +1 -0
  45. package/yeaft/utf8.js +44 -0
  46. package/yeaft/web-bridge.js +6 -0
  47. package/yeaft/work-center/runner.js +1 -0
@@ -26,7 +26,7 @@ import { sendToServer, flushMessageBuffer } from './buffer.js';
26
26
  import { sendAgentMetricsSnapshot } from '../metrics.js';
27
27
  import { handleRestartAgent, handleUpgradeAgent } from './upgrade.js';
28
28
  import { loadMcpServers, updateMcpConfig } from '../mcp.js';
29
- import { getLlmConfig, updateLlmConfig, getYeaftSettings, updateYeaftSettings, getSearchSettings, updateSearchSettings, fetchTavilyUsage } from '../yeaft/config-api.js';
29
+ import { getLlmConfig, updateLlmConfig, getYeaftSettings, updateYeaftSettings, getTelemetrySettings, updateTelemetrySettings, getSearchSettings, updateSearchSettings, fetchTavilyUsage } from '../yeaft/config-api.js';
30
30
  import { loadConfig } from '../yeaft/config.js';
31
31
  import { discoverLlmModels } from '../llm-model-discovery.js';
32
32
  import { fetchModelsDev } from '../yeaft/llm/models-dev.js';
@@ -448,6 +448,34 @@ export async function handleMessage(msg) {
448
448
  break;
449
449
  }
450
450
 
451
+ // Local performance telemetry settings — read/write the `telemetry`
452
+ // section of config.json. This does not expose trace payloads.
453
+ case 'get_telemetry_settings': {
454
+ const settings = getTelemetrySettings(ctx.CONFIG?.yeaftDir);
455
+ sendToServer({ type: 'telemetry_settings', ...settings });
456
+ break;
457
+ }
458
+
459
+ case 'update_telemetry_settings': {
460
+ const result = updateTelemetrySettings(msg.settings || msg.config || {}, ctx.CONFIG?.yeaftDir);
461
+ if (!result.error) {
462
+ // Bridge trace producers use the agent-owned config object directly.
463
+ // The result is the normalized section that was successfully written,
464
+ // so apply it before refresh can yield and leave no enabled-by-default
465
+ // gap for diagnostics emitted outside a loaded Session.
466
+ if (ctx.CONFIG && typeof ctx.CONFIG === 'object') {
467
+ ctx.CONFIG.telemetry = { ...result };
468
+ }
469
+ try {
470
+ await refreshLiveSessionConfig({});
471
+ } catch (error) {
472
+ result.runtimeRefreshError = error?.message || String(error);
473
+ }
474
+ }
475
+ sendToServer({ type: 'telemetry_settings_updated', ...result });
476
+ break;
477
+ }
478
+
451
479
  // Search settings (web-search backend + Tavily key) — read/write the
452
480
  // `search` section of config.json. `get_tavily_usage` hits Tavily's
453
481
  // /usage endpoint with the saved key and is fired from the UI only
package/index.js CHANGED
@@ -14,6 +14,7 @@ import { getDefaultAgentName, getDefaultYeaftDir, resolveRuntimeIdentity, getCon
14
14
  import { loadNodePty } from './terminal.js';
15
15
  import { connect } from './connection.js';
16
16
  import { loadMcpServers } from './mcp.js';
17
+ import { loadConfig as loadYeaftConfig } from './yeaft/config.js';
17
18
  import {
18
19
  ensureManagedCliTools,
19
20
  prepareManagedCliToolEnvironment,
@@ -105,6 +106,7 @@ const CONFIG = {
105
106
  agentName: AGENT_NAME,
106
107
  workDir: process.env.WORK_DIR || fileConfig.workDir || process.cwd(),
107
108
  yeaftDir: YEAFT_DIR,
109
+ telemetry: loadYeaftConfig({ dir: YEAFT_DIR }).telemetry,
108
110
  reconnectInterval: fileConfig.reconnectInterval,
109
111
  agentSecret: process.env.AGENT_SECRET || fileConfig.agentSecret,
110
112
  // 显式禁用的工具(非 MCP 相关)
@@ -16,7 +16,7 @@ export const pendingProxyRequests = new Map(); // requestId → { res, timeout,
16
16
  export const proxyWsConnections = new Map(); // proxyWsId → { browserWs, agentId }
17
17
 
18
18
  // Store pending agent connections (waiting for auth message)
19
- // tempId -> { ws, agentId, agentName, workDir, timeout }
19
+ // tempId -> { ws, agentId, agentName, instanceId, workDir, skipAgentAuth, connectionGeneration, timeout }
20
20
  export const pendingAgentConnections = new Map();
21
21
 
22
22
  // ★ Phase 3: Server-side message queues
@@ -356,6 +356,20 @@ export async function handleAgentSync(agentId, agent, msg) {
356
356
  break;
357
357
  }
358
358
 
359
+ // Local telemetry settings relay. Only bounded config is forwarded;
360
+ // trace payloads stay on the Agent.
361
+ case 'telemetry_settings':
362
+ case 'telemetry_settings_updated':
363
+ for (const [, client] of webClients) {
364
+ if (client.authenticated && (CONFIG.skipAuth ||
365
+ (agent.ownerId && client.userId === agent.ownerId) ||
366
+ (!agent.ownerId && client.role === 'admin')
367
+ )) {
368
+ await sendToWebClient(client, { ...msg, agentId });
369
+ }
370
+ }
371
+ break;
372
+
359
373
  // Search settings + Tavily usage relays. We pass the whole msg
360
374
  // through (minus agentId, which we set ourselves) — the payload
361
375
  // shapes differ per type and the front-end already filters on
@@ -3,6 +3,25 @@ import {
3
3
  sendToWebClient, forwardToAgent, broadcastAgentList
4
4
  } from '../ws-utils.js';
5
5
 
6
+ // v1.0.342 is the first release with the bootstrap -> detached runner handoff.
7
+ // Older Windows Agents can lose their updater to PM2 tree-kill before npm runs.
8
+ export const MIN_SAFE_REMOTE_UPGRADE_VERSION = '1.0.342';
9
+
10
+ function parseVersion(version) {
11
+ const match = String(version || '').trim().match(/^v?(\d+)\.(\d+)\.(\d+)$/u);
12
+ return match ? match.slice(1).map(Number) : null;
13
+ }
14
+
15
+ export function requiresManualUpgradeBridge(version) {
16
+ const current = parseVersion(version);
17
+ const minimum = parseVersion(MIN_SAFE_REMOTE_UPGRADE_VERSION);
18
+ if (!current || !minimum) return true;
19
+ for (let index = 0; index < minimum.length; index++) {
20
+ if (current[index] !== minimum[index]) return current[index] < minimum[index];
21
+ }
22
+ return false;
23
+ }
24
+
6
25
  /**
7
26
  * Handle miscellaneous messages from web client.
8
27
  * Types: ping, restart_agent, upgrade_agent,
@@ -26,6 +45,19 @@ export async function handleClientMisc(clientId, client, msg, checkAgentAccess)
26
45
  const upgradeAgentId = msg.agentId;
27
46
  if (!upgradeAgentId) break;
28
47
  if (!await checkAgentAccess(upgradeAgentId)) break;
48
+ const upgradeAgent = agents.get(upgradeAgentId);
49
+ if (requiresManualUpgradeBridge(upgradeAgent?.version)) {
50
+ await sendToWebClient(client, {
51
+ type: 'upgrade_agent_ack',
52
+ agentId: upgradeAgentId,
53
+ success: false,
54
+ reason: 'manual_upgrade_required',
55
+ version: upgradeAgent?.version || null,
56
+ minimumVersion: MIN_SAFE_REMOTE_UPGRADE_VERSION,
57
+ error: `Agent ${upgradeAgent?.version || 'unknown'} predates the safe remote-upgrade handoff; manually install ${MIN_SAFE_REMOTE_UPGRADE_VERSION} or newer once`,
58
+ });
59
+ break;
60
+ }
29
61
  await forwardToAgent(upgradeAgentId, { type: 'upgrade_agent' });
30
62
  break;
31
63
  }
@@ -166,6 +198,27 @@ export async function handleClientMisc(clientId, client, msg, checkAgentAccess)
166
198
  break;
167
199
  }
168
200
 
201
+ // Local performance telemetry settings. The agent owns the config file;
202
+ // the server only checks access and relays the request.
203
+ case 'get_telemetry_settings': {
204
+ const a = msg.agentId || client.currentAgent;
205
+ if (!a) break;
206
+ if (!await checkAgentAccess(a)) break;
207
+ await forwardToAgent(a, { type: 'get_telemetry_settings' });
208
+ break;
209
+ }
210
+
211
+ case 'update_telemetry_settings': {
212
+ const a = msg.agentId || client.currentAgent;
213
+ if (!a) break;
214
+ if (!await checkAgentAccess(a)) break;
215
+ await forwardToAgent(a, {
216
+ type: 'update_telemetry_settings',
217
+ settings: msg.settings || msg.config || {},
218
+ });
219
+ break;
220
+ }
221
+
169
222
  // Search settings (web-search backend + Tavily key + on-demand usage probe).
170
223
  // Mirrors the get/update_yeaft_settings pair: the agent owns the
171
224
  // config file, server is just a relay.
@@ -25,60 +25,45 @@ function buildAgentMapKey(ownerId, agentName) {
25
25
  return `${prefix}:${agentName}`;
26
26
  }
27
27
 
28
+ let nextAgentConnectionGeneration = 0;
29
+ // Keep the latest generation as a tombstone while an older auth may still arrive.
30
+ const latestAgentConnectionGenerations = new Map();
31
+
32
+ function claimAgentConnection(agentId, generation) {
33
+ const latestGeneration = latestAgentConnectionGenerations.get(agentId);
34
+ if (latestGeneration !== undefined && latestGeneration > generation) return false;
35
+ latestAgentConnectionGenerations.set(agentId, generation);
36
+ return true;
37
+ }
38
+
39
+ function pruneAgentConnectionGenerations() {
40
+ for (const [agentId, generation] of latestAgentConnectionGenerations) {
41
+ if (agents.has(agentId)) continue;
42
+ const hasPotentiallyOlderConnection = [...pendingAgentConnections.values()].some(pending => {
43
+ if (pending.connectionGeneration >= generation) return false;
44
+ return pending.skipAgentAuth ? pending.agentId === agentId : true;
45
+ });
46
+ if (!hasPotentiallyOlderConnection) latestAgentConnectionGenerations.delete(agentId);
47
+ }
48
+ }
49
+
28
50
  export function handleAgentConnection(ws, url) {
29
51
  const clientAgentId = url.searchParams.get('id') || randomUUID();
30
52
  const agentName = url.searchParams.get('name') || `Agent-${clientAgentId.slice(0, 8)}`;
31
53
  const instanceId = url.searchParams.get('instanceId') || clientAgentId;
32
54
  const workDir = url.searchParams.get('workDir') || '';
33
55
 
34
- // In development mode (SKIP_AUTH), register immediately
35
- if (CONFIG.skipAuth) {
36
- // Dev mode: no owner isolation, use clientAgentId as-is for backward compat
37
- const capabilities = (url.searchParams.get('capabilities') || '').split(',').filter(Boolean);
38
- completeAgentRegistration(ws, clientAgentId, agentName, workDir, null, capabilities, null, null, null, instanceId);
56
+ // Both authenticated and SKIP_AUTH connections use the existing auth frame
57
+ // for registration metadata. Released Agents already send their version there;
58
+ // SKIP_AUTH only bypasses secret validation and owner scoping.
59
+ const skipAgentAuth = CONFIG.skipAuth;
60
+ const urlCapabilities = (url.searchParams.get('capabilities') || '').split(',').filter(Boolean);
61
+ const connectionGeneration = ++nextAgentConnectionGeneration;
39
62
 
40
- ws.on('message', async (data) => {
41
- const agent = agents.get(clientAgentId);
42
- if (!agent || agent.ws !== ws) {
43
- if (!agent) console.error(`[Agent] No agent found for id: ${clientAgentId}`);
44
- return;
45
- }
46
- markAgentHeartbeatSeen(agent);
47
- const msg = await parseMessage(data, agent.sessionKey);
48
- if (msg) {
49
- console.log(`[Agent] Received message from ${clientAgentId}: ${msg.type}`);
50
- if (msg.perfTraceId) {
51
- recordPerfTraceEvent({
52
- traceId: msg.perfTraceId,
53
- source: 'server',
54
- phase: 'websocket.agent_received',
55
- at: Date.now(),
56
- agentId: clientAgentId,
57
- sessionId: msg.sessionId || null,
58
- vpId: msg.vpId || null,
59
- turnId: msg.turnId || null,
60
- threadId: msg.threadId || null,
61
- messageType: msg.type,
62
- bytes: data.length || 0,
63
- });
64
- }
65
- handleAgentMessage(clientAgentId, msg, ws);
66
- } else {
67
- console.error(`[Agent] Failed to parse message from ${clientAgentId}`);
68
- }
69
- });
63
+ // SKIP_AUTH has a complete identity at connect time. Authenticated connections
64
+ // can only claim an owner-scoped identity after their secret is verified.
65
+ if (skipAgentAuth) claimAgentConnection(clientAgentId, connectionGeneration);
70
66
 
71
- ws.on('close', () => {
72
- handleAgentDisconnect(clientAgentId, agentName, ws);
73
- });
74
-
75
- ws.on('error', (err) => {
76
- console.error(`Agent error (${agentName}):`, err.message);
77
- });
78
- return;
79
- }
80
-
81
- // In production mode, wait for auth message with secret
82
67
  const tempId = randomUUID();
83
68
  // Mutable: will be updated to the owner-scoped key after auth succeeds
84
69
  let resolvedAgentId = null;
@@ -86,6 +71,7 @@ export function handleAgentConnection(ws, url) {
86
71
  const authTimeout = setTimeout(() => {
87
72
  console.log(`Agent auth timeout: ${agentName}`);
88
73
  pendingAgentConnections.delete(tempId);
74
+ pruneAgentConnectionGenerations();
89
75
  ws.close(1008, 'Authentication timeout');
90
76
  }, 30000);
91
77
 
@@ -95,10 +81,12 @@ export function handleAgentConnection(ws, url) {
95
81
  agentName,
96
82
  instanceId,
97
83
  workDir,
84
+ skipAgentAuth,
85
+ connectionGeneration,
98
86
  timeout: authTimeout
99
87
  });
100
88
 
101
- // Request authentication
89
+ // Request the existing registration frame. SKIP_AUTH ignores its secret.
102
90
  ws.send(JSON.stringify({
103
91
  type: 'auth_required',
104
92
  tempId
@@ -114,22 +102,34 @@ export function handleAgentConnection(ws, url) {
114
102
  clearTimeout(pending.timeout);
115
103
  pendingAgentConnections.delete(tempId);
116
104
 
117
- const authResult = verifyAgent(msg.secret);
105
+ const authResult = skipAgentAuth
106
+ ? { valid: true, sessionKey: null, userId: null, username: null }
107
+ : verifyAgent(msg.secret);
118
108
  if (!authResult.valid) {
109
+ pruneAgentConnectionGenerations();
119
110
  console.log(`Agent auth failed: ${agentName}`);
120
111
  ws.close(1008, 'Invalid agent secret');
121
112
  return;
122
113
  }
123
114
 
124
- const capabilities = msg.capabilities || [];
115
+ const capabilities = Array.isArray(msg.capabilities) ? msg.capabilities : urlCapabilities;
125
116
  const agentVersion = msg.version || null;
126
- // Build owner-scoped key to prevent cross-user collision. New agents
127
- // identify local service instances separately from display names;
128
- // old agents keep using their name as the stable id.
129
- resolvedAgentId = buildAgentMapKey(authResult.userId, pending.instanceId || pending.agentId || pending.agentName);
117
+ // Authenticated Agents use an owner-scoped key. SKIP_AUTH preserves
118
+ // its historical unscoped id while still receiving version metadata.
119
+ resolvedAgentId = skipAgentAuth
120
+ ? clientAgentId
121
+ : buildAgentMapKey(authResult.userId, pending.instanceId || pending.agentId || pending.agentName);
122
+ if (!claimAgentConnection(resolvedAgentId, connectionGeneration)) {
123
+ resolvedAgentId = null;
124
+ pruneAgentConnectionGenerations();
125
+ ws.close(1008, 'Superseded by a newer Agent connection');
126
+ return;
127
+ }
130
128
  completeAgentRegistration(ws, resolvedAgentId, pending.agentName, pending.workDir, authResult.sessionKey, capabilities, authResult.userId, authResult.username, agentVersion, pending.instanceId || pending.agentId || pending.agentName);
129
+ pruneAgentConnectionGenerations();
131
130
  }
132
131
  } catch (e) {
132
+ pruneAgentConnectionGenerations();
133
133
  console.error('Failed to parse agent auth message:', e.message);
134
134
  }
135
135
  } else {
@@ -171,6 +171,7 @@ export function handleAgentConnection(ws, url) {
171
171
  if (pending) {
172
172
  clearTimeout(pending.timeout);
173
173
  pendingAgentConnections.delete(tempId);
174
+ pruneAgentConnectionGenerations();
174
175
  }
175
176
  // Use resolvedAgentId if auth completed, otherwise nothing to clean
176
177
  if (resolvedAgentId) {
@@ -199,6 +200,7 @@ function handleAgentDisconnect(agentId, agentName, ws) {
199
200
  }
200
201
  // Remove agent entirely — eliminates zombie agents from broadcastAgentList
201
202
  agents.delete(agentId);
203
+ pruneAgentConnectionGenerations();
202
204
  console.log(`Agent disconnected: ${agentName}`);
203
205
  broadcastAgentList();
204
206
  }
@@ -1 +1 @@
1
- {"version":"1.0.351"}
1
+ {"version":"1.0.353"}