opencode-tbot 0.1.28 → 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,7 +303,10 @@ 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
312
  try {
@@ -303,25 +317,43 @@ var OpenCodeClient = class {
303
317
  }
304
318
  }
305
319
  async abortSession(sessionId) {
306
- return this.callScopedSdkMethod("session", "abort", { parameters: { sessionID: sessionId } });
320
+ return this.callScopedSdkMethod("session", "abort", {
321
+ legacyParameters: { sessionID: sessionId },
322
+ parameters: { path: { id: sessionId } }
323
+ });
307
324
  }
308
325
  async deleteSession(sessionId) {
309
- return this.callScopedSdkMethod("session", "delete", { parameters: { sessionID: sessionId } });
326
+ return this.callScopedSdkMethod("session", "delete", {
327
+ legacyParameters: { sessionID: sessionId },
328
+ parameters: { path: { id: sessionId } }
329
+ });
310
330
  }
311
331
  async forkSession(sessionId, messageId) {
312
- return this.callScopedSdkMethod("session", "fork", { parameters: {
313
- sessionID: sessionId,
314
- ...messageId?.trim() ? { messageID: messageId.trim() } : {}
315
- } });
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
+ });
316
342
  }
317
343
  async getPath() {
318
344
  return this.callScopedSdkMethod("path", "get", {});
319
345
  }
320
346
  async listLspStatuses(directory) {
321
- 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
+ });
322
351
  }
323
352
  async listMcpStatuses(directory) {
324
- 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
+ });
325
357
  }
326
358
  async getSessionStatuses() {
327
359
  return this.loadSessionStatuses();
@@ -336,29 +368,69 @@ var OpenCodeClient = class {
336
368
  return this.callScopedSdkMethod("project", "current", {});
337
369
  }
338
370
  async createSessionForDirectory(directory, title) {
339
- return this.callScopedSdkMethod("session", "create", { parameters: title ? {
340
- directory,
341
- title
342
- } : { 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
+ });
343
381
  }
344
382
  async renameSession(sessionId, title) {
345
- return this.callScopedSdkMethod("session", "update", { parameters: {
346
- sessionID: sessionId,
347
- title
348
- } });
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
+ });
349
393
  }
350
394
  async listAgents() {
351
395
  return this.callScopedSdkMethod("app", "agents", {});
352
396
  }
353
397
  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
- } });
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
+ });
362
434
  }
