zardbot-telegram 1.0.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.
Files changed (111) hide show
  1. package/.env.example +116 -0
  2. package/LICENSE +21 -0
  3. package/README.md +250 -0
  4. package/dist/agent/manager.js +88 -0
  5. package/dist/agent/types.js +26 -0
  6. package/dist/app/start-bot-app.js +49 -0
  7. package/dist/bot/commands/abort.js +121 -0
  8. package/dist/bot/commands/commands.js +480 -0
  9. package/dist/bot/commands/definitions.js +27 -0
  10. package/dist/bot/commands/help.js +10 -0
  11. package/dist/bot/commands/models.js +38 -0
  12. package/dist/bot/commands/new.js +70 -0
  13. package/dist/bot/commands/opencode-start.js +101 -0
  14. package/dist/bot/commands/opencode-stop.js +44 -0
  15. package/dist/bot/commands/projects.js +223 -0
  16. package/dist/bot/commands/rename.js +139 -0
  17. package/dist/bot/commands/sessions.js +351 -0
  18. package/dist/bot/commands/start.js +43 -0
  19. package/dist/bot/commands/status.js +95 -0
  20. package/dist/bot/commands/task.js +399 -0
  21. package/dist/bot/commands/tasklist.js +220 -0
  22. package/dist/bot/commands/voice.js +145 -0
  23. package/dist/bot/handlers/agent.js +118 -0
  24. package/dist/bot/handlers/context.js +100 -0
  25. package/dist/bot/handlers/document.js +65 -0
  26. package/dist/bot/handlers/inline-menu.js +119 -0
  27. package/dist/bot/handlers/model.js +143 -0
  28. package/dist/bot/handlers/permission.js +235 -0
  29. package/dist/bot/handlers/prompt.js +240 -0
  30. package/dist/bot/handlers/question.js +390 -0
  31. package/dist/bot/handlers/tts.js +89 -0
  32. package/dist/bot/handlers/variant.js +138 -0
  33. package/dist/bot/handlers/voice.js +173 -0
  34. package/dist/bot/index.js +977 -0
  35. package/dist/bot/message-patterns.js +4 -0
  36. package/dist/bot/middleware/auth.js +30 -0
  37. package/dist/bot/middleware/interaction-guard.js +95 -0
  38. package/dist/bot/middleware/unknown-command.js +22 -0
  39. package/dist/bot/streaming/response-streamer.js +286 -0
  40. package/dist/bot/streaming/tool-call-streamer.js +285 -0
  41. package/dist/bot/utils/busy-guard.js +15 -0
  42. package/dist/bot/utils/commands.js +21 -0
  43. package/dist/bot/utils/file-download.js +91 -0
  44. package/dist/bot/utils/finalize-assistant-response.js +52 -0
  45. package/dist/bot/utils/keyboard.js +69 -0
  46. package/dist/bot/utils/send-with-markdown-fallback.js +165 -0
  47. package/dist/bot/utils/telegram-text.js +28 -0
  48. package/dist/bot/utils/thinking-message.js +8 -0
  49. package/dist/cli/args.js +98 -0
  50. package/dist/cli.js +80 -0
  51. package/dist/config.js +97 -0
  52. package/dist/i18n/de.js +357 -0
  53. package/dist/i18n/en.js +357 -0
  54. package/dist/i18n/es.js +357 -0
  55. package/dist/i18n/fr.js +357 -0
  56. package/dist/i18n/index.js +109 -0
  57. package/dist/i18n/ru.js +357 -0
  58. package/dist/i18n/zh.js +357 -0
  59. package/dist/index.js +26 -0
  60. package/dist/interaction/busy.js +8 -0
  61. package/dist/interaction/cleanup.js +32 -0
  62. package/dist/interaction/guard.js +140 -0
  63. package/dist/interaction/manager.js +106 -0
  64. package/dist/interaction/types.js +1 -0
  65. package/dist/keyboard/manager.js +172 -0
  66. package/dist/keyboard/types.js +1 -0
  67. package/dist/model/capabilities.js +62 -0
  68. package/dist/model/context-limit.js +57 -0
  69. package/dist/model/manager.js +259 -0
  70. package/dist/model/types.js +24 -0
  71. package/dist/opencode/client.js +13 -0
  72. package/dist/opencode/events.js +140 -0
  73. package/dist/permission/manager.js +100 -0
  74. package/dist/permission/types.js +1 -0
  75. package/dist/pinned/format.js +29 -0
  76. package/dist/pinned/manager.js +682 -0
  77. package/dist/pinned/types.js +1 -0
  78. package/dist/process/manager.js +273 -0
  79. package/dist/process/types.js +1 -0
  80. package/dist/project/manager.js +88 -0
  81. package/dist/question/manager.js +176 -0
  82. package/dist/question/types.js +1 -0
  83. package/dist/rename/manager.js +53 -0
  84. package/dist/runtime/bootstrap.js +350 -0
  85. package/dist/runtime/mode.js +74 -0
  86. package/dist/runtime/paths.js +37 -0
  87. package/dist/scheduled-task/creation-manager.js +113 -0
  88. package/dist/scheduled-task/display.js +239 -0
  89. package/dist/scheduled-task/executor.js +87 -0
  90. package/dist/scheduled-task/foreground-state.js +32 -0
  91. package/dist/scheduled-task/next-run.js +207 -0
  92. package/dist/scheduled-task/runtime.js +368 -0
  93. package/dist/scheduled-task/schedule-parser.js +169 -0
  94. package/dist/scheduled-task/store.js +65 -0
  95. package/dist/scheduled-task/types.js +19 -0
  96. package/dist/session/cache-manager.js +455 -0
  97. package/dist/session/manager.js +10 -0
  98. package/dist/settings/manager.js +158 -0
  99. package/dist/stt/client.js +97 -0
  100. package/dist/summary/aggregator.js +1136 -0
  101. package/dist/summary/formatter.js +491 -0
  102. package/dist/summary/subagent-formatter.js +63 -0
  103. package/dist/summary/tool-message-batcher.js +90 -0
  104. package/dist/tts/client.js +130 -0
  105. package/dist/utils/error-format.js +29 -0
  106. package/dist/utils/logger.js +127 -0
  107. package/dist/utils/safe-background-task.js +33 -0
  108. package/dist/utils/telegram-rate-limit-retry.js +93 -0
  109. package/dist/variant/manager.js +103 -0
  110. package/dist/variant/types.js +1 -0
  111. package/package.json +79 -0
