@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 CHANGED
@@ -80,14 +80,14 @@ Use the relevant local skill before non-trivial work in its domain. Keep skill o
80
80
  - Foreign forwarding settles as `accepted`, `retryable`, or `terminal-rejected`. Only an authenticated acknowledgement carrying the expected `deliveryId` and `sourceUpdateId` releases leader journal authority. Negative, missing, stale, mismatched, or capacity-failed settlement remains durable; callback error answers are side effects only.
81
81
  - A forwarding delivery id is stable across registration replacement and derives from envelope kind, source `update_id`, and stable recipient binding. Runtime instance and registration generation remain separate attempt fences. Persisted message ownership carries the stable binding so replay can rebind only to its current authenticated registration.
82
82
  - A queued receipt persists its acquiring runtime instance, OS pid/process-birth identity, session generation, acquisition id, and acquisition time. Only exact authority may settle or discard it. Same-process session replacement may reconstruct the claim and the original process may settle after transport ownership moves; a foreign process may neither replay nor settle it through generic removal or a copied acquisition id.
83
- - Startup and elapsed time are not owner-death proof; queued authority has no time lease. Dead-owner recovery groups the complete receipt and transactionally rechecks pid liveness plus process-birth identity: only an absent PID or mismatched stable Linux/macOS birth proof returns all sources to `pending`; a matching proof is `alive`, while Windows or inaccessible birth metadata is `unverifiable`, and both non-dead outcomes keep authority queued. The live-transfer contract is authenticated offer → exact-generation bounded payload staging → recipient CAS acceptance → exact receipt-and-owner ACK → donor removal → recipient readiness. The offer freezes donor settlement/recovery; controls rebuild local closures; negative/mismatched pre-acceptance ACK cancels only an unaccepted offer and retains donor work; a lost post-acceptance ACK cannot cancel recipient authority and leaves donor memory frozen for explicit reconciliation.
83
+ - Startup and elapsed time are not owner-death proof; queued authority has no time lease. Dead-owner cleanup groups the complete receipt and transactionally rechecks pid liveness plus process-birth identity: only an absent PID or mismatched stable Linux/macOS birth proof discards all session-owned sources without replay; a matching proof is `alive`, while Windows or inaccessible birth metadata is `unverifiable`, and both non-dead outcomes keep authority queued. The live-transfer contract is authenticated offer → exact-generation bounded payload staging → recipient CAS acceptance → exact receipt-and-owner ACK → donor removal → recipient readiness. The offer freezes donor settlement/recovery; controls rebuild local closures; negative/mismatched pre-acceptance ACK cancels only an unaccepted offer and retains donor work; a lost post-acceptance ACK cannot cancel recipient authority and leaves donor memory frozen for explicit reconciliation.
84
84
  - Execution failures persist bounded diagnostics and attempt state as `retry-wait`. Automatic retry continues indefinitely with exponential `1s → 2s → 4s → 8s → 16s → 32s → 60s` delay capped at 60 seconds; later independent updates continue draining, durable authority is never silently discarded, and legacy `failed` entries resume automatically at startup. Snapshot-plus-segment journals compact only after 256 unapplied revisions or 4 MiB; snapshot-first cleanup tolerates redundant segments, and empty authority may atomically rebind bot/profile identity. Missing snapshots left by the retired broad temp cleanup rebuild only from a complete provably empty segment chain, while revisionless snapshots may recover from a validated later segment predecessor; otherwise the snapshot and segments move atomically under `tmp/telegram/recovery/` before a fresh journal is published and startup continues with informational recovery evidence.
85
85
  - An unresolved reaction delays only the exact governed queue item identified by chat/message sources, not unrelated queue work. Queue receipt publication follows in-memory append and precedes dispatch request; receipt-bearing turns remain queued until every exact source commits.
86
86
  - The detailed implementation and release gates live in [`docs/architecture.md`](./docs/architecture.md), [`docs/multi-instance-bus.md`](./docs/multi-instance-bus.md), and [`BACKLOG.md`](./BACKLOG.md).
87
87
 
88
88
  ### 4.4 Queue, Delivery, And User Surfaces
89
89
 
90
- - Queue lane/kind admission is explicit. Dispatch waits for active-turn, pending-dispatch, control, compaction, `ctx.isIdle()`, and Pi pending-message guards; a dispatched prompt stays queued until `agent_start` consumes it. Each prompt is one object with one active lane and no reserved return slot. Normal and Priority are separate FIFO lanes: crossing lanes removes it from the source and appends it at the destination tail, while Keep/Skip and same-category emoji changes preserve lane position. Complete reaction sets independently derive Priority from recognized positive emoji and Skip from recognized negative emoji; both may coexist, Skip wins only at dispatch, and suppressed turns remain visible without blocking unrelated work.
90
+ - Queue lane/kind admission is explicit. Dispatch waits for active-turn, pending-dispatch, control, compaction, `ctx.isIdle()`, and Pi pending-message guards; a dispatched prompt stays queued until `agent_start` consumes it. Each prompt is one object with one active lane and no reserved return slot. Normal and Priority are separate FIFO lanes: crossing lanes removes it from the source and appends it at the destination tail, while Keep/Skip and same-category emoji changes preserve lane position. Complete reaction sets independently derive Priority from recognized positive emoji and Skip from recognized negative emoji; both may coexist, suppressed turns retain durable receipts while waiting, and Skip settles them only when the prompt reaches dispatch before dropping it without inference. Suppressed turns remain visible at a struck-through physical ordinal without contributing to executable queue counters, while graceful session shutdown discards all remaining queue authority before clearing memory.
91
91
  - `/stop`, `/abort`, `/next`, and `/continue` respectively reset+abort, abort while preserving queue, force the next turn, and enqueue a control-lane continuation. Abort-history folding applies only to Telegram-owned active turns.
92
92
  - Telegram extension side effects must not hold Pi's core lifecycle hostage after semantic completion. Preserve ordering in extension-owned background work, record failures, and fence target/profile/transport/session authority.
93
93
  - Complete assistant/guest model answers use Telegram-native Rich Markdown. Harness-owned menus, status, diagnostics, thinking, and tool evidence remain explicit HTML/plain or their documented native surface. Preserve literal code and structurally safe chunking; never split invalid markup.
