@llblab/pi-telegram 0.17.5 → 0.18.0

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 +36 -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 +483 -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 +1 -1
  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 +405 -40
  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
  }
@@ -654,17 +951,38 @@ export async function callTelegramMultipart<TResponse>(
654
951
  const fileBlob = await openAsBlob(filePath);
655
952
  return callTelegramWithRetry(
656
953
  method,
657
- async () => {
954
+ async (family) => {
955
+ if (family) {
956
+ const multipart = await buildTelegramMultipartBody(
957
+ fields,
958
+ fileField,
959
+ fileBlob,
960
+ fileName,
961
+ );
962
+ return telegramFetch(
963
+ `${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`,
964
+ {
965
+ method: "POST",
966
+ headers: { "content-type": multipart.contentType },
967
+ body: multipart.body as unknown as BodyInit,
968
+ signal: options?.signal,
969
+ },
970
+ family,
971
+ );
972
+ }
658
973
  const form = new FormData();
659
974
  for (const [key, value] of Object.entries(fields)) {
660
975
  form.set(key, value);
661
976
  }
662
977
  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
- });
978
+ return telegramFetch(
979
+ `${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`,
980
+ {
981
+ method: "POST",
982
+ body: form,
983
+ signal: options?.signal,
984
+ },
985
+ );
668
986
  },
669
987
  options,
670
988
  );
@@ -690,9 +1008,12 @@ export async function downloadTelegramFile(
690
1008
  tempDir,
691
1009
  `${randomUUID()}-${sanitizeFileName(suggestedName)}`,
692
1010
  );
693
- const response = await fetch(
694
- `${TELEGRAM_API_BASE}/file/bot${configuredBotToken}/${file.file_path}`,
695
- { signal: options?.signal },
1011
+ const response = await callTelegramTransportRequest((family) =>
1012
+ telegramFetch(
1013
+ `${TELEGRAM_API_BASE}/file/bot${configuredBotToken}/${file.file_path}`,
1014
+ { signal: options?.signal },
1015
+ family,
1016
+ ),
696
1017
  );
697
1018
  if (!response.ok) {
698
1019
  throw new Error(`Failed to download Telegram file: ${response.status}`);
@@ -730,9 +1051,13 @@ export async function answerTelegramCallbackQuery(
730
1051
  : { callback_query_id: callbackQueryId },
731
1052
  );
732
1053
  } catch (error) {
733
- options.recordRuntimeEvent?.("api", error, {
734
- method: "answerCallbackQuery",
735
- });
1054
+ options.recordRuntimeEvent?.(
1055
+ "api",
1056
+ error,
1057
+ withTelegramTransportDiagnostics(error, {
1058
+ method: "answerCallbackQuery",
1059
+ }),
1060
+ );
736
1061
  }
737
1062
  }
738
1063
 
@@ -752,10 +1077,17 @@ export async function deleteTelegramMessage(
752
1077
  }
753
1078
 
754
1079
  export function createTelegramChatActionSender<TAction extends string>(
755
- sendChatAction: (chatId: number, action: TAction) => Promise<unknown>,
1080
+ sendChatAction: (
1081
+ chatId: number,
1082
+ action: TAction,
1083
+ options?: { message_thread_id?: number },
1084
+ ) => Promise<unknown>,
756
1085
  action: TAction,
757
- ): (chatId: number) => Promise<unknown> {
758
- return (chatId) => sendChatAction(chatId, action);
1086
+ ): (
1087
+ chatId: number,
1088
+ options?: { message_thread_id?: number },
1089
+ ) => Promise<unknown> {
1090
+ return (chatId, options) => sendChatAction(chatId, action, options);
759
1091
  }
760
1092
 
761
1093
  export function createTelegramNativeMarkdownDraftSender(deps: {
@@ -801,9 +1133,13 @@ export function createTelegramBridgeApiRuntime(
801
1133
  options?: TelegramApiCallOptions,
802
1134
  ): Promise<TResponse> => {
803
1135
  try {
804
- return await deps.client.call(method, body, options);
1136
+ return await deps.client.call<TResponse>(method, body, options);
805
1137
  } catch (error) {
806
- deps.recordRuntimeEvent("api", error, { method });
1138
+ deps.recordRuntimeEvent(
1139
+ "api",
1140
+ error,
1141
+ withTelegramTransportDiagnostics(error, { method }),
1142
+ );
807
1143
  throw error;
808
1144
  }
809
1145
  };
@@ -833,7 +1169,11 @@ export function createTelegramBridgeApiRuntime(
833
1169
  options,
834
1170
  );
835
1171
  } catch (error) {
836
- deps.recordRuntimeEvent("multipart", error, { method, fileName });
1172
+ deps.recordRuntimeEvent(
1173
+ "multipart",
1174
+ error,
1175
+ withTelegramTransportDiagnostics(error, { method, fileName }),
1176
+ );
837
1177
  throw error;
838
1178
  }
839
1179
  },
