@llblab/pi-telegram 0.17.5 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/AGENTS.md +67 -32
  2. package/BACKLOG.md +59 -19
  3. package/CHANGELOG.md +42 -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 +410 -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 +9 -2
  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 +409 -41
  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/docs/telegram-bot-api-rich-messages.md +0 -890
@@ -0,0 +1,410 @@
1
+ # Telegram Multi-Instance Bus Architecture
2
+
3
+ ## Status
4
+
5
+ Implemented for paired bots where Telegram exposes private-chat Threaded Mode. 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:
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 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 surfaces 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
+ In Threaded Mode, the lock means "this instance is the current Telegram bus leader" rather than "this instance is the only usable Telegram extension".
71
+
72
+ Classic lock meaning:
73
+
74
+ ```text
75
+ locks.json / @llblab/pi-telegram -> polling/control owner
76
+ ```
77
+
78
+ Threaded Mode 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
+ The bridge uses 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
+ Threaded Mode uses `{ chatId: privateChatId, threadId: messageThreadId }` over Bot API topic transport.
100
+
101
+ Every session/instance-scoped path carries or preserves 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
+ ## Binding Model
116
+
117
+ A thread maps to a currently running Pi instance, not to a historical session file. `instanceId` is the live routing owner, while `instanceProfileKey` is a reuse hint for reclaiming a compatible current thread across process replacement.
118
+
119
+ ```text
120
+ runtime owner: live instance id
121
+ reuse hint: cwd/profile/user-chosen alias/session id when available
122
+ ```
123
+
124
+ This keeps thread liveness honest: if an instance is registered, there is a live owner to answer. It also avoids coupling `/new`, compaction, and session-file internals to Telegram routing. Restarted projects may still reclaim previous current bindings through profile-aware reuse when that does not conflict with live ownership.
125
+
126
+ ## Instance Identity
127
+
128
+ A registered instance exposes:
129
+
130
+ ```json
131
+ {
132
+ "instanceId": "uuid-or-runtime-id",
133
+ "pid": 12345,
134
+ "cwd": "/home/user/project",
135
+ "startedAt": "2026-05-20T10:00:00.000Z",
136
+ "owner": { "kind": "leader", "cwd": "/home/user/project" },
137
+ "threadName": "<valid-instance-identity>",
138
+ "target": { "chatId": 123456789, "threadId": 42 },
139
+ "status": "idle|active|queued|compacting|disconnected",
140
+ "lastHeartbeatAt": "2026-05-20T10:00:05.000Z"
141
+ }
142
+ ```
143
+
144
+ `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.
145
+
146
+ ## Leader Election
147
+
148
+ Leader election is heartbeat-gated and lock-backed:
149
+
150
+ 1. On startup, read the Telegram lock.
151
+ 2. If no leader exists, acquire leadership and start polling.
152
+ 3. If a live leader exists, register as follower.
153
+ 4. If the leader heartbeat is stale, attempt an atomic leadership takeover; ordinary `/telegram-connect` on a follower is not a leadership move while the leader is live.
154
+ 5. If several followers detect stale leadership, atomic compare/write lock acquisition ensures only one becomes leader.
155
+
156
+ Followers first try to re-register after leader reload or unknown-heartbeat responses, then promote only after the grace window expires. This preserves thread bindings through transient reload gaps without allowing competing pollers.
157
+
158
+ ## Leader/Follower Communication
159
+
160
+ Implemented transport:
161
+
162
+ ### Local IPC endpoint under agent temp dir
163
+
164
+ 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.
165
+
166
+ Pros:
167
+
168
+ - Natural request/response for sending Telegram API calls through the leader.
169
+ - Can route inbound updates to followers while preserving one poller.
170
+ - Good fit for live process membership.
171
+
172
+ Cons:
173
+
174
+ - Adds IPC lifecycle and security concerns.
175
+ - Cross-machine workers need tunneling or a different transport.
176
+
177
+ Alternative transports such as file-backed mailboxes or an external daemon remain out of the current product boundary. Local IPC is the default internal bus while the public design stays compatible with a future daemon if deployment needs outgrow one host.
178
+
179
+ ## Native Windows Smoke Plan
180
+
181
+ 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.
182
+
183
+ Manual smoke checklist:
184
+
185
+ 1. Enable Telegram private-chat Threaded Mode for the paired bot.
186
+ 2. Start Pi in one Windows terminal and run `/telegram-connect`; verify it becomes the leader and gets a named Telegram thread.
187
+ 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`.
188
+ 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.
189
+ 5. From the follower thread, request a voice reply and/or attachment; verify upload routes through the leader transport into the follower thread.
190
+ 6. Close the follower terminal; verify heartbeat pruning, disconnected notice, and cleanup behavior match Unix-like behavior.
191
+ 7. Reload the leader and verify status/debug output does not expose raw pipe internals except in explicit diagnostics.
192
+
193
+ If any step fails, capture `telegram-status --debug`, `tmp/telegram/state.json`, and `tmp/telegram/logs.jsonl` before retrying.
194
+
195
+ ### Native Windows Assumption Audit
196
+
197
+ Current portability audit:
198
+
199
+ - Local bus transport: adapted. Unix-like platforms use filesystem socket paths; native Windows uses named pipes so no POSIX socket pathname is required.
200
+ - 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.
201
+ - 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.
202
+ - 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.
203
+ - 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.
204
+ - Manual follower identity: process ids are used as local liveness/profile hints only, not cross-machine identifiers.
205
+
206
+ Remaining risk is live native Windows behavior: named-pipe creation/connect timing, antivirus/firewall/ACL interference, and provider command availability need operator smoke evidence.
207
+
208
+ ## Telegram Thread UX
209
+
210
+ In Telegram private-chat Threaded Mode:
211
+
212
+ - The private bot chat is a tabbed instance workspace, not a classic `General + threads` forum.
213
+ - `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.
214
+ - The leader proactively creates or reclaims its own thread on startup/activation when Threaded Mode is available, so the visible leader has the same two-way binding as followers.
215
+ - **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`.
216
+ - 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.
217
+ - 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.
218
+ - Bidirectional binding is a core UX requirement, not an implementation detail: Pi instances actively advertise/remember their thread identity, while the bot observes Telegram-client thread state and reflects 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.
219
+
220
+ In Telegram private-chat Threaded Mode:
221
+
222
+ - The private bot DM becomes the operator's multi-instance dashboard.
223
+ - Each live bound instance gets one visible thread.
224
+ - Each instance has a durable single-letter slot (`A`-`Z`) assigned by the extension and a bridge-authored `threadName`.
225
+ - 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.
226
+ - A follower that later becomes leader keeps its existing slot and thread name; leadership changes are transport role changes, not identity resets.
227
+ - Instance-thread names are 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.
228
+ - A thread-local `/start` opens that instance's menu.
229
+ - Prompts typed in a thread route to the owning instance.
230
+ - Replies, previews, files, voice, and buttons stay in that thread.
231
+ - Queue controls and reactions affect only that instance target.
232
+ - 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.
233
+ - If the instance disconnects, the leader can post/update a compact status: `Instance offline`.
234
+ - If the same live binding identity returns, it can reclaim the thread and post a compact reconnect status.
235
+
236
+ ## Bot API Evidence For Private-Chat Threaded Mode
237
+
238
+ 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:
239
+
240
+ - `User` returned by `getMe` can include `has_topics_enabled` and `allows_users_to_create_topics`; these are the private-chat Threaded Mode capability fields and are the startup/runtime probe source for this extension.
241
+ - `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.
242
+ - 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 Threaded Mode is disabled or unavailable for the bot.
243
+ - `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.
244
+ - 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`.
245
+ - `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`.
246
+
247
+ Non-goal: group detection is not the control-plane model for this extension. 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.
248
+
249
+ Remaining live-verification points:
250
+
251
+ - 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.
252
+ - 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.
253
+ - Whether every rich draft/final/upload/chat-action path behaves identically in Telegram clients when `message_thread_id` is supplied.
254
+
255
+ Implemented behavior stays evidence-gated: when Telegram client or Bot API behavior differs from the contract above, capture a minimized fixture or documented client caveat before changing routing.
256
+
257
+ ## Inbound Routing
258
+
259
+ The leader polls all updates for the bot token. It classifies each update into a target key:
260
+
261
+ ```text
262
+ targetKey = chatId + ':' + (threadId ?? 'private')
263
+ ```
264
+
265
+ Then it dispatches:
266
+
267
+ - If target belongs to the leader instance, handle locally.
268
+ - If target belongs to a follower, forward the normalized update/event to that follower.
269
+ - If target is unknown but authorized and setup allows provisioning, offer or create a binding.
270
+ - If target is unknown or unauthorized, ignore or send a safe denial.
271
+
272
+ Follower instances receive normalized events, not raw Telegram transport internals where possible. The follower still runs the same queue/routing logic, but Telegram API calls go back through the leader transport port.
273
+
274
+ ## Outbound Routing
275
+
276
+ Followers do not call Telegram Bot API directly for routed Telegram work. Instead, they call a leader-owned transport port:
277
+
278
+ ```text
279
+ follower reply/preview/upload/chat-action/download/callback-answer -> leader IPC -> Telegram API
280
+ ```
281
+
282
+ 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.
283
+
284
+ Every outbound request carries its target. The leader injects `message_thread_id` when `target.threadId` exists.
285
+
286
+ ## Queue And State Scoping
287
+
288
+ Each instance owns its own queue and active turn state. The leader does not become a central queue scheduler for all agents; that would be a separate daemon-mode architecture.
289
+
290
+ Target-scoped state requirements:
291
+
292
+ - Queue item identity includes target plus source message id.
293
+ - Reply deduplication is keyed by target, not just chat id.
294
+ - Preview draft state is keyed by target.
295
+ - Button callbacks store target and owning instance id.
296
+ - Reactions resolve to target/instance before mutation.
297
+ - Attachments generated by a follower are uploaded by the leader into the follower's target.
298
+
299
+ ## Configuration
300
+
301
+ There is no public `telegram.json` switch for the bus. Telegram private-chat Threaded Mode is the runtime switch: when Telegram exposes threads for the bot, 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.
302
+
303
+ Typical config remains just bot identity and authorization:
304
+
305
+ ```json
306
+ {
307
+ "botToken": "...",
308
+ "allowedUserId": 123456789
309
+ }
310
+ ```
311
+
312
+ Rules:
313
+
314
+ - 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.
315
+ - Telegram private-chat 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.
316
+ - 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.
317
+ - 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.
318
+ - 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.
319
+ - `allowedUserId` remains the primary authorization boundary unless explicit allowlists are added. Forum/group membership alone must not grant control.
320
+
321
+ ## Runtime State
322
+
323
+ Current state under the agent dir:
324
+
325
+ - `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.
326
+ - `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 mirrors `/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.
327
+ - 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.
328
+
329
+ 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.
330
+
331
+ All files containing routing, chat ids, thread ids, or process details use private permissions and represent current state rather than historical target caches.
332
+
333
+ ## Failure Modes
334
+
335
+ ### Leader exits cleanly
336
+
337
+ - Leader stops polling and marks itself offline.
338
+ - Followers detect missing heartbeat.
339
+ - One follower promotes itself after jitter/tie-break.
340
+ - New leader resumes `getUpdates` from the persisted offset if safe.
341
+
342
+ ### Leader crashes
343
+
344
+ - Followers detect stale heartbeat.
345
+ - One follower promotes itself.
346
+ - Some updates may be delayed or skipped depending on offset persistence; dispatcher design must define this explicitly.
347
+
348
+ ### Follower heartbeat is missed
349
+
350
+ - Leader prunes the follower from the live registry after missed heartbeats, but heartbeat pruning is only liveness bookkeeping.
351
+ - 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.
352
+ - 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.
353
+ - 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.
354
+ - Successful forwarded updates and follower-originated API calls refresh liveness, so active followers are not pruned only because the interval heartbeat tick lagged.
355
+ - Destructive follower thread teardown belongs to explicit `/telegram-disconnect` or confirmed reconciliation actions, not generic heartbeat pruning.
356
+ - Historical offline thread entries are not retained as reusable source of truth.
357
+
358
+ ### Thread is deleted
359
+
360
+ - Target mapping becomes stale.
361
+ - On next outbound failure or reconnect, leader records a diagnostic.
362
+ - Depending on policy, recreate a thread or mark the instance as needing operator action.
363
+
364
+ ### Split brain
365
+
366
+ - Two leaders calling `getUpdates` is the main safety failure.
367
+ - Lock heartbeat/takeover must be atomic enough to prevent this under normal local concurrency.
368
+ - If Telegram returns API conflict behavior, record diagnostics and force one leader to step down.
369
+
370
+ ## Security Boundaries
371
+
372
+ - Messages, edits, callbacks, and reactions check user authorization, not only chat/thread membership.
373
+ - 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.
374
+ - 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.
375
+ - Button and section callbacks verify authorized `from.id` and owning target/instance.
376
+ - Generated artifacts stay scoped to the owning thread after leader failover.
377
+ - Diagnostics redact bot tokens, large prompts, attachment paths, and handler output.
378
+
379
+ ## Acceptance Criteria
380
+
381
+ - [x] The lock semantics are redesigned as Telegram bus leadership with heartbeat and stale takeover rules.
382
+ - [x] A first-class `TelegramTarget` can represent classic private chats and thread destinations.
383
+ - [x] The bridge can run in classic mode with unchanged private-chat behavior.
384
+ - [x] A live Pi instance can register as a follower when another live instance is leader.
385
+ - [x] Followers never call `getUpdates` for the shared bot token.
386
+ - [x] Followers can send replies, previews, voice, attachments, menus, and chat actions through the leader transport.
387
+ - [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.
388
+ - [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.
389
+ - [x] Leader failover promotes one remaining follower without creating competing pollers.
390
+ - [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.
391
+ - [x] Authorization prevents arbitrary Telegram users or local processes from controlling agents or receiving artifacts.
392
+
393
+ Live client and native Windows evidence gates are tracked in `BACKLOG.md`; this architecture document records the implemented contract, not the active smoke queue.
394
+
395
+ ## Implemented Shape
396
+
397
+ - Bus semantics are the feature frame: Telegram threads are one Telegram UI substrate for a local multi-instance bus.
398
+ - `TelegramTarget` and target-key helpers represent classic private chats and thread destinations.
399
+ - Outbound ports, previews, replies, voice, attachments, chat actions, menus, sections, buttons, queue mutations, and direct local delivery carry target metadata where needed.
400
+ - The transport lock distinguishes live bus leadership from ordinary classic ownership through heartbeat, leader epoch, and stale takeover rules.
401
+ - The leader records live follower registration, heartbeat, thread identity, slot, and target mapping; followers do not poll `getUpdates`.
402
+ - Local IPC is the default internal bus. Registered followers receive normalized inbound updates and send allowlisted, target-scoped Bot API calls through the leader.
403
+ - Thread targets are current-state bindings, not durable historical delivery addresses. Stale/offline/failed entries are reconciliation evidence only.
404
+ - 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.
405
+ - Thread cleanup is centralized in `thread-reconciler`, fenced by leader epoch, and requires confirmed delete/stale evidence before state is marked deleted.
406
+ - 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.
407
+
408
+ ## Evidence Gates
409
+
410
+ Open live/client questions belong in `BACKLOG.md` until confirmed. Capture confirmed quirks as focused regressions or documented caveats, not broad speculative matrices.
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`