@llblab/pi-telegram 0.5.0 → 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
@@ -82,7 +82,7 @@ Use these inside the Telegram DM with your bot:
82
82
  - **`/status`**: View session stats, cost, and use inline buttons to change models.
83
83
  - **`/model`**: Open the interactive model selector.
84
84
  - **`/compact`**: Start session compaction (only works when the session is idle).
85
- - **`/stop`**: Abort the active run.
85
+ - **`/stop`**: Abort the active run and clear all waiting Telegram queue items.
86
86
 
87
87
  Telegram command admission is explicit: `/compact`, `/stop`, `/help`, `/start`, `/status`, and `/model` execute immediately. Synthetic model-switch continuation turns still enter the high-priority control lane so they can resume before normal queued prompts when pi becomes safe to dispatch.
88
88
 
@@ -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 | 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
 
@@ -113,7 +114,7 @@ This prevents queue races around rapid follow-ups, `/compact`, and mixed local p
113
114
 
114
115
  ### Abort Behavior
115
116
 
116
- When `/stop` aborts an active Telegram turn, queued follow-up Telegram messages can be preserved as prior-user history for the next turn. This keeps later Telegram input from being silently dropped after an interrupted run.
117
+ When `/stop` runs from Telegram, it clears pending model-switch state, clears every waiting Telegram queue item, resets aborted-turn history preservation, and then aborts the active Telegram turn when an abort handler exists. This intentionally favors recovery over preservation: priority/default/control queue items are dropped so the next Telegram message can enter a clean queue and dispatch like a fresh TUI prompt after an interrupted run.
117
118
 
118
119
  ## Rendering Model
119
120
 
@@ -162,7 +163,7 @@ Current operator controls include:
162
163
  - Inline status buttons for model and thinking adjustments, applying idle selections immediately while still respecting busy-run restart rules; model-menu inputs are cached briefly and stored inline-menu states are pruned by TTL/LRU so old keyboards expire predictably
163
164
  - `/model` for interactive model selection, executed immediately from Telegram and supporting in-flight restart of the active Telegram-owned run on a newly selected model
164
165
  - `/compact` for Telegram-triggered pi session compaction when the bridge is idle
165
- - `/stop` for aborting the active Telegram-owned run
166
+ - `/stop` for aborting the active Telegram-owned run and clearing waiting Telegram queue items
166
167
  - `/telegram-status` for pi-side diagnostics as grouped line-by-line sections separated by blank lines: connection, polling, execution, queue, and the recent redacted runtime/API event ring. These sections include polling state, last update id, active turn source ids, pending dispatch, compaction state, active tool count, pending model-switch state, total queue depth, and queue-lane counts. The event ring records transport/API, polling/update, prompt-dispatch, control-action, typing, compaction, setup, session-lifecycle, and attachment queue/delivery failures; benign unchanged edit responses and unsupported empty draft-clear attempts are filtered out so expected preview transport noise does not obscure real failures
167
168
  - Queue reactions using `👍` and `👎` apply to waiting text, voice, file, image, and media-group turns by matching the turn's source Telegram message ids; `👎` acts as the canonical queue-removal path because ordinary Telegram DM message deletions are not exposed through the Bot API polling path this bridge uses
168
169
 
package/lib/commands.ts CHANGED
@@ -32,7 +32,10 @@ export const TELEGRAM_BOT_COMMANDS: readonly TelegramBotCommandDefinition[] = [
32
32
  },
33
33
  { command: "model", description: "Open the interactive model selector" },
34
34
  { command: "compact", description: "Compact the current pi session" },
35
- { command: "stop", description: "Abort the current pi task" },
35
+ {
36
+ command: "stop",
37
+ description: "Abort the current pi task and clear queued turns",
38
+ },
36
39
  ];
37
40
 