@@ -853,7 +1193,11 @@ export function createTelegramBridgeApiRuntime(
853
1193
  },
854
1194
  );
855
1195
  } catch (error) {
856
- deps.recordRuntimeEvent("download", error, { suggestedName });
1196
+ deps.recordRuntimeEvent(
1197
+ "download",
1198
+ error,
1199
+ withTelegramTransportDiagnostics(error, { suggestedName }),
1200
+ );
857
1201
  throw error;
858
1202
  }
859
1203
  },
@@ -867,24 +1211,33 @@ export function createTelegramBridgeApiRuntime(
867
1211
  callRecorded<TelegramUpdate[]>("getUpdates", body, { signal }),
868
1212
  setMyCommands: (commands) =>
869
1213
  callRecorded<boolean>("setMyCommands", { commands }),
870
- sendChatAction: (chatId, action) =>
1214
+ sendChatAction: (chatId, action, options) =>
871
1215
  callRecorded<boolean>("sendChatAction", {
872
1216
  chat_id: chatId,
873
1217
  action,
1218
+ ...(options?.message_thread_id !== undefined
1219
+ ? { message_thread_id: options.message_thread_id }
1220
+ : {}),
874
1221
  }),
875
1222
  sendTypingAction: createTelegramChatActionSender(
876
- (chatId, action) =>
1223
+ (chatId, action, options) =>
877
1224
  callRecorded<boolean>("sendChatAction", {
878
1225
  chat_id: chatId,
879
1226
  action,
1227
+ ...(options?.message_thread_id !== undefined
1228
+ ? { message_thread_id: options.message_thread_id }
1229
+ : {}),
880
1230
  }),
881
1231
  "typing",
882
1232
  ),
883
1233
  sendRecordVoiceAction: createTelegramChatActionSender(
884
- (chatId, action) =>
1234
+ (chatId, action, options) =>
885
1235
  callRecorded<boolean>("sendChatAction", {
886
1236
  chat_id: chatId,
887
1237
  action,
1238
+ ...(options?.message_thread_id !== undefined
1239
+ ? { message_thread_id: options.message_thread_id }
1240
+ : {}),
888
1241
  }),
889
1242
  "record_voice",
890
1243
  ),
@@ -913,7 +1266,13 @@ export function createTelegramBridgeApiRuntime(
913
1266
  return "edited";
914
1267
  } catch (error) {
915
1268
  if (isTelegramMessageNotModifiedError(error)) return "unchanged";
916
- deps.recordRuntimeEvent("api", error, { method: "editMessageText" });
1269
+ deps.recordRuntimeEvent(
1270
+ "api",
1271
+ error,
1272
+ withTelegramTransportDiagnostics(error, {
1273
+ method: "editMessageText",
1274
+ }),
1275
+ );
917
1276
  throw error;
918
1277
  }
919
1278
  },
@@ -921,15 +1280,21 @@ export function createTelegramBridgeApiRuntime(
921
1280
  try {
922
1281
  await deps.client.answerCallbackQuery(callbackQueryId, text);
923
1282
  } catch (error) {
924
- deps.recordRuntimeEvent("api", error, {
925
- method: "answerCallbackQuery",
926
- });
1283
+ deps.recordRuntimeEvent(
1284
+ "api",
1285
+ error,
1286
+ withTelegramTransportDiagnostics(error, {
1287
+ method: "answerCallbackQuery",
1288
+ }),
1289
+ );
927
1290
  }
928
1291
  },
929
1292
  answerGuestQuery: (
930
1293
  guestQueryId: string,
931
1294
  text: string | undefined,
932
- options: { parseMode?: string; richMessage?: TelegramInputRichMessage } | undefined,
1295
+ options:
1296
+ | { parseMode?: string; richMessage?: TelegramInputRichMessage }
1297
+ | undefined,
933
1298
  ) => {
934
1299
  const body: Record<string, unknown> = { guest_query_id: guestQueryId };
935
1300
  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(