@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
|
@@ -39,6 +39,7 @@ class SummaryAggregator {
|
|
|
39
39
|
onThinkingCallback = null;
|
|
40
40
|
onTokensCallback = null;
|
|
41
41
|
onCostCallback = null;
|
|
42
|
+
onSubagentCallback = null;
|
|
42
43
|
onSessionCompactedCallback = null;
|
|
43
44
|
onSessionErrorCallback = null;
|
|
44
45
|
onSessionRetryCallback = null;
|
|
@@ -54,6 +55,13 @@ class SummaryAggregator {
|
|
|
54
55
|
typingTimer = null;
|
|
55
56
|
typingIndicatorEnabled = true;
|
|
56
57
|
partHashes = new Map();
|
|
58
|
+
trackedSessionParents = new Map();
|
|
59
|
+
subagentStates = new Map();
|
|
60
|
+
subagentOrder = [];
|
|
61
|
+
subagentCardIdBySessionId = new Map();
|
|
62
|
+
pendingSubagentCardIdsByParent = new Map();
|
|
63
|
+
pendingChildSessionIdsByParent = new Map();
|
|
64
|
+
fallbackSubagentCardIdsByParent = new Map();
|
|
57
65
|
setBotAndChatId(bot, chatId) {
|
|
58
66
|
this.bot = bot;
|
|
59
67
|
this.chatId = chatId;
|
|
@@ -85,6 +93,9 @@ class SummaryAggregator {
|
|
|
85
93
|
setOnCost(callback) {
|
|
86
94
|
this.onCostCallback = callback;
|
|
87
95
|
}
|
|
96
|
+
setOnSubagent(callback) {
|
|
97
|
+
this.onSubagentCallback = callback;
|
|
98
|
+
}
|
|
88
99
|
setOnSessionCompacted(callback) {
|
|
89
100
|
this.onSessionCompactedCallback = callback;
|
|
90
101
|
}
|
|
@@ -150,6 +161,10 @@ class SummaryAggregator {
|
|
|
150
161
|
logger.debug(`[Aggregator] Session event: ${event.type}`, JSON.stringify(event.properties, null, 2));
|
|
151
162
|
}
|
|
152
163
|
switch (event.type) {
|
|
164
|
+
case "session.created":
|
|
165
|
+
case "session.updated":
|
|
166
|
+
this.handleSessionCreatedOrUpdated(event);
|
|
167
|
+
break;
|
|
153
168
|
case "message.updated":
|
|
154
169
|
this.handleMessageUpdated(event);
|
|
155
170
|
break;
|
|
@@ -195,6 +210,7 @@ class SummaryAggregator {
|
|
|
195
210
|
if (this.currentSessionId !== sessionId) {
|
|
196
211
|
this.clear();
|
|
197
212
|
this.currentSessionId = sessionId;
|
|
213
|
+
this.trackedSessionParents.set(sessionId, null);
|
|
198
214
|
}
|
|
199
215
|
}
|
|
200
216
|
clear() {
|
|
@@ -206,6 +222,13 @@ class SummaryAggregator {
|
|
|
206
222
|
this.knownTextPartIds.clear();
|
|
207
223
|
this.processedToolStates.clear();
|
|
208
224
|
this.thinkingFiredForMessages.clear();
|
|
225
|
+
this.trackedSessionParents.clear();
|
|
226
|
+
this.subagentStates.clear();
|
|
227
|
+
this.subagentOrder = [];
|
|
228
|
+
this.subagentCardIdBySessionId.clear();
|
|
229
|
+
this.pendingSubagentCardIdsByParent.clear();
|
|
230
|
+
this.pendingChildSessionIdsByParent.clear();
|
|
231
|
+
this.fallbackSubagentCardIdsByParent.clear();
|
|
209
232
|
this.messageCount = 0;
|
|
210
233
|
this.lastUpdated = 0;
|
|
211
234
|
if (this.onClearedCallback) {
|
|
@@ -217,8 +240,362 @@ class SummaryAggregator {
|
|
|
217
240
|
}
|
|
218
241
|
}
|
|
219
242
|
}
|
|
243
|
+
isTrackedChildSession(sessionId) {
|
|
244
|
+
return this.trackedSessionParents.has(sessionId) && sessionId !== this.currentSessionId;
|
|
245
|
+
}
|
|
246
|
+
getQueue(map, parentSessionId) {
|
|
247
|
+
const existing = map.get(parentSessionId);
|
|
248
|
+
if (existing) {
|
|
249
|
+
return existing;
|
|
250
|
+
}
|
|
251
|
+
const queue = [];
|
|
252
|
+
map.set(parentSessionId, queue);
|
|
253
|
+
return queue;
|
|
254
|
+
}
|
|
255
|
+
dequeue(map, parentSessionId) {
|
|
256
|
+
const queue = map.get(parentSessionId);
|
|
257
|
+
if (!queue || queue.length === 0) {
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
const value = queue.shift();
|
|
261
|
+
if (queue.length === 0) {
|
|
262
|
+
map.delete(parentSessionId);
|
|
263
|
+
}
|
|
264
|
+
return value;
|
|
265
|
+
}
|
|
266
|
+
removeFromQueue(map, parentSessionId, value) {
|
|
267
|
+
const queue = map.get(parentSessionId);
|
|
268
|
+
if (!queue) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const index = queue.indexOf(value);
|
|
272
|
+
if (index >= 0) {
|
|
273
|
+
queue.splice(index, 1);
|
|
274
|
+
}
|
|
275
|
+
if (queue.length === 0) {
|
|
276
|
+
map.delete(parentSessionId);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
emitSubagentState() {
|
|
280
|
+
if (!this.currentSessionId || !this.onSubagentCallback || this.subagentOrder.length === 0) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const subagents = this.subagentOrder
|
|
284
|
+
.map((cardId) => this.subagentStates.get(cardId))
|
|
285
|
+
.filter((state) => Boolean(state))
|
|
286
|
+
.map((state) => ({
|
|
287
|
+
cardId: state.cardId,
|
|
288
|
+
sessionId: state.sessionId,
|
|
289
|
+
parentSessionId: state.parentSessionId,
|
|
290
|
+
agent: state.agent,
|
|
291
|
+
description: state.description,
|
|
292
|
+
prompt: state.prompt,
|
|
293
|
+
command: state.command,
|
|
294
|
+
status: state.status,
|
|
295
|
+
providerID: state.providerID,
|
|
296
|
+
modelID: state.modelID,
|
|
297
|
+
tokens: { ...state.tokens },
|
|
298
|
+
cost: state.cost,
|
|
299
|
+
currentTool: state.currentTool,
|
|
300
|
+
currentToolInput: state.currentToolInput ? { ...state.currentToolInput } : undefined,
|
|
301
|
+
currentToolTitle: state.currentToolTitle,
|
|
302
|
+
terminalMessage: state.terminalMessage,
|
|
303
|
+
updatedAt: state.updatedAt,
|
|
304
|
+
}));
|
|
305
|
+
this.onSubagentCallback(this.currentSessionId, subagents);
|
|
306
|
+
}
|
|
307
|
+
createSubagentState(parentSessionId, sessionId, cardId = `subagent-${parentSessionId}-${Date.now()}-${this.subagentOrder.length}`) {
|
|
308
|
+
const state = {
|
|
309
|
+
cardId,
|
|
310
|
+
sessionId,
|
|
311
|
+
parentSessionId,
|
|
312
|
+
agent: "",
|
|
313
|
+
description: "",
|
|
314
|
+
prompt: "",
|
|
315
|
+
status: "pending",
|
|
316
|
+
tokens: {
|
|
317
|
+
input: 0,
|
|
318
|
+
output: 0,
|
|
319
|
+
reasoning: 0,
|
|
320
|
+
cacheRead: 0,
|
|
321
|
+
cacheWrite: 0,
|
|
322
|
+
},
|
|
323
|
+
cost: 0,
|
|
324
|
+
terminalMessage: undefined,
|
|
325
|
+
updatedAt: Date.now(),
|
|
326
|
+
hasSubtaskMetadata: false,
|
|
327
|
+
hasTaskToolMetadata: false,
|
|
328
|
+
hasSessionTitleMetadata: false,
|
|
329
|
+
createdAt: Date.now(),
|
|
330
|
+
};
|
|
331
|
+
this.subagentStates.set(cardId, state);
|
|
332
|
+
this.subagentOrder.push(cardId);
|
|
333
|
+
if (sessionId) {
|
|
334
|
+
this.subagentCardIdBySessionId.set(sessionId, cardId);
|
|
335
|
+
}
|
|
336
|
+
return state;
|
|
337
|
+
}
|
|
338
|
+
enrichSubagentFromSubtask(state, details) {
|
|
339
|
+
state.agent = details.agent || state.agent;
|
|
340
|
+
state.description = details.description || details.prompt || state.description;
|
|
341
|
+
state.prompt = details.prompt;
|
|
342
|
+
state.command = details.command;
|
|
343
|
+
state.hasSubtaskMetadata = true;
|
|
344
|
+
state.updatedAt = Date.now();
|
|
345
|
+
}
|
|
346
|
+
enrichSubagentFromTaskTool(state, details) {
|
|
347
|
+
const nextDescription = details.description?.trim() || details.prompt?.trim();
|
|
348
|
+
if (details.agent?.trim()) {
|
|
349
|
+
state.agent = details.agent.trim();
|
|
350
|
+
}
|
|
351
|
+
if (nextDescription) {
|
|
352
|
+
state.description = nextDescription;
|
|
353
|
+
}
|
|
354
|
+
if (details.prompt?.trim()) {
|
|
355
|
+
state.prompt = details.prompt.trim();
|
|
356
|
+
}
|
|
357
|
+
if (details.command?.trim()) {
|
|
358
|
+
state.command = details.command.trim();
|
|
359
|
+
}
|
|
360
|
+
state.hasTaskToolMetadata = true;
|
|
361
|
+
state.updatedAt = Date.now();
|
|
362
|
+
}
|
|
363
|
+
enrichSubagentFromSessionTitle(state, title) {
|
|
364
|
+
const trimmedTitle = title?.trim();
|
|
365
|
+
if (!trimmedTitle) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const match = trimmedTitle.match(/^(.*?)(?:\s+\(@([^\s)]+)\s+subagent\))?$/i);
|
|
369
|
+
const rawDescription = match?.[1]?.trim() || trimmedTitle;
|
|
370
|
+
const rawAgent = match?.[2]?.trim();
|
|
371
|
+
if (rawDescription) {
|
|
372
|
+
state.description = rawDescription;
|
|
373
|
+
}
|
|
374
|
+
if (rawAgent) {
|
|
375
|
+
state.agent = rawAgent.replace(/^@/, "");
|
|
376
|
+
}
|
|
377
|
+
state.hasSessionTitleMetadata = true;
|
|
378
|
+
state.updatedAt = Date.now();
|
|
379
|
+
}
|
|
380
|
+
attachSessionToSubagent(cardId, sessionId) {
|
|
381
|
+
const state = this.subagentStates.get(cardId);
|
|
382
|
+
if (!state) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
state.sessionId = sessionId;
|
|
386
|
+
state.updatedAt = Date.now();
|
|
387
|
+
this.subagentCardIdBySessionId.set(sessionId, cardId);
|
|
388
|
+
this.removeFromQueue(this.pendingSubagentCardIdsByParent, state.parentSessionId, cardId);
|
|
389
|
+
}
|
|
390
|
+
findPendingSubagentWithoutSession() {
|
|
391
|
+
for (const cardId of this.subagentOrder) {
|
|
392
|
+
const state = this.subagentStates.get(cardId);
|
|
393
|
+
if (state && !state.sessionId) {
|
|
394
|
+
return state;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
attachUnknownSessionToPendingSubagent(sessionId) {
|
|
400
|
+
const pendingState = this.findPendingSubagentWithoutSession();
|
|
401
|
+
if (!pendingState) {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
this.trackedSessionParents.set(sessionId, pendingState.parentSessionId);
|
|
405
|
+
this.attachSessionToSubagent(pendingState.cardId, sessionId);
|
|
406
|
+
this.removeFromQueue(this.pendingChildSessionIdsByParent, pendingState.parentSessionId, sessionId);
|
|
407
|
+
this.emitSubagentState();
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
findNextSubagentForTaskTool(parentSessionId) {
|
|
411
|
+
for (const cardId of this.subagentOrder) {
|
|
412
|
+
const state = this.subagentStates.get(cardId);
|
|
413
|
+
if (state && state.parentSessionId === parentSessionId && !state.hasTaskToolMetadata) {
|
|
414
|
+
return state;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
updateSubagentFromTaskTool(parentSessionId, input) {
|
|
420
|
+
const subagent = this.findNextSubagentForTaskTool(parentSessionId);
|
|
421
|
+
if (!subagent || !input) {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
const description = typeof input.description === "string" ? input.description : undefined;
|
|
425
|
+
const prompt = typeof input.prompt === "string" ? input.prompt : undefined;
|
|
426
|
+
const agent = typeof input.subagent_type === "string" ? input.subagent_type : undefined;
|
|
427
|
+
const command = typeof input.command === "string" ? input.command : undefined;
|
|
428
|
+
if (!description && !prompt && !agent && !command) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
this.enrichSubagentFromTaskTool(subagent, { agent, description, prompt, command });
|
|
432
|
+
this.emitSubagentState();
|
|
433
|
+
}
|
|
434
|
+
getOrCreateSubagentForSession(sessionId) {
|
|
435
|
+
const existingCardId = this.subagentCardIdBySessionId.get(sessionId);
|
|
436
|
+
if (existingCardId) {
|
|
437
|
+
return this.subagentStates.get(existingCardId);
|
|
438
|
+
}
|
|
439
|
+
const parentSessionId = this.trackedSessionParents.get(sessionId) ?? this.currentSessionId ?? sessionId;
|
|
440
|
+
this.removeFromQueue(this.pendingChildSessionIdsByParent, parentSessionId, sessionId);
|
|
441
|
+
const state = this.createSubagentState(parentSessionId, sessionId);
|
|
442
|
+
this.getQueue(this.fallbackSubagentCardIdsByParent, parentSessionId).push(state.cardId);
|
|
443
|
+
return state;
|
|
444
|
+
}
|
|
445
|
+
registerSubtaskPart(parentSessionId, partId, agent, description, prompt, command) {
|
|
446
|
+
const fallbackCardId = this.dequeue(this.fallbackSubagentCardIdsByParent, parentSessionId);
|
|
447
|
+
if (fallbackCardId) {
|
|
448
|
+
const fallbackState = this.subagentStates.get(fallbackCardId);
|
|
449
|
+
if (fallbackState) {
|
|
450
|
+
this.enrichSubagentFromSubtask(fallbackState, { agent, description, prompt, command });
|
|
451
|
+
this.emitSubagentState();
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
const state = this.createSubagentState(parentSessionId, null, `subtask-${parentSessionId}-${partId}`);
|
|
456
|
+
this.enrichSubagentFromSubtask(state, { agent, description, prompt, command });
|
|
457
|
+
const pendingChildSessionId = this.dequeue(this.pendingChildSessionIdsByParent, parentSessionId);
|
|
458
|
+
if (pendingChildSessionId) {
|
|
459
|
+
this.attachSessionToSubagent(state.cardId, pendingChildSessionId);
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
this.getQueue(this.pendingSubagentCardIdsByParent, parentSessionId).push(state.cardId);
|
|
463
|
+
}
|
|
464
|
+
this.emitSubagentState();
|
|
465
|
+
}
|
|
466
|
+
trackChildSession(sessionId, parentSessionId) {
|
|
467
|
+
this.trackedSessionParents.set(sessionId, parentSessionId);
|
|
468
|
+
const pendingCardId = this.dequeue(this.pendingSubagentCardIdsByParent, parentSessionId);
|
|
469
|
+
if (pendingCardId) {
|
|
470
|
+
this.attachSessionToSubagent(pendingCardId, sessionId);
|
|
471
|
+
this.emitSubagentState();
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
this.getQueue(this.pendingChildSessionIdsByParent, parentSessionId).push(sessionId);
|
|
475
|
+
}
|
|
476
|
+
handleSessionCreatedOrUpdated(event) {
|
|
477
|
+
if (!this.currentSessionId) {
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
const { info } = event.properties;
|
|
481
|
+
if (!info.parentID) {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (!this.trackedSessionParents.has(info.parentID)) {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
if (info.id === this.currentSessionId) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
if (!this.trackedSessionParents.has(info.id)) {
|
|
491
|
+
this.trackChildSession(info.id, info.parentID);
|
|
492
|
+
}
|
|
493
|
+
const subagent = this.getOrCreateSubagentForSession(info.id);
|
|
494
|
+
this.enrichSubagentFromSessionTitle(subagent, info.title);
|
|
495
|
+
this.emitSubagentState();
|
|
496
|
+
}
|
|
497
|
+
updateSubagentFromAssistantMessage(info) {
|
|
498
|
+
const subagent = this.getOrCreateSubagentForSession(info.sessionID);
|
|
499
|
+
if (info.agent) {
|
|
500
|
+
subagent.agent = info.agent;
|
|
501
|
+
}
|
|
502
|
+
if (info.providerID) {
|
|
503
|
+
subagent.providerID = info.providerID;
|
|
504
|
+
}
|
|
505
|
+
if (info.modelID) {
|
|
506
|
+
subagent.modelID = info.modelID;
|
|
507
|
+
}
|
|
508
|
+
if (info.tokens) {
|
|
509
|
+
subagent.tokens = {
|
|
510
|
+
input: info.tokens.input,
|
|
511
|
+
output: info.tokens.output,
|
|
512
|
+
reasoning: info.tokens.reasoning,
|
|
513
|
+
cacheRead: info.tokens.cache?.read || 0,
|
|
514
|
+
cacheWrite: info.tokens.cache?.write || 0,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
if (typeof info.cost === "number") {
|
|
518
|
+
subagent.cost = info.cost;
|
|
519
|
+
}
|
|
520
|
+
subagent.updatedAt = Date.now();
|
|
521
|
+
this.emitSubagentState();
|
|
522
|
+
}
|
|
523
|
+
updateSubagentToolState(sessionId, state, tool, input, title) {
|
|
524
|
+
const subagent = this.getOrCreateSubagentForSession(sessionId);
|
|
525
|
+
const status = "status" in state ? state.status : undefined;
|
|
526
|
+
if (status === "running") {
|
|
527
|
+
subagent.status = "running";
|
|
528
|
+
subagent.terminalMessage = undefined;
|
|
529
|
+
}
|
|
530
|
+
if (status === "pending" && subagent.status === "pending") {
|
|
531
|
+
subagent.status = "pending";
|
|
532
|
+
subagent.terminalMessage = undefined;
|
|
533
|
+
}
|
|
534
|
+
subagent.currentTool = tool;
|
|
535
|
+
subagent.currentToolInput = input ? { ...input } : undefined;
|
|
536
|
+
subagent.currentToolTitle = title;
|
|
537
|
+
subagent.updatedAt = Date.now();
|
|
538
|
+
this.emitSubagentState();
|
|
539
|
+
}
|
|
540
|
+
updateSubagentStepStart(sessionId, snapshot) {
|
|
541
|
+
const subagent = this.getOrCreateSubagentForSession(sessionId);
|
|
542
|
+
subagent.status = "running";
|
|
543
|
+
subagent.terminalMessage = undefined;
|
|
544
|
+
subagent.currentTool = undefined;
|
|
545
|
+
subagent.currentToolInput = undefined;
|
|
546
|
+
subagent.currentToolTitle = snapshot?.trim() || subagent.currentToolTitle;
|
|
547
|
+
subagent.updatedAt = Date.now();
|
|
548
|
+
this.emitSubagentState();
|
|
549
|
+
}
|
|
550
|
+
updateSubagentStepFinish(sessionId, tokens, cost, snapshot) {
|
|
551
|
+
const subagent = this.getOrCreateSubagentForSession(sessionId);
|
|
552
|
+
subagent.status = "running";
|
|
553
|
+
subagent.terminalMessage = undefined;
|
|
554
|
+
subagent.tokens = {
|
|
555
|
+
input: tokens.input,
|
|
556
|
+
output: tokens.output,
|
|
557
|
+
reasoning: tokens.reasoning,
|
|
558
|
+
cacheRead: tokens.cache.read,
|
|
559
|
+
cacheWrite: tokens.cache.write,
|
|
560
|
+
};
|
|
561
|
+
subagent.cost += cost;
|
|
562
|
+
if (snapshot?.trim()) {
|
|
563
|
+
subagent.currentToolTitle = snapshot.trim();
|
|
564
|
+
}
|
|
565
|
+
subagent.updatedAt = Date.now();
|
|
566
|
+
this.emitSubagentState();
|
|
567
|
+
}
|
|
568
|
+
setSubagentTerminalStatus(sessionId, status, terminalMessage) {
|
|
569
|
+
const cardId = this.subagentCardIdBySessionId.get(sessionId);
|
|
570
|
+
if (!cardId) {
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
const subagent = this.subagentStates.get(cardId);
|
|
574
|
+
if (!subagent) {
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
subagent.status = status;
|
|
578
|
+
subagent.currentTool = undefined;
|
|
579
|
+
subagent.currentToolInput = undefined;
|
|
580
|
+
subagent.currentToolTitle = undefined;
|
|
581
|
+
subagent.terminalMessage = terminalMessage?.trim() || undefined;
|
|
582
|
+
subagent.updatedAt = Date.now();
|
|
583
|
+
this.emitSubagentState();
|
|
584
|
+
}
|
|
220
585
|
handleMessageUpdated(event) {
|
|
221
586
|
const { info } = event.properties;
|
|
587
|
+
if (info.sessionID !== this.currentSessionId &&
|
|
588
|
+
!this.trackedSessionParents.has(info.sessionID) &&
|
|
589
|
+
info.role === "assistant") {
|
|
590
|
+
this.attachUnknownSessionToPendingSubagent(info.sessionID);
|
|
591
|
+
}
|
|
592
|
+
if (this.isTrackedChildSession(info.sessionID)) {
|
|
593
|
+
if (info.role === "assistant") {
|
|
594
|
+
const assistantInfo = info;
|
|
595
|
+
this.updateSubagentFromAssistantMessage(assistantInfo);
|
|
596
|
+
}
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
222
599
|
if (info.sessionID !== this.currentSessionId) {
|
|
223
600
|
return;
|
|
224
601
|
}
|
|
@@ -242,23 +619,24 @@ class SummaryAggregator {
|
|
|
242
619
|
if (!isCompleted && textState.optimisticUpdateCount === 1) {
|
|
243
620
|
this.emitPartialText(info.sessionID, messageID, messageText);
|
|
244
621
|
}
|
|
622
|
+
// Extract and report tokens for EVERY message.updated with token data
|
|
623
|
+
// (both intermediate and completed). This keeps keyboard context in sync.
|
|
624
|
+
const assistantInfo = info;
|
|
625
|
+
if (this.onTokensCallback && assistantInfo.tokens) {
|
|
626
|
+
const tokens = {
|
|
627
|
+
input: assistantInfo.tokens.input,
|
|
628
|
+
output: assistantInfo.tokens.output,
|
|
629
|
+
reasoning: assistantInfo.tokens.reasoning,
|
|
630
|
+
cacheRead: assistantInfo.tokens.cache?.read || 0,
|
|
631
|
+
cacheWrite: assistantInfo.tokens.cache?.write || 0,
|
|
632
|
+
};
|
|
633
|
+
logger.debug(`[Aggregator] Tokens: input=${tokens.input}, output=${tokens.output}, reasoning=${tokens.reasoning}, cacheRead=${tokens.cacheRead}, cacheWrite=${tokens.cacheWrite}, completed=${isCompleted}`);
|
|
634
|
+
// Call synchronously so keyboardManager is updated before onComplete sends the reply
|
|
635
|
+
this.onTokensCallback(tokens, isCompleted);
|
|
636
|
+
}
|
|
245
637
|
if (isCompleted) {
|
|
246
638
|
const finalText = messageText;
|
|
247
639
|
logger.debug(`[Aggregator] Message part completed: messageId=${messageID}, textLength=${finalText.length}, totalParts=${textState.orderedPartIds.length}, session=${this.currentSessionId}`);
|
|
248
|
-
// Extract and report tokens BEFORE onComplete so keyboard context is updated
|
|
249
|
-
const assistantInfo = info;
|
|
250
|
-
if (this.onTokensCallback && assistantInfo.tokens) {
|
|
251
|
-
const tokens = {
|
|
252
|
-
input: assistantInfo.tokens.input,
|
|
253
|
-
output: assistantInfo.tokens.output,
|
|
254
|
-
reasoning: assistantInfo.tokens.reasoning,
|
|
255
|
-
cacheRead: assistantInfo.tokens.cache?.read || 0,
|
|
256
|
-
cacheWrite: assistantInfo.tokens.cache?.write || 0,
|
|
257
|
-
};
|
|
258
|
-
logger.debug(`[Aggregator] Tokens: input=${tokens.input}, output=${tokens.output}, reasoning=${tokens.reasoning}`);
|
|
259
|
-
// Call synchronously so keyboardManager is updated before onComplete sends the reply
|
|
260
|
-
this.onTokensCallback(tokens);
|
|
261
|
-
}
|
|
262
640
|
// Extract and report cost
|
|
263
641
|
if (this.onCostCallback && assistantInfo.cost !== undefined) {
|
|
264
642
|
logger.debug(`[Aggregator] Cost: $${assistantInfo.cost.toFixed(2)}`);
|
|
@@ -282,7 +660,35 @@ class SummaryAggregator {
|
|
|
282
660
|
}
|
|
283
661
|
handleMessagePartUpdated(event) {
|
|
284
662
|
const { part } = event.properties;
|
|
285
|
-
if (part.sessionID !== this.currentSessionId
|
|
663
|
+
if (part.sessionID !== this.currentSessionId &&
|
|
664
|
+
!this.trackedSessionParents.has(part.sessionID) &&
|
|
665
|
+
part.type !== "subtask") {
|
|
666
|
+
this.attachUnknownSessionToPendingSubagent(part.sessionID);
|
|
667
|
+
}
|
|
668
|
+
const isCurrentRootSession = part.sessionID === this.currentSessionId;
|
|
669
|
+
const isTrackedChildSession = this.isTrackedChildSession(part.sessionID);
|
|
670
|
+
if (!isCurrentRootSession && !isTrackedChildSession) {
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
if (part.type === "subtask") {
|
|
674
|
+
this.registerSubtaskPart(part.sessionID, part.id, part.agent, part.description, part.prompt, part.command);
|
|
675
|
+
this.lastUpdated = Date.now();
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
if (isTrackedChildSession) {
|
|
679
|
+
if (part.type === "tool") {
|
|
680
|
+
const state = part.state;
|
|
681
|
+
const input = "input" in state ? state.input : undefined;
|
|
682
|
+
const title = "title" in state ? state.title : undefined;
|
|
683
|
+
this.updateSubagentToolState(part.sessionID, state, part.tool, input, title);
|
|
684
|
+
}
|
|
685
|
+
if (part.type === "step-start") {
|
|
686
|
+
this.updateSubagentStepStart(part.sessionID, part.snapshot);
|
|
687
|
+
}
|
|
688
|
+
if (part.type === "step-finish") {
|
|
689
|
+
this.updateSubagentStepFinish(part.sessionID, part.tokens, part.cost, part.snapshot);
|
|
690
|
+
}
|
|
691
|
+
this.lastUpdated = Date.now();
|
|
286
692
|
return;
|
|
287
693
|
}
|
|
288
694
|
const messageID = part.messageID;
|
|
@@ -337,6 +743,9 @@ class SummaryAggregator {
|
|
|
337
743
|
const state = part.state;
|
|
338
744
|
const input = "input" in state ? state.input : undefined;
|
|
339
745
|
const title = "title" in state ? state.title : undefined;
|
|
746
|
+
if (part.tool === "task") {
|
|
747
|
+
this.updateSubagentFromTaskTool(part.sessionID, input);
|
|
748
|
+
}
|
|
340
749
|
logger.debug(`[Aggregator] Tool event: callID=${part.callID}, tool=${part.tool}, status=${"status" in state ? state.status : "unknown"}`);
|
|
341
750
|
if (part.tool === "question") {
|
|
342
751
|
logger.debug(`[Aggregator] Question tool part update:`, JSON.stringify(part, null, 2));
|
|
@@ -618,6 +1027,11 @@ class SummaryAggregator {
|
|
|
618
1027
|
}
|
|
619
1028
|
handleSessionIdle(event) {
|
|
620
1029
|
const { sessionID } = event.properties;
|
|
1030
|
+
if (this.isTrackedChildSession(sessionID)) {
|
|
1031
|
+
logger.info(`[Aggregator] Subagent session became idle: ${sessionID}`);
|
|
1032
|
+
this.setSubagentTerminalStatus(sessionID, "completed");
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
621
1035
|
if (sessionID !== this.currentSessionId) {
|
|
622
1036
|
return;
|
|
623
1037
|
}
|
|
@@ -644,10 +1058,15 @@ class SummaryAggregator {
|
|
|
644
1058
|
}
|
|
645
1059
|
handleSessionError(event) {
|
|
646
1060
|
const { sessionID, error } = event.properties;
|
|
1061
|
+
const message = error?.data?.message || error?.message || error?.name || "Unknown session error";
|
|
1062
|
+
if (sessionID && this.isTrackedChildSession(sessionID)) {
|
|
1063
|
+
logger.warn(`[Aggregator] Subagent session error: ${sessionID}: ${message}`);
|
|
1064
|
+
this.setSubagentTerminalStatus(sessionID, "error", message);
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
647
1067
|
if (sessionID !== this.currentSessionId) {
|
|
648
1068
|
return;
|
|
649
1069
|
}
|
|
650
|
-
const message = error?.data?.message || error?.message || error?.name || "Unknown session error";
|
|
651
1070
|
logger.warn(`[Aggregator] Session error: ${sessionID}: ${message}`);
|
|
652
1071
|
this.stopTypingIndicator();
|
|
653
1072
|
if (this.onSessionErrorCallback) {
|
|
@@ -5,18 +5,45 @@ import { logger } from "../utils/logger.js";
|
|
|
5
5
|
import { t } from "../i18n/index.js";
|
|
6
6
|
import { getCurrentProject } from "../settings/manager.js";
|
|
7
7
|
const TELEGRAM_MESSAGE_LIMIT = 4096;
|
|
8
|
-
|
|
8
|
+
const MARKDOWN_V2_RESERVED_CHARS = /([_\*\[\]\(\)~`>#+\-=|{}.!\\])/g;
|
|
9
|
+
function endsWithOddTrailingBackslashes(text, start, end) {
|
|
10
|
+
let backslashCount = 0;
|
|
11
|
+
for (let index = end - 1; index >= start; index--) {
|
|
12
|
+
if (text[index] !== "\\") {
|
|
13
|
+
break;
|
|
14
|
+
}
|
|
15
|
+
backslashCount += 1;
|
|
16
|
+
}
|
|
17
|
+
return backslashCount % 2 === 1;
|
|
18
|
+
}
|
|
19
|
+
function resolveSplitEndIndex(text, currentIndex, maxLength, options) {
|
|
20
|
+
const hardLimit = Math.min(text.length, currentIndex + maxLength);
|
|
21
|
+
if (hardLimit >= text.length) {
|
|
22
|
+
return text.length;
|
|
23
|
+
}
|
|
24
|
+
let endIndex = hardLimit;
|
|
25
|
+
const breakPoint = text.lastIndexOf("\n", endIndex);
|
|
26
|
+
if (breakPoint > currentIndex) {
|
|
27
|
+
endIndex = breakPoint + 1;
|
|
28
|
+
}
|
|
29
|
+
if (!options?.avoidTrailingMarkdownEscape) {
|
|
30
|
+
return endIndex;
|
|
31
|
+
}
|
|
32
|
+
while (endIndex > currentIndex && endsWithOddTrailingBackslashes(text, currentIndex, endIndex)) {
|
|
33
|
+
endIndex -= 1;
|
|
34
|
+
}
|
|
35
|
+
return endIndex > currentIndex ? endIndex : hardLimit;
|
|
36
|
+
}
|
|
37
|
+
function splitText(text, maxLength, options) {
|
|
9
38
|
const parts = [];
|
|
10
39
|
let currentIndex = 0;
|
|
11
40
|
while (currentIndex < text.length) {
|
|
12
|
-
|
|
13
|
-
if (endIndex
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (breakPoint > currentIndex) {
|
|
19
|
-
endIndex = breakPoint + 1;
|
|
41
|
+
const endIndex = resolveSplitEndIndex(text, currentIndex, maxLength, options);
|
|
42
|
+
if (endIndex <= currentIndex) {
|
|
43
|
+
const fallbackEnd = Math.min(text.length, currentIndex + 1);
|
|
44
|
+
parts.push(text.slice(currentIndex, fallbackEnd));
|
|
45
|
+
currentIndex = fallbackEnd;
|
|
46
|
+
continue;
|
|
20
47
|
}
|
|
21
48
|
parts.push(text.slice(currentIndex, endIndex));
|
|
22
49
|
currentIndex = endIndex;
|
|
@@ -134,16 +161,48 @@ export function getAssistantParseMode() {
|
|
|
134
161
|
}
|
|
135
162
|
return undefined;
|
|
136
163
|
}
|
|
164
|
+
export function escapePlainTextForTelegramMarkdownV2(text) {
|
|
165
|
+
return text.replace(MARKDOWN_V2_RESERVED_CHARS, "\\$1");
|
|
166
|
+
}
|
|
137
167
|
function formatMarkdownForTelegram(text) {
|
|
138
168
|
try {
|
|
139
169
|
const preprocessed = preprocessMarkdownForTelegram(text);
|
|
140
|
-
return convert(preprocessed, "keep");
|
|
170
|
+
return escapeMarkdownV2PipesOutsideCode(convert(preprocessed, "keep"));
|
|
141
171
|
}
|
|
142
172
|
catch (error) {
|
|
143
173
|
logger.warn("[Formatter] Failed to convert markdown summary, falling back to raw text", error);
|
|
144
174
|
return text;
|
|
145
175
|
}
|
|
146
176
|
}
|
|
177
|
+
function escapeMarkdownV2PipesOutsideCode(text) {
|
|
178
|
+
let result = "";
|
|
179
|
+
let index = 0;
|
|
180
|
+
let inInlineCode = false;
|
|
181
|
+
let inCodeFence = false;
|
|
182
|
+
while (index < text.length) {
|
|
183
|
+
if (text.startsWith("```", index)) {
|
|
184
|
+
result += "```";
|
|
185
|
+
index += 3;
|
|
186
|
+
inCodeFence = !inCodeFence;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const char = text[index];
|
|
190
|
+
if (!inCodeFence && char === "`") {
|
|
191
|
+
inInlineCode = !inInlineCode;
|
|
192
|
+
result += char;
|
|
193
|
+
index += 1;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (!inCodeFence && !inInlineCode && char === "|" && text[index - 1] !== "\\") {
|
|
197
|
+
result += "\\|";
|
|
198
|
+
index += 1;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
result += char;
|
|
202
|
+
index += 1;
|
|
203
|
+
}
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
147
206
|
export function formatSummaryWithMode(text, mode, maxLength = TELEGRAM_MESSAGE_LIMIT) {
|
|
148
207
|
if (!text || text.trim().length === 0) {
|
|
149
208
|
return [];
|
|
@@ -159,7 +218,9 @@ export function formatSummaryWithMode(text, mode, maxLength = TELEGRAM_MESSAGE_L
|
|
|
159
218
|
}
|
|
160
219
|
if (mode === "markdown") {
|
|
161
220
|
const converted = formatMarkdownForTelegram(trimmed);
|
|
162
|
-
const convertedParts = splitText(converted, normalizedMaxLength
|
|
221
|
+
const convertedParts = splitText(converted, normalizedMaxLength, {
|
|
222
|
+
avoidTrailingMarkdownEscape: true,
|
|
223
|
+
});
|
|
163
224
|
for (const convertedPart of convertedParts) {
|
|
164
225
|
const normalizedPart = convertedPart.trim();
|
|
165
226
|
if (normalizedPart) {
|
|
@@ -310,7 +371,7 @@ export function formatToolInfo(toolInfo) {
|
|
|
310
371
|
const todos = toolInfo.metadata.todos;
|
|
311
372
|
const toolIcon = getToolIcon(tool);
|
|
312
373
|
const todosList = formatTodos(todos);
|
|
313
|
-
return `${toolIcon} ${tool} (${todos.length})\n${todosList}`;
|
|
374
|
+
return `${toolIcon} ${tool} (${todos.length})\n\n${todosList}`;
|
|
314
375
|
}
|
|
315
376
|
let details = title || getToolDetails(tool, input);
|
|
316
377
|
const toolIcon = getToolIcon(tool);
|
|
@@ -360,6 +421,17 @@ export function formatToolInfo(toolInfo) {
|
|
|
360
421
|
}
|
|
361
422
|
return `${toolIcon} ${description}${tool}${detailsStr}${lineInfo}`;
|
|
362
423
|
}
|
|
424
|
+
export function formatCompactToolInfo(toolInfo, maxLength = 64, fallback = "-") {
|
|
425
|
+
const formatted = formatToolInfo(toolInfo);
|
|
426
|
+
const normalized = formatted?.replace(/\s*\n+\s*/g, " ").trim() ?? "";
|
|
427
|
+
if (!normalized) {
|
|
428
|
+
return fallback;
|
|
429
|
+
}
|
|
430
|
+
if (normalized.length <= maxLength) {
|
|
431
|
+
return normalized;
|
|
432
|
+
}
|
|
433
|
+
return `${normalized.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;
|
|
434
|
+
}
|
|
363
435
|
function countLines(text) {
|
|
364
436
|
return text.split("\n").length;
|
|
365
437
|
}
|