opencode-tbot 0.1.27 → 0.1.30

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
@@ -3,10 +3,10 @@ import { mkdir, readFile, rename, stat, 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";
6
+ import { createOpencodeClient } from "@opencode-ai/sdk";
7
7
  import { randomUUID } from "node:crypto";
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
11
  var REDACTED = "[REDACTED]";
12
12
  var DEFAULT_PREVIEW_LENGTH = 160;
@@ -46,7 +46,18 @@ function createOpenCodeAppLogger(client, options = {}) {
46
46
  };
47
47
  queue = queue.catch(() => void 0).then(async () => {
48
48
  try {
49
- await client.app.log(payload);
49
+ if (client.app.log.length >= 2) {
50
+ await client.app.log(payload, {
51
+ responseStyle: "data",
52
+ throwOnError: true
53
+ });
54
+ return;
55
+ }
56
+ await client.app.log({
57
+ body: payload,
58
+ responseStyle: "data",
59
+ throwOnError: true
60
+ });
50
61
  } catch {}
51
62
  });
52
63
  };
@@ -292,31 +303,57 @@ var OpenCodeClient = class {
292
303
  ...SDK_OPTIONS,
293
304
  ...input.signal ? { signal: input.signal } : {}
294
305
  };
295
- return unwrapSdkData(input.parameters === void 0 ? await handler.call(target, options) : await handler.call(target, input.parameters, options));
306
+ return unwrapSdkData(handler.length >= 2 ? await handler.call(target, input.legacyParameters, options) : await handler.call(target, input.parameters === void 0 ? options : {
307
+ ...input.parameters,
308
+ ...options
309
+ }));
296
310
  }
297
311
  async getHealth() {
298
- return this.callScopedSdkMethod("global", "health", {});
312
+ try {
313
+ return await this.callScopedSdkMethod("global", "health", {});
314
+ } catch (error) {
315
+ if (!isMissingScopedSdkMethodError(error, "global", "health")) throw error;
316
+ return this.callRawSdkGet("/global/health");
317
+ }
299
318
  }
300
319
  async abortSession(sessionId) {
301
- return this.callScopedSdkMethod("session", "abort", { parameters: { sessionID: sessionId } });
320
+ return this.callScopedSdkMethod("session", "abort", {
321
+ legacyParameters: { sessionID: sessionId },
322
+ parameters: { path: { id: sessionId } }
323
+ });
302
324
  }
303
325
  async deleteSession(sessionId) {
304
- return this.callScopedSdkMethod("session", "delete", { parameters: { sessionID: sessionId } });
326
+ return this.callScopedSdkMethod("session", "delete", {
327
+ legacyParameters: { sessionID: sessionId },
328
+ parameters: { path: { id: sessionId } }
329
+ });
305
330
  }
306
331
  async forkSession(sessionId, messageId) {
307
- return this.callScopedSdkMethod("session", "fork", { parameters: {
308
- sessionID: sessionId,
309
- ...messageId?.trim() ? { messageID: messageId.trim() } : {}
310
- } });
332
+ return this.callScopedSdkMethod("session", "fork", {
333
+ legacyParameters: {
334
+ sessionID: sessionId,
335
+ ...messageId?.trim() ? { messageID: messageId.trim() } : {}
336
+ },
337
+ parameters: {
338
+ path: { id: sessionId },
339
+ ...messageId?.trim() ? { body: { messageID: messageId.trim() } } : {}
340
+ }
341
+ });
311
342
  }
312
343
  async getPath() {
313
344
  return this.callScopedSdkMethod("path", "get", {});
314
345
  }
315
346
  async listLspStatuses(directory) {
316
- return this.callScopedSdkMethod("lsp", "status", { parameters: directory ? { directory } : void 0 });
347
+ return this.callScopedSdkMethod("lsp", "status", {
348
+ legacyParameters: directory ? { directory } : void 0,
349
+ parameters: directory ? { query: { directory } } : void 0
350
+ });
317
351
  }
318
352
  async listMcpStatuses(directory) {
319
- return this.callScopedSdkMethod("mcp", "status", { parameters: directory ? { directory } : void 0 });
353
+ return this.callScopedSdkMethod("mcp", "status", {
354
+ legacyParameters: directory ? { directory } : void 0,
355
+ parameters: directory ? { query: { directory } } : void 0
356
+ });
320
357
  }
