@llblab/pi-telegram 0.13.2 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -57,10 +57,12 @@
57
57
  ## 5.2 Session And Queue Semantics
58
58
 
59
59
  - The bridge is session-local, paired to one allowed Telegram user, and owns a local queue aligned with π lifecycle hooks
60
+ - The Telegram lock owns only external control/polling. Local extension state and queue runtime are per Pi instance: losing `/telegram-connect` ownership stops live Telegram control here, but must not clear, silence, or stop active/queued preview, final delivery, or dispatch in this instance
60
61
  - Queue admission is explicit and validated: immediate commands, control lane, priority lane, and default lane must preserve allowed kind/lane pairings
61
62
  - 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
63
  - 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
64
  - `/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
65
+ - Abort-history mode is scoped to Telegram-owned active turns. Internally this is `foldQueuedPromptsIntoHistory`: local/non-Telegram agent starts after abort clear it so the next Telegram prompt appends to the local queue instead of absorbing older queued turns as history
64
66
  - `/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
67
  - 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
68
  - Telegram `reply_to_message` context is prompt-only and must not affect slash-command parsing
@@ -71,7 +73,7 @@
71
73
 
72
74
  - Telegram replies render through Telegram HTML, not raw Markdown
73
75
  - Real code blocks must stay literal and escaped
74
- - `telegram_attach` is the canonical outbound file-delivery path for Telegram-originated requests
76
+ - `telegram_attach` is the canonical outbound file-delivery path for Telegram-originated requests; outside active Telegram turns it may send immediately to the paired/default chat for explicit local/TUI delivery requests only when this π instance owns `/telegram-connect`. `telegram_message` is the first-class direct Telegram Markdown text tool for local/TUI prompts and is also gated by `/telegram-connect`; neither direct tool replaces normal active-turn replies. It reuses top-level `telegram_button` comments for inline buttons; buttons must be attached to a text message, never sent as standalone actions
75
77
  - 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
76
78
  - `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
77
79
  - 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
@@ -79,7 +81,7 @@
79
81
  - 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
80
82
  - 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
81
83
  - `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
82
- - `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
84
+ - `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. Do not author JSON button specs, inline-after-text comments, standalone button tools, or comments inside code/quotes/lists/indented examples; write normal Markdown plus top-level hidden comments, and add visible parent text when buttons would otherwise be the only output
83
85
 
84
86
  ## 6. Engineering Conventions
85
87
 
