@grinev/opencode-telegram-bot 0.13.2 → 0.14.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 +4 -8
- package/README.md +26 -26
- package/dist/bot/commands/sessions.js +1 -0
- package/dist/bot/index.js +73 -28
- package/dist/bot/streaming/response-streamer.js +6 -4
- package/dist/bot/utils/finalize-assistant-response.js +25 -13
- package/dist/bot/utils/send-with-markdown-fallback.js +93 -4
- package/dist/bot/utils/telegram-text.js +4 -2
- package/dist/bot/utils/thinking-message.js +0 -1
- package/dist/config.js +1 -16
- package/dist/i18n/de.js +12 -0
- package/dist/i18n/en.js +12 -0
- package/dist/i18n/es.js +12 -0
- package/dist/i18n/fr.js +12 -0
- package/dist/i18n/ru.js +12 -0
- package/dist/i18n/zh.js +12 -0
- package/dist/model/context-limit.js +57 -0
- package/dist/pinned/format.js +29 -0
- package/dist/pinned/manager.js +127 -81
- package/dist/summary/aggregator.js +435 -16
- package/dist/summary/formatter.js +50 -11
- package/dist/summary/subagent-formatter.js +63 -0
- package/dist/summary/tool-message-batcher.js +14 -209
- package/dist/utils/telegram-rate-limit-retry.js +93 -0
- 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) {
|
|
@@ -6,18 +6,44 @@ 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
|
|
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) {
|
|
10
38
|
const parts = [];
|
|
11
39
|
let currentIndex = 0;
|
|
12
40
|
while (currentIndex < text.length) {
|
|
13
|
-
|
|
14
|
-
if (endIndex
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (breakPoint > currentIndex) {
|
|
20
|
-
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;
|
|
21
47
|
}
|
|
22
48
|
parts.push(text.slice(currentIndex, endIndex));
|
|
23
49
|
currentIndex = endIndex;
|
|
@@ -192,7 +218,9 @@ export function formatSummaryWithMode(text, mode, maxLength = TELEGRAM_MESSAGE_L
|
|
|
192
218
|
}
|
|
193
219
|
if (mode === "markdown") {
|
|
194
220
|
const converted = formatMarkdownForTelegram(trimmed);
|
|
195
|
-
const convertedParts = splitText(converted, normalizedMaxLength
|
|
221
|
+
const convertedParts = splitText(converted, normalizedMaxLength, {
|
|
222
|
+
avoidTrailingMarkdownEscape: true,
|
|
223
|
+
});
|
|
196
224
|
for (const convertedPart of convertedParts) {
|
|
197
225
|
const normalizedPart = convertedPart.trim();
|
|
198
226
|
if (normalizedPart) {
|
|
@@ -343,7 +371,7 @@ export function formatToolInfo(toolInfo) {
|
|
|
343
371
|
const todos = toolInfo.metadata.todos;
|
|
344
372
|
const toolIcon = getToolIcon(tool);
|
|
345
373
|
const todosList = formatTodos(todos);
|
|
346
|
-
return `${toolIcon} ${tool} (${todos.length})\n${todosList}`;
|
|
374
|
+
return `${toolIcon} ${tool} (${todos.length})\n\n${todosList}`;
|
|
347
375
|
}
|
|
348
376
|
let details = title || getToolDetails(tool, input);
|
|
349
377
|
const toolIcon = getToolIcon(tool);
|
|
@@ -393,6 +421,17 @@ export function formatToolInfo(toolInfo) {
|
|
|
393
421
|
}
|
|
394
422
|
return `${toolIcon} ${description}${tool}${detailsStr}${lineInfo}`;
|
|
395
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
|
+
}
|
|
396
435
|
function countLines(text) {
|
|
397
436
|
return text.split("\n").length;
|
|
398
437
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { formatModelDisplayName } from "../pinned/format.js";
|
|
2
|
+
import { t } from "../i18n/index.js";
|
|
3
|
+
import { formatCompactToolInfo } from "./formatter.js";
|
|
4
|
+
function formatToolStep(subagent) {
|
|
5
|
+
if (!subagent.currentTool) {
|
|
6
|
+
return "";
|
|
7
|
+
}
|
|
8
|
+
const toolInfo = {
|
|
9
|
+
sessionId: subagent.sessionId ?? subagent.parentSessionId,
|
|
10
|
+
messageId: subagent.cardId,
|
|
11
|
+
callId: subagent.cardId,
|
|
12
|
+
tool: subagent.currentTool,
|
|
13
|
+
state: {
|
|
14
|
+
status: "running",
|
|
15
|
+
input: subagent.currentToolInput ?? {},
|
|
16
|
+
title: subagent.currentToolTitle,
|
|
17
|
+
metadata: {},
|
|
18
|
+
time: { start: subagent.updatedAt },
|
|
19
|
+
},
|
|
20
|
+
input: subagent.currentToolInput,
|
|
21
|
+
title: subagent.currentToolTitle,
|
|
22
|
+
metadata: {},
|
|
23
|
+
hasFileAttachment: false,
|
|
24
|
+
};
|
|
25
|
+
const formatted = formatCompactToolInfo(toolInfo, 128, "").trim();
|
|
26
|
+
const firstSpaceIndex = formatted.indexOf(" ");
|
|
27
|
+
if (firstSpaceIndex >= 0 && formatted.slice(firstSpaceIndex + 1) === subagent.currentTool) {
|
|
28
|
+
return "";
|
|
29
|
+
}
|
|
30
|
+
return formatted;
|
|
31
|
+
}
|
|
32
|
+
function formatSubagentActivity(subagent) {
|
|
33
|
+
if (subagent.status === "completed") {
|
|
34
|
+
return `✅ ${t("subagent.completed")}`;
|
|
35
|
+
}
|
|
36
|
+
if (subagent.status === "error") {
|
|
37
|
+
const message = subagent.terminalMessage?.trim() || t("subagent.failed");
|
|
38
|
+
return `❌ ${message}`;
|
|
39
|
+
}
|
|
40
|
+
const toolStep = formatToolStep(subagent);
|
|
41
|
+
if (toolStep) {
|
|
42
|
+
return toolStep;
|
|
43
|
+
}
|
|
44
|
+
return `⚙️ ${t("subagent.working")}`;
|
|
45
|
+
}
|
|
46
|
+
async function formatSubagentCard(subagent) {
|
|
47
|
+
const modelName = formatModelDisplayName(subagent.providerID, subagent.modelID);
|
|
48
|
+
const lines = [
|
|
49
|
+
`🧩 ${t("subagent.line.task", { task: subagent.description })}`,
|
|
50
|
+
t("subagent.line.agent", { agent: subagent.agent }),
|
|
51
|
+
t("pinned.line.model", { model: modelName }),
|
|
52
|
+
"",
|
|
53
|
+
formatSubagentActivity(subagent),
|
|
54
|
+
];
|
|
55
|
+
return lines.join("\n");
|
|
56
|
+
}
|
|
57
|
+
export async function renderSubagentCards(subagents) {
|
|
58
|
+
if (subagents.length === 0) {
|
|
59
|
+
return "";
|
|
60
|
+
}
|
|
61
|
+
const parts = await Promise.all(subagents.map((subagent) => formatSubagentCard(subagent)));
|
|
62
|
+
return parts.filter(Boolean).join("\n\n");
|
|
63
|
+
}
|