let-them-talk 3.6.1 → 3.6.2

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.
@@ -2,6 +2,7 @@ import * as THREE from 'three';
2
2
  import { CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
3
3
  import { S } from './state.js';
4
4
  import { FLOOR_W, FLOOR_D, DESK_POSITIONS, RECEPTION_POS, ENVS, DRESSING_ROOM_POS, REST_AREA_POS } from './constants.js';
5
+ import { buildCampusEnvironment, getCampusDeskPositions } from './campus-env.js';
5
6
 
6
7
  export function buildEnvironment() {
7
8
  if (S.furnitureGroup) {
@@ -27,6 +28,15 @@ export function buildEnvironment() {
27
28
  }
28
29
  S.furnitureGroup = new THREE.Group();
29
30
  S.deskMeshes = [];
31
+
32
+ if (S.currentEnv === 'campus') {
33
+ buildCampusEnvironment();
34
+ // Store campus desk positions for agent assignment
35
+ S._campusDeskPositions = getCampusDeskPositions();
36
+ S.scene.add(S.furnitureGroup);
37
+ return;
38
+ }
39
+
30
40
  var env = ENVS[S.currentEnv] || ENVS.modern;
31
41
 
32
42
  buildFloor(env);
package/office/index.js CHANGED
@@ -5,7 +5,7 @@ import { DESK_POSITIONS, DRESSING_ROOM_POS, DRESSING_ROOM_ENTRANCE, REST_AREA_PO
5
5
  import { initScene } from './scene.js';
6
6
  import { buildEnvironment, updateTVScreen } from './environment.js';
7
7
  import { updateAgent } from './animation.js';
8
- import { syncAgents, processMessages, walkTo, showBubble } from './agents.js';
8
+ import { syncAgents, processMessages, walkTo, navigateTo, showBubble } from './agents.js';
9
9
  // Side-effect: registers window.officeGetAppearance
10
10
  import './appearance.js';
11
11
 
@@ -141,8 +141,8 @@ function executeCommand(agentName, action) {
141
141
  agent.location = 'walking';
142
142
  agent.isSitting = false;
143
143
  showBubble(agent, 'Going to change!');
144
- walkTo(agent, DRESSING_ROOM_ENTRANCE.x, DRESSING_ROOM_ENTRANCE.z, function() {
145
- walkTo(agent, DRESSING_ROOM_POS.x, DRESSING_ROOM_POS.z, function() {
144
+ navigateTo(agent, DRESSING_ROOM_ENTRANCE.x, DRESSING_ROOM_ENTRANCE.z, function() {
145
+ navigateTo(agent, DRESSING_ROOM_POS.x, DRESSING_ROOM_POS.z, function() {
146
146
  agent.location = 'dressing_room';
147
147
  agent.isSitting = false;
148
148
  showBubble(agent, 'Time for a new look!');
@@ -161,8 +161,8 @@ function executeCommand(agentName, action) {
161
161
  agent.location = 'walking';
162
162
  agent.isSitting = false;
163
163
  showBubble(agent, 'Need a break...');
164
- walkTo(agent, REST_AREA_ENTRANCE.x, REST_AREA_ENTRANCE.z, function() {
165
- walkTo(agent, REST_AREA_POS.x, REST_AREA_POS.z, function() {
164
+ navigateTo(agent, REST_AREA_ENTRANCE.x, REST_AREA_ENTRANCE.z, function() {
165
+ navigateTo(agent, REST_AREA_POS.x, REST_AREA_POS.z, function() {
166
166
  agent.location = 'rest';
167
167
  agent.state = 'sleeping';
168
168
  agent.isSitting = false;
@@ -176,7 +176,7 @@ function executeCommand(agentName, action) {
176
176
  agent.state = 'active';
177
177
  agent.isSitting = false;
178
178
  showBubble(agent, 'Back to work!');
179
- walkTo(agent, agent.deskPos.x, agent.deskPos.z + 0.7, function() {
179
+ navigateTo(agent, agent.deskPos.x, agent.deskPos.z + 0.7, function() {
180
180
  agent.location = 'desk';
181
181
  agent.registered = true;
182
182
  });
@@ -201,8 +201,8 @@ function waitForEditorClose(agent) {
201
201
  if (agent.location === 'dressing_room') {
202
202
  agent.location = 'walking';
203
203
  showBubble(agent, 'Looking good!');
204
- walkTo(agent, DRESSING_ROOM_ENTRANCE.x, DRESSING_ROOM_ENTRANCE.z, function() {
205
- walkTo(agent, agent.deskPos.x, agent.deskPos.z + 0.7, function() {
204
+ navigateTo(agent, DRESSING_ROOM_ENTRANCE.x, DRESSING_ROOM_ENTRANCE.z, function() {
205
+ navigateTo(agent, agent.deskPos.x, agent.deskPos.z + 0.7, function() {
206
206
  agent.location = 'desk';
207
207
  agent.registered = true;
208
208
  });
@@ -226,6 +226,29 @@ function animate() {
226
226
  updateAgent(S.agents3d[name], dt, time);
227
227
  }
228
228
 
229
+ // Hide roof when camera is above ceiling height
230
+ if (S._roofGroup) {
231
+ S._roofGroup.visible = S.camera.position.y < 6.5;
232
+ }
233
+
234
+ // Manager office door animation — opens when agent is near the door
235
+ if (S._managerDoor && S._managerOfficePos) {
236
+ var doorX = S._managerOfficePos.x;
237
+ var doorZ = S._managerOfficePos.z - 3.5; // front of office
238
+ var shouldOpen = false;
239
+ for (var an in S.agents3d) {
240
+ var ag = S.agents3d[an];
241
+ if (ag.target || ag.location === 'walking') {
242
+ var adx = ag.pos.x - doorX;
243
+ var adz = ag.pos.z - doorZ;
244
+ if (Math.sqrt(adx * adx + adz * adz) < 3) { shouldOpen = true; break; }
245
+ }
246
+ }
247
+ S._managerDoorOpen = shouldOpen ? 1 : 0;
248
+ S._managerDoorLerp += (S._managerDoorOpen - S._managerDoorLerp) * Math.min(1, dt * 4);
249
+ S._managerDoor.position.x = S._managerDoorLerp * 1.3; // slide open to the right
250
+ }
251
+
229
252
  // Update TV screen every ~0.5s for smooth ticker
230
253
  if (!S._tvTimer) S._tvTimer = 0;
231
254
  S._tvTimer += dt;
@@ -309,17 +332,24 @@ window.office3dSetEnvironment = function(env) {
309
332
  if (env === S.currentEnv) return;
310
333
  S.currentEnv = env;
311
334
  if (S.scene) {
312
- buildEnvironment();
313
- var i = 0;
335
+ // Remove all existing agents so they get recreated with proper desk assignments
314
336
  for (var name in S.agents3d) {
315
337
  var agent = S.agents3d[name];
316
- if (i < DESK_POSITIONS.length) {
317
- agent.deskIdx = i;
318
- agent.deskPos = { x: DESK_POSITIONS[i].x, z: DESK_POSITIONS[i].z };
319
- walkTo(agent, agent.deskPos.x, agent.deskPos.z + 0.7);
320
- }
321
- i++;
338
+ S.scene.remove(agent.parts.group);
339
+ agent.parts.group.traverse(function(child) {
340
+ if (child.geometry) child.geometry.dispose();
341
+ if (child.material) {
342
+ if (child.material.map) child.material.map.dispose();
343
+ child.material.dispose();
344
+ }
345
+ });
322
346
  }
347
+ S.agents3d = {};
348
+ S.lastProcessedMsg = 0;
349
+ buildEnvironment();
350
+ // syncAgents will recreate all agents with correct desk assignments
351
+ syncAgents();
352
+ processMessages();
323
353
  }
324
354
  };
325
355
 
@@ -0,0 +1,263 @@
1
+ import { S } from './state.js';
2
+
3
+ // ============================================================
4
+ // NAVIGATION SYSTEM — Waypoint graph pathfinding
5
+ // Agents walk along connected waypoints to avoid walls/objects
6
+ // ============================================================
7
+
8
+ // Manager office geometry reference:
9
+ // Office group at (12, 5), size 8x7, walls at:
10
+ // Front (door): z = 5 - 3.5 = 1.5 (door at center x=12)
11
+ // Back: z = 5 + 3.5 = 8.5
12
+ // Left: x = 12 - 4 = 8
13
+ // Right: x = 12 + 4 = 16
14
+ // Glass partition at z = -7 (between workspace and rec)
15
+ // Glass partition at x = -8 (between designer and main)
16
+
17
+ var CAMPUS_WAYPOINTS = [
18
+ // === LOBBY / ENTRANCE ===
19
+ { id: 'spawn', x: 0, z: 14 },
20
+ { id: 'lobby', x: 0, z: 10 },
21
+ { id: 'lobby_left', x: -6, z: 10 },
22
+ { id: 'lobby_right', x: 6, z: 10 },
23
+
24
+ // === MAIN CORRIDOR (runs along z=6, above workspace) ===
25
+ { id: 'corr_L', x: -8, z: 7 },
26
+ { id: 'corr_CL', x: -3, z: 7 },
27
+ { id: 'corr_C', x: 0, z: 7 },
28
+ { id: 'corr_CR', x: 3, z: 7 },
29
+ { id: 'corr_R', x: 7, z: 7 },
30
+
31
+ // === WORKSPACE ZONE (center area, between glass partitions) ===
32
+ { id: 'work_N', x: 0, z: 4 }, // north end
33
+ { id: 'work_NW', x: -5, z: 4 },
34
+ { id: 'work_NE', x: 5, z: 4 },
35
+ { id: 'work_W', x: -5, z: 0 },
36
+ { id: 'work_C', x: 0, z: 0 },
37
+ { id: 'work_E', x: 5, z: 0 },
38
+ { id: 'work_SW', x: -5, z: -3 },
39
+ { id: 'work_S', x: 0, z: -5 },
40
+ { id: 'work_SE', x: 5, z: -3 },
41
+
42
+ // === DESIGNER WING (left of glass partition x=-8) ===
43
+ { id: 'design_gate', x: -8, z: 3 }, // gap in partition
44
+ { id: 'design_N', x: -12, z: 3 },
45
+ { id: 'design_C', x: -12.5, z: 0 },
46
+ { id: 'design_S', x: -12, z: -3 },
47
+
48
+ // === MANAGER OFFICE (right side, enclosed glass room) ===
49
+ // Office walls: left x=8, right x=16, front z=1.5, back z=8.5
50
+ // Door at front wall center (x=12, z=1.5)
51
+ // Path must go AROUND the left-front corner, then to door from outside
52
+ { id: 'mgr_hallway', x: 7, z: 3 }, // south of corridor, OUTSIDE left wall (x<8)
53
+ { id: 'mgr_corner', x: 7, z: 0 }, // past the front-left corner (x<8, z<1.5)
54
+ { id: 'mgr_outside', x: 12, z: 0 }, // in front of door, OUTSIDE front wall (z<1.5)
55
+ { id: 'mgr_doorstep', x: 12, z: 1.5 }, // at the door threshold (triggers door open)
56
+ { id: 'mgr_entry', x: 12, z: 3 }, // just inside the door
57
+ { id: 'mgr_center', x: 12, z: 5 }, // middle of office
58
+ { id: 'mgr_desk', x: 12, z: 7 }, // at the desk/chair
59
+
60
+ // === BACK ZONE CORRIDOR (runs along z=-7 to z=-8, south of glass partition) ===
61
+ { id: 'back_gate', x: 0, z: -6.5 }, // gap in glass partition
62
+ { id: 'back_L', x: -8, z: -8 },
63
+ { id: 'back_C', x: 0, z: -8 },
64
+ { id: 'back_R', x: 8, z: -8 },
65
+
66
+ // === BAR (back left) ===
67
+ { id: 'bar_entry', x: -10, z: -10 },
68
+ { id: 'bar_center', x: -14, z: -12 },
69
+
70
+ // === REC CENTER (back center) ===
71
+ { id: 'rec_entry', x: 0, z: -10 },
72
+ { id: 'rec_center', x: 0, z: -12 },
73
+
74
+ // === GYM (back right) ===
75
+ { id: 'gym_entry', x: 10, z: -10 },
76
+ { id: 'gym_center', x: 14, z: -12 },
77
+
78
+ // === MEZZANINE / STAIRS ===
79
+ { id: 'stairs_bot', x: 20, z: -5 },
80
+ { id: 'stairs_top', x: 20, z: -8 },
81
+ { id: 'mezz_C', x: 0, z: -13 },
82
+
83
+ // === REST / DRESSING (right wing, for old office compat) ===
84
+ { id: 'rest_entry', x: 7.5, z: -5.5 },
85
+ { id: 'dress_entry', x: 7.5, z: -1.5 },
86
+ ];
87
+
88
+ var CAMPUS_CONNECTIONS = [
89
+ // Lobby connections
90
+ ['spawn', 'lobby'],
91
+ ['lobby', 'lobby_left'],
92
+ ['lobby', 'lobby_right'],
93
+ ['lobby', 'corr_C'],
94
+ ['lobby_left', 'corr_L'],
95
+ ['lobby_right', 'corr_R'],
96
+
97
+ // Main corridor (horizontal)
98
+ ['corr_L', 'corr_CL'],
99
+ ['corr_CL', 'corr_C'],
100
+ ['corr_C', 'corr_CR'],
101
+ ['corr_CR', 'corr_R'],
102
+
103
+ // Corridor → workspace
104
+ ['corr_C', 'work_N'],
105
+ ['corr_CL', 'work_NW'],
106
+ ['corr_CR', 'work_NE'],
107
+
108
+ // Workspace grid
109
+ ['work_N', 'work_NW'],
110
+ ['work_N', 'work_NE'],
111
+ ['work_N', 'work_C'],
112
+ ['work_NW', 'work_W'],
113
+ ['work_NE', 'work_E'],
114
+ ['work_W', 'work_C'],
115
+ ['work_C', 'work_E'],
116
+ ['work_W', 'work_SW'],
117
+ ['work_C', 'work_S'],
118
+ ['work_E', 'work_SE'],
119
+ ['work_SW', 'work_S'],
120
+ ['work_S', 'work_SE'],
121
+
122
+ // Designer wing (through gap in glass partition)
123
+ ['work_NW', 'design_gate'],
124
+ ['corr_L', 'design_gate'],
125
+ ['design_gate', 'design_N'],
126
+ ['design_N', 'design_C'],
127
+ ['design_C', 'design_S'],
128
+
129
+ // Manager office — path goes AROUND the corner then through door
130
+ // corr_R(7,7) → mgr_hallway(7,3) → mgr_corner(7,0) → mgr_outside(12,0) → door → inside
131
+ ['corr_R', 'mgr_hallway'], // walk south, outside left wall (x=7 < wall x=8)
132
+ ['mgr_hallway', 'mgr_corner'], // walk further south past front-left corner (z=0 < wall z=1.5)
133
+ ['mgr_corner', 'mgr_outside'], // walk east to front of door (z=0, safely below front wall z=1.5)
134
+ ['mgr_outside', 'mgr_doorstep'], // step to door threshold (triggers open)
135
+ ['mgr_doorstep', 'mgr_entry'], // walk through open door into office
136
+ ['mgr_entry', 'mgr_center'], // walk deeper inside
137
+ ['mgr_center', 'mgr_desk'], // walk to desk
138
+
139
+ // Workspace → back zone (through gap in glass partition at z=-7)
140
+ ['work_S', 'back_gate'],
141
+ ['back_gate', 'back_C'],
142
+
143
+ // Back corridor
144
+ ['back_L', 'back_C'],
145
+ ['back_C', 'back_R'],
146
+ ['work_SW', 'back_L'],
147
+ ['work_SE', 'back_R'],
148
+
149
+ // Back zones
150
+ ['back_L', 'bar_entry'],
151
+ ['bar_entry', 'bar_center'],
152
+ ['back_C', 'rec_entry'],
153
+ ['rec_entry', 'rec_center'],
154
+ ['back_R', 'gym_entry'],
155
+ ['gym_entry', 'gym_center'],
156
+
157
+ // Stairs / mezzanine
158
+ ['back_R', 'stairs_bot'],
159
+ ['stairs_bot', 'stairs_top'],
160
+ ['stairs_top', 'mezz_C'],
161
+
162
+ // Rest/dressing (legacy)
163
+ ['work_SE', 'rest_entry'],
164
+ ['work_E', 'dress_entry'],
165
+ ];
166
+
167
+ // === BUILD GRAPH ===
168
+ var adjacency = {};
169
+ var waypointMap = {};
170
+
171
+ function buildGraph() {
172
+ adjacency = {};
173
+ waypointMap = {};
174
+ CAMPUS_WAYPOINTS.forEach(function(wp) {
175
+ adjacency[wp.id] = [];
176
+ waypointMap[wp.id] = wp;
177
+ });
178
+ CAMPUS_CONNECTIONS.forEach(function(conn) {
179
+ if (adjacency[conn[0]] && adjacency[conn[1]]) {
180
+ adjacency[conn[0]].push(conn[1]);
181
+ adjacency[conn[1]].push(conn[0]);
182
+ }
183
+ });
184
+ }
185
+ buildGraph();
186
+
187
+ // Find nearest waypoint to a world position
188
+ function nearestWaypoint(x, z) {
189
+ var best = null, bestDist = Infinity;
190
+ CAMPUS_WAYPOINTS.forEach(function(wp) {
191
+ var dx = wp.x - x, dz = wp.z - z;
192
+ var d = dx * dx + dz * dz;
193
+ if (d < bestDist) { bestDist = d; best = wp.id; }
194
+ });
195
+ return best;
196
+ }
197
+
198
+ // BFS shortest path
199
+ function findPath(startId, endId) {
200
+ if (startId === endId) return [startId];
201
+ var visited = {};
202
+ var queue = [[startId]];
203
+ visited[startId] = true;
204
+ while (queue.length > 0) {
205
+ var path = queue.shift();
206
+ var current = path[path.length - 1];
207
+ var neighbors = adjacency[current] || [];
208
+ for (var i = 0; i < neighbors.length; i++) {
209
+ var next = neighbors[i];
210
+ if (visited[next]) continue;
211
+ var newPath = path.concat([next]);
212
+ if (next === endId) return newPath;
213
+ visited[next] = true;
214
+ queue.push(newPath);
215
+ }
216
+ }
217
+ return null;
218
+ }
219
+
220
+ // ==================== PUBLIC API ====================
221
+
222
+ export function getNavigationPath(fromX, fromZ, toX, toZ) {
223
+ if (S.currentEnv !== 'campus') {
224
+ return [{ x: toX, z: toZ }];
225
+ }
226
+
227
+ var startWP = nearestWaypoint(fromX, fromZ);
228
+ var endWP = nearestWaypoint(toX, toZ);
229
+
230
+ if (!startWP || !endWP || startWP === endWP) {
231
+ return [{ x: toX, z: toZ }];
232
+ }
233
+
234
+ var wpPath = findPath(startWP, endWP);
235
+ if (!wpPath || wpPath.length === 0) {
236
+ return [{ x: toX, z: toZ }];
237
+ }
238
+
239
+ var result = [];
240
+ // Skip first waypoint if very close to current position
241
+ var firstWP = waypointMap[wpPath[0]];
242
+ var dx0 = firstWP.x - fromX, dz0 = firstWP.z - fromZ;
243
+ var startIdx = (dx0 * dx0 + dz0 * dz0 < 4) ? 1 : 0;
244
+
245
+ for (var i = startIdx; i < wpPath.length; i++) {
246
+ var wp = waypointMap[wpPath[i]];
247
+ var point = { x: wp.x, z: wp.z };
248
+ // Flag door waypoint
249
+ if (wpPath[i] === 'mgr_doorstep') {
250
+ point.triggerDoor = 'open';
251
+ }
252
+ result.push(point);
253
+ }
254
+
255
+ // Add final walk to exact destination if far from last waypoint
256
+ var lastWP = waypointMap[wpPath[wpPath.length - 1]];
257
+ var dxEnd = lastWP.x - toX, dzEnd = lastWP.z - toZ;
258
+ if (dxEnd * dxEnd + dzEnd * dzEnd > 1) {
259
+ result.push({ x: toX, z: toZ });
260
+ }
261
+
262
+ return result;
263
+ }
package/office/scene.js CHANGED
@@ -9,10 +9,10 @@ export function initScene() {
9
9
 
10
10
  S.scene = new THREE.Scene();
11
11
  S.scene.background = new THREE.Color(0x0d1117);
12
- S.scene.fog = new THREE.Fog(0x0d1117, 25, 55);
12
+ S.scene.fog = new THREE.Fog(0x0d1117, 30, 80);
13
13
 
14
- S.camera = new THREE.PerspectiveCamera(50, S.container.clientWidth / S.container.clientHeight, 0.1, 200);
15
- S.camera.position.set(0, 12, 16);
14
+ S.camera = new THREE.PerspectiveCamera(50, S.container.clientWidth / S.container.clientHeight, 0.1, 300);
15
+ S.camera.position.set(0, 15, 22);
16
16
  S.camera.lookAt(0, 0, 0);
17
17
 
18
18
  S.renderer = new THREE.WebGLRenderer({ antialias: true });
@@ -131,7 +131,7 @@ export function SpectatorCamera(camera, domElement) {
131
131
  if (keys['KeyA'] || keys['ArrowLeft']) moveDir.x -= 1;
132
132
  if (keys['KeyD'] || keys['ArrowRight']) moveDir.x += 1;
133
133
  if (keys['KeyE'] || keys['Space']) moveDir.y += 1;
134
- if (keys['KeyQ'] || keys['ControlLeft']) moveDir.y -= 1;
134
+ if (keys['KeyQ']) moveDir.y -= 1;
135
135
 
136
136
  if (moveDir.lengthSq() > 0) {
137
137
  moveDir.normalize();
package/office/state.js CHANGED
@@ -12,7 +12,7 @@ export const S = {
12
12
  clock: null,
13
13
  agents3d: {},
14
14
  lastProcessedMsg: 0,
15
- currentEnv: 'modern',
15
+ currentEnv: 'campus',
16
16
  furnitureGroup: null,
17
17
  deskMeshes: [],
18
18
  syncInterval: null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "let-them-talk",
3
- "version": "3.6.1",
3
+ "version": "3.6.2",
4
4
  "description": "MCP message broker + web dashboard for inter-agent communication. Let AI CLI agents talk to each other.",
5
5
  "main": "server.js",
6
6
  "bin": {
package/server.js CHANGED
@@ -871,6 +871,15 @@ async function toolSendMessage(content, to = null, reply_to = null) {
871
871
  if (currentBranch !== 'main') result.branch = currentBranch;
872
872
  if (!recipientAlive) {
873
873
  result.warning = `Agent "${to}" appears offline (PID not running). Message queued but may not be received until they reconnect.`;
874
+ } else if (agents[to] && !agents[to].listening_since) {
875
+ 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().`;
876
+ }
877
+
878
+ // Nudge: check if THIS agent has unread messages waiting
879
+ const myPending = getUnconsumedMessages(registeredName);
880
+ if (myPending.length > 0) {
881
+ result.you_have_messages = myPending.length;
882
+ result.urgent = `You have ${myPending.length} unread message(s) waiting. Call listen_group() after this to read them.`;
874
883
  }
875
884
  return result;
876
885
  }
@@ -925,6 +934,19 @@ function toolBroadcast(content) {
925
934
 
926
935
  const result = { success: true, sent_to: ids, recipient_count: ids.length };
927
936
  if (skipped.length > 0) result.skipped = skipped;
937
+ // Show which recipients are busy vs listening
938
+ const agentsNow = getAgents();
939
+ const busy = ids.filter(function(i) { return agentsNow[i.to] && !agentsNow[i.to].listening_since; }).map(function(i) { return i.to; });
940
+ if (busy.length > 0) {
941
+ result.busy_agents = busy;
942
+ result.note = busy.join(', ') + (busy.length === 1 ? ' is' : ' are') + ' currently working (not listening). Messages queued.';
943
+ }
944
+ // Nudge for own unread messages
945
+ const myPending = getUnconsumedMessages(registeredName);
946
+ if (myPending.length > 0) {
947
+ result.you_have_messages = myPending.length;
948
+ result.urgent = `You have ${myPending.length} unread message(s). Call listen_group() soon.`;
949
+ }
928
950
  return result;
929
951
  }
930
952
 
@@ -1370,17 +1392,28 @@ async function toolListenGroup(timeout_seconds = 300) {
1370
1392
  const recentSpeakers = new Set(history.slice(-10).map(m => m.from));
1371
1393
  const silent = agentNames.filter(n => !recentSpeakers.has(n) && n !== registeredName);
1372
1394
 
1395
+ const now = Date.now();
1373
1396
  const result = {
1374
- messages: batch.map(m => ({
1375
- id: m.id, from: m.from, to: m.to, content: m.content,
1376
- timestamp: m.timestamp,
1377
- ...(m.reply_to && { reply_to: m.reply_to }),
1378
- ...(m.thread_id && { thread_id: m.thread_id }),
1379
- })),
1397
+ messages: batch.map(m => {
1398
+ const ageMs = now - new Date(m.timestamp).getTime();
1399
+ const ageSec = Math.round(ageMs / 1000);
1400
+ return {
1401
+ id: m.id, from: m.from, to: m.to, content: m.content,
1402
+ timestamp: m.timestamp,
1403
+ age_seconds: ageSec,
1404
+ ...(ageSec > 30 && { delayed: true }),
1405
+ ...(m.reply_to && { reply_to: m.reply_to }),
1406
+ ...(m.thread_id && { thread_id: m.thread_id }),
1407
+ };
1408
+ }),
1380
1409
  message_count: batch.length,
1381
1410
  context: recentHistory,
1382
1411
  agents_online: agentNames.length,
1383
1412
  agents_silent: silent,
1413
+ agents_status: agentNames.reduce(function(acc, n) {
1414
+ acc[n] = agents[n].listening_since ? 'listening' : 'working';
1415
+ return acc;
1416
+ }, {}),
1384
1417
  hint: silent.length > 0
1385
1418
  ? `${silent.join(', ')} haven't spoken recently. Consider addressing them.`
1386
1419
  : 'All agents are active in the conversation.',
@@ -1416,6 +1449,7 @@ async function toolListenGroup(timeout_seconds = 300) {
1416
1449
  }
1417
1450
  }
1418
1451
 
1452
+ result.next_action = 'After processing these messages and sending your response, call listen_group() again immediately. Never stop listening.';
1419
1453
  return result;
1420
1454
  }
1421
1455
 
@@ -1423,7 +1457,13 @@ async function toolListenGroup(timeout_seconds = 300) {
1423
1457
  }
1424
1458
 
1425
1459
  setListening(false);
1426
- return { timeout: true, message: 'No messages received within timeout.', messages: [], message_count: 0 };
1460
+ return {
1461
+ timeout: true,
1462
+ retry: true,
1463
+ message: 'No messages yet. Call listen_group() again immediately to keep listening. Do NOT stop — you must stay in the conversation.',
1464
+ messages: [],
1465
+ message_count: 0,
1466
+ };
1427
1467
  }
1428
1468
 
1429
1469
  function toolGetHistory(limit = 50, thread_id = null) {
@@ -2094,7 +2134,7 @@ function toolListBranches() {
2094
2134
  // --- MCP Server setup ---
2095
2135
 
2096
2136
  const server = new Server(
2097
- { name: 'agent-bridge', version: '3.6.1' },
2137
+ { name: 'agent-bridge', version: '3.6.2' },
2098
2138
  { capabilities: { tools: {} } }
2099
2139
  );
2100
2140
 
@@ -2505,7 +2545,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2505
2545
  },
2506
2546
  {
2507
2547
  name: 'listen_group',
2508
- description: 'Listen for messages in group or managed conversation mode. Returns ALL unconsumed messages as a batch (not just one), plus recent conversation context and hints about which agents are silent. In managed mode, also includes floor/phase context and instructions on whether you should respond. Use this instead of listen() when in group or managed mode.',
2548
+ description: 'Listen for messages in group or managed conversation mode. Returns ALL unconsumed messages as a batch, plus conversation context and hints. IMPORTANT: After processing messages and responding, you MUST call listen_group() again immediately. If it times out with retry:true, call it again. Never stop listening — this is how you stay in the conversation.',
2509
2549
  inputSchema: {
2510
2550
  type: 'object',
2511
2551
  properties: {
@@ -2666,6 +2706,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2666
2706
  };
2667
2707
  }
2668
2708
 
2709
+ // Global hook: on non-listen tools, check for pending messages and nudge the agent
2710
+ // This catches agents who are mid-work and have messages piling up
2711
+ const listenTools = ['listen', 'listen_group', 'listen_codex', 'wait_for_reply', 'check_messages'];
2712
+ if (registeredName && !listenTools.includes(name) && (isGroupMode() || isManagedMode())) {
2713
+ try {
2714
+ const pending = getUnconsumedMessages(registeredName);
2715
+ if (pending.length > 0 && !result.you_have_messages) {
2716
+ result._pending_messages = pending.length;
2717
+ result._nudge = `You have ${pending.length} unread message(s) from the team. Finish your current task quickly, then call listen_group() to read them.`;
2718
+ }
2719
+ } catch {}
2720
+ }
2721
+
2669
2722
  return {
2670
2723
  content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
2671
2724
  };
@@ -2698,7 +2751,7 @@ async function main() {
2698
2751
  ensureDataDir();
2699
2752
  const transport = new StdioServerTransport();
2700
2753
  await server.connect(transport);
2701
- console.error('Agent Bridge MCP server v3.6.1 running (32 tools)');
2754
+ console.error('Agent Bridge MCP server v3.6.2 running (32 tools)');
2702
2755
  }
2703
2756
 
2704
2757
  main().catch(console.error);