@llblab/pi-telegram 0.5.1 → 0.5.2

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/README.md CHANGED
@@ -102,6 +102,7 @@ Run these inside pi, not Telegram:
102
102
  - `👎` removes a waiting turn from the queue. Telegram Bot API does not expose ordinary DM message-deletion events through the polling path used here, so queue removal is bound to the dislike reaction.
103
103
  - Reactions apply to any waiting Telegram turn, including text, voice, files, images, and media groups. For media groups, a reaction on any message in the group applies to the whole queued turn.
104
104
  - If you edit a Telegram message while it is still waiting in the queue, the queued turn is updated instead of creating a duplicate prompt. Edits after a turn has already started may not affect the active run.
105
+ - Telegram replies to earlier text or caption messages are forwarded as `[reply]` context for normal prompts, while slash commands still parse from the new message text only.
105
106
  - Inbound images, albums, and files are saved to `~/.pi/agent/tmp/telegram`. Unhandled local file paths are included in the prompt, handled attachment output is injected into the prompt text, and inbound images are forwarded to pi as image inputs. Inbound downloads default to a 50 MiB limit and can be adjusted with `PI_TELEGRAM_INBOUND_FILE_MAX_BYTES` or `TELEGRAM_MAX_FILE_SIZE_BYTES`.
106
107
  - Queue reactions depend on Telegram delivering `message_reaction` updates for your bot and chat type.
107
108
 
@@ -23,21 +23,21 @@ Naming rule: because the repository already scopes this codebase to Telegram, ex
23
23
 
24
24
  Current runtime areas use these ownership boundaries:
25
25
 
26
- | Domain | Owns |
27
- | ------ | ---- |
28
- | `index.ts` | Single composition root for live pi/Telegram ports, session state, API-bound transport adapters, and status updates |
29
- | `api` | Bot API transport shapes/helpers, retries, file download, temp-dir lifecycle, inbound limits, chat actions, lazy bot-token clients, runtime error recording |
30
- | `config` / `setup` | Persisted bot/session pairing state, authorization, first-user pairing, token prompting, env fallback, validation, config persistence |
31
- | `locks` / `polling` | Singleton `locks.json` ownership, takeover/restart semantics, long-poll controller state, update offset persistence, poll-loop runtime wiring |
32
- | `updates` / `routing` | Update classification/execution planning, paired authorization, reactions, edits, callbacks, and inbound route composition |
33
- | `media` / `turns` / `handlers` | Text/media extraction, media-group debounce, inbound downloads, turn building/editing, image reads, attachment-handler matching/execution/fallback output |
34
- | `queue` | Queue item contracts, lane admission/order, stores, mutations, dispatch readiness/runtime, prompt/control enqueueing, session and agent/tool lifecycle sequencing |
35
- | `runtime` | Session-local coordination primitives: counters, lifecycle flags, setup guard, abort handler, typing-loop timers, prompt-dispatch flags, agent-end reset binding |
36
- | `model` / `menu` / `commands` | Model identity/thinking levels, scoped model resolution, in-flight switching, inline status/model/thinking UI, slash commands, bot command registration |
37
- | `preview` / `replies` / `rendering` | Preview lifecycle/transports, final reply delivery and reply parameters, Telegram HTML Markdown rendering, chunking, stable-preview snapshots |
38
- | `attachments` | `telegram_attach` registration, outbound attachment queueing, stat/limit checks, photo/document delivery classification |
39
- | `status` | Status-bar/status-message rendering, queue-lane status views, redacted runtime event ring, grouped pi diagnostics |
40
- | `lifecycle` / `prompts` / `pi` | pi hook registration, Telegram-specific before-agent prompt injection, centralized direct pi SDK imports and context adapters |
26
+ | Domain | Owns |
27
+ | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
28
+ | `index.ts` | Single composition root for live pi/Telegram ports, session state, API-bound transport adapters, and status updates |
29
+ | `api` | Bot API transport shapes/helpers, retries, file download, temp-dir lifecycle, inbound limits, chat actions, lazy bot-token clients, runtime error recording |
30
+ | `config` / `setup` | Persisted bot/session pairing state, authorization, first-user pairing, token prompting, env fallback, validation, config persistence |
31
+ | `locks` / `polling` | Singleton `locks.json` ownership, takeover/restart semantics, long-poll controller state, update offset persistence, poll-loop runtime wiring |
32
+ | `updates` / `routing` | Update classification/execution planning, paired authorization, reactions, edits, callbacks, and inbound route composition |
33
+ | `media` / `turns` / `handlers` | Text/media extraction, media-group debounce, inbound downloads, turn building/editing, image reads, attachment-handler matching/execution/fallback output |
34
+ | `queue` | Queue item contracts, lane admission/order, stores, mutations, dispatch readiness/runtime, prompt/control enqueueing, session and agent/tool lifecycle sequencing |
35
+ | `runtime` | Session-local coordination primitives: counters, lifecycle flags, setup guard, abort handler, typing-loop timers, prompt-dispatch flags, agent-end reset binding |
36
+ | `model` / `menu` / `commands` | Model identity/thinking levels, scoped model resolution, in-flight switching, inline status/model/thinking UI, slash commands, bot command registration |
37
+ | `preview` / `replies` / `rendering` | Preview lifecycle/transports, final reply delivery and reply parameters, Telegram HTML Markdown rendering, chunking, stable-preview snapshots |
38
+ | `attachments` | `telegram_attach` registration, outbound attachment queueing, stat/limit checks, photo/document delivery classification |
39
+ | `status` | Status-bar/status-message rendering, queue-lane status views, redacted runtime event ring, grouped pi diagnostics |
40
+ | `lifecycle` / `prompts` / `pi` | pi hook registration, Telegram-specific before-agent prompt injection, centralized direct pi SDK imports and context adapters |
41
41
 
