@llblab/pi-telegram 0.9.4 → 0.9.5

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
@@ -7,6 +7,7 @@
7
7
  - `Boundary Clarity`: Separate Telegram transport concerns, π integration concerns, rendering behavior, and release/documentation state
8
8
  - `Progressive Enhancement + Graceful Degradation`: Prefer behavior that upgrades automatically when richer runtime context exists, but always preserves a useful fallback path when it does not
9
9
  - `Runtime Safety`: Prefer queue and rendering behavior that fails predictably over clever behavior that can desynchronize the Telegram bridge from π session state
10
+ - `Pi-Native Extensibility`: `pi-telegram` should inherit π's own extension philosophy. It is not only a Telegram adapter; it should become a small, convenient, composable Telegram shell for π extensions, where new capabilities plug into stable contracts instead of forking polling, transport, or menu ownership.
10
11
 
11
12
  ## 1. Concept
12
13
 
package/BACKLOG.md CHANGED
@@ -2,4 +2,9 @@
2
2
 
3
3
  ## Open Work
4
4
 
5
- No open work.
5
+ - Implement Telegram Extension Sections Platform for the 0.10.0 line.
6
+ - Exit: Runtime registry, main-menu integration, `section:` callback routing, safe section context ports, diagnostics, docs, and at least one small demo/fixture prove ordinary pi extensions can add Telegram menu sections without owning a second poller.
7
+ - Explore always-available outbound Telegram tools for queued artifacts and controls.
8
+ - Priority: Low.
9
+ - Idea: Provide tools such as `telegram_attach_file` and `telegram_attach_button` that can be called outside an active Telegram turn, using the paired chat/session as the delivery target when safe.
10
+ - Exit: Design note defines active-turn versus ambient delivery semantics, safety constraints, failure modes, and whether the current `telegram_attach` contract should stay turn-scoped or gain an ambient companion.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.5: Telegram Delivery Resilience Hotfix
4
+
5
+ - `[Preview Delivery]` Preview flush failures from Telegram transport errors such as `fetch failed` / `ECONNRESET` are now caught and recorded as runtime diagnostics instead of escaping from the preview pipeline. Impact: transient Telegram connectivity failures no longer crash the extension during streamed preview edits.
6
+ - `[Final Delivery]` Final Markdown preview replacement now catches Telegram transport failures and returns a normal fallback signal; the agent-end delivery path records final-text delivery failures and continues cleanup, attachment handling, and queue dispatch. Impact: a failed `editMessageText` at `agent_end` no longer breaks the bridge lifecycle or blocks the next queued Telegram turn.
7
+ - `[Diagnostics]` Preview and final delivery failures now flow through the runtime event recorder with compact phase metadata. Impact: `/telegram-status` can show recent transport failures without dumping noisy stack traces into the extension runner.
8
+ - `[Tests]` Added preview and queue regressions for non-fatal Telegram transport failures during preview flush and final delivery.
9
+ - `[Extension Sections Draft]` Added a draft design note for pi-native Telegram extension sections, reserved the future `section:` callback prefix, linked the draft from docs, and recorded the project philosophy that `pi-telegram` should inherit π's extensibility model as a shared Telegram shell for loaded extensions. Impact: the future 0.10.0 extension platform direction is documented without exposing a stable API yet.
10
+ - `[Docs Formatting]` Normalized project Markdown so prose paragraphs stay as single logical lines and Markdown tables remain narrow instead of using artificial hard wraps. Impact: editors and viewers can handle visual wrapping naturally while fixed-width structures stay readable.
11
+ - `[Settings Copy]` Tightened the proactive-push settings text by removing redundant persistence/default wording.
12
+
3
13
  ## 0.9.4: Temp Dir And Command Template Hotfix
4
14
 
5
15
  - `[Telegram Temp Dir]` Default Telegram API temp files now respect `PI_CODING_AGENT_DIR`, falling back to `~/.pi/agent` when the env var is unset. Impact: sandboxed or relocated agent dirs no longer force Telegram downloads through the default home-directory path.
package/README.md CHANGED
@@ -87,8 +87,8 @@ Use these inside the Telegram DM with your bot:
87
87
  - **`/compact`**: Start session compaction (only works when the session is idle).
88
88
  - **`/next`**: Dispatch the next queued turn (aborts π first if busy).
89
89
  - **`/continue`**: Enqueue a priority `continue` prompt. It waits like normal Telegram work when π is busy and can trigger prompt/skill handling that listens for `continue`.
90
- - **`/stop`**: Abort the active run and clear all waiting Telegram queue items.
91
90
  - **`/abort`**: Abort the active run without touching the queue.
91
+ - **`/stop`**: Abort the active run and clear all waiting Telegram queue items.
92
92
 
93
93
  Prompt-template commands: π prompt templates are mapped to Telegram-safe aliases (`fix-tests.md` becomes `/fix_tests`) and shown as compact command-only rows between the built-in commands and status rows in `/start`. They are not registered in the Telegram bot command menu, keeping the bot menu focused on bridge controls. Sending `/template_name args` from Telegram expands the matching π prompt-template file and queues the expanded prompt like normal Telegram work.
94
94
 
package/docs/README.md CHANGED
@@ -11,3 +11,4 @@ Living index of project documentation in `/docs`.
11
11
  - [locks.md](./locks.md) — Shared `locks.json` standard for singleton extension ownership
12
12
  - [callback-namespaces.md](./callback-namespaces.md) — Shared Telegram `callback_data` namespace standard for layered extensions
13
13
  - [external-handlers.md](./external-handlers.md) — Runtime interceptor registry that lets layered extensions observe and consume Telegram updates without owning their own polling connection
14
+ - [extension-sections.md](./extension-sections.md) — Draft Telegram Extension Sections Standard for external menu sections and structured Telegram UI extension points
@@ -23,29 +23,28 @@ Naming rule: because the repository already scopes this codebase to Telegram, ex
23
23
 
24
24
  Current runtime areas use these ownership boundaries:
25
25
 
