@tloncorp/openclaw 0.4.3 → 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.
Files changed (112) hide show
  1. package/README.md +130 -141
  2. package/dist/index.js +703 -152
  3. package/dist/index.js.map +1 -1
  4. package/dist/setup-api.js +2 -2
  5. package/dist/setup-entry.js +2 -2
  6. package/dist/setup-entry.js.map +1 -1
  7. package/dist/src/account-fields.js +7 -3
  8. package/dist/src/account-fields.js.map +1 -1
  9. package/dist/src/actions.js +73 -52
  10. package/dist/src/actions.js.map +1 -1
  11. package/dist/src/channel.js +63 -39
  12. package/dist/src/channel.js.map +1 -1
  13. package/dist/src/channel.runtime.js +61 -32
  14. package/dist/src/channel.runtime.js.map +1 -1
  15. package/dist/src/config-schema.js +24 -4
  16. package/dist/src/config-schema.js.map +1 -1
  17. package/dist/src/diagnostic-subscriptions.js +49 -0
  18. package/dist/src/diagnostic-subscriptions.js.map +1 -0
  19. package/dist/src/effective-owner.js.map +1 -1
  20. package/dist/src/gateway-status.js +55 -7
  21. package/dist/src/gateway-status.js.map +1 -1
  22. package/dist/src/monitor/approval.js +71 -62
  23. package/dist/src/monitor/approval.js.map +1 -1
  24. package/dist/src/monitor/command-auth.js +7 -7
  25. package/dist/src/monitor/command-auth.js.map +1 -1
  26. package/dist/src/monitor/command-bridge.js +3 -2
  27. package/dist/src/monitor/command-bridge.js.map +1 -1
  28. package/dist/src/monitor/computing-presence.js +76 -12
  29. package/dist/src/monitor/computing-presence.js.map +1 -1
  30. package/dist/src/monitor/discovery.js +16 -9
  31. package/dist/src/monitor/discovery.js.map +1 -1
  32. package/dist/src/monitor/history.js +58 -26
  33. package/dist/src/monitor/history.js.map +1 -1
  34. package/dist/src/monitor/index.js +3018 -2496
  35. package/dist/src/monitor/index.js.map +1 -1
  36. package/dist/src/monitor/media.js +106 -78
  37. package/dist/src/monitor/media.js.map +1 -1
  38. package/dist/src/monitor/nudge-runner.js +36 -27
  39. package/dist/src/monitor/nudge-runner.js.map +1 -1
  40. package/dist/src/monitor/nudge-state.js +7 -11
  41. package/dist/src/monitor/nudge-state.js.map +1 -1
  42. package/dist/src/monitor/owner-reply-persistence.js +27 -26
  43. package/dist/src/monitor/owner-reply-persistence.js.map +1 -1
  44. package/dist/src/monitor/processed-messages.js.map +1 -1
  45. package/dist/src/monitor/session-routing.js +261 -0
  46. package/dist/src/monitor/session-routing.js.map +1 -0
  47. package/dist/src/monitor/settings-sync.js +1 -8
  48. package/dist/src/monitor/settings-sync.js.map +1 -1
  49. package/dist/src/monitor/utils.js +77 -71
  50. package/dist/src/monitor/utils.js.map +1 -1
  51. package/dist/src/nudge-decision.js +40 -43
  52. package/dist/src/nudge-decision.js.map +1 -1
  53. package/dist/src/nudge-messages.js +9 -9
  54. package/dist/src/nudge-scheduler.js.map +1 -1
  55. package/dist/src/owner-listen-command.js +38 -28
  56. package/dist/src/owner-listen-command.js.map +1 -1
  57. package/dist/src/pending-nudge.js.map +1 -1
  58. package/dist/src/runtime.js +10 -6
  59. package/dist/src/runtime.js.map +1 -1
  60. package/dist/src/session-roles.js +2 -1
  61. package/dist/src/session-roles.js.map +1 -1
  62. package/dist/src/session-route.js +44 -0
  63. package/dist/src/session-route.js.map +1 -0
  64. package/dist/src/settings.js +233 -102
  65. package/dist/src/settings.js.map +1 -1
  66. package/dist/src/setup-core.js +32 -32
  67. package/dist/src/setup-core.js.map +1 -1
  68. package/dist/src/setup-surface.js +19 -19
  69. package/dist/src/setup-surface.js.map +1 -1
  70. package/dist/src/shared-state.js +46 -0
  71. package/dist/src/shared-state.js.map +1 -0
  72. package/dist/src/targets.js +17 -10
  73. package/dist/src/targets.js.map +1 -1
  74. package/dist/src/telemetry.js +764 -34
  75. package/dist/src/telemetry.js.map +1 -1
  76. package/dist/src/tlon-binary.js +20 -12
  77. package/dist/src/tlon-binary.js.map +1 -1
  78. package/dist/src/tlon-tool-guard.js +5 -5
  79. package/dist/src/tool-trace.js +17 -13
  80. package/dist/src/tool-trace.js.map +1 -1
  81. package/dist/src/types.js +30 -12
  82. package/dist/src/types.js.map +1 -1
  83. package/dist/src/urbit/api-client.js +16 -12
  84. package/dist/src/urbit/api-client.js.map +1 -1
  85. package/dist/src/urbit/auth.js +9 -9
  86. package/dist/src/urbit/auth.js.map +1 -1
  87. package/dist/src/urbit/base-url.js +11 -11
  88. package/dist/src/urbit/base-url.js.map +1 -1
  89. package/dist/src/urbit/channel-ops.js +25 -19
  90. package/dist/src/urbit/channel-ops.js.map +1 -1
  91. package/dist/src/urbit/context.js +8 -8
  92. package/dist/src/urbit/context.js.map +1 -1
  93. package/dist/src/urbit/errors.js +33 -7
  94. package/dist/src/urbit/errors.js.map +1 -1
  95. package/dist/src/urbit/fetch.js +3 -3
  96. package/dist/src/urbit/fetch.js.map +1 -1
  97. package/dist/src/urbit/http-poke.js +10 -10
  98. package/dist/src/urbit/http-poke.js.map +1 -1
  99. package/dist/src/urbit/send.js +27 -23
  100. package/dist/src/urbit/send.js.map +1 -1
  101. package/dist/src/urbit/sse-client.js +45 -41
  102. package/dist/src/urbit/sse-client.js.map +1 -1
  103. package/dist/src/urbit/story.js +31 -30
  104. package/dist/src/urbit/story.js.map +1 -1
  105. package/dist/src/urbit/upload.js +8 -8
  106. package/dist/src/urbit/upload.js.map +1 -1
  107. package/dist/src/version.generated.js +2 -1
  108. package/dist/src/version.generated.js.map +1 -1
  109. package/dist/src/version.js +134 -0
  110. package/dist/src/version.js.map +1 -0
  111. package/openclaw.plugin.json +37 -0
  112. package/package.json +9 -15
