@llblab/pi-telegram 0.43.1 → 0.44.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.
package/lib/activity.ts CHANGED
@@ -32,6 +32,7 @@ export interface TelegramActivityEnvelope {
32
32
  sequence: number;
33
33
  source: TelegramActivitySource;
34
34
  target?: TelegramActivityTarget;
35
+ replyToMessageId?: number;
35
36
  timestamp: number;
36
37
  }
37
38
 
@@ -422,8 +423,8 @@ export function createTelegramActivityBridgeRuntime(deps: {
422
423
  recordInputSource(source) {
423
424
  getRuntime()?.recordInputSource(source);
424
425
  },
425
- onAgentStart(target) {
426
- getRuntime()?.onAgentStart(target);
426
+ onAgentStart(target, replyToMessageId) {
427
+ getRuntime()?.onAgentStart(target, replyToMessageId);
427
428
  },
428
429
  onAssistantEvent(event) {
429
430
  getRuntime()?.onAssistantEvent(event);
@@ -492,7 +493,7 @@ export type TelegramAssistantStreamEvent =
492
493
  export interface TelegramActivityRuntime {
493
494
  onSessionStart?: () => void;
494
495
  recordInputSource: (source: TelegramActivityInputSource) => void;
495
- onAgentStart: (activeTelegramTarget?: TelegramActivityTarget) => void;
496
+ onAgentStart: (activeTelegramTarget?: TelegramActivityTarget, replyToMessageId?: number) => void;
496
497
  onAssistantEvent: (event: TelegramAssistantStreamEvent) => void;
497
498
  onAssistantMessageEnd: (stopReason?: string) => void;
498
499
  onToolStart: (event: {
@@ -549,6 +550,7 @@ export function createTelegramActivityRuntime(deps: {
549
550
  let activityId: string | undefined;
550
551
  let activitySource: TelegramActivitySource = "unknown";
551
552
  let activityTarget: TelegramActivityTarget | undefined;
553
+ let activityReplyToMessageId: number | undefined;
552
554
  let sequence = 0;
553
555
  let pendingInputSource: TelegramActivityInputSource = "unknown";
554
556
  let pendingAssistantSegment: PendingAssistantSegment | undefined;
@@ -584,6 +586,7 @@ export function createTelegramActivityRuntime(deps: {
584
586
  sequence,
585
587
  source: activitySource,
586
588
  ...(activityTarget ? { target: activityTarget } : {}),
589
+ ...(activityReplyToMessageId !== undefined ? { replyToMessageId: activityReplyToMessageId } : {}),
587
590
  timestamp: now(),
588
591
  } as TelegramActivityEvent;
589
592
  try {
@@ -610,6 +613,7 @@ export function createTelegramActivityRuntime(deps: {
610
613
  activityId = undefined;
611
614
  activitySource = "unknown";
612
615
  activityTarget = undefined;
616
+ activityReplyToMessageId = undefined;
613
617
  sequence = 0;
614
618
  pendingAssistantSegment = undefined;
615
619
  compactionInProgress = false;
@@ -627,9 +631,10 @@ export function createTelegramActivityRuntime(deps: {
627
631
  recordInputSource(source) {
628
632
  pendingInputSource = source;
629
633
  },
630
- onAgentStart(activeTelegramTarget) {
634
+ onAgentStart(activeTelegramTarget, replyToMessageId) {
631
635
  abandonCompaction();
632
636
  ensureActivity(activeTelegramTarget);
637
+ activityReplyToMessageId = activitySource === "telegram" ? replyToMessageId : undefined;
633
638
  emit({ type: "agent-start" });
634
639
  },
635
640
  onAssistantEvent(event) {
@@ -738,6 +743,63 @@ export function createTelegramActivityRuntime(deps: {
738
743
  };
739
744
  }
740
745
 
746
+ // --- Ordered Bridge-Owned Publication ---
747
+
748
+ export interface TelegramActivityPublicationReservation {
749
+ publish: (task: () => Promise<void>) => Promise<void>;
750
+ cancel: () => void;
751
+ }
752
+
753
+ export interface TelegramActivityPublicationRuntime {
754
+ enqueue: (task: () => Promise<void>) => Promise<void>;
755
+ reserve: () => TelegramActivityPublicationReservation;
756
+ reset: () => void;
757
+ }
758
+
759
+ export function createTelegramActivityPublicationRuntime(): TelegramActivityPublicationRuntime {
760
+ let generation = 0;
761
+ let tail = Promise.resolve();
762
+ const pending = new Set<() => void>();
763
+ const reserve = (): TelegramActivityPublicationReservation => {
764
+ const admittedGeneration = generation;
765
+ let state: "pending" | "published" | "cancelled" = "pending";
766
+ let resolve!: (task: (() => Promise<void>) | undefined) => void;
767
+ const ready = new Promise<(() => Promise<void>) | undefined>((accept) => { resolve = accept; });
768
+ const cancel = () => {
769
+ if (state !== "pending") return;
770
+ state = "cancelled";
771
+ pending.delete(cancel);
772
+ resolve(undefined);
773
+ };
774
+ pending.add(cancel);
775
+ const result = tail.then(async () => {
776
+ const task = await ready;
777
+ if (admittedGeneration === generation && task) await task();
778
+ });
779
+ tail = result.catch(() => {});
780
+ return {
781
+ publish(task) {
782
+ if (state === "cancelled") return result;
783
+ if (state === "published") return Promise.reject(new Error("Publication reservation already published."));
784
+ state = "published";
785
+ pending.delete(cancel);
786
+ resolve(task);
787
+ return result;
788
+ },
789
+ cancel,
790
+ };
791
+ };
792
+ return {
793
+ reserve,
794
+ enqueue: (task) => reserve().publish(task),
795
+ reset() {
796
+ generation += 1;
797
+ for (const cancel of pending) cancel();
798
+ tail = Promise.resolve();
799
+ },
800
+ };
801
+ }
802
+
741
803
  // --- Public Assistant Output Projection ---
742
804
 
743
805
  export interface TelegramAssistantOutputRuntime {
@@ -747,7 +809,14 @@ export interface TelegramAssistantOutputRuntime {
747
809
  stop: () => void;
748
810
  }
749
811
 
812
+ export interface TelegramAssistantOutputPreparation {
813
+ wait: () => Promise<void>;
814
+ settle: () => void;
815
+ }
816
+
750
817
  export function createTelegramAssistantOutputRuntime<TAuthority = undefined>(deps: {
818
+ prepareSend?: (event: TelegramAssistantSegmentEvent) => TelegramAssistantOutputPreparation | undefined;
819
+ enqueue?: TelegramActivityPublicationRuntime["enqueue"];
751
820
  captureAuthority?: () => TAuthority;
752
821
  isAuthorityActive?: (authority: TAuthority) => boolean;
753
822
  canDeliver: (event: TelegramAssistantSegmentEvent) => boolean;
@@ -785,7 +854,9 @@ export function createTelegramAssistantOutputRuntime<TAuthority = undefined>(dep
785
854
  admitted.add(key);
786
855
  const admittedGeneration = generation;
787
856
  const admittedAuthority = deps.captureAuthority?.();
788
- tail = tail.then(async () => {
857
+ const preparation = deps.prepareSend?.(event);
858
+ const enqueue = deps.enqueue ?? ((task: () => Promise<void>) => tail.then(task));
859
+ tail = enqueue(async () => {
789
860
  const isAdmittedAuthorityActive = () =>
790
861
  running &&
791
862
  generation === admittedGeneration &&
@@ -794,6 +865,8 @@ export function createTelegramAssistantOutputRuntime<TAuthority = undefined>(dep
794
865
  deps.isAuthorityActive(admittedAuthority as TAuthority));
795
866
  if (!isAdmittedAuthorityActive() || !deps.canDeliver(event)) return;
796
867
  try {
868
+ if (preparation) await preparation.wait();
869
+ if (!isAdmittedAuthorityActive() || !deps.canDeliver(event)) return;
797
870
  await deps.send(
798
871
  event,
799
872
  admittedAuthority as TAuthority,
@@ -802,7 +875,7 @@ export function createTelegramAssistantOutputRuntime<TAuthority = undefined>(dep
802
875
  } catch (error) {
803
876
  deps.recordFailure?.(event, error);
804
877
  }
805
- });
878
+ }).finally(() => preparation?.settle());
806
879
  },
807
880
  waitForIdle() {
808
881
  return tail;
package/lib/bindings.ts CHANGED
@@ -353,6 +353,8 @@ export function createTelegramAssistantOutputBindingRuntime<
353
353
  typeof OutboundHandlers.createTelegramAssistantOutputSender<TTransportStamp>
354
354
  >[0];
355
355
  waitForActivityIdle?: () => Promise<void>;
356
+ prepareTelegramPreview?: () => Activity.TelegramAssistantOutputPreparation | undefined;
357
+ enqueue?: Activity.TelegramActivityPublicationRuntime["enqueue"];
356
358
  recordRuntimeEvent: TelegramRuntimeEventRecorder;
357
359
  }): TelegramAssistantOutputBindingRuntime<TTransportStamp> {
358
360
  const authority = Routing.createTelegramAssistantOutputAuthorityRuntime(
@@ -364,6 +366,8 @@ export function createTelegramAssistantOutputBindingRuntime<
364
366
  );
365
367
  const runtime = Activity.createTelegramAssistantOutputRuntime({
366
368
  ...authority,
369
+ enqueue: deps.enqueue,
370
+ prepareSend: (event) => event.source === "telegram" ? deps.prepareTelegramPreview?.() : undefined,
367
371
  async send(event, authority, isAuthorityActive) {
368
372
  await deps.waitForActivityIdle?.();
369
373
  if (!isAuthorityActive()) return;
@@ -390,7 +394,14 @@ type TelegramAssistantOutputAuthority<TTransportStamp> = ReturnType<
390
394
  Routing.TelegramAssistantOutputAuthorityRuntime<TTransportStamp>["captureAuthority"]
391
395
  >;
392
396
 
397
+ export interface TelegramBridgePublicationRuntime {
398
+ enqueue: Activity.TelegramActivityPublicationRuntime["enqueue"];
399
+ reserve: Activity.TelegramActivityPublicationRuntime["reserve"];
400
+ capture: () => { target?: Queue.TelegramQueueTarget; isCurrent: () => boolean };
401
+ }
402
+
393
403
  export interface TelegramActivityBindingRuntime {
404
+ publicationRuntime: TelegramBridgePublicationRuntime;
394
405
  activityRuntime: Activity.TelegramActivityRuntime;
395
406
  activityVerbosityRuntime: ActivityVerbosity.TelegramActivityVerbosityRuntime;
396
407
  assistantOutputRuntime: Activity.TelegramAssistantOutputRuntime;
@@ -403,7 +414,7 @@ export function createTelegramActivityBindingRuntime<TTransportStamp>(deps: {
403
414
  Parameters<
404
415
  typeof createTelegramAssistantOutputBindingRuntime<TTransportStamp>
405
416
  >[0],
406
- "waitForActivityIdle"
417
+ "waitForActivityIdle" | "enqueue"
407
418
  >;
408
419
  activityVerbosity: Omit<
409
420
  Parameters<
@@ -411,19 +422,19 @@ export function createTelegramActivityBindingRuntime<TTransportStamp>(deps: {
411
422
  TelegramAssistantOutputAuthority<TTransportStamp>
412
423
  >
413
424
  >[0],
414
- "captureAuthority" | "isAuthorityActive" | "recordFailure"
425
+ "captureAuthority" | "isAuthorityActive" | "recordFailure" | "enqueue"
415
426
  >;
416
427
  }): TelegramActivityBindingRuntime {
417
- const activityVerbosityBinding =
418
- ActivityVerbosity.createTelegramActivityVerbosityBinding();
428
+ const publication = Activity.createTelegramActivityPublicationRuntime();
419
429
  const assistantOutputBinding =
420
430
  createTelegramAssistantOutputBindingRuntime({
421
431
  ...deps.assistantOutput,
422
- waitForActivityIdle: activityVerbosityBinding.waitForIdle,
432
+ enqueue: publication.enqueue,
423
433
  });
424
434
  const activityVerbosityRuntime =
425
435
  ActivityVerbosity.createTelegramActivityVerbosityRuntime({
426
436
  ...deps.activityVerbosity,
437
+ enqueue: publication.enqueue,
427
438
  captureAuthority: assistantOutputBinding.authority.captureAuthority,
428
439
  isAuthorityActive: assistantOutputBinding.authority.isAuthorityActive,
429
440
  recordFailure(operation, event, error) {
@@ -434,7 +445,6 @@ export function createTelegramActivityBindingRuntime<TTransportStamp>(deps: {
434
445
  });
435
446
  },
436
447
  });
437
- activityVerbosityBinding.bind(activityVerbosityRuntime);
438
448
  const activityRuntime = Activity.createTelegramActivityBridgeRuntime({
439
449
  generation: deps.generation,
440
450
  observeEvent(event) {
@@ -450,9 +460,30 @@ export function createTelegramActivityBindingRuntime<TTransportStamp>(deps: {
450
460
  },
451
461
  });
452
462
  return {
453
- activityRuntime,
463
+ activityRuntime: {
464
+ ...activityRuntime,
465
+ onSessionStart() {
466
+ publication.reset();
467
+ activityRuntime.onSessionStart?.();
468
+ },
469
+ onSessionShutdown() {
470
+ publication.reset();
471
+ activityRuntime.onSessionShutdown();
472
+ },
473
+ },
454
474
  activityVerbosityRuntime,
455
475
  assistantOutputRuntime: assistantOutputBinding.runtime,
476
+ publicationRuntime: {
477
+ enqueue: publication.enqueue,
478
+ reserve: publication.reserve,
479
+ capture() {
480
+ const authority = assistantOutputBinding.authority.captureAuthority();
481
+ return {
482
+ target: authority.target ? { ...authority.target } : undefined,
483
+ isCurrent: () => assistantOutputBinding.authority.isAuthorityActive(authority),
484
+ };
485
+ },
486
+ },
456
487
  };
457
488
  }
458
489
 
@@ -680,6 +711,7 @@ export function registerTelegramCommandsAndTools({
680
711
 
681
712
  interface TelegramLifecycleBindingDeps {
682
713
  pi: Pi.ExtensionAPI;
714
+ publicationRuntime: TelegramBridgePublicationRuntime;
683
715
  activityRuntime: Activity.TelegramActivityRuntime;
684
716
  activityVerbosityRuntime?: ActivityVerbosity.TelegramActivityVerbosityRuntime;
685
717
  assistantOutputRuntime: Pick<
@@ -748,6 +780,7 @@ interface TelegramLifecycleBindingDeps {
748
780
  Keyboard.TelegramInlineKeyboardMarkup
749
781
  >["sendGuestReply"]
750
782
  >;
783
+ preparePreviewDelivery?: Queue.TelegramAgentEndRuntimeDeps<Queue.PendingTelegramTurn>["preparePreviewDelivery"];
751
784
  finalizeMarkdownPreview: Queue.TelegramAgentEndHookRuntimeDeps<
752
785
  Queue.PendingTelegramTurn,
753
786
  Pi.ExtensionContext,
@@ -770,6 +803,7 @@ interface TelegramLifecycleBindingDeps {
770
803
 
771
804
  export function registerTelegramLifecycleRuntimeHooks({
772
805
  pi,
806
+ publicationRuntime,
773
807
  activityRuntime,
774
808
  activityVerbosityRuntime,
775
809
  assistantOutputRuntime,
@@ -798,6 +832,7 @@ export function registerTelegramLifecycleRuntimeHooks({
798
832
  answerGuestQuery,
799
833
  deleteMessage,
800
834
  sendGuestReply,
835
+ preparePreviewDelivery,
801
836
  finalizeMarkdownPreview,
802
837
  proactivePushTargetGetter,
803
838
  getAssistantRenderingMode,
@@ -936,24 +971,19 @@ export function registerTelegramLifecycleRuntimeHooks({
936
971
  { replyToPrompt: false },
937
972
  );
938
973
  };
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
- });
974
+ let pendingFinalPublication: {
975
+ turn: Queue.PendingTelegramTurn;
976
+ reservation: Activity.TelegramActivityPublicationReservation;
977
+ } | undefined;
978
+ const cancelPendingFinalPublication = (): void => {
979
+ pendingFinalPublication?.reservation.cancel();
980
+ pendingFinalPublication = undefined;
954
981
  };
955
- const waitForActiveTurnDelivery = async (): Promise<void> => {
956
- await activeTurnDeliveryTail;
982
+ const recordPublicationFailure = (error: unknown): void => {
983
+ recordRuntimeEvent("delivery", error, { phase: "agent-end-background-delivery" });
984
+ };
985
+ const scheduleActiveTurnDelivery = (task: () => Promise<void>): void => {
986
+ void publicationRuntime.enqueue(task).catch(recordPublicationFailure);
957
987
  };
958
988
  const agentLifecycleHooks = Queue.createTelegramAgentLifecycleHooks<
959
989
  Queue.PendingTelegramTurn,
@@ -989,14 +1019,23 @@ export function registerTelegramLifecycleRuntimeHooks({
989
1019
  isSessionActive: isSessionContextActive,
990
1020
  isTurnTransportActive,
991
1021
  waitForTypingIdle: typing.waitForIdle,
992
- async waitForActivityIdle() {
993
- await activityVerbosityRuntime?.waitForIdle();
994
- await assistantOutputRuntime.waitForIdle();
995
- },
996
1022
  dispatchNextQueuedTelegramTurn,
997
1023
  requestDeferredDispatchNextQueuedTelegramTurn:
998
1024
  deferredQueueDispatchRuntime.request,
999
1025
  scheduleActiveTurnDelivery,
1026
+ reserveActiveTurnDelivery() {
1027
+ const pending = pendingFinalPublication;
1028
+ pendingFinalPublication = undefined;
1029
+ const matches = pending?.turn === activeTurnRuntime.get();
1030
+ if (!matches) pending?.reservation.cancel();
1031
+ const reservation = matches && pending ? pending.reservation : publicationRuntime.reserve();
1032
+ return {
1033
+ schedule: (task) => { void reservation.publish(task).catch(recordPublicationFailure); },
1034
+ cancel: reservation.cancel,
1035
+ };
1036
+ },
1037
+ preparePreviewDelivery,
1038
+ preparePreviewClear: previewRuntime.prepareClear,
1000
1039
  clearPreview: previewRuntime.clear,
1001
1040
  setPreviewPendingText: previewRuntime.setPendingText,
1002
1041
  finalizeMarkdownPreview,
@@ -1018,6 +1057,7 @@ export function registerTelegramLifecycleRuntimeHooks({
1018
1057
  Lifecycle.setResetTransportReplyDedup(Replies.resetTransportReplyDedup);
1019
1058
  const agentStartWithDedupReset = Lifecycle.createAgentStartDedupHook(
1020
1059
  agentLifecycleHooks.onAgentStart,
1060
+ scheduleActiveTurnDelivery,
1021
1061
  );
1022
1062
  let uiPromptActive = false;
1023
1063
  const startAgentActivityTypingLoop = (ctx: Pi.ExtensionContext): boolean => {
@@ -1038,25 +1078,24 @@ export function registerTelegramLifecycleRuntimeHooks({
1038
1078
  };
1039
1079
  let observedAutomaticCompaction = false;
1040
1080
  let agentWorkActive = false;
1041
- let terminalAssistantMessagePendingDelivery = false;
1042
- const deferredAutomaticCompactionNotices: string[] = [];
1043
- const sendCompactionNotice = async (text: string): Promise<void> => {
1081
+ const prepareCompactionNotice = (text: string, ctx: Pi.ExtensionContext): (() => Promise<void>) => {
1082
+ const authority = publicationRuntime.capture();
1044
1083
  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
- }
1084
+ const selectedTarget = turn?.target ?? authority.target;
1085
+ const target = selectedTarget ? { ...selectedTarget } : undefined;
1086
+ const replyToMessageId = turn?.replyToMessageId;
1087
+ return async () => {
1088
+ if (!target || !isSessionContextActive(ctx) || !authority.isCurrent()) return;
1089
+ if (turn && isTurnTransportActive?.(turn) === false) return;
1090
+ try {
1091
+ await sendMarkdownReply(target.chatId, replyToMessageId, text, { target });
1092
+ } catch (error) {
1093
+ recordRuntimeEvent("delivery", error, { phase: "compaction-notice" });
1094
+ }
1095
+ };
1056
1096
  };
1057
- const flushDeferredAutomaticCompactionNotices = async (): Promise<void> => {
1058
- const notices = deferredAutomaticCompactionNotices.splice(0);
1059
- for (const notice of notices) await sendCompactionNotice(notice);
1097
+ const sendCompactionNotice = (text: string, ctx: Pi.ExtensionContext): void => {
1098
+ scheduleActiveTurnDelivery(prepareCompactionNotice(text, ctx));
1060
1099
  };
1061
1100
  const compactionObserver = Lifecycle.createTelegramCompactionObserverRuntime({
1062
1101
  isContextActive: isSessionContextActive,
@@ -1070,7 +1109,6 @@ export function registerTelegramLifecycleRuntimeHooks({
1070
1109
  recordRuntimeEvent,
1071
1110
  onCompactionAbandoned: () => {
1072
1111
  observedAutomaticCompaction = false;
1073
- deferredAutomaticCompactionNotices.length = 0;
1074
1112
  activityRuntime.onCompactionAbandoned();
1075
1113
  },
1076
1114
  });
@@ -1091,6 +1129,7 @@ export function registerTelegramLifecycleRuntimeHooks({
1091
1129
  activityRuntime.recordInputSource(event.source ?? "unknown");
1092
1130
  },
1093
1131
  async onSessionStart(event, ctx) {
1132
+ cancelPendingFinalPublication();
1094
1133
  previewRuntime.invalidate();
1095
1134
  assistantOutputRuntime.start();
1096
1135
  activityRuntime.onSessionStart?.();
@@ -1106,9 +1145,8 @@ export function registerTelegramLifecycleRuntimeHooks({
1106
1145
  assistantOutputRuntime.stop();
1107
1146
  observedAutomaticCompaction = false;
1108
1147
  agentWorkActive = false;
1109
- terminalAssistantMessagePendingDelivery = false;
1148
+ cancelPendingFinalPublication();
1110
1149
  uiPromptActive = false;
1111
- deferredAutomaticCompactionNotices.length = 0;
1112
1150
  compactionObserver.onSessionShutdown();
1113
1151
  if (event.reason === "quit" && disconnectOnQuit) {
1114
1152
  try {
@@ -1129,19 +1167,7 @@ export function registerTelegramLifecycleRuntimeHooks({
1129
1167
  if (shouldNotify) observedAutomaticCompaction = true;
1130
1168
  activityRuntime.onCompactionStart(Pi.getSessionCompactionReason(event));
1131
1169
  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
- }
1170
+ if (shouldNotify) sendCompactionNotice(Commands.TELEGRAM_COMPACTION_STARTED_MARKDOWN, ctx);
1145
1171
  },
1146
1172
  async onSessionCompact(event, ctx) {
1147
1173
  if (!isSessionContextActive(ctx)) return;
@@ -1149,40 +1175,26 @@ export function registerTelegramLifecycleRuntimeHooks({
1149
1175
  compactionObserver.onSessionCompact(event, ctx);
1150
1176
  if (observedAutomaticCompaction) {
1151
1177
  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
- }
1178
+ sendCompactionNotice(Commands.TELEGRAM_COMPACTION_COMPLETED_MARKDOWN, ctx);
1161
1179
  }
1162
1180
  },
1163
1181
  async onSessionCompactFailed(event, ctx) {
1164
1182
  if (!isSessionContextActive(ctx)) return;
1165
1183
  const shouldNotify = observedAutomaticCompaction;
1166
- const deferredNotices = deferredAutomaticCompactionNotices.splice(0);
1167
- const shouldDefer =
1168
- deferredNotices.length > 0 || terminalAssistantMessagePendingDelivery;
1169
1184
  compactionObserver.onSessionCompactFailed(event, ctx);
1170
1185
  if (!shouldNotify) return;
1171
1186
  const notice = event.aborted
1172
1187
  ? "**⚠️ Compaction cancelled.**"
1173
1188
  : "**⚠️ Compaction failed.**";
1174
- if (shouldDefer) {
1175
- deferredAutomaticCompactionNotices.push(...deferredNotices, notice);
1176
- } else {
1177
- await sendCompactionNotice(notice);
1178
- }
1189
+ sendCompactionNotice(notice, ctx);
1179
1190
  },
1180
1191
  async onAgentStart(event, ctx) {
1181
1192
  if (!isSessionContextActive(ctx)) return;
1182
1193
  agentWorkActive = true;
1183
- terminalAssistantMessagePendingDelivery = false;
1194
+ cancelPendingFinalPublication();
1184
1195
  await agentStartWithDedupReset(event, ctx);
1185
- activityRuntime.onAgentStart(activeTurnRuntime.get()?.target);
1196
+ const turn = activeTurnRuntime.get();
1197
+ activityRuntime.onAgentStart(turn?.target, turn?.replyToMessageId);
1186
1198
  startAgentActivityTypingLoop(ctx);
1187
1199
  },
1188
1200
  async onToolExecutionStart(event, ctx) {
@@ -1228,13 +1240,16 @@ export function registerTelegramLifecycleRuntimeHooks({
1228
1240
  onMessageEnd(event, ctx) {
1229
1241
  if (!isSessionContextActive(ctx)) return;
1230
1242
  if (event.message.role === "assistant") {
1243
+ previewRuntime.seal();
1231
1244
  activityRuntime.onAssistantMessageEnd(event.message.stopReason);
1232
1245
  }
1233
- terminalAssistantMessagePendingDelivery =
1234
- event.message.role === "assistant" &&
1235
- event.message.stopReason !== "toolUse" &&
1236
- event.message.stopReason !== "error" &&
1237
- event.message.stopReason !== "aborted";
1246
+ if (event.message.role !== "assistant" || event.message.stopReason === "toolUse" || event.message.stopReason === "aborted") return;
1247
+ const turn = activeTurnRuntime.get();
1248
+ if (!turn || turn.guestQueryId || pendingFinalPublication?.turn === turn) return;
1249
+ cancelPendingFinalPublication();
1250
+ const assistant = Replies.extractLatestAssistantMessageText([event.message]);
1251
+ if (!assistant.text && assistant.stopReason !== "error" && turn.queuedAttachments.length === 0) return;
1252
+ pendingFinalPublication = { turn, reservation: publicationRuntime.reserve() };
1238
1253
  },
1239
1254
  onUiPromptStart(event, ctx) {
1240
1255
  if (!isSessionContextActive(ctx)) return;
@@ -1254,18 +1269,22 @@ export function registerTelegramLifecycleRuntimeHooks({
1254
1269
  },
1255
1270
  async onAgentEnd(event, ctx) {
1256
1271
  if (!isSessionContextActive(ctx)) return;
1272
+ if (pendingFinalPublication && pendingFinalPublication.turn !== activeTurnRuntime.get()) {
1273
+ cancelPendingFinalPublication();
1274
+ return;
1275
+ }
1257
1276
  activityRuntime.onAgentEnd();
1258
1277
  await agentLifecycleHooks.onAgentEnd(event, ctx);
1259
1278
  },
1260
1279
  async onAgentSettled(event, ctx) {
1261
1280
  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();
1281
+ const pending = pendingFinalPublication;
1282
+ try {
1283
+ await agentLifecycleHooks.onAgentSettled(event, ctx);
1284
+ } finally {
1285
+ if (pendingFinalPublication === pending) cancelPendingFinalPublication();
1267
1286
  }
1268
- terminalAssistantMessagePendingDelivery = false;
1287
+ if (!isSessionContextActive(ctx)) return;
1269
1288
  agentWorkActive = false;
1270
1289
  activityRuntime.onAgentSettled();
1271
1290
  modelContextAvailabilityRuntime.reconcile();
package/lib/config.ts CHANGED
@@ -744,7 +744,7 @@ export function createTelegramDraftPreviewsChecker(
744
744
  config.assistant?.draftPreviews ??
745
745
  config.draftPreviews ??
746
746
  config.richDraftPreviews ??
747
- false
747
+ true
748
748
  );
749
749
  };
750
750
  }
package/lib/delivery.ts CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  type TelegramInlineKeyboardMarkup,
11
11
  } from "./keyboard.ts";
12
12
  import {
13
- buildTelegramReplyParameters,
13
+ withTelegramReplyParameters,
14
14
  renderTelegramMessage,
15
15
  } from "./replies.ts";
16
16
  import {
@@ -724,23 +724,23 @@ export function createTelegramBridgeDeliveryRuntime(
724
724
  },
725
725
  async sendChunk(target, chunk, options) {
726
726
  assertTransportActive();
727
- const replyParameters = buildTelegramReplyParameters(
728
- target.chatId,
729
- options.replyToMessageId,
730
- target,
731
- );
732
- const body = {
733
- chat_id: target.chatId,
734
- text: chunk.text,
735
- ...(chunk.parseMode === "html" ? { parse_mode: "HTML" as const } : {}),
736
- ...getTelegramTargetThreadParams(target),
737
- ...(replyParameters ? { reply_parameters: replyParameters } : {}),
738
- ...(options.replyMarkup ? { reply_markup: options.replyMarkup } : {}),
739
- };
740
- const sent = await deps.api.sendMessage(
741
- target.threadId === undefined
742
- ? markTelegramBusAggregateDelivery(body)
743
- : body,
727
+ const sent = await withTelegramReplyParameters(
728
+ target.chatId, options.replyToMessageId, target,
729
+ (replyParameters) => {
730
+ const body = {
731
+ chat_id: target.chatId,
732
+ text: chunk.text,
733
+ ...(chunk.parseMode === "html" ? { parse_mode: "HTML" as const } : {}),
734
+ ...getTelegramTargetThreadParams(target),
735
+ ...(replyParameters ? { reply_parameters: replyParameters } : {}),
736
+ ...(options.replyMarkup ? { reply_markup: options.replyMarkup } : {}),
737
+ };
738
+ return deps.api.sendMessage(
739
+ target.threadId === undefined
740
+ ? markTelegramBusAggregateDelivery(body)
741
+ : body,
742
+ );
743
+ },
744
744
  );
745
745
  assertTransportActive();
746
746
  deps.recordOwnership({
package/lib/lifecycle.ts CHANGED
@@ -37,9 +37,15 @@ export function setResetTransportReplyDedup(fn: () => void): void {
37
37
 
38
38
  export function createAgentStartDedupHook(
39
39
  inner: (event: AgentStartEvent, ctx: ExtensionContext) => Promise<void>,
40
+ schedulePublication?: (task: () => Promise<void>) => void,
40
41
  ): (event: AgentStartEvent, ctx: ExtensionContext) => Promise<void> {
41
42
  return async (event, ctx) => {
42
- if (resetTransportReplyDedupFn) resetTransportReplyDedupFn();
43
+ const reset = resetTransportReplyDedupFn;
44
+ if (reset) {
45
+ // A new turn must not erase the anchor of a final still ahead in the FIFO.
46
+ if (schedulePublication) schedulePublication(async () => { reset(); });
47
+ else reset();
48
+ }
43
49
  return inner(event, ctx);
44
50
  };
45
51
  }
@@ -450,8 +456,10 @@ export function createTelegramCompactionObserverRuntime<TContext>(
450
456
  const setTimer = deps.setTimer ?? setTimeout;
451
457
  const clearTimer = deps.clearTimer ?? clearTimeout;
452
458
  let fallbackTimer: TelegramLifecycleTimer | undefined;
459
+ let observationGeneration = 0;
453
460
  let typingStartedByObserver = false;
454
461
  const clearFallbackTimer = (): void => {
462
+ observationGeneration += 1;
455
463
  if (!fallbackTimer) return;
456
464
  clearTimer(fallbackTimer);
457
465
  fallbackTimer = undefined;
@@ -470,7 +478,10 @@ export function createTelegramCompactionObserverRuntime<TContext>(
470
478
  !!deps.startTypingLoop && typingStartResult !== false;
471
479
  deps.updateStatus(ctx);
472
480
  clearFallbackTimer();
481
+ const admittedGeneration = observationGeneration;
473
482
  fallbackTimer = setTimer(() => {
483
+ if (observationGeneration !== admittedGeneration) return;
484
+ observationGeneration += 1;
474
485
  fallbackTimer = undefined;
475
486
  if (deps.isContextActive && !deps.isContextActive(ctx)) return;
476
487
  deps.setCompactionInProgress(false);
@@ -487,8 +498,8 @@ export function createTelegramCompactionObserverRuntime<TContext>(
487
498
  unrefTelegramLifecycleTimer(fallbackTimer);
488
499
  },
489
500
  onSessionCompact: (_event, ctx) => {
490
- clearFallbackTimer();
491
501
  if (deps.isContextActive && !deps.isContextActive(ctx)) return;
502
+ clearFallbackTimer();
492
503
  deps.setCompactionInProgress(false);
493
504
  if (typingStartedByObserver) deps.stopTypingLoop?.();
494
505
  typingStartedByObserver = false;
@@ -496,8 +507,8 @@ export function createTelegramCompactionObserverRuntime<TContext>(
496
507
  requestDispatch();
497
508
  },
498
509
  onSessionCompactFailed: (_event, ctx) => {
499
- clearFallbackTimer();
500
510
  if (deps.isContextActive && !deps.isContextActive(ctx)) return;
511
+ clearFallbackTimer();
501
512
  deps.setCompactionInProgress(false);
502
513
  if (typingStartedByObserver) deps.stopTypingLoop?.();
503
514
  typingStartedByObserver = false;