@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/AGENTS.md +3 -3
- package/CHANGELOG.md +15 -0
- package/README.md +3 -3
- package/api/voice.ts +0 -1
- package/docs/architecture.md +4 -4
- package/docs/multi-instance-bus.md +1 -1
- package/docs/outbound.md +2 -3
- package/docs/public-api.md +4 -13
- package/docs/ui-style.md +21 -8
- package/docs/voice.md +9 -37
- package/index.ts +2 -0
- package/lib/bindings.ts +49 -13
- package/lib/commands.ts +103 -26
- package/lib/config.ts +10 -7
- package/lib/journal.ts +2 -9
- package/lib/lifecycle.ts +20 -18
- package/lib/menu-queue.ts +15 -4
- package/lib/menu-settings.ts +15 -11
- package/lib/menu.ts +10 -7
- package/lib/outbound-voice.ts +3 -18
- package/lib/prompts.ts +2 -2
- package/lib/queue.ts +49 -12
- package/lib/status.ts +4 -2
- package/lib/updates.ts +3 -3
- package/lib/voice.ts +7 -31
- package/package.json +1 -1
- package/skills/telegram-bridge/SKILL.md +1 -1
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
|
-
|
|
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 (
|
|
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
|
-
|
|
682
|
+
queuedItemCount > 0,
|
|
681
683
|
processingStatus: getTelegramStatusBarProcessingStatus({
|
|
682
684
|
hasActiveTurn,
|
|
683
685
|
hasPendingDispatch,
|
|
684
686
|
hasPendingModelSwitch,
|
|
685
687
|
activeToolExecutions,
|
|
686
|
-
queuedItems:
|
|
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
|
-
"
|
|
4586
|
+
"Discarded session-owned queue authority from a confirmed-dead process.",
|
|
4587
4587
|
{
|
|
4588
|
-
phase: "dead-queue-owner-
|
|
4588
|
+
phase: "dead-queue-owner-cleanup",
|
|
4589
4589
|
receiptId,
|
|
4590
|
-
|
|
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 = "
|
|
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
|
-
"
|
|
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 `
|
|
228
|
-
* mode is
|
|
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
|
-
|
|
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
|
@@ -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 `
|
|
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
|
|