@grinev/opencode-telegram-bot 0.12.1 → 0.13.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/.env.example +6 -0
- package/README.md +2 -0
- package/dist/bot/commands/commands.js +105 -9
- package/dist/bot/commands/new.js +5 -0
- package/dist/bot/commands/projects.js +9 -0
- package/dist/bot/commands/sessions.js +9 -0
- package/dist/bot/index.js +128 -21
- package/dist/bot/middleware/interaction-guard.js +6 -2
- package/dist/bot/streaming/response-streamer.js +284 -0
- package/dist/bot/utils/busy-guard.js +15 -0
- package/dist/bot/utils/finalize-assistant-response.js +23 -0
- package/dist/bot/utils/send-with-markdown-fallback.js +6 -8
- package/dist/bot/utils/thinking-message.js +12 -0
- package/dist/config.js +2 -0
- package/dist/i18n/de.js +5 -0
- package/dist/i18n/en.js +5 -0
- package/dist/i18n/es.js +5 -0
- package/dist/i18n/fr.js +5 -0
- package/dist/i18n/ru.js +5 -0
- package/dist/i18n/zh.js +8 -3
- package/dist/interaction/busy.js +8 -0
- package/dist/interaction/guard.js +42 -6
- package/dist/pinned/manager.js +8 -9
- package/dist/summary/aggregator.js +186 -36
- package/dist/summary/formatter.js +5 -3
- package/dist/summary/tool-message-batcher.js +9 -0
- package/package.json +1 -1
|
@@ -26,12 +26,12 @@ function countDiffChangesFromText(text) {
|
|
|
26
26
|
}
|
|
27
27
|
class SummaryAggregator {
|
|
28
28
|
currentSessionId = null;
|
|
29
|
-
|
|
30
|
-
pendingParts = new Map();
|
|
29
|
+
textMessageStates = new Map();
|
|
31
30
|
messages = new Map();
|
|
32
31
|
messageCount = 0;
|
|
33
32
|
lastUpdated = 0;
|
|
34
33
|
onCompleteCallback = null;
|
|
34
|
+
onPartialCallback = null;
|
|
35
35
|
onToolCallback = null;
|
|
36
36
|
onToolFileCallback = null;
|
|
37
37
|
onQuestionCallback = null;
|
|
@@ -48,9 +48,11 @@ class SummaryAggregator {
|
|
|
48
48
|
onClearedCallback = null;
|
|
49
49
|
processedToolStates = new Set();
|
|
50
50
|
thinkingFiredForMessages = new Set();
|
|
51
|
+
knownTextPartIds = new Map();
|
|
51
52
|
bot = null;
|
|
52
53
|
chatId = null;
|
|
53
54
|
typingTimer = null;
|
|
55
|
+
typingIndicatorEnabled = true;
|
|
54
56
|
partHashes = new Map();
|
|
55
57
|
setBotAndChatId(bot, chatId) {
|
|
56
58
|
this.bot = bot;
|
|
@@ -59,6 +61,9 @@ class SummaryAggregator {
|
|
|
59
61
|
setOnComplete(callback) {
|
|
60
62
|
this.onCompleteCallback = callback;
|
|
61
63
|
}
|
|
64
|
+
setOnPartial(callback) {
|
|
65
|
+
this.onPartialCallback = callback;
|
|
66
|
+
}
|
|
62
67
|
setOnTool(callback) {
|
|
63
68
|
this.onToolCallback = callback;
|
|
64
69
|
}
|
|
@@ -101,7 +106,16 @@ class SummaryAggregator {
|
|
|
101
106
|
setOnCleared(callback) {
|
|
102
107
|
this.onClearedCallback = callback;
|
|
103
108
|
}
|
|
109
|
+
setTypingIndicatorEnabled(enabled) {
|
|
110
|
+
this.typingIndicatorEnabled = enabled;
|
|
111
|
+
if (!enabled) {
|
|
112
|
+
this.stopTypingIndicator();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
104
115
|
startTypingIndicator() {
|
|
116
|
+
if (!this.typingIndicatorEnabled) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
105
119
|
if (this.typingTimer) {
|
|
106
120
|
return;
|
|
107
121
|
}
|
|
@@ -122,6 +136,11 @@ class SummaryAggregator {
|
|
|
122
136
|
}
|
|
123
137
|
}
|
|
124
138
|
processEvent(event) {
|
|
139
|
+
const eventType = event.type;
|
|
140
|
+
if (eventType === "message.part.delta") {
|
|
141
|
+
this.handleMessagePartDelta(event);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
125
144
|
// Log all question-related events for debugging
|
|
126
145
|
if (event.type.startsWith("question.")) {
|
|
127
146
|
logger.info(`[Aggregator] Question event: ${event.type}`, JSON.stringify(event.properties, null, 2));
|
|
@@ -181,10 +200,10 @@ class SummaryAggregator {
|
|
|
181
200
|
clear() {
|
|
182
201
|
this.stopTypingIndicator();
|
|
183
202
|
this.currentSessionId = null;
|
|
184
|
-
this.
|
|
185
|
-
this.pendingParts.clear();
|
|
203
|
+
this.textMessageStates.clear();
|
|
186
204
|
this.messages.clear();
|
|
187
205
|
this.partHashes.clear();
|
|
206
|
+
this.knownTextPartIds.clear();
|
|
188
207
|
this.processedToolStates.clear();
|
|
189
208
|
this.thinkingFiredForMessages.clear();
|
|
190
209
|
this.messageCount = 0;
|
|
@@ -206,21 +225,26 @@ class SummaryAggregator {
|
|
|
206
225
|
const messageID = info.id;
|
|
207
226
|
this.messages.set(messageID, { role: info.role });
|
|
208
227
|
if (info.role === "assistant") {
|
|
209
|
-
if (!this.
|
|
210
|
-
this.
|
|
228
|
+
if (!this.textMessageStates.has(messageID)) {
|
|
229
|
+
this.textMessageStates.set(messageID, {
|
|
230
|
+
orderedPartIds: [],
|
|
231
|
+
partTexts: new Map(),
|
|
232
|
+
optimisticUpdateCount: 0,
|
|
233
|
+
});
|
|
211
234
|
this.messageCount++;
|
|
212
235
|
this.startTypingIndicator();
|
|
213
236
|
}
|
|
214
|
-
const
|
|
215
|
-
const current = this.currentMessageParts.get(messageID) || [];
|
|
216
|
-
this.currentMessageParts.set(messageID, [...current, ...pending]);
|
|
217
|
-
this.pendingParts.delete(messageID);
|
|
237
|
+
const textState = this.getOrCreateTextMessageState(messageID);
|
|
218
238
|
const assistantMessage = info;
|
|
219
239
|
const time = assistantMessage.time;
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
240
|
+
const isCompleted = Boolean(time?.completed);
|
|
241
|
+
const messageText = this.getCombinedMessageText(messageID);
|
|
242
|
+
if (!isCompleted && textState.optimisticUpdateCount === 1) {
|
|
243
|
+
this.emitPartialText(info.sessionID, messageID, messageText);
|
|
244
|
+
}
|
|
245
|
+
if (isCompleted) {
|
|
246
|
+
const finalText = messageText;
|
|
247
|
+
logger.debug(`[Aggregator] Message part completed: messageId=${messageID}, textLength=${finalText.length}, totalParts=${textState.orderedPartIds.length}, session=${this.currentSessionId}`);
|
|
224
248
|
// Extract and report tokens BEFORE onComplete so keyboard context is updated
|
|
225
249
|
const assistantInfo = info;
|
|
226
250
|
if (this.onTokensCallback && assistantInfo.tokens) {
|
|
@@ -240,14 +264,15 @@ class SummaryAggregator {
|
|
|
240
264
|
logger.debug(`[Aggregator] Cost: $${assistantInfo.cost.toFixed(2)}`);
|
|
241
265
|
this.onCostCallback(assistantInfo.cost);
|
|
242
266
|
}
|
|
243
|
-
if (this.onCompleteCallback &&
|
|
244
|
-
this.onCompleteCallback(this.currentSessionId,
|
|
267
|
+
if (this.onCompleteCallback && finalText.length > 0) {
|
|
268
|
+
this.onCompleteCallback(this.currentSessionId, messageID, finalText);
|
|
245
269
|
}
|
|
246
|
-
this.
|
|
270
|
+
this.textMessageStates.delete(messageID);
|
|
247
271
|
this.messages.delete(messageID);
|
|
248
272
|
this.partHashes.delete(messageID);
|
|
249
|
-
|
|
250
|
-
|
|
273
|
+
this.knownTextPartIds.delete(messageID);
|
|
274
|
+
logger.debug(`[Aggregator] Message completed cleanup: remaining messages=${this.textMessageStates.size}`);
|
|
275
|
+
if (this.textMessageStates.size === 0) {
|
|
251
276
|
logger.debug("[Aggregator] No more active messages, stopping typing indicator");
|
|
252
277
|
this.stopTypingIndicator();
|
|
253
278
|
}
|
|
@@ -262,6 +287,18 @@ class SummaryAggregator {
|
|
|
262
287
|
}
|
|
263
288
|
const messageID = part.messageID;
|
|
264
289
|
const messageInfo = this.messages.get(messageID);
|
|
290
|
+
if (part.type === "text") {
|
|
291
|
+
this.registerKnownTextPart(messageID, part.id);
|
|
292
|
+
this.registerTextPart(messageID, part.id);
|
|
293
|
+
}
|
|
294
|
+
const deltaFromUpdated = event.properties.delta;
|
|
295
|
+
if (part.type === "text" &&
|
|
296
|
+
typeof deltaFromUpdated === "string" &&
|
|
297
|
+
deltaFromUpdated.length > 0) {
|
|
298
|
+
this.applyTextDelta(part.sessionID, messageID, part.id, deltaFromUpdated, part.text);
|
|
299
|
+
this.lastUpdated = Date.now();
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
265
302
|
if (part.type === "reasoning") {
|
|
266
303
|
// Fire the thinking callback once per message on the first reasoning part.
|
|
267
304
|
// This is the signal that the model is actually doing extended thinking.
|
|
@@ -277,29 +314,23 @@ class SummaryAggregator {
|
|
|
277
314
|
}
|
|
278
315
|
}
|
|
279
316
|
else if (part.type === "text" && "text" in part && part.text) {
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
this.
|
|
283
|
-
|
|
284
|
-
const hashes = this.partHashes.get(messageID);
|
|
285
|
-
if (hashes.has(partHash)) {
|
|
317
|
+
const wasUpdated = messageInfo && messageInfo.role === "assistant"
|
|
318
|
+
? this.setTextPartSnapshot(messageID, part.id, part.text)
|
|
319
|
+
: this.setOptimisticTextSnapshot(messageID, part.id, part.text);
|
|
320
|
+
if (!wasUpdated) {
|
|
286
321
|
return;
|
|
287
322
|
}
|
|
288
|
-
|
|
323
|
+
const fullText = this.getCombinedMessageText(messageID);
|
|
289
324
|
if (messageInfo && messageInfo.role === "assistant") {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
this.startTypingIndicator();
|
|
293
|
-
}
|
|
294
|
-
const parts = this.currentMessageParts.get(messageID);
|
|
295
|
-
parts.push(part.text);
|
|
325
|
+
this.startTypingIndicator();
|
|
326
|
+
this.emitPartialText(part.sessionID, messageID, fullText);
|
|
296
327
|
}
|
|
297
328
|
else {
|
|
298
|
-
|
|
299
|
-
|
|
329
|
+
const state = this.getOrCreateTextMessageState(messageID);
|
|
330
|
+
state.optimisticUpdateCount++;
|
|
331
|
+
if (state.optimisticUpdateCount >= 2) {
|
|
332
|
+
this.emitPartialText(part.sessionID, messageID, fullText);
|
|
300
333
|
}
|
|
301
|
-
const pending = this.pendingParts.get(messageID);
|
|
302
|
-
pending.push(part.text);
|
|
303
334
|
}
|
|
304
335
|
}
|
|
305
336
|
else if (part.type === "tool") {
|
|
@@ -360,6 +391,125 @@ class SummaryAggregator {
|
|
|
360
391
|
}
|
|
361
392
|
this.lastUpdated = Date.now();
|
|
362
393
|
}
|
|
394
|
+
handleMessagePartDelta(event) {
|
|
395
|
+
const part = event.properties.part;
|
|
396
|
+
const sessionID = part?.sessionID || event.properties.sessionID;
|
|
397
|
+
const messageID = part?.messageID || event.properties.messageID;
|
|
398
|
+
const partID = part?.id || event.properties.partID || "text";
|
|
399
|
+
const partType = part?.type || event.properties.type;
|
|
400
|
+
const delta = event.properties.delta;
|
|
401
|
+
if (!sessionID || !messageID || typeof delta !== "string" || delta.length === 0) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (partType && partType !== "text") {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (partType === "text") {
|
|
408
|
+
this.registerKnownTextPart(messageID, partID);
|
|
409
|
+
this.registerTextPart(messageID, partID);
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
const knownTextIds = this.knownTextPartIds.get(messageID);
|
|
413
|
+
const isKnownTextPart = knownTextIds?.has(partID) ?? false;
|
|
414
|
+
const thinkingFired = this.thinkingFiredForMessages.has(messageID);
|
|
415
|
+
if (thinkingFired && !isKnownTextPart) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (!thinkingFired && !isKnownTextPart) {
|
|
419
|
+
this.registerKnownTextPart(messageID, partID);
|
|
420
|
+
this.registerTextPart(messageID, partID);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
this.applyTextDelta(sessionID, messageID, partID, delta, part?.text);
|
|
424
|
+
}
|
|
425
|
+
applyTextDelta(sessionID, messageID, partID, delta, fullTextHint) {
|
|
426
|
+
if (sessionID !== this.currentSessionId) {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
this.registerTextPart(messageID, partID);
|
|
430
|
+
const state = this.getOrCreateTextMessageState(messageID);
|
|
431
|
+
const previous = state.partTexts.get(partID) || "";
|
|
432
|
+
let accumulated = `${previous}${delta}`;
|
|
433
|
+
if (typeof fullTextHint === "string" && fullTextHint.length > accumulated.length) {
|
|
434
|
+
accumulated = fullTextHint;
|
|
435
|
+
}
|
|
436
|
+
state.partTexts.set(partID, accumulated);
|
|
437
|
+
const combined = this.getCombinedMessageText(messageID);
|
|
438
|
+
if (!combined.trim()) {
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
this.startTypingIndicator();
|
|
442
|
+
this.emitPartialText(sessionID, messageID, combined);
|
|
443
|
+
}
|
|
444
|
+
emitPartialText(sessionId, messageId, messageText) {
|
|
445
|
+
if (!this.onPartialCallback || !messageText.trim()) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
try {
|
|
449
|
+
this.onPartialCallback(sessionId, messageId, messageText);
|
|
450
|
+
}
|
|
451
|
+
catch (err) {
|
|
452
|
+
logger.error("[Aggregator] Error in partial callback:", err);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
getOrCreateTextMessageState(messageID) {
|
|
456
|
+
const existing = this.textMessageStates.get(messageID);
|
|
457
|
+
if (existing) {
|
|
458
|
+
return existing;
|
|
459
|
+
}
|
|
460
|
+
const state = {
|
|
461
|
+
orderedPartIds: [],
|
|
462
|
+
partTexts: new Map(),
|
|
463
|
+
optimisticUpdateCount: 0,
|
|
464
|
+
};
|
|
465
|
+
this.textMessageStates.set(messageID, state);
|
|
466
|
+
return state;
|
|
467
|
+
}
|
|
468
|
+
registerKnownTextPart(messageID, partID) {
|
|
469
|
+
if (!this.knownTextPartIds.has(messageID)) {
|
|
470
|
+
this.knownTextPartIds.set(messageID, new Set());
|
|
471
|
+
}
|
|
472
|
+
this.knownTextPartIds.get(messageID).add(partID);
|
|
473
|
+
}
|
|
474
|
+
registerTextPart(messageID, partID) {
|
|
475
|
+
const state = this.getOrCreateTextMessageState(messageID);
|
|
476
|
+
if (!state.orderedPartIds.includes(partID)) {
|
|
477
|
+
state.orderedPartIds.push(partID);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
setTextPartSnapshot(messageID, partID, text) {
|
|
481
|
+
const normalized = text;
|
|
482
|
+
const partHash = this.hashString(`${partID}\n${normalized}`);
|
|
483
|
+
if (!this.partHashes.has(messageID)) {
|
|
484
|
+
this.partHashes.set(messageID, new Set());
|
|
485
|
+
}
|
|
486
|
+
const hashes = this.partHashes.get(messageID);
|
|
487
|
+
if (hashes.has(partHash)) {
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
hashes.add(partHash);
|
|
491
|
+
this.registerTextPart(messageID, partID);
|
|
492
|
+
const state = this.getOrCreateTextMessageState(messageID);
|
|
493
|
+
state.partTexts.set(partID, normalized);
|
|
494
|
+
return true;
|
|
495
|
+
}
|
|
496
|
+
setOptimisticTextSnapshot(messageID, partID, text) {
|
|
497
|
+
const wasUpdated = this.setTextPartSnapshot(messageID, partID, text);
|
|
498
|
+
if (!wasUpdated) {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
const state = this.getOrCreateTextMessageState(messageID);
|
|
502
|
+
state.orderedPartIds = [partID];
|
|
503
|
+
state.partTexts = new Map([[partID, text]]);
|
|
504
|
+
return true;
|
|
505
|
+
}
|
|
506
|
+
getCombinedMessageText(messageID) {
|
|
507
|
+
const state = this.textMessageStates.get(messageID);
|
|
508
|
+
if (!state) {
|
|
509
|
+
return "";
|
|
510
|
+
}
|
|
511
|
+
return state.orderedPartIds.map((partID) => state.partTexts.get(partID) || "").join("");
|
|
512
|
+
}
|
|
363
513
|
prepareToolFileContext(tool, input, title, metadata) {
|
|
364
514
|
if (tool === "write" && input) {
|
|
365
515
|
const filePath = typeof input.filePath === "string" ? normalizePathForDisplay(input.filePath) : "";
|
|
@@ -144,11 +144,13 @@ function formatMarkdownForTelegram(text) {
|
|
|
144
144
|
return text;
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
|
-
export function formatSummaryWithMode(text, mode) {
|
|
147
|
+
export function formatSummaryWithMode(text, mode, maxLength = TELEGRAM_MESSAGE_LIMIT) {
|
|
148
148
|
if (!text || text.trim().length === 0) {
|
|
149
149
|
return [];
|
|
150
150
|
}
|
|
151
|
-
const
|
|
151
|
+
const normalizedMaxLength = Math.max(1, Math.floor(maxLength));
|
|
152
|
+
const rawTextLimit = mode === "raw" ? Math.max(1, normalizedMaxLength - "```\n\n```".length) : normalizedMaxLength;
|
|
153
|
+
const parts = splitText(text, rawTextLimit);
|
|
152
154
|
const formattedParts = [];
|
|
153
155
|
for (const part of parts) {
|
|
154
156
|
const trimmed = part.trim();
|
|
@@ -157,7 +159,7 @@ export function formatSummaryWithMode(text, mode) {
|
|
|
157
159
|
}
|
|
158
160
|
if (mode === "markdown") {
|
|
159
161
|
const converted = formatMarkdownForTelegram(trimmed);
|
|
160
|
-
const convertedParts = splitText(converted,
|
|
162
|
+
const convertedParts = splitText(converted, normalizedMaxLength);
|
|
161
163
|
for (const convertedPart of convertedParts) {
|
|
162
164
|
const normalizedPart = convertedPart.trim();
|
|
163
165
|
if (normalizedPart) {
|
|
@@ -46,6 +46,15 @@ export class ToolMessageBatcher {
|
|
|
46
46
|
enqueue(sessionId, message) {
|
|
47
47
|
this.enqueueTextInternal(sessionId, message);
|
|
48
48
|
}
|
|
49
|
+
sendTextNow(sessionId, message, reason) {
|
|
50
|
+
const normalizedMessage = message.trim();
|
|
51
|
+
if (!sessionId || normalizedMessage.length === 0) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const expectedGeneration = this.generation;
|
|
55
|
+
logger.debug(`[ToolBatcher] Sending immediate text message outside queue: session=${sessionId}, reason=${reason}`);
|
|
56
|
+
void this.enqueueTask(sessionId, () => this.sendTextSafe(sessionId, normalizedMessage, reason, expectedGeneration));
|
|
57
|
+
}
|
|
49
58
|
enqueueUniqueByPrefix(sessionId, message, prefix) {
|
|
50
59
|
this.enqueueTextInternal(sessionId, message, prefix);
|
|
51
60
|
}
|