@llblab/pi-telegram 0.19.3 → 0.20.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
@@ -32,7 +32,7 @@ import * as Queue from "./lib/queue.ts";
32
32
  import * as Replies from "./lib/replies.ts";
33
33
  import * as Routing from "./lib/routing.ts";
34
34
  import * as Runtime from "./lib/runtime.ts";
35
- import * as RuntimeLog from "./lib/runtime-log.ts";
35
+ import * as Logs from "./lib/logs.ts";
36
36
  import * as Sections from "./lib/sections.ts";
37
37
  import * as Status from "./lib/status.ts";
38
38
  import * as Sync from "./lib/sync.ts";
@@ -58,16 +58,40 @@ export default function (pi: Pi.ExtensionAPI) {
58
58
  } = piRuntime;
59
59
  const bridgeRuntime = Runtime.createTelegramBridgeRuntime();
60
60
  const telegramInstanceId = `${process.pid}:${Date.now()}`;
61
- const telegramManualFollowerOwnerId = String(process.pid);
62
- const telegramManualFollowerProfileKey = `manual:${telegramManualFollowerOwnerId}`;
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
+ const getActiveTelegramThreadProfile = function (): string | undefined {
66
+ return configStore.getActiveProfileName();
67
+ };
68
+ const getTelegramManualFollowerProfileKey = function (): string {
69
+ return Threads.getTelegramThreadOwnerKey({
70
+ kind: "manual-follower",
71
+ instanceId: telegramManualFollowerOwnerId,
72
+ telegramProfile: getActiveTelegramThreadProfile(),
73
+ });
74
+ };
63
75
  const telegramBusAuthSecret = Bus.createTelegramBusAuthSecret();
64
76
  let telegramActiveBusAuthSecret: string | undefined;
65
77
  let telegramBusLifecycleOverridePhase:
66
78
  | Status.TelegramBridgeBusLifecyclePhase
67
79
  | undefined;
68
- const telegramBusSocketPath = Bus.getTelegramBusSocketPath();
69
- const telegramBusFollowerSocketPath =
70
- Bus.getTelegramBusFollowerSocketPath(telegramInstanceId);
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
+ };
71
95
  let telegramBusRequestSequence = 0;
72
96
  const telegramBusFollowerRegistry = Bus.createTelegramBusFollowerRegistry();
73
97
  const telegramBusFollowerRegistrationState =
@@ -85,7 +109,23 @@ export default function (pi: Pi.ExtensionAPI) {
85
109
  return configStoreForRedaction?.getBotToken();
86
110
  },
87
111
  });
