@grinev/opencode-telegram-bot 0.13.2 → 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 +59 -23
- package/dist/bot/streaming/response-streamer.js +6 -4
- 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 +0 -1
- package/dist/config.js +0 -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 +30 -55
- 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/package.json +1 -1
package/dist/pinned/manager.js
CHANGED
|
@@ -3,7 +3,9 @@ import { opencodeClient } from "../opencode/client.js";
|
|
|
3
3
|
import { getCurrentSession } from "../session/manager.js";
|
|
4
4
|
import { getCurrentProject, getPinnedMessageId, setPinnedMessageId, clearPinnedMessageId, } from "../settings/manager.js";
|
|
5
5
|
import { getStoredModel } from "../model/manager.js";
|
|
6
|
+
import { getModelContextLimit } from "../model/context-limit.js";
|
|
6
7
|
import { t } from "../i18n/index.js";
|
|
8
|
+
import { DEFAULT_CONTEXT_LIMIT, formatContextLine, formatCostLine, formatModelDisplayName, } from "./format.js";
|
|
7
9
|
class PinnedMessageManager {
|
|
8
10
|
api = null;
|
|
9
11
|
chatId = null;
|
|
@@ -147,6 +149,22 @@ class PinnedMessageManager {
|
|
|
147
149
|
await this.refreshSessionTitle();
|
|
148
150
|
await this.updatePinnedMessage();
|
|
149
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Update tokens in memory without triggering an API call.
|
|
154
|
+
* Used for intermediate (non-completed) message.updated events
|
|
155
|
+
* to keep pinned state in sync with keyboardManager.
|
|
156
|
+
*/
|
|
157
|
+
updateTokensSilent(tokens) {
|
|
158
|
+
this.state.tokensUsed = tokens.input + tokens.cacheRead;
|
|
159
|
+
logger.debug(`[PinnedManager] Tokens updated (silent): ${this.state.tokensUsed}/${this.state.tokensLimit}`);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Refresh the pinned message with current in-memory state.
|
|
163
|
+
* Used at thinking time to push accumulated silent updates to Telegram.
|
|
164
|
+
*/
|
|
165
|
+
async refresh() {
|
|
166
|
+
await this.updatePinnedMessage();
|
|
167
|
+
}
|
|
150
168
|
/**
|
|
151
169
|
* Called when cost info is received from SSE events
|
|
152
170
|
*/
|
|
@@ -162,6 +180,12 @@ class PinnedMessageManager {
|
|
|
162
180
|
setOnKeyboardUpdate(callback) {
|
|
163
181
|
this.onKeyboardUpdateCallback = callback;
|
|
164
182
|
logger.debug("[PinnedManager] Keyboard update callback registered");
|
|
183
|
+
// Fire immediately with current state to fix race condition:
|
|
184
|
+
// onSessionChange may have already run before this callback was registered.
|
|
185
|
+
const limit = this.state.tokensLimit > 0 ? this.state.tokensLimit : this.contextLimit || 0;
|
|
186
|
+
if (limit > 0) {
|
|
187
|
+
callback(this.state.tokensUsed, limit);
|
|
188
|
+
}
|
|
165
189
|
}
|
|
166
190
|
/**
|
|
167
191
|
* Get current context information
|
|
@@ -412,38 +436,13 @@ class PinnedMessageManager {
|
|
|
412
436
|
async fetchContextLimit() {
|
|
413
437
|
try {
|
|
414
438
|
const model = getStoredModel();
|
|
415
|
-
|
|
416
|
-
logger.warn("[PinnedManager] No model configured, using default limit");
|
|
417
|
-
this.contextLimit = 200000;
|
|
418
|
-
this.state.tokensLimit = this.contextLimit;
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
const { data: providersData, error } = await opencodeClient.config.providers();
|
|
422
|
-
if (error || !providersData) {
|
|
423
|
-
logger.warn("[PinnedManager] Failed to fetch providers, using default limit");
|
|
424
|
-
this.contextLimit = 200000;
|
|
425
|
-
this.state.tokensLimit = this.contextLimit;
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
428
|
-
// Find the model in providers
|
|
429
|
-
for (const provider of providersData.providers) {
|
|
430
|
-
if (provider.id === model.providerID) {
|
|
431
|
-
const modelInfo = provider.models[model.modelID];
|
|
432
|
-
if (modelInfo?.limit?.context) {
|
|
433
|
-
this.contextLimit = modelInfo.limit.context;
|
|
434
|
-
this.state.tokensLimit = this.contextLimit;
|
|
435
|
-
logger.debug(`[PinnedManager] Context limit: ${this.contextLimit}`);
|
|
436
|
-
return;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
logger.warn("[PinnedManager] Model not found in providers, using default limit");
|
|
441
|
-
this.contextLimit = 200000;
|
|
439
|
+
this.contextLimit = await getModelContextLimit(model.providerID, model.modelID);
|
|
442
440
|
this.state.tokensLimit = this.contextLimit;
|
|
441
|
+
logger.debug(`[PinnedManager] Context limit: ${this.contextLimit}`);
|
|
443
442
|
}
|
|
444
443
|
catch (err) {
|
|
445
444
|
logger.error("[PinnedManager] Error fetching context limit:", err);
|
|
446
|
-
this.contextLimit =
|
|
445
|
+
this.contextLimit = DEFAULT_CONTEXT_LIMIT;
|
|
447
446
|
this.state.tokensLimit = this.contextLimit;
|
|
448
447
|
}
|
|
449
448
|
}
|
|
@@ -451,28 +450,16 @@ class PinnedMessageManager {
|
|
|
451
450
|
* Format the pinned message text
|
|
452
451
|
*/
|
|
453
452
|
formatMessage() {
|
|
454
|
-
const percentage = this.state.tokensLimit > 0
|
|
455
|
-
? Math.round((this.state.tokensUsed / this.state.tokensLimit) * 100)
|
|
456
|
-
: 0;
|
|
457
|
-
const tokensFormatted = this.formatTokenCount(this.state.tokensUsed);
|
|
458
|
-
const limitFormatted = this.formatTokenCount(this.state.tokensLimit);
|
|
459
|
-
// Get current model info
|
|
460
453
|
const currentModel = getStoredModel();
|
|
461
|
-
const modelName = currentModel.providerID
|
|
462
|
-
? `${currentModel.providerID}/${currentModel.modelID}`
|
|
463
|
-
: t("pinned.unknown");
|
|
454
|
+
const modelName = formatModelDisplayName(currentModel.providerID, currentModel.modelID);
|
|
464
455
|
const lines = [
|
|
465
456
|
`${this.state.sessionTitle}`,
|
|
466
457
|
t("pinned.line.project", { project: this.state.projectName }),
|
|
467
458
|
t("pinned.line.model", { model: modelName }),
|
|
468
|
-
|
|
469
|
-
used: tokensFormatted,
|
|
470
|
-
limit: limitFormatted,
|
|
471
|
-
percent: percentage,
|
|
472
|
-
}),
|
|
459
|
+
formatContextLine(this.state.tokensUsed, this.state.tokensLimit),
|
|
473
460
|
];
|
|
474
461
|
if (this.state.cost !== undefined && this.state.cost !== null) {
|
|
475
|
-
lines.push(
|
|
462
|
+
lines.push(formatCostLine(this.state.cost));
|
|
476
463
|
}
|
|
477
464
|
if (this.state.changedFiles.length > 0) {
|
|
478
465
|
const maxFiles = 10;
|
|
@@ -496,18 +483,6 @@ class PinnedMessageManager {
|
|
|
496
483
|
}
|
|
497
484
|
return lines.join("\n");
|
|
498
485
|
}
|
|
499
|
-
/**
|
|
500
|
-
* Format token count (e.g., 150000 -> "150K")
|
|
501
|
-
*/
|
|
502
|
-
formatTokenCount(count) {
|
|
503
|
-
if (count >= 1000000) {
|
|
504
|
-
return `${(count / 1000000).toFixed(1)}M`;
|
|
505
|
-
}
|
|
506
|
-
else if (count >= 1000) {
|
|
507
|
-
return `${Math.round(count / 1000)}K`;
|
|
508
|
-
}
|
|
509
|
-
return count.toString();
|
|
510
|
-
}
|
|
511
486
|
/**
|
|
512
487
|
* Create and pin a new status message
|
|
513
488
|
*/
|
|
@@ -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) {
|