@llblab/pi-telegram 0.23.0 → 0.23.2

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.
@@ -62,11 +62,6 @@ export interface TelegramReservedThreadMessageObservation {
62
62
  leaderEpoch?: number | string;
63
63
  }
64
64
 
65
- export interface ThreadReservationProbeResult {
66
- target: ThreadTarget;
67
- stale: boolean;
68
- }
69
-
70
65
  export interface ReplacedInstanceBindingInput {
71
66
  instanceId: string;
72
67
  replacementTarget: ThreadTarget;
@@ -111,14 +106,6 @@ export type ThreadReconciliationAction =
111
106
  instanceId?: string;
112
107
  leaderEpoch?: number | string;
113
108
  }
114
- | {
115
- kind: "close-delete-pruned-follower-topic";
116
- target: TelegramTarget & { threadId: number };
117
- reason: "pruned-follower";
118
- instanceId?: string;
119
- messageId?: number;
120
- leaderEpoch?: number | string;
121
- }
122
109
  | {
123
110
  kind: "close-delete-replaced-follower-topic";
124
111
  target: TelegramTarget & { threadId: number };
@@ -150,11 +137,6 @@ export type ThreadReconciliationAction =
150
137
  pendingProvisionId: string;
151
138
  instanceId?: string;
152
139
  leaderEpoch?: number | string;
153
- }
154
- | {
155
- kind: "remove-reservation";
156
- target: TelegramTarget & { threadId: number };
157
- reason: "reservation-probe-stale";
158
140
  };
159
141
 
160
142
  export type ThreadReconciliationPhase =
@@ -203,7 +185,6 @@ export interface ThreadReconciliationApplyPorts {
203
185
  lastSyncError?: string,
204
186
  ) => boolean;
205
187
  persist?: () => Promise<void>;
206
- removeReservationByTarget?: (target: ThreadTarget) => boolean;
207
188
  removePendingProvisionById?: (id: string) => boolean;
208
189
  getCurrentLeaderEpoch?: () => number | string | undefined;
