@llblab/pi-telegram 0.37.2 → 0.39.0

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/queue.ts CHANGED
@@ -427,12 +427,22 @@ export function createTelegramTransportStampedQueueStore<TContext>(
427
427
  };
428
428
  }
429
429
 
430
+ export function isTelegramQueueItemSkipped<TContext = unknown>(
431
+ item: TelegramQueueItem<TContext>,
432
+ ): boolean {
433
+ return item.kind === "prompt" && Boolean(item.reactionSuppressionEmoji);
434
+ }
435
+
436
+ export function countExecutableTelegramQueueItems<TContext = unknown>(
437
+ items: readonly TelegramQueueItem<TContext>[],
438
+ ): number {
439
+ return items.filter((item) => !isTelegramQueueItemSkipped(item)).length;
440
+ }
441
+
430
442
  export function createTelegramQueueItemCountGetter<TContext = unknown>(
431
443
  store: Pick<TelegramQueueStore<TContext>, "getQueuedItems">,
432
444
  ): () => number {
433
- return () => {
434
- return store.getQueuedItems().length;
435
- };
445
+ return () => countExecutableTelegramQueueItems(store.getQueuedItems());
436
446
  }
437
447
 
438
448
  export function createTelegramActiveTurnStore<
@@ -993,7 +1003,8 @@ export function consumeDispatchedTelegramPrompt<TContext = unknown>(
993
1003
  export function formatQueuedTelegramItemsStatus<TContext = unknown>(
994
1004
  items: TelegramQueueItem<TContext>[],
995
1005
  ): string {
996
- return items.length === 0 ? "" : ` +${items.length}`;
1006
+ const count = countExecutableTelegramQueueItems(items);
1007
+ return count === 0 ? "" : ` +${count}`;
997
1008
  }
998
1009
 
999
1010
  export function truncateTelegramQueueSummary(
@@ -2062,6 +2073,7 @@ export interface TelegramSessionStartRuntimeDeps<TContext, TModel = unknown> {
2062
2073
  export interface TelegramSessionShutdownRuntimeDeps<TQueueItem> {
2063
2074
  isSessionActive?: () => boolean;
2064
2075
  unbindDeferredDispatchContext?: () => void;
2076
+ discardQueuedItems?: () => void;
2065
2077
  applyState: (state: TelegramSessionShutdownState<TQueueItem>) => void;
2066
2078
  clearPendingMediaGroups: () => void;
2067
2079
  clearModelMenuState: () => void;
@@ -2090,6 +2102,7 @@ export interface TelegramSessionLifecycleHookRuntimeDeps<
2090
2102
  updateStatus: (ctx: TContext) => void;
2091
2103
  isSessionActive?: (ctx: TContext) => boolean;
2092
2104
  unbindDeferredDispatchContext?: () => void;
2105
+ discardQueuedItems?: (ctx: TContext) => void;
2093
2106
  applySessionShutdownState: (
2094
2107
  state: TelegramSessionShutdownState<TQueueItem>,
2095
2108
  ) => void;
@@ -2286,6 +2299,7 @@ export async function shutdownTelegramSessionRuntime<TQueueItem>(
2286
2299
  deps.unbindDeferredDispatchContext?.();
2287
2300
  await deps.stopPolling();
2288
2301
  if (deps.isSessionActive?.() === false) return;
2302
+ deps.discardQueuedItems?.();
2289
2303
  deps.applyState(buildTelegramSessionShutdownState<TQueueItem>());
2290
2304
  deps.clearPendingMediaGroups();
2291
2305
  deps.clearModelMenuState();
@@ -2339,6 +2353,7 @@ export function createTelegramSessionLifecycleRuntime<
2339
2353
  updateStatus: deps.updateStatus,
2340
2354
  isSessionActive: deps.isSessionActive,
2341
2355
  unbindDeferredDispatchContext: deps.unbindDeferredDispatchContext,
2356
+ discardQueuedItems: deps.discardQueuedItems,
2342
2357
  applySessionShutdownState: stateApplier.applyShutdownState,
2343
2358
  clearPendingMediaGroups: deps.clearPendingMediaGroups,
2344
2359
  clearModelMenuState: deps.clearModelMenuState,
@@ -2387,6 +2402,10 @@ export function createTelegramSessionLifecycleHooks<
2387
2402
  isSessionActive: () =>
2388
2403
  ctx === undefined ? true : (deps.isSessionActive?.(ctx) ?? true),
2389
2404
  unbindDeferredDispatchContext: deps.unbindDeferredDispatchContext,
2405
+ discardQueuedItems:
2406
+ ctx === undefined || !deps.discardQueuedItems
2407
+ ? undefined
2408
+ : () => deps.discardQueuedItems!(ctx),
2390
2409
  applyState: deps.applySessionShutdownState,
2391
2410
  clearPendingMediaGroups: deps.clearPendingMediaGroups,
2392
2411
  clearModelMenuState: deps.clearModelMenuState,
@@ -2476,14 +2495,8 @@ export function clearTelegramQueueItemsRuntime<TContext>(
2476
2495
  const removedItems = deps.getQueuedItems();
2477
2496
  const removedCount = removedItems.length;
2478
2497
  if (removedCount === 0) return 0;
2498
+ deps.onItemsDiscarded?.(removedItems, deps.ctx);
2479
2499
  deps.setQueuedItems([]);
2480
- try {
2481
- deps.onItemsDiscarded?.(removedItems, deps.ctx);
2482
- } catch (error) {
2483
- deps.recordRuntimeEvent?.("queue", error, {
2484
- phase: "discard-receipt-settlement",
2485
- });
2486
- }
2487
2500
  updateTelegramQueueStatusRuntime(deps);
2488
2501
  return removedCount;
2489
2502
  }
@@ -2877,6 +2890,7 @@ export interface TelegramQueueDispatchControllerDeps<
2877
2890
  item: PendingTelegramControlItem<TContext>,
2878
2891
  ctx: TContext,
2879
2892
  ) => void;
2893
+ onPromptSkipped?: (item: PendingTelegramTurn, ctx: TContext) => boolean;
2880
2894
  }
2881
2895
 
2882
2896
  export interface TelegramQueueDispatchController<TContext = unknown> {
@@ -2940,6 +2954,7 @@ export function createTelegramQueueDispatchRuntime<TContext = unknown>(
2940
2954
  deps.hasPendingInboundQueueMutationForItem,
2941
2955
  isQueueItemAdmissionReady: deps.isQueueItemAdmissionReady,
2942
2956
  onControlSettled: deps.onControlSettled,
2957
+ onPromptSkipped: deps.onPromptSkipped,
2943
2958
  recordRuntimeEvent: deps.recordRuntimeEvent,
2944
2959
  });
2945
2960
  }
@@ -3007,7 +3022,29 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
3007
3022
  deps.updateStatus(ctx);
3008
3023
  return;
3009
3024
  }
3025
+ try {
3026
+ if (deps.onPromptSkipped && !deps.onPromptSkipped(candidate, ctx)) {
3027
+ deps.updateStatus(
3028
+ ctx,
3029
+ "Telegram skipped prompt could not be settled durably.",
3030
+ );
3031
+ return;
3032
+ }
3033
+ } catch (error) {
3034
+ deps.recordRuntimeEvent?.("dispatch", error, {
3035
+ phase: "skip-receipt-settlement",
3036
+ });
3037
+ deps.updateStatus(
3038
+ ctx,
3039
+ "Telegram skipped prompt could not be settled durably.",
3040
+ );
3041
+ return;
3042
+ }
3010
3043
  nextActiveIndex += 1;
3044
+ deps.setQueuedItems([
3045
+ ...activeItems.slice(nextActiveIndex),
3046
+ ...protectedInactiveItems,
3047
+ ]);
3011
3048
  }