42
42
  Boundary invariants:
43
43
 
@@ -71,13 +71,14 @@ Telegram bot configuration stays in `~/.pi/agent/telegram.json`; singleton runti
71
71
  2. Each update offset is persisted only after the update handler succeeds; repeated handler failures are bounded so one poisoned update cannot stall polling forever
72
72
  3. The bridge filters to the paired private user
73
73
  4. Media groups are coalesced into a single Telegram turn when needed
74
- 5. Files are streamed into `~/.pi/agent/tmp/telegram` with a default 50 MiB size limit, partial-download cleanup on failures, and stale temp cleanup on session start; operators can tune the limit with `PI_TELEGRAM_INBOUND_FILE_MAX_BYTES` or `TELEGRAM_MAX_FILE_SIZE_BYTES`
75
- 6. Configured inbound attachment handlers may run on downloaded files by MIME wildcard, Telegram attachment type, or generic match selector; command templates receive safe command-arg substitution for `{file}`/`{mime}`/`{type}`
76
- 7. Matching handlers are tried in config order: a non-zero exit records diagnostics and falls back to the next matching handler, while the first successful handler stops the chain
77
- 8. Local attachments stay visible under `[attachments] <directory>` with relative file entries, and handler stdout is appended under `[outputs]` before the agent sees the turn; failed handlers omit output while keeping the attachment entry
78
- 9. A `PendingTelegramTurn` is created and queued locally
79
- 10. Telegram `edited_message` updates are routed separately and update a matching queued turn when the original message has not been dispatched yet
80
- 11. The queue dispatcher sends the turn into pi only when dispatch is safe
74
+ 5. Slash command parsing uses only the new message text/caption, while Telegram `reply_to_message` text/caption is injected later as prompt-only `[reply]` context for normal queued turns
75
+ 6. Files are streamed into `~/.pi/agent/tmp/telegram` with a default 50 MiB size limit, partial-download cleanup on failures, and stale temp cleanup on session start; operators can tune the limit with `PI_TELEGRAM_INBOUND_FILE_MAX_BYTES` or `TELEGRAM_MAX_FILE_SIZE_BYTES`
76
+ 7. Configured inbound attachment handlers may run on downloaded files by MIME wildcard, Telegram attachment type, or generic match selector; command templates receive safe command-arg substitution for `{file}`/`{mime}`/`{type}`
77
+ 8. Matching handlers are tried in config order: a non-zero exit records diagnostics and falls back to the next matching handler, while the first successful handler stops the chain
78
+ 9. Local attachments stay visible under `[attachments] <directory>` with relative file entries, and handler stdout is appended under `[outputs]` before the agent sees the turn; failed handlers omit output while keeping the attachment entry
79
+ 10. A `PendingTelegramTurn` is created and queued locally
80
+ 11. Telegram `edited_message` updates are routed separately and update a matching queued turn when the original message has not been dispatched yet
81
+ 12. The queue dispatcher sends the turn into pi only when dispatch is safe
81
82
 
