@llblab/pi-telegram 0.9.3 → 0.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -0
- package/BACKLOG.md +6 -1
- package/CHANGELOG.md +17 -0
- package/README.md +1 -1
- package/docs/README.md +1 -0
- package/docs/architecture.md +24 -27
- package/docs/callback-namespaces.md +13 -1
- package/docs/command-templates.md +123 -22
- package/docs/extension-sections.md +293 -0
- package/docs/external-handlers.md +33 -75
- package/docs/inbound-handlers.md +6 -6
- package/docs/outbound-handlers.md +12 -12
- package/index.ts +1 -0
- package/lib/api.ts +8 -3
- package/lib/command-templates.ts +152 -6
- package/lib/menu-queue.ts +37 -7
- package/lib/menu-settings.ts +0 -1
- package/lib/preview.ts +37 -8
- package/lib/queue.ts +18 -10
- package/package.json +1 -1
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# Telegram Extension Sections Standard Draft
|
|
2
|
+
|
|
3
|
+
**Status:** Draft. This document is a design note for the upcoming Extension Sections platform and is not an implemented or stable public API yet. Treat all names, shapes, and examples as provisional until the implementation lands.
|
|
4
|
+
|
|
5
|
+
**Meta-contract:** transportable (bit-for-bit identical across projects), high-density (zero fluff), constant (evolve by crystallizing, not speculating), optimal minimum (add only when it hurts).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
Telegram Extension Sections are a proposed registration contract that lets ordinary pi extensions add structured UI sections to the `pi-telegram` inline application menu.
|
|
10
|
+
|
|
11
|
+
The guiding philosophy is pi-native extensibility: `pi-telegram` should inherit π's own model of small composable extensions. The bridge should act as a shared Telegram shell for loaded π extensions, not as a closed one-off bot or a place where every feature must fork Telegram polling and transport.
|
|
12
|
+
|
|
13
|
+
They are not a new extension loader. pi still loads extensions through its normal TypeScript/package system. A loaded extension registers a Telegram section with `pi-telegram`; `pi-telegram` owns bot polling, menu rendering, callback routing, Telegram authorization, and message lifecycle.
|
|
14
|
+
|
|
15
|
+
## Purpose
|
|
16
|
+
|
|
17
|
+
Use sections when an extension needs a Telegram-native UI surface inside the existing bot shell:
|
|
18
|
+
|
|
19
|
+
- File or project explorers
|
|
20
|
+
- Prompt/session history viewers
|
|
21
|
+
- Tool approval dashboards
|
|
22
|
+
- Runtime status panels
|
|
23
|
+
- Extension settings or diagnostics
|
|
24
|
+
- Human-in-the-loop forms that should not become agent turns
|
|
25
|
+
|
|
26
|
+
Do not use sections for plain agent prompts, one-shot buttons authored by the assistant, or command-template pipelines. Those stay in the normal queue, outbound action comments, inbound/outbound handlers, or command-template domains.
|
|
27
|
+
|
|
28
|
+
## Identity key
|
|
29
|
+
|
|
30
|
+
Each section has one stable identity key.
|
|
31
|
+
|
|
32
|
+
Use the same identity rules as the Extension Locks Standard:
|
|
33
|
+
|
|
34
|
+
1. `package.json/name` for npm-style pi packages
|
|
35
|
+
2. Directory name when the extension entrypoint is `index.ts` but there is no package name
|
|
36
|
+
3. File basename when the extension is a single file
|
|
37
|
+
|
|
38
|
+
For npm-style package extensions, the canonical value is the `package.json` `name`.
|
|
39
|
+
|
|
40
|
+
Examples:
|
|
41
|
+
|
|
42
|
+
```text
|
|
43
|
+
extensions/pi-telegram-explorer/package.json name=@llblab/pi-telegram-explorer -> @llblab/pi-telegram-explorer
|
|
44
|
+
extensions/pi-telegram-explorer/index.ts without package.json -> pi-telegram-explorer
|
|
45
|
+
extensions/pi-telegram-explorer.ts -> pi-telegram-explorer
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The section `id` is also the owner identity. Do not add a separate `owner` field unless a later concrete need appears.
|
|
49
|
+
|
|
50
|
+
The identity key is used for:
|
|
51
|
+
|
|
52
|
+
- Registry ownership
|
|
53
|
+
- Conflict detection
|
|
54
|
+
- Diagnostics
|
|
55
|
+
- Cleanup
|
|
56
|
+
- Callback routing lookup
|
|
57
|
+
- Future capability policy
|
|
58
|
+
|
|
59
|
+
## Registration shape
|
|
60
|
+
|
|
61
|
+
Minimum shape:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
registerTelegramSection({
|
|
65
|
+
id: "@llblab/pi-telegram-explorer",
|
|
66
|
+
label: "🗂 Explorer",
|
|
67
|
+
render(ctx) {
|
|
68
|
+
return {
|
|
69
|
+
text: "<b>Explorer</b>",
|
|
70
|
+
parseMode: "html",
|
|
71
|
+
replyMarkup,
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
handleCallback(ctx) {
|
|
75
|
+
return "handled";
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Recommended TypeScript shape:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
type TelegramSectionId = string;
|
|
84
|
+
type TelegramSectionCallbackResult = "handled" | "pass";
|
|
85
|
+
|
|
86
|
+
interface TelegramSectionRegistration {
|
|
87
|
+
id: TelegramSectionId;
|
|
88
|
+
label: string;
|
|
89
|
+
order?: number;
|
|
90
|
+
render: (
|
|
91
|
+
ctx: TelegramSectionRenderContext,
|
|
92
|
+
) => TelegramSectionView | Promise<TelegramSectionView>;
|
|
93
|
+
handleCallback?: (
|
|
94
|
+
ctx: TelegramSectionCallbackContext,
|
|
95
|
+
) => TelegramSectionCallbackResult | Promise<TelegramSectionCallbackResult>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface TelegramSectionView {
|
|
99
|
+
text: string;
|
|
100
|
+
parseMode?: "html" | "plain";
|
|
101
|
+
replyMarkup?: TelegramInlineKeyboardMarkup;
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Registration returns a disposer:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
const unregister = registerTelegramSection(section);
|
|
109
|
+
unregister();
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Loading model
|
|
113
|
+
|
|
114
|
+
Sections are registered by normal pi extensions:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
118
|
+
import { registerTelegramSection } from "@llblab/pi-telegram/lib/extension-sections.ts";
|
|
119
|
+
|
|
120
|
+
export default function (pi: ExtensionAPI) {
|
|
121
|
+
const unregister = registerTelegramSection({
|
|
122
|
+
id: "@llblab/pi-telegram-explorer",
|
|
123
|
+
label: "🗂 Explorer",
|
|
124
|
+
render: async (ctx) => ctx.html("<b>Explorer</b>"),
|
|
125
|
+
});
|
|
126
|
+
pi.on("shutdown", () => unregister());
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`pi-telegram` may expose a typed import and a zero-coupling `globalThis` registry. The typed import is the preferred authoring path. The global registry exists only to tolerate load order and package coupling constraints.
|
|
131
|
+
|
|
132
|
+
## Menu integration
|
|
133
|
+
|
|
134
|
+
`pi-telegram` owns the main Telegram application menu.
|
|
135
|
+
|
|
136
|
+
Registered sections appear as top-level menu rows after built-in core sections unless an `order` value says otherwise.
|
|
137
|
+
|
|
138
|
+
Rules:
|
|
139
|
+
|
|
140
|
+
- `label` must be compact enough for mobile Telegram
|
|
141
|
+
- Built-in sections keep priority over external sections
|
|
142
|
+
- Duplicate `id` registration is rejected or replaces only the same live owner through an explicit disposer path
|
|
143
|
+
- Section errors must not break the whole main menu
|
|
144
|
+
- If a section fails to render, `pi-telegram` should show a compact error row or omit the section and record diagnostics
|
|
145
|
+
|
|
146
|
+
## Callback routing
|
|
147
|
+
|
|
148
|
+
`pi-telegram` owns section callback transport.
|
|
149
|
+
|
|
150
|
+
A section callback must include a `pi-telegram` owned prefix plus a compact section token that maps back to the full identity key.
|
|
151
|
+
|
|
152
|
+
Conceptual form:
|
|
153
|
+
|
|
154
|
+
```text
|
|
155
|
+
section:<token>:<action>:<payload>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The token is an implementation detail. The registry maps it to the canonical section `id`. `section:` is reserved in the [Callback Namespace Standard](./callback-namespaces.md); section authors should build callbacks through section context helpers rather than hand-crafting `section:` payloads.
|
|
159
|
+
|
|
160
|
+
Routing order:
|
|
161
|
+
|
|
162
|
+
1. Telegram update arrives through the single `pi-telegram` poller
|
|
163
|
+
2. Existing low-level external handlers may observe or consume the update first
|
|
164
|
+
3. Built-in menu callbacks are handled by built-in domains
|
|
165
|
+
4. Section callbacks are resolved by token and sent to the registered section
|
|
166
|
+
5. Unknown callbacks fall back to the existing callback namespace behavior when appropriate
|
|
167
|
+
|
|
168
|
+
Section handlers return:
|
|
169
|
+
|
|
170
|
+
- `"handled"` — callback was handled, do not continue routing
|
|
171
|
+
- `"pass"` — section declines this callback, allow fallback routing
|
|
172
|
+
|
|
173
|
+
Stale callbacks:
|
|
174
|
+
|
|
175
|
+
- Missing section id or token should answer the callback with a short stale/expired notice
|
|
176
|
+
- Missing target state should re-render the section root when possible
|
|
177
|
+
- Section errors should be caught, surfaced as a short callback answer, and recorded in diagnostics
|
|
178
|
+
|
|
179
|
+
## Runtime ports
|
|
180
|
+
|
|
181
|
+
A section receives a narrow context, not raw `pi-telegram` internals.
|
|
182
|
+
|
|
183
|
+
Initial safe ports:
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
interface TelegramSectionContext {
|
|
187
|
+
sectionId: string;
|
|
188
|
+
chatId: number;
|
|
189
|
+
messageId?: number;
|
|
190
|
+
answerCallback(text?: string): Promise<void>;
|
|
191
|
+
edit(view: TelegramSectionView): Promise<void>;
|
|
192
|
+
open(view: TelegramSectionView): Promise<void>;
|
|
193
|
+
enqueuePrompt(prompt: string): Promise<void>;
|
|
194
|
+
getQueueSnapshot(): TelegramQueueSnapshot;
|
|
195
|
+
getSessionSnapshot?(): TelegramSessionSnapshot;
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Filesystem or prompt-history mutation is not part of the baseline. Add capability-specific ports only when the first real extension needs them.
|
|
200
|
+
|
|
201
|
+
## Security and authorization
|
|
202
|
+
|
|
203
|
+
`pi-telegram` keeps Telegram authorization ownership.
|
|
204
|
+
|
|
205
|
+
Baseline rules:
|
|
206
|
+
|
|
207
|
+
- Section callbacks are accepted only from the paired/authorized Telegram user
|
|
208
|
+
- Sections should not receive unauthorized updates
|
|
209
|
+
- Sections must not start their own Telegram poller
|
|
210
|
+
- Sections must not assume filesystem or session mutation rights
|
|
211
|
+
- Sensitive capabilities should be exposed as explicit typed ports, not by passing raw process or bot clients
|
|
212
|
+
|
|
213
|
+
For filesystem explorers, default to read-only browse and file-send behavior. Deleting, writing, shell execution, or rollback-like mutations require separate explicit capabilities and confirmation UI.
|
|
214
|
+
|
|
215
|
+
## Diagnostics
|
|
216
|
+
|
|
217
|
+
`pi-telegram` should be able to report registered sections.
|
|
218
|
+
|
|
219
|
+
Minimum diagnostic fields:
|
|
220
|
+
|
|
221
|
+
```text
|
|
222
|
+
id
|
|
223
|
+
label
|
|
224
|
+
status: active | stale | error
|
|
225
|
+
lastError
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
The identity key is sufficient as the owner label. Do not add a second owner field.
|
|
229
|
+
|
|
230
|
+
Useful future fields:
|
|
231
|
+
|
|
232
|
+
```text
|
|
233
|
+
registeredAt
|
|
234
|
+
lastRenderAt
|
|
235
|
+
lastCallbackAt
|
|
236
|
+
callbackCount
|
|
237
|
+
errorCount
|
|
238
|
+
capabilities
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Diagnostics should be available through a status/debug surface without cluttering normal Telegram UI.
|
|
242
|
+
|
|
243
|
+
## Relationship to callback namespaces and external handlers
|
|
244
|
+
|
|
245
|
+
[Callback Namespaces](./callback-namespaces.md) define callback ownership names. [External Handlers](./external-handlers.md) define low-level raw update interception. Extension sections define the structured Telegram UI layer above both.
|
|
246
|
+
|
|
247
|
+
Sections still use namespaced callback data, but `pi-telegram` owns the `section:` prefix and maps compact tokens to canonical section identities. That keeps Telegram's 64-byte callback limit compatible with full npm package names such as `@llblab/pi-telegram-explorer`.
|
|
248
|
+
|
|
249
|
+
Use external handlers when an extension needs direct raw Telegram update access, a custom callback namespace, or out-of-band Promise resolution.
|
|
250
|
+
|
|
251
|
+
Use extension sections when an extension needs a durable menu surface, callback routing, and Telegram UI lifecycle managed by `pi-telegram`.
|
|
252
|
+
|
|
253
|
+
## Relationship to command templates
|
|
254
|
+
|
|
255
|
+
Command templates execute local commands and pipelines through stdin/stdout.
|
|
256
|
+
|
|
257
|
+
Extension sections do not execute command templates by default. A section may call an extension-owned command or tool internally, but the section standard is a UI registration and callback-routing contract, not a shell execution contract.
|
|
258
|
+
|
|
259
|
+
## Non-goals
|
|
260
|
+
|
|
261
|
+
- No second Telegram poller
|
|
262
|
+
- No new pi extension loader
|
|
263
|
+
- No generic webview system
|
|
264
|
+
- No default filesystem mutation API
|
|
265
|
+
- No prompt rollback semantics in the base standard
|
|
266
|
+
- No separate owner field while identity key is sufficient
|
|
267
|
+
|
|
268
|
+
## Evolution path
|
|
269
|
+
|
|
270
|
+
0.10.0 minimum:
|
|
271
|
+
|
|
272
|
+
- Section registry
|
|
273
|
+
- Main menu integration
|
|
274
|
+
- Section callback routing
|
|
275
|
+
- Narrow runtime ports
|
|
276
|
+
- Diagnostics for registered sections
|
|
277
|
+
- Documentation for extension authors
|
|
278
|
+
|
|
279
|
+
First demo extension candidate:
|
|
280
|
+
|
|
281
|
+
```text
|
|
282
|
+
@llblab/pi-telegram-explorer
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Initial demo scope:
|
|
286
|
+
|
|
287
|
+
- Browse current project tree read-only
|
|
288
|
+
- View compact file previews
|
|
289
|
+
- Send selected files as Telegram documents
|
|
290
|
+
- Browse recent prompt/session snapshots read-only
|
|
291
|
+
- Enqueue a prompt derived from a selected item
|
|
292
|
+
|
|
293
|
+
Defer rollback, filesystem writes, deletes, and broad mutation until the read-only and enqueue-only model is proven.
|
|
@@ -1,60 +1,38 @@
|
|
|
1
1
|
# External Handlers
|
|
2
2
|
|
|
3
|
-
`pi-telegram` owns a single `getUpdates` long-poll connection per bot. Other
|
|
4
|
-
pi extensions cannot open a competing poller against the same bot — the
|
|
5
|
-
Telegram Bot API uses a per-bot `offset` cursor, and two pollers race each
|
|
6
|
-
other and lose updates.
|
|
3
|
+
`pi-telegram` owns a single `getUpdates` long-poll connection per bot. Other pi extensions cannot open a competing poller against the same bot — the Telegram Bot API uses a per-bot `offset` cursor, and two pollers race each other and lose updates.
|
|
7
4
|
|
|
8
|
-
This document describes the registry that lets layered pi extensions
|
|
9
|
-
(running in the same pi process) hook into `pi-telegram`'s polling loop and
|
|
10
|
-
react to inbound Telegram updates **before** `pi-telegram`'s default routing
|
|
11
|
-
fires.
|
|
5
|
+
This document describes the registry that lets layered pi extensions running in the same pi process hook into `pi-telegram`'s polling loop and react to inbound Telegram updates **before** `pi-telegram`'s default routing fires.
|
|
12
6
|
|
|
13
|
-
It is the runtime counterpart to
|
|
14
|
-
[Callback Namespaces](./callback-namespaces.md): callback namespaces define
|
|
15
|
-
how to share `callback_data` cleanly; external handlers define how to
|
|
16
|
-
observe and optionally short-circuit the dispatch of those updates.
|
|
7
|
+
It is the runtime counterpart to [Callback Namespaces](./callback-namespaces.md): callback namespaces define how to share `callback_data` cleanly; external handlers define how to observe and optionally short-circuit the dispatch of those updates.
|
|
17
8
|
|
|
18
9
|
## When to use it
|
|
19
10
|
|
|
20
11
|
Use it when a layered extension needs to:
|
|
21
12
|
|
|
22
|
-
- Resolve out-of-band state
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
- Suppress `pi-telegram`'s default routing for callbacks owned by the
|
|
26
|
-
layered extension (so `pi-telegram` does not also forward them as
|
|
27
|
-
`[callback] <data>` text).
|
|
28
|
-
- Observe arbitrary update types (messages, edits, channel posts, reactions)
|
|
29
|
-
without owning the polling connection.
|
|
13
|
+
- Resolve out-of-band state, for example a `tool_call` approval Promise, the moment a Telegram callback arrives, rather than waiting for the next agent turn.
|
|
14
|
+
- Suppress `pi-telegram`'s default routing for callbacks owned by the layered extension, so `pi-telegram` does not also forward them as `[callback] <data>` text.
|
|
15
|
+
- Observe arbitrary update types such as messages, edits, channel posts, or reactions without owning the polling connection.
|
|
30
16
|
|
|
31
|
-
If the layered extension only needs to read assistant-visible callbacks, the
|
|
32
|
-
|
|
33
|
-
[
|
|
17
|
+
If the layered extension only needs to read assistant-visible callbacks, the existing `[callback] <data>` fallback documented in [Callback Namespaces](./callback-namespaces.md) is enough.
|
|
18
|
+
|
|
19
|
+
If the extension needs a durable top-level Telegram menu section with managed rendering, callback routing, authorization, and diagnostics, use the higher-level [Telegram Extension Sections](./extension-sections.md) contract instead of a raw external handler.
|
|
34
20
|
|
|
35
21
|
## Constraints
|
|
36
22
|
|
|
37
|
-
- One bot, one pi process, one `getUpdates` poller. This registry does **not**
|
|
38
|
-
|
|
39
|
-
-
|
|
40
|
-
|
|
41
|
-
- Interceptor errors are caught and logged silently so polling never breaks.
|
|
42
|
-
If you need durable error reporting, do it inside your interceptor.
|
|
43
|
-
- The registry lives on `globalThis`. Module instance identity is not
|
|
44
|
-
required, so layered extensions can reach it without importing
|
|
45
|
-
`@llblab/pi-telegram`.
|
|
23
|
+
- One bot, one pi process, one `getUpdates` poller. This registry does **not** enable running multiple pi instances against the same bot.
|
|
24
|
+
- Interceptors run in the polling loop. They must return quickly; long awaits delay subsequent updates.
|
|
25
|
+
- Interceptor errors are caught and logged silently so polling never breaks. If you need durable error reporting, do it inside your interceptor.
|
|
26
|
+
- The registry lives on `globalThis`. Module instance identity is not required, so layered extensions can reach it without importing `@llblab/pi-telegram`.
|
|
46
27
|
|
|
47
28
|
## Verdicts
|
|
48
29
|
|
|
49
30
|
Each interceptor returns one of:
|
|
50
31
|
|
|
51
32
|
- `"consume"` — `pi-telegram` skips its default routing for this update.
|
|
52
|
-
- `"pass"`
|
|
53
|
-
normally. Other interceptors registered after this one still run for the
|
|
54
|
-
same update.
|
|
33
|
+
- `"pass"` or `void` / `undefined` — `pi-telegram` routes the update normally. Other interceptors registered after this one still run for the same update.
|
|
55
34
|
|
|
56
|
-
The first interceptor that returns `"consume"` wins; later interceptors are
|
|
57
|
-
not called for that update.
|
|
35
|
+
The first interceptor that returns `"consume"` wins; later interceptors are not called for that update.
|
|
58
36
|
|
|
59
37
|
## Registering an interceptor
|
|
60
38
|
|
|
@@ -79,18 +57,9 @@ off();
|
|
|
79
57
|
|
|
80
58
|
### Zero-coupling globalThis lookup
|
|
81
59
|
|
|
82
|
-
When the layered extension prefers no `import` from `@llblab/pi-telegram`
|
|
83
|
-
load order between the two extensions does not matter, and either can be
|
|
84
|
-
installed first), it must implement the **full v1 registry contract**, not
|
|
85
|
-
just `version` and `add`. pi-telegram's polling runtime calls `dispatch` on
|
|
86
|
-
whatever object it finds at `globalThis.__piTelegramExternalHandlerRegistry__`,
|
|
87
|
-
so a partial object would silently break the first update.
|
|
60
|
+
When the layered extension prefers no `import` from `@llblab/pi-telegram`, so load order between the two extensions does not matter and either can be installed first, it must implement the **full v1 registry contract**, not just `version` and `add`. pi-telegram's polling runtime calls `dispatch` on whatever object it finds at `globalThis.__piTelegramExternalHandlerRegistry__`, so a partial object would silently break the first update.
|
|
88
61
|
|
|
89
|
-
pi-telegram defensively re-creates the registry if the object on `globalThis`
|
|
90
|
-
is missing `add` or `dispatch` (validated as `version === 1`,
|
|
91
|
-
`typeof add === "function"`, `typeof dispatch === "function"`). Handlers
|
|
92
|
-
registered against a malformed object are dropped — make sure your bootstrap
|
|
93
|
-
implements all three fields.
|
|
62
|
+
pi-telegram defensively re-creates the registry if the object on `globalThis` is missing `add` or `dispatch`, validated as `version === 1`, `typeof add === "function"`, and `typeof dispatch === "function"`. Handlers registered against a malformed object are dropped — make sure your bootstrap implements all three fields.
|
|
94
63
|
|
|
95
64
|
```ts
|
|
96
65
|
type PiTelegramVerdict =
|
|
@@ -151,43 +120,32 @@ const off = getOrCreateRegistry().add((update) => {
|
|
|
151
120
|
});
|
|
152
121
|
```
|
|
153
122
|
|
|
154
|
-
The registry object on `globalThis.__piTelegramExternalHandlerRegistry__` is
|
|
155
|
-
versioned (`version: 1`) and stable across pi-telegram releases; future
|
|
156
|
-
breaking changes will use a new schema version and a new key.
|
|
123
|
+
The registry object on `globalThis.__piTelegramExternalHandlerRegistry__` is versioned (`version: 1`) and stable across pi-telegram releases; future breaking changes will use a new schema version and a new key.
|
|
157
124
|
|
|
158
125
|
## Interaction with built-in routing
|
|
159
126
|
|
|
160
|
-
`pi-telegram` invokes registered interceptors first, then routes the update
|
|
161
|
-
through its own handlers (commands, app menu, queue menu, model menu,
|
|
162
|
-
default prompt routing, callback namespace fallback). If any interceptor
|
|
163
|
-
returns `"consume"`, `pi-telegram` skips the rest of routing for that update.
|
|
127
|
+
`pi-telegram` invokes registered interceptors first, then routes the update through its own handlers: commands, app menu, queue menu, model menu, default prompt routing, and callback namespace fallback. If any interceptor returns `"consume"`, `pi-telegram` skips the rest of routing for that update.
|
|
164
128
|
|
|
165
129
|
This means:
|
|
166
130
|
|
|
167
|
-
- Extensions can claim callback namespaces that `pi-telegram` would
|
|
168
|
-
|
|
169
|
-
- Extensions
|
|
170
|
-
`"pass"`.
|
|
171
|
-
- Extensions must not consume updates that belong to `pi-telegram`'s own
|
|
172
|
-
prefixes (`tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`)
|
|
173
|
-
unless they are deliberately replacing that behavior.
|
|
131
|
+
- Extensions can claim callback namespaces that `pi-telegram` would otherwise forward as `[callback] <data>` text.
|
|
132
|
+
- Extensions can observe updates by always returning `"pass"`.
|
|
133
|
+
- Extensions must not consume updates that belong to `pi-telegram`'s own prefixes (`tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`) unless they are deliberately replacing that behavior.
|
|
174
134
|
|
|
175
135
|
## Ownership semantics
|
|
176
136
|
|
|
177
|
-
The interceptor registry is ownership-agnostic and does not interact with
|
|
178
|
-
the `locks.json` singleton lock documented in [Locks](./locks.md). When the
|
|
179
|
-
locked polling runtime stops `pi-telegram`'s poller (for example, after
|
|
180
|
-
ownership is moved to another pi process), interceptors stop receiving
|
|
181
|
-
updates because no updates are being fetched. They are not unregistered.
|
|
137
|
+
The interceptor registry is ownership-agnostic and does not interact with the `locks.json` singleton lock documented in [Locks](./locks.md). When the locked polling runtime stops `pi-telegram`'s poller, for example after ownership is moved to another pi process, interceptors stop receiving updates because no updates are being fetched. They are not unregistered.
|
|
182
138
|
|
|
183
|
-
If a layered extension needs to react to ownership changes, it should
|
|
184
|
-
observe `pi-telegram` lifecycle events through the standard pi extension
|
|
185
|
-
hooks rather than through the interceptor registry.
|
|
139
|
+
If a layered extension needs to react to ownership changes, it should observe `pi-telegram` lifecycle events through the standard pi extension hooks rather than through the interceptor registry.
|
|
186
140
|
|
|
187
141
|
## Not a multiplexer
|
|
188
142
|
|
|
189
|
-
This registry does not multiplex one bot across multiple pi processes, and
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
143
|
+
This registry does not multiplex one bot across multiple pi processes, and it does not bypass Telegram's single-poller-per-bot constraint. To run multiple pi instances on Telegram, give each instance its own bot and its own `~/.pi/agent` directory; the registry is for layered extensions inside **one** pi process.
|
|
144
|
+
|
|
145
|
+
## Relationship to extension sections
|
|
146
|
+
|
|
147
|
+
External handlers are the raw update primitive. Extension sections are the structured Telegram UI layer above that primitive.
|
|
148
|
+
|
|
149
|
+
Use external handlers for immediate update interception, custom callback namespaces, out-of-band Promise resolution, and update types that should not become a Telegram menu surface.
|
|
150
|
+
|
|
151
|
+
Use extension sections when the desired behavior is a menu-integrated UI: `render(ctx)`, managed callback dispatch, safe runtime ports, stale-callback handling, and diagnostics owned by `pi-telegram`.
|
package/docs/inbound-handlers.md
CHANGED
|
@@ -71,12 +71,12 @@ Media/file handlers keep the legacy attachment-handler behavior: downloaded file
|
|
|
71
71
|
|
|
72
72
|
Built-in placeholders for media/file handlers:
|
|
73
73
|
|
|
74
|
-
| Placeholder | Value
|
|
75
|
-
| ----------- |
|
|
76
|
-
| `{file}` |
|
|
77
|
-
| `{mime}` | MIME type if known
|
|
78
|
-
| `{type}` |
|
|
79
|
-
| `{text}` | Empty string
|
|
74
|
+
| Placeholder | Value |
|
|
75
|
+
| ----------- | ---------------------------------------------- |
|
|
76
|
+
| `{file}` | Downloaded file path |
|
|
77
|
+
| `{mime}` | MIME type if known |
|
|
78
|
+
| `{type}` | Kind: `voice`, `audio`, `document`, or `photo` |
|
|
79
|
+
| `{text}` | Empty string |
|
|
80
80
|
|
|
81
81
|
If a top-level one-step media handler template has no `{file}` placeholder, the downloaded file path is appended as the last command arg as a one-step handler convenience. Composition steps are plain command templates and do not receive implicit file-path args; include `{file}` explicitly where needed.
|
|
82
82
|
|
|
@@ -10,11 +10,11 @@ This document is the local outbound adaptation of the portable [Command Template
|
|
|
10
10
|
|
|
11
11
|
An outbound handler is selected by `type`. Text replies and assistant markup map to handler types:
|
|
12
12
|
|
|
13
|
-
| Source
|
|
14
|
-
|
|
|
15
|
-
| Final text
|
|
16
|
-
| `telegram_voice`
|
|
17
|
-
| `telegram_button` | Built-in
|
|
13
|
+
| Source | Type | Action |
|
|
14
|
+
| --- | --- | --- |
|
|
15
|
+
| Final text | `text` | Transform before render |
|
|
16
|
+
| `telegram_voice` | `voice` | Generate OGG/Opus |
|
|
17
|
+
| `telegram_button` | Built-in | Attach inline button |
|
|
18
18
|
|
|
19
19
|
Configured command-template handlers provide `template`. A string is one command; an array is ordered composition. Top-level `args` and `defaults` apply to all composed steps unless a step defines private values. The command-template default timeout applies automatically. `output` selects the primary artifact path when the handler produces a file instead of stdout text. Legacy configs may still use `pipe`, but `template: [...]` is the preferred standard shape.
|
|
20
20
|
|
|
@@ -96,13 +96,13 @@ The bridge strips the comment from Telegram text. On `agent_end`, it maps each `
|
|
|
96
96
|
|
|
97
97
|
Voice outbound handlers receive these runtime placeholders:
|
|
98
98
|
|
|
99
|
-
| Placeholder | Value
|
|
100
|
-
|
|
|
101
|
-
| `{text}`
|
|
102
|
-
| `{lang}`
|
|
103
|
-
| `{rate}`
|
|
104
|
-
| `{mp3}`
|
|
105
|
-
| `{ogg}`
|
|
99
|
+
| Placeholder | Value |
|
|
100
|
+
| --- | --- |
|
|
101
|
+
| `{text}` | Voice text from body, attr, or colon form |
|
|
102
|
+
| `{lang}` | Optional override, e.g. `lang=ru` |
|
|
103
|
+
| `{rate}` | Optional override, e.g. `rate=+30%` |
|
|
104
|
+
| `{mp3}` | Temp MP3 path under agent temp |
|
|
105
|
+
| `{ogg}` | Temp OGG path under agent temp |
|
|
106
106
|
|
|
107
107
|
Temp artifacts use unique flat names such as `<uuid>-voice.mp3` and `<uuid>-voice.ogg`. The bridge does not create per-handler directory trees.
|
|
108
108
|
|
package/index.ts
CHANGED
|
@@ -212,6 +212,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
212
212
|
sendMessage,
|
|
213
213
|
editMessageText: editTelegramMessageText,
|
|
214
214
|
canSend: lockOwnershipGuard.ownsCurrentProcess,
|
|
215
|
+
recordRuntimeEvent,
|
|
215
216
|
...replyTransport,
|
|
216
217
|
});
|
|
217
218
|
const { finalizeMarkdownPreview } =
|
package/lib/api.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { randomUUID } from "node:crypto";
|
|
|
8
8
|
import { createWriteStream, openAsBlob } from "node:fs";
|
|
9
9
|
import { mkdir, readdir, stat, unlink, writeFile } from "node:fs/promises";
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
|
-
import { join } from "node:path";
|
|
11
|
+
import { join, resolve } from "node:path";
|
|
12
12
|
import { Readable, Transform } from "node:stream";
|
|
13
13
|
import { pipeline } from "node:stream/promises";
|
|
14
14
|
|
|
@@ -28,7 +28,12 @@ export function getTelegramInboundFileByteLimitFromEnv(
|
|
|
28
28
|
return defaultValue;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
function getTelegramApiTempDir(): string {
|
|
32
|
+
const agentDir = process.env.PI_CODING_AGENT_DIR
|
|
33
|
+
? resolve(process.env.PI_CODING_AGENT_DIR)
|
|
34
|
+
: join(homedir(), ".pi", "agent");
|
|
35
|
+
return join(agentDir, "tmp", "telegram");
|
|
36
|
+
}
|
|
32
37
|
const TELEGRAM_TEMP_FILE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
33
38
|
const TELEGRAM_INBOUND_FILE_MAX_BYTES = getTelegramInboundFileByteLimitFromEnv(
|
|
34
39
|
process.env,
|
|
@@ -641,7 +646,7 @@ export function createDefaultTelegramBridgeApiRuntime(deps: {
|
|
|
641
646
|
}): TelegramBridgeApiRuntime {
|
|
642
647
|
return createTelegramBridgeApiRuntime({
|
|
643
648
|
client: createTelegramApiClient(deps.getBotToken),
|
|
644
|
-
tempDir:
|
|
649
|
+
tempDir: getTelegramApiTempDir(),
|
|
645
650
|
maxFileSizeBytes: TELEGRAM_INBOUND_FILE_MAX_BYTES,
|
|
646
651
|
tempFileMaxAgeMs: TELEGRAM_TEMP_FILE_MAX_AGE_MS,
|
|
647
652
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|