3012
3049
  }
3013
3050
  const dispatchableItems = activeItems.slice(nextActiveIndex);
@@ -3031,7 +3068,7 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
3031
3068
  dispatchableItems,
3032
3069
  canDispatch,
3033
3070
  );
3034
- if (nextActiveIndex > 0 || dispatchPlan.kind !== "none") {
3071
+ if (dispatchPlan.kind !== "none") {
3035
3072
  deps.setQueuedItems([
3036
3073
  ...dispatchPlan.remainingItems,
3037
3074
  ...protectedInactiveItems,
package/lib/status.ts CHANGED
@@ -344,6 +344,7 @@ export interface TelegramBridgeStatusRuntimeDeps<
344
344
  getActiveToolExecutions: () => number;
345
345
  hasPendingModelSwitch: () => boolean;
346
346
  getQueuedItems: () => TQueueItem[];
347
+ getQueuedItemCount?: (items: TQueueItem[]) => number;
347
348
  formatQueuedStatus: (items: TQueueItem[]) => string;
348
349
  getRecentRuntimeEvents: () => TelegramRuntimeEvent[];
349
350
  getRuntimeLockState?: () => string;
@@ -658,6 +659,7 @@ export function createTelegramBridgeStatusRuntime<
658
659
  getStatusBarState: (_ctx, error) => {
659
660
  const config = deps.getConfig();
660
661
  const queuedItems = deps.getQueuedItems();
662
+ const queuedItemCount = deps.getQueuedItemCount?.(queuedItems) ?? queuedItems.length;
661
663
  const hasActiveTurn = deps.hasActiveTurn();
662
664
  const hasPendingDispatch = deps.hasDispatchPending();
663
665
  const hasPendingModelSwitch = deps.hasPendingModelSwitch();
@@ -677,13 +679,13 @@ export function createTelegramBridgeStatusRuntime<
677
679
  hasPendingDispatch ||
678
680
  hasPendingModelSwitch ||
679
681
  activeToolExecutions > 0 ||
680
- queuedItems.length > 0,
682
+ queuedItemCount > 0,
681
683
  processingStatus: getTelegramStatusBarProcessingStatus({
682
684
  hasActiveTurn,
683
685
  hasPendingDispatch,
684
686
  hasPendingModelSwitch,
685
687
  activeToolExecutions,
686
- queuedItems: queuedItems.length,
688
+ queuedItems: queuedItemCount,
687
689
  }),
688
690
  queuedStatus: deps.formatQueuedStatus(queuedItems),
689
691
  error,
package/lib/updates.ts CHANGED
@@ -4583,11 +4583,11 @@ export function createTelegramUpdateAdmissionLifecycleRuntime<TContext>(
4583
4583
  try {
4584
4584
  deps.recordRuntimeEvent?.(
4585
4585
  "inbound-worker",
4586
- "Recovered queued authority from a confirmed-dead process.",
4586
+ "Discarded session-owned queue authority from a confirmed-dead process.",
4587
4587
  {
4588
- phase: "dead-queue-owner-recovery",
4588
+ phase: "dead-queue-owner-cleanup",
4589
4589
  receiptId,
4590
- recoveredUpdateCount: result.recoveredUpdateIds.length,
4590
+ removedUpdateCount: result.recoveredUpdateIds.length,
4591
4591
  },
4592
4592
  );
4593
4593
  } catch {
package/lib/voice.ts CHANGED
@@ -38,15 +38,9 @@ function getNextAvailableProviderId<T>(
38
38
  return id;
39
39
  }
40
40
 
41
- export type TelegramVoiceReplyMode = "hidden" | "mirror" | "always";
41
+ export type TelegramVoiceReplyMode = "manual" | "mirror" | "always";
42
42
 
43
- export type TelegramVoiceSynthesisProviderResult =
44
- | string
45
- | {
46
- audioPath: string;
47
- transcriptText?: string;
48
- }
49
- | undefined;
43
+ export type TelegramVoiceSynthesisProviderResult = string | undefined;
50
44
 
51
45
  export interface TelegramVoiceTurnView {
52
46
  voiceReplyPreferred?: boolean;
@@ -215,7 +209,7 @@ export function clearTelegramVoiceTranscriptionProviders(): void {
215
209
  // --- Voice Reply Modes ---
216
210
 
217
211
  export const TELEGRAM_VOICE_REPLY_MODES = [
218
- "hidden",
212
+ "manual",
219
213
  "mirror",
220
214
  "always",
221
215
  ] as const;
@@ -224,33 +218,15 @@ export const TELEGRAM_VOICE_REPLY_MODES = [
224
218
  * Returns the active voice reply mode for the current session.
225
219
  *
226
220
  * Pi-telegram owns reply-mode policy through telegram.json. If
227
- * config.voice.replyMode is missing, invalid, or legacy `manual`, the effective
228
- * mode is hidden.
221
+ * config.voice.replyMode is missing, invalid, or legacy `hidden`, the effective
222
+ * mode is manual.
229
223
  */
230
224
  export function getTelegramVoiceReplyMode(config?: {
231
225
  voice?: { replyMode?: string };
232
226
  }): TelegramVoiceReplyMode {
233
227
  const configMode = config?.voice?.replyMode;
234
- if (
235
- configMode &&
236
- (TELEGRAM_VOICE_REPLY_MODES as readonly string[]).includes(configMode)
237
- ) {
238
- return configMode as TelegramVoiceReplyMode;
239
- }
240
- return "hidden";
241
- }
242
-
243
- /**
244
- * Returns whether the user wants the voice synthesis provider's transcript attached
245
- * as a caption on the voice message.
246
- *
247
- * Reads from `config.voice.sendTranscript`.
248
- * Default: false (no transcript text sent at all).
249
- */
250
- export function getTelegramVoiceSendTranscript(config?: {
251
- voice?: { sendTranscript?: boolean };
252
- }): boolean {
253
- return !!config?.voice?.sendTranscript;
228
+ if (configMode === "mirror" || configMode === "always") return configMode;
229
+ return "manual";
254
230
  }
255
231
 
256
232
  // --- Voice Turn Helpers ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.37.2",
3
+ "version": "0.39.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -122,7 +122,7 @@ Prefer no-code command-template configuration in `telegram.json` before adding a
122
122
  - `outboundHandlers` transforms final replies.
123
123
  - Voice transcription handlers can match `type: "voice"` or `mime: "audio/*"`; stdout becomes `[outputs]`.
124
124
 
125
- When asked to configure voice rather than merely operate it, follow the provider-neutral contracts in `docs/voice.md`, `docs/inbound.md`, `docs/outbound.md`, and `docs/command-templates.md` from the pi-telegram package or repository. Inspect the available Skill catalog and trusted local executables for STT, TTS, and media conversion capabilities; check only whether required environment variables exist, never reveal their values. Preserve unrelated `telegram.json` fields, order multiple matching inbound handlers as fallbacks, require OGG/Opus output for native voice delivery, and validate each stage before a live Telegram smoke test. Keep `voice.replyMode` at its existing value unless the user requests a policy change: the default `hidden` mode is fully functional because explicit top-level `telegram_voice` actions still use the configured synthesis pipeline.
125
+ When asked to configure voice rather than merely operate it, follow the provider-neutral contracts in `docs/voice.md`, `docs/inbound.md`, `docs/outbound.md`, and `docs/command-templates.md` from the pi-telegram package or repository. Inspect the available Skill catalog and trusted local executables for STT, TTS, and media conversion capabilities; check only whether required environment variables exist, never reveal their values. Preserve unrelated `telegram.json` fields, order multiple matching inbound handlers as fallbacks, require OGG/Opus output for native voice delivery, and validate each stage before a live Telegram smoke test. Keep `voice.replyMode` at its existing value unless the user requests a policy change: the default `manual` mode is fully functional because explicit top-level `telegram_voice` actions still use the configured synthesis pipeline.
126
126
 
127
127
  When configuration is insufficient, use documented `@llblab/pi-telegram/*` public API subpaths. Never import package-private `lib/*`, start another polling loop, or bypass bridge ownership with raw Bot API access.
128
128