@llblab/pi-telegram 0.51.0 → 0.51.2

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 (46) hide show
  1. package/AGENTS.md +4 -3
  2. package/BACKLOG.md +3 -8
  3. package/CHANGELOG.md +11 -0
  4. package/README.md +1 -1
  5. package/dist/lib/bindings.d.ts +2 -0
  6. package/dist/lib/bindings.js +3 -4
  7. package/dist/lib/bus-api.d.ts +0 -1
  8. package/dist/lib/bus-api.js +0 -3
  9. package/dist/lib/bus-follower.d.ts +3 -1
  10. package/dist/lib/bus-follower.js +6 -1
  11. package/dist/lib/commands.d.ts +5 -0
  12. package/dist/lib/commands.js +9 -0
  13. package/dist/lib/extension.js +6 -3
  14. package/dist/lib/menu-model.d.ts +3 -3
  15. package/dist/lib/menu-model.js +17 -3
  16. package/dist/lib/menu.d.ts +5 -7
  17. package/dist/lib/menu.js +4 -5
  18. package/dist/lib/model.d.ts +12 -12
  19. package/dist/lib/model.js +49 -35
  20. package/dist/lib/queue.d.ts +8 -2
  21. package/dist/lib/queue.js +64 -19
  22. package/dist/lib/replies.d.ts +4 -0
  23. package/dist/lib/replies.js +14 -0
  24. package/dist/lib/routing.d.ts +1 -0
  25. package/dist/lib/routing.js +5 -1
  26. package/dist/lib/runtime.d.ts +3 -6
  27. package/dist/lib/runtime.js +4 -55
  28. package/dist/lib/telegram-api.js +36 -10
  29. package/dist/package.json +1 -1
  30. package/docs/architecture.md +9 -9
  31. package/docs/multi-instance-bus.md +3 -3
  32. package/docs/public-api.md +1 -1
  33. package/lib/bindings.ts +14 -4
  34. package/lib/bus-api.ts +0 -7
  35. package/lib/bus-follower.ts +13 -2
  36. package/lib/commands.ts +19 -0
  37. package/lib/extension.ts +10 -3
  38. package/lib/menu-model.ts +25 -4
  39. package/lib/menu.ts +14 -9
  40. package/lib/model.ts +77 -44
  41. package/lib/queue.ts +76 -24
  42. package/lib/replies.ts +18 -0
  43. package/lib/routing.ts +7 -1
  44. package/lib/runtime.ts +7 -58
  45. package/lib/telegram-api.ts +43 -17
  46. package/package.json +1 -1
package/dist/lib/model.js CHANGED
@@ -261,12 +261,12 @@ export function createPendingModelSwitchStore() {
261
261
  has: () => selection !== undefined,
262
262
  };
263
263
  }
264
- export function canRestartTelegramTurnForModelSwitch(state) {
265
- return !state.isIdle && state.hasActiveTelegramTurn && state.hasAbortHandler;
264
+ export function canRestartAgentRunForTelegramModelSwitch(state) {
265
+ return !state.isIdle && state.hasAbortHandler;
266
266
  }
267
267
  export function shouldTriggerPendingTelegramModelSwitchAbort(state) {
268
268
  return (state.hasPendingModelSwitch &&
269
- state.hasActiveTelegramTurn &&
269
+ state.hasContinuationTurn &&
270
270
  state.hasAbortHandler &&
271
271
  state.activeToolExecutions === 0);
272
272
  }
@@ -297,7 +297,7 @@ export function buildTelegramModelSwitchContinuationText(telegramPrefix, model,
297
297
  const thinkingSuffix = thinkingLevel
298
298
  ? ` Keep the selected thinking level (${thinkingLevel}) if it still applies.`
299
299
  : "";
300
- return `${telegramPrefix} Continue the interrupted previous Telegram request using the newly selected model (${modelLabel}). Resume from the last unfinished step instead of restarting from scratch unless necessary.${thinkingSuffix}`;
300
+ return `${telegramPrefix} Continue the interrupted previous request using the newly selected model (${modelLabel}). Resume from the last unfinished step instead of restarting from scratch unless necessary.${thinkingSuffix}`;
301
301
  }
302
302
  export function buildTelegramModelSwitchContinuationTurn(options) {
303
303
  const modelLabel = `${options.selection.model.provider}/${options.selection.model.id}`;
@@ -318,7 +318,7 @@ export function buildTelegramModelSwitchContinuationTurn(options) {
318
318
  text: buildTelegramModelSwitchContinuationText(options.telegramPrefix ?? TELEGRAM_PREFIX, options.selection.model, options.selection.thinkingLevel),
319
319
  },
320
320
  ],
321
- historyText: `Continue interrupted Telegram request on ${modelLabel}`,
321
+ historyText: `Continue interrupted request on ${modelLabel}`,
322
322
  statusSummary: `↻ ${statusLabel || "continue"}`,
323
323
  };
324
324
  }
@@ -364,46 +364,60 @@ export function createTelegramModelSwitchControllerRuntime(deps) {
364
364
  });
365
365
  }
