let-them-talk 3.4.3 → 3.5.0
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/CHANGELOG.md +40 -0
- package/LICENSE +1 -1
- package/README.md +14 -44
- package/cli.js +163 -5
- package/dashboard.html +185 -3
- package/dashboard.js +288 -16
- package/package.json +1 -1
- package/server.js +227 -31
package/server.js
CHANGED
|
@@ -27,6 +27,28 @@ let lastReadOffset = 0; // byte offset into messages.jsonl for efficient polling
|
|
|
27
27
|
let heartbeatInterval = null; // heartbeat timer reference
|
|
28
28
|
let messageSeq = 0; // monotonic sequence counter for message ordering
|
|
29
29
|
let currentBranch = 'main'; // which branch this agent is on
|
|
30
|
+
let lastSentAt = 0; // timestamp of last sent message (for group cooldown)
|
|
31
|
+
|
|
32
|
+
// --- Group conversation mode ---
|
|
33
|
+
const CONFIG_FILE = path.join(DATA_DIR, 'config.json');
|
|
34
|
+
|
|
35
|
+
function getConfig() {
|
|
36
|
+
if (!fs.existsSync(CONFIG_FILE)) return {};
|
|
37
|
+
try { return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); } catch { return {}; }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function saveConfig(config) {
|
|
41
|
+
ensureDataDir();
|
|
42
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isGroupMode() {
|
|
46
|
+
return getConfig().conversation_mode === 'group';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getGroupCooldown() {
|
|
50
|
+
return getConfig().group_cooldown || 3000; // default 3s
|
|
51
|
+
}
|
|
30
52
|
|
|
31
53
|
// Rate limiting — prevent broadcast storms and message flooding
|
|
32
54
|
const rateLimitWindow = 60000; // 1 minute window
|
|
@@ -86,6 +108,22 @@ function readJsonl(file) {
|
|
|
86
108
|
}).filter(Boolean);
|
|
87
109
|
}
|
|
88
110
|
|
|
111
|
+
// File-based lock for agents.json (prevents registration race conditions)
|
|
112
|
+
const AGENTS_LOCK = AGENTS_FILE + '.lock';
|
|
113
|
+
function lockAgentsFile() {
|
|
114
|
+
const maxWait = 5000; const start = Date.now();
|
|
115
|
+
while (Date.now() - start < maxWait) {
|
|
116
|
+
try { fs.writeFileSync(AGENTS_LOCK, String(process.pid), { flag: 'wx' }); return true; }
|
|
117
|
+
catch { /* lock exists, wait */ }
|
|
118
|
+
const wait = Date.now(); while (Date.now() - wait < 50) {} // busy-wait 50ms
|
|
119
|
+
}
|
|
120
|
+
// Force-break stale lock after timeout
|
|
121
|
+
try { fs.unlinkSync(AGENTS_LOCK); } catch {}
|
|
122
|
+
try { fs.writeFileSync(AGENTS_LOCK, String(process.pid), { flag: 'wx' }); return true; } catch {}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
function unlockAgentsFile() { try { fs.unlinkSync(AGENTS_LOCK); } catch {} }
|
|
126
|
+
|
|
89
127
|
function getAgents() {
|
|
90
128
|
if (!fs.existsSync(AGENTS_FILE)) return {};
|
|
91
129
|
try {
|
|
@@ -108,9 +146,15 @@ function getAcks() {
|
|
|
108
146
|
}
|
|
109
147
|
}
|
|
110
148
|
|
|
111
|
-
function isPidAlive(pid) {
|
|
149
|
+
function isPidAlive(pid, lastActivity) {
|
|
112
150
|
try {
|
|
113
151
|
process.kill(pid, 0);
|
|
152
|
+
// On Windows, PIDs get reused. If the heartbeat stopped (no activity for 30s = 3 missed
|
|
153
|
+
// heartbeats), treat as stale even if PID exists (it's likely a different process now)
|
|
154
|
+
if (lastActivity) {
|
|
155
|
+
const stale = Date.now() - new Date(lastActivity).getTime();
|
|
156
|
+
if (stale > 30000) return false; // 30s = 3 missed heartbeats
|
|
157
|
+
}
|
|
114
158
|
return true;
|
|
115
159
|
} catch {
|
|
116
160
|
return false;
|
|
@@ -185,7 +229,7 @@ function buildMessageResponse(msg, consumedIds) {
|
|
|
185
229
|
|
|
186
230
|
// Count online agents
|
|
187
231
|
const agents = getAgents();
|
|
188
|
-
const agentsOnline = Object.entries(agents).filter(([, info]) => isPidAlive(info.pid)).length;
|
|
232
|
+
const agentsOnline = Object.entries(agents).filter(([, info]) => isPidAlive(info.pid, info.last_activity)).length;
|
|
189
233
|
|
|
190
234
|
// Count total messages for context window management
|
|
191
235
|
let totalMessages = 0;
|
|
@@ -422,30 +466,32 @@ function getHistoryFile(branch) {
|
|
|
422
466
|
function toolRegister(name, provider = null) {
|
|
423
467
|
ensureDataDir();
|
|
424
468
|
sanitizeName(name);
|
|
469
|
+
lockAgentsFile();
|
|
425
470
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
471
|
+
try {
|
|
472
|
+
const agents = getAgents();
|
|
473
|
+
if (agents[name] && agents[name].pid !== process.pid && isPidAlive(agents[name].pid, agents[name].last_activity)) {
|
|
474
|
+
return { error: `Agent "${name}" is already registered by a live process. Choose a different name.` };
|
|
475
|
+
}
|
|
430
476
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
477
|
+
// If name was previously registered by a dead process, verify token to prevent impersonation
|
|
478
|
+
if (agents[name] && agents[name].token && !isPidAlive(agents[name].pid, agents[name].last_activity)) {
|
|
479
|
+
// Dead agent — only allow re-registration from the same process (same token)
|
|
480
|
+
if (registeredToken && registeredToken !== agents[name].token) {
|
|
481
|
+
return { error: `Agent "${name}" was previously registered by another process. Choose a different name.` };
|
|
482
|
+
}
|
|
436
483
|
}
|
|
437
|
-
}
|
|
438
484
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
485
|
+
// Clean up old registration if re-registering with a different name
|
|
486
|
+
if (registeredName && registeredName !== name && agents[registeredName] && agents[registeredName].pid === process.pid) {
|
|
487
|
+
delete agents[registeredName];
|
|
488
|
+
}
|
|
443
489
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
490
|
+
const now = new Date().toISOString();
|
|
491
|
+
const token = (agents[name] && agents[name].token) || generateToken();
|
|
492
|
+
agents[name] = { pid: process.pid, timestamp: now, last_activity: now, provider: provider || 'unknown', branch: currentBranch, token, started_at: now };
|
|
493
|
+
saveAgents(agents);
|
|
494
|
+
registeredName = name;
|
|
449
495
|
registeredToken = token;
|
|
450
496
|
|
|
451
497
|
// Auto-create profile if not exists
|
|
@@ -468,7 +514,10 @@ function toolRegister(name, provider = null) {
|
|
|
468
514
|
}, 10000);
|
|
469
515
|
heartbeatInterval.unref(); // Don't prevent process exit
|
|
470
516
|
|
|
471
|
-
|
|
517
|
+
return { success: true, message: `Registered as Agent ${name} (PID ${process.pid})` };
|
|
518
|
+
} finally {
|
|
519
|
+
unlockAgentsFile();
|
|
520
|
+
}
|
|
472
521
|
}
|
|
473
522
|
|
|
474
523
|
// Update last_activity timestamp for this agent
|
|
@@ -500,7 +549,7 @@ function toolListAgents() {
|
|
|
500
549
|
const profiles = getProfiles();
|
|
501
550
|
const result = {};
|
|
502
551
|
for (const [name, info] of Object.entries(agents)) {
|
|
503
|
-
const alive = isPidAlive(info.pid);
|
|
552
|
+
const alive = isPidAlive(info.pid, info.last_activity);
|
|
504
553
|
const lastActivity = info.last_activity || info.timestamp;
|
|
505
554
|
const idleSeconds = Math.floor((Date.now() - new Date(lastActivity).getTime()) / 1000);
|
|
506
555
|
const profile = profiles[name] || {};
|
|
@@ -523,7 +572,7 @@ function toolListAgents() {
|
|
|
523
572
|
return { agents: result };
|
|
524
573
|
}
|
|
525
574
|
|
|
526
|
-
function toolSendMessage(content, to = null, reply_to = null) {
|
|
575
|
+
async function toolSendMessage(content, to = null, reply_to = null) {
|
|
527
576
|
if (!registeredName) {
|
|
528
577
|
return { error: 'You must call register() first' };
|
|
529
578
|
}
|
|
@@ -531,6 +580,15 @@ function toolSendMessage(content, to = null, reply_to = null) {
|
|
|
531
580
|
const rateErr = checkRateLimit();
|
|
532
581
|
if (rateErr) return rateErr;
|
|
533
582
|
|
|
583
|
+
// Group mode cooldown — prevent agents from responding too fast
|
|
584
|
+
if (isGroupMode()) {
|
|
585
|
+
const cooldown = getGroupCooldown();
|
|
586
|
+
const elapsed = Date.now() - lastSentAt;
|
|
587
|
+
if (elapsed < cooldown) {
|
|
588
|
+
await sleep(cooldown - elapsed);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
534
592
|
const agents = getAgents();
|
|
535
593
|
const otherAgents = Object.keys(agents).filter(n => n !== registeredName);
|
|
536
594
|
|
|
@@ -564,7 +622,7 @@ function toolSendMessage(content, to = null, reply_to = null) {
|
|
|
564
622
|
if (sizeErr) return sizeErr;
|
|
565
623
|
|
|
566
624
|
// Check if recipient is alive — warn if dead
|
|
567
|
-
const recipientAlive = isPidAlive(agents[to].pid);
|
|
625
|
+
const recipientAlive = isPidAlive(agents[to].pid, agents[to].last_activity);
|
|
568
626
|
|
|
569
627
|
// Resolve threading
|
|
570
628
|
let thread_id = null;
|
|
@@ -594,6 +652,19 @@ function toolSendMessage(content, to = null, reply_to = null) {
|
|
|
594
652
|
fs.appendFileSync(getMessagesFile(currentBranch), JSON.stringify(msg) + '\n');
|
|
595
653
|
fs.appendFileSync(getHistoryFile(currentBranch), JSON.stringify(msg) + '\n');
|
|
596
654
|
touchActivity();
|
|
655
|
+
lastSentAt = Date.now();
|
|
656
|
+
|
|
657
|
+
// In group mode, auto-broadcast: also write to all other agents' queues
|
|
658
|
+
// Skip if this message is already a response to a broadcast (prevents cascade)
|
|
659
|
+
if (isGroupMode() && !reply_to && !msg.broadcast) {
|
|
660
|
+
const otherRecipients = Object.keys(getAgents()).filter(n => n !== registeredName && n !== to);
|
|
661
|
+
for (const other of otherRecipients) {
|
|
662
|
+
if (!canSendTo(registeredName, other)) continue; // respect permissions
|
|
663
|
+
const broadcastMsg = { ...msg, id: generateId(), to: other, broadcast: true, original_to: to };
|
|
664
|
+
fs.appendFileSync(getMessagesFile(currentBranch), JSON.stringify(broadcastMsg) + '\n');
|
|
665
|
+
fs.appendFileSync(getHistoryFile(currentBranch), JSON.stringify(broadcastMsg) + '\n');
|
|
666
|
+
}
|
|
667
|
+
}
|
|
597
668
|
|
|
598
669
|
const result = { success: true, messageId: msg.id, from: msg.from, to: msg.to };
|
|
599
670
|
if (currentBranch !== 'main') result.branch = currentBranch;
|
|
@@ -641,6 +712,7 @@ function toolBroadcast(content) {
|
|
|
641
712
|
ids.push({ to, messageId: msg.id });
|
|
642
713
|
}
|
|
643
714
|
touchActivity();
|
|
715
|
+
lastSentAt = Date.now();
|
|
644
716
|
|
|
645
717
|
const result = { success: true, sent_to: ids, recipient_count: ids.length };
|
|
646
718
|
if (skipped.length > 0) result.skipped = skipped;
|
|
@@ -868,6 +940,97 @@ async function toolListenCodex(from = null) {
|
|
|
868
940
|
};
|
|
869
941
|
}
|
|
870
942
|
|
|
943
|
+
// --- Group conversation tools ---
|
|
944
|
+
|
|
945
|
+
function toolSetConversationMode(mode) {
|
|
946
|
+
if (!registeredName) return { error: 'You must call register() first' };
|
|
947
|
+
if (!['group', 'direct'].includes(mode)) return { error: 'Mode must be "group" or "direct"' };
|
|
948
|
+
const config = getConfig();
|
|
949
|
+
config.conversation_mode = mode;
|
|
950
|
+
if (mode === 'group' && !config.group_cooldown) config.group_cooldown = 3000;
|
|
951
|
+
saveConfig(config);
|
|
952
|
+
return {
|
|
953
|
+
success: true,
|
|
954
|
+
mode,
|
|
955
|
+
message: mode === 'group'
|
|
956
|
+
? 'Group mode enabled. Use listen_group() to receive batched messages. All messages are shared with everyone.'
|
|
957
|
+
: 'Direct mode enabled. Use listen() for point-to-point messaging.'
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
async function toolListenGroup(timeout_seconds = 300) {
|
|
962
|
+
if (!registeredName) return { error: 'You must call register() first' };
|
|
963
|
+
const timeoutMs = Math.min(Math.max(1, timeout_seconds || 300), 3600) * 1000;
|
|
964
|
+
|
|
965
|
+
setListening(true);
|
|
966
|
+
|
|
967
|
+
// Random stagger to prevent all agents from responding simultaneously (1-3s)
|
|
968
|
+
const stagger = 1000 + Math.random() * 2000;
|
|
969
|
+
await new Promise(r => setTimeout(r, stagger));
|
|
970
|
+
|
|
971
|
+
const deadline = Date.now() + timeoutMs;
|
|
972
|
+
const consumed = getConsumedIds(registeredName);
|
|
973
|
+
|
|
974
|
+
while (Date.now() < deadline) {
|
|
975
|
+
// Collect ALL unconsumed messages addressed to us or broadcast
|
|
976
|
+
const messages = readJsonl(getMessagesFile(currentBranch));
|
|
977
|
+
const batch = [];
|
|
978
|
+
for (const msg of messages) {
|
|
979
|
+
if (consumed.has(msg.id)) continue;
|
|
980
|
+
if (msg.to !== registeredName && msg.to !== '__all__') continue;
|
|
981
|
+
// Permission check
|
|
982
|
+
const perms = getPermissions();
|
|
983
|
+
if (perms[registeredName] && perms[registeredName].can_read) {
|
|
984
|
+
const allowed = perms[registeredName].can_read;
|
|
985
|
+
if (allowed !== '*' && Array.isArray(allowed) && !allowed.includes(msg.from) && !msg.system) continue;
|
|
986
|
+
}
|
|
987
|
+
batch.push(msg);
|
|
988
|
+
consumed.add(msg.id);
|
|
989
|
+
markAsRead(registeredName, msg.id);
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
if (batch.length > 0) {
|
|
993
|
+
saveConsumedIds(registeredName, consumed);
|
|
994
|
+
touchActivity();
|
|
995
|
+
setListening(false);
|
|
996
|
+
|
|
997
|
+
// Get recent history for context
|
|
998
|
+
const history = readJsonl(getHistoryFile(currentBranch));
|
|
999
|
+
const recentHistory = history.slice(-20).map(m => ({
|
|
1000
|
+
from: m.from, to: m.to, content: m.content.substring(0, 500),
|
|
1001
|
+
timestamp: m.timestamp, id: m.id,
|
|
1002
|
+
}));
|
|
1003
|
+
|
|
1004
|
+
// Count agents and who hasn't spoken recently
|
|
1005
|
+
const agents = getAgents();
|
|
1006
|
+
const agentNames = Object.keys(agents).filter(n => isPidAlive(agents[n].pid, agents[n].last_activity));
|
|
1007
|
+
const recentSpeakers = new Set(history.slice(-10).map(m => m.from));
|
|
1008
|
+
const silent = agentNames.filter(n => !recentSpeakers.has(n) && n !== registeredName);
|
|
1009
|
+
|
|
1010
|
+
return {
|
|
1011
|
+
messages: batch.map(m => ({
|
|
1012
|
+
id: m.id, from: m.from, to: m.to, content: m.content,
|
|
1013
|
+
timestamp: m.timestamp,
|
|
1014
|
+
...(m.reply_to && { reply_to: m.reply_to }),
|
|
1015
|
+
...(m.thread_id && { thread_id: m.thread_id }),
|
|
1016
|
+
})),
|
|
1017
|
+
message_count: batch.length,
|
|
1018
|
+
context: recentHistory,
|
|
1019
|
+
agents_online: agentNames.length,
|
|
1020
|
+
agents_silent: silent,
|
|
1021
|
+
hint: silent.length > 0
|
|
1022
|
+
? `${silent.join(', ')} haven't spoken recently. Consider addressing them.`
|
|
1023
|
+
: 'All agents are active in the conversation.',
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
await adaptiveSleep(0);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
setListening(false);
|
|
1031
|
+
return { timeout: true, message: 'No messages received within timeout.', messages: [], message_count: 0 };
|
|
1032
|
+
}
|
|
1033
|
+
|
|
871
1034
|
function toolGetHistory(limit = 50, thread_id = null) {
|
|
872
1035
|
limit = Math.min(Math.max(1, limit || 50), 500);
|
|
873
1036
|
let history = readJsonl(getHistoryFile(currentBranch));
|
|
@@ -911,6 +1074,11 @@ function toolHandoff(to, context) {
|
|
|
911
1074
|
const sizeErr = validateContentSize(context);
|
|
912
1075
|
if (sizeErr) return sizeErr;
|
|
913
1076
|
|
|
1077
|
+
// Permission check
|
|
1078
|
+
if (!canSendTo(registeredName, to)) {
|
|
1079
|
+
return { error: `Permission denied: you are not allowed to hand off to "${to}"` };
|
|
1080
|
+
}
|
|
1081
|
+
|
|
914
1082
|
const agents = getAgents();
|
|
915
1083
|
if (!agents[to]) {
|
|
916
1084
|
return { error: `Agent "${to}" is not registered` };
|
|
@@ -1170,7 +1338,7 @@ function toolReset() {
|
|
|
1170
1338
|
}
|
|
1171
1339
|
}
|
|
1172
1340
|
// Remove profiles, workflows, branches, permissions, read receipts
|
|
1173
|
-
for (const f of [PROFILES_FILE, WORKFLOWS_FILE, BRANCHES_FILE, PERMISSIONS_FILE, READ_RECEIPTS_FILE]) {
|
|
1341
|
+
for (const f of [PROFILES_FILE, WORKFLOWS_FILE, BRANCHES_FILE, PERMISSIONS_FILE, READ_RECEIPTS_FILE, CONFIG_FILE]) {
|
|
1174
1342
|
if (fs.existsSync(f)) fs.unlinkSync(f);
|
|
1175
1343
|
}
|
|
1176
1344
|
// Remove workspaces dir
|
|
@@ -1354,9 +1522,9 @@ function toolAdvanceWorkflow(workflowId, notes) {
|
|
|
1354
1522
|
nextStep.status = 'in_progress';
|
|
1355
1523
|
nextStep.started_at = new Date().toISOString();
|
|
1356
1524
|
|
|
1357
|
-
// Auto-handoff to next assignee
|
|
1525
|
+
// Auto-handoff to next assignee (respecting permissions)
|
|
1358
1526
|
const agents = getAgents();
|
|
1359
|
-
if (nextStep.assignee && agents[nextStep.assignee] && nextStep.assignee !== registeredName) {
|
|
1527
|
+
if (nextStep.assignee && agents[nextStep.assignee] && nextStep.assignee !== registeredName && canSendTo(registeredName, nextStep.assignee)) {
|
|
1360
1528
|
const handoffContent = `[Workflow "${wf.name}"] Step ${nextStep.id} assigned to you: ${nextStep.description}`;
|
|
1361
1529
|
messageSeq++;
|
|
1362
1530
|
const msg = { id: generateId(), seq: messageSeq, from: registeredName, to: nextStep.assignee, content: handoffContent, timestamp: new Date().toISOString(), type: 'handoff' };
|
|
@@ -1480,7 +1648,7 @@ function toolListBranches() {
|
|
|
1480
1648
|
// --- MCP Server setup ---
|
|
1481
1649
|
|
|
1482
1650
|
const server = new Server(
|
|
1483
|
-
{ name: 'agent-bridge', version: '3.
|
|
1651
|
+
{ name: 'agent-bridge', version: '3.5.0' },
|
|
1484
1652
|
{ capabilities: { tools: {} } }
|
|
1485
1653
|
);
|
|
1486
1654
|
|
|
@@ -1858,6 +2026,27 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1858
2026
|
properties: {},
|
|
1859
2027
|
},
|
|
1860
2028
|
},
|
|
2029
|
+
{
|
|
2030
|
+
name: 'set_conversation_mode',
|
|
2031
|
+
description: 'Switch between "group" (free multi-agent chat with auto-broadcast, cooldown, and batched delivery) and "direct" (default point-to-point messaging). Group mode lets all agents see all messages and collaborate freely.',
|
|
2032
|
+
inputSchema: {
|
|
2033
|
+
type: 'object',
|
|
2034
|
+
properties: {
|
|
2035
|
+
mode: { type: 'string', description: '"group" for free multi-agent chat, "direct" for point-to-point (default)', enum: ['group', 'direct'] },
|
|
2036
|
+
},
|
|
2037
|
+
required: ['mode'],
|
|
2038
|
+
},
|
|
2039
|
+
},
|
|
2040
|
+
{
|
|
2041
|
+
name: 'listen_group',
|
|
2042
|
+
description: 'Listen for messages in group conversation mode. Returns ALL unconsumed messages as a batch (not just one), plus recent conversation context and hints about which agents are silent. Includes a random stagger delay (1-3s) to prevent all agents from responding simultaneously. Use this instead of listen() when in group mode.',
|
|
2043
|
+
inputSchema: {
|
|
2044
|
+
type: 'object',
|
|
2045
|
+
properties: {
|
|
2046
|
+
timeout_seconds: { type: 'number', description: 'Max seconds to wait for messages (default 300)' },
|
|
2047
|
+
},
|
|
2048
|
+
},
|
|
2049
|
+
},
|
|
1861
2050
|
],
|
|
1862
2051
|
};
|
|
1863
2052
|
});
|
|
@@ -1876,7 +2065,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1876
2065
|
result = toolListAgents();
|
|
1877
2066
|
break;
|
|
1878
2067
|
case 'send_message':
|
|
1879
|
-
result = toolSendMessage(args.content, args?.to, args?.reply_to);
|
|
2068
|
+
result = await toolSendMessage(args.content, args?.to, args?.reply_to);
|
|
1880
2069
|
break;
|
|
1881
2070
|
case 'wait_for_reply':
|
|
1882
2071
|
result = await toolWaitForReply(args?.timeout_seconds, args?.from);
|
|
@@ -1950,6 +2139,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1950
2139
|
case 'list_branches':
|
|
1951
2140
|
result = toolListBranches();
|
|
1952
2141
|
break;
|
|
2142
|
+
case 'set_conversation_mode':
|
|
2143
|
+
result = toolSetConversationMode(args.mode);
|
|
2144
|
+
break;
|
|
2145
|
+
case 'listen_group':
|
|
2146
|
+
result = await toolListenGroup(args?.timeout_seconds);
|
|
2147
|
+
break;
|
|
1953
2148
|
default:
|
|
1954
2149
|
return {
|
|
1955
2150
|
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
|
@@ -1977,6 +2172,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1977
2172
|
|
|
1978
2173
|
// Clean up agent registration on exit for instant status updates
|
|
1979
2174
|
process.on('exit', () => {
|
|
2175
|
+
unlockAgentsFile(); // Clean up any held lock
|
|
1980
2176
|
if (registeredName) {
|
|
1981
2177
|
try {
|
|
1982
2178
|
const agents = getAgents();
|
|
@@ -1994,7 +2190,7 @@ async function main() {
|
|
|
1994
2190
|
ensureDataDir();
|
|
1995
2191
|
const transport = new StdioServerTransport();
|
|
1996
2192
|
await server.connect(transport);
|
|
1997
|
-
console.error('Agent Bridge MCP server v3.
|
|
2193
|
+
console.error('Agent Bridge MCP server v3.5.0 running (29 tools)');
|
|
1998
2194
|
}
|
|
1999
2195
|
|
|
2000
2196
|
main().catch(console.error);
|