@llblab/pi-kit 0.6.0 → 0.7.1

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 (129) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +4 -4
  3. package/node_modules/@llblab/pi-actors/AGENTS.md +1 -1
  4. package/node_modules/@llblab/pi-actors/CHANGELOG.md +6 -0
  5. package/node_modules/@llblab/pi-actors/README.md +1 -1
  6. package/node_modules/@llblab/pi-actors/dist/lib/async-runs.d.ts +3 -0
  7. package/node_modules/@llblab/pi-actors/dist/lib/async-runs.js +14 -1
  8. package/node_modules/@llblab/pi-actors/dist/lib/command-templates.js +45 -3
  9. package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.js +1 -1
  10. package/node_modules/@llblab/pi-actors/dist/lib/observability.d.ts +16 -3
  11. package/node_modules/@llblab/pi-actors/dist/lib/observability.js +92 -7
  12. package/node_modules/@llblab/pi-actors/dist/lib/pi.d.ts +0 -1
  13. package/node_modules/@llblab/pi-actors/dist/lib/pi.js +15 -24
  14. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery-lineage.d.ts +17 -0
  15. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery-lineage.js +44 -0
  16. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.d.ts +4 -0
  17. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.js +102 -4
  18. package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.js +58 -39
  19. package/node_modules/@llblab/pi-actors/dist/lib/runtime.js +14 -6
  20. package/node_modules/@llblab/pi-actors/dist/skills/actors/SKILL.md +1 -1
  21. package/node_modules/@llblab/pi-actors/docs/async-runs.md +1 -1
  22. package/node_modules/@llblab/pi-actors/docs/coordinator-delivery.md +18 -23
  23. package/node_modules/@llblab/pi-actors/lib/async-runs.ts +18 -1
  24. package/node_modules/@llblab/pi-actors/lib/command-templates.ts +41 -3
  25. package/node_modules/@llblab/pi-actors/lib/extension-runtime.ts +1 -1
  26. package/node_modules/@llblab/pi-actors/lib/observability.ts +119 -5
  27. package/node_modules/@llblab/pi-actors/lib/pi.ts +15 -28
  28. package/node_modules/@llblab/pi-actors/lib/run-delivery-lineage.ts +68 -0
  29. package/node_modules/@llblab/pi-actors/lib/run-delivery.ts +120 -4
  30. package/node_modules/@llblab/pi-actors/lib/run-ui-runtime.ts +69 -44
  31. package/node_modules/@llblab/pi-actors/lib/runtime.ts +17 -6
  32. package/node_modules/@llblab/pi-actors/package.json +1 -1
  33. package/node_modules/@llblab/pi-actors/skills/actors/SKILL.md +1 -1
  34. package/node_modules/@llblab/pi-state-flow/AGENTS.md +33 -12
  35. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +125 -2
  36. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +87 -45
  37. package/node_modules/@llblab/pi-state-flow/README.md +203 -107
  38. package/node_modules/@llblab/pi-state-flow/docs/README.md +4 -0
  39. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +188 -0
  40. package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +42 -0
  41. package/node_modules/@llblab/pi-state-flow/index.ts +164 -2
  42. package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +138 -0
  43. package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +273 -0
  44. package/node_modules/@llblab/pi-state-flow/lib/config.ts +48 -0
  45. package/node_modules/@llblab/pi-state-flow/lib/context.ts +18 -5
  46. package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +268 -0
  47. package/node_modules/@llblab/pi-state-flow/lib/discovery.ts +117 -0
  48. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +562 -0
  49. package/node_modules/@llblab/pi-state-flow/lib/episode.ts +24 -12
  50. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +606 -70
  51. package/node_modules/@llblab/pi-state-flow/lib/git.ts +666 -0
  52. package/node_modules/@llblab/pi-state-flow/lib/history.ts +95 -0
  53. package/node_modules/@llblab/pi-state-flow/lib/json.ts +24 -0
  54. package/node_modules/@llblab/pi-state-flow/lib/maintenance.ts +141 -0
  55. package/node_modules/@llblab/pi-state-flow/lib/memory.ts +52 -0
  56. package/node_modules/@llblab/pi-state-flow/lib/migration.ts +88 -0
  57. package/node_modules/@llblab/pi-state-flow/lib/publication.ts +296 -0
  58. package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +23 -7
  59. package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +79 -0
  60. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +264 -0
  61. package/node_modules/@llblab/pi-state-flow/lib/session.ts +6 -0
  62. package/node_modules/@llblab/pi-state-flow/lib/skills.ts +99 -7
  63. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +305 -48
  64. package/node_modules/@llblab/pi-state-flow/lib/state.ts +74 -7
  65. package/node_modules/@llblab/pi-state-flow/lib/status.ts +125 -6
  66. package/node_modules/@llblab/pi-state-flow/lib/storage.ts +196 -0
  67. package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +233 -0
  68. package/node_modules/@llblab/pi-state-flow/lib/terminal.ts +70 -24
  69. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +254 -29
  70. package/node_modules/@llblab/pi-state-flow/package.json +8 -2
  71. package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +128 -0
  72. package/node_modules/@llblab/pi-telegram/AGENTS.md +14 -9
  73. package/node_modules/@llblab/pi-telegram/BACKLOG.md +23 -5
  74. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +18 -0
  75. package/node_modules/@llblab/pi-telegram/README.md +13 -9
  76. package/node_modules/@llblab/pi-telegram/docs/README.md +1 -0
  77. package/node_modules/@llblab/pi-telegram/docs/architecture.md +227 -23
  78. package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +1 -1
  79. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +70 -19
  80. package/node_modules/@llblab/pi-telegram/docs/outbound.md +6 -6
  81. package/node_modules/@llblab/pi-telegram/docs/public-api.md +13 -6
  82. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +3 -1
  83. package/node_modules/@llblab/pi-telegram/index.ts +4 -1418
  84. package/node_modules/@llblab/pi-telegram/lib/agent-messages.ts +6 -3
  85. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +46 -1
  86. package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +32 -19
  87. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +600 -135
  88. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +962 -55
  89. package/node_modules/@llblab/pi-telegram/lib/bus.ts +355 -26
  90. package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +718 -0
  91. package/node_modules/@llblab/pi-telegram/lib/commands.ts +237 -11
  92. package/node_modules/@llblab/pi-telegram/lib/config.ts +242 -26
  93. package/node_modules/@llblab/pi-telegram/lib/extension.ts +1851 -0
  94. package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +20 -2
  95. package/node_modules/@llblab/pi-telegram/lib/journal.ts +2184 -126
  96. package/node_modules/@llblab/pi-telegram/lib/locks.ts +44 -2
  97. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +152 -13
  98. package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +97 -10
  99. package/node_modules/@llblab/pi-telegram/lib/paths.ts +29 -0
  100. package/node_modules/@llblab/pi-telegram/lib/polling.ts +85 -17
  101. package/node_modules/@llblab/pi-telegram/lib/preview.ts +17 -0
  102. package/node_modules/@llblab/pi-telegram/lib/prompts.ts +6 -2
  103. package/node_modules/@llblab/pi-telegram/lib/queue.ts +118 -26
  104. package/node_modules/@llblab/pi-telegram/lib/rendering.ts +4 -1
  105. package/node_modules/@llblab/pi-telegram/lib/replies.ts +21 -2
  106. package/node_modules/@llblab/pi-telegram/lib/routing.ts +344 -112
  107. package/node_modules/@llblab/pi-telegram/lib/setup.ts +44 -4
  108. package/node_modules/@llblab/pi-telegram/lib/status.ts +51 -4
  109. package/node_modules/@llblab/pi-telegram/lib/sync.ts +308 -39
  110. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +353 -22
  111. package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +664 -0
  112. package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +226 -0
  113. package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +118 -0
  114. package/node_modules/@llblab/pi-telegram/lib/threads.ts +1686 -129
  115. package/node_modules/@llblab/pi-telegram/lib/turns.ts +7 -0
  116. package/node_modules/@llblab/pi-telegram/lib/updates.ts +1319 -97
  117. package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +1643 -0
  118. package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +968 -0
  119. package/node_modules/@llblab/pi-telegram/lib/workspace-slots.ts +84 -0
  120. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  121. package/node_modules/@llblab/pi-telegram/screenshot.png +0 -0
  122. package/node_modules/@llblab/pi-telegram/scripts/measure-bus.mjs +83 -0
  123. package/node_modules/@llblab/pi-telegram/scripts/measure-workspace.mjs +101 -0
  124. package/node_modules/@llblab/{skills → pi-telegram/skills}/show-me/SKILL.md +28 -6
  125. package/node_modules/@llblab/pi-telegram/skills/show-me/references/telegram-surfaces.md +43 -0
  126. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/delivery-and-threads.md +1 -1
  127. package/node_modules/@llblab/skills/package.json +2 -3
  128. package/package.json +6 -5
  129. /package/node_modules/@llblab/pi-telegram/lib/{logs.ts → logging.ts} +0 -0