26
- | Domain | Owns |
27
- | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
28
- | `index.ts` | Single composition root for live π/Telegram ports, session state, API-bound transport adapters, and status updates |
29
- | `api` | Bot API transport shapes/helpers, retries, file download, temp-dir lifecycle, inbound limits, chat actions, lazy bot-token clients, runtime error recording |
30
- | `config` / `setup` | Persisted bot/session pairing state, authorization, first-user pairing, token prompting, env fallback, validation, config persistence |
31
- | `locks` / `polling` | Singleton `locks.json` ownership, takeover/restart semantics, long-poll controller state, update offset persistence, poll-loop runtime wiring |
32
- | `updates` / `routing` | Update classification/execution planning, paired authorization, reactions, edits, callbacks, and inbound route composition |
33
- | `media` / `text-groups` / `turns` / `inbound-handlers` | Text/media extraction, media-group debounce, long-text split coalescing, inbound downloads, inbound text/media handler execution, turn building/editing, image reads, legacy `attachmentHandlers` compatibility |
34
- | `queue` | Queue item contracts, lane admission/order, stores, mutations, dispatch readiness/runtime, prompt/control enqueueing, session and agent/tool lifecycle sequencing |
35
- | `runtime` | Session-local coordination primitives: counters, lifecycle flags, setup guard, abort handler, typing-loop timers, prompt-dispatch flags, agent-end reset binding |
36
- | `model` / `menu-model` / `menu-thinking` / `menu-status` / `menu` / `menu-queue` / `menu-settings` / `commands` | Model identity/thinking levels, scoped model resolution, in-flight switching, model-menu UI, thinking-menu UI, status-menu UI, inline application callback composition, queue-menu UI, hidden settings UI, slash commands, bot command registration |
37
- | `keyboard` | Shared Telegram inline-keyboard reply-markup structure; feature domains own callback semantics and button construction |
38
- | `preview` / `replies` / `rendering` | Preview lifecycle/transports, final reply delivery and reply parameters, Telegram HTML Markdown rendering, chunking, stable-preview snapshots |
39
- | `outbound-handlers` | Outbound text transformation, assistant-authored outbound comments, generated reply artifacts, inline-keyboard callbacks, and post-`agent_end` outbound action delivery |
40
- | `outbound-attachments` | `telegram_attach` registration, outbound attachment queueing, stat/limit checks, photo/document delivery classification |
41
- | `status` | Status-bar/status-message rendering, queue-lane status views, redacted runtime event ring, grouped π diagnostics |
42
- | `lifecycle` / `prompts` / `prompt-templates` / `pi` | π hook registration, Telegram-specific before-agent prompt injection, π prompt-template discovery/expansion, centralized direct pi SDK imports and context adapters |
43
- | `command-templates` | Portable shell-free command-template standard helpers, composition expansion, placeholder substitution, and executable resolution |
26
+ - `index.ts`: single composition root for live π/Telegram ports, session state, API-bound transport adapters, and status updates.
27
+ - `api`: Bot API transport shapes/helpers, retries, file download, temp-dir lifecycle, inbound limits, chat actions, lazy bot-token clients, and runtime error recording.
28
+ - `config` / `setup`: persisted bot/session pairing state, authorization, first-user pairing, token prompting, env fallback, validation, and config persistence.
29
+ - `locks` / `polling`: singleton `locks.json` ownership, takeover/restart semantics, long-poll controller state, update offset persistence, and poll-loop runtime wiring.
30
+ - `updates` / `routing`: update classification/execution planning, paired authorization, reactions, edits, callbacks, and inbound route composition.
31
+ - `media` / `text-groups` / `turns` / `inbound-handlers`: text/media extraction, media-group debounce, long-text split coalescing, inbound downloads, inbound text/media handler execution, turn building/editing, image reads, and legacy `attachmentHandlers` compatibility.
32
+ - `queue`: queue item contracts, lane admission/order, stores, mutations, dispatch readiness/runtime, prompt/control enqueueing, and session/agent/tool lifecycle sequencing.
33
+ - `runtime`: session-local coordination primitives: counters, lifecycle flags, setup guard, abort handler, typing-loop timers, prompt-dispatch flags, and agent-end reset binding.
34
+ - `model` / `menu-model` / `menu-thinking` / `menu-status` / `menu` / `menu-queue` / `menu-settings` / `commands`: model identity/thinking levels, scoped model resolution, in-flight switching, model/thinking/status/queue/settings menu UI, inline application callback composition, slash commands, and bot command registration.
35
+ - Future `extension-sections`: structured external Telegram menu sections registered by ordinary pi extensions; owns section registry, compact section callback tokens, section render/callback dispatch, safe section runtime ports, and diagnostics.
36
+ - `keyboard`: shared Telegram inline-keyboard reply-markup structure; feature domains own callback semantics and button construction.
37
+ - `preview` / `replies` / `rendering`: preview lifecycle/transports, final reply delivery and reply parameters, Telegram HTML Markdown rendering, chunking, and stable-preview snapshots.
38
+ - `outbound-handlers`: outbound text transformation, assistant-authored outbound comments, generated reply artifacts, inline-keyboard callbacks, and post-`agent_end` outbound action delivery.
39
+ - `outbound-attachments`: `telegram_attach` registration, outbound attachment queueing, stat/limit checks, and photo/document delivery classification.
40
+ - `status`: status-bar/status-message rendering, queue-lane status views, redacted runtime event ring, and grouped π diagnostics.
41
+ - `lifecycle` / `prompts` / `prompt-templates` / `pi`: π hook registration, Telegram-specific before-agent prompt injection, π prompt-template discovery/expansion, and centralized direct pi SDK imports/context adapters.
42
+ - `command-templates`: portable shell-free command-template standard helpers, composition expansion, placeholder substitution, and executable resolution.
44
43
 
45
44
  Boundary invariants:
46
45
 
47
46
  - Constants and state types live with their owning domains; do not reintroduce shared buckets such as `lib/constants.ts` or `lib/types.ts`
48
- - Shared Telegram inline-keyboard structure belongs to `keyboard`; application-control labels, callback data, and callback behavior stay in `menu`/`menu-model`/`menu-thinking`/`menu-status`/`menu-queue`; core queue mechanics stay in `queue`
47
+ - Shared Telegram inline-keyboard structure belongs to `keyboard`; application-control labels, callback data, and callback behavior stay in `menu`/`menu-model`/`menu-thinking`/`menu-status`/`menu-queue`; future external section labels, callbacks, and dispatch stay in `extension-sections`; core queue mechanics stay in `queue`
49
48
  - Domain helpers use narrow structural projections when that avoids importing concrete wire DTOs or broader runtime objects unnecessarily
50
49
  - Preview appearance stays in `rendering`; preview transport/lifecycle stays in `preview`
51
50
  - Direct `node:*` file-operation imports stay in owning domains, not in `index.ts`
@@ -95,13 +94,11 @@ Queued items now use two explicit dimensions:
95
94
 
96
95
  Admission contract:
97
96
 
