@llblab/pi-telegram 0.20.6 → 0.21.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
@@ -83,7 +83,7 @@ The core product loop is mobile continuation: start or supervise work in the ter
83
83
  - Instance thread names are provisioner-authored identity metadata, not model-authored output. Fresh Threaded Mode threads select one of five curated compact 4-6 letter Latin names for the assigned slot letter using provisioning timestamp entropy, then create the Telegram thread with that thread name immediately. The title is the thread name itself, not `{slot} {threadName}` or `{slot} — {threadName}`. Do not register or reintroduce an agent-facing thread rename tool; removing the extra rename prompt/tool turn is an intentional simplicity constraint. Thread names must mirror into terminal status, live diagnostics, and `[telegram|thread:name]` through one target-aware current-instance identity resolver. Registered follower/leader metadata takes precedence over a stale shared record for the same target, with the record used only as fallback; do not hide a valid baked/custom thread name behind old slot-prefix validation. A follower that later becomes leader keeps its existing name; leadership changes are transport role changes, not identity resets.
84
84
  - Telegram private-chat Threaded Mode treats `All` as an aggregate/control surface, not a durable `General` thread and not a process launcher. Leader activation creates/reuses a leader thread in the private bot DM (chat = allowedUserId) proactively. If all threads are closed and the owner writes from `All`, a first new unbound thread should be reclaimed by the leader when the leader lacks an active bound thread. Later unknown threads require explicit routing or restore; creating another Pi instance is a manual terminal action followed by `/telegram-connect`.
85
85
  - Unbound thread detection: when a message arrives from the owner in an unknown `message_thread_id` while Threaded Mode is active, the default runtime first reclaims it for the leader if the leader has no active bound thread and routes the prompt locally. Later unknown threads are preserved by default and receive an in-thread reroute chooser that can send the captured prompt to a live thread or restore a stale leader/follower binding through explicit restore actions; destructive cleanup happens only after explicit user confirmation and through `thread-reconciler`. If the operator wants another Pi instance, they start Pi in a terminal and run `/telegram-connect`; Telegram-originated hidden auto-spawn and `/thread`-style process launch flows are intentionally absent. If Threaded Mode is unavailable, the message is processed normally through classic routing.
86
- - Telegram extension work should not hold Pi's core agent lifecycle hostage once Pi has produced the semantic result. For Telegram-originated turns, final Telegram delivery, attachment upload, and transport cleanup are extension-owned side effects; schedule them off the critical `agent_end` path when ordering can still be preserved, record failures as runtime events, and keep dispatch of the next Telegram turn behind the delivery task when needed to avoid interleaving previews/finals.
86
+ - Telegram extension work should not hold Pi's core agent lifecycle hostage once Pi has produced the semantic result. For Telegram-originated turns, final Telegram delivery, attachment upload, and transport cleanup are extension-owned side effects; schedule them off the critical `agent_end` path when ordering can still be preserved, record failures as runtime events, and keep dispatch of the next Telegram turn behind the delivery task when needed to avoid interleaving previews/finals. Public activity handlers run through isolated asynchronous per-handler queues; lifecycle hooks enqueue normalized events and never await consumer work.
87
87
  - Telegram runtime state should be treated as event-driven reconciliation of extension assumptions against observable Telegram signals, not as a full Telegram bot read-model and not as a reason to query Telegram on every action. Keep a local assumption model for bot identity/capabilities, pairing assumptions, thread support, known thread lifecycle, bound instance targets, reservations, and transport health. Invalidate and reconcile the relevant slice on meaningful events: startup/reload, lifecycle service messages, stale-send/API errors, setup/pairing changes, follower register/prune, explicit status/debug requests, and bounded low-frequency leader health ticks. `lib/sync.ts` owns sync slices, observation intake, invalidation triggers, status/debug freshness, and reconciliation scheduling; it must not promise complete bot-state mirroring because Bot API lacks a complete topic listing surface. `lib/thread-reconciler.ts` owns pure Threaded Mode lifecycle planning and should become the only policy authority for destructive thread cleanup decisions. `threads` owns current Telegram UI thread/tab binding primitives and thread-name helpers mapped to Bot API topic transport, `status` renders projections, and `index.ts` only wires ports. `tmp/telegram/logs.jsonl` is a session-local redacted runtime evidence stream for race debugging and resets on extension start / runtime scope changes; it is diagnostics only, not routing/provisioning authority. `state.json` should be an observable/debug snapshot aligned with `/telegram-status` (extension runtime, bot capabilities including `bot.lastSlot`, runtime role, live roster projection, reservations, diagnostics), not routing/provisioning authority. Because every process on one Telegram profile shares this file, only the active transport lock owner may persist it; followers read current state and gain write authority only after promotion. Status-only writes must refresh disk-backed bindings before serialization so a stale loaded snapshot cannot erase newer leader state. Live bus/runtime state is authoritative; file reservations and fresh capability observations may be startup hints/collision guards only; stale snapshots must re-probe before suppressing bus/topic behavior. Do not persist stale/offline/failed thread target history as source of truth.
88
88
  - Thread bindings are bidirectional. From the Pi-instance side, an instance knows and preserves its target, slot, thread name, lifecycle state, and direct-delivery defaults. From the Telegram-client/bot side, the bridge observes thread creation/close/reopen/stale-send signals and reconciles them into instance binding state. Lifecycle transitions that affect operator understanding should be Telegram-visible when a live transport remains: every successful follower registration/re-registration gets a compact connected notice in the assigned thread, while heartbeat pruning stays silent because it is only liveness suspicion. These notices should use the instance thread name when known and fall back to the slot letter only while the thread is still unnamed. New unknown threads (owner writing in `All`) are preserved until the user explicitly chooses a reroute/restore/new-instance action that makes cleanup eligible; any destructive cleanup must go through `thread-reconciler`. Unknown `forum_topic_created` service events are observations, not destructive cleanup proof.
