@llblab/pi-telegram 0.11.0 → 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -73,7 +73,8 @@
73
73
  - Telegram delivery strips top-level HTML comments from preview/final text; column-zero top-level `<!-- telegram_voice ... -->` and `<!-- telegram_button ... -->` blocks are special outbound comments handled after `agent_end` without requiring agent-side transport tool calls, while comments inside code, quotes, lists, or indented examples stay literal
74
74
  - `telegram_voice` and `telegram_button` are not π tools; keep prompts/docs explicit that agents should author markup while voice synthesis provider extensions own TTS/OGG conversion, and pi-telegram owns button routing plus Telegram delivery
75
75
  - Voice reply policy and prompt context are owned by pi-telegram's `telegram.json` `voice.replyMode`: missing/invalid config behaves as `manual` but does not add a `[voice]` prompt-context block; only an explicit valid `voice.replyMode` renders context. Render a single voice field as `[voice] reply mode: manual|mirror|always`, and render multiple fields as a `[voice]` list; place voice context after `[outputs]` when handler output exists, otherwise after `[attachments]`; provider prompt contributions are optional provider-specific additions, not the default policy channel
76
- - Voice reply mode Settings UI standard: the top-level Settings row is `👄 Voice reply: hidden|manual|mirror|always`; `hidden` is the true default and means no valid `voice.replyMode` is persisted, behavior is manual, and no voice policy is added to prompt context; explicit `manual` behaves the same operationally but renders reply-mode context. The submenu title is `Voice reply mode:`; choice buttons use lowercase labels with a model-style active dot (`🟢 hidden`, `🟢 mirror`) rather than per-mode emoji; the explanatory submenu body uses compact HTML-code bullets such as `<code>-</code> <code>hidden</code> (default): ...`. Preserve this wording/icons unless the operator explicitly asks to redesign it
76
+ - Optional `telegram.json` `time` may add `[time] YYYY-MM-DD HH:mm:ss <timezone>` to Telegram-originated prompts for wall-clock context. It is hidden by default, uses `time.injectionMode` values `hidden|always|interval`, stores `time.interval` in milliseconds, uses the system timezone, and should render last after `[attachments]`, `[outputs]`, and `[voice]` sections. The Settings row `🕒 Time injection: hidden|always|interval` controls `time.injectionMode` only.
77
+ - Voice reply mode Settings UI standard: the top-level Settings row is `👄 Voice reply: hidden|manual|mirror|always`; `hidden` is the true default and means no valid `voice.replyMode` is persisted, behavior is manual, and no voice policy is added to prompt context; explicit `manual` behaves the same operationally but renders reply-mode context. The submenu title is `👄 Voice reply mode:`; choice buttons use lowercase labels with a model-style active dot (`🟢 hidden`, `🟢 mirror`) rather than per-mode emoji; the explanatory submenu body uses compact HTML-code bullets such as `<code>-</code> <code>hidden</code> (default): ...`. Preserve this wording/icons unless the operator explicitly asks to redesign it
77
78
  - Outbound voice delivery is one fallback pipeline: configured `outboundHandlers` with `type: "voice"` run first in `telegram.json` order, then programmatic voice handlers, then registered voice synthesis providers as zero-config progressive fallbacks; provider extensions must not override operator-configured handlers
78
79
  - `telegram_voice` text is arbitrary TTS-target text: use body form for multiline text, `<!-- telegram_voice text="Short summary" -->` for explicit one-line text, or `<!-- telegram_voice: Short summary -->` for one-line text with no attributes
79
80
  - `telegram_button` has three canonical forms: `<!-- telegram_button: OK -->` for label-only buttons, `<!-- telegram_button label=Continue prompt="Continue with the current plan." -->` for one-line prompts, or `<!-- telegram_button label="Show risks"\nList the main risks first.\n-->` for multiline prompts