98
- | Admission | Examples | Queue shape | Dispatch rank |
99
- | --------------------- | ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
100
- | Immediate execution | `/compact`, `/queue`, `/stop`, `/help`, `/start` | Does not enter the Telegram queue; `/help` opens the same menu as `/start`; `/stop` also clears queued items | N/A |
101
- | Queued prompt command | `/continue`, `/template_name args` | `/continue` enqueues a Telegram-owned `continue` prompt; prompt-template commands expand the matching π template before entering the normal prompt queue | priority for `/continue`, otherwise default |
102
- | Control queue | Model-switch continuation turns and future deferred controls | `queueLane: control`; accepts control items and continuation prompts | 0 |
103
- | Priority prompt queue | A waiting prompt promoted by `👍`, `⚡️`, `❤️`, `🕊`, or `🔥` | `kind: prompt`, `queueLane: priority` | 1 |
104
- | Default prompt queue | Normal Telegram text/media turns | `kind: prompt`, `queueLane: default` | 2 |
97
+ - Immediate execution: `/compact`, `/queue`, `/stop`, `/help`, and `/start` do not enter the Telegram queue. `/help` opens the same menu as `/start`; `/stop` also clears queued items. Dispatch rank: N/A.
98
+ - Queued prompt command: `/continue` enqueues a priority Telegram-owned `continue` prompt. Prompt-template commands such as `/template_name args` expand the matching π template before entering the normal prompt queue. Dispatch rank: priority for `/continue`, otherwise default.
99
+ - Control queue: model-switch continuation turns and future deferred controls use `queueLane: control`, accept control items and continuation prompts, and dispatch at rank `0`.
100
+ - Priority prompt queue: a waiting prompt promoted by `👍`, `⚡️`, `❤️`, `🕊`, or `🔥` uses `kind: prompt`, `queueLane: priority`, and dispatches at rank `1`.
101
+ - Default prompt queue: normal Telegram text/media turns use `kind: prompt`, `queueLane: default`, and dispatch at rank `2`.
105
102
 
106
103
  The command action itself carries its execution mode, and the queue domain exposes lane contracts for admission mode, dispatch rank, and allowed item kinds. Queue append and planning paths validate lane admission so a malformed control/default or other invalid lane pairing fails predictably instead of silently changing priority. This lets synthetic control actions and Telegram prompts share one stable ordering model while still rendering distinctly in status output. In the π status bar, busy labels distinguish `active`, `dispatching`, `queued`, `tool running`, `model`, and `compacting`; priority prompts and priority control items are marked with `⚡`. If a queue mutation removes the last waiting item while Telegram-owned work still has running tools, the status remains yellow `active` instead of degrading to green `connected`.
107
104
 
@@ -160,7 +157,7 @@ Telegram prompt responses use explicit delivery context to attach outbound text,
160
157
 
161
158
  Outbound files are sent only after the active Telegram turn completes, must be staged through the `telegram_attach` tool, are staged atomically per tool call, are checked against a default 50 MiB limit configurable through `PI_TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES` or `TELEGRAM_MAX_ATTACHMENT_SIZE_BYTES`, and use file-backed multipart blobs so large sends do not require preloading whole files into memory.
162
159
 
163
- Assistant-authored outbound actions use final-message markup instead of agent tool calls. Preview updates strip closed top-level HTML comments and currently open/partial top-level comment starts before rendering, so users do not see transient metadata even when streaming flushes happen after only `<`, `<!`, or `<!--`. On `agent_end`, the bridge removes top-level comments from the Markdown text reply, but treats column-zero top-level `<!-- telegram_voice ... -->` and `<!-- telegram_button ... -->` blocks specially before delivery; comments inside fenced code, quotes, lists, or indented examples stay literal, including fenced blocks with Markdown-valid indented closing fences. Voice maps to the first matching `outboundHandlers[]` entry with `type: "voice"`, synthesizes body text, `text="..."`, or colon shorthand through command-template execution, and uploads the generated OGG/Opus file via Telegram `sendVoice`; when no outbound voice handler is configured, it silently skips voice delivery. The `template: [...]` form can express TTS plus MP3-to-OGG conversion using configured templates and bridge-provided `{text}`, `{mp3}`, and `{ogg}` placeholders. Top-level `args` and `defaults` apply to all composed steps unless a step defines private values, the default command timeout applies automatically, and each step receives the previous step's stdout on stdin by default, without hard-coded filesystem defaults. Button blocks are built in: each `telegram_button` block becomes one inline-keyboard button on the final text, and callback clicks enqueue the configured prompt text as a normal Telegram prompt turn; the `telegram_button: Label` shorthand uses the same text for label and prompt, `prompt="..."` supports explicit one-line prompts, and body-form buttons use the body as the prompt. Unknown callback data that does not match pi-telegram-owned prefixes (`tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`) is forwarded to π as `[callback] <data>` after built-in handlers decline it, giving layered extensions a simple namespaced button channel without separate polling; layered callback payloads should follow the [Callback Namespace Standard](./callback-namespaces.md). When proactive push is enabled, successful local non-Telegram final replies are sent to the paired chat. Local prompt text is not sent because the bot does not own or mirror terminal user messages. This keeps terminal-originated results visible in Telegram without changing Telegram-originated turn delivery.
160
+ Assistant-authored outbound actions use final-message markup instead of agent tool calls. Preview updates strip closed top-level HTML comments and currently open/partial top-level comment starts before rendering, so users do not see transient metadata even when streaming flushes happen after only `<`, `<!`, or `<!--`. On `agent_end`, the bridge removes top-level comments from the Markdown text reply, but treats column-zero top-level `<!-- telegram_voice ... -->` and `<!-- telegram_button ... -->` blocks specially before delivery; comments inside fenced code, quotes, lists, or indented examples stay literal, including fenced blocks with Markdown-valid indented closing fences. Voice maps to the first matching `outboundHandlers[]` entry with `type: "voice"`, synthesizes body text, `text="..."`, or colon shorthand through command-template execution, and uploads the generated OGG/Opus file via Telegram `sendVoice`; when no outbound voice handler is configured, it silently skips voice delivery. The `template: [...]` form can express TTS plus MP3-to-OGG conversion using configured templates and bridge-provided `{text}`, `{mp3}`, and `{ogg}` placeholders. Top-level `args` and `defaults` apply to all composed steps unless a step defines private values, the default command timeout applies automatically, and each step receives the previous step's stdout on stdin by default, without hard-coded filesystem defaults. Button blocks are built in: each `telegram_button` block becomes one inline-keyboard button on the final text, and callback clicks enqueue the configured prompt text as a normal Telegram prompt turn; the `telegram_button: Label` shorthand uses the same text for label and prompt, `prompt="..."` supports explicit one-line prompts, and body-form buttons use the body as the prompt. Unknown callback data that does not match pi-telegram-owned prefixes (`tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`, future `section:`) is forwarded to π as `[callback] <data>` after built-in handlers decline it, giving layered extensions a simple namespaced button channel without separate polling; layered callback payloads should follow the [Callback Namespace Standard](./callback-namespaces.md). Future structured menu integrations should use the [Telegram Extension Sections Standard](./extension-sections.md) instead of hand-rolled fallback callbacks. When proactive push is enabled, successful local non-Telegram final replies are sent to the paired chat. Local prompt text is not sent because the bot does not own or mirror terminal user messages. This keeps terminal-originated results visible in Telegram without changing Telegram-originated turn delivery.
164
161
 
165
162
  This keeps technical Markdown, code, tables, formulas, and numbered lists in the text channel when appropriate while allowing TTS-friendly voice messages and tappable continuations without invoking `telegram_attach` or extra transport tools. Telegram prompt guidance targets about 37 visible cells for tables, dense list items, and compact text blocks because emoji and other wide glyphs make raw character counts misleading on mobile screens.