@@ -1,11 +1,26 @@
1
- import { PostHog } from "posthog-node";
2
- const TLON_TELEMETRY_EVENT_NAME = "TlonBot Reply Handled";
3
- const TLON_HEARTBEAT_NUDGE_EVENT = "TlonBot Heartbeat Nudge Sent";
4
- const TLON_HEARTBEAT_REENGAGED_EVENT = "TlonBot Heartbeat Nudge Reengaged";
5
- const TLON_TELEMETRY_LOG_SOURCE = "openclawPlugin";
1
+ import { PostHog } from 'posthog-node';
2
+ import { sharedMap, sharedSlot } from './shared-state.js';
3
+ import { getTlonVersionIdentity } from './version.js';
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';
13
+ const TLON_HEARTBEAT_NUDGE_EVENT = 'TlonBot Heartbeat Nudge Sent';
14
+ const TLON_HEARTBEAT_REENGAGED_EVENT = 'TlonBot Heartbeat Nudge Reengaged';
15
+ const TLON_TELEMETRY_LOG_SOURCE = 'openclawPlugin';
6
16
  const TOOL_TRACE_TTL_MS = 60 * 60 * 1000;
7
17
  const MAX_TOOL_CALLS_PER_SESSION = 200;
8
- const toolCallsBySession = new Map();
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;
22
+ const toolCallsBySession = sharedMap('telemetry.toolCallsBySession');
23
+ const sessionContextsBySessionKey = sharedMap('telemetry.sessionContextsBySessionKey');
9
24
  function cleanupToolCalls(now = Date.now()) {
10
25
  for (const [sessionKey, trace] of toolCallsBySession) {
11
26
  if (now - trace.updatedAt > TOOL_TRACE_TTL_MS) {
@@ -28,7 +43,7 @@ export function recordToolCall(params) {
28
43
  trace.updatedAt = now;
29
44
  trace.calls.push({
30
45
  toolName: params.toolName,
31
- durationMs: typeof params.durationMs === "number" ? params.durationMs : null,
46
+ durationMs: typeof params.durationMs === 'number' ? params.durationMs : null,
32
47
  error: params.error ?? null,
33
48
  recordedAt: now,
34
49
  });
@@ -58,16 +73,141 @@ function collectToolUsageSince(sessionKey, cursor) {
58
73
  errorCount: calls.filter((call) => call.error).length,
59
74
  };
60
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
+ }
61
182
  function resolveReplyOutcome(params) {
62
183
  if (params.deliveredMessageCount > 0) {
63
- return "responded";
184
+ return 'responded';
64
185
  }
65
- 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;
66
204
  }
67
205
  class PostHogTlonTelemetry {
68
206
  client;
69
207
  runtime;
208
+ versionIdentity = getTlonVersionIdentity();
70
209
  identifiedOwners = new Set();
210
+ activeReplyTraces = new Map();
71
211
  missingOwnerWarningLogged = false;
72
212
  constructor(params) {
73
213
  this.runtime = params.runtime;
@@ -80,23 +220,109 @@ class PostHogTlonTelemetry {
80
220
  disableRemoteConfig: true,
81
221
  });
82
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
+ }
83
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
+ };
84
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();
85
286
  return {
86
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);
87
294
  // Yield once so after_tool_call hooks for the just-finished reply have time to run.
88
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;
89
308
  this.captureReplyOutcome({
90
- ownerShip: params.ownerShip,
91
- botShip: params.botShip,
92
- outcome: resolveReplyOutcome({
93
- deliveredMessageCount: result.deliveredMessageCount,
94
- dispatchError: result.dispatchError,
95
- }),
96
- chatType: params.chatType,
97
- isThreadReply: params.isThreadReply,
98
- senderRole: params.senderRole,
99
- 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,
100
326
  deliveredMessageCount: result.deliveredMessageCount,
101
327
  replyCharCount: result.replyCharCount,
102
328
  replyWordCount: result.replyWordCount,
@@ -105,32 +331,119 @@ class PostHogTlonTelemetry {
105
331
  queuedFinal: result.queuedFinal,
106
332
  queuedFinalCount: result.queuedFinalCount,
107
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,
108
348
  provider: result.provider,
109
349
  model: result.model,
110
350
  thinkLevel: result.thinkLevel,
111
- toolUsage: collectToolUsageSince(params.sessionKey, toolTraceCursor),
351
+ toolUsage: collectToolUsageSince(activeTrace.params.sessionKey, activeTrace.toolTraceCursor),
112
352
  });
113
353
  },
114
354
  };
115
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
+ }
116
420
  captureReplyOutcome(event) {
117
- const ownerShip = event.ownerShip ?? "";
421
+ const ownerShip = event.ownerShip ?? '';
118
422
  if (!this.ensureIdentified(ownerShip, event.botShip)) {
119
423
  return;
120
424
  }
121
425
  this.client.capture({
122
426
  distinctId: ownerShip,
123
427
  event: TLON_TELEMETRY_EVENT_NAME,
124
- properties: {
125
- 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,
126
434
  botShip: event.botShip,
127
435
  ownerShip: event.ownerShip,
128
436
  outcome: event.outcome,
129
437
  chatType: event.chatType,
438
+ destinationKind: event.destinationKind,
130
439
  isThreadReply: event.isThreadReply,
131
440
  senderRole: event.senderRole,
132
441
  attachmentCount: event.attachmentCount,
133
442
  hasAttachments: event.attachmentCount > 0,
443
+ sendAttemptCount: event.sendAttemptCount,
444
+ sendError: event.sendError,
445
+ sendErrorCount: event.sendErrorCount,
446
+ sendErrorKind: event.sendErrorKind,
134
447
  deliveredMessageCount: event.deliveredMessageCount,
135
448
  replyCharCount: event.replyCharCount,
136
449
  replyWordCount: event.replyWordCount,
@@ -139,6 +452,16 @@ class PostHogTlonTelemetry {
139
452
  queuedFinal: event.queuedFinal,
140
453
  queuedFinalCount: event.queuedFinalCount,
141
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,
142
465
  provider: event.provider,
143
466
  model: event.model,
144
467
  thinkLevel: event.thinkLevel,
@@ -151,14 +474,195 @@ class PostHogTlonTelemetry {
151
474
  durationMs: call.durationMs,
152
475
  error: call.error,
153
476
  })),
154
- },
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
+ }),
155
659
  });
