@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.
@@ -1,12 +1,26 @@
1
1
  import { PostHog } from 'posthog-node';
2
- import { sharedMap } from './shared-state.js';
2
+ import { sharedMap, sharedSlot } from './shared-state.js';
3
+ import { getTlonVersionIdentity } from './version.js';
3
4
  const TLON_TELEMETRY_EVENT_NAME = 'TlonBot Reply Handled';
5
+ const TLON_GATEWAY_CONNECTED_EVENT = 'TlonBot Gateway Connected';
6
+ const TLON_OUTBOUND_ROUTED_EVENT = 'TlonBot Outbound Routed';
7
+ const TLON_SESSION_LIFECYCLE_EVENT = 'TlonBot Session Lifecycle';
8
+ const TLON_SESSION_WATCHDOG_EVENT = 'TlonBot Session Watchdog';
9
+ const TLON_SESSION_RECOVERY_EVENT = 'TlonBot Session Recovery';
10
+ const TLON_HARNESS_ERROR_EVENT = 'TlonBot Harness Error';
11
+ const TLON_PLUGIN_ERROR_EVENT = 'TlonBot Plugin Error';
12
+ const TLON_TELEMETRY_ERROR_EVENT = 'TlonBot Telemetry Error';
4
13
  const TLON_HEARTBEAT_NUDGE_EVENT = 'TlonBot Heartbeat Nudge Sent';
5
14
  const TLON_HEARTBEAT_REENGAGED_EVENT = 'TlonBot Heartbeat Nudge Reengaged';
6
15
  const TLON_TELEMETRY_LOG_SOURCE = 'openclawPlugin';
7
16
  const TOOL_TRACE_TTL_MS = 60 * 60 * 1000;
8
17
  const MAX_TOOL_CALLS_PER_SESSION = 200;
18
+ const REPLY_TRACE_TTL_MS = 60 * 60 * 1000;
19
+ const MAX_ACTIVE_REPLY_TRACES = 50;
20
+ const SESSION_CONTEXT_TTL_MS = 30 * 24 * 60 * 60 * 1000;
21
+ const MAX_SESSION_CONTEXTS = 5_000;
9
22
  const toolCallsBySession = sharedMap('telemetry.toolCallsBySession');