166
163
 
@@ -20,7 +20,7 @@ myext:page:2
20
20
 
21
21
  - Use a stable extension-owned namespace, preferably the package or extension name without scope punctuation.
22
22
  - Keep the namespace lowercase ASCII: `a-z`, `0-9`, `_`, `-`.
23
- - Do not use `pi-telegram` owned prefixes: `tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`. Current app navigation uses `menu:`; `status:` remains reserved for legacy/owned status callbacks but is not emitted by current UI.
23
+ - Do not use `pi-telegram` owned prefixes: `tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`, `section:`. Current app navigation uses `menu:`; `status:` remains reserved for legacy/owned status callbacks but is not emitted by current UI. `section:` is reserved for the structured extension-section router documented in [Extension Sections](./extension-sections.md).
24
24
  - Keep the full `callback_data` within Telegram's 64-byte limit.
25
25
  - Put only opaque ids or small enum values in payloads; do not store secrets, full prompts, or large state.
26
26
  - Treat callbacks as untrusted input. Validate namespace, action, and payload before executing side effects.
@@ -34,3 +34,15 @@ If `pi-telegram` receives callback data that is not owned by its built-in prefix
34
34
  ```
35
35
 
36
36
  Layered extensions may intercept that message and handle their own namespace. If no extension handles it, the assistant may see the fallback message and should tell the user the callback was not handled and the environment may be misconfigured.
37
+
38
+ ## Extension sections
39
+
40
+ [Telegram Extension Sections](./extension-sections.md) are a higher-level UI contract over this namespace rule. A section owns a canonical extension identity such as `@llblab/pi-telegram-explorer`, but its Telegram `callback_data` should use the `pi-telegram` owned `section:` prefix plus a compact token, because Telegram limits callback payloads to 64 bytes.
41
+
42
+ Conceptual form:
43
+
44
+ ```text
45
+ section:<token>:<action>[:<payload>]
46
+ ```
47
+
48
+ The token maps back to the full section identity inside the section registry. Section authors should not hand-roll `section:` callbacks outside the section context helpers, and ordinary layered extensions should continue using their own namespace plus external handlers or the `[callback]` fallback.
@@ -0,0 +1,293 @@
1
+ # Telegram Extension Sections Standard Draft
2
+
3
+ **Status:** Draft. This document is a design note for the upcoming Extension Sections platform and is not an implemented or stable public API yet. Treat all names, shapes, and examples as provisional until the implementation lands.
4
+
5
+ **Meta-contract:** transportable (bit-for-bit identical across projects), high-density (zero fluff), constant (evolve by crystallizing, not speculating), optimal minimum (add only when it hurts).
6
+
7
+ ---
8
+
9
+ Telegram Extension Sections are a proposed registration contract that lets ordinary pi extensions add structured UI sections to the `pi-telegram` inline application menu.
10
+
11
+ The guiding philosophy is pi-native extensibility: `pi-telegram` should inherit π's own model of small composable extensions. The bridge should act as a shared Telegram shell for loaded π extensions, not as a closed one-off bot or a place where every feature must fork Telegram polling and transport.
12
+
13
+ They are not a new extension loader. pi still loads extensions through its normal TypeScript/package system. A loaded extension registers a Telegram section with `pi-telegram`; `pi-telegram` owns bot polling, menu rendering, callback routing, Telegram authorization, and message lifecycle.
14
+
15
+ ## Purpose
16
+
17
+ Use sections when an extension needs a Telegram-native UI surface inside the existing bot shell:
18
+
19
+ - File or project explorers
20
+ - Prompt/session history viewers
21
+ - Tool approval dashboards
22
+ - Runtime status panels
23
+ - Extension settings or diagnostics
24
+ - Human-in-the-loop forms that should not become agent turns
25
+
26
+ Do not use sections for plain agent prompts, one-shot buttons authored by the assistant, or command-template pipelines. Those stay in the normal queue, outbound action comments, inbound/outbound handlers, or command-template domains.
27
+
28
+ ## Identity key
29
+
30
+ Each section has one stable identity key.
31
+
32
+ Use the same identity rules as the Extension Locks Standard:
33
+
34
+ 1. `package.json/name` for npm-style pi packages
35
+ 2. Directory name when the extension entrypoint is `index.ts` but there is no package name
36
+ 3. File basename when the extension is a single file
37
+
38
+ For npm-style package extensions, the canonical value is the `package.json` `name`.
39
+
40
+ Examples:
41
+
42
+ ```text
43
+ extensions/pi-telegram-explorer/package.json name=@llblab/pi-telegram-explorer -> @llblab/pi-telegram-explorer
44
+ extensions/pi-telegram-explorer/index.ts without package.json -> pi-telegram-explorer
45
+ extensions/pi-telegram-explorer.ts -> pi-telegram-explorer
46
+ ```
47
+
48
+ The section `id` is also the owner identity. Do not add a separate `owner` field unless a later concrete need appears.
49
+
50
+ The identity key is used for:
51
+
52
+ - Registry ownership
53
+ - Conflict detection
54
+ - Diagnostics
55
+ - Cleanup
56
+ - Callback routing lookup
57
+ - Future capability policy
58
+
59
+ ## Registration shape
60
+
61
+ Minimum shape:
62
+
63
+ ```ts
64
+ registerTelegramSection({
65
+ id: "@llblab/pi-telegram-explorer",
66
+ label: "🗂 Explorer",
67
+ render(ctx) {
68
+ return {
69
+ text: "<b>Explorer</b>",
70
+ parseMode: "html",
71
+ replyMarkup,
72
+ };
73
+ },
74
+ handleCallback(ctx) {
75
+ return "handled";
76
+ },
77
+ });
78
+ ```
79
+
80
+ Recommended TypeScript shape:
81
+
82
+ ```ts
83
+ type TelegramSectionId = string;
84
+ type TelegramSectionCallbackResult = "handled" | "pass";
85
+
86
+ interface TelegramSectionRegistration {
87
+ id: TelegramSectionId;
88
+ label: string;
89
+ order?: number;
90
+ render: (
91
+ ctx: TelegramSectionRenderContext,
92
+ ) => TelegramSectionView | Promise<TelegramSectionView>;
93
+ handleCallback?: (
94
+ ctx: TelegramSectionCallbackContext,
95
+ ) => TelegramSectionCallbackResult | Promise<TelegramSectionCallbackResult>;
96
+ }
97
+
98
+ interface TelegramSectionView {
99
+ text: string;
100
+ parseMode?: "html" | "plain";
101
+ replyMarkup?: TelegramInlineKeyboardMarkup;
102
+ }
103
+ ```
104
+
105
+ Registration returns a disposer:
106
+
107
+ ```ts
108
+ const unregister = registerTelegramSection(section);
109
+ unregister();
110
+ ```
111
+
112
+ ## Loading model
113
+
114
+ Sections are registered by normal pi extensions:
115
+
116
+ ```ts
117
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
118
+ import { registerTelegramSection } from "@llblab/pi-telegram/lib/extension-sections.ts";
119
+
120
+ export default function (pi: ExtensionAPI) {
121
+ const unregister = registerTelegramSection({
122
+ id: "@llblab/pi-telegram-explorer",
123
+ label: "🗂 Explorer",
124
+ render: async (ctx) => ctx.html("<b>Explorer</b>"),
125
+ });
126
+ pi.on("shutdown", () => unregister());
127
+ }
128
+ ```
129
+
130
+ `pi-telegram` may expose a typed import and a zero-coupling `globalThis` registry. The typed import is the preferred authoring path. The global registry exists only to tolerate load order and package coupling constraints.
131
+
132
+ ## Menu integration
133
+
134
+ `pi-telegram` owns the main Telegram application menu.
135
+
136
+ Registered sections appear as top-level menu rows after built-in core sections unless an `order` value says otherwise.
137
+
138
+ Rules:
139
+
140
+ - `label` must be compact enough for mobile Telegram
141
+ - Built-in sections keep priority over external sections
142
+ - Duplicate `id` registration is rejected or replaces only the same live owner through an explicit disposer path
143
+ - Section errors must not break the whole main menu
144
+ - If a section fails to render, `pi-telegram` should show a compact error row or omit the section and record diagnostics
145
+
146
+ ## Callback routing
147
+
148
+ `pi-telegram` owns section callback transport.
149
+
150
+ A section callback must include a `pi-telegram` owned prefix plus a compact section token that maps back to the full identity key.
151
+
152
+ Conceptual form:
153
+
154
+ ```text
155
+ section:<token>:<action>:<payload>
156
+ ```
157
+
158
+ The token is an implementation detail. The registry maps it to the canonical section `id`. `section:` is reserved in the [Callback Namespace Standard](./callback-namespaces.md); section authors should build callbacks through section context helpers rather than hand-crafting `section:` payloads.
159
+
160
+ Routing order:
161
+
162
+ 1. Telegram update arrives through the single `pi-telegram` poller
163
+ 2. Existing low-level external handlers may observe or consume the update first
164
+ 3. Built-in menu callbacks are handled by built-in domains
165
+ 4. Section callbacks are resolved by token and sent to the registered section
166
+ 5. Unknown callbacks fall back to the existing callback namespace behavior when appropriate
167
+
168
+ Section handlers return:
169
+
170
+ - `"handled"` — callback was handled, do not continue routing
171
+ - `"pass"` — section declines this callback, allow fallback routing
172
+
173
+ Stale callbacks:
174
+
175
+ - Missing section id or token should answer the callback with a short stale/expired notice
176
+ - Missing target state should re-render the section root when possible
177
+ - Section errors should be caught, surfaced as a short callback answer, and recorded in diagnostics
178
+
179
+ ## Runtime ports
180
+
181
+ A section receives a narrow context, not raw `pi-telegram` internals.
182
+
183
+ Initial safe ports:
184
+
185
+ ```ts
186
+ interface TelegramSectionContext {
187
+ sectionId: string;
188
+ chatId: number;
189
+ messageId?: number;
190
+ answerCallback(text?: string): Promise<void>;
191
+ edit(view: TelegramSectionView): Promise<void>;
192
+ open(view: TelegramSectionView): Promise<void>;
193
+ enqueuePrompt(prompt: string): Promise<void>;
194
+ getQueueSnapshot(): TelegramQueueSnapshot;
195
+ getSessionSnapshot?(): TelegramSessionSnapshot;
196
+ }
197
+ ```
198
+
199
+ Filesystem or prompt-history mutation is not part of the baseline. Add capability-specific ports only when the first real extension needs them.
200
+
201
+ ## Security and authorization
202
+
203
+ `pi-telegram` keeps Telegram authorization ownership.
204
+
205
+ Baseline rules:
206
+
207
+ - Section callbacks are accepted only from the paired/authorized Telegram user
208
+ - Sections should not receive unauthorized updates
209
+ - Sections must not start their own Telegram poller
210
+ - Sections must not assume filesystem or session mutation rights
211
+ - Sensitive capabilities should be exposed as explicit typed ports, not by passing raw process or bot clients
212
+
213
+ For filesystem explorers, default to read-only browse and file-send behavior. Deleting, writing, shell execution, or rollback-like mutations require separate explicit capabilities and confirmation UI.
214
+
215
+ ## Diagnostics
216
+
217
+ `pi-telegram` should be able to report registered sections.
218
+
219
+ Minimum diagnostic fields:
220
+
221
+ ```text
222
+ id
223
+ label
224
+ status: active | stale | error
225
+ lastError
226
+ ```
227
+
228
+ The identity key is sufficient as the owner label. Do not add a second owner field.
229
+
230
+ Useful future fields:
231
+
232
+ ```text
233
+ registeredAt
234
+ lastRenderAt
235
+ lastCallbackAt
236
+ callbackCount
237
+ errorCount
238
+ capabilities
239
+ ```
240
+
241
+ Diagnostics should be available through a status/debug surface without cluttering normal Telegram UI.
242
+
243
+ ## Relationship to callback namespaces and external handlers
244
+
245
+ [Callback Namespaces](./callback-namespaces.md) define callback ownership names. [External Handlers](./external-handlers.md) define low-level raw update interception. Extension sections define the structured Telegram UI layer above both.
246
+
247
+ Sections still use namespaced callback data, but `pi-telegram` owns the `section:` prefix and maps compact tokens to canonical section identities. That keeps Telegram's 64-byte callback limit compatible with full npm package names such as `@llblab/pi-telegram-explorer`.
248
+
249
+ Use external handlers when an extension needs direct raw Telegram update access, a custom callback namespace, or out-of-band Promise resolution.
250
+
251
+ Use extension sections when an extension needs a durable menu surface, callback routing, and Telegram UI lifecycle managed by `pi-telegram`.
252
+
253
+ ## Relationship to command templates
254
+
255
+ Command templates execute local commands and pipelines through stdin/stdout.
256
+
257
+ Extension sections do not execute command templates by default. A section may call an extension-owned command or tool internally, but the section standard is a UI registration and callback-routing contract, not a shell execution contract.
258
+
259
+ ## Non-goals
260
+
261
+ - No second Telegram poller
262
+ - No new pi extension loader
263
+ - No generic webview system
264
+ - No default filesystem mutation API
265
+ - No prompt rollback semantics in the base standard
266
+ - No separate owner field while identity key is sufficient
267
+
268
+ ## Evolution path
269
+
270
+ 0.10.0 minimum:
271
+
272
+ - Section registry
273
+ - Main menu integration
274
+ - Section callback routing
275
+ - Narrow runtime ports
276
+ - Diagnostics for registered sections
277
+ - Documentation for extension authors
278
+
279
+ First demo extension candidate:
280
+
281
+ ```text
282
+ @llblab/pi-telegram-explorer
283
+ ```
284
+
285
+ Initial demo scope:
286
+
287
+ - Browse current project tree read-only
288
+ - View compact file previews
289
+ - Send selected files as Telegram documents
290
+ - Browse recent prompt/session snapshots read-only
291
+ - Enqueue a prompt derived from a selected item
292
+
293
+ Defer rollback, filesystem writes, deletes, and broad mutation until the read-only and enqueue-only model is proven.
@@ -1,60 +1,38 @@
1
1
  # External Handlers
2
2
 
3
- `pi-telegram` owns a single `getUpdates` long-poll connection per bot. Other
4
- pi extensions cannot open a competing poller against the same bot — the
5
- Telegram Bot API uses a per-bot `offset` cursor, and two pollers race each
6
- other and lose updates.
3
+ `pi-telegram` owns a single `getUpdates` long-poll connection per bot. Other pi extensions cannot open a competing poller against the same bot — the Telegram Bot API uses a per-bot `offset` cursor, and two pollers race each other and lose updates.
7
4
 
8
- This document describes the registry that lets layered pi extensions
9
- (running in the same pi process) hook into `pi-telegram`'s polling loop and
10
- react to inbound Telegram updates **before** `pi-telegram`'s default routing
11
- fires.
5
+ This document describes the registry that lets layered pi extensions running in the same pi process hook into `pi-telegram`'s polling loop and react to inbound Telegram updates **before** `pi-telegram`'s default routing fires.
12
6
 
13
- It is the runtime counterpart to
14
- [Callback Namespaces](./callback-namespaces.md): callback namespaces define
15
- how to share `callback_data` cleanly; external handlers define how to
16
- observe and optionally short-circuit the dispatch of those updates.
7
+ It is the runtime counterpart to [Callback Namespaces](./callback-namespaces.md): callback namespaces define how to share `callback_data` cleanly; external handlers define how to observe and optionally short-circuit the dispatch of those updates.
17
8
 
18
9
  ## When to use it
19
10
 
20
11
  Use it when a layered extension needs to:
21
12
 
22
- - Resolve out-of-band state (for example, a `tool_call` approval Promise)
23
- the moment a Telegram callback arrives, rather than waiting for the next
24
- agent turn.
25
- - Suppress `pi-telegram`'s default routing for callbacks owned by the
26
- layered extension (so `pi-telegram` does not also forward them as
27
- `[callback] <data>` text).
28
- - Observe arbitrary update types (messages, edits, channel posts, reactions)
29
- without owning the polling connection.
13
+ - Resolve out-of-band state, for example a `tool_call` approval Promise, the moment a Telegram callback arrives, rather than waiting for the next agent turn.
14
+ - Suppress `pi-telegram`'s default routing for callbacks owned by the layered extension, so `pi-telegram` does not also forward them as `[callback] <data>` text.
15
+ - Observe arbitrary update types such as messages, edits, channel posts, or reactions without owning the polling connection.
30
16
 
31
- If the layered extension only needs to read assistant-visible callbacks, the
32
- existing `[callback] <data>` fallback documented in
33
- [Callback Namespaces](./callback-namespaces.md) is enough.
17
+ If the layered extension only needs to read assistant-visible callbacks, the existing `[callback] <data>` fallback documented in [Callback Namespaces](./callback-namespaces.md) is enough.
18
+
19
+ If the extension needs a durable top-level Telegram menu section with managed rendering, callback routing, authorization, and diagnostics, use the higher-level [Telegram Extension Sections](./extension-sections.md) contract instead of a raw external handler.
34
20
 
35
21
  ## Constraints
36
22
 
37
- - One bot, one pi process, one `getUpdates` poller. This registry does **not**
38
- enable running multiple pi instances against the same bot.
39
- - Interceptors run in the polling loop. They must return quickly; long
40
- awaits delay subsequent updates.
41
- - Interceptor errors are caught and logged silently so polling never breaks.
42
- If you need durable error reporting, do it inside your interceptor.
43
- - The registry lives on `globalThis`. Module instance identity is not
44
- required, so layered extensions can reach it without importing
45
- `@llblab/pi-telegram`.
23
+ - One bot, one pi process, one `getUpdates` poller. This registry does **not** enable running multiple pi instances against the same bot.
24
+ - Interceptors run in the polling loop. They must return quickly; long awaits delay subsequent updates.
25
+ - Interceptor errors are caught and logged silently so polling never breaks. If you need durable error reporting, do it inside your interceptor.
26
+ - The registry lives on `globalThis`. Module instance identity is not required, so layered extensions can reach it without importing `@llblab/pi-telegram`.
46
27
 
47
28
  ## Verdicts
48
29
 
49
30
  Each interceptor returns one of:
50
31
 
51
32
  - `"consume"` — `pi-telegram` skips its default routing for this update.
52
- - `"pass"` (or `void` / `undefined`) — `pi-telegram` routes the update
53
- normally. Other interceptors registered after this one still run for the
54
- same update.
33
+ - `"pass"` or `void` / `undefined` — `pi-telegram` routes the update normally. Other interceptors registered after this one still run for the same update.
55
34
 
56
- The first interceptor that returns `"consume"` wins; later interceptors are
57
- not called for that update.
35
+ The first interceptor that returns `"consume"` wins; later interceptors are not called for that update.
58
36
 
59
37
  ## Registering an interceptor
60
38
 
@@ -79,18 +57,9 @@ off();
79
57
 
80
58
  ### Zero-coupling globalThis lookup
81
59
 
82
- When the layered extension prefers no `import` from `@llblab/pi-telegram` (so
83
- load order between the two extensions does not matter, and either can be
84
- installed first), it must implement the **full v1 registry contract**, not
85
- just `version` and `add`. pi-telegram's polling runtime calls `dispatch` on
86
- whatever object it finds at `globalThis.__piTelegramExternalHandlerRegistry__`,
87
- so a partial object would silently break the first update.
60
+ When the layered extension prefers no `import` from `@llblab/pi-telegram`, so load order between the two extensions does not matter and either can be installed first, it must implement the **full v1 registry contract**, not just `version` and `add`. pi-telegram's polling runtime calls `dispatch` on whatever object it finds at `globalThis.__piTelegramExternalHandlerRegistry__`, so a partial object would silently break the first update.
88
61
 
89
- pi-telegram defensively re-creates the registry if the object on `globalThis`
90
- is missing `add` or `dispatch` (validated as `version === 1`,
91
- `typeof add === "function"`, `typeof dispatch === "function"`). Handlers
92
- registered against a malformed object are dropped — make sure your bootstrap
93
- implements all three fields.
62
+ pi-telegram defensively re-creates the registry if the object on `globalThis` is missing `add` or `dispatch`, validated as `version === 1`, `typeof add === "function"`, and `typeof dispatch === "function"`. Handlers registered against a malformed object are dropped — make sure your bootstrap implements all three fields.
94
63
 
95
64
  ```ts