366
366
  export function createTelegramModelSwitchController(deps) {
367
+ let pendingContinuationTurn;
368
+ const triggerPendingAbort = (ctx) => {
369
+ const turn = pendingContinuationTurn ?? deps.getActiveTurn();
370
+ if (!shouldTriggerPendingTelegramModelSwitchAbort({
371
+ hasPendingModelSwitch: !!deps.getPendingModelSwitch(),
372
+ hasContinuationTurn: !!turn,
373
+ hasAbortHandler: deps.hasAbortHandler(),
374
+ activeToolExecutions: deps.getActiveToolExecutions(),
375
+ })) {
376
+ return false;
377
+ }
378
+ const selection = deps.getPendingModelSwitch();
379
+ const abort = deps.getAbortHandler();
380
+ if (!selection || !turn || !abort)
381
+ return false;
382
+ pendingContinuationTurn = undefined;
383
+ deps.setPendingModelSwitch(undefined);
384
+ deps.queueContinuation(turn, selection, ctx);
385
+ abort();
386
+ return true;
387
+ };
367
388
  return {
368
- canOfferInFlightSwitch: (ctx) => canRestartTelegramTurnForModelSwitch({
389
+ canOfferInFlightSwitch: (ctx) => canRestartAgentRunForTelegramModelSwitch({
369
390
  isIdle: deps.isIdle(ctx),
370
- hasActiveTelegramTurn: !!deps.getActiveTurn(),
371
391
  hasAbortHandler: deps.hasAbortHandler(),
372
392
  }),
373
- stagePendingSwitch: (selection, ctx) => {
393
+ stagePendingSwitch: (selection, ctx, continuationTurn) => {
394
+ pendingContinuationTurn = deps.getActiveTurn() ?? continuationTurn;
374
395
  deps.setPendingModelSwitch(selection);
375
- deps.updateStatus(ctx);
396
+ try {
397
+ deps.updateStatus(ctx);
398
+ }
399
+ finally {
400
+ triggerPendingAbort(ctx);
401
+ }
376
402
  },
377
403
  clearPendingSwitch: () => {
404
+ pendingContinuationTurn = undefined;
378
405
  deps.setPendingModelSwitch(undefined);
379
406
  },
380
407
  queueContinuation: deps.queueContinuation,
381
- triggerPendingAbort: (ctx) => {
382
- if (!shouldTriggerPendingTelegramModelSwitchAbort({
383
- hasPendingModelSwitch: !!deps.getPendingModelSwitch(),
384
- hasActiveTelegramTurn: !!deps.getActiveTurn(),
385
- hasAbortHandler: deps.hasAbortHandler(),
386
- activeToolExecutions: deps.getActiveToolExecutions(),
387
- })) {
388
- return false;
389
- }
390
- const selection = deps.getPendingModelSwitch();
391
- const turn = deps.getActiveTurn();
392
- const abort = deps.getAbortHandler();
393
- if (!selection || !turn || !abort)
394
- return false;
395
- deps.setPendingModelSwitch(undefined);
396
- deps.queueContinuation(turn, selection, ctx);
397
- abort();
398
- return true;
408
+ triggerPendingAbort,
409
+ restartInterruptedTurn: (selection, ctx, continuationTurn) => {
410
+ const restarted = restartTelegramModelSwitchContinuation({
411
+ activeTurn: deps.getActiveTurn() ?? continuationTurn,
412
+ abort: deps.getAbortHandler(),
413
+ selection,
414
+ queueContinuation: (turn, nextSelection) => {
415
+ deps.queueContinuation(turn, nextSelection, ctx);
416
+ },
417
+ });
418
+ if (restarted)
419
+ pendingContinuationTurn = undefined;
420
+ return restarted;
399
421
  },
400
- restartInterruptedTurn: (selection, ctx) => restartTelegramModelSwitchContinuation({
401
- activeTurn: deps.getActiveTurn(),
402
- abort: deps.getAbortHandler(),
403
- selection,
404
- queueContinuation: (turn, nextSelection) => {
405
- deps.queueContinuation(turn, nextSelection, ctx);
406
- },
407
- }),
408
422
  };
409
423
  }
@@ -84,6 +84,8 @@ export interface PendingTelegramTurn extends TelegramQueueItemBase {
84
84
  historyText: string;
85
85
  priorityEmoji?: string;
86
86
  reactionSuppressionEmoji?: string;
87
+ /** Emit the explicit aborted-turn notice when /next settles this active turn. */
88
+ announceNextAbortOnEnd?: boolean;
87
89
  /** Turn should preferably be delivered as voice (mirror mode + user sent voice) */
88
90
  voiceReplyPreferred?: boolean;
89
91
  /** Turn must be delivered as voice (voice mode) */
@@ -165,6 +167,8 @@ export interface TelegramActiveTurnStore<TTurn extends PendingTelegramTurn = Pen
165
167
  has: () => boolean;
166
168
  set: (turn: TTurn) => void;
167
169
  clear: () => void;
170
+ markNextAbortAnnouncement: () => boolean;
171
+ clearNextAbortAnnouncement: () => boolean;
168
172
  getChatId: () => number | undefined;
169
173
  getTarget: () => TelegramQueueTarget | undefined;
170
174
  getReplyToMessageId: () => number | undefined;
@@ -349,7 +353,6 @@ export interface TelegramAgentStartHookRuntimeDeps<TTurn extends PendingTelegram
349
353
  }
350
354
  export type TelegramAgentStartHookEvent = unknown;
351
355
  export interface TelegramToolExecutionRuntimeDeps {
352
- hasActiveTurn: () => boolean;
353
356
  getActiveToolExecutions: () => number;
354
357
  setActiveToolExecutions: (count: number) => void;
355
358
  }
@@ -368,7 +371,6 @@ export declare function buildTelegramAgentStartPlan<TContext = unknown>(options:
368
371
  export declare function handleTelegramAgentStartRuntime<TTurn extends PendingTelegramTurn, TContext = unknown>(deps: TelegramAgentStartRuntimeDeps<TTurn, TContext>): void;
369
372
  export declare function createTelegramAgentStartHook<TTurn extends PendingTelegramTurn, TContext = unknown>(deps: TelegramAgentStartHookRuntimeDeps<TTurn, TContext>): (_event: TelegramAgentStartHookEvent, ctx: TContext) => Promise<void>;
370
373
  export declare function getNextTelegramToolExecutionCount(options: {
371
- hasActiveTurn: boolean;
372
374
  currentCount: number;
373
375
  event: "start" | "end";
374
376
  }): number;
@@ -391,6 +393,7 @@ export interface TelegramAgentEndPlan {
391
393
  kind: "no-turn" | "aborted" | "error" | "text" | "attachments-only" | "empty";
392
394
  shouldClearPreview: boolean;
393
395
  shouldDispatchNext: boolean;
396
+ shouldSendAbortMessage: boolean;
394
397
  shouldSendErrorMessage: boolean;
395
398
  shouldSendAttachmentNotice: boolean;
396
399
  }
@@ -517,6 +520,7 @@ export declare function buildTelegramAgentEndPlan(options: {
517
520
  hasFinalText: boolean;
518
521
  hasQueuedAttachments: boolean;
519
522
  foldQueuedPromptsIntoHistory: boolean;
523
+ announceNextAbortOnEnd?: boolean;
520
524
  }): TelegramAgentEndPlan;
521
525
  export declare function createTelegramAgentEndHook<TTurn extends PendingTelegramTurn, TContext, TMessage, TReplyMarkup = unknown>(deps: TelegramAgentEndHookRuntimeDeps<TTurn, TContext, TMessage, TReplyMarkup>): (event: TelegramAgentEndHookEvent<TMessage>, ctx: TContext, assistantOverride?: TelegramAgentEndAssistantResult) => Promise<void>;
522
526
  export declare function handleTelegramAgentEndRuntime<TTurn extends PendingTelegramTurn, TReplyMarkup = unknown>(deps: TelegramAgentEndRuntimeDeps<TTurn, TReplyMarkup>): Promise<void>;
@@ -759,6 +763,7 @@ export interface TelegramQueueDispatchControllerDeps<TContext = unknown> extends
759
763
  }>["item"], ctx: TContext) => boolean;
760
764
  sendUserMessage: TelegramDispatchRuntimeDeps<TContext>["sendUserMessage"];
761
765
  onPromptDispatchFailure: (ctx: TContext, message: string) => void;
766
+ reconcileNextDispatchAnnouncementReplyOwnership?: (item: PendingTelegramTurn) => void;
762
767
  isQueueItemTransportActive?: (item: TelegramQueueItem<TContext>) => boolean;
763
768
  hasPendingInboundQueueMutationForItem?: (item: TelegramQueueItem<TContext>) => boolean;
764
769
  isQueueItemAdmissionReady?: (item: TelegramQueueItem<TContext>) => boolean;
@@ -768,6 +773,7 @@ export interface TelegramQueueDispatchControllerDeps<TContext = unknown> extends
768
773
  export interface TelegramQueueDispatchController<TContext = unknown> {
769
774
  dispatchNext: (ctx: TContext) => void;
770
775
  requestNextDispatchAnnouncement: () => void;
776
+ cancelNextDispatchAnnouncement: () => void;
771
777
  }
772
778
  export declare function executeTelegramQueueDispatchPlan<TContext = unknown>(plan: TelegramQueueDispatchAction<TContext>, deps: TelegramDispatchRuntimeDeps<TContext>): void;
773
779
  export type TelegramQueueDispatchRuntimeDeps<TContext = unknown> = Omit<TelegramQueueDispatchControllerDeps<TContext>, "canDispatch"> & TelegramDispatchReadinessDeps<TContext>;
package/dist/lib/queue.js CHANGED
@@ -168,6 +168,18 @@ export function createTelegramActiveTurnStore() {
168
168
  clear: () => {
169
169
  activeTurn = undefined;
170
170
  },
171
+ markNextAbortAnnouncement: () => {
172
+ if (!activeTurn)
173
+ return false;
174
+ activeTurn.announceNextAbortOnEnd = true;
175
+ return true;
176
+ },
177
+ clearNextAbortAnnouncement: () => {
178
+ if (!activeTurn?.announceNextAbortOnEnd)
179
+ return false;
180
+ delete activeTurn.announceNextAbortOnEnd;
181
+ return true;
182
+ },
171
183
  getChatId: () => activeTurn?.chatId,
172
184
  getTarget: () => activeTurn?.target ? { ...activeTurn.target } : undefined,
173
185
  getReplyToMessageId: () => activeTurn?.replyToMessageId,
@@ -729,8 +741,6 @@ export function createTelegramAgentStartHook(deps) {
729
741
  };
730
742
  }
731
743
  export function getNextTelegramToolExecutionCount(options) {
732
- if (!options.hasActiveTurn)
733
- return options.currentCount;
734
744
  if (options.event === "start") {
735
745
  return options.currentCount + 1;
736
746
  }
@@ -738,20 +748,16 @@ export function getNextTelegramToolExecutionCount(options) {
738
748
  }
739
749
  export function handleTelegramToolExecutionStartRuntime(deps) {
740
750
  deps.setActiveToolExecutions(getNextTelegramToolExecutionCount({
741
- hasActiveTurn: deps.hasActiveTurn(),
742
751
  currentCount: deps.getActiveToolExecutions(),
743
752
  event: "start",
744
753
  }));
745
754
  }
746
755
  export function handleTelegramToolExecutionEndRuntime(deps) {
747
- const hasActiveTurn = deps.hasActiveTurn();
748
756
  deps.setActiveToolExecutions(getNextTelegramToolExecutionCount({
749
- hasActiveTurn,
750
757
  currentCount: deps.getActiveToolExecutions(),
751
758
  event: "end",
752
759
  }));
753
- if (hasActiveTurn)
754
- deps.triggerPendingModelSwitchAbort();
760
+ deps.triggerPendingModelSwitchAbort();
755
761
  }
756
762
  export function createTelegramAgentLifecycleHooks(deps) {
757
763
  const onAgentStart = createTelegramAgentStartHook(deps);
@@ -794,7 +800,6 @@ export function createTelegramToolExecutionHooks(deps) {
794
800
  },
795
801
  onToolExecutionEnd: (_event, ctx) => {
796
802
  handleTelegramToolExecutionEndRuntime({
797
- hasActiveTurn: deps.hasActiveTurn,
798
803
  getActiveToolExecutions: deps.getActiveToolExecutions,
799
804
  setActiveToolExecutions: deps.setActiveToolExecutions,
800
805
  triggerPendingModelSwitchAbort: () => {
@@ -815,6 +820,7 @@ export function buildTelegramAgentEndPlan(options) {
815
820
  kind: "no-turn",
816
821
  shouldClearPreview: false,
817
822
  shouldDispatchNext,
823
+ shouldSendAbortMessage: false,
818
824
  shouldSendErrorMessage: false,
819
825
  shouldSendAttachmentNotice: false,
820
826
  };
@@ -824,6 +830,7 @@ export function buildTelegramAgentEndPlan(options) {
824
830
  kind: "aborted",
825
831
  shouldClearPreview: true,
826
832
  shouldDispatchNext,
833
+ shouldSendAbortMessage: options.announceNextAbortOnEnd === true,
827
834
  shouldSendErrorMessage: false,
828
835
  shouldSendAttachmentNotice: false,
829
836
  };
@@ -833,6 +840,7 @@ export function buildTelegramAgentEndPlan(options) {
833
840
  kind: "error",
834
841
  shouldClearPreview: true,
835
842
  shouldDispatchNext,
843
+ shouldSendAbortMessage: false,
836
844
  shouldSendErrorMessage: true,
837
845
  shouldSendAttachmentNotice: false,
838
846
  };
@@ -842,6 +850,7 @@ export function buildTelegramAgentEndPlan(options) {
842
850
  kind: "text",
843
851
  shouldClearPreview: false,
844
852
  shouldDispatchNext,
853
+ shouldSendAbortMessage: false,
845
854
  shouldSendErrorMessage: false,
846
855
  shouldSendAttachmentNotice: false,
847
856
  };
@@ -851,6 +860,7 @@ export function buildTelegramAgentEndPlan(options) {
851
860
  kind: "attachments-only",
852
861
  shouldClearPreview: true,
853
862
  shouldDispatchNext,
863
+ shouldSendAbortMessage: false,
854
864
  shouldSendErrorMessage: false,
855
865
  shouldSendAttachmentNotice: true,
856
866
  };
@@ -859,6 +869,7 @@ export function buildTelegramAgentEndPlan(options) {
859
869
  kind: "empty",
860
870
  shouldClearPreview: true,
861
871
  shouldDispatchNext,
872
+ shouldSendAbortMessage: false,
862
873
  shouldSendErrorMessage: false,
863
874
  shouldSendAttachmentNotice: false,
864
875
  };
@@ -991,6 +1002,7 @@ export async function handleTelegramAgentEndRuntime(deps) {
991
1002
  hasFinalText: !!finalText || hasOutboundArtifacts,
992
1003
  hasQueuedAttachments: (turn?.queuedAttachments.length ?? 0) > 0,
993
1004
  foldQueuedPromptsIntoHistory: deps.foldQueuedPromptsIntoHistory,
1005
+ announceNextAbortOnEnd: turn?.announceNextAbortOnEnd === true,
994
1006
  });
995
1007
  if (!turn) {
996
1008
  if (endPlan.shouldDispatchNext)
@@ -1116,17 +1128,26 @@ export async function handleTelegramAgentEndRuntime(deps) {
1116
1128
  if (!isDeliveryActive())
1117
1129
  return;
1118
1130
  }
1119
- if (endPlan.shouldSendErrorMessage) {
1131
+ if (endPlan.shouldSendAbortMessage || endPlan.shouldSendErrorMessage) {
1120
1132
  const errorMessage = assistant.errorMessage ||
1121
1133
  "Telegram bridge: Pi failed while processing the request.";
1122
- const isOperationAborted = errorMessage.trim().replace(/\.$/, "") ===
1123
- "This operation was aborted";
1124
- await deps.sendTextReply(turn.chatId, turn.replyToMessageId, isOperationAborted
1125
- ? "<b>⏹️ This operation was aborted.</b>"
1126
- : errorMessage, {
1127
- target: turn.target,
1128
- ...(isOperationAborted ? { parseMode: "HTML" } : {}),
1129
- });
1134
+ const isOperationAborted = endPlan.shouldSendAbortMessage ||
1135
+ errorMessage.trim().replace(/\.$/, "") === "This operation was aborted";
1136
+ try {
1137
+ await deps.sendTextReply(turn.chatId, turn.replyToMessageId, isOperationAborted
1138
+ ? "<b>⏹️ This operation was aborted.</b>"
1139
+ : errorMessage, {
1140
+ target: turn.target,
1141
+ ...(isOperationAborted ? { parseMode: "HTML" } : {}),
1142
+ });
1143
+ }
1144
+ catch (error) {
1145
+ if (!isOperationAborted)
1146
+ throw error;
1147
+ deps.recordRuntimeEvent?.("dispatch", error, {
1148
+ phase: "next-abort-announcement",
1149
+ });
1150
+ }
1130
1151
  if (!isDeliveryActive())
1131
1152
  return;
1132
1153
  if (endPlan.shouldDispatchNext)
@@ -1267,7 +1288,7 @@ export async function handleTelegramAgentEndRuntime(deps) {
1267
1288
  deps.dispatchNextQueuedTelegramTurn();
1268
1289
  };
1269
1290
  if (deps.scheduleActiveTurnDelivery &&
1270
- (endPlan.kind === "text" || endPlan.kind === "attachments-only" || endPlan.shouldSendErrorMessage || endPlan.shouldClearPreview)) {
1291
+ (endPlan.kind === "text" || endPlan.kind === "attachments-only" || endPlan.shouldSendAbortMessage || endPlan.shouldSendErrorMessage || endPlan.shouldClearPreview)) {
1271
1292
  deps.scheduleActiveTurnDelivery(deliverActiveTurn);
1272
1293
  return;
1273
1294
  }
@@ -1765,6 +1786,7 @@ export function createTelegramQueueDispatchRuntime(deps) {
1765
1786
  commitPromptDispatch: deps.commitPromptDispatch,
1766
1787
  sendUserMessage: deps.sendUserMessage,
1767
1788
  onPromptDispatchFailure: deps.onPromptDispatchFailure,
1789
+ reconcileNextDispatchAnnouncementReplyOwnership: deps.reconcileNextDispatchAnnouncementReplyOwnership,
1768
1790
  isQueueItemTransportActive: deps.isQueueItemTransportActive,
1769
1791
  hasPendingInboundQueueMutationForItem: deps.hasPendingInboundQueueMutationForItem,
1770
1792
  isQueueItemAdmissionReady: deps.isQueueItemAdmissionReady,
@@ -1776,12 +1798,19 @@ export function createTelegramQueueDispatchRuntime(deps) {
1776
1798
  export function createTelegramQueueDispatchController(deps) {
1777
1799
  let controlDispatchPending = false;
1778
1800
  let nextDispatchAnnouncementRequested = false;
1801
+ let nextDispatchAnnouncementGeneration = 0;
1779
1802
  let nextDispatchAnnouncementAnchor;
1780
1803
  const controller = {
1781
1804
  requestNextDispatchAnnouncement: () => {
1805
+ nextDispatchAnnouncementGeneration += 1;
1782
1806
  nextDispatchAnnouncementRequested = true;
1783
1807
  nextDispatchAnnouncementAnchor = undefined;
1784
1808
  },
1809
+ cancelNextDispatchAnnouncement: () => {
1810
+ nextDispatchAnnouncementGeneration += 1;
1811
+ nextDispatchAnnouncementRequested = false;
1812
+ nextDispatchAnnouncementAnchor = undefined;
1813
+ },
1785
1814
  dispatchNext: (ctx) => {
1786
1815
  if (deps.hasDispatchContext && !deps.hasDispatchContext())
1787
1816
  return;
@@ -1972,11 +2001,22 @@ export function createTelegramQueueDispatchController(deps) {
1972
2001
  if (dispatchPlan.kind === "prompt" && nextDispatchAnnouncementRequested) {
1973
2002
  nextDispatchAnnouncementRequested = false;
1974
2003
  controlDispatchPending = true;
2004
+ const announcementGeneration = nextDispatchAnnouncementGeneration;
1975
2005
  const dispatchGeneration = deps.getDispatchGeneration?.();
1976
2006
  deps.updateStatus(ctx);
1977
2007
  void deps.sendTextReply(dispatchPlan.item.chatId, dispatchPlan.item.replyToMessageId, "<b>⏩ Dispatching next queued turn.</b>", { target: dispatchPlan.item.target }).catch((error) => {
1978
- deps.recordRuntimeEvent?.("dispatch", error, { phase: "next-announcement" });
2008
+ deps.recordRuntimeEvent?.("dispatch", error, {
2009
+ phase: "next-announcement",
2010
+ });
1979
2011
  }).finally(() => {
2012
+ try {
2013
+ deps.reconcileNextDispatchAnnouncementReplyOwnership?.(dispatchPlan.item);
2014
+ }
2015
+ catch (error) {
2016
+ deps.recordRuntimeEvent?.("dispatch", error, {
2017
+ phase: "next-announcement-reply-ownership",
2018
+ });
2019
+ }
1980
2020
  controlDispatchPending = false;
1981
2021
  if (deps.hasDispatchContext && !deps.hasDispatchContext())
1982
2022
  return;
@@ -1984,6 +2024,11 @@ export function createTelegramQueueDispatchController(deps) {
1984
2024
  deps.isDispatchGenerationActive &&
1985
2025
  !deps.isDispatchGenerationActive(dispatchGeneration))
1986
2026
  return;
2027
+ if (announcementGeneration !== nextDispatchAnnouncementGeneration) {
2028
+ if (nextDispatchAnnouncementRequested)
2029
+ controller.dispatchNext(ctx);
2030
+ return;
2031
+ }
1987
2032
  executePlan();
1988
2033
  });
1989
2034
  return;
@@ -23,6 +23,10 @@ export interface ReplyDedupRuntime {
23
23
  }
24
24
  export declare function createReplyDedupRuntime(): ReplyDedupRuntime;
25
25
  export declare function resetTransportReplyDedup(): void;
26
+ /** Keeps a successfully published transition notice as the first reply of the
27
+ * next agent turn. The following agent-start reset consumes this one-shot
28
+ * preservation, so later messages in that turn do not repeat the reply header. */
29
+ export declare function preserveTransportReplyDedupOnNextReset(chatId: number, messageId: number, target?: TelegramTarget): void;
26
30
  export declare function buildTelegramReplyParameters(chatId: number, messageId: number | undefined, target?: TelegramTarget): TelegramReplyParameters | undefined;
27
31
  export declare function withTelegramReplyParameters<T>(chatId: number, messageId: number | undefined, target: TelegramTarget | undefined, send: (parameters: TelegramReplyParameters | undefined) => Promise<T>): Promise<T>;
28
32
  export declare function isAssistantAgentMessage(message: unknown): boolean;
@@ -31,6 +31,7 @@ export function createReplyDedupRuntime() {
31
31
  }
32
32
  // --- Transport-level dedup ---
33
33
  const lastRepliedToMessageIdByTarget = new Map();
34
+ const replyDedupPreservedOnNextReset = new Map();
34
35
  let replyDedupGeneration = 0;
35
36
  function getReplyDedupTargetKey(chatId, target) {
36
37
  const threadId = target?.threadId;
@@ -41,6 +42,19 @@ function getReplyDedupTargetKey(chatId, target) {
41
42
  export function resetTransportReplyDedup() {
42
43
  replyDedupGeneration += 1;
43
44
  lastRepliedToMessageIdByTarget.clear();
45
+ for (const [key, messageId] of replyDedupPreservedOnNextReset) {
46
+ lastRepliedToMessageIdByTarget.set(key, messageId);
47
+ }
48
+ replyDedupPreservedOnNextReset.clear();
49
+ }
50
+ /** Keeps a successfully published transition notice as the first reply of the
51
+ * next agent turn. The following agent-start reset consumes this one-shot
52
+ * preservation, so later messages in that turn do not repeat the reply header. */
53
+ export function preserveTransportReplyDedupOnNextReset(chatId, messageId, target) {
54
+ const key = getReplyDedupTargetKey(chatId, target);
55
+ if (lastRepliedToMessageIdByTarget.get(key) !== messageId)
56
+ return;
57
+ replyDedupPreservedOnNextReset.set(key, messageId);
44
58
  }
45
59
  export function buildTelegramReplyParameters(chatId, messageId, target) {
46
60
  if (messageId === undefined || messageId <= 0)
@@ -122,6 +122,7 @@ export interface TelegramInboundRouteRuntimeDeps<TMessage extends TelegramRouted
122
122
  isContextActive?: (ctx: TContext) => boolean;
123
123
  dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
124
124
  requestNextDispatchAnnouncement?: () => void;
125
+ cancelNextDispatchAnnouncement?: () => void;
125
126
  requestDeferredDispatchNextQueuedTelegramTurn?: (dispatch: (ctx: TContext) => void) => void;
126
127
  hasDeferredDispatchContext?: () => boolean;
127
128
  startTypingLoop?: (ctx: TContext, chatId?: number, options?: {
@@ -535,7 +535,6 @@ export function createTelegramInboundRouteRuntime(deps) {
535
535
  updateSettingsMenuMessage: deps.updateSettingsMenuMessage,
536
536
  answerCallbackQuery: deps.answerCallbackQuery,
537
537
  isIdle: deps.isIdle,
538
- hasActiveTelegramTurn: deps.activeTurnRuntime.has,
539
538
  hasAbortHandler: deps.bridgeRuntime.abort.hasHandler,
540
539
  getActiveToolExecutions: deps.bridgeRuntime.lifecycle.getActiveToolExecutions,
541
540
  persistScopedModelPatterns: deps.persistScopedModelPatterns,
@@ -1418,6 +1417,10 @@ export function createTelegramInboundRouteRuntime(deps) {
1418
1417
  isContextActive: deps.isContextActive,
1419
1418
  dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
1420
1419
  requestNextDispatchAnnouncement: deps.requestNextDispatchAnnouncement,
1420
+ cancelNextTransitionAnnouncements: () => {
1421
+ deps.activeTurnRuntime.clearNextAbortAnnouncement();
1422
+ deps.cancelNextDispatchAnnouncement?.();
1423
+ },
1421
1424
  requestDeferredDispatchNextQueuedTelegramTurn: deps.requestDeferredDispatchNextQueuedTelegramTurn,
1422
1425
  startTypingLoop: deps.startTypingLoop,
1423
1426
  stopTypingLoop: deps.stopTypingLoop,
@@ -1476,6 +1479,7 @@ export function createTelegramInboundRouteRuntime(deps) {
1476
1479
  },
1477
1480
  getPromptTemplateCommands,
1478
1481
  sendTextReply: deps.sendTextReply,
1482
+ markActiveTurnNextAbortAnnouncement: deps.activeTurnRuntime.markNextAbortAnnouncement,
1479
1483
  getActiveTurnReply: () => {
1480
1484
  const activeTurn = deps.activeTurnRuntime.get();
1481
1485
  if (!activeTurn)
@@ -98,7 +98,6 @@ export interface TelegramTypingLoopDeps {
98
98
  sendTypingAction: (chatId: number, options?: {
99
99
  message_thread_id?: number;
100
100
  }) => Promise<unknown>;
101
- sendAggregateTypingAction?: (chatId: number) => Promise<unknown>;
102
101
  shouldContinue?: () => boolean;
103
102
  onStopped?: () => void;
104
103
  }
@@ -111,7 +110,6 @@ export interface TelegramTypingLoopStarterDeps<TContext> extends TelegramRuntime
111
110
  sendTypingAction: (chatId: number, options?: {
112
111
  message_thread_id?: number;
113
112
  }) => Promise<unknown>;
114
- sendAggregateTypingAction?: (chatId: number) => Promise<unknown>;
115
113
  updateStatus: (ctx: TContext, error?: string) => void;
116
114
  isContextActive?: (ctx: TContext) => boolean;
117
115
  isTransportAvailable?: () => boolean;
@@ -120,7 +118,7 @@ export interface TelegramTypingLoopStarterDeps<TContext> extends TelegramRuntime
120
118
  }
121
119
  export declare function createTelegramTypingLoopStarter<TContext>(deps: TelegramTypingLoopStarterDeps<TContext>): (ctx: TContext, chatId?: number, options?: {
122
120
  target?: TelegramTypingLoopTarget;
123
- }) => void;
121
+ }) => boolean;
124
122
  export declare function startTelegramTypingLoop(state: TelegramBridgeRuntimeState, deps: TelegramTypingLoopDeps): boolean;
125
123
  export declare function stopTelegramTypingLoop(state: TelegramBridgeRuntimeState): boolean;
126
124
  export declare function waitForTelegramTypingLoopIdle(state: TelegramBridgeRuntimeState, timeoutMs?: number): Promise<void>;
@@ -141,7 +139,7 @@ export interface TelegramPromptDispatchLifecycleDeps<TContext> extends TelegramR
141
139
  typing: Pick<TelegramRuntimeTypingPort, "stop">;
142
140
  startTypingLoop: (ctx: TContext, chatId?: number, options?: {
143
141
  target?: TelegramTypingLoopTarget;
144
- }) => void;
142
+ }) => boolean | void;
145
143
  updateStatus: (ctx: TContext, error?: string) => void;
146
144
  }
147
145
  export interface TelegramPromptDispatchRuntimeDeps<TContext> extends TelegramRuntimeEventRecorderPort {
@@ -151,7 +149,6 @@ export interface TelegramPromptDispatchRuntimeDeps<TContext> extends TelegramRun
151
149
  sendTypingAction: (chatId: number, options?: {
152
150
  message_thread_id?: number;
153
151
  }) => Promise<unknown>;
154
- sendAggregateTypingAction?: (chatId: number) => Promise<unknown>;
155
152
  updateStatus: (ctx: TContext, error?: string) => void;
156
153
  isContextActive?: (ctx: TContext) => boolean;
157
154
  isTransportAvailable?: () => boolean;
@@ -161,7 +158,7 @@ export interface TelegramPromptDispatchRuntimeDeps<TContext> extends TelegramRun
161
158
  export interface TelegramPromptDispatchRuntime<TContext> {
162
159
  startTypingLoop: (ctx: TContext, chatId?: number, options?: {
163
160
  target?: TelegramTypingLoopTarget;
164
- }) => void;
161
+ }) => boolean | void;
165
162
  onPromptDispatchStart: (ctx: TContext, chatId?: number) => void;
166
163
  onPromptDispatchFailure: (ctx: TContext, message: string) => void;
167
164
  }
@@ -3,7 +3,7 @@
3
3
  * Zones: pi agent runtime state, telegram session, shared coordination
4
4
  * Owns small session-local runtime primitives that are shared by orchestration but are not specific to queueing, rendering, polling, or Telegram transport
5
5
  */
6
- const TELEGRAM_TYPING_ACTION_INTERVAL_MS = 2500;
6
+ const TELEGRAM_TYPING_ACTION_INTERVAL_MS = 3_000;
7
7
  const TELEGRAM_TYPING_IDLE_DRAIN_MAX_MS = 250;
8
8
  export function createTelegramBridgeRuntimeState() {
9
9
  return {
@@ -177,9 +177,9 @@ export function createTelegramTypingLoopStarter(deps) {
177
177
  Object.is(deps.getTransportAuthority(), transportAuthority)
178
178
  : deps.isTransportAvailable?.() !== false;
179
179
  if (!hasTransport())
180
- return;
180
+ return false;
181
181
  let active = true;
182
- deps.typing.start({
182
+ return deps.typing.start({
183
183
  chatId: chatId ?? deps.getDefaultChatId(),
184
184
  target: options?.target,
185
185
  intervalMs: deps.intervalMs ?? TELEGRAM_TYPING_ACTION_INTERVAL_MS,
@@ -202,13 +202,6 @@ export function createTelegramTypingLoopStarter(deps) {
202
202
  deps.typing.stop();
203
203
  return;
204
204
  }
205
- const message = error instanceof Error ? error.message : String(error);
206
- updateTelegramRuntimeStatusSafely(deps.updateStatus, ctx, {
207
- error: message,
208
- category: "typing",
209
- phase: "status-update",
210
- recordRuntimeEvent: deps.recordRuntimeEvent,
211
- });
212
205
  try {
213
206
  deps.recordRuntimeEvent?.("typing", error, {
214
207
  chatId: targetChatId,
@@ -219,45 +212,6 @@ export function createTelegramTypingLoopStarter(deps) {
219
212
  }
220
213
  }
221
214
  },
222
- sendAggregateTypingAction: deps.sendAggregateTypingAction
223
- ? async (targetChatId) => {
224
- if (!active)
225
- return;
226
- if (!hasTransport()) {
227
- deps.typing.stop();
228
- return;
229
- }
230
- try {
231
- await deps.sendAggregateTypingAction?.(targetChatId);
232
- }
233
- catch (error) {
234
- if (deps.isContextActive?.(ctx) === false)
235
- return;
236
- if (!active)
237
- return;
238
- if (!hasTransport()) {
239
- deps.typing.stop();
240
- return;
241
- }
242
- const message = error instanceof Error ? error.message : String(error);
243
- updateTelegramRuntimeStatusSafely(deps.updateStatus, ctx, {
244
- error: message,
245
- category: "typing",
246
- phase: "status-update",
247
- recordRuntimeEvent: deps.recordRuntimeEvent,
248
- });
249
- try {
250
- deps.recordRuntimeEvent?.("typing", error, {
251
- chatId: targetChatId,
252
- aggregate: true,
253
- });
254
- }
255
- catch {
256
- // Typing diagnostics cannot escape the in-flight action owner.
257
- }
258
- }
259
- }
260
- : undefined,
261
215
  shouldContinue: hasTransport,
262
216
  onStopped: () => {
263
217
  active = false;
@@ -296,12 +250,7 @@ export function startTelegramTypingLoop(state, deps) {
296
250
  const targetChatId = activeDeps.chatId;
297
251
  const threadParams = getTelegramTypingLoopThreadParams(activeDeps.target);
298
252
  const typing = Promise.resolve()
299
- .then(async () => {
300
- await activeDeps.sendTypingAction(targetChatId, threadParams);
301
- if (threadParams?.message_thread_id !== undefined) {
302
- await activeDeps.sendAggregateTypingAction?.(targetChatId);
303
- }
304
- })
253
+ .then(() => activeDeps.sendTypingAction(targetChatId, threadParams))
305
254
  .then(() => undefined)
306
255
  .catch(() => undefined);
307
256
  state.typingInFlight = typing;