let-them-talk 3.7.0 → 3.8.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 +42 -0
- package/README.md +3 -3
- package/cli.js +1 -1
- package/dashboard.html +7480 -7399
- package/dashboard.js +8 -3
- package/office/animation.js +1 -0
- package/office/campus-env.js +1 -1
- package/office/environment.js +60 -67
- package/office/index.js +50 -0
- package/office/monitors.js +2 -2
- package/office/player.js +436 -0
- package/office/spectator-camera.js +30 -21
- package/package.json +1 -1
- package/server.js +242 -32
package/server.js
CHANGED
|
@@ -72,7 +72,13 @@ function isGroupMode() {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
function getGroupCooldown() {
|
|
75
|
-
|
|
75
|
+
// Adaptive cooldown: scales with online agent count — max(500, N * 500)
|
|
76
|
+
// 2 agents = 1s, 3 = 1.5s, 4 = 2s, 6 = 3s, 10 = 5s
|
|
77
|
+
const configured = getConfig().group_cooldown;
|
|
78
|
+
if (configured) return configured; // respect explicit config
|
|
79
|
+
const agents = getAgents();
|
|
80
|
+
const aliveCount = Object.values(agents).filter(a => isPidAlive(a.pid, a.last_activity)).length;
|
|
81
|
+
return Math.max(500, aliveCount * 500);
|
|
76
82
|
}
|
|
77
83
|
|
|
78
84
|
// --- Managed conversation mode ---
|
|
@@ -364,21 +370,33 @@ function autoCompact() {
|
|
|
364
370
|
|
|
365
371
|
const messages = lines.map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
366
372
|
|
|
367
|
-
// Collect
|
|
373
|
+
// Collect consumed IDs — for __group__ messages, only check ALIVE agents
|
|
374
|
+
const agents = getAgents();
|
|
375
|
+
const aliveAgentNames = Object.keys(agents).filter(n => isPidAlive(agents[n].pid, agents[n].last_activity));
|
|
368
376
|
const allConsumed = new Set();
|
|
377
|
+
const perAgentConsumed = {};
|
|
369
378
|
if (fs.existsSync(DATA_DIR)) {
|
|
370
379
|
for (const f of fs.readdirSync(DATA_DIR)) {
|
|
371
380
|
if (f.startsWith('consumed-') && f.endsWith('.json')) {
|
|
381
|
+
const agentName = f.replace('consumed-', '').replace('.json', '');
|
|
372
382
|
try {
|
|
373
383
|
const ids = JSON.parse(fs.readFileSync(path.join(DATA_DIR, f), 'utf8'));
|
|
384
|
+
perAgentConsumed[agentName] = new Set(ids);
|
|
374
385
|
ids.forEach(id => allConsumed.add(id));
|
|
375
386
|
} catch {}
|
|
376
387
|
}
|
|
377
388
|
}
|
|
378
389
|
}
|
|
379
390
|
|
|
380
|
-
// Keep
|
|
391
|
+
// Keep messages that are NOT fully consumed
|
|
392
|
+
// For __group__ messages: consumed when ALL ALIVE agents have consumed it (dead agents don't block)
|
|
393
|
+
// For direct messages: consumed when the recipient has consumed it
|
|
381
394
|
const active = messages.filter(m => {
|
|
395
|
+
if (m.to === '__group__') {
|
|
396
|
+
// __group__: check if all alive agents (except sender) have consumed
|
|
397
|
+
return !aliveAgentNames.every(n => n === m.from || (perAgentConsumed[n] && perAgentConsumed[n].has(m.id)));
|
|
398
|
+
}
|
|
399
|
+
// Direct: standard check
|
|
382
400
|
if (!allConsumed.has(m.id)) return true;
|
|
383
401
|
return false;
|
|
384
402
|
});
|
|
@@ -451,7 +469,9 @@ function getUnconsumedMessages(agentName, fromFilter = null) {
|
|
|
451
469
|
const consumed = getConsumedIds(agentName);
|
|
452
470
|
const perms = getPermissions();
|
|
453
471
|
return messages.filter(m => {
|
|
454
|
-
if (m.to !== agentName) return false;
|
|
472
|
+
if (m.to !== agentName && m.to !== '__group__' && m.to !== '__all__') return false;
|
|
473
|
+
// Skip own group messages
|
|
474
|
+
if (m.to === '__group__' && m.from === agentName) return false;
|
|
455
475
|
if (consumed.has(m.id)) return false;
|
|
456
476
|
if (fromFilter && m.from !== fromFilter && !m.system) return false;
|
|
457
477
|
// Permission check: skip messages from senders this agent can't read
|
|
@@ -637,7 +657,43 @@ function toolRegister(name, provider = null) {
|
|
|
637
657
|
heartbeatInterval.unref(); // Don't prevent process exit
|
|
638
658
|
|
|
639
659
|
// Fire join event + recovery data for returning agents
|
|
640
|
-
const
|
|
660
|
+
const config = getConfig();
|
|
661
|
+
const mode = config.conversation_mode || 'direct';
|
|
662
|
+
const otherAgents = Object.keys(getAgents()).filter(n => n !== name);
|
|
663
|
+
|
|
664
|
+
const result = {
|
|
665
|
+
success: true,
|
|
666
|
+
message: `Registered as Agent ${name} (PID ${process.pid})`,
|
|
667
|
+
conversation_mode: mode,
|
|
668
|
+
agents_online: otherAgents,
|
|
669
|
+
guide: {
|
|
670
|
+
critical_rules: [
|
|
671
|
+
'AFTER EVERY ACTION YOU TAKE, call listen_group() (group/managed mode) or listen() (direct mode) immediately. This is how you receive messages. If you stop listening, you are invisible to the team.',
|
|
672
|
+
'Never send multiple messages in a row without calling listen_group() between them — you will miss responses.',
|
|
673
|
+
'Keep messages concise. 2-3 paragraphs max. No essays.',
|
|
674
|
+
'When you finish a task, report what you did AND what files you changed, then listen again.',
|
|
675
|
+
],
|
|
676
|
+
first_steps: mode === 'direct'
|
|
677
|
+
? '1. Call list_agents() to see who is online. 2. Send a message or call listen() to wait for one.'
|
|
678
|
+
: '1. Call get_briefing() for full project context. 2. Call listen_group() to join the conversation. 3. When you receive messages, respond and immediately call listen_group() again.',
|
|
679
|
+
tool_categories: {
|
|
680
|
+
'MESSAGING (always use these)': 'send_message, broadcast, listen_group (group/managed), listen (direct), check_messages, get_history, get_summary, handoff, share_file',
|
|
681
|
+
'TEAM COORDINATION': 'get_briefing (project overview), log_decision / get_decisions (prevent re-debating), kb_write / kb_read (shared knowledge), call_vote / cast_vote (team decisions)',
|
|
682
|
+
'TASK MANAGEMENT': 'create_task, update_task, list_tasks, declare_dependency, check_dependencies, suggest_task (what should I do next?)',
|
|
683
|
+
'PROGRESS & QUALITY': 'update_progress / get_progress (feature %), request_review / submit_review (code review), get_reputation (leaderboard)',
|
|
684
|
+
'FILE SAFETY': 'lock_file / unlock_file (prevent conflicts — ALWAYS lock before editing shared files)',
|
|
685
|
+
'PROFILES & WORKSPACES': 'update_profile, workspace_write / workspace_read (personal storage)',
|
|
686
|
+
'MANAGED MODE (if active)': 'claim_manager, yield_floor, set_phase — only the manager uses these',
|
|
687
|
+
},
|
|
688
|
+
patterns: {
|
|
689
|
+
'Starting work': 'get_briefing → check list_tasks → claim a task with update_task(id, "in_progress") → lock_file → do the work → unlock_file → update_task(id, "done") → listen_group',
|
|
690
|
+
'Sharing knowledge': 'kb_write("api-schema", "POST /auth → {token}") — so others can kb_read it without asking you',
|
|
691
|
+
'Making decisions': 'log_decision("Use PostgreSQL", "Better JSON support") — so no one re-debates this later',
|
|
692
|
+
'Disagreements': 'call_vote("Use Redis for caching?", ["yes", "no"]) — let the team decide democratically',
|
|
693
|
+
'Code review': 'request_review("src/auth.ts", "Check token expiry logic") — another agent will review and approve/request changes',
|
|
694
|
+
},
|
|
695
|
+
},
|
|
696
|
+
};
|
|
641
697
|
|
|
642
698
|
// Recovery: if this agent has prior data, include it
|
|
643
699
|
const myTasks = getTasks().filter(t => t.assignee === name && t.status !== 'done');
|
|
@@ -681,6 +737,10 @@ function setListening(isListening) {
|
|
|
681
737
|
const agents = getAgents();
|
|
682
738
|
if (agents[registeredName]) {
|
|
683
739
|
agents[registeredName].listening_since = isListening ? new Date().toISOString() : null;
|
|
740
|
+
// Persist last_listened_at so other agents can detect unresponsive agents
|
|
741
|
+
if (isListening) {
|
|
742
|
+
agents[registeredName].last_listened_at = new Date().toISOString();
|
|
743
|
+
}
|
|
684
744
|
saveAgents(agents);
|
|
685
745
|
}
|
|
686
746
|
} catch {}
|
|
@@ -703,6 +763,7 @@ function toolListAgents() {
|
|
|
703
763
|
status: !alive ? 'dead' : idleSeconds > 60 ? 'sleeping' : 'active',
|
|
704
764
|
listening_since: info.listening_since || null,
|
|
705
765
|
is_listening: !!(info.listening_since && alive),
|
|
766
|
+
last_listened_at: info.last_listened_at || null,
|
|
706
767
|
provider: info.provider || 'unknown',
|
|
707
768
|
branch: info.branch || 'main',
|
|
708
769
|
display_name: profile.display_name || name,
|
|
@@ -827,13 +888,16 @@ async function toolSendMessage(content, to = null, reply_to = null) {
|
|
|
827
888
|
}
|
|
828
889
|
|
|
829
890
|
messageSeq++;
|
|
891
|
+
// In group mode: rewrite to → __group__, original to becomes addressed_to
|
|
892
|
+
const isGroup = isGroupMode() && !isManagedMode();
|
|
830
893
|
const msg = {
|
|
831
894
|
id: generateId(),
|
|
832
895
|
seq: messageSeq,
|
|
833
896
|
from: registeredName,
|
|
834
|
-
to,
|
|
897
|
+
to: isGroup ? '__group__' : to,
|
|
835
898
|
content,
|
|
836
899
|
timestamp: new Date().toISOString(),
|
|
900
|
+
...(isGroup && to && { addressed_to: [to] }),
|
|
837
901
|
...(reply_to && { reply_to }),
|
|
838
902
|
...(thread_id && { thread_id }),
|
|
839
903
|
};
|
|
@@ -844,18 +908,8 @@ async function toolSendMessage(content, to = null, reply_to = null) {
|
|
|
844
908
|
touchActivity();
|
|
845
909
|
lastSentAt = Date.now();
|
|
846
910
|
|
|
847
|
-
//
|
|
848
|
-
//
|
|
849
|
-
// NEVER auto-broadcast in managed mode — manager controls communication flow
|
|
850
|
-
if (isGroupMode() && !isManagedMode() && !reply_to && !msg.broadcast) {
|
|
851
|
-
const otherRecipients = Object.keys(getAgents()).filter(n => n !== registeredName && n !== to);
|
|
852
|
-
for (const other of otherRecipients) {
|
|
853
|
-
if (!canSendTo(registeredName, other)) continue; // respect permissions
|
|
854
|
-
const broadcastMsg = { ...msg, id: generateId(), to: other, broadcast: true, original_to: to };
|
|
855
|
-
fs.appendFileSync(getMessagesFile(currentBranch), JSON.stringify(broadcastMsg) + '\n');
|
|
856
|
-
fs.appendFileSync(getHistoryFile(currentBranch), JSON.stringify(broadcastMsg) + '\n');
|
|
857
|
-
}
|
|
858
|
-
}
|
|
911
|
+
// Group mode: O(N) auto-broadcast REMOVED. Messages now use __group__ single-write.
|
|
912
|
+
// The to→__group__ rewrite happens above when the message is created.
|
|
859
913
|
|
|
860
914
|
// Managed mode: auto-advance turns after non-manager sends
|
|
861
915
|
if (isManagedMode()) {
|
|
@@ -906,6 +960,12 @@ async function toolSendMessage(content, to = null, reply_to = null) {
|
|
|
906
960
|
result.note = `Agent "${to}" is currently working (not in listen mode). Message queued — they'll see it when they finish their current task and call listen_group().`;
|
|
907
961
|
}
|
|
908
962
|
|
|
963
|
+
// Mode awareness hint: warn if agent seems to be in wrong mode
|
|
964
|
+
const currentMode = getConfig().conversation_mode || 'direct';
|
|
965
|
+
if (currentMode === 'group' || currentMode === 'managed') {
|
|
966
|
+
result.mode_hint = `You're in ${currentMode} mode. Use listen_group() (or listen() — both auto-detect) to stay in the conversation.`;
|
|
967
|
+
}
|
|
968
|
+
|
|
909
969
|
// Nudge: check if THIS agent has unread messages waiting
|
|
910
970
|
const myPending = getUnconsumedMessages(registeredName);
|
|
911
971
|
if (myPending.length > 0) {
|
|
@@ -942,6 +1002,32 @@ function toolBroadcast(content) {
|
|
|
942
1002
|
}
|
|
943
1003
|
|
|
944
1004
|
ensureDataDir();
|
|
1005
|
+
|
|
1006
|
+
// In group mode: single __group__ write instead of per-agent copies
|
|
1007
|
+
if (isGroupMode() && !isManagedMode()) {
|
|
1008
|
+
messageSeq++;
|
|
1009
|
+
const msg = {
|
|
1010
|
+
id: generateId(),
|
|
1011
|
+
seq: messageSeq,
|
|
1012
|
+
from: registeredName,
|
|
1013
|
+
to: '__group__',
|
|
1014
|
+
content,
|
|
1015
|
+
timestamp: new Date().toISOString(),
|
|
1016
|
+
broadcast: true,
|
|
1017
|
+
};
|
|
1018
|
+
fs.appendFileSync(getMessagesFile(currentBranch), JSON.stringify(msg) + '\n');
|
|
1019
|
+
fs.appendFileSync(getHistoryFile(currentBranch), JSON.stringify(msg) + '\n');
|
|
1020
|
+
touchActivity();
|
|
1021
|
+
lastSentAt = Date.now();
|
|
1022
|
+
const aliveOthers = otherAgents.filter(n => { const a = agents[n]; return isPidAlive(a.pid, a.last_activity); });
|
|
1023
|
+
const result = { success: true, messageId: msg.id, recipient_count: aliveOthers.length, sent_to: aliveOthers.map(n => ({ to: n, messageId: msg.id })) };
|
|
1024
|
+
// Nudge for own unread messages
|
|
1025
|
+
const myPending = getUnconsumedMessages(registeredName);
|
|
1026
|
+
if (myPending.length > 0) { result.you_have_messages = myPending.length; result.urgent = `You have ${myPending.length} unread message(s). Call listen_group() soon.`; }
|
|
1027
|
+
return result;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// Direct/managed mode: per-agent writes (original behavior)
|
|
945
1031
|
const ids = [];
|
|
946
1032
|
const skipped = [];
|
|
947
1033
|
for (const to of otherAgents) {
|
|
@@ -1089,6 +1175,12 @@ async function toolListen(from = null) {
|
|
|
1089
1175
|
return { error: 'You must call register() first' };
|
|
1090
1176
|
}
|
|
1091
1177
|
|
|
1178
|
+
// Auto-detect group/managed mode and delegate to toolListenGroup
|
|
1179
|
+
// This prevents agents from calling the "wrong" listen function
|
|
1180
|
+
if (isGroupMode() || isManagedMode()) {
|
|
1181
|
+
return toolListenGroup();
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1092
1184
|
setListening(true);
|
|
1093
1185
|
|
|
1094
1186
|
// Check for existing unconsumed messages first
|
|
@@ -1232,6 +1324,11 @@ function toolSetConversationMode(mode) {
|
|
|
1232
1324
|
}
|
|
1233
1325
|
saveConfig(config);
|
|
1234
1326
|
|
|
1327
|
+
// Notify all agents about mode change (managed mode already broadcasts above)
|
|
1328
|
+
if (mode !== 'managed') {
|
|
1329
|
+
broadcastSystemMessage(`[MODE] Conversation switched to ${mode} mode by ${registeredName}. ${mode === 'group' ? 'All messages are now shared with everyone.' : 'Messages are now point-to-point.'}`, registeredName);
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1235
1332
|
const messages = {
|
|
1236
1333
|
group: 'Group mode enabled. Use listen_group() to receive batched messages. All messages are shared with everyone.',
|
|
1237
1334
|
direct: 'Direct mode enabled. Use listen() for point-to-point messaging.',
|
|
@@ -1374,14 +1471,22 @@ function toolSetPhase(phase) {
|
|
|
1374
1471
|
};
|
|
1375
1472
|
}
|
|
1376
1473
|
|
|
1474
|
+
// Deterministic stagger delay based on agent name (500-1500ms)
|
|
1475
|
+
// Same agent always gets the same delay, making response ordering predictable
|
|
1476
|
+
function hashStagger(name) {
|
|
1477
|
+
const hash = name.split('').reduce((h, c) => h + c.charCodeAt(0), 0);
|
|
1478
|
+
return 500 + (hash * 137) % 1000; // 0.5-1.5s range
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1377
1481
|
async function toolListenGroup() {
|
|
1378
1482
|
if (!registeredName) return { error: 'You must call register() first' };
|
|
1379
1483
|
|
|
1380
|
-
|
|
1484
|
+
// Auto-detect direct mode and delegate to toolListen (prevents wrong-function bugs)
|
|
1485
|
+
if (!isGroupMode() && !isManagedMode()) {
|
|
1486
|
+
return toolListen();
|
|
1487
|
+
}
|
|
1381
1488
|
|
|
1382
|
-
|
|
1383
|
-
const stagger = 1000 + Math.random() * 2000;
|
|
1384
|
-
await new Promise(r => setTimeout(r, stagger));
|
|
1489
|
+
setListening(true);
|
|
1385
1490
|
|
|
1386
1491
|
const consumed = getConsumedIds(registeredName);
|
|
1387
1492
|
|
|
@@ -1390,12 +1495,14 @@ async function toolListenGroup() {
|
|
|
1390
1495
|
const chunkDeadline = Date.now() + 300000;
|
|
1391
1496
|
|
|
1392
1497
|
while (Date.now() < chunkDeadline) {
|
|
1393
|
-
// Collect ALL unconsumed messages
|
|
1498
|
+
// Collect ALL unconsumed messages: direct to us, __group__ (everyone), __all__, or system
|
|
1394
1499
|
const messages = readJsonl(getMessagesFile(currentBranch));
|
|
1395
1500
|
const batch = [];
|
|
1396
1501
|
for (const msg of messages) {
|
|
1397
1502
|
if (consumed.has(msg.id)) continue;
|
|
1398
|
-
|
|
1503
|
+
// Skip own messages in group mode (agent already knows what it sent)
|
|
1504
|
+
if (msg.to === '__group__' && msg.from === registeredName) { consumed.add(msg.id); continue; }
|
|
1505
|
+
if (msg.to !== registeredName && msg.to !== '__all__' && msg.to !== '__group__') continue;
|
|
1399
1506
|
// Permission check
|
|
1400
1507
|
const perms = getPermissions();
|
|
1401
1508
|
if (perms[registeredName] && perms[registeredName].can_read) {
|
|
@@ -1412,6 +1519,42 @@ async function toolListenGroup() {
|
|
|
1412
1519
|
touchActivity();
|
|
1413
1520
|
setListening(false);
|
|
1414
1521
|
|
|
1522
|
+
// Post-receive stagger: deterministic delay based on agent name
|
|
1523
|
+
// Prevents all agents from responding simultaneously to the same batch
|
|
1524
|
+
const staggerMs = hashStagger(registeredName);
|
|
1525
|
+
if (staggerMs > 0) {
|
|
1526
|
+
await new Promise(r => setTimeout(r, staggerMs));
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
// Sort batch by priority: system > threaded replies > direct > broadcast
|
|
1530
|
+
// Within each category, maintain chronological order
|
|
1531
|
+
function messagePriority(m) {
|
|
1532
|
+
if (m.system || m.from === '__system__') return 0;
|
|
1533
|
+
if (m.reply_to || m.thread_id) return 1;
|
|
1534
|
+
if (!m.broadcast) return 2;
|
|
1535
|
+
return 3;
|
|
1536
|
+
}
|
|
1537
|
+
batch.sort((a, b) => {
|
|
1538
|
+
const pa = messagePriority(a), pb = messagePriority(b);
|
|
1539
|
+
if (pa !== pb) return pa - pb;
|
|
1540
|
+
return new Date(a.timestamp) - new Date(b.timestamp);
|
|
1541
|
+
});
|
|
1542
|
+
|
|
1543
|
+
// Build batch summary for triage
|
|
1544
|
+
const summaryCounts = {};
|
|
1545
|
+
for (const m of batch) {
|
|
1546
|
+
const type = m.system || m.from === '__system__' ? 'system'
|
|
1547
|
+
: m.broadcast ? 'broadcast' : (m.reply_to || m.thread_id) ? 'thread' : 'direct';
|
|
1548
|
+
const key = `${m.from}:${type}`;
|
|
1549
|
+
summaryCounts[key] = (summaryCounts[key] || 0) + 1;
|
|
1550
|
+
}
|
|
1551
|
+
const summaryParts = [];
|
|
1552
|
+
for (const [key, count] of Object.entries(summaryCounts)) {
|
|
1553
|
+
const [from, type] = key.split(':');
|
|
1554
|
+
summaryParts.push(`${count} ${type} from ${from}`);
|
|
1555
|
+
}
|
|
1556
|
+
const batchSummary = `${batch.length} messages: ${summaryParts.join(', ')}`;
|
|
1557
|
+
|
|
1415
1558
|
// Get recent history for context
|
|
1416
1559
|
const history = readJsonl(getHistoryFile(currentBranch));
|
|
1417
1560
|
const recentHistory = history.slice(-20).map(m => ({
|
|
@@ -1437,14 +1580,33 @@ async function toolListenGroup() {
|
|
|
1437
1580
|
...(ageSec > 30 && { delayed: true }),
|
|
1438
1581
|
...(m.reply_to && { reply_to: m.reply_to }),
|
|
1439
1582
|
...(m.thread_id && { thread_id: m.thread_id }),
|
|
1583
|
+
// addressed_to hint for group messages
|
|
1584
|
+
...(m.addressed_to && { addressed_to: m.addressed_to }),
|
|
1585
|
+
...(m.to === '__group__' && {
|
|
1586
|
+
addressed_to_you: !m.addressed_to || m.addressed_to.includes(registeredName),
|
|
1587
|
+
should_respond: !m.addressed_to || m.addressed_to.includes(registeredName),
|
|
1588
|
+
}),
|
|
1440
1589
|
};
|
|
1441
1590
|
}),
|
|
1442
1591
|
message_count: batch.length,
|
|
1592
|
+
batch_summary: batchSummary,
|
|
1443
1593
|
context: recentHistory,
|
|
1444
1594
|
agents_online: agentNames.length,
|
|
1445
1595
|
agents_silent: silent,
|
|
1446
1596
|
agents_status: agentNames.reduce(function(acc, n) {
|
|
1447
|
-
|
|
1597
|
+
if (agents[n].listening_since) {
|
|
1598
|
+
acc[n] = 'listening';
|
|
1599
|
+
} else {
|
|
1600
|
+
// Check for unresponsive: not listening, >2min since last listen, has pending messages
|
|
1601
|
+
const lastListened = agents[n].last_listened_at;
|
|
1602
|
+
const sinceLastListen = lastListened ? Date.now() - new Date(lastListened).getTime() : Infinity;
|
|
1603
|
+
const pendingForAgent = getUnconsumedMessages(n);
|
|
1604
|
+
if (sinceLastListen > 120000 && pendingForAgent.length > 0) {
|
|
1605
|
+
acc[n] = 'unresponsive';
|
|
1606
|
+
} else {
|
|
1607
|
+
acc[n] = 'working';
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1448
1610
|
return acc;
|
|
1449
1611
|
}, {}),
|
|
1450
1612
|
hint: silent.length > 0
|
|
@@ -2238,6 +2400,34 @@ function fireEvent(eventName, data) {
|
|
|
2238
2400
|
}
|
|
2239
2401
|
}
|
|
2240
2402
|
|
|
2403
|
+
function toolGetGuide() {
|
|
2404
|
+
if (!registeredName) return { error: 'You must call register() first' };
|
|
2405
|
+
const config = getConfig();
|
|
2406
|
+
const mode = config.conversation_mode || 'direct';
|
|
2407
|
+
return {
|
|
2408
|
+
your_name: registeredName,
|
|
2409
|
+
conversation_mode: mode,
|
|
2410
|
+
critical_rules: [
|
|
2411
|
+
'AFTER EVERY ACTION, call listen_group() (group/managed) or listen() (direct). This is how you receive messages.',
|
|
2412
|
+
'Never send multiple messages without listening between them.',
|
|
2413
|
+
'Keep messages concise — 2-3 paragraphs max.',
|
|
2414
|
+
'When you finish a task, report what you did + files changed, then listen again.',
|
|
2415
|
+
'ALWAYS lock_file() before editing shared files, unlock_file() when done.',
|
|
2416
|
+
'Use log_decision() for any team decisions so they are not re-debated.',
|
|
2417
|
+
'Use kb_write() to share knowledge (API specs, conventions) so others can read without asking.',
|
|
2418
|
+
],
|
|
2419
|
+
tool_categories: {
|
|
2420
|
+
'MESSAGING': 'send_message, broadcast, listen_group, listen, check_messages, get_history, get_summary, handoff, share_file',
|
|
2421
|
+
'COORDINATION': 'get_briefing, log_decision, get_decisions, kb_write, kb_read, kb_list, call_vote, cast_vote, vote_status',
|
|
2422
|
+
'TASKS': 'create_task, update_task, list_tasks, declare_dependency, check_dependencies, suggest_task',
|
|
2423
|
+
'QUALITY': 'update_progress, get_progress, request_review, submit_review, get_reputation',
|
|
2424
|
+
'SAFETY': 'lock_file, unlock_file',
|
|
2425
|
+
'MANAGED MODE': 'claim_manager, yield_floor, set_phase (manager only)',
|
|
2426
|
+
},
|
|
2427
|
+
workflow: '1. get_briefing → 2. check list_tasks/suggest_task → 3. claim task → 4. lock_file → 5. do work → 6. unlock_file → 7. update_task done → 8. listen_group',
|
|
2428
|
+
};
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2241
2431
|
function toolGetBriefing() {
|
|
2242
2432
|
if (!registeredName) return { error: 'You must call register() first' };
|
|
2243
2433
|
|
|
@@ -2798,7 +2988,7 @@ function toolSuggestTask() {
|
|
|
2798
2988
|
// --- MCP Server setup ---
|
|
2799
2989
|
|
|
2800
2990
|
const server = new Server(
|
|
2801
|
-
{ name: 'agent-bridge', version: '3.
|
|
2991
|
+
{ name: 'agent-bridge', version: '3.8.0' },
|
|
2802
2992
|
{ capabilities: { tools: {} } }
|
|
2803
2993
|
);
|
|
2804
2994
|
|
|
@@ -2807,7 +2997,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2807
2997
|
tools: [
|
|
2808
2998
|
{
|
|
2809
2999
|
name: 'register',
|
|
2810
|
-
description: 'Register this agent\'s identity
|
|
3000
|
+
description: 'Register this agent\'s identity. Must be called first. Returns a collaboration guide with all tool categories, critical rules, and workflow patterns — READ IT CAREFULLY before doing anything else. Then call get_briefing() for project context, then listen_group() to join the conversation.',
|
|
2811
3001
|
inputSchema: {
|
|
2812
3002
|
type: 'object',
|
|
2813
3003
|
properties: {
|
|
@@ -2886,7 +3076,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2886
3076
|
},
|
|
2887
3077
|
{
|
|
2888
3078
|
name: 'listen',
|
|
2889
|
-
description: 'Listen for messages indefinitely.
|
|
3079
|
+
description: 'Listen for messages indefinitely. Auto-detects conversation mode: in group/managed mode, behaves like listen_group() (returns batched messages with agent statuses). In direct mode, returns one message at a time. Either listen() or listen_group() works in any mode — they auto-delegate to the correct behavior.',
|
|
2890
3080
|
inputSchema: {
|
|
2891
3081
|
type: 'object',
|
|
2892
3082
|
properties: {
|
|
@@ -3209,13 +3399,18 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
3209
3399
|
},
|
|
3210
3400
|
{
|
|
3211
3401
|
name: 'listen_group',
|
|
3212
|
-
description: 'Listen for messages in group or managed conversation mode.
|
|
3402
|
+
description: 'Listen for messages in group or managed conversation mode. Auto-detects mode: in direct mode, behaves like listen(). Returns ALL unconsumed messages as a sorted batch (system > threaded > direct > broadcast), plus batch_summary, agent statuses, and hints. Either listen() or listen_group() works in any mode — they auto-delegate. Call again immediately after responding.',
|
|
3213
3403
|
inputSchema: {
|
|
3214
3404
|
type: 'object',
|
|
3215
3405
|
properties: {},
|
|
3216
3406
|
},
|
|
3217
3407
|
},
|
|
3218
3408
|
// --- Briefing & Recovery ---
|
|
3409
|
+
{
|
|
3410
|
+
name: 'get_guide',
|
|
3411
|
+
description: 'Get the collaboration guide — all tool categories, critical rules, and workflow patterns. Call this if you are unsure how to use the tools or need a refresher on best practices.',
|
|
3412
|
+
inputSchema: { type: 'object', properties: {} },
|
|
3413
|
+
},
|
|
3219
3414
|
{
|
|
3220
3415
|
name: 'get_briefing',
|
|
3221
3416
|
description: 'Get a full project briefing: who is online, active tasks, recent decisions, knowledge base, locked files, progress, and project files. Call this when joining a project or after being away. One call = fully onboarded.',
|
|
@@ -3455,6 +3650,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3455
3650
|
case 'listen_group':
|
|
3456
3651
|
result = await toolListenGroup();
|
|
3457
3652
|
break;
|
|
3653
|
+
case 'get_guide':
|
|
3654
|
+
result = toolGetGuide();
|
|
3655
|
+
break;
|
|
3458
3656
|
case 'get_briefing':
|
|
3459
3657
|
result = toolGetBriefing();
|
|
3460
3658
|
break;
|
|
@@ -3538,14 +3736,26 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3538
3736
|
};
|
|
3539
3737
|
}
|
|
3540
3738
|
|
|
3541
|
-
// Global hook: on non-listen tools, check for pending messages and nudge
|
|
3739
|
+
// Global hook: on non-listen tools, check for pending messages and nudge with escalating urgency
|
|
3542
3740
|
const listenTools = ['listen', 'listen_group', 'listen_codex', 'wait_for_reply', 'check_messages'];
|
|
3543
3741
|
if (registeredName && !listenTools.includes(name) && (isGroupMode() || isManagedMode())) {
|
|
3544
3742
|
try {
|
|
3545
3743
|
const pending = getUnconsumedMessages(registeredName);
|
|
3546
3744
|
if (pending.length > 0 && !result.you_have_messages) {
|
|
3547
3745
|
result._pending_messages = pending.length;
|
|
3548
|
-
|
|
3746
|
+
// Escalate urgency based on oldest pending message age
|
|
3747
|
+
const oldestAge = pending.reduce((max, m) => {
|
|
3748
|
+
const age = Date.now() - new Date(m.timestamp).getTime();
|
|
3749
|
+
return age > max ? age : max;
|
|
3750
|
+
}, 0);
|
|
3751
|
+
const ageSec = Math.round(oldestAge / 1000);
|
|
3752
|
+
if (ageSec > 120) {
|
|
3753
|
+
result._nudge = `CRITICAL: ${pending.length} message(s) waiting ${Math.round(ageSec / 60)}+ min. Team is likely blocked on you. Call listen_group() NOW.`;
|
|
3754
|
+
} else if (ageSec > 30) {
|
|
3755
|
+
result._nudge = `URGENT: ${pending.length} message(s) waiting ${ageSec}s. Team may be blocked. Call listen_group() soon.`;
|
|
3756
|
+
} else {
|
|
3757
|
+
result._nudge = `You have ${pending.length} unread message(s). Call listen_group() after this to read them.`;
|
|
3758
|
+
}
|
|
3549
3759
|
}
|
|
3550
3760
|
} catch {}
|
|
3551
3761
|
}
|
|
@@ -3603,7 +3813,7 @@ async function main() {
|
|
|
3603
3813
|
ensureDataDir();
|
|
3604
3814
|
const transport = new StdioServerTransport();
|
|
3605
3815
|
await server.connect(transport);
|
|
3606
|
-
console.error('Agent Bridge MCP server v3.
|
|
3816
|
+
console.error('Agent Bridge MCP server v3.8.0 running (53 tools)');
|
|
3607
3817
|
}
|
|
3608
3818
|
|
|
3609
3819
|
main().catch(console.error);
|