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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.6.2] - 2026-03-16
4
+
5
+ ### Added — Message Awareness System
6
+ - **Sender gets busy status** — `send_message` and `broadcast` tell you when recipients are working (not listening) so you know messages are queued
7
+ - **Pending message nudge** — every non-listen tool call checks for unread messages and tells the agent to call `listen_group()` soon
8
+ - **Message age tracking** — `listen_group` shows `age_seconds` per message and `delayed: true` flag for messages older than 30s
9
+ - **Agent status in batch** — `listen_group` returns `agents_status` map showing who is `listening` vs `working`
10
+ - **listen_group retry** — timeout now returns `retry: true` with explicit instruction to call again immediately
11
+ - **next_action field** — successful `listen_group` response tells agent to call `listen_group()` again after responding
12
+ - **Ctrl key removed from camera** — no longer moves camera down (Q/E only)
13
+
14
+ ### Added — 3D World: Campus Environment & Navigation
15
+ - **Campus environment** — new outdoor environment option with buildings, paths, green spaces
16
+ - **Navigation system** — pathfinding for agents to walk around obstacles instead of through walls
17
+ - **Door animations** — manager office door slides open when agents approach, closes when they leave
18
+ - **Roof visibility** — roof hides when camera is above ceiling height
19
+
3
20
  ## [3.6.1] - 2026-03-16
4
21
 
5
22
  ### Fixed
package/cli.js CHANGED
@@ -9,7 +9,7 @@ const command = process.argv[2];
9
9
 
