@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.
- package/connection/message-router.js +29 -1
- package/index.js +2 -0
- package/local-runtime/server/context.js +1 -1
- package/local-runtime/server/handlers/agent-sync.js +14 -0
- package/local-runtime/server/handlers/client-misc.js +53 -0
- package/local-runtime/server/ws-agent.js +55 -53
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +27 -8
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/package.json +1 -1
- package/yeaft/config-api.js +53 -1
- package/yeaft/config.js +54 -0
- package/yeaft/conversation/history-index-worker.js +13 -10
- package/yeaft/conversation/internal-control.js +1 -0
- package/yeaft/debug-trace.js +164 -47
- package/yeaft/engine.js +318 -28
- package/yeaft/llm/adapter.js +38 -0
- package/yeaft/llm/anthropic.js +11 -8
- package/yeaft/llm/openai-responses.js +11 -8
- package/yeaft/llm/router.js +1 -1
- package/yeaft/perf-trace.js +156 -24
- package/yeaft/session.js +7 -0
- package/yeaft/sessions/session-crud.js +19 -4
- package/yeaft/sub-agent/runner.js +4 -0
- package/yeaft/tools/agent.js +4 -0
- package/yeaft/tools/ask-user.js +1 -0
- package/yeaft/tools/bash.js +4 -0
- package/yeaft/tools/create-work-item.js +3 -0
- package/yeaft/tools/file-read.js +1 -0
- package/yeaft/tools/glob.js +1 -0
- package/yeaft/tools/grep.js +1 -0
- package/yeaft/tools/history-search.js +74 -20
- package/yeaft/tools/js-repl.js +1 -0
- package/yeaft/tools/list-agents.js +1 -0
- package/yeaft/tools/list-dir.js +1 -0
- package/yeaft/tools/list-tasks.js +1 -0
- package/yeaft/tools/read-task-log.js +1 -0
- package/yeaft/tools/route-forward.js +4 -0
- package/yeaft/tools/send-message.js +3 -0
- package/yeaft/tools/types.js +8 -0
- package/yeaft/tools/wait-agent.js +1 -0
- package/yeaft/utf8.js +44 -0
- package/yeaft/web-bridge.js +6 -0
- 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
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
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 =
|
|
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
|
-
//
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
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.
|
|
1
|
+
{"version":"1.0.353"}
|