opencode-tbot 0.1.28 → 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,73 +1,361 @@
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/v2/client";
7
6
  import { randomUUID } from "node:crypto";
7
+ import { createOpencodeClient } from "@opencode-ai/sdk";
8
8
  import { run } from "@grammyjs/runner";
9
- import { Bot, InlineKeyboard } from "grammy";
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
- await client.app.log(payload);
50
- } catch {}
78
+ await Promise.allSettled(sinks.map((sink) => sink.write(structuredEvent)));
51
79
  });
52
80
  };
53
- return {
81
+ const createLogger = (context) => ({
54
82
  debug(input, message) {
55
- enqueue("debug", input, message);
83
+ enqueue({
84
+ context,
85
+ input,
86
+ level: "debug",
87
+ message
88
+ });
56
89
  },
57
90
  info(input, message) {
58
- enqueue("info", input, message);
91
+ enqueue({
92
+ context,
93
+ input,
94
+ level: "info",
95
+ message
96
+ });
59
97
  },
60
98
  warn(input, message) {
61
- enqueue("warn", input, message);
99
+ enqueue({
100
+ context,
101
+ input,
102
+ level: "warn",
103
+ message
104
+ });
62
105
  },
63
106
  error(input, message) {
64
- 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
+ });
65
119
  },
66
120
  async flush() {
67
121
  await queue.catch(() => void 0);
122
+ await Promise.allSettled(sinks.map(async (sink) => {
123
+ await sink.flush?.();
124
+ }));
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;
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;
68
307
  }
308
+ extra[key] = sanitizeFieldValue(key, value);
309
+ }
310
+ return {
311
+ context,
312
+ extra
69
313
  };
70
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";
358
+ }
71
359
  function normalizeServiceName(value) {
72
360
  const normalized = value?.trim();
73
361
  return normalized && normalized.length > 0 ? normalized : DEFAULT_SERVICE_NAME;
@@ -80,59 +368,140 @@ function normalizeLogLevel(value) {
80
368
  default: return "info";
81
369
  }
82
370
  }
83
- function normalizeLogArguments(input, message) {
84
- if (typeof input === "string") return { text: input };
85
- if (message && message.trim().length > 0) {
86
- const extra = serializeExtra(input);
87
- return {
88
- ...extra ? { extra } : {},
89
- text: message.trim()
90
- };
91
- }
92
- if (input instanceof Error) return {
93
- extra: serializeExtra(input) ?? void 0,
94
- text: input.message.trim() || input.name
95
- };
96
- 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) {
97
385
  return {
98
- ...extra ? { extra } : {},
99
- 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) } : {}
100
390
  };
101
391
  }
102
- function serializeExtra(input) {
103
- if (input === null || input === void 0) return null;
104
- if (input instanceof Error) return { error: serializeError(input) };
105
- if (Array.isArray(input)) return { items: input.map((item) => sanitizeValue(item)) };
106
- if (typeof input !== "object") return { value: sanitizeValue(input) };
107
- return sanitizeRecord(input);
392
+ function sanitizePlainObject(value) {
393
+ return Object.fromEntries(Object.entries(value).map(([key, entryValue]) => [key, sanitizeFieldValue(key, entryValue)]));
108
394
  }
109
- function sanitizeRecord(record) {
110
- 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);
111
401
  }
112
402
  function sanitizeValue(value) {
113
403
  if (value instanceof Error) return serializeError(value);
114
- if (Array.isArray(value)) return value.map((item) => sanitizeValue(item));
404
+ if (Array.isArray(value)) return value.map((entry) => sanitizeValue(entry));
115
405
  if (typeof value === "string") return redactSensitiveText(value);
116
406
  if (!value || typeof value !== "object") return value;
117
- return sanitizeRecord(value);
407
+ return sanitizePlainObject(value);
118
408
  }
119
- 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);
120
433
  return {
121
- name: error.name,
122
- message: redactSensitiveText(error.message),
123
- ...error.stack ? { stack: redactSensitiveText(error.stack) } : {},
124
- ..."data" in error && error.data && typeof error.data === "object" ? { data: sanitizeValue(error.data) } : {}
434
+ count: value.length,
435
+ items: value.map((entry) => summarizeAttachmentValue(entry))
125
436
  };
126
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
+ }
127
454
  function isSensitiveKey(key) {
128
- 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);
129
465
  }
130
466
  function redactSensitiveFieldValue(value) {
131
- if (typeof value === "string" && value.trim().length > 0) return "[REDACTED]";
132
- if (Array.isArray(value)) return value.map(() => "[REDACTED]");
133
- 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;
134
470
  return value;
135
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
+ }
136
505
  //#endregion
137
506
  //#region src/repositories/pending-action.repo.ts
138
507
  var FilePendingActionRepository = class {
@@ -292,7 +661,10 @@ var OpenCodeClient = class {
292
661
  ...SDK_OPTIONS,
293
662
  ...input.signal ? { signal: input.signal } : {}
294
663
  };
295
- return unwrapSdkData(input.parameters === void 0 ? await handler.call(target, options) : await handler.call(target, input.parameters, options));
664
+ return unwrapSdkData(handler.length >= 2 ? await handler.call(target, input.legacyParameters, options) : await handler.call(target, input.parameters === void 0 ? options : {
665
+ ...input.parameters,
666
+ ...options
667
+ }));
296
668
  }
