@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.
- package/README.md +15 -14
- package/dist/core/async/cli.js +25 -34
- package/dist/core/async/cli.js.map +1 -1
- package/dist/core/async/http_server.js +0 -18
- package/dist/core/async/http_server.js.map +1 -1
- package/dist/core/async/index.js +1 -1
- package/dist/core/async/index.js.map +1 -1
- package/dist/core/async/session_manager.js +46 -27
- package/dist/core/async/session_manager.js.map +1 -1
- package/dist/core/async/telegram.js +425 -33
- package/dist/core/async/telegram.js.map +1 -1
- package/dist/core/async/workspace.js +60 -2
- package/dist/core/async/workspace.js.map +1 -1
- package/dist/core/version.js +1 -1
- package/package.json +1 -1
|
@@ -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 (
|
|
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 === "
|
|
433
|
-
await this.
|
|
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: "/
|
|
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.
|
|
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.
|
|
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.
|
|
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(),
|