@llblab/pi-telegram 0.45.8 → 0.45.10

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/CHANGELOG.md CHANGED
@@ -4,6 +4,16 @@
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.45.10: Rich headings and state-scope markers
8
+
9
+ - `Rich section headings`: Public Rich Message block types now include native headings, allowing companion sections to place a fixed title above sibling disclosures instead of wrapping the entire view in another collapsible block.
10
+ - `State-scope markers`: Registers 🌐 Global, 📂 CWD, 💬 Session, and 🧬 Effective as canonical state-scope semantics for chooser buttons and selected-scope headings, with composition flowing Global → CWD → Session → Effective.
11
+
12
+ ## 0.45.9: Rich sections and exact queue continuation
13
+
14
+ - `Extension Sections`: Public section contexts now expose `openRich(message)` for one standalone Telegram Native Rich Message routed to the exact callback chat/thread with normal message-ownership recording and scoped callback diagnostics. Companion extensions can reuse native `details`/`pre` disclosure without raw bot access; `👁` is the canonical read-only inspection marker.
15
+ - `/next` ordering: Active interruption settles before continuation is announced. The notice is bound to the exact selected queued item and revalidates queue identity, readiness, admission, pending mutations, and transport authority before dispatch; removal, clear, reordering, concurrent append, or transport loss cannot transfer stale intent to unrelated work.
16
+
7
17
  ## 0.45.8: Follower Forwarding Hotfix
8
18
 
9
19
  - `Follower Forwarding`: Message-ownership lookups now project the matching live registration's protocol identity before forwarding. Voice/message and edit retries, message-only callbacks, and reactions no longer stall on an incomplete cached owner after a rejection or re-registration. Exact generation, binding, protocol, and durable-receipt checks remain enforced.
package/api/sections.ts CHANGED
@@ -4,6 +4,8 @@
4
4
  * Exposes the stable managed Telegram menu-section surface while keeping registry internals package-private
5
5
  */
6
6
 
