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/README.md +2 -2
- package/USAGE.md +1 -1
- package/cli.js +1184 -1264
- package/dashboard.html +0 -19
- package/dashboard.js +2 -49
- package/package.json +1 -1
- package/scripts/check-dashboard-control-plane.js +7 -63
- package/server.js +9 -250
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
|
-
|
|
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.
|
|
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
|
@@ -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
|
-
|
|
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:
|
|
780
|
-
mainHistoryCount:
|
|
781
|
-
messageEventCount:
|
|
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
|
|
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 ===
|
|
1045
|
-
assert(finalHistory.length ===
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
2659
|
-
//
|
|
2660
|
-
//
|
|
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.
|
|
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.
|
|
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.');
|