23
+ const sessionContextsBySessionKey = sharedMap('telemetry.sessionContextsBySessionKey');
10
24
  function cleanupToolCalls(now = Date.now()) {
11
25
  for (const [sessionKey, trace] of toolCallsBySession) {
12
26
  if (now - trace.updatedAt > TOOL_TRACE_TTL_MS) {
@@ -59,16 +73,141 @@ function collectToolUsageSince(sessionKey, cursor) {
59
73
  errorCount: calls.filter((call) => call.error).length,
60
74
  };
61
75
  }
76
+ function cleanupSessionContexts(now = Date.now()) {
77
+ for (const [sessionKey, context] of sessionContextsBySessionKey) {
78
+ if (now - context.updatedAt > SESSION_CONTEXT_TTL_MS) {
79
+ sessionContextsBySessionKey.delete(sessionKey);
80
+ }
81
+ }
82
+ while (sessionContextsBySessionKey.size > MAX_SESSION_CONTEXTS) {
83
+ const oldestKey = [...sessionContextsBySessionKey.entries()].sort((a, b) => a[1].updatedAt - b[1].updatedAt)[0]?.[0];
84
+ if (!oldestKey) {
85
+ break;
86
+ }
87
+ sessionContextsBySessionKey.delete(oldestKey);
88
+ }
89
+ }
90
+ function rememberTlonSessionContext(params) {
91
+ const sessionKey = params.sessionKey.trim();
92
+ if (!sessionKey) {
93
+ return;
94
+ }
95
+ const now = Date.now();
96
+ cleanupSessionContexts(now);
97
+ const existing = sessionContextsBySessionKey.get(sessionKey);
98
+ sessionContextsBySessionKey.set(sessionKey, {
99
+ sessionKey,
100
+ sessionId: optionalString(params.sessionId) ?? existing?.sessionId ?? null,
101
+ runId: optionalString(params.runId) ?? existing?.runId ?? null,
102
+ ownerShip: params.ownerShip,
103
+ botShip: params.botShip,
104
+ accountId: params.accountId ?? null,
105
+ agentId: params.agentId ?? null,
106
+ destinationKind: params.destinationKind,
107
+ updatedAt: now,
108
+ });
109
+ }
110
+ function lookupTlonSessionContext(sessionKey) {
111
+ const normalized = sessionKey?.trim();
112
+ if (!normalized) {
113
+ return null;
114
+ }
115
+ cleanupSessionContexts();
116
+ const context = sessionContextsBySessionKey.get(normalized);
117
+ if (!context) {
118
+ return null;
119
+ }
120
+ context.updatedAt = Date.now();
121
+ sessionContextsBySessionKey.set(normalized, context);
122
+ return context;
123
+ }
124
+ function updateTlonSessionContextSessionId(context, sessionId) {
125
+ const normalizedSessionId = optionalString(sessionId);
126
+ if (!normalizedSessionId || context.sessionId === normalizedSessionId) {
127
+ return context;
128
+ }
129
+ const updated = {
130
+ ...context,
131
+ sessionId: normalizedSessionId,
132
+ updatedAt: Date.now(),
133
+ };
134
+ sessionContextsBySessionKey.set(updated.sessionKey, updated);
135
+ return updated;
136
+ }
137
+ function updateTlonSessionContextRuntime(context, params) {
138
+ const normalizedSessionId = optionalString(params.sessionId);
139
+ const normalizedRunId = optionalString(params.runId);
140
+ const normalizedAgentId = optionalString(params.agentId);
141
+ if ((!normalizedSessionId || context.sessionId === normalizedSessionId) &&
142
+ (!normalizedRunId || context.runId === normalizedRunId) &&
143
+ (!normalizedAgentId || context.agentId === normalizedAgentId)) {
144
+ return context;
145
+ }
146
+ const updated = {
147
+ ...context,
148
+ sessionId: normalizedSessionId ?? context.sessionId,
149
+ runId: normalizedRunId ?? context.runId,
150
+ agentId: normalizedAgentId ?? context.agentId,
151
+ updatedAt: Date.now(),
152
+ };
153
+ sessionContextsBySessionKey.set(updated.sessionKey, updated);
154
+ return updated;
155
+ }
156
+ function countDispatchFailures(counts, kind) {
157
+ const value = counts?.[kind];
158
+ return typeof value === 'number' && Number.isFinite(value)
159
+ ? Math.max(0, value)
160
+ : 0;
161
+ }
162
+ function classifyError(error) {
163
+ if (!error) {
164
+ return null;
165
+ }
166
+ if (error instanceof Error) {
167
+ return error.name || 'Error';
168
+ }
169
+ if (typeof error === 'object' && error !== null) {
170
+ const code = error.code;
171
+ if (typeof code === 'string' && code.trim()) {
172
+ return code.trim().slice(0, 80);
173
+ }
174
+ const name = error.name;
175
+ if (typeof name === 'string' && name.trim()) {
176
+ return name.trim().slice(0, 80);
177
+ }
178
+ return 'object';
179
+ }
180
+ return typeof error;
181
+ }
62
182
  function resolveReplyOutcome(params) {
63
183
  if (params.deliveredMessageCount > 0) {
64
184
  return 'responded';
65
185
  }
66
- return params.dispatchError ? 'error' : 'no_reply';
186
+ const failedReplyCount = countDispatchFailures(params.failedCounts, 'tool') +
187
+ countDispatchFailures(params.failedCounts, 'block') +
188
+ countDispatchFailures(params.failedCounts, 'final');
189
+ return params.dispatchError || params.sendError || failedReplyCount > 0
190
+ ? 'error'
191
+ : 'no_reply';
192
+ }
193
+ function resolveDeliverySkipReason(params) {
194
+ if (params.outcome !== 'no_reply') {
195
+ return null;
196
+ }
197
+ if (params.deliverySkipReason) {
198
+ return params.deliverySkipReason;
199
+ }
200
+ if (params.sourceReplyDeliveryMode === 'message_tool_only') {
201
+ return 'source_reply_delivery_mode_message_tool_only';
202
+ }
203
+ return null;
67
204
  }
68
205
  class PostHogTlonTelemetry {
69
206
  client;
70
207
  runtime;
208
+ versionIdentity = getTlonVersionIdentity();
71
209
  identifiedOwners = new Set();
210
+ activeReplyTraces = new Map();
72
211
  missingOwnerWarningLogged = false;
73
212
  constructor(params) {
74
213
  this.runtime = params.runtime;
@@ -81,23 +220,109 @@ class PostHogTlonTelemetry {
81
220
  disableRemoteConfig: true,
82
221
  });
83
222
  }
223
+ properties(props) {
224
+ return {
225
+ logSource: TLON_TELEMETRY_LOG_SOURCE,
226
+ ...this.versionIdentity,
227
+ ...props,
228
+ };
229
+ }
230
+ captureGatewayConnected(event) {
231
+ const ownerShip = event.ownerShip ?? '';
232
+ if (!this.ensureIdentified(ownerShip, event.botShip)) {
233
+ return;
234
+ }
235
+ this.client.capture({
236
+ distinctId: ownerShip,
237
+ event: TLON_GATEWAY_CONNECTED_EVENT,
238
+ properties: this.properties({
239
+ botShip: event.botShip,
240
+ ownerShip: event.ownerShip,
241
+ tlonSkillVersion: event.tlonSkillVersion,
242
+ accountId: event.accountId,
243
+ configured: event.configured,
244
+ watchedChannelCount: event.watchedChannelCount,
245
+ dmAllowlistCount: event.dmAllowlistCount,
246
+ defaultAuthorizedShipsCount: event.defaultAuthorizedShipsCount,
247
+ pendingApprovalCount: event.pendingApprovalCount,
248
+ autoDiscoverChannels: event.autoDiscoverChannels,
249
+ ownerListenEnabled: event.ownerListenEnabled,
250
+ }),
251
+ });
252
+ }
84
253
  startReply(params) {
254
+ const normalizedParams = {
255
+ ...params,
256
+ sessionId: optionalString(params.sessionId),
257
+ runId: params.runId ?? null,
258
+ accountId: params.accountId ?? null,
259
+ agentId: params.agentId ?? null,
260
+ destinationKind: params.destinationKind ?? params.chatType,
261
+ };
85
262
  const toolTraceCursor = createToolTraceCursor(params.sessionKey);
263
+ rememberTlonSessionContext({
264
+ sessionKey: normalizedParams.sessionKey,
265
+ sessionId: normalizedParams.sessionId,
266
+ runId: normalizedParams.runId,
267
+ ownerShip: normalizedParams.ownerShip,
268
+ botShip: normalizedParams.botShip,
269
+ accountId: normalizedParams.accountId,
270
+ agentId: normalizedParams.agentId,
271
+ destinationKind: normalizedParams.destinationKind,
272
+ });
273
+ this.pruneActiveReplyTraces();
274
+ const token = Symbol(normalizedParams.sessionKey);
275
+ const trace = {
276
+ params: normalizedParams,
277
+ startedAt: Date.now(),
278
+ toolTraceCursor,
279
+ timeout: setTimeout(() => {
280
+ this.captureAbandonedReplyTrace(token, 'stale');
281
+ }, REPLY_TRACE_TTL_MS),
282
+ };
283
+ trace.timeout.unref?.();
284
+ this.activeReplyTraces.set(token, trace);
285
+ this.pruneActiveReplyTraces();
86
286
  return {
87
287
  capture: async (result) => {
288
+ const activeTrace = this.activeReplyTraces.get(token);
289
+ if (!activeTrace) {
290
+ return;
291
+ }
292
+ this.activeReplyTraces.delete(token);
293
+ clearTimeout(activeTrace.timeout);
88
294
  // Yield once so after_tool_call hooks for the just-finished reply have time to run.
89
295
  await new Promise((resolve) => setTimeout(resolve, 0));
296
+ const failedToolCount = countDispatchFailures(result.failedCounts, 'tool');
297
+ const failedBlockCount = countDispatchFailures(result.failedCounts, 'block');
298
+ const failedFinalCount = countDispatchFailures(result.failedCounts, 'final');
299
+ const sendErrorCount = Math.max(0, result.sendErrorCount ?? 0);
300
+ const sessionContext = lookupTlonSessionContext(activeTrace.params.sessionKey);
301
+ const outcome = resolveReplyOutcome({
302
+ deliveredMessageCount: result.deliveredMessageCount,
303
+ sendError: sendErrorCount > 0,
304
+ failedCounts: result.failedCounts,
305
+ dispatchError: result.dispatchError,
306
+ });
307
+ const sourceReplyDeliveryMode = result.sourceReplyDeliveryMode ?? null;
90
308
  this.captureReplyOutcome({
91
- ownerShip: params.ownerShip,
92
- botShip: params.botShip,
93
- outcome: resolveReplyOutcome({
94
- deliveredMessageCount: result.deliveredMessageCount,
95
- dispatchError: result.dispatchError,
96
- }),
97
- chatType: params.chatType,
98
- isThreadReply: params.isThreadReply,
99
- senderRole: params.senderRole,
100
- attachmentCount: params.attachmentCount,
309
+ sessionKey: activeTrace.params.sessionKey,
310
+ sessionId: sessionContext?.sessionId ?? activeTrace.params.sessionId,
311
+ runId: activeTrace.params.runId,
312
+ accountId: activeTrace.params.accountId,
313
+ agentId: activeTrace.params.agentId,
314
+ ownerShip: activeTrace.params.ownerShip,
315
+ botShip: activeTrace.params.botShip,
316
+ outcome,
317
+ chatType: activeTrace.params.chatType,
318
+ destinationKind: activeTrace.params.destinationKind,
319
+ isThreadReply: activeTrace.params.isThreadReply,
320
+ senderRole: activeTrace.params.senderRole,
321
+ attachmentCount: activeTrace.params.attachmentCount,
322
+ sendAttemptCount: Math.max(0, result.sendAttemptCount ?? 0),
323
+ sendError: sendErrorCount > 0,
324
+ sendErrorCount,
325
+ sendErrorKind: result.sendErrorKind ?? null,
101
326
  deliveredMessageCount: result.deliveredMessageCount,
102
327
  replyCharCount: result.replyCharCount,
103
328
  replyWordCount: result.replyWordCount,
@@ -106,14 +331,92 @@ class PostHogTlonTelemetry {
106
331
  queuedFinal: result.queuedFinal,
107
332
  queuedFinalCount: result.queuedFinalCount,
108
333
  queuedBlockCount: result.queuedBlockCount,
334
+ failedToolCount,
335
+ failedBlockCount,
336
+ failedFinalCount,
337
+ failedReplyCount: failedToolCount + failedBlockCount + failedFinalCount,
338
+ deliverySkipReason: resolveDeliverySkipReason({
339
+ outcome,
340
+ deliverySkipReason: result.deliverySkipReason,
341
+ sourceReplyDeliveryMode,
342
+ }),
343
+ sourceReplyDeliveryMode,
344
+ beforeAgentRunBlocked: result.beforeAgentRunBlocked === true,
345
+ dispatchError: Boolean(result.dispatchError),
346
+ dispatchErrorKind: classifyError(result.dispatchError),
347
+ abandonedReason: null,
109
348
  provider: result.provider,
110
349
  model: result.model,
111
350
  thinkLevel: result.thinkLevel,
112
- toolUsage: collectToolUsageSince(params.sessionKey, toolTraceCursor),
351
+ toolUsage: collectToolUsageSince(activeTrace.params.sessionKey, activeTrace.toolTraceCursor),
113
352
  });
114
353
  },
115
354
  };
116
355
  }
356
+ captureAbandonedReplyTrace(token, reason) {
357
+ const trace = this.activeReplyTraces.get(token);
358
+ if (!trace) {
359
+ return;
360
+ }
361
+ this.activeReplyTraces.delete(token);
362
+ clearTimeout(trace.timeout);
363
+ const sessionContext = lookupTlonSessionContext(trace.params.sessionKey);
364
+ this.captureReplyOutcome({
365
+ sessionKey: trace.params.sessionKey,
366
+ sessionId: sessionContext?.sessionId ?? trace.params.sessionId,
367
+ runId: trace.params.runId,
368
+ accountId: trace.params.accountId,
369
+ agentId: trace.params.agentId,
370
+ ownerShip: trace.params.ownerShip,
371
+ botShip: trace.params.botShip,
372
+ outcome: 'abandoned',
373
+ chatType: trace.params.chatType,
374
+ destinationKind: trace.params.destinationKind,
375
+ isThreadReply: trace.params.isThreadReply,
376
+ senderRole: trace.params.senderRole,
377
+ attachmentCount: trace.params.attachmentCount,
378
+ sendAttemptCount: 0,
379
+ sendError: false,
380
+ sendErrorCount: 0,
381
+ sendErrorKind: null,
382
+ deliveredMessageCount: 0,
383
+ replyCharCount: 0,
384
+ replyWordCount: 0,
385
+ replyMediaCount: 0,
386
+ dispatchDurationMs: Date.now() - trace.startedAt,
387
+ queuedFinal: false,
388
+ queuedFinalCount: 0,
389
+ queuedBlockCount: 0,
390
+ failedToolCount: 0,
391
+ failedBlockCount: 0,
392
+ failedFinalCount: 0,
393
+ failedReplyCount: 0,
394
+ deliverySkipReason: null,
395
+ sourceReplyDeliveryMode: null,
396
+ beforeAgentRunBlocked: false,
397
+ dispatchError: false,
398
+ dispatchErrorKind: null,
399
+ abandonedReason: reason,
400
+ provider: null,
401
+ model: null,
402
+ thinkLevel: null,
403
+ toolUsage: collectToolUsageSince(trace.params.sessionKey, trace.toolTraceCursor),
404
+ });
405
+ }
406
+ pruneActiveReplyTraces(now = Date.now()) {
407
+ for (const [token, trace] of this.activeReplyTraces) {
408
+ if (now - trace.startedAt > REPLY_TRACE_TTL_MS) {
409
+ this.captureAbandonedReplyTrace(token, 'stale');
410
+ }
411
+ }
412
+ while (this.activeReplyTraces.size > MAX_ACTIVE_REPLY_TRACES) {
413
+ const oldest = [...this.activeReplyTraces.entries()].sort((a, b) => a[1].startedAt - b[1].startedAt)[0];
414
+ if (!oldest) {
415
+ break;
416
+ }
417
+ this.captureAbandonedReplyTrace(oldest[0], 'max_traces');
418
+ }
419
+ }
117
420
  captureReplyOutcome(event) {
118
421
  const ownerShip = event.ownerShip ?? '';
119
422
  if (!this.ensureIdentified(ownerShip, event.botShip)) {
@@ -122,16 +425,25 @@ class PostHogTlonTelemetry {
122
425
  this.client.capture({
123
426
  distinctId: ownerShip,
124
427
  event: TLON_TELEMETRY_EVENT_NAME,
125
- properties: {
126
- logSource: TLON_TELEMETRY_LOG_SOURCE,
428
+ properties: this.properties({
429
+ sessionKey: event.sessionKey,
430
+ sessionId: event.sessionId,
431
+ runId: event.runId,
432
+ accountId: event.accountId,
433
+ agentId: event.agentId,
127
434
  botShip: event.botShip,
128
435
  ownerShip: event.ownerShip,
129
436
  outcome: event.outcome,
130
437
  chatType: event.chatType,
438
+ destinationKind: event.destinationKind,
131
439
  isThreadReply: event.isThreadReply,
132
440
  senderRole: event.senderRole,
133
441
  attachmentCount: event.attachmentCount,
134
442
  hasAttachments: event.attachmentCount > 0,
443
+ sendAttemptCount: event.sendAttemptCount,
444
+ sendError: event.sendError,
445
+ sendErrorCount: event.sendErrorCount,
446
+ sendErrorKind: event.sendErrorKind,
135
447
  deliveredMessageCount: event.deliveredMessageCount,
136
448
  replyCharCount: event.replyCharCount,
137
449
  replyWordCount: event.replyWordCount,
@@ -140,6 +452,16 @@ class PostHogTlonTelemetry {
140
452
  queuedFinal: event.queuedFinal,
141
453
  queuedFinalCount: event.queuedFinalCount,
142
454
  queuedBlockCount: event.queuedBlockCount,
455
+ failedToolCount: event.failedToolCount,
456
+ failedBlockCount: event.failedBlockCount,
457
+ failedFinalCount: event.failedFinalCount,
458
+ failedReplyCount: event.failedReplyCount,
459
+ deliverySkipReason: event.deliverySkipReason,
460
+ sourceReplyDeliveryMode: event.sourceReplyDeliveryMode,
461
+ beforeAgentRunBlocked: event.beforeAgentRunBlocked,
462
+ dispatchError: event.dispatchError,
463
+ dispatchErrorKind: event.dispatchErrorKind,
464
+ abandonedReason: event.abandonedReason,
143
465
  provider: event.provider,
144
466
  model: event.model,
145
467
  thinkLevel: event.thinkLevel,
@@ -152,7 +474,188 @@ class PostHogTlonTelemetry {
152
474
  durationMs: call.durationMs,
153
475
  error: call.error,
154
476
  })),
155
- },
477
+ }),
478
+ });
479
+ }
480
+ captureSessionLifecycle(event) {
481
+ const ownerShip = event.ownerShip ?? '';
482
+ if (!this.ensureIdentified(ownerShip, event.botShip)) {
483
+ return;
484
+ }
485
+ this.client.capture({
486
+ distinctId: ownerShip,
487
+ event: TLON_SESSION_LIFECYCLE_EVENT,
488
+ properties: this.properties({
489
+ botShip: event.botShip,
490
+ ownerShip: event.ownerShip,
491
+ accountId: event.accountId,
492
+ agentId: event.agentId,
493
+ sessionKey: event.sessionKey,
494
+ sessionId: event.sessionId,
495
+ lifecycleEvent: event.lifecycleEvent,
496
+ destinationKind: event.destinationKind,
497
+ reason: event.reason,
498
+ messageCount: event.messageCount,
499
+ durationMs: event.durationMs,
500
+ transcriptArchived: event.transcriptArchived,
501
+ hasNextSession: event.hasNextSession,
502
+ }),
503
+ });
504
+ }
505
+ captureSessionWatchdog(event) {
506
+ const ownerShip = event.ownerShip ?? '';
507
+ if (!this.ensureIdentified(ownerShip, event.botShip)) {
508
+ return;
509
+ }
510
+ this.client.capture({
511
+ distinctId: ownerShip,
512
+ event: TLON_SESSION_WATCHDOG_EVENT,
513
+ properties: this.properties({
514
+ botShip: event.botShip,
515
+ ownerShip: event.ownerShip,
516
+ accountId: event.accountId,
517
+ agentId: event.agentId,
518
+ sessionKey: event.sessionKey,
519
+ sessionId: event.sessionId,
520
+ diagnosticType: event.diagnosticType,
521
+ destinationKind: event.destinationKind,
522
+ state: event.state,
523
+ ageMs: event.ageMs,
524
+ queueDepth: event.queueDepth,
525
+ reason: event.reason,
526
+ classification: event.classification,
527
+ activeWorkKind: event.activeWorkKind,
528
+ lastProgressAgeMs: event.lastProgressAgeMs,
529
+ lastProgressReason: event.lastProgressReason,
530
+ activeToolName: event.activeToolName,
531
+ activeToolAgeMs: event.activeToolAgeMs,
532
+ terminalProgressStale: event.terminalProgressStale,
533
+ }),
534
+ });
535
+ }
536
+ captureSessionRecovery(event) {
537
+ const ownerShip = event.ownerShip ?? '';
538
+ if (!this.ensureIdentified(ownerShip, event.botShip)) {
539
+ return;
540
+ }
541
+ this.client.capture({
542
+ distinctId: ownerShip,
543
+ event: TLON_SESSION_RECOVERY_EVENT,
544
+ properties: this.properties({
545
+ botShip: event.botShip,
546
+ ownerShip: event.ownerShip,
547
+ accountId: event.accountId,
548
+ agentId: event.agentId,
549
+ sessionKey: event.sessionKey,
550
+ sessionId: event.sessionId,
551
+ diagnosticType: event.diagnosticType,
552
+ destinationKind: event.destinationKind,
553
+ state: event.state,
554
+ stateGeneration: event.stateGeneration,
555
+ ageMs: event.ageMs,
556
+ queueDepth: event.queueDepth,
557
+ reason: event.reason,
558
+ activeWorkKind: event.activeWorkKind,
559
+ allowActiveAbort: event.allowActiveAbort,
560
+ status: event.status,
561
+ action: event.action,
562
+ outcomeReason: event.outcomeReason,
563
+ released: event.released,
564
+ stale: event.stale,
565
+ }),
566
+ });
567
+ }
568
+ captureOutboundRoute(event) {
569
+ const ownerShip = event.ownerShip ?? '';
570
+ if (!this.ensureIdentified(ownerShip, event.botShip)) {
571
+ return;
572
+ }
573
+ this.client.capture({
574
+ distinctId: ownerShip,
575
+ event: TLON_OUTBOUND_ROUTED_EVENT,
576
+ properties: this.properties({
577
+ botShip: event.botShip,
578
+ ownerShip: event.ownerShip,
579
+ resolvedChannel: event.resolvedChannel,
580
+ routedToTlon: event.routedToTlon,
581
+ targetKind: event.targetKind,
582
+ }),
583
+ });
584
+ }
585
+ captureHarnessError(event) {
586
+ const ownerShip = event.ownerShip ?? '';
587
+ if (!this.ensureIdentified(ownerShip, event.botShip)) {
588
+ return;
589
+ }
590
+ this.client.capture({
591
+ distinctId: ownerShip,
592
+ event: TLON_HARNESS_ERROR_EVENT,
593
+ properties: this.properties({
594
+ harness: event.harness,
595
+ harnessEventType: event.harnessEventType,
596
+ errorScope: event.errorScope,
597
+ sessionKey: event.sessionKey,
598
+ sessionId: event.sessionId,
599
+ runId: event.runId,
600
+ accountId: event.accountId,
601
+ agentId: event.agentId,
602
+ ownerShip: event.ownerShip,
603
+ botShip: event.botShip,
604
+ destinationKind: event.destinationKind,
605
+ provider: event.provider,
606
+ model: event.model,
607
+ toolName: event.toolName,
608
+ phase: event.phase,
609
+ outcome: event.outcome,
610
+ errorCategory: event.errorCategory,
611
+ failureKind: event.failureKind,
612
+ durationMs: event.durationMs,
613
+ errorText: event.errorText,
614
+ }),
615
+ });
616
+ }
617
+ capturePluginError(event) {
618
+ const ownerShip = event.ownerShip ?? '';
619
+ if (!this.ensureIdentified(ownerShip, event.botShip)) {
620
+ return;
621
+ }
622
+ this.client.capture({
623
+ distinctId: ownerShip,
624
+ event: TLON_PLUGIN_ERROR_EVENT,
625
+ properties: this.properties({
626
+ harness: event.harness,
627
+ pluginErrorSource: event.pluginErrorSource,
628
+ accountId: event.accountId,
629
+ ownerShip: event.ownerShip,
630
+ botShip: event.botShip,
631
+ errorKind: event.errorKind,
632
+ errorText: event.errorText,
633
+ attempt: event.attempt,
634
+ }),
635
+ });
636
+ }
637
+ captureTelemetryError(event) {
638
+ const ownerShip = event.ownerShip ?? '';
639
+ if (!this.ensureIdentified(ownerShip, event.botShip)) {
640
+ return;
641
+ }
642
+ this.client.capture({
643
+ distinctId: ownerShip,
644
+ event: TLON_TELEMETRY_ERROR_EVENT,
645
+ properties: this.properties({
646
+ harness: event.harness,
647
+ telemetrySource: event.telemetrySource,
648
+ sourceEventName: event.sourceEventName,
649
+ sessionKey: event.sessionKey,
650
+ sessionId: event.sessionId,
651
+ runId: event.runId,
652
+ accountId: event.accountId,
653
+ agentId: event.agentId,
654
+ ownerShip: event.ownerShip,
655
+ botShip: event.botShip,
656
+ errorKind: event.errorKind,
657
+ errorText: event.errorText,
658
+ }),
156
659
  });
157
660
  }
158
661
  ensureIdentified(ownerShip, botShip) {
@@ -169,6 +672,7 @@ class PostHogTlonTelemetry {
169
672
  distinctId: ownerShip,
170
673
  properties: {
171
674
  logSource: TLON_TELEMETRY_LOG_SOURCE,
675
+ ...this.versionIdentity,
172
676
  tlonOwnerShip: ownerShip,
173
677
  tlonBotShip: botShip,
174
678
  },
@@ -183,8 +687,7 @@ class PostHogTlonTelemetry {
183
687
  this.client.capture({
184
688
  distinctId: event.ownerShip,
185
689
  event: TLON_HEARTBEAT_NUDGE_EVENT,
186
- properties: {
187
- logSource: TLON_TELEMETRY_LOG_SOURCE,
690
+ properties: this.properties({
188
691
  botShip: event.botShip,
189
692
  ownerShip: event.ownerShip,
190
693
  trigger: 'heartbeat',
@@ -195,7 +698,7 @@ class PostHogTlonTelemetry {
195
698
  accountId: event.accountId,
196
699
  messageId: event.messageId,
197
700
  nudgeSentAtMs: event.nudgeSentAtMs,
198
- },
701
+ }),
199
702
  });
200
703
  }
201
704
  captureHeartbeatReengagement(event) {
@@ -205,8 +708,7 @@ class PostHogTlonTelemetry {
205
708
  this.client.capture({
206
709
  distinctId: event.ownerShip,
207
710
  event: TLON_HEARTBEAT_REENGAGED_EVENT,
208
- properties: {
209
- logSource: TLON_TELEMETRY_LOG_SOURCE,
711
+ properties: this.properties({
210
712
  botShip: event.botShip,
211
713
  ownerShip: event.ownerShip,
212
714
  nudgeStage: event.nudgeStage,
@@ -215,10 +717,13 @@ class PostHogTlonTelemetry {
215
717
  reengagementDelayMs: event.reengagementDelayMs,
216
718
  channel: event.channel,
217
719
  accountId: event.accountId,
218
- },
720
+ }),
219
721
  });
220
722
  }
221
723
  async close() {
724
+ for (const token of [...this.activeReplyTraces.keys()]) {
725
+ this.captureAbandonedReplyTrace(token, 'telemetry_close');
726
+ }
222
727
  try {
223
728
  await this.client.flush();
224
729
  }
@@ -248,8 +753,230 @@ export function createTlonTelemetry(params) {
248
753
  runtime: params.runtime,
249
754
  });
250
755
  }
756
+ const outboundRouteReporterSlot = sharedSlot('telemetry.outboundRouteReporter');
757
+ const sessionTelemetryReporterSlot = sharedSlot('telemetry.sessionTelemetryReporter');
758
+ const errorTelemetryReporterSlot = sharedSlot('telemetry.errorTelemetryReporter');
759
+ export function setOutboundRouteReporter(reporter) {
760
+ outboundRouteReporterSlot.set(reporter);
761
+ }
762
+ export function reportOutboundRoute(event) {
763
+ outboundRouteReporterSlot.get()?.(event);
764
+ }
765
+ export function setSessionTelemetryReporter(reporter) {
766
+ sessionTelemetryReporterSlot.set(reporter);
767
+ }
768
+ export function setErrorTelemetryReporter(reporter) {
769
+ errorTelemetryReporterSlot.set(reporter);
770
+ }
771
+ function optionalString(value) {
772
+ const normalized = value?.trim();
773
+ return normalized ? normalized : null;
774
+ }
775
+ function optionalErrorText(value) {
776
+ return typeof value === 'string' && value.trim() ? value : null;
777
+ }
778
+ function optionalNumber(value) {
779
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
780
+ }
781
+ export function formatTlonTelemetryErrorText(error) {
782
+ if (error instanceof Error) {
783
+ return error.stack || error.message || String(error);
784
+ }
785
+ if (typeof error === 'string') {
786
+ return error;
787
+ }
788
+ if (error === null) {
789
+ return 'null';
790
+ }
791
+ if (error === undefined) {
792
+ return 'undefined';
793
+ }
794
+ try {
795
+ const json = JSON.stringify(error);
796
+ if (json) {
797
+ return json;
798
+ }
799
+ }
800
+ catch {
801
+ // Fall through to String(error).
802
+ }
803
+ return String(error);
804
+ }
805
+ export function reportSessionTurnCreated(event) {
806
+ const context = lookupTlonSessionContext(event.sessionKey);
807
+ if (!context) {
808
+ return;
809
+ }
810
+ updateTlonSessionContextRuntime(context, {
811
+ sessionId: event.sessionId,
812
+ runId: event.runId,
813
+ agentId: event.agentId,
814
+ });
815
+ }
816
+ export function reportHarnessError(event) {
817
+ const rememberedContext = lookupTlonSessionContext(event.sessionKey);
818
+ const context = rememberedContext
819
+ ? updateTlonSessionContextRuntime(rememberedContext, {
820
+ sessionId: event.sessionId,
821
+ runId: event.runId,
822
+ agentId: event.agentId,
823
+ })
824
+ : null;
825
+ if (!context && event.sessionKey) {
826
+ return;
827
+ }
828
+ errorTelemetryReporterSlot.get()?.({
829
+ kind: 'harness',
830
+ event: {
831
+ harness: 'openclaw',
832
+ harnessEventType: event.harnessEventType,
833
+ errorScope: event.errorScope,
834
+ sessionKey: context?.sessionKey ?? null,
835
+ sessionId: context?.sessionId ?? optionalString(event.sessionId),
836
+ runId: optionalString(event.runId) ?? context?.runId ?? null,
837
+ accountId: context?.accountId ?? optionalString(event.accountId),
838
+ agentId: optionalString(event.agentId) ?? context?.agentId ?? null,
839
+ ownerShip: context?.ownerShip ?? optionalString(event.ownerShip),
840
+ botShip: context?.botShip ?? optionalString(event.botShip) ?? '',
841
+ destinationKind: context?.destinationKind ?? event.destinationKind ?? null,
842
+ provider: optionalString(event.provider),
843
+ model: optionalString(event.model),
844
+ toolName: optionalString(event.toolName),
845
+ phase: optionalString(event.phase),
846
+ outcome: optionalString(event.outcome),
847
+ errorCategory: optionalString(event.errorCategory),
848
+ failureKind: optionalString(event.failureKind),
849
+ durationMs: optionalNumber(event.durationMs),
850
+ errorText: optionalErrorText(event.errorText),
851
+ },
852
+ });
853
+ }
854
+ export function reportPluginError(event) {
855
+ errorTelemetryReporterSlot.get()?.({
856
+ kind: 'plugin',
857
+ event,
858
+ });
859
+ }
860
+ export function reportTelemetryError(event) {
861
+ const rememberedContext = lookupTlonSessionContext(event.sessionKey);
862
+ const context = rememberedContext
863
+ ? updateTlonSessionContextRuntime(rememberedContext, {
864
+ sessionId: event.sessionId,
865
+ runId: event.runId,
866
+ agentId: event.agentId,
867
+ })
868
+ : null;
869
+ errorTelemetryReporterSlot.get()?.({
870
+ kind: 'telemetry',
871
+ event: {
872
+ ...event,
873
+ sessionKey: context?.sessionKey ?? optionalString(event.sessionKey),
874
+ sessionId: context?.sessionId ?? optionalString(event.sessionId),
875
+ runId: optionalString(event.runId) ?? context?.runId ?? null,
876
+ agentId: optionalString(event.agentId) ?? context?.agentId ?? null,
877
+ accountId: event.accountId ?? context?.accountId ?? null,
878
+ ownerShip: event.ownerShip ?? context?.ownerShip ?? null,
879
+ botShip: event.botShip ?? context?.botShip ?? null,
880
+ sourceEventName: optionalString(event.sourceEventName),
881
+ errorKind: optionalString(event.errorKind),
882
+ },
883
+ });
884
+ }
885
+ export function reportSessionLifecycle(event) {
886
+ const rememberedContext = lookupTlonSessionContext(event.sessionKey);
887
+ const context = rememberedContext
888
+ ? updateTlonSessionContextSessionId(rememberedContext, event.sessionId)
889
+ : null;
890
+ if (!context) {
891
+ return;
892
+ }
893
+ sessionTelemetryReporterSlot.get()?.({
894
+ kind: 'lifecycle',
895
+ event: {
896
+ lifecycleEvent: event.lifecycleEvent,
897
+ sessionKey: context.sessionKey,
898
+ sessionId: context.sessionId,
899
+ accountId: context.accountId,
900
+ agentId: optionalString(event.agentId) ?? context.agentId,
901
+ ownerShip: context.ownerShip,
902
+ botShip: context.botShip,
903
+ destinationKind: context.destinationKind,
904
+ reason: optionalString(event.reason),
905
+ messageCount: optionalNumber(event.messageCount),
906
+ durationMs: optionalNumber(event.durationMs),
907
+ transcriptArchived: typeof event.transcriptArchived === 'boolean'
908
+ ? event.transcriptArchived
909
+ : null,
910
+ hasNextSession: event.hasNextSession === true,
911
+ },
912
+ });
913
+ }
914
+ export function reportSessionDiagnostic(event) {
915
+ const rememberedContext = lookupTlonSessionContext(event.sessionKey);
916
+ const context = rememberedContext
917
+ ? updateTlonSessionContextSessionId(rememberedContext, event.sessionId)
918
+ : null;
919
+ if (!context) {
920
+ return;
921
+ }
922
+ if (event.type === 'session.stalled' || event.type === 'session.stuck') {
923
+ sessionTelemetryReporterSlot.get()?.({
924
+ kind: 'watchdog',
925
+ event: {
926
+ diagnosticType: event.type,
927
+ sessionKey: context.sessionKey,
928
+ sessionId: context.sessionId,
929
+ accountId: context.accountId,
930
+ agentId: context.agentId,
931
+ ownerShip: context.ownerShip,
932
+ botShip: context.botShip,
933
+ destinationKind: context.destinationKind,
934
+ state: event.state,
935
+ ageMs: event.ageMs,
936
+ queueDepth: optionalNumber(event.queueDepth),
937
+ reason: optionalString(event.reason),
938
+ classification: event.classification,
939
+ activeWorkKind: optionalString(event.activeWorkKind),
940
+ lastProgressAgeMs: optionalNumber(event.lastProgressAgeMs),
941
+ lastProgressReason: optionalString(event.lastProgressReason),
942
+ activeToolName: optionalString(event.activeToolName),
943
+ activeToolAgeMs: optionalNumber(event.activeToolAgeMs),
944
+ terminalProgressStale: event.terminalProgressStale === true,
945
+ },
946
+ });
947
+ return;
948
+ }
949
+ const recoveryEvent = event;
950
+ sessionTelemetryReporterSlot.get()?.({
951
+ kind: 'recovery',
952
+ event: {
953
+ diagnosticType: recoveryEvent.type,
954
+ sessionKey: context.sessionKey,
955
+ sessionId: context.sessionId,
956
+ accountId: context.accountId,
957
+ agentId: context.agentId,
958
+ ownerShip: context.ownerShip,
959
+ botShip: context.botShip,
960
+ destinationKind: context.destinationKind,
961
+ state: recoveryEvent.state,
962
+ stateGeneration: optionalNumber(recoveryEvent.stateGeneration),
963
+ ageMs: recoveryEvent.ageMs,
964
+ queueDepth: optionalNumber(recoveryEvent.queueDepth),
965
+ reason: optionalString(recoveryEvent.reason),
966
+ activeWorkKind: optionalString(recoveryEvent.activeWorkKind),
967
+ allowActiveAbort: recoveryEvent.allowActiveAbort === true,
968
+ status: optionalString(recoveryEvent.status),
969
+ action: optionalString(recoveryEvent.action),
970
+ outcomeReason: optionalString(recoveryEvent.outcomeReason),
971
+ released: optionalNumber(recoveryEvent.released),
972
+ stale: recoveryEvent.stale === true,
973
+ },
974
+ });
975
+ }
251
976
  export const _testing = {
252
977
  clearToolCalls: () => toolCallsBySession.clear(),
978
+ clearSessionContexts: () => sessionContextsBySessionKey.clear(),
979
+ getReplyTraceTtlMs: () => REPLY_TRACE_TTL_MS,
253
980
  getToolTraceTtlMs: () => TOOL_TRACE_TTL_MS,
254
981
  };
255
982
  //# sourceMappingURL=telemetry.js.map