297
669
  async getHealth() {
298
670
  try {
@@ -303,25 +675,43 @@ var OpenCodeClient = class {
303
675
  }
304
676
  }
305
677
  async abortSession(sessionId) {
306
- return this.callScopedSdkMethod("session", "abort", { parameters: { sessionID: sessionId } });
678
+ return this.callScopedSdkMethod("session", "abort", {
679
+ legacyParameters: { sessionID: sessionId },
680
+ parameters: { path: { id: sessionId } }
681
+ });
307
682
  }
308
683
  async deleteSession(sessionId) {
309
- return this.callScopedSdkMethod("session", "delete", { parameters: { sessionID: sessionId } });
684
+ return this.callScopedSdkMethod("session", "delete", {
685
+ legacyParameters: { sessionID: sessionId },
686
+ parameters: { path: { id: sessionId } }
687
+ });
310
688
  }
311
689
  async forkSession(sessionId, messageId) {
312
- return this.callScopedSdkMethod("session", "fork", { parameters: {
313
- sessionID: sessionId,
314
- ...messageId?.trim() ? { messageID: messageId.trim() } : {}
315
- } });
690
+ return this.callScopedSdkMethod("session", "fork", {
691
+ legacyParameters: {
692
+ sessionID: sessionId,
693
+ ...messageId?.trim() ? { messageID: messageId.trim() } : {}
694
+ },
695
+ parameters: {
696
+ path: { id: sessionId },
697
+ ...messageId?.trim() ? { body: { messageID: messageId.trim() } } : {}
698
+ }
699
+ });
316
700
  }
317
701
  async getPath() {
318
702
  return this.callScopedSdkMethod("path", "get", {});
319
703
  }
320
704
  async listLspStatuses(directory) {
321
- return this.callScopedSdkMethod("lsp", "status", { parameters: directory ? { directory } : void 0 });
705
+ return this.callScopedSdkMethod("lsp", "status", {
706
+ legacyParameters: directory ? { directory } : void 0,
707
+ parameters: directory ? { query: { directory } } : void 0
708
+ });
322
709
  }
323
710
  async listMcpStatuses(directory) {
324
- return this.callScopedSdkMethod("mcp", "status", { parameters: directory ? { directory } : void 0 });
711
+ return this.callScopedSdkMethod("mcp", "status", {
712
+ legacyParameters: directory ? { directory } : void 0,
713
+ parameters: directory ? { query: { directory } } : void 0
714
+ });
325
715
  }
326
716
  async getSessionStatuses() {
327
717
  return this.loadSessionStatuses();
@@ -336,29 +726,69 @@ var OpenCodeClient = class {
336
726
  return this.callScopedSdkMethod("project", "current", {});
337
727
  }
338
728
  async createSessionForDirectory(directory, title) {
339
- return this.callScopedSdkMethod("session", "create", { parameters: title ? {
340
- directory,
341
- title
342
- } : { directory } });
729
+ return this.callScopedSdkMethod("session", "create", {
730
+ legacyParameters: title ? {
731
+ directory,
732
+ title
733
+ } : { directory },
734
+ parameters: {
735
+ query: { directory },
736
+ ...title ? { body: { title } } : {}
737
+ }
738
+ });
343
739
  }
344
740
  async renameSession(sessionId, title) {
345
- return this.callScopedSdkMethod("session", "update", { parameters: {
346
- sessionID: sessionId,
347
- title
348
- } });
741
+ return this.callScopedSdkMethod("session", "update", {
742
+ legacyParameters: {
743
+ sessionID: sessionId,
744
+ title
745
+ },
746
+ parameters: {
747
+ path: { id: sessionId },
748
+ body: { title }
749
+ }
750
+ });
349
751
  }
350
752
  async listAgents() {
351
753
  return this.callScopedSdkMethod("app", "agents", {});
352
754
  }
353
755
  async listPendingPermissions(directory) {
354
- return this.callScopedSdkMethod("permission", "list", { parameters: directory ? { directory } : void 0 });
355
- }
356
- async replyToPermission(requestId, reply, message, _sessionId) {
357
- return this.callScopedSdkMethod("permission", "reply", { parameters: {
358
- requestID: requestId,
359
- reply,
360
- ...message?.trim() ? { message: message.trim() } : {}
361
- } });
756
+ return (await this.callScopedSdkMethod("permission", "list", {
757
+ legacyParameters: directory ? { directory } : void 0,
758
+ parameters: directory ? { query: { directory } } : void 0
759
+ })).map(normalizePermissionRequest).filter((permission) => !!permission);
760
+ }
761
+ async replyToPermission(requestId, reply, message, sessionId) {
762
+ const normalizedMessage = message?.trim();
763
+ const rootPermissionHandler = this.client.postSessionIdPermissionsPermissionId;
764
+ if (sessionId && typeof rootPermissionHandler === "function") return unwrapSdkData(await rootPermissionHandler.call(this.client, {
765
+ ...SDK_OPTIONS,
766
+ body: {
767
+ response: reply,
768
+ ...normalizedMessage ? { message: normalizedMessage } : {}
769
+ },
770
+ path: {
771
+ id: sessionId,
772
+ permissionID: requestId
773
+ }
774
+ }));
775
+ return this.callScopedSdkMethod("permission", "reply", {
776
+ legacyParameters: {
777
+ requestID: requestId,
778
+ reply,
779
+ ...normalizedMessage ? { message: normalizedMessage } : {}
780
+ },
781
+ parameters: sessionId ? {
782
+ body: {
783
+ response: reply,
784
+ ...normalizedMessage ? { message: normalizedMessage } : {}
785
+ },
786
+ path: {
787
+ id: sessionId,
788
+ permissionID: requestId
789
+ }
790
+ } : void 0
791
+ });
362
792
  }
363
793
  async listModels() {
364
794
  const now = Date.now();
@@ -494,10 +924,14 @@ var OpenCodeClient = class {
494
924
  messageId
495
925
  }, async (requestSignal) => {
496
926
  return normalizePromptResponse(await this.callScopedSdkMethod("session", "message", {
497
- parameters: {
927
+ legacyParameters: {
498
928
  sessionID: sessionId,
499
929
  messageID: messageId
500
930
  },
931
+ parameters: { path: {
932
+ id: sessionId,
933
+ messageID: messageId
934
+ } },
501
935
  signal: requestSignal
502
936
  }));
503
937
  }, signal);
@@ -525,10 +959,14 @@ var OpenCodeClient = class {
525
959
  timeoutMs: this.promptTimeoutPolicy.pollRequestTimeoutMs
526
960
  }, async (requestSignal) => {
527
961
  return normalizePromptResponses(await this.callScopedSdkMethod("session", "messages", {
528
- parameters: {
962
+ legacyParameters: {
529
963
  sessionID: sessionId,
530
964
  limit: PROMPT_MESSAGE_POLL_LIMIT
531
965
  },
966
+ parameters: {
967
+ path: { id: sessionId },
968
+ query: { limit: PROMPT_MESSAGE_POLL_LIMIT }
969
+ },
532
970
  signal: requestSignal
533
971
  }));
534
972
  }, signal);
@@ -589,10 +1027,14 @@ var OpenCodeClient = class {
589
1027
  ...input.variant ? { variant: input.variant } : {},
590
1028
  parts
591
1029
  };
592
- const requestParameters = {
1030
+ const legacyRequestParameters = {
593
1031
  sessionID: input.sessionId,
594
1032
  ...requestBody
595
1033
  };
1034
+ const requestParameters = {
1035
+ body: requestBody,
1036
+ path: { id: input.sessionId }
1037
+ };
596
1038
  try {
597
1039
  if (typeof this.client.session?.promptAsync === "function") {
598
1040
  await this.runPromptRequestWithTimeout({
@@ -601,6 +1043,7 @@ var OpenCodeClient = class {
601
1043
  timeoutMs: this.promptTimeoutPolicy.waitTimeoutMs
602
1044
  }, async (signal) => {
603
1045
  await this.callScopedSdkMethod("session", "promptAsync", {
1046
+ legacyParameters: legacyRequestParameters,
604
1047
  parameters: requestParameters,
605
1048
  signal
606
1049
  });
@@ -618,10 +1061,7 @@ var OpenCodeClient = class {
618
1061
  throw new Error("OpenCode SDK client does not expose session.promptAsync().");
619
1062
  }
620
1063
  async loadSessionStatuses(signal) {
621
- return this.callScopedSdkMethod("session", "status", {
622
- signal,
623
- parameters: void 0
624
- });
1064
+ return this.callScopedSdkMethod("session", "status", { signal });
625
1065
  }
626
1066
  async callRawSdkGet(url, signal) {
627
1067
  const rawClient = getRawSdkRequestClient(this.client);
@@ -698,12 +1138,16 @@ var OpenCodeClient = class {
698
1138
  logPromptRequest(level, extra, message) {
699
1139
  const log = this.client.app?.log;
700
1140
  if (typeof log !== "function") return;
701
- log.call(this.client.app, {
1141
+ const payload = {
702
1142
  service: PROMPT_LOG_SERVICE,
703
1143
  level,
704
1144
  message,
705
1145
  extra
706
- }).catch(() => void 0);
1146
+ };
1147
+ (log.length >= 2 ? log.call(this.client.app, payload, SDK_OPTIONS) : log.call(this.client.app, {
1148
+ body: payload,
1149
+ ...SDK_OPTIONS
1150
+ })).catch(() => void 0);
707
1151
  }
708
1152
  };
709
1153
  function createOpenCodeClientFromSdkClient(client, fetchFn = fetch, promptTimeoutPolicy = {}) {
@@ -825,7 +1269,7 @@ function isPromptResponseUsable(data, structured) {
825
1269
  }
826
1270
  function normalizePromptResponse(response) {
827
1271
  return {
828
- info: isPlainRecord(response?.info) ? response.info : null,
1272
+ info: isPlainRecord$1(response?.info) ? response.info : null,
829
1273
  parts: normalizePromptParts(response?.parts)
830
1274
  };
831
1275
  }
@@ -849,7 +1293,7 @@ function toAssistantMessage(message) {
849
1293
  if ("mode" in message && typeof message.mode === "string" && message.mode.trim().length > 0) normalized.mode = message.mode;
850
1294
  if ("modelID" in message && typeof message.modelID === "string" && message.modelID.trim().length > 0) normalized.modelID = message.modelID;
851
1295
  if ("parentID" in message && typeof message.parentID === "string" && message.parentID.trim().length > 0) normalized.parentID = message.parentID;
852
- if ("path" in message && isPlainRecord(message.path)) normalized.path = {
1296
+ if ("path" in message && isPlainRecord$1(message.path)) normalized.path = {
853
1297
  ...typeof message.path.cwd === "string" && message.path.cwd.trim().length > 0 ? { cwd: message.path.cwd } : {},
854
1298
  ...typeof message.path.root === "string" && message.path.root.trim().length > 0 ? { root: message.path.root } : {}
855
1299
  };
@@ -859,16 +1303,16 @@ function toAssistantMessage(message) {
859
1303
  const structuredPayload = extractStructuredPayload(message);
860
1304
  if (structuredPayload !== null) normalized.structured = structuredPayload;
861
1305
  if ("summary" in message && typeof message.summary === "boolean") normalized.summary = message.summary;
862
- if ("time" in message && isPlainRecord(message.time)) normalized.time = {
1306
+ if ("time" in message && isPlainRecord$1(message.time)) normalized.time = {
863
1307
  ...typeof message.time.created === "number" && Number.isFinite(message.time.created) ? { created: message.time.created } : {},
864
1308
  ...typeof message.time.completed === "number" && Number.isFinite(message.time.completed) ? { completed: message.time.completed } : {}
865
1309
  };
866
- if ("tokens" in message && isPlainRecord(message.tokens)) normalized.tokens = {
1310
+ if ("tokens" in message && isPlainRecord$1(message.tokens)) normalized.tokens = {
867
1311
  ...typeof message.tokens.input === "number" && Number.isFinite(message.tokens.input) ? { input: message.tokens.input } : {},
868
1312
  ...typeof message.tokens.output === "number" && Number.isFinite(message.tokens.output) ? { output: message.tokens.output } : {},
869
1313
  ...typeof message.tokens.reasoning === "number" && Number.isFinite(message.tokens.reasoning) ? { reasoning: message.tokens.reasoning } : {},
870
1314
  ...typeof message.tokens.total === "number" && Number.isFinite(message.tokens.total) ? { total: message.tokens.total } : {},
871
- ...isPlainRecord(message.tokens.cache) ? { cache: {
1315
+ ...isPlainRecord$1(message.tokens.cache) ? { cache: {
872
1316
  ...typeof message.tokens.cache.read === "number" && Number.isFinite(message.tokens.cache.read) ? { read: message.tokens.cache.read } : {},
873
1317
  ...typeof message.tokens.cache.write === "number" && Number.isFinite(message.tokens.cache.write) ? { write: message.tokens.cache.write } : {}
874
1318
  } } : {}
@@ -877,7 +1321,7 @@ function toAssistantMessage(message) {
877
1321
  return normalized;
878
1322
  }
879
1323
  function extractMessageId(message) {
880
- if (!isPlainRecord(message)) return null;
1324
+ if (!isPlainRecord$1(message)) return null;
881
1325
  return typeof message.id === "string" && message.id.trim().length > 0 ? message.id : null;
882
1326
  }
883
1327
  function delay(ms, signal) {
@@ -973,6 +1417,27 @@ function unwrapSdkData(response) {
973
1417
  if (response && typeof response === "object" && "data" in response) return response.data;
974
1418
  return response;
975
1419
  }
1420
+ function normalizePermissionRequest(permission) {
1421
+ if (!isPlainRecord$1(permission)) return null;
1422
+ const id = typeof permission.id === "string" && permission.id.trim().length > 0 ? permission.id : null;
1423
+ const sessionID = typeof permission.sessionID === "string" && permission.sessionID.trim().length > 0 ? permission.sessionID : null;
1424
+ const permissionName = typeof permission.permission === "string" && permission.permission.trim().length > 0 ? permission.permission : typeof permission.type === "string" && permission.type.trim().length > 0 ? permission.type : null;
1425
+ if (!id || !sessionID || !permissionName) return null;
1426
+ return {
1427
+ always: Array.isArray(permission.always) ? permission.always.filter((value) => typeof value === "string") : [],
1428
+ id,
1429
+ metadata: isPlainRecord$1(permission.metadata) ? permission.metadata : {},
1430
+ patterns: normalizePermissionPatterns$1(permission),
1431
+ permission: permissionName,
1432
+ sessionID
1433
+ };
1434
+ }
1435
+ function normalizePermissionPatterns$1(permission) {
1436
+ if (Array.isArray(permission.patterns)) return permission.patterns.filter((value) => typeof value === "string");
1437
+ if (typeof permission.pattern === "string" && permission.pattern.trim().length > 0) return [permission.pattern];
1438
+ if (Array.isArray(permission.pattern)) return permission.pattern.filter((value) => typeof value === "string");
1439
+ return [];
1440
+ }
976
1441
  function getRawSdkRequestClient(client) {
977
1442
  const compatibleClient = client;
978
1443
  return compatibleClient.client ?? compatibleClient._client ?? null;
@@ -988,11 +1453,11 @@ function resolvePromptTimeoutPolicy(input) {
988
1453
  };
989
1454
  }
990
1455
  function normalizeAssistantError(value) {
991
- if (!isPlainRecord(value) || typeof value.name !== "string" || value.name.trim().length === 0) return;
1456
+ if (!isPlainRecord$1(value) || typeof value.name !== "string" || value.name.trim().length === 0) return;
992
1457
  return {
993
1458
  ...value,
994
1459
  name: value.name,
995
- ...isPlainRecord(value.data) ? { data: value.data } : {}
1460
+ ...isPlainRecord$1(value.data) ? { data: value.data } : {}
996
1461
  };
997
1462
  }
998
1463
  function isAssistantMessageCompleted(message) {
@@ -1005,7 +1470,7 @@ function isCompletedEmptyPromptResponse(data, structured) {
1005
1470
  return isAssistantMessageCompleted(assistantInfo) && !assistantInfo?.error && !hasText && !bodyMd;
1006
1471
  }
1007
1472
  function extractStructuredPayload(message) {
1008
- if (!isPlainRecord(message)) return null;
1473
+ if (!isPlainRecord$1(message)) return null;
1009
1474
  if ("structured" in message && message.structured !== void 0) return message.structured;
1010
1475
  if ("structured_output" in message && message.structured_output !== void 0) return message.structured_output;
1011
1476
  return null;
@@ -1079,7 +1544,7 @@ function throwIfAborted(signal) {
1079
1544
  if (!signal?.aborted) return;
1080
1545
  throw normalizeAbortReason(signal.reason);
1081
1546
  }
1082
- function isPlainRecord(value) {
1547
+ function isPlainRecord$1(value) {
1083
1548
  return value !== null && typeof value === "object" && !Array.isArray(value);
1084
1549
  }
1085
1550
  async function resolveProviderAvailability(config, fetchFn) {
@@ -1872,10 +2337,11 @@ var SendPromptUseCase = class {
1872
2337
  binding
1873
2338
  });
1874
2339
  binding = createdSession.binding;
1875
- this.logger.info({
2340
+ logPromptLifecycle(this.logger, {
1876
2341
  chatId: input.chatId,
1877
- sessionId: createdSession.session.id,
2342
+ event: "prompt.session.created",
1878
2343
  projectId: createdSession.session.projectID,
2344
+ sessionId: createdSession.session.id,
1879
2345
  directory: createdSession.session.directory
1880
2346
  }, "session created");
1881
2347
  }
@@ -1883,11 +2349,15 @@ var SendPromptUseCase = class {
1883
2349
  const selectedModel = (await this.opencodeClient.listModels()).find((model) => model.providerID === binding?.modelProviderId && model.id === binding?.modelId);
1884
2350
  if (!selectedModel) {
1885
2351
  binding = await clearStoredModelSelection(this.sessionRepo, binding);
1886
- 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");
1887
2356
  } else if (binding.modelVariant && !(binding.modelVariant in selectedModel.variants)) {
1888
2357
  binding = await clearStoredModelVariant(this.sessionRepo, binding);
1889
2358
  this.logger.warn?.({
1890
2359
  chatId: input.chatId,
2360
+ event: "prompt.model.variant_unavailable",
1891
2361
  providerId: selectedModel.providerID,
1892
2362
  modelId: selectedModel.id
1893
2363
  }, "selected model variant is no longer available, falling back to default variant");
@@ -1903,13 +2373,24 @@ var SendPromptUseCase = class {
1903
2373
  const selectedAgent = resolveSelectedAgent(await this.opencodeClient.listAgents(), activeBinding.agentName);
1904
2374
  if (activeBinding.agentName && selectedAgent?.name !== activeBinding.agentName) {
1905
2375
  activeBinding = await clearStoredAgentSelection(this.sessionRepo, activeBinding);
1906
- 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");
1907
2380
  }
1908
2381
  const temporarySessionId = shouldIsolateImageTurn ? await this.createTemporaryImageSession(input.chatId, activeBinding.sessionId) : null;
1909
2382
  const executionSessionId = temporarySessionId ?? activeBinding.sessionId;
1910
2383
  input.onExecutionSession?.(executionSessionId);
1911
2384
  let result;
1912
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");
1913
2394
  result = await this.opencodeClient.promptSession({
1914
2395
  sessionId: executionSessionId,
1915
2396
  prompt: promptText,
@@ -1920,6 +2401,13 @@ var SendPromptUseCase = class {
1920
2401
  ...input.signal ? { signal: input.signal } : {},
1921
2402
  ...activeBinding.modelVariant ? { variant: activeBinding.modelVariant } : {}
1922
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");
1923
2411
  } finally {
1924
2412
  if (temporarySessionId) await this.cleanupTemporaryImageSession(input.chatId, activeBinding.sessionId, temporarySessionId);
1925
2413
  }
@@ -1932,14 +2420,18 @@ var SendPromptUseCase = class {
1932
2420
  }
1933
2421
  async clearInvalidSessionContext(chatId, binding, reason) {
1934
2422
  const nextBinding = await clearStoredSessionContext(this.sessionRepo, binding);
1935
- 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`);
1936
2427
  return nextBinding;
1937
2428
  }
1938
2429
  async createTemporaryImageSession(chatId, sessionId) {
1939
2430
  const temporarySession = await this.opencodeClient.forkSession(sessionId);
1940
2431
  if (!temporarySession.id || temporarySession.id === sessionId) throw new Error("OpenCode did not return a distinct temporary session for the image turn.");
1941
- this.logger.info?.({
2432
+ logPromptLifecycle(this.logger, {
1942
2433
  chatId,
2434
+ event: "prompt.temporary_session.created",
1943
2435
  parentSessionId: sessionId,
1944
2436
  sessionId: temporarySession.id
1945
2437
  }, "created temporary image session");
@@ -1949,6 +2441,7 @@ var SendPromptUseCase = class {
1949
2441
  try {
1950
2442
  if (!await this.opencodeClient.deleteSession(sessionId)) this.logger.warn?.({
1951
2443
  chatId,
2444
+ event: "prompt.temporary_session.cleanup_failed",
1952
2445
  parentSessionId,
1953
2446
  sessionId
1954
2447
  }, "failed to delete temporary image session");
@@ -1956,6 +2449,7 @@ var SendPromptUseCase = class {
1956
2449
  this.logger.warn?.({
1957
2450
  error,
1958
2451
  chatId,
2452
+ event: "prompt.temporary_session.cleanup_failed",
1959
2453
  parentSessionId,
1960
2454
  sessionId
1961
2455
  }, "failed to delete temporary image session");
@@ -2156,7 +2650,23 @@ function resolveExtension(mimeType) {
2156
2650
  //#endregion
2157
2651
  //#region src/app/container.ts
2158
2652
  function createAppContainer(config, client) {
2159
- 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
+ });
2160
2670
  return createContainer(config, createOpenCodeClientFromSdkClient(client, fetch, {
2161
2671
  waitTimeoutMs: config.promptWaitTimeoutMs,
2162
2672
  pollRequestTimeoutMs: config.promptPollRequestTimeoutMs,
@@ -2164,6 +2674,9 @@ function createAppContainer(config, client) {
2164
2674
  }), logger);
2165
2675
  }
2166
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" });
2167
2680
  const stateStore = new JsonStateStore({
2168
2681
  filePath: config.stateFilePath,
2169
2682
  createDefaultState: createDefaultOpencodeTbotState
@@ -2178,7 +2691,7 @@ function createContainer(config, opencodeClient, logger) {
2178
2691
  });
2179
2692
  const uploadFileUseCase = new UploadFileUseCase(telegramFileClient);
2180
2693
  const abortPromptUseCase = new AbortPromptUseCase(sessionRepo, opencodeClient, foregroundSessionTracker);
2181
- const createSessionUseCase = new CreateSessionUseCase(sessionRepo, opencodeClient, logger);
2694
+ const createSessionUseCase = new CreateSessionUseCase(sessionRepo, opencodeClient, opencodeLogger);
2182
2695
  const getHealthUseCase = new GetHealthUseCase(opencodeClient);
2183
2696
  const getPathUseCase = new GetPathUseCase(opencodeClient);
2184
2697
  const listAgentsUseCase = new ListAgentsUseCase(sessionRepo, opencodeClient);
@@ -2187,11 +2700,11 @@ function createContainer(config, opencodeClient, logger) {
2187
2700
  const listSessionsUseCase = new ListSessionsUseCase(sessionRepo, opencodeClient);
2188
2701
  const getStatusUseCase = new GetStatusUseCase(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo);
2189
2702
  const listModelsUseCase = new ListModelsUseCase(sessionRepo, opencodeClient);
2190
- const renameSessionUseCase = new RenameSessionUseCase(sessionRepo, opencodeClient, logger);
2191
- const sendPromptUseCase = new SendPromptUseCase(sessionRepo, opencodeClient, logger);
2192
- const switchAgentUseCase = new SwitchAgentUseCase(sessionRepo, opencodeClient, logger);
2193
- const switchModelUseCase = new SwitchModelUseCase(sessionRepo, opencodeClient, logger);
2194
- 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);
2195
2708
  let disposed = false;
2196
2709
  return {
2197
2710
  abortPromptUseCase,
@@ -2220,7 +2733,10 @@ function createContainer(config, opencodeClient, logger) {
2220
2733
  async dispose() {
2221
2734
  if (disposed) return;
2222
2735
  disposed = true;
2223
- 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");
2224
2740
  await logger.flush();
2225
2741
  }
2226
2742
  };
@@ -2295,25 +2811,40 @@ function escapeMarkdownV2(value) {
2295
2811
  async function handleTelegramBotPluginEvent(runtime, event) {
2296
2812
  switch (event.type) {
2297
2813
  case "permission.asked":
2298
- await handlePermissionAsked(runtime, event);
2814
+ case "permission.updated": {
2815
+ const request = normalizePermissionRequestEvent(event.properties);
2816
+ if (request) await handlePermissionAsked(runtime, request);
2299
2817
  return;
2300
- case "permission.replied":
2301
- await handlePermissionReplied(runtime, event);
2818
+ }
2819
+ case "permission.replied": {
2820
+ const replyEvent = normalizePermissionReplyEvent(event.properties);
2821
+ if (replyEvent) await handlePermissionReplied(runtime, replyEvent);
2302
2822
  return;
2303
- case "session.error":
2304
- await handleSessionError(runtime, event);
2823
+ }
2824
+ case "session.error": {
2825
+ const sessionError = normalizeSessionErrorEvent(event.properties);
2826
+ if (sessionError) await handleSessionError(runtime, sessionError);
2305
2827
  return;
2306
- case "session.idle":
2307
- await handleSessionIdle(runtime, event);
2828
+ }
2829
+ case "session.idle": {
2830
+ const sessionIdle = normalizeSessionIdleEvent(event.properties);
2831
+ if (sessionIdle) await handleSessionIdle(runtime, sessionIdle);
2308
2832
  return;
2309
- case "session.status":
2310
- await handleSessionStatus(runtime, event);
2833
+ }
2834
+ case "session.status": {
2835
+ const sessionStatus = normalizeSessionStatusEvent(event.properties);
2836
+ if (sessionStatus) await handleSessionStatus(runtime, sessionStatus);
2311
2837
  return;
2838
+ }
2312
2839
  default: return;
2313
2840
  }
2314
2841
  }
2315
- async function handlePermissionAsked(runtime, event) {
2316
- const request = event.properties;
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
+ });
2317
2848
  const bindings = await runtime.container.sessionRepo.listBySessionId(request.sessionID);
2318
2849
  const chatIds = new Set([...bindings.map((binding) => binding.chatId), ...runtime.container.foregroundSessionTracker.listChatIds(request.sessionID)]);
2319
2850
  const approvals = await runtime.container.permissionApprovalRepo.listByRequestId(request.id);
@@ -2334,73 +2865,81 @@ async function handlePermissionAsked(runtime, event) {
2334
2865
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2335
2866
  });
2336
2867
  } catch (error) {
2337
- runtime.container.logger.error({
2868
+ logger.error({
2338
2869
  error,
2339
2870
  chatId,
2871
+ event: "plugin-event.permission.ask.delivery_failed",
2340
2872
  requestId: request.id
2341
2873
  }, "failed to deliver permission request to Telegram");
2342
2874
  }
2343
2875
  }
2344
2876
  }
2345
2877
  async function handlePermissionReplied(runtime, event) {
2346
- const requestId = event.properties.requestID;
2347
- const reply = event.properties.reply;
2348
- const approvals = await runtime.container.permissionApprovalRepo.listByRequestId(requestId);
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
+ });
2884
+ const approvals = await runtime.container.permissionApprovalRepo.listByRequestId(event.requestId);
2349
2885
  await Promise.all(approvals.map(async (approval) => {
2350
2886
  try {
2351
- await runtime.bot.api.editMessageText(approval.chatId, approval.messageId, buildPermissionApprovalResolvedMessage(requestId, reply));
2887
+ await runtime.bot.api.editMessageText(approval.chatId, approval.messageId, buildPermissionApprovalResolvedMessage(event.requestId, event.reply));
2352
2888
  } catch (error) {
2353
- runtime.container.logger.warn({
2889
+ logger.warn({
2354
2890
  error,
2355
2891
  chatId: approval.chatId,
2356
- requestId
2892
+ event: "plugin-event.permission.reply_message_failed"
2357
2893
  }, "failed to update Telegram permission message");
2358
2894
  }
2359
- await runtime.container.permissionApprovalRepo.set(toResolvedApproval(approval, reply));
2895
+ await runtime.container.permissionApprovalRepo.set(toResolvedApproval(approval, event.reply));
2360
2896
  }));
2361
2897
  }
2362
2898
  async function handleSessionError(runtime, event) {
2363
- const sessionId = event.properties.sessionID;
2364
- const error = event.properties.error;
2365
- if (!sessionId) {
2366
- runtime.container.logger.error({ error }, "session error received without a session id");
2367
- return;
2368
- }
2369
- if (runtime.container.foregroundSessionTracker.fail(sessionId, error ?? /* @__PURE__ */ new Error("Unknown session error."))) {
2370
- runtime.container.logger.warn({
2371
- error,
2372
- sessionId
2899
+ const logger = runtime.container.logger.child({
2900
+ component: "plugin-event",
2901
+ sessionId: event.sessionId
2902
+ });
2903
+ if (runtime.container.foregroundSessionTracker.fail(event.sessionId, event.error instanceof Error ? event.error : /* @__PURE__ */ new Error("Unknown session error."))) {
2904
+ logger.warn({
2905
+ error: event.error,
2906
+ event: "plugin-event.session.error.foreground_suppressed"
2373
2907
  }, "session error suppressed for foreground Telegram session");
2374
2908
  return;
2375
2909
  }
2376
- await notifyBoundChats(runtime, sessionId, `Session failed.\n\nSession: ${sessionId}\nError: ${(typeof error?.data?.message === "string" ? error.data.message.trim() : "") || error?.name?.trim() || "Unknown session error."}`);
2910
+ const message = extractSessionErrorMessage(event.error) ?? "Unknown session error.";
2911
+ await notifyBoundChats(runtime, event.sessionId, `Session failed.\n\nSession: ${event.sessionId}\nError: ${message}`);
2377
2912
  }
2378
2913
  async function handleSessionIdle(runtime, event) {
2379
- const sessionId = event.properties.sessionID;
2380
- if (runtime.container.foregroundSessionTracker.clear(sessionId)) {
2381
- runtime.container.logger.info({ sessionId }, "session idle notification suppressed for foreground Telegram session");
2914
+ const logger = runtime.container.logger.child({
2915
+ component: "plugin-event",
2916
+ sessionId: event.sessionId
2917
+ });
2918
+ if (runtime.container.foregroundSessionTracker.clear(event.sessionId)) {
2919
+ logPluginEvent(logger, { event: "plugin-event.session.idle.foreground_suppressed" }, "session idle notification suppressed for foreground Telegram session");
2382
2920
  return;
2383
2921
  }
2384
- await notifyBoundChats(runtime, sessionId, `Session finished.\n\nSession: ${sessionId}`);
2922
+ await notifyBoundChats(runtime, event.sessionId, `Session finished.\n\nSession: ${event.sessionId}`);
2385
2923
  }
2386
2924
  async function handleSessionStatus(runtime, event) {
2387
- if (event.properties.status.type !== "idle") return;
2388
- await handleSessionIdle(runtime, {
2389
- type: "session.idle",
2390
- properties: { sessionID: event.properties.sessionID }
2391
- });
2925
+ if (event.statusType !== "idle") return;
2926
+ await handleSessionIdle(runtime, event);
2392
2927
  }
2393
2928
  async function notifyBoundChats(runtime, sessionId, text) {
2929
+ const logger = runtime.container.logger.child({
2930
+ component: "plugin-event",
2931
+ sessionId
2932
+ });
2394
2933
  const bindings = await runtime.container.sessionRepo.listBySessionId(sessionId);
2395
2934
  const chatIds = [...new Set(bindings.map((binding) => binding.chatId))];
2396
2935
  await Promise.all(chatIds.map(async (chatId) => {
2397
2936
  try {
2398
2937
  await runtime.bot.api.sendMessage(chatId, text);
2399
2938
  } catch (error) {
2400
- runtime.container.logger.warn({
2939
+ logger.warn({
2401
2940
  error,
2402
2941
  chatId,
2403
- sessionId
2942
+ event: "plugin-event.session.notify_failed"
2404
2943
  }, "failed to notify Telegram chat about session event");
2405
2944
  }
2406
2945
  }));
@@ -2412,6 +2951,79 @@ function toResolvedApproval(approval, reply) {
2412
2951
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2413
2952
  };
2414
2953
  }
2954
+ function normalizePermissionRequestEvent(properties) {
2955
+ if (!isPlainRecord(properties)) return null;
2956
+ const id = asNonEmptyString(properties.id);
2957
+ const sessionID = asNonEmptyString(properties.sessionID);
2958
+ const permission = asNonEmptyString(properties.permission) ?? asNonEmptyString(properties.type);
2959
+ if (!id || !sessionID || !permission) return null;
2960
+ return {
2961
+ always: Array.isArray(properties.always) ? properties.always.filter((value) => typeof value === "string") : [],
2962
+ id,
2963
+ metadata: isPlainRecord(properties.metadata) ? properties.metadata : {},
2964
+ patterns: normalizePermissionPatterns(properties),
2965
+ permission,
2966
+ sessionID
2967
+ };
2968
+ }
2969
+ function normalizePermissionReplyEvent(properties) {
2970
+ if (!isPlainRecord(properties)) return null;
2971
+ const requestId = asNonEmptyString(properties.requestID) ?? asNonEmptyString(properties.permissionID);
2972
+ const reply = normalizePermissionReply(asNonEmptyString(properties.reply) ?? asNonEmptyString(properties.response));
2973
+ const sessionId = asNonEmptyString(properties.sessionID);
2974
+ if (!requestId || !reply || !sessionId) return null;
2975
+ return {
2976
+ reply,
2977
+ requestId,
2978
+ sessionId
2979
+ };
2980
+ }
2981
+ function normalizeSessionErrorEvent(properties) {
2982
+ if (!isPlainRecord(properties)) return null;
2983
+ const sessionId = asNonEmptyString(properties.sessionID);
2984
+ if (!sessionId) return null;
2985
+ return {
2986
+ error: properties.error,
2987
+ sessionId
2988
+ };
2989
+ }
2990
+ function normalizeSessionIdleEvent(properties) {
2991
+ if (!isPlainRecord(properties)) return null;
2992
+ const sessionId = asNonEmptyString(properties.sessionID);
2993
+ return sessionId ? { sessionId } : null;
2994
+ }
2995
+ function normalizeSessionStatusEvent(properties) {
2996
+ if (!isPlainRecord(properties) || !isPlainRecord(properties.status)) return null;
2997
+ const sessionId = asNonEmptyString(properties.sessionID);
2998
+ const statusType = asNonEmptyString(properties.status.type);
2999
+ if (!sessionId || !statusType) return null;
3000
+ return {
3001
+ sessionId,
3002
+ statusType
3003
+ };
3004
+ }
3005
+ function normalizePermissionPatterns(properties) {
3006
+ if (Array.isArray(properties.patterns)) return properties.patterns.filter((value) => typeof value === "string");
3007
+ if (typeof properties.pattern === "string" && properties.pattern.trim().length > 0) return [properties.pattern];
3008
+ if (Array.isArray(properties.pattern)) return properties.pattern.filter((value) => typeof value === "string");
3009
+ return [];
3010
+ }
3011
+ function normalizePermissionReply(value) {
3012
+ if (value === "once" || value === "always" || value === "reject") return value;
3013
+ return null;
3014
+ }
3015
+ function extractSessionErrorMessage(error) {
3016
+ if (error instanceof Error && error.message.trim().length > 0) return error.message.trim();
3017
+ if (!isPlainRecord(error)) return null;
3018
+ if (isPlainRecord(error.data) && typeof error.data.message === "string" && error.data.message.trim().length > 0) return error.data.message.trim();
3019
+ return asNonEmptyString(error.name);
3020
+ }
3021
+ function asNonEmptyString(value) {
3022
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
3023
+ }
3024
+ function isPlainRecord(value) {
3025
+ return value !== null && typeof value === "object" && !Array.isArray(value);
3026
+ }
2415
3027
  var SUPPORTED_BOT_LANGUAGES = [
2416
3028
  "en",
2417
3029
  "zh-CN",
@@ -3104,7 +3716,9 @@ var TELEGRAM_COMMAND_SYNC_SCOPES = [{ type: "default" }, { type: "all_private_ch
3104
3716
  async function syncTelegramCommands(bot, logger) {
3105
3717
  await Promise.all(TELEGRAM_COMMAND_SYNC_SCOPES.map((scope) => bot.api.setMyCommands(TELEGRAM_COMMANDS, { scope })));
3106
3718
  logger.info({
3719
+ component: "runtime",
3107
3720
  commands: TELEGRAM_COMMANDS.map((command) => command.command),
3721
+ event: "runtime.commands.synced",
3108
3722
  scopes: TELEGRAM_COMMAND_SYNC_SCOPES.map((scope) => scope.type)
3109
3723
  }, "telegram commands synced");
3110
3724
  }
@@ -3115,109 +3729,42 @@ async function syncTelegramCommandsForChat(api, chatId, language) {
3115
3729
  } });
3116
3730
  }
3117
3731
  //#endregion
3118
- //#region src/bot/i18n.ts
3119
- async function getChatLanguage(sessionRepo, chatId) {
3120
- if (!chatId) return "en";
3121
- return normalizeBotLanguage((await sessionRepo.getByChatId(chatId))?.language);
3122
- }
3123
- async function getChatCopy(sessionRepo, chatId) {
3124
- return getBotCopy(await getChatLanguage(sessionRepo, chatId));
3125
- }
3126
- async function setChatLanguage(sessionRepo, chatId, language) {
3127
- const binding = await sessionRepo.getByChatId(chatId);
3128
- await sessionRepo.setCurrent({
3129
- chatId,
3130
- sessionId: binding?.sessionId ?? null,
3131
- projectId: binding?.projectId ?? null,
3132
- directory: binding?.directory ?? null,
3133
- agentName: binding?.agentName ?? null,
3134
- modelProviderId: binding?.modelProviderId ?? null,
3135
- modelId: binding?.modelId ?? null,
3136
- modelVariant: binding?.modelVariant ?? null,
3137
- language,
3138
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3139
- });
3140
- }
3141
- var NUMBERED_BUTTONS_PER_ROW = 5;
3142
- function buildModelsKeyboard(models, requestedPage, copy = BOT_COPY) {
3143
- const page = getModelsPage(models, requestedPage);
3144
- const keyboard = buildNumberedKeyboard(page.items, page.startIndex, (_, index) => `model:pick:${page.startIndex + index + 1}`);
3145
- appendPaginationButtons(keyboard, page.page, page.totalPages, "model:page", copy);
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;
3146
3738
  return {
3147
- keyboard,
3148
- page
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
3149
3746
  };
3150
3747
  }
3151
- function buildAgentsKeyboard(agents, requestedPage, copy = BOT_COPY) {
3152
- const page = getAgentsPage(agents, requestedPage);
3153
- const keyboard = buildNumberedKeyboard(page.items, page.startIndex, (_, index) => `agents:select:${page.startIndex + index + 1}`);
3154
- appendPaginationButtons(keyboard, page.page, page.totalPages, "agents:page", copy);
3155
- return {
3156
- keyboard,
3157
- page
3158
- };
3748
+ function scopeLoggerToTelegramContext(logger, ctx, component = "telegram") {
3749
+ return logger.child(buildTelegramLoggerContext(ctx, component));
3159
3750
  }
3160
- function buildSessionsKeyboard(sessions, requestedPage, copy = BOT_COPY) {
3161
- const page = getSessionsPage(sessions, requestedPage);
3162
- const keyboard = buildNumberedKeyboard(page.items, page.startIndex, (session) => `sessions:pick:${page.page}:${session.id}`);
3163
- appendPaginationButtons(keyboard, page.page, page.totalPages, "sessions:page", copy);
3751
+ function scopeDependenciesToTelegramContext(dependencies, ctx, component = "telegram") {
3164
3752
  return {
3165
- keyboard,
3166
- page
3753
+ ...dependencies,
3754
+ logger: scopeLoggerToTelegramContext(dependencies.logger, ctx, component)
3167
3755
  };
3168
3756
  }
3169
- function buildSessionActionKeyboard(sessionId, page, copy = BOT_COPY) {
3170
- return new InlineKeyboard().text(copy.sessions.switchAction, `sessions:switch:${page}:${sessionId}`).text(copy.sessions.renameAction, `sessions:rename:${page}:${sessionId}`).row().text(copy.sessions.backToList, `sessions:back:${page}`);
3171
- }
3172
- function buildModelVariantsKeyboard(variants, modelIndex) {
3173
- return buildNumberedKeyboard(variants, 0, (_, index) => `model:variant:${modelIndex}:${index + 1}`);
3174
- }
3175
- function buildLanguageKeyboard(currentLanguage, copy = BOT_COPY) {
3176
- const keyboard = new InlineKeyboard();
3177
- SUPPORTED_BOT_LANGUAGES.forEach((language, index) => {
3178
- const label = currentLanguage === language ? `[${getLanguageLabel(language, copy)}]` : getLanguageLabel(language, copy);
3179
- keyboard.text(label, `language:select:${language}`);
3180
- if (index !== SUPPORTED_BOT_LANGUAGES.length - 1) keyboard.row();
3181
- });
3182
- return keyboard;
3757
+ function resolveMessageText(ctx) {
3758
+ return normalizeTelegramString(ctx.message?.text) ?? normalizeTelegramString(ctx.msg?.text);
3183
3759
  }
3184
- function getModelsPage(models, requestedPage) {
3185
- return getPagedItems(models, requestedPage, 10);
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;
3186
3765
  }
3187
- function getAgentsPage(agents, requestedPage) {
3188
- return getPagedItems(agents, requestedPage, 10);
3189
- }
3190
- function getSessionsPage(sessions, requestedPage) {
3191
- return getPagedItems(sessions, requestedPage, 10);
3192
- }
3193
- function buildNumberedKeyboard(items, startIndex, buildCallbackData) {
3194
- const keyboard = new InlineKeyboard();
3195
- items.forEach((item, index) => {
3196
- const displayIndex = startIndex + index + 1;
3197
- keyboard.text(`${displayIndex}`, buildCallbackData(item, index));
3198
- if (index !== items.length - 1 && (index + 1) % NUMBERED_BUTTONS_PER_ROW === 0) keyboard.row();
3199
- });
3200
- return keyboard;
3201
- }
3202
- function appendPaginationButtons(keyboard, page, totalPages, prefix, copy) {
3203
- if (totalPages <= 1) return;
3204
- if (page > 0) keyboard.text(copy.common.previousPage, `${prefix}:${page - 1}`);
3205
- if (page < totalPages - 1) keyboard.text(copy.common.nextPage, `${prefix}:${page + 1}`);
3206
- }
3207
- function getPagedItems(items, requestedPage, pageSize) {
3208
- const totalPages = Math.max(1, Math.ceil(items.length / pageSize));
3209
- const page = clampPage(requestedPage, totalPages);
3210
- const startIndex = page * pageSize;
3211
- return {
3212
- items: items.slice(startIndex, startIndex + pageSize),
3213
- page,
3214
- startIndex,
3215
- totalPages
3216
- };
3217
- }
3218
- function clampPage(page, totalPages) {
3219
- if (!Number.isInteger(page) || page < 0) return 0;
3220
- return Math.min(page, totalPages - 1);
3766
+ function normalizeTelegramString(value) {
3767
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
3221
3768
  }
3222
3769
  //#endregion
3223
3770
  //#region src/bot/presenters/error.presenter.ts
@@ -3334,6 +3881,179 @@ function stringifyUnknown(value) {
3334
3881
  }
3335
3882
  }
3336
3883
  //#endregion
3884
+ //#region src/bot/error-boundary.ts
3885
+ function extractTelegramUpdateContext(ctx) {
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
+ });
3892
+ }
3893
+ async function replyWithDefaultTelegramError(ctx, logger, error) {
3894
+ const text = presentError(error, BOT_COPY);
3895
+ const editMessageText = getFunction(ctx, "editMessageText");
3896
+ const reply = getFunction(ctx, "reply");
3897
+ const callbackData = getNestedString(ctx, ["callbackQuery", "data"]);
3898
+ try {
3899
+ if (typeof callbackData === "string" && editMessageText) {
3900
+ await editMessageText.call(ctx, text);
3901
+ return;
3902
+ }
3903
+ if (reply) await reply.call(ctx, text);
3904
+ } catch (replyError) {
3905
+ logger.warn?.({
3906
+ ...extractTelegramUpdateContext(ctx),
3907
+ error: replyError
3908
+ }, "failed to deliver fallback Telegram error message");
3909
+ }
3910
+ }
3911
+ function getFunction(value, key) {
3912
+ if (!(key in value)) return null;
3913
+ const candidate = value[key];
3914
+ return typeof candidate === "function" ? candidate : null;
3915
+ }
3916
+ function getNestedNumber(value, path) {
3917
+ const candidate = getNestedValue(value, path);
3918
+ return typeof candidate === "number" ? candidate : null;
3919
+ }
3920
+ function getNestedString(value, path) {
3921
+ const candidate = getNestedValue(value, path);
3922
+ return typeof candidate === "string" ? candidate : null;
3923
+ }
3924
+ function getNestedValue(value, path) {
3925
+ let current = value;
3926
+ for (const segment of path) {
3927
+ if (!current || typeof current !== "object" || !(segment in current)) return null;
3928
+ current = current[segment];
3929
+ }
3930
+ return current;
3931
+ }
3932
+ //#endregion
3933
+ //#region src/bot/i18n.ts
3934
+ async function getChatLanguage(sessionRepo, chatId) {
3935
+ if (!chatId) return "en";
3936
+ return normalizeBotLanguage((await sessionRepo.getByChatId(chatId))?.language);
3937
+ }
3938
+ async function getSafeChatLanguage(sessionRepo, chatId, logger) {
3939
+ try {
3940
+ return await getChatLanguage(sessionRepo, chatId);
3941
+ } catch (error) {
3942
+ logger?.warn?.({
3943
+ error,
3944
+ chatId: chatId ?? void 0
3945
+ }, "failed to resolve Telegram chat language; falling back to the default locale");
3946
+ return "en";
3947
+ }
3948
+ }
3949
+ async function getSafeChatCopy(sessionRepo, chatId, logger) {
3950
+ try {
3951
+ return getBotCopy(await getSafeChatLanguage(sessionRepo, chatId, logger));
3952
+ } catch (error) {
3953
+ logger?.warn?.({
3954
+ error,
3955
+ chatId: chatId ?? void 0
3956
+ }, "failed to resolve Telegram copy; falling back to the default locale");
3957
+ return BOT_COPY;
3958
+ }
3959
+ }
3960
+ async function setChatLanguage(sessionRepo, chatId, language) {
3961
+ const binding = await sessionRepo.getByChatId(chatId);
3962
+ await sessionRepo.setCurrent({
3963
+ chatId,
3964
+ sessionId: binding?.sessionId ?? null,
3965
+ projectId: binding?.projectId ?? null,
3966
+ directory: binding?.directory ?? null,
3967
+ agentName: binding?.agentName ?? null,
3968
+ modelProviderId: binding?.modelProviderId ?? null,
3969
+ modelId: binding?.modelId ?? null,
3970
+ modelVariant: binding?.modelVariant ?? null,
3971
+ language,
3972
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3973
+ });
3974
+ }
3975
+ var NUMBERED_BUTTONS_PER_ROW = 5;
3976
+ function buildModelsKeyboard(models, requestedPage, copy = BOT_COPY) {
3977
+ const page = getModelsPage(models, requestedPage);
3978
+ const keyboard = buildNumberedKeyboard(page.items, page.startIndex, (_, index) => `model:pick:${page.startIndex + index + 1}`);
3979
+ appendPaginationButtons(keyboard, page.page, page.totalPages, "model:page", copy);
3980
+ return {
3981
+ keyboard,
3982
+ page
3983
+ };
3984
+ }
3985
+ function buildAgentsKeyboard(agents, requestedPage, copy = BOT_COPY) {
3986
+ const page = getAgentsPage(agents, requestedPage);
3987
+ const keyboard = buildNumberedKeyboard(page.items, page.startIndex, (_, index) => `agents:select:${page.startIndex + index + 1}`);
3988
+ appendPaginationButtons(keyboard, page.page, page.totalPages, "agents:page", copy);
3989
+ return {
3990
+ keyboard,
3991
+ page
3992
+ };
3993
+ }
3994
+ function buildSessionsKeyboard(sessions, requestedPage, copy = BOT_COPY) {
3995
+ const page = getSessionsPage(sessions, requestedPage);
3996
+ const keyboard = buildNumberedKeyboard(page.items, page.startIndex, (session) => `sessions:pick:${page.page}:${session.id}`);
3997
+ appendPaginationButtons(keyboard, page.page, page.totalPages, "sessions:page", copy);
3998
+ return {
3999
+ keyboard,
4000
+ page
4001
+ };
4002
+ }
4003
+ function buildSessionActionKeyboard(sessionId, page, copy = BOT_COPY) {
4004
+ return new InlineKeyboard().text(copy.sessions.switchAction, `sessions:switch:${page}:${sessionId}`).text(copy.sessions.renameAction, `sessions:rename:${page}:${sessionId}`).row().text(copy.sessions.backToList, `sessions:back:${page}`);
4005
+ }
4006
+ function buildModelVariantsKeyboard(variants, modelIndex) {
4007
+ return buildNumberedKeyboard(variants, 0, (_, index) => `model:variant:${modelIndex}:${index + 1}`);
4008
+ }
4009
+ function buildLanguageKeyboard(currentLanguage, copy = BOT_COPY) {
4010
+ const keyboard = new InlineKeyboard();
4011
+ SUPPORTED_BOT_LANGUAGES.forEach((language, index) => {
4012
+ const label = currentLanguage === language ? `[${getLanguageLabel(language, copy)}]` : getLanguageLabel(language, copy);
4013
+ keyboard.text(label, `language:select:${language}`);
4014
+ if (index !== SUPPORTED_BOT_LANGUAGES.length - 1) keyboard.row();
4015
+ });
4016
+ return keyboard;
4017
+ }
4018
+ function getModelsPage(models, requestedPage) {
4019
+ return getPagedItems(models, requestedPage, 10);
4020
+ }
4021
+ function getAgentsPage(agents, requestedPage) {
4022
+ return getPagedItems(agents, requestedPage, 10);
4023
+ }
4024
+ function getSessionsPage(sessions, requestedPage) {
4025
+ return getPagedItems(sessions, requestedPage, 10);
4026
+ }
4027
+ function buildNumberedKeyboard(items, startIndex, buildCallbackData) {
4028
+ const keyboard = new InlineKeyboard();
4029
+ items.forEach((item, index) => {
4030
+ const displayIndex = startIndex + index + 1;
4031
+ keyboard.text(`${displayIndex}`, buildCallbackData(item, index));
4032
+ if (index !== items.length - 1 && (index + 1) % NUMBERED_BUTTONS_PER_ROW === 0) keyboard.row();
4033
+ });
4034
+ return keyboard;
4035
+ }
4036
+ function appendPaginationButtons(keyboard, page, totalPages, prefix, copy) {
4037
+ if (totalPages <= 1) return;
4038
+ if (page > 0) keyboard.text(copy.common.previousPage, `${prefix}:${page - 1}`);
4039
+ if (page < totalPages - 1) keyboard.text(copy.common.nextPage, `${prefix}:${page + 1}`);
4040
+ }
4041
+ function getPagedItems(items, requestedPage, pageSize) {
4042
+ const totalPages = Math.max(1, Math.ceil(items.length / pageSize));
4043
+ const page = clampPage(requestedPage, totalPages);
4044
+ const startIndex = page * pageSize;
4045
+ return {
4046
+ items: items.slice(startIndex, startIndex + pageSize),
4047
+ page,
4048
+ startIndex,
4049
+ totalPages
4050
+ };
4051
+ }
4052
+ function clampPage(page, totalPages) {
4053
+ if (!Number.isInteger(page) || page < 0) return 0;
4054
+ return Math.min(page, totalPages - 1);
4055
+ }
4056
+ //#endregion
3337
4057
  //#region src/bot/presenters/message.presenter.ts
3338
4058
  var VARIANT_ORDER = [
3339
4059
  "minimal",
@@ -3713,7 +4433,7 @@ function formatSessionLabel(session) {
3713
4433
  //#endregion
3714
4434
  //#region src/bot/commands/agents.ts
3715
4435
  async function handleAgentsCommand(ctx, dependencies) {
3716
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4436
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
3717
4437
  try {
3718
4438
  const result = await dependencies.listAgentsUseCase.execute({ chatId: ctx.chat.id });
3719
4439
  if (result.agents.length === 0) {
@@ -3733,13 +4453,13 @@ async function handleAgentsCommand(ctx, dependencies) {
3733
4453
  }
3734
4454
  function registerAgentsCommand(bot, dependencies) {
3735
4455
  bot.command(["agents", "agent"], async (ctx) => {
3736
- await handleAgentsCommand(ctx, dependencies);
4456
+ await handleAgentsCommand(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
3737
4457
  });
3738
4458
  }
3739
4459
  //#endregion
3740
4460
  //#region src/bot/sessions-menu.ts
3741
4461
  async function buildSessionsListView(chatId, requestedPage, dependencies) {
3742
- const copy = await getChatCopy(dependencies.sessionRepo, chatId);
4462
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, chatId, dependencies.logger);
3743
4463
  const result = await dependencies.listSessionsUseCase.execute({ chatId });
3744
4464
  if (result.sessions.length === 0) return {
3745
4465
  copy,
@@ -3798,14 +4518,14 @@ async function getPendingSessionRenameAction(dependencies, chatId) {
3798
4518
  }
3799
4519
  async function replyIfSessionRenamePending(ctx, dependencies) {
3800
4520
  if (!await getPendingSessionRenameAction(dependencies, ctx.chat.id)) return false;
3801
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4521
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
3802
4522
  await ctx.reply(copy.sessions.renamePendingInput);
3803
4523
  return true;
3804
4524
  }
3805
4525
  async function handlePendingSessionRenameText(ctx, dependencies) {
3806
4526
  const pendingAction = await getPendingSessionRenameAction(dependencies, ctx.chat.id);
3807
4527
  if (!pendingAction) return false;
3808
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4528
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
3809
4529
  const title = ctx.message.text?.trim() ?? "";
3810
4530
  if (title.startsWith("/")) {
3811
4531
  await ctx.reply(copy.sessions.renamePendingInput);
@@ -3837,7 +4557,7 @@ async function handlePendingSessionRenameText(ctx, dependencies) {
3837
4557
  async function cancelPendingSessionRename(ctx, dependencies) {
3838
4558
  const pendingAction = await getPendingSessionRenameAction(dependencies, ctx.chat.id);
3839
4559
  if (!pendingAction) return false;
3840
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4560
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
3841
4561
  await dependencies.pendingActionRepo.clear(ctx.chat.id);
3842
4562
  await bestEffortRestoreSessionsList(ctx.api, pendingAction, dependencies);
3843
4563
  await ctx.reply(copy.sessions.renameCancelled);
@@ -3856,7 +4576,7 @@ function isSessionRenamePendingAction(action) {
3856
4576
  //#endregion
3857
4577
  //#region src/bot/commands/cancel.ts
3858
4578
  async function handleCancelCommand(ctx, dependencies) {
3859
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4579
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
3860
4580
  try {
3861
4581
  if (await cancelPendingSessionRename(ctx, dependencies)) return;
3862
4582
  const result = await dependencies.abortPromptUseCase.execute({ chatId: ctx.chat.id });
@@ -3876,14 +4596,14 @@ async function handleCancelCommand(ctx, dependencies) {
3876
4596
  }
3877
4597
  function registerCancelCommand(bot, dependencies) {
3878
4598
  bot.command("cancel", async (ctx) => {
3879
- await handleCancelCommand(ctx, dependencies);
4599
+ await handleCancelCommand(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
3880
4600
  });
3881
4601
  }
3882
4602
  //#endregion
3883
4603
  //#region src/bot/commands/language.ts
3884
4604
  async function handleLanguageCommand(ctx, dependencies) {
3885
- const language = await getChatLanguage(dependencies.sessionRepo, ctx.chat.id);
3886
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4605
+ const language = await getSafeChatLanguage(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
4606
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
3887
4607
  try {
3888
4608
  await syncTelegramCommandsForChat(ctx.api, ctx.chat.id, language);
3889
4609
  await ctx.reply(presentLanguageMessage(language, copy), { reply_markup: buildLanguageKeyboard(language, copy) });
@@ -3893,7 +4613,7 @@ async function handleLanguageCommand(ctx, dependencies) {
3893
4613
  }
3894
4614
  }
3895
4615
  async function switchLanguageForChat(api, chatId, language, dependencies) {
3896
- const currentCopy = await getChatCopy(dependencies.sessionRepo, chatId);
4616
+ const currentCopy = await getSafeChatCopy(dependencies.sessionRepo, chatId, dependencies.logger);
3897
4617
  if (!isBotLanguage(language)) return {
3898
4618
  found: false,
3899
4619
  copy: currentCopy
@@ -3902,7 +4622,7 @@ async function switchLanguageForChat(api, chatId, language, dependencies) {
3902
4622
  await syncTelegramCommandsForChat(api, chatId, language);
3903
4623
  return {
3904
4624
  found: true,
3905
- copy: await getChatCopy(dependencies.sessionRepo, chatId),
4625
+ copy: await getSafeChatCopy(dependencies.sessionRepo, chatId, dependencies.logger),
3906
4626
  language
3907
4627
  };
3908
4628
  }
@@ -3912,7 +4632,7 @@ async function presentLanguageSwitchForChat(chatId, api, language, dependencies)
3912
4632
  found: false,
3913
4633
  copy: result.copy,
3914
4634
  text: result.copy.language.expired,
3915
- keyboard: buildLanguageKeyboard(await getChatLanguage(dependencies.sessionRepo, chatId), result.copy)
4635
+ keyboard: buildLanguageKeyboard(await getSafeChatLanguage(dependencies.sessionRepo, chatId, dependencies.logger), result.copy)
3916
4636
  };
3917
4637
  return {
3918
4638
  found: true,
@@ -3923,13 +4643,13 @@ async function presentLanguageSwitchForChat(chatId, api, language, dependencies)
3923
4643
  }
3924
4644
  function registerLanguageCommand(bot, dependencies) {
3925
4645
  bot.command("language", async (ctx) => {
3926
- await handleLanguageCommand(ctx, dependencies);
4646
+ await handleLanguageCommand(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
3927
4647
  });
3928
4648
  }
3929
4649
  //#endregion
3930
4650
  //#region src/bot/commands/models.ts
3931
4651
  async function handleModelsCommand(ctx, dependencies) {
3932
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4652
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
3933
4653
  try {
3934
4654
  const result = await dependencies.listModelsUseCase.execute({ chatId: ctx.chat.id });
3935
4655
  if (result.models.length === 0) {
@@ -3951,13 +4671,13 @@ async function handleModelsCommand(ctx, dependencies) {
3951
4671
  }
3952
4672
  function registerModelsCommand(bot, dependencies) {
3953
4673
  bot.command(["model", "models"], async (ctx) => {
3954
- await handleModelsCommand(ctx, dependencies);
4674
+ await handleModelsCommand(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
3955
4675
  });
3956
4676
  }
3957
4677
  //#endregion
3958
4678
  //#region src/bot/commands/new.ts
3959
4679
  async function handleNewCommand(ctx, dependencies) {
3960
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4680
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
3961
4681
  try {
3962
4682
  const title = extractSessionTitle(ctx);
3963
4683
  const result = await dependencies.createSessionUseCase.execute({
@@ -3972,7 +4692,7 @@ async function handleNewCommand(ctx, dependencies) {
3972
4692
  }
3973
4693
  function registerNewCommand(bot, dependencies) {
3974
4694
  bot.command("new", async (ctx) => {
3975
- await handleNewCommand(ctx, dependencies);
4695
+ await handleNewCommand(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
3976
4696
  });
3977
4697
  }
3978
4698
  function extractSessionTitle(ctx) {
@@ -4321,7 +5041,7 @@ function escapeLinkDestination(url) {
4321
5041
  //#endregion
4322
5042
  //#region src/bot/commands/status.ts
4323
5043
  async function handleStatusCommand(ctx, dependencies) {
4324
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat?.id);
5044
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat?.id, dependencies.logger);
4325
5045
  try {
4326
5046
  const result = await dependencies.getStatusUseCase.execute({ chatId: ctx.chat?.id ?? 0 });
4327
5047
  const renderedMarkdown = renderMarkdownToTelegramMarkdownV2(presentStatusMarkdownMessage(result, copy));
@@ -4337,13 +5057,13 @@ async function handleStatusCommand(ctx, dependencies) {
4337
5057
  }
4338
5058
  function registerStatusCommand(bot, dependencies) {
4339
5059
  bot.command("status", async (ctx) => {
4340
- await handleStatusCommand(ctx, dependencies);
5060
+ await handleStatusCommand(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
4341
5061
  });
4342
5062
  }
4343
5063
  //#endregion
4344
5064
  //#region src/bot/commands/sessions.ts
4345
5065
  async function handleSessionsCommand(ctx, dependencies) {
4346
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
5066
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
4347
5067
  try {
4348
5068
  await dependencies.pendingActionRepo.clear(ctx.chat.id);
4349
5069
  const view = await buildSessionsListView(ctx.chat.id, 0, dependencies);
@@ -4355,7 +5075,7 @@ async function handleSessionsCommand(ctx, dependencies) {
4355
5075
  }
4356
5076
  function registerSessionsCommand(bot, dependencies) {
4357
5077
  bot.command("sessions", async (ctx) => {
4358
- await handleSessionsCommand(ctx, dependencies);
5078
+ await handleSessionsCommand(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
4359
5079
  });
4360
5080
  }
4361
5081
  //#endregion
@@ -4366,7 +5086,7 @@ function presentStartMarkdownMessage(copy = BOT_COPY) {
4366
5086
  //#endregion
4367
5087
  //#region src/bot/commands/start.ts
4368
5088
  async function handleStartCommand(ctx, dependencies) {
4369
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat?.id);
5089
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat?.id, dependencies.logger);
4370
5090
  const reply = buildTelegramStaticReply(presentStartMarkdownMessage(copy));
4371
5091
  try {
4372
5092
  await ctx.reply(reply.preferred.text, reply.preferred.options);
@@ -4382,7 +5102,7 @@ async function handleStartCommand(ctx, dependencies) {
4382
5102
  }
4383
5103
  function registerStartCommand(bot, dependencies) {
4384
5104
  bot.command("start", async (ctx) => {
4385
- await handleStartCommand(ctx, dependencies);
5105
+ await handleStartCommand(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
4386
5106
  });
4387
5107
  }
4388
5108
  //#endregion
@@ -4404,7 +5124,7 @@ async function handleAgentsCallback(ctx, dependencies) {
4404
5124
  if (!data.startsWith("agents:")) return;
4405
5125
  await ctx.answerCallbackQuery();
4406
5126
  if (!ctx.chat) return;
4407
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
5127
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
4408
5128
  try {
4409
5129
  if (data.startsWith(AGENTS_PAGE_PREFIX)) {
4410
5130
  const requestedPage = Number(data.slice(12));
@@ -4448,7 +5168,7 @@ async function handleModelsCallback(ctx, dependencies) {
4448
5168
  if (!data.startsWith("model:")) return;
4449
5169
  await ctx.answerCallbackQuery();
4450
5170
  if (!ctx.chat) return;
4451
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
5171
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
4452
5172
  try {
4453
5173
  if (data.startsWith(MODEL_PAGE_PREFIX)) {
4454
5174
  const requestedPage = Number(data.slice(11));
@@ -4527,7 +5247,7 @@ async function handleSessionsCallback(ctx, dependencies) {
4527
5247
  if (!data.startsWith("sessions:")) return;
4528
5248
  await ctx.answerCallbackQuery();
4529
5249
  if (!ctx.chat) return;
4530
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
5250
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
4531
5251
  try {
4532
5252
  if (data.startsWith(SESSIONS_PAGE_PREFIX)) {
4533
5253
  const requestedPage = Number(data.slice(14));
@@ -4608,10 +5328,10 @@ async function handleLanguageCallback(ctx, dependencies) {
4608
5328
  if (!data.startsWith("language:")) return;
4609
5329
  await ctx.answerCallbackQuery();
4610
5330
  if (!ctx.chat || !ctx.api) return;
4611
- const currentCopy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
5331
+ const currentCopy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
4612
5332
  try {
4613
5333
  if (!data.startsWith(LANGUAGE_SELECT_PREFIX)) {
4614
- await ctx.editMessageText(presentLanguageMessage(await getChatLanguage(dependencies.sessionRepo, ctx.chat.id), currentCopy), { reply_markup: buildLanguageKeyboard(await getChatLanguage(dependencies.sessionRepo, ctx.chat.id), currentCopy) });
5334
+ await ctx.editMessageText(presentLanguageMessage(await getSafeChatLanguage(dependencies.sessionRepo, ctx.chat.id, dependencies.logger), currentCopy), { reply_markup: buildLanguageKeyboard(await getSafeChatLanguage(dependencies.sessionRepo, ctx.chat.id, dependencies.logger), currentCopy) });
4615
5335
  return;
4616
5336
  }
4617
5337
  const selectedLanguage = data.slice(16);
@@ -4648,19 +5368,19 @@ async function handlePermissionApprovalCallback(ctx, dependencies) {
4648
5368
  }
4649
5369
  function registerCallbackHandler(bot, dependencies) {
4650
5370
  bot.callbackQuery(/^agents:/, async (ctx) => {
4651
- await handleAgentsCallback(ctx, dependencies);
5371
+ await handleAgentsCallback(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
4652
5372
  });
4653
5373
  bot.callbackQuery(/^sessions:/, async (ctx) => {
4654
- await handleSessionsCallback(ctx, dependencies);
5374
+ await handleSessionsCallback(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
4655
5375
  });
4656
5376
  bot.callbackQuery(/^model:/, async (ctx) => {
4657
- await handleModelsCallback(ctx, dependencies);
5377
+ await handleModelsCallback(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
4658
5378
  });
4659
5379
  bot.callbackQuery(/^language:/, async (ctx) => {
4660
- await handleLanguageCallback(ctx, dependencies);
5380
+ await handleLanguageCallback(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
4661
5381
  });
4662
5382
  bot.callbackQuery(/^permission:/, async (ctx) => {
4663
- await handlePermissionApprovalCallback(ctx, dependencies);
5383
+ await handlePermissionApprovalCallback(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
4664
5384
  });
4665
5385
  }
4666
5386
  function parseSessionActionTarget(data, prefix) {
@@ -4678,7 +5398,7 @@ function parseSessionActionTarget(data, prefix) {
4678
5398
  //#endregion
4679
5399
  //#region src/bot/handlers/prompt.handler.ts
4680
5400
  async function executePromptRequest(ctx, dependencies, resolvePrompt) {
4681
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
5401
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
4682
5402
  const foregroundRequest = dependencies.foregroundSessionTracker.acquire(ctx.chat.id);
4683
5403
  if (!foregroundRequest) {
4684
5404
  await ctx.reply(copy.status.alreadyProcessing);
@@ -4762,10 +5482,10 @@ async function handleImageMessage(ctx, dependencies) {
4762
5482
  }
4763
5483
  function registerFileHandler(bot, dependencies) {
4764
5484
  bot.on("message:photo", async (ctx) => {
4765
- await handleImageMessage(ctx, dependencies);
5485
+ await handleImageMessage(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
4766
5486
  });
4767
5487
  bot.on("message:document", async (ctx) => {
4768
- await handleImageMessage(ctx, dependencies);
5488
+ await handleImageMessage(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
4769
5489
  });
4770
5490
  }
4771
5491
  function resolveTelegramImage(message) {
@@ -4807,7 +5527,7 @@ async function handleTextMessage(ctx, dependencies) {
4807
5527
  }
4808
5528
  function registerMessageHandler(bot, dependencies) {
4809
5529
  bot.on("message:text", async (ctx) => {
4810
- await handleTextMessage(ctx, dependencies);
5530
+ await handleTextMessage(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
4811
5531
  });
4812
5532
  }
4813
5533
  //#endregion
@@ -4815,12 +5535,12 @@ function registerMessageHandler(bot, dependencies) {
4815
5535
  async function handleVoiceMessage(ctx, dependencies) {
4816
5536
  if (!ctx.message.voice) return;
4817
5537
  if (await replyIfSessionRenamePending(ctx, dependencies)) return;
4818
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
5538
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
4819
5539
  await ctx.reply(copy.errors.voiceUnsupported);
4820
5540
  }
4821
5541
  function registerVoiceHandler(bot, dependencies) {
4822
5542
  bot.on("message:voice", async (ctx) => {
4823
- await handleVoiceMessage(ctx, dependencies);
5543
+ await handleVoiceMessage(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
4824
5544
  });
4825
5545
  }
4826
5546
  //#endregion
@@ -4841,17 +5561,18 @@ function createAuthMiddleware(allowedChatIds) {
4841
5561
  function buildIncomingUpdateLogFields(ctx) {
4842
5562
  const messageText = ctx.msg && "text" in ctx.msg ? ctx.msg.text : void 0;
4843
5563
  return {
5564
+ ...buildTelegramLoggerContext(ctx),
5565
+ event: "telegram.update.received",
4844
5566
  updateId: ctx.update.update_id,
4845
5567
  chatId: ctx.chat?.id,
4846
5568
  fromId: ctx.from?.id,
4847
5569
  hasText: typeof messageText === "string" && messageText.length > 0,
4848
- textLength: typeof messageText === "string" ? messageText.length : 0,
4849
- textPreview: typeof messageText === "string" && messageText.length > 0 ? createRedactedPreview(messageText) : void 0
5570
+ textLength: typeof messageText === "string" ? messageText.length : 0
4850
5571
  };
4851
5572
  }
4852
5573
  function createLoggingMiddleware(logger) {
4853
5574
  return async (ctx, next) => {
4854
- logger.info(buildIncomingUpdateLogFields(ctx), "incoming update");
5575
+ logTelegramUpdate(logger, { ...buildIncomingUpdateLogFields(ctx) }, "incoming update");
4855
5576
  return next();
4856
5577
  };
4857
5578
  }
@@ -4860,45 +5581,133 @@ function createLoggingMiddleware(logger) {
4860
5581
  function registerBot(bot, container, options) {
4861
5582
  bot.use(createLoggingMiddleware(container.logger));
4862
5583
  bot.use(createAuthMiddleware(options.telegramAllowedChatIds));
4863
- registerStartCommand(bot, container);
4864
- registerStatusCommand(bot, container);
4865
- registerNewCommand(bot, container);
4866
- registerAgentsCommand(bot, container);
4867
- registerSessionsCommand(bot, container);
4868
- registerCancelCommand(bot, container);
4869
- registerModelsCommand(bot, container);
4870
- registerLanguageCommand(bot, container);
4871
- registerCallbackHandler(bot, container);
4872
- registerFileHandler(bot, container);
4873
- registerMessageHandler(bot, container);
4874
- registerVoiceHandler(bot, container);
5584
+ const safeBot = bot.errorBoundary(async (error) => {
5585
+ const scopedLogger = scopeLoggerToTelegramContext(container.logger, error.ctx, "telegram");
5586
+ scopedLogger.error({
5587
+ ...extractTelegramUpdateContext(error.ctx),
5588
+ event: "telegram.middleware.failed",
5589
+ error: error.error
5590
+ }, "telegram middleware failed");
5591
+ await replyWithDefaultTelegramError(error.ctx, scopedLogger, error.error);
5592
+ });
5593
+ registerStartCommand(safeBot, container);
5594
+ registerStatusCommand(safeBot, container);
5595
+ registerNewCommand(safeBot, container);
5596
+ registerAgentsCommand(safeBot, container);
5597
+ registerSessionsCommand(safeBot, container);
5598
+ registerCancelCommand(safeBot, container);
5599
+ registerModelsCommand(safeBot, container);
5600
+ registerLanguageCommand(safeBot, container);
5601
+ registerCallbackHandler(safeBot, container);
5602
+ registerFileHandler(safeBot, container);
5603
+ registerMessageHandler(safeBot, container);
5604
+ registerVoiceHandler(safeBot, container);
4875
5605
  }
4876
5606
  //#endregion
4877
5607
  //#region src/app/runtime.ts
5608
+ var TELEGRAM_RUNNER_OPTIONS = { runner: {
5609
+ fetch: { timeout: 30 },
5610
+ maxRetryTime: 900 * 1e3,
5611
+ retryInterval: "exponential",
5612
+ silent: true
5613
+ } };
4878
5614
  async function startTelegramBotRuntime(input) {
4879
- const bot = new Bot(input.config.telegramBotToken, { client: { apiRoot: input.config.telegramApiRoot } });
4880
- registerBot(bot, input.container, { telegramAllowedChatIds: input.config.telegramAllowedChatIds });
5615
+ const runtimeKey = buildTelegramRuntimeKey(input.config);
5616
+ const registry = getTelegramBotRuntimeRegistry();
5617
+ const existingRuntime = registry.activeByKey.get(runtimeKey);
5618
+ const runtimeLogger = input.container.logger.child({ component: "runtime" });
5619
+ if (existingRuntime) {
5620
+ runtimeLogger.warn({
5621
+ event: "runtime.reused",
5622
+ runtimeKey,
5623
+ telegramApiRoot: input.config.telegramApiRoot
5624
+ }, "telegram runtime already active in this process; reusing the existing runner");
5625
+ await input.container.dispose();
5626
+ return existingRuntime;
5627
+ }
5628
+ const runtimePromise = startTelegramBotRuntimeInternal(input, runtimeKey, () => {
5629
+ if (registry.activeByKey.get(runtimeKey) === runtimePromise) registry.activeByKey.delete(runtimeKey);
5630
+ }).catch((error) => {
5631
+ if (registry.activeByKey.get(runtimeKey) === runtimePromise) registry.activeByKey.delete(runtimeKey);
5632
+ throw error;
5633
+ });
5634
+ registry.activeByKey.set(runtimeKey, runtimePromise);
5635
+ return runtimePromise;
5636
+ }
5637
+ async function startTelegramBotRuntimeInternal(input, runtimeKey, releaseRuntime) {
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" });
5640
+ wrapTelegramGetUpdates(bot, input.container);
5641
+ (input.registerBotHandlers ?? registerBot)(bot, input.container, { telegramAllowedChatIds: input.config.telegramAllowedChatIds });
4881
5642
  bot.catch((error) => {
4882
- input.container.logger.error({
4883
- error: error.error,
4884
- update: error.ctx.update
5643
+ const metadata = extractTelegramUpdateContext(error.ctx);
5644
+ const telegramLogger = input.container.logger.child({
5645
+ component: "telegram",
5646
+ ...metadata
5647
+ });
5648
+ if (error.error instanceof GrammyError) {
5649
+ telegramLogger.error({
5650
+ event: "telegram.api.error",
5651
+ errorCode: error.error.error_code,
5652
+ description: error.error.description,
5653
+ method: error.error.method,
5654
+ parameters: error.error.parameters,
5655
+ payload: error.error.payload
5656
+ }, "telegram bot api request failed");
5657
+ return;
5658
+ }
5659
+ if (error.error instanceof HttpError) {
5660
+ telegramLogger.error({
5661
+ event: "telegram.http.error",
5662
+ error: error.error.error,
5663
+ message: error.error.message
5664
+ }, "telegram bot network request failed");
5665
+ return;
5666
+ }
5667
+ telegramLogger.error({
5668
+ event: "telegram.update.failed",
5669
+ error: error.error
4885
5670
  }, "telegram bot update failed");
4886
5671
  });
4887
- input.container.logger.info("bot starting...");
4888
- if (input.syncCommands ?? true) await syncTelegramCommands(bot, input.container.logger);
4889
- const runner = run(bot);
5672
+ runtimeLogger.info({
5673
+ event: "runtime.polling.starting",
5674
+ runtimeKey
5675
+ }, "telegram bot polling starting");
5676
+ const runner = (input.runBot ?? run)(bot, TELEGRAM_RUNNER_OPTIONS);
4890
5677
  let stopped = false;
4891
5678
  let disposed = false;
4892
- const stop = () => {
5679
+ if (input.syncCommands ?? true) (input.syncCommandsHandler ?? syncTelegramCommands)(bot, input.container.logger).catch((error) => {
5680
+ runtimeLogger.warn({
5681
+ event: "runtime.commands.sync_failed",
5682
+ error,
5683
+ runtimeKey
5684
+ }, "failed to sync telegram commands; polling continues without command registration updates");
5685
+ });
5686
+ let stopPromise = null;
5687
+ const requestStop = async () => {
4893
5688
  if (stopped) return;
4894
5689
  stopped = true;
4895
- runner.stop();
5690
+ stopPromise = runner.stop().catch((error) => {
5691
+ runtimeLogger.warn({
5692
+ event: "runtime.stop.failed",
5693
+ error,
5694
+ runtimeKey
5695
+ }, "failed to stop telegram runner cleanly");
5696
+ });
5697
+ await stopPromise;
5698
+ };
5699
+ const stop = () => {
5700
+ requestStop();
4896
5701
  };
4897
5702
  const dispose = async () => {
4898
5703
  if (disposed) return;
4899
5704
  disposed = true;
4900
- stop();
4901
- await input.container.dispose();
5705
+ try {
5706
+ await requestStop();
5707
+ await input.container.dispose();
5708
+ } finally {
5709
+ releaseRuntime();
5710
+ }
4902
5711
  };
4903
5712
  return {
4904
5713
  bot,
@@ -4907,39 +5716,71 @@ async function startTelegramBotRuntime(input) {
4907
5716
  dispose
4908
5717
  };
4909
5718
  }
5719
+ function wrapTelegramGetUpdates(bot, container) {
5720
+ const originalGetUpdates = bot.api.getUpdates.bind(bot.api);
5721
+ const runtimeLogger = container.logger.child({ component: "runtime" });
5722
+ bot.api.getUpdates = async (options, signal) => {
5723
+ const requestOptions = options ?? {
5724
+ limit: 100,
5725
+ offset: 0,
5726
+ timeout: 30
5727
+ };
5728
+ try {
5729
+ return await originalGetUpdates(requestOptions, signal);
5730
+ } catch (error) {
5731
+ runtimeLogger.warn({
5732
+ event: "runtime.telegram.get_updates_failed",
5733
+ error,
5734
+ limit: requestOptions.limit,
5735
+ offset: requestOptions.offset,
5736
+ timeout: requestOptions.timeout
5737
+ }, "telegram getUpdates failed");
5738
+ throw error;
5739
+ }
5740
+ };
5741
+ }
5742
+ function buildTelegramRuntimeKey(config) {
5743
+ return `${config.telegramApiRoot}::${config.telegramBotToken}`;
5744
+ }
5745
+ function getTelegramBotRuntimeRegistry() {
5746
+ const globalScope = globalThis;
5747
+ globalScope.__opencodeTbotTelegramRuntimeRegistry__ ??= { activeByKey: /* @__PURE__ */ new Map() };
5748
+ return globalScope.__opencodeTbotTelegramRuntimeRegistry__;
5749
+ }
4910
5750
  //#endregion
4911
5751
  //#region src/plugin.ts
4912
- var runtimeState = null;
4913
5752
  async function ensureTelegramBotPluginRuntime(options) {
5753
+ const runtimeStateHolder = getTelegramBotPluginRuntimeStateHolder();
4914
5754
  const cwd = resolvePluginRuntimeCwd(options.context);
4915
- if (runtimeState && runtimeState.cwd !== cwd) {
4916
- const activeState = runtimeState;
4917
- runtimeState = null;
5755
+ if (runtimeStateHolder.state && runtimeStateHolder.state.cwd !== cwd) {
5756
+ const activeState = runtimeStateHolder.state;
5757
+ runtimeStateHolder.state = null;
4918
5758
  await disposeTelegramBotPluginRuntimeState(activeState);
4919
5759
  }
4920
- if (!runtimeState) {
5760
+ if (!runtimeStateHolder.state) {
4921
5761
  const runtimePromise = startPluginRuntime(options, cwd).then((runtime) => {
4922
- if (runtimeState?.runtimePromise === runtimePromise) runtimeState.runtime = runtime;
5762
+ if (runtimeStateHolder.state?.runtimePromise === runtimePromise) runtimeStateHolder.state.runtime = runtime;
4923
5763
  return runtime;
4924
5764
  }).catch((error) => {
4925
- if (runtimeState?.runtimePromise === runtimePromise) runtimeState = null;
5765
+ if (runtimeStateHolder.state?.runtimePromise === runtimePromise) runtimeStateHolder.state = null;
4926
5766
  throw error;
4927
5767
  });
4928
- runtimeState = {
5768
+ runtimeStateHolder.state = {
4929
5769
  cwd,
4930
5770
  runtime: null,
4931
5771
  runtimePromise
4932
5772
  };
4933
5773
  }
4934
- return runtimeState.runtimePromise;
5774
+ return runtimeStateHolder.state.runtimePromise;
4935
5775
  }
4936
5776
  var TelegramBotPlugin = async (context) => {
4937
5777
  return createHooks(await ensureTelegramBotPluginRuntime({ context }));
4938
5778
  };
4939
5779
  async function resetTelegramBotPluginRuntimeForTests() {
4940
- if (!runtimeState) return;
4941
- const activeState = runtimeState;
4942
- runtimeState = null;
5780
+ const runtimeStateHolder = getTelegramBotPluginRuntimeStateHolder();
5781
+ if (!runtimeStateHolder.state) return;
5782
+ const activeState = runtimeStateHolder.state;
5783
+ runtimeStateHolder.state = null;
4943
5784
  await disposeTelegramBotPluginRuntimeState(activeState);
4944
5785
  }
4945
5786
  async function startPluginRuntime(options, cwd) {
@@ -4952,8 +5793,9 @@ async function startPluginRuntime(options, cwd) {
4952
5793
  });
4953
5794
  const { config, container } = bootstrapApp(options.context.client, preparedConfiguration.config, { cwd: preparedConfiguration.cwd });
4954
5795
  try {
4955
- if (preparedConfiguration.ignoredProjectConfigFilePath) container.logger.warn({
5796
+ if (preparedConfiguration.ignoredProjectConfigFilePath) container.logger.child({ component: "runtime" }).warn({
4956
5797
  cwd: preparedConfiguration.cwd,
5798
+ event: "runtime.config.legacy_worktree_ignored",
4957
5799
  ignoredProjectConfigFilePath: preparedConfiguration.ignoredProjectConfigFilePath,
4958
5800
  globalConfigFilePath: preparedConfiguration.globalConfigFilePath
4959
5801
  }, "legacy worktree plugin config is ignored; migrate settings to the global opencode-tbot config");
@@ -4961,8 +5803,9 @@ async function startPluginRuntime(options, cwd) {
4961
5803
  config,
4962
5804
  container
4963
5805
  });
4964
- container.logger.info({
5806
+ container.logger.child({ component: "runtime" }).info({
4965
5807
  cwd: preparedConfiguration.cwd,
5808
+ event: "runtime.plugin.started",
4966
5809
  globalConfigFilePath: preparedConfiguration.globalConfigFilePath,
4967
5810
  ignoredProjectConfigFilePath: preparedConfiguration.ignoredProjectConfigFilePath,
4968
5811
  configFilePath: preparedConfiguration.configFilePath,
@@ -4992,6 +5835,11 @@ function createHooks(runtime) {
4992
5835
  }
4993
5836
  };
4994
5837
  }
5838
+ function getTelegramBotPluginRuntimeStateHolder() {
5839
+ const globalScope = globalThis;
5840
+ globalScope.__opencodeTbotPluginRuntimeState__ ??= { state: null };
5841
+ return globalScope.__opencodeTbotPluginRuntimeState__;
5842
+ }
4995
5843
  //#endregion
4996
5844
  export { TelegramBotPlugin, TelegramBotPlugin as default, ensureTelegramBotPluginRuntime, resetTelegramBotPluginRuntimeForTests };
4997
5845