89
89
  - The multi-instance bus uses private bot DMs with Telegram private-chat Threaded Mode enabled for the bot. No supergroup, group, or forum configuration is needed; the thread chat is always the private chat with the paired owner. Threaded capability checks must use bot/private-chat evidence such as `getMe.has_topics_enabled`, incoming `message_thread_id`, and topic operation success/failure; do not use group chat metadata as the control-plane truth for Telegram private-chat Threaded Mode.
@@ -159,9 +159,9 @@ The canonical detailed ownership map lives in [`docs/architecture.md`](./docs/ar
159
159
 
160
160
  - Scheduling and lifecycle: `queue`, `runtime`, `lifecycle`, `locks`
161
161
  - Telegram transport, polling, bus, sync, and inbound flow: `api`, `polling`, `bus`, `bus-api`, `bus-leader`, `bus-follower`, `ownership`, `target`, `sync`, `thread-reconciler`, `threads`, `updates`, `routing`, `media`, `turns`, `inbound`, `config`, `setup`
162
- - Response surfaces: `preview`, `replies`, `rendering`, `keyboard`, `outbound-markup`, `outbound-attachments`, `outbound`, `voice`, `status`
162
+ - Response surfaces: `preview`, `replies`, `rendering`, `keyboard`, `delivery`, `activity`, `outbound-markup`, `outbound-attachments`, `outbound`, `voice`, `status`
163
163
  - Controls and application menu UI: `commands`, `menu`, `menu-model`, `menu-thinking`, `menu-status`, `menu-queue`, `model`, `prompts`
164
- - 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
164
+ - Extension platform: `sections` owns section registry, token mapping, callback dispatch, context building, and its globalThis bridge; `delivery` owns target-aware extension views, logical handles, target policy, ordering, lifecycle fencing, and its globalThis runtime membrane; `activity` owns normalized lifecycle registration, activity/source identity, non-blocking dispatch, delivery contexts, and its globalThis runtime membrane; `voice` owns the voice-provider registry and its globalThis bridge
165
165
  - 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
166
166
 
167
167
  ## 6.5 Entrypoint And Import Boundaries
@@ -203,9 +203,11 @@ The canonical detailed ownership map lives in [`docs/architecture.md`](./docs/ar
203
203
  - 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.
204
204
  - 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
205
205
  - 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
206
- - 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
207
- - 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
206
+ - 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`, `/delivery`, `/voice`, `/inbound`, `/outbound`, and `/updates`; `0.12.0` intentionally removes the published `@llblab/pi-telegram/lib/*.ts` compatibility wildcard
207
+ - `Companion` names the extension consumer/product relationship, not a core domain responsibility. Source headers, Zones, module names, public API names, and domain docs should use the owned capability (`delivery`, `activity`, `sections`, `voice`, or extension interop); reserve companion terminology for the external extension that composes those APIs.
208
+ - 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 })`). Imperative `/delivery` operations resolve the current runtime on every call and use generation-bound logical handles rather than registration ids or captured Pi contexts. 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
208
209
  - Telegram prompt-template commands are discovered from Pi slash commands with `source: "prompt"`; Pi template names are mapped to Bot API-compatible aliases (`fix-tests` → `/fix_tests`), aliases that conflict with built-in bridge commands or hidden shortcuts are not displayed, prompt-template aliases stay out of the Telegram bot command menu, and the bridge expands template files before queueing because extension-originated `sendUserMessage()` bypasses Pi's interactive template expansion
210
+ - Target-aware companion delivery follows [`docs/delivery.md`](./docs/delivery.md): `active-turn`, `instance`, `aggregate`, and explicit target scopes resolve through current profile/role state; followers may target only their assigned thread or aggregate surface, leaders may target current live bindings, logical handles reconcile chunked send/edit/delete, and reload/session replacement invalidates old generations
209
211
  - 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
210
212
  - 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
211
213
  - 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
package/BACKLOG.md CHANGED
@@ -1,50 +1,6 @@
1
1
  # Project Backlog
2
2
 
3
- _This backlog tracks only open release-relevant work: live promoted-follower verification, evidence-gated Telegram client/runtime follow-ups, and upstream Pi API blockers. Completed validation evidence belongs in `CHANGELOG.md`, not in this queue._
4
-
5
- ## P1 — Guest Media Live Follow-Ups
6
-
7
- Context: 0.20.5 shipped deterministic Guest Mode file/audio delivery coverage. Post-release private-DM smoke confirmed that one local document reaches the remote conversation through `answerGuestQuery`; remaining checks validate Telegram client behavior rather than gate the implemented transport.
8
-
9
- Open work:
10
-
11
- - [x] Confirm one local document in a private Guest Mode DM after extension reload.
12
- - [x] Confirm one synthesized paired-comment voice result in a private Guest Mode DM; Telegram delivered it to the correctly attributed remote peer without visible fallback text.
13
- - [ ] Confirm one local document and one synthesized voice/audio result in group Guest Mode.
14
- - [ ] Record a focused client/API caveat only if live behavior contradicts the one-result and staging contracts.
15
-
16
- Done when: private and group Guest Mode each have direct document and voice/audio delivery evidence, or a confirmed Telegram limitation is documented.
17
-
18
- ## P1 — Compaction Status Ownership And Native Activity
19
-
20
- Context: Pi already renders its own compaction lifecycle, while pi-telegram currently overrides its terminal status row with `compacting` whenever the shared compaction flag is set. This duplicates Pi-owned state and hides the distinction between Telegram-owned activity and unrelated automatic/session compaction. Manual `/compact` already calls the typing-loop port and automatic compaction starts typing only when an active Telegram turn exists, so the reported absence of Telegram `…typing` needs transport-level and live verification rather than an assumed rewrite.
21
-
22
- Planned work:
23
-
24
- - [x] Remove `compacting` as a pi-telegram terminal status label while retaining the internal compaction flag for queue/dispatch safety and explicit diagnostics.
25
- - [x] Track compaction origin for status projection: confirmed Telegram `/compact` and auto-compaction inside a Telegram-owned turn render normal `Active`; local/autonomous/background compaction keeps the stable `connected`, `leader`, or `follower` role.
26
- - [x] Define and verify the native activity matrix: Telegram-owned compaction targets the invoking/active thread plus `All`; non-Telegram compaction uses the connected instance target plus `All` without changing terminal role semantics.
27
- - [x] Trace manual confirmation, `session_before_compact`, `session_compact`, completion, error, timeout, abort, and shutdown ordering to ensure one keyed typing loop remains active for the whole compaction window and always stops.
28
- - [x] Add transport-level regressions that assert actual `sendChatAction(typing)` targets and keepalive lifecycle, not only invocation of a mocked `startTypingLoop` callback.
29
- - [x] Replace status tests that currently require `compacting` with Telegram-owned `Active` and non-Telegram stable-role cases; preserve `/telegram-status` compaction diagnostics where operationally useful.
30
- - [ ] Capture live evidence for manual Telegram compaction, auto-compaction during a Telegram turn, and non-Telegram auto-compaction before finalizing the activity contract.
31
-
32
- Done when: Pi remains the only terminal owner of the `compacting` label, pi-telegram status reflects Telegram ownership rather than generic compaction, and Telegram native `…typing` remains visible and correctly targeted throughout every confirmed compaction class without leaking afterward.
33
-
34
- ## P1 — Leader Endpoint Loss Recovery
35
-
36
- Context: live evidence showed a process retaining a fresh transport lock and active polling while its Threaded Mode Unix socket path was absent. The likely trigger was external removal of the shared Telegram temp directory while the owner process remained alive. The local server keeps listening on the unlinked Unix socket but `start()` treats its in-memory server handle as sufficient, leader health checks only Bot API transport, and a new instance therefore exhausts follower-registration retries with `ENOENT`. This is a real diagnosable recovery gap, but not yet evidence for a broad readiness protocol or automatic takeover; force-acquiring while the old owner may still run `getUpdates` would risk split-brain.
37
-
38
- Planned work:
39
-
40
- - [x] Reproduce deterministically by unlinking only the active Unix leader socket while its process, polling runtime, and in-memory server remain live. Native Windows named pipes have no equivalent filesystem path to unlink, so recovery remains Unix-specific unless separate named-pipe evidence appears.
41
- - [x] Let the owning Threaded Mode runtime detect an externally missing Unix endpoint during its existing health/prune cadence and restart only the local bus server without changing lock ownership, leader epoch, polling, or thread bindings.
42
- - [x] Make initial follower registration report `live owner / unreachable bus endpoint` after bounded retries, with direct operator guidance; do not add automatic or force takeover without separate evidence that the old owner cannot still poll.
43
- - [x] Keep intentional classic ownership unchanged because classic mode does not require a bus endpoint.
44
- - [x] Add focused regressions for Unix endpoint unlink/rebind, bounded follower diagnosis, leader reload overlap, and no duplicate `getUpdates` ownership; add Windows coverage only for behavior the named-pipe transport can reproduce.
45
- - [ ] Capture live recovery evidence without deleting lock/state or creating a replacement Telegram thread.
46
-
47
- Done when: the confirmed endpoint-loss scenario either self-recovers under the existing owner or produces precise safe remediation, while classic mode and single-owner polling remain unchanged.
3
+ _This backlog tracks only open release-relevant work: live runtime verification, evidence-gated Telegram client follow-ups, and upstream Pi API blockers. Completed outcomes and validation evidence belong in `CHANGELOG.md`, not in this queue._
48
4
 
49
5
  ## P1 — Promoted Follower Reload Evidence
50
6
 
@@ -98,6 +54,8 @@ Blocked: upstream Pi core API. Issue: https://github.com/earendil-works/pi/issue
98
54
 
99
55
  Context: Threaded Mode manual followers are separate visible Pi processes. Same-thread `/new` is a different feature: replacing the current Pi session inside the same Telegram thread. Extension-only hacks are rejected because they would desynchronize Pi lifecycle/TUI semantics.
100
56
 
57
+ Current upstream evidence: Pi 0.80.6 safely exposes `ctx.newSession()` to registered extension commands through `ExtensionCommandContext`, including fresh-context rebinding after replacement. Telegram update and callback handlers still receive only `ExtensionContext`, and extension-origin `pi.sendUserMessage()` deliberately disables slash-command handling. The upstream maintainer described an async extension bridge as potentially possible after the current refactor, but no supported API exists yet.
58
+
101
59
  Required upstream shape:
102
60
 
103
61
  - `pi.newSession(...)` or `pi.requestSessionReplacement(...)` callable from trusted extension runtime code.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.21.0: Activity And Delivery Extension Platform
4
+
5
+ - `[Platform Documentation]` Completed 0.21 discoverability across the README, public API inventory/smoke checklist, Activity and Delivery contracts, architecture map, Sections reference, package contents, documented consumer policy examples, and the external demo-project reference. Removed the completed platform epic from `BACKLOG.md` while preserving deferred media/process boundaries and the concrete callback revisit trigger in docs. Impact: extension authors can find and validate the complete supported surface without `/lib` imports, and open-work context now contains only unfinished work.
6
+ - `[Platform Validation]` Added a full-runtime classic-mode regression proving public activity delivery reaches the assigned Telegram surface while `agent_start` remains independent from the consumer's pending Bot API send. Expanded scope coverage to distinguish active-turn, assigned-instance, and aggregate targets and verify autonomous activity can opt into aggregate delivery; existing leader/follower transport, authorization, stale-generation, ordering, chunk reconciliation, and cleanup regressions complete the platform matrix. Impact: the 0.21 extension contracts now carry focused domain, public-boundary, and runtime integration proof.
7
+ - `[Callback Boundary]` Confirmed that the issue #126 consumer shape needs no second managed callback registry: Sections already own interactive Settings namespacing, answers, edits, navigation, and cleanup, while delivered activity rows remain non-interactive and raw update handlers stay the low-level escape hatch. Impact: the platform avoids duplicate callback ownership and records the concrete trigger for revisiting the boundary.
8
+ - `[Activity API]` Added the public `@llblab/pi-telegram/activity` membrane and wired Pi input, agent, assistant text/reasoning, executed-tool, compaction, settlement, and shutdown lifecycle signals into evidence-based activity/source identities. Isolated asynchronous per-handler queues preserve semantic order, coalesce adjacent deltas, fence shutdown generations, report handler failures, and provide fresh target-aware delivery contexts without awaiting consumer work or duplicating bridge previews/finals. Impact: extension consumers can build lifecycle-adjacent Telegram surfaces without private Pi contexts, raw provider messages, or bridge transport ownership.
9
+ - `[Delivery API]` Added the public `@llblab/pi-telegram/delivery` membrane for ownership-gated operational views and chat actions across active-turn, current-instance, aggregate, and explicitly authorized targets. Logical handles reconcile chunked send/edit/delete operations, per-target queues preserve ordering, followers route through the leader transport, and reload/session replacement invalidates old generations without captured Pi contexts. Impact: companion extensions can now deliver lifecycle-adjacent Telegram UI through stable ports instead of importing bridge internals or owning another bot client.
10
+ - `[Public API]` Documented the complete capability inventory, intentional private boundaries, target policy, structured delivery failures, callback boundary decision, and deferred media/process controls; added public-boundary, classic/leader/follower policy, chunk reconciliation, ordering, and lifecycle regressions. Impact: extension authors can distinguish supported platform capabilities from deliberate safety boundaries and the completed 0.21 extension-platform surface.
11
+ - `[Pi Compatibility]` Set the Activity platform floor to Pi/core/AI `0.80.6`, registered the public typed `agent_settled` lifecycle event directly, and aligned Telegram model controls with Pi's `max` thinking level. Impact: activity identity now closes at a supported terminal boundary without unsafe event casts or silently merging unrelated runs on older Pi versions.
12
+ - `[Delivery Lifecycle]` Constructed a fresh Delivery runtime and generation for every session start, shut down displaced bindings, and fenced queued or multi-step in-flight work at every transport boundary. Impact: session replacement cannot revive old handles or let an old operation continue chunks, edits, deletes, or chat actions through a new session.
13
+ - `[Delivery Recovery]` Added recoverable partial handles to failed multi-chunk sends and edit growth, tracking every message id that remains visible after transport failure. Impact: callers can deterministically retry or clean up a partially materialized logical view instead of losing its only valid handle.
14
+ - `[Activity Compaction]` Added explicit abandonment for standalone compaction activities that never receive `session_compact`, triggered by the next run/compaction boundary or the bounded observer timeout, while preserving compaction inside an active agent identity and ignoring late completion. Impact: cancelled or failed compaction cannot leak its activity id or source into an unrelated run.
15
+ - `[Activity Diagnostics]` Reconciled the public contract with implemented diagnostics: handler failures carry redacted identity metadata through the bounded bridge recorder, while 0.21 intentionally exposes no queue phase/length/latency telemetry or drop policy beyond adjacent delta coalescing. Impact: consumers can reason from documented backpressure behavior without relying on diagnostics the runtime does not emit.
16
+ - `[Activity Lifecycle]` Recreated the Activity dispatcher with a fresh generation on every session start while stopping and clearing the retiring generation at shutdown, and bound handler contexts to their exact dispatcher generation and registration. Impact: same-process session replacement resumes normalized lifecycle delivery while blocked handlers from the retiring session fail closed instead of sending stale work through replacement Delivery.
17
+ - `[Guest Identity]` Kept private Guest Mode location identity anchored to the strongest remote-conversation evidence and excluded bot-authored replies from guest resolution, while retaining a non-bot replied peer as a final fallback when chat/caller identity is absent. Impact: replying to a Guest Mode bot response preserves `[telegram|guest:<remote-user>]` alongside `[reply|from:<bot>]` instead of replacing the conversation peer with the bot username.
18
+
3
19
  ## 0.20.6: Guest Attribution And Voice Action Hotfix
4
20
 
5
21
  - `[Voice Action Syntax]` Hidden `telegram_voice` actions now accept the intuitive paired form `<!-- telegram_voice ... -->...<!-- /telegram_voice -->` alongside inline, attribute-text, and single-comment multiline forms. The parser captures a non-empty multiline body and preserves language/rate attributes while leaving surrounding prose visible. Impact: agents can use an explicit closing tag without leaking the intended TTS payload as ordinary text.
package/README.md CHANGED
@@ -24,6 +24,8 @@ From git:
24
24
  pi install git:github.com/llblab/pi-telegram
25
25
  ```
26
26
 
27
+ The 0.21 extension platform requires Pi `0.80.6` or newer. Its Activity API uses the public `agent_settled` lifecycle event to keep retries/continuations under one activity identity and release that identity only after the run fully settles.
28
+
27
29
  ## Quick Start
28
30
 
29
31
  ### 1. Create a Telegram bot
@@ -217,12 +219,14 @@ Companion extensions can integrate with Telegram without owning polling or trans
217
219
  - Register Telegram slash commands.
218
220
  - Add menu sections and settings surfaces.
219
221
  - Add compact status rows.
222
+ - Deliver target-aware operational views and chat actions from companion code.
223
+ - Observe normalized assistant, reasoning, tool, compaction, and settlement activity without blocking Pi.
220
224
  - Handle update/callback namespaces.
221
225
  - Provide inbound preprocessing handlers.
222
226
  - Provide outbound voice synthesis.
223
227
  - Use direct delivery helpers for explicit local/TUI sends.
224
228
 
225
- Stable public entrypoints are documented in [Public API](./docs/public-api.md), [Extension Sections](./docs/sections.md), [Inbound Handlers](./docs/inbound.md), [Outbound Handlers](./docs/outbound.md), [Updates](./docs/updates.md), and [Voice Integration](./docs/voice.md).
229
+ Stable public entrypoints are documented in [Public API](./docs/public-api.md), [Telegram Delivery API](./docs/delivery.md), [Telegram Activity API](./docs/activity.md), [Extension Sections](./docs/sections.md), [Inbound Handlers](./docs/inbound.md), [Outbound Handlers](./docs/outbound.md), [Updates](./docs/updates.md), and [Voice Integration](./docs/voice.md).
226
230
 
227
231
  ## Safety Boundaries
228
232
 
@@ -241,6 +245,8 @@ Telegram is a companion surface around a live Pi runtime, not a second runtime.
241
245
 
242
246
  - [Architecture](./docs/architecture.md) — runtime, domains, queue, transport, and Threaded Mode overview.
243
247
  - [Public API](./docs/public-api.md) — package entrypoints and stable companion-extension contracts.
248
+ - [Telegram Delivery API](./docs/delivery.md) — target-aware operational views, logical message handles, and lifecycle-safe transport.
249
+ - [Telegram Activity API](./docs/activity.md) — normalized lifecycle events, source identity, non-blocking delivery contexts, and consumer policy examples.
244
250
  - [Inbound Handlers](./docs/inbound.md) — Telegram-to-Pi preprocessing pipelines.
245
251
  - [Outbound Handlers](./docs/outbound.md) — final text/voice/file transformation and delivery.
246
252
  - [Voice Integration](./docs/voice.md) — STT/TTS provider model and reply policies.
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Public Telegram activity API
3
+ * Zones: package boundary, pi agent lifecycle, extension interop
4
+ * Exposes normalized non-blocking activity registration while keeping lifecycle wiring and dispatch internals package-private
5
+ */
6
+
7
+ export {
8
+ registerTelegramActivityHandler,
9
+ type TelegramActivityContext,
10
+ type TelegramActivityEnvelope,
11
+ type TelegramActivityEvent,
12
+ type TelegramActivityHandlerRegistration,
13
+ type TelegramActivitySource,
14
+ type TelegramActivityTarget,
15
+ } from "../lib/activity.ts";
package/api/commands.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Public Telegram commands API
3
- * Zones: package boundary, companion extension interop
3
+ * Zones: package boundary, extension interop
4
4
  * Exposes the stable Telegram slash-command registration surface while keeping registry internals package-private
5
5
  */
6
6
 
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Public Telegram delivery API
3
+ * Zones: package boundary, telegram delivery, extension interop
4
+ * Exposes target-aware operational view delivery while keeping transport and runtime binding internals package-private
5
+ */
6
+
7
+ export {
8
+ deleteTelegramView,
9
+ editTelegramView,
10
+ sendTelegramChatAction,
11
+ sendTelegramView,
12
+ type SendTelegramViewOptions,
13
+ type TelegramDeliveryChatAction,
14
+ type TelegramDeliveryFailureReason,
15
+ type TelegramDeliveryHandle,
16
+ type TelegramDeliveryParseMode,
17
+ type TelegramDeliveryResult,
18
+ type TelegramDeliveryScope,
19
+ type TelegramDeliveryTarget,
20
+ type TelegramDeliveryView,
21
+ } from "../lib/delivery.ts";
package/api/inbound.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Public Telegram inbound API
3
- * Zones: package boundary, companion extension interop
3
+ * Zones: package boundary, extension interop
4
4
  * Exposes the stable programmatic inbound handler surface while keeping handler runtime internals package-private
5
5
  */
6
6
 
package/api/keyboard.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Public Telegram keyboard API
3
- * Zones: package boundary, companion extension interop
3
+ * Zones: package boundary, extension interop
4
4
  * Exposes shared inline-keyboard structural types without runtime exports
5
5
  */
6
6
 
package/api/outbound.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Public Telegram outbound API
3
- * Zones: package boundary, companion extension interop
3
+ * Zones: package boundary, extension interop
4
4
  * Exposes stable outbound handler and diagnostics surfaces while keeping delivery internals package-private
5
5
  */
6
6
 
package/api/sections.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Public Telegram sections API
3
- * Zones: package boundary, companion extension interop
3
+ * Zones: package boundary, extension interop
4
4
  * Exposes the stable managed Telegram menu-section surface while keeping registry internals package-private
5
5
  */
6
6
 
package/api/status.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Public Telegram status API
3
- * Zones: package boundary, companion extension interop
4
- * Exposes compact status-menu line registration for companion extensions while keeping status rendering internals package-private
3
+ * Zones: package boundary, extension interop
4
+ * Exposes compact status-menu line registration for extension consumers while keeping status rendering internals package-private
5
5
  */
6
6
 
7
7
  export {
package/api/updates.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Public Telegram updates API
3
- * Zones: package boundary, companion extension interop
3
+ * Zones: package boundary, extension interop
4
4
  * Exposes the stable raw-update handler surface while keeping update routing internals package-private
5
5
  */
6
6
 
package/api/voice.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Public Telegram voice API
3
- * Zones: package boundary, companion extension interop
3
+ * Zones: package boundary, extension interop
4
4
  * Exposes the stable STT/TTS provider surface and voice policy helpers
5
5
  */
6
6
 
package/docs/README.md CHANGED
@@ -8,6 +8,8 @@ Living index of project documentation in `/docs`.
8
8
 
9
9
  - [architecture.md](./architecture.md) — Overview of the Telegram bridge runtime, optional profile isolation, queueing model, native Rich Markdown delivery, UI/compat rendering, and interactive controls
10
10
  - [public-api.md](./public-api.md) — Stable public API map: package entrypoints, commands, config, assistant markup, extension APIs, smoke examples, and compatibility boundaries
11
+ - [delivery.md](./delivery.md) — Target-aware companion delivery contract for operational views, logical message handles, target scopes, lifecycle fencing, and leader/follower transport
12
+ - [activity.md](./activity.md) — Normalized Pi lifecycle API for extension-owned reasoning, intermediate prose, tool activity, source identity, delivery contexts, and consumer policy examples
11
13
  - [../.agents/skills/telegram-bot/SKILL.md](../.agents/skills/telegram-bot/SKILL.md) — Agent-facing Telegram Bot API lookup skill backed by a local full Bot API reference
12
14
  - [../.agents/skills/domain-dag/SKILL.md](../.agents/skills/domain-dag/SKILL.md) — Project-local Domain DAG architecture skill and validator guidance
13
15
  - [command-templates.md](./command-templates.md) — Portable command-template standard core
@@ -0,0 +1,294 @@
1
+ # Telegram Activity API
2
+
3
+ ## Purpose
4
+
5
+ The Telegram Activity API lets trusted extension consumers observe normalized Pi work lifecycle without importing pi-telegram internals, correlating raw Pi events with bridge state, or capturing session contexts.
6
+
7
+ It is a higher-level event surface over the [Telegram Delivery API](./delivery.md). Activity owns lifecycle normalization and event routing; delivery owns target authorization, rendering transport, ordering, chunk reconciliation, and stale-generation behavior.
8
+
9
+ The public membrane is:
10
+
11
+ ```ts
12
+ import {
13
+ registerTelegramActivityHandler,
14
+ } from "@llblab/pi-telegram/activity";
15
+ ```
16
+
17
+ An issue #126 consumer can own optional Settings policy for reasoning, intermediate assistant prose, and tool rows. Those visibility choices do not become mandatory bridge-core settings.
18
+
19
+ ## Ownership Boundary
20
+
21
+ Consumer extension code owns:
22
+
23
+ - Which activity event classes are visible.
24
+ - Settings state and presentation policy.
25
+ - Rendering event payloads into operational views.
26
+ - Coalescing or replacing its own visible activity documents.
27
+ - Redacting additional domain-specific data before display.
28
+
29
+ pi-telegram owns:
30
+
31
+ - Mapping Pi lifecycle events into one normalized activity stream.
32
+ - Correlating events with the current Telegram turn or Pi instance.
33
+ - Stable activity/run identity within one runtime generation.
34
+ - Default delivery scope selection.
35
+ - Handler registration, ordering, isolation, disposal, and diagnostics.
36
+ - Avoiding duplicate provider tool-call versus executed-tool events.
37
+ - Session shutdown fencing and fresh delivery contexts.
38
+
39
+ Pi owns the source lifecycle and provider event protocol. The Activity API requires Pi `0.80.6` or newer because `agent_settled` is the terminal boundary that distinguishes a fully settled activity from automatic retry, compaction, and queued continuation. The API does not invent reasoning when a provider does not expose it.
40
+
41
+ ## Registration
42
+
43
+ ```ts
44
+ export interface TelegramActivityHandlerRegistration {
45
+ id: string;
46
+ order?: number;
47
+ handle: (
48
+ event: TelegramActivityEvent,
49
+ ctx: TelegramActivityContext,
50
+ ) => void | Promise<void>;
51
+ }
52
+
53
+ export function registerTelegramActivityHandler(
54
+ registration: TelegramActivityHandlerRegistration,
55
+ ): () => void;
56
+ ```
57
+
58
+ Rules:
59
+
60
+ - `id` is a stable consumer identity, normally derived from package identity plus a local activity suffix.
61
+ - Duplicate active ids are rejected rather than silently stacking duplicate Telegram output.
62
+ - Handlers run by `order`, then `id`.
63
+ - Registration returns a stale-safe disposer that removes only its own registration.
64
+ - Consumers register on `session_start` and dispose on `session_shutdown`.
65
+ - The bridge creates a fresh dispatcher generation on every `session_start`; shutdown stops and clears only the retiring generation, so same-process session replacement resumes delivery instead of leaving Activity permanently stopped.
66
+ - Handler failures are isolated and recorded in `/telegram-status`; they never break Pi lifecycle or other handlers.
67
+
68
+ ## Activity Identity And Source
69
+
70
+ ```ts
71
+ export type TelegramActivitySource =
72
+ | "telegram"
73
+ | "local"
74
+ | "autonomous"
75
+ | "unknown";
76
+
77
+ export type TelegramActivityTarget = Readonly<TelegramDeliveryTarget>;
78
+
79
+ export interface TelegramActivityEnvelope {
80
+ activityId: string;
81
+ sequence: number;
82
+ source: TelegramActivitySource;
83
+ target?: TelegramActivityTarget;
84
+ timestamp: number;
85
+ }
86
+ ```
87
+
88
+ `activityId` identifies one logical settled-work sequence inside the current delivery generation. `sequence` increases monotonically within that activity and lets consumers reject stale asynchronous rendering. Telegram-owned activities carry a frozen `target` captured when the activity starts; it contains only safe chat/thread identity, never a Telegram client or Pi context.
89
+
90
+ Source classification follows evidence, not guesses:
91
+
92
+ - `telegram`: a dispatched active Telegram turn exists.
93
+ - `local`: the initiating Pi input event reports `interactive` or `rpc` and no Telegram turn owns the run.
94
+ - `autonomous`: the initiating input reports `extension` and no Telegram turn owns the run.
95
+ - `unknown`: retries, restored continuations, or runtimes without enough input evidence cannot be classified safely.
96
+
97
+ Automatic retries, overflow compaction retries, and tool continuations inherit the current activity identity/source until `agent_settled`. A new unrelated `agent_start` after settlement allocates a new activity id.
98
+
99
+ A standalone compaction owns a temporary activity only until `session_compact`. Pi does not expose a sibling-extension cancellation/failure callback after `session_before_compact`, so pi-telegram abandons that temporary identity at the first provable fallback boundary: the next `agent_start`, a replacement compaction start, session shutdown, or the existing five-minute compaction-observer timeout. A late `session_compact` after abandonment is ignored and cannot attach to the next run. Compaction inside an existing agent activity never clears that agent's identity.
100
+
101
+ ## Event Contract
102
+
103
+ ```ts
104
+ export type TelegramActivityEvent = TelegramActivityEnvelope & (
105
+ | { type: "agent-start" }
106
+ | {
107
+ type: "assistant-text-delta";
108
+ contentIndex: number;
109
+ delta: string;
110
+ }
111
+ | {
112
+ type: "assistant-segment";
113
+ contentIndex: number;
114
+ text: string;
115
+ placement: "intermediate" | "final" | "terminal-partial";
116
+ }
117
+ | {
118
+ type: "reasoning-delta";
119
+ contentIndex: number;
120
+ delta: string;
121
+ }
122
+ | {
123
+ type: "reasoning-end";
124
+ contentIndex: number;
125
+ text: string;
126
+ }
127
+ | {
128
+ type: "tool-start";
129
+ toolCallId: string;
130
+ toolName: string;
131
+ args: unknown;
132
+ }
133
+ | {
134
+ type: "tool-update";
135
+ toolCallId: string;
136
+ toolName: string;
137
+ update: unknown;
138
+ }
139
+ | {
140
+ type: "tool-end";
141
+ toolCallId: string;
142
+ toolName: string;
143
+ result: unknown;
144
+ isError: boolean;
145
+ }
146
+ | {
147
+ type: "compaction-start";
148
+ reason: "manual" | "threshold" | "overflow" | "unknown";
149
+ }
150
+ | {
151
+ type: "compaction-end";
152
+ reason: "manual" | "threshold" | "overflow" | "unknown";
153
+ }
154
+ | { type: "agent-end" }
155
+ | { type: "agent-settled" }
156
+ );
157
+ ```
158
+
159
+ ### Assistant segment classification
160
+
161
+ Provider stream events expose `text_start`, `text_delta`, and `text_end`, but `text_end` alone does not prove whether prose is intermediate or final. The normalizer therefore holds a completed text segment briefly until the next provider boundary proves placement:
162
+
163
+ - A following `toolcall_start` classifies the pending text as `intermediate`.
164
+ - A following successful `done` classifies it as `final`.
165
+ - A following `error` classifies it as `terminal-partial`.
166
+ - A new text block flushes any older still-pending segment as `intermediate` only when the provider event order proves another block follows.
167
+
168
+ `assistant-text-delta` remains available for extensions that want progressive rendering. Consumers that only want complete intermediate prose should ignore deltas and render `assistant-segment` where `placement === "intermediate"`.
169
+
170
+ ### Reasoning
171
+
172
+ Pi provider events use `thinking_*`; the public product term is `reasoning`. The normalizer maps `thinking_delta` to `reasoning-delta` and `thinking_end` to `reasoning-end`.
173
+
174
+ - Signed/redacted provider metadata is not exposed as display text.
175
+ - Empty reasoning blocks are skipped.
176
+ - Providers that hide reasoning produce no reasoning events.
177
+ - Consumers must treat reasoning as potentially sensitive and disabled by default.
178
+
179
+ ### Tools
180
+
181
+ Tool activity uses Pi's executed-tool lifecycle (`tool_execution_start/update/end`), not provider `toolcall_*` payloads. Provider tool-call boundaries are used only to classify preceding assistant prose. This prevents duplicate tool rows and reports actual execution results.
182
+
183
+ `args`, `update`, and `result` may contain paths, source text, command output, or other sensitive data. They are available to trusted local extension code but must not be rendered wholesale by default. Reference UI should summarize tool name/state and expose bounded details only through explicit policy.
184
+
185
+ ## Delivery Context
186
+
187
+ ```ts
188
+ export interface TelegramActivityContext {
189
+ activityId: string;
190
+ sequence: number;
191
+ source: TelegramActivitySource;
192
+ defaultScope: TelegramDeliveryScope;
193
+ send(
194
+ view: TelegramDeliveryView,
195
+ options?: {
196
+ scope?: TelegramDeliveryScope;
197
+ replyToMessageId?: number;
198
+ },
199
+ ): Promise<TelegramDeliveryResult<TelegramDeliveryHandle>>;
200
+ edit(
201
+ handle: TelegramDeliveryHandle,
202
+ view: TelegramDeliveryView,
203
+ ): Promise<TelegramDeliveryResult<TelegramDeliveryHandle>>;
204
+ delete(
205
+ handle: TelegramDeliveryHandle,
206
+ ): Promise<TelegramDeliveryResult<void>>;
207
+ chatAction(
208
+ action: TelegramDeliveryChatAction,
209
+ options?: { scope?: TelegramDeliveryScope },
210
+ ): Promise<TelegramDeliveryResult<void>>;
211
+ }
212
+ ```
213
+
214
+ Default scope:
215
+
216
+ - `telegram` uses `{ kind: "target", target: event.target }`, binding delayed handlers to the immutable originating thread rather than whichever turn happens to be active later.
217
+ - `local`, `autonomous`, and `unknown` use `{ kind: "instance" }`.
218
+
219
+ Handlers may explicitly choose aggregate or another authorized scope. Context methods delegate to the public delivery runtime on every call; they do not retain Telegram clients, Pi contexts, or transport ownership objects. The Delivery API rechecks authorization when an operation runs, so a captured target that is no longer owned fails closed instead of rerouting.
220
+
221
+ A handler may still receive events while no target is currently deliverable. Delivery then returns the normal structured `target-unavailable`, `target-unauthorized`, or `runtime-unavailable` result.
222
+
223
+ ## Dispatch And Backpressure
224
+
225
+ Pi lifecycle must not wait for extension rendering or Telegram transport.
226
+
227
+ - Lifecycle hooks normalize and enqueue activity events synchronously, then return.
228
+ - Each handler owns a serialized asynchronous queue so its event order is stable without blocking other handlers.
229
+ - Handler queues and contexts are generation-bound. `session_shutdown` stops queued events immediately, and an already-running handler receives `runtime-unavailable` instead of delivering through a replacement session after it resumes.
230
+ - Disposing a handler also fences contexts captured from that registration; they cannot outlive their ownership and adopt another registration with the same id.
231
+ - High-frequency `assistant-text-delta`, `reasoning-delta`, and `tool-update` events may coalesce only with the immediately adjacent event of the same type, activity id, content/tool id, and handler queue.
232
+ - Boundary events (`assistant-segment`, `reasoning-end`, tool start/end, compaction, agent end/settled) are never dropped or reordered.
233
+ - The 0.21 dispatcher does not impose an event-count drop policy or emit queue-length diagnostics. Adjacent delta/update coalescing reduces common streaming pressure, but boundary events accumulate behind a slow handler; consumers must keep handler work bounded and delegate long-lived work outside the callback when appropriate.
234
+
235
+ The Delivery API independently serializes concrete Telegram operations per target. Activity serialization preserves semantic event order; delivery serialization preserves transport order.
236
+
237
+ ## Lifecycle Mapping
238
+
239
+ The bridge maps Pi hooks as follows:
240
+
241
+ - `input`: capture source evidence for the next logical activity.
242
+ - `agent_start`: allocate or reuse activity identity and emit `agent-start` after Telegram queue consumption establishes active-turn ownership.
243
+ - `message_update.assistantMessageEvent`: normalize text/reasoning/provider boundaries.
244
+ - `tool_execution_start/update/end`: emit executed tool events.
245
+ - `session_before_compact` / `session_compact`: emit compaction boundaries and preserve activity identity across retry compaction; abandon an unterminated standalone compaction at the next lifecycle boundary or observer timeout rather than merging it into another run. A missing or unrecognized reason maps to `unknown` rather than guessing.
246
+ - `agent_end`: emit low-level run completion but keep identity alive for retry/follow-up work.
247
+ - `agent_settled`: emit terminal settlement, flush pending terminal segments, and release activity identity.
248
+ - `session_shutdown`: stop dispatch, clear pending normalization state, and invalidate delivery generation through the existing delivery lifecycle.
249
+
250
+ ## Diagnostics
251
+
252
+ Duplicate registration fails synchronously. When a handler throws or rejects, the dispatcher forwards only the handler id, event type, activity id, and error to pi-telegram's existing bounded/redacted runtime event recorder. The bridge does not copy reasoning, assistant prose, tool arguments/results, Telegram payloads, or queue contents into diagnostic metadata. It does not currently record queue phase, queue length, coalescing counts, or handler latency.
253
+
254
+ The first implementation has no public Activity diagnostics getter because handler failures already flow through bridge runtime diagnostics and `/telegram-status`. Add queue telemetry or a dedicated getter only when an observed consumer needs inspectable backpressure state.
255
+
256
+ ## Security And Non-Goals
257
+
258
+ The Activity API does not:
259
+
260
+ - Enable reasoning visibility by default.
261
+ - Guarantee reasoning availability across providers.
262
+ - Expose signed/redacted reasoning metadata.
263
+ - Render raw tool arguments or results automatically.
264
+ - Replace assistant final replies or Rich Draft previews.
265
+ - Mutate queues, models, thinking levels, sessions, or process state.
266
+ - Expose Telegram clients, bot tokens, Pi contexts, or private runtime objects.
267
+ - Block Pi lifecycle on extension handlers or Telegram delivery.
268
+
269
+ ## Validation Contract
270
+
271
+ The implementation must cover:
272
+
273
+ - Stable id registration, ordering, duplicate rejection, and stale-safe disposal.
274
+ - Telegram, local, autonomous, and unknown source evidence.
275
+ - Identity reuse across retry/compaction and reset at settlement.
276
+ - Text delta flow and intermediate/final/terminal segment classification.
277
+ - Reasoning mapping and empty/hidden reasoning behavior.
278
+ - Executed tool start/update/end without provider tool-call duplication.
279
+ - Default active-turn versus instance delivery scopes.
280
+ - Adjacent delta/update coalescing and boundary preservation.
281
+ - Handler error isolation, non-blocking lifecycle, shutdown fencing, and redacted diagnostics.
282
+ - Public package import without `/lib` reach-through.
283
+
284
+ ## Consumer Policy Pattern
285
+
286
+ The registration and delivery examples above provide the complete public building blocks for issue #126-style visibility policy:
287
+
288
+ - Reasoning can default off and send completed `reasoning-end` blocks only when enabled.
289
+ - Intermediate prose can default off and send only `assistant-segment` events with `placement: "intermediate"`, never the final assistant segment.
290
+ - Tool rows can default on, show only tool name/state, and edit generation-bound logical handles from running to done/failed without exposing arguments or results.
291
+ - Interactive toggles belong in a registered Section and Settings row; activity messages can remain non-interactive.
292
+ - `session_shutdown` should dispose stable registrations and drop retained delivery handles, so reload/session replacement cannot reuse old contexts or handles.
293
+
294
+ The separate [`pi-telegram-extension-demo`](https://github.com/llblab/pi-telegram-extension-demo) project remains the maintained companion-extension and managed-UI reference. This document owns the Activity-specific usage pattern; pi-telegram does not ship a redundant `examples/` package directory.