@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 +2 -1
- package/docs/architecture.md +30 -29
- package/lib/commands.ts +28 -10
- package/lib/media.ts +54 -0
- package/lib/queue.ts +12 -0
- package/lib/routing.ts +1 -0
- package/lib/turns.ts +32 -12
- package/package.json +1 -1
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
|
|
package/docs/architecture.md
CHANGED
|
@@ -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
|
|
27
|
-
|
|
|
28
|
-
| `index.ts`
|
|
29
|
-
| `api`
|
|
30
|
-
| `config` / `setup`
|
|
31
|
-
| `locks` / `polling`
|
|
32
|
-
| `updates` / `routing`
|
|
33
|
-
| `media` / `turns` / `handlers`
|
|
34
|
-
| `queue`
|
|
35
|
-
| `runtime`
|
|
36
|
-
| `model` / `menu` / `commands`
|
|
37
|
-
| `preview` / `replies` / `rendering` | Preview lifecycle/transports, final reply delivery and reply parameters, Telegram HTML Markdown rendering, chunking, stable-preview snapshots
|
|
38
|
-
| `attachments`
|
|
39
|
-
| `status`
|
|
40
|
-
| `lifecycle` / `prompts` / `pi`
|
|
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.
|
|
75
|
-
6.
|
|
76
|
-
7.
|
|
77
|
-
8.
|
|
78
|
-
9.
|
|
79
|
-
10.
|
|
80
|
-
11.
|
|
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
|
|
94
|
-
| --------------------- |
|
|
95
|
-
| Immediate execution | `/compact`, `/stop`, `/help`, `/start`
|
|
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 `👍`
|
|
98
|
-
| Default prompt queue | Normal Telegram text/media turns
|
|
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`
|
|
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
|
-
{
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
?
|
|
118
|
-
:
|
|
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: `${
|
|
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:
|
|
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:
|
|
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
|
),
|