@llblab/pi-telegram 0.20.4 → 0.20.6

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.
@@ -88,6 +88,7 @@ export interface TelegramQueuedOutboundAttachmentView {
88
88
 
89
89
  export interface TelegramOutboundAttachmentQueueTargetView {
90
90
  queuedAttachments: TelegramQueuedOutboundAttachmentView[];
91
+ guestQueryId?: string;
91
92
  }
92
93
 
93
94
  export interface TelegramQueuedOutboundAttachmentTurnView extends TelegramOutboundAttachmentQueueTargetView {
@@ -96,6 +97,42 @@ export interface TelegramQueuedOutboundAttachmentTurnView extends TelegramOutbou
96
97
  target?: TelegramTarget;
97
98
  }
98
99
 
100
+ export type TelegramGuestCachedAttachmentResult =
101
+ | {
102
+ type: "document";
103
+ id: string;
104
+ title: string;
105
+ document_file_id: string;
106
+ caption?: string;
107
+ }
108
+ | {
109
+ type: "photo";
110
+ id: string;
111
+ photo_file_id: string;
112
+ caption?: string;
113
+ }
114
+ | {
115
+ type: "audio";
116
+ id: string;
117
+ audio_file_id: string;
118
+ caption?: string;
119
+ }
120
+ | {
121
+ type: "voice";
122
+ id: string;
123
+ voice_file_id: string;
124
+ title: string;
125
+ caption?: string;
126
+ };
127
+
128
+ interface TelegramGuestStagingMessage {
129
+ message_id?: number;
130
+ document?: { file_id?: string };
131
+ photo?: Array<{ file_id?: string; file_size?: number }>;
132
+ audio?: { file_id?: string };
133
+ voice?: { file_id?: string };
134
+ }
135
+
99
136
  function isTelegramOutboundPhotoAttachmentPath(path: string): boolean {
100
137
  const normalized = path.toLowerCase();
101
138
  return (
@@ -107,6 +144,27 @@ function isTelegramOutboundPhotoAttachmentPath(path: string): boolean {
107
144
  );
108
145
  }
109
146
 
147
+ function getTelegramGuestAttachmentTransport(path: string): {
148
+ method: "sendDocument" | "sendPhoto" | "sendAudio" | "sendVoice";
149
+ fileField: "document" | "photo" | "audio" | "voice";
150
+ } {
151
+ const normalized = path.toLowerCase();
152
+ if (
153
+ normalized.endsWith(".jpg") ||
154
+ normalized.endsWith(".jpeg") ||
155
+ normalized.endsWith(".png")
156
+ ) {
157
+ return { method: "sendPhoto", fileField: "photo" };
158
+ }
159
+ if (normalized.endsWith(".ogg") || normalized.endsWith(".opus")) {
160
+ return { method: "sendVoice", fileField: "voice" };
161
+ }
162
+ if (normalized.endsWith(".mp3")) {
163
+ return { method: "sendAudio", fileField: "audio" };
164
+ }
165
+ return { method: "sendDocument", fileField: "document" };
166
+ }
167
+
110
168
  function formatTelegramOutboundAttachmentSizeLimitError(
111
169
  size: number,
112
170
  maxSize: number,
@@ -388,6 +446,14 @@ export async function queueTelegramOutboundAttachments(options: {
388
446
  statPath: options.statPath,
389
447
  });
390
448
  }
449
+ if (
450
+ options.activeTurn.guestQueryId &&
451
+ options.activeTurn.queuedAttachments.length + options.paths.length > 1
452
+ ) {
453
+ throw new Error(
454
+ "Telegram Guest Mode supports one attachment per reply; no attachment was queued",
455
+ );
456
+ }
391
457
  if (
392
458
  options.activeTurn.queuedAttachments.length + options.paths.length >
393
459
  options.maxAttachmentsPerTurn
@@ -414,6 +480,110 @@ export async function queueTelegramOutboundAttachments(options: {
414
480
  };
415
481
  }
416
482
 
483
+ export async function deliverTelegramGuestCachedAttachment(options: {
484
+ guestQueryId: string;
485
+ stagingChatId: number;
486
+ stagingTarget?: TelegramTarget;
487
+ attachment: TelegramQueuedOutboundAttachmentView;
488
+ caption?: string;
489
+ sendMultipart: TelegramQueuedOutboundAttachmentDeliveryDeps["sendMultipart"];
490
+ answerGuestQuery: (
491
+ guestQueryId: string,
492
+ result: TelegramGuestCachedAttachmentResult,
493
+ ) => Promise<void>;
494
+ answerGuestText?: (guestQueryId: string, text: string) => Promise<void>;
495
+ fallbackText?: string;
496
+ deleteMessage: (chatId: number, messageId: number) => Promise<void>;
497
+ recordRuntimeEvent?: TelegramOutboundAttachmentRuntimeEventRecorderPort["recordRuntimeEvent"];
498
+ }): Promise<void> {
499
+ const transport = getTelegramGuestAttachmentTransport(options.attachment.path);
500
+ let stagingMessageId: number | undefined;
501
+ let answerAttempted = false;
502
+ try {
503
+ const message = (await options.sendMultipart(
504
+ transport.method,
505
+ {
506
+ chat_id: String(options.stagingChatId),
507
+ ...getTelegramMultipartTargetFields(options.stagingTarget),
508
+ },
509
+ transport.fileField,
510
+ options.attachment.path,
511
+ options.attachment.fileName,
512
+ )) as TelegramGuestStagingMessage;
513
+ stagingMessageId = message.message_id;
514
+ const caption = options.caption
515
+ ? Array.from(options.caption).slice(0, 1024).join("")
516
+ : undefined;
517
+ let result: TelegramGuestCachedAttachmentResult;
518
+ if (transport.fileField === "photo") {
519
+ const photo = [...(message.photo ?? [])]
520
+ .sort((left, right) => (left.file_size ?? 0) - (right.file_size ?? 0))
521
+ .at(-1);
522
+ if (!photo?.file_id) throw new Error("Guest staging upload returned no photo file_id");
523
+ result = {
524
+ type: "photo",
525
+ id: "attachment-1",
526
+ photo_file_id: photo.file_id,
527
+ ...(caption ? { caption } : {}),
528
+ };
529
+ } else if (transport.fileField === "audio") {
530
+ if (!message.audio?.file_id)
531
+ throw new Error("Guest staging upload returned no audio file_id");
532
+ result = {
533
+ type: "audio",
534
+ id: "attachment-1",
535
+ audio_file_id: message.audio.file_id,
536
+ ...(caption ? { caption } : {}),
537
+ };
538
+ } else if (transport.fileField === "voice") {
539
+ if (!message.voice?.file_id)
540
+ throw new Error("Guest staging upload returned no voice file_id");
541
+ result = {
542
+ type: "voice",
543
+ id: "attachment-1",
544
+ voice_file_id: message.voice.file_id,
545
+ title: options.attachment.fileName,
546
+ ...(caption ? { caption } : {}),
547
+ };
548
+ } else {
549
+ if (!message.document?.file_id)
550
+ throw new Error("Guest staging upload returned no document file_id");
551
+ result = {
552
+ type: "document",
553
+ id: "attachment-1",
554
+ title: options.attachment.fileName,
555
+ document_file_id: message.document.file_id,
556
+ ...(caption ? { caption } : {}),
557
+ };
558
+ }
559
+ answerAttempted = true;
560
+ await options.answerGuestQuery(options.guestQueryId, result);
561
+ } catch (error) {
562
+ if (
563
+ !answerAttempted &&
564
+ options.answerGuestText &&
565
+ options.fallbackText
566
+ ) {
567
+ answerAttempted = true;
568
+ await options.answerGuestText(options.guestQueryId, options.fallbackText);
569
+ } else {
570
+ throw error;
571
+ }
572
+ } finally {
573
+ if (stagingMessageId !== undefined) {
574
+ try {
575
+ await options.deleteMessage(options.stagingChatId, stagingMessageId);
576
+ } catch (error) {
577
+ options.recordRuntimeEvent?.("attachment", error, {
578
+ phase: "guest-staging-cleanup",
579
+ chatId: options.stagingChatId,
580
+ messageId: stagingMessageId,
581
+ });
582
+ }
583
+ }
584
+ }
585
+ }
586
+
417
587
  export async function sendTelegramOutboundMessage(options: {
418
588
  text: string;
419
589
  chatId?: number;
@@ -64,6 +64,36 @@ function isTopLevelClosingFence(
64
64
  );
65
65
  }
66
66
 
67
+ function collectPairedTelegramVoiceActionBody(
68
+ markdown: string,
69
+ bodyStart: number,
70
+ commentContent: string,
71
+ ): { content: string; end: number } | undefined {
72
+ const normalizedContent = commentContent.trim();
73
+ if (
74
+ !normalizedContent.startsWith("telegram_voice") ||
75
+ !isTelegramActionCommentContent(commentContent)
76
+ ) {
77
+ return undefined;
78
+ }
79
+ let offset = bodyStart;
80
+ while (offset < markdown.length) {
81
+ const lineEnd = getMarkdownLineEnd(markdown, offset);
82
+ const line = getMarkdownLineText(markdown, offset, lineEnd);
83
+ if (line === "<!-- /telegram_voice -->") {
84
+ const body = markdown.slice(bodyStart, offset).trim();
85
+ if (!body) return undefined;
86
+ return {
87
+ content: `${commentContent.trimEnd()}\n${body}`,
88
+ end: lineEnd,
89
+ };
90
+ }
91
+ if (line.startsWith("<!--")) return undefined;
92
+ offset = lineEnd;
93
+ }
94
+ return undefined;
95
+ }
96
+
67
97
  function collectInlineClosedTelegramActionBody(
68
98
  markdown: string,
69
99
  bodyStart: number,
@@ -117,14 +147,19 @@ export function collectTopLevelHtmlComments(markdown: string): {
117
147
  const closesOnOpeningLine = closeIndex < lineEnd;
118
148
  const hasOnlyWhitespaceAfterClose =
119
149
  line.slice(closeColumn + 3).trim() === "";
120
- const inlineBody =
150
+ const pairedVoiceBody =
121
151
  closesOnOpeningLine && hasOnlyWhitespaceAfterClose
152
+ ? collectPairedTelegramVoiceActionBody(markdown, lineEnd, content)
153
+ : undefined;
154
+ const inlineBody =
155
+ !pairedVoiceBody && closesOnOpeningLine && hasOnlyWhitespaceAfterClose
122
156
  ? collectInlineClosedTelegramActionBody(markdown, lineEnd, content)
123
157
  : undefined;
124
- if (inlineBody) {
125
- end = inlineBody.end;
158
+ const recoveredBody = pairedVoiceBody ?? inlineBody;
159
+ if (recoveredBody) {
160
+ end = recoveredBody.end;
126
161
  raw = markdown.slice(offset, end);
127
- content = inlineBody.content;
162
+ content = recoveredBody.content;
128
163
  }
129
164
  comments.push({ raw, content, start: offset, end });
130
165
  offset = getMarkdownLineEnd(markdown, end);
package/lib/paths.ts CHANGED
@@ -76,9 +76,10 @@ export function getTelegramDiagnosticsDisplayPaths(profileName?: string): {
76
76
  logs: string;
77
77
  } {
78
78
  const suffix = getTelegramProfilePathSuffix(profileName);
79
+ const profileSlug = suffix.slice(1);
79
80
  return {
80
81
  state: `~/.pi/agent/tmp/telegram/state${suffix}.json`,
81
- logs: `~/.pi/agent/tmp/telegram/logs${suffix}.jsonl`,
82
+ logs: `~/.pi/agent/tmp/telegram/logs${profileSlug ? `.${profileSlug}` : ""}.jsonl`,
82
83
  };
83
84
  }
84
85
 
package/lib/prompts.ts CHANGED
@@ -16,7 +16,7 @@ Telegram bridge available. Do not use it from local/TUI prompts unless explicitl
16
16
 
17
17
  const TELEGRAM_TURN_SYSTEM_PROMPT_SUFFIX = `
18
18
 
19
- Telegram turn note: If context was compacted or you need the pi-telegram bridge contract, call tool \`telegram_help\`; hidden comments are valid only for explicit \`telegram_voice\` or \`telegram_button\` actions with payload.`;
19
+ Telegram turn note: If context was compacted or you need the pi-telegram bridge contract, call tool \`telegram_help\`; hidden comments are valid only for explicit \`telegram_voice\` or \`telegram_button\` actions with payload. For voice use a top-level HTML action: \`<!-- telegram_voice: Speak this. -->\`, multiline \`<!-- telegram_voice lang=ru\nSpeak this.\n-->\`, or paired \`<!-- telegram_voice lang=ru -->\nSpeak this.\n<!-- /telegram_voice -->\`.`;
20
20
 
21
21
  function buildTelegramHelpText(profileName?: string): string {
22
22
  const diagnosticsPaths = getTelegramDiagnosticsDisplayPaths(profileName);
@@ -37,7 +37,8 @@ How to answer Telegram turns:
37
37
  Assistant-authored Telegram actions:
38
38
  - \`telegram_voice\` and \`telegram_button\` are hidden top-level HTML comments, not Pi tools.
39
39
  - Put action comments at column zero, outside code, quotes, lists, and indented examples.
40
- - Voice forms: \`<!-- telegram_voice text="Short summary" -->\` or \`<!-- telegram_voice: Short summary -->\`.
40
+ - Voice forms: \`<!-- telegram_voice text="Short summary" -->\`, \`<!-- telegram_voice: Short summary -->\`, multiline \`<!-- telegram_voice lang=ru\nShort summary.\n-->\`, or paired \`<!-- telegram_voice lang=ru -->\nShort summary.\n<!-- /telegram_voice -->\`.
41
+ - Keep the complete action at top level and include a non-empty voice payload.
41
42
  - Keep voice text TTS-friendly; avoid raw Markdown, code, and tables in voice text.
42
43
  - Voice delivery generates and attaches OGG automatically; do not also call \`telegram_attach\` for the same audio.
43
44
  - Button forms: \`<!-- telegram_button: OK -->\`, \`<!-- telegram_button label=Continue prompt="Continue with the current plan." -->\`, or multiline \`<!-- telegram_button label="Show risks"\nList the main risks first.\n-->\`.
package/lib/queue.ts CHANGED
@@ -899,6 +899,16 @@ export interface TelegramAgentEndRuntimeDeps<
899
899
  options?: { parseMode?: string },
900
900
  ) => Promise<void>;
901
901
  sendGuestReply?: (guestQueryId: string, markdown: string) => Promise<void>;
902
+ sendGuestAttachment?: (
903
+ turn: TTurn,
904
+ attachment: QueuedAttachment,
905
+ caption?: string,
906
+ ) => Promise<void>;
907
+ sendGuestVoiceReply?: (
908
+ turn: TTurn,
909
+ plan: TelegramAgentEndOutboundReplyPlan<TReplyMarkup>,
910
+ caption?: string,
911
+ ) => Promise<void>;
902
912
  planOutboundReply?: (
903
913
  markdown: string,
904
914
  ) => TelegramAgentEndOutboundReplyPlan<TReplyMarkup>;
@@ -958,6 +968,8 @@ export interface TelegramAgentEndHookRuntimeDeps<
958
968
  sendQueuedAttachments: (turn: TTurn) => Promise<void>;
959
969
  answerGuestQuery?: TelegramAgentEndRuntimeDeps<TTurn>["answerGuestQuery"];
960
970
  sendGuestReply?: TelegramAgentEndRuntimeDeps<TTurn>["sendGuestReply"];
971
+ sendGuestAttachment?: TelegramAgentEndRuntimeDeps<TTurn>["sendGuestAttachment"];
972
+ sendGuestVoiceReply?: TelegramAgentEndRuntimeDeps<TTurn>["sendGuestVoiceReply"];
961
973
  planOutboundReply?: TelegramAgentEndRuntimeDeps<
962
974
  TTurn,
963
975
  TReplyMarkup
@@ -1083,6 +1095,8 @@ export function createTelegramAgentEndHook<
1083
1095
  sendQueuedAttachments: deps.sendQueuedAttachments,
1084
1096
  answerGuestQuery: deps.answerGuestQuery,
1085
1097
  sendGuestReply: deps.sendGuestReply,
1098
+ sendGuestAttachment: deps.sendGuestAttachment,
1099
+ sendGuestVoiceReply: deps.sendGuestVoiceReply,
1086
1100
  planOutboundReply: deps.planOutboundReply,
1087
1101
  sendOutboundReplyArtifacts: deps.sendOutboundReplyArtifacts,
1088
1102
  getDefaultChatId: deps.getDefaultChatId,
@@ -1181,7 +1195,38 @@ export async function handleTelegramAgentEndRuntime<
1181
1195
  if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
1182
1196
  return;
1183
1197
  }
1184
- if (finalText) {
1198
+ const [guestAttachment] = turn.queuedAttachments;
1199
+ if (guestAttachment && deps.sendGuestAttachment) {
1200
+ try {
1201
+ await deps.sendGuestAttachment(
1202
+ turn,
1203
+ guestAttachment,
1204
+ finalText || undefined,
1205
+ );
1206
+ } catch (error) {
1207
+ deps.recordRuntimeEvent?.("delivery", error, {
1208
+ phase: "guest-attachment",
1209
+ guestQueryId: turn.guestQueryId,
1210
+ });
1211
+ }
1212
+ } else if (
1213
+ outboundReply &&
1214
+ (outboundReply.voiceText || outboundReply.voiceReplies?.length) &&
1215
+ deps.sendGuestVoiceReply
1216
+ ) {
1217
+ try {
1218
+ await deps.sendGuestVoiceReply(
1219
+ turn,
1220
+ outboundReply,
1221
+ finalText || undefined,
1222
+ );
1223
+ } catch (error) {
1224
+ deps.recordRuntimeEvent?.("delivery", error, {
1225
+ phase: "guest-voice",
1226
+ guestQueryId: turn.guestQueryId,
1227
+ });
1228
+ }
1229
+ } else if (finalText) {
1185
1230
  if (deps.sendGuestReply) {
1186
1231
  await deps.sendGuestReply(turn.guestQueryId, finalText);
1187
1232
  } else {
package/lib/routing.ts CHANGED
@@ -21,10 +21,67 @@ import * as TextGroups from "./text-groups.ts";
21
21
  import * as ThreadReconciler from "./thread-reconciler.ts";
22
22
  import * as Turns from "./turns.ts";
23
23
 
24
- function formatTelegramPromptPeer(user: { id?: unknown; username?: unknown } | undefined): string | undefined {
25
- if (!user) return undefined;
26
- if (typeof user.username === "string" && user.username.length > 0) return user.username;
27
- return typeof user.id === "number" ? String(user.id) : undefined;
24
+ interface TelegramPromptPeerView {
25
+ id?: unknown;
26
+ username?: unknown;
27
+ first_name?: unknown;
28
+ last_name?: unknown;
29
+ title?: unknown;
30
+ }
31
+
32
+ function formatTelegramPromptPeer(
33
+ peer: TelegramPromptPeerView | undefined,
34
+ ): string | undefined {
35
+ if (!peer) return undefined;
36
+ if (typeof peer.username === "string" && peer.username.length > 0) {
37
+ return peer.username;
38
+ }
39
+ const displayName = [peer.first_name, peer.last_name]
40
+ .filter(
41
+ (part): part is string =>
42
+ typeof part === "string" && part.length > 0,
43
+ )
44
+ .join(" ");
45
+ if (displayName) return displayName;
46
+ if (typeof peer.title === "string" && peer.title.length > 0) {
47
+ return peer.title;
48
+ }
49
+ return typeof peer.id === "number" ? String(peer.id) : undefined;
50
+ }
51
+
52
+ function isTelegramPromptOwnerPeer(
53
+ peer: TelegramPromptPeerView | undefined,
54
+ ownerUserId: number | undefined,
55
+ ): boolean {
56
+ return ownerUserId !== undefined && peer?.id === ownerUserId;
57
+ }
58
+
59
+ export function resolveTelegramGuestPromptPeer(input: {
60
+ chatType?: string;
61
+ chat?: TelegramPromptPeerView;
62
+ from?: TelegramPromptPeerView;
63
+ replyFrom?: TelegramPromptPeerView;
64
+ guestBotCallerUser?: TelegramPromptPeerView;
65
+ guestBotCallerChat?: TelegramPromptPeerView;
66
+ ownerUserId?: number;
67
+ }): string | undefined {
68
+ if (input.chatType !== "private") {
69
+ return formatTelegramPromptPeer(input.chat);
70
+ }
71
+ if (!isTelegramPromptOwnerPeer(input.from, input.ownerUserId)) {
72
+ return formatTelegramPromptPeer(input.from);
73
+ }
74
+ for (const candidate of [
75
+ input.replyFrom,
76
+ input.chat,
77
+ input.guestBotCallerUser,
78
+ input.guestBotCallerChat,
79
+ ]) {
80
+ if (isTelegramPromptOwnerPeer(candidate, input.ownerUserId)) continue;
81
+ const peer = formatTelegramPromptPeer(candidate);
82
+ if (peer) return peer;
83
+ }
84
+ return undefined;
28
85
  }
29
86
 
30
87
  function appendTelegramSourceAttachmentSection(
@@ -1812,22 +1869,43 @@ export function createTelegramInboundRouteRuntime<
1812
1869
  const gm = guestMessage as unknown as Record<string, unknown>;
1813
1870
  // Build telegram prefix with guest context
1814
1871
  const chatRaw = gm.chat as Record<string, unknown>;
1815
- const chatTitle = chatRaw?.title as string | undefined;
1816
1872
  const chatType = chatRaw?.type as string;
1817
1873
  const fromRaw = gm.from as Record<string, unknown> | undefined;
1818
1874
  const replyMsg = gm.reply_to_message as Record<string, unknown> | undefined;
1819
1875
  const replyFromRaw = replyMsg?.from as Record<string, unknown> | undefined;
1820
- const fromPeer = formatTelegramPromptPeer(fromRaw);
1876
+ const guestBotCallerUser = gm.guest_bot_caller_user as
1877
+ | Record<string, unknown>
1878
+ | undefined;
1879
+ const guestBotCallerChat = gm.guest_bot_caller_chat as
1880
+ | Record<string, unknown>
1881
+ | undefined;
1882
+ const ownerUserId = deps.configStore.getAllowedUserId();
1821
1883
  const replyPeer = formatTelegramPromptPeer(replyFromRaw);
1822
- const fromIsOwner = fromRaw?.id === deps.configStore.getAllowedUserId();
1823
- const guestPeer = chatType === "private" && fromIsOwner && replyPeer
1824
- ? replyPeer
1825
- : fromPeer;
1884
+ const guestPeer = resolveTelegramGuestPromptPeer({
1885
+ chatType,
1886
+ chat: chatRaw,
1887
+ from: fromRaw,
1888
+ replyFrom: replyFromRaw,
1889
+ guestBotCallerUser,
1890
+ guestBotCallerChat,
1891
+ ownerUserId,
1892
+ });
1826
1893
  const prefixParts = ["telegram"];
1827
- if (chatType !== "private" && chatTitle) {
1828
- prefixParts.push(`guest:${chatTitle}`);
1829
- } else if (chatType === "private" && guestPeer) {
1894
+ if (guestPeer) {
1830
1895
  prefixParts.push(`guest:${guestPeer}`);
1896
+ } else if (chatType === "private") {
1897
+ deps.recordRuntimeEvent?.(
1898
+ "guest",
1899
+ new Error("Private Guest Mode remote peer could not be resolved"),
1900
+ {
1901
+ phase: "peer-attribution",
1902
+ chatId: typeof chatRaw?.id === "number" ? chatRaw.id : undefined,
1903
+ fromId: typeof fromRaw?.id === "number" ? fromRaw.id : undefined,
1904
+ hasReplyFrom: !!replyFromRaw,
1905
+ hasCallerUser: !!guestBotCallerUser,
1906
+ hasCallerChat: !!guestBotCallerChat,
1907
+ },
1908
+ );
1831
1909
  }
1832
1910
  const telegramPrefix = `[${prefixParts.join("|")}]`;
1833
1911
  // Extract reply context
package/lib/setup.ts CHANGED
@@ -197,8 +197,14 @@ export function createTelegramSetupPromptRuntime<
197
197
  promptEditor: (label, value) => ctx.ui.editor(label, value),
198
198
  getMe: deps.getMe,
199
199
  persistConfig: async (config) => {
200
- await deps.persistConfig(config);
200
+ const previousConfig = deps.getConfig();
201
201
  deps.setConfig(config);
202
+ try {
203
+ await deps.persistConfig(config);
204
+ } catch (error) {
205
+ deps.setConfig(previousConfig);
206
+ throw error;
207
+ }
202
208
  },
203
209
  notify: (message, level) => ctx.ui.notify(message, level),
204
210
  startPolling: () => deps.startPolling(ctx),
package/lib/status.ts CHANGED
@@ -181,6 +181,7 @@ export interface TelegramBridgeStatusLineState {
181
181
  hasBotToken?: boolean;
182
182
  botUsername?: string;
183
183
  activeProfileName?: string;
184
+ diagnosticPaths?: { state: string; logs: string };
184
185
  allowedUserId?: number;
185
186
  botThreadMode?: "unknown" | "enabled" | "disabled";
186
187
  botThreadModeUpdatedAtMs?: number;
@@ -259,6 +260,9 @@ export interface TelegramBridgeStatusRuntimeDeps<
259
260
  statusKey?: string;
260
261
  getConfig: () => TelegramBridgeStatusConfig;
261
262
  getActiveProfileName?: () => string | undefined;
263
+ getDiagnosticPaths?: (
264
+ profileName?: string,
265
+ ) => { state: string; logs: string };
262
266
  isPollingActive: () => boolean;
263
267
  getActiveSourceMessageIds: () => number[] | undefined;
264
268
  hasActiveTurn: () => boolean;
@@ -620,10 +624,12 @@ export function createTelegramBridgeStatusRuntime<
620
624
  getBridgeStatusLineState: () => {
621
625
  const config = deps.getConfig();
622
626
  const botThreadMode = deps.getBotThreadMode?.();
627
+ const activeProfileName = deps.getActiveProfileName?.();
623
628
  return {
624
629
  hasBotToken: Boolean(config.botToken),
625
630
  botUsername: config.botUsername,
626
- activeProfileName: deps.getActiveProfileName?.(),
631
+ activeProfileName,
632
+ diagnosticPaths: deps.getDiagnosticPaths?.(activeProfileName),
627
633
  allowedUserId: config.allowedUserId,
628
634
  botThreadMode: botThreadMode?.threadMode,
629
635
  botThreadModeUpdatedAtMs: botThreadMode?.updatedAtMs,
@@ -774,9 +780,6 @@ export function buildTelegramStatusBarText(
774
780
  return `${label} ${theme.fg("warning", "electing")}${queued}`;
775
781
  if (!state.pollingActive && state.busRole !== "follower")
776
782
  return `${theme.fg("accent", "telegram")} ${theme.fg("muted", "disconnected")}${queued}`;
777
- if (state.compactionInProgress) {
778
- return `${label} ${theme.fg("warning", "compacting")}${queued}`;
779
- }
780
783
  if (state.processing) {
781
784
  const processingStatus = state.queuedStatus
782
785
  ? "active"
@@ -1058,19 +1061,18 @@ function buildTelegramBridgeCompactStatusLines(
1058
1061
  ? ` (control=${controlQueueCount}, priority=${priorityQueueCount}, default=${defaultQueueCount})`
1059
1062
  : ""
1060
1063
  }`;
1061
- const executionState = state.compactionInProgress
1062
- ? "compacting"
1063
- : state.pendingDispatch
1064
- ? "pending dispatch"
1065
- : state.activeSourceMessageIds?.length
1066
- ? "active"
1067
- : "idle";
1064
+ const executionState = state.pendingDispatch
1065
+ ? "pending dispatch"
1066
+ : state.activeSourceMessageIds?.length
1067
+ ? "active"
1068
+ : "idle";
1068
1069
  const profileSuffix = state.activeProfileName
1069
1070
  ? `.${state.activeProfileName.replace(/[^a-zA-Z0-9._-]+/g, "_")}`
1070
1071
  : "";
1071
- const diagnosticsPaths = {
1072
+ const profileSlug = profileSuffix.slice(1);
1073
+ const diagnosticsPaths = state.diagnosticPaths ?? {
1072
1074
  state: `~/.pi/agent/tmp/telegram/state${profileSuffix}.json`,
1073
- logs: `~/.pi/agent/tmp/telegram/logs${profileSuffix}.jsonl`,
1075
+ logs: `~/.pi/agent/tmp/telegram/logs${profileSlug ? `.${profileSlug}` : ""}.jsonl`,
1074
1076
  };
1075
1077
  return [
1076
1078
  "connection:",
@@ -1251,7 +1253,6 @@ function buildContextSummary(
1251
1253
  }
1252
1254
 
1253
1255
  function buildStatusSummary(ctx: TelegramStatusContext): string {
1254
- if (ctx.isCompactionInProgress?.()) return "compacting";
1255
1256
  if (ctx.hasPendingMessages?.()) return "pending";
1256
1257
  if (ctx.isIdle?.() === false) return "active";
1257
1258
  if (ctx.isIdle?.() === true) return "idle";
@@ -287,6 +287,44 @@ export interface TelegramFileDownloadOptions {
287
287
  maxFileSizeBytes?: number;
288
288
  }
289
289
 
290
+ export type TelegramGuestCachedMediaResult =
291
+ | {
292
+ type: "document";
293
+ id: string;
294
+ title: string;
295
+ document_file_id: string;
296
+ caption?: string;
297
+ parse_mode?: string;
298
+ }
299
+ | {
300
+ type: "photo";
301
+ id: string;
302
+ photo_file_id: string;
303
+ caption?: string;
304
+ parse_mode?: string;
305
+ }
306
+ | {
307
+ type: "audio";
308
+ id: string;
309
+ audio_file_id: string;
310
+ caption?: string;
311
+ parse_mode?: string;
312
+ }
313
+ | {
314
+ type: "voice";
315
+ id: string;
316
+ voice_file_id: string;
317
+ title: string;
318
+ caption?: string;
319
+ parse_mode?: string;
320
+ };
321
+
322
+ export interface TelegramAnswerGuestQueryOptions {
323
+ parseMode?: string;
324
+ richMessage?: TelegramInputRichMessage;
325
+ result?: TelegramGuestCachedMediaResult;
326
+ }
327
+
290
328
  export interface TelegramAnswerCallbackQueryOptions {
291
329
  recordRuntimeEvent?: (
292
330
  kind: "api",
@@ -322,7 +360,7 @@ export interface TelegramApiClient {
322
360
  answerGuestQuery?: (
323
361
  guestQueryId: string,
324
362
  text?: string,
325
- options?: { parseMode?: string; richMessage?: TelegramInputRichMessage },
363
+ options?: TelegramAnswerGuestQueryOptions,
326
364
  ) => Promise<void>;
327
365
  }
328
366
 
@@ -401,7 +439,7 @@ export interface TelegramBridgeApiRuntime {
401
439
  answerGuestQuery: (
402
440
  guestQueryId: string,
403
441
  text?: string,
404
- options?: { parseMode?: string; richMessage?: TelegramInputRichMessage },
442
+ options?: TelegramAnswerGuestQueryOptions,
405
443
  ) => Promise<void>;
406
444
  deleteMessage: (chatId: number, messageId: number) => Promise<void>;
407
445
  prepareTempDir: () => Promise<number>;
@@ -1312,12 +1350,12 @@ export function createTelegramBridgeApiRuntime(
1312
1350
  answerGuestQuery: (
1313
1351
  guestQueryId: string,
1314
1352
  text: string | undefined,
1315
- options:
1316
- | { parseMode?: string; richMessage?: TelegramInputRichMessage }
1317
- | undefined,
1353
+ options: TelegramAnswerGuestQueryOptions | undefined,
1318
1354
  ) => {
1319
1355
  const body: Record<string, unknown> = { guest_query_id: guestQueryId };
1320
- if (text !== undefined || options?.richMessage) {
1356
+ if (options?.result) {
1357
+ body.result = options.result;
1358
+ } else if (text !== undefined || options?.richMessage) {
1321
1359
  const inputContent: Record<string, unknown> = options?.richMessage
1322
1360
  ? { rich_message: options.richMessage }
1323
1361
  : { message_text: text };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.20.4",
3
+ "version": "0.20.6",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"