@llblab/pi-telegram 0.22.1 → 0.23.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.
package/index.ts CHANGED
@@ -155,11 +155,6 @@ export default function (pi: Pi.ExtensionAPI) {
155
155
  contextStore: telegramSessionContextStore,
156
156
  });
157
157
  const activeTurnRuntime = Queue.createTelegramActiveTurnStore();
158
- const proactivePushChatIdGetter =
159
- Config.createTelegramProactivePushChatIdGetter({
160
- getActiveTurnChatId: activeTurnRuntime.getChatId,
161
- getAllowedUserId: configStore.getAllowedUserId,
162
- });
163
158
  const proactivePushTargetGetter =
164
159
  Config.createTelegramProactivePushTargetGetter({
165
160
  getActiveTurnTarget: activeTurnRuntime.getTarget,
@@ -171,6 +166,8 @@ export default function (pi: Pi.ExtensionAPI) {
171
166
  },
172
167
  getAllowedUserId: configStore.getAllowedUserId,
173
168
  });
169
+ const proactivePushChatIdGetter =
170
+ Config.createTelegramProactivePushChatIdGetter(proactivePushTargetGetter);
174
171
  const buttonActionStore = Outbound.createTelegramButtonActionStore();
175
172
  const pendingModelSwitchStore =
176
173
  Model.createPendingModelSwitchStore<
@@ -220,14 +217,11 @@ export default function (pi: Pi.ExtensionAPI) {
220
217
  scheduleSnapshotPersist: runtimeDiagnostics.scheduleSnapshotPersist,
221
218
  });
222
219
  const recordThreadReconciliationPlan = threadReconciliationRuntime.recordPlan;
223
- const markTelegramConfigSyncChange =
224
- telegramSyncStateRuntime.markConfigChange;
225
- const persistTelegramConfigWithSync = async function (
226
- nextConfig?: Config.TelegramConfig,
227
- ) {
228
- await configStore.persist(nextConfig);
229
- markTelegramConfigSyncChange("config-persist");
230
- };
220
+ const persistTelegramConfigWithSync =
221
+ Sync.createTelegramConfigSyncPersister<Config.TelegramConfig>({
222
+ persist: configStore.persist,
223
+ markConfigChange: telegramSyncStateRuntime.markConfigChange,
224
+ });
231
225
  const currentInstanceThreadRuntime =
232
226
  Threads.createTelegramCurrentInstanceThreadRuntime({
233
227
  instanceId: telegramInstanceId,
@@ -271,7 +265,7 @@ export default function (pi: Pi.ExtensionAPI) {
271
265
  getFollowerTarget: telegramBusFollowerRegistrationState.getTarget,
272
266
  getFollowerSlot: telegramBusFollowerRegistrationState.getSlot,
273
267
  getFollowerThreadName: telegramBusFollowerRegistrationState.getThreadName,
274
- getCurrentIdentity: getCurrentInstanceThreadIdentity,
268
+ getCurrentIdentity: currentInstanceThreadRuntime.getRestorationIdentity,
275
269
  });
276
270
  const statusRuntime = Status.createTelegramBridgeStatusRuntime<
277
271
  Pi.ExtensionContext,
@@ -347,6 +341,9 @@ export default function (pi: Pi.ExtensionAPI) {
347
341
  },
348
342
  getRegistrationGeneration:
349
343
  telegramBusFollowerRegistrationState.getGeneration,
344
+ getForwardCommentBatchPosition:
345
+ textGroupRuntime.getPreparedForwardingPosition,
346
+ recordRuntimeEvent,
350
347
  timeoutMs: 30_000,
351
348
  });
352
349
  const telegramApiRuntime = BusApi.createTelegramBusAwareApiRuntime({
@@ -445,8 +442,45 @@ export default function (pi: Pi.ExtensionAPI) {
445
442
  });
446
443
  },
447
444
  });