96
65
  type PiTelegramVerdict =
@@ -151,43 +120,32 @@ const off = getOrCreateRegistry().add((update) => {
151
120
  });
152
121
  ```
153
122
 
154
- The registry object on `globalThis.__piTelegramExternalHandlerRegistry__` is
155
- versioned (`version: 1`) and stable across pi-telegram releases; future
156
- breaking changes will use a new schema version and a new key.
123
+ The registry object on `globalThis.__piTelegramExternalHandlerRegistry__` is versioned (`version: 1`) and stable across pi-telegram releases; future breaking changes will use a new schema version and a new key.
157
124
 
158
125
  ## Interaction with built-in routing
159
126
 
160
- `pi-telegram` invokes registered interceptors first, then routes the update
161
- through its own handlers (commands, app menu, queue menu, model menu,
162
- default prompt routing, callback namespace fallback). If any interceptor
163
- returns `"consume"`, `pi-telegram` skips the rest of routing for that update.
127
+ `pi-telegram` invokes registered interceptors first, then routes the update through its own handlers: commands, app menu, queue menu, model menu, default prompt routing, and callback namespace fallback. If any interceptor returns `"consume"`, `pi-telegram` skips the rest of routing for that update.
164
128
 
165
129
  This means:
166
130
 
167
- - Extensions can claim callback namespaces that `pi-telegram` would
168
- otherwise forward as `[callback] <data>` text.
169
- - Extensions can observe (but not consume) updates by always returning
170
- `"pass"`.
171
- - Extensions must not consume updates that belong to `pi-telegram`'s own
172
- prefixes (`tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`)
173
- unless they are deliberately replacing that behavior.
131
+ - Extensions can claim callback namespaces that `pi-telegram` would otherwise forward as `[callback] <data>` text.
132
+ - Extensions can observe updates by always returning `"pass"`.
133
+ - Extensions must not consume updates that belong to `pi-telegram`'s own prefixes (`tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`) unless they are deliberately replacing that behavior.
174
134
 
175
135
  ## Ownership semantics
176
136
 
177
- The interceptor registry is ownership-agnostic and does not interact with
178
- the `locks.json` singleton lock documented in [Locks](./locks.md). When the
179
- locked polling runtime stops `pi-telegram`'s poller (for example, after
180
- ownership is moved to another pi process), interceptors stop receiving
181
- updates because no updates are being fetched. They are not unregistered.
137
+ The interceptor registry is ownership-agnostic and does not interact with the `locks.json` singleton lock documented in [Locks](./locks.md). When the locked polling runtime stops `pi-telegram`'s poller, for example after ownership is moved to another pi process, interceptors stop receiving updates because no updates are being fetched. They are not unregistered.
182
138
 
183
- If a layered extension needs to react to ownership changes, it should
184
- observe `pi-telegram` lifecycle events through the standard pi extension
185
- hooks rather than through the interceptor registry.
139
+ If a layered extension needs to react to ownership changes, it should observe `pi-telegram` lifecycle events through the standard pi extension hooks rather than through the interceptor registry.
186
140
 
187
141
  ## Not a multiplexer
188
142
 
189
- This registry does not multiplex one bot across multiple pi processes, and
190
- it does not bypass Telegram's single-poller-per-bot constraint. To run
191
- multiple pi instances on Telegram, give each instance its own bot and its
192
- own `~/.pi/agent` directory; the registry is for layered extensions inside
193
- **one** pi process.
143
+ This registry does not multiplex one bot across multiple pi processes, and it does not bypass Telegram's single-poller-per-bot constraint. To run multiple pi instances on Telegram, give each instance its own bot and its own `~/.pi/agent` directory; the registry is for layered extensions inside **one** pi process.
144
+
145
+ ## Relationship to extension sections
146
+
147
+ External handlers are the raw update primitive. Extension sections are the structured Telegram UI layer above that primitive.
148
+
149
+ Use external handlers for immediate update interception, custom callback namespaces, out-of-band Promise resolution, and update types that should not become a Telegram menu surface.
150
+
151
+ Use extension sections when the desired behavior is a menu-integrated UI: `render(ctx)`, managed callback dispatch, safe runtime ports, stale-callback handling, and diagnostics owned by `pi-telegram`.
@@ -71,12 +71,12 @@ Media/file handlers keep the legacy attachment-handler behavior: downloaded file
71
71
 
72
72
  Built-in placeholders for media/file handlers:
73
73
 
74
- | Placeholder | Value |
75
- | ----------- | ---------------------------------------------------------------- |
76
- | `{file}` | Full local path to the downloaded file |
77
- | `{mime}` | MIME type if known |
78
- | `{type}` | Attachment kind such as `voice`, `audio`, `document`, or `photo` |
79
- | `{text}` | Empty string |
74
+ | Placeholder | Value |
75
+ | ----------- | ---------------------------------------------- |
76
+ | `{file}` | Downloaded file path |
77
+ | `{mime}` | MIME type if known |
78
+ | `{type}` | Kind: `voice`, `audio`, `document`, or `photo` |
79
+ | `{text}` | Empty string |
80
80
 
81
81
  If a top-level one-step media handler template has no `{file}` placeholder, the downloaded file path is appended as the last command arg as a one-step handler convenience. Composition steps are plain command templates and do not receive implicit file-path args; include `{file}` explicitly where needed.
82
82
 
@@ -10,11 +10,11 @@ This document is the local outbound adaptation of the portable [Command Template
10
10
 
11
11
  An outbound handler is selected by `type`. Text replies and assistant markup map to handler types:
12
12
 
13
- | Source | Handler type | Telegram action |
14
- | ----------------- | ------------ | -------------------------------------------------- |
15
- | Final text reply | `text` | Transform text/Markdown before Telegram rendering |
16
- | `telegram_voice` | `voice` | Generate OGG/Opus and call `sendVoice` |
17
- | `telegram_button` | Built-in | Attach an inline keyboard button to the final text |
13
+ | Source | Type | Action |
14
+ | --- | --- | --- |
15
+ | Final text | `text` | Transform before render |
16
+ | `telegram_voice` | `voice` | Generate OGG/Opus |
17
+ | `telegram_button` | Built-in | Attach inline button |
18
18
 
19
19
  Configured command-template handlers provide `template`. A string is one command; an array is ordered composition. Top-level `args` and `defaults` apply to all composed steps unless a step defines private values. The command-template default timeout applies automatically. `output` selects the primary artifact path when the handler produces a file instead of stdout text. Legacy configs may still use `pipe`, but `template: [...]` is the preferred standard shape.
20
20
 
@@ -96,13 +96,13 @@ The bridge strips the comment from Telegram text. On `agent_end`, it maps each `
96
96
 