88
- const runtimeJsonlLog = RuntimeLog.createTelegramRuntimeJsonlLog();
112
+ let getRuntimeLogProfileName = function (): string | undefined {
113
+ return undefined;
114
+ };
115
+ const runtimeJsonlLog = Logs.createTelegramRuntimeJsonlLog({
116
+ path: function () {
117
+ return Logs.getTelegramRuntimeLogPath(
118
+ undefined,
119
+ getRuntimeLogProfileName(),
120
+ );
121
+ },
122
+ previousPath: function () {
123
+ return Logs.getTelegramPreviousRuntimeLogPath(
124
+ undefined,
125
+ getRuntimeLogProfileName(),
126
+ );
127
+ },
128
+ });
89
129
  runtimeJsonlLog.reset("extension-start", {
90
130
  instanceId: telegramInstanceId,
91
131
  pid: process.pid,
@@ -103,6 +143,7 @@ export default function (pi: Pi.ExtensionAPI) {
103
143
  };
104
144
  const configStore = Config.createTelegramConfigStore({ recordRuntimeEvent });
105
145
  configStoreForRedaction = configStore;
146
+ getRuntimeLogProfileName = configStore.getActiveProfileName;
106
147
  const isTelegramBusConfigured = function (): boolean {
107
148
  return true;
108
149
  };
@@ -112,9 +153,15 @@ export default function (pi: Pi.ExtensionAPI) {
112
153
  Config.bindGlobalTelegramConfigRuntime(configStore);
113
154
  const configControls = Config.createTelegramConfigControls(configStore);
114
155
  const threadStore = Threads.createTelegramTopicTargetStore({
115
- path: Threads.getTelegramTopicTargetsPath(),
156
+ path: function () {
157
+ return Threads.getTelegramTopicTargetsPath(
158
+ undefined,
159
+ configStore.getActiveProfileName(),
160
+ );
161
+ },
116
162
  });
117
163
  const lockRuntime = Locks.createTelegramLockRuntime<Pi.ExtensionContext>({
164
+ key: Locks.createTelegramLockKeyResolver(configStore),
118
165
  instanceId: telegramInstanceId,
119
166
  busSecret: telegramBusAuthSecret,
120
167
  staleHeartbeatMs: Locks.TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS,
@@ -201,7 +248,7 @@ export default function (pi: Pi.ExtensionAPI) {
201
248
  await configStore.persist();
202
249
  markTelegramConfigSyncChange("config-persist");
203
250
  };
204
- const getCurrentThreadRecord = function ():
251
+ const findCurrentThreadRecord = function ():
205
252
  | Threads.TelegramTopicTargetRecord
206
253
  | undefined {
207
254
  return Threads.findCurrentTelegramInstanceThreadRecord({
@@ -213,11 +260,24 @@ export default function (pi: Pi.ExtensionAPI) {
213
260
  telegramBusLeaderTarget,
214
261
  });
215
262
  };
263
+ const getCurrentThreadRecord = function ():
264
+ | Threads.TelegramTopicTargetRecord
265
+ | undefined {
266
+ const record = findCurrentThreadRecord();
267
+ if (
268
+ record?.owner?.kind === "manual-follower" &&
269
+ !telegramBusFollowerRegistrationState.isRegistered()
270
+ ) {
271
+ return undefined;
272
+ }
273
+ return record;
274
+ };
216
275
  const statusRuntime = Status.createTelegramBridgeStatusRuntime<
217
276
  Pi.ExtensionContext,
218
277
  Queue.TelegramQueueItem<Pi.ExtensionContext>
219
278
  >({
220
279
  getConfig: configStore.get,
280
+ getActiveProfileName: configStore.getActiveProfileName,
221
281
  isPollingActive: Polling.createTelegramPollingActivityReader(
222
282
  pollingControllerState,
223
283
  ),
@@ -252,12 +312,13 @@ export default function (pi: Pi.ExtensionAPI) {
252
312
  },
253
313
  getLocalBus() {
254
314
  return {
255
- leaderSocketPath: telegramBusSocketPath,
256
- leaderTransport:
257
- BusTransport.getTelegramBusTransportKind(telegramBusSocketPath),
258
- followerSocketPath: telegramBusFollowerSocketPath,
315
+ leaderSocketPath: getTelegramBusSocketPath(),
316
+ leaderTransport: BusTransport.getTelegramBusTransportKind(
317
+ getTelegramBusSocketPath(),
318
+ ),
319
+ followerSocketPath: getTelegramBusFollowerSocketPath(),
259
320
  followerTransport: BusTransport.getTelegramBusTransportKind(
260
- telegramBusFollowerSocketPath,
321
+ getTelegramBusFollowerSocketPath(),
261
322
  ),
262
323
  followerRegistered: telegramBusFollowerRegistrationState.isRegistered(),
263
324
  followerTarget: telegramBusFollowerRegistrationState.getTarget(),
@@ -302,15 +363,12 @@ export default function (pi: Pi.ExtensionAPI) {
302
363
  },
303
364
  });
304
365
  const { updateStatus: updateStatusLine } = statusRuntime;
305
- let lastRuntimeLogScopeKey: string | undefined;
306
366
  const updateRuntimeLogScope = function (reason: string) {
307
367
  const scope = Status.createTelegramRuntimeLogScope({
308
368
  state: statusRuntime.getStatusState(),
309
369
  instanceId: telegramInstanceId,
310
370
  });
311
371
  const scopeKey = JSON.stringify(scope);
312
- if (scopeKey === lastRuntimeLogScopeKey) return;
313
- lastRuntimeLogScopeKey = scopeKey;
314
372
  runtimeJsonlLog.resetIfScopeChanged(scopeKey, reason, scope);
315
373
  };
316
374
  const updateStatus = function (ctx: Pi.ExtensionContext) {
@@ -367,7 +425,7 @@ export default function (pi: Pi.ExtensionAPI) {
367
425
  },
368
426
  getDefaultTarget: proactivePushTargetGetter,
369
427
  callFollowerApi: BusFollower.createTelegramBusFollowerApiCaller({
370
- socketPath: telegramBusSocketPath,
428
+ socketPath: getTelegramBusSocketPath,
371
429
  instanceId: telegramInstanceId,
372
430
  createRequestId() {
373
431
  telegramBusRequestSequence += 1;
@@ -596,7 +654,7 @@ export default function (pi: Pi.ExtensionAPI) {
596
654
  Routing.TelegramRoutedCallbackQuery,
597
655
  Routing.TelegramRoutedMessage
598
656
  >({
599
- socketPath: telegramBusSocketPath,
657
+ socketPath: getTelegramBusSocketPath,
600
658
  createRequestId() {
601
659
  telegramBusRequestSequence += 1;
602
660
  return Bus.createTelegramBusRequestId({
@@ -611,7 +669,7 @@ export default function (pi: Pi.ExtensionAPI) {
611
669
  });
612
670
  const followerTargetController =
613
671
  Bus.createTelegramBusFollowerTargetController({
614
- socketPath: telegramBusSocketPath,
672
+ socketPath: getTelegramBusSocketPath,
615
673
  createRequestId() {
616
674
  telegramBusRequestSequence += 1;
617
675
  return Bus.createTelegramBusRequestId({
@@ -766,7 +824,7 @@ export default function (pi: Pi.ExtensionAPI) {
766
824
  Routing.TelegramRoutedCallbackQuery,
767
825
  Routing.TelegramRoutedMessage
768
826
  >({
769
- socketPath: telegramBusFollowerSocketPath,
827
+ socketPath: getTelegramBusFollowerSocketPath,
770
828
  instanceId: telegramInstanceId,
771
829
  getContext() {
772
830
  return telegramSessionContextStore.get();
@@ -800,7 +858,7 @@ export default function (pi: Pi.ExtensionAPI) {
800
858
  topicTargetStore: threadStore,
801
859
  registrationState: telegramBusFollowerRegistrationState,
802
860
  instanceId: telegramInstanceId,
803
- manualFollowerProfileKey: telegramManualFollowerProfileKey,
861
+ manualFollowerProfileKey: getTelegramManualFollowerProfileKey(),
804
862
  manualFollowerOwnerId: telegramManualFollowerOwnerId,
805
863
  getSyncState() {
806
864
  return telegramSyncState;
@@ -834,6 +892,7 @@ export default function (pi: Pi.ExtensionAPI) {
834
892
  store: threadStore,
835
893
  instanceId: telegramInstanceId,
836
894
  cwd: ctx.cwd,
895
+ telegramProfile: getActiveTelegramThreadProfile(),
837
896
  target: binding.target,
838
897
  slot: binding.slot,
839
898
  threadName: binding.threadName,
@@ -864,7 +923,8 @@ export default function (pi: Pi.ExtensionAPI) {
864
923
  BusFollower.createTelegramBusFollowerRegistrationRuntime<Pi.ExtensionContext>(
865
924
  {
866
925
  instanceId: telegramInstanceId,
867
- followerBusSocketPath: telegramBusFollowerSocketPath,
926
+ getFollowerBusSocketPath: getTelegramBusFollowerSocketPath,
927
+ getLeaderSocketPath: getTelegramBusSocketPath,
868
928
  startReceiving: telegramBusForwardedUpdateReceiver.start,
869
929
  stopReceiving: telegramBusForwardedUpdateReceiver.stop,
870
930
  registrationState: telegramBusFollowerRegistrationState,
@@ -885,7 +945,7 @@ export default function (pi: Pi.ExtensionAPI) {
885
945
  return undefined;
886
946
  },
887
947
  getProfileKey() {
888
- return telegramManualFollowerProfileKey;
948
+ return getTelegramManualFollowerProfileKey();
889
949
  },
890
950
  onHeartbeatFailure: telegramFollowerHeartbeatRecovery,
891
951
  recordRuntimeEvent,
@@ -921,7 +981,7 @@ export default function (pi: Pi.ExtensionAPI) {
921
981
  });
922
982
  };
923
983
  const telegramBusLeaderRuntime = BusLeader.createTelegramBusLeaderRuntime({
924
- socketPath: telegramBusSocketPath,
984
+ socketPath: getTelegramBusSocketPath,
925
985
  followerRegistry: telegramBusFollowerRegistry,
926
986
  authSecret: telegramBusAuthSecret,
927
987
  startPolling: pollingRuntime.start,
@@ -942,6 +1002,7 @@ export default function (pi: Pi.ExtensionAPI) {
942
1002
  getCwd(ctx) {
943
1003
  return typeof ctx.cwd === "string" ? ctx.cwd : undefined;
944
1004
  },
1005
+ getTelegramProfile: getActiveTelegramThreadProfile,
945
1006
  shouldForceFreshUnnamed() {
946
1007
  return forceFreshLeaderThreadOnNextStart;
947
1008
  },
@@ -1174,7 +1235,7 @@ export default function (pi: Pi.ExtensionAPI) {
1174
1235
  const disconnectTelegramAndDeleteCurrentThread =
1175
1236
  Sync.createTelegramManualThreadDisconnectHandler({
1176
1237
  instanceId: telegramInstanceId,
1177
- getCurrentThreadRecord,
1238
+ getCurrentThreadRecord: findCurrentThreadRecord,
1178
1239
  topicTargetStore: threadStore,
1179
1240
  callApi: callTelegramApi,
1180
1241
  getLeaderTarget() {
@@ -1199,6 +1260,14 @@ export default function (pi: Pi.ExtensionAPI) {
1199
1260
  registrationState: telegramBusFollowerRegistrationState,
1200
1261
  instanceId: telegramInstanceId,
1201
1262
  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
+ },
1202
1271
  recordRuntimeEvent,
1203
1272
  });
1204
1273
  const queueSessionLifecycle = Queue.createTelegramSessionLifecycleRuntime({
package/lib/bindings.ts CHANGED
@@ -73,8 +73,8 @@ export function registerTelegramCommandsAndTools({
73
73
  getDefaultChatId,
74
74
  getDefaultTarget,
75
75
  canSendDirect,
76
- updateStatus,
77
76
  recordRuntimeEvent,
77
+ updateStatus,
78
78
  }: TelegramCommandsAndToolsBindingDeps): void {
79
79
  OutboundAttachments.registerTelegramOutboundAttachmentTool(pi, {
80
80
  getActiveTurn: activeTurnRuntime.get,
@@ -96,22 +96,72 @@ export function registerTelegramCommandsAndTools({
96
96
  });
97
97
  Prompts.registerTelegramHelpTool(pi);
98
98
  Commands.registerTelegramBridgeCommands(pi, {
99
- promptForConfig: Setup.createTelegramSetupPromptRuntime({
100
- getConfig: configStore.get,
101
- setConfig: configStore.set,
102
- setupGuard: setup,
103
- getMe: TelegramApi.fetchTelegramBotIdentity,
104
- persistConfig,
105
- startPolling: lockedPollingRuntime.start,
106
- updateStatus,
107
- recordRuntimeEvent,
108
- }),
99
+ promptForConfig: async (ctx, profileName) => {
100
+ const nextProfileName = profileName ?? undefined;
101
+ if (profileName && !Config.isValidTelegramProfileName(profileName)) {
102
+ ctx.ui.notify(`Invalid Telegram profile name: ${profileName}`, "error");
103
+ return;
104
+ }
105
+ const previousProfileName = configStore.getActiveProfileName();
106
+ if (previousProfileName !== nextProfileName) {
107
+ await (stopPolling ?? lockedPollingRuntime.stop)();
108
+ }
109
+ if (!profileName) {
110
+ configStore.activateProfile(undefined);
111
+ } else {
112
+ const storedConfig = configStore.getStoredConfig();
113
+ if (!storedConfig.profiles?.[profileName]) {
114
+ configStore.set({
115
+ ...storedConfig,
116
+ profiles: {
117
+ ...(storedConfig.profiles ?? {}),
118
+ [profileName]: { botToken: "" },
119
+ },
120
+ });
121
+ }
122
+ configStore.activateProfile(profileName);
123
+ }
124
+ const runSetup = Setup.createTelegramSetupPromptRuntime({
125
+ getConfig: configStore.get,
126
+ setConfig: configStore.set,
127
+ setupGuard: setup,
128
+ getMe: TelegramApi.fetchTelegramBotIdentity,
129
+ persistConfig,
130
+ startPolling: lockedPollingRuntime.start,
131
+ updateStatus,
132
+ recordRuntimeEvent,
133
+ });
134
+ await runSetup(ctx);
135
+ if (profileName) {
136
+ ctx.ui.notify(`Profile "${profileName}" saved and connected.`, "info");
137
+ }
138
+ },
109
139
  getStatusLines,
110
140
  reloadConfig: configStore.load,
111
141
  hasBotToken: configStore.hasBotToken,
112
142
  startPolling: lockedPollingRuntime.start,
113
143
  stopPolling: stopPolling ?? lockedPollingRuntime.stop,
114
144
  updateStatus,
145
+ getProfileNames: () =>
146
+ Config.getTelegramProfileNames(configStore.getStoredConfig()),
147
+ activateDefaultProfileConfig: async () => {
148
+ await configStore.load();
149
+ const previousProfileName = configStore.getActiveProfileName();
150
+ if (previousProfileName) {
151
+ await (stopPolling ?? lockedPollingRuntime.stop)();
152
+ }
153
+ configStore.activateProfile(undefined);
154
+ },
155
+ activateProfileConfig: async (_ctx, profileName) => {
156
+ await configStore.load();
157
+ if (!Config.isValidTelegramProfileName(profileName)) return false;
158
+ const previousProfileName = configStore.getActiveProfileName();
159
+ if (previousProfileName !== profileName) {
160
+ await (stopPolling ?? lockedPollingRuntime.stop)();
161
+ }
162
+ if (!configStore.activateProfile(profileName)) return false;
163
+ return true;
164
+ },
115
165
  });
116
166
  }
117
167
 
@@ -16,8 +16,10 @@ import {
16
16
  createTelegramBusLocalServer,
17
17
  createUnauthorizedBusAck,
18
18
  getTelegramBusSocketPath,
19
+ resolveTelegramBusSocketPath,
19
20
  sendTelegramBusLocalEnvelope,
20
21
  type TelegramBusEnvelope,
22
+ type TelegramBusSocketPathSource,
21
23
  } from "./bus.ts";
22
24
  import {
23
25
  getTelegramBusTransportRetryPolicy,
@@ -90,6 +92,7 @@ export interface TelegramBusFollowerSessionReplacementSuspenderDeps {
90
92
  registrationState: Pick<TelegramBusFollowerRegistrationState, "isRegistered">;
91
93
  instanceId: string;
92
94
  suspendPolling: () => Promise<void>;
95
+ onFollowerSessionDisconnect?: () => Promise<void> | void;
93
96
  recordRuntimeEvent: (
94
97
  category: string,
95
98
  error: unknown,
@@ -132,7 +135,7 @@ export interface TelegramBusForwardedUpdateReceiverRuntime {
132
135
  }
133
136
 
134
137
  export interface TelegramBusFollowerApiCallerDeps {
135
- socketPath: string;
138
+ socketPath: TelegramBusSocketPathSource;
136
139
  instanceId: string;
137
140
  createRequestId: () => string;
138
141
  getAuthSecret?: () => string | undefined;
@@ -148,6 +151,7 @@ export interface TelegramBusFollowerRegistrationRuntimeDeps<
148
151
  getLeaderAuthSecret?: (leader: { busSecret?: string }) => string | undefined;
149
152
  setActiveAuthSecret?: (secret: string | undefined) => void;
150
153
  followerBusSocketPath?: string;
154
+ getFollowerBusSocketPath?: () => string;
151
155
  getLeaderSocketPath?: () => string;
152
156
  startReceiving?: () => Promise<void>;
153
157
  stopReceiving?: () => Promise<void> | void;
@@ -237,7 +241,7 @@ export interface TelegramBusForwardedUpdateReceiverRuntimeDeps<
237
241
  TCallbackQuery,
238
242
  TMessage = unknown,
239
243
  > {
240
- socketPath: string;
244
+ socketPath: TelegramBusSocketPathSource;
241
245
  instanceId: string;
242
246
  getAuthSecret?: () => string | undefined;
243
247
  getContext: () => TContext | undefined;
@@ -353,11 +357,12 @@ export function createTelegramBusFollowerApiCaller(
353
357
  const getNowMs = deps.getNowMs ?? Date.now;
354
358
  const timeoutMs = deps.timeoutMs ?? 30000;
355
359
  return async (method, args) => {
360
+ const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
356
361
  const response = await sendTelegramBusLocalEnvelope({
357
- socketPath: deps.socketPath,
362
+ socketPath,
358
363
  timeoutMs,
359
364
  retry: getTelegramBusTransportRetryPolicy({
360
- endpoint: deps.socketPath,
365
+ endpoint: socketPath,
361
366
  operation: "operation",
362
367
  }),
363
368
  envelope: {
@@ -387,24 +392,42 @@ function isTelegramStaleContextError(error: unknown): boolean {
387
392
  );
388
393
  }
389
394
 
395
+ export async function markTelegramFollowerThreadStaleForSessionDisconnect(input: {
396
+ topicTargetStore: Threads.TelegramTopicTargetStore;
397
+ target: TelegramTarget;
398
+ }): Promise<void> {
399
+ await input.topicTargetStore.load();
400
+ const record = input.topicTargetStore.list().find(
401
+ (candidate) =>
402
+ candidate.target.chatId === input.target.chatId &&
403
+ candidate.target.threadId === input.target.threadId,
404
+ );
405
+ const marked = input.topicTargetStore.markStaleByTarget(
406
+ input.target,
407
+ "unknown",
408
+ "Follower session reloaded before reconnect.",
409
+ );
410
+ const forgotIdentity = record?.profileKey
411
+ ? input.topicTargetStore.forgetIdentityByProfileKey(record.profileKey)
412
+ : false;
413
+ if (marked || forgotIdentity) await input.topicTargetStore.persist();
414
+ }
415
+
390
416
  export function createTelegramBusFollowerSessionReplacementSuspender(
391
417
  deps: TelegramBusFollowerSessionReplacementSuspenderDeps,
392
418
  ): () => Promise<void> {
393
419
  return async () => {
394
420
  if (deps.registrationState.isRegistered()) {
395
- setTelegramFollowerSessionHandoff({
396
- pid: (deps.getPid ?? (() => process.pid))(),
397
- instanceId: deps.instanceId,
398
- createdAtMs: (deps.getNowMs ?? Date.now)(),
399
- });
421
+ setTelegramFollowerSessionHandoff(undefined);
422
+ await deps.onFollowerSessionDisconnect?.();
400
423
  deps.recordRuntimeEvent(
401
424
  "bus",
402
- "Telegram follower registration preserved",
425
+ "Telegram follower registration stopped for session replacement",
403
426
  {
404
- phase: "follower-session-preserve",
427
+ phase: "follower-session-disconnect",
428
+ instanceId: deps.instanceId,
405
429
  },
406
430
  );
407
- return;
408
431
  }
409
432
  await deps.suspendPolling();
410
433
  };
@@ -706,7 +729,8 @@ export function createTelegramBusFollowerRegistrationRuntime<
706
729
  (ctx.cwd ? basename(ctx.cwd) : undefined),
707
730
  cwd: ctx.cwd,
708
731
  pid: getPid(),
709
- busSocketPath: deps.followerBusSocketPath,
732
+ busSocketPath:
733
+ deps.getFollowerBusSocketPath?.() ?? deps.followerBusSocketPath,
710
734
  connectedAtMs: getNowMs(),
711
735
  },
712
736
  });
package/lib/bus-leader.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  type TelegramBusFollowerRegistry,
21
21
  type TelegramBusFollowerView,
22
22
  type TelegramBusInstanceRegistration,
23
+ type TelegramBusSocketPathSource,
23
24
  } from "./bus.ts";
24
25
  import { getTelegramBusTransportRetryPolicy } from "./bus-transport.ts";
25
26
 
@@ -54,6 +55,7 @@ export interface TelegramBusLeaderTargetProvisionerDeps<TContext> {
54
55
  getAllowedUserId: () => number | undefined;
55
56
  instanceId: string;
56
57
  getCwd?: (ctx: TContext) => string | undefined;
58
+ getTelegramProfile?: () => string | undefined;
57
59
  shouldForceFreshUnnamed?: () => boolean;
58
60
  topicTargetStore: Threads.TelegramTopicTargetStore;
59
61
  callApi: <TResponse>(
@@ -175,7 +177,7 @@ export type TelegramBusFollowerMessageOwnershipRecorder = (
175
177
  ) => void;
176
178
 
177
179
  export interface TelegramBusLeaderRuntimeDeps<TContext> {
178
- socketPath: string;
180
+ socketPath: TelegramBusSocketPathSource;
179
181
  followerRegistry: TelegramBusFollowerRegistry;
180
182
  authSecret?: string;
181
183
  startPolling: (ctx: TContext) => void | Promise<void>;
@@ -315,6 +317,10 @@ export function createTelegramBusFollowerTargetProvisioner(
315
317
  (TelegramTarget & { slot?: string; threadName?: string }) | undefined
316
318
  > {
317
319
  const getNowMs = deps.getNowMs ?? Date.now;
320
+ const pendingRegistrations = new Map<
321
+ string,
322
+ Promise<(TelegramTarget & { slot?: string; threadName?: string }) | undefined>
323
+ >();
318
324
  return async (registration) => {
319
325
  const registrationStartedAtMs = Date.now();
320
326
  const chatId = deps.getAllowedUserId();
@@ -337,6 +343,9 @@ export function createTelegramBusFollowerTargetProvisioner(
337
343
  registration.profileKey ?? `manual:${registration.instanceId}`;
338
344
  const followerOwner =
339
345
  Threads.getTelegramThreadOwnerFromProfileKey(followerProfileKey);
346
+ const registrationKey = followerProfileKey || registration.instanceId;
347
+ const pendingRegistration = pendingRegistrations.get(registrationKey);
348
+ if (pendingRegistration) return pendingRegistration;
340
349
  const provisionTarget = async () => {
341
350
  deps.onProvisioningStart?.();
342
351
  try {
@@ -356,6 +365,9 @@ export function createTelegramBusFollowerTargetProvisioner(
356
365
  deps.onProvisioningEnd?.();
357
366
  }
358
367
  };
368
+ const runRegistration = async (): Promise<
369
+ (TelegramTarget & { slot?: string; threadName?: string }) | undefined
370
+ > => {
359
371
  let result = await provisionTarget();
360
372
  deps.setSyncState(
361
373
  Sync.markTelegramSyncSliceFresh(deps.getSyncState(), "target-bindings", {
@@ -363,48 +375,14 @@ export function createTelegramBusFollowerTargetProvisioner(
363
375
  action: "follower-register",
364
376
  }),
365
377
  );
366
- let connectedAnnouncement =
367
- createTelegramBusInstanceLifecycleAnnouncement({
368
- target: result.target,
369
- threadName: result.record.threadName,
370
- slot: result.record.slot,
371
- state: "connected",
372
- });
373
- let connectedAnnouncementSent = false;
374
- if (result.reused && connectedAnnouncement) {
375
- try {
376
- await deps.callApi("sendMessage", {
377
- chat_id: connectedAnnouncement.target.chatId,
378
- message_thread_id: connectedAnnouncement.target.threadId,
379
- text: connectedAnnouncement.text,
380
- parse_mode: connectedAnnouncement.parseMode,
381
- });
382
- connectedAnnouncementSent = true;
383
- } catch (error) {
384
- deps.recordRuntimeEvent("telegram", error, {
385
- phase: "follower-topic-reuse-probe",
386
- instanceId: registration.instanceId,
387
- chatId: result.target.chatId,
388
- threadId: result.target.threadId,
378
+ const connectedAnnouncement = result.reused
379
+ ? undefined
380
+ : createTelegramBusInstanceLifecycleAnnouncement({
381
+ target: result.target,
382
+ threadName: result.record.threadName,
383
+ slot: result.record.slot,
384
+ state: "connected",
389
385
  });
390
- if (Threads.isTelegramTopicTargetStaleError(error)) {
391
- deps.topicTargetStore.markStaleByTarget(
392
- result.target,
393
- "deleted",
394
- "Follower registration found the reusable thread target stale.",
395
- );
396
- await deps.topicTargetStore.persist();
397
- result = await provisionTarget();
398
- connectedAnnouncement = createTelegramBusInstanceLifecycleAnnouncement({
399
- target: result.target,
400
- threadName: result.record.threadName,
401
- slot: result.record.slot,
402
- state: "connected",
403
- });
404
- connectedAnnouncementSent = false;
405
- }
406
- }
407
- }
408
386
  recordSlowTelegramBusFollowerRegistrationStep(deps, {
409
387
  phase: "follower-register-critical",
410
388
  elapsedMs: Date.now() - registrationStartedAtMs,
@@ -414,7 +392,7 @@ export function createTelegramBusFollowerTargetProvisioner(
414
392
  });
415
393
  scheduleTelegramBusLeaderBackgroundTask(async () => {
416
394
  const backgroundStartedAtMs = Date.now();
417
- if (connectedAnnouncement && !connectedAnnouncementSent) {
395
+ if (connectedAnnouncement) {
418
396
  try {
419
397
  await deps.callApi("sendMessage", {
420
398
  chat_id: connectedAnnouncement.target.chatId,
@@ -486,6 +464,12 @@ export function createTelegramBusFollowerTargetProvisioner(
486
464
  slot: result.record.slot,
487
465
  threadName: result.record.threadName,
488
466
  };
467
+ };
468
+ const registrationPromise = runRegistration().finally(() => {
469
+ pendingRegistrations.delete(registrationKey);
470
+ });
471
+ pendingRegistrations.set(registrationKey, registrationPromise);
472
+ return registrationPromise;
489
473
  };
490
474
  }
491
475
 
@@ -516,6 +500,7 @@ export function createTelegramBusLeaderTargetProvisioner<TContext>(
516
500
  getAllowedUserId: deps.getAllowedUserId,
517
501
  instanceId: deps.instanceId,
518
502
  cwd: deps.getCwd?.(ctx),
503
+ telegramProfile: deps.getTelegramProfile?.(),
519
504
  forceFreshUnnamed: deps.shouldForceFreshUnnamed?.(),
520
505
  getNowMs,
521
506
  getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,