@mono-agent/telegram-adapter 0.3.0 → 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.
@@ -1,397 +1,99 @@
1
- import { DEFAULT_EMPTY_FINAL_TEXT, DEFAULT_MAX_MESSAGE_CHARS, buildStreamingTailPreview, normalizeTrailing, splitTextByCodePoints, } from "@mono-agent/agent-contracts";
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 Error {
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
- const DEFAULT_EDIT_DEBOUNCE_MS = 750;
21
- const DEFAULT_MAX_SEND_RETRIES = 3;
22
- const DEFAULT_RETRY_CAP_MS = 60_000;
23
- const DEFAULT_RETRY_BASE_DELAY_MS = 500;
24
- const EMPTY_FINAL_TEXT = DEFAULT_EMPTY_FINAL_TEXT;
25
- const THOUGHT_PREFIX = "💭 ";
26
- export class TelegramMessageStream {
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.initialStatusText = normalizeTelegramText(options.initialStatusText ?? DEFAULT_INITIAL_STATUS_TEXT);
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.maxSendRetries = options.maxSendRetries ?? DEFAULT_MAX_SEND_RETRIES;
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
- async finish(finalText, options) {
152
- if (this.finished) {
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
- interimDisplayText() {
193
- if (this.hasAnswerText || this.currentText.length > 0) {
194
- return this.currentText;
195
- }
196
- if (this.showThoughts && this.thinkingText.length > 0) {
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 ensureMessage() {
202
- if (this.sentMessage !== undefined) {
203
- return this.sentMessage;
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
- scheduleEdit() {
231
- this.cancelScheduledEdit();
232
- if (this.editDebounceMs === 0) {
233
- this.startInFlightEdit();
234
- return;
67
+ classifyError(error) {
68
+ if (isMarkdownOverflowError(error)) {
69
+ return { kind: "reformat_plain" };
235
70
  }
236
- this.editTimer = setTimeout(() => {
237
- this.editTimer = undefined;
238
- this.startInFlightEdit();
239
- }, this.editDebounceMs);
71
+ return classifyTelegramError(error);
240
72
  }
241
- startInFlightEdit() {
242
- const text = buildStreamingPreview(this.interimDisplayText(), this.maxMessageChars);
243
- this.inFlightEdit = this.deliverText(text, { final: false }).catch((error) => {
244
- // Interim edits are best-effort; deliverText already swallows, but guard
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
- * 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.
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
- 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
- });
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(message, text, useMarkdown) {
93
+ buildEditParams(ref, text, useMarkdown) {
392
94
  const params = {
393
95
  chat_id: this.chatId,
394
- message_id: message.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
- retryDelayMs(retryAfterMs, attempt) {
410
- const backoff = this.retryBaseDelayMs * 2 ** (attempt - 1);
411
- const chosen = retryAfterMs ?? backoff;
412
- return Math.min(chosen, this.retryCapMs);
413
- }
414
- sleep(ms) {
415
- if (ms <= 0 || this.abortSignal?.aborted === true) {
416
- return Promise.resolve();
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
- return new Promise((resolve) => {
419
- const signal = this.abortSignal;
420
- const cleanup = () => {
421
- clearTimeout(timer);
422
- signal?.removeEventListener("abort", onAbort);
423
- };
424
- const onAbort = () => {
425
- cleanup();
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
- cancelScheduledEdit() {
436
- if (this.editTimer !== undefined) {
437
- clearTimeout(this.editTimer);
438
- this.editTimer = undefined;
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
- async awaitInFlightEdit() {
442
- if (this.inFlightEdit !== undefined) {
443
- await this.inFlightEdit;
444
- this.inFlightEdit = undefined;
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
- assertOpen() {
448
- if (this.finished) {
449
- throw new Error("Cannot write to a finished TelegramMessageStream.");
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, EMPTY_FINAL_TEXT), maxChars);
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