@grinev/opencode-telegram-bot 0.12.0 → 0.13.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 +6 -0
- package/README.md +2 -0
- package/dist/bot/commands/commands.js +87 -5
- 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 +150 -22
- 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 +6 -0
- package/dist/i18n/en.js +6 -0
- package/dist/i18n/es.js +6 -0
- package/dist/i18n/fr.js +6 -0
- package/dist/i18n/ru.js +6 -0
- package/dist/i18n/zh.js +9 -3
- package/dist/interaction/busy.js +8 -0
- package/dist/interaction/guard.js +42 -6
- package/dist/pinned/manager.js +22 -3
- package/dist/summary/aggregator.js +195 -36
- package/dist/summary/formatter.js +5 -3
- package/dist/summary/tool-message-batcher.js +9 -0
- package/package.json +1 -1
package/dist/pinned/manager.js
CHANGED
|
@@ -17,6 +17,7 @@ class PinnedMessageManager {
|
|
|
17
17
|
tokensLimit: 0,
|
|
18
18
|
lastUpdated: 0,
|
|
19
19
|
changedFiles: [],
|
|
20
|
+
cost: 0,
|
|
20
21
|
};
|
|
21
22
|
contextLimit = null;
|
|
22
23
|
onKeyboardUpdateCallback;
|
|
@@ -40,6 +41,7 @@ class PinnedMessageManager {
|
|
|
40
41
|
logger.info(`[PinnedManager] Session changed: ${sessionId}, title: ${sessionTitle}`);
|
|
41
42
|
// Reset tokens for new session
|
|
42
43
|
this.state.tokensUsed = 0;
|
|
44
|
+
this.state.cost = 0;
|
|
43
45
|
// Update state
|
|
44
46
|
this.state.sessionId = sessionId;
|
|
45
47
|
this.state.sessionTitle = sessionTitle || t("pinned.default_session_title");
|
|
@@ -84,9 +86,10 @@ class PinnedMessageManager {
|
|
|
84
86
|
logger.warn("[PinnedManager] Failed to load session history:", error);
|
|
85
87
|
return;
|
|
86
88
|
}
|
|
87
|
-
// Get the maximum context size from session history
|
|
89
|
+
// Get the maximum context size and total cost from session history
|
|
88
90
|
// Context = input + cache.read (cache.read contains previously cached context)
|
|
89
91
|
let maxContextSize = 0;
|
|
92
|
+
let totalCost = 0;
|
|
90
93
|
logger.debug(`[PinnedManager] Processing ${messagesData.length} messages from history`);
|
|
91
94
|
messagesData.forEach(({ info }) => {
|
|
92
95
|
if (info.role === "assistant") {
|
|
@@ -99,16 +102,20 @@ class PinnedMessageManager {
|
|
|
99
102
|
const input = assistantInfo.tokens?.input || 0;
|
|
100
103
|
const cacheRead = assistantInfo.tokens?.cache?.read || 0;
|
|
101
104
|
const contextSize = input + cacheRead;
|
|
102
|
-
|
|
105
|
+
const cost = assistantInfo.cost || 0;
|
|
106
|
+
logger.debug(`[PinnedManager] Assistant message: input=${input}, cache.read=${cacheRead}, total=${contextSize}, cost=$${cost.toFixed(2)}`);
|
|
103
107
|
// Keep track of maximum context size (peak usage in session)
|
|
104
108
|
if (contextSize > maxContextSize) {
|
|
105
109
|
maxContextSize = contextSize;
|
|
106
110
|
}
|
|
111
|
+
// Accumulate total session cost
|
|
112
|
+
totalCost += cost;
|
|
107
113
|
}
|
|
108
114
|
});
|
|
109
115
|
this.state.tokensUsed = maxContextSize;
|
|
116
|
+
this.state.cost = totalCost;
|
|
110
117
|
this.state.sessionId = sessionId;
|
|
111
|
-
logger.info(`[PinnedManager] Loaded context from history: ${this.state.tokensUsed} tokens`);
|
|
118
|
+
logger.info(`[PinnedManager] Loaded context from history: ${this.state.tokensUsed} tokens, cost: $${this.state.cost.toFixed(2)}`);
|
|
112
119
|
await this.updatePinnedMessage();
|
|
113
120
|
}
|
|
114
121
|
catch (err) {
|
|
@@ -140,6 +147,15 @@ class PinnedMessageManager {
|
|
|
140
147
|
await this.refreshSessionTitle();
|
|
141
148
|
await this.updatePinnedMessage();
|
|
142
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Called when cost info is received from SSE events
|
|
152
|
+
*/
|
|
153
|
+
async onCostUpdate(cost) {
|
|
154
|
+
const currentCost = this.state.cost || 0;
|
|
155
|
+
this.state.cost = currentCost + cost;
|
|
156
|
+
logger.debug(`[PinnedManager] Cost added: $${cost.toFixed(2)}, total session: $${(this.state.cost || 0).toFixed(2)}`);
|
|
157
|
+
await this.updatePinnedMessage();
|
|
158
|
+
}
|
|
143
159
|
/**
|
|
144
160
|
* Set callback for keyboard updates when context changes
|
|
145
161
|
*/
|
|
@@ -455,6 +471,9 @@ class PinnedMessageManager {
|
|
|
455
471
|
percent: percentage,
|
|
456
472
|
}),
|
|
457
473
|
];
|
|
474
|
+
if (this.state.cost !== undefined && this.state.cost !== null) {
|
|
475
|
+
lines.push(t("pinned.line.cost", { cost: `$${this.state.cost.toFixed(2)}` }));
|
|
476
|
+
}
|
|
458
477
|
if (this.state.changedFiles.length > 0) {
|
|
459
478
|
const maxFiles = 10;
|
|
460
479
|
const total = this.state.changedFiles.length;
|
|
@@ -26,18 +26,19 @@ 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;
|
|
38
38
|
onQuestionErrorCallback = null;
|
|
39
39
|
onThinkingCallback = null;
|
|
40
40
|
onTokensCallback = null;
|
|
41
|
+
onCostCallback = null;
|
|
41
42
|
onSessionCompactedCallback = null;
|
|
42
43
|
onSessionErrorCallback = null;
|
|
43
44
|
onSessionRetryCallback = null;
|
|
@@ -47,9 +48,11 @@ class SummaryAggregator {
|
|
|
47
48
|
onClearedCallback = null;
|
|
48
49
|
processedToolStates = new Set();
|
|
49
50
|
thinkingFiredForMessages = new Set();
|
|
51
|
+
knownTextPartIds = new Map();
|
|
50
52
|
bot = null;
|
|
51
53
|
chatId = null;
|
|
52
54
|
typingTimer = null;
|
|
55
|
+
typingIndicatorEnabled = true;
|
|
53
56
|
partHashes = new Map();
|
|
54
57
|
setBotAndChatId(bot, chatId) {
|
|
55
58
|
this.bot = bot;
|
|
@@ -58,6 +61,9 @@ class SummaryAggregator {
|
|
|
58
61
|
setOnComplete(callback) {
|
|
59
62
|
this.onCompleteCallback = callback;
|
|
60
63
|
}
|
|
64
|
+
setOnPartial(callback) {
|
|
65
|
+
this.onPartialCallback = callback;
|
|
66
|
+
}
|
|
61
67
|
setOnTool(callback) {
|
|
62
68
|
this.onToolCallback = callback;
|
|
63
69
|
}
|
|
@@ -76,6 +82,9 @@ class SummaryAggregator {
|
|
|
76
82
|
setOnTokens(callback) {
|
|
77
83
|
this.onTokensCallback = callback;
|
|
78
84
|
}
|
|
85
|
+
setOnCost(callback) {
|
|
86
|
+
this.onCostCallback = callback;
|
|
87
|
+
}
|
|
79
88
|
setOnSessionCompacted(callback) {
|
|
80
89
|
this.onSessionCompactedCallback = callback;
|
|
81
90
|
}
|
|
@@ -97,7 +106,16 @@ class SummaryAggregator {
|
|
|
97
106
|
setOnCleared(callback) {
|
|
98
107
|
this.onClearedCallback = callback;
|
|
99
108
|
}
|
|
109
|
+
setTypingIndicatorEnabled(enabled) {
|
|
110
|
+
this.typingIndicatorEnabled = enabled;
|
|
111
|
+
if (!enabled) {
|
|
112
|
+
this.stopTypingIndicator();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
100
115
|
startTypingIndicator() {
|
|
116
|
+
if (!this.typingIndicatorEnabled) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
101
119
|
if (this.typingTimer) {
|
|
102
120
|
return;
|
|
103
121
|
}
|
|
@@ -118,6 +136,11 @@ class SummaryAggregator {
|
|
|
118
136
|
}
|
|
119
137
|
}
|
|
120
138
|
processEvent(event) {
|
|
139
|
+
const eventType = event.type;
|
|
140
|
+
if (eventType === "message.part.delta") {
|
|
141
|
+
this.handleMessagePartDelta(event);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
121
144
|
// Log all question-related events for debugging
|
|
122
145
|
if (event.type.startsWith("question.")) {
|
|
123
146
|
logger.info(`[Aggregator] Question event: ${event.type}`, JSON.stringify(event.properties, null, 2));
|
|
@@ -177,10 +200,10 @@ class SummaryAggregator {
|
|
|
177
200
|
clear() {
|
|
178
201
|
this.stopTypingIndicator();
|
|
179
202
|
this.currentSessionId = null;
|
|
180
|
-
this.
|
|
181
|
-
this.pendingParts.clear();
|
|
203
|
+
this.textMessageStates.clear();
|
|
182
204
|
this.messages.clear();
|
|
183
205
|
this.partHashes.clear();
|
|
206
|
+
this.knownTextPartIds.clear();
|
|
184
207
|
this.processedToolStates.clear();
|
|
185
208
|
this.thinkingFiredForMessages.clear();
|
|
186
209
|
this.messageCount = 0;
|
|
@@ -202,21 +225,26 @@ class SummaryAggregator {
|
|
|
202
225
|
const messageID = info.id;
|
|
203
226
|
this.messages.set(messageID, { role: info.role });
|
|
204
227
|
if (info.role === "assistant") {
|
|
205
|
-
if (!this.
|
|
206
|
-
this.
|
|
228
|
+
if (!this.textMessageStates.has(messageID)) {
|
|
229
|
+
this.textMessageStates.set(messageID, {
|
|
230
|
+
orderedPartIds: [],
|
|
231
|
+
partTexts: new Map(),
|
|
232
|
+
optimisticUpdateCount: 0,
|
|
233
|
+
});
|
|
207
234
|
this.messageCount++;
|
|
208
235
|
this.startTypingIndicator();
|
|
209
236
|
}
|
|
210
|
-
const
|
|
211
|
-
const current = this.currentMessageParts.get(messageID) || [];
|
|
212
|
-
this.currentMessageParts.set(messageID, [...current, ...pending]);
|
|
213
|
-
this.pendingParts.delete(messageID);
|
|
237
|
+
const textState = this.getOrCreateTextMessageState(messageID);
|
|
214
238
|
const assistantMessage = info;
|
|
215
239
|
const time = assistantMessage.time;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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}`);
|
|
220
248
|
// Extract and report tokens BEFORE onComplete so keyboard context is updated
|
|
221
249
|
const assistantInfo = info;
|
|
222
250
|
if (this.onTokensCallback && assistantInfo.tokens) {
|
|
@@ -231,14 +259,20 @@ class SummaryAggregator {
|
|
|
231
259
|
// Call synchronously so keyboardManager is updated before onComplete sends the reply
|
|
232
260
|
this.onTokensCallback(tokens);
|
|
233
261
|
}
|
|
234
|
-
|
|
235
|
-
|
|
262
|
+
// Extract and report cost
|
|
263
|
+
if (this.onCostCallback && assistantInfo.cost !== undefined) {
|
|
264
|
+
logger.debug(`[Aggregator] Cost: $${assistantInfo.cost.toFixed(2)}`);
|
|
265
|
+
this.onCostCallback(assistantInfo.cost);
|
|
266
|
+
}
|
|
267
|
+
if (this.onCompleteCallback && finalText.length > 0) {
|
|
268
|
+
this.onCompleteCallback(this.currentSessionId, messageID, finalText);
|
|
236
269
|
}
|
|
237
|
-
this.
|
|
270
|
+
this.textMessageStates.delete(messageID);
|
|
238
271
|
this.messages.delete(messageID);
|
|
239
272
|
this.partHashes.delete(messageID);
|
|
240
|
-
|
|
241
|
-
|
|
273
|
+
this.knownTextPartIds.delete(messageID);
|
|
274
|
+
logger.debug(`[Aggregator] Message completed cleanup: remaining messages=${this.textMessageStates.size}`);
|
|
275
|
+
if (this.textMessageStates.size === 0) {
|
|
242
276
|
logger.debug("[Aggregator] No more active messages, stopping typing indicator");
|
|
243
277
|
this.stopTypingIndicator();
|
|
244
278
|
}
|
|
@@ -253,6 +287,18 @@ class SummaryAggregator {
|
|
|
253
287
|
}
|
|
254
288
|
const messageID = part.messageID;
|
|
255
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
|
+
}
|
|
256
302
|
if (part.type === "reasoning") {
|
|
257
303
|
// Fire the thinking callback once per message on the first reasoning part.
|
|
258
304
|
// This is the signal that the model is actually doing extended thinking.
|
|
@@ -268,29 +314,23 @@ class SummaryAggregator {
|
|
|
268
314
|
}
|
|
269
315
|
}
|
|
270
316
|
else if (part.type === "text" && "text" in part && part.text) {
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
this.
|
|
274
|
-
|
|
275
|
-
const hashes = this.partHashes.get(messageID);
|
|
276
|
-
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) {
|
|
277
321
|
return;
|
|
278
322
|
}
|
|
279
|
-
|
|
323
|
+
const fullText = this.getCombinedMessageText(messageID);
|
|
280
324
|
if (messageInfo && messageInfo.role === "assistant") {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
this.startTypingIndicator();
|
|
284
|
-
}
|
|
285
|
-
const parts = this.currentMessageParts.get(messageID);
|
|
286
|
-
parts.push(part.text);
|
|
325
|
+
this.startTypingIndicator();
|
|
326
|
+
this.emitPartialText(part.sessionID, messageID, fullText);
|
|
287
327
|
}
|
|
288
328
|
else {
|
|
289
|
-
|
|
290
|
-
|
|
329
|
+
const state = this.getOrCreateTextMessageState(messageID);
|
|
330
|
+
state.optimisticUpdateCount++;
|
|
331
|
+
if (state.optimisticUpdateCount >= 2) {
|
|
332
|
+
this.emitPartialText(part.sessionID, messageID, fullText);
|
|
291
333
|
}
|
|
292
|
-
const pending = this.pendingParts.get(messageID);
|
|
293
|
-
pending.push(part.text);
|
|
294
334
|
}
|
|
295
335
|
}
|
|
296
336
|
else if (part.type === "tool") {
|
|
@@ -351,6 +391,125 @@ class SummaryAggregator {
|
|
|
351
391
|
}
|
|
352
392
|
this.lastUpdated = Date.now();
|
|
353
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
|
+
}
|
|
354
513
|
prepareToolFileContext(tool, input, title, metadata) {
|
|
355
514
|
if (tool === "write" && input) {
|
|
356
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
|
}
|