@llblab/pi-telegram 0.17.4 → 0.18.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.
Files changed (62) hide show
  1. package/AGENTS.md +67 -32
  2. package/BACKLOG.md +59 -14
  3. package/CHANGELOG.md +40 -15
  4. package/README.md +63 -35
  5. package/docs/README.md +3 -1
  6. package/docs/architecture.md +55 -23
  7. package/docs/callback-namespaces.md +1 -1
  8. package/docs/inbound.md +1 -1
  9. package/docs/locks.md +0 -2
  10. package/docs/multi-instance-bus.md +483 -0
  11. package/docs/outbound.md +4 -3
  12. package/docs/public-api.md +12 -10
  13. package/docs/sections.md +2 -2
  14. package/docs/ui-style.md +76 -0
  15. package/index.ts +789 -32
  16. package/lib/bindings.ts +68 -12
  17. package/lib/bus-api.ts +314 -0
  18. package/lib/bus-follower.ts +853 -0
  19. package/lib/bus-leader.ts +915 -0
  20. package/lib/bus.ts +866 -0
  21. package/lib/command-templates.ts +9 -11
  22. package/lib/commands.ts +133 -47
  23. package/lib/config.ts +53 -5
  24. package/lib/lifecycle.ts +23 -7
  25. package/lib/locks.ts +230 -66
  26. package/lib/media.ts +30 -2
  27. package/lib/menu-model.ts +48 -17
  28. package/lib/menu-queue.ts +51 -20
  29. package/lib/menu-settings.ts +9 -5
  30. package/lib/menu-status.ts +3 -0
  31. package/lib/menu-thinking.ts +3 -0
  32. package/lib/menu.ts +67 -26
  33. package/lib/outbound-attachments.ts +102 -17
  34. package/lib/outbound-buttons.ts +6 -2
  35. package/lib/outbound-voice.ts +31 -11
  36. package/lib/outbound.ts +6 -4
  37. package/lib/ownership.ts +119 -0
  38. package/lib/pi.ts +26 -3
  39. package/lib/polling.ts +477 -7
  40. package/lib/preview.ts +141 -88
  41. package/lib/prompt-templates.ts +3 -3
  42. package/lib/prompts.ts +80 -30
  43. package/lib/queue.ts +193 -91
  44. package/lib/rendering.ts +0 -25
  45. package/lib/replies.ts +187 -55
  46. package/lib/routing.ts +1673 -9
  47. package/lib/runtime-log.ts +123 -0
  48. package/lib/runtime.ts +84 -12
  49. package/lib/sections.ts +28 -21
  50. package/lib/setup.ts +1 -1
  51. package/lib/status.ts +532 -9
  52. package/lib/sync.ts +618 -0
  53. package/lib/target.ts +49 -0
  54. package/lib/telegram-api.ts +405 -40
  55. package/lib/text-groups.ts +5 -1
  56. package/lib/thread-reconciler.ts +915 -0
  57. package/lib/threads.ts +2205 -0
  58. package/lib/turns.ts +48 -3
  59. package/lib/updates.ts +355 -32
  60. package/package.json +24 -2
  61. package/screenshot.png +0 -0
  62. package/docs/telegram-bot-api-rich-messages.md +0 -890
