@llblab/pi-kit 0.5.1 → 0.6.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 (41) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +3 -3
  3. package/node_modules/@llblab/pi-grow-loop/AGENTS.md +2 -2
  4. package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +4 -0
  5. package/node_modules/@llblab/pi-grow-loop/README.md +6 -6
  6. package/node_modules/@llblab/pi-grow-loop/index.ts +6 -3
  7. package/node_modules/@llblab/pi-grow-loop/package.json +1 -1
  8. package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
  9. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +20 -0
  10. package/node_modules/@llblab/pi-telegram/README.md +1 -1
  11. package/node_modules/@llblab/pi-telegram/docs/architecture.md +2 -2
  12. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +4 -0
  13. package/node_modules/@llblab/pi-telegram/docs/outbound.md +27 -5
  14. package/node_modules/@llblab/pi-telegram/docs/public-api.md +1 -0
  15. package/node_modules/@llblab/pi-telegram/index.ts +24 -19
  16. package/node_modules/@llblab/pi-telegram/lib/activity-verbosity.ts +43 -25
  17. package/node_modules/@llblab/pi-telegram/lib/activity.ts +79 -6
  18. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +110 -91
  19. package/node_modules/@llblab/pi-telegram/lib/config.ts +1 -1
  20. package/node_modules/@llblab/pi-telegram/lib/delivery.ts +18 -18
  21. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +14 -3
  22. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +2 -2
  23. package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +41 -37
  24. package/node_modules/@llblab/pi-telegram/lib/outbound-voice.ts +39 -42
  25. package/node_modules/@llblab/pi-telegram/lib/outbound.ts +28 -17
  26. package/node_modules/@llblab/pi-telegram/lib/preview.ts +134 -73
  27. package/node_modules/@llblab/pi-telegram/lib/queue.ts +113 -72
  28. package/node_modules/@llblab/pi-telegram/lib/replies.ts +46 -38
  29. package/node_modules/@llblab/pi-telegram/lib/routing.ts +192 -58
  30. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +36 -3
  31. package/node_modules/@llblab/pi-telegram/lib/updates.ts +27 -35
  32. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  33. package/node_modules/@llblab/skills/abcd-context/AGENTS.md +1 -0
  34. package/node_modules/@llblab/skills/abcd-context/CHANGELOG.md +6 -2
  35. package/node_modules/@llblab/skills/abcd-context/SKILL.md +1 -1
  36. package/node_modules/@llblab/skills/abcd-context/docs/validation-design.md +11 -5
  37. package/node_modules/@llblab/skills/abcd-context/scripts/_self-test.mjs +61 -0
  38. package/node_modules/@llblab/skills/abcd-context/scripts/validate-context.mjs +67 -0
  39. package/node_modules/@llblab/skills/package.json +1 -1
  40. package/node_modules/@llblab/skills/release-flow/SKILL.md +2 -4
  41. package/package.json +4 -4
@@ -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();
@@ -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
  }