445
+ const { sendTextReply, sendMarkdownReply } =
446
+ Outbound.createTelegramOutboundTextReplyRuntime({
447
+ sendTextReply: replyRuntime.sendTextReply,
448
+ sendMarkdownReply: replyRuntime.sendMarkdownReply,
449
+ execCommand: CommandTemplates.execCommandTemplate,
450
+ getHandlers: configStore.getOutboundHandlers,
451
+ recordRuntimeEvent,
452
+ });
453
+ const assistantOutputBindingRuntime =
454
+ Bindings.createTelegramAssistantOutputBindingRuntime({
455
+ isEnabled: configControls.isProactivePushEnabled,
456
+ authority: {
457
+ getPreferredTarget: proactivePushTargetGetter,
458
+ getFallbackChatId: proactivePushChatIdGetter,
459
+ getTransportStamp: telegramTransportStampRuntime.getStamp,
460
+ isTransportStampActive: telegramTransportStampRuntime.isActive,
461
+ ownsDirect: lockRuntime.owns,
462
+ getDirectEpoch: lockRuntime.getOwnedLeaderEpoch,
463
+ isFollowerRegistered:
464
+ telegramBusFollowerRegistrationState.isRegistered,
465
+ getFollowerGeneration:
466
+ telegramBusFollowerRegistrationState.getGeneration,
467
+ },
468
+ sender: {
469
+ recordOwnership: messageOwnershipRuntime.recordLocal,
470
+ sendMessage,
471
+ sendRichMessage,
472
+ editMessage: editTelegramMessageText,
473
+ getAssistantRenderingMode: configControls.getAssistantRenderingMode,
474
+ execCommand: CommandTemplates.execCommandTemplate,
475
+ getHandlers: configStore.getOutboundHandlers,
476
+ recordRuntimeEvent,
477
+ },
478
+ recordRuntimeEvent,
479
+ });
480
+ const assistantOutputRuntime = assistantOutputBindingRuntime.runtime;
448
481
  const activityRuntime = Activity.createTelegramActivityBridgeRuntime({
449
482
  generation: deliveryGenerationSeed,
483
+ observeEvent: assistantOutputBindingRuntime.observeEvent,
450
484
  recordFailure(handlerId, event, error) {
451
485
  recordRuntimeEvent("activity", error, {
452
486
  handlerId,
@@ -455,14 +489,6 @@ export default function (pi: Pi.ExtensionAPI) {
455
489
  });
456
490
  },
457
491
  });
458
- const { sendTextReply, sendMarkdownReply } =
459
- Outbound.createTelegramOutboundTextReplyRuntime({
460
- sendTextReply: replyRuntime.sendTextReply,
461
- sendMarkdownReply: replyRuntime.sendMarkdownReply,
462
- execCommand: CommandTemplates.execCommandTemplate,
463
- getHandlers: configStore.getOutboundHandlers,
464
- recordRuntimeEvent,
465
- });
466
492
  const dispatchNextQueuedTelegramTurn =
467
493
  Queue.createTelegramQueueDispatchRuntime({
468
494
  ...telegramQueueStore,
@@ -622,9 +648,7 @@ export default function (pi: Pi.ExtensionAPI) {
622
648
  callApi: callTelegramApi,
623
649
  isTopicProvisioningActive: telegramProvisioningActivity.isActive,
624
650
  getCurrentLeaderEpoch,
625
- getThreadReconciliationMachineState() {
626
- return threadReconciliationRuntime.getState();
627
- },
651
+ getThreadReconciliationMachineState: threadReconciliationRuntime.getState,
628
652
  recordThreadReconciliationPlan,
629
653
  getSyncState: telegramSyncStateRuntime.getState,
630
654
  setSyncState: telegramSyncStateRuntime.setState,
@@ -650,9 +674,7 @@ export default function (pi: Pi.ExtensionAPI) {
650
674
  recordMessageOwnership: messageOwnershipRuntime.recordRouted,
651
675
  ...inboundBusProjectionRuntime,
652
676
  getCurrentLeaderEpoch,
653
- getThreadReconciliationMachineState() {
654
- return threadReconciliationRuntime.getState();
655
- },
677
+ getThreadReconciliationMachineState: threadReconciliationRuntime.getState,
656
678
  recordThreadReconciliationPlan,
657
679
  handleTelegramTopicLifecycleUpdate: topicLifecycleSync,
658
680
  handleTelegramThreadTargetObserved(_target, ctx) {
@@ -744,13 +766,13 @@ export default function (pi: Pi.ExtensionAPI) {
744
766
  receiver: {
745
767
  socketPath: getTelegramBusFollowerSocketPath,
746
768
  instanceId: telegramInstanceId,
747
- getContext() {
748
- return telegramSessionContextStore.get();
749
- },
769
+ getContext: telegramSessionContextStore.get,
750
770
  getAuthSecret() {
751
771
  return telegramActiveBusAuthSecret;
752
772
  },
753
773
  ...forwardedRouteHandlers,
774
+ prepareForwardedMessage:
775
+ textGroupRuntime.prepareForwardedMessage,
754
776
  recordRuntimeEvent,
755
777
  },
756
778
  targetReplacement: {
@@ -766,17 +788,13 @@ export default function (pi: Pi.ExtensionAPI) {
766
788
  },
767
789
  recovery: {
768
790
  registrationState: telegramBusFollowerRegistrationState,
769
- getLeaderState() {
770
- return lockRuntime.getState();
771
- },
791
+ getLeaderState: lockRuntime.getState,
772
792
  setLifecyclePhase(phase) {
773
793
  telegramBusLifecycleOverridePhase = phase;
774
794
  },
775
795
  updateStatus,
776
796
  promoteToLeader: promoteTelegramBusFollowerToLeader,
777
- getActiveContext() {
778
- return telegramSessionContextStore.get();
779
- },
797
+ getActiveContext: telegramSessionContextStore.get,
780
798
  recordRuntimeEvent,
781
799
  },
782
800
  registration: {
@@ -784,9 +802,7 @@ export default function (pi: Pi.ExtensionAPI) {
784
802
  getFollowerBusSocketPath: getTelegramBusFollowerSocketPath,
785
803
  getLeaderSocketPath: getTelegramBusSocketPath,
786
804
  registrationState: telegramBusFollowerRegistrationState,
787
- isContextActive(ctx) {
788
- return telegramSessionContextStore.isCurrent(ctx);
789
- },
805
+ isContextActive: telegramSessionContextStore.isCurrent,
790
806
  createRequestId: telegramBusFollowerClients.createRequestId,
791
807
  getLeaderAuthSecret(owner) {
792
808
  return owner.busSecret;
@@ -794,12 +810,7 @@ export default function (pi: Pi.ExtensionAPI) {
794
810
  setActiveAuthSecret(secret) {
795
811
  telegramActiveBusAuthSecret = secret;
796
812
  },
797
- getThreadName() {
798
- return undefined;
799
- },
800
- getProfileKey() {
801
- return getTelegramManualFollowerProfileKey();
802
- },
813
+ getProfileKey: getTelegramManualFollowerProfileKey,
803
814
  recordRuntimeEvent,
804
815
  },
805
816
  });
@@ -812,6 +823,7 @@ export default function (pi: Pi.ExtensionAPI) {
812
823
  deleteWebhook,
813
824
  getUpdates,
814
825
  persistConfig: persistTelegramConfigWithSync,
826
+ prepareUpdateBatch: textGroupRuntime.prepareUpdateBatch,
815
827
  handleUpdate: Updates.createTelegramUpdateHandle({
816
828
  defaultHandle: inboundRouteRuntime.handleUpdate,
817
829
  }),
@@ -819,17 +831,13 @@ export default function (pi: Pi.ExtensionAPI) {
819
831
  updateStatus,
820
832
  recordRuntimeEvent,
821
833
  });
822
- const recoverStaleTelegramTopicApiError = function (
823
- apiBody: unknown,
824
- error: unknown,
825
- ) {
826
- return Sync.recoverStaleTelegramTopicApiError(apiBody, error, {
834
+ const recoverStaleTelegramTopicApiError =
835
+ Sync.createTelegramStaleTopicApiErrorRecoveryRuntime({
827
836
  topicTargetStore: threadStore,
828
837
  getSyncState: telegramSyncStateRuntime.getState,
829
838
  setSyncState: telegramSyncStateRuntime.setState,
830
839
  recordEvent: recordRuntimeEvent,
831
840
  });
832
- };
833
841
  const authorizeFollowerApiCall = Bus.createTelegramFollowerApiCallAuthorizer({
834
842
  isMessageOwned: messageOwnershipRuntime.isOwnedByFollower,
835
843
  });
@@ -851,9 +859,7 @@ export default function (pi: Pi.ExtensionAPI) {
851
859
  },
852
860
  getAllowedUserId: configStore.getAllowedUserId,
853
861
  instanceId: telegramInstanceId,
854
- getCwd(ctx) {
855
- return typeof ctx.cwd === "string" ? ctx.cwd : undefined;
856
- },
862
+ getCwd: Pi.getExtensionContextCwd,
857
863
  getTelegramProfile: getActiveTelegramThreadProfile,
858
864
  shouldForceFreshUnnamed:
859
865
  telegramThreadCapabilityState.shouldForceFreshLeaderThread,
@@ -865,9 +871,7 @@ export default function (pi: Pi.ExtensionAPI) {
865
871
  downloadFile: directTelegramApiRuntime.downloadFile,
866
872
  recoverStaleTargetError: recoverStaleTelegramTopicApiError,
867
873
  getCurrentLeaderEpoch,
868
- getThreadReconciliationMachineState() {
869
- return threadReconciliationRuntime.getState();
870
- },
874
+ getThreadReconciliationMachineState: threadReconciliationRuntime.getState,
871
875
  recordThreadReconciliationPlan,
872
876
  getSyncState: telegramSyncStateRuntime.getState,
873
877
  setSyncState: telegramSyncStateRuntime.setState,
@@ -937,6 +941,8 @@ export default function (pi: Pi.ExtensionAPI) {
937
941
  getCurrentLeaderEpoch,
938
942
  getLeaderTarget: telegramBusLeaderState.getTarget,
939
943
  clearLeaderTarget: telegramBusLeaderState.clear,
944
+ disconnectFollowerThread:
945
+ telegramBusFollowerRegistration.disconnectFromLeader,
940
946
  getSyncState: telegramSyncStateRuntime.getState,
941
947
  setSyncState: telegramSyncStateRuntime.setState,
942
948
  stopPolling: lockedPollingRuntime.stop,
@@ -970,6 +976,10 @@ export default function (pi: Pi.ExtensionAPI) {
970
976
  registrationRuntime: telegramBusFollowerRegistration,
971
977
  instanceId: telegramInstanceId,
972
978
  suspendPolling: lockedPollingRuntime.suspend,
979
+ isLeader: lockRuntime.owns,
980
+ getLeaderBinding: currentInstanceThreadRuntime.getRestorationIdentity,
981
+ getActiveContext: telegramSessionContextStore.get,
982
+ getActiveProfileName: getActiveTelegramThreadProfile,
973
983
  getLeaderState: lockRuntime.getState,
974
984
  updateStatus,
975
985
  recordRuntimeEvent,
@@ -1000,6 +1010,11 @@ export default function (pi: Pi.ExtensionAPI) {
1000
1010
  activeTurnRuntime,
1001
1011
  lockedPollingRuntime,
1002
1012
  stopPolling: disconnectTelegramAndDeleteCurrentThread,
1013
+ getDisconnectThreadName() {
1014
+ const record = findCurrentThreadRecord();
1015
+ if (!record?.target.threadId) return undefined;
1016
+ return record.threadName ?? "current Telegram thread";
1017
+ },
1003
1018
  onTransportChanged: deliveryLifecycleRuntime.onSessionStart,
1004
1019
  getStatusLines,
1005
1020
  buttonActionStore,
@@ -1026,6 +1041,7 @@ export default function (pi: Pi.ExtensionAPI) {
1026
1041
  onModelSelect: currentModelRuntime.onModelSelect,
1027
1042
  },
1028
1043
  activityRuntime,
1044
+ assistantOutputRuntime,
1029
1045
  configStore,
1030
1046
  abort,
1031
1047
  typing,
@@ -1048,15 +1064,10 @@ export default function (pi: Pi.ExtensionAPI) {
1048
1064
  deleteMessage: deleteTelegramMessage,
1049
1065
  sendGuestReply,
1050
1066
  finalizeMarkdownPreview,
1051
- proactivePushChatIdGetter,
1052
1067
  proactivePushTargetGetter,
1053
1068
  isProactivePushEnabled: configControls.isProactivePushEnabled,
1054
- canSendProactivePush(ctx) {
1055
- return (
1056
- lockOwnershipGuard.ownsContext(ctx) ||
1057
- telegramBusFollowerRegistrationState.isRegistered()
1058
- );
1059
- },
1069
+ getAssistantRenderingMode: configControls.getAssistantRenderingMode,
1070
+ recordMessageOwnership: messageOwnershipRuntime.recordLocal,
1060
1071
  canSendAgentActivity(ctx) {
1061
1072
  return (
1062
1073
  lockOwnershipGuard.ownsContext(ctx) ||
package/lib/activity.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Telegram activity lifecycle normalization and extension dispatch
3
3
  * Zones: pi agent lifecycle, extension API, operational delivery
4
- * Owns stable handler registration, evidence-based activity/source identity, assistant segment and reasoning normalization, executed-tool and compaction events, isolated non-blocking queues, shutdown fencing, diagnostics, and fresh delivery contexts; excludes Pi hook wiring, visibility policy, Telegram rendering, and consumer-extension behavior
4
+ * Owns stable handler registration, evidence-based activity/source identity, assistant segment and reasoning normalization, ordered public-output projection, executed-tool and compaction events, isolated non-blocking queues, shutdown fencing, diagnostics, and fresh delivery contexts; excludes Pi hook wiring, Telegram rendering implementation, raw transport clients, and consumer-extension behavior
5
5
  */
6
6
 
7
7
  import {
@@ -91,6 +91,9 @@ export type TelegramActivityPayload =
91
91
  export type TelegramActivityEvent = TelegramActivityEnvelope &
92
92
  TelegramActivityPayload;
93
93
 
94
+ export type TelegramAssistantSegmentEvent = TelegramActivityEnvelope &
95
+ Extract<TelegramActivityPayload, { type: "assistant-segment" }>;
96
+
94
97
  export interface TelegramActivityContext {
95
98
  activityId: string;
96
99
  sequence: number;
@@ -383,6 +386,7 @@ export function createTelegramActivityDispatcher(deps: {
383
386
  /** @internal */
384
387
  export function createTelegramActivityBridgeRuntime(deps: {
385
388
  generation: string;
389
+ observeEvent?: (event: TelegramActivityEvent) => void;
386
390
  recordFailure?: (
387
391
  handlerId: string,
388
392
  event: TelegramActivityEvent,
@@ -401,6 +405,11 @@ export function createTelegramActivityBridgeRuntime(deps: {
401
405
  dispatcher: createTelegramActivityDispatcher({
402
406
  recordFailure: deps.recordFailure,
403
407
  }),
408
+ observeEvent: deps.observeEvent,
409
+ recordObserverFailure: deps.recordFailure
410
+ ? (event, error) =>
411
+ deps.recordFailure!("@llblab/pi-telegram/proactive", event, error)
412
+ : undefined,
404
413
  now: deps.now,
405
414
  });
406
415
  },
@@ -507,6 +516,11 @@ interface PendingAssistantSegment {
507
516
  export function createTelegramActivityRuntime(deps: {
508
517
  generation: string;
509
518
  dispatcher: TelegramActivityDispatcher;
519
+ observeEvent?: (event: TelegramActivityEvent) => void;
520
+ recordObserverFailure?: (
521
+ event: TelegramActivityEvent,
522
+ error: unknown,
523
+ ) => void;
510
524
  now?: () => number;
511
525
  }): TelegramActivityRuntime {
512
526
  const now = deps.now ?? Date.now;
@@ -542,14 +556,20 @@ export function createTelegramActivityRuntime(deps: {
542
556
  const emit = (event: TelegramActivityPayload): void => {
543
557
  const currentActivityId = ensureActivity();
544
558
  sequence += 1;
545
- deps.dispatcher.dispatch({
559
+ const normalizedEvent = {
546
560
  ...event,
547
561
  activityId: currentActivityId,
548
562
  sequence,
549
563
  source: activitySource,
550
564
  ...(activityTarget ? { target: activityTarget } : {}),
551
565
  timestamp: now(),
552
- } as TelegramActivityEvent);
566
+ } as TelegramActivityEvent;
567
+ try {
568
+ deps.observeEvent?.(normalizedEvent);
569
+ } catch (error) {
570
+ deps.recordObserverFailure?.(normalizedEvent, error);
571
+ }
572
+ deps.dispatcher.dispatch(normalizedEvent);
553
573
  };
554
574
  const flushPendingSegment = (
555
575
  placement: "intermediate" | "final" | "terminal-partial",
@@ -681,3 +701,80 @@ export function createTelegramActivityRuntime(deps: {
681
701
  },
682
702
  };
683
703
  }
704
+
705
+ // --- Public Assistant Output Projection ---
706
+
707
+ export interface TelegramAssistantOutputRuntime {
708
+ start: () => void;
709
+ accept: (event: TelegramAssistantSegmentEvent) => void;
710
+ waitForIdle: () => Promise<void>;
711
+ stop: () => void;
712
+ }
713
+
714
+ export function createTelegramAssistantOutputRuntime<TAuthority = undefined>(deps: {
715
+ isEnabled: () => boolean;
716
+ captureAuthority?: () => TAuthority;
717
+ isAuthorityActive?: (authority: TAuthority) => boolean;
718
+ canDeliver: (event: TelegramAssistantSegmentEvent) => boolean;
719
+ send: (
720
+ event: TelegramAssistantSegmentEvent,
721
+ authority: TAuthority,
722
+ isAuthorityActive: () => boolean,
723
+ ) => Promise<void>;
724
+ recordFailure?: (
725
+ event: TelegramAssistantSegmentEvent,
726
+ error: unknown,
727
+ ) => void;
728
+ }): TelegramAssistantOutputRuntime {
729
+ let generation = 0;
730
+ let running = false;
731
+ let tail: Promise<void> = Promise.resolve();
732
+ const admitted = new Set<string>();
733
+ const isEligibleSource = (
734
+ source: TelegramAssistantSegmentEvent["source"],
735
+ ): boolean => source === "local" || source === "autonomous";
736
+
737
+ return {
738
+ start() {
739
+ generation += 1;
740
+ running = true;
741
+ admitted.clear();
742
+ tail = Promise.resolve();
743
+ },
744
+ accept(event) {
745
+ if (!running || !deps.isEnabled()) return;
746
+ if (!isEligibleSource(event.source) || !event.text.trim()) return;
747
+ const key = `${event.activityId}:${event.sequence}`;
748
+ if (admitted.has(key)) return;
749
+ admitted.add(key);
750
+ const admittedGeneration = generation;
751
+ const admittedAuthority = deps.captureAuthority?.();
752
+ tail = tail.then(async () => {
753
+ const isAdmittedAuthorityActive = () =>
754
+ running &&
755
+ generation === admittedGeneration &&
756
+ deps.isEnabled() &&
757
+ (deps.isAuthorityActive === undefined ||
758
+ deps.isAuthorityActive(admittedAuthority as TAuthority));
759
+ if (!isAdmittedAuthorityActive() || !deps.canDeliver(event)) return;
760
+ try {
761
+ await deps.send(
762
+ event,
763
+ admittedAuthority as TAuthority,
764
+ isAdmittedAuthorityActive,
765
+ );
766
+ } catch (error) {
767
+ deps.recordFailure?.(event, error);
768
+ }
769
+ });
770
+ },
771
+ waitForIdle() {
772
+ return tail;
773
+ },
774
+ stop() {
775
+ generation += 1;
776
+ running = false;
777
+ admitted.clear();
778
+ },
779
+ };
780
+ }
package/lib/bindings.ts CHANGED
@@ -19,6 +19,7 @@ import * as Preview from "./preview.ts";
19
19
  import * as Prompts from "./prompts.ts";
20
20
  import * as Queue from "./queue.ts";
21
21
  import * as Replies from "./replies.ts";
22
+ import * as Routing from "./routing.ts";
22
23
  import * as Runtime from "./runtime.ts";
23
24
  import * as Setup from "./setup.ts";
24
25
  import * as Status from "./status.ts";
@@ -35,6 +36,60 @@ type TelegramRuntimeEventRecorder = (
35
36
  type TelegramBridgeStatusUpdater =
36
37
  Status.TelegramStatusRuntime<Pi.ExtensionContext>["updateStatus"];
37
38
 
39
+ export interface TelegramAssistantOutputBindingRuntime<
40
+ TTransportStamp,
41
+ > {
42
+ runtime: Activity.TelegramAssistantOutputRuntime;
43
+ observeEvent: (event: Activity.TelegramActivityEvent) => void;
44
+ authority: Routing.TelegramAssistantOutputAuthorityRuntime<TTransportStamp>;
45
+ }
46
+
47
+ export function createTelegramAssistantOutputBindingRuntime<
48
+ TTransportStamp,
49
+ >(deps: {
50
+ isEnabled: () => boolean;
51
+ authority: {
52
+ getPreferredTarget: () => OutboundAttachments.TelegramQueuedOutboundAttachmentTurnView["target"] | undefined;
53
+ getFallbackChatId: () => number | undefined;
54
+ getTransportStamp: () => TTransportStamp;
55
+ isTransportStampActive: (stamp: TTransportStamp) => boolean;
56
+ ownsDirect: () => boolean;
57
+ getDirectEpoch: () => number | string | undefined;
58
+ isFollowerRegistered: () => boolean;
59
+ getFollowerGeneration: () => string | undefined;
60
+ };
61
+ sender: Parameters<
62
+ typeof OutboundHandlers.createTelegramAssistantOutputSender<TTransportStamp>
63
+ >[0];
64
+ recordRuntimeEvent: TelegramRuntimeEventRecorder;
65
+ }): TelegramAssistantOutputBindingRuntime<TTransportStamp> {
66
+ const authority =
67
+ Routing.createTelegramAssistantOutputAuthorityRuntime(deps.authority);
68
+ const send =
69
+ OutboundHandlers.createTelegramAssistantOutputSender<TTransportStamp>(
70
+ deps.sender,
71
+ );
72
+ const runtime = Activity.createTelegramAssistantOutputRuntime({
73
+ isEnabled: deps.isEnabled,
74
+ ...authority,
75
+ send,
76
+ recordFailure(event, error) {
77
+ deps.recordRuntimeEvent("proactive-push", error, {
78
+ activityId: event.activityId,
79
+ sequence: event.sequence,
80
+ placement: event.placement,
81
+ });
82
+ },
83
+ });
84
+ return {
85
+ runtime,
86
+ authority,
87
+ observeEvent(event) {
88
+ if (event.type === "assistant-segment") runtime.accept(event);
89
+ },
90
+ };
91
+ }
92
+
38
93
  interface TelegramCommandsAndToolsBindingDeps {
39
94
  pi: Pi.ExtensionAPI;
40
95
  configStore: Config.TelegramConfigStore;
@@ -43,6 +98,7 @@ interface TelegramCommandsAndToolsBindingDeps {
43
98
  activeTurnRuntime: Queue.TelegramActiveTurnStore<Queue.PendingTelegramTurn>;
44
99
  lockedPollingRuntime: Locks.TelegramLockedPollingRuntime<Pi.ExtensionContext>;
45
100
  stopPolling?: () => Promise<void | string>;
101
+ getDisconnectThreadName?: () => string | undefined;
46
102
  onTransportChanged?: () => Promise<void> | void;
47
103
  getStatusLines: (
48
104
  options?: Status.TelegramBridgeStatusLineOptions,
@@ -70,6 +126,7 @@ export function registerTelegramCommandsAndTools({
70
126
  activeTurnRuntime,
71
127
  lockedPollingRuntime,
72
128
  stopPolling,
129
+ getDisconnectThreadName,
73
130
  onTransportChanged,
74
131
  getStatusLines,
75
132
  buttonActionStore,
@@ -186,6 +243,7 @@ export function registerTelegramCommandsAndTools({
186
243
  hasBotToken: configStore.hasBotToken,
187
244
  startPolling: lockedPollingRuntime.start,
188
245
  stopPolling: stopPolling ?? lockedPollingRuntime.stop,
246
+ getDisconnectThreadName,
189
247
  updateStatus,
190
248
  getProfileNames: () =>
191
249
  Config.getTelegramProfileNames(configStore.getStoredConfig()),
@@ -217,6 +275,10 @@ export function registerTelegramCommandsAndTools({
217
275
  interface TelegramLifecycleBindingDeps {
218
276
  pi: Pi.ExtensionAPI;
219
277
  activityRuntime: Activity.TelegramActivityRuntime;
278
+ assistantOutputRuntime: Pick<
279
+ Activity.TelegramAssistantOutputRuntime,
280
+ "start" | "stop"
281
+ >;
220
282
  sessionLifecycleRuntime: Pick<
221
283
  Lifecycle.TelegramLifecycleRegistrationDeps,
222
284
  "onSessionStart" | "onSessionShutdown" | "onModelSelect"
@@ -282,10 +344,14 @@ interface TelegramLifecycleBindingDeps {
282
344
  Pi.AgentEndEvent["messages"][number],
283
345
  Keyboard.TelegramInlineKeyboardMarkup
284
346
  >["finalizeMarkdownPreview"];
285
- proactivePushChatIdGetter: () => number | undefined;
286
347
  proactivePushTargetGetter: () => Queue.TelegramQueueTarget | undefined;
287
348
  isProactivePushEnabled: () => boolean;
288
- canSendProactivePush: (ctx: Pi.ExtensionContext) => boolean;
349
+ getAssistantRenderingMode: () => "rich" | "html";
350
+ recordMessageOwnership?: (input: {
351
+ chatId: number;
352
+ messageId: number;
353
+ target?: Queue.TelegramQueueTarget;
354
+ }) => void;
289
355
  canSendAgentActivity: (ctx: Pi.ExtensionContext) => boolean;
290
356
  isSessionContextActive: (ctx: Pi.ExtensionContext) => boolean;
291
357
  isTurnTransportActive?: (turn: Queue.PendingTelegramTurn) => boolean;
@@ -296,6 +362,7 @@ interface TelegramLifecycleBindingDeps {
296
362
  export function registerTelegramLifecycleRuntimeHooks({
297
363
  pi,
298
364
  activityRuntime,
365
+ assistantOutputRuntime,
299
366
  sessionLifecycleRuntime,
300
367
  configStore,
301
368
  abort,
@@ -319,10 +386,10 @@ export function registerTelegramLifecycleRuntimeHooks({
319
386
  deleteMessage,
320
387
  sendGuestReply,
321
388
  finalizeMarkdownPreview,
322
- proactivePushChatIdGetter,
323
389
  proactivePushTargetGetter,
324
390
  isProactivePushEnabled,
325
- canSendProactivePush,
391
+ getAssistantRenderingMode,
392
+ recordMessageOwnership,
326
393
  canSendAgentActivity,
327
394
  isSessionContextActive = () => true,
328
395
  isTurnTransportActive,
@@ -343,13 +410,20 @@ export function registerTelegramLifecycleRuntimeHooks({
343
410
  sendTextReply,
344
411
  recordRuntimeEvent,
345
412
  });
413
+ const richAttachmentSender =
414
+ OutboundAttachments.createTelegramRichOutboundAttachmentSender({
415
+ sendMultipart: callMultipart,
416
+ getRenderingMode: getAssistantRenderingMode,
417
+ recordOwnership: recordMessageOwnership,
418
+ recordRuntimeEvent,
419
+ });
346
420
  const sendGuestAttachment = async (
347
421
  turn: Queue.PendingTelegramTurn,
348
422
  attachment: Queue.QueuedAttachment,
349
423
  caption?: string,
350
424
  ): Promise<void> => {
351
425
  const stagingTarget = proactivePushTargetGetter();
352
- const stagingChatId = stagingTarget?.chatId ?? proactivePushChatIdGetter();
426
+ const stagingChatId = stagingTarget?.chatId;
353
427
  if (stagingChatId === undefined) {
354
428
  throw new Error(
355
429
  "Guest attachment staging requires a paired Telegram chat",
@@ -394,7 +468,7 @@ export function registerTelegramLifecycleRuntimeHooks({
394
468
  caption?: string,
395
469
  ): Promise<void> => {
396
470
  const stagingTarget = proactivePushTargetGetter();
397
- const stagingChatId = stagingTarget?.chatId ?? proactivePushChatIdGetter();
471
+ const stagingChatId = stagingTarget?.chatId;
398
472
  if (stagingChatId === undefined) {
399
473
  throw new Error("Guest voice staging requires a paired Telegram chat");
400
474
  }
@@ -499,16 +573,13 @@ export function registerTelegramLifecycleRuntimeHooks({
499
573
  sendMarkdownReply,
500
574
  sendTextReply,
501
575
  sendQueuedAttachments: queuedAttachmentSender,
576
+ sendRichAttachmentReply: richAttachmentSender,
502
577
  answerGuestQuery,
503
578
  sendGuestReply,
504
579
  sendGuestAttachment,
505
580
  sendGuestVoiceReply,
506
581
  planOutboundReply: outboundReplyPlanner,
507
582
  sendOutboundReplyArtifacts: outboundReplyArtifactSender,
508
- getDefaultChatId: proactivePushChatIdGetter,
509
- getDefaultTarget: proactivePushTargetGetter,
510
- isProactivePushEnabled,
511
- canSendProactivePush,
512
583
  recordRuntimeEvent,
513
584
  getActiveToolExecutions: lifecycle.getActiveToolExecutions,
514
585
  setActiveToolExecutions: lifecycle.setActiveToolExecutions,
@@ -522,11 +593,9 @@ export function registerTelegramLifecycleRuntimeHooks({
522
593
  if (!canSendAgentActivity(ctx)) return false;
523
594
  const turn = activeTurnRuntime.get();
524
595
  const target = turn?.target ?? proactivePushTargetGetter();
525
- promptDispatchRuntime.startTypingLoop(
526
- ctx,
527
- turn?.chatId ?? target?.chatId ?? proactivePushChatIdGetter(),
528
- { target },
529
- );
596
+ promptDispatchRuntime.startTypingLoop(ctx, turn?.chatId ?? target?.chatId, {
597
+ target,
598
+ });
530
599
  return true;
531
600
  };
532
601
  const startActiveTurnTypingLoop = (ctx: Pi.ExtensionContext): void => {
@@ -565,12 +634,14 @@ export function registerTelegramLifecycleRuntimeHooks({
565
634
  },
566
635
  async onSessionStart(event, ctx) {
567
636
  previewRuntime.invalidate();
637
+ assistantOutputRuntime.start();
568
638
  activityRuntime.onSessionStart?.();
569
639
  await sessionLifecycleRuntime.onSessionStart(event, ctx);
570
640
  },
571
641
  async onSessionShutdown(event, ctx) {
572
642
  if (!isSessionContextActive(ctx)) return;
573
643
  activityRuntime.onSessionShutdown();
644
+ assistantOutputRuntime.stop();
574
645
  compactionObserver.onSessionShutdown();
575
646
  await sessionLifecycleRuntime.onSessionShutdown(event, ctx);
576
647
  },