@mono-agent/telegram-adapter 0.2.2 → 0.4.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/README.md +13 -8
- package/dist/adapter.d.ts +128 -2
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +382 -10
- package/dist/adapter.js.map +1 -1
- package/dist/bot.d.ts +57 -5
- package/dist/bot.d.ts.map +1 -1
- package/dist/bot.js +534 -24
- package/dist/bot.js.map +1 -1
- package/dist/grammy-client.d.ts +1 -0
- package/dist/grammy-client.d.ts.map +1 -1
- package/dist/grammy-client.js +17 -1
- package/dist/grammy-client.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/message-stream.d.ts +32 -52
- package/dist/message-stream.d.ts.map +1 -1
- package/dist/message-stream.js +163 -404
- package/dist/message-stream.js.map +1 -1
- package/dist/start.d.ts +7 -1
- package/dist/start.d.ts.map +1 -1
- package/dist/start.js +1 -0
- package/dist/start.js.map +1 -1
- package/dist/types.d.ts +45 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/message-stream.js
CHANGED
|
@@ -1,397 +1,99 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ChannelDeliveryError, DEFAULT_MAX_MESSAGE_CHARS, ResilientMessageStream, normalizeTrailing, splitTextByCodePoints, } from "@mono-agent/agent-contracts";
|
|
2
2
|
import { TelegramApiError } from "./telegram-error.js";
|
|
3
3
|
import { renderTelegramMarkdown } from "./telegram-markdown.js";
|
|
4
4
|
/**
|
|
5
5
|
* Raised only when a *final* delivery cannot reach Telegram after retries and
|
|
6
6
|
* the last-resort fresh send. The AI request itself already succeeded, so the
|
|
7
7
|
* adapter treats this as a degraded delivery — never as an agent failure.
|
|
8
|
+
*
|
|
9
|
+
* A thin specialization of the shared {@link ChannelDeliveryError}: the substrate
|
|
10
|
+
* throws the shared base type, and the wrapper's `finish()` normalizes it to this
|
|
11
|
+
* Telegram type (preserving `{ cause, attempts }`) so callers catching
|
|
12
|
+
* `TelegramDeliveryError` keep working and the base type never escapes — parity
|
|
13
|
+
* with how the Slack adapter wraps delivery failures.
|
|
8
14
|
*/
|
|
9
|
-
export class TelegramDeliveryError extends
|
|
10
|
-
cause;
|
|
11
|
-
attempts;
|
|
15
|
+
export class TelegramDeliveryError extends ChannelDeliveryError {
|
|
12
16
|
constructor(message, details) {
|
|
13
|
-
super(message);
|
|
17
|
+
super(message, details);
|
|
14
18
|
this.name = "TelegramDeliveryError";
|
|
15
|
-
this.cause = details.cause;
|
|
16
|
-
this.attempts = details.attempts;
|
|
17
19
|
}
|
|
18
20
|
}
|
|
19
21
|
const DEFAULT_INITIAL_STATUS_TEXT = "Thinking…";
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
/** Sentinel raised by the transport when a rendered MarkdownV2 chunk overflows. */
|
|
23
|
+
const MARKDOWN_OVERFLOW = Symbol("telegram-markdown-overflow");
|
|
24
|
+
function isMarkdownOverflowError(error) {
|
|
25
|
+
return (typeof error === "object" &&
|
|
26
|
+
error !== null &&
|
|
27
|
+
error[MARKDOWN_OVERFLOW] === true);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Telegram-specific {@link ChannelTransport}. Wraps the {@link TelegramMessageSender}
|
|
31
|
+
* (sendMessage / editMessageText), renders MarkdownV2, and maps Telegram failures
|
|
32
|
+
* onto {@link ChannelSendOutcome}. Markdown rendering and `parse_mode` are gated by
|
|
33
|
+
* a mutable `markdownEnabled` flag so the wrapper can deliver fixed system copy
|
|
34
|
+
* (e.g. "Cancelled.") as plain text without re-rendering it.
|
|
35
|
+
*/
|
|
36
|
+
class TelegramChannelTransport {
|
|
37
|
+
maxMessageChars;
|
|
38
|
+
/** Gates MarkdownV2 rendering + `parse_mode`. Toggled per finish() call. */
|
|
39
|
+
markdownEnabled;
|
|
27
40
|
api;
|
|
28
41
|
chatId;
|
|
29
|
-
initialStatusText;
|
|
30
|
-
editDebounceMs;
|
|
31
|
-
maxMessageChars;
|
|
32
42
|
replyToMessageId;
|
|
33
|
-
maxSendRetries;
|
|
34
|
-
retryCapMs;
|
|
35
|
-
retryBaseDelayMs;
|
|
36
|
-
showThoughts;
|
|
37
|
-
formatMarkdown;
|
|
38
|
-
abortSignal;
|
|
39
|
-
logger;
|
|
40
|
-
currentText = "";
|
|
41
|
-
thinkingText = "";
|
|
42
|
-
hasAnswerText = false;
|
|
43
|
-
statusText;
|
|
44
|
-
sentMessage;
|
|
45
|
-
sendMessagePromise;
|
|
46
|
-
editTimer;
|
|
47
|
-
inFlightEdit;
|
|
48
|
-
lastFlushedText;
|
|
49
|
-
lastFlushedMarkdown = false;
|
|
50
|
-
finished = false;
|
|
51
43
|
constructor(options) {
|
|
52
44
|
this.api = options.api;
|
|
53
45
|
this.chatId = options.chatId;
|
|
54
|
-
this.
|
|
55
|
-
this.statusText = this.initialStatusText;
|
|
56
|
-
this.editDebounceMs = options.editDebounceMs ?? DEFAULT_EDIT_DEBOUNCE_MS;
|
|
57
|
-
this.maxMessageChars = options.maxMessageChars ?? DEFAULT_MAX_MESSAGE_CHARS;
|
|
46
|
+
this.maxMessageChars = options.maxMessageChars;
|
|
58
47
|
this.replyToMessageId = options.replyToMessageId;
|
|
59
|
-
this.
|
|
60
|
-
this.retryCapMs = options.retryCapMs ?? DEFAULT_RETRY_CAP_MS;
|
|
61
|
-
this.retryBaseDelayMs = options.retryBaseDelayMs ?? DEFAULT_RETRY_BASE_DELAY_MS;
|
|
62
|
-
this.showThoughts = options.showThoughts ?? true;
|
|
63
|
-
this.formatMarkdown = options.formatMarkdown ?? true;
|
|
64
|
-
this.abortSignal = options.abortSignal;
|
|
65
|
-
this.logger = options.logger;
|
|
66
|
-
if (!Number.isInteger(this.maxMessageChars) || this.maxMessageChars < 32) {
|
|
67
|
-
throw new RangeError("maxMessageChars must be an integer of at least 32.");
|
|
68
|
-
}
|
|
69
|
-
if (!Number.isFinite(this.editDebounceMs) || this.editDebounceMs < 0) {
|
|
70
|
-
throw new RangeError("editDebounceMs must be a non-negative number.");
|
|
71
|
-
}
|
|
72
|
-
if (!Number.isInteger(this.maxSendRetries) || this.maxSendRetries < 0) {
|
|
73
|
-
throw new RangeError("maxSendRetries must be a non-negative integer.");
|
|
74
|
-
}
|
|
75
|
-
if (!Number.isFinite(this.retryCapMs) || this.retryCapMs < 0) {
|
|
76
|
-
throw new RangeError("retryCapMs must be a non-negative number.");
|
|
77
|
-
}
|
|
78
|
-
if (!Number.isFinite(this.retryBaseDelayMs) || this.retryBaseDelayMs < 0) {
|
|
79
|
-
throw new RangeError("retryBaseDelayMs must be a non-negative number.");
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
async status(text) {
|
|
83
|
-
this.assertOpen();
|
|
84
|
-
await this.awaitInFlightEdit();
|
|
85
|
-
this.statusText = normalizeTelegramText(text);
|
|
86
|
-
const hadMessage = this.sentMessage !== undefined;
|
|
87
|
-
await this.ensureMessage();
|
|
88
|
-
if (hadMessage) {
|
|
89
|
-
await this.deliverText(this.statusText, { final: false });
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
async append(delta) {
|
|
93
|
-
this.assertOpen();
|
|
94
|
-
await this.awaitInFlightEdit();
|
|
95
|
-
if (delta.length === 0) {
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
this.currentText += delta;
|
|
99
|
-
if (!this.hasAnswerText && delta.trim().length > 0) {
|
|
100
|
-
this.hasAnswerText = true;
|
|
101
|
-
}
|
|
102
|
-
await this.ensureMessage();
|
|
103
|
-
this.scheduleEdit();
|
|
104
|
-
}
|
|
105
|
-
async replace(text) {
|
|
106
|
-
this.assertOpen();
|
|
107
|
-
await this.awaitInFlightEdit();
|
|
108
|
-
this.currentText = text;
|
|
109
|
-
if (text.trim().length > 0) {
|
|
110
|
-
this.hasAnswerText = true;
|
|
111
|
-
}
|
|
112
|
-
await this.ensureMessage();
|
|
113
|
-
this.scheduleEdit();
|
|
114
|
-
}
|
|
115
|
-
async event(event) {
|
|
116
|
-
this.assertOpen();
|
|
117
|
-
await this.awaitInFlightEdit();
|
|
118
|
-
if (event.type === "assistant_thought") {
|
|
119
|
-
// Reasoning is shown live as a single accumulating "💭" message, then
|
|
120
|
-
// superseded the moment the answer starts streaming.
|
|
121
|
-
if (!this.showThoughts || this.hasAnswerText || event.text.length === 0) {
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
this.thinkingText += event.text;
|
|
125
|
-
await this.ensureMessage();
|
|
126
|
-
this.scheduleEdit();
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
if (event.type === "runtime_warning") {
|
|
130
|
-
this.logger?.warn?.("Telegram stream received runtime warning.", {
|
|
131
|
-
warningKind: event.warningKind,
|
|
132
|
-
message: event.message,
|
|
133
|
-
});
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
if (event.type === "tool_call_started") {
|
|
137
|
-
this.logger?.debug?.("Telegram stream received tool start event.", {
|
|
138
|
-
id: event.id,
|
|
139
|
-
name: event.name,
|
|
140
|
-
});
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
if (event.type === "tool_call_completed") {
|
|
144
|
-
this.logger?.debug?.("Telegram stream received tool completion event.", {
|
|
145
|
-
id: event.id,
|
|
146
|
-
name: event.name,
|
|
147
|
-
isError: event.isError === true,
|
|
148
|
-
});
|
|
149
|
-
}
|
|
48
|
+
this.markdownEnabled = options.markdownEnabled;
|
|
150
49
|
}
|
|
151
|
-
|
|
152
|
-
if (this.
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
this.finished = true;
|
|
156
|
-
if (finalText !== undefined) {
|
|
157
|
-
this.currentText = finalText;
|
|
158
|
-
if (finalText.trim().length > 0) {
|
|
159
|
-
this.hasAnswerText = true;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
this.cancelScheduledEdit();
|
|
163
|
-
await this.awaitInFlightEdit();
|
|
164
|
-
const chunks = splitTelegramText(this.currentText, this.maxMessageChars);
|
|
165
|
-
const [firstChunk, ...remainingChunks] = chunks;
|
|
166
|
-
await this.ensureMessage();
|
|
167
|
-
try {
|
|
168
|
-
await this.deliverText(firstChunk ?? EMPTY_FINAL_TEXT, {
|
|
169
|
-
final: true,
|
|
170
|
-
format: options?.format ?? true,
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
catch (error) {
|
|
174
|
-
if (this.abortSignal?.aborted === true) {
|
|
175
|
-
// Cancelled: deliver in place if we can, but never post a brand-new
|
|
176
|
-
// message carrying content the user has already asked us to drop.
|
|
177
|
-
this.logger?.warn?.("Telegram final delivery skipped after cancellation.", {
|
|
178
|
-
error: errorMessage(error),
|
|
179
|
-
});
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
await this.lastResortSend(firstChunk ?? EMPTY_FINAL_TEXT, error);
|
|
183
|
-
}
|
|
184
|
-
if (this.abortSignal?.aborted === true) {
|
|
185
|
-
// Do not spray overflow continuation messages onto a cancelled run.
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
for (const chunk of remainingChunks) {
|
|
189
|
-
await this.sendOverflowChunk(chunk);
|
|
50
|
+
renderMarkdown(text) {
|
|
51
|
+
if (!this.markdownEnabled) {
|
|
52
|
+
return text;
|
|
190
53
|
}
|
|
54
|
+
return renderTelegramMarkdown(text);
|
|
191
55
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
return `${THOUGHT_PREFIX}${this.thinkingText}`;
|
|
198
|
-
}
|
|
199
|
-
return this.statusText;
|
|
56
|
+
async post(text, options) {
|
|
57
|
+
const useMarkdown = options.markdown && this.markdownEnabled;
|
|
58
|
+
this.assertWithinLimit(text, useMarkdown);
|
|
59
|
+
const sent = await this.api.sendMessage(this.buildSendParams(text, useMarkdown));
|
|
60
|
+
return { id: String(sent.message_id), message_id: sent.message_id };
|
|
200
61
|
}
|
|
201
|
-
async
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if (this.sendMessagePromise === undefined) {
|
|
206
|
-
const initialText = this.statusText;
|
|
207
|
-
const params = {
|
|
208
|
-
chat_id: this.chatId,
|
|
209
|
-
text: initialText,
|
|
210
|
-
};
|
|
211
|
-
if (this.replyToMessageId !== undefined) {
|
|
212
|
-
params.reply_to_message_id = this.replyToMessageId;
|
|
213
|
-
}
|
|
214
|
-
this.sendMessagePromise = this.api.sendMessage(params).then((message) => {
|
|
215
|
-
this.lastFlushedText = initialText;
|
|
216
|
-
this.lastFlushedMarkdown = false;
|
|
217
|
-
return message;
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
try {
|
|
221
|
-
this.sentMessage = await this.sendMessagePromise;
|
|
222
|
-
}
|
|
223
|
-
catch (error) {
|
|
224
|
-
// Do not poison future sends with a rejected promise.
|
|
225
|
-
this.sendMessagePromise = undefined;
|
|
226
|
-
throw error;
|
|
227
|
-
}
|
|
228
|
-
return this.sentMessage;
|
|
62
|
+
async edit(ref, text, options) {
|
|
63
|
+
const useMarkdown = options.markdown && this.markdownEnabled;
|
|
64
|
+
this.assertWithinLimit(text, useMarkdown);
|
|
65
|
+
await this.api.editMessageText(this.buildEditParams(ref, text, useMarkdown));
|
|
229
66
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
this.startInFlightEdit();
|
|
234
|
-
return;
|
|
67
|
+
classifyError(error) {
|
|
68
|
+
if (isMarkdownOverflowError(error)) {
|
|
69
|
+
return { kind: "reformat_plain" };
|
|
235
70
|
}
|
|
236
|
-
|
|
237
|
-
this.editTimer = undefined;
|
|
238
|
-
this.startInFlightEdit();
|
|
239
|
-
}, this.editDebounceMs);
|
|
71
|
+
return classifyTelegramError(error);
|
|
240
72
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
// against an abort rejection so a streaming hiccup never aborts the run.
|
|
246
|
-
this.logger?.warn?.("Telegram stream interim edit failed (ignored).", {
|
|
247
|
-
error: errorMessage(error),
|
|
248
|
-
});
|
|
249
|
-
});
|
|
250
|
-
void this.inFlightEdit;
|
|
73
|
+
async indicateActivity() {
|
|
74
|
+
// Telegram "typing…" chat action; expires after ~5s so the substrate
|
|
75
|
+
// refreshes it while the agent works. No-op if the sender lacks the method.
|
|
76
|
+
await this.api.sendChatAction?.({ chat_id: this.chatId, action: "typing" });
|
|
251
77
|
}
|
|
252
78
|
/**
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
79
|
+
* MarkdownV2 escaping can expand a chunk past Telegram's size limit even though
|
|
80
|
+
* the plain source is within it (chunks are split on the source length). Rather
|
|
81
|
+
* than send and fail with "message is too long", we signal a reformat-to-plain
|
|
82
|
+
* recovery; the substrate then re-delivers the plain source within the limit.
|
|
83
|
+
* (We never test renderedText === source to decide this: telegramify renders
|
|
84
|
+
* inline code / links back to identical bytes that still need parse_mode, so
|
|
85
|
+
* equality is not a plain-text signal.)
|
|
257
86
|
*/
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
if (useMarkdown && countCodePoints(renderedText) > this.maxMessageChars) {
|
|
263
|
-
// MarkdownV2 escaping can expand a chunk past Telegram's size limit, but
|
|
264
|
-
// chunks are split on the source length. The plain source is within the
|
|
265
|
-
// limit, so deliver it as plain text rather than fail with
|
|
266
|
-
// "message is too long". (We never test renderedText === source to decide
|
|
267
|
-
// this: telegramify renders inline code / links back to identical bytes
|
|
268
|
-
// that still need parse_mode to render, so equality is not a plain-text
|
|
269
|
-
// signal.)
|
|
270
|
-
useMarkdown = false;
|
|
271
|
-
renderedText = normalizedSource;
|
|
272
|
-
}
|
|
273
|
-
if (renderedText === this.lastFlushedText && useMarkdown === this.lastFlushedMarkdown) {
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
const maxAttempts = options.final ? this.maxSendRetries + 1 : 1;
|
|
277
|
-
let recreate = false;
|
|
278
|
-
let lastError;
|
|
279
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
280
|
-
try {
|
|
281
|
-
if (recreate) {
|
|
282
|
-
const sent = await this.api.sendMessage(this.buildSendParams(renderedText, useMarkdown));
|
|
283
|
-
this.sentMessage = sent;
|
|
284
|
-
}
|
|
285
|
-
else {
|
|
286
|
-
const message = await this.ensureMessage();
|
|
287
|
-
await this.api.editMessageText(this.buildEditParams(message, renderedText, useMarkdown));
|
|
288
|
-
}
|
|
289
|
-
this.lastFlushedText = renderedText;
|
|
290
|
-
this.lastFlushedMarkdown = useMarkdown;
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
catch (error) {
|
|
294
|
-
lastError = error;
|
|
295
|
-
const outcome = classifyTelegramError(error);
|
|
296
|
-
if (outcome.kind === "not_modified") {
|
|
297
|
-
this.lastFlushedText = renderedText;
|
|
298
|
-
this.lastFlushedMarkdown = useMarkdown;
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
if (outcome.kind === "reformat_plain" && useMarkdown) {
|
|
302
|
-
useMarkdown = false;
|
|
303
|
-
renderedText = normalizedSource;
|
|
304
|
-
continue;
|
|
305
|
-
}
|
|
306
|
-
if (outcome.kind === "recreate" && this.abortSignal?.aborted !== true) {
|
|
307
|
-
recreate = true;
|
|
308
|
-
this.sentMessage = undefined;
|
|
309
|
-
this.lastFlushedText = undefined;
|
|
310
|
-
continue;
|
|
311
|
-
}
|
|
312
|
-
if (outcome.kind === "retry" && options.final && attempt < maxAttempts) {
|
|
313
|
-
await this.sleep(this.retryDelayMs(outcome.retryAfterMs, attempt));
|
|
314
|
-
if (this.abortSignal?.aborted === true) {
|
|
315
|
-
break;
|
|
316
|
-
}
|
|
317
|
-
continue;
|
|
318
|
-
}
|
|
319
|
-
break;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
if (options.final) {
|
|
323
|
-
throw new TelegramDeliveryError("Telegram final delivery failed.", {
|
|
324
|
-
cause: lastError,
|
|
325
|
-
attempts: maxAttempts,
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
this.logger?.warn?.("Telegram stream interim edit failed (ignored).", {
|
|
329
|
-
error: errorMessage(lastError),
|
|
330
|
-
});
|
|
331
|
-
}
|
|
332
|
-
/**
|
|
333
|
-
* The streamed message could not be edited or recreated in place. Post the
|
|
334
|
-
* final answer as a brand-new plain message so the user still receives it.
|
|
335
|
-
*/
|
|
336
|
-
async lastResortSend(text, cause) {
|
|
337
|
-
const normalized = normalizeTelegramText(text);
|
|
338
|
-
const maxAttempts = this.maxSendRetries + 1;
|
|
339
|
-
let lastError = cause;
|
|
340
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
341
|
-
try {
|
|
342
|
-
const sent = await this.api.sendMessage(this.buildSendParams(normalized, false));
|
|
343
|
-
this.sentMessage = sent;
|
|
344
|
-
this.lastFlushedText = normalized;
|
|
345
|
-
this.lastFlushedMarkdown = false;
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
catch (error) {
|
|
349
|
-
lastError = error;
|
|
350
|
-
const outcome = classifyTelegramError(error);
|
|
351
|
-
if (outcome.kind === "retry" && attempt < maxAttempts) {
|
|
352
|
-
await this.sleep(this.retryDelayMs(outcome.retryAfterMs, attempt));
|
|
353
|
-
if (this.abortSignal?.aborted === true) {
|
|
354
|
-
break;
|
|
355
|
-
}
|
|
356
|
-
continue;
|
|
357
|
-
}
|
|
358
|
-
break;
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
throw new TelegramDeliveryError("Telegram delivery failed after fallback send.", {
|
|
362
|
-
cause: lastError,
|
|
363
|
-
attempts: maxAttempts,
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
/** Overflow continuation chunks are best-effort: the primary answer already landed. */
|
|
367
|
-
async sendOverflowChunk(chunk) {
|
|
368
|
-
const normalized = normalizeTelegramText(chunk);
|
|
369
|
-
const maxAttempts = this.maxSendRetries + 1;
|
|
370
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
371
|
-
try {
|
|
372
|
-
await this.api.sendMessage(this.buildSendParams(normalized, false));
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
375
|
-
catch (error) {
|
|
376
|
-
const outcome = classifyTelegramError(error);
|
|
377
|
-
if (outcome.kind === "retry" && attempt < maxAttempts) {
|
|
378
|
-
await this.sleep(this.retryDelayMs(outcome.retryAfterMs, attempt));
|
|
379
|
-
if (this.abortSignal?.aborted === true) {
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
382
|
-
continue;
|
|
383
|
-
}
|
|
384
|
-
this.logger?.warn?.("Telegram overflow chunk failed (ignored).", {
|
|
385
|
-
error: errorMessage(error),
|
|
386
|
-
});
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
87
|
+
assertWithinLimit(text, useMarkdown) {
|
|
88
|
+
if (useMarkdown && countCodePoints(text) > this.maxMessageChars) {
|
|
89
|
+
const overflow = { [MARKDOWN_OVERFLOW]: true };
|
|
90
|
+
throw overflow;
|
|
389
91
|
}
|
|
390
92
|
}
|
|
391
|
-
buildEditParams(
|
|
93
|
+
buildEditParams(ref, text, useMarkdown) {
|
|
392
94
|
const params = {
|
|
393
95
|
chat_id: this.chatId,
|
|
394
|
-
message_id:
|
|
96
|
+
message_id: messageIdOf(ref),
|
|
395
97
|
text,
|
|
396
98
|
};
|
|
397
99
|
if (useMarkdown) {
|
|
@@ -404,49 +106,112 @@ export class TelegramMessageStream {
|
|
|
404
106
|
if (useMarkdown) {
|
|
405
107
|
params.parse_mode = "MarkdownV2";
|
|
406
108
|
}
|
|
109
|
+
if (this.replyToMessageId !== undefined) {
|
|
110
|
+
params.reply_to_message_id = this.replyToMessageId;
|
|
111
|
+
}
|
|
407
112
|
return params;
|
|
408
113
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
114
|
+
}
|
|
115
|
+
function messageIdOf(ref) {
|
|
116
|
+
const raw = ref.message_id;
|
|
117
|
+
return typeof raw === "number" ? raw : Number(ref.id);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Thin wrapper over the shared {@link ResilientMessageStream}: builds a Telegram
|
|
121
|
+
* {@link ChannelTransport} and delegates all streaming/finish behavior to the
|
|
122
|
+
* substrate, preserving this adapter's public API and no-labels + activity-hints
|
|
123
|
+
* behavior. The only Telegram-specific lever the wrapper retains is the per-call
|
|
124
|
+
* `finish(text, { format })` toggle, which fixed system copy uses to deliver
|
|
125
|
+
* plain text without MarkdownV2 escaping.
|
|
126
|
+
*/
|
|
127
|
+
export class TelegramMessageStream {
|
|
128
|
+
transport;
|
|
129
|
+
inner;
|
|
130
|
+
formatMarkdown;
|
|
131
|
+
constructor(options) {
|
|
132
|
+
const maxMessageChars = options.maxMessageChars ?? DEFAULT_MAX_MESSAGE_CHARS;
|
|
133
|
+
if (!Number.isInteger(maxMessageChars) || maxMessageChars < 32) {
|
|
134
|
+
throw new RangeError("maxMessageChars must be an integer of at least 32.");
|
|
417
135
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
resolve();
|
|
427
|
-
};
|
|
428
|
-
const timer = setTimeout(() => {
|
|
429
|
-
cleanup();
|
|
430
|
-
resolve();
|
|
431
|
-
}, ms);
|
|
432
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
136
|
+
this.formatMarkdown = options.formatMarkdown ?? true;
|
|
137
|
+
this.transport = new TelegramChannelTransport({
|
|
138
|
+
api: options.api,
|
|
139
|
+
chatId: options.chatId,
|
|
140
|
+
maxMessageChars,
|
|
141
|
+
replyToMessageId: options.replyToMessageId,
|
|
142
|
+
// Streaming begins with the configured formatting; finish() may override it.
|
|
143
|
+
markdownEnabled: this.formatMarkdown,
|
|
433
144
|
});
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
145
|
+
const innerOptions = {
|
|
146
|
+
transport: this.transport,
|
|
147
|
+
maxMessageChars,
|
|
148
|
+
// The transport gates MarkdownV2 via `markdownEnabled`; the substrate always
|
|
149
|
+
// attempts the final render so its render() hook reaches our transport.
|
|
150
|
+
formatMarkdown: true,
|
|
151
|
+
};
|
|
152
|
+
if (options.initialStatusText !== undefined) {
|
|
153
|
+
innerOptions.initialStatusText = normalizeTelegramText(options.initialStatusText);
|
|
439
154
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
155
|
+
else {
|
|
156
|
+
innerOptions.initialStatusText = DEFAULT_INITIAL_STATUS_TEXT;
|
|
157
|
+
}
|
|
158
|
+
if (options.editDebounceMs !== undefined) {
|
|
159
|
+
innerOptions.editDebounceMs = options.editDebounceMs;
|
|
160
|
+
}
|
|
161
|
+
if (options.maxSendRetries !== undefined) {
|
|
162
|
+
innerOptions.maxSendRetries = options.maxSendRetries;
|
|
163
|
+
}
|
|
164
|
+
if (options.retryCapMs !== undefined) {
|
|
165
|
+
innerOptions.retryCapMs = options.retryCapMs;
|
|
166
|
+
}
|
|
167
|
+
if (options.retryBaseDelayMs !== undefined) {
|
|
168
|
+
innerOptions.retryBaseDelayMs = options.retryBaseDelayMs;
|
|
169
|
+
}
|
|
170
|
+
if (options.showHints !== undefined) {
|
|
171
|
+
innerOptions.showHints = options.showHints;
|
|
445
172
|
}
|
|
173
|
+
if (options.finalOnly !== undefined) {
|
|
174
|
+
innerOptions.finalOnly = options.finalOnly;
|
|
175
|
+
}
|
|
176
|
+
if (options.abortSignal !== undefined) {
|
|
177
|
+
innerOptions.abortSignal = options.abortSignal;
|
|
178
|
+
}
|
|
179
|
+
if (options.logger !== undefined) {
|
|
180
|
+
innerOptions.logger = options.logger;
|
|
181
|
+
}
|
|
182
|
+
this.inner = new ResilientMessageStream(innerOptions);
|
|
183
|
+
}
|
|
184
|
+
async status(text) {
|
|
185
|
+
await this.inner.status(text);
|
|
186
|
+
}
|
|
187
|
+
async append(delta) {
|
|
188
|
+
await this.inner.append(delta);
|
|
446
189
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
190
|
+
async replace(text) {
|
|
191
|
+
await this.inner.replace(text);
|
|
192
|
+
}
|
|
193
|
+
async event(event) {
|
|
194
|
+
await this.inner.event(event);
|
|
195
|
+
}
|
|
196
|
+
async finish(finalText, options) {
|
|
197
|
+
// Fixed system copy (e.g. "Cancelled.") is delivered as plain text — the
|
|
198
|
+
// transport's markdown gate is toggled for this finish so the answer is not
|
|
199
|
+
// re-rendered as MarkdownV2.
|
|
200
|
+
this.transport.markdownEnabled = this.formatMarkdown && (options?.format ?? true);
|
|
201
|
+
try {
|
|
202
|
+
await this.inner.finish(finalText);
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
// The substrate throws the shared ChannelDeliveryError. Normalize it to
|
|
206
|
+
// TelegramDeliveryError so callers that catch the Telegram type keep
|
|
207
|
+
// working and the base type never escapes the adapter (parity with Slack).
|
|
208
|
+
if (error instanceof ChannelDeliveryError && !(error instanceof TelegramDeliveryError)) {
|
|
209
|
+
throw new TelegramDeliveryError("Telegram final delivery failed.", {
|
|
210
|
+
cause: error.cause,
|
|
211
|
+
attempts: error.attempts,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
throw error;
|
|
450
215
|
}
|
|
451
216
|
}
|
|
452
217
|
}
|
|
@@ -497,10 +262,7 @@ export function classifyTelegramError(error) {
|
|
|
497
262
|
return { kind: "retry" };
|
|
498
263
|
}
|
|
499
264
|
export function splitTelegramText(text, maxChars) {
|
|
500
|
-
return splitTextByCodePoints(normalizeTrailing(text,
|
|
501
|
-
}
|
|
502
|
-
function buildStreamingPreview(text, maxChars) {
|
|
503
|
-
return buildStreamingTailPreview(normalizeTrailing(text, EMPTY_FINAL_TEXT), maxChars, "…\n");
|
|
265
|
+
return splitTextByCodePoints(normalizeTrailing(text, ""), maxChars);
|
|
504
266
|
}
|
|
505
267
|
function normalizeTelegramText(text) {
|
|
506
268
|
return text.trimEnd();
|
|
@@ -509,7 +271,4 @@ function normalizeTelegramText(text) {
|
|
|
509
271
|
function countCodePoints(text) {
|
|
510
272
|
return [...text].length;
|
|
511
273
|
}
|
|
512
|
-
function errorMessage(error) {
|
|
513
|
-
return error instanceof Error ? error.message : String(error);
|
|
514
|
-
}
|
|
515
274
|
//# sourceMappingURL=message-stream.js.map
|