97
97
  Voice outbound handlers receive these runtime placeholders:
98
98
 
99
- | Placeholder | Value |
100
- | ----------- | -------------------------------------------------------- |
101
- | `{text}` | Voice text from body, `text="..."`, or colon shorthand |
102
- | `{lang}` | Optional markup override such as `lang=ru` |
103
- | `{rate}` | Optional markup override such as `rate=+30%` |
104
- | `{mp3}` | Flat temp artifact path under `~/.pi/agent/tmp/telegram` |
105
- | `{ogg}` | Flat temp artifact path under `~/.pi/agent/tmp/telegram` |
99
+ | Placeholder | Value |
100
+ | --- | --- |
101
+ | `{text}` | Voice text from body, attr, or colon form |
102
+ | `{lang}` | Optional override, e.g. `lang=ru` |
103
+ | `{rate}` | Optional override, e.g. `rate=+30%` |
104
+ | `{mp3}` | Temp MP3 path under agent temp |
105
+ | `{ogg}` | Temp OGG path under agent temp |
106
106
 
107
107
  Temp artifacts use unique flat names such as `<uuid>-voice.mp3` and `<uuid>-voice.ogg`. The bridge does not create per-handler directory trees.
108
108
 
package/index.ts CHANGED
@@ -212,6 +212,7 @@ export default function (pi: Pi.ExtensionAPI) {
212
212
  sendMessage,
213
213
  editMessageText: editTelegramMessageText,
214
214
  canSend: lockOwnershipGuard.ownsCurrentProcess,
215
+ recordRuntimeEvent,
215
216
  ...replyTransport,
216
217
  });