@@ -0,0 +1,1136 @@
1
+ import { normalizePathForDisplay, prepareCodeFile } from "./formatter.js";
2
+ import { logger } from "../utils/logger.js";
3
+ import { getCurrentProject } from "../settings/manager.js";
4
+ function extractFirstUpdatedFileFromTitle(title) {
5
+ for (const rawLine of title.split("\n")) {
6
+ const line = rawLine.trim();
7
+ if (line.length >= 3 && line[1] === " " && /[AMDURC]/.test(line[0])) {
8
+ return line.slice(2).trim();
9
+ }
10
+ }
11
+ return "";
12
+ }
13
+ function countDiffChangesFromText(text) {
14
+ let additions = 0;
15
+ let deletions = 0;
16
+ for (const line of text.split("\n")) {
17
+ if (line.startsWith("+") && !line.startsWith("+++")) {
18
+ additions++;
19
+ continue;
20
+ }
21
+ if (line.startsWith("-") && !line.startsWith("---")) {
22
+ deletions++;
23
+ }
24
+ }
25
+ return { additions, deletions };
26
+ }
27
+ class SummaryAggregator {
28
+ currentSessionId = null;
29
+ textMessageStates = new Map();
30
+ messages = new Map();
31
+ messageCount = 0;
32
+ lastUpdated = 0;
33
+ onCompleteCallback = null;
34
+ onPartialCallback = null;
35
+ onToolCallback = null;
36
+ onToolFileCallback = null;
37
+ onQuestionCallback = null;
38
+ onQuestionErrorCallback = null;
39
+ onThinkingCallback = null;
40
+ onTokensCallback = null;
41
+ onCostCallback = null;
42
+ onSubagentCallback = null;
43
+ onSessionCompactedCallback = null;
44
+ onSessionErrorCallback = null;
45
+ onSessionRetryCallback = null;
46
+ onPermissionCallback = null;
47
+ onSessionDiffCallback = null;
48
+ onFileChangeCallback = null;
49
+ onClearedCallback = null;
50
+ processedToolStates = new Set();
51
+ thinkingFiredForMessages = new Set();
52
+ knownTextPartIds = new Map();
53
+ bot = null;
54
+ chatId = null;
55
+ typingTimer = null;
56
+ typingIndicatorEnabled = true;
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();
65
+ setBotAndChatId(bot, chatId) {
66
+ this.bot = bot;
67
+ this.chatId = chatId;
68
+ }
69
+ setOnComplete(callback) {
70
+ this.onCompleteCallback = callback;
71
+ }
72
+ setOnPartial(callback) {
73
+ this.onPartialCallback = callback;
74
+ }
75
+ setOnTool(callback) {
76
+ this.onToolCallback = callback;
77
+ }
78
+ setOnToolFile(callback) {
79
+ this.onToolFileCallback = callback;
80
+ }
81
+ setOnQuestion(callback) {
82
+ this.onQuestionCallback = callback;
83
+ }
84
+ setOnQuestionError(callback) {
85
+ this.onQuestionErrorCallback = callback;
86
+ }
87
+ setOnThinking(callback) {
88
+ this.onThinkingCallback = callback;
89
+ }
90
+ setOnTokens(callback) {
91
+ this.onTokensCallback = callback;
92
+ }
93
+ setOnCost(callback) {
94
+ this.onCostCallback = callback;
95
+ }
96
+ setOnSubagent(callback) {
97
+ this.onSubagentCallback = callback;
98
+ }
99
+ setOnSessionCompacted(callback) {
100
+ this.onSessionCompactedCallback = callback;
101
+ }
102
+ setOnSessionError(callback) {
103
+ this.onSessionErrorCallback = callback;
104
+ }
105
+ setOnSessionRetry(callback) {
106
+ this.onSessionRetryCallback = callback;
107
+ }
108
+ setOnPermission(callback) {
109
+ this.onPermissionCallback = callback;
110
+ }
111
+ setOnSessionDiff(callback) {
112
+ this.onSessionDiffCallback = callback;
113
+ }
114
+ setOnFileChange(callback) {
115
+ this.onFileChangeCallback = callback;
116
+ }
117
+ setOnCleared(callback) {
118
+ this.onClearedCallback = callback;
119
+ }
120
+ setTypingIndicatorEnabled(enabled) {
121
+ this.typingIndicatorEnabled = enabled;
122
+ if (!enabled) {
123
+ this.stopTypingIndicator();
124
+ }
125
+ }
126
+ startTypingIndicator() {
127
+ if (!this.typingIndicatorEnabled) {
128
+ return;
129
+ }
130
+ if (this.typingTimer) {
131
+ return;
132
+ }
133
+ const sendTyping = () => {
134
+ if (this.bot && this.chatId) {
135
+ this.bot.api.sendChatAction(this.chatId, "typing").catch((err) => {
136
+ logger.error("Failed to send typing action:", err);
137
+ });
138
+ }
139
+ };
140
+ sendTyping();
141
+ this.typingTimer = setInterval(sendTyping, 4000);
142
+ }
143
+ stopTypingIndicator() {
144
+ if (this.typingTimer) {
145
+ clearInterval(this.typingTimer);
146
+ this.typingTimer = null;
147
+ }
148
+ }
149
+ processEvent(event) {
150
+ const eventType = event.type;
151
+ if (eventType === "message.part.delta") {
152
+ this.handleMessagePartDelta(event);
153
+ return;
154
+ }
155
+ // Log all question-related events for debugging
156
+ if (event.type.startsWith("question.")) {
157
+ logger.info(`[Aggregator] Question event: ${event.type}`, JSON.stringify(event.properties, null, 2));
158
+ }
159
+ // Log all session-related events for debugging
160
+ if (event.type.startsWith("session.")) {
161
+ logger.debug(`[Aggregator] Session event: ${event.type}`, JSON.stringify(event.properties, null, 2));
162
+ }
163
+ switch (event.type) {
164
+ case "session.created":
165
+ case "session.updated":
166
+ this.handleSessionCreatedOrUpdated(event);
167
+ break;
168
+ case "message.updated":
169
+ this.handleMessageUpdated(event);
170
+ break;
171
+ case "message.part.updated":
172
+ this.handleMessagePartUpdated(event);
173
+ break;
174
+ case "session.status":
175
+ this.handleSessionStatus(event);
176
+ break;
177
+ case "session.idle":
178
+ this.handleSessionIdle(event);
179
+ break;
180
+ case "session.compacted":
181
+ this.handleSessionCompacted(event);
182
+ break;
183
+ case "session.error":
184
+ this.handleSessionError(event);
185
+ break;
186
+ case "question.asked":
187
+ this.handleQuestionAsked(event);
188
+ break;
189
+ case "question.replied":
190
+ logger.info(`[Aggregator] Question replied: requestID=${event.properties.requestID}`);
191
+ break;
192
+ case "question.rejected":
193
+ logger.info(`[Aggregator] Question rejected: requestID=${event.properties.requestID}`);
194
+ break;
195
+ case "session.diff":
196
+ this.handleSessionDiff(event);
197
+ break;
198
+ case "permission.asked":
199
+ this.handlePermissionAsked(event);
200
+ break;
201
+ case "permission.replied":
202
+ logger.info(`[Aggregator] Permission replied: requestID=${event.properties.requestID}`);
203
+ break;
204
+ default:
205
+ logger.debug(`[Aggregator] Unhandled event type: ${event.type}`);
206
+ break;
207
+ }
208
+ }
209
+ setSession(sessionId) {
210
+ if (this.currentSessionId !== sessionId) {
211
+ this.clear();
212
+ this.currentSessionId = sessionId;
213
+ this.trackedSessionParents.set(sessionId, null);
214
+ }
215
+ }
216
+ clear() {
217
+ this.stopTypingIndicator();
218
+ this.currentSessionId = null;
219
+ this.textMessageStates.clear();
220
+ this.messages.clear();
221
+ this.partHashes.clear();
222
+ this.knownTextPartIds.clear();
223
+ this.processedToolStates.clear();
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();
232
+ this.messageCount = 0;
233
+ this.lastUpdated = 0;
234
+ if (this.onClearedCallback) {
235
+ try {
236
+ this.onClearedCallback();
237
+ }
238
+ catch (err) {
239
+ logger.error("[Aggregator] Error in clear callback:", err);
240
+ }
241
+ }
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
+ }
585
+ handleMessageUpdated(event) {
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
+ }
599
+ if (info.sessionID !== this.currentSessionId) {
600
+ return;
601
+ }
602
+ const messageID = info.id;
603
+ this.messages.set(messageID, { role: info.role });
604
+ if (info.role === "assistant") {
605
+ if (!this.textMessageStates.has(messageID)) {
606
+ this.textMessageStates.set(messageID, {
607
+ orderedPartIds: [],
608
+ partTexts: new Map(),
609
+ optimisticUpdateCount: 0,
610
+ });
611
+ this.messageCount++;
612
+ this.startTypingIndicator();
613
+ }
614
+ const textState = this.getOrCreateTextMessageState(messageID);
615
+ const assistantMessage = info;
616
+ const time = assistantMessage.time;
617
+ const isCompleted = Boolean(time?.completed);
618
+ const messageText = this.getCombinedMessageText(messageID);
619
+ if (!isCompleted && textState.optimisticUpdateCount === 1) {
620
+ this.emitPartialText(info.sessionID, messageID, messageText);
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
+ }
637
+ if (isCompleted) {
638
+ const finalText = messageText;
639
+ logger.debug(`[Aggregator] Message part completed: messageId=${messageID}, textLength=${finalText.length}, totalParts=${textState.orderedPartIds.length}, session=${this.currentSessionId}`);
640
+ // Extract and report cost
641
+ if (this.onCostCallback && assistantInfo.cost !== undefined) {
642
+ logger.debug(`[Aggregator] Cost: $${assistantInfo.cost.toFixed(2)}`);
643
+ this.onCostCallback(assistantInfo.cost);
644
+ }
645
+ if (this.onCompleteCallback && finalText.length > 0) {
646
+ this.onCompleteCallback(this.currentSessionId, messageID, finalText);
647
+ }
648
+ this.textMessageStates.delete(messageID);
649
+ this.messages.delete(messageID);
650
+ this.partHashes.delete(messageID);
651
+ this.knownTextPartIds.delete(messageID);
652
+ logger.debug(`[Aggregator] Message completed cleanup: remaining messages=${this.textMessageStates.size}`);
653
+ if (this.textMessageStates.size === 0) {
654
+ logger.debug("[Aggregator] No more active messages, stopping typing indicator");
655
+ this.stopTypingIndicator();
656
+ }
657
+ }
658
+ this.lastUpdated = Date.now();
659
+ }
660
+ }
661
+ handleMessagePartUpdated(event) {
662
+ const { part } = event.properties;
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();
692
+ return;
693
+ }
694
+ const messageID = part.messageID;
695
+ const messageInfo = this.messages.get(messageID);
696
+ if (part.type === "text") {
697
+ this.registerKnownTextPart(messageID, part.id);
698
+ this.registerTextPart(messageID, part.id);
699
+ }
700
+ const deltaFromUpdated = event.properties.delta;
701
+ if (part.type === "text" &&
702
+ typeof deltaFromUpdated === "string" &&
703
+ deltaFromUpdated.length > 0) {
704
+ this.applyTextDelta(part.sessionID, messageID, part.id, deltaFromUpdated, part.text);
705
+ this.lastUpdated = Date.now();
706
+ return;
707
+ }
708
+ if (part.type === "reasoning") {
709
+ // Fire the thinking callback once per message on the first reasoning part.
710
+ // This is the signal that the model is actually doing extended thinking.
711
+ if (!this.thinkingFiredForMessages.has(messageID) && this.onThinkingCallback) {
712
+ this.thinkingFiredForMessages.add(messageID);
713
+ const callback = this.onThinkingCallback;
714
+ const sessionID = part.sessionID;
715
+ setImmediate(() => {
716
+ if (typeof callback === "function") {
717
+ callback(sessionID);
718
+ }
719
+ });
720
+ }
721
+ }
722
+ else if (part.type === "text" && "text" in part && part.text) {
723
+ const wasUpdated = messageInfo && messageInfo.role === "assistant"
724
+ ? this.setTextPartSnapshot(messageID, part.id, part.text)
725
+ : this.setOptimisticTextSnapshot(messageID, part.id, part.text);
726
+ if (!wasUpdated) {
727
+ return;
728
+ }
729
+ const fullText = this.getCombinedMessageText(messageID);
730
+ if (messageInfo && messageInfo.role === "assistant") {
731
+ this.startTypingIndicator();
732
+ this.emitPartialText(part.sessionID, messageID, fullText);
733
+ }
734
+ else {
735
+ const state = this.getOrCreateTextMessageState(messageID);
736
+ state.optimisticUpdateCount++;
737
+ if (state.optimisticUpdateCount >= 2) {
738
+ this.emitPartialText(part.sessionID, messageID, fullText);
739
+ }
740
+ }
741
+ }
742
+ else if (part.type === "tool") {
743
+ const state = part.state;
744
+ const input = "input" in state ? state.input : undefined;
745
+ const title = "title" in state ? state.title : undefined;
746
+ if (part.tool === "task") {
747
+ this.updateSubagentFromTaskTool(part.sessionID, input);
748
+ }
749
+ logger.debug(`[Aggregator] Tool event: callID=${part.callID}, tool=${part.tool}, status=${"status" in state ? state.status : "unknown"}`);
750
+ if (part.tool === "question") {
751
+ logger.debug(`[Aggregator] Question tool part update:`, JSON.stringify(part, null, 2));
752
+ // If the question tool fails, clear the active poll
753
+ // so the agent can recreate it with corrected data
754
+ if ("status" in state && state.status === "error") {
755
+ logger.info(`[Aggregator] Question tool failed with error, clearing active poll. callID=${part.callID}`);
756
+ if (this.onQuestionErrorCallback) {
757
+ setImmediate(() => {
758
+ this.onQuestionErrorCallback();
759
+ });
760
+ }
761
+ return;
762
+ }
763
+ // NOTE: Questions are now handled via "question.asked" event, not via tool part updates.
764
+ // This ensures we have access to the requestID needed for question.reply().
765
+ }
766
+ if ("status" in state && state.status === "completed") {
767
+ logger.debug(`[Aggregator] Tool completed: callID=${part.callID}, tool=${part.tool}`, JSON.stringify(state, null, 2));
768
+ const completedKey = `completed-${part.callID}`;
769
+ if (!this.processedToolStates.has(completedKey)) {
770
+ this.processedToolStates.add(completedKey);
771
+ const preparedFileContext = this.prepareToolFileContext(part.tool, input, title, state.metadata);
772
+ const toolData = {
773
+ sessionId: part.sessionID,
774
+ messageId: messageID,
775
+ callId: part.callID,
776
+ tool: part.tool,
777
+ state: part.state,
778
+ input,
779
+ title,
780
+ metadata: state.metadata,
781
+ hasFileAttachment: !!preparedFileContext.fileData,
782
+ };
783
+ logger.debug(`[Aggregator] Sending tool notification to Telegram: tool=${part.tool}, title=${title || "N/A"}`);
784
+ if (this.onToolCallback) {
785
+ this.onToolCallback(toolData);
786
+ }
787
+ if (preparedFileContext.fileData && this.onToolFileCallback) {
788
+ logger.debug(`[Aggregator] Sending ${part.tool} file: ${preparedFileContext.fileData.filename} (${preparedFileContext.fileData.buffer.length} bytes)`);
789
+ this.onToolFileCallback({
790
+ ...toolData,
791
+ hasFileAttachment: true,
792
+ fileData: preparedFileContext.fileData,
793
+ });
794
+ }
795
+ if (preparedFileContext.fileChange && this.onFileChangeCallback) {
796
+ this.onFileChangeCallback(preparedFileContext.fileChange);
797
+ }
798
+ }
799
+ }
800
+ }
801
+ this.lastUpdated = Date.now();
802
+ }
803
+ handleMessagePartDelta(event) {
804
+ const part = event.properties.part;
805
+ const sessionID = part?.sessionID || event.properties.sessionID;
806
+ const messageID = part?.messageID || event.properties.messageID;
807
+ const partID = part?.id || event.properties.partID || "text";
808
+ const partType = part?.type || event.properties.type;
809
+ const delta = event.properties.delta;
810
+ if (!sessionID || !messageID || typeof delta !== "string" || delta.length === 0) {
811
+ return;
812
+ }
813
+ if (partType && partType !== "text") {
814
+ return;
815
+ }
816
+ if (partType === "text") {
817
+ this.registerKnownTextPart(messageID, partID);
818
+ this.registerTextPart(messageID, partID);
819
+ }
820
+ else {
821
+ const knownTextIds = this.knownTextPartIds.get(messageID);
822
+ const isKnownTextPart = knownTextIds?.has(partID) ?? false;
823
+ const thinkingFired = this.thinkingFiredForMessages.has(messageID);
824
+ if (thinkingFired && !isKnownTextPart) {
825
+ return;
826
+ }
827
+ if (!thinkingFired && !isKnownTextPart) {
828
+ this.registerKnownTextPart(messageID, partID);
829
+ this.registerTextPart(messageID, partID);
830
+ }
831
+ }
832
+ this.applyTextDelta(sessionID, messageID, partID, delta, part?.text);
833
+ }
834
+ applyTextDelta(sessionID, messageID, partID, delta, fullTextHint) {
835
+ if (sessionID !== this.currentSessionId) {
836
+ return;
837
+ }
838
+ this.registerTextPart(messageID, partID);
839
+ const state = this.getOrCreateTextMessageState(messageID);
840
+ const previous = state.partTexts.get(partID) || "";
841
+ let accumulated = `${previous}${delta}`;
842
+ if (typeof fullTextHint === "string" && fullTextHint.length > accumulated.length) {
843
+ accumulated = fullTextHint;
844
+ }
845
+ state.partTexts.set(partID, accumulated);
846
+ const combined = this.getCombinedMessageText(messageID);
847
+ if (!combined.trim()) {
848
+ return;
849
+ }
850
+ this.startTypingIndicator();
851
+ this.emitPartialText(sessionID, messageID, combined);
852
+ }
853
+ emitPartialText(sessionId, messageId, messageText) {
854
+ if (!this.onPartialCallback || !messageText.trim()) {
855
+ return;
856
+ }
857
+ try {
858
+ this.onPartialCallback(sessionId, messageId, messageText);
859
+ }
860
+ catch (err) {
861
+ logger.error("[Aggregator] Error in partial callback:", err);
862
+ }
863
+ }
864
+ getOrCreateTextMessageState(messageID) {
865
+ const existing = this.textMessageStates.get(messageID);
866
+ if (existing) {
867
+ return existing;
868
+ }
869
+ const state = {
870
+ orderedPartIds: [],
871
+ partTexts: new Map(),
872
+ optimisticUpdateCount: 0,
873
+ };
874
+ this.textMessageStates.set(messageID, state);
875
+ return state;
876
+ }
877
+ registerKnownTextPart(messageID, partID) {
878
+ if (!this.knownTextPartIds.has(messageID)) {
879
+ this.knownTextPartIds.set(messageID, new Set());
880
+ }
881
+ this.knownTextPartIds.get(messageID).add(partID);
882
+ }
883
+ registerTextPart(messageID, partID) {
884
+ const state = this.getOrCreateTextMessageState(messageID);
885
+ if (!state.orderedPartIds.includes(partID)) {
886
+ state.orderedPartIds.push(partID);
887
+ }
888
+ }
889
+ setTextPartSnapshot(messageID, partID, text) {
890
+ const normalized = text;
891
+ const partHash = this.hashString(`${partID}\n${normalized}`);
892
+ if (!this.partHashes.has(messageID)) {
893
+ this.partHashes.set(messageID, new Set());
894
+ }
895
+ const hashes = this.partHashes.get(messageID);
896
+ if (hashes.has(partHash)) {
897
+ return false;
898
+ }
899
+ hashes.add(partHash);
900
+ this.registerTextPart(messageID, partID);
901
+ const state = this.getOrCreateTextMessageState(messageID);
902
+ state.partTexts.set(partID, normalized);
903
+ return true;
904
+ }
905
+ setOptimisticTextSnapshot(messageID, partID, text) {
906
+ const wasUpdated = this.setTextPartSnapshot(messageID, partID, text);
907
+ if (!wasUpdated) {
908
+ return false;
909
+ }
910
+ const state = this.getOrCreateTextMessageState(messageID);
911
+ state.orderedPartIds = [partID];
912
+ state.partTexts = new Map([[partID, text]]);
913
+ return true;
914
+ }
915
+ getCombinedMessageText(messageID) {
916
+ const state = this.textMessageStates.get(messageID);
917
+ if (!state) {
918
+ return "";
919
+ }
920
+ return state.orderedPartIds.map((partID) => state.partTexts.get(partID) || "").join("");
921
+ }
922
+ prepareToolFileContext(tool, input, title, metadata) {
923
+ if (tool === "write" && input) {
924
+ const filePath = typeof input.filePath === "string" ? normalizePathForDisplay(input.filePath) : "";
925
+ const hasContent = typeof input.content === "string";
926
+ const content = hasContent ? input.content : "";
927
+ if (!filePath || !hasContent) {
928
+ return { fileData: null, fileChange: null };
929
+ }
930
+ return {
931
+ fileData: prepareCodeFile(content, filePath, "write"),
932
+ fileChange: {
933
+ file: filePath,
934
+ additions: content.split("\n").length,
935
+ deletions: 0,
936
+ },
937
+ };
938
+ }
939
+ if (tool === "edit" && metadata) {
940
+ const editMetadata = metadata;
941
+ const filePath = editMetadata.filediff?.file
942
+ ? normalizePathForDisplay(editMetadata.filediff.file)
943
+ : "";
944
+ const diffText = typeof editMetadata.diff === "string" ? editMetadata.diff : "";
945
+ if (!filePath || !diffText) {
946
+ return { fileData: null, fileChange: null };
947
+ }
948
+ return {
949
+ fileData: prepareCodeFile(diffText, filePath, "edit"),
950
+ fileChange: {
951
+ file: filePath,
952
+ additions: editMetadata.filediff?.additions || 0,
953
+ deletions: editMetadata.filediff?.deletions || 0,
954
+ },
955
+ };
956
+ }
957
+ if (tool === "apply_patch") {
958
+ const patchMetadata = metadata;
959
+ const filePathFromInput = input && typeof input.filePath === "string"
960
+ ? normalizePathForDisplay(input.filePath)
961
+ : input && typeof input.path === "string"
962
+ ? normalizePathForDisplay(input.path)
963
+ : "";
964
+ const filePathFromTitle = title ? extractFirstUpdatedFileFromTitle(title) : "";
965
+ const filePath = (patchMetadata?.filediff?.file && normalizePathForDisplay(patchMetadata.filediff.file)) ||
966
+ filePathFromInput ||
967
+ normalizePathForDisplay(filePathFromTitle);
968
+ const diffText = typeof patchMetadata?.diff === "string"
969
+ ? patchMetadata.diff
970
+ : input && typeof input.patchText === "string"
971
+ ? input.patchText
972
+ : "";
973
+ if (!filePath) {
974
+ return { fileData: null, fileChange: null };
975
+ }
976
+ const fileChange = patchMetadata?.filediff
977
+ ? {
978
+ file: filePath,
979
+ additions: patchMetadata.filediff.additions || 0,
980
+ deletions: patchMetadata.filediff.deletions || 0,
981
+ }
982
+ : diffText
983
+ ? (() => {
984
+ const changes = countDiffChangesFromText(diffText);
985
+ return {
986
+ file: filePath,
987
+ additions: changes.additions,
988
+ deletions: changes.deletions,
989
+ };
990
+ })()
991
+ : null;
992
+ return {
993
+ fileData: diffText ? prepareCodeFile(diffText, filePath, "edit") : null,
994
+ fileChange,
995
+ };
996
+ }
997
+ return { fileData: null, fileChange: null };
998
+ }
999
+ hashString(str) {
1000
+ let hash = 0;
1001
+ for (let i = 0; i < str.length; i++) {
1002
+ const char = str.charCodeAt(i);
1003
+ hash = (hash << 5) - hash + char;
1004
+ hash = hash & hash;
1005
+ }
1006
+ return hash.toString(36);
1007
+ }
1008
+ handleSessionStatus(event) {
1009
+ const { sessionID, status } = event.properties;
1010
+ if (sessionID !== this.currentSessionId) {
1011
+ return;
1012
+ }
1013
+ if (status?.type !== "retry" || !this.onSessionRetryCallback) {
1014
+ return;
1015
+ }
1016
+ const callback = this.onSessionRetryCallback;
1017
+ const message = status.message?.trim() || "Unknown retry error";
1018
+ logger.warn(`[Aggregator] Session retry: session=${sessionID}, attempt=${status.attempt ?? "n/a"}, message=${message}`);
1019
+ setImmediate(() => {
1020
+ callback({
1021
+ sessionId: sessionID,
1022
+ attempt: status.attempt,
1023
+ message,
1024
+ next: status.next,
1025
+ });
1026
+ });
1027
+ }
1028
+ handleSessionIdle(event) {
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
+ }
1035
+ if (sessionID !== this.currentSessionId) {
1036
+ return;
1037
+ }
1038
+ logger.info(`[Aggregator] Session became idle: ${sessionID}`);
1039
+ // Stop typing indicator when session goes idle
1040
+ this.stopTypingIndicator();
1041
+ }
1042
+ handleSessionCompacted(event) {
1043
+ const properties = event.properties;
1044
+ const { sessionID } = properties;
1045
+ if (sessionID !== this.currentSessionId) {
1046
+ return;
1047
+ }
1048
+ logger.info(`[Aggregator] Session compacted: ${sessionID}`);
1049
+ // Reload context from history after compaction
1050
+ if (this.onSessionCompactedCallback) {
1051
+ setImmediate(() => {
1052
+ const project = getCurrentProject();
1053
+ if (project) {
1054
+ this.onSessionCompactedCallback(sessionID, project.worktree);
1055
+ }
1056
+ });
1057
+ }
1058
+ }
1059
+ handleSessionError(event) {
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
+ }
1067
+ if (sessionID !== this.currentSessionId) {
1068
+ return;
1069
+ }
1070
+ logger.warn(`[Aggregator] Session error: ${sessionID}: ${message}`);
1071
+ this.stopTypingIndicator();
1072
+ if (this.onSessionErrorCallback) {
1073
+ const callback = this.onSessionErrorCallback;
1074
+ setImmediate(() => {
1075
+ callback(sessionID, message);
1076
+ });
1077
+ }
1078
+ }
1079
+ handleQuestionAsked(event) {
1080
+ const { id, sessionID, questions } = event.properties;
1081
+ if (sessionID !== this.currentSessionId) {
1082
+ logger.debug(`[Aggregator] Ignoring question.asked for different session: ${sessionID} (current: ${this.currentSessionId})`);
1083
+ return;
1084
+ }
1085
+ logger.info(`[Aggregator] Question asked: requestID=${id}, questions=${questions.length}`);
1086
+ if (this.onQuestionCallback) {
1087
+ const callback = this.onQuestionCallback;
1088
+ setImmediate(async () => {
1089
+ try {
1090
+ await callback(questions, id);
1091
+ }
1092
+ catch (err) {
1093
+ logger.error("[Aggregator] Error in question callback:", err);
1094
+ }
1095
+ });
1096
+ }
1097
+ }
1098
+ handleSessionDiff(event) {
1099
+ const properties = event.properties;
1100
+ if (properties.sessionID !== this.currentSessionId) {
1101
+ return;
1102
+ }
1103
+ logger.debug(`[Aggregator] Session diff: ${properties.diff.length} files changed`);
1104
+ if (this.onSessionDiffCallback) {
1105
+ const diffs = properties.diff.map((d) => ({
1106
+ file: d.file,
1107
+ additions: d.additions,
1108
+ deletions: d.deletions,
1109
+ }));
1110
+ const callback = this.onSessionDiffCallback;
1111
+ setImmediate(() => {
1112
+ callback(properties.sessionID, diffs);
1113
+ });
1114
+ }
1115
+ }
1116
+ handlePermissionAsked(event) {
1117
+ const request = event.properties;
1118
+ if (request.sessionID !== this.currentSessionId) {
1119
+ logger.debug(`[Aggregator] Ignoring permission.asked for different session: ${request.sessionID} (current: ${this.currentSessionId})`);
1120
+ return;
1121
+ }
1122
+ logger.info(`[Aggregator] Permission asked: requestID=${request.id}, type=${request.permission}, patterns=${request.patterns.length}`);
1123
+ if (this.onPermissionCallback) {
1124
+ const callback = this.onPermissionCallback;
1125
+ setImmediate(async () => {
1126
+ try {
1127
+ await callback(request);
1128
+ }
1129
+ catch (err) {
1130
+ logger.error("[Aggregator] Error in permission callback:", err);
1131
+ }
1132
+ });
1133
+ }
1134
+ }
1135
+ }
1136
+ export const summaryAggregator = new SummaryAggregator();