@llblab/pi-kit 0.21.0 → 0.21.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/CHANGELOG.md +12 -0
  2. package/README.md +3 -3
  3. package/node_modules/@llblab/pi-state-flow/AGENTS.md +2 -2
  4. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +6 -0
  5. package/node_modules/@llblab/pi-state-flow/README.md +2 -2
  6. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +17 -6
  7. package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +4 -0
  8. package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +29 -1
  9. package/node_modules/@llblab/pi-state-flow/dist/lib/logging.d.ts +3 -0
  10. package/node_modules/@llblab/pi-state-flow/dist/lib/logging.js +33 -7
  11. package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
  12. package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +1 -1
  13. package/node_modules/@llblab/pi-state-flow/docs/performance.md +10 -2
  14. package/node_modules/@llblab/pi-state-flow/docs/usage.md +4 -4
  15. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +15 -6
  16. package/node_modules/@llblab/pi-state-flow/lib/git.ts +24 -1
  17. package/node_modules/@llblab/pi-state-flow/lib/logging.ts +31 -6
  18. package/node_modules/@llblab/pi-state-flow/package.json +1 -1
  19. package/node_modules/@llblab/pi-telegram/AGENTS.md +2 -2
  20. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +5 -0
  21. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +2 -0
  22. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +2 -0
  23. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +3 -1
  24. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +6 -1
  25. package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +5 -0
  26. package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +9 -0
  27. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +5 -1
  28. package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +8 -0
  29. package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +63 -11
  30. package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +4 -0
  31. package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +14 -0
  32. package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +1 -0
  33. package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +5 -0
  34. package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
  35. package/node_modules/@llblab/pi-telegram/docs/architecture.md +1 -1
  36. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +1 -1
  37. package/node_modules/@llblab/pi-telegram/docs/public-api.md +1 -1
  38. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +7 -0
  39. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +13 -2
  40. package/node_modules/@llblab/pi-telegram/lib/commands.ts +19 -0
  41. package/node_modules/@llblab/pi-telegram/lib/extension.ts +9 -0
  42. package/node_modules/@llblab/pi-telegram/lib/queue.ts +75 -16
  43. package/node_modules/@llblab/pi-telegram/lib/replies.ts +18 -0
  44. package/node_modules/@llblab/pi-telegram/lib/routing.ts +7 -0
  45. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  46. package/package.json +3 -3
@@ -52,6 +52,7 @@ export interface TelegramQueueBindingRuntime<TContext> {
52
52
  mutation: Queue.TelegramQueueMutationController<TContext>;
53
53
  dispatchNext: (ctx: TContext) => void;
54
54
  requestNextDispatchAnnouncement: () => void;
55
+ cancelNextDispatchAnnouncement: () => void;
55
56
  watchdog: Queue.TelegramQueueDispatchWatchdogRuntime<TContext>;
56
57
  }
57
58
 
