opencode-telegram-group-topics-bot 0.11.2
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 +74 -0
- package/LICENSE +21 -0
- package/README.md +305 -0
- package/dist/agent/manager.js +60 -0
- package/dist/agent/types.js +26 -0
- package/dist/app/start-bot-app.js +47 -0
- package/dist/bot/commands/abort.js +116 -0
- package/dist/bot/commands/commands.js +389 -0
- package/dist/bot/commands/constants.js +20 -0
- package/dist/bot/commands/definitions.js +25 -0
- package/dist/bot/commands/help.js +27 -0
- package/dist/bot/commands/models.js +38 -0
- package/dist/bot/commands/new.js +247 -0
- package/dist/bot/commands/opencode-start.js +85 -0
- package/dist/bot/commands/opencode-stop.js +44 -0
- package/dist/bot/commands/projects.js +304 -0
- package/dist/bot/commands/rename.js +173 -0
- package/dist/bot/commands/sessions.js +491 -0
- package/dist/bot/commands/start.js +67 -0
- package/dist/bot/commands/status.js +138 -0
- package/dist/bot/constants.js +49 -0
- package/dist/bot/handlers/agent.js +127 -0
- package/dist/bot/handlers/context.js +125 -0
- package/dist/bot/handlers/document.js +65 -0
- package/dist/bot/handlers/inline-menu.js +124 -0
- package/dist/bot/handlers/model.js +152 -0
- package/dist/bot/handlers/permission.js +281 -0
- package/dist/bot/handlers/prompt.js +263 -0
- package/dist/bot/handlers/question.js +285 -0
- package/dist/bot/handlers/variant.js +147 -0
- package/dist/bot/handlers/voice.js +173 -0
- package/dist/bot/index.js +945 -0
- package/dist/bot/message-patterns.js +4 -0
- package/dist/bot/middleware/auth.js +30 -0
- package/dist/bot/middleware/interaction-guard.js +80 -0
- package/dist/bot/middleware/unknown-command.js +22 -0
- package/dist/bot/scope.js +222 -0
- package/dist/bot/telegram-constants.js +3 -0
- package/dist/bot/telegram-rate-limiter.js +263 -0
- package/dist/bot/utils/commands.js +21 -0
- package/dist/bot/utils/file-download.js +91 -0
- package/dist/bot/utils/keyboard.js +85 -0
- package/dist/bot/utils/send-with-markdown-fallback.js +57 -0
- package/dist/bot/utils/session-error-filter.js +34 -0
- package/dist/bot/utils/topic-link.js +29 -0
- package/dist/cli/args.js +98 -0
- package/dist/cli.js +80 -0
- package/dist/config.js +103 -0
- package/dist/i18n/de.js +330 -0
- package/dist/i18n/en.js +330 -0
- package/dist/i18n/es.js +330 -0
- package/dist/i18n/index.js +102 -0
- package/dist/i18n/ru.js +330 -0
- package/dist/i18n/zh.js +330 -0
- package/dist/index.js +28 -0
- package/dist/interaction/cleanup.js +24 -0
- package/dist/interaction/constants.js +25 -0
- package/dist/interaction/guard.js +100 -0
- package/dist/interaction/manager.js +113 -0
- package/dist/interaction/types.js +1 -0
- package/dist/keyboard/manager.js +115 -0
- package/dist/keyboard/types.js +1 -0
- package/dist/model/capabilities.js +62 -0
- package/dist/model/manager.js +257 -0
- package/dist/model/types.js +24 -0
- package/dist/opencode/client.js +13 -0
- package/dist/opencode/events.js +159 -0
- package/dist/opencode/prompt-submit-error.js +101 -0
- package/dist/permission/manager.js +92 -0
- package/dist/permission/types.js +1 -0
- package/dist/pinned/manager.js +405 -0
- package/dist/pinned/types.js +1 -0
- package/dist/process/manager.js +273 -0
- package/dist/process/types.js +1 -0
- package/dist/project/manager.js +88 -0
- package/dist/question/manager.js +186 -0
- package/dist/question/types.js +1 -0
- package/dist/rename/manager.js +64 -0
- package/dist/runtime/bootstrap.js +350 -0
- package/dist/runtime/mode.js +74 -0
- package/dist/runtime/paths.js +37 -0
- package/dist/runtime/process-error-handlers.js +24 -0
- package/dist/session/cache-manager.js +455 -0
- package/dist/session/manager.js +87 -0
- package/dist/settings/manager.js +283 -0
- package/dist/stt/client.js +64 -0
- package/dist/summary/aggregator.js +625 -0
- package/dist/summary/formatter.js +417 -0
- package/dist/summary/tool-message-batcher.js +277 -0
- package/dist/topic/colors.js +8 -0
- package/dist/topic/constants.js +10 -0
- package/dist/topic/manager.js +161 -0
- package/dist/topic/title-constants.js +2 -0
- package/dist/topic/title-format.js +10 -0
- package/dist/topic/title-sync.js +17 -0
- package/dist/utils/error-format.js +29 -0
- package/dist/utils/logger.js +175 -0
- package/dist/utils/safe-background-task.js +33 -0
- package/dist/variant/manager.js +103 -0
- package/dist/variant/types.js +1 -0
- package/package.json +76 -0
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
import { normalizePathForDisplay, prepareCodeFile } from "./formatter.js";
|
|
2
|
+
import { logger } from "../utils/logger.js";
|
|
3
|
+
import { getSessionById } from "../session/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
|
+
function extractEventSessionId(event) {
|
|
28
|
+
const eventWithProperties = event;
|
|
29
|
+
if (typeof eventWithProperties.properties?.info?.sessionID === "string") {
|
|
30
|
+
return eventWithProperties.properties.info.sessionID;
|
|
31
|
+
}
|
|
32
|
+
if (typeof eventWithProperties.properties?.part?.sessionID === "string") {
|
|
33
|
+
return eventWithProperties.properties.part.sessionID;
|
|
34
|
+
}
|
|
35
|
+
if (typeof eventWithProperties.properties?.request?.sessionID === "string") {
|
|
36
|
+
return eventWithProperties.properties.request.sessionID;
|
|
37
|
+
}
|
|
38
|
+
if (typeof eventWithProperties.properties?.session?.id === "string") {
|
|
39
|
+
return eventWithProperties.properties.session.id;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
class SummaryAggregator {
|
|
44
|
+
trackedSessionIds = new Set();
|
|
45
|
+
currentMessageParts = new Map();
|
|
46
|
+
pendingParts = new Map();
|
|
47
|
+
messages = new Map();
|
|
48
|
+
messageCount = 0;
|
|
49
|
+
lastUpdated = 0;
|
|
50
|
+
onCompleteCallback = null;
|
|
51
|
+
onToolCallback = null;
|
|
52
|
+
onToolFileCallback = null;
|
|
53
|
+
onQuestionCallback = null;
|
|
54
|
+
onQuestionErrorCallback = null;
|
|
55
|
+
onThinkingCallback = null;
|
|
56
|
+
onTypingIndicatorCallback = null;
|
|
57
|
+
onTokensCallback = null;
|
|
58
|
+
onSessionCompactedCallback = null;
|
|
59
|
+
onSessionErrorCallback = null;
|
|
60
|
+
onSessionRetryCallback = null;
|
|
61
|
+
onPermissionCallback = null;
|
|
62
|
+
onSessionDiffCallback = null;
|
|
63
|
+
onFileChangeCallback = null;
|
|
64
|
+
onClearedCallback = null;
|
|
65
|
+
processedToolStates = new Set();
|
|
66
|
+
thinkingFiredForMessages = new Set();
|
|
67
|
+
typingTimer = null;
|
|
68
|
+
activeTypingSessions = new Set();
|
|
69
|
+
partHashes = new Map();
|
|
70
|
+
getMessageKey(sessionId, messageId) {
|
|
71
|
+
return `${sessionId}:${messageId}`;
|
|
72
|
+
}
|
|
73
|
+
isTrackedSession(sessionId) {
|
|
74
|
+
return this.trackedSessionIds.has(sessionId);
|
|
75
|
+
}
|
|
76
|
+
setOnComplete(callback) {
|
|
77
|
+
this.onCompleteCallback = callback;
|
|
78
|
+
}
|
|
79
|
+
setOnTool(callback) {
|
|
80
|
+
this.onToolCallback = callback;
|
|
81
|
+
}
|
|
82
|
+
setOnToolFile(callback) {
|
|
83
|
+
this.onToolFileCallback = callback;
|
|
84
|
+
}
|
|
85
|
+
setOnQuestion(callback) {
|
|
86
|
+
this.onQuestionCallback = callback;
|
|
87
|
+
}
|
|
88
|
+
setOnQuestionError(callback) {
|
|
89
|
+
this.onQuestionErrorCallback = callback;
|
|
90
|
+
}
|
|
91
|
+
setOnThinking(callback) {
|
|
92
|
+
this.onThinkingCallback = callback;
|
|
93
|
+
}
|
|
94
|
+
setOnTypingIndicator(callback) {
|
|
95
|
+
this.onTypingIndicatorCallback = callback;
|
|
96
|
+
}
|
|
97
|
+
setOnTokens(callback) {
|
|
98
|
+
this.onTokensCallback = callback;
|
|
99
|
+
}
|
|
100
|
+
setOnSessionCompacted(callback) {
|
|
101
|
+
this.onSessionCompactedCallback = callback;
|
|
102
|
+
}
|
|
103
|
+
setOnSessionError(callback) {
|
|
104
|
+
this.onSessionErrorCallback = callback;
|
|
105
|
+
}
|
|
106
|
+
setOnSessionRetry(callback) {
|
|
107
|
+
this.onSessionRetryCallback = callback;
|
|
108
|
+
}
|
|
109
|
+
setOnPermission(callback) {
|
|
110
|
+
this.onPermissionCallback = callback;
|
|
111
|
+
}
|
|
112
|
+
setOnSessionDiff(callback) {
|
|
113
|
+
this.onSessionDiffCallback = callback;
|
|
114
|
+
}
|
|
115
|
+
setOnFileChange(callback) {
|
|
116
|
+
this.onFileChangeCallback = callback;
|
|
117
|
+
}
|
|
118
|
+
setOnCleared(callback) {
|
|
119
|
+
this.onClearedCallback = callback;
|
|
120
|
+
}
|
|
121
|
+
hasActiveMessageForSession(sessionId) {
|
|
122
|
+
for (const value of this.messages.values()) {
|
|
123
|
+
if (value.sessionId === sessionId && value.role === "assistant") {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
emitTypingIndicators() {
|
|
130
|
+
const callback = this.onTypingIndicatorCallback;
|
|
131
|
+
if (!callback) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
for (const sessionId of this.activeTypingSessions) {
|
|
135
|
+
try {
|
|
136
|
+
callback(sessionId);
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
logger.error("[Aggregator] Typing callback failed", { sessionId }, error);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
startTypingIndicator(sessionId) {
|
|
144
|
+
this.activeTypingSessions.add(sessionId);
|
|
145
|
+
if (this.typingTimer) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
this.emitTypingIndicators();
|
|
149
|
+
this.typingTimer = setInterval(() => {
|
|
150
|
+
this.emitTypingIndicators();
|
|
151
|
+
}, 4000);
|
|
152
|
+
}
|
|
153
|
+
stopTypingIndicator(sessionId) {
|
|
154
|
+
if (sessionId) {
|
|
155
|
+
this.activeTypingSessions.delete(sessionId);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
this.activeTypingSessions.clear();
|
|
159
|
+
}
|
|
160
|
+
if (!this.typingTimer || this.activeTypingSessions.size > 0) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
clearInterval(this.typingTimer);
|
|
164
|
+
this.typingTimer = null;
|
|
165
|
+
}
|
|
166
|
+
processEvent(event) {
|
|
167
|
+
try {
|
|
168
|
+
// Log all question-related events for debugging
|
|
169
|
+
if (event.type.startsWith("question.")) {
|
|
170
|
+
logger.info(`[Aggregator] Question event: ${event.type}`, JSON.stringify(event.properties, null, 2));
|
|
171
|
+
}
|
|
172
|
+
// Log all session-related events for debugging
|
|
173
|
+
if (event.type.startsWith("session.")) {
|
|
174
|
+
logger.debug(`[Aggregator] Session event: ${event.type}`, JSON.stringify(event.properties, null, 2));
|
|
175
|
+
}
|
|
176
|
+
switch (event.type) {
|
|
177
|
+
case "message.updated":
|
|
178
|
+
this.handleMessageUpdated(event);
|
|
179
|
+
break;
|
|
180
|
+
case "message.part.updated":
|
|
181
|
+
this.handleMessagePartUpdated(event);
|
|
182
|
+
break;
|
|
183
|
+
case "session.status":
|
|
184
|
+
this.handleSessionStatus(event);
|
|
185
|
+
break;
|
|
186
|
+
case "session.idle":
|
|
187
|
+
this.handleSessionIdle(event);
|
|
188
|
+
break;
|
|
189
|
+
case "session.compacted":
|
|
190
|
+
this.handleSessionCompacted(event);
|
|
191
|
+
break;
|
|
192
|
+
case "session.error":
|
|
193
|
+
this.handleSessionError(event);
|
|
194
|
+
break;
|
|
195
|
+
case "question.asked":
|
|
196
|
+
this.handleQuestionAsked(event);
|
|
197
|
+
break;
|
|
198
|
+
case "question.replied":
|
|
199
|
+
logger.info(`[Aggregator] Question replied: requestID=${event.properties.requestID}`);
|
|
200
|
+
break;
|
|
201
|
+
case "question.rejected":
|
|
202
|
+
logger.info(`[Aggregator] Question rejected: requestID=${event.properties.requestID}`);
|
|
203
|
+
break;
|
|
204
|
+
case "session.diff":
|
|
205
|
+
this.handleSessionDiff(event);
|
|
206
|
+
break;
|
|
207
|
+
case "permission.asked":
|
|
208
|
+
this.handlePermissionAsked(event);
|
|
209
|
+
break;
|
|
210
|
+
case "permission.replied":
|
|
211
|
+
logger.info(`[Aggregator] Permission replied: requestID=${event.properties.requestID}`);
|
|
212
|
+
break;
|
|
213
|
+
default:
|
|
214
|
+
logger.debug(`[Aggregator] Unhandled event type: ${event.type}`);
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
logger.error("[Aggregator] Failed to process event", {
|
|
220
|
+
eventType: event.type,
|
|
221
|
+
sessionId: extractEventSessionId(event),
|
|
222
|
+
}, error);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
setSession(sessionId) {
|
|
226
|
+
this.trackedSessionIds.add(sessionId);
|
|
227
|
+
}
|
|
228
|
+
clearSession(sessionId) {
|
|
229
|
+
this.trackedSessionIds.delete(sessionId);
|
|
230
|
+
this.stopTypingIndicator(sessionId);
|
|
231
|
+
for (const [messageKey, message] of this.messages.entries()) {
|
|
232
|
+
if (message.sessionId !== sessionId) {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
this.messages.delete(messageKey);
|
|
236
|
+
this.currentMessageParts.delete(messageKey);
|
|
237
|
+
this.pendingParts.delete(messageKey);
|
|
238
|
+
this.partHashes.delete(messageKey);
|
|
239
|
+
this.thinkingFiredForMessages.delete(messageKey);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
clear() {
|
|
243
|
+
this.stopTypingIndicator();
|
|
244
|
+
this.trackedSessionIds.clear();
|
|
245
|
+
this.currentMessageParts.clear();
|
|
246
|
+
this.pendingParts.clear();
|
|
247
|
+
this.messages.clear();
|
|
248
|
+
this.partHashes.clear();
|
|
249
|
+
this.processedToolStates.clear();
|
|
250
|
+
this.thinkingFiredForMessages.clear();
|
|
251
|
+
this.messageCount = 0;
|
|
252
|
+
this.lastUpdated = 0;
|
|
253
|
+
if (this.onClearedCallback) {
|
|
254
|
+
try {
|
|
255
|
+
this.onClearedCallback();
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
logger.error("[Aggregator] Error in clear callback:", err);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
handleMessageUpdated(event) {
|
|
263
|
+
const { info } = event.properties;
|
|
264
|
+
if (!this.isTrackedSession(info.sessionID)) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const messageID = info.id;
|
|
268
|
+
const messageKey = this.getMessageKey(info.sessionID, messageID);
|
|
269
|
+
this.messages.set(messageKey, { role: info.role, sessionId: info.sessionID });
|
|
270
|
+
if (info.role === "assistant") {
|
|
271
|
+
if (!this.currentMessageParts.has(messageKey)) {
|
|
272
|
+
this.currentMessageParts.set(messageKey, []);
|
|
273
|
+
this.messageCount++;
|
|
274
|
+
this.startTypingIndicator(info.sessionID);
|
|
275
|
+
}
|
|
276
|
+
const pending = this.pendingParts.get(messageKey) || [];
|
|
277
|
+
const current = this.currentMessageParts.get(messageKey) || [];
|
|
278
|
+
this.currentMessageParts.set(messageKey, [...current, ...pending]);
|
|
279
|
+
this.pendingParts.delete(messageKey);
|
|
280
|
+
const assistantMessage = info;
|
|
281
|
+
const time = assistantMessage.time;
|
|
282
|
+
if (time?.completed) {
|
|
283
|
+
const parts = this.currentMessageParts.get(messageKey) || [];
|
|
284
|
+
const lastPart = parts[parts.length - 1] || "";
|
|
285
|
+
logger.debug(`[Aggregator] Message part completed: messageId=${messageID}, textLength=${lastPart.length}, totalParts=${parts.length}, session=${info.sessionID}`);
|
|
286
|
+
// Extract and report tokens BEFORE onComplete so keyboard context is updated
|
|
287
|
+
const assistantInfo = info;
|
|
288
|
+
if (this.onTokensCallback && assistantInfo.tokens) {
|
|
289
|
+
const tokens = {
|
|
290
|
+
input: assistantInfo.tokens.input,
|
|
291
|
+
output: assistantInfo.tokens.output,
|
|
292
|
+
reasoning: assistantInfo.tokens.reasoning,
|
|
293
|
+
cacheRead: assistantInfo.tokens.cache?.read || 0,
|
|
294
|
+
cacheWrite: assistantInfo.tokens.cache?.write || 0,
|
|
295
|
+
};
|
|
296
|
+
logger.debug(`[Aggregator] Tokens: input=${tokens.input}, output=${tokens.output}, reasoning=${tokens.reasoning}`);
|
|
297
|
+
// Call synchronously so keyboardManager is updated before onComplete sends the reply
|
|
298
|
+
this.onTokensCallback(info.sessionID, tokens);
|
|
299
|
+
}
|
|
300
|
+
if (this.onCompleteCallback && lastPart.length > 0) {
|
|
301
|
+
this.onCompleteCallback(info.sessionID, lastPart);
|
|
302
|
+
}
|
|
303
|
+
this.currentMessageParts.delete(messageKey);
|
|
304
|
+
this.messages.delete(messageKey);
|
|
305
|
+
this.partHashes.delete(messageKey);
|
|
306
|
+
logger.debug(`[Aggregator] Message completed cleanup: remaining messages=${this.currentMessageParts.size}`);
|
|
307
|
+
if (!this.hasActiveMessageForSession(info.sessionID)) {
|
|
308
|
+
logger.debug(`[Aggregator] No more active messages for session ${info.sessionID}, stopping typing indicator`);
|
|
309
|
+
this.stopTypingIndicator(info.sessionID);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
this.lastUpdated = Date.now();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
handleMessagePartUpdated(event) {
|
|
316
|
+
const { part } = event.properties;
|
|
317
|
+
if (!this.isTrackedSession(part.sessionID)) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const messageID = part.messageID;
|
|
321
|
+
const messageKey = this.getMessageKey(part.sessionID, messageID);
|
|
322
|
+
const messageInfo = this.messages.get(messageKey);
|
|
323
|
+
if (part.type === "reasoning") {
|
|
324
|
+
// Fire the thinking callback once per message on the first reasoning part.
|
|
325
|
+
// This is the signal that the model is actually doing extended thinking.
|
|
326
|
+
if (!this.thinkingFiredForMessages.has(messageKey) && this.onThinkingCallback) {
|
|
327
|
+
this.thinkingFiredForMessages.add(messageKey);
|
|
328
|
+
const callback = this.onThinkingCallback;
|
|
329
|
+
const sessionID = part.sessionID;
|
|
330
|
+
setImmediate(() => {
|
|
331
|
+
if (typeof callback === "function") {
|
|
332
|
+
callback(sessionID);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
else if (part.type === "text" && "text" in part && part.text) {
|
|
338
|
+
const partHash = this.hashString(part.text);
|
|
339
|
+
if (!this.partHashes.has(messageKey)) {
|
|
340
|
+
this.partHashes.set(messageKey, new Set());
|
|
341
|
+
}
|
|
342
|
+
const hashes = this.partHashes.get(messageKey);
|
|
343
|
+
if (hashes.has(partHash)) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
hashes.add(partHash);
|
|
347
|
+
if (messageInfo && messageInfo.role === "assistant") {
|
|
348
|
+
if (!this.currentMessageParts.has(messageKey)) {
|
|
349
|
+
this.currentMessageParts.set(messageKey, []);
|
|
350
|
+
this.startTypingIndicator(part.sessionID);
|
|
351
|
+
}
|
|
352
|
+
const parts = this.currentMessageParts.get(messageKey);
|
|
353
|
+
parts.push(part.text);
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
if (!this.pendingParts.has(messageKey)) {
|
|
357
|
+
this.pendingParts.set(messageKey, []);
|
|
358
|
+
}
|
|
359
|
+
const pending = this.pendingParts.get(messageKey);
|
|
360
|
+
pending.push(part.text);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
else if (part.type === "tool") {
|
|
364
|
+
const state = part.state;
|
|
365
|
+
const input = "input" in state ? state.input : undefined;
|
|
366
|
+
const title = "title" in state ? state.title : undefined;
|
|
367
|
+
logger.debug(`[Aggregator] Tool event: callID=${part.callID}, tool=${part.tool}, status=${"status" in state ? state.status : "unknown"}`);
|
|
368
|
+
if (part.tool === "question") {
|
|
369
|
+
logger.debug(`[Aggregator] Question tool part update:`, JSON.stringify(part, null, 2));
|
|
370
|
+
// If the question tool fails, clear the active poll
|
|
371
|
+
// so the agent can recreate it with corrected data
|
|
372
|
+
if ("status" in state && state.status === "error") {
|
|
373
|
+
logger.info(`[Aggregator] Question tool failed with error, clearing active poll. callID=${part.callID}`);
|
|
374
|
+
if (this.onQuestionErrorCallback) {
|
|
375
|
+
setImmediate(() => {
|
|
376
|
+
this.onQuestionErrorCallback();
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
// NOTE: Questions are now handled via "question.asked" event, not via tool part updates.
|
|
382
|
+
// This ensures we have access to the requestID needed for question.reply().
|
|
383
|
+
}
|
|
384
|
+
if ("status" in state && state.status === "completed") {
|
|
385
|
+
logger.debug(`[Aggregator] Tool completed: callID=${part.callID}, tool=${part.tool}`, JSON.stringify(state, null, 2));
|
|
386
|
+
const completedKey = `completed-${part.callID}`;
|
|
387
|
+
if (!this.processedToolStates.has(completedKey)) {
|
|
388
|
+
this.processedToolStates.add(completedKey);
|
|
389
|
+
const preparedFileContext = this.prepareToolFileContext(part.tool, input, title, state.metadata);
|
|
390
|
+
const toolData = {
|
|
391
|
+
sessionId: part.sessionID,
|
|
392
|
+
messageId: messageID,
|
|
393
|
+
callId: part.callID,
|
|
394
|
+
tool: part.tool,
|
|
395
|
+
state: part.state,
|
|
396
|
+
input,
|
|
397
|
+
title,
|
|
398
|
+
metadata: state.metadata,
|
|
399
|
+
hasFileAttachment: !!preparedFileContext.fileData,
|
|
400
|
+
};
|
|
401
|
+
logger.debug(`[Aggregator] Sending tool notification to Telegram: tool=${part.tool}, title=${title || "N/A"}`);
|
|
402
|
+
if (this.onToolCallback) {
|
|
403
|
+
this.onToolCallback(toolData);
|
|
404
|
+
}
|
|
405
|
+
if (preparedFileContext.fileData && this.onToolFileCallback) {
|
|
406
|
+
logger.debug(`[Aggregator] Sending ${part.tool} file: ${preparedFileContext.fileData.filename} (${preparedFileContext.fileData.buffer.length} bytes)`);
|
|
407
|
+
this.onToolFileCallback({
|
|
408
|
+
...toolData,
|
|
409
|
+
hasFileAttachment: true,
|
|
410
|
+
fileData: preparedFileContext.fileData,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
if (preparedFileContext.fileChange && this.onFileChangeCallback) {
|
|
414
|
+
this.onFileChangeCallback(preparedFileContext.fileChange, toolData.sessionId);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
this.lastUpdated = Date.now();
|
|
420
|
+
}
|
|
421
|
+
prepareToolFileContext(tool, input, title, metadata) {
|
|
422
|
+
if (tool === "write" && input) {
|
|
423
|
+
const filePath = typeof input.filePath === "string" ? normalizePathForDisplay(input.filePath) : "";
|
|
424
|
+
const hasContent = typeof input.content === "string";
|
|
425
|
+
const content = hasContent ? input.content : "";
|
|
426
|
+
if (!filePath || !hasContent) {
|
|
427
|
+
return { fileData: null, fileChange: null };
|
|
428
|
+
}
|
|
429
|
+
return {
|
|
430
|
+
fileData: prepareCodeFile(content, filePath, "write"),
|
|
431
|
+
fileChange: {
|
|
432
|
+
file: filePath,
|
|
433
|
+
additions: content.split("\n").length,
|
|
434
|
+
deletions: 0,
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
if (tool === "edit" && metadata) {
|
|
439
|
+
const editMetadata = metadata;
|
|
440
|
+
const filePath = editMetadata.filediff?.file
|
|
441
|
+
? normalizePathForDisplay(editMetadata.filediff.file)
|
|
442
|
+
: "";
|
|
443
|
+
const diffText = typeof editMetadata.diff === "string" ? editMetadata.diff : "";
|
|
444
|
+
if (!filePath || !diffText) {
|
|
445
|
+
return { fileData: null, fileChange: null };
|
|
446
|
+
}
|
|
447
|
+
return {
|
|
448
|
+
fileData: prepareCodeFile(diffText, filePath, "edit"),
|
|
449
|
+
fileChange: {
|
|
450
|
+
file: filePath,
|
|
451
|
+
additions: editMetadata.filediff?.additions || 0,
|
|
452
|
+
deletions: editMetadata.filediff?.deletions || 0,
|
|
453
|
+
},
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
if (tool === "apply_patch") {
|
|
457
|
+
const patchMetadata = metadata;
|
|
458
|
+
const filePathFromInput = input && typeof input.filePath === "string"
|
|
459
|
+
? normalizePathForDisplay(input.filePath)
|
|
460
|
+
: input && typeof input.path === "string"
|
|
461
|
+
? normalizePathForDisplay(input.path)
|
|
462
|
+
: "";
|
|
463
|
+
const filePathFromTitle = title ? extractFirstUpdatedFileFromTitle(title) : "";
|
|
464
|
+
const filePath = (patchMetadata?.filediff?.file && normalizePathForDisplay(patchMetadata.filediff.file)) ||
|
|
465
|
+
filePathFromInput ||
|
|
466
|
+
normalizePathForDisplay(filePathFromTitle);
|
|
467
|
+
const diffText = typeof patchMetadata?.diff === "string"
|
|
468
|
+
? patchMetadata.diff
|
|
469
|
+
: input && typeof input.patchText === "string"
|
|
470
|
+
? input.patchText
|
|
471
|
+
: "";
|
|
472
|
+
if (!filePath) {
|
|
473
|
+
return { fileData: null, fileChange: null };
|
|
474
|
+
}
|
|
475
|
+
const fileChange = patchMetadata?.filediff
|
|
476
|
+
? {
|
|
477
|
+
file: filePath,
|
|
478
|
+
additions: patchMetadata.filediff.additions || 0,
|
|
479
|
+
deletions: patchMetadata.filediff.deletions || 0,
|
|
480
|
+
}
|
|
481
|
+
: diffText
|
|
482
|
+
? (() => {
|
|
483
|
+
const changes = countDiffChangesFromText(diffText);
|
|
484
|
+
return {
|
|
485
|
+
file: filePath,
|
|
486
|
+
additions: changes.additions,
|
|
487
|
+
deletions: changes.deletions,
|
|
488
|
+
};
|
|
489
|
+
})()
|
|
490
|
+
: null;
|
|
491
|
+
return {
|
|
492
|
+
fileData: diffText ? prepareCodeFile(diffText, filePath, "edit") : null,
|
|
493
|
+
fileChange,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
return { fileData: null, fileChange: null };
|
|
497
|
+
}
|
|
498
|
+
hashString(str) {
|
|
499
|
+
let hash = 0;
|
|
500
|
+
for (let i = 0; i < str.length; i++) {
|
|
501
|
+
const char = str.charCodeAt(i);
|
|
502
|
+
hash = (hash << 5) - hash + char;
|
|
503
|
+
hash = hash & hash;
|
|
504
|
+
}
|
|
505
|
+
return hash.toString(36);
|
|
506
|
+
}
|
|
507
|
+
handleSessionStatus(event) {
|
|
508
|
+
const { sessionID, status } = event.properties;
|
|
509
|
+
if (!this.isTrackedSession(sessionID)) {
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
if (status?.type !== "retry" || !this.onSessionRetryCallback) {
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
const callback = this.onSessionRetryCallback;
|
|
516
|
+
const message = status.message?.trim() || "Unknown retry error";
|
|
517
|
+
logger.warn(`[Aggregator] Session retry: session=${sessionID}, attempt=${status.attempt ?? "n/a"}, message=${message}`);
|
|
518
|
+
setImmediate(() => {
|
|
519
|
+
callback({
|
|
520
|
+
sessionId: sessionID,
|
|
521
|
+
attempt: status.attempt,
|
|
522
|
+
message,
|
|
523
|
+
next: status.next,
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
handleSessionIdle(event) {
|
|
528
|
+
const { sessionID } = event.properties;
|
|
529
|
+
if (!this.isTrackedSession(sessionID)) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
logger.info(`[Aggregator] Session became idle: ${sessionID}`);
|
|
533
|
+
// Stop typing indicator when session goes idle
|
|
534
|
+
this.stopTypingIndicator();
|
|
535
|
+
}
|
|
536
|
+
handleSessionCompacted(event) {
|
|
537
|
+
const properties = event.properties;
|
|
538
|
+
const { sessionID } = properties;
|
|
539
|
+
if (!this.isTrackedSession(sessionID)) {
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
logger.info(`[Aggregator] Session compacted: ${sessionID}`);
|
|
543
|
+
// Reload context from history after compaction
|
|
544
|
+
if (this.onSessionCompactedCallback) {
|
|
545
|
+
setImmediate(() => {
|
|
546
|
+
const session = getSessionById(sessionID);
|
|
547
|
+
if (session?.directory) {
|
|
548
|
+
this.onSessionCompactedCallback(sessionID, session.directory);
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
handleSessionError(event) {
|
|
554
|
+
const { sessionID, error } = event.properties;
|
|
555
|
+
if (!this.isTrackedSession(sessionID)) {
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
const message = error?.data?.message || error?.message || error?.name || "Unknown session error";
|
|
559
|
+
logger.warn(`[Aggregator] Session error: ${sessionID}: ${message}`);
|
|
560
|
+
this.stopTypingIndicator();
|
|
561
|
+
if (this.onSessionErrorCallback) {
|
|
562
|
+
const callback = this.onSessionErrorCallback;
|
|
563
|
+
setImmediate(() => {
|
|
564
|
+
callback(sessionID, message);
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
handleQuestionAsked(event) {
|
|
569
|
+
const { id, sessionID, questions } = event.properties;
|
|
570
|
+
if (!this.isTrackedSession(sessionID)) {
|
|
571
|
+
logger.debug(`[Aggregator] Ignoring question.asked for untracked session: ${sessionID}`);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
logger.info(`[Aggregator] Question asked: requestID=${id}, questions=${questions.length}`);
|
|
575
|
+
if (this.onQuestionCallback) {
|
|
576
|
+
const callback = this.onQuestionCallback;
|
|
577
|
+
setImmediate(async () => {
|
|
578
|
+
try {
|
|
579
|
+
await callback(sessionID, questions, id);
|
|
580
|
+
}
|
|
581
|
+
catch (err) {
|
|
582
|
+
logger.error("[Aggregator] Error in question callback:", err);
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
handleSessionDiff(event) {
|
|
588
|
+
const properties = event.properties;
|
|
589
|
+
if (!this.isTrackedSession(properties.sessionID)) {
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
logger.debug(`[Aggregator] Session diff: ${properties.diff.length} files changed`);
|
|
593
|
+
if (this.onSessionDiffCallback) {
|
|
594
|
+
const diffs = properties.diff.map((d) => ({
|
|
595
|
+
file: d.file,
|
|
596
|
+
additions: d.additions,
|
|
597
|
+
deletions: d.deletions,
|
|
598
|
+
}));
|
|
599
|
+
const callback = this.onSessionDiffCallback;
|
|
600
|
+
setImmediate(() => {
|
|
601
|
+
callback(properties.sessionID, diffs);
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
handlePermissionAsked(event) {
|
|
606
|
+
const request = event.properties;
|
|
607
|
+
if (!this.isTrackedSession(request.sessionID)) {
|
|
608
|
+
logger.debug(`[Aggregator] Ignoring permission.asked for untracked session: ${request.sessionID}`);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
logger.info(`[Aggregator] Permission asked: requestID=${request.id}, type=${request.permission}, patterns=${request.patterns.length}`);
|
|
612
|
+
if (this.onPermissionCallback) {
|
|
613
|
+
const callback = this.onPermissionCallback;
|
|
614
|
+
setImmediate(async () => {
|
|
615
|
+
try {
|
|
616
|
+
await callback(request);
|
|
617
|
+
}
|
|
618
|
+
catch (err) {
|
|
619
|
+
logger.error("[Aggregator] Error in permission callback:", err);
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
export const summaryAggregator = new SummaryAggregator();
|