@llblab/pi-telegram 0.26.16 → 0.27.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -88,8 +88,8 @@ The core product loop is mobile continuation: start or supervise work in the ter
88
88
  - Instance slots are extension-owned ring-order metadata. Allocate fresh slots from the `bot.lastSlot` cursor as `A` → `B` → ... → `Z` → `A`, allowing the cursor to move to lower letters after wraparound. Only current live/recovering bindings plus unexpired pending provisions and explicit reservations occupy slots; historical records must not block allocation indefinitely. Preserve a slot on ordinary current binding/thread reuse without advancing or rewinding the fresh-allocation cursor. Follower bindings survive process absence as stable restoration hints unless enabled Thread cleanup turns stale heartbeat plus exact-PID death confirmation into fenced cleanup authority; otherwise only explicit stale/deleted/offline evidence invalidates them. Successful target reuse must refresh the binding timestamp and replacement runtime identity; generic leader startup or heartbeat pruning alone must not erase the target merely because no follower registration is currently live. An authenticated live follower whose carried exact target is missing from current persisted bindings should recover that target instead of creating another Telegram thread; preserve a carried slot only when it does not collide with another current record. Preserve a cursor that belongs to a live or recoverable binding and continue skipping retained restart hints plus unexpired pending/reserved slots. Explicit stale/deleted/offline reconciliation may release that slot; generic process absence may not. The alphabet cap is a feature — compact single-letter tabs are scannable.
89
89
  - 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.
90
90
  - Telegram private-chat Threaded Mode treats `All` as an aggregate/control surface, not a durable `General` thread or process launcher, and leader activation proactively creates or reuses its private-DM thread. When the owner writes in an unknown thread, the leader reclaims and routes the first one only if it lacks an active binding; later unknown threads remain intact and receive an in-thread chooser to reroute the captured prompt or restore a stale binding. Destructive cleanup requires explicit confirmation through `thread-reconciler`; another Pi instance still requires a manually started process followed by `/telegram-connect`. Without Threaded Mode, process the message through classic routing.