@@ -280,11 +280,12 @@ export type TelegramDurablePollingRuntimeAssemblyDeps<
280
280
  "appendUpdateBatch" | "getJournalEntryCount" | "signalUpdateWorker"
281
281
  > & {
282
282
  canStart?: (ctx: TContext) => boolean;
283
+ prepareUpdateBatch?: (updates: readonly TUpdate[]) => void;
283
284
  journal: {
284
285
  appendBatch: (
285
286
  updates: readonly TUpdate[],
286
287
  acceptedThroughUpdateId?: number,
287
- ) => MaybePromise<unknown>;
288
+ ) => { nonExcludedUpdateIds: readonly number[] };
288
289
  getAcceptedThroughUpdateId: () => number | undefined;
289
290
  prepareCursorCutover?: () => MaybePromise<void>;
290
291
  getEntryCount: () => number;
@@ -303,7 +304,30 @@ export function createTelegramDurablePollingRuntimeAssembly<
303
304
  ): TelegramDurablePollingRuntimeAssembly<TContext> {
304
305
  const controller = createTelegramPollingControllerRuntime({
305
306
  ...deps,
306
- appendUpdateBatch: deps.journal.appendBatch,
307
+ appendUpdateBatch(updates, cursor) {
308
+ const result = deps.journal.appendBatch(updates, cursor);
309
+ if (!deps.prepareUpdateBatch) return result;
310
+ // No await: an already-draining worker must not observe the new batch before preparation.
311
+ try {
312
+ const included = new Set(result.nonExcludedUpdateIds);
313
+ let batch: TUpdate[] = [];
314
+ for (const update of updates) {
315
+ if (included.has(update.update_id)) batch.push(update);
316
+ else if (batch.length > 0) {
317
+ deps.prepareUpdateBatch(batch);
318
+ batch = [];
319
+ }
320
+ }
321
+ if (batch.length > 0) deps.prepareUpdateBatch(batch);
322
+ } catch (error) {
323
+ try {
324
+ deps.recordRuntimeEvent?.("polling", error, { phase: "batch-preparation", updateCount: updates.length });
325
+ } catch {
326
+ // Already-published input must still reach the worker if diagnostics fail.
327
+ }
328
+ }
329
+ return result;
330
+ },
307
331
  getAcceptedThroughUpdateId: deps.journal.getAcceptedThroughUpdateId,
308
332
  getJournalEntryCount: deps.journal.getEntryCount,
309
333
  signalUpdateWorker: deps.journal.signalWorker,
@@ -393,7 +417,6 @@ export function createTelegramPollingControllerRuntime<
393
417
  getAcceptedThroughUpdateId: deps.getAcceptedThroughUpdateId,
394
418
  getJournalEntryCount: deps.getJournalEntryCount,
395
419
  signalUpdateWorker: deps.signalUpdateWorker,
396
- prepareUpdateBatch: deps.prepareUpdateBatch,
397
420
  updateStatus: deps.updateStatus,
398
421
  sleep: deps.sleep,
399
422
  onPhaseChange(phase, currentUpdateId) {
@@ -651,6 +674,7 @@ export interface TelegramThreadCapabilityRuntimeDeps<
651
674
  getPollingStartedWithTelegramBus: () => boolean;
652
675
  setPollingStartedWithTelegramBus: (started: boolean) => void;
653
676
  setTopicModeUnavailable: (unavailable: boolean) => void;
677
+ suspendLiveThreadTarget?: () => void;
654
678
  stopFollowerRegistration: () => void;
655
679
  startClassicPolling: (ctx: TContext) => MaybePromise<void>;
656
680
  stopClassicPolling: () => MaybePromise<void>;
@@ -682,6 +706,8 @@ export interface TelegramThreadCapabilityStateRuntime {
682
706
  isBusRuntimeEnabled(): boolean;
683
707
  shouldForceFreshLeaderThread(): boolean;
684
708
  setForceFreshLeaderThread(forceFresh: boolean): void;
709
+ getRequestedThreadName(): string | undefined;
710
+ setRequestedThreadName(threadName: string | undefined): void;
685
711
  }
686
712
 
687
713
  export type TelegramThreadTargetObservationHandler<TContext> = (
@@ -715,6 +741,10 @@ export interface TelegramThreadAwarePollingPorts<TContext, TOwner> {
715
741
  ctx: TContext,
716
742
  owner: TOwner,
717
743
  ) => Promise<boolean | undefined>;
744
+ restoreFollowerWithOwner: (
745
+ ctx: TContext,
746
+ owner: TOwner,
747
+ ) => Promise<boolean | undefined>;
718
748
  stopFollowerRegistration: () => void;
719
749
  }
720
750
 
@@ -738,6 +768,11 @@ export interface TelegramThreadAwarePollingDeps<
738
768
  ctx: TContext,
739
769
  owner: TOwner,
740
770
  ) => Promise<boolean | undefined>;
771
+ restoreFollowerWithLeader?: (
772
+ ctx: TContext,
773
+ owner: TOwner,
774
+ ) => Promise<boolean | undefined>;
775
+ hasRememberedWorkspaceBinding?: (ctx: TContext) => boolean;
741
776
  stopFollowerRegistration: () => void;
742
777
  }
743
778
 
@@ -760,6 +795,12 @@ export interface TelegramThreadCapabilityOrchestrationDeps<
760
795
  ctx: TContext,
761
796
  owner: TOwner,
762
797
  ) => Promise<boolean | undefined>;
798
+ restoreFollowerWithLeader?: (
799
+ ctx: TContext,
800
+ owner: TOwner,
801
+ ) => Promise<boolean | undefined>;
802
+ hasRememberedWorkspaceBinding?: (ctx: TContext) => boolean;
803
+ suspendLiveThreadTarget?: () => void;
763
804
  stopFollowerRegistration: () => void;
764
805
  isTopicModeUnavailableError: (error: unknown) => boolean;
765
806
  updateStatus: (ctx: TContext) => void;
@@ -780,6 +821,7 @@ export function createTelegramThreadCapabilityStateRuntime(): TelegramThreadCapa
780
821
  let busPollingStarted = false;
781
822
  let topicModeUnavailable = false;
782
823
  let forceFreshLeaderThread = false;
824
+ let requestedThreadName: string | undefined;
783
825
  return {
784
826
  isBusPollingStarted: () => busPollingStarted,
785
827
  setBusPollingStarted(started) {
@@ -794,6 +836,10 @@ export function createTelegramThreadCapabilityStateRuntime(): TelegramThreadCapa
794
836
  setForceFreshLeaderThread(forceFresh) {
795
837
  forceFreshLeaderThread = forceFresh;
796
838
  },
839
+ getRequestedThreadName: () => requestedThreadName,
840
+ setRequestedThreadName(threadName) {
841
+ requestedThreadName = threadName;
842
+ },
797
843
  };
798
844
  }
799
845
 
@@ -818,6 +864,7 @@ export function createTelegramThreadCapabilityOrchestration<TContext, TOwner>(
818
864
  getPollingStartedWithTelegramBus: deps.state.isBusPollingStarted,
819
865
  setPollingStartedWithTelegramBus: deps.state.setBusPollingStarted,
820
866
  setTopicModeUnavailable: deps.state.setTopicModeUnavailable,
867
+ suspendLiveThreadTarget: deps.suspendLiveThreadTarget,
821
868
  stopFollowerRegistration: deps.stopFollowerRegistration,
822
869
  startClassicPolling: deps.startClassicPolling,
823
870
  stopClassicPolling: deps.stopClassicPolling,
@@ -850,6 +897,8 @@ export function createTelegramThreadCapabilityOrchestration<TContext, TOwner>(
850
897
  startLeaderHealth: deps.startLeaderHealth,
851
898
  stopLeaderHealth: deps.stopLeaderHealth,
852
899
  registerFollowerWithLeader: deps.registerFollowerWithLeader,
900
+ restoreFollowerWithLeader: deps.restoreFollowerWithLeader,
901
+ hasRememberedWorkspaceBinding: deps.hasRememberedWorkspaceBinding,
853
902
  stopFollowerRegistration: deps.stopFollowerRegistration,
854
903
  recordEvent: deps.recordEvent,
855
904
  setTopicModeUnavailable: deps.state.setTopicModeUnavailable,
@@ -958,14 +1007,16 @@ export async function applyTelegramThreadCapability<TContext>(
958
1007
  if (!isCurrent()) return;
959
1008
  deps.setTopicModeUnavailable(true);
960
1009
  deps.stopFollowerRegistration();
1010
+ const hadLiveThreadTransport = deps.getPollingStartedWithTelegramBus();
961
1011
  if (
962
- deps.getPollingStartedWithTelegramBus() ||
1012
+ hadLiveThreadTransport ||
963
1013
  hasTelegramClassicRestoreFailure(previousBotState)
964
1014
  ) {
965
1015
  deps.stopLeaderHealth();
966
1016
  await deps.stopBusPolling();
967
1017
  if (!isCurrent()) return;
968
1018
  deps.setPollingStartedWithTelegramBus(false);
1019
+ if (hadLiveThreadTransport) deps.suspendLiveThreadTarget?.();
969
1020
  try {
970
1021
  await deps.startClassicPolling(ctx);
971
1022
  if (!isCurrent()) return;
@@ -1110,24 +1161,34 @@ export function createTelegramThreadAwarePollingPorts<TContext, TOwner>(
1110
1161
  }
1111
1162
  await deps.stopClassicPolling();
1112
1163
  };
1113
- const registerFollowerWithOwner = async (
1114
- ctx: TContext,
1115
- owner: TOwner,
1116
- ): Promise<boolean | undefined> => {
1164
+ const refreshFollowerState = async (): Promise<boolean> => {
1117
1165
  if (deps.topicTargetStore.refresh) {
1118
1166
  await deps.topicTargetStore.refresh();
1119
1167
  } else {
1120
1168
  await deps.topicTargetStore.load();
1121
1169
  }
1122
- if (deps.topicTargetStore.getBotState().threadMode !== "enabled") {
1123
- return undefined;
1124
- }
1170
+ return deps.topicTargetStore.getBotState().threadMode === "enabled";
1171
+ };
1172
+ const registerFollowerWithOwner = async (
1173
+ ctx: TContext,
1174
+ owner: TOwner,
1175
+ ): Promise<boolean | undefined> => {
1176
+ if (!(await refreshFollowerState())) return undefined;
1125
1177
  return deps.registerFollowerWithLeader(ctx, owner);
1126
1178
  };
1179
+ const restoreFollowerWithOwner = async (
1180
+ ctx: TContext,
1181
+ owner: TOwner,
1182
+ ): Promise<boolean | undefined> => {
1183
+ if (!(await refreshFollowerState())) return undefined;
1184
+ if (!deps.hasRememberedWorkspaceBinding?.(ctx)) return undefined;
1185
+ return deps.restoreFollowerWithLeader?.(ctx, owner);
1186
+ };
1127
1187
  return {
1128
1188
  startPolling,
1129
1189
  stopPolling,
1130
1190
  registerFollowerWithOwner,
1191
+ restoreFollowerWithOwner,
1131
1192
  stopFollowerRegistration: deps.stopFollowerRegistration,
1132
1193
  };
1133
1194
  }
@@ -1179,9 +1240,9 @@ export function createTelegramThreadCapabilityMonitor<TContext>(
1179
1240
  let consecutiveDisabledProbes = 0;
1180
1241
  const stop = (): void => {
1181
1242
  generation += 1;
1182
- deps.lifecycle?.invalidate();
1183
1243
  if (interval) clearInterval(interval);
1184
1244
  interval = undefined;
1245
+ deps.lifecycle?.invalidate();
1185
1246
  };
1186
1247
  const check = (ctx: TContext): void => {
1187
1248
  if (transitionPromise || !canProbeTelegramThreadCapability(ctx, deps)) {
@@ -1276,8 +1337,19 @@ export function createTelegramThreadCapabilityMonitor<TContext>(
1276
1337
  return {
1277
1338
  start(ctx) {
1278
1339
  stop();
1340
+ const expectedGeneration = generation;
1279
1341
  interval = setInterval(() => {
1280
- check(ctx);
1342
+ if (generation !== expectedGeneration) return;
1343
+ try {
1344
+ check(ctx);
1345
+ } catch (error) {
1346
+ try { stop(); } catch { /* Timer shutdown must not escape the callback. */ }
1347
+ try {
1348
+ deps.recordEvent("bus", error, { phase: "capability-monitor" });
1349
+ } catch {
1350
+ // Monitor diagnostics cannot create an uncaught interval exception.
1351
+ }
1352
+ }
1281
1353
  }, intervalMs);
1282
1354
  interval.unref?.();
1283
1355
  },
@@ -1416,7 +1488,6 @@ export interface TelegramPollLoopDeps<
1416
1488
  getAcceptedThroughUpdateId?: () => number | undefined;
1417
1489
  getJournalEntryCount: () => number;
1418
1490
  signalUpdateWorker: () => void;
1419
- prepareUpdateBatch?: (updates: readonly TUpdate[]) => void;
1420
1491
  onErrorStatus: (message: string) => void;
1421
1492
  onStatusReset: () => void;
1422
1493
  sleep: (ms: number, signal?: AbortSignal) => Promise<void>;
@@ -1446,7 +1517,6 @@ export interface TelegramPollLoopRunnerDeps<
1446
1517
  getAcceptedThroughUpdateId?: () => number | undefined;
1447
1518
  getJournalEntryCount: () => number;
1448
1519
  signalUpdateWorker: () => void;
1449
- prepareUpdateBatch?: (updates: readonly TUpdate[]) => void;
1450
1520
  updateStatus: (ctx: TContext, message?: string) => void;
1451
1521
  sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
1452
1522
  onPhaseChange?: (
@@ -1501,7 +1571,6 @@ export function createTelegramPollLoopRunner<
1501
1571
  getAcceptedThroughUpdateId: deps.getAcceptedThroughUpdateId,
1502
1572
  getJournalEntryCount: deps.getJournalEntryCount,
1503
1573
  signalUpdateWorker: deps.signalUpdateWorker,
1504
- prepareUpdateBatch: deps.prepareUpdateBatch,
1505
1574
  onErrorStatus: (message) => {
1506
1575
  updateTelegramPollingStatusSafely(deps.updateStatus, ctx, {
1507
1576
  message,
@@ -1702,7 +1771,6 @@ export async function runTelegramPollLoop<
1702
1771
  reportTelegramPollingPhase(deps, "long-poll");
1703
1772
  const updates = await requestTelegramUpdatesWithinBudget(deps, request);
1704
1773
  reportTelegramPollingResponse(deps, updates.length);
1705
- deps.prepareUpdateBatch?.(updates);
1706
1774
  consecutiveGetUpdatesConflicts = 0;
1707
1775
  currentUpdateId = updates[0]?.update_id;
1708
1776
  await admitTelegramPollingUpdateBatch({
@@ -70,6 +70,7 @@ export interface TelegramPreviewActiveTurn {
70
70
  target?: TelegramTarget;
71
71
  voiceReplyPreferred?: boolean;
72
72
  voiceReplyRequired?: boolean;
73
+ guestQueryId?: string;
73
74
  }
74
75
 
75
76
  export interface TelegramAssistantMessagePreviewStartDeps<TMessage> {
@@ -408,6 +409,17 @@ export function createTelegramAssistantMessagePreviewHooks<TMessage>(
408
409
  };
409
410
  }
410
411
 
412
+ /**
413
+ * Returns true when the active turn is a Telegram Guest Mode query. A guest
414
+ * query allows exactly one answer within a limited Telegram response window,
415
+ * so it must never emit streaming draft previews.
416
+ */
417
+ export function shouldSuppressPreviewForGuestTurn(
418
+ turn: { guestQueryId?: string } | null | undefined,
419
+ ): boolean {
420
+ return !!turn?.guestQueryId;
421
+ }
422
+
411
423
  export async function handleTelegramAssistantMessagePreviewStart<TMessage>(
412
424
  message: TMessage,
413
425
  deps: TelegramAssistantMessagePreviewStartDeps<TMessage>,
@@ -422,6 +434,10 @@ export async function handleTelegramAssistantMessagePreviewStart<TMessage>(
422
434
  deps.setState(undefined);
423
435
  return;
424
436
  }
437
+ if (shouldSuppressPreviewForGuestTurn(turn)) {
438
+ deps.setState(undefined);
439
+ return;
440
+ }
425
441
  const state = deps.getState();
426
442
  sealTelegramPreviewState(state);
427
443
  const next = deps.createPreviewState();
@@ -443,6 +459,7 @@ export async function handleTelegramAssistantMessagePreviewUpdate<TMessage>(
443
459
  return;
444
460
  }
445
461
  if (shouldSuppressPreviewForVoice(turn)) return;
462
+ if (shouldSuppressPreviewForGuestTurn(turn)) return;
446
463
  let state = deps.getState();
447
464
  if (!state) {
448
465
  state = deps.createPreviewState();
@@ -14,7 +14,7 @@ export const TELEGRAM_DISCONNECTED_CONTEXT_MESSAGE =
14
14
 
15
15
  const LOCAL_SYSTEM_PROMPT_SUFFIX = `
16
16
 
17
- ${TELEGRAM_CONNECTED_CONTEXT_MESSAGE} For Telegram work, consult bundled Skills in routing order: \`telegram-bridge\` for the transport and turn protocol, \`generated-control-surface\` when contextual controls materially shorten feedback, then \`generative-apps\` when the interaction warrants a reusable deterministic app. Load a Skill only if its instructions are not already present in the current context. Do not use Telegram-specific features from unrelated local/TUI prompts.`;
17
+ ${TELEGRAM_CONNECTED_CONTEXT_MESSAGE} For Telegram work, consult bundled Skills in routing order: \`telegram-bridge\` for the transport and turn protocol, \`show-me\` when a user needs a truthful visual explanation of work or behavior, \`generated-control-surface\` when contextual controls materially shorten feedback, then \`generative-apps\` when the interaction warrants a reusable deterministic app. Load a Skill only if its instructions are not already present in the current context. Do not use Telegram-specific features from unrelated local/TUI prompts.`;
18
18
 
19
19
  const TELEGRAM_TURN_SYSTEM_PROMPT_SUFFIX = `
20
20
 
@@ -28,9 +28,11 @@ export const TELEGRAM_ATTACH_PROMPT_GUIDELINES = [
28
28
  "For an explicit thread target, provide chat_id plus thread_id; registered multi-instance followers default to their assigned thread target.",
29
29
  ] as const;
30
30
  export const TELEGRAM_MESSAGE_PROMPT_SNIPPET =
31
- "Send direct Telegram Markdown text when the user explicitly asks for Telegram delivery outside the normal reply flow.";
31
+ "Send direct Telegram Markdown text when the user explicitly asks for Telegram delivery to a concrete chat, channel, or live Pi Thread outside the normal reply flow.";
32
32
  export const TELEGRAM_MESSAGE_PROMPT_GUIDELINES = [
33
33
  "Use telegram_message only when the user explicitly asks to send a message to Telegram from the local/TUI side, or names a concrete Telegram delivery target.",
34
+ "For an explicitly requested channel post, pass its exact numeric id or public @username as chat_id; no local channel registry is required, and Telegram remains the authority on the bot's posting permission.",
35
+ "For an explicitly requested channel media post, pass one local .jpg/.jpeg/.png/.webp photo or .mp4 video as media; the text becomes its caption (max 1024 characters), and albums or other media types are rejected.",
34
36
  "For a live Pi thread target, provide thread as its case-insensitive name or numeric id; the bridge sends visibly and admits one attributed turn to that live instance. Unknown, ambiguous, same, or offline targets fail before sending.",
35
37
  "Add buttons by embedding the same top-level telegram_button HTML comments used in normal Telegram replies; Telegram does not support standalone buttons.",
36
38
  "During an active Telegram turn, omit telegram_message for the current target and answer normally; use thread only when the user requests delivery to a different live Pi thread.",
@@ -48,6 +50,8 @@ const TELEGRAM_TOOL_METADATA_LINES = Object.fromEntries(
48
50
  const TELEGRAM_MODEL_CONTEXT_TOOL_NAMES = new Set([
49
51
  "telegram_attach",
50
52
  "telegram_bind",
53
+ "telegram_channel_post",
54
+ "telegram_channel_posts",
51
55
  "telegram_message",
52
56
  ]);
53
57
  const TELEGRAM_MODEL_CONTEXT_MEMORY_KEY = Symbol.for(
@@ -110,6 +110,7 @@ export interface TelegramQueueItemBase {
110
110
  transportStamp?: TelegramTransportStamp;
111
111
  replyToMessageId: number;
112
112
  guestQueryId?: string;
113
+ guestInlineMessageId?: string;
113
114
  queueOrder: number;
114
115
  queueLane: TelegramQueueLane;
115
116
  laneOrder: number;
@@ -152,6 +153,7 @@ export interface TelegramQueueHandoffBase {
152
153
  transportStamp?: TelegramTransportStamp;
153
154
  replyToMessageId: number;
154
155
  guestQueryId?: string;
156
+ guestInlineMessageId?: string;
155
157
  queueOrder: number;
156
158
  queueLane: TelegramQueueLane;
157
159
  laneOrder: number;
@@ -612,6 +614,9 @@ export function createTelegramQueueHandoffPayload<TContext>(
612
614
  ...(item.transportStamp ? { transportStamp: item.transportStamp } : {}),
613
615
  replyToMessageId: item.replyToMessageId,
614
616
  ...(item.guestQueryId ? { guestQueryId: item.guestQueryId } : {}),
617
+ ...(item.guestInlineMessageId
618
+ ? { guestInlineMessageId: item.guestInlineMessageId }
619
+ : {}),
615
620
  queueOrder: item.queueOrder,
616
621
  queueLane: item.queueLane,
617
622
  laneOrder: item.laneOrder,
@@ -626,6 +631,9 @@ export function createTelegramQueueHandoffPayload<TContext>(
626
631
  ...(item.transportStamp ? { transportStamp: item.transportStamp } : {}),
627
632
  replyToMessageId: item.replyToMessageId,
628
633
  ...(item.guestQueryId ? { guestQueryId: item.guestQueryId } : {}),
634
+ ...(item.guestInlineMessageId
635
+ ? { guestInlineMessageId: item.guestInlineMessageId }
636
+ : {}),
629
637
  queueOrder: item.queueOrder,
630
638
  queueLane: item.queueLane,
631
639
  laneOrder: item.laneOrder,
@@ -1527,7 +1535,7 @@ export interface TelegramAgentEndRuntimeDeps<
1527
1535
  chatId: number,
1528
1536
  replyToMessageId: number,
1529
1537
  text: string,
1530
- options?: { target?: TelegramQueueTarget },
1538
+ options?: { target?: TelegramQueueTarget; parseMode?: "HTML" },
1531
1539
  ) => Promise<unknown>;
1532
1540
  sendQueuedAttachments: (
1533
1541
  turn: TTurn,
@@ -1544,6 +1552,8 @@ export interface TelegramAgentEndRuntimeDeps<
1544
1552
  options?: { parseMode?: string },
1545
1553
  ) => Promise<void>;
1546
1554
  sendGuestReply?: (guestQueryId: string, markdown: string) => Promise<void>;
1555
+ /** Replaces the early guest ACK with the final text. */
1556
+ editGuestReply?: (inlineMessageId: string, markdown: string) => Promise<void>;
1547
1557
  sendGuestAttachment?: (
1548
1558
  turn: TTurn,
1549
1559
  attachment: QueuedAttachment,
@@ -1622,6 +1632,7 @@ export interface TelegramAgentEndHookRuntimeDeps<
1622
1632
  >["sendRichAttachmentReply"];
1623
1633
  answerGuestQuery?: TelegramAgentEndRuntimeDeps<TTurn>["answerGuestQuery"];
1624
1634
  sendGuestReply?: TelegramAgentEndRuntimeDeps<TTurn>["sendGuestReply"];
1635
+ editGuestReply?: TelegramAgentEndRuntimeDeps<TTurn>["editGuestReply"];
1625
1636
  sendGuestAttachment?: TelegramAgentEndRuntimeDeps<TTurn>["sendGuestAttachment"];
1626
1637
  sendGuestVoiceReply?: TelegramAgentEndRuntimeDeps<TTurn>["sendGuestVoiceReply"];
1627
1638
  planOutboundReply?: TelegramAgentEndRuntimeDeps<
@@ -1762,6 +1773,7 @@ export function createTelegramAgentEndHook<
1762
1773
  sendRichAttachmentReply: deps.sendRichAttachmentReply,
1763
1774
  answerGuestQuery: deps.answerGuestQuery,
1764
1775
  sendGuestReply: deps.sendGuestReply,
1776
+ editGuestReply: deps.editGuestReply,
1765
1777
  sendGuestAttachment: deps.sendGuestAttachment,
1766
1778
  sendGuestVoiceReply: deps.sendGuestVoiceReply,
1767
1779
  planOutboundReply: deps.planOutboundReply,
@@ -1854,11 +1866,47 @@ export async function handleTelegramAgentEndRuntime<
1854
1866
  return;
1855
1867
  }
1856
1868
  if (turn.guestQueryId) {
1869
+ if (turn.guestInlineMessageId && deps.editGuestReply) {
1870
+ const experimentText = assistant.errorMessage
1871
+ ? "Telegram bridge: Pi failed while processing the request."
1872
+ : finalText;
1873
+ if (experimentText) {
1874
+ try {
1875
+ await deps.editGuestReply(turn.guestInlineMessageId, experimentText);
1876
+ deps.recordRuntimeEvent?.(
1877
+ "guest",
1878
+ new Error("Guest ACK experiment edited the guest answer"),
1879
+ { phase: "guest-ack-edited", guestQueryId: turn.guestQueryId },
1880
+ );
1881
+ } catch (error) {
1882
+ deps.recordRuntimeEvent?.("delivery", error, {
1883
+ phase: "guest-ack-edit",
1884
+ guestQueryId: turn.guestQueryId,
1885
+ });
1886
+ }
1887
+ } else {
1888
+ deps.recordRuntimeEvent?.(
1889
+ "delivery",
1890
+ new Error("Guest ACK experiment turn produced no editable text"),
1891
+ { phase: "guest-ack-edit-empty", guestQueryId: turn.guestQueryId },
1892
+ );
1893
+ }
1894
+ if (!isDeliveryActive()) return;
1895
+ if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
1896
+ return;
1897
+ }
1857
1898
  if (assistant.errorMessage) {
1858
- await deps.answerGuestQuery?.(
1859
- turn.guestQueryId,
1860
- "Telegram bridge: Pi failed while processing the request.",
1861
- );
1899
+ try {
1900
+ await deps.answerGuestQuery?.(
1901
+ turn.guestQueryId,
1902
+ "Telegram bridge: Pi failed while processing the request.",
1903
+ );
1904
+ } catch (error) {
1905
+ deps.recordRuntimeEvent?.("delivery", error, {
1906
+ phase: "guest-error-reply",
1907
+ guestQueryId: turn.guestQueryId,
1908
+ });
1909
+ }
1862
1910
  if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
1863
1911
  return;
1864
1912
  }
@@ -1894,10 +1942,20 @@ export async function handleTelegramAgentEndRuntime<
1894
1942
  });
1895
1943
  }
1896
1944
  } else if (finalText) {
1897
- if (deps.sendGuestReply) {
1898
- await deps.sendGuestReply(turn.guestQueryId, finalText);
1899
- } else {
1900
- await deps.answerGuestQuery?.(turn.guestQueryId, finalText);
1945
+ try {
1946
+ if (deps.sendGuestReply) {
1947
+ await deps.sendGuestReply(turn.guestQueryId, finalText);
1948
+ } else {
1949
+ await deps.answerGuestQuery?.(turn.guestQueryId, finalText);
1950
+ }
1951
+ } catch (error) {
1952
+ // Guest queries expire after Telegram's response timeout, so a slow
1953
+ // turn can fail the only delivery attempt. Record and continue the
1954
+ // agent-end lifecycle instead of rejecting the extension hook.
1955
+ deps.recordRuntimeEvent?.("delivery", error, {
1956
+ phase: "guest-reply",
1957
+ guestQueryId: turn.guestQueryId,
1958
+ });
1901
1959
  }
1902
1960
  }
1903
1961
  if (!isDeliveryActive()) return;
@@ -1908,23 +1966,37 @@ export async function handleTelegramAgentEndRuntime<
1908
1966
  const deliverActiveTurn = async () => {
1909
1967
  await deps.waitForActivityIdle?.();
1910
1968
  if (!isDeliveryActive()) return;
1911
- if (endPlan.shouldClearPreview || (!finalText && hasOutboundArtifacts)) {
1969
+ let previewCleared = false;
1970
+ const clearTurnPreview = async () => {
1971
+ if (previewCleared) return;
1912
1972
  await clearPreview?.();
1973
+ previewCleared = true;
1974
+ };
1975
+ if (endPlan.shouldClearPreview || (!finalText && hasOutboundArtifacts)) {
1976
+ await clearTurnPreview();
1913
1977
  if (!isDeliveryActive()) return;
1914
1978
  }
1915
1979
  if (endPlan.shouldSendErrorMessage) {
1980
+ const errorMessage = assistant.errorMessage ||
1981
+ "Telegram bridge: Pi failed while processing the request.";
1982
+ const isOperationAborted = errorMessage.trim().replace(/\.$/, "") ===
1983
+ "This operation was aborted";
1916
1984
  await deps.sendTextReply(
1917
1985
  turn.chatId,
1918
1986
  turn.replyToMessageId,
1919
- assistant.errorMessage ||
1920
- "Telegram bridge: Pi failed while processing the request.",
1921
- { target: turn.target },
1987
+ isOperationAborted
1988
+ ? "<b>⏹️ This operation was aborted.</b>"
1989
+ : errorMessage,
1990
+ {
1991
+ target: turn.target,
1992
+ ...(isOperationAborted ? { parseMode: "HTML" as const } : {}),
1993
+ },
1922
1994
  );
1923
1995
  if (!isDeliveryActive()) return;
1924
1996
  if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
1925
1997
  return;
1926
1998
  }
1927
- if (finalText) setPreviewPendingText(finalText);
1999
+ if (finalText && turn.queuedAttachments.length === 0) setPreviewPendingText(finalText);
1928
2000
 
1929
2001
  if (!isDeliveryActive()) return;
1930
2002
  let richAttachmentDelivered = false;
@@ -1942,7 +2014,7 @@ export async function handleTelegramAgentEndRuntime<
1942
2014
  );
1943
2015
  if (!isDeliveryActive()) return;
1944
2016
  if (richAttachmentDelivered) {
1945
- await clearPreview?.();
2017
+ await clearTurnPreview();
1946
2018
  if (!isDeliveryActive()) return;
1947
2019
  setPreviewPendingText("");
1948
2020
  }
@@ -1957,24 +2029,42 @@ export async function handleTelegramAgentEndRuntime<
1957
2029
  }
1958
2030
  }
1959
2031
  if (!isDeliveryActive()) return;
2032
+ let queuedAttachmentsDelivered = false;
2033
+ if (!richAttachmentDelivered && turn.queuedAttachments.length > 0) {
2034
+ await clearTurnPreview();
2035
+ if (!isDeliveryActive()) return;
2036
+ setPreviewPendingText("");
2037
+ await deps.sendQueuedAttachments(turn, { isDeliveryActive });
2038
+ if (!isDeliveryActive()) return;
2039
+ queuedAttachmentsDelivered = true;
2040
+ }
1960
2041
  if (!richAttachmentDelivered && endPlan.kind === "text" && finalText) {
1961
2042
  try {
1962
- const finalized = await finalizeMarkdownPreview(
1963
- turn.chatId,
1964
- finalText,
1965
- turn.replyToMessageId,
1966
- { replyMarkup, target: turn.target },
1967
- );
1968
- if (!isDeliveryActive()) return;
1969
- if (!finalized) {
1970
- await clearPreview?.();
1971
- if (!isDeliveryActive()) return;
2043
+ if (queuedAttachmentsDelivered) {
1972
2044
  await deps.sendMarkdownReply(
1973
2045
  turn.chatId,
1974
2046
  turn.replyToMessageId,
1975
2047
  finalText,
1976
2048
  { replyMarkup, target: turn.target },
1977
2049
  );
2050
+ } else {
2051
+ const finalized = await finalizeMarkdownPreview(
2052
+ turn.chatId,
2053
+ finalText,
2054
+ turn.replyToMessageId,
2055
+ { replyMarkup, target: turn.target },
2056
+ );
2057
+ if (!isDeliveryActive()) return;
2058
+ if (!finalized) {
2059
+ await clearTurnPreview();
2060
+ if (!isDeliveryActive()) return;
2061
+ await deps.sendMarkdownReply(
2062
+ turn.chatId,
2063
+ turn.replyToMessageId,
2064
+ finalText,
2065
+ { replyMarkup, target: turn.target },
2066
+ );
2067
+ }
1978
2068
  }
1979
2069
  if (!isDeliveryActive()) return;
1980
2070
  setPreviewPendingText("");
@@ -2041,7 +2131,9 @@ export async function handleTelegramAgentEndRuntime<
2041
2131
  );
2042
2132
  }
2043
2133
  if (!isDeliveryActive()) return;
2044
- if (!richAttachmentDelivered) await deps.sendQueuedAttachments(turn, { isDeliveryActive });
2134
+ if (!richAttachmentDelivered && !queuedAttachmentsDelivered) {
2135
+ await deps.sendQueuedAttachments(turn, { isDeliveryActive });
2136
+ }
2045
2137
  if (!isDeliveryActive()) return;
2046
2138
  if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
2047
2139
  };
@@ -551,9 +551,12 @@ function applyInlineMarkdownStyles(text: string): string {
551
551
  result = renderDelimitedInlineStyle(result, "*", (content) => {
552
552
  return `<i>${content}</i>`;
553
553
  });
554
- return renderDelimitedInlineStyle(result, "_", (content) => {
554
+ result = renderDelimitedInlineStyle(result, "_", (content) => {
555
555
  return `<i>${content}</i>`;
556
556
  });
557
+ return renderDelimitedInlineStyle(result, "||", (content) => {
558
+ return `<tg-spoiler>${content}</tg-spoiler>`;
559
+ });
557
560
  }
558
561
 
559
562
  function restoreInlineMarkdownTokens(
@@ -672,7 +672,7 @@ export async function sendTelegramNativeMarkdownReply<TReplyMarkup = unknown>(
672
672
  chatId, index === 0 ? replyToMessageId : undefined, options?.target,
673
673
  (replyParameters) => deps.sendRichMessage({
674
674
  chat_id: chatId,
675
- rich_message: { markdown: chunk, skip_entity_detection: true },
675
+ rich_message: { markdown: chunk },
676
676
  reply_markup:
677
677
  index === chunks.length - 1 ? options?.replyMarkup : undefined,
678
678
  ...(replyParameters ? { reply_parameters: replyParameters } : {}),
@@ -894,7 +894,26 @@ export function createGuestMarkdownReplySender(deps: {
894
894
  return async (guestQueryId: string, markdown: string) => {
895
895
  const [richMarkdown = markdown] = splitTelegramNativeMarkdown(markdown);
896
896
  await deps.answerGuestQuery(guestQueryId, undefined, {
897
- richMessage: { markdown: richMarkdown, skip_entity_detection: true },
897
+ richMessage: { markdown: richMarkdown },
898
+ });
899
+ };
900
+ }
901
+
902
+ /**
903
+ * Guest reply editor: replaces an early Guest Mode answer (the temporary ACK
904
+ * experiment) with native Rich Markdown content addressed by
905
+ * `inline_message_id` instead of a chat/message pair.
906
+ */
907
+ export function createGuestMarkdownReplyEditor(deps: {
908
+ editGuestInlineMessage: (
909
+ inlineMessageId: string,
910
+ content: { richMessage?: TelegramInputRichMessage; text?: string },
911
+ ) => Promise<void>;
912
+ }) {
913
+ return async (inlineMessageId: string, markdown: string) => {
914
+ const [richMarkdown = markdown] = splitTelegramNativeMarkdown(markdown);
915
+ await deps.editGuestInlineMessage(inlineMessageId, {
916
+ richMessage: { markdown: richMarkdown },
898
917
  });
899
918
  };
900
919
  }