321
358
  async getSessionStatuses() {
322
359
  return this.loadSessionStatuses();
@@ -331,29 +368,69 @@ var OpenCodeClient = class {
331
368
  return this.callScopedSdkMethod("project", "current", {});
332
369
  }
333
370
  async createSessionForDirectory(directory, title) {
334
- return this.callScopedSdkMethod("session", "create", { parameters: title ? {
335
- directory,
336
- title
337
- } : { directory } });
371
+ return this.callScopedSdkMethod("session", "create", {
372
+ legacyParameters: title ? {
373
+ directory,
374
+ title
375
+ } : { directory },
376
+ parameters: {
377
+ query: { directory },
378
+ ...title ? { body: { title } } : {}
379
+ }
380
+ });
338
381
  }
339
382
  async renameSession(sessionId, title) {
340
- return this.callScopedSdkMethod("session", "update", { parameters: {
341
- sessionID: sessionId,
342
- title
343
- } });
383
+ return this.callScopedSdkMethod("session", "update", {
384
+ legacyParameters: {
385
+ sessionID: sessionId,
386
+ title
387
+ },
388
+ parameters: {
389
+ path: { id: sessionId },
390
+ body: { title }
391
+ }
392
+ });
344
393
  }
345
394
  async listAgents() {
346
395
  return this.callScopedSdkMethod("app", "agents", {});
347
396
  }
348
397
  async listPendingPermissions(directory) {
349
- return this.callScopedSdkMethod("permission", "list", { parameters: directory ? { directory } : void 0 });
350
- }
351
- async replyToPermission(requestId, reply, message, _sessionId) {
352
- return this.callScopedSdkMethod("permission", "reply", { parameters: {
353
- requestID: requestId,
354
- reply,
355
- ...message?.trim() ? { message: message.trim() } : {}
356
- } });
398
+ return (await this.callScopedSdkMethod("permission", "list", {
399
+ legacyParameters: directory ? { directory } : void 0,
400
+ parameters: directory ? { query: { directory } } : void 0
401
+ })).map(normalizePermissionRequest).filter((permission) => !!permission);
402
+ }
403
+ async replyToPermission(requestId, reply, message, sessionId) {
404
+ const normalizedMessage = message?.trim();
405
+ const rootPermissionHandler = this.client.postSessionIdPermissionsPermissionId;
406
+ if (sessionId && typeof rootPermissionHandler === "function") return unwrapSdkData(await rootPermissionHandler.call(this.client, {
407
+ ...SDK_OPTIONS,
408
+ body: {
409
+ response: reply,
410
+ ...normalizedMessage ? { message: normalizedMessage } : {}
411
+ },
412
+ path: {
413
+ id: sessionId,
414
+ permissionID: requestId
415
+ }
416
+ }));
417
+ return this.callScopedSdkMethod("permission", "reply", {
418
+ legacyParameters: {
419
+ requestID: requestId,
420
+ reply,
421
+ ...normalizedMessage ? { message: normalizedMessage } : {}
422
+ },
423
+ parameters: sessionId ? {
424
+ body: {
425
+ response: reply,
426
+ ...normalizedMessage ? { message: normalizedMessage } : {}
427
+ },
428
+ path: {
429
+ id: sessionId,
430
+ permissionID: requestId
431
+ }
432
+ } : void 0
433
+ });
357
434
  }
358
435
  async listModels() {
359
436
  const now = Date.now();
@@ -489,10 +566,14 @@ var OpenCodeClient = class {
489
566
  messageId
490
567
  }, async (requestSignal) => {
491
568
  return normalizePromptResponse(await this.callScopedSdkMethod("session", "message", {
492
- parameters: {
569
+ legacyParameters: {
493
570
  sessionID: sessionId,
494
571
  messageID: messageId
495
572
  },
573
+ parameters: { path: {
574
+ id: sessionId,
575
+ messageID: messageId
576
+ } },
496
577
  signal: requestSignal
497
578
  }));
498
579
  }, signal);
@@ -520,10 +601,14 @@ var OpenCodeClient = class {
520
601
  timeoutMs: this.promptTimeoutPolicy.pollRequestTimeoutMs
521
602
  }, async (requestSignal) => {
522
603
  return normalizePromptResponses(await this.callScopedSdkMethod("session", "messages", {
523
- parameters: {
604
+ legacyParameters: {
524
605
  sessionID: sessionId,
525
606
  limit: PROMPT_MESSAGE_POLL_LIMIT
526
607
  },
608
+ parameters: {
609
+ path: { id: sessionId },
610
+ query: { limit: PROMPT_MESSAGE_POLL_LIMIT }
611
+ },
527
612
  signal: requestSignal
528
613
  }));
529
614
  }, signal);
@@ -584,10 +669,14 @@ var OpenCodeClient = class {
584
669
  ...input.variant ? { variant: input.variant } : {},
585
670
  parts
586
671
  };
587
- const requestParameters = {
672
+ const legacyRequestParameters = {
588
673
  sessionID: input.sessionId,
589
674
  ...requestBody
590
675
  };
676
+ const requestParameters = {
677
+ body: requestBody,
678
+ path: { id: input.sessionId }
679
+ };
591
680
  try {
592
681
  if (typeof this.client.session?.promptAsync === "function") {
593
682
  await this.runPromptRequestWithTimeout({
@@ -596,6 +685,7 @@ var OpenCodeClient = class {
596
685
  timeoutMs: this.promptTimeoutPolicy.waitTimeoutMs
597
686
  }, async (signal) => {
598
687
  await this.callScopedSdkMethod("session", "promptAsync", {
688
+ legacyParameters: legacyRequestParameters,
599
689
  parameters: requestParameters,
600
690
  signal
601
691
  });
@@ -613,10 +703,16 @@ var OpenCodeClient = class {
613
703
  throw new Error("OpenCode SDK client does not expose session.promptAsync().");
614
704
  }
615
705
  async loadSessionStatuses(signal) {
616
- return this.callScopedSdkMethod("session", "status", {
617
- signal,
618
- parameters: void 0
619
- });
706
+ return this.callScopedSdkMethod("session", "status", { signal });
707
+ }
708
+ async callRawSdkGet(url, signal) {
709
+ const rawClient = getRawSdkRequestClient(this.client);
710
+ if (typeof rawClient?.get !== "function") throw new Error(`OpenCode SDK client does not expose a compatible raw GET endpoint for ${url}.`);
711
+ return unwrapSdkData(await rawClient.get({
712
+ ...SDK_OPTIONS,
713
+ ...signal ? { signal } : {},
714
+ url
715
+ }));
620
716
  }
621
717
  async runPromptRequestWithTimeout(input, operation, signal) {
622
718
  const startedAt = Date.now();
@@ -684,12 +780,16 @@ var OpenCodeClient = class {
684
780
  logPromptRequest(level, extra, message) {
685
781
  const log = this.client.app?.log;
686
782
  if (typeof log !== "function") return;
687
- log.call(this.client.app, {
783
+ const payload = {
688
784
  service: PROMPT_LOG_SERVICE,
689
785
  level,
690
786
  message,
691
787
  extra
692
- }).catch(() => void 0);
788
+ };
789
+ (log.length >= 2 ? log.call(this.client.app, payload, SDK_OPTIONS) : log.call(this.client.app, {
790
+ body: payload,
791
+ ...SDK_OPTIONS
792
+ })).catch(() => void 0);
693
793
  }
694
794
  };
695
795
  function createOpenCodeClientFromSdkClient(client, fetchFn = fetch, promptTimeoutPolicy = {}) {
@@ -811,7 +911,7 @@ function isPromptResponseUsable(data, structured) {
811
911
  }
812
912
  function normalizePromptResponse(response) {
813
913
  return {
814
- info: isPlainRecord(response?.info) ? response.info : null,
914
+ info: isPlainRecord$1(response?.info) ? response.info : null,
815
915
  parts: normalizePromptParts(response?.parts)
816
916
  };
817
917
  }
@@ -835,7 +935,7 @@ function toAssistantMessage(message) {
835
935
  if ("mode" in message && typeof message.mode === "string" && message.mode.trim().length > 0) normalized.mode = message.mode;
836
936
  if ("modelID" in message && typeof message.modelID === "string" && message.modelID.trim().length > 0) normalized.modelID = message.modelID;
837
937
  if ("parentID" in message && typeof message.parentID === "string" && message.parentID.trim().length > 0) normalized.parentID = message.parentID;
838
- if ("path" in message && isPlainRecord(message.path)) normalized.path = {
938
+ if ("path" in message && isPlainRecord$1(message.path)) normalized.path = {
839
939
  ...typeof message.path.cwd === "string" && message.path.cwd.trim().length > 0 ? { cwd: message.path.cwd } : {},
840
940
  ...typeof message.path.root === "string" && message.path.root.trim().length > 0 ? { root: message.path.root } : {}
841
941
  };
@@ -845,16 +945,16 @@ function toAssistantMessage(message) {
845
945
  const structuredPayload = extractStructuredPayload(message);
846
946
  if (structuredPayload !== null) normalized.structured = structuredPayload;
847
947
  if ("summary" in message && typeof message.summary === "boolean") normalized.summary = message.summary;
848
- if ("time" in message && isPlainRecord(message.time)) normalized.time = {
948
+ if ("time" in message && isPlainRecord$1(message.time)) normalized.time = {
849
949
  ...typeof message.time.created === "number" && Number.isFinite(message.time.created) ? { created: message.time.created } : {},
850
950
  ...typeof message.time.completed === "number" && Number.isFinite(message.time.completed) ? { completed: message.time.completed } : {}
851
951
  };
852
- if ("tokens" in message && isPlainRecord(message.tokens)) normalized.tokens = {
952
+ if ("tokens" in message && isPlainRecord$1(message.tokens)) normalized.tokens = {
853
953
  ...typeof message.tokens.input === "number" && Number.isFinite(message.tokens.input) ? { input: message.tokens.input } : {},
854
954
  ...typeof message.tokens.output === "number" && Number.isFinite(message.tokens.output) ? { output: message.tokens.output } : {},
855
955
  ...typeof message.tokens.reasoning === "number" && Number.isFinite(message.tokens.reasoning) ? { reasoning: message.tokens.reasoning } : {},
856
956
  ...typeof message.tokens.total === "number" && Number.isFinite(message.tokens.total) ? { total: message.tokens.total } : {},
857
- ...isPlainRecord(message.tokens.cache) ? { cache: {
957
+ ...isPlainRecord$1(message.tokens.cache) ? { cache: {
858
958
  ...typeof message.tokens.cache.read === "number" && Number.isFinite(message.tokens.cache.read) ? { read: message.tokens.cache.read } : {},
859
959
  ...typeof message.tokens.cache.write === "number" && Number.isFinite(message.tokens.cache.write) ? { write: message.tokens.cache.write } : {}
860
960
  } } : {}
@@ -863,7 +963,7 @@ function toAssistantMessage(message) {
863
963
  return normalized;
864
964
  }
865
965
  function extractMessageId(message) {
866
- if (!isPlainRecord(message)) return null;
966
+ if (!isPlainRecord$1(message)) return null;
867
967
  return typeof message.id === "string" && message.id.trim().length > 0 ? message.id : null;
868
968
  }
869
969
  function delay(ms, signal) {
@@ -959,6 +1059,34 @@ function unwrapSdkData(response) {
959
1059
  if (response && typeof response === "object" && "data" in response) return response.data;
960
1060
  return response;
961
1061
  }
1062
+ function normalizePermissionRequest(permission) {
1063
+ if (!isPlainRecord$1(permission)) return null;
1064
+ const id = typeof permission.id === "string" && permission.id.trim().length > 0 ? permission.id : null;
1065
+ const sessionID = typeof permission.sessionID === "string" && permission.sessionID.trim().length > 0 ? permission.sessionID : null;
1066
+ 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;
1067
+ if (!id || !sessionID || !permissionName) return null;
1068
+ return {
1069
+ always: Array.isArray(permission.always) ? permission.always.filter((value) => typeof value === "string") : [],
1070
+ id,
1071
+ metadata: isPlainRecord$1(permission.metadata) ? permission.metadata : {},
1072
+ patterns: normalizePermissionPatterns$1(permission),
1073
+ permission: permissionName,
1074
+ sessionID
1075
+ };
1076
+ }
1077
+ function normalizePermissionPatterns$1(permission) {
1078
+ if (Array.isArray(permission.patterns)) return permission.patterns.filter((value) => typeof value === "string");
1079
+ if (typeof permission.pattern === "string" && permission.pattern.trim().length > 0) return [permission.pattern];
1080
+ if (Array.isArray(permission.pattern)) return permission.pattern.filter((value) => typeof value === "string");
1081
+ return [];
1082
+ }
1083
+ function getRawSdkRequestClient(client) {
1084
+ const compatibleClient = client;
1085
+ return compatibleClient.client ?? compatibleClient._client ?? null;
1086
+ }
1087
+ function isMissingScopedSdkMethodError(error, scope, method) {
1088
+ return error instanceof Error && error.message === `OpenCode SDK client does not expose a compatible ${scope}.${method} method.`;
1089
+ }
962
1090
  function resolvePromptTimeoutPolicy(input) {
963
1091
  return {
964
1092
  pollRequestTimeoutMs: input.pollRequestTimeoutMs ?? DEFAULT_OPENCODE_PROMPT_TIMEOUT_POLICY.pollRequestTimeoutMs,
@@ -967,11 +1095,11 @@ function resolvePromptTimeoutPolicy(input) {
967
1095
  };
968
1096
  }
969
1097
  function normalizeAssistantError(value) {
970
- if (!isPlainRecord(value) || typeof value.name !== "string" || value.name.trim().length === 0) return;
1098
+ if (!isPlainRecord$1(value) || typeof value.name !== "string" || value.name.trim().length === 0) return;
971
1099
  return {
972
1100
  ...value,
973
1101
  name: value.name,
974
- ...isPlainRecord(value.data) ? { data: value.data } : {}
1102
+ ...isPlainRecord$1(value.data) ? { data: value.data } : {}
975
1103
  };
976
1104
  }
977
1105
  function isAssistantMessageCompleted(message) {
@@ -984,7 +1112,7 @@ function isCompletedEmptyPromptResponse(data, structured) {
984
1112
  return isAssistantMessageCompleted(assistantInfo) && !assistantInfo?.error && !hasText && !bodyMd;
985
1113
  }
986
1114
  function extractStructuredPayload(message) {
987
- if (!isPlainRecord(message)) return null;
1115
+ if (!isPlainRecord$1(message)) return null;
988
1116
  if ("structured" in message && message.structured !== void 0) return message.structured;
989
1117
  if ("structured_output" in message && message.structured_output !== void 0) return message.structured_output;
990
1118
  return null;
@@ -1058,7 +1186,7 @@ function throwIfAborted(signal) {
1058
1186
  if (!signal?.aborted) return;
1059
1187
  throw normalizeAbortReason(signal.reason);
1060
1188
  }
1061
- function isPlainRecord(value) {
1189
+ function isPlainRecord$1(value) {
1062
1190
  return value !== null && typeof value === "object" && !Array.isArray(value);
1063
1191
  }
1064
1192
  async function resolveProviderAvailability(config, fetchFn) {
@@ -2274,25 +2402,35 @@ function escapeMarkdownV2(value) {
2274
2402
  async function handleTelegramBotPluginEvent(runtime, event) {
2275
2403
  switch (event.type) {
2276
2404
  case "permission.asked":
2277
- await handlePermissionAsked(runtime, event);
2405
+ case "permission.updated": {
2406
+ const request = normalizePermissionRequestEvent(event.properties);
2407
+ if (request) await handlePermissionAsked(runtime, request);
2278
2408
  return;
2279
- case "permission.replied":
2280
- await handlePermissionReplied(runtime, event);
2409
+ }
2410
+ case "permission.replied": {
2411
+ const replyEvent = normalizePermissionReplyEvent(event.properties);
2412
+ if (replyEvent) await handlePermissionReplied(runtime, replyEvent);
2281
2413
  return;
2282
- case "session.error":
2283
- await handleSessionError(runtime, event);
2414
+ }
2415
+ case "session.error": {
2416
+ const sessionError = normalizeSessionErrorEvent(event.properties);
2417
+ if (sessionError) await handleSessionError(runtime, sessionError);
2284
2418
  return;
2285
- case "session.idle":
2286
- await handleSessionIdle(runtime, event);
2419
+ }
2420
+ case "session.idle": {
2421
+ const sessionIdle = normalizeSessionIdleEvent(event.properties);
2422
+ if (sessionIdle) await handleSessionIdle(runtime, sessionIdle);
2287
2423
  return;
2288
- case "session.status":
2289
- await handleSessionStatus(runtime, event);
2424
+ }
2425
+ case "session.status": {
2426
+ const sessionStatus = normalizeSessionStatusEvent(event.properties);
2427
+ if (sessionStatus) await handleSessionStatus(runtime, sessionStatus);
2290
2428
  return;
2429
+ }
2291
2430
  default: return;
2292
2431
  }
2293
2432
  }
2294
- async function handlePermissionAsked(runtime, event) {
2295
- const request = event.properties;
2433
+ async function handlePermissionAsked(runtime, request) {
2296
2434
  const bindings = await runtime.container.sessionRepo.listBySessionId(request.sessionID);
2297
2435
  const chatIds = new Set([...bindings.map((binding) => binding.chatId), ...runtime.container.foregroundSessionTracker.listChatIds(request.sessionID)]);
2298
2436
  const approvals = await runtime.container.permissionApprovalRepo.listByRequestId(request.id);
@@ -2322,52 +2460,42 @@ async function handlePermissionAsked(runtime, event) {
2322
2460
  }
2323
2461
  }
2324
2462
  async function handlePermissionReplied(runtime, event) {
2325
- const requestId = event.properties.requestID;
2326
- const reply = event.properties.reply;
2327
- const approvals = await runtime.container.permissionApprovalRepo.listByRequestId(requestId);
2463
+ const approvals = await runtime.container.permissionApprovalRepo.listByRequestId(event.requestId);
2328
2464
  await Promise.all(approvals.map(async (approval) => {
2329
2465
  try {
2330
- await runtime.bot.api.editMessageText(approval.chatId, approval.messageId, buildPermissionApprovalResolvedMessage(requestId, reply));
2466
+ await runtime.bot.api.editMessageText(approval.chatId, approval.messageId, buildPermissionApprovalResolvedMessage(event.requestId, event.reply));
2331
2467
  } catch (error) {
2332
2468
  runtime.container.logger.warn({
2333
2469
  error,
2334
2470
  chatId: approval.chatId,
2335
- requestId
2471
+ requestId: event.requestId,
2472
+ sessionId: event.sessionId
2336
2473
  }, "failed to update Telegram permission message");
2337
2474
  }
2338
- await runtime.container.permissionApprovalRepo.set(toResolvedApproval(approval, reply));
2475
+ await runtime.container.permissionApprovalRepo.set(toResolvedApproval(approval, event.reply));
2339
2476
  }));
2340
2477
  }
2341
2478
  async function handleSessionError(runtime, event) {
2342
- const sessionId = event.properties.sessionID;
2343
- const error = event.properties.error;
2344
- if (!sessionId) {
2345
- runtime.container.logger.error({ error }, "session error received without a session id");
2346
- return;
2347
- }
2348
- if (runtime.container.foregroundSessionTracker.fail(sessionId, error ?? /* @__PURE__ */ new Error("Unknown session error."))) {
2479
+ if (runtime.container.foregroundSessionTracker.fail(event.sessionId, event.error instanceof Error ? event.error : /* @__PURE__ */ new Error("Unknown session error."))) {
2349
2480
  runtime.container.logger.warn({
2350
- error,
2351
- sessionId
2481
+ error: event.error,
2482
+ sessionId: event.sessionId
2352
2483
  }, "session error suppressed for foreground Telegram session");
2353
2484
  return;
2354
2485
  }
2355
- 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."}`);
2486
+ const message = extractSessionErrorMessage(event.error) ?? "Unknown session error.";
2487
+ await notifyBoundChats(runtime, event.sessionId, `Session failed.\n\nSession: ${event.sessionId}\nError: ${message}`);
2356
2488
  }
2357
2489
  async function handleSessionIdle(runtime, event) {
2358
- const sessionId = event.properties.sessionID;
2359
- if (runtime.container.foregroundSessionTracker.clear(sessionId)) {
2360
- runtime.container.logger.info({ sessionId }, "session idle notification suppressed for foreground Telegram session");
2490
+ if (runtime.container.foregroundSessionTracker.clear(event.sessionId)) {
2491
+ runtime.container.logger.info({ sessionId: event.sessionId }, "session idle notification suppressed for foreground Telegram session");
2361
2492
  return;
2362
2493
  }
2363
- await notifyBoundChats(runtime, sessionId, `Session finished.\n\nSession: ${sessionId}`);
2494
+ await notifyBoundChats(runtime, event.sessionId, `Session finished.\n\nSession: ${event.sessionId}`);
2364
2495
  }
2365
2496
  async function handleSessionStatus(runtime, event) {
2366
- if (event.properties.status.type !== "idle") return;
2367
- await handleSessionIdle(runtime, {
2368
- type: "session.idle",
2369
- properties: { sessionID: event.properties.sessionID }
2370
- });
2497
+ if (event.statusType !== "idle") return;
2498
+ await handleSessionIdle(runtime, event);
2371
2499
  }
2372
2500
  async function notifyBoundChats(runtime, sessionId, text) {
2373
2501
  const bindings = await runtime.container.sessionRepo.listBySessionId(sessionId);
@@ -2391,6 +2519,79 @@ function toResolvedApproval(approval, reply) {
2391
2519
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2392
2520
  };
2393
2521
  }
2522
+ function normalizePermissionRequestEvent(properties) {
2523
+ if (!isPlainRecord(properties)) return null;
2524
+ const id = asNonEmptyString(properties.id);
2525
+ const sessionID = asNonEmptyString(properties.sessionID);
2526
+ const permission = asNonEmptyString(properties.permission) ?? asNonEmptyString(properties.type);
2527
+ if (!id || !sessionID || !permission) return null;
2528
+ return {
2529
+ always: Array.isArray(properties.always) ? properties.always.filter((value) => typeof value === "string") : [],
2530
+ id,
2531
+ metadata: isPlainRecord(properties.metadata) ? properties.metadata : {},
2532
+ patterns: normalizePermissionPatterns(properties),
2533
+ permission,
2534
+ sessionID
2535
+ };
2536
+ }
2537
+ function normalizePermissionReplyEvent(properties) {
2538
+ if (!isPlainRecord(properties)) return null;
2539
+ const requestId = asNonEmptyString(properties.requestID) ?? asNonEmptyString(properties.permissionID);
2540
+ const reply = normalizePermissionReply(asNonEmptyString(properties.reply) ?? asNonEmptyString(properties.response));
2541
+ const sessionId = asNonEmptyString(properties.sessionID);
2542
+ if (!requestId || !reply || !sessionId) return null;
2543
+ return {
2544
+ reply,
2545
+ requestId,
2546
+ sessionId
2547
+ };
2548
+ }
2549
+ function normalizeSessionErrorEvent(properties) {
2550
+ if (!isPlainRecord(properties)) return null;
2551
+ const sessionId = asNonEmptyString(properties.sessionID);
2552
+ if (!sessionId) return null;
2553
+ return {
2554
+ error: properties.error,
2555
+ sessionId
2556
+ };
2557
+ }
2558
+ function normalizeSessionIdleEvent(properties) {
2559
+ if (!isPlainRecord(properties)) return null;
2560
+ const sessionId = asNonEmptyString(properties.sessionID);
2561
+ return sessionId ? { sessionId } : null;
2562
+ }
2563
+ function normalizeSessionStatusEvent(properties) {
2564
+ if (!isPlainRecord(properties) || !isPlainRecord(properties.status)) return null;
2565
+ const sessionId = asNonEmptyString(properties.sessionID);
2566
+ const statusType = asNonEmptyString(properties.status.type);
2567
+ if (!sessionId || !statusType) return null;
2568
+ return {
2569
+ sessionId,
2570
+ statusType
2571
+ };
2572
+ }
2573
+ function normalizePermissionPatterns(properties) {
2574
+ if (Array.isArray(properties.patterns)) return properties.patterns.filter((value) => typeof value === "string");
2575
+ if (typeof properties.pattern === "string" && properties.pattern.trim().length > 0) return [properties.pattern];
2576
+ if (Array.isArray(properties.pattern)) return properties.pattern.filter((value) => typeof value === "string");
2577
+ return [];
2578
+ }
2579
+ function normalizePermissionReply(value) {
2580
+ if (value === "once" || value === "always" || value === "reject") return value;
2581
+ return null;
2582
+ }
2583
+ function extractSessionErrorMessage(error) {
2584
+ if (error instanceof Error && error.message.trim().length > 0) return error.message.trim();
2585
+ if (!isPlainRecord(error)) return null;
2586
+ if (isPlainRecord(error.data) && typeof error.data.message === "string" && error.data.message.trim().length > 0) return error.data.message.trim();
2587
+ return asNonEmptyString(error.name);
2588
+ }
2589
+ function asNonEmptyString(value) {
2590
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
2591
+ }
2592
+ function isPlainRecord(value) {
2593
+ return value !== null && typeof value === "object" && !Array.isArray(value);
2594
+ }
2394
2595
  var SUPPORTED_BOT_LANGUAGES = [
2395
2596
  "en",
2396
2597
  "zh-CN",
@@ -3094,111 +3295,6 @@ async function syncTelegramCommandsForChat(api, chatId, language) {
3094
3295
  } });
3095
3296
  }
3096
3297
  //#endregion
3097
- //#region src/bot/i18n.ts
3098
- async function getChatLanguage(sessionRepo, chatId) {
3099
- if (!chatId) return "en";
3100
- return normalizeBotLanguage((await sessionRepo.getByChatId(chatId))?.language);
3101
- }
3102
- async function getChatCopy(sessionRepo, chatId) {
3103
- return getBotCopy(await getChatLanguage(sessionRepo, chatId));
3104
- }
3105
- async function setChatLanguage(sessionRepo, chatId, language) {
3106
- const binding = await sessionRepo.getByChatId(chatId);
3107
- await sessionRepo.setCurrent({
3108
- chatId,
3109
- sessionId: binding?.sessionId ?? null,
3110
- projectId: binding?.projectId ?? null,
3111
- directory: binding?.directory ?? null,
3112
- agentName: binding?.agentName ?? null,
3113
- modelProviderId: binding?.modelProviderId ?? null,
3114
- modelId: binding?.modelId ?? null,
3115
- modelVariant: binding?.modelVariant ?? null,
3116
- language,
3117
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3118
- });
3119
- }
3120
- var NUMBERED_BUTTONS_PER_ROW = 5;
3121
- function buildModelsKeyboard(models, requestedPage, copy = BOT_COPY) {
3122
- const page = getModelsPage(models, requestedPage);
3123
- const keyboard = buildNumberedKeyboard(page.items, page.startIndex, (_, index) => `model:pick:${page.startIndex + index + 1}`);
3124
- appendPaginationButtons(keyboard, page.page, page.totalPages, "model:page", copy);
3125
- return {
3126
- keyboard,
3127
- page
3128
- };
3129
- }
3130
- function buildAgentsKeyboard(agents, requestedPage, copy = BOT_COPY) {
3131
- const page = getAgentsPage(agents, requestedPage);
3132
- const keyboard = buildNumberedKeyboard(page.items, page.startIndex, (_, index) => `agents:select:${page.startIndex + index + 1}`);
3133
- appendPaginationButtons(keyboard, page.page, page.totalPages, "agents:page", copy);
3134
- return {
3135
- keyboard,
3136
- page
3137
- };
3138
- }
3139
- function buildSessionsKeyboard(sessions, requestedPage, copy = BOT_COPY) {
3140
- const page = getSessionsPage(sessions, requestedPage);
3141
- const keyboard = buildNumberedKeyboard(page.items, page.startIndex, (session) => `sessions:pick:${page.page}:${session.id}`);
3142
- appendPaginationButtons(keyboard, page.page, page.totalPages, "sessions:page", copy);
3143
- return {
3144
- keyboard,
3145
- page
3146
- };
3147
- }
3148
- function buildSessionActionKeyboard(sessionId, page, copy = BOT_COPY) {
3149
- 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}`);
3150
- }
3151
- function buildModelVariantsKeyboard(variants, modelIndex) {
3152
- return buildNumberedKeyboard(variants, 0, (_, index) => `model:variant:${modelIndex}:${index + 1}`);
3153
- }
3154
- function buildLanguageKeyboard(currentLanguage, copy = BOT_COPY) {
3155
- const keyboard = new InlineKeyboard();
3156
- SUPPORTED_BOT_LANGUAGES.forEach((language, index) => {
3157
- const label = currentLanguage === language ? `[${getLanguageLabel(language, copy)}]` : getLanguageLabel(language, copy);
3158
- keyboard.text(label, `language:select:${language}`);
3159
- if (index !== SUPPORTED_BOT_LANGUAGES.length - 1) keyboard.row();
3160
- });
3161
- return keyboard;
3162
- }
3163
- function getModelsPage(models, requestedPage) {
3164
- return getPagedItems(models, requestedPage, 10);
3165
- }
3166
- function getAgentsPage(agents, requestedPage) {
3167
- return getPagedItems(agents, requestedPage, 10);
3168
- }
3169
- function getSessionsPage(sessions, requestedPage) {
3170
- return getPagedItems(sessions, requestedPage, 10);
3171
- }
3172
- function buildNumberedKeyboard(items, startIndex, buildCallbackData) {
3173
- const keyboard = new InlineKeyboard();
3174
- items.forEach((item, index) => {
3175
- const displayIndex = startIndex + index + 1;
3176
- keyboard.text(`${displayIndex}`, buildCallbackData(item, index));
3177
- if (index !== items.length - 1 && (index + 1) % NUMBERED_BUTTONS_PER_ROW === 0) keyboard.row();
3178
- });
3179
- return keyboard;
3180
- }
3181
- function appendPaginationButtons(keyboard, page, totalPages, prefix, copy) {
3182
- if (totalPages <= 1) return;
3183
- if (page > 0) keyboard.text(copy.common.previousPage, `${prefix}:${page - 1}`);
3184
- if (page < totalPages - 1) keyboard.text(copy.common.nextPage, `${prefix}:${page + 1}`);
3185
- }
3186
- function getPagedItems(items, requestedPage, pageSize) {
3187
- const totalPages = Math.max(1, Math.ceil(items.length / pageSize));
3188
- const page = clampPage(requestedPage, totalPages);
3189
- const startIndex = page * pageSize;
3190
- return {
3191
- items: items.slice(startIndex, startIndex + pageSize),
3192
- page,
3193
- startIndex,
3194
- totalPages
3195
- };
3196
- }
3197
- function clampPage(page, totalPages) {
3198
- if (!Number.isInteger(page) || page < 0) return 0;
3199
- return Math.min(page, totalPages - 1);
3200
- }
3201
- //#endregion
3202
3298
  //#region src/bot/presenters/error.presenter.ts
3203
3299
  function presentError(error, copy = BOT_COPY) {
3204
3300
  const presented = normalizeError(error, copy);
@@ -3313,6 +3409,183 @@ function stringifyUnknown(value) {
3313
3409
  }
3314
3410
  }
3315
3411
  //#endregion
3412
+ //#region src/bot/error-boundary.ts
3413
+ function extractTelegramUpdateContext(ctx) {
3414
+ const updateId = getNestedNumber(ctx, ["update", "update_id"]);
3415
+ const chatId = getNestedNumber(ctx, ["chat", "id"]);
3416
+ const messageText = getNestedString(ctx, ["message", "text"]);
3417
+ const callbackData = getNestedString(ctx, ["callbackQuery", "data"]);
3418
+ return {
3419
+ ...typeof updateId === "number" ? { updateId } : {},
3420
+ ...typeof chatId === "number" ? { chatId } : {},
3421
+ ...typeof messageText === "string" && messageText.trim().length > 0 ? { messageText } : {},
3422
+ ...typeof callbackData === "string" && callbackData.trim().length > 0 ? { callbackData } : {}
3423
+ };
3424
+ }
3425
+ async function replyWithDefaultTelegramError(ctx, logger, error) {
3426
+ const text = presentError(error, BOT_COPY);
3427
+ const editMessageText = getFunction(ctx, "editMessageText");
3428
+ const reply = getFunction(ctx, "reply");
3429
+ const callbackData = getNestedString(ctx, ["callbackQuery", "data"]);
3430
+ try {
3431
+ if (typeof callbackData === "string" && editMessageText) {
3432
+ await editMessageText.call(ctx, text);
3433
+ return;
3434
+ }
3435
+ if (reply) await reply.call(ctx, text);
3436
+ } catch (replyError) {
3437
+ logger.warn?.({
3438
+ ...extractTelegramUpdateContext(ctx),
3439
+ error: replyError
3440
+ }, "failed to deliver fallback Telegram error message");
3441
+ }
3442
+ }
3443
+ function getFunction(value, key) {
3444
+ if (!(key in value)) return null;
3445
+ const candidate = value[key];
3446
+ return typeof candidate === "function" ? candidate : null;
3447
+ }
3448
+ function getNestedNumber(value, path) {
3449
+ const candidate = getNestedValue(value, path);
3450
+ return typeof candidate === "number" ? candidate : null;
3451
+ }
3452
+ function getNestedString(value, path) {
3453
+ const candidate = getNestedValue(value, path);
3454
+ return typeof candidate === "string" ? candidate : null;
3455
+ }
3456
+ function getNestedValue(value, path) {
3457
+ let current = value;
3458
+ for (const segment of path) {
3459
+ if (!current || typeof current !== "object" || !(segment in current)) return null;
3460
+ current = current[segment];
3461
+ }
3462
+ return current;
3463
+ }
3464
+ //#endregion
3465
+ //#region src/bot/i18n.ts
3466
+ async function getChatLanguage(sessionRepo, chatId) {
3467
+ if (!chatId) return "en";
3468
+ return normalizeBotLanguage((await sessionRepo.getByChatId(chatId))?.language);
3469
+ }
3470
+ async function getSafeChatLanguage(sessionRepo, chatId, logger) {
3471
+ try {
3472
+ return await getChatLanguage(sessionRepo, chatId);
3473
+ } catch (error) {
3474
+ logger?.warn?.({
3475
+ error,
3476
+ chatId: chatId ?? void 0
3477
+ }, "failed to resolve Telegram chat language; falling back to the default locale");
3478
+ return "en";
3479
+ }
3480
+ }
3481
+ async function getSafeChatCopy(sessionRepo, chatId, logger) {
3482
+ try {
3483
+ return getBotCopy(await getSafeChatLanguage(sessionRepo, chatId, logger));
3484
+ } catch (error) {
3485
+ logger?.warn?.({
3486
+ error,
3487
+ chatId: chatId ?? void 0
3488
+ }, "failed to resolve Telegram copy; falling back to the default locale");
3489
+ return BOT_COPY;
3490
+ }
3491
+ }
3492
+ async function setChatLanguage(sessionRepo, chatId, language) {
3493
+ const binding = await sessionRepo.getByChatId(chatId);
3494
+ await sessionRepo.setCurrent({
3495
+ chatId,
3496
+ sessionId: binding?.sessionId ?? null,
3497
+ projectId: binding?.projectId ?? null,
3498
+ directory: binding?.directory ?? null,
3499
+ agentName: binding?.agentName ?? null,
3500
+ modelProviderId: binding?.modelProviderId ?? null,
3501
+ modelId: binding?.modelId ?? null,
3502
+ modelVariant: binding?.modelVariant ?? null,
3503
+ language,
3504
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3505
+ });
3506
+ }
3507
+ var NUMBERED_BUTTONS_PER_ROW = 5;
3508
+ function buildModelsKeyboard(models, requestedPage, copy = BOT_COPY) {
3509
+ const page = getModelsPage(models, requestedPage);
3510
+ const keyboard = buildNumberedKeyboard(page.items, page.startIndex, (_, index) => `model:pick:${page.startIndex + index + 1}`);
3511
+ appendPaginationButtons(keyboard, page.page, page.totalPages, "model:page", copy);
3512
+ return {
3513
+ keyboard,
3514
+ page
3515
+ };
3516
+ }
3517
+ function buildAgentsKeyboard(agents, requestedPage, copy = BOT_COPY) {
3518
+ const page = getAgentsPage(agents, requestedPage);
3519
+ const keyboard = buildNumberedKeyboard(page.items, page.startIndex, (_, index) => `agents:select:${page.startIndex + index + 1}`);
3520
+ appendPaginationButtons(keyboard, page.page, page.totalPages, "agents:page", copy);
3521
+ return {
3522
+ keyboard,
3523
+ page
3524
+ };
3525
+ }
3526
+ function buildSessionsKeyboard(sessions, requestedPage, copy = BOT_COPY) {
3527
+ const page = getSessionsPage(sessions, requestedPage);
3528
+ const keyboard = buildNumberedKeyboard(page.items, page.startIndex, (session) => `sessions:pick:${page.page}:${session.id}`);
3529
+ appendPaginationButtons(keyboard, page.page, page.totalPages, "sessions:page", copy);
3530
+ return {
3531
+ keyboard,
3532
+ page
3533
+ };
3534
+ }
3535
+ function buildSessionActionKeyboard(sessionId, page, copy = BOT_COPY) {
3536
+ 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}`);
3537
+ }
3538
+ function buildModelVariantsKeyboard(variants, modelIndex) {
3539
+ return buildNumberedKeyboard(variants, 0, (_, index) => `model:variant:${modelIndex}:${index + 1}`);
3540
+ }
3541
+ function buildLanguageKeyboard(currentLanguage, copy = BOT_COPY) {
3542
+ const keyboard = new InlineKeyboard();
3543
+ SUPPORTED_BOT_LANGUAGES.forEach((language, index) => {
3544
+ const label = currentLanguage === language ? `[${getLanguageLabel(language, copy)}]` : getLanguageLabel(language, copy);
3545
+ keyboard.text(label, `language:select:${language}`);
3546
+ if (index !== SUPPORTED_BOT_LANGUAGES.length - 1) keyboard.row();
3547
+ });
3548
+ return keyboard;
3549
+ }
3550
+ function getModelsPage(models, requestedPage) {
3551
+ return getPagedItems(models, requestedPage, 10);
3552
+ }
3553
+ function getAgentsPage(agents, requestedPage) {
3554
+ return getPagedItems(agents, requestedPage, 10);
3555
+ }
3556
+ function getSessionsPage(sessions, requestedPage) {
3557
+ return getPagedItems(sessions, requestedPage, 10);
3558
+ }
3559
+ function buildNumberedKeyboard(items, startIndex, buildCallbackData) {
3560
+ const keyboard = new InlineKeyboard();
3561
+ items.forEach((item, index) => {
3562
+ const displayIndex = startIndex + index + 1;
3563
+ keyboard.text(`${displayIndex}`, buildCallbackData(item, index));
3564
+ if (index !== items.length - 1 && (index + 1) % NUMBERED_BUTTONS_PER_ROW === 0) keyboard.row();
3565
+ });
3566
+ return keyboard;
3567
+ }
3568
+ function appendPaginationButtons(keyboard, page, totalPages, prefix, copy) {
3569
+ if (totalPages <= 1) return;
3570
+ if (page > 0) keyboard.text(copy.common.previousPage, `${prefix}:${page - 1}`);
3571
+ if (page < totalPages - 1) keyboard.text(copy.common.nextPage, `${prefix}:${page + 1}`);
3572
+ }
3573
+ function getPagedItems(items, requestedPage, pageSize) {
3574
+ const totalPages = Math.max(1, Math.ceil(items.length / pageSize));
3575
+ const page = clampPage(requestedPage, totalPages);
3576
+ const startIndex = page * pageSize;
3577
+ return {
3578
+ items: items.slice(startIndex, startIndex + pageSize),
3579
+ page,
3580
+ startIndex,
3581
+ totalPages
3582
+ };
3583
+ }
3584
+ function clampPage(page, totalPages) {
3585
+ if (!Number.isInteger(page) || page < 0) return 0;
3586
+ return Math.min(page, totalPages - 1);
3587
+ }
3588
+ //#endregion
3316
3589
  //#region src/bot/presenters/message.presenter.ts
3317
3590
  var VARIANT_ORDER = [
3318
3591
  "minimal",
@@ -3692,7 +3965,7 @@ function formatSessionLabel(session) {
3692
3965
  //#endregion
3693
3966
  //#region src/bot/commands/agents.ts
3694
3967
  async function handleAgentsCommand(ctx, dependencies) {
3695
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
3968
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
3696
3969
  try {
3697
3970
  const result = await dependencies.listAgentsUseCase.execute({ chatId: ctx.chat.id });
3698
3971
  if (result.agents.length === 0) {
@@ -3718,7 +3991,7 @@ function registerAgentsCommand(bot, dependencies) {
3718
3991
  //#endregion
3719
3992
  //#region src/bot/sessions-menu.ts
3720
3993
  async function buildSessionsListView(chatId, requestedPage, dependencies) {
3721
- const copy = await getChatCopy(dependencies.sessionRepo, chatId);
3994
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, chatId, dependencies.logger);
3722
3995
  const result = await dependencies.listSessionsUseCase.execute({ chatId });
3723
3996
  if (result.sessions.length === 0) return {
3724
3997
  copy,
@@ -3777,14 +4050,14 @@ async function getPendingSessionRenameAction(dependencies, chatId) {
3777
4050
  }
3778
4051
  async function replyIfSessionRenamePending(ctx, dependencies) {
3779
4052
  if (!await getPendingSessionRenameAction(dependencies, ctx.chat.id)) return false;
3780
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4053
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
3781
4054
  await ctx.reply(copy.sessions.renamePendingInput);
3782
4055
  return true;
3783
4056
  }
3784
4057
  async function handlePendingSessionRenameText(ctx, dependencies) {
3785
4058
  const pendingAction = await getPendingSessionRenameAction(dependencies, ctx.chat.id);
3786
4059
  if (!pendingAction) return false;
3787
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4060
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
3788
4061
  const title = ctx.message.text?.trim() ?? "";
3789
4062
  if (title.startsWith("/")) {
3790
4063
  await ctx.reply(copy.sessions.renamePendingInput);
@@ -3816,7 +4089,7 @@ async function handlePendingSessionRenameText(ctx, dependencies) {
3816
4089
  async function cancelPendingSessionRename(ctx, dependencies) {
3817
4090
  const pendingAction = await getPendingSessionRenameAction(dependencies, ctx.chat.id);
3818
4091
  if (!pendingAction) return false;
3819
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4092
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
3820
4093
  await dependencies.pendingActionRepo.clear(ctx.chat.id);
3821
4094
  await bestEffortRestoreSessionsList(ctx.api, pendingAction, dependencies);
3822
4095
  await ctx.reply(copy.sessions.renameCancelled);
@@ -3835,7 +4108,7 @@ function isSessionRenamePendingAction(action) {
3835
4108
  //#endregion
3836
4109
  //#region src/bot/commands/cancel.ts
3837
4110
  async function handleCancelCommand(ctx, dependencies) {
3838
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4111
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
3839
4112
  try {
3840
4113
  if (await cancelPendingSessionRename(ctx, dependencies)) return;
3841
4114
  const result = await dependencies.abortPromptUseCase.execute({ chatId: ctx.chat.id });
@@ -3861,8 +4134,8 @@ function registerCancelCommand(bot, dependencies) {
3861
4134
  //#endregion
3862
4135
  //#region src/bot/commands/language.ts
3863
4136
  async function handleLanguageCommand(ctx, dependencies) {
3864
- const language = await getChatLanguage(dependencies.sessionRepo, ctx.chat.id);
3865
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4137
+ const language = await getSafeChatLanguage(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
4138
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
3866
4139
  try {
3867
4140
  await syncTelegramCommandsForChat(ctx.api, ctx.chat.id, language);
3868
4141
  await ctx.reply(presentLanguageMessage(language, copy), { reply_markup: buildLanguageKeyboard(language, copy) });
@@ -3872,7 +4145,7 @@ async function handleLanguageCommand(ctx, dependencies) {
3872
4145
  }
3873
4146
  }
3874
4147
  async function switchLanguageForChat(api, chatId, language, dependencies) {
3875
- const currentCopy = await getChatCopy(dependencies.sessionRepo, chatId);
4148
+ const currentCopy = await getSafeChatCopy(dependencies.sessionRepo, chatId, dependencies.logger);
3876
4149
  if (!isBotLanguage(language)) return {
3877
4150
  found: false,
3878
4151
  copy: currentCopy
@@ -3881,7 +4154,7 @@ async function switchLanguageForChat(api, chatId, language, dependencies) {
3881
4154
  await syncTelegramCommandsForChat(api, chatId, language);
3882
4155
  return {
3883
4156
  found: true,
3884
- copy: await getChatCopy(dependencies.sessionRepo, chatId),
4157
+ copy: await getSafeChatCopy(dependencies.sessionRepo, chatId, dependencies.logger),
3885
4158
  language
3886
4159
  };
3887
4160
  }
@@ -3891,7 +4164,7 @@ async function presentLanguageSwitchForChat(chatId, api, language, dependencies)
3891
4164
  found: false,
3892
4165
  copy: result.copy,
3893
4166
  text: result.copy.language.expired,
3894
- keyboard: buildLanguageKeyboard(await getChatLanguage(dependencies.sessionRepo, chatId), result.copy)
4167
+ keyboard: buildLanguageKeyboard(await getSafeChatLanguage(dependencies.sessionRepo, chatId, dependencies.logger), result.copy)
3895
4168
  };
3896
4169
  return {
3897
4170
  found: true,
@@ -3908,7 +4181,7 @@ function registerLanguageCommand(bot, dependencies) {
3908
4181
  //#endregion
3909
4182
  //#region src/bot/commands/models.ts
3910
4183
  async function handleModelsCommand(ctx, dependencies) {
3911
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4184
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
3912
4185
  try {
3913
4186
  const result = await dependencies.listModelsUseCase.execute({ chatId: ctx.chat.id });
3914
4187
  if (result.models.length === 0) {
@@ -3936,7 +4209,7 @@ function registerModelsCommand(bot, dependencies) {
3936
4209
  //#endregion
3937
4210
  //#region src/bot/commands/new.ts
3938
4211
  async function handleNewCommand(ctx, dependencies) {
3939
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4212
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
3940
4213
  try {
3941
4214
  const title = extractSessionTitle(ctx);
3942
4215
  const result = await dependencies.createSessionUseCase.execute({
@@ -4300,7 +4573,7 @@ function escapeLinkDestination(url) {
4300
4573
  //#endregion
4301
4574
  //#region src/bot/commands/status.ts
4302
4575
  async function handleStatusCommand(ctx, dependencies) {
4303
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat?.id);
4576
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat?.id, dependencies.logger);
4304
4577
  try {
4305
4578
  const result = await dependencies.getStatusUseCase.execute({ chatId: ctx.chat?.id ?? 0 });
4306
4579
  const renderedMarkdown = renderMarkdownToTelegramMarkdownV2(presentStatusMarkdownMessage(result, copy));
@@ -4322,7 +4595,7 @@ function registerStatusCommand(bot, dependencies) {
4322
4595
  //#endregion
4323
4596
  //#region src/bot/commands/sessions.ts
4324
4597
  async function handleSessionsCommand(ctx, dependencies) {
4325
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4598
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
4326
4599
  try {
4327
4600
  await dependencies.pendingActionRepo.clear(ctx.chat.id);
4328
4601
  const view = await buildSessionsListView(ctx.chat.id, 0, dependencies);
@@ -4345,7 +4618,7 @@ function presentStartMarkdownMessage(copy = BOT_COPY) {
4345
4618
  //#endregion
4346
4619
  //#region src/bot/commands/start.ts
4347
4620
  async function handleStartCommand(ctx, dependencies) {
4348
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat?.id);
4621
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat?.id, dependencies.logger);
4349
4622
  const reply = buildTelegramStaticReply(presentStartMarkdownMessage(copy));
4350
4623
  try {
4351
4624
  await ctx.reply(reply.preferred.text, reply.preferred.options);
@@ -4383,7 +4656,7 @@ async function handleAgentsCallback(ctx, dependencies) {
4383
4656
  if (!data.startsWith("agents:")) return;
4384
4657
  await ctx.answerCallbackQuery();
4385
4658
  if (!ctx.chat) return;
4386
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4659
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
4387
4660
  try {
4388
4661
  if (data.startsWith(AGENTS_PAGE_PREFIX)) {
4389
4662
  const requestedPage = Number(data.slice(12));
@@ -4427,7 +4700,7 @@ async function handleModelsCallback(ctx, dependencies) {
4427
4700
  if (!data.startsWith("model:")) return;
4428
4701
  await ctx.answerCallbackQuery();
4429
4702
  if (!ctx.chat) return;
4430
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4703
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
4431
4704
  try {
4432
4705
  if (data.startsWith(MODEL_PAGE_PREFIX)) {
4433
4706
  const requestedPage = Number(data.slice(11));
@@ -4506,7 +4779,7 @@ async function handleSessionsCallback(ctx, dependencies) {
4506
4779
  if (!data.startsWith("sessions:")) return;
4507
4780
  await ctx.answerCallbackQuery();
4508
4781
  if (!ctx.chat) return;
4509
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4782
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
4510
4783
  try {
4511
4784
  if (data.startsWith(SESSIONS_PAGE_PREFIX)) {
4512
4785
  const requestedPage = Number(data.slice(14));
@@ -4587,10 +4860,10 @@ async function handleLanguageCallback(ctx, dependencies) {
4587
4860
  if (!data.startsWith("language:")) return;
4588
4861
  await ctx.answerCallbackQuery();
4589
4862
  if (!ctx.chat || !ctx.api) return;
4590
- const currentCopy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4863
+ const currentCopy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
4591
4864
  try {
4592
4865
  if (!data.startsWith(LANGUAGE_SELECT_PREFIX)) {
4593
- await ctx.editMessageText(presentLanguageMessage(await getChatLanguage(dependencies.sessionRepo, ctx.chat.id), currentCopy), { reply_markup: buildLanguageKeyboard(await getChatLanguage(dependencies.sessionRepo, ctx.chat.id), currentCopy) });
4866
+ 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) });
4594
4867
  return;
4595
4868
  }
4596
4869
  const selectedLanguage = data.slice(16);
@@ -4657,7 +4930,7 @@ function parseSessionActionTarget(data, prefix) {
4657
4930
  //#endregion
4658
4931
  //#region src/bot/handlers/prompt.handler.ts
4659
4932
  async function executePromptRequest(ctx, dependencies, resolvePrompt) {
4660
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4933
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
4661
4934
  const foregroundRequest = dependencies.foregroundSessionTracker.acquire(ctx.chat.id);
4662
4935
  if (!foregroundRequest) {
4663
4936
  await ctx.reply(copy.status.alreadyProcessing);
@@ -4794,7 +5067,7 @@ function registerMessageHandler(bot, dependencies) {
4794
5067
  async function handleVoiceMessage(ctx, dependencies) {
4795
5068
  if (!ctx.message.voice) return;
4796
5069
  if (await replyIfSessionRenamePending(ctx, dependencies)) return;
4797
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
5070
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
4798
5071
  await ctx.reply(copy.errors.voiceUnsupported);
4799
5072
  }
4800
5073
  function registerVoiceHandler(bot, dependencies) {
@@ -4839,45 +5112,119 @@ function createLoggingMiddleware(logger) {
4839
5112
  function registerBot(bot, container, options) {
4840
5113
  bot.use(createLoggingMiddleware(container.logger));
4841
5114
  bot.use(createAuthMiddleware(options.telegramAllowedChatIds));
4842
- registerStartCommand(bot, container);
4843
- registerStatusCommand(bot, container);
4844
- registerNewCommand(bot, container);
4845
- registerAgentsCommand(bot, container);
4846
- registerSessionsCommand(bot, container);
4847
- registerCancelCommand(bot, container);
4848
- registerModelsCommand(bot, container);
4849
- registerLanguageCommand(bot, container);
4850
- registerCallbackHandler(bot, container);
4851
- registerFileHandler(bot, container);
4852
- registerMessageHandler(bot, container);
4853
- registerVoiceHandler(bot, container);
5115
+ const safeBot = bot.errorBoundary(async (error) => {
5116
+ container.logger.error({
5117
+ ...extractTelegramUpdateContext(error.ctx),
5118
+ error: error.error
5119
+ }, "telegram middleware failed");
5120
+ await replyWithDefaultTelegramError(error.ctx, container.logger, error.error);
5121
+ });
5122
+ registerStartCommand(safeBot, container);
5123
+ registerStatusCommand(safeBot, container);
5124
+ registerNewCommand(safeBot, container);
5125
+ registerAgentsCommand(safeBot, container);
5126
+ registerSessionsCommand(safeBot, container);
5127
+ registerCancelCommand(safeBot, container);
5128
+ registerModelsCommand(safeBot, container);
5129
+ registerLanguageCommand(safeBot, container);
5130
+ registerCallbackHandler(safeBot, container);
5131
+ registerFileHandler(safeBot, container);
5132
+ registerMessageHandler(safeBot, container);
5133
+ registerVoiceHandler(safeBot, container);
4854
5134
  }
4855
5135
  //#endregion
4856
5136
  //#region src/app/runtime.ts
5137
+ var TELEGRAM_RUNNER_OPTIONS = { runner: {
5138
+ fetch: { timeout: 30 },
5139
+ maxRetryTime: 900 * 1e3,
5140
+ retryInterval: "exponential",
5141
+ silent: true
5142
+ } };
4857
5143
  async function startTelegramBotRuntime(input) {
4858
- const bot = new Bot(input.config.telegramBotToken, { client: { apiRoot: input.config.telegramApiRoot } });
4859
- registerBot(bot, input.container, { telegramAllowedChatIds: input.config.telegramAllowedChatIds });
5144
+ const runtimeKey = buildTelegramRuntimeKey(input.config);
5145
+ const registry = getTelegramBotRuntimeRegistry();
5146
+ const existingRuntime = registry.activeByKey.get(runtimeKey);
5147
+ if (existingRuntime) {
5148
+ input.container.logger.warn({
5149
+ runtimeKey,
5150
+ telegramApiRoot: input.config.telegramApiRoot
5151
+ }, "telegram runtime already active in this process; reusing the existing runner");
5152
+ await input.container.dispose();
5153
+ return existingRuntime;
5154
+ }
5155
+ const runtimePromise = startTelegramBotRuntimeInternal(input, runtimeKey, () => {
5156
+ if (registry.activeByKey.get(runtimeKey) === runtimePromise) registry.activeByKey.delete(runtimeKey);
5157
+ }).catch((error) => {
5158
+ if (registry.activeByKey.get(runtimeKey) === runtimePromise) registry.activeByKey.delete(runtimeKey);
5159
+ throw error;
5160
+ });
5161
+ registry.activeByKey.set(runtimeKey, runtimePromise);
5162
+ return runtimePromise;
5163
+ }
5164
+ async function startTelegramBotRuntimeInternal(input, runtimeKey, releaseRuntime) {
5165
+ const bot = (input.botFactory ?? ((token, options) => new Bot(token, options)))(input.config.telegramBotToken, { client: { apiRoot: input.config.telegramApiRoot } });
5166
+ wrapTelegramGetUpdates(bot, input.container);
5167
+ (input.registerBotHandlers ?? registerBot)(bot, input.container, { telegramAllowedChatIds: input.config.telegramAllowedChatIds });
4860
5168
  bot.catch((error) => {
5169
+ const metadata = extractTelegramUpdateContext(error.ctx);
5170
+ if (error.error instanceof GrammyError) {
5171
+ input.container.logger.error({
5172
+ ...metadata,
5173
+ errorCode: error.error.error_code,
5174
+ description: error.error.description,
5175
+ method: error.error.method,
5176
+ parameters: error.error.parameters,
5177
+ payload: error.error.payload
5178
+ }, "telegram bot api request failed");
5179
+ return;
5180
+ }
5181
+ if (error.error instanceof HttpError) {
5182
+ input.container.logger.error({
5183
+ ...metadata,
5184
+ error: error.error.error,
5185
+ message: error.error.message
5186
+ }, "telegram bot network request failed");
5187
+ return;
5188
+ }
4861
5189
  input.container.logger.error({
4862
- error: error.error,
4863
- update: error.ctx.update
5190
+ ...metadata,
5191
+ error: error.error
4864
5192
  }, "telegram bot update failed");
4865
5193
  });
4866
- input.container.logger.info("bot starting...");
4867
- if (input.syncCommands ?? true) await syncTelegramCommands(bot, input.container.logger);
4868
- const runner = run(bot);
5194
+ input.container.logger.info({ runtimeKey }, "telegram bot polling starting");
5195
+ const runner = (input.runBot ?? run)(bot, TELEGRAM_RUNNER_OPTIONS);
4869
5196
  let stopped = false;
4870
5197
  let disposed = false;
4871
- const stop = () => {
5198
+ if (input.syncCommands ?? true) (input.syncCommandsHandler ?? syncTelegramCommands)(bot, input.container.logger).catch((error) => {
5199
+ input.container.logger.warn({
5200
+ error,
5201
+ runtimeKey
5202
+ }, "failed to sync telegram commands; polling continues without command registration updates");
5203
+ });
5204
+ let stopPromise = null;
5205
+ const requestStop = async () => {
4872
5206
  if (stopped) return;
4873
5207
  stopped = true;
4874
- runner.stop();
5208
+ stopPromise = runner.stop().catch((error) => {
5209
+ input.container.logger.warn({
5210
+ error,
5211
+ runtimeKey
5212
+ }, "failed to stop telegram runner cleanly");
5213
+ });
5214
+ await stopPromise;
5215
+ };
5216
+ const stop = () => {
5217
+ requestStop();
4875
5218
  };
4876
5219
  const dispose = async () => {
4877
5220
  if (disposed) return;
4878
5221
  disposed = true;
4879
- stop();
4880
- await input.container.dispose();
5222
+ try {
5223
+ await requestStop();
5224
+ await input.container.dispose();
5225
+ } finally {
5226
+ releaseRuntime();
5227
+ }
4881
5228
  };
4882
5229
  return {
4883
5230
  bot,
@@ -4886,39 +5233,69 @@ async function startTelegramBotRuntime(input) {
4886
5233
  dispose
4887
5234
  };
4888
5235
  }
5236
+ function wrapTelegramGetUpdates(bot, container) {
5237
+ const originalGetUpdates = bot.api.getUpdates.bind(bot.api);
5238
+ bot.api.getUpdates = async (options, signal) => {
5239
+ const requestOptions = options ?? {
5240
+ limit: 100,
5241
+ offset: 0,
5242
+ timeout: 30
5243
+ };
5244
+ try {
5245
+ return await originalGetUpdates(requestOptions, signal);
5246
+ } catch (error) {
5247
+ container.logger.warn({
5248
+ error,
5249
+ limit: requestOptions.limit,
5250
+ offset: requestOptions.offset,
5251
+ timeout: requestOptions.timeout
5252
+ }, "telegram getUpdates failed");
5253
+ throw error;
5254
+ }
5255
+ };
5256
+ }
5257
+ function buildTelegramRuntimeKey(config) {
5258
+ return `${config.telegramApiRoot}::${config.telegramBotToken}`;
5259
+ }
5260
+ function getTelegramBotRuntimeRegistry() {
5261
+ const globalScope = globalThis;
5262
+ globalScope.__opencodeTbotTelegramRuntimeRegistry__ ??= { activeByKey: /* @__PURE__ */ new Map() };
5263
+ return globalScope.__opencodeTbotTelegramRuntimeRegistry__;
5264
+ }
4889
5265
  //#endregion
4890
5266
  //#region src/plugin.ts
4891
- var runtimeState = null;
4892
5267
  async function ensureTelegramBotPluginRuntime(options) {
5268
+ const runtimeStateHolder = getTelegramBotPluginRuntimeStateHolder();
4893
5269
  const cwd = resolvePluginRuntimeCwd(options.context);
4894
- if (runtimeState && runtimeState.cwd !== cwd) {
4895
- const activeState = runtimeState;
4896
- runtimeState = null;
5270
+ if (runtimeStateHolder.state && runtimeStateHolder.state.cwd !== cwd) {
5271
+ const activeState = runtimeStateHolder.state;
5272
+ runtimeStateHolder.state = null;
4897
5273
  await disposeTelegramBotPluginRuntimeState(activeState);
4898
5274
  }
4899
- if (!runtimeState) {
5275
+ if (!runtimeStateHolder.state) {
4900
5276
  const runtimePromise = startPluginRuntime(options, cwd).then((runtime) => {
4901
- if (runtimeState?.runtimePromise === runtimePromise) runtimeState.runtime = runtime;
5277
+ if (runtimeStateHolder.state?.runtimePromise === runtimePromise) runtimeStateHolder.state.runtime = runtime;
4902
5278
  return runtime;
4903
5279
  }).catch((error) => {
4904
- if (runtimeState?.runtimePromise === runtimePromise) runtimeState = null;
5280
+ if (runtimeStateHolder.state?.runtimePromise === runtimePromise) runtimeStateHolder.state = null;
4905
5281
  throw error;
4906
5282
  });
4907
- runtimeState = {
5283
+ runtimeStateHolder.state = {
4908
5284
  cwd,
4909
5285
  runtime: null,
4910
5286
  runtimePromise
4911
5287
  };
4912
5288
  }
4913
- return runtimeState.runtimePromise;
5289
+ return runtimeStateHolder.state.runtimePromise;
4914
5290
  }
4915
5291
  var TelegramBotPlugin = async (context) => {
4916
5292
  return createHooks(await ensureTelegramBotPluginRuntime({ context }));
4917
5293
  };
4918
5294
  async function resetTelegramBotPluginRuntimeForTests() {
4919
- if (!runtimeState) return;
4920
- const activeState = runtimeState;
4921
- runtimeState = null;
5295
+ const runtimeStateHolder = getTelegramBotPluginRuntimeStateHolder();
5296
+ if (!runtimeStateHolder.state) return;
5297
+ const activeState = runtimeStateHolder.state;
5298
+ runtimeStateHolder.state = null;
4922
5299
  await disposeTelegramBotPluginRuntimeState(activeState);
4923
5300
  }
4924
5301
  async function startPluginRuntime(options, cwd) {
@@ -4971,6 +5348,11 @@ function createHooks(runtime) {
4971
5348
  }
4972
5349
  };
4973
5350
  }
5351
+ function getTelegramBotPluginRuntimeStateHolder() {
5352
+ const globalScope = globalThis;
5353
+ globalScope.__opencodeTbotPluginRuntimeState__ ??= { state: null };
5354
+ return globalScope.__opencodeTbotPluginRuntimeState__;
5355
+ }
4974
5356
  //#endregion
4975
5357
  export { TelegramBotPlugin, TelegramBotPlugin as default, ensureTelegramBotPluginRuntime, resetTelegramBotPluginRuntimeForTests };
4976
5358