@llblab/pi-telegram 0.43.2 → 0.44.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 +1 -1
- package/BACKLOG.md +0 -1
- package/CHANGELOG.md +9 -5
- package/README.md +1 -1
- package/docs/outbound.md +8 -2
- package/docs/public-api.md +1 -0
- package/index.ts +22 -19
- package/lib/activity.ts +19 -5
- package/lib/bindings.ts +9 -1
- package/lib/config.ts +1 -1
- package/lib/delivery.ts +18 -18
- package/lib/lifecycle.ts +7 -1
- package/lib/menu-settings.ts +2 -2
- package/lib/outbound-attachments.ts +23 -30
- package/lib/outbound-voice.ts +28 -42
- package/lib/outbound.ts +18 -14
- package/lib/preview.ts +115 -70
- package/lib/queue.ts +18 -8
- package/lib/replies.ts +46 -38
- package/lib/telegram-api.ts +36 -3
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -126,7 +126,7 @@ The detailed map is canonical in [`docs/architecture.md`](./docs/architecture.md
|
|
|
126
126
|
- Remove dead code immediately. Reachability from composition roots, public exports, tests, registered surfaces, and documented APIs—not recent usefulness—determines whether code is live.
|
|
127
127
|
- Treat every meaningful `index.ts` edit as a composition-pressure check, but keep one-off live adapter wiring there when extraction would only hide cross-domain state.
|
|
128
128
|
- Follow [`docs/ui-style.md`](./docs/ui-style.md) for interface copy, emoji, buttons, menus, and dialogs. Update the registry before assigning a new UI emoji meaning. Standalone notices use one fully bold emoji-led sentence with a terminal period; menu or chooser headings use the same hierarchy with a terminal colon. Material names may add nested italic emphasis without breaking the outer bold span. Callback alerts preserve equivalent emoji-led plain text because Telegram does not support rich formatting there.
|
|
129
|
-
- Markdown tables use compact source formatting with `---` separator cells and one surrounding space per cell. Preserve vendored references unchanged.
|
|
129
|
+
- Markdown lists never contain blank lines between adjacent items; list items are not paragraphs. Use blank lines only between paragraphs or independently separated blocks. Markdown tables use compact source formatting with `---` separator cells and one surrounding space per cell. Preserve vendored references unchanged.
|
|
130
130
|
- Treat Windows filesystem, named-pipe, lock, heartbeat, and atomic-rename reports as high-signal evidence; reduce them to regressions or explicit platform caveats.
|
|
131
131
|
- Route significant runtime failures through the redacted recent-event recorder. Keep the compact TUI status at generic `error`; details belong in diagnostics.
|
|
132
132
|
|
package/BACKLOG.md
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
_This file owns unresolved project work only. Completed behavior belongs in `CHANGELOG.md`; durable contracts belong in `AGENTS.md` and `/docs`._
|
|
4
4
|
|
|
5
|
-
- [ ] `Post-release command and ordering smoke`: By explicit operator decision, perform live smoke after the combined `0.43.2` hotfix rather than gate publication on it. Verify threadless command routing and Telegram-originated final/artifact/compaction ordering, including follower delivery, without deleting live journals or accepted work. Deterministic verification does not establish the operator's exact incident or constitute live-client evidence. Retain the limits in `docs/outbound.md#publication-limits`; no exactly-once or bounded-pending-queue guarantee is claimed. Installed-runtime replacement remains a separate operator action.
|
|
6
5
|
- [ ] `Live-thread continuity and recovery`: Make loss and restoration of a live thread truthful and safe without discarding accepted work or deleting a currently owned target; the initial cause of the observed stale-thread API failures remains unproven.
|
|
7
6
|
- [ ] Complete the remaining operator-coordinated live smoke beyond the operator-confirmed successful leader Restore: verify follower Restore, inaccessible callbacks, already-absent chooser cleanup, direct stale-target diagnostics, and preservation of accepted local work/active-turn target. Use disposable test threads. The successful leader flow does not independently prove these failure paths or establish the initial stale-thread failure cause.
|
|
8
7
|
- [ ] `Native in-body controls live smoke`: Extend the operator-confirmed successful current-client smoke (singleton CML and mixed JSON/CML rows between paragraphs, ordinary prompt callbacks, labeled disabled control, separate footer callback, and HTML-mode footer fallback) to an independently identified second client, follower routing, blank disabled cells, and app-method dispatch/revision rejection. Do not infer coverage from the confirmed ordinary-button flow; in-body selection uses callback acknowledgement, not body recoloring.
|
package/CHANGELOG.md
CHANGED
|
@@ -2,20 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
> Each release keeps at most 8 outcome records of at most 512 characters.
|
|
4
4
|
|
|
5
|
+
## Unreleased
|
|
6
|
+
|
|
7
|
+
## 0.44.0: Reliable Draft Previews
|
|
8
|
+
|
|
9
|
+
- `Draft Experience`: Enables previews when no current or legacy preference is configured, preserving explicit opt-outs without migration and identifying on as the default in Settings. A two-second leading/trailing throttle sends the first eligible frame immediately and then only the latest accumulated text, producing an operator-accepted rhythm on short and long answers while reducing Telegram rate-limit pressure.
|
|
10
|
+
- `Preview Completion`: Seals draft updates when an assistant message ends, cancels pending throttle timers, and waits only for an already-issued request. Retryable failures defer fresh frames without replaying stale bodies or delaying the final; queued, late, failed, cancelled, and replacement drafts cannot append an obsolete tail.
|
|
11
|
+
- `Publication Ownership`: Gives intermediate and final text one permanent publication owner, eliminating rollover duplication. Background finals capture their originating preview before admission, so delayed work cannot seal, overwrite, await, or deadlock a replacement turn. Rollover barriers remain outside Pi lifecycle hooks and release on failure or cancellation.
|
|
12
|
+
- `Prompt Anchors`: Preserves once-per-turn quoting across delayed text, voice, ordinary uploads, Rich media, and operational delivery. The first successfully delivered permanent response carries the originating prompt anchor; known rejection preserves it for fallback, unknown ACK avoids unsafe replay, and later responses omit the repeated quote.
|
|
13
|
+
|
|
5
14
|
## 0.43.2: Command Lifetime And Publication Order
|
|
6
15
|
|
|
7
16
|
- `All-Tab Command Expiry`: Uses the original Telegram timestamp to expire unselected Threaded Mode command choosers after 60 minutes, settle their deferred source, and reject stale buttons; replay of an expired command creates no new chooser. Active dispatch pauses expiry; failed attempts retain the original deadline, and accepted queue receipts remain protected. Excludes `/thread`, bound threads, classic mode, and invalid timestamps; storage failures retain journal authority.
|
|
8
17
|
- `Selected All-Tab Commands`: Settles still-deferred sources after successful local command dispatch or confirmed follower acceptance, preventing replay after chooser cleanup. Failed follower transfers retain their sources. Background menu rendering remains non-blocking, and commands admitted to the Pi queue retain receipt-governed settlement.
|
|
9
18
|
- `Repeated All-Tab Starts`: A newly delivered chooser for an identical unselected `/start` supersedes older sources from the same user/chat and active admission worker. Different arguments, other commands, and previously selected intents remain separate; superseded callbacks become inert. Failed chooser sends discard their in-memory attempt without settling the journal source, so retries do not exhaust chooser capacity.
|
|
10
|
-
|
|
11
19
|
- `Causal Publication Order`: Serializes bridge-owned assistant blocks, activity disclosures, active-turn finals/artifacts, and automatic compaction notices through their existing activity domain before transport routing. Compaction notices cannot overtake delayed local finals; Pi hooks do not wait for their network delivery. Captures notice/activity target and authority at admission and fences queued publications across session replacement; independent handler queues remain separate.
|
|
12
|
-
|
|
13
20
|
- `Artifact Delivery Authority`: Rechecks active-turn/session authority after voice/file preparation and recording actions. Cancellation suppresses later uploads, provider/text fallbacks, and stale Rich-message ownership writes without discarding the attachment list. Already-issued requests and in-flight synthesis are not undone; ambiguous uploads never authorize replay.
|
|
14
|
-
|
|
15
21
|
- `Final Admission`: Reserves final/error publication at the terminal message boundary and transfers it only to the originating turn before asynchronous config loading. Compaction uses the same queue instead of a separate buffer. Empty outcomes reserve no slot; replacement, preparation failure, settlement, and session reset release unused reservations. Terminal preview cleanup is background and bound to the captured draft.
|
|
16
|
-
|
|
17
22
|
- `Compaction Observation`: Fences superseded timeout callbacks and stale-context terminal hooks so they cannot clear a newer observation. The five-minute fallback remains an observer timeout, not proof of Pi completion or cancellation.
|
|
18
|
-
|
|
19
23
|
- `Activity Replacement`: Fences late config refresh, thinking acknowledgements, tool edits, failures, and settlement cleanup before state mutation or HTML fallback. Old work can no longer overwrite a replacement target/message, erase new tool arguments, or block new thinking output; loss of transport authority also suppresses fallback without requiring a session reset.
|
|
20
24
|
|
|
21
25
|
## 0.43.1: Transport And Preview Continuity
|
package/README.md
CHANGED
|
@@ -256,7 +256,7 @@ Most controls live in Pi commands or the Telegram menu. Environment variables re
|
|
|
256
256
|
| Inbound file limit | `PI_TELEGRAM_INBOUND_FILE_MAX_BYTES`, `TELEGRAM_MAX_FILE_SIZE_BYTES` |
|
|
257
257
|
| Outbound attachment limit | `PI_TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES`, `TELEGRAM_MAX_ATTACHMENT_SIZE_BYTES` |
|
|
258
258
|
|
|
259
|
-
Defaults are chosen for ordinary private-bot use: saved config in `~/.pi/agent`, inbound temp files in `~/.pi/agent/tmp/telegram`, `assistant: { rendering: "rich", draftPreviews:
|
|
259
|
+
Defaults are chosen for ordinary private-bot use: saved config in `~/.pi/agent`, inbound temp files in `~/.pi/agent/tmp/telegram`, `assistant: { rendering: "rich", draftPreviews: true, activity: "verbose", timeInjection: "interval" }` for assistant output and activity, and native Telegram active status for long-running turns.
|
|
260
260
|
|
|
261
261
|
## Extension Platform
|
|
262
262
|
|
package/docs/outbound.md
CHANGED
|
@@ -8,12 +8,18 @@ Text handlers use the portable [Command Template Standard](./command-templates.m
|
|
|
8
8
|
|
|
9
9
|
## Public Assistant Output
|
|
10
10
|
|
|
11
|
-
Every completed `assistant-segment` with `placement: "intermediate"` from a Telegram-originated turn is delivered as its own message to the immutable originating target before the ordinary active-turn final reply. Final and terminal-partial Telegram segments remain owned by active-turn settlement so the final answer, voice, buttons, previews, and artifacts are not duplicated.
|
|
11
|
+
Every completed `assistant-segment` with `placement: "intermediate"` from a Telegram-originated turn is delivered as its own message to the immutable originating target before the ordinary active-turn final reply. Final and terminal-partial Telegram segments remain owned by active-turn settlement so the final answer, voice, buttons, previews, and artifacts are not duplicated. Activity captures the originating turn's optional `replyToMessageId` at agent start; delayed projection uses that captured prompt rather than the current active turn. Once-per-turn quoting remains: the first successfully delivered permanent reply carries the prompt anchor, and subsequent messages/chunks do not repeat it. Native/HTML text, voice, ordinary uploads, Rich media, and operational delivery share the reply reservation owner. Known send rejection releases an unused anchor for fallback; an unknown ACK retains it because Telegram may already have delivered the message. Turn-start reset joins the publication FIFO behind the preceding turn's final instead of clearing its quote state early. Local/autonomous output without a Telegram prompt invents no anchor.
|
|
12
12
|
|
|
13
13
|
While Telegram is connected, local, autonomous, and unclassified extension follow-up Pi work always projects every completed public block—including visible commentary/checkpoints and the final answer—to the instance's authorized target in source order. There is no projection setting or opt-out: disconnecting Telegram is the boundary that stops this companion surface. Both paths consume normalized complete Activity segments rather than raw token deltas, reasoning, or tool traffic.
|
|
14
14
|
|
|
15
15
|
Projected blocks use `assistant.rendering` independently of voice policy. Rich mode sends native Rich Markdown and HTML mode keeps the established HTML renderer; assistant-authored `telegram_button` comments are planned into prompt buttons before either renderer runs, while projection does not synthesize voice or attach queued files merely because Rich rendering is active. Ordered admission revalidates the exact target, profile/token transport generation, leader epoch or follower registration generation, and session generation before each send. Active-turn final delivery joins the shared [activity-publication order](./architecture.md), preserving commentary-before-final order without blocking Pi lifecycle completion. A `commit-unknown` outcome never permits replay.
|
|
16
16
|
|
|
17
|
+
Assistant-message completion seals its preview state: queued follow-up drafts and late updates are suppressed. Native final delivery still waits for the already-issued draft request before sending the permanent answer, so an older draft is not deliberately allowed to overtake the final. Intermediate publication seals and drains its captured preview before sending permanent text. Preview rollover itself sends no permanent message: it carries the preceding delivery boundary and draft identity into the next state without holding the Pi message-start hook. The next draft waits for that publication to settle, including failure or cancellation. Active-turn final delivery captures its preview operations before entering the background queue: it drains only the originating draft, leaves a successor's preview untouched, and cannot wait for a successor publication queued behind itself. Delivery authority is rechecked after the captured flush; if the original preview has been replaced, ordinary final sending remains the queue's responsibility. This does not promise instant delivery or eliminate Telegram/client latency.
|
|
18
|
+
|
|
19
|
+
Assistant previews use a two-second leading/trailing throttle per preview controller, with at most one request in flight. The first eligible snapshot sends immediately; changes inside the window replace pending text, and one trailing timer sends the latest safe snapshot without moving its deadline on every delta. Message/turn rollover preserves the remaining interval. Sealing, clearing, or replacing preview state cancels its timer; final publication drains only an already-issued request and never waits for the throttle deadline.
|
|
20
|
+
|
|
21
|
+
The Bot API client does not replay draft snapshots through API retry backoff. After a retryable draft HTTP failure (`429` or `5xx`), it defers new draft requests for that bot/chat/thread using the existing `Retry-After` or default backoff delay; it stores a deadline, not a body or a background retry. A deferred update does not advance the preview's last-delivered text. Fresh updates can send after the deadline, while a sealed preview cannot resume. Different bots and thread targets have independent cooldowns; credential rotation for the same bot does not bypass its deadline. Permanent replies retain their existing retry policy. Already-issued requests and Telegram/client rendering can still delay visible completion.
|
|
22
|
+
|
|
17
23
|
### Publication Limits
|
|
18
24
|
|
|
19
25
|
The ordered publication queue is process-local, in-memory, and not a durable outbox. Slow preparation or delivery holds later publications from the same instance; Pi lifecycle hooks do not wait for network publication. Terminal preview cleanup also runs in the background, captures the original draft before scheduling, and rechecks draft identity and delivery authority after a pending flush; it cannot clear a replacement draft. A new session drops queued old-generation work and starts its own sequence without waiting for an already-running call. Separate instances have separate publication queues, but still share Telegram/leader transport limits.
|
|
@@ -64,7 +70,7 @@ The voice pipeline is detailed below: configured `type: "voice"` handlers first,
|
|
|
64
70
|
|
|
65
71
|
### Single Rich attachment result
|
|
66
72
|
|
|
67
|
-
When `assistant.rendering` is `"rich"`, a Telegram-originated turn that queues exactly one probe-confirmed PNG/JPEG photo, MP4 video, or MP3 audio file through `telegram_attach` can combine that artifact with the final assistant Markdown in one multipart `sendRichMessage` result. The bridge normalizes the Markdown, adds one `tg://photo`, `tg://video`, or `tg://audio` reference, preserves the
|
|
73
|
+
When `assistant.rendering` is `"rich"`, a Telegram-originated turn that queues exactly one probe-confirmed PNG/JPEG photo, MP4 video, or MP3 audio file through `telegram_attach` can combine that artifact with the final assistant Markdown in one multipart `sendRichMessage` result. The bridge normalizes the Markdown, adds one `tg://photo`, `tg://video`, or `tg://audio` reference, preserves the assigned thread and once-per-turn reply policy, carries assistant-authored inline buttons, and records the returned message id under the exact local/follower ownership scope.
|
|
68
74
|
|
|
69
75
|
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.
|
|
70
76
|
|
package/docs/public-api.md
CHANGED
|
@@ -119,6 +119,7 @@ Hidden/default semantics are represented by absence:
|
|
|
119
119
|
|
|
120
120
|
- `threads.automaticCleanup` defaults to `true`; graceful Pi quit deletes the instance's bound Threaded Mode tab without prompting but preserves the owner slot as independent restart intent. Set it to `false`, or use `🧹 Thread cleanup` in Telegram Settings, to preserve the tab too. A confirmed `/telegram-disconnect`, unlike quit, clears restart ownership. Settings views and cleanup reload shared config before evaluating this switch, so another live Pi instance's update takes effect without restarting. Confirmed leader/follower teardown persists an exact target/runtime-generation cleanup intent before Telegram deletion; an interrupted attempt remains retryable by the current or successor leader under current authority and clears only after confirmed deletion. A same-profile replacement leader first adopts any still-active binding and cancels its superseded cleanup, so startup never deletes and recreates a reusable thread. If a follower's graceful envelope is missed, the leader may create the same fenced cleanup only after its heartbeat is stale, the OS confirms the exact registered PID no longer exists, cleanup remains enabled, and no replacement registration can overtake deletion. Heartbeat loss alone, live/unknown process liveness, IPC failure, and auth failure remain non-destructive. Invalid-config recovery makes the setting unresolved and therefore skips destructive cleanup. Manual `/telegram-disconnect` keeps its confirmation and teardown behavior regardless of this setting.
|
|
121
121
|
- Every complete intermediate assistant text block from a Telegram-originated turn is delivered once to its immutable target before the existing final reply; final and terminal-partial segments stay with settlement to prevent duplicate replies. While Telegram transport remains authorized, local/autonomous work also projects every completed public block, including commentary and the final block. This connected companion projection is always active and excludes token deltas, hidden reasoning, tool calls/arguments/results, empty blocks, unknown sources, 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. Retired top-level and `assistant.proactivePush` keys are ignored; the nested key is removed during config normalization.
|
|
122
|
+
- `assistant.draftPreviews` defaults to `true` when absent; explicit `true` or `false` values, including compatibility aliases, remain authoritative. Reading the default does not persist or migrate configuration. Telegram Settings writes the canonical field.
|
|
122
123
|
- `assistant.activity` accepts exactly `"quiet"`, `"thinking"`, `"tools"`, or `"verbose"`; omitted values default to `"verbose"`, explicit values remain unchanged, and invalid values fail closed to `"quiet"`. Each Pi process reloads the shared file-backed value at `agent-start`, so multi-instance activity isolation never relies on a stale process-local config snapshot. `thinking` shows only provider-exposed thinking, `tools` shows only completed tool activity, and `verbose` shows both. Thinking uses persistent ordinary HTML `sendMessage`/`editMessageText` disclosure with a standard expandable blockquote, a `🧠` header carrying the current Pi thinking level, and bounded redacted text whose inline Markdown renders as Telegram HTML. Tools use native Rich Messages with one header followed by separate closed details and JSON pre blocks for bounded redacted arguments, retained updates, and results/errors. Thinking disables link previews on every HTML send/edit and neutralizes HTTP(S) auto-link detection; Rich tool output disables automatic entity detection, with the same protections retained by its HTML fallback. Consecutive tools coalesce only inside the same ordered activity segment and bounded message. Legacy `assistant.activityVerbosity` is read only when `assistant.activity` is absent and is removed by the next Activity Settings write.
|
|
123
124
|
- 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.
|
|
124
125
|
- Agent activity status is not configurable. While Telegram transport remains authorized, Telegram uses native `sendChatAction(typing)` / product `...active` status as the automatic in-chat work signal for unsettled agent and compaction work. Extension-owned blocking UI prompts pause it and completion resumes it while either work owner remains active.
|
package/index.ts
CHANGED
|
@@ -553,6 +553,24 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
553
553
|
editInteractiveMessage,
|
|
554
554
|
recordRuntimeEvent,
|
|
555
555
|
});
|
|
556
|
+
const nativeMarkdownDraftSender =
|
|
557
|
+
TelegramApi.createTelegramAssistantDraftSender({
|
|
558
|
+
getAssistantRenderingMode: configControls.getAssistantRenderingMode,
|
|
559
|
+
renderMarkdownToHtmlDraft: Replies.renderTelegramMarkdownToHtmlDraft,
|
|
560
|
+
sendMessageDraft,
|
|
561
|
+
sendRichMessageDraft,
|
|
562
|
+
});
|
|
563
|
+
const previewRuntime = Preview.createTelegramAssistantPreviewRuntime({
|
|
564
|
+
getActiveTurn: activeTurnRuntime.get,
|
|
565
|
+
isAssistantMessage: Replies.isAssistantAgentMessage,
|
|
566
|
+
getMessageText: Replies.getAgentMessageText,
|
|
567
|
+
getDefaultReplyToMessageId: activeTurnRuntime.getReplyToMessageId,
|
|
568
|
+
sendDraft: nativeMarkdownDraftSender,
|
|
569
|
+
canSend: configControls.areDraftPreviewsEnabled,
|
|
570
|
+
sendMarkdownReply,
|
|
571
|
+
recordRuntimeEvent,
|
|
572
|
+
...replyTransport,
|
|
573
|
+
});
|
|
556
574
|
const {
|
|
557
575
|
activityRuntime,
|
|
558
576
|
activityVerbosityRuntime,
|
|
@@ -561,6 +579,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
561
579
|
} = Bindings.createTelegramActivityBindingRuntime({
|
|
562
580
|
generation: deliveryGenerationSeed,
|
|
563
581
|
assistantOutput: {
|
|
582
|
+
prepareTelegramPreview: previewRuntime.preparePublication,
|
|
564
583
|
authority: {
|
|
565
584
|
getPreferredTarget: proactivePushTargetGetter,
|
|
566
585
|
getFallbackChatId: proactivePushChatIdGetter,
|
|
@@ -617,27 +636,10 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
617
636
|
sendUserMessage,
|
|
618
637
|
recordRuntimeEvent,
|
|
619
638
|
});
|
|
620
|
-
const
|
|
621
|
-
TelegramApi.createTelegramAssistantDraftSender({
|
|
622
|
-
getAssistantRenderingMode: configControls.getAssistantRenderingMode,
|
|
623
|
-
renderMarkdownToHtmlDraft: Replies.renderTelegramMarkdownToHtmlDraft,
|
|
624
|
-
sendMessageDraft,
|
|
625
|
-
sendRichMessageDraft,
|
|
626
|
-
});
|
|
627
|
-
const previewRuntime = Preview.createTelegramAssistantPreviewRuntime({
|
|
628
|
-
getActiveTurn: activeTurnRuntime.get,
|
|
629
|
-
isAssistantMessage: Replies.isAssistantAgentMessage,
|
|
630
|
-
getMessageText: Replies.getAgentMessageText,
|
|
631
|
-
getDefaultReplyToMessageId: activeTurnRuntime.getReplyToMessageId,
|
|
632
|
-
sendDraft: nativeMarkdownDraftSender,
|
|
633
|
-
canSend: configControls.areDraftPreviewsEnabled,
|
|
634
|
-
sendMarkdownReply,
|
|
635
|
-
recordRuntimeEvent,
|
|
636
|
-
...replyTransport,
|
|
637
|
-
});
|
|
638
|
-
const { finalizeMarkdownPreview } =
|
|
639
|
+
const { finalizeMarkdownPreview, preparePreviewDelivery } =
|
|
639
640
|
Outbound.createTelegramOutboundTextPreviewRuntime({
|
|
640
641
|
finalizeMarkdownPreview: previewRuntime.finalizeMarkdown,
|
|
642
|
+
preparePreviewDelivery: previewRuntime.prepareDelivery,
|
|
641
643
|
execCommand: CommandTemplates.execCommandTemplate,
|
|
642
644
|
getHandlers: configStore.getOutboundHandlers,
|
|
643
645
|
recordRuntimeEvent,
|
|
@@ -1397,6 +1399,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1397
1399
|
deleteMessage: deleteTelegramMessage,
|
|
1398
1400
|
sendGuestReply,
|
|
1399
1401
|
finalizeMarkdownPreview,
|
|
1402
|
+
preparePreviewDelivery,
|
|
1400
1403
|
proactivePushTargetGetter,
|
|
1401
1404
|
getAssistantRenderingMode: configControls.getAssistantRenderingMode,
|
|
1402
1405
|
recordMessageOwnership: messageOwnershipRuntime.recordLocal,
|
package/lib/activity.ts
CHANGED
|
@@ -32,6 +32,7 @@ export interface TelegramActivityEnvelope {
|
|
|
32
32
|
sequence: number;
|
|
33
33
|
source: TelegramActivitySource;
|
|
34
34
|
target?: TelegramActivityTarget;
|
|
35
|
+
replyToMessageId?: number;
|
|
35
36
|
timestamp: number;
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -422,8 +423,8 @@ export function createTelegramActivityBridgeRuntime(deps: {
|
|
|
422
423
|
recordInputSource(source) {
|
|
423
424
|
getRuntime()?.recordInputSource(source);
|
|
424
425
|
},
|
|
425
|
-
onAgentStart(target) {
|
|
426
|
-
getRuntime()?.onAgentStart(target);
|
|
426
|
+
onAgentStart(target, replyToMessageId) {
|
|
427
|
+
getRuntime()?.onAgentStart(target, replyToMessageId);
|
|
427
428
|
},
|
|
428
429
|
onAssistantEvent(event) {
|
|
429
430
|
getRuntime()?.onAssistantEvent(event);
|
|
@@ -492,7 +493,7 @@ export type TelegramAssistantStreamEvent =
|
|
|
492
493
|
export interface TelegramActivityRuntime {
|
|
493
494
|
onSessionStart?: () => void;
|
|
494
495
|
recordInputSource: (source: TelegramActivityInputSource) => void;
|
|
495
|
-
onAgentStart: (activeTelegramTarget?: TelegramActivityTarget) => void;
|
|
496
|
+
onAgentStart: (activeTelegramTarget?: TelegramActivityTarget, replyToMessageId?: number) => void;
|
|
496
497
|
onAssistantEvent: (event: TelegramAssistantStreamEvent) => void;
|
|
497
498
|
onAssistantMessageEnd: (stopReason?: string) => void;
|
|
498
499
|
onToolStart: (event: {
|
|
@@ -549,6 +550,7 @@ export function createTelegramActivityRuntime(deps: {
|
|
|
549
550
|
let activityId: string | undefined;
|
|
550
551
|
let activitySource: TelegramActivitySource = "unknown";
|
|
551
552
|
let activityTarget: TelegramActivityTarget | undefined;
|
|
553
|
+
let activityReplyToMessageId: number | undefined;
|
|
552
554
|
let sequence = 0;
|
|
553
555
|
let pendingInputSource: TelegramActivityInputSource = "unknown";
|
|
554
556
|
let pendingAssistantSegment: PendingAssistantSegment | undefined;
|
|
@@ -584,6 +586,7 @@ export function createTelegramActivityRuntime(deps: {
|
|
|
584
586
|
sequence,
|
|
585
587
|
source: activitySource,
|
|
586
588
|
...(activityTarget ? { target: activityTarget } : {}),
|
|
589
|
+
...(activityReplyToMessageId !== undefined ? { replyToMessageId: activityReplyToMessageId } : {}),
|
|
587
590
|
timestamp: now(),
|
|
588
591
|
} as TelegramActivityEvent;
|
|
589
592
|
try {
|
|
@@ -610,6 +613,7 @@ export function createTelegramActivityRuntime(deps: {
|
|
|
610
613
|
activityId = undefined;
|
|
611
614
|
activitySource = "unknown";
|
|
612
615
|
activityTarget = undefined;
|
|
616
|
+
activityReplyToMessageId = undefined;
|
|
613
617
|
sequence = 0;
|
|
614
618
|
pendingAssistantSegment = undefined;
|
|
615
619
|
compactionInProgress = false;
|
|
@@ -627,9 +631,10 @@ export function createTelegramActivityRuntime(deps: {
|
|
|
627
631
|
recordInputSource(source) {
|
|
628
632
|
pendingInputSource = source;
|
|
629
633
|
},
|
|
630
|
-
onAgentStart(activeTelegramTarget) {
|
|
634
|
+
onAgentStart(activeTelegramTarget, replyToMessageId) {
|
|
631
635
|
abandonCompaction();
|
|
632
636
|
ensureActivity(activeTelegramTarget);
|
|
637
|
+
activityReplyToMessageId = activitySource === "telegram" ? replyToMessageId : undefined;
|
|
633
638
|
emit({ type: "agent-start" });
|
|
634
639
|
},
|
|
635
640
|
onAssistantEvent(event) {
|
|
@@ -804,7 +809,13 @@ export interface TelegramAssistantOutputRuntime {
|
|
|
804
809
|
stop: () => void;
|
|
805
810
|
}
|
|
806
811
|
|
|
812
|
+
export interface TelegramAssistantOutputPreparation {
|
|
813
|
+
wait: () => Promise<void>;
|
|
814
|
+
settle: () => void;
|
|
815
|
+
}
|
|
816
|
+
|
|
807
817
|
export function createTelegramAssistantOutputRuntime<TAuthority = undefined>(deps: {
|
|
818
|
+
prepareSend?: (event: TelegramAssistantSegmentEvent) => TelegramAssistantOutputPreparation | undefined;
|
|
808
819
|
enqueue?: TelegramActivityPublicationRuntime["enqueue"];
|
|
809
820
|
captureAuthority?: () => TAuthority;
|
|
810
821
|
isAuthorityActive?: (authority: TAuthority) => boolean;
|
|
@@ -843,6 +854,7 @@ export function createTelegramAssistantOutputRuntime<TAuthority = undefined>(dep
|
|
|
843
854
|
admitted.add(key);
|
|
844
855
|
const admittedGeneration = generation;
|
|
845
856
|
const admittedAuthority = deps.captureAuthority?.();
|
|
857
|
+
const preparation = deps.prepareSend?.(event);
|
|
846
858
|
const enqueue = deps.enqueue ?? ((task: () => Promise<void>) => tail.then(task));
|
|
847
859
|
tail = enqueue(async () => {
|
|
848
860
|
const isAdmittedAuthorityActive = () =>
|
|
@@ -853,6 +865,8 @@ export function createTelegramAssistantOutputRuntime<TAuthority = undefined>(dep
|
|
|
853
865
|
deps.isAuthorityActive(admittedAuthority as TAuthority));
|
|
854
866
|
if (!isAdmittedAuthorityActive() || !deps.canDeliver(event)) return;
|
|
855
867
|
try {
|
|
868
|
+
if (preparation) await preparation.wait();
|
|
869
|
+
if (!isAdmittedAuthorityActive() || !deps.canDeliver(event)) return;
|
|
856
870
|
await deps.send(
|
|
857
871
|
event,
|
|
858
872
|
admittedAuthority as TAuthority,
|
|
@@ -861,7 +875,7 @@ export function createTelegramAssistantOutputRuntime<TAuthority = undefined>(dep
|
|
|
861
875
|
} catch (error) {
|
|
862
876
|
deps.recordFailure?.(event, error);
|
|
863
877
|
}
|
|
864
|
-
});
|
|
878
|
+
}).finally(() => preparation?.settle());
|
|
865
879
|
},
|
|
866
880
|
waitForIdle() {
|
|
867
881
|
return tail;
|
package/lib/bindings.ts
CHANGED
|
@@ -353,6 +353,7 @@ export function createTelegramAssistantOutputBindingRuntime<
|
|
|
353
353
|
typeof OutboundHandlers.createTelegramAssistantOutputSender<TTransportStamp>
|
|
354
354
|
>[0];
|
|
355
355
|
waitForActivityIdle?: () => Promise<void>;
|
|
356
|
+
prepareTelegramPreview?: () => Activity.TelegramAssistantOutputPreparation | undefined;
|
|
356
357
|
enqueue?: Activity.TelegramActivityPublicationRuntime["enqueue"];
|
|
357
358
|
recordRuntimeEvent: TelegramRuntimeEventRecorder;
|
|
358
359
|
}): TelegramAssistantOutputBindingRuntime<TTransportStamp> {
|
|
@@ -366,6 +367,7 @@ export function createTelegramAssistantOutputBindingRuntime<
|
|
|
366
367
|
const runtime = Activity.createTelegramAssistantOutputRuntime({
|
|
367
368
|
...authority,
|
|
368
369
|
enqueue: deps.enqueue,
|
|
370
|
+
prepareSend: (event) => event.source === "telegram" ? deps.prepareTelegramPreview?.() : undefined,
|
|
369
371
|
async send(event, authority, isAuthorityActive) {
|
|
370
372
|
await deps.waitForActivityIdle?.();
|
|
371
373
|
if (!isAuthorityActive()) return;
|
|
@@ -778,6 +780,7 @@ interface TelegramLifecycleBindingDeps {
|
|
|
778
780
|
Keyboard.TelegramInlineKeyboardMarkup
|
|
779
781
|
>["sendGuestReply"]
|
|
780
782
|
>;
|
|
783
|
+
preparePreviewDelivery?: Queue.TelegramAgentEndRuntimeDeps<Queue.PendingTelegramTurn>["preparePreviewDelivery"];
|
|
781
784
|
finalizeMarkdownPreview: Queue.TelegramAgentEndHookRuntimeDeps<
|
|
782
785
|
Queue.PendingTelegramTurn,
|
|
783
786
|
Pi.ExtensionContext,
|
|
@@ -829,6 +832,7 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
829
832
|
answerGuestQuery,
|
|
830
833
|
deleteMessage,
|
|
831
834
|
sendGuestReply,
|
|
835
|
+
preparePreviewDelivery,
|
|
832
836
|
finalizeMarkdownPreview,
|
|
833
837
|
proactivePushTargetGetter,
|
|
834
838
|
getAssistantRenderingMode,
|
|
@@ -1030,6 +1034,7 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
1030
1034
|
cancel: reservation.cancel,
|
|
1031
1035
|
};
|
|
1032
1036
|
},
|
|
1037
|
+
preparePreviewDelivery,
|
|
1033
1038
|
preparePreviewClear: previewRuntime.prepareClear,
|
|
1034
1039
|
clearPreview: previewRuntime.clear,
|
|
1035
1040
|
setPreviewPendingText: previewRuntime.setPendingText,
|
|
@@ -1052,6 +1057,7 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
1052
1057
|
Lifecycle.setResetTransportReplyDedup(Replies.resetTransportReplyDedup);
|
|
1053
1058
|
const agentStartWithDedupReset = Lifecycle.createAgentStartDedupHook(
|
|
1054
1059
|
agentLifecycleHooks.onAgentStart,
|
|
1060
|
+
scheduleActiveTurnDelivery,
|
|
1055
1061
|
);
|
|
1056
1062
|
let uiPromptActive = false;
|
|
1057
1063
|
const startAgentActivityTypingLoop = (ctx: Pi.ExtensionContext): boolean => {
|
|
@@ -1187,7 +1193,8 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
1187
1193
|
agentWorkActive = true;
|
|
1188
1194
|
cancelPendingFinalPublication();
|
|
1189
1195
|
await agentStartWithDedupReset(event, ctx);
|
|
1190
|
-
|
|
1196
|
+
const turn = activeTurnRuntime.get();
|
|
1197
|
+
activityRuntime.onAgentStart(turn?.target, turn?.replyToMessageId);
|
|
1191
1198
|
startAgentActivityTypingLoop(ctx);
|
|
1192
1199
|
},
|
|
1193
1200
|
async onToolExecutionStart(event, ctx) {
|
|
@@ -1233,6 +1240,7 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
1233
1240
|
onMessageEnd(event, ctx) {
|
|
1234
1241
|
if (!isSessionContextActive(ctx)) return;
|
|
1235
1242
|
if (event.message.role === "assistant") {
|
|
1243
|
+
previewRuntime.seal();
|
|
1236
1244
|
activityRuntime.onAssistantMessageEnd(event.message.stopReason);
|
|
1237
1245
|
}
|
|
1238
1246
|
if (event.message.role !== "assistant" || event.message.stopReason === "toolUse" || event.message.stopReason === "aborted") return;
|
package/lib/config.ts
CHANGED
package/lib/delivery.ts
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
type TelegramInlineKeyboardMarkup,
|
|
11
11
|
} from "./keyboard.ts";
|
|
12
12
|
import {
|
|
13
|
-
|
|
13
|
+
withTelegramReplyParameters,
|
|
14
14
|
renderTelegramMessage,
|
|
15
15
|
} from "./replies.ts";
|
|
16
16
|
import {
|
|
@@ -724,23 +724,23 @@ export function createTelegramBridgeDeliveryRuntime(
|
|
|
724
724
|
},
|
|
725
725
|
async sendChunk(target, chunk, options) {
|
|
726
726
|
assertTransportActive();
|
|
727
|
-
const
|
|
728
|
-
target.chatId,
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
727
|
+
const sent = await withTelegramReplyParameters(
|
|
728
|
+
target.chatId, options.replyToMessageId, target,
|
|
729
|
+
(replyParameters) => {
|
|
730
|
+
const body = {
|
|
731
|
+
chat_id: target.chatId,
|
|
732
|
+
text: chunk.text,
|
|
733
|
+
...(chunk.parseMode === "html" ? { parse_mode: "HTML" as const } : {}),
|
|
734
|
+
...getTelegramTargetThreadParams(target),
|
|
735
|
+
...(replyParameters ? { reply_parameters: replyParameters } : {}),
|
|
736
|
+
...(options.replyMarkup ? { reply_markup: options.replyMarkup } : {}),
|
|
737
|
+
};
|
|
738
|
+
return deps.api.sendMessage(
|
|
739
|
+
target.threadId === undefined
|
|
740
|
+
? markTelegramBusAggregateDelivery(body)
|
|
741
|
+
: body,
|
|
742
|
+
);
|
|
743
|
+
},
|
|
744
744
|
);
|
|
745
745
|
assertTransportActive();
|
|
746
746
|
deps.recordOwnership({
|
package/lib/lifecycle.ts
CHANGED
|
@@ -37,9 +37,15 @@ export function setResetTransportReplyDedup(fn: () => void): void {
|
|
|
37
37
|
|
|
38
38
|
export function createAgentStartDedupHook(
|
|
39
39
|
inner: (event: AgentStartEvent, ctx: ExtensionContext) => Promise<void>,
|
|
40
|
+
schedulePublication?: (task: () => Promise<void>) => void,
|
|
40
41
|
): (event: AgentStartEvent, ctx: ExtensionContext) => Promise<void> {
|
|
41
42
|
return async (event, ctx) => {
|
|
42
|
-
|
|
43
|
+
const reset = resetTransportReplyDedupFn;
|
|
44
|
+
if (reset) {
|
|
45
|
+
// A new turn must not erase the anchor of a final still ahead in the FIFO.
|
|
46
|
+
if (schedulePublication) schedulePublication(async () => { reset(); });
|
|
47
|
+
else reset();
|
|
48
|
+
}
|
|
43
49
|
return inner(event, ctx);
|
|
44
50
|
};
|
|
45
51
|
}
|
package/lib/menu-settings.ts
CHANGED
|
@@ -184,8 +184,8 @@ export function buildDraftPreviewsSettingsText(enabled: boolean): string {
|
|
|
184
184
|
"",
|
|
185
185
|
"Show live answer drafts while the model is answering.",
|
|
186
186
|
"",
|
|
187
|
-
"<code>-</code> <code>on</code
|
|
188
|
-
"<code>-</code> <code>off</code
|
|
187
|
+
"<code>-</code> <code>on</code> (default): stream safe Telegram Rich Draft frames before the final answer.",
|
|
188
|
+
"<code>-</code> <code>off</code>: show native active status, then send one final answer.",
|
|
189
189
|
].join("\n");
|
|
190
190
|
}
|
|
191
191
|
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
TELEGRAM_MESSAGE_PROMPT_SNIPPET,
|
|
22
22
|
} from "./prompts.ts";
|
|
23
23
|
import {
|
|
24
|
-
|
|
24
|
+
withTelegramReplyParameters,
|
|
25
25
|
normalizeTelegramNativeMarkdown,
|
|
26
26
|
} from "./replies.ts";
|
|
27
27
|
import {
|
|
@@ -189,18 +189,10 @@ export function planTelegramRichOutboundAttachment(options: {
|
|
|
189
189
|
],
|
|
190
190
|
skip_entity_detection: true,
|
|
191
191
|
};
|
|
192
|
-
const replyParameters =
|
|
193
|
-
options.turn.replyToMessageId > 0
|
|
194
|
-
? JSON.stringify({
|
|
195
|
-
message_id: options.turn.replyToMessageId,
|
|
196
|
-
allow_sending_without_reply: true,
|
|
197
|
-
})
|
|
198
|
-
: undefined;
|
|
199
192
|
return {
|
|
200
193
|
method: "sendRichMessage",
|
|
201
194
|
fields: {
|
|
202
195
|
chat_id: String(options.turn.chatId),
|
|
203
|
-
...(replyParameters ? { reply_parameters: replyParameters } : {}),
|
|
204
196
|
...getTelegramMultipartTargetFields(options.turn.target),
|
|
205
197
|
rich_message: JSON.stringify(richMessage),
|
|
206
198
|
...(options.replyMarkup
|
|
@@ -230,12 +222,15 @@ export function createTelegramRichOutboundAttachmentSender(
|
|
|
230
222
|
});
|
|
231
223
|
if (!plan) return false;
|
|
232
224
|
try {
|
|
233
|
-
const result = await
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
225
|
+
const result = await withTelegramReplyParameters(
|
|
226
|
+
turn.chatId, turn.replyToMessageId, turn.target,
|
|
227
|
+
(replyParameters) => deps.sendMultipart(
|
|
228
|
+
plan.method,
|
|
229
|
+
{ ...plan.fields, ...(replyParameters ? { reply_parameters: JSON.stringify(replyParameters) } : {}) },
|
|
230
|
+
plan.fileField,
|
|
231
|
+
plan.filePath,
|
|
232
|
+
plan.fileName,
|
|
233
|
+
),
|
|
239
234
|
);
|
|
240
235
|
const messageId =
|
|
241
236
|
result && typeof result === "object" &&
|
|
@@ -964,21 +959,19 @@ export async function sendQueuedTelegramOutboundAttachments(
|
|
|
964
959
|
const isPhoto = isTelegramOutboundPhotoAttachmentPath(attachment.path);
|
|
965
960
|
const method = isPhoto ? "sendPhoto" : "sendDocument";
|
|
966
961
|
const fieldName = isPhoto ? "photo" : "document";
|
|
967
|
-
|
|
968
|
-
turn.chatId,
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
attachment.path,
|
|
981
|
-
attachment.fileName,
|
|
962
|
+
await withTelegramReplyParameters(
|
|
963
|
+
turn.chatId, turn.replyToMessageId, turn.target,
|
|
964
|
+
(replyParameters) => deps.sendMultipart(
|
|
965
|
+
method,
|
|
966
|
+
{
|
|
967
|
+
chat_id: String(turn.chatId),
|
|
968
|
+
...(replyParameters ? { reply_parameters: JSON.stringify(replyParameters) } : {}),
|
|
969
|
+
...getTelegramMultipartTargetFields(turn.target),
|
|
970
|
+
},
|
|
971
|
+
fieldName,
|
|
972
|
+
attachment.path,
|
|
973
|
+
attachment.fileName,
|
|
974
|
+
),
|
|
982
975
|
);
|
|
983
976
|
} catch (error) {
|
|
984
977
|
if (deps.isDeliveryActive?.() === false) return;
|
package/lib/outbound-voice.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { unlink } from "node:fs/promises";
|
|
|
8
8
|
import { basename, extname } from "node:path";
|
|
9
9
|
|
|
10
10
|
import { assertTelegramInlineKeyboardCallbackData } from "./keyboard.ts";
|
|
11
|
-
import {
|
|
11
|
+
import { withTelegramReplyParameters } from "./replies.ts";
|
|
12
12
|
import {
|
|
13
13
|
getTelegramTargetThreadParams,
|
|
14
14
|
type TelegramTarget,
|
|
@@ -80,17 +80,6 @@ export interface TelegramVoiceReplySenderPorts<THandler = unknown> {
|
|
|
80
80
|
getProgrammaticVoiceHandlers?: () => TelegramOutboundProgrammaticVoiceHandler[];
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
function buildVoiceReplyParameters(
|
|
84
|
-
chatId: number,
|
|
85
|
-
replyToPrompt: boolean | undefined,
|
|
86
|
-
replyToMessageId: number | undefined,
|
|
87
|
-
target?: TelegramTarget,
|
|
88
|
-
): string | undefined {
|
|
89
|
-
if (replyToPrompt === false || replyToMessageId === undefined)
|
|
90
|
-
return undefined;
|
|
91
|
-
return buildTelegramMultipartReplyParameters(chatId, replyToMessageId, target);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
83
|
async function ensureTelegramVoiceFileFormat(
|
|
95
84
|
filePath: string,
|
|
96
85
|
): Promise<string> {
|
|
@@ -131,36 +120,33 @@ export function createTelegramVoiceReplySender<THandler = unknown>(
|
|
|
131
120
|
if (deps.isDeliveryActive?.() === false) return;
|
|
132
121
|
await sendVoiceChatAction(deps, turn.chatId);
|
|
133
122
|
if (deps.isDeliveryActive?.() === false) return;
|
|
134
|
-
|
|
135
|
-
turn.chatId,
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
"voice",
|
|
162
|
-
voiceFilePath,
|
|
163
|
-
basename(voiceFilePath),
|
|
123
|
+
await withTelegramReplyParameters(
|
|
124
|
+
turn.chatId, options?.replyToPrompt === false ? undefined : turn.replyToMessageId, turn.target,
|
|
125
|
+
(replyParameters) => deps.sendMultipart(
|
|
126
|
+
"sendVoice",
|
|
127
|
+
{
|
|
128
|
+
chat_id: String(turn.chatId),
|
|
129
|
+
...(replyParameters ? { reply_parameters: JSON.stringify(replyParameters) } : {}),
|
|
130
|
+
...(turn.target
|
|
131
|
+
? Object.fromEntries(
|
|
132
|
+
Object.entries(getTelegramTargetThreadParams(turn.target)).map(
|
|
133
|
+
([key, value]) => [key, String(value)],
|
|
134
|
+
),
|
|
135
|
+
)
|
|
136
|
+
: {}),
|
|
137
|
+
...(options?.replyMarkup !== undefined && options.replyMarkup !== null
|
|
138
|
+
? {
|
|
139
|
+
reply_markup:
|
|
140
|
+
typeof options.replyMarkup === "string"
|
|
141
|
+
? options.replyMarkup
|
|
142
|
+
: JSON.stringify(options.replyMarkup),
|
|
143
|
+
}
|
|
144
|
+
: {}),
|
|
145
|
+
},
|
|
146
|
+
"voice",
|
|
147
|
+
voiceFilePath,
|
|
148
|
+
basename(voiceFilePath),
|
|
149
|
+
),
|
|
164
150
|
);
|
|
165
151
|
};
|
|
166
152
|
|