@llblab/pi-telegram 0.20.1 → 0.20.3

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
@@ -57,41 +57,31 @@ export default function (pi: Pi.ExtensionAPI) {
57
57
  setThinkingLevel,
58
58
  } = piRuntime;
59
59
  const bridgeRuntime = Runtime.createTelegramBridgeRuntime();
60
- const telegramInstanceId = `${process.pid}:${Date.now()}`;
61
- // Manual follower identity must survive a Pi process reload in the same
62
- // terminal. The process id changes on reload, but the parent shell/terminal id
63
- // is stable enough to keep the follower bound to its existing Telegram thread.
64
- const telegramManualFollowerOwnerId = String(process.ppid || process.pid);
65
60
  const getActiveTelegramThreadProfile = function (): string | undefined {
66
61
  return configStore.getActiveProfileName();
67
62
  };
68
- const getTelegramManualFollowerProfileKey = function (): string {
69
- return Threads.getTelegramThreadOwnerKey({
70
- kind: "manual-follower",
71
- instanceId: telegramManualFollowerOwnerId,
72
- telegramProfile: getActiveTelegramThreadProfile(),
63
+ const busProcessRuntime = Bus.createTelegramBusProcessRuntime({
64
+ getActiveProfileName: getActiveTelegramThreadProfile,
65
+ pid: process.pid,
66
+ parentPid: process.ppid,
67
+ createdAtMs: Date.now(),
68
+ });
69
+ const {
70
+ instanceId: telegramInstanceId,
71
+ manualFollowerOwnerId: telegramManualFollowerOwnerId,
72
+ getLeaderSocketPath: getTelegramBusSocketPath,
73
+ getFollowerSocketPath: getTelegramBusFollowerSocketPath,
74
+ } = busProcessRuntime;
75
+ const getTelegramManualFollowerProfileKey =
76
+ BusFollower.createTelegramManualFollowerProfileKeyResolver({
77
+ getActiveProfileName: getActiveTelegramThreadProfile,
78
+ manualFollowerOwnerId: telegramManualFollowerOwnerId,
73
79
  });
74
- };
75
80
  const telegramBusAuthSecret = Bus.createTelegramBusAuthSecret();
76
81
  let telegramActiveBusAuthSecret: string | undefined;
77
82
  let telegramBusLifecycleOverridePhase:
78
83
  | Status.TelegramBridgeBusLifecyclePhase
79
84
  | undefined;
80
- const getTelegramBusSocketPath = function (): string {
81
- return Bus.getTelegramBusSocketPath(
82
- undefined,
83
- undefined,
84
- getActiveTelegramThreadProfile(),
85
- );
86
- };
87
- const getTelegramBusFollowerSocketPath = function (): string {
88
- return Bus.getTelegramBusFollowerSocketPath(
89
- telegramInstanceId,
90
- undefined,
91
- undefined,
92
- getActiveTelegramThreadProfile(),
93
- );
94
- };
95
85
  let telegramBusRequestSequence = 0;
96
86
  const telegramBusFollowerRegistry = Bus.createTelegramBusFollowerRegistry();
97
87
  const telegramBusFollowerRegistrationState =
@@ -817,68 +807,72 @@ export default function (pi: Pi.ExtensionAPI) {
817
807
  compact,
818
808
  recordRuntimeEvent,
819
809
  });
820
- const telegramBusForwardedUpdateReceiver =
821
- BusFollower.createTelegramBusForwardedUpdateReceiverRuntime<
810
+ const promoteTelegramBusFollowerToLeader: BusFollower.TelegramBusFollowerPromotionHandler<Pi.ExtensionContext> =
811
+ BusFollower.createTelegramBusFollowerPromotionHandler<Pi.ExtensionContext>({
812
+ topicTargetStore: threadStore,
813
+ instanceId: telegramInstanceId,
814
+ getActiveProfileName: getActiveTelegramThreadProfile,
815
+ async startLeader(ctx): Promise<void> {
816
+ await lockedPollingRuntime.start(ctx, { force: true });
817
+ },
818
+ recordRuntimeEvent,
819
+ });
820
+ const telegramBusFollowerAssembly: BusFollower.TelegramBusFollowerRuntimeAssembly<Pi.ExtensionContext> =
821
+ BusFollower.createTelegramBusFollowerRuntimeAssembly<
822
822
  Pi.ExtensionContext,
823
823
  Updates.TelegramMessageReactionUpdated,
824
824
  Routing.TelegramRoutedCallbackQuery,
825
825
  Routing.TelegramRoutedMessage
826
826
  >({
827
- socketPath: getTelegramBusFollowerSocketPath,
828
- instanceId: telegramInstanceId,
829
- getContext() {
830
- return telegramSessionContextStore.get();
831
- },
832
- getAuthSecret() {
833
- return telegramActiveBusAuthSecret;
834
- },
835
- handleForwardedCallback(query, ctx) {
836
- return inboundRouteRuntime.handleUpdate({ callback_query: query }, ctx);
837
- },
838
- handleForwardedReaction(reactionUpdate, ctx) {
839
- return inboundRouteRuntime.handleAuthorizedReactionUpdate(
840
- reactionUpdate,
841
- ctx,
842
- );
843
- },
844
- handleForwardedMessage(message, ctx) {
845
- return inboundRouteRuntime.handleUpdate(
846
- { message: message as never },
847
- ctx,
848
- );
849
- },
850
- async handleForwardedEditedMessage(message, ctx) {
851
- return inboundRouteRuntime.handleUpdate(
852
- { edited_message: message as never },
853
- ctx,
854
- );
827
+ receiver: {
828
+ socketPath: getTelegramBusFollowerSocketPath,
829
+ instanceId: telegramInstanceId,
830
+ getContext() {
831
+ return telegramSessionContextStore.get();
832
+ },
833
+ getAuthSecret() {
834
+ return telegramActiveBusAuthSecret;
835
+ },
836
+ handleForwardedCallback(query, ctx) {
837
+ return inboundRouteRuntime.handleUpdate({ callback_query: query }, ctx);
838
+ },
839
+ handleForwardedReaction(reactionUpdate, ctx) {
840
+ return inboundRouteRuntime.handleAuthorizedReactionUpdate(
841
+ reactionUpdate,
842
+ ctx,
843
+ );
844
+ },
845
+ handleForwardedMessage(message, ctx) {
846
+ return inboundRouteRuntime.handleUpdate(
847
+ { message: message as never },
848
+ ctx,
849
+ );
850
+ },
851
+ async handleForwardedEditedMessage(message, ctx) {
852
+ return inboundRouteRuntime.handleUpdate(
853
+ { edited_message: message as never },
854
+ ctx,
855
+ );
856
+ },
857
+ recordRuntimeEvent,
855
858
  },
856
- handleReplaceTarget:
857
- BusFollower.createTelegramBusFollowerTargetReplacementHandler({
858
- topicTargetStore: threadStore,
859
- registrationState: telegramBusFollowerRegistrationState,
860
- instanceId: telegramInstanceId,
861
- manualFollowerProfileKey: getTelegramManualFollowerProfileKey(),
862
- manualFollowerOwnerId: telegramManualFollowerOwnerId,
863
- getSyncState() {
864
- return telegramSyncState;
865
- },
866
- setSyncState(state) {
867
- telegramSyncState = state;
868
- },
869
- updateStatus,
870
- recordRuntimeEvent,
871
- }),
872
- recordRuntimeEvent,
873
- });
874
- let telegramBusFollowerRegistration: BusFollower.TelegramBusFollowerRegistrationRuntime<Pi.ExtensionContext>;
875
- const telegramFollowerHeartbeatRecovery =
876
- BusFollower.createTelegramBusFollowerHeartbeatRecoveryHandler<Pi.ExtensionContext>(
877
- {
859
+ targetReplacement: {
860
+ topicTargetStore: threadStore,
878
861
  registrationState: telegramBusFollowerRegistrationState,
879
- getRegistrationRuntime() {
880
- return telegramBusFollowerRegistration;
862
+ instanceId: telegramInstanceId,
863
+ getManualFollowerProfileKey: getTelegramManualFollowerProfileKey,
864
+ manualFollowerOwnerId: telegramManualFollowerOwnerId,
865
+ getSyncState() {
866
+ return telegramSyncState;
867
+ },
868
+ setSyncState(state) {
869
+ telegramSyncState = state;
881
870
  },
871
+ updateStatus,
872
+ recordRuntimeEvent,
873
+ },
874
+ recovery: {
875
+ registrationState: telegramBusFollowerRegistrationState,
882
876
  getLeaderState() {
883
877
  return lockRuntime.getState();
884
878
  },
@@ -886,47 +880,17 @@ export default function (pi: Pi.ExtensionAPI) {
886
880
  telegramBusLifecycleOverridePhase = phase;
887
881
  },
888
882
  updateStatus,
889
- async promoteToLeader(ctx, binding) {
890
- const promotedRecord =
891
- await Threads.promoteTelegramFollowerBindingToLeader({
892
- store: threadStore,
893
- instanceId: telegramInstanceId,
894
- cwd: ctx.cwd,
895
- telegramProfile: getActiveTelegramThreadProfile(),
896
- target: binding.target,
897
- slot: binding.slot,
898
- threadName: binding.threadName,
899
- });
900
- if (promotedRecord) {
901
- recordRuntimeEvent(
902
- "bus",
903
- "Follower thread binding promoted to leader",
904
- {
905
- phase: "follower-promoted-binding",
906
- chatId: promotedRecord.target.chatId,
907
- threadId: promotedRecord.target.threadId,
908
- slot: promotedRecord.slot,
909
- threadName: promotedRecord.threadName,
910
- },
911
- );
912
- }
913
- await lockedPollingRuntime.start(ctx, { force: true });
914
- },
883
+ promoteToLeader: promoteTelegramBusFollowerToLeader,
915
884
  sleep(ms) {
916
885
  return Polling.sleepTelegramPollingRetry(ms);
917
886
  },
918
887
  promotionGraceMs: BusFollower.TELEGRAM_BUS_FOLLOWER_PROMOTION_GRACE_MS,
919
888
  recordRuntimeEvent,
920
889
  },
921
- );
922
- telegramBusFollowerRegistration =
923
- BusFollower.createTelegramBusFollowerRegistrationRuntime<Pi.ExtensionContext>(
924
- {
890
+ registration: {
925
891
  instanceId: telegramInstanceId,
926
892
  getFollowerBusSocketPath: getTelegramBusFollowerSocketPath,
927
893
  getLeaderSocketPath: getTelegramBusSocketPath,
928
- startReceiving: telegramBusForwardedUpdateReceiver.start,
929
- stopReceiving: telegramBusForwardedUpdateReceiver.stop,
930
894
  registrationState: telegramBusFollowerRegistrationState,
931
895
  createRequestId() {
932
896
  telegramBusRequestSequence += 1;
@@ -947,10 +911,11 @@ export default function (pi: Pi.ExtensionAPI) {
947
911
  getProfileKey() {
948
912
  return getTelegramManualFollowerProfileKey();
949
913
  },
950
- onHeartbeatFailure: telegramFollowerHeartbeatRecovery,
951
914
  recordRuntimeEvent,
952
915
  },
953
- );
916
+ });
917
+ const telegramBusFollowerRegistration =
918
+ telegramBusFollowerAssembly.registration;
954
919
  const pollingRuntime = Polling.createTelegramPollingControllerRuntime({
955
920
  state: pollingControllerState,
956
921
  getConfig: configStore.get,
@@ -980,116 +945,67 @@ export default function (pi: Pi.ExtensionAPI) {
980
945
  recordEvent: recordRuntimeEvent,
981
946
  });
982
947
  };
983
- const telegramBusLeaderRuntime = BusLeader.createTelegramBusLeaderRuntime({
984
- socketPath: getTelegramBusSocketPath,
985
- followerRegistry: telegramBusFollowerRegistry,
986
- authSecret: telegramBusAuthSecret,
987
- startPolling: pollingRuntime.start,
988
- stopPolling: pollingRuntime.stop,
989
- authorizeFollowerApiCall: Bus.isTelegramFollowerApiCallAllowed,
990
- recordFollowerMessageOwnership(record) {
991
- messageOwnershipStore.record({
992
- chatId: record.chatId,
993
- messageId: record.messageId,
994
- target: record.target,
995
- instanceId: record.follower.instanceId,
996
- });
997
- },
998
- provisionLeaderTarget:
999
- BusLeader.createTelegramBusLeaderTargetProvisioner<Pi.ExtensionContext>({
1000
- getAllowedUserId: configStore.getAllowedUserId,
1001
- instanceId: telegramInstanceId,
1002
- getCwd(ctx) {
1003
- return typeof ctx.cwd === "string" ? ctx.cwd : undefined;
1004
- },
1005
- getTelegramProfile: getActiveTelegramThreadProfile,
1006
- shouldForceFreshUnnamed() {
1007
- return forceFreshLeaderThreadOnNextStart;
1008
- },
1009
- topicTargetStore: threadStore,
1010
- callApi(method, body) {
1011
- return directTelegramApiRuntime.call(method, body);
1012
- },
1013
- getCurrentLeaderEpoch,
1014
- getThreadReconciliationMachineState() {
1015
- return threadReconciliationRuntime.getState();
1016
- },
1017
- recordThreadReconciliationPlan,
1018
- getSyncState() {
1019
- return telegramSyncState;
1020
- },
1021
- setSyncState(state) {
1022
- telegramSyncState = state;
1023
- },
1024
- setLeaderTarget(input) {
1025
- telegramBusLeaderTarget = input.target;
1026
- telegramBusLeaderSlot = input.slot;
1027
- telegramBusLeaderThreadName = input.threadName;
1028
- },
1029
- onProvisioningStart() {
1030
- telegramTopicProvisioningCount += 1;
1031
- },
1032
- onProvisioningEnd() {
1033
- telegramTopicProvisioningCount = Math.max(
1034
- 0,
1035
- telegramTopicProvisioningCount - 1,
1036
- );
948
+ const telegramBusLeaderRuntime =
949
+ BusLeader.createTelegramBusLeaderRuntimeAssembly<Pi.ExtensionContext>({
950
+ runtime: {
951
+ socketPath: getTelegramBusSocketPath,
952
+ followerRegistry: telegramBusFollowerRegistry,
953
+ authSecret: telegramBusAuthSecret,
954
+ startPolling: pollingRuntime.start,
955
+ stopPolling: pollingRuntime.stop,
956
+ authorizeFollowerApiCall: Bus.isTelegramFollowerApiCallAllowed,
957
+ recordFollowerMessageOwnership(record) {
958
+ messageOwnershipStore.record({
959
+ chatId: record.chatId,
960
+ messageId: record.messageId,
961
+ target: record.target,
962
+ instanceId: record.follower.instanceId,
963
+ });
1037
964
  },
1038
- recordRuntimeEvent,
1039
- }),
1040
- onFollowerPruned: BusLeader.createTelegramBusFollowerPruneHandler({
965
+ },
966
+ getAllowedUserId: configStore.getAllowedUserId,
967
+ instanceId: telegramInstanceId,
968
+ getCwd(ctx) {
969
+ return typeof ctx.cwd === "string" ? ctx.cwd : undefined;
970
+ },
971
+ getTelegramProfile: getActiveTelegramThreadProfile,
972
+ shouldForceFreshUnnamed() {
973
+ return forceFreshLeaderThreadOnNextStart;
974
+ },
1041
975
  topicTargetStore: threadStore,
1042
976
  callApi(method, body) {
1043
977
  return directTelegramApiRuntime.call(method, body);
1044
978
  },
979
+ callMultipart: directTelegramApiRuntime.callMultipart,
980
+ downloadFile: directTelegramApiRuntime.downloadFile,
981
+ recoverStaleTargetError: recoverStaleTelegramTopicApiError,
1045
982
  getCurrentLeaderEpoch,
983
+ getThreadReconciliationMachineState() {
984
+ return threadReconciliationRuntime.getState();
985
+ },
986
+ recordThreadReconciliationPlan,
1046
987
  getSyncState() {
1047
988
  return telegramSyncState;
1048
989
  },
1049
990
  setSyncState(state) {
1050
991
  telegramSyncState = state;
1051
992
  },
993
+ setLeaderTarget(input) {
994
+ telegramBusLeaderTarget = input.target;
995
+ telegramBusLeaderSlot = input.slot;
996
+ telegramBusLeaderThreadName = input.threadName;
997
+ },
998
+ onProvisioningStart() {
999
+ telegramTopicProvisioningCount += 1;
1000
+ },
1001
+ onProvisioningEnd() {
1002
+ telegramTopicProvisioningCount = Math.max(
1003
+ 0,
1004
+ telegramTopicProvisioningCount - 1,
1005
+ );
1006
+ },
1052
1007
  recordRuntimeEvent,
1053
- }),
1054
- provisionFollowerTarget:
1055
- BusLeader.createTelegramBusFollowerTargetProvisioner({
1056
- getAllowedUserId: configStore.getAllowedUserId,
1057
- topicTargetStore: threadStore,
1058
- callApi(method, body) {
1059
- return directTelegramApiRuntime.call(method, body);
1060
- },
1061
- getCurrentLeaderEpoch,
1062
- getSyncState() {
1063
- return telegramSyncState;
1064
- },
1065
- setSyncState(state) {
1066
- telegramSyncState = state;
1067
- },
1068
- onProvisioningStart() {
1069
- telegramTopicProvisioningCount += 1;
1070
- },
1071
- onProvisioningEnd() {
1072
- telegramTopicProvisioningCount = Math.max(
1073
- 0,
1074
- telegramTopicProvisioningCount - 1,
1075
- );
1076
- },
1077
- recordRuntimeEvent,
1078
- }),
1079
- restoreFollowerRegistry:
1080
- BusLeader.createTelegramBusFollowerRegistryRestoreHandler({
1081
- topicTargetStore: threadStore,
1082
- followerRegistry: telegramBusFollowerRegistry,
1083
- recordRuntimeEvent,
1084
- }),
1085
- callApi: BusLeader.createTelegramBusLeaderApiProxy({
1086
- call: directTelegramApiRuntime.call,
1087
- callMultipart: directTelegramApiRuntime.callMultipart,
1088
- downloadFile: directTelegramApiRuntime.downloadFile,
1089
- recoverStaleTargetError: recoverStaleTelegramTopicApiError,
1090
- }),
1091
- recordRuntimeEvent,
1092
- });
1008
+ });
1093
1009
  let pollingStartedWithTelegramBus = false;
1094
1010
  let forceFreshLeaderThreadOnNextStart = false;
1095
1011
  const telegramLeaderHealthRuntime = Sync.createTelegramLeaderHealthRuntime({
@@ -1260,14 +1176,6 @@ export default function (pi: Pi.ExtensionAPI) {
1260
1176
  registrationState: telegramBusFollowerRegistrationState,
1261
1177
  instanceId: telegramInstanceId,
1262
1178
  suspendPolling: lockedPollingRuntime.suspend,
1263
- onFollowerSessionDisconnect() {
1264
- const target = telegramBusFollowerRegistrationState.getTarget();
1265
- if (!target) return undefined;
1266
- return BusFollower.markTelegramFollowerThreadStaleForSessionDisconnect({
1267
- topicTargetStore: threadStore,
1268
- target,
1269
- });
1270
- },
1271
1179
  recordRuntimeEvent,
1272
1180
  });
1273
1181
  const queueSessionLifecycle = Queue.createTelegramSessionLifecycleRuntime({
@@ -1392,6 +1300,12 @@ export default function (pi: Pi.ExtensionAPI) {
1392
1300
  telegramBusFollowerRegistrationState.isRegistered()
1393
1301
  );
1394
1302
  },
1303
+ canSendAgentActivity(ctx) {
1304
+ return (
1305
+ lockOwnershipGuard.ownsContext(ctx) ||
1306
+ telegramBusFollowerRegistrationState.isRegistered()
1307
+ );
1308
+ },
1395
1309
  updateStatus,
1396
1310
  recordRuntimeEvent,
1397
1311
  });
package/lib/bindings.ts CHANGED
@@ -94,7 +94,9 @@ export function registerTelegramCommandsAndTools({
94
94
  sendMarkdownReply(chatId, undefined, markdown, options),
95
95
  recordRuntimeEvent,
96
96
  });
97
- Prompts.registerTelegramHelpTool(pi);
97
+ Prompts.registerTelegramHelpTool(pi, {
98
+ getActiveProfileName: configStore.getActiveProfileName,
99
+ });
98
100
  Commands.registerTelegramBridgeCommands(pi, {
99
101
  promptForConfig: async (ctx, profileName) => {
100
102
  const nextProfileName = profileName ?? undefined;
@@ -103,36 +105,58 @@ export function registerTelegramCommandsAndTools({
103
105
  return;
104
106
  }
105
107
  const previousProfileName = configStore.getActiveProfileName();
106
- if (previousProfileName !== nextProfileName) {
107
- await (stopPolling ?? lockedPollingRuntime.stop)();
108
- }
108
+ let setupConfigStore = configStore;
109
+ let persistSetupConfig = persistConfig;
109
110
  if (!profileName) {
111
+ if (previousProfileName !== nextProfileName) {
112
+ await (stopPolling ?? lockedPollingRuntime.stop)();
113
+ }
110
114
  configStore.activateProfile(undefined);
111
115
  } else {
112
116
  const storedConfig = configStore.getStoredConfig();
113
- if (!storedConfig.profiles?.[profileName]) {
117
+ setupConfigStore = Config.createTelegramConfigStore({
118
+ initialConfig: {
119
+ ...storedConfig,
120
+ profiles: {
121
+ ...(storedConfig.profiles ?? {}),
122
+ [profileName]: storedConfig.profiles?.[profileName] ?? {
123
+ botToken: "",
124
+ },
125
+ },
126
+ },
127
+ });
128
+ setupConfigStore.activateProfile(profileName);
129
+ persistSetupConfig = async () => {
130
+ if (previousProfileName !== profileName) {
131
+ await (stopPolling ?? lockedPollingRuntime.stop)();
132
+ }
133
+ configStore.activateProfile(undefined);
114
134
  configStore.set({
115
135
  ...storedConfig,
116
136
  profiles: {
117
137
  ...(storedConfig.profiles ?? {}),
118
- [profileName]: { botToken: "" },
138
+ [profileName]: storedConfig.profiles?.[profileName] ?? {
139
+ botToken: "",
140
+ },
119
141
  },
120
142
  });
121
- }
122
- configStore.activateProfile(profileName);
143
+ configStore.activateProfile(profileName);
144
+ configStore.set(setupConfigStore.get());
145
+ await persistConfig();
146
+ };
123
147
  }
124
148
  const runSetup = Setup.createTelegramSetupPromptRuntime({
125
- getConfig: configStore.get,
126
- setConfig: configStore.set,
149
+ getConfig: setupConfigStore.get,
150
+ setConfig: setupConfigStore.set,
127
151
  setupGuard: setup,
128
152
  getMe: TelegramApi.fetchTelegramBotIdentity,
129
- persistConfig,
153
+ persistConfig: persistSetupConfig,
130
154
  startPolling: lockedPollingRuntime.start,
131
155
  updateStatus,
132
156
  recordRuntimeEvent,
133
157
  });
134
- await runSetup(ctx);
135
- if (profileName) {
158
+ const completion = await runSetup(ctx);
159
+ if (profileName && completion.status === "success") {
136
160
  ctx.ui.notify(`Profile "${profileName}" saved and connected.`, "info");
137
161
  }
138
162
  },
@@ -155,6 +179,8 @@ export function registerTelegramCommandsAndTools({
155
179
  activateProfileConfig: async (_ctx, profileName) => {
156
180
  await configStore.load();
157
181
  if (!Config.isValidTelegramProfileName(profileName)) return false;
182
+ const storedConfig = configStore.getStoredConfig();
183
+ if (!storedConfig.profiles?.[profileName]) return false;
158
184
  const previousProfileName = configStore.getActiveProfileName();
159
185
  if (previousProfileName !== profileName) {
160
186
  await (stopPolling ?? lockedPollingRuntime.stop)();
@@ -242,6 +268,7 @@ interface TelegramLifecycleBindingDeps {
242
268
  proactivePushTargetGetter: () => Queue.TelegramQueueTarget | undefined;
243
269
  isProactivePushEnabled: () => boolean;
244
270
  canSendProactivePush: (ctx: Pi.ExtensionContext) => boolean;
271
+ canSendAgentActivity: (ctx: Pi.ExtensionContext) => boolean;
245
272
  updateStatus: TelegramBridgeStatusUpdater;
246
273
  recordRuntimeEvent: TelegramRuntimeEventRecorder;
247
274
  }
@@ -274,6 +301,7 @@ export function registerTelegramLifecycleRuntimeHooks({
274
301
  proactivePushTargetGetter,
275
302
  isProactivePushEnabled,
276
303
  canSendProactivePush,
304
+ canSendAgentActivity,
277
305
  updateStatus,
278
306
  recordRuntimeEvent,
279
307
  }: TelegramLifecycleBindingDeps): void {
@@ -370,6 +398,16 @@ export function registerTelegramLifecycleRuntimeHooks({
370
398
  const agentStartWithDedupReset = Lifecycle.createAgentStartDedupHook(
371
399
  agentLifecycleHooks.onAgentStart,
372
400
  );
401
+ const startAgentActivityTypingLoop = (ctx: Pi.ExtensionContext): void => {
402
+ if (!canSendAgentActivity(ctx)) return;
403
+ const turn = activeTurnRuntime.get();
404
+ const target = turn?.target ?? proactivePushTargetGetter();
405
+ promptDispatchRuntime.startTypingLoop(
406
+ ctx,
407
+ turn?.chatId ?? target?.chatId ?? proactivePushChatIdGetter(),
408
+ { target },
409
+ );
410
+ };
373
411
  const startActiveTurnTypingLoop = (ctx: Pi.ExtensionContext): void => {
374
412
  const turn = activeTurnRuntime.get();
375
413
  promptDispatchRuntime.startTypingLoop(ctx, turn?.chatId, {
@@ -405,7 +443,10 @@ export function registerTelegramLifecycleRuntimeHooks({
405
443
  },
406
444
  onSessionBeforeCompact: compactionObserver.onSessionBeforeCompact,
407
445
  onSessionCompact: compactionObserver.onSessionCompact,
408
- onAgentStart: agentStartWithDedupReset,
446
+ async onAgentStart(event, ctx) {
447
+ await agentStartWithDedupReset(event, ctx);
448
+ startAgentActivityTypingLoop(ctx);
449
+ },
409
450
  async onToolExecutionStart(_event, _ctx) {
410
451
  agentLifecycleHooks.onToolExecutionStart();
411
452
  },