@llblab/pi-telegram 0.9.9 → 0.10.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.
@@ -1,83 +1,84 @@
1
- # Telegram Extension Sections Standard Draft
1
+ # Telegram Extension Sections Standard
2
2
 
3
- **Status:** Draft. This document is a design note for the upcoming Extension Sections platform and is not an implemented or stable public API yet. Treat all names, shapes, and examples as provisional until the implementation lands.
3
+ **Status:** Implemented in 0.10.0. Stable public API.
4
4
 
5
5
  **Meta-contract:** transportable (bit-for-bit identical across projects), high-density (zero fluff), constant (evolve by crystallizing, not speculating), optimal minimum (add only when it hurts).
6
6
 
7
7
  ---
8
8
 
9
- Telegram Extension Sections are a proposed registration contract that lets ordinary pi extensions add structured UI sections to the `pi-telegram` inline application menu.
9
+ ## 1. Philosophy
10
10
 
11
- The guiding philosophy is pi-native extensibility: `pi-telegram` should inherit π's own model of small composable extensions. The bridge should act as a shared Telegram shell for loaded π extensions, not as a closed one-off bot or a place where every feature must fork Telegram polling and transport.
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.
12
12
 
13
- They are not a new extension loader. pi still loads extensions through its normal TypeScript/package system. A loaded extension registers a Telegram section with `pi-telegram`; `pi-telegram` owns bot polling, menu rendering, callback routing, Telegram authorization, and message lifecycle.
13
+ `pi-telegram` stays the single bot operator. Extensions register typed sections; the bridge handles rendering, callback routing, token mapping, navigation hierarchy, and diagnostics. No second poller, no new loader just one `registerTelegramSection()` call.
14
14
 
15
- ## Purpose
15
+ ## 2. Contract Layers
16
16
 
17
- Use sections when an extension needs a Telegram-native UI surface inside the existing bot shell:
17
+ The standard operates across three integration surfaces:
18
18
 
19
- - File or project explorers
20
- - Prompt/session history viewers
21
- - Tool approval dashboards
22
- - Runtime status panels
23
- - Extension settings or diagnostics
24
- - Human-in-the-loop forms that should not become agent turns
25
-
26
- Do not use sections for plain agent prompts, one-shot buttons authored by the assistant, or command-template pipelines. Those stay in the normal queue, outbound action comments, inbound/outbound handlers, or command-template domains.
27
-
28
- ## Identity key
19
+ - **Extension API**: registration shape, context ports, `callbackData()`, `getLabel()`, navigation, disposer
20
+ - **Telegram Bot API**: 64-byte limit → token mapping, inline keyboard, `menu:back`/`settings:list` routing, stale-token answers
21
+ - **Pi Extension API**: typed import + `globalThis`, `pi.on("shutdown")` cleanup, load-order, identity
29
22
 
30
- Each section has one stable identity key.
23
+ ## 3. Identity Key
31
24
 
32
- Use the same identity rules as the Extension Locks Standard:
25
+ Each section has one stable identity key. Use the same rules as the Extension Locks Standard:
33
26
 
34
27
  1. `package.json/name` for npm-style pi packages
35
- 2. Directory name when the extension entrypoint is `index.ts` but there is no package name
36
- 3. File basename when the extension is a single file
28
+ 2. Directory name when the entrypoint is `index.ts` without `package.json`
29
+ 3. File basename for single-file extensions
37
30
 
38
- For npm-style package extensions, the canonical value is the `package.json` `name`.
39
-
40
- Examples:
41
-
42
- ```text
43
- extensions/pi-telegram-explorer/package.json name=@llblab/pi-telegram-explorer -> @llblab/pi-telegram-explorer
44
- extensions/pi-telegram-explorer/index.ts without package.json -> pi-telegram-explorer
45
- extensions/pi-telegram-explorer.ts -> pi-telegram-explorer
31
+ ```
32
+ extensions/pi-telegram-extension-demo/package.json name=@llblab/pi-telegram-extension-demo → @llblab/pi-telegram-extension-demo
33
+ extensions/pi-telegram-extension-demo/index.ts without package.json → pi-telegram-extension-demo
34
+ extensions/pi-telegram-extension-demo.ts → pi-telegram-extension-demo
46
35
  ```
47
36
 
48
- The section `id` is also the owner identity. Do not add a separate `owner` field unless a later concrete need appears.
49
-
50
- The identity key is used for:
51
-
52
- - Registry ownership
53
- - Conflict detection
54
- - Diagnostics
55
- - Cleanup
56
- - Callback routing lookup
57
- - Future capability policy
58
-
59
- ## Registration shape
37
+ The `id` is the owner identity. No separate `owner` field. Used for registry ownership, conflict detection, diagnostics, cleanup, and callback routing lookup.
60
38
 
61
- Minimum shape:
39
+ ## 4. Registration Shape
62
40
 
63
41
  ```ts
