@llblab/pi-telegram 0.17.4 → 0.18.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 (62) hide show
  1. package/AGENTS.md +67 -32
  2. package/BACKLOG.md +59 -14
  3. package/CHANGELOG.md +40 -15
  4. package/README.md +63 -35
  5. package/docs/README.md +3 -1
  6. package/docs/architecture.md +55 -23
  7. package/docs/callback-namespaces.md +1 -1
  8. package/docs/inbound.md +1 -1
  9. package/docs/locks.md +0 -2
  10. package/docs/multi-instance-bus.md +483 -0
  11. package/docs/outbound.md +4 -3
  12. package/docs/public-api.md +12 -10
  13. package/docs/sections.md +2 -2
  14. package/docs/ui-style.md +76 -0
  15. package/index.ts +789 -32
  16. package/lib/bindings.ts +68 -12
  17. package/lib/bus-api.ts +314 -0
  18. package/lib/bus-follower.ts +853 -0
  19. package/lib/bus-leader.ts +915 -0
  20. package/lib/bus.ts +866 -0
  21. package/lib/command-templates.ts +9 -11
  22. package/lib/commands.ts +133 -47
  23. package/lib/config.ts +53 -5
  24. package/lib/lifecycle.ts +23 -7
  25. package/lib/locks.ts +230 -66
  26. package/lib/media.ts +30 -2
  27. package/lib/menu-model.ts +48 -17
  28. package/lib/menu-queue.ts +51 -20
  29. package/lib/menu-settings.ts +9 -5
  30. package/lib/menu-status.ts +3 -0
  31. package/lib/menu-thinking.ts +3 -0
  32. package/lib/menu.ts +67 -26
  33. package/lib/outbound-attachments.ts +102 -17
  34. package/lib/outbound-buttons.ts +6 -2
  35. package/lib/outbound-voice.ts +31 -11
  36. package/lib/outbound.ts +6 -4
  37. package/lib/ownership.ts +119 -0
  38. package/lib/pi.ts +26 -3
  39. package/lib/polling.ts +477 -7
  40. package/lib/preview.ts +141 -88
  41. package/lib/prompt-templates.ts +3 -3
  42. package/lib/prompts.ts +80 -30
  43. package/lib/queue.ts +193 -91
  44. package/lib/rendering.ts +0 -25
  45. package/lib/replies.ts +187 -55
  46. package/lib/routing.ts +1673 -9
  47. package/lib/runtime-log.ts +123 -0
  48. package/lib/runtime.ts +84 -12
  49. package/lib/sections.ts +28 -21
  50. package/lib/setup.ts +1 -1
  51. package/lib/status.ts +532 -9
  52. package/lib/sync.ts +618 -0
  53. package/lib/target.ts +49 -0
  54. package/lib/telegram-api.ts +405 -40
  55. package/lib/text-groups.ts +5 -1
  56. package/lib/thread-reconciler.ts +915 -0
  57. package/lib/threads.ts +2205 -0
  58. package/lib/turns.ts +48 -3
  59. package/lib/updates.ts +355 -32
  60. package/package.json +24 -2
  61. package/screenshot.png +0 -0
  62. package/docs/telegram-bot-api-rich-messages.md +0 -890
package/index.ts CHANGED
@@ -5,9 +5,14 @@
5
5
  */
6
6
 
7
7
  import * as Bindings from "./lib/bindings.ts";
8
+ import * as BusApi from "./lib/bus-api.ts";
9
+ import * as Bus from "./lib/bus.ts";
10
+ import * as BusFollower from "./lib/bus-follower.ts";
11
+ import * as BusLeader from "./lib/bus-leader.ts";
8
12
  import * as CommandTemplates from "./lib/command-templates.ts";
9
13
  import * as Commands from "./lib/commands.ts";
10
14
  import * as Config from "./lib/config.ts";
15
+ import * as Threads from "./lib/threads.ts";
11
16
  import * as Inbound from "./lib/inbound.ts";
12
17
  import * as Lifecycle from "./lib/lifecycle.ts";
13
18
  import * as Locks from "./lib/locks.ts";
@@ -17,6 +22,7 @@ import * as MenuSettings from "./lib/menu-settings.ts";
17
22
  import * as Menu from "./lib/menu.ts";
18
23
  import * as Model from "./lib/model.ts";
19
24
  import * as Outbound from "./lib/outbound.ts";
25
+ import * as Ownership from "./lib/ownership.ts";
20
26
  import * as Pi from "./lib/pi.ts";
21
27
  import * as Polling from "./lib/polling.ts";
22
28
  import * as Preview from "./lib/preview.ts";
@@ -25,10 +31,13 @@ import * as Queue from "./lib/queue.ts";
25
31
  import * as Replies from "./lib/replies.ts";
26
32
  import * as Routing from "./lib/routing.ts";
27
33
  import * as Runtime from "./lib/runtime.ts";
34
+ import * as RuntimeLog from "./lib/runtime-log.ts";
28
35
  import * as Sections from "./lib/sections.ts";
29
36
  import * as Status from "./lib/status.ts";
37
+ import * as Sync from "./lib/sync.ts";
30
38
  import * as TelegramApi from "./lib/telegram-api.ts";
31
39
  import * as TextGroups from "./lib/text-groups.ts";
40
+ import * as ThreadReconciler from "./lib/thread-reconciler.ts";
32
41
  import * as TimeInjection from "./lib/time-injection.ts";
33
42
  import * as Updates from "./lib/updates.ts";
34
43
  import * as Voice from "./lib/voice.ts";
@@ -47,6 +56,26 @@ export default function (pi: Pi.ExtensionAPI) {
47
56
  setThinkingLevel,
48
57
  } = piRuntime;
49
58
  const bridgeRuntime = Runtime.createTelegramBridgeRuntime();
59
+ const telegramInstanceId = `${process.pid}:${Date.now()}`;
60
+ const telegramManualFollowerOwnerId = String(process.pid);
61
+ const telegramManualFollowerProfileKey = `manual:${telegramManualFollowerOwnerId}`;
62
+ const telegramBusAuthSecret = Bus.createTelegramBusAuthSecret();
63
+ let telegramActiveBusAuthSecret: string | undefined;
64
+ let telegramBusLifecycleOverridePhase:
65
+ | Status.TelegramBridgeBusLifecyclePhase
66
+ | undefined;
67
+ const telegramBusSocketPath = Bus.getTelegramBusSocketPath();
68
+ const telegramBusFollowerSocketPath =
69
+ Bus.getTelegramBusFollowerSocketPath(telegramInstanceId);
70
+ let telegramBusRequestSequence = 0;
71
+ const telegramBusFollowerRegistry = Bus.createTelegramBusFollowerRegistry();
72
+ const telegramBusFollowerRegistrationState =
73
+ BusFollower.createTelegramBusFollowerRegistrationState();
74
+ let telegramBusLeaderTarget: Config.TelegramProactivePushTarget | undefined;
75
+ let telegramBusLeaderSlot: string | undefined;
76
+ let telegramTopicModeUnavailable = false;
77
+ let telegramTopicProvisioningCount = 0;
78
+ const messageOwnershipStore = Ownership.createTelegramMessageOwnershipStore();
50
79
  const { abort, lifecycle, queue, setup, typing } = bridgeRuntime;
