let-them-talk 5.4.2 → 5.4.3

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/dashboard.html CHANGED
@@ -4533,12 +4533,6 @@
4533
4533
  <button onclick="clearAttachment()" style="position:absolute;top:-6px;right:-6px;background:#ef4444;color:#fff;border:none;border-radius:50%;width:18px;height:18px;cursor:pointer;font-size:11px;line-height:18px;padding:0;">X</button>
4534
4534
  <div id="inject-file-name" style="font-size:9px;color:var(--text-dim);margin-top:2px;"></div>
4535
4535
  </div>
4536
- <div id="assistant-private-row" style="display:none;align-items:center;gap:6px;margin-top:6px;font-size:10px;color:var(--text-muted);">
4537
- <label style="display:flex;align-items:center;gap:6px;cursor:pointer;">
4538
- <input type="checkbox" id="assistant-private-optin" style="accent-color:var(--accent)">
4539
- Send privately to Assistant only
4540
- </label>
4541
- </div>
4542
4536
  </div>
4543
4537
  <button class="send-btn" onclick="doInject()" id="inject-btn" disabled>Send</button>
4544
4538
  </div>
@@ -6149,7 +6143,6 @@ window.scopedApiUrl = scopedApiUrl;
6149
6143
  function updateSendBtn() {
6150
6144
  var target = document.getElementById('inject-target').value;
6151
6145
  var content = document.getElementById('inject-content').value.trim();
6152
- updateAssistantPrivateVisibility();
6153
6146
  document.getElementById('inject-btn').disabled = !target || (!content && !_attachedFile);
6154
6147
  }
6155
6148
 
@@ -6173,15 +6166,6 @@ function renderMainBranchOnlyView(elementId, surface) {
6173
6166
  if (el) el.innerHTML = mainBranchOnlyViewHtml(surface);
6174
6167
  }
6175
6168
 
6176
- function updateAssistantPrivateVisibility() {
6177
- var row = document.getElementById('assistant-private-row');
6178
- var checkbox = document.getElementById('assistant-private-optin');
6179
- var isAssistantTarget = document.getElementById('inject-target').value === 'Assistant';
6180
- if (!row || !checkbox) return;
6181
- row.style.display = isAssistantTarget ? 'flex' : 'none';
6182
- if (!isAssistantTarget) checkbox.checked = false;
6183
- }
6184
-
6185
6169
  // ==================== FILE ATTACHMENT ====================
6186
6170
  var _attachedFile = null; // { name, mimeType, base64 }
6187
6171
 
@@ -6218,9 +6202,6 @@ function doInject() {
6218
6202
  if (!content && _attachedFile) content = 'Analyze this image';
6219
6203
 
6220
6204
  var payload = { to: target, content: content };
6221
- if (target === 'Assistant') {
6222
- payload.assistant_private = !!document.getElementById('assistant-private-optin').checked;
6223
- }
6224
6205
 
6225
6206
  // Include attachment if present
6226
6207
  if (_attachedFile) {
package/dashboard.js CHANGED
@@ -884,13 +884,7 @@ function apiInjectMessage(body, query) {
884
884
  };
885
885
  if (body.attachments && body.attachments.length > 0) msg.attachments = body.attachments;
886
886
 
887
- // Route to private assistant channel if targeting Assistant
888
- if (body.to === 'Assistant' && body.assistant_private === true) {
889
- const assistantMsgFile = path.join(dataDir, 'assistant-messages.jsonl');
890
- fs.appendFileSync(assistantMsgFile, JSON.stringify(msg) + '\n');
891
- } else {
892
- canonicalState.appendMessage(msg, { branch });
893
- }
887
+ canonicalState.appendMessage(msg, { branch });
894
888
 
895
889
  return { success: true, messageId: msg.id };
896
890
  }
@@ -1883,47 +1877,6 @@ const server = http.createServer(async (req, res) => {
1883
1877
  });
1884
1878
  res.end(html);
1885
1879
  }
