@tloncorp/openclaw 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +117 -3
- package/dist/index.js.map +1 -1
- package/dist/src/channel.js +50 -2
- package/dist/src/channel.js.map +1 -1
- package/dist/src/config-schema.js +4 -0
- package/dist/src/config-schema.js.map +1 -1
- package/dist/src/monitor/approval.js +194 -96
- package/dist/src/monitor/approval.js.map +1 -1
- package/dist/src/monitor/command-auth.js +62 -0
- package/dist/src/monitor/command-auth.js.map +1 -0
- package/dist/src/monitor/command-bridge.js +27 -0
- package/dist/src/monitor/command-bridge.js.map +1 -0
- package/dist/src/monitor/discovery.js +15 -7
- package/dist/src/monitor/discovery.js.map +1 -1
- package/dist/src/monitor/index.js +343 -128
- package/dist/src/monitor/index.js.map +1 -1
- package/dist/src/monitor/media.js +33 -27
- package/dist/src/monitor/media.js.map +1 -1
- package/dist/src/monitor/utils.js +21 -0
- package/dist/src/monitor/utils.js.map +1 -1
- package/dist/src/session-roles.js +39 -0
- package/dist/src/session-roles.js.map +1 -0
- package/dist/src/settings.js +5 -1
- package/dist/src/settings.js.map +1 -1
- package/dist/src/types.js +5 -0
- package/dist/src/types.js.map +1 -1
- package/dist/src/urbit/send.js +6 -2
- package/dist/src/urbit/send.js.map +1 -1
- package/dist/src/urbit/sse-client.js +13 -2
- package/dist/src/urbit/sse-client.js.map +1 -1
- package/dist/src/urbit/upload.js +25 -20
- package/dist/src/urbit/upload.js.map +1 -1
- package/dist/src/version.generated.js +3 -0
- package/dist/src/version.generated.js.map +1 -0
- package/package.json +11 -11
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { format } from "node:util";
|
|
2
2
|
import { getTlonRuntime } from "../runtime.js";
|
|
3
|
+
import { setSessionRole } from "../session-roles.js";
|
|
3
4
|
import { createSettingsManager } from "../settings.js";
|
|
4
5
|
import { normalizeShip, parseChannelNest } from "../targets.js";
|
|
5
6
|
import { resolveTlonAccount } from "../types.js";
|
|
@@ -9,12 +10,22 @@ import { configureTlonApiWithPoke } from "../urbit/api-client.js";
|
|
|
9
10
|
import { sendDm, sendChannelPost } from "../urbit/send.js";
|
|
10
11
|
import { markdownToStory } from "../urbit/story.js";
|
|
11
12
|
import { UrbitSSEClient } from "../urbit/sse-client.js";
|
|
12
|
-
import { createPendingApproval, formatApprovalRequest, formatApprovalConfirmation,
|
|
13
|
+
import { createPendingApproval, formatApprovalRequest, formatApprovalConfirmation, findPendingApproval, removePendingApproval, pruneExpired, formatBlockedList, formatPendingList, isExpired, emojiToApprovalAction, normalizeNotificationId, } from "./approval.js";
|
|
14
|
+
import { setBridge, removeBridge } from "./command-bridge.js";
|
|
13
15
|
import { fetchAllChannels, fetchInitData } from "./discovery.js";
|
|
14
16
|
import { cacheMessage, lookupCachedMessage, getChannelHistory, fetchThreadHistory } from "./history.js";
|
|
15
17
|
import { downloadMessageImages } from "./media.js";
|
|
16
18
|
import { createProcessedMessageTracker } from "./processed-messages.js";
|
|
17
|
-
import { extractMessageText, extractCites, formatModelName, isBotMentioned, stripBotMention, isDmAllowed, isSummarizationRequest, } from "./utils.js";
|
|
19
|
+
import { extractMessageText, extractCites, formatModelName, isBotMentioned, stripBotMention, isDmAllowed, isSummarizationRequest, sanitizeMessageText, } from "./utils.js";
|
|
20
|
+
/**
|
|
21
|
+
* Extract ship from author field, handling both string (ship) and object (bot-meta) formats.
|
|
22
|
+
*/
|
|
23
|
+
function extractAuthorShip(author) {
|
|
24
|
+
if (typeof author === "object" && author !== null && "ship" in author) {
|
|
25
|
+
return author.ship;
|
|
26
|
+
}
|
|
27
|
+
return typeof author === "string" ? author : "";
|
|
28
|
+
}
|
|
18
29
|
/**
|
|
19
30
|
* Resolve channel authorization by merging file config with settings store.
|
|
20
31
|
* Settings store takes precedence for fields it defines.
|
|
@@ -116,6 +127,11 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
116
127
|
let groupChannels = [];
|
|
117
128
|
const channelToGroup = new Map();
|
|
118
129
|
let botNickname = null;
|
|
130
|
+
let botAvatar = null;
|
|
131
|
+
// Helper to get bot profile for outbound messages
|
|
132
|
+
const getBotProfile = () => botNickname || botAvatar
|
|
133
|
+
? { nickname: botNickname || "", avatar: botAvatar || "" }
|
|
134
|
+
: undefined;
|
|
119
135
|
// Settings store manager for hot-reloading config
|
|
120
136
|
const settingsManager = createSettingsManager(api, {
|
|
121
137
|
log: (msg) => runtime.log?.(msg),
|
|
@@ -135,6 +151,12 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
135
151
|
let currentSettings = {};
|
|
136
152
|
// Track threads we've participated in (by parentId) - respond without mention requirement
|
|
137
153
|
const participatedThreads = new Set();
|
|
154
|
+
// Track consecutive bot responses per channel/DM for rate limiting
|
|
155
|
+
// Key: channel nest or dm partner ship, Value: count of consecutive bot messages
|
|
156
|
+
const consecutiveBotMessages = new Map();
|
|
157
|
+
// Known bot ships (ships that have sent messages with BotProfile author)
|
|
158
|
+
const knownBotShips = new Set();
|
|
159
|
+
const maxBotResponses = account.maxConsecutiveBotResponses ?? 3;
|
|
138
160
|
// Track DM senders per session to detect shared sessions (security warning)
|
|
139
161
|
const dmSendersBySession = new Map();
|
|
140
162
|
let sharedSessionWarningSent = false;
|
|
@@ -160,9 +182,10 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
160
182
|
if (selfProfile && typeof selfProfile === "object") {
|
|
161
183
|
const profile = selfProfile;
|
|
162
184
|
botNickname = profile.nickname?.value || null;
|
|
185
|
+
botAvatar = profile.avatar?.value || null;
|
|
163
186
|
if (botNickname) {
|
|
164
187
|
runtime.log?.(`[tlon] Bot nickname: ${botNickname}`);
|
|
165
|
-
nicknameCache.set(botShipName, botNickname);
|
|
188
|
+
nicknameCache.set(botShipName, sanitizeNickname(botNickname));
|
|
166
189
|
}
|
|
167
190
|
}
|
|
168
191
|
}
|
|
@@ -176,7 +199,7 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
176
199
|
for (const [ship, contact] of Object.entries(allContacts)) {
|
|
177
200
|
const nickname = contact?.nickname?.value ?? contact?.nickname;
|
|
178
201
|
if (nickname && typeof nickname === "string") {
|
|
179
|
-
nicknameCache.set(normalizeShip(ship), nickname);
|
|
202
|
+
nicknameCache.set(normalizeShip(ship), sanitizeNickname(nickname));
|
|
180
203
|
}
|
|
181
204
|
}
|
|
182
205
|
runtime.log?.(`[tlon] Loaded ${nicknameCache.size} contact nickname(s)`);
|
|
@@ -187,6 +210,22 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
187
210
|
}
|
|
188
211
|
// Store init foreigns for processing after settings are loaded
|
|
189
212
|
let initForeigns = null;
|
|
213
|
+
// Group name cache for human-readable display (flag -> title)
|
|
214
|
+
const groupNameCache = new Map();
|
|
215
|
+
// Build display context for approval formatting
|
|
216
|
+
function buildDisplayContext() {
|
|
217
|
+
const channelNames = new Map();
|
|
218
|
+
for (const nest of watchedChannels) {
|
|
219
|
+
const parsed = parseChannelNest(nest);
|
|
220
|
+
if (parsed) {
|
|
221
|
+
channelNames.set(nest, parsed.channelName);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
channelNames,
|
|
226
|
+
groupNames: groupNameCache,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
190
229
|
// Migrate file config to settings store (seed on first run)
|
|
191
230
|
async function migrateConfigToSettings() {
|
|
192
231
|
const migrations = [
|
|
@@ -230,12 +269,17 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
230
269
|
fileValue: account.showModelSignature,
|
|
231
270
|
settingsValue: currentSettings.showModelSig,
|
|
232
271
|
},
|
|
272
|
+
{
|
|
273
|
+
key: "ownerShip",
|
|
274
|
+
fileValue: account.ownerShip,
|
|
275
|
+
settingsValue: currentSettings.ownerShip,
|
|
276
|
+
},
|
|
233
277
|
];
|
|
234
278
|
for (const { key, fileValue, settingsValue } of migrations) {
|
|
235
279
|
// Only migrate if file has a value and settings store doesn't
|
|
236
280
|
const hasFileValue = Array.isArray(fileValue) ? fileValue.length > 0 : fileValue != null;
|
|
237
281
|
const hasSettingsValue = Array.isArray(settingsValue)
|
|
238
|
-
?
|
|
282
|
+
? true // empty array = intentionally set in settings store
|
|
239
283
|
: settingsValue != null;
|
|
240
284
|
if (hasFileValue && !hasSettingsValue) {
|
|
241
285
|
try {
|
|
@@ -273,9 +317,9 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
273
317
|
effectiveAutoDiscoverChannels = currentSettings.autoDiscoverChannels;
|
|
274
318
|
runtime.log?.(`[tlon] Using autoDiscoverChannels from settings store: ${effectiveAutoDiscoverChannels}`);
|
|
275
319
|
}
|
|
276
|
-
if (currentSettings.dmAllowlist
|
|
320
|
+
if (currentSettings.dmAllowlist !== undefined) {
|
|
277
321
|
effectiveDmAllowlist = currentSettings.dmAllowlist;
|
|
278
|
-
runtime.log?.(`[tlon] Using dmAllowlist from settings store: ${effectiveDmAllowlist.join(", ")}`);
|
|
322
|
+
runtime.log?.(`[tlon] Using dmAllowlist from settings store: ${effectiveDmAllowlist.length > 0 ? effectiveDmAllowlist.join(", ") : "(empty)"}`);
|
|
279
323
|
}
|
|
280
324
|
if (currentSettings.showModelSig !== undefined) {
|
|
281
325
|
effectiveShowModelSig = currentSettings.showModelSig;
|
|
@@ -315,6 +359,10 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
315
359
|
for (const [nest, groupFlag] of initData.channelToGroup) {
|
|
316
360
|
channelToGroup.set(nest, groupFlag);
|
|
317
361
|
}
|
|
362
|
+
// Populate group name cache for human-readable display
|
|
363
|
+
for (const [flag, title] of initData.groupNames) {
|
|
364
|
+
groupNameCache.set(flag, title);
|
|
365
|
+
}
|
|
318
366
|
initForeigns = initData.foreigns;
|
|
319
367
|
}
|
|
320
368
|
catch (error) {
|
|
@@ -427,6 +475,33 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
427
475
|
runtime.error?.(`[tlon] Failed to update dmAllowlist: ${String(err)}`);
|
|
428
476
|
}
|
|
429
477
|
}
|
|
478
|
+
// Helper to remove ship from dmAllowlist in both memory and settings store
|
|
479
|
+
async function removeFromDmAllowlist(ship) {
|
|
480
|
+
const normalizedShip = normalizeShip(ship);
|
|
481
|
+
const before = effectiveDmAllowlist.length;
|
|
482
|
+
effectiveDmAllowlist = effectiveDmAllowlist.filter((s) => s !== normalizedShip);
|
|
483
|
+
if (effectiveDmAllowlist.length === before) {
|
|
484
|
+
return; // Ship wasn't on the list
|
|
485
|
+
}
|
|
486
|
+
try {
|
|
487
|
+
await api.poke({
|
|
488
|
+
app: "settings",
|
|
489
|
+
mark: "settings-event",
|
|
490
|
+
json: {
|
|
491
|
+
"put-entry": {
|
|
492
|
+
desk: "moltbot",
|
|
493
|
+
"bucket-key": "tlon",
|
|
494
|
+
"entry-key": "dmAllowlist",
|
|
495
|
+
value: effectiveDmAllowlist,
|
|
496
|
+
},
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
runtime.log?.(`[tlon] Removed ${normalizedShip} from dmAllowlist`);
|
|
500
|
+
}
|
|
501
|
+
catch (err) {
|
|
502
|
+
runtime.error?.(`[tlon] Failed to update dmAllowlist: ${String(err)}`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
430
505
|
// Helper to update channelRules in settings store
|
|
431
506
|
async function addToChannelAllowlist(ship, channelNest) {
|
|
432
507
|
const normalizedShip = normalizeShip(ship);
|
|
@@ -476,12 +551,26 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
476
551
|
runtime.error?.(`[tlon] Failed to block ship ${normalizedShip}: ${String(err)}`);
|
|
477
552
|
}
|
|
478
553
|
}
|
|
554
|
+
/**
|
|
555
|
+
* Scry the chat agent's blocked ship list with an explicit timeout.
|
|
556
|
+
* The urbitFetch timeout (30s) may not fire if the underlying connection
|
|
557
|
+
* stalls (e.g. after a chat-block-ship poke causes the agent to restart).
|
|
558
|
+
* This wrapper guarantees resolution within SCRY_TIMEOUT_MS.
|
|
559
|
+
*/
|
|
560
|
+
const SCRY_TIMEOUT_MS = 15_000;
|
|
561
|
+
async function scryBlockedShips() {
|
|
562
|
+
const blocked = await Promise.race([
|
|
563
|
+
api.scry("/chat/blocked.json"),
|
|
564
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("blocked list scry timeout")), SCRY_TIMEOUT_MS)),
|
|
565
|
+
]);
|
|
566
|
+
return Array.isArray(blocked) ? blocked : [];
|
|
567
|
+
}
|
|
479
568
|
// Check if a ship is blocked using Tlon's native block list
|
|
480
569
|
async function isShipBlocked(ship) {
|
|
481
570
|
const normalizedShip = normalizeShip(ship);
|
|
482
571
|
try {
|
|
483
|
-
const blocked =
|
|
484
|
-
return
|
|
572
|
+
const blocked = await scryBlockedShips();
|
|
573
|
+
return blocked.some((s) => normalizeShip(s) === normalizedShip);
|
|
485
574
|
}
|
|
486
575
|
catch (err) {
|
|
487
576
|
runtime.log?.(`[tlon] Failed to check blocked list: ${String(err)}`);
|
|
@@ -491,8 +580,7 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
491
580
|
// Get all blocked ships
|
|
492
581
|
async function getBlockedShips() {
|
|
493
582
|
try {
|
|
494
|
-
|
|
495
|
-
return Array.isArray(blocked) ? blocked : [];
|
|
583
|
+
return await scryBlockedShips();
|
|
496
584
|
}
|
|
497
585
|
catch (err) {
|
|
498
586
|
runtime.log?.(`[tlon] Failed to get blocked list: ${String(err)}`);
|
|
@@ -516,24 +604,65 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
516
604
|
return false;
|
|
517
605
|
}
|
|
518
606
|
}
|
|
519
|
-
// Helper to send DM notification to owner
|
|
607
|
+
// Helper to send DM notification to owner. Returns the message ID if sent successfully.
|
|
520
608
|
async function sendOwnerNotification(message) {
|
|
521
609
|
if (!effectiveOwnerShip) {
|
|
522
610
|
runtime.log?.("[tlon] No ownerShip configured, cannot send notification");
|
|
523
|
-
return;
|
|
611
|
+
return undefined;
|
|
524
612
|
}
|
|
525
613
|
try {
|
|
526
|
-
await sendDm({
|
|
614
|
+
const result = await sendDm({
|
|
615
|
+
botProfile: getBotProfile(),
|
|
527
616
|
fromShip: botShipName,
|
|
528
617
|
toShip: effectiveOwnerShip,
|
|
529
618
|
text: message,
|
|
530
619
|
});
|
|
531
620
|
runtime.log?.(`[tlon] Sent notification to owner ${effectiveOwnerShip}`);
|
|
621
|
+
return result.messageId;
|
|
532
622
|
}
|
|
533
623
|
catch (err) {
|
|
534
624
|
runtime.error?.(`[tlon] Failed to send notification to owner: ${String(err)}`);
|
|
625
|
+
return undefined;
|
|
535
626
|
}
|
|
536
627
|
}
|
|
628
|
+
// Regex to match block directives in agent responses
|
|
629
|
+
// Format: [BLOCK_USER: ~ship-name | reason for blocking]
|
|
630
|
+
const blockDirectiveRegex = /\[BLOCK_USER:\s*(~[\w-]+)\s*\|\s*(.+?)\]/g;
|
|
631
|
+
// Process block directives from agent response and return text with directives stripped
|
|
632
|
+
async function processBlockDirectives(text, senderShip) {
|
|
633
|
+
const matches = [...text.matchAll(blockDirectiveRegex)];
|
|
634
|
+
if (matches.length > 0) {
|
|
635
|
+
runtime.log?.(`[tlon] Found ${matches.length} block directive(s) in response`);
|
|
636
|
+
runtime.log?.(`[tlon] Sender ship: "${senderShip}" -> normalized: "${normalizeShip(senderShip)}"`);
|
|
637
|
+
runtime.log?.(`[tlon] Owner ship: "${effectiveOwnerShip}"`);
|
|
638
|
+
}
|
|
639
|
+
for (const match of matches) {
|
|
640
|
+
const targetShip = normalizeShip(match[1]);
|
|
641
|
+
const reason = match[2].trim();
|
|
642
|
+
runtime.log?.(`[tlon] Processing block directive: target="${targetShip}", reason="${reason}"`);
|
|
643
|
+
// Safety: Never block the owner
|
|
644
|
+
if (effectiveOwnerShip && targetShip === effectiveOwnerShip) {
|
|
645
|
+
runtime.log?.(`[tlon] Agent attempted to block owner ship ${targetShip} - ignoring`);
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
// Only allow blocking the current message sender (not arbitrary third parties)
|
|
649
|
+
const normalizedSender = normalizeShip(senderShip);
|
|
650
|
+
if (targetShip !== normalizedSender) {
|
|
651
|
+
runtime.log?.(`[tlon] Agent tried to block "${targetShip}" but sender is "${normalizedSender}" - ignoring`);
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
// Block the abusive sender
|
|
655
|
+
runtime.log?.(`[tlon] Executing block for ${targetShip}...`);
|
|
656
|
+
await blockShip(targetShip);
|
|
657
|
+
// Notify owner
|
|
658
|
+
if (effectiveOwnerShip) {
|
|
659
|
+
await sendOwnerNotification(`[Agent Action] Blocked ${targetShip}\nReason: ${reason}`);
|
|
660
|
+
}
|
|
661
|
+
runtime.log?.(`[tlon] Agent blocked ${targetShip}: ${reason}`);
|
|
662
|
+
}
|
|
663
|
+
// Strip directives from visible response
|
|
664
|
+
return text.replace(blockDirectiveRegex, "").trim();
|
|
665
|
+
}
|
|
537
666
|
// Queue a new approval request and notify the owner
|
|
538
667
|
async function queueApprovalRequest(approval) {
|
|
539
668
|
// Check if ship is blocked - silently ignore
|
|
@@ -554,33 +683,36 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
554
683
|
existing.messagePreview = approval.messagePreview;
|
|
555
684
|
}
|
|
556
685
|
runtime.log?.(`[tlon] Updated existing approval for ${approval.requestingShip} (${approval.type}) - re-sending notification`);
|
|
686
|
+
// Send notification first, then save once with the notification ID.
|
|
687
|
+
// Saving before sendOwnerNotification causes a race: the settings subscription
|
|
688
|
+
// event replaces pendingApprovals in-memory, so the notificationMessageId
|
|
689
|
+
// set on the old object reference is lost.
|
|
690
|
+
const existMsg = formatApprovalRequest(existing, buildDisplayContext());
|
|
691
|
+
const existNotifId = await sendOwnerNotification(existMsg);
|
|
692
|
+
if (existNotifId) {
|
|
693
|
+
existing.notificationMessageId = normalizeNotificationId(existNotifId);
|
|
694
|
+
}
|
|
557
695
|
await savePendingApprovals();
|
|
558
|
-
const message = formatApprovalRequest(existing);
|
|
559
|
-
await sendOwnerNotification(message);
|
|
560
696
|
return;
|
|
561
697
|
}
|
|
698
|
+
// Send notification before saving so notificationMessageId is included
|
|
699
|
+
// in the single save. See comment above about the settings subscription race.
|
|
700
|
+
const message = formatApprovalRequest(approval, buildDisplayContext());
|
|
701
|
+
const notifId = await sendOwnerNotification(message);
|
|
702
|
+
if (notifId) {
|
|
703
|
+
approval.notificationMessageId = normalizeNotificationId(notifId);
|
|
704
|
+
}
|
|
562
705
|
pendingApprovals.push(approval);
|
|
563
706
|
await savePendingApprovals();
|
|
564
|
-
const message = formatApprovalRequest(approval);
|
|
565
|
-
await sendOwnerNotification(message);
|
|
566
707
|
runtime.log?.(`[tlon] Queued approval request: ${approval.id} (${approval.type} from ${approval.requestingShip})`);
|
|
567
708
|
}
|
|
568
|
-
//
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
if (
|
|
572
|
-
return false;
|
|
573
|
-
}
|
|
574
|
-
const approval = findPendingApproval(pendingApprovals, parsed.id);
|
|
575
|
-
if (!approval) {
|
|
576
|
-
await sendOwnerNotification("No pending approval found" + (parsed.id ? ` for ID: ${parsed.id}` : ""));
|
|
577
|
-
return true; // Still consumed the message
|
|
578
|
-
}
|
|
579
|
-
if (parsed.action === "approve") {
|
|
709
|
+
// ── Approval action execution ─────────────────────────────────────
|
|
710
|
+
// Shared by the slash command bridge and the reaction-based approval handler.
|
|
711
|
+
async function executeApprovalAction(approval, action) {
|
|
712
|
+
if (action === "approve") {
|
|
580
713
|
switch (approval.type) {
|
|
581
714
|
case "dm":
|
|
582
715
|
await addToDmAllowlist(approval.requestingShip);
|
|
583
|
-
// Process the original message if available
|
|
584
716
|
if (approval.originalMessage) {
|
|
585
717
|
runtime.log?.(`[tlon] Processing original message from ${approval.requestingShip} after approval`);
|
|
586
718
|
await processMessage({
|
|
@@ -596,9 +728,8 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
596
728
|
case "channel":
|
|
597
729
|
if (approval.channelNest) {
|
|
598
730
|
await addToChannelAllowlist(approval.requestingShip, approval.channelNest);
|
|
599
|
-
// Process the original message if available
|
|
600
731
|
if (approval.originalMessage) {
|
|
601
|
-
const
|
|
732
|
+
const nest = parseChannelNest(approval.channelNest);
|
|
602
733
|
runtime.log?.(`[tlon] Processing original message from ${approval.requestingShip} in ${approval.channelNest} after approval`);
|
|
603
734
|
await processMessage({
|
|
604
735
|
messageId: approval.originalMessage.messageId,
|
|
@@ -607,8 +738,8 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
607
738
|
messageContent: approval.originalMessage.messageContent,
|
|
608
739
|
isGroup: true,
|
|
609
740
|
channelNest: approval.channelNest,
|
|
610
|
-
hostShip:
|
|
611
|
-
channelName:
|
|
741
|
+
hostShip: nest?.hostShip,
|
|
742
|
+
channelName: nest?.channelName,
|
|
612
743
|
timestamp: approval.originalMessage.timestamp,
|
|
613
744
|
parentId: approval.originalMessage.parentId,
|
|
614
745
|
isThreadReply: approval.originalMessage.isThreadReply,
|
|
@@ -617,7 +748,6 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
617
748
|
}
|
|
618
749
|
break;
|
|
619
750
|
case "group":
|
|
620
|
-
// Accept the group invite (don't add to allowlist - each invite requires approval)
|
|
621
751
|
if (approval.groupFlag) {
|
|
622
752
|
try {
|
|
623
753
|
await api.poke({
|
|
@@ -629,8 +759,6 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
629
759
|
},
|
|
630
760
|
});
|
|
631
761
|
runtime.log?.(`[tlon] Joined group ${approval.groupFlag} after approval`);
|
|
632
|
-
// Immediately discover channels from the newly joined group
|
|
633
|
-
// Small delay to allow the join to propagate
|
|
634
762
|
setTimeout(async () => {
|
|
635
763
|
try {
|
|
636
764
|
const discoveredChannels = await fetchAllChannels(api, runtime);
|
|
@@ -656,58 +784,52 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
656
784
|
}
|
|
657
785
|
break;
|
|
658
786
|
}
|
|
659
|
-
await sendOwnerNotification(formatApprovalConfirmation(approval, "approve"));
|
|
660
787
|
}
|
|
661
|
-
else if (
|
|
662
|
-
// Block the ship using Tlon's native blocking
|
|
788
|
+
else if (action === "block") {
|
|
663
789
|
await blockShip(approval.requestingShip);
|
|
664
|
-
await
|
|
790
|
+
await removeFromDmAllowlist(approval.requestingShip);
|
|
665
791
|
}
|
|
666
|
-
|
|
667
|
-
// Denied - just remove from pending, no notification to requester
|
|
668
|
-
await sendOwnerNotification(formatApprovalConfirmation(approval, "deny"));
|
|
669
|
-
}
|
|
670
|
-
// Remove from pending
|
|
792
|
+
// "deny" — no side effects beyond removing from pending
|
|
671
793
|
pendingApprovals = removePendingApproval(pendingApprovals, approval.id);
|
|
672
794
|
await savePendingApprovals();
|
|
673
|
-
return
|
|
795
|
+
return formatApprovalConfirmation(approval, action, buildDisplayContext());
|
|
674
796
|
}
|
|
675
|
-
//
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
runtime.log?.(`[tlon] Owner requested pending approvals list (${pendingApprovals.length} pending)`);
|
|
691
|
-
return true;
|
|
692
|
-
}
|
|
693
|
-
case "unblock": {
|
|
694
|
-
const shipToUnblock = command.ship;
|
|
695
|
-
const isBlocked = await isShipBlocked(shipToUnblock);
|
|
696
|
-
if (!isBlocked) {
|
|
697
|
-
await sendOwnerNotification(`${shipToUnblock} is not blocked.`);
|
|
698
|
-
return true;
|
|
699
|
-
}
|
|
700
|
-
const success = await unblockShip(shipToUnblock);
|
|
701
|
-
if (success) {
|
|
702
|
-
await sendOwnerNotification(`Unblocked ${shipToUnblock}.`);
|
|
703
|
-
}
|
|
704
|
-
else {
|
|
705
|
-
await sendOwnerNotification(`Failed to unblock ${shipToUnblock}.`);
|
|
706
|
-
}
|
|
707
|
-
return true;
|
|
797
|
+
// ── Command bridge ──────────────────────────────────────────────────
|
|
798
|
+
// Exposes approval/admin actions to slash commands registered in index.ts.
|
|
799
|
+
// Handlers return response text; the slash command framework sends it back.
|
|
800
|
+
const accountKey = opts.accountId ?? undefined;
|
|
801
|
+
const commandBridge = {
|
|
802
|
+
get ownerShip() {
|
|
803
|
+
return effectiveOwnerShip;
|
|
804
|
+
},
|
|
805
|
+
async handleAction(action, id) {
|
|
806
|
+
// Prune expired approvals
|
|
807
|
+
pendingApprovals = pruneExpired(pendingApprovals);
|
|
808
|
+
await savePendingApprovals();
|
|
809
|
+
const approval = findPendingApproval(pendingApprovals, id);
|
|
810
|
+
if (!approval) {
|
|
811
|
+
return "No pending approval found" + (id ? ` for ID: #${id}` : ".");
|
|
708
812
|
}
|
|
709
|
-
|
|
710
|
-
|
|
813
|
+
return executeApprovalAction(approval, action);
|
|
814
|
+
},
|
|
815
|
+
async getPendingList() {
|
|
816
|
+
return formatPendingList(pendingApprovals, buildDisplayContext());
|
|
817
|
+
},
|
|
818
|
+
async getBlockedList() {
|
|
819
|
+
const blockedShips = await getBlockedShips();
|
|
820
|
+
return formatBlockedList(blockedShips);
|
|
821
|
+
},
|
|
822
|
+
async handleUnblock(ship) {
|
|
823
|
+
runtime.log?.(`[tlon] handleUnblock: checking if ${ship} is blocked...`);
|
|
824
|
+
const blocked = await isShipBlocked(ship);
|
|
825
|
+
if (!blocked) {
|
|
826
|
+
return `${ship} is not blocked.`;
|
|
827
|
+
}
|
|
828
|
+
const success = await unblockShip(ship);
|
|
829
|
+
return success ? `Unblocked ${ship}.` : `Failed to unblock ${ship}.`;
|
|
830
|
+
},
|
|
831
|
+
};
|
|
832
|
+
setBridge(accountKey, commandBridge);
|
|
711
833
|
// Check if a ship is the owner (always allowed to DM)
|
|
712
834
|
function isOwner(ship) {
|
|
713
835
|
if (!effectiveOwnerShip) {
|
|
@@ -731,14 +853,14 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
731
853
|
return /^~?[a-z-]+$/i.test(normalized) ? normalized : "";
|
|
732
854
|
}
|
|
733
855
|
const processMessage = async (params) => {
|
|
734
|
-
const { messageId, senderShip, isGroup, channelNest, hostShip, channelName, timestamp, parentId, isThreadReply, messageContent, } = params;
|
|
856
|
+
const { messageId, senderShip, isGroup, channelNest, hostShip: _hostShip, channelName: _channelName, timestamp, parentId, isThreadReply, messageContent, } = params;
|
|
735
857
|
// replyParentId overrides parentId for the deliver callback (thread reply routing)
|
|
736
858
|
// but doesn't affect the ctx payload (MessageThreadId/ReplyToId).
|
|
737
859
|
// Used for reactions: agent sees no thread context (so it responds), but
|
|
738
860
|
// the reply is still delivered as a thread reply.
|
|
739
861
|
const deliverParentId = params.replyParentId ?? parentId;
|
|
740
862
|
const groupChannel = channelNest; // For compatibility
|
|
741
|
-
let messageText = params.messageText;
|
|
863
|
+
let messageText = sanitizeMessageText(params.messageText);
|
|
742
864
|
const rawMessageText = messageText; // Preserve original before any modifications
|
|
743
865
|
// Strip bot mention EARLY, before thread context is prepended.
|
|
744
866
|
// This ensures [Current message] in thread context won't contain the bot ship name,
|
|
@@ -803,7 +925,7 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
803
925
|
if (threadHistory.length > 0) {
|
|
804
926
|
const threadContext = threadHistory
|
|
805
927
|
.slice(-10) // Last 10 messages for context
|
|
806
|
-
.map((msg) => `${msg.author}: ${msg.content}`)
|
|
928
|
+
.map((msg) => `${msg.author}: ${sanitizeMessageText(msg.content)}`)
|
|
807
929
|
.join("\n");
|
|
808
930
|
// Prepend thread context to the message
|
|
809
931
|
// Include note about ongoing conversation for agent judgment
|
|
@@ -824,6 +946,7 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
824
946
|
const noHistoryMsg = "I couldn't fetch any messages for this channel. It might be empty or there might be a permissions issue.";
|
|
825
947
|
if (isGroup && groupChannel) {
|
|
826
948
|
await sendChannelPost({
|
|
949
|
+
botProfile: getBotProfile(),
|
|
827
950
|
fromShip: botShipName,
|
|
828
951
|
nest: groupChannel,
|
|
829
952
|
story: markdownToStory(noHistoryMsg),
|
|
@@ -831,6 +954,7 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
831
954
|
}
|
|
832
955
|
else {
|
|
833
956
|
await sendDm({
|
|
957
|
+
botProfile: getBotProfile(),
|
|
834
958
|
fromShip: botShipName,
|
|
835
959
|
toShip: senderShip,
|
|
836
960
|
text: noHistoryMsg,
|
|
@@ -839,7 +963,7 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
839
963
|
return;
|
|
840
964
|
}
|
|
841
965
|
const historyText = history
|
|
842
|
-
.map((msg) => `[${new Date(msg.timestamp).toLocaleString()}] ${msg.author}: ${msg.content}`)
|
|
966
|
+
.map((msg) => `[${new Date(msg.timestamp).toLocaleString()}] ${msg.author}: ${sanitizeMessageText(msg.content)}`)
|
|
843
967
|
.join("\n");
|
|
844
968
|
messageText =
|
|
845
969
|
`Please summarize this channel conversation (${history.length} recent messages):\n\n${historyText}\n\n` +
|
|
@@ -853,13 +977,14 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
853
977
|
const errorMsg = `Sorry, I encountered an error while fetching the channel history: ${error?.message ?? String(error)}`;
|
|
854
978
|
if (isGroup && groupChannel) {
|
|
855
979
|
await sendChannelPost({
|
|
980
|
+
botProfile: getBotProfile(),
|
|
856
981
|
fromShip: botShipName,
|
|
857
982
|
nest: groupChannel,
|
|
858
983
|
story: markdownToStory(errorMsg),
|
|
859
984
|
});
|
|
860
985
|
}
|
|
861
986
|
else {
|
|
862
|
-
await sendDm({ fromShip: botShipName, toShip: senderShip, text: errorMsg });
|
|
987
|
+
await sendDm({ botProfile: getBotProfile(), fromShip: botShipName, toShip: senderShip, text: errorMsg });
|
|
863
988
|
}
|
|
864
989
|
return;
|
|
865
990
|
}
|
|
@@ -894,6 +1019,7 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
894
1019
|
`Docs: https://docs.openclaw.ai/concepts/session#secure-dm-mode`;
|
|
895
1020
|
// Send async, don't block message processing
|
|
896
1021
|
sendDm({
|
|
1022
|
+
botProfile: getBotProfile(),
|
|
897
1023
|
fromShip: botShipName,
|
|
898
1024
|
toShip: effectiveOwnerShip,
|
|
899
1025
|
text: warningMsg,
|
|
@@ -903,6 +1029,9 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
903
1029
|
senders.add(senderShip);
|
|
904
1030
|
}
|
|
905
1031
|
const senderRole = isOwner(senderShip) ? "owner" : "user";
|
|
1032
|
+
// Store role for before_tool_call hook (tool access control)
|
|
1033
|
+
setSessionRole(route.sessionKey, senderRole);
|
|
1034
|
+
runtime.log?.(`[tlon] Stored session role: sessionKey=${route.sessionKey}, role=${senderRole}`);
|
|
906
1035
|
const senderDisplay = formatShipWithNickname(senderShip);
|
|
907
1036
|
const fromLabel = isGroup
|
|
908
1037
|
? `${senderDisplay} [${senderRole}] in ${channelNest}`
|
|
@@ -969,7 +1098,7 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
969
1098
|
// Include downloaded media attachments
|
|
970
1099
|
...(attachments.length > 0 && { Attachments: attachments }),
|
|
971
1100
|
OriginatingChannel: "tlon",
|
|
972
|
-
OriginatingTo: `tlon:${isGroup ? groupChannel :
|
|
1101
|
+
OriginatingTo: `tlon:${isGroup ? groupChannel : senderShip}`,
|
|
973
1102
|
// Include thread context for automatic reply routing
|
|
974
1103
|
...(parentId && { MessageThreadId: String(parentId), ReplyToId: String(parentId) }),
|
|
975
1104
|
});
|
|
@@ -987,18 +1116,33 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
987
1116
|
if (!replyText) {
|
|
988
1117
|
return;
|
|
989
1118
|
}
|
|
1119
|
+
// Process any block directives in the response (strips them from text)
|
|
1120
|
+
replyText = await processBlockDirectives(replyText, senderShip);
|
|
1121
|
+
if (!replyText) {
|
|
1122
|
+
return;
|
|
1123
|
+
} // Response was only a directive
|
|
990
1124
|
// Use settings store value if set, otherwise fall back to file config
|
|
991
1125
|
const showSignature = effectiveShowModelSig;
|
|
992
1126
|
if (showSignature) {
|
|
1127
|
+
const modelCfg = cfg.agents?.defaults?.model;
|
|
993
1128
|
const modelInfo = payload.metadata?.model ||
|
|
994
1129
|
payload.model ||
|
|
995
1130
|
route.model ||
|
|
996
|
-
|
|
1131
|
+
(typeof modelCfg === "string" ? modelCfg : modelCfg?.primary);
|
|
997
1132
|
replyText = `${replyText}\n\n_[Generated by ${formatModelName(modelInfo)}]_`;
|
|
998
1133
|
}
|
|
1134
|
+
// Add addendum if this is the last response before bot rate limit
|
|
1135
|
+
if (isGroup && groupChannel && knownBotShips.has(senderShip)) {
|
|
1136
|
+
const count = consecutiveBotMessages.get(groupChannel) ?? 0;
|
|
1137
|
+
if (maxBotResponses > 0 && count === maxBotResponses) {
|
|
1138
|
+
const otherBot = formatShipWithNickname(senderShip);
|
|
1139
|
+
replyText += `\n\n---\n_This is my last response to ${otherBot} for now. To continue our conversation, someone will need to mention me._`;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
999
1142
|
if (isGroup && groupChannel) {
|
|
1000
1143
|
// Send to any channel type (chat, heap, diary) using the nest directly
|
|
1001
1144
|
await sendChannelPost({
|
|
1145
|
+
botProfile: getBotProfile(),
|
|
1002
1146
|
fromShip: botShipName,
|
|
1003
1147
|
nest: groupChannel,
|
|
1004
1148
|
story: markdownToStory(replyText),
|
|
@@ -1011,7 +1155,13 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
1011
1155
|
}
|
|
1012
1156
|
}
|
|
1013
1157
|
else {
|
|
1014
|
-
await sendDm({
|
|
1158
|
+
await sendDm({
|
|
1159
|
+
botProfile: getBotProfile(),
|
|
1160
|
+
fromShip: botShipName,
|
|
1161
|
+
toShip: senderShip,
|
|
1162
|
+
text: replyText,
|
|
1163
|
+
replyToId: deliverParentId ? String(deliverParentId) : undefined,
|
|
1164
|
+
});
|
|
1015
1165
|
}
|
|
1016
1166
|
},
|
|
1017
1167
|
onError: (err, info) => {
|
|
@@ -1117,16 +1267,16 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
1117
1267
|
// Handle post responses (new posts and replies)
|
|
1118
1268
|
const essay = response?.post?.["r-post"]?.set?.essay;
|
|
1119
1269
|
const memo = response?.post?.["r-post"]?.reply?.["r-reply"]?.set?.memo;
|
|
1120
|
-
|
|
1270
|
+
const content = memo || essay;
|
|
1271
|
+
if (!content) {
|
|
1121
1272
|
return;
|
|
1122
1273
|
}
|
|
1123
|
-
const content = memo || essay;
|
|
1124
1274
|
const isThreadReply = Boolean(memo);
|
|
1125
1275
|
const messageId = isThreadReply ? response?.post?.["r-post"]?.reply?.id : response?.post?.id;
|
|
1126
1276
|
if (!processedTracker.mark(messageId)) {
|
|
1127
1277
|
return;
|
|
1128
1278
|
}
|
|
1129
|
-
const senderShip = normalizeShip(content?.author
|
|
1279
|
+
const senderShip = normalizeShip(extractAuthorShip(content?.author));
|
|
1130
1280
|
if (!senderShip) {
|
|
1131
1281
|
return;
|
|
1132
1282
|
}
|
|
@@ -1144,10 +1294,18 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
1144
1294
|
timestamp: content.sent || Date.now(),
|
|
1145
1295
|
id: messageId,
|
|
1146
1296
|
});
|
|
1297
|
+
// Check if sender is a bot (BotProfile object has ship, nickname, avatar)
|
|
1298
|
+
const authorRaw = content?.author;
|
|
1299
|
+
const isSenderBot = typeof authorRaw === 'object' && authorRaw !== null && 'ship' in authorRaw;
|
|
1300
|
+
if (isSenderBot) {
|
|
1301
|
+
knownBotShips.add(senderShip);
|
|
1302
|
+
}
|
|
1147
1303
|
// Skip processing bot's own messages (but they're already cached above)
|
|
1148
1304
|
if (senderShip === botShipName) {
|
|
1149
1305
|
return;
|
|
1150
1306
|
}
|
|
1307
|
+
// Check if sender is a known bot (for rate limiting later)
|
|
1308
|
+
const isKnownBot = isSenderBot || knownBotShips.has(senderShip);
|
|
1151
1309
|
// Get thread info early for participation check
|
|
1152
1310
|
const seal = isThreadReply
|
|
1153
1311
|
? response?.post?.["r-post"]?.reply?.["r-reply"]?.set?.seal
|
|
@@ -1165,6 +1323,22 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
1165
1323
|
if (inParticipatedThread && !mentioned) {
|
|
1166
1324
|
runtime.log?.(`[tlon] Responding to thread we participated in (no mention): ${parentId}`);
|
|
1167
1325
|
}
|
|
1326
|
+
// Rate limit consecutive bot responses (only in group channels)
|
|
1327
|
+
if (isKnownBot) {
|
|
1328
|
+
const count = (consecutiveBotMessages.get(nest) ?? 0) + 1;
|
|
1329
|
+
consecutiveBotMessages.set(nest, count);
|
|
1330
|
+
runtime.log?.(`[tlon] Bot mention from ${senderShip} in ${nest}: consecutive count = ${count}`);
|
|
1331
|
+
if (maxBotResponses > 0 && count > maxBotResponses) {
|
|
1332
|
+
runtime.log?.(`[tlon] Rate limiting: skipping response to bot ${senderShip} (count ${count} > limit ${maxBotResponses})`);
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
else {
|
|
1337
|
+
// Human mention resets the consecutive bot counter
|
|
1338
|
+
// (requires explicit engagement, not just any human message)
|
|
1339
|
+
consecutiveBotMessages.set(nest, 0);
|
|
1340
|
+
runtime.log?.(`[tlon] Human mention from ${senderShip} in ${nest}: reset bot counter`);
|
|
1341
|
+
}
|
|
1168
1342
|
// Owner is always allowed
|
|
1169
1343
|
if (isOwner(senderShip)) {
|
|
1170
1344
|
runtime.log?.(`[tlon] Owner ${senderShip} is always allowed in channels`);
|
|
@@ -1189,7 +1363,7 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
1189
1363
|
parentId: parentId ?? undefined,
|
|
1190
1364
|
isThreadReply,
|
|
1191
1365
|
},
|
|
1192
|
-
});
|
|
1366
|
+
}, pendingApprovals.map((a) => a.id));
|
|
1193
1367
|
await queueApprovalRequest(approval);
|
|
1194
1368
|
}
|
|
1195
1369
|
else {
|
|
@@ -1268,7 +1442,7 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
1268
1442
|
type: "dm",
|
|
1269
1443
|
requestingShip: ship,
|
|
1270
1444
|
messagePreview: "(DM invite - no message yet)",
|
|
1271
|
-
});
|
|
1445
|
+
}, pendingApprovals.map((a) => a.id));
|
|
1272
1446
|
await queueApprovalRequest(approval);
|
|
1273
1447
|
processedDmInvites.add(ship); // Mark as processed to avoid duplicate notifications
|
|
1274
1448
|
}
|
|
@@ -1290,9 +1464,34 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
1290
1464
|
if (dmAddReact || dmDelReact) {
|
|
1291
1465
|
const isAdd = Boolean(dmAddReact);
|
|
1292
1466
|
const reactData = dmAddReact || dmDelReact;
|
|
1293
|
-
const reactAuthor = normalizeShip(reactData?.author
|
|
1294
|
-
const reactEmoji =
|
|
1467
|
+
const reactAuthor = normalizeShip(extractAuthorShip(reactData?.author) || reactData?.ship || "");
|
|
1468
|
+
const reactEmoji = dmAddReact?.react ?? "";
|
|
1295
1469
|
if (reactAuthor && reactAuthor !== botShipName) {
|
|
1470
|
+
// Check if this is an approval reaction from the owner on a notification message
|
|
1471
|
+
if (isAdd && isOwner(reactAuthor)) {
|
|
1472
|
+
const approvalAction = emojiToApprovalAction(reactEmoji);
|
|
1473
|
+
if (approvalAction) {
|
|
1474
|
+
const normalizedEventId = normalizeNotificationId(messageId);
|
|
1475
|
+
const matchedApproval = pendingApprovals.find((a) => a.notificationMessageId === normalizedEventId);
|
|
1476
|
+
if (matchedApproval) {
|
|
1477
|
+
if (isExpired(matchedApproval)) {
|
|
1478
|
+
runtime.log?.(`[tlon] Ignoring reaction on expired approval #${matchedApproval.id}`);
|
|
1479
|
+
// Fall through to normal reaction handling
|
|
1480
|
+
}
|
|
1481
|
+
else {
|
|
1482
|
+
runtime.log?.(`[tlon] Reaction-based approval: ${reactEmoji} → ${approvalAction} for #${matchedApproval.id}`);
|
|
1483
|
+
try {
|
|
1484
|
+
const confirmText = await executeApprovalAction(matchedApproval, approvalAction);
|
|
1485
|
+
await sendOwnerNotification(confirmText);
|
|
1486
|
+
}
|
|
1487
|
+
catch (err) {
|
|
1488
|
+
runtime.error?.(`[tlon] Reaction approval error: ${String(err)}`);
|
|
1489
|
+
}
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1296
1495
|
try {
|
|
1297
1496
|
const partnerShip = extractDmPartnerShip(whom);
|
|
1298
1497
|
const route = core.channel.routing.resolveAgentRoute({
|
|
@@ -1354,7 +1553,7 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
1354
1553
|
dmReplyOwnId = dmReply.id ?? dmReply.delta?.add?.id;
|
|
1355
1554
|
// If no explicit reply ID, construct from author/sent (same format as our outbound)
|
|
1356
1555
|
if (!dmReplyOwnId && dmReplyMemo?.author && dmReplyMemo?.sent) {
|
|
1357
|
-
dmReplyOwnId = `${normalizeShip(dmReplyMemo.author)}/${dmReplyMemo.sent}`;
|
|
1556
|
+
dmReplyOwnId = `${normalizeShip(extractAuthorShip(dmReplyMemo.author))}/${dmReplyMemo.sent}`;
|
|
1358
1557
|
}
|
|
1359
1558
|
}
|
|
1360
1559
|
if (!dmContent) {
|
|
@@ -1365,7 +1564,7 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
1365
1564
|
if (!processedTracker.mark(effectiveMessageId)) {
|
|
1366
1565
|
return;
|
|
1367
1566
|
}
|
|
1368
|
-
const authorShip = normalizeShip(dmContent?.author
|
|
1567
|
+
const authorShip = normalizeShip(extractAuthorShip(dmContent?.author));
|
|
1369
1568
|
const partnerShip = extractDmPartnerShip(whom);
|
|
1370
1569
|
const senderShip = partnerShip || authorShip;
|
|
1371
1570
|
// Cache DM messages (including bot's own) so reaction lookups have context
|
|
@@ -1397,22 +1596,6 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
1397
1596
|
if (!messageText.trim()) {
|
|
1398
1597
|
return;
|
|
1399
1598
|
}
|
|
1400
|
-
// Check if this is the owner sending an approval response
|
|
1401
|
-
if (isOwner(senderShip) && isApprovalResponse(messageText)) {
|
|
1402
|
-
const handled = await handleApprovalResponse(messageText);
|
|
1403
|
-
if (handled) {
|
|
1404
|
-
runtime.log?.(`[tlon] Processed approval response from owner: ${messageText}`);
|
|
1405
|
-
return;
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
// Check if this is the owner sending an admin command
|
|
1409
|
-
if (isOwner(senderShip) && isAdminCommand(messageText)) {
|
|
1410
|
-
const handled = await handleAdminCommand(messageText);
|
|
1411
|
-
if (handled) {
|
|
1412
|
-
runtime.log?.(`[tlon] Processed admin command from owner: ${messageText}`);
|
|
1413
|
-
return;
|
|
1414
|
-
}
|
|
1415
|
-
}
|
|
1416
1599
|
// Owner is always allowed to DM (bypass allowlist)
|
|
1417
1600
|
if (isOwner(senderShip)) {
|
|
1418
1601
|
runtime.log?.(`[tlon] Processing DM from owner ${senderShip}${isDmThreadReply ? ` (thread reply, parent=${dmReplyParentId}, replyId=${effectiveMessageId})` : ""}`);
|
|
@@ -1442,7 +1625,7 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
1442
1625
|
messageContent: dmContent.content,
|
|
1443
1626
|
timestamp: dmContent.sent || Date.now(),
|
|
1444
1627
|
},
|
|
1445
|
-
});
|
|
1628
|
+
}, pendingApprovals.map((a) => a.id));
|
|
1446
1629
|
await queueApprovalRequest(approval);
|
|
1447
1630
|
}
|
|
1448
1631
|
else {
|
|
@@ -1471,7 +1654,7 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
1471
1654
|
await api.subscribe({
|
|
1472
1655
|
app: "channels",
|
|
1473
1656
|
path: "/v2",
|
|
1474
|
-
event: handleChannelsFirehose,
|
|
1657
|
+
event: (data) => handleChannelsFirehose(data),
|
|
1475
1658
|
err: (error) => {
|
|
1476
1659
|
runtime.error?.(`[tlon] Channels firehose error: ${String(error)}`);
|
|
1477
1660
|
},
|
|
@@ -1484,7 +1667,7 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
1484
1667
|
await api.subscribe({
|
|
1485
1668
|
app: "chat",
|
|
1486
1669
|
path: "/v3",
|
|
1487
|
-
event: handleChatFirehose,
|
|
1670
|
+
event: (data) => handleChatFirehose(data),
|
|
1488
1671
|
err: (error) => {
|
|
1489
1672
|
runtime.error?.(`[tlon] Chat firehose error: ${String(error)}`);
|
|
1490
1673
|
},
|
|
@@ -1502,18 +1685,23 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
1502
1685
|
// Look for self profile updates
|
|
1503
1686
|
if (event?.self) {
|
|
1504
1687
|
const selfUpdate = event.self;
|
|
1505
|
-
if (selfUpdate?.contact?.nickname?.value !== undefined) {
|
|
1688
|
+
if (selfUpdate?.contact?.nickname?.value !== undefined || selfUpdate?.contact?.avatar?.value !== undefined) {
|
|
1506
1689
|
const newNickname = selfUpdate.contact.nickname.value || null;
|
|
1507
1690
|
if (newNickname !== botNickname) {
|
|
1508
1691
|
botNickname = newNickname;
|
|
1509
1692
|
runtime.log?.(`[tlon] Bot nickname updated: ${botNickname}`);
|
|
1510
1693
|
if (botNickname) {
|
|
1511
|
-
nicknameCache.set(botShipName, botNickname);
|
|
1694
|
+
nicknameCache.set(botShipName, sanitizeNickname(botNickname));
|
|
1512
1695
|
}
|
|
1513
1696
|
else {
|
|
1514
1697
|
nicknameCache.delete(botShipName);
|
|
1515
1698
|
}
|
|
1516
1699
|
}
|
|
1700
|
+
const newAvatar = selfUpdate.contact?.avatar?.value || null;
|
|
1701
|
+
if (newAvatar !== botAvatar) {
|
|
1702
|
+
botAvatar = newAvatar;
|
|
1703
|
+
runtime.log?.(`[tlon] Bot avatar updated: ${botAvatar ? "set" : "cleared"}`);
|
|
1704
|
+
}
|
|
1517
1705
|
}
|
|
1518
1706
|
}
|
|
1519
1707
|
// Look for peer profile updates (other users)
|
|
@@ -1522,7 +1710,7 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
1522
1710
|
const nickname = event.peer.contact?.nickname?.value ?? event.peer.contact?.nickname;
|
|
1523
1711
|
if (ship) {
|
|
1524
1712
|
if (nickname && typeof nickname === "string") {
|
|
1525
|
-
nicknameCache.set(ship, nickname);
|
|
1713
|
+
nicknameCache.set(ship, sanitizeNickname(nickname));
|
|
1526
1714
|
}
|
|
1527
1715
|
else {
|
|
1528
1716
|
nicknameCache.delete(ship);
|
|
@@ -1557,11 +1745,10 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
1557
1745
|
// Note: we don't remove channels from watchedChannels to avoid missing messages
|
|
1558
1746
|
// during transitions. The authorization check handles access control.
|
|
1559
1747
|
}
|
|
1560
|
-
// Update DM allowlist
|
|
1748
|
+
// Update DM allowlist — respect empty lists (don't fall back to file config)
|
|
1561
1749
|
if (newSettings.dmAllowlist !== undefined) {
|
|
1562
|
-
effectiveDmAllowlist =
|
|
1563
|
-
|
|
1564
|
-
runtime.log?.(`[tlon] Settings: dmAllowlist updated to ${effectiveDmAllowlist.join(", ")}`);
|
|
1750
|
+
effectiveDmAllowlist = newSettings.dmAllowlist;
|
|
1751
|
+
runtime.log?.(`[tlon] Settings: dmAllowlist updated to ${effectiveDmAllowlist.length > 0 ? effectiveDmAllowlist.join(", ") : "(empty)"}`);
|
|
1565
1752
|
}
|
|
1566
1753
|
// Update model signature setting
|
|
1567
1754
|
if (newSettings.showModelSig !== undefined) {
|
|
@@ -1803,7 +1990,8 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
1803
1990
|
type: "group",
|
|
1804
1991
|
requestingShip: inviterShip,
|
|
1805
1992
|
groupFlag,
|
|
1806
|
-
|
|
1993
|
+
groupTitle: validInvite.preview?.meta?.title,
|
|
1994
|
+
}, pendingApprovals.map((a) => a.id));
|
|
1807
1995
|
await queueApprovalRequest(approval);
|
|
1808
1996
|
processedGroupInvites.add(groupFlag);
|
|
1809
1997
|
}
|
|
@@ -1822,7 +2010,8 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
1822
2010
|
type: "group",
|
|
1823
2011
|
requestingShip: inviterShip,
|
|
1824
2012
|
groupFlag,
|
|
1825
|
-
|
|
2013
|
+
groupTitle: validInvite.preview?.meta?.title,
|
|
2014
|
+
}, pendingApprovals.map((a) => a.id));
|
|
1826
2015
|
await queueApprovalRequest(approval);
|
|
1827
2016
|
processedGroupInvites.add(groupFlag);
|
|
1828
2017
|
}
|
|
@@ -1915,11 +2104,36 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
1915
2104
|
}
|
|
1916
2105
|
}
|
|
1917
2106
|
}, 2 * 60 * 1000);
|
|
2107
|
+
// Periodically re-scry settings as a fallback for stale subscriptions.
|
|
2108
|
+
// The settings subscription can silently die (SSE quit without reconnect),
|
|
2109
|
+
// leaving the in-memory allowlist permanently stale.
|
|
2110
|
+
const settingsRefreshInterval = setInterval(async () => {
|
|
2111
|
+
if (opts.abortSignal?.aborted) {
|
|
2112
|
+
return;
|
|
2113
|
+
}
|
|
2114
|
+
try {
|
|
2115
|
+
const refreshed = await settingsManager.load();
|
|
2116
|
+
if (refreshed.dmAllowlist !== undefined) {
|
|
2117
|
+
const newList = refreshed.dmAllowlist;
|
|
2118
|
+
if (JSON.stringify(newList) !== JSON.stringify(effectiveDmAllowlist)) {
|
|
2119
|
+
effectiveDmAllowlist = newList;
|
|
2120
|
+
runtime.log?.(`[tlon] Settings refresh: dmAllowlist updated to ${effectiveDmAllowlist.join(", ")}`);
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
if (refreshed.defaultAuthorizedShips !== undefined) {
|
|
2124
|
+
currentSettings = { ...currentSettings, defaultAuthorizedShips: refreshed.defaultAuthorizedShips };
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
catch (err) {
|
|
2128
|
+
runtime.error?.(`[tlon] Settings refresh failed: ${String(err)}`);
|
|
2129
|
+
}
|
|
2130
|
+
}, 5 * 60 * 1000);
|
|
1918
2131
|
if (opts.abortSignal) {
|
|
1919
2132
|
const signal = opts.abortSignal;
|
|
1920
2133
|
await new Promise((resolve) => {
|
|
1921
2134
|
signal.addEventListener("abort", () => {
|
|
1922
2135
|
clearInterval(pollInterval);
|
|
2136
|
+
clearInterval(settingsRefreshInterval);
|
|
1923
2137
|
resolve(null);
|
|
1924
2138
|
}, { once: true });
|
|
1925
2139
|
});
|
|
@@ -1929,6 +2143,7 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
1929
2143
|
}
|
|
1930
2144
|
}
|
|
1931
2145
|
finally {
|
|
2146
|
+
removeBridge(accountKey, commandBridge);
|
|
1932
2147
|
try {
|
|
1933
2148
|
await api?.close();
|
|
1934
2149
|
}
|