@llblab/pi-telegram 0.12.0 → 0.13.1

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
@@ -60,7 +60,7 @@
60
60
  - Queue admission is explicit and validated: immediate commands, control lane, priority lane, and default lane must preserve allowed kind/lane pairings
61
61
  - Dispatch is gated by active turns, pending dispatch, unsettled control work, compaction, `ctx.isIdle()`, and π pending messages; dispatched prompts remain queued until `agent_start` consumes them
62
62
  - Telegram `/compact` owns a native `typing` keepalive for the compaction window so phone clients show activity between the started/completed notices; stop it on both completion and failure
63
- - `/stop`, `/abort`, `/next`, and `/continue` have distinct contracts: reset queue and abort; abort while preserving queue; force next queued turn; enqueue a priority `continue` prompt
63
+ - `/stop`, `/abort`, `/next`, and `/continue` have distinct contracts: reset queue and abort; abort while preserving queue; force next queued turn; enqueue a control-lane `continue` resume prompt without folding queued prompts into history
64
64
  - `/start`, `/help`, and `/status` open the unified command-help/status-row/control menu; `/model`, `/thinking`, and `/queue` jump to sections directly; visible bot commands are `/start`, `/compact`, `/next`, `/continue`, `/abort`, `/stop`
65
65
  - Command/menu emoji are fixed UI adornments owned by the `commands` map; do not add a persisted emoji toggle or Settings menu until there is a real setting to own
66
66
  - Telegram `reply_to_message` context is prompt-only and must not affect slash-command parsing
@@ -97,7 +97,7 @@
97
97
 
98
98
  - Keep comments and user-facing docs in English unless the surrounding file already follows another convention
99
99
  - Each project `.ts` file should start with a short multi-line responsibility header comment that explains the file boundary to future maintainers; source-module headers must include `Zones:` tags for cross-cutting responsibility areas
100
- - Name extracted `/lib` modules and mirrored `/tests` suites by bare domain when the repository already supplies the Telegram scope; prefer `api.ts`, `queue.ts`, `updates.ts`, and `queue.test.ts` over redundant `telegram-*` filename prefixes
100
+ - Name extracted `/lib` modules and mirrored `/tests` suites by bare domain when the repository already supplies the Telegram scope; prefer `queue.ts`, `updates.ts`, and `queue.test.ts` over redundant `telegram-*` filename prefixes. Exception: the concrete Bot API transport domain is named `telegram-api.ts` / `telegram-api.test.ts` to avoid ambiguity with the public `/api/*.ts` package membranes
101
101
  - Keep test helpers with the mirrored domain suite by default because test files mirror module-domain boundaries; introduce shared `tests/fixtures` only when multiple domain suites truly reuse the same setup
102
102
  - Prefer targeted edits, keeping `index.ts` as the orchestration layer and moving reusable logic into flat `/lib` domain modules when a subsystem becomes large enough to earn extraction
103
103
  - Keep composition wiring DRY with small local adapters or owning-domain contracts when repetition appears, but do not hide live mutable session state behind broad facades just to reduce repeated closures