82
83
  ### Queue Safety Model
83
84
 
@@ -90,12 +91,12 @@ Queued items now use two explicit dimensions:
90
91
 
91
92
  Admission contract:
92
93
 
93
- | Admission | Examples | Queue shape | Dispatch rank |
94
- | --------------------- | ---------------------------------------------------- | -------------------------------------------------------------------- | ------------- |
95
- | Immediate execution | `/compact`, `/stop`, `/help`, `/start` | Does not enter the Telegram queue; `/stop` also clears queued items | N/A |
94
+ | Admission | Examples | Queue shape | Dispatch rank |
95
+ | --------------------- | ------------------------------------------------------------ | -------------------------------------------------------------------- | ------------- |
96
+ | Immediate execution | `/compact`, `/stop`, `/help`, `/start` | Does not enter the Telegram queue; `/stop` also clears queued items | N/A |
96
97
  | Control queue | Model-switch continuation turns and future deferred controls | `queueLane: control`; accepts control items and continuation prompts | 0 |
97
- | Priority prompt queue | A waiting prompt promoted by `👍` | `kind: prompt`, `queueLane: priority` | 1 |
98
- | Default prompt queue | Normal Telegram text/media turns | `kind: prompt`, `queueLane: default` | 2 |
98
+ | Priority prompt queue | A waiting prompt promoted by `👍` | `kind: prompt`, `queueLane: priority` | 1 |
99
+ | Default prompt queue | Normal Telegram text/media turns | `kind: prompt`, `queueLane: default` | 2 |
99
100
 
100
101
  The command action itself carries its execution mode, and the queue domain exposes lane contracts for admission mode, dispatch rank, and allowed item kinds. Queue append and planning paths validate lane admission so a malformed control/default or other invalid lane pairing fails predictably instead of silently changing priority. This lets synthetic control actions and Telegram prompts share one stable ordering model while still rendering distinctly in status output. In the pi status bar, busy labels distinguish `active`, `dispatching`, `queued`, `tool running`, `model`, and `compacting`; priority prompts are marked with `⬆` while control items keep markers such as `⚡`.
101
102
 
package/lib/media.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  import { basename, dirname } from "node:path";
7
7
 
8
8
  const TELEGRAM_MEDIA_GROUP_DEBOUNCE_MS = 1200;
9
+ const TELEGRAM_REPLY_CONTEXT_MAX_LENGTH = 1000;
9
10
 
