@llblab/pi-kit 0.1.7 → 0.1.9

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.
Files changed (26) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +1 -1
  3. package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -0
  4. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +17 -0
  5. package/node_modules/@llblab/pi-telegram/README.md +3 -3
  6. package/node_modules/@llblab/pi-telegram/api/voice.ts +0 -1
  7. package/node_modules/@llblab/pi-telegram/docs/architecture.md +5 -5
  8. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +2 -2
  9. package/node_modules/@llblab/pi-telegram/docs/outbound.md +2 -3
  10. package/node_modules/@llblab/pi-telegram/docs/public-api.md +5 -15
  11. package/node_modules/@llblab/pi-telegram/docs/voice.md +9 -37
  12. package/node_modules/@llblab/pi-telegram/index.ts +30 -7
  13. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +112 -25
  14. package/node_modules/@llblab/pi-telegram/lib/config.ts +38 -46
  15. package/node_modules/@llblab/pi-telegram/lib/journal.ts +107 -2
  16. package/node_modules/@llblab/pi-telegram/lib/menu-model.ts +4 -2
  17. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +15 -11
  18. package/node_modules/@llblab/pi-telegram/lib/outbound-voice.ts +3 -18
  19. package/node_modules/@llblab/pi-telegram/lib/polling.ts +66 -24
  20. package/node_modules/@llblab/pi-telegram/lib/setup.ts +0 -1
  21. package/node_modules/@llblab/pi-telegram/lib/status.ts +2 -2
  22. package/node_modules/@llblab/pi-telegram/lib/updates.ts +11 -4
  23. package/node_modules/@llblab/pi-telegram/lib/voice.ts +7 -31
  24. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  25. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/SKILL.md +1 -1
  26. package/package.json +2 -2
