@llblab/pi-telegram 0.52.2 → 0.53.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/AGENTS.md +9 -7
- package/BACKLOG.md +1 -18
- package/CHANGELOG.md +15 -0
- package/README.md +7 -11
- package/api/commands.ts +3 -0
- package/dist/api/commands.d.ts +1 -1
- package/dist/lib/activity-verbosity.d.ts +1 -5
- package/dist/lib/activity-verbosity.js +15 -42
- package/dist/lib/activity.d.ts +9 -0
- package/dist/lib/activity.js +39 -10
- package/dist/lib/agent-messages.js +7 -3
- package/dist/lib/bindings.d.ts +43 -5
- package/dist/lib/bindings.js +163 -20
- package/dist/lib/bus-api.d.ts +12 -0
- package/dist/lib/bus-api.js +182 -79
- package/dist/lib/bus-follower.d.ts +111 -31
- package/dist/lib/bus-follower.js +1324 -359
- package/dist/lib/bus-leader.d.ts +41 -91
- package/dist/lib/bus-leader.js +969 -637
- package/dist/lib/bus-transport.js +16 -9
- package/dist/lib/bus.d.ts +215 -18
- package/dist/lib/bus.js +1048 -327
- package/dist/lib/channel-posts.d.ts +5 -0
- package/dist/lib/channel-posts.js +434 -261
- package/dist/lib/command-templates.d.ts +26 -5
- package/dist/lib/command-templates.js +92 -176
- package/dist/lib/commands.d.ts +168 -82
- package/dist/lib/commands.js +1409 -291
- package/dist/lib/config.d.ts +1 -8
- package/dist/lib/config.js +34 -76
- package/dist/lib/delivery.d.ts +2 -2
- package/dist/lib/delivery.js +16 -16
- package/dist/lib/extension.js +639 -171
- package/dist/lib/generative-app-worker.mjs +19 -7
- package/dist/lib/generative-apps.d.ts +0 -5
- package/dist/lib/generative-apps.js +61 -49
- package/dist/lib/inbound.d.ts +5 -0
- package/dist/lib/inbound.js +35 -59
- package/dist/lib/journal.d.ts +33 -3
- package/dist/lib/journal.js +2063 -782
- package/dist/lib/lifecycle.d.ts +2 -6
- package/dist/lib/lifecycle.js +75 -58
- package/dist/lib/locks.d.ts +29 -3
- package/dist/lib/locks.js +289 -108
- package/dist/lib/logging.js +50 -14
- package/dist/lib/media.d.ts +22 -1
- package/dist/lib/media.js +46 -28
- package/dist/lib/menu-model.d.ts +19 -5
- package/dist/lib/menu-model.js +53 -11
- package/dist/lib/menu-queue.d.ts +5 -4
- package/dist/lib/menu-queue.js +45 -63
- package/dist/lib/menu-settings.d.ts +5 -4
- package/dist/lib/menu-settings.js +114 -78
- package/dist/lib/menu-status.d.ts +1 -1
- package/dist/lib/menu-status.js +8 -25
- package/dist/lib/menu-thinking.d.ts +6 -1
- package/dist/lib/menu-thinking.js +14 -21
- package/dist/lib/menu.d.ts +8 -61
- package/dist/lib/menu.js +92 -164
- package/dist/lib/model.d.ts +1 -1
- package/dist/lib/model.js +2 -16
- package/dist/lib/outbound-attachments.d.ts +6 -27
- package/dist/lib/outbound-attachments.js +45 -38
- package/dist/lib/outbound-buttons.js +22 -16
- package/dist/lib/outbound-markup.d.ts +0 -2
- package/dist/lib/outbound-markup.js +51 -64
- package/dist/lib/outbound-voice.js +6 -3
- package/dist/lib/outbound.d.ts +6 -6
- package/dist/lib/outbound.js +65 -51
- package/dist/lib/ownership.js +6 -2
- package/dist/lib/paths.d.ts +5 -0
- package/dist/lib/paths.js +37 -21
- package/dist/lib/pi.d.ts +2 -0
- package/dist/lib/pi.js +8 -1
- package/dist/lib/polling.d.ts +21 -37
- package/dist/lib/polling.js +59 -50
- package/dist/lib/preview.js +39 -19
- package/dist/lib/process-identity.d.ts +24 -0
- package/dist/lib/process-identity.js +133 -0
- package/dist/lib/prompts.js +2 -7
- package/dist/lib/queue.d.ts +14 -16
- package/dist/lib/queue.js +252 -127
- package/dist/lib/recovery.d.ts +2 -83
- package/dist/lib/recovery.js +20 -274
- package/dist/lib/replies.d.ts +16 -24
- package/dist/lib/replies.js +99 -116
- package/dist/lib/routing.d.ts +120 -7
- package/dist/lib/routing.js +4667 -1152
- package/dist/lib/runtime.d.ts +1 -11
- package/dist/lib/sections.d.ts +4 -29
- package/dist/lib/sections.js +7 -20
- package/dist/lib/skills.js +4 -3
- package/dist/lib/status.js +86 -57
- package/dist/lib/sync.d.ts +1 -3
- package/dist/lib/sync.js +67 -43
- package/dist/lib/target.d.ts +4 -0
- package/dist/lib/target.js +18 -0
- package/dist/lib/telegram-api.d.ts +18 -13
- package/dist/lib/telegram-api.js +148 -53
- package/dist/lib/text-groups.js +4 -39
- package/dist/lib/thread-cleanup-manager.d.ts +6 -27
- package/dist/lib/thread-cleanup-manager.js +369 -186
- package/dist/lib/thread-display.d.ts +1 -2
- package/dist/lib/thread-display.js +58 -38
- package/dist/lib/thread-naming.d.ts +28 -8
- package/dist/lib/thread-naming.js +175 -66
- package/dist/lib/thread-reconciler.d.ts +42 -16
- package/dist/lib/thread-reconciler.js +90 -50
- package/dist/lib/threads.d.ts +82 -20
- package/dist/lib/threads.js +2247 -855
- package/dist/lib/time-injection.js +1 -2
- package/dist/lib/turns.js +1 -19
- package/dist/lib/updates.d.ts +169 -80
- package/dist/lib/updates.js +2418 -641
- package/dist/lib/voice.d.ts +7 -22
- package/dist/lib/voice.js +7 -22
- package/dist/lib/wire.js +1 -1
- package/dist/lib/workspace-admission.d.ts +3 -1
- package/dist/lib/workspace-admission.js +425 -211
- package/dist/lib/workspace-identity.js +8 -13
- package/dist/lib/workspace-retirement.d.ts +1 -1
- package/dist/lib/workspace-retirement.js +328 -140
- package/dist/lib/workspace-slots.js +8 -4
- package/dist/package.json +1 -1
- package/docs/architecture.md +31 -29
- package/docs/inbound.md +1 -1
- package/docs/multi-instance-bus.md +15 -3
- package/docs/outbound.md +1 -1
- package/docs/public-api.md +29 -1
- package/docs/ui-style.md +25 -8
- package/lib/activity-verbosity.ts +59 -82
- package/lib/activity.ts +146 -102
- package/lib/agent-messages.ts +12 -5
- package/lib/bindings.ts +389 -82
- package/lib/bus-api.ts +263 -102
- package/lib/bus-follower.ts +2440 -601
- package/lib/bus-leader.ts +2209 -1290
- package/lib/bus-transport.ts +46 -12
- package/lib/bus.ts +1673 -444
- package/lib/channel-posts.ts +848 -327
- package/lib/command-templates.ts +152 -265
- package/lib/commands.ts +2398 -555
- package/lib/config.ts +177 -170
- package/lib/delivery.ts +41 -29
- package/lib/extension.ts +1206 -433
- package/lib/generative-app-worker.mjs +19 -7
- package/lib/generative-apps.ts +324 -211
- package/lib/inbound.ts +95 -97
- package/lib/journal.ts +5311 -1763
- package/lib/keyboard.ts +1 -3
- package/lib/lifecycle.ts +128 -78
- package/lib/locks.ts +677 -203
- package/lib/logging.ts +147 -58
- package/lib/media.ts +82 -43
- package/lib/menu-model.ts +85 -18
- package/lib/menu-queue.ts +88 -129
- package/lib/menu-settings.ts +260 -155
- package/lib/menu-status.ts +20 -60
- package/lib/menu-thinking.ts +41 -56
- package/lib/menu.ts +112 -285
- package/lib/model.ts +8 -22
- package/lib/outbound-attachments.ts +156 -125
- package/lib/outbound-buttons.ts +76 -49
- package/lib/outbound-markup.ts +96 -94
- package/lib/outbound-voice.ts +33 -27
- package/lib/outbound.ts +128 -125
- package/lib/ownership.ts +19 -10
- package/lib/paths.ts +148 -49
- package/lib/pi.ts +35 -15
- package/lib/polling.ts +144 -147
- package/lib/preview.ts +93 -40
- package/lib/process-identity.ts +164 -0
- package/lib/prompts.ts +4 -11
- package/lib/queue.ts +516 -301
- package/lib/recovery.ts +50 -454
- package/lib/rendering.ts +1 -3
- package/lib/replies.ts +248 -209
- package/lib/routing.ts +8509 -2186
- package/lib/runtime.ts +1 -12
- package/lib/sections.ts +24 -89
- package/lib/setup.ts +1 -3
- package/lib/skills.ts +9 -4
- package/lib/status.ts +215 -110
- package/lib/sync.ts +448 -361
- package/lib/target.ts +22 -3
- package/lib/telegram-api.ts +302 -127
- package/lib/text-groups.ts +5 -39
- package/lib/thread-cleanup-manager.ts +833 -331
- package/lib/thread-display.ts +213 -97
- package/lib/thread-naming.ts +232 -87
- package/lib/thread-reconciler.ts +155 -72
- package/lib/threads.ts +5318 -1845
- package/lib/time-injection.ts +1 -4
- package/lib/turns.ts +15 -28
- package/lib/updates.ts +5184 -1612
- package/lib/voice.ts +7 -35
- package/lib/wire.ts +1 -1
- package/lib/workspace-admission.ts +996 -416
- package/lib/workspace-identity.ts +14 -17
- package/lib/workspace-retirement.ts +878 -376
- package/lib/workspace-slots.ts +22 -13
- package/package.json +1 -1
- package/scripts/check-downgrade.mjs +0 -472
- package/scripts/measure-workspace.mjs +0 -108
package/lib/replies.ts
CHANGED
|
@@ -5,22 +5,27 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { assertTelegramInlineKeyboardCallbackData } from "./keyboard.ts";
|
|
8
|
+
import {
|
|
9
|
+
renderTelegramMessage,
|
|
10
|
+
type TelegramRenderedChunk,
|
|
11
|
+
type TelegramRenderMode,
|
|
12
|
+
} from "./rendering.ts";
|
|
8
13
|
import {
|
|
9
14
|
getTelegramTargetThreadParams,
|
|
10
15
|
type TelegramTarget,
|
|
11
16
|
} from "./target.ts";
|
|
12
|
-
import { isTelegramApiCommitUnknownError } from "./telegram-api.ts";
|
|
13
17
|
import type {
|
|
18
|
+
TelegramApiCallOptions,
|
|
14
19
|
TelegramInputRichMessage,
|
|
15
20
|
TelegramReplyParameters,
|
|
16
21
|
TelegramSendRichMessageBody,
|
|
17
22
|
TelegramSentMessage,
|
|
18
23
|
} from "./telegram-api.ts";
|
|
19
24
|
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
} from "./
|
|
25
|
+
assertTelegramApiCallAuthority,
|
|
26
|
+
isTelegramApiCommitUnknownError,
|
|
27
|
+
TelegramApiAuthorityError,
|
|
28
|
+
} from "./telegram-api.ts";
|
|
24
29
|
|
|
25
30
|
export {
|
|
26
31
|
renderTelegramMessage,
|
|
@@ -37,36 +42,6 @@ export function renderTelegramMarkdownToHtmlDraft(markdown: string): string {
|
|
|
37
42
|
export const TELEGRAM_RICH_MESSAGE_MAX_CHARS = 32768;
|
|
38
43
|
export const TELEGRAM_RICH_MESSAGE_MAX_BLOCKS = 500;
|
|
39
44
|
|
|
40
|
-
// --- Reply Dedup ---
|
|
41
|
-
|
|
42
|
-
/** Non-persistent reply deduplication for a single agent turn.
|
|
43
|
-
* First reply to a prompt gets `reply_parameters.message_id`;
|
|
44
|
-
* subsequent replies in the same turn skip it to avoid stacking
|
|
45
|
-
* duplicate reply headers in the chat viewport. */
|
|
46
|
-
export interface ReplyDedupRuntime {
|
|
47
|
-
/** Returns true if this is the first reply for the given prompt
|
|
48
|
-
* message id in the current turn. Side-effect: marks it replied. */
|
|
49
|
-
shouldReply(promptMessageId: number): boolean;
|
|
50
|
-
/** Reset the tracker when a new prompt enters the queue. */
|
|
51
|
-
reset(): void;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function createReplyDedupRuntime(): ReplyDedupRuntime {
|
|
55
|
-
const replied = new Map<number, boolean>();
|
|
56
|
-
return {
|
|
57
|
-
shouldReply(promptMessageId: number): boolean {
|
|
58
|
-
if (replied.has(promptMessageId)) return false;
|
|
59
|
-
replied.set(promptMessageId, true);
|
|
60
|
-
return true;
|
|
61
|
-
},
|
|
62
|
-
reset(): void {
|
|
63
|
-
replied.clear();
|
|
64
|
-
},
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// --- Transport-level dedup ---
|
|
69
|
-
|
|
70
45
|
const lastRepliedToMessageIdByTarget = new Map<string, number>();
|
|
71
46
|
const replyDedupPreservedOnNextReset = new Map<string, number>();
|
|
72
47
|
let replyDedupGeneration = 0;
|
|
@@ -135,9 +110,13 @@ export async function withTelegramReplyParameters<T>(
|
|
|
135
110
|
try {
|
|
136
111
|
return await send(parameters);
|
|
137
112
|
} catch (error) {
|
|
138
|
-
if (
|
|
139
|
-
&&
|
|
140
|
-
|
|
113
|
+
if (
|
|
114
|
+
parameters &&
|
|
115
|
+
!isTelegramApiCommitUnknownError(error) &&
|
|
116
|
+
!(error instanceof TelegramApiAuthorityError && error.requestIssued) &&
|
|
117
|
+
generation === replyDedupGeneration &&
|
|
118
|
+
lastRepliedToMessageIdByTarget.get(key) === messageId
|
|
119
|
+
) {
|
|
141
120
|
if (previous === undefined) lastRepliedToMessageIdByTarget.delete(key);
|
|
142
121
|
else lastRepliedToMessageIdByTarget.set(key, previous);
|
|
143
122
|
}
|
|
@@ -203,9 +182,7 @@ export function extractLatestAssistantMessageText(
|
|
|
203
182
|
* is the latest earlier completed assistant message that carries text;
|
|
204
183
|
* tool-use prefaces, errors, and aborts stay excluded.
|
|
205
184
|
*/
|
|
206
|
-
export function extractRunAssistantMessage(
|
|
207
|
-
messages: readonly unknown[],
|
|
208
|
-
): {
|
|
185
|
+
export function extractRunAssistantMessage(messages: readonly unknown[]): {
|
|
209
186
|
text?: string;
|
|
210
187
|
stopReason?: string;
|
|
211
188
|
errorMessage?: string;
|
|
@@ -248,27 +225,36 @@ export interface TelegramReplyOwnershipRecorder {
|
|
|
248
225
|
|
|
249
226
|
export interface TelegramReplyDeliveryDeps<TReplyMarkup> {
|
|
250
227
|
recordOwnership?: TelegramReplyOwnershipRecorder["record"];
|
|
251
|
-
sendMessage: (
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
228
|
+
sendMessage: (
|
|
229
|
+
body: {
|
|
230
|
+
chat_id: number;
|
|
231
|
+
text: string;
|
|
232
|
+
parse_mode?: "HTML";
|
|
233
|
+
reply_markup?: TReplyMarkup;
|
|
234
|
+
reply_parameters?: TelegramReplyParameters;
|
|
235
|
+
reply_to_message_id?: number;
|
|
236
|
+
message_thread_id?: number;
|
|
237
|
+
},
|
|
238
|
+
options?: Pick<TelegramApiCallOptions, "assertAuthority">,
|
|
239
|
+
) => Promise<TelegramSentMessage>;
|
|
240
|
+
editMessage: (
|
|
241
|
+
body: {
|
|
242
|
+
chat_id: number;
|
|
243
|
+
message_id: number;
|
|
244
|
+
text?: string;
|
|
245
|
+
rich_message?: TelegramInputRichMessage;
|
|
246
|
+
parse_mode?: "HTML";
|
|
247
|
+
reply_markup?: TReplyMarkup;
|
|
248
|
+
message_thread_id?: number;
|
|
249
|
+
},
|
|
250
|
+
options?: Pick<TelegramApiCallOptions, "assertAuthority">,
|
|
251
|
+
) => Promise<unknown>;
|
|
269
252
|
}
|
|
270
253
|
|
|
271
|
-
export interface TelegramReplyTargetOptions
|
|
254
|
+
export interface TelegramReplyTargetOptions extends Pick<
|
|
255
|
+
TelegramApiCallOptions,
|
|
256
|
+
"assertAuthority"
|
|
257
|
+
> {
|
|
272
258
|
target?: TelegramTarget;
|
|
273
259
|
replyToMessageId?: number;
|
|
274
260
|
}
|
|
@@ -316,27 +302,75 @@ export async function sendTelegramRenderedChunks<TReplyMarkup>(
|
|
|
316
302
|
replyMarkup?: TReplyMarkup;
|
|
317
303
|
},
|
|
318
304
|
): Promise<number | undefined> {
|
|
319
|
-
|
|
305
|
+
return sendTelegramChunkSequence(
|
|
306
|
+
chatId,
|
|
307
|
+
chunks,
|
|
308
|
+
options?.replyToMessageId,
|
|
309
|
+
options,
|
|
310
|
+
deps.recordOwnership,
|
|
311
|
+
(chunk, fields, callOptions) =>
|
|
312
|
+
deps.sendMessage(
|
|
313
|
+
{
|
|
314
|
+
chat_id: chatId,
|
|
315
|
+
text: chunk.text,
|
|
316
|
+
parse_mode: chunk.parseMode,
|
|
317
|
+
...fields,
|
|
318
|
+
},
|
|
319
|
+
callOptions,
|
|
320
|
+
),
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Ordered chunk delivery: only the first chunk replies, only the last carries markup, and authority is rechecked
|
|
326
|
+
* before issuance and after every response/ownership publication.
|
|
327
|
+
*/
|
|
328
|
+
async function sendTelegramChunkSequence<TChunk, TReplyMarkup>(
|
|
329
|
+
chatId: number,
|
|
330
|
+
chunks: readonly TChunk[],
|
|
331
|
+
replyToMessageId: number | undefined,
|
|
332
|
+
options:
|
|
333
|
+
(TelegramReplyTargetOptions & { replyMarkup?: TReplyMarkup }) | undefined,
|
|
334
|
+
recordOwnership: TelegramReplyOwnershipRecorder["record"] | undefined,
|
|
335
|
+
send: (
|
|
336
|
+
chunk: TChunk,
|
|
337
|
+
fields: {
|
|
338
|
+
reply_markup?: TReplyMarkup;
|
|
339
|
+
reply_parameters?: TelegramReplyParameters;
|
|
340
|
+
message_thread_id?: number;
|
|
341
|
+
},
|
|
342
|
+
callOptions: Pick<TelegramApiCallOptions, "assertAuthority"> | undefined,
|
|
343
|
+
) => Promise<TelegramSentMessage>,
|
|
344
|
+
): Promise<number | undefined> {
|
|
345
|
+
const target = options?.target && { ...options.target },
|
|
346
|
+
assertAuthority = options?.assertAuthority,
|
|
347
|
+
replyMarkup = options?.replyMarkup;
|
|
348
|
+
assertTelegramInlineKeyboardCallbackData(replyMarkup);
|
|
320
349
|
let lastMessageId: number | undefined;
|
|
321
350
|
for (const [index, chunk] of chunks.entries()) {
|
|
351
|
+
assertTelegramApiCallAuthority(assertAuthority, false);
|
|
322
352
|
const sent = await withTelegramReplyParameters(
|
|
323
|
-
chatId,
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
353
|
+
chatId,
|
|
354
|
+
index === 0 ? replyToMessageId : undefined,
|
|
355
|
+
target,
|
|
356
|
+
async (replyParameters) => {
|
|
357
|
+
const sent = await send(
|
|
358
|
+
chunk,
|
|
359
|
+
{
|
|
360
|
+
reply_markup: index === chunks.length - 1 ? replyMarkup : undefined,
|
|
361
|
+
...(replyParameters ? { reply_parameters: replyParameters } : {}),
|
|
362
|
+
...(target ? getTelegramTargetThreadParams(target) : {}),
|
|
363
|
+
},
|
|
364
|
+
assertAuthority ? { assertAuthority } : undefined,
|
|
365
|
+
);
|
|
366
|
+
assertTelegramApiCallAuthority(assertAuthority, true);
|
|
367
|
+
return sent;
|
|
368
|
+
},
|
|
333
369
|
);
|
|
370
|
+
assertTelegramApiCallAuthority(assertAuthority, true);
|
|
334
371
|
lastMessageId = sent.message_id;
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
messageId: sent.message_id,
|
|
338
|
-
target: options?.target,
|
|
339
|
-
});
|
|
372
|
+
recordOwnership?.({ chatId, messageId: sent.message_id, target });
|
|
373
|
+
assertTelegramApiCallAuthority(assertAuthority, true);
|
|
340
374
|
}
|
|
341
375
|
return lastMessageId;
|
|
342
376
|
}
|
|
@@ -348,23 +382,35 @@ export async function editTelegramRenderedMessage<TReplyMarkup>(
|
|
|
348
382
|
deps: TelegramReplyDeliveryDeps<TReplyMarkup>,
|
|
349
383
|
options?: TelegramReplyTargetOptions & { replyMarkup?: TReplyMarkup },
|
|
350
384
|
): Promise<number | undefined> {
|
|
385
|
+
options = { ...options, target: options?.target && { ...options.target } };
|
|
386
|
+
const assertAuthority = options.assertAuthority;
|
|
387
|
+
assertTelegramApiCallAuthority(assertAuthority, false);
|
|
351
388
|
assertTelegramInlineKeyboardCallbackData(options?.replyMarkup);
|
|
352
389
|
if (chunks.length === 0) return messageId;
|
|
353
390
|
const [firstChunk, ...remainingChunks] = chunks;
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
391
|
+
if (!assertAuthority)
|
|
392
|
+
deps.recordOwnership?.({ chatId, messageId, target: options.target });
|
|
393
|
+
await deps.editMessage(
|
|
394
|
+
{
|
|
395
|
+
chat_id: chatId,
|
|
396
|
+
message_id: messageId,
|
|
397
|
+
text: firstChunk.text,
|
|
398
|
+
parse_mode: firstChunk.parseMode,
|
|
399
|
+
reply_markup:
|
|
400
|
+
remainingChunks.length === 0 ? options?.replyMarkup : undefined,
|
|
401
|
+
...(options?.target ? getTelegramTargetThreadParams(options.target) : {}),
|
|
402
|
+
},
|
|
403
|
+
assertAuthority ? { assertAuthority } : undefined,
|
|
404
|
+
);
|
|
405
|
+
assertTelegramApiCallAuthority(assertAuthority, true);
|
|
406
|
+
if (assertAuthority)
|
|
407
|
+
deps.recordOwnership?.({ chatId, messageId, target: options.target });
|
|
408
|
+
assertTelegramApiCallAuthority(assertAuthority, true);
|
|
364
409
|
if (remainingChunks.length > 0) {
|
|
365
410
|
return sendTelegramRenderedChunks(chatId, remainingChunks, deps, {
|
|
366
411
|
replyMarkup: options?.replyMarkup,
|
|
367
412
|
target: options?.target,
|
|
413
|
+
assertAuthority,
|
|
368
414
|
});
|
|
369
415
|
}
|
|
370
416
|
return messageId;
|
|
@@ -390,12 +436,14 @@ export async function sendTelegramPlainReply(
|
|
|
390
436
|
deps: TelegramReplyRuntimeDeps,
|
|
391
437
|
options?: TelegramTextReplyOptions,
|
|
392
438
|
): Promise<number | undefined> {
|
|
439
|
+
options = { ...options, target: options?.target && { ...options.target } };
|
|
393
440
|
const chunks = deps.renderTelegramMessage(text, {
|
|
394
441
|
mode: options?.parseMode === "HTML" ? "html" : "plain",
|
|
395
442
|
});
|
|
396
443
|
return deps.sendRenderedChunks(chunks, {
|
|
397
444
|
target: options?.target,
|
|
398
445
|
replyToMessageId: options?.replyToMessageId,
|
|
446
|
+
assertAuthority: options.assertAuthority,
|
|
399
447
|
});
|
|
400
448
|
}
|
|
401
449
|
|
|
@@ -431,37 +479,56 @@ function normalizeTelegramNativeMarkdownLine(line: string): string {
|
|
|
431
479
|
);
|
|
432
480
|
}
|
|
433
481
|
|
|
482
|
+
/** An open fenced code block: its marker character and the minimum closing run length. */
|
|
483
|
+
export interface TelegramMarkdownFence {
|
|
484
|
+
marker: "`" | "~";
|
|
485
|
+
length: number;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
export function parseTelegramMarkdownFenceOpening(
|
|
489
|
+
line: string,
|
|
490
|
+
): TelegramMarkdownFence | undefined {
|
|
491
|
+
const markerText = line.match(/^ {0,3}(`{3,}|~{3,})/)?.[1];
|
|
492
|
+
return markerText
|
|
493
|
+
? { marker: markerText[0] as "`" | "~", length: markerText.length }
|
|
494
|
+
: undefined;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export function isTelegramMarkdownFenceClosing(
|
|
498
|
+
line: string,
|
|
499
|
+
fence: TelegramMarkdownFence,
|
|
500
|
+
): boolean {
|
|
501
|
+
return new RegExp(`^ {0,3}${fence.marker}{${fence.length},}\\s*$`).test(line);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** Fence state after one line: outside a fence a marker opens one; inside, a matching marker closes it. */
|
|
505
|
+
function advanceTelegramMarkdownFence(
|
|
506
|
+
fence: TelegramMarkdownFence | undefined,
|
|
507
|
+
line: string,
|
|
508
|
+
): TelegramMarkdownFence | undefined {
|
|
509
|
+
if (!fence) return parseTelegramMarkdownFenceOpening(line);
|
|
510
|
+
return isTelegramMarkdownFenceClosing(line, fence) ? undefined : fence;
|
|
511
|
+
}
|
|
512
|
+
|
|
434
513
|
function hasClosingDisplayMathDelimiter(
|
|
435
514
|
lines: readonly string[],
|
|
436
515
|
startIndex: number,
|
|
437
516
|
): boolean {
|
|
438
|
-
let fence:
|
|
517
|
+
let fence: TelegramMarkdownFence | undefined;
|
|
439
518
|
for (let index = startIndex + 1; index < lines.length; index += 1) {
|
|
440
519
|
const line = lines[index] ?? "";
|
|
441
|
-
const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})/);
|
|
442
520
|
if (!fence && line.trim() === "$$") return true;
|
|
443
|
-
|
|
444
|
-
const markerText = fenceMatch[1] ?? "```";
|
|
445
|
-
fence = { marker: markerText[0] as "`" | "~", length: markerText.length };
|
|
446
|
-
continue;
|
|
447
|
-
}
|
|
448
|
-
if (
|
|
449
|
-
fence &&
|
|
450
|
-
new RegExp(`^ {0,3}${fence.marker}{${fence.length},}\\s*$`).test(line)
|
|
451
|
-
) {
|
|
452
|
-
fence = undefined;
|
|
453
|
-
}
|
|
521
|
+
fence = advanceTelegramMarkdownFence(fence, line);
|
|
454
522
|
}
|
|
455
523
|
return false;
|
|
456
524
|
}
|
|
457
525
|
|
|
458
526
|
export function normalizeTelegramNativeMarkdown(markdown: string): string {
|
|
459
527
|
const lines = markdown.replace(/\r\n/g, "\n").split("\n");
|
|
460
|
-
let fence:
|
|
528
|
+
let fence: TelegramMarkdownFence | undefined;
|
|
461
529
|
let displayMath = false;
|
|
462
530
|
return lines
|
|
463
531
|
.map((line, index) => {
|
|
464
|
-
const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})/);
|
|
465
532
|
const inFence = fence !== undefined;
|
|
466
533
|
if (!inFence && line.trim() === "$$") {
|
|
467
534
|
if (displayMath) {
|
|
@@ -474,19 +541,9 @@ export function normalizeTelegramNativeMarkdown(markdown: string): string {
|
|
|
474
541
|
}
|
|
475
542
|
}
|
|
476
543
|
if (displayMath) return line;
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
fence =
|
|
480
|
-
marker: markerText[0] as "`" | "~",
|
|
481
|
-
length: markerText.length,
|
|
482
|
-
};
|
|
483
|
-
return line;
|
|
484
|
-
}
|
|
485
|
-
if (
|
|
486
|
-
inFence &&
|
|
487
|
-
new RegExp(`^ {0,3}${fence?.marker}{${fence?.length},}\\s*$`).test(line)
|
|
488
|
-
) {
|
|
489
|
-
fence = undefined;
|
|
544
|
+
const nextFence = advanceTelegramMarkdownFence(fence, line);
|
|
545
|
+
if (nextFence !== fence) {
|
|
546
|
+
fence = nextFence;
|
|
490
547
|
return line;
|
|
491
548
|
}
|
|
492
549
|
if (!inFence) return normalizeTelegramNativeMarkdownLine(line);
|
|
@@ -542,30 +599,19 @@ export function splitTelegramNativeMarkdown(markdown: string): string[] {
|
|
|
542
599
|
function splitTelegramNativeMarkdownBlocks(markdown: string): string[] {
|
|
543
600
|
const blocks: string[] = [];
|
|
544
601
|
const current: string[] = [];
|
|
545
|
-
let fence:
|
|
602
|
+
let fence: TelegramMarkdownFence | undefined;
|
|
546
603
|
const flush = (): void => {
|
|
547
604
|
if (current.length === 0) return;
|
|
548
605
|
blocks.push(current.join("\n"));
|
|
549
606
|
current.length = 0;
|
|
550
607
|
};
|
|
551
608
|
for (const line of markdown.split("\n")) {
|
|
552
|
-
const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})/);
|
|
553
609
|
if (!fence && line.trim().length === 0) {
|
|
554
610
|
flush();
|
|
555
611
|
continue;
|
|
556
612
|
}
|
|
557
613
|
current.push(line);
|
|
558
|
-
|
|
559
|
-
const markerText = fenceMatch[1] ?? "```";
|
|
560
|
-
fence = { marker: markerText[0] as "`" | "~", length: markerText.length };
|
|
561
|
-
continue;
|
|
562
|
-
}
|
|
563
|
-
if (
|
|
564
|
-
fence &&
|
|
565
|
-
new RegExp(`^ {0,3}${fence.marker}{${fence.length},}\\s*$`).test(line)
|
|
566
|
-
) {
|
|
567
|
-
fence = undefined;
|
|
568
|
-
}
|
|
614
|
+
fence = advanceTelegramMarkdownFence(fence, line);
|
|
569
615
|
}
|
|
570
616
|
flush();
|
|
571
617
|
return blocks;
|
|
@@ -579,25 +625,14 @@ function splitTelegramNativeMarkdownCountedBlocks(block: string): string[] {
|
|
|
579
625
|
}
|
|
580
626
|
const chunks: string[] = [];
|
|
581
627
|
let current: string[] = [];
|
|
582
|
-
let fence:
|
|
628
|
+
let fence: TelegramMarkdownFence | undefined;
|
|
583
629
|
for (const line of block.split("\n")) {
|
|
584
|
-
const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})/);
|
|
585
630
|
if (!fence && current.length >= TELEGRAM_RICH_MESSAGE_MAX_BLOCKS) {
|
|
586
631
|
chunks.push(current.join("\n"));
|
|
587
632
|
current = [];
|
|
588
633
|
}
|
|
589
634
|
current.push(line);
|
|
590
|
-
|
|
591
|
-
const markerText = fenceMatch[1] ?? "```";
|
|
592
|
-
fence = { marker: markerText[0] as "`" | "~", length: markerText.length };
|
|
593
|
-
continue;
|
|
594
|
-
}
|
|
595
|
-
if (
|
|
596
|
-
fence &&
|
|
597
|
-
new RegExp(`^ {0,3}${fence.marker}{${fence.length},}\\s*$`).test(line)
|
|
598
|
-
) {
|
|
599
|
-
fence = undefined;
|
|
600
|
-
}
|
|
635
|
+
fence = advanceTelegramMarkdownFence(fence, line);
|
|
601
636
|
}
|
|
602
637
|
if (current.length > 0) chunks.push(current.join("\n"));
|
|
603
638
|
return chunks;
|
|
@@ -606,7 +641,9 @@ function splitTelegramNativeMarkdownCountedBlocks(block: string): string[] {
|
|
|
606
641
|
function countTelegramNativeMarkdownBlocks(block: string): number {
|
|
607
642
|
if (/^ {0,3}(`{3,}|~{3,})/.test(block)) return 1;
|
|
608
643
|
const lines = block.split("\n").filter((line) => line.trim().length > 0);
|
|
609
|
-
if (
|
|
644
|
+
if (
|
|
645
|
+
lines.some((line) => /^\s*([-*+] |\d+\. |>|\||<tg-button-row>)/.test(line))
|
|
646
|
+
) {
|
|
610
647
|
return Math.max(1, lines.length);
|
|
611
648
|
}
|
|
612
649
|
return 1;
|
|
@@ -639,13 +676,9 @@ function splitTelegramNativeMarkdownLongFenceBlock(
|
|
|
639
676
|
const lines = block.split("\n");
|
|
640
677
|
const opening = lines[0] ?? "";
|
|
641
678
|
const closing = lines[lines.length - 1] ?? "";
|
|
642
|
-
const
|
|
643
|
-
if (!
|
|
644
|
-
|
|
645
|
-
const marker = markerText[0] as "`" | "~";
|
|
646
|
-
if (
|
|
647
|
-
!new RegExp(`^ {0,3}${marker}{${markerText.length},}\\s*$`).test(closing)
|
|
648
|
-
) {
|
|
679
|
+
const openingFence = parseTelegramMarkdownFenceOpening(opening);
|
|
680
|
+
if (!openingFence || !closing || lines.length < 2) return undefined;
|
|
681
|
+
if (!isTelegramMarkdownFenceClosing(closing, openingFence)) {
|
|
649
682
|
return undefined;
|
|
650
683
|
}
|
|
651
684
|
const maxContentLength =
|
|
@@ -721,33 +754,23 @@ export async function sendTelegramNativeMarkdownReply<TReplyMarkup = unknown>(
|
|
|
721
754
|
recordOwnership?: TelegramReplyOwnershipRecorder["record"];
|
|
722
755
|
sendRichMessage: (
|
|
723
756
|
body: TelegramSendRichMessageBody,
|
|
757
|
+
options?: Pick<TelegramApiCallOptions, "assertAuthority">,
|
|
724
758
|
) => Promise<TelegramSentMessage>;
|
|
725
759
|
},
|
|
726
760
|
options?: TelegramReplyTargetOptions & { replyMarkup?: TReplyMarkup },
|
|
727
761
|
): Promise<number | undefined> {
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
rich_message: { markdown: chunk },
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
...(options?.target ? getTelegramTargetThreadParams(options.target) : {}),
|
|
741
|
-
}),
|
|
742
|
-
);
|
|
743
|
-
lastMessageId = sent.message_id;
|
|
744
|
-
deps.recordOwnership?.({
|
|
745
|
-
chatId,
|
|
746
|
-
messageId: sent.message_id,
|
|
747
|
-
target: options?.target,
|
|
748
|
-
});
|
|
749
|
-
}
|
|
750
|
-
return lastMessageId;
|
|
762
|
+
return sendTelegramChunkSequence(
|
|
763
|
+
chatId,
|
|
764
|
+
splitTelegramNativeMarkdown(markdown),
|
|
765
|
+
replyToMessageId,
|
|
766
|
+
options,
|
|
767
|
+
deps.recordOwnership,
|
|
768
|
+
(chunk, fields, callOptions) =>
|
|
769
|
+
deps.sendRichMessage(
|
|
770
|
+
{ chat_id: chatId, rich_message: { markdown: chunk }, ...fields },
|
|
771
|
+
callOptions,
|
|
772
|
+
),
|
|
773
|
+
);
|
|
751
774
|
}
|
|
752
775
|
|
|
753
776
|
async function sendTelegramNativeRichMessage(
|
|
@@ -755,16 +778,31 @@ async function sendTelegramNativeRichMessage(
|
|
|
755
778
|
richMessage: TelegramInputRichMessage,
|
|
756
779
|
deps: {
|
|
757
780
|
recordOwnership?: TelegramReplyOwnershipRecorder["record"];
|
|
758
|
-
sendRichMessage: (
|
|
781
|
+
sendRichMessage: (
|
|
782
|
+
body: TelegramSendRichMessageBody,
|
|
783
|
+
options?: Pick<TelegramApiCallOptions, "assertAuthority">,
|
|
784
|
+
) => Promise<TelegramSentMessage>;
|
|
759
785
|
},
|
|
760
786
|
options?: TelegramReplyTargetOptions,
|
|
761
787
|
): Promise<number> {
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
788
|
+
options = { ...options, target: options?.target && { ...options.target } };
|
|
789
|
+
const assertAuthority = options.assertAuthority;
|
|
790
|
+
assertTelegramApiCallAuthority(assertAuthority, false);
|
|
791
|
+
const sent = await deps.sendRichMessage(
|
|
792
|
+
{
|
|
793
|
+
chat_id: chatId,
|
|
794
|
+
rich_message: richMessage,
|
|
795
|
+
...(options?.target ? getTelegramTargetThreadParams(options.target) : {}),
|
|
796
|
+
},
|
|
797
|
+
assertAuthority ? { assertAuthority } : undefined,
|
|
798
|
+
);
|
|
799
|
+
assertTelegramApiCallAuthority(assertAuthority, true);
|
|
800
|
+
deps.recordOwnership?.({
|
|
801
|
+
chatId,
|
|
802
|
+
messageId: sent.message_id,
|
|
803
|
+
target: options?.target,
|
|
766
804
|
});
|
|
767
|
-
|
|
805
|
+
assertTelegramApiCallAuthority(assertAuthority, true);
|
|
768
806
|
return sent.message_id;
|
|
769
807
|
}
|
|
770
808
|
|
|
@@ -783,6 +821,7 @@ export interface TelegramRenderedMessageRuntimeDeps<TReplyMarkup> {
|
|
|
783
821
|
getAssistantRenderingMode?: () => TelegramAssistantRenderingMode;
|
|
784
822
|
sendRichMessage: (
|
|
785
823
|
body: TelegramSendRichMessageBody,
|
|
824
|
+
options?: Pick<TelegramApiCallOptions, "assertAuthority">,
|
|
786
825
|
) => Promise<TelegramSentMessage>;
|
|
787
826
|
}
|
|
788
827
|
|
|
@@ -805,6 +844,7 @@ export interface TelegramRenderedMessageRuntime<TReplyMarkup> {
|
|
|
805
844
|
text: string,
|
|
806
845
|
mode: TelegramRenderMode,
|
|
807
846
|
replyMarkup: TReplyMarkup,
|
|
847
|
+
options?: TelegramReplyTargetOptions,
|
|
808
848
|
) => Promise<void>;
|
|
809
849
|
sendInteractiveMessage: (
|
|
810
850
|
chatId: number,
|
|
@@ -836,6 +876,7 @@ export interface TelegramRenderedMessageDeliveryRuntimeDeps<
|
|
|
836
876
|
getAssistantRenderingMode?: () => TelegramAssistantRenderingMode;
|
|
837
877
|
sendRichMessage: (
|
|
838
878
|
body: TelegramSendRichMessageBody,
|
|
879
|
+
options?: Pick<TelegramApiCallOptions, "assertAuthority">,
|
|
839
880
|
) => Promise<TelegramSentMessage>;
|
|
840
881
|
}
|
|
841
882
|
|
|
@@ -872,6 +913,7 @@ export function createTelegramRenderedMessageRuntime<TReplyMarkup>(
|
|
|
872
913
|
sendRenderedChunks: (chunks, chunkOptions) =>
|
|
873
914
|
deps.replyTransport.sendRenderedChunks(chatId, chunks, {
|
|
874
915
|
target: chunkOptions?.target,
|
|
916
|
+
assertAuthority: chunkOptions?.assertAuthority,
|
|
875
917
|
replyToMessageId:
|
|
876
918
|
chunkOptions?.replyToMessageId ?? replyToMessageId,
|
|
877
919
|
}),
|
|
@@ -880,6 +922,10 @@ export function createTelegramRenderedMessageRuntime<TReplyMarkup>(
|
|
|
880
922
|
);
|
|
881
923
|
},
|
|
882
924
|
sendMarkdownReply: async (chatId, replyToMessageId, markdown, options) => {
|
|
925
|
+
options = {
|
|
926
|
+
...options,
|
|
927
|
+
target: options?.target && { ...options.target },
|
|
928
|
+
};
|
|
883
929
|
const renderingMode = deps.getAssistantRenderingMode?.() ?? "rich";
|
|
884
930
|
if (renderingMode === "html") {
|
|
885
931
|
return deps.replyTransport.sendRenderedChunks(
|
|
@@ -889,6 +935,7 @@ export function createTelegramRenderedMessageRuntime<TReplyMarkup>(
|
|
|
889
935
|
replyMarkup: options?.replyMarkup,
|
|
890
936
|
target: options?.target,
|
|
891
937
|
replyToMessageId,
|
|
938
|
+
assertAuthority: options.assertAuthority,
|
|
892
939
|
},
|
|
893
940
|
);
|
|
894
941
|
}
|
|
@@ -909,12 +956,17 @@ export function createTelegramRenderedMessageRuntime<TReplyMarkup>(
|
|
|
909
956
|
text,
|
|
910
957
|
mode,
|
|
911
958
|
replyMarkup,
|
|
959
|
+
options,
|
|
912
960
|
) => {
|
|
961
|
+
options = {
|
|
962
|
+
...options,
|
|
963
|
+
target: options?.target && { ...options.target },
|
|
964
|
+
};
|
|
913
965
|
await deps.replyTransport.editRenderedMessage(
|
|
914
966
|
chatId,
|
|
915
967
|
messageId,
|
|
916
968
|
deps.renderTelegramMessage(text, { mode }),
|
|
917
|
-
{ replyMarkup },
|
|
969
|
+
{ ...options, replyMarkup },
|
|
918
970
|
);
|
|
919
971
|
},
|
|
920
972
|
sendInteractiveMessage: async (
|
|
@@ -924,6 +976,10 @@ export function createTelegramRenderedMessageRuntime<TReplyMarkup>(
|
|
|
924
976
|
replyMarkup,
|
|
925
977
|
options,
|
|
926
978
|
) => {
|
|
979
|
+
options = {
|
|
980
|
+
...options,
|
|
981
|
+
target: options?.target && { ...options.target },
|
|
982
|
+
};
|
|
927
983
|
return deps.replyTransport.sendRenderedChunks(
|
|
928
984
|
chatId,
|
|
929
985
|
deps.renderTelegramMessage(text, { mode }),
|
|
@@ -931,40 +987,20 @@ export function createTelegramRenderedMessageRuntime<TReplyMarkup>(
|
|
|
931
987
|
replyMarkup,
|
|
932
988
|
target: options?.target,
|
|
933
989
|
replyToMessageId: options?.replyToMessageId,
|
|
990
|
+
assertAuthority: options.assertAuthority,
|
|
934
991
|
},
|
|
935
992
|
);
|
|
936
993
|
},
|
|
937
994
|
sendSectionRichMessage: (chatId, message, options) =>
|
|
938
|
-
sendTelegramNativeRichMessage(
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
/** Wrap a sendTextReply with reply dedup so only the first message
|
|
948
|
-
* in a turn carries reply metadata. */
|
|
949
|
-
export function dedupSendTextReply(
|
|
950
|
-
dedup: ReplyDedupRuntime,
|
|
951
|
-
inner: (
|
|
952
|
-
chatId: number,
|
|
953
|
-
replyToMessageId: number | undefined,
|
|
954
|
-
text: string,
|
|
955
|
-
options?: TelegramTextReplyOptions,
|
|
956
|
-
) => Promise<number | undefined>,
|
|
957
|
-
): (
|
|
958
|
-
chatId: number,
|
|
959
|
-
replyToMessageId: number,
|
|
960
|
-
text: string,
|
|
961
|
-
options?: TelegramTextReplyOptions,
|
|
962
|
-
) => Promise<number | undefined> {
|
|
963
|
-
return async (chatId, replyToMessageId, text, options) => {
|
|
964
|
-
const effectiveReplyTo = dedup.shouldReply(replyToMessageId)
|
|
965
|
-
? replyToMessageId
|
|
966
|
-
: undefined;
|
|
967
|
-
return inner(chatId, effectiveReplyTo, text, options);
|
|
995
|
+
sendTelegramNativeRichMessage(
|
|
996
|
+
chatId,
|
|
997
|
+
message,
|
|
998
|
+
{
|
|
999
|
+
recordOwnership: deps.recordOwnership,
|
|
1000
|
+
sendRichMessage: deps.sendRichMessage,
|
|
1001
|
+
},
|
|
1002
|
+
options,
|
|
1003
|
+
),
|
|
968
1004
|
};
|
|
969
1005
|
}
|
|
970
1006
|
|
|
@@ -1174,7 +1210,10 @@ export function createTelegramGuestPlaceholderRuntime(
|
|
|
1174
1210
|
parseMode: "HTML",
|
|
1175
1211
|
});
|
|
1176
1212
|
} catch (error) {
|
|
1177
|
-
nextDelayMs = getTelegramGuestPlaceholderRetryDelayMs(
|
|
1213
|
+
nextDelayMs = getTelegramGuestPlaceholderRetryDelayMs(
|
|
1214
|
+
error,
|
|
1215
|
+
intervalMs,
|
|
1216
|
+
);
|
|
1178
1217
|
deps.recordRuntimeEvent?.("guest", error, {
|
|
1179
1218
|
phase: "guest-placeholder-edit",
|
|
1180
1219
|
retryAfterMs: nextDelayMs,
|