10
11
  export interface TelegramPhotoSize {
11
12
  file_id: string;
@@ -27,6 +28,12 @@ export interface TelegramVoice {
27
28
  mime_type?: string;
28
29
  }
29
30
 
31
+ export interface TelegramReplyToMessage {
32
+ message_id?: number;
33
+ text?: string;
34
+ caption?: string;
35
+ }
36
+
30
37
  export interface TelegramSticker {
31
38
  file_id: string;
32
39
  }
@@ -35,6 +42,7 @@ export interface TelegramMediaMessage {
35
42
  message_id: number;
36
43
  text?: string;
37
44
  caption?: string;
45
+ reply_to_message?: TelegramReplyToMessage;
38
46
  media_group_id?: string;
39
47
  photo?: TelegramPhotoSize[];
40
48
  document?: TelegramDocument;
@@ -167,12 +175,58 @@ export function extractTelegramMessageText(
167
175
  return (message.text || message.caption || "").trim();
168
176
  }
169
177
 
178
+ function truncateTelegramReplyContextText(text: string): string {
179
+ if (text.length <= TELEGRAM_REPLY_CONTEXT_MAX_LENGTH) return text;
180
+ return `${text.slice(0, TELEGRAM_REPLY_CONTEXT_MAX_LENGTH).trimEnd()}…`;
181
+ }
182
+
183
+ export function extractTelegramReplyContextText(
184
+ message: TelegramMediaMessage,
185
+ ): string {
186
+ const quoted = (
187
+ message.reply_to_message?.text ||
188
+ message.reply_to_message?.caption ||
189
+ ""
190
+ ).trim();
191
+ return quoted ? truncateTelegramReplyContextText(quoted) : "";
192
+ }
193
+
194
+ export function appendTelegramReplyContext(
195
+ text: string,
196
+ replyContext: string,
197
+ ): string {
198
+ if (!replyContext) return text;
199
+ const replyBlock = `[reply] ${replyContext}`;
200
+ return text ? `${text}\n\n${replyBlock}` : `_\n\n${replyBlock}`;
201
+ }
202
+
203
+ export function extractTelegramMessagePromptText(
204
+ message: TelegramMediaMessage,
205
+ ): string {
206
+ return appendTelegramReplyContext(
207
+ extractTelegramMessageText(message),
208
+ extractTelegramReplyContextText(message),
209
+ );
210
+ }
211
+
170
212
  export function extractTelegramMessagesText(
171
213
  messages: TelegramMediaMessage[],
172
214
  ): string {
173
215
  return messages.map(extractTelegramMessageText).filter(Boolean).join("\n\n");
174
216
  }
175
217
 
218
+ export function extractTelegramMessagesPromptText(
219
+ messages: TelegramMediaMessage[],
220
+ ): string {
221
+ const text = extractTelegramMessagesText(messages);
222
+ const firstMessage = messages[0];
223
+ if (!firstMessage) return text;
224
+ return appendTelegramReplyContext(
225
+ text,
226
+ extractTelegramReplyContextText(firstMessage),
227
+ );
228
+ }
229
+
176
230
  export function extractFirstTelegramMessageText(
177
231
  messages: TelegramMediaMessage[],
178
232
  ): string {
package/lib/turns.ts CHANGED
@@ -11,7 +11,10 @@ import {
11
11
  type DownloadedTelegramMessageFile,
12
12
  type DownloadTelegramMessageFilesDeps,
13
13
  downloadTelegramMessageFiles,
14
+ extractTelegramMessagesPromptText,
14
15
  extractTelegramMessagesText,
16
+ appendTelegramReplyContext,
17
+ extractTelegramReplyContextText,
15
18
  formatTelegramHistoryText,
16
19
  guessMediaType,
17
20
  type TelegramMediaMessage,
@@ -94,6 +97,11 @@ function appendTelegramAttachmentSection(
94
97
  return `${prefix}${header}\n${items.map((item) => `- ${item}`).join("\n")}`;
95
98
  }
96
99
 
100
+ function appendTelegramPromptText(prompt: string, rawText: string): string {
101
+ if (!rawText) return prompt;
102
+ return `${prompt} ${rawText}`;
103
+ }
104
+
97
105
  export function buildTelegramTurnPrompt(options: {
98
106
  telegramPrefix: string;
99
107
  rawText: string;
@@ -112,10 +120,10 @@ export function buildTelegramTurnPrompt(options: {
112
120
  prompt += "\n\nCurrent Telegram message:";
113
121
  }
114
122
  if (options.rawText.length > 0) {
115
- prompt +=
123
+ prompt =
116
124
  (options.historyTurns?.length ?? 0) > 0
117
- ? `\n${options.rawText}`
118
- : ` ${options.rawText}`;
125
+ ? `${prompt}\n${options.rawText}`
126
+ : appendTelegramPromptText(prompt, options.rawText);
119
127
  }
120
128
  const promptFiles = options.promptFiles ?? options.files;
121
129
  prompt = appendTelegramAttachmentSection(prompt, promptFiles);
@@ -193,12 +201,11 @@ function buildEditedTelegramPromptText(options: {
193
201
  attachmentFiles,
194
202
  };
195
203
  }
196
- const promptText =
197
- options.rawText.length > 0
198
- ? `${options.telegramPrefix} ${options.rawText}`
199
- : options.telegramPrefix;
200
204
  return {
201
- text: `${promptText}${attachmentSuffix}`,
205
+ text: `${appendTelegramPromptText(
206
+ options.telegramPrefix,
207
+ options.rawText,
208
+ )}${attachmentSuffix}`,
202
209
  attachmentFiles,
203
210
  };
204
211
  }
@@ -207,6 +214,7 @@ export function updateTelegramPromptTurnText(options: {
207
214
  turn: PendingTelegramTurn;
208
215
  telegramPrefix: string;
209
216
  rawText: string;
217
+ statusText?: string;
210
218
  }): PendingTelegramTurn {
211
219
  let attachmentFiles: DownloadedTelegramTurnFile[] = [];
212
220
  const nextContent = options.turn.content.map((block, index) => {
@@ -227,7 +235,7 @@ export function updateTelegramPromptTurnText(options: {
227
235
  content: nextContent,
228
236
  historyText: formatTelegramHistoryText(options.rawText, attachmentFiles),
229
237
  statusSummary: formatTelegramTurnStatusSummary(
230
- options.rawText,
238
+ options.statusText ?? options.rawText,
231
239
  attachmentFiles,
232
240
  ),
233
241
  };
@@ -240,6 +248,7 @@ export function updateQueuedTelegramPromptTurnText<
240
248
  sourceMessageId: number | undefined;
241
249
  telegramPrefix: string;
242
250
  rawText: string;
251
+ statusText?: string;
243
252
  }): { items: TelegramQueueItem<TContext>[]; changed: boolean } {
244
253
  if (options.sourceMessageId === undefined) {
245
254
  return { items: options.items, changed: false };
@@ -257,6 +266,7 @@ export function updateQueuedTelegramPromptTurnText<
257
266
  turn: item,
258
267
  telegramPrefix: options.telegramPrefix,
259
268
  rawText: options.rawText,
269
+ statusText: options.statusText,
260
270
  });
261
271
  });
262
272
  return { items, changed };
@@ -278,7 +288,8 @@ export function createTelegramQueuedPromptEditRuntime<
278
288
  items: deps.getQueuedItems(),
279
289
  sourceMessageId: message.message_id,
280
290
  telegramPrefix: TELEGRAM_PREFIX,
281
- rawText: extractTelegramMessagesText([message]),
291
+ rawText: extractTelegramMessagesPromptText([message]),
292
+ statusText: extractTelegramMessagesText([message]),
282
293
  });
283
294
  deps.setQueuedItems(items);
284
295
  if (changed) deps.updateStatus(ctx);
@@ -293,6 +304,7 @@ export interface BuildTelegramPromptTurnOptions {
293
304
  historyTurns?: PendingTelegramTurn[];
294
305
  queueOrder: number;
295
306
  rawText: string;
307
+ statusText?: string;
296
308
  files: DownloadedTelegramTurnFile[];
297
309
  promptFiles?: DownloadedTelegramTurnFile[];
298
310
  handlerOutputs?: string[];
@@ -332,18 +344,26 @@ export function createTelegramPromptTurnRuntimeBuilder<
332
344
  ) => Promise<PendingTelegramTurn> {
333
345
  return async (messages, historyTurns = [], ctx) => {
334
346
  const rawText = extractTelegramMessagesText(messages);
347
+ const replyContext = messages[0]
348
+ ? extractTelegramReplyContextText(messages[0])
349
+ : "";
335
350
  const files = await downloadTelegramMessageFiles(messages, {
336
351
  downloadFile: deps.downloadFile,
337
352
  });
338
353
  const processed = deps.processAttachments
339
354
  ? await deps.processAttachments(files, rawText, ctx as TContext)
340
355
  : { rawText, promptFiles: files };
356
+ const promptText = appendTelegramReplyContext(
357
+ processed.rawText,
358
+ replyContext,
359
+ );
341
360
  return buildTelegramPromptTurnRuntime({
342
361
  telegramPrefix: TELEGRAM_PREFIX,
343
362
  messages,
344
363
  historyTurns,
345
364
  queueOrder: deps.allocateQueueOrder(),
346
- rawText: processed.rawText,
365
+ rawText: promptText,
366
+ statusText: processed.rawText,
347
367
  files,
348
368
  promptFiles: processed.promptFiles,
349
369
  handlerOutputs: processed.handlerOutputs,
@@ -399,7 +419,7 @@ export async function buildTelegramPromptTurn(
399
419
  options.handlerOutputs,
400
420
  ),
401
421
  statusSummary: formatTelegramTurnStatusSummary(
402
- options.rawText,
422
+ options.statusText ?? options.rawText,
403
423
  options.promptFiles ?? options.files,
404
424
  options.handlerOutputs,
405
425
  ),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "private": false,
5
5
  "description": "Better Telegram DM bridge extension for pi",
6
6
  "type": "module",