1886
- // --- Assistant private messages API ---
1887
- else if (url.pathname === '/api/assistant/messages' && req.method === 'GET') {
1888
- const projectPath = url.searchParams.get('project') || null;
1889
- const dataDir = resolveDataDir(projectPath);
1890
- const assistantMsgFile = path.join(dataDir, 'assistant-messages.jsonl');
1891
- const assistantRepliesFile = path.join(dataDir, 'assistant-replies.jsonl');
1892
- const limit = parseInt(url.searchParams.get('limit') || '100', 10);
1893
- let messages = [];
1894
- // Read Dashboard→Assistant messages
1895
- if (fs.existsSync(assistantMsgFile)) {
1896
- const lines = fs.readFileSync(assistantMsgFile, 'utf8').split('\n').filter(l => l.trim());
1897
- for (const line of lines) {
1898
- try { messages.push(JSON.parse(line)); } catch {}
1899
- }
1900
- }
1901
- // Read Assistant→Dashboard replies
1902
- if (fs.existsSync(assistantRepliesFile)) {
1903
- const lines = fs.readFileSync(assistantRepliesFile, 'utf8').split('\n').filter(l => l.trim());
1904
- for (const line of lines) {
1905
- try { messages.push(JSON.parse(line)); } catch {}
1906
- }
1907
- }
1908
- // Sort by timestamp and return last N
1909
- messages.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
1910
- if (messages.length > limit) messages = messages.slice(-limit);
1911
- res.writeHead(200, { 'Content-Type': 'application/json' });
1912
- res.end(JSON.stringify({ messages, total: messages.length }));
1913
- }
1914
- // Clear assistant chat
1915
- else if (url.pathname === '/api/assistant/clear' && req.method === 'POST') {
1916
- const projectPath = url.searchParams.get('project') || null;
1917
- const dataDir = resolveDataDir(projectPath);
1918
- const assistantMsgFile = path.join(dataDir, 'assistant-messages.jsonl');
1919
- const assistantRepliesFile = path.join(dataDir, 'assistant-replies.jsonl');
1920
- const consumedFile = path.join(dataDir, 'consumed-assistant-private.json');
1921
- try { fs.writeFileSync(assistantMsgFile, ''); } catch {}
1922
- try { fs.writeFileSync(assistantRepliesFile, ''); } catch {}
1923
- try { fs.writeFileSync(consumedFile, '[]'); } catch {}
1924
- res.writeHead(200, { 'Content-Type': 'application/json' });
1925
- res.end(JSON.stringify({ success: true }));
1926
- }
1927
1880
  // Existing APIs (now with ?project= param support)
