opencode-tbot 0.1.30 → 0.1.31

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/plugin.js CHANGED
@@ -1,83 +1,360 @@
1
- import { c as loadAppConfig, i as preparePluginConfiguration, o as OPENCODE_TBOT_VERSION } from "./assets/plugin-config-DNeV2Ckw.js";
2
- import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
1
+ import { i as OPENCODE_TBOT_VERSION, n as preparePluginConfiguration, o as loadAppConfig } from "./assets/plugin-config-jkAZYbFW.js";
2
+ import { appendFile, mkdir, readFile, readdir, rename, stat, unlink, writeFile } from "node:fs/promises";
3
3
  import { dirname, isAbsolute, join } from "node:path";
4
4
  import { parse, printParseErrorCode } from "jsonc-parser";
5
5
  import { z } from "zod";
6
- import { createOpencodeClient } from "@opencode-ai/sdk";
7
6
  import { randomUUID } from "node:crypto";
7
+ import { createOpencodeClient } from "@opencode-ai/sdk";
8
8
  import { run } from "@grammyjs/runner";
9
9
  import { Bot, GrammyError, HttpError, InlineKeyboard } from "grammy";
10
10
  //#region src/infra/utils/redact.ts
11
- var REDACTED = "[REDACTED]";
12
- var DEFAULT_PREVIEW_LENGTH = 160;
11
+ var REDACTED$1 = "[REDACTED]";
13
12
  var TELEGRAM_TOKEN_PATTERN = /\b\d{6,}:[A-Za-z0-9_-]{20,}\b/g;
14
13
  var BEARER_TOKEN_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/-]+=*\b/gi;
15
14
  var NAMED_SECRET_PATTERN = /\b(api[_\s-]?key|token|secret|password)\b(\s*[:=]\s*)([^\s,;]+)/gi;
16
15
  var API_KEY_LIKE_PATTERN = /\b(?:sk|pk)_[A-Za-z0-9_-]{10,}\b/g;
17
16
  function redactSensitiveText(input) {
18
- return input.replace(BEARER_TOKEN_PATTERN, `Bearer ${REDACTED}`).replace(TELEGRAM_TOKEN_PATTERN, REDACTED).replace(NAMED_SECRET_PATTERN, (_, name, separator) => `${name}${separator}${REDACTED}`).replace(API_KEY_LIKE_PATTERN, REDACTED);
19
- }
20
- function createRedactedPreview(input, maxLength = DEFAULT_PREVIEW_LENGTH) {
21
- const redacted = redactSensitiveText(input).replace(/\s+/g, " ").trim();
22
- if (redacted.length <= maxLength) return redacted;
23
- return `${redacted.slice(0, Math.max(0, maxLength - 3))}...`;
17
+ return input.replace(BEARER_TOKEN_PATTERN, `Bearer ${REDACTED$1}`).replace(TELEGRAM_TOKEN_PATTERN, REDACTED$1).replace(NAMED_SECRET_PATTERN, (_, name, separator) => `${name}${separator}${REDACTED$1}`).replace(API_KEY_LIKE_PATTERN, REDACTED$1);
24
18
  }
25
19
  //#endregion
26
20
  //#region src/infra/logger/index.ts
21
+ var DEFAULT_COMPONENT = "app";
22
+ var DEFAULT_EVENT = "log";
27
23
  var DEFAULT_SERVICE_NAME = "opencode-tbot";
24
+ var DEFAULT_MAX_LOG_FILES = 30;
25
+ var DEFAULT_MAX_TOTAL_LOG_BYTES = 314572800;
26
+ var CONTENT_OMITTED = "[OMITTED]";
27
+ var REDACTED = "[REDACTED]";
28
28
  var LEVEL_PRIORITY = {
29
29
  debug: 10,
30
30
  info: 20,
31
31
  warn: 30,
32
32
  error: 40
33
33
  };
