@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.
@@ -1,397 +1,101 @@
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;
43
+ silent;
51
44
  constructor(options) {
52
45
  this.api = options.api;
53
46
  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;
47
+ this.maxMessageChars = options.maxMessageChars;
58
48
  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
- }
49
+ this.markdownEnabled = options.markdownEnabled;
50
+ this.silent = options.silent;
150
51
  }
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);
52
+ renderMarkdown(text) {
53
+ if (!this.markdownEnabled) {
54
+ return text;
190
55
  }
56
+ return renderTelegramMarkdown(text);
191
57
  }
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;
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 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;
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
- scheduleEdit() {
231
- this.cancelScheduledEdit();
232
- if (this.editDebounceMs === 0) {
233
- this.startInFlightEdit();
234
- return;
69
+ classifyError(error) {
70
+ if (isMarkdownOverflowError(error)) {
71
+ return { kind: "reformat_plain" };
235
72
  }
236
- this.editTimer = setTimeout(() => {
237
- this.editTimer = undefined;
238
- this.startInFlightEdit();
239
- }, this.editDebounceMs);
73
+ return classifyTelegramError(error);
240
74
  }
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;
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
- * 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.
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
- 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
- }
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(message, text, useMarkdown) {
95
+ buildEditParams(ref, text, useMarkdown) {
392
96
  const params = {
393
97
  chat_id: this.chatId,
394
- message_id: message.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
- 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();
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
- 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 });
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
- cancelScheduledEdit() {
436
- if (this.editTimer !== undefined) {
437
- clearTimeout(this.editTimer);
438
- this.editTimer = undefined;
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
- async awaitInFlightEdit() {
442
- if (this.inFlightEdit !== undefined) {
443
- await this.inFlightEdit;
444
- this.inFlightEdit = undefined;
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
- assertOpen() {
448
- if (this.finished) {
449
- throw new Error("Cannot write to a finished TelegramMessageStream.");
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, EMPTY_FINAL_TEXT), maxChars);
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