1928
1881
  else if (url.pathname === '/api/history' && req.method === 'GET') {
1929
1882
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -3462,7 +3415,7 @@ server.listen(PORT, LAN_MODE ? '0.0.0.0' : '127.0.0.1', () => {
3462
3415
  const dataDir = resolveDataDir();
3463
3416
  const lanIP = getLanIP();
3464
3417
  console.log('');
3465
- console.log(' Let Them Talk - Agent Bridge Dashboard v5.4.2');
3418
+ console.log(' Let Them Talk - Agent Bridge Dashboard v5.4.3');
3466
3419
  console.log(' ============================================');
3467
3420
  console.log(' Dashboard: http://localhost:' + PORT);
3468
3421
  if (LAN_MODE && lanIP) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "let-them-talk",
3
- "version": "5.4.2",
3
+ "version": "5.4.3",
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": {
@@ -716,69 +716,13 @@ async function assertBranchScopedDashboardReads(baseUrl, fixture, problems) {
716
716
  assert(!exportReplayResponse.raw.includes(fixture.mainChannelMessage.content), 'Replay export must exclude main-branch channel content when exporting a feature branch.', problems);
717
717
  }
718
718
 
719
- async function assertAssistantInjectRouting(baseUrl, dataDir, eventLog, problems) {
719
+ function captureMessageBaseline(dataDir, eventLog) {
720
720
  const messagesFile = path.join(dataDir, 'messages.jsonl');
721
721
  const historyFile = path.join(dataDir, 'history.jsonl');
722
- const assistantMessagesFile = path.join(dataDir, 'assistant-messages.jsonl');
723
-
724
- const beforeMessages = readJsonl(messagesFile);
725
- const beforeHistory = readJsonl(historyFile);
726
- const beforeAssistantMessages = readJsonl(assistantMessagesFile);
727
- const beforeMessageEvents = readMessageEvents(eventLog);
728
-
729
- const defaultResponse = await requestJson(baseUrl, '/api/inject', {
730
- method: 'POST',
731
- body: {
732
- to: 'Assistant',
733
- content: 'Assistant default canonical route validation',
734
- },
735
- });
736
- assert(defaultResponse.status === 200, `POST /api/inject to Assistant without opt-in should return 200, got ${defaultResponse.status}.`, problems);
737
- assert(defaultResponse.body && defaultResponse.body.success === true, 'POST /api/inject to Assistant without opt-in should succeed.', problems);
738
-
739
- const afterDefaultMessages = readJsonl(messagesFile);
740
- const afterDefaultHistory = readJsonl(historyFile);
741
- const afterDefaultAssistantMessages = readJsonl(assistantMessagesFile);
742
- const afterDefaultMessageEvents = readMessageEvents(eventLog);
743
- const defaultMessageId = defaultResponse.body && defaultResponse.body.messageId;
744
- const defaultCanonicalMessage = afterDefaultMessages.find((message) => message.id === defaultMessageId);
745
-
746
- assert(afterDefaultMessages.length === beforeMessages.length + 1, 'Assistant default inject should append to the canonical messages projection.', problems);
747
- assert(afterDefaultHistory.length === beforeHistory.length + 1, 'Assistant default inject should append to the canonical history projection.', problems);
748
- assert(afterDefaultAssistantMessages.length === beforeAssistantMessages.length, 'Assistant default inject must not write to assistant-messages.jsonl without explicit opt-in.', problems);
749
- assert(defaultCanonicalMessage && defaultCanonicalMessage.to === 'Assistant', 'Assistant default inject should keep the Assistant target in canonical projections.', problems);
750
- assert(afterDefaultMessageEvents.length === beforeMessageEvents.length + 1, 'Assistant default inject should append one canonical message event.', problems);
751
- assert(afterDefaultMessageEvents.some((event) => event.type === 'message.sent' && event.payload && event.payload.message && event.payload.message.id === defaultMessageId), 'Assistant default inject should be recorded in the canonical message event log.', problems);
752
-
753
- const privateResponse = await requestJson(baseUrl, '/api/inject', {
754
- method: 'POST',
755
- body: {
756
- to: 'Assistant',
757
- content: 'Assistant private opt-in validation',
758
- assistant_private: true,
759
- },
760
- });
761
- assert(privateResponse.status === 200, `POST /api/inject to Assistant with assistant_private=true should return 200, got ${privateResponse.status}.`, problems);
762
- assert(privateResponse.body && privateResponse.body.success === true, 'POST /api/inject to Assistant with assistant_private=true should succeed.', problems);
763
-
764
- const afterPrivateMessages = readJsonl(messagesFile);
765
- const afterPrivateHistory = readJsonl(historyFile);
766
- const afterPrivateAssistantMessages = readJsonl(assistantMessagesFile);
767
- const afterPrivateMessageEvents = readMessageEvents(eventLog);
768
- const privateMessageId = privateResponse.body && privateResponse.body.messageId;
769
- const privateAssistantMessage = afterPrivateAssistantMessages.find((message) => message.id === privateMessageId);
770
-
771
- assert(afterPrivateMessages.length === afterDefaultMessages.length, 'Assistant private opt-in inject must not append to canonical messages.jsonl.', problems);
772
- assert(afterPrivateHistory.length === afterDefaultHistory.length, 'Assistant private opt-in inject must not append to canonical history.jsonl.', problems);
773
- assert(afterPrivateAssistantMessages.length === afterDefaultAssistantMessages.length + 1, 'Assistant private opt-in inject should append exactly one private assistant message.', problems);
774
- assert(privateAssistantMessage && privateAssistantMessage.to === 'Assistant', 'Assistant private opt-in inject should persist the Assistant-targeted private message.', problems);
775
- assert(afterPrivateMessageEvents.length === afterDefaultMessageEvents.length, 'Assistant private opt-in inject must not append canonical message events.', problems);
776
- assert(!afterPrivateMessageEvents.some((event) => event.payload && event.payload.message && event.payload.message.id === privateMessageId), 'Assistant private opt-in inject must stay out of the canonical branch event log.', problems);
777
-
778
722
  return {
779
- mainMessageCount: afterPrivateMessages.length,
780
- mainHistoryCount: afterPrivateHistory.length,
781
- messageEventCount: afterPrivateMessageEvents.length,
723
+ mainMessageCount: readJsonl(messagesFile).length,
724
+ mainHistoryCount: readJsonl(historyFile).length,
725
+ messageEventCount: readMessageEvents(eventLog).length,
782
726
  };
783
727
  }
784
728
 
@@ -884,7 +828,7 @@ async function runHealthyScenario() {
884
828
  assertDashboardScopedMessageTaskUi(problems);
885
829
  await assertBranchScopedDashboardReads(baseUrl, branchFixture, problems);
886
830
  await assertBranchAwareRespawnPrompt(baseUrl, branchFixture, respawnFixture, problems);
887
- const assistantBaseline = await assertAssistantInjectRouting(baseUrl, dataDir, eventLog, problems);
831
+ const messageBaseline = captureMessageBaseline(dataDir, eventLog);
888
832
  await assertDashboardRuleRoutes(baseUrl, canonicalState, eventLog, problems);
889
833
 
890
834
  const messagesFile = path.join(dataDir, 'messages.jsonl');
@@ -1041,8 +985,8 @@ async function runHealthyScenario() {
1041
985
  const reassignMessages = finalHistory.filter((message) => typeof message.content === 'string' && message.content.startsWith('[REASSIGNED]'));
1042
986
  const stopMessages = finalHistory.filter((message) => typeof message.content === 'string' && message.content.startsWith('[PLAN STOPPED]'));
1043
987
 
1044
- assert(finalMessages.length === assistantBaseline.mainMessageCount + 7, `Plan control routes should leave ${assistantBaseline.mainMessageCount + 7} live main-branch messages, found ${finalMessages.length}.`, problems);
1045
- assert(finalHistory.length === assistantBaseline.mainHistoryCount + 7, `Plan control routes should leave ${assistantBaseline.mainHistoryCount + 7} canonical main-branch history rows after deleting the injected dashboard message, found ${finalHistory.length}.`, problems);
988
+ assert(finalMessages.length === messageBaseline.mainMessageCount + 7, `Plan control routes should leave ${messageBaseline.mainMessageCount + 7} live main-branch messages, found ${finalMessages.length}.`, problems);
989
+ assert(finalHistory.length === messageBaseline.mainHistoryCount + 7, `Plan control routes should leave ${messageBaseline.mainHistoryCount + 7} canonical main-branch history rows after deleting the injected dashboard message, found ${finalHistory.length}.`, problems);
1046
990
  assert(pauseMessages.length === 2, 'Pause route should broadcast one message per registered agent.', problems);
1047
991
  assert(resumeMessages.length === 2, 'Resume route should broadcast one message per registered agent.', problems);
1048
992
  assert(reassignMessages.length === 1 && reassignMessages[0].to === 'beta', 'Reassign route should inject one direct message to the new assignee.', problems);
package/server.js CHANGED
@@ -57,7 +57,6 @@ const REVIEWS_FILE = path.join(DATA_DIR, 'reviews.json');
57
57
  const DEPS_FILE = path.join(DATA_DIR, 'dependencies.json');
58
58
  const REPUTATION_FILE = path.join(DATA_DIR, 'reputation.json');
59
59
  const RULES_FILE = path.join(DATA_DIR, 'rules.json');
60
- const ASSISTANT_REPLIES_FILE = path.join(DATA_DIR, 'assistant-replies.jsonl');
61
60
  // Plugins removed in v3.4.3 — unnecessary attack surface, CLIs have their own extension systems
62
61
 
63
62
  // In-memory state for this process
@@ -1287,10 +1286,6 @@ function appendChannelConversationMessage(message, channel, branch = currentBran
1287
1286
  return appendConversationMessage(message, getChannelMessagesFile(channel, branch), getChannelHistoryFile(channel, branch));
1288
1287
  }
1289
1288
 
1290
- function appendAssistantReplyMessage(message) {
1291
- return messagesState.appendAuxiliaryMessage(message, ASSISTANT_REPLIES_FILE);
1292
- }
1293
-
1294
1289
  function emptyCompressedState() {
1295
1290
  return { segments: [], last_compressed_at: null };
1296
1291
  }
@@ -2121,12 +2116,8 @@ function toolRegister(name, provider = null) {
2121
2116
  }
2122
2117
 
2123
2118
  // Prevent re-registration under a different name from the same process
2124
- // EXCEPTION: Allow transition to "Assistant" for setup/reset — force clear old registration
2125
2119
  if (registeredName && registeredName !== name) {
2126
- if (name === 'Assistant') {
2127
- registeredName = null; // Force clear for Assistant registration
2128
- registeredToken = null;
2129
- } else {
2120
+ {
2130
2121
  unlockAgentsFile();
2131
2122
  return { error: `Already registered as "${registeredName}". Cannot change name mid-session.`, current_name: registeredName };
2132
2123
  }
@@ -2451,7 +2442,7 @@ async function toolSendMessage(content, to = null, reply_to = null, channel = nu
2451
2442
 
2452
2443
  // Send-after-listen enforcement: must call listen_group between sends in group mode
2453
2444
  // Autonomous mode: relaxed to 5 sends per listen cycle
2454
- // Assistant mode: skip enforcement when replying to Dashboard (owner)
2445
+ // Skip send-after-listen enforcement when replying to Dashboard (owner)
2455
2446
  const effectiveSendLimit = isAutonomousMode() ? 5 : sendLimit;
2456
2447
  if (isGroupMode() && sendsSinceLastListen >= effectiveSendLimit && !isDashboardTarget) {
2457
2448
  return { error: `You must call listen_group() before sending again. You've sent ${sendsSinceLastListen} message(s) without listening (limit: ${effectiveSendLimit}). This prevents message storms.` };
@@ -2655,16 +2646,15 @@ async function toolSendMessage(content, to = null, reply_to = null, channel = nu
2655
2646
  }
2656
2647
 
2657
2648
  ensureDataDir();
2658
- // Messages involving Dashboard: route to private assistant-messages.jsonl
2659
- // Only Dashboard→Agent messages go to the assistant file (so assistant() can pick them up)
2660
- // Agent→Dashboard replies go to a separate file (so they don't trigger fs.watch loops)
2649
+ // Dashboard-targeted messages go through the normal conversation log like
2650
+ // any other DM so they appear in the Messages tab via /api/history. The
2651
+ // listen_group DM filter (msg.to === agent) still keeps them private from
2652
+ // other agents.
2661
2653
  if (isDashboardTarget) {
2662
2654
  msg.to = 'Dashboard';
2663
2655
  delete msg.addressed_to;
2664
- appendAssistantReplyMessage(msg);
2665
- } else {
2666
- appendChannelConversationMessage(msg, channel);
2667
2656
  }
2657
+ appendChannelConversationMessage(msg, channel);
2668
2658
  touchActivity();
2669
2659
  lastSentAt = Date.now();
2670
2660
 
@@ -3190,226 +3180,6 @@ async function toolListenCodex(from = null) {
3190
3180
  });
3191
3181
  }
3192
3182
 
3193
- // --- Assistant mode ---
3194
- // Personal assistant listen loop — only receives Dashboard messages,
3195
- // reads personality + safety files, returns safety-checked context with each message
3196
-
3197
- // Track how many messages processed — full context on first + every 15th message
3198
- let assistantMsgCount = 0;
3199
- const ASSISTANT_REFRESH_INTERVAL = 15;
3200
-
3201
- async function toolAssistant() {
3202
- if (!registeredName) {
3203
- return { error: 'You must call register() first' };
3204
- }
3205
-
3206
- setListening(true);
3207
-
3208
- // Private assistant message file — separate from main messages.jsonl
3209
- const assistantMsgFile = path.join(DATA_DIR, 'assistant-messages.jsonl');
3210
- ensureDataDir();
3211
-
3212
- // Read assistant personality and safety files
3213
- const assistantDir = path.join(DATA_DIR, 'assistant');
3214
- const readFile = (name) => {
3215
- const p = path.join(assistantDir, name);
3216
- try { return fs.readFileSync(p, 'utf8'); } catch { return null; }
3217
- };
3218
-
3219
- const soul = readFile('Soul.md');
3220
- const identity = readFile('Identity.md');
3221
- const memory = readFile('Memory.md');
3222
- const skills = readFile('Skills.md');
3223
- const tools = readFile('Tools.md');
3224
- const safetyRules = readFile('SafetyRules.md');
3225
-
3226
- if (!safetyRules) {
3227
- setListening(false);
3228
- return {
3229
- error: 'SafetyRules.md not found in .agent-bridge/assistant/. Run assistant init first.',
3230
- };
3231
- }
3232
-
3233
- // Read unconsumed messages from private assistant file
3234
- const assistantConsumedFile = path.join(DATA_DIR, 'consumed-assistant-private.json');
3235
- let aConsumed = new Set();
3236
- try {
3237
- const raw = fs.readFileSync(assistantConsumedFile, 'utf8');
3238
- aConsumed = new Set(JSON.parse(raw));
3239
- } catch {}
3240
-
3241
- const readAssistantMessages = (offset) => {
3242
- if (!fs.existsSync(assistantMsgFile)) return { messages: [], newOffset: 0 };
3243
- const stat = fs.statSync(assistantMsgFile);
3244
- if (stat.size <= offset) return { messages: [], newOffset: offset };
3245
- const fd = fs.openSync(assistantMsgFile, 'r');
3246
- const buf = Buffer.alloc(stat.size - offset);
3247
- fs.readSync(fd, buf, 0, buf.length, offset);
3248
- fs.closeSync(fd);
3249
- const lines = buf.toString('utf8').split('\n').filter(l => l.trim());
3250
- const messages = [];
3251
- for (const line of lines) {
3252
- try { messages.push(JSON.parse(line)); } catch {}
3253
- }
3254
- return { messages, newOffset: stat.size };
3255
- };
3256
-
3257
- const saveAConsumed = () => {
3258
- fs.writeFileSync(assistantConsumedFile, JSON.stringify([...aConsumed]));
3259
- };
3260
-
3261
- // Check for existing unconsumed messages
3262
- let aOffset = 0;
3263
- if (fs.existsSync(assistantMsgFile)) {
3264
- const { messages: allMsgs } = readAssistantMessages(0);
3265
- for (const msg of allMsgs) {
3266
- if (aConsumed.has(msg.id)) continue;
3267
- if (msg.from !== 'Dashboard') continue;
3268
- aConsumed.add(msg.id);
3269
- saveAConsumed();
3270
- aOffset = fs.statSync(assistantMsgFile).size;
3271
- touchActivity();
3272
- setListening(false);
3273
- const fullRefresh = assistantMsgCount === 0 || assistantMsgCount % ASSISTANT_REFRESH_INTERVAL === 0;
3274
- assistantMsgCount++;
3275
- return buildAssistantResponse(msg, { soul, identity, memory, skills, tools, safetyRules }, fullRefresh);
3276
- }
3277
- aOffset = fs.statSync(assistantMsgFile).size;
3278
- }
3279
-
3280
- // Wait for new messages using fs.watch on assistant-messages.jsonl
3281
- return new Promise((resolve) => {
3282
- let resolved = false;
3283
- const done = (result) => {
3284
- if (resolved) return;
3285
- resolved = true;
3286
- try { if (watcher) watcher.close(); } catch {}
3287
- clearTimeout(timer);
3288
- clearInterval(heartbeatTimer);
3289
- if (fallbackInterval) clearInterval(fallbackInterval);
3290
- resolve(result);
3291
- };
3292
-
3293
- let watcher;
3294
- let fallbackInterval;
3295
-
3296
- const checkMessages = () => {
3297
- const { messages: newMsgs, newOffset } = readAssistantMessages(aOffset);
3298
- aOffset = newOffset;
3299
- for (const msg of newMsgs) {
3300
- if (aConsumed.has(msg.id)) continue;
3301
- if (msg.from !== 'Dashboard') continue;
3302
- aConsumed.add(msg.id);
3303
- saveAConsumed();
3304
- touchActivity();
3305
- setListening(false);
3306
- const fullRefresh = assistantMsgCount === 0 || assistantMsgCount % ASSISTANT_REFRESH_INTERVAL === 0;
3307
- assistantMsgCount++;
3308
- if (fullRefresh) {
3309
- // Re-read all files on refresh cycles (user may edit them)
3310
- const freshSoul = readFile('Soul.md');
3311
- const freshIdentity = readFile('Identity.md');
3312
- const freshMemory = readFile('Memory.md');
3313
- const freshSkills = readFile('Skills.md');
3314
- const freshTools = readFile('Tools.md');
3315
- const freshSafety = readFile('SafetyRules.md');
3316
- done(buildAssistantResponse(msg, {
3317
- soul: freshSoul, identity: freshIdentity, memory: freshMemory,
3318
- skills: freshSkills, tools: freshTools, safetyRules: freshSafety,
3319
- }, true));
3320
- } else {
3321
- // Lightweight — only re-read safety rules (always enforced)
3322
- const freshSafety = readFile('SafetyRules.md');
3323
- done(buildAssistantResponse(msg, { safetyRules: freshSafety }, false));
3324
- }
3325
- return true;
3326
- }
3327
- return false;
3328
- };
3329
-
3330
- // Create file if it doesn't exist so fs.watch works
3331
- if (!fs.existsSync(assistantMsgFile)) {
3332
- fs.writeFileSync(assistantMsgFile, '');
3333
- }
3334
-
3335
- try {
3336
- watcher = fs.watch(assistantMsgFile, () => { checkMessages(); });
3337
- watcher.on('error', () => {});
3338
- } catch {
3339
- let pollCount = 0;
3340
- fallbackInterval = setInterval(() => {
3341
- if (checkMessages()) { clearInterval(fallbackInterval); return; }
3342
- pollCount++;
3343
- if (pollCount === 10) {
3344
- clearInterval(fallbackInterval);
3345
- fallbackInterval = setInterval(() => {
3346
- if (checkMessages()) clearInterval(fallbackInterval);
3347
- }, 2000);
3348
- }
3349
- }, 500);
3350
- }
3351
-
3352
- // Heartbeat every 15s
3353
- const heartbeatTimer = setInterval(() => { touchHeartbeat(registeredName); }, 15000);
3354
-
3355
- // 5 min timeout
3356
- const timer = setTimeout(() => {
3357
- setListening(false);
3358
- touchActivity();
3359
- done({ retry: true, message: 'No messages from owner in 5 minutes. Call assistant() again to keep waiting.' });
3360
- }, 300000);
3361
- });
3362
- }
3363
-
3364
- function buildAssistantResponse(msg, files, fullRefresh) {
3365
- const response = {
3366
- message: {
3367
- id: msg.id,
3368
- from: msg.from,
3369
- content: msg.content,
3370
- timestamp: msg.timestamp,
3371
- },
3372
- context_refreshed: fullRefresh,
3373
- };
3374
-
3375
- if (fullRefresh) {
3376
- // Full context — first message + every 15th message
3377
- response.assistant_context = {
3378
- soul: files.soul,
3379
- identity: files.identity,
3380
- memory: files.memory,
3381
- skills: files.skills,
3382
- tools: files.tools,
3383
- safety_rules: files.safetyRules,
3384
- };
3385
- response.instructions = [
3386
- 'You are in Assistant mode. Read your Soul.md and Identity.md to know your personality.',
3387
- 'BEFORE executing ANY action, check the request against safety_rules. If it matches a CRITICAL rule, REFUSE. If it needs confirmation, ASK FIRST.',
3388
- 'Check your Memory.md for context from previous conversations.',
3389
- 'Check Skills.md and Tools.md to know what you are allowed to do.',
3390
- 'Keep responses short (2-3 sentences) since the user is on their phone.',
3391
- 'After responding, call assistant() again immediately to keep listening.',
3392
- 'To reply, use send_message(to: "Dashboard", content: "your reply").',
3393
- 'If the voice transcription looks garbled or unclear, ask the user to repeat.',
3394
- ];
3395
- } else {
3396
- // Lightweight — only safety rules (always needed) + reminder
3397
- response.assistant_context = {
3398
- safety_rules: files.safetyRules,
3399
- };
3400
- response.instructions = [
3401
- 'Continue in Assistant mode — your personality files are already in context from earlier.',
3402
- 'BEFORE executing ANY action, check the request against safety_rules. If it matches a CRITICAL rule, REFUSE. If it needs confirmation, ASK FIRST.',
3403
- 'Keep responses short (2-3 sentences) since the user is on their phone.',
3404
- 'After responding, call assistant() again immediately to keep listening.',
3405
- 'To reply, use send_message(to: "Dashboard", content: "your reply").',
3406
- ];
3407
- }
3408
-
3409
- response.next_action = 'Process this message following your personality and safety rules, then call assistant() again.';
3410
- return response;
3411
- }
3412
-
3413
3183
  // --- Group conversation tools ---
3414
3184
 
3415
3185
  function toolSetConversationMode(mode) {
@@ -8425,7 +8195,7 @@ function toolToggleRule(ruleId) {
8425
8195
  // --- MCP Server setup ---
8426
8196
 
8427
8197
  const server = new Server(
8428
- { name: 'agent-bridge', version: '5.4.2' },
8198
+ { name: 'agent-bridge', version: '5.4.3' },
8429
8199
  { capabilities: { tools: {} } }
8430
8200
  );
8431
8201
 
@@ -8541,14 +8311,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
8541
8311
  },
8542
8312
  },
8543
8313
  },
8544
- {
8545
- name: 'assistant',
8546
- description: 'Assistant mode — personal assistant listen loop. Only receives messages from Dashboard (the owner). Returns message with safety context. Full personality files (Soul, Identity, Memory, Skills, Tools, SafetyRules) included on first call and every 15th message (context_refreshed: true). In between, only SafetyRules are sent to save tokens — your earlier context still applies. Use this instead of listen() when registered as an Assistant agent.',
8547
- inputSchema: {
8548
- type: 'object',
8549
- properties: {},
8550
- },
8551
- },
8552
8314
  {
8553
8315
  name: 'check_messages',
8554
8316
  description: 'Non-blocking PEEK at your inbox — shows message previews but does NOT consume them. Use listen() to actually receive and process messages. Do NOT call this in a loop — it wastes tokens returning the same messages repeatedly. Use listen() instead which blocks efficiently and consumes messages.',
@@ -9185,9 +8947,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
9185
8947
  case 'listen_codex':
9186
8948
  result = await toolListenCodex(args?.from);
9187
8949
  break;
9188
- case 'assistant':
9189
- result = await toolAssistant();
9190
- break;
9191
8950
  case 'check_messages':
9192
8951
  result = toolCheckMessages(args?.from);
9193
8952
  break;
@@ -9565,7 +9324,7 @@ async function main() {
9565
9324
  try {
9566
9325
  const transport = new StdioServerTransport();
9567
9326
  await server.connect(transport);
9568
- console.error('Agent Bridge MCP server v5.4.2 running (66 tools)');
9327
+ console.error('Agent Bridge MCP server v5.4.3 running (65 tools)');
9569
9328
  } catch (e) {
9570
9329
  console.error('ERROR: MCP server failed to start: ' + e.message);
9571
9330
  console.error('Fix: Run "npx let-them-talk doctor" to check your setup.');