64
- registerTelegramSection({
65
- id: "@llblab/pi-telegram-explorer",
66
- label: "🗂 Explorer",
67
- render(ctx) {
68
- return {
69
- text: "<b>Explorer</b>",
42
+ import { registerTelegramSection } from "@llblab/pi-telegram/lib/extension-sections.ts";
43
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
44
+
45
+ export default function (pi: ExtensionAPI) {
46
+ const unregister = registerTelegramSection({
47
+ id: "@llblab/pi-telegram-extension-demo",
48
+ label: "🧪 Demo submenu",
49
+ order: 10,
50
+ render: async (ctx) => ({
51
+ text: "<b>Demo</b>",
70
52
  parseMode: "html",
71
- replyMarkup,
72
- };
73
- },
74
- handleCallback(ctx) {
75
- return "handled";
76
- },
77
- });
53
+ replyMarkup: {
54
+ inline_keyboard: [
55
+ [{ text: "Click me", callback_data: ctx.callbackData("act", "x") }],
56
+ ],
57
+ },
58
+ }),
59
+ handleCallback: async (ctx) => {
60
+ if (ctx.action === "act") {
61
+ await ctx.answerCallback(`payload: ${ctx.payload}`);
62
+ return "handled";
63
+ }
64
+ return "pass";
65
+ },
66
+ settings: {
67
+ label: "🧪 Demo settings",
68
+ order: 0,
69
+ getLabel: () => `${flag ? "🟢" : "⚫️"} Demo settings`,
70
+ open: async (ctx) => ({ text: "<b>Settings</b>", parseMode: "html" }),
71
+ handleCallback: async (ctx) => {
72
+ flag = ctx.payload === "on";
73
+ return "handled";
74
+ },
75
+ },
76
+ });
77
+ pi.on("shutdown", () => unregister());
78
+ }
78
79
  ```
79
80
 
80
- Recommended TypeScript shape:
81
+ ### Full TypeScript shape
81
82
 
82
83
  ```ts
83
84
  type TelegramSectionId = string;
@@ -88,11 +89,22 @@ interface TelegramSectionRegistration {
88
89
  label: string;
89
90
  order?: number;
90
91
  render: (
91
- ctx: TelegramSectionRenderContext,
92
+ ctx: TelegramSectionContext,
92
93
  ) => TelegramSectionView | Promise<TelegramSectionView>;
93
94
  handleCallback?: (
94
95
  ctx: TelegramSectionCallbackContext,
95
96
  ) => TelegramSectionCallbackResult | Promise<TelegramSectionCallbackResult>;
97
+ settings?: {
98
+ label: string;
99
+ order?: number;
100
+ getLabel?: () => string;
101
+ open: (
102
+ ctx: TelegramSectionContext,
103
+ ) => TelegramSectionView | Promise<TelegramSectionView>;
104
+ handleCallback?: (
105
+ ctx: TelegramSectionCallbackContext,
106
+ ) => TelegramSectionCallbackResult | Promise<TelegramSectionCallbackResult>;
107
+ };
96
108
  }
97
109
 
