@llblab/pi-kit 0.7.0 → 0.8.0
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 +8 -0
- package/README.md +2 -2
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +21 -21
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +3 -122
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +11 -0
- package/node_modules/@llblab/pi-state-flow/README.md +41 -35
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +36 -16
- package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +4 -4
- package/node_modules/@llblab/pi-state-flow/index.ts +23 -1
- package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +3 -0
- package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +191 -29
- package/node_modules/@llblab/pi-state-flow/lib/config.ts +6 -1
- package/node_modules/@llblab/pi-state-flow/lib/context.ts +42 -7
- package/node_modules/@llblab/pi-state-flow/lib/durable.ts +38 -8
- package/node_modules/@llblab/pi-state-flow/lib/episode.ts +1 -13
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +290 -134
- package/node_modules/@llblab/pi-state-flow/lib/git.ts +32 -16
- package/node_modules/@llblab/pi-state-flow/lib/logging.ts +41 -0
- package/node_modules/@llblab/pi-state-flow/lib/maintenance.ts +12 -6
- package/node_modules/@llblab/pi-state-flow/lib/migration.ts +16 -0
- package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +3 -0
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +167 -31
- package/node_modules/@llblab/pi-state-flow/lib/skills.ts +15 -6
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +40 -34
- package/node_modules/@llblab/pi-state-flow/lib/state.ts +6 -0
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +4 -9
- package/node_modules/@llblab/pi-state-flow/lib/storage.ts +25 -5
- package/node_modules/@llblab/pi-state-flow/lib/terminal.ts +17 -147
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +49 -27
- package/node_modules/@llblab/pi-state-flow/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/AGENTS.md +2 -2
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +3 -2
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +6 -1
- package/node_modules/@llblab/pi-telegram/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +8 -6
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +4 -4
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +18 -0
- package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +32 -19
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +5 -0
- package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +189 -15
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +3 -0
- package/node_modules/@llblab/pi-telegram/lib/config.ts +67 -4
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +65 -6
- package/node_modules/@llblab/pi-telegram/lib/locks.ts +6 -1
- package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +49 -3
- package/node_modules/@llblab/pi-telegram/lib/preview.ts +17 -0
- package/node_modules/@llblab/pi-telegram/lib/prompts.ts +1 -0
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +66 -8
- package/node_modules/@llblab/pi-telegram/lib/rendering.ts +4 -1
- package/node_modules/@llblab/pi-telegram/lib/replies.ts +19 -0
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +39 -0
- package/node_modules/@llblab/pi-telegram/lib/setup.ts +44 -4
- package/node_modules/@llblab/pi-telegram/lib/status.ts +41 -4
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +74 -18
- package/node_modules/@llblab/pi-telegram/lib/turns.ts +7 -0
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/package.json +3 -3
- package/node_modules/@llblab/pi-state-flow/lib/validation.ts +0 -27
- /package/node_modules/@llblab/pi-telegram/lib/{logs.ts → logging.ts} +0 -0
|
@@ -13,6 +13,10 @@ import type {
|
|
|
13
13
|
TelegramBusAgentMessage,
|
|
14
14
|
TelegramBusAgentTargetSelector,
|
|
15
15
|
} from "./bus.ts";
|
|
16
|
+
import {
|
|
17
|
+
isTelegramChannelPostValidationError,
|
|
18
|
+
TelegramChannelPostValidationError,
|
|
19
|
+
} from "./channel-posts.ts";
|
|
16
20
|
import type { ExtensionAPI } from "./pi.ts";
|
|
17
21
|
import {
|
|
18
22
|
TELEGRAM_ATTACH_PROMPT_GUIDELINES,
|
|
@@ -104,6 +108,12 @@ export interface TelegramOutboundMessageToolRegistrationDeps extends TelegramOut
|
|
|
104
108
|
markdown: string,
|
|
105
109
|
options: { operationId: string; replyMarkup?: unknown },
|
|
106
110
|
) => Promise<number | undefined>;
|
|
111
|
+
sendChannelMediaMessage?: (
|
|
112
|
+
channel: number | string,
|
|
113
|
+
mediaPath: string,
|
|
114
|
+
markdown: string,
|
|
115
|
+
options: { operationId: string; replyMarkup?: unknown },
|
|
116
|
+
) => Promise<number | undefined>;
|
|
107
117
|
}
|
|
108
118
|
|
|
109
119
|
export interface TelegramQueuedOutboundAttachmentView {
|
|
@@ -517,11 +527,18 @@ export function registerTelegramOutboundMessageTool(
|
|
|
517
527
|
name: "telegram_message",
|
|
518
528
|
label: "Telegram Message",
|
|
519
529
|
description:
|
|
520
|
-
"Send Markdown text directly to the paired/default Telegram chat, an exact channel chat_id, or an explicit live target. Channel posting requires Telegram-granted bot permission. Hidden telegram_button comments become inline prompt buttons.",
|
|
530
|
+
"Send Markdown text directly to the paired/default Telegram chat, an exact channel chat_id, or an explicit live target. Channel delivery supports one optional local photo or video upload with the text as its caption. Channel posting requires Telegram-granted bot permission. Hidden telegram_button comments become inline prompt buttons.",
|
|
521
531
|
promptSnippet: TELEGRAM_MESSAGE_PROMPT_SNIPPET,
|
|
522
532
|
promptGuidelines: [...TELEGRAM_MESSAGE_PROMPT_GUIDELINES],
|
|
523
533
|
parameters: Type.Object({
|
|
524
534
|
text: Type.String({ description: "Message text to send" }),
|
|
535
|
+
media: Type.Optional(
|
|
536
|
+
Type.String({
|
|
537
|
+
minLength: 1,
|
|
538
|
+
description:
|
|
539
|
+
"Local single-file channel upload: .jpg/.jpeg/.png/.webp photo or .mp4 video; the text becomes its caption (max 1024 characters). Albums and other media types are rejected.",
|
|
540
|
+
}),
|
|
541
|
+
),
|
|
525
542
|
chat_id: Type.Optional(
|
|
526
543
|
Type.Union([
|
|
527
544
|
Type.Number(),
|
|
@@ -547,6 +564,7 @@ export function registerTelegramOutboundMessageTool(
|
|
|
547
564
|
try {
|
|
548
565
|
return await sendTelegramOutboundMessage({
|
|
549
566
|
text: params.text,
|
|
567
|
+
media: params.media,
|
|
550
568
|
operationId: toolCallId,
|
|
551
569
|
channel: params.channel,
|
|
552
570
|
chatId: params.chat_id,
|
|
@@ -561,9 +579,12 @@ export function registerTelegramOutboundMessageTool(
|
|
|
561
579
|
planMessage: deps.planMessage,
|
|
562
580
|
sendMarkdownMessage: deps.sendMarkdownMessage,
|
|
563
581
|
sendChannelMarkdownMessage: deps.sendChannelMarkdownMessage,
|
|
582
|
+
sendChannelMediaMessage: deps.sendChannelMediaMessage,
|
|
564
583
|
});
|
|
565
584
|
} catch (error) {
|
|
566
|
-
const
|
|
585
|
+
const isChannelTarget = typeof params.chat_id === "string" || params.channel === true;
|
|
586
|
+
const reportableError = isChannelTarget &&
|
|
587
|
+
!isTelegramChannelPostValidationError(error)
|
|
567
588
|
? new Error("Telegram channel publication failed; inspect the retained local record before retrying.")
|
|
568
589
|
: error;
|
|
569
590
|
deps.recordRuntimeEvent?.("message", reportableError, { phase: "direct" });
|
|
@@ -771,6 +792,7 @@ export async function deliverTelegramGuestCachedAttachment(options: {
|
|
|
771
792
|
|
|
772
793
|
export async function sendTelegramOutboundMessage(options: {
|
|
773
794
|
text: string;
|
|
795
|
+
media?: string;
|
|
774
796
|
operationId?: string;
|
|
775
797
|
channel?: boolean;
|
|
776
798
|
chatId?: number | string;
|
|
@@ -798,6 +820,12 @@ export async function sendTelegramOutboundMessage(options: {
|
|
|
798
820
|
markdown: string,
|
|
799
821
|
options: { operationId: string; replyMarkup?: unknown },
|
|
800
822
|
) => Promise<number | undefined>;
|
|
823
|
+
sendChannelMediaMessage?: (
|
|
824
|
+
channel: number | string,
|
|
825
|
+
mediaPath: string,
|
|
826
|
+
markdown: string,
|
|
827
|
+
options: { operationId: string; replyMarkup?: unknown },
|
|
828
|
+
) => Promise<number | undefined>;
|
|
801
829
|
}): Promise<{
|
|
802
830
|
content: Array<{ type: "text"; text: string }>;
|
|
803
831
|
details: { chatId: number | string; messageId?: number };
|
|
@@ -811,10 +839,23 @@ export async function sendTelegramOutboundMessage(options: {
|
|
|
811
839
|
options.target !== undefined || options.agentThread !== undefined) {
|
|
812
840
|
throw new Error("Telegram channel delivery requires one exact @username or negative numeric channel ID without a thread target.");
|
|
813
841
|
}
|
|
842
|
+
const plan = options.planMessage(options.text);
|
|
843
|
+
if (options.media !== undefined) {
|
|
844
|
+
if (!options.sendChannelMediaMessage || !options.operationId) {
|
|
845
|
+
throw new TelegramChannelPostValidationError(
|
|
846
|
+
"Telegram channel media delivery requires direct leader transport ownership and operation identity.",
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
const messageId = await options.sendChannelMediaMessage(
|
|
850
|
+
options.chatId, options.media, plan.markdown,
|
|
851
|
+
{ operationId: options.operationId, replyMarkup: plan.replyMarkup });
|
|
852
|
+
return { content: [{ type: "text",
|
|
853
|
+
text: formatTelegramOutboundMessageToolResultText(options.chatId) }],
|
|
854
|
+
details: { chatId: options.chatId, messageId } };
|
|
855
|
+
}
|
|
814
856
|
if (!options.sendChannelMarkdownMessage || !options.operationId) {
|
|
815
857
|
throw new Error("Telegram channel delivery requires direct leader transport ownership and operation identity.");
|
|
816
858
|
}
|
|
817
|
-
const plan = options.planMessage(options.text);
|
|
818
859
|
const messageId = await options.sendChannelMarkdownMessage(
|
|
819
860
|
options.chatId, plan.markdown, { operationId: options.operationId,
|
|
820
861
|
replyMarkup: plan.replyMarkup });
|
|
@@ -822,6 +863,11 @@ export async function sendTelegramOutboundMessage(options: {
|
|
|
822
863
|
text: formatTelegramOutboundMessageToolResultText(options.chatId) }],
|
|
823
864
|
details: { chatId: options.chatId, messageId } };
|
|
824
865
|
}
|
|
866
|
+
if (options.media !== undefined) {
|
|
867
|
+
throw new TelegramChannelPostValidationError(
|
|
868
|
+
"telegram_message media uploads require channel delivery with an exact @username or negative numeric channel ID and channel: true.",
|
|
869
|
+
);
|
|
870
|
+
}
|
|
825
871
|
const requestedAgentSelector: TelegramBusAgentTargetSelector | undefined =
|
|
826
872
|
options.agentThread !== undefined
|
|
827
873
|
? typeof options.agentThread === "number"
|
|
@@ -70,6 +70,7 @@ export interface TelegramPreviewActiveTurn {
|
|
|
70
70
|
target?: TelegramTarget;
|
|
71
71
|
voiceReplyPreferred?: boolean;
|
|
72
72
|
voiceReplyRequired?: boolean;
|
|
73
|
+
guestQueryId?: string;
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
export interface TelegramAssistantMessagePreviewStartDeps<TMessage> {
|
|
@@ -408,6 +409,17 @@ export function createTelegramAssistantMessagePreviewHooks<TMessage>(
|
|
|
408
409
|
};
|
|
409
410
|
}
|
|
410
411
|
|
|
412
|
+
/**
|
|
413
|
+
* Returns true when the active turn is a Telegram Guest Mode query. A guest
|
|
414
|
+
* query allows exactly one answer within a limited Telegram response window,
|
|
415
|
+
* so it must never emit streaming draft previews.
|
|
416
|
+
*/
|
|
417
|
+
export function shouldSuppressPreviewForGuestTurn(
|
|
418
|
+
turn: { guestQueryId?: string } | null | undefined,
|
|
419
|
+
): boolean {
|
|
420
|
+
return !!turn?.guestQueryId;
|
|
421
|
+
}
|
|
422
|
+
|
|
411
423
|
export async function handleTelegramAssistantMessagePreviewStart<TMessage>(
|
|
412
424
|
message: TMessage,
|
|
413
425
|
deps: TelegramAssistantMessagePreviewStartDeps<TMessage>,
|
|
@@ -422,6 +434,10 @@ export async function handleTelegramAssistantMessagePreviewStart<TMessage>(
|
|
|
422
434
|
deps.setState(undefined);
|
|
423
435
|
return;
|
|
424
436
|
}
|
|
437
|
+
if (shouldSuppressPreviewForGuestTurn(turn)) {
|
|
438
|
+
deps.setState(undefined);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
425
441
|
const state = deps.getState();
|
|
426
442
|
sealTelegramPreviewState(state);
|
|
427
443
|
const next = deps.createPreviewState();
|
|
@@ -443,6 +459,7 @@ export async function handleTelegramAssistantMessagePreviewUpdate<TMessage>(
|
|
|
443
459
|
return;
|
|
444
460
|
}
|
|
445
461
|
if (shouldSuppressPreviewForVoice(turn)) return;
|
|
462
|
+
if (shouldSuppressPreviewForGuestTurn(turn)) return;
|
|
446
463
|
let state = deps.getState();
|
|
447
464
|
if (!state) {
|
|
448
465
|
state = deps.createPreviewState();
|
|
@@ -32,6 +32,7 @@ export const TELEGRAM_MESSAGE_PROMPT_SNIPPET =
|
|
|
32
32
|
export const TELEGRAM_MESSAGE_PROMPT_GUIDELINES = [
|
|
33
33
|
"Use telegram_message only when the user explicitly asks to send a message to Telegram from the local/TUI side, or names a concrete Telegram delivery target.",
|
|
34
34
|
"For an explicitly requested channel post, pass its exact numeric id or public @username as chat_id; no local channel registry is required, and Telegram remains the authority on the bot's posting permission.",
|
|
35
|
+
"For an explicitly requested channel media post, pass one local .jpg/.jpeg/.png/.webp photo or .mp4 video as media; the text becomes its caption (max 1024 characters), and albums or other media types are rejected.",
|
|
35
36
|
"For a live Pi thread target, provide thread as its case-insensitive name or numeric id; the bridge sends visibly and admits one attributed turn to that live instance. Unknown, ambiguous, same, or offline targets fail before sending.",
|
|
36
37
|
"Add buttons by embedding the same top-level telegram_button HTML comments used in normal Telegram replies; Telegram does not support standalone buttons.",
|
|
37
38
|
"During an active Telegram turn, omit telegram_message for the current target and answer normally; use thread only when the user requests delivery to a different live Pi thread.",
|
|
@@ -110,6 +110,7 @@ export interface TelegramQueueItemBase {
|
|
|
110
110
|
transportStamp?: TelegramTransportStamp;
|
|
111
111
|
replyToMessageId: number;
|
|
112
112
|
guestQueryId?: string;
|
|
113
|
+
guestInlineMessageId?: string;
|
|
113
114
|
queueOrder: number;
|
|
114
115
|
queueLane: TelegramQueueLane;
|
|
115
116
|
laneOrder: number;
|
|
@@ -152,6 +153,7 @@ export interface TelegramQueueHandoffBase {
|
|
|
152
153
|
transportStamp?: TelegramTransportStamp;
|
|
153
154
|
replyToMessageId: number;
|
|
154
155
|
guestQueryId?: string;
|
|
156
|
+
guestInlineMessageId?: string;
|
|
155
157
|
queueOrder: number;
|
|
156
158
|
queueLane: TelegramQueueLane;
|
|
157
159
|
laneOrder: number;
|
|
@@ -612,6 +614,9 @@ export function createTelegramQueueHandoffPayload<TContext>(
|
|
|
612
614
|
...(item.transportStamp ? { transportStamp: item.transportStamp } : {}),
|
|
613
615
|
replyToMessageId: item.replyToMessageId,
|
|
614
616
|
...(item.guestQueryId ? { guestQueryId: item.guestQueryId } : {}),
|
|
617
|
+
...(item.guestInlineMessageId
|
|
618
|
+
? { guestInlineMessageId: item.guestInlineMessageId }
|
|
619
|
+
: {}),
|
|
615
620
|
queueOrder: item.queueOrder,
|
|
616
621
|
queueLane: item.queueLane,
|
|
617
622
|
laneOrder: item.laneOrder,
|
|
@@ -626,6 +631,9 @@ export function createTelegramQueueHandoffPayload<TContext>(
|
|
|
626
631
|
...(item.transportStamp ? { transportStamp: item.transportStamp } : {}),
|
|
627
632
|
replyToMessageId: item.replyToMessageId,
|
|
628
633
|
...(item.guestQueryId ? { guestQueryId: item.guestQueryId } : {}),
|
|
634
|
+
...(item.guestInlineMessageId
|
|
635
|
+
? { guestInlineMessageId: item.guestInlineMessageId }
|
|
636
|
+
: {}),
|
|
629
637
|
queueOrder: item.queueOrder,
|
|
630
638
|
queueLane: item.queueLane,
|
|
631
639
|
laneOrder: item.laneOrder,
|
|
@@ -1544,6 +1552,8 @@ export interface TelegramAgentEndRuntimeDeps<
|
|
|
1544
1552
|
options?: { parseMode?: string },
|
|
1545
1553
|
) => Promise<void>;
|
|
1546
1554
|
sendGuestReply?: (guestQueryId: string, markdown: string) => Promise<void>;
|
|
1555
|
+
/** Replaces the early guest ACK with the final text. */
|
|
1556
|
+
editGuestReply?: (inlineMessageId: string, markdown: string) => Promise<void>;
|
|
1547
1557
|
sendGuestAttachment?: (
|
|
1548
1558
|
turn: TTurn,
|
|
1549
1559
|
attachment: QueuedAttachment,
|
|
@@ -1622,6 +1632,7 @@ export interface TelegramAgentEndHookRuntimeDeps<
|
|
|
1622
1632
|
>["sendRichAttachmentReply"];
|
|
1623
1633
|
answerGuestQuery?: TelegramAgentEndRuntimeDeps<TTurn>["answerGuestQuery"];
|
|
1624
1634
|
sendGuestReply?: TelegramAgentEndRuntimeDeps<TTurn>["sendGuestReply"];
|
|
1635
|
+
editGuestReply?: TelegramAgentEndRuntimeDeps<TTurn>["editGuestReply"];
|
|
1625
1636
|
sendGuestAttachment?: TelegramAgentEndRuntimeDeps<TTurn>["sendGuestAttachment"];
|
|
1626
1637
|
sendGuestVoiceReply?: TelegramAgentEndRuntimeDeps<TTurn>["sendGuestVoiceReply"];
|
|
1627
1638
|
planOutboundReply?: TelegramAgentEndRuntimeDeps<
|
|
@@ -1762,6 +1773,7 @@ export function createTelegramAgentEndHook<
|
|
|
1762
1773
|
sendRichAttachmentReply: deps.sendRichAttachmentReply,
|
|
1763
1774
|
answerGuestQuery: deps.answerGuestQuery,
|
|
1764
1775
|
sendGuestReply: deps.sendGuestReply,
|
|
1776
|
+
editGuestReply: deps.editGuestReply,
|
|
1765
1777
|
sendGuestAttachment: deps.sendGuestAttachment,
|
|
1766
1778
|
sendGuestVoiceReply: deps.sendGuestVoiceReply,
|
|
1767
1779
|
planOutboundReply: deps.planOutboundReply,
|
|
@@ -1854,11 +1866,47 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
1854
1866
|
return;
|
|
1855
1867
|
}
|
|
1856
1868
|
if (turn.guestQueryId) {
|
|
1869
|
+
if (turn.guestInlineMessageId && deps.editGuestReply) {
|
|
1870
|
+
const experimentText = assistant.errorMessage
|
|
1871
|
+
? "Telegram bridge: Pi failed while processing the request."
|
|
1872
|
+
: finalText;
|
|
1873
|
+
if (experimentText) {
|
|
1874
|
+
try {
|
|
1875
|
+
await deps.editGuestReply(turn.guestInlineMessageId, experimentText);
|
|
1876
|
+
deps.recordRuntimeEvent?.(
|
|
1877
|
+
"guest",
|
|
1878
|
+
new Error("Guest ACK experiment edited the guest answer"),
|
|
1879
|
+
{ phase: "guest-ack-edited", guestQueryId: turn.guestQueryId },
|
|
1880
|
+
);
|
|
1881
|
+
} catch (error) {
|
|
1882
|
+
deps.recordRuntimeEvent?.("delivery", error, {
|
|
1883
|
+
phase: "guest-ack-edit",
|
|
1884
|
+
guestQueryId: turn.guestQueryId,
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
} else {
|
|
1888
|
+
deps.recordRuntimeEvent?.(
|
|
1889
|
+
"delivery",
|
|
1890
|
+
new Error("Guest ACK experiment turn produced no editable text"),
|
|
1891
|
+
{ phase: "guest-ack-edit-empty", guestQueryId: turn.guestQueryId },
|
|
1892
|
+
);
|
|
1893
|
+
}
|
|
1894
|
+
if (!isDeliveryActive()) return;
|
|
1895
|
+
if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1857
1898
|
if (assistant.errorMessage) {
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1899
|
+
try {
|
|
1900
|
+
await deps.answerGuestQuery?.(
|
|
1901
|
+
turn.guestQueryId,
|
|
1902
|
+
"Telegram bridge: Pi failed while processing the request.",
|
|
1903
|
+
);
|
|
1904
|
+
} catch (error) {
|
|
1905
|
+
deps.recordRuntimeEvent?.("delivery", error, {
|
|
1906
|
+
phase: "guest-error-reply",
|
|
1907
|
+
guestQueryId: turn.guestQueryId,
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1862
1910
|
if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
|
|
1863
1911
|
return;
|
|
1864
1912
|
}
|
|
@@ -1894,10 +1942,20 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
1894
1942
|
});
|
|
1895
1943
|
}
|
|
1896
1944
|
} else if (finalText) {
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1945
|
+
try {
|
|
1946
|
+
if (deps.sendGuestReply) {
|
|
1947
|
+
await deps.sendGuestReply(turn.guestQueryId, finalText);
|
|
1948
|
+
} else {
|
|
1949
|
+
await deps.answerGuestQuery?.(turn.guestQueryId, finalText);
|
|
1950
|
+
}
|
|
1951
|
+
} catch (error) {
|
|
1952
|
+
// Guest queries expire after Telegram's response timeout, so a slow
|
|
1953
|
+
// turn can fail the only delivery attempt. Record and continue the
|
|
1954
|
+
// agent-end lifecycle instead of rejecting the extension hook.
|
|
1955
|
+
deps.recordRuntimeEvent?.("delivery", error, {
|
|
1956
|
+
phase: "guest-reply",
|
|
1957
|
+
guestQueryId: turn.guestQueryId,
|
|
1958
|
+
});
|
|
1901
1959
|
}
|
|
1902
1960
|
}
|
|
1903
1961
|
if (!isDeliveryActive()) return;
|
|
@@ -551,9 +551,12 @@ function applyInlineMarkdownStyles(text: string): string {
|
|
|
551
551
|
result = renderDelimitedInlineStyle(result, "*", (content) => {
|
|
552
552
|
return `<i>${content}</i>`;
|
|
553
553
|
});
|
|
554
|
-
|
|
554
|
+
result = renderDelimitedInlineStyle(result, "_", (content) => {
|
|
555
555
|
return `<i>${content}</i>`;
|
|
556
556
|
});
|
|
557
|
+
return renderDelimitedInlineStyle(result, "||", (content) => {
|
|
558
|
+
return `<tg-spoiler>${content}</tg-spoiler>`;
|
|
559
|
+
});
|
|
557
560
|
}
|
|
558
561
|
|
|
559
562
|
function restoreInlineMarkdownTokens(
|
|
@@ -898,3 +898,22 @@ export function createGuestMarkdownReplySender(deps: {
|
|
|
898
898
|
});
|
|
899
899
|
};
|
|
900
900
|
}
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* Guest reply editor: replaces an early Guest Mode answer (the temporary ACK
|
|
904
|
+
* experiment) with native Rich Markdown content addressed by
|
|
905
|
+
* `inline_message_id` instead of a chat/message pair.
|
|
906
|
+
*/
|
|
907
|
+
export function createGuestMarkdownReplyEditor(deps: {
|
|
908
|
+
editGuestInlineMessage: (
|
|
909
|
+
inlineMessageId: string,
|
|
910
|
+
content: { richMessage?: TelegramInputRichMessage; text?: string },
|
|
911
|
+
) => Promise<void>;
|
|
912
|
+
}) {
|
|
913
|
+
return async (inlineMessageId: string, markdown: string) => {
|
|
914
|
+
const [richMarkdown = markdown] = splitTelegramNativeMarkdown(markdown);
|
|
915
|
+
await deps.editGuestInlineMessage(inlineMessageId, {
|
|
916
|
+
richMessage: { markdown: richMarkdown },
|
|
917
|
+
});
|
|
918
|
+
};
|
|
919
|
+
}
|
|
@@ -659,6 +659,12 @@ export interface TelegramInboundRouteRuntimeDeps<
|
|
|
659
659
|
) => Promise<number | undefined>;
|
|
660
660
|
deleteMessage?: (chatId: number, messageId: number) => Promise<void>;
|
|
661
661
|
answerGuestQuery: (guestQueryId: string, text?: string) => Promise<void>;
|
|
662
|
+
/** Answers the guest query immediately and returns its inline message id. */
|
|
663
|
+
answerGuestQueryForInlineMessage?: (
|
|
664
|
+
guestQueryId: string,
|
|
665
|
+
text: string,
|
|
666
|
+
options?: { parseMode?: "HTML" },
|
|
667
|
+
) => Promise<string | undefined>;
|
|
662
668
|
sendTextReply: (
|
|
663
669
|
chatId: number,
|
|
664
670
|
replyToMessageId: number,
|
|
@@ -2655,6 +2661,9 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2655
2661
|
await deps.threadStore.load();
|
|
2656
2662
|
assertExecutionCurrent();
|
|
2657
2663
|
};
|
|
2664
|
+
// Answer the guest query immediately so the agent-end edit can replace the
|
|
2665
|
+
// early ACK once the turn settles. See BACKLOG.md for live acceptance.
|
|
2666
|
+
const TELEGRAM_GUEST_ACK_HTML = "<b>⚙️ Received. Working on it…</b>";
|
|
2658
2667
|
const handleAuthorizedTelegramGuestMessage = async (
|
|
2659
2668
|
guestMessage: Updates.TelegramGuestMessage & { from: TelegramUser },
|
|
2660
2669
|
ctx: TContext,
|
|
@@ -2662,6 +2671,31 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2662
2671
|
const assertExecutionCurrent =
|
|
2663
2672
|
Updates.createTelegramUpdateExecutionFenceGuard(guestMessage);
|
|
2664
2673
|
assertExecutionCurrent();
|
|
2674
|
+
let guestInlineMessageId: string | undefined;
|
|
2675
|
+
if (deps.answerGuestQueryForInlineMessage) {
|
|
2676
|
+
try {
|
|
2677
|
+
guestInlineMessageId = await deps.answerGuestQueryForInlineMessage(
|
|
2678
|
+
guestMessage.guest_query_id,
|
|
2679
|
+
TELEGRAM_GUEST_ACK_HTML,
|
|
2680
|
+
{ parseMode: "HTML" },
|
|
2681
|
+
);
|
|
2682
|
+
deps.recordRuntimeEvent?.(
|
|
2683
|
+
"guest",
|
|
2684
|
+
new Error("Guest ACK experiment answered the guest query"),
|
|
2685
|
+
{
|
|
2686
|
+
phase: "guest-ack-sent",
|
|
2687
|
+
guestQueryId: guestMessage.guest_query_id,
|
|
2688
|
+
hasInlineMessageId: !!guestInlineMessageId,
|
|
2689
|
+
},
|
|
2690
|
+
);
|
|
2691
|
+
} catch (error) {
|
|
2692
|
+
deps.recordRuntimeEvent?.("guest", error, {
|
|
2693
|
+
phase: "guest-ack-failed",
|
|
2694
|
+
guestQueryId: guestMessage.guest_query_id,
|
|
2695
|
+
});
|
|
2696
|
+
}
|
|
2697
|
+
assertExecutionCurrent();
|
|
2698
|
+
}
|
|
2665
2699
|
const text = guestMessage.text ?? "";
|
|
2666
2700
|
const gm = guestMessage as unknown as Record<string, unknown>;
|
|
2667
2701
|
// Build telegram prefix with guest context
|
|
@@ -2752,6 +2786,10 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2752
2786
|
promptFiles: processed.promptFiles,
|
|
2753
2787
|
handlerOutputs: processed.handlerOutputs,
|
|
2754
2788
|
sourceContext,
|
|
2789
|
+
// Guest Mode allows exactly one reply within Telegram's limited response
|
|
2790
|
+
// window; the note travels with the turn text so the agent sees it at
|
|
2791
|
+
// execution time without a guest-specific system prompt variant.
|
|
2792
|
+
guestTurn: true,
|
|
2755
2793
|
});
|
|
2756
2794
|
const order = deps.bridgeRuntime.queue.allocateItemOrder();
|
|
2757
2795
|
const content: Queue.TelegramPromptContent[] = [
|
|
@@ -2778,6 +2816,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2778
2816
|
chatId: 0,
|
|
2779
2817
|
replyToMessageId: 0,
|
|
2780
2818
|
guestQueryId: guestMessage.guest_query_id,
|
|
2819
|
+
...(guestInlineMessageId ? { guestInlineMessageId } : {}),
|
|
2781
2820
|
sourceMessageIds: [],
|
|
2782
2821
|
queueOrder: order,
|
|
2783
2822
|
queueLane: "default",
|
|
@@ -42,6 +42,10 @@ export interface TelegramSetupDeps {
|
|
|
42
42
|
result?: TelegramSetupUser;
|
|
43
43
|
description?: string;
|
|
44
44
|
}>;
|
|
45
|
+
/** Resolve a submitted literal token or `$NAME`/`${NAME}` reference. */
|
|
46
|
+
resolveBotToken?: (value: string) => string | undefined;
|
|
47
|
+
/** Redacted diagnostic for an unresolved or malformed token reference. */
|
|
48
|
+
describeBotToken?: (value: string) => string | undefined;
|
|
45
49
|
persistConfig: (config: TelegramSetupConfig) => Promise<void>;
|
|
46
50
|
notify: (message: string, level: "info" | "error") => void;
|
|
47
51
|
startPolling: () => unknown | Promise<unknown>;
|
|
@@ -70,6 +74,8 @@ export interface TelegramSetupPromptRuntimeDeps<
|
|
|
70
74
|
setConfig: (config: TelegramSetupConfig) => void;
|
|
71
75
|
setupGuard: TelegramSetupGuard;
|
|
72
76
|
getMe: TelegramSetupDeps["getMe"];
|
|
77
|
+
resolveBotToken?: TelegramSetupDeps["resolveBotToken"];
|
|
78
|
+
describeBotToken?: TelegramSetupDeps["describeBotToken"];
|
|
73
79
|
persistConfig: (config: TelegramSetupConfig) => Promise<void>;
|
|
74
80
|
startPolling: (ctx: TContext) => unknown | Promise<unknown>;
|
|
75
81
|
updateStatus: (ctx: TContext) => void;
|
|
@@ -88,6 +94,25 @@ const TELEGRAM_BOT_TOKEN_ENV_VARS = [
|
|
|
88
94
|
"TELEGRAM_KEY",
|
|
89
95
|
] as const;
|
|
90
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Default submitted-token handling for structural callers that inject no
|
|
99
|
+
* reference port: plain literals pass through, while `$`-prefixed values fail
|
|
100
|
+
* closed instead of being sent to the Bot API as a literal token.
|
|
101
|
+
*/
|
|
102
|
+
function resolveSubmittedTelegramBotToken(value: string): string | undefined {
|
|
103
|
+
const trimmed = value.trim();
|
|
104
|
+
if (!trimmed || trimmed.startsWith("$")) return undefined;
|
|
105
|
+
return trimmed;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function describeSubmittedTelegramBotToken(
|
|
109
|
+
value: string,
|
|
110
|
+
): string | undefined {
|
|
111
|
+
return value.trim().startsWith("$")
|
|
112
|
+
? "Telegram bot token environment reference is unavailable in this setup environment."
|
|
113
|
+
: undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
91
116
|
function isTelegramPollingStartResult(
|
|
92
117
|
value: unknown,
|
|
93
118
|
): value is TelegramPollingStartResult {
|
|
@@ -105,8 +130,8 @@ export function getTelegramBotTokenInputDefault(
|
|
|
105
130
|
const trimmedConfigToken = configToken?.trim();
|
|
106
131
|
if (trimmedConfigToken) return trimmedConfigToken;
|
|
107
132
|
for (const key of TELEGRAM_BOT_TOKEN_ENV_VARS) {
|
|
108
|
-
|
|
109
|
-
if (
|
|
133
|
+
// Persist the originating alias rather than copying the resolved secret.
|
|
134
|
+
if (env[key]?.trim()) return `$${key}`;
|
|
110
135
|
}
|
|
111
136
|
return TELEGRAM_BOT_TOKEN_INPUT_PLACEHOLDER;
|
|
112
137
|
}
|
|
@@ -135,13 +160,26 @@ export async function runTelegramSetup(
|
|
|
135
160
|
? await deps.promptEditor("Telegram bot token", tokenPrompt.value)
|
|
136
161
|
: await deps.promptInput("Telegram bot token", tokenPrompt.value);
|
|
137
162
|
if (!token) return { status: "cancelled" };
|
|
163
|
+
const submittedToken = token.trim();
|
|
164
|
+
const resolveBotToken =
|
|
165
|
+
deps.resolveBotToken ?? resolveSubmittedTelegramBotToken;
|
|
166
|
+
const describeBotToken =
|
|
167
|
+
deps.describeBotToken ?? describeSubmittedTelegramBotToken;
|
|
168
|
+
const resolvedToken = resolveBotToken(submittedToken);
|
|
138
169
|
const nextConfig: TelegramSetupConfig = {
|
|
139
170
|
...deps.config,
|
|
140
|
-
botToken:
|
|
171
|
+
botToken: submittedToken,
|
|
141
172
|
};
|
|
173
|
+
if (!resolvedToken) {
|
|
174
|
+
deps.notify(
|
|
175
|
+
describeBotToken(submittedToken) ?? "Invalid Telegram bot token",
|
|
176
|
+
"error",
|
|
177
|
+
);
|
|
178
|
+
return { status: "validation-failed" };
|
|
179
|
+
}
|
|
142
180
|
let data: Awaited<ReturnType<TelegramSetupDeps["getMe"]>>;
|
|
143
181
|
try {
|
|
144
|
-
data = await deps.getMe(
|
|
182
|
+
data = await deps.getMe(resolvedToken);
|
|
145
183
|
} catch (error) {
|
|
146
184
|
const message = error instanceof Error ? error.message : String(error);
|
|
147
185
|
deps.notify(`Telegram API check failed: ${message}`, "error");
|
|
@@ -195,6 +233,8 @@ export function createTelegramSetupPromptRuntime<
|
|
|
195
233
|
promptInput: (label, value) => ctx.ui.input(label, value),
|
|
196
234
|
promptEditor: (label, value) => ctx.ui.editor(label, value),
|
|
197
235
|
getMe: deps.getMe,
|
|
236
|
+
resolveBotToken: deps.resolveBotToken,
|
|
237
|
+
describeBotToken: deps.describeBotToken,
|
|
198
238
|
persistConfig: async (config) => {
|
|
199
239
|
const previousConfig = deps.getConfig();
|
|
200
240
|
deps.setConfig(config);
|
|
@@ -245,6 +245,8 @@ export interface TelegramBridgeInboundWorkerState {
|
|
|
245
245
|
export interface TelegramBridgeStatusLineState {
|
|
246
246
|
hasBotToken?: boolean;
|
|
247
247
|
botUsername?: string;
|
|
248
|
+
/** Redacted named-variable diagnostic when the stored token reference cannot resolve. */
|
|
249
|
+
botTokenDiagnostic?: string;
|
|
248
250
|
activeProfileName?: string;
|
|
249
251
|
diagnosticPaths?: { state: string; logs: string };
|
|
250
252
|
allowedUserId?: number;
|
|
@@ -323,10 +325,40 @@ export interface TelegramStatusRuntimeDeps<
|
|
|
323
325
|
|
|
324
326
|
export interface TelegramBridgeStatusConfig {
|
|
325
327
|
botToken?: string;
|
|
328
|
+
/** Caller-resolved token availability; falls back to raw presence. */
|
|
329
|
+
botHasToken?: boolean;
|
|
330
|
+
/** Caller-supplied redacted diagnostic for an unresolved token reference. */
|
|
331
|
+
botTokenDiagnostic?: string;
|
|
326
332
|
botUsername?: string;
|
|
327
333
|
allowedUserId?: number;
|
|
328
334
|
}
|
|
329
335
|
|
|
336
|
+
/** Narrow config-store view used to project resolved bot-token availability. */
|
|
337
|
+
export interface TelegramBridgeStatusConfigSource {
|
|
338
|
+
get: () => TelegramBridgeStatusConfig;
|
|
339
|
+
hasBotToken?: () => boolean;
|
|
340
|
+
getBotTokenDiagnostic?: () => string | undefined;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Project a config store into the status view without moving token-reference
|
|
345
|
+
* resolution into this structural leaf domain.
|
|
346
|
+
*/
|
|
347
|
+
export function createTelegramBridgeStatusConfigGetter(
|
|
348
|
+
source: TelegramBridgeStatusConfigSource,
|
|
349
|
+
): () => TelegramBridgeStatusConfig {
|
|
350
|
+
return () => {
|
|
351
|
+
const config = source.get();
|
|
352
|
+
return {
|
|
353
|
+
...config,
|
|
354
|
+
...(source.hasBotToken ? { botHasToken: source.hasBotToken() } : {}),
|
|
355
|
+
...(source.getBotTokenDiagnostic
|
|
356
|
+
? { botTokenDiagnostic: source.getBotTokenDiagnostic() }
|
|
357
|
+
: {}),
|
|
358
|
+
};
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
330
362
|
export interface TelegramBridgeStatusRuntimeDeps<
|
|
331
363
|
TQueueItem extends { queueLane: TelegramStatusQueueLane },
|
|
332
364
|
> {
|
|
@@ -681,7 +713,7 @@ export function createTelegramBridgeStatusRuntime<
|
|
|
681
713
|
const compactionInProgress = deps.isCompactionInProgress();
|
|
682
714
|
const localBus = deps.getLocalBus?.();
|
|
683
715
|
return {
|
|
684
|
-
hasBotToken:
|
|
716
|
+
hasBotToken: config.botHasToken ?? Boolean(config.botToken),
|
|
685
717
|
pollingActive: deps.isPollingActive(),
|
|
686
718
|
paired: !!config.allowedUserId,
|
|
687
719
|
busRole: deps.getBusRole?.(),
|
|
@@ -715,8 +747,9 @@ export function createTelegramBridgeStatusRuntime<
|
|
|
715
747
|
? (deps.getActiveProfileName() ?? TELEGRAM_STATUS_DEFAULT_PROFILE_NAME)
|
|
716
748
|
: undefined;
|
|
717
749
|
return {
|
|
718
|
-
hasBotToken: Boolean(config.botToken),
|
|
750
|
+
hasBotToken: config.botHasToken ?? Boolean(config.botToken),
|
|
719
751
|
botUsername: config.botUsername,
|
|
752
|
+
botTokenDiagnostic: config.botTokenDiagnostic,
|
|
720
753
|
activeProfileName,
|
|
721
754
|
diagnosticPaths: deps.getDiagnosticPaths?.(activeProfileName),
|
|
722
755
|
allowedUserId: config.allowedUserId,
|
|
@@ -925,10 +958,14 @@ export function buildTelegramStatusBarText(
|
|
|
925
958
|
}
|
|
926
959
|
|
|
927
960
|
function formatTelegramBridgeBotStatus(
|
|
928
|
-
state: Pick<
|
|
961
|
+
state: Pick<
|
|
962
|
+
TelegramBridgeStatusLineState,
|
|
963
|
+
"hasBotToken" | "botUsername" | "botTokenDiagnostic"
|
|
964
|
+
>,
|
|
929
965
|
): string {
|
|
930
966
|
if (state.botUsername) return `@${state.botUsername}`;
|
|
931
|
-
|
|
967
|
+
if (state.hasBotToken) return "unknown";
|
|
968
|
+
return state.botTokenDiagnostic ?? "not configured";
|
|
932
969
|
}
|
|
933
970
|
|
|
934
971
|
function formatTelegramStatusTarget(
|