51
80
  let configStoreForRedaction: Config.TelegramConfigStore | undefined;
52
81
  const runtimeEvents = Status.createTelegramRuntimeEventRecorder({
@@ -54,14 +83,46 @@ export default function (pi: Pi.ExtensionAPI) {
54
83
  return configStoreForRedaction?.getBotToken();
55
84
  },
56
85
  });
57
- const recordRuntimeEvent = runtimeEvents.record;
86
+ const runtimeJsonlLog = RuntimeLog.createTelegramRuntimeJsonlLog();
87
+ runtimeJsonlLog.reset("extension-start", {
88
+ instanceId: telegramInstanceId,
89
+ pid: process.pid,
90
+ });
91
+ let scheduleRuntimeDiagnosticsSnapshotPersist = function () {};
92
+ const recordRuntimeEvent = function (
93
+ category: string,
94
+ error: unknown,
95
+ details?: Record<string, unknown>,
96
+ ) {
97
+ runtimeEvents.record(category, error, details);
98
+ const latestEvent = runtimeEvents.getEvents().at(-1);
99
+ if (latestEvent) runtimeJsonlLog.record(latestEvent);
100
+ scheduleRuntimeDiagnosticsSnapshotPersist();
101
+ };
58
102
  const configStore = Config.createTelegramConfigStore({ recordRuntimeEvent });
59
103
  configStoreForRedaction = configStore;
104
+ const isTelegramBusConfigured = function (): boolean {
105
+ return true;
106
+ };
107
+ const isTelegramBusRuntimeEnabled = function (): boolean {
108
+ return isTelegramBusConfigured() && !telegramTopicModeUnavailable;
109
+ };
60
110
  Config.bindGlobalTelegramConfigRuntime(configStore);
61
111
  const configControls = Config.createTelegramConfigControls(configStore);
62
- const lockRuntime = Locks.createTelegramLockRuntime<Pi.ExtensionContext>();
112
+ const threadStore = Threads.createTelegramTopicTargetStore({
113
+ path: Threads.getTelegramTopicTargetsPath(),
114
+ });
115
+ const lockRuntime = Locks.createTelegramLockRuntime<Pi.ExtensionContext>({
116
+ instanceId: telegramInstanceId,
117
+ busSecret: telegramBusAuthSecret,
118
+ staleHeartbeatMs: Locks.TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS,
119
+ });
63
120
  const lockOwnershipGuard =
64
121
  Locks.createTelegramLockOwnershipGuard(lockRuntime);
122
+ const getCurrentLeaderEpoch = function (): number | undefined {
123
+ const state = lockRuntime.getState();
124
+ return state.kind === "active-here" ? state.lock.leaderEpoch : undefined;
125
+ };
65
126
  const telegramSessionContextStore =
66
127
  Lifecycle.createTelegramSessionContextStore<Pi.ExtensionContext>();
67
128
  const ownsTelegramDirectDelivery =
@@ -75,6 +136,17 @@ export default function (pi: Pi.ExtensionAPI) {
75
136
  getActiveTurnChatId: activeTurnRuntime.getChatId,
76
137
  getAllowedUserId: configStore.getAllowedUserId,
77
138
  });
139
+ const proactivePushTargetGetter =
140
+ Config.createTelegramProactivePushTargetGetter({
141
+ getActiveTurnTarget: activeTurnRuntime.getTarget,
142
+ getAssignedTarget() {
143
+ return (
144
+ telegramBusFollowerRegistrationState.getTarget() ??
145
+ telegramBusLeaderTarget
146
+ );
147
+ },
148
+ getAllowedUserId: configStore.getAllowedUserId,
149
+ });
78
150
  const buttonActionStore = Outbound.createTelegramButtonActionStore();
79
151
  const pendingModelSwitchStore =
80
152
  Model.createPendingModelSwitchStore<
@@ -108,26 +180,151 @@ export default function (pi: Pi.ExtensionAPI) {
108
180
  recordRuntimeEvent,
109
181
  });
110
182
  const pollingControllerState = Polling.createTelegramPollingControllerState();
183
+ let telegramSyncState = Sync.createUnknownTelegramSyncState();
184
+ const threadReconciliationRuntime =
185
+ ThreadReconciler.createThreadReconciliationRuntime({
186
+ recordRuntimeEvent,
187
+ scheduleSnapshotPersist() {
188
+ scheduleRuntimeDiagnosticsSnapshotPersist();
189
+ },
190
+ });
191
+ const recordThreadReconciliationPlan = threadReconciliationRuntime.recordPlan;
192
+ const markTelegramConfigSyncChange = function (action: string) {
193
+ telegramSyncState = Sync.markTelegramConfigSyncChange(
194
+ telegramSyncState,
195
+ action,
196
+ );
197
+ };
198
+ const persistTelegramConfigWithSync = async function () {
199
+ await configStore.persist();
200
+ markTelegramConfigSyncChange("config-persist");
201
+ };
202
+ const getCurrentThreadRecord = function ():
203
+ | Threads.TelegramTopicTargetRecord
204
+ | undefined {
205
+ return Threads.findCurrentTelegramInstanceThreadRecord({
206
+ records: threadStore.list(),
207
+ instanceId: telegramInstanceId,
208
+ preferredTarget:
209
+ activeTurnRuntime.getTarget() ??
210
+ telegramBusFollowerRegistrationState.getTarget() ??
211
+ telegramBusLeaderTarget,
212
+ });
213
+ };
111
214
  const statusRuntime = Status.createTelegramBridgeStatusRuntime<
112
215
  Pi.ExtensionContext,
113
216
  Queue.TelegramQueueItem<Pi.ExtensionContext>