@@ -128,7 +129,8 @@ The canonical detailed ownership map lives in [`docs/architecture.md`](./docs/ar
128
129
  - When durable runtime constraints or repeat bug patterns emerge, record them here instead of burying them in changelog prose
129
130
  - When fork identity changes, keep `README.md`, package metadata, and docs aligned so the published package does not point back at stale upstream coordinates
130
131
  - README positioning should emphasize `/start` as the primary Telegram operator menu and keep reaction shortcuts secondary. Reactions are useful queue affordances, but menu controls are the core CLI-to-Telegram bridge.
131
- - Document configuration knobs without UI in the root README when they affect bootstrap, networking, or transport limits; currently this includes token env bootstrap, Node env proxy mode, and inbound/outbound size limits.
132
+ - Document configuration knobs without UI in the root README when they affect bootstrap, networking, transport limits, or prompt context; currently this includes token env bootstrap, Node env proxy mode, inbound/outbound size limits, and `time`.
133
+ - Keep extension-local standards self-contained: shared patterns such as command templates may evolve independently in multiple extensions, but pi-telegram docs/changelog should describe the standard without naming sibling extension implementations as dependencies or authorities.
132
134
  - Work only inside this repository during development tasks; updating the installed Pi extension checkout is a separate manual operator step, not part of normal in-repo implementation work
133
135
 
134
136
  ## 8. Integration Protocols
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.11.2: Queue Continuation, Compaction Safety, And Settings Polish
6
+
7
+ - `[Time Context]` Renamed the disabled time injection mode from `off` to `hidden` in Settings and config defaults, matching voice reply mode semantics where no prompt-context line is injected. Legacy `off` callbacks/config values are still treated as hidden.
8
+ - `[Settings UI]` Settings detail headings now show the current value immediately after the bold label in monospace style, making each submenu title double as the active setting summary.
9
+ - `[Entrypoint]` Removed root-level API re-exports from `index.ts`; public extension APIs remain available from their owning `lib/*` modules while the root file stays a default-only composition root.
10
+ - `[Continue Queue]` `/continue` now enqueues a standalone priority prompt without preserving already queued prompts as history. Impact: queued prompts stay iterative queue items instead of being folded into one combined `continue` prompt.
11
+ - `[Auto Compaction]` Observes native `session_before_compact` / `session_compact` events and blocks queued Telegram prompt dispatch while compaction is running, then resumes the queue after compaction settles. Impact: queued Telegram turns no longer race Pi auto-compaction and trigger `Cannot read properties of undefined (reading 'signal')` after compaction.
12
+ - `[Command Templates]` Updated the command-template helper library and regressions with typed placeholders, array-index placeholder resolution, repeat fanout from array length, unbounded default timeout semantics, and trusted-command warnings.
13
+ - `[Docs]` Updated the command-template standard with typed args, array placeholders, failure propagation, recover cleanup, and trust-boundary guidance.
14
+ - `[Docs]` Decomposed oversized architecture documentation blocks into focused sections for runtime ownership, queue validation, application menu shape, outbound actions, and interactive controls. Impact: the architecture entry point is easier to scan without changing runtime behavior.
15
+ - `[README]` Split queue reaction shortcuts into priority and removal lists, added `pi-xai-voice` as a companion extension, and kept the time-injection row label compact while preserving the full detail-heading label. Impact: operator-facing docs match the current Telegram menu language.
16
+ - `[Context]` Recorded the convention that extension-local standards should stay self-contained and avoid naming sibling extension implementations as authorities. Impact: shared standards can evolve independently without creating false extension dependencies.
17
+
18
+ ## 0.11.1: Time Context And Settings Polish
19
+
20
+ - `[Time Context]` Added optional `telegram.json` `time` prompt context for Telegram-originated turns. `time.injectionMode` values are `off`, `always`, and per-chat `interval`; `time.interval` is stored in milliseconds and timezone comes from the system. The `[time]` line renders last after attachments, handler outputs, and voice context, and Settings exposes a `🕒 Time` mode selector.
21
+ - `[Settings UI]` The proactive push row now uses `📌 Proactive push: on|off`, and proactive push, time, and voice reply submenus use matching emoji headings.
22
+
3
23
  ## 0.11.0: Voice Provider Platform
4
24
 
5
25
  - `[Voice Synthesis Provider API]` Added a first-class voice synthesis provider surface for Telegram voice replies. Providers register with `registerTelegramVoiceSynthesisProvider()` from `@llblab/pi-telegram/lib/voice.ts`, synthesize text into `.ogg`/`.opus`, may return `{ audioPath, transcriptText }`, and can contribute voice-specific prompt guidance through `getVoicePromptContribution(view)`.
@@ -131,7 +151,7 @@
131
151
  ## 0.9.4: Temp Dir And Command Template Hotfix
132
152
 
133
153
  - `[Telegram Temp Dir]` Default Telegram API temp files now respect `PI_CODING_AGENT_DIR`, falling back to `~/.pi/agent` when the env var is unset. Impact: sandboxed or relocated agent dirs no longer force Telegram downloads through the default home-directory path.
134
- - `[Command Templates]` Synced the local Command Template Standard with `pi-auto-tools@0.5.5`: command-template nodes now document `mode`, `label`, `delay`, `repeat`, parallel fanout semantics, zero-based repeat placeholders, padding, and limited arithmetic expressions such as `{_(index+1)}`. Impact: inbound/outbound Telegram handler docs and helpers share the current portable automation contract.
154
+ - `[Command Templates]` Updated the local Command Template Standard: command-template nodes now document `mode`, `label`, `delay`, `repeat`, parallel fanout semantics, zero-based repeat placeholders, padding, and limited arithmetic expressions such as `{_(index+1)}`. Impact: inbound/outbound Telegram handler docs and helpers share the current portable automation contract without depending on another extension's documentation.
135
155
  - `[Queue Menu]` Empty queue refresh clicks now rotate through compact alternate empty-state headings while preserving the default first-open `⌛ Queue is empty.` state, and the Refresh button now stays directly under Back for both empty and populated queue lists. Impact: manual queue polling feels alive and the primary refresh control stays in a stable location without changing queue semantics.
136
156
  - `[Package]` Bumped package metadata to `0.9.4` and kept the lockfile in sync.
137
157
 
@@ -183,7 +203,7 @@
183
203
  - `[Configuration Docs]` Documented the configuration philosophy that rich visual/TUI setup stays minimal for now while agents can read README/docs and update `telegram.json` for advanced workflows. Impact: configuration guidance matches the extension's agent-assisted operator model without adding premature TUI surfaces.
184
204
  - `[Outbound Docs]` Tightened voice-handler critical-step wording around transform → TTS → conversion pipelines and handler-level fallbacks. Impact: docs now match translated voice pipelines without implying provider-specific TTS fallbacks.
185
205
  - `[Package]` Bumped package metadata to `0.8.1` and kept the lockfile in sync.
186
- - `[Command Template Docs]` Synchronized `docs/command-templates.md` bit-for-bit with the current portable standard shared by `pi-auto-tools`. Impact: the documented standard now includes retry, fail-open composition, critical-step abort semantics, and the 30s default timeout in the same wording across both extensions.
206
+ - `[Command Template Docs]` Updated `docs/command-templates.md` to the current portable standard. Impact: the documented standard now includes retry, fail-open composition, critical-step abort semantics, and the 30s default timeout without requiring cross-extension references.
187
207
  - `[Lock Docs]` Synchronized `docs/locks.md` bit-for-bit with the extension-neutral Locks Standard shared by `pi-wakeup`. Impact: singleton ownership documentation no longer carries project-specific examples that prevent exact reuse across extensions.
188
208
 
189
209
  ## 0.8.0: Handler Bus
package/README.md CHANGED
@@ -120,7 +120,10 @@ The inline application menu is the primary operator surface. It exposes status,
120
120
 
121
121
  Messages sent while π is busy enter the prompt queue and are processed in order. Control actions and model-switch continuation turns use higher-priority lanes so operational commands can resume before normal prompts.
122
122
 
123
- The menu is the primary way to inspect and mutate the queue. Reactions are an extra shortcut when Telegram delivers `message_reaction` updates for the chat: `👍`, `⚡️`, `❤️`, `🕊`, and `🔥` promote waiting work; `👎`, `👻`, `💔`, `💩`, and `🗑` remove it. The same rules apply to text, voice, files, images, and media groups.
123
+ The menu is the primary way to inspect and mutate the queue. Reactions are an extra shortcut when Telegram delivers `message_reaction` updates for the chat. The same rules apply to text, voice, files, images, and media groups:
124
+
125
+ - Priority shortcuts: `👍`, `⚡️`, `❤️`, `🕊`, and `🔥` promote waiting work.
126
+ - Removal shortcuts: `👎`, `👻`, `💔`, `💩`, and `🗑` remove waiting work from the queue.
124
127
 
125
128
  ### Streaming and Telegram HTML rendering
126
129
 
@@ -203,17 +206,21 @@ import { registerTelegramVoiceSynthesisProvider } from "@llblab/pi-telegram/lib/
203
206
  import { recordTelegramRuntimeEvent } from "@llblab/pi-telegram/lib/outbound-handlers.ts";
204
207
 
205
208
  // Return path only (backward compatible)
206
- const dispose = registerTelegramVoiceSynthesisProvider(async (text, { lang, rate }) => {
207
- const path = await myTTS(text, { language: lang });
208
- return path; // must be .ogg or .opus
209
- });
209
+ const dispose = registerTelegramVoiceSynthesisProvider(
210
+ async (text, { lang, rate }) => {
211
+ const path = await myTTS(text, { language: lang });
212
+ return path; // must be .ogg or .opus
213
+ },
214
+ );
210
215
 
211
216
  // Return path + transcript caption
212
- const dispose2 = registerTelegramVoiceSynthesisProvider(async (text, { lang, rate }) => {
213
- const rewritten = rewriteWithSpeechTags(text); // internal TTS optimization
214
- const path = await myTTS(rewritten, { language: lang });
215
- return { audioPath: path, transcriptText: text };
216
- });
217
+ const dispose2 = registerTelegramVoiceSynthesisProvider(
218
+ async (text, { lang, rate }) => {
219
+ const rewritten = rewriteWithSpeechTags(text); // internal TTS optimization
220
+ const path = await myTTS(rewritten, { language: lang });
221
+ return { audioPath: path, transcriptText: text };
222
+ },
223
+ );
217
224
 
218
225
  // Surface diagnostics in /telegram-status
219
226
  recordTelegramRuntimeEvent("xai-voice", new Error("TTS complete"), {
@@ -238,6 +245,21 @@ Import `registerTelegramSection()` from `@llblab/pi-telegram/lib/extension-secti
238
245
 
239
246
  `telegram.json` can set `proactivePush: true` to send successful local non-Telegram final replies to the paired Telegram chat when no Telegram turn is active. Local prompt text is not mirrored because the bot does not own terminal user messages. The mode is off by default and can be toggled from settings.
240
247
 
248
+ ### Time context
249
+
250
+ `telegram.json` can opt into a compact `[time]` line in Telegram-originated prompts so π has a wall-clock reference for requests such as "today", "now", or scheduling. It is hidden by default and uses the system timezone; the mode can also be changed from Settings → `🕒 Time injection`.
251
+
252
+ ```json
253
+ {
254
+ "time": {
255
+ "injectionMode": "interval",
256
+ "interval": 3600000
257
+ }
258
+ }
259
+ ```
260
+
261
+ Modes are `hidden`, `always`, and `interval`. `hidden` means no time line is added to prompt context. `interval` is measured in milliseconds and rate-limits the time line per chat in memory, so back-to-back messages do not repeatedly spend context on the same timestamp. When present, `[time]` is the final prompt-context section after attachments, handler outputs, and voice policy.
262
+
241
263
  ## Docs
242
264
 
243
265
  - [Project Context](./AGENTS.md): durable engineering conventions and architecture constraints.
@@ -270,6 +292,12 @@ Third-party extensions that integrate with `pi-telegram`:
270
292
  pi install npm:pi-telegram-tool-status
271
293
  ```
272
294
 
295
+ - [`pi-xai-voice`](https://github.com/luxus/pi-xai-voice) — xAI voice companion for Telegram voice replies through the `pi-telegram` voice provider API.
296
+
297
+ ```bash
298
+ pi install npm:pi-xai-voice
299
+ ```
300
+
273
301
  ## License
274
302
 
275
303
  MIT
@@ -28,11 +28,11 @@ Current runtime areas use these ownership boundaries:
28
28
  - `config` / `setup`: persisted bot/session pairing state, authorization, first-user pairing, token prompting, env fallback, validation, and config persistence.
29
29
  - `locks` / `polling`: singleton `locks.json` ownership, takeover/restart semantics, long-poll controller state, update offset persistence, and poll-loop runtime wiring.
30
30
  - `updates` / `routing`: update classification/execution planning, paired authorization, reactions, edits, callbacks, and inbound route composition.
31
- - `media` / `text-groups` / `turns` / `inbound-handlers`: text/media extraction, media-group debounce, long-text split coalescing, inbound downloads, configured and programmatic inbound text/media handler execution, turn building/editing, image reads, and legacy `attachmentHandlers` compatibility.
31
+ - `media` / `text-groups` / `time-injection` / `turns` / `inbound-handlers`: text/media extraction, media-group debounce, long-text split coalescing, optional per-chat wall-clock prompt context, inbound downloads, configured and programmatic inbound text/media handler execution, turn building/editing, image reads, and legacy `attachmentHandlers` compatibility.
32
32
  - `queue`: queue item contracts, lane admission/order, stores, mutations, dispatch readiness/runtime, prompt/control enqueueing, and session/agent/tool lifecycle sequencing.
33
33
  - `runtime`: session-local coordination primitives: counters, lifecycle flags, setup guard, abort handler, typing-loop timers, prompt-dispatch flags, and agent-end reset binding.
34
34
  - `model` / `menu-model` / `menu-thinking` / `menu-status` / `menu` / `menu-queue` / `menu-settings` / `commands`: model identity/thinking levels, scoped model resolution, in-flight switching, model/thinking/status/queue/settings menu UI, inline application callback composition, slash commands, and bot command registration.
35
- - Future `extension-sections`: structured external Telegram menu sections registered by ordinary pi extensions; owns section registry, compact section callback tokens, section render/callback dispatch, safe section runtime ports, and diagnostics.
35
+ - `extension-sections`: structured external Telegram menu sections registered by ordinary pi extensions; owns section registry, compact section callback tokens, section render/callback dispatch, safe section runtime ports, and diagnostics.
36
36
  - `keyboard`: shared Telegram inline-keyboard reply-markup structure; feature domains own callback semantics and button construction.
37
37
  - `preview` / `replies` / `rendering`: preview lifecycle/transports, final reply delivery and reply parameters, Telegram HTML Markdown rendering, chunking, and stable-preview snapshots.
38
38
  - `outbound-handlers`: outbound text transformation, assistant-authored outbound comments, generated reply artifacts, inline-keyboard callbacks, and post-`agent_end` outbound action delivery.
@@ -44,7 +44,7 @@ Current runtime areas use these ownership boundaries:
44
44
  Boundary invariants:
45
45
 
46
46
  - Constants and state types live with their owning domains; do not reintroduce shared buckets such as `lib/constants.ts` or `lib/types.ts`
47
- - Shared Telegram inline-keyboard structure belongs to `keyboard`; application-control labels, callback data, and callback behavior stay in `menu`/`menu-model`/`menu-thinking`/`menu-status`/`menu-queue`; future external section labels, callbacks, and dispatch stay in `extension-sections`; core queue mechanics stay in `queue`
47
+ - Shared Telegram inline-keyboard structure belongs to `keyboard`; application-control labels, callback data, and callback behavior stay in `menu`/`menu-model`/`menu-thinking`/`menu-status`/`menu-queue`; external section labels, callbacks, and dispatch stay in `extension-sections`; core queue mechanics stay in `queue`
48
48
  - Domain helpers use narrow structural projections when that avoids importing concrete wire DTOs or broader runtime objects unnecessarily
49
49
  - Preview appearance stays in `rendering`; preview transport/lifecycle stays in `preview`
50
50
  - Direct `node:*` file-operation imports stay in owning domains, not in `index.ts`
@@ -64,7 +64,19 @@ Because `ctx.ui.input()` only exposes placeholder text, the bridge uses `ctx.ui.
64
64
 
65
65
  ## Runtime Ownership
66
66
 
67
- Telegram bot configuration stays in `~/.pi/agent/telegram.json`; singleton runtime ownership lives separately in `~/.pi/agent/locks.json` under `@llblab/pi-telegram`. `/telegram-connect` acquires or moves that lock before polling starts, and `/telegram-disconnect` stops polling and releases it. Session start may read the existing lock and resume polling when the lock already points at the current `pid`/`cwd`; after a full π process restart, it may also replace a stale lock from the same `cwd` and resume polling automatically. Session start does not create new ownership from an inactive lock, a live external lock, or a stale lock from another directory. Session replacement suspends polling and ownership watchers without releasing the lock, allowing the next session-start hook in the same `pid`/`cwd` to resume from the existing explicit ownership. When a live external owner exists, `/telegram-connect` asks whether to move singleton ownership to the current π instance. Active owners poll the lock while running through a snapshotted ownership context, so long-lived timers do not touch stale π contexts after `/new`; they stop local polling when `locks.json` no longer points at their own `pid`/`cwd`, without deleting the new owner lock. Deleting `locks.json` resets runtime ownership without deleting Telegram configuration.
67
+ Telegram bot configuration stays in `~/.pi/agent/telegram.json`. Singleton runtime ownership lives separately in `~/.pi/agent/locks.json` under `@llblab/pi-telegram`.
68
+
69
+ Ownership lifecycle:
70
+
71
+ - `/telegram-connect` acquires or moves the singleton lock before polling starts.
72
+ - `/telegram-disconnect` stops polling and releases the lock.
73
+ - Session start resumes polling when the existing lock already points at the current `pid`/`cwd`.
74
+ - After a full π process restart, session start may replace a stale lock from the same `cwd` and resume polling automatically.
75
+ - Session start does not create new ownership from an inactive lock, a live external lock, or a stale lock from another directory.
76
+ - Session replacement suspends polling and ownership watchers without releasing the lock, allowing the next session-start hook in the same `pid`/`cwd` to resume from explicit ownership.
77
+ - When a live external owner exists, `/telegram-connect` asks whether to move singleton ownership to the current π instance.
78
+
79
+ Active owners poll the lock through a snapshotted ownership context. Long-lived timers therefore avoid stale π contexts after `/new`; they stop local polling when `locks.json` no longer points at their own `pid`/`cwd`, without deleting the new owner lock. Deleting `locks.json` resets runtime ownership without deleting Telegram configuration.
68
80
 
69
81
  ## Message And Queue Flow
70
82
 
@@ -79,9 +91,10 @@ Telegram bot configuration stays in `~/.pi/agent/telegram.json`; singleton runti
79
91
  7. Configured inbound handlers may run on raw text or downloaded files by MIME wildcard, Telegram attachment type, or generic match selector; command templates receive safe command-arg substitution for `{text}`, `{file}`, `{mime}`, and `{type}` where applicable
80
92
  8. Matching media/file handlers are tried in config order: a non-zero exit records diagnostics and falls back to the next matching handler, while the first successful handler stops the chain
81
93
  9. Local attachments stay visible under `[attachments] <directory>` with relative file entries, and handler stdout is appended under `[outputs]` before the agent sees the turn; failed handlers omit output while keeping the attachment entry
82
- 10. A `PendingTelegramTurn` is created and queued locally
83
- 11. Telegram `edited_message` updates are routed separately and update a matching queued turn when the original message has not been dispatched yet
84
- 12. The queue dispatcher sends the turn into π only when dispatch is safe
94
+ 10. Optional `time` config may add a compact final `[time]` prompt line after attachment/output/voice sections, either every turn or per-chat after the configured millisecond interval, using the system timezone
95
+ 11. A `PendingTelegramTurn` is created and queued locally
96
+ 12. Telegram `edited_message` updates are routed separately and update a matching queued turn when the original message has not been dispatched yet
97
+ 13. The queue dispatcher sends the turn into π only when dispatch is safe
85
98
 
86
99
  ### Queue Safety Model
87
100
 
@@ -100,7 +113,19 @@ Admission contract:
100
113
  - Priority prompt queue: a waiting prompt promoted by `👍`, `⚡️`, `❤️`, `🕊`, or `🔥` uses `kind: prompt`, `queueLane: priority`, and dispatches at rank `1`.
101
114
  - Default prompt queue: normal Telegram text/media turns use `kind: prompt`, `queueLane: default`, and dispatch at rank `2`.
102
115
 
103
- The command action itself carries its execution mode, and the queue domain exposes lane contracts for admission mode, dispatch rank, and allowed item kinds. Queue append and planning paths validate lane admission so a malformed control/default or other invalid lane pairing fails predictably instead of silently changing priority. This lets synthetic control actions and Telegram prompts share one stable ordering model while still rendering distinctly in status output. In the π status bar, busy labels distinguish `active`, `dispatching`, `queued`, `tool running`, `model`, and `compacting`; priority prompts and priority control items are marked with `⚡`. If a queue mutation removes the last waiting item while Telegram-owned work still has running tools, the status remains yellow `active` instead of degrading to green `connected`.
116
+ The command action itself carries its execution mode. The queue domain exposes lane contracts for admission mode, dispatch rank, and allowed item kinds.
117
+
118
+ Queue validation rules:
119
+
120
+ - Queue append and planning paths validate lane admission.
121
+ - Malformed control/default or other invalid lane pairings fail predictably instead of silently changing priority.
122
+ - Synthetic control actions and Telegram prompts share one stable ordering model while still rendering distinctly in status output.
123
+
124
+ Status rendering rules:
125
+
126
+ - Busy labels distinguish `active`, `dispatching`, `queued`, `tool running`, `model`, and `compacting`.
127
+ - Priority prompts and priority control items are marked with `⚡`.
128
+ - If a queue mutation removes the last waiting item while Telegram-owned work still has running tools, the status remains yellow `active` instead of degrading to green `connected`.
104
129
 
105
130
  A dispatched prompt remains in the queue until `agent_start` consumes it. That keeps the active Telegram turn bound correctly for previews, attachments, abort handling, and final reply delivery.
106
131
 
@@ -112,9 +137,49 @@ Dispatch is gated by:
112
137
  - `ctx.isIdle()` being true
113
138
  - `ctx.hasPendingMessages()` being false
114
139
 
115
- This prevents queue races around rapid follow-ups, `/compact`, and mixed local plus Telegram activity. Post-agent-end dispatch retries are scheduled through a session-bound deferred dispatcher that activates on session start, cancels timers on session shutdown, and skips callbacks from older generations before they touch `ExtensionContext`. Telegram `/start` and hidden compatibility shortcuts `/status`, `/model`, `/thinking`, `/queue`, and `/settings` execute immediately; the dispatch controller still serializes any deferred control items so a queued control action must settle before the next queued action can dispatch.
140
+ These gates prevent queue races around rapid follow-ups, `/compact`, and mixed local plus Telegram activity.
141
+
142
+ Post-agent-end dispatch retries use a session-bound deferred dispatcher:
143
+
144
+ - It activates on session start.
145
+ - It cancels timers on session shutdown.
146
+ - It skips callbacks from older generations before they touch `ExtensionContext`.
147
+
148
+ Telegram `/start` and hidden compatibility shortcuts `/status`, `/model`, `/thinking`, `/queue`, and `/settings` execute immediately. The dispatch controller still serializes deferred control items so a queued control action must settle before the next queued action can dispatch.
116
149
 
117
- `/start` opens the main application menu: visible command help, compact command-only prompt-template rows when π exposes Telegram-compatible prompt-template names, status rows (`Status`, `Usage`, `Cost`, `Context`), and top-level buttons for model, thinking, and queue sections. The `Status` row reports `compacting` while a Telegram `/compact` run is active, and the bridge sends Telegram's native `typing` chat action as a keepalive for the same compaction window. The Queue button includes the current queued-item count. Hidden compatibility shortcuts `/help`, `/status`, `/model`, `/thinking`, and `/queue` jump directly to their corresponding menu screens, while `/settings` opens the hidden settings menu for bridge toggles such as proactive push. Settings options open detail submenus; checkbox-like settings use Back plus green/black/yellow `on` and `off` controls instead of mutating directly from the list. Command emoji come from the `commands` domain map so visible command descriptions and matching menu buttons share one fixed adornment source. Prompt-template commands use a fixed `🧩` marker, map π template names to Telegram-safe aliases such as `fix-tests` → `/fix_tests`, stay visible only inside the `/start` menu, and expand before queueing because `ExtensionAPI.sendUserMessage()` intentionally bypasses π prompt-template expansion for extension-originated messages. Every submenu starts with a top Back row so navigation stays anchored near the original user message above the inline keyboard; model-menu pagination controls sit near the top, tapping the pagination indicator opens a compact page picker headed by `<b>Choose a page:</b>`, and tapping a model opens a detail submenu with Back, ☑️ Activate/🟢 Active selection, and yellow/black-marked Scoped/All membership tabs. `menu-model` owns model-menu state, scoped model pages, model detail rendering, scoped-list persistence planning, and model-menu rendering while `model` owns core model identity/switching semantics. `menu-thinking` owns thinking-menu text, reply markup, callback handling, and message rendering. `menu-status` owns status-menu payloads, status callback handling, and status-message rendering. `menu-queue` owns queue-menu UI only: queue items are rendered under a compact `<b>Queue:</b>` heading, top-to-bottom in dispatch order, numbered, and marked with `⚡` for priority prompts or `📎` for prompts with attachments. An empty queue renders bold message text with the bottom-filled `⌛` hourglass plus the top Main menu button, while non-empty queue states keep the running `⏳` hourglass. Selecting an item opens a submenu that displays the queue item number above the full queued prompt text with Back, side-by-side Priority/Normal tabs, and Cancel. If a callback targets an item that has already left the queue, the menu refreshes the list instead of applying a stale mutation.
150
+ ### Application Menu Shape
151
+
152
+ `/start` opens the main application menu. It contains visible command help, compact command-only prompt-template rows when π exposes Telegram-compatible prompt-template names, status rows (`Status`, `Usage`, `Cost`, `Context`), and top-level buttons for model, thinking, and queue sections.
153
+
154
+ Menu rules:
155
+
156
+ - The `Status` row reports `compacting` while a Telegram `/compact` run is active, and the bridge sends Telegram's native `typing` chat action as a keepalive for the same compaction window.
157
+ - The Queue button includes the current queued-item count.
158
+ - Hidden compatibility shortcuts `/help`, `/status`, `/model`, `/thinking`, and `/queue` jump directly to their corresponding menu screens.
159
+ - `/settings` opens the hidden settings menu for bridge toggles such as proactive push, voice reply mode, and `time.injectionMode`.
160
+ - Settings options open detail submenus. Boolean settings use Back plus green/black/yellow `on` and `off` controls; list-like settings such as time injection use explicit mode names like `hidden`, `always`, and `interval`.
161
+ - Command emoji come from the `commands` domain map so visible command descriptions and matching menu buttons share one fixed adornment source.
162
+ - Prompt-template commands use a fixed `🧩` marker, map π template names to Telegram-safe aliases such as `fix-tests` → `/fix_tests`, stay visible only inside the `/start` menu, and expand before queueing because `ExtensionAPI.sendUserMessage()` bypasses π prompt-template expansion for extension-originated messages.
163
+
164
+ Navigation and ownership:
165
+
166
+ - Every submenu starts with a top Back row so navigation stays anchored near the original user message above the inline keyboard.
167
+ - Model-menu pagination controls sit near the top; tapping the pagination indicator opens a compact page picker headed by `<b>Choose a page:</b>`.
168
+ - Tapping a model opens a detail submenu with Back, ☑️ Activate/🟢 Active selection, and yellow/black-marked Scoped/All membership tabs.
169
+ - `model` owns core model identity/switching semantics.
170
+ - `menu-model` owns model-menu state, scoped model pages, model detail rendering, scoped-list persistence planning, and model-menu rendering.
171
+ - `menu-thinking` owns thinking-menu text, reply markup, callback handling, and message rendering.
172
+ - `menu-status` owns status-menu payloads, status callback handling, and status-message rendering.
173
+ - `menu-queue` owns queue-menu UI only.
174
+
175
+ Queue menu rendering:
176
+
177
+ - Queue items render under a compact `<b>Queue:</b>` heading, top-to-bottom in dispatch order.
178
+ - Items are numbered and marked with `⚡` for priority prompts or `📎` for prompts with attachments.
179
+ - An empty queue renders bold message text with the bottom-filled `⌛` hourglass plus the top Main menu button.
180
+ - Non-empty queue states keep the running `⏳` hourglass.
181
+ - Selecting an item opens a submenu with the queue item number, full queued prompt text, Back, side-by-side Priority/Normal tabs, and Cancel.
182
+ - If a callback targets an item that has already left the queue, the menu refreshes the list instead of applying a stale mutation.
118
183
 
119
184
  ### Abort Behavior
120
185
 
@@ -153,32 +218,74 @@ Preferred order:
153
218
 
154
219
  Draft streaming can remain as a plain-text fallback path, but rich Telegram previews are driven through editable messages and stable-block snapshot selection.
155
220
 
156
- Telegram prompt responses use explicit delivery context to attach outbound text, rich previews, errors, attachment notices, and uploads as Telegram replies to the source prompt when possible. Reply metadata is opt-in per delivery path, uses `reply_parameters` with `allow_sending_without_reply: true`, and is applied only to the first chunk of split long responses; continuation chunks are sent as normal adjacent messages. Media-group turns reply to the turn's representative `replyToMessageId`, not to every source message in the group. Long text split coalescing is intentionally conservative: only human text messages at or above the 3600-character near-limit threshold open the short debounce window, immediate same-chat/user contiguous text tails join that prompt, and commands, bot messages, captions, media groups, and normal short follow-ups bypass the coalescer.
221
+ ### Response Context
222
+
223
+ Telegram prompt responses use explicit delivery context to attach outbound text, rich previews, errors, attachment notices, and uploads as Telegram replies to the source prompt when possible.
224
+
225
+ Reply metadata rules:
226
+
227
+ - Reply metadata is opt-in per delivery path.
228
+ - It uses `reply_parameters` with `allow_sending_without_reply: true`.
229
+ - It is applied only to the first chunk of split long responses; continuation chunks are sent as normal adjacent messages.
230
+ - Media-group turns reply to the turn's representative `replyToMessageId`, not to every source message in the group.
157
231
 
158
- Outbound files are sent only after the active Telegram turn completes, must be staged through the `telegram_attach` tool, are staged atomically per tool call, are checked against a default 50 MiB limit configurable through `PI_TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES` or `TELEGRAM_MAX_ATTACHMENT_SIZE_BYTES`, and use file-backed multipart blobs so large sends do not require preloading whole files into memory.
232
+ Long text split coalescing is intentionally conservative. Only human text messages at or above the 3600-character near-limit threshold open the short debounce window. Immediate same-chat/user contiguous text tails join that prompt; commands, bot messages, captions, media groups, and normal short follow-ups bypass the coalescer.
159
233
 
160
- Assistant-authored outbound actions use final-message markup instead of agent tool calls. Preview updates strip closed top-level HTML comments and currently open/partial top-level comment starts before rendering, so users do not see transient metadata even when streaming flushes happen after only `<`, `<!`, or `<!--`. On `agent_end`, the bridge removes top-level comments from the Markdown text reply, but treats column-zero top-level `<!-- telegram_voice ... -->` and `<!-- telegram_button ... -->` blocks specially before delivery; comments inside fenced code, quotes, lists, or indented examples stay literal, including fenced blocks with Markdown-valid indented closing fences. Voice uses a single fallback pipeline: configured `outboundHandlers` with `type: "voice"`, then programmatic `voice` handlers, then registered synthesis providers from `lib/voice.ts`. The bridge extracts body text, `text="..."`, or colon shorthand, asks the pipeline for an `.ogg`/`.opus` artifact, validates native voice format, and uploads the generated file via Telegram `sendVoice`; when delivery fails, the queue runtime records diagnostics and falls back to the planned text reply when no text was already delivered. Synthesis providers own TTS, speech rewriting, transcript choice, and format conversion. Button blocks are built in: each `telegram_button` block becomes one inline-keyboard button on the final text, and callback clicks enqueue the configured prompt text as a normal Telegram prompt turn; the `telegram_button: Label` shorthand uses the same text for label and prompt, `prompt="..."` supports explicit one-line prompts, and body-form buttons use the body as the prompt. Unknown callback data that does not match pi-telegram-owned prefixes (`tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`, future `section:`) is forwarded to π as `[callback] <data>` after built-in handlers decline it, giving layered extensions a simple namespaced button channel without separate polling; layered callback payloads should follow the [Callback Namespace Standard](./callback-namespaces.md). Future structured menu integrations should use the [Telegram Extension Sections Standard](./extension-sections.md) instead of hand-rolled fallback callbacks. When proactive push is enabled, successful local non-Telegram final replies are sent to the paired chat. Local prompt text is not sent because the bot does not own or mirror terminal user messages. This keeps terminal-originated results visible in Telegram without changing Telegram-originated turn delivery.
234
+ ### Outbound Files
161
235
 
162
- This keeps technical Markdown, code, tables, formulas, and numbered lists in the text channel when appropriate while allowing TTS-friendly voice messages and tappable continuations without invoking `telegram_attach` or extra transport tools. Telegram prompt guidance targets about 37 visible cells for tables, dense list items, and compact text blocks because emoji and other wide glyphs make raw character counts misleading on mobile screens.
236
+ Outbound files are sent only after the active Telegram turn completes. They must be staged through the `telegram_attach` tool, are staged atomically per tool call, and are checked against a default 50 MiB limit configurable through `PI_TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES` or `TELEGRAM_MAX_ATTACHMENT_SIZE_BYTES`. Delivery uses file-backed multipart blobs so large sends do not require preloading whole files into memory.
237
+
238
+ ### Assistant-Authored Actions
239
+
240
+ Assistant-authored outbound actions use final-message markup instead of agent tool calls. Preview updates strip closed top-level HTML comments and currently open/partial top-level comment starts before rendering, so users do not see transient metadata even when streaming flushes happen after only `<`, `<!`, or `<!--`.
241
+
242
+ On `agent_end`, the bridge removes top-level comments from the Markdown text reply, but treats these column-zero top-level blocks specially before delivery:
243
+
244
+ - `<!-- telegram_voice ... -->`
245
+ - `<!-- telegram_button ... -->`
246
+
247
+ Comments inside fenced code, quotes, lists, or indented examples stay literal, including fenced blocks with Markdown-valid indented closing fences.
248
+
249
+ Voice delivery uses one fallback pipeline:
250
+
251
+ 1. Configured `outboundHandlers` with `type: "voice"`
252
+ 2. Programmatic `voice` handlers
253
+ 3. Registered synthesis providers from `lib/voice.ts`
254
+
255
+ The bridge extracts body text, `text="..."`, or colon shorthand, asks the pipeline for an `.ogg`/`.opus` artifact, validates native voice format, and uploads the generated file via Telegram `sendVoice`. When delivery fails, the queue runtime records diagnostics and falls back to the planned text reply when no text was already delivered. Synthesis providers own TTS, speech rewriting, transcript choice, and format conversion.
256
+
257
+ Button blocks are built in. Each `telegram_button` block becomes one inline-keyboard button on the final text, and callback clicks enqueue the configured prompt text as a normal Telegram prompt turn. The `telegram_button: Label` shorthand uses the same text for label and prompt, `prompt="..."` supports explicit one-line prompts, and body-form buttons use the body as the prompt.
258
+
259
+ Unknown callback data that does not match pi-telegram-owned prefixes (`tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`, `section:`) is forwarded to π as `[callback] <data>` after built-in handlers decline it. Layered callback payloads should follow the [Callback Namespace Standard](./callback-namespaces.md). Structured menu integrations should use the [Telegram Extension Sections Standard](./extension-sections.md) instead of hand-rolled fallback callbacks.
260
+
261
+ ### Proactive Push And Mobile Guidance
262
+
263
+ When proactive push is enabled, successful local non-Telegram final replies are sent to the paired chat. Local prompt text is not sent because the bot does not own or mirror terminal user messages. This keeps terminal-originated results visible in Telegram without changing Telegram-originated turn delivery.
264
+
265
+ Technical Markdown, code, tables, formulas, and numbered lists stay in the text channel when appropriate while TTS-friendly voice messages and tappable continuations do not require `telegram_attach` or extra transport tools. Telegram prompt guidance targets about 37 visible cells for tables, dense list items, and compact text blocks because emoji and other wide glyphs make raw character counts misleading on mobile screens.
163
266
 
164
267
  ## Interactive Controls
165
268
 
166
- The bridge exposes Telegram-side session controls in addition to regular chat forwarding.
167
-
168
- Current operator controls include:
169
-
170
- - `/start` for the main application menu: command help, prompt-template commands, model, usage, cost, context visibility, and inline controls, executed immediately from Telegram even while generation is active
171
- - Inline application-menu buttons for model, thinking, and queue controls, applying idle selections immediately while still respecting busy-run restart rules; model-menu inputs are cached briefly and stored inline-menu states are pruned by TTL/LRU so old keyboards expire predictably
172
- - Hidden `/model` and `/thinking` shortcuts for opening the model and thinking sections directly while keeping settings out of the visible bot command menu
173
- - `/compact` for Telegram-triggered π session compaction when the bridge is idle
174
- - `/queue` for opening the queue section of the inline application menu; the same section is reachable from the status/main menu and supports top-anchored Back navigation, Priority/Normal tabs, and cancellation
175
- - `/next` for dispatching the next queued turn, aborting the active run first when π is busy
176
- - `/continue` for enqueueing a Telegram-owned `continue` prompt, without aborting the current turn or forcing the next queued item
177
- - `/abort` for aborting the active Telegram-owned run while preserving queued items for manual continuation
178
- - `/stop` for aborting the active Telegram-owned run and clearing waiting Telegram queue items
179
- - `/telegram-status` for π-side diagnostics as grouped line-by-line sections separated by blank lines: connection, polling, execution, queue, and the recent redacted runtime/API event ring. These sections include polling state, last update id, active turn source ids, pending dispatch, compaction state, active tool count, pending model-switch state, total queue depth, and queue-lane counts. The event ring records transport/API, polling/update, prompt-dispatch, control-action, typing, compaction, setup, session-lifecycle, and attachment queue/delivery failures; benign unchanged edit responses and unsupported empty draft-clear attempts are filtered out so expected preview transport noise does not obscure real failures
180
- - `/telegram-settings` for π-side bridge settings; it currently exposes proactive push as a local toggle backed by the same `telegram.json` flag as the hidden Telegram `/settings` menu
181
- - Queue reactions apply to waiting text, voice, file, image, and media-group turns by matching the turn's source Telegram message ids: `👍`, `⚡️`, `❤️`, `🕊`, and `🔥` promote waiting prompts, while `👎`, `👻`, `💔`, `💩`, and `🗑` remove waiting turns because ordinary Telegram DM message deletions are not exposed through the Bot API polling path this bridge uses
269
+ The bridge exposes Telegram-side session controls in addition to regular chat forwarding. Menu layout details live in [Application Menu Shape](#application-menu-shape); this section summarizes the command/control surface.
270
+
271
+ Telegram chat controls:
272
+
273
+ - `/start`: opens the main application menu and runs immediately even while generation is active.
274
+ - `/model`, `/thinking`, `/queue`: hidden shortcuts for opening the matching menu sections directly.
275
+ - `/compact`: triggers π session compaction when the bridge is idle.
276
+ - `/next`: dispatches the next queued turn, aborting the active run first when π is busy.
277
+ - `/continue`: enqueues a Telegram-owned priority `continue` prompt without aborting the current turn.
278
+ - `/abort`: aborts the active Telegram-owned run while preserving queued items for manual continuation.
279
+ - `/stop`: aborts the active Telegram-owned run and clears waiting Telegram queue items.
280
+
281
+ Pi-side diagnostics and settings:
282
+
283
+ - `/telegram-status`: renders grouped diagnostics for connection, polling, execution, queue, and the recent redacted runtime/API event ring.
284
+ - `/telegram-settings`: exposes π-side bridge settings; currently this includes proactive push backed by the same `telegram.json` flag as the hidden Telegram `/settings` menu.
285
+
286
+ Queue reactions are shortcut controls for waiting text, voice, file, image, and media-group turns. Matching uses the turn's source Telegram message ids. `👍`, `⚡️`, `❤️`, `🕊`, and `🔥` promote waiting prompts; `👎`, `👻`, `💔`, `💩`, and `🗑` remove waiting turns because ordinary Telegram DM message deletions are not exposed through the Bot API polling path this bridge uses.
287
+
288
+ The `/telegram-status` event ring records transport/API, polling/update, prompt-dispatch, control-action, typing, compaction, setup, session-lifecycle, and attachment queue/delivery failures. Benign unchanged edit responses and unsupported empty draft-clear attempts are filtered out so expected preview transport noise does not obscure real failures.
182
289
 
183
290
  ## In-Flight Model Switching
184
291