@llblab/pi-kit 0.31.2 → 0.33.0

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 (94) hide show
  1. package/BACKLOG.md +1 -1
  2. package/CHANGELOG.md +8 -0
  3. package/README.md +5 -3
  4. package/node_modules/@llblab/pi-codex-usage/AGENTS.md +2 -1
  5. package/node_modules/@llblab/pi-codex-usage/CHANGELOG.md +4 -0
  6. package/node_modules/@llblab/pi-codex-usage/README.md +11 -9
  7. package/node_modules/@llblab/pi-codex-usage/lib/extension.ts +13 -10
  8. package/node_modules/@llblab/pi-codex-usage/lib/fast.ts +6 -5
  9. package/node_modules/@llblab/pi-codex-usage/lib/query.ts +4 -4
  10. package/node_modules/@llblab/pi-codex-usage/lib/status.ts +10 -10
  11. package/node_modules/@llblab/pi-codex-usage/lib/telegram.ts +1 -1
  12. package/node_modules/@llblab/pi-codex-usage/lib/usage.ts +2 -1
  13. package/node_modules/@llblab/pi-codex-usage/package.json +1 -1
  14. package/node_modules/@llblab/pi-telegram/AGENTS.md +4 -2
  15. package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -1
  16. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +11 -0
  17. package/node_modules/@llblab/pi-telegram/README.md +5 -3
  18. package/node_modules/@llblab/pi-telegram/dist/lib/activity.d.ts +5 -1
  19. package/node_modules/@llblab/pi-telegram/dist/lib/activity.js +31 -2
  20. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +3 -1
  21. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +8 -2
  22. package/node_modules/@llblab/pi-telegram/dist/lib/bus-api.d.ts +2 -2
  23. package/node_modules/@llblab/pi-telegram/dist/lib/bus-api.js +7 -2
  24. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +49 -3
  25. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +313 -146
  26. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +3 -0
  27. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +99 -18
  28. package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +5 -3
  29. package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +13 -10
  30. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.d.ts +4 -0
  31. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.js +48 -15
  32. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +48 -6
  33. package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.d.ts +68 -0
  34. package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +178 -58
  35. package/node_modules/@llblab/pi-telegram/dist/lib/machine.d.ts +23 -0
  36. package/node_modules/@llblab/pi-telegram/dist/lib/machine.js +37 -0
  37. package/node_modules/@llblab/pi-telegram/dist/lib/media.d.ts +114 -28
  38. package/node_modules/@llblab/pi-telegram/dist/lib/media.js +292 -119
  39. package/node_modules/@llblab/pi-telegram/dist/lib/model.d.ts +54 -15
  40. package/node_modules/@llblab/pi-telegram/dist/lib/model.js +98 -53
  41. package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +2 -0
  42. package/node_modules/@llblab/pi-telegram/dist/lib/preview.d.ts +128 -113
  43. package/node_modules/@llblab/pi-telegram/dist/lib/preview.js +448 -366
  44. package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +2 -2
  45. package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +3 -3
  46. package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +6 -1
  47. package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +334 -210
  48. package/node_modules/@llblab/pi-telegram/dist/lib/runtime.d.ts +77 -11
  49. package/node_modules/@llblab/pi-telegram/dist/lib/runtime.js +174 -92
  50. package/node_modules/@llblab/pi-telegram/dist/lib/sections.js +0 -1
  51. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +11 -9
  52. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +53 -4
  53. package/node_modules/@llblab/pi-telegram/dist/lib/text-groups.d.ts +0 -26
  54. package/node_modules/@llblab/pi-telegram/dist/lib/text-groups.js +51 -98
  55. package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.js +14 -6
  56. package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +18 -5
  57. package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +107 -30
  58. package/node_modules/@llblab/pi-telegram/dist/lib/turns.d.ts +2 -0
  59. package/node_modules/@llblab/pi-telegram/dist/lib/turns.js +6 -1
  60. package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +11 -4
  61. package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +54 -19
  62. package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
  63. package/node_modules/@llblab/pi-telegram/docs/activity.md +1 -1
  64. package/node_modules/@llblab/pi-telegram/docs/architecture.md +12 -11
  65. package/node_modules/@llblab/pi-telegram/docs/delivery.md +7 -5
  66. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +18 -8
  67. package/node_modules/@llblab/pi-telegram/docs/public-api.md +2 -2
  68. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +2 -2
  69. package/node_modules/@llblab/pi-telegram/lib/activity.ts +29 -5
  70. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +9 -1
  71. package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +11 -3
  72. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +417 -203
  73. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +88 -14
  74. package/node_modules/@llblab/pi-telegram/lib/bus.ts +17 -15
  75. package/node_modules/@llblab/pi-telegram/lib/delivery.ts +47 -19
  76. package/node_modules/@llblab/pi-telegram/lib/extension.ts +51 -9
  77. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +229 -55
  78. package/node_modules/@llblab/pi-telegram/lib/machine.ts +60 -0
  79. package/node_modules/@llblab/pi-telegram/lib/media.ts +438 -172
  80. package/node_modules/@llblab/pi-telegram/lib/model.ts +169 -74
  81. package/node_modules/@llblab/pi-telegram/lib/polling.ts +2 -0
  82. package/node_modules/@llblab/pi-telegram/lib/preview.ts +715 -566
  83. package/node_modules/@llblab/pi-telegram/lib/queue.ts +5 -5
  84. package/node_modules/@llblab/pi-telegram/lib/routing.ts +325 -238
  85. package/node_modules/@llblab/pi-telegram/lib/runtime.ts +260 -104
  86. package/node_modules/@llblab/pi-telegram/lib/sections.ts +0 -2
  87. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +61 -8
  88. package/node_modules/@llblab/pi-telegram/lib/text-groups.ts +75 -136
  89. package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +15 -4
  90. package/node_modules/@llblab/pi-telegram/lib/threads.ts +120 -36
  91. package/node_modules/@llblab/pi-telegram/lib/turns.ts +8 -1
  92. package/node_modules/@llblab/pi-telegram/lib/updates.ts +58 -21
  93. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  94. package/package.json +3 -3
