@llblab/pi-telegram 0.45.8 → 0.45.10
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 +10 -0
- package/api/sections.ts +2 -0
- package/docs/architecture.md +1 -1
- package/docs/public-api.md +1 -1
- package/docs/sections.md +9 -0
- package/docs/ui-style.md +5 -0
- package/lib/bindings.ts +6 -4
- package/lib/commands.ts +5 -14
- package/lib/extension.ts +9 -2
- package/lib/menu.ts +21 -0
- package/lib/queue.ts +87 -3
- package/lib/replies.ts +28 -0
- package/lib/routing.ts +9 -0
- package/lib/sections.ts +25 -0
- package/lib/telegram-api.ts +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.45.10: Rich headings and state-scope markers
|
|
8
|
+
|
|
9
|
+
- `Rich section headings`: Public Rich Message block types now include native headings, allowing companion sections to place a fixed title above sibling disclosures instead of wrapping the entire view in another collapsible block.
|
|
10
|
+
- `State-scope markers`: Registers 🌐 Global, 📂 CWD, 💬 Session, and 🧬 Effective as canonical state-scope semantics for chooser buttons and selected-scope headings, with composition flowing Global → CWD → Session → Effective.
|
|
11
|
+
|
|
12
|
+
## 0.45.9: Rich sections and exact queue continuation
|
|
13
|
+
|
|
14
|
+
- `Extension Sections`: Public section contexts now expose `openRich(message)` for one standalone Telegram Native Rich Message routed to the exact callback chat/thread with normal message-ownership recording and scoped callback diagnostics. Companion extensions can reuse native `details`/`pre` disclosure without raw bot access; `👁` is the canonical read-only inspection marker.
|
|
15
|
+
- `/next` ordering: Active interruption settles before continuation is announced. The notice is bound to the exact selected queued item and revalidates queue identity, readiness, admission, pending mutations, and transport authority before dispatch; removal, clear, reordering, concurrent append, or transport loss cannot transfer stale intent to unrelated work.
|
|
16
|
+
|
|
7
17
|
## 0.45.8: Follower Forwarding Hotfix
|
|
8
18
|
|
|
9
19
|
- `Follower Forwarding`: Message-ownership lookups now project the matching live registration's protocol identity before forwarding. Voice/message and edit retries, message-only callbacks, and reactions no longer stall on an incomplete cached owner after a rejection or re-registration. Exact generation, binding, protocol, and durable-receipt checks remain enforced.
|
package/api/sections.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Exposes the stable managed Telegram menu-section surface while keeping registry internals package-private
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
export type { TelegramInputRichBlock, TelegramInputRichMessage, TelegramRichText } from "../lib/telegram-api.ts";
|
|
8
|
+
|
|
7
9
|
export {
|
|
8
10
|
getTelegramSectionDiagnostics,
|
|
9
11
|
registerTelegramSection,
|
package/docs/architecture.md
CHANGED
|
@@ -507,7 +507,7 @@ Immediate controls:
|
|
|
507
507
|
- `/start` opens the main inline application menu.
|
|
508
508
|
- `/model`, `/thinking`, `/queue`, and `/settings` are hidden shortcuts to menu sections.
|
|
509
509
|
- `/compact` opens an inline confirmation dialog and then runs compaction when the bridge is idle.
|
|
510
|
-
- `/next` dispatches the next queued turn, aborting Pi first when needed.
|
|
510
|
+
- `/next` dispatches the next queued turn, aborting Pi first when needed. Active-turn settlement delivers its abort notice to the interrupted prompt before queue dispatch continues. The queue owner then emits `Dispatching next queued turn.` against the exact selected prompt's chat/thread/reply id before its one model dispatch; skipped, inactive, pending-mutation, and admission-blocked candidates never receive it. The `/next` command itself is never a lifecycle-notice reply target. Aborted pending assistant text is not projected as a second reply, while already completed intermediate output remains visible.
|
|
511
511
|
- `/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.
|
|
512
512
|
- `/stop` aborts and clears waiting Telegram queue items.
|
|
513
513
|
|
package/docs/public-api.md
CHANGED
|
@@ -52,7 +52,7 @@ Stable commands inside the paired Telegram DM:
|
|
|
52
52
|
|
|
53
53
|
- `/start` — pair when needed and open the main application menu.
|
|
54
54
|
- `/compact` — open confirmation and compact when idle.
|
|
55
|
-
- `/next` —
|
|
55
|
+
- `/next` — abort active work first when needed, let the interrupted prompt receive its abort notice, then reply `Dispatching next queued turn.` to the exact queued prompt selected for the next model turn. The command itself is never the lifecycle-notice reply target, and aborted pending assistant text is suppressed.
|
|
56
56
|
- `/continue` — enqueue a priority `continue` prompt.
|
|
57
57
|
- `/abort` — abort active work and keep the queue; abort-history is scoped to Telegram-owned active turns.
|
|
58
58
|
- `/stop` — abort active Telegram-owned work and clear waiting Telegram queue items.
|
package/docs/sections.md
CHANGED
|
@@ -262,6 +262,8 @@ interface TelegramSectionContext {
|
|
|
262
262
|
edit(view: TelegramSectionView): Promise<void>;
|
|
263
263
|
/** Send a standalone chat message without auto-navigation */
|
|
264
264
|
open(view: TelegramSectionView): Promise<void>;
|
|
265
|
+
/** Send one standalone Telegram Native Rich Message without exposing transport */
|
|
266
|
+
openRich(message: TelegramInputRichMessage): Promise<void>;
|
|
265
267
|
/** Enqueue a plain-text prompt turn */
|
|
266
268
|
enqueuePrompt(prompt: string): Promise<void>;
|
|
267
269
|
/** Build a section-namespaced callback_data string */
|
|
@@ -285,6 +287,7 @@ interface TelegramSectionCallbackContext {
|
|
|
285
287
|
answerCallback(text?: string): Promise<void>;
|
|
286
288
|
edit(view: TelegramSectionView): Promise<void>;
|
|
287
289
|
open(view: TelegramSectionView): Promise<void>;
|
|
290
|
+
openRich(message: TelegramInputRichMessage): Promise<void>;
|
|
288
291
|
enqueuePrompt(prompt: string): Promise<void>;
|
|
289
292
|
callbackData(action: string, payload?: string): string;
|
|
290
293
|
/** Delete the message that triggered this callback */
|
|
@@ -292,6 +295,12 @@ interface TelegramSectionCallbackContext {
|
|
|
292
295
|
}
|
|
293
296
|
```
|
|
294
297
|
|
|
298
|
+
### `openRich` semantics
|
|
299
|
+
|
|
300
|
+
`ctx.openRich(message)` sends exactly one standalone Telegram Native Rich Message to the callback's exact chat/thread target. It accepts the public `TelegramInputRichMessage` tree (`details` and `pre` blocks) used by native tool evidence, records message ownership, and routes through the active direct/follower transport. It adds no menu navigation and exposes no raw bot client or arbitrary API method.
|
|
301
|
+
|
|
302
|
+
Use it when disclosure behavior is part of the document, such as one selected read-only state or evidence tree. Do not imitate nested disclosure with HTML or Markdown when the native tree is required.
|
|
303
|
+
|
|
295
304
|
### `enqueuePrompt` semantics
|
|
296
305
|
|
|
297
306
|
Queues a `[telegram] <prompt>` turn in the default lane with the paired user's `chatId`. Uses `queueMutationRuntime.append()` and triggers `dispatchNextQueuedTelegramTurn()`. The prompt arrives as a normal Telegram-owned turn — the agent sees it as if the user typed it.
|
package/docs/ui-style.md
CHANGED
|
@@ -36,6 +36,11 @@ Use emoji as stable semantic markers, not decoration. Emoji carry transportable
|
|
|
36
36
|
| `🔬` | Activity / technical detail | Activity settings row and detail card | Chooses quiet, thinking, tools, or verbose bridge activity; not a generic diagnostics marker. |
|
|
37
37
|
| `🧠` | Model thinking controls | Thinking menus and status rows | Thinking activity quotes omit this icon and their header entirely to minimize chat height. |
|
|
38
38
|
| `📎` | Attachment | Attachment summaries, queue rows for attachment-only turns | Not for thread binding. |
|
|
39
|
+
| `👁` | Read-only inspection | State/detail viewers and inspection entrypoints | Opens evidence without mutating the inspected object; do not use for edit or refresh actions. |
|
|
40
|
+
| `🌐` | Global state scope | State-scope chooser and selected-scope heading | Cross-project memory composed first. |
|
|
41
|
+
| `📂` | Current-working-directory state scope | State-scope chooser and selected-scope heading | Project-local memory composed after Global. |
|
|
42
|
+
| `💬` | Session state scope | State-scope chooser and selected-scope heading | Conversation-branch memory composed after CWD. |
|
|
43
|
+
| `🧬` | Effective composed state | State-scope chooser and selected-scope heading | The resulting Global → CWD → Session projection. |
|
|
39
44
|
|
|
40
45
|
### Command And Control Actions
|
|
41
46
|
|
package/lib/bindings.ts
CHANGED
|
@@ -50,6 +50,7 @@ type TelegramAgentMessageRouter = NonNullable<
|
|
|
50
50
|
export interface TelegramQueueBindingRuntime<TContext> {
|
|
51
51
|
mutation: Queue.TelegramQueueMutationController<TContext>;
|
|
52
52
|
dispatchNext: (ctx: TContext) => void;
|
|
53
|
+
requestNextDispatchAnnouncement: () => void;
|
|
53
54
|
watchdog: Queue.TelegramQueueDispatchWatchdogRuntime<TContext>;
|
|
54
55
|
}
|
|
55
56
|
|
|
@@ -122,7 +123,7 @@ export function createTelegramQueueBindingRuntime<TContext>(deps: {
|
|
|
122
123
|
updateStatus: deps.updateStatus,
|
|
123
124
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
124
125
|
});
|
|
125
|
-
const
|
|
126
|
+
const dispatch = Queue.createTelegramQueueDispatchRuntime({
|
|
126
127
|
...deps.store,
|
|
127
128
|
isCompactionInProgress: deps.lifecycle.isCompactionInProgress,
|
|
128
129
|
hasActiveTurn: deps.activeTurn.has,
|
|
@@ -162,13 +163,14 @@ export function createTelegramQueueBindingRuntime<TContext>(deps: {
|
|
|
162
163
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
163
164
|
...deps.promptDispatch,
|
|
164
165
|
sendUserMessage: deps.sendUserMessage,
|
|
165
|
-
})
|
|
166
|
+
});
|
|
166
167
|
return {
|
|
167
168
|
mutation,
|
|
168
|
-
dispatchNext,
|
|
169
|
+
dispatchNext: dispatch.dispatchNext,
|
|
170
|
+
requestNextDispatchAnnouncement: dispatch.requestNextDispatchAnnouncement,
|
|
169
171
|
watchdog: Queue.createTelegramQueueDispatchWatchdogRuntime({
|
|
170
172
|
hasQueuedItems: deps.store.hasQueuedItems,
|
|
171
|
-
dispatchNextQueuedTelegramTurn: dispatchNext,
|
|
173
|
+
dispatchNextQueuedTelegramTurn: dispatch.dispatchNext,
|
|
172
174
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
173
175
|
}),
|
|
174
176
|
};
|
package/lib/commands.ts
CHANGED
|
@@ -1147,6 +1147,7 @@ export interface TelegramCommandRuntimeDeps<
|
|
|
1147
1147
|
updateStatus: (ctx: TContext) => void;
|
|
1148
1148
|
isContextActive?: (ctx: TContext) => boolean;
|
|
1149
1149
|
dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
|
|
1150
|
+
requestNextDispatchAnnouncement?: () => void;
|
|
1150
1151
|
requestDeferredDispatchNextQueuedTelegramTurn?: (
|
|
1151
1152
|
dispatch: (ctx: TContext) => void,
|
|
1152
1153
|
) => void;
|
|
@@ -1416,6 +1417,7 @@ export async function handleTelegramNextCommand(deps: {
|
|
|
1416
1417
|
clearPendingModelSwitch: () => void;
|
|
1417
1418
|
abortCurrentTurn: () => void;
|
|
1418
1419
|
dispatchNextQueuedTurn: () => void;
|
|
1420
|
+
requestNextDispatchAnnouncement?: () => void;
|
|
1419
1421
|
clearFoldForDispatch: () => void;
|
|
1420
1422
|
updateStatus: () => void;
|
|
1421
1423
|
sendTextReply: (
|
|
@@ -1435,19 +1437,10 @@ export async function handleTelegramNextCommand(deps: {
|
|
|
1435
1437
|
return;
|
|
1436
1438
|
}
|
|
1437
1439
|
if (!deps.isIdle() && deps.hasAbortHandler()) {
|
|
1438
|
-
const activeTurnReply = deps.getActiveTurnReply?.();
|
|
1439
1440
|
deps.clearFoldForDispatch();
|
|
1441
|
+
deps.requestNextDispatchAnnouncement?.();
|
|
1440
1442
|
deps.abortCurrentTurn();
|
|
1441
1443
|
deps.updateStatus();
|
|
1442
|
-
const notice = formatTelegramInformationHeading(
|
|
1443
|
-
"⏩",
|
|
1444
|
-
"Dispatching next queued turn.",
|
|
1445
|
-
);
|
|
1446
|
-
if (activeTurnReply) {
|
|
1447
|
-
await activeTurnReply(notice, { parseMode: "HTML" });
|
|
1448
|
-
} else {
|
|
1449
|
-
await deps.sendTextReply(notice, { parseMode: "HTML" });
|
|
1450
|
-
}
|
|
1451
1444
|
return;
|
|
1452
1445
|
}
|
|
1453
1446
|
if (!deps.isIdle()) {
|
|
@@ -1460,12 +1453,9 @@ export async function handleTelegramNextCommand(deps: {
|
|
|
1460
1453
|
);
|
|
1461
1454
|
return;
|
|
1462
1455
|
}
|
|
1456
|
+
deps.requestNextDispatchAnnouncement?.();
|
|
1463
1457
|
deps.dispatchNextQueuedTurn();
|
|
1464
1458
|
deps.updateStatus();
|
|
1465
|
-
await deps.sendTextReply(
|
|
1466
|
-
formatTelegramInformationHeading("▶️", "Dispatching next queued turn."),
|
|
1467
|
-
{ parseMode: "HTML" },
|
|
1468
|
-
);
|
|
1469
1459
|
}
|
|
1470
1460
|
|
|
1471
1461
|
export async function handleTelegramContinueCommand<TMessage, TContext>(
|
|
@@ -2029,6 +2019,7 @@ async function handleTelegramCommandRuntime<
|
|
|
2029
2019
|
abortCurrentTurn: deps.abortCurrentTurn,
|
|
2030
2020
|
dispatchNextQueuedTurn: () =>
|
|
2031
2021
|
deps.dispatchNextQueuedTelegramTurn(commandCtx),
|
|
2022
|
+
requestNextDispatchAnnouncement: deps.requestNextDispatchAnnouncement,
|
|
2032
2023
|
clearFoldForDispatch: () =>
|
|
2033
2024
|
deps.setFoldQueuedPromptsIntoHistory(false),
|
|
2034
2025
|
updateStatus: updateStatusFor(commandCtx),
|
package/lib/extension.ts
CHANGED
|
@@ -629,8 +629,12 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
629
629
|
getAssistantRenderingMode: configControls.getAssistantRenderingMode,
|
|
630
630
|
editMessage: editTelegramMessageText,
|
|
631
631
|
});
|
|
632
|
-
const {
|
|
633
|
-
|
|
632
|
+
const {
|
|
633
|
+
replyTransport,
|
|
634
|
+
editInteractiveMessage,
|
|
635
|
+
sendInteractiveMessage,
|
|
636
|
+
sendSectionRichMessage,
|
|
637
|
+
} = replyRuntime;
|
|
634
638
|
const deliveryTargetPolicyRuntime =
|
|
635
639
|
Delivery.createTelegramDeliveryTargetPolicyRuntime({
|
|
636
640
|
ownsDirect: lockRuntime.owns,
|
|
@@ -744,6 +748,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
744
748
|
const {
|
|
745
749
|
mutation: queueMutationRuntime,
|
|
746
750
|
dispatchNext: dispatchNextQueuedTelegramTurn,
|
|
751
|
+
requestNextDispatchAnnouncement,
|
|
747
752
|
watchdog: queueDispatchWatchdogRuntime,
|
|
748
753
|
} = Bindings.createTelegramQueueBindingRuntime({
|
|
749
754
|
store: telegramQueueStore,
|
|
@@ -964,6 +969,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
964
969
|
openSettingsMenu: settingsMenuRuntime.openSettingsMenu,
|
|
965
970
|
settingsMenuCallbackHandler: settingsMenuRuntime.handleCallbackQuery,
|
|
966
971
|
sectionRegistry,
|
|
972
|
+
sendSectionRichMessage,
|
|
967
973
|
buttonActionStore,
|
|
968
974
|
invokeBoundButtonAction: invokeGenerativeAppBoundButtonAction,
|
|
969
975
|
inboundHandlerRuntime,
|
|
@@ -972,6 +978,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
972
978
|
updateStatus,
|
|
973
979
|
isContextActive: telegramSessionContextStore.isCurrent,
|
|
974
980
|
dispatchNextQueuedTelegramTurn,
|
|
981
|
+
requestNextDispatchAnnouncement,
|
|
975
982
|
requestDeferredDispatchNextQueuedTelegramTurn:
|
|
976
983
|
deferredQueueDispatchRuntime.request,
|
|
977
984
|
hasDeferredDispatchContext: deferredQueueDispatchRuntime.isBound,
|
package/lib/menu.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
type ScopedTelegramModel,
|
|
33
33
|
type ThinkingLevel,
|
|
34
34
|
} from "./model.ts";
|
|
35
|
+
import type { TelegramInputRichMessage } from "./telegram-api.ts";
|
|
35
36
|
import {
|
|
36
37
|
handleTelegramSectionCallback,
|
|
37
38
|
handleTelegramSectionOpen,
|
|
@@ -225,6 +226,11 @@ export interface TelegramMenuCallbackRuntimeDeps<
|
|
|
225
226
|
replyMarkup: TelegramReplyMarkup,
|
|
226
227
|
options?: { target?: { chatId: number; threadId?: number } },
|
|
227
228
|
) => Promise<number | undefined>;
|
|
229
|
+
sendSectionRichMessage?: (
|
|
230
|
+
chatId: number,
|
|
231
|
+
message: TelegramInputRichMessage,
|
|
232
|
+
options?: { target?: { chatId: number; threadId?: number } },
|
|
233
|
+
) => Promise<number | undefined>;
|
|
228
234
|
enqueueSectionPrompt?: (
|
|
229
235
|
prompt: string,
|
|
230
236
|
ctx: TContext,
|
|
@@ -478,6 +484,11 @@ export interface TelegramMenuCallbackRuntimeAdapterDeps<
|
|
|
478
484
|
replyMarkup: TelegramReplyMarkup,
|
|
479
485
|
options?: { target?: { chatId: number; threadId?: number } },
|
|
480
486
|
) => Promise<number | undefined>;
|
|
487
|
+
sendSectionRichMessage?: (
|
|
488
|
+
chatId: number,
|
|
489
|
+
message: TelegramInputRichMessage,
|
|
490
|
+
options?: { target?: { chatId: number; threadId?: number } },
|
|
491
|
+
) => Promise<number | undefined>;
|
|
481
492
|
enqueueSectionPrompt?: (
|
|
482
493
|
prompt: string,
|
|
483
494
|
ctx: TContext,
|
|
@@ -527,6 +538,7 @@ export function createTelegramMenuCallbackHandlerForContext<
|
|
|
527
538
|
sectionRegistry: deps.sectionRegistry,
|
|
528
539
|
editInteractiveMessage: deps.editInteractiveMessage,
|
|
529
540
|
sendInteractiveMessage: deps.sendInteractiveMessage,
|
|
541
|
+
sendSectionRichMessage: deps.sendSectionRichMessage,
|
|
530
542
|
enqueueSectionPrompt: deps.enqueueSectionPrompt,
|
|
531
543
|
deleteMessage: deps.deleteMessage,
|
|
532
544
|
});
|
|
@@ -591,6 +603,9 @@ export async function handleTelegramMenuCallbackRuntime<
|
|
|
591
603
|
deps.editInteractiveMessage ?? (async () => {}),
|
|
592
604
|
sendInteractiveMessage:
|
|
593
605
|
deps.sendInteractiveMessage ?? (async () => undefined),
|
|
606
|
+
sendRichMessage: deps.sendSectionRichMessage ?? (async () => {
|
|
607
|
+
throw new Error("Rich Message delivery is unavailable");
|
|
608
|
+
}),
|
|
594
609
|
enqueuePrompt: deps.enqueueSectionPrompt
|
|
595
610
|
? (prompt: string) =>
|
|
596
611
|
deps.enqueueSectionPrompt!(prompt, ctx, target, query)
|
|
@@ -614,6 +629,9 @@ export async function handleTelegramMenuCallbackRuntime<
|
|
|
614
629
|
deps.editInteractiveMessage ?? (async () => {}),
|
|
615
630
|
sendInteractiveMessage:
|
|
616
631
|
deps.sendInteractiveMessage ?? (async () => undefined),
|
|
632
|
+
sendRichMessage: deps.sendSectionRichMessage ?? (async () => {
|
|
633
|
+
throw new Error("Rich Message delivery is unavailable");
|
|
634
|
+
}),
|
|
617
635
|
enqueuePrompt: deps.enqueueSectionPrompt
|
|
618
636
|
? (prompt: string) =>
|
|
619
637
|
deps.enqueueSectionPrompt!(prompt, ctx, target, query)
|
|
@@ -639,6 +657,9 @@ export async function handleTelegramMenuCallbackRuntime<
|
|
|
639
657
|
deps.editInteractiveMessage ?? (async () => {}),
|
|
640
658
|
sendInteractiveMessage:
|
|
641
659
|
deps.sendInteractiveMessage ?? (async () => undefined),
|
|
660
|
+
sendRichMessage: deps.sendSectionRichMessage ?? (async () => {
|
|
661
|
+
throw new Error("Rich Message delivery is unavailable");
|
|
662
|
+
}),
|
|
642
663
|
enqueuePrompt: deps.enqueueSectionPrompt
|
|
643
664
|
? (prompt: string) =>
|
|
644
665
|
deps.enqueueSectionPrompt!(prompt, ctx, target, query)
|
package/lib/queue.ts
CHANGED
|
@@ -3055,6 +3055,7 @@ export interface TelegramQueueDispatchControllerDeps<
|
|
|
3055
3055
|
|
|
3056
3056
|
export interface TelegramQueueDispatchController<TContext = unknown> {
|
|
3057
3057
|
dispatchNext: (ctx: TContext) => void;
|
|
3058
|
+
requestNextDispatchAnnouncement: () => void;
|
|
3058
3059
|
}
|
|
3059
3060
|
|
|
3060
3061
|
export function executeTelegramQueueDispatchPlan<TContext = unknown>(
|
|
@@ -3123,7 +3124,13 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
|
|
|
3123
3124
|
deps: TelegramQueueDispatchControllerDeps<TContext>,
|
|
3124
3125
|
): TelegramQueueDispatchController<TContext> {
|
|
3125
3126
|
let controlDispatchPending = false;
|
|
3127
|
+
let nextDispatchAnnouncementRequested = false;
|
|
3128
|
+
let nextDispatchAnnouncementAnchor: TelegramQueueItem<TContext> | undefined;
|
|
3126
3129
|
const controller: TelegramQueueDispatchController<TContext> = {
|
|
3130
|
+
requestNextDispatchAnnouncement: () => {
|
|
3131
|
+
nextDispatchAnnouncementRequested = true;
|
|
3132
|
+
nextDispatchAnnouncementAnchor = undefined;
|
|
3133
|
+
},
|
|
3127
3134
|
dispatchNext: (ctx) => {
|
|
3128
3135
|
if (deps.hasDispatchContext && !deps.hasDispatchContext()) return;
|
|
3129
3136
|
if (controlDispatchPending) {
|
|
@@ -3151,6 +3158,10 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
|
|
|
3151
3158
|
}
|
|
3152
3159
|
if (droppedInactiveItemCount > 0) {
|
|
3153
3160
|
deps.setQueuedItems(retainedItems);
|
|
3161
|
+
if (retainedItems.length === 0) {
|
|
3162
|
+
nextDispatchAnnouncementRequested = false;
|
|
3163
|
+
nextDispatchAnnouncementAnchor = undefined;
|
|
3164
|
+
}
|
|
3154
3165
|
deps.recordRuntimeEvent?.(
|
|
3155
3166
|
"dispatch",
|
|
3156
3167
|
new Error(
|
|
@@ -3209,6 +3220,21 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
|
|
|
3209
3220
|
}
|
|
3210
3221
|
const dispatchableItems = activeItems.slice(nextActiveIndex);
|
|
3211
3222
|
const nextItem = dispatchableItems[0];
|
|
3223
|
+
if (nextDispatchAnnouncementRequested) {
|
|
3224
|
+
if (nextDispatchAnnouncementAnchor) {
|
|
3225
|
+
const anchorRetained = retainedItems.includes(nextDispatchAnnouncementAnchor);
|
|
3226
|
+
const anchorTransportActive =
|
|
3227
|
+
deps.isQueueItemTransportActive?.(nextDispatchAnnouncementAnchor) !== false;
|
|
3228
|
+
if (!anchorRetained || !anchorTransportActive) {
|
|
3229
|
+
nextDispatchAnnouncementRequested = false;
|
|
3230
|
+
nextDispatchAnnouncementAnchor = undefined;
|
|
3231
|
+
}
|
|
3232
|
+
} else if (nextItem) {
|
|
3233
|
+
nextDispatchAnnouncementAnchor = nextItem;
|
|
3234
|
+
} else if (retainedItems.length === 0) {
|
|
3235
|
+
nextDispatchAnnouncementRequested = false;
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3212
3238
|
if (
|
|
3213
3239
|
nextItem &&
|
|
3214
3240
|
deps.hasPendingInboundQueueMutationForItem?.(nextItem)
|
|
@@ -3224,17 +3250,49 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
|
|
|
3224
3250
|
deps.updateStatus(ctx);
|
|
3225
3251
|
return;
|
|
3226
3252
|
}
|
|
3253
|
+
const dispatchBasisItems = deps.getQueuedItems();
|
|
3227
3254
|
const dispatchPlan = planNextTelegramQueueAction(
|
|
3228
3255
|
dispatchableItems,
|
|
3229
3256
|
canDispatch,
|
|
3230
3257
|
);
|
|
3231
|
-
|
|
3258
|
+
const commitDispatchPlan = (): boolean => {
|
|
3259
|
+
if (dispatchPlan.kind === "none") return true;
|
|
3260
|
+
const currentItems = deps.getQueuedItems();
|
|
3261
|
+
if (dispatchPlan.kind === "prompt") {
|
|
3262
|
+
const queueDrifted =
|
|
3263
|
+
currentItems.length !== dispatchBasisItems.length ||
|
|
3264
|
+
currentItems.some((item, index) => item !== dispatchBasisItems[index]);
|
|
3265
|
+
const dispatchEligibilityDrifted =
|
|
3266
|
+
!deps.canDispatch(ctx) ||
|
|
3267
|
+
deps.hasPendingInboundQueueMutationForItem?.(dispatchPlan.item) === true ||
|
|
3268
|
+
(deps.isQueueItemAdmissionReady?.(dispatchPlan.item) === false) ||
|
|
3269
|
+
(deps.isQueueItemTransportActive?.(dispatchPlan.item) === false);
|
|
3270
|
+
if (queueDrifted || dispatchEligibilityDrifted) {
|
|
3271
|
+
const selectedItemRetained = currentItems.includes(dispatchPlan.item);
|
|
3272
|
+
const selectedTransportActive =
|
|
3273
|
+
deps.isQueueItemTransportActive?.(dispatchPlan.item) !== false;
|
|
3274
|
+
nextDispatchAnnouncementRequested =
|
|
3275
|
+
selectedItemRetained && selectedTransportActive;
|
|
3276
|
+
nextDispatchAnnouncementAnchor = nextDispatchAnnouncementRequested
|
|
3277
|
+
? dispatchPlan.item
|
|
3278
|
+
: undefined;
|
|
3279
|
+
deps.updateStatus(ctx);
|
|
3280
|
+
if (nextDispatchAnnouncementRequested && queueDrifted && !dispatchEligibilityDrifted) {
|
|
3281
|
+
controller.dispatchNext(ctx);
|
|
3282
|
+
}
|
|
3283
|
+
return false;
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3232
3286
|
deps.setQueuedItems([
|
|
3233
3287
|
...dispatchPlan.remainingItems,
|
|
3234
3288
|
...protectedInactiveItems,
|
|
3235
3289
|
]);
|
|
3236
|
-
|
|
3237
|
-
|
|
3290
|
+
nextDispatchAnnouncementAnchor = undefined;
|
|
3291
|
+
return true;
|
|
3292
|
+
};
|
|
3293
|
+
const executePlan = (): void => {
|
|
3294
|
+
if (!commitDispatchPlan()) return;
|
|
3295
|
+
executeTelegramQueueDispatchPlan(dispatchPlan, {
|
|
3238
3296
|
executeControlItem: (item) => {
|
|
3239
3297
|
controlDispatchPending = true;
|
|
3240
3298
|
const dispatchGeneration = deps.getDispatchGeneration?.();
|
|
@@ -3280,6 +3338,32 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
|
|
|
3280
3338
|
deps.updateStatus(ctx);
|
|
3281
3339
|
},
|
|
3282
3340
|
});
|
|
3341
|
+
};
|
|
3342
|
+
if (dispatchPlan.kind === "prompt" && nextDispatchAnnouncementRequested) {
|
|
3343
|
+
nextDispatchAnnouncementRequested = false;
|
|
3344
|
+
controlDispatchPending = true;
|
|
3345
|
+
const dispatchGeneration = deps.getDispatchGeneration?.();
|
|
3346
|
+
deps.updateStatus(ctx);
|
|
3347
|
+
void deps.sendTextReply(
|
|
3348
|
+
dispatchPlan.item.chatId,
|
|
3349
|
+
dispatchPlan.item.replyToMessageId,
|
|
3350
|
+
"<b>⏩ Dispatching next queued turn.</b>",
|
|
3351
|
+
{ target: dispatchPlan.item.target },
|
|
3352
|
+
).catch((error) => {
|
|
3353
|
+
deps.recordRuntimeEvent?.("dispatch", error, { phase: "next-announcement" });
|
|
3354
|
+
}).finally(() => {
|
|
3355
|
+
controlDispatchPending = false;
|
|
3356
|
+
if (deps.hasDispatchContext && !deps.hasDispatchContext()) return;
|
|
3357
|
+
if (
|
|
3358
|
+
dispatchGeneration !== undefined &&
|
|
3359
|
+
deps.isDispatchGenerationActive &&
|
|
3360
|
+
!deps.isDispatchGenerationActive(dispatchGeneration)
|
|
3361
|
+
) return;
|
|
3362
|
+
executePlan();
|
|
3363
|
+
});
|
|
3364
|
+
return;
|
|
3365
|
+
}
|
|
3366
|
+
executePlan();
|
|
3283
3367
|
},
|
|
3284
3368
|
};
|
|
3285
3369
|
return controller;
|
package/lib/replies.ts
CHANGED
|
@@ -730,6 +730,24 @@ export async function sendTelegramNativeMarkdownReply<TReplyMarkup = unknown>(
|
|
|
730
730
|
return lastMessageId;
|
|
731
731
|
}
|
|
732
732
|
|
|
733
|
+
export async function sendTelegramNativeRichMessage(
|
|
734
|
+
chatId: number,
|
|
735
|
+
richMessage: TelegramInputRichMessage,
|
|
736
|
+
deps: {
|
|
737
|
+
recordOwnership?: TelegramReplyOwnershipRecorder["record"];
|
|
738
|
+
sendRichMessage: (body: TelegramSendRichMessageBody) => Promise<TelegramSentMessage>;
|
|
739
|
+
},
|
|
740
|
+
options?: TelegramReplyTargetOptions,
|
|
741
|
+
): Promise<number> {
|
|
742
|
+
const sent = await deps.sendRichMessage({
|
|
743
|
+
chat_id: chatId,
|
|
744
|
+
rich_message: richMessage,
|
|
745
|
+
...(options?.target ? getTelegramTargetThreadParams(options.target) : {}),
|
|
746
|
+
});
|
|
747
|
+
deps.recordOwnership?.({ chatId, messageId: sent.message_id, target: options?.target });
|
|
748
|
+
return sent.message_id;
|
|
749
|
+
}
|
|
750
|
+
|
|
733
751
|
// UI/compat regular-message runtime for bridge-owned text and interactive
|
|
734
752
|
// surfaces. Assistant and guest Markdown delivery bypass this path and use
|
|
735
753
|
// native Rich Message helpers above.
|
|
@@ -775,6 +793,11 @@ export interface TelegramRenderedMessageRuntime<TReplyMarkup> {
|
|
|
775
793
|
replyMarkup: TReplyMarkup,
|
|
776
794
|
options?: TelegramReplyTargetOptions,
|
|
777
795
|
) => Promise<number | undefined>;
|
|
796
|
+
sendSectionRichMessage: (
|
|
797
|
+
chatId: number,
|
|
798
|
+
message: TelegramInputRichMessage,
|
|
799
|
+
options?: TelegramReplyTargetOptions,
|
|
800
|
+
) => Promise<number>;
|
|
778
801
|
}
|
|
779
802
|
|
|
780
803
|
export interface TelegramRenderedMessageDeliveryRuntime<
|
|
@@ -891,6 +914,11 @@ export function createTelegramRenderedMessageRuntime<TReplyMarkup>(
|
|
|
891
914
|
},
|
|
892
915
|
);
|
|
893
916
|
},
|
|
917
|
+
sendSectionRichMessage: (chatId, message, options) =>
|
|
918
|
+
sendTelegramNativeRichMessage(chatId, message, {
|
|
919
|
+
recordOwnership: deps.recordOwnership,
|
|
920
|
+
sendRichMessage: deps.sendRichMessage,
|
|
921
|
+
}, options),
|
|
894
922
|
};
|
|
895
923
|
}
|
|
896
924
|
|
package/lib/routing.ts
CHANGED
|
@@ -19,6 +19,7 @@ import * as Queue from "./queue.ts";
|
|
|
19
19
|
import * as Replies from "./replies.ts";
|
|
20
20
|
import type { TelegramBridgeRuntime } from "./runtime.ts";
|
|
21
21
|
import type { TelegramSectionRegistry } from "./sections.ts";
|
|
22
|
+
import type { TelegramInputRichMessage } from "./telegram-api.ts";
|
|
22
23
|
import * as TextGroups from "./text-groups.ts";
|
|
23
24
|
import * as ThreadNaming from "./thread-naming.ts";
|
|
24
25
|
import * as ThreadReconciler from "./thread-reconciler.ts";
|
|
@@ -625,6 +626,7 @@ export interface TelegramInboundRouteRuntimeDeps<
|
|
|
625
626
|
updateStatus: (ctx: TContext, error?: string) => void;
|
|
626
627
|
isContextActive?: (ctx: TContext) => boolean;
|
|
627
628
|
dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
|
|
629
|
+
requestNextDispatchAnnouncement?: () => void;
|
|
628
630
|
requestDeferredDispatchNextQueuedTelegramTurn?: (
|
|
629
631
|
dispatch: (ctx: TContext) => void,
|
|
630
632
|
) => void;
|
|
@@ -706,6 +708,11 @@ export interface TelegramInboundRouteRuntimeDeps<
|
|
|
706
708
|
details?: Record<string, unknown>,
|
|
707
709
|
) => void;
|
|
708
710
|
sectionRegistry?: TelegramSectionRegistry;
|
|
711
|
+
sendSectionRichMessage?: (
|
|
712
|
+
chatId: number,
|
|
713
|
+
message: TelegramInputRichMessage,
|
|
714
|
+
options?: { target?: { chatId: number; threadId?: number } },
|
|
715
|
+
) => Promise<number | undefined>;
|
|
709
716
|
}
|
|
710
717
|
|
|
711
718
|
const TELEGRAM_OWNED_CALLBACK_PREFIXES = [
|
|
@@ -1013,6 +1020,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1013
1020
|
sectionRegistry: deps.sectionRegistry,
|
|
1014
1021
|
editInteractiveMessage: deps.editInteractiveMessage,
|
|
1015
1022
|
sendInteractiveMessage: deps.sendInteractiveMessage,
|
|
1023
|
+
sendSectionRichMessage: deps.sendSectionRichMessage,
|
|
1016
1024
|
deleteMessage: deps.deleteMessage,
|
|
1017
1025
|
enqueueSectionPrompt: async (
|
|
1018
1026
|
prompt: string,
|
|
@@ -2171,6 +2179,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2171
2179
|
updateStatus: deps.updateStatus,
|
|
2172
2180
|
isContextActive: deps.isContextActive,
|
|
2173
2181
|
dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
|
|
2182
|
+
requestNextDispatchAnnouncement: deps.requestNextDispatchAnnouncement,
|
|
2174
2183
|
requestDeferredDispatchNextQueuedTelegramTurn:
|
|
2175
2184
|
deps.requestDeferredDispatchNextQueuedTelegramTurn,
|
|
2176
2185
|
startTypingLoop: deps.startTypingLoop,
|
package/lib/sections.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
assertTelegramCallbackData,
|
|
9
9
|
type TelegramInlineKeyboardMarkup,
|
|
10
10
|
} from "./keyboard.ts";
|
|
11
|
+
import type { TelegramInputRichMessage } from "./telegram-api.ts";
|
|
11
12
|
|
|
12
13
|
const SECTION_REGISTRY_KEY = "__piTelegramSectionRegistry__";
|
|
13
14
|
|
|
@@ -66,6 +67,7 @@ export interface TelegramSectionContext {
|
|
|
66
67
|
answerCallback(text?: string): Promise<void>;
|
|
67
68
|
edit(view: TelegramSectionView): Promise<void>;
|
|
68
69
|
open(view: TelegramSectionView): Promise<void>;
|
|
70
|
+
openRich(message: TelegramInputRichMessage): Promise<void>;
|
|
69
71
|
enqueuePrompt(prompt: string): Promise<void>;
|
|
70
72
|
callbackData(action: string, payload?: string): string;
|
|
71
73
|
/** Delete the message that triggered this callback (dialog cleanup) */
|
|
@@ -81,6 +83,7 @@ export interface TelegramSectionCallbackContext {
|
|
|
81
83
|
answerCallback(text?: string): Promise<void>;
|
|
82
84
|
edit(view: TelegramSectionView): Promise<void>;
|
|
83
85
|
open(view: TelegramSectionView): Promise<void>;
|
|
86
|
+
openRich(message: TelegramInputRichMessage): Promise<void>;
|
|
84
87
|
enqueuePrompt(prompt: string): Promise<void>;
|
|
85
88
|
callbackData(action: string, payload?: string): string;
|
|
86
89
|
/** Delete the message that triggered this callback (dialog cleanup) */
|
|
@@ -160,6 +163,11 @@ export interface TelegramSectionRuntimeDeps {
|
|
|
160
163
|
replyMarkup: TelegramInlineKeyboardMarkup,
|
|
161
164
|
options?: { target?: TelegramSectionTarget },
|
|
162
165
|
) => Promise<number | undefined>;
|
|
166
|
+
sendRichMessage: (
|
|
167
|
+
chatId: number,
|
|
168
|
+
message: TelegramInputRichMessage,
|
|
169
|
+
options?: { target?: TelegramSectionTarget },
|
|
170
|
+
) => Promise<number | undefined>;
|
|
163
171
|
enqueuePrompt: (prompt: string) => Promise<void>;
|
|
164
172
|
deleteMessage: (chatId: number, messageId: number) => Promise<void>;
|
|
165
173
|
}
|
|
@@ -202,6 +210,12 @@ function buildTelegramSectionContext(
|
|
|
202
210
|
deps.target ? { target: deps.target } : undefined,
|
|
203
211
|
)
|
|
204
212
|
.then(() => {}),
|
|
213
|
+
openRich: (message) =>
|
|
214
|
+
deps.sendRichMessage(
|
|
215
|
+
chatId,
|
|
216
|
+
message,
|
|
217
|
+
deps.target ? { target: deps.target } : undefined,
|
|
218
|
+
).then(() => {}),
|
|
205
219
|
enqueuePrompt: deps.enqueuePrompt,
|
|
206
220
|
callbackData: (action, payload) =>
|
|
207
221
|
buildTelegramSectionCallbackData(token, action, payload),
|
|
@@ -251,6 +265,12 @@ function buildTelegramSectionCallbackContext(
|
|
|
251
265
|
deps.target ? { target: deps.target } : undefined,
|
|
252
266
|
)
|
|
253
267
|
.then(() => {}),
|
|
268
|
+
openRich: (message) =>
|
|
269
|
+
deps.sendRichMessage(
|
|
270
|
+
chatId,
|
|
271
|
+
message,
|
|
272
|
+
deps.target ? { target: deps.target } : undefined,
|
|
273
|
+
).then(() => {}),
|
|
254
274
|
enqueuePrompt: deps.enqueuePrompt,
|
|
255
275
|
callbackData: (action, payload) =>
|
|
256
276
|
buildTelegramSectionCallbackData(token, action, payload),
|
|
@@ -526,6 +546,11 @@ export interface TelegramSectionCallbackHandlerDeps {
|
|
|
526
546
|
replyMarkup: TelegramInlineKeyboardMarkup,
|
|
527
547
|
options?: { target?: TelegramSectionTarget },
|
|
528
548
|
) => Promise<number | undefined>;
|
|
549
|
+
sendRichMessage: (
|
|
550
|
+
chatId: number,
|
|
551
|
+
message: TelegramInputRichMessage,
|
|
552
|
+
options?: { target?: TelegramSectionTarget },
|
|
553
|
+
) => Promise<number | undefined>;
|
|
529
554
|
enqueuePrompt: (prompt: string) => Promise<void>;
|
|
530
555
|
deleteMessage: (chatId: number, messageId: number) => Promise<void>;
|
|
531
556
|
}
|
package/lib/telegram-api.ts
CHANGED
|
@@ -292,6 +292,7 @@ export type TelegramRichText =
|
|
|
292
292
|
| { type: "bold" | "code"; text: TelegramRichText };
|
|
293
293
|
|
|
294
294
|
export type TelegramInputRichBlock =
|
|
295
|
+
| { type: "heading"; text: TelegramRichText; size?: 1 | 2 | 3 }
|
|
295
296
|
| { type: "pre"; text: TelegramRichText; language?: string }
|
|
296
297
|
| {
|
|
297
298
|
type: "details";
|