@llblab/pi-telegram 0.20.6 → 0.21.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.
- package/AGENTS.md +7 -5
- package/BACKLOG.md +3 -45
- package/CHANGELOG.md +24 -0
- package/README.md +16 -8
- package/api/activity.ts +15 -0
- package/api/commands.ts +1 -1
- package/api/delivery.ts +21 -0
- package/api/inbound.ts +1 -1
- package/api/keyboard.ts +1 -1
- package/api/outbound.ts +1 -1
- package/api/sections.ts +1 -1
- package/api/status.ts +2 -2
- package/api/updates.ts +1 -1
- package/api/voice.ts +1 -1
- package/docs/README.md +2 -0
- package/docs/activity.md +294 -0
- package/docs/architecture.md +21 -5
- package/docs/delivery.md +224 -0
- package/docs/public-api.md +93 -2
- package/docs/sections.md +6 -5
- package/index.ts +53 -0
- package/lib/activity.ts +683 -0
- package/lib/bindings.ts +60 -8
- package/lib/bus-api.ts +2 -1
- package/lib/bus-leader.ts +4 -1
- package/lib/bus.ts +38 -0
- package/lib/delivery.ts +800 -0
- package/lib/lifecycle.ts +23 -4
- package/lib/model.ts +3 -1
- package/lib/pi.ts +21 -5
- package/lib/routing.ts +18 -3
- package/lib/sections.ts +1 -1
- package/lib/status.ts +2 -2
- package/lib/updates.ts +4 -4
- package/package.json +6 -4
package/docs/activity.md
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# Telegram Activity API
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
The Telegram Activity API lets trusted extension consumers observe normalized Pi work lifecycle without importing pi-telegram internals, correlating raw Pi events with bridge state, or capturing session contexts.
|
|
6
|
+
|
|
7
|
+
It is a higher-level event surface over the [Telegram Delivery API](./delivery.md). Activity owns lifecycle normalization and event routing; delivery owns target authorization, rendering transport, ordering, chunk reconciliation, and stale-generation behavior.
|
|
8
|
+
|
|
9
|
+
The public membrane is:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import {
|
|
13
|
+
registerTelegramActivityHandler,
|
|
14
|
+
} from "@llblab/pi-telegram/activity";
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
An issue #126 consumer can own optional Settings policy for reasoning, intermediate assistant prose, and tool rows. Those visibility choices do not become mandatory bridge-core settings.
|
|
18
|
+
|
|
19
|
+
## Ownership Boundary
|
|
20
|
+
|
|
21
|
+
Consumer extension code owns:
|
|
22
|
+
|
|
23
|
+
- Which activity event classes are visible.
|
|
24
|
+
- Settings state and presentation policy.
|
|
25
|
+
- Rendering event payloads into operational views.
|
|
26
|
+
- Coalescing or replacing its own visible activity documents.
|
|
27
|
+
- Redacting additional domain-specific data before display.
|
|
28
|
+
|
|
29
|
+
pi-telegram owns:
|
|
30
|
+
|
|
31
|
+
- Mapping Pi lifecycle events into one normalized activity stream.
|
|
32
|
+
- Correlating events with the current Telegram turn or Pi instance.
|
|
33
|
+
- Stable activity/run identity within one runtime generation.
|
|
34
|
+
- Default delivery scope selection.
|
|
35
|
+
- Handler registration, ordering, isolation, disposal, and diagnostics.
|
|
36
|
+
- Avoiding duplicate provider tool-call versus executed-tool events.
|
|
37
|
+
- Session shutdown fencing and fresh delivery contexts.
|
|
38
|
+
|
|
39
|
+
Pi owns the source lifecycle and provider event protocol. The Activity API requires Pi `0.80.6` or newer because `agent_settled` is the terminal boundary that distinguishes a fully settled activity from automatic retry, compaction, and queued continuation. The API does not invent reasoning when a provider does not expose it.
|
|
40
|
+
|
|
41
|
+
## Registration
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
export interface TelegramActivityHandlerRegistration {
|
|
45
|
+
id: string;
|
|
46
|
+
order?: number;
|
|
47
|
+
handle: (
|
|
48
|
+
event: TelegramActivityEvent,
|
|
49
|
+
ctx: TelegramActivityContext,
|
|
50
|
+
) => void | Promise<void>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function registerTelegramActivityHandler(
|
|
54
|
+
registration: TelegramActivityHandlerRegistration,
|
|
55
|
+
): () => void;
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Rules:
|
|
59
|
+
|
|
60
|
+
- `id` is a stable consumer identity, normally derived from package identity plus a local activity suffix.
|
|
61
|
+
- Duplicate active ids are rejected rather than silently stacking duplicate Telegram output.
|
|
62
|
+
- Handlers run by `order`, then `id`.
|
|
63
|
+
- Registration returns a stale-safe disposer that removes only its own registration.
|
|
64
|
+
- Consumers register on `session_start` and dispose on `session_shutdown`.
|
|
65
|
+
- The bridge creates a fresh dispatcher generation on every `session_start`; shutdown stops and clears only the retiring generation, so same-process session replacement resumes delivery instead of leaving Activity permanently stopped.
|
|
66
|
+
- Handler failures are isolated and recorded in `/telegram-status`; they never break Pi lifecycle or other handlers.
|
|
67
|
+
|
|
68
|
+
## Activity Identity And Source
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
export type TelegramActivitySource =
|
|
72
|
+
| "telegram"
|
|
73
|
+
| "local"
|
|
74
|
+
| "autonomous"
|
|
75
|
+
| "unknown";
|
|
76
|
+
|
|
77
|
+
export type TelegramActivityTarget = Readonly<TelegramDeliveryTarget>;
|
|
78
|
+
|
|
79
|
+
export interface TelegramActivityEnvelope {
|
|
80
|
+
activityId: string;
|
|
81
|
+
sequence: number;
|
|
82
|
+
source: TelegramActivitySource;
|
|
83
|
+
target?: TelegramActivityTarget;
|
|
84
|
+
timestamp: number;
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`activityId` identifies one logical settled-work sequence inside the current delivery generation. `sequence` increases monotonically within that activity and lets consumers reject stale asynchronous rendering. Telegram-owned activities carry a frozen `target` captured when the activity starts; it contains only safe chat/thread identity, never a Telegram client or Pi context.
|
|
89
|
+
|
|
90
|
+
Source classification follows evidence, not guesses:
|
|
91
|
+
|
|
92
|
+
- `telegram`: a dispatched active Telegram turn exists.
|
|
93
|
+
- `local`: the initiating Pi input event reports `interactive` or `rpc` and no Telegram turn owns the run.
|
|
94
|
+
- `autonomous`: the initiating input reports `extension` and no Telegram turn owns the run.
|
|
95
|
+
- `unknown`: retries, restored continuations, or runtimes without enough input evidence cannot be classified safely.
|
|
96
|
+
|
|
97
|
+
Automatic retries, overflow compaction retries, and tool continuations inherit the current activity identity/source until `agent_settled`. A new unrelated `agent_start` after settlement allocates a new activity id.
|
|
98
|
+
|
|
99
|
+
A standalone compaction owns a temporary activity only until `session_compact`. Pi does not expose a sibling-extension cancellation/failure callback after `session_before_compact`, so pi-telegram abandons that temporary identity at the first provable fallback boundary: the next `agent_start`, a replacement compaction start, session shutdown, or the existing five-minute compaction-observer timeout. A late `session_compact` after abandonment is ignored and cannot attach to the next run. Compaction inside an existing agent activity never clears that agent's identity.
|
|
100
|
+
|
|
101
|
+
## Event Contract
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
export type TelegramActivityEvent = TelegramActivityEnvelope & (
|
|
105
|
+
| { type: "agent-start" }
|
|
106
|
+
| {
|
|
107
|
+
type: "assistant-text-delta";
|
|
108
|
+
contentIndex: number;
|
|
109
|
+
delta: string;
|
|
110
|
+
}
|
|
111
|
+
| {
|
|
112
|
+
type: "assistant-segment";
|
|
113
|
+
contentIndex: number;
|
|
114
|
+
text: string;
|
|
115
|
+
placement: "intermediate" | "final" | "terminal-partial";
|
|
116
|
+
}
|
|
117
|
+
| {
|
|
118
|
+
type: "reasoning-delta";
|
|
119
|
+
contentIndex: number;
|
|
120
|
+
delta: string;
|
|
121
|
+
}
|
|
122
|
+
| {
|
|
123
|
+
type: "reasoning-end";
|
|
124
|
+
contentIndex: number;
|
|
125
|
+
text: string;
|
|
126
|
+
}
|
|
127
|
+
| {
|
|
128
|
+
type: "tool-start";
|
|
129
|
+
toolCallId: string;
|
|
130
|
+
toolName: string;
|
|
131
|
+
args: unknown;
|
|
132
|
+
}
|
|
133
|
+
| {
|
|
134
|
+
type: "tool-update";
|
|
135
|
+
toolCallId: string;
|
|
136
|
+
toolName: string;
|
|
137
|
+
update: unknown;
|
|
138
|
+
}
|
|
139
|
+
| {
|
|
140
|
+
type: "tool-end";
|
|
141
|
+
toolCallId: string;
|
|
142
|
+
toolName: string;
|
|
143
|
+
result: unknown;
|
|
144
|
+
isError: boolean;
|
|
145
|
+
}
|
|
146
|
+
| {
|
|
147
|
+
type: "compaction-start";
|
|
148
|
+
reason: "manual" | "threshold" | "overflow" | "unknown";
|
|
149
|
+
}
|
|
150
|
+
| {
|
|
151
|
+
type: "compaction-end";
|
|
152
|
+
reason: "manual" | "threshold" | "overflow" | "unknown";
|
|
153
|
+
}
|
|
154
|
+
| { type: "agent-end" }
|
|
155
|
+
| { type: "agent-settled" }
|
|
156
|
+
);
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Assistant segment classification
|
|
160
|
+
|
|
161
|
+
Provider stream events expose `text_start`, `text_delta`, and `text_end`, but `text_end` alone does not prove whether prose is intermediate or final. The normalizer therefore holds a completed text segment briefly until the next provider boundary proves placement:
|
|
162
|
+
|
|
163
|
+
- A following `toolcall_start` classifies the pending text as `intermediate`.
|
|
164
|
+
- A following successful `done` classifies it as `final`.
|
|
165
|
+
- A following `error` classifies it as `terminal-partial`.
|
|
166
|
+
- A new text block flushes any older still-pending segment as `intermediate` only when the provider event order proves another block follows.
|
|
167
|
+
|
|
168
|
+
`assistant-text-delta` remains available for extensions that want progressive rendering. Consumers that only want complete intermediate prose should ignore deltas and render `assistant-segment` where `placement === "intermediate"`.
|
|
169
|
+
|
|
170
|
+
### Reasoning
|
|
171
|
+
|
|
172
|
+
Pi provider events use `thinking_*`; the public product term is `reasoning`. The normalizer maps `thinking_delta` to `reasoning-delta` and `thinking_end` to `reasoning-end`.
|
|
173
|
+
|
|
174
|
+
- Signed/redacted provider metadata is not exposed as display text.
|
|
175
|
+
- Empty reasoning blocks are skipped.
|
|
176
|
+
- Providers that hide reasoning produce no reasoning events.
|
|
177
|
+
- Consumers must treat reasoning as potentially sensitive and disabled by default.
|
|
178
|
+
|
|
179
|
+
### Tools
|
|
180
|
+
|
|
181
|
+
Tool activity uses Pi's executed-tool lifecycle (`tool_execution_start/update/end`), not provider `toolcall_*` payloads. Provider tool-call boundaries are used only to classify preceding assistant prose. This prevents duplicate tool rows and reports actual execution results.
|
|
182
|
+
|
|
183
|
+
`args`, `update`, and `result` may contain paths, source text, command output, or other sensitive data. They are available to trusted local extension code but must not be rendered wholesale by default. Reference UI should summarize tool name/state and expose bounded details only through explicit policy.
|
|
184
|
+
|
|
185
|
+
## Delivery Context
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
export interface TelegramActivityContext {
|
|
189
|
+
activityId: string;
|
|
190
|
+
sequence: number;
|
|
191
|
+
source: TelegramActivitySource;
|
|
192
|
+
defaultScope: TelegramDeliveryScope;
|
|
193
|
+
send(
|
|
194
|
+
view: TelegramDeliveryView,
|
|
195
|
+
options?: {
|
|
196
|
+
scope?: TelegramDeliveryScope;
|
|
197
|
+
replyToMessageId?: number;
|
|
198
|
+
},
|
|
199
|
+
): Promise<TelegramDeliveryResult<TelegramDeliveryHandle>>;
|
|
200
|
+
edit(
|
|
201
|
+
handle: TelegramDeliveryHandle,
|
|
202
|
+
view: TelegramDeliveryView,
|
|
203
|
+
): Promise<TelegramDeliveryResult<TelegramDeliveryHandle>>;
|
|
204
|
+
delete(
|
|
205
|
+
handle: TelegramDeliveryHandle,
|
|
206
|
+
): Promise<TelegramDeliveryResult<void>>;
|
|
207
|
+
chatAction(
|
|
208
|
+
action: TelegramDeliveryChatAction,
|
|
209
|
+
options?: { scope?: TelegramDeliveryScope },
|
|
210
|
+
): Promise<TelegramDeliveryResult<void>>;
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Default scope:
|
|
215
|
+
|
|
216
|
+
- `telegram` uses `{ kind: "target", target: event.target }`, binding delayed handlers to the immutable originating thread rather than whichever turn happens to be active later.
|
|
217
|
+
- `local`, `autonomous`, and `unknown` use `{ kind: "instance" }`.
|
|
218
|
+
|
|
219
|
+
Handlers may explicitly choose aggregate or another authorized scope. Context methods delegate to the public delivery runtime on every call; they do not retain Telegram clients, Pi contexts, or transport ownership objects. The Delivery API rechecks authorization when an operation runs, so a captured target that is no longer owned fails closed instead of rerouting.
|
|
220
|
+
|
|
221
|
+
A handler may still receive events while no target is currently deliverable. Delivery then returns the normal structured `target-unavailable`, `target-unauthorized`, or `runtime-unavailable` result.
|
|
222
|
+
|
|
223
|
+
## Dispatch And Backpressure
|
|
224
|
+
|
|
225
|
+
Pi lifecycle must not wait for extension rendering or Telegram transport.
|
|
226
|
+
|
|
227
|
+
- Lifecycle hooks normalize and enqueue activity events synchronously, then return.
|
|
228
|
+
- Each handler owns a serialized asynchronous queue so its event order is stable without blocking other handlers.
|
|
229
|
+
- Handler queues and contexts are generation-bound. `session_shutdown` stops queued events immediately, and an already-running handler receives `runtime-unavailable` instead of delivering through a replacement session after it resumes.
|
|
230
|
+
- Disposing a handler also fences contexts captured from that registration; they cannot outlive their ownership and adopt another registration with the same id.
|
|
231
|
+
- High-frequency `assistant-text-delta`, `reasoning-delta`, and `tool-update` events may coalesce only with the immediately adjacent event of the same type, activity id, content/tool id, and handler queue.
|
|
232
|
+
- Boundary events (`assistant-segment`, `reasoning-end`, tool start/end, compaction, agent end/settled) are never dropped or reordered.
|
|
233
|
+
- The 0.21 dispatcher does not impose an event-count drop policy or emit queue-length diagnostics. Adjacent delta/update coalescing reduces common streaming pressure, but boundary events accumulate behind a slow handler; consumers must keep handler work bounded and delegate long-lived work outside the callback when appropriate.
|
|
234
|
+
|
|
235
|
+
The Delivery API independently serializes concrete Telegram operations per target. Activity serialization preserves semantic event order; delivery serialization preserves transport order.
|
|
236
|
+
|
|
237
|
+
## Lifecycle Mapping
|
|
238
|
+
|
|
239
|
+
The bridge maps Pi hooks as follows:
|
|
240
|
+
|
|
241
|
+
- `input`: capture source evidence for the next logical activity.
|
|
242
|
+
- `agent_start`: allocate or reuse activity identity and emit `agent-start` after Telegram queue consumption establishes active-turn ownership.
|
|
243
|
+
- `message_update.assistantMessageEvent`: normalize text/reasoning/provider boundaries.
|
|
244
|
+
- `tool_execution_start/update/end`: emit executed tool events.
|
|
245
|
+
- `session_before_compact` / `session_compact`: emit compaction boundaries and preserve activity identity across retry compaction; abandon an unterminated standalone compaction at the next lifecycle boundary or observer timeout rather than merging it into another run. A missing or unrecognized reason maps to `unknown` rather than guessing.
|
|
246
|
+
- `agent_end`: emit low-level run completion but keep identity alive for retry/follow-up work.
|
|
247
|
+
- `agent_settled`: emit terminal settlement, flush pending terminal segments, and release activity identity.
|
|
248
|
+
- `session_shutdown`: stop dispatch, clear pending normalization state, and invalidate delivery generation through the existing delivery lifecycle.
|
|
249
|
+
|
|
250
|
+
## Diagnostics
|
|
251
|
+
|
|
252
|
+
Duplicate registration fails synchronously. When a handler throws or rejects, the dispatcher forwards only the handler id, event type, activity id, and error to pi-telegram's existing bounded/redacted runtime event recorder. The bridge does not copy reasoning, assistant prose, tool arguments/results, Telegram payloads, or queue contents into diagnostic metadata. It does not currently record queue phase, queue length, coalescing counts, or handler latency.
|
|
253
|
+
|
|
254
|
+
The first implementation has no public Activity diagnostics getter because handler failures already flow through bridge runtime diagnostics and `/telegram-status`. Add queue telemetry or a dedicated getter only when an observed consumer needs inspectable backpressure state.
|
|
255
|
+
|
|
256
|
+
## Security And Non-Goals
|
|
257
|
+
|
|
258
|
+
The Activity API does not:
|
|
259
|
+
|
|
260
|
+
- Enable reasoning visibility by default.
|
|
261
|
+
- Guarantee reasoning availability across providers.
|
|
262
|
+
- Expose signed/redacted reasoning metadata.
|
|
263
|
+
- Render raw tool arguments or results automatically.
|
|
264
|
+
- Replace assistant final replies or Rich Draft previews.
|
|
265
|
+
- Mutate queues, models, thinking levels, sessions, or process state.
|
|
266
|
+
- Expose Telegram clients, bot tokens, Pi contexts, or private runtime objects.
|
|
267
|
+
- Block Pi lifecycle on extension handlers or Telegram delivery.
|
|
268
|
+
|
|
269
|
+
## Validation Contract
|
|
270
|
+
|
|
271
|
+
The implementation must cover:
|
|
272
|
+
|
|
273
|
+
- Stable id registration, ordering, duplicate rejection, and stale-safe disposal.
|
|
274
|
+
- Telegram, local, autonomous, and unknown source evidence.
|
|
275
|
+
- Identity reuse across retry/compaction and reset at settlement.
|
|
276
|
+
- Text delta flow and intermediate/final/terminal segment classification.
|
|
277
|
+
- Reasoning mapping and empty/hidden reasoning behavior.
|
|
278
|
+
- Executed tool start/update/end without provider tool-call duplication.
|
|
279
|
+
- Default active-turn versus instance delivery scopes.
|
|
280
|
+
- Adjacent delta/update coalescing and boundary preservation.
|
|
281
|
+
- Handler error isolation, non-blocking lifecycle, shutdown fencing, and redacted diagnostics.
|
|
282
|
+
- Public package import without `/lib` reach-through.
|
|
283
|
+
|
|
284
|
+
## Consumer Policy Pattern
|
|
285
|
+
|
|
286
|
+
The registration and delivery examples above provide the complete public building blocks for issue #126-style visibility policy:
|
|
287
|
+
|
|
288
|
+
- Reasoning can default off and send completed `reasoning-end` blocks only when enabled.
|
|
289
|
+
- Intermediate prose can default off and send only `assistant-segment` events with `placement: "intermediate"`, never the final assistant segment.
|
|
290
|
+
- Tool rows can default on, show only tool name/state, and edit generation-bound logical handles from running to done/failed without exposing arguments or results.
|
|
291
|
+
- Interactive toggles belong in a registered Section and Settings row; activity messages can remain non-interactive.
|
|
292
|
+
- `session_shutdown` should dispose stable registrations and drop retained delivery handles, so reload/session replacement cannot reuse old contexts or handles.
|
|
293
|
+
|
|
294
|
+
The separate [`pi-telegram-extension-demo`](https://github.com/llblab/pi-telegram-extension-demo) project remains the maintained companion-extension and managed-UI reference. This document owns the Activity-specific usage pattern; pi-telegram does not ship a redundant `examples/` package directory.
|
package/docs/architecture.md
CHANGED
|
@@ -2,18 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## Purpose
|
|
4
4
|
|
|
5
|
-
`pi-telegram` is a session-
|
|
5
|
+
`pi-telegram` is a session-aware Pi runtime extension that binds Telegram destinations to running Pi instances and routes each accepted prompt into the assigned instance's currently active session. It owns the Telegram bridge boundary:
|
|
6
6
|
|
|
7
7
|
- Poll Telegram updates and enforce single-user pairing.
|
|
8
8
|
- Translate Telegram text, callbacks, media, and files into Pi turns.
|
|
9
9
|
- Stream previews and deliver final Pi responses back to Telegram.
|
|
10
10
|
- Provide Telegram-native controls for queueing, model/thinking/settings menus, compaction, abort/stop, prompt templates, reactions, and outbound artifacts.
|
|
11
11
|
|
|
12
|
-
The bridge is a mobile companion for a live Pi
|
|
12
|
+
The bridge is a mobile companion for a live Pi runtime, not a remote terminal or session browser. It should let an operator start work in the TUI and continue supervising the instance's active session from Telegram, while staying inside Pi's extension-facing contracts.
|
|
13
13
|
|
|
14
14
|
This document is the architectural map. Focused behavior standards live in sibling docs:
|
|
15
15
|
|
|
16
16
|
- [Public API](./public-api.md) — stable commands, config, package entrypoints, assistant markup, extension APIs, and compatibility boundaries.
|
|
17
|
+
- [Telegram Delivery API](./delivery.md) — target-aware operational views, logical message handles, lifecycle fencing, and leader/follower transport.
|
|
18
|
+
- [Telegram Activity API](./activity.md) — normalized Pi lifecycle events, activity/source identity, non-blocking extension dispatch, and delivery contexts.
|
|
17
19
|
- [UI Style](./ui-style.md) — inline UI labels, navigation, state markers, cards, and dialogs.
|
|
18
20
|
- [Callback Namespaces](./callback-namespaces.md) — callback prefix ownership and fallback rules.
|
|
19
21
|
- [Sections](./sections.md) — structured Telegram menu sections.
|
|
@@ -28,7 +30,7 @@ This document is the architectural map. Focused behavior standards live in sibli
|
|
|
28
30
|
|
|
29
31
|
### Extension Boundary Vs Supervisor Control
|
|
30
32
|
|
|
31
|
-
`pi-telegram` runs inside the current Pi process as an extension. That gives it safe access to public extension APIs such as aborting work, compacting,
|
|
33
|
+
`pi-telegram` runs inside the current Pi process as an extension. That gives it safe access to public extension APIs such as aborting work, compacting, dispatching queued prompts, observing lifecycle events, and rendering Telegram-native controls. It does not own the terminal, the interactive-mode chat transcript, or the process lifecycle.
|
|
32
34
|
|
|
33
35
|
Keep this boundary explicit:
|
|
34
36
|
|
|
@@ -37,6 +39,16 @@ Keep this boundary explicit:
|
|
|
37
39
|
- Commands that require interactive session replacement or TUI rerendering, such as a true Telegram `/new`, need a public Pi API that invokes the same runtime path as the terminal command.
|
|
38
40
|
- A separate PTY supervisor or daemon could choose to own those risks, but that would be a different product mode rather than this extension's runtime contract.
|
|
39
41
|
|
|
42
|
+
### Instance, Session, And Context Cost
|
|
43
|
+
|
|
44
|
+
A Telegram destination follows a Pi instance, not an immutable Pi session file. Ordinary Telegram prompts enter whichever session is active in that assigned instance when dispatch occurs. If the operator replaces or resumes a session locally, pi-telegram rebinds its session-scoped runtime state while preserving the instance's Telegram target where supported. Telegram currently exposes compaction for the active session, but not new-session, resume, fork, tree navigation, session switching, or full reload; those operations require safe public Pi extension APIs.
|
|
45
|
+
|
|
46
|
+
`/telegram-connect` never launches a hidden or headless Pi process. A long-lived background Pi process can own Telegram only when something else explicitly launched that process and it satisfies the normal lock/runtime rules. Pi `print` and `json` modes stay passive and exit rather than becoming hidden polling owners.
|
|
47
|
+
|
|
48
|
+
Pi session JSONL and pi-telegram runtime JSONL serve different purposes. Pi session files contain model conversation, tool, usage, branch, and compaction entries. Profile-scoped `logs.jsonl` / `logs.<profile>.jsonl` contain redacted bridge operations from one or more instances and never become model context. Sharing a Telegram profile or working directory does not by itself merge Pi session identities or model histories.
|
|
49
|
+
|
|
50
|
+
A Telegram prompt is a normal Pi model turn. It inherits the active post-compaction context just like a TUI prompt in the same session; pi-telegram does not promise context isolation or token cost proportional only to the new message. Current prompt guidance uses a small transient system note and the on-demand `telegram_help` tool instead of persisting the former large guidance suffix in every user turn. Existing session files created by older versions may still contain those historical repeated suffixes until session replacement or compaction removes them from active context.
|
|
51
|
+
|
|
40
52
|
The repository uses a **Flat Domain DAG**:
|
|
41
53
|
|
|
42
54
|
- Local imports must form a directed acyclic graph.
|
|
@@ -64,6 +76,8 @@ The repository uses a **Flat Domain DAG**:
|
|
|
64
76
|
- `sections`: Telegram menu-section registry, opaque section callback tokens, render/callback dispatch, safe section ports, and diagnostics.
|
|
65
77
|
- `keyboard`: shared inline-keyboard reply-markup shape only; feature domains own labels, callback data, and behavior.
|
|
66
78
|
- `preview` / `replies` / `rendering`: throttled native Rich Markdown draft delivery, native final reply delivery, reply parameters, transport-limit chunking, and remaining Telegram HTML rendering for bridge-owned UI/compatibility surfaces.
|
|
79
|
+
- `delivery`: public extension operational-view delivery, active-turn/instance/aggregate/authorized target policy, logical chunk handles, per-target ordering, runtime generation fencing, and the process-local runtime membrane. Its bridge adapter composes the established UI/compat reply renderer with narrow bus-aware Telegram API and ownership ports; it never exposes bot clients or Pi contexts.
|
|
80
|
+
- `activity`: public normalized Pi lifecycle registration, activity/source identity, assistant segment and reasoning normalization, executed-tool events, non-blocking per-handler queues, delivery contexts, compatibility adapters, and shutdown fencing. It does not own visibility policy or Telegram rendering.
|
|
67
81
|
- `outbound-markup`: top-level assistant action comment parsing, attribute parsing, voice reply planning, and preview/delivery stripping.
|
|
68
82
|
- `outbound`: outbound text transformations, voice/button artifact delivery, and generated callback actions.
|
|
69
83
|
- `outbound-attachments`: `telegram_attach`, queued outbound files, stat/limit checks, and photo/document delivery classification.
|
|
@@ -189,7 +203,7 @@ Dispatch requires:
|
|
|
189
203
|
|
|
190
204
|
A dispatched prompt remains queued until `agent_start` consumes it. This keeps the active Telegram turn bound for previews, attachments, aborts, and final replies.
|
|
191
205
|
|
|
192
|
-
Post-agent-end queue dispatch uses a session-bound deferred dispatcher. It is activated on session start, clears timers on shutdown, and skips callbacks from older generations before touching `ExtensionContext`. Dispatch stays session-bound after polling ownership moves elsewhere. When a queued Telegram prompt is forwarded into Pi, it uses
|
|
206
|
+
Post-agent-end queue dispatch uses a session-bound deferred dispatcher. It is activated on session start, clears timers on shutdown, and skips callbacks from older generations before touching `ExtensionContext`. Dispatch stays session-bound after polling ownership moves elsewhere. When a queued Telegram prompt is forwarded into Pi, it uses a normal `sendUserMessage(content)` turn after the bridge's idle/dispatch guards pass; it does not use Pi's `followUp` delivery option or inject terminal input.
|
|
193
207
|
|
|
194
208
|
### Controls And Menus
|
|
195
209
|
|
|
@@ -233,7 +247,7 @@ Assistant delivery guarantees:
|
|
|
233
247
|
|
|
234
248
|
- Model-authored Markdown is the source of truth; the bridge does not pre-render assistant Markdown to HTML unless the operator selects `assistant.rendering: "html"` for compatibility.
|
|
235
249
|
- Before native Rich Markdown delivery, the bridge normalizes known Bot-API-fragile source forms without changing visible meaning, including space-after-marker blockquotes and dollar-prefixed ticker atoms that Telegram may otherwise treat as unterminated math.
|
|
236
|
-
- Prompt context blocks use compact metadata (`[tag|key:value]`) as the stable inbound contract. `[telegram...]` names the current surface only: owner/current turns use `[telegram]` or `[telegram|thread:<name>]`; guest-mode turns use `[telegram|guest:<group-title-or-peer-username-or-id>]`. In a private Guest Mode turn the paired owner's `from` identity is never the guest:
|
|
250
|
+
- Prompt context blocks use compact metadata (`[tag|key:value]`) as the stable inbound contract. `[telegram...]` names the current surface only: owner/current turns use `[telegram]` or `[telegram|thread:<name>]`; guest-mode turns use `[telegram|guest:<group-title-or-peer-username-or-id>]`. In a private Guest Mode turn the paired owner's `from` identity is never the guest: the remote private-chat identity wins, then non-owner caller metadata, with a non-bot replied peer available only as a final identity fallback when stronger conversation evidence is absent; username falls back to the remote display name and numeric id. Reply attribution still belongs independently in `[reply|from:...]`, and a replied bot can never define or replace the current `[telegram|guest:...]` location identity. Source authors for quoted/forwarded material and their files are carried by `[reply|from:<username-or-id>]`, `[forward|from:<username-or-id>]`, and `[attachments|from:<username-or-id>]`, while plain `[attachments]` remains current-turn attachments and is ordered before reply/forward/source context.
|
|
237
251
|
- Quoted rich replies use Telegram `rich_message` blocks as the prompt-context source when available, so `[reply]` context receives rendered plain text instead of raw `InputRichMessage.markdown` fallback text.
|
|
238
252
|
- Long native Markdown replies are split only at Telegram Rich Message transport limits; oversized fenced code, display-math, and fully wrapped inline-formatting blocks are rewrapped per chunk so persisted Rich Markdown chunks remain structurally valid.
|
|
239
253
|
- When Draft previews are enabled, streaming previews pass structurally closed assistant Markdown prefixes through to `sendRichMessageDraft` with ownership checks, voice suppression, and serialized flushes. Unclosed inline spans, links, fenced code, comments, and display-math blocks are held back until a safe boundary exists. Draft failures are recorded and the failing frame is skipped instead of degrading to raw plain-message previews, because partial Markdown can be invalid while the final message remains valid.
|
|
@@ -265,6 +279,8 @@ Unknown callback data outside owned prefixes is forwarded as `[callback] <data>`
|
|
|
265
279
|
|
|
266
280
|
- Raw update observation/consumption: [Updates](./updates.md).
|
|
267
281
|
- Telegram-native slash commands: `registerTelegramCommand()` from [Public API](./public-api.md#commands).
|
|
282
|
+
- Target-aware operational views and chat actions: [Telegram Delivery API](./delivery.md).
|
|
283
|
+
- Normalized non-blocking Pi lifecycle events: [Telegram Activity API](./activity.md); the separate [`pi-telegram-extension-demo`](https://github.com/llblab/pi-telegram-extension-demo) project remains the companion-extension reference.
|
|
268
284
|
- Structured inline UI sections: [Sections](./sections.md).
|
|
269
285
|
- Callback namespace discipline: [Callback Namespaces](./callback-namespaces.md).
|
|
270
286
|
- Voice/STT/TTS providers: [Voice Integration](./voice.md).
|
package/docs/delivery.md
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# Telegram Delivery API
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
The Telegram Delivery API gives trusted extension consumers a safe programmatic way to render operational Telegram UI without importing bridge internals or owning Telegram transport.
|
|
6
|
+
|
|
7
|
+
It fills the boundary between callback-scoped `TelegramSectionContext.open()` and agent-callable `telegram_message` / `telegram_attach`. It does not replace normal active-turn final replies, Sections, outbound handlers, or raw update handlers.
|
|
8
|
+
|
|
9
|
+
The public package membrane is:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import {
|
|
13
|
+
deleteTelegramView,
|
|
14
|
+
editTelegramView,
|
|
15
|
+
sendTelegramChatAction,
|
|
16
|
+
sendTelegramView,
|
|
17
|
+
} from "@llblab/pi-telegram/delivery";
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Ownership Boundary
|
|
21
|
+
|
|
22
|
+
Consumer extension code owns:
|
|
23
|
+
|
|
24
|
+
- When an operational view should appear.
|
|
25
|
+
- View text, parse mode, and optional inline keyboard.
|
|
26
|
+
- Its own settings and callback policy.
|
|
27
|
+
- Retaining a returned handle only for the current live extension generation.
|
|
28
|
+
|
|
29
|
+
pi-telegram owns:
|
|
30
|
+
|
|
31
|
+
- Active-turn and current-instance target resolution.
|
|
32
|
+
- Pairing and target authorization.
|
|
33
|
+
- Classic direct transport versus follower-to-leader routing.
|
|
34
|
+
- Named-profile isolation.
|
|
35
|
+
- Per-target operation ordering.
|
|
36
|
+
- Telegram text limits, rendering, and chunk reconciliation.
|
|
37
|
+
- Runtime generation checks, shutdown behavior, and diagnostics.
|
|
38
|
+
- Bot credentials, polling, offsets, retries, and raw API clients.
|
|
39
|
+
|
|
40
|
+
## Public Contract
|
|
41
|
+
|
|
42
|
+
### Views
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
export type TelegramDeliveryParseMode = "plain" | "html" | "markdown";
|
|
46
|
+
|
|
47
|
+
export interface TelegramDeliveryView {
|
|
48
|
+
text: string;
|
|
49
|
+
parseMode?: TelegramDeliveryParseMode;
|
|
50
|
+
replyMarkup?: TelegramInlineKeyboardMarkup;
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`plain` is the default. Operational activity should prefer `plain` or explicit `html`. `markdown` exists for extension-authored content that naturally owns Markdown; the bridge converts it through the existing UI/compat Markdown-to-HTML renderer rather than entering the native assistant final-reply pipeline.
|
|
55
|
+
|
|
56
|
+
`replyMarkup` accepts only structural keyboard data. Callback ownership stays with Sections or a registered raw update handler. The documented issue #126 consumer shape uses Sections for interactive Settings toggles and keeps delivered activity rows non-interactive, so a second managed callback registry would duplicate token, answer, edit, navigation, and cleanup ownership without a proven use case. Revisit only when a public-import-only consumer must generate managed callbacks independently of a registered Section context for arbitrary delivered messages.
|
|
57
|
+
|
|
58
|
+
### Target scopes
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
export type TelegramDeliveryScope =
|
|
62
|
+
| { kind: "active-turn" }
|
|
63
|
+
| { kind: "instance" }
|
|
64
|
+
| { kind: "aggregate" }
|
|
65
|
+
| { kind: "target"; target: TelegramDeliveryTarget };
|
|
66
|
+
|
|
67
|
+
export interface TelegramDeliveryTarget {
|
|
68
|
+
chatId: number;
|
|
69
|
+
threadId?: number;
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Resolution rules:
|
|
74
|
+
|
|
75
|
+
- `active-turn` requires a current Telegram-owned turn and resolves its exact `{ chatId, threadId? }`.
|
|
76
|
+
- `instance` resolves the current process's assigned follower/leader thread, or the paired private chat in classic mode. It does not silently fall back to an unrelated active thread.
|
|
77
|
+
- `aggregate` resolves the paired private chat without `threadId`; it is the Threaded Mode `All` surface and the ordinary classic chat. Follower aggregate messages carry an internal authenticated-bus marker that the leader validates for the assigned chat and strips before Bot API transport; unmarked or cross-chat threadless follower writes remain denied.
|
|
78
|
+
- `target` validates an explicit destination against the active profile and current runtime authority. A classic owner may target its paired private chat. A follower may target only its assigned thread or aggregate surface. A leader may target its own thread, aggregate surface, or a currently live bound thread for that profile. Unknown, stale, cross-profile, and unpaired targets are rejected.
|
|
79
|
+
|
|
80
|
+
No scope selects another named profile. Profile activation remains session-local bridge state.
|
|
81
|
+
|
|
82
|
+
### Handles
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
export interface TelegramDeliveryHandle {
|
|
86
|
+
readonly target: TelegramDeliveryTarget;
|
|
87
|
+
readonly messageIds: readonly number[];
|
|
88
|
+
readonly generation: string;
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
A handle represents one logical view, which may span multiple Telegram messages after chunking. `generation` is an opaque runtime identity, not an authorization secret. Callers must not construct handles or persist them across reload/session replacement.
|
|
93
|
+
|
|
94
|
+
Edit reconciles the logical view as one operation:
|
|
95
|
+
|
|
96
|
+
- Existing chunks are edited in order.
|
|
97
|
+
- Additional chunks are sent when the new view grows.
|
|
98
|
+
- Surplus old chunks are deleted when the new view shrinks.
|
|
99
|
+
- The returned handle replaces the previous handle.
|
|
100
|
+
|
|
101
|
+
Delete removes every message still represented by the handle. Partial transport failure returns a structured failure and records diagnostics; it never pretends the whole logical view succeeded.
|
|
102
|
+
|
|
103
|
+
### Results
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
export type TelegramDeliveryFailureReason =
|
|
107
|
+
| "runtime-unavailable"
|
|
108
|
+
| "target-unavailable"
|
|
109
|
+
| "target-unauthorized"
|
|
110
|
+
| "stale-handle"
|
|
111
|
+
| "invalid-view"
|
|
112
|
+
| "transport-failed";
|
|
113
|
+
|
|
114
|
+
export type TelegramDeliveryResult<T> =
|
|
115
|
+
| { ok: true; value: T }
|
|
116
|
+
| {
|
|
117
|
+
ok: false;
|
|
118
|
+
reason: TelegramDeliveryFailureReason;
|
|
119
|
+
message: string;
|
|
120
|
+
partial?: T;
|
|
121
|
+
};
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Expected availability, authorization, and lifecycle outcomes return failures rather than throwing. Programmer errors may still throw for malformed objects that cannot satisfy the TypeScript contract. Transport failures are redacted before reaching callers and are also recorded in bridge runtime diagnostics.
|
|
125
|
+
|
|
126
|
+
When a multi-chunk `send` or growing `edit` fails after materializing part of the logical view, `partial` contains a valid handle for every message still visible from that operation. Callers may pass it to `editTelegramView` to reconcile the view or to `deleteTelegramView` for cleanup. A failure before any message exists omits `partial`; callers must never infer message ids or construct a handle.
|
|
127
|
+
|
|
128
|
+
### Operations
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
export interface SendTelegramViewOptions {
|
|
132
|
+
scope: TelegramDeliveryScope;
|
|
133
|
+
replyToMessageId?: number;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function sendTelegramView(
|
|
137
|
+
view: TelegramDeliveryView,
|
|
138
|
+
options: SendTelegramViewOptions,
|
|
139
|
+
): Promise<TelegramDeliveryResult<TelegramDeliveryHandle>>;
|
|
140
|
+
|
|
141
|
+
export function editTelegramView(
|
|
142
|
+
handle: TelegramDeliveryHandle,
|
|
143
|
+
view: TelegramDeliveryView,
|
|
144
|
+
): Promise<TelegramDeliveryResult<TelegramDeliveryHandle>>;
|
|
145
|
+
|
|
146
|
+
export function deleteTelegramView(
|
|
147
|
+
handle: TelegramDeliveryHandle,
|
|
148
|
+
): Promise<TelegramDeliveryResult<void>>;
|
|
149
|
+
|
|
150
|
+
export function sendTelegramChatAction(
|
|
151
|
+
action: "typing" | "upload_document" | "upload_photo" | "record_voice",
|
|
152
|
+
options: { scope: TelegramDeliveryScope },
|
|
153
|
+
): Promise<TelegramDeliveryResult<void>>;
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`replyToMessageId` applies only to the first chunk and must belong to the resolved chat. The initial contract exposes only actions already used by bridge-owned activity and delivery paths; it is not a generic Bot API action string.
|
|
157
|
+
|
|
158
|
+
## Runtime Binding
|
|
159
|
+
|
|
160
|
+
The public functions resolve a process-local runtime binding on every call. They never capture a Pi `ExtensionContext` or command context.
|
|
161
|
+
|
|
162
|
+
The bridge constructs and binds a genuinely fresh delivery runtime during every `session_start`. It unbinds and shuts down the current runtime during `session_shutdown` before session-bound transport state is discarded; binding an unexpected replacement also shuts down the displaced runtime. Reload and session replacement therefore produce these outcomes:
|
|
163
|
+
|
|
164
|
+
- A new call resolves the newly bound runtime after startup.
|
|
165
|
+
- An old handle returns `stale-handle` for edit and delete.
|
|
166
|
+
- A call while no generation is bound returns `runtime-unavailable`.
|
|
167
|
+
- Queued operations from the old generation stop before transport begins.
|
|
168
|
+
- An already-issued Telegram request may resolve during shutdown, but the old operation returns `runtime-unavailable` and cannot issue another edit, delete, chunk, or chat action afterward.
|
|
169
|
+
- Old operations never adopt the replacement generation implicitly.
|
|
170
|
+
|
|
171
|
+
The binding uses the same `globalThis` membrane pattern as other extension registries so package load order does not expose bridge internals. Only pi-telegram may bind or replace the runtime port.
|
|
172
|
+
|
|
173
|
+
## Ordering And Delivery Semantics
|
|
174
|
+
|
|
175
|
+
- Operations serialize per profile and concrete target. Different targets may progress independently.
|
|
176
|
+
- `send`, logical `edit`, and logical `delete` preserve caller order for the same target.
|
|
177
|
+
- Delivery uses the existing bridge API runtime so followers route allowlisted calls through the leader rather than contacting Telegram directly.
|
|
178
|
+
- Text is chunked through the existing parse-mode-appropriate renderer/splitter. HTML chunks remain balanced, extension-authored Markdown becomes balanced UI/compat HTML, and plain chunks preserve text.
|
|
179
|
+
- Inline keyboard markup attaches only to the final chunk of a logical view. Reply parameters attach only to the first chunk.
|
|
180
|
+
- Edit growth sends additional chunks; edit shrink deletes surplus chunks; delete removes every chunk in handle order.
|
|
181
|
+
- A partial send failure returns the ids already sent. A partial edit failure returns the original surviving ids plus every newly sent id, minus any surplus ids already deleted during shrink. The returned handle therefore remains sufficient for deterministic retry or cleanup.
|
|
182
|
+
- The API does not participate in assistant preview/final deduplication and does not mutate the Telegram turn queue.
|
|
183
|
+
- A successful operational send does not imply agent work, create a Pi prompt, or alter terminal status.
|
|
184
|
+
|
|
185
|
+
## Diagnostics
|
|
186
|
+
|
|
187
|
+
Failures record redacted runtime events under a delivery-specific category with operation, scope kind, profile, and failure reason. Diagnostics must not include bot tokens, unrestricted message bodies, callback payload secrets, or raw transport responses.
|
|
188
|
+
|
|
189
|
+
A future `getTelegramDeliveryDiagnostics()` is unnecessary for the first slice because callers receive structured results and `/telegram-status` already owns bridge diagnostics. Add a dedicated diagnostics getter only if a real consumer needs registry-level introspection.
|
|
190
|
+
|
|
191
|
+
## Security And Non-Goals
|
|
192
|
+
|
|
193
|
+
The API does not expose:
|
|
194
|
+
|
|
195
|
+
- Bot tokens or raw Telegram clients.
|
|
196
|
+
- Arbitrary Bot API methods.
|
|
197
|
+
- A second polling loop.
|
|
198
|
+
- Cross-profile delivery.
|
|
199
|
+
- Unrestricted cross-instance targeting.
|
|
200
|
+
- Session replacement, reload, process launch, or Pi slash-command dispatch.
|
|
201
|
+
- File/media uploads in the first 0.21 slice.
|
|
202
|
+
- Captured Pi contexts or mutable queue/session state.
|
|
203
|
+
|
|
204
|
+
Extension consumers remain trusted local code, but the contract still preserves product ownership boundaries so accidental misuse cannot silently bypass Threaded Mode routing or target identity.
|
|
205
|
+
|
|
206
|
+
## Validation Contract
|
|
207
|
+
|
|
208
|
+
The implementation must cover:
|
|
209
|
+
|
|
210
|
+
- Classic `instance` and `aggregate` resolution.
|
|
211
|
+
- Leader own-thread, aggregate, and live-bound explicit target resolution.
|
|
212
|
+
- Follower assigned-thread and aggregate routing through the leader.
|
|
213
|
+
- Missing active turns and disconnected runtimes.
|
|
214
|
+
- Cross-profile, unknown, stale, and unauthorized explicit targets.
|
|
215
|
+
- Plain, HTML, and Markdown chunking.
|
|
216
|
+
- Reply-first and keyboard-last chunk placement.
|
|
217
|
+
- Send/edit growth/edit shrink/delete ordering.
|
|
218
|
+
- Reload/session-replacement generation invalidation.
|
|
219
|
+
- In-flight shutdown fencing and redacted diagnostics.
|
|
220
|
+
- Package-boundary imports with no `/lib` access.
|
|
221
|
+
|
|
222
|
+
## Relationship To Activity
|
|
223
|
+
|
|
224
|
+
The Activity API builds on this contract rather than duplicating transport. Activity handlers receive a fresh target-aware context whose `send`, `edit`, `delete`, and chat-action methods delegate to the same delivery runtime. The Activity API owns lifecycle normalization; this API owns delivery only.
|