@mono-agent/telegram-adapter 0.3.0 → 0.4.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/README.md +13 -8
- package/dist/adapter.d.ts +128 -2
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +387 -10
- package/dist/adapter.js.map +1 -1
- package/dist/ask.d.ts +17 -0
- package/dist/ask.d.ts.map +1 -0
- package/dist/ask.js +21 -0
- package/dist/ask.js.map +1 -0
- package/dist/bot.d.ts +198 -6
- package/dist/bot.d.ts.map +1 -1
- package/dist/bot.js +1292 -40
- package/dist/bot.js.map +1 -1
- package/dist/config.d.ts +83 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +306 -45
- package/dist/config.js.map +1 -1
- package/dist/grammy-client.d.ts +3 -0
- package/dist/grammy-client.d.ts.map +1 -1
- package/dist/grammy-client.js +64 -1
- package/dist/grammy-client.js.map +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/message-stream.d.ts +37 -52
- package/dist/message-stream.d.ts.map +1 -1
- package/dist/message-stream.js +169 -404
- package/dist/message-stream.js.map +1 -1
- package/dist/start.d.ts +46 -2
- package/dist/start.d.ts.map +1 -1
- package/dist/start.js +16 -1
- package/dist/start.js.map +1 -1
- package/dist/types.d.ts +96 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -3
package/dist/message-stream.js
CHANGED
|
@@ -1,397 +1,101 @@
|
|
|
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
|
-
|
|
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;
|
|
43
|
+
silent;
|
|
51
44
|
constructor(options) {
|
|
52
45
|
this.api = options.api;
|
|
53
46
|
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;
|
|
47
|
+
this.maxMessageChars = options.maxMessageChars;
|
|
58
48
|
this.replyToMessageId = options.replyToMessageId;
|
|
59
|
-
this.
|
|
60
|
-
this.
|
|
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
|
-
}
|
|
49
|
+
this.markdownEnabled = options.markdownEnabled;
|
|
50
|
+
this.silent = options.silent;
|
|
150
51
|
}
|
|
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);
|
|
52
|
+
renderMarkdown(text) {
|
|
53
|
+
if (!this.markdownEnabled) {
|
|
54
|
+
return text;
|
|
190
55
|
}
|
|
56
|
+
return renderTelegramMarkdown(text);
|
|
191
57
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
return `${THOUGHT_PREFIX}${this.thinkingText}`;
|
|
198
|
-
}
|
|
199
|
-
return this.statusText;
|
|
58
|
+
async post(text, options) {
|
|
59
|
+
const useMarkdown = options.markdown && this.markdownEnabled;
|
|
60
|
+
this.assertWithinLimit(text, useMarkdown);
|
|
61
|
+
const sent = await this.api.sendMessage(this.buildSendParams(text, useMarkdown));
|
|
62
|
+
return { id: String(sent.message_id), message_id: sent.message_id };
|
|
200
63
|
}
|
|
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;
|
|
64
|
+
async edit(ref, text, options) {
|
|
65
|
+
const useMarkdown = options.markdown && this.markdownEnabled;
|
|
66
|
+
this.assertWithinLimit(text, useMarkdown);
|
|
67
|
+
await this.api.editMessageText(this.buildEditParams(ref, text, useMarkdown));
|
|
229
68
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
this.startInFlightEdit();
|
|
234
|
-
return;
|
|
69
|
+
classifyError(error) {
|
|
70
|
+
if (isMarkdownOverflowError(error)) {
|
|
71
|
+
return { kind: "reformat_plain" };
|
|
235
72
|
}
|
|
236
|
-
|
|
237
|
-
this.editTimer = undefined;
|
|
238
|
-
this.startInFlightEdit();
|
|
239
|
-
}, this.editDebounceMs);
|
|
73
|
+
return classifyTelegramError(error);
|
|
240
74
|
}
|
|
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;
|
|
251
|
-
}
|
|
252
|
-
/**
|
|
253
|
-
* Send `sourceText` to Telegram, classifying failures and recovering where
|
|
254
|
-
* possible. Interim edits (`final: false`) are best-effort and never throw;
|
|
255
|
-
* final delivery retries transient failures and throws TelegramDeliveryError
|
|
256
|
-
* only when every path is exhausted.
|
|
257
|
-
*/
|
|
258
|
-
async deliverText(sourceText, options) {
|
|
259
|
-
const normalizedSource = normalizeTelegramText(sourceText);
|
|
260
|
-
let useMarkdown = options.final && this.formatMarkdown && (options.format ?? true);
|
|
261
|
-
let renderedText = useMarkdown ? renderTelegramMarkdown(normalizedSource) : normalizedSource;
|
|
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
|
-
});
|
|
75
|
+
async indicateActivity() {
|
|
76
|
+
// Telegram "typing…" chat action; expires after ~5s so the substrate
|
|
77
|
+
// refreshes it while the agent works. No-op if the sender lacks the method.
|
|
78
|
+
await this.api.sendChatAction?.({ chat_id: this.chatId, action: "typing" });
|
|
331
79
|
}
|
|
332
80
|
/**
|
|
333
|
-
*
|
|
334
|
-
*
|
|
81
|
+
* MarkdownV2 escaping can expand a chunk past Telegram's size limit even though
|
|
82
|
+
* the plain source is within it (chunks are split on the source length). Rather
|
|
83
|
+
* than send and fail with "message is too long", we signal a reformat-to-plain
|
|
84
|
+
* recovery; the substrate then re-delivers the plain source within the limit.
|
|
85
|
+
* (We never test renderedText === source to decide this: telegramify renders
|
|
86
|
+
* inline code / links back to identical bytes that still need parse_mode, so
|
|
87
|
+
* equality is not a plain-text signal.)
|
|
335
88
|
*/
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
}
|
|
89
|
+
assertWithinLimit(text, useMarkdown) {
|
|
90
|
+
if (useMarkdown && countCodePoints(text) > this.maxMessageChars) {
|
|
91
|
+
const overflow = { [MARKDOWN_OVERFLOW]: true };
|
|
92
|
+
throw overflow;
|
|
389
93
|
}
|
|
390
94
|
}
|
|
391
|
-
buildEditParams(
|
|
95
|
+
buildEditParams(ref, text, useMarkdown) {
|
|
392
96
|
const params = {
|
|
393
97
|
chat_id: this.chatId,
|
|
394
|
-
message_id:
|
|
98
|
+
message_id: messageIdOf(ref),
|
|
395
99
|
text,
|
|
396
100
|
};
|
|
397
101
|
if (useMarkdown) {
|
|
@@ -404,49 +108,116 @@ export class TelegramMessageStream {
|
|
|
404
108
|
if (useMarkdown) {
|
|
405
109
|
params.parse_mode = "MarkdownV2";
|
|
406
110
|
}
|
|
111
|
+
if (this.replyToMessageId !== undefined) {
|
|
112
|
+
params.reply_to_message_id = this.replyToMessageId;
|
|
113
|
+
}
|
|
114
|
+
if (this.silent) {
|
|
115
|
+
params.disable_notification = true;
|
|
116
|
+
}
|
|
407
117
|
return params;
|
|
408
118
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
119
|
+
}
|
|
120
|
+
function messageIdOf(ref) {
|
|
121
|
+
const raw = ref.message_id;
|
|
122
|
+
return typeof raw === "number" ? raw : Number(ref.id);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Thin wrapper over the shared {@link ResilientMessageStream}: builds a Telegram
|
|
126
|
+
* {@link ChannelTransport} and delegates all streaming/finish behavior to the
|
|
127
|
+
* substrate, preserving this adapter's public API and no-labels + activity-hints
|
|
128
|
+
* behavior. The only Telegram-specific lever the wrapper retains is the per-call
|
|
129
|
+
* `finish(text, { format })` toggle, which fixed system copy uses to deliver
|
|
130
|
+
* plain text without MarkdownV2 escaping.
|
|
131
|
+
*/
|
|
132
|
+
export class TelegramMessageStream {
|
|
133
|
+
transport;
|
|
134
|
+
inner;
|
|
135
|
+
formatMarkdown;
|
|
136
|
+
constructor(options) {
|
|
137
|
+
const maxMessageChars = options.maxMessageChars ?? DEFAULT_MAX_MESSAGE_CHARS;
|
|
138
|
+
if (!Number.isInteger(maxMessageChars) || maxMessageChars < 32) {
|
|
139
|
+
throw new RangeError("maxMessageChars must be an integer of at least 32.");
|
|
417
140
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
};
|
|
428
|
-
const timer = setTimeout(() => {
|
|
429
|
-
cleanup();
|
|
430
|
-
resolve();
|
|
431
|
-
}, ms);
|
|
432
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
141
|
+
this.formatMarkdown = options.formatMarkdown ?? true;
|
|
142
|
+
this.transport = new TelegramChannelTransport({
|
|
143
|
+
api: options.api,
|
|
144
|
+
chatId: options.chatId,
|
|
145
|
+
maxMessageChars,
|
|
146
|
+
replyToMessageId: options.replyToMessageId,
|
|
147
|
+
// Streaming begins with the configured formatting; finish() may override it.
|
|
148
|
+
markdownEnabled: this.formatMarkdown,
|
|
149
|
+
silent: options.silent ?? false,
|
|
433
150
|
});
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
151
|
+
const innerOptions = {
|
|
152
|
+
transport: this.transport,
|
|
153
|
+
maxMessageChars,
|
|
154
|
+
// The transport gates MarkdownV2 via `markdownEnabled`; the substrate always
|
|
155
|
+
// attempts the final render so its render() hook reaches our transport.
|
|
156
|
+
formatMarkdown: true,
|
|
157
|
+
};
|
|
158
|
+
if (options.initialStatusText !== undefined) {
|
|
159
|
+
innerOptions.initialStatusText = normalizeTelegramText(options.initialStatusText);
|
|
439
160
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
161
|
+
else {
|
|
162
|
+
innerOptions.initialStatusText = DEFAULT_INITIAL_STATUS_TEXT;
|
|
163
|
+
}
|
|
164
|
+
if (options.editDebounceMs !== undefined) {
|
|
165
|
+
innerOptions.editDebounceMs = options.editDebounceMs;
|
|
166
|
+
}
|
|
167
|
+
if (options.maxSendRetries !== undefined) {
|
|
168
|
+
innerOptions.maxSendRetries = options.maxSendRetries;
|
|
169
|
+
}
|
|
170
|
+
if (options.retryCapMs !== undefined) {
|
|
171
|
+
innerOptions.retryCapMs = options.retryCapMs;
|
|
172
|
+
}
|
|
173
|
+
if (options.retryBaseDelayMs !== undefined) {
|
|
174
|
+
innerOptions.retryBaseDelayMs = options.retryBaseDelayMs;
|
|
445
175
|
}
|
|
176
|
+
if (options.showHints !== undefined) {
|
|
177
|
+
innerOptions.showHints = options.showHints;
|
|
178
|
+
}
|
|
179
|
+
if (options.finalOnly !== undefined) {
|
|
180
|
+
innerOptions.finalOnly = options.finalOnly;
|
|
181
|
+
}
|
|
182
|
+
if (options.abortSignal !== undefined) {
|
|
183
|
+
innerOptions.abortSignal = options.abortSignal;
|
|
184
|
+
}
|
|
185
|
+
if (options.logger !== undefined) {
|
|
186
|
+
innerOptions.logger = options.logger;
|
|
187
|
+
}
|
|
188
|
+
this.inner = new ResilientMessageStream(innerOptions);
|
|
189
|
+
}
|
|
190
|
+
async status(text) {
|
|
191
|
+
await this.inner.status(text);
|
|
192
|
+
}
|
|
193
|
+
async append(delta) {
|
|
194
|
+
await this.inner.append(delta);
|
|
195
|
+
}
|
|
196
|
+
async replace(text) {
|
|
197
|
+
await this.inner.replace(text);
|
|
446
198
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
199
|
+
async event(event) {
|
|
200
|
+
await this.inner.event(event);
|
|
201
|
+
}
|
|
202
|
+
async finish(finalText, options) {
|
|
203
|
+
// Fixed system copy (e.g. "Cancelled.") is delivered as plain text — the
|
|
204
|
+
// transport's markdown gate is toggled for this finish so the answer is not
|
|
205
|
+
// re-rendered as MarkdownV2.
|
|
206
|
+
this.transport.markdownEnabled = this.formatMarkdown && (options?.format ?? true);
|
|
207
|
+
try {
|
|
208
|
+
await this.inner.finish(finalText);
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
// The substrate throws the shared ChannelDeliveryError. Normalize it to
|
|
212
|
+
// TelegramDeliveryError so callers that catch the Telegram type keep
|
|
213
|
+
// working and the base type never escapes the adapter (parity with Slack).
|
|
214
|
+
if (error instanceof ChannelDeliveryError && !(error instanceof TelegramDeliveryError)) {
|
|
215
|
+
throw new TelegramDeliveryError("Telegram final delivery failed.", {
|
|
216
|
+
cause: error.cause,
|
|
217
|
+
attempts: error.attempts,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
throw error;
|
|
450
221
|
}
|
|
451
222
|
}
|
|
452
223
|
}
|
|
@@ -497,10 +268,7 @@ export function classifyTelegramError(error) {
|
|
|
497
268
|
return { kind: "retry" };
|
|
498
269
|
}
|
|
499
270
|
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");
|
|
271
|
+
return splitTextByCodePoints(normalizeTrailing(text, ""), maxChars);
|
|
504
272
|
}
|
|
505
273
|
function normalizeTelegramText(text) {
|
|
506
274
|
return text.trimEnd();
|
|
@@ -509,7 +277,4 @@ function normalizeTelegramText(text) {
|
|
|
509
277
|
function countCodePoints(text) {
|
|
510
278
|
return [...text].length;
|
|
511
279
|
}
|
|
512
|
-
function errorMessage(error) {
|
|
513
|
-
return error instanceof Error ? error.message : String(error);
|
|
514
|
-
}
|
|
515
280
|
//# sourceMappingURL=message-stream.js.map
|