38
41
  export interface TelegramBotCommandRegistrationDeps {
@@ -178,7 +181,7 @@ export interface TelegramCommandActionDeps<TMessage, TContext> {
178
181
  export interface TelegramStopCommandDeps {
179
182
  hasAbortHandler: () => boolean;
180
183
  clearPendingModelSwitch: () => void;
181
- hasQueuedTelegramItems: () => boolean;
184
+ clearQueuedTelegramItems: () => number;
182
185
  setPreserveQueuedTurnsAsHistory: (preserve: boolean) => void;
183
186
  abortCurrentTurn: () => void;
184
187
  updateStatus: () => void;
@@ -408,6 +411,7 @@ export interface TelegramCommandRuntimeDeps<
408
411
  hasAbortHandler: () => boolean;
409
412
  clearPendingModelSwitch: () => void;
410
413
  hasQueuedTelegramItems: () => boolean;
414
+ clearQueuedTelegramItems: (ctx: TContext) => number;
411
415
  setPreserveQueuedTurnsAsHistory: (preserve: boolean) => void;
412
416
  abortCurrentTurn: () => void;
413
417
  isIdle: (ctx: TContext) => boolean;
@@ -439,7 +443,7 @@ export interface TelegramCommandRuntimeDeps<
439
443
  }
440
444
 
441
445
  export const TELEGRAM_HELP_TEXT =
442
- "Send me a message and I will forward it to pi. Commands: /status, /model, /compact, /stop.";
446
+ "Send me a message and I will forward it to pi. Commands: /status, /model, /compact, /stop. /stop aborts the current run and clears queued Telegram turns.";
443
447
 
444
448
  function getTelegramCommandErrorMessage(error: unknown): string {
445
449
  return error instanceof Error ? error.message : String(error);
@@ -482,20 +486,32 @@ export function getTelegramCommandExecutionMode(
482
486
  return action.executionMode;
483
487
  }
484
488
 
489
+ function formatTelegramQueuedTurnCount(count: number): string {
490
+ return count === 1 ? "1 queued turn" : `${count} queued turns`;
491
+ }
492
+
485
493
  export async function handleTelegramStopCommand(
486
494
  deps: TelegramStopCommandDeps,
487
495
  ): Promise<void> {
496
+ deps.clearPendingModelSwitch();
497
+ const clearedCount = deps.clearQueuedTelegramItems();
498
+ deps.setPreserveQueuedTurnsAsHistory(false);
488
499
  if (!deps.hasAbortHandler()) {
489
- await deps.sendTextReply("No active turn.");
500
+ const clearedSuffix =
501
+ clearedCount > 0
502
+ ? ` Cleared ${formatTelegramQueuedTurnCount(clearedCount)}.`
503
+ : "";
504
+ if (clearedCount > 0) deps.updateStatus();
505
+ await deps.sendTextReply(`No active turn.${clearedSuffix}`);
490
506
  return;
491
507
  }
492
- deps.clearPendingModelSwitch();
493
- if (deps.hasQueuedTelegramItems()) {
494
- deps.setPreserveQueuedTurnsAsHistory(true);
495
- }
496
508
  deps.abortCurrentTurn();
497
509
  deps.updateStatus();
498
- await deps.sendTextReply("Aborted current turn.");
510
+ const clearedSuffix =
511
+ clearedCount > 0
512
+ ? ` Cleared ${formatTelegramQueuedTurnCount(clearedCount)}.`
513
+ : "";
514
+ await deps.sendTextReply(`Aborted current turn.${clearedSuffix}`);
499
515
  }
500
516
 
501
517
  export async function handleTelegramCompactCommand(
@@ -656,6 +672,7 @@ export function createTelegramCommandHandlerTargetRuntime<
656
672
  hasAbortHandler: deps.hasAbortHandler,
657
673
  clearPendingModelSwitch: deps.clearPendingModelSwitch,
658
674
  hasQueuedTelegramItems: deps.hasQueuedTelegramItems,
675
+ clearQueuedTelegramItems: deps.clearQueuedTelegramItems,
659
676
  setPreserveQueuedTurnsAsHistory: deps.setPreserveQueuedTurnsAsHistory,
660
677
  abortCurrentTurn: deps.abortCurrentTurn,
661
678
  isIdle: deps.isIdle,
@@ -736,7 +753,8 @@ async function handleTelegramCommandRuntime<
736
753
  await handleTelegramStopCommand({
737
754
  hasAbortHandler: deps.hasAbortHandler,
738
755
  clearPendingModelSwitch: deps.clearPendingModelSwitch,
739
- hasQueuedTelegramItems: deps.hasQueuedTelegramItems,
756
+ clearQueuedTelegramItems: () =>
757
+ deps.clearQueuedTelegramItems(commandCtx),
740
758
  setPreserveQueuedTurnsAsHistory: deps.setPreserveQueuedTurnsAsHistory,
741
759
  abortCurrentTurn: deps.abortCurrentTurn,
742
760
  updateStatus: updateStatusFor(commandCtx),
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/queue.ts CHANGED
@@ -1061,6 +1061,7 @@ export interface TelegramQueueMutationControllerDeps<
1061
1061
  export interface TelegramQueueMutationController<TContext> {
1062
1062
  append: (item: TelegramQueueItem<TContext>, ctx: TContext) => void;
1063
1063
  reorder: (ctx: TContext) => void;
1064
+ clear: (ctx: TContext) => number;
1064
1065
  removeByMessageIds: (messageIds: number[], ctx: TContext) => number;
1065
1066
  clearPriorityByMessageId: (messageId: number, ctx: TContext) => boolean;
1066
1067
  prioritizeByMessageId: (messageId: number, ctx: TContext) => boolean;
@@ -1262,6 +1263,7 @@ export function createTelegramQueueMutationController<TContext>(
1262
1263
  append: (item, ctx) =>
1263
1264
  appendTelegramQueueItemRuntime(item, buildRuntimeDeps(ctx)),
1264
1265
  reorder: (ctx) => reorderTelegramQueueItemsRuntime(buildRuntimeDeps(ctx)),
1266
+ clear: (ctx) => clearTelegramQueueItemsRuntime(buildRuntimeDeps(ctx)),
1265
1267
  removeByMessageIds: (messageIds, ctx) =>
1266
1268
  removeTelegramQueueItemsByMessageIdsRuntime(
1267
1269
  messageIds,
@@ -1291,6 +1293,16 @@ export function reorderTelegramQueueItemsRuntime<TContext>(
1291
1293
  deps.updateStatus(deps.ctx);
1292
1294
  }
1293
1295
 
1296
+ export function clearTelegramQueueItemsRuntime<TContext>(
1297
+ deps: TelegramQueueMutationRuntimeDeps<TContext>,
1298
+ ): number {
1299
+ const removedCount = deps.getQueuedItems().length;
1300
+ if (removedCount === 0) return 0;
1301
+ deps.setQueuedItems([]);
1302
+ deps.updateStatus(deps.ctx);
1303
+ return removedCount;
1304
+ }
1305
+
1294
1306
  export function removeTelegramQueueItemsByMessageIdsRuntime<TContext>(
1295
1307
  messageIds: number[],
1296
1308
  deps: TelegramQueueMutationRuntimeDeps<TContext>,
package/lib/routing.ts CHANGED
@@ -131,6 +131,7 @@ export function createTelegramInboundRouteRuntime<
131
131
  hasAbortHandler: deps.bridgeRuntime.abort.hasHandler,
132
132
  clearPendingModelSwitch: deps.modelSwitchController.clearPendingSwitch,
133
133
  hasQueuedTelegramItems: deps.telegramQueueStore.hasQueuedItems,
134
+ clearQueuedTelegramItems: deps.queueMutationRuntime.clear,
134
135
  setPreserveQueuedTurnsAsHistory:
135
136
  deps.bridgeRuntime.lifecycle.setPreserveQueuedTurnsAsHistory,
136
137
  abortCurrentTurn: deps.bridgeRuntime.abort.abortTurn,
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.0",
3
+ "version": "0.5.2",
4
4
  "private": false,
5
5
  "description": "Better Telegram DM bridge extension for pi",
6
6
  "type": "module",