@fonz/tgcc 0.6.15 β 0.6.18
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 +74 -50
- package/dist/bridge.d.ts +19 -7
- package/dist/bridge.js +898 -629
- package/dist/bridge.js.map +1 -1
- package/dist/cc-process.js +11 -0
- package/dist/cc-process.js.map +1 -1
- package/dist/cc-protocol.d.ts +11 -1
- package/dist/cc-protocol.js.map +1 -1
- package/dist/cli.js +0 -0
- package/dist/ctl-server.d.ts +4 -0
- package/dist/ctl-server.js +30 -4
- package/dist/ctl-server.js.map +1 -1
- package/dist/event-buffer.d.ts +27 -0
- package/dist/event-buffer.js +50 -0
- package/dist/event-buffer.js.map +1 -0
- package/dist/high-signal.d.ts +53 -0
- package/dist/high-signal.js +391 -0
- package/dist/high-signal.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-bridge.d.ts +3 -5
- package/dist/mcp-server.js +80 -0
- package/dist/mcp-server.js.map +1 -1
- package/dist/session.d.ts +13 -8
- package/dist/session.js +63 -39
- package/dist/session.js.map +1 -1
- package/dist/streaming.d.ts +30 -8
- package/dist/streaming.js +270 -118
- package/dist/streaming.js.map +1 -1
- package/dist/telegram.d.ts +3 -1
- package/dist/telegram.js +19 -1
- package/dist/telegram.js.map +1 -1
- package/package.json +1 -1
- package/dist/telegram-html.d.ts +0 -5
- package/dist/telegram-html.js +0 -126
- package/dist/telegram-html.js.map +0 -1
package/dist/streaming.js
CHANGED
|
@@ -10,6 +10,12 @@ export function escapeHtml(text) {
|
|
|
10
10
|
.replace(/</g, '<')
|
|
11
11
|
.replace(/>/g, '>');
|
|
12
12
|
}
|
|
13
|
+
/** Create visually distinct system message with enhanced styling */
|
|
14
|
+
export function formatSystemMessage(type, content, expandable = false) {
|
|
15
|
+
const emoji = { thinking: 'π', tool: 'β‘', usage: 'π', error: 'β οΈ', status: 'βΉοΈ' }[type];
|
|
16
|
+
const wrapper = expandable ? 'blockquote expandable' : 'blockquote';
|
|
17
|
+
return `<${wrapper}>${emoji} ${content}</${wrapper.split(' ')[0]}>`;
|
|
18
|
+
}
|
|
13
19
|
/**
|
|
14
20
|
* Convert markdown text to Telegram-safe HTML using the marked library.
|
|
15
21
|
* Replaces the old hand-rolled implementation with a proper markdown parser.
|
|
@@ -45,33 +51,59 @@ export class StreamAccumulator {
|
|
|
45
51
|
lastEditTime = 0;
|
|
46
52
|
editTimer = null;
|
|
47
53
|
thinkingIndicatorShown = false;
|
|
54
|
+
thinkingMessageId = null; // preserved separately from tgMessageId
|
|
48
55
|
messageIds = []; // all message IDs sent during this turn
|
|
49
56
|
finished = false;
|
|
50
57
|
sendQueue = Promise.resolve();
|
|
51
58
|
turnUsage = null;
|
|
52
|
-
|
|
59
|
+
/** Usage from the most recent message_start event β represents a single API call's context (not cumulative). */
|
|
60
|
+
_lastMsgStartCtx = null;
|
|
61
|
+
// Per-tool-use consolidated indicator message (persists across resets)
|
|
53
62
|
toolMessages = new Map();
|
|
54
63
|
toolInputBuffers = new Map(); // tool block ID β accumulated input JSON
|
|
55
64
|
currentToolBlockId = null;
|
|
65
|
+
consolidatedToolMsgId = null; // shared TG message for all tool indicators
|
|
56
66
|
constructor(options) {
|
|
57
67
|
this.chatId = options.chatId;
|
|
58
68
|
this.sender = options.sender;
|
|
59
69
|
this.editIntervalMs = options.editIntervalMs ?? 1000;
|
|
60
|
-
this.splitThreshold = options.splitThreshold ??
|
|
70
|
+
this.splitThreshold = options.splitThreshold ?? 3500;
|
|
61
71
|
this.logger = options.logger;
|
|
62
72
|
this.onError = options.onError;
|
|
63
73
|
}
|
|
64
74
|
get allMessageIds() { return [...this.messageIds]; }
|
|
65
75
|
/** Set usage stats for the current turn (called from bridge on result event) */
|
|
66
76
|
setTurnUsage(usage) {
|
|
67
|
-
|
|
77
|
+
// Merge in per-API-call ctx tokens if we captured them from message_start events.
|
|
78
|
+
// These are bounded by the context window (unlike result event usage which accumulates across tool loops).
|
|
79
|
+
if (this._lastMsgStartCtx) {
|
|
80
|
+
this.turnUsage = {
|
|
81
|
+
...usage,
|
|
82
|
+
ctxInputTokens: this._lastMsgStartCtx.input,
|
|
83
|
+
ctxCacheReadTokens: this._lastMsgStartCtx.cacheRead,
|
|
84
|
+
ctxCacheCreationTokens: this._lastMsgStartCtx.cacheCreation,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
this.turnUsage = usage;
|
|
89
|
+
}
|
|
68
90
|
}
|
|
69
91
|
// ββ Process stream events ββ
|
|
70
92
|
async handleEvent(event) {
|
|
71
93
|
switch (event.type) {
|
|
72
|
-
case 'message_start':
|
|
94
|
+
case 'message_start': {
|
|
73
95
|
// Bridge handles reset decision - no automatic reset here
|
|
96
|
+
// Capture per-API-call token counts for accurate context % (not cumulative like result event)
|
|
97
|
+
const msUsage = event.message?.usage;
|
|
98
|
+
if (msUsage) {
|
|
99
|
+
this._lastMsgStartCtx = {
|
|
100
|
+
input: msUsage.input_tokens ?? 0,
|
|
101
|
+
cacheRead: msUsage.cache_read_input_tokens ?? 0,
|
|
102
|
+
cacheCreation: msUsage.cache_creation_input_tokens ?? 0,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
74
105
|
break;
|
|
106
|
+
}
|
|
75
107
|
case 'content_block_start':
|
|
76
108
|
await this.onContentBlockStart(event);
|
|
77
109
|
break;
|
|
@@ -98,23 +130,44 @@ export class StreamAccumulator {
|
|
|
98
130
|
if (blockType === 'thinking') {
|
|
99
131
|
this.currentBlockType = 'thinking';
|
|
100
132
|
if (!this.thinkingIndicatorShown && !this.buffer) {
|
|
101
|
-
await this.sendOrEdit('
|
|
133
|
+
await this.sendOrEdit(formatSystemMessage('thinking', 'Processing...'), true);
|
|
102
134
|
this.thinkingIndicatorShown = true;
|
|
103
135
|
}
|
|
104
136
|
}
|
|
105
137
|
else if (blockType === 'text') {
|
|
106
138
|
this.currentBlockType = 'text';
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
139
|
+
// If thinking indicator was shown, update it with actual thinking content NOW
|
|
140
|
+
// (before text starts streaming below) β don't wait for finalize()
|
|
141
|
+
if (this.thinkingIndicatorShown && this.tgMessageId) {
|
|
142
|
+
this.thinkingMessageId = this.tgMessageId;
|
|
143
|
+
this.tgMessageId = null; // force new message for response text
|
|
144
|
+
// Flush "Processing..." β actual thinking content immediately
|
|
145
|
+
if (this.thinkingBuffer && this.thinkingMessageId) {
|
|
146
|
+
const thinkingPreview = this.thinkingBuffer.length > 1024
|
|
147
|
+
? this.thinkingBuffer.slice(0, 1024) + 'β¦'
|
|
148
|
+
: this.thinkingBuffer;
|
|
149
|
+
const html = formatSystemMessage('thinking', markdownToTelegramHtml(thinkingPreview), true);
|
|
150
|
+
this.sendQueue = this.sendQueue.then(async () => {
|
|
151
|
+
try {
|
|
152
|
+
await this.sender.editMessage(this.chatId, this.thinkingMessageId, html, 'HTML');
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// Non-critical β thinking message update
|
|
156
|
+
}
|
|
157
|
+
}).catch(err => {
|
|
158
|
+
this.logger?.error?.({ err }, 'Failed to flush thinking indicator');
|
|
159
|
+
});
|
|
160
|
+
}
|
|
110
161
|
}
|
|
111
162
|
}
|
|
112
163
|
else if (blockType === 'tool_use') {
|
|
113
164
|
this.currentBlockType = 'tool_use';
|
|
114
165
|
const block = event.content_block;
|
|
115
166
|
this.currentToolBlockId = block.id;
|
|
116
|
-
//
|
|
117
|
-
|
|
167
|
+
// Sub-agent tools are handled by SubAgentTracker β skip duplicate indicator
|
|
168
|
+
if (!isSubAgentTool(block.name)) {
|
|
169
|
+
await this.sendToolIndicator(block.id, block.name);
|
|
170
|
+
}
|
|
118
171
|
}
|
|
119
172
|
else if (blockType === 'image') {
|
|
120
173
|
this.currentBlockType = 'image';
|
|
@@ -214,23 +267,74 @@ export class StreamAccumulator {
|
|
|
214
267
|
}
|
|
215
268
|
this.imageBase64Buffer = '';
|
|
216
269
|
}
|
|
217
|
-
/** Send
|
|
270
|
+
/** Send or update the consolidated tool indicator message. */
|
|
218
271
|
async sendToolIndicator(blockId, toolName) {
|
|
219
272
|
const startTime = Date.now();
|
|
273
|
+
if (this.consolidatedToolMsgId) {
|
|
274
|
+
// Reuse existing consolidated message
|
|
275
|
+
this.toolMessages.set(blockId, { msgId: this.consolidatedToolMsgId, toolName, startTime, resolved: false, isError: false, elapsed: null });
|
|
276
|
+
this.updateConsolidatedToolMessage();
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
// Create the first consolidated tool message
|
|
280
|
+
this.sendQueue = this.sendQueue.then(async () => {
|
|
281
|
+
try {
|
|
282
|
+
const html = this.buildConsolidatedToolHtml(blockId, toolName);
|
|
283
|
+
const msgId = await this.sender.sendMessage(this.chatId, html, 'HTML');
|
|
284
|
+
this.consolidatedToolMsgId = msgId;
|
|
285
|
+
this.toolMessages.set(blockId, { msgId, toolName, startTime, resolved: false, isError: false, elapsed: null });
|
|
286
|
+
}
|
|
287
|
+
catch (err) {
|
|
288
|
+
this.logger?.debug?.({ err, toolName }, 'Failed to send tool indicator');
|
|
289
|
+
}
|
|
290
|
+
}).catch(err => {
|
|
291
|
+
this.logger?.error?.({ err }, 'sendToolIndicator queue error');
|
|
292
|
+
});
|
|
293
|
+
return this.sendQueue;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/** Build HTML for the consolidated tool indicator message. */
|
|
297
|
+
buildConsolidatedToolHtml(pendingBlockId, pendingToolName) {
|
|
298
|
+
const lines = [];
|
|
299
|
+
for (const [blockId, entry] of this.toolMessages) {
|
|
300
|
+
const summary = this.toolIndicatorLastSummary.get(blockId);
|
|
301
|
+
const codePart = summary ? ` Β· <code>${escapeHtml(summary)}</code>` : '';
|
|
302
|
+
if (entry.resolved) {
|
|
303
|
+
const statLine = this.toolResolvedStats.get(blockId);
|
|
304
|
+
const statPart = statLine ? ` Β· ${escapeHtml(statLine)}` : '';
|
|
305
|
+
const icon = entry.isError ? 'β' : 'β
';
|
|
306
|
+
lines.push(`${icon} ${escapeHtml(entry.toolName)} (${entry.elapsed})${codePart}${statPart}`);
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
lines.push(`β‘ ${escapeHtml(entry.toolName)}β¦${codePart}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// Include pending tool that hasn't been added to toolMessages yet
|
|
313
|
+
if (pendingBlockId && pendingToolName && !this.toolMessages.has(pendingBlockId)) {
|
|
314
|
+
lines.push(`β‘ ${escapeHtml(pendingToolName)}β¦`);
|
|
315
|
+
}
|
|
316
|
+
return `<blockquote expandable>${lines.join('\n')}</blockquote>`;
|
|
317
|
+
}
|
|
318
|
+
/** Resolved tool result stats, keyed by blockId */
|
|
319
|
+
toolResolvedStats = new Map();
|
|
320
|
+
/** Edit the consolidated tool message with current state of all tools. */
|
|
321
|
+
updateConsolidatedToolMessage() {
|
|
322
|
+
if (!this.consolidatedToolMsgId)
|
|
323
|
+
return;
|
|
324
|
+
const msgId = this.consolidatedToolMsgId;
|
|
325
|
+
const html = this.buildConsolidatedToolHtml();
|
|
220
326
|
this.sendQueue = this.sendQueue.then(async () => {
|
|
221
327
|
try {
|
|
222
|
-
|
|
223
|
-
const msgId = await this.sender.sendMessage(this.chatId, html, 'HTML');
|
|
224
|
-
this.toolMessages.set(blockId, { msgId, toolName, startTime });
|
|
328
|
+
await this.sender.editMessage(this.chatId, msgId, html, 'HTML');
|
|
225
329
|
}
|
|
226
330
|
catch (err) {
|
|
227
|
-
|
|
228
|
-
|
|
331
|
+
if (err instanceof Error && err.message.includes('message is not modified'))
|
|
332
|
+
return;
|
|
333
|
+
this.logger?.debug?.({ err }, 'Failed to update consolidated tool message');
|
|
229
334
|
}
|
|
230
335
|
}).catch(err => {
|
|
231
|
-
this.logger?.error?.({ err }, '
|
|
336
|
+
this.logger?.error?.({ err }, 'updateConsolidatedToolMessage queue error');
|
|
232
337
|
});
|
|
233
|
-
return this.sendQueue;
|
|
234
338
|
}
|
|
235
339
|
/** Update a tool indicator message with input preview once the JSON value is complete. */
|
|
236
340
|
toolIndicatorLastSummary = new Map(); // blockId β last rendered summary
|
|
@@ -248,105 +352,118 @@ export class StreamAccumulator {
|
|
|
248
352
|
if (this.toolIndicatorLastSummary.get(blockId) === summary)
|
|
249
353
|
return;
|
|
250
354
|
this.toolIndicatorLastSummary.set(blockId, summary);
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
this.sendQueue = this.sendQueue.then(async () => {
|
|
254
|
-
try {
|
|
255
|
-
await this.sender.editMessage(this.chatId, entry.msgId, html, 'HTML');
|
|
256
|
-
}
|
|
257
|
-
catch (err) {
|
|
258
|
-
// "message is not modified" or other edit failure β non-critical
|
|
259
|
-
this.logger?.debug?.({ err }, 'Failed to update tool indicator with input');
|
|
260
|
-
}
|
|
261
|
-
}).catch(err => {
|
|
262
|
-
this.logger?.error?.({ err }, 'updateToolIndicatorWithInput queue error');
|
|
263
|
-
});
|
|
264
|
-
return this.sendQueue;
|
|
355
|
+
// Update the consolidated tool message
|
|
356
|
+
this.updateConsolidatedToolMessage();
|
|
265
357
|
}
|
|
266
|
-
/**
|
|
358
|
+
/** MCP media tools β on success, delete indicator (the media was sent directly). On failure, keep + react β. */
|
|
359
|
+
static MCP_MEDIA_TOOLS = new Set(['mcp__tgcc__send_image', 'mcp__tgcc__send_file', 'mcp__tgcc__send_voice']);
|
|
360
|
+
/** Resolve a tool indicator with success/failure status. Updates the consolidated message. */
|
|
267
361
|
async resolveToolMessage(blockId, isError, errorMessage, resultContent, toolUseResult) {
|
|
268
362
|
const entry = this.toolMessages.get(blockId);
|
|
269
363
|
if (!entry)
|
|
270
364
|
return;
|
|
271
|
-
const {
|
|
365
|
+
const { toolName, startTime } = entry;
|
|
272
366
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
367
|
+
// MCP media tools: remove from consolidated on success (the media itself is the result)
|
|
368
|
+
if (StreamAccumulator.MCP_MEDIA_TOOLS.has(toolName) && !isError) {
|
|
369
|
+
this.toolInputBuffers.delete(blockId);
|
|
370
|
+
this.toolIndicatorLastSummary.delete(blockId);
|
|
371
|
+
this.toolResolvedStats.delete(blockId);
|
|
372
|
+
this.toolMessages.delete(blockId);
|
|
373
|
+
// If that was the only tool, delete the consolidated message
|
|
374
|
+
if (this.toolMessages.size === 0 && this.consolidatedToolMsgId) {
|
|
375
|
+
const msgId = this.consolidatedToolMsgId;
|
|
376
|
+
this.consolidatedToolMsgId = null;
|
|
377
|
+
this.sendQueue = this.sendQueue.then(async () => {
|
|
378
|
+
try {
|
|
379
|
+
if (this.sender.deleteMessage)
|
|
380
|
+
await this.sender.deleteMessage(this.chatId, msgId);
|
|
381
|
+
}
|
|
382
|
+
catch (err) {
|
|
383
|
+
this.logger?.debug?.({ err, toolName }, 'Failed to delete consolidated tool message');
|
|
384
|
+
}
|
|
385
|
+
}).catch(err => {
|
|
386
|
+
this.logger?.error?.({ err }, 'resolveToolMessage queue error');
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
this.updateConsolidatedToolMessage();
|
|
391
|
+
}
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
// Compute final input summary if we don't have one yet
|
|
273
395
|
const inputJson = this.toolInputBuffers.get(blockId) ?? '';
|
|
274
|
-
|
|
396
|
+
if (!this.toolIndicatorLastSummary.has(blockId)) {
|
|
397
|
+
const summary = extractToolInputSummary(toolName, inputJson);
|
|
398
|
+
if (summary)
|
|
399
|
+
this.toolIndicatorLastSummary.set(blockId, summary);
|
|
400
|
+
}
|
|
401
|
+
// Store result stat for display
|
|
275
402
|
const resultStat = extractToolResultStat(toolName, resultContent, toolUseResult);
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
403
|
+
if (resultStat)
|
|
404
|
+
this.toolResolvedStats.set(blockId, resultStat);
|
|
405
|
+
// Mark as resolved
|
|
406
|
+
entry.resolved = true;
|
|
407
|
+
entry.isError = isError;
|
|
408
|
+
entry.elapsed = elapsed + 's';
|
|
280
409
|
// Clean up input buffer
|
|
281
410
|
this.toolInputBuffers.delete(blockId);
|
|
282
|
-
|
|
283
|
-
this.
|
|
284
|
-
try {
|
|
285
|
-
await this.sender.editMessage(this.chatId, msgId, html, 'HTML');
|
|
286
|
-
}
|
|
287
|
-
catch (err) {
|
|
288
|
-
// Edit failure on resolve β non-critical
|
|
289
|
-
this.logger?.debug?.({ err, toolName }, 'Failed to resolve tool indicator');
|
|
290
|
-
}
|
|
291
|
-
}).catch(err => {
|
|
292
|
-
this.logger?.error?.({ err }, 'resolveToolMessage queue error');
|
|
293
|
-
});
|
|
294
|
-
return this.sendQueue;
|
|
411
|
+
// Update the consolidated message
|
|
412
|
+
this.updateConsolidatedToolMessage();
|
|
295
413
|
}
|
|
296
|
-
/** Edit a specific tool indicator message by block ID. */
|
|
297
|
-
async editToolMessage(blockId,
|
|
298
|
-
|
|
299
|
-
if (!entry)
|
|
414
|
+
/** Edit a specific tool indicator message by block ID (updates the consolidated message). */
|
|
415
|
+
async editToolMessage(blockId, _html) {
|
|
416
|
+
if (!this.toolMessages.has(blockId))
|
|
300
417
|
return;
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
await this.sender.editMessage(this.chatId, entry.msgId, html, 'HTML');
|
|
304
|
-
}
|
|
305
|
-
catch (err) {
|
|
306
|
-
// Edit failure β non-critical
|
|
307
|
-
this.logger?.debug?.({ err }, 'Failed to edit tool message');
|
|
308
|
-
}
|
|
309
|
-
}).catch(err => {
|
|
310
|
-
this.logger?.error?.({ err }, 'editToolMessage queue error');
|
|
311
|
-
});
|
|
312
|
-
return this.sendQueue;
|
|
418
|
+
// With consolidation, just rebuild the consolidated message
|
|
419
|
+
this.updateConsolidatedToolMessage();
|
|
313
420
|
}
|
|
314
|
-
/** Delete a specific tool
|
|
421
|
+
/** Delete a specific tool from the consolidated indicator. */
|
|
315
422
|
async deleteToolMessage(blockId) {
|
|
316
|
-
|
|
317
|
-
if (!entry)
|
|
423
|
+
if (!this.toolMessages.has(blockId))
|
|
318
424
|
return;
|
|
319
425
|
this.toolMessages.delete(blockId);
|
|
320
|
-
this.
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
426
|
+
this.toolInputBuffers.delete(blockId);
|
|
427
|
+
this.toolIndicatorLastSummary.delete(blockId);
|
|
428
|
+
this.toolResolvedStats.delete(blockId);
|
|
429
|
+
if (this.toolMessages.size === 0 && this.consolidatedToolMsgId) {
|
|
430
|
+
// Last tool removed β delete the consolidated message
|
|
431
|
+
const msgId = this.consolidatedToolMsgId;
|
|
432
|
+
this.consolidatedToolMsgId = null;
|
|
433
|
+
this.sendQueue = this.sendQueue.then(async () => {
|
|
434
|
+
try {
|
|
435
|
+
if (this.sender.deleteMessage)
|
|
436
|
+
await this.sender.deleteMessage(this.chatId, msgId);
|
|
324
437
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
438
|
+
catch (err) {
|
|
439
|
+
this.logger?.debug?.({ err }, 'Failed to delete consolidated tool message');
|
|
440
|
+
}
|
|
441
|
+
}).catch(err => {
|
|
442
|
+
this.logger?.error?.({ err }, 'deleteToolMessage queue error');
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
this.updateConsolidatedToolMessage();
|
|
447
|
+
}
|
|
334
448
|
}
|
|
335
|
-
/** Delete
|
|
449
|
+
/** Delete the consolidated tool indicator message. */
|
|
336
450
|
async deleteAllToolMessages() {
|
|
337
|
-
const ids = [...this.toolMessages.values()];
|
|
338
451
|
this.toolMessages.clear();
|
|
339
|
-
|
|
452
|
+
this.toolInputBuffers.clear();
|
|
453
|
+
this.toolIndicatorLastSummary.clear();
|
|
454
|
+
this.toolResolvedStats.clear();
|
|
455
|
+
if (!this.consolidatedToolMsgId || !this.sender.deleteMessage) {
|
|
456
|
+
this.consolidatedToolMsgId = null;
|
|
340
457
|
return;
|
|
458
|
+
}
|
|
459
|
+
const msgId = this.consolidatedToolMsgId;
|
|
460
|
+
this.consolidatedToolMsgId = null;
|
|
341
461
|
this.sendQueue = this.sendQueue.then(async () => {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
// Delete failure β non-critical
|
|
348
|
-
this.logger?.debug?.({ err }, 'Failed to delete tool message in batch');
|
|
349
|
-
}
|
|
462
|
+
try {
|
|
463
|
+
await this.sender.deleteMessage(this.chatId, msgId);
|
|
464
|
+
}
|
|
465
|
+
catch (err) {
|
|
466
|
+
this.logger?.debug?.({ err }, 'Failed to delete consolidated tool message');
|
|
350
467
|
}
|
|
351
468
|
}).catch(err => {
|
|
352
469
|
this.logger?.error?.({ err }, 'deleteAllToolMessages queue error');
|
|
@@ -377,16 +494,19 @@ export class StreamAccumulator {
|
|
|
377
494
|
*/
|
|
378
495
|
buildFullText(includeSuffix = false) {
|
|
379
496
|
let text = '';
|
|
380
|
-
|
|
497
|
+
// Thinking goes in its own message (thinkingMessageId), not prepended to response
|
|
498
|
+
if (this.thinkingBuffer && !this.thinkingMessageId) {
|
|
499
|
+
// Only prepend if thinking didn't get its own message (edge case: no indicator was shown)
|
|
381
500
|
const thinkingPreview = this.thinkingBuffer.length > 1024
|
|
382
501
|
? this.thinkingBuffer.slice(0, 1024) + 'β¦'
|
|
383
502
|
: this.thinkingBuffer;
|
|
384
|
-
text +=
|
|
503
|
+
text += formatSystemMessage('thinking', markdownToTelegramHtml(thinkingPreview), true) + '\n';
|
|
385
504
|
}
|
|
386
505
|
// Convert markdown buffer to HTML-safe text
|
|
387
506
|
text += makeHtmlSafe(this.buffer);
|
|
388
507
|
if (includeSuffix && this.turnUsage) {
|
|
389
|
-
|
|
508
|
+
const usageContent = formatUsageFooter(this.turnUsage, this.turnUsage.model).replace(/<\/?i>/g, '');
|
|
509
|
+
text += '\n' + formatSystemMessage('usage', usageContent);
|
|
390
510
|
}
|
|
391
511
|
return { text, hasHtmlSuffix: includeSuffix && !!this.turnUsage };
|
|
392
512
|
}
|
|
@@ -443,17 +563,29 @@ export class StreamAccumulator {
|
|
|
443
563
|
clearTimeout(this.editTimer);
|
|
444
564
|
this.editTimer = null;
|
|
445
565
|
}
|
|
566
|
+
// Update thinking message with final content (if it has its own message)
|
|
567
|
+
if (this.thinkingBuffer && this.thinkingMessageId) {
|
|
568
|
+
const thinkingPreview = this.thinkingBuffer.length > 1024
|
|
569
|
+
? this.thinkingBuffer.slice(0, 1024) + 'β¦'
|
|
570
|
+
: this.thinkingBuffer;
|
|
571
|
+
try {
|
|
572
|
+
await this.sender.editMessage(this.chatId, this.thinkingMessageId, formatSystemMessage('thinking', markdownToTelegramHtml(thinkingPreview), true), 'HTML');
|
|
573
|
+
}
|
|
574
|
+
catch {
|
|
575
|
+
// Thinking message edit failed β not critical
|
|
576
|
+
}
|
|
577
|
+
}
|
|
446
578
|
if (this.buffer) {
|
|
447
|
-
// Final edit with complete text
|
|
579
|
+
// Final edit with complete text (thinking is in its own message)
|
|
448
580
|
const { text } = this.buildFullText(true);
|
|
449
581
|
await this.sendOrEdit(text, true); // buildFullText already does makeHtmlSafe
|
|
450
582
|
}
|
|
451
|
-
else if (this.thinkingBuffer && this.thinkingIndicatorShown) {
|
|
452
|
-
// Only thinking happened, no text β show
|
|
583
|
+
else if (this.thinkingBuffer && !this.thinkingMessageId && this.thinkingIndicatorShown) {
|
|
584
|
+
// Only thinking happened, no text, no separate message β show as expandable blockquote
|
|
453
585
|
const thinkingPreview = this.thinkingBuffer.length > 1024
|
|
454
586
|
? this.thinkingBuffer.slice(0, 1024) + 'β¦'
|
|
455
587
|
: this.thinkingBuffer;
|
|
456
|
-
await this.sendOrEdit(
|
|
588
|
+
await this.sendOrEdit(formatSystemMessage('thinking', markdownToTelegramHtml(thinkingPreview), true), true);
|
|
457
589
|
}
|
|
458
590
|
}
|
|
459
591
|
clearEditTimer() {
|
|
@@ -472,18 +604,26 @@ export class StreamAccumulator {
|
|
|
472
604
|
this.currentToolBlockId = null;
|
|
473
605
|
this.lastEditTime = 0;
|
|
474
606
|
this.thinkingIndicatorShown = false;
|
|
607
|
+
this.thinkingMessageId = null;
|
|
475
608
|
this.finished = false;
|
|
476
609
|
this.turnUsage = null;
|
|
610
|
+
this._lastMsgStartCtx = null;
|
|
477
611
|
this.clearEditTimer();
|
|
478
612
|
}
|
|
479
613
|
/** Full reset: also clears tgMessageId (next send creates a new message).
|
|
480
614
|
* Chains on the existing sendQueue so any pending finalize() edits complete first.
|
|
481
|
-
*
|
|
615
|
+
* Consolidated tool message resets so next turn starts a fresh batch. */
|
|
482
616
|
reset() {
|
|
483
617
|
const prevQueue = this.sendQueue;
|
|
484
618
|
this.softReset();
|
|
485
619
|
this.tgMessageId = null;
|
|
486
620
|
this.messageIds = [];
|
|
621
|
+
// Reset tool consolidation for next turn
|
|
622
|
+
this.consolidatedToolMsgId = null;
|
|
623
|
+
this.toolMessages.clear();
|
|
624
|
+
this.toolInputBuffers.clear();
|
|
625
|
+
this.toolIndicatorLastSummary.clear();
|
|
626
|
+
this.toolResolvedStats.clear();
|
|
487
627
|
this.sendQueue = prevQueue.catch(() => { }); // swallow errors from prev turn
|
|
488
628
|
}
|
|
489
629
|
}
|
|
@@ -691,17 +831,24 @@ function formatTokens(n) {
|
|
|
691
831
|
return String(n);
|
|
692
832
|
}
|
|
693
833
|
/** Format usage stats as an HTML italic footer line */
|
|
694
|
-
export function formatUsageFooter(usage) {
|
|
695
|
-
|
|
696
|
-
|
|
834
|
+
export function formatUsageFooter(usage, _model) {
|
|
835
|
+
// Use per-API-call ctx tokens (from message_start) for context % β these are bounded by the
|
|
836
|
+
// context window. Fall back to cumulative result event tokens only if ctx tokens unavailable.
|
|
837
|
+
const ctxInput = usage.ctxInputTokens ?? usage.inputTokens;
|
|
838
|
+
const ctxRead = usage.ctxCacheReadTokens ?? usage.cacheReadTokens;
|
|
839
|
+
const ctxCreation = usage.ctxCacheCreationTokens ?? usage.cacheCreationTokens;
|
|
840
|
+
const totalCtx = ctxInput + ctxRead + ctxCreation;
|
|
841
|
+
const CONTEXT_WINDOW = 200_000;
|
|
842
|
+
const ctxPct = Math.round(totalCtx / CONTEXT_WINDOW * 100);
|
|
843
|
+
const overLimit = ctxPct > 90;
|
|
697
844
|
const parts = [
|
|
698
|
-
|
|
845
|
+
`${formatTokens(usage.inputTokens)} in`,
|
|
699
846
|
`${formatTokens(usage.outputTokens)} out`,
|
|
700
847
|
];
|
|
701
848
|
if (usage.costUsd != null) {
|
|
702
849
|
parts.push(`$${usage.costUsd.toFixed(4)}`);
|
|
703
850
|
}
|
|
704
|
-
parts.push(`${ctxPct}%`);
|
|
851
|
+
parts.push(overLimit ? `β οΈ ${ctxPct}%` : `${ctxPct}%`);
|
|
705
852
|
return `<i>${parts.join(' Β· ')}</i>`;
|
|
706
853
|
}
|
|
707
854
|
// ββ Sub-agent detection patterns ββ
|
|
@@ -846,10 +993,12 @@ export class SubAgentTracker {
|
|
|
846
993
|
continue;
|
|
847
994
|
info.status = 'completed';
|
|
848
995
|
const label = info.label || info.toolName;
|
|
849
|
-
const text =
|
|
996
|
+
const text = `<blockquote>π€ ${escapeHtml(label)} β see main message</blockquote>`;
|
|
997
|
+
const agentMsgId = info.tgMessageId;
|
|
850
998
|
this.sendQueue = this.sendQueue.then(async () => {
|
|
851
999
|
try {
|
|
852
|
-
await this.sender.editMessage(this.chatId,
|
|
1000
|
+
await this.sender.editMessage(this.chatId, agentMsgId, text, 'HTML');
|
|
1001
|
+
await this.sender.setReaction?.(this.chatId, agentMsgId, 'π');
|
|
853
1002
|
}
|
|
854
1003
|
catch (err) {
|
|
855
1004
|
// Non-critical β edit failure on dispatched agent status
|
|
@@ -924,14 +1073,17 @@ export class SubAgentTracker {
|
|
|
924
1073
|
return;
|
|
925
1074
|
info.status = 'completed';
|
|
926
1075
|
const label = info.label || info.toolName;
|
|
927
|
-
// Truncate result for blockquote (TG message limit ~4096 chars)
|
|
928
1076
|
const maxResultLen = 3500;
|
|
929
1077
|
const resultText = result.length > maxResultLen ? result.slice(0, maxResultLen) + 'β¦' : result;
|
|
930
|
-
//
|
|
931
|
-
const text = `<blockquote
|
|
1078
|
+
// Blockquote header + expandable result (not nested β TG doesn't support nested blockquotes)
|
|
1079
|
+
const text = `<blockquote>π€ ${escapeHtml(label)}</blockquote>\n<blockquote expandable>${escapeHtml(resultText)}</blockquote>`;
|
|
1080
|
+
const msgId = info.tgMessageId;
|
|
932
1081
|
this.sendQueue = this.sendQueue.then(async () => {
|
|
933
1082
|
try {
|
|
934
|
-
await this.sender.editMessage(this.chatId,
|
|
1083
|
+
await this.sender.editMessage(this.chatId, msgId, text, 'HTML');
|
|
1084
|
+
if (this.sender.setReaction) {
|
|
1085
|
+
await this.sender.setReaction(this.chatId, msgId, 'π');
|
|
1086
|
+
}
|
|
935
1087
|
}
|
|
936
1088
|
catch {
|
|
937
1089
|
// Edit failure on tool result β non-critical
|
|
@@ -968,7 +1120,7 @@ export class SubAgentTracker {
|
|
|
968
1120
|
else {
|
|
969
1121
|
this.sendQueue = this.sendQueue.then(async () => {
|
|
970
1122
|
try {
|
|
971
|
-
const msgId = await this.sender.sendMessage(this.chatId, '
|
|
1123
|
+
const msgId = await this.sender.sendMessage(this.chatId, '<blockquote>π€ Starting sub-agentβ¦</blockquote>', 'HTML');
|
|
972
1124
|
info.tgMessageId = msgId;
|
|
973
1125
|
this.consolidatedAgentMsgId = msgId;
|
|
974
1126
|
}
|
|
@@ -992,7 +1144,7 @@ export class SubAgentTracker {
|
|
|
992
1144
|
: 'Workingβ¦';
|
|
993
1145
|
lines.push(`π€ ${escapeHtml(label)} β ${status}`);
|
|
994
1146
|
}
|
|
995
|
-
const text = lines.join('\n')
|
|
1147
|
+
const text = `<blockquote>${lines.join('\n')}</blockquote>`;
|
|
996
1148
|
this.sendQueue = this.sendQueue.then(async () => {
|
|
997
1149
|
try {
|
|
998
1150
|
await this.sender.editMessage(this.chatId, msgId, text, 'HTML');
|
|
@@ -1232,7 +1384,7 @@ export class SubAgentTracker {
|
|
|
1232
1384
|
lines.push(`π€ ${escapeHtml(label)} β ${status}`);
|
|
1233
1385
|
}
|
|
1234
1386
|
}
|
|
1235
|
-
const text = lines.join('\n')
|
|
1387
|
+
const text = `<blockquote>${lines.join('\n')}</blockquote>`;
|
|
1236
1388
|
this.sendQueue = this.sendQueue.then(async () => {
|
|
1237
1389
|
try {
|
|
1238
1390
|
await this.sender.editMessage(this.chatId, msgId, text, 'HTML');
|
|
@@ -1263,7 +1415,7 @@ export class SubAgentTracker {
|
|
|
1263
1415
|
}
|
|
1264
1416
|
}
|
|
1265
1417
|
// ββ Utility: split a completed text into TG-sized chunks ββ
|
|
1266
|
-
export function splitText(text, maxLength =
|
|
1418
|
+
export function splitText(text, maxLength = 3500) {
|
|
1267
1419
|
if (text.length <= maxLength)
|
|
1268
1420
|
return [text];
|
|
1269
1421
|
const chunks = [];
|