363
435
  async listModels() {
364
436
  const now = Date.now();
@@ -494,10 +566,14 @@ var OpenCodeClient = class {
494
566
  messageId
495
567
  }, async (requestSignal) => {
496
568
  return normalizePromptResponse(await this.callScopedSdkMethod("session", "message", {
497
- parameters: {
569
+ legacyParameters: {
498
570
  sessionID: sessionId,
499
571
  messageID: messageId
500
572
  },
573
+ parameters: { path: {
574
+ id: sessionId,
575
+ messageID: messageId
576
+ } },
501
577
  signal: requestSignal
502
578
  }));
503
579
  }, signal);
@@ -525,10 +601,14 @@ var OpenCodeClient = class {
525
601
  timeoutMs: this.promptTimeoutPolicy.pollRequestTimeoutMs
526
602
  }, async (requestSignal) => {
527
603
  return normalizePromptResponses(await this.callScopedSdkMethod("session", "messages", {
528
- parameters: {
604
+ legacyParameters: {
529
605
  sessionID: sessionId,
530
606
  limit: PROMPT_MESSAGE_POLL_LIMIT
531
607
  },
608
+ parameters: {
609
+ path: { id: sessionId },
610
+ query: { limit: PROMPT_MESSAGE_POLL_LIMIT }
611
+ },
532
612
  signal: requestSignal
533
613
  }));
534
614
  }, signal);
@@ -589,10 +669,14 @@ var OpenCodeClient = class {
589
669
  ...input.variant ? { variant: input.variant } : {},
590
670
  parts
591
671
  };
592
- const requestParameters = {
672
+ const legacyRequestParameters = {
593
673
  sessionID: input.sessionId,
594
674
  ...requestBody
595
675
  };
676
+ const requestParameters = {
677
+ body: requestBody,
678
+ path: { id: input.sessionId }
679
+ };
596
680
  try {
597
681
  if (typeof this.client.session?.promptAsync === "function") {
598
682
  await this.runPromptRequestWithTimeout({
@@ -601,6 +685,7 @@ var OpenCodeClient = class {
601
685
  timeoutMs: this.promptTimeoutPolicy.waitTimeoutMs
602
686
  }, async (signal) => {
603
687
  await this.callScopedSdkMethod("session", "promptAsync", {
688
+ legacyParameters: legacyRequestParameters,
604
689
  parameters: requestParameters,
605
690
  signal
606
691
  });
@@ -618,10 +703,7 @@ var OpenCodeClient = class {
618
703
  throw new Error("OpenCode SDK client does not expose session.promptAsync().");
619
704
  }
620
705
  async loadSessionStatuses(signal) {
621
- return this.callScopedSdkMethod("session", "status", {
622
- signal,
623
- parameters: void 0
624
- });
706
+ return this.callScopedSdkMethod("session", "status", { signal });
625
707
  }
626
708
  async callRawSdkGet(url, signal) {
627
709
  const rawClient = getRawSdkRequestClient(this.client);
@@ -698,12 +780,16 @@ var OpenCodeClient = class {
698
780
  logPromptRequest(level, extra, message) {
699
781
  const log = this.client.app?.log;
700
782
  if (typeof log !== "function") return;
701
- log.call(this.client.app, {
783
+ const payload = {
702
784
  service: PROMPT_LOG_SERVICE,
703
785
  level,
704
786
  message,
705
787
  extra
706
- }).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);
707
793
  }
708
794
  };
709
795
  function createOpenCodeClientFromSdkClient(client, fetchFn = fetch, promptTimeoutPolicy = {}) {
@@ -825,7 +911,7 @@ function isPromptResponseUsable(data, structured) {
825
911
  }
826
912
  function normalizePromptResponse(response) {
827
913
  return {
828
- info: isPlainRecord(response?.info) ? response.info : null,
914
+ info: isPlainRecord$1(response?.info) ? response.info : null,
829
915
  parts: normalizePromptParts(response?.parts)
830
916
  };
831
917
  }
@@ -849,7 +935,7 @@ function toAssistantMessage(message) {
849
935
  if ("mode" in message && typeof message.mode === "string" && message.mode.trim().length > 0) normalized.mode = message.mode;
850
936
  if ("modelID" in message && typeof message.modelID === "string" && message.modelID.trim().length > 0) normalized.modelID = message.modelID;
851
937
  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 = {
938
+ if ("path" in message && isPlainRecord$1(message.path)) normalized.path = {
853
939
  ...typeof message.path.cwd === "string" && message.path.cwd.trim().length > 0 ? { cwd: message.path.cwd } : {},
854
940
  ...typeof message.path.root === "string" && message.path.root.trim().length > 0 ? { root: message.path.root } : {}
855
941
  };
@@ -859,16 +945,16 @@ function toAssistantMessage(message) {
859
945
  const structuredPayload = extractStructuredPayload(message);
860
946
  if (structuredPayload !== null) normalized.structured = structuredPayload;
861
947
  if ("summary" in message && typeof message.summary === "boolean") normalized.summary = message.summary;
862
- if ("time" in message && isPlainRecord(message.time)) normalized.time = {
948
+ if ("time" in message && isPlainRecord$1(message.time)) normalized.time = {
863
949
  ...typeof message.time.created === "number" && Number.isFinite(message.time.created) ? { created: message.time.created } : {},
864
950
  ...typeof message.time.completed === "number" && Number.isFinite(message.time.completed) ? { completed: message.time.completed } : {}
865
951
  };
866
- if ("tokens" in message && isPlainRecord(message.tokens)) normalized.tokens = {
952
+ if ("tokens" in message && isPlainRecord$1(message.tokens)) normalized.tokens = {
867
953
  ...typeof message.tokens.input === "number" && Number.isFinite(message.tokens.input) ? { input: message.tokens.input } : {},
868
954
  ...typeof message.tokens.output === "number" && Number.isFinite(message.tokens.output) ? { output: message.tokens.output } : {},
869
955
  ...typeof message.tokens.reasoning === "number" && Number.isFinite(message.tokens.reasoning) ? { reasoning: message.tokens.reasoning } : {},
870
956
  ...typeof message.tokens.total === "number" && Number.isFinite(message.tokens.total) ? { total: message.tokens.total } : {},
871
- ...isPlainRecord(message.tokens.cache) ? { cache: {
957
+ ...isPlainRecord$1(message.tokens.cache) ? { cache: {
872
958
  ...typeof message.tokens.cache.read === "number" && Number.isFinite(message.tokens.cache.read) ? { read: message.tokens.cache.read } : {},
873
959
  ...typeof message.tokens.cache.write === "number" && Number.isFinite(message.tokens.cache.write) ? { write: message.tokens.cache.write } : {}
874
960
  } } : {}
@@ -877,7 +963,7 @@ function toAssistantMessage(message) {
877
963
  return normalized;
878
964
  }
879
965
  function extractMessageId(message) {
880
- if (!isPlainRecord(message)) return null;
966
+ if (!isPlainRecord$1(message)) return null;
881
967
  return typeof message.id === "string" && message.id.trim().length > 0 ? message.id : null;
882
968
  }
883
969
  function delay(ms, signal) {
@@ -973,6 +1059,27 @@ function unwrapSdkData(response) {
973
1059
  if (response && typeof response === "object" && "data" in response) return response.data;
974
1060
  return response;
975
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
+ }
976
1083
  function getRawSdkRequestClient(client) {
977
1084
  const compatibleClient = client;
978
1085
  return compatibleClient.client ?? compatibleClient._client ?? null;
@@ -988,11 +1095,11 @@ function resolvePromptTimeoutPolicy(input) {
988
1095
  };
989
1096
  }
990
1097
  function normalizeAssistantError(value) {
991
- 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;
992
1099
  return {
993
1100
  ...value,
994
1101
  name: value.name,
995
- ...isPlainRecord(value.data) ? { data: value.data } : {}
1102
+ ...isPlainRecord$1(value.data) ? { data: value.data } : {}
996
1103
  };
997
1104
  }
998
1105
  function isAssistantMessageCompleted(message) {
@@ -1005,7 +1112,7 @@ function isCompletedEmptyPromptResponse(data, structured) {
1005
1112
  return isAssistantMessageCompleted(assistantInfo) && !assistantInfo?.error && !hasText && !bodyMd;
1006
1113
  }
1007
1114
  function extractStructuredPayload(message) {
1008
- if (!isPlainRecord(message)) return null;
1115
+ if (!isPlainRecord$1(message)) return null;
1009
1116
  if ("structured" in message && message.structured !== void 0) return message.structured;
1010
1117
  if ("structured_output" in message && message.structured_output !== void 0) return message.structured_output;
1011
1118
  return null;
@@ -1079,7 +1186,7 @@ function throwIfAborted(signal) {
1079
1186
  if (!signal?.aborted) return;
1080
1187
  throw normalizeAbortReason(signal.reason);
1081
1188
  }
1082
- function isPlainRecord(value) {
1189
+ function isPlainRecord$1(value) {
1083
1190
  return value !== null && typeof value === "object" && !Array.isArray(value);
1084
1191
  }
1085
1192
  async function resolveProviderAvailability(config, fetchFn) {
@@ -2295,25 +2402,35 @@ function escapeMarkdownV2(value) {
2295
2402
  async function handleTelegramBotPluginEvent(runtime, event) {
2296
2403
  switch (event.type) {
2297
2404
  case "permission.asked":
2298
- await handlePermissionAsked(runtime, event);
2405
+ case "permission.updated": {
2406
+ const request = normalizePermissionRequestEvent(event.properties);
2407
+ if (request) await handlePermissionAsked(runtime, request);
2299
2408
  return;
2300
- case "permission.replied":
2301
- await handlePermissionReplied(runtime, event);
2409
+ }
2410
+ case "permission.replied": {
2411
+ const replyEvent = normalizePermissionReplyEvent(event.properties);
2412
+ if (replyEvent) await handlePermissionReplied(runtime, replyEvent);
2302
2413
  return;
2303
- case "session.error":
2304
- await handleSessionError(runtime, event);
2414
+ }
2415
+ case "session.error": {
2416
+ const sessionError = normalizeSessionErrorEvent(event.properties);
2417
+ if (sessionError) await handleSessionError(runtime, sessionError);
2305
2418
  return;
2306
- case "session.idle":
2307
- await handleSessionIdle(runtime, event);
2419
+ }
2420
+ case "session.idle": {
2421
+ const sessionIdle = normalizeSessionIdleEvent(event.properties);
2422
+ if (sessionIdle) await handleSessionIdle(runtime, sessionIdle);
2308
2423
  return;
2309
- case "session.status":
2310
- await handleSessionStatus(runtime, event);
2424
+ }
2425
+ case "session.status": {
2426
+ const sessionStatus = normalizeSessionStatusEvent(event.properties);
2427
+ if (sessionStatus) await handleSessionStatus(runtime, sessionStatus);
2311
2428
  return;
2429
+ }
2312
2430
  default: return;
2313
2431
  }
2314
2432
  }
2315
- async function handlePermissionAsked(runtime, event) {
2316
- const request = event.properties;
2433
+ async function handlePermissionAsked(runtime, request) {
2317
2434
  const bindings = await runtime.container.sessionRepo.listBySessionId(request.sessionID);
2318
2435
  const chatIds = new Set([...bindings.map((binding) => binding.chatId), ...runtime.container.foregroundSessionTracker.listChatIds(request.sessionID)]);
2319
2436
  const approvals = await runtime.container.permissionApprovalRepo.listByRequestId(request.id);
@@ -2343,52 +2460,42 @@ async function handlePermissionAsked(runtime, event) {
2343
2460
  }
2344
2461
  }
2345
2462
  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);
2463
+ const approvals = await runtime.container.permissionApprovalRepo.listByRequestId(event.requestId);
2349
2464
  await Promise.all(approvals.map(async (approval) => {
2350
2465
  try {
2351
- 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));
2352
2467
  } catch (error) {
2353
2468
  runtime.container.logger.warn({
2354
2469
  error,
2355
2470
  chatId: approval.chatId,
2356
- requestId
2471
+ requestId: event.requestId,
2472
+ sessionId: event.sessionId
2357
2473
  }, "failed to update Telegram permission message");
2358
2474
  }
2359
- await runtime.container.permissionApprovalRepo.set(toResolvedApproval(approval, reply));
2475
+ await runtime.container.permissionApprovalRepo.set(toResolvedApproval(approval, event.reply));
2360
2476
  }));
2361
2477
  }
2362
2478
  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."))) {
2479
+ if (runtime.container.foregroundSessionTracker.fail(event.sessionId, event.error instanceof Error ? event.error : /* @__PURE__ */ new Error("Unknown session error."))) {
2370
2480
  runtime.container.logger.warn({
2371
- error,
2372
- sessionId
2481
+ error: event.error,
2482
+ sessionId: event.sessionId
2373
2483
  }, "session error suppressed for foreground Telegram session");
2374
2484
  return;
2375
2485
  }
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."}`);
2486
+ const message = extractSessionErrorMessage(event.error) ?? "Unknown session error.";
2487
+ await notifyBoundChats(runtime, event.sessionId, `Session failed.\n\nSession: ${event.sessionId}\nError: ${message}`);
2377
2488
  }
2378
2489
  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");
2490
+ if (runtime.container.foregroundSessionTracker.clear(event.sessionId)) {
2491
+ runtime.container.logger.info({ sessionId: event.sessionId }, "session idle notification suppressed for foreground Telegram session");
2382
2492
  return;
2383
2493
  }
2384
- await notifyBoundChats(runtime, sessionId, `Session finished.\n\nSession: ${sessionId}`);
2494
+ await notifyBoundChats(runtime, event.sessionId, `Session finished.\n\nSession: ${event.sessionId}`);
2385
2495
  }
2386
2496
  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
- });
2497
+ if (event.statusType !== "idle") return;
2498
+ await handleSessionIdle(runtime, event);
2392
2499
  }
2393
2500
  async function notifyBoundChats(runtime, sessionId, text) {
2394
2501
  const bindings = await runtime.container.sessionRepo.listBySessionId(sessionId);
@@ -2412,6 +2519,79 @@ function toResolvedApproval(approval, reply) {
2412
2519
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2413
2520
  };
2414
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
+ }
2415
2595
  var SUPPORTED_BOT_LANGUAGES = [
2416
2596
  "en",
2417
2597
  "zh-CN",
@@ -3115,111 +3295,6 @@ async function syncTelegramCommandsForChat(api, chatId, language) {
3115
3295
  } });
3116
3296
  }
3117
3297
  //#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);
3146
- return {
3147
- keyboard,
3148
- page
3149
- };
3150
- }
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
- };
3159
- }
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);
3164
- return {
3165
- keyboard,
3166
- page
3167
- };
3168
- }
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;
3183
- }
3184
- function getModelsPage(models, requestedPage) {
3185
- return getPagedItems(models, requestedPage, 10);
3186
- }
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);
3221
- }
3222
- //#endregion
3223
3298
  //#region src/bot/presenters/error.presenter.ts
3224
3299
  function presentError(error, copy = BOT_COPY) {
3225
3300
  const presented = normalizeError(error, copy);
@@ -3334,6 +3409,183 @@ function stringifyUnknown(value) {
3334
3409
  }
3335
3410
  }
3336
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
3337
3589
  //#region src/bot/presenters/message.presenter.ts
3338
3590
  var VARIANT_ORDER = [
3339
3591
  "minimal",
@@ -3713,7 +3965,7 @@ function formatSessionLabel(session) {
3713
3965
  //#endregion
3714
3966
  //#region src/bot/commands/agents.ts
3715
3967
  async function handleAgentsCommand(ctx, dependencies) {
3716
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
3968
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
3717
3969
  try {
3718
3970
  const result = await dependencies.listAgentsUseCase.execute({ chatId: ctx.chat.id });
3719
3971
  if (result.agents.length === 0) {
@@ -3739,7 +3991,7 @@ function registerAgentsCommand(bot, dependencies) {
3739
3991
  //#endregion
3740
3992
  //#region src/bot/sessions-menu.ts
3741
3993
  async function buildSessionsListView(chatId, requestedPage, dependencies) {
3742
- const copy = await getChatCopy(dependencies.sessionRepo, chatId);
3994
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, chatId, dependencies.logger);
3743
3995
  const result = await dependencies.listSessionsUseCase.execute({ chatId });
3744
3996
  if (result.sessions.length === 0) return {
3745
3997
  copy,
@@ -3798,14 +4050,14 @@ async function getPendingSessionRenameAction(dependencies, chatId) {
3798
4050
  }
3799
4051
  async function replyIfSessionRenamePending(ctx, dependencies) {
3800
4052
  if (!await getPendingSessionRenameAction(dependencies, ctx.chat.id)) return false;
3801
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4053
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
3802
4054
  await ctx.reply(copy.sessions.renamePendingInput);
3803
4055
  return true;
3804
4056
  }
3805
4057
  async function handlePendingSessionRenameText(ctx, dependencies) {
3806
4058
  const pendingAction = await getPendingSessionRenameAction(dependencies, ctx.chat.id);
3807
4059
  if (!pendingAction) return false;
3808
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4060
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
3809
4061
  const title = ctx.message.text?.trim() ?? "";
3810
4062
  if (title.startsWith("/")) {
3811
4063
  await ctx.reply(copy.sessions.renamePendingInput);
@@ -3837,7 +4089,7 @@ async function handlePendingSessionRenameText(ctx, dependencies) {
3837
4089
  async function cancelPendingSessionRename(ctx, dependencies) {
3838
4090
  const pendingAction = await getPendingSessionRenameAction(dependencies, ctx.chat.id);
3839
4091
  if (!pendingAction) return false;
3840
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4092
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
3841
4093
  await dependencies.pendingActionRepo.clear(ctx.chat.id);
3842
4094
  await bestEffortRestoreSessionsList(ctx.api, pendingAction, dependencies);
3843
4095
  await ctx.reply(copy.sessions.renameCancelled);
@@ -3856,7 +4108,7 @@ function isSessionRenamePendingAction(action) {
3856
4108
  //#endregion
3857
4109
  //#region src/bot/commands/cancel.ts
3858
4110
  async function handleCancelCommand(ctx, dependencies) {
3859
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4111
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
3860
4112
  try {
3861
4113
  if (await cancelPendingSessionRename(ctx, dependencies)) return;
3862
4114
  const result = await dependencies.abortPromptUseCase.execute({ chatId: ctx.chat.id });
@@ -3882,8 +4134,8 @@ function registerCancelCommand(bot, dependencies) {
3882
4134
  //#endregion
3883
4135
  //#region src/bot/commands/language.ts
3884
4136
  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);
4137
+ const language = await getSafeChatLanguage(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
4138
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
3887
4139
  try {
3888
4140
  await syncTelegramCommandsForChat(ctx.api, ctx.chat.id, language);
3889
4141
  await ctx.reply(presentLanguageMessage(language, copy), { reply_markup: buildLanguageKeyboard(language, copy) });
@@ -3893,7 +4145,7 @@ async function handleLanguageCommand(ctx, dependencies) {
3893
4145
  }
3894
4146
  }
3895
4147
  async function switchLanguageForChat(api, chatId, language, dependencies) {
3896
- const currentCopy = await getChatCopy(dependencies.sessionRepo, chatId);
4148
+ const currentCopy = await getSafeChatCopy(dependencies.sessionRepo, chatId, dependencies.logger);
3897
4149
  if (!isBotLanguage(language)) return {
3898
4150
  found: false,
3899
4151
  copy: currentCopy
@@ -3902,7 +4154,7 @@ async function switchLanguageForChat(api, chatId, language, dependencies) {
3902
4154
  await syncTelegramCommandsForChat(api, chatId, language);
3903
4155
  return {
3904
4156
  found: true,
3905
- copy: await getChatCopy(dependencies.sessionRepo, chatId),
4157
+ copy: await getSafeChatCopy(dependencies.sessionRepo, chatId, dependencies.logger),
3906
4158
  language
3907
4159
  };
3908
4160
  }
@@ -3912,7 +4164,7 @@ async function presentLanguageSwitchForChat(chatId, api, language, dependencies)
3912
4164
  found: false,
3913
4165
  copy: result.copy,
3914
4166
  text: result.copy.language.expired,
3915
- keyboard: buildLanguageKeyboard(await getChatLanguage(dependencies.sessionRepo, chatId), result.copy)
4167
+ keyboard: buildLanguageKeyboard(await getSafeChatLanguage(dependencies.sessionRepo, chatId, dependencies.logger), result.copy)
3916
4168
  };
3917
4169
  return {
3918
4170
  found: true,
@@ -3929,7 +4181,7 @@ function registerLanguageCommand(bot, dependencies) {
3929
4181
  //#endregion
3930
4182
  //#region src/bot/commands/models.ts
3931
4183
  async function handleModelsCommand(ctx, dependencies) {
3932
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4184
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
3933
4185
  try {
3934
4186
  const result = await dependencies.listModelsUseCase.execute({ chatId: ctx.chat.id });
3935
4187
  if (result.models.length === 0) {
@@ -3957,7 +4209,7 @@ function registerModelsCommand(bot, dependencies) {
3957
4209
  //#endregion
3958
4210
  //#region src/bot/commands/new.ts
3959
4211
  async function handleNewCommand(ctx, dependencies) {
3960
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4212
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
3961
4213
  try {
3962
4214
  const title = extractSessionTitle(ctx);
3963
4215
  const result = await dependencies.createSessionUseCase.execute({
@@ -4321,7 +4573,7 @@ function escapeLinkDestination(url) {
4321
4573
  //#endregion
4322
4574
  //#region src/bot/commands/status.ts
4323
4575
  async function handleStatusCommand(ctx, dependencies) {
4324
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat?.id);
4576
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat?.id, dependencies.logger);
4325
4577
  try {
4326
4578
  const result = await dependencies.getStatusUseCase.execute({ chatId: ctx.chat?.id ?? 0 });
4327
4579
  const renderedMarkdown = renderMarkdownToTelegramMarkdownV2(presentStatusMarkdownMessage(result, copy));
@@ -4343,7 +4595,7 @@ function registerStatusCommand(bot, dependencies) {
4343
4595
  //#endregion
4344
4596
  //#region src/bot/commands/sessions.ts
4345
4597
  async function handleSessionsCommand(ctx, dependencies) {
4346
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4598
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
4347
4599
  try {
4348
4600
  await dependencies.pendingActionRepo.clear(ctx.chat.id);
4349
4601
  const view = await buildSessionsListView(ctx.chat.id, 0, dependencies);
@@ -4366,7 +4618,7 @@ function presentStartMarkdownMessage(copy = BOT_COPY) {
4366
4618
  //#endregion
4367
4619
  //#region src/bot/commands/start.ts
4368
4620
  async function handleStartCommand(ctx, dependencies) {
4369
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat?.id);
4621
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat?.id, dependencies.logger);
4370
4622
  const reply = buildTelegramStaticReply(presentStartMarkdownMessage(copy));
4371
4623
  try {
4372
4624
  await ctx.reply(reply.preferred.text, reply.preferred.options);
@@ -4404,7 +4656,7 @@ async function handleAgentsCallback(ctx, dependencies) {
4404
4656
  if (!data.startsWith("agents:")) return;
4405
4657
  await ctx.answerCallbackQuery();
4406
4658
  if (!ctx.chat) return;
4407
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4659
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
4408
4660
  try {
4409
4661
  if (data.startsWith(AGENTS_PAGE_PREFIX)) {
4410
4662
  const requestedPage = Number(data.slice(12));
@@ -4448,7 +4700,7 @@ async function handleModelsCallback(ctx, dependencies) {
4448
4700
  if (!data.startsWith("model:")) return;
4449
4701
  await ctx.answerCallbackQuery();
4450
4702
  if (!ctx.chat) return;
4451
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4703
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
4452
4704
  try {
4453
4705
  if (data.startsWith(MODEL_PAGE_PREFIX)) {
4454
4706
  const requestedPage = Number(data.slice(11));
@@ -4527,7 +4779,7 @@ async function handleSessionsCallback(ctx, dependencies) {
4527
4779
  if (!data.startsWith("sessions:")) return;
4528
4780
  await ctx.answerCallbackQuery();
4529
4781
  if (!ctx.chat) return;
4530
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4782
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
4531
4783
  try {
4532
4784
  if (data.startsWith(SESSIONS_PAGE_PREFIX)) {
4533
4785
  const requestedPage = Number(data.slice(14));
@@ -4608,10 +4860,10 @@ async function handleLanguageCallback(ctx, dependencies) {
4608
4860
  if (!data.startsWith("language:")) return;
4609
4861
  await ctx.answerCallbackQuery();
4610
4862
  if (!ctx.chat || !ctx.api) return;
4611
- const currentCopy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4863
+ const currentCopy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
4612
4864
  try {
4613
4865
  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) });
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) });
4615
4867
  return;
4616
4868
  }
4617
4869
  const selectedLanguage = data.slice(16);
@@ -4678,7 +4930,7 @@ function parseSessionActionTarget(data, prefix) {
4678
4930
  //#endregion
4679
4931
  //#region src/bot/handlers/prompt.handler.ts
4680
4932
  async function executePromptRequest(ctx, dependencies, resolvePrompt) {
4681
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4933
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
4682
4934
  const foregroundRequest = dependencies.foregroundSessionTracker.acquire(ctx.chat.id);
4683
4935
  if (!foregroundRequest) {
4684
4936
  await ctx.reply(copy.status.alreadyProcessing);
@@ -4815,7 +5067,7 @@ function registerMessageHandler(bot, dependencies) {
4815
5067
  async function handleVoiceMessage(ctx, dependencies) {
4816
5068
  if (!ctx.message.voice) return;
4817
5069
  if (await replyIfSessionRenamePending(ctx, dependencies)) return;
4818
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
5070
+ const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
4819
5071
  await ctx.reply(copy.errors.voiceUnsupported);
4820
5072
  }
4821
5073
  function registerVoiceHandler(bot, dependencies) {
@@ -4860,45 +5112,119 @@ function createLoggingMiddleware(logger) {
4860
5112
  function registerBot(bot, container, options) {
4861
5113
  bot.use(createLoggingMiddleware(container.logger));
4862
5114
  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);
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);
4875
5134
  }
4876
5135
  //#endregion
4877
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
+ } };
4878
5143
  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 });
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 });
4881
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
+ }
4882
5189
  input.container.logger.error({
4883
- error: error.error,
4884
- update: error.ctx.update
5190
+ ...metadata,
5191
+ error: error.error
4885
5192
  }, "telegram bot update failed");
4886
5193
  });
4887
- input.container.logger.info("bot starting...");
4888
- if (input.syncCommands ?? true) await syncTelegramCommands(bot, input.container.logger);
4889
- const runner = run(bot);
5194
+ input.container.logger.info({ runtimeKey }, "telegram bot polling starting");
5195
+ const runner = (input.runBot ?? run)(bot, TELEGRAM_RUNNER_OPTIONS);
4890
5196
  let stopped = false;
4891
5197
  let disposed = false;
4892
- 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 () => {
4893
5206
  if (stopped) return;
4894
5207
  stopped = true;
4895
- 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();
4896
5218
  };
4897
5219
  const dispose = async () => {
4898
5220
  if (disposed) return;
4899
5221
  disposed = true;
4900
- stop();
4901
- await input.container.dispose();
5222
+ try {
5223
+ await requestStop();
5224
+ await input.container.dispose();
5225
+ } finally {
5226
+ releaseRuntime();
5227
+ }
4902
5228
  };
4903
5229
  return {
4904
5230
  bot,
@@ -4907,39 +5233,69 @@ async function startTelegramBotRuntime(input) {
4907
5233
  dispose
4908
5234
  };
4909
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
+ }
4910
5265
  //#endregion
4911
5266
  //#region src/plugin.ts
4912
- var runtimeState = null;
4913
5267
  async function ensureTelegramBotPluginRuntime(options) {
5268
+ const runtimeStateHolder = getTelegramBotPluginRuntimeStateHolder();
4914
5269
  const cwd = resolvePluginRuntimeCwd(options.context);
4915
- if (runtimeState && runtimeState.cwd !== cwd) {
4916
- const activeState = runtimeState;
4917
- runtimeState = null;
5270
+ if (runtimeStateHolder.state && runtimeStateHolder.state.cwd !== cwd) {
5271
+ const activeState = runtimeStateHolder.state;
5272
+ runtimeStateHolder.state = null;
4918
5273
  await disposeTelegramBotPluginRuntimeState(activeState);
4919
5274
  }
4920
- if (!runtimeState) {
5275
+ if (!runtimeStateHolder.state) {
4921
5276
  const runtimePromise = startPluginRuntime(options, cwd).then((runtime) => {
4922
- if (runtimeState?.runtimePromise === runtimePromise) runtimeState.runtime = runtime;
5277
+ if (runtimeStateHolder.state?.runtimePromise === runtimePromise) runtimeStateHolder.state.runtime = runtime;
4923
5278
  return runtime;
4924
5279
  }).catch((error) => {
4925
- if (runtimeState?.runtimePromise === runtimePromise) runtimeState = null;
5280
+ if (runtimeStateHolder.state?.runtimePromise === runtimePromise) runtimeStateHolder.state = null;
4926
5281
  throw error;
4927
5282
  });
4928
- runtimeState = {
5283
+ runtimeStateHolder.state = {
4929
5284
  cwd,
4930
5285
  runtime: null,
4931
5286
  runtimePromise
4932
5287
  };
4933
5288
  }
4934
- return runtimeState.runtimePromise;
5289
+ return runtimeStateHolder.state.runtimePromise;
4935
5290
  }
4936
5291
  var TelegramBotPlugin = async (context) => {
4937
5292
  return createHooks(await ensureTelegramBotPluginRuntime({ context }));
4938
5293
  };
4939
5294
  async function resetTelegramBotPluginRuntimeForTests() {
4940
- if (!runtimeState) return;
4941
- const activeState = runtimeState;
4942
- runtimeState = null;
5295
+ const runtimeStateHolder = getTelegramBotPluginRuntimeStateHolder();
5296
+ if (!runtimeStateHolder.state) return;
5297
+ const activeState = runtimeStateHolder.state;
5298
+ runtimeStateHolder.state = null;
4943
5299
  await disposeTelegramBotPluginRuntimeState(activeState);
4944
5300
  }
4945
5301
  async function startPluginRuntime(options, cwd) {
@@ -4992,6 +5348,11 @@ function createHooks(runtime) {
4992
5348
  }
4993
5349
  };
4994
5350
  }
5351
+ function getTelegramBotPluginRuntimeStateHolder() {
5352
+ const globalScope = globalThis;
5353
+ globalScope.__opencodeTbotPluginRuntimeState__ ??= { state: null };
5354
+ return globalScope.__opencodeTbotPluginRuntimeState__;
5355
+ }
4995
5356
  //#endregion
4996
5357
  export { TelegramBotPlugin, TelegramBotPlugin as default, ensureTelegramBotPluginRuntime, resetTelegramBotPluginRuntimeForTests };
4997
5358