@llblab/pi-telegram 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,7 +13,7 @@ This repository is an actively maintained fork of [`badlogic/pi-telegram`](https
13
13
 
14
14
  ## Key Features
15
15
 
16
- - **Priority Command Queue**: Control commands such as `/status` and `/model` use a high-priority control queue, so they do not get stuck behind normal queued prompts when pi is busy.
16
+ - **Immediate Telegram Controls**: `/status` and `/model` respond immediately from Telegram, while model-switch continuation turns still use the control lane when a restart needs to resume safely.
17
17
  - **Interactive UI**: Manage your session directly from Telegram. Inline buttons allow you to switch models and adjust reasoning (thinking) levels on the fly.
18
18
  - **In-flight Model Switching**: Change the active model mid-generation. The agent gracefully pauses, applies the new model, and restarts its response without losing context.
19
19
  - **Smart Message Queue**: Messages sent while the agent is busy are queued and previewed in the pi status bar, and queued turns can be reprioritized or removed with Telegram reactions.
@@ -84,7 +84,7 @@ Use these inside the Telegram DM with your bot:
84
84
  - **`/compact`**: Start session compaction (only works when the session is idle).
85
85
  - **`/stop`**: Abort the active run.
86
86
 
87
- Telegram command admission is explicit: `/compact`, `/stop`, `/help`, and `/start` execute immediately; `/status` and `/model` enter the high-priority control lane so they can run before normal queued prompts when pi becomes safe to dispatch.
87
+ Telegram command admission is explicit: `/compact`, `/stop`, `/help`, `/start`, `/status`, and `/model` execute immediately. Synthetic model-switch continuation turns still enter the high-priority control lane so they can resume before normal queued prompts when pi becomes safe to dispatch.
88
88
 
89
89
  ### Pi Commands
90
90
 
@@ -100,32 +100,41 @@ Run these inside pi, not Telegram:
100
100
  - If you send more Telegram messages while pi is busy, they enter the default prompt queue and are processed in order.
101
101
  - `👍` moves a waiting prompt into the priority prompt queue, behind control actions but ahead of default prompts. Removing `👍` sends it back to its normal queue position, and adding `👍` again gives it a fresh priority position.
102
102
  - `👎` removes a waiting turn from the queue. Telegram Bot API does not expose ordinary DM message-deletion events through the polling path used here, so queue removal is bound to the dislike reaction.
103
- - For media groups, a reaction on any message in the group applies to the whole queued turn.
103
+ - Reactions apply to any waiting Telegram turn, including text, voice, files, images, and media groups. For media groups, a reaction on any message in the group applies to the whole queued turn.
104
104
  - If you edit a Telegram message while it is still waiting in the queue, the queued turn is updated instead of creating a duplicate prompt. Edits after a turn has already started may not affect the active run.
105
105
  - Inbound images, albums, and files are saved to `~/.pi/agent/tmp/telegram`. Unhandled local file paths are included in the prompt, handled attachment output is injected into the prompt text, and inbound images are forwarded to pi as image inputs. Inbound downloads default to a 50 MiB limit and can be adjusted with `PI_TELEGRAM_INBOUND_FILE_MAX_BYTES` or `TELEGRAM_MAX_FILE_SIZE_BYTES`.
106
106
  - Queue reactions depend on Telegram delivering `message_reaction` updates for your bot and chat type.
107
107
 
108
108
  ### Inbound Attachment Handlers
109
109
 
110
- `telegram.json` can define `attachmentHandlers` for common preprocessing such as voice transcription. The first matching handler runs after download and before the Telegram turn enters the pi queue.
110
+ `telegram.json` can define ordered `attachmentHandlers` for common preprocessing such as voice transcription. Matching handlers run after download and before the Telegram turn enters the pi queue. If a matching handler fails, the next matching handler is tried as a fallback.
111
111
 
112
112
  ```json
113
113
  {
114
114
  "attachmentHandlers": [
115
115
  {
116
- "mime": "audio/*",
117
- "command": "/home/me/bin/transcribe {filename} ru"
116
+ "type": "voice",
117
+ "template": "~/.pi/agent/skills/mistral-stt/scripts/transcribe.mjs {file} {lang} {model}",
118
+ "args": ["file", "lang", "model"],
119
+ "defaults": {
120
+ "lang": "ru",
121
+ "model": "voxtral-mini-latest"
122
+ }
118
123
  },
119
124
  {
120
- "type": "voice",
121
- "tool": "transcribe_groq",
122
- "args": { "lang": "ru" }
125
+ "mime": "audio/*",
126
+ "template": "~/.pi/agent/skills/groq-stt/scripts/transcribe.mjs {file} {lang} {model}",
127
+ "args": ["file", "lang", "model"],
128
+ "defaults": {
129
+ "lang": "ru",
130
+ "model": "whisper-large-v3-turbo"
131
+ }
123
132
  }
124
133
  ]
125
134
  }
126
135
  ```
127
136
 
128
- Matching supports `mime`, `type`, or `match`; wildcards like `audio/*` are accepted. Command placeholders are substituted as argv, not shell text: `{filename}` and `{path}` are the downloaded file path, `{basename}` is the display filename, `{mime}` is the MIME type, and `{type}` is the Telegram attachment type. Tool handlers invoke a locally available tool by name, for example `transcribe_groq`; how that tool is provided is outside pi-telegram. Local attachments stay in the prompt under `[attachments] <directory>` with relative file entries; successful handler stdout is added under `[outputs]`; failed or empty handlers simply produce no output block.
137
+ Matching supports `mime`, `type`, or `match`; wildcards like `audio/*` are accepted. Template placeholders are substituted into command args, not shell text: `{file}` is the downloaded file path, `{mime}` is the MIME type, `{type}` is the Telegram attachment type, and `defaults` can provide additional values such as `{lang}` or `{model}`. Local attachments stay in the prompt under `[attachments] <directory>` with relative file entries; successful handler stdout is added under `[outputs]`; failed handlers record diagnostics and fall back to the next matching handler. The portable command-template contract is documented in [`docs/command-templates.md`](./docs/command-templates.md); Telegram-specific handler config is documented in [`docs/attachment-handlers.md`](./docs/attachment-handlers.md).
129
138
 
130
139
  ### Requesting Files
131
140
 
@@ -139,10 +148,10 @@ Rich previews are sent through editable messages because Telegram drafts are tex
139
148
 
140
149
  ## Status bar
141
150
 
142
- The pi status bar shows queued Telegram turns as compact previews, for example:
151
+ The pi status bar shows the current bridge state plus queued Telegram turns as compact previews. Busy labels distinguish states such as `active`, `dispatching`, `queued`, `tool running`, `model`, and `compacting`.
143
152
 
144
153
  ```text
145
- +3: [⬆ write a shell script…, summarize this image…, 📎 2 attachments]
154
+ telegram queued +3: [⬆ write a shell script…, summarize this image…, 📎 2 attachments]
146
155
  ```
147
156
 
148
157
  ## Notes
package/docs/README.md CHANGED
@@ -5,4 +5,6 @@ Living index of project documentation in `/docs`.
5
5
  ## Documents
6
6
 
7
7
  - [architecture.md](./architecture.md) — Overview of the Telegram bridge runtime, queueing model, rendering pipeline, and interactive controls
8
+ - [command-templates.md](./command-templates.md) — Portable command-template standard core
9
+ - [attachment-handlers.md](./attachment-handlers.md) — Local `pi-telegram` attachment-handler config, placeholders, and fallbacks
8
10
  - [locks.md](./locks.md) — Shared `locks.json` standard for singleton extension ownership
@@ -21,37 +21,33 @@ Interface consistency rule: when two modules mean the same runtime entity, they
21
21
 
22
22
  Naming rule: because the repository already scopes this codebase to Telegram, extracted module and test filenames use bare domain names such as `api.ts`, `queue.ts`, `updates.ts`, and `queue.test.ts` rather than repeating `telegram-*` in every filename.
23
23
 
24
- Current runtime areas include:
25
-
26
- - Telegram Bot API concrete transport shapes live with Telegram API helpers in `/lib/api.ts`, while persisted bot/session pairing state lives in `/lib/config.ts`; domain-owned runtime state types stay with their owners, such as queued/active turn state in `/lib/queue.ts` and preview state in `/lib/preview.ts`, while domain helpers prefer local structural `*Like` contracts instead of importing concrete wire DTOs
27
- - Direct pi SDK imports are centralized in `/lib/pi.ts`, which exposes concrete pi SDK type exports, bound extension API runtime ports, and narrow bridge-facing helpers such as settings-manager creation plus context model/idle/pending-message/compaction adapters; `index.ts` uses this adapter namespace instead of importing `@mariozechner/pi-coding-agent` directly
28
- - Session-local runtime primitives such as queue/control/priority ordering counters, lifecycle/dispatch flags, setup guard state, abort-handler storage/binding, typing-loop timer lifecycle, typing-loop starter binding, prompt-dispatch lifecycle/runtime adapters, and agent-end reset sequencing in `/lib/runtime.ts`; the runtime domain's essence is mutable cross-domain session coordination rather than business behavior. It exposes a grouped bridge runtime facade with named queue/lifecycle/setup/abort/typing ports that bind those primitives to one session state while remaining a cohesive state/runtime boundary, and `index.ts` still wires live Telegram API calls and status updates into those helpers. Preview-specific state, draft-support detection, and draft-id allocation live in `/lib/preview.ts`.
29
- - Constants live in their owning domains instead of a shared constants module: API paths/inbound limits and inbound file-size env parsing in `/lib/api.ts`, outbound attachment limits and outbound attachment-size env parsing in `/lib/attachments.ts`, media-group debounce in `/lib/media.ts`, attachment-handler timeout and local tool lookup defaults in `/lib/handlers.ts`, menu cache/state bounds in `/lib/menu.ts`, preview throttle/draft bounds in `/lib/preview.ts`, typing cadence in `/lib/runtime.ts`, diagnostic ring limits in `/lib/status.ts`, Telegram prompt prefix in `/lib/turns.ts`, and system-prompt guidance in `/lib/registration.ts`.
30
- - Queueing, narrow Telegram prompt content contracts, queue-store contracts/state helpers, active-turn state helpers, dispatch-readiness adapters, queue append/mutation runtime/controller adapters, control enqueue controllers, queue dispatch readiness/controller/runtime adapters, prompt enqueue/history planning/runtime/controllers, queue-runtime, session state appliers plus lifecycle/runtime sequencing, session start/shutdown sequencing plus hook binding, agent-start/agent-end lifecycle handling plus hook/runtime binding, and tool lifecycle handling plus tool-execution hook/runtime binding in `/lib/queue.ts`
31
- - Model identity/thinking-level contracts, scoped model-pattern parsing/resolution/sorting, current-model store/update/runtime helpers, in-flight model-switch state helpers, restart eligibility, delayed abort decisions, Telegram-prefix defaulted continuation prompt construction, continuation queue adapters, and model-switch controller/runtime binding over queue-owned turns in `/lib/model.ts`
32
- - Preview transport-selection, assistant-message preview lifecycle hook binding/handling, preview-finalization, preview controller state/reset helpers, preview Bot API message/rendered-chunk transport adapters, preview-controller/assistant-preview runtime binding, reply-metadata defaulting through the replies-domain helper, and preview-runtime helpers in `/lib/preview.ts`
33
- - Reply-transport, rendered-message delivery runtime/binding, structural assistant-message extraction, reply-parameter construction over API-owned transport shapes, and plain/Markdown final-reply helpers in `/lib/replies.ts`
34
- - Preview appearance and snapshot derivation stay in `/lib/rendering.ts`, while `/lib/preview.ts` owns transport and lifecycle decisions, so richer preview strategies can evolve without entangling Markdown formatting with Telegram delivery state
35
- - Polling request, start/stop controller state orchestration, polling activity readers, stop-condition, structural config contract, long-poll loop helpers, and poll-loop/controller runtime wiring over Telegram transport ports in `/lib/polling.ts`
36
- - Telegram persisted config shape, config-path defaults, config file read/write helpers, mutable config-store accessors, single-user authorization, and first-user pairing side effects/runtime adapters in `/lib/config.ts`
37
- - Telegram API helpers, concrete Bot API transport shapes including reply parameters and send/edit message bodies, typed/default Bot API runtime helpers, bot identity fetch transport, chat-action sender adapters/runtime-bound typing action, lazy bot-token client wrappers, API runtime error-recording wrappers, temp paths, inbound file-size limits, and runtime-bound temp-directory preparation/default cleanup in `/lib/api.ts`
38
- - Telegram turn-building helpers, runtime turn-builder wiring over media download ports and media-owned downloaded-file metadata contracts, inbound attachment handler processing, queued-prompt edit runtime binding, and Node-backed image-file reads for pi image inputs in `/lib/turns.ts`
39
- - Telegram media/text extraction, file-info normalization, downloaded-message-file metadata contracts, inbound file download assembly, media-group debounce helpers, media-group controller state, and media-group-aware authorized-message dispatch adapter wiring in `/lib/media.ts`
40
- - Telegram inbound attachment handler matching, command placeholder substitution, local tool invocation, handler-output collection, and quiet failure handling in `/lib/handlers.ts`
41
- - Telegram slash-command parsing, command-message target helpers/adapters, command control-enqueue adapters/runtime binding, command-action routing, command-handler/target-runtime and command-or-prompt dispatch binding, command runtime port orchestration, shared command-runtime reply/status/control adapter closures, stop/compact/status/model/help command side-effect branching, bound Bot API command registration, and Bot API command metadata helpers in `/lib/commands.ts`
42
- - Telegram updates extraction, authorization classification, execution-planning, authorized reaction priority/removal handling, direct execute-from-update routing, and runtime helpers in `/lib/updates.ts`
43
- - Inbound route composition across paired update execution, callback menus, command-or-prompt dispatch, media grouping, prompt enqueueing, queued edits, and attachment-handler turn building in `/lib/routing.ts`
44
- - Telegram attachment queueing, narrow structural attachment turn targets, queued-attachment sender runtime binding, delivery helpers, Node-backed file stat checks, outbound photo-vs-document classification, and outbound attachment limits/env parsing in `/lib/attachments.ts`
45
- - Telegram tool, command, before-agent prompt, and lifecycle-hook registration helpers in `/lib/registration.ts`
46
- - Setup/token prompt, environment fallback, guarded setup runtime adapter wiring, structural setup config contract, token validation, config persistence orchestration, and setup notification helpers in `/lib/setup.ts`
47
- - Markdown block scanning/rendering, inline-token/style rendering, text-piece rendering, stable-preview block scanning, final rendered-block chunk balancing, preview-snapshot derivation, HTML escaping, raw HTML tag-preserving chunking, and Telegram message rendering helpers in `/lib/rendering.ts`
48
- - Status-bar rendering/runtime adapters, bridge status state adapters, status-message rendering and status-HTML binding, structural queue-lane status view contracts, structured redacted runtime-event recording, recent-event recorder state, recent-event line formatting, and grouped pi-side diagnostics helpers in `/lib/status.ts`
49
- - Menu settings/model-registry access through structural ports, menu-state construction, menu runtime state/cache controller, menu-state storage pruning/refresh helpers, command open-flow branching, action runtime/state-builder adapters, menu callback handler adapters, stored callback entry/runtime routing, model-menu input-cache/state-building resolution, pure menu-page derivation, pure menu render-payload builders, menu-message runtime, callback parsing, callback mutation helpers, full model-callback planning and execution, interface-polished callback effect ports, status-thinking callback handling, and UI helpers in `/lib/menu.ts`
50
- - Telegram API-bound transport adapters and top-level runtime registration stay in `index.ts`; broader inbound event-side orchestration lives in `/lib/routing.ts`; direct Node file-operation imports stay in the owning domains rather than the entrypoint
51
- - Remaining `index.ts` wiring is intentionally cross-domain adapter code that closes over live extension state, pi callbacks, Telegram API ports, and status updates; keep repeated wiring DRY through small local adapter helpers or owning-domain contracts when that reduces duplication without obscuring live state, and extract more only when a boundary can move cohesive behavior into an owning domain instead of relocating one-off closures
52
- - Additional domains can be extracted into `/lib/*.ts` as the bridge grows, while keeping `index.ts` as the single entrypoint
53
- - `index.ts` uses namespace imports for local bridge domains so orchestration reads as domain-scoped calls such as `Queue.*`, `Turns.*`, and `Rendering.*` instead of long flat import lists
54
- - Mirrored domain regression coverage lives in `/tests/*.test.ts` using the same bare domain naming scheme, and architecture-invariant coverage in `/tests/invariants.test.ts` checks that the local `index.ts` plus `/lib/*.ts` import graph stays acyclic, shared bucket domains such as `lib/constants.ts` or `lib/types.ts` are not reintroduced, empty interface-extension shells stay collapsed into clearer type aliases, direct pi SDK imports stay centralized, `index.ts` source code stays free of direct Node runtime imports, local helper declarations, local arrow adapters, direct `process.env`, and direct `pi.*` receiver access, `/lib/runtime.ts` stays free of local domain imports, structural leaf domains stay free of local nominal imports, the menu domain stays on structural ports without re-exporting model, API transport stays decoupled from persisted config defaults, structural update/media domains stay decoupled from concrete API transport shapes, and attachment delivery stays decoupled from queue/inbound media/API helpers
24
+ Current runtime areas use these ownership boundaries:
25
+
26
+ | Domain | Owns |
27
+ | ------ | ---- |
28
+ | `index.ts` | Single composition root for live pi/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` / `turns` / `handlers` | Text/media extraction, media-group debounce, inbound downloads, turn building/editing, image reads, attachment-handler matching/execution/fallback output |
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` / `commands` | Model identity/thinking levels, scoped model resolution, in-flight switching, inline status/model/thinking UI, slash commands, bot command registration |
37
+ | `preview` / `replies` / `rendering` | Preview lifecycle/transports, final reply delivery and reply parameters, Telegram HTML Markdown rendering, chunking, stable-preview snapshots |
38
+ | `attachments` | `telegram_attach` registration, outbound attachment queueing, stat/limit checks, photo/document delivery classification |
39
+ | `status` | Status-bar/status-message rendering, queue-lane status views, redacted runtime event ring, grouped pi diagnostics |
40
+ | `lifecycle` / `prompts` / `pi` | pi hook registration, Telegram-specific before-agent prompt injection, centralized direct pi SDK imports and context adapters |
41
+
42
+ Boundary invariants:
43
+
44
+ - Constants and state types live with their owning domains; do not reintroduce shared buckets such as `lib/constants.ts` or `lib/types.ts`
45
+ - Domain helpers use narrow structural projections when that avoids importing concrete wire DTOs or broader runtime objects unnecessarily
46
+ - Preview appearance stays in `rendering`; preview transport/lifecycle stays in `preview`
47
+ - Direct `node:*` file-operation imports stay in owning domains, not in `index.ts`
48
+ - `index.ts` uses namespace imports for local bridge domains so orchestration reads as `Queue.*`, `Turns.*`, and `Rendering.*`
49
+ - Architecture-invariant tests guard the acyclic import graph, pi SDK centralization, entrypoint purity, runtime-domain isolation, structural leaf-domain isolation, menu/model boundaries, API/config separation, media/update/API separation, and attachment boundary isolation
50
+ - Mirrored domain regression coverage lives in `/tests/*.test.ts`; test helpers stay local to the mirrored suite by default, and shared fixture folders are justified only by reuse across multiple domain suites
55
51
 
56
52
  ## Configuration UX
57
53
 
@@ -76,11 +72,12 @@ Telegram bot configuration stays in `~/.pi/agent/telegram.json`; singleton runti
76
72
  3. The bridge filters to the paired private user
77
73
  4. Media groups are coalesced into a single Telegram turn when needed
78
74
  5. Files are streamed into `~/.pi/agent/tmp/telegram` with a default 50 MiB size limit, partial-download cleanup on failures, and stale temp cleanup on session start; operators can tune the limit with `PI_TELEGRAM_INBOUND_FILE_MAX_BYTES` or `TELEGRAM_MAX_FILE_SIZE_BYTES`
79
- 6. Configured inbound attachment handlers may run on downloaded files by MIME wildcard, Telegram attachment type, or generic match selector; command handlers receive safe argv substitution for `{filename}`/`{path}`/`{basename}`/`{mime}`/`{type}`, and tool handlers invoke locally available tools by name
80
- 7. Local attachments stay visible under `[attachments] <directory>` with relative file entries, and handler stdout is appended under `[outputs]` before the agent sees the turn; failed or empty handlers simply omit handler output while keeping the attachment entry
81
- 8. A `PendingTelegramTurn` is created and queued locally
82
- 9. Telegram `edited_message` updates are routed separately and update a matching queued turn when the original message has not been dispatched yet
83
- 10. The queue dispatcher sends the turn into pi only when dispatch is safe
75
+ 6. Configured inbound attachment handlers may run on downloaded files by MIME wildcard, Telegram attachment type, or generic match selector; command templates receive safe command-arg substitution for `{file}`/`{mime}`/`{type}`
76
+ 7. Matching handlers are tried in config order: a non-zero exit records diagnostics and falls back to the next matching handler, while the first successful handler stops the chain
77
+ 8. Local attachments stay visible under `[attachments] <directory>` with relative file entries, and handler stdout is appended under `[outputs]` before the agent sees the turn; failed handlers omit output while keeping the attachment entry
78
+ 9. A `PendingTelegramTurn` is created and queued locally
79
+ 10. Telegram `edited_message` updates are routed separately and update a matching queued turn when the original message has not been dispatched yet
80
+ 11. The queue dispatcher sends the turn into pi only when dispatch is safe
84
81
 
85
82
  ### Queue Safety Model
86
83
 
@@ -96,11 +93,11 @@ Admission contract:
96
93
  | Admission | Examples | Queue shape | Dispatch rank |
97
94
  | --------------------- | ---------------------------------------------------- | -------------------------------------------------------------------- | ------------- |
98
95
  | Immediate execution | `/compact`, `/stop`, `/help`, `/start` | Does not enter the Telegram queue | N/A |
99
- | Control queue | `/status`, `/model`, model-switch continuation turns | `queueLane: control`; accepts control items and continuation prompts | 0 |
96
+ | Control queue | Model-switch continuation turns and future deferred controls | `queueLane: control`; accepts control items and continuation prompts | 0 |
100
97
  | Priority prompt queue | A waiting prompt promoted by `👍` | `kind: prompt`, `queueLane: priority` | 1 |
101
98
  | Default prompt queue | Normal Telegram text/media turns | `kind: prompt`, `queueLane: default` | 2 |
102
99
 
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 pi status bar queue preview, priority prompts are marked with `⬆` while control items keep their own control-specific summary markers such as `⚡`.
100
+ 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 pi status bar, busy labels distinguish `active`, `dispatching`, `queued`, `tool running`, `model`, and `compacting`; priority prompts are marked with `⬆` while control items keep markers such as `⚡`.
104
101
 
105
102
  A dispatched prompt remains in the queue until `agent_start` consumes it. That keeps the active Telegram turn bound correctly for previews, attachments, abort handling, and final reply delivery.
106
103
 
@@ -112,7 +109,7 @@ Dispatch is gated by:
112
109
  - `ctx.isIdle()` being true
113
110
  - `ctx.hasPendingMessages()` being false
114
111
 
115
- This prevents queue races around rapid follow-ups, `/compact`, and mixed local plus Telegram activity. The dispatch controller also serializes asynchronous control items, so a queued `/status` or `/model` action must settle before the next queued action can dispatch.
112
+ This prevents queue races around rapid follow-ups, `/compact`, and mixed local plus Telegram activity. Telegram `/status` and `/model` execute immediately; the dispatch controller still serializes any deferred control items so a queued control action must settle before the next queued action can dispatch.
116
113
 
117
114
  ### Abort Behavior
118
115
 
@@ -161,13 +158,13 @@ The bridge exposes Telegram-side session controls in addition to regular chat fo
161
158
 
162
159
  Current operator controls include:
163
160
 
164
- - `/status` for model, usage, cost, and context visibility, queued as a high-priority control item when needed
161
+ - `/status` for model, usage, cost, and context visibility, executed immediately from Telegram even while generation is active
165
162
  - Inline status buttons for model and thinking adjustments, applying idle selections immediately while still respecting busy-run restart rules; model-menu inputs are cached briefly and stored inline-menu states are pruned by TTL/LRU so old keyboards expire predictably
166
- - `/model` for interactive model selection, queued as a high-priority control item when needed and supporting in-flight restart of the active Telegram-owned run on a newly selected model
163
+ - `/model` for interactive model selection, executed immediately from Telegram and supporting in-flight restart of the active Telegram-owned run on a newly selected model
167
164
  - `/compact` for Telegram-triggered pi session compaction when the bridge is idle
168
165
  - `/stop` for aborting the active Telegram-owned run
169
166
  - `/telegram-status` for pi-side diagnostics as grouped line-by-line sections separated by blank lines: connection, polling, execution, queue, and the recent redacted runtime/API event ring. These sections include polling state, last update id, active turn source ids, pending dispatch, compaction state, active tool count, pending model-switch state, total queue depth, and queue-lane counts. The event ring records transport/API, polling/update, prompt-dispatch, control-action, typing, compaction, setup, session-lifecycle, and attachment queue/delivery failures; benign unchanged edit responses and unsupported empty draft-clear attempts are filtered out so expected preview transport noise does not obscure real failures
170
- - Queue reactions using `👍` and `👎`, with `👎` acting as the canonical queue-removal path because ordinary Telegram DM message deletions are not exposed through the Bot API polling path this bridge uses
167
+ - Queue reactions using `👍` and `👎` apply to waiting text, voice, file, image, and media-group turns by matching the turn's source Telegram message ids; `👎` acts as the canonical queue-removal path because ordinary Telegram DM message deletions are not exposed through the Bot API polling path this bridge uses
171
168
 
172
169
  ## In-Flight Model Switching
173
170
 
@@ -0,0 +1,60 @@
1
+ # Attachment Handlers
2
+
3
+ `pi-telegram` can run ordered inbound attachment handlers after downloading files and before the Telegram turn enters the pi queue.
4
+
5
+ This document is the local adaptation of the portable [Command Template Standard](./command-templates.md).
6
+
7
+ ## Config Shape
8
+
9
+ `telegram.json` may define `attachmentHandlers`:
10
+
11
+ ```json
12
+ {
13
+ "attachmentHandlers": [
14
+ {
15
+ "type": "voice",
16
+ "template": "~/.pi/agent/skills/mistral-stt/scripts/transcribe.mjs {file} {lang} {model}",
17
+ "args": ["file", "lang", "model"],
18
+ "defaults": {
19
+ "lang": "ru",
20
+ "model": "voxtral-mini-latest"
21
+ }
22
+ },
23
+ {
24
+ "mime": "audio/*",
25
+ "template": "~/.pi/agent/skills/groq-stt/scripts/transcribe.mjs {file} {lang} {model}",
26
+ "args": ["file", "lang", "model"],
27
+ "defaults": {
28
+ "lang": "ru",
29
+ "model": "whisper-large-v3-turbo"
30
+ }
31
+ }
32
+ ]
33
+ }
34
+ ```
35
+
36
+ Handlers match by `type`, `mime`, or `match`. Wildcards such as `audio/*` are accepted. Each matching handler must provide a `template`; optional `args` and `defaults` document or fill placeholder values.
37
+
38
+ ## Template Placeholders
39
+
40
+ Attachment handlers support these built-in placeholders:
41
+
42
+ | Placeholder | Value |
43
+ | ----------- | ---------------------------------------------------------------- |
44
+ | `{file}` | Full local path to the downloaded file |
45
+ | `{mime}` | MIME type if known |
46
+ | `{type}` | Attachment kind such as `voice`, `audio`, `document`, or `photo` |
47
+
48
+ `defaults` may provide additional placeholder values such as `{lang}` or `{model}`. `args` documents supported placeholders and may also encode defaults in compact form, for example `"file,lang=ru,model=voxtral-mini-latest"`.
49
+
50
+ If a template has no `{file}` placeholder, the downloaded file path is appended as the last command arg.
51
+
52
+ ## Ordered Fallbacks
53
+
54
+ A handler list is ordered. For each attachment, matching handlers run in list order and stop after the first successful handler.
55
+
56
+ If a matching handler fails with a non-zero exit code, the runtime records diagnostics and tries the next matching handler. If every matching handler fails, the attachment remains visible in the prompt as a normal local file reference.
57
+
58
+ ## Prompt Output
59
+
60
+ Local attachments stay in the prompt under `[attachments] <directory>` with relative file entries. Successful handler stdout is added under `[outputs]`. Empty output and failed handler output are omitted from the prompt text.
@@ -0,0 +1,75 @@
1
+ # Command Template Standard
2
+
3
+ Command templates are the stable integration format for deterministic local automation.
4
+
5
+ This document is the portable core. Extensions may adapt local examples, placeholder sources, and config locations, but should preserve this contract to stay compatible with the shared command-template model.
6
+
7
+ ## Definition
8
+
9
+ A command template is a single command-line string with named placeholders:
10
+
11
+ ```text
12
+ ~/bin/transcribe {file} {lang}
13
+ ```
14
+
15
+ ## Execution Contract
16
+
17
+ The runtime must:
18
+
19
+ 1. Split the template into shell-like words, honoring simple single quotes, double quotes, and backslash escapes
20
+ 2. Substitute placeholders inside each split word
21
+ 3. Execute the first word as the command and the remaining words as args
22
+ 4. Avoid evaluating the template through a shell
23
+ 5. Treat exit code `0` as success and non-zero exit as failure
24
+ 6. Use stdout as the result channel
25
+ 7. Use stderr only for diagnostics
26
+
27
+ Implementations may expand `~` in the command position and may resolve relative command paths against the caller cwd.
28
+
29
+ ## Quoting Model
30
+
31
+ Placeholder values are not shell-escaped because templates are not executed through a shell. A value containing spaces remains one command arg when it replaces one split word:
32
+
33
+ ```text
34
+ template="echo {text}"
35
+ text="hello world"
36
+ args=["hello world"]
37
+ ```
38
+
39
+ A placeholder can also be embedded inside one word:
40
+
41
+ ```text
42
+ template="tool --file={file}"
43
+ file="/tmp/a b.ogg"
44
+ args=["--file=/tmp/a b.ogg"]
45
+ ```
46
+
47
+ Use quotes only for literal template words that should contain spaces before placeholder substitution:
48
+
49
+ ```text
50
+ template="echo 'literal words' {text}"
51
+ ```
52
+
53
+ ## Storage Vocabulary
54
+
55
+ JSON storage is part of the standard vocabulary, but not one universal schema. Extensions may store command templates in different config files and surrounding shapes.
56
+
57
+ Common field names:
58
+
59
+ | Field | Meaning |
60
+ | ---------- | ------------------------------------------------------------------------------------------ |
61
+ | `template` | Command-line template string, usually attached to a named capability or handler |
62
+ | `args` | Declared placeholder names, represented as a string or array according to the local schema |
63
+ | `defaults` | Object mapping placeholder names to default values |
64
+
65
+ Config file locations, selectors, labels, descriptions, and surrounding registry shapes belong to each extension's local adaptation.
66
+
67
+ ## Tool Boundary
68
+
69
+ Agent tools are a separate abstraction. A tool name is not a portable command template because the pi extension API currently exposes tool registration and metadata, but not a public extension-to-extension `executeTool(name, args)` call.
70
+
71
+ Until such an API exists, extensions should prefer command templates for deterministic local automation.
72
+
73
+ ## Compatibility
74
+
75
+ Consumers should share this template contract, not private registry fields or implementation details from any specific extension.
package/index.ts CHANGED
@@ -5,8 +5,10 @@
5
5
 
6
6
  import * as Api from "./lib/api.ts";
7
7
  import * as Attachments from "./lib/attachments.ts";
8
+ import * as Commands from "./lib/commands.ts";
8
9
  import * as Config from "./lib/config.ts";
9
10
  import * as Handlers from "./lib/handlers.ts";
11
+ import * as Lifecycle from "./lib/lifecycle.ts";
10
12
  import * as Locks from "./lib/locks.ts";
11
13
  import * as Media from "./lib/media.ts";
12
14
  import * as Menu from "./lib/menu.ts";
@@ -14,8 +16,8 @@ import * as Model from "./lib/model.ts";
14
16
  import * as Pi from "./lib/pi.ts";
15
17
  import * as Polling from "./lib/polling.ts";
16
18
  import * as Preview from "./lib/preview.ts";
19
+ import * as Prompts from "./lib/prompts.ts";
17
20
  import * as Queue from "./lib/queue.ts";
18
- import * as Registration from "./lib/registration.ts";
19
21
  import * as Replies from "./lib/replies.ts";
20
22
  import * as Runtime from "./lib/runtime.ts";
21
23
  import * as Routing from "./lib/routing.ts";
@@ -255,7 +257,7 @@ export default function (pi: Pi.ExtensionAPI) {
255
257
  updateStatus,
256
258
  recordRuntimeEvent: runtimeEvents.record,
257
259
  });
