@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.
- package/AGENTS.md +19 -12
- package/BACKLOG.md +1 -1
- package/CHANGELOG.md +42 -59
- package/README.md +3 -3
- package/docs/architecture.md +7 -8
- package/docs/multi-instance-bus.md +1 -1
- package/docs/outbound.md +1 -1
- package/docs/public-api.md +2 -2
- package/docs/ui-style.md +1 -1
- package/docs/voice.md +8 -9
- package/index.ts +8 -7
- package/lib/bindings.ts +15 -16
- package/lib/bus-leader.ts +11 -128
- package/lib/config.ts +32 -14
- package/lib/menu-settings.ts +37 -97
- package/lib/prompts.ts +4 -2
- package/lib/queue.ts +52 -8
- package/lib/routing.ts +10 -16
- package/lib/status.ts +1 -1
- package/lib/text-groups.ts +29 -15
- package/lib/thread-reconciler.ts +3 -71
- package/lib/threads.ts +0 -43
- package/lib/turns.ts +25 -21
- package/lib/voice.ts +6 -7
- package/package.json +3 -2
package/docs/outbound.md
CHANGED
|
@@ -30,7 +30,7 @@ When `assistant.rendering` is `"rich"`, a Telegram-originated turn that queues e
|
|
|
30
30
|
|
|
31
31
|
The optimization is deliberately narrow. HTML rendering, empty final text, multiple files, documents and other unsupported formats, Guest Mode, explicit `telegram_voice`, voice-preferred turns, and OGG/Opus artifacts retain their established text/attachment/voice paths. A known-safe Rich upload rejection falls back to those paths. A `commit-unknown` transport outcome or a nominally successful upload without a verifiable message id never falls back or replays because the first non-idempotent send may already have committed.
|
|
32
32
|
|
|
33
|
-
This behavior does not generate media or alter voice policy. `telegram_attach` still represents an explicit assistant artifact decision, while `
|
|
33
|
+
This behavior does not generate media or alter voice policy. `telegram_attach` still represents an explicit assistant artifact decision, while `hidden`, `mirror`, and `always` continue to decide voice synthesis independently.
|
|
34
34
|
|
|
35
35
|
Core assistant output accepts only the Markdown or HTML `InputRichMessage` forms and does not construct explicit block arrays or `InputRichBlockThinking`. Telegram's Thinking block is draft-only and must never become a projection of hidden reasoning or chain-of-thought. Any future use for Activity would require an explicitly public user-visible summary rather than provider reasoning content.
|
|
36
36
|
|
package/docs/public-api.md
CHANGED
|
@@ -93,7 +93,7 @@ interface TelegramConfig {
|
|
|
93
93
|
proactivePush?: boolean;
|
|
94
94
|
};
|
|
95
95
|
voice?: {
|
|
96
|
-
replyMode?: "
|
|
96
|
+
replyMode?: "hidden" | "mirror" | "always";
|
|
97
97
|
sendTranscript?: boolean;
|
|
98
98
|
};
|
|
99
99
|
time?: {
|
|
@@ -106,7 +106,7 @@ interface TelegramConfig {
|
|
|
106
106
|
Hidden/default semantics are represented by absence:
|
|
107
107
|
|
|
108
108
|
- `assistant.proactivePush` defaults to `true`; omit it to keep projection enabled, or set it explicitly to `false` to disable it. When enabled, each completed public assistant text block from local or autonomous work is projected to the authorized Telegram target once and in source order. This includes visible intermediate commentary/checkpoints and the final block. It excludes token deltas, hidden reasoning, tool calls/arguments/results, Telegram-owned turns, empty blocks, and stale authority. Projection uses the configured Rich or HTML assistant renderer and binds admitted work to the exact target, profile/token transport generation, direct leader epoch or follower registration generation, and session generation. The old top-level `proactivePush` key is ignored; move the setting manually under `assistant`.
|
|
109
|
-
- Voice Reply `hidden`: no `voice.replyMode` key is persisted.
|
|
109
|
+
- Voice Reply `hidden`: no `voice.replyMode` key is persisted; legacy `manual` resolves to this silent default. `mirror` adds `[voice] delivery: automatic voice` only to voice/audio-input turns, while `always` adds the same effective line to every Telegram turn.
|
|
110
110
|
- Agent activity status is not configurable in this release. Telegram uses native `sendChatAction(typing)` / product `...active` status as the only automatic in-chat work signal before the final reply.
|
|
111
111
|
- Time Injection `hidden`: no `time.injectionMode` key is persisted; if `time` becomes empty, the whole `time` object may be omitted.
|
|
112
112
|
|
package/docs/ui-style.md
CHANGED
package/docs/voice.md
CHANGED
|
@@ -33,14 +33,13 @@ Voice provider extensions can also register STT backends with `registerTelegramV
|
|
|
33
33
|
|
|
34
34
|
## Voice Reply Policy
|
|
35
35
|
|
|
36
|
-
The bridge decides **when** to reply with voice from `voice.replyMode` in `TelegramConfig` (stored in `telegram.json`).
|
|
36
|
+
The bridge decides **when** to reply with voice from `voice.replyMode` in `TelegramConfig` (stored in `telegram.json`). Missing, invalid, `hidden`, and legacy `manual` values resolve to the silent `hidden` default.
|
|
37
37
|
|
|
38
38
|
### Modes
|
|
39
39
|
|
|
40
|
-
- **`hidden` (default):** no `voice.replyMode` is stored
|
|
41
|
-
- **`
|
|
42
|
-
- **`
|
|
43
|
-
- **`always`:** always reply with voice.
|
|
40
|
+
- **`hidden` (default):** no `voice.replyMode` is stored and no automatic voice context is added; explicit agent-authored `telegram_voice` actions still work.
|
|
41
|
+
- **`mirror`:** voice/audio input activates automatic voice delivery. Text input follows `hidden` behavior.
|
|
42
|
+
- **`always`:** every Telegram turn activates automatic voice delivery.
|
|
44
43
|
|
|
45
44
|
**Warning:** In `always` mode, the bridge transparently intercepts ALL text replies and converts them to voice on success. Users will only receive voice messages when voice generation succeeds. If voice generation fails, the bridge falls back to sending the planned text reply.
|
|
46
45
|
|
|
@@ -97,7 +96,7 @@ Registration returns a disposer function for cleanup. Stable provider registrati
|
|
|
97
96
|
|
|
98
97
|
## Outbound Voice Handlers
|
|
99
98
|
|
|
100
|
-
Users can also configure `outboundHandlers` with `type: "voice"` in `telegram.json`. This is the command-template path for TTS without a provider extension. Reply modes (`
|
|
99
|
+
Users can also configure `outboundHandlers` with `type: "voice"` in `telegram.json`. This is the command-template path for TTS without a provider extension. Reply modes (`hidden`, `mirror`, `always`) affect these handlers the same way they affect providers: explicit `telegram_voice` blocks and automatic mirror/always interception both produce a voice reply plan, then delivery tries configured outbound voice handlers first and registered synthesis providers as progressive fallbacks.
|
|
101
100
|
|
|
102
101
|
Voice handlers receive the text on stdin in composed pipelines and can use `{text}`, `{lang}`, `{rate}`, `{mp3}`, and `{ogg}` placeholders. Set `output` to `"ogg"` or another placeholder name when the template writes to a known path:
|
|
103
102
|
|
|
@@ -167,7 +166,7 @@ Voice provider extensions can register a Voice Extension Section (settings UI) v
|
|
|
167
166
|
|
|
168
167
|
## Prompt Guidance
|
|
169
168
|
|
|
170
|
-
The bridge keeps voice prompt context compact and policy-owned.
|
|
169
|
+
The bridge keeps voice prompt context compact, effective, and policy-owned. `hidden` and text-originated `mirror` turns add no voice line. Voice/audio-originated `mirror` turns and every `always` turn add exactly `[voice] delivery: automatic voice`, describing the current delivery environment without exposing the underlying mode matrix or an instruction list. The marker is appended after `[outputs]` when handler output exists, otherwise after `[attachments]`. Voice inputs also appear in `[attachments]` with their downloaded file names, MIME data, and handler output, so agents can infer concrete voice-file context from attachment metadata.
|
|
171
170
|
|
|
172
171
|
Voice synthesis providers can supply prompt guidance through `getVoicePromptContribution(view)`, but provider text should stay optional and provider-specific. Reply-mode context belongs to pi-telegram.
|
|
173
172
|
|
|
@@ -204,12 +203,12 @@ Voice synthesis providers can supply prompt guidance through `getVoicePromptCont
|
|
|
204
203
|
```json
|
|
205
204
|
{
|
|
206
205
|
"voice": {
|
|
207
|
-
"replyMode": "
|
|
206
|
+
"replyMode": "mirror"
|
|
208
207
|
}
|
|
209
208
|
}
|
|
210
209
|
```
|
|
211
210
|
|
|
212
|
-
Valid
|
|
211
|
+
Valid modes are `"hidden"`, `"mirror"`, and `"always"`; selecting `hidden` removes the key. Missing, invalid, and legacy `"manual"` values resolve to `hidden` and stay silent in prompt context.
|
|
213
212
|
|
|
214
213
|
The bridge reads `voice.replyMode` from the config when building a turn.
|
|
215
214
|
|
package/index.ts
CHANGED
|
@@ -155,11 +155,6 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
155
155
|
contextStore: telegramSessionContextStore,
|
|
156
156
|
});
|
|
157
157
|
const activeTurnRuntime = Queue.createTelegramActiveTurnStore();
|
|
158
|
-
const proactivePushChatIdGetter =
|
|
159
|
-
Config.createTelegramProactivePushChatIdGetter({
|
|
160
|
-
getActiveTurnChatId: activeTurnRuntime.getChatId,
|
|
161
|
-
getAllowedUserId: configStore.getAllowedUserId,
|
|
162
|
-
});
|
|
163
158
|
const proactivePushTargetGetter =
|
|
164
159
|
Config.createTelegramProactivePushTargetGetter({
|
|
165
160
|
getActiveTurnTarget: activeTurnRuntime.getTarget,
|
|
@@ -171,6 +166,8 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
171
166
|
},
|
|
172
167
|
getAllowedUserId: configStore.getAllowedUserId,
|
|
173
168
|
});
|
|
169
|
+
const proactivePushChatIdGetter =
|
|
170
|
+
Config.createTelegramProactivePushChatIdGetter(proactivePushTargetGetter);
|
|
174
171
|
const buttonActionStore = Outbound.createTelegramButtonActionStore();
|
|
175
172
|
const pendingModelSwitchStore =
|
|
176
173
|
Model.createPendingModelSwitchStore<
|
|
@@ -225,6 +222,11 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
225
222
|
persist: configStore.persist,
|
|
226
223
|
markConfigChange: telegramSyncStateRuntime.markConfigChange,
|
|
227
224
|
});
|
|
225
|
+
const persistTelegramPollingOffset =
|
|
226
|
+
Config.createTelegramPollingOffsetPersister(
|
|
227
|
+
configStore,
|
|
228
|
+
persistTelegramConfigWithSync,
|
|
229
|
+
);
|
|
228
230
|
const currentInstanceThreadRuntime =
|
|
229
231
|
Threads.createTelegramCurrentInstanceThreadRuntime({
|
|
230
232
|
instanceId: telegramInstanceId,
|
|
@@ -825,7 +827,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
825
827
|
hasBotToken: configStore.hasBotToken,
|
|
826
828
|
deleteWebhook,
|
|
827
829
|
getUpdates,
|
|
828
|
-
persistConfig:
|
|
830
|
+
persistConfig: persistTelegramPollingOffset,
|
|
829
831
|
prepareUpdateBatch: textGroupRuntime.prepareUpdateBatch,
|
|
830
832
|
handleUpdate: Updates.createTelegramUpdateHandle({
|
|
831
833
|
defaultHandle: inboundRouteRuntime.handleUpdate,
|
|
@@ -1067,7 +1069,6 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1067
1069
|
deleteMessage: deleteTelegramMessage,
|
|
1068
1070
|
sendGuestReply,
|
|
1069
1071
|
finalizeMarkdownPreview,
|
|
1070
|
-
proactivePushChatIdGetter,
|
|
1071
1072
|
proactivePushTargetGetter,
|
|
1072
1073
|
isProactivePushEnabled: configControls.isProactivePushEnabled,
|
|
1073
1074
|
getAssistantRenderingMode: configControls.getAssistantRenderingMode,
|
package/lib/bindings.ts
CHANGED
|
@@ -36,9 +36,7 @@ type TelegramRuntimeEventRecorder = (
|
|
|
36
36
|
type TelegramBridgeStatusUpdater =
|
|
37
37
|
Status.TelegramStatusRuntime<Pi.ExtensionContext>["updateStatus"];
|
|
38
38
|
|
|
39
|
-
export interface TelegramAssistantOutputBindingRuntime<
|
|
40
|
-
TTransportStamp,
|
|
41
|
-
> {
|
|
39
|
+
export interface TelegramAssistantOutputBindingRuntime<TTransportStamp> {
|
|
42
40
|
runtime: Activity.TelegramAssistantOutputRuntime;
|
|
43
41
|
observeEvent: (event: Activity.TelegramActivityEvent) => void;
|
|
44
42
|
authority: Routing.TelegramAssistantOutputAuthorityRuntime<TTransportStamp>;
|
|
@@ -49,7 +47,9 @@ export function createTelegramAssistantOutputBindingRuntime<
|
|
|
49
47
|
>(deps: {
|
|
50
48
|
isEnabled: () => boolean;
|
|
51
49
|
authority: {
|
|
52
|
-
getPreferredTarget: () =>
|
|
50
|
+
getPreferredTarget: () =>
|
|
51
|
+
| OutboundAttachments.TelegramQueuedOutboundAttachmentTurnView["target"]
|
|
52
|
+
| undefined;
|
|
53
53
|
getFallbackChatId: () => number | undefined;
|
|
54
54
|
getTransportStamp: () => TTransportStamp;
|
|
55
55
|
isTransportStampActive: (stamp: TTransportStamp) => boolean;
|
|
@@ -63,8 +63,9 @@ export function createTelegramAssistantOutputBindingRuntime<
|
|
|
63
63
|
>[0];
|
|
64
64
|
recordRuntimeEvent: TelegramRuntimeEventRecorder;
|
|
65
65
|
}): TelegramAssistantOutputBindingRuntime<TTransportStamp> {
|
|
66
|
-
const authority =
|
|
67
|
-
|
|
66
|
+
const authority = Routing.createTelegramAssistantOutputAuthorityRuntime(
|
|
67
|
+
deps.authority,
|
|
68
|
+
);
|
|
68
69
|
const send =
|
|
69
70
|
OutboundHandlers.createTelegramAssistantOutputSender<TTransportStamp>(
|
|
70
71
|
deps.sender,
|
|
@@ -344,7 +345,6 @@ interface TelegramLifecycleBindingDeps {
|
|
|
344
345
|
Pi.AgentEndEvent["messages"][number],
|
|
345
346
|
Keyboard.TelegramInlineKeyboardMarkup
|
|
346
347
|
>["finalizeMarkdownPreview"];
|
|
347
|
-
proactivePushChatIdGetter: () => number | undefined;
|
|
348
348
|
proactivePushTargetGetter: () => Queue.TelegramQueueTarget | undefined;
|
|
349
349
|
isProactivePushEnabled: () => boolean;
|
|
350
350
|
getAssistantRenderingMode: () => "rich" | "html";
|
|
@@ -387,7 +387,6 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
387
387
|
deleteMessage,
|
|
388
388
|
sendGuestReply,
|
|
389
389
|
finalizeMarkdownPreview,
|
|
390
|
-
proactivePushChatIdGetter,
|
|
391
390
|
proactivePushTargetGetter,
|
|
392
391
|
isProactivePushEnabled,
|
|
393
392
|
getAssistantRenderingMode,
|
|
@@ -425,7 +424,7 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
425
424
|
caption?: string,
|
|
426
425
|
): Promise<void> => {
|
|
427
426
|
const stagingTarget = proactivePushTargetGetter();
|
|
428
|
-
const stagingChatId = stagingTarget?.chatId
|
|
427
|
+
const stagingChatId = stagingTarget?.chatId;
|
|
429
428
|
if (stagingChatId === undefined) {
|
|
430
429
|
throw new Error(
|
|
431
430
|
"Guest attachment staging requires a paired Telegram chat",
|
|
@@ -470,7 +469,7 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
470
469
|
caption?: string,
|
|
471
470
|
): Promise<void> => {
|
|
472
471
|
const stagingTarget = proactivePushTargetGetter();
|
|
473
|
-
const stagingChatId = stagingTarget?.chatId
|
|
472
|
+
const stagingChatId = stagingTarget?.chatId;
|
|
474
473
|
if (stagingChatId === undefined) {
|
|
475
474
|
throw new Error("Guest voice staging requires a paired Telegram chat");
|
|
476
475
|
}
|
|
@@ -595,11 +594,9 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
595
594
|
if (!canSendAgentActivity(ctx)) return false;
|
|
596
595
|
const turn = activeTurnRuntime.get();
|
|
597
596
|
const target = turn?.target ?? proactivePushTargetGetter();
|
|
598
|
-
promptDispatchRuntime.startTypingLoop(
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
{ target },
|
|
602
|
-
);
|
|
597
|
+
promptDispatchRuntime.startTypingLoop(ctx, turn?.chatId ?? target?.chatId, {
|
|
598
|
+
target,
|
|
599
|
+
});
|
|
603
600
|
return true;
|
|
604
601
|
};
|
|
605
602
|
const startActiveTurnTypingLoop = (ctx: Pi.ExtensionContext): void => {
|
|
@@ -644,6 +641,7 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
644
641
|
},
|
|
645
642
|
async onSessionShutdown(event, ctx) {
|
|
646
643
|
if (!isSessionContextActive(ctx)) return;
|
|
644
|
+
agentLifecycleHooks.clearRetainedAgentEnd();
|
|
647
645
|
activityRuntime.onSessionShutdown();
|
|
648
646
|
assistantOutputRuntime.stop();
|
|
649
647
|
compactionObserver.onSessionShutdown();
|
|
@@ -710,8 +708,9 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
710
708
|
activityRuntime.onAgentEnd();
|
|
711
709
|
await agentLifecycleHooks.onAgentEnd(event, ctx);
|
|
712
710
|
},
|
|
713
|
-
onAgentSettled(
|
|
711
|
+
async onAgentSettled(event, ctx) {
|
|
714
712
|
if (!isSessionContextActive(ctx)) return;
|
|
713
|
+
await agentLifecycleHooks.onAgentSettled(event, ctx);
|
|
715
714
|
activityRuntime.onAgentSettled();
|
|
716
715
|
},
|
|
717
716
|
onBeforeAgentStart: Prompts.createTelegramProactiveBeforeAgentStartHook({
|
package/lib/bus-leader.ts
CHANGED
|
@@ -109,17 +109,10 @@ export interface TelegramBusFollowerTargetProvisionerDeps {
|
|
|
109
109
|
) => void;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
export interface
|
|
112
|
+
export interface TelegramBusFollowerDisconnectHandlerDeps {
|
|
113
113
|
topicTargetStore: Pick<
|
|
114
114
|
Threads.TelegramTopicTargetStore,
|
|
115
|
-
| "
|
|
116
|
-
| "getActiveByInstanceId"
|
|
117
|
-
| "list"
|
|
118
|
-
| "listPendingProvisions"
|
|
119
|
-
| "markStaleByTarget"
|
|
120
|
-
| "markOfflineByInstanceId"
|
|
121
|
-
| "persist"
|
|
122
|
-
| "removePendingProvision"
|
|
115
|
+
"markOfflineByInstanceId" | "persist"
|
|
123
116
|
>;
|
|
124
117
|
callApi: <TResponse>(
|
|
125
118
|
method: string,
|
|
@@ -161,11 +154,9 @@ export interface TelegramBusLeaderRuntimeAssemblyDeps<TContext> {
|
|
|
161
154
|
runtime: Omit<
|
|
162
155
|
TelegramBusLeaderRuntimeDeps<TContext>,
|
|
163
156
|
| "callApi"
|
|
164
|
-
| "onFollowerPruned"
|
|
165
157
|
| "onFollowerDisconnected"
|
|
166
158
|
| "provisionFollowerTarget"
|
|
167
159
|
| "provisionLeaderTarget"
|
|
168
|
-
| "reconcileFollowerBindings"
|
|
169
160
|
| "recordRuntimeEvent"
|
|
170
161
|
>;
|
|
171
162
|
getAllowedUserId: () => number | undefined;
|
|
@@ -218,9 +209,6 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
|
|
|
218
209
|
recordThreadReconciliationPlan: deps.recordThreadReconciliationPlan,
|
|
219
210
|
setLeaderTarget: deps.setLeaderTarget,
|
|
220
211
|
}),
|
|
221
|
-
onFollowerPruned: createTelegramBusFollowerPruneHandler({
|
|
222
|
-
...provisionerPorts,
|
|
223
|
-
}),
|
|
224
212
|
onFollowerDisconnected: createTelegramBusFollowerDisconnectHandler({
|
|
225
213
|
...provisionerPorts,
|
|
226
214
|
}),
|
|
@@ -228,12 +216,6 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
|
|
|
228
216
|
...provisionerPorts,
|
|
229
217
|
}),
|
|
230
218
|
getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
|
|
231
|
-
reconcileFollowerBindings:
|
|
232
|
-
createTelegramBusFollowerBindingRealityReconciler({
|
|
233
|
-
topicTargetStore: deps.topicTargetStore,
|
|
234
|
-
followerRegistry: deps.runtime.followerRegistry,
|
|
235
|
-
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
236
|
-
}),
|
|
237
219
|
callApi: createTelegramBusLeaderApiProxy({
|
|
238
220
|
call: deps.callApi,
|
|
239
221
|
callMultipart: deps.callMultipart,
|
|
@@ -244,29 +226,6 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
|
|
|
244
226
|
});
|
|
245
227
|
}
|
|
246
228
|
|
|
247
|
-
export const TELEGRAM_BUS_RECENT_FOLLOWER_BINDING_GRACE_MS = 10_000;
|
|
248
|
-
|
|
249
|
-
export interface TelegramBusFollowerBindingRealityDeps {
|
|
250
|
-
topicTargetStore: Pick<
|
|
251
|
-
Threads.TelegramTopicTargetStore,
|
|
252
|
-
| "load"
|
|
253
|
-
| "list"
|
|
254
|
-
| "markOfflineByInstanceId"
|
|
255
|
-
| "forgetIdentityByProfileKey"
|
|
256
|
-
| "getBotState"
|
|
257
|
-
| "persist"
|
|
258
|
-
| "setBotState"
|
|
259
|
-
>;
|
|
260
|
-
followerRegistry: Pick<TelegramBusFollowerRegistry, "list">;
|
|
261
|
-
getNowMs?: () => number;
|
|
262
|
-
recentBindingGraceMs?: number;
|
|
263
|
-
recordRuntimeEvent: (
|
|
264
|
-
category: string,
|
|
265
|
-
error: unknown,
|
|
266
|
-
details?: Record<string, unknown>,
|
|
267
|
-
) => void;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
229
|
export interface TelegramBusFollowerMessageOwnershipRecord {
|
|
271
230
|
follower: TelegramBusFollowerView;
|
|
272
231
|
chatId: number;
|
|
@@ -296,15 +255,10 @@ export interface TelegramBusLeaderRuntimeDeps<TContext> {
|
|
|
296
255
|
registration: TelegramBusInstanceRegistration,
|
|
297
256
|
) => Promise<TelegramTarget | undefined> | TelegramTarget | undefined;
|
|
298
257
|
getCurrentLeaderEpoch?: () => number | string | undefined;
|
|
299
|
-
reconcileFollowerBindings?: () => Promise<unknown> | unknown;
|
|
300
258
|
provisionLeaderTarget?: (ctx: TContext) => Promise<void> | void;
|
|
301
259
|
getNowMs?: () => number;
|
|
302
260
|
followerPruneIntervalMs?: number;
|
|
303
261
|
followerStaleAfterMs?: number;
|
|
304
|
-
followerRecoveryGraceMs?: number;
|
|
305
|
-
onFollowerPruned?: (
|
|
306
|
-
follower: TelegramBusFollowerView,
|
|
307
|
-
) => Promise<void> | void;
|
|
308
262
|
onFollowerDisconnected?: (
|
|
309
263
|
follower: TelegramBusFollowerView,
|
|
310
264
|
) => Promise<void> | void;
|
|
@@ -315,23 +269,11 @@ export interface TelegramBusLeaderRuntimeDeps<TContext> {
|
|
|
315
269
|
) => void;
|
|
316
270
|
}
|
|
317
271
|
|
|
318
|
-
export function createTelegramBusFollowerBindingRealityReconciler(
|
|
319
|
-
deps: TelegramBusFollowerBindingRealityDeps,
|
|
320
|
-
): () => Promise<number> {
|
|
321
|
-
return async () => {
|
|
322
|
-
await deps.topicTargetStore.load();
|
|
323
|
-
// A missing live registration proves only that the follower process is
|
|
324
|
-
// currently absent. Keep its durable owner/target binding so a replacement
|
|
325
|
-
// process with the same manual profile can reclaim the existing thread.
|
|
326
|
-
return 0;
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
|
|
330
272
|
export function createTelegramBusInstanceLifecycleAnnouncement(input: {
|
|
331
273
|
target: TelegramTarget & { threadId: number };
|
|
332
274
|
threadName?: string;
|
|
333
275
|
slot?: string;
|
|
334
|
-
state: "connected"
|
|
276
|
+
state: "connected";
|
|
335
277
|
}): TelegramBusFollowerLifecycleAnnouncement {
|
|
336
278
|
return {
|
|
337
279
|
target: { ...input.target },
|
|
@@ -340,23 +282,6 @@ export function createTelegramBusInstanceLifecycleAnnouncement(input: {
|
|
|
340
282
|
};
|
|
341
283
|
}
|
|
342
284
|
|
|
343
|
-
export function createTelegramBusFollowerDisconnectedAnnouncement(input: {
|
|
344
|
-
follower: TelegramBusFollowerView;
|
|
345
|
-
threadName?: string;
|
|
346
|
-
slot?: string;
|
|
347
|
-
}): TelegramBusFollowerLifecycleAnnouncement | undefined {
|
|
348
|
-
if (!input.follower.target?.threadId) return undefined;
|
|
349
|
-
return createTelegramBusInstanceLifecycleAnnouncement({
|
|
350
|
-
target: {
|
|
351
|
-
chatId: input.follower.target.chatId,
|
|
352
|
-
threadId: input.follower.target.threadId,
|
|
353
|
-
},
|
|
354
|
-
threadName: input.threadName,
|
|
355
|
-
slot: input.slot,
|
|
356
|
-
state: "disconnected",
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
|
|
360
285
|
const TELEGRAM_BUS_SLOW_FOLLOWER_REGISTRATION_MS = 1000;
|
|
361
286
|
|
|
362
287
|
function scheduleTelegramBusLeaderBackgroundTask(
|
|
@@ -753,23 +678,8 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
753
678
|
};
|
|
754
679
|
}
|
|
755
680
|
|
|
756
|
-
export function createTelegramBusFollowerPruneHandler(
|
|
757
|
-
deps: TelegramBusFollowerPruneHandlerDeps,
|
|
758
|
-
): (follower: TelegramBusFollowerView) => Promise<void> {
|
|
759
|
-
return async (follower) => {
|
|
760
|
-
deps.recordRuntimeEvent(
|
|
761
|
-
"bus",
|
|
762
|
-
"Telegram bus follower heartbeat stale; preserving thread binding",
|
|
763
|
-
{
|
|
764
|
-
phase: "follower-pruned",
|
|
765
|
-
instanceId: follower.instanceId,
|
|
766
|
-
},
|
|
767
|
-
);
|
|
768
|
-
};
|
|
769
|
-
}
|
|
770
|
-
|
|
771
681
|
export function createTelegramBusFollowerDisconnectHandler(
|
|
772
|
-
deps:
|
|
682
|
+
deps: TelegramBusFollowerDisconnectHandlerDeps,
|
|
773
683
|
): (follower: TelegramBusFollowerView) => Promise<void> {
|
|
774
684
|
return async (follower) => {
|
|
775
685
|
const target = follower.target;
|
|
@@ -1458,32 +1368,12 @@ export function createTelegramBusLeaderRuntime<TContext>(
|
|
|
1458
1368
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
1459
1369
|
const followerPruneIntervalMs = deps.followerPruneIntervalMs ?? 1000;
|
|
1460
1370
|
const followerStaleAfterMs = deps.followerStaleAfterMs ?? 5000;
|
|
1461
|
-
const followerRecoveryGraceMs = deps.followerRecoveryGraceMs ?? 5000;
|
|
1462
1371
|
let pruneInterval: ReturnType<typeof setInterval> | undefined;
|
|
1463
|
-
let followerRealityTimer: ReturnType<typeof setTimeout> | undefined;
|
|
1464
1372
|
const stopPruning = () => {
|
|
1465
1373
|
if (!pruneInterval) return;
|
|
1466
1374
|
clearInterval(pruneInterval);
|
|
1467
1375
|
pruneInterval = undefined;
|
|
1468
1376
|
};
|
|
1469
|
-
const stopFollowerRealityTimer = () => {
|
|
1470
|
-
if (!followerRealityTimer) return;
|
|
1471
|
-
clearTimeout(followerRealityTimer);
|
|
1472
|
-
followerRealityTimer = undefined;
|
|
1473
|
-
};
|
|
1474
|
-
const scheduleFollowerBindingReality = () => {
|
|
1475
|
-
stopFollowerRealityTimer();
|
|
1476
|
-
if (!deps.reconcileFollowerBindings) return;
|
|
1477
|
-
followerRealityTimer = setTimeout(() => {
|
|
1478
|
-
followerRealityTimer = undefined;
|
|
1479
|
-
void Promise.resolve(deps.reconcileFollowerBindings?.()).catch((error) =>
|
|
1480
|
-
deps.recordRuntimeEvent?.("bus", error, {
|
|
1481
|
-
phase: "follower-binding-reality",
|
|
1482
|
-
}),
|
|
1483
|
-
);
|
|
1484
|
-
}, followerRecoveryGraceMs);
|
|
1485
|
-
followerRealityTimer.unref?.();
|
|
1486
|
-
};
|
|
1487
1377
|
const pruneFollowers = async () => {
|
|
1488
1378
|
try {
|
|
1489
1379
|
await localServer.ensureEndpoint();
|
|
@@ -1497,18 +1387,14 @@ export function createTelegramBusLeaderRuntime<TContext>(
|
|
|
1497
1387
|
followerStaleAfterMs,
|
|
1498
1388
|
);
|
|
1499
1389
|
for (const follower of removed) {
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
phase: "follower-
|
|
1390
|
+
deps.recordRuntimeEvent?.(
|
|
1391
|
+
"bus",
|
|
1392
|
+
"Telegram bus follower heartbeat stale; preserving thread binding",
|
|
1393
|
+
{
|
|
1394
|
+
phase: "follower-pruned",
|
|
1505
1395
|
instanceId: follower.instanceId,
|
|
1506
|
-
}
|
|
1507
|
-
|
|
1508
|
-
deps.recordRuntimeEvent?.("bus", "Telegram bus follower timed out", {
|
|
1509
|
-
phase: "follower-prune",
|
|
1510
|
-
instanceId: follower.instanceId,
|
|
1511
|
-
});
|
|
1396
|
+
},
|
|
1397
|
+
);
|
|
1512
1398
|
}
|
|
1513
1399
|
};
|
|
1514
1400
|
const startPruning = () => {
|
|
@@ -1542,21 +1428,18 @@ export function createTelegramBusLeaderRuntime<TContext>(
|
|
|
1542
1428
|
return {
|
|
1543
1429
|
startPolling: async (ctx) => {
|
|
1544
1430
|
await localServer.start();
|
|
1545
|
-
scheduleFollowerBindingReality();
|
|
1546
1431
|
startPruning();
|
|
1547
1432
|
try {
|
|
1548
1433
|
await deps.provisionLeaderTarget?.(ctx);
|
|
1549
1434
|
await deps.startPolling(ctx);
|
|
1550
1435
|
} catch (error) {
|
|
1551
1436
|
stopPruning();
|
|
1552
|
-
stopFollowerRealityTimer();
|
|
1553
1437
|
await localServer.stop();
|
|
1554
1438
|
throw error;
|
|
1555
1439
|
}
|
|
1556
1440
|
},
|
|
1557
1441
|
stopPolling: async () => {
|
|
1558
1442
|
stopPruning();
|
|
1559
|
-
stopFollowerRealityTimer();
|
|
1560
1443
|
try {
|
|
1561
1444
|
await deps.stopPolling();
|
|
1562
1445
|
} finally {
|
package/lib/config.ts
CHANGED
|
@@ -17,8 +17,8 @@ import {
|
|
|
17
17
|
import { chmod, mkdir, rename, writeFile } from "node:fs/promises";
|
|
18
18
|
import { resolveAgentDir, resolveTelegramConfigPath } from "./paths.ts";
|
|
19
19
|
|
|
20
|
-
import type { TelegramInboundHandlerConfig } from "./inbound.ts";
|
|
21
20
|
import type { CommandTemplateObjectConfig } from "./command-templates.ts";
|
|
21
|
+
import type { TelegramInboundHandlerConfig } from "./inbound.ts";
|
|
22
22
|
import { withTelegramFileTransaction } from "./locks.ts";
|
|
23
23
|
|
|
24
24
|
const CONFIG_RUNTIME_KEY = "__piTelegramConfigRuntime__";
|
|
@@ -72,7 +72,7 @@ export interface TelegramConfig {
|
|
|
72
72
|
/** @deprecated use assistant.rendering */
|
|
73
73
|
assistantRendering?: TelegramAssistantRenderingMode;
|
|
74
74
|
voice?: {
|
|
75
|
-
replyMode?: "
|
|
75
|
+
replyMode?: "hidden" | "mirror" | "always";
|
|
76
76
|
/** Whether to attach the provider's transcriptText as caption on voice messages */
|
|
77
77
|
sendTranscript?: boolean;
|
|
78
78
|
};
|
|
@@ -543,6 +543,27 @@ export function createTelegramConfigStore(
|
|
|
543
543
|
};
|
|
544
544
|
}
|
|
545
545
|
|
|
546
|
+
export function createTelegramPollingOffsetPersister(
|
|
547
|
+
configStore: Pick<TelegramConfigStore, "get" | "set" | "persist">,
|
|
548
|
+
persist: () => Promise<void> = () => configStore.persist(),
|
|
549
|
+
): (pollingConfig: { lastUpdateId?: number }) => Promise<void> {
|
|
550
|
+
return async (pollingConfig) => {
|
|
551
|
+
const nextOffset = pollingConfig.lastUpdateId;
|
|
552
|
+
if (typeof nextOffset === "number") {
|
|
553
|
+
const current = configStore.get();
|
|
554
|
+
const currentOffset = current.lastUpdateId;
|
|
555
|
+
configStore.set({
|
|
556
|
+
...current,
|
|
557
|
+
lastUpdateId:
|
|
558
|
+
typeof currentOffset === "number"
|
|
559
|
+
? Math.max(currentOffset, nextOffset)
|
|
560
|
+
: nextOffset,
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
await persist();
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
546
567
|
export function createTelegramProactivePushChecker(
|
|
547
568
|
configStore: Pick<TelegramConfigStore, "get">,
|
|
548
569
|
): () => boolean {
|
|
@@ -625,12 +646,10 @@ export function createTelegramAssistantRenderingModeSetter(
|
|
|
625
646
|
|
|
626
647
|
export function createTelegramVoiceReplyModeGetter(
|
|
627
648
|
configStore: Pick<TelegramConfigStore, "get">,
|
|
628
|
-
): () => "
|
|
649
|
+
): () => "hidden" | "mirror" | "always" {
|
|
629
650
|
return () => {
|
|
630
651
|
const mode = configStore.get().voice?.replyMode;
|
|
631
|
-
return mode === "mirror" || mode === "always"
|
|
632
|
-
? mode
|
|
633
|
-
: "manual";
|
|
652
|
+
return mode === "mirror" || mode === "always" ? mode : "hidden";
|
|
634
653
|
};
|
|
635
654
|
}
|
|
636
655
|
|
|
@@ -639,17 +658,17 @@ export function createTelegramVoiceReplyModeConfiguredChecker(
|
|
|
639
658
|
): () => boolean {
|
|
640
659
|
return () => {
|
|
641
660
|
const mode = configStore.get().voice?.replyMode;
|
|
642
|
-
return mode === "mirror" || mode === "always"
|
|
661
|
+
return mode === "mirror" || mode === "always";
|
|
643
662
|
};
|
|
644
663
|
}
|
|
645
664
|
|
|
646
665
|
export function createTelegramVoiceReplyModeSetter(
|
|
647
666
|
configStore: TelegramMutableConfigStore,
|
|
648
|
-
): (replyMode: "
|
|
667
|
+
): (replyMode: "hidden" | "mirror" | "always" | undefined) => Promise<void> {
|
|
649
668
|
return async (replyMode) => {
|
|
650
669
|
await loadLatestTelegramConfig(configStore);
|
|
651
670
|
const current = configStore.get();
|
|
652
|
-
if (replyMode === undefined) {
|
|
671
|
+
if (replyMode === undefined || replyMode === "hidden") {
|
|
653
672
|
const { replyMode: _replyMode, ...remainingVoice } = current.voice ?? {};
|
|
654
673
|
const next = { ...current };
|
|
655
674
|
if (Object.keys(remainingVoice).length > 0) next.voice = remainingVoice;
|
|
@@ -730,11 +749,10 @@ export interface TelegramProactivePushTarget {
|
|
|
730
749
|
threadId?: number;
|
|
731
750
|
}
|
|
732
751
|
|
|
733
|
-
export function createTelegramProactivePushChatIdGetter(
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
return () => deps.getActiveTurnChatId() ?? deps.getAllowedUserId();
|
|
752
|
+
export function createTelegramProactivePushChatIdGetter(
|
|
753
|
+
getTarget: () => TelegramProactivePushTarget | undefined,
|
|
754
|
+
): () => number | undefined {
|
|
755
|
+
return () => getTarget()?.chatId;
|
|
738
756
|
}
|
|
739
757
|
|
|
740
758
|
export function createTelegramProactivePushTargetGetter(deps: {
|