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/server.js CHANGED
@@ -72,7 +72,13 @@ function isGroupMode() {
72
72
  }
73
73
 
74
74
  function getGroupCooldown() {
75
- return getConfig().group_cooldown || 3000; // default 3s
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 ALL consumed IDs across all agents
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 only unconsumed messages (for direct messages, only the recipient consumes)
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 result = { success: true, message: `Registered as Agent ${name} (PID ${process.pid})` };
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
- // In group mode, auto-broadcast: also write to all other agents' queues
848
- // Skip if this message is already a response to a broadcast (prevents cascade)
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
- setListening(true);
1484
+ // Auto-detect direct mode and delegate to toolListen (prevents wrong-function bugs)
1485
+ if (!isGroupMode() && !isManagedMode()) {
1486
+ return toolListen();
1487
+ }
1381
1488
 
1382
- // Random stagger to prevent all agents from responding simultaneously (1-3s)
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 addressed to us or broadcast
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
- if (msg.to !== registeredName && msg.to !== '__all__') continue;
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
- acc[n] = agents[n].listening_since ? 'listening' : 'working';
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.7.0' },
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 (any name, e.g. "A", "Coder", "Reviewer"). Must be called before any other tool.',
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. Unlike wait_for_reply, this never times out it blocks until a message arrives. The agent should call listen() after finishing any task to stay available. After receiving a message, process it, respond, then call listen() again.',
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. Blocks indefinitely until messages arrive never times out. Returns ALL unconsumed messages as a batch, plus conversation context, agent statuses, and hints. After processing messages and responding, call listen_group() again immediately. This is how you stay in the conversation.',
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 the agent
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
- result._nudge = `You have ${pending.length} unread message(s) from the team. Finish your current task quickly, then call listen_group() to read them.`;
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.7.0 running (52 tools)');
3816
+ console.error('Agent Bridge MCP server v3.8.0 running (53 tools)');
3607
3817
  }
3608
3818
 
3609
3819
  main().catch(console.error);