258
- const sessionLifecycleRuntime = Registration.appendTelegramLifecycleHooks(
260
+ const sessionLifecycleRuntime = Lifecycle.appendTelegramLifecycleHooks(
259
261
  Queue.createTelegramSessionLifecycleRuntime<
260
262
  Pi.ExtensionContext,
261
263
  RuntimeTelegramQueueItem,
@@ -282,14 +284,14 @@ export default function (pi: Pi.ExtensionAPI) {
282
284
  { onSessionStart: lockedPollingRuntime.onSessionStart },
283
285
  );
284
286
 
285
- // --- Extension Registration ---
287
+ // --- Extension API Bindings ---
286
288
 
287
- Registration.registerTelegramAttachmentTool(pi, {
289
+ Attachments.registerTelegramAttachmentTool(pi, {
288
290
  getActiveTurn: activeTurnRuntime.get,
289
291
  recordRuntimeEvent: runtimeEvents.record,
290
292
  });
291
293
 
292
- Registration.registerTelegramCommands(pi, {
294
+ Commands.registerTelegramBridgeCommands(pi, {
293
295
  promptForConfig: Setup.createTelegramSetupPromptRuntime({
294
296
  getConfig: configStore.get,
295
297
  setConfig: configStore.set,
@@ -310,9 +312,9 @@ export default function (pi: Pi.ExtensionAPI) {
310
312
 
311
313
  // --- Lifecycle Hooks ---
312
314
 
313
- Registration.registerTelegramLifecycleHooks(pi, {
315
+ Lifecycle.registerTelegramLifecycleHooks(pi, {
314
316
  ...sessionLifecycleRuntime,
315
- onBeforeAgentStart: Registration.createTelegramBeforeAgentStartHook(),
317
+ onBeforeAgentStart: Prompts.createTelegramBeforeAgentStartHook(),
316
318
  onModelSelect: currentModelRuntime.onModelSelect,
317
319
  ...Queue.createTelegramAgentLifecycleHooks<
318
320
  Queue.PendingTelegramTurn,
@@ -1,13 +1,18 @@
1
1
  /**
2
2
  * Telegram attachment domain helpers
3
- * Owns attachment queueing and attachment delivery so Telegram file output stays in one domain module
3
+ * Owns telegram_attach registration, attachment queueing, and attachment delivery so Telegram file output stays in one domain module
4
4
  */
5
5
 
6
6
  import { stat } from "node:fs/promises";
7
7
  import { basename } from "node:path";
8
8
 
9
+ import { Type } from "@sinclair/typebox";
10
+
11
+ import type { ExtensionAPI } from "./pi.ts";
9
12
  import { buildTelegramMultipartReplyParameters } from "./replies.ts";
10
13
 
14
+ const MAX_ATTACHMENTS_PER_TURN = 10;
15
+
11
16
  export const TELEGRAM_OUTBOUND_ATTACHMENT_DEFAULT_MAX_BYTES = 50 * 1024 * 1024;
12
17
 
13
18
  export function getTelegramAttachmentByteLimitFromEnv(
@@ -35,6 +40,21 @@ export interface TelegramAttachmentToolResult {
35
40
  details: { paths: string[] };
36
41
  }
37
42
 
43
+ export interface TelegramAttachmentRuntimeEventRecorderPort {
44
+ recordRuntimeEvent?: (
45
+ category: string,
46
+ error: unknown,
47
+ details?: Record<string, unknown>,
48
+ ) => void;
49
+ }
50
+
51
+ export interface TelegramAttachmentToolRegistrationDeps extends TelegramAttachmentRuntimeEventRecorderPort {
52
+ maxAttachmentsPerTurn?: number;
53
+ maxAttachmentSizeBytes?: number;
54
+ getActiveTurn: () => TelegramAttachmentQueueTargetView | undefined;
55
+ statPath?: (path: string) => Promise<{ isFile(): boolean; size?: number }>;
56
+ }
57
+
38
58
  export interface TelegramQueuedAttachmentView {
39
59
  path: string;
40
60
  fileName: string;
@@ -69,6 +89,54 @@ function formatTelegramAttachmentSizeLimitError(
69
89
  return path ? `${message}: ${path}` : message;
70
90
  }
71
91
 
92
+ function formatTelegramAttachmentToolResultText(count: number): string {
93
+ // Pi's compact tool rows need an empty first line to visually separate header and result
94
+ return ["", `Queued ${count} Telegram attachment(s).`].join("\n");
95
+ }
96
+
97
+ export function registerTelegramAttachmentTool(
98
+ pi: ExtensionAPI,
99
+ deps: TelegramAttachmentToolRegistrationDeps,
100
+ ): void {
101
+ const maxAttachmentsPerTurn =
102
+ deps.maxAttachmentsPerTurn ?? MAX_ATTACHMENTS_PER_TURN;
103
+ const maxAttachmentSizeBytes =
104
+ deps.maxAttachmentSizeBytes ?? TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES;
105
+ pi.registerTool({
106
+ name: "telegram_attach",
107
+ label: "Telegram Attach",
108
+ description:
109
+ "Queue one or more local files to be sent with the next Telegram reply.",
110
+ promptSnippet: "Queue local files to be sent with the next Telegram reply.",
111
+ promptGuidelines: [
112
+ "When handling a [telegram] message and the user asked for a file or generated artifact, call telegram_attach with the local path instead of only mentioning the path in text.",
113
+ ],
114
+ parameters: Type.Object({
115
+ paths: Type.Array(
116
+ Type.String({ description: "Local file path to attach" }),
117
+ { minItems: 1, maxItems: maxAttachmentsPerTurn },
118
+ ),
119
+ }),
120
+ async execute(_toolCallId, params) {
121
+ try {
122
+ return await queueTelegramAttachments({
123
+ activeTurn: deps.getActiveTurn(),
124
+ paths: params.paths,
125
+ maxAttachmentsPerTurn,
126
+ maxAttachmentSizeBytes,
127
+ statPath: deps.statPath,
128
+ });
129
+ } catch (error) {
130
+ deps.recordRuntimeEvent?.("attachment", error, {
131
+ phase: "queue",
132
+ count: params.paths.length,
133
+ });
134
+ throw error;
135
+ }
136
+ },
137
+ });
138
+ }
139
+
72
140
  export interface TelegramQueuedAttachmentDeliveryDeps {
73
141
  sendMultipart: (
74
142
  method: string,
@@ -141,7 +209,7 @@ export async function queueTelegramAttachments(options: {
141
209
  content: [
142
210
  {
143
211
  type: "text",
144
- text: `Queued ${added.length} Telegram attachment(s).`,
212
+ text: formatTelegramAttachmentToolResultText(added.length),
145
213
  },
146
214
  ],
147
215
  details: { paths: added },