114
217
  >({
115
- getConfig: configStore.get,
116
- isPollingActive: Polling.createTelegramPollingActivityReader(
117
- pollingControllerState,
118
- ),
119
- getActiveSourceMessageIds: activeTurnRuntime.getSourceMessageIds,
120
- hasActiveTurn: activeTurnRuntime.has,
121
- hasDispatchPending: lifecycle.hasDispatchPending,
122
- isCompactionInProgress: lifecycle.isCompactionInProgress,
123
- getActiveToolExecutions: lifecycle.getActiveToolExecutions,
124
- hasPendingModelSwitch: pendingModelSwitchStore.has,
125
- getQueuedItems: telegramQueueStore.getQueuedItems,
126
- formatQueuedStatus: Queue.formatQueuedTelegramItemsStatus,
127
- getRecentRuntimeEvents: runtimeEvents.getEvents,
128
- getRuntimeLockState: lockRuntime.getStatusLabel,
218
+ getConfig: configStore.get,
219
+ isPollingActive: Polling.createTelegramPollingActivityReader(
220
+ pollingControllerState,
221
+ ),
222
+ getActiveSourceMessageIds: activeTurnRuntime.getSourceMessageIds,
223
+ hasActiveTurn: activeTurnRuntime.has,
224
+ hasDispatchPending: lifecycle.hasDispatchPending,
225
+ isCompactionInProgress: lifecycle.isCompactionInProgress,
226
+ getActiveToolExecutions: lifecycle.getActiveToolExecutions,
227
+ hasPendingModelSwitch: pendingModelSwitchStore.has,
228
+ getQueuedItems: telegramQueueStore.getQueuedItems,
229
+ formatQueuedStatus: Queue.formatQueuedTelegramItemsStatus,
230
+ getRecentRuntimeEvents: runtimeEvents.getEvents,
231
+ getRuntimeLockState: lockRuntime.getStatusLabel,
232
+ getBusRole() {
233
+ if (threadStore.getBotState().threadMode === "disabled") return undefined;
234
+ if (pollingStartedWithTelegramBus) return "leader";
235
+ if (telegramBusFollowerRegistrationState.isRegistered())
236
+ return "follower";
237
+ return undefined;
238
+ },
239
+ getBusLifecyclePhase() {
240
+ return telegramBusLifecycleOverridePhase;
241
+ },
242
+ getBotThreadMode() {
243
+ return threadStore.getBotState();
244
+ },
245
+ getBusFollowers() {
246
+ return Threads.listTelegramThreadStatusFollowers({
247
+ followers: telegramBusFollowerRegistry.list(),
248
+ records: threadStore.list(),
249
+ });
250
+ },
251
+ getTopicTargets() {
252
+ return Threads.listTelegramThreadStatusTargets(threadStore.list());
253
+ },
254
+ getThreadReservations() {
255
+ return Threads.listTelegramThreadStatusReservations(
256
+ threadStore.listReservations(),
257
+ );
258
+ },
259
+ getTopicSyncObservations() {
260
+ return Threads.listTelegramThreadStatusObservations(
261
+ threadStore.listSyncObservations(),
262
+ );
263
+ },
264
+ getSyncState() {
265
+ return telegramSyncState;
266
+ },
267
+ getThreadReconciliationState() {
268
+ return threadReconciliationRuntime.getState();
269
+ },
270
+ getInstanceSlot() {
271
+ if (threadStore.getBotState().threadMode === "disabled") return undefined;
272
+ return (
273
+ getCurrentThreadRecord()?.slot ??
274
+ telegramBusFollowerRegistrationState.getSlot() ??
275
+ telegramBusLeaderSlot
276
+ );
277
+ },
278
+ getInstanceThreadName() {
279
+ if (threadStore.getBotState().threadMode === "disabled") return undefined;
280
+ return (
281
+ getCurrentThreadRecord()?.threadName ??
282
+ telegramBusFollowerRegistrationState.getThreadName()
283
+ );
284
+ },
285
+ });
286
+ const { updateStatus: updateStatusLine } = statusRuntime;
287
+ let lastRuntimeLogScopeKey: string | undefined;
288
+ const updateRuntimeLogScope = function (reason: string) {
289
+ const scope = Status.createTelegramRuntimeLogScope({
290
+ state: statusRuntime.getStatusState(),
291
+ instanceId: telegramInstanceId,
292
+ });
293
+ const scopeKey = JSON.stringify(scope);
294
+ if (scopeKey === lastRuntimeLogScopeKey) return;
295
+ lastRuntimeLogScopeKey = scopeKey;
296
+ runtimeJsonlLog.resetIfScopeChanged(scopeKey, reason, scope);
297
+ };
298
+ const updateStatus = function (ctx: Pi.ExtensionContext) {
299
+ updateStatusLine(ctx);
300
+ updateRuntimeLogScope("status-scope-change");
301
+ };
302
+ const persistCurrentStatusSnapshot = function () {
303
+ threadStore.setStatusSnapshot(
304
+ Status.createTelegramStatusSnapshot(statusRuntime.getStatusState()),
305
+ );
306
+ return threadStore.persist();
307
+ };
308
+ scheduleRuntimeDiagnosticsSnapshotPersist =
309
+ Status.createTelegramRuntimeDiagnosticsSnapshotScheduler({
310
+ persistSnapshot: persistCurrentStatusSnapshot,
311
+ recordError(error) {
312
+ runtimeEvents.record("telegram", error, {
313
+ phase: "runtime-diagnostics-snapshot-persist",
314
+ });
315
+ },
129
316
  });
