@markusylisiurunen/tau 0.2.53 → 0.2.55

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,3 +1,6 @@
1
+ import { mkdtemp, readdir, rm, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { basename, extname, join } from "node:path";
1
4
  import { Api } from "grammy";
2
5
  import { transcribeMistralAudio } from "../utils/mistral_transcription.js";
3
6
  import { isRecord } from "../utils/type_guards.js";
@@ -9,12 +12,86 @@ const DEFAULT_TELEGRAM_VOICE_MIME_TYPE = "audio/ogg";
9
12
  const DEFAULT_TELEGRAM_VOICE_FILE_NAME = "voice.ogg";
10
13
  const DEFAULT_TELEGRAM_AUDIO_MIME_TYPE = "audio/mpeg";
11
14
  const DEFAULT_TELEGRAM_AUDIO_FILE_NAME = "audio.mp3";
15
+ const DEFAULT_TELEGRAM_PHOTO_MIME_TYPE = "image/jpeg";
16
+ const DEFAULT_TELEGRAM_DOCUMENT_MIME_TYPE = "application/octet-stream";
12
17
  const MESSAGE_QUEUED_REACTION_EMOJI = "👀";
13
18
  const MESSAGE_QUEUED_REACTION_DELAY_MS = 1000;
14
19
  const ABORTED = Symbol("aborted");
15
20
  const CALLBACK_ACTION_PREFIX = "tau:action:";
16
21
  const CALLBACK_USE_PREFIX = "tau:use:";
17
22
  const MAX_SESSION_PREVIEW_CHARS = 64;
23
+ const MAX_TELEGRAM_ATTACHMENTS_PER_TURN = 10;
24
+ const MAX_TELEGRAM_ATTACHMENT_FILE_BYTES = 20 * 1024 * 1024;
25
+ const MAX_TELEGRAM_ATTACHMENT_TOTAL_BYTES = 50 * 1024 * 1024;
26
+ const TELEGRAM_ATTACHMENT_TEMP_DIR_PREFIX = "tau-telegram-attachments-";
27
+ const SUPPORTED_TEXT_ATTACHMENT_EXTENSIONS = new Set([
28
+ ".txt",
29
+ ".md",
30
+ ".json",
31
+ ".csv",
32
+ ".yaml",
33
+ ".yml",
34
+ ]);
35
+ const SUPPORTED_IMAGE_ATTACHMENT_EXTENSIONS = new Set([".jpg", ".jpeg", ".png", ".webp", ".gif"]);
36
+ const SUPPORTED_TEXT_ATTACHMENT_MIME_TYPES = new Set([
37
+ "text/plain",
38
+ "text/markdown",
39
+ "application/json",
40
+ "text/csv",
41
+ "application/csv",
42
+ "text/yaml",
43
+ "application/yaml",
44
+ "application/x-yaml",
45
+ "text/x-yaml",
46
+ ]);
47
+ const MIME_EXTENSION_BY_TYPE = {
48
+ "application/pdf": ".pdf",
49
+ "application/json": ".json",
50
+ "text/markdown": ".md",
51
+ "text/csv": ".csv",
52
+ "application/csv": ".csv",
53
+ "text/plain": ".txt",
54
+ "text/yaml": ".yaml",
55
+ "application/yaml": ".yaml",
56
+ "application/x-yaml": ".yaml",
57
+ "text/x-yaml": ".yaml",
58
+ "image/jpeg": ".jpg",
59
+ "image/png": ".png",
60
+ "image/webp": ".webp",
61
+ "image/gif": ".gif",
62
+ };
63
+ const MIME_TYPE_BY_EXTENSION = {
64
+ ".pdf": "application/pdf",
65
+ ".txt": "text/plain",
66
+ ".md": "text/markdown",
67
+ ".json": "application/json",
68
+ ".csv": "text/csv",
69
+ ".yaml": "text/yaml",
70
+ ".yml": "text/yaml",
71
+ ".jpg": "image/jpeg",
72
+ ".jpeg": "image/jpeg",
73
+ ".png": "image/png",
74
+ ".webp": "image/webp",
75
+ ".gif": "image/gif",
76
+ };
77
+ async function sweepStaleTelegramAttachmentTempDirs() {
78
+ const systemTmpDir = tmpdir();
79
+ try {
80
+ const entries = await readdir(systemTmpDir, { withFileTypes: true, encoding: "utf8" });
81
+ for (const entry of entries) {
82
+ if (!entry.isDirectory() || !entry.name.startsWith(TELEGRAM_ATTACHMENT_TEMP_DIR_PREFIX)) {
83
+ continue;
84
+ }
85
+ try {
86
+ await rm(join(systemTmpDir, entry.name), { recursive: true, force: true });
87
+ }
88
+ catch { }
89
+ }
90
+ }
91
+ catch {
92
+ return;
93
+ }
94
+ }
18
95
  const TELEGRAM_COMMANDS = [
19
96
  { command: "help", description: "show supported commands" },
20
97
  { command: "new", description: "start a new session" },
@@ -23,7 +100,6 @@ const TELEGRAM_COMMANDS = [
23
100
  { command: "sessions", description: "list sessions" },
24
101
  { command: "status", description: "show active session status" },
25
102
  { command: "interrupt", description: "interrupt active run" },
26
- { command: "cancel", description: "cancel active session" },
27
103
  { command: "close", description: "close session(s)" },
28
104
  { command: "verbose", description: "stream progress updates" },
29
105
  { command: "quiet", description: "only send final assistant message" },
@@ -52,6 +128,112 @@ function truncateText(text, maxChars) {
52
128
  }
53
129
  return `${trimmed.slice(0, maxChars - 1)}…`;
54
130
  }
131
+ function normalizeSizeBytes(value) {
132
+ if (typeof value !== "number" || !Number.isFinite(value)) {
133
+ return undefined;
134
+ }
135
+ const rounded = Math.floor(value);
136
+ if (rounded <= 0) {
137
+ return undefined;
138
+ }
139
+ return rounded;
140
+ }
141
+ function selectLargestPhotoVariant(variants) {
142
+ if (!variants || variants.length === 0) {
143
+ return undefined;
144
+ }
145
+ let selected;
146
+ let selectedScore = -1;
147
+ for (const variant of variants) {
148
+ const fileId = variant.file_id?.trim();
149
+ if (!fileId) {
150
+ continue;
151
+ }
152
+ const fileSize = normalizeSizeBytes(variant.file_size);
153
+ const width = normalizeSizeBytes(variant.width) ?? 0;
154
+ const height = normalizeSizeBytes(variant.height) ?? 0;
155
+ const resolutionScore = width * height;
156
+ const sizeScore = fileSize ?? 0;
157
+ const score = Math.max(sizeScore, resolutionScore);
158
+ if (!selected || score >= selectedScore) {
159
+ selected = variant;
160
+ selectedScore = score;
161
+ }
162
+ }
163
+ const selectedFileId = selected?.file_id?.trim();
164
+ if (!selectedFileId) {
165
+ return undefined;
166
+ }
167
+ return {
168
+ fileId: selectedFileId,
169
+ sizeBytes: normalizeSizeBytes(selected?.file_size),
170
+ };
171
+ }
172
+ function inferExtensionFromMimeType(mimeType) {
173
+ const trimmed = mimeType.trim().toLowerCase();
174
+ if (!trimmed) {
175
+ return undefined;
176
+ }
177
+ return MIME_EXTENSION_BY_TYPE[trimmed];
178
+ }
179
+ function sanitizeAttachmentFileName(fileName, fallback) {
180
+ const trimmed = basename(fileName.trim());
181
+ const normalized = trimmed
182
+ .replace(/\s+/g, "-")
183
+ .replace(/[^a-zA-Z0-9._-]/g, "-")
184
+ .replace(/-+/g, "-")
185
+ .replace(/^[-.]+/, "")
186
+ .replace(/[-.]+$/, "");
187
+ if (!normalized) {
188
+ return fallback;
189
+ }
190
+ return normalized;
191
+ }
192
+ function inferMimeTypeFromFileName(fileName) {
193
+ const extension = extname(fileName.trim()).toLowerCase();
194
+ if (!extension) {
195
+ return undefined;
196
+ }
197
+ return MIME_TYPE_BY_EXTENSION[extension];
198
+ }
199
+ function isSupportedDocumentAttachment(mimeType, fileName) {
200
+ const normalizedMimeType = mimeType.trim().toLowerCase();
201
+ if (normalizedMimeType.startsWith("image/")) {
202
+ return true;
203
+ }
204
+ if (normalizedMimeType === "application/pdf") {
205
+ return true;
206
+ }
207
+ if (SUPPORTED_TEXT_ATTACHMENT_MIME_TYPES.has(normalizedMimeType)) {
208
+ return true;
209
+ }
210
+ const extension = extname(fileName.trim()).toLowerCase();
211
+ if (SUPPORTED_TEXT_ATTACHMENT_EXTENSIONS.has(extension)) {
212
+ return true;
213
+ }
214
+ if (extension === ".pdf") {
215
+ return true;
216
+ }
217
+ return SUPPORTED_IMAGE_ATTACHMENT_EXTENSIONS.has(extension);
218
+ }
219
+ function describeAttachmentLimitBytes(sizeBytes) {
220
+ const megabytes = sizeBytes / (1024 * 1024);
221
+ if (Number.isInteger(megabytes)) {
222
+ return `${megabytes} MB`;
223
+ }
224
+ return `${megabytes.toFixed(1)} MB`;
225
+ }
226
+ function describeAttachment(fileName, mimeType) {
227
+ const trimmedFileName = fileName.trim();
228
+ const trimmedMimeType = mimeType.trim();
229
+ if (trimmedFileName) {
230
+ return `'${trimmedFileName}'`;
231
+ }
232
+ if (trimmedMimeType) {
233
+ return `'${trimmedMimeType}'`;
234
+ }
235
+ return "attachment";
236
+ }
55
237
  function describeSession(session, details = {}) {
56
238
  return [
57
239
  formatSessionHeadline(session.id, "status"),
@@ -136,6 +318,9 @@ class AsyncTelegramAdapterImpl {
136
318
  lastCommandBySession = new Map();
137
319
  lastAssistantMessageBySession = new Map();
138
320
  latestAssistantMessageByRun = new Map();
321
+ pendingAttachmentsBySession = new Map();
322
+ pendingAttachmentTempDirBySession = new Map();
323
+ attachmentTempDirsBySession = new Map();
139
324
  unsubscribeSessionEvents;
140
325
  loopPromise;
141
326
  nextUpdateOffset = 0;
@@ -176,6 +361,11 @@ class AsyncTelegramAdapterImpl {
176
361
  this.closed = true;
177
362
  this.abortController.abort();
178
363
  this.unsubscribeSessionEvents();
364
+ for (const sessionId of Array.from(this.attachmentTempDirsBySession.keys())) {
365
+ await this.cleanupSessionAttachmentTempDirs(sessionId);
366
+ }
367
+ this.pendingAttachmentsBySession.clear();
368
+ this.pendingAttachmentTempDirBySession.clear();
179
369
  try {
180
370
  await this.loopPromise;
181
371
  }
@@ -275,8 +465,12 @@ class AsyncTelegramAdapterImpl {
275
465
  return;
276
466
  }
277
467
  const text = typeof message.text === "string" ? message.text.trim() : "";
468
+ const isCommand = text.startsWith("/");
469
+ if (!isCommand) {
470
+ await this.queueMessageAttachments(chatId, message);
471
+ }
278
472
  if (text) {
279
- if (text.startsWith("/")) {
473
+ if (isCommand) {
280
474
  await this.handleCommand(chatId, text);
281
475
  return;
282
476
  }
@@ -335,6 +529,222 @@ class AsyncTelegramAdapterImpl {
335
529
  fileName: message.audio?.file_name?.trim() || DEFAULT_TELEGRAM_AUDIO_FILE_NAME,
336
530
  };
337
531
  }
532
+ async queueMessageAttachments(chatId, message) {
533
+ const caption = typeof message.caption === "string" ? message.caption.trim() : "";
534
+ const attachmentCaption = caption || undefined;
535
+ const parsedAttachments = [];
536
+ const photo = selectLargestPhotoVariant(message.photo);
537
+ if (photo) {
538
+ const extension = inferExtensionFromMimeType(DEFAULT_TELEGRAM_PHOTO_MIME_TYPE) ?? ".jpg";
539
+ const fileName = sanitizeAttachmentFileName(`photo${extension}`, `photo${extension}`);
540
+ parsedAttachments.push({
541
+ fileId: photo.fileId,
542
+ fileName,
543
+ mimeType: DEFAULT_TELEGRAM_PHOTO_MIME_TYPE,
544
+ sizeBytes: photo.sizeBytes,
545
+ caption: attachmentCaption,
546
+ });
547
+ }
548
+ const documentFileId = message.document?.file_id?.trim();
549
+ if (documentFileId) {
550
+ const rawMimeType = message.document?.mime_type?.trim().toLowerCase();
551
+ const mimeTypeForExtension = rawMimeType && rawMimeType !== DEFAULT_TELEGRAM_DOCUMENT_MIME_TYPE
552
+ ? rawMimeType
553
+ : undefined;
554
+ const inferredExtension = inferExtensionFromMimeType(mimeTypeForExtension ?? "") ?? "";
555
+ const fallbackFileName = `attachment${inferredExtension}`;
556
+ const rawFileName = message.document?.file_name?.trim() || fallbackFileName;
557
+ let fileName = sanitizeAttachmentFileName(rawFileName, fallbackFileName || "attachment");
558
+ if (!extname(fileName) && inferredExtension) {
559
+ fileName = `${fileName}${inferredExtension}`;
560
+ }
561
+ const mimeType = mimeTypeForExtension ??
562
+ inferMimeTypeFromFileName(fileName) ??
563
+ rawMimeType ??
564
+ DEFAULT_TELEGRAM_DOCUMENT_MIME_TYPE;
565
+ if (!isSupportedDocumentAttachment(mimeType, fileName)) {
566
+ await this.reply(chatId, `skipped attachment ${describeAttachment(fileName, mimeType)}: unsupported file type`);
567
+ }
568
+ else {
569
+ parsedAttachments.push({
570
+ fileId: documentFileId,
571
+ fileName,
572
+ mimeType,
573
+ sizeBytes: normalizeSizeBytes(message.document?.file_size),
574
+ caption: attachmentCaption,
575
+ });
576
+ }
577
+ }
578
+ if (parsedAttachments.length === 0) {
579
+ return;
580
+ }
581
+ const session = this.getActiveOrSingleSession(chatId);
582
+ if (!session) {
583
+ await this.reply(chatId, "no active session. use /new or /sessions");
584
+ return;
585
+ }
586
+ const pending = this.pendingAttachmentsBySession.get(session.id) ?? [];
587
+ let totalSizeBytes = pending.reduce((total, attachment) => {
588
+ return total + (attachment.materialized?.sizeBytes ?? attachment.declaredSizeBytes ?? 0);
589
+ }, 0);
590
+ for (const attachment of parsedAttachments) {
591
+ const attachmentLabel = describeAttachment(attachment.fileName, attachment.mimeType);
592
+ if (pending.length >= MAX_TELEGRAM_ATTACHMENTS_PER_TURN) {
593
+ await this.reply(chatId, `skipped attachment ${attachmentLabel}: exceeds attachment limit (${MAX_TELEGRAM_ATTACHMENTS_PER_TURN} files per turn)`);
594
+ continue;
595
+ }
596
+ if (typeof attachment.sizeBytes === "number" &&
597
+ attachment.sizeBytes > MAX_TELEGRAM_ATTACHMENT_FILE_BYTES) {
598
+ await this.reply(chatId, `skipped attachment ${attachmentLabel}: exceeds per-file limit (${describeAttachmentLimitBytes(MAX_TELEGRAM_ATTACHMENT_FILE_BYTES)})`);
599
+ continue;
600
+ }
601
+ if (typeof attachment.sizeBytes === "number" &&
602
+ totalSizeBytes + attachment.sizeBytes > MAX_TELEGRAM_ATTACHMENT_TOTAL_BYTES) {
603
+ await this.reply(chatId, `skipped attachment ${attachmentLabel}: exceeds per-turn total limit (${describeAttachmentLimitBytes(MAX_TELEGRAM_ATTACHMENT_TOTAL_BYTES)})`);
604
+ continue;
605
+ }
606
+ let bytes;
607
+ try {
608
+ bytes = await this.api.downloadFile(attachment.fileId);
609
+ }
610
+ catch (error) {
611
+ await this.reply(chatId, `failed to download attachment ${attachmentLabel}: ${this.formatManagerError(error)}`);
612
+ continue;
613
+ }
614
+ const sizeBytes = bytes.byteLength;
615
+ if (sizeBytes > MAX_TELEGRAM_ATTACHMENT_FILE_BYTES) {
616
+ await this.reply(chatId, `skipped attachment ${attachmentLabel}: exceeds per-file limit (${describeAttachmentLimitBytes(MAX_TELEGRAM_ATTACHMENT_FILE_BYTES)})`);
617
+ continue;
618
+ }
619
+ if (totalSizeBytes + sizeBytes > MAX_TELEGRAM_ATTACHMENT_TOTAL_BYTES) {
620
+ await this.reply(chatId, `skipped attachment ${attachmentLabel}: exceeds per-turn total limit (${describeAttachmentLimitBytes(MAX_TELEGRAM_ATTACHMENT_TOTAL_BYTES)})`);
621
+ continue;
622
+ }
623
+ const tempDirPath = await this.getOrCreatePendingAttachmentTempDir(session.id);
624
+ const indexedFileName = `${String(pending.length + 1).padStart(2, "0")}-${attachment.fileName}`;
625
+ const filePath = join(tempDirPath, indexedFileName);
626
+ try {
627
+ await writeFile(filePath, bytes);
628
+ }
629
+ catch (error) {
630
+ await this.reply(chatId, `failed to materialize attachment ${attachmentLabel}: ${this.formatManagerError(error)}`);
631
+ continue;
632
+ }
633
+ pending.push({
634
+ fileId: attachment.fileId,
635
+ fileName: attachment.fileName,
636
+ mimeType: attachment.mimeType,
637
+ declaredSizeBytes: attachment.sizeBytes,
638
+ caption: attachment.caption,
639
+ materialized: {
640
+ path: filePath,
641
+ sizeBytes,
642
+ },
643
+ });
644
+ totalSizeBytes += sizeBytes;
645
+ }
646
+ if (pending.length > 0) {
647
+ this.pendingAttachmentsBySession.set(session.id, pending);
648
+ }
649
+ }
650
+ async buildMessageTextWithAttachments(sessionId, text, chatId) {
651
+ const attachments = await this.materializePendingAttachments(sessionId, chatId);
652
+ if (attachments.length === 0) {
653
+ return text;
654
+ }
655
+ return [this.formatAttachmentBlock(attachments), text].join("\n\n");
656
+ }
657
+ formatAttachmentBlock(attachments) {
658
+ const lines = ["attachments:"];
659
+ for (const attachment of attachments) {
660
+ lines.push(`- path: ${attachment.path}`);
661
+ lines.push(` mime: ${attachment.mimeType}`);
662
+ lines.push(` size_bytes: ${attachment.sizeBytes}`);
663
+ if (attachment.caption) {
664
+ lines.push(` caption: ${JSON.stringify(attachment.caption)}`);
665
+ }
666
+ }
667
+ return lines.join("\n");
668
+ }
669
+ async materializePendingAttachments(sessionId, chatId) {
670
+ const pending = this.pendingAttachmentsBySession.get(sessionId);
671
+ if (!pending || pending.length === 0) {
672
+ return [];
673
+ }
674
+ let totalSizeBytes = 0;
675
+ const nextPending = [];
676
+ const readyAttachments = [];
677
+ for (const attachment of pending) {
678
+ const attachmentLabel = describeAttachment(attachment.fileName, attachment.mimeType);
679
+ const materialized = attachment.materialized;
680
+ if (!materialized) {
681
+ await this.reply(chatId, `skipped attachment ${attachmentLabel}: local temp file is missing`);
682
+ continue;
683
+ }
684
+ if (materialized.sizeBytes > MAX_TELEGRAM_ATTACHMENT_FILE_BYTES) {
685
+ await this.reply(chatId, `skipped attachment ${attachmentLabel}: exceeds per-file limit (${describeAttachmentLimitBytes(MAX_TELEGRAM_ATTACHMENT_FILE_BYTES)})`);
686
+ continue;
687
+ }
688
+ if (totalSizeBytes + materialized.sizeBytes > MAX_TELEGRAM_ATTACHMENT_TOTAL_BYTES) {
689
+ await this.reply(chatId, `skipped attachment ${attachmentLabel}: exceeds per-turn total limit (${describeAttachmentLimitBytes(MAX_TELEGRAM_ATTACHMENT_TOTAL_BYTES)})`);
690
+ continue;
691
+ }
692
+ totalSizeBytes += materialized.sizeBytes;
693
+ nextPending.push(attachment);
694
+ readyAttachments.push({
695
+ path: materialized.path,
696
+ mimeType: attachment.mimeType,
697
+ sizeBytes: materialized.sizeBytes,
698
+ caption: attachment.caption,
699
+ });
700
+ }
701
+ if (nextPending.length === 0) {
702
+ this.pendingAttachmentsBySession.delete(sessionId);
703
+ }
704
+ else {
705
+ this.pendingAttachmentsBySession.set(sessionId, nextPending);
706
+ }
707
+ return readyAttachments;
708
+ }
709
+ async getOrCreatePendingAttachmentTempDir(sessionId) {
710
+ const existingPath = this.pendingAttachmentTempDirBySession.get(sessionId);
711
+ if (existingPath) {
712
+ return existingPath;
713
+ }
714
+ const directoryPath = await mkdtemp(join(tmpdir(), TELEGRAM_ATTACHMENT_TEMP_DIR_PREFIX));
715
+ this.pendingAttachmentTempDirBySession.set(sessionId, directoryPath);
716
+ const directories = this.attachmentTempDirsBySession.get(sessionId) ?? new Set();
717
+ directories.add(directoryPath);
718
+ this.attachmentTempDirsBySession.set(sessionId, directories);
719
+ return directoryPath;
720
+ }
721
+ resetPendingAttachmentQueue(sessionId) {
722
+ this.pendingAttachmentsBySession.delete(sessionId);
723
+ this.pendingAttachmentTempDirBySession.delete(sessionId);
724
+ }
725
+ clearSessionAttachments(sessionId) {
726
+ this.resetPendingAttachmentQueue(sessionId);
727
+ void this.cleanupSessionAttachmentTempDirs(sessionId);
728
+ }
729
+ async cleanupSessionAttachmentTempDirs(sessionId) {
730
+ const directories = this.attachmentTempDirsBySession.get(sessionId);
731
+ if (!directories || directories.size === 0) {
732
+ return;
733
+ }
734
+ this.attachmentTempDirsBySession.delete(sessionId);
735
+ for (const directoryPath of directories) {
736
+ try {
737
+ await rm(directoryPath, { recursive: true, force: true });
738
+ }
739
+ catch (error) {
740
+ this.log("warn", "failed to clean up telegram attachment temp directory", {
741
+ sessionId,
742
+ directoryPath,
743
+ cause: this.formatManagerError(error),
744
+ });
745
+ }
746
+ }
747
+ }
338
748
  isChatAllowed(chatId) {
339
749
  if (!this.allowedChatIds) {
340
750
  return true;
@@ -382,10 +792,6 @@ class AsyncTelegramAdapterImpl {
382
792
  await this.handleInterrupt(chatId);
383
793
  return;
384
794
  }
385
- if (command === "/cancel") {
386
- await this.handleCancel(chatId);
387
- return;
388
- }
389
795
  if (command === "/close") {
390
796
  await this.handleClose(chatId, args);
391
797
  return;
@@ -429,8 +835,8 @@ class AsyncTelegramAdapterImpl {
429
835
  await this.handleInterrupt(chatId);
430
836
  return true;
431
837
  }
432
- if (action === "cancel") {
433
- await this.handleCancel(chatId);
838
+ if (action === "close") {
839
+ await this.handleClose(chatId, []);
434
840
  return true;
435
841
  }
436
842
  if (action === "quiet") {
@@ -453,7 +859,6 @@ class AsyncTelegramAdapterImpl {
453
859
  "/use <sessionId|prefix|index>",
454
860
  "/status",
455
861
  "/interrupt",
456
- "/cancel",
457
862
  "/close [<sessionId>|all]",
458
863
  "/verbose",
459
864
  "/quiet",
@@ -651,7 +1056,7 @@ class AsyncTelegramAdapterImpl {
651
1056
  ],
652
1057
  [
653
1058
  { text: "/interrupt", callback_data: `${CALLBACK_ACTION_PREFIX}interrupt` },
654
- { text: "/cancel", callback_data: `${CALLBACK_ACTION_PREFIX}cancel` },
1059
+ { text: "/close", callback_data: `${CALLBACK_ACTION_PREFIX}close` },
655
1060
  ],
656
1061
  [
657
1062
  { text: "/quiet", callback_data: `${CALLBACK_ACTION_PREFIX}quiet` },
@@ -692,20 +1097,6 @@ class AsyncTelegramAdapterImpl {
692
1097
  await this.reply(chatId, this.formatManagerError(error));
693
1098
  }
694
1099
  }
695
- async handleCancel(chatId) {
696
- const session = this.getActiveSession(chatId);
697
- if (!session) {
698
- await this.reply(chatId, "no active session. use /new or /sessions");
699
- return;
700
- }
701
- try {
702
- const canceled = await this.sessionManager.cancelSession(session.id);
703
- await this.reply(chatId, formatSessionHeadline(canceled.id, "canceled"));
704
- }
705
- catch (error) {
706
- await this.reply(chatId, this.formatManagerError(error));
707
- }
708
- }
709
1100
  async handleClose(chatId, args) {
710
1101
  if (args.length > 1) {
711
1102
  await this.reply(chatId, "usage: /close [<sessionId>|all]");
@@ -760,7 +1151,9 @@ class AsyncTelegramAdapterImpl {
760
1151
  return;
761
1152
  }
762
1153
  try {
763
- await this.sessionManager.sendMessage(session.id, text, this.systemMessage ? { additionalSystemMessage: this.systemMessage } : undefined);
1154
+ const textWithAttachments = await this.buildMessageTextWithAttachments(session.id, text, chatId);
1155
+ await this.sessionManager.sendMessage(session.id, textWithAttachments, this.systemMessage ? { additionalSystemMessage: this.systemMessage } : undefined);
1156
+ this.resetPendingAttachmentQueue(session.id);
764
1157
  await this.reactToQueuedMessage(chatId, sourceMessageId);
765
1158
  if (this.isVerboseSession(session.id)) {
766
1159
  await this.reply(chatId, this.formatMessageQueued(session.id));
@@ -771,7 +1164,7 @@ class AsyncTelegramAdapterImpl {
771
1164
  }
772
1165
  }
773
1166
  async handleAudioMessage(chatId, message, sourceMessageId) {
774
- const session = this.getActiveSession(chatId);
1167
+ const session = this.getActiveOrSingleSession(chatId);
775
1168
  if (!session) {
776
1169
  await this.reply(chatId, "no active session. use /new or /sessions");
777
1170
  return;
@@ -800,7 +1193,9 @@ class AsyncTelegramAdapterImpl {
800
1193
  return;
801
1194
  }
802
1195
  try {
803
- await this.sessionManager.sendMessage(session.id, transcript, this.systemMessage ? { additionalSystemMessage: this.systemMessage } : undefined);
1196
+ const textWithAttachments = await this.buildMessageTextWithAttachments(session.id, transcript, chatId);
1197
+ await this.sessionManager.sendMessage(session.id, textWithAttachments, this.systemMessage ? { additionalSystemMessage: this.systemMessage } : undefined);
1198
+ this.resetPendingAttachmentQueue(session.id);
804
1199
  await this.reactToQueuedMessage(chatId, sourceMessageId);
805
1200
  if (this.isVerboseSession(session.id)) {
806
1201
  await this.reply(chatId, this.formatMessageQueued(session.id));
@@ -818,6 +1213,7 @@ class AsyncTelegramAdapterImpl {
818
1213
  const session = this.sessionManager.getSession(sessionId);
819
1214
  if (!session) {
820
1215
  this.clearActiveSession(chatId);
1216
+ this.clearSessionAttachments(sessionId);
821
1217
  return undefined;
822
1218
  }
823
1219
  return session;
@@ -889,6 +1285,7 @@ class AsyncTelegramAdapterImpl {
889
1285
  this.lastCommandBySession.delete(sessionId);
890
1286
  this.lastAssistantMessageBySession.delete(sessionId);
891
1287
  this.latestAssistantMessageByRun.delete(sessionId);
1288
+ this.clearSessionAttachments(sessionId);
892
1289
  }
893
1290
  getSessionVerbosity(sessionId) {
894
1291
  return this.sessionVerbosityBySession.get(sessionId) ?? "quiet";
@@ -916,11 +1313,6 @@ class AsyncTelegramAdapterImpl {
916
1313
  this.notifyLifecycle(event.sessionId, event.projectId, "failed");
917
1314
  return;
918
1315
  }
919
- if (event.state === "canceled") {
920
- this.latestAssistantMessageByRun.delete(event.sessionId);
921
- this.notifyLifecycle(event.sessionId, event.projectId, "canceled");
922
- return;
923
- }
924
1316
  if (event.state === "waiting-input" && event.previousState === "preparing-workspace") {
925
1317
  this.notifySession(event.sessionId, this.formatSessionReady(event.sessionId, event.projectId));
926
1318
  return;
@@ -976,7 +1368,6 @@ class AsyncTelegramAdapterImpl {
976
1368
  started: "run started",
977
1369
  finished: "run finished",
978
1370
  failed: "run failed",
979
- canceled: "run canceled",
980
1371
  }[state];
981
1372
  this.notifySession(sessionId, [formatSessionHeadline(sessionId, stateLabel), `project: ${projectId}`].join("\n"));
982
1373
  }
@@ -1074,6 +1465,7 @@ class AsyncTelegramAdapterImpl {
1074
1465
  }
1075
1466
  }
1076
1467
  export async function startAsyncTelegramAdapter(options) {
1468
+ await sweepStaleTelegramAttachmentTempDirs();
1077
1469
  const adapter = new AsyncTelegramAdapterImpl(options);
1078
1470
  return {
1079
1471
  close: () => adapter.close(),