@@ -116,10 +116,28 @@ function coalesceActivityEvents(previous, next) {
116
116
  }
117
117
  return next;
118
118
  }
119
+ /** Optional observation must not change queue/transport settlement. */
120
+ function captureActivityDrainObserver(capture) {
121
+ try {
122
+ const notify = capture?.();
123
+ return () => {
124
+ try {
125
+ const emitted = notify?.();
126
+ if (emitted)
127
+ void Promise.resolve(emitted).catch(() => undefined);
128
+ }
129
+ catch { /* Observer failure is inert. */ }
130
+ };
131
+ }
132
+ catch {
133
+ return () => { };
134
+ }
135
+ }
119
136
  /** @internal */
120
137
  export function createTelegramActivityDispatcher(deps = {}) {
121
138
  const queues = new Map();
122
139
  let stopped = false;
140
+ const hasPending = () => Array.from(queues.values()).some(queue => queue.active && (queue.running || queue.events.length > 0));
123
141
  const drain = async (queue) => {
124
142
  if (queue.running || !queue.active)
125
143
  return;
@@ -146,10 +164,12 @@ export function createTelegramActivityDispatcher(deps = {}) {
146
164
  }
147
165
  finally {
148
166
  queue.running = false;
167
+ if (!stopped && !hasPending())
168
+ queue.notifyDrained?.();
149
169
  }
150
170
  };
151
171
  return {
152
- hasPending: () => Array.from(queues.values()).some((queue) => queue.active && (queue.running || queue.events.length > 0)),
172
+ hasPending,
153
173
  dispatch(event) {
154
174
  if (stopped)
155
175
  return;
@@ -164,6 +184,8 @@ export function createTelegramActivityDispatcher(deps = {}) {
164
184
  };
165
185
  queues.set(registration.id, queue);
166
186
  }
187
+ if (!queue.running && queue.events.length === 0)
188
+ queue.notifyDrained = captureActivityDrainObserver(deps.captureDrain);
167
189
  const previous = queue.events.at(-1);
168
190
  if (previous && canCoalesceActivityEvents(previous, event)) {
169
191
  queue.events[queue.events.length - 1] = coalesceActivityEvents(previous, event);
@@ -198,6 +220,7 @@ export function createTelegramActivityBridgeRuntime(deps) {
198
220
  runtime = createTelegramActivityRuntime({
199
221
  generation: `${deps.generation}:${++generationSequence}`,
200
222
  dispatcher: createTelegramActivityDispatcher({
223
+ captureDrain: deps.captureDrain,
201
224
  recordFailure: deps.recordFailure,
202
225
  }),
203
226
  observeEvent: deps.observeEvent,
@@ -470,13 +493,14 @@ export function createTelegramActivityRuntime(deps) {
470
493
  },
471
494
  };
472
495
  }
473
- export function createTelegramActivityPublicationRuntime() {
496
+ export function createTelegramActivityPublicationRuntime(deps = {}) {
474
497
  let generation = 0;
475
498
  let tail = Promise.resolve();
476
499
  let outstanding = 0, unconfirmed = false;
477
500
  const pending = new Set();
478
501
  const reserve = () => {
479
502
  const admittedGeneration = generation;
503
+ const notifyDrained = captureActivityDrainObserver(deps.captureDrain);
480
504
  let state = "pending";
481
505
  let resolve;
482
506
  const ready = new Promise((accept) => {
@@ -499,6 +523,8 @@ export function createTelegramActivityPublicationRuntime() {
499
523
  })
500
524
  .finally(() => {
501
525
  outstanding -= 1;
526
+ if (admittedGeneration === generation && outstanding === 0 && !unconfirmed)
527
+ notifyDrained();
502
528
  });
503
529
  tail = result.catch(() => { });
504
530
  return {
@@ -522,6 +548,7 @@ export function createTelegramActivityPublicationRuntime() {
522
548
  hasUnconfirmed: () => unconfirmed,
523
549
  beginWork() {
524
550
  const admittedGeneration = generation;
551
+ const notifyDrained = captureActivityDrainObserver(deps.captureDrain);
525
552
  let settled = false;
526
553
  outstanding += 1;
527
554
  return {
@@ -532,6 +559,8 @@ export function createTelegramActivityPublicationRuntime() {
532
559
  outstanding -= 1;
533
560
  if (outcome === "unconfirmed" && admittedGeneration === generation)
534
561
  unconfirmed = true;
562
+ if (admittedGeneration === generation && outstanding === 0 && !unconfirmed)
563
+ notifyDrained();
535
564
  },
536
565
  };
537
566
  },
@@ -177,6 +177,7 @@ export interface TelegramActivityBindingRuntime {
177
177
  /** Compose public activity fanout, verbosity, and assistant output ordering. */
178
178
  export declare function createTelegramActivityBindingRuntime<TTransportStamp>(deps: {
179
179
  generation: string;
180
+ captureDrain?: () => (() => void) | undefined;
180
181
  assistantOutput: Omit<Parameters<typeof createTelegramAssistantOutputBindingRuntime<TTransportStamp>>[0], "waitForActivityIdle" | "enqueue">;
181
182
  activityVerbosity: Omit<Parameters<typeof ActivityVerbosity.createTelegramActivityVerbosityRuntime<TelegramAssistantOutputAuthority<TTransportStamp>>>[0], "captureAuthority" | "isAuthorityActive" | "recordFailure" | "enqueue">;
182
183
  }): TelegramActivityBindingRuntime;
@@ -288,11 +289,12 @@ interface TelegramLifecycleBindingDeps {
288
289
  }) => void;
289
290
  canSendAgentActivity: (ctx: Pi.ExtensionContext) => boolean;
290
291
  isSessionContextActive: (ctx: Pi.ExtensionContext) => boolean;
292
+ getSessionGeneration?: () => number;
291
293
  isTurnTransportActive?: (turn: Queue.PendingTelegramTurn) => boolean;
292
294
  updateStatus: TelegramBridgeStatusUpdater;
293
295
  recordRuntimeEvent: TelegramRuntimeEventRecorder;
294
296
  /** Settled-session hint for owners that wait on agent idleness, such as old-tab cleanup after a live rebind. */
295
297
  onSessionSettled?: (ctx: Pi.ExtensionContext) => void;
296
298
  }
297
- export declare function registerTelegramLifecycleRuntimeHooks({ pi, publicationRuntime, activityRuntime, activityVerbosityRuntime, diagnostics, assistantOutputRuntime, sessionLifecycleRuntime, configStore, abort, typing, lifecycle, activeTurnRuntime, telegramQueueStore, modelSwitchController, previewRuntime, promptDispatchRuntime, deferredQueueDispatchRuntime, modelContextAvailabilityRuntime, disconnectOnQuit, onSessionStarted, shutdownGenerativeAppLiveSurfaces, resolveAutomaticThreadCleanupEnabled, buttonActionStore, callMultipart, sendChatAction, sendRecordVoiceAction, sendMarkdownReply, sendTextReply, dispatchNextQueuedTelegramTurn, onPromptHandedOff, answerGuestQuery, deleteMessage, sendGuestReply, editGuestReply, stopGuestPlaceholder, preparePreviewDelivery, finalizeMarkdownPreview, proactivePushTargetGetter, getAssistantRenderingMode, recordMessageOwnership, canSendAgentActivity, isSessionContextActive, isTurnTransportActive, updateStatus, recordRuntimeEvent, onSessionSettled, }: TelegramLifecycleBindingDeps): void;
299
+ export declare function registerTelegramLifecycleRuntimeHooks({ pi, publicationRuntime, activityRuntime, activityVerbosityRuntime, diagnostics, assistantOutputRuntime, sessionLifecycleRuntime, configStore, abort, typing, lifecycle, activeTurnRuntime, telegramQueueStore, modelSwitchController, previewRuntime, promptDispatchRuntime, deferredQueueDispatchRuntime, modelContextAvailabilityRuntime, disconnectOnQuit, onSessionStarted, shutdownGenerativeAppLiveSurfaces, resolveAutomaticThreadCleanupEnabled, buttonActionStore, callMultipart, sendChatAction, sendRecordVoiceAction, sendMarkdownReply, sendTextReply, dispatchNextQueuedTelegramTurn, onPromptHandedOff, answerGuestQuery, deleteMessage, sendGuestReply, editGuestReply, stopGuestPlaceholder, preparePreviewDelivery, finalizeMarkdownPreview, proactivePushTargetGetter, getAssistantRenderingMode, recordMessageOwnership, canSendAgentActivity, isSessionContextActive, getSessionGeneration, isTurnTransportActive, updateStatus, recordRuntimeEvent, onSessionSettled, }: TelegramLifecycleBindingDeps): void;
298
300
  export {};
@@ -354,7 +354,7 @@ export function createTelegramAssistantOutputBindingRuntime(deps) {
354
354
  }
355
355
  /** Compose public activity fanout, verbosity, and assistant output ordering. */
356
356
  export function createTelegramActivityBindingRuntime(deps) {
357
- const publication = Activity.createTelegramActivityPublicationRuntime();
357
+ const publication = Activity.createTelegramActivityPublicationRuntime({ captureDrain: deps.captureDrain });
358
358
  const assistantOutputBinding = createTelegramAssistantOutputBindingRuntime({
359
359
  ...deps.assistantOutput,
360
360
  enqueue: publication.enqueue,
@@ -374,6 +374,7 @@ export function createTelegramActivityBindingRuntime(deps) {
374
374
  });
375
375
  const activityRuntime = Activity.createTelegramActivityBridgeRuntime({
376
376
  generation: deps.generation,
377
+ captureDrain: deps.captureDrain,
377
378
  observeEvent(event) {
378
379
  assistantOutputBinding.observeEvent(event);
379
380
  activityVerbosityRuntime.accept(event);
@@ -626,7 +627,7 @@ export function registerTelegramCommandsAndTools({ pi, agentDir, configStore, pe
626
627
  },
627
628
  });
628
629
  }
629
- export function registerTelegramLifecycleRuntimeHooks({ pi, publicationRuntime, activityRuntime, activityVerbosityRuntime, diagnostics, assistantOutputRuntime, sessionLifecycleRuntime, configStore, abort, typing, lifecycle, activeTurnRuntime, telegramQueueStore, modelSwitchController, previewRuntime, promptDispatchRuntime, deferredQueueDispatchRuntime, modelContextAvailabilityRuntime, disconnectOnQuit, onSessionStarted, shutdownGenerativeAppLiveSurfaces, resolveAutomaticThreadCleanupEnabled, buttonActionStore, callMultipart, sendChatAction, sendRecordVoiceAction, sendMarkdownReply, sendTextReply, dispatchNextQueuedTelegramTurn, onPromptHandedOff, answerGuestQuery, deleteMessage, sendGuestReply, editGuestReply, stopGuestPlaceholder, preparePreviewDelivery, finalizeMarkdownPreview, proactivePushTargetGetter, getAssistantRenderingMode, recordMessageOwnership, canSendAgentActivity, isSessionContextActive = () => true, isTurnTransportActive, updateStatus, recordRuntimeEvent, onSessionSettled, }) {
630
+ export function registerTelegramLifecycleRuntimeHooks({ pi, publicationRuntime, activityRuntime, activityVerbosityRuntime, diagnostics, assistantOutputRuntime, sessionLifecycleRuntime, configStore, abort, typing, lifecycle, activeTurnRuntime, telegramQueueStore, modelSwitchController, previewRuntime, promptDispatchRuntime, deferredQueueDispatchRuntime, modelContextAvailabilityRuntime, disconnectOnQuit, onSessionStarted, shutdownGenerativeAppLiveSurfaces, resolveAutomaticThreadCleanupEnabled, buttonActionStore, callMultipart, sendChatAction, sendRecordVoiceAction, sendMarkdownReply, sendTextReply, dispatchNextQueuedTelegramTurn, onPromptHandedOff, answerGuestQuery, deleteMessage, sendGuestReply, editGuestReply, stopGuestPlaceholder, preparePreviewDelivery, finalizeMarkdownPreview, proactivePushTargetGetter, getAssistantRenderingMode, recordMessageOwnership, canSendAgentActivity, isSessionContextActive = () => true, getSessionGeneration, isTurnTransportActive, updateStatus, recordRuntimeEvent, onSessionSettled, }) {
630
631
  const agentEndResetter = Runtime.createTelegramAgentEndResetter({
631
632
  abort,
632
633
  typing,
@@ -864,6 +865,8 @@ export function registerTelegramLifecycleRuntimeHooks({ pi, publicationRuntime,
864
865
  });
865
866
  const messageActivityTypingHooks = Lifecycle.createTelegramMessageActivityTypingHooks({
866
867
  hasActiveTurn: activeTurnRuntime.has,
868
+ isContextActive: isSessionContextActive,
869
+ getSessionGeneration,
867
870
  startTypingLoop: startActiveTurnTypingLoop,
868
871
  onMessageStart: previewRuntime.onMessageStart,
869
872
  onMessageUpdate: previewRuntime.onMessageUpdate,
@@ -955,9 +958,12 @@ export function registerTelegramLifecycleRuntimeHooks({ pi, publicationRuntime,
955
958
  async onAgentStart(event, ctx) {
956
959
  if (!isSessionContextActive(ctx))
957
960
  return;
961
+ const generation = getSessionGeneration?.();
958
962
  agentWorkActive = true;
959
963
  cancelPendingFinalPublication();
960
964
  await agentStartWithDedupReset(event, ctx);
965
+ if (!isSessionContextActive(ctx) || (getSessionGeneration && !Object.is(getSessionGeneration(), generation)))
966
+ return;
961
967
  const turn = activeTurnRuntime.get();
962
968
  activityRuntime.onAgentStart(turn?.target, turn?.replyToMessageId);
963
969
  startAgentActivityTypingLoop(ctx);
@@ -4,8 +4,8 @@
4
4
  * Wraps the direct Telegram Bot API runtime so follower instances can route outbound calls through the bus leader
5
5
  */
6
6
  import { type TelegramBusSelectedMenuDeliveryObservation, type TelegramBusSelectedMenuTextEffect } from "./bus.ts";
7
- import type { TelegramBridgeApiRuntime } from "./telegram-api.ts";
8
- export type TelegramBusApiCall = (method: string, args: unknown[]) => Promise<unknown>;
7
+ import type { TelegramApiCallOptions, TelegramBridgeApiRuntime } from "./telegram-api.ts";
8
+ export type TelegramBusApiCall = (method: string, args: unknown[], localOptions?: Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<unknown>;
9
9
  export interface TelegramBusAwareApiRuntimeDeps {
10
10
  directRuntime: TelegramBridgeApiRuntime;
11
11
  ownsDirect: () => boolean;
@@ -4,7 +4,7 @@
4
4
  * Wraps the direct Telegram Bot API runtime so follower instances can route outbound calls through the bus leader
5
5
  */
6
6
  import { markTelegramBusCrossTargetDelivery, parseTelegramBusSelectedMenuTextEffect, stripTelegramBusApiMetadata, } from "./bus.js";
7
- import { buildTelegramAnswerGuestQueryBody, isTelegramApiCommitUnknownError, isTelegramMessageNotModifiedError, TelegramApiAuthorityError, TelegramApiCommitUnknownError, } from "./telegram-api.js";
7
+ import { buildTelegramAnswerGuestQueryBody, assertTelegramApiCallAuthority, isTelegramApiCommitUnknownError, isTelegramMessageNotModifiedError, TelegramApiAuthorityError, TelegramApiCommitUnknownError, } from "./telegram-api.js";
8
8
  function asRecord(value) {
9
9
  return value && typeof value === "object" && !Array.isArray(value)
10
10
  ? value
@@ -142,6 +142,7 @@ export function createTelegramSelectedMenuTextApi(deps) {
142
142
  }
143
143
  /** Followers route every chat action through the leader's generic `sendChatAction` call. */
144
144
  function sendTelegramFollowerChatAction(deps, chatId, action, options) {
145
+ assertTelegramApiCallAuthority(options?.assertAuthority, false);
145
146
  const body = withDefaultThreadTarget({
146
147
  chat_id: chatId,
147
148
  action,
@@ -149,7 +150,11 @@ function sendTelegramFollowerChatAction(deps, chatId, action, options) {
149
150
  ? { message_thread_id: options.message_thread_id }
150
151
  : {}),
151
152
  }, deps.getDefaultTarget?.());
152
- return deps.callFollowerApi("call", ["sendChatAction", body]).then(asBoolean);
153
+ // Only plain single-attempt policy crosses IPC; the captured assertion is a local issuance fence.
154
+ const args = ["sendChatAction", body, ...(action === "typing"
155
+ ? [{ maxAttempts: 1, retrySafety: "non-idempotent" }] : [])];
156
+ return deps.callFollowerApi("call", args, options?.assertAuthority
157
+ ? { assertAuthority: options.assertAuthority } : undefined).then(asBoolean);
153
158
  }
154
159
  export function createTelegramBusAwareApiRuntime(deps) {
155
160
  return {
@@ -12,8 +12,10 @@ import { TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY, TELEGRAM_BUS_CAPABILITY_SELE
12
12
  import type { TelegramConfigStore, TelegramThreadDisplayMode } from "./config.ts";
13
13
  import { type TelegramUpdateJournalStoreOptions } from "./journal.ts";
14
14
  import { type TelegramLockEntry, type TelegramLockState } from "./locks.ts";
15
+ import * as Machine from "./machine.ts";
15
16
  import type { TelegramQueueHandoffStageResult } from "./queue.ts";
16
17
  import { type TelegramTarget } from "./target.ts";
18
+ import { type TelegramApiCallOptions } from "./telegram-api.ts";
17
19
  import * as Threads from "./threads.ts";
18
20
  import { type TelegramWorkspaceAdmissionLedger } from "./workspace-admission.ts";
19
21
  export declare const TELEGRAM_BUS_FOLLOWER_HEARTBEAT_TIMEOUT_MS = 8000;
@@ -343,7 +345,9 @@ export declare function createTelegramBusFollowerWorkspaceRestoreHandler<TContex
343
345
  expectedTarget?: TelegramTarget & {
344
346
  threadId: number;
345
347
  };
346
- release?: (canRelease: () => boolean) => void;
348
+ release?: (canRelease: () => boolean, target: TelegramTarget & {
349
+ threadId: number;
350
+ }) => void;
347
351
  observeWork?: (oldTarget: TelegramTarget & {
348
352
  threadId: number;
349
353
  }) => void;
@@ -362,7 +366,7 @@ export declare function createTelegramBusFollowerWorkspaceRestoreHandler<TContex
362
366
  };
363
367
  export declare function createTelegramBusFollowerClientRuntime<TContext, TReactionUpdate, TCallbackQuery, TMessage = unknown>(deps: TelegramBusFollowerClientRuntimeDeps<TMessage>): {
364
368
  createRequestId: () => string;
365
- callApi: (method: string, args: unknown[]) => Promise<unknown>;
369
+ callApi: (method: string, args: unknown[], localOptions?: Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<unknown>;
366
370
  agentMessages: {
367
371
  resolveTarget: (selector: TelegramBusAgentTargetSelector) => Promise<TelegramTarget & {
368
372
  threadId: number;
@@ -414,7 +418,7 @@ export declare function createTelegramBusFollowerSelectedMenuCaller<TContext>(de
414
418
  effect: TelegramBusSelectedMenuTextEffect;
415
419
  assertAuthority: () => void;
416
420
  }) => Promise<TelegramBusSelectedMenuDeliveryObservation>;
417
- export declare function createTelegramBusFollowerApiCaller(deps: TelegramBusFollowerApiCallerDeps): (method: string, args: unknown[]) => Promise<unknown>;
421
+ export declare function createTelegramBusFollowerApiCaller(deps: TelegramBusFollowerApiCallerDeps): (method: string, args: unknown[], localOptions?: Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<unknown>;
418
422
  export declare function createTelegramBusFollowerSessionReplacementSuspender(deps: TelegramBusFollowerSessionReplacementSuspenderDeps): (preserveTarget?: boolean) => Promise<void>;
419
423
  export declare function createTelegramBusFollowerSessionRefreshHook<TContext>(deps: TelegramBusFollowerSessionRefreshHookDeps<TContext>): (_event: unknown, ctx: TContext) => Promise<void>;
420
424
  export declare function createTelegramBusFollowerControlState(): TelegramBusFollowerControlState;
@@ -426,6 +430,48 @@ export type TelegramBusFollowerHeartbeatRecoveryHandler<TContext> = ((error: unk
426
430
  halt: () => void;
427
431
  resume: () => void;
428
432
  };
433
+ /** The whole recovery lifecycle state; only `transitionTelegramBusFollowerRecovery` produces a new one. */
434
+ export interface TelegramBusFollowerRecoveryState {
435
+ /** Bumped by halt and resume, so a recovery or retry from before an operator stop or connect is inert. */
436
+ readonly epoch: number;
437
+ readonly halted: boolean;
438
+ /** One recovery runs at a time; it belongs to the epoch it started in. */
439
+ readonly recovering: boolean;
440
+ readonly retryId: number | null;
441
+ readonly nextRetryId: number;
442
+ }
443
+ export type TelegramBusFollowerRecoveryEvent = {
444
+ type: "recoverRequested";
445
+ } | {
446
+ type: "recoverySettled";
447
+ epoch: number;
448
+ } | {
449
+ type: "retryRequested";
450
+ epoch: number;
451
+ } | {
452
+ type: "retryFired";
453
+ retryId: number;
454
+ } | {
455
+ type: "halted";
456
+ } | {
457
+ type: "resumed";
458
+ };
459
+ export type TelegramBusFollowerRecoveryEffect = {
460
+ type: "runRecovery";
461
+ epoch: number;
462
+ } | {
463
+ type: "armRetry";
464
+ retryId: number;
465
+ } | {
466
+ type: "cancelRetry";
467
+ retryId: number;
468
+ };
469
+ export declare function createTelegramBusFollowerRecoveryState(): TelegramBusFollowerRecoveryState;
470
+ /**
471
+ * Pure follower-recovery lifecycle. An operator stop halts it and a connect resumes it, each in a new epoch: work
472
+ * from the old epoch, a recovery still awaiting or a retry still armed, can no longer register or promote.
473
+ */
474
+ export declare function transitionTelegramBusFollowerRecovery(state: TelegramBusFollowerRecoveryState, event: TelegramBusFollowerRecoveryEvent): Machine.TelegramMachineStep<TelegramBusFollowerRecoveryState, TelegramBusFollowerRecoveryEffect>;
429
475
  export declare function createTelegramBusFollowerHeartbeatRecoveryHandler<TContext>(deps: TelegramBusFollowerHeartbeatRecoveryHandlerDeps<TContext>): TelegramBusFollowerHeartbeatRecoveryHandler<TContext>;
430
476
  export declare function createTelegramBusFollowerRegistrationRuntime<TContext extends {
431
477
  cwd?: string;