@grinev/opencode-telegram-bot 0.6.0 → 0.6.1-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -5
- package/dist/bot/index.js +49 -18
- package/dist/summary/aggregator.js +91 -88
- package/dist/summary/tool-message-batcher.js +97 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,14 +5,16 @@
|
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
[](https://nodejs.org)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
OpenCode Telegram Bot is a secure Telegram client for [OpenCode](https://opencode.ai) CLI that runs on your local machine.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Run AI coding tasks, monitor progress, switch models, and manage sessions from your phone.
|
|
11
|
+
|
|
12
|
+
No open ports, no exposed APIs. The bot communicates with your local OpenCode server and the Telegram Bot API only.
|
|
13
|
+
|
|
14
|
+
Quick start: `npx @grinev/opencode-telegram-bot`
|
|
11
15
|
|
|
12
16
|
<p align="center">
|
|
13
|
-
<img src="assets/
|
|
14
|
-
<img src="assets/Screenshot-2.png" width="32%" alt="Live session status with context usage and changed files" />
|
|
15
|
-
<img src="assets/Screenshot-3.png" width="32%" alt="Switching between AI models from favorites" />
|
|
17
|
+
<img src="assets/screencast.gif" width="45%" alt="OpenCode Telegram Bot screencast" />
|
|
16
18
|
</p>
|
|
17
19
|
|
|
18
20
|
## Features
|
package/dist/bot/index.js
CHANGED
|
@@ -53,9 +53,23 @@ import { t } from "../i18n/index.js";
|
|
|
53
53
|
let botInstance = null;
|
|
54
54
|
let chatIdInstance = null;
|
|
55
55
|
let commandsInitialized = false;
|
|
56
|
+
const TELEGRAM_DOCUMENT_CAPTION_MAX_LENGTH = 1024;
|
|
57
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
58
|
+
const __dirname = path.dirname(__filename);
|
|
59
|
+
const TEMP_DIR = path.join(__dirname, "..", ".tmp");
|
|
60
|
+
function prepareDocumentCaption(caption) {
|
|
61
|
+
const normalizedCaption = caption.trim();
|
|
62
|
+
if (!normalizedCaption) {
|
|
63
|
+
return "";
|
|
64
|
+
}
|
|
65
|
+
if (normalizedCaption.length <= TELEGRAM_DOCUMENT_CAPTION_MAX_LENGTH) {
|
|
66
|
+
return normalizedCaption;
|
|
67
|
+
}
|
|
68
|
+
return `${normalizedCaption.slice(0, TELEGRAM_DOCUMENT_CAPTION_MAX_LENGTH - 3)}...`;
|
|
69
|
+
}
|
|
56
70
|
const toolMessageBatcher = new ToolMessageBatcher({
|
|
57
71
|
intervalSeconds: 5,
|
|
58
|
-
|
|
72
|
+
sendText: async (sessionId, text) => {
|
|
59
73
|
if (!botInstance || !chatIdInstance) {
|
|
60
74
|
return;
|
|
61
75
|
}
|
|
@@ -67,6 +81,28 @@ const toolMessageBatcher = new ToolMessageBatcher({
|
|
|
67
81
|
disable_notification: true,
|
|
68
82
|
});
|
|
69
83
|
},
|
|
84
|
+
sendFile: async (sessionId, fileData) => {
|
|
85
|
+
if (!botInstance || !chatIdInstance) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const currentSession = getCurrentSession();
|
|
89
|
+
if (!currentSession || currentSession.id !== sessionId) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const tempFilePath = path.join(TEMP_DIR, fileData.filename);
|
|
93
|
+
try {
|
|
94
|
+
logger.debug(`[Bot] Sending code file: ${fileData.filename} (${fileData.buffer.length} bytes, session=${sessionId})`);
|
|
95
|
+
await fs.mkdir(TEMP_DIR, { recursive: true });
|
|
96
|
+
await fs.writeFile(tempFilePath, fileData.buffer);
|
|
97
|
+
await botInstance.api.sendDocument(chatIdInstance, new InputFile(tempFilePath), {
|
|
98
|
+
caption: fileData.caption,
|
|
99
|
+
disable_notification: true,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
await fs.unlink(tempFilePath).catch(() => { });
|
|
104
|
+
}
|
|
105
|
+
},
|
|
70
106
|
});
|
|
71
107
|
async function ensureCommandsInitialized(ctx, next) {
|
|
72
108
|
if (commandsInitialized || !ctx.from || ctx.from.id !== config.telegram.allowedUserId) {
|
|
@@ -142,9 +178,6 @@ async function ensureEventSubscription(directory) {
|
|
|
142
178
|
}
|
|
143
179
|
});
|
|
144
180
|
summaryAggregator.setOnTool(async (toolInfo) => {
|
|
145
|
-
if (config.bot.hideToolCallMessages) {
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
181
|
if (!botInstance || !chatIdInstance) {
|
|
149
182
|
logger.error("Bot or chat ID not available for sending tool notification");
|
|
150
183
|
return;
|
|
@@ -153,6 +186,11 @@ async function ensureEventSubscription(directory) {
|
|
|
153
186
|
if (!currentSession || currentSession.id !== toolInfo.sessionId) {
|
|
154
187
|
return;
|
|
155
188
|
}
|
|
189
|
+
const shouldIncludeToolInfoInFileCaption = toolInfo.hasFileAttachment &&
|
|
190
|
+
(toolInfo.tool === "write" || toolInfo.tool === "edit" || toolInfo.tool === "apply_patch");
|
|
191
|
+
if (config.bot.hideToolCallMessages || shouldIncludeToolInfoInFileCaption) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
156
194
|
try {
|
|
157
195
|
const message = formatToolInfo(toolInfo);
|
|
158
196
|
if (message) {
|
|
@@ -163,29 +201,22 @@ async function ensureEventSubscription(directory) {
|
|
|
163
201
|
logger.error("Failed to send tool notification to Telegram:", err);
|
|
164
202
|
}
|
|
165
203
|
});
|
|
166
|
-
summaryAggregator.setOnToolFile(async (
|
|
204
|
+
summaryAggregator.setOnToolFile(async (fileInfo) => {
|
|
167
205
|
if (!botInstance || !chatIdInstance) {
|
|
168
206
|
logger.error("Bot or chat ID not available for sending file");
|
|
169
207
|
return;
|
|
170
208
|
}
|
|
171
209
|
const currentSession = getCurrentSession();
|
|
172
|
-
if (!currentSession) {
|
|
210
|
+
if (!currentSession || currentSession.id !== fileInfo.sessionId) {
|
|
173
211
|
return;
|
|
174
212
|
}
|
|
175
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
176
|
-
const __dirname = path.dirname(__filename);
|
|
177
|
-
const tempDir = path.join(__dirname, "..", ".tmp");
|
|
178
213
|
try {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
caption: fileData.caption,
|
|
185
|
-
disable_notification: true,
|
|
214
|
+
const toolMessage = formatToolInfo(fileInfo);
|
|
215
|
+
const caption = prepareDocumentCaption(toolMessage || fileInfo.fileData.caption);
|
|
216
|
+
toolMessageBatcher.enqueueFile(fileInfo.sessionId, {
|
|
217
|
+
...fileInfo.fileData,
|
|
218
|
+
caption,
|
|
186
219
|
});
|
|
187
|
-
await fs.unlink(tempFilePath);
|
|
188
|
-
logger.debug(`[Bot] Temporary file deleted: ${fileData.filename}`);
|
|
189
220
|
}
|
|
190
221
|
catch (err) {
|
|
191
222
|
logger.error("Failed to send file to Telegram:", err);
|
|
@@ -298,9 +298,10 @@ class SummaryAggregator {
|
|
|
298
298
|
}
|
|
299
299
|
if ("status" in state && state.status === "completed") {
|
|
300
300
|
logger.debug(`[Aggregator] Tool completed: callID=${part.callID}, tool=${part.tool}`, JSON.stringify(state, null, 2));
|
|
301
|
-
const
|
|
302
|
-
if (!this.processedToolStates.has(
|
|
303
|
-
this.processedToolStates.add(
|
|
301
|
+
const completedKey = `completed-${part.callID}`;
|
|
302
|
+
if (!this.processedToolStates.has(completedKey)) {
|
|
303
|
+
this.processedToolStates.add(completedKey);
|
|
304
|
+
const preparedFileContext = this.prepareToolFileContext(part.tool, input, title, state.metadata);
|
|
304
305
|
const toolData = {
|
|
305
306
|
sessionId: part.sessionID,
|
|
306
307
|
messageId: messageID,
|
|
@@ -310,103 +311,105 @@ class SummaryAggregator {
|
|
|
310
311
|
input,
|
|
311
312
|
title,
|
|
312
313
|
metadata: state.metadata,
|
|
314
|
+
hasFileAttachment: !!preparedFileContext.fileData,
|
|
313
315
|
};
|
|
314
316
|
logger.debug(`[Aggregator] Sending tool notification to Telegram: tool=${part.tool}, title=${title || "N/A"}`);
|
|
315
317
|
if (this.onToolCallback) {
|
|
316
318
|
this.onToolCallback(toolData);
|
|
317
319
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const filePath = normalizePathForDisplay(input.filePath);
|
|
326
|
-
const fileData = prepareCodeFile(input.content, filePath, "write");
|
|
327
|
-
if (fileData && this.onToolFileCallback) {
|
|
328
|
-
logger.debug(`[Aggregator] Sending write file: ${fileData.filename} (${fileData.buffer.length} bytes)`);
|
|
329
|
-
this.onToolFileCallback(fileData);
|
|
330
|
-
}
|
|
331
|
-
// Notify about file change for pinned message
|
|
332
|
-
if (this.onFileChangeCallback) {
|
|
333
|
-
const lines = input.content.split("\n").length;
|
|
334
|
-
this.onFileChangeCallback({
|
|
335
|
-
file: filePath,
|
|
336
|
-
additions: lines,
|
|
337
|
-
deletions: 0,
|
|
338
|
-
});
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
else if (part.tool === "edit" &&
|
|
342
|
-
state.metadata &&
|
|
343
|
-
"diff" in state.metadata &&
|
|
344
|
-
"filediff" in state.metadata) {
|
|
345
|
-
const filediff = state.metadata.filediff;
|
|
346
|
-
const filePath = filediff.file ? normalizePathForDisplay(filediff.file) : undefined;
|
|
347
|
-
const diff = state.metadata.diff;
|
|
348
|
-
if (filePath && diff) {
|
|
349
|
-
const fileData = prepareCodeFile(diff, filePath, "edit");
|
|
350
|
-
if (fileData && this.onToolFileCallback) {
|
|
351
|
-
logger.debug(`[Aggregator] Sending ${part.tool} file: ${fileData.filename} (${fileData.buffer.length} bytes)`);
|
|
352
|
-
this.onToolFileCallback(fileData);
|
|
353
|
-
}
|
|
354
|
-
// Notify about file change for pinned message
|
|
355
|
-
if (this.onFileChangeCallback) {
|
|
356
|
-
this.onFileChangeCallback({
|
|
357
|
-
file: filePath,
|
|
358
|
-
additions: filediff.additions || 0,
|
|
359
|
-
deletions: filediff.deletions || 0,
|
|
360
|
-
});
|
|
361
|
-
}
|
|
362
|
-
}
|
|
320
|
+
if (preparedFileContext.fileData && this.onToolFileCallback) {
|
|
321
|
+
logger.debug(`[Aggregator] Sending ${part.tool} file: ${preparedFileContext.fileData.filename} (${preparedFileContext.fileData.buffer.length} bytes)`);
|
|
322
|
+
this.onToolFileCallback({
|
|
323
|
+
...toolData,
|
|
324
|
+
hasFileAttachment: true,
|
|
325
|
+
fileData: preparedFileContext.fileData,
|
|
326
|
+
});
|
|
363
327
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
const filePathFromInput = input && typeof input.filePath === "string"
|
|
367
|
-
? normalizePathForDisplay(input.filePath)
|
|
368
|
-
: input && typeof input.path === "string"
|
|
369
|
-
? normalizePathForDisplay(input.path)
|
|
370
|
-
: "";
|
|
371
|
-
const filePathFromTitle = title ? extractFirstUpdatedFileFromTitle(title) : "";
|
|
372
|
-
const filePath = (metadata?.filediff?.file && normalizePathForDisplay(metadata.filediff.file)) ||
|
|
373
|
-
filePathFromInput ||
|
|
374
|
-
normalizePathForDisplay(filePathFromTitle);
|
|
375
|
-
const diffText = typeof metadata?.diff === "string"
|
|
376
|
-
? metadata.diff
|
|
377
|
-
: input && typeof input.patchText === "string"
|
|
378
|
-
? input.patchText
|
|
379
|
-
: "";
|
|
380
|
-
if (filePath && diffText) {
|
|
381
|
-
const fileData = prepareCodeFile(diffText, filePath, "edit");
|
|
382
|
-
if (fileData && this.onToolFileCallback) {
|
|
383
|
-
logger.debug(`[Aggregator] Sending apply_patch file: ${fileData.filename} (${fileData.buffer.length} bytes)`);
|
|
384
|
-
this.onToolFileCallback(fileData);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
if (filePath && this.onFileChangeCallback) {
|
|
388
|
-
if (metadata?.filediff) {
|
|
389
|
-
this.onFileChangeCallback({
|
|
390
|
-
file: filePath,
|
|
391
|
-
additions: metadata.filediff.additions || 0,
|
|
392
|
-
deletions: metadata.filediff.deletions || 0,
|
|
393
|
-
});
|
|
394
|
-
}
|
|
395
|
-
else if (diffText) {
|
|
396
|
-
const changes = countDiffChangesFromText(diffText);
|
|
397
|
-
this.onFileChangeCallback({
|
|
398
|
-
file: filePath,
|
|
399
|
-
additions: changes.additions,
|
|
400
|
-
deletions: changes.deletions,
|
|
401
|
-
});
|
|
402
|
-
}
|
|
403
|
-
}
|
|
328
|
+
if (preparedFileContext.fileChange && this.onFileChangeCallback) {
|
|
329
|
+
this.onFileChangeCallback(preparedFileContext.fileChange);
|
|
404
330
|
}
|
|
405
331
|
}
|
|
406
332
|
}
|
|
407
333
|
}
|
|
408
334
|
this.lastUpdated = Date.now();
|
|
409
335
|
}
|
|
336
|
+
prepareToolFileContext(tool, input, title, metadata) {
|
|
337
|
+
if (tool === "write" && input) {
|
|
338
|
+
const filePath = typeof input.filePath === "string" ? normalizePathForDisplay(input.filePath) : "";
|
|
339
|
+
const hasContent = typeof input.content === "string";
|
|
340
|
+
const content = hasContent ? input.content : "";
|
|
341
|
+
if (!filePath || !hasContent) {
|
|
342
|
+
return { fileData: null, fileChange: null };
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
fileData: prepareCodeFile(content, filePath, "write"),
|
|
346
|
+
fileChange: {
|
|
347
|
+
file: filePath,
|
|
348
|
+
additions: content.split("\n").length,
|
|
349
|
+
deletions: 0,
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
if (tool === "edit" && metadata) {
|
|
354
|
+
const editMetadata = metadata;
|
|
355
|
+
const filePath = editMetadata.filediff?.file
|
|
356
|
+
? normalizePathForDisplay(editMetadata.filediff.file)
|
|
357
|
+
: "";
|
|
358
|
+
const diffText = typeof editMetadata.diff === "string" ? editMetadata.diff : "";
|
|
359
|
+
if (!filePath || !diffText) {
|
|
360
|
+
return { fileData: null, fileChange: null };
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
fileData: prepareCodeFile(diffText, filePath, "edit"),
|
|
364
|
+
fileChange: {
|
|
365
|
+
file: filePath,
|
|
366
|
+
additions: editMetadata.filediff?.additions || 0,
|
|
367
|
+
deletions: editMetadata.filediff?.deletions || 0,
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
if (tool === "apply_patch") {
|
|
372
|
+
const patchMetadata = metadata;
|
|
373
|
+
const filePathFromInput = input && typeof input.filePath === "string"
|
|
374
|
+
? normalizePathForDisplay(input.filePath)
|
|
375
|
+
: input && typeof input.path === "string"
|
|
376
|
+
? normalizePathForDisplay(input.path)
|
|
377
|
+
: "";
|
|
378
|
+
const filePathFromTitle = title ? extractFirstUpdatedFileFromTitle(title) : "";
|
|
379
|
+
const filePath = (patchMetadata?.filediff?.file && normalizePathForDisplay(patchMetadata.filediff.file)) ||
|
|
380
|
+
filePathFromInput ||
|
|
381
|
+
normalizePathForDisplay(filePathFromTitle);
|
|
382
|
+
const diffText = typeof patchMetadata?.diff === "string"
|
|
383
|
+
? patchMetadata.diff
|
|
384
|
+
: input && typeof input.patchText === "string"
|
|
385
|
+
? input.patchText
|
|
386
|
+
: "";
|
|
387
|
+
if (!filePath) {
|
|
388
|
+
return { fileData: null, fileChange: null };
|
|
389
|
+
}
|
|
390
|
+
const fileChange = patchMetadata?.filediff
|
|
391
|
+
? {
|
|
392
|
+
file: filePath,
|
|
393
|
+
additions: patchMetadata.filediff.additions || 0,
|
|
394
|
+
deletions: patchMetadata.filediff.deletions || 0,
|
|
395
|
+
}
|
|
396
|
+
: diffText
|
|
397
|
+
? (() => {
|
|
398
|
+
const changes = countDiffChangesFromText(diffText);
|
|
399
|
+
return {
|
|
400
|
+
file: filePath,
|
|
401
|
+
additions: changes.additions,
|
|
402
|
+
deletions: changes.deletions,
|
|
403
|
+
};
|
|
404
|
+
})()
|
|
405
|
+
: null;
|
|
406
|
+
return {
|
|
407
|
+
fileData: diffText ? prepareCodeFile(diffText, filePath, "edit") : null,
|
|
408
|
+
fileChange,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
return { fileData: null, fileChange: null };
|
|
412
|
+
}
|
|
410
413
|
hashString(str) {
|
|
411
414
|
let hash = 0;
|
|
412
415
|
for (let i = 0; i < str.length; i++) {
|
|
@@ -13,13 +13,16 @@ function normalizeIntervalSeconds(value) {
|
|
|
13
13
|
}
|
|
14
14
|
export class ToolMessageBatcher {
|
|
15
15
|
intervalSeconds;
|
|
16
|
-
|
|
16
|
+
sendText;
|
|
17
|
+
sendFile;
|
|
17
18
|
queues = new Map();
|
|
18
19
|
timers = new Map();
|
|
20
|
+
sessionTasks = new Map();
|
|
19
21
|
generation = 0;
|
|
20
22
|
constructor(options) {
|
|
21
23
|
this.intervalSeconds = normalizeIntervalSeconds(options.intervalSeconds);
|
|
22
|
-
this.
|
|
24
|
+
this.sendText = options.sendText;
|
|
25
|
+
this.sendFile = options.sendFile;
|
|
23
26
|
}
|
|
24
27
|
setIntervalSeconds(nextIntervalSeconds) {
|
|
25
28
|
const normalized = normalizeIntervalSeconds(nextIntervalSeconds);
|
|
@@ -47,29 +50,34 @@ export class ToolMessageBatcher {
|
|
|
47
50
|
}
|
|
48
51
|
if (this.intervalSeconds === 0) {
|
|
49
52
|
const expectedGeneration = this.generation;
|
|
50
|
-
logger.debug(`[ToolBatcher] Sending immediate message: session=${sessionId}`);
|
|
51
|
-
void this.
|
|
53
|
+
logger.debug(`[ToolBatcher] Sending immediate text message: session=${sessionId}`);
|
|
54
|
+
void this.enqueueTask(sessionId, () => this.sendTextSafe(sessionId, normalizedMessage, "immediate", expectedGeneration));
|
|
52
55
|
return;
|
|
53
56
|
}
|
|
54
57
|
const queue = this.queues.get(sessionId) ?? [];
|
|
55
|
-
queue.push(normalizedMessage);
|
|
58
|
+
queue.push({ kind: "text", text: normalizedMessage });
|
|
56
59
|
this.queues.set(sessionId, queue);
|
|
57
|
-
logger.debug(`[ToolBatcher] Queued message: session=${sessionId}, queueSize=${queue.length}, interval=${this.intervalSeconds}s`);
|
|
60
|
+
logger.debug(`[ToolBatcher] Queued text message: session=${sessionId}, queueSize=${queue.length}, interval=${this.intervalSeconds}s`);
|
|
58
61
|
this.ensureTimer(sessionId);
|
|
59
62
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
this.clearTimer(sessionId);
|
|
63
|
-
const queuedMessages = this.queues.get(sessionId);
|
|
64
|
-
if (!queuedMessages || queuedMessages.length === 0) {
|
|
63
|
+
enqueueFile(sessionId, fileData) {
|
|
64
|
+
if (!sessionId) {
|
|
65
65
|
return;
|
|
66
66
|
}
|
|
67
|
-
this.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
67
|
+
if (this.intervalSeconds === 0) {
|
|
68
|
+
const expectedGeneration = this.generation;
|
|
69
|
+
logger.debug(`[ToolBatcher] Sending immediate file message: session=${sessionId}`);
|
|
70
|
+
void this.enqueueTask(sessionId, () => this.sendFileSafe(sessionId, fileData, "immediate", expectedGeneration));
|
|
71
|
+
return;
|
|
72
72
|
}
|
|
73
|
+
const queue = this.queues.get(sessionId) ?? [];
|
|
74
|
+
queue.push({ kind: "file", fileData });
|
|
75
|
+
this.queues.set(sessionId, queue);
|
|
76
|
+
logger.debug(`[ToolBatcher] Queued file message: session=${sessionId}, queueSize=${queue.length}, interval=${this.intervalSeconds}s`);
|
|
77
|
+
this.ensureTimer(sessionId);
|
|
78
|
+
}
|
|
79
|
+
async flushSession(sessionId, reason) {
|
|
80
|
+
await this.enqueueTask(sessionId, () => this.flushSessionInternal(sessionId, reason));
|
|
73
81
|
}
|
|
74
82
|
async flushAll(reason) {
|
|
75
83
|
for (const sessionId of Array.from(this.timers.keys())) {
|
|
@@ -121,17 +129,86 @@ export class ToolMessageBatcher {
|
|
|
121
129
|
}, this.intervalSeconds * 1000);
|
|
122
130
|
this.timers.set(sessionId, timer);
|
|
123
131
|
}
|
|
124
|
-
|
|
132
|
+
enqueueTask(sessionId, task) {
|
|
133
|
+
const previousTask = this.sessionTasks.get(sessionId) ?? Promise.resolve();
|
|
134
|
+
const nextTask = previousTask
|
|
135
|
+
.catch(() => undefined)
|
|
136
|
+
.then(task)
|
|
137
|
+
.finally(() => {
|
|
138
|
+
if (this.sessionTasks.get(sessionId) === nextTask) {
|
|
139
|
+
this.sessionTasks.delete(sessionId);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
this.sessionTasks.set(sessionId, nextTask);
|
|
143
|
+
return nextTask;
|
|
144
|
+
}
|
|
145
|
+
async flushSessionInternal(sessionId, reason) {
|
|
146
|
+
const expectedGeneration = this.generation;
|
|
147
|
+
this.clearTimer(sessionId);
|
|
148
|
+
const queuedItems = this.queues.get(sessionId);
|
|
149
|
+
if (!queuedItems || queuedItems.length === 0) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
this.queues.delete(sessionId);
|
|
153
|
+
const flushItems = this.buildFlushItems(queuedItems);
|
|
154
|
+
logger.debug(`[ToolBatcher] Flushing ${queuedItems.length} queued items as ${flushItems.length} Telegram sends (session=${sessionId}, reason=${reason})`);
|
|
155
|
+
for (const item of flushItems) {
|
|
156
|
+
if (item.kind === "text") {
|
|
157
|
+
await this.sendTextSafe(sessionId, item.text, reason, expectedGeneration);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
await this.sendFileSafe(sessionId, item.fileData, reason, expectedGeneration);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async sendTextSafe(sessionId, text, reason, expectedGeneration) {
|
|
125
165
|
if (this.generation !== expectedGeneration) {
|
|
126
|
-
logger.debug(`[ToolBatcher] Dropping stale tool
|
|
166
|
+
logger.debug(`[ToolBatcher] Dropping stale tool text message: session=${sessionId}, reason=${reason}`);
|
|
127
167
|
return;
|
|
128
168
|
}
|
|
129
169
|
try {
|
|
130
|
-
await this.
|
|
170
|
+
await this.sendText(sessionId, text);
|
|
131
171
|
}
|
|
132
172
|
catch (err) {
|
|
133
|
-
logger.error(`[ToolBatcher] Failed to send tool
|
|
173
|
+
logger.error(`[ToolBatcher] Failed to send tool text message: session=${sessionId}, reason=${reason}`, err);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async sendFileSafe(sessionId, fileData, reason, expectedGeneration) {
|
|
177
|
+
if (this.generation !== expectedGeneration) {
|
|
178
|
+
logger.debug(`[ToolBatcher] Dropping stale tool file message: session=${sessionId}, reason=${reason}`);
|
|
179
|
+
return;
|
|
134
180
|
}
|
|
181
|
+
try {
|
|
182
|
+
await this.sendFile(sessionId, fileData);
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
logger.error(`[ToolBatcher] Failed to send tool file message: session=${sessionId}, reason=${reason}`, err);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
buildFlushItems(entries) {
|
|
189
|
+
const result = [];
|
|
190
|
+
const textBuffer = [];
|
|
191
|
+
const flushTextBuffer = () => {
|
|
192
|
+
if (textBuffer.length === 0) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const packedTextMessages = this.packMessages(textBuffer);
|
|
196
|
+
for (const text of packedTextMessages) {
|
|
197
|
+
result.push({ kind: "text", text });
|
|
198
|
+
}
|
|
199
|
+
textBuffer.length = 0;
|
|
200
|
+
};
|
|
201
|
+
for (const entry of entries) {
|
|
202
|
+
if (entry.kind === "text") {
|
|
203
|
+
textBuffer.push(entry.text);
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
flushTextBuffer();
|
|
207
|
+
result.push({ kind: "file", fileData: entry.fileData });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
flushTextBuffer();
|
|
211
|
+
return result;
|
|
135
212
|
}
|
|
136
213
|
packMessages(messages) {
|
|
137
214
|
const normalizedEntries = messages
|