91
- - 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. Treat a low-level error `agent_end` as retryable while its Telegram turn remains active; only a later successful `agent_end` or Pi's `agent_settled` boundary may finalize and release that turn. Public activity handlers run through isolated asynchronous per-handler queues; lifecycle hooks enqueue normalized events and never await consumer work. Proactive push defaults enabled and requires explicit `assistant.proactivePush: false` to opt out. While enabled, core delivery projects each completed Pi-visible assistant text block from local/autonomous work once and in order; bind admitted blocks to the exact target, profile/token transport stamp, direct leader epoch or follower registration generation, and session generation. Token deltas, hidden reasoning, tool traffic, stale authority, and Telegram-owned turns never enter that projection.
92
- - Core thinking/tool visibility is controlled by file-backed `assistant.activity: "quiet" | "thinking" | "tools" | "verbose"`; every Pi instance reloads the shared config at `agent-start` before admitting activity so process-local cache cannot broaden the selected class. It defaults to `verbose` when absent and remains separate from proactive public prose and assistant Markdown: provider-exposed thinking uses only a durable ordinary HTML expandable blockquote, while completed executed tools use native Rich Messages whose closed `<Tool>: <status>` root details preserve sentence case while uppercasing an initial two- or three-letter repeated prefix, then contain an open-by-default `arguments` child plus closed `update`, `result`, or `error` children with lowercase monospaced labels and JSON pre blocks; treat each label as one quote-free outer JSON key and add no icon, list marker, or heading emphasis. Thinking renders inline Markdown as Telegram HTML without spending a separate header row; `verbose` enables both classes, and every activity send/edit disables Telegram link previews while technical evidence also neutralizes HTTP(S) auto-link detection as a client-safe fallback. Keep it compact by default, exact-target/transport-generation fenced, start-order preserving, boundary-aware, memory/transport bounded, and non-blocking; final delivery waits for admitted technical activity only inside the extension-owned background task, and session replacement abandons queued old-generation work without waiting on old transport. Read legacy `assistant.activityVerbosity` only when `assistant.activity` is absent and remove it on the next Settings write. Never invent unavailable provider-private thinking or replay a possibly committed technical message.
91
+ - 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. Treat a low-level error `agent_end` as retryable while its Telegram turn remains active; only a later successful `agent_end` or Pi's `agent_settled` boundary may finalize and release that turn. Public activity handlers run through isolated asynchronous per-handler queues; lifecycle hooks enqueue normalized events and never await consumer work. Core assistant-output projection always delivers each completed `intermediate` Telegram-originated text segment once to its immutable turn target before final delivery; final and terminal-partial segments remain with settlement to prevent duplication. Proactive push defaults enabled for local/autonomous work and requires explicit `assistant.proactivePush: false` to opt out of only that source class. Bind every admitted block to the exact target, profile/token transport stamp, direct leader epoch or follower registration generation, and session generation. Token deltas, hidden reasoning, tool traffic, unknown sources, and stale authority never enter projection.
92
+ - Core thinking/tool visibility is controlled by file-backed `assistant.activity: "quiet" | "thinking" | "tools" | "verbose"`; every Pi instance reloads the shared config at `agent-start` before admitting activity so process-local cache cannot broaden the selected class. It defaults to `verbose` when absent and remains separate from proactive public prose and assistant Markdown: provider-exposed thinking uses only a durable ordinary HTML expandable blockquote, while completed executed tools use native Rich Messages whose closed `<Tool>: <status>` root details replace snake-case separators with spaces, title-case each word, and preserve an uppercase initial two- or three-letter repeated prefix per word, then contain an open-by-default `arguments` child plus closed `update`, `result`, or `error` children with lowercase monospaced labels and JSON pre blocks; treat each label as one quote-free outer JSON key and add no icon, list marker, or heading emphasis. Thinking renders inline Markdown as Telegram HTML without spending a separate header row; `verbose` enables both classes, and every activity send/edit disables Telegram link previews while technical evidence also neutralizes HTTP(S) auto-link detection as a client-safe fallback. Keep it compact by default, exact-target/transport-generation fenced, start-order preserving, boundary-aware, memory/transport bounded, and non-blocking; final delivery waits for admitted technical activity only inside the extension-owned background task, and session replacement abandons queued old-generation work without waiting on old transport. Read legacy `assistant.activityVerbosity` only when `assistant.activity` is absent and remove it on the next Settings write. Never invent unavailable provider-private thinking or replay a possibly committed technical message.
93
93
  - 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.
94
94
  - 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 or command-created temporary threads receive the complete forward plus replace/restore control set and remain preserved until the user chooses one. Successful forward deletes the chooser and temporary thread; successful replace/restore always deletes the chooser, rebinds the temporary thread, and deletes only the replaced old thread. Any destructive cleanup must go through `thread-reconciler`. Unknown `forum_topic_created` service events are observations, not destructive cleanup proof.
95
95
  - 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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.27.1: Humanized Tool Labels Hotfix
4
+
5
+ - `Tool Labels`: Tool activity roots now replace snake-case underscores with spaces and capitalize every resulting word while preserving recognizable uppercase repeated-letter prefixes per word (`telegram_attach` → `Telegram Attach`, `ff_find_items` → `FF Find Items`). Impact: Rich tool disclosures and their HTML fallback present multi-word tool names as user-facing labels instead of code identifiers.
6
+
7
+ ## 0.27.0: Telegram Commentary Delivery
8
+
9
+ - `Telegram Commentary`: Completed intermediate assistant text segments from Telegram-originated turns now arrive once as independent messages on the immutable originating target before the existing final reply. The path remains active independently of local/autonomous Proactive Push, preserves normalized segment order, rendering, authority fencing, and no-replay behavior, and leaves final/terminal settlement with the established active-turn delivery pipeline. Impact: visible checkpoints and other standalone commentary no longer disappear between thinking/tool activity and the final answer in classic or Threaded Mode.
10
+ - `Delivery Ordering`: Active-turn final delivery now waits for both admitted technical activity and assistant commentary inside its existing extension-owned background task. Impact: commentary cannot be overtaken by the final result, while Pi lifecycle completion remains non-blocking and stale session/transport generations still fail closed.
11
+
3
12
  ## 0.26.16: Follower Edit Normalization Hotfix
4
13
 
