@grinev/opencode-telegram-bot 0.13.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +0 -8
- package/README.md +25 -26
- package/dist/bot/commands/sessions.js +1 -0
- package/dist/bot/index.js +136 -29
- package/dist/bot/streaming/response-streamer.js +6 -4
- package/dist/bot/streaming/tool-call-streamer.js +285 -0
- package/dist/bot/utils/finalize-assistant-response.js +20 -11
- package/dist/bot/utils/send-with-markdown-fallback.js +13 -2
- package/dist/bot/utils/thinking-message.js +1 -5
- package/dist/config.js +0 -16
- package/dist/i18n/de.js +13 -1
- package/dist/i18n/en.js +13 -1
- package/dist/i18n/es.js +13 -1
- package/dist/i18n/fr.js +13 -1
- package/dist/i18n/ru.js +13 -1
- package/dist/i18n/zh.js +13 -1
- package/dist/model/context-limit.js +57 -0
- package/dist/pinned/format.js +29 -0
- package/dist/pinned/manager.js +30 -55
- package/dist/scheduled-task/runtime.js +27 -21
- package/dist/summary/aggregator.js +435 -16
- package/dist/summary/formatter.js +84 -12
- package/dist/summary/subagent-formatter.js +63 -0
- package/dist/summary/tool-message-batcher.js +14 -209
- package/package.json +1 -1
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { formatModelDisplayName } from "../pinned/format.js";
|
|
2
|
+
import { t } from "../i18n/index.js";
|
|
3
|
+
import { formatCompactToolInfo } from "./formatter.js";
|
|
4
|
+
function formatToolStep(subagent) {
|
|
5
|
+
if (!subagent.currentTool) {
|
|
6
|
+
return "";
|
|
7
|
+
}
|
|
8
|
+
const toolInfo = {
|
|
9
|
+
sessionId: subagent.sessionId ?? subagent.parentSessionId,
|
|
10
|
+
messageId: subagent.cardId,
|
|
11
|
+
callId: subagent.cardId,
|
|
12
|
+
tool: subagent.currentTool,
|
|
13
|
+
state: {
|
|
14
|
+
status: "running",
|
|
15
|
+
input: subagent.currentToolInput ?? {},
|
|
16
|
+
title: subagent.currentToolTitle,
|
|
17
|
+
metadata: {},
|
|
18
|
+
time: { start: subagent.updatedAt },
|
|
19
|
+
},
|
|
20
|
+
input: subagent.currentToolInput,
|
|
21
|
+
title: subagent.currentToolTitle,
|
|
22
|
+
metadata: {},
|
|
23
|
+
hasFileAttachment: false,
|
|
24
|
+
};
|
|
25
|
+
const formatted = formatCompactToolInfo(toolInfo, 128, "").trim();
|
|
26
|
+
const firstSpaceIndex = formatted.indexOf(" ");
|
|
27
|
+
if (firstSpaceIndex >= 0 && formatted.slice(firstSpaceIndex + 1) === subagent.currentTool) {
|
|
28
|
+
return "";
|
|
29
|
+
}
|
|
30
|
+
return formatted;
|
|
31
|
+
}
|
|
32
|
+
function formatSubagentActivity(subagent) {
|
|
33
|
+
if (subagent.status === "completed") {
|
|
34
|
+
return `✅ ${t("subagent.completed")}`;
|
|
35
|
+
}
|
|
36
|
+
if (subagent.status === "error") {
|
|
37
|
+
const message = subagent.terminalMessage?.trim() || t("subagent.failed");
|
|
38
|
+
return `❌ ${message}`;
|
|
39
|
+
}
|
|
40
|
+
const toolStep = formatToolStep(subagent);
|
|
41
|
+
if (toolStep) {
|
|
42
|
+
return toolStep;
|
|
43
|
+
}
|
|
44
|
+
return `⚙️ ${t("subagent.working")}`;
|
|
45
|
+
}
|
|
46
|
+
async function formatSubagentCard(subagent) {
|
|
47
|
+
const modelName = formatModelDisplayName(subagent.providerID, subagent.modelID);
|
|
48
|
+
const lines = [
|
|
49
|
+
`🧩 ${t("subagent.line.task", { task: subagent.description })}`,
|
|
50
|
+
t("subagent.line.agent", { agent: subagent.agent }),
|
|
51
|
+
t("pinned.line.model", { model: modelName }),
|
|
52
|
+
"",
|
|
53
|
+
formatSubagentActivity(subagent),
|
|
54
|
+
];
|
|
55
|
+
return lines.join("\n");
|
|
56
|
+
}
|
|
57
|
+
export async function renderSubagentCards(subagents) {
|
|
58
|
+
if (subagents.length === 0) {
|
|
59
|
+
return "";
|
|
60
|
+
}
|
|
61
|
+
const parts = await Promise.all(subagents.map((subagent) => formatSubagentCard(subagent)));
|
|
62
|
+
return parts.filter(Boolean).join("\n\n");
|
|
63
|
+
}
|
|
@@ -1,50 +1,15 @@
|
|
|
1
1
|
import { logger } from "../utils/logger.js";
|
|
2
|
-
const DEFAULT_INTERVAL_SECONDS = 5;
|
|
3
|
-
const TELEGRAM_MESSAGE_MAX_LENGTH = 4096;
|
|
4
|
-
function normalizeIntervalSeconds(value) {
|
|
5
|
-
if (!Number.isFinite(value)) {
|
|
6
|
-
return DEFAULT_INTERVAL_SECONDS;
|
|
7
|
-
}
|
|
8
|
-
const normalized = Math.floor(value);
|
|
9
|
-
if (normalized < 0) {
|
|
10
|
-
return DEFAULT_INTERVAL_SECONDS;
|
|
11
|
-
}
|
|
12
|
-
return normalized;
|
|
13
|
-
}
|
|
14
2
|
export class ToolMessageBatcher {
|
|
15
|
-
intervalSeconds;
|
|
16
3
|
sendText;
|
|
17
4
|
sendFile;
|
|
18
|
-
queues = new Map();
|
|
19
|
-
timers = new Map();
|
|
20
5
|
sessionTasks = new Map();
|
|
21
6
|
generation = 0;
|
|
22
7
|
constructor(options) {
|
|
23
|
-
this.intervalSeconds = normalizeIntervalSeconds(options.intervalSeconds);
|
|
24
8
|
this.sendText = options.sendText;
|
|
25
9
|
this.sendFile = options.sendFile;
|
|
26
10
|
}
|
|
27
|
-
setIntervalSeconds(nextIntervalSeconds) {
|
|
28
|
-
const normalized = normalizeIntervalSeconds(nextIntervalSeconds);
|
|
29
|
-
if (this.intervalSeconds === normalized) {
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
this.intervalSeconds = normalized;
|
|
33
|
-
logger.info(`[ToolBatcher] Interval updated: ${normalized}s`);
|
|
34
|
-
if (normalized === 0) {
|
|
35
|
-
void this.flushAll("interval_updated");
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
const sessionIds = Array.from(this.queues.keys());
|
|
39
|
-
for (const sessionId of sessionIds) {
|
|
40
|
-
this.restartTimer(sessionId);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
getIntervalSeconds() {
|
|
44
|
-
return this.intervalSeconds;
|
|
45
|
-
}
|
|
46
11
|
enqueue(sessionId, message) {
|
|
47
|
-
this.
|
|
12
|
+
this.sendTextNow(sessionId, message, "enqueue");
|
|
48
13
|
}
|
|
49
14
|
sendTextNow(sessionId, message, reason) {
|
|
50
15
|
const normalizedMessage = message.trim();
|
|
@@ -52,80 +17,38 @@ export class ToolMessageBatcher {
|
|
|
52
17
|
return;
|
|
53
18
|
}
|
|
54
19
|
const expectedGeneration = this.generation;
|
|
55
|
-
logger.debug(`[ToolBatcher] Sending
|
|
20
|
+
logger.debug(`[ToolBatcher] Sending text message: session=${sessionId}, reason=${reason}`);
|
|
56
21
|
void this.enqueueTask(sessionId, () => this.sendTextSafe(sessionId, normalizedMessage, reason, expectedGeneration));
|
|
57
22
|
}
|
|
58
23
|
enqueueUniqueByPrefix(sessionId, message, prefix) {
|
|
59
|
-
|
|
24
|
+
void prefix;
|
|
25
|
+
this.sendTextNow(sessionId, message, "enqueue_unique_by_prefix");
|
|
60
26
|
}
|
|
61
27
|
enqueueFile(sessionId, fileData) {
|
|
62
28
|
if (!sessionId) {
|
|
63
29
|
return;
|
|
64
30
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
void this.enqueueTask(sessionId, () => this.sendFileSafe(sessionId, fileData, "immediate", expectedGeneration));
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
const queue = this.queues.get(sessionId) ?? [];
|
|
72
|
-
queue.push({ kind: "file", fileData });
|
|
73
|
-
this.queues.set(sessionId, queue);
|
|
74
|
-
logger.debug(`[ToolBatcher] Queued file message: session=${sessionId}, queueSize=${queue.length}, interval=${this.intervalSeconds}s`);
|
|
75
|
-
this.ensureTimer(sessionId);
|
|
31
|
+
const expectedGeneration = this.generation;
|
|
32
|
+
logger.debug(`[ToolBatcher] Sending file message: session=${sessionId}`);
|
|
33
|
+
void this.enqueueTask(sessionId, () => this.sendFileSafe(sessionId, fileData, "enqueue_file", expectedGeneration));
|
|
76
34
|
}
|
|
77
35
|
async flushSession(sessionId, reason) {
|
|
78
|
-
|
|
36
|
+
void reason;
|
|
37
|
+
await (this.sessionTasks.get(sessionId) ?? Promise.resolve());
|
|
79
38
|
}
|
|
80
39
|
async flushAll(reason) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const sessionIds = Array.from(this.queues.keys());
|
|
85
|
-
for (const sessionId of sessionIds) {
|
|
86
|
-
await this.flushSession(sessionId, reason);
|
|
40
|
+
void reason;
|
|
41
|
+
for (const task of this.sessionTasks.values()) {
|
|
42
|
+
await task;
|
|
87
43
|
}
|
|
88
44
|
}
|
|
89
45
|
clearSession(sessionId, reason) {
|
|
90
46
|
this.generation++;
|
|
91
|
-
|
|
92
|
-
if (this.queues.delete(sessionId)) {
|
|
93
|
-
logger.debug(`[ToolBatcher] Cleared session queue: session=${sessionId}, reason=${reason}`);
|
|
94
|
-
}
|
|
47
|
+
logger.debug(`[ToolBatcher] Cleared session sends: session=${sessionId}, reason=${reason}`);
|
|
95
48
|
}
|
|
96
49
|
clearAll(reason) {
|
|
97
50
|
this.generation++;
|
|
98
|
-
|
|
99
|
-
clearTimeout(timer);
|
|
100
|
-
}
|
|
101
|
-
const queuedSessions = this.queues.size;
|
|
102
|
-
this.timers.clear();
|
|
103
|
-
this.queues.clear();
|
|
104
|
-
if (queuedSessions > 0) {
|
|
105
|
-
logger.debug(`[ToolBatcher] Cleared all queued tool messages: sessions=${queuedSessions}, reason=${reason}`);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
clearTimer(sessionId) {
|
|
109
|
-
const timer = this.timers.get(sessionId);
|
|
110
|
-
if (!timer) {
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
clearTimeout(timer);
|
|
114
|
-
this.timers.delete(sessionId);
|
|
115
|
-
}
|
|
116
|
-
ensureTimer(sessionId) {
|
|
117
|
-
if (this.timers.has(sessionId)) {
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
this.restartTimer(sessionId);
|
|
121
|
-
}
|
|
122
|
-
restartTimer(sessionId) {
|
|
123
|
-
this.clearTimer(sessionId);
|
|
124
|
-
const timer = setTimeout(() => {
|
|
125
|
-
this.timers.delete(sessionId);
|
|
126
|
-
void this.flushSession(sessionId, "interval_elapsed");
|
|
127
|
-
}, this.intervalSeconds * 1000);
|
|
128
|
-
this.timers.set(sessionId, timer);
|
|
51
|
+
logger.debug(`[ToolBatcher] Cleared all pending tool sends: reason=${reason}`);
|
|
129
52
|
}
|
|
130
53
|
enqueueTask(sessionId, task) {
|
|
131
54
|
const previousTask = this.sessionTasks.get(sessionId) ?? Promise.resolve();
|
|
@@ -140,53 +63,6 @@ export class ToolMessageBatcher {
|
|
|
140
63
|
this.sessionTasks.set(sessionId, nextTask);
|
|
141
64
|
return nextTask;
|
|
142
65
|
}
|
|
143
|
-
enqueueTextInternal(sessionId, message, uniquePrefix) {
|
|
144
|
-
const normalizedMessage = message.trim();
|
|
145
|
-
if (!sessionId || normalizedMessage.length === 0) {
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
if (this.intervalSeconds === 0) {
|
|
149
|
-
const expectedGeneration = this.generation;
|
|
150
|
-
logger.debug(`[ToolBatcher] Sending immediate text message: session=${sessionId}`);
|
|
151
|
-
void this.enqueueTask(sessionId, () => this.sendTextSafe(sessionId, normalizedMessage, "immediate", expectedGeneration));
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
const normalizedPrefix = uniquePrefix?.trim();
|
|
155
|
-
const queue = this.queues.get(sessionId) ?? [];
|
|
156
|
-
if (normalizedPrefix) {
|
|
157
|
-
const existingUniqueMessage = queue.find((item) => item.kind === "text" && item.text.startsWith(normalizedPrefix));
|
|
158
|
-
if (existingUniqueMessage) {
|
|
159
|
-
existingUniqueMessage.text = normalizedMessage;
|
|
160
|
-
this.queues.set(sessionId, queue);
|
|
161
|
-
logger.debug(`[ToolBatcher] Updated queued unique text message: session=${sessionId}, prefix=${normalizedPrefix}, interval=${this.intervalSeconds}s`);
|
|
162
|
-
this.ensureTimer(sessionId);
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
queue.push({ kind: "text", text: normalizedMessage });
|
|
167
|
-
this.queues.set(sessionId, queue);
|
|
168
|
-
logger.debug(`[ToolBatcher] Queued text message: session=${sessionId}, queueSize=${queue.length}, interval=${this.intervalSeconds}s`);
|
|
169
|
-
this.ensureTimer(sessionId);
|
|
170
|
-
}
|
|
171
|
-
async flushSessionInternal(sessionId, reason) {
|
|
172
|
-
const expectedGeneration = this.generation;
|
|
173
|
-
this.clearTimer(sessionId);
|
|
174
|
-
const queuedItems = this.queues.get(sessionId);
|
|
175
|
-
if (!queuedItems || queuedItems.length === 0) {
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
this.queues.delete(sessionId);
|
|
179
|
-
const flushItems = this.buildFlushItems(queuedItems);
|
|
180
|
-
logger.debug(`[ToolBatcher] Flushing ${queuedItems.length} queued items as ${flushItems.length} Telegram sends (session=${sessionId}, reason=${reason})`);
|
|
181
|
-
for (const item of flushItems) {
|
|
182
|
-
if (item.kind === "text") {
|
|
183
|
-
await this.sendTextSafe(sessionId, item.text, reason, expectedGeneration);
|
|
184
|
-
}
|
|
185
|
-
else {
|
|
186
|
-
await this.sendFileSafe(sessionId, item.fileData, reason, expectedGeneration);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
66
|
async sendTextSafe(sessionId, text, reason, expectedGeneration) {
|
|
191
67
|
if (this.generation !== expectedGeneration) {
|
|
192
68
|
logger.debug(`[ToolBatcher] Dropping stale tool text message: session=${sessionId}, reason=${reason}`);
|
|
@@ -211,75 +87,4 @@ export class ToolMessageBatcher {
|
|
|
211
87
|
logger.error(`[ToolBatcher] Failed to send tool file message: session=${sessionId}, reason=${reason}`, err);
|
|
212
88
|
}
|
|
213
89
|
}
|
|
214
|
-
buildFlushItems(entries) {
|
|
215
|
-
const result = [];
|
|
216
|
-
const textBuffer = [];
|
|
217
|
-
const flushTextBuffer = () => {
|
|
218
|
-
if (textBuffer.length === 0) {
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
const packedTextMessages = this.packMessages(textBuffer);
|
|
222
|
-
for (const text of packedTextMessages) {
|
|
223
|
-
result.push({ kind: "text", text });
|
|
224
|
-
}
|
|
225
|
-
textBuffer.length = 0;
|
|
226
|
-
};
|
|
227
|
-
for (const entry of entries) {
|
|
228
|
-
if (entry.kind === "text") {
|
|
229
|
-
textBuffer.push(entry.text);
|
|
230
|
-
}
|
|
231
|
-
else {
|
|
232
|
-
flushTextBuffer();
|
|
233
|
-
result.push({ kind: "file", fileData: entry.fileData });
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
flushTextBuffer();
|
|
237
|
-
return result;
|
|
238
|
-
}
|
|
239
|
-
packMessages(messages) {
|
|
240
|
-
const normalizedEntries = messages
|
|
241
|
-
.flatMap((message) => this.splitLongText(message, TELEGRAM_MESSAGE_MAX_LENGTH))
|
|
242
|
-
.filter((entry) => entry.length > 0);
|
|
243
|
-
if (normalizedEntries.length === 0) {
|
|
244
|
-
return [];
|
|
245
|
-
}
|
|
246
|
-
const result = [];
|
|
247
|
-
let current = "";
|
|
248
|
-
for (const entry of normalizedEntries) {
|
|
249
|
-
if (!current) {
|
|
250
|
-
current = entry;
|
|
251
|
-
continue;
|
|
252
|
-
}
|
|
253
|
-
const candidate = `${current}\n\n${entry}`;
|
|
254
|
-
if (candidate.length <= TELEGRAM_MESSAGE_MAX_LENGTH) {
|
|
255
|
-
current = candidate;
|
|
256
|
-
continue;
|
|
257
|
-
}
|
|
258
|
-
result.push(current);
|
|
259
|
-
current = entry;
|
|
260
|
-
}
|
|
261
|
-
if (current) {
|
|
262
|
-
result.push(current);
|
|
263
|
-
}
|
|
264
|
-
return result;
|
|
265
|
-
}
|
|
266
|
-
splitLongText(text, limit) {
|
|
267
|
-
if (text.length <= limit) {
|
|
268
|
-
return [text];
|
|
269
|
-
}
|
|
270
|
-
const chunks = [];
|
|
271
|
-
let remaining = text;
|
|
272
|
-
while (remaining.length > limit) {
|
|
273
|
-
let splitIndex = remaining.lastIndexOf("\n", limit);
|
|
274
|
-
if (splitIndex <= 0 || splitIndex < Math.floor(limit * 0.5)) {
|
|
275
|
-
splitIndex = limit;
|
|
276
|
-
}
|
|
277
|
-
chunks.push(remaining.slice(0, splitIndex));
|
|
278
|
-
remaining = remaining.slice(splitIndex).replace(/^\n+/, "");
|
|
279
|
-
}
|
|
280
|
-
if (remaining.length > 0) {
|
|
281
|
-
chunks.push(remaining);
|
|
282
|
-
}
|
|
283
|
-
return chunks;
|
|
284
|
-
}
|
|
285
90
|
}
|