@llblab/pi-kit 0.5.1 → 0.5.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.
@@ -353,6 +353,7 @@ export function createTelegramAssistantOutputBindingRuntime<
353
353
  typeof OutboundHandlers.createTelegramAssistantOutputSender<TTransportStamp>
354
354
  >[0];
355
355
  waitForActivityIdle?: () => Promise<void>;
356
+ enqueue?: Activity.TelegramActivityPublicationRuntime["enqueue"];
356
357
  recordRuntimeEvent: TelegramRuntimeEventRecorder;
357
358
  }): TelegramAssistantOutputBindingRuntime<TTransportStamp> {
358
359
  const authority = Routing.createTelegramAssistantOutputAuthorityRuntime(
@@ -364,6 +365,7 @@ export function createTelegramAssistantOutputBindingRuntime<
364
365
  );
365
366
  const runtime = Activity.createTelegramAssistantOutputRuntime({
366
367
  ...authority,
368
+ enqueue: deps.enqueue,
367
369
  async send(event, authority, isAuthorityActive) {
368
370
  await deps.waitForActivityIdle?.();
369
371
  if (!isAuthorityActive()) return;
@@ -390,7 +392,14 @@ type TelegramAssistantOutputAuthority<TTransportStamp> = ReturnType<
390
392
  Routing.TelegramAssistantOutputAuthorityRuntime<TTransportStamp>["captureAuthority"]
391
393
  >;
392
394
 
395
+ export interface TelegramBridgePublicationRuntime {
396
+ enqueue: Activity.TelegramActivityPublicationRuntime["enqueue"];
397
+ reserve: Activity.TelegramActivityPublicationRuntime["reserve"];
398
+ capture: () => { target?: Queue.TelegramQueueTarget; isCurrent: () => boolean };
399
+ }
400
+
393
401
  export interface TelegramActivityBindingRuntime {
402
+ publicationRuntime: TelegramBridgePublicationRuntime;
394
403
  activityRuntime: Activity.TelegramActivityRuntime;
395
404
  activityVerbosityRuntime: ActivityVerbosity.TelegramActivityVerbosityRuntime;
396
405
  assistantOutputRuntime: Activity.TelegramAssistantOutputRuntime;
@@ -403,7 +412,7 @@ export function createTelegramActivityBindingRuntime<TTransportStamp>(deps: {
403
412
  Parameters<
404
413
  typeof createTelegramAssistantOutputBindingRuntime<TTransportStamp>
405
414
  >[0],
406
- "waitForActivityIdle"
415
+ "waitForActivityIdle" | "enqueue"
407
416
  >;
408
417
  activityVerbosity: Omit<
409
418
  Parameters<
@@ -411,19 +420,19 @@ export function createTelegramActivityBindingRuntime<TTransportStamp>(deps: {
411
420
  TelegramAssistantOutputAuthority<TTransportStamp>
412
421
  >
413
422
  >[0],
414
- "captureAuthority" | "isAuthorityActive" | "recordFailure"
423
+ "captureAuthority" | "isAuthorityActive" | "recordFailure" | "enqueue"
415
424
  >;
416
425
  }): TelegramActivityBindingRuntime {
417
- const activityVerbosityBinding =
418
- ActivityVerbosity.createTelegramActivityVerbosityBinding();
426
+ const publication = Activity.createTelegramActivityPublicationRuntime();
419
427
  const assistantOutputBinding =
420
428
  createTelegramAssistantOutputBindingRuntime({
421
429
  ...deps.assistantOutput,
422
- waitForActivityIdle: activityVerbosityBinding.waitForIdle,
430
+ enqueue: publication.enqueue,
423
431
  });
424
432
  const activityVerbosityRuntime =
425
433
  ActivityVerbosity.createTelegramActivityVerbosityRuntime({
426
434
  ...deps.activityVerbosity,
435
+ enqueue: publication.enqueue,
427
436
  captureAuthority: assistantOutputBinding.authority.captureAuthority,
428
437
  isAuthorityActive: assistantOutputBinding.authority.isAuthorityActive,
429
438
  recordFailure(operation, event, error) {
@@ -434,7 +443,6 @@ export function createTelegramActivityBindingRuntime<TTransportStamp>(deps: {
434
443
  });
435
444
  },
436
445
  });
437
- activityVerbosityBinding.bind(activityVerbosityRuntime);
438
446
  const activityRuntime = Activity.createTelegramActivityBridgeRuntime({
439
447
  generation: deps.generation,
440
448
  observeEvent(event) {
@@ -450,9 +458,30 @@ export function createTelegramActivityBindingRuntime<TTransportStamp>(deps: {
450
458
  },
451
459
  });
452
460
  return {
453
- activityRuntime,
461
+ activityRuntime: {
462
+ ...activityRuntime,
463
+ onSessionStart() {
464
+ publication.reset();
465
+ activityRuntime.onSessionStart?.();
466
+ },
467
+ onSessionShutdown() {
468
+ publication.reset();
469
+ activityRuntime.onSessionShutdown();
470
+ },
471
+ },
454
472
  activityVerbosityRuntime,
455
473
  assistantOutputRuntime: assistantOutputBinding.runtime,
474
+ publicationRuntime: {
475
+ enqueue: publication.enqueue,
476
+ reserve: publication.reserve,
477
+ capture() {
478
+ const authority = assistantOutputBinding.authority.captureAuthority();
479
+ return {
480
+ target: authority.target ? { ...authority.target } : undefined,
481
+ isCurrent: () => assistantOutputBinding.authority.isAuthorityActive(authority),
482
+ };
483
+ },
484
+ },
456
485
  };
457
486
  }
458
487
 
@@ -680,6 +709,7 @@ export function registerTelegramCommandsAndTools({
680
709
 
681
710
  interface TelegramLifecycleBindingDeps {
682
711
  pi: Pi.ExtensionAPI;
712
+ publicationRuntime: TelegramBridgePublicationRuntime;
683
713
  activityRuntime: Activity.TelegramActivityRuntime;
684
714
  activityVerbosityRuntime?: ActivityVerbosity.TelegramActivityVerbosityRuntime;
685
715
  assistantOutputRuntime: Pick<
@@ -770,6 +800,7 @@ interface TelegramLifecycleBindingDeps {
770
800
 
771
801
  export function registerTelegramLifecycleRuntimeHooks({
772
802
  pi,
803
+ publicationRuntime,
773
804
  activityRuntime,
774
805
  activityVerbosityRuntime,
775
806
  assistantOutputRuntime,
@@ -936,24 +967,19 @@ export function registerTelegramLifecycleRuntimeHooks({
936
967
  { replyToPrompt: false },
937
968
  );
938
969
  };
939
- let activeTurnDeliveryTail = Promise.resolve();
940
- const scheduleActiveTurnDelivery = (task: () => Promise<void>): void => {
941
- const previous = activeTurnDeliveryTail;
942
- activeTurnDeliveryTail = (async () => {
943
- await previous;
944
- await new Promise<void>((resolve) => {
945
- const timer = setTimeout(resolve, 0);
946
- timer.unref?.();
947
- });
948
- await task();
949
- })().catch((error) => {
950
- recordRuntimeEvent("delivery", error, {
951
- phase: "agent-end-background-delivery",
952
- });
953
- });
970
+ let pendingFinalPublication: {
971
+ turn: Queue.PendingTelegramTurn;
972
+ reservation: Activity.TelegramActivityPublicationReservation;
973
+ } | undefined;
974
+ const cancelPendingFinalPublication = (): void => {
975
+ pendingFinalPublication?.reservation.cancel();
976
+ pendingFinalPublication = undefined;
954
977
  };
955
- const waitForActiveTurnDelivery = async (): Promise<void> => {
956
- await activeTurnDeliveryTail;
978
+ const recordPublicationFailure = (error: unknown): void => {
979
+ recordRuntimeEvent("delivery", error, { phase: "agent-end-background-delivery" });
980
+ };
981
+ const scheduleActiveTurnDelivery = (task: () => Promise<void>): void => {
982
+ void publicationRuntime.enqueue(task).catch(recordPublicationFailure);
957
983
  };
958
984
  const agentLifecycleHooks = Queue.createTelegramAgentLifecycleHooks<
959
985
  Queue.PendingTelegramTurn,
@@ -989,14 +1015,22 @@ export function registerTelegramLifecycleRuntimeHooks({
989
1015
  isSessionActive: isSessionContextActive,
990
1016
  isTurnTransportActive,
991
1017
  waitForTypingIdle: typing.waitForIdle,
992
- async waitForActivityIdle() {
993
- await activityVerbosityRuntime?.waitForIdle();
994
- await assistantOutputRuntime.waitForIdle();
995
- },
996
1018
  dispatchNextQueuedTelegramTurn,
997
1019
  requestDeferredDispatchNextQueuedTelegramTurn:
998
1020
  deferredQueueDispatchRuntime.request,
999
1021
  scheduleActiveTurnDelivery,
1022
+ reserveActiveTurnDelivery() {
1023
+ const pending = pendingFinalPublication;
1024
+ pendingFinalPublication = undefined;
1025
+ const matches = pending?.turn === activeTurnRuntime.get();
1026
+ if (!matches) pending?.reservation.cancel();
1027
+ const reservation = matches && pending ? pending.reservation : publicationRuntime.reserve();
1028
+ return {
1029
+ schedule: (task) => { void reservation.publish(task).catch(recordPublicationFailure); },
1030
+ cancel: reservation.cancel,
1031
+ };
1032
+ },
1033
+ preparePreviewClear: previewRuntime.prepareClear,
1000
1034
  clearPreview: previewRuntime.clear,
1001
1035
  setPreviewPendingText: previewRuntime.setPendingText,
1002
1036
  finalizeMarkdownPreview,
@@ -1038,25 +1072,24 @@ export function registerTelegramLifecycleRuntimeHooks({
1038
1072
  };
1039
1073
  let observedAutomaticCompaction = false;
1040
1074
  let agentWorkActive = false;
1041
- let terminalAssistantMessagePendingDelivery = false;
1042
- const deferredAutomaticCompactionNotices: string[] = [];
1043
- const sendCompactionNotice = async (text: string): Promise<void> => {
1075
+ const prepareCompactionNotice = (text: string, ctx: Pi.ExtensionContext): (() => Promise<void>) => {
1076
+ const authority = publicationRuntime.capture();
1044
1077
  const turn = activeTurnRuntime.get();
1045
- const target = turn?.target ?? proactivePushTargetGetter?.();
1046
- if (!target) return;
1047
- try {
1048
- await sendMarkdownReply(target.chatId, turn?.replyToMessageId, text, {
1049
- target,
1050
- });
1051
- } catch (error) {
1052
- recordRuntimeEvent("delivery", error, {
1053
- phase: "compaction-notice",
1054
- });
1055
- }
1078
+ const selectedTarget = turn?.target ?? authority.target;
1079
+ const target = selectedTarget ? { ...selectedTarget } : undefined;
1080
+ const replyToMessageId = turn?.replyToMessageId;
1081
+ return async () => {
1082
+ if (!target || !isSessionContextActive(ctx) || !authority.isCurrent()) return;
1083
+ if (turn && isTurnTransportActive?.(turn) === false) return;
1084
+ try {
1085
+ await sendMarkdownReply(target.chatId, replyToMessageId, text, { target });
1086
+ } catch (error) {
1087
+ recordRuntimeEvent("delivery", error, { phase: "compaction-notice" });
1088
+ }
1089
+ };
1056
1090
  };
1057
- const flushDeferredAutomaticCompactionNotices = async (): Promise<void> => {
1058
- const notices = deferredAutomaticCompactionNotices.splice(0);
1059
- for (const notice of notices) await sendCompactionNotice(notice);
1091
+ const sendCompactionNotice = (text: string, ctx: Pi.ExtensionContext): void => {
1092
+ scheduleActiveTurnDelivery(prepareCompactionNotice(text, ctx));
1060
1093
  };
1061
1094
  const compactionObserver = Lifecycle.createTelegramCompactionObserverRuntime({
1062
1095
  isContextActive: isSessionContextActive,
@@ -1070,7 +1103,6 @@ export function registerTelegramLifecycleRuntimeHooks({
1070
1103
  recordRuntimeEvent,
1071
1104
  onCompactionAbandoned: () => {
1072
1105
  observedAutomaticCompaction = false;
1073
- deferredAutomaticCompactionNotices.length = 0;
1074
1106
  activityRuntime.onCompactionAbandoned();
1075
1107
  },
1076
1108
  });
@@ -1091,6 +1123,7 @@ export function registerTelegramLifecycleRuntimeHooks({
1091
1123
  activityRuntime.recordInputSource(event.source ?? "unknown");
1092
1124
  },
1093
1125
  async onSessionStart(event, ctx) {
1126
+ cancelPendingFinalPublication();
1094
1127
  previewRuntime.invalidate();
1095
1128
  assistantOutputRuntime.start();
1096
1129
  activityRuntime.onSessionStart?.();
@@ -1106,9 +1139,8 @@ export function registerTelegramLifecycleRuntimeHooks({
1106
1139
  assistantOutputRuntime.stop();
1107
1140
  observedAutomaticCompaction = false;
1108
1141
  agentWorkActive = false;
1109
- terminalAssistantMessagePendingDelivery = false;
1142
+ cancelPendingFinalPublication();
1110
1143
  uiPromptActive = false;
1111
- deferredAutomaticCompactionNotices.length = 0;
1112
1144
  compactionObserver.onSessionShutdown();
1113
1145
  if (event.reason === "quit" && disconnectOnQuit) {
1114
1146
  try {
@@ -1129,19 +1161,7 @@ export function registerTelegramLifecycleRuntimeHooks({
1129
1161
  if (shouldNotify) observedAutomaticCompaction = true;
1130
1162
  activityRuntime.onCompactionStart(Pi.getSessionCompactionReason(event));
1131
1163
  compactionObserver.onSessionBeforeCompact(event, ctx);
1132
- if (shouldNotify) {
1133
- if (terminalAssistantMessagePendingDelivery) {
1134
- deferredAutomaticCompactionNotices.push(
1135
- Commands.TELEGRAM_COMPACTION_STARTED_MARKDOWN,
1136
- );
1137
- } else {
1138
- await waitForActiveTurnDelivery();
1139
- if (!isSessionContextActive(ctx)) return;
1140
- await sendCompactionNotice(
1141
- Commands.TELEGRAM_COMPACTION_STARTED_MARKDOWN,
1142
- );
1143
- }
1144
- }
1164
+ if (shouldNotify) sendCompactionNotice(Commands.TELEGRAM_COMPACTION_STARTED_MARKDOWN, ctx);
1145
1165
  },
1146
1166
  async onSessionCompact(event, ctx) {
1147
1167
  if (!isSessionContextActive(ctx)) return;
@@ -1149,38 +1169,23 @@ export function registerTelegramLifecycleRuntimeHooks({
1149
1169
  compactionObserver.onSessionCompact(event, ctx);
1150
1170
  if (observedAutomaticCompaction) {
1151
1171
  observedAutomaticCompaction = false;
1152
- if (deferredAutomaticCompactionNotices.length > 0) {
1153
- deferredAutomaticCompactionNotices.push(
1154
- Commands.TELEGRAM_COMPACTION_COMPLETED_MARKDOWN,
1155
- );
1156
- } else {
1157
- await sendCompactionNotice(
1158
- Commands.TELEGRAM_COMPACTION_COMPLETED_MARKDOWN,
1159
- );
1160
- }
1172
+ sendCompactionNotice(Commands.TELEGRAM_COMPACTION_COMPLETED_MARKDOWN, ctx);
1161
1173
  }
1162
1174
  },
1163
1175
  async onSessionCompactFailed(event, ctx) {
1164
1176
  if (!isSessionContextActive(ctx)) return;
1165
1177
  const shouldNotify = observedAutomaticCompaction;
1166
- const deferredNotices = deferredAutomaticCompactionNotices.splice(0);
1167
- const shouldDefer =
1168
- deferredNotices.length > 0 || terminalAssistantMessagePendingDelivery;
1169
1178
  compactionObserver.onSessionCompactFailed(event, ctx);
1170
1179
  if (!shouldNotify) return;
1171
1180
  const notice = event.aborted
1172
1181
  ? "**⚠️ Compaction cancelled.**"
1173
1182
  : "**⚠️ Compaction failed.**";
1174
- if (shouldDefer) {
1175
- deferredAutomaticCompactionNotices.push(...deferredNotices, notice);
1176
- } else {
1177
- await sendCompactionNotice(notice);
1178
- }
1183
+ sendCompactionNotice(notice, ctx);
1179
1184
  },
1180
1185
  async onAgentStart(event, ctx) {
1181
1186
  if (!isSessionContextActive(ctx)) return;
1182
1187
  agentWorkActive = true;
1183
- terminalAssistantMessagePendingDelivery = false;
1188
+ cancelPendingFinalPublication();
1184
1189
  await agentStartWithDedupReset(event, ctx);
1185
1190
  activityRuntime.onAgentStart(activeTurnRuntime.get()?.target);
1186
1191
  startAgentActivityTypingLoop(ctx);
@@ -1230,11 +1235,13 @@ export function registerTelegramLifecycleRuntimeHooks({
1230
1235
  if (event.message.role === "assistant") {
1231
1236
  activityRuntime.onAssistantMessageEnd(event.message.stopReason);
1232
1237
  }
1233
- terminalAssistantMessagePendingDelivery =
1234
- event.message.role === "assistant" &&
1235
- event.message.stopReason !== "toolUse" &&
1236
- event.message.stopReason !== "error" &&
1237
- event.message.stopReason !== "aborted";
1238
+ if (event.message.role !== "assistant" || event.message.stopReason === "toolUse" || event.message.stopReason === "aborted") return;
1239
+ const turn = activeTurnRuntime.get();
1240
+ if (!turn || turn.guestQueryId || pendingFinalPublication?.turn === turn) return;
1241
+ cancelPendingFinalPublication();
1242
+ const assistant = Replies.extractLatestAssistantMessageText([event.message]);
1243
+ if (!assistant.text && assistant.stopReason !== "error" && turn.queuedAttachments.length === 0) return;
1244
+ pendingFinalPublication = { turn, reservation: publicationRuntime.reserve() };
1238
1245
  },
1239
1246
  onUiPromptStart(event, ctx) {
1240
1247
  if (!isSessionContextActive(ctx)) return;
@@ -1254,18 +1261,22 @@ export function registerTelegramLifecycleRuntimeHooks({
1254
1261
  },
1255
1262
  async onAgentEnd(event, ctx) {
1256
1263
  if (!isSessionContextActive(ctx)) return;
1264
+ if (pendingFinalPublication && pendingFinalPublication.turn !== activeTurnRuntime.get()) {
1265
+ cancelPendingFinalPublication();
1266
+ return;
1267
+ }
1257
1268
  activityRuntime.onAgentEnd();
1258
1269
  await agentLifecycleHooks.onAgentEnd(event, ctx);
1259
1270
  },
1260
1271
  async onAgentSettled(event, ctx) {
1261
1272
  if (!isSessionContextActive(ctx)) return;
1262
- await agentLifecycleHooks.onAgentSettled(event, ctx);
1263
- if (deferredAutomaticCompactionNotices.length > 0) {
1264
- await waitForActiveTurnDelivery();
1265
- if (!isSessionContextActive(ctx)) return;
1266
- await flushDeferredAutomaticCompactionNotices();
1273
+ const pending = pendingFinalPublication;
1274
+ try {
1275
+ await agentLifecycleHooks.onAgentSettled(event, ctx);
1276
+ } finally {
1277
+ if (pendingFinalPublication === pending) cancelPendingFinalPublication();
1267
1278
  }
1268
- terminalAssistantMessagePendingDelivery = false;
1279
+ if (!isSessionContextActive(ctx)) return;
1269
1280
  agentWorkActive = false;
1270
1281
  activityRuntime.onAgentSettled();
1271
1282
  modelContextAvailabilityRuntime.reconcile();
@@ -450,8 +450,10 @@ export function createTelegramCompactionObserverRuntime<TContext>(
450
450
  const setTimer = deps.setTimer ?? setTimeout;
451
451
  const clearTimer = deps.clearTimer ?? clearTimeout;
452
452
  let fallbackTimer: TelegramLifecycleTimer | undefined;
453
+ let observationGeneration = 0;
453
454
  let typingStartedByObserver = false;
454
455
  const clearFallbackTimer = (): void => {
456
+ observationGeneration += 1;
455
457
  if (!fallbackTimer) return;
456
458
  clearTimer(fallbackTimer);
457
459
  fallbackTimer = undefined;
@@ -470,7 +472,10 @@ export function createTelegramCompactionObserverRuntime<TContext>(
470
472
  !!deps.startTypingLoop && typingStartResult !== false;
471
473
  deps.updateStatus(ctx);
472
474
  clearFallbackTimer();
475
+ const admittedGeneration = observationGeneration;
473
476
  fallbackTimer = setTimer(() => {
477
+ if (observationGeneration !== admittedGeneration) return;
478
+ observationGeneration += 1;
474
479
  fallbackTimer = undefined;
475
480
  if (deps.isContextActive && !deps.isContextActive(ctx)) return;
476
481
  deps.setCompactionInProgress(false);
@@ -487,8 +492,8 @@ export function createTelegramCompactionObserverRuntime<TContext>(
487
492
  unrefTelegramLifecycleTimer(fallbackTimer);
488
493
  },
489
494
  onSessionCompact: (_event, ctx) => {
490
- clearFallbackTimer();
491
495
  if (deps.isContextActive && !deps.isContextActive(ctx)) return;
496
+ clearFallbackTimer();
492
497
  deps.setCompactionInProgress(false);
493
498
  if (typingStartedByObserver) deps.stopTypingLoop?.();
494
499
  typingStartedByObserver = false;
@@ -496,8 +501,8 @@ export function createTelegramCompactionObserverRuntime<TContext>(
496
501
  requestDispatch();
497
502
  },
498
503
  onSessionCompactFailed: (_event, ctx) => {
499
- clearFallbackTimer();
500
504
  if (deps.isContextActive && !deps.isContextActive(ctx)) return;
505
+ clearFallbackTimer();
501
506
  deps.setCompactionInProgress(false);
502
507
  if (typingStartedByObserver) deps.stopTypingLoop?.();
503
508
  typingStartedByObserver = false;
@@ -219,8 +219,9 @@ export function createTelegramRichOutboundAttachmentSender(
219
219
  return async (
220
220
  turn: TelegramQueuedOutboundAttachmentTurnView,
221
221
  markdown: string,
222
- options?: { replyMarkup?: unknown },
222
+ options?: { replyMarkup?: unknown; isDeliveryActive?: () => boolean },
223
223
  ): Promise<boolean> => {
224
+ if (options?.isDeliveryActive?.() === false) return false;
224
225
  const plan = planTelegramRichOutboundAttachment({
225
226
  turn,
226
227
  markdown,
@@ -246,11 +247,13 @@ export function createTelegramRichOutboundAttachmentSender(
246
247
  new Error("Successful Rich media upload omitted message_id."),
247
248
  );
248
249
  }
249
- deps.recordOwnership?.({
250
- chatId: turn.chatId,
251
- messageId,
252
- target: turn.target,
253
- });
250
+ if (options?.isDeliveryActive?.() !== false) {
251
+ deps.recordOwnership?.({
252
+ chatId: turn.chatId,
253
+ messageId,
254
+ target: turn.target,
255
+ });
256
+ }
254
257
  return true;
255
258
  } catch (error) {
256
259
  if (isTelegramRichAttachmentCommitUnknownError(error)) throw error;
@@ -580,6 +583,7 @@ export interface TelegramQueuedOutboundAttachmentDeliveryDeps {
580
583
  ) => void;
581
584
  statPath?: (path: string) => Promise<{ size: number }>;
582
585
  maxAttachmentSizeBytes?: number;
586
+ isDeliveryActive?: () => boolean;
583
587
  }
584
588
 
585
589
  export async function queueTelegramOutboundAttachments(options: {
@@ -925,9 +929,13 @@ export async function sendTelegramOutboundFiles(options: {
925
929
  export function createTelegramQueuedOutboundAttachmentSender(
926
930
  deps: TelegramQueuedOutboundAttachmentDeliveryDeps,
927
931
  ) {
928
- return async (turn: TelegramQueuedOutboundAttachmentTurnView): Promise<void> => {
932
+ return async (
933
+ turn: TelegramQueuedOutboundAttachmentTurnView,
934
+ options?: { isDeliveryActive?: () => boolean },
935
+ ): Promise<void> => {
929
936
  await sendQueuedTelegramOutboundAttachments(turn, {
930
937
  ...deps,
938
+ isDeliveryActive: () => deps.isDeliveryActive?.() !== false && options?.isDeliveryActive?.() !== false,
931
939
  maxAttachmentSizeBytes:
932
940
  deps.maxAttachmentSizeBytes ?? TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES,
933
941
  });
@@ -939,9 +947,11 @@ export async function sendQueuedTelegramOutboundAttachments(
939
947
  deps: TelegramQueuedOutboundAttachmentDeliveryDeps,
940
948
  ): Promise<void> {
941
949
  for (const attachment of turn.queuedAttachments) {
950
+ if (deps.isDeliveryActive?.() === false) return;
942
951
  try {
943
952
  if (deps.maxAttachmentSizeBytes !== undefined) {
944
953
  const stats = await (deps.statPath ?? stat)(attachment.path);
954
+ if (deps.isDeliveryActive?.() === false) return;
945
955
  if (stats.size > deps.maxAttachmentSizeBytes) {
946
956
  throw new Error(
947
957
  formatTelegramOutboundAttachmentSizeLimitError(
@@ -971,6 +981,7 @@ export async function sendQueuedTelegramOutboundAttachments(
971
981
  attachment.fileName,
972
982
  );
973
983
  } catch (error) {
984
+ if (deps.isDeliveryActive?.() === false) return;
974
985
  const message = error instanceof Error ? error.message : String(error);
975
986
  deps.recordRuntimeEvent?.("attachment", error, {
976
987
  fileName: attachment.fileName,
@@ -14,6 +14,7 @@ import {
14
14
  type TelegramTarget,
15
15
  } from "./target.ts";
16
16
  import { getTelegramVoiceSynthesisProviders } from "./voice.ts";
17
+ import { isTelegramApiCommitUnknownError } from "./telegram-api.ts";
17
18
 
18
19
  export interface TelegramVoiceReplyTurnView {
19
20
  chatId: number;
@@ -47,6 +48,7 @@ export interface TelegramVoiceReplySenderDeps {
47
48
  ) => Promise<unknown>;
48
49
  sendChatAction?: (chatId: number, action: string) => Promise<unknown>;
49
50
  sendRecordVoiceAction?: (chatId: number) => Promise<unknown>;
51
+ isDeliveryActive?: () => boolean;
50
52
  getHandlers?: () => unknown[] | undefined;
51
53
  cwd?: string;
52
54
  tempDir?: string;
@@ -123,9 +125,12 @@ export function createTelegramVoiceReplySender<THandler = unknown>(
123
125
  replyMarkup?: unknown;
124
126
  },
125
127
  ): Promise<void> => {
128
+ if (deps.isDeliveryActive?.() === false) return;
126
129
  const voiceFilePath = await ensureTelegramVoiceFileFormat(filePath);
127
130
  assertTelegramInlineKeyboardCallbackData(options?.replyMarkup);
131
+ if (deps.isDeliveryActive?.() === false) return;
128
132
  await sendVoiceChatAction(deps, turn.chatId);
133
+ if (deps.isDeliveryActive?.() === false) return;
129
134
  const replyParameters = buildVoiceReplyParameters(
130
135
  turn.chatId,
131
136
  options?.replyToPrompt,
@@ -171,6 +176,7 @@ export function createTelegramVoiceReplySender<THandler = unknown>(
171
176
  ): Promise<void> => {
172
177
  for (const handler of ports.findVoiceHandlers?.(deps.getHandlers?.()) ??
173
178
  []) {
179
+ if (deps.isDeliveryActive?.() === false) return;
174
180
  try {
175
181
  const filePath = await ports.generateVoiceFile?.(text, {
176
182
  lang: options?.lang,
@@ -187,6 +193,7 @@ export function createTelegramVoiceReplySender<THandler = unknown>(
187
193
  });
188
194
  return;
189
195
  } catch (error) {
196
+ if (isTelegramApiCommitUnknownError(error)) throw error;
190
197
  deps.recordRuntimeEvent?.("voice", error, {
191
198
  phase: "template-handler-send",
192
199
  });
@@ -194,6 +201,7 @@ export function createTelegramVoiceReplySender<THandler = unknown>(
194
201
  }
195
202
 
196
203
  for (const handler of ports.getProgrammaticVoiceHandlers?.() ?? []) {
204
+ if (deps.isDeliveryActive?.() === false) return;
197
205
  try {
198
206
  const filePath = await handler(text, {
199
207
  lang: options?.lang,
@@ -206,6 +214,7 @@ export function createTelegramVoiceReplySender<THandler = unknown>(
206
214
  });
207
215
  return;
208
216
  } catch (error) {
217
+ if (isTelegramApiCommitUnknownError(error)) throw error;
209
218
  deps.recordRuntimeEvent?.("voice", error, {
210
219
  phase: "programmatic-handler-send",
211
220
  });
@@ -215,6 +224,7 @@ export function createTelegramVoiceReplySender<THandler = unknown>(
215
224
  const providers = getTelegramVoiceSynthesisProviders();
216
225
 
217
226
  for (const provider of providers) {
227
+ if (deps.isDeliveryActive?.() === false) return;
218
228
  let voiceFilePath: string | undefined;
219
229
  let originalFilePath: string | undefined;
220
230
 
@@ -252,6 +262,7 @@ export function createTelegramVoiceReplySender<THandler = unknown>(
252
262
  });
253
263
  return;
254
264
  } catch (error) {
265
+ if (isTelegramApiCommitUnknownError(error)) throw error;
255
266
  deps.recordRuntimeEvent?.("voice", error, { phase: "send" });
256
267
  } finally {
257
268
  if (voiceFilePath && voiceFilePath !== originalFilePath) {
@@ -11,6 +11,7 @@ import { join } from "node:path";
11
11
  import type { TelegramAssistantSegmentEvent } from "./activity.ts";
12
12
  import { resolveTelegramTempDir } from "./paths.ts";
13
13
  import * as Replies from "./replies.ts";
14
+ import { isTelegramApiCommitUnknownError } from "./telegram-api.ts";
14
15
  import type {
15
16
  TelegramEditMessageTextBody,
16
17
  TelegramSendMessageBody,
@@ -137,6 +138,7 @@ export interface TelegramVoiceReplySenderDeps {
137
138
  ) => Promise<unknown>;
138
139
  sendChatAction?: (chatId: number, action: string) => Promise<unknown>;
139
140
  sendRecordVoiceAction?: (chatId: number) => Promise<unknown>;
141
+ isDeliveryActive?: () => boolean;
140
142
  getHandlers?: () => TelegramOutboundHandlerConfig[] | undefined;
141
143
  cwd?: string;
142
144
  tempDir?: string;
@@ -877,15 +879,17 @@ export function createTelegramOutboundReplyPlanner(
877
879
  export function createTelegramOutboundReplyArtifactSender(
878
880
  deps: TelegramVoiceReplySenderDeps,
879
881
  ) {
880
- const sendVoiceReply = createTelegramVoiceReplySender(deps);
881
882
  return async (
882
883
  turn: TelegramVoiceReplyTurnView,
883
884
  plan: Pick<
884
885
  TelegramOutboundReplyPlan,
885
886
  "voiceText" | "voiceReplies" | "lang" | "rate" | "replyMarkup"
886
887
  >,
887
- options?: { replyToPrompt?: boolean },
888
+ options?: { replyToPrompt?: boolean; isDeliveryActive?: () => boolean },
888
889
  ): Promise<void> => {
890
+ const isDeliveryActive = () =>
891
+ deps.isDeliveryActive?.() !== false && options?.isDeliveryActive?.() !== false;
892
+ const sendVoiceReply = createTelegramVoiceReplySender({ ...deps, isDeliveryActive });
889
893
  // Normalize voice replies: either use explicit voiceReplies array or fall back to voiceText
890
894
  const voiceReplies = plan.voiceReplies?.length
891
895
  ? plan.voiceReplies
@@ -896,6 +900,7 @@ export function createTelegramOutboundReplyArtifactSender(
896
900
  let anyDelivered = false;
897
901
 
898
902
  for (const reply of voiceReplies) {
903
+ if (!isDeliveryActive()) return;
899
904
  try {
900
905
  await sendVoiceReply(turn, reply.text, {
901
906
  lang: reply.lang ?? plan.lang,
@@ -905,11 +910,13 @@ export function createTelegramOutboundReplyArtifactSender(
905
910
  replyMarkup: !anyDelivered ? plan.replyMarkup : undefined,
906
911
  });
907
912
  anyDelivered = true;
908
- } catch {
913
+ } catch (error) {
914
+ if (isTelegramApiCommitUnknownError(error)) throw error;
909
915
  // sendVoiceReply already recorded the error; continue to next reply
910
916
  }
911
917
  }
912
918
 
919
+ if (!isDeliveryActive()) return;
913
920
  if (!anyDelivered) {
914
921
  throw new Error(
915
922
  "Failed to send voice reply: every voice synthesis provider failed.",
@@ -140,6 +140,10 @@ export interface TelegramPreviewControllerDeps {
140
140
  }
141
141
 
142
142
  export interface TelegramPreviewController {
143
+ prepareClear: (
144
+ chatId: number,
145
+ options?: { target?: TelegramTarget; isDeliveryActive?: () => boolean },
146
+ ) => () => Promise<void>;
143
147
  getState: () => TelegramPreviewRuntimeState | undefined;
144
148
  setState: (state: TelegramPreviewRuntimeState | undefined) => void;
145
149
  setPendingText: (text: string) => void;
@@ -322,6 +326,17 @@ export function createTelegramPreviewController(
322
326
  generation += 1;
323
327
  state = undefined;
324
328
  },
329
+ prepareClear: (chatId, options) => {
330
+ const admittedState = state;
331
+ const runtime = getRuntimeDeps();
332
+ return async () => {
333
+ if (state !== admittedState) return;
334
+ await clearTelegramPreview(chatId, runtime, {
335
+ ...options,
336
+ isDeliveryActive: () => runtime.canSend?.() !== false && options?.isDeliveryActive?.() !== false,
337
+ });
338
+ };
339
+ },
325
340
  clear: (chatId, options) =>
326
341
  clearTelegramPreview(chatId, getRuntimeDeps(), options),
327
342
  flush: (chatId, options) =>
@@ -454,17 +469,18 @@ export function shouldUseTelegramDraftPreview(_options: {
454
469
  export async function clearTelegramPreview(
455
470
  chatId: number,
456
471
  deps: TelegramPreviewRuntimeDeps,
457
- options: { awaitFlush?: boolean; target?: TelegramTarget } = {},
472
+ options: { awaitFlush?: boolean; target?: TelegramTarget; isDeliveryActive?: () => boolean } = {},
458
473
  ): Promise<void> {
459
474
  const state = deps.getState();
460
- if (!state) return;
475
+ if (!state || options.isDeliveryActive?.() === false) return;
461
476
  if (state.flushPromise && options.awaitFlush !== false) {
462
477
  state.flushRequested = false;
463
478
  await state.flushPromise.catch(() => {});
464
479
  if (deps.getState() !== state) return;
465
480
  }
481
+ if (options.isDeliveryActive?.() === false) return;
466
482
  deps.setState(undefined);
467
- if (state.mode === "draft" && state.draftId !== undefined) {
483
+ if (state.mode === "draft" && state.draftId !== undefined && deps.canSend?.() !== false) {
468
484
  try {
469
485
  await deps.sendDraft(chatId, state.draftId, undefined, {
470
486
  ...getTelegramTargetThreadParams(options.target ?? { chatId }),