@@ -47,6 +47,7 @@ import {
47
47
  export const TELEGRAM_BUS_FOLLOWER_PROMOTION_GRACE_MS = 2_500;
48
48
  export const TELEGRAM_FOLLOWER_SESSION_HANDOFF_TTL_MS = 30_000;
49
49
  export const TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS = 30_000;
50
+ export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_WAIT_MS = 30_000;
50
51
  export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_ATTEMPTS =
51
52
  TELEGRAM_BUS_REGISTRATION_RETRY.attempts;
52
53
  export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_DELAY_MS =
@@ -167,6 +168,9 @@ export interface TelegramBusFollowerRegistrationState {
167
168
  getSlot: () => string | undefined;
168
169
  getThreadName: () => string | undefined;
169
170
  getGeneration: () => string | undefined;
171
+ beginRecovery: () => number;
172
+ cancelRecovery: () => void;
173
+ waitForGeneration: (timeoutMs?: number) => Promise<string | undefined>;
170
174
  getLeaderProtocol: () => TelegramBusProtocolIdentity | undefined;
171
175
  getEligibleElectionSlots: () => readonly string[];
172
176
  setEligibleElectionSlots: (slots: readonly string[]) => void;
@@ -214,6 +218,9 @@ export interface TelegramBusFollowerClientRuntimeDeps<TMessage = unknown> {
214
218
  getApiAuthSecret?: () => string | undefined;
215
219
  getForwardingAuthSecret?: () => string | undefined;
216
220
  getRegistrationGeneration: () => string | undefined;
221
+ waitForRegistrationGeneration?: (
222
+ timeoutMs?: number,
223
+ ) => Promise<string | undefined>;
217
224
  getForwardCommentBatchPosition?: (
218
225
  message: TMessage,
219
226
  ) => "comment" | "forward" | undefined;
@@ -231,6 +238,9 @@ export interface TelegramBusFollowerApiCallerDeps {
231
238
  createRequestId: () => string;
232
239
  getAuthSecret?: () => string | undefined;
233
240
  getRegistrationGeneration: () => string | undefined;
241
+ waitForRegistrationGeneration?: (
242
+ timeoutMs?: number,
243
+ ) => Promise<string | undefined>;
234
244
  getNowMs?: () => number;
235
245
  timeoutMs?: number;
236
246
  }
@@ -417,6 +427,7 @@ export interface TelegramBusFollowerHeartbeatRecoveryHandlerDeps<TContext> {
417
427
  | "getSlot"
418
428
  | "getThreadName"
419
429
  | "getEligibleElectionSlots"
430
+ | "beginRecovery"
420
431
  | "setRegistered"
421
432
  >;
422
433
  getRegistrationRuntime: () => TelegramBusFollowerRegistrationRuntime<TContext>;
@@ -638,6 +649,7 @@ export function createTelegramBusFollowerClientRuntime<
638
649
  socketPath: deps.socketPath,
639
650
  createRequestId,
640
651
  timeoutMs,
652
+ waitForRegistrationGeneration: deps.waitForRegistrationGeneration,
641
653
  };
642
654
  return {
643
655
  createRequestId,
@@ -695,14 +707,14 @@ export function createTelegramBusFollowerQueueHandoffClient(
695
707
  const timeoutMs =
696
708
  deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS;
697
709
  return async (input) => {
698
- const registrationGeneration = deps.getRegistrationGeneration();
699
- if (!registrationGeneration) {
700
- throw new Error("Telegram bus follower is not registered.");
701
- }
710
+ const registration = await resolveTelegramBusFollowerRegistration(
711
+ deps,
712
+ timeoutMs,
713
+ );
702
714
  const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
703
715
  const response = await sendTelegramBusLocalEnvelope({
704
716
  socketPath,
705
- timeoutMs,
717
+ timeoutMs: registration.remainingTimeoutMs,
706
718
  retry: getTelegramBusTransportRetryPolicy({
707
719
  endpoint: socketPath,
708
720
  operation: "operation",
@@ -712,7 +724,7 @@ export function createTelegramBusFollowerQueueHandoffClient(
712
724
  requestId: deps.createRequestId(),
713
725
  auth: deps.getAuthSecret?.(),
714
726
  instanceId: deps.instanceId,
715
- registrationGeneration,
727
+ registrationGeneration: registration.generation,
716
728
  ...input,
717
729
  sentAtMs: getNowMs(),
718
730
  },
@@ -770,11 +782,12 @@ export function createTelegramBusAgentMessageClient(
770
782
  envelope:
771
783
  | Extract<TelegramBusEnvelope, { kind: "follower.resolveAgentTarget" }>
772
784
  | Extract<TelegramBusEnvelope, { kind: "follower.routeAgentMessage" }>,
785
+ requestTimeoutMs = timeoutMs,
773
786
  ): Promise<unknown> => {
774
787
  const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
775
788
  const response = await sendTelegramBusLocalEnvelope({
776
789
  socketPath,
777
- timeoutMs,
790
+ timeoutMs: requestTimeoutMs,
778
791
  retry: getTelegramBusTransportRetryPolicy({
779
792
  endpoint: socketPath,
780
793
  operation: "operation",
@@ -788,26 +801,30 @@ export function createTelegramBusAgentMessageClient(
788
801
  : "Telegram bus agent message did not return an acknowledgement.",
789
802
  );
790
803
  };
791
- const registrationFields = () => {
792
- const registrationGeneration = deps.getRegistrationGeneration();
793
- if (!registrationGeneration) {
794
- throw new Error("Telegram bus follower is not registered.");
795
- }
804
+ const registrationFields = async () => {
805
+ const registration = await resolveTelegramBusFollowerRegistration(
806
+ deps,
807
+ timeoutMs,
808
+ );
796
809
  return {
797
- auth: deps.getAuthSecret?.(),
798
- instanceId: deps.instanceId,
799
- registrationGeneration,
810
+ fields: {
811
+ auth: deps.getAuthSecret?.(),
812
+ instanceId: deps.instanceId,
813
+ registrationGeneration: registration.generation,
814
+ },
815
+ remainingTimeoutMs: registration.remainingTimeoutMs,
800
816
  };
801
817
  };
802
818
  return {
803
819
  async resolveTarget(selector) {
820
+ const registration = await registrationFields();
804
821
  const result = await request({
805
822
  kind: "follower.resolveAgentTarget",
806
823
  requestId: deps.createRequestId(),
807
- ...registrationFields(),
824
+ ...registration.fields,
808
825
  selector,
809
826
  sentAtMs: getNowMs(),
810
- });
827
+ }, registration.remainingTimeoutMs);
811
828
  if (!result || typeof result !== "object" || Array.isArray(result)) {
812
829
  throw new Error("Telegram bus returned an invalid agent target.");
813
830
  }
@@ -821,13 +838,14 @@ export function createTelegramBusAgentMessageClient(
821
838
  return { chatId: target.chatId, threadId: target.threadId };
822
839
  },
823
840
  async routeMessage(message) {
841
+ const registration = await registrationFields();
824
842
  await request({
825
843
  kind: "follower.routeAgentMessage",
826
844
  requestId: deps.createRequestId(),
827
- ...registrationFields(),
845
+ ...registration.fields,
828
846
  message,
829
847
  sentAtMs: getNowMs(),
830
- });
848
+ }, registration.remainingTimeoutMs);
831
849
  },
832
850
  };
833
851
  }
@@ -839,16 +857,16 @@ export function createTelegramBusFollowerApiCaller(
839
857
  const timeoutMs =
840
858
  deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS;
841
859
  return async (method, args) => {
860
+ const registration = await resolveTelegramBusFollowerRegistration(
861
+ deps,
862
+ timeoutMs,
863
+ );
842
864
  const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
843
- const registrationGeneration = deps.getRegistrationGeneration();
844
- if (!registrationGeneration) {
845
- throw new Error("Telegram bus follower is not registered.");
846
- }
847
865
  let response: TelegramBusEnvelope | undefined;
848
866
  try {
849
867
  response = await sendTelegramBusLocalEnvelope({
850
868
  socketPath,
851
- timeoutMs,
869
+ timeoutMs: registration.remainingTimeoutMs,
852
870
  retry: getTelegramBusTransportRetryPolicy({
853
871
  endpoint: socketPath,
854
872
  operation: "operation",
@@ -858,7 +876,7 @@ export function createTelegramBusFollowerApiCaller(
858
876
  requestId: deps.createRequestId(),
859
877
  auth: deps.getAuthSecret?.(),
860
878
  instanceId: deps.instanceId,
861
- registrationGeneration,
879
+ registrationGeneration: registration.generation,
862
880
  method,
863
881
  args,
864
882
  sentAtMs: getNowMs(),
@@ -893,6 +911,29 @@ export function createTelegramBusFollowerApiCaller(
893
911
  };
894
912
  }
895
913
 
914
+ async function resolveTelegramBusFollowerRegistration(
915
+ deps: Pick<
916
+ TelegramBusFollowerApiCallerDeps,
917
+ | "getRegistrationGeneration"
918
+ | "waitForRegistrationGeneration"
919
+ | "getNowMs"
920
+ >,
921
+ timeoutMs: number,
922
+ ): Promise<{ generation: string; remainingTimeoutMs: number }> {
923
+ const current = deps.getRegistrationGeneration();
924
+ if (current) return { generation: current, remainingTimeoutMs: timeoutMs };
925
+ const getNowMs = deps.getNowMs ?? Date.now;
926
+ const startedAtMs = getNowMs();
927
+ const restored = await deps.waitForRegistrationGeneration?.(
928
+ timeoutMs,
929
+ );
930
+ const remainingTimeoutMs = Math.max(0, timeoutMs - (getNowMs() - startedAtMs));
931
+ if (restored && remainingTimeoutMs > 0) {
932
+ return { generation: restored, remainingTimeoutMs };
933
+ }
934
+ throw new Error("Telegram bus follower is not registered.");
935
+ }
936
+
896
937
  function isTelegramStaleContextError(error: unknown): boolean {
897
938
  return (
898
939
  error instanceof Error &&
@@ -1046,12 +1087,52 @@ export function createTelegramBusFollowerRegistrationState(
1046
1087
  let generation: string | undefined;
1047
1088
  let leaderProtocol: TelegramBusProtocolIdentity | undefined;
1048
1089
  let eligibleElectionSlots: string[] = [];
1090
+ let recoveryEpoch = 0;
1091
+ let activeRecoveryEpoch: number | undefined;
1092
+ const generationWaiters = new Set<{
1093
+ epoch: number;
1094
+ settle: (value: string | undefined) => void;
1095
+ }>();
1096
+ const settleGenerationWaiters = (
1097
+ value: string | undefined,
1098
+ epoch?: number,
1099
+ ) => {
1100
+ for (const waiter of [...generationWaiters]) {
1101
+ if (epoch === undefined || waiter.epoch === epoch) waiter.settle(value);
1102
+ }
1103
+ };
1049
1104
  return {
1050
1105
  isRegistered: () => registered,
1051
1106
  getTarget: () => (target ? { ...target } : undefined),
1052
1107
  getSlot: () => slot,
1053
1108
  getThreadName: () => threadName,
1054
1109
  getGeneration: () => generation,
1110
+ beginRecovery: () => {
1111
+ if (activeRecoveryEpoch !== undefined) return activeRecoveryEpoch;
1112
+ activeRecoveryEpoch = ++recoveryEpoch;
1113
+ return activeRecoveryEpoch;
1114
+ },
1115
+ cancelRecovery: () => {
1116
+ const epoch = activeRecoveryEpoch;
1117
+ activeRecoveryEpoch = undefined;
1118
+ if (epoch !== undefined) settleGenerationWaiters(undefined, epoch);
1119
+ },
1120
+ waitForGeneration: (timeoutMs = TELEGRAM_BUS_FOLLOWER_REGISTRATION_WAIT_MS) => {
1121
+ if (generation) return Promise.resolve(generation);
1122
+ const epoch = activeRecoveryEpoch;
1123
+ if (epoch === undefined) return Promise.resolve(undefined);
1124
+ return new Promise((resolve) => {
1125
+ let timer: NodeJS.Timeout | undefined;
1126
+ const settle = (value: string | undefined) => {
1127
+ generationWaiters.delete(waiter);
1128
+ if (timer) clearTimeout(timer);
1129
+ resolve(value);
1130
+ };
1131
+ const waiter = { epoch, settle };
1132
+ generationWaiters.add(waiter);
1133
+ timer = setTimeout(() => settle(undefined), Math.max(0, timeoutMs));
1134
+ });
1135
+ },
1055
1136
  getLeaderProtocol: () =>
1056
1137
  leaderProtocol
1057
1138
  ? { ...leaderProtocol, capabilities: [...leaderProtocol.capabilities] }
@@ -1077,6 +1158,10 @@ export function createTelegramBusFollowerRegistrationState(
1077
1158
  }
1078
1159
  : undefined;
1079
1160
  if (availabilityChanged) options.onAvailabilityChanged?.();
1161
+ if (generation) {
1162
+ activeRecoveryEpoch = undefined;
1163
+ settleGenerationWaiters(generation);
1164
+ }
1080
1165
  },
1081
1166
  };
1082
1167
  }
@@ -1253,6 +1338,7 @@ export function createTelegramBusFollowerHeartbeatRecoveryHandler<TContext>(
1253
1338
  ): Promise<void> => {
1254
1339
  if (promotionPending) return;
1255
1340
  promotionPending = true;
1341
+ deps.registrationState.beginRecovery();
1256
1342
  try {
1257
1343
  const initialBinding = carriedBinding ?? snapshotBinding();
1258
1344
  const state = deps.getLeaderState();
@@ -1366,6 +1452,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
1366
1452
  heartbeatPromise = undefined;
1367
1453
  heartbeatPromiseGeneration = undefined;
1368
1454
  deps.setActiveAuthSecret?.(undefined);
1455
+ deps.registrationState?.cancelRecovery();
1369
1456
  deps.registrationState?.setRegistered(false);
1370
1457
  lastKnownTarget = undefined;
1371
1458
  lastKnownSlot = undefined;
@@ -99,8 +99,6 @@ export interface TelegramConfig {
99
99
  botId?: number;
100
100
  /** @deprecated persisted identity belongs in profiles.default; retained for effective/legacy views */
101
101
  allowedUserId?: number;
102
- /** @deprecated persisted identity belongs in profiles.default; retained for effective/legacy views */
103
- lastUpdateId?: number;
104
102
  inboundHandlers?: TelegramInboundHandlerConfig[];
105
103
  attachmentHandlers?: TelegramInboundHandlerConfig[];
106
104
  outboundHandlers?: TelegramOutboundHandlerConfig[];
@@ -120,9 +118,8 @@ export interface TelegramConfig {
120
118
  /** @deprecated use assistant.rendering */
121
119
  assistantRendering?: TelegramAssistantRenderingMode;
122
120
  voice?: {
123
- replyMode?: "hidden" | "mirror" | "always";
124
- /** Whether to attach the provider's transcriptText as caption on voice messages */
125
- sendTranscript?: boolean;
121
+ /** `hidden` is a read-only compatibility alias for the former manual mode. */
122
+ replyMode?: "manual" | "hidden" | "mirror" | "always";
126
123
  };
127
124
  time?: TelegramTimeConfig;
128
125
  threads?: {
@@ -144,6 +141,9 @@ export interface TelegramBotProfile {
144
141
  botUsername?: string;
145
142
  botId?: number;
146
143
  allowedUserId?: number;
144
+ }
145
+
146
+ interface TelegramLegacyCursorCarrier {
147
147
  lastUpdateId?: number;
148
148
  }
149
149
 
@@ -177,6 +177,8 @@ export interface TelegramConfigStore {
177
177
  getBotToken: () => string | undefined;
178
178
  hasBotToken: () => boolean;
179
179
  getAllowedUserId: () => number | undefined;
180
+ getLegacyPollingCursor: () => number | undefined;
181
+ removeLegacyPollingCursor: () => void;
180
182
  getInboundHandlers: () => TelegramInboundHandlerConfig[] | undefined;
181
183
  getAttachmentHandlers: () => TelegramInboundHandlerConfig[] | undefined;
182
184
  getOutboundHandlers: () => TelegramOutboundHandlerConfig[] | undefined;
@@ -368,15 +370,7 @@ function mergeTelegramConfigDelta(
368
370
  continue;
369
371
  }
370
372
  if (!desiredHas) {
371
- if (key !== "lastUpdateId") delete merged[key];
372
- continue;
373
- }
374
- if (
375
- key === "lastUpdateId" &&
376
- typeof desiredValue === "number" &&
377
- typeof merged[key] === "number"
378
- ) {
379
- merged[key] = Math.max(desiredValue, merged[key] as number);
373
+ delete merged[key];
380
374
  continue;
381
375
  }
382
376
  if (
@@ -433,6 +427,8 @@ export function getTelegramProfileFields(
433
427
  ): TelegramBotProfile | undefined {
434
428
  const token = config.botToken?.trim();
435
429
  if (!token) return undefined;
430
+ const legacyCursor = (config as TelegramConfig & TelegramLegacyCursorCarrier)
431
+ .lastUpdateId;
436
432
  return {
437
433
  botToken: token,
438
434
  ...(config.botUsername !== undefined
@@ -442,9 +438,7 @@ export function getTelegramProfileFields(
442
438
  ...(config.allowedUserId !== undefined
443
439
  ? { allowedUserId: config.allowedUserId }
444
440
  : {}),
445
- ...(config.lastUpdateId !== undefined
446
- ? { lastUpdateId: config.lastUpdateId }
447
- : {}),
441
+ ...(legacyCursor !== undefined ? { lastUpdateId: legacyCursor } : {}),
448
442
  };
449
443
  }
450
444
 
@@ -456,7 +450,7 @@ function omitTelegramRootProfileFields(config: TelegramConfig): TelegramConfig {
456
450
  allowedUserId: _allowedUserId,
457
451
  lastUpdateId: _lastUpdateId,
458
452
  ...sharedConfig
459
- } = config;
453
+ } = config as TelegramConfig & TelegramLegacyCursorCarrier;
460
454
  return sharedConfig;
461
455
  }
462
456
 
@@ -477,7 +471,8 @@ export function normalizeTelegramDefaultProfileConfig(config: TelegramConfig): {
477
471
  if (Object.hasOwn(config, "botToken") && !legacyToken) {
478
472
  throw new Error("Legacy Telegram default profile has no bot token");
479
473
  }
480
- const legacyProfile: Partial<TelegramBotProfile> = {
474
+ const legacyProfile: Partial<TelegramBotProfile> &
475
+ TelegramLegacyCursorCarrier = {
481
476
  ...(legacyToken ? { botToken: legacyToken } : {}),
482
477
  ...(config.botUsername !== undefined
483
478
  ? { botUsername: config.botUsername }
@@ -486,8 +481,12 @@ export function normalizeTelegramDefaultProfileConfig(config: TelegramConfig): {
486
481
  ...(config.allowedUserId !== undefined
487
482
  ? { allowedUserId: config.allowedUserId }
488
483
  : {}),
489
- ...(config.lastUpdateId !== undefined
490
- ? { lastUpdateId: config.lastUpdateId }
484
+ ...((config as TelegramConfig & TelegramLegacyCursorCarrier)
485
+ .lastUpdateId !== undefined
486
+ ? {
487
+ lastUpdateId: (config as TelegramConfig & TelegramLegacyCursorCarrier)
488
+ .lastUpdateId,
489
+ }
491
490
  : {}),
492
491
  };
493
492
  if (!canonicalProfile && !legacyToken) {
@@ -609,6 +608,16 @@ export function createTelegramConfigStore(
609
608
  getBotToken: () => getEffectiveConfig().botToken,
610
609
  hasBotToken: () => !!getEffectiveConfig().botToken,
611
610
  getAllowedUserId: () => getEffectiveConfig().allowedUserId,
611
+ getLegacyPollingCursor: () =>
612
+ (getEffectiveConfig() as TelegramConfig & TelegramLegacyCursorCarrier)
613
+ .lastUpdateId,
614
+ removeLegacyPollingCursor: () => {
615
+ const next = {
616
+ ...(getEffectiveConfig() as TelegramConfig & TelegramLegacyCursorCarrier),
617
+ };
618
+ delete next.lastUpdateId;
619
+ setEffectiveConfig(next);
620
+ },
612
621
  getInboundHandlers: () => [
613
622
  ...(config.inboundHandlers ?? []),
614
623
  ...(config.attachmentHandlers ?? []),
@@ -703,27 +712,6 @@ export function createTelegramConfigStore(
703
712
  };
704
713
  }
705
714
 
706
- export function createTelegramPollingOffsetPersister(
707
- configStore: Pick<TelegramConfigStore, "get" | "set" | "persist">,
708
- persist: () => Promise<void> = () => configStore.persist(),
709
- ): (pollingConfig: { lastUpdateId?: number }) => Promise<void> {
710
- return async (pollingConfig) => {
711
- const nextOffset = pollingConfig.lastUpdateId;
712
- if (typeof nextOffset === "number") {
713
- const current = configStore.get();
714
- const currentOffset = current.lastUpdateId;
715
- configStore.set({
716
- ...current,
717
- lastUpdateId:
718
- typeof currentOffset === "number"
719
- ? Math.max(currentOffset, nextOffset)
720
- : nextOffset,
721
- });
722
- }
723
- await persist();
724
- };
725
- }
726
-
727
715
  export function createTelegramProactivePushChecker(
728
716
  configStore: Pick<TelegramConfigStore, "get">,
729
717
  ): () => boolean {
@@ -856,10 +844,10 @@ export function createTelegramActivityVerbositySetter(
856
844
 
857
845
  export function createTelegramVoiceReplyModeGetter(
858
846
  configStore: Pick<TelegramConfigStore, "get">,
859
- ): () => "hidden" | "mirror" | "always" {
847
+ ): () => "manual" | "mirror" | "always" {
860
848
  return () => {
861
849
  const mode = configStore.get().voice?.replyMode;
862
- return mode === "mirror" || mode === "always" ? mode : "hidden";
850
+ return mode === "mirror" || mode === "always" ? mode : "manual";
863
851
  };
864
852
  }
865
853
 
@@ -874,11 +862,15 @@ export function createTelegramVoiceReplyModeConfiguredChecker(
874
862
 
875
863
  export function createTelegramVoiceReplyModeSetter(
876
864
  configStore: TelegramMutableConfigStore,
877
- ): (replyMode: "hidden" | "mirror" | "always" | undefined) => Promise<void> {
865
+ ): (replyMode: "manual" | "hidden" | "mirror" | "always" | undefined) => Promise<void> {
878
866
  return async (replyMode) => {
879
867
  await loadLatestTelegramConfig(configStore);
880
868
  const current = configStore.get();
881
- if (replyMode === undefined || replyMode === "hidden") {
869
+ if (
870
+ replyMode === undefined ||
871
+ replyMode === "manual" ||
872
+ replyMode === "hidden"
873
+ ) {
882
874
  const { replyMode: _replyMode, ...remainingVoice } = current.voice ?? {};
883
875
  const next = { ...current };
884
876
  if (Object.keys(remainingVoice).length > 0) next.voice = remainingVoice;