@@ -0,0 +1,483 @@
1
+ # Telegram Multi-Instance Bus Architecture
2
+
3
+ ## Status
4
+
5
+ Implemented behind BotFather private-chat Threaded Mode capability. Classic/private-chat mode remains the default whenever Telegram Threads are unavailable, and live Telegram client smoke remains the release gate for client-visible thread UX.
6
+
7
+ This document uses **thread** as the canonical product term because Telegram clients present the tabbed UI as threads. The Bot API calls the underlying primitive a `Topic` / `ForumTopic`, but project language follows user-perceived client reality rather than API naming. Use **topic** only when discussing Bot API method names, service-message names, or transport-level evidence. User/operator UX and product docs should say thread.
8
+
9
+ This document supersedes the narrower API-topic framing. Telegram threads are a UI/routing substrate, but the deeper design problem is multi-instance coordination: one bot token has one Telegram API update bus, while multiple live Pi agent instances may want to expose their own Telegram workspace through that bus.
10
+
11
+ ## Problem
12
+
13
+ Classic `pi-telegram` mode binds one private Telegram DM to one live Pi instance through one bot token and one singleton polling owner. The lock currently answers: "which Pi instance owns Telegram control/polling?"
14
+
15
+ That model is safe, but it leaves concurrency on the table:
16
+
17
+ - Only one live Pi instance can receive Telegram updates for a bot token.
18
+ - Moving `/telegram-connect` changes the active Telegram control owner instead of letting several instances coexist.
19
+ - Multiple projects, tmux panes, remote workers, or long-running Pi instances require separate bot tokens or manual ownership switching.
20
+ - A thread workspace is only useful if it routes to a live agent instance, not to a dead session record that can no longer answer.
21
+
22
+ Telegram itself has one relevant constraint: for a bot token, `getUpdates` must be owned by one poller. The architecture must embrace that by electing one local Telegram bus leader and routing work to follower instances.
23
+
24
+ ## Goal
25
+
26
+ Support a Threaded Mode multi-instance runtime where:
27
+
28
+ ```text
29
+ one bot token -> one local Pi organism -> ephemeral bus leader -> many live Pi instances -> many Telegram targets
30
+ ```
31
+
32
+ The leader is a temporary transport role, not the ontological owner of the system. A terminal-visible Pi instance may become the initial leader because it is the operator's visible harness, while additional terminal-visible Pi instances can explicitly register as followers through `/telegram-connect` and one of them can later take over bus leadership if the leader exits.
33
+
34
+ A practical Telegram UI can then use threads:
35
+
36
+ ```text
37
+ one private bot chat -> one thread per live Pi instance
38
+ ```
39
+
40
+ The operator experience should feel like:
41
+
42
+ 1. Start one Pi instance; it becomes the Telegram bus leader and polls Telegram.
43
+ 2. Start another Pi instance with `pi-telegram` and run `/telegram-connect`; the follower registers instead of fighting for `getUpdates`.
44
+ 3. The leader provisions or reuses a Telegram thread target for that instance.
45
+ 4. Messages, callbacks, reactions, files, voice, previews, and menus in that target route to the owning live Pi instance.
46
+ 5. If the leader exits, remaining followers elect/promote a new leader, which resumes polling and keeps the registered target routes alive where possible.
47
+
48
+ ## Non-goals
49
+
50
+ - Do not let more than one process call `getUpdates` for the same bot token.
51
+ - Do not treat Telegram as a raw terminal, PTY, or process supervisor.
52
+ - Do not couple the first design to Pi sessions if live instance ownership is the better runtime truth.
53
+ - Do not expose arbitrary group participants to prompts, controls, or artifacts.
54
+ - Do not require Threaded Mode for classic private-chat users; classic private-chat mode remains valid and should not receive slot/thread-name guidance.
55
+ - Do not implement leader election through unsafe lock stealing without heartbeats or stale-owner checks.
56
+
57
+ ## Terms
58
+
59
+ - `Telegram bus`: The singleton local capability to poll Telegram updates and send Telegram API calls for one bot token.
60
+ - `Leader`: The live Pi instance that currently owns the Telegram bus and calls `getUpdates`; this is an ephemeral role that should be transferable after stale heartbeat detection.
61
+ - `Follower`: A live Pi instance that wants Telegram presence but routes Telegram API access through the leader.
62
+ - `Bus lifecycle`: Transient recovery state only. Stable identity is the bus role (`leader` / `follower`); lifecycle should surface exceptional handoff states such as `electing`, not duplicate roles with labels like `leader-active`.
63
+ - `Agent instance`: A running Pi process/session with its own extension state, queue, active turn, model, tools, and lifecycle hooks.
64
+ - `Telegram target`: The concrete Telegram destination for an instance, represented as `{ chatId, threadId? }`.
65
+ - `Thread target`: A Telegram UI thread destination, represented as `{ chatId, threadId: message_thread_id }` over Bot API topic transport.
66
+ - `Classic target`: The existing private-chat target, represented as `{ chatId: allowedUserId }`.
67
+
68
+ ## Core Shift
69
+
70
+ The lock should evolve from "this instance is the only usable Telegram extension" to "this instance is the current Telegram bus leader".
71
+
72
+ Current meaning:
73
+
74
+ ```text
75
+ locks.json / @llblab/pi-telegram -> polling/control owner
76
+ ```
77
+
78
+ Proposed meaning:
79
+
80
+ ```text
81
+ locks.json / @llblab/pi-telegram -> bus leader identity + heartbeat
82
+ ```
83
+
84
+ Followers do not poll. They register with the leader and receive routed inbound updates from it. Followers still own their local queue, active-turn state, previews, final delivery planning, model switches, and Pi lifecycle. The leader owns only Telegram transport and update fanout. Pi session replacement (`new`) changes follower agent context, not bus membership: a registered follower preserves its registration and refreshes the live context instead of disconnecting. The Telegram bus belongs to the local set of cooperating visible Pi instances rather than to the first terminal session forever: if the visible terminal leader exits, a live registered follower can take over leadership.
85
+
86
+ ## Target Abstraction
87
+
88
+ Introduce a first-class target abstraction:
89
+
90
+ ```ts
91
+ type TelegramTarget = {
92
+ chatId: number;
93
+ threadId?: number;
94
+ };
95
+ ```
96
+
97
+ Private-chat mode uses `{ chatId: allowedUserId }`.
98
+
99
+ Thread instance mode uses `{ chatId: topicChatId, threadId: messageThreadId }` over Bot API topic transport.
100
+
101
+ Every session/instance-scoped path should eventually carry a target:
102
+
103
+ - Inbound update routing.
104
+ - Queue item identity.
105
+ - Active turn state.
106
+ - Preview draft state.
107
+ - Final replies and reply deduplication.
108
+ - Voice and attachment uploads.
109
+ - Menu/status/settings/queue/section messages.
110
+ - Button callback ownership.
111
+ - Reactions.
112
+ - Typing/record-voice chat actions.
113
+ - Direct local/TUI Telegram delivery.
114
+
115
+ ## Instance Binding Vs Session Binding
116
+
117
+ The user's proposed instance binding is stronger than the original session binding.
118
+
119
+ ### Session binding
120
+
121
+ A thread maps to a Pi session id/session file/cwd.
122
+
123
+ Pros:
124
+
125
+ - Durable history can survive process restarts.
126
+ - A session transcript can be reopened if Pi exposes a stable session identity.
127
+ - Instance-thread identity is conceptually tied to work history.
128
+
129
+ Cons:
130
+
131
+ - A thread may point at a dead session with no live agent to answer.
132
+ - `/new`, compaction, session replacement, and session file behavior depend on Pi internals.
133
+ - Multi-instance liveness still needs a separate routing layer.
134
+
135
+ ### Instance binding
136
+
137
+ A thread maps to a currently running Pi instance.
138
+
139
+ Pros:
140
+
141
+ - Thread liveness is honest: if the instance is registered, there is a live owner.
142
+ - Routing can use process identity, heartbeat, cwd, model, and current status directly.
143
+ - Leader election and target registration naturally operate over live instances.
144
+
145
+ Cons:
146
+
147
+ - Threads become ephemeral unless the instance identity has a durable resume key.
148
+ - Closing a Pi instance can leave an orphan thread/history unless cleanup/status rules are clear.
149
+ - Restarting the same project may create a new thread unless reuse is based on cwd/profile/name.
150
+
151
+ ### Recommended stance
152
+
153
+ Use instance binding as the runtime truth, with an optional durable `instanceProfileKey` for thread reuse.
154
+
155
+ ```text
156
+ runtime owner: live instance id
157
+ reuse key: cwd/profile/user-chosen alias/session id when available
158
+ ```
159
+
160
+ This avoids dead-thread routing while still allowing a restarted project to reclaim a previous thread when the operator wants stable workspace history.
161
+
162
+ ## Instance Identity
163
+
164
+ A registered instance should expose:
165
+
166
+ ```json
167
+ {
168
+ "instanceId": "uuid-or-runtime-id",
169
+ "pid": 12345,
170
+ "cwd": "/home/user/project",
171
+ "startedAt": "2026-05-20T10:00:00.000Z",
172
+ "owner": { "kind": "leader", "cwd": "/home/user/project" },
173
+ "threadName": "<valid-instance-identity>",
174
+ "target": { "chatId": -1001234567890, "threadId": 42 },
175
+ "status": "idle|active|queued|compacting|disconnected",
176
+ "lastHeartbeatAt": "2026-05-20T10:00:05.000Z"
177
+ }
178
+ ```
179
+
180
+ `instanceId` is liveness identity. `owner` is explicit current binding identity (`leader`, `manual-follower`, or `pending-topic`). Internal compatibility keys may be derived, but `state.json` should not hide ownership direction inside legacy string keys. `threadName` is the user-facing instance-thread name: it drives Telegram UI thread naming and the Telegram-originated prompt identity label. Fresh threads receive a baked compact thread name from the assigned slot's curated palette; bare slot labels are fallback state only, and role/cwd seeds never replace the thread label.
181
+
182
+ ## Leader Election
183
+
184
+ Minimum viable election:
185
+
186
+ 1. On startup, read the Telegram lock.
187
+ 2. If no leader exists, acquire leadership and start polling.
188
+ 3. If a live leader exists, register as follower.
189
+ 4. If the leader heartbeat is stale, attempt an atomic leadership takeover; do not treat ordinary `/telegram-connect` on a follower as a leadership move while the leader is live.
190
+ 5. If several followers detect stale leadership, use deterministic tie-break or atomic lock write so only one wins.
191
+
192
+ Possible tie-breakers:
193
+
194
+ - Oldest live follower wins: stable and predictable.
195
+ - Lowest pid wins: simple on one host, weak across machines.
196
+ - Random backoff before takeover: reduces stampede, less deterministic.
197
+ - Highest priority role wins: future config-driven choice.
198
+
199
+ Recommended first pass: stale heartbeat + random jitter + atomic compare/write lock. Later, add a deterministic priority if needed.
200
+
201
+ ## Leader/Follower Communication
202
+
203
+ Open implementation choices:
204
+
205
+ ### Option A: Local IPC endpoint under agent temp dir
206
+
207
+ Leader opens a local Node `net` endpoint: a Unix-domain socket under the agent temp directory on Unix-like platforms, or a deterministic Windows named pipe (`\\.\pipe\pi-telegram-...`) on native Windows. Followers register, heartbeat, and exchange routed events. Follower registration uses a longer registration-specific response timeout than ordinary heartbeat/forwarding calls because the leader may need to provision a Telegram thread before it can return the assigned target; timing out that handshake leaves a visible tab with no follower heartbeat. Keep this handshake to the true critical path: create/reuse the target, persist the live binding, and return it. Connected notices and replaced-thread reconciliation cleanup are non-critical and should run after registration so a follower becomes routable before Telegram client/server UI convergence work finishes.
208
+
209
+ Pros:
210
+
211
+ - Natural request/response for sending Telegram API calls through the leader.
212
+ - Can route inbound updates to followers while preserving one poller.
213
+ - Good fit for live process membership.
214
+
215
+ Cons:
216
+
217
+ - Adds IPC lifecycle and security concerns.
218
+ - Cross-machine workers need tunneling or a different transport.
219
+
220
+ ### Option B: File-backed mailbox plus wakeups
221
+
222
+ Followers write registrations and outbound requests to files; leader scans/watches.
223
+
224
+ Pros:
225
+
226
+ - Simple local persistence and debugging.
227
+ - No socket protocol initially.
228
+
229
+ Cons:
230
+
231
+ - Harder to do low-latency streaming previews and backpressure.
232
+ - File locking and cleanup become subtle.
233
+
234
+ ### Option C: External daemon
235
+
236
+ A dedicated Telegram bus daemon owns polling and all Pi instances connect to it.
237
+
238
+ Pros:
239
+
240
+ - Cleanest conceptual bus owner.
241
+ - Best long-term fit for multi-host or always-on operation.
242
+
243
+ Cons:
244
+
245
+ - Bigger installation/product boundary than an extension.
246
+ - More operational burden.
247
+
248
+ Recommended path: keep local IPC as the default internal bus, while keeping the public design compatible with a future daemon if deployment needs outgrow one host.
249
+
250
+ ## Native Windows Smoke Plan
251
+
252
+ Native Windows support should not require WSL. The baseline transport uses Windows named pipes for leader/follower IPC, but live verification still needs an operator with a native Windows Pi install.
253
+
254
+ Manual smoke checklist:
255
+
256
+ 1. Enable BotFather Threaded Mode for the paired bot.
257
+ 2. Start Pi in one Windows terminal and run `/telegram-connect`; verify it becomes the leader and gets a named Telegram thread.
258
+ 3. Start Pi in a second Windows terminal and run `/telegram-connect`; verify it registers as follower rather than offering takeover, creates/uses its assigned thread, and terminal status shows `<ThreadName> Follower`.
259
+ 4. From the follower thread, send a prompt that requests inline buttons; tap a button and verify the follow-up prompt queues in the follower instance.
260
+ 5. From the follower thread, request a voice reply and/or attachment; verify upload routes through the leader transport into the follower thread.
261
+ 6. Close the follower terminal; verify heartbeat pruning, disconnected notice, and cleanup behavior match Unix-like behavior.
262
+ 7. Reload the leader and verify status/debug output does not expose raw pipe internals except in explicit diagnostics.
263
+
264
+ If any step fails, capture `telegram-status --debug`, `tmp/telegram/state.json`, and `tmp/telegram/logs.jsonl` before retrying.
265
+
266
+ ### Native Windows Assumption Audit
267
+
268
+ Current portability audit:
269
+
270
+ - Local bus transport: adapted. Unix-like platforms use filesystem socket paths; native Windows uses named pipes so no POSIX socket pathname is required.
271
+ - Bus endpoint permissions: Unix sockets/directories use `chmod`; Windows named-pipe endpoints skip POSIX chmod/unlink path handling because the pipe is not a filesystem node.
272
+ - Shared lock/config/state/temp files: path construction uses `path.join`/`path.resolve` under the Pi agent directory. File permission calls remain best-effort private-mode hardening; native Windows may emulate POSIX modes, so broad Windows ACL auditing is outside this extension's current local-bus baseline.
273
+ - Process liveness: lock ownership uses `process.kill(pid, 0)`, which Node supports on Windows for existence checks. Cross-user permission failures are treated as alive, matching Unix semantics.
274
+ - Shell/provider commands: outbound handler command templates remain operator-configured and platform-dependent; Threaded Mode bus portability does not guarantee every configured STT/TTS/shell provider is Windows-native.
275
+ - Manual follower identity: process ids are used as local liveness/profile hints only, not cross-machine identifiers.
276
+
277
+ Remaining risk is live native Windows behavior: named-pipe creation/connect timing, antivirus/firewall/ACL interference, and provider command availability need operator smoke evidence.
278
+
279
+ ## Telegram Thread UX
280
+
281
+ In BotFather private-chat Threaded Mode:
282
+
283
+ - The private bot chat is a tabbed instance workspace, not a classic `General + threads` forum.
284
+ - `All` is an aggregate view, not a process launcher. Explicit new instances use live Pi follower registration: the operator starts Pi in a terminal and runs `/telegram-connect`; owner-created empty threads are observed but not treated as a Pi instance until the user chooses a route or restore action.
285
+ - The leader should proactively create or reclaim its own thread on startup/activation when Threaded Mode is available, so the visible leader has the same two-way binding as followers.
286
+ - **Unbound thread detection**: when the owner writes in an unknown `message_thread_id`, the bridge checks effective Threaded Mode state. If the current leader has no active bound thread, that new thread is reclaimed for the leader and the prompt is served locally. Otherwise the bridge preserves the prompt in that Telegram thread and shows a target-thread chooser; explicit routing may later close/delete only extra confirmed source threads through `thread-reconciler`.
287
+ - Unknown later threads and threadless prompt messages are not silently routed to the leader and never launch hidden Pi processes. The default and only operator path for a new visible instance is starting a visible second Pi process and letting it register as follower through `/telegram-connect`. Manual follower registration creates a fresh visible thread unless the same explicit binding identity already has a live binding; old offline/failed records may point at closed/deleted Telegram tabs and are not silently claimed.
288
+ - Thread lifecycle service messages (`forum_topic_created`, `forum_topic_closed`, `forum_topic_reopened`, deletion/stale send errors) update observations and binding state. Closed/deleted leader or follower threads can be reclaimed or recreated deliberately. Leader startup also probes reused own threads with a non-visible chat action; if Telegram reports the thread closed/deleted, the binding is marked stale and a fresh leader thread is created. Unknown `forum_topic_created` service events are observation-only and are not destructive cleanup proof.
289
+ - Bidirectional binding is a core UX requirement, not an implementation detail: Pi instances should actively advertise/remember their thread identity, while the bot should observe Telegram-client thread state and reflect it back into instance state. This keeps the system responsive, recognizable, and controllable even when the operator closes tabs, writes from `All`, or a follower later becomes leader.
290
+
291
+ In BotFather private-chat Threaded Mode:
292
+
293
+ - The private bot DM becomes the operator's multi-instance dashboard.
294
+ - Each live bound instance gets one visible thread.
295
+ - Each instance has a durable single-letter slot (`A`-`Z`) assigned by the extension and a bridge-authored `threadName`.
296
+ - New slots advance monotonically through the alphabet and wrap after `Z` only to a free slot; closed earlier slots are not backfilled out of order. This preserves sequence feel and intentionally caps concurrent visible instances to the alphabet without duplicating occupied letters. The compact `bot.lastSlot` cursor persists across reloads and live-test history, so after `Z` the next truly new thread can be `A` again when `A` is currently free.
297
+ - A follower that later becomes leader keeps its existing slot and thread name; leadership changes are transport role changes, not identity resets.
298
+ - Instance-thread names should be short and recognizable. Default provisioning chooses one baked 4-6 letter single-word Latin thread name from the assigned slot's five-name palette using provisioning timestamp entropy and creates the Telegram thread with that title immediately. The slot remains internal ordering metadata and is not redundantly included in the thread name. Bare slot titles are fallback/legacy state only; do not prompt agents to self-name and do not expose a rename tool. Existing human-named threads are preserved across reloads and leadership changes when they remain the current live binding. If reload creates a new runtime instance while the previous leader thread is still alive, the new leader should take the next free slot instead of reusing the old slot immediately.
299
+ - A thread-local `/start` opens that instance's menu.
300
+ - Prompts typed in a thread route to the owning instance.
301
+ - Replies, previews, files, voice, and buttons stay in that thread.
302
+ - Queue controls and reactions affect only that instance target.
303
+ - Native typing/activity for real work is sent to that instance thread and mirrored to `All`; `All` is the aggregate surface and should show active when any bound thread is active. Startup/connect/reload/recovery must not send typing by themselves.
304
+ - If the instance disconnects, the leader can post/update a compact status: `Instance offline`.
305
+ - If the same live binding identity returns, it can reclaim the thread and post a compact reconnect status.
306
+
307
+ ## Bot API Evidence For BotFather Threaded Mode
308
+
309
+ The local Bot API reference in [`../.agents/skills/telegram-bot/api.md`](../.agents/skills/telegram-bot/api.md) supports private bot Threaded Mode through bot capability fields and thread-target transport:
310
+
311
+ - `User` returned by `getMe` can include `has_topics_enabled` and `allows_users_to_create_topics`; these are the BotFather private-chat Threaded Mode capability fields and are the startup/runtime probe source for this extension.
312
+ - `createForumTopic` works in a private chat with a user and returns a `ForumTopic`, so the returned `message_thread_id` is persistable as an instance thread target.
313
+ - Private-thread management uses Bot API methods such as `editForumTopic`, `closeForumTopic`, `reopenForumTopic`, `deleteForumTopic`, and related unpin methods. Thread-unavailable errors from these methods are degradation evidence when BotFather Threaded Mode is disabled.
314
+ - `Message` exposes `message_thread_id` and `is_topic_message`; an incoming private-chat message with `message_thread_id` is a live Threaded Mode observation and can trigger progressive upgrade.
315
+ - Topic lifecycle service messages include `forum_topic_created`, `forum_topic_edited`, `forum_topic_closed`, `forum_topic_reopened`, `general_forum_topic_hidden`, and `general_forum_topic_unhidden`.
316
+ - `message_thread_id` is supported by the send/upload methods the bridge uses or may need: `sendMessage`, `sendPhoto`, `sendDocument`, `sendVoice`, `sendMediaGroup`, `sendSticker`, `sendRichMessage`, `sendMessageDraft`, `sendRichMessageDraft`, and `sendChatAction`.
317
+
318
+ Non-goal: group detection is not the control-plane model for this extension. BotFather Threaded Mode lives in the private bot chat, so startup and runtime switching must not depend on group chat metadata or group admin capability fields.
319
+
320
+ Remaining live-verification points:
321
+
322
+ - Whether callback query messages always carry `message_thread_id` in private bot threads, or whether generated button callbacks must rely on stored message id -> target ownership.
323
+ - Whether message-reaction updates carry thread identity in the current Bot API shape. The reference exposes chat id and message id for reactions, so routing may need stored message ownership.
324
+ - Whether every rich draft/final/upload/chat-action path behaves identically in Telegram clients when `message_thread_id` is supplied.
325
+
326
+ Implementation should start with target plumbing and fixtures, then run a Telegram smoke test before marking Threaded Mode stable.
327
+
328
+ ## Inbound Routing
329
+
330
+ The leader polls all updates for the bot token. It classifies each update into a target key:
331
+
332
+ ```text
333
+ targetKey = chatId + ':' + (threadId ?? 'private')
334
+ ```
335
+
336
+ Then it dispatches:
337
+
338
+ - If target belongs to the leader instance, handle locally.
339
+ - If target belongs to a follower, forward the normalized update/event to that follower.
340
+ - If target is unknown but authorized and setup allows provisioning, offer or create a binding.
341
+ - If target is unknown or unauthorized, ignore or send a safe denial.
342
+
343
+ Follower instances should receive normalized events, not raw Telegram transport internals where possible. The follower should still run the same queue/routing logic, but Telegram API calls go back through the leader transport port.
344
+
345
+ ## Outbound Routing
346
+
347
+ Followers should not call Telegram Bot API directly for routed Telegram work. Instead, they call a leader-owned transport port:
348
+
349
+ ```text
350
+ follower reply/preview/upload/chat-action/download/callback-answer -> leader IPC -> Telegram API
351
+ ```
352
+
353
+ This preserves one API bus and one set of rate-limit/retry diagnostics. The current local bus routes JSON calls, multipart uploads, chat actions, message deletes, callback/guest answers, and file downloads through the leader when a follower is registered.
354
+
355
+ Every outbound request carries its target. The leader injects `message_thread_id` when `target.threadId` exists.
356
+
357
+ ## Queue And State Scoping
358
+
359
+ Each instance owns its own queue and active turn state. The leader should not become a central queue scheduler for all agents unless a future daemon mode deliberately chooses that architecture.
360
+
361
+ Target-scoped state requirements:
362
+
363
+ - Queue item identity includes target plus source message id.
364
+ - Reply deduplication is keyed by target, not just chat id.
365
+ - Preview draft state is keyed by target.
366
+ - Button callbacks store target and owning instance id.
367
+ - Reactions resolve to target/instance before mutation.
368
+ - Attachments generated by a follower are uploaded by the leader into the follower's target.
369
+
370
+ ## Configuration
371
+
372
+ There is no public `telegram.json` switch for the bus. BotFather private-chat Threaded Mode is the capability switch: when Telegram exposes Threads, the bridge enables the local bus; when Telegram runs as an ordinary private DM, the bridge uses classic private-chat flow as the base mode.
373
+
374
+ Typical config remains just bot identity and authorization:
375
+
376
+ ```json
377
+ {
378
+ "botToken": "...",
379
+ "allowedUserId": 123456789
380
+ }
381
+ ```
382
+
383
+ Rules:
384
+
385
+ - Classic mode is selected by Telegram capability: when private-chat Threads are unavailable or disabled, the polling owner uses ordinary single-DM behavior and blocked instances do not register as followers.
386
+ - BotFather Threaded Mode enables local leader/follower behavior automatically. The leader owns `getUpdates`; registered followers route Telegram API work through the leader. `/telegram-connect` registers as follower when a live leader exists and does not offer manual takeover in that state. The TUI status bar reports `telegram leader` or `telegram follower` so transport role is visible without opening diagnostics.
387
+ - The thread chat is the owner's private bot DM (`allowedUserId`); no `topics.chatId` config is needed. Thread names are assigned by the bridge from a baked compact per-slot palette. There is no agent-facing `telegram_rename_thread` tool and no separate user-facing slash command for manual thread renames.
388
+ - Thread reuse is extension-owned through current live binding identity; there is no separate `topics` config surface in the active private-chat thread model. Manual followers use instance-scoped internal keys by default so multiple terminal processes in the same cwd can receive separate threads.
389
+ - Thread cleanup remains conservative and centralized: destructive close/delete actions are planned and applied through `thread-reconciler` with proof-before-delete checks, leader-epoch fencing, and retry-preserving failure semantics.
390
+ - `allowedUserId` remains the primary authorization boundary unless explicit allowlists are added. Forum/group membership alone must not grant control.
391
+
392
+ ## Runtime State
393
+
394
+ Current state under the agent dir:
395
+
396
+ - `locks.json`: current bus leader identity, capability secret, heartbeat, and cleanup fencing epoch. The local bus endpoint is derived from the agent directory by default; legacy `busSocketPath` entries are tolerated but are not required.
397
+ - `tmp/telegram/state.json`: volatile extension+bot observable/debug snapshot, not routing authority. It writes `source: "snapshot"` and `writtenAtMs` so consumers do not confuse it with an authoritative database. It should mirror `/telegram-status`-style projections: top-level `bot` stores bot-wide capability state such as `threadMode: "unknown" | "enabled" | "disabled"`, `runtime` identifies leader/follower role and process status, `liveRoster` mirrors followers/current targets/reservations, `diagnostics` mirrors status/debug signals, `threads` stores current routeable bindings, `bot.lastSlot` stores the compact slot cursor used when all current threads are gone, and `reservations` records short-lived slot collision guards.
398
+ - Local bus endpoints: Unix-like platforms use `tmp/telegram/bus.sock` and `tmp/telegram/followers/*`; native Windows uses deterministic named pipes under `\\.\pipe\pi-telegram-...`. These are transient IPC endpoints, not durable routing state.
399
+
400
+ The bridge must not keep a durable `telegram-targets.json` history. Stale/offline/failed thread entries are reconciliation observations, not reusable source-of-truth state; persisting them increases collision risk. `sync` is event-driven assumption reconciliation, not full Telegram bot-state mirroring, because Bot API does not expose a complete topic/thread listing surface. `state.json` therefore exists for extension+bot observability, diagnostics, startup hints, and explaining reconciliation decisions; live bus/runtime state remains authoritative for routing and provisioning. Non-current routeable thread bindings are pruned during load/persist; old session records must not be retained just to compute the next slot because `bot.lastSlot` is the only durable cursor. Previous-process leader bindings are treated as occupied TTL-bounded reservations until Telegram confirms deletion: reload/startup may close/delete/probe the old thread, known reservations are retried proactively on leader startup, and if Telegram still accepts the old thread id, the new leader should provision the next free slot (`B`, `C`, …) rather than creating a duplicate same-letter tab or blocking startup on Telegram UI convergence. Routing must use live current threads/follower registry, never reservations. The bus leader provisions its own thread during bus startup/connect and provisions follower threads on `follower.register`; registered followers also live in the leader's in-memory registry and communicate over the local bus socket. The live follower registry can resolve a follower by exact `{ chatId, threadId? }`; the leader uses that target ownership to forward message and edited-message updates to followers, and the follower receiver accepts those updates in addition to callbacks and reactions. Media album grouping and split-text coalescing keys include the thread target, queue reaction mutations can scope by chat/thread to avoid cross-target message-id collisions, active-turn target is exposed for lifecycle cleanup and local direct-tool defaults, transport reply dedup is chat/thread-scoped, stored menu state is keyed by chat/message so callback state lookup cannot collide across chats, and generated button turns plus section prompt/open actions preserve the callback thread target. `telegram_message` and immediate `telegram_attach` delivery can also carry an explicit `thread_id` with `chat_id`; when a follower is registered, their default direct-tool target is the assigned thread target and the bus-aware API runtime routes the send through the leader instead of calling Bot API transport locally.
401
+
402
+ All files containing routing, chat ids, thread ids, or process details should use private permissions and should represent current state rather than historical target caches.
403
+
404
+ ## Failure Modes
405
+
406
+ ### Leader exits cleanly
407
+
408
+ - Leader stops polling and marks itself offline.
409
+ - Followers detect missing heartbeat.
410
+ - One follower promotes itself after jitter/tie-break.
411
+ - New leader resumes `getUpdates` from the persisted offset if safe.
412
+
413
+ ### Leader crashes
414
+
415
+ - Followers detect stale heartbeat.
416
+ - One follower promotes itself.
417
+ - Some updates may be delayed or skipped depending on offset persistence; dispatcher design must define this explicitly.
418
+
419
+ ### Follower heartbeat is missed
420
+
421
+ - Leader prunes the follower from the live registry after missed heartbeats, but heartbeat pruning is only liveness bookkeeping.
422
+ - A missed heartbeat does not delete, close, mark offline, or send a disconnected notice for the follower's Telegram thread binding because the common cause may be leader reload, IPC handoff, or transient reconnect rather than a dead follower.
423
+ - Followers treat rejected/missing heartbeat acknowledgements as registration loss: clear local registered truth, try to re-register with the current leader, wait a short leader-reload grace window, retry, and then promote themselves if the leader still cannot route them.
424
+ - Every successful follower registration/re-registration sends a compact connected notice in the assigned thread so recovery and reconnection are visible during live testing without confusing heartbeat suspicion with real disconnect.
425
+ - Successful forwarded updates and follower-originated API calls refresh liveness, so active followers are not pruned only because the interval heartbeat tick lagged.
426
+ - Destructive follower thread teardown belongs to explicit `/telegram-disconnect` or confirmed reconciliation actions, not generic heartbeat pruning.
427
+ - Historical offline thread entries are not retained as reusable source of truth.
428
+
429
+ ### Thread is deleted
430
+
431
+ - Target mapping becomes stale.
432
+ - On next outbound failure or reconnect, leader records a diagnostic.
433
+ - Depending on policy, recreate a thread or mark the instance as needing operator action.
434
+
435
+ ### Split brain
436
+
437
+ - Two leaders calling `getUpdates` is the main safety failure.
438
+ - Lock heartbeat/takeover must be atomic enough to prevent this under normal local concurrency.
439
+ - If Telegram returns API conflict behavior, record diagnostics and force one leader to step down.
440
+
441
+ ## Security Boundaries
442
+
443
+ - Messages, edits, callbacks, and reactions must check user authorization, not only chat/thread membership.
444
+ - Followers authenticate to the local leader IPC with a leader-minted capability secret carried in the active lock entry; registration, heartbeat, forwarded updates, and follower API calls without the secret are rejected. Registration rejections are surfaced verbatim in the follower `/telegram-connect` result, registration waits through leader-side Telegram thread provisioning, and successful registrations send an immediate heartbeat before the interval ticker so the leader does not prune a live follower before its first scheduled heartbeat. The local bus socket is also created under a private `0700` directory with `0600` socket permissions as a first local-only boundary.
445
+ - Follower Bot API proxying is allowlisted and target-scoped where applicable so a follower can reply in its assigned thread without gaining arbitrary bot control.
446
+ - Button and section callbacks must verify authorized `from.id` and owning target/instance.
447
+ - Generated artifacts must not leak to the wrong thread after leader failover.
448
+ - Diagnostics should redact bot tokens, large prompts, attachment paths, and handler output.
449
+
450
+ ## Acceptance Criteria
451
+
452
+ - [x] The lock semantics are redesigned as Telegram bus leadership with heartbeat and stale takeover rules.
453
+ - [x] A first-class `TelegramTarget` can represent classic private chats and thread destinations.
454
+ - [x] The bridge can run in classic mode with unchanged private-chat behavior.
455
+ - [x] A live Pi instance can register as a follower when another live instance is leader.
456
+ - [x] Followers never call `getUpdates` for the shared bot token.
457
+ - [x] Followers can send replies, previews, voice, attachments, menus, and chat actions through the leader transport.
458
+ - [x] The leader can route inbound messages, edits, callbacks, reactions, media groups, and split text to the owning instance by target. Message/edit and callback/reaction routing is authorized by user id; media and split-text coalescing are target-keyed locally.
459
+ - [x] Telegram UI thread targets can be provisioned as current state bindings; stale/deleted Telegram observations and explicit disconnect/reconciliation actions remove unusable bindings instead of persisting reusable history, while heartbeat-pruned followers preserve their thread binding so transient leader reload/reconnect gaps do not create split-brain Telegram UX.
460
+ - [x] Leader failover promotes one remaining follower without creating competing pollers.
461
+ - [x] Queue, active turn, preview, reply deduplication, menu, section, button, reaction, and attachment state are scoped by instance/target. Queue reaction mutations and transport reply dedup are chat/thread-scoped; active-turn target is available to lifecycle cleanup; stored menu state is chat/message-keyed; generated button turns and section prompt/open actions preserve callback targets; preview and attachment delivery already carry targets.
462
+ - [x] Authorization prevents arbitrary Telegram users or local processes from controlling agents or receiving artifacts.
463
+ - [ ] Live smoke still needs operator/client confirmation for restore chooser ordering, leader/follower restore, native active-status scoping, follower attachments/buttons, and close/reopen thread lifecycle. Deterministic docs and tests already cover classic compatibility, single leader/follower registration, target routing, stale leader takeover, follower exit, and wrong-target denial.
464
+
465
+ ## Implemented Shape
466
+
467
+ - Bus semantics are the feature frame: Telegram threads are one Telegram UI substrate for a local multi-instance bus.
468
+ - `TelegramTarget` and target-key helpers represent classic private chats and thread destinations.
469
+ - Outbound ports, previews, replies, voice, attachments, chat actions, menus, sections, buttons, queue mutations, and direct local delivery carry target metadata where needed.
470
+ - The transport lock distinguishes live bus leadership from ordinary classic ownership through heartbeat, leader epoch, and stale takeover rules.
471
+ - The leader records live follower registration, heartbeat, thread identity, slot, and target mapping; followers do not poll `getUpdates`.
472
+ - Local IPC is the default internal bus. Registered followers receive normalized inbound updates and send allowlisted, target-scoped Bot API calls through the leader.
473
+ - Thread targets are current-state bindings, not durable historical delivery addresses. Stale/offline/failed entries are reconciliation evidence only.
474
+ - Failover promotes a remaining follower after dead or clean-disconnected leaders without creating competing pollers; follower heartbeat recovery owns re-register → grace → promotion while preserving thread bindings across transient leader reload gaps.
475
+ - Thread cleanup is centralized in `thread-reconciler`, fenced by leader epoch, and requires confirmed delete/stale evidence before state is marked deleted.
476
+ - Stable docs/UI now describe classic mode, opt-in Threaded Mode, manual follower registration, status/diagnostics, unbound-thread reroute/restore UX, and operator recovery boundaries.
477
+
478
+ ## Remaining Live Questions
479
+
480
+ - Do Telegram clients consistently render restore chooser ordering, leader/follower restore, native active status, follower attachments/buttons, and close/reopen lifecycle after reload?
481
+ - Which Telegram client quirks besides the known Desktop private-thread reply-header issue need documented exceptions?
482
+ - Should a future daemon/companion own leadership and fanout for multi-host deployments, or is local IPC sufficient for the supported product shape?
483
+ - Should offline instance threads eventually get a user-visible archived/offline status surface, or should current conservative cleanup/reclaim rules stay minimal?
package/docs/outbound.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  `pi-telegram` maps hidden assistant-authored HTML comments to Telegram-native outbound actions.
4
4
 
