opencode-tbot 0.1.30 → 0.1.32

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,359 @@
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
- import { z } from "zod";
6
- import { createOpencodeClient } from "@opencode-ai/sdk";
7
5
  import { randomUUID } from "node:crypto";
6
+ import { createOpencodeClient } from "@opencode-ai/sdk";
8
7
  import { run } from "@grammyjs/runner";
9
8
  import { Bot, GrammyError, HttpError, InlineKeyboard } from "grammy";
10
9
  //#region src/infra/utils/redact.ts
11
- var REDACTED = "[REDACTED]";
12
- var DEFAULT_PREVIEW_LENGTH = 160;
10
+ var REDACTED$1 = "[REDACTED]";
13
11
  var TELEGRAM_TOKEN_PATTERN = /\b\d{6,}:[A-Za-z0-9_-]{20,}\b/g;
14
12
  var BEARER_TOKEN_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/-]+=*\b/gi;
15
13
  var NAMED_SECRET_PATTERN = /\b(api[_\s-]?key|token|secret|password)\b(\s*[:=]\s*)([^\s,;]+)/gi;
16
14
  var API_KEY_LIKE_PATTERN = /\b(?:sk|pk)_[A-Za-z0-9_-]{10,}\b/g;
17
15
  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))}...`;
16
+ 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
17
  }
25
18
  //#endregion
26
19
  //#region src/infra/logger/index.ts
20
+ var DEFAULT_COMPONENT = "app";
21
+ var DEFAULT_EVENT = "log";
27
22
  var DEFAULT_SERVICE_NAME = "opencode-tbot";
23
+ var DEFAULT_MAX_LOG_FILES = 30;
24
+ var DEFAULT_MAX_TOTAL_LOG_BYTES = 314572800;
25
+ var CONTENT_OMITTED = "[OMITTED]";
26
+ var REDACTED = "[REDACTED]";
28
27
  var LEVEL_PRIORITY = {
29
28
  debug: 10,
30
29
  info: 20,
31
30
  warn: 30,
32
31
  error: 40
33
32
  };
33
+ var RESERVED_EVENT_FIELDS = new Set([
34
+ "attempt",
35
+ "callbackData",
36
+ "chatId",
37
+ "command",
38
+ "component",
39
+ "correlationId",
40
+ "durationMs",
41
+ "error",
42
+ "event",
43
+ "operationId",
44
+ "projectId",
45
+ "requestId",
46
+ "runtimeId",
47
+ "sessionId",
48
+ "sizeBytes",
49
+ "status",
50
+ "updateId",
51
+ "worktree"
52
+ ]);
34
53
  function createOpenCodeAppLogger(client, options = {}) {
35
54
  const service = normalizeServiceName(options.service);
36
55
  const minimumLevel = normalizeLogLevel(options.level);
56
+ const runtimeId = normalizeString(options.runtimeId) ?? randomUUID();
57
+ const boundRootContext = {
58
+ component: DEFAULT_COMPONENT,
59
+ runtimeId,
60
+ ...options.worktree ? { worktree: options.worktree } : {}
61
+ };
62
+ const sinks = createSinks(client, {
63
+ file: {
64
+ dir: options.file?.dir,
65
+ maxFiles: options.file?.retention?.maxFiles,
66
+ maxTotalBytes: options.file?.retention?.maxTotalBytes
67
+ },
68
+ runtimeId,
69
+ service,
70
+ sinks: options.sinks
71
+ });
37
72
  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
- };
73
+ const enqueue = (event) => {
74
+ if (LEVEL_PRIORITY[event.level] < LEVEL_PRIORITY[minimumLevel]) return;
75
+ const structuredEvent = buildStructuredLogEvent(event.level, event.input, event.message, service, event.context);
47
76
  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 {}
77
+ await Promise.allSettled(sinks.map((sink) => sink.write(structuredEvent)));
62
78
  });
63
79
  };
64
- return {
80
+ const createLogger = (context) => ({
65
81
  debug(input, message) {
66
- enqueue("debug", input, message);
82
+ enqueue({
83
+ context,
84
+ input,
85
+ level: "debug",
86
+ message
87
+ });
67
88
  },
68
89
  info(input, message) {
69
- enqueue("info", input, message);
90
+ enqueue({
91
+ context,
92
+ input,
93
+ level: "info",
94
+ message
95
+ });
70
96
  },
71
97
  warn(input, message) {
72
- enqueue("warn", input, message);
98
+ enqueue({
99
+ context,
100
+ input,
101
+ level: "warn",
102
+ message
103
+ });
73
104
  },
74
105
  error(input, message) {
75
- enqueue("error", input, message);
106
+ enqueue({
107
+ context,
108
+ input,
109
+ level: "error",
110
+ message
111
+ });
112
+ },
113
+ child(childContext) {
114
+ return createLogger({
115
+ ...context,
116
+ ...removeUndefinedFields(childContext)
117
+ });
76
118
  },
77
119
  async flush() {
78
120
  await queue.catch(() => void 0);
121
+ await Promise.allSettled(sinks.map(async (sink) => {
122
+ await sink.flush?.();
123
+ }));
79
124
  }
125
+ });
126
+ return createLogger(boundRootContext);
127
+ }
128
+ function logTelegramUpdate(logger, input, message) {
129
+ logger.info({
130
+ component: "telegram",
131
+ ...input
132
+ }, message);
133
+ }
134
+ function logPromptLifecycle(logger, input, message) {
135
+ logger.info({
136
+ component: "prompt",
137
+ ...input
138
+ }, message);
139
+ }
140
+ function logOpenCodeRequest(logger, input, message) {
141
+ logger.info({
142
+ component: "opencode",
143
+ ...input
144
+ }, message);
145
+ }
146
+ function logPluginEvent(logger, input, message) {
147
+ logger.info({
148
+ component: "plugin-event",
149
+ ...input
150
+ }, message);
151
+ }
152
+ function createSinks(client, options) {
153
+ const sinkOptions = options.sinks ?? {};
154
+ const sinks = [];
155
+ if (sinkOptions.host !== false) sinks.push(createHostSink(client, options.service));
156
+ if (sinkOptions.file !== false && options.file?.dir) sinks.push(createJsonlFileSink({
157
+ dir: options.file.dir,
158
+ maxFiles: options.file.maxFiles,
159
+ maxTotalBytes: options.file.maxTotalBytes,
160
+ runtimeId: options.runtimeId
161
+ }));
162
+ return sinks;
163
+ }
164
+ function createHostSink(client, service) {
165
+ return { async write(event) {
166
+ const payload = {
167
+ service,
168
+ level: event.level,
169
+ message: event.message,
170
+ extra: buildHostLogExtra(event)
171
+ };
172
+ if (client.app.log.length >= 2) {
173
+ await client.app.log(payload, {
174
+ responseStyle: "data",
175
+ throwOnError: true
176
+ });
177
+ return;
178
+ }
179
+ await client.app.log({
180
+ body: payload,
181
+ responseStyle: "data",
182
+ throwOnError: true
183
+ });
184
+ } };
185
+ }
186
+ function createJsonlFileSink(options) {
187
+ const maxFiles = options.maxFiles ?? DEFAULT_MAX_LOG_FILES;
188
+ const maxTotalBytes = options.maxTotalBytes ?? DEFAULT_MAX_TOTAL_LOG_BYTES;
189
+ const runtimeSegment = sanitizeFileSegment(options.runtimeId);
190
+ const timestampSegment = (/* @__PURE__ */ new Date()).toISOString().replace(/:/gu, "-");
191
+ const filePath = join(options.dir, `${timestampSegment}.${process.pid}.${runtimeSegment}.jsonl`);
192
+ let initialized = false;
193
+ let writeCount = 0;
194
+ const initialize = async () => {
195
+ if (initialized) return;
196
+ await mkdir(options.dir, { recursive: true });
197
+ await cleanupLogDirectory(options.dir, maxFiles, maxTotalBytes);
198
+ initialized = true;
199
+ };
200
+ return { async write(event) {
201
+ await initialize();
202
+ await appendFile(filePath, `${JSON.stringify(event)}\n`, "utf8");
203
+ writeCount += 1;
204
+ if (writeCount === 1 || writeCount % 100 === 0) await cleanupLogDirectory(options.dir, maxFiles, maxTotalBytes);
205
+ } };
206
+ }
207
+ async function cleanupLogDirectory(directory, maxFiles, maxTotalBytes) {
208
+ let entries = [];
209
+ try {
210
+ const names = await readdir(directory);
211
+ entries = await Promise.all(names.filter((name) => name.endsWith(".jsonl")).map(async (name) => {
212
+ const entryPath = join(directory, name);
213
+ const entryStat = await stat(entryPath);
214
+ return {
215
+ path: entryPath,
216
+ size: entryStat.size,
217
+ time: entryStat.mtimeMs
218
+ };
219
+ }));
220
+ } catch {
221
+ return;
222
+ }
223
+ entries.sort((left, right) => right.time - left.time);
224
+ let totalBytes = 0;
225
+ const retained = [];
226
+ const deleted = [];
227
+ for (const entry of entries) {
228
+ const canKeepByCount = retained.length < maxFiles;
229
+ const canKeepBySize = retained.length === 0 || totalBytes + entry.size <= maxTotalBytes;
230
+ if (canKeepByCount && canKeepBySize) {
231
+ retained.push(entry);
232
+ totalBytes += entry.size;
233
+ continue;
234
+ }
235
+ deleted.push(entry);
236
+ }
237
+ await Promise.allSettled(deleted.map(async (entry) => {
238
+ await unlink(entry.path);
239
+ }));
240
+ }
241
+ function buildStructuredLogEvent(level, input, message, service, boundContext) {
242
+ const extracted = extractContextAndExtra(input);
243
+ const context = {
244
+ ...removeUndefinedFields(boundContext),
245
+ ...removeUndefinedFields(extracted.context)
80
246
  };
247
+ const resolvedMessage = normalizeLogMessage(input, message);
248
+ return {
249
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
250
+ level,
251
+ service,
252
+ component: normalizeComponent(context.component),
253
+ event: normalizeEventName(context.event),
254
+ message: redactSensitiveText(resolvedMessage),
255
+ runtimeId: context.runtimeId ?? null,
256
+ operationId: context.operationId ?? null,
257
+ correlationId: resolveCorrelationId(context),
258
+ ...context.worktree ? { worktree: redactSensitiveText(context.worktree) } : {},
259
+ ...typeof context.chatId === "number" ? { chatId: context.chatId } : {},
260
+ ...context.sessionId ? { sessionId: context.sessionId } : {},
261
+ ...context.projectId ? { projectId: context.projectId } : {},
262
+ ...context.requestId ? { requestId: context.requestId } : {},
263
+ ...typeof context.updateId === "number" ? { updateId: context.updateId } : {},
264
+ ...context.command ? { command: context.command } : {},
265
+ ...context.callbackData ? { callbackData: context.callbackData } : {},
266
+ ...typeof context.durationMs === "number" ? { durationMs: context.durationMs } : {},
267
+ ...typeof context.attempt === "number" ? { attempt: context.attempt } : {},
268
+ ...context.status ? { status: context.status } : {},
269
+ ...typeof context.sizeBytes === "number" ? { sizeBytes: context.sizeBytes } : {},
270
+ ...context.error ? { error: context.error } : {},
271
+ ...removeUndefinedFields(extracted.extra)
272
+ };
273
+ }
274
+ function buildHostLogExtra(event) {
275
+ const { service: _service, level: _level, message: _message, ...rest } = event;
276
+ return rest;
277
+ }
278
+ function extractContextAndExtra(input) {
279
+ if (input instanceof Error) return {
280
+ context: { error: serializeError(input) },
281
+ extra: {}
282
+ };
283
+ if (Array.isArray(input)) return {
284
+ context: {},
285
+ extra: { items: input.map((item) => sanitizeValue(item)) }
286
+ };
287
+ if (input === null || input === void 0) return {
288
+ context: {},
289
+ extra: {}
290
+ };
291
+ if (typeof input === "string") return {
292
+ context: {},
293
+ extra: {}
294
+ };
295
+ if (typeof input !== "object") return {
296
+ context: {},
297
+ extra: { value: sanitizeValue(input) }
298
+ };
299
+ const record = input;
300
+ const context = {};
301
+ const extra = {};
302
+ for (const [key, value] of Object.entries(record)) {
303
+ if (RESERVED_EVENT_FIELDS.has(key)) {
304
+ assignReservedField(context, key, value);
305
+ continue;
306
+ }
307
+ extra[key] = sanitizeFieldValue(key, value);
308
+ }
309
+ return {
310
+ context,
311
+ extra
312
+ };
313
+ }
314
+ function assignReservedField(context, key, value) {
315
+ switch (key) {
316
+ case "attempt":
317
+ case "durationMs":
318
+ case "sizeBytes":
319
+ if (typeof value === "number") context[key] = value;
320
+ return;
321
+ case "chatId":
322
+ case "updateId":
323
+ if (typeof value === "number") context[key] = value;
324
+ return;
325
+ case "callbackData":
326
+ case "command":
327
+ case "component":
328
+ case "event":
329
+ case "projectId":
330
+ case "requestId":
331
+ case "sessionId":
332
+ case "status":
333
+ case "worktree":
334
+ if (typeof value === "string" && value.trim().length > 0) context[key] = redactSensitiveText(value.trim());
335
+ return;
336
+ case "correlationId":
337
+ case "operationId":
338
+ case "runtimeId":
339
+ if (typeof value === "string" && value.trim().length > 0) context[key] = value.trim();
340
+ else if (value === null) context[key] = null;
341
+ return;
342
+ case "error":
343
+ if (value instanceof Error) context.error = serializeError(value);
344
+ else if (isPlainObject(value)) context.error = normalizeStructuredLogErrorRecord(value);
345
+ return;
346
+ default: return;
347
+ }
348
+ }
349
+ function normalizeLogMessage(input, message) {
350
+ if (typeof input === "string") {
351
+ const normalizedInput = input.trim();
352
+ return normalizedInput.length > 0 ? normalizedInput : "log";
353
+ }
354
+ if (message && message.trim().length > 0) return message.trim();
355
+ if (input instanceof Error) return input.message.trim() || input.name;
356
+ return "log";
81
357
  }
82
358
  function normalizeServiceName(value) {
83
359
  const normalized = value?.trim();
@@ -91,59 +367,140 @@ function normalizeLogLevel(value) {
91
367
  default: return "info";
92
368
  }
93
369
  }
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);
370
+ function normalizeComponent(value) {
371
+ const normalized = value?.trim();
372
+ return normalized && normalized.length > 0 ? normalized : DEFAULT_COMPONENT;
373
+ }
374
+ function normalizeEventName(value) {
375
+ const normalized = value?.trim();
376
+ return normalized && normalized.length > 0 ? normalized : DEFAULT_EVENT;
377
+ }
378
+ function resolveCorrelationId(context) {
379
+ if (context.correlationId) return context.correlationId;
380
+ if (typeof context.updateId === "number") return String(context.updateId);
381
+ return context.operationId ?? null;
382
+ }
383
+ function serializeError(error) {
108
384
  return {
109
- ...extra ? { extra } : {},
110
- text: "log"
385
+ name: error.name,
386
+ message: redactSensitiveText(error.message),
387
+ ...error.stack ? { stack: redactSensitiveText(error.stack) } : {},
388
+ ..."data" in error && error.data && typeof error.data === "object" ? { data: sanitizeValue(error.data) } : {}
111
389
  };
112
390
  }
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);
391
+ function sanitizePlainObject(value) {
392
+ return Object.fromEntries(Object.entries(value).map(([key, entryValue]) => [key, sanitizeFieldValue(key, entryValue)]));
119
393
  }
120
- function sanitizeRecord(record) {
121
- return Object.fromEntries(Object.entries(record).map(([key, value]) => [key, isSensitiveKey(key) ? redactSensitiveFieldValue(value) : sanitizeValue(value)]));
394
+ function sanitizeFieldValue(key, value) {
395
+ if (isSensitiveKey(key)) return redactSensitiveFieldValue(value);
396
+ if (isUrlLikeKey(key)) return summarizeUrlValue(value);
397
+ if (isTextContentKey(key)) return summarizeTextValue(value);
398
+ if (isAttachmentCollectionKey(key)) return summarizeAttachmentCollection(value);
399
+ return sanitizeValue(value);
122
400
  }
123
401
  function sanitizeValue(value) {
124
402
  if (value instanceof Error) return serializeError(value);
125
- if (Array.isArray(value)) return value.map((item) => sanitizeValue(item));
403
+ if (Array.isArray(value)) return value.map((entry) => sanitizeValue(entry));
126
404
  if (typeof value === "string") return redactSensitiveText(value);
127
405
  if (!value || typeof value !== "object") return value;
128
- return sanitizeRecord(value);
406
+ return sanitizePlainObject(value);
129
407
  }
130
- function serializeError(error) {
408
+ function summarizeTextValue(value) {
409
+ if (typeof value === "string") return {
410
+ omitted: CONTENT_OMITTED,
411
+ length: value.length
412
+ };
413
+ if (Array.isArray(value)) return {
414
+ omitted: CONTENT_OMITTED,
415
+ count: value.length
416
+ };
417
+ if (value && typeof value === "object") return {
418
+ omitted: CONTENT_OMITTED,
419
+ kind: "object"
420
+ };
421
+ return value;
422
+ }
423
+ function summarizeUrlValue(value) {
424
+ if (typeof value === "string" && value.trim().length > 0) return {
425
+ omitted: CONTENT_OMITTED,
426
+ kind: "url"
427
+ };
428
+ return sanitizeValue(value);
429
+ }
430
+ function summarizeAttachmentCollection(value) {
431
+ if (!Array.isArray(value)) return summarizeTextValue(value);
131
432
  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) } : {}
433
+ count: value.length,
434
+ items: value.map((entry) => summarizeAttachmentValue(entry))
136
435
  };
137
436
  }
437
+ function summarizeAttachmentValue(value) {
438
+ if (!value || typeof value !== "object" || Array.isArray(value)) return summarizeTextValue(value);
439
+ const record = value;
440
+ return removeUndefinedFields({
441
+ filename: normalizeString(record.filename) ?? normalizeString(record.fileName) ?? void 0,
442
+ mime: normalizeString(record.mime) ?? normalizeString(record.mimeType) ?? void 0,
443
+ sizeBytes: pickNumericValue(record, [
444
+ "sizeBytes",
445
+ "size",
446
+ "fileSize"
447
+ ]),
448
+ hasCaption: pickStringValue(record, ["caption"]) !== null,
449
+ textLength: pickStringValue(record, ["text", "prompt"])?.length,
450
+ type: normalizeString(record.type) ?? void 0
451
+ });
452
+ }
138
453
  function isSensitiveKey(key) {
139
- return /token|secret|api[-_]?key|authorization|password/iu.test(key);
454
+ return /token|secret|api[-_]?key|authorization|password|cookie/iu.test(key);
455
+ }
456
+ function isTextContentKey(key) {
457
+ return /(^|[-_])(text|prompt|caption|body|markdown|content|messageText|fallbackText|input|raw)$/iu.test(key) || /bodyMd|bodyText|messageText|fallbackText/iu.test(key);
458
+ }
459
+ function isUrlLikeKey(key) {
460
+ return /(^|[-_])(url|uri|href|filePath|downloadPath|downloadUrl|fileUrl)$/iu.test(key);
461
+ }
462
+ function isAttachmentCollectionKey(key) {
463
+ return /(^|[-_])(files|parts|attachments)$/iu.test(key);
140
464
  }
141
465
  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]";
466
+ if (typeof value === "string" && value.trim().length > 0) return REDACTED;
467
+ if (Array.isArray(value)) return value.map(() => REDACTED);
468
+ if (value && typeof value === "object") return REDACTED;
145
469
  return value;
146
470
  }
471
+ function removeUndefinedFields(record) {
472
+ return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== void 0));
473
+ }
474
+ function isPlainObject(value) {
475
+ return value !== null && typeof value === "object" && !Array.isArray(value);
476
+ }
477
+ function normalizeString(value) {
478
+ return typeof value === "string" && value.trim().length > 0 ? redactSensitiveText(value.trim()) : null;
479
+ }
480
+ function pickNumericValue(record, keys) {
481
+ for (const key of keys) {
482
+ const value = record[key];
483
+ if (typeof value === "number") return value;
484
+ }
485
+ }
486
+ function pickStringValue(record, keys) {
487
+ for (const key of keys) {
488
+ const value = record[key];
489
+ if (typeof value === "string" && value.trim().length > 0) return value.trim();
490
+ }
491
+ return null;
492
+ }
493
+ function sanitizeFileSegment(value) {
494
+ return value.replace(/[^a-z0-9._-]+/giu, "_");
495
+ }
496
+ function normalizeStructuredLogErrorRecord(value) {
497
+ return {
498
+ name: normalizeString(value.name) ?? "Error",
499
+ message: normalizeString(value.message) ?? "Unknown error",
500
+ ...typeof value.stack === "string" ? { stack: redactSensitiveText(value.stack) } : {},
501
+ ...value.data !== void 0 ? { data: sanitizeValue(value.data) } : {}
502
+ };
503
+ }
147
504
  //#endregion
148
505
  //#region src/repositories/pending-action.repo.ts
149
506
  var FilePendingActionRepository = class {
@@ -213,6 +570,139 @@ var FileSessionRepository = class {
213
570
  }
214
571
  };
215
572
  //#endregion
573
+ //#region src/infra/utils/markdown-text.ts
574
+ var HTML_TAG_PATTERN = /<\/?[A-Za-z][^>]*>/g;
575
+ function stripMarkdownToPlainText(markdown) {
576
+ const lines = preprocessMarkdownForPlainText(markdown).split("\n");
577
+ const rendered = [];
578
+ for (let index = 0; index < lines.length; index += 1) {
579
+ const tableBlock = consumeMarkdownTable$1(lines, index);
580
+ if (tableBlock) {
581
+ rendered.push(renderTableAsPlainText(tableBlock.rows));
582
+ index = tableBlock.nextIndex - 1;
583
+ continue;
584
+ }
585
+ rendered.push(lines[index] ?? "");
586
+ }
587
+ return rendered.join("\n").replace(/```[A-Za-z0-9_-]*\n?/g, "").replace(/```/g, "").replace(/^#{1,6}\s+/gm, "").replace(/^\s*>\s?/gm, "").replace(/^(\s*)[-+*]\s+/gm, "$1- ").replace(/^(\s*)(\d+)[.)]\s+/gm, "$1$2. ").replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)").replace(/(\*\*|__)(.*?)\1/g, "$2").replace(/(\*|_)(.*?)\1/g, "$2").replace(/`([^`]+)`/g, "$1").replace(HTML_TAG_PATTERN, "").trim();
588
+ }
589
+ function preprocessMarkdownForPlainText(markdown) {
590
+ const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
591
+ const processed = [];
592
+ let activeFence = null;
593
+ for (const line of lines) {
594
+ const fenceMatch = line.match(/^```([A-Za-z0-9_-]+)?\s*$/);
595
+ if (fenceMatch) {
596
+ const language = (fenceMatch[1] ?? "").toLowerCase();
597
+ if (activeFence === "markdown") {
598
+ activeFence = null;
599
+ continue;
600
+ }
601
+ if (activeFence === "plain") {
602
+ processed.push(line);
603
+ activeFence = null;
604
+ continue;
605
+ }
606
+ if (language === "md" || language === "markdown") {
607
+ activeFence = "markdown";
608
+ continue;
609
+ }
610
+ activeFence = "plain";
611
+ processed.push(line);
612
+ continue;
613
+ }
614
+ processed.push(line);
615
+ }
616
+ return processed.join("\n");
617
+ }
618
+ function consumeMarkdownTable$1(lines, startIndex) {
619
+ if (startIndex + 1 >= lines.length) return null;
620
+ const headerCells = parseMarkdownTableRow$1(lines[startIndex] ?? "");
621
+ const separatorCells = parseMarkdownTableSeparator$1(lines[startIndex + 1] ?? "");
622
+ if (!headerCells || !separatorCells || headerCells.length !== separatorCells.length) return null;
623
+ const rows = [headerCells];
624
+ let index = startIndex + 2;
625
+ while (index < lines.length) {
626
+ const rowCells = parseMarkdownTableRow$1(lines[index] ?? "");
627
+ if (!rowCells || rowCells.length !== headerCells.length) break;
628
+ rows.push(rowCells);
629
+ index += 1;
630
+ }
631
+ return {
632
+ rows,
633
+ nextIndex: index
634
+ };
635
+ }
636
+ function parseMarkdownTableRow$1(line) {
637
+ const trimmed = line.trim();
638
+ if (!trimmed.includes("|")) return null;
639
+ const cells = splitMarkdownTableCells$1(trimmed).map((cell) => normalizeTableCell$1(cell));
640
+ return cells.length >= 2 ? cells : null;
641
+ }
642
+ function parseMarkdownTableSeparator$1(line) {
643
+ const cells = splitMarkdownTableCells$1(line.trim());
644
+ if (cells.length < 2) return null;
645
+ return cells.every((cell) => /^:?-{3,}:?$/.test(cell.trim())) ? cells : null;
646
+ }
647
+ function splitMarkdownTableCells$1(line) {
648
+ const content = line.replace(/^\|/, "").replace(/\|$/, "");
649
+ const cells = [];
650
+ let current = "";
651
+ let escaped = false;
652
+ for (const char of content) {
653
+ if (escaped) {
654
+ current += char;
655
+ escaped = false;
656
+ continue;
657
+ }
658
+ if (char === "\\") {
659
+ escaped = true;
660
+ current += char;
661
+ continue;
662
+ }
663
+ if (char === "|") {
664
+ cells.push(current);
665
+ current = "";
666
+ continue;
667
+ }
668
+ current += char;
669
+ }
670
+ cells.push(current);
671
+ return cells;
672
+ }
673
+ function normalizeTableCell$1(cell) {
674
+ return cell.trim().replace(/\\\|/g, "|").replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)").replace(/(\*\*|__)(.*?)\1/g, "$2").replace(/(\*|_)(.*?)\1/g, "$2").replace(/`([^`]+)`/g, "$1").replace(HTML_TAG_PATTERN, "");
675
+ }
676
+ function renderTableAsPlainText(rows) {
677
+ return buildAlignedTableLines$1(rows).join("\n");
678
+ }
679
+ function buildAlignedTableLines$1(rows) {
680
+ const columnWidths = calculateTableColumnWidths$1(rows);
681
+ return [
682
+ formatTableRow$1(rows[0] ?? [], columnWidths),
683
+ columnWidths.map((width) => "-".repeat(Math.max(3, width))).join("-+-"),
684
+ ...rows.slice(1).map((row) => formatTableRow$1(row, columnWidths))
685
+ ];
686
+ }
687
+ function calculateTableColumnWidths$1(rows) {
688
+ return (rows[0] ?? []).map((_, columnIndex) => rows.reduce((maxWidth, row) => Math.max(maxWidth, getDisplayWidth$1(row[columnIndex] ?? "")), 0));
689
+ }
690
+ function formatTableRow$1(row, columnWidths) {
691
+ return row.map((cell, index) => padDisplayWidth$1(cell, columnWidths[index] ?? 0)).join(" | ");
692
+ }
693
+ function padDisplayWidth$1(value, targetWidth) {
694
+ const padding = Math.max(0, targetWidth - getDisplayWidth$1(value));
695
+ return `${value}${" ".repeat(padding)}`;
696
+ }
697
+ function getDisplayWidth$1(value) {
698
+ let width = 0;
699
+ for (const char of value) width += isWideCharacter$1(char.codePointAt(0) ?? 0) ? 2 : 1;
700
+ return width;
701
+ }
702
+ function isWideCharacter$1(codePoint) {
703
+ return codePoint >= 4352 && (codePoint <= 4447 || codePoint === 9001 || codePoint === 9002 || codePoint >= 11904 && codePoint <= 42191 && codePoint !== 12351 || codePoint >= 44032 && codePoint <= 55203 || codePoint >= 63744 && codePoint <= 64255 || codePoint >= 65040 && codePoint <= 65049 || codePoint >= 65072 && codePoint <= 65135 || codePoint >= 65280 && codePoint <= 65376 || codePoint >= 65504 && codePoint <= 65510);
704
+ }
705
+ //#endregion
216
706
  //#region src/services/opencode/opencode.client.ts
217
707
  function buildOpenCodeSdkConfig(options) {
218
708
  const apiKey = options.apiKey?.trim();
@@ -256,24 +746,11 @@ var DEFAULT_OPENCODE_PROMPT_TIMEOUT_POLICY = {
256
746
  recoveryInactivityTimeoutMs: 12e4,
257
747
  waitTimeoutMs: 18e5
258
748
  };
259
- var STRUCTURED_REPLY_SCHEMA = {
260
- type: "json_schema",
261
- retryCount: 2,
262
- schema: {
263
- type: "object",
264
- additionalProperties: false,
265
- required: ["body_md"],
266
- properties: { body_md: {
267
- type: "string",
268
- description: "Markdown body only. Do not include duration, token usage, or any footer. Do not wrap the whole answer in ```md or ```markdown fences. Use Markdown formatting directly unless the user explicitly asks for raw Markdown source."
269
- } }
270
- }
271
- };
272
749
  var SDK_OPTIONS = {
273
750
  responseStyle: "data",
274
751
  throwOnError: true
275
752
  };
276
- var StructuredReplySchema = z.object({ body_md: z.string() });
753
+ var TEXT_OUTPUT_FORMAT = { type: "text" };
277
754
  var OpenCodeClient = class {
278
755
  client;
279
756
  fetchFn;
@@ -461,20 +938,17 @@ var OpenCodeClient = class {
461
938
  return buildPromptSessionResult(await this.resolvePromptResponse(input, null, knownMessageIds, startedAt), {
462
939
  emptyResponseText: EMPTY_RESPONSE_TEXT,
463
940
  finishedAt: Date.now(),
464
- startedAt,
465
- structured: input.structured ?? false
941
+ startedAt
466
942
  });
467
943
  }
468
944
  async resolvePromptResponse(input, data, knownMessageIds, startedAt) {
469
- const structured = input.structured ?? false;
470
- if (data && shouldReturnPromptResponseImmediately(data, structured)) return data;
945
+ if (data && shouldReturnPromptResponseImmediately(data)) return data;
471
946
  const messageId = data ? extractMessageId(data.info) : null;
472
947
  const candidateOptions = {
473
948
  initialMessageId: messageId,
474
949
  initialParentId: data ? toAssistantMessage(data.info)?.parentID ?? null : null,
475
950
  knownMessageIds,
476
- requestStartedAt: resolvePromptCandidateStartTime(startedAt, data),
477
- structured
951
+ requestStartedAt: resolvePromptCandidateStartTime(startedAt, data)
478
952
  };
479
953
  let bestCandidate = selectPromptResponseCandidate(data ? [data] : [], candidateOptions);
480
954
  let lastProgressAt = Date.now();
@@ -499,26 +973,26 @@ var OpenCodeClient = class {
499
973
  if (next) {
500
974
  const nextCandidate = selectPromptResponseCandidate([bestCandidate, next], candidateOptions);
501
975
  if (nextCandidate) {
502
- if (didPromptResponseAdvance(bestCandidate, nextCandidate, structured)) {
976
+ if (didPromptResponseAdvance(bestCandidate, nextCandidate)) {
503
977
  lastProgressAt = Date.now();
504
978
  idleStatusSeen = false;
505
979
  }
506
980
  bestCandidate = nextCandidate;
507
981
  }
508
- if (bestCandidate && isPromptResponseForCurrentRequest(bestCandidate, candidateOptions) && shouldReturnPromptResponseImmediately(bestCandidate, structured)) return bestCandidate;
982
+ if (bestCandidate && isPromptResponseForCurrentRequest(bestCandidate, candidateOptions) && shouldReturnPromptResponseImmediately(bestCandidate)) return bestCandidate;
509
983
  }
510
984
  }
511
985
  const latest = await this.findLatestPromptResponse(input.sessionId, candidateOptions, "poll-messages", input.signal);
512
986
  if (latest) {
513
987
  const nextCandidate = selectPromptResponseCandidate([bestCandidate, latest], candidateOptions);
514
988
  if (nextCandidate) {
515
- if (didPromptResponseAdvance(bestCandidate, nextCandidate, structured)) {
989
+ if (didPromptResponseAdvance(bestCandidate, nextCandidate)) {
516
990
  lastProgressAt = Date.now();
517
991
  idleStatusSeen = false;
518
992
  }
519
993
  bestCandidate = nextCandidate;
520
994
  }
521
- if (bestCandidate && isPromptResponseForCurrentRequest(bestCandidate, candidateOptions) && shouldReturnPromptResponseImmediately(bestCandidate, structured)) return bestCandidate;
995
+ if (bestCandidate && isPromptResponseForCurrentRequest(bestCandidate, candidateOptions) && shouldReturnPromptResponseImmediately(bestCandidate)) return bestCandidate;
522
996
  }
523
997
  const status = await this.fetchPromptSessionStatus(input.sessionId, input.signal);
524
998
  lastStatus = status;
@@ -529,14 +1003,14 @@ var OpenCodeClient = class {
529
1003
  if (idleStatusSeen) break;
530
1004
  idleStatusSeen = true;
531
1005
  }
532
- if (bestCandidate && isPromptResponseForCurrentRequest(bestCandidate, candidateOptions) && isCompletedEmptyPromptResponse(bestCandidate, structured) && status?.type !== "busy" && status?.type !== "retry") break;
1006
+ if (bestCandidate && isPromptResponseForCurrentRequest(bestCandidate, candidateOptions) && isCompletedEmptyPromptResponse(bestCandidate) && status?.type !== "busy" && status?.type !== "retry") break;
533
1007
  if (Date.now() >= deadlineAt) break;
534
1008
  }
535
1009
  const latest = await this.findLatestPromptResponse(input.sessionId, candidateOptions, "final-scan", input.signal);
536
1010
  const resolved = selectPromptResponseCandidate([bestCandidate, latest], candidateOptions);
537
1011
  const requestScopedResolved = resolved && isPromptResponseForCurrentRequest(resolved, candidateOptions) ? resolved : null;
538
- if (lastStatus?.type === "idle" && (!requestScopedResolved || shouldPollPromptMessage(requestScopedResolved, structured))) throw createMessageAbortedError();
539
- if (!requestScopedResolved || shouldPollPromptMessage(requestScopedResolved, structured)) {
1012
+ if (lastStatus?.type === "idle" && (!requestScopedResolved || shouldPollPromptMessage(requestScopedResolved))) throw createMessageAbortedError();
1013
+ if (!requestScopedResolved || shouldPollPromptMessage(requestScopedResolved)) {
540
1014
  const timeoutReason = Date.now() >= deadlineAt ? "max-wait" : "recovery-inactivity";
541
1015
  const timeoutMs = timeoutReason === "max-wait" ? this.promptTimeoutPolicy.waitTimeoutMs : this.promptTimeoutPolicy.recoveryInactivityTimeoutMs;
542
1016
  const error = createOpenCodePromptTimeoutError({
@@ -662,9 +1136,10 @@ var OpenCodeClient = class {
662
1136
  return this.callScopedSdkMethod("config", "providers", {});
663
1137
  }
664
1138
  async sendPromptRequest(input, parts) {
1139
+ const format = input.format ?? TEXT_OUTPUT_FORMAT;
665
1140
  const requestBody = {
666
1141
  ...input.agent ? { agent: input.agent } : {},
667
- ...input.structured ? { format: STRUCTURED_REPLY_SCHEMA } : {},
1142
+ format,
668
1143
  ...input.model ? { model: input.model } : {},
669
1144
  ...input.variant ? { variant: input.variant } : {},
670
1145
  parts
@@ -881,10 +1356,9 @@ function extractTextFromParts(parts) {
881
1356
  }
882
1357
  function buildPromptSessionResult(data, options) {
883
1358
  const assistantInfo = toAssistantMessage(data.info);
884
- const structuredPayload = extractStructuredPayload(assistantInfo);
885
- const bodyMd = options.structured ? extractStructuredMarkdown(structuredPayload) : null;
886
1359
  const responseParts = Array.isArray(data.parts) ? data.parts : [];
887
- const fallbackText = extractTextFromParts(responseParts) || bodyMd || options.emptyResponseText;
1360
+ const bodyMd = extractTextFromParts(responseParts) || null;
1361
+ const fallbackText = bodyMd ? stripMarkdownToPlainText(bodyMd) || bodyMd : options.emptyResponseText;
888
1362
  return {
889
1363
  assistantError: assistantInfo?.error ?? null,
890
1364
  bodyMd,
@@ -892,22 +1366,21 @@ function buildPromptSessionResult(data, options) {
892
1366
  info: assistantInfo,
893
1367
  metrics: extractPromptMetrics(assistantInfo, options.startedAt, options.finishedAt),
894
1368
  parts: responseParts,
895
- structured: structuredPayload ?? null
1369
+ structured: null
896
1370
  };
897
1371
  }
898
- function shouldPollPromptMessage(data, structured) {
1372
+ function shouldPollPromptMessage(data) {
899
1373
  const assistantInfo = toAssistantMessage(data.info);
900
- const bodyMd = structured ? extractStructuredMarkdown(extractStructuredPayload(assistantInfo)) : null;
901
1374
  const hasText = extractTextFromParts(Array.isArray(data.parts) ? data.parts : []).length > 0;
902
1375
  const hasAssistantError = !!assistantInfo?.error;
903
1376
  const isCompleted = isAssistantMessageCompleted(assistantInfo);
904
- return !hasText && !bodyMd && !hasAssistantError && !isCompleted;
1377
+ return !hasText && !hasAssistantError && !isCompleted;
905
1378
  }
906
- function shouldReturnPromptResponseImmediately(data, structured) {
907
- return !shouldPollPromptMessage(data, structured) && !isCompletedEmptyPromptResponse(data, structured);
1379
+ function shouldReturnPromptResponseImmediately(data) {
1380
+ return !shouldPollPromptMessage(data) && !isCompletedEmptyPromptResponse(data);
908
1381
  }
909
- function isPromptResponseUsable(data, structured) {
910
- return !shouldPollPromptMessage(data, structured) && !isCompletedEmptyPromptResponse(data, structured);
1382
+ function isPromptResponseUsable(data) {
1383
+ return !shouldPollPromptMessage(data) && !isCompletedEmptyPromptResponse(data);
911
1384
  }
912
1385
  function normalizePromptResponse(response) {
913
1386
  return {
@@ -942,8 +1415,6 @@ function toAssistantMessage(message) {
942
1415
  if ("providerID" in message && typeof message.providerID === "string" && message.providerID.trim().length > 0) normalized.providerID = message.providerID;
943
1416
  if ("role" in message && message.role === "assistant") normalized.role = "assistant";
944
1417
  if ("sessionID" in message && typeof message.sessionID === "string" && message.sessionID.trim().length > 0) normalized.sessionID = message.sessionID;
945
- const structuredPayload = extractStructuredPayload(message);
946
- if (structuredPayload !== null) normalized.structured = structuredPayload;
947
1418
  if ("summary" in message && typeof message.summary === "boolean") normalized.summary = message.summary;
948
1419
  if ("time" in message && isPlainRecord$1(message.time)) normalized.time = {
949
1420
  ...typeof message.time.created === "number" && Number.isFinite(message.time.created) ? { created: message.time.created } : {},
@@ -984,8 +1455,8 @@ function delay(ms, signal) {
984
1455
  signal?.addEventListener("abort", onAbort, { once: true });
985
1456
  });
986
1457
  }
987
- function didPromptResponseAdvance(previous, next, structured) {
988
- return getPromptResponseProgressSignature(previous, structured) !== getPromptResponseProgressSignature(next, structured);
1458
+ function didPromptResponseAdvance(previous, next) {
1459
+ return getPromptResponseProgressSignature(previous) !== getPromptResponseProgressSignature(next);
989
1460
  }
990
1461
  function createOpenCodePromptTimeoutError(input) {
991
1462
  return new OpenCodePromptTimeoutError({
@@ -1007,12 +1478,6 @@ function resolvePromptEndpointKind(stage) {
1007
1478
  function getPromptMessagePollDelayMs(attempt) {
1008
1479
  return PROMPT_MESSAGE_POLL_INITIAL_DELAYS_MS[attempt] ?? PROMPT_MESSAGE_POLL_INTERVAL_MS;
1009
1480
  }
1010
- function extractStructuredMarkdown(structured) {
1011
- const parsed = StructuredReplySchema.safeParse(structured);
1012
- if (!parsed.success) return null;
1013
- const bodyMd = parsed.data.body_md.replace(/\r\n?/g, "\n").trim();
1014
- return bodyMd.length > 0 ? bodyMd : null;
1015
- }
1016
1481
  function extractPromptMetrics(info, startedAt, finishedAt) {
1017
1482
  const createdAt = typeof info?.time?.created === "number" && Number.isFinite(info.time.created) ? info.time.created : null;
1018
1483
  const completedAt = typeof info?.time?.completed === "number" && Number.isFinite(info.time.completed) ? info.time.completed : null;
@@ -1105,17 +1570,10 @@ function normalizeAssistantError(value) {
1105
1570
  function isAssistantMessageCompleted(message) {
1106
1571
  return !!message?.error || typeof message?.time?.completed === "number" || typeof message?.finish === "string" && message.finish.trim().length > 0;
1107
1572
  }
1108
- function isCompletedEmptyPromptResponse(data, structured) {
1573
+ function isCompletedEmptyPromptResponse(data) {
1109
1574
  const assistantInfo = toAssistantMessage(data.info);
1110
- const bodyMd = structured ? extractStructuredMarkdown(extractStructuredPayload(assistantInfo)) : null;
1111
1575
  const hasText = extractTextFromParts(Array.isArray(data.parts) ? data.parts : []).length > 0;
1112
- return isAssistantMessageCompleted(assistantInfo) && !assistantInfo?.error && !hasText && !bodyMd;
1113
- }
1114
- function extractStructuredPayload(message) {
1115
- if (!isPlainRecord$1(message)) return null;
1116
- if ("structured" in message && message.structured !== void 0) return message.structured;
1117
- if ("structured_output" in message && message.structured_output !== void 0) return message.structured_output;
1118
- return null;
1576
+ return isAssistantMessageCompleted(assistantInfo) && !assistantInfo?.error && !hasText;
1119
1577
  }
1120
1578
  function selectPromptResponseCandidate(candidates, options) {
1121
1579
  const availableCandidates = candidates.filter((candidate) => !!candidate).filter((candidate) => toAssistantMessage(candidate.info) !== null);
@@ -1135,7 +1593,7 @@ function getPromptResponseCandidateRank(message, options) {
1135
1593
  createdAt,
1136
1594
  isInitial: !!id && id === options.initialMessageId,
1137
1595
  isNewSinceRequestStart: isPromptResponseNewSinceRequestStart(id, createdAt, options.knownMessageIds, options.requestStartedAt),
1138
- isUsable: isPromptResponseUsable(message, options.structured),
1596
+ isUsable: isPromptResponseUsable(message),
1139
1597
  sharesParent: !!assistant?.parentID && assistant.parentID === options.initialParentId
1140
1598
  };
1141
1599
  }
@@ -1145,13 +1603,13 @@ function resolvePromptCandidateStartTime(startedAt, initialMessage) {
1145
1603
  if (initialCreatedAt === null) return startedAt;
1146
1604
  return areComparablePromptTimestamps(startedAt, initialCreatedAt) ? startedAt : initialCreatedAt;
1147
1605
  }
1148
- function getPromptResponseProgressSignature(response, structured) {
1606
+ function getPromptResponseProgressSignature(response) {
1149
1607
  if (!response) return "null";
1150
1608
  const assistant = toAssistantMessage(response.info);
1151
1609
  const responseParts = Array.isArray(response.parts) ? response.parts : [];
1152
1610
  return JSON.stringify({
1153
1611
  assistantError: assistant?.error?.name ?? null,
1154
- bodyMd: structured ? extractStructuredMarkdown(extractStructuredPayload(assistant)) : null,
1612
+ bodyMd: extractTextFromParts(responseParts) || null,
1155
1613
  completedAt: assistant?.time?.completed ?? null,
1156
1614
  finish: assistant?.finish ?? null,
1157
1615
  id: assistant?.id ?? null,
@@ -1979,10 +2437,11 @@ var SendPromptUseCase = class {
1979
2437
  binding
1980
2438
  });
1981
2439
  binding = createdSession.binding;
1982
- this.logger.info({
2440
+ logPromptLifecycle(this.logger, {
1983
2441
  chatId: input.chatId,
1984
- sessionId: createdSession.session.id,
2442
+ event: "prompt.session.created",
1985
2443
  projectId: createdSession.session.projectID,
2444
+ sessionId: createdSession.session.id,
1986
2445
  directory: createdSession.session.directory
1987
2446
  }, "session created");
1988
2447
  }
@@ -1990,11 +2449,15 @@ var SendPromptUseCase = class {
1990
2449
  const selectedModel = (await this.opencodeClient.listModels()).find((model) => model.providerID === binding?.modelProviderId && model.id === binding?.modelId);
1991
2450
  if (!selectedModel) {
1992
2451
  binding = await clearStoredModelSelection(this.sessionRepo, binding);
1993
- this.logger.warn?.({ chatId: input.chatId }, "selected model is no longer available, falling back to OpenCode default");
2452
+ this.logger.warn?.({
2453
+ chatId: input.chatId,
2454
+ event: "prompt.model.unavailable"
2455
+ }, "selected model is no longer available, falling back to OpenCode default");
1994
2456
  } else if (binding.modelVariant && !(binding.modelVariant in selectedModel.variants)) {
1995
2457
  binding = await clearStoredModelVariant(this.sessionRepo, binding);
1996
2458
  this.logger.warn?.({
1997
2459
  chatId: input.chatId,
2460
+ event: "prompt.model.variant_unavailable",
1998
2461
  providerId: selectedModel.providerID,
1999
2462
  modelId: selectedModel.id
2000
2463
  }, "selected model variant is no longer available, falling back to default variant");
@@ -2010,23 +2473,41 @@ var SendPromptUseCase = class {
2010
2473
  const selectedAgent = resolveSelectedAgent(await this.opencodeClient.listAgents(), activeBinding.agentName);
2011
2474
  if (activeBinding.agentName && selectedAgent?.name !== activeBinding.agentName) {
2012
2475
  activeBinding = await clearStoredAgentSelection(this.sessionRepo, activeBinding);
2013
- this.logger.warn?.({ chatId: input.chatId }, "selected agent is no longer available, falling back to OpenCode default");
2476
+ this.logger.warn?.({
2477
+ chatId: input.chatId,
2478
+ event: "prompt.agent.unavailable"
2479
+ }, "selected agent is no longer available, falling back to OpenCode default");
2014
2480
  }
2015
2481
  const temporarySessionId = shouldIsolateImageTurn ? await this.createTemporaryImageSession(input.chatId, activeBinding.sessionId) : null;
2016
2482
  const executionSessionId = temporarySessionId ?? activeBinding.sessionId;
2017
2483
  input.onExecutionSession?.(executionSessionId);
2018
2484
  let result;
2019
2485
  try {
2486
+ logOpenCodeRequest(this.logger, {
2487
+ chatId: input.chatId,
2488
+ event: "opencode.prompt.submit",
2489
+ projectId: activeBinding.projectId,
2490
+ sessionId: executionSessionId,
2491
+ fileCount: files.length,
2492
+ status: "started"
2493
+ }, "submitting OpenCode prompt");
2020
2494
  result = await this.opencodeClient.promptSession({
2021
2495
  sessionId: executionSessionId,
2022
2496
  prompt: promptText,
2023
2497
  ...files.length > 0 ? { files } : {},
2024
2498
  ...selectedAgent ? { agent: selectedAgent.name } : {},
2025
- structured: true,
2499
+ format: { type: "text" },
2026
2500
  ...model ? { model } : {},
2027
2501
  ...input.signal ? { signal: input.signal } : {},
2028
2502
  ...activeBinding.modelVariant ? { variant: activeBinding.modelVariant } : {}
2029
2503
  });
2504
+ logPromptLifecycle(this.logger, {
2505
+ chatId: input.chatId,
2506
+ event: "prompt.completed",
2507
+ projectId: activeBinding.projectId,
2508
+ sessionId: executionSessionId,
2509
+ status: "completed"
2510
+ }, "prompt completed");
2030
2511
  } finally {
2031
2512
  if (temporarySessionId) await this.cleanupTemporaryImageSession(input.chatId, activeBinding.sessionId, temporarySessionId);
2032
2513
  }
@@ -2039,14 +2520,18 @@ var SendPromptUseCase = class {
2039
2520
  }
2040
2521
  async clearInvalidSessionContext(chatId, binding, reason) {
2041
2522
  const nextBinding = await clearStoredSessionContext(this.sessionRepo, binding);
2042
- this.logger.warn?.({ chatId }, `${reason}, falling back to the current OpenCode project`);
2523
+ this.logger.warn?.({
2524
+ chatId,
2525
+ event: "prompt.session.invalid_context"
2526
+ }, `${reason}, falling back to the current OpenCode project`);
2043
2527
  return nextBinding;
2044
2528
  }
2045
2529
  async createTemporaryImageSession(chatId, sessionId) {
2046
2530
  const temporarySession = await this.opencodeClient.forkSession(sessionId);
2047
2531
  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?.({
2532
+ logPromptLifecycle(this.logger, {
2049
2533
  chatId,
2534
+ event: "prompt.temporary_session.created",
2050
2535
  parentSessionId: sessionId,
2051
2536
  sessionId: temporarySession.id
2052
2537
  }, "created temporary image session");
@@ -2056,6 +2541,7 @@ var SendPromptUseCase = class {
2056
2541
  try {
2057
2542
  if (!await this.opencodeClient.deleteSession(sessionId)) this.logger.warn?.({
2058
2543
  chatId,
2544
+ event: "prompt.temporary_session.cleanup_failed",
2059
2545
  parentSessionId,
2060
2546
  sessionId
2061
2547
  }, "failed to delete temporary image session");
@@ -2063,6 +2549,7 @@ var SendPromptUseCase = class {
2063
2549
  this.logger.warn?.({
2064
2550
  error,
2065
2551
  chatId,
2552
+ event: "prompt.temporary_session.cleanup_failed",
2066
2553
  parentSessionId,
2067
2554
  sessionId
2068
2555
  }, "failed to delete temporary image session");
@@ -2263,7 +2750,23 @@ function resolveExtension(mimeType) {
2263
2750
  //#endregion
2264
2751
  //#region src/app/container.ts
2265
2752
  function createAppContainer(config, client) {
2266
- const logger = createOpenCodeAppLogger(client, { level: config.logLevel });
2753
+ const runtimeId = randomUUID();
2754
+ const logger = createOpenCodeAppLogger(client, {
2755
+ file: {
2756
+ dir: config.loggingFileDir,
2757
+ retention: {
2758
+ maxFiles: config.loggingRetentionMaxFiles,
2759
+ maxTotalBytes: config.loggingRetentionMaxTotalBytes
2760
+ }
2761
+ },
2762
+ level: config.loggingLevel,
2763
+ runtimeId,
2764
+ sinks: {
2765
+ file: config.loggingFileSinkEnabled,
2766
+ host: config.loggingHostSinkEnabled
2767
+ },
2768
+ worktree: config.worktreePath
2769
+ });
2267
2770
  return createContainer(config, createOpenCodeClientFromSdkClient(client, fetch, {
2268
2771
  waitTimeoutMs: config.promptWaitTimeoutMs,
2269
2772
  pollRequestTimeoutMs: config.promptPollRequestTimeoutMs,
@@ -2271,6 +2774,9 @@ function createAppContainer(config, client) {
2271
2774
  }), logger);
2272
2775
  }
2273
2776
  function createContainer(config, opencodeClient, logger) {
2777
+ const storageLogger = logger.child({ component: "storage" });
2778
+ const opencodeLogger = logger.child({ component: "opencode" });
2779
+ const promptLogger = logger.child({ component: "prompt" });
2274
2780
  const stateStore = new JsonStateStore({
2275
2781
  filePath: config.stateFilePath,
2276
2782
  createDefaultState: createDefaultOpencodeTbotState
@@ -2285,7 +2791,7 @@ function createContainer(config, opencodeClient, logger) {
2285
2791
  });
2286
2792
  const uploadFileUseCase = new UploadFileUseCase(telegramFileClient);
2287
2793
  const abortPromptUseCase = new AbortPromptUseCase(sessionRepo, opencodeClient, foregroundSessionTracker);
2288
- const createSessionUseCase = new CreateSessionUseCase(sessionRepo, opencodeClient, logger);
2794
+ const createSessionUseCase = new CreateSessionUseCase(sessionRepo, opencodeClient, opencodeLogger);
2289
2795
  const getHealthUseCase = new GetHealthUseCase(opencodeClient);
2290
2796
  const getPathUseCase = new GetPathUseCase(opencodeClient);
2291
2797
  const listAgentsUseCase = new ListAgentsUseCase(sessionRepo, opencodeClient);
@@ -2294,11 +2800,11 @@ function createContainer(config, opencodeClient, logger) {
2294
2800
  const listSessionsUseCase = new ListSessionsUseCase(sessionRepo, opencodeClient);
2295
2801
  const getStatusUseCase = new GetStatusUseCase(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo);
2296
2802
  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);
2803
+ const renameSessionUseCase = new RenameSessionUseCase(sessionRepo, opencodeClient, opencodeLogger);
2804
+ const sendPromptUseCase = new SendPromptUseCase(sessionRepo, opencodeClient, promptLogger);
2805
+ const switchAgentUseCase = new SwitchAgentUseCase(sessionRepo, opencodeClient, opencodeLogger);
2806
+ const switchModelUseCase = new SwitchModelUseCase(sessionRepo, opencodeClient, opencodeLogger);
2807
+ const switchSessionUseCase = new SwitchSessionUseCase(sessionRepo, opencodeClient, opencodeLogger);
2302
2808
  let disposed = false;
2303
2809
  return {
2304
2810
  abortPromptUseCase,
@@ -2327,7 +2833,10 @@ function createContainer(config, opencodeClient, logger) {
2327
2833
  async dispose() {
2328
2834
  if (disposed) return;
2329
2835
  disposed = true;
2330
- logger.info({ filePath: config.stateFilePath }, "disposing telegram bot container");
2836
+ storageLogger.info({
2837
+ event: "storage.container.disposed",
2838
+ filePath: config.stateFilePath
2839
+ }, "disposing telegram bot container");
2331
2840
  await logger.flush();
2332
2841
  }
2333
2842
  };
@@ -2431,6 +2940,11 @@ async function handleTelegramBotPluginEvent(runtime, event) {
2431
2940
  }
2432
2941
  }
2433
2942
  async function handlePermissionAsked(runtime, request) {
2943
+ const logger = runtime.container.logger.child({
2944
+ component: "plugin-event",
2945
+ requestId: request.id,
2946
+ sessionId: request.sessionID
2947
+ });
2434
2948
  const bindings = await runtime.container.sessionRepo.listBySessionId(request.sessionID);
2435
2949
  const chatIds = new Set([...bindings.map((binding) => binding.chatId), ...runtime.container.foregroundSessionTracker.listChatIds(request.sessionID)]);
2436
2950
  const approvals = await runtime.container.permissionApprovalRepo.listByRequestId(request.id);
@@ -2451,35 +2965,45 @@ async function handlePermissionAsked(runtime, request) {
2451
2965
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2452
2966
  });
2453
2967
  } catch (error) {
2454
- runtime.container.logger.error({
2968
+ logger.error({
2455
2969
  error,
2456
2970
  chatId,
2971
+ event: "plugin-event.permission.ask.delivery_failed",
2457
2972
  requestId: request.id
2458
2973
  }, "failed to deliver permission request to Telegram");
2459
2974
  }
2460
2975
  }
2461
2976
  }
2462
2977
  async function handlePermissionReplied(runtime, event) {
2978
+ const logger = runtime.container.logger.child({
2979
+ component: "plugin-event",
2980
+ event: "plugin-event.permission.replied",
2981
+ requestId: event.requestId,
2982
+ sessionId: event.sessionId
2983
+ });
2463
2984
  const approvals = await runtime.container.permissionApprovalRepo.listByRequestId(event.requestId);
2464
2985
  await Promise.all(approvals.map(async (approval) => {
2465
2986
  try {
2466
2987
  await runtime.bot.api.editMessageText(approval.chatId, approval.messageId, buildPermissionApprovalResolvedMessage(event.requestId, event.reply));
2467
2988
  } catch (error) {
2468
- runtime.container.logger.warn({
2989
+ logger.warn({
2469
2990
  error,
2470
2991
  chatId: approval.chatId,
2471
- requestId: event.requestId,
2472
- sessionId: event.sessionId
2992
+ event: "plugin-event.permission.reply_message_failed"
2473
2993
  }, "failed to update Telegram permission message");
2474
2994
  }
2475
2995
  await runtime.container.permissionApprovalRepo.set(toResolvedApproval(approval, event.reply));
2476
2996
  }));
2477
2997
  }
2478
2998
  async function handleSessionError(runtime, event) {
2479
- if (runtime.container.foregroundSessionTracker.fail(event.sessionId, event.error instanceof Error ? event.error : /* @__PURE__ */ new Error("Unknown session error."))) {
2480
- runtime.container.logger.warn({
2999
+ const logger = runtime.container.logger.child({
3000
+ component: "plugin-event",
3001
+ sessionId: event.sessionId
3002
+ });
3003
+ if (runtime.container.foregroundSessionTracker.fail(event.sessionId, normalizeForegroundSessionError(event.error))) {
3004
+ logger.warn({
2481
3005
  error: event.error,
2482
- sessionId: event.sessionId
3006
+ event: "plugin-event.session.error.foreground_suppressed"
2483
3007
  }, "session error suppressed for foreground Telegram session");
2484
3008
  return;
2485
3009
  }
@@ -2487,8 +3011,12 @@ async function handleSessionError(runtime, event) {
2487
3011
  await notifyBoundChats(runtime, event.sessionId, `Session failed.\n\nSession: ${event.sessionId}\nError: ${message}`);
2488
3012
  }
2489
3013
  async function handleSessionIdle(runtime, event) {
3014
+ const logger = runtime.container.logger.child({
3015
+ component: "plugin-event",
3016
+ sessionId: event.sessionId
3017
+ });
2490
3018
  if (runtime.container.foregroundSessionTracker.clear(event.sessionId)) {
2491
- runtime.container.logger.info({ sessionId: event.sessionId }, "session idle notification suppressed for foreground Telegram session");
3019
+ logPluginEvent(logger, { event: "plugin-event.session.idle.foreground_suppressed" }, "session idle notification suppressed for foreground Telegram session");
2492
3020
  return;
2493
3021
  }
2494
3022
  await notifyBoundChats(runtime, event.sessionId, `Session finished.\n\nSession: ${event.sessionId}`);
@@ -2498,16 +3026,20 @@ async function handleSessionStatus(runtime, event) {
2498
3026
  await handleSessionIdle(runtime, event);
2499
3027
  }
2500
3028
  async function notifyBoundChats(runtime, sessionId, text) {
3029
+ const logger = runtime.container.logger.child({
3030
+ component: "plugin-event",
3031
+ sessionId
3032
+ });
2501
3033
  const bindings = await runtime.container.sessionRepo.listBySessionId(sessionId);
2502
3034
  const chatIds = [...new Set(bindings.map((binding) => binding.chatId))];
2503
3035
  await Promise.all(chatIds.map(async (chatId) => {
2504
3036
  try {
2505
3037
  await runtime.bot.api.sendMessage(chatId, text);
2506
3038
  } catch (error) {
2507
- runtime.container.logger.warn({
3039
+ logger.warn({
2508
3040
  error,
2509
3041
  chatId,
2510
- sessionId
3042
+ event: "plugin-event.session.notify_failed"
2511
3043
  }, "failed to notify Telegram chat about session event");
2512
3044
  }
2513
3045
  }));
@@ -2586,6 +3118,16 @@ function extractSessionErrorMessage(error) {
2586
3118
  if (isPlainRecord(error.data) && typeof error.data.message === "string" && error.data.message.trim().length > 0) return error.data.message.trim();
2587
3119
  return asNonEmptyString(error.name);
2588
3120
  }
3121
+ function normalizeForegroundSessionError(error) {
3122
+ if (error instanceof Error) return error;
3123
+ if (isPlainRecord(error) && typeof error.name === "string" && error.name.trim().length > 0) {
3124
+ const normalized = new Error(extractSessionErrorMessage(error) ?? "Unknown session error.");
3125
+ normalized.name = error.name.trim();
3126
+ if (isPlainRecord(error.data)) normalized.data = error.data;
3127
+ return normalized;
3128
+ }
3129
+ return /* @__PURE__ */ new Error("Unknown session error.");
3130
+ }
2589
3131
  function asNonEmptyString(value) {
2590
3132
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
2591
3133
  }
@@ -3284,7 +3826,9 @@ var TELEGRAM_COMMAND_SYNC_SCOPES = [{ type: "default" }, { type: "all_private_ch
3284
3826
  async function syncTelegramCommands(bot, logger) {
3285
3827
  await Promise.all(TELEGRAM_COMMAND_SYNC_SCOPES.map((scope) => bot.api.setMyCommands(TELEGRAM_COMMANDS, { scope })));
3286
3828
  logger.info({
3829
+ component: "runtime",
3287
3830
  commands: TELEGRAM_COMMANDS.map((command) => command.command),
3831
+ event: "runtime.commands.synced",
3288
3832
  scopes: TELEGRAM_COMMAND_SYNC_SCOPES.map((scope) => scope.type)
3289
3833
  }, "telegram commands synced");
3290
3834
  }
@@ -3295,6 +3839,44 @@ async function syncTelegramCommandsForChat(api, chatId, language) {
3295
3839
  } });
3296
3840
  }
3297
3841
  //#endregion
3842
+ //#region src/bot/logger-context.ts
3843
+ function buildTelegramLoggerContext(ctx, component = "telegram") {
3844
+ const updateId = typeof ctx.update?.update_id === "number" ? ctx.update.update_id : void 0;
3845
+ const command = extractTelegramCommand(resolveMessageText(ctx));
3846
+ const callbackData = normalizeTelegramString(ctx.callbackQuery?.data);
3847
+ const operationId = typeof updateId === "number" ? `telegram-${updateId}` : null;
3848
+ return {
3849
+ component,
3850
+ ...typeof ctx.chat?.id === "number" ? { chatId: ctx.chat.id } : {},
3851
+ ...typeof updateId === "number" ? { updateId } : {},
3852
+ ...command ? { command } : {},
3853
+ ...callbackData ? { callbackData } : {},
3854
+ correlationId: typeof updateId === "number" ? String(updateId) : operationId,
3855
+ operationId
3856
+ };
3857
+ }
3858
+ function scopeLoggerToTelegramContext(logger, ctx, component = "telegram") {
3859
+ return logger.child(buildTelegramLoggerContext(ctx, component));
3860
+ }
3861
+ function scopeDependenciesToTelegramContext(dependencies, ctx, component = "telegram") {
3862
+ return {
3863
+ ...dependencies,
3864
+ logger: scopeLoggerToTelegramContext(dependencies.logger, ctx, component)
3865
+ };
3866
+ }
3867
+ function resolveMessageText(ctx) {
3868
+ return normalizeTelegramString(ctx.message?.text) ?? normalizeTelegramString(ctx.msg?.text);
3869
+ }
3870
+ function extractTelegramCommand(value) {
3871
+ if (!value || !value.startsWith("/")) return null;
3872
+ const token = value.split(/\s+/u, 1)[0]?.trim();
3873
+ if (!token) return null;
3874
+ return token.replace(/^\/+/u, "").split("@", 1)[0] ?? null;
3875
+ }
3876
+ function normalizeTelegramString(value) {
3877
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
3878
+ }
3879
+ //#endregion
3298
3880
  //#region src/bot/presenters/error.presenter.ts
3299
3881
  function presentError(error, copy = BOT_COPY) {
3300
3882
  const presented = normalizeError(error, copy);
@@ -3411,16 +3993,12 @@ function stringifyUnknown(value) {
3411
3993
  //#endregion
3412
3994
  //#region src/bot/error-boundary.ts
3413
3995
  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
- };
3996
+ return buildTelegramLoggerContext({
3997
+ callbackQuery: { data: getNestedString(ctx, ["callbackQuery", "data"]) },
3998
+ chat: { id: getNestedNumber(ctx, ["chat", "id"]) ?? void 0 },
3999
+ message: { text: getNestedString(ctx, ["message", "text"]) },
4000
+ update: { update_id: getNestedNumber(ctx, ["update", "update_id"]) ?? void 0 }
4001
+ });
3424
4002
  }
3425
4003
  async function replyWithDefaultTelegramError(ctx, logger, error) {
3426
4004
  const text = presentError(error, BOT_COPY);
@@ -3985,7 +4563,7 @@ async function handleAgentsCommand(ctx, dependencies) {
3985
4563
  }
3986
4564
  function registerAgentsCommand(bot, dependencies) {
3987
4565
  bot.command(["agents", "agent"], async (ctx) => {
3988
- await handleAgentsCommand(ctx, dependencies);
4566
+ await handleAgentsCommand(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
3989
4567
  });
3990
4568
  }
3991
4569
  //#endregion
@@ -4128,7 +4706,7 @@ async function handleCancelCommand(ctx, dependencies) {
4128
4706
  }
4129
4707
  function registerCancelCommand(bot, dependencies) {
4130
4708
  bot.command("cancel", async (ctx) => {
4131
- await handleCancelCommand(ctx, dependencies);
4709
+ await handleCancelCommand(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
4132
4710
  });
4133
4711
  }
4134
4712
  //#endregion
@@ -4175,7 +4753,7 @@ async function presentLanguageSwitchForChat(chatId, api, language, dependencies)
4175
4753
  }
4176
4754
  function registerLanguageCommand(bot, dependencies) {
4177
4755
  bot.command("language", async (ctx) => {
4178
- await handleLanguageCommand(ctx, dependencies);
4756
+ await handleLanguageCommand(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
4179
4757
  });
4180
4758
  }
4181
4759
  //#endregion
@@ -4203,7 +4781,7 @@ async function handleModelsCommand(ctx, dependencies) {
4203
4781
  }
4204
4782
  function registerModelsCommand(bot, dependencies) {
4205
4783
  bot.command(["model", "models"], async (ctx) => {
4206
- await handleModelsCommand(ctx, dependencies);
4784
+ await handleModelsCommand(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
4207
4785
  });
4208
4786
  }
4209
4787
  //#endregion
@@ -4224,7 +4802,7 @@ async function handleNewCommand(ctx, dependencies) {
4224
4802
  }
4225
4803
  function registerNewCommand(bot, dependencies) {
4226
4804
  bot.command("new", async (ctx) => {
4227
- await handleNewCommand(ctx, dependencies);
4805
+ await handleNewCommand(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
4228
4806
  });
4229
4807
  }
4230
4808
  function extractSessionTitle(ctx) {
@@ -4245,7 +4823,7 @@ var MARKDOWN_SPECIAL_CHARACTERS = /([_*\[\]()~`>#+\-=|{}.!\\])/g;
4245
4823
  function buildTelegramPromptReply(result, copy = BOT_COPY) {
4246
4824
  const renderedMarkdown = result.bodyMd ? renderMarkdownToTelegramMarkdownV2(result.bodyMd) : null;
4247
4825
  const footerPlain = formatPlainMetricsFooter(result.metrics, copy);
4248
- const fallback = { text: joinBodyAndFooter(truncatePlainBody(normalizePlainBody(result, renderedMarkdown !== null, copy), footerPlain), footerPlain) };
4826
+ const fallback = { text: joinBodyAndFooter(truncatePlainBody(normalizePlainBody(result, copy), footerPlain), footerPlain) };
4249
4827
  if (!renderedMarkdown) return {
4250
4828
  preferred: fallback,
4251
4829
  fallback
@@ -4349,24 +4927,10 @@ function renderMarkdownToTelegramMarkdownV2(markdown) {
4349
4927
  if (inCodeBlock) return null;
4350
4928
  return rendered.join("\n");
4351
4929
  }
4352
- function stripMarkdownToPlainText(markdown) {
4353
- const lines = preprocessMarkdownForTelegram(markdown).split("\n");
4354
- const rendered = [];
4355
- for (let index = 0; index < lines.length; index += 1) {
4356
- const tableBlock = consumeMarkdownTable(lines, index);
4357
- if (tableBlock) {
4358
- rendered.push(renderTableAsPlainText(tableBlock.rows));
4359
- index = tableBlock.nextIndex - 1;
4360
- continue;
4361
- }
4362
- rendered.push(lines[index]);
4363
- }
4364
- return rendered.join("\n").replace(/```[A-Za-z0-9_-]*\n?/g, "").replace(/```/g, "").replace(/^#{1,6}\s+/gm, "").replace(/^\s*>\s?/gm, "").replace(/^(\s*)[-+*]\s+/gm, "$1- ").replace(/^(\s*)(\d+)[.)]\s+/gm, "$1$2. ").replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)").replace(/(\*\*|__)(.*?)\1/g, "$2").replace(/(\*|_)(.*?)\1/g, "$2").replace(/`([^`]+)`/g, "$1").trim();
4365
- }
4366
- function normalizePlainBody(result, preferStructured, copy) {
4930
+ function normalizePlainBody(result, copy) {
4367
4931
  const fromStructured = result.bodyMd ? stripMarkdownToPlainText(result.bodyMd) : "";
4368
4932
  const fromFallback = result.fallbackText.trim();
4369
- return (preferStructured ? fromStructured || fromFallback : fromFallback || fromStructured).trim() || result.fallbackText || copy.prompt.emptyResponse;
4933
+ return (fromStructured || fromFallback).trim() || result.fallbackText || copy.prompt.emptyResponse;
4370
4934
  }
4371
4935
  function truncatePlainBody(body, footer) {
4372
4936
  const reservedLength = footer.length + 2;
@@ -4490,9 +5054,6 @@ function renderTableAsTelegramCodeBlock(rows) {
4490
5054
  "```"
4491
5055
  ].join("\n");
4492
5056
  }
4493
- function renderTableAsPlainText(rows) {
4494
- return buildAlignedTableLines(rows).join("\n");
4495
- }
4496
5057
  function buildAlignedTableLines(rows) {
4497
5058
  const columnWidths = calculateTableColumnWidths(rows);
4498
5059
  return [
@@ -4589,7 +5150,7 @@ async function handleStatusCommand(ctx, dependencies) {
4589
5150
  }
4590
5151
  function registerStatusCommand(bot, dependencies) {
4591
5152
  bot.command("status", async (ctx) => {
4592
- await handleStatusCommand(ctx, dependencies);
5153
+ await handleStatusCommand(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
4593
5154
  });
4594
5155
  }
4595
5156
  //#endregion
@@ -4607,7 +5168,7 @@ async function handleSessionsCommand(ctx, dependencies) {
4607
5168
  }
4608
5169
  function registerSessionsCommand(bot, dependencies) {
4609
5170
  bot.command("sessions", async (ctx) => {
4610
- await handleSessionsCommand(ctx, dependencies);
5171
+ await handleSessionsCommand(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
4611
5172
  });
4612
5173
  }
4613
5174
  //#endregion
@@ -4634,7 +5195,7 @@ async function handleStartCommand(ctx, dependencies) {
4634
5195
  }
4635
5196
  function registerStartCommand(bot, dependencies) {
4636
5197
  bot.command("start", async (ctx) => {
4637
- await handleStartCommand(ctx, dependencies);
5198
+ await handleStartCommand(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
4638
5199
  });
4639
5200
  }
4640
5201
  //#endregion
@@ -4900,19 +5461,19 @@ async function handlePermissionApprovalCallback(ctx, dependencies) {
4900
5461
  }
4901
5462
  function registerCallbackHandler(bot, dependencies) {
4902
5463
  bot.callbackQuery(/^agents:/, async (ctx) => {
4903
- await handleAgentsCallback(ctx, dependencies);
5464
+ await handleAgentsCallback(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
4904
5465
  });
4905
5466
  bot.callbackQuery(/^sessions:/, async (ctx) => {
4906
- await handleSessionsCallback(ctx, dependencies);
5467
+ await handleSessionsCallback(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
4907
5468
  });
4908
5469
  bot.callbackQuery(/^model:/, async (ctx) => {
4909
- await handleModelsCallback(ctx, dependencies);
5470
+ await handleModelsCallback(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
4910
5471
  });
4911
5472
  bot.callbackQuery(/^language:/, async (ctx) => {
4912
- await handleLanguageCallback(ctx, dependencies);
5473
+ await handleLanguageCallback(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
4913
5474
  });
4914
5475
  bot.callbackQuery(/^permission:/, async (ctx) => {
4915
- await handlePermissionApprovalCallback(ctx, dependencies);
5476
+ await handlePermissionApprovalCallback(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
4916
5477
  });
4917
5478
  }
4918
5479
  function parseSessionActionTarget(data, prefix) {
@@ -4949,7 +5510,7 @@ async function executePromptRequest(ctx, dependencies, resolvePrompt) {
4949
5510
  },
4950
5511
  signal: foregroundRequest.signal,
4951
5512
  text: promptInput.text
4952
- })).assistantReply, copy, dependencies), copy);
5513
+ })).assistantReply, copy), copy);
4953
5514
  try {
4954
5515
  await ctx.reply(telegramReply.preferred.text, telegramReply.preferred.options);
4955
5516
  } catch (replyError) {
@@ -4970,26 +5531,14 @@ async function executePromptRequest(ctx, dependencies, resolvePrompt) {
4970
5531
  }
4971
5532
  }
4972
5533
  }
4973
- function normalizePromptReplyForDisplay(promptReply, copy, dependencies) {
5534
+ function normalizePromptReplyForDisplay(promptReply, copy) {
4974
5535
  if (!promptReply.assistantError) return promptReply;
4975
- if (isRecoverableStructuredOutputError(promptReply)) {
4976
- dependencies.logger.warn?.({ error: promptReply.assistantError }, "structured output validation failed, falling back to assistant text reply");
4977
- return {
4978
- ...promptReply,
4979
- assistantError: null
4980
- };
4981
- }
4982
5536
  return {
4983
5537
  ...promptReply,
4984
5538
  bodyMd: null,
4985
5539
  fallbackText: presentError(promptReply.assistantError, copy)
4986
5540
  };
4987
5541
  }
4988
- function isRecoverableStructuredOutputError(promptReply) {
4989
- if (promptReply.assistantError?.name !== "StructuredOutputError") return false;
4990
- if (promptReply.bodyMd?.trim()) return true;
4991
- return promptReply.parts.some((part) => part.type === "text" && typeof part.text === "string" && part.text.trim().length > 0);
4992
- }
4993
5542
  //#endregion
4994
5543
  //#region src/bot/handlers/file.handler.ts
4995
5544
  var TELEGRAM_MAX_DOWNLOAD_BYTES = 20 * 1024 * 1024;
@@ -5014,10 +5563,10 @@ async function handleImageMessage(ctx, dependencies) {
5014
5563
  }
5015
5564
  function registerFileHandler(bot, dependencies) {
5016
5565
  bot.on("message:photo", async (ctx) => {
5017
- await handleImageMessage(ctx, dependencies);
5566
+ await handleImageMessage(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
5018
5567
  });
5019
5568
  bot.on("message:document", async (ctx) => {
5020
- await handleImageMessage(ctx, dependencies);
5569
+ await handleImageMessage(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
5021
5570
  });
5022
5571
  }
5023
5572
  function resolveTelegramImage(message) {
@@ -5059,7 +5608,7 @@ async function handleTextMessage(ctx, dependencies) {
5059
5608
  }
5060
5609
  function registerMessageHandler(bot, dependencies) {
5061
5610
  bot.on("message:text", async (ctx) => {
5062
- await handleTextMessage(ctx, dependencies);
5611
+ await handleTextMessage(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
5063
5612
  });
5064
5613
  }
5065
5614
  //#endregion
@@ -5072,7 +5621,7 @@ async function handleVoiceMessage(ctx, dependencies) {
5072
5621
  }
5073
5622
  function registerVoiceHandler(bot, dependencies) {
5074
5623
  bot.on("message:voice", async (ctx) => {
5075
- await handleVoiceMessage(ctx, dependencies);
5624
+ await handleVoiceMessage(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
5076
5625
  });
5077
5626
  }
5078
5627
  //#endregion
@@ -5093,17 +5642,18 @@ function createAuthMiddleware(allowedChatIds) {
5093
5642
  function buildIncomingUpdateLogFields(ctx) {
5094
5643
  const messageText = ctx.msg && "text" in ctx.msg ? ctx.msg.text : void 0;
5095
5644
  return {
5645
+ ...buildTelegramLoggerContext(ctx),
5646
+ event: "telegram.update.received",
5096
5647
  updateId: ctx.update.update_id,
5097
5648
  chatId: ctx.chat?.id,
5098
5649
  fromId: ctx.from?.id,
5099
5650
  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
5651
+ textLength: typeof messageText === "string" ? messageText.length : 0
5102
5652
  };
5103
5653
  }
5104
5654
  function createLoggingMiddleware(logger) {
5105
5655
  return async (ctx, next) => {
5106
- logger.info(buildIncomingUpdateLogFields(ctx), "incoming update");
5656
+ logTelegramUpdate(logger, { ...buildIncomingUpdateLogFields(ctx) }, "incoming update");
5107
5657
  return next();
5108
5658
  };
5109
5659
  }
@@ -5113,11 +5663,13 @@ function registerBot(bot, container, options) {
5113
5663
  bot.use(createLoggingMiddleware(container.logger));
5114
5664
  bot.use(createAuthMiddleware(options.telegramAllowedChatIds));
5115
5665
  const safeBot = bot.errorBoundary(async (error) => {
5116
- container.logger.error({
5666
+ const scopedLogger = scopeLoggerToTelegramContext(container.logger, error.ctx, "telegram");
5667
+ scopedLogger.error({
5117
5668
  ...extractTelegramUpdateContext(error.ctx),
5669
+ event: "telegram.middleware.failed",
5118
5670
  error: error.error
5119
5671
  }, "telegram middleware failed");
5120
- await replyWithDefaultTelegramError(error.ctx, container.logger, error.error);
5672
+ await replyWithDefaultTelegramError(error.ctx, scopedLogger, error.error);
5121
5673
  });
5122
5674
  registerStartCommand(safeBot, container);
5123
5675
  registerStatusCommand(safeBot, container);
@@ -5144,8 +5696,10 @@ async function startTelegramBotRuntime(input) {
5144
5696
  const runtimeKey = buildTelegramRuntimeKey(input.config);
5145
5697
  const registry = getTelegramBotRuntimeRegistry();
5146
5698
  const existingRuntime = registry.activeByKey.get(runtimeKey);
5699
+ const runtimeLogger = input.container.logger.child({ component: "runtime" });
5147
5700
  if (existingRuntime) {
5148
- input.container.logger.warn({
5701
+ runtimeLogger.warn({
5702
+ event: "runtime.reused",
5149
5703
  runtimeKey,
5150
5704
  telegramApiRoot: input.config.telegramApiRoot
5151
5705
  }, "telegram runtime already active in this process; reusing the existing runner");
@@ -5163,13 +5717,18 @@ async function startTelegramBotRuntime(input) {
5163
5717
  }
5164
5718
  async function startTelegramBotRuntimeInternal(input, runtimeKey, releaseRuntime) {
5165
5719
  const bot = (input.botFactory ?? ((token, options) => new Bot(token, options)))(input.config.telegramBotToken, { client: { apiRoot: input.config.telegramApiRoot } });
5720
+ const runtimeLogger = input.container.logger.child({ component: "runtime" });
5166
5721
  wrapTelegramGetUpdates(bot, input.container);
5167
5722
  (input.registerBotHandlers ?? registerBot)(bot, input.container, { telegramAllowedChatIds: input.config.telegramAllowedChatIds });
5168
5723
  bot.catch((error) => {
5169
5724
  const metadata = extractTelegramUpdateContext(error.ctx);
5725
+ const telegramLogger = input.container.logger.child({
5726
+ component: "telegram",
5727
+ ...metadata
5728
+ });
5170
5729
  if (error.error instanceof GrammyError) {
5171
- input.container.logger.error({
5172
- ...metadata,
5730
+ telegramLogger.error({
5731
+ event: "telegram.api.error",
5173
5732
  errorCode: error.error.error_code,
5174
5733
  description: error.error.description,
5175
5734
  method: error.error.method,
@@ -5179,24 +5738,28 @@ async function startTelegramBotRuntimeInternal(input, runtimeKey, releaseRuntime
5179
5738
  return;
5180
5739
  }
5181
5740
  if (error.error instanceof HttpError) {
5182
- input.container.logger.error({
5183
- ...metadata,
5741
+ telegramLogger.error({
5742
+ event: "telegram.http.error",
5184
5743
  error: error.error.error,
5185
5744
  message: error.error.message
5186
5745
  }, "telegram bot network request failed");
5187
5746
  return;
5188
5747
  }
5189
- input.container.logger.error({
5190
- ...metadata,
5748
+ telegramLogger.error({
5749
+ event: "telegram.update.failed",
5191
5750
  error: error.error
5192
5751
  }, "telegram bot update failed");
5193
5752
  });
5194
- input.container.logger.info({ runtimeKey }, "telegram bot polling starting");
5753
+ runtimeLogger.info({
5754
+ event: "runtime.polling.starting",
5755
+ runtimeKey
5756
+ }, "telegram bot polling starting");
5195
5757
  const runner = (input.runBot ?? run)(bot, TELEGRAM_RUNNER_OPTIONS);
5196
5758
  let stopped = false;
5197
5759
  let disposed = false;
5198
5760
  if (input.syncCommands ?? true) (input.syncCommandsHandler ?? syncTelegramCommands)(bot, input.container.logger).catch((error) => {
5199
- input.container.logger.warn({
5761
+ runtimeLogger.warn({
5762
+ event: "runtime.commands.sync_failed",
5200
5763
  error,
5201
5764
  runtimeKey
5202
5765
  }, "failed to sync telegram commands; polling continues without command registration updates");
@@ -5206,7 +5769,8 @@ async function startTelegramBotRuntimeInternal(input, runtimeKey, releaseRuntime
5206
5769
  if (stopped) return;
5207
5770
  stopped = true;
5208
5771
  stopPromise = runner.stop().catch((error) => {
5209
- input.container.logger.warn({
5772
+ runtimeLogger.warn({
5773
+ event: "runtime.stop.failed",
5210
5774
  error,
5211
5775
  runtimeKey
5212
5776
  }, "failed to stop telegram runner cleanly");
@@ -5235,6 +5799,7 @@ async function startTelegramBotRuntimeInternal(input, runtimeKey, releaseRuntime
5235
5799
  }
5236
5800
  function wrapTelegramGetUpdates(bot, container) {
5237
5801
  const originalGetUpdates = bot.api.getUpdates.bind(bot.api);
5802
+ const runtimeLogger = container.logger.child({ component: "runtime" });
5238
5803
  bot.api.getUpdates = async (options, signal) => {
5239
5804
  const requestOptions = options ?? {
5240
5805
  limit: 100,
@@ -5244,7 +5809,8 @@ function wrapTelegramGetUpdates(bot, container) {
5244
5809
  try {
5245
5810
  return await originalGetUpdates(requestOptions, signal);
5246
5811
  } catch (error) {
5247
- container.logger.warn({
5812
+ runtimeLogger.warn({
5813
+ event: "runtime.telegram.get_updates_failed",
5248
5814
  error,
5249
5815
  limit: requestOptions.limit,
5250
5816
  offset: requestOptions.offset,
@@ -5308,8 +5874,9 @@ async function startPluginRuntime(options, cwd) {
5308
5874
  });
5309
5875
  const { config, container } = bootstrapApp(options.context.client, preparedConfiguration.config, { cwd: preparedConfiguration.cwd });
5310
5876
  try {
5311
- if (preparedConfiguration.ignoredProjectConfigFilePath) container.logger.warn({
5877
+ if (preparedConfiguration.ignoredProjectConfigFilePath) container.logger.child({ component: "runtime" }).warn({
5312
5878
  cwd: preparedConfiguration.cwd,
5879
+ event: "runtime.config.legacy_worktree_ignored",
5313
5880
  ignoredProjectConfigFilePath: preparedConfiguration.ignoredProjectConfigFilePath,
5314
5881
  globalConfigFilePath: preparedConfiguration.globalConfigFilePath
5315
5882
  }, "legacy worktree plugin config is ignored; migrate settings to the global opencode-tbot config");
@@ -5317,8 +5884,9 @@ async function startPluginRuntime(options, cwd) {
5317
5884
  config,
5318
5885
  container
5319
5886
  });
5320
- container.logger.info({
5887
+ container.logger.child({ component: "runtime" }).info({
5321
5888
  cwd: preparedConfiguration.cwd,
5889
+ event: "runtime.plugin.started",
5322
5890
  globalConfigFilePath: preparedConfiguration.globalConfigFilePath,
5323
5891
  ignoredProjectConfigFilePath: preparedConfiguration.ignoredProjectConfigFilePath,
5324
5892
  configFilePath: preparedConfiguration.configFilePath,