@llblab/pi-kit 0.10.5 β 0.10.6
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 +4 -0
- package/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +8 -0
- package/node_modules/@llblab/pi-telegram/docs/ui-style.md +2 -2
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +9 -1
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +9 -0
- package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +4 -0
- package/node_modules/@llblab/pi-telegram/lib/menu-queue.ts +13 -13
- package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +0 -1
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +16 -2
- package/node_modules/@llblab/pi-telegram/lib/replies.ts +261 -4
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +11 -3
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +58 -10
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@llblab/pi-kit` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.10.6 - 2026-09-12
|
|
6
|
+
|
|
7
|
+
- `Guest Placeholder And Delivery`: Advances the exact Telegram pin to `0.45.6`, so the guest ACK rotates the globe placeholder in whole cycles and holds `π Working on it...`, guest answers are still delivered when a companion extension suppresses the final assistant message, `429` retry waits are recorded as `api` runtime events, the Thread display settings card drops the manual `/name` hint, and empty-queue headings carry no trailing period. The package set, resource inventory, and explicit load order remain unchanged.
|
|
8
|
+
|
|
5
9
|
## 0.10.5 - 2026-09-11
|
|
6
10
|
|
|
7
11
|
- `State Flow Telegram Surface`: Advances the exact State Flow pin to `0.9.6`, so the submenu help line is exactly what State Flow records and when a new session resumes from the last committed point, and the state value (`off` or `#N`) renders in monospace. The package set, resource inventory, and explicit load order remain unchanged.
|
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ Package links lead to the owning repositories for usage, documentation, issues,
|
|
|
15
15
|
| [`@llblab/pi-codex-usage`](https://github.com/llblab/pi-codex-usage) | `0.9.4` | Compact Codex/Spark subscription-limit status |
|
|
16
16
|
| [`@llblab/pi-grow-loop`](https://github.com/llblab/pi-grow-loop) | `0.8.1` | Visible continuation scheduling and bounded worker Skills |
|
|
17
17
|
| [`@llblab/pi-state-flow`](https://github.com/llblab/pi-state-flow) | `0.9.6` | Atomic scoped state cohorts with final-eligibility resolution |
|
|
18
|
-
| [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.45.
|
|
18
|
+
| [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.45.6` | Telegram companion, queues, files, voice, controls, and Generative Apps guidance |
|
|
19
19
|
| [`@llblab/skills`](https://github.com/llblab/skills) | `1.15.0` | Portable workflows for engineering, review, design, context maintenance, and other focused tasks |
|
|
20
20
|
|
|
21
21
|
Versions are exact by design. An upstream release does not change an installed kit until this repository explicitly advances the dependency and publishes a new kit version. Runtime defects and package-specific feature requests belong in the linked repository; package selection and kit installation issues belong here.
|
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.45.6: Guest Placeholder And Delivery Hotfix
|
|
8
|
+
|
|
9
|
+
- `Guest Placeholder`: The guest ACK answers with the first bold globe frame (`π Working on it.`) and rotates `π`/`π` once per second with the dots growing every two seconds. Rotation now completes whole 6-frame cycles and stops only after the 20 s minimum, so a pending answer holds the cycle's final frame (`π Working on it...`) instead of cutting mid-step; a 26 s safety bound stays clear of the ~28 s Telegram flood-control wall, and error backoff and pre-replacement cancellation are unchanged.
|
|
10
|
+
- `Guest Answer Delivery`: The guest replacement now falls back to the run's latest completed assistant text when Pi's final assistant message is empty, so an answer preserved by a companion extension's fallback turn (for example State Flow's suppressed final:true patch turn) is still edited into the guest message instead of being skipped as "no editable text".
|
|
11
|
+
- `Rate-Limit Visibility`: A `429` retry wait now records an `api` runtime event with the method, wait duration, attempt, and server `retry_after`, so a silent flood-control pause before the final guest replacement is visible in the runtime log instead of appearing only as a frozen frame.
|
|
12
|
+
- `Thread Display Hint`: The Thread display settings card no longer carries the manual `/name Name` override hint; the current-value line, the three mode descriptions, and the live chooser are unchanged.
|
|
13
|
+
- `Queue Empty Headings`: The default empty-queue line and every rotating refresh title drop the trailing period, so the fully bold queue headings read as headings; wording, emoji, rotation order, callbacks, and refresh behavior are unchanged.
|
|
14
|
+
|
|
7
15
|
## 0.45.5: Queue Refresh Icon Hotfix
|
|
8
16
|
|
|
9
17
|
- `Queue Refresh Icon`: The queue menu's Refresh row now uses `π`, the canonical refresh glyph, reserving `π` for the State Flow Telegram identity. The button label is the only change; queue refresh behavior, callbacks, and the rotating empty-queue notices are unchanged.
|
|
@@ -104,7 +104,7 @@ The menu may clear internal Skip but cannot remove a reaction created by the use
|
|
|
104
104
|
|
|
105
105
|
### Decorative Or Local-Example Emoji
|
|
106
106
|
|
|
107
|
-
Some emoji are intentionally local examples or decorative variants, not global semantics. Empty-queue rotating messages (`π«`, `π`, `π³`, `π¦`, `π`, `π§`, `πͺ`, `π§Ί`, `π`, `π«§`, `πΈ`) are copy flavor only and must not become controls. Example extension icons such as `π§ͺ`, `π§`, and `π` are documentation fixtures for companion extensions, not built-in pi-telegram meanings.
|
|
107
|
+
Some emoji are intentionally local examples or decorative variants, not global semantics. Empty-queue rotating messages (`π«`, `π`, `π³`, `π¦`, `π`, `π§`, `πͺ`, `π§Ί`, `π`, `π«§`, `πΈ`) are copy flavor only and must not become controls. The Guest Mode placeholder frames (`π`, `π`, `π` stepping every second with dots growing once every two seconds) are the same kind of decorative copy: they complete whole 6-frame cycles over at least a ~20 s rotation while a guest answer is pending and then hold the cycle's final frame (a 26 s safety bound caps slow streams), never become controls, and must not carry another meaning. Example extension icons such as `π§ͺ`, `π§`, and `π` are documentation fixtures for companion extensions, not built-in pi-telegram meanings.
|
|
108
108
|
|
|
109
109
|
Thread UI rule: when a message heading, chooser, or status line is specifically about Telegram/Pi threads or target thread selection, start the heading with `π§΅`. Button labels for concrete thread targets should stay clean (`threadName` or slot fallback) and should not add `π§΅` to every target button unless the row would otherwise be ambiguous.
|
|
110
110
|
|
|
@@ -224,7 +224,7 @@ Message cards and standalone informational notices sent by the bot should start
|
|
|
224
224
|
Rules:
|
|
225
225
|
|
|
226
226
|
- Start with a bold heading or, for dialogs, a bold question.
|
|
227
|
-
- Format standalone notices as one fully bold line: relevant emoji, one space, concise sentence, and terminal period. Menu or chooser headings use the same fully bold form but end in a colon when controls or detail follow.
|
|
227
|
+
- Format standalone notices as one fully bold line: relevant emoji, one space, concise sentence, and terminal period. Menu or chooser headings use the same fully bold form but end in a colon when controls or detail follow. Empty-queue headings are the deliberate exception: fully bold, with no trailing period or colon.
|
|
228
228
|
- Keep the emoji and complete sentence or heading inside the single bold span; do not bold only a fragment. A material name or phrase may receive nested italic emphasis without breaking the outer bold hierarchyβfor example `<b>π‘ Instance <i>Cedar</i> connected.</b>`.
|
|
229
229
|
- Apply the same hierarchy to success, progress, empty, busy, unavailable, cancellation, and failure notices.
|
|
230
230
|
- Callback alerts remain plain text because Telegram does not support rich text there, but still keep the relevant emoji and concise sentence.
|
|
@@ -823,6 +823,12 @@ interface TelegramLifecycleBindingDeps {
|
|
|
823
823
|
Pi.AgentEndEvent["messages"][number],
|
|
824
824
|
Keyboard.TelegramInlineKeyboardMarkup
|
|
825
825
|
>["editGuestReply"];
|
|
826
|
+
stopGuestPlaceholder?: Queue.TelegramAgentEndHookRuntimeDeps<
|
|
827
|
+
Queue.PendingTelegramTurn,
|
|
828
|
+
Pi.ExtensionContext,
|
|
829
|
+
Pi.AgentEndEvent["messages"][number],
|
|
830
|
+
Keyboard.TelegramInlineKeyboardMarkup
|
|
831
|
+
>["stopGuestPlaceholder"];
|
|
826
832
|
preparePreviewDelivery?: Queue.TelegramAgentEndRuntimeDeps<Queue.PendingTelegramTurn>["preparePreviewDelivery"];
|
|
827
833
|
finalizeMarkdownPreview: Queue.TelegramAgentEndHookRuntimeDeps<
|
|
828
834
|
Queue.PendingTelegramTurn,
|
|
@@ -876,6 +882,7 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
876
882
|
deleteMessage,
|
|
877
883
|
sendGuestReply,
|
|
878
884
|
editGuestReply,
|
|
885
|
+
stopGuestPlaceholder,
|
|
879
886
|
preparePreviewDelivery,
|
|
880
887
|
finalizeMarkdownPreview,
|
|
881
888
|
proactivePushTargetGetter,
|
|
@@ -1056,7 +1063,7 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
1056
1063
|
updateStatus,
|
|
1057
1064
|
getActiveTurn: activeTurnRuntime.get,
|
|
1058
1065
|
loadConfig: configStore.load,
|
|
1059
|
-
extractAssistant: Replies.
|
|
1066
|
+
extractAssistant: Replies.extractRunAssistantMessage,
|
|
1060
1067
|
getFoldQueuedPromptsIntoHistory:
|
|
1061
1068
|
lifecycle.shouldFoldQueuedPromptsIntoHistory,
|
|
1062
1069
|
resetRuntimeState: agentEndResetter,
|
|
@@ -1090,6 +1097,7 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
1090
1097
|
answerGuestQuery,
|
|
1091
1098
|
sendGuestReply,
|
|
1092
1099
|
editGuestReply,
|
|
1100
|
+
stopGuestPlaceholder,
|
|
1093
1101
|
sendGuestAttachment,
|
|
1094
1102
|
sendGuestVoiceReply,
|
|
1095
1103
|
planOutboundReply: outboundReplyPlanner,
|
|
@@ -1429,7 +1429,7 @@ export async function handleTelegramNextCommand(deps: {
|
|
|
1429
1429
|
deps.clearPendingModelSwitch();
|
|
1430
1430
|
if (!deps.hasQueuedItems()) {
|
|
1431
1431
|
await deps.sendTextReply(
|
|
1432
|
-
formatTelegramInformationHeading("β", "Queue is empty
|
|
1432
|
+
formatTelegramInformationHeading("β", "Queue is empty"),
|
|
1433
1433
|
{ parseMode: "HTML" },
|
|
1434
1434
|
);
|
|
1435
1435
|
return;
|
|
@@ -588,6 +588,12 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
588
588
|
const editGuestReply = Replies.createGuestMarkdownReplyEditor({
|
|
589
589
|
editGuestInlineMessage: telegramApiRuntime.editGuestInlineMessage,
|
|
590
590
|
});
|
|
591
|
+
// Rotate the guest placeholder frames until the final replacement stops them.
|
|
592
|
+
const guestPlaceholderRuntime =
|
|
593
|
+
Replies.createTelegramGuestPlaceholderRuntime({
|
|
594
|
+
editGuestInlineMessage: telegramApiRuntime.editGuestInlineMessage,
|
|
595
|
+
recordRuntimeEvent,
|
|
596
|
+
});
|
|
591
597
|
|
|
592
598
|
const promptDispatchRuntime = Runtime.createTelegramPromptDispatchRuntime({
|
|
593
599
|
lifecycle,
|
|
@@ -978,6 +984,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
978
984
|
deleteMessage: deleteTelegramMessage,
|
|
979
985
|
answerGuestQuery,
|
|
980
986
|
answerGuestQueryForInlineMessage,
|
|
987
|
+
startGuestPlaceholder: guestPlaceholderRuntime.start,
|
|
981
988
|
sendTextReply,
|
|
982
989
|
setMyCommands,
|
|
983
990
|
validateThreadName(threadName) {
|
|
@@ -1508,6 +1515,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1508
1515
|
},
|
|
1509
1516
|
capabilityMonitor: telegramThreadCapabilityMonitor,
|
|
1510
1517
|
queueWatchdog: queueDispatchWatchdogRuntime,
|
|
1518
|
+
guestPlaceholder: { stopAll: guestPlaceholderRuntime.stopAll },
|
|
1511
1519
|
},
|
|
1512
1520
|
});
|
|
1513
1521
|
const sessionLifecycleRuntime =
|
|
@@ -1828,6 +1836,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1828
1836
|
deleteMessage: deleteTelegramMessage,
|
|
1829
1837
|
sendGuestReply,
|
|
1830
1838
|
editGuestReply,
|
|
1839
|
+
stopGuestPlaceholder: guestPlaceholderRuntime.stop,
|
|
1831
1840
|
finalizeMarkdownPreview,
|
|
1832
1841
|
preparePreviewDelivery,
|
|
1833
1842
|
proactivePushTargetGetter,
|
|
@@ -265,6 +265,7 @@ export interface TelegramBridgeSessionServiceRuntime {
|
|
|
265
265
|
inboundWorker: { onSessionShutdown(): Promise<void> };
|
|
266
266
|
capabilityMonitor: { start(ctx: ExtensionContext): void; stop(): void };
|
|
267
267
|
queueWatchdog: { start(ctx: ExtensionContext): void; stop(): void };
|
|
268
|
+
guestPlaceholder?: { stopAll(): void };
|
|
268
269
|
}
|
|
269
270
|
|
|
270
271
|
export interface TelegramBridgeSessionLifecycleAssemblyDeps<
|
|
@@ -320,6 +321,7 @@ export interface TelegramBridgeSessionLifecyclePorts<
|
|
|
320
321
|
inboundWorker: TelegramBridgeSessionServiceRuntime["inboundWorker"];
|
|
321
322
|
capabilityMonitor: TelegramBridgeSessionServiceRuntime["capabilityMonitor"];
|
|
322
323
|
queueWatchdog: TelegramBridgeSessionServiceRuntime["queueWatchdog"];
|
|
324
|
+
guestPlaceholder?: TelegramBridgeSessionServiceRuntime["guestPlaceholder"];
|
|
323
325
|
};
|
|
324
326
|
}
|
|
325
327
|
|
|
@@ -347,6 +349,7 @@ export function createTelegramBridgeSessionLifecycleDeps<
|
|
|
347
349
|
inboundWorker: ports.services.inboundWorker,
|
|
348
350
|
capabilityMonitor: ports.services.capabilityMonitor,
|
|
349
351
|
queueWatchdog: ports.services.queueWatchdog,
|
|
352
|
+
guestPlaceholder: ports.services.guestPlaceholder,
|
|
350
353
|
},
|
|
351
354
|
};
|
|
352
355
|
}
|
|
@@ -383,6 +386,7 @@ export function createTelegramBridgeSessionLifecycleAssembly<
|
|
|
383
386
|
},
|
|
384
387
|
async onSessionShutdown(event, ctx) {
|
|
385
388
|
if (!isSessionActive(ctx)) return;
|
|
389
|
+
deps.services.guestPlaceholder?.stopAll();
|
|
386
390
|
await deps.services.delivery.onSessionShutdown();
|
|
387
391
|
if (!isSessionActive(ctx)) return;
|
|
388
392
|
deps.services.queueWatchdog.stop();
|
|
@@ -14,18 +14,18 @@ import * as Queue from "./queue.ts";
|
|
|
14
14
|
const QUEUE_ITEM_PROMPT_HTML_LIMIT = 3600;
|
|
15
15
|
const QUEUE_ITEM_PROMPT_TRUNCATION_SUFFIX = "\n⦠[truncated]";
|
|
16
16
|
const EMPTY_QUEUE_REFRESH_TITLES = [
|
|
17
|
-
"<b>β Queue is still empty
|
|
18
|
-
"<b>π« Still nothing in queue
|
|
19
|
-
"<b>π Queue remains empty
|
|
20
|
-
"<b>π³ Nothing queued yet
|
|
21
|
-
"<b>π¦ Queue crickets continue
|
|
22
|
-
"<b>π Queue is peacefully idle
|
|
23
|
-
"<b>π§ Nothing waiting. Very zen
|
|
24
|
-
"<b>πͺ Queue orbit is clear
|
|
25
|
-
"<b>π§Ί Basket is empty
|
|
26
|
-
"<b>π No prompts on the horizon
|
|
27
|
-
"<b>π«§ Queue bubbles: none
|
|
28
|
-
"<b>πΈ No queued signals detected
|
|
17
|
+
"<b>β Queue is still empty</b>",
|
|
18
|
+
"<b>π« Still nothing in queue</b>",
|
|
19
|
+
"<b>π Queue remains empty</b>",
|
|
20
|
+
"<b>π³ Nothing queued yet</b>",
|
|
21
|
+
"<b>π¦ Queue crickets continue</b>",
|
|
22
|
+
"<b>π Queue is peacefully idle</b>",
|
|
23
|
+
"<b>π§ Nothing waiting. Very zen</b>",
|
|
24
|
+
"<b>πͺ Queue orbit is clear</b>",
|
|
25
|
+
"<b>π§Ί Basket is empty</b>",
|
|
26
|
+
"<b>π No prompts on the horizon</b>",
|
|
27
|
+
"<b>π«§ Queue bubbles: none</b>",
|
|
28
|
+
"<b>πΈ No queued signals detected</b>",
|
|
29
29
|
] as const;
|
|
30
30
|
type TelegramQueueMenuReplyMarkup = TelegramInlineKeyboardMarkup;
|
|
31
31
|
interface TelegramQueueMenuItem {
|
|
@@ -340,7 +340,7 @@ function getTelegramQueueMenuListText(
|
|
|
340
340
|
emptyRefreshIndex?: number,
|
|
341
341
|
): string {
|
|
342
342
|
if (items.length > 0) return "<b>β³ Queue:</b>";
|
|
343
|
-
if (emptyRefreshIndex === undefined) return "<b>β Queue is empty
|
|
343
|
+
if (emptyRefreshIndex === undefined) return "<b>β Queue is empty</b>";
|
|
344
344
|
return EMPTY_QUEUE_REFRESH_TITLES[
|
|
345
345
|
emptyRefreshIndex % EMPTY_QUEUE_REFRESH_TITLES.length
|
|
346
346
|
];
|
|
@@ -184,7 +184,6 @@ export function buildThreadDisplaySettingsText(mode: TelegramThreadDisplayMode):
|
|
|
184
184
|
"<code>-</code> <code>letters</code> (default): show the unique slot, such as <b><i>A</i></b> or <b><i>B</i></b>.",
|
|
185
185
|
"<code>-</code> <code>names</code>: show the generated dictionary name for the slot, such as <b><i>Anchor</i></b> or <b><i>Briar</i></b>.",
|
|
186
186
|
"<code>-</code> <code>directories</code>: show the directory, such as <b><i>extensions</i></b>; shared Workspaces keep slot suffixes, such as <b><i>extensions_a</i></b> and <b><i>extensions_c</i></b>.",
|
|
187
|
-
"A manual <code>/name Name</code> overrides this Thread display name until reset.",
|
|
188
187
|
].join("\n");
|
|
189
188
|
}
|
|
190
189
|
|
|
@@ -1554,6 +1554,8 @@ export interface TelegramAgentEndRuntimeDeps<
|
|
|
1554
1554
|
sendGuestReply?: (guestQueryId: string, markdown: string) => Promise<void>;
|
|
1555
1555
|
/** Replaces the early guest ACK with the final text. */
|
|
1556
1556
|
editGuestReply?: (inlineMessageId: string, markdown: string) => Promise<void>;
|
|
1557
|
+
/** Cancels the animated guest placeholder before the final edit. */
|
|
1558
|
+
stopGuestPlaceholder?: (inlineMessageId: string) => Promise<void>;
|
|
1557
1559
|
sendGuestAttachment?: (
|
|
1558
1560
|
turn: TTurn,
|
|
1559
1561
|
attachment: QueuedAttachment,
|
|
@@ -1633,6 +1635,7 @@ export interface TelegramAgentEndHookRuntimeDeps<
|
|
|
1633
1635
|
answerGuestQuery?: TelegramAgentEndRuntimeDeps<TTurn>["answerGuestQuery"];
|
|
1634
1636
|
sendGuestReply?: TelegramAgentEndRuntimeDeps<TTurn>["sendGuestReply"];
|
|
1635
1637
|
editGuestReply?: TelegramAgentEndRuntimeDeps<TTurn>["editGuestReply"];
|
|
1638
|
+
stopGuestPlaceholder?: TelegramAgentEndRuntimeDeps<TTurn>["stopGuestPlaceholder"];
|
|
1636
1639
|
sendGuestAttachment?: TelegramAgentEndRuntimeDeps<TTurn>["sendGuestAttachment"];
|
|
1637
1640
|
sendGuestVoiceReply?: TelegramAgentEndRuntimeDeps<TTurn>["sendGuestVoiceReply"];
|
|
1638
1641
|
planOutboundReply?: TelegramAgentEndRuntimeDeps<
|
|
@@ -1774,6 +1777,7 @@ export function createTelegramAgentEndHook<
|
|
|
1774
1777
|
answerGuestQuery: deps.answerGuestQuery,
|
|
1775
1778
|
sendGuestReply: deps.sendGuestReply,
|
|
1776
1779
|
editGuestReply: deps.editGuestReply,
|
|
1780
|
+
stopGuestPlaceholder: deps.stopGuestPlaceholder,
|
|
1777
1781
|
sendGuestAttachment: deps.sendGuestAttachment,
|
|
1778
1782
|
sendGuestVoiceReply: deps.sendGuestVoiceReply,
|
|
1779
1783
|
planOutboundReply: deps.planOutboundReply,
|
|
@@ -1866,6 +1870,16 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
1866
1870
|
return;
|
|
1867
1871
|
}
|
|
1868
1872
|
if (turn.guestQueryId) {
|
|
1873
|
+
if (turn.guestInlineMessageId && deps.stopGuestPlaceholder) {
|
|
1874
|
+
try {
|
|
1875
|
+
await deps.stopGuestPlaceholder(turn.guestInlineMessageId);
|
|
1876
|
+
} catch (error) {
|
|
1877
|
+
deps.recordRuntimeEvent?.("delivery", error, {
|
|
1878
|
+
phase: "guest-placeholder-stop",
|
|
1879
|
+
guestQueryId: turn.guestQueryId,
|
|
1880
|
+
});
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1869
1883
|
if (turn.guestInlineMessageId && deps.editGuestReply) {
|
|
1870
1884
|
const experimentText = assistant.errorMessage
|
|
1871
1885
|
? "Telegram bridge: Pi failed while processing the request."
|
|
@@ -1875,7 +1889,7 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
1875
1889
|
await deps.editGuestReply(turn.guestInlineMessageId, experimentText);
|
|
1876
1890
|
deps.recordRuntimeEvent?.(
|
|
1877
1891
|
"guest",
|
|
1878
|
-
new Error("Guest ACK
|
|
1892
|
+
new Error("Guest ACK edited the guest answer"),
|
|
1879
1893
|
{ phase: "guest-ack-edited", guestQueryId: turn.guestQueryId },
|
|
1880
1894
|
);
|
|
1881
1895
|
} catch (error) {
|
|
@@ -1887,7 +1901,7 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
1887
1901
|
} else {
|
|
1888
1902
|
deps.recordRuntimeEvent?.(
|
|
1889
1903
|
"delivery",
|
|
1890
|
-
new Error("Guest ACK
|
|
1904
|
+
new Error("Guest ACK turn produced no editable text"),
|
|
1891
1905
|
{ phase: "guest-ack-edit-empty", guestQueryId: turn.guestQueryId },
|
|
1892
1906
|
);
|
|
1893
1907
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Telegram reply delivery helpers
|
|
3
3
|
* Zones: telegram outbound, native rich markdown, UI/compat rendering transport
|
|
4
|
-
* Owns native assistant replies, rendered UI delivery, reply transport wiring, and plain text replies
|
|
4
|
+
* Owns native assistant replies, rendered UI delivery, guest placeholder rotation, reply transport wiring, and plain text replies
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { assertTelegramInlineKeyboardCallbackData } from "./keyboard.ts";
|
|
@@ -177,6 +177,47 @@ export function extractLatestAssistantMessageText(
|
|
|
177
177
|
return {};
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Extract the run's answer without trusting an empty final assistant message.
|
|
182
|
+
* A low-level run may end with a completed assistant message whose content was
|
|
183
|
+
* suppressed (a companion extension preserving an earlier draft, for example
|
|
184
|
+
* State Flow's fallback final:true patch turn). In that case the run's answer
|
|
185
|
+
* is the latest earlier completed assistant message that carries text;
|
|
186
|
+
* tool-use prefaces, errors, and aborts stay excluded.
|
|
187
|
+
*/
|
|
188
|
+
export function extractRunAssistantMessage(
|
|
189
|
+
messages: readonly unknown[],
|
|
190
|
+
): {
|
|
191
|
+
text?: string;
|
|
192
|
+
stopReason?: string;
|
|
193
|
+
errorMessage?: string;
|
|
194
|
+
} {
|
|
195
|
+
const latest = extractLatestAssistantMessageText(messages);
|
|
196
|
+
if (latest.text || latest.stopReason !== "stop") return latest;
|
|
197
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
198
|
+
const message = messages[i];
|
|
199
|
+
if (!message || !isAssistantAgentMessage(message)) continue;
|
|
200
|
+
const rawStopReason = getAgentMessageField(message, "stopReason");
|
|
201
|
+
if (
|
|
202
|
+
rawStopReason === "toolUse" ||
|
|
203
|
+
rawStopReason === "error" ||
|
|
204
|
+
rawStopReason === "aborted"
|
|
205
|
+
) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const text = getAgentMessageText(message);
|
|
209
|
+
if (!text) continue;
|
|
210
|
+
const rawErrorMessage = getAgentMessageField(message, "errorMessage");
|
|
211
|
+
return {
|
|
212
|
+
text,
|
|
213
|
+
stopReason: typeof rawStopReason === "string" ? rawStopReason : undefined,
|
|
214
|
+
errorMessage:
|
|
215
|
+
typeof rawErrorMessage === "string" ? rawErrorMessage : undefined,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
return latest;
|
|
219
|
+
}
|
|
220
|
+
|
|
180
221
|
export interface TelegramReplyOwnershipRecorder {
|
|
181
222
|
record: (input: {
|
|
182
223
|
chatId: number;
|
|
@@ -900,9 +941,9 @@ export function createGuestMarkdownReplySender(deps: {
|
|
|
900
941
|
}
|
|
901
942
|
|
|
902
943
|
/**
|
|
903
|
-
* Guest reply editor: replaces
|
|
904
|
-
*
|
|
905
|
-
*
|
|
944
|
+
* Guest reply editor: replaces the early Guest Mode placeholder ACK with
|
|
945
|
+
* native Rich Markdown content addressed by `inline_message_id` instead of a
|
|
946
|
+
* chat/message pair.
|
|
906
947
|
*/
|
|
907
948
|
export function createGuestMarkdownReplyEditor(deps: {
|
|
908
949
|
editGuestInlineMessage: (
|
|
@@ -917,3 +958,219 @@ export function createGuestMarkdownReplyEditor(deps: {
|
|
|
917
958
|
});
|
|
918
959
|
};
|
|
919
960
|
}
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* Guest Mode placeholder rotation: the early ACK is the first placeholder
|
|
964
|
+
* frame and the runtime steps the remaining frames once per interval, moving
|
|
965
|
+
* the globe every second while the trailing dots grow once every two seconds.
|
|
966
|
+
*
|
|
967
|
+
* Rotation completes whole six-frame cycles and only stops once at least
|
|
968
|
+
* `TELEGRAM_GUEST_PLACEHOLDER_MIN_MS` has elapsed, so a pending answer holds
|
|
969
|
+
* the finished cycle's last frame (`π Working on it...`) instead of whatever
|
|
970
|
+
* step a hard time cap happens to cut. The `TELEGRAM_GUEST_PLACEHOLDER_MAX_MS`
|
|
971
|
+
* safety bound keeps the edit stream clear of the first flood-control
|
|
972
|
+
* rejections measured in live guest runs (+26.5 s at ~53 edits, +27.8 s at
|
|
973
|
+
* ~28 edits): rotation caps at 23 edits and never starts a frame after 26 s.
|
|
974
|
+
*/
|
|
975
|
+
|
|
976
|
+
export const TELEGRAM_GUEST_PLACEHOLDER_FRAME_MS = 1_000;
|
|
977
|
+
|
|
978
|
+
export const TELEGRAM_GUEST_PLACEHOLDER_MIN_MS = 20_000;
|
|
979
|
+
|
|
980
|
+
export const TELEGRAM_GUEST_PLACEHOLDER_MAX_MS = 26_000;
|
|
981
|
+
|
|
982
|
+
export const TELEGRAM_GUEST_PLACEHOLDER_FRAMES = [
|
|
983
|
+
"<b>π Working on it.</b>",
|
|
984
|
+
"<b>π Working on it.</b>",
|
|
985
|
+
"<b>π Working on it..</b>",
|
|
986
|
+
"<b>π Working on it..</b>",
|
|
987
|
+
"<b>π Working on it...</b>",
|
|
988
|
+
"<b>π Working on it...</b>",
|
|
989
|
+
] as const;
|
|
990
|
+
|
|
991
|
+
export function buildTelegramGuestPlaceholderFrame(step: number): string {
|
|
992
|
+
const frames: readonly string[] = TELEGRAM_GUEST_PLACEHOLDER_FRAMES;
|
|
993
|
+
const index = ((step % frames.length) + frames.length) % frames.length;
|
|
994
|
+
return frames[index]!;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
export interface TelegramGuestPlaceholderRuntimeDeps {
|
|
998
|
+
editGuestInlineMessage: (
|
|
999
|
+
inlineMessageId: string,
|
|
1000
|
+
content: { text: string; parseMode: "HTML" },
|
|
1001
|
+
) => Promise<void>;
|
|
1002
|
+
recordRuntimeEvent?: (
|
|
1003
|
+
category: string,
|
|
1004
|
+
error: unknown,
|
|
1005
|
+
details?: Record<string, unknown>,
|
|
1006
|
+
) => void;
|
|
1007
|
+
intervalMs?: number;
|
|
1008
|
+
minMs?: number;
|
|
1009
|
+
maxMs?: number;
|
|
1010
|
+
now?: () => number;
|
|
1011
|
+
setTimer?: (
|
|
1012
|
+
callback: () => void,
|
|
1013
|
+
ms: number,
|
|
1014
|
+
) => ReturnType<typeof setTimeout>;
|
|
1015
|
+
clearTimer?: (timer: ReturnType<typeof setTimeout>) => void;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
export interface TelegramGuestPlaceholderRuntime {
|
|
1019
|
+
/** Starts the frame loop on an answered guest inline message. */
|
|
1020
|
+
start: (inlineMessageId: string) => void;
|
|
1021
|
+
/** Cancels the loop and waits for any in-flight frame edit before returning. */
|
|
1022
|
+
stop: (inlineMessageId: string) => Promise<void>;
|
|
1023
|
+
/** Cancels every loop without waiting for in-flight edits (session shutdown). */
|
|
1024
|
+
stopAll: () => void;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
interface TelegramGuestPlaceholderSession {
|
|
1028
|
+
step: number;
|
|
1029
|
+
stopped: boolean;
|
|
1030
|
+
startedAtMs: number;
|
|
1031
|
+
finished: boolean;
|
|
1032
|
+
timer?: ReturnType<typeof setTimeout>;
|
|
1033
|
+
inflight?: Promise<void>;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function getTelegramGuestPlaceholderRetryDelayMs(
|
|
1037
|
+
error: unknown,
|
|
1038
|
+
fallbackMs: number,
|
|
1039
|
+
): number {
|
|
1040
|
+
const retryAfterSeconds = (
|
|
1041
|
+
error as { retryAfterSeconds?: unknown } | undefined
|
|
1042
|
+
)?.retryAfterSeconds;
|
|
1043
|
+
return typeof retryAfterSeconds === "number" && retryAfterSeconds > 0
|
|
1044
|
+
? Math.max(fallbackMs, retryAfterSeconds * 1_000)
|
|
1045
|
+
: fallbackMs;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
export function createTelegramGuestPlaceholderRuntime(
|
|
1049
|
+
deps: TelegramGuestPlaceholderRuntimeDeps,
|
|
1050
|
+
): TelegramGuestPlaceholderRuntime {
|
|
1051
|
+
const intervalMs = deps.intervalMs ?? TELEGRAM_GUEST_PLACEHOLDER_FRAME_MS;
|
|
1052
|
+
const minMs = deps.minMs ?? TELEGRAM_GUEST_PLACEHOLDER_MIN_MS;
|
|
1053
|
+
const maxMs = deps.maxMs ?? TELEGRAM_GUEST_PLACEHOLDER_MAX_MS;
|
|
1054
|
+
const now = deps.now ?? Date.now;
|
|
1055
|
+
const setTimer =
|
|
1056
|
+
deps.setTimer ??
|
|
1057
|
+
((callback: () => void, ms: number): ReturnType<typeof setTimeout> =>
|
|
1058
|
+
setTimeout(callback, ms));
|
|
1059
|
+
const clearTimer =
|
|
1060
|
+
deps.clearTimer ??
|
|
1061
|
+
((timer: ReturnType<typeof setTimeout>): void => clearTimeout(timer));
|
|
1062
|
+
const sessions = new Map<string, TelegramGuestPlaceholderSession>();
|
|
1063
|
+
|
|
1064
|
+
const finishRotation = (
|
|
1065
|
+
session: TelegramGuestPlaceholderSession,
|
|
1066
|
+
elapsedMs: number,
|
|
1067
|
+
): void => {
|
|
1068
|
+
if (session.finished) return;
|
|
1069
|
+
session.finished = true;
|
|
1070
|
+
deps.recordRuntimeEvent?.(
|
|
1071
|
+
"guest",
|
|
1072
|
+
"Guest placeholder rotation reached its bound",
|
|
1073
|
+
{
|
|
1074
|
+
phase: "guest-placeholder-capped",
|
|
1075
|
+
minMs,
|
|
1076
|
+
maxMs,
|
|
1077
|
+
elapsedMs,
|
|
1078
|
+
step: session.step,
|
|
1079
|
+
},
|
|
1080
|
+
);
|
|
1081
|
+
};
|
|
1082
|
+
|
|
1083
|
+
const scheduleFrame = (
|
|
1084
|
+
inlineMessageId: string,
|
|
1085
|
+
session: TelegramGuestPlaceholderSession,
|
|
1086
|
+
delayMs: number,
|
|
1087
|
+
): void => {
|
|
1088
|
+
if (session.stopped) return;
|
|
1089
|
+
const elapsedMs = now() - session.startedAtMs;
|
|
1090
|
+
const frameCount = TELEGRAM_GUEST_PLACEHOLDER_FRAMES.length;
|
|
1091
|
+
const cycleComplete =
|
|
1092
|
+
session.step > 0 && session.step % frameCount === frameCount - 1;
|
|
1093
|
+
if (cycleComplete && elapsedMs >= minMs) {
|
|
1094
|
+
finishRotation(session, elapsedMs);
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
if (elapsedMs + delayMs > maxMs) {
|
|
1098
|
+
finishRotation(session, elapsedMs);
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
const timer = setTimer(() => {
|
|
1102
|
+
session.timer = undefined;
|
|
1103
|
+
runFrame(inlineMessageId, session);
|
|
1104
|
+
}, delayMs);
|
|
1105
|
+
timer.unref?.();
|
|
1106
|
+
session.timer = timer;
|
|
1107
|
+
};
|
|
1108
|
+
|
|
1109
|
+
const runFrame = (
|
|
1110
|
+
inlineMessageId: string,
|
|
1111
|
+
session: TelegramGuestPlaceholderSession,
|
|
1112
|
+
): void => {
|
|
1113
|
+
if (session.stopped) return;
|
|
1114
|
+
session.step += 1;
|
|
1115
|
+
let nextDelayMs = intervalMs;
|
|
1116
|
+
session.inflight = (async () => {
|
|
1117
|
+
try {
|
|
1118
|
+
await deps.editGuestInlineMessage(inlineMessageId, {
|
|
1119
|
+
text: buildTelegramGuestPlaceholderFrame(session.step),
|
|
1120
|
+
parseMode: "HTML",
|
|
1121
|
+
});
|
|
1122
|
+
} catch (error) {
|
|
1123
|
+
nextDelayMs = getTelegramGuestPlaceholderRetryDelayMs(error, intervalMs);
|
|
1124
|
+
deps.recordRuntimeEvent?.("guest", error, {
|
|
1125
|
+
phase: "guest-placeholder-edit",
|
|
1126
|
+
retryAfterMs: nextDelayMs,
|
|
1127
|
+
});
|
|
1128
|
+
} finally {
|
|
1129
|
+
session.inflight = undefined;
|
|
1130
|
+
scheduleFrame(inlineMessageId, session, nextDelayMs);
|
|
1131
|
+
}
|
|
1132
|
+
})();
|
|
1133
|
+
};
|
|
1134
|
+
|
|
1135
|
+
const stop = async (inlineMessageId: string): Promise<void> => {
|
|
1136
|
+
const session = sessions.get(inlineMessageId);
|
|
1137
|
+
if (!session) return;
|
|
1138
|
+
sessions.delete(inlineMessageId);
|
|
1139
|
+
session.stopped = true;
|
|
1140
|
+
if (session.timer !== undefined) {
|
|
1141
|
+
clearTimer(session.timer);
|
|
1142
|
+
session.timer = undefined;
|
|
1143
|
+
}
|
|
1144
|
+
try {
|
|
1145
|
+
await session.inflight;
|
|
1146
|
+
} catch {
|
|
1147
|
+
// Frame failures are reported as runtime events; stopping stays fail-open.
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
1150
|
+
|
|
1151
|
+
return {
|
|
1152
|
+
start(inlineMessageId) {
|
|
1153
|
+
const existing = sessions.get(inlineMessageId);
|
|
1154
|
+
if (existing) {
|
|
1155
|
+
existing.stopped = true;
|
|
1156
|
+
if (existing.timer !== undefined) clearTimer(existing.timer);
|
|
1157
|
+
}
|
|
1158
|
+
const session: TelegramGuestPlaceholderSession = {
|
|
1159
|
+
step: 0,
|
|
1160
|
+
stopped: false,
|
|
1161
|
+
startedAtMs: now(),
|
|
1162
|
+
finished: false,
|
|
1163
|
+
};
|
|
1164
|
+
sessions.set(inlineMessageId, session);
|
|
1165
|
+
scheduleFrame(inlineMessageId, session, intervalMs);
|
|
1166
|
+
},
|
|
1167
|
+
stop,
|
|
1168
|
+
stopAll() {
|
|
1169
|
+
for (const session of sessions.values()) {
|
|
1170
|
+
session.stopped = true;
|
|
1171
|
+
if (session.timer !== undefined) clearTimer(session.timer);
|
|
1172
|
+
}
|
|
1173
|
+
sessions.clear();
|
|
1174
|
+
},
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
@@ -16,6 +16,7 @@ import * as Model from "./model.ts";
|
|
|
16
16
|
import * as OutboundHandlers from "./outbound.ts";
|
|
17
17
|
import * as PromptTemplates from "./prompt-templates.ts";
|
|
18
18
|
import * as Queue from "./queue.ts";
|
|
19
|
+
import * as Replies from "./replies.ts";
|
|
19
20
|
import type { TelegramBridgeRuntime } from "./runtime.ts";
|
|
20
21
|
import type { TelegramSectionRegistry } from "./sections.ts";
|
|
21
22
|
import * as TextGroups from "./text-groups.ts";
|
|
@@ -665,6 +666,8 @@ export interface TelegramInboundRouteRuntimeDeps<
|
|
|
665
666
|
text: string,
|
|
666
667
|
options?: { parseMode?: "HTML" },
|
|
667
668
|
) => Promise<string | undefined>;
|
|
669
|
+
/** Starts the animated placeholder on an answered guest inline message. */
|
|
670
|
+
startGuestPlaceholder?: (inlineMessageId: string) => void;
|
|
668
671
|
sendTextReply: (
|
|
669
672
|
chatId: number,
|
|
670
673
|
replyToMessageId: number,
|
|
@@ -2662,8 +2665,10 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2662
2665
|
assertExecutionCurrent();
|
|
2663
2666
|
};
|
|
2664
2667
|
// Answer the guest query immediately so the agent-end edit can replace the
|
|
2665
|
-
// early ACK once the turn settles.
|
|
2666
|
-
|
|
2668
|
+
// early ACK once the turn settles. The ACK is the first placeholder frame and
|
|
2669
|
+
// the loop rotates through the remaining frames until the replacement.
|
|
2670
|
+
const TELEGRAM_GUEST_ACK_HTML =
|
|
2671
|
+
Replies.buildTelegramGuestPlaceholderFrame(0);
|
|
2667
2672
|
const handleAuthorizedTelegramGuestMessage = async (
|
|
2668
2673
|
guestMessage: Updates.TelegramGuestMessage & { from: TelegramUser },
|
|
2669
2674
|
ctx: TContext,
|
|
@@ -2679,9 +2684,12 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2679
2684
|
TELEGRAM_GUEST_ACK_HTML,
|
|
2680
2685
|
{ parseMode: "HTML" },
|
|
2681
2686
|
);
|
|
2687
|
+
if (guestInlineMessageId) {
|
|
2688
|
+
deps.startGuestPlaceholder?.(guestInlineMessageId);
|
|
2689
|
+
}
|
|
2682
2690
|
deps.recordRuntimeEvent?.(
|
|
2683
2691
|
"guest",
|
|
2684
|
-
new Error("Guest ACK
|
|
2692
|
+
new Error("Guest ACK answered the guest query"),
|
|
2685
2693
|
{
|
|
2686
2694
|
phase: "guest-ack-sent",
|
|
2687
2695
|
guestQueryId: guestMessage.guest_query_id,
|
|
@@ -374,6 +374,13 @@ interface TelegramApiResponse<T> {
|
|
|
374
374
|
parameters?: { retry_after?: number };
|
|
375
375
|
}
|
|
376
376
|
|
|
377
|
+
export interface TelegramApiRetryWait {
|
|
378
|
+
method: string;
|
|
379
|
+
delayMs: number;
|
|
380
|
+
attempt: number;
|
|
381
|
+
retryAfterSeconds?: number;
|
|
382
|
+
}
|
|
383
|
+
|
|
377
384
|
export interface TelegramApiCallOptions {
|
|
378
385
|
signal?: AbortSignal;
|
|
379
386
|
maxAttempts?: number;
|
|
@@ -381,6 +388,8 @@ export interface TelegramApiCallOptions {
|
|
|
381
388
|
retrySafety?: "safe" | "non-idempotent";
|
|
382
389
|
retryBaseDelayMs?: number;
|
|
383
390
|
sleep?: (ms: number) => Promise<void>;
|
|
391
|
+
/** Observability hook fired before a 429 retry wait, never for 5xx waits. */
|
|
392
|
+
onRetryWait?: (wait: TelegramApiRetryWait) => void;
|
|
384
393
|
}
|
|
385
394
|
|
|
386
395
|
interface TelegramGetFileResult {
|
|
@@ -434,6 +443,7 @@ export interface TelegramAnswerGuestQueryOptions {
|
|
|
434
443
|
export interface TelegramEditGuestInlineMessageContent {
|
|
435
444
|
text?: string;
|
|
436
445
|
richMessage?: TelegramInputRichMessage;
|
|
446
|
+
parseMode?: "HTML";
|
|
437
447
|
}
|
|
438
448
|
|
|
439
449
|
export interface TelegramAnswerCallbackQueryOptions {
|
|
@@ -1380,7 +1390,25 @@ async function callTelegramWithRetry<TResponse>(
|
|
|
1380
1390
|
isTelegramApiMethodRetrySafe(method));
|
|
1381
1391
|
const maxAttempts = Math.max(1, options?.maxAttempts ?? 3);
|
|
1382
1392
|
const retryBaseDelayMs = options?.retryBaseDelayMs ?? 500;
|
|
1383
|
-
const waitBeforeRetry = async (
|
|
1393
|
+
const waitBeforeRetry = async (
|
|
1394
|
+
error: unknown,
|
|
1395
|
+
attempt: number,
|
|
1396
|
+
): Promise<void> => {
|
|
1397
|
+
const ms = getTelegramRetryDelayMs(error, attempt, retryBaseDelayMs);
|
|
1398
|
+
if (
|
|
1399
|
+
!options?.signal?.aborted &&
|
|
1400
|
+
error instanceof TelegramApiHttpError &&
|
|
1401
|
+
error.status === 429
|
|
1402
|
+
) {
|
|
1403
|
+
options?.onRetryWait?.({
|
|
1404
|
+
method,
|
|
1405
|
+
delayMs: ms,
|
|
1406
|
+
attempt,
|
|
1407
|
+
...(error.retryAfterSeconds !== undefined
|
|
1408
|
+
? { retryAfterSeconds: error.retryAfterSeconds }
|
|
1409
|
+
: {}),
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1384
1412
|
if (options?.sleep) await options.sleep(ms);
|
|
1385
1413
|
else await sleepTelegramRetry(ms, options?.signal);
|
|
1386
1414
|
throwIfTelegramApiCallAborted(options?.signal);
|
|
@@ -1406,9 +1434,7 @@ async function callTelegramWithRetry<TResponse>(
|
|
|
1406
1434
|
if (!retrySafe) {
|
|
1407
1435
|
if (error instanceof TelegramApiHttpError && error.status === 429) {
|
|
1408
1436
|
if (attempt >= maxAttempts - 1) throw error;
|
|
1409
|
-
await waitBeforeRetry(
|
|
1410
|
-
getTelegramRetryDelayMs(error, attempt, retryBaseDelayMs),
|
|
1411
|
-
);
|
|
1437
|
+
await waitBeforeRetry(error, attempt);
|
|
1412
1438
|
continue;
|
|
1413
1439
|
}
|
|
1414
1440
|
if (
|
|
@@ -1423,9 +1449,7 @@ async function callTelegramWithRetry<TResponse>(
|
|
|
1423
1449
|
throw error;
|
|
1424
1450
|
}
|
|
1425
1451
|
if (attempt >= maxAttempts - 1 || !retryable) throw error;
|
|
1426
|
-
await waitBeforeRetry(
|
|
1427
|
-
getTelegramRetryDelayMs(error, attempt, retryBaseDelayMs),
|
|
1428
|
-
);
|
|
1452
|
+
await waitBeforeRetry(error, attempt);
|
|
1429
1453
|
}
|
|
1430
1454
|
}
|
|
1431
1455
|
}
|
|
@@ -2095,6 +2119,7 @@ export function createTelegramBridgeApiRuntime(
|
|
|
2095
2119
|
...(content.richMessage
|
|
2096
2120
|
? { rich_message: content.richMessage }
|
|
2097
2121
|
: { text: content.text }),
|
|
2122
|
+
...(content.parseMode ? { parse_mode: content.parseMode } : {}),
|
|
2098
2123
|
});
|
|
2099
2124
|
},
|
|
2100
2125
|
prepareTempDir: () =>
|
|
@@ -2117,6 +2142,7 @@ export function createTelegramApiClient(
|
|
|
2117
2142
|
options: TelegramAnswerCallbackQueryOptions & { now?: () => number } = {},
|
|
2118
2143
|
): TelegramApiClient {
|
|
2119
2144
|
const now = options.now ?? Date.now;
|
|
2145
|
+
const recordRuntimeEvent = options.recordRuntimeEvent;
|
|
2120
2146
|
const draftRetryNotBeforeByTarget = new Map<string, number>();
|
|
2121
2147
|
return {
|
|
2122
2148
|
call: async <TResponse>(
|
|
@@ -2140,9 +2166,31 @@ export function createTelegramApiClient(
|
|
|
2140
2166
|
}
|
|
2141
2167
|
try {
|
|
2142
2168
|
// A draft is a replaceable snapshot, not a body to replay after backoff.
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2169
|
+
const retryWaitOptions: TelegramApiCallOptions = recordRuntimeEvent
|
|
2170
|
+
? {
|
|
2171
|
+
onRetryWait: (wait) => {
|
|
2172
|
+
recordRuntimeEvent(
|
|
2173
|
+
"api",
|
|
2174
|
+
new Error(
|
|
2175
|
+
`Telegram API rate limit: waiting ${wait.delayMs} ms before retrying ${wait.method}`,
|
|
2176
|
+
),
|
|
2177
|
+
{
|
|
2178
|
+
phase: "retry-wait",
|
|
2179
|
+
method: wait.method,
|
|
2180
|
+
waitMs: wait.delayMs,
|
|
2181
|
+
attempt: wait.attempt,
|
|
2182
|
+
...(wait.retryAfterSeconds !== undefined
|
|
2183
|
+
? { retryAfterSeconds: wait.retryAfterSeconds }
|
|
2184
|
+
: {}),
|
|
2185
|
+
},
|
|
2186
|
+
);
|
|
2187
|
+
},
|
|
2188
|
+
}
|
|
2189
|
+
: {};
|
|
2190
|
+
return await callTelegram<TResponse>(token, method, body, {
|
|
2191
|
+
...(isDraft ? { ...options, maxAttempts: 1 } : options),
|
|
2192
|
+
...retryWaitOptions,
|
|
2193
|
+
});
|
|
2146
2194
|
} catch (error) {
|
|
2147
2195
|
if (draftKey && isRetryableTelegramApiError(error)) {
|
|
2148
2196
|
draftRetryNotBeforeByTarget.set(draftKey, Math.max(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-kit",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.6",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"@llblab/pi-codex-usage": "0.9.4",
|
|
46
46
|
"@llblab/pi-grow-loop": "0.8.1",
|
|
47
47
|
"@llblab/pi-state-flow": "0.9.6",
|
|
48
|
-
"@llblab/pi-telegram": "0.45.
|
|
48
|
+
"@llblab/pi-telegram": "0.45.6",
|
|
49
49
|
"@llblab/skills": "1.15.0"
|
|
50
50
|
},
|
|
51
51
|
"bundledDependencies": [
|