5
- Normal Telegram-turn replies are intentionally prompt-driven: the agent writes Markdown plus small hidden top-level blocks, and the bridge performs transport after `agent_end`. `telegram_voice` and `telegram_button` are not π tools. For local/TUI-initiated work where the user explicitly asks to send something to Telegram, the bridge also exposes direct tools: `telegram_message` for Markdown text and `telegram_attach` for file delivery when no Telegram turn is active. Direct local/TUI delivery requires this π instance to own `/telegram-connect`; if polling/control ownership moved elsewhere, the tools fail instead of bypassing the singleton lock. Outbound behavior combines assistant prompt markup, text command-template handlers, registered voice synthesis providers, generated artifacts, direct Telegram tools, and reply delivery. Direct `telegram_message` text is planned through the same reply markup path, so embedded top-level `telegram_button` comments become buttons attached to that text message.
5
+ Normal Telegram-turn replies are intentionally prompt-driven: the agent writes Markdown plus small hidden top-level blocks, and the bridge performs transport after `agent_end`. `telegram_voice` and `telegram_button` are not Pi tools. For local/TUI-initiated work where the user explicitly asks to send something to Telegram, the bridge also exposes direct tools: `telegram_message` for Markdown text and `telegram_attach` for file delivery when no Telegram turn is active. In classic mode, direct local/TUI delivery requires this Pi instance to own `/telegram-connect`; in Threaded Mode, a registered follower may route direct-tool sends through the leader-owned bus transport. If neither condition is true, the tools fail instead of bypassing singleton ownership. Explicit thread delivery uses `chat_id` plus `thread_id`; registered followers default to their assigned thread target. Outbound behavior combines assistant prompt markup, text command-template handlers, registered voice synthesis providers, generated artifacts, direct Telegram tools, and reply delivery. Direct `telegram_message` text is planned through the same reply markup path, so embedded top-level `telegram_button` comments become buttons attached to that text message.
6
6
 