130
- const { getStatusLines, updateStatus } = statusRuntime;
317
+ const getStatusLines = function (
318
+ options?: Status.TelegramBridgeStatusLineOptions,
319
+ ): string[] {
320
+ const state = statusRuntime.getStatusState();
321
+ void persistCurrentStatusSnapshot().catch(function (error) {
322
+ recordRuntimeEvent("telegram", error, {
323
+ phase: "status-snapshot-persist",
324
+ });
325
+ });
326
+ return Status.buildTelegramBridgeStatusLines(state, options);
327
+ };
131
328
  const inboundHandlerRuntime = Inbound.createTelegramInboundHandlerRuntime({
132
329
  getHandlers: configStore.getInboundHandlers,
133
330
  execCommand: CommandTemplates.execCommandTemplate,
@@ -137,7 +334,37 @@ export default function (pi: Pi.ExtensionAPI) {
137
334
 
138
335
  // --- Telegram API ---
139
336
 
337
+ const directTelegramApiRuntime =
338
+ TelegramApi.createDefaultTelegramBridgeApiRuntime({
339
+ getBotToken: configStore.getBotToken,
340
+ recordRuntimeEvent,
341
+ });
342
+ const telegramApiRuntime = BusApi.createTelegramBusAwareApiRuntime({
343
+ directRuntime: directTelegramApiRuntime,
344
+ ownsDirect() {
345
+ return (
346
+ lockRuntime.owns() ||
347
+ !telegramBusFollowerRegistrationState.isRegistered()
348
+ );
349
+ },
350
+ getDefaultTarget: proactivePushTargetGetter,
351
+ callFollowerApi: BusFollower.createTelegramBusFollowerApiCaller({
352
+ socketPath: telegramBusSocketPath,
353
+ instanceId: telegramInstanceId,
354
+ createRequestId() {
355
+ telegramBusRequestSequence += 1;
356
+ return Bus.createTelegramBusRequestId({
357
+ instanceId: telegramInstanceId,
358
+ sequence: telegramBusRequestSequence,
359
+ });
360
+ },
361
+ getAuthSecret() {
362
+ return telegramActiveBusAuthSecret;
363
+ },
364
+ }),
365
+ });
140
366
  const {
367
+ call: callTelegramApi,
141
368
  callMultipart,
142
369
  deleteWebhook,
143
370
  getUpdates,
@@ -155,10 +382,7 @@ export default function (pi: Pi.ExtensionAPI) {
155
382
  answerGuestQuery,
156
383
  deleteMessage: deleteTelegramMessage,
157
384
  prepareTempDir,
158
- } = TelegramApi.createDefaultTelegramBridgeApiRuntime({
159
- getBotToken: configStore.getBotToken,
160
- recordRuntimeEvent,
161
- });
385
+ } = telegramApiRuntime;
162
386
 
163
387
  // --- Message Delivery ---
164
388
 
@@ -171,6 +395,8 @@ export default function (pi: Pi.ExtensionAPI) {
171
395
  typing,
172
396
  getDefaultChatId: proactivePushChatIdGetter,
173
397
  sendTypingAction,
398
+ sendAggregateTypingAction:
399
+ BusApi.createTelegramAggregateTypingActionSender(telegramApiRuntime),
174
400
  updateStatus,
175
401
  recordRuntimeEvent,
176
402
  });
@@ -189,6 +415,14 @@ export default function (pi: Pi.ExtensionAPI) {
189
415
  // --- Reply Runtime & Preview ---
190
416
 
191
417
  const replyRuntime = Replies.createTelegramRenderedMessageDeliveryRuntime({
418
+ recordOwnership(input) {
419
+ messageOwnershipStore.record({
420
+ chatId: input.chatId,
421
+ messageId: input.messageId,
422
+ target: input.target,
423
+ instanceId: telegramInstanceId,
424
+ });
425
+ },
192
426
  sendMessage,
193
427
  sendRichMessage,
194
428
  editMessage: editTelegramMessageText,
@@ -218,15 +452,17 @@ export default function (pi: Pi.ExtensionAPI) {
218
452
  ...promptDispatchRuntime,
219
453
  sendUserMessage,
220
454
  }).dispatchNext;
455
+ const nativeMarkdownDraftSender =
456
+ TelegramApi.createTelegramNativeMarkdownDraftSender({
457
+ sendMessageDraft,
458
+ sendRichMessageDraft,
459
+ });
221
460
  const previewRuntime = Preview.createTelegramAssistantPreviewRuntime({
222
461
  getActiveTurn: activeTurnRuntime.get,
223
462
  isAssistantMessage: Replies.isAssistantAgentMessage,
224
463
  getMessageText: Replies.getAgentMessageText,
225
464
  getDefaultReplyToMessageId: activeTurnRuntime.getReplyToMessageId,
226
- sendDraft: TelegramApi.createTelegramNativeMarkdownDraftSender({
227
- sendMessageDraft,
228
- sendRichMessageDraft,
229
- }),
465
+ sendDraft: nativeMarkdownDraftSender,
230
466
  sendMarkdownReply,
231
467
  recordRuntimeEvent,
232
468
  ...replyTransport,
@@ -272,6 +508,7 @@ export default function (pi: Pi.ExtensionAPI) {
272
508
  buildStatusHtml: Status.createTelegramStatusHtmlBuilder({
273
509
  getActiveModel: currentModelRuntime.get,
274
510
  isCompactionInProgress: lifecycle.isCompactionInProgress,
511
+ getBridgeStatusLineState: statusRuntime.getStatusState,
275
512
  }),
276
513
  getPromptTemplateCommands,
277
514
  }),
@@ -324,8 +561,103 @@ export default function (pi: Pi.ExtensionAPI) {
324
561
 
325
562
  // --- Polling ---
326
563
 
564
+ const foreignOwnedUpdateForwarder =
565
+ Bus.createTelegramBusForeignOwnedUpdateForwarder<
566
+ Pi.ExtensionContext,
567
+ Updates.TelegramMessageReactionUpdated,
568
+ Routing.TelegramRoutedCallbackQuery,
569
+ Routing.TelegramRoutedMessage
570
+ >({
571
+ socketPath: telegramBusSocketPath,
572
+ createRequestId() {
573
+ telegramBusRequestSequence += 1;
574
+ return Bus.createTelegramBusRequestId({
575
+ instanceId: telegramInstanceId,
576
+ sequence: telegramBusRequestSequence,
577
+ });
578
+ },
579
+ getAuthSecret() {
580
+ return telegramBusAuthSecret;
581
+ },
582
+ timeoutMs: 30_000,
583
+ });
584
+ const followerTargetController =
585
+ Bus.createTelegramBusFollowerTargetController({
586
+ socketPath: telegramBusSocketPath,
587
+ createRequestId() {
588
+ telegramBusRequestSequence += 1;
589
+ return Bus.createTelegramBusRequestId({
590
+ instanceId: telegramInstanceId,
591
+ sequence: telegramBusRequestSequence,
592
+ });
593
+ },
594
+ getAuthSecret() {
595
+ return telegramBusAuthSecret;
596
+ },
597
+ timeoutMs: 30_000,
598
+ });
599
+ const restoreFollowerThreadTarget =
600
+ Bus.createTelegramBusFollowerThreadRestoreHandler({
601
+ followerRegistry: telegramBusFollowerRegistry,
602
+ followerTargetController,
603
+ onRestored() {
604
+ telegramSyncState = Sync.markTelegramSyncSliceFresh(
605
+ telegramSyncState,
606
+ "target-bindings",
607
+ { nowMs: Date.now(), action: "follower-thread-restore" },
608
+ );
609
+ },
610
+ });
611
+ let observeTelegramThreadTarget: Polling.TelegramThreadTargetObservationHandler<
612
+ Pi.ExtensionContext
613
+ > = async function noopTelegramThreadTargetObservation() {};
614
+ const topicLifecycleSync = Sync.createTelegramObservedTopicLifecycleSyncHandler({
615
+ topicTargetStore: threadStore,
616
+ isBusEnabled: isTelegramBusRuntimeEnabled,
617
+ callApi: callTelegramApi,
618
+ isTopicProvisioningActive() {
619
+ return telegramTopicProvisioningCount > 0;
620
+ },
621
+ getCurrentLeaderEpoch,
622
+ getThreadReconciliationMachineState() {
623
+ return threadReconciliationRuntime.getState();
624
+ },
625
+ recordThreadReconciliationPlan,
626
+ getSyncState() {
627
+ return telegramSyncState;
628
+ },
629
+ setSyncState(state) {
630
+ telegramSyncState = state;
631
+ },
632
+ recordEvent: recordRuntimeEvent,
633
+ });
327
634
  const inboundRouteRuntime = Routing.createTelegramInboundRouteRuntime({
328
635
  configStore,
636
+ callApi: callTelegramApi,
637
+ getCurrentInstanceId() {
638
+ return telegramInstanceId;
639
+ },
640
+ getMessageOwnership: messageOwnershipStore.get,
641
+ getTargetOwnership(target) {
642
+ return telegramBusFollowerRegistry.getByTarget(target);
643
+ },
644
+ getLiveThreadTargets() {
645
+ return Bus.listTelegramBusLiveThreadTargets({
646
+ leaderTarget: telegramBusLeaderTarget,
647
+ followers: telegramBusFollowerRegistry.list(),
648
+ });
649
+ },
650
+ getCurrentLeaderEpoch,
651
+ getThreadReconciliationMachineState() {
652
+ return threadReconciliationRuntime.getState();
653
+ },
654
+ recordThreadReconciliationPlan,
655
+ handleTelegramTopicLifecycleUpdate: topicLifecycleSync,
656
+ handleTelegramThreadTargetObserved(_target, ctx) {
657
+ return observeTelegramThreadTarget(ctx);
658
+ },
659
+ foreignOwnedUpdateForwarder,
660
+ replaceFollowerThreadTarget: restoreFollowerThreadTarget,
329
661
  bridgeRuntime,
330
662
  activeTurnRuntime,
331
663
  mediaGroupRuntime,
@@ -344,6 +676,7 @@ export default function (pi: Pi.ExtensionAPI) {
344
676
  sectionRegistry,
345
677
  buttonActionStore,
346
678
  inboundHandlerRuntime,
679
+ threadStore,
347
680
  updateStatus,
348
681
  dispatchNextQueuedTelegramTurn,
349
682
  requestDeferredDispatchNextQueuedTelegramTurn:
@@ -373,13 +706,123 @@ export default function (pi: Pi.ExtensionAPI) {
373
706
  compact,
374
707
  recordRuntimeEvent,
375
708
  });
709
+ const telegramBusForwardedUpdateReceiver =
710
+ BusFollower.createTelegramBusForwardedUpdateReceiverRuntime<
711
+ Pi.ExtensionContext,
712
+ Updates.TelegramMessageReactionUpdated,
713
+ Routing.TelegramRoutedCallbackQuery,
714
+ Routing.TelegramRoutedMessage
715
+ >({
716
+ socketPath: telegramBusFollowerSocketPath,
717
+ instanceId: telegramInstanceId,
718
+ getContext() {
719
+ return telegramSessionContextStore.get();
720
+ },
721
+ getAuthSecret() {
722
+ return telegramActiveBusAuthSecret;
723
+ },
724
+ handleForwardedCallback(query, ctx) {
725
+ return inboundRouteRuntime.handleUpdate({ callback_query: query }, ctx);
726
+ },
727
+ handleForwardedReaction(reactionUpdate, ctx) {
728
+ return inboundRouteRuntime.handleAuthorizedReactionUpdate(
729
+ reactionUpdate,
730
+ ctx,
731
+ );
732
+ },
733
+ handleForwardedMessage(message, ctx) {
734
+ return inboundRouteRuntime.handleUpdate(
735
+ { message: message as never },
736
+ ctx,
737
+ );
738
+ },
739
+ async handleForwardedEditedMessage(message, ctx) {
740
+ return inboundRouteRuntime.handleUpdate(
741
+ { edited_message: message as never },
742
+ ctx,
743
+ );
744
+ },
745
+ handleReplaceTarget:
746
+ BusFollower.createTelegramBusFollowerTargetReplacementHandler({
747
+ topicTargetStore: threadStore,
748
+ registrationState: telegramBusFollowerRegistrationState,
749
+ instanceId: telegramInstanceId,
750
+ manualFollowerProfileKey: telegramManualFollowerProfileKey,
751
+ manualFollowerOwnerId: telegramManualFollowerOwnerId,
752
+ getSyncState() {
753
+ return telegramSyncState;
754
+ },
755
+ setSyncState(state) {
756
+ telegramSyncState = state;
757
+ },
758
+ updateStatus,
759
+ recordRuntimeEvent,
760
+ }),
761
+ recordRuntimeEvent,
762
+ });
763
+ let telegramBusFollowerRegistration: BusFollower.TelegramBusFollowerRegistrationRuntime<Pi.ExtensionContext>;
764
+ const telegramFollowerHeartbeatRecovery =
765
+ BusFollower.createTelegramBusFollowerHeartbeatRecoveryHandler<Pi.ExtensionContext>(
766
+ {
767
+ registrationState: telegramBusFollowerRegistrationState,
768
+ getRegistrationRuntime() {
769
+ return telegramBusFollowerRegistration;
770
+ },
771
+ getLeaderState() {
772
+ return lockRuntime.getState();
773
+ },
774
+ setLifecyclePhase(phase) {
775
+ telegramBusLifecycleOverridePhase = phase;
776
+ },
777
+ updateStatus,
778
+ async promoteToLeader(ctx) {
779
+ await lockedPollingRuntime.start(ctx, { force: true });
780
+ },
781
+ sleep(ms) {
782
+ return Polling.sleepTelegramPollingRetry(ms);
783
+ },
784
+ promotionGraceMs: BusFollower.TELEGRAM_BUS_FOLLOWER_PROMOTION_GRACE_MS,
785
+ recordRuntimeEvent,
786
+ },
787
+ );
788
+ telegramBusFollowerRegistration =
789
+ BusFollower.createTelegramBusFollowerRegistrationRuntime<Pi.ExtensionContext>(
790
+ {
791
+ instanceId: telegramInstanceId,
792
+ followerBusSocketPath: telegramBusFollowerSocketPath,
793
+ startReceiving: telegramBusForwardedUpdateReceiver.start,
794
+ stopReceiving: telegramBusForwardedUpdateReceiver.stop,
795
+ registrationState: telegramBusFollowerRegistrationState,
796
+ createRequestId() {
797
+ telegramBusRequestSequence += 1;
798
+ return Bus.createTelegramBusRequestId({
799
+ instanceId: telegramInstanceId,
800
+ sequence: telegramBusRequestSequence,
801
+ });
802
+ },
803
+ getLeaderAuthSecret(owner) {
804
+ return owner.busSecret;
805
+ },
806
+ setActiveAuthSecret(secret) {
807
+ telegramActiveBusAuthSecret = secret;
808
+ },
809
+ getThreadName() {
810
+ return undefined;
811
+ },
812
+ getProfileKey() {
813
+ return telegramManualFollowerProfileKey;
814
+ },
815
+ onHeartbeatFailure: telegramFollowerHeartbeatRecovery,
816
+ recordRuntimeEvent,
817
+ },
818
+ );
376
819
  const pollingRuntime = Polling.createTelegramPollingControllerRuntime({
377
820
  state: pollingControllerState,
378
821
  getConfig: configStore.get,
379
822
  hasBotToken: configStore.hasBotToken,
380
823
  deleteWebhook,
381
824
  getUpdates,
382
- persistConfig: configStore.persist,
825
+ persistConfig: persistTelegramConfigWithSync,
383
826
  handleUpdate: Updates.createTelegramUpdateHandle({
384
827
  defaultHandle: inboundRouteRuntime.handleUpdate,
385
828
  }),
@@ -387,16 +830,292 @@ export default function (pi: Pi.ExtensionAPI) {
387
830
  updateStatus,
388
831
  recordRuntimeEvent,
389
832
  });
833
+ const recoverStaleTelegramTopicApiError = function (
834
+ apiBody: unknown,
835
+ error: unknown,
836
+ ) {
837
+ return Sync.recoverStaleTelegramTopicApiError(apiBody, error, {
838
+ topicTargetStore: threadStore,
839
+ getSyncState() {
840
+ return telegramSyncState;
841
+ },
842
+ setSyncState(state) {
843
+ telegramSyncState = state;
844
+ },
845
+ recordEvent: recordRuntimeEvent,
846
+ });
847
+ };
848
+ const telegramBusLeaderRuntime = BusLeader.createTelegramBusLeaderRuntime({
849
+ socketPath: telegramBusSocketPath,
850
+ followerRegistry: telegramBusFollowerRegistry,
851
+ authSecret: telegramBusAuthSecret,
852
+ startPolling: pollingRuntime.start,
853
+ stopPolling: pollingRuntime.stop,
854
+ authorizeFollowerApiCall: Bus.isTelegramFollowerApiCallAllowed,
855
+ provisionLeaderTarget:
856
+ BusLeader.createTelegramBusLeaderTargetProvisioner<Pi.ExtensionContext>({
857
+ getAllowedUserId: configStore.getAllowedUserId,
858
+ instanceId: telegramInstanceId,
859
+ getCwd(ctx) {
860
+ return typeof ctx.cwd === "string" ? ctx.cwd : undefined;
861
+ },
862
+ shouldForceFreshUnnamed() {
863
+ return forceFreshLeaderThreadOnNextStart;
864
+ },
865
+ topicTargetStore: threadStore,
866
+ callApi(method, body) {
867
+ return directTelegramApiRuntime.call(method, body);
868
+ },
869
+ getCurrentLeaderEpoch,
870
+ getThreadReconciliationMachineState() {
871
+ return threadReconciliationRuntime.getState();
872
+ },
873
+ recordThreadReconciliationPlan,
874
+ getSyncState() {
875
+ return telegramSyncState;
876
+ },
877
+ setSyncState(state) {
878
+ telegramSyncState = state;
879
+ },
880
+ setLeaderTarget(input) {
881
+ telegramBusLeaderTarget = input.target;
882
+ telegramBusLeaderSlot = input.slot;
883
+ },
884
+ onProvisioningStart() {
885
+ telegramTopicProvisioningCount += 1;
886
+ },
887
+ onProvisioningEnd() {
888
+ telegramTopicProvisioningCount = Math.max(
889
+ 0,
890
+ telegramTopicProvisioningCount - 1,
891
+ );
892
+ },
893
+ recordRuntimeEvent,
894
+ }),
895
+ onFollowerPruned: BusLeader.createTelegramBusFollowerPruneHandler({
896
+ topicTargetStore: threadStore,
897
+ callApi(method, body) {
898
+ return directTelegramApiRuntime.call(method, body);
899
+ },
900
+ getCurrentLeaderEpoch,
901
+ getSyncState() {
902
+ return telegramSyncState;
903
+ },
904
+ setSyncState(state) {
905
+ telegramSyncState = state;
906
+ },
907
+ recordRuntimeEvent,
908
+ }),
909
+ provisionFollowerTarget:
910
+ BusLeader.createTelegramBusFollowerTargetProvisioner({
911
+ getAllowedUserId: configStore.getAllowedUserId,
912
+ topicTargetStore: threadStore,
913
+ callApi(method, body) {
914
+ return directTelegramApiRuntime.call(method, body);
915
+ },
916
+ getCurrentLeaderEpoch,
917
+ getSyncState() {
918
+ return telegramSyncState;
919
+ },
920
+ setSyncState(state) {
921
+ telegramSyncState = state;
922
+ },
923
+ onProvisioningStart() {
924
+ telegramTopicProvisioningCount += 1;
925
+ },
926
+ onProvisioningEnd() {
927
+ telegramTopicProvisioningCount = Math.max(
928
+ 0,
929
+ telegramTopicProvisioningCount - 1,
930
+ );
931
+ },
932
+ recordRuntimeEvent,
933
+ }),
934
+ restoreFollowerRegistry:
935
+ BusLeader.createTelegramBusFollowerRegistryRestoreHandler({
936
+ topicTargetStore: threadStore,
937
+ followerRegistry: telegramBusFollowerRegistry,
938
+ recordRuntimeEvent,
939
+ }),
940
+ callApi: BusLeader.createTelegramBusLeaderApiProxy({
941
+ call: directTelegramApiRuntime.call,
942
+ callMultipart: directTelegramApiRuntime.callMultipart,
943
+ downloadFile: directTelegramApiRuntime.downloadFile,
944
+ recoverStaleTargetError: recoverStaleTelegramTopicApiError,
945
+ }),
946
+ recordRuntimeEvent,
947
+ });
948
+ let pollingStartedWithTelegramBus = false;
949
+ let forceFreshLeaderThreadOnNextStart = false;
950
+ const telegramLeaderHealthRuntime = Sync.createTelegramLeaderHealthRuntime({
951
+ callGetMe() {
952
+ return directTelegramApiRuntime.call("getMe", {});
953
+ },
954
+ getSyncState() {
955
+ return telegramSyncState;
956
+ },
957
+ setSyncState(state) {
958
+ telegramSyncState = state;
959
+ },
960
+ recordEvent: recordRuntimeEvent,
961
+ });
962
+ const telegramThreadCapabilityMonitor =
963
+ Polling.createTelegramThreadCapabilityMonitor<Pi.ExtensionContext>({
964
+ getAllowedUserId: configStore.getAllowedUserId,
965
+ callApi: callTelegramApi,
966
+ topicTargetStore: threadStore,
967
+ isBusConfigured: isTelegramBusConfigured,
968
+ ownsLock(ctx) {
969
+ return lockRuntime.owns(ctx);
970
+ },
971
+ getPollingStartedWithTelegramBus() {
972
+ return pollingStartedWithTelegramBus;
973
+ },
974
+ setPollingStartedWithTelegramBus(started) {
975
+ pollingStartedWithTelegramBus = started;
976
+ },
977
+ setTopicModeUnavailable(unavailable) {
978
+ telegramTopicModeUnavailable = unavailable;
979
+ },
980
+ stopFollowerRegistration: telegramBusFollowerRegistration.stop,
981
+ startClassicPolling(ctx) {
982
+ return pollingRuntime.start(ctx);
983
+ },
984
+ stopClassicPolling() {
985
+ return pollingRuntime.stop();
986
+ },
987
+ startBusPolling(ctx) {
988
+ return telegramBusLeaderRuntime.startPolling(ctx);
989
+ },
990
+ stopBusPolling() {
991
+ return telegramBusLeaderRuntime.stopPolling();
992
+ },
993
+ startLeaderHealth: telegramLeaderHealthRuntime.start,
994
+ stopLeaderHealth: telegramLeaderHealthRuntime.stop,
995
+ isTopicModeUnavailableError: Threads.isTelegramTopicModeUnavailableError,
996
+ updateStatus,
997
+ recordEvent: recordRuntimeEvent,
998
+ });
999
+ observeTelegramThreadTarget = Polling.createTelegramThreadTargetObservationHandler({
1000
+ getAllowedUserId: configStore.getAllowedUserId,
1001
+ callApi: callTelegramApi,
1002
+ topicTargetStore: threadStore,
1003
+ isBusConfigured: isTelegramBusConfigured,
1004
+ ownsLock(ctx) {
1005
+ return lockRuntime.owns(ctx);
1006
+ },
1007
+ getPollingStartedWithTelegramBus() {
1008
+ return pollingStartedWithTelegramBus;
1009
+ },
1010
+ setPollingStartedWithTelegramBus(started) {
1011
+ pollingStartedWithTelegramBus = started;
1012
+ },
1013
+ setTopicModeUnavailable(unavailable) {
1014
+ telegramTopicModeUnavailable = unavailable;
1015
+ },
1016
+ stopFollowerRegistration: telegramBusFollowerRegistration.stop,
1017
+ startClassicPolling(ctx) {
1018
+ return pollingRuntime.start(ctx);
1019
+ },
1020
+ stopClassicPolling() {
1021
+ return pollingRuntime.stop();
1022
+ },
1023
+ startBusPolling(ctx) {
1024
+ return telegramBusLeaderRuntime.startPolling(ctx);
1025
+ },
1026
+ stopBusPolling() {
1027
+ return telegramBusLeaderRuntime.stopPolling();
1028
+ },
1029
+ startLeaderHealth: telegramLeaderHealthRuntime.start,
1030
+ stopLeaderHealth: telegramLeaderHealthRuntime.stop,
1031
+ isTopicModeUnavailableError: Threads.isTelegramTopicModeUnavailableError,
1032
+ updateStatus,
1033
+ recordEvent: recordRuntimeEvent,
1034
+ });
1035
+ const threadAwarePollingPorts =
1036
+ Polling.createTelegramThreadAwarePollingPorts<
1037
+ Pi.ExtensionContext,
1038
+ Locks.TelegramLockEntry
1039
+ >({
1040
+ getAllowedUserId: configStore.getAllowedUserId,
1041
+ callApi: callTelegramApi,
1042
+ topicTargetStore: threadStore,
1043
+ isBusConfigured: isTelegramBusConfigured,
1044
+ isBusRuntimeEnabled: isTelegramBusRuntimeEnabled,
1045
+ isTopicModeUnavailableError: Threads.isTelegramTopicModeUnavailableError,
1046
+ getPollingStartedWithTelegramBus() {
1047
+ return pollingStartedWithTelegramBus;
1048
+ },
1049
+ setPollingStartedWithTelegramBus(started) {
1050
+ pollingStartedWithTelegramBus = started;
1051
+ },
1052
+ setForceFreshLeaderThreadOnNextStart(forceFresh) {
1053
+ forceFreshLeaderThreadOnNextStart = forceFresh;
1054
+ },
1055
+ setTopicModeUnavailable(unavailable) {
1056
+ telegramTopicModeUnavailable = unavailable;
1057
+ },
1058
+ startClassicPolling(ctx) {
1059
+ return pollingRuntime.start(ctx);
1060
+ },
1061
+ stopClassicPolling() {
1062
+ return pollingRuntime.stop();
1063
+ },
1064
+ startBusLeaderPolling(ctx) {
1065
+ return telegramBusLeaderRuntime.startPolling(ctx);
1066
+ },
1067
+ stopBusLeaderPolling() {
1068
+ return telegramBusLeaderRuntime.stopPolling();
1069
+ },
1070
+ startLeaderHealth: telegramLeaderHealthRuntime.start,
1071
+ stopLeaderHealth: telegramLeaderHealthRuntime.stop,
1072
+ registerFollowerWithLeader(ctx, owner) {
1073
+ return telegramBusFollowerRegistration.registerWithLeader(ctx, owner);
1074
+ },
1075
+ stopFollowerRegistration: telegramBusFollowerRegistration.stop,
1076
+ recordEvent: recordRuntimeEvent,
1077
+ });
390
1078
  const lockedPollingRuntime = Locks.createTelegramLockedPollingRuntime({
391
1079
  lock: lockRuntime,
392
1080
  hasBotToken: configStore.hasBotToken,
393
1081
  canStartPolling: Pi.canStartPollingInExtensionContext,
394
1082
  formatStartBlockedMessage: Pi.formatPollingStartBlockedByRunMode,
395
- startPolling: pollingRuntime.start,
396
- stopPolling: pollingRuntime.stop,
1083
+ startPolling: threadAwarePollingPorts.startPolling,
1084
+ stopPolling: threadAwarePollingPorts.stopPolling,
1085
+ registerFollowerWithOwner: threadAwarePollingPorts.registerFollowerWithOwner,
1086
+ stopFollowerRegistration: threadAwarePollingPorts.stopFollowerRegistration,
397
1087
  updateStatus,
398
1088
  recordRuntimeEvent,
399
1089
  });
1090
+ const disconnectTelegramAndDeleteCurrentThread =
1091
+ Sync.createTelegramManualThreadDisconnectHandler({
1092
+ instanceId: telegramInstanceId,
1093
+ getCurrentThreadRecord,
1094
+ topicTargetStore: threadStore,
1095
+ callApi: callTelegramApi,
1096
+ getLeaderTarget() {
1097
+ return telegramBusLeaderTarget;
1098
+ },
1099
+ clearLeaderTarget() {
1100
+ telegramBusLeaderTarget = undefined;
1101
+ telegramBusLeaderSlot = undefined;
1102
+ },
1103
+ getSyncState() {
1104
+ return telegramSyncState;
1105
+ },
1106
+ setSyncState(state) {
1107
+ telegramSyncState = state;
1108
+ },
1109
+ stopPolling: lockedPollingRuntime.stop,
1110
+ recordRuntimeEvent,
1111
+ });
1112
+ const suspendTelegramForSessionReplacement =
1113
+ BusFollower.createTelegramBusFollowerSessionReplacementSuspender({
1114
+ registrationState: telegramBusFollowerRegistrationState,
1115
+ instanceId: telegramInstanceId,
1116
+ suspendPolling: lockedPollingRuntime.suspend,
1117
+ recordRuntimeEvent,
1118
+ });
400
1119
  const queueSessionLifecycle = Queue.createTelegramSessionLifecycleRuntime({
401
1120
  getCurrentModel: getContextModel,
402
1121
  loadConfig: configStore.load,
@@ -415,37 +1134,66 @@ export default function (pi: Pi.ExtensionAPI) {
415
1134
  }),
416
1135
  clearModelMenuState: modelMenuRuntime.clear,
417
1136
  getActiveTurnChatId: activeTurnRuntime.getChatId,
1137
+ getActiveTurnTarget: activeTurnRuntime.getTarget,
418
1138
  clearPreview: previewRuntime.clear,
419
1139
  clearActiveTurn: activeTurnRuntime.clear,
420
1140
  clearAbort: abort.clearHandler,
421
- stopPolling: lockedPollingRuntime.suspend,
1141
+ stopPolling: suspendTelegramForSessionReplacement,
422
1142
  recordRuntimeEvent,
423
1143
  });
424
1144
  const baseSessionLifecycleRuntime = Lifecycle.appendTelegramLifecycleHooks(
425
1145
  queueSessionLifecycle,
426
1146
  {
427
- onSessionStart: lockedPollingRuntime.onSessionStart,
1147
+ async onSessionStart(event, ctx) {
1148
+ await lockedPollingRuntime.onSessionStart(event, ctx);
1149
+ telegramThreadCapabilityMonitor.start(ctx);
1150
+ },
1151
+ async onSessionShutdown() {
1152
+ telegramThreadCapabilityMonitor.stop();
1153
+ },
428
1154
  },
429
1155
  );
430
- const sessionLifecycleRuntime = Lifecycle.appendTelegramLifecycleHooks(
1156
+ const sessionLifecycleWithContext = Lifecycle.appendTelegramLifecycleHooks(
431
1157
  baseSessionLifecycleRuntime,
432
1158
  Lifecycle.createTelegramSessionContextTracker(telegramSessionContextStore),
433
1159
  );
1160
+ const sessionLifecycleRuntime = Lifecycle.appendTelegramLifecycleHooks(
1161
+ sessionLifecycleWithContext,
1162
+ {
1163
+ onSessionStart: BusFollower.createTelegramBusFollowerSessionRefreshHook({
1164
+ registrationState: telegramBusFollowerRegistrationState,
1165
+ registrationRuntime: telegramBusFollowerRegistration,
1166
+ getLeaderState() {
1167
+ return lockRuntime.getState();
1168
+ },
1169
+ updateStatus,
1170
+ recordRuntimeEvent,
1171
+ }),
1172
+ },
1173
+ );
434
1174
 
435
1175
  // --- Extension API Bindings ---
436
1176
 
437
1177
  Bindings.registerTelegramCommandsAndTools({
438
1178
  pi,
439
1179
  configStore,
1180
+ persistConfig: persistTelegramConfigWithSync,
440
1181
  setup,
441
1182
  activeTurnRuntime,
442
1183
  lockedPollingRuntime,
1184
+ stopPolling: disconnectTelegramAndDeleteCurrentThread,
443
1185
  getStatusLines,
444
1186
  buttonActionStore,
445
1187
  sendMarkdownReply,
446
1188
  callMultipart,
447
1189
  getDefaultChatId: proactivePushChatIdGetter,
448
- canSendDirect: ownsTelegramDirectDelivery,
1190
+ getDefaultTarget: proactivePushTargetGetter,
1191
+ canSendDirect() {
1192
+ return (
1193
+ ownsTelegramDirectDelivery() ||
1194
+ telegramBusFollowerRegistrationState.isRegistered()
1195
+ );
1196
+ },
449
1197
  updateStatus,
450
1198
  recordRuntimeEvent,
451
1199
  });
@@ -475,12 +1223,21 @@ export default function (pi: Pi.ExtensionAPI) {
475
1223
  sendRecordVoiceAction,
476
1224
  sendMarkdownReply,
477
1225
  sendTextReply,
1226
+ editInteractiveMessage,
1227
+ deleteMessage: deleteTelegramMessage,
478
1228
  dispatchNextQueuedTelegramTurn,
479
1229
  answerGuestQuery,
480
1230
  sendGuestReply,
481
1231
  finalizeMarkdownPreview,
482
1232
  proactivePushChatIdGetter,
1233
+ proactivePushTargetGetter,
483
1234
  isProactivePushEnabled: configControls.isProactivePushEnabled,
1235
+ canSendProactivePush(ctx) {
1236
+ return (
1237
+ lockOwnershipGuard.ownsContext(ctx) ||
1238
+ telegramBusFollowerRegistrationState.isRegistered()
1239
+ );
1240
+ },
484
1241
  updateStatus,
485
1242
  recordRuntimeEvent,
486
1243
  });