@tloncorp/openclaw 0.6.0 → 0.6.1
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 +559 -17
- package/dist/index.js.map +1 -1
- package/dist/src/channel.js +12 -0
- package/dist/src/channel.js.map +1 -1
- package/dist/src/diagnostic-subscriptions.js +49 -0
- package/dist/src/diagnostic-subscriptions.js.map +1 -0
- package/dist/src/monitor/index.js +275 -41
- package/dist/src/monitor/index.js.map +1 -1
- package/dist/src/monitor/session-routing.js +261 -0
- package/dist/src/monitor/session-routing.js.map +1 -0
- package/dist/src/session-route.js +44 -0
- package/dist/src/session-route.js.map +1 -0
- package/dist/src/telemetry.js +749 -22
- package/dist/src/telemetry.js.map +1 -1
- package/dist/src/version.generated.js +2 -2
- package/dist/src/version.js +134 -0
- package/dist/src/version.js.map +1 -0
- package/package.json +3 -29
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { configureGatewayStatus, gatewayStart } from '@tloncorp/api';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { format } from 'node:util';
|
|
3
4
|
import { createTypingCallbacks } from 'openclaw/plugin-sdk/channel-runtime';
|
|
4
5
|
import { getEffectiveOwnerShip, setEffectiveOwnerShip, } from '../effective-owner.js';
|
|
@@ -11,7 +12,7 @@ import { setSessionRole } from '../session-roles.js';
|
|
|
11
12
|
import { DM_INVITE_PREVIEW, createSettingsManager, } from '../settings.js';
|
|
12
13
|
import { sharedSlot } from '../shared-state.js';
|
|
13
14
|
import { canonicalizeNest, normalizeShip, parseChannelNest, } from '../targets.js';
|
|
14
|
-
import { createTlonTelemetry } from '../telemetry.js';
|
|
15
|
+
import { createTlonTelemetry, formatTlonTelemetryErrorText, setErrorTelemetryReporter, setOutboundRouteReporter, setSessionTelemetryReporter, } from '../telemetry.js';
|
|
15
16
|
import { resolveTlonAccount } from '../types.js';
|
|
16
17
|
import { configureTlonApiWithPoke } from '../urbit/api-client.js';
|
|
17
18
|
import { authenticate } from '../urbit/auth.js';
|
|
@@ -20,6 +21,7 @@ import { describeError } from '../urbit/errors.js';
|
|
|
20
21
|
import { sendChannelPost, sendDm } from '../urbit/send.js';
|
|
21
22
|
import { UrbitSSEClient } from '../urbit/sse-client.js';
|
|
22
23
|
import { markdownToStory } from '../urbit/story.js';
|
|
24
|
+
import { formatTlonVersionIdentity, resolveTlonSkillVersion, } from '../version.js';
|
|
23
25
|
import { createPendingApproval, emojiToApprovalAction, findPendingApproval, formatApprovalConfirmation, formatApprovalRequest, formatBlockedList, formatPendingList, isExpired, normalizeNotificationId, pruneExpired, removePendingApproval, } from './approval.js';
|
|
24
26
|
import { removeBridge, setBridge, } from './command-bridge.js';
|
|
25
27
|
import { createComputingPresenceTracker } from './computing-presence.js';
|
|
@@ -31,6 +33,7 @@ import { clearShadowsForAccount, getLastNudgeStageShadow, getLastOwnerActivity,
|
|
|
31
33
|
import { createOwnerReplyPersistenceQueue } from './owner-reply-persistence.js';
|
|
32
34
|
import { createPendingNudgePersistenceQueue } from './pending-nudge-persistence.js';
|
|
33
35
|
import { createProcessedMessageTracker } from './processed-messages.js';
|
|
36
|
+
import { isRouteDebugEnabled, recordTlonRouteAndDispatch, routeUpdateWillSkipByPin, tlonDeliveryContext, } from './session-routing.js';
|
|
34
37
|
import { resolveSettingsMirrorSync } from './settings-sync.js';
|
|
35
38
|
import { extractCites, extractMessageText, formatModelName, isBotMentioned, isDmAllowed, isOwnerListenSlashCommand, isSummarizationRequest, sanitizeMessageText, shouldEngageInGroup, stripBotMention, } from './utils.js';
|
|
36
39
|
// Holds the data needed for any module-loader context to (re)configure its
|
|
@@ -41,6 +44,12 @@ const apiClientParamsSlot = sharedSlot(API_CLIENT_PARAMS_SLOT);
|
|
|
41
44
|
const SETTINGS_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
|
|
42
45
|
const GATEWAY_STATUS_ACTIVATION_TIMEOUT_MS = 15_000;
|
|
43
46
|
const GATEWAY_STATUS_ACTIVATION_RETRY_MS = 30_000;
|
|
47
|
+
function classifyPluginError(error) {
|
|
48
|
+
if (error instanceof Error) {
|
|
49
|
+
return error.name || 'Error';
|
|
50
|
+
}
|
|
51
|
+
return typeof error;
|
|
52
|
+
}
|
|
44
53
|
// Bound an activation poke so a silently-hung promise surfaces as a
|
|
45
54
|
// retryable error instead of leaving gateway-status dead for the process
|
|
46
55
|
// lifetime. The underlying poke may still settle after the timeout; the
|
|
@@ -135,10 +144,36 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
135
144
|
const accountUrl = account.url;
|
|
136
145
|
const accountCode = account.code;
|
|
137
146
|
const botShipName = normalizeShip(account.ship);
|
|
147
|
+
const tlonSkillVersion = await resolveTlonSkillVersion();
|
|
148
|
+
let effectiveOwnerShip = account.ownerShip
|
|
149
|
+
? normalizeShip(account.ownerShip)
|
|
150
|
+
: null;
|
|
151
|
+
setEffectiveOwnerShip(account.accountId, effectiveOwnerShip);
|
|
152
|
+
const telemetry = createTlonTelemetry({
|
|
153
|
+
config: account.telemetry,
|
|
154
|
+
runtime,
|
|
155
|
+
});
|
|
156
|
+
const currentTelemetryOwnerShip = () => getEffectiveOwnerShip(account.accountId) ?? effectiveOwnerShip;
|
|
157
|
+
const capturePluginError = (pluginErrorSource, error, extra) => {
|
|
158
|
+
telemetry?.capturePluginError({
|
|
159
|
+
harness: 'openclaw',
|
|
160
|
+
pluginErrorSource,
|
|
161
|
+
accountId: account.accountId,
|
|
162
|
+
ownerShip: currentTelemetryOwnerShip(),
|
|
163
|
+
botShip: botShipName,
|
|
164
|
+
errorKind: extra?.errorKind ?? classifyPluginError(error),
|
|
165
|
+
errorText: formatTlonTelemetryErrorText(error),
|
|
166
|
+
attempt: extra?.attempt ?? null,
|
|
167
|
+
});
|
|
168
|
+
};
|
|
138
169
|
runtime.log?.(`[tlon] Starting monitor for ${botShipName}`);
|
|
170
|
+
runtime.log?.(`[tlon] version: ${formatTlonVersionIdentity({
|
|
171
|
+
markdown: false,
|
|
172
|
+
tlonSkillVersion,
|
|
173
|
+
}).replace(/\n/g, ' | ')}`);
|
|
139
174
|
const ssrfPolicy = ssrfPolicyFromAllowPrivateNetwork(account.allowPrivateNetwork);
|
|
140
175
|
// Helper to authenticate with retry logic
|
|
141
|
-
async function authenticateWithRetry(maxAttempts = 10) {
|
|
176
|
+
async function authenticateWithRetry(maxAttempts = 10, source = 'auth') {
|
|
142
177
|
for (let attempt = 1;; attempt++) {
|
|
143
178
|
if (opts.abortSignal?.aborted) {
|
|
144
179
|
throw new Error('Aborted while waiting to authenticate');
|
|
@@ -148,6 +183,7 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
148
183
|
return await authenticate(accountUrl, accountCode, { ssrfPolicy });
|
|
149
184
|
}
|
|
150
185
|
catch (error) {
|
|
186
|
+
capturePluginError(source, error, { attempt });
|
|
151
187
|
runtime.error?.(`[tlon] Failed to authenticate (attempt ${attempt}): ${error?.message ?? String(error)}`);
|
|
152
188
|
if (attempt >= maxAttempts) {
|
|
153
189
|
throw error;
|
|
@@ -168,22 +204,29 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
168
204
|
}
|
|
169
205
|
}
|
|
170
206
|
let api = null;
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
207
|
+
let cookie;
|
|
208
|
+
try {
|
|
209
|
+
cookie = await authenticateWithRetry();
|
|
210
|
+
api = new UrbitSSEClient(account.url, cookie, {
|
|
211
|
+
ship: botShipName,
|
|
212
|
+
ssrfPolicy,
|
|
213
|
+
logger: {
|
|
214
|
+
log: (message) => runtime.log?.(message),
|
|
215
|
+
error: (message) => runtime.error?.(message),
|
|
216
|
+
},
|
|
217
|
+
// Re-authenticate on reconnect in case the session expired
|
|
218
|
+
onReconnect: async (client) => {
|
|
219
|
+
runtime.log?.('[tlon] Re-authenticating on SSE reconnect...');
|
|
220
|
+
const newCookie = await authenticateWithRetry(5, 're_auth');
|
|
221
|
+
client.updateCookie(newCookie);
|
|
222
|
+
runtime.log?.('[tlon] Re-authentication successful');
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
await telemetry?.close();
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
187
230
|
// Configure @tloncorp/api's global client to use the SSE client's poke for all send operations
|
|
188
231
|
configureTlonApiWithPoke(api.poke.bind(api), botShipName, account.url);
|
|
189
232
|
// Publish the SSE-bound poke + ship coords so other module contexts (e.g.
|
|
@@ -251,11 +294,11 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
251
294
|
once: true,
|
|
252
295
|
});
|
|
253
296
|
// Outer try/finally wraps everything from slot publication onward.
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
//
|
|
297
|
+
// A synchronous throw between slot publication and the inner try
|
|
298
|
+
// (constructor, queue setup, bridge setup, channel discovery, future
|
|
299
|
+
// edits in this large pre-try region) would leave the shared slot
|
|
300
|
+
// orphaned. This outer finally catches all of those and runs cleanup
|
|
301
|
+
// unconditionally.
|
|
259
302
|
try {
|
|
260
303
|
const computingPresence = createComputingPresenceTracker({ runtime });
|
|
261
304
|
const processedTracker = createProcessedMessageTracker(2000);
|
|
@@ -279,10 +322,6 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
279
322
|
let effectiveAutoAcceptGroupInvites = account.autoAcceptGroupInvites ?? false;
|
|
280
323
|
let effectiveGroupInviteAllowlist = account.groupInviteAllowlist;
|
|
281
324
|
let effectiveAutoDiscoverChannels = account.autoDiscoverChannels ?? false;
|
|
282
|
-
let effectiveOwnerShip = account.ownerShip
|
|
283
|
-
? normalizeShip(account.ownerShip)
|
|
284
|
-
: null;
|
|
285
|
-
setEffectiveOwnerShip(account.accountId, effectiveOwnerShip);
|
|
286
325
|
let effectiveOwnerListenEnabled = account.ownerListenEnabled ?? true;
|
|
287
326
|
// Canonicalize on every read so an entry stored from a slightly-off user
|
|
288
327
|
// input (e.g. missing "~" or wrong case) still matches incoming nest events.
|
|
@@ -313,9 +352,66 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
313
352
|
clearPendingNudge(accountId);
|
|
314
353
|
pendingNudgeRehydrated = true;
|
|
315
354
|
};
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
355
|
+
// Bridge route-resolution telemetry from the global `message_sending` hook
|
|
356
|
+
// to this account's telemetry client. Reports every route-dependent send so
|
|
357
|
+
// we can measure how often a reply lands on webchat instead of Tlon.
|
|
358
|
+
setOutboundRouteReporter((event) => telemetry?.captureOutboundRoute({
|
|
359
|
+
...event,
|
|
360
|
+
ownerShip: getEffectiveOwnerShip(account.accountId) ?? effectiveOwnerShip,
|
|
361
|
+
botShip: botShipName,
|
|
362
|
+
}));
|
|
363
|
+
setSessionTelemetryReporter((report) => {
|
|
364
|
+
switch (report.kind) {
|
|
365
|
+
case 'lifecycle':
|
|
366
|
+
telemetry?.captureSessionLifecycle(report.event);
|
|
367
|
+
break;
|
|
368
|
+
case 'watchdog':
|
|
369
|
+
telemetry?.captureSessionWatchdog(report.event);
|
|
370
|
+
break;
|
|
371
|
+
case 'recovery':
|
|
372
|
+
telemetry?.captureSessionRecovery(report.event);
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
setErrorTelemetryReporter((report) => {
|
|
377
|
+
switch (report.kind) {
|
|
378
|
+
case 'harness':
|
|
379
|
+
telemetry?.captureHarnessError({
|
|
380
|
+
...report.event,
|
|
381
|
+
accountId: report.event.accountId ?? account.accountId,
|
|
382
|
+
ownerShip: report.event.ownerShip ?? currentTelemetryOwnerShip(),
|
|
383
|
+
botShip: report.event.botShip || botShipName,
|
|
384
|
+
});
|
|
385
|
+
break;
|
|
386
|
+
case 'plugin':
|
|
387
|
+
telemetry?.capturePluginError({
|
|
388
|
+
harness: 'openclaw',
|
|
389
|
+
pluginErrorSource: report.event.pluginErrorSource,
|
|
390
|
+
accountId: report.event.accountId ?? account.accountId,
|
|
391
|
+
ownerShip: report.event.ownerShip ?? currentTelemetryOwnerShip(),
|
|
392
|
+
botShip: report.event.botShip ?? botShipName,
|
|
393
|
+
errorKind: report.event.errorKind ?? null,
|
|
394
|
+
errorText: report.event.errorText,
|
|
395
|
+
attempt: report.event.attempt ?? null,
|
|
396
|
+
});
|
|
397
|
+
break;
|
|
398
|
+
case 'telemetry':
|
|
399
|
+
telemetry?.captureTelemetryError({
|
|
400
|
+
harness: 'openclaw',
|
|
401
|
+
telemetrySource: report.event.telemetrySource,
|
|
402
|
+
sourceEventName: report.event.sourceEventName ?? null,
|
|
403
|
+
sessionKey: report.event.sessionKey ?? null,
|
|
404
|
+
sessionId: report.event.sessionId ?? null,
|
|
405
|
+
runId: report.event.runId ?? null,
|
|
406
|
+
accountId: report.event.accountId ?? account.accountId,
|
|
407
|
+
agentId: report.event.agentId ?? null,
|
|
408
|
+
ownerShip: report.event.ownerShip ?? currentTelemetryOwnerShip(),
|
|
409
|
+
botShip: report.event.botShip ?? botShipName,
|
|
410
|
+
errorKind: report.event.errorKind ?? null,
|
|
411
|
+
errorText: report.event.errorText,
|
|
412
|
+
});
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
319
415
|
});
|
|
320
416
|
// Track threads we've participated in (by parentId) - respond without mention requirement
|
|
321
417
|
const participatedThreads = new Set();
|
|
@@ -698,12 +794,16 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
698
794
|
return;
|
|
699
795
|
}
|
|
700
796
|
catch (err) {
|
|
797
|
+
capturePluginError('gateway_status_activation', err, {
|
|
798
|
+
attempt,
|
|
799
|
+
});
|
|
701
800
|
runtime.error?.(`[gateway-status] activation attempt ${attempt} failed: ${String(err)} — retrying in ${GATEWAY_STATUS_ACTIVATION_RETRY_MS / 1000}s`);
|
|
702
801
|
}
|
|
703
802
|
await abortableDelay(GATEWAY_STATUS_ACTIVATION_RETRY_MS, signal);
|
|
704
803
|
}
|
|
705
804
|
}
|
|
706
805
|
catch (err) {
|
|
806
|
+
capturePluginError('gateway_status_activation', err);
|
|
707
807
|
runtime.error?.(`[gateway-status] start failed: ${String(err)}`);
|
|
708
808
|
}
|
|
709
809
|
})();
|
|
@@ -1671,12 +1771,51 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
1671
1771
|
ReplyToId: String(parentId),
|
|
1672
1772
|
}),
|
|
1673
1773
|
});
|
|
1774
|
+
// ── Durable session-route persistence ───────────────────────
|
|
1775
|
+
// The streamed reply below goes out through our own `deliver` callback
|
|
1776
|
+
// and does not consult session metadata. But later route-dependent sends
|
|
1777
|
+
// (the shared `message` tool, subagents, system-event turns) resolve
|
|
1778
|
+
// their destination from the session store; without a persisted Tlon
|
|
1779
|
+
// route they fall back to webchat. recordTlonRouteAndDispatch (below)
|
|
1780
|
+
// records the route before dispatch and fails open — never blocks the
|
|
1781
|
+
// reply.
|
|
1782
|
+
const routeDebug = isRouteDebugEnabled()
|
|
1783
|
+
? (rec) => runtime.log?.(`[tlon][route-debug] inbound ${JSON.stringify({
|
|
1784
|
+
messageId,
|
|
1785
|
+
agentId: route.agentId,
|
|
1786
|
+
sessionKey: route.sessionKey,
|
|
1787
|
+
mainSessionKey: route.mainSessionKey,
|
|
1788
|
+
lastRoutePolicy: route.lastRoutePolicy,
|
|
1789
|
+
matchedBy: route.matchedBy,
|
|
1790
|
+
provider: ctxPayload.Provider,
|
|
1791
|
+
surface: ctxPayload.Surface,
|
|
1792
|
+
originatingChannel: ctxPayload.OriginatingChannel,
|
|
1793
|
+
originatingTo: ctxPayload.OriginatingTo,
|
|
1794
|
+
ctxSessionKey: ctxPayload.SessionKey,
|
|
1795
|
+
isGroup,
|
|
1796
|
+
groupChannel: groupChannel ?? null,
|
|
1797
|
+
senderShip,
|
|
1798
|
+
parentId: parentId ?? null,
|
|
1799
|
+
deliverParentId: deliverParentId ?? null,
|
|
1800
|
+
recordSessionKey: rec.recordSessionKey,
|
|
1801
|
+
lastRouteSessionKey: rec.lastRouteSessionKey,
|
|
1802
|
+
target: rec.target,
|
|
1803
|
+
hadUpdateLastRoute: Boolean(rec.updateLastRoute),
|
|
1804
|
+
pinWillSkip: routeUpdateWillSkipByPin(rec.updateLastRoute),
|
|
1805
|
+
skippedReason: rec.skippedReason ?? null,
|
|
1806
|
+
})}`)
|
|
1807
|
+
: undefined;
|
|
1674
1808
|
const dispatchStartTime = Date.now();
|
|
1809
|
+
const runId = randomUUID();
|
|
1675
1810
|
const replyTelemetry = telemetry?.startReply({
|
|
1676
1811
|
sessionKey: route.sessionKey,
|
|
1812
|
+
runId,
|
|
1813
|
+
accountId: account.accountId,
|
|
1814
|
+
agentId: route.agentId,
|
|
1677
1815
|
ownerShip: effectiveOwnerShip,
|
|
1678
1816
|
botShip: botShipName,
|
|
1679
1817
|
chatType: isGroup ? 'groupChannel' : 'dm',
|
|
1818
|
+
destinationKind: isGroup ? 'groupChannel' : 'dm',
|
|
1680
1819
|
isThreadReply: Boolean(isThreadReply),
|
|
1681
1820
|
senderRole,
|
|
1682
1821
|
attachmentCount,
|
|
@@ -1685,9 +1824,16 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
1685
1824
|
let selectedModel = null;
|
|
1686
1825
|
let selectedThinkLevel = null;
|
|
1687
1826
|
let deliveredMessageCount = 0;
|
|
1827
|
+
let sendAttemptCount = 0;
|
|
1828
|
+
let sendErrorCount = 0;
|
|
1829
|
+
let sendErrorKind = null;
|
|
1688
1830
|
let replyCharCount = 0;
|
|
1689
1831
|
let replyWordCount = 0;
|
|
1690
1832
|
let replyMediaCount = 0;
|
|
1833
|
+
let deliverySkipReason = null;
|
|
1834
|
+
const recordDeliverySkip = (reason) => {
|
|
1835
|
+
deliverySkipReason ??= reason;
|
|
1836
|
+
};
|
|
1691
1837
|
const responsePrefix = core.channel.reply.resolveEffectiveMessagesConfig(cfg, route.agentId).responsePrefix;
|
|
1692
1838
|
const humanDelay = core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId);
|
|
1693
1839
|
const presenceConversationId = isGroup
|
|
@@ -1726,6 +1872,7 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
1726
1872
|
})
|
|
1727
1873
|
: undefined;
|
|
1728
1874
|
const replyOptions = {
|
|
1875
|
+
runId,
|
|
1729
1876
|
onModelSelected: ({ provider, model, thinkLevel }) => {
|
|
1730
1877
|
selectedProvider = provider;
|
|
1731
1878
|
selectedModel = model;
|
|
@@ -1752,8 +1899,29 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
1752
1899
|
let dispatchResult;
|
|
1753
1900
|
let dispatchError;
|
|
1754
1901
|
try {
|
|
1755
|
-
dispatchResult =
|
|
1756
|
-
|
|
1902
|
+
dispatchResult = await recordTlonRouteAndDispatch({
|
|
1903
|
+
session: core.channel.session,
|
|
1904
|
+
cfg,
|
|
1905
|
+
route,
|
|
1906
|
+
ctxPayload,
|
|
1907
|
+
ctxSessionKey: ctxPayload.SessionKey,
|
|
1908
|
+
isGroup,
|
|
1909
|
+
groupChannel,
|
|
1910
|
+
senderShip,
|
|
1911
|
+
parentId,
|
|
1912
|
+
deliverParentId,
|
|
1913
|
+
effectiveOwnerShip,
|
|
1914
|
+
effectiveDmAllowlist,
|
|
1915
|
+
messageId,
|
|
1916
|
+
sessionStore: cfg.session?.store,
|
|
1917
|
+
logError: (msg) => runtime.error?.(msg),
|
|
1918
|
+
// Routine skip / pin-skip diagnostics are debug-gated to avoid
|
|
1919
|
+
// high-volume logs for expected policy cases.
|
|
1920
|
+
logDebug: isRouteDebugEnabled()
|
|
1921
|
+
? (msg) => runtime.log?.(msg)
|
|
1922
|
+
: undefined,
|
|
1923
|
+
onRecord: routeDebug,
|
|
1924
|
+
dispatch: () => core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
1757
1925
|
ctx: ctxPayload,
|
|
1758
1926
|
cfg,
|
|
1759
1927
|
replyOptions,
|
|
@@ -1761,14 +1929,24 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
1761
1929
|
responsePrefix,
|
|
1762
1930
|
humanDelay,
|
|
1763
1931
|
typingCallbacks,
|
|
1932
|
+
onSkip: (_payload, info) => {
|
|
1933
|
+
recordDeliverySkip(info.reason);
|
|
1934
|
+
},
|
|
1764
1935
|
deliver: async (payload) => {
|
|
1765
1936
|
let replyText = payload.text;
|
|
1766
1937
|
if (!replyText) {
|
|
1938
|
+
const hasMedia = Array.isArray(payload.mediaUrls)
|
|
1939
|
+
? payload.mediaUrls.length > 0
|
|
1940
|
+
: Boolean(payload.mediaUrl);
|
|
1941
|
+
recordDeliverySkip(hasMedia
|
|
1942
|
+
? 'media_only_payload_not_sent'
|
|
1943
|
+
: 'empty_payload_text');
|
|
1767
1944
|
return;
|
|
1768
1945
|
}
|
|
1769
1946
|
// Process any block directives in the response (strips them from text)
|
|
1770
1947
|
replyText = await processBlockDirectives(replyText, senderShip);
|
|
1771
1948
|
if (!replyText) {
|
|
1949
|
+
recordDeliverySkip('block_directive_only');
|
|
1772
1950
|
return;
|
|
1773
1951
|
} // Response was only a directive
|
|
1774
1952
|
// Use settings store value if set, otherwise fall back to file config
|
|
@@ -1786,13 +1964,26 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
1786
1964
|
replyText = `${replyText}\n\n_[Generated by ${formatModelName(modelInfo)}]_`;
|
|
1787
1965
|
}
|
|
1788
1966
|
// Add addendum if this is the last response before bot rate limit
|
|
1789
|
-
if (isGroup &&
|
|
1967
|
+
if (isGroup &&
|
|
1968
|
+
groupChannel &&
|
|
1969
|
+
knownBotShips.has(senderShip)) {
|
|
1790
1970
|
const count = consecutiveBotMessages.get(groupChannel) ?? 0;
|
|
1791
1971
|
if (maxBotResponses > 0 && count === maxBotResponses) {
|
|
1792
1972
|
const otherBot = formatShipWithNickname(senderShip);
|
|
1793
1973
|
replyText += `\n\n---\n_This is my last response to ${otherBot} for now. To continue our conversation, someone will need to mention me._`;
|
|
1794
1974
|
}
|
|
1795
1975
|
}
|
|
1976
|
+
if (isRouteDebugEnabled()) {
|
|
1977
|
+
runtime.log?.(`[tlon][route-debug] deliver ${JSON.stringify({
|
|
1978
|
+
messageId,
|
|
1979
|
+
isGroup,
|
|
1980
|
+
destination: isGroup
|
|
1981
|
+
? groupChannel ?? null
|
|
1982
|
+
: senderShip,
|
|
1983
|
+
deliverParentId: deliverParentId ?? null,
|
|
1984
|
+
})}`);
|
|
1985
|
+
}
|
|
1986
|
+
sendAttemptCount += 1;
|
|
1796
1987
|
if (isGroup && groupChannel) {
|
|
1797
1988
|
// Send to any channel type (chat, heap, diary) using the nest directly
|
|
1798
1989
|
await sendChannelPost({
|
|
@@ -1819,12 +2010,6 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
1819
2010
|
: undefined,
|
|
1820
2011
|
});
|
|
1821
2012
|
}
|
|
1822
|
-
if (presenceConversationId) {
|
|
1823
|
-
await computingPresence.stopRun({
|
|
1824
|
-
conversationId: presenceConversationId,
|
|
1825
|
-
runId: presenceRunId,
|
|
1826
|
-
});
|
|
1827
|
-
}
|
|
1828
2013
|
deliveredMessageCount += 1;
|
|
1829
2014
|
replyCharCount += replyText.length;
|
|
1830
2015
|
replyWordCount += replyText.trim()
|
|
@@ -1835,13 +2020,22 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
1835
2020
|
: payload.mediaUrl
|
|
1836
2021
|
? 1
|
|
1837
2022
|
: 0;
|
|
2023
|
+
if (presenceConversationId) {
|
|
2024
|
+
await computingPresence.stopRun({
|
|
2025
|
+
conversationId: presenceConversationId,
|
|
2026
|
+
runId: presenceRunId,
|
|
2027
|
+
});
|
|
2028
|
+
}
|
|
1838
2029
|
},
|
|
1839
2030
|
onError: (err, info) => {
|
|
1840
2031
|
const dispatchDuration = Date.now() - dispatchStartTime;
|
|
2032
|
+
sendErrorCount += 1;
|
|
2033
|
+
sendErrorKind = info.kind;
|
|
1841
2034
|
runtime.error?.(`[tlon] ${info.kind} reply failed after ${dispatchDuration}ms: ${String(err)}`);
|
|
1842
2035
|
},
|
|
1843
2036
|
},
|
|
1844
|
-
})
|
|
2037
|
+
}),
|
|
2038
|
+
});
|
|
1845
2039
|
}
|
|
1846
2040
|
catch (error) {
|
|
1847
2041
|
dispatchError = error;
|
|
@@ -1849,6 +2043,9 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
1849
2043
|
}
|
|
1850
2044
|
finally {
|
|
1851
2045
|
await replyTelemetry?.capture({
|
|
2046
|
+
sendAttemptCount,
|
|
2047
|
+
sendErrorCount,
|
|
2048
|
+
sendErrorKind,
|
|
1852
2049
|
deliveredMessageCount,
|
|
1853
2050
|
replyCharCount,
|
|
1854
2051
|
replyWordCount,
|
|
@@ -1857,6 +2054,10 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
1857
2054
|
queuedFinal: dispatchResult?.queuedFinal ?? false,
|
|
1858
2055
|
queuedFinalCount: dispatchResult?.counts.final ?? 0,
|
|
1859
2056
|
queuedBlockCount: dispatchResult?.counts.block ?? 0,
|
|
2057
|
+
failedCounts: dispatchResult?.failedCounts,
|
|
2058
|
+
deliverySkipReason,
|
|
2059
|
+
sourceReplyDeliveryMode: dispatchResult?.sourceReplyDeliveryMode ?? null,
|
|
2060
|
+
beforeAgentRunBlocked: dispatchResult?.beforeAgentRunBlocked === true,
|
|
1860
2061
|
provider: selectedProvider,
|
|
1861
2062
|
model: selectedModel,
|
|
1862
2063
|
thinkLevel: selectedThinkLevel,
|
|
@@ -1956,6 +2157,8 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
1956
2157
|
core.system.enqueueSystemEvent(eventText, {
|
|
1957
2158
|
sessionKey: route.sessionKey,
|
|
1958
2159
|
contextKey: `tlon:reaction:${nest}:${postId}:${reactEmoji}:${ship}`,
|
|
2160
|
+
// Route any resulting system/heartbeat turn back to Tlon.
|
|
2161
|
+
deliveryContext: tlonDeliveryContext(`tlon:${nest}`, route.accountId),
|
|
1959
2162
|
});
|
|
1960
2163
|
}
|
|
1961
2164
|
}
|
|
@@ -2290,6 +2493,8 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
2290
2493
|
core.system.enqueueSystemEvent(eventText, {
|
|
2291
2494
|
sessionKey: route.sessionKey,
|
|
2292
2495
|
contextKey: `tlon:dm-reaction:${messageId}:${reactEmoji}:${reactAuthor}:${action}`,
|
|
2496
|
+
// Route any resulting system/heartbeat turn back to Tlon.
|
|
2497
|
+
deliveryContext: tlonDeliveryContext(`tlon:${partnerShip || reactAuthor}`, route.accountId),
|
|
2293
2498
|
});
|
|
2294
2499
|
runtime.log?.(`[tlon] DM_REACTION: ${eventText}`);
|
|
2295
2500
|
}
|
|
@@ -2421,6 +2626,7 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
2421
2626
|
path: '/v4',
|
|
2422
2627
|
event: (data) => handleChannelsFirehose(data),
|
|
2423
2628
|
err: (error) => {
|
|
2629
|
+
capturePluginError('channels_firehose', error);
|
|
2424
2630
|
runtime.error?.(`[tlon] Channels firehose error: ${String(error)}`);
|
|
2425
2631
|
},
|
|
2426
2632
|
quit: () => {
|
|
@@ -2434,6 +2640,7 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
2434
2640
|
path: '/v4',
|
|
2435
2641
|
event: (data) => handleChatFirehose(data),
|
|
2436
2642
|
err: (error) => {
|
|
2643
|
+
capturePluginError('chat_firehose', error);
|
|
2437
2644
|
runtime.error?.(`[tlon] Chat firehose error: ${String(error)}`);
|
|
2438
2645
|
},
|
|
2439
2646
|
quit: () => {
|
|
@@ -2492,6 +2699,7 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
2492
2699
|
}
|
|
2493
2700
|
},
|
|
2494
2701
|
err: (error) => {
|
|
2702
|
+
capturePluginError('contacts_subscription', error);
|
|
2495
2703
|
runtime.error?.(`[tlon] Contacts subscription error: ${String(error)}`);
|
|
2496
2704
|
},
|
|
2497
2705
|
quit: () => {
|
|
@@ -2685,7 +2893,11 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
2685
2893
|
});
|
|
2686
2894
|
if (route?.sessionKey) {
|
|
2687
2895
|
const memberDisplay = formatShipWithNickname(ship);
|
|
2688
|
-
core.system.enqueueSystemEvent(`[${memberDisplay} joined group ${groupFlag}]`, {
|
|
2896
|
+
core.system.enqueueSystemEvent(`[${memberDisplay} joined group ${groupFlag}]`, {
|
|
2897
|
+
sessionKey: route.sessionKey,
|
|
2898
|
+
// Route any resulting system turn back to Tlon.
|
|
2899
|
+
deliveryContext: tlonDeliveryContext(`tlon:${nest}`, route.accountId),
|
|
2900
|
+
});
|
|
2689
2901
|
runtime.log?.(`[tlon] Member joined: ${ship} → ${groupFlag}`);
|
|
2690
2902
|
break; // Only inject once per group
|
|
2691
2903
|
}
|
|
@@ -2796,6 +3008,7 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
2796
3008
|
}
|
|
2797
3009
|
},
|
|
2798
3010
|
err: (error) => {
|
|
3011
|
+
capturePluginError('groups_ui_subscription', error);
|
|
2799
3012
|
runtime.error?.(`[tlon] Groups-ui subscription error: ${String(error)}`);
|
|
2800
3013
|
},
|
|
2801
3014
|
quit: () => {
|
|
@@ -2806,6 +3019,7 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
2806
3019
|
}
|
|
2807
3020
|
catch (err) {
|
|
2808
3021
|
// Groups-ui subscription is optional - channel discovery will still work via polling
|
|
3022
|
+
capturePluginError('groups_ui_subscription', err);
|
|
2809
3023
|
runtime.log?.(`[tlon] Groups-ui subscription failed (will rely on polling): ${String(err)}`);
|
|
2810
3024
|
}
|
|
2811
3025
|
// Subscribe to foreigns for auto-accepting group invites
|
|
@@ -2925,6 +3139,7 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
2925
3139
|
})();
|
|
2926
3140
|
},
|
|
2927
3141
|
err: (error) => {
|
|
3142
|
+
capturePluginError('foreigns_subscription', error);
|
|
2928
3143
|
runtime.error?.(`[tlon] Foreigns subscription error: ${String(error)}`);
|
|
2929
3144
|
},
|
|
2930
3145
|
quit: () => {
|
|
@@ -2934,6 +3149,7 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
2934
3149
|
runtime.log?.('[tlon] Subscribed to foreigns (/v1/foreigns) for auto-accepting group invites');
|
|
2935
3150
|
}
|
|
2936
3151
|
catch (err) {
|
|
3152
|
+
capturePluginError('foreigns_subscription', err);
|
|
2937
3153
|
runtime.log?.(`[tlon] Foreigns subscription failed: ${String(err)}`);
|
|
2938
3154
|
}
|
|
2939
3155
|
}
|
|
@@ -2952,6 +3168,20 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
2952
3168
|
runtime.log?.('[tlon] All subscriptions registered, connecting to SSE stream...');
|
|
2953
3169
|
await api.connect();
|
|
2954
3170
|
runtime.log?.('[tlon] Connected! Firehose subscriptions active');
|
|
3171
|
+
telemetry?.captureGatewayConnected({
|
|
3172
|
+
ownerShip: effectiveOwnerShip,
|
|
3173
|
+
botShip: botShipName,
|
|
3174
|
+
tlonSkillVersion: await resolveTlonSkillVersion(),
|
|
3175
|
+
accountId: account.accountId,
|
|
3176
|
+
configured: account.configured,
|
|
3177
|
+
watchedChannelCount: watchedChannels.size,
|
|
3178
|
+
dmAllowlistCount: effectiveDmAllowlist.length,
|
|
3179
|
+
defaultAuthorizedShipsCount: (currentSettings.defaultAuthorizedShips ??
|
|
3180
|
+
account.defaultAuthorizedShips).length,
|
|
3181
|
+
pendingApprovalCount: pendingApprovals.length,
|
|
3182
|
+
autoDiscoverChannels: effectiveAutoDiscoverChannels,
|
|
3183
|
+
ownerListenEnabled: effectiveOwnerListenEnabled,
|
|
3184
|
+
});
|
|
2955
3185
|
// Periodically refresh channel discovery
|
|
2956
3186
|
const pollInterval = setInterval(async () => {
|
|
2957
3187
|
if (!opts.abortSignal?.aborted) {
|
|
@@ -2985,6 +3215,7 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
2985
3215
|
});
|
|
2986
3216
|
}
|
|
2987
3217
|
catch (err) {
|
|
3218
|
+
capturePluginError('settings_refresh', err);
|
|
2988
3219
|
runtime.error?.(`[tlon] Settings refresh failed: ${String(err)}`);
|
|
2989
3220
|
}
|
|
2990
3221
|
}, SETTINGS_REFRESH_INTERVAL_MS);
|
|
@@ -3077,6 +3308,9 @@ export async function monitorTlonProvider(opts = {}) {
|
|
|
3077
3308
|
await ownerReplyPersistence.flush();
|
|
3078
3309
|
await pendingNudgePersistence.flush();
|
|
3079
3310
|
clearShadowsForAccount(account.accountId);
|
|
3311
|
+
setOutboundRouteReporter(null);
|
|
3312
|
+
setSessionTelemetryReporter(null);
|
|
3313
|
+
setErrorTelemetryReporter(null);
|
|
3080
3314
|
await telemetry?.close();
|
|
3081
3315
|
try {
|
|
3082
3316
|
await api?.close();
|