7
7
  Text handlers use the portable [Command Template Standard](./command-templates.md). Programmatic outbound handlers use `registerTelegramOutboundHandler(kind, handler)`. Voice replies can use configured command-template handlers or the provider API described in [Voice Integration](./voice.md).
8
8
 
@@ -102,7 +102,7 @@ The bridge strips the comment from Telegram text. On `agent_end`, it maps each `
102
102
 
103
103
  ## Buttons Markup
104
104
 
105
- Assistant replies can include independent button blocks. The prompt is sent back to π when the user taps the button; use the colon shorthand when the prompt should equal the label, `prompt="..."` for one-line prompts, or the body form for multiline prompts:
105
+ Assistant replies can include independent button blocks. The prompt is sent back to Pi when the user taps the button; use the colon shorthand when the prompt should equal the label, `prompt="..."` for one-line prompts, or the body form for multiline prompts:
106
106
 
107
107
  ```md
108
108
  I can continue.
@@ -135,7 +135,8 @@ Buttons are built in and do not need a command template because they are pure Te
135
135
  The extension injects prompt guidance by context:
136
136
 
137
137
  - If no bot token is configured, no Telegram bridge suffix is injected.
138
- - For ordinary local/TUI prompts, the agent only sees explicit direct-delivery guidance: use `telegram_attach` or `telegram_message` when the user asks to send something to Telegram, and otherwise answer locally as normal.
138
+ - For ordinary local/TUI prompts, the agent only sees compact direct-delivery guidance: use `telegram_attach` or `telegram_message` when the user asks to send something to Telegram, and otherwise answer locally as normal.
139
+ - For Telegram-originated turns, the prompt carries only minimal mobile/reply/file guidance; agents can call `telegram_help()` for full voice/button/direct-delivery/Threaded Mode/formatting/debug details.
139
140
  - For Telegram-originated turns, write the full technical answer as normal Markdown.