@@ -96,6 +97,9 @@ export function createTelegramQueueBindingRuntime<TContext>(deps: {
96
97
  updateStatus: (ctx: TContext, error?: string) => void;
97
98
  sendTextReply: Queue.TelegramQueueDispatchRuntimeDeps<TContext>["sendTextReply"];
98
99
  sendUserMessage: Queue.TelegramQueueDispatchRuntimeDeps<TContext>["sendUserMessage"];
100
+ reconcileNextDispatchAnnouncementReplyOwnership?: (
101
+ item: Queue.PendingTelegramTurn,
102
+ ) => void;
99
103
  recordRuntimeEvent?: TelegramRuntimeEventRecorder;
100
104
  }): TelegramQueueBindingRuntime<TContext> {
101
105
  const settleDiscardedItems = (
@@ -159,6 +163,8 @@ export function createTelegramQueueBindingRuntime<TContext>(deps: {
159
163
  },
160
164
  updateStatus: deps.updateStatus,
161
165
  sendTextReply: deps.sendTextReply,
166
+ reconcileNextDispatchAnnouncementReplyOwnership:
167
+ deps.reconcileNextDispatchAnnouncementReplyOwnership,
162
168
  recordRuntimeEvent: deps.recordRuntimeEvent,
163
169
  ...deps.promptDispatch,
164
170
  sendUserMessage: deps.sendUserMessage,
@@ -167,6 +173,7 @@ export function createTelegramQueueBindingRuntime<TContext>(deps: {
167
173
  mutation,
168
174
  dispatchNext: dispatch.dispatchNext,
169
175
  requestNextDispatchAnnouncement: dispatch.requestNextDispatchAnnouncement,
176
+ cancelNextDispatchAnnouncement: dispatch.cancelNextDispatchAnnouncement,
170
177
  watchdog: Queue.createTelegramQueueDispatchWatchdogRuntime({
171
178
  hasQueuedItems: deps.store.hasQueuedItems,
172
179
  dispatchNextQueuedTelegramTurn: dispatch.dispatchNext,
@@ -11,7 +11,11 @@ import { basename } from "node:path";
11
11
  import * as Sync from "./sync.ts";
12
12
  import * as Threads from "./threads.ts";
13
13
  import { parseTelegramUpdateJournalQueueOwner, type TelegramUpdateJournalStoreOptions } from "./journal.ts";
14
- import type { TelegramLockEntry, TelegramLockState } from "./locks.ts";
14
+ import {
15
+ TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS,
16
+ type TelegramLockEntry,
17
+ type TelegramLockState,
18
+ } from "./locks.ts";
15
19
  import type {
16
20
  TelegramQueueHandoffPayload,
17
21
  TelegramQueueHandoffStageResult,
@@ -60,6 +64,8 @@ export const TELEGRAM_BUS_FOLLOWER_PROMOTION_GRACE_MS = 2_500;
60
64
  export const TELEGRAM_FOLLOWER_SESSION_HANDOFF_TTL_MS = 30_000;
61
65
  export const TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS = 30_000;
62
66
  export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_WAIT_MS = 30_000;
67
+ export const TELEGRAM_BUS_FOLLOWER_HEARTBEAT_TIMEOUT_MS =
68
+ TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS;
63
69
  export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_ATTEMPTS =
64
70
  TELEGRAM_BUS_REGISTRATION_RETRY.attempts;
65
71
  export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_DELAY_MS =
@@ -301,6 +307,7 @@ export interface TelegramBusFollowerRegistrationRuntimeDeps<
301
307
  registrationRetryAttempts?: number;
302
308
  registrationRetryDelayMs?: number;
303
309
  heartbeatMs?: number;
310
+ heartbeatTimeoutMs?: number;
304
311
  recordRuntimeEvent?: (
305
312
  category: string,
306
313
  error: unknown,
@@ -1619,6 +1626,10 @@ export function createTelegramBusFollowerRegistrationRuntime<
1619
1626
  const getNowMs = deps.getNowMs ?? Date.now;
1620
1627
  const getPid = deps.getPid ?? (() => process.pid);
1621
1628
  const heartbeatMs = deps.heartbeatMs ?? 1000;
1629
+ const heartbeatTimeoutMs =
1630
+ deps.heartbeatTimeoutMs ??
1631
+ deps.timeoutMs ??
1632
+ TELEGRAM_BUS_FOLLOWER_HEARTBEAT_TIMEOUT_MS;
1622
1633
  const registrationTimeoutMs =
1623
1634
  deps.registrationTimeoutMs ?? deps.timeoutMs ?? 30000;
1624
1635
  const registrationRetryAttempts =
@@ -1679,7 +1690,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
1679
1690
  try {
1680
1691
  const response = await sendTelegramBusLocalEnvelope({
1681
1692
  socketPath: leaderSocketPath,
1682
- timeoutMs: deps.timeoutMs,
1693
+ timeoutMs: heartbeatTimeoutMs,
1683
1694
  retry: getTelegramBusTransportRetryPolicy({
1684
1695
  endpoint: leaderSocketPath,
1685
1696
  operation: "operation",
@@ -711,6 +711,7 @@ export interface TelegramCommandActionDeps<TMessage, TContext> {
711
711
  export interface TelegramStopCommandDeps {
712
712
  hasAbortHandler: () => boolean;
713
713
  clearPendingModelSwitch: () => void;
714
+ cancelNextTransitionAnnouncements?: () => void;
714
715
  clearQueuedTelegramItems: () => number;
715
716
  setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
716
717
  abortCurrentTurn: () => void;
@@ -1146,6 +1147,8 @@ export interface TelegramCommandRuntimeDeps<
1146
1147
  isContextActive?: (ctx: TContext) => boolean;
1147
1148
  dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
1148
1149
  requestNextDispatchAnnouncement?: () => void;
1150
+ markActiveTurnNextAbortAnnouncement?: () => boolean;
1151
+ cancelNextTransitionAnnouncements?: () => void;
1149
1152
  requestDeferredDispatchNextQueuedTelegramTurn?: (
1150
1153
  dispatch: (ctx: TContext) => void,
1151
1154
  ) => void;
@@ -1352,6 +1355,7 @@ export async function handleTelegramStopCommand(
1352
1355
  deps: TelegramStopCommandDeps,
1353
1356
  ): Promise<void> {
1354
1357
  deps.clearPendingModelSwitch();
1358
+ deps.cancelNextTransitionAnnouncements?.();
1355
1359
  const clearedCount = deps.clearQueuedTelegramItems();
1356
1360
  deps.setFoldQueuedPromptsIntoHistory(false);
1357
1361
  if (!deps.hasAbortHandler()) {
@@ -1385,6 +1389,7 @@ export async function handleTelegramAbortCommand(deps: {
1385
1389
  hasAbortHandler: () => boolean;
1386
1390
  hasActiveTelegramTurn: () => boolean;
1387
1391
  clearPendingModelSwitch: () => void;
1392
+ cancelNextTransitionAnnouncements?: () => void;
1388
1393
  abortCurrentTurn: () => void;
1389
1394
  setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
1390
1395
  updateStatus: () => void;
@@ -1394,6 +1399,7 @@ export async function handleTelegramAbortCommand(deps: {
1394
1399
  ) => Promise<void>;
1395
1400
  }): Promise<void> {
1396
1401
  deps.clearPendingModelSwitch();
1402
+ deps.cancelNextTransitionAnnouncements?.();
1397
1403
  if (!deps.hasAbortHandler()) {
1398
1404
  await deps.sendTextReply(
1399
1405
  formatTelegramInformationHeading("💤", "No active turn."),
@@ -1418,6 +1424,7 @@ export async function handleTelegramNextCommand(deps: {
1418
1424
  abortCurrentTurn: () => void;
1419
1425
  dispatchNextQueuedTurn: () => void;
1420
1426
  requestNextDispatchAnnouncement?: () => void;
1427
+ markActiveTurnNextAbortAnnouncement?: () => boolean;
1421
1428
  clearFoldForDispatch: () => void;
1422
1429
  updateStatus: () => void;
1423
1430
  sendTextReply: (
@@ -1439,6 +1446,7 @@ export async function handleTelegramNextCommand(deps: {
1439
1446
  if (!deps.isIdle() && deps.hasAbortHandler()) {
1440
1447
  deps.clearFoldForDispatch();
1441
1448
  deps.requestNextDispatchAnnouncement?.();
1449
+ deps.markActiveTurnNextAbortAnnouncement?.();
1442
1450
  deps.abortCurrentTurn();
1443
1451
  deps.updateStatus();
1444
1452
  return;
@@ -1880,6 +1888,11 @@ export function createTelegramCommandHandlerTargetRuntime<
1880
1888
  updateStatus: deps.updateStatus,
1881
1889
  isContextActive: deps.isContextActive,
1882
1890
  dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
1891
+ requestNextDispatchAnnouncement: deps.requestNextDispatchAnnouncement,
1892
+ markActiveTurnNextAbortAnnouncement:
1893
+ deps.markActiveTurnNextAbortAnnouncement,
1894
+ cancelNextTransitionAnnouncements:
1895
+ deps.cancelNextTransitionAnnouncements,
1883
1896
  startTypingLoop: deps.startTypingLoop,
1884
1897
  stopTypingLoop: deps.stopTypingLoop,
1885
1898
  enqueueContinueTurn: deps.enqueueContinueTurn,
@@ -2037,6 +2050,8 @@ async function handleTelegramCommandRuntime<
2037
2050
  await handleTelegramStopCommand({
2038
2051
  hasAbortHandler: deps.hasAbortHandler,
2039
2052
  clearPendingModelSwitch: deps.clearPendingModelSwitch,
2053
+ cancelNextTransitionAnnouncements:
2054
+ deps.cancelNextTransitionAnnouncements,
2040
2055
  clearQueuedTelegramItems: () =>
2041
2056
  deps.clearQueuedTelegramItems(commandCtx),
2042
2057
  setFoldQueuedPromptsIntoHistory: deps.setFoldQueuedPromptsIntoHistory,
@@ -2113,6 +2128,8 @@ async function handleTelegramCommandRuntime<
2113
2128
  hasAbortHandler: deps.hasAbortHandler,
2114
2129
  hasActiveTelegramTurn: deps.hasActiveTelegramTurn,
2115
2130
  clearPendingModelSwitch: deps.clearPendingModelSwitch,
2131
+ cancelNextTransitionAnnouncements:
2132
+ deps.cancelNextTransitionAnnouncements,
2116
2133
  abortCurrentTurn: deps.abortCurrentTurn,
2117
2134
  setFoldQueuedPromptsIntoHistory: deps.setFoldQueuedPromptsIntoHistory,
2118
2135
  updateStatus: updateStatusFor(commandCtx),
@@ -2129,6 +2146,8 @@ async function handleTelegramCommandRuntime<
2129
2146
  dispatchNextQueuedTurn: () =>
2130
2147
  deps.dispatchNextQueuedTelegramTurn(commandCtx),
2131
2148
  requestNextDispatchAnnouncement: deps.requestNextDispatchAnnouncement,
2149
+ markActiveTurnNextAbortAnnouncement:
2150
+ deps.markActiveTurnNextAbortAnnouncement,
2132
2151
  clearFoldForDispatch: () =>
2133
2152
  deps.setFoldQueuedPromptsIntoHistory(false),
2134
2153
  updateStatus: updateStatusFor(commandCtx),
@@ -820,6 +820,7 @@ export default function (pi: Pi.ExtensionAPI) {
820
820
  mutation: queueMutationRuntime,
821
821
  dispatchNext: dispatchNextQueuedTelegramTurn,
822
822
  requestNextDispatchAnnouncement,
823
+ cancelNextDispatchAnnouncement,
823
824
  watchdog: queueDispatchWatchdogRuntime,
824
825
  } = Bindings.createTelegramQueueBindingRuntime({
825
826
  store: telegramQueueStore,
@@ -835,6 +836,13 @@ export default function (pi: Pi.ExtensionAPI) {
835
836
  updateStatus,
836
837
  sendTextReply,
837
838
  sendUserMessage,
839
+ reconcileNextDispatchAnnouncementReplyOwnership(item) {
840
+ Replies.preserveTransportReplyDedupOnNextReset(
841
+ item.chatId,
842
+ item.replyToMessageId,
843
+ item.target,
844
+ );
845
+ },
838
846
  recordRuntimeEvent,
839
847
  });
840
848
  const { finalizeMarkdownPreview, preparePreviewDelivery } =
@@ -1093,6 +1101,7 @@ export default function (pi: Pi.ExtensionAPI) {
1093
1101
  isContextActive: telegramSessionContextStore.isCurrent,
1094
1102
  dispatchNextQueuedTelegramTurn,
1095
1103
  requestNextDispatchAnnouncement,
1104
+ cancelNextDispatchAnnouncement,
1096
1105
  requestDeferredDispatchNextQueuedTelegramTurn:
1097
1106
  deferredQueueDispatchRuntime.request,
1098
1107
  hasDeferredDispatchContext: deferredQueueDispatchRuntime.isBound,
@@ -126,6 +126,8 @@ export interface PendingTelegramTurn extends TelegramQueueItemBase {
126
126
  historyText: string;
127
127
  priorityEmoji?: string;
128
128
  reactionSuppressionEmoji?: string;
129
+ /** Emit the explicit aborted-turn notice when /next settles this active turn. */
130
+ announceNextAbortOnEnd?: boolean;
129
131
 
130
132
  /** Turn should preferably be delivered as voice (mirror mode + user sent voice) */
131
133
  voiceReplyPreferred?: boolean;
@@ -235,6 +237,8 @@ export interface TelegramActiveTurnStore<
235
237
  has: () => boolean;
236
238
  set: (turn: TTurn) => void;
237
239
  clear: () => void;
240
+ markNextAbortAnnouncement: () => boolean;
241
+ clearNextAbortAnnouncement: () => boolean;
238
242
  getChatId: () => number | undefined;
239
243
  getTarget: () => TelegramQueueTarget | undefined;
240
244
  getReplyToMessageId: () => number | undefined;
@@ -461,6 +465,16 @@ export function createTelegramActiveTurnStore<
461
465
  clear: () => {
462
466
  activeTurn = undefined;
463
467
  },
468
+ markNextAbortAnnouncement: () => {
469
+ if (!activeTurn) return false;
470
+ activeTurn.announceNextAbortOnEnd = true;
471
+ return true;
472
+ },
473
+ clearNextAbortAnnouncement: () => {
474
+ if (!activeTurn?.announceNextAbortOnEnd) return false;
475
+ delete activeTurn.announceNextAbortOnEnd;
476
+ return true;
477
+ },
464
478
  getChatId: () => activeTurn?.chatId,
465
479
  getTarget: () =>
466
480
  activeTurn?.target ? { ...activeTurn.target } : undefined,
@@ -1488,6 +1502,7 @@ export interface TelegramAgentEndPlan {
1488
1502
  kind: "no-turn" | "aborted" | "error" | "text" | "attachments-only" | "empty";
1489
1503
  shouldClearPreview: boolean;
1490
1504
  shouldDispatchNext: boolean;
1505
+ shouldSendAbortMessage: boolean;
1491
1506
  shouldSendErrorMessage: boolean;
1492
1507
  shouldSendAttachmentNotice: boolean;
1493
1508
  }
@@ -1680,6 +1695,7 @@ export function buildTelegramAgentEndPlan(options: {
1680
1695
  hasFinalText: boolean;
1681
1696
  hasQueuedAttachments: boolean;
1682
1697
  foldQueuedPromptsIntoHistory: boolean;
1698
+ announceNextAbortOnEnd?: boolean;
1683
1699
  }): TelegramAgentEndPlan {
1684
1700
  const shouldDispatchNext = shouldDispatchAfterTelegramAgentEnd({
1685
1701
  hasTurn: options.hasTurn,
@@ -1691,6 +1707,7 @@ export function buildTelegramAgentEndPlan(options: {
1691
1707
  kind: "no-turn",
1692
1708
  shouldClearPreview: false,
1693
1709
  shouldDispatchNext,
1710
+ shouldSendAbortMessage: false,
1694
1711
  shouldSendErrorMessage: false,
1695
1712
  shouldSendAttachmentNotice: false,
1696
1713
  };
@@ -1700,6 +1717,7 @@ export function buildTelegramAgentEndPlan(options: {
1700
1717
  kind: "aborted",
1701
1718
  shouldClearPreview: true,
1702
1719
  shouldDispatchNext,
1720
+ shouldSendAbortMessage: options.announceNextAbortOnEnd === true,
1703
1721
  shouldSendErrorMessage: false,
1704
1722
  shouldSendAttachmentNotice: false,
1705
1723
  };
@@ -1709,6 +1727,7 @@ export function buildTelegramAgentEndPlan(options: {
1709
1727
  kind: "error",
1710
1728
  shouldClearPreview: true,
1711
1729
  shouldDispatchNext,
1730
+ shouldSendAbortMessage: false,
1712
1731
  shouldSendErrorMessage: true,
1713
1732
  shouldSendAttachmentNotice: false,
1714
1733
  };
@@ -1718,6 +1737,7 @@ export function buildTelegramAgentEndPlan(options: {
1718
1737
  kind: "text",
1719
1738
  shouldClearPreview: false,
1720
1739
  shouldDispatchNext,
1740
+ shouldSendAbortMessage: false,
1721
1741
  shouldSendErrorMessage: false,
1722
1742
  shouldSendAttachmentNotice: false,
1723
1743
  };
@@ -1727,6 +1747,7 @@ export function buildTelegramAgentEndPlan(options: {
1727
1747
  kind: "attachments-only",
1728
1748
  shouldClearPreview: true,
1729
1749
  shouldDispatchNext,
1750
+ shouldSendAbortMessage: false,
1730
1751
  shouldSendErrorMessage: false,
1731
1752
  shouldSendAttachmentNotice: true,
1732
1753
  };
@@ -1735,6 +1756,7 @@ export function buildTelegramAgentEndPlan(options: {
1735
1756
  kind: "empty",
1736
1757
  shouldClearPreview: true,
1737
1758
  shouldDispatchNext,
1759
+ shouldSendAbortMessage: false,
1738
1760
  shouldSendErrorMessage: false,
1739
1761
  shouldSendAttachmentNotice: false,
1740
1762
  };
@@ -1891,6 +1913,7 @@ export async function handleTelegramAgentEndRuntime<
1891
1913
  hasFinalText: !!finalText || hasOutboundArtifacts,
1892
1914
  hasQueuedAttachments: (turn?.queuedAttachments.length ?? 0) > 0,
1893
1915
  foldQueuedPromptsIntoHistory: deps.foldQueuedPromptsIntoHistory,
1916
+ announceNextAbortOnEnd: turn?.announceNextAbortOnEnd === true,
1894
1917
  });
1895
1918
  if (!turn) {
1896
1919
  if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
@@ -2017,22 +2040,29 @@ export async function handleTelegramAgentEndRuntime<
2017
2040
  await clearTurnPreview();
2018
2041
  if (!isDeliveryActive()) return;
2019
2042
  }
2020
- if (endPlan.shouldSendErrorMessage) {
2043
+ if (endPlan.shouldSendAbortMessage || endPlan.shouldSendErrorMessage) {
2021
2044
  const errorMessage = assistant.errorMessage ||
2022
2045
  "Telegram bridge: Pi failed while processing the request.";
2023
- const isOperationAborted = errorMessage.trim().replace(/\.$/, "") ===
2024
- "This operation was aborted";
2025
- await deps.sendTextReply(
2026
- turn.chatId,
2027
- turn.replyToMessageId,
2028
- isOperationAborted
2029
- ? "<b>⏹️ This operation was aborted.</b>"
2030
- : errorMessage,
2031
- {
2032
- target: turn.target,
2033
- ...(isOperationAborted ? { parseMode: "HTML" as const } : {}),
2034
- },
2035
- );
2046
+ const isOperationAborted = endPlan.shouldSendAbortMessage ||
2047
+ errorMessage.trim().replace(/\.$/, "") === "This operation was aborted";
2048
+ try {
2049
+ await deps.sendTextReply(
2050
+ turn.chatId,
2051
+ turn.replyToMessageId,
2052
+ isOperationAborted
2053
+ ? "<b>⏹️ This operation was aborted.</b>"
2054
+ : errorMessage,
2055
+ {
2056
+ target: turn.target,
2057
+ ...(isOperationAborted ? { parseMode: "HTML" as const } : {}),
2058
+ },
2059
+ );
2060
+ } catch (error) {
2061
+ if (!isOperationAborted) throw error;
2062
+ deps.recordRuntimeEvent?.("dispatch", error, {
2063
+ phase: "next-abort-announcement",
2064
+ });
2065
+ }
2036
2066
  if (!isDeliveryActive()) return;
2037
2067
  if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
2038
2068
  return;
@@ -2180,7 +2210,7 @@ export async function handleTelegramAgentEndRuntime<
2180
2210
  };
2181
2211
  if (
2182
2212
  deps.scheduleActiveTurnDelivery &&
2183
- (endPlan.kind === "text" || endPlan.kind === "attachments-only" || endPlan.shouldSendErrorMessage || endPlan.shouldClearPreview)
2213
+ (endPlan.kind === "text" || endPlan.kind === "attachments-only" || endPlan.shouldSendAbortMessage || endPlan.shouldSendErrorMessage || endPlan.shouldClearPreview)
2184
2214
  ) {
2185
2215
  deps.scheduleActiveTurnDelivery(deliverActiveTurn);
2186
2216
  return;
@@ -3092,6 +3122,9 @@ export interface TelegramQueueDispatchControllerDeps<
3092
3122
  ) => boolean;
3093
3123
  sendUserMessage: TelegramDispatchRuntimeDeps<TContext>["sendUserMessage"];
3094
3124
  onPromptDispatchFailure: (ctx: TContext, message: string) => void;
3125
+ reconcileNextDispatchAnnouncementReplyOwnership?: (
3126
+ item: PendingTelegramTurn,
3127
+ ) => void;
3095
3128
  isQueueItemTransportActive?: (item: TelegramQueueItem<TContext>) => boolean;
3096
3129
  hasPendingInboundQueueMutationForItem?: (
3097
3130
  item: TelegramQueueItem<TContext>,
@@ -3107,6 +3140,7 @@ export interface TelegramQueueDispatchControllerDeps<
3107
3140
  export interface TelegramQueueDispatchController<TContext = unknown> {
3108
3141
  dispatchNext: (ctx: TContext) => void;
3109
3142
  requestNextDispatchAnnouncement: () => void;
3143
+ cancelNextDispatchAnnouncement: () => void;
3110
3144
  }
3111
3145
 
3112
3146
  export function executeTelegramQueueDispatchPlan<TContext = unknown>(
@@ -3161,6 +3195,8 @@ export function createTelegramQueueDispatchRuntime<TContext = unknown>(
3161
3195
  commitPromptDispatch: deps.commitPromptDispatch,
3162
3196
  sendUserMessage: deps.sendUserMessage,
3163
3197
  onPromptDispatchFailure: deps.onPromptDispatchFailure,
3198
+ reconcileNextDispatchAnnouncementReplyOwnership:
3199
+ deps.reconcileNextDispatchAnnouncementReplyOwnership,
3164
3200
  isQueueItemTransportActive: deps.isQueueItemTransportActive,
3165
3201
  hasPendingInboundQueueMutationForItem:
3166
3202
  deps.hasPendingInboundQueueMutationForItem,
@@ -3176,12 +3212,19 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
3176
3212
  ): TelegramQueueDispatchController<TContext> {
3177
3213
  let controlDispatchPending = false;
3178
3214
  let nextDispatchAnnouncementRequested = false;
3215
+ let nextDispatchAnnouncementGeneration = 0;
3179
3216
  let nextDispatchAnnouncementAnchor: TelegramQueueItem<TContext> | undefined;
3180
3217
  const controller: TelegramQueueDispatchController<TContext> = {
3181
3218
  requestNextDispatchAnnouncement: () => {
3219
+ nextDispatchAnnouncementGeneration += 1;
3182
3220
  nextDispatchAnnouncementRequested = true;
3183
3221
  nextDispatchAnnouncementAnchor = undefined;
3184
3222
  },
3223
+ cancelNextDispatchAnnouncement: () => {
3224
+ nextDispatchAnnouncementGeneration += 1;
3225
+ nextDispatchAnnouncementRequested = false;
3226
+ nextDispatchAnnouncementAnchor = undefined;
3227
+ },
3185
3228
  dispatchNext: (ctx) => {
3186
3229
  if (deps.hasDispatchContext && !deps.hasDispatchContext()) return;
3187
3230
  if (controlDispatchPending) {
@@ -3393,6 +3436,7 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
3393
3436
  if (dispatchPlan.kind === "prompt" && nextDispatchAnnouncementRequested) {
3394
3437
  nextDispatchAnnouncementRequested = false;
3395
3438
  controlDispatchPending = true;
3439
+ const announcementGeneration = nextDispatchAnnouncementGeneration;
3396
3440
  const dispatchGeneration = deps.getDispatchGeneration?.();
3397
3441
  deps.updateStatus(ctx);
3398
3442
  void deps.sendTextReply(
@@ -3401,8 +3445,19 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
3401
3445
  "<b>⏩ Dispatching next queued turn.</b>",
3402
3446
  { target: dispatchPlan.item.target },
3403
3447
  ).catch((error) => {
3404
- deps.recordRuntimeEvent?.("dispatch", error, { phase: "next-announcement" });
3448
+ deps.recordRuntimeEvent?.("dispatch", error, {
3449
+ phase: "next-announcement",
3450
+ });
3405
3451
  }).finally(() => {
3452
+ try {
3453
+ deps.reconcileNextDispatchAnnouncementReplyOwnership?.(
3454
+ dispatchPlan.item,
3455
+ );
3456
+ } catch (error) {
3457
+ deps.recordRuntimeEvent?.("dispatch", error, {
3458
+ phase: "next-announcement-reply-ownership",
3459
+ });
3460
+ }
3406
3461
  controlDispatchPending = false;
3407
3462
  if (deps.hasDispatchContext && !deps.hasDispatchContext()) return;
3408
3463
  if (
@@ -3410,6 +3465,10 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
3410
3465
  deps.isDispatchGenerationActive &&
3411
3466
  !deps.isDispatchGenerationActive(dispatchGeneration)
3412
3467
  ) return;
3468
+ if (announcementGeneration !== nextDispatchAnnouncementGeneration) {
3469
+ if (nextDispatchAnnouncementRequested) controller.dispatchNext(ctx);
3470
+ return;
3471
+ }
3413
3472
  executePlan();
3414
3473
  });
3415
3474
  return;
@@ -68,6 +68,7 @@ export function createReplyDedupRuntime(): ReplyDedupRuntime {
68
68
  // --- Transport-level dedup ---
69
69
 
70
70
  const lastRepliedToMessageIdByTarget = new Map<string, number>();
71
+ const replyDedupPreservedOnNextReset = new Map<string, number>();
71
72
  let replyDedupGeneration = 0;
72
73
 
73
74
  function getReplyDedupTargetKey(
@@ -83,6 +84,23 @@ function getReplyDedupTargetKey(
83
84
  export function resetTransportReplyDedup(): void {
84
85
  replyDedupGeneration += 1;
85
86
  lastRepliedToMessageIdByTarget.clear();
87
+ for (const [key, messageId] of replyDedupPreservedOnNextReset) {
88
+ lastRepliedToMessageIdByTarget.set(key, messageId);
89
+ }
90
+ replyDedupPreservedOnNextReset.clear();
91
+ }
92
+
93
+ /** Keeps a successfully published transition notice as the first reply of the
94
+ * next agent turn. The following agent-start reset consumes this one-shot
95
+ * preservation, so later messages in that turn do not repeat the reply header. */
96
+ export function preserveTransportReplyDedupOnNextReset(
97
+ chatId: number,
98
+ messageId: number,
99
+ target?: TelegramTarget,
100
+ ): void {
101
+ const key = getReplyDedupTargetKey(chatId, target);
102
+ if (lastRepliedToMessageIdByTarget.get(key) !== messageId) return;
103
+ replyDedupPreservedOnNextReset.set(key, messageId);
86
104
  }
87
105
 
88
106
  export function buildTelegramReplyParameters(
@@ -627,6 +627,7 @@ export interface TelegramInboundRouteRuntimeDeps<
627
627
  isContextActive?: (ctx: TContext) => boolean;
628
628
  dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
629
629
  requestNextDispatchAnnouncement?: () => void;
630
+ cancelNextDispatchAnnouncement?: () => void;
630
631
  requestDeferredDispatchNextQueuedTelegramTurn?: (
631
632
  dispatch: (ctx: TContext) => void,
632
633
  ) => void;
@@ -2222,6 +2223,10 @@ export function createTelegramInboundRouteRuntime<
2222
2223
  isContextActive: deps.isContextActive,
2223
2224
  dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
2224
2225
  requestNextDispatchAnnouncement: deps.requestNextDispatchAnnouncement,
2226
+ cancelNextTransitionAnnouncements: () => {
2227
+ deps.activeTurnRuntime.clearNextAbortAnnouncement();
2228
+ deps.cancelNextDispatchAnnouncement?.();
2229
+ },
2225
2230
  requestDeferredDispatchNextQueuedTelegramTurn:
2226
2231
  deps.requestDeferredDispatchNextQueuedTelegramTurn,
2227
2232
  startTypingLoop: deps.startTypingLoop,
@@ -2297,6 +2302,8 @@ export function createTelegramInboundRouteRuntime<
2297
2302
  },
2298
2303
  getPromptTemplateCommands,
2299
2304
  sendTextReply: deps.sendTextReply,
2305
+ markActiveTurnNextAbortAnnouncement:
2306
+ deps.activeTurnRuntime.markNextAbortAnnouncement,
2300
2307
  getActiveTurnReply: () => {
2301
2308
  const activeTurn = deps.activeTurnRuntime.get();
2302
2309
  if (!activeTurn) return undefined;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.51.0",
3
+ "version": "0.51.1",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-kit",
3
- "version": "0.21.0",
3
+ "version": "0.21.2",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -44,8 +44,8 @@
44
44
  "@llblab/pi-clean-room": "0.1.1",
45
45
  "@llblab/pi-codex-usage": "0.10.0",
46
46
  "@llblab/pi-grow-loop": "0.8.1",
47
- "@llblab/pi-state-flow": "0.18.0",
48
- "@llblab/pi-telegram": "0.51.0",
47
+ "@llblab/pi-state-flow": "0.18.1",
48
+ "@llblab/pi-telegram": "0.51.1",
49
49
  "@llblab/skills": "1.15.0"
50
50
  },
51
51
  "bundledDependencies": [