5
14
  - `Follower Edit Normalization`: The bus-routed `editMessageText` path now treats Telegram's "message is not modified" rejection as an unchanged edit instead of a failure, matching the direct transport path. Impact: reasoning activity on follower instances no longer freezes its thinking block when an edit targets identical content (for example a repeated reasoning-end flush), because the failed edit no longer marks the whole block as blocked.
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  It is a **runtime adapter**, not a remote terminal. Start or supervise work in the Pi TUI, then continue from Telegram while away from the keyboard. Each Telegram destination follows a running Pi instance and sends prompts into that instance's currently active session; it is not permanently bound to one session file or session identity. The bridge preserves Pi session semantics instead of pretending Telegram is a PTY, shell, process launcher, or session browser. That boundary is the product: Telegram gets safe runtime handles, not raw terminal power.
10
10
 
11
- Proactive push is enabled by default. `assistant.proactivePush` projects every completed public assistant text block from local or autonomous work—including visible checkpoints and the final answerto the authorized Telegram target once and in order; set it explicitly to `false` to disable projection. Proactive push itself never mirrors local prompts, thinking, tool traffic, token deltas, Telegram-owned turns, or stale-generation work. The separate `Activity` setting defaults to `verbose` so new installations discover collapsed provider-exposed thinking and tool evidence immediately; operators can narrow it to one class or choose `quiet`. See [Outbound](docs/outbound.md#proactive-public-output) and the [configuration reference](docs/public-api.md#configuration-api).
11
+ Every completed intermediate commentary block from a Telegram-originated turn is delivered once as its own message before the existing final reply. Proactive push is enabled by default for local and autonomous work: `assistant.proactivePush` projects visible checkpoints and the final answer to the authorized Telegram target once and in order; set it explicitly to `false` to disable only that local/autonomous projection. Neither path mirrors local prompts, thinking, tool traffic, token deltas, or stale-generation work. The separate `Activity` setting defaults to `verbose` so new installations discover collapsed provider-exposed thinking and tool evidence immediately; operators can narrow it to one class or choose `quiet`. See [Outbound](docs/outbound.md#public-assistant-output) and the [configuration reference](docs/public-api.md#configuration-api).
12
12
 
13
13
  This repository is an actively maintained fork of [`badlogic/pi-telegram`](https://github.com/badlogic/pi-telegram). It started from upstream commit [`cb34008`](https://github.com/badlogic/pi-telegram/commit/cb34008460b6c1ca036d92322f69d87f626be0fc) and has since diverged substantially.
14
14
 
package/docs/activity.md CHANGED
@@ -234,11 +234,11 @@ Pi lifecycle must not wait for extension rendering or Telegram transport.
234
234
 
235
235
  The Delivery API independently serializes concrete Telegram operations per target. Activity serialization preserves semantic event order; delivery serialization preserves transport order.
236
236
 
237
- ### Core proactive projection
237
+ ### Core assistant-output projection
238
238
 
239
- When `assistant.proactivePush` is enabled, Activity's built-in assistant-output projection uses the same normalized `assistant-segment` boundary exposed to public handlers. It projects every completed public block from `local` or `autonomous` activity, including intermediate commentary/checkpoints and the final block. It never projects text token deltas, reasoning events, tool events or payloads, Telegram-owned activity, unknown-source activity, or empty text.
239
+ Activity's built-in assistant-output projection uses the same normalized `assistant-segment` boundary exposed to public handlers. For `telegram` activity it always projects complete `intermediate` commentary to the immutable originating target, while final and terminal-partial segments remain with active-turn settlement. For `local` or `autonomous` activity, `assistant.proactivePush` enables projection of every completed public block, including intermediate commentary/checkpoints and the final block. It never projects text token deltas, reasoning events, tool events or payloads, unknown-source activity, or empty text.
240
240
 
241
- The projection does not delay Activity dispatch or Pi lifecycle. Its ordered admission tail deduplicates normalized event identity, while existing routing and outbound owners revalidate the immutable admission-time target, profile/token transport generation, direct leader epoch or follower registration generation, and session generation immediately before each send. A replacement or stale owner drops queued work rather than rerouting it. An already-started non-idempotent Bot API mutation follows the normal `commit-unknown` no-replay contract.
241
+ The projection does not delay Activity dispatch or Pi lifecycle. Its ordered admission tail deduplicates normalized event identity, while existing routing and outbound owners revalidate the immutable admission-time target, profile/token transport generation, direct leader epoch or follower registration generation, and session generation immediately before each send. Active-turn final delivery waits for admitted commentary inside its existing background task. A replacement or stale owner drops queued work rather than rerouting it, and an already-started non-idempotent Bot API mutation follows the normal `commit-unknown` no-replay contract.
242
242
 
243
243
  This projection consumes ordinary normalized Pi output. It has no runtime dependency on any workflow, loop, or companion extension that produced the public blocks.
244
244
 
@@ -313,9 +313,9 @@ Queue reactions are shortcut controls for waiting turns. Promotion reactions (`
313
313
 
314
314
  `/telegram-status` records grouped diagnostics for transport/API, polling/update, prompt dispatch, controls, typing, compaction, setup, session lifecycle, attachment queue/delivery, and recent redacted runtime events. Expected preview noise such as unchanged edit responses is filtered out. The compact TUI status renders only `error`; detailed failure text remains in diagnostics and profile-scoped logs instead of expanding the status line.
315
315
 
316
- When `assistant.proactivePush` is enabled and this instance has exact direct or follower transport authority, completed public assistant text blocks from local/autonomous work are sent once and in source order to the instance's authorized target. Visible commentary/checkpoints and the final block use the configured Rich or HTML renderer. Proactive projection excludes reasoning, tool traffic, token deltas, local prompt text, Telegram-owned turns, and stale generations. Each admitted block remains fenced to its exact target, profile/token stamp, leader epoch or follower registration generation, and session generation; non-idempotent acknowledgement ambiguity never authorizes replay.
316
+ Complete intermediate assistant text blocks from Telegram-originated activity are sent once to the immutable originating target before active-turn final delivery; final and terminal-partial segments stay with settlement so replies are not duplicated. When `assistant.proactivePush` is enabled and this instance has exact direct or follower transport authority, completed public blocks from local/autonomous work are also sent once and in source order to the instance's authorized target. Both paths use the configured Rich or HTML renderer and exclude reasoning, tool traffic, token deltas, local prompt text, unknown sources, and stale generations. Each admitted block remains fenced to its exact target, profile/token stamp, leader epoch or follower registration generation, and session generation; non-idempotent acknowledgement ambiguity never authorizes replay.
317
317
 
318
- `assistant.activity` is an independent bridge-owned projection over normalized Activity events. Each process reloads the shared file-backed setting at `agent-start` before activity admission, so multi-instance mode cannot continue projecting a stale broader process-local selection. Omitted values resolve to `verbose`, while invalid values fail closed to `quiet`; `thinking` and `tools` select one technical class, while `verbose` enables both. Provider-exposed thinking uses persistent ordinary HTML containing only a standard expandable blockquote with a bounded redacted latest-text window and inline Markdown rendered as Telegram HTML. Completed executed tools use native Rich Messages: each closed `<Tool>: <status>` root details node uses sentence case with a preserved uppercase two- or three-letter repeated prefix, then the native disclosure chevron reveals an open-by-default `arguments` child plus closed retained `update N` and `result`/`error` child details with lowercase monospaced, marker-free summaries and JSON pre blocks; known-safe Rich rejections fall back to the previous HTML disclosure. The projection captures the exact target and transport stamp at activity admission, serializes updates, preserves tool-start order, closes coalescing across assistant/thinking boundaries, bounds retained text/update memory plus edit frames and message/tool size, disables previews and HTTP(S) auto-link recognition inside technical evidence, and never replays a possibly committed send. Session generations own independent queues, so replacement drops queued old work without waiting on an old call. Both proactive prose and active-turn final delivery wait for the admitted activity queue inside their extension-owned delivery tasks, preserving technical-before-semantic ordering without delaying Pi lifecycle completion. Settlement, replacement, disconnect, failure, or stale authority clears only local ownership; already-sent activity messages remain in chat.
318
+ `assistant.activity` is an independent bridge-owned projection over normalized Activity events. Each process reloads the shared file-backed setting at `agent-start` before activity admission, so multi-instance mode cannot continue projecting a stale broader process-local selection. Omitted values resolve to `verbose`, while invalid values fail closed to `quiet`; `thinking` and `tools` select one technical class, while `verbose` enables both. Provider-exposed thinking uses persistent ordinary HTML containing only a standard expandable blockquote with a bounded redacted latest-text window and inline Markdown rendered as Telegram HTML. Completed executed tools use native Rich Messages: each closed `<Tool>: <status>` root details node renders snake-case names as title words while preserving an uppercase two- or three-letter repeated prefix per word, then the native disclosure chevron reveals an open-by-default `arguments` child plus closed retained `update N` and `result`/`error` child details with lowercase monospaced, marker-free summaries and JSON pre blocks; known-safe Rich rejections fall back to the previous HTML disclosure. The projection captures the exact target and transport stamp at activity admission, serializes updates, preserves tool-start order, closes coalescing across assistant/thinking boundaries, bounds retained text/update memory plus edit frames and message/tool size, disables previews and HTTP(S) auto-link recognition inside technical evidence, and never replays a possibly committed send. Session generations own independent queues, so replacement drops queued old work without waiting on an old call. Both proactive prose and active-turn final delivery wait for the admitted activity queue inside their extension-owned delivery tasks, preserving technical-before-semantic ordering without delaying Pi lifecycle completion. Settlement, replacement, disconnect, failure, or stale authority clears only local ownership; already-sent activity messages remain in chat.
319
319
 
320
320
  Telegram prompt guidance is context- and authority-aware. Only an exact direct owner or live registered follower exposes the three pi-telegram model tools, their active-tool metadata, and the compact local bridge suffix. Disconnect or authority loss removes those surfaces for subsequent requests without touching foreign tools; reconnect/recovery restores only the pi-telegram subset that was active before suspension, including across same-process reload. Telegram-originated turns receive a compact pointer to `telegram_help` plus dynamic prompt blocks such as `[voice] delivery: automatic voice`; full voice/button/direct-delivery/Threaded Mode syntax stays in the help tool rather than every system prompt.
321
321
 
package/docs/outbound.md CHANGED
@@ -6,18 +6,20 @@ Normal Telegram-turn replies are intentionally prompt-driven: the agent writes M
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
 
9
- ## Proactive Public Output
9
+ ## Public Assistant Output
10
10
 
11
- Proactive projection defaults on. With `assistant.proactivePush` omitted or set to `true`, completed public assistant text blocks from local or autonomous Pi work are projected to the instance's authorized Telegram target; set it explicitly to `false` to opt out. A visible intermediate commentary/checkpoint and the final answer become separate Telegram messages in source order; this is not a final-only `agent_end` notification. The bridge consumes normalized Activity `assistant-segment` events, not raw token deltas, reasoning, or tool traffic.
11
+ Every completed `assistant-segment` with `placement: "intermediate"` from a Telegram-originated turn is delivered as its own message to the immutable originating target before the ordinary active-turn final reply. This commentary path always applies; `assistant.proactivePush` does not disable it. Final and terminal-partial Telegram segments remain owned by active-turn settlement so the final answer, voice, buttons, previews, and artifacts are not duplicated.
12
12
 
13
- Proactive blocks use `assistant.rendering` independently of voice policy. Rich mode sends native Rich Markdown and HTML mode keeps the established HTML renderer; proactive projection does not synthesize voice or attach queued files merely because Rich rendering is active. The queue revalidates exact target, profile/token transport generation, leader epoch or follower registration generation, and session generation before each send. Telegram-owned turns remain on their ordinary reply path, and `commit-unknown` never permits proactive replay.
13
+ Proactive projection defaults on for local and autonomous Pi work. With `assistant.proactivePush` omitted or set to `true`, every completed public block—including visible commentary/checkpoints and the final answer—is projected to the instance's authorized target in source order; set it explicitly to `false` to opt out of only this local/autonomous projection. Both paths consume normalized complete Activity segments rather than raw token deltas, reasoning, or tool traffic.
14
+
15
+ Projected blocks use `assistant.rendering` independently of voice policy. Rich mode sends native Rich Markdown and HTML mode keeps the established HTML renderer; projection does not synthesize voice or attach queued files merely because Rich rendering is active. Ordered admission revalidates the exact target, profile/token transport generation, leader epoch or follower registration generation, and session generation before each send. Active-turn final delivery waits for admitted commentary inside its existing background delivery task, preserving commentary-before-final order without blocking Pi lifecycle completion. A `commit-unknown` outcome never permits replay.
14
16
 
15
17
  ## Technical Activity
16
18
 
17
19
  `assistant.activity` defaults to `verbose` when absent and accepts four modes: `quiet`, `thinking`, `tools`, and `verbose`. Explicit stored values remain unchanged; invalid values fail closed to `quiet`. Every Pi instance reloads this shared file-backed mode at `agent-start`, so a setting changed from one thread applies to subsequent runs in the other live instances without requiring process reload. `thinking` shows only provider-exposed thinking, `tools` shows only completed executed tools, and `verbose` shows both without mixing technical UI into assistant Markdown.
18
20
 
19
21
  - Provider-exposed thinking updates one persistent ordinary HTML message containing only a standard Telegram `<blockquote expandable>` with a bounded redacted latest-text window. Omitting a separate icon/level header saves one chat row while the disclosure's unique shape remains recognizable. Inline Markdown emphasis and code render as Telegram HTML instead of leaking raw markers. The bridge sends the message once, edits it only as thinking grows, and leaves the disclosure in chat; it never uses Rich drafts/Rich Messages. Providers that expose no thinking produce none.
20
- - Completed executed tools use native `sendRichMessage` block objects. Each tool is one closed root details node summarized as bold `<Tool>:` plus monospaced `<status>`; root labels remain sentence-case except that a leading two- or three-character repeated-letter prefix stays uppercase (`fffind` → `FFFind`). The native disclosure chevron identifies the row, and opening it immediately reveals the open `arguments` child plus separate closed retained `update N`, `result`, or `error` child details. Each child summary is one lowercase monospaced label—visually a quote-free outer JSON key with no icon, list marker, or heading emphasis—and contains one preformatted `json` block. Updates remain chronological, dropped-update counts appear in the first retained update summary, and arrays of object entries keep the denser `[{ ... }, { ... }]` layout. A known-safe Rich HTTP 400 rejection falls back once to the previous expandable HTML representation; ambiguous non-idempotent outcomes never replay.
22
+ - Completed executed tools use native `sendRichMessage` block objects. Each tool is one closed root details node summarized as bold `<Tool>:` plus monospaced `<status>`; snake-case root labels render as title words (`telegram_attach` → `Telegram Attach`), while each word preserves a leading two- or three-character repeated-letter prefix in uppercase (`ff_find` → `FF Find`). The native disclosure chevron identifies the row, and opening it immediately reveals the open `arguments` child plus separate closed retained `update N`, `result`, or `error` child details. Each child summary is one lowercase monospaced label—visually a quote-free outer JSON key with no icon, list marker, or heading emphasis—and contains one preformatted `json` block. Updates remain chronological, dropped-update counts appear in the first retained update summary, and arrays of object entries keep the denser `[{ ... }, { ... }]` layout. A known-safe Rich HTTP 400 rejection falls back once to the previous expandable HTML representation; ambiguous non-idempotent outcomes never replay.
21
23
  - Thinking `sendMessage`/`editMessageText` disables link previews and breaks HTTP(S) auto-link recognition inside evidence. Rich tool messages set `skip_entity_detection: true`, so URL-like arguments, updates, and results remain literal code. Consecutive tools coalesce by editing one message only while target, activity, generation, ordering boundary, tool count, and serialized-size bounds still match. Assistant or thinking content closes the batch. A non-idempotent send with unknown commit state is never replayed; failed or ambiguous edits start no fallback send.
22
24
 
23
25
  Thinking retains only a bounded latest-text window, tool updates retain only a bounded latest-entry window, and a session reset abandons queued old-generation work without making the replacement session wait for an old transport call. Final-answer delivery waits for the admitted activity queue inside the extension-owned background delivery task, so completed technical evidence cannot be overtaken by the semantic answer and Pi lifecycle completion remains non-blocking.
@@ -118,7 +118,7 @@ The file is global across Pi instances. Cooperating instances serialize recursiv
118
118
  Hidden/default semantics are represented by absence:
119
119
 
120
120
  - `threads.automaticCleanup` defaults to `true`; graceful Pi quit deletes the instance's bound Threaded Mode tab without prompting. Set it to `false`, or use `🧹 Thread cleanup` in Telegram Settings, to preserve the tab as a restart hint. Settings views and cleanup reload shared config before evaluating this switch, so another live Pi instance's update takes effect without restarting. Confirmed leader/follower teardown persists an exact target/runtime-generation cleanup intent before Telegram deletion; an interrupted attempt remains retryable by the current or successor leader under current authority and clears only after confirmed deletion. A same-profile replacement leader first adopts any still-active binding and cancels its superseded cleanup, so startup never deletes and recreates a reusable thread. If a follower's graceful envelope is missed, the leader may create the same fenced cleanup only after its heartbeat is stale, the OS confirms the exact registered PID no longer exists, cleanup remains enabled, and no replacement registration can overtake deletion. Heartbeat loss alone, live/unknown process liveness, IPC failure, and auth failure remain non-destructive. Invalid-config recovery makes the setting unresolved and therefore skips destructive cleanup. Manual `/telegram-disconnect` keeps its confirmation and teardown behavior regardless of this setting.
121
- - `assistant.proactivePush` defaults to `true`; omit it to keep projection enabled, or set it explicitly to `false` to disable it. When enabled, each completed public assistant text block from local or autonomous work is projected to the authorized Telegram target once and in source order. This includes visible intermediate commentary/checkpoints and the final block. It excludes token deltas, hidden reasoning, tool calls/arguments/results, Telegram-owned turns, empty blocks, and stale authority. Projection uses the configured Rich or HTML assistant renderer and binds admitted work to the exact target, profile/token transport generation, direct leader epoch or follower registration generation, and session generation. The old top-level `proactivePush` key is ignored; move the setting manually under `assistant`.
121
+ - Every complete intermediate assistant text block from a Telegram-originated turn is delivered once to its immutable target before the existing final reply. This active-turn commentary path remains enabled when `assistant.proactivePush` is `false`; final and terminal-partial segments stay with settlement to prevent duplicate replies. `assistant.proactivePush` defaults to `true` only for local/autonomous work: omit it to project every completed public block, including commentary and the final block, or set it explicitly to `false` to disable that projection. Both paths exclude token deltas, hidden reasoning, tool calls/arguments/results, empty blocks, unknown sources, and stale authority. Projection uses the configured Rich or HTML assistant renderer and binds admitted work to the exact target, profile/token transport generation, direct leader epoch or follower registration generation, and session generation. The old top-level `proactivePush` key is ignored; move the setting manually under `assistant`.
122
122
  - `assistant.activity` accepts exactly `"quiet"`, `"thinking"`, `"tools"`, or `"verbose"`; omitted values default to `"verbose"`, explicit values remain unchanged, and invalid values fail closed to `"quiet"`. Each Pi process reloads the shared file-backed value at `agent-start`, so multi-instance activity isolation never relies on a stale process-local config snapshot. `thinking` shows only provider-exposed thinking, `tools` shows only completed tool activity, and `verbose` shows both. Thinking uses persistent ordinary HTML `sendMessage`/`editMessageText` disclosure with a standard expandable blockquote, a `🧠` header carrying the current Pi thinking level, and bounded redacted text whose inline Markdown renders as Telegram HTML. Tools use native Rich Messages with one header followed by separate closed details and JSON pre blocks for bounded redacted arguments, retained updates, and results/errors. Thinking disables link previews on every HTML send/edit and neutralizes HTTP(S) auto-link detection; Rich tool output disables automatic entity detection, with the same protections retained by its HTML fallback. Consecutive tools coalesce only inside the same ordered activity segment and bounded message. Legacy `assistant.activityVerbosity` is read only when `assistant.activity` is absent and is removed by the next Activity Settings write.
123
123
  - Voice Reply `hidden`: no `voice.replyMode` key is persisted; legacy `manual` resolves to this silent default. `mirror` adds `[voice] delivery: automatic voice` only to voice/audio-input turns, while `always` adds the same effective line to every Telegram turn.
124
124
  - Agent activity status is not configurable in this release. Telegram uses native `sendChatAction(typing)` / product `...active` status as the only automatic in-chat work signal before the final reply.
@@ -168,12 +168,17 @@ function renderThinkingActivityEvidenceHtml(text: string): string {
168
168
  }
169
169
 
170
170
  function formatToolActivityLabel(label: string): string {
171
- if (label.length === 0) return label;
172
- const repeatedPrefix = label.match(/^([a-z])\1*/iu)?.[0] ?? "";
173
- if (repeatedPrefix.length === 2 || repeatedPrefix.length === 3) {
174
- return `${repeatedPrefix.toUpperCase()}${label.slice(repeatedPrefix.length)}`;
175
- }
176
- return `${label[0]!.toUpperCase()}${label.slice(1)}`;
171
+ return label
172
+ .split("_")
173
+ .filter(Boolean)
174
+ .map((word) => {
175
+ const repeatedPrefix = word.match(/^([a-z])\1*/iu)?.[0] ?? "";
176
+ if (repeatedPrefix.length === 2 || repeatedPrefix.length === 3) {
177
+ return `${repeatedPrefix.toUpperCase()}${word.slice(repeatedPrefix.length)}`;
178
+ }
179
+ return `${word[0]!.toUpperCase()}${word.slice(1)}`;
180
+ })
181
+ .join(" ");
177
182
  }
178
183
 
179
184
  function renderToolActivityHtml(tool: ToolActivity): string {
package/lib/activity.ts CHANGED
@@ -730,9 +730,10 @@ export function createTelegramAssistantOutputRuntime<TAuthority = undefined>(dep
730
730
  let running = false;
731
731
  let tail: Promise<void> = Promise.resolve();
732
732
  const admitted = new Set<string>();
733
- const isEligibleSource = (
734
- source: TelegramAssistantSegmentEvent["source"],
735
- ): boolean => source === "local" || source === "autonomous";
733
+ const isEligibleEvent = (event: TelegramAssistantSegmentEvent): boolean =>
734
+ (event.source === "telegram" && event.placement === "intermediate") ||
735
+ ((event.source === "local" || event.source === "autonomous") &&
736
+ deps.isEnabled());
736
737
 
737
738
  return {
738
739
  start() {
@@ -742,8 +743,7 @@ export function createTelegramAssistantOutputRuntime<TAuthority = undefined>(dep
742
743
  tail = Promise.resolve();
743
744
  },
744
745
  accept(event) {
745
- if (!running || !deps.isEnabled()) return;
746
- if (!isEligibleSource(event.source) || !event.text.trim()) return;
746
+ if (!running || !isEligibleEvent(event) || !event.text.trim()) return;
747
747
  const key = `${event.activityId}:${event.sequence}`;
748
748
  if (admitted.has(key)) return;
749
749
  admitted.add(key);
@@ -753,7 +753,7 @@ export function createTelegramAssistantOutputRuntime<TAuthority = undefined>(dep
753
753
  const isAdmittedAuthorityActive = () =>
754
754
  running &&
755
755
  generation === admittedGeneration &&
756
- deps.isEnabled() &&
756
+ isEligibleEvent(event) &&
757
757
  (deps.isAuthorityActive === undefined ||
758
758
  deps.isAuthorityActive(admittedAuthority as TAuthority));
759
759
  if (!isAdmittedAuthorityActive() || !deps.canDeliver(event)) return;
package/lib/bindings.ts CHANGED
@@ -287,7 +287,7 @@ interface TelegramLifecycleBindingDeps {
287
287
  activityVerbosityRuntime?: TelegramActivityVerbosityRuntime;
288
288
  assistantOutputRuntime: Pick<
289
289
  Activity.TelegramAssistantOutputRuntime,
290
- "start" | "stop"
290
+ "start" | "waitForIdle" | "stop"
291
291
  >;
292
292
  sessionLifecycleRuntime: Pick<
293
293
  Lifecycle.TelegramLifecycleRegistrationDeps,
@@ -564,7 +564,10 @@ export function registerTelegramLifecycleRuntimeHooks({
564
564
  isSessionActive: isSessionContextActive,
565
565
  isTurnTransportActive,
566
566
  waitForTypingIdle: typing.waitForIdle,
567
- waitForActivityIdle: activityVerbosityRuntime?.waitForIdle,
567
+ async waitForActivityIdle() {
568
+ await activityVerbosityRuntime?.waitForIdle();
569
+ await assistantOutputRuntime.waitForIdle();
570
+ },
568
571
  dispatchNextQueuedTelegramTurn,
569
572
  requestDeferredDispatchNextQueuedTelegramTurn:
570
573
  deferredQueueDispatchRuntime.request,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.26.16",
3
+ "version": "0.27.1",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"