217
218
  const { finalizeMarkdownPreview } =
@@ -111,7 +111,6 @@ export function buildProactivePushSettingsText(): string {
111
111
  PROACTIVE_PUSH_SETTINGS_TITLE,
112
112
  "",
113
113
  "Send successful local π task results to Telegram when the bridge is connected.",
114
- "Default: off. Persists until disabled or removed from config.",
115
114
  ].join("\n");
116
115
  }
117
116
 
package/lib/preview.ts CHANGED
@@ -90,6 +90,11 @@ export interface TelegramPreviewRuntimeDeps<
90
90
  options?: { replyMarkup?: TReplyMarkup },
91
91
  ) => Promise<number | undefined>;
92
92
  canSend?: () => boolean;
93
+ recordRuntimeEvent?: (
94
+ category: string,
95
+ error: unknown,
96
+ details?: Record<string, unknown>,
97
+ ) => void;
93
98
  }
94
99
 
95
100
  export interface TelegramPreviewActiveTurn {
@@ -191,6 +196,11 @@ export interface TelegramPreviewControllerDeps<
191
196
  ms: number,
192
197
  ) => ReturnType<typeof setTimeout>;
193
198
  clearTimer?: (timer: ReturnType<typeof setTimeout>) => void;
199
+ recordRuntimeEvent?: (
200
+ category: string,
201
+ error: unknown,
202
+ details?: Record<string, unknown>,
203
+ ) => void;
194
204
  }