@@ -145,7 +147,7 @@ The canonical detailed ownership map lives in [`docs/architecture.md`](./docs/ar
145
147
  - Shared inline-keyboard structure belongs to `keyboard`; application-control button labels, callback data, and callback behavior stay in `menu`/`menu-model`/`menu-thinking`/`menu-status`/`menu-queue` while core queue mechanics stay in `queue`
146
148
  - Telegram `/settings` options should open nested detail submenus by default: checkbox options show a description plus Back, `on`, and `off`; list options show Back plus selectable values. One-shot actions such as syncing may run directly without a submenu when there is no meaningful choice or description step.
147
149
  - Inline UI labels and dialogs follow [`docs/ui-style.md`](./docs/ui-style.md): action buttons use emoji plus capitalized action text, state & navigation buttons show state and lead to a submenu, first-level submenus start with `⬆️ Main menu` while deeper submenus start with `⬆️ Back`, boolean toggles use Capitalized horizontal `On`/`Off` with green active `On`, yellow active `Off`, and black inactive dots, tabs use capitalized labels with purple default-state and yellow elevated-state active dots and black inactive dots, vertical option lists mark only the current value green, and confirmation dialogs use a single bold text-only question with emoji on buttons only.
148
- - Inbound text/media may be transformed through configured `inboundHandlers` before queueing; legacy `attachmentHandlers` are deprecated compatibility aliases appended after `inboundHandlers`; outbound files must flow through `telegram_attach`
150
+ - Inbound text/media may be transformed through configured `inboundHandlers` before queueing; legacy `attachmentHandlers` are deprecated compatibility aliases appended after `inboundHandlers`; active-turn outbound files must flow through `telegram_attach`, while explicit local/TUI Telegram sends use `telegram_attach` for files or `telegram_message` for text/buttons
149
151
  - Long Telegram text split recovery belongs to `text-groups`: keep it conservative, short-debounced, same chat/user/message-id contiguous, and gated by near-limit human text so normal rapid follow-ups and slash commands stay separate
150
152
  - Public API boundaries live in [`docs/public-api.md`](./docs/public-api.md): companion extensions must use public API domain subpaths such as `@llblab/pi-telegram/sections`, `/voice`, `/inbound`, `/outbound`, and `/updates`; `0.12.0` intentionally removes the published `@llblab/pi-telegram/lib/*.ts` compatibility wildcard
151
153
  - Public handler API matrix: low-level buses use `registerTelegramUpdateHandler(handler)`, `registerTelegramInboundHandler(kind, handler)`, and `registerTelegramOutboundHandler(kind, handler)` without ids; high-level surfaces use stable identity (`registerTelegramSection({ id, ... })`, `registerTelegramVoiceTranscriptionProvider(provider, { id })`, and `registerTelegramVoiceSynthesisProvider(provider, { id })`). Inbound handlers and command-backed outbound handlers use command templates as the standard config contract; built-in outbound buttons use inline keyboards plus callback routing because no polling command execution is needed
package/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ No open changes.
6
+
7
+ ## 0.14.0: Direct Telegram Delivery, Queue Semantics, And Section Diagnostics
8
+
9
+ - `[Prompt Guidance]` Tightened agent context for Telegram buttons: use normal Markdown plus top-level hidden `telegram_button` comments, never JSON button specs or standalone button actions, and keep comments out of code/quotes/lists/indented examples. Impact: agents immediately know how to author visible Telegram text, inline buttons, and direct `telegram_message` payloads without transport hacks.
10
+ - `[Command Templates]` Synced `lib/command-templates.ts` with the current `pi-actors` standard, including advisory risk labels, actor recipe context metadata, bundled short-flag detection, and fuller trusted-executable mitigation text. Impact: pi-telegram command-template tooling no longer lags the actor recipe/tooling implementation.
11
+ - `[Tools]` Telegram is now a first-class local delivery target: `telegram_attach` sends files immediately to the paired/default chat when no Telegram turn is active, and new `telegram_message` supports explicit local/TUI requests to push Markdown text messages. `telegram_message` reuses the normal `telegram_button` comment planner, so direct buttons are authored exactly like ordinary Telegram replies and always attach to a real message. Direct local/TUI delivery is gated by `/telegram-connect` ownership, while active-turn reply delivery remains session-local. Impact: agents can deliver requested artifacts or notices to Telegram from terminal-originated work without bypassing singleton polling/control ownership.
12
+ - `[Status]` TUI status now renders `compacting` with the same warning color used for `active`, while keeping the `telegram` domain label accented. Native typing cleanup now gives the last in-flight `sendChatAction` a short bounded drain before final reply delivery, and the typing keepalive interval is relaxed to 3s while staying below Telegram's typical chat-action TTL. Impact: manual or automatic context compaction reads as active model work, and Telegram typing is less likely to outlive a completed agent turn.
13
+ - `[Queue]` Telegram queue and reply delivery now stay per Pi instance, independent from the singleton polling/control lock. `/abort` only enables abort-history preservation for Telegram-owned turns, and local/non-Telegram agent starts clear stale abort-history mode. Impact: moving `/telegram-connect` no longer silences an already accepted queue, and local prompts after abort no longer fold old queued turns into the next Telegram prompt.
14
+ - `[Sections]` Section label, render, and callback failures now record source-scoped diagnostics and recover only when the matching surface succeeds. Section diagnostics expose only `active`/`error`, and settings-only callbacks keep Settings-level Back navigation. Impact: one broken companion section cannot break menu rendering or hide unrelated diagnostics.
15
+
5
16
  ## 0.13.2: Config Recovery And Inbound Output Bounds Hotfix
6
17
 
7
18
  - `[Config]` Invalid `telegram.json` now recovers on session startup by renaming the broken file to an `.invalid-*` recovery path, loading safe empty defaults, and recording a runtime diagnostic. Impact: a hand-edited or partially written config no longer bricks `/telegram-setup` or session startup.
@@ -12,7 +23,7 @@
12
23
 
13
24
  - `[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.
14
25
  - `[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.
15
- - `[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.
26
+ - `[Continue Queue]` `/continue` now enqueues as a control-lane resume prompt and explicitly clears abort-history mode, so queued Telegram prompts stay separate and the continuation runs ahead of queued prompt work after abort or compaction recovery.
16
27
 
17
28
  ## 0.13.0: Command Template Standard, Voice Hardening, And Domain Cleanup
18
29
 
@@ -46,7 +57,7 @@
46
57
  - `[Compaction Safety]` Telegram `/compact` now opens an inline confirmation dialog before manual compaction starts, protecting the operator from accidental taps near `/start`. The dialog follows the dedicated UI style guide: a bold text-only question with emoji only on the explicit `Yes, compact` and `No` buttons. Confirming edits the dialog directly to `Compaction started.` instead of showing a separate `Compaction confirmed.` step.
47
58
  - `[Docs]` Added `docs/ui-style.md` as the focused style guide for inline buttons, toggles, tabs, option lists, cards, and dialogs.
48
59
  - `[Docs]` Restructured `docs/architecture.md` into a clearer architectural map with runtime topology, domain ownership, core flows, extension surfaces, and operational behavior while pushing detailed UI/callback rules toward focused standards.
49
- - `[Breaking API]` Bumped package version to `0.12.0` and renamed the public implementation domains to `lib/sections.ts`, `lib/updates.ts`, `lib/inbound.ts`, and `lib/outbound.ts`. Package exports now expose only the stable public API domains (`/sections`, `/updates`, `/inbound`, `/outbound`, `/voice`, `/keyboard`) and no longer expose the compatibility `./lib/*.ts` wildcard.
60
+ - `[Breaking API]` Renamed the public implementation domains to `lib/sections.ts`, `lib/updates.ts`, `lib/inbound.ts`, and `lib/outbound.ts`. Package exports now expose only the stable public API domains (`/sections`, `/updates`, `/inbound`, `/outbound`, `/voice`, `/keyboard`) and no longer expose the compatibility `./lib/*.ts` wildcard.
50
61
  - `[Architecture]` Folded the public update-handler interop surface into `updates` and renamed the internal long-poll loop module back to `polling`, giving the pair concise one-word domains: `updates` for update contracts/classification/handler registry and `polling` for the `getUpdates` runtime. Entrypoint extraction now lives in `bindings`, a concrete pi-facing command/tool/lifecycle wiring boundary rather than a new product domain.
51
62
  - `[Tests]` Added an architecture invariant and package self-import regressions that pin the `0.12.0` package exports to stable `/api` membranes with exact runtime export shapes and prevent accidental restoration of the removed `./lib/*.ts` wildcard.
52
63
  - `[Docs]` Added `docs/public-api.md` as the public API map for commands, config, assistant markup, extension APIs, callback ownership, and public/internal stability boundaries.
@@ -186,7 +197,7 @@
186
197
 
187
198
  ## 0.9.6: Runtime Adapter Positioning
188
199
 
189
- - `[Package]` Bumped package metadata to `0.9.6` and repositioned the package description from "Better Telegram DM bridge extension for π" to "Telegram runtime adapter for π". Impact: package metadata now reflects the runtime adapter/operator-console role rather than a narrow pipe metaphor.
200
+ - `[Package]` Repositioned the package description from "Better Telegram DM bridge extension for π" to "Telegram runtime adapter for π". Impact: package metadata now reflects the runtime adapter/operator-console role rather than a narrow pipe metaphor.
190
201
  - `[Telegram API]` Introduced `TELEGRAM_API_BASE` for the Bot API endpoint and documented native HTTP/HTTPS proxy operation through `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`, and explicit `NODE_USE_ENV_PROXY=1` / `--use-env-proxy` enablement. Impact: users behind corporate proxies, local HTTP tunnels, or restricted networks get a zero-runtime-dependency proxy path without replacing native `fetch`; SOCKS5 remains outside the zero-dependency core.
191
202
  - `[Dependencies]` Refreshed the lockfile transitive dependency set so `npm audit` clears current `fast-uri` and `fast-xml-builder` advisories inherited through development peer installs. Impact: the full `npm run validate` pipeline passes without changing runtime dependencies.
192
203
  - `[README]` Restructured the user entrypoint around install → connect → use → core features → docs, then consolidated examples, terminology, proxy setup, `PI_CODING_AGENT_DIR`, and other environment-only configuration around the runtime-adapter/operator-console model. Impact: first-time users get a clearer path from installation to operation, while vivid examples and non-UI runtime knobs stay discoverable.
@@ -207,13 +218,11 @@
207
218
  - `[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.
208
219
  - `[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.
209
220
  - `[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.
210
- - `[Package]` Bumped package metadata to `0.9.4` and kept the lockfile in sync.
211
221
 
212
222
  ## 0.9.3: External Handlers Rename
213
223
 
214
224
  - `[External Handlers]` Renamed the external update handlers domain to `external-handlers` across source, tests, and docs. Impact: the interop domain now has a cleaner name aligned with inbound/outbound handler naming.
215
225
  - `[Breaking]` Removed the old `external-update-handlers` module/doc path and old exported update/interceptor aliases. Impact: layered extensions should import from `@llblab/pi-telegram/lib/external-handlers.ts` and use the `TelegramExternalHandler*` names.
216
- - `[Package]` Bumped package metadata to `0.9.3` and kept the lockfile in sync.
217
226
 
218
227
  ## 0.9.2: External Update Interceptors
219
228
 
@@ -221,7 +230,6 @@
221
230
  - `[External Update Interceptors]` Validated the full v1 registry shape (`version`, `add`, and `dispatch`) before reusing a pre-existing global registry and documented the zero-coupling bootstrap contract. Impact: install-order interop stays safe even when another extension initializes the registry first.
222
231
  - `[Queue Menu]` Non-empty queue lists now keep the `🌀 Refresh` row below queued items, matching the empty-queue surface. Impact: users can manually refresh the queue screen while waiting for changes without navigating away.
223
232
  - `[Security]` Refreshed the lockfile to resolve the transitive `basic-ftp` audit advisory. Impact: release validation returns to a clean npm audit state.
224
- - `[Package]` Bumped package metadata to `0.9.2` and kept the lockfile in sync.
225
233
 
226
234
  ## 0.9.1: Model Detail Hotfix
227
235
 
@@ -230,7 +238,6 @@
230
238
  - `[Proactive Push]` Removed the unused proactive reply-target store and always sends proactive local-result pushes without `reply_to_message_id`. Impact: the runtime no longer carries dead state for a target-capture behavior that does not exist yet.
231
239
  - `[Queue Reactions]` Added `🔥` as a priority reaction and `🗑` as a queue-removal reaction. Impact: the intuitive fire/removal gestures now work alongside the existing reaction controls.
232
240
  - `[Docs]` Updated the status-bar example to match the compact active/queued display.
233
- - `[Package]` Bumped package metadata to `0.9.1` and kept the lockfile in sync.
234
241
 
235
242
  ## 0.9.0: Hidden Settings And Proactive Push
236
243
 
@@ -246,7 +253,6 @@
246
253
  - `[Lock Safety]` Active Telegram turns now re-check singleton ownership before preview flushes and final agent-end delivery. Impact: an old π instance stays silent after another instance takes the Telegram bridge lock, even if the old instance finishes a long-running prompt later.
247
254
  - `[Inbound Handlers]` The first step of an inbound composition now receives the full configured handler timeout before elapsed-time accounting starts on later steps. Impact: composition timeout behavior is deterministic and avoids one-millisecond test/runtime drift at pipeline start.
248
255
  - `[Menu UI]` Model and Thinking submenu headers now include their matching command icons (`🤖` and `🧠`). Impact: submenu headings match the Queue menu's icon-led style.
249
- - `[Package]` Bumped package metadata to `0.8.2` and kept the lockfile in sync.
250
256
 
251
257
  ## 0.8.1: Outbound Voice Translation Hotfix
252
258
 
@@ -256,7 +262,6 @@
256
262
  - `[Queue Priority]` Priority reactions now preserve the exact normalized promotion emoji and render it in both queue-menu rows and the π status-bar queued preview. Reaction metadata is grouped into semantic id ranges (`10..13` for priority, `20..23` for removal). Impact: `👍`, `⚡`, `❤️`, and `🕊️` keep the same priority semantics while making the user's chosen reaction visible across Telegram and TUI surfaces.
257
263
  - `[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.
258
264
  - `[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.
259
- - `[Package]` Bumped package metadata to `0.8.1` and kept the lockfile in sync.
260
265
  - `[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.
261
266
  - `[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.
262
267
 
@@ -270,20 +275,17 @@
270
275
  - `[Attachment Handlers]` `attachmentHandlers` is now deprecated but remains supported as a compatibility alias appended after `inboundHandlers`. Impact: existing voice/file preprocessing configs keep working while new configs can move to the unified inbound bus.
271
276
  - `[Outbound Handlers]` Added `outboundHandlers` support for `type: "text"`; final text/Markdown replies can be transformed before Telegram rendering and delivery. Impact: translation-back or other outbound text normalization can be configured without hard-coded providers.
272
277
  - `[Outbound Text Preview]` Finalized rich preview messages now pass through outbound `type: "text"` handlers before Telegram edit/delivery, with expanded README/docs examples for machine translation, final text rewrites, composed translated voice-over, and inline-button compatibility. Impact: outbound text transforms apply even when the final answer reuses an existing preview instead of falling back to a separate send path, while inline buttons remain attached and visible labels are transformed without changing callback prompts.
273
- - `[Package]` Bumped package metadata to `0.8.0` through npm and kept the lockfile in sync.
274
278
 
275
279
  ## 0.7.2: Split Text Coalescing Hotfix
276
280
 
277
281
  - `[Text Coalescing]` Telegram text messages that look like automatic splits of one near-limit human message are now short-debounced and forwarded to π as one prompt, using a conservative 3600-character near-limit threshold. Commands, bot messages, media groups, captions, non-contiguous messages, and normal short follow-ups bypass coalescing. Impact: long pasted logs/prompts are less likely to arrive as separate π turns when Telegram chunks them.
278
282
  - `[Runtime Tests]` The media-group runtime regression now waits for the real debounce instead of mixing fake timers with the polling loop, and the reaction-priority runtime test flushes pending microtasks before ending the active turn. Impact: CI should stop failing on timing-only races around delayed dispatch and queued reaction mutations.
279
283
  - `[Callback Namespaces]` Current status-screen navigation callbacks now use the canonical `menu:` namespace (`menu:model`, `menu:thinking`, `menu:queue`). `status:` remains reserved as an owned legacy prefix but is no longer emitted by current UI. Impact: new inline menu callbacks align with the unified app-menu model while old `status:` payloads still cannot leak to external fallback handlers.
280
- - `[Package]` Bumped package metadata to `0.7.2` through npm and kept the lockfile in sync.
281
284
 
282
285
  ## 0.7.1: Layered Callback Interop
283
286
 
284
287
  - `[Callback Interop]` Unknown Telegram inline-button callback data that does not belong to pi-telegram-owned prefixes (`tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`) is now forwarded to π as `[callback] <data>` after assistant-button, queue-menu, and app-menu handlers decline it. `docs/callback-namespaces.md` defines the shared callback namespace standard for layered extensions. Impact: layered π extensions can namespace and handle their own Telegram inline buttons without polling the same bot or forking pi-telegram.
285
288
  - `[Prompt Templates]` Prompt-template aliases stay visible only inside `/start` and are no longer registered in the Telegram bot command menu. Impact: reusable π workflows remain discoverable without making Telegram's global command menu noisy.
286
- - `[Package]` Bumped package metadata to `0.7.1` through npm and kept the lockfile in sync.
287
289
 
288
290
  ## 0.7.0: Unified App Menu & Command Template Hardening
289
291
 
package/README.md CHANGED
@@ -47,7 +47,7 @@ Paste your bot token when prompted. If a bot token is already saved in `~/.pi/ag
47
47
  /telegram-connect
48
48
  ```
49
49
 
50
- The adapter is session-local: only one π instance polls Telegram at a time. `/telegram-connect` records polling ownership in `~/.pi/agent/locks.json`; live ownership moves require confirmation, while `/new` and same-`cwd` process restarts resume automatically.
50
+ The adapter is session-local: only one π instance polls Telegram at a time. `/telegram-connect` records only external control/polling ownership in `~/.pi/agent/locks.json`; live ownership moves require confirmation, while `/new` and same-`cwd` restarts resume automatically. Local queue and reply state stay per Pi instance, so an instance that loses Telegram control still finishes work it already accepted.
51
51
 
52
52
  ### 4. Pair your Telegram account
53
53
 
@@ -78,7 +78,7 @@ What it feels like:
78
78
  - Switch models from Telegram mid-run; the adapter schedules a safe continuation instead of tearing state apart.
79
79
  - Send a voice note; a configured inbound handler or registered STT provider transcribes it; π answers in the same chat.
80
80
  - Drop a screenshot and ask, "what is broken here?" The image payload reaches π with the local file context.
81
- - Ask for a generated file; when π calls `telegram_attach`, the artifact returns to Telegram with the next reply.
81
+ - Ask for a generated file; when π calls `telegram_attach`, the artifact returns with the active Telegram reply or is sent directly to the paired/default chat from local work.
82
82
 
83
83
  ### Telegram controls
84
84
 
@@ -88,7 +88,7 @@ Use these inside the Telegram DM with your bot. The main entrypoint is `/start`:
88
88
  - **`/compact`**: Ask for inline confirmation, then start session compaction when the session is idle; Telegram shows the native typing indicator while manual or automatic compaction is running.
89
89
  - **`/next`**: Dispatch the next queued turn, aborting π first if needed.
90
90
  - **`/continue`**: Enqueue a priority `continue` prompt.
91
- - **`/abort`**: Abort the active run without touching the queue.
91
+ - **`/abort`**: Abort the active run without touching the queue. Abort-history applies only to Telegram-owned active turns; later local prompts do not make the next Telegram prompt absorb older queue items.
92
92
  - **`/stop`**: Abort the active run and clear waiting Telegram queue items.
93
93
 
94
94
  Hidden compatibility shortcuts: `/help` and `/status` open the main application menu, `/model` opens model controls, `/thinking` opens reasoning controls, `/queue` opens queue controls, and `/settings` opens bridge settings.
@@ -108,7 +108,7 @@ Run these inside π, not Telegram:
108
108
 
109
109
  Send files or images directly to the bot. Inbound downloads are saved under `<agent-dir>/tmp/telegram` and default to a 50 MiB limit. The agent dir is `~/.pi/agent` unless `PI_CODING_AGENT_DIR` overrides it.
110
110
 
111
- If you ask π for a generated file, π can call the `telegram_attach` tool and the adapter sends the file with the next Telegram reply. Outbound attachments also default to a 50 MiB limit. Environment variables for both limits are listed in [Environment-only configuration](#environment-only-configuration).
111
+ If you ask π for a generated file, π can call `telegram_attach`: during a Telegram-originated turn the adapter sends it with the next Telegram reply, and during local/TUI work it sends directly to the paired/default chat or explicit `chat_id`. Local work can also use `telegram_message` when you explicitly ask the agent to push a Markdown text message to Telegram; embedded `telegram_button` comments are parsed and attached to that message. Direct local/TUI delivery requires the current π instance to own `/telegram-connect`; if the lock belongs elsewhere, take over before sending. Outbound attachments default to a 50 MiB limit. Environment variables for both limits are listed in [Environment-only configuration](#environment-only-configuration).
112
112
 
113
113
  ## Core features
114
114
 
@@ -118,7 +118,7 @@ The inline application menu is the primary operator surface. It exposes status,
118
118
 
119
119
  ### Queue runtime
120
120
 
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.
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. Queue processing and reply delivery stay local to the Pi instance that accepted the work, even if `/telegram-connect` later moves elsewhere.
122
122
 
123
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
124
 
@@ -168,7 +168,7 @@ A practical voice setup is simple: Telegram `.ogg` arrives, STT runs locally or
168
168
 
169
169
  ### Outbound handlers, voice synthesis providers, and buttons
170
170
 
171
- Assistant replies can include hidden outbound blocks. `telegram_voice` and `telegram_button` are not π tools; they are assistant-authored HTML comments that the adapter removes from Telegram text and handles after `agent_end`. Recognized blocks must start at column zero on a top-level line outside fenced code, quotes, and lists.
171
+ Assistant replies can include hidden outbound blocks. `telegram_voice` and `telegram_button` are not π tools; they are assistant-authored HTML comments that the adapter removes from Telegram text and handles after `agent_end`. Recognized blocks must start at column zero on a top-level line outside fenced code, quotes, lists, and indented examples. Do not use JSON button specs, inline comments after visible text, or standalone button tool calls; write normal Markdown plus hidden comments, and add visible parent text if buttons would otherwise be the only output.
172
172
 
173
173
  ```md
174
174
  Full technical answer stays readable as text.
@@ -85,12 +85,13 @@ Telegram configuration lives in `~/.pi/agent/telegram.json`. Polling ownership l
85
85
 
86
86
  ### Runtime Ownership
87
87
 
88
- - `/telegram-connect` acquires or moves singleton ownership before polling starts.
88
+ - `/telegram-connect` acquires or moves singleton polling ownership before polling starts.
89
89
  - `/telegram-disconnect` stops polling and releases ownership.
90
90
  - Session start resumes polling only when the existing lock already points at the current `pid`/`cwd`, or when a stale same-`cwd` lock can be safely replaced after process restart.
91
91
  - Session replacement suspends polling/watchers without releasing ownership so the next session-start hook in the same process can resume.
92
92
  - Live polling owners require explicit takeover confirmation.
93
- - Long-lived timers use snapshotted ownership context and stop local polling when the lock no longer points at their own process.
93
+ - Long-lived polling timers use snapshotted ownership context and stop local polling when the lock no longer points at their own process.
94
+ - `locks.json` owns only external Telegram control/polling. Local extension and queue state are per Pi instance: losing the lock stops live Telegram control here, but does not drain or silence this instance's accepted queue, previews, final delivery, or dispatch.
94
95
 
95
96
  Deleting `locks.json` resets runtime ownership without deleting Telegram configuration.
96
97
 
@@ -137,7 +138,7 @@ Dispatch requires:
137
138
 
138
139
  A dispatched prompt remains queued until `agent_start` consumes it. This keeps the active Telegram turn bound for previews, attachments, aborts, and final replies.
139
140
 
140
- Post-agent-end queue dispatch uses a session-bound deferred dispatcher. It is activated on session start, clears timers on shutdown, and skips callbacks from older generations before touching `ExtensionContext`.
141
+ Post-agent-end queue dispatch uses a session-bound deferred dispatcher. It is activated on session start, clears timers on shutdown, and skips callbacks from older generations before touching `ExtensionContext`. Dispatch stays session-bound after polling ownership moves elsewhere.
141
142
 
142
143
  ### Controls And Menus
143
144
 
@@ -149,7 +150,7 @@ Immediate controls:
149
150
  - `/model`, `/thinking`, `/queue`, and `/settings` are hidden shortcuts to menu sections.
150
151
  - `/compact` opens an inline confirmation dialog and then runs compaction when the bridge is idle.
151
152
  - `/next` dispatches the next queued turn, aborting π first when needed.
152
- - `/abort` aborts the active Telegram-owned run while preserving queued items.
153
+ - `/abort` aborts active work while preserving queued items. Abort-history preservation is enabled only for Telegram-owned active turns; later local/non-Telegram agent starts clear stale abort-history mode so the next Telegram prompt appends instead of absorbing old queued turns as history.
153
154
  - `/stop` aborts and clears waiting Telegram queue items.
154
155
 
155
156
  Queued controls:
@@ -190,7 +191,7 @@ Final delivery attaches reply metadata only where requested. Reply parameters ap
190
191
 
191
192
  ### Outbound Artifacts And Assistant Actions
192
193
 
193
- Outbound files are delivered after the active Telegram turn completes. They must be staged with `telegram_attach`, are checked atomically per tool call, and use configurable size limits before photo/document upload.
194
+ Outbound files staged during an active Telegram turn are delivered after that turn completes. They use `telegram_attach`, are checked atomically per tool call, and use configurable size limits before photo/document upload. When no Telegram turn is active, `telegram_attach` sends files immediately to the paired/default chat or explicit `chat_id`; `telegram_message` provides direct local/TUI Markdown text delivery for explicit user requests and runs the same `telegram_button` markup planner so buttons attach to that text message. Direct local/TUI delivery is singleton-controlled: it requires this π instance to own `/telegram-connect`, while already accepted active-turn reply/attachment delivery remains session-local.
194
195
 
195
196
  Assistant-authored final-message actions use hidden top-level comments:
196
197
 
package/docs/locks.md CHANGED
@@ -107,7 +107,7 @@ Start/connect commands should make singleton moves easy:
107
107
  1. If no live owner exists, take ownership without an extra prompt
108
108
  2. If a live polling owner exists, ask whether to move singleton ownership to this pi instance
109
109
  3. On confirmation, write the current `{ "pid": ..., "cwd": ... }` to this extension's key in `locks.json`
110
- 4. The previous owner must notice that `locks.json` no longer points at its own `pid`/`cwd` and stop local runtime work without deleting the new lock
110
+ 4. The previous owner must notice that `locks.json` no longer points at its own `pid`/`cwd` and stop singleton-owned work such as polling/watchers without deleting the new lock or unrelated session-local queues
111
111
 
112
112
  Takeover prompts should use the extension name as the dialog title, then the question, a blank line, and source/target lines:
113
113
 
@@ -121,7 +121,9 @@ to: /new
121
121
 
122
122
  Avoid repeating the extension name in the body. Color is encouraged: extension title/name accent, question warning, `from:`/`to:` muted.
123
123
 
124
- The previous owner may use `fs.watch`, mtime polling, or an existing status/timer tick. Long-lived watchers should compare against a snapshotted `pid`/`cwd` identity rather than a live pi context object, because session replacement such as `/new` makes captured contexts stale. The important contract is graceful local shutdown after ownership mismatch.
124
+ The previous owner may use `fs.watch`, mtime polling, or an existing status/timer tick. Long-lived watchers should compare against a snapshotted `pid`/`cwd` identity rather than a live pi context object, because session replacement such as `/new` makes captured contexts stale. The important contract is graceful singleton-runtime shutdown after ownership mismatch while session-local state that does not require polling remains owned by its original instance.
125
+
126
+ For `pi-telegram`, direct local/TUI delivery tools (`telegram_message` and no-active-turn `telegram_attach`) are singleton-controlled and require current `/telegram-connect` ownership. They must fail when the lock is inactive or active elsewhere. Already accepted Telegram-turn reply delivery, previews, queued attachments, and queue finalization remain session-local and may complete after polling ownership moves away.
125
127
 
126
128
  ## Reset
127
129
 
package/docs/outbound.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  `pi-telegram` maps hidden assistant-authored HTML comments to Telegram-native outbound actions.
4
4
 
5
- This is intentionally prompt-driven: the agent writes normal Markdown plus small hidden top-level blocks, and the bridge performs the transport work after `agent_end`. `telegram_voice` and `telegram_button` are not π tools. Outbound behavior is an emergent result of the assistant prompt, text command-template handlers, registered voice synthesis providers, generated artifacts, and reply delivery. That avoids extra agent-side tool calls, avoids fragile parameter plumbing inside the conversation, and minimizes latency because text, voice, and buttons are planned in one standard assistant reply.
5
+ Normal Telegram-turn replies are intentionally prompt-driven: the agent writes Markdown plus small hidden top-level blocks, and the bridge performs transport after `agent_end`. `telegram_voice` and `telegram_button` are not π tools. For local/TUI-initiated work where the user explicitly asks to send something to Telegram, the bridge also exposes direct tools: `telegram_message` for Markdown text and `telegram_attach` for file delivery when no Telegram turn is active. Direct local/TUI delivery requires this π instance to own `/telegram-connect`; if polling/control ownership moved elsewhere, the tools fail instead of bypassing the singleton lock. Outbound behavior combines assistant prompt markup, text command-template handlers, registered voice synthesis providers, generated artifacts, direct Telegram tools, and reply delivery. Direct `telegram_message` text is planned through the same reply markup path, so embedded top-level `telegram_button` comments become buttons attached to that text message.
6
6
 
7
7
  Text handlers use the portable [Command Template Standard](./command-templates.md). Programmatic outbound handlers use `registerTelegramOutboundHandler(kind, handler)`. Voice replies can use configured command-template handlers or the provider API described in [Voice Integration](./voice.md).
8
8
 
@@ -121,11 +121,13 @@ Rules:
121
121
  - `telegram_button: Label` creates one independent label-only button row whose prompt equals the label.
122
122
  - `telegram_button label="Label" prompt="Prompt"` creates one independent button row whose prompt is the `prompt` attribute.
123
123
  - `telegram_button label="Label"` with a body creates one independent button row whose prompt is the block body.
124
- - The opening `<!-- telegram_button` marker must start at column zero on a top-level line outside fenced code, quotes, and lists; otherwise it is rendered as literal Markdown.
124
+ - The opening `<!-- telegram_button` marker must start at column zero on a top-level line outside fenced code, quotes, lists, and indented examples; otherwise it is literal Markdown.
125
125
  - Keep the canonical body form as `<!-- telegram_button label="Label"` + body + `-->`; closed heads must use `prompt="..."` or the colon shorthand to create a button.
126
126
  - Use one block per button; this mirrors HTML's singular element model and avoids a nested button DSL inside comments.
127
127
  - Button actions are stored in memory with short `callback_data`; Telegram never sees the full prompt in the button payload.
128
128
 
129
+ Do not emit JSON button specs, inline comments after visible text, standalone button actions, or tool calls for ordinary Telegram-turn buttons. The agent writes Markdown plus hidden comments; the bridge strips comments and attaches Telegram `reply_markup` after `agent_end`. For local/TUI-originated direct sends, put the same Markdown and `telegram_button` comments in `telegram_message(text)`.
130
+
129
131
  Buttons are built in and do not need a command template because they are pure Telegram reply markup plus callback routing.
130
132
 
131
133
  ## Prompt Contract
@@ -135,6 +137,7 @@ The extension injects Telegram-specific system prompt guidance so agents know th
135
137
  - Write the full technical answer as normal Markdown.
136
138
  - Add `telegram_voice` when a Telegram-native voice message is useful; use body text, `text="..."`, or colon shorthand for the text to synthesize. A companion summary is optional, no specific summary format is required.
137
139
  - Add `telegram_button: ...` when label equals prompt, `telegram_button label="..." prompt="..."` for one-line prompts, or `telegram_button label="..."` with a body for multiline prompts. If the reply contains only button/voice comment blocks, add a short visible marker (for example `Choose one:`) before them so Telegram always has a visible parent message for attachment.
138
- - Do not call Telegram transport tools for voice or buttons; the bridge owns delivery, while registered voice synthesis providers own TTS and OGG/Opus conversion.
140
+ - For ordinary Telegram-turn replies, do not call transport tools for voice or buttons; the bridge owns delivery, while registered voice synthesis providers own TTS and OGG/Opus conversion. For explicit local/TUI direct sends, `telegram_message` may include top-level `telegram_button` comments in its Markdown text because those buttons are attached to that text message.
141
+ - Never send buttons without visible parent text. If the answer would contain only hidden comments, add a compact line such as `Choose one:` first.
139
142
 
140
143
  This keeps the agent focused on semantics and lets the bridge handle low-latency Telegram adaptation.
@@ -34,8 +34,8 @@ import {
34
34
  Stable commands inside π:
35
35
 
36
36
  - `/telegram-setup` — configure/update the bot token.
37
- - `/telegram-connect` — start polling in the current session and acquire ownership.
38
- - `/telegram-disconnect` — stop polling and release ownership.
37
+ - `/telegram-connect` — start polling here and acquire external Telegram control ownership. Accepted queue/reply state stays local if ownership later moves elsewhere.
38
+ - `/telegram-disconnect` — stop polling and release ownership without deleting or silencing accepted local queue state.
39
39
  - `/telegram-status` — show connection, polling, execution, queue, and recent event diagnostics.
40
40
 
41
41
  ### Telegram commands
@@ -46,16 +46,17 @@ Stable commands inside the paired Telegram DM:
46
46
  - `/compact` — open confirmation and compact when idle.
47
47
  - `/next` — dispatch the next queued turn, aborting active work first when needed.
48
48
  - `/continue` — enqueue a priority `continue` prompt.
49
- - `/abort` — abort active Telegram-owned work and keep the queue.
49
+ - `/abort` — abort active work and keep the queue; abort-history is scoped to Telegram-owned active turns.
50
50
  - `/stop` — abort active Telegram-owned work and clear waiting Telegram queue items.
51
51
 
52
52
  Hidden compatibility shortcuts may open sections directly: `/help`, `/status`, `/model`, `/thinking`, `/queue`, and `/settings`.
53
53
 
54
54
  ### Tools and assistant-authored actions
55
55
 
56
- - `telegram_attach(paths)` is the stable artifact delivery tool for generated files.
56
+ - `telegram_attach(paths, chat_id?, caption?)` is the stable artifact delivery tool for generated files. During Telegram turns it queues files for the active reply; outside Telegram turns it sends files directly to the paired/default chat or explicit `chat_id` when this π instance owns `/telegram-connect`.
57
+ - `telegram_message(text, chat_id?)` sends a direct Telegram Markdown message from local/TUI-initiated work when this π instance owns `/telegram-connect`. Top-level `telegram_button` comments inside `text` are parsed with the same planner used for normal replies and attached to that message; buttons are never standalone Telegram messages.
57
58
  - `telegram_voice` hidden comments request Telegram-native voice delivery.
58
- - `telegram_button` hidden comments create inline buttons whose taps enqueue prompts.
59
+ - `telegram_button` hidden comments create inline buttons whose taps enqueue prompts. Use top-level column-zero comments outside code, quotes, lists, and indented examples; do not emit JSON button specs or standalone button actions.
59
60
 
60
61
  See [Outbound Handlers](./outbound.md) for exact markup forms.
61
62
 
@@ -160,7 +161,7 @@ Contract:
160
161
  - `id` is unique per active registry. Duplicate ids are rejected.
161
162
  - `ctx.callbackData(action, payload?)` builds compact `section:` callbacks and validates Telegram's 64-byte limit.
162
163
  - `ctx.edit()` auto-prepends the correct Back/Main-menu row. `ctx.open()` sends a standalone chat message without auto-navigation.
163
- - Section errors are isolated and surfaced as callback popups/diagnostics.
164
+ - Section dynamic-label, render, and callback errors are isolated, surfaced as callback popups where applicable, and reflected by `getTelegramSectionDiagnostics()` until the matching surface succeeds.
164
165
 
165
166
  Full behavior: [Extension Sections](./sections.md).
166
167
 
package/docs/sections.md CHANGED
@@ -157,7 +157,7 @@ Section rows are injected **before the ⚙️ Settings row**. Ordered by `order`
157
157
  ⚙️ Settings
158
158
  ```
159
159
 
160
- Built-in core rows keep priority. Section errors do not break menu rendering — a failed section is omitted with a diagnostic entry.
160
+ Built-in core rows keep priority. Section errors do not break menu rendering — a failed dynamic label is omitted with a diagnostic entry until a later label render succeeds.
161
161
 
162
162
  ### Settings submenu
163
163
 
@@ -222,7 +222,7 @@ If a section is unregistered or a token is unknown, the callback is answered wit
222
222
 
223
223
  > "This section is no longer available."
224
224
 
225
- Section errors are caught and surfaced as popup text. No unhandled exceptions leak to polling.
225
+ Section render and callback errors are caught, surfaced as popup text, and stored in section diagnostics until the matching surface later succeeds. No unhandled exceptions leak to polling.
226
226
 
227
227
  ## 8. Navigation Hierarchy
228
228
 
@@ -397,12 +397,12 @@ interface TelegramSectionDiagnostic {
397
397
  id: string;
398
398
  token: string;
399
399
  label: string;
400
- status: "active" | "stale" | "error";
400
+ status: "active" | "error";
401
401
  lastError?: string;
402
402
  }
403
403
  ```
404
404
 
405
- Available programmatically via `getTelegramSectionDiagnostics()`. Section runtime state is not shown in Telegram status text; sections should surface user-facing state through dynamic button labels and their own submenus.
405
+ Available programmatically via `getTelegramSectionDiagnostics()`. Main-menu/settings dynamic label failures, section render failures, and callback failures set `status: "error"` with `lastError`; the entry returns to `active` only after the matching label render, section render, or callback succeeds for that token. Section runtime state is not shown in Telegram status text; sections should surface user-facing state through dynamic button labels and their own submenus.
406
406
 
407
407
  ## 13. Purpose and Non-Goals
408
408
 
package/index.ts CHANGED
@@ -62,6 +62,13 @@ export default function (pi: Pi.ExtensionAPI) {
62
62
  const lockRuntime = Locks.createTelegramLockRuntime<Pi.ExtensionContext>();
63
63
  const lockOwnershipGuard =
64
64
  Locks.createTelegramLockOwnershipGuard(lockRuntime);
65
+ const telegramSessionContextStore =
66
+ Lifecycle.createTelegramSessionContextStore<Pi.ExtensionContext>();
67
+ const ownsTelegramDirectDelivery =
68
+ Locks.createTelegramDirectDeliveryOwnershipChecker({
69
+ lock: lockRuntime,
70
+ contextStore: telegramSessionContextStore,
71
+ });
65
72
  const activeTurnRuntime = Queue.createTelegramActiveTurnStore();
66
73
  const proactivePushChatIdGetter =
67
74
  Config.createTelegramProactivePushChatIdGetter({
@@ -101,11 +108,10 @@ export default function (pi: Pi.ExtensionAPI) {
101
108
  recordRuntimeEvent,
102
109
  });
103
110
  const pollingControllerState = Polling.createTelegramPollingControllerState();
104
- const { getStatusLines, updateStatus } =
105
- Status.createTelegramBridgeStatusRuntime<
106
- Pi.ExtensionContext,
107
- Queue.TelegramQueueItem<Pi.ExtensionContext>
108
- >({
111
+ const statusRuntime = Status.createTelegramBridgeStatusRuntime<
112
+ Pi.ExtensionContext,
113
+ Queue.TelegramQueueItem<Pi.ExtensionContext>
114
+ >({
109
115
  getConfig: configStore.get,
110
116
  isPollingActive: Polling.createTelegramPollingActivityReader(
111
117
  pollingControllerState,
@@ -121,17 +127,7 @@ export default function (pi: Pi.ExtensionAPI) {
121
127
  getRecentRuntimeEvents: runtimeEvents.getEvents,
122
128
  getRuntimeLockState: lockRuntime.getStatusLabel,
123
129
  });
124
- const currentModelRuntime = Model.createCurrentModelRuntime({
125
- getContextModel,
126
- updateStatus,
127
- });
128
- const queueMutationRuntime = Queue.createTelegramQueueMutationController({
129
- ...telegramQueueStore,
130
- getNextPriorityReactionOrder: queue.getNextPriorityReactionOrder,
131
- incrementNextPriorityReactionOrder:
132
- queue.incrementNextPriorityReactionOrder,
133
- updateStatus,
134
- });
130
+ const { getStatusLines, updateStatus } = statusRuntime;
135
131
  const inboundHandlerRuntime = Inbound.createTelegramInboundHandlerRuntime({
136
132
  getHandlers: configStore.getInboundHandlers,
137
133
  execCommand: CommandTemplates.execCommandTemplate,
@@ -177,6 +173,17 @@ export default function (pi: Pi.ExtensionAPI) {
177
173
  updateStatus,
178
174
  recordRuntimeEvent,
179
175
  });
176
+ const currentModelRuntime = Model.createCurrentModelRuntime({
177
+ getContextModel,
178
+ updateStatus,
179
+ });
180
+ const queueMutationRuntime = Queue.createTelegramQueueMutationController({
181
+ ...telegramQueueStore,
182
+ getNextPriorityReactionOrder: queue.getNextPriorityReactionOrder,
183
+ incrementNextPriorityReactionOrder:
184
+ queue.incrementNextPriorityReactionOrder,
185
+ updateStatus,
186
+ });
180
187
 
181
188
  // --- Reply Runtime & Preview ---
182
189
 
@@ -217,7 +224,6 @@ export default function (pi: Pi.ExtensionAPI) {
217
224
  sendDraft: sendMessageDraft,
218
225
  sendMessage,
219
226
  editMessageText: editTelegramMessageText,
220
- canSend: lockOwnershipGuard.ownsCurrentProcess,
221
227
  recordRuntimeEvent,
222
228
  ...replyTransport,
223
229
  });
@@ -409,12 +415,16 @@ export default function (pi: Pi.ExtensionAPI) {
409
415
  stopPolling: lockedPollingRuntime.suspend,
410
416
  recordRuntimeEvent,
411
417
  });
412
- const sessionLifecycleRuntime = Lifecycle.appendTelegramLifecycleHooks(
418
+ const baseSessionLifecycleRuntime = Lifecycle.appendTelegramLifecycleHooks(
413
419
  queueSessionLifecycle,
414
420
  {
415
421
  onSessionStart: lockedPollingRuntime.onSessionStart,
416
422
  },
417
423
  );
424
+ const sessionLifecycleRuntime = Lifecycle.appendTelegramLifecycleHooks(
425
+ baseSessionLifecycleRuntime,
426
+ Lifecycle.createTelegramSessionContextTracker(telegramSessionContextStore),
427
+ );
418
428
 
419
429
  // --- Extension API Bindings ---
420
430
 
@@ -425,6 +435,11 @@ export default function (pi: Pi.ExtensionAPI) {
425
435
  activeTurnRuntime,
426
436
  lockedPollingRuntime,
427
437
  getStatusLines,
438
+ buttonActionStore,
439
+ sendMarkdownReply,
440
+ callMultipart,
441
+ getDefaultChatId: proactivePushChatIdGetter,
442
+ canSendDirect: ownsTelegramDirectDelivery,
428
443
  updateStatus,
429
444
  recordRuntimeEvent,
430
445
  });
package/lib/bindings.ts CHANGED
@@ -41,6 +41,16 @@ interface TelegramCommandsAndToolsBindingDeps {
41
41
  activeTurnRuntime: Queue.TelegramActiveTurnStore<Queue.PendingTelegramTurn>;
42
42
  lockedPollingRuntime: Locks.TelegramLockedPollingRuntime<Pi.ExtensionContext>;
43
43
  getStatusLines: () => string[];
44
+ buttonActionStore: OutboundHandlers.TelegramButtonActionStore;
45
+ sendMarkdownReply: (
46
+ chatId: number,
47
+ replyToMessageId: number | undefined,
48
+ markdown: string,
49
+ options?: { replyMarkup?: unknown },
50
+ ) => Promise<number | undefined>;
51
+ callMultipart: OutboundHandlers.TelegramVoiceReplySenderDeps["sendMultipart"];
52
+ getDefaultChatId: () => number | undefined;
53
+ canSendDirect: () => boolean;
44
54
  updateStatus: TelegramBridgeStatusUpdater;
45
55
  recordRuntimeEvent: TelegramRuntimeEventRecorder;
46
56
  }
@@ -52,11 +62,29 @@ export function registerTelegramCommandsAndTools({
52
62
  activeTurnRuntime,
53
63
  lockedPollingRuntime,
54
64
  getStatusLines,
65
+ buttonActionStore,
66
+ sendMarkdownReply,
67
+ callMultipart,
68
+ getDefaultChatId,
69
+ canSendDirect,
55
70
  updateStatus,
56
71
  recordRuntimeEvent,
57
72
  }: TelegramCommandsAndToolsBindingDeps): void {
58
73
  OutboundAttachments.registerTelegramOutboundAttachmentTool(pi, {
59
74
  getActiveTurn: activeTurnRuntime.get,
75
+ getDefaultChatId,
76
+ canSendDirect,
77
+ sendMultipart: callMultipart,
78
+ recordRuntimeEvent,
79
+ });
80
+ OutboundAttachments.registerTelegramOutboundMessageTool(pi, {
81
+ getDefaultChatId,
82
+ canSendDirect,
83
+ planMessage: OutboundHandlers.createTelegramOutboundReplyPlanner(
84
+ buttonActionStore,
85
+ ),
86
+ sendMarkdownMessage: (chatId, markdown, options) =>
87
+ sendMarkdownReply(chatId, undefined, markdown, options),
60
88
  recordRuntimeEvent,
61
89
  });
62
90
  Commands.registerTelegramBridgeCommands(pi, {
@@ -229,15 +257,17 @@ export function registerTelegramLifecycleRuntimeHooks({
229
257
  resetPendingModelSwitch: modelSwitchController.clearPendingSwitch,
230
258
  setQueuedItems: telegramQueueStore.setQueuedItems,
231
259
  clearDispatchPending: lifecycle.clearDispatchPending,
260
+ setFoldQueuedPromptsIntoHistory: lifecycle.setFoldQueuedPromptsIntoHistory,
232
261
  setActiveTurn: activeTurnRuntime.set,
233
262
  createPreviewState: previewRuntime.resetState,
234
263
  startTypingLoop: promptDispatchRuntime.startTypingLoop,
235
264
  updateStatus,
236
265
  getActiveTurn: activeTurnRuntime.get,
237
266
  extractAssistant: Replies.extractLatestAssistantMessageText,
238
- getPreserveQueuedTurnsAsHistory:
239
- lifecycle.shouldPreserveQueuedTurnsAsHistory,
267
+ getFoldQueuedPromptsIntoHistory:
268
+ lifecycle.shouldFoldQueuedPromptsIntoHistory,
240
269
  resetRuntimeState: agentEndResetter,
270
+ waitForTypingIdle: typing.waitForIdle,
241
271
  dispatchNextQueuedTelegramTurn,
242
272
  requestDeferredDispatchNextQueuedTelegramTurn:
243
273
  deferredQueueDispatchRuntime.request,
@@ -251,7 +281,6 @@ export function registerTelegramLifecycleRuntimeHooks({
251
281
  sendGuestReply,
252
282
  planOutboundReply: outboundReplyPlanner,
253
283
  sendOutboundReplyArtifacts: outboundReplyArtifactSender,
254
- isCurrentOwner: lockOwnershipGuard.ownsContext,
255
284
  getDefaultChatId: proactivePushChatIdGetter,
256
285
  isProactivePushEnabled,
257
286
  recordRuntimeEvent,