@llblab/pi-kit 0.36.3 → 0.37.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +2 -2
- package/node_modules/@llblab/pi-telegram/AGENTS.md +4 -4
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +9 -11
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +19 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +2 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +2 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +5 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +36 -35
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +2 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-buttons.js +20 -18
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.d.ts +6 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.js +23 -65
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound.js +3 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/prompts.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/prompts.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/runtime.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/runtime.js +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +4 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +29 -20
- package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/dist/skills/telegram-bridge/SKILL.md +5 -5
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +6 -6
- package/node_modules/@llblab/pi-telegram/docs/compact-matrix-literal.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/outbound.md +8 -8
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +5 -3
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +4 -0
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +53 -43
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +1 -3
- package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +17 -15
- package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +28 -68
- package/node_modules/@llblab/pi-telegram/lib/outbound.ts +3 -3
- package/node_modules/@llblab/pi-telegram/lib/prompts.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/runtime.ts +2 -1
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +31 -18
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/SKILL.md +5 -5
- package/package.json +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Telegram outbound markup parsing helpers
|
|
3
3
|
* Zones: telegram outbound, assistant markup
|
|
4
|
-
* Owns
|
|
4
|
+
* Owns delimiter-based assistant action comment extraction, attribute parsing, and markup stripping shared by voice and outbound delivery
|
|
5
5
|
*/
|
|
6
6
|
import { isWireRecord as isTelegramActionPayload } from "./wire.js";
|
|
7
7
|
function getMarkdownLineEnd(markdown, offset) {
|
|
@@ -12,7 +12,7 @@ function getMarkdownLineText(markdown, offset, end) {
|
|
|
12
12
|
return markdown.slice(offset, end).replace(/\r?\n$/, "");
|
|
13
13
|
}
|
|
14
14
|
function getTopLevelOpeningFence(line) {
|
|
15
|
-
const match = line.match(/^
|
|
15
|
+
const match = line.match(/^[ \t]*(`{3,}|~{3,})/);
|
|
16
16
|
const sequence = match?.[1];
|
|
17
17
|
if (!sequence)
|
|
18
18
|
return undefined;
|
|
@@ -22,43 +22,26 @@ function getTopLevelOpeningFence(line) {
|
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
24
|
function isTopLevelClosingFence(line, fence) {
|
|
25
|
-
const match = line.match(/^
|
|
25
|
+
const match = line.match(/^[ \t]*(`{3,}|~{3,})[ \t]*$/);
|
|
26
26
|
const sequence = match?.[1];
|
|
27
27
|
return (!!sequence &&
|
|
28
28
|
sequence[0] === fence.marker &&
|
|
29
29
|
sequence.length >= fence.length);
|
|
30
30
|
}
|
|
31
|
-
export function
|
|
31
|
+
export function collectTelegramHtmlComments(markdown) {
|
|
32
32
|
const comments = [];
|
|
33
33
|
let offset = 0;
|
|
34
|
-
let fence;
|
|
35
34
|
while (offset < markdown.length) {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
fence = nextFence;
|
|
47
|
-
offset = lineEnd;
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
if (line.startsWith("<!--")) {
|
|
51
|
-
const closeIndex = markdown.indexOf("-->", offset + 4);
|
|
52
|
-
if (closeIndex === -1)
|
|
53
|
-
return { comments, openCommentStart: offset };
|
|
54
|
-
const end = closeIndex + 3;
|
|
55
|
-
const raw = markdown.slice(offset, end);
|
|
56
|
-
const content = raw.slice(4, -3);
|
|
57
|
-
comments.push({ raw, content, start: offset, end });
|
|
58
|
-
offset = getMarkdownLineEnd(markdown, end);
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
offset = lineEnd;
|
|
35
|
+
const start = markdown.indexOf("<!--", offset);
|
|
36
|
+
if (start === -1)
|
|
37
|
+
break;
|
|
38
|
+
const closeIndex = markdown.indexOf("-->", start + 4);
|
|
39
|
+
if (closeIndex === -1)
|
|
40
|
+
return { comments, openCommentStart: start };
|
|
41
|
+
const end = closeIndex + 3;
|
|
42
|
+
const raw = markdown.slice(start, end);
|
|
43
|
+
comments.push({ raw, content: raw.slice(4, -3), start, end });
|
|
44
|
+
offset = end;
|
|
62
45
|
}
|
|
63
46
|
return { comments };
|
|
64
47
|
}
|
|
@@ -92,7 +75,7 @@ export function replaceTelegramButtonFences(markdown, replace) {
|
|
|
92
75
|
}
|
|
93
76
|
else {
|
|
94
77
|
fence = getTopLevelOpeningFence(line);
|
|
95
|
-
if (fence &&
|
|
78
|
+
if (fence && /^[ \t]*(?:`{3,}|~{3,})[ \t]*telegram_button[ \t]*$/.test(line)) {
|
|
96
79
|
actionStart = offset;
|
|
97
80
|
contentStart = end;
|
|
98
81
|
}
|
|
@@ -105,8 +88,8 @@ export function replaceTelegramButtonFences(markdown, replace) {
|
|
|
105
88
|
}
|
|
106
89
|
return result + markdown.slice(copied);
|
|
107
90
|
}
|
|
108
|
-
export function
|
|
109
|
-
const { comments } =
|
|
91
|
+
export function replaceTelegramHtmlComments(markdown, replacer) {
|
|
92
|
+
const { comments } = collectTelegramHtmlComments(markdown);
|
|
110
93
|
if (comments.length === 0)
|
|
111
94
|
return markdown;
|
|
112
95
|
let result = "";
|
|
@@ -118,34 +101,9 @@ export function replaceTopLevelHtmlComments(markdown, replacer) {
|
|
|
118
101
|
}
|
|
119
102
|
return result + markdown.slice(offset);
|
|
120
103
|
}
|
|
121
|
-
function
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
return openCommentStart;
|
|
125
|
-
let offset = 0;
|
|
126
|
-
let fence;
|
|
127
|
-
while (offset < markdown.length) {
|
|
128
|
-
const lineEnd = getMarkdownLineEnd(markdown, offset);
|
|
129
|
-
const line = getMarkdownLineText(markdown, offset, lineEnd);
|
|
130
|
-
const isLastLine = lineEnd >= markdown.length;
|
|
131
|
-
if (fence) {
|
|
132
|
-
if (isTopLevelClosingFence(line, fence))
|
|
133
|
-
fence = undefined;
|
|
134
|
-
offset = lineEnd;
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
const nextFence = getTopLevelOpeningFence(line);
|
|
138
|
-
if (nextFence) {
|
|
139
|
-
fence = nextFence;
|
|
140
|
-
offset = lineEnd;
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
if (isLastLine && (line === "<" || line === "<!" || line === "<!-")) {
|
|
144
|
-
return offset;
|
|
145
|
-
}
|
|
146
|
-
offset = lineEnd;
|
|
147
|
-
}
|
|
148
|
-
return -1;
|
|
104
|
+
function findOpenOrPartialHtmlCommentIndex(markdown) {
|
|
105
|
+
const start = markdown.indexOf("<!--");
|
|
106
|
+
return start >= 0 ? start : (markdown.match(/<(?:!-?)?$/u)?.index ?? -1);
|
|
149
107
|
}
|
|
150
108
|
function parseTolerantTelegramAttributes(source, names) {
|
|
151
109
|
const attributes = {};
|
|
@@ -592,7 +550,7 @@ function stripTelegramHtmlCommentBlocks(markdown) {
|
|
|
592
550
|
}
|
|
593
551
|
export function stripTelegramCommentMarkupForPreview(markdown) {
|
|
594
552
|
const withoutClosedBlocks = stripTelegramHtmlCommentBlocks(replaceTelegramButtonFences(markdown, () => ""));
|
|
595
|
-
const openBlockIndex =
|
|
553
|
+
const openBlockIndex = findOpenOrPartialHtmlCommentIndex(withoutClosedBlocks);
|
|
596
554
|
const previewMarkdown = openBlockIndex >= 0
|
|
597
555
|
? withoutClosedBlocks.slice(0, openBlockIndex)
|
|
598
556
|
: withoutClosedBlocks;
|
|
@@ -600,7 +558,7 @@ export function stripTelegramCommentMarkupForPreview(markdown) {
|
|
|
600
558
|
}
|
|
601
559
|
export function stripTelegramCommentMarkupForDelivery(markdown) {
|
|
602
560
|
const withoutClosedBlocks = stripTelegramHtmlCommentBlocks(markdown);
|
|
603
|
-
const openBlockIndex =
|
|
561
|
+
const openBlockIndex = findOpenOrPartialHtmlCommentIndex(withoutClosedBlocks);
|
|
604
562
|
const deliveryMarkdown = openBlockIndex >= 0
|
|
605
563
|
? withoutClosedBlocks.slice(0, openBlockIndex)
|
|
606
564
|
: withoutClosedBlocks;
|
|
@@ -617,7 +575,7 @@ export function planTelegramVoiceReply(markdown) {
|
|
|
617
575
|
const voiceReplies = [];
|
|
618
576
|
let lang;
|
|
619
577
|
let rate;
|
|
620
|
-
const stripped =
|
|
578
|
+
const stripped = replaceTelegramHtmlComments(markdown, (comment) => {
|
|
621
579
|
const command = "telegram_voice";
|
|
622
580
|
const normalizedContent = comment.content
|
|
623
581
|
.replace(/^\s+/, "")
|
|
@@ -412,10 +412,10 @@ export function createTelegramOutboundReplyPlanner(store, getRenderingMode = ()
|
|
|
412
412
|
rendering: getRenderingMode(),
|
|
413
413
|
...(options?.binding ? { binding: options.binding } : {}),
|
|
414
414
|
});
|
|
415
|
-
//
|
|
416
|
-
const voiceReply = planTelegramVoiceReply(
|
|
415
|
+
// Extract voice from the original text before button-fence rendering consumes it.
|
|
416
|
+
const voiceReply = planTelegramVoiceReply(markdown);
|
|
417
417
|
return {
|
|
418
|
-
markdown:
|
|
418
|
+
markdown: stripTelegramCommentMarkupForDelivery(buttonReply.markdown),
|
|
419
419
|
...(buttonReply.replyMarkup
|
|
420
420
|
? { replyMarkup: buttonReply.replyMarkup }
|
|
421
421
|
: {}),
|
|
@@ -9,7 +9,7 @@ export declare const TELEGRAM_DISCONNECTED_CONTEXT_MESSAGE = "Telegram session d
|
|
|
9
9
|
export declare const TELEGRAM_ATTACH_PROMPT_SNIPPET = "Queue files for the active Telegram reply; outside Telegram turns, send files directly to Telegram.";
|
|
10
10
|
export declare const TELEGRAM_ATTACH_PROMPT_GUIDELINES: readonly ["When handling a [telegram] message and the user asked for a file or generated artifact, call telegram_attach with the local path instead of only mentioning the path in text.", "When a local/TUI user explicitly asks to send a generated file to Telegram, telegram_attach can deliver it to the paired/default Telegram chat even without an active Telegram turn.", "For an explicit thread target, provide chat_id plus thread_id; registered multi-instance followers default to their assigned thread target."];
|
|
11
11
|
export declare const TELEGRAM_MESSAGE_PROMPT_SNIPPET = "Send direct Telegram Markdown text when the user explicitly asks for Telegram delivery to a concrete chat, channel, or live Pi Thread outside the normal reply flow.";
|
|
12
|
-
export declare const TELEGRAM_MESSAGE_PROMPT_GUIDELINES: readonly ["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.", "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.", "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.", "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.", "Add buttons by embedding the same
|
|
12
|
+
export declare const TELEGRAM_MESSAGE_PROMPT_GUIDELINES: readonly ["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.", "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.", "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.", "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.", "Add buttons by embedding the same telegram_button HTML comments used in normal Telegram replies; Telegram does not support standalone buttons.", "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."];
|
|
13
13
|
export interface TelegramModelContextAvailabilityMemory {
|
|
14
14
|
suspended: boolean;
|
|
15
15
|
toolNames: Set<string>;
|
|
@@ -24,7 +24,7 @@ export const TELEGRAM_MESSAGE_PROMPT_GUIDELINES = [
|
|
|
24
24
|
"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.",
|
|
25
25
|
"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.",
|
|
26
26
|
"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.",
|
|
27
|
-
"Add buttons by embedding the same
|
|
27
|
+
"Add buttons by embedding the same telegram_button HTML comments used in normal Telegram replies; Telegram does not support standalone buttons.",
|
|
28
28
|
"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.",
|
|
29
29
|
];
|
|
30
30
|
const TELEGRAM_TOOL_METADATA_LINES = Object.fromEntries([
|
|
@@ -119,6 +119,7 @@ export interface TelegramTypingLoopStarterDeps<TContext> extends TelegramRuntime
|
|
|
119
119
|
sendTypingAction: (chatId: number, options?: {
|
|
120
120
|
message_thread_id?: number;
|
|
121
121
|
assertAuthority?: () => void;
|
|
122
|
+
isCancelled?: () => boolean;
|
|
122
123
|
}) => Promise<unknown>;
|
|
123
124
|
updateStatus: (ctx: TContext, error?: string) => void;
|
|
124
125
|
isContextActive?: (ctx: TContext) => boolean;
|
|
@@ -236,6 +236,7 @@ export function createTelegramTypingLoopStarter(deps) {
|
|
|
236
236
|
try {
|
|
237
237
|
await deps.sendTypingAction(targetChatId, {
|
|
238
238
|
...actionOptions,
|
|
239
|
+
isCancelled: () => !active,
|
|
239
240
|
// This stays process-local; adapters recheck it before HTTP/IPC issuance, not just loop ticks.
|
|
240
241
|
assertAuthority() {
|
|
241
242
|
if (!active || !canContinue())
|
|
@@ -304,10 +304,10 @@ export interface TelegramApiCallOptions {
|
|
|
304
304
|
}
|
|
305
305
|
export declare class TelegramApiAuthorityError extends Error {
|
|
306
306
|
readonly requestIssued: boolean;
|
|
307
|
-
constructor(requestIssued: boolean);
|
|
307
|
+
constructor(requestIssued: boolean, options?: ErrorOptions);
|
|
308
308
|
}
|
|
309
309
|
/** Recheck caller authority, classifying refusal by whether a Bot API request was already issued. */
|
|
310
|
-
export declare function assertTelegramApiCallAuthority(assertAuthority: (() => void) | undefined, requestIssued: boolean): void;
|
|
310
|
+
export declare function assertTelegramApiCallAuthority(assertAuthority: (() => void) | undefined, requestIssued: boolean, errorOptions?: ErrorOptions): void;
|
|
311
311
|
export interface TelegramFileDownloadOptions {
|
|
312
312
|
signal?: AbortSignal;
|
|
313
313
|
maxFileSizeBytes?: number;
|
|
@@ -444,6 +444,8 @@ export interface TelegramBridgeApiRuntimeDeps {
|
|
|
444
444
|
/** Typing issuance guard is local authority, never Bot API body or IPC data. */
|
|
445
445
|
export interface TelegramTypingActionOptions extends Pick<TelegramApiCallOptions, "assertAuthority"> {
|
|
446
446
|
message_thread_id?: number;
|
|
447
|
+
/** Diagnostic observation only: an ended loop does not grant issuance, success or retry. */
|
|
448
|
+
isCancelled?: () => boolean;
|
|
447
449
|
}
|
|
448
450
|
export interface TelegramBridgeApiRuntime extends Pick<TelegramApiClient, "call" | "callMultipart"> {
|
|
449
451
|
downloadFile: (fileId: string, suggestedName: string, source?: TelegramAttachmentSource) => Promise<string>;
|
|
@@ -43,19 +43,19 @@ const TELEGRAM_NETWORK_FAMILY_VALUES = new Set([
|
|
|
43
43
|
]);
|
|
44
44
|
export class TelegramApiAuthorityError extends Error {
|
|
45
45
|
requestIssued;
|
|
46
|
-
constructor(requestIssued) {
|
|
47
|
-
super("Telegram API call authority is unavailable.");
|
|
46
|
+
constructor(requestIssued, options) {
|
|
47
|
+
super("Telegram API call authority is unavailable.", options);
|
|
48
48
|
this.name = "TelegramApiAuthorityError";
|
|
49
49
|
this.requestIssued = requestIssued;
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
/** Recheck caller authority, classifying refusal by whether a Bot API request was already issued. */
|
|
53
|
-
export function assertTelegramApiCallAuthority(assertAuthority, requestIssued) {
|
|
53
|
+
export function assertTelegramApiCallAuthority(assertAuthority, requestIssued, errorOptions) {
|
|
54
54
|
try {
|
|
55
55
|
assertAuthority?.();
|
|
56
56
|
}
|
|
57
57
|
catch {
|
|
58
|
-
throw new TelegramApiAuthorityError(requestIssued);
|
|
58
|
+
throw new TelegramApiAuthorityError(requestIssued, errorOptions);
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
function parseTelegramApiTargetInteger(value) {
|
|
@@ -765,7 +765,7 @@ async function callTelegramWithRetry(method, request, options) {
|
|
|
765
765
|
// Authority refusal is never a transport failure, retry grant or acknowledged success.
|
|
766
766
|
if (error instanceof TelegramApiAuthorityError)
|
|
767
767
|
throw error;
|
|
768
|
-
assertTelegramApiCallAuthority(assertAuthority, requestIssued);
|
|
768
|
+
assertTelegramApiCallAuthority(assertAuthority, requestIssued, { cause: error });
|
|
769
769
|
const retryable = isRetryableTelegramApiError(error) &&
|
|
770
770
|
!(options?.retryRateLimit === false &&
|
|
771
771
|
error instanceof TelegramApiHttpError &&
|
|
@@ -1039,6 +1039,7 @@ export function createTelegramBridgeApiRuntime(deps) {
|
|
|
1039
1039
|
const chatActionMinIntervalMs = Math.max(0, deps.chatActionMinIntervalMs ?? 2_000);
|
|
1040
1040
|
const chatActionMaxGates = Math.max(1, deps.chatActionMaxGates ?? 256);
|
|
1041
1041
|
const chatActionGates = new Map();
|
|
1042
|
+
// Pin shared cooldowns until every concurrent Thread settles; this state never owns their sends.
|
|
1042
1043
|
const chatActionChatGates = new Map();
|
|
1043
1044
|
const getChatActionKeys = (method, body) => {
|
|
1044
1045
|
if (method !== "sendChatAction")
|
|
@@ -1069,7 +1070,7 @@ export function createTelegramBridgeApiRuntime(deps) {
|
|
|
1069
1070
|
}
|
|
1070
1071
|
}
|
|
1071
1072
|
for (const [key, candidate] of chatActionChatGates) {
|
|
1072
|
-
if (
|
|
1073
|
+
if (candidate.pendingRequests === 0 && nowMs >= candidate.notBeforeMs) {
|
|
1073
1074
|
chatActionChatGates.delete(key);
|
|
1074
1075
|
}
|
|
1075
1076
|
}
|
|
@@ -1085,13 +1086,11 @@ export function createTelegramBridgeApiRuntime(deps) {
|
|
|
1085
1086
|
if (chatActionChatGates.size >= chatActionMaxGates) {
|
|
1086
1087
|
return true;
|
|
1087
1088
|
}
|
|
1088
|
-
chatGate = { notBeforeMs: 0 };
|
|
1089
|
+
chatGate = { pendingRequests: 0, notBeforeMs: 0 };
|
|
1089
1090
|
chatActionChatGates.set(chatActionKeys.chat, chatGate);
|
|
1090
1091
|
}
|
|
1091
1092
|
if (gate.inFlight)
|
|
1092
1093
|
return (await gate.inFlight);
|
|
1093
|
-
if (chatGate.inFlight)
|
|
1094
|
-
return true;
|
|
1095
1094
|
if (nowMs < gate.notBeforeMs || nowMs < chatGate.notBeforeMs) {
|
|
1096
1095
|
return true;
|
|
1097
1096
|
}
|
|
@@ -1121,17 +1120,19 @@ export function createTelegramBridgeApiRuntime(deps) {
|
|
|
1121
1120
|
}));
|
|
1122
1121
|
return true;
|
|
1123
1122
|
}
|
|
1124
|
-
|
|
1123
|
+
const diagnosticError = error instanceof TelegramApiAuthorityError && Object.hasOwn(error, "cause")
|
|
1124
|
+
? error.cause : error;
|
|
1125
|
+
if (!options?.isExpectedError?.(error))
|
|
1126
|
+
deps.recordRuntimeEvent("api", diagnosticError, withTelegramTransportDiagnostics(diagnosticError, { method }));
|
|
1125
1127
|
throw error;
|
|
1126
1128
|
})
|
|
1127
1129
|
.finally(() => {
|
|
1128
1130
|
if (gate.inFlight === request)
|
|
1129
1131
|
gate.inFlight = undefined;
|
|
1130
|
-
|
|
1131
|
-
chatGate.inFlight = undefined;
|
|
1132
|
+
chatGate.pendingRequests--;
|
|
1132
1133
|
});
|
|
1133
1134
|
gate.inFlight = request;
|
|
1134
|
-
chatGate.
|
|
1135
|
+
chatGate.pendingRequests++;
|
|
1135
1136
|
return request;
|
|
1136
1137
|
}
|
|
1137
1138
|
try {
|
|
@@ -1196,13 +1197,21 @@ export function createTelegramBridgeApiRuntime(deps) {
|
|
|
1196
1197
|
? { message_thread_id: options.message_thread_id }
|
|
1197
1198
|
: {}),
|
|
1198
1199
|
}),
|
|
1199
|
-
sendTypingAction: createTelegramChatActionSender((chatId, action, options) =>
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1200
|
+
sendTypingAction: createTelegramChatActionSender((chatId, action, options) => {
|
|
1201
|
+
const isCancelled = options?.isCancelled;
|
|
1202
|
+
return callRecorded("sendChatAction", {
|
|
1203
|
+
chat_id: chatId,
|
|
1204
|
+
action,
|
|
1205
|
+
...(options?.message_thread_id !== undefined
|
|
1206
|
+
? { message_thread_id: options.message_thread_id }
|
|
1207
|
+
: {}),
|
|
1208
|
+
}, {
|
|
1209
|
+
assertAuthority: options?.assertAuthority,
|
|
1210
|
+
...(isCancelled ? {
|
|
1211
|
+
isExpectedError: (error) => error instanceof TelegramApiAuthorityError && !Object.hasOwn(error, "cause") && isCancelled() === true,
|
|
1212
|
+
} : {}),
|
|
1213
|
+
});
|
|
1214
|
+
}, "typing"),
|
|
1206
1215
|
sendRecordVoiceAction: createTelegramChatActionSender((chatId, action, options) => callRecorded("sendChatAction", {
|
|
1207
1216
|
chat_id: chatId,
|
|
1208
1217
|
action,
|
|
@@ -16,12 +16,12 @@ Use Telegram as a mobile companion to the current Pi session. Preserve the exact
|
|
|
16
16
|
| Explicitly send from local/TUI to Telegram | `telegram_message` or `telegram_attach` |
|
|
17
17
|
| Explicitly send to a different live Thread | `telegram_message(thread=...)` |
|
|
18
18
|
| Add buttons | Hidden comment for footer; `telegram_button` fence for in-body rows |
|
|
19
|
-
| Add explicit voice |
|
|
19
|
+
| Add explicit voice | Hidden action comment |
|
|
20
20
|
| Build a repeated deterministic interaction | Follow `generative-apps` |
|
|
21
21
|
|
|
22
22
|
A connected Telegram session proves capability, not user intent. Use Telegram features on Telegram-originated turns or explicit Telegram delivery requests only. Never call `telegram_message` for the current active target.
|
|
23
23
|
|
|
24
|
-
For
|
|
24
|
+
For delivery, Thread routing, configuration or diagnosis, read the applicable reference under [Conditional References](#conditional-references).
|
|
25
25
|
|
|
26
26
|
## Turn Context
|
|
27
27
|
|
|
@@ -40,7 +40,7 @@ Reply in concise, phone-width Telegram Rich Markdown. Use `$...$` and `$$...$$`
|
|
|
40
40
|
|
|
41
41
|
## Assistant Actions
|
|
42
42
|
|
|
43
|
-
`telegram_button` and `telegram_voice` are markup, not tools.
|
|
43
|
+
`telegram_button` and `telegram_voice` are markup, not tools. Complete comments activate regardless of surrounding Markdown. Escape `<!--` as `<!--` for literal examples; backticks and backslashes do not disable actions. Comments create footer buttons or voice artifacts. In-body fences: ≥3 backticks/tildes, optional indentation/whitespace, exact `telegram_button`; separate same-marker closure, no shorter. Both wrappers accept a singleton JSON/CML cell or mixed matrix; the wrapper determines placement. Telegram removes every assistant-authored HTML comment from previews and final replies regardless of owner or Markdown position; only complete recognized comments or valid button fences activate actions; comment-only output sends no text message.
|
|
44
44
|
|
|
45
45
|
### Shared Encoding Rule
|
|
46
46
|
|
|
@@ -61,7 +61,7 @@ Every enabled button has a self-contained prompt and an optional selection style
|
|
|
61
61
|
- `{label|prompt|selected_style}` and `{|prompt|selected_style}` accept `primary`, `success`, or `danger`.
|
|
62
62
|
- Fourth-position `1`/`true` disables, `0`/`false` enables; omission means enabled. JSON uses boolean `disabled`. `{|Next||1}` omits label/style; `{Next|||1}` omits prompt/style; `{|||1}` is blank (Telegram receives a non-breaking space). Prefer meaningful labels and retain a useful enabled action. Enabled CML requires a prompt. Disabled controls stay visible but have no callback, queued prompt, or bound-method invocation.
|
|
63
63
|
- Both wrappers concatenate rows from adjacent JSON/CML cells or matrices, with optional commas. Cells are vertical; horizontal peers require a nested row: `[[{A|a}{B|b}]]`.
|
|
64
|
-
- Prefer one matrix per related group. Fenced blocks stay in place in Rich mode; HTML compatibility moves them to the footer. Native rows allow eight buttons. Malformed/oversized/unclosed blocks activate nothing; drafts hide them.
|
|
64
|
+
- Prefer one matrix per related group. Fenced blocks stay in place in Rich mode; HTML compatibility moves them to the footer. Native rows allow eight buttons. Malformed/oversized/unclosed blocks activate nothing; drafts hide them. Enclosing code fences and quoted/list-prefixed button fences remain literal.
|
|
65
65
|
- Both placements share prompt/app routing. In-body clicks acknowledge without recoloring; selected-style highlighting remains footer-only.
|
|
66
66
|
|
|
67
67
|
A single in-body button (the four-backtick wrapper below makes this a literal example):
|
|
@@ -124,6 +124,6 @@ Before replying:
|
|
|
124
124
|
|
|
125
125
|
- Use the ordinary path for the current target and direct tools only for explicit other delivery.
|
|
126
126
|
- Attach requested files rather than only mentioning them.
|
|
127
|
-
- Keep
|
|
127
|
+
- Keep comments complete and button fences top-level and canonical: CML first, JSON when necessary; either syntax may coexist within one matrix.
|
|
128
128
|
- Give every enabled button a self-contained prompt; preserve confirmation for dangerous actions.
|
|
129
129
|
- Expose no secret or hidden reasoning.
|
|
@@ -42,7 +42,7 @@ Keep this boundary explicit:
|
|
|
42
42
|
|
|
43
43
|
### Instance, Session, And Context Cost
|
|
44
44
|
|
|
45
|
-
Live Telegram routing belongs to the active authenticated Pi instance, while a durable Workspace Thread belongs to the exact profile/CWD/session identity. Ordinary Telegram prompts enter the session currently bound to that target. Local `/resume` replaces session-scoped runtime state and selects the destination session's binding; it never inherits the source Thread merely because the process is unchanged. Telegram exposes compaction and new-session replacement for the active session: `/new` first completes and removes its exact durable update, then dispatches
|
|
45
|
+
Live Telegram routing belongs to the active authenticated Pi instance, while a durable Workspace Thread belongs to the exact profile/CWD/session identity. Ordinary Telegram prompts enter the session currently bound to that target. Local `/resume` replaces session-scoped runtime state and selects the destination session's binding; it never inherits the source Thread merely because the process is unchanged. Telegram exposes compaction and new-session replacement for the active session: `/new` first completes and removes its exact durable update, then dispatches the runtime-armed `/telegram-connect --internal new <token>` branch through `pi.sendUserMessage(..., { expandPromptTemplates: true })`, without entering ordinary Connect profile or transport startup, so the handler runs with a real `ExtensionCommandContext` and calls the same `AgentSessionRuntime.newSession()` path as the terminal. Before invoking that path, the current transport owner CAS-publishes one expiring replacement intent inside the existing profile target snapshot. The intent discriminates `workspace-thread` from `classic-chat`. A same- or cross-process successor may consume either only for the exact profile, CWD, source session, target, and fresh lifetime: Threaded continuity re-keys the retained Workspace binding and preserves Thread, slot, and display identity, while Classic continuity preserves the exact chat target without creating a Workspace binding or entering topic lifecycle APIs. A confirmed callback is acknowledged and its dialog is deleted before session replacement begins. Once the successor has delivery authority, it atomically claims the intent by CAS-clearing it and then sends one separate terminal success result with bounded in-process transport retry. The old `withSession` path never publishes success, and an unclaimed intent never sends, preventing duplicate terminal notices across same-process startup and cleanup-ack ambiguity. Mismatch or expiry never authorizes binding takeover. Resume, fork, tree navigation, session switching, and full reload remain outside the stable Telegram API until Pi exposes safe public extension hooks for them.
|
|
46
46
|
|
|
47
47
|
`/telegram-connect` never launches a hidden or headless Pi process. A long-lived background Pi process can own Telegram only when something else explicitly launched that process and it satisfies the normal lock/runtime rules. Pi `print` and `json` modes stay passive and exit rather than becoming hidden polling owners.
|
|
48
48
|
|
|
@@ -106,7 +106,7 @@ Only the live cleanup origin uses the approved historical-protection reduction.
|
|
|
106
106
|
- `preview` / `replies` / `rendering`: throttled native Rich Markdown draft delivery, native final reply delivery, reply parameters, transport-limit chunking, and remaining Telegram HTML rendering (bold, italic, strikethrough, spoilers, code, links) for bridge-owned UI/compatibility surfaces.
|
|
107
107
|
- `delivery`: public extension operational-view delivery, active-turn/instance/aggregate/authorized target policy, logical chunk handles, per-target ordering, runtime generation fencing, and the process-local runtime membrane. Its bridge adapter composes the established UI/compat reply renderer with narrow bus-aware Telegram API and ownership ports; it never exposes bot clients or Pi contexts.
|
|
108
108
|
- `activity`: public normalized Pi lifecycle registration, activity/source identity, assistant segment and reasoning normalization, executed-tool events, non-blocking per-handler queues, delivery contexts, compatibility adapters, and shutdown fencing. Internal publication/command-work and handler drain notifications share the lifecycle-owned captured-session/transport observer with companion queues and API target counters; bindings/root wire those neutral ports into routing's existing leader cleanup scheduler, never into deletion authority. Routing retains busy/unproven-idle delivery hints without early admission, preserves ordinary timers, and lets semantic settlement or post-settlement output drain request fresh checks. The same domain extends assistant-output observation for connected companion projection: eligible completed local/autonomous public segments retain source order and deduplicate event identity. `bindings` assembles observation, authority, sender, and failure-projection ports; routing owns exact delivery authority, outbound composes established transformations and reply delivery, and Bot API domains implement transport. No separate proactive state-machine domain exists.
|
|
109
|
-
- `outbound-markup`:
|
|
109
|
+
- `outbound-markup`: delimiter-based assistant action comments, top-level button fence parsing, shared JSON/CML grammar, attribute parsing, voice reply planning, and preview/delivery stripping.
|
|
110
110
|
- `outbound`: outbound text transformations, voice/button artifact delivery, and generated callback actions.
|
|
111
111
|
- `generative-apps`: managed deterministic application identity, canonical installation and explicit replacement, content-addressed module loading, state timelines, cross-process transition serialization, bounded executable-plus-argv adaptation, `telegram_bind`, and pre-model-queue `app::method` invocation. It does not own Telegram transport, arbitrary shell execution, or the external application adapted by one Generative App.
|
|
112
112
|
- `outbound-attachments`: `telegram_attach`, queued outbound files, stat/limit checks, ordinary photo/document delivery, and narrow single-artifact Rich Message planning/sending for probe-confirmed photo/video/audio formats. It owns known-failure fallback eligibility and ambiguous-send no-replay classification through structural error contracts without importing Bot API helpers.
|
|
@@ -685,7 +685,7 @@ Native typing during compaction follows connected-instance activity rather than
|
|
|
685
685
|
- Thread-targeted typing is sent only to the concrete thread. Aggregate `All` mirroring is intentionally omitted because duplicating every keepalive multiplies shared-chat flood pressure. Compaction completion, failure, or timeout stops only a loop actually started by the compaction observer; a pre-existing agent-owned loop remains active. Ownership is sticky across a repeated compaction start, whose shared-loop start reports `false` for the observer's own running loop. Authority loss and shutdown still stop the keyed loop.
|
|
686
686
|
- Pi `ui_prompt_start` pauses typing while an extension-owned local prompt waits for the operator; `ui_prompt_end` emits the matching Activity boundary and resumes typing whenever agent or compaction work remains unsettled.
|
|
687
687
|
|
|
688
|
-
At every connected instance `agent_start`, the lifecycle binding starts Telegram's native `…typing` indicator in that instance's assigned target, whether the run came from Telegram, the local TUI, or an autonomous continuation such as Grow Loop. Terminal `Active` remains Telegram-turn-specific; the native indicator answers the separate question of whether the instance is doing agent work. Streaming keepalives reuse a valid typing owner with the same session generation, transport and target rather than revoking its pending HTTP/IPC action. Fresh context wrappers may reuse that owner within a proven generation; without generation evidence, context identity must match. Explicit queue dispatch and Telegram prompt handoff still replace the owner. Queue dispatch carries the queued turn's exact target into typing before `agent_start`, rather than reducing it to a chat ID. In Threaded Mode, missing or invalid Thread targets are refused before starting a loop or invoking the API; mode changes also invalidate a captured Classic chat-only loop. A rejected start never replaces an existing valid Thread loop. The starter also requires the captured Pi context and session generation to remain current at start, queued send, tick and late failure boundaries, even if the transport epoch is unchanged; replacing a context object in place cannot revive old activity. Message activity hooks capture generation across awaited preview work, and agent-start bindings recheck it before post-await activity/typing, so an obsolete finally or continuation cannot acquire replacement ownership. The typing sender carries its captured loop/context/generation/transport assertion as process-local call authority: direct API issuance rechecks it after queued work, and the follower caller rechecks it after registration waits and immediately before socket issuance without putting callbacks in IPC data. Automatic typing uses one HTTP attempt with no transport-family fallback or delayed replay, including forwarded calls; a future keepalive is a fresh owner-checked attempt. An already issued HTTP/RPC cannot be retroactively unsent. Each loop refreshes its exact target every three seconds and keeps one action in flight. The leader API runtime coalesces identical chat/thread/action calls for two seconds,
|
|
688
|
+
At every connected instance `agent_start`, the lifecycle binding starts Telegram's native `…typing` indicator in that instance's assigned target, whether the run came from Telegram, the local TUI, or an autonomous continuation such as Grow Loop. Terminal `Active` remains Telegram-turn-specific; the native indicator answers the separate question of whether the instance is doing agent work. Streaming keepalives reuse a valid typing owner with the same session generation, transport and target rather than revoking its pending HTTP/IPC action. Fresh context wrappers may reuse that owner within a proven generation; without generation evidence, context identity must match. Explicit queue dispatch and Telegram prompt handoff still replace the owner. Queue dispatch carries the queued turn's exact target into typing before `agent_start`, rather than reducing it to a chat ID. In Threaded Mode, missing or invalid Thread targets are refused before starting a loop or invoking the API; mode changes also invalidate a captured Classic chat-only loop. A rejected start never replaces an existing valid Thread loop. The starter also requires the captured Pi context and session generation to remain current at start, queued send, tick and late failure boundaries, even if the transport epoch is unchanged; replacing a context object in place cannot revive old activity. Message activity hooks capture generation across awaited preview work, and agent-start bindings recheck it before post-await activity/typing, so an obsolete finally or continuation cannot acquire replacement ownership. The typing sender carries its captured loop/context/generation/transport assertion as process-local call authority: direct API issuance rechecks it after queued work, and the follower caller rechecks it after registration waits and immediately before socket issuance without putting callbacks in IPC data. Automatic typing uses one HTTP attempt with no transport-family fallback or delayed replay, including forwarded calls; a future keepalive is a fresh owner-checked attempt. An already issued HTTP/RPC cannot be retroactively unsent. An ended loop supplies a process-local cancellation observation: only a typed API authority refusal with no underlying failure and paired with that proof is omitted from API failure logs, while the call still rejects without success or retry. If authority expires while handling a real HTTP/network failure, the authority error retains that failure as its cause and the API recorder logs the original failure. Active-owner refusals and HTTP/network failures keep their diagnostics; the cancellation observation never crosses IPC. Each loop refreshes its exact target every three seconds and keeps one action in flight. The leader API runtime coalesces identical chat/thread/action calls for two seconds, admits different Threads independently, and shares a real Telegram 429 `retry_after` fence across every Thread key in that chat. A pending-request count retains that shared state until every concurrent action settles, so completion of a faster peer cannot orphan a late 429; expired gates prune opportunistically and each gate family retains at most 256 active keys. Suppression never schedules a delayed retry. A failed typing action remains a structured diagnostic but cannot project `error` onto an otherwise healthy connected/leader/follower status. Assistant message start/update hooks still re-arm typing during Telegram-owned turns so transient provider/model errors do not leave a continuing run without activity feedback, and agent/session completion stops it.
|
|
689
689
|
|
|
690
690
|
### Rendering And Delivery
|
|
691
691
|
|
|
@@ -714,14 +714,14 @@ Outbound files staged during an active Telegram turn are delivered after that tu
|
|
|
714
714
|
|
|
715
715
|
The channel-post journal records only this agent path's own publication intent. `prepare` is exact and idempotent; `beginPublication` durably changes one prepared operation to outcome-unknown before any future non-idempotent send; only `confirmPublished` records the returned numeric channel/message identity. A retry never regains issuance from outcome-unknown or published state. The store validates profile and token fingerprint, refuses malformed/duplicate/over-capacity state, and lists newest retained records without claiming Telegram history. The public-`@username` direct-leader sender now uses the tool-call ID to prepare and begin this journal transition before `sendRichMessage`, then records the returned numeric channel/message identity plus bounded username/title observed by `getChat`. Retained outcome-unknown state refuses automatic replay. Bounded agent-facing listing is available locally. Exact edit/delete journal transitions fence each tool-call mutation as outcome-unknown before direct-leader `editMessageText` or `deleteMessage`, and require the same mutation ID to confirm edited or deleted state. Numeric-channel publication requires explicit `channel: true`; direct-leader `getChat` must prove the exact negative ID and channel type before issuance, and the send response must preserve that identity. Cross-process regressions prove one winner for concurrent publication/edit/delete issuance and no restart regrant. Reads fail closed before parsing links, foreign/loose files, unsupported no-follow platforms, identity races, or oversized input. No legacy post schema is migrated: absent state starts empty and unknown versions/fields fail closed. The 0.44 downgrade checker leaves this inert journal untouched because old code cannot issue its effects. Only an explicit successful list returns retained authored Markdown. Channel publication/list/mutation failures and runtime events use fixed messages without retained content, token, path, or arbitrary transport detail. Production-helper regressions model a lost successful caller ACK and an ambiguous send response: the same operation returns retained success or refuses outcome-unknown without a second transport call. The retry uses a freshly opened replacement store, proving durable behavior across helper replacement/restart. A native entrypoint regression disconnects/reconnects direct ownership and proves both retained success and ambiguous outcome avoid a second `sendRichMessage`. Sender-rights and live rollout evidence remain separate 0.45.0 work. Channel media posts upload one local `.jpg`/`.jpeg`/`.png`/`.webp` photo or `.mp4` video through the multipart transport with `text` as the HTML caption; kind, byte size (photo ≤ 10 MiB, video ≤ 50 MiB), and 1024 visible caption characters are validated before issuance, and unsupported types or albums are rejected rather than downgraded to links. The journal binds the inspected kind/file name/byte size/SHA-256 media identity and caption, so duplicate requests and lost acknowledgements never re-upload. A media-post edit replaces the caption through `editMessageCaption` with the same Markdown-to-HTML rendering, where `||spoiler||` now renders as `<tg-spoiler>`.
|
|
716
716
|
|
|
717
|
-
Assistant-authored final-message actions use hidden
|
|
717
|
+
Assistant-authored final-message actions use hidden HTML comments, with an additional fenced wrapper for in-body buttons:
|
|
718
718
|
|
|
719
719
|
- `telegram_voice` accepts one positional compact action cell or JSON object and creates one voice artifact through configured outbound handlers, programmatic voice handlers, or registered synthesis providers.
|
|
720
720
|
- `telegram_button` accepts a JSON object, adaptive JSON/CML matrix, or positional Compact Matrix Literal. Named JSON objects and positional cells may coexist, with commas optional only between completed matrix or row elements. Each top-level cell creates one full-width row, while a nested row groups buttons horizontally without an artificial parser-width cap; every callback enqueues its configured prompt text as a normal Telegram prompt turn. The JSON-first grammar, positional trim/escape rules, atomic rejection, and renderer-owned width policy are specified in [Adaptive Button Literal](./compact-matrix-literal.md).
|
|
721
721
|
|
|
722
|
-
Standalone
|
|
722
|
+
Standalone `telegram_button` fenced blocks reuse the same cell/matrix grammar and callback store, including adjacent complete cells and matrices whose normalized rows concatenate in source order, and compile to native Rich Markdown button rows between paragraphs. Rendering validates each complete block before registering any callbacks, escapes label text, preserves disabled cells, and leaves enclosing fenced examples literal. Opening and closing lines allow spaces/tabs; openings accept three or more backticks or tildes, optional whitespace before the exact `telegram_button` name, and trailing whitespace. Closures must use the same marker and at least the opening length, with no other text. Previews hide complete and unfinished action fences. HTML mode projects those controls into the footer. In-body callbacks acknowledge without rewriting the Rich message; selected-style highlighting remains footer-only.
|
|
723
723
|
|
|
724
|
-
|
|
724
|
+
Complete HTML action comments activate solely by their delimiters and accepted payload, independent of whitespace, adjacent text, lists, quotes or code fences. Same-line comments are independent. Escape the opening delimiter as `<!--` to show a literal example; backticks and backslashes do not disable an action comment. Exact-name button fences retain their separate top-level recognition rules. The Telegram surface independently strips every complete assistant-authored HTML comment from previews and final delivery regardless of Markdown position or comment owner; an unclosed comment is withheld through the remaining tail, and a comment-only result sends no text message. Pi's terminal transcript and model context remain unchanged.
|
|
725
725
|
|
|
726
726
|
Unknown callback data outside owned prefixes is forwarded as `[callback] <data>` only after built-in and extension handlers decline it.
|
|
727
727
|
|
|
@@ -154,7 +154,7 @@ For `telegram_button` hidden comments and fenced blocks:
|
|
|
154
154
|
- Nested rows become horizontal keyboard rows; horizontal intent still requires the explicit nested array.
|
|
155
155
|
- Invalid payloads are stripped with their recognized action comment and register no callbacks.
|
|
156
156
|
|
|
157
|
-
The wrapper selects placement without changing cell semantics. A hidden `telegram_button` HTML comment builds the footer keyboard. A standalone
|
|
157
|
+
The wrapper selects placement without changing cell semantics. A hidden `telegram_button` HTML comment builds the footer keyboard. A standalone fenced block opened by at least three backticks or tildes plus the exact name `telegram_button` renders rows between paragraphs in Native Rich Markdown. Delimiter lines accept horizontal indentation, optional whitespace before the name, and trailing spaces/tabs. Close on a separate line using the same marker and at least the opening length, with no other text. Enclosing code fences keep button-block examples literal. A singleton JSON/CML object needs no array in either wrapper. Fenced content must be one complete group sequence without trailing envelope text; any invalid group rejects the entire block before callback registration. Larger outer fences and ordinary code blocks remain literal examples; unclosed action fences are withheld. Native Rich rows support at most eight buttons and must fit one message chunk; these are renderer constraints, not grammar limits. HTML compatibility places fenced controls in the footer. See [Outbound](./outbound.md) for delivery and callback behavior.
|
|
158
158
|
|
|
159
159
|
````markdown
|
|
160
160
|
Description.
|
|
@@ -49,7 +49,7 @@ An app name is a unique lowercase ASCII identifier accepted by the runtime's pat
|
|
|
49
49
|
|
|
50
50
|
## Inference-Bypass Syntax
|
|
51
51
|
|
|
52
|
-
Compact Matrix Literal and full JSON buttons keep their existing `label + prompt` contract. App output may use hidden `telegram_button` comments for footer controls or standalone
|
|
52
|
+
Compact Matrix Literal and full JSON buttons keep their existing `label + prompt` contract. App output may use hidden `telegram_button` comments for footer controls or standalone `telegram_button` fenced blocks between paragraphs (three or more backticks or tildes, horizontal indentation/whitespace tolerated, same-marker closure no shorter than the opening); both singleton cells and mixed matrices retain the same generation/revision-bound routing. In-body controls acknowledge clicks without recoloring the message; HTML compatibility moves them to the footer. A bound action is encoded entirely in the prompt string:
|
|
53
53
|
|
|
54
54
|
```ebnf
|
|
55
55
|
bound-action = app "::" method [ "(" json-value ")" ]
|
|
@@ -485,7 +485,7 @@ Threaded Mode should make follower threads behave like normal Telegram instance
|
|
|
485
485
|
| Replies/finals | Final replies land in the same thread | Follower finals go through leader transport into follower thread | Outbound calls carry target and inject `message_thread_id` | Reply delivery and bus API tests |
|
|
486
486
|
| Previews/Rich Drafts | Draft previews use the active thread target | Follower previews use the same native draft lifecycle through the leader | Preview transport preserves target and draft id | Preview thread-target tests |
|
|
487
487
|
| Attachments/voice | Files and voice upload in the instance thread | Follower uploads route through leader multipart transport | Multipart calls are target-scoped and follower-authorized | Bus allowlist and outbound delivery tests |
|
|
488
|
-
| Native activity status | `sendChatAction(typing)` refreshes Telegram's native `…typing` indicator only in the assigned Thread for every agent run, including local and autonomous work; aggregate `All` is not mirrored, terminal identity remains `connected`, and active work stays included in the green Queue count | Followers route the exact Thread action through leader transport while retaining their stable terminal `follower` identity and green activity count; the leader admits
|
|
488
|
+
| Native activity status | `sendChatAction(typing)` refreshes Telegram's native `…typing` indicator only in the assigned Thread for every agent run, including local and autonomous work; aggregate `All` is not mirrored, terminal identity remains `connected`, and active work stays included in the green Queue count | Followers route the exact Thread action through leader transport while retaining their stable terminal `follower` identity and green activity count; the leader admits different Threads independently, coalesces exact chat/Thread/action calls and shares real Telegram 429 cooldown across that chat's Thread keys | Agent lifecycle targets the active Telegram turn when present and otherwise the instance binding; one keyed loop avoids duplicate aggregate sends/rate-limit pressure | Agent-start binding, typing-loop, target-routing, and terminal-status regressions |
|
|
489
489
|
| Leader election / promotion | Current leader keeps its thread across reload | A promoted follower keeps its existing thread, slot, and name when elected and after later reload | Promotion converts the current follower binding into the leader profile before forced lock acquisition, so leader startup reuses it instead of provisioning a new thread | Follower heartbeat recovery passes binding snapshot into promotion; own-topic provisioner reuses promoted bindings |
|
|
490
490
|
| `/start` command/menu bootstrap | Registers visible bot commands and opens the menu | Follower `/start` can refresh the bot command menu through the leader and open its local menu without warnings | Bot command registration is a validated global Bot API call allowed through trusted follower bus transport | Bus allowlist regression for `setMyCommands` |
|
|
491
491
|
| Follower reconnect | Existing leader binding is reused only when still usable | Same-process `/new` or `/reload` suspends the old follower socket/context and automatically re-registers the new session to the exact prior target; explicit reconnect to a genuinely closed/stale Telegram tab still recreates a visible thread before success | A short-lived handoff carries the assigned target across session replacement, and the leader transfers that binding to the new runtime instance id by stable manual-follower identity; stale Bot API errors remain the proof for fresh provisioning | Session handoff/refresh, leader binding-transfer, persisted leader-reload reuse, and stale-target replacement regressions |
|