7
+ export type { TelegramInputRichBlock, TelegramInputRichMessage, TelegramRichText } from "../lib/telegram-api.ts";
8
+
7
9
  export {
8
10
  getTelegramSectionDiagnostics,
9
11
  registerTelegramSection,
@@ -507,7 +507,7 @@ Immediate controls:
507
507
  - `/start` opens the main inline application menu.
508
508
  - `/model`, `/thinking`, `/queue`, and `/settings` are hidden shortcuts to menu sections.
509
509
  - `/compact` opens an inline confirmation dialog and then runs compaction when the bridge is idle.
510
- - `/next` dispatches the next queued turn, aborting Pi first when needed. When an active Telegram turn is aborted, its single Pi-aligned informational notice replies to a pre-abort snapshot of that turn; otherwise the command message is the fallback target. Aborted pending assistant text is not projected as a second reply, while already completed intermediate output remains visible.
510
+ - `/next` dispatches the next queued turn, aborting Pi first when needed. Active-turn settlement delivers its abort notice to the interrupted prompt before queue dispatch continues. The queue owner then emits `Dispatching next queued turn.` against the exact selected prompt's chat/thread/reply id before its one model dispatch; skipped, inactive, pending-mutation, and admission-blocked candidates never receive it. The `/next` command itself is never a lifecycle-notice reply target. Aborted pending assistant text is not projected as a second reply, while already completed intermediate output remains visible.
511
511
  - `/abort` aborts active work while preserving queued items. Abort-history preservation is enabled only for Telegram-owned active turns; later local/non-Telegram agent starts clear stale abort-history mode so the next Telegram prompt appends instead of absorbing old queued turns as history.
512
512
  - `/stop` aborts and clears waiting Telegram queue items.
513
513
 
@@ -52,7 +52,7 @@ Stable commands inside the paired Telegram DM:
52
52
 
53
53
  - `/start` — pair when needed and open the main application menu.
54
54
  - `/compact` — open confirmation and compact when idle.
55
- - `/next` — dispatch the next queued turn, aborting active work first when needed; one Pi-aligned informational reply anchors to a pre-abort snapshot of the Telegram turn or falls back to the command, and aborted pending assistant text is suppressed.
55
+ - `/next` — abort active work first when needed, let the interrupted prompt receive its abort notice, then reply `Dispatching next queued turn.` to the exact queued prompt selected for the next model turn. The command itself is never the lifecycle-notice reply target, and aborted pending assistant text is suppressed.
56
56
  - `/continue` — enqueue a priority `continue` prompt.
57
57
  - `/abort` — abort active work and keep the queue; abort-history is scoped to Telegram-owned active turns.
58
58
  - `/stop` — abort active Telegram-owned work and clear waiting Telegram queue items.
package/docs/sections.md CHANGED
@@ -262,6 +262,8 @@ interface TelegramSectionContext {
262
262
  edit(view: TelegramSectionView): Promise<void>;
263
263
  /** Send a standalone chat message without auto-navigation */
264
264
  open(view: TelegramSectionView): Promise<void>;
265
+ /** Send one standalone Telegram Native Rich Message without exposing transport */
266
+ openRich(message: TelegramInputRichMessage): Promise<void>;
265
267
  /** Enqueue a plain-text prompt turn */
266
268
  enqueuePrompt(prompt: string): Promise<void>;
267
269
  /** Build a section-namespaced callback_data string */
@@ -285,6 +287,7 @@ interface TelegramSectionCallbackContext {
285
287
  answerCallback(text?: string): Promise<void>;
286
288
  edit(view: TelegramSectionView): Promise<void>;
287
289
  open(view: TelegramSectionView): Promise<void>;
290
+ openRich(message: TelegramInputRichMessage): Promise<void>;
288
291
  enqueuePrompt(prompt: string): Promise<void>;
289
292
  callbackData(action: string, payload?: string): string;
290
293
  /** Delete the message that triggered this callback */
@@ -292,6 +295,12 @@ interface TelegramSectionCallbackContext {
292
295
  }
293
296
  ```
294
297
 
298
+ ### `openRich` semantics
299
+
300
+ `ctx.openRich(message)` sends exactly one standalone Telegram Native Rich Message to the callback's exact chat/thread target. It accepts the public `TelegramInputRichMessage` tree (`details` and `pre` blocks) used by native tool evidence, records message ownership, and routes through the active direct/follower transport. It adds no menu navigation and exposes no raw bot client or arbitrary API method.
301
+
302
+ Use it when disclosure behavior is part of the document, such as one selected read-only state or evidence tree. Do not imitate nested disclosure with HTML or Markdown when the native tree is required.
303
+
295
304
  ### `enqueuePrompt` semantics
296
305
 
297
306
  Queues a `[telegram] <prompt>` turn in the default lane with the paired user's `chatId`. Uses `queueMutationRuntime.append()` and triggers `dispatchNextQueuedTelegramTurn()`. The prompt arrives as a normal Telegram-owned turn — the agent sees it as if the user typed it.
package/docs/ui-style.md CHANGED
@@ -36,6 +36,11 @@ Use emoji as stable semantic markers, not decoration. Emoji carry transportable
36
36
  | `🔬` | Activity / technical detail | Activity settings row and detail card | Chooses quiet, thinking, tools, or verbose bridge activity; not a generic diagnostics marker. |
37
37
  | `🧠` | Model thinking controls | Thinking menus and status rows | Thinking activity quotes omit this icon and their header entirely to minimize chat height. |
38
38
  | `📎` | Attachment | Attachment summaries, queue rows for attachment-only turns | Not for thread binding. |
39
+ | `👁` | Read-only inspection | State/detail viewers and inspection entrypoints | Opens evidence without mutating the inspected object; do not use for edit or refresh actions. |
40
+ | `🌐` | Global state scope | State-scope chooser and selected-scope heading | Cross-project memory composed first. |
41
+ | `📂` | Current-working-directory state scope | State-scope chooser and selected-scope heading | Project-local memory composed after Global. |
42
+ | `💬` | Session state scope | State-scope chooser and selected-scope heading | Conversation-branch memory composed after CWD. |
43
+ | `🧬` | Effective composed state | State-scope chooser and selected-scope heading | The resulting Global → CWD → Session projection. |
39
44
 
40
45
  ### Command And Control Actions
41
46
 
package/lib/bindings.ts CHANGED
@@ -50,6 +50,7 @@ type TelegramAgentMessageRouter = NonNullable<
50
50
  export interface TelegramQueueBindingRuntime<TContext> {
51
51
  mutation: Queue.TelegramQueueMutationController<TContext>;
52
52
  dispatchNext: (ctx: TContext) => void;
53
+ requestNextDispatchAnnouncement: () => void;
53
54
  watchdog: Queue.TelegramQueueDispatchWatchdogRuntime<TContext>;
54
55
  }
55
56
 
@@ -122,7 +123,7 @@ export function createTelegramQueueBindingRuntime<TContext>(deps: {
122
123
  updateStatus: deps.updateStatus,
123
124
  recordRuntimeEvent: deps.recordRuntimeEvent,
124
125
  });
125
- const dispatchNext = Queue.createTelegramQueueDispatchRuntime({
126
+ const dispatch = Queue.createTelegramQueueDispatchRuntime({
126
127
  ...deps.store,
127
128
  isCompactionInProgress: deps.lifecycle.isCompactionInProgress,
128
129
  hasActiveTurn: deps.activeTurn.has,
@@ -162,13 +163,14 @@ export function createTelegramQueueBindingRuntime<TContext>(deps: {
162
163
  recordRuntimeEvent: deps.recordRuntimeEvent,
163
164
  ...deps.promptDispatch,
164
165
  sendUserMessage: deps.sendUserMessage,
165
- }).dispatchNext;
166
+ });
166
167
  return {
167
168
  mutation,
168
- dispatchNext,
169
+ dispatchNext: dispatch.dispatchNext,
170
+ requestNextDispatchAnnouncement: dispatch.requestNextDispatchAnnouncement,
169
171
  watchdog: Queue.createTelegramQueueDispatchWatchdogRuntime({
170
172
  hasQueuedItems: deps.store.hasQueuedItems,
171
- dispatchNextQueuedTelegramTurn: dispatchNext,
173
+ dispatchNextQueuedTelegramTurn: dispatch.dispatchNext,
172
174
  recordRuntimeEvent: deps.recordRuntimeEvent,
173
175
  }),
174
176
  };
package/lib/commands.ts CHANGED
@@ -1147,6 +1147,7 @@ export interface TelegramCommandRuntimeDeps<
1147
1147
  updateStatus: (ctx: TContext) => void;
1148
1148
  isContextActive?: (ctx: TContext) => boolean;
1149
1149
  dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
1150
+ requestNextDispatchAnnouncement?: () => void;
1150
1151
  requestDeferredDispatchNextQueuedTelegramTurn?: (
1151
1152
  dispatch: (ctx: TContext) => void,
1152
1153
  ) => void;
@@ -1416,6 +1417,7 @@ export async function handleTelegramNextCommand(deps: {
1416
1417
  clearPendingModelSwitch: () => void;
1417
1418
  abortCurrentTurn: () => void;
1418
1419
  dispatchNextQueuedTurn: () => void;
1420
+ requestNextDispatchAnnouncement?: () => void;
1419
1421
  clearFoldForDispatch: () => void;
1420
1422
  updateStatus: () => void;
1421
1423
  sendTextReply: (
@@ -1435,19 +1437,10 @@ export async function handleTelegramNextCommand(deps: {
1435
1437
  return;
1436
1438
  }
1437
1439
  if (!deps.isIdle() && deps.hasAbortHandler()) {
1438
- const activeTurnReply = deps.getActiveTurnReply?.();
1439
1440
  deps.clearFoldForDispatch();
1441
+ deps.requestNextDispatchAnnouncement?.();
1440
1442
  deps.abortCurrentTurn();
1441
1443
  deps.updateStatus();
1442
- const notice = formatTelegramInformationHeading(
1443
- "⏩",
1444
- "Dispatching next queued turn.",
1445
- );
1446
- if (activeTurnReply) {
1447
- await activeTurnReply(notice, { parseMode: "HTML" });
1448
- } else {
1449
- await deps.sendTextReply(notice, { parseMode: "HTML" });
1450
- }
1451
1444
  return;
1452
1445
  }
1453
1446
  if (!deps.isIdle()) {
@@ -1460,12 +1453,9 @@ export async function handleTelegramNextCommand(deps: {
1460
1453
  );
1461
1454
  return;
1462
1455
  }
1456
+ deps.requestNextDispatchAnnouncement?.();
1463
1457
  deps.dispatchNextQueuedTurn();
1464
1458
  deps.updateStatus();
1465
- await deps.sendTextReply(
1466
- formatTelegramInformationHeading("▶️", "Dispatching next queued turn."),
1467
- { parseMode: "HTML" },
1468
- );
1469
1459
  }
1470
1460
 
1471
1461
  export async function handleTelegramContinueCommand<TMessage, TContext>(
@@ -2029,6 +2019,7 @@ async function handleTelegramCommandRuntime<
2029
2019
  abortCurrentTurn: deps.abortCurrentTurn,
2030
2020
  dispatchNextQueuedTurn: () =>
2031
2021
  deps.dispatchNextQueuedTelegramTurn(commandCtx),
2022
+ requestNextDispatchAnnouncement: deps.requestNextDispatchAnnouncement,
2032
2023
  clearFoldForDispatch: () =>
2033
2024
  deps.setFoldQueuedPromptsIntoHistory(false),
2034
2025
  updateStatus: updateStatusFor(commandCtx),
package/lib/extension.ts CHANGED
@@ -629,8 +629,12 @@ export default function (pi: Pi.ExtensionAPI) {
629
629
  getAssistantRenderingMode: configControls.getAssistantRenderingMode,
630
630
  editMessage: editTelegramMessageText,
631
631
  });
632
- const { replyTransport, editInteractiveMessage, sendInteractiveMessage } =
633
- replyRuntime;
632
+ const {
633
+ replyTransport,
634
+ editInteractiveMessage,
635
+ sendInteractiveMessage,
636
+ sendSectionRichMessage,
637
+ } = replyRuntime;
634
638
  const deliveryTargetPolicyRuntime =
635
639
  Delivery.createTelegramDeliveryTargetPolicyRuntime({
636
640
  ownsDirect: lockRuntime.owns,
@@ -744,6 +748,7 @@ export default function (pi: Pi.ExtensionAPI) {
744
748
  const {
745
749
  mutation: queueMutationRuntime,
746
750
  dispatchNext: dispatchNextQueuedTelegramTurn,
751
+ requestNextDispatchAnnouncement,
747
752
  watchdog: queueDispatchWatchdogRuntime,
748
753
  } = Bindings.createTelegramQueueBindingRuntime({
749
754
  store: telegramQueueStore,
@@ -964,6 +969,7 @@ export default function (pi: Pi.ExtensionAPI) {
964
969
  openSettingsMenu: settingsMenuRuntime.openSettingsMenu,
965
970
  settingsMenuCallbackHandler: settingsMenuRuntime.handleCallbackQuery,
966
971
  sectionRegistry,
972
+ sendSectionRichMessage,
967
973
  buttonActionStore,
968
974
  invokeBoundButtonAction: invokeGenerativeAppBoundButtonAction,
969
975
  inboundHandlerRuntime,
@@ -972,6 +978,7 @@ export default function (pi: Pi.ExtensionAPI) {
972
978
  updateStatus,
973
979
  isContextActive: telegramSessionContextStore.isCurrent,
974
980
  dispatchNextQueuedTelegramTurn,
981
+ requestNextDispatchAnnouncement,
975
982
  requestDeferredDispatchNextQueuedTelegramTurn:
976
983
  deferredQueueDispatchRuntime.request,
977
984
  hasDeferredDispatchContext: deferredQueueDispatchRuntime.isBound,
package/lib/menu.ts CHANGED
@@ -32,6 +32,7 @@ import {
32
32
  type ScopedTelegramModel,
33
33
  type ThinkingLevel,
34
34
  } from "./model.ts";
35
+ import type { TelegramInputRichMessage } from "./telegram-api.ts";
35
36
  import {
36
37
  handleTelegramSectionCallback,
37
38
  handleTelegramSectionOpen,
@@ -225,6 +226,11 @@ export interface TelegramMenuCallbackRuntimeDeps<
225
226
  replyMarkup: TelegramReplyMarkup,
226
227
  options?: { target?: { chatId: number; threadId?: number } },
227
228
  ) => Promise<number | undefined>;
229
+ sendSectionRichMessage?: (
230
+ chatId: number,
231
+ message: TelegramInputRichMessage,
232
+ options?: { target?: { chatId: number; threadId?: number } },
233
+ ) => Promise<number | undefined>;
228
234
  enqueueSectionPrompt?: (
229
235
  prompt: string,
230
236
  ctx: TContext,
@@ -478,6 +484,11 @@ export interface TelegramMenuCallbackRuntimeAdapterDeps<
478
484
  replyMarkup: TelegramReplyMarkup,
479
485
  options?: { target?: { chatId: number; threadId?: number } },
480
486
  ) => Promise<number | undefined>;
487
+ sendSectionRichMessage?: (
488
+ chatId: number,
489
+ message: TelegramInputRichMessage,
490
+ options?: { target?: { chatId: number; threadId?: number } },
491
+ ) => Promise<number | undefined>;
481
492
  enqueueSectionPrompt?: (
482
493
  prompt: string,
483
494
  ctx: TContext,
@@ -527,6 +538,7 @@ export function createTelegramMenuCallbackHandlerForContext<
527
538
  sectionRegistry: deps.sectionRegistry,
528
539
  editInteractiveMessage: deps.editInteractiveMessage,
529
540
  sendInteractiveMessage: deps.sendInteractiveMessage,
541
+ sendSectionRichMessage: deps.sendSectionRichMessage,
530
542
  enqueueSectionPrompt: deps.enqueueSectionPrompt,
531
543
  deleteMessage: deps.deleteMessage,
532
544
  });
@@ -591,6 +603,9 @@ export async function handleTelegramMenuCallbackRuntime<
591
603
  deps.editInteractiveMessage ?? (async () => {}),
592
604
  sendInteractiveMessage:
593
605
  deps.sendInteractiveMessage ?? (async () => undefined),
606
+ sendRichMessage: deps.sendSectionRichMessage ?? (async () => {
607
+ throw new Error("Rich Message delivery is unavailable");
608
+ }),
594
609
  enqueuePrompt: deps.enqueueSectionPrompt
595
610
  ? (prompt: string) =>
596
611
  deps.enqueueSectionPrompt!(prompt, ctx, target, query)
@@ -614,6 +629,9 @@ export async function handleTelegramMenuCallbackRuntime<
614
629
  deps.editInteractiveMessage ?? (async () => {}),
615
630
  sendInteractiveMessage:
616
631
  deps.sendInteractiveMessage ?? (async () => undefined),
632
+ sendRichMessage: deps.sendSectionRichMessage ?? (async () => {
633
+ throw new Error("Rich Message delivery is unavailable");
634
+ }),
617
635
  enqueuePrompt: deps.enqueueSectionPrompt
618
636
  ? (prompt: string) =>
619
637
  deps.enqueueSectionPrompt!(prompt, ctx, target, query)
@@ -639,6 +657,9 @@ export async function handleTelegramMenuCallbackRuntime<
639
657
  deps.editInteractiveMessage ?? (async () => {}),
640
658
  sendInteractiveMessage:
641
659
  deps.sendInteractiveMessage ?? (async () => undefined),
660
+ sendRichMessage: deps.sendSectionRichMessage ?? (async () => {
661
+ throw new Error("Rich Message delivery is unavailable");
662
+ }),
642
663
  enqueuePrompt: deps.enqueueSectionPrompt
643
664
  ? (prompt: string) =>
644
665
  deps.enqueueSectionPrompt!(prompt, ctx, target, query)
package/lib/queue.ts CHANGED
@@ -3055,6 +3055,7 @@ export interface TelegramQueueDispatchControllerDeps<
3055
3055
 
3056
3056
  export interface TelegramQueueDispatchController<TContext = unknown> {
3057
3057
  dispatchNext: (ctx: TContext) => void;
3058
+ requestNextDispatchAnnouncement: () => void;
3058
3059
  }
3059
3060
 
3060
3061
  export function executeTelegramQueueDispatchPlan<TContext = unknown>(
@@ -3123,7 +3124,13 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
3123
3124
  deps: TelegramQueueDispatchControllerDeps<TContext>,
3124
3125
  ): TelegramQueueDispatchController<TContext> {
3125
3126
  let controlDispatchPending = false;
3127
+ let nextDispatchAnnouncementRequested = false;
3128
+ let nextDispatchAnnouncementAnchor: TelegramQueueItem<TContext> | undefined;
3126
3129
  const controller: TelegramQueueDispatchController<TContext> = {
3130
+ requestNextDispatchAnnouncement: () => {
3131
+ nextDispatchAnnouncementRequested = true;
3132
+ nextDispatchAnnouncementAnchor = undefined;
3133
+ },
3127
3134
  dispatchNext: (ctx) => {
3128
3135
  if (deps.hasDispatchContext && !deps.hasDispatchContext()) return;
3129
3136
  if (controlDispatchPending) {
@@ -3151,6 +3158,10 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
3151
3158
  }
3152
3159
  if (droppedInactiveItemCount > 0) {
3153
3160
  deps.setQueuedItems(retainedItems);
3161
+ if (retainedItems.length === 0) {
3162
+ nextDispatchAnnouncementRequested = false;
3163
+ nextDispatchAnnouncementAnchor = undefined;
3164
+ }
3154
3165
  deps.recordRuntimeEvent?.(
3155
3166
  "dispatch",
3156
3167
  new Error(
@@ -3209,6 +3220,21 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
3209
3220
  }
3210
3221
  const dispatchableItems = activeItems.slice(nextActiveIndex);
3211
3222
  const nextItem = dispatchableItems[0];
3223
+ if (nextDispatchAnnouncementRequested) {
3224
+ if (nextDispatchAnnouncementAnchor) {
3225
+ const anchorRetained = retainedItems.includes(nextDispatchAnnouncementAnchor);
3226
+ const anchorTransportActive =
3227
+ deps.isQueueItemTransportActive?.(nextDispatchAnnouncementAnchor) !== false;
3228
+ if (!anchorRetained || !anchorTransportActive) {
3229
+ nextDispatchAnnouncementRequested = false;
3230
+ nextDispatchAnnouncementAnchor = undefined;
3231
+ }
3232
+ } else if (nextItem) {
3233
+ nextDispatchAnnouncementAnchor = nextItem;
3234
+ } else if (retainedItems.length === 0) {
3235
+ nextDispatchAnnouncementRequested = false;
3236
+ }
3237
+ }
3212
3238
  if (
3213
3239
  nextItem &&
3214
3240
  deps.hasPendingInboundQueueMutationForItem?.(nextItem)
@@ -3224,17 +3250,49 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
3224
3250
  deps.updateStatus(ctx);
3225
3251
  return;
3226
3252
  }
3253
+ const dispatchBasisItems = deps.getQueuedItems();
3227
3254
  const dispatchPlan = planNextTelegramQueueAction(
3228
3255
  dispatchableItems,
3229
3256
  canDispatch,
3230
3257
  );
3231
- if (dispatchPlan.kind !== "none") {
3258
+ const commitDispatchPlan = (): boolean => {
3259
+ if (dispatchPlan.kind === "none") return true;
3260
+ const currentItems = deps.getQueuedItems();
3261
+ if (dispatchPlan.kind === "prompt") {
3262
+ const queueDrifted =
3263
+ currentItems.length !== dispatchBasisItems.length ||
3264
+ currentItems.some((item, index) => item !== dispatchBasisItems[index]);
3265
+ const dispatchEligibilityDrifted =
3266
+ !deps.canDispatch(ctx) ||
3267
+ deps.hasPendingInboundQueueMutationForItem?.(dispatchPlan.item) === true ||
3268
+ (deps.isQueueItemAdmissionReady?.(dispatchPlan.item) === false) ||
3269
+ (deps.isQueueItemTransportActive?.(dispatchPlan.item) === false);
3270
+ if (queueDrifted || dispatchEligibilityDrifted) {
3271
+ const selectedItemRetained = currentItems.includes(dispatchPlan.item);
3272
+ const selectedTransportActive =
3273
+ deps.isQueueItemTransportActive?.(dispatchPlan.item) !== false;
3274
+ nextDispatchAnnouncementRequested =
3275
+ selectedItemRetained && selectedTransportActive;
3276
+ nextDispatchAnnouncementAnchor = nextDispatchAnnouncementRequested
3277
+ ? dispatchPlan.item
3278
+ : undefined;
3279
+ deps.updateStatus(ctx);
3280
+ if (nextDispatchAnnouncementRequested && queueDrifted && !dispatchEligibilityDrifted) {
3281
+ controller.dispatchNext(ctx);
3282
+ }
3283
+ return false;
3284
+ }
3285
+ }
3232
3286
  deps.setQueuedItems([
3233
3287
  ...dispatchPlan.remainingItems,
3234
3288
  ...protectedInactiveItems,
3235
3289
  ]);
3236
- }
3237
- executeTelegramQueueDispatchPlan(dispatchPlan, {
3290
+ nextDispatchAnnouncementAnchor = undefined;
3291
+ return true;
3292
+ };
3293
+ const executePlan = (): void => {
3294
+ if (!commitDispatchPlan()) return;
3295
+ executeTelegramQueueDispatchPlan(dispatchPlan, {
3238
3296
  executeControlItem: (item) => {
3239
3297
  controlDispatchPending = true;
3240
3298
  const dispatchGeneration = deps.getDispatchGeneration?.();
@@ -3280,6 +3338,32 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
3280
3338
  deps.updateStatus(ctx);
3281
3339
  },
3282
3340
  });
3341
+ };
3342
+ if (dispatchPlan.kind === "prompt" && nextDispatchAnnouncementRequested) {
3343
+ nextDispatchAnnouncementRequested = false;
3344
+ controlDispatchPending = true;
3345
+ const dispatchGeneration = deps.getDispatchGeneration?.();
3346
+ deps.updateStatus(ctx);
3347
+ void deps.sendTextReply(
3348
+ dispatchPlan.item.chatId,
3349
+ dispatchPlan.item.replyToMessageId,
3350
+ "<b>⏩ Dispatching next queued turn.</b>",
3351
+ { target: dispatchPlan.item.target },
3352
+ ).catch((error) => {
3353
+ deps.recordRuntimeEvent?.("dispatch", error, { phase: "next-announcement" });
3354
+ }).finally(() => {
3355
+ controlDispatchPending = false;
3356
+ if (deps.hasDispatchContext && !deps.hasDispatchContext()) return;
3357
+ if (
3358
+ dispatchGeneration !== undefined &&
3359
+ deps.isDispatchGenerationActive &&
3360
+ !deps.isDispatchGenerationActive(dispatchGeneration)
3361
+ ) return;
3362
+ executePlan();
3363
+ });
3364
+ return;
3365
+ }
3366
+ executePlan();
3283
3367
  },
3284
3368
  };
3285
3369
  return controller;
package/lib/replies.ts CHANGED
@@ -730,6 +730,24 @@ export async function sendTelegramNativeMarkdownReply<TReplyMarkup = unknown>(
730
730
  return lastMessageId;
731
731
  }
732
732
 
733
+ export async function sendTelegramNativeRichMessage(
734
+ chatId: number,
735
+ richMessage: TelegramInputRichMessage,
736
+ deps: {
737
+ recordOwnership?: TelegramReplyOwnershipRecorder["record"];
738
+ sendRichMessage: (body: TelegramSendRichMessageBody) => Promise<TelegramSentMessage>;
739
+ },
740
+ options?: TelegramReplyTargetOptions,
741
+ ): Promise<number> {
742
+ const sent = await deps.sendRichMessage({
743
+ chat_id: chatId,
744
+ rich_message: richMessage,
745
+ ...(options?.target ? getTelegramTargetThreadParams(options.target) : {}),
746
+ });
747
+ deps.recordOwnership?.({ chatId, messageId: sent.message_id, target: options?.target });
748
+ return sent.message_id;
749
+ }
750
+
733
751
  // UI/compat regular-message runtime for bridge-owned text and interactive
734
752
  // surfaces. Assistant and guest Markdown delivery bypass this path and use
735
753
  // native Rich Message helpers above.
@@ -775,6 +793,11 @@ export interface TelegramRenderedMessageRuntime<TReplyMarkup> {
775
793
  replyMarkup: TReplyMarkup,
776
794
  options?: TelegramReplyTargetOptions,
777
795
  ) => Promise<number | undefined>;
796
+ sendSectionRichMessage: (
797
+ chatId: number,
798
+ message: TelegramInputRichMessage,
799
+ options?: TelegramReplyTargetOptions,
800
+ ) => Promise<number>;
778
801
  }
779
802
 
780
803
  export interface TelegramRenderedMessageDeliveryRuntime<
@@ -891,6 +914,11 @@ export function createTelegramRenderedMessageRuntime<TReplyMarkup>(
891
914
  },
892
915
  );
893
916
  },
917
+ sendSectionRichMessage: (chatId, message, options) =>
918
+ sendTelegramNativeRichMessage(chatId, message, {
919
+ recordOwnership: deps.recordOwnership,
920
+ sendRichMessage: deps.sendRichMessage,
921
+ }, options),
894
922
  };
895
923
  }
896
924
 
package/lib/routing.ts CHANGED
@@ -19,6 +19,7 @@ import * as Queue from "./queue.ts";
19
19
  import * as Replies from "./replies.ts";
20
20
  import type { TelegramBridgeRuntime } from "./runtime.ts";
21
21
  import type { TelegramSectionRegistry } from "./sections.ts";
22
+ import type { TelegramInputRichMessage } from "./telegram-api.ts";
22
23
  import * as TextGroups from "./text-groups.ts";
23
24
  import * as ThreadNaming from "./thread-naming.ts";
24
25
  import * as ThreadReconciler from "./thread-reconciler.ts";
@@ -625,6 +626,7 @@ export interface TelegramInboundRouteRuntimeDeps<
625
626
  updateStatus: (ctx: TContext, error?: string) => void;
626
627
  isContextActive?: (ctx: TContext) => boolean;
627
628
  dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
629
+ requestNextDispatchAnnouncement?: () => void;
628
630
  requestDeferredDispatchNextQueuedTelegramTurn?: (
629
631
  dispatch: (ctx: TContext) => void,
630
632
  ) => void;
@@ -706,6 +708,11 @@ export interface TelegramInboundRouteRuntimeDeps<
706
708
  details?: Record<string, unknown>,
707
709
  ) => void;
708
710
  sectionRegistry?: TelegramSectionRegistry;
711
+ sendSectionRichMessage?: (
712
+ chatId: number,
713
+ message: TelegramInputRichMessage,
714
+ options?: { target?: { chatId: number; threadId?: number } },
715
+ ) => Promise<number | undefined>;
709
716
  }
710
717
 
711
718
  const TELEGRAM_OWNED_CALLBACK_PREFIXES = [
@@ -1013,6 +1020,7 @@ export function createTelegramInboundRouteRuntime<
1013
1020
  sectionRegistry: deps.sectionRegistry,
1014
1021
  editInteractiveMessage: deps.editInteractiveMessage,
1015
1022
  sendInteractiveMessage: deps.sendInteractiveMessage,
1023
+ sendSectionRichMessage: deps.sendSectionRichMessage,
1016
1024
  deleteMessage: deps.deleteMessage,
1017
1025
  enqueueSectionPrompt: async (
1018
1026
  prompt: string,
@@ -2171,6 +2179,7 @@ export function createTelegramInboundRouteRuntime<
2171
2179
  updateStatus: deps.updateStatus,
2172
2180
  isContextActive: deps.isContextActive,
2173
2181
  dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
2182
+ requestNextDispatchAnnouncement: deps.requestNextDispatchAnnouncement,
2174
2183
  requestDeferredDispatchNextQueuedTelegramTurn:
2175
2184
  deps.requestDeferredDispatchNextQueuedTelegramTurn,
2176
2185
  startTypingLoop: deps.startTypingLoop,
package/lib/sections.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  assertTelegramCallbackData,
9
9
  type TelegramInlineKeyboardMarkup,
10
10
  } from "./keyboard.ts";
11
+ import type { TelegramInputRichMessage } from "./telegram-api.ts";
11
12
 
12
13
  const SECTION_REGISTRY_KEY = "__piTelegramSectionRegistry__";
13
14
 
@@ -66,6 +67,7 @@ export interface TelegramSectionContext {
66
67
  answerCallback(text?: string): Promise<void>;
67
68
  edit(view: TelegramSectionView): Promise<void>;
68
69
  open(view: TelegramSectionView): Promise<void>;
70
+ openRich(message: TelegramInputRichMessage): Promise<void>;
69
71
  enqueuePrompt(prompt: string): Promise<void>;
70
72
  callbackData(action: string, payload?: string): string;
71
73
  /** Delete the message that triggered this callback (dialog cleanup) */
@@ -81,6 +83,7 @@ export interface TelegramSectionCallbackContext {
81
83
  answerCallback(text?: string): Promise<void>;
82
84
  edit(view: TelegramSectionView): Promise<void>;
83
85
  open(view: TelegramSectionView): Promise<void>;
86
+ openRich(message: TelegramInputRichMessage): Promise<void>;
84
87
  enqueuePrompt(prompt: string): Promise<void>;
85
88
  callbackData(action: string, payload?: string): string;
86
89
  /** Delete the message that triggered this callback (dialog cleanup) */
@@ -160,6 +163,11 @@ export interface TelegramSectionRuntimeDeps {
160
163
  replyMarkup: TelegramInlineKeyboardMarkup,
161
164
  options?: { target?: TelegramSectionTarget },
162
165
  ) => Promise<number | undefined>;
166
+ sendRichMessage: (
167
+ chatId: number,
168
+ message: TelegramInputRichMessage,
169
+ options?: { target?: TelegramSectionTarget },
170
+ ) => Promise<number | undefined>;
163
171
  enqueuePrompt: (prompt: string) => Promise<void>;
164
172
  deleteMessage: (chatId: number, messageId: number) => Promise<void>;
165
173
  }
@@ -202,6 +210,12 @@ function buildTelegramSectionContext(
202
210
  deps.target ? { target: deps.target } : undefined,
203
211
  )
204
212
  .then(() => {}),
213
+ openRich: (message) =>
214
+ deps.sendRichMessage(
215
+ chatId,
216
+ message,
217
+ deps.target ? { target: deps.target } : undefined,
218
+ ).then(() => {}),
205
219
  enqueuePrompt: deps.enqueuePrompt,
206
220
  callbackData: (action, payload) =>
207
221
  buildTelegramSectionCallbackData(token, action, payload),
@@ -251,6 +265,12 @@ function buildTelegramSectionCallbackContext(
251
265
  deps.target ? { target: deps.target } : undefined,
252
266
  )
253
267
  .then(() => {}),
268
+ openRich: (message) =>
269
+ deps.sendRichMessage(
270
+ chatId,
271
+ message,
272
+ deps.target ? { target: deps.target } : undefined,
273
+ ).then(() => {}),
254
274
  enqueuePrompt: deps.enqueuePrompt,
255
275
  callbackData: (action, payload) =>
256
276
  buildTelegramSectionCallbackData(token, action, payload),
@@ -526,6 +546,11 @@ export interface TelegramSectionCallbackHandlerDeps {
526
546
  replyMarkup: TelegramInlineKeyboardMarkup,
527
547
  options?: { target?: TelegramSectionTarget },
528
548
  ) => Promise<number | undefined>;
549
+ sendRichMessage: (
550
+ chatId: number,
551
+ message: TelegramInputRichMessage,
552
+ options?: { target?: TelegramSectionTarget },
553
+ ) => Promise<number | undefined>;
529
554
  enqueuePrompt: (prompt: string) => Promise<void>;
530
555
  deleteMessage: (chatId: number, messageId: number) => Promise<void>;
531
556
  }
@@ -292,6 +292,7 @@ export type TelegramRichText =
292
292
  | { type: "bold" | "code"; text: TelegramRichText };
293
293
 
294
294
  export type TelegramInputRichBlock =
295
+ | { type: "heading"; text: TelegramRichText; size?: 1 | 2 | 3 }
295
296
  | { type: "pre"; text: TelegramRichText; language?: string }
296
297
  | {
297
298
  type: "details";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.45.8",
3
+ "version": "0.45.10",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"