140
141
  - Add `telegram_voice` when a Telegram-native voice message is useful; use body text, `text="..."`, or colon shorthand for the text to synthesize. A companion summary is optional, no specific summary format is required.
141
142
  - Add `telegram_button: ...` when label equals prompt, `telegram_button label="..." prompt="..."` for one-line prompts, or `telegram_button label="..."` with a body for multiline prompts. If the reply contains only button/voice comment blocks, add a short visible marker (for example `Choose one:`) before them so Telegram always has a visible parent message for attachment.
@@ -1,6 +1,6 @@
1
1
  # Public API
2
2
 
3
- `pi-telegram` is both a π extension and a small Telegram platform for companion extensions. This document defines the stable public surface. Everything outside this document is implementation detail unless another focused doc explicitly marks it stable.
3
+ `pi-telegram` is both a Pi extension and a small Telegram platform for companion extensions. This document defines the stable public surface. Everything outside this document is implementation detail unless another focused doc explicitly marks it stable.
4
4
 
5
5
  ## Stability Levels
6
6
 
@@ -27,13 +27,13 @@ import {
27
27
  } from "@llblab/pi-telegram/voice";
28
28
  ```
29
29
 
30
- `0.12.0` intentionally removes the published `@llblab/pi-telegram/lib/*.ts` compatibility wildcard. Integrations should use the public API domain subpaths above. Package exports point at `/api/*.ts` membranes that re-export only stable companion-extension symbols; implementation modules under `lib/` remain package-private. Telegram command extensions use `/commands` as an explicit opt-in surface instead of automatically exposing arbitrary π slash commands to Telegram. See [Public API Smoke Examples](#public-api-smoke-examples) below for minimal companion-extension patterns that avoid implementation imports.
30
+ `0.12.0` intentionally removes the published `@llblab/pi-telegram/lib/*.ts` compatibility wildcard. Integrations should use the public API domain subpaths above. Package exports point at `/api/*.ts` membranes that re-export only stable companion-extension symbols; implementation modules under `lib/` remain package-private. Telegram command extensions use `/commands` as an explicit opt-in surface instead of automatically exposing arbitrary Pi slash commands to Telegram. See [Public API Smoke Examples](#public-api-smoke-examples) below for minimal companion-extension patterns that avoid implementation imports.
31
31
 
32
32
  ## User-Facing API
33
33
 
34
- ### π commands
34
+ ### Pi commands
35
35
 
36
- Stable commands inside π:
36
+ Stable commands inside Pi:
37
37
 
38
38
  - `/telegram-setup` — configure/update the bot token.
39
39
  - `/telegram-connect` — start polling here and acquire external Telegram control ownership. Accepted queue/reply state stays local if ownership later moves elsewhere.
@@ -57,8 +57,9 @@ This command surface is a mobile companion subset, not a raw terminal-command br
57
57
 
58
58
  ### Tools and assistant-authored actions
59
59
 
60
- - `telegram_attach(paths, chat_id?, caption?)` is the stable artifact delivery tool for generated files. During Telegram turns it queues files for the active reply; outside Telegram turns it sends files directly to the paired/default chat or explicit `chat_id` when this π instance owns `/telegram-connect`.
61
- - `telegram_message(text, chat_id?)` sends a direct Telegram Markdown message from local/TUI-initiated work when this π instance owns `/telegram-connect`. Top-level `telegram_button` comments inside `text` are parsed with the same planner used for normal replies and attached to that message; buttons are never standalone Telegram messages.
60
+ - `telegram_attach(paths, chat_id?, thread_id?, caption?)` is the stable artifact delivery tool for generated files. During Telegram turns it queues files for the active reply; outside Telegram turns it sends files directly to the paired/default chat, the registered follower's assigned thread, or an explicit `chat_id` plus optional `thread_id` when this Pi instance owns `/telegram-connect` or is registered with the multi-instance bus.
61
+ - `telegram_message(text, chat_id?, thread_id?)` sends a direct Telegram Markdown message from local/TUI-initiated work when this Pi instance owns `/telegram-connect` or is registered with the multi-instance bus. Top-level `telegram_button` comments inside `text` are parsed with the same planner used for normal replies and attached to that message; buttons are never standalone Telegram messages.
62
+ - `telegram_help()` returns detailed agent-facing guidance for pi-telegram delivery actions, Threaded Mode, formatting, and debugging. The regular prompt only points agents at this tool instead of repeating the full guidance on every turn.
62
63
  - `telegram_voice` hidden comments request Telegram-native voice delivery.
63
64
  - `telegram_button` hidden comments create inline buttons whose taps enqueue prompts. Use top-level column-zero comments outside code, quotes, lists, and indented examples; do not emit JSON button specs or standalone button actions.
64
65
 
@@ -97,6 +98,7 @@ interface TelegramConfig {
97
98
  Hidden/default semantics are represented by absence:
98
99
 
99
100
  - Voice Reply `hidden`: no `voice.replyMode` key is persisted.
101
+ - Agent activity status is not configurable in this release. Telegram uses native `sendChatAction(typing)` / product `...active` status as the only automatic in-chat work signal before the final reply.
100
102
  - Time Injection `hidden`: no `time.injectionMode` key is persisted; if `time` becomes empty, the whole `time` object may be omitted.
101
103
 
102
104
  Assistant Markdown delivery is native: final replies are sent as `InputRichMessage.markdown` via `sendRichMessage`, and draft previews use `sendRichMessageDraft` when a structurally closed preview frame is available. Draft-frame failures are recorded and skipped rather than converted into raw plain preview messages, because partial Markdown can be temporarily invalid while the final answer remains valid. Long native replies are split at Telegram Rich Message transport limits, with oversized fenced code, display-math, and fully wrapped inline-formatting blocks rewrapped per chunk so persisted chunks remain structurally valid. Guest replies use `InputRichMessageContent` in `answerGuestQuery` results. Bridge-owned UI surfaces such as menus, status, queue controls, commands, and sections keep explicit Telegram HTML/plain rendering by default because those texts are authored by the bridge or companion extensions for Telegram UI. Companion extension sections may explicitly request `"markdown"`, `"html"`, or `"plain"` per view. There is no `telegram.json` rendering toggle for assistant delivery. The bridge sets `skip_entity_detection: true` for assistant and guest Markdown so technical text such as `/commands`, hashtags, URLs, phone numbers, and card-like numbers does not gain unintended automatic entities; explicit Markdown links still belong in the Markdown source.
@@ -130,7 +132,7 @@ Low-level stable buses:
130
132
  - Purpose: observe or consume raw Telegram updates before default routing.
131
133
  - `registerTelegramInboundHandler()`
132
134
  - Identity: no id.
133
- - Purpose: generic Telegram-to transforms.
135
+ - Purpose: generic Telegram-to-Pi transforms.
134
136
  - `registerTelegramOutboundHandler()`
135
137
  - Identity: no id.
136
138
  - Purpose: generic final-reply transforms or voice command fallbacks.
@@ -145,7 +147,7 @@ All registration APIs return a disposer. Companion extensions should call dispos
145
147
 
146
148
  ## Commands
147
149
 
148
- Import from `@llblab/pi-telegram/commands`. This registers Telegram slash commands only; it does not expose π slash commands and is unrelated to command-template handlers.
150
+ Import from `@llblab/pi-telegram/commands`. This registers Telegram slash commands only; it does not expose Pi slash commands and is unrelated to command-template handlers.
149
151
 
150
152
  ```ts
151
153
  const off = registerTelegramCommand({
@@ -166,7 +168,7 @@ Contract:
166
168
  - Duplicate extension command names are rejected. The disposer removes only its own command registration.
167
169
  - Routing precedence is built-in bridge commands first, registered extension commands second, and prompt-template aliases after that. This lets an extension intentionally claim a command name; prompt-template owners can resolve collisions by renaming the template alias.
168
170
  - `showInMenu` defaults to `false`. When `true`, `emoji` is required and the command appears in `/start` help with that marker; it also joins Bot API command sync only when `description` is provided, because Telegram command-list entries require descriptions. The emoji is prefixed to the Bot API description as well. Workflow/product commands should opt in deliberately instead of expanding the core command row by default.
169
- - The command context currently provides `name`, `args`, `reply(text)`, and `enqueuePrompt(prompt)`. Use `enqueuePrompt()` when a command should create normal queued π work rather than perform immediate Telegram-side handling.
171
+ - The command context currently provides `name`, `args`, `reply(text)`, and `enqueuePrompt(prompt)`. Use `enqueuePrompt()` when a command should create normal queued Pi work rather than perform immediate Telegram-side handling.
170
172
  - Handler failures are isolated: the bridge records a `telegram-command` runtime diagnostic, sends a compact failure reply, and keeps Telegram polling/routing alive.
171
173
 
172
174
  Core commands stay reserved for bridge lifecycle, transport ownership, queue safety, and essential operator controls. Opinionated workflow commands should live in companion extensions through this registry.
@@ -466,7 +468,7 @@ async function synthesizeDemoOgg(_text: string): Promise<string> {
466
468
 
467
469
  Owned prefixes are reserved by `pi-telegram`: `compact:`, `tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`, `settings:`, and `section:`.
468
470
 
469
- Companion extensions should use their own short prefix for raw callbacks or use `ctx.callbackData()` inside sections. Unknown unowned callbacks may be forwarded to π as `[callback] <data>` after built-in handlers decline them.
471
+ Companion extensions should use their own short prefix for raw callbacks or use `ctx.callbackData()` inside sections. Unknown unowned callbacks may be forwarded to Pi as `[callback] <data>` after built-in handlers decline them.
470
472
 
471
473
  Full behavior: [Callback Namespaces](./callback-namespaces.md).
472
474
 
package/docs/sections.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  ## 1. Philosophy
10
10
 
11
- Telegram Extension Sections let ordinary pi extensions add structured UI surfaces to the `pi-telegram` inline application menu. The platform mirrors π's own extensibility model: small, composable extensions that plug into a shared shell without owning transport, polling, authorization, or menu lifecycle.
11
+ Telegram Extension Sections let ordinary pi extensions add structured UI surfaces to the `pi-telegram` inline application menu. The platform mirrors Pi's own extensibility model: small, composable extensions that plug into a shared shell without owning transport, polling, authorization, or menu lifecycle.
12
12
 
13
13
  `pi-telegram` stays the single bot operator. Extensions register typed sections; the bridge handles Telegram UI rendering, callback routing, token mapping, navigation hierarchy, and diagnostics. Section views default to explicit Telegram HTML UI markup, while extensions can request Markdown or plain text when that better matches their content. No second polling loop, no new loader — just one `registerTelegramSection()` call.
14
14
 
@@ -380,7 +380,7 @@ This applies to section callbacks as well — the state check runs before dispat
380
380
 
381
381
  ## 11. Pi Extension API Inspiration
382
382
 
383
- The platform inherits from π's own extension model:
383
+ The platform inherits from Pi's own extension model:
384
384
 
385
385
  - `export default function(pi)` → `registerTelegramSection(section)`
386
386
  - `pi.on("shutdown", ...)` → disposer from `registerTelegramSection`