@llblab/pi-telegram 0.41.0 → 0.41.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/CHANGELOG.md +7 -0
- package/docs/architecture.md +3 -3
- package/docs/inbound.md +1 -1
- package/docs/public-api.md +2 -2
- package/lib/activity.ts +7 -0
- package/lib/bindings.ts +3 -0
- package/lib/commands.ts +16 -6
- package/lib/media.ts +12 -2
- package/lib/routing.ts +34 -8
- package/lib/status.ts +13 -4
- package/lib/turns.ts +10 -1
- package/lib/updates.ts +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
> Each release keeps at most 8 outcome records of at most 512 characters.
|
|
4
4
|
|
|
5
|
+
## 0.41.1: Focused Operator Status Hotfix
|
|
6
|
+
|
|
7
|
+
- `Next Turn Feedback`: Replies one Pi-aligned fully bold abort notice to the aborted Telegram turn, falls back to the `/next` command for local work, preserves completed intermediate output, and suppresses pending aborted assistant text so one forced transition no longer produces a contradictory second reply.
|
|
8
|
+
- `Status Telemetry`: Keeps the Tokens row compact with input/output totals and moves cache read/write plus the latest cache-hit rate into a dedicated Cache row.
|
|
9
|
+
- `Reply Transcription`: Runs replied voice and other media through the inbound handler/provider pipeline and appends provenance-scoped outputs inside the reply context, including Guest Mode, instead of leaving quoted audio as an untranscribed attachment.
|
|
10
|
+
- `Information Hierarchy`: Brings access-denial and Threaded Mode chooser headings back under the common fully bold emoji-led sentence/colon contract.
|
|
11
|
+
|
|
5
12
|
## 0.41.0: Connected Companion Presence
|
|
6
13
|
|
|
7
14
|
- `Always-On Projection`: Treats authorized Telegram connection as the public-output boundary for local, autonomous, and unclassified Pi work, always delivering completed visible blocks in order instead of exposing an independent proactive-push opt-out.
|
package/docs/architecture.md
CHANGED
|
@@ -301,7 +301,7 @@ Immediate controls:
|
|
|
301
301
|
- `/start` opens the main inline application menu.
|
|
302
302
|
- `/model`, `/thinking`, `/queue`, and `/settings` are hidden shortcuts to menu sections.
|
|
303
303
|
- `/compact` opens an inline confirmation dialog and then runs compaction when the bridge is idle.
|
|
304
|
-
- `/next` dispatches the next queued turn, aborting Pi first when needed.
|
|
304
|
+
- `/next` dispatches the next queued turn, aborting Pi first when needed. When an active Telegram turn is aborted, its single Pi-aligned informational notice replies to a pre-abort snapshot of that turn; otherwise the command message is the fallback target. Aborted pending assistant text is not projected as a second reply, while already completed intermediate output remains visible.
|
|
305
305
|
- `/abort` aborts active work while preserving queued items. Abort-history preservation is enabled only for Telegram-owned active turns; later local/non-Telegram agent starts clear stale abort-history mode so the next Telegram prompt appends instead of absorbing old queued turns as history.
|
|
306
306
|
- `/stop` aborts and clears waiting Telegram queue items.
|
|
307
307
|
|
|
@@ -336,7 +336,7 @@ Assistant delivery guarantees:
|
|
|
336
336
|
- 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.
|
|
337
337
|
- 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.
|
|
338
338
|
- 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. Media embedded in inbound Telegram `rich_message` blocks is downloaded like ordinary message media and stays attached to its forward-source block instead of being mislabeled as current-user material.
|
|
339
|
-
- 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.
|
|
339
|
+
- 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. Replied media runs through the same inbound handlers and voice transcription providers as current-message media, with provenance-scoped `[outputs|from:…]` appended inside the reply block.
|
|
340
340
|
- 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.
|
|
341
341
|
- 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.
|
|
342
342
|
- Preview flushes are serialized so older edits cannot race newer drafts; final delivery waits for active draft flushes and does not perform a post-final draft-clear call.
|
|
@@ -390,7 +390,7 @@ The bridge does not mirror arbitrary `ctx.ui.confirm/input/select/custom` prompt
|
|
|
390
390
|
|
|
391
391
|
## Diagnostics And Operational Behavior
|
|
392
392
|
|
|
393
|
-
Status rendering distinguishes connected, active, dispatching, queued, tool-running, model-switching, and compacting states; the Telegram status menu gives compaction precedence over generic active or pending work. Its Tokens row
|
|
393
|
+
Status rendering distinguishes connected, active, dispatching, queued, tool-running, model-switching, and compacting states; the Telegram status menu gives compaction precedence over generic active or pending work. Its compact Tokens row shows only input and output totals, while the adjacent Cache row groups `R` cache-read tokens, `W` cache-write tokens, and `CH` for the latest assistant request's cache-read share of prompt tokens rather than a misleading cumulative-session ratio; the labels remain distinct from companion-provided usage limits. Observed automatic compaction sends the same start and completion notices as the manual command without duplicating notices for command-owned compaction. If a queue mutation removes the last waiting item while Telegram-owned work still has running tools, status remains active instead of degrading to connected.
|
|
394
394
|
|
|
395
395
|
Queue reaction behavior, lane-tail transitions, Keep/Skip independence, multi-reaction precedence, and the Bot API reaction-removal limitation are defined in [Priority, Reactions, Keep, and Skip](#priority-reactions-keep-and-skip). Reaction changes first flush a matching delayed text or media group so the governed turn exists before mutation, and dropping marked heads cannot leave status permanently queued.
|
|
396
396
|
|
package/docs/inbound.md
CHANGED
|
@@ -67,7 +67,7 @@ Built-in placeholders for text handlers:
|
|
|
67
67
|
|
|
68
68
|
## Media/File Handlers
|
|
69
69
|
|
|
70
|
-
Media/file handlers keep the legacy attachment-handler behavior: downloaded files are matched by `mime`, `type`, or `match`, then each file runs the first successful matching handler. Downloaded files with `mime: "text/plain"` or any `text/*` MIME type have a built-in fail-open handler that reads UTF-8 content into `[outputs]` when no configured handler produced output. Composition is useful for pipelines such as voice transcription followed by machine translation, so the agent receives translated `[outputs]` instead of the raw STT language.
|
|
70
|
+
Media/file handlers keep the legacy attachment-handler behavior: downloaded files are matched by `mime`, `type`, or `match`, then each file runs the first successful matching handler. Attachments from a replied-to message use the same handler/provider pipeline independently; their `[outputs|from:…]` section remains inside that `[reply]` context after its attachment list instead of being attributed to the current message. Downloaded files with `mime: "text/plain"` or any `text/*` MIME type have a built-in fail-open handler that reads UTF-8 content into `[outputs]` when no configured handler produced output. Composition is useful for pipelines such as voice transcription followed by machine translation, so the agent receives translated `[outputs]` instead of the raw STT language.
|
|
71
71
|
|
|
72
72
|
Built-in placeholders for media/file handlers:
|
|
73
73
|
|
package/docs/public-api.md
CHANGED
|
@@ -50,7 +50,7 @@ Stable commands inside the paired Telegram DM:
|
|
|
50
50
|
|
|
51
51
|
- `/start` — pair when needed and open the main application menu.
|
|
52
52
|
- `/compact` — open confirmation and compact when idle.
|
|
53
|
-
- `/next` — dispatch the next queued turn, aborting active work first when needed.
|
|
53
|
+
- `/next` — dispatch the next queued turn, aborting active work first when needed; one Pi-aligned informational reply anchors to a pre-abort snapshot of the Telegram turn or falls back to the command, and aborted pending assistant text is suppressed.
|
|
54
54
|
- `/continue` — enqueue a priority `continue` prompt.
|
|
55
55
|
- `/abort` — abort active work and keep the queue; abort-history is scoped to Telegram-owned active turns.
|
|
56
56
|
- `/stop` — abort active Telegram-owned work and clear waiting Telegram queue items.
|
|
@@ -336,7 +336,7 @@ Contract:
|
|
|
336
336
|
- Providers are synchronous because `/start` status text is rendered inline with the menu.
|
|
337
337
|
- Return `undefined` when the line is not relevant for the active model.
|
|
338
338
|
- Provider failures are isolated and skipped so optional companion status cannot break the core Telegram menu.
|
|
339
|
-
- The bridge renders rows as `<Label>: <value>` in the same HTML status block as Status, Tokens, Cost, and Context, capitalizing the first label character for Telegram UI consistency. Tokens
|
|
339
|
+
- The bridge renders rows as `<Label>: <value>` in the same HTML status block as Status, Tokens, Cache, Cost, and Context, capitalizing the first label character for Telegram UI consistency. Tokens contains only `↑` input and `↓` output totals. Cache groups `R` cache-read tokens, `W` cache-write tokens, and `CH` for the latest assistant request's `cacheRead / (input + cacheRead + cacheWrite)`, shown to one decimal place only after the session reports cache activity. These precise labels avoid conflating token telemetry with companion-provided usage-limit rows.
|
|
340
340
|
|
|
341
341
|
## Updates
|
|
342
342
|
|
package/lib/activity.ts
CHANGED
|
@@ -428,6 +428,9 @@ export function createTelegramActivityBridgeRuntime(deps: {
|
|
|
428
428
|
onAssistantEvent(event) {
|
|
429
429
|
getRuntime()?.onAssistantEvent(event);
|
|
430
430
|
},
|
|
431
|
+
onAssistantMessageEnd(stopReason) {
|
|
432
|
+
getRuntime()?.onAssistantMessageEnd(stopReason);
|
|
433
|
+
},
|
|
431
434
|
onToolStart(event) {
|
|
432
435
|
getRuntime()?.onToolStart(event);
|
|
433
436
|
},
|
|
@@ -491,6 +494,7 @@ export interface TelegramActivityRuntime {
|
|
|
491
494
|
recordInputSource: (source: TelegramActivityInputSource) => void;
|
|
492
495
|
onAgentStart: (activeTelegramTarget?: TelegramActivityTarget) => void;
|
|
493
496
|
onAssistantEvent: (event: TelegramAssistantStreamEvent) => void;
|
|
497
|
+
onAssistantMessageEnd: (stopReason?: string) => void;
|
|
494
498
|
onToolStart: (event: {
|
|
495
499
|
toolCallId: string;
|
|
496
500
|
toolName: string;
|
|
@@ -677,6 +681,9 @@ export function createTelegramActivityRuntime(deps: {
|
|
|
677
681
|
}
|
|
678
682
|
if (event.type === "error") flushPendingSegment("terminal-partial");
|
|
679
683
|
},
|
|
684
|
+
onAssistantMessageEnd(stopReason) {
|
|
685
|
+
if (stopReason === "aborted") pendingAssistantSegment = undefined;
|
|
686
|
+
},
|
|
680
687
|
onToolStart(event) {
|
|
681
688
|
emit({ type: "tool-start", ...event });
|
|
682
689
|
},
|
package/lib/bindings.ts
CHANGED
|
@@ -1218,6 +1218,9 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
1218
1218
|
},
|
|
1219
1219
|
onMessageEnd(event, ctx) {
|
|
1220
1220
|
if (!isSessionContextActive(ctx)) return;
|
|
1221
|
+
if (event.message.role === "assistant") {
|
|
1222
|
+
activityRuntime.onAssistantMessageEnd(event.message.stopReason);
|
|
1223
|
+
}
|
|
1221
1224
|
terminalAssistantMessagePendingDelivery =
|
|
1222
1225
|
event.message.role === "assistant" &&
|
|
1223
1226
|
event.message.stopReason !== "toolUse" &&
|
package/lib/commands.ts
CHANGED
|
@@ -1049,6 +1049,9 @@ export interface TelegramCommandRuntimeDeps<
|
|
|
1049
1049
|
text: string,
|
|
1050
1050
|
options?: { parseMode?: "HTML" },
|
|
1051
1051
|
) => Promise<void>;
|
|
1052
|
+
getActiveTurnReply?: () =>
|
|
1053
|
+
| ((text: string, options?: { parseMode?: "HTML" }) => Promise<void>)
|
|
1054
|
+
| undefined;
|
|
1052
1055
|
sendInteractiveMessage?: TelegramCompactConfirmationDeps["sendInteractiveMessage"];
|
|
1053
1056
|
assertExecutionCurrent?: (message: TMessage) => void;
|
|
1054
1057
|
}
|
|
@@ -1272,6 +1275,9 @@ export async function handleTelegramNextCommand(deps: {
|
|
|
1272
1275
|
text: string,
|
|
1273
1276
|
options?: { parseMode?: "HTML" },
|
|
1274
1277
|
) => Promise<void>;
|
|
1278
|
+
getActiveTurnReply?: () =>
|
|
1279
|
+
| ((text: string, options?: { parseMode?: "HTML" }) => Promise<void>)
|
|
1280
|
+
| undefined;
|
|
1275
1281
|
}): Promise<void> {
|
|
1276
1282
|
deps.clearPendingModelSwitch();
|
|
1277
1283
|
if (!deps.hasQueuedItems()) {
|
|
@@ -1282,16 +1288,19 @@ export async function handleTelegramNextCommand(deps: {
|
|
|
1282
1288
|
return;
|
|
1283
1289
|
}
|
|
1284
1290
|
if (!deps.isIdle() && deps.hasAbortHandler()) {
|
|
1291
|
+
const activeTurnReply = deps.getActiveTurnReply?.();
|
|
1285
1292
|
deps.clearFoldForDispatch();
|
|
1286
1293
|
deps.abortCurrentTurn();
|
|
1287
1294
|
deps.updateStatus();
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
"Aborted! Dispatching next queued turn.",
|
|
1292
|
-
),
|
|
1293
|
-
{ parseMode: "HTML" },
|
|
1295
|
+
const notice = formatTelegramInformationHeading(
|
|
1296
|
+
"⏩",
|
|
1297
|
+
"Operation aborted. Dispatching next queued turn.",
|
|
1294
1298
|
);
|
|
1299
|
+
if (activeTurnReply) {
|
|
1300
|
+
await activeTurnReply(notice, { parseMode: "HTML" });
|
|
1301
|
+
} else {
|
|
1302
|
+
await deps.sendTextReply(notice, { parseMode: "HTML" });
|
|
1303
|
+
}
|
|
1295
1304
|
return;
|
|
1296
1305
|
}
|
|
1297
1306
|
if (!deps.isIdle()) {
|
|
@@ -1799,6 +1808,7 @@ async function handleTelegramCommandRuntime<
|
|
|
1799
1808
|
deps.setFoldQueuedPromptsIntoHistory(false),
|
|
1800
1809
|
updateStatus: updateStatusFor(commandCtx),
|
|
1801
1810
|
sendTextReply: sendReplyFor(nextMessage),
|
|
1811
|
+
getActiveTurnReply: deps.getActiveTurnReply,
|
|
1802
1812
|
});
|
|
1803
1813
|
},
|
|
1804
1814
|
handleContinue: async (nextMessage, commandCtx) => {
|
package/lib/media.ts
CHANGED
|
@@ -366,6 +366,7 @@ export function extractTelegramReplyContextText(
|
|
|
366
366
|
export function buildTelegramReplyContextBlock(
|
|
367
367
|
message: TelegramMediaMessage,
|
|
368
368
|
replyFiles: Pick<DownloadedTelegramFile, "path">[] = [],
|
|
369
|
+
replyOutputs: readonly string[] = [],
|
|
369
370
|
): string {
|
|
370
371
|
const from = formatTelegramUser(message.reply_to_message?.from);
|
|
371
372
|
const header = from ? `[reply|from:${from}]` : "[reply]";
|
|
@@ -379,10 +380,19 @@ export function buildTelegramReplyContextBlock(
|
|
|
379
380
|
? replyFiles.map((file) => `- /${basename(file.path)}`)
|
|
380
381
|
: replyFiles.map((file) => `- ${file.path}`);
|
|
381
382
|
const replyBlock = text ? `${header} ${text}` : header;
|
|
383
|
+
const sections = [replyBlock];
|
|
382
384
|
if (fileLines.length > 0) {
|
|
383
|
-
|
|
385
|
+
sections.push(`${attachmentHeader}\n${fileLines.join("\n")}`);
|
|
386
|
+
}
|
|
387
|
+
if (replyOutputs.length > 0) {
|
|
388
|
+
const outputHeader = `[outputs${from ? `|from:${from}` : ""}]`;
|
|
389
|
+
sections.push(
|
|
390
|
+
`${outputHeader}\n${replyOutputs.map((output) => `- ${output}`).join("\n")}`,
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
if (text || fileLines.length > 0 || replyOutputs.length > 0) {
|
|
394
|
+
return sections.join("\n\n");
|
|
384
395
|
}
|
|
385
|
-
if (text) return replyBlock;
|
|
386
396
|
return "";
|
|
387
397
|
}
|
|
388
398
|
|
package/lib/routing.ts
CHANGED
|
@@ -107,8 +107,9 @@ function appendTelegramSourceAttachmentSection(
|
|
|
107
107
|
text: string,
|
|
108
108
|
from: string | undefined,
|
|
109
109
|
files: Pick<Media.DownloadedTelegramFile, "path">[],
|
|
110
|
+
outputs: readonly string[] = [],
|
|
110
111
|
): string {
|
|
111
|
-
if (files.length === 0) return text;
|
|
112
|
+
if (files.length === 0 && outputs.length === 0) return text;
|
|
112
113
|
const dirs = [...new Set(files.map((file) => dirname(file.path)))];
|
|
113
114
|
const sameDir = dirs.length === 1;
|
|
114
115
|
const source = from ? `|from:${from}` : "";
|
|
@@ -118,8 +119,15 @@ function appendTelegramSourceAttachmentSection(
|
|
|
118
119
|
const items = sameDir
|
|
119
120
|
? files.map((file) => `/${basename(file.path)}`)
|
|
120
121
|
: files.map((file) => file.path);
|
|
121
|
-
const
|
|
122
|
-
|
|
122
|
+
const sections = text ? [text] : [];
|
|
123
|
+
if (items.length > 0) {
|
|
124
|
+
sections.push(`${header}\n${items.map((item) => `- ${item}`).join("\n")}`);
|
|
125
|
+
}
|
|
126
|
+
if (outputs.length > 0) {
|
|
127
|
+
const outputHeader = `[outputs${source}]`;
|
|
128
|
+
sections.push(`${outputHeader}\n${outputs.map((output) => `- ${output}`).join("\n")}`);
|
|
129
|
+
}
|
|
130
|
+
return sections.join("\n\n");
|
|
123
131
|
}
|
|
124
132
|
|
|
125
133
|
function getContextCwd(ctx: unknown): string | undefined {
|
|
@@ -269,7 +277,7 @@ function getTelegramRoutableThreadRecords(
|
|
|
269
277
|
|
|
270
278
|
function formatTelegramAllTabMenuChooserText(command: string): string {
|
|
271
279
|
return [
|
|
272
|
-
"<b>🧵 Choose target thread
|
|
280
|
+
"<b>🧵 Choose target thread:</b>",
|
|
273
281
|
"",
|
|
274
282
|
`You used <code>/${escapeHtml(command)}</code> from the <b>All</b> tab.`,
|
|
275
283
|
"Select the Pi thread that should handle it:",
|
|
@@ -332,7 +340,7 @@ function buildTelegramUnboundRerouteRestoreChooserMarkup(
|
|
|
332
340
|
|
|
333
341
|
function formatTelegramUnboundRerouteRestoreChooserText(): string {
|
|
334
342
|
return [
|
|
335
|
-
"<b>🧵 Replace/restore Telegram thread
|
|
343
|
+
"<b>🧵 Replace/restore Telegram thread:</b>",
|
|
336
344
|
"",
|
|
337
345
|
"Choose the Pi instance to move to this new Telegram thread:",
|
|
338
346
|
].join("\n");
|
|
@@ -340,7 +348,7 @@ function formatTelegramUnboundRerouteRestoreChooserText(): string {
|
|
|
340
348
|
|
|
341
349
|
function formatTelegramUnboundTopicGuidance(): string {
|
|
342
350
|
return [
|
|
343
|
-
"
|
|
351
|
+
"<b>⚠️ New thread is not a Pi instance.</b>",
|
|
344
352
|
"",
|
|
345
353
|
"To create a bound Telegram tab:",
|
|
346
354
|
"<code>1.</code> Start another Pi instance in your terminal.",
|
|
@@ -358,7 +366,7 @@ function formatTelegramUnboundRerouteChooserText(
|
|
|
358
366
|
options: { includeGuidance?: boolean } = {},
|
|
359
367
|
): string {
|
|
360
368
|
const rerouteText = [
|
|
361
|
-
"
|
|
369
|
+
"<b>🧵 Choose target thread:</b>",
|
|
362
370
|
"",
|
|
363
371
|
"Your message is still in this Telegram thread.",
|
|
364
372
|
"Select the Pi thread that should handle it:",
|
|
@@ -1877,6 +1885,18 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1877
1885
|
getPromptTemplateCommands,
|
|
1878
1886
|
persistConfig: deps.configStore.persist,
|
|
1879
1887
|
sendTextReply: deps.sendTextReply,
|
|
1888
|
+
getActiveTurnReply: () => {
|
|
1889
|
+
const activeTurn = deps.activeTurnRuntime.get();
|
|
1890
|
+
if (!activeTurn) return undefined;
|
|
1891
|
+
return async (text, options) => {
|
|
1892
|
+
await deps.sendTextReply(
|
|
1893
|
+
activeTurn.chatId,
|
|
1894
|
+
activeTurn.replyToMessageId,
|
|
1895
|
+
text,
|
|
1896
|
+
{ target: activeTurn.target, parseMode: options?.parseMode },
|
|
1897
|
+
);
|
|
1898
|
+
};
|
|
1899
|
+
},
|
|
1880
1900
|
sendInteractiveMessage: deps.sendInteractiveMessage,
|
|
1881
1901
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
1882
1902
|
});
|
|
@@ -2265,6 +2285,11 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2265
2285
|
)
|
|
2266
2286
|
: [];
|
|
2267
2287
|
assertExecutionCurrent();
|
|
2288
|
+
const processedReply =
|
|
2289
|
+
replyFiles.length > 0
|
|
2290
|
+
? await deps.inboundHandlerRuntime.process(replyFiles, "", ctx)
|
|
2291
|
+
: undefined;
|
|
2292
|
+
assertExecutionCurrent();
|
|
2268
2293
|
const files = await Media.downloadTelegramMessageFiles([guestMsg], {
|
|
2269
2294
|
downloadFile: deps.downloadFile,
|
|
2270
2295
|
});
|
|
@@ -2285,7 +2310,8 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2285
2310
|
sourceContext = appendTelegramSourceAttachmentSection(
|
|
2286
2311
|
replyBlock,
|
|
2287
2312
|
replyPeer,
|
|
2288
|
-
replyFiles,
|
|
2313
|
+
processedReply?.promptFiles ?? replyFiles,
|
|
2314
|
+
processedReply?.handlerOutputs,
|
|
2289
2315
|
);
|
|
2290
2316
|
}
|
|
2291
2317
|
const promptText = Turns.buildTelegramTurnPrompt({
|
package/lib/status.ts
CHANGED
|
@@ -1494,17 +1494,22 @@ function buildUsageSummary(stats: TelegramUsageStats): string | undefined {
|
|
|
1494
1494
|
const tokenParts: string[] = [];
|
|
1495
1495
|
if (stats.totalInput) tokenParts.push(`↑${formatTokens(stats.totalInput)}`);
|
|
1496
1496
|
if (stats.totalOutput) tokenParts.push(`↓${formatTokens(stats.totalOutput)}`);
|
|
1497
|
+
return tokenParts.length > 0 ? tokenParts.join(" ") : undefined;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
function buildCacheSummary(stats: TelegramUsageStats): string | undefined {
|
|
1501
|
+
const cacheParts: string[] = [];
|
|
1497
1502
|
if (stats.totalCacheRead)
|
|
1498
|
-
|
|
1503
|
+
cacheParts.push(`R${formatTokens(stats.totalCacheRead)}`);
|
|
1499
1504
|
if (stats.totalCacheWrite)
|
|
1500
|
-
|
|
1505
|
+
cacheParts.push(`W${formatTokens(stats.totalCacheWrite)}`);
|
|
1501
1506
|
if (
|
|
1502
1507
|
(stats.totalCacheRead > 0 || stats.totalCacheWrite > 0) &&
|
|
1503
1508
|
stats.latestCacheHitRate !== undefined
|
|
1504
1509
|
) {
|
|
1505
|
-
|
|
1510
|
+
cacheParts.push(`CH${stats.latestCacheHitRate.toFixed(1)}%`);
|
|
1506
1511
|
}
|
|
1507
|
-
return
|
|
1512
|
+
return cacheParts.length > 0 ? cacheParts.join(" ") : undefined;
|
|
1508
1513
|
}
|
|
1509
1514
|
|
|
1510
1515
|
function buildCostSummary(
|
|
@@ -1557,10 +1562,14 @@ export function buildStatusHtml(
|
|
|
1557
1562
|
),
|
|
1558
1563
|
];
|
|
1559
1564
|
const usageSummary = buildUsageSummary(stats);
|
|
1565
|
+
const cacheSummary = buildCacheSummary(stats);
|
|
1560
1566
|
const costSummary = buildCostSummary(stats, usesSubscription);
|
|
1561
1567
|
if (usageSummary) {
|
|
1562
1568
|
lines.push(buildStatusRow("Tokens", usageSummary));
|
|
1563
1569
|
}
|
|
1570
|
+
if (cacheSummary) {
|
|
1571
|
+
lines.push(buildStatusRow("Cache", cacheSummary));
|
|
1572
|
+
}
|
|
1564
1573
|
if (costSummary) {
|
|
1565
1574
|
lines.push(buildStatusRow("Cost", costSummary));
|
|
1566
1575
|
}
|
package/lib/turns.ts
CHANGED
|
@@ -468,8 +468,17 @@ export function createTelegramPromptTurnRuntimeBuilder<
|
|
|
468
468
|
)
|
|
469
469
|
: [];
|
|
470
470
|
if (firstMessage) deps.assertExecutionCurrent?.(firstMessage);
|
|
471
|
+
const processedReply =
|
|
472
|
+
deps.processAttachments && replyFiles.length > 0
|
|
473
|
+
? await deps.processAttachments(replyFiles, "", ctx as TContext)
|
|
474
|
+
: undefined;
|
|
475
|
+
if (firstMessage) deps.assertExecutionCurrent?.(firstMessage);
|
|
471
476
|
const replyContext = firstMessage
|
|
472
|
-
? buildTelegramReplyContextBlock(
|
|
477
|
+
? buildTelegramReplyContextBlock(
|
|
478
|
+
firstMessage,
|
|
479
|
+
processedReply?.promptFiles ?? replyFiles,
|
|
480
|
+
processedReply?.handlerOutputs,
|
|
481
|
+
)
|
|
473
482
|
: "";
|
|
474
483
|
const forwardEntries = messages.flatMap((message) => {
|
|
475
484
|
const context = extractTelegramForwardContextText(
|
package/lib/updates.ts
CHANGED
|
@@ -895,7 +895,7 @@ const TELEGRAM_UNAUTHORIZED_DENIAL_COPY = "Access denied.";
|
|
|
895
895
|
|
|
896
896
|
function formatTelegramUnauthorizedDenial(format: "plain" | "html"): string {
|
|
897
897
|
return format === "html"
|
|
898
|
-
?
|
|
898
|
+
? `<b>🚫 ${TELEGRAM_UNAUTHORIZED_DENIAL_COPY}</b>`
|
|
899
899
|
: `🚫 ${TELEGRAM_UNAUTHORIZED_DENIAL_COPY}`;
|
|
900
900
|
}
|
|
901
901
|
|