@llblab/pi-telegram 0.20.3 → 0.20.5

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/lib/bindings.ts CHANGED
@@ -127,22 +127,29 @@ export function registerTelegramCommandsAndTools({
127
127
  });
128
128
  setupConfigStore.activateProfile(profileName);
129
129
  persistSetupConfig = async () => {
130
+ try {
131
+ configStore.activateProfile(undefined);
132
+ configStore.set({
133
+ ...storedConfig,
134
+ profiles: {
135
+ ...(storedConfig.profiles ?? {}),
136
+ [profileName]: storedConfig.profiles?.[profileName] ?? {
137
+ botToken: "",
138
+ },
139
+ },
140
+ });
141
+ configStore.activateProfile(profileName);
142
+ configStore.set(setupConfigStore.get());
143
+ await persistConfig();
144
+ } catch (error) {
145
+ configStore.activateProfile(undefined);
146
+ configStore.set(storedConfig);
147
+ configStore.activateProfile(previousProfileName);
148
+ throw error;
149
+ }
130
150
  if (previousProfileName !== profileName) {
131
151
  await (stopPolling ?? lockedPollingRuntime.stop)();
132
152
  }
133
- configStore.activateProfile(undefined);
134
- configStore.set({
135
- ...storedConfig,
136
- profiles: {
137
- ...(storedConfig.profiles ?? {}),
138
- [profileName]: storedConfig.profiles?.[profileName] ?? {
139
- botToken: "",
140
- },
141
- },
142
- });
143
- configStore.activateProfile(profileName);
144
- configStore.set(setupConfigStore.get());
145
- await persistConfig();
146
153
  };
147
154
  }
148
155
  const runSetup = Setup.createTelegramSetupPromptRuntime({
@@ -242,14 +249,8 @@ interface TelegramLifecycleBindingDeps {
242
249
  >["sendTextReply"] &
243
250
  NonNullable<OutboundHandlers.TelegramVoiceReplySenderDeps["sendTextReply"]>;
244
251
  dispatchNextQueuedTelegramTurn: (ctx: Pi.ExtensionContext) => void;
245
- answerGuestQuery: NonNullable<
246
- Queue.TelegramAgentEndHookRuntimeDeps<
247
- Queue.PendingTelegramTurn,
248
- Pi.ExtensionContext,
249
- Pi.AgentEndEvent["messages"][number],
250
- Keyboard.TelegramInlineKeyboardMarkup
251
- >["answerGuestQuery"]
252
- >;
252
+ answerGuestQuery: TelegramApi.TelegramBridgeApiRuntime["answerGuestQuery"];
253
+ deleteMessage: TelegramApi.TelegramBridgeApiRuntime["deleteMessage"];
253
254
  sendGuestReply: NonNullable<
254
255
  Queue.TelegramAgentEndHookRuntimeDeps<
255
256
  Queue.PendingTelegramTurn,
@@ -295,6 +296,7 @@ export function registerTelegramLifecycleRuntimeHooks({
295
296
  sendTextReply,
296
297
  dispatchNextQueuedTelegramTurn,
297
298
  answerGuestQuery,
299
+ deleteMessage,
298
300
  sendGuestReply,
299
301
  finalizeMarkdownPreview,
300
302
  proactivePushChatIdGetter,
@@ -319,18 +321,107 @@ export function registerTelegramLifecycleRuntimeHooks({
319
321
  sendTextReply,
320
322
  recordRuntimeEvent,
321
323
  });
322
- const outboundReplyPlanner =
323
- OutboundHandlers.createTelegramOutboundReplyPlanner(buttonActionStore);
324
- const outboundReplyArtifactSender =
325
- OutboundHandlers.createTelegramOutboundReplyArtifactSender({
326
- execCommand: CommandTemplates.execCommandTemplate,
324
+ const sendGuestAttachment = async (
325
+ turn: Queue.PendingTelegramTurn,
326
+ attachment: Queue.QueuedAttachment,
327
+ caption?: string,
328
+ ): Promise<void> => {
329
+ const stagingTarget = proactivePushTargetGetter();
330
+ const stagingChatId = stagingTarget?.chatId ?? proactivePushChatIdGetter();
331
+ if (stagingChatId === undefined) {
332
+ throw new Error("Guest attachment staging requires a paired Telegram chat");
333
+ }
334
+ await OutboundAttachments.deliverTelegramGuestCachedAttachment({
335
+ guestQueryId: turn.guestQueryId!,
336
+ stagingChatId,
337
+ stagingTarget,
338
+ attachment,
339
+ caption,
327
340
  sendMultipart: callMultipart,
328
- sendTextReply,
329
- sendChatAction,
330
- sendRecordVoiceAction,
331
- getHandlers: configStore.getOutboundHandlers,
341
+ answerGuestQuery: (guestQueryId, result) =>
342
+ answerGuestQuery(guestQueryId, undefined, { result }),
343
+ answerGuestText: (guestQueryId, text) =>
344
+ answerGuestQuery(guestQueryId, text),
345
+ fallbackText:
346
+ caption || "Telegram bridge could not deliver the requested attachment.",
347
+ deleteMessage,
332
348
  recordRuntimeEvent,
333
349
  });
350
+ };
351
+ const outboundReplyPlanner =
352
+ OutboundHandlers.createTelegramOutboundReplyPlanner(buttonActionStore);
353
+ const voiceReplySenderDeps = {
354
+ execCommand: CommandTemplates.execCommandTemplate,
355
+ sendMultipart: callMultipart,
356
+ sendTextReply,
357
+ sendChatAction,
358
+ sendRecordVoiceAction,
359
+ getHandlers: configStore.getOutboundHandlers,
360
+ recordRuntimeEvent,
361
+ };
362
+ const outboundReplyArtifactSender =
363
+ OutboundHandlers.createTelegramOutboundReplyArtifactSender(
364
+ voiceReplySenderDeps,
365
+ );
366
+ const sendGuestVoiceReply = async (
367
+ turn: Queue.PendingTelegramTurn,
368
+ plan: OutboundHandlers.TelegramOutboundReplyPlan,
369
+ caption?: string,
370
+ ): Promise<void> => {
371
+ const stagingTarget = proactivePushTargetGetter();
372
+ const stagingChatId = stagingTarget?.chatId ?? proactivePushChatIdGetter();
373
+ if (stagingChatId === undefined) {
374
+ throw new Error("Guest voice staging requires a paired Telegram chat");
375
+ }
376
+ const guestVoiceSender =
377
+ OutboundHandlers.createTelegramOutboundReplyArtifactSender({
378
+ ...voiceReplySenderDeps,
379
+ sendChatAction: undefined,
380
+ sendRecordVoiceAction: undefined,
381
+ sendMultipart: async (
382
+ _method,
383
+ _fields,
384
+ _fileField,
385
+ filePath,
386
+ fileName,
387
+ ) => {
388
+ try {
389
+ await OutboundAttachments.deliverTelegramGuestCachedAttachment({
390
+ guestQueryId: turn.guestQueryId!,
391
+ stagingChatId,
392
+ stagingTarget,
393
+ attachment: { path: filePath, fileName },
394
+ caption,
395
+ sendMultipart: callMultipart,
396
+ answerGuestQuery: (guestQueryId, result) =>
397
+ answerGuestQuery(guestQueryId, undefined, { result }),
398
+ answerGuestText: (guestQueryId, text) =>
399
+ answerGuestQuery(guestQueryId, text),
400
+ fallbackText:
401
+ caption || "Telegram bridge could not deliver the voice reply.",
402
+ deleteMessage,
403
+ recordRuntimeEvent,
404
+ });
405
+ } catch (error) {
406
+ recordRuntimeEvent("delivery", error, {
407
+ phase: "guest-voice-answer",
408
+ guestQueryId: turn.guestQueryId,
409
+ });
410
+ }
411
+ return {};
412
+ },
413
+ });
414
+ await guestVoiceSender(
415
+ turn,
416
+ {
417
+ ...plan,
418
+ ...(plan.voiceReplies?.length
419
+ ? { voiceReplies: [plan.voiceReplies[0]!] }
420
+ : {}),
421
+ },
422
+ { replyToPrompt: false },
423
+ );
424
+ };
334
425
  const agentLifecycleHooks = Queue.createTelegramAgentLifecycleHooks<
335
426
  Queue.PendingTelegramTurn,
336
427
  Pi.ExtensionContext,
@@ -383,6 +474,8 @@ export function registerTelegramLifecycleRuntimeHooks({
383
474
  sendQueuedAttachments: queuedAttachmentSender,
384
475
  answerGuestQuery,
385
476
  sendGuestReply,
477
+ sendGuestAttachment,
478
+ sendGuestVoiceReply,
386
479
  planOutboundReply: outboundReplyPlanner,
387
480
  sendOutboundReplyArtifacts: outboundReplyArtifactSender,
388
481
  getDefaultChatId: proactivePushChatIdGetter,
@@ -398,8 +491,8 @@ export function registerTelegramLifecycleRuntimeHooks({
398
491
  const agentStartWithDedupReset = Lifecycle.createAgentStartDedupHook(
399
492
  agentLifecycleHooks.onAgentStart,
400
493
  );
401
- const startAgentActivityTypingLoop = (ctx: Pi.ExtensionContext): void => {
402
- if (!canSendAgentActivity(ctx)) return;
494
+ const startAgentActivityTypingLoop = (ctx: Pi.ExtensionContext): boolean => {
495
+ if (!canSendAgentActivity(ctx)) return false;
403
496
  const turn = activeTurnRuntime.get();
404
497
  const target = turn?.target ?? proactivePushTargetGetter();
405
498
  promptDispatchRuntime.startTypingLoop(
@@ -407,6 +500,7 @@ export function registerTelegramLifecycleRuntimeHooks({
407
500
  turn?.chatId ?? target?.chatId ?? proactivePushChatIdGetter(),
408
501
  { target },
409
502
  );
503
+ return true;
410
504
  };
411
505
  const startActiveTurnTypingLoop = (ctx: Pi.ExtensionContext): void => {
412
506
  const turn = activeTurnRuntime.get();
@@ -417,9 +511,8 @@ export function registerTelegramLifecycleRuntimeHooks({
417
511
  const compactionObserver = Lifecycle.createTelegramCompactionObserverRuntime({
418
512
  setCompactionInProgress: lifecycle.setCompactionInProgress,
419
513
  updateStatus,
420
- startTypingLoop: startActiveTurnTypingLoop,
514
+ startTypingLoop: startAgentActivityTypingLoop,
421
515
  stopTypingLoop: typing.stop,
422
- shouldStartTypingLoop: activeTurnRuntime.has,
423
516
  requestDeferredDispatchNextQueuedTelegramTurn:
424
517
  deferredQueueDispatchRuntime.request,
425
518
  dispatchNextQueuedTelegramTurn,
package/lib/bus-api.ts CHANGED
@@ -5,10 +5,10 @@
5
5
  */
6
6
 
7
7
  import type {
8
+ TelegramAnswerGuestQueryOptions,
8
9
  TelegramApiCallOptions,
9
10
  TelegramBridgeApiRuntime,
10
11
  TelegramEditMessageTextBody,
11
- TelegramInputRichMessage,
12
12
  TelegramSendMessageBody,
13
13
  TelegramSendMessageDraftBody,
14
14
  TelegramSendRichMessageBody,
@@ -273,14 +273,16 @@ export function createTelegramBusAwareApiRuntime(
273
273
  async answerGuestQuery(
274
274
  guestQueryId: string,
275
275
  text?: string,
276
- options?: { parseMode?: string; richMessage?: TelegramInputRichMessage },
276
+ options?: TelegramAnswerGuestQueryOptions,
277
277
  ): Promise<void> {
278
278
  if (deps.ownsDirect()) {
279
279
  await deps.directRuntime.answerGuestQuery(guestQueryId, text, options);
280
280
  return;
281
281
  }
282
282
  const body: Record<string, unknown> = { guest_query_id: guestQueryId };
283
- if (text !== undefined || options?.richMessage) {
283
+ if (options?.result) {
284
+ body.result = options.result;
285
+ } else if (text !== undefined || options?.richMessage) {
284
286
  const inputContent: Record<string, unknown> = options?.richMessage
285
287
  ? { rich_message: options.richMessage }
286
288
  : { message_text: text };
@@ -777,6 +777,8 @@ export function createTelegramBusFollowerRegistrationRuntime<
777
777
  let activeAuthSecret: string | undefined;
778
778
  let activeContext: TContext | undefined;
779
779
  let lastKnownTarget: TelegramTarget | undefined;
780
+ let lastKnownSlot: string | undefined;
781
+ let lastKnownThreadName: string | undefined;
780
782
  const stopHeartbeat = () => {
781
783
  if (!heartbeatInterval) return;
782
784
  clearInterval(heartbeatInterval);
@@ -788,6 +790,8 @@ export function createTelegramBusFollowerRegistrationRuntime<
788
790
  deps.setActiveAuthSecret?.(undefined);
789
791
  deps.registrationState?.setRegistered(false);
790
792
  lastKnownTarget = undefined;
793
+ lastKnownSlot = undefined;
794
+ lastKnownThreadName = undefined;
791
795
  activeContext = undefined;
792
796
  void deps.stopReceiving?.();
793
797
  };
@@ -849,8 +853,13 @@ export function createTelegramBusFollowerRegistrationRuntime<
849
853
  deps.getProfileKey?.(ctx) ??
850
854
  (ctx.cwd ? `cwd:${ctx.cwd}` : undefined),
851
855
  threadName:
856
+ deps.registrationState?.getThreadName() ??
857
+ lastKnownThreadName ??
852
858
  deps.getThreadName?.(ctx) ??
853
859
  (ctx.cwd ? basename(ctx.cwd) : undefined),
860
+ ...(deps.registrationState?.getSlot() ?? lastKnownSlot
861
+ ? { slot: deps.registrationState?.getSlot() ?? lastKnownSlot }
862
+ : {}),
854
863
  cwd: ctx.cwd,
855
864
  pid: getPid(),
856
865
  target:
@@ -912,6 +921,8 @@ export function createTelegramBusFollowerRegistrationRuntime<
912
921
  registrationResult,
913
922
  );
914
923
  lastKnownTarget = registrationResult.target;
924
+ lastKnownSlot = registrationResult.slot;
925
+ lastKnownThreadName = registrationResult.threadName;
915
926
  activeLeaderSocketPath = leaderSocketPath;
916
927
  activeContext = ctx;
917
928
  await sendHeartbeat();
package/lib/bus-leader.ts CHANGED
@@ -477,6 +477,22 @@ export function createTelegramBusFollowerTargetProvisioner(
477
477
  : undefined;
478
478
  const followerOwner =
479
479
  Threads.getTelegramThreadOwnerFromProfileKey(followerProfileKey);
480
+ const recoverableTarget =
481
+ !reconnectRecord &&
482
+ registration.target?.chatId === chatId &&
483
+ registration.target.threadId !== undefined &&
484
+ !recordsBeforeProvision.some(
485
+ (record) =>
486
+ record.target.chatId === registration.target?.chatId &&
487
+ record.target.threadId === registration.target.threadId,
488
+ )
489
+ ? registration.target
490
+ : undefined;
491
+ const recoveryHint = recoverableTarget
492
+ ? deps.topicTargetStore.getFollowerRecoveryHintByTarget?.(
493
+ recoverableTarget,
494
+ )
495
+ : undefined;
480
496
  const registrationKey = followerProfileKey || registration.instanceId;
481
497
  const pendingRegistration = pendingRegistrations.get(registrationKey);
482
498
  if (pendingRegistration) return pendingRegistration;
@@ -499,12 +515,69 @@ export function createTelegramBusFollowerTargetProvisioner(
499
515
  deps.onProvisioningEnd?.();
500
516
  }
501
517
  };
518
+ const recoverRequestedTarget = async () => {
519
+ const nowMs = getNowMs();
520
+ const requestedThreadName =
521
+ registration.threadName &&
522
+ Threads.isTelegramTopicThreadNameValidForSlot(
523
+ registration.threadName,
524
+ registration.slot,
525
+ )
526
+ ? registration.threadName
527
+ : recoveryHint?.threadName &&
528
+ Threads.isTelegramTopicThreadNameValidForSlot(
529
+ recoveryHint.threadName,
530
+ recoveryHint.slot,
531
+ )
532
+ ? recoveryHint.threadName
533
+ : undefined;
534
+ let recoveredRecord = deps.topicTargetStore.upsert({
535
+ profileKey: followerProfileKey,
536
+ owner:
537
+ followerOwner.kind === "manual-follower"
538
+ ? followerOwner
539
+ : {
540
+ kind: "manual-follower",
541
+ instanceId: registration.instanceId,
542
+ },
543
+ target: {
544
+ chatId: recoverableTarget!.chatId,
545
+ threadId: recoverableTarget!.threadId!,
546
+ },
547
+ status: "active",
548
+ createdAtMs: registration.connectedAtMs || nowMs,
549
+ updatedAtMs: nowMs,
550
+ instanceId: registration.instanceId,
551
+ ...(requestedThreadName ? { threadName: requestedThreadName } : {}),
552
+ lastSyncObservedAtMs: nowMs,
553
+ lastReconcileAction: "follower-live-target-recovery",
554
+ });
555
+ const requestedSlot = registration.slot ?? recoveryHint?.slot;
556
+ const requestedSlotAvailable =
557
+ !!requestedSlot &&
558
+ /^[A-Z]$/.test(requestedSlot) &&
559
+ !recordsBeforeProvision.some((record) => record.slot === requestedSlot);
560
+ if (requestedSlotAvailable) {
561
+ recoveredRecord = deps.topicTargetStore.upsert({
562
+ ...recoveredRecord,
563
+ slot: requestedSlot,
564
+ });
565
+ }
566
+ await deps.topicTargetStore.persist();
567
+ return {
568
+ target: recoveredRecord.target,
569
+ reused: true,
570
+ record: recoveredRecord,
571
+ };
572
+ };
502
573
  const runRegistration = async (): Promise<
503
574
  (TelegramTarget & { slot?: string; threadName?: string }) | undefined
504
575
  > => {
505
576
  let result = reconnectRecord
506
577
  ? { target: reconnectRecord.target, reused: true, record: reconnectRecord }
507
- : await provisionTarget();
578
+ : recoverableTarget
579
+ ? await recoverRequestedTarget()
580
+ : await provisionTarget();
508
581
  if (reconnectRecord) {
509
582
  const nowMs = getNowMs();
510
583
  const transferredRecord = deps.topicTargetStore.upsert({
@@ -1126,6 +1199,13 @@ export function createTelegramBusLeaderRuntime<TContext>(
1126
1199
  followerRealityTimer.unref?.();
1127
1200
  };
1128
1201
  const pruneFollowers = async () => {
1202
+ try {
1203
+ await localServer.ensureEndpoint();
1204
+ } catch (error) {
1205
+ deps.recordRuntimeEvent?.("bus", error, {
1206
+ phase: "leader-endpoint-recovery",
1207
+ });
1208
+ }
1129
1209
  const removed = deps.followerRegistry.pruneStale(
1130
1210
  getNowMs(),
1131
1211
  followerStaleAfterMs,
package/lib/bus.ts CHANGED
@@ -98,6 +98,7 @@ export interface TelegramBusInstanceRegistration {
98
98
  instanceId: string;
99
99
  profileKey?: string;
100
100
  threadName?: string;
101
+ slot?: string;
101
102
  cwd?: string;
102
103
  pid?: number;
103
104
  target?: TelegramTarget;
@@ -164,6 +165,7 @@ export function isTelegramFollowerApiCallAllowed(input: {
164
165
  "sendRichMessageDraft",
165
166
  ]);
166
167
  const allowedMultipartMethods = new Set([
168
+ "sendAudio",
167
169
  "sendDocument",
168
170
  "sendMediaGroup",
169
171
  "sendPhoto",
@@ -383,6 +385,7 @@ export function parseTelegramBusEnvelope(
383
385
  export interface TelegramBusLocalServer {
384
386
  start: () => Promise<void>;
385
387
  stop: () => Promise<void>;
388
+ ensureEndpoint: () => Promise<boolean>;
386
389
  }
387
390
 
388
391
  export type TelegramBusSocketPathSource = string | (() => string);
@@ -609,12 +612,14 @@ export function createTelegramBusLocalServer(
609
612
  ): TelegramBusLocalServer {
610
613
  let server: Server | undefined;
611
614
  let activeSocketPath: string | undefined;
615
+ let endpointRecovery: Promise<boolean> | undefined;
616
+ let stopGeneration = 0;
612
617
  const sockets = new Set<Socket>();
613
618
  const closeSocket = (socket: Socket) => {
614
619
  sockets.delete(socket);
615
620
  socket.destroy();
616
621
  };
617
- return {
622
+ const runtime: TelegramBusLocalServer = {
618
623
  start: async () => {
619
624
  if (server) return;
620
625
  const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
@@ -678,6 +683,7 @@ export function createTelegramBusLocalServer(
678
683
  if (!usesWindowsPipe) chmodSync(socketPath, 0o600);
679
684
  },
680
685
  stop: async () => {
686
+ stopGeneration += 1;
681
687
  const activeServer = server;
682
688
  const socketPath = activeSocketPath;
683
689
  server = undefined;
@@ -702,7 +708,44 @@ export function createTelegramBusLocalServer(
702
708
  );
703
709
  }
704
710
  },
711
+ ensureEndpoint: async () => {
712
+ const socketPath = activeSocketPath;
713
+ if (
714
+ !server ||
715
+ !socketPath ||
716
+ isTelegramBusPipePath(socketPath) ||
717
+ existsSync(socketPath)
718
+ ) {
719
+ return false;
720
+ }
721
+ if (endpointRecovery) return endpointRecovery;
722
+ endpointRecovery = (async () => {
723
+ deps.recordTransportEvent?.(
724
+ "server-endpoint-missing",
725
+ getTelegramBusEndpointDiagnostics(socketPath),
726
+ );
727
+ const recoveryStopGeneration = stopGeneration + 1;
728
+ await runtime.stop();
729
+ if (stopGeneration !== recoveryStopGeneration) return false;
730
+ await runtime.start();
731
+ if (stopGeneration !== recoveryStopGeneration) {
732
+ await runtime.stop();
733
+ return false;
734
+ }
735
+ deps.recordTransportEvent?.(
736
+ "server-endpoint-recovered",
737
+ getTelegramBusEndpointDiagnostics(socketPath),
738
+ );
739
+ return true;
740
+ })();
741
+ try {
742
+ return await endpointRecovery;
743
+ } finally {
744
+ endpointRecovery = undefined;
745
+ }
746
+ },
705
747
  };
748
+ return runtime;
706
749
  }
707
750
 
708
751
  function getTelegramBusEnvelopeDiagnostics(
@@ -1068,6 +1111,9 @@ function parseRegistration(
1068
1111
  registration.profileKey = value.profileKey;
1069
1112
  if (typeof value.threadName === "string")
1070
1113
  registration.threadName = value.threadName;
1114
+ if (typeof value.slot === "string" && /^[A-Z]$/.test(value.slot)) {
1115
+ registration.slot = value.slot;
1116
+ }
1071
1117
  if (typeof value.cwd === "string") registration.cwd = value.cwd;
1072
1118
  if (typeof value.pid === "number") registration.pid = value.pid;
1073
1119
  if (typeof value.busSocketPath === "string") {
package/lib/config.ts CHANGED
@@ -85,8 +85,8 @@ export interface TelegramBotProfile {
85
85
  lastUpdateId?: number;
86
86
  }
87
87
 
88
- /** Profile names must be lowercase letters, digits, hyphens, underscores; max 32 chars. */
89
- const TELEGRAM_PROFILE_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]{0,31}$/;
88
+ /** Profile names must contain only lowercase ASCII letters and digits; max 32 chars. */
89
+ const TELEGRAM_PROFILE_NAME_PATTERN = /^[a-z0-9]{1,32}$/;
90
90
  const TELEGRAM_RESERVED_PROFILE_NAMES: ReadonlySet<string> = new Set([
91
91
  "default",
92
92
  "main",
package/lib/lifecycle.ts CHANGED
@@ -153,9 +153,8 @@ function unrefTelegramLifecycleTimer(timer: TelegramLifecycleTimer): void {
153
153
  export interface TelegramCompactionObserverRuntimeDeps<TContext> {
154
154
  setCompactionInProgress: (inProgress: boolean) => void;
155
155
  updateStatus: (ctx: TContext) => void;
156
- startTypingLoop?: (ctx: TContext) => void;
156
+ startTypingLoop?: (ctx: TContext) => boolean | void;
157
157
  stopTypingLoop?: () => void;
158
- shouldStartTypingLoop?: () => boolean;
159
158
  requestDeferredDispatchNextQueuedTelegramTurn: (
160
159
  dispatch: (ctx: TContext) => void,
161
160
  ) => void;
@@ -196,8 +195,9 @@ export function createTelegramCompactionObserverRuntime<TContext>(
196
195
  return {
197
196
  onSessionBeforeCompact: (_event, ctx) => {
198
197
  deps.setCompactionInProgress(true);
199
- typingStartedByObserver = deps.shouldStartTypingLoop?.() ?? true;
200
- if (typingStartedByObserver) deps.startTypingLoop?.(ctx);
198
+ const typingStartResult = deps.startTypingLoop?.(ctx);
199
+ typingStartedByObserver =
200
+ !!deps.startTypingLoop && typingStartResult !== false;
201
201
  deps.updateStatus(ctx);
202
202
  clearFallbackTimer();
203
203
  fallbackTimer = setTimer(() => {
package/lib/locks.ts CHANGED
@@ -203,6 +203,17 @@ export function formatTelegramLockEntry(lock: TelegramLockEntry): string {
203
203
  return lock.cwd ? `pid ${lock.pid}, cwd ${lock.cwd}` : `pid ${lock.pid}`;
204
204
  }
205
205
 
206
+ function formatTelegramFollowerRegistrationFailure(message: string): string {
207
+ if (/\b(?:ENOENT|ECONNREFUSED|ETIMEDOUT)\b/u.test(message)) {
208
+ return (
209
+ `live owner / unreachable bus endpoint after bounded retries (${message}); ` +
210
+ "wait briefly for owner recovery, then retry /telegram-connect. " +
211
+ "Do not force takeover while the owner remains live"
212
+ );
213
+ }
214
+ return message;
215
+ }
216
+
206
217
  function getLockState(
207
218
  lock: TelegramLockEntry | undefined,
208
219
  pid: number,
@@ -509,7 +520,7 @@ export function createTelegramLockedPollingRuntime<
509
520
  ok: false,
510
521
  canTakeover: false,
511
522
  owner,
512
- message: `Telegram bridge is active in another Pi instance (${owner}); follower registration failed: ${failureMessage}.`,
523
+ message: `Telegram bridge is active in another Pi instance (${owner}); follower registration failed: ${formatTelegramFollowerRegistrationFailure(failureMessage)}.`,
513
524
  };
514
525
  }
515
526
  }
package/lib/logs.ts CHANGED
@@ -65,7 +65,7 @@ export function getTelegramPreviousRuntimeLogPath(
65
65
  ): string {
66
66
  return resolveTelegramProfileTempFilePath(
67
67
  "logs",
68
- "previous.jsonl",
68
+ "_prev.jsonl",
69
69
  agentDir,
70
70
  profileName,
71
71
  );
@@ -91,7 +91,7 @@ export function createTelegramRuntimeJsonlLog(
91
91
  const resolvePreviousPath = () => {
92
92
  if (typeof options.previousPath === "function") return options.previousPath();
93
93
  if (options.previousPath) return options.previousPath;
94
- return resolvePath().replace(/\.jsonl$/u, ".previous.jsonl");
94
+ return resolvePath().replace(/\.jsonl$/u, "._prev.jsonl");
95
95
  };
96
96
  const maxBytes = options.maxBytes ?? DEFAULT_MAX_LOG_BYTES;
97
97
  const getNowMs = options.getNowMs ?? Date.now;