@@ -109,7 +109,7 @@ The canonical detailed ownership map lives in [`docs/architecture.md`](./docs/ar
109
109
 
110
110
  - Scheduling and lifecycle: `queue`, `runtime`, `lifecycle`, `locks`
111
111
  - Telegram transport and inbound flow: `api`, `polling`, `updates`, `routing`, `media`, `turns`, `inbound`, `config`, `setup`
112
- - Response surfaces: `preview`, `replies`, `rendering`, `keyboard`, `outbound-attachments`, `outbound`, `voice`, `status`
112
+ - Response surfaces: `preview`, `replies`, `rendering`, `keyboard`, `outbound-markup`, `outbound-attachments`, `outbound`, `voice`, `status`
113
113
  - Controls and application menu UI: `commands`, `menu`, `menu-model`, `menu-thinking`, `menu-status`, `menu-queue`, `model`, `prompts`
114
114
  - Extension platform: `sections` owns section registry, token mapping, callback dispatch, context building, and its globalThis bridge; `voice` owns the voice-provider registry and its globalThis bridge
115
115
  - Pi SDK boundary: `pi` owns direct pi imports and bound extension API ports; `bindings` owns pi-facing command/tool/lifecycle registration wiring extracted from the entrypoint
@@ -119,7 +119,7 @@ The canonical detailed ownership map lives in [`docs/architecture.md`](./docs/ar
119
119
  - Keep preview appearance logic in the rendering domain and preview transport/lifecycle logic in the preview domain so richer streaming strategies can evolve without entangling Telegram delivery state with Markdown formatting rules
120
120
  - Keep direct `node:*` file-operation dependencies out of `index.ts` when an owning domain exists; the entrypoint should compose ports while domains own local filesystem details such as temp-dir preparation, attachment stats, and turn image reads
121
121
  - In `index.ts`, prefer namespace imports for local bridge domains so orchestration reads as domain-scoped calls such as `Queue.*`, `Turns.*`, and `Rendering.*` instead of long flat import lists
122
- - Keep the local `index.ts` plus `/lib/*.ts` import graph acyclic; `tests/invariants.test.ts` guards this boundary plus shared-bucket bans, empty interface-extension shell regressions, pi SDK centralization, source-only entrypoint Node-runtime/local-adapter/process/direct-pi access avoidance, runtime-domain isolation, structural leaf-domain import isolation, menu/model boundary drift, API/config default coupling, structural update/media coupling to API transport shapes, and attachment coupling to queue/inbound media/API helpers as domains keep evolving
122
+ - Keep the local `index.ts` plus `/lib/*.ts` import graph acyclic; `tests/invariants.test.ts` guards this boundary plus shared-bucket bans, empty interface-extension shell regressions, pi SDK centralization, source-only entrypoint Node-runtime/local-adapter/process/direct-pi access avoidance, runtime-domain isolation, structural leaf-domain import isolation, menu/model boundary drift, Telegram API/config default coupling, structural update/media coupling to Telegram API transport shapes, and attachment coupling to queue/inbound media/Telegram API helpers as domains keep evolving
123
123
  - Do not reintroduce shared bucket domains such as `lib/constants.ts`, `lib/types.ts`, `lib/globals.ts`, or broad global-augmentation files; constants, registry keys, state interfaces, and concrete transport shapes should stay in their owning domains, and `index.ts` should not grow new shared magic constants
124
124
  - Keep remaining `index.ts` code focused on cross-domain adapter wiring that needs live extension state, pi callbacks, Telegram API ports, or status updates; do not extract one-off closures solely to reduce line count
125
125
  - Domain-specific queue planning, preview transport/controller behavior, rendering, Telegram API transport, menu state, and command behavior should stay in their owning domains instead of moving to `/lib/runtime.ts` solely to shrink `index.ts`
@@ -153,12 +153,13 @@ The canonical detailed ownership map lives in [`docs/architecture.md`](./docs/ar
153
153
  - Unknown callback data not owned by pi-telegram prefixes (`compact:`, `tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`, `section:`, `settings:`) may be forwarded as `[callback] <data>` after built-in handlers decline it; update-handler extensions should follow `docs/callback-namespaces.md` and must not poll the same bot independently
154
154
  - Command templates stay compact and shell-free: no `command` field, no shell execution, inline defaults are allowed as `{name=default}`, `template` may be a string or an ordered composition array, only `args`/`defaults` inherit into leaves, top-level `timeout` wraps composed sequences, stdout pipes to the next step's stdin by default, and multi-step work should use `template: [...]` rather than provider-specific fields; `pipe` is only a legacy local alias
155
155
  - Command-template documentation examples should use portable executable placeholders such as `/path/to/stt` and `/path/to/tts`, not host-local skill paths or machine-specific install locations
156
+ - Abstract pi-telegram docs and README examples must not leak real companion-extension identities into generic provider ids, runtime-event categories, or sample code. Use neutral ids such as `@scope/voice-provider/tts` and reserve real companion names for explicit companion-extension lists or case-study references.
156
157
 
157
158
  ## 9. Extension Sections Conventions
158
159
 
159
160
  - `Section identity`: use the same identity-key rules as the Extension Locks Standard (`package.json/name` → canonical id); no separate `owner` field
160
161
  - `Token mapping`: Telegram's 64-byte `callback_data` limit forces compact numeric tokens (`section:0:action:payload`). Section authors never hand-roll `section:` strings — use `ctx.callbackData(action, payload?)`
161
- - `Navigation hierarchy`: Back buttons are auto-prepended by `ctx.edit()` / `ctx.open()`. Root views use `⬆️ Main menu` → `menu:back`. Nested views from `handleCallback` use `⬆️ Back` → `section:<token>:open`. Settings views use `⬆️ Back` → `settings:list`
162
+ - `Navigation hierarchy`: Back buttons are auto-prepended by `ctx.edit()` only. Root views use `⬆️ Main menu` → `menu:back`. Nested views from `handleCallback` use `⬆️ Back` → `section:<token>:open`. Settings views use `⬆️ Back` → `settings:list`. `ctx.open()` sends standalone chat messages without auto-navigation
162
163
  - `Context ports`: sections receive `TelegramSectionContext` / `TelegramSectionCallbackContext` with `answerCallback`, `edit`, `open`, `enqueuePrompt`, and `callbackData`. No filesystem access, no raw bot clients, no second polling loop
163
164
  - `Settings indicators`: use `settings.getLabel()` for dynamic status rows in the Settings submenu (e.g., `🟢`/`⚫️` based on internal state). Called on every Settings list render
164
165
  - `Handler fallback`: `section.handleCallback` runs first; if it returns `"pass"` and `settings.handleCallback` exists, the settings handler runs with a fresh context carrying `backCallback="settings:list"`
@@ -168,7 +169,7 @@ The canonical detailed ownership map lives in [`docs/architecture.md`](./docs/ar
168
169
  - `Section separators`: extension-injected main-menu rows appear before the **⚙️ Settings** row. Extension settings rows appear before built-in Proactive push controls
169
170
  - `Model button format`: use `provider/ModelId` format (e.g., `anthropic/claude-sonnet-4-5`) across model menu buttons and status row. The compact `provider/id` form is canonical
170
171
  - `Section domain ownership`: `lib/sections.ts` owns the registry, token mapping, callback dispatch, and context building. `lib/menu.ts` dispatches `section:` callbacks before built-in handling. `lib/menu-status.ts` injects section rows. `lib/menu-settings.ts` injects settings rows and passes `sectionRegistry` through callback deps
171
- - `Callback routing order`: button actions → queue menu → settings menu → section callbacks → built-in menu handling → `[callback]` fallback. Settings menu callbacks always pass `sectionRegistry` to `updateTelegramSettingsMenuMessage` and `handleTelegramSettingsMenuCallbackAction`
172
+ - `Callback routing order`: button actions → compact confirmations → queue menu → settings menu → section callbacks → built-in menu handling → `[callback]` fallback. Settings menu callbacks always pass `sectionRegistry` to `updateTelegramSettingsMenuMessage` and `handleTelegramSettingsMenuCallbackAction`
172
173
 
173
174
  ## 9. Pre-Task Preparation Protocol
174
175
 
package/BACKLOG.md CHANGED
@@ -1,3 +1,28 @@
1
1
  # Project Backlog
2
2
 
3
- ## Open Work
3
+ ## Bound inbound handler output
4
+
5
+ `Task`: Bound inbound handler stdout, stderr, and recorded failure text before they enter prompts or runtime status.
6
+
7
+ `Why`: Large OCR, PDF, STT, or failing command output can inflate prompt context, memory use, and `/telegram-status`.
8
+
9
+ `Exit criteria`:
10
+
11
+ - Handler stdout added to `[outputs]` is truncated or externalized behind a bounded artifact reference.
12
+ - Handler stderr and stdout included in failure messages are bounded.
13
+ - Runtime event messages and details are bounded before storage and rendering.
14
+ - Regression tests cover large handler stdout and large failure output.
15
+
16
+ ## Recover from invalid config JSON
17
+
18
+ `Task`: Make `telegram.json` load failures recoverable without bricking pi-telegram session startup.
19
+
20
+ `Why`: A hand-edited or partially written invalid config currently bubbles `JSON.parse` failure through session start, which can block the normal repair path.
21
+
22
+ `Exit criteria`:
23
+
24
+ - Invalid config JSON is reported through a runtime event or clear status diagnostic.
25
+ - Session startup continues with safe empty config defaults.
26
+ - The invalid file is preserved or renamed for operator recovery.
27
+ - `/telegram-setup` remains usable after an invalid config is detected.
28
+ - Regression tests cover invalid config startup behavior.
package/CHANGELOG.md CHANGED
@@ -2,7 +2,37 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
- - Nothing yet.
5
+ ## 0.13.1: Rendering, Typing, And Continue Queue Hotfix
6
+
7
+ - `[Rendering]` Fixed Telegram HTML rendering for Markdown bold/italic spans that cross soft line breaks, so assistant replies like `**first line\nsecond line**` render as bold text instead of showing raw asterisks. Added a regression for the guest-mode-style multiline bold reply shape.
8
+ - `[Typing Status]` Hardened assistant message activity hooks so transient preview/provider transport failures are recorded but do not break the native Telegram `typing` keepalive while an active turn continues.
9
+ - `[Continue Queue]` `/continue` now enqueues as a control-lane resume prompt and explicitly clears preserved-abort history mode, so queued Telegram prompts stay separate and the continuation runs ahead of queued prompt work after abort or compaction recovery.
10
+
11
+ ## 0.13.0: Command Template Standard, Voice Hardening, And Domain Cleanup
12
+
13
+ - `[Architecture]` Extracted outbound assistant-action markup parsing into `lib/outbound-markup.ts` and removed the temporary Voice/Outbound/Queue import-cycle allowance. Impact: project source imports are fully acyclic again while preserving existing voice and outbound helper exports.
14
+ - `[Tests]` Updated the Pi SDK centralization invariant to guard the current `@earendil-works/*` package scope as well as the legacy scope. Impact: new direct SDK imports outside `lib/pi.ts` are caught again.
15
+ - `[Voice Providers]` Generated compatibility ids for STT/TTS providers now use monotonic counters with registry probing, and provider disposers now remove only their own registered instance. Impact: anonymous ids stay collision-safe across stale registry entries/module reloads, and stale disposers cannot delete replacement providers.
16
+ - `[Security]` Telegram temp directories, downloaded inbound files, and `locks.json` writes now use explicit private filesystem modes. Impact: private attachments and local polling ownership metadata are less exposed on permissive-umask or shared hosts.
17
+ - `[Docs]` Clarified that `voice.sendTranscript` is the bridge-owned transcript preference and updated provider examples to gate `transcriptText` with `getTelegramVoiceSendTranscript(config)`. Impact: companion voice providers no longer need to invent duplicate transcript/reply-policy UI.
18
+ - `[Tests]` Added a long-session runtime regression that preserves queued work across `/abort`, explicit `/next`, and in-flight model switching. Impact: the high-risk queue/dispatch path now has coverage for an operator session that chains abort recovery into model-switch continuation.
19
+ - `[Naming]` Renamed the concrete Bot API transport domain from `lib/api.ts` / `tests/api.test.ts` to `lib/telegram-api.ts` / `tests/telegram-api.test.ts`. Impact: the transport owner is clearer and no longer competes conceptually with the public `/api/*.ts` package membranes.
20
+ - `[Status]` `/telegram-status` recent runtime events now include a category summary before the detailed event list. Impact: provider, handler, and transport failure categories are easier to scan during companion-extension diagnostics.
21
+ - `[Tests]` Added a combined `telegram_voice` + `telegram_button` outbound planner smoke regression and corrected the text-group non-contiguous-tail fixture to reflect the widened split-message id gap. Impact: the rendering/markup watchlist now has direct coverage for mixed assistant action markup without changing runtime behavior.
22
+ - `[Audit]` Added fixture lifecycle coverage for STT/TTS provider re-registration across session-start/resume/reload-style boundaries, and scanned the local workspace for removed `@llblab/pi-telegram/lib/*` companion imports. Impact: the remaining hotfix lifecycle and migration audit items are now closed in this environment.
23
+ - `[Refactor]` Split assistant-authored button planning, callback storage, callback handling, and button prompt-turn construction from `lib/outbound.ts` into `lib/outbound-buttons.ts`, with symmetric `tests/outbound-buttons.test.ts` coverage. Impact: the outbound domain is smaller and button behavior now has a direct module-level test peer while existing `lib/outbound.ts` exports remain compatible.
24
+ - `[Tests]` Added symmetric `tests/outbound-markup.test.ts` coverage for `lib/outbound-markup.ts`, including top-level comment collection, fenced-code exclusion, partial-comment stripping, attribute parsing, and multi-voice planning. Impact: assistant action markup has direct module-level regression coverage instead of relying only on umbrella outbound tests.
25
+ - `[Tests]` Added symmetric `tests/menu-status.test.ts` coverage for `lib/menu-status.ts`, including status reply markup rows, voice-active thinking suppression, callback routing, and status message send/update helpers. Impact: status-menu behavior now has a direct module-level regression peer.
26
+ - `[Tests]` Added symmetric `tests/menu-thinking.test.ts` coverage for `lib/menu-thinking.ts`, including reply markup, thinking-level callbacks, invalid/voice/non-reasoning guards, and menu send/update helpers. Impact: thinking-menu behavior now has direct module-level regression coverage.
27
+ - `[Tests]` Added symmetric `tests/setup.test.ts` coverage for `lib/setup.ts`, including token defaults, prompt mode selection, setup success/failure, and prompt-runtime guard cleanup after errors. Impact: `/telegram-setup` prompt behavior now has direct module-level regression coverage.
28
+ - `[Tests]` Added symmetric `tests/menu-settings.test.ts` coverage for `lib/menu-settings.ts`, including built-in settings markup, detail menu active states, settings mutations, and stale-message fallback toggles. Impact: settings-menu behavior now has direct module-level regression coverage.
29
+ - `[Context]` Cleared the completed backlog down to `No open work` and tightened the `lib/outbound.ts` domain header after button/markup extraction. Impact: historical completion notes stay in the changelog, while backlog and domain ownership now reflect current reality.
30
+ - `[Refactor]` Split native Telegram voice delivery from `lib/outbound.ts` into `lib/outbound-voice.ts`, with symmetric `tests/outbound-voice.test.ts` coverage. Impact: outbound voice upload/provider orchestration now has a direct domain owner and `lib/outbound.ts` is reduced to the outbound surface, command handlers, text transforms, artifact composition, and compatibility re-exports.
31
+ - `[Command Templates]` Replaced `pi-telegram`'s command-template implementation with the current `pi-actors` standard as a deliberate breaking 0.x minor change. Impact: templates now use `parallel` and `when`, support string `timeout`/`delay`/`retry`, inherited default references, `{value??fallback}`, `{flag?yes:no}`, and empty-arg filtering; old local `mode`, `critical`, and `pipe` shapes were removed/migrated to `parallel`, `failure`, and `template: [...]`.
32
+ - `[Docs]` Removed real companion-extension identities from abstract voice-provider examples and README guidance, keeping concrete names only in the explicit Companion Extensions list. Impact: pi-telegram documentation now keeps bridge responsibilities separate from third-party companion-extension ownership.
33
+ - `[Docs]` Integrated public API smoke examples for Extension Sections, raw updates, inbound handlers, outbound handlers, and voice providers into `docs/public-api.md` using only stable public membranes. Impact: companion-extension authors get copyable patterns in the canonical public API guide and avoid removed `@llblab/pi-telegram/lib/*` imports.
34
+ - `[Docs]` Updated README release positioning and corrected Extension Sections docs so `ctx.edit()` owns automatic Back-row insertion, `ctx.open()` is documented as a standalone chat-message sender, and section callbacks appear before built-in menu callbacks. Impact: companion extension authors get an accurate public contract.
35
+ - `[Backlog]` Restored concrete near-term follow-up slices for public API migration, provider lifecycle, smoke examples, diagnostics, voice-domain DAG cleanup, section examples, and voice transcript config clarity. Impact: README's open-work link points to actionable development priorities again.
6
36
 
7
37
  ## 0.12.0: Public API Membranes, Telegram UX Safety, And Extension Interop
8
38
 
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  **Telegram runtime adapter for π.**
6
6
 
7
- `pi-telegram` turns a private Telegram DM into a session-local operator console for π. It admits work, preserves context, streams readable replies, keeps busy sessions usable through queues, lets other extensions share one bot, and turns assistant-authored intent into native Telegram artifacts. In `0.11.0`, it also becomes a voice-provider platform: companion extensions can supply Telegram transcription and synthesis providers while `pi-telegram` keeps ownership of transport, queueing, and reply policy.
7
+ `pi-telegram` turns a private Telegram DM into a session-local operator console for π. It admits work, preserves context, streams readable replies, keeps busy sessions usable through queues, lets other extensions share one bot, and turns assistant-authored intent into native Telegram artifacts. It is also a voice-provider platform: companion extensions can supply Telegram transcription and synthesis providers while `pi-telegram` keeps ownership of transport, queueing, and reply policy.
8
8
 
9
9
  This repository is an actively maintained fork of [`badlogic/pi-telegram`](https://github.com/badlogic/pi-telegram). It started from upstream commit [`cb34008`](https://github.com/badlogic/pi-telegram/commit/cb34008460b6c1ca036d92322f69d87f626be0fc) and has since diverged substantially.
10
10
 
@@ -199,39 +199,9 @@ If `telegram.json` explicitly sets a valid `voice.replyMode`, prompts include co
199
199
 
200
200
  In `mirror` and `always` modes, the bridge transparently intercepts agent text responses and routes them through the outbound voice pipeline. Configured `outboundHandlers` with `type: "voice"` run first in their configured order; zero-config registered synthesis providers run after them as progressive fallbacks. If several synthesis providers are installed, they are tried in registration order and the first one that returns a valid `.ogg`/`.opus` artifact handles the reply; `undefined`, errors, or invalid output fall through to the next provider. If every voice generator fails, the bridge falls back to sending the text reply instead.
201
201
 
202
- Voice synthesis provider extensions (e.g. `pi-xai-voice`) register a TTS backend at runtime through public API domain subpaths:
203
-
204
- ```typescript
205
- import { registerTelegramVoiceSynthesisProvider } from "@llblab/pi-telegram/voice";
206
- import { recordTelegramRuntimeEvent } from "@llblab/pi-telegram/outbound";
207
-
208
- // Return path only (backward compatible)
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
- { id: "pi-xai-voice/tts" },
215
- );
216
-
217
- // Return path + transcript caption
218
- const dispose2 = registerTelegramVoiceSynthesisProvider(
219
- async (text, { lang, rate }) => {
220
- const rewritten = rewriteWithSpeechTags(text); // internal TTS optimization
221
- const path = await myTTS(rewritten, { language: lang });
222
- return { audioPath: path, transcriptText: text };
223
- },
224
- { id: "pi-xai-voice/tts-with-transcript" },
225
- );
226
-
227
- // Surface diagnostics in /telegram-status
228
- recordTelegramRuntimeEvent("xai-voice", new Error("TTS complete"), {
229
- phase: "tts",
230
- durationMs: 1200,
231
- });
232
- ```
202
+ Voice synthesis provider extensions register TTS backends at runtime through public API domain subpaths. Multiple synthesis providers can be registered; stable provider registrations pass a durable abstract id owned by the companion extension, such as `"@scope/voice-provider/tts"`. The bridge tries configured `type: "voice"` handlers first, then programmatic handlers, then registered synthesis providers in registration order until one succeeds. Providers and handlers receive the text to synthesize and optional `lang`/`rate` hints from `<!-- telegram_voice -->` markup or the automatic interception path. Voice delivery must produce `.ogg` or `.opus` files.
233
203
 
234
- Multiple synthesis providers can be registered; stable provider registrations pass a durable `id`, while omitted ids remain a compatibility path for older providers. The bridge tries configured `type: "voice"` handlers first, then programmatic handlers, then registered synthesis providers in registration order until one succeeds. Providers and handlers receive the text to synthesize and optional `lang`/`rate` hints from `<!-- telegram_voice -->` markup or the automatic interception path. Voice delivery must produce `.ogg` or `.opus` files.
204
+ Provider code examples, transcript-caption behavior, and diagnostics patterns live in [Voice Integration](./docs/voice.md) and [Public API](./docs/public-api.md). The important boundary is: providers own TTS and any private optimization, while pi-telegram owns reply policy, prompt context, fallback ordering, and Telegram transport.
235
205
 
236
206
  ### Extension interop
237
207
 
package/docs/README.md CHANGED
@@ -5,7 +5,7 @@ Living index of project documentation in `/docs`.
5
5
  ## Documents
6
6
 
7
7
  - [architecture.md](./architecture.md) — Overview of the Telegram bridge runtime, queueing model, rendering pipeline, and interactive controls
8
- - [public-api.md](./public-api.md) — Stable public API map: package entrypoints, commands, config, assistant markup, extension APIs, and compatibility boundaries
8
+ - [public-api.md](./public-api.md) — Stable public API map: package entrypoints, commands, config, assistant markup, extension APIs, smoke examples, and compatibility boundaries
9
9
  - [command-templates.md](./command-templates.md) — Portable command-template standard core
10
10
  - [inbound.md](./inbound.md) — Local `pi-telegram` inbound text/media handler bus, programmatic inbound handlers, registered STT provider fallbacks, legacy `attachmentHandlers` compatibility, placeholders, and fallbacks
11
11
  - [outbound.md](./outbound.md) — Local `pi-telegram` outbound-handler config, text/voice/button behavior, voice synthesis provider fallback priority, artifact outputs, and callback routing
@@ -46,7 +46,8 @@ The repository uses a **Flat Domain DAG**:
46
46
  - `sections`: Telegram menu-section registry, opaque section callback tokens, render/callback dispatch, safe section ports, and diagnostics.
47
47
  - `keyboard`: shared inline-keyboard reply-markup shape only; feature domains own labels, callback data, and behavior.
48
48
  - `preview` / `replies` / `rendering`: streaming preview lifecycle, final reply delivery, reply parameters, Telegram HTML rendering, chunking, and stable preview snapshots.
49
- - `outbound`: outbound text transformations, assistant-authored action comments, voice/button artifacts, and generated callback actions.
49
+ - `outbound-markup`: top-level assistant action comment parsing, attribute parsing, voice reply planning, and preview/delivery stripping.
50
+ - `outbound`: outbound text transformations, voice/button artifact delivery, and generated callback actions.
50
51
  - `outbound-attachments`: `telegram_attach`, queued outbound files, stat/limit checks, and photo/document delivery classification.
51
52
  - `status`: status bar/status-message rendering, queue-lane summaries, redacted event ring, and grouped diagnostics.
52
53
  - `lifecycle` / `prompts` / `prompt-templates` / `pi`: π hook registration, Telegram prompt guidance, prompt-template discovery/expansion, and centralized direct π SDK imports.
@@ -33,14 +33,14 @@ There is no portable `command` field. The command is derived from `template`: af
33
33
  Common object fields:
34
34
 
35
35
  - `label`: Optional human label for diagnostics and parallel branch reports.
36
- - `mode`: Optional execution mode for array templates. Default is `"sequence"`; `"parallel"` runs children concurrently.
36
+ - `parallel`: Optional boolean execution flag for array templates. Default is sequential execution; `true` runs children concurrently when the host execution layer supports branch fanout.
37
+ - `when`: Optional boolean or condition string. Falsy values skip the node. String forms may reference a flag name, `!flag`, or a placeholder expression such as `{flag?yes:}`.
37
38
  - `args`: Optional placeholder declarations. Untyped names remain valid; compact typed forms such as `file:path`, `timeout:int`, `speed:number`, `dry_run:bool`, `prompts:array`, and `mode:enum(check,fix)` are valid when the host supports typed tool schemas. Defaults belong in `defaults` or inline placeholder defaults; hosts may normalize interactive shorthand such as `timeout:int=60000` before persistence.
38
39
  - `defaults`: Placeholder default values by name.
39
- - `timeout`: Optional execution timeout in milliseconds. Omit it, or set `0`, to leave the command unbounded. Set an explicit positive timeout when a tool must fail closed instead of waiting indefinitely.
40
- - `delay`: Optional wait in milliseconds before starting this node. Default is no delay.
40
+ - `timeout`: Optional execution timeout in milliseconds, as a number or placeholder-resolved string. Omit it, or set `0`, to leave the command unbounded. Set an explicit positive timeout when a tool must fail closed instead of waiting indefinitely.
41
+ - `delay`: Optional wait in milliseconds before starting this node, as a number or placeholder-resolved string. Default is no delay.
41
42
  - `output`: Optional result selector. Default is `"stdout"`; runtime values such as `"ogg"` are valid.
42
- - `retry`: Optional max attempts including the first. Default is `1`.
43
- - `critical`: Optional boolean. Backward-compatible alias for `failure: "root"`.
43
+ - `retry`: Optional max attempts including the first, as a number or placeholder-resolved string. Default is `1`.
44
44
  - `failure`: Optional failure propagation scope: `continue`, `branch`, or `root`. Default is `continue`.
45
45
  - `recover`: Optional command template run between failed retry attempts. Recovery output is ignored; recovery failure stops retries.
46
46
  - `template`: Required command string or ordered composition array.
@@ -68,6 +68,8 @@ Supported forms:
68
68
  | `{name}` | Required value from runtime values or `defaults` |
69
69
  | `{name=default}` | Inline default when no value is provided |
70
70
  | `{items[index]}` | Array item selected by literal or repeat index |
71
+ | `{value??fallback}` | Fallback when the value is absent or falsy |
72
+ | `{flag?yes:no}` | Conditional text selected by flag truthiness |
71
73
 
72
74
  Resolution order is runtime values → `defaults` → inline default → error. Default values that are themselves a single placeholder, such as `{prompt}` resolving to `{prompts[index]}`, are resolved recursively with a small depth guard. A repeat node may set `repeat` to `{items.length}` when an array arg should determine fanout width.
73
75
 
@@ -127,8 +129,8 @@ template="echo 'literal words' {text}"
127
129
 
128
130
  Composition rules:
129
131
 
130
- - Execute leaves in order when `mode` is omitted or set to `"sequence"`
131
- - Execute child templates concurrently when `mode` is set to `"parallel"`
132
+ - Execute leaves in order by default
133
+ - Execute child templates concurrently when `parallel` is `true`
132
134
  - Parallel composition uses soft-quorum semantics by default: failed children are reported as degraded branches unless failure propagation escalates
133
135
  - Non-critical failures are recorded and execution continues, while `failure: "branch"` stops the current branch and `failure: "root"` aborts the root composition
134
136
  - Treat the whole composition as one handler for selector matching and fallback
@@ -163,7 +165,7 @@ Composition rules:
163
165
 
164
166
  ```json
165
167
  {
166
- "mode": "parallel",
168
+ "parallel": true,
167
169
  "repeat": 8,
168
170
  "template": "render page{_(index+1)}.html --prev page{_(prev+1)}.html --next page{_(next+1)}.html --zero page{_index}.html"
169
171
  }
@@ -198,7 +200,7 @@ Parallel nodes use the same object shape. Flags come first and `template` stays
198
200
  "template": [
199
201
  "prepare {out_dir}",
200
202
  {
201
- "mode": "parallel",
203
+ "parallel": true,
202
204
  "template": [
203
205
  {
204
206
  "label": "gpt-5.5",
@@ -232,7 +234,7 @@ exit: 1
232
234
  stderr: provider balance exhausted
233
235
  ```
234
236
 
235
- Legacy local schemas may accept `pipe` as an alias, but the portable standard is `template: [...]`.
237
+ Use `template: [...]` for ordered composition. Older local `pipe` aliases are not part of the 0.13.0 command-template standard.
236
238
 
237
239
  ## Fail-Open Default Policy
238
240
 
@@ -250,7 +252,7 @@ Use `failure` when a node should stop more aggressively:
250
252
 
251
253
  ```json
252
254
  {
253
- "mode": "parallel",
255
+ "parallel": true,
254
256
  "template": [
255
257
  {
256
258
  "label": "agent-a",
@@ -276,7 +278,7 @@ Use `failure` when a node should stop more aggressively:
276
278
 
277
279
  If `agent-a-validate` fails, `agent-a-push` is skipped, `agent-b` can still finish, and the parallel join reports degraded branch coverage.
278
280
 
279
- `critical: true` remains a backward-compatible alias for `failure: "root"`. Prefer `failure` for new templates because it names the propagation scope directly.
281
+ Use `failure: "root"` to abort the root composition. Older local `critical: true` shapes are not part of the 0.13.0 command-template standard.
280
282
 
281
283
  ## Retry
282
284
 
@@ -334,13 +336,13 @@ The standard uses a single `template` field that grows with the user's needs:
334
336
  string → leaf command
335
337
  string[] → sequential composition
336
338
  { template } → leaf command object
337
- { mode, template } → sequence or parallel subtree
338
- { mode, args, defaults, delay, retry, failure, recover, output, template } → full node
339
+ { parallel, template } → parallel subtree
340
+ { parallel, when, args, defaults, delay, retry, failure, recover, output, template } → full node
339
341
  ```
340
342
 
341
- Start with a string. Add composition when needed. Add `mode: "parallel"` when independent work can run concurrently. Add delay when launch pacing matters. Add retry when flaky. Add `failure` when propagation scope matters. Add `recover` when a retried node needs cleanup before another attempt. Same contract, growing capability, no dead weight.
343
+ Start with a string. Add composition when needed. Add `parallel: true` when independent work can run concurrently. Add `when` for conditional nodes. Add delay when launch pacing matters. Add retry when flaky. Add `failure` when propagation scope matters. Add `recover` when a retried node needs cleanup before another attempt. Same contract, growing capability, no dead weight.
342
344
 
343
- `mode: "parallel"` is the synchronous fanout shape. Saved JSON recipes and detached lifecycle concerns such as logs, cancellation, and durable state belong to host-specific recipe/async-run standards, not to command templates.
345
+ `parallel: true` is the synchronous fanout shape. Saved JSON recipes and detached lifecycle concerns such as logs, cancellation, and durable state belong to host-specific recipe/async-run standards, not to command templates.
344
346
 
345
347
  ## Trust Boundary
346
348
 
package/docs/inbound.md CHANGED
@@ -49,7 +49,7 @@ Legacy `telegram.json` files may still define `attachmentHandlers` for media/fil
49
49
 
50
50
  At runtime, `attachmentHandlers` is appended after `inboundHandlers`. Existing configs continue to work, while new configs should use `inboundHandlers`.
51
51
 
52
- Handlers match by optional `type`, `mime`, or `match`. `mime` and `type` are independent selectors: if `mime` is present, `type` is not required. Wildcards such as `audio/*` or `text/*` are accepted. Each matching handler must provide `template`; a string is one command, and an array is ordered composition. Top-level `args` and `defaults` apply to composed steps unless a step defines private values. The command-template default timeout applies automatically. Legacy configs may still use `pipe` as a local alias.
52
+ Handlers match by optional `type`, `mime`, or `match`. `mime` and `type` are independent selectors: if `mime` is present, `type` is not required. Wildcards such as `audio/*` or `text/*` are accepted. Each matching handler must provide `template`; a string is one command, and an array is ordered composition. Top-level `args` and `defaults` apply to composed steps unless a step defines private values. The command-template default timeout applies automatically. Use `template: [...]` for composition; the old local `pipe` alias is removed in 0.13.0.
53
53
 
54
54
  `defaults` may provide additional placeholder values such as `{lang}` or `{model}`. `args` is only a string-array declaration of supported placeholders; defaults belong in `defaults` or inline placeholders such as `{lang=ru}`. Examples prefer explicit flag-style CLIs such as `--file {file}` and `--lang {lang=ru}` for readability, but positional forms such as `/path/to/stt {file} {lang=ru} {model=voxtral-mini-latest}` are equally valid when the target script supports them.
55
55
 
@@ -90,7 +90,7 @@ If a matching handler fails with a non-zero exit code, the runtime records diagn
90
90
 
91
91
  Extensions can register programmatic inbound handlers with `registerTelegramInboundHandler(kind, handler)` from `@llblab/pi-telegram/inbound`. This is the code-level counterpart to configured `inboundHandlers`; use it for extension-owned transformations that are not voice-specific.
92
92
 
93
- Voice extensions can register STT providers with `registerTelegramVoiceTranscriptionProvider()` from `@llblab/pi-telegram/voice`. This is the zero-config extension path for voice/audio input: an extension such as `pi-xai-voice` can transcribe Telegram voice notes without requiring the operator to write an `inboundHandlers` command template.
93
+ Voice extensions can register STT providers with `registerTelegramVoiceTranscriptionProvider()` from `@llblab/pi-telegram/voice`. This is the zero-config extension path for voice/audio input: a companion extension can transcribe Telegram voice notes without requiring the operator to write an `inboundHandlers` command template.
94
94
 
95
95
  Priority stays explicit and predictable:
96
96
 
package/docs/outbound.md CHANGED
@@ -18,7 +18,7 @@ An outbound handler is selected by `type`. Text replies and assistant markup map
18
18
 
19
19
  The voice pipeline is detailed below: configured `type: "voice"` handlers first, then programmatic handlers, then registered synthesis providers.
20
20
 
21
- Configured text handlers provide `template`. A string is one command; an array is ordered composition. Top-level `args` and `defaults` apply to all composed steps unless a step defines private values. The command-template default timeout applies automatically. Legacy configs may still use `pipe`, but `template: [...]` is the preferred standard shape.
21
+ Configured text handlers provide `template`. A string is one command; an array is ordered composition. Top-level `args` and `defaults` apply to all composed steps unless a step defines private values. The command-template default timeout applies automatically. Use `template: [...]` for composition; the old local `pipe` alias is removed in 0.13.0.
22
22
 
23
23
  ## Text Handler Config
24
24
 
@@ -25,7 +25,7 @@ import {
25
25
  } from "@llblab/pi-telegram/voice";
26
26
  ```
27
27
 
28
- `0.12.0` intentionally removes the published `@llblab/pi-telegram/lib/*.ts` compatibility wildcard. Integrations should use the public API domain subpaths above. Package exports point at `/api/*.ts` membranes that re-export only stable companion-extension symbols; implementation modules under `lib/` remain package-private.
28
+ `0.12.0` intentionally removes the published `@llblab/pi-telegram/lib/*.ts` compatibility wildcard. Integrations should use the public API domain subpaths above. Package exports point at `/api/*.ts` membranes that re-export only stable companion-extension symbols; implementation modules under `lib/` remain package-private. See [Public API Smoke Examples](#public-api-smoke-examples) below for minimal companion-extension patterns that avoid implementation imports.
29
29
 
30
30
  ## User-Facing API
31
31
 
@@ -159,7 +159,7 @@ Contract:
159
159
 
160
160
  - `id` is unique per active registry. Duplicate ids are rejected.
161
161
  - `ctx.callbackData(action, payload?)` builds compact `section:` callbacks and validates Telegram's 64-byte limit.
162
- - `ctx.edit()` and `ctx.open()` auto-prepend the correct Back/Main-menu row.
162
+ - `ctx.edit()` auto-prepends the correct Back/Main-menu row. `ctx.open()` sends a standalone chat message without auto-navigation.
163
163
  - Section errors are isolated and surfaced as callback popups/diagnostics.
164
164
 
165
165
  Full behavior: [Extension Sections](./sections.md).
@@ -233,16 +233,170 @@ const offStt = registerTelegramVoiceTranscriptionProvider(
233
233
  const offTts = registerTelegramVoiceSynthesisProvider(
234
234
  async (text, options) => {
235
235
  const audioPath = await synthesizeOggOpus(text, options);
236
- return { audioPath, transcriptText: text };
236
+ return getTelegramVoiceSendTranscript(getCurrentTelegramConfigView())
237
+ ? { audioPath, transcriptText: text }
238
+ : { audioPath };
237
239
  },
238
240
  { id: "@scope/my-extension/tts" },
239
241
  );
240
242
  ```
241
243
 
242
- 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.
244
+ 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.
243
245
 
244
246
  Full behavior: [Voice Integration](./voice.md).
245
247
 
248
+ ## Public API Smoke Examples
249
+
250
+ Minimal companion-extension examples that import only stable `@llblab/pi-telegram/*` public membranes. Copy one into an extension `index.ts`, load it beside `pi-telegram`, and verify that it starts without importing any `@llblab/pi-telegram/lib/*` implementation path.
251
+
252
+ ### Extension Sections
253
+
254
+ ```ts
255
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
256
+ import { registerTelegramSection } from "@llblab/pi-telegram/sections";
257
+
258
+ export default function demoSection(pi: ExtensionAPI) {
259
+ let unregister: (() => void) | undefined;
260
+ pi.on("session_start", async () => {
261
+ unregister?.();
262
+ unregister = registerTelegramSection({
263
+ id: "demo-section/status",
264
+ label: "🧩 Demo section",
265
+ order: 50,
266
+ render: () => ({
267
+ text: "<b>Demo section</b>\n\nThis section was rendered by a companion extension.",
268
+ replyMarkup: { inline_keyboard: [] },
269
+ }),
270
+ });
271
+ });
272
+ pi.on("session_shutdown", async () => {
273
+ unregister?.();
274
+ unregister = undefined;
275
+ });
276
+ }
277
+ ```
278
+
279
+ ### Raw Update Handler
280
+
281
+ ```ts
282
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
283
+ import { registerTelegramUpdateHandler } from "@llblab/pi-telegram/updates";
284
+
285
+ export default function demoUpdates(pi: ExtensionAPI) {
286
+ let unregister: (() => void) | undefined;
287
+ pi.on("session_start", async () => {
288
+ unregister?.();
289
+ unregister = registerTelegramUpdateHandler((update) => {
290
+ if (!update || typeof update !== "object") return "pass";
291
+ return "pass";
292
+ });
293
+ });
294
+ pi.on("session_shutdown", async () => {
295
+ unregister?.();
296
+ unregister = undefined;
297
+ });
298
+ }
299
+ ```
300
+
301
+ ### Inbound Handler
302
+
303
+ ```ts
304
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
305
+ import { registerTelegramInboundHandler } from "@llblab/pi-telegram/inbound";
306
+
307
+ export default function demoInbound(pi: ExtensionAPI) {
308
+ let unregister: (() => void) | undefined;
309
+ pi.on("session_start", async () => {
310
+ unregister?.();
311
+ unregister = registerTelegramInboundHandler("text/*", async (file) => {
312
+ if (!file.path.endsWith(".demo.txt")) return undefined;
313
+ return `Demo inbound handler saw ${file.fileName ?? file.path}`;
314
+ });
315
+ });
316
+ pi.on("session_shutdown", async () => {
317
+ unregister?.();
318
+ unregister = undefined;
319
+ });
320
+ }
321
+ ```
322
+
323
+ ### Outbound Handler
324
+
325
+ ```ts
326
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
327
+ import { registerTelegramOutboundHandler } from "@llblab/pi-telegram/outbound";
328
+
329
+ export default function demoOutbound(pi: ExtensionAPI) {
330
+ let unregister: (() => void) | undefined;
331
+ pi.on("session_start", async () => {
332
+ unregister?.();
333
+ unregister = registerTelegramOutboundHandler("text", async (text) => {
334
+ if (!text.includes("[demo-outbound]")) return undefined;
335
+ return text.replace("[demo-outbound]", "Demo outbound handler:");
336
+ });
337
+ });
338
+ pi.on("session_shutdown", async () => {
339
+ unregister?.();
340
+ unregister = undefined;
341
+ });
342
+ }
343
+ ```
344
+
345
+ ### Voice Providers
346
+
347
+ ```ts
348
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
349
+ import {
350
+ getTelegramVoiceSendTranscript,
351
+ registerTelegramVoiceSynthesisProvider,
352
+ registerTelegramVoiceTranscriptionProvider,
353
+ } from "@llblab/pi-telegram/voice";
354
+
355
+ export default function demoVoice(pi: ExtensionAPI) {
356
+ let unregisterTts: (() => void) | undefined;
357
+ let unregisterStt: (() => void) | undefined;
358
+ let currentConfig: { voice?: { sendTranscript?: boolean } } = {};
359
+ pi.on("session_start", async () => {
360
+ unregisterTts?.();
361
+ unregisterStt?.();
362
+ unregisterTts = registerTelegramVoiceSynthesisProvider(
363
+ async (text) => {
364
+ const audioPath = await synthesizeDemoOgg(text);
365
+ return getTelegramVoiceSendTranscript(currentConfig)
366
+ ? { audioPath, transcriptText: text }
367
+ : { audioPath };
368
+ },
369
+ { id: "demo-voice/tts" },
370
+ );
371
+ unregisterStt = registerTelegramVoiceTranscriptionProvider(
372
+ async (file) => {
373
+ if (file.kind !== "voice" && file.kind !== "audio") return undefined;
374
+ return { text: `Demo transcript for ${file.fileName ?? file.path}` };
375
+ },
376
+ { id: "demo-voice/stt" },
377
+ );
378
+ });
379
+ pi.on("session_shutdown", async () => {
380
+ unregisterTts?.();
381
+ unregisterStt?.();
382
+ unregisterTts = undefined;
383
+ unregisterStt = undefined;
384
+ });
385
+ }
386
+
387
+ async function synthesizeDemoOgg(_text: string): Promise<string> {
388
+ throw new Error("Replace synthesizeDemoOgg with a real OGG/Opus generator.");
389
+ }
390
+ ```
391
+
392
+ ### Smoke Checklist
393
+
394
+ - The extension imports only `@llblab/pi-telegram/sections`, `/updates`, `/inbound`, `/outbound`, `/voice`, or `/keyboard`.
395
+ - It does not import `@llblab/pi-telegram/lib/*`.
396
+ - It registers on `session_start` and disposes on `session_shutdown`.
397
+ - Stable high-level registrations use durable ids.
398
+ - Failures are visible during manual testing through `/telegram-status` or extension-owned logging.
399
+
246
400
  ## Callback Namespaces
247
401
 
248
402
  Owned prefixes are reserved by `pi-telegram`: `compact:`, `tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`, `settings:`, and `section:`.
package/docs/sections.md CHANGED
@@ -197,8 +197,8 @@ The token is an implementation detail. Section authors **never** write `section:
197
197
  4. Compact confirmation callbacks (`compact:*`)
198
198
  5. Queue menu callbacks (`queue:*`)
199
199
  6. Settings menu callbacks (`settings:*`)
200
- 7. Built-in menu callbacks (`menu:*`, `model:*`, `thinking:*`, `status:*`)
201
- 8. Section callbacks (`section:*`) dispatched before step 7's full handler
200
+ 7. Section callbacks (`section:*`)
201
+ 8. Built-in menu callbacks (`menu:*`, `model:*`, `thinking:*`, `status:*`)
202
202
  9. Unknown callbacks fall back to `[callback]` prompt text
203
203
 
204
204
  ### Handler return values
@@ -226,14 +226,14 @@ Section errors are caught and surfaced as popup text. No unhandled exceptions le
226
226
 
227
227
  ## 8. Navigation Hierarchy
228
228
 
229
- `ctx.edit()` and `ctx.open()` automatically prepend a Back row. The Back target depends on the navigation level:
229
+ `ctx.edit()` automatically prepends a Back row for menu-bound views. The Back target depends on the navigation level:
230
230
 
231
231
  - Section root (from main menu): `⬆️ Main menu` → `menu:back`
232
232
  - Section sub-view (`ctx.edit()` in handler): `⬆️ Back` → `section:<token>:open`
233
233
  - Settings root (from Settings list): `⬆️ Back` → `settings:list`
234
234
  - Settings sub-view (`ctx.edit()` in settings handler): `⬆️ Back` → `settings:list`
235
235
 
236
- Section authors do not need to manage the Back button — it is added automatically and deduplicated when already present.
236
+ Section authors do not need to manage the Back button for `ctx.edit()` — it is added automatically and deduplicated when already present. `ctx.open()` sends a standalone chat message and does not prepend a Back row.
237
237
 
238
238
  ```
239
239
  Main menu
@@ -257,7 +257,7 @@ interface TelegramSectionContext {
257
257
  answerCallback(text?: string): Promise<void>;
258
258
  /** Edit the current message (auto-prepends Back row) */
259
259
  edit(view: TelegramSectionView): Promise<void>;
260
- /** Send a new message (auto-prepends Back row) */
260
+ /** Send a standalone chat message without auto-navigation */
261
261
  open(view: TelegramSectionView): Promise<void>;
262
262
  /** Enqueue a plain-text prompt turn */
263
263
  enqueuePrompt(prompt: string): Promise<void>;