98
110
  interface TelegramSectionView {
@@ -102,192 +114,321 @@ interface TelegramSectionView {
102
114
  }
103
115
  ```
104
116
 
105
- Registration returns a disposer:
117
+ ### Registration returns a disposer
106
118
 
107
119
  ```ts
108
120
  const unregister = registerTelegramSection(section);
109
- unregister();
121
+ unregister(); // removes from main menu, settings, and callback routing
110
122
  ```
111
123
 
112
- ## Loading model
124
+ ## 5. Loading Model
113
125
 
114
- Sections are registered by normal pi extensions:
126
+ Two paths, same registry:
115
127
 
116
- ```ts
117
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
118
- import { registerTelegramSection } from "@llblab/pi-telegram/lib/extension-sections.ts";
128
+ **Typed import (preferred):** Extension imports `registerTelegramSection` from `@llblab/pi-telegram/lib/extension-sections.ts`. The function reads from a `globalThis` registry set by `pi-telegram` at startup.
119
129
 
120
- export default function (pi: ExtensionAPI) {
121
- const unregister = registerTelegramSection({
122
- id: "@llblab/pi-telegram-explorer",
123
- label: "🗂 Explorer",
124
- render: async (ctx) => ctx.html("<b>Explorer</b>"),
125
- });
126
- pi.on("shutdown", () => unregister());
127
- }
130
+ **Relative import (local):** When the extension cannot resolve `@llblab/pi-telegram` as an npm package, use a relative path:
131
+
132
+ ```ts
133
+ import { registerTelegramSection } from "../pi-telegram/lib/extension-sections.ts";
128
134
  ```
129
135
 
130
- `pi-telegram` may expose a typed import and a zero-coupling `globalThis` registry. The typed import is the preferred authoring path. The global registry exists only to tolerate load order and package coupling constraints.
136
+ **GlobalThis bridge (zero-coupling):** `pi-telegram` exposes `__piTelegramSectionRegistry__` on `globalThis`. The typed import is a thin wrapper. Extensions never touch the raw registry.
137
+
138
+ **Load order:** `pi-telegram` must load first (sets the global registry). Demo/consumer extensions load second (call `registerTelegramSection`). Pi's normal extension loader guarantees this when `pi-telegram` is listed first.
139
+
140
+ **Shutdown:** Call `pi.on("shutdown", () => unregister())` to clean up. The registry is cleared on `pi-telegram` unload.
141
+
142
+ ## 6. Menu Integration
143
+
144
+ Sections appear in two locations:
145
+
146
+ ### Main menu
147
+
148
+ Section rows are injected **before the ⚙️ Settings row**. Ordered by `order` (lower first), then `id` alphabetically.
131
149
 
132
- ## Menu integration
150
+ ```
151
+ 🤖 Model: anthropic/claude-sonnet-4-5
152
+ 🧠 Thinking: off
153
+ ⌛ Queue: 0
154
+ 🧪 Demo submenu ← extension section
155
+ ⚙️ Settings
156
+ ```
133
157
 
134
- `pi-telegram` owns the main Telegram application menu.
158
+ Built-in core rows keep priority. Section errors do not break menu rendering — a failed section is omitted with a diagnostic entry.
135
159
 
136
- Registered sections appear as top-level menu rows after built-in core sections unless an `order` value says otherwise.
160
+ ### Settings submenu
137
161
 
138
- Rules:
162
+ Extensions with a `settings` block inject rows **before built-in Proactive push**. The `getLabel()` function (if present) is called on every render to produce a dynamic label — use it for status indicators:
139
163
 
140
- - `label` must be compact enough for mobile Telegram
141
- - Built-in sections keep priority over external sections
142
- - Duplicate `id` registration is rejected or replaces only the same live owner through an explicit disposer path
143
- - Section errors must not break the whole main menu
144
- - If a section fails to render, `pi-telegram` should show a compact error row or omit the section and record diagnostics
164
+ ```ts
165
+ getLabel: () => `${flag ? "🟢" : "⚫️"} Demo settings`;
166
+ ```
167
+
168
+ ```
169
+ ⬆️ Main menu
170
+ 🟢 Demo settings ← extension settings (dynamic label)
171
+ 🟢 Proactive push
172
+ ```
145
173
 
146
- ## Callback routing
174
+ Ordered by `settings.order` (lower first), then `id` alphabetically.
147
175
 
148
- `pi-telegram` owns section callback transport.
176
+ ## 7. Callback Routing
149
177
 
150
- A section callback must include a `pi-telegram` owned prefix plus a compact section token that maps back to the full identity key.
178
+ ### Token mapping
151
179
 
152
- Conceptual form:
180
+ Telegram limits `callback_data` to 64 bytes. Full npm names like `@llblab/pi-telegram-explorer` often exceed this. `pi-telegram` maps each registered section to a compact numeric token:
153
181
 
154
182
  ```text
155
183
  section:<token>:<action>:<payload>
156
184
  ```
157
185
 
158
- The token is an implementation detail. The registry maps it to the canonical section `id`. `section:` is reserved in the [Callback Namespace Standard](./callback-namespaces.md); section authors should build callbacks through section context helpers rather than hand-crafting `section:` payloads.
186
+ Example: `section:0:counter:5`
159
187
 
160
- Routing order:
188
+ The token is an implementation detail. Section authors **never** write `section:` strings manually. Use `ctx.callbackData(action, payload?)` which fills in the correct token.
189
+
190
+ ### Routing order
161
191
 
162
192
  1. Telegram update arrives through the single `pi-telegram` poller
163
- 2. Existing low-level external handlers may observe or consume the update first
164
- 3. Built-in menu callbacks are handled by built-in domains
165
- 4. Section callbacks are resolved by token and sent to the registered section
166
- 5. Unknown callbacks fall back to the existing callback namespace behavior when appropriate
193
+ 2. External handlers observe/consume (raw update interception)
194
+ 3. Button action store (`tgbtn:*`)
195
+ 4. Queue menu callbacks (`queue:*`)
196
+ 5. Settings menu callbacks (`settings:*`)
197
+ 6. Built-in menu callbacks (`menu:*`, `model:*`, `thinking:*`, `status:*`)
198
+ 7. Section callbacks (`section:*`) — dispatched before step 6's full handler
199
+ 8. Unknown callbacks fall back to `[callback]` prompt text
200
+
201
+ ### Handler return values
202
+
203
+ - `"handled"`: callback consumed, stop routing, `answerCallbackQuery` already called
204
+ - `"pass"`: section declines; fallback to settings handler (if exists), then to caller
205
+
206
+ ### Fallback chain in `handleCallback`
207
+
208
+ ```
209
+ section.handleCallback(ctx) → "handled" | "pass"
210
+ └─ if "pass" and settings.handleCallback exists →
211
+ settings.handleCallback(newCtx with backCallback="settings:list")
212
+ ```
213
+
214
+ The fallback creates a **new context** with the correct `backCallback` for the navigation level.
215
+
216
+ ### Stale tokens
217
+
218
+ If a section is unregistered or a token is unknown, the callback is answered with a short Telegram native popup:
167
219
 
168
- Section handlers return:
220
+ > "This section is no longer available."
169
221
 
170
- - `"handled"` callback was handled, do not continue routing
171
- - `"pass"` — section declines this callback, allow fallback routing
222
+ Section errors are caught and surfaced as popup text. No unhandled exceptions leak to the poller.
172
223
 
173
- Stale callbacks:
224
+ ## 8. Navigation Hierarchy
174
225
 
175
- - Missing section id or token should answer the callback with a short stale/expired notice
176
- - Missing target state should re-render the section root when possible
177
- - Section errors should be caught, surfaced as a short callback answer, and recorded in diagnostics
226
+ `ctx.edit()` and `ctx.open()` automatically prepend a Back row. The Back target depends on the navigation level:
178
227
 
179
- ## Runtime ports
228
+ - Section root (from main menu): `⬆️ Main menu` → `menu:back`
229
+ - Section sub-view (`ctx.edit()` in handler): `⬆️ Back` → `section:<token>:open`
230
+ - Settings root (from Settings list): `⬆️ Back` → `settings:list`
231
+ - Settings sub-view (`ctx.edit()` in settings handler): `⬆️ Back` → `settings:list`
180
232
 
181
- A section receives a narrow context, not raw `pi-telegram` internals.
233
+ Section authors do not need to manage the Back button — it is added automatically and deduplicated when already present.
234
+
235
+ ```
236
+ Main menu
237
+ ├─ 🧪 Demo submenu ──── [⬆️ Main menu]
238
+ │ └─ Counter ─────── [⬆️ Back → Demo submenu]
239
+ └─ ⚙️ Settings ──────── [⬆️ Main menu]
240
+ └─ Demo settings ─ [⬆️ Back → Settings list]
241
+ └─ toggle ─── [⬆️ Back → Settings list] (via ctx.edit)
242
+ ```
182
243
 
183
- Initial safe ports:
244
+ ## 9. Context Ports
245
+
246
+ ### `TelegramSectionContext` — for `render()` and `settings.open()`
184
247
 
185
248
  ```ts
186
249
  interface TelegramSectionContext {
187
250
  sectionId: string;
188
251
  chatId: number;
189
252
  messageId?: number;
253
+ /** Answer the callback query with an optional popup text */
254
+ answerCallback(text?: string): Promise<void>;
255
+ /** Edit the current message (auto-prepends Back row) */
256
+ edit(view: TelegramSectionView): Promise<void>;
257
+ /** Send a new message (auto-prepends Back row) */
258
+ open(view: TelegramSectionView): Promise<void>;
259
+ /** Enqueue a plain-text prompt turn */
260
+ enqueuePrompt(prompt: string): Promise<void>;
261
+ /** Build a section-namespaced callback_data string */
262
+ callbackData(action: string, payload?: string): string;
263
+ }
264
+ ```
265
+
266
+ ### `TelegramSectionCallbackContext` — for `handleCallback()`
267
+
268
+ ```ts
269
+ interface TelegramSectionCallbackContext {
270
+ sectionId: string;
271
+ chatId: number;
272
+ messageId?: number;
273
+ /** The action segment from callback_data */
274
+ action: string;
275
+ /** The payload segment from callback_data */
276
+ payload: string;
190
277
  answerCallback(text?: string): Promise<void>;
191
278
  edit(view: TelegramSectionView): Promise<void>;
192
279
  open(view: TelegramSectionView): Promise<void>;
193
280
  enqueuePrompt(prompt: string): Promise<void>;
194
- getQueueSnapshot(): TelegramQueueSnapshot;
195
- getSessionSnapshot?(): TelegramSessionSnapshot;
281
+ callbackData(action: string, payload?: string): string;
196
282
  }
197
283
  ```
198
284
 
199
- Filesystem or prompt-history mutation is not part of the baseline. Add capability-specific ports only when the first real extension needs them.
285
+ ### `enqueuePrompt` semantics
200
286
 
201
- ## Security and authorization
287
+ Queues a `[telegram] <prompt>` turn in the default lane with the paired user's `chatId`. Uses `queueMutationRuntime.append()` and triggers `dispatchNextQueuedTelegramTurn()`. The prompt arrives as a normal Telegram-owned turn — the agent sees it as if the user typed it.
202
288
 
203
- `pi-telegram` keeps Telegram authorization ownership.
289
+ ### Capability scope
204
290
 
205
- Baseline rules:
291
+ Context ports are intentionally narrow. Sections **cannot**:
206
292
 
207
- - Section callbacks are accepted only from the paired/authorized Telegram user
208
- - Sections should not receive unauthorized updates
209
- - Sections must not start their own Telegram poller
210
- - Sections must not assume filesystem or session mutation rights
211
- - Sensitive capabilities should be exposed as explicit typed ports, not by passing raw process or bot clients
293
+ - Read/write filesystem
294
+ - Access raw process or bot clients
295
+ - Start a second poller
296
+ - Mutate session state
297
+ - Send arbitrary Telegram API calls
212
298
 
213
- For filesystem explorers, default to read-only browse and file-send behavior. Deleting, writing, shell execution, or rollback-like mutations require separate explicit capabilities and confirmation UI.
299
+ Add capability-specific ports only when the first real extension proves the need.
214
300
 
215
- ## Diagnostics
301
+ ### Interactive messages in chat (`ctx.open`)
216
302
 
217
- `pi-telegram` should be able to report registered sections.
303
+ `ctx.open()` sends a new message directly into the Telegram chat — outside the menu hierarchy. No Back row is prepended. Use it for extension-driven interactions that live in the conversation:
218
304
 
219
- Minimum diagnostic fields:
305
+ - Confirmation dialogs ("Delete file.txt?")
306
+ - Approve/deny gates ("Allow tool execution?")
307
+ - Multi-step forms that should not be menu-bound
308
+ - Status reports with action buttons
220
309
 
221
- ```text
222
- id
223
- label
224
- status: active | stale | error
225
- lastError
310
+ ```ts
311
+ handleCallback: async (ctx) => {
312
+ if (ctx.action === "delete-file") {
313
+ await ctx.open({
314
+ text: `<b>Delete ${ctx.payload}?</b>\n\nThis cannot be undone.`,
315
+ parseMode: "html",
316
+ replyMarkup: {
317
+ inline_keyboard: [[
318
+ { text: "✅ Yes, delete",
319
+ callback_data: ctx.callbackData("confirm-delete", ctx.payload) },
320
+ { text: "❌ Cancel",
321
+ callback_data: ctx.callbackData("cancel") },
322
+ ]],
323
+ },
324
+ });
325
+ return "handled";
326
+ }
327
+ if (ctx.action === "confirm-delete") {
328
+ // actual deletion logic
329
+ await ctx.answerCallback(`Deleted: ${ctx.payload}`);
330
+ return "handled";
331
+ }
332
+ }
226
333
  ```
227
334
 
228
- The identity key is sufficient as the owner label. Do not add a second owner field.
335
+ Callbacks from chat buttons route through the same `handleCallback` — the same `ctx.callbackData()` works regardless of where the button lives. The extension owns its callback namespace; the bridge owns transport.
229
336
 
230
- Useful future fields:
337
+ ## 10. Telegram Bot API Integration
338
+
339
+ ### `callback_data` contract
340
+
341
+ Section callbacks use the `section:` prefix owned by `pi-telegram`:
231
342
 
232
343
  ```text
233
- registeredAt
234
- lastRenderAt
235
- lastCallbackAt
236
- callbackCount
237
- errorCount
238
- capabilities
344
+ section:0:open → open section root
345
+ section:0:settings:open → open settings root
346
+ section:0:<action>:<payload> → forwarded to handleCallback
239
347
  ```
240
348
 
241
- Diagnostics should be available through a status/debug surface without cluttering normal Telegram UI.
349
+ `section:` is listed in `TELEGRAM_OWNED_CALLBACK_PREFIXES` alongside `menu:`, `model:`, `settings:`, `status:`, `tgbtn:`, `thinking:`, `queue:`. Layered extensions must not use this prefix.
350
+
351
+ ### Inline keyboard layout
352
+
353
+ Section views return `TelegramSectionView.replyMarkup` — a standard `TelegramInlineKeyboardMarkup`. The bridge prepends the Back row and sends the result through `editMessageText` / `sendMessage` with `parse_mode: "HTML"`.
354
+
355
+ Button labels are not truncated by the bridge. Section authors should keep labels compact for mobile Telegram (under ~30 display-width cells). Use `truncateTelegramButtonLabel` for long dynamic text.
356
+
357
+ ### Stale message handling
358
+
359
+ If the interactive message has expired (no stored model menu state), the callback receives:
360
+
361
+ > "Interactive message expired."
362
+
363
+ This applies to section callbacks as well — the state check runs before dispatch.
364
+
365
+ ## 11. Pi Extension API Inspiration
366
+
367
+ The platform inherits from π's own extension model:
368
+
369
+ - `export default function(pi)` → `registerTelegramSection(section)`
370
+ - `pi.on("shutdown", ...)` → disposer from `registerTelegramSection`
371
+ - Typed imports → typed import from `@llblab/pi-telegram/lib/extension-sections.ts`
372
+ - `globalThis` registry → `__piTelegramSectionRegistry__` on `globalThis`
373
+ - Identity from `package.json/name` → same identity rules as Locks Standard
374
+ - Narrow typed context ports → `TelegramSectionContext` / `TelegramSectionCallbackContext`
375
+ - Extension does not own transport → `pi-telegram` owns polling, message lifecycle
376
+
377
+ ## 12. Diagnostics
242
378
 
243
- ## Relationship to callback namespaces and external handlers
379
+ `getTelegramSectionDiagnostics()` returns:
244
380
 
245
- [Callback Namespaces](./callback-namespaces.md) define callback ownership names. [External Handlers](./external-handlers.md) define low-level raw update interception. Extension sections define the structured Telegram UI layer above both.
381
+ ```ts
382
+ interface TelegramSectionDiagnostic {
383
+ id: string;
384
+ token: string;
385
+ label: string;
386
+ status: "active" | "stale" | "error";
387
+ lastError?: string;
388
+ }
389
+ ```
246
390
 
247
- Sections still use namespaced callback data, but `pi-telegram` owns the `section:` prefix and maps compact tokens to canonical section identities. That keeps Telegram's 64-byte callback limit compatible with full npm package names such as `@llblab/pi-telegram-explorer`.
391
+ Available through `pi-telegram`'s `/telegram-status` or programmatically via `getTelegramSectionDiagnostics()`.
248
392
 
249
- Use external handlers when an extension needs direct raw Telegram update access, a custom callback namespace, or out-of-band Promise resolution.
393
+ ## 13. Purpose and Non-Goals
250
394
 
251
- Use extension sections when an extension needs a durable menu surface, callback routing, and Telegram UI lifecycle managed by `pi-telegram`.
395
+ ### Use sections for:
252
396
 
253
- ## Relationship to command templates
397
+ - File/project explorers
398
+ - Prompt/session history viewers
399
+ - Tool approval dashboards
400
+ - Runtime status panels
401
+ - Extension settings or diagnostics
402
+ - Human-in-the-loop forms that should not become agent turns
254
403
 
255
- Command templates execute local commands and pipelines through stdin/stdout.
404
+ ### Do not use sections for:
256
405
 
257
- Extension sections do not execute command templates by default. A section may call an extension-owned command or tool internally, but the section standard is a UI registration and callback-routing contract, not a shell execution contract.
406
+ - Plain agent prompts (use normal queue)
407
+ - One-shot assistant-authored buttons (use `telegram_button` outbound comments)
408
+ - Command-template pipelines (use inbound/outbound handlers)
258
409
 
259
- ## Non-goals
410
+ ### Non-goals:
260
411
 
261
412
  - No second Telegram poller
262
413
  - No new pi extension loader
263
414
  - No generic webview system
264
415
  - No default filesystem mutation API
265
- - No prompt rollback semantics in the base standard
266
- - No separate owner field while identity key is sufficient
267
-
268
- ## Evolution path
416
+ - No prompt rollback semantics
417
+ - No separate `owner` field while identity key is sufficient
269
418
 
270
- 0.10.0 minimum:
419
+ ## 14. Relationship to Other Standards
271
420
 
272
- - Section registry
273
- - Main menu integration
274
- - Section callback routing
275
- - Narrow runtime ports
276
- - Diagnostics for registered sections
277
- - Documentation for extension authors
421
+ - [Callback Namespaces](./callback-namespaces.md): defines `section:` as pi-telegram-owned prefix. Sections use namespaced callbacks but authors never hand-roll them
422
+ - [External Handlers](./external-handlers.md): raw update interception for direct Telegram update access. Sections are the structured UI layer above
423
+ - [Extension Locks](../docs/locks.md) (external): same identity key rules (`package.json/name` → canonical id)
424
+ - [Command Templates](./command-templates.md): sections do not execute command templates by default. UI registration + callback routing, not shell execution
278
425
 
279
- First demo extension candidate:
280
-
281
- ```text
282
- @llblab/pi-telegram-explorer
283
- ```
426
+ ## 15. Demo Extension
284
427
 
285
- Initial demo scope:
428
+ `@llblab/pi-telegram-extension-demo` (`extensions/pi-telegram-extension-demo/`) is the reference implementation:
286
429
 
287
- - Browse current project tree read-only
288
- - View compact file previews
289
- - Send selected files as Telegram documents
290
- - Browse recent prompt/session snapshots read-only
291
- - Enqueue a prompt derived from a selected item
430
+ - Main menu: `🧪 Demo submenu` — enqueue prompt, answer callback, show info, interactive counter
431
+ - Settings: `🧪 Demo settings` — ON/OFF toggle with dynamic `getLabel()` status indicator, enqueue from settings
432
+ - Navigation: full Back/Main menu hierarchy across all three levels
292
433
 
293
- Defer rollback, filesystem writes, deletes, and broad mutation until the read-only and enqueue-only model is proven.
434
+ Use it as a template for new section-based extensions.