10
10
  function printUsage() {
11
11
  console.log(`
12
- Let Them Talk — Agent Bridge v3.6.1
12
+ Let Them Talk — Agent Bridge v3.6.2
13
13
  MCP message broker for inter-agent communication
14
14
  Supports: Claude Code, Gemini CLI, Codex CLI, Ollama
15
15
 
package/dashboard.html CHANGED
@@ -2558,6 +2558,15 @@
2558
2558
  opacity: 0;
2559
2559
  transition: none;
2560
2560
  }
2561
+ .office3d-listen-lost {
2562
+ pointer-events: none;
2563
+ font-family: Inter, sans-serif;
2564
+ white-space: nowrap;
2565
+ }
2566
+ @keyframes office3d-pulse {
2567
+ 0%, 100% { opacity: 1; transform: scale(1); }
2568
+ 50% { opacity: 0.5; transform: scale(1.1); }
2569
+ }
2561
2570
 
2562
2571
  /* 3D task indicator */
2563
2572
  .office3d-task-indicator {
@@ -3300,6 +3309,7 @@
3300
3309
  <div class="office-toolbar">
3301
3310
  <label>Environment:</label>
3302
3311
  <select id="office-env-select" onchange="officeSetEnvironment(this.value)">
3312
+ <option value="campus" selected>Tech Campus</option>
3303
3313
  <option value="modern">Modern Office</option>
3304
3314
  <option value="startup">Startup Garage</option>
3305
3315
  </select>
package/office/agents.js CHANGED
@@ -5,6 +5,34 @@ import { resolveAppearance } from './appearance.js';
5
5
  import { buildHair } from './hair.js';
6
6
  import { buildFaceSprite } from './face.js';
7
7
  import { buildOutfit, removeOutfit } from './outfits.js';
8
+ import { getNavigationPath } from './navigation.js';
9
+
10
+ // Navigate agent using waypoint pathfinding (campus) or direct walk (other envs)
11
+ export function navigateTo(agent, tx, tz, callback) {
12
+ var path = getNavigationPath(agent.pos.x, agent.pos.z, tx, tz);
13
+ if (!path || path.length === 0) {
14
+ walkTo(agent, tx, tz, callback);
15
+ return;
16
+ }
17
+ // Queue all waypoints, put callback on the last one
18
+ agent.walkQueue = [];
19
+ for (var i = 1; i < path.length; i++) {
20
+ agent.walkQueue.push({ x: path[i].x, z: path[i].z, cb: null, triggerDoor: path[i].triggerDoor });
21
+ }
22
+ // Attach callback to last queued point (or first walk if only 1 point)
23
+ if (agent.walkQueue.length > 0) {
24
+ agent.walkQueue[agent.walkQueue.length - 1].cb = callback;
25
+ }
26
+ // Start walking to first point
27
+ var first = path[0];
28
+ walkTo(agent, first.x, first.z, first.triggerDoor ? function() { triggerManagerDoor(true); } : (path.length === 1 ? callback : null));
29
+ }
30
+
31
+ function triggerManagerDoor(open) {
32
+ if (S._managerDoor) {
33
+ S._managerDoorOpen = open ? 1 : 0;
34
+ }
35
+ }
8
36
 
9
37
  export function walkTo(agent, tx, tz, callback) {
10
38
  var dx = tx - agent.pos.x;
@@ -25,13 +53,39 @@ export function showBubble(agent, text) {
25
53
  agent.bubbleText = display;
26
54
  }
27
55
 
56
+ function getDeskPositions() {
57
+ return S._campusDeskPositions || DESK_POSITIONS;
58
+ }
59
+
28
60
  function assignDesk(agentName) {
61
+ var desks = getDeskPositions();
29
62
  var used = {};
30
63
  for (var n in S.agents3d) used[S.agents3d[n].deskIdx] = true;
31
- for (var i = 0; i < DESK_POSITIONS.length; i++) {
32
- if (!used[i]) return i;
64
+
65
+ // If campus mode, check if agent has "Manager" role or name — assign last desk (manager office)
66
+ if (S.currentEnv === 'campus' && desks.length > 0) {
67
+ var info = (window.cachedAgents || {})[agentName] || {};
68
+ var role = (info.role || '').toLowerCase();
69
+ var dname = (info.display_name || agentName).toLowerCase();
70
+ var regName = agentName.toLowerCase();
71
+ var isManager = role === 'manager' || role === 'project lead' || role === 'ceo' || role === 'director' ||
72
+ role.indexOf('project manager') >= 0 || role.indexOf('team lead') >= 0 ||
73
+ dname === 'manager' || regName === 'manager';
74
+ var managerIdx = desks.length - 1; // last desk is manager office
75
+ if (isManager && !used[managerIdx]) {
76
+ return managerIdx;
77
+ }
78
+ // Non-manager agents skip the manager desk
79
+ for (var i = 0; i < desks.length - 1; i++) {
80
+ if (!used[i]) return i;
81
+ }
82
+ return Object.keys(S.agents3d).length % (desks.length - 1);
83
+ }
84
+
85
+ for (var j = 0; j < desks.length; j++) {
86
+ if (!used[j]) return j;
33
87
  }
34
- return Object.keys(S.agents3d).length % DESK_POSITIONS.length;
88
+ return Object.keys(S.agents3d).length % desks.length;
35
89
  }
36
90
 
37
91
  function fetchTasks() {
@@ -75,13 +129,19 @@ function updateLabel(agent) {
75
129
  }
76
130
  }
77
131
 
78
- function updateDeskScreen(deskIdx, status) {
132
+ function updateDeskScreen(deskIdx, status, isListening) {
79
133
  var desk = S.deskMeshes[deskIdx];
80
134
  if (!desk) return;
81
- if (status === 'active') {
82
- desk.screenMat.emissive.setHex(0x58a6ff);
135
+ if (status === 'active' && isListening) {
136
+ // Listening — green screen
137
+ desk.screenMat.emissive.setHex(0x22c55e);
83
138
  desk.screenMat.emissiveIntensity = 0.5;
84
- desk.screenMat.color.setHex(0x58a6ff);
139
+ desk.screenMat.color.setHex(0x22c55e);
140
+ } else if (status === 'active' && !isListening) {
141
+ // Active but NOT listening — red screen
142
+ desk.screenMat.emissive.setHex(0xef4444);
143
+ desk.screenMat.emissiveIntensity = 0.6;
144
+ desk.screenMat.color.setHex(0xef4444);
85
145
  } else if (status === 'sleeping') {
86
146
  desk.screenMat.emissive.setHex(0x1a2744);
87
147
  desk.screenMat.emissiveIntensity = 0.15;
@@ -151,7 +211,8 @@ export function syncAgents() {
151
211
  var info = window.cachedAgents[name];
152
212
  if (!S.agents3d[name]) {
153
213
  var deskIdx = assignDesk(name);
154
- var deskPos = DESK_POSITIONS[deskIdx] || DESK_POSITIONS[0];
214
+ var allDesks = getDeskPositions();
215
+ var deskPos = allDesks[deskIdx] || allDesks[0];
155
216
  var parts = createCharacter(info.display_name || name, info.appearance || {});
156
217
  var agent = {
157
218
  name: name,
@@ -189,6 +250,7 @@ export function syncAgents() {
189
250
  celebrateTimer: 0,
190
251
  stretchTimer: 0,
191
252
  idleGestureTimer: 5 + Math.random() * 10,
253
+ listenLostTimer: 0,
192
254
  lastMessageTime: 0,
193
255
  monitorTimer: 0,
194
256
  location: 'desk', // 'desk', 'dressing_room', 'rest', 'walking'
@@ -203,10 +265,10 @@ export function syncAgents() {
203
265
  showBubble(agent, 'Checking in...');
204
266
  (function(a) {
205
267
  setTimeout(function() {
206
- walkTo(a, a.deskPos.x, a.deskPos.z + 0.7, function() {
268
+ navigateTo(a, a.deskPos.x, a.deskPos.z + 0.7, function() {
207
269
  a.registered = true;
208
270
  showBubble(a, 'Ready to work!');
209
- updateDeskScreen(a.deskIdx, a.state);
271
+ updateDeskScreen(a.deskIdx, a.state, a.isListening);
210
272
  });
211
273
  }, 800);
212
274
  })(agent);
@@ -227,8 +289,20 @@ export function syncAgents() {
227
289
  }
228
290
 
229
291
  existing.displayName = info.display_name || name;
292
+ var wasListening = existing.isListening;
230
293
  existing.isListening = !!(info.is_listening);
231
294
 
295
+ // Detect listen mode change
296
+ if (wasListening && !existing.isListening) {
297
+ // Left listen mode — flash alert
298
+ existing.listenLostTimer = 3;
299
+ flashDeskScreen(existing.deskIdx);
300
+ }
301
+ if (!wasListening && existing.isListening) {
302
+ // Entered listen mode
303
+ existing.listenLostTimer = 0;
304
+ }
305
+
232
306
  var task = getAgentTask(name);
233
307
  if (task) {
234
308
  var prevTask = existing.currentTask;
@@ -248,7 +322,7 @@ export function syncAgents() {
248
322
  }
249
323
 
250
324
  updateLabel(existing);
251
- if (existing.registered) updateDeskScreen(existing.deskIdx, existing.state);
325
+ if (existing.registered) updateDeskScreen(existing.deskIdx, existing.state, existing.isListening);
252
326
  }
253
327
  }
254
328
 
@@ -309,7 +383,7 @@ export function processMessages() {
309
383
  stopX = tx + 1.5;
310
384
  stopZ = tz;
311
385
  }
312
- walkTo(f, stopX, stopZ, function() {
386
+ navigateTo(f, stopX, stopZ, function() {
313
387
  // Sender faces target
314
388
  var dx2 = t.pos.x - f.pos.x;
315
389
  var dz2 = t.pos.z - f.pos.z;
@@ -325,7 +399,7 @@ export function processMessages() {
325
399
 
326
400
  setTimeout(function() {
327
401
  // Sender walks back to desk
328
- walkTo(f, f.deskPos.x, f.deskPos.z + 0.7);
402
+ navigateTo(f, f.deskPos.x, f.deskPos.z + 0.7);
329
403
  // Target turns back to desk after a short delay
330
404
  setTimeout(function() {
331
405
  if (t._listeningTo === f.name) {
@@ -342,7 +416,7 @@ export function processMessages() {
342
416
  (function(f, txt) {
343
417
  setTimeout(function() {
344
418
  f.walkQueue = [];
345
- walkTo(f, 0, 0, function() {
419
+ navigateTo(f, 0, 0, function() {
346
420
  showBubble(f, txt);
347
421
  // All nearby agents turn toward the broadcaster
348
422
  for (var an in S.agents3d) {
@@ -355,7 +429,7 @@ export function processMessages() {
355
429
  a._listeningTo = f.name;
356
430
  }
357
431
  setTimeout(function() {
358
- walkTo(f, f.deskPos.x, f.deskPos.z + 0.7);
432
+ navigateTo(f, f.deskPos.x, f.deskPos.z + 0.7);
359
433
  // All listeners turn back
360
434
  setTimeout(function() {
361
435
  for (var an2 in S.agents3d) {
@@ -1,4 +1,6 @@
1
1
  import { S } from './state.js';
2
+ import * as THREE from 'three';
3
+ import { CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
2
4
  import { updateMonitorScreen, setMonitorDim } from './monitors.js';
3
5
 
4
6
  export function easeInOutQuad(t) {
@@ -35,6 +37,10 @@ export function updateAgent(agent, dt, time) {
35
37
  agent.walkProgress = 0;
36
38
  if (agent.walkQueue && agent.walkQueue.length > 0) {
37
39
  var next = agent.walkQueue.shift();
40
+ // Trigger door animation if waypoint requires it
41
+ if (next.triggerDoor && S._managerDoor) {
42
+ S._managerDoorOpen = 1;
43
+ }
38
44
  walkTo(agent, next.x, next.z, next.cb);
39
45
  } else if (cb) {
40
46
  cb();
@@ -251,6 +257,34 @@ export function updateAgent(agent, dt, time) {
251
257
  agent.parts.head.rotation.z = Math.sin(time * 1.5) * 0.08;
252
258
  }
253
259
 
260
+ // Listen-lost alert (head shake + warning indicator)
261
+ if (agent.listenLostTimer > 0) {
262
+ agent.listenLostTimer -= dt;
263
+ // Head shake animation (rapid left-right)
264
+ var shakeT = agent.listenLostTimer;
265
+ if (shakeT > 1.5) {
266
+ agent.parts.head.rotation.y = Math.sin(time * 20) * 0.15;
267
+ }
268
+ // Show warning indicator above head
269
+ if (!agent._listenLostDiv) {
270
+ agent._listenLostDiv = document.createElement('div');
271
+ agent._listenLostDiv.className = 'office3d-listen-lost';
272
+ agent._listenLostDiv.innerHTML = '<span style="color:#ef4444;font-size:14px;font-weight:bold;text-shadow:0 0 6px rgba(239,68,68,0.6);animation:office3d-pulse 0.5s infinite">&#x26A0; NOT LISTENING</span>';
273
+ agent._listenLostLabel = new CSS2DObject(agent._listenLostDiv);
274
+ agent._listenLostLabel.position.set(0, 1.9, 0);
275
+ agent.parts.group.add(agent._listenLostLabel);
276
+ }
277
+ agent._listenLostDiv.style.display = 'block';
278
+ agent._listenLostDiv.style.opacity = String(Math.min(1, agent.listenLostTimer));
279
+ if (agent.listenLostTimer <= 0) {
280
+ agent._listenLostDiv.style.display = 'none';
281
+ agent.listenLostTimer = 0;
282
+ agent.parts.head.rotation.y = 0;
283
+ }
284
+ } else if (agent._listenLostDiv) {
285
+ agent._listenLostDiv.style.display = 'none';
286
+ }
287
+
254
288
  // Typing dots
255
289
  var showTyping = agent.state === 'active' && !agent.isListening && !isWalking && !isSleeping && agent.registered && agent.isSitting;
256
290
  agent.parts.typingLabel.visible = showTyping;