156
660
  }
157
661
  ensureIdentified(ownerShip, botShip) {
158
662
  if (!ownerShip) {
159
663
  if (!this.missingOwnerWarningLogged) {
160
664
  this.missingOwnerWarningLogged = true;
161
- this.runtime?.log?.("[tlon] Telemetry is enabled but ownerShip is not configured; skipping telemetry events");
665
+ this.runtime?.log?.('[tlon] Telemetry is enabled but ownerShip is not configured; skipping telemetry events');
162
666
  }
163
667
  return false;
164
668
  }
@@ -168,6 +672,7 @@ class PostHogTlonTelemetry {
168
672
  distinctId: ownerShip,
169
673
  properties: {
170
674
  logSource: TLON_TELEMETRY_LOG_SOURCE,
675
+ ...this.versionIdentity,
171
676
  tlonOwnerShip: ownerShip,
172
677
  tlonBotShip: botShip,
173
678
  },
@@ -182,17 +687,18 @@ class PostHogTlonTelemetry {
182
687
  this.client.capture({
183
688
  distinctId: event.ownerShip,
184
689
  event: TLON_HEARTBEAT_NUDGE_EVENT,
185
- properties: {
186
- logSource: TLON_TELEMETRY_LOG_SOURCE,
690
+ properties: this.properties({
187
691
  botShip: event.botShip,
188
692
  ownerShip: event.ownerShip,
189
- trigger: "heartbeat",
693
+ trigger: 'heartbeat',
190
694
  nudgeStage: event.nudgeStage,
191
695
  nudgeTarget: event.nudgeTarget,
192
696
  channel: event.channel,
193
697
  success: event.success,
194
698
  accountId: event.accountId,
195
- },
699
+ messageId: event.messageId,
700
+ nudgeSentAtMs: event.nudgeSentAtMs,
701
+ }),
196
702
  });
197
703
  }
198
704
  captureHeartbeatReengagement(event) {
@@ -202,8 +708,7 @@ class PostHogTlonTelemetry {
202
708
  this.client.capture({
203
709
  distinctId: event.ownerShip,
204
710
  event: TLON_HEARTBEAT_REENGAGED_EVENT,
205
- properties: {
206
- logSource: TLON_TELEMETRY_LOG_SOURCE,
711
+ properties: this.properties({
207
712
  botShip: event.botShip,
208
713
  ownerShip: event.ownerShip,
209
714
  nudgeStage: event.nudgeStage,
@@ -212,10 +717,13 @@ class PostHogTlonTelemetry {
212
717
  reengagementDelayMs: event.reengagementDelayMs,
213
718
  channel: event.channel,
214
719
  accountId: event.accountId,
215
- },
720
+ }),
216
721
  });
217
722
  }
218
723
  async close() {
724
+ for (const token of [...this.activeReplyTraces.keys()]) {
725
+ this.captureAbandonedReplyTrace(token, 'telemetry_close');
726
+ }
219
727
  try {
220
728
  await this.client.flush();
221
729
  }
@@ -235,18 +743,240 @@ export function createTlonTelemetry(params) {
235
743
  return null;
236
744
  }
237
745
  if (!params.config.apiKey) {
238
- params.runtime?.log?.("[tlon] Telemetry is enabled but telemetry.apiKey is missing; telemetry disabled");
746
+ params.runtime?.log?.('[tlon] Telemetry is enabled but telemetry.apiKey is missing; telemetry disabled');
239
747
  return null;
240
748
  }
241
- params.runtime?.log?.(`[tlon] Telemetry enabled${params.config.host ? ` (${params.config.host})` : ""}`);
749
+ params.runtime?.log?.(`[tlon] Telemetry enabled${params.config.host ? ` (${params.config.host})` : ''}`);
242
750
  return new PostHogTlonTelemetry({
243
751
  apiKey: params.config.apiKey,
244
752
  host: params.config.host,
245
753
  runtime: params.runtime,
246
754
  });
247
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
+ }
248
976
  export const _testing = {
249
977
  clearToolCalls: () => toolCallsBySession.clear(),
978
+ clearSessionContexts: () => sessionContextsBySessionKey.clear(),
979
+ getReplyTraceTtlMs: () => REPLY_TRACE_TTL_MS,
250
980
  getToolTraceTtlMs: () => TOOL_TRACE_TTL_MS,
251
981
  };
252
982
  //# sourceMappingURL=telemetry.js.map