@llblab/pi-telegram 0.17.5 → 0.18.1

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.
Files changed (61) hide show
  1. package/AGENTS.md +67 -32
  2. package/BACKLOG.md +59 -19
  3. package/CHANGELOG.md +42 -15
  4. package/README.md +63 -35
  5. package/docs/README.md +3 -1
  6. package/docs/architecture.md +55 -23
  7. package/docs/callback-namespaces.md +1 -1
  8. package/docs/inbound.md +1 -1
  9. package/docs/locks.md +0 -2
  10. package/docs/multi-instance-bus.md +410 -0
  11. package/docs/outbound.md +4 -3
  12. package/docs/public-api.md +12 -10
  13. package/docs/sections.md +2 -2
  14. package/docs/ui-style.md +76 -0
  15. package/index.ts +789 -32
  16. package/lib/bindings.ts +68 -12
  17. package/lib/bus-api.ts +314 -0
  18. package/lib/bus-follower.ts +853 -0
  19. package/lib/bus-leader.ts +915 -0
  20. package/lib/bus.ts +866 -0
  21. package/lib/command-templates.ts +9 -11
  22. package/lib/commands.ts +133 -47
  23. package/lib/config.ts +53 -5
  24. package/lib/lifecycle.ts +23 -7
  25. package/lib/locks.ts +230 -66
  26. package/lib/media.ts +30 -2
  27. package/lib/menu-model.ts +48 -17
  28. package/lib/menu-queue.ts +51 -20
  29. package/lib/menu-settings.ts +9 -5
  30. package/lib/menu-status.ts +3 -0
  31. package/lib/menu-thinking.ts +3 -0
  32. package/lib/menu.ts +67 -26
  33. package/lib/outbound-attachments.ts +102 -17
  34. package/lib/outbound-buttons.ts +6 -2
  35. package/lib/outbound-voice.ts +31 -11
  36. package/lib/outbound.ts +6 -4
  37. package/lib/ownership.ts +119 -0
  38. package/lib/pi.ts +26 -3
  39. package/lib/polling.ts +477 -7
  40. package/lib/preview.ts +141 -88
  41. package/lib/prompt-templates.ts +3 -3
  42. package/lib/prompts.ts +80 -30
  43. package/lib/queue.ts +193 -91
  44. package/lib/rendering.ts +0 -25
  45. package/lib/replies.ts +187 -55
  46. package/lib/routing.ts +1673 -9
  47. package/lib/runtime-log.ts +123 -0
  48. package/lib/runtime.ts +84 -12
  49. package/lib/sections.ts +28 -21
  50. package/lib/setup.ts +9 -2
  51. package/lib/status.ts +532 -9
  52. package/lib/sync.ts +618 -0
  53. package/lib/target.ts +49 -0
  54. package/lib/telegram-api.ts +409 -41
  55. package/lib/text-groups.ts +5 -1
  56. package/lib/thread-reconciler.ts +915 -0
  57. package/lib/threads.ts +2205 -0
  58. package/lib/turns.ts +48 -3
  59. package/lib/updates.ts +355 -32
  60. package/package.json +24 -2
  61. package/docs/telegram-bot-api-rich-messages.md +0 -890
@@ -9,6 +9,7 @@
9
9
  import { randomUUID } from "node:crypto";
10
10
  import { createWriteStream, openAsBlob } from "node:fs";
11
11
  import { mkdir, readdir, stat, unlink, writeFile } from "node:fs/promises";
12
+ import { request as requestHttps } from "node:https";
12
13
  import { homedir } from "node:os";
13
14
  import { join, resolve } from "node:path";
14
15
  import { Readable, Transform } from "node:stream";
@@ -45,6 +46,21 @@ const TELEGRAM_INBOUND_FILE_MAX_BYTES = getTelegramInboundFileByteLimitFromEnv(
45
46
  TELEGRAM_FILE_MAX_BYTES,
46
47
  );
47
48
 