195
205
 
196
206
  export interface TelegramPreviewController<
@@ -421,6 +431,7 @@ export function createTelegramPreviewController<
421
431
  ),
422
432
  editRenderedMessage: deps.editRenderedMessage,
423
433
  canSend: deps.canSend,
434
+ recordRuntimeEvent: deps.recordRuntimeEvent,
424
435
  });
425
436
  return {
426
437
  getState: () => state,
@@ -645,7 +656,16 @@ export async function flushTelegramPreview<
645
656
  state.flushPromise = (async () => {
646
657
  do {
647
658
  state.flushRequested = false;
648
- await performTelegramPreviewFlush(chatId, state, deps);
659
+ try {
660
+ await performTelegramPreviewFlush(chatId, state, deps);
661
+ } catch (error) {
662
+ deps.recordRuntimeEvent?.("preview", error, {
663
+ phase: "flush",
664
+ chatId,
665
+ messageId: state.messageId,
666
+ });
667
+ break;
668
+ }
649
669
  } while (deps.getState() === state && state.flushRequested);
650
670
  })();
651
671
  try {
@@ -704,13 +724,22 @@ export async function finalizeTelegramMarkdownPreview<
704
724
  await clearTelegramPreview(chatId, deps);
705
725
  return false;
706
726
  }
707
- if (state.mode === "draft") {
708
- await deps.sendRenderedChunks(chatId, chunks, options);
709
- await clearTelegramPreview(chatId, deps);
727
+ try {
728
+ if (state.mode === "draft") {
729
+ await deps.sendRenderedChunks(chatId, chunks, options);
730
+ await clearTelegramPreview(chatId, deps);
731
+ return true;
732
+ }
733
+ if (state.messageId === undefined) return false;
734
+ await deps.editRenderedMessage(chatId, state.messageId, chunks, options);
735
+ deps.setState(undefined);
710
736
  return true;
737
+ } catch (error) {
738
+ deps.recordRuntimeEvent?.("preview", error, {
739
+ phase: "finalize-markdown",
740
+ chatId,
741
+ messageId: state.messageId,
742
+ });
743
+ return false;
711
744
  }
712
- if (state.messageId === undefined) return false;
713
- await deps.editRenderedMessage(chatId, state.messageId, chunks, options);
714
- deps.setState(undefined);
715
- return true;
716
745
  }
package/lib/queue.ts CHANGED
@@ -1023,20 +1023,28 @@ export async function handleTelegramAgentEndRuntime<
1023
1023
  if (finalText) deps.setPreviewPendingText(finalText);
1024
1024
  if (!finalText && hasOutboundArtifacts) await deps.clearPreview(turn.chatId);
1025
1025
  if (endPlan.kind === "text" && finalText) {
1026
- const finalized = await deps.finalizeMarkdownPreview(
1027
- turn.chatId,
1028
- finalText,
1029
- turn.replyToMessageId,
1030
- { replyMarkup },
1031
- );
1032
- if (!finalized) {
1033
- await deps.clearPreview(turn.chatId);
1034
- await deps.sendMarkdownReply(
1026
+ try {
1027
+ const finalized = await deps.finalizeMarkdownPreview(
1035
1028
  turn.chatId,
1036
- turn.replyToMessageId,
1037
1029
  finalText,
1030
+ turn.replyToMessageId,
1038
1031
  { replyMarkup },
1039
1032
  );
1033
+ if (!finalized) {
1034
+ await deps.clearPreview(turn.chatId);
1035
+ await deps.sendMarkdownReply(
1036
+ turn.chatId,
1037
+ turn.replyToMessageId,
1038
+ finalText,
1039
+ { replyMarkup },
1040
+ );
1041
+ }
1042
+ } catch (error) {
1043
+ deps.recordRuntimeEvent?.("delivery", error, {
1044
+ phase: "final-text",
1045
+ chatId: turn.chatId,
1046
+ replyToMessageId: turn.replyToMessageId,
1047
+ });
1040
1048
  }
1041
1049
  }
1042
1050
  if (outboundReply && deps.sendOutboundReplyArtifacts) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.9.4",
3
+ "version": "0.9.5",
4
4
  "private": false,
5
5
  "description": "Better Telegram DM bridge extension for π",
6
6
  "type": "module",