@yeaft/webchat-agent 1.0.81 → 1.0.83
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/package.json +1 -1
- package/yeaft/mcp.js +98 -11
- package/yeaft/web-bridge.js +13 -5
package/package.json
CHANGED
package/yeaft/mcp.js
CHANGED
|
@@ -34,6 +34,71 @@ const DEFAULT_TIMEOUT_MS = 30000;
|
|
|
34
34
|
/** Server startup timeout (10 seconds). */
|
|
35
35
|
const STARTUP_TIMEOUT_MS = 10000;
|
|
36
36
|
|
|
37
|
+
// ─── Process-level connection pool ────────────────────────────
|
|
38
|
+
|
|
39
|
+
/** @type {Map<string, { connection: MCPServerConnection, refs: number, startPromise: Promise<void> }>} */
|
|
40
|
+
const mcpConnectionPool = new Map();
|
|
41
|
+
|
|
42
|
+
function stableEnv(env = {}) {
|
|
43
|
+
return Object.fromEntries(
|
|
44
|
+
Object.entries(env || {})
|
|
45
|
+
.filter(([, value]) => value !== undefined)
|
|
46
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function mcpConnectionKey(config) {
|
|
51
|
+
return JSON.stringify({
|
|
52
|
+
name: config.name,
|
|
53
|
+
command: config.command,
|
|
54
|
+
args: Array.isArray(config.args) ? config.args : [],
|
|
55
|
+
env: stableEnv(config.env),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function acquireMcpConnection(serverConfig) {
|
|
60
|
+
const key = mcpConnectionKey(serverConfig);
|
|
61
|
+
let entry = mcpConnectionPool.get(key);
|
|
62
|
+
if (!entry) {
|
|
63
|
+
const connection = new MCPServerConnection(serverConfig.name, serverConfig);
|
|
64
|
+
entry = {
|
|
65
|
+
connection,
|
|
66
|
+
refs: 0,
|
|
67
|
+
startPromise: connection.start(),
|
|
68
|
+
};
|
|
69
|
+
connection.once('close', () => {
|
|
70
|
+
if (mcpConnectionPool.get(key) === entry) {
|
|
71
|
+
mcpConnectionPool.delete(key);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
mcpConnectionPool.set(key, entry);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
entry.refs += 1;
|
|
78
|
+
try {
|
|
79
|
+
await entry.startPromise;
|
|
80
|
+
return { key, connection: entry.connection };
|
|
81
|
+
} catch (err) {
|
|
82
|
+
entry.refs -= 1;
|
|
83
|
+
if (entry.refs <= 0 && mcpConnectionPool.get(key) === entry) {
|
|
84
|
+
mcpConnectionPool.delete(key);
|
|
85
|
+
}
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function releaseMcpConnection(key, connection) {
|
|
91
|
+
const entry = mcpConnectionPool.get(key);
|
|
92
|
+
if (!entry || entry.connection !== connection) {
|
|
93
|
+
await connection.stop();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
entry.refs -= 1;
|
|
97
|
+
if (entry.refs > 0) return;
|
|
98
|
+
mcpConnectionPool.delete(key);
|
|
99
|
+
await connection.stop();
|
|
100
|
+
}
|
|
101
|
+
|
|
37
102
|
// ─── MCP Server Connection ─────────────────────────────────
|
|
38
103
|
|
|
39
104
|
/**
|
|
@@ -46,6 +111,7 @@ class MCPServerConnection extends EventEmitter {
|
|
|
46
111
|
#pendingRequests;
|
|
47
112
|
#tools;
|
|
48
113
|
#ready;
|
|
114
|
+
#closed;
|
|
49
115
|
#buffer;
|
|
50
116
|
|
|
51
117
|
/**
|
|
@@ -59,6 +125,7 @@ class MCPServerConnection extends EventEmitter {
|
|
|
59
125
|
this.#pendingRequests = new Map();
|
|
60
126
|
this.#tools = [];
|
|
61
127
|
this.#ready = false;
|
|
128
|
+
this.#closed = false;
|
|
62
129
|
this.#buffer = '';
|
|
63
130
|
this.config = config;
|
|
64
131
|
}
|
|
@@ -102,6 +169,7 @@ class MCPServerConnection extends EventEmitter {
|
|
|
102
169
|
|
|
103
170
|
this.#process.on('close', (code) => {
|
|
104
171
|
this.#ready = false;
|
|
172
|
+
this.#closed = true;
|
|
105
173
|
this.emit('close', code);
|
|
106
174
|
// Reject all pending requests
|
|
107
175
|
for (const [, { reject: rej }] of this.#pendingRequests) {
|
|
@@ -252,9 +320,16 @@ class MCPServerConnection extends EventEmitter {
|
|
|
252
320
|
if (this.#process) {
|
|
253
321
|
this.#ready = false;
|
|
254
322
|
this.#process.kill('SIGTERM');
|
|
255
|
-
// Wait a bit then force kill
|
|
256
|
-
|
|
257
|
-
|
|
323
|
+
// Wait a bit then force kill. Test fakes do not emit close, so cap the
|
|
324
|
+
// wait at 2s and do not require a close event for cleanup to finish.
|
|
325
|
+
await Promise.race([
|
|
326
|
+
new Promise(resolve => {
|
|
327
|
+
if (this.#closed) return resolve();
|
|
328
|
+
this.#process.once('close', resolve);
|
|
329
|
+
}),
|
|
330
|
+
new Promise(resolve => setTimeout(resolve, 2000)),
|
|
331
|
+
]);
|
|
332
|
+
if (this.#process && !this.#process.killed && !this.#closed) {
|
|
258
333
|
this.#process.kill('SIGKILL');
|
|
259
334
|
}
|
|
260
335
|
this.#process = null;
|
|
@@ -268,7 +343,7 @@ class MCPServerConnection extends EventEmitter {
|
|
|
268
343
|
* MCPManager — manages multiple MCP server connections.
|
|
269
344
|
*/
|
|
270
345
|
export class MCPManager {
|
|
271
|
-
/** @type {Map<string, MCPServerConnection>} */
|
|
346
|
+
/** @type {Map<string, { key: string, connection: MCPServerConnection }>} */
|
|
272
347
|
#servers = new Map();
|
|
273
348
|
|
|
274
349
|
/** @type {Map<string, object>} */
|
|
@@ -288,10 +363,10 @@ export class MCPManager {
|
|
|
288
363
|
await this.disconnect(name);
|
|
289
364
|
}
|
|
290
365
|
|
|
291
|
-
const
|
|
292
|
-
|
|
366
|
+
const pooled = await acquireMcpConnection(serverConfig);
|
|
367
|
+
const connection = pooled.connection;
|
|
293
368
|
|
|
294
|
-
this.#servers.set(name,
|
|
369
|
+
this.#servers.set(name, pooled);
|
|
295
370
|
|
|
296
371
|
// Index tools
|
|
297
372
|
for (const tool of connection.tools) {
|
|
@@ -329,14 +404,15 @@ export class MCPManager {
|
|
|
329
404
|
* @param {string} name — server name
|
|
330
405
|
*/
|
|
331
406
|
async disconnect(name) {
|
|
332
|
-
const
|
|
333
|
-
if (
|
|
407
|
+
const entry = this.#servers.get(name);
|
|
408
|
+
if (entry) {
|
|
409
|
+
const connection = entry.connection;
|
|
334
410
|
// Remove tools from index
|
|
335
411
|
for (const tool of connection.tools) {
|
|
336
412
|
this.#toolIndex.delete(tool.name);
|
|
337
413
|
}
|
|
338
|
-
await connection.stop();
|
|
339
414
|
this.#servers.delete(name);
|
|
415
|
+
await releaseMcpConnection(entry.key, connection);
|
|
340
416
|
}
|
|
341
417
|
}
|
|
342
418
|
|
|
@@ -400,7 +476,7 @@ export class MCPManager {
|
|
|
400
476
|
* @returns {{ name: string, ready: boolean, toolCount: number }[]}
|
|
401
477
|
*/
|
|
402
478
|
status() {
|
|
403
|
-
return [...this.#servers.entries()].map(([name, conn]) => ({
|
|
479
|
+
return [...this.#servers.entries()].map(([name, { connection: conn }]) => ({
|
|
404
480
|
name,
|
|
405
481
|
ready: conn.ready,
|
|
406
482
|
toolCount: conn.tools.length,
|
|
@@ -439,3 +515,14 @@ export async function createMCPManager(config) {
|
|
|
439
515
|
|
|
440
516
|
return manager;
|
|
441
517
|
}
|
|
518
|
+
|
|
519
|
+
export async function __resetMcpConnectionPoolForTests({ stop = true } = {}) {
|
|
520
|
+
const entries = [...mcpConnectionPool.values()];
|
|
521
|
+
mcpConnectionPool.clear();
|
|
522
|
+
if (!stop) return;
|
|
523
|
+
await Promise.all(entries.map(entry => entry.connection.stop().catch(() => {})));
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
export function __mcpConnectionPoolSizeForTests() {
|
|
527
|
+
return mcpConnectionPool.size;
|
|
528
|
+
}
|
package/yeaft/web-bridge.js
CHANGED
|
@@ -1190,6 +1190,12 @@ export function __testGroupHistory(sessionId) {
|
|
|
1190
1190
|
return getOrCreateSessionHistory(sessionId);
|
|
1191
1191
|
}
|
|
1192
1192
|
|
|
1193
|
+
export function __testResolveVpEffectiveConfig(sessionId) {
|
|
1194
|
+
if (!session) return null;
|
|
1195
|
+
const sessionConfigRoot = ctx.CONFIG?.yeaftDir || session.yeaftDir;
|
|
1196
|
+
return resolveSessionConfig(session.config, loadSessionConfig(sessionConfigRoot, sessionId));
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1193
1199
|
/**
|
|
1194
1200
|
* Test-only: install a minimal `session` so `hydrateGroupHistory` can
|
|
1195
1201
|
* read from a real `ConversationStore`. Pass `null` to clear.
|
|
@@ -1293,11 +1299,13 @@ function getOrCreateVpEngine(sessionId, vpId, threadId = 'main') {
|
|
|
1293
1299
|
let eng = vpEngines.get(key);
|
|
1294
1300
|
if (eng) return eng;
|
|
1295
1301
|
if (!session) throw new Error('getOrCreateVpEngine: session not loaded');
|
|
1296
|
-
// Per-
|
|
1297
|
-
// session's user-level config when no override is set.
|
|
1298
|
-
//
|
|
1299
|
-
|
|
1300
|
-
|
|
1302
|
+
// Per-session config overlay (v1: model only). Falls back to the
|
|
1303
|
+
// session's user-level config when no override is set. Prefer the agent-local
|
|
1304
|
+
// config root: sessionConfigPath() resolves registered workDir-backed
|
|
1305
|
+
// sessions from there, while still allowing a later agent-local session in
|
|
1306
|
+
// the same bridge runtime to read its own override after a workDir-first boot.
|
|
1307
|
+
const sessionConfigRoot = ctx.CONFIG?.yeaftDir || session.yeaftDir;
|
|
1308
|
+
const groupCfg = loadSessionConfig(sessionConfigRoot, sessionId);
|
|
1301
1309
|
const effectiveConfig = resolveSessionConfig(session.config, groupCfg);
|
|
1302
1310
|
eng = new Engine({
|
|
1303
1311
|
adapter: session.adapter,
|