49
+ export type TelegramNetworkFamilyPolicy =
50
+ | "auto"
51
+ | "ipv4"
52
+ | "ipv6"
53
+ | "ipv4-fallback";
54
+
55
+ const TELEGRAM_NETWORK_FAMILY_ENV = "PI_TELEGRAM_NETWORK_FAMILY";
56
+ const TELEGRAM_NETWORK_FAMILY_VALUES = new Set<TelegramNetworkFamilyPolicy>([
57
+ "auto",
58
+ "ipv4",
59
+ "ipv6",
60
+ "ipv4-fallback",
61
+ ]);
62
+ type TelegramNetworkFamily = 4 | 6;
63
+
48
64
  export interface TelegramUser {
49
65
  id: number;
50
66
  is_bot: boolean;
@@ -189,7 +205,9 @@ export interface TelegramSentMessage {
189
205
 
190
206
  export interface TelegramReplyParameters {
191
207
  message_id: number;
192
- allow_sending_without_reply: true;
208
+ allow_sending_without_reply?: boolean;
209
+ chat_id?: number;
210
+ message_thread_id?: number;
193
211
  }
194
212
 
195
213
  export type TelegramSendMessageBody = Record<string, unknown> & {
@@ -349,9 +367,19 @@ export interface TelegramBridgeApiRuntime {
349
367
  setMyCommands: (
350
368
  commands: readonly { command: string; description: string }[],
351
369
  ) => Promise<boolean>;
352
- sendChatAction: (chatId: number, action: string) => Promise<boolean>;
353
- sendTypingAction: (chatId: number) => Promise<unknown>;
354
- sendRecordVoiceAction: (chatId: number) => Promise<unknown>;
370
+ sendChatAction: (
371
+ chatId: number,
372
+ action: string,
373
+ options?: { message_thread_id?: number },
374
+ ) => Promise<boolean>;
375
+ sendTypingAction: (
376
+ chatId: number,
377
+ options?: { message_thread_id?: number },
378
+ ) => Promise<unknown>;
379
+ sendRecordVoiceAction: (
380
+ chatId: number,
381
+ options?: { message_thread_id?: number },
382
+ ) => Promise<unknown>;
355
383
  sendMessageDraft: (
356
384
  chatId: number,
357
385
  draftId: number,
@@ -537,9 +565,271 @@ function unwrapTelegramApiResult<TResponse>(
537
565
  return data.result;
538
566
  }
539
567
 
568
+ function getTelegramNetworkFamilyPolicy(
569
+ env: NodeJS.ProcessEnv = process.env,
570
+ ): TelegramNetworkFamilyPolicy {
571
+ const value = env[TELEGRAM_NETWORK_FAMILY_ENV]?.trim().toLowerCase();
572
+ if (
573
+ TELEGRAM_NETWORK_FAMILY_VALUES.has(value as TelegramNetworkFamilyPolicy)
574
+ ) {
575
+ return value as TelegramNetworkFamilyPolicy;
576
+ }
577
+ return "ipv4-fallback";
578
+ }
579
+
580
+ function getTelegramNetworkFamily(
581
+ policy: TelegramNetworkFamilyPolicy,
582
+ ): TelegramNetworkFamily | undefined {
583
+ if (policy === "ipv4") return 4;
584
+ if (policy === "ipv6") return 6;
585
+ return undefined;
586
+ }
587
+
588
+ function isTelegramTransportFailure(error: unknown): boolean {
589
+ if (!(error instanceof Error)) return false;
590
+ if (error.name === "AbortError") return false;
591
+ if (error instanceof TypeError && /fetch failed/i.test(error.message)) {
592
+ return true;
593
+ }
594
+ if (error instanceof AggregateError) return true;
595
+ const code = getErrorCode(error);
596
+ if (
597
+ code === "ECONNREFUSED" ||
598
+ code === "ETIMEDOUT" ||
599
+ code === "ENETUNREACH" ||
600
+ code === "EHOSTUNREACH" ||
601
+ code === "ECONNRESET" ||
602
+ code === "EAI_AGAIN"
603
+ ) {
604
+ return true;
605
+ }
606
+ return isTelegramTransportFailure(error.cause);
607
+ }
608
+
609
+ function getTelegramRequestBodyBuffer(
610
+ body: BodyInit | null | undefined,
611
+ ): Buffer | undefined {
612
+ if (body === undefined || body === null) return undefined;
613
+ if (typeof body === "string") return Buffer.from(body);
614
+ if (body instanceof Uint8Array) return Buffer.from(body);
615
+ throw new Error("Unsupported Telegram HTTPS request body");
616
+ }
617
+
618
+ async function buildTelegramMultipartBody(
619
+ fields: Record<string, string>,
620
+ fileField: string,
621
+ fileBlob: Blob,
622
+ fileName: string,
623
+ ): Promise<{ body: Buffer; contentType: string }> {
624
+ const boundary = `pi-telegram-${randomUUID()}`;
625
+ const chunks: Buffer[] = [];
626
+ for (const [key, value] of Object.entries(fields)) {
627
+ chunks.push(
628
+ Buffer.from(
629
+ `--${boundary}\r\nContent-Disposition: form-data; name="${key}"\r\n\r\n${value}\r\n`,
630
+ ),
631
+ );
632
+ }
633
+ chunks.push(
634
+ Buffer.from(
635
+ `--${boundary}\r\nContent-Disposition: form-data; name="${fileField}"; filename="${fileName}"\r\nContent-Type: ${fileBlob.type || "application/octet-stream"}\r\n\r\n`,
636
+ ),
637
+ Buffer.from(await fileBlob.arrayBuffer()),
638
+ Buffer.from(`\r\n--${boundary}--\r\n`),
639
+ );
640
+ return {
641
+ body: Buffer.concat(chunks),
642
+ contentType: `multipart/form-data; boundary=${boundary}`,
643
+ };
644
+ }
645
+
646
+ async function telegramHttpsFetch(
647
+ input: string | URL | Request,
648
+ init: RequestInit,
649
+ family: TelegramNetworkFamily,
650
+ ): Promise<Response> {
651
+ const url = new URL(
652
+ typeof input === "string" || input instanceof URL ? input : input.url,
653
+ );
654
+ const body = getTelegramRequestBodyBuffer(init.body);
655
+ const headers = new Headers(init.headers);
656
+ if (body && !headers.has("content-length")) {
657
+ headers.set("content-length", String(body.byteLength));
658
+ }
659
+ return new Promise<Response>((resolve, reject) => {
660
+ const req = requestHttps(
661
+ url,
662
+ {
663
+ method: init.method ?? "GET",
664
+ family,
665
+ headers: Object.fromEntries(headers.entries()),
666
+ },
667
+ (res) => {
668
+ const responseHeaders = new Headers();
669
+ for (const [key, value] of Object.entries(res.headers)) {
670
+ if (Array.isArray(value)) responseHeaders.set(key, value.join(", "));
671
+ else if (value !== undefined) responseHeaders.set(key, String(value));
672
+ }
673
+ resolve(
674
+ new Response(Readable.toWeb(res) as ReadableStream<Uint8Array>, {
675
+ status: res.statusCode ?? 200,
676
+ statusText: res.statusMessage,
677
+ headers: responseHeaders,
678
+ }),
679
+ );
680
+ },
681
+ );
682
+ req.on("error", reject);
683
+ if (init.signal) {
684
+ if (init.signal.aborted)
685
+ req.destroy(new DOMException("Aborted", "AbortError"));
686
+ else {
687
+ init.signal.addEventListener(
688
+ "abort",
689
+ () => req.destroy(new DOMException("Aborted", "AbortError")),
690
+ { once: true },
691
+ );
692
+ }
693
+ }
694
+ req.end(body);
695
+ });
696
+ }
697
+
698
+ let telegramHttpsFetchForTesting: typeof telegramHttpsFetch | undefined;
699
+
700
+ export function setTelegramApiHttpsFetchForTesting(
701
+ fetchImpl: typeof telegramHttpsFetch | undefined,
702
+ ): () => void {
703
+ const previous = telegramHttpsFetchForTesting;
704
+ telegramHttpsFetchForTesting = fetchImpl;
705
+ return () => {
706
+ telegramHttpsFetchForTesting = previous;
707
+ };
708
+ }
709
+
710
+ async function telegramFetch(
711
+ input: string | URL | Request,
712
+ init: RequestInit = {},
713
+ family?: TelegramNetworkFamily,
714
+ ): Promise<Response> {
715
+ if (!family) return fetch(input, init);
716
+ return (telegramHttpsFetchForTesting ?? telegramHttpsFetch)(
717
+ input,
718
+ init,
719
+ family,
720
+ );
721
+ }
722
+
723
+ async function callTelegramTransportRequest(
724
+ request: (family?: TelegramNetworkFamily) => Promise<Response>,
725
+ ): Promise<Response> {
726
+ const policy = getTelegramNetworkFamilyPolicy();
727
+ if (policy === "auto") return request();
728
+ const family = getTelegramNetworkFamily(policy);
729
+ if (family) return request(family);
730
+ try {
731
+ return await request();
732
+ } catch (error) {
733
+ if (!isTelegramTransportFailure(error)) throw error;
734
+ return request(4);
735
+ }
736
+ }
737
+
738
+ function getErrorCode(error: Error): string | undefined {
739
+ const maybeCode = (error as { code?: unknown }).code;
740
+ return typeof maybeCode === "string" ? maybeCode : undefined;
741
+ }
742
+
743
+ function getErrorAddress(error: Error): string | undefined {
744
+ const maybeAddress = (error as { address?: unknown }).address;
745
+ return typeof maybeAddress === "string" ? maybeAddress : undefined;
746
+ }
747
+
748
+ function getErrorPort(error: Error): number | undefined {
749
+ const maybePort = (error as { port?: unknown }).port;
750
+ return typeof maybePort === "number" ? maybePort : undefined;
751
+ }
752
+
753
+ function getErrorFamily(error: Error): number | string | undefined {
754
+ const maybeFamily = (error as { family?: unknown }).family;
755
+ if (typeof maybeFamily === "number" || typeof maybeFamily === "string") {
756
+ return maybeFamily;
757
+ }
758
+ return undefined;
759
+ }
760
+
761
+ function describeTelegramErrorSummary(error: Error): {
762
+ name: string;
763
+ message: string;
764
+ code?: string;
765
+ } {
766
+ return {
767
+ name: error.name,
768
+ message: error.message,
769
+ ...(getErrorCode(error) ? { code: getErrorCode(error) } : {}),
770
+ };
771
+ }
772
+
773
+ function describeTelegramTransportAttempt(error: Error): {
774
+ name: string;
775
+ code?: string;
776
+ address?: string;
777
+ port?: number;
778
+ family?: number | string;
779
+ } {
780
+ return {
781
+ name: error.name,
782
+ ...(getErrorCode(error) ? { code: getErrorCode(error) } : {}),
783
+ ...(getErrorAddress(error) ? { address: getErrorAddress(error) } : {}),
784
+ ...(getErrorPort(error) ? { port: getErrorPort(error) } : {}),
785
+ ...(getErrorFamily(error) ? { family: getErrorFamily(error) } : {}),
786
+ };
787
+ }
788
+
789
+ function describeTelegramTransportError(error: unknown):
790
+ | {
791
+ error: { name: string; message: string; code?: string };
792
+ cause?: { name: string; message: string; code?: string };
793
+ attempts?: Array<{
794
+ name: string;
795
+ code?: string;
796
+ address?: string;
797
+ port?: number;
798
+ family?: number | string;
799
+ }>;
800
+ }
801
+ | undefined {
802
+ if (!isTelegramTransportFailure(error) || !(error instanceof Error)) {
803
+ return undefined;
804
+ }
805
+ const cause = error.cause instanceof Error ? error.cause : undefined;
806
+ const aggregate =
807
+ error instanceof AggregateError
808
+ ? error
809
+ : cause instanceof AggregateError
810
+ ? cause
811
+ : undefined;
812
+ const attempts = aggregate?.errors
813
+ .filter((attempt): attempt is Error => attempt instanceof Error)
814
+ .map(describeTelegramTransportAttempt);
815
+ return {
816
+ error: describeTelegramErrorSummary(error),
817
+ ...(cause ? { cause: describeTelegramErrorSummary(cause) } : {}),
818
+ ...(attempts && attempts.length > 0 ? { attempts } : {}),
819
+ };
820
+ }
821
+
822
+ function withTelegramTransportDiagnostics(
823
+ error: unknown,
824
+ details: Record<string, unknown>,
825
+ ): Record<string, unknown> {
826
+ const transport = describeTelegramTransportError(error);
827
+ return transport ? { ...details, transport } : details;
828
+ }
829
+
540
830
  async function callTelegramWithRetry<TResponse>(
541
831
  method: string,
542
- request: () => Promise<Response>,
832
+ request: (family?: TelegramNetworkFamily) => Promise<Response>,
543
833
  options: TelegramApiCallOptions | undefined,
544
834
  ): Promise<TResponse> {
545
835
  const maxAttempts = Math.max(1, options?.maxAttempts ?? 3);
@@ -549,7 +839,10 @@ async function callTelegramWithRetry<TResponse>(
549
839
  try {
550
840
  return unwrapTelegramApiResult(
551
841
  method,
552
- await parseTelegramApiResponse<TResponse>(await request(), method),
842
+ await parseTelegramApiResponse<TResponse>(
843
+ await callTelegramTransportRequest(request),
844
+ method,
845
+ ),
553
846
  );
554
847
  } catch (error) {
555
848
  if (attempt >= maxAttempts - 1 || !isRetryableTelegramApiError(error)) {
@@ -611,13 +904,17 @@ export async function callTelegram<TResponse>(
611
904
  const configuredBotToken = assertTelegramBotTokenConfigured(botToken);
612
905
  return callTelegramWithRetry(
613
906
  method,
614
- async () =>
615
- fetch(`${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`, {
616
- method: "POST",
617
- headers: { "content-type": "application/json" },
618
- body: JSON.stringify(body),
619
- signal: options?.signal,
620
- }),
907
+ async (family) =>
908
+ telegramFetch(
909
+ `${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`,
910
+ {
911
+ method: "POST",
912
+ headers: { "content-type": "application/json" },
913
+ body: JSON.stringify(body),
914
+ signal: options?.signal,
915
+ },
916
+ family,
917
+ ),
621
918
  options,
622
919
  );
623
920
  }
@@ -631,7 +928,10 @@ export async function fetchTelegramBotIdentity(
631
928
  botToken: string,
632
929
  fetchImpl: typeof fetch = fetch,
633
930
  ): Promise<TelegramBotIdentityResponse> {
634
- const response = await fetchImpl(`${TELEGRAM_API_BASE}/bot${botToken}/getMe`);
931
+ const url = `${TELEGRAM_API_BASE}/bot${botToken}/getMe`;
932
+ const response = await callTelegramTransportRequest((family) =>
933
+ fetchImpl === fetch ? telegramFetch(url, {}, family) : fetchImpl(url),
934
+ );
635
935
  return response.json() as Promise<TelegramBotIdentityResponse>;
636
936
  }
637
937
 
@@ -654,17 +954,38 @@ export async function callTelegramMultipart<TResponse>(
654
954
  const fileBlob = await openAsBlob(filePath);
655
955
  return callTelegramWithRetry(
656
956
  method,
657
- async () => {
957
+ async (family) => {
958
+ if (family) {
959
+ const multipart = await buildTelegramMultipartBody(
960
+ fields,
961
+ fileField,
962
+ fileBlob,
963
+ fileName,
964
+ );
965
+ return telegramFetch(
966
+ `${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`,
967
+ {
968
+ method: "POST",
969
+ headers: { "content-type": multipart.contentType },
970
+ body: multipart.body as unknown as BodyInit,
971
+ signal: options?.signal,
972
+ },
973
+ family,
974
+ );
975
+ }
658
976
  const form = new FormData();
659
977
  for (const [key, value] of Object.entries(fields)) {
660
978
  form.set(key, value);
661
979
  }
662
980
  form.set(fileField, fileBlob, fileName);
663
- return fetch(`${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`, {
664
- method: "POST",
665
- body: form,
666
- signal: options?.signal,
667
- });
981
+ return telegramFetch(
982
+ `${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`,
983
+ {
984
+ method: "POST",
985
+ body: form,
986
+ signal: options?.signal,
987
+ },
988
+ );
668
989
  },
669
990
  options,
670
991
  );
@@ -690,9 +1011,12 @@ export async function downloadTelegramFile(
690
1011
  tempDir,
691
1012
  `${randomUUID()}-${sanitizeFileName(suggestedName)}`,
692
1013
  );
693
- const response = await fetch(
694
- `${TELEGRAM_API_BASE}/file/bot${configuredBotToken}/${file.file_path}`,
695
- { signal: options?.signal },
1014
+ const response = await callTelegramTransportRequest((family) =>
1015
+ telegramFetch(
1016
+ `${TELEGRAM_API_BASE}/file/bot${configuredBotToken}/${file.file_path}`,
1017
+ { signal: options?.signal },
1018
+ family,
1019
+ ),
696
1020
  );
697
1021
  if (!response.ok) {
698
1022
  throw new Error(`Failed to download Telegram file: ${response.status}`);
@@ -730,9 +1054,13 @@ export async function answerTelegramCallbackQuery(
730
1054
  : { callback_query_id: callbackQueryId },
731
1055
  );
732
1056
  } catch (error) {
733
- options.recordRuntimeEvent?.("api", error, {
734
- method: "answerCallbackQuery",
735
- });
1057
+ options.recordRuntimeEvent?.(
1058
+ "api",
1059
+ error,
1060
+ withTelegramTransportDiagnostics(error, {
1061
+ method: "answerCallbackQuery",
1062
+ }),
1063
+ );
736
1064
  }
737
1065
  }
738
1066
 
@@ -752,10 +1080,17 @@ export async function deleteTelegramMessage(
752
1080
  }
753
1081
 
754
1082
  export function createTelegramChatActionSender<TAction extends string>(
755
- sendChatAction: (chatId: number, action: TAction) => Promise<unknown>,
1083
+ sendChatAction: (
1084
+ chatId: number,
1085
+ action: TAction,
1086
+ options?: { message_thread_id?: number },
1087
+ ) => Promise<unknown>,
756
1088
  action: TAction,
757
- ): (chatId: number) => Promise<unknown> {
758
- return (chatId) => sendChatAction(chatId, action);
1089
+ ): (
1090
+ chatId: number,
1091
+ options?: { message_thread_id?: number },
1092
+ ) => Promise<unknown> {
1093
+ return (chatId, options) => sendChatAction(chatId, action, options);
759
1094
  }
760
1095
 
761
1096
  export function createTelegramNativeMarkdownDraftSender(deps: {
@@ -801,9 +1136,13 @@ export function createTelegramBridgeApiRuntime(
801
1136
  options?: TelegramApiCallOptions,
802
1137
  ): Promise<TResponse> => {
803
1138
  try {
804
- return await deps.client.call(method, body, options);
1139
+ return await deps.client.call<TResponse>(method, body, options);
805
1140
  } catch (error) {
806
- deps.recordRuntimeEvent("api", error, { method });
1141
+ deps.recordRuntimeEvent(
1142
+ "api",
1143
+ error,
1144
+ withTelegramTransportDiagnostics(error, { method }),
1145
+ );
807
1146
  throw error;
808
1147
  }
809
1148
  };
@@ -833,7 +1172,11 @@ export function createTelegramBridgeApiRuntime(
833
1172
  options,
834
1173
  );
835
1174
  } catch (error) {
836
- deps.recordRuntimeEvent("multipart", error, { method, fileName });
1175
+ deps.recordRuntimeEvent(
1176
+ "multipart",
1177
+ error,
1178
+ withTelegramTransportDiagnostics(error, { method, fileName }),
1179
+ );
837
1180
  throw error;
838
1181
  }
839
1182
  },
@@ -853,7 +1196,11 @@ export function createTelegramBridgeApiRuntime(
853
1196
  },
854
1197
  );
855
1198
  } catch (error) {
856
- deps.recordRuntimeEvent("download", error, { suggestedName });
1199
+ deps.recordRuntimeEvent(
1200
+ "download",
1201
+ error,
1202
+ withTelegramTransportDiagnostics(error, { suggestedName }),
1203
+ );
857
1204
  throw error;
858
1205
  }
859
1206
  },
@@ -867,24 +1214,33 @@ export function createTelegramBridgeApiRuntime(
867
1214
  callRecorded<TelegramUpdate[]>("getUpdates", body, { signal }),
868
1215
  setMyCommands: (commands) =>
869
1216
  callRecorded<boolean>("setMyCommands", { commands }),
870
- sendChatAction: (chatId, action) =>
1217
+ sendChatAction: (chatId, action, options) =>
871
1218
  callRecorded<boolean>("sendChatAction", {
872
1219
  chat_id: chatId,
873
1220
  action,
1221
+ ...(options?.message_thread_id !== undefined
1222
+ ? { message_thread_id: options.message_thread_id }
1223
+ : {}),
874
1224
  }),
875
1225
  sendTypingAction: createTelegramChatActionSender(
876
- (chatId, action) =>
1226
+ (chatId, action, options) =>
877
1227
  callRecorded<boolean>("sendChatAction", {
878
1228
  chat_id: chatId,
879
1229
  action,
1230
+ ...(options?.message_thread_id !== undefined
1231
+ ? { message_thread_id: options.message_thread_id }
1232
+ : {}),
880
1233
  }),
881
1234
  "typing",
882
1235
  ),
883
1236
  sendRecordVoiceAction: createTelegramChatActionSender(
884
- (chatId, action) =>
1237
+ (chatId, action, options) =>
885
1238
  callRecorded<boolean>("sendChatAction", {
886
1239
  chat_id: chatId,
887
1240
  action,
1241
+ ...(options?.message_thread_id !== undefined
1242
+ ? { message_thread_id: options.message_thread_id }
1243
+ : {}),
888
1244
  }),
889
1245
  "record_voice",
890
1246
  ),
@@ -913,7 +1269,13 @@ export function createTelegramBridgeApiRuntime(
913
1269
  return "edited";
914
1270
  } catch (error) {
915
1271
  if (isTelegramMessageNotModifiedError(error)) return "unchanged";
916
- deps.recordRuntimeEvent("api", error, { method: "editMessageText" });
1272
+ deps.recordRuntimeEvent(
1273
+ "api",
1274
+ error,
1275
+ withTelegramTransportDiagnostics(error, {
1276
+ method: "editMessageText",
1277
+ }),
1278
+ );
917
1279
  throw error;
918
1280
  }
919
1281
  },
@@ -921,15 +1283,21 @@ export function createTelegramBridgeApiRuntime(
921
1283
  try {
922
1284
  await deps.client.answerCallbackQuery(callbackQueryId, text);
923
1285
  } catch (error) {
924
- deps.recordRuntimeEvent("api", error, {
925
- method: "answerCallbackQuery",
926
- });
1286
+ deps.recordRuntimeEvent(
1287
+ "api",
1288
+ error,
1289
+ withTelegramTransportDiagnostics(error, {
1290
+ method: "answerCallbackQuery",
1291
+ }),
1292
+ );
927
1293
  }
928
1294
  },
929
1295
  answerGuestQuery: (
930
1296
  guestQueryId: string,
931
1297
  text: string | undefined,
932
- options: { parseMode?: string; richMessage?: TelegramInputRichMessage } | undefined,
1298
+ options:
1299
+ | { parseMode?: string; richMessage?: TelegramInputRichMessage }
1300
+ | undefined,
933
1301
  ) => {
934
1302
  const body: Record<string, unknown> = { guest_query_id: guestQueryId };
935
1303
  if (text !== undefined || options?.richMessage) {
@@ -12,6 +12,7 @@ export interface TelegramTextGroupMessage {
12
12
  message_id: number;
13
13
  media_group_id?: string;
14
14
  chat: { id: number };
15
+ message_thread_id?: number;
15
16
  from?: { id: number; is_bot?: boolean };
16
17
  text?: string;
17
18
  caption?: string;
@@ -70,7 +71,10 @@ function getTelegramTextGroupKey(
70
71
  if (message.media_group_id) return undefined;
71
72
  if (!message.from || message.from.is_bot) return undefined;
72
73
  if (typeof message.text !== "string") return undefined;
73
- return `${message.chat.id}:${message.from.id}`;
74
+ const threadKey = typeof message.message_thread_id === "number"
75
+ ? `thread:${message.message_thread_id}`
76
+ : "private";
77
+ return `${message.chat.id}:${threadKey}:${message.from.id}`;
74
78
  }
75
79
 
76
80
  function canStartTelegramTextGroup(