@@ -125,7 +125,7 @@ The detailed map is canonical in [`docs/architecture.md`](./docs/architecture.md
125
125
  - Keep interfaces consistent with their owning exported contract. Use local structural `*Like`/view types only for deliberate narrow projections, not duplicate source-of-truth models.
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
- - 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.
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 heading with terminal punctuation; callback alerts preserve equivalent emoji-led plain text because Telegram does not support rich formatting there.
129
129
  - 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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  > Each release keeps at most 8 outcome records of at most 512 characters.
4
4
 
5
+ ## 0.39.0: Session-Owned Queue Lifecycle
6
+
7
+ - `Queue Counters`: Excludes Skip-marked prompts immediately from the shared executable queue count used by the Pi status bar and Telegram main menu while preserving physical queue position, deferred drop semantics, and reversible Keep behavior.
8
+ - `Skip Settlement`: Preserves durable receipts while a skipped prompt waits, then settles them atomically when that prompt reaches dispatch before dropping it without inference; settlement failure retains the head instead of creating replay ambiguity.
9
+ - `Session-Owned Queue`: Graceful shutdown discards all remaining queue receipts before clearing memory, while startup atomically discards receipts whose former process is proven dead instead of replaying them; replacement and unrelated sessions therefore start without inherited work.
10
+ - `Consistent Information Headings`: Standardizes standalone progress, success, empty, busy, unavailable, cancellation, and failure notices as fully bold emoji-led headings, while callback alerts retain equivalent plain text and automatic compaction waits for the preceding final answer before announcing its start.
11
+ - `Queue Menu`: Keeps skipped prompts visible in place with their physical ordinal struck through in list and detail views, so deferred removal is legible without renumbering or moving later work.
12
+ - `Agent Context`: Routes connected and Telegram-turn guidance across `telegram-bridge`, `generated-control-surface`, and `generative-apps` by activation role, asks the agent to load only missing Skill instructions, and removes Telegram tool context while transport is unavailable.
13
+ - `Bot API Reference`: Updates the vendored Telegram Bot API reference and lookup Skill through Bot API 10.3 while retaining the 10.0–10.3 freshness history and validated symbol, anchor, and line indexes.
14
+
15
+ ## 0.38.0: Coherent Voice Reply Policy
16
+
17
+ - `Voice Reply Policy`: Renames the default user-facing `hidden` mode to `manual` while retaining `hidden` as a read-only configuration and callback compatibility alias. `manual`, `mirror`, and `always` remain the complete policy set: explicit `telegram_voice`, modality mirroring, or automatic voice replacement.
18
+ - `Voice Provider Contract`: Removes the redundant `voice.sendTranscript` configuration, `getTelegramVoiceSendTranscript()` public helper, provider-returned `transcriptText`, and voice-caption path. Synthesis providers now return only an OGG/Opus path or `undefined`; text-plus-voice remains an explicit agent composition rather than a parallel automatic policy.
19
+
5
20
  ## 0.37.2: Follower Recovery Delivery Hotfix
6
21
 
7
22
  - `Follower Recovery Delivery`: Holds follower Bot API calls behind a bounded registration wait when heartbeat recovery temporarily clears local authority, then sends once with the restored exact generation. Calls still fail closed if registration is not restored, and acknowledgement ambiguity remains non-retryable.
package/README.md CHANGED
@@ -130,7 +130,7 @@ Enable the optional capabilities the bridge needs in the [@BotFather](https://t.
130
130
  | Inbound files | Download inbound files to the Pi agent temp directory with size limits. | Screenshots, PDFs, datasets, and artifacts enter Pi as inspectable local files. |
131
131
  | Outbound artifacts | Return generated files through `telegram_attach` during active turns or explicit direct delivery. | Agents send real artifacts as files, not pasted blobs. |
132
132
  | Voice input | Route audio through configured command-template handlers, programmatic handlers, or STT providers. | Voice notes become usable prompt context. |
133
- | Voice output | Choose `hidden`, `mirror`, or `always`; active automatic turns carry one compact `[voice] delivery: automatic voice` line, while explicit `telegram_voice` remains available. | Voice policy stays dynamic and model-legible without duplicating the full action contract in every prompt. |
133
+ | Voice output | Choose `manual`, `mirror`, or `always`; active automatic turns carry one compact `[voice] delivery: automatic voice` line, while explicit `telegram_voice` remains available. | Voice policy stays dynamic and model-legible without duplicating the full action contract in every prompt. |
134
134
  | Buttons | Turn top-level `telegram_button` comments into inline buttons. | Assistant-authored choices become native Telegram interactions. |
135
135
  | Generative Apps | Install or explicitly replace a reviewed `.mjs` application whose generated JSON button view may mix direct `app::method` actions with ordinary model prompts. | Repeated games, controls, tutors, and adapters compile routine interaction without losing selective model interpretation, explanation, or adaptation. |
136
136
  | Callback routing | Route known callbacks to the owner extension and unknown callbacks back into Pi. | Companion extensions can build UI without polling Telegram themselves. |
@@ -199,7 +199,7 @@ Queue policy:
199
199
  - One prompt is one queue object with exactly one current lane and one current position; it never reserves a shadow place in the other lane.
200
200
  - Priority and Normal are separate FIFO lanes; Priority dispatches first.
201
201
  - Moving `Normal → Priority` removes the prompt from Normal and places it at the Priority tail. Moving `Priority → Normal` removes it from Priority and places it at the Normal tail; no former position is restored.
202
- - Keep/Skip never changes lane position. Skip remains reversible while waiting and drops the prompt without a model turn only when dispatch reaches it.
202
+ - Keep/Skip never changes lane position. Skip preserves durable authority while waiting so Keep remains reversible, then settles that authority and drops the prompt without a model turn when dispatch reaches it. Skipped prompts stay visible at their physical queue position with a struck-through ordinal, but are excluded immediately from the executable queue count shown in both the Pi status bar and Telegram main menu. Graceful session shutdown discards all remaining queued authority, so a new session starts empty.
203
203
  - Reactions control two independent dimensions; changing one category preserves the other:
204
204
  - `Positive`: `👍`, `⚡️`, `❤️`, `🕊`, `🔥` — controls Priority.
205
205
  - `Negative`: `👎`, `👻`, `💔`, `💩`, `🗑` — controls Skip.
@@ -218,7 +218,7 @@ Inbound files land under `<agent-dir>/tmp/telegram` and default to a 50 MiB limi
218
218
 
219
219
  ### Voice And Media
220
220
 
221
- Voice notes, audio, images, PDFs, and other media can pass through configured inbound handlers, programmatic handlers, or registered STT providers. Outbound voice can use configured `outboundHandlers` or registered TTS providers; `pi-telegram` owns reply policy and Telegram transport, while providers own synthesis. Configure provider-neutral local/API pipelines and ordered fallbacks through [`telegram.json` command templates](./docs/voice.md#choose-an-integration-path). The default `hidden` reply mode still supports intentional voice delivery through explicit `telegram_voice` actions; `mirror` and `always` add automatic voice policy. Explicit actions accept either a JSON object or compact double-quoted attributes, with equivalent `text` and `value` payload keys and one colon-free action marker.
221
+ Voice notes, audio, images, PDFs, and other media can pass through configured inbound handlers, programmatic handlers, or registered STT providers. Outbound voice can use configured `outboundHandlers` or registered TTS providers; `pi-telegram` owns reply policy and Telegram transport, while providers own synthesis. Configure provider-neutral local/API pipelines and ordered fallbacks through [`telegram.json` command templates](./docs/voice.md#choose-an-integration-path). The default `manual` reply mode still supports intentional voice delivery through explicit `telegram_voice` actions; `mirror` and `always` add automatic voice policy. Explicit actions accept either a JSON object or compact double-quoted attributes, with equivalent `text` and `value` payload keys and one colon-free action marker.
222
222
 
223
223
  ### Buttons And Callbacks
224
224
 
package/api/voice.ts CHANGED
@@ -9,7 +9,6 @@ export {
9
9
  computeVoicePromptContribution,
10
10
  computeVoiceTurnFlags,
11
11
  getTelegramVoiceReplyMode,
12
- getTelegramVoiceSendTranscript,
13
12
  isVoiceTurn,
14
13
  registerTelegramVoiceSynthesisProvider,
15
14
  registerTelegramVoiceTranscriptionProvider,
@@ -213,13 +213,13 @@ Compaction runs under the journal transaction lock when either 256 unapplied seg
213
213
 
214
214
  Queue receipt ownership is independent from the Telegram transport lock. A same-instance, same-process generation may reconstruct its local receipt across a fenced session replacement and may settle it after transport ownership moves. A different process reports the receipt as foreign, never republishes it into its local queue, and cannot complete it even if it reads the acquisition id. Startup no longer treats process replacement as proof that an owner died: foreign and legacy unowned receipts remain durable.
215
215
 
216
- Recovery and live handoff are compare-and-set under the journal transaction. Queue discard during exact queue-lifecycle cancellation requires the exact local owner/acquisition and removes all receipt sources atomically. Before admission worker start, the lifecycle groups each foreign receipt and asks the journal to recheck OS pid liveness plus process-birth identity under the same transaction; a live owner returns `owner-alive` and a live owner without stable birth proof returns `owner-unverifiable`, both without mutation, while exact negative proof converts the complete receipt back to `pending`. Replacement registration carries its exact pid/process-birth before this check; when registration and recovery race, that live identity wins the liveness proof and the queued receipt remains untouched. Only then does the replacement worker start, replay, and acquire a fresh receipt id/acquisition, fencing every stale owner.
216
+ Cleanup and live handoff are compare-and-set under the journal transaction. Queue discard during exact queue-lifecycle cancellation requires the exact local owner/acquisition and removes all receipt sources atomically. Before admission worker start, the lifecycle groups each foreign receipt and asks the journal to recheck OS pid liveness plus process-birth identity under the same transaction; a live owner returns `owner-alive` and a live owner without stable birth proof returns `owner-unverifiable`, both without mutation, while exact negative proof atomically discards the complete session-owned receipt without replay. Replacement registration carries its exact pid/process-birth before this check; when registration and cleanup race, that live identity wins the liveness proof and the queued receipt remains untouched. A replacement or unrelated worker therefore never executes queue work whose prior owner is proven dead.
217
217
 
218
218
  Authenticated live handoff uses journal CAS plus bounded local IPC. The donor creates a one-time high-entropy token and durably offers the complete receipt to one exact recipient runtime/process/session identity; the journal stores only a digest bound to queue kind, receipt sources, donor acquisition, and recipient identity. While offered, donor completion/discard and dead-owner recovery fail closed, so authority cannot disappear during payload transfer. Prompt payloads carry all queue fields; control payloads carry only their stable `status`/`model` identity and rebuild executable closures locally. The separately negotiated `queue-handoff-v1` capability gates this envelope for leader and both peer generations. Each receipt carries its exact source journal binding; the donor derives the recipient follower-journal binding from the authenticated stable follower profile before routing. The bus validates payload shape/size and exact donor/recipient registration generations, and the recipient selects only that matching active lifecycle, stages one complete receipt idempotently, accepts the journal CAS, and returns the exact receipt plus newly minted owner in its ACK. Malformed, legacy-unbound, inactive-generation, or unavailable bindings fail closed.
219
219
 
220
220
  During recipient staging, presenting the token atomically replaces the journal owner with a fresh acquisition carrying the handoff digest, removes the offer, and permanently fences donor settlement. The donor treats only an ACK carrying that exact accepted owner as success and never repeats acceptance against a donor-bound journal runtime. The recipient can repeat the same acceptance idempotently; a different token cannot claim an already accepted receipt. The coordinator contract orders offer → stage/accept exact receipt-and-owner ACK → donor removal → recipient readiness for direct leader→follower and follower→follower routing. Before acceptance, negative or mismatched acknowledgement exactly cancels the offer and keeps donor work. After acceptance, a lost acknowledgement cannot roll authority back: cancellation fails closed and donor memory remains frozen until exact accepted-owner reconciliation removes it. Recipient registration carries exact process-birth/session identity, and staged payloads remain outside the live dispatch store until accepted journal authority has been reconstructed. Production advertises `queue-handoff-v1` only with this exact role/journal selection and uses the same coordinator ordering for direct leader→follower and follower→follower routes.
221
221
 
222
- Queued semantic authority has no elapsed-time lease. A timeout cannot prove either owner death or effect quiescence, so it cannot safely recover a receipt. Resolution is limited to authenticated live handoff, exact owner discard/settlement, or transaction-rechecked negative PID plus process-birth evidence. Live or unverifiable owners remain queued indefinitely rather than risking duplicate execution.
222
+ Queued semantic authority has no elapsed-time lease. A timeout cannot prove either owner death or effect quiescence, so it cannot safely resolve a receipt. Resolution is limited to authenticated live handoff, exact owner discard/settlement, or transaction-rechecked negative PID plus process-birth evidence that permits terminal cleanup without replay. Live or unverifiable owners remain queued rather than risking duplicate or cross-session execution.
223
223
 
224
224
  The initial `offset: -1` cursor bootstrap is allowed only when both cursor and journal are absent or empty. Thereafter process-level ordering is journal atomic rename → one monotonic offset atomic rename → worker signal. Failure before journal publication leaves the offset unchanged; failure after journal publication but before offset publication permits Telegram redelivery and journal dedupe; failure after offset publication but before worker signal replays from the journal on restart. Queue-owner, retry, terminal, handoff, and completion transitions use the same journal publication primitive and therefore share this process-crash boundary. The final completion window is at-least-once, so replay-sensitive external effects must use `update_id` or the stable delivery id as an idempotency key.
225
225
 
@@ -236,7 +236,7 @@ The canonical update transition contract is:
236
236
  - `retry-wait → executing`: only after `nextRetryAtMs`; repeated signals before eligibility do not execute the entry. Automatic retries continue indefinitely with exponential `1s → 2s → 4s → 8s → 16s → 32s → 60s` delay capped at `60s`, while later independent updates continue draining.
237
237
  - Legacy `failed → retry-wait`: startup atomically resumes terminal entries written by earlier `0.28.0` candidates. Runtime policy never silently discards durable inbound authority and exposes no Pi command for manual retry/discard.
238
238
  - `queued → offered → staged → queued`: only the exact persisted donor may offer or cancel a live handoff; an offer preserves donor ownership but freezes ordinary settlement and recovery. Authenticated bounded IPC stages one exact payload/receipt outside the live queue. Exact recipient acceptance mints a fresh acquisition, reconstructs local ownership, removes donor work, then publishes recipient dispatch readiness.
239
- - `queued → completed | pending`: only the exact persisted owner receipt may complete or discard queued sources; generic completion rejects queued state. Process-birth-proven owner death may atomically recover the complete unoffered receipt to `pending`; live, unverifiable, or offered owners remain queued.
239
+ - `queued → completed`: only the exact persisted owner receipt may complete or discard queued sources; generic completion rejects queued state. Process-birth-proven owner death atomically discards the complete unoffered session-owned receipt without replay; live, unverifiable, or offered owners remain queued.
240
240
 
241
241
  The worker executes at most 64 eligible entries from one validated journal snapshot, commits ordinary completions through one journal transaction, then yields through a generation-checked event-loop boundary. Retry, queue, or prior-generation boundaries first flush completed ids and force a fresh snapshot, preserving exact state-transition atomicity without per-entry parse/rewrite churn. A deterministic 2,048-entry stress gate requires exactly 32 completion publications, 33 reads including the final empty snapshot, continued 1ms timer progress, and less than 250ms maximum observed heartbeat delay. Byte-capacity tests cover failed and retry-wait diagnostics, queue receipt/owner and handoff metadata, and operator dispositions; every rejected growth leaves the prior authority bytes unchanged. It still scans later independent entries after retry or terminal persistence. An unresolved reaction remains a queue-mutation dependency even in `retry-wait` or `failed`, but dispatch checks that dependency against the candidate queue item's exact chat and source message ids instead of globally blocking unrelated targets. Successful replay or an exact discard disposition releases the dependency. Worker state, debug status, state snapshots, and redacted runtime events expose journal depth, retry/terminal counts, the next retry, latest terminal identity, copyable operator commands, and the exact first foreign queued owner identity (instance, PID/birth, session, and acquisition) when semantic authority belongs to another process.
242
242
 
@@ -270,7 +270,7 @@ A prompt is one queue object with exactly one active lane membership and one cur
270
270
 
271
271
  Telegram reactions are shortcut controls over those dimensions. Positive reactions (`👍`, `⚡️`, `❤️`, `🕊`, `🔥`) control Priority; negative reactions (`👎`, `👻`, `💔`, `💩`, `🗑`) control Skip. The runtime compares the complete old and new reaction sets and mutates only categories that changed, so adding or removing a negative reaction cannot silently change Priority, and changing a positive reaction cannot silently change Skip. The listed order selects the retained display emoji when several recognized emoji from one category coexist; it does not let one category override the other.
272
272
 
273
- Priority and Skip may coexist, including `👍 + 💩`. The prompt remains at its Priority-lane position while waiting, but Skip wins when dispatch reaches it: the dispatcher drops it without a model turn and continues. A skipped prompt remains visible with only its negative emoji, survives authenticated queue handoff, and can return to Keep until it reaches the head. Queue item detail exposes symmetric Priority/Normal and Keep/Skip selectors instead of an irreversible Delete action.
273
+ Priority and Skip may coexist, including `👍 + 💩`. The prompt remains at its Priority-lane position while waiting, and its durable journal receipts remain intact so Keep stays reversible and exact live handoff remains possible. Skip wins when dispatch reaches the prompt: the dispatcher first settles those receipts durably, then drops it without a model turn and continues; settlement failure retains the skipped head instead of allowing replay ambiguity. The prompt stays visible with only its negative emoji, is excluded immediately from the executable queue count shared by the Pi status bar and Telegram main menu, and keeps a struck-through physical ordinal. Returning it to Keep restores its contribution to the count without moving it. Graceful session shutdown discards all remaining queue receipts before clearing session-local memory, and startup discards receipts only after proving their former process dead, so a new or unrelated session never inherits queued work. Queue item detail exposes symmetric Priority/Normal and Keep/Skip selectors instead of an irreversible Delete action.
274
274
 
275
275
  Menu and reaction controls share the same canonical queue state. A menu Keep can clear internal Skip without changing Priority or queue position, but Telegram's Bot API cannot remove a reaction created by the user; the visible user reaction can therefore remain until that user removes it. Once Pi has consumed or dropped a prompt, later reactions cannot retract or restore it.
276
276
 
@@ -314,7 +314,7 @@ Every outbound request carries its target. The leader injects `message_thread_id
314
314
  Threaded Mode should make follower threads behave like normal Telegram instance surfaces, with the leader acting only as transport owner. Any feature in the matrix below that works for the leader must either work for followers or have an explicit documented exception.
315
315
 
316
316
  | Surface | Leader behavior | Follower requirement | Routing/ownership invariant | Regression evidence |
317
- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
317
+ | --- | --- | --- | --- | --- |
318
318
  | Prompt intake | Thread prompt queues locally | Thread prompt is forwarded and queued by the owning follower | Target ownership routes by `{ chatId, threadId }` before local handling | Routing tests for foreign target message forwarding |
319
319
  | Queued-message removal reactions | 👎/👻/💔/💩/🗑 marks a pending prompt/media turn for deletion when it reaches dispatch | Same reaction on a queued follower prompt marks that follower's pending turn for deletion before model dispatch | When the leader forwards a prompt to a follower, it records `chatId/messageId -> follower instance` because Bot API reaction updates expose chat/message but not thread id | Update runtime regression records forwarded message ownership and forwards the later reaction |
320
320
  | Queue priority reactions | 👍/⚡/❤/🕊/🔥 prioritizes queued prompts | Same reactions prioritize follower queued prompts | Reaction forwarding uses stored message ownership, then follower mutates its local queue | Reaction mutation tests plus forwarded-reaction coverage |
package/docs/outbound.md CHANGED
@@ -119,14 +119,13 @@ import { registerTelegramVoiceSynthesisProvider } from "@llblab/pi-telegram/voic
119
119
 
120
120
  const dispose = registerTelegramVoiceSynthesisProvider(
121
121
  async (text, options) => {
122
- const audioPath = await synthesizeToOggOpus(text, options);
123
- return { audioPath, transcriptText: text };
122
+ return await synthesizeToOggOpus(text, options);
124
123
  },
125
124
  { id: "my-extension/tts" },
126
125
  );
127
126
  ```
128
127
 
129
- Synthesis providers receive the extracted `telegram_voice` text plus optional `lang`/`rate` hints. Stable registrations pass a durable `id`; omitted ids remain a compatibility path for older providers. Providers own translation, TTS, speech rewriting, transcript choice, and OGG/Opus conversion. The bridge validates that the returned file ends in `.ogg` or `.opus`, sends it through Telegram `sendVoice`, and falls back to planned text if delivery fails before any visible text was delivered. Providers run after configured and programmatic voice handlers in the priority chain above.
128
+ Synthesis providers receive the extracted `telegram_voice` text plus optional `lang`/`rate` hints. Stable registrations pass a durable `id`; omitted ids remain a compatibility path for older providers. Providers own translation, TTS, speech rewriting, and OGG/Opus conversion. The bridge validates that the returned file ends in `.ogg` or `.opus`, sends it through Telegram `sendVoice`, and falls back to planned text if delivery fails before any visible text was delivered. Providers run after configured and programmatic voice handlers in the priority chain above.
130
129
 
131
130
  ## Voice Markup
132
131
 
@@ -99,8 +99,7 @@ interface TelegramConfig {
99
99
  timeInjection?: "hidden" | "always" | "interval";
100
100
  };
101
101
  voice?: {
102
- replyMode?: "hidden" | "mirror" | "always";
103
- sendTranscript?: boolean;
102
+ replyMode?: "manual" | "mirror" | "always";
104
103
  };
105
104
  time?: {
106
105
  interval?: number;
@@ -409,16 +408,13 @@ const offStt = registerTelegramVoiceTranscriptionProvider(
409
408
 
410
409
  const offTts = registerTelegramVoiceSynthesisProvider(
411
410
  async (text, options) => {
412
- const audioPath = await synthesizeOggOpus(text, options);
413
- return getTelegramVoiceSendTranscript(getCurrentTelegramConfigView())
414
- ? { audioPath, transcriptText: text }
415
- : { audioPath };
411
+ return await synthesizeOggOpus(text, options);
416
412
  },
417
413
  { id: "@scope/my-extension/tts" },
418
414
  );
419
415
  ```
420
416
 
421
- Stable voice-provider registrations pass a durable `id`. Omitting `id` is a compatibility path for older providers and receives a generated session-local id. Providers return `undefined` to pass. TTS providers must return `.ogg` or `.opus` files for native Telegram voice notes. `voice.sendTranscript` is the bridge-owned transcript preference; providers that expose captions should gate `transcriptText` with `getTelegramVoiceSendTranscript(config)` instead of defining a second reply-policy toggle.
417
+ Stable voice-provider registrations pass a durable `id`. Omitting `id` is a compatibility path for older providers and receives a generated session-local id. Providers return `undefined` to pass. TTS providers must return `.ogg` or `.opus` files for native Telegram voice notes.
422
418
 
423
419
  Full behavior: [Voice Integration](./voice.md).
424
420
 
@@ -525,7 +521,6 @@ export default function demoOutbound(pi: ExtensionAPI) {
525
521
  ```ts
526
522
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
527
523
  import {
528
- getTelegramVoiceSendTranscript,
529
524
  registerTelegramVoiceSynthesisProvider,
530
525
  registerTelegramVoiceTranscriptionProvider,
531
526
  } from "@llblab/pi-telegram/voice";
@@ -533,16 +528,12 @@ import {
533
528
  export default function demoVoice(pi: ExtensionAPI) {
534
529
  let unregisterTts: (() => void) | undefined;
535
530
  let unregisterStt: (() => void) | undefined;
536
- let currentConfig: { voice?: { sendTranscript?: boolean } } = {};
537
531
  pi.on("session_start", async () => {
538
532
  unregisterTts?.();
539
533
  unregisterStt?.();
540
534
  unregisterTts = registerTelegramVoiceSynthesisProvider(
541
535
  async (text) => {
542
- const audioPath = await synthesizeDemoOgg(text);
543
- return getTelegramVoiceSendTranscript(currentConfig)
544
- ? { audioPath, transcriptText: text }
545
- : { audioPath };
536
+ return await synthesizeDemoOgg(text);
546
537
  },
547
538
  { id: "demo-voice/tts" },
548
539
  );
package/docs/ui-style.md CHANGED
@@ -26,8 +26,8 @@ Use emoji as stable semantic markers, not decoration. Emoji carry transportable
26
26
  | `🧠` | Thinking level | `/thinking`, thinking menu headings, thinking status rows | Use only for reasoning/thinking controls. |
27
27
  | `🔢` | Queue list / ordered work | `/queue`, queue menu entrypoints | Queue item rows may also use numeric labels. |
28
28
  | `⏱️` | Queue is ticking / current work is active | Inline main-menu Queue row only | Running-clock queue state: the narrow present moment is being worked now. |
29
- | `⏳` | Queue has waiting prompts | Inline main-menu Queue row only | Hourglass queue state: sand above the neck is future work still waiting. |
30
- | `⌛` | Queue is empty / standing idle | Inline main-menu Queue row only | Standing hourglass queue state: no future work is waiting above the neck. |
29
+ | `⏳` | Waiting / temporarily busy | Inline main-menu Queue row, busy notices | Hourglass means work or availability is pending; the sentence must name what is busy. |
30
+ | `⌛` | Empty / standing idle | Inline main-menu Queue row, empty-queue notices | Standing hourglass means no future work is waiting above the neck. |
31
31
  | `⚙️` | Settings / configuration | Settings menu headings and Settings navigation rows | Extension-injected rows appear before the built-in `⚙️ Settings` row. |
32
32
  | `🧩` | Extension-provided surface | Extension command examples, extension section examples | Companion extensions may choose their own emoji, but `🧩` means generic extension/plugin. |
33
33
  | `👄` | Voice reply policy | Voice reply settings row and detail card | Not a generic audio attachment marker. |
@@ -43,19 +43,27 @@ Use emoji as stable semantic markers, not decoration. Emoji carry transportable
43
43
  | --- | --- | --- | --- |
44
44
  | `🟢` | Start / active / current positive state | `/start`, active row, current selected option, active `On` toggle | In command context it means “open/start menu”; in state context it means selected/active. |
45
45
  | `🗜` | Compact session | `/compact`, compact confirmation action | Do not use for generic cleanup/delete. |
46
- | `⏩` | Force next queued turn | `/next` command and matching menu action | Means skip/advance to next waiting item. |
47
- | `▶️` | Continue/resume generation | `/continue` command and matching menu action | Means resume/continue current session flow, not force-next. |
48
- | `⏹️` | Abort current Pi work | `/abort` command description | Stops active work but is not a destructive queue clear by itself. |
49
- | `🟥` | Stop / abort-and-clear danger | `/stop` command description | Stronger than `⏹️`; use for disruptive stop/clear semantics. |
46
+ | `⏩` | Abort and advance | Busy `/next` command result and matching menu action | Means the active turn is aborted before advancing to queued work. |
47
+ | `▶️` | Play / continue immediately | Idle `/next` result, `/continue` command, and matching menu action | Means work can start or resume directly without first aborting an active turn. |
48
+ | `⏹️` | Abort current Pi work | `/abort` command description and active `/stop` result | Stops active work; accompanying copy states separately when queued work is cleared. |
49
+ | `🟥` | Destructive stop command | `/stop` command description | Strong warning at the command/action entrypoint; standalone results use the more precise idle or abort state icon. |
50
50
  | `🆕` | New session / fresh start | Reserved visible extension command example for `/new`-like flows | Same-thread Telegram `/new` is currently blocked by Pi core API; keep this meaning reserved. |
51
51
  | `🌀` | Refresh | Queue refresh row and future refresh buttons | Re-fetch/re-render current surface, not transport reconnect. |
52
52
  | `↪️` | Reroute to an existing target | Thread chooser buttons that send a captured command/message from one thread to another live thread | Curved arrow means the message arrived here but bends to another target. |
53
53
  | `🔁` | Replace/restore mode | Thread replace/restore chooser entrypoints | Opens a second step for moving a Pi instance binding to the current source thread. |
54
54
  | `➡️` | Choose replacement target | Thread replace/restore target buttons that select which Pi instance should move to the current thread | Use inside the second replace/restore chooser, not for ordinary reroutes. |
55
55
  | `☑️` | Activate / choose this item | Model detail activation action, generated button-only choice heading | Positive selection cue; use `🟢 Active` for already-current state. |
56
- | `❌` | No / cancel | Confirmation cancel buttons | Use for safe cancellation, not destructive removal. |
56
+ | `❌` | No / cancel / failed | Confirmation cancel buttons and failure notices | Use for safe cancellation or operation failure, not destructive removal. |
57
57
  | `🗑` | Delete / defer removal | Destructive confirmations and removal reaction | In the queue menu, reversible Keep/Skip selectors replace immediate deletion. |
58
58
 
59
+ ### Informational Feedback
60
+
61
+ | Emoji | Meaning | Canonical surfaces | Notes |
62
+ | --- | --- | --- | --- |
63
+ | `💤` | Nothing active | No-active-turn notices | Neutral idle result, not an error. |
64
+ | `✅` | Completed successfully | Compaction and other completion notices | Use only after the operation has completed. |
65
+ | `🚫` | Unavailable, denied, or cancelled operation | Missing capability/auth notices, access denial, and compaction cancellation | Callback alerts use the same emoji without bold markup. |
66
+
59
67
  ### State Indicators And Button Grammars
60
68
 
61
69
  | Emoji | Meaning | Canonical surfaces | Notes |
@@ -71,6 +79,8 @@ Use emoji as stable semantic markers, not decoration. Emoji carry transportable
71
79
 
72
80
  Queue reactions are shortcut controls for waiting turns. Preserve their semantics across Telegram reactions, queue-menu rows, status previews, and tests. Positive emoji control the Priority/Normal lane; negative emoji control Keep/Skip. These categories are independent, may coexist, and mutate only their own dimension. Crossing lanes appends the prompt at the destination FIFO tail; changing Keep/Skip or changing emoji within one category preserves lane position. Skip wins only when dispatch reaches the prompt.
73
81
 
82
+ Skipped queue ordinals strike only the numeric position. Detail HTML closes `<s>` before the period; list-button labels place a hair-space boundary between the combining-struck number and the plain period so Telegram font overhang cannot visually strike punctuation.
83
+
74
84
  Queue item detail renders two independent selector rows:
75
85
 
76
86
  - `🟡 Priority` / `⚫️ Normal` or `⚫️ Priority` / `🟣 Normal` selects the lane.
@@ -209,11 +219,14 @@ Examples:
209
219
 
210
220
  ## Message Cards
211
221
 
212
- Message cards sent by the bot should start with a strong heading.
222
+ Message cards and standalone informational notices sent by the bot should start with a strong heading.
213
223
 
214
224
  Rules:
215
225
 
216
226
  - Start with a bold heading or, for dialogs, a bold question.
227
+ - Format standalone notices as one fully bold line: relevant emoji, one space, concise sentence, and terminal period or colon.
228
+ - Apply the same hierarchy to success, progress, empty, busy, unavailable, cancellation, and failure notices; do not bold only a fragment of a standalone notice.
229
+ - Callback alerts remain plain text because Telegram does not support rich text there, but still keep the relevant emoji and concise sentence.
217
230
  - Setting detail cards may include an emoji in the heading, then a colon and the current value in `<code>`.
218
231
  - Explain what the setting does and what the options mean only as much as needed.
219
232
  - Order setting value descriptions exactly like the chooser: rows top-to-bottom and values in a shared row left-to-right. Keep `(default)` on the actual default wherever it falls; default status never changes order.
package/docs/voice.md CHANGED
@@ -43,12 +43,12 @@ Voice provider extensions can also register STT backends with `registerTelegramV
43
43
 
44
44
  ## Voice Reply Policy
45
45
 
46
- 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.
46
+ The bridge decides **when** to reply with voice from `voice.replyMode` in `TelegramConfig` (stored in `telegram.json`). Missing and invalid values resolve to the `manual` default; the former `hidden` value remains a read-only compatibility alias for `manual`.
47
47
 
48
48
  ### Modes
49
49
 
50
- - **`hidden` (default):** no `voice.replyMode` is stored and no automatic voice context is added; explicit agent-authored `telegram_voice` actions still work.
51
- - **`mirror`:** voice/audio input activates automatic voice delivery. Text input follows `hidden` behavior.
50
+ - **`manual` (default):** no `voice.replyMode` is stored and no automatic voice context is added; explicit agent-authored `telegram_voice` actions still work.
51
+ - **`mirror`:** voice/audio input activates automatic voice delivery. Text input follows `manual` behavior.
52
52
  - **`always`:** every Telegram turn activates automatic voice delivery.
53
53
 
54
54
  **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.
@@ -70,7 +70,7 @@ A voice extension may combine three public seams:
70
70
 
71
71
  - `registerTelegramVoiceTranscriptionProvider()` for inbound STT fallback on voice/audio files
72
72
  - `registerTelegramVoiceSynthesisProvider()` for outbound TTS/synthesis fallback to Telegram voice messages
73
- - `registerTelegramSection()` for provider-specific Telegram UI such as voice, language, style, transcript, or provider on/off controls
73
+ - `registerTelegramSection()` for provider-specific Telegram UI such as voice, language, style, or provider on/off controls
74
74
 
75
75
  The reply policy itself remains a built-in pi-telegram setting (`voice.replyMode`) rather than a provider-owned menu.
76
76
 
@@ -81,10 +81,8 @@ Voice synthesis provider extensions register themselves through `registerTelegra
81
81
  - Text optimisation / speech-style rewriting
82
82
  - Adding speech tags (when desired)
83
83
  - Running TTS + ffmpeg conversion to OGG/Opus
84
- - Deciding whether to return `transcriptText` at all based on the bridge-owned `voice.sendTranscript` preference when the provider has access to the current Telegram config
85
- - `transcriptText` (when returned) is attached by the bridge as the voice message **caption** only. Separate transcript messages are no longer sent.
86
84
 
87
- The bridge shows a `record_voice` action while delivering and sends the final audio with Telegram `sendVoice`. When a provider returns `transcriptText`, the bridge attaches it as the voice caption.
85
+ The bridge shows a `record_voice` action while delivering and sends the final audio with Telegram `sendVoice`.
88
86
 
89
87
  Providers can implement `getVoicePromptContribution(view)` to inject voice-specific instructions into voice-tagged prompts (for example: "Reply only with the spoken text"). The bridge appends the first non-empty provider contribution when `mirror` or `always` mode tags the turn.
90
88
 
@@ -95,7 +93,6 @@ The provider receives the raw agent text plus optional `{ lang?, rate? }`.
95
93
  It must return one of:
96
94
 
97
95
  - `string` — path to a ready `.ogg` or `.opus` file
98
- - `{ audioPath: string, transcriptText?: string }` — `audioPath` must be OGG/Opus. When `transcriptText` is present it is attached as the voice message **caption**. Providers should treat pi-telegram's `voice.sendTranscript` as the bridge-owned transcript preference instead of inventing a second reply-policy UI.
99
96
  - `undefined` — skip this text block
100
97
 
101
98
  **Important:** Providers are fully responsible for producing a clean, TTS-optimised native voice file. The bridge may also run configured outbound voice command templates for users who prefer process-boundary handlers instead of provider extensions.
@@ -106,7 +103,7 @@ Registration returns a disposer function for cleanup. Stable provider registrati
106
103
 
107
104
  ## Outbound Voice Handlers
108
105
 
109
- 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.
106
+ 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 (`manual`, `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.
110
107
 
111
108
  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:
112
109
 
@@ -128,31 +125,6 @@ Voice handlers receive the text on stdin in composed pipelines and can use `{tex
128
125
 
129
126
  Priority for outbound voice delivery is: configured `outboundHandlers` with `type: "voice"` in their `telegram.json` order, then programmatic `voice` outbound handlers, then registered voice synthesis providers. Provider extensions are the zero-config tail of the same pipeline: they handle voice when no explicit configured handler succeeds, but they do not override operator-configured handlers. If multiple providers are registered, only one handles a given voice reply: the first provider that returns a valid `.ogg`/`.opus` artifact wins. Providers that return `undefined` explicitly pass to the next provider; providers that throw or return invalid output are recorded and the next fallback is tried.
130
127
 
131
- ### Provider with transcript caption (controlled by user toggle)
132
-
133
- When the user's "Send Transcript" toggle is ON, return the clean spoken text as `transcriptText`. The bridge attaches it as the caption on the voice message. When the toggle is OFF, return only the audio path (no `transcriptText`).
134
-
135
- ```typescript
136
- import {
137
- getTelegramVoiceSendTranscript,
138
- registerTelegramVoiceSynthesisProvider,
139
- } from "@llblab/pi-telegram/voice";
140
-
141
- registerTelegramVoiceSynthesisProvider(
142
- async (text, options) => {
143
- const rewritten = rewriteWithSpeechTags(text);
144
- const audioPath = await myTTS(rewritten, { language: options?.lang });
145
- const sendTranscript = getTelegramVoiceSendTranscript(
146
- getCurrentTelegramConfigView(),
147
- );
148
- return sendTranscript ? { audioPath, transcriptText: text } : { audioPath };
149
- },
150
- { id: "my-voice-provider/tts" },
151
- );
152
- ```
153
-
154
- `getCurrentTelegramConfigView()` represents whatever current `TelegramConfig` view your extension already owns or receives; pi-telegram does not require providers to read config directly. The bridge never sends a separate transcript message. Caption-only is the "ON" behavior.
155
-
156
128
  ### Surfacing provider diagnostics
157
129
 
158
130
  Voice provider extensions can record runtime events that appear in `/telegram-status` alongside pi-telegram's own events:
@@ -170,13 +142,13 @@ recordTelegramRuntimeEvent("voice-provider", new Error("TTS failed"), {
170
142
 
171
143
  ## Voice Extension Section
172
144
 
173
- Voice provider extensions can register a Voice Extension Section (settings UI) via `registerTelegramSection`. The section can expose provider-specific controls such as TTS voice, language, speech style, transcript behavior, or STT/TTS enablement. Reply mode is a core pi-telegram setting and belongs in the built-in Settings menu.
145
+ Voice provider extensions can register a Voice Extension Section (settings UI) via `registerTelegramSection`. The section can expose provider-specific controls such as TTS voice, language, speech style, or STT/TTS enablement. Reply mode is a core pi-telegram setting and belongs in the built-in Settings menu.
174
146
 
175
147
  **Note on resume:** Because the previous automatic persistent re-registration system has been removed, extensions are responsible for re-registering their Voice Extension Section on `session_start` if they want the menu to survive a `pi resume`. See `registerTelegramSection` from `@llblab/pi-telegram/sections`.
176
148
 
177
149
  ## Prompt Guidance
178
150
 
179
- 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.
151
+ The bridge keeps voice prompt context compact, effective, and policy-owned. `manual` 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.
180
152
 
181
153
  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.
182
154
 
@@ -224,4 +196,4 @@ The bridge reads `voice.replyMode` from the config when building a turn.
224
196
 
225
197
  ### Provider config
226
198
 
227
- Provider-specific settings (voice ID, language, speech style, transcript behavior, STT/TTS enablement) are owned by the voice provider extension. Reply mode is owned by pi-telegram's `voice.replyMode` and configured from the built-in pi-telegram Settings menu, not duplicated in provider UIs.
199
+ Provider-specific settings (voice ID, language, speech style, STT/TTS enablement) are owned by the voice provider extension. Reply mode is owned by pi-telegram's `voice.replyMode` and configured from the built-in pi-telegram Settings menu, not duplicated in provider UIs.
package/index.ts CHANGED
@@ -359,6 +359,7 @@ export default function (pi: Pi.ExtensionAPI) {
359
359
  getActiveToolExecutions: lifecycle.getActiveToolExecutions,
360
360
  hasPendingModelSwitch: pendingModelSwitchStore.has,
361
361
  getQueuedItems: telegramQueueStore.getQueuedItems,
362
+ getQueuedItemCount: Queue.countExecutableTelegramQueueItems,
362
363
  formatQueuedStatus: Queue.formatQueuedTelegramItemsStatus,
363
364
  getRecentRuntimeEvents: runtimeEvents.getEvents,
364
365
  getRuntimeLockState: lockRuntime.getStatusLabel,
@@ -1234,6 +1235,7 @@ export default function (pi: Pi.ExtensionAPI) {
1234
1235
  prepareTempDir,
1235
1236
  updateStatus,
1236
1237
  unbindDeferredDispatchContext: deferredQueueDispatchRuntime.unbind,
1238
+ discardQueuedItems: queueMutationRuntime.clear,
1237
1239
  clearModelMenuState: modelMenuRuntime.clear,
1238
1240
  getActiveTurnChatId: activeTurnRuntime.getChatId,
1239
1241
  getActiveTurnTarget: activeTurnRuntime.getTarget,
package/lib/bindings.ts CHANGED
@@ -95,11 +95,28 @@ export function createTelegramQueueBindingRuntime<TContext>(deps: {
95
95
  sendUserMessage: Queue.TelegramQueueDispatchRuntimeDeps<TContext>["sendUserMessage"];
96
96
  recordRuntimeEvent?: TelegramRuntimeEventRecorder;
97
97
  }): TelegramQueueBindingRuntime<TContext> {
98
+ const settleDiscardedItems = (
99
+ items: readonly Queue.TelegramQueueItem<TContext>[],
100
+ ctx: TContext,
101
+ ): boolean => {
102
+ const durableItems = items.filter(
103
+ (item) => (item.admissionReceipts?.length ?? 0) > 0,
104
+ );
105
+ if (durableItems.length === 0) return true;
106
+ const settlement = deps.admission.getSettlement();
107
+ if (!settlement) return false;
108
+ settlement.onItemsDiscarded(durableItems, ctx);
109
+ return durableItems.every((item) => !settlement.isItemReady(item));
110
+ };
98
111
  const mutation = Queue.createTelegramQueueMutationController({
99
112
  ...deps.store,
100
113
  allocateLaneOrder: deps.queue.allocateItemOrder,
101
114
  onItemsDiscarded(items, ctx) {
102
- deps.admission.getSettlement()?.onItemsDiscarded(items, ctx);
115
+ if (!settleDiscardedItems(items, ctx)) {
116
+ throw new Error(
117
+ "Telegram queue items could not be discarded durably.",
118
+ );
119
+ }
103
120
  },
104
121
  updateStatus: deps.updateStatus,
105
122
  recordRuntimeEvent: deps.recordRuntimeEvent,
@@ -136,6 +153,9 @@ export function createTelegramQueueBindingRuntime<TContext>(deps: {
136
153
  onControlSettled(item, ctx) {
137
154
  deps.admission.getSettlement()?.onControlSettled(item, ctx);
138
155
  },
156
+ onPromptSkipped(item, ctx) {
157
+ return settleDiscardedItems([item], ctx);
158
+ },
139
159
  updateStatus: deps.updateStatus,
140
160
  sendTextReply: deps.sendTextReply,
141
161
  recordRuntimeEvent: deps.recordRuntimeEvent,
@@ -909,6 +929,25 @@ export function registerTelegramLifecycleRuntimeHooks({
909
929
  { replyToPrompt: false },
910
930
  );
911
931
  };
932
+ let activeTurnDeliveryTail = Promise.resolve();
933
+ const scheduleActiveTurnDelivery = (task: () => Promise<void>): void => {
934
+ const previous = activeTurnDeliveryTail;
935
+ activeTurnDeliveryTail = (async () => {
936
+ await previous;
937
+ await new Promise<void>((resolve) => {
938
+ const timer = setTimeout(resolve, 0);
939
+ timer.unref?.();
940
+ });
941
+ await task();
942
+ })().catch((error) => {
943
+ recordRuntimeEvent("delivery", error, {
944
+ phase: "agent-end-background-delivery",
945
+ });
946
+ });
947
+ };
948
+ const waitForActiveTurnDelivery = async (): Promise<void> => {
949
+ await activeTurnDeliveryTail;
950
+ };
912
951
  const agentLifecycleHooks = Queue.createTelegramAgentLifecycleHooks<
913
952
  Queue.PendingTelegramTurn,
914
953
  Pi.ExtensionContext,
@@ -950,16 +989,7 @@ export function registerTelegramLifecycleRuntimeHooks({
950
989
  dispatchNextQueuedTelegramTurn,
951
990
  requestDeferredDispatchNextQueuedTelegramTurn:
952
991
  deferredQueueDispatchRuntime.request,
953
- scheduleActiveTurnDelivery(task) {
954
- const timer = setTimeout(() => {
955
- void task().catch((error) => {
956
- recordRuntimeEvent("delivery", error, {
957
- phase: "agent-end-background-delivery",
958
- });
959
- });
960
- }, 0);
961
- timer.unref?.();
962
- },
992
+ scheduleActiveTurnDelivery,
963
993
  clearPreview: previewRuntime.clear,
964
994
  setPreviewPendingText: previewRuntime.setPendingText,
965
995
  finalizeMarkdownPreview,
@@ -1079,7 +1109,11 @@ export function registerTelegramLifecycleRuntimeHooks({
1079
1109
  activityRuntime.onCompactionStart(Pi.getSessionCompactionReason(event));
1080
1110
  compactionObserver.onSessionBeforeCompact(event, ctx);
1081
1111
  if (shouldNotify) {
1082
- await sendCompactionNotice(Commands.TELEGRAM_COMPACTION_STARTED_TEXT);
1112
+ await waitForActiveTurnDelivery();
1113
+ if (!isSessionContextActive(ctx)) return;
1114
+ await sendCompactionNotice(
1115
+ Commands.TELEGRAM_COMPACTION_STARTED_MARKDOWN,
1116
+ );
1083
1117
  }
1084
1118
  },
1085
1119
  async onSessionCompact(event, ctx) {
@@ -1088,7 +1122,9 @@ export function registerTelegramLifecycleRuntimeHooks({
1088
1122
  compactionObserver.onSessionCompact(event, ctx);
1089
1123
  if (observedAutomaticCompaction) {
1090
1124
  observedAutomaticCompaction = false;
1091
- await sendCompactionNotice(Commands.TELEGRAM_COMPACTION_COMPLETED_TEXT);
1125
+ await sendCompactionNotice(
1126
+ Commands.TELEGRAM_COMPACTION_COMPLETED_MARKDOWN,
1127
+ );
1092
1128
  }
1093
1129
  },
1094
1130
  async onAgentStart(event, ctx) {