@llblab/pi-telegram 0.6.3 → 0.7.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.
package/lib/queue.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  /**
2
- * Telegram queue and queue-runtime domain helpers
3
- * Owns queue items, queue mutations, dispatch and lifecycle planning, session resets, and queue-adjacent runtime helpers
2
+ * Telegram queue core contracts and pure planning helpers
3
+ * Zones: telegram queue, pi agent lifecycle, scheduling
4
+ * Owns queue item contracts, lane admission, pure queue mutations, and dispatch planning
4
5
  */
5
6
 
6
7
  // --- Queue Items ---
@@ -178,6 +179,14 @@ export function createTelegramQueueStore<TContext = unknown>(
178
179
  };
179
180
  }
180
181
 
182
+ export function createTelegramQueueItemCountGetter<TContext = unknown>(
183
+ store: Pick<TelegramQueueStore<TContext>, "getQueuedItems">,
184
+ ): () => number {
185
+ return function getTelegramQueueItemCount() {
186
+ return store.getQueuedItems().length;
187
+ };
188
+ }
189
+
181
190
  export function createTelegramActiveTurnStore<
182
191
  TTurn extends PendingTelegramTurn = PendingTelegramTurn,
183
192
  >(): TelegramActiveTurnStore<TTurn> {
@@ -344,7 +353,7 @@ function formatTelegramQueueItemStatusSummary<TContext = unknown>(
344
353
  item: TelegramQueueItem<TContext>,
345
354
  ): string {
346
355
  if (item.queueLane === "priority") {
347
- return `⬆ ${item.statusSummary}`;
356
+ return `⚡ ${item.statusSummary}`;
348
357
  }
349
358
  return item.statusSummary;
350
359
  }
@@ -665,18 +674,32 @@ export type TelegramAgentLifecycleHooksRuntimeDeps<
665
674
  TTurn extends PendingTelegramTurn,
666
675
  TContext,
667
676
  TMessage,
677
+ TReplyMarkup = unknown,
668
678
  > = TelegramAgentStartHookRuntimeDeps<TTurn, TContext> &
669
- TelegramAgentEndHookRuntimeDeps<TTurn, TContext, TMessage> &
679
+ TelegramAgentEndHookRuntimeDeps<TTurn, TContext, TMessage, TReplyMarkup> &
670
680
  TelegramToolExecutionHookRuntimeDeps<TContext>;
671
681
 
672
682
  export function createTelegramAgentLifecycleHooks<
673
683
  TTurn extends PendingTelegramTurn,
674
684
  TContext,
675
685
  TMessage,
676
- >(deps: TelegramAgentLifecycleHooksRuntimeDeps<TTurn, TContext, TMessage>) {
686
+ TReplyMarkup = unknown,
687
+ >(
688
+ deps: TelegramAgentLifecycleHooksRuntimeDeps<
689
+ TTurn,
690
+ TContext,
691
+ TMessage,
692
+ TReplyMarkup
693
+ >,
694
+ ) {
677
695
  return {
678
696
  onAgentStart: createTelegramAgentStartHook<TTurn, TContext>(deps),
679
- onAgentEnd: createTelegramAgentEndHook<TTurn, TContext, TMessage>(deps),
697
+ onAgentEnd: createTelegramAgentEndHook<
698
+ TTurn,
699
+ TContext,
700
+ TMessage,
701
+ TReplyMarkup
702
+ >(deps),
680
703
  ...createTelegramToolExecutionHooks<TContext>(deps),
681
704
  };
682
705
  }
@@ -737,6 +760,7 @@ export interface TelegramAgentEndOutboundReplyPlan<TReplyMarkup = unknown> {
737
760
 
738
761
  export interface TelegramAgentEndRuntimeDeps<
739
762
  TTurn extends PendingTelegramTurn,
763
+ TReplyMarkup = unknown,
740
764
  > {
741
765
  turn: TTurn | undefined;
742
766
  assistant: TelegramAgentEndAssistantResult;
@@ -750,13 +774,13 @@ export interface TelegramAgentEndRuntimeDeps<
750
774
  chatId: number,
751
775
  markdown: string,
752
776
  replyToMessageId: number,
753
- options?: { replyMarkup?: unknown },
777
+ options?: { replyMarkup?: TReplyMarkup },
754
778
  ) => Promise<boolean>;
755
779
  sendMarkdownReply: (
756
780
  chatId: number,
757
781
  replyToMessageId: number,
758
782
  markdown: string,
759
- options?: { replyMarkup?: unknown },
783
+ options?: { replyMarkup?: TReplyMarkup },
760
784
  ) => Promise<unknown>;
761
785
  sendTextReply: (
762
786
  chatId: number,
@@ -764,7 +788,9 @@ export interface TelegramAgentEndRuntimeDeps<
764
788
  text: string,
765
789
  ) => Promise<unknown>;
766
790
  sendQueuedAttachments: (turn: TTurn) => Promise<void>;
767
- planOutboundReply?: (markdown: string) => TelegramAgentEndOutboundReplyPlan;
791
+ planOutboundReply?: (
792
+ markdown: string,
793
+ ) => TelegramAgentEndOutboundReplyPlan<TReplyMarkup>;
768
794
  sendOutboundReplyArtifacts?: (
769
795
  turn: TTurn,
770
796
  plan: TelegramAgentEndOutboundReplyPlan,
@@ -776,6 +802,7 @@ export interface TelegramAgentEndHookRuntimeDeps<
776
802
  TTurn extends PendingTelegramTurn,
777
803
  TContext,
778
804
  TMessage,
805
+ TReplyMarkup = unknown,
779
806
  > {
780
807
  getActiveTurn: () => TTurn | undefined;
781
808
  extractAssistant: (
@@ -790,11 +817,20 @@ export interface TelegramAgentEndHookRuntimeDeps<
790
817
  ) => void;
791
818
  clearPreview: (chatId: number) => Promise<void>;
792
819
  setPreviewPendingText: (text: string) => void;
793
- finalizeMarkdownPreview: TelegramAgentEndRuntimeDeps<TTurn>["finalizeMarkdownPreview"];
794
- sendMarkdownReply: TelegramAgentEndRuntimeDeps<TTurn>["sendMarkdownReply"];
820
+ finalizeMarkdownPreview: TelegramAgentEndRuntimeDeps<
821
+ TTurn,
822
+ TReplyMarkup
823
+ >["finalizeMarkdownPreview"];
824
+ sendMarkdownReply: TelegramAgentEndRuntimeDeps<
825
+ TTurn,
826
+ TReplyMarkup
827
+ >["sendMarkdownReply"];
795
828
  sendTextReply: TelegramAgentEndRuntimeDeps<TTurn>["sendTextReply"];
796
829
  sendQueuedAttachments: (turn: TTurn) => Promise<void>;
797
- planOutboundReply?: TelegramAgentEndRuntimeDeps<TTurn>["planOutboundReply"];
830
+ planOutboundReply?: TelegramAgentEndRuntimeDeps<
831
+ TTurn,
832
+ TReplyMarkup
833
+ >["planOutboundReply"];
798
834
  sendOutboundReplyArtifacts?: TelegramAgentEndRuntimeDeps<TTurn>["sendOutboundReplyArtifacts"];
799
835
  }
800
836
 
@@ -872,7 +908,15 @@ export function createTelegramAgentEndHook<
872
908
  TTurn extends PendingTelegramTurn,
873
909
  TContext,
874
910
  TMessage,
875
- >(deps: TelegramAgentEndHookRuntimeDeps<TTurn, TContext, TMessage>) {
911
+ TReplyMarkup = unknown,
912
+ >(
913
+ deps: TelegramAgentEndHookRuntimeDeps<
914
+ TTurn,
915
+ TContext,
916
+ TMessage,
917
+ TReplyMarkup
918
+ >,
919
+ ) {
876
920
  return async function onAgentEnd(
877
921
  event: TelegramAgentEndHookEvent<TMessage>,
878
922
  ctx: TContext,
@@ -903,7 +947,8 @@ export function createTelegramAgentEndHook<
903
947
 
904
948
  export async function handleTelegramAgentEndRuntime<
905
949
  TTurn extends PendingTelegramTurn,
906
- >(deps: TelegramAgentEndRuntimeDeps<TTurn>): Promise<void> {
950
+ TReplyMarkup = unknown,
951
+ >(deps: TelegramAgentEndRuntimeDeps<TTurn, TReplyMarkup>): Promise<void> {
907
952
  const { turn, assistant } = deps;
908
953
  const rawFinalText = assistant.text;
909
954
  const outboundReply = rawFinalText
@@ -934,7 +979,7 @@ export async function handleTelegramAgentEndRuntime<
934
979
  turn.chatId,
935
980
  turn.replyToMessageId,
936
981
  assistant.errorMessage ||
937
- "Telegram bridge: pi failed while processing the request.",
982
+ "Telegram bridge: π failed while processing the request.",
938
983
  );
939
984
  if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
940
985
  return;
package/lib/rendering.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Telegram preview and markdown rendering helpers
3
+ * Zones: telegram rendering, shared text utils
3
4
  * Converts assistant output into Telegram-safe plain text and HTML chunks with chunk-boundary handling
4
5
  */
5
6
 
package/lib/replies.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Telegram reply delivery helpers
3
+ * Zones: telegram outbound, rendering transport
3
4
  * Owns rendered-message delivery, reply transport wiring, and plain or markdown final replies
4
5
  */
5
6
 
@@ -10,10 +11,48 @@ import {
10
11
  type TelegramRenderMode,
11
12
  } from "./rendering.ts";
12
13
 
14
+ // --- Reply Dedup ---
15
+
16
+ /** Non-persistent reply deduplication for a single agent turn.
17
+ * First reply to a prompt gets `reply_parameters.reply_to_message_id`;
18
+ * subsequent replies in the same turn skip it to avoid stacking
19
+ * duplicate reply headers in the chat viewport. */
20
+ export interface ReplyDedupRuntime {
21
+ /** Returns true if this is the first reply for the given prompt
22
+ * message id in the current turn. Side-effect: marks it replied. */
23
+ shouldReply(promptMessageId: number): boolean;
24
+ /** Reset the tracker when a new prompt enters the queue. */
25
+ reset(): void;
26
+ }
27
+
28
+ export function createReplyDedupRuntime(): ReplyDedupRuntime {
29
+ const replied = new Map<number, boolean>();
30
+ return {
31
+ shouldReply(promptMessageId: number): boolean {
32
+ if (replied.has(promptMessageId)) return false;
33
+ replied.set(promptMessageId, true);
34
+ return true;
35
+ },
36
+ reset(): void {
37
+ replied.clear();
38
+ },
39
+ };
40
+ }
41
+
42
+ // --- Transport-level dedup ---
43
+
44
+ let lastRepliedToMessageId: number | undefined;
45
+
46
+ export function resetTransportReplyDedup(): void {
47
+ lastRepliedToMessageId = undefined;
48
+ }
49
+
13
50
  export function buildTelegramReplyParameters(
14
51
  messageId: number | undefined,
15
52
  ): TelegramReplyParameters | undefined {
16
53
  if (messageId === undefined) return undefined;
54
+ if (messageId === lastRepliedToMessageId) return undefined;
55
+ lastRepliedToMessageId = messageId;
17
56
  return { message_id: messageId, allow_sending_without_reply: true };
18
57
  }
19
58
 
@@ -219,13 +258,13 @@ export interface TelegramRenderedMessageRuntimeDeps<TReplyMarkup> {
219
258
  export interface TelegramRenderedMessageRuntime<TReplyMarkup> {
220
259
  sendTextReply: (
221
260
  chatId: number,
222
- replyToMessageId: number,
261
+ replyToMessageId: number | undefined,
223
262
  text: string,
224
263
  options?: { parseMode?: "HTML" },
225
264
  ) => Promise<number | undefined>;
226
265
  sendMarkdownReply: (
227
266
  chatId: number,
228
- replyToMessageId: number,
267
+ replyToMessageId: number | undefined,
229
268
  markdown: string,
230
269
  options?: { replyMarkup?: unknown },
231
270
  ) => Promise<number | undefined>;
@@ -332,3 +371,52 @@ export function createTelegramRenderedMessageRuntime<TReplyMarkup>(
332
371
  },
333
372
  };
334
373
  }
374
+
375
+ // --- Dedup-wrapped Reply Wrappers ---
376
+
377
+ /** Wrap a sendTextReply with reply dedup so only the first message
378
+ * in a turn carries `reply_to_message_id`. */
379
+ export function dedupSendTextReply(
380
+ dedup: ReplyDedupRuntime,
381
+ inner: (
382
+ chatId: number,
383
+ replyToMessageId: number | undefined,
384
+ text: string,
385
+ options?: { parseMode?: "HTML" },
386
+ ) => Promise<number | undefined>,
387
+ ): (
388
+ chatId: number,
389
+ replyToMessageId: number,
390
+ text: string,
391
+ options?: { parseMode?: "HTML" },
392
+ ) => Promise<number | undefined> {
393
+ return async (chatId, replyToMessageId, text, options) => {
394
+ const effectiveReplyTo = dedup.shouldReply(replyToMessageId)
395
+ ? replyToMessageId
396
+ : undefined;
397
+ return inner(chatId, effectiveReplyTo, text, options);
398
+ };
399
+ }
400
+
401
+ /** Wrap a sendMarkdownReply with reply dedup. */
402
+ export function dedupSendMarkdownReply<TReplyMarkup = unknown>(
403
+ dedup: ReplyDedupRuntime,
404
+ inner: (
405
+ chatId: number,
406
+ replyToMessageId: number | undefined,
407
+ markdown: string,
408
+ options?: { replyMarkup?: TReplyMarkup },
409
+ ) => Promise<number | undefined>,
410
+ ): (
411
+ chatId: number,
412
+ replyToMessageId: number,
413
+ markdown: string,
414
+ options?: { replyMarkup?: TReplyMarkup },
415
+ ) => Promise<number | undefined> {
416
+ return async (chatId, replyToMessageId, markdown, options) => {
417
+ const effectiveReplyTo = dedup.shouldReply(replyToMessageId)
418
+ ? replyToMessageId
419
+ : undefined;
420
+ return inner(chatId, effectiveReplyTo, markdown, options);
421
+ };
422
+ }
package/lib/routing.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Telegram inbound routing composition
3
+ * Zones: telegram inbound, orchestration, queue/menu/command composition
3
4
  * Wires authorized updates into menus, commands, media grouping, and prompt queueing
4
5
  */
5
6
 
@@ -11,6 +12,7 @@ import * as Media from "./media.ts";
11
12
  import * as Menu from "./menu.ts";
12
13
  import * as Model from "./model.ts";
13
14
  import * as Queue from "./queue.ts";
15
+ import * as PromptTemplates from "./prompt-templates.ts";
14
16
  import type { TelegramBridgeRuntime } from "./runtime.ts";
15
17
  import * as Turns from "./turns.ts";
16
18
  import * as Updates from "./updates.ts";
@@ -25,11 +27,6 @@ export type TelegramRoutedCallbackQuery = Updates.TelegramCallbackQuery &
25
27
  Menu.MenuCallbackQuery;
26
28
 
27
29
  export interface TelegramInboundRouteRuntimeDeps<
28
- TUpdate extends Updates.TelegramUpdateFlow & {
29
- message?: TMessage;
30
- edited_message?: TMessage;
31
- callback_query?: TCallbackQuery;
32
- },
33
30
  TMessage extends TelegramRoutedMessage,
34
31
  TCallbackQuery extends TelegramRoutedCallbackQuery,
35
32
  TContext,
@@ -51,6 +48,15 @@ export interface TelegramInboundRouteRuntimeDeps<
51
48
  Model.ScopedTelegramModel<TModel>
52
49
  >;
53
50
  menuActions: Menu.TelegramMenuActionRuntime<TContext, TModel>;
51
+ openQueueMenu: (
52
+ chatId: number,
53
+ replyToMessageId: number,
54
+ ctx: TContext,
55
+ ) => Promise<void>;
56
+ queueMenuCallbackHandler: (
57
+ query: TCallbackQuery,
58
+ ctx: TContext,
59
+ ) => Promise<boolean>;
54
60
  buttonActionStore?: OutboundHandlers.TelegramButtonActionStore;
55
61
  attachmentHandlerRuntime: TelegramAttachmentHandlerRuntime<TContext>;
56
62
  updateStatus: (ctx: TContext, error?: string) => void;
@@ -65,10 +71,14 @@ export interface TelegramInboundRouteRuntimeDeps<
65
71
  text: string,
66
72
  ) => Promise<number | undefined>;
67
73
  setMyCommands: Commands.TelegramBotCommandRegistrationDeps["setMyCommands"];
74
+ getCommands: () => Parameters<
75
+ typeof PromptTemplates.getTelegramPromptTemplateCommands
76
+ >[0];
68
77
  downloadFile: Media.DownloadTelegramMessageFilesDeps["downloadFile"];
69
78
  getThinkingLevel: () => Model.ThinkingLevel;
70
79
  setThinkingLevel: (level: Model.ThinkingLevel) => void;
71
80
  setModel: (model: TModel) => Promise<boolean>;
81
+ sendUserMessage?: (message: string) => void;
72
82
  isIdle: (ctx: TContext) => boolean;
73
83
  hasPendingMessages: (ctx: TContext) => boolean;
74
84
  compact: (
@@ -82,6 +92,21 @@ export interface TelegramInboundRouteRuntimeDeps<
82
92
  ) => void;
83
93
  }
84
94
 
95
+ const TELEGRAM_OWNED_CALLBACK_PREFIXES = [
96
+ "menu:",
97
+ "model:",
98
+ "queue:",
99
+ "status:",
100
+ "tgbtn:",
101
+ "thinking:",
102
+ ] as const;
103
+
104
+ function isTelegramOwnedCallbackData(data: string): boolean {
105
+ return TELEGRAM_OWNED_CALLBACK_PREFIXES.some((prefix) =>
106
+ data.startsWith(prefix),
107
+ );
108
+ }
109
+
85
110
  export function createTelegramInboundRouteRuntime<
86
111
  TUpdate extends Updates.TelegramUpdateFlow & {
87
112
  message?: TMessage;
@@ -94,7 +119,6 @@ export function createTelegramInboundRouteRuntime<
94
119
  TModel extends Model.MenuModel,
95
120
  >(
96
121
  deps: TelegramInboundRouteRuntimeDeps<
97
- TUpdate,
98
122
  TMessage,
99
123
  TCallbackQuery,
100
124
  TContext,
@@ -159,8 +183,65 @@ export function createTelegramInboundRouteRuntime<
159
183
  );
160
184
  if (handled) return;
161
185
  }
186
+ const handledByQueue = await deps.queueMenuCallbackHandler(query, ctx);
187
+ if (handledByQueue) return;
188
+ const callbackData = query.data;
189
+ if (
190
+ deps.sendUserMessage &&
191
+ callbackData &&
192
+ !isTelegramOwnedCallbackData(callbackData)
193
+ ) {
194
+ deps.sendUserMessage(`[callback] ${callbackData}`);
195
+ await deps.answerCallbackQuery(query.id);
196
+ return;
197
+ }
162
198
  await menuCallbackHandler(query, ctx);
163
199
  };
200
+ const promptTurnBuilder = Turns.createTelegramPromptTurnRuntimeBuilder<
201
+ TMessage,
202
+ TContext
203
+ >({
204
+ allocateQueueOrder: deps.bridgeRuntime.queue.allocateItemOrder,
205
+ downloadFile: deps.downloadFile,
206
+ processAttachments: deps.attachmentHandlerRuntime.process,
207
+ });
208
+ const enqueueContinueTurn = async (
209
+ message: TMessage,
210
+ ctx: TContext,
211
+ ): Promise<void> => {
212
+ const enqueuePlan = Queue.planTelegramPromptEnqueue(
213
+ deps.telegramQueueStore.getQueuedItems(),
214
+ deps.bridgeRuntime.lifecycle.shouldPreserveQueuedTurnsAsHistory(),
215
+ );
216
+ deps.bridgeRuntime.lifecycle.setPreserveQueuedTurnsAsHistory(false);
217
+ const continueMessage = {
218
+ ...message,
219
+ text: "continue",
220
+ caption: undefined,
221
+ } as TMessage;
222
+ const turn = await promptTurnBuilder(
223
+ [continueMessage],
224
+ enqueuePlan.historyTurns,
225
+ ctx,
226
+ );
227
+ const continueTurn = {
228
+ ...turn,
229
+ queueLane: "priority" as const,
230
+ laneOrder: Number.MIN_SAFE_INTEGER + turn.queueOrder,
231
+ statusSummary: "continue",
232
+ };
233
+ deps.telegramQueueStore.setQueuedItems(enqueuePlan.remainingItems);
234
+ deps.queueMutationRuntime.append(continueTurn, ctx);
235
+ deps.dispatchNextQueuedTelegramTurn(ctx);
236
+ };
237
+ const reservedCommandNames = new Set(
238
+ Commands.TELEGRAM_RESERVED_COMMAND_NAMES,
239
+ );
240
+ const getPromptTemplateCommands = () =>
241
+ PromptTemplates.getTelegramPromptTemplateCommands(
242
+ deps.getCommands(),
243
+ reservedCommandNames,
244
+ );
164
245
  const commandHandler = Commands.createTelegramCommandHandlerTargetRuntime<
165
246
  TMessage,
166
247
  TContext
@@ -181,15 +262,25 @@ export function createTelegramInboundRouteRuntime<
181
262
  deps.bridgeRuntime.lifecycle.setCompactionInProgress,
182
263
  updateStatus: deps.updateStatus,
183
264
  dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
265
+ enqueueContinueTurn,
184
266
  compact: deps.compact,
185
267
  allocateItemOrder: deps.bridgeRuntime.queue.allocateItemOrder,
186
268
  allocateControlOrder: deps.bridgeRuntime.queue.allocateControlOrder,
187
269
  appendControlItem: deps.queueMutationRuntime.append,
188
270
  showStatus: deps.menuActions.sendStatusMessage,
189
271
  openModelMenu: deps.menuActions.openModelMenu,
272
+ openThinkingMenu: (message, ctx) => {
273
+ const chatId = (message as { chat: { id: number } }).chat.id;
274
+ return deps.menuActions.openThinkingMenu(chatId, message.message_id, ctx);
275
+ },
276
+ openQueueMenu: (message, ctx) => {
277
+ const chatId = (message as { chat: { id: number } }).chat.id;
278
+ return deps.openQueueMenu(chatId, message.message_id, ctx);
279
+ },
190
280
  getAllowedUserId: deps.configStore.getAllowedUserId,
191
281
  setAllowedUserId: deps.configStore.setAllowedUserId,
192
282
  setMyCommands: deps.setMyCommands,
283
+ getPromptTemplateCommands,
193
284
  persistConfig: deps.configStore.persist,
194
285
  sendTextReply: deps.sendTextReply,
195
286
  recordRuntimeEvent: deps.recordRuntimeEvent,
@@ -203,14 +294,7 @@ export function createTelegramInboundRouteRuntime<
203
294
  deps.bridgeRuntime.lifecycle.shouldPreserveQueuedTurnsAsHistory,
204
295
  setPreserveQueuedTurnsAsHistory:
205
296
  deps.bridgeRuntime.lifecycle.setPreserveQueuedTurnsAsHistory,
206
- createTurn: Turns.createTelegramPromptTurnRuntimeBuilder<
207
- TMessage,
208
- TContext
209
- >({
210
- allocateQueueOrder: deps.bridgeRuntime.queue.allocateItemOrder,
211
- downloadFile: deps.downloadFile,
212
- processAttachments: deps.attachmentHandlerRuntime.process,
213
- }),
297
+ createTurn: promptTurnBuilder,
214
298
  updateStatus: deps.updateStatus,
215
299
  dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
216
300
  }).enqueue;
@@ -220,6 +304,14 @@ export function createTelegramInboundRouteRuntime<
220
304
  >({
221
305
  extractRawText: Media.extractFirstTelegramMessageText,
222
306
  handleCommand: commandHandler,
307
+ expandPromptTemplateCommand: (commandName, args) =>
308
+ PromptTemplates.expandTelegramPromptTemplateCommand(
309
+ commandName,
310
+ args,
311
+ getPromptTemplateCommands(),
312
+ ),
313
+ replaceMessageText: (message, text) =>
314
+ ({ ...message, text, caption: undefined }) as TMessage,
223
315
  enqueueTurn: promptEnqueue,
224
316
  });
225
317
  const mediaDispatch = Media.createTelegramMediaGroupDispatchRuntime<
package/lib/runtime.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Telegram bridge runtime-state helpers
3
+ * Zones: pi agent runtime state, telegram session, shared coordination
3
4
  * Owns small session-local runtime primitives that are shared by orchestration but are not specific to queueing, rendering, polling, or Telegram transport
4
5
  */
5
6
 
@@ -350,6 +351,7 @@ export function createTelegramTypingLoopStarter<TContext>(
350
351
  } catch (error) {
351
352
  const message =
352
353
  error instanceof Error ? error.message : String(error);
354
+ deps.updateStatus(ctx, message);
353
355
  deps.recordRuntimeEvent?.("typing", error, {
354
356
  chatId: targetChatId,
355
357
  });
package/lib/setup.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Telegram setup prompt helpers
3
+ * Zones: pi agent command ui, telegram config
3
4
  * Computes token-prefill defaults and prompt mode selection for /telegram-setup
4
5
  */
5
6
 
package/lib/status.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Telegram status rendering helpers
3
+ * Zones: telegram ui, pi agent diagnostics, tui
3
4
  * Builds usage, cost, and context summaries for the interactive Telegram status view
4
5
  */
5
6
 
@@ -41,6 +42,8 @@ export interface TelegramStatusActiveModel {
41
42
  export interface TelegramStatusContext {
42
43
  sessionManager: { getEntries(): TelegramStatusSessionEntry[] };
43
44
  getContextUsage(): TelegramContextUsage | undefined;
45
+ isIdle?: () => boolean;
46
+ hasPendingMessages?: () => boolean;
44
47
  modelRegistry: {
45
48
  isUsingOAuth(model: TelegramStatusActiveModel): boolean;
46
49
  };
@@ -406,12 +409,17 @@ export function buildTelegramStatusBarText(
406
409
  return `${label} ${theme.fg("muted", "disconnected")}`;
407
410
  if (!state.paired)
408
411
  return `${label} ${theme.fg("warning", "awaiting pairing")}`;
409
- const queued = theme.fg("muted", state.queuedStatus);
412
+ const queued = state.queuedStatus
413
+ ? theme.fg("muted", state.queuedStatus)
414
+ : "";
410
415
  if (state.compactionInProgress) {
411
416
  return `${label} ${theme.fg("accent", "compacting")}${queued}`;
412
417
  }
413
418
  if (state.processing) {
414
- return `${label} ${theme.fg("accent", state.processingStatus ?? "processing")}${queued}`;
419
+ const processingStatus = state.processingStatus ?? "processing";
420
+ const processingToken =
421
+ processingStatus === "active" ? "warning" : "accent";
422
+ return `${label} ${theme.fg(processingToken, processingStatus)}${queued}`;
415
423
  }
416
424
  return `${label} ${theme.fg("success", "connected")}`;
417
425
  }
@@ -528,6 +536,13 @@ function buildContextSummary(
528
536
  return `${percent}/${formatTokens(contextWindow)}`;
529
537
  }
530
538
 
539
+ function buildStatusSummary(ctx: TelegramStatusContext): string {
540
+ if (ctx.hasPendingMessages?.()) return "pending";
541
+ if (ctx.isIdle?.() === false) return "active";
542
+ if (ctx.isIdle?.() === true) return "idle";
543
+ return "unknown";
544
+ }
545
+
531
546
  export function buildStatusHtml(
532
547
  ctx: TelegramStatusContext,
533
548
  activeModel: TelegramStatusActiveModel | undefined,
@@ -536,7 +551,7 @@ export function buildStatusHtml(
536
551
  const usesSubscription = activeModel
537
552
  ? ctx.modelRegistry.isUsingOAuth(activeModel)
538
553
  : false;
539
- const lines: string[] = [];
554
+ const lines: string[] = [buildStatusRow("Status", buildStatusSummary(ctx))];
540
555
  const usageSummary = buildUsageSummary(stats);
541
556
  const costSummary = buildCostSummary(stats, usesSubscription);
542
557
  if (usageSummary) {
@@ -546,8 +561,5 @@ export function buildStatusHtml(
546
561
  lines.push(buildStatusRow("Cost", costSummary));
547
562
  }
548
563
  lines.push(buildStatusRow("Context", buildContextSummary(ctx, activeModel)));
549
- if (lines.length === 0) {
550
- lines.push(buildStatusRow("Status", "No usage data yet."));
551
- }
552
564
  return lines.join("\n");
553
565
  }
package/lib/turns.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Telegram turn-building helpers
3
+ * Zones: telegram inbound, pi agent prompt content, queue
3
4
  * Owns prompt-turn summary and content construction so queued Telegram turns are assembled consistently
4
5
  */
5
6