209
190
  recordRuntimeEvent?: (
@@ -223,8 +204,6 @@ export interface ThreadReconciliationInput {
223
204
  unboundMessages?: readonly TelegramUnboundThreadMessageObservation[];
224
205
  reservedMessages?: readonly TelegramReservedThreadMessageObservation[];
225
206
  proactiveReservationCleanup?: boolean;
226
- reservationProbeResults?: readonly ThreadReservationProbeResult[];
227
- prunedFollowerInstanceIds?: readonly string[];
228
207
  replacedBindings?: readonly ReplacedInstanceBindingInput[];
229
208
  previousLeaderCleanup?: PreviousLeaderCleanupInput;
230
209
  previousState?: ThreadReconciliationMachineState;
@@ -309,7 +288,6 @@ function isCleanupAction(action: ThreadReconciliationAction): boolean {
309
288
  action.kind === "close-delete-unbound-topic" ||
310
289
  action.kind === "close-delete-reserved-topic" ||
311
290
  action.kind === "close-stale-replaced-topic" ||
312
- action.kind === "close-delete-pruned-follower-topic" ||
313
291
  action.kind === "close-delete-replaced-follower-topic" ||
314
292
  action.kind === "close-delete-previous-leader-topic" ||
315
293
  action.kind === "close-delete-disconnected-instance-topic" ||
@@ -318,11 +296,7 @@ function isCleanupAction(action: ThreadReconciliationAction): boolean {
318
296
  }
319
297
 
320
298
  function isSyncAction(action: ThreadReconciliationAction): boolean {
321
- return (
322
- action.kind === "mark-topic-active" ||
323
- action.kind === "mark-topic-stale" ||
324
- action.kind === "remove-reservation"
325
- );
299
+ return action.kind === "mark-topic-active" || action.kind === "mark-topic-stale";
326
300
  }
327
301
 
328
302
  function createThreadReconciliationMachineState(
@@ -520,11 +494,6 @@ export async function applyThreadReconciliationPlan(
520
494
  const persistFences: ThreadReconciliationAction[] = [];
521
495
  const incompleteActions: ThreadReconciliationAction[] = [];
522
496
  for (const action of plan.actions) {
523
- if (action.kind === "remove-reservation") {
524
- shouldPersist =
525
- ports.removeReservationByTarget?.(action.target) || shouldPersist;
526
- continue;
527
- }
528
497
  if (action.kind === "mark-topic-active") {
529
498
  shouldPersist =
530
499
  ports.markActiveByTarget?.(action.target) || shouldPersist;
@@ -586,7 +555,6 @@ export async function applyThreadReconciliationPlan(
586
555
  if (
587
556
  action.kind === "close-delete-unbound-topic" ||
588
557
  action.kind === "close-delete-reserved-topic" ||
589
- action.kind === "close-delete-pruned-follower-topic" ||
590
558
  action.kind === "close-delete-replaced-follower-topic" ||
591
559
  action.kind === "close-delete-previous-leader-topic" ||
592
560
  action.kind === "close-delete-disconnected-instance-topic" ||
@@ -658,7 +626,6 @@ export async function applyThreadReconciliationPlan(
658
626
  if (shouldSkipForStaleLeaderEpoch(action, ports)) continue;
659
627
  if (
660
628
  action.kind !== "close-delete-previous-leader-topic" &&
661
- action.kind !== "close-delete-pruned-follower-topic" &&
662
629
  action.kind !== "close-delete-replaced-follower-topic" &&
663
630
  action.kind !== "close-delete-disconnected-instance-topic" &&
664
631
  action.kind !== "close-delete-expired-pending-provision-topic"
@@ -674,9 +641,7 @@ export async function applyThreadReconciliationPlan(
674
641
  ? "Unbound Telegram topic deleted"
675
642
  : action.kind === "close-delete-reserved-topic"
676
643
  ? "Reserved Telegram topic deleted"
677
- : action.kind === "close-delete-pruned-follower-topic"
678
- ? "Pruned follower Telegram topic deleted"
679
- : action.kind === "close-delete-replaced-follower-topic"
644
+ : action.kind === "close-delete-replaced-follower-topic"
680
645
  ? "Replaced follower Telegram topic deleted"
681
646
  : action.kind === "close-delete-previous-leader-topic"
682
647
  ? "Previous leader Telegram topic deleted"
@@ -689,9 +654,7 @@ export async function applyThreadReconciliationPlan(
689
654
  ? "thread-reconciler-unbound-topic-delete"
690
655
  : action.kind === "close-delete-reserved-topic"
691
656
  ? "thread-reconciler-reserved-topic-delete"
692
- : action.kind === "close-delete-pruned-follower-topic"
693
- ? "thread-reconciler-pruned-follower-topic-delete"
694
- : action.kind === "close-delete-replaced-follower-topic"
657
+ : action.kind === "close-delete-replaced-follower-topic"
695
658
  ? "thread-reconciler-replaced-follower-topic-delete"
696
659
  : action.kind === "close-delete-previous-leader-topic"
697
660
  ? "thread-reconciler-previous-leader-topic-delete"
@@ -746,11 +709,6 @@ export function planThreadReconciliation(
746
709
  .filter(isCurrentRecord)
747
710
  .map((record) => targetKey(record.target)),
748
711
  );
749
- const allReservationTargets = new Set(
750
- (input.reservations ?? []).map((reservation) =>
751
- targetKey(reservation.target),
752
- ),
753
- );
754
712
  const reservedTargets = new Set(
755
713
  (input.reservations ?? [])
756
714
  .filter((reservation) => isReservationAlive(reservation, input.nowMs))
@@ -826,22 +784,6 @@ export function planThreadReconciliation(
826
784
  }
827
785
  }
828
786
 
829
- for (const prunedInstanceId of input.prunedFollowerInstanceIds ?? []) {
830
- for (const record of input.records) {
831
- if (record.instanceId !== prunedInstanceId) continue;
832
- if (!isActiveOrStartingRecord(record)) continue;
833
- actions.push({
834
- kind: "close-delete-pruned-follower-topic",
835
- target: record.target,
836
- reason: "pruned-follower",
837
- instanceId: prunedInstanceId,
838
- ...(input.currentLeaderEpoch !== undefined
839
- ? { leaderEpoch: input.currentLeaderEpoch }
840
- : {}),
841
- });
842
- }
843
- }
844
-
845
787
  for (const replacement of input.replacedBindings ?? []) {
846
788
  for (const record of input.records) {
847
789
  if (record.instanceId !== replacement.instanceId) continue;
@@ -900,16 +842,6 @@ export function planThreadReconciliation(
900
842
  }
901
843
  }
902
844
 
903
- for (const probe of input.reservationProbeResults ?? []) {
904
- if (!probe.stale) continue;
905
- if (!allReservationTargets.has(targetKey(probe.target))) continue;
906
- actions.push({
907
- kind: "remove-reservation",
908
- target: probe.target,
909
- reason: "reservation-probe-stale",
910
- });
911
- }
912
-
913
845
  for (const message of input.reservedMessages ?? []) {
914
846
  const key = targetKey(message.target);
915
847
  if (!reservedTargets.has(key)) continue;
package/lib/threads.ts CHANGED
@@ -237,7 +237,6 @@ export interface TelegramTopicTargetStore {
237
237
  target: TelegramTarget & { threadId: number },
238
238
  ) => Promise<boolean>;
239
239
  removePendingProvision: (id: string) => boolean;
240
- removeReservationByTarget: (target: TelegramTarget) => boolean;
241
240
  getBotState: () => TelegramBotStateSnapshot;
242
241
  setBotState: (state: Partial<TelegramBotStateSnapshot>) => void;
243
242
  setStatusSnapshot: (snapshot: {
@@ -1396,15 +1395,6 @@ export function createTelegramTopicTargetStore(
1396
1395
  if (changed) markDirty();
1397
1396
  return changed;
1398
1397
  },
1399
- removeReservationByTarget(target) {
1400
- const before = reservations.length;
1401
- reservations = reservations.filter(
1402
- (reservation) => !targetMatches(reservation.target, target),
1403
- );
1404
- const changed = reservations.length !== before;
1405
- if (changed) markDirty();
1406
- return changed;
1407
- },
1408
1398
  getBotState() {
1409
1399
  return Object.fromEntries(
1410
1400
  Object.entries(botState).filter(([, value]) => value !== undefined),
@@ -1983,9 +1973,6 @@ export async function provisionOwnBusTopic(
1983
1973
  syncStatus?: "closed" | "deleted",
1984
1974
  lastSyncError?: string,
1985
1975
  ) => deps.store.markStaleByTarget(target, syncStatus, lastSyncError),
1986
- removeReservationByTarget: (
1987
- target: TelegramTarget & { threadId: number },
1988
- ) => deps.store.removeReservationByTarget(target),
1989
1976
  removePendingProvisionById: (id: string) =>
1990
1977
  deps.store.removePendingProvision(id),
1991
1978
  persist: () => deps.store.persist(),
@@ -2024,36 +2011,6 @@ export async function provisionOwnBusTopic(
2024
2011
  durationMs: Date.now() - reservationCleanupApplyStartedAtMs,
2025
2012
  actions: reservationCleanupPlan.actions.length,
2026
2013
  });
2027
- const reservationProbeResults: ThreadReconciler.ThreadReservationProbeResult[] =
2028
- [];
2029
- deps.recordEvent("bus", "Bus leader reservation probes skipped", {
2030
- phase: "leader-topic-reservation-probe-skipped",
2031
- reservations: reservationsBeforeCleanup.length,
2032
- });
2033
- const reservationProbePlan = ThreadReconciler.planThreadReconciliation({
2034
- nowMs: Date.now(),
2035
- currentLeaderEpoch: deps.getCurrentLeaderEpoch?.(),
2036
- previousState: deps.getThreadReconciliationMachineState?.(),
2037
- records: deps.store.list(),
2038
- reservations: deps.store.listReservations(),
2039
- pendingProvisions: deps.store.listPendingProvisions(),
2040
- reservationProbeResults,
2041
- });
2042
- deps.recordThreadReconciliationPlan?.(reservationProbePlan);
2043
- const reservationProbeApplyStartedAtMs = Date.now();
2044
- await ThreadReconciler.applyThreadReconciliationPlan(
2045
- reservationProbePlan,
2046
- reservationCleanupPorts,
2047
- );
2048
- deps.recordEvent(
2049
- "bus",
2050
- "Bus leader reservation probe reconciliation applied",
2051
- {
2052
- phase: "leader-topic-reservation-probe-apply-duration",
2053
- durationMs: Date.now() - reservationProbeApplyStartedAtMs,
2054
- actions: reservationProbePlan.actions.length,
2055
- },
2056
- );
2057
2014
  const nowMs = Date.now();
2058
2015
  const currentLeaderOwner: TelegramThreadOwner = {
2059
2016
  kind: "leader",
package/lib/turns.ts CHANGED
@@ -13,9 +13,9 @@ import {
13
13
  collectTelegramMessageIds,
14
14
  downloadTelegramMessageFiles,
15
15
  extractTelegramForwardContextText,
16
- extractTelegramMessageText,
17
16
  extractTelegramMessagesPromptText,
18
17
  extractTelegramMessagesText,
18
+ extractTelegramMessageText,
19
19
  formatTelegramHistoryText,
20
20
  guessMediaType,
21
21
  type DownloadedTelegramMessageFile,
@@ -59,14 +59,19 @@ export interface TelegramTurnMessage {
59
59
 
60
60
  export type DownloadedTelegramTurnFile = DownloadedTelegramMessageFile;
61
61
 
62
- function getTelegramTurnTarget(message: TelegramTurnMessage): TelegramTurnTarget {
62
+ function getTelegramTurnTarget(
63
+ message: TelegramTurnMessage,
64
+ ): TelegramTurnTarget {
63
65
  return Number.isInteger(message.message_thread_id)
64
66
  ? { chatId: message.chat.id, threadId: message.message_thread_id }
65
67
  : { chatId: message.chat.id };
66
68
  }
67
69
 
68
70
  function formatTelegramPrefixAttributeValue(value: string): string {
69
- return value.replace(/[\]\n\r|]+/g, " ").replace(/\s+/g, " ").trim();
71
+ return value
72
+ .replace(/[\]\n\r|]+/g, " ")
73
+ .replace(/\s+/g, " ")
74
+ .trim();
70
75
  }
71
76
 
72
77
  export function createTelegramTurnPrefix(
@@ -142,7 +147,6 @@ function appendTelegramSourceContext(
142
147
  return text ? `${text}\n\n${sourceContext}` : sourceContext;
143
148
  }
144
149
 
145
-
146
150
  function buildTelegramForwardContextBlock(options: {
147
151
  context: string;
148
152
  text: string;
@@ -411,7 +415,6 @@ export interface BuildTelegramPromptTurnOptions {
411
415
  readBinaryFile: (path: string) => Promise<Uint8Array>;
412
416
  inferImageMimeType: (path: string) => string | undefined;
413
417
  voiceReplyMode?: TelegramVoiceReplyMode;
414
- voiceReplyModeConfigured?: boolean;
415
418
  voicePromptContribution?: string;
416
419
  }
417
420
 
@@ -435,9 +438,11 @@ export interface TelegramPromptTurnRuntimeBuilderDeps<
435
438
  }>;
436
439
  resolveTimeLine?: (chatId: number) => string | null;
437
440
  getVoiceReplyMode?: () => TelegramVoiceReplyMode;
438
- isVoiceReplyModeConfigured?: () => boolean;
439
441
  /** Returns the visible thread label for a message target, used to add thread context to the prompt prefix. */
440
- getTelegramThreadLabel?: (message: { chat: { id: number }; message_thread_id?: number }) => string | undefined;
442
+ getTelegramThreadLabel?: (message: {
443
+ chat: { id: number };
444
+ message_thread_id?: number;
445
+ }) => string | undefined;
441
446
  getAllowedUserId?: () => number | undefined;
442
447
  }
443
448
 
@@ -475,9 +480,7 @@ export function createTelegramPromptTurnRuntimeBuilder<
475
480
  text: extractTelegramMessageText(message),
476
481
  message,
477
482
  fileNames: new Set(
478
- collectTelegramFileInfos(message.rich_message ? [message] : []).map(
479
- (file) => file.fileName,
480
- ),
483
+ collectTelegramFileInfos([message]).map((file) => file.fileName),
481
484
  ),
482
485
  },
483
486
  ]
@@ -567,7 +570,6 @@ export function createTelegramPromptTurnRuntimeBuilder<
567
570
  timeLine,
568
571
  inferImageMimeType: guessMediaType,
569
572
  voiceReplyMode,
570
- voiceReplyModeConfigured: deps.isVoiceReplyModeConfigured?.(),
571
573
  voicePromptContribution: computeVoicePromptContribution(
572
574
  voiceReplyMode,
573
575
  files,
@@ -581,9 +583,13 @@ function getTelegramVoicePromptContext(
581
583
  voiceReplyMode: TelegramVoiceReplyMode,
582
584
  hasVoiceFile: boolean,
583
585
  ): Record<string, string> | undefined {
584
- if (voiceReplyMode === "always") return { "reply mode": "always" };
585
- if (!hasVoiceFile) return undefined;
586
- return { "reply mode": voiceReplyMode };
586
+ if (
587
+ voiceReplyMode !== "always" &&
588
+ !(voiceReplyMode === "mirror" && hasVoiceFile)
589
+ ) {
590
+ return undefined;
591
+ }
592
+ return { delivery: "automatic voice" };
587
593
  }
588
594
 
589
595
  export async function buildTelegramPromptTurn(
@@ -597,8 +603,6 @@ export async function buildTelegramPromptTurn(
597
603
  (f) => f.kind === "voice" || f.kind === "audio",
598
604
  );
599
605
  const voiceReplyMode = options.voiceReplyMode ?? getTelegramVoiceReplyMode();
600
- const showVoiceContext =
601
- options.voiceReplyModeConfigured ?? options.voiceReplyMode !== undefined;
602
606
  const content: TelegramPromptContent[] = [
603
607
  {
604
608
  type: "text",
@@ -615,9 +619,10 @@ export async function buildTelegramPromptTurn(
615
619
  sourceContext: options.sourceContext,
616
620
  historyTurns: options.historyTurns,
617
621
  timeLine: options.timeLine,
618
- voiceContext: showVoiceContext
619
- ? getTelegramVoicePromptContext(voiceReplyMode, hasVoiceFile)
620
- : undefined,
622
+ voiceContext: getTelegramVoicePromptContext(
623
+ voiceReplyMode,
624
+ hasVoiceFile,
625
+ ),
621
626
  }),
622
627
  },
623
628
  ];
@@ -634,8 +639,7 @@ export async function buildTelegramPromptTurn(
634
639
  }
635
640
  if (options.voicePromptContribution?.trim()) {
636
641
  const textItem = content.find((c) => c.type === "text") as
637
- | { type: "text"; text: string }
638
- | undefined;
642
+ { type: "text"; text: string } | undefined;
639
643
  if (textItem) {
640
644
  textItem.text = `${textItem.text}\n\n${options.voicePromptContribution.trim()}`;
641
645
  }
package/lib/voice.ts CHANGED
@@ -38,7 +38,7 @@ function getNextAvailableProviderId<T>(
38
38
  return id;
39
39
  }
40
40
 
41
- export type TelegramVoiceReplyMode = "mirror" | "always" | "manual";
41
+ export type TelegramVoiceReplyMode = "hidden" | "mirror" | "always";
42
42
 
43
43
  export type TelegramVoiceSynthesisProviderResult =
44
44
  | string
@@ -67,9 +67,7 @@ export interface TelegramVoiceSynthesisProvider {
67
67
  }
68
68
 
69
69
  export type TelegramVoiceTranscriptionProviderResult =
70
- | string
71
- | { text: string; language?: string }
72
- | undefined;
70
+ string | { text: string; language?: string } | undefined;
73
71
 
74
72
  export interface TelegramVoiceTranscriptionFile {
75
73
  path: string;
@@ -217,16 +215,17 @@ export function clearTelegramVoiceTranscriptionProviders(): void {
217
215
  // --- Voice Reply Modes ---
218
216
 
219
217
  export const TELEGRAM_VOICE_REPLY_MODES = [
218
+ "hidden",
220
219
  "mirror",
221
220
  "always",
222
- "manual",
223
221
  ] as const;
224
222
 
225
223
  /**
226
224
  * Returns the active voice reply mode for the current session.
227
225
  *
228
226
  * Pi-telegram owns reply-mode policy through telegram.json. If
229
- * config.voice.replyMode is missing or invalid, the safe default is manual.
227
+ * config.voice.replyMode is missing, invalid, or legacy `manual`, the effective
228
+ * mode is hidden.
230
229
  */
231
230
  export function getTelegramVoiceReplyMode(config?: {
232
231
  voice?: { replyMode?: string };
@@ -238,7 +237,7 @@ export function getTelegramVoiceReplyMode(config?: {
238
237
  ) {
239
238
  return configMode as TelegramVoiceReplyMode;
240
239
  }
241
- return "manual";
240
+ return "hidden";
242
241
  }
243
242
 
244
243
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.23.0",
3
+ "version": "0.23.2",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -27,7 +27,8 @@
27
27
  "node": ">=22.19.0"
28
28
  },
29
29
  "scripts": {
30
- "test": "node --experimental-strip-types --test tests/*.test.ts",
30
+ "test": "node --experimental-strip-types --test --test-reporter=dot tests/*.test.ts",
31
+ "test:verbose": "node --experimental-strip-types --test --test-reporter=spec tests/*.test.ts",
31
32
  "typecheck": "tsc --noEmit",
32
33
  "audit": "npm audit",
33
34
  "pack:check": "npm pack --dry-run",