34
+ var RESERVED_EVENT_FIELDS = new Set([
35
+ "attempt",
36
+ "callbackData",
37
+ "chatId",
38
+ "command",
39
+ "component",
40
+ "correlationId",
41
+ "durationMs",
42
+ "error",
43
+ "event",
44
+ "operationId",
45
+ "projectId",
46
+ "requestId",
47
+ "runtimeId",
48
+ "sessionId",
49
+ "sizeBytes",
50
+ "status",
51
+ "updateId",
52
+ "worktree"
53
+ ]);
34
54
  function createOpenCodeAppLogger(client, options = {}) {
35
55
  const service = normalizeServiceName(options.service);
36
56
  const minimumLevel = normalizeLogLevel(options.level);
57
+ const runtimeId = normalizeString(options.runtimeId) ?? randomUUID();
58
+ const boundRootContext = {
59
+ component: DEFAULT_COMPONENT,
60
+ runtimeId,
61
+ ...options.worktree ? { worktree: options.worktree } : {}
62
+ };
63
+ const sinks = createSinks(client, {
64
+ file: {
65
+ dir: options.file?.dir,
66
+ maxFiles: options.file?.retention?.maxFiles,
67
+ maxTotalBytes: options.file?.retention?.maxTotalBytes
68
+ },
69
+ runtimeId,
70
+ service,
71
+ sinks: options.sinks
72
+ });
37
73
  let queue = Promise.resolve();
38
- const enqueue = (level, input, message) => {
39
- if (LEVEL_PRIORITY[level] < LEVEL_PRIORITY[minimumLevel]) return;
40
- const { extra, text } = normalizeLogArguments(input, message);
41
- const payload = {
42
- service,
43
- level,
44
- message: text,
45
- ...extra ? { extra } : {}
46
- };
74
+ const enqueue = (event) => {
75
+ if (LEVEL_PRIORITY[event.level] < LEVEL_PRIORITY[minimumLevel]) return;
76
+ const structuredEvent = buildStructuredLogEvent(event.level, event.input, event.message, service, event.context);
47
77
  queue = queue.catch(() => void 0).then(async () => {
48
- try {
49
- if (client.app.log.length >= 2) {
50
- await client.app.log(payload, {
51
- responseStyle: "data",
52
- throwOnError: true
53
- });
54
- return;
55
- }
56
- await client.app.log({
57
- body: payload,
58
- responseStyle: "data",
59
- throwOnError: true
60
- });
61
- } catch {}
78
+ await Promise.allSettled(sinks.map((sink) => sink.write(structuredEvent)));
62
79
  });
63
80
  };
64
- return {
81
+ const createLogger = (context) => ({
65
82
  debug(input, message) {
66
- enqueue("debug", input, message);
83
+ enqueue({
84
+ context,
85
+ input,
86
+ level: "debug",
87
+ message
88
+ });
67
89
  },
68
90
  info(input, message) {
69
- enqueue("info", input, message);
91
+ enqueue({
92
+ context,
93
+ input,
94
+ level: "info",
95
+ message
96
+ });
70
97
  },
71
98
  warn(input, message) {
72
- enqueue("warn", input, message);
99
+ enqueue({
100
+ context,
101
+ input,
102
+ level: "warn",
103
+ message
104
+ });
73
105
  },
74
106
  error(input, message) {
75
- enqueue("error", input, message);
107
+ enqueue({
108
+ context,
109
+ input,
110
+ level: "error",
111
+ message
112
+ });
113
+ },
114
+ child(childContext) {
115
+ return createLogger({
116
+ ...context,
117
+ ...removeUndefinedFields(childContext)
118
+ });
76
119
  },
77
120
  async flush() {
78
121
  await queue.catch(() => void 0);
122
+ await Promise.allSettled(sinks.map(async (sink) => {
123
+ await sink.flush?.();
124
+ }));
79
125
  }
126
+ });
127
+ return createLogger(boundRootContext);
128
+ }
129
+ function logTelegramUpdate(logger, input, message) {
130
+ logger.info({
131
+ component: "telegram",
132
+ ...input
133
+ }, message);
134
+ }
135
+ function logPromptLifecycle(logger, input, message) {
136
+ logger.info({
137
+ component: "prompt",
138
+ ...input
139
+ }, message);
140
+ }
141
+ function logOpenCodeRequest(logger, input, message) {
142
+ logger.info({
143
+ component: "opencode",
144
+ ...input
145
+ }, message);
146
+ }
147
+ function logPluginEvent(logger, input, message) {
148
+ logger.info({
149
+ component: "plugin-event",
150
+ ...input
151
+ }, message);
152
+ }
153
+ function createSinks(client, options) {
154
+ const sinkOptions = options.sinks ?? {};
155
+ const sinks = [];
156
+ if (sinkOptions.host !== false) sinks.push(createHostSink(client, options.service));
157
+ if (sinkOptions.file !== false && options.file?.dir) sinks.push(createJsonlFileSink({
158
+ dir: options.file.dir,
159
+ maxFiles: options.file.maxFiles,
160
+ maxTotalBytes: options.file.maxTotalBytes,
161
+ runtimeId: options.runtimeId
162
+ }));
163
+ return sinks;
164
+ }
165
+ function createHostSink(client, service) {
166
+ return { async write(event) {
167
+ const payload = {
168
+ service,
169
+ level: event.level,
170
+ message: event.message,
171
+ extra: buildHostLogExtra(event)
172
+ };
173
+ if (client.app.log.length >= 2) {
174
+ await client.app.log(payload, {
175
+ responseStyle: "data",
176
+ throwOnError: true
177
+ });
178
+ return;
179
+ }
180
+ await client.app.log({
181
+ body: payload,
182
+ responseStyle: "data",
183
+ throwOnError: true
184
+ });
185
+ } };
186
+ }
187
+ function createJsonlFileSink(options) {
188
+ const maxFiles = options.maxFiles ?? DEFAULT_MAX_LOG_FILES;
189
+ const maxTotalBytes = options.maxTotalBytes ?? DEFAULT_MAX_TOTAL_LOG_BYTES;
190
+ const runtimeSegment = sanitizeFileSegment(options.runtimeId);
191
+ const timestampSegment = (/* @__PURE__ */ new Date()).toISOString().replace(/:/gu, "-");
192
+ const filePath = join(options.dir, `${timestampSegment}.${process.pid}.${runtimeSegment}.jsonl`);
193
+ let initialized = false;
194
+ let writeCount = 0;
195
+ const initialize = async () => {
196
+ if (initialized) return;
197
+ await mkdir(options.dir, { recursive: true });
198
+ await cleanupLogDirectory(options.dir, maxFiles, maxTotalBytes);
199
+ initialized = true;
80
200
  };
201
+ return { async write(event) {
202
+ await initialize();
203
+ await appendFile(filePath, `${JSON.stringify(event)}\n`, "utf8");
204
+ writeCount += 1;
205
+ if (writeCount === 1 || writeCount % 100 === 0) await cleanupLogDirectory(options.dir, maxFiles, maxTotalBytes);
206
+ } };
207
+ }
208
+ async function cleanupLogDirectory(directory, maxFiles, maxTotalBytes) {
209
+ let entries = [];
210
+ try {
211
+ const names = await readdir(directory);
212
+ entries = await Promise.all(names.filter((name) => name.endsWith(".jsonl")).map(async (name) => {
213
+ const entryPath = join(directory, name);
214
+ const entryStat = await stat(entryPath);
215
+ return {
216
+ path: entryPath,
217
+ size: entryStat.size,
218
+ time: entryStat.mtimeMs
219
+ };
220
+ }));
221
+ } catch {
222
+ return;
223
+ }
224
+ entries.sort((left, right) => right.time - left.time);
225
+ let totalBytes = 0;
226
+ const retained = [];
227
+ const deleted = [];
228
+ for (const entry of entries) {
229
+ const canKeepByCount = retained.length < maxFiles;
230
+ const canKeepBySize = retained.length === 0 || totalBytes + entry.size <= maxTotalBytes;
231
+ if (canKeepByCount && canKeepBySize) {
232
+ retained.push(entry);
233
+ totalBytes += entry.size;
234
+ continue;
235
+ }
236
+ deleted.push(entry);
237
+ }
238
+ await Promise.allSettled(deleted.map(async (entry) => {
239
+ await unlink(entry.path);
240
+ }));
241
+ }
242
+ function buildStructuredLogEvent(level, input, message, service, boundContext) {
243
+ const extracted = extractContextAndExtra(input);
244
+ const context = {
245
+ ...removeUndefinedFields(boundContext),
246
+ ...removeUndefinedFields(extracted.context)
247
+ };
248
+ const resolvedMessage = normalizeLogMessage(input, message);
249
+ return {
250
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
251
+ level,
252
+ service,
253
+ component: normalizeComponent(context.component),
254
+ event: normalizeEventName(context.event),
255
+ message: redactSensitiveText(resolvedMessage),
256
+ runtimeId: context.runtimeId ?? null,
257
+ operationId: context.operationId ?? null,
258
+ correlationId: resolveCorrelationId(context),
259
+ ...context.worktree ? { worktree: redactSensitiveText(context.worktree) } : {},
260
+ ...typeof context.chatId === "number" ? { chatId: context.chatId } : {},
261
+ ...context.sessionId ? { sessionId: context.sessionId } : {},
262
+ ...context.projectId ? { projectId: context.projectId } : {},
263
+ ...context.requestId ? { requestId: context.requestId } : {},
264
+ ...typeof context.updateId === "number" ? { updateId: context.updateId } : {},
265
+ ...context.command ? { command: context.command } : {},
266
+ ...context.callbackData ? { callbackData: context.callbackData } : {},
267
+ ...typeof context.durationMs === "number" ? { durationMs: context.durationMs } : {},
268
+ ...typeof context.attempt === "number" ? { attempt: context.attempt } : {},
269
+ ...context.status ? { status: context.status } : {},
270
+ ...typeof context.sizeBytes === "number" ? { sizeBytes: context.sizeBytes } : {},
271
+ ...context.error ? { error: context.error } : {},
272
+ ...removeUndefinedFields(extracted.extra)
273
+ };
274
+ }
275
+ function buildHostLogExtra(event) {
276
+ const { service: _service, level: _level, message: _message, ...rest } = event;
277
+ return rest;
278
+ }
279
+ function extractContextAndExtra(input) {
280
+ if (input instanceof Error) return {
281
+ context: { error: serializeError(input) },
282
+ extra: {}
283
+ };
284
+ if (Array.isArray(input)) return {
285
+ context: {},
286
+ extra: { items: input.map((item) => sanitizeValue(item)) }
287
+ };
288
+ if (input === null || input === void 0) return {
289
+ context: {},
290
+ extra: {}
291
+ };
292
+ if (typeof input === "string") return {
293
+ context: {},
294
+ extra: {}
295
+ };
296
+ if (typeof input !== "object") return {
297
+ context: {},
298
+ extra: { value: sanitizeValue(input) }
299
+ };
300
+ const record = input;
301
+ const context = {};
302
+ const extra = {};
303
+ for (const [key, value] of Object.entries(record)) {
304
+ if (RESERVED_EVENT_FIELDS.has(key)) {
305
+ assignReservedField(context, key, value);
306
+ continue;
307
+ }
308
+ extra[key] = sanitizeFieldValue(key, value);
309
+ }
310
+ return {
311
+ context,
312
+ extra
313
+ };
314
+ }
315
+ function assignReservedField(context, key, value) {
316
+ switch (key) {
317
+ case "attempt":
318
+ case "durationMs":
319
+ case "sizeBytes":
320
+ if (typeof value === "number") context[key] = value;
321
+ return;
322
+ case "chatId":
323
+ case "updateId":
324
+ if (typeof value === "number") context[key] = value;
325
+ return;
326
+ case "callbackData":
327
+ case "command":
328
+ case "component":
329
+ case "event":
330
+ case "projectId":
331
+ case "requestId":
332
+ case "sessionId":
333
+ case "status":
334
+ case "worktree":
335
+ if (typeof value === "string" && value.trim().length > 0) context[key] = redactSensitiveText(value.trim());
336
+ return;
337
+ case "correlationId":
338
+ case "operationId":
339
+ case "runtimeId":
340
+ if (typeof value === "string" && value.trim().length > 0) context[key] = value.trim();
341
+ else if (value === null) context[key] = null;
342
+ return;
343
+ case "error":
344
+ if (value instanceof Error) context.error = serializeError(value);
345
+ else if (isPlainObject(value)) context.error = normalizeStructuredLogErrorRecord(value);
346
+ return;
347
+ default: return;
348
+ }
349
+ }
350
+ function normalizeLogMessage(input, message) {
351
+ if (typeof input === "string") {
352
+ const normalizedInput = input.trim();
353
+ return normalizedInput.length > 0 ? normalizedInput : "log";
354
+ }
355
+ if (message && message.trim().length > 0) return message.trim();
356
+ if (input instanceof Error) return input.message.trim() || input.name;
357
+ return "log";
81
358
  }
82
359
  function normalizeServiceName(value) {
83
360
  const normalized = value?.trim();
@@ -91,59 +368,140 @@ function normalizeLogLevel(value) {
91
368
  default: return "info";
92
369
  }
93
370
  }
94
- function normalizeLogArguments(input, message) {
95
- if (typeof input === "string") return { text: input };
96
- if (message && message.trim().length > 0) {
97
- const extra = serializeExtra(input);
98
- return {
99
- ...extra ? { extra } : {},
100
- text: message.trim()
101
- };
102
- }
103
- if (input instanceof Error) return {
104
- extra: serializeExtra(input) ?? void 0,
105
- text: input.message.trim() || input.name
106
- };
107
- const extra = serializeExtra(input);
371
+ function normalizeComponent(value) {
372
+ const normalized = value?.trim();
373
+ return normalized && normalized.length > 0 ? normalized : DEFAULT_COMPONENT;
374
+ }
375
+ function normalizeEventName(value) {
376
+ const normalized = value?.trim();
377
+ return normalized && normalized.length > 0 ? normalized : DEFAULT_EVENT;
378
+ }
379
+ function resolveCorrelationId(context) {
380
+ if (context.correlationId) return context.correlationId;
381
+ if (typeof context.updateId === "number") return String(context.updateId);
382
+ return context.operationId ?? null;
383
+ }
384
+ function serializeError(error) {
108
385
  return {
109
- ...extra ? { extra } : {},
110
- text: "log"
386
+ name: error.name,
387
+ message: redactSensitiveText(error.message),
388
+ ...error.stack ? { stack: redactSensitiveText(error.stack) } : {},
389
+ ..."data" in error && error.data && typeof error.data === "object" ? { data: sanitizeValue(error.data) } : {}
111
390
  };
112
391
  }
113
- function serializeExtra(input) {
114
- if (input === null || input === void 0) return null;
115
- if (input instanceof Error) return { error: serializeError(input) };
116
- if (Array.isArray(input)) return { items: input.map((item) => sanitizeValue(item)) };
117
- if (typeof input !== "object") return { value: sanitizeValue(input) };
118
- return sanitizeRecord(input);
392
+ function sanitizePlainObject(value) {
393
+ return Object.fromEntries(Object.entries(value).map(([key, entryValue]) => [key, sanitizeFieldValue(key, entryValue)]));
119
394
  }
120
- function sanitizeRecord(record) {
121
- return Object.fromEntries(Object.entries(record).map(([key, value]) => [key, isSensitiveKey(key) ? redactSensitiveFieldValue(value) : sanitizeValue(value)]));
395
+ function sanitizeFieldValue(key, value) {
396
+ if (isSensitiveKey(key)) return redactSensitiveFieldValue(value);
397
+ if (isUrlLikeKey(key)) return summarizeUrlValue(value);
398
+ if (isTextContentKey(key)) return summarizeTextValue(value);
399
+ if (isAttachmentCollectionKey(key)) return summarizeAttachmentCollection(value);
400
+ return sanitizeValue(value);
122
401
  }
123
402
  function sanitizeValue(value) {
124
403
  if (value instanceof Error) return serializeError(value);
125
- if (Array.isArray(value)) return value.map((item) => sanitizeValue(item));
404
+ if (Array.isArray(value)) return value.map((entry) => sanitizeValue(entry));
126
405
  if (typeof value === "string") return redactSensitiveText(value);
127
406
  if (!value || typeof value !== "object") return value;
128
- return sanitizeRecord(value);
407
+ return sanitizePlainObject(value);
129
408
  }
130
- function serializeError(error) {
409
+ function summarizeTextValue(value) {
410
+ if (typeof value === "string") return {
411
+ omitted: CONTENT_OMITTED,
412
+ length: value.length
413
+ };
414
+ if (Array.isArray(value)) return {
415
+ omitted: CONTENT_OMITTED,
416
+ count: value.length
417
+ };
418
+ if (value && typeof value === "object") return {
419
+ omitted: CONTENT_OMITTED,
420
+ kind: "object"
421
+ };
422
+ return value;
423
+ }
424
+ function summarizeUrlValue(value) {
425
+ if (typeof value === "string" && value.trim().length > 0) return {
426
+ omitted: CONTENT_OMITTED,
427
+ kind: "url"
428
+ };
429
+ return sanitizeValue(value);
430
+ }
431
+ function summarizeAttachmentCollection(value) {
432
+ if (!Array.isArray(value)) return summarizeTextValue(value);
131
433
  return {
132
- name: error.name,
133
- message: redactSensitiveText(error.message),
134
- ...error.stack ? { stack: redactSensitiveText(error.stack) } : {},
135
- ..."data" in error && error.data && typeof error.data === "object" ? { data: sanitizeValue(error.data) } : {}
434
+ count: value.length,
435
+ items: value.map((entry) => summarizeAttachmentValue(entry))
136
436
  };
137
437
  }
438
+ function summarizeAttachmentValue(value) {
439
+ if (!value || typeof value !== "object" || Array.isArray(value)) return summarizeTextValue(value);
440
+ const record = value;
441
+ return removeUndefinedFields({
442
+ filename: normalizeString(record.filename) ?? normalizeString(record.fileName) ?? void 0,
443
+ mime: normalizeString(record.mime) ?? normalizeString(record.mimeType) ?? void 0,
444
+ sizeBytes: pickNumericValue(record, [
445
+ "sizeBytes",
446
+ "size",
447
+ "fileSize"
448
+ ]),
449
+ hasCaption: pickStringValue(record, ["caption"]) !== null,
450
+ textLength: pickStringValue(record, ["text", "prompt"])?.length,
451
+ type: normalizeString(record.type) ?? void 0
452
+ });
453
+ }
138
454
  function isSensitiveKey(key) {
139
- return /token|secret|api[-_]?key|authorization|password/iu.test(key);
455
+ return /token|secret|api[-_]?key|authorization|password|cookie/iu.test(key);
456
+ }
457
+ function isTextContentKey(key) {
458
+ return /(^|[-_])(text|prompt|caption|body|markdown|content|messageText|fallbackText|input|raw)$/iu.test(key) || /bodyMd|bodyText|messageText|fallbackText/iu.test(key);
459
+ }
460
+ function isUrlLikeKey(key) {
461
+ return /(^|[-_])(url|uri|href|filePath|downloadPath|downloadUrl|fileUrl)$/iu.test(key);
462
+ }
463
+ function isAttachmentCollectionKey(key) {
464
+ return /(^|[-_])(files|parts|attachments)$/iu.test(key);
140
465
  }
141
466
  function redactSensitiveFieldValue(value) {
142
- if (typeof value === "string" && value.trim().length > 0) return "[REDACTED]";
143
- if (Array.isArray(value)) return value.map(() => "[REDACTED]");
144
- if (value && typeof value === "object") return "[REDACTED]";
467
+ if (typeof value === "string" && value.trim().length > 0) return REDACTED;
468
+ if (Array.isArray(value)) return value.map(() => REDACTED);
469
+ if (value && typeof value === "object") return REDACTED;
145
470
  return value;
146
471
  }
472
+ function removeUndefinedFields(record) {
473
+ return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== void 0));
474
+ }
475
+ function isPlainObject(value) {
476
+ return value !== null && typeof value === "object" && !Array.isArray(value);
477
+ }
478
+ function normalizeString(value) {
479
+ return typeof value === "string" && value.trim().length > 0 ? redactSensitiveText(value.trim()) : null;
480
+ }
481
+ function pickNumericValue(record, keys) {
482
+ for (const key of keys) {
483
+ const value = record[key];
484
+ if (typeof value === "number") return value;
485
+ }
486
+ }
487
+ function pickStringValue(record, keys) {
488
+ for (const key of keys) {
489
+ const value = record[key];
490
+ if (typeof value === "string" && value.trim().length > 0) return value.trim();
491
+ }
492
+ return null;
493
+ }
494
+ function sanitizeFileSegment(value) {
495
+ return value.replace(/[^a-z0-9._-]+/giu, "_");
496
+ }
497
+ function normalizeStructuredLogErrorRecord(value) {
498
+ return {
499
+ name: normalizeString(value.name) ?? "Error",
500
+ message: normalizeString(value.message) ?? "Unknown error",
501
+ ...typeof value.stack === "string" ? { stack: redactSensitiveText(value.stack) } : {},
502
+ ...value.data !== void 0 ? { data: sanitizeValue(value.data) } : {}
503
+ };
504
+ }
147
505
  //#endregion
148
506
  //#region src/repositories/pending-action.repo.ts
149
507
  var FilePendingActionRepository = class {
@@ -1979,10 +2337,11 @@ var SendPromptUseCase = class {
1979
2337
  binding
1980
2338
  });
1981
2339
  binding = createdSession.binding;
1982
- this.logger.info({
2340
+ logPromptLifecycle(this.logger, {
1983
2341
  chatId: input.chatId,
1984
- sessionId: createdSession.session.id,
2342
+ event: "prompt.session.created",
1985
2343
  projectId: createdSession.session.projectID,
2344
+ sessionId: createdSession.session.id,
1986
2345
  directory: createdSession.session.directory
1987
2346
  }, "session created");
1988
2347
  }
@@ -1990,11 +2349,15 @@ var SendPromptUseCase = class {
1990
2349
  const selectedModel = (await this.opencodeClient.listModels()).find((model) => model.providerID === binding?.modelProviderId && model.id === binding?.modelId);
1991
2350
  if (!selectedModel) {
1992
2351
  binding = await clearStoredModelSelection(this.sessionRepo, binding);
1993
- this.logger.warn?.({ chatId: input.chatId }, "selected model is no longer available, falling back to OpenCode default");
2352
+ this.logger.warn?.({
2353
+ chatId: input.chatId,
2354
+ event: "prompt.model.unavailable"
2355
+ }, "selected model is no longer available, falling back to OpenCode default");
1994
2356
  } else if (binding.modelVariant && !(binding.modelVariant in selectedModel.variants)) {
1995
2357
  binding = await clearStoredModelVariant(this.sessionRepo, binding);
1996
2358
  this.logger.warn?.({
1997
2359
  chatId: input.chatId,
2360
+ event: "prompt.model.variant_unavailable",
1998
2361
  providerId: selectedModel.providerID,
1999
2362
  modelId: selectedModel.id
2000
2363
  }, "selected model variant is no longer available, falling back to default variant");
@@ -2010,13 +2373,24 @@ var SendPromptUseCase = class {
2010
2373
  const selectedAgent = resolveSelectedAgent(await this.opencodeClient.listAgents(), activeBinding.agentName);
2011
2374
  if (activeBinding.agentName && selectedAgent?.name !== activeBinding.agentName) {
2012
2375
  activeBinding = await clearStoredAgentSelection(this.sessionRepo, activeBinding);
2013
- this.logger.warn?.({ chatId: input.chatId }, "selected agent is no longer available, falling back to OpenCode default");
2376
+ this.logger.warn?.({
2377
+ chatId: input.chatId,
2378
+ event: "prompt.agent.unavailable"
2379
+ }, "selected agent is no longer available, falling back to OpenCode default");
2014
2380
  }
2015
2381
  const temporarySessionId = shouldIsolateImageTurn ? await this.createTemporaryImageSession(input.chatId, activeBinding.sessionId) : null;
2016
2382
  const executionSessionId = temporarySessionId ?? activeBinding.sessionId;
2017
2383
  input.onExecutionSession?.(executionSessionId);
2018
2384
  let result;
2019
2385
  try {
2386
+ logOpenCodeRequest(this.logger, {
2387
+ chatId: input.chatId,
2388
+ event: "opencode.prompt.submit",
2389
+ projectId: activeBinding.projectId,
2390
+ sessionId: executionSessionId,
2391
+ fileCount: files.length,
2392
+ status: "started"
2393
+ }, "submitting OpenCode prompt");
2020
2394
  result = await this.opencodeClient.promptSession({
2021
2395
  sessionId: executionSessionId,
2022
2396
  prompt: promptText,
@@ -2027,6 +2401,13 @@ var SendPromptUseCase = class {
2027
2401
  ...input.signal ? { signal: input.signal } : {},
2028
2402
  ...activeBinding.modelVariant ? { variant: activeBinding.modelVariant } : {}
2029
2403
  });
2404
+ logPromptLifecycle(this.logger, {
2405
+ chatId: input.chatId,
2406
+ event: "prompt.completed",
2407
+ projectId: activeBinding.projectId,
2408
+ sessionId: executionSessionId,
2409
+ status: "completed"
2410
+ }, "prompt completed");
2030
2411
  } finally {
2031
2412
  if (temporarySessionId) await this.cleanupTemporaryImageSession(input.chatId, activeBinding.sessionId, temporarySessionId);
2032
2413
  }
@@ -2039,14 +2420,18 @@ var SendPromptUseCase = class {
2039
2420
  }
2040
2421
  async clearInvalidSessionContext(chatId, binding, reason) {
2041
2422
  const nextBinding = await clearStoredSessionContext(this.sessionRepo, binding);
2042
- this.logger.warn?.({ chatId }, `${reason}, falling back to the current OpenCode project`);
2423
+ this.logger.warn?.({
2424
+ chatId,
2425
+ event: "prompt.session.invalid_context"
2426
+ }, `${reason}, falling back to the current OpenCode project`);
2043
2427
  return nextBinding;
2044
2428
  }
2045
2429
  async createTemporaryImageSession(chatId, sessionId) {
2046
2430
  const temporarySession = await this.opencodeClient.forkSession(sessionId);
2047
2431
  if (!temporarySession.id || temporarySession.id === sessionId) throw new Error("OpenCode did not return a distinct temporary session for the image turn.");
2048
- this.logger.info?.({
2432
+ logPromptLifecycle(this.logger, {
2049
2433
  chatId,
2434
+ event: "prompt.temporary_session.created",
2050
2435
  parentSessionId: sessionId,
2051
2436
  sessionId: temporarySession.id
2052
2437
  }, "created temporary image session");
@@ -2056,6 +2441,7 @@ var SendPromptUseCase = class {
2056
2441
  try {
2057
2442
  if (!await this.opencodeClient.deleteSession(sessionId)) this.logger.warn?.({
2058
2443
  chatId,
2444
+ event: "prompt.temporary_session.cleanup_failed",
2059
2445
  parentSessionId,
2060
2446
  sessionId
2061
2447
  }, "failed to delete temporary image session");
@@ -2063,6 +2449,7 @@ var SendPromptUseCase = class {
2063
2449
  this.logger.warn?.({
2064
2450
  error,
2065
2451
  chatId,
2452
+ event: "prompt.temporary_session.cleanup_failed",
2066
2453
  parentSessionId,
2067
2454
  sessionId
2068
2455
  }, "failed to delete temporary image session");
@@ -2263,7 +2650,23 @@ function resolveExtension(mimeType) {
2263
2650
  //#endregion
2264
2651
  //#region src/app/container.ts
2265
2652
  function createAppContainer(config, client) {
2266
- const logger = createOpenCodeAppLogger(client, { level: config.logLevel });
2653
+ const runtimeId = randomUUID();
2654
+ const logger = createOpenCodeAppLogger(client, {
2655
+ file: {
2656
+ dir: config.loggingFileDir,
2657
+ retention: {
2658
+ maxFiles: config.loggingRetentionMaxFiles,
2659
+ maxTotalBytes: config.loggingRetentionMaxTotalBytes
2660
+ }
2661
+ },
2662
+ level: config.loggingLevel,
2663
+ runtimeId,
2664
+ sinks: {
2665
+ file: config.loggingFileSinkEnabled,
2666
+ host: config.loggingHostSinkEnabled
2667
+ },
2668
+ worktree: config.worktreePath
2669
+ });
2267
2670
  return createContainer(config, createOpenCodeClientFromSdkClient(client, fetch, {
2268
2671
  waitTimeoutMs: config.promptWaitTimeoutMs,
2269
2672
  pollRequestTimeoutMs: config.promptPollRequestTimeoutMs,
@@ -2271,6 +2674,9 @@ function createAppContainer(config, client) {
2271
2674
  }), logger);
2272
2675
  }
2273
2676
  function createContainer(config, opencodeClient, logger) {
2677
+ const storageLogger = logger.child({ component: "storage" });
2678
+ const opencodeLogger = logger.child({ component: "opencode" });
2679
+ const promptLogger = logger.child({ component: "prompt" });
2274
2680
  const stateStore = new JsonStateStore({
2275
2681
  filePath: config.stateFilePath,
2276
2682
  createDefaultState: createDefaultOpencodeTbotState
@@ -2285,7 +2691,7 @@ function createContainer(config, opencodeClient, logger) {
2285
2691
  });
2286
2692
  const uploadFileUseCase = new UploadFileUseCase(telegramFileClient);
2287
2693
  const abortPromptUseCase = new AbortPromptUseCase(sessionRepo, opencodeClient, foregroundSessionTracker);
2288
- const createSessionUseCase = new CreateSessionUseCase(sessionRepo, opencodeClient, logger);
2694
+ const createSessionUseCase = new CreateSessionUseCase(sessionRepo, opencodeClient, opencodeLogger);
2289
2695
  const getHealthUseCase = new GetHealthUseCase(opencodeClient);
2290
2696
  const getPathUseCase = new GetPathUseCase(opencodeClient);
2291
2697
  const listAgentsUseCase = new ListAgentsUseCase(sessionRepo, opencodeClient);
@@ -2294,11 +2700,11 @@ function createContainer(config, opencodeClient, logger) {
2294
2700
  const listSessionsUseCase = new ListSessionsUseCase(sessionRepo, opencodeClient);
2295
2701
  const getStatusUseCase = new GetStatusUseCase(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo);
2296
2702
  const listModelsUseCase = new ListModelsUseCase(sessionRepo, opencodeClient);
2297
- const renameSessionUseCase = new RenameSessionUseCase(sessionRepo, opencodeClient, logger);
2298
- const sendPromptUseCase = new SendPromptUseCase(sessionRepo, opencodeClient, logger);
2299
- const switchAgentUseCase = new SwitchAgentUseCase(sessionRepo, opencodeClient, logger);
2300
- const switchModelUseCase = new SwitchModelUseCase(sessionRepo, opencodeClient, logger);
2301
- const switchSessionUseCase = new SwitchSessionUseCase(sessionRepo, opencodeClient, logger);
2703
+ const renameSessionUseCase = new RenameSessionUseCase(sessionRepo, opencodeClient, opencodeLogger);
2704
+ const sendPromptUseCase = new SendPromptUseCase(sessionRepo, opencodeClient, promptLogger);
2705
+ const switchAgentUseCase = new SwitchAgentUseCase(sessionRepo, opencodeClient, opencodeLogger);
2706
+ const switchModelUseCase = new SwitchModelUseCase(sessionRepo, opencodeClient, opencodeLogger);
2707
+ const switchSessionUseCase = new SwitchSessionUseCase(sessionRepo, opencodeClient, opencodeLogger);
2302
2708
  let disposed = false;
2303
2709
  return {
2304
2710
  abortPromptUseCase,
@@ -2327,7 +2733,10 @@ function createContainer(config, opencodeClient, logger) {
2327
2733
  async dispose() {
2328
2734
  if (disposed) return;
2329
2735
  disposed = true;
2330
- logger.info({ filePath: config.stateFilePath }, "disposing telegram bot container");
2736
+ storageLogger.info({
2737
+ event: "storage.container.disposed",
2738
+ filePath: config.stateFilePath
2739
+ }, "disposing telegram bot container");
2331
2740
  await logger.flush();
2332
2741
  }
2333
2742
  };
@@ -2431,6 +2840,11 @@ async function handleTelegramBotPluginEvent(runtime, event) {
2431
2840
  }
2432
2841
  }
2433
2842
  async function handlePermissionAsked(runtime, request) {
2843
+ const logger = runtime.container.logger.child({
2844
+ component: "plugin-event",
2845
+ requestId: request.id,
2846
+ sessionId: request.sessionID
2847
+ });
2434
2848
  const bindings = await runtime.container.sessionRepo.listBySessionId(request.sessionID);
2435
2849
  const chatIds = new Set([...bindings.map((binding) => binding.chatId), ...runtime.container.foregroundSessionTracker.listChatIds(request.sessionID)]);
2436
2850
  const approvals = await runtime.container.permissionApprovalRepo.listByRequestId(request.id);
@@ -2451,35 +2865,45 @@ async function handlePermissionAsked(runtime, request) {
2451
2865
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2452
2866
  });
2453
2867
  } catch (error) {
2454
- runtime.container.logger.error({
2868
+ logger.error({
2455
2869
  error,
2456
2870
  chatId,
2871
+ event: "plugin-event.permission.ask.delivery_failed",
2457
2872
  requestId: request.id
2458
2873
  }, "failed to deliver permission request to Telegram");
2459
2874
  }
2460
2875
  }
2461
2876
  }
2462
2877
  async function handlePermissionReplied(runtime, event) {
2878
+ const logger = runtime.container.logger.child({
2879
+ component: "plugin-event",
2880
+ event: "plugin-event.permission.replied",
2881
+ requestId: event.requestId,
2882
+ sessionId: event.sessionId
2883
+ });
2463
2884
  const approvals = await runtime.container.permissionApprovalRepo.listByRequestId(event.requestId);
2464
2885
  await Promise.all(approvals.map(async (approval) => {
2465
2886
  try {
2466
2887
  await runtime.bot.api.editMessageText(approval.chatId, approval.messageId, buildPermissionApprovalResolvedMessage(event.requestId, event.reply));
2467
2888
  } catch (error) {
2468
- runtime.container.logger.warn({
2889
+ logger.warn({
2469
2890
  error,
2470
2891
  chatId: approval.chatId,
2471
- requestId: event.requestId,
2472
- sessionId: event.sessionId
2892
+ event: "plugin-event.permission.reply_message_failed"
2473
2893
  }, "failed to update Telegram permission message");
2474
2894
  }
2475
2895
  await runtime.container.permissionApprovalRepo.set(toResolvedApproval(approval, event.reply));
2476
2896
  }));
2477
2897
  }
2478
2898
  async function handleSessionError(runtime, event) {
2899
+ const logger = runtime.container.logger.child({
2900
+ component: "plugin-event",
2901
+ sessionId: event.sessionId
2902
+ });
2479
2903
  if (runtime.container.foregroundSessionTracker.fail(event.sessionId, event.error instanceof Error ? event.error : /* @__PURE__ */ new Error("Unknown session error."))) {
2480
- runtime.container.logger.warn({
2904
+ logger.warn({
2481
2905
  error: event.error,
2482
- sessionId: event.sessionId
2906
+ event: "plugin-event.session.error.foreground_suppressed"
2483
2907
  }, "session error suppressed for foreground Telegram session");
2484
2908
  return;
2485
2909
  }
@@ -2487,8 +2911,12 @@ async function handleSessionError(runtime, event) {
2487
2911
  await notifyBoundChats(runtime, event.sessionId, `Session failed.\n\nSession: ${event.sessionId}\nError: ${message}`);
2488
2912
  }
2489
2913
  async function handleSessionIdle(runtime, event) {
2914
+ const logger = runtime.container.logger.child({
2915
+ component: "plugin-event",
2916
+ sessionId: event.sessionId
2917
+ });
2490
2918
  if (runtime.container.foregroundSessionTracker.clear(event.sessionId)) {
2491
- runtime.container.logger.info({ sessionId: event.sessionId }, "session idle notification suppressed for foreground Telegram session");
2919
+ logPluginEvent(logger, { event: "plugin-event.session.idle.foreground_suppressed" }, "session idle notification suppressed for foreground Telegram session");
2492
2920
  return;
2493
2921
  }
2494
2922
  await notifyBoundChats(runtime, event.sessionId, `Session finished.\n\nSession: ${event.sessionId}`);
@@ -2498,16 +2926,20 @@ async function handleSessionStatus(runtime, event) {
2498
2926
  await handleSessionIdle(runtime, event);
2499
2927
  }
2500
2928
  async function notifyBoundChats(runtime, sessionId, text) {
2929
+ const logger = runtime.container.logger.child({
2930
+ component: "plugin-event",
2931
+ sessionId
2932
+ });
2501
2933
  const bindings = await runtime.container.sessionRepo.listBySessionId(sessionId);
2502
2934
  const chatIds = [...new Set(bindings.map((binding) => binding.chatId))];
2503
2935
  await Promise.all(chatIds.map(async (chatId) => {
2504
2936
  try {
2505
2937
  await runtime.bot.api.sendMessage(chatId, text);
2506
2938
  } catch (error) {
2507
- runtime.container.logger.warn({
2939
+ logger.warn({
2508
2940
  error,
2509
2941
  chatId,
2510
- sessionId
2942
+ event: "plugin-event.session.notify_failed"
2511
2943
  }, "failed to notify Telegram chat about session event");
2512
2944
  }
2513
2945
  }));
@@ -3284,7 +3716,9 @@ var TELEGRAM_COMMAND_SYNC_SCOPES = [{ type: "default" }, { type: "all_private_ch
3284
3716
  async function syncTelegramCommands(bot, logger) {
3285
3717
  await Promise.all(TELEGRAM_COMMAND_SYNC_SCOPES.map((scope) => bot.api.setMyCommands(TELEGRAM_COMMANDS, { scope })));
3286
3718
  logger.info({
3719
+ component: "runtime",
3287
3720
  commands: TELEGRAM_COMMANDS.map((command) => command.command),
3721
+ event: "runtime.commands.synced",
3288
3722
  scopes: TELEGRAM_COMMAND_SYNC_SCOPES.map((scope) => scope.type)
3289
3723
  }, "telegram commands synced");
3290
3724
  }
@@ -3295,6 +3729,44 @@ async function syncTelegramCommandsForChat(api, chatId, language) {
3295
3729
  } });
3296
3730
  }
3297
3731
  //#endregion
3732
+ //#region src/bot/logger-context.ts
3733
+ function buildTelegramLoggerContext(ctx, component = "telegram") {
3734
+ const updateId = typeof ctx.update?.update_id === "number" ? ctx.update.update_id : void 0;
3735
+ const command = extractTelegramCommand(resolveMessageText(ctx));
3736
+ const callbackData = normalizeTelegramString(ctx.callbackQuery?.data);
3737
+ const operationId = typeof updateId === "number" ? `telegram-${updateId}` : null;
3738
+ return {
3739
+ component,
3740
+ ...typeof ctx.chat?.id === "number" ? { chatId: ctx.chat.id } : {},
3741
+ ...typeof updateId === "number" ? { updateId } : {},
3742
+ ...command ? { command } : {},
3743
+ ...callbackData ? { callbackData } : {},
3744
+ correlationId: typeof updateId === "number" ? String(updateId) : operationId,
3745
+ operationId
3746
+ };
3747
+ }
3748
+ function scopeLoggerToTelegramContext(logger, ctx, component = "telegram") {
3749
+ return logger.child(buildTelegramLoggerContext(ctx, component));
3750
+ }
3751
+ function scopeDependenciesToTelegramContext(dependencies, ctx, component = "telegram") {
3752
+ return {
3753
+ ...dependencies,
3754
+ logger: scopeLoggerToTelegramContext(dependencies.logger, ctx, component)
3755
+ };
3756
+ }
3757
+ function resolveMessageText(ctx) {
3758
+ return normalizeTelegramString(ctx.message?.text) ?? normalizeTelegramString(ctx.msg?.text);
3759
+ }
3760
+ function extractTelegramCommand(value) {
3761
+ if (!value || !value.startsWith("/")) return null;
3762
+ const token = value.split(/\s+/u, 1)[0]?.trim();
3763
+ if (!token) return null;
3764
+ return token.replace(/^\/+/u, "").split("@", 1)[0] ?? null;
3765
+ }
3766
+ function normalizeTelegramString(value) {
3767
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
3768
+ }
3769
+ //#endregion
3298
3770
  //#region src/bot/presenters/error.presenter.ts
3299
3771
  function presentError(error, copy = BOT_COPY) {
3300
3772
  const presented = normalizeError(error, copy);
@@ -3411,16 +3883,12 @@ function stringifyUnknown(value) {
3411
3883
  //#endregion
3412
3884
  //#region src/bot/error-boundary.ts
3413
3885
  function extractTelegramUpdateContext(ctx) {
3414
- const updateId = getNestedNumber(ctx, ["update", "update_id"]);
3415
- const chatId = getNestedNumber(ctx, ["chat", "id"]);
3416
- const messageText = getNestedString(ctx, ["message", "text"]);
3417
- const callbackData = getNestedString(ctx, ["callbackQuery", "data"]);
3418
- return {
3419
- ...typeof updateId === "number" ? { updateId } : {},
3420
- ...typeof chatId === "number" ? { chatId } : {},
3421
- ...typeof messageText === "string" && messageText.trim().length > 0 ? { messageText } : {},
3422
- ...typeof callbackData === "string" && callbackData.trim().length > 0 ? { callbackData } : {}
3423
- };
3886
+ return buildTelegramLoggerContext({
3887
+ callbackQuery: { data: getNestedString(ctx, ["callbackQuery", "data"]) },
3888
+ chat: { id: getNestedNumber(ctx, ["chat", "id"]) ?? void 0 },
3889
+ message: { text: getNestedString(ctx, ["message", "text"]) },
3890
+ update: { update_id: getNestedNumber(ctx, ["update", "update_id"]) ?? void 0 }
3891
+ });
3424
3892
  }
3425
3893
  async function replyWithDefaultTelegramError(ctx, logger, error) {
3426
3894
  const text = presentError(error, BOT_COPY);
@@ -3985,7 +4453,7 @@ async function handleAgentsCommand(ctx, dependencies) {
3985
4453
  }
3986
4454
  function registerAgentsCommand(bot, dependencies) {
3987
4455
  bot.command(["agents", "agent"], async (ctx) => {
3988
- await handleAgentsCommand(ctx, dependencies);
4456
+ await handleAgentsCommand(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
3989
4457
  });
3990
4458
  }
3991
4459
  //#endregion
@@ -4128,7 +4596,7 @@ async function handleCancelCommand(ctx, dependencies) {
4128
4596
  }
4129
4597
  function registerCancelCommand(bot, dependencies) {
4130
4598
  bot.command("cancel", async (ctx) => {
4131
- await handleCancelCommand(ctx, dependencies);
4599
+ await handleCancelCommand(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
4132
4600
  });
4133
4601
  }
4134
4602
  //#endregion
@@ -4175,7 +4643,7 @@ async function presentLanguageSwitchForChat(chatId, api, language, dependencies)
4175
4643
  }
4176
4644
  function registerLanguageCommand(bot, dependencies) {
4177
4645
  bot.command("language", async (ctx) => {
4178
- await handleLanguageCommand(ctx, dependencies);
4646
+ await handleLanguageCommand(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
4179
4647
  });
4180
4648
  }
4181
4649
  //#endregion
@@ -4203,7 +4671,7 @@ async function handleModelsCommand(ctx, dependencies) {
4203
4671
  }
4204
4672
  function registerModelsCommand(bot, dependencies) {
4205
4673
  bot.command(["model", "models"], async (ctx) => {
4206
- await handleModelsCommand(ctx, dependencies);
4674
+ await handleModelsCommand(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
4207
4675
  });
4208
4676
  }
4209
4677
  //#endregion
@@ -4224,7 +4692,7 @@ async function handleNewCommand(ctx, dependencies) {
4224
4692
  }
4225
4693
  function registerNewCommand(bot, dependencies) {
4226
4694
  bot.command("new", async (ctx) => {
4227
- await handleNewCommand(ctx, dependencies);
4695
+ await handleNewCommand(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
4228
4696
  });
4229
4697
  }
4230
4698
  function extractSessionTitle(ctx) {
@@ -4589,7 +5057,7 @@ async function handleStatusCommand(ctx, dependencies) {
4589
5057
  }
4590
5058
  function registerStatusCommand(bot, dependencies) {
4591
5059
  bot.command("status", async (ctx) => {
4592
- await handleStatusCommand(ctx, dependencies);
5060
+ await handleStatusCommand(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
4593
5061
  });
4594
5062
  }
4595
5063
  //#endregion
@@ -4607,7 +5075,7 @@ async function handleSessionsCommand(ctx, dependencies) {
4607
5075
  }
4608
5076
  function registerSessionsCommand(bot, dependencies) {
4609
5077
  bot.command("sessions", async (ctx) => {
4610
- await handleSessionsCommand(ctx, dependencies);
5078
+ await handleSessionsCommand(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
4611
5079
  });
4612
5080
  }
4613
5081
  //#endregion
@@ -4634,7 +5102,7 @@ async function handleStartCommand(ctx, dependencies) {
4634
5102
  }
4635
5103
  function registerStartCommand(bot, dependencies) {
4636
5104
  bot.command("start", async (ctx) => {
4637
- await handleStartCommand(ctx, dependencies);
5105
+ await handleStartCommand(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
4638
5106
  });
4639
5107
  }
4640
5108
  //#endregion
@@ -4900,19 +5368,19 @@ async function handlePermissionApprovalCallback(ctx, dependencies) {
4900
5368
  }
4901
5369
  function registerCallbackHandler(bot, dependencies) {
4902
5370
  bot.callbackQuery(/^agents:/, async (ctx) => {
4903
- await handleAgentsCallback(ctx, dependencies);
5371
+ await handleAgentsCallback(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
4904
5372
  });
4905
5373
  bot.callbackQuery(/^sessions:/, async (ctx) => {
4906
- await handleSessionsCallback(ctx, dependencies);
5374
+ await handleSessionsCallback(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
4907
5375
  });
4908
5376
  bot.callbackQuery(/^model:/, async (ctx) => {
4909
- await handleModelsCallback(ctx, dependencies);
5377
+ await handleModelsCallback(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
4910
5378
  });
4911
5379
  bot.callbackQuery(/^language:/, async (ctx) => {
4912
- await handleLanguageCallback(ctx, dependencies);
5380
+ await handleLanguageCallback(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
4913
5381
  });
4914
5382
  bot.callbackQuery(/^permission:/, async (ctx) => {
4915
- await handlePermissionApprovalCallback(ctx, dependencies);
5383
+ await handlePermissionApprovalCallback(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
4916
5384
  });
4917
5385
  }
4918
5386
  function parseSessionActionTarget(data, prefix) {
@@ -5014,10 +5482,10 @@ async function handleImageMessage(ctx, dependencies) {
5014
5482
  }
5015
5483
  function registerFileHandler(bot, dependencies) {
5016
5484
  bot.on("message:photo", async (ctx) => {
5017
- await handleImageMessage(ctx, dependencies);
5485
+ await handleImageMessage(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
5018
5486
  });
5019
5487
  bot.on("message:document", async (ctx) => {
5020
- await handleImageMessage(ctx, dependencies);
5488
+ await handleImageMessage(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
5021
5489
  });
5022
5490
  }
5023
5491
  function resolveTelegramImage(message) {
@@ -5059,7 +5527,7 @@ async function handleTextMessage(ctx, dependencies) {
5059
5527
  }
5060
5528
  function registerMessageHandler(bot, dependencies) {
5061
5529
  bot.on("message:text", async (ctx) => {
5062
- await handleTextMessage(ctx, dependencies);
5530
+ await handleTextMessage(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
5063
5531
  });
5064
5532
  }
5065
5533
  //#endregion
@@ -5072,7 +5540,7 @@ async function handleVoiceMessage(ctx, dependencies) {
5072
5540
  }
5073
5541
  function registerVoiceHandler(bot, dependencies) {
5074
5542
  bot.on("message:voice", async (ctx) => {
5075
- await handleVoiceMessage(ctx, dependencies);
5543
+ await handleVoiceMessage(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
5076
5544
  });
5077
5545
  }
5078
5546
  //#endregion
@@ -5093,17 +5561,18 @@ function createAuthMiddleware(allowedChatIds) {
5093
5561
  function buildIncomingUpdateLogFields(ctx) {
5094
5562
  const messageText = ctx.msg && "text" in ctx.msg ? ctx.msg.text : void 0;
5095
5563
  return {
5564
+ ...buildTelegramLoggerContext(ctx),
5565
+ event: "telegram.update.received",
5096
5566
  updateId: ctx.update.update_id,
5097
5567
  chatId: ctx.chat?.id,
5098
5568
  fromId: ctx.from?.id,
5099
5569
  hasText: typeof messageText === "string" && messageText.length > 0,
5100
- textLength: typeof messageText === "string" ? messageText.length : 0,
5101
- textPreview: typeof messageText === "string" && messageText.length > 0 ? createRedactedPreview(messageText) : void 0
5570
+ textLength: typeof messageText === "string" ? messageText.length : 0
5102
5571
  };
5103
5572
  }
5104
5573
  function createLoggingMiddleware(logger) {
5105
5574
  return async (ctx, next) => {
5106
- logger.info(buildIncomingUpdateLogFields(ctx), "incoming update");
5575
+ logTelegramUpdate(logger, { ...buildIncomingUpdateLogFields(ctx) }, "incoming update");
5107
5576
  return next();
5108
5577
  };
5109
5578
  }
@@ -5113,11 +5582,13 @@ function registerBot(bot, container, options) {
5113
5582
  bot.use(createLoggingMiddleware(container.logger));
5114
5583
  bot.use(createAuthMiddleware(options.telegramAllowedChatIds));
5115
5584
  const safeBot = bot.errorBoundary(async (error) => {
5116
- container.logger.error({
5585
+ const scopedLogger = scopeLoggerToTelegramContext(container.logger, error.ctx, "telegram");
5586
+ scopedLogger.error({
5117
5587
  ...extractTelegramUpdateContext(error.ctx),
5588
+ event: "telegram.middleware.failed",
5118
5589
  error: error.error
5119
5590
  }, "telegram middleware failed");
5120
- await replyWithDefaultTelegramError(error.ctx, container.logger, error.error);
5591
+ await replyWithDefaultTelegramError(error.ctx, scopedLogger, error.error);
5121
5592
  });
5122
5593
  registerStartCommand(safeBot, container);
5123
5594
  registerStatusCommand(safeBot, container);
@@ -5144,8 +5615,10 @@ async function startTelegramBotRuntime(input) {
5144
5615
  const runtimeKey = buildTelegramRuntimeKey(input.config);
5145
5616
  const registry = getTelegramBotRuntimeRegistry();
5146
5617
  const existingRuntime = registry.activeByKey.get(runtimeKey);
5618
+ const runtimeLogger = input.container.logger.child({ component: "runtime" });
5147
5619
  if (existingRuntime) {
5148
- input.container.logger.warn({
5620
+ runtimeLogger.warn({
5621
+ event: "runtime.reused",
5149
5622
  runtimeKey,
5150
5623
  telegramApiRoot: input.config.telegramApiRoot
5151
5624
  }, "telegram runtime already active in this process; reusing the existing runner");
@@ -5163,13 +5636,18 @@ async function startTelegramBotRuntime(input) {
5163
5636
  }
5164
5637
  async function startTelegramBotRuntimeInternal(input, runtimeKey, releaseRuntime) {
5165
5638
  const bot = (input.botFactory ?? ((token, options) => new Bot(token, options)))(input.config.telegramBotToken, { client: { apiRoot: input.config.telegramApiRoot } });
5639
+ const runtimeLogger = input.container.logger.child({ component: "runtime" });
5166
5640
  wrapTelegramGetUpdates(bot, input.container);
5167
5641
  (input.registerBotHandlers ?? registerBot)(bot, input.container, { telegramAllowedChatIds: input.config.telegramAllowedChatIds });
5168
5642
  bot.catch((error) => {
5169
5643
  const metadata = extractTelegramUpdateContext(error.ctx);
5644
+ const telegramLogger = input.container.logger.child({
5645
+ component: "telegram",
5646
+ ...metadata
5647
+ });
5170
5648
  if (error.error instanceof GrammyError) {
5171
- input.container.logger.error({
5172
- ...metadata,
5649
+ telegramLogger.error({
5650
+ event: "telegram.api.error",
5173
5651
  errorCode: error.error.error_code,
5174
5652
  description: error.error.description,
5175
5653
  method: error.error.method,
@@ -5179,24 +5657,28 @@ async function startTelegramBotRuntimeInternal(input, runtimeKey, releaseRuntime
5179
5657
  return;
5180
5658
  }
5181
5659
  if (error.error instanceof HttpError) {
5182
- input.container.logger.error({
5183
- ...metadata,
5660
+ telegramLogger.error({
5661
+ event: "telegram.http.error",
5184
5662
  error: error.error.error,
5185
5663
  message: error.error.message
5186
5664
  }, "telegram bot network request failed");
5187
5665
  return;
5188
5666
  }
5189
- input.container.logger.error({
5190
- ...metadata,
5667
+ telegramLogger.error({
5668
+ event: "telegram.update.failed",
5191
5669
  error: error.error
5192
5670
  }, "telegram bot update failed");
5193
5671
  });
5194
- input.container.logger.info({ runtimeKey }, "telegram bot polling starting");
5672
+ runtimeLogger.info({
5673
+ event: "runtime.polling.starting",
5674
+ runtimeKey
5675
+ }, "telegram bot polling starting");
5195
5676
  const runner = (input.runBot ?? run)(bot, TELEGRAM_RUNNER_OPTIONS);
5196
5677
  let stopped = false;
5197
5678
  let disposed = false;
5198
5679
  if (input.syncCommands ?? true) (input.syncCommandsHandler ?? syncTelegramCommands)(bot, input.container.logger).catch((error) => {
5199
- input.container.logger.warn({
5680
+ runtimeLogger.warn({
5681
+ event: "runtime.commands.sync_failed",
5200
5682
  error,
5201
5683
  runtimeKey
5202
5684
  }, "failed to sync telegram commands; polling continues without command registration updates");
@@ -5206,7 +5688,8 @@ async function startTelegramBotRuntimeInternal(input, runtimeKey, releaseRuntime
5206
5688
  if (stopped) return;
5207
5689
  stopped = true;
5208
5690
  stopPromise = runner.stop().catch((error) => {
5209
- input.container.logger.warn({
5691
+ runtimeLogger.warn({
5692
+ event: "runtime.stop.failed",
5210
5693
  error,
5211
5694
  runtimeKey
5212
5695
  }, "failed to stop telegram runner cleanly");
@@ -5235,6 +5718,7 @@ async function startTelegramBotRuntimeInternal(input, runtimeKey, releaseRuntime
5235
5718
  }
5236
5719
  function wrapTelegramGetUpdates(bot, container) {
5237
5720
  const originalGetUpdates = bot.api.getUpdates.bind(bot.api);
5721
+ const runtimeLogger = container.logger.child({ component: "runtime" });
5238
5722
  bot.api.getUpdates = async (options, signal) => {
5239
5723
  const requestOptions = options ?? {
5240
5724
  limit: 100,
@@ -5244,7 +5728,8 @@ function wrapTelegramGetUpdates(bot, container) {
5244
5728
  try {
5245
5729
  return await originalGetUpdates(requestOptions, signal);
5246
5730
  } catch (error) {
5247
- container.logger.warn({
5731
+ runtimeLogger.warn({
5732
+ event: "runtime.telegram.get_updates_failed",
5248
5733
  error,
5249
5734
  limit: requestOptions.limit,
5250
5735
  offset: requestOptions.offset,
@@ -5308,8 +5793,9 @@ async function startPluginRuntime(options, cwd) {
5308
5793
  });
5309
5794
  const { config, container } = bootstrapApp(options.context.client, preparedConfiguration.config, { cwd: preparedConfiguration.cwd });
5310
5795
  try {
5311
- if (preparedConfiguration.ignoredProjectConfigFilePath) container.logger.warn({
5796
+ if (preparedConfiguration.ignoredProjectConfigFilePath) container.logger.child({ component: "runtime" }).warn({
5312
5797
  cwd: preparedConfiguration.cwd,
5798
+ event: "runtime.config.legacy_worktree_ignored",
5313
5799
  ignoredProjectConfigFilePath: preparedConfiguration.ignoredProjectConfigFilePath,
5314
5800
  globalConfigFilePath: preparedConfiguration.globalConfigFilePath
5315
5801
  }, "legacy worktree plugin config is ignored; migrate settings to the global opencode-tbot config");
@@ -5317,8 +5803,9 @@ async function startPluginRuntime(options, cwd) {
5317
5803
  config,
5318
5804
  container
5319
5805
  });
5320
- container.logger.info({
5806
+ container.logger.child({ component: "runtime" }).info({
5321
5807
  cwd: preparedConfiguration.cwd,
5808
+ event: "runtime.plugin.started",
5322
5809
  globalConfigFilePath: preparedConfiguration.globalConfigFilePath,
5323
5810
  ignoredProjectConfigFilePath: preparedConfiguration.ignoredProjectConfigFilePath,
5324
5811
  configFilePath: preparedConfiguration.configFilePath,