@@ -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({
@@ -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;
@@ -184,8 +184,8 @@ export function buildDraftPreviewsSettingsText(enabled: boolean): string {
184
184
  "",
185
185
  "Show live answer drafts while the model is answering.",
186
186
  "",
187
- "<code>-</code> <code>on</code>: stream safe Telegram Rich Draft frames before the final answer.",
188
- "<code>-</code> <code>off</code> (default): show native active status, then send one final answer.",
187
+ "<code>-</code> <code>on</code> (default): stream safe Telegram Rich Draft frames before the final answer.",
188
+ "<code>-</code> <code>off</code>: show native active status, then send one final answer.",
189
189
  ].join("\n");
190
190
  }
191
191
 
@@ -21,7 +21,7 @@ import {
21
21
  TELEGRAM_MESSAGE_PROMPT_SNIPPET,
22
22
  } from "./prompts.ts";
23
23
  import {
24
- buildTelegramMultipartReplyParameters,
24
+ withTelegramReplyParameters,
25
25
  normalizeTelegramNativeMarkdown,
26
26
  } from "./replies.ts";
27
27
  import {
@@ -189,18 +189,10 @@ export function planTelegramRichOutboundAttachment(options: {
189
189
  ],
190
190
  skip_entity_detection: true,
191
191
  };
192
- const replyParameters =
193
- options.turn.replyToMessageId > 0
194
- ? JSON.stringify({
195
- message_id: options.turn.replyToMessageId,
196
- allow_sending_without_reply: true,
197
- })
198
- : undefined;
199
192
  return {
200
193
  method: "sendRichMessage",
201
194
  fields: {
202
195
  chat_id: String(options.turn.chatId),
203
- ...(replyParameters ? { reply_parameters: replyParameters } : {}),
204
196
  ...getTelegramMultipartTargetFields(options.turn.target),
205
197
  rich_message: JSON.stringify(richMessage),
206
198
  ...(options.replyMarkup
@@ -219,8 +211,9 @@ export function createTelegramRichOutboundAttachmentSender(
219
211
  return async (
220
212
  turn: TelegramQueuedOutboundAttachmentTurnView,
221
213
  markdown: string,
222
- options?: { replyMarkup?: unknown },
214
+ options?: { replyMarkup?: unknown; isDeliveryActive?: () => boolean },
223
215
  ): Promise<boolean> => {
216
+ if (options?.isDeliveryActive?.() === false) return false;
224
217
  const plan = planTelegramRichOutboundAttachment({
225
218
  turn,
226
219
  markdown,
@@ -229,12 +222,15 @@ export function createTelegramRichOutboundAttachmentSender(
229
222
  });
230
223
  if (!plan) return false;
231
224
  try {
232
- const result = await deps.sendMultipart(
233
- plan.method,
234
- plan.fields,
235
- plan.fileField,
236
- plan.filePath,
237
- plan.fileName,
225
+ const result = await withTelegramReplyParameters(
226
+ turn.chatId, turn.replyToMessageId, turn.target,
227
+ (replyParameters) => deps.sendMultipart(
228
+ plan.method,
229
+ { ...plan.fields, ...(replyParameters ? { reply_parameters: JSON.stringify(replyParameters) } : {}) },
230
+ plan.fileField,
231
+ plan.filePath,
232
+ plan.fileName,
233
+ ),
238
234
  );
239
235
  const messageId =
240
236
  result && typeof result === "object" &&
@@ -246,11 +242,13 @@ export function createTelegramRichOutboundAttachmentSender(
246
242
  new Error("Successful Rich media upload omitted message_id."),
247
243
  );
248
244
  }
249
- deps.recordOwnership?.({
250
- chatId: turn.chatId,
251
- messageId,
252
- target: turn.target,
253
- });
245
+ if (options?.isDeliveryActive?.() !== false) {
246
+ deps.recordOwnership?.({
247
+ chatId: turn.chatId,
248
+ messageId,
249
+ target: turn.target,
250
+ });
251
+ }
254
252
  return true;
255
253
  } catch (error) {
256
254
  if (isTelegramRichAttachmentCommitUnknownError(error)) throw error;
@@ -580,6 +578,7 @@ export interface TelegramQueuedOutboundAttachmentDeliveryDeps {
580
578
  ) => void;
581
579
  statPath?: (path: string) => Promise<{ size: number }>;
582
580
  maxAttachmentSizeBytes?: number;
581
+ isDeliveryActive?: () => boolean;
583
582
  }
584
583
 
585
584
  export async function queueTelegramOutboundAttachments(options: {
@@ -925,9 +924,13 @@ export async function sendTelegramOutboundFiles(options: {
925
924
  export function createTelegramQueuedOutboundAttachmentSender(
926
925
  deps: TelegramQueuedOutboundAttachmentDeliveryDeps,
927
926
  ) {
928
- return async (turn: TelegramQueuedOutboundAttachmentTurnView): Promise<void> => {
927
+ return async (
928
+ turn: TelegramQueuedOutboundAttachmentTurnView,
929
+ options?: { isDeliveryActive?: () => boolean },
930
+ ): Promise<void> => {
929
931
  await sendQueuedTelegramOutboundAttachments(turn, {
930
932
  ...deps,
933
+ isDeliveryActive: () => deps.isDeliveryActive?.() !== false && options?.isDeliveryActive?.() !== false,
931
934
  maxAttachmentSizeBytes:
932
935
  deps.maxAttachmentSizeBytes ?? TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES,
933
936
  });
@@ -939,9 +942,11 @@ export async function sendQueuedTelegramOutboundAttachments(
939
942
  deps: TelegramQueuedOutboundAttachmentDeliveryDeps,
940
943
  ): Promise<void> {
941
944
  for (const attachment of turn.queuedAttachments) {
945
+ if (deps.isDeliveryActive?.() === false) return;
942
946
  try {
943
947
  if (deps.maxAttachmentSizeBytes !== undefined) {
944
948
  const stats = await (deps.statPath ?? stat)(attachment.path);
949
+ if (deps.isDeliveryActive?.() === false) return;
945
950
  if (stats.size > deps.maxAttachmentSizeBytes) {
946
951
  throw new Error(
947
952
  formatTelegramOutboundAttachmentSizeLimitError(
@@ -954,23 +959,22 @@ export async function sendQueuedTelegramOutboundAttachments(
954
959
  const isPhoto = isTelegramOutboundPhotoAttachmentPath(attachment.path);
955
960
  const method = isPhoto ? "sendPhoto" : "sendDocument";
956
961
  const fieldName = isPhoto ? "photo" : "document";
957
- const replyParameters = buildTelegramMultipartReplyParameters(
958
- turn.chatId,
959
- turn.replyToMessageId,
960
- turn.target,
961
- );
962
- await deps.sendMultipart(
963
- method,
964
- {
965
- chat_id: String(turn.chatId),
966
- ...(replyParameters ? { reply_parameters: replyParameters } : {}),
967
- ...getTelegramMultipartTargetFields(turn.target),
968
- },
969
- fieldName,
970
- attachment.path,
971
- attachment.fileName,
962
+ await withTelegramReplyParameters(
963
+ turn.chatId, turn.replyToMessageId, turn.target,
964
+ (replyParameters) => deps.sendMultipart(
965
+ method,
966
+ {
967
+ chat_id: String(turn.chatId),
968
+ ...(replyParameters ? { reply_parameters: JSON.stringify(replyParameters) } : {}),
969
+ ...getTelegramMultipartTargetFields(turn.target),
970
+ },
971
+ fieldName,
972
+ attachment.path,
973
+ attachment.fileName,
974
+ ),
972
975
  );
973
976
  } catch (error) {
977
+ if (deps.isDeliveryActive?.() === false) return;
974
978
  const message = error instanceof Error ? error.message : String(error);
975
979
  deps.recordRuntimeEvent?.("attachment", error, {
976
980
  fileName: attachment.fileName,