@llblab/pi-telegram 0.48.1 → 0.48.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/CHANGELOG.md CHANGED
@@ -4,6 +4,15 @@
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.48.3: Assistant publication identity hotfix
8
+
9
+ - `Thread restore display identity`: Restoring or reclaiming a Thread now renames the destination from its current Workspace display title when available, rather than leaking the internal baked slot name such as `Moss` into directory-title mode.
10
+ - `Assistant publication identity`: An empty terminal assistant message may recover an earlier preserved answer, but it no longer republishes that text when the same Telegram intermediate output was already admitted; the pre-send turn fence also prevents lost-acknowledgement retries while allowing later turns.
11
+
12
+ ## 0.48.2: Follower promotion recovery
13
+
14
+ - `Follower promotion recovery`: A failed follower-to-leader promotion, including unavailable Thread slot authority, is now contained as a retryable bus recovery event instead of escaping a detached heartbeat recovery promise and terminating Pi.
15
+
7
16
  ## 0.48.1: Truthful terminal queue count hotfix
8
17
 
9
18
  - `Terminal queue status`: The yellow `+N` suffix now counts only executable prompts still waiting in the Telegram queue. The current agent run never contributes—whether it started from Telegram, the terminal, or autonomous continuation—and a dispatched Telegram prompt leaves the visible count before its run settles.
@@ -201,7 +201,9 @@ export interface TelegramActivityPublicationRuntime {
201
201
  export declare function createTelegramActivityPublicationRuntime(): TelegramActivityPublicationRuntime;
202
202
  export interface TelegramAssistantOutputRuntime {
203
203
  start: () => void;
204
+ beginTurn: () => void;
204
205
  accept: (event: TelegramAssistantSegmentEvent) => void;
206
+ hasAdmittedTelegramIntermediate: (text: string) => boolean;
205
207
  waitForIdle: () => Promise<void>;
206
208
  stop: () => void;
207
209
  }
@@ -520,6 +520,7 @@ export function createTelegramAssistantOutputRuntime(deps) {
520
520
  let running = false;
521
521
  let tail = Promise.resolve();
522
522
  const admitted = new Set();
523
+ const admittedTelegramIntermediateText = new Set();
523
524
  const isEligibleEvent = (event) => (event.source === "telegram" && event.placement === "intermediate") ||
524
525
  event.source === "local" ||
525
526
  event.source === "autonomous" ||
@@ -529,8 +530,12 @@ export function createTelegramAssistantOutputRuntime(deps) {
529
530
  generation += 1;
530
531
  running = true;
531
532
  admitted.clear();
533
+ admittedTelegramIntermediateText.clear();
532
534
  tail = Promise.resolve();
533
535
  },
536
+ beginTurn() {
537
+ admittedTelegramIntermediateText.clear();
538
+ },
534
539
  accept(event) {
535
540
  if (!running || !isEligibleEvent(event) || !event.text.trim())
536
541
  return;
@@ -538,6 +543,9 @@ export function createTelegramAssistantOutputRuntime(deps) {
538
543
  if (admitted.has(key))
539
544
  return;
540
545
  admitted.add(key);
546
+ if (event.source === "telegram" && event.placement === "intermediate") {
547
+ admittedTelegramIntermediateText.add(event.text.trim());
548
+ }
541
549
  const admittedGeneration = generation;
542
550
  const admittedAuthority = deps.captureAuthority?.();
543
551
  const preparation = deps.prepareSend?.(event);
@@ -562,6 +570,9 @@ export function createTelegramAssistantOutputRuntime(deps) {
562
570
  }
563
571
  }).finally(() => preparation?.settle());
564
572
  },
573
+ hasAdmittedTelegramIntermediate(text) {
574
+ return admittedTelegramIntermediateText.has(text.trim());
575
+ },
565
576
  waitForIdle() {
566
577
  return tail;
567
578
  },
@@ -569,6 +580,7 @@ export function createTelegramAssistantOutputRuntime(deps) {
569
580
  generation += 1;
570
581
  running = false;
571
582
  admitted.clear();
583
+ admittedTelegramIntermediateText.clear();
572
584
  },
573
585
  };
574
586
  }
@@ -202,7 +202,7 @@ interface TelegramLifecycleBindingDeps {
202
202
  publicationRuntime: TelegramBridgePublicationRuntime;
203
203
  activityRuntime: Activity.TelegramActivityRuntime;
204
204
  activityVerbosityRuntime?: ActivityVerbosity.TelegramActivityVerbosityRuntime;
205
- assistantOutputRuntime: Pick<Activity.TelegramAssistantOutputRuntime, "start" | "waitForIdle" | "stop">;
205
+ assistantOutputRuntime: Pick<Activity.TelegramAssistantOutputRuntime, "start" | "beginTurn" | "hasAdmittedTelegramIntermediate" | "waitForIdle" | "stop">;
206
206
  sessionLifecycleRuntime: Pick<Lifecycle.TelegramLifecycleRegistrationDeps, "onSessionStart" | "onSessionShutdown" | "onModelSelect">;
207
207
  configStore: Pick<Config.TelegramConfigStore, "get" | "getOutboundHandlers" | "hasBotToken" | "load">;
208
208
  abort: Runtime.TelegramRuntimeAbortPort;
@@ -615,7 +615,10 @@ export function registerTelegramLifecycleRuntimeHooks({ pi, publicationRuntime,
615
615
  clearDispatchPending: lifecycle.clearDispatchPending,
616
616
  setFoldQueuedPromptsIntoHistory: lifecycle.setFoldQueuedPromptsIntoHistory,
617
617
  setActiveTurn: activeTurnRuntime.set,
618
- onPromptHandedOff,
618
+ onPromptHandedOff: (turn, ctx) => {
619
+ assistantOutputRuntime.beginTurn();
620
+ onPromptHandedOff?.(turn, ctx);
621
+ },
619
622
  createPreviewState: previewRuntime.resetState,
620
623
  startTypingLoop: (ctx) => {
621
624
  const turn = activeTurnRuntime.get();
@@ -627,6 +630,7 @@ export function registerTelegramLifecycleRuntimeHooks({ pi, publicationRuntime,
627
630
  getActiveTurn: activeTurnRuntime.get,
628
631
  loadConfig: configStore.load,
629
632
  extractAssistant: Replies.extractRunAssistantMessage,
633
+ isRecoveredAssistantAlreadyPublished: (assistant) => !!assistant.text && assistantOutputRuntime.hasAdmittedTelegramIntermediate(assistant.text),
630
634
  getFoldQueuedPromptsIntoHistory: lifecycle.shouldFoldQueuedPromptsIntoHistory,
631
635
  resetRuntimeState: agentEndResetter,
632
636
  isSessionActive: isSessionContextActive,
@@ -821,8 +821,8 @@ export function createTelegramBusFollowerHeartbeatRecoveryHandler(deps) {
821
821
  return;
822
822
  promotionPending = true;
823
823
  deps.registrationState.beginRecovery();
824
+ const initialBinding = carriedBinding ?? snapshotBinding();
824
825
  try {
825
- const initialBinding = carriedBinding ?? snapshotBinding();
826
826
  const state = deps.getLeaderState();
827
827
  if (state.kind === "active-elsewhere") {
828
828
  clearRegisteredState(ctx);
@@ -862,7 +862,10 @@ export function createTelegramBusFollowerHeartbeatRecoveryHandler(deps) {
862
862
  });
863
863
  return;
864
864
  }
865
- throw promotionError;
865
+ deps.recordRuntimeEvent("bus", promotionError, {
866
+ phase: "follower-promotion-failed",
867
+ });
868
+ scheduleRecovery(promotionError, ctx, initialBinding);
866
869
  }
867
870
  finally {
868
871
  promotionPending = false;
@@ -393,6 +393,7 @@ export interface TelegramAgentEndAssistantResult {
393
393
  text?: string;
394
394
  stopReason?: string;
395
395
  errorMessage?: string;
396
+ recoveredFromEarlier?: boolean;
396
397
  }
397
398
  export interface TelegramAgentEndOutboundVoiceReply {
398
399
  text: string;
@@ -468,6 +469,7 @@ export interface TelegramAgentEndHookRuntimeDeps<TTurn extends PendingTelegramTu
468
469
  getActiveTurn: () => TTurn | undefined;
469
470
  loadConfig?: () => Promise<void>;
470
471
  extractAssistant: (messages: readonly TMessage[]) => TelegramAgentEndAssistantResult;
472
+ isRecoveredAssistantAlreadyPublished?: (assistant: TelegramAgentEndAssistantResult) => boolean;
471
473
  getFoldQueuedPromptsIntoHistory: () => boolean;
472
474
  resetRuntimeState: () => void;
473
475
  isSessionActive?: (ctx: TContext) => boolean;
package/dist/lib/queue.js CHANGED
@@ -853,7 +853,11 @@ export function createTelegramAgentEndHook(deps) {
853
853
  if (deps.isSessionActive?.(ctx) === false)
854
854
  return;
855
855
  const turn = deps.getActiveTurn();
856
- const assistant = assistantOverride ?? (turn ? deps.extractAssistant(event.messages) : {});
856
+ const extractedAssistant = assistantOverride ?? (turn ? deps.extractAssistant(event.messages) : {});
857
+ const assistant = extractedAssistant.recoveredFromEarlier &&
858
+ deps.isRecoveredAssistantAlreadyPublished?.(extractedAssistant)
859
+ ? { stopReason: extractedAssistant.stopReason }
860
+ : extractedAssistant;
857
861
  const hasPublication = !!assistant.text || assistant.stopReason === "error" || !!turn?.queuedAttachments.length;
858
862
  const reservation = turn && !turn.guestQueryId && hasPublication ? deps.reserveActiveTurnDelivery?.() : undefined;
859
863
  const scheduleDelivery = reservation?.schedule ?? deps.scheduleActiveTurnDelivery;
@@ -44,6 +44,7 @@ export declare function extractRunAssistantMessage(messages: readonly unknown[])
44
44
  text?: string;
45
45
  stopReason?: string;
46
46
  errorMessage?: string;
47
+ recoveredFromEarlier?: boolean;
47
48
  };
48
49
  export interface TelegramReplyOwnershipRecorder {
49
50
  record: (input: {
@@ -142,6 +142,7 @@ export function extractRunAssistantMessage(messages) {
142
142
  text,
143
143
  stopReason: typeof rawStopReason === "string" ? rawStopReason : undefined,
144
144
  errorMessage: typeof rawErrorMessage === "string" ? rawErrorMessage : undefined,
145
+ recoveredFromEarlier: true,
145
146
  };
146
147
  }
147
148
  return latest;
@@ -923,7 +923,8 @@ export function createTelegramInboundRouteRuntime(deps) {
923
923
  await deps.callApi("editForumTopic", {
924
924
  chat_id: sourceTarget.chatId,
925
925
  message_thread_id: sourceTarget.threadId,
926
- name: Threads.getTelegramTopicTitleForThreadName(threadName, slot),
926
+ name: deps.getDisplayTitle?.(sourceTarget) ??
927
+ Threads.getTelegramTopicTitleForThreadName(threadName, slot),
927
928
  });
928
929
  assertExecutionCurrent();
929
930
  }
@@ -1024,7 +1025,8 @@ export function createTelegramInboundRouteRuntime(deps) {
1024
1025
  await deps.callApi("editForumTopic", {
1025
1026
  chat_id: sourceTarget.chatId,
1026
1027
  message_thread_id: sourceTarget.threadId,
1027
- name: Threads.getTelegramTopicTitleForThreadName(threadName, slot),
1028
+ name: deps.getDisplayTitle?.(sourceTarget) ??
1029
+ Threads.getTelegramTopicTitleForThreadName(threadName, slot),
1028
1030
  });
1029
1031
  assertExecutionCurrent();
1030
1032
  }
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.48.1",
3
+ "version": "0.48.3",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
package/lib/activity.ts CHANGED
@@ -804,7 +804,9 @@ export function createTelegramActivityPublicationRuntime(): TelegramActivityPubl
804
804
 
805
805
  export interface TelegramAssistantOutputRuntime {
806
806
  start: () => void;
807
+ beginTurn: () => void;
807
808
  accept: (event: TelegramAssistantSegmentEvent) => void;
809
+ hasAdmittedTelegramIntermediate: (text: string) => boolean;
808
810
  waitForIdle: () => Promise<void>;
809
811
  stop: () => void;
810
812
  }
@@ -834,6 +836,7 @@ export function createTelegramAssistantOutputRuntime<TAuthority = undefined>(dep
834
836
  let running = false;
835
837
  let tail: Promise<void> = Promise.resolve();
836
838
  const admitted = new Set<string>();
839
+ const admittedTelegramIntermediateText = new Set<string>();
837
840
  const isEligibleEvent = (event: TelegramAssistantSegmentEvent): boolean =>
838
841
  (event.source === "telegram" && event.placement === "intermediate") ||
839
842
  event.source === "local" ||
@@ -845,13 +848,20 @@ export function createTelegramAssistantOutputRuntime<TAuthority = undefined>(dep
845
848
  generation += 1;
846
849
  running = true;
847
850
  admitted.clear();
851
+ admittedTelegramIntermediateText.clear();
848
852
  tail = Promise.resolve();
849
853
  },
854
+ beginTurn() {
855
+ admittedTelegramIntermediateText.clear();
856
+ },
850
857
  accept(event) {
851
858
  if (!running || !isEligibleEvent(event) || !event.text.trim()) return;
852
859
  const key = `${event.activityId}:${event.sequence}`;
853
860
  if (admitted.has(key)) return;
854
861
  admitted.add(key);
862
+ if (event.source === "telegram" && event.placement === "intermediate") {
863
+ admittedTelegramIntermediateText.add(event.text.trim());
864
+ }
855
865
  const admittedGeneration = generation;
856
866
  const admittedAuthority = deps.captureAuthority?.();
857
867
  const preparation = deps.prepareSend?.(event);
@@ -877,6 +887,9 @@ export function createTelegramAssistantOutputRuntime<TAuthority = undefined>(dep
877
887
  }
878
888
  }).finally(() => preparation?.settle());
879
889
  },
890
+ hasAdmittedTelegramIntermediate(text) {
891
+ return admittedTelegramIntermediateText.has(text.trim());
892
+ },
880
893
  waitForIdle() {
881
894
  return tail;
882
895
  },
@@ -884,6 +897,7 @@ export function createTelegramAssistantOutputRuntime<TAuthority = undefined>(dep
884
897
  generation += 1;
885
898
  running = false;
886
899
  admitted.clear();
900
+ admittedTelegramIntermediateText.clear();
887
901
  },
888
902
  };
889
903
  }
package/lib/bindings.ts CHANGED
@@ -835,7 +835,7 @@ interface TelegramLifecycleBindingDeps {
835
835
  activityVerbosityRuntime?: ActivityVerbosity.TelegramActivityVerbosityRuntime;
836
836
  assistantOutputRuntime: Pick<
837
837
  Activity.TelegramAssistantOutputRuntime,
838
- "start" | "waitForIdle" | "stop"
838
+ "start" | "beginTurn" | "hasAdmittedTelegramIntermediate" | "waitForIdle" | "stop"
839
839
  >;
840
840
  sessionLifecycleRuntime: Pick<
841
841
  Lifecycle.TelegramLifecycleRegistrationDeps,
@@ -1136,7 +1136,10 @@ export function registerTelegramLifecycleRuntimeHooks({
1136
1136
  clearDispatchPending: lifecycle.clearDispatchPending,
1137
1137
  setFoldQueuedPromptsIntoHistory: lifecycle.setFoldQueuedPromptsIntoHistory,
1138
1138
  setActiveTurn: activeTurnRuntime.set,
1139
- onPromptHandedOff,
1139
+ onPromptHandedOff: (turn, ctx) => {
1140
+ assistantOutputRuntime.beginTurn();
1141
+ onPromptHandedOff?.(turn, ctx);
1142
+ },
1140
1143
  createPreviewState: previewRuntime.resetState,
1141
1144
  startTypingLoop: (ctx) => {
1142
1145
  const turn = activeTurnRuntime.get();
@@ -1148,6 +1151,8 @@ export function registerTelegramLifecycleRuntimeHooks({
1148
1151
  getActiveTurn: activeTurnRuntime.get,
1149
1152
  loadConfig: configStore.load,
1150
1153
  extractAssistant: Replies.extractRunAssistantMessage,
1154
+ isRecoveredAssistantAlreadyPublished: (assistant) =>
1155
+ !!assistant.text && assistantOutputRuntime.hasAdmittedTelegramIntermediate(assistant.text),
1151
1156
  getFoldQueuedPromptsIntoHistory:
1152
1157
  lifecycle.shouldFoldQueuedPromptsIntoHistory,
1153
1158
  resetRuntimeState: agentEndResetter,
@@ -1496,8 +1496,8 @@ export function createTelegramBusFollowerHeartbeatRecoveryHandler<TContext>(
1496
1496
  if (promotionPending) return;
1497
1497
  promotionPending = true;
1498
1498
  deps.registrationState.beginRecovery();
1499
+ const initialBinding = carriedBinding ?? snapshotBinding();
1499
1500
  try {
1500
- const initialBinding = carriedBinding ?? snapshotBinding();
1501
1501
  const state = deps.getLeaderState();
1502
1502
  if (state.kind === "active-elsewhere") {
1503
1503
  clearRegisteredState(ctx);
@@ -1563,7 +1563,10 @@ export function createTelegramBusFollowerHeartbeatRecoveryHandler<TContext>(
1563
1563
  });
1564
1564
  return;
1565
1565
  }
1566
- throw promotionError;
1566
+ deps.recordRuntimeEvent("bus", promotionError, {
1567
+ phase: "follower-promotion-failed",
1568
+ });
1569
+ scheduleRecovery(promotionError, ctx, initialBinding);
1567
1570
  } finally {
1568
1571
  promotionPending = false;
1569
1572
  }
package/lib/queue.ts CHANGED
@@ -1474,6 +1474,7 @@ export interface TelegramAgentEndAssistantResult {
1474
1474
  text?: string;
1475
1475
  stopReason?: string;
1476
1476
  errorMessage?: string;
1477
+ recoveredFromEarlier?: boolean;
1477
1478
  }
1478
1479
 
1479
1480
  export interface TelegramAgentEndOutboundVoiceReply {
@@ -1592,6 +1593,7 @@ export interface TelegramAgentEndHookRuntimeDeps<
1592
1593
  extractAssistant: (
1593
1594
  messages: readonly TMessage[],
1594
1595
  ) => TelegramAgentEndAssistantResult;
1596
+ isRecoveredAssistantAlreadyPublished?: (assistant: TelegramAgentEndAssistantResult) => boolean;
1595
1597
  getFoldQueuedPromptsIntoHistory: () => boolean;
1596
1598
  resetRuntimeState: () => void;
1597
1599
  isSessionActive?: (ctx: TContext) => boolean;
@@ -1736,7 +1738,11 @@ export function createTelegramAgentEndHook<
1736
1738
  ): Promise<void> => {
1737
1739
  if (deps.isSessionActive?.(ctx) === false) return;
1738
1740
  const turn = deps.getActiveTurn();
1739
- const assistant = assistantOverride ?? (turn ? deps.extractAssistant(event.messages) : {});
1741
+ const extractedAssistant = assistantOverride ?? (turn ? deps.extractAssistant(event.messages) : {});
1742
+ const assistant = extractedAssistant.recoveredFromEarlier &&
1743
+ deps.isRecoveredAssistantAlreadyPublished?.(extractedAssistant)
1744
+ ? { stopReason: extractedAssistant.stopReason }
1745
+ : extractedAssistant;
1740
1746
  const hasPublication = !!assistant.text || assistant.stopReason === "error" || !!turn?.queuedAttachments.length;
1741
1747
  const reservation = turn && !turn.guestQueryId && hasPublication ? deps.reserveActiveTurnDelivery?.() : undefined;
1742
1748
  const scheduleDelivery = reservation?.schedule ?? deps.scheduleActiveTurnDelivery;
package/lib/replies.ts CHANGED
@@ -191,6 +191,7 @@ export function extractRunAssistantMessage(
191
191
  text?: string;
192
192
  stopReason?: string;
193
193
  errorMessage?: string;
194
+ recoveredFromEarlier?: boolean;
194
195
  } {
195
196
  const latest = extractLatestAssistantMessageText(messages);
196
197
  if (latest.text || latest.stopReason !== "stop") return latest;
@@ -213,6 +214,7 @@ export function extractRunAssistantMessage(
213
214
  stopReason: typeof rawStopReason === "string" ? rawStopReason : undefined,
214
215
  errorMessage:
215
216
  typeof rawErrorMessage === "string" ? rawErrorMessage : undefined,
217
+ recoveredFromEarlier: true,
216
218
  };
217
219
  }
218
220
  return latest;
package/lib/routing.ts CHANGED
@@ -1560,7 +1560,9 @@ export function createTelegramInboundRouteRuntime<
1560
1560
  await deps.callApi("editForumTopic", {
1561
1561
  chat_id: sourceTarget.chatId,
1562
1562
  message_thread_id: sourceTarget.threadId,
1563
- name: Threads.getTelegramTopicTitleForThreadName(threadName, slot),
1563
+ name:
1564
+ deps.getDisplayTitle?.(sourceTarget) ??
1565
+ Threads.getTelegramTopicTitleForThreadName(threadName, slot),
1564
1566
  });
1565
1567
  assertExecutionCurrent();
1566
1568
  } catch (renameError) {
@@ -1717,7 +1719,9 @@ export function createTelegramInboundRouteRuntime<
1717
1719
  await deps.callApi("editForumTopic", {
1718
1720
  chat_id: sourceTarget.chatId,
1719
1721
  message_thread_id: sourceTarget.threadId,
1720
- name: Threads.getTelegramTopicTitleForThreadName(threadName, slot),
1722
+ name:
1723
+ deps.getDisplayTitle?.(sourceTarget) ??
1724
+ Threads.getTelegramTopicTitleForThreadName(threadName, slot),
1721
1725
  });
1722
1726
  assertExecutionCurrent();
1723
1727
  } catch (renameError) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.48.1",
3
+ "version": "0.48.3",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"