@fonz/tgcc 0.6.17 → 0.6.18
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/README.md +74 -50
- package/dist/bridge.d.ts +19 -7
- package/dist/bridge.js +882 -646
- package/dist/bridge.js.map +1 -1
- package/dist/cc-process.js +7 -0
- package/dist/cc-process.js.map +1 -1
- package/dist/ctl-server.d.ts +4 -0
- package/dist/ctl-server.js +30 -4
- package/dist/ctl-server.js.map +1 -1
- package/dist/event-buffer.d.ts +27 -0
- package/dist/event-buffer.js +50 -0
- package/dist/event-buffer.js.map +1 -0
- package/dist/high-signal.d.ts +53 -0
- package/dist/high-signal.js +391 -0
- package/dist/high-signal.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-bridge.d.ts +3 -5
- package/dist/mcp-server.js +80 -0
- package/dist/mcp-server.js.map +1 -1
- package/dist/session.d.ts +13 -8
- package/dist/session.js +61 -37
- package/dist/session.js.map +1 -1
- package/dist/streaming.d.ts +20 -7
- package/dist/streaming.js +232 -112
- package/dist/streaming.js.map +1 -1
- package/dist/telegram.d.ts +3 -1
- package/dist/telegram.js +18 -1
- package/dist/telegram.js.map +1 -1
- package/package.json +1 -1
package/dist/bridge.js
CHANGED
|
@@ -6,13 +6,16 @@ import pino from 'pino';
|
|
|
6
6
|
import { resolveUserConfig, resolveRepoPath, updateConfig, isValidRepoName, findRepoOwner } from './config.js';
|
|
7
7
|
import { CCProcess, generateMcpConfig } from './cc-process.js';
|
|
8
8
|
import { createTextMessage, createImageMessage, createDocumentMessage, } from './cc-protocol.js';
|
|
9
|
-
import { StreamAccumulator, SubAgentTracker, escapeHtml } from './streaming.js';
|
|
9
|
+
import { StreamAccumulator, SubAgentTracker, escapeHtml, formatSystemMessage } from './streaming.js';
|
|
10
10
|
import { TelegramBot } from './telegram.js';
|
|
11
11
|
import { InlineKeyboard } from 'grammy';
|
|
12
12
|
import { McpBridgeServer } from './mcp-bridge.js';
|
|
13
13
|
import { SessionStore, discoverCCSessions, } from './session.js';
|
|
14
14
|
import { CtlServer, } from './ctl-server.js';
|
|
15
15
|
import { ProcessRegistry } from './process-registry.js';
|
|
16
|
+
import { EventBuffer } from './event-buffer.js';
|
|
17
|
+
import { HighSignalDetector } from './high-signal.js';
|
|
18
|
+
import { randomUUID } from 'node:crypto';
|
|
16
19
|
// ── Message Batcher ──
|
|
17
20
|
class MessageBatcher {
|
|
18
21
|
pending = [];
|
|
@@ -100,11 +103,18 @@ export class Bridge extends EventEmitter {
|
|
|
100
103
|
config;
|
|
101
104
|
agents = new Map();
|
|
102
105
|
processRegistry = new ProcessRegistry();
|
|
103
|
-
sessionModelOverrides = new Map(); // "agentId:userId" → model (from /model cmd, not persisted)
|
|
104
106
|
mcpServer;
|
|
105
107
|
ctlServer;
|
|
106
108
|
sessionStore;
|
|
107
109
|
logger;
|
|
110
|
+
// High-signal event detection
|
|
111
|
+
highSignalDetector;
|
|
112
|
+
// Supervisor protocol
|
|
113
|
+
supervisorWrite = null;
|
|
114
|
+
supervisorAgentId = null;
|
|
115
|
+
supervisorSubscriptions = new Set(); // "agentId:sessionId" or "agentId:*"
|
|
116
|
+
suppressExitForProcess = new Set(); // sessionIds where takeover suppresses exit event
|
|
117
|
+
supervisorPendingRequests = new Map();
|
|
108
118
|
constructor(config, logger) {
|
|
109
119
|
super();
|
|
110
120
|
this.config = config;
|
|
@@ -112,6 +122,18 @@ export class Bridge extends EventEmitter {
|
|
|
112
122
|
this.sessionStore = new SessionStore(config.global.stateFile, this.logger);
|
|
113
123
|
this.mcpServer = new McpBridgeServer((req) => this.handleMcpToolRequest(req), this.logger);
|
|
114
124
|
this.ctlServer = new CtlServer(this, this.logger);
|
|
125
|
+
this.highSignalDetector = new HighSignalDetector({
|
|
126
|
+
emitSupervisorEvent: (event) => {
|
|
127
|
+
if (this.isSupervisorSubscribed(event.agentId, this.agents.get(event.agentId)?.ccProcess?.sessionId ?? null)) {
|
|
128
|
+
this.sendToSupervisor(event);
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
pushEventBuffer: (agentId, line) => {
|
|
132
|
+
const agent = this.agents.get(agentId);
|
|
133
|
+
if (agent)
|
|
134
|
+
agent.eventBuffer.push(line);
|
|
135
|
+
},
|
|
136
|
+
});
|
|
115
137
|
}
|
|
116
138
|
// ── Startup ──
|
|
117
139
|
async start() {
|
|
@@ -120,20 +142,37 @@ export class Bridge extends EventEmitter {
|
|
|
120
142
|
await this.startAgent(agentId, agentConfig);
|
|
121
143
|
}
|
|
122
144
|
this.logger.info({ agents: Object.keys(this.config.agents) }, 'Bridge started');
|
|
145
|
+
// Emit bridge_started event to supervisor
|
|
146
|
+
this.sendToSupervisor({
|
|
147
|
+
type: 'event',
|
|
148
|
+
event: 'bridge_started',
|
|
149
|
+
agents: Object.keys(this.config.agents),
|
|
150
|
+
uptime: 0,
|
|
151
|
+
});
|
|
123
152
|
}
|
|
124
153
|
async startAgent(agentId, agentConfig) {
|
|
125
154
|
this.logger.info({ agentId }, 'Starting agent');
|
|
126
155
|
const tgBot = new TelegramBot(agentId, agentConfig, this.config.global.mediaDir, (msg) => this.handleTelegramMessage(agentId, msg), (cmd) => this.handleSlashCommand(agentId, cmd), this.logger, (query) => this.handleCallbackQuery(agentId, query));
|
|
156
|
+
// Resolve initial repo and model from config + persisted state
|
|
157
|
+
const agentState = this.sessionStore.getAgent(agentId);
|
|
158
|
+
const configDefaults = agentConfig.defaults;
|
|
127
159
|
const instance = {
|
|
128
160
|
id: agentId,
|
|
129
161
|
config: agentConfig,
|
|
130
162
|
tgBot,
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
163
|
+
ephemeral: false,
|
|
164
|
+
repo: agentState.repo || configDefaults.repo,
|
|
165
|
+
model: agentState.model || configDefaults.model,
|
|
166
|
+
ccProcess: null,
|
|
167
|
+
accumulator: null,
|
|
168
|
+
subAgentTracker: null,
|
|
169
|
+
batcher: null,
|
|
135
170
|
pendingPermissions: new Map(),
|
|
136
|
-
|
|
171
|
+
typingInterval: null,
|
|
172
|
+
typingChatId: null,
|
|
173
|
+
pendingSessionId: null,
|
|
174
|
+
destroyTimer: null,
|
|
175
|
+
eventBuffer: new EventBuffer(),
|
|
137
176
|
};
|
|
138
177
|
this.agents.set(agentId, instance);
|
|
139
178
|
await tgBot.start();
|
|
@@ -174,67 +213,62 @@ export class Bridge extends EventEmitter {
|
|
|
174
213
|
const agent = this.agents.get(agentId);
|
|
175
214
|
if (!agent)
|
|
176
215
|
return;
|
|
177
|
-
this.logger.info({ agentId }, 'Stopping agent');
|
|
178
|
-
// Stop bot
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
216
|
+
this.logger.info({ agentId, ephemeral: agent.ephemeral }, 'Stopping agent');
|
|
217
|
+
// Stop bot (persistent agents only)
|
|
218
|
+
if (agent.tgBot)
|
|
219
|
+
await agent.tgBot.stop();
|
|
220
|
+
// Clear auto-destroy timer (ephemeral agents)
|
|
221
|
+
if (agent.destroyTimer) {
|
|
222
|
+
clearTimeout(agent.destroyTimer);
|
|
223
|
+
agent.destroyTimer = null;
|
|
185
224
|
}
|
|
186
|
-
// Kill
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
// Create a promise that resolves when the process exits or times out
|
|
225
|
+
// Kill CC process if active
|
|
226
|
+
if (agent.ccProcess) {
|
|
227
|
+
const proc = agent.ccProcess;
|
|
190
228
|
const exitPromise = new Promise((resolve) => {
|
|
191
229
|
const onExit = () => {
|
|
192
230
|
proc.off('exit', onExit);
|
|
193
231
|
resolve();
|
|
194
232
|
};
|
|
195
233
|
proc.on('exit', onExit);
|
|
196
|
-
// Timeout after 3 seconds if process doesn't exit
|
|
197
234
|
const timeoutId = setTimeout(() => {
|
|
198
235
|
proc.off('exit', onExit);
|
|
199
236
|
resolve();
|
|
200
237
|
}, 3000);
|
|
201
|
-
// Clear timeout if process exits before timeout
|
|
202
238
|
proc.on('exit', () => clearTimeout(timeoutId));
|
|
203
239
|
});
|
|
204
|
-
processExitPromises.push(exitPromise);
|
|
205
240
|
proc.destroy();
|
|
241
|
+
await exitPromise;
|
|
242
|
+
// Unsubscribe from registry
|
|
243
|
+
const clientRef = { agentId, userId: agentId, chatId: 0 };
|
|
244
|
+
this.processRegistry.unsubscribe(clientRef);
|
|
206
245
|
}
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
batcher.cancel();
|
|
212
|
-
batcher.destroy();
|
|
213
|
-
}
|
|
214
|
-
// Close MCP sockets
|
|
215
|
-
for (const userId of agent.processes.keys()) {
|
|
216
|
-
const socketPath = join(this.config.global.socketDir, `${agentId}-${userId}.sock`);
|
|
217
|
-
this.mcpServer.close(socketPath);
|
|
246
|
+
// Cancel batcher
|
|
247
|
+
if (agent.batcher) {
|
|
248
|
+
agent.batcher.cancel();
|
|
249
|
+
agent.batcher.destroy();
|
|
218
250
|
}
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
251
|
+
// Close MCP socket
|
|
252
|
+
const socketPath = join(this.config.global.socketDir, `${agentId}.sock`);
|
|
253
|
+
this.mcpServer.close(socketPath);
|
|
254
|
+
// Clean up accumulator
|
|
255
|
+
if (agent.accumulator) {
|
|
256
|
+
agent.accumulator.finalize();
|
|
257
|
+
agent.accumulator = null;
|
|
222
258
|
}
|
|
223
|
-
agent
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
259
|
+
// Clean up sub-agent tracker
|
|
260
|
+
if (agent.subAgentTracker) {
|
|
261
|
+
agent.subAgentTracker.reset();
|
|
262
|
+
agent.subAgentTracker = null;
|
|
227
263
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
clearInterval(interval);
|
|
264
|
+
// Clear typing indicator
|
|
265
|
+
if (agent.typingInterval) {
|
|
266
|
+
clearInterval(agent.typingInterval);
|
|
267
|
+
agent.typingInterval = null;
|
|
233
268
|
}
|
|
234
|
-
agent.typingIntervals.clear();
|
|
235
269
|
agent.pendingPermissions.clear();
|
|
236
|
-
agent.
|
|
237
|
-
agent.
|
|
270
|
+
agent.ccProcess = null;
|
|
271
|
+
agent.batcher = null;
|
|
238
272
|
// Close control socket
|
|
239
273
|
const ctlSocketPath = join('/tmp/tgcc/ctl', `${agentId}.sock`);
|
|
240
274
|
this.ctlServer.close(ctlSocketPath);
|
|
@@ -246,19 +280,18 @@ export class Bridge extends EventEmitter {
|
|
|
246
280
|
if (!agent)
|
|
247
281
|
return;
|
|
248
282
|
this.logger.debug({ agentId, userId: msg.userId, type: msg.type }, 'TG message received');
|
|
249
|
-
// Ensure batcher exists
|
|
250
|
-
if (!agent.
|
|
251
|
-
agent.
|
|
252
|
-
this.sendToCC(agentId,
|
|
253
|
-
})
|
|
283
|
+
// Ensure batcher exists (one per agent, not per user)
|
|
284
|
+
if (!agent.batcher) {
|
|
285
|
+
agent.batcher = new MessageBatcher(2000, (combined) => {
|
|
286
|
+
this.sendToCC(agentId, combined, { chatId: msg.chatId, spawnSource: 'telegram' });
|
|
287
|
+
});
|
|
254
288
|
}
|
|
255
289
|
// Prepare text with reply context
|
|
256
290
|
let text = msg.text;
|
|
257
291
|
if (msg.replyToText) {
|
|
258
292
|
text = `[Replying to: '${msg.replyToText}']\n\n${text}`;
|
|
259
293
|
}
|
|
260
|
-
|
|
261
|
-
batcher.add({
|
|
294
|
+
agent.batcher.add({
|
|
262
295
|
text,
|
|
263
296
|
imageBase64: msg.imageBase64,
|
|
264
297
|
imageMediaType: msg.imageMediaType,
|
|
@@ -266,7 +299,7 @@ export class Bridge extends EventEmitter {
|
|
|
266
299
|
fileName: msg.fileName,
|
|
267
300
|
});
|
|
268
301
|
}
|
|
269
|
-
sendToCC(agentId,
|
|
302
|
+
sendToCC(agentId, data, source) {
|
|
270
303
|
const agent = this.agents.get(agentId);
|
|
271
304
|
if (!agent)
|
|
272
305
|
return;
|
|
@@ -281,378 +314,306 @@ export class Bridge extends EventEmitter {
|
|
|
281
314
|
else {
|
|
282
315
|
ccMsg = createTextMessage(data.text);
|
|
283
316
|
}
|
|
284
|
-
|
|
285
|
-
// Check if this client already has a process via the registry
|
|
286
|
-
let existingEntry = this.processRegistry.findByClient(clientRef);
|
|
287
|
-
let proc = existingEntry?.ccProcess;
|
|
317
|
+
let proc = agent.ccProcess;
|
|
288
318
|
if (proc?.takenOver) {
|
|
289
319
|
// Session was taken over externally — discard old process
|
|
290
|
-
const entry = this.processRegistry.
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
}
|
|
296
|
-
// Staleness check: detect if session was modified by another client
|
|
297
|
-
// Skip if background sub-agents are running — their results grow the JSONL
|
|
298
|
-
// and would cause false-positive staleness detection
|
|
299
|
-
const accKey2 = `${userId}:${chatId}`;
|
|
300
|
-
const activeTracker = agent.subAgentTrackers.get(accKey2);
|
|
301
|
-
const hasBackgroundAgents = activeTracker?.hasDispatchedAgents ?? false;
|
|
302
|
-
if (proc && proc.state !== 'idle' && !hasBackgroundAgents) {
|
|
303
|
-
const staleInfo = this.checkSessionStaleness(agentId, userId);
|
|
304
|
-
if (staleInfo) {
|
|
305
|
-
// Session was modified externally — silently reconnect for roaming support
|
|
306
|
-
this.logger.info({ agentId, userId }, 'Session modified externally — reconnecting for roaming');
|
|
307
|
-
const entry = this.processRegistry.findByClient(clientRef);
|
|
308
|
-
this.processRegistry.unsubscribe(clientRef);
|
|
309
|
-
agent.processes.delete(userId);
|
|
310
|
-
proc = undefined;
|
|
311
|
-
existingEntry = null;
|
|
312
|
-
}
|
|
320
|
+
const entry = this.processRegistry.findByProcess(proc);
|
|
321
|
+
if (entry)
|
|
322
|
+
this.processRegistry.destroy(entry.repo, entry.sessionId);
|
|
323
|
+
agent.ccProcess = null;
|
|
324
|
+
proc = null;
|
|
313
325
|
}
|
|
314
326
|
if (!proc || proc.state === 'idle') {
|
|
315
|
-
// Warn if no repo is configured
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}
|
|
321
|
-
// Check if another client already has a process for this repo+session
|
|
322
|
-
const sessionId = userState2.currentSessionId;
|
|
323
|
-
if (sessionId && resolvedRepo) {
|
|
324
|
-
const sharedEntry = this.processRegistry.get(resolvedRepo, sessionId);
|
|
325
|
-
if (sharedEntry && sharedEntry.ccProcess.state !== 'idle') {
|
|
326
|
-
// Attach to existing process as subscriber
|
|
327
|
-
this.processRegistry.subscribe(resolvedRepo, sessionId, clientRef);
|
|
328
|
-
proc = sharedEntry.ccProcess;
|
|
329
|
-
agent.processes.set(userId, proc);
|
|
330
|
-
// Notify the user they've attached
|
|
331
|
-
agent.tgBot.sendText(chatId, '<blockquote>📎 Attached to existing session process.</blockquote>', 'HTML').catch(err => this.logger.error({ err }, 'Failed to send attach notification'));
|
|
332
|
-
// Show typing indicator and forward message
|
|
333
|
-
this.startTypingIndicator(agent, userId, chatId);
|
|
334
|
-
proc.sendMessage(ccMsg);
|
|
335
|
-
return;
|
|
327
|
+
// Warn if no repo is configured
|
|
328
|
+
if (agent.repo === homedir()) {
|
|
329
|
+
const chatId = source?.chatId;
|
|
330
|
+
if (chatId && agent.tgBot) {
|
|
331
|
+
agent.tgBot.sendText(chatId, formatSystemMessage('status', 'No project selected. Use /repo to pick one, or CC will run in your home directory.'), 'HTML', true).catch(err => this.logger.error({ err }, 'Failed to send no-repo warning'));
|
|
336
332
|
}
|
|
337
333
|
}
|
|
338
|
-
|
|
339
|
-
|
|
334
|
+
proc = this.spawnCCProcess(agentId);
|
|
335
|
+
agent.ccProcess = proc;
|
|
336
|
+
// Emit cc_spawned event to supervisor
|
|
337
|
+
const spawnSource = source?.spawnSource ?? 'telegram';
|
|
338
|
+
this.logger.info({ agentId, sessionId: proc.sessionId, source: spawnSource }, 'CC process spawned');
|
|
339
|
+
if (this.isSupervisorSubscribed(agentId, proc.sessionId)) {
|
|
340
|
+
this.sendToSupervisor({
|
|
341
|
+
type: 'event',
|
|
342
|
+
event: 'cc_spawned',
|
|
343
|
+
agentId,
|
|
344
|
+
sessionId: proc.sessionId,
|
|
345
|
+
source: spawnSource,
|
|
346
|
+
});
|
|
340
347
|
}
|
|
341
|
-
proc = this.spawnCCProcess(agentId, userId, chatId);
|
|
342
|
-
agent.processes.set(userId, proc);
|
|
343
348
|
}
|
|
344
|
-
// Show typing indicator
|
|
345
|
-
|
|
349
|
+
// Show typing indicator
|
|
350
|
+
if (source?.chatId) {
|
|
351
|
+
this.startTypingIndicator(agent, source.chatId);
|
|
352
|
+
}
|
|
353
|
+
// Log user message in event buffer
|
|
354
|
+
agent.eventBuffer.push({ ts: Date.now(), type: 'user', text: data.text });
|
|
346
355
|
proc.sendMessage(ccMsg);
|
|
347
356
|
}
|
|
348
|
-
/** Check if the session JSONL was modified externally since we last tracked it. */
|
|
349
|
-
checkSessionStaleness(_agentId, _userId) {
|
|
350
|
-
// With shared process registry, staleness is handled by the registry itself
|
|
351
|
-
return null;
|
|
352
|
-
}
|
|
353
357
|
// ── Process cleanup helper ──
|
|
354
358
|
/**
|
|
355
|
-
*
|
|
356
|
-
* If other subscribers remain, the process stays alive.
|
|
357
|
-
* If this was the last subscriber, the process is destroyed.
|
|
359
|
+
* Kill the agent's CC process and clean up.
|
|
358
360
|
*/
|
|
359
|
-
|
|
361
|
+
killAgentProcess(agentId) {
|
|
360
362
|
const agent = this.agents.get(agentId);
|
|
361
363
|
if (!agent)
|
|
362
364
|
return;
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
365
|
+
const proc = agent.ccProcess;
|
|
366
|
+
if (proc) {
|
|
367
|
+
const entry = this.processRegistry.findByProcess(proc);
|
|
368
|
+
if (entry) {
|
|
369
|
+
this.processRegistry.destroy(entry.repo, entry.sessionId);
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
proc.destroy();
|
|
373
|
+
}
|
|
368
374
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
375
|
+
agent.ccProcess = null;
|
|
376
|
+
// Clean up accumulator & tracker
|
|
377
|
+
if (agent.accumulator) {
|
|
378
|
+
agent.accumulator.finalize();
|
|
379
|
+
agent.accumulator = null;
|
|
372
380
|
}
|
|
373
|
-
agent.
|
|
381
|
+
if (agent.subAgentTracker) {
|
|
382
|
+
agent.subAgentTracker.reset();
|
|
383
|
+
agent.subAgentTracker = null;
|
|
384
|
+
}
|
|
385
|
+
this.stopTypingIndicator(agent);
|
|
374
386
|
}
|
|
375
387
|
// ── Typing indicator management ──
|
|
376
|
-
|
|
377
|
-
|
|
388
|
+
/** Get the primary TG chat ID for an agent (first allowed user). */
|
|
389
|
+
getAgentChatId(agent) {
|
|
390
|
+
// If we have a typing chatId, use that (most recent active chat)
|
|
391
|
+
if (agent.typingChatId)
|
|
392
|
+
return agent.typingChatId;
|
|
393
|
+
// Fall back to first allowed user
|
|
394
|
+
const firstUser = agent.config.allowedUsers[0];
|
|
395
|
+
return firstUser ? Number(firstUser) : null;
|
|
396
|
+
}
|
|
397
|
+
startTypingIndicator(agent, chatId) {
|
|
378
398
|
// Don't create duplicate intervals
|
|
379
|
-
if (agent.
|
|
399
|
+
if (agent.typingInterval)
|
|
400
|
+
return;
|
|
401
|
+
if (!agent.tgBot)
|
|
380
402
|
return;
|
|
403
|
+
agent.typingChatId = chatId;
|
|
381
404
|
// Send immediately, then repeat every 4s (TG typing badge lasts ~5s)
|
|
382
405
|
agent.tgBot.sendTyping(chatId);
|
|
383
406
|
const interval = setInterval(() => {
|
|
384
|
-
agent.
|
|
407
|
+
if (agent.typingChatId)
|
|
408
|
+
agent.tgBot?.sendTyping(agent.typingChatId);
|
|
385
409
|
}, 4_000);
|
|
386
|
-
agent.
|
|
410
|
+
agent.typingInterval = interval;
|
|
387
411
|
}
|
|
388
|
-
stopTypingIndicator(agent
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
agent.typingIntervals.delete(key);
|
|
412
|
+
stopTypingIndicator(agent) {
|
|
413
|
+
if (agent.typingInterval) {
|
|
414
|
+
clearInterval(agent.typingInterval);
|
|
415
|
+
agent.typingInterval = null;
|
|
416
|
+
agent.typingChatId = null;
|
|
394
417
|
}
|
|
395
418
|
}
|
|
396
|
-
spawnCCProcess(agentId
|
|
419
|
+
spawnCCProcess(agentId) {
|
|
397
420
|
const agent = this.agents.get(agentId);
|
|
398
|
-
const
|
|
399
|
-
//
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
if (modelOverride) {
|
|
406
|
-
userConfig.model = modelOverride;
|
|
407
|
-
// Don't delete yet — cleared when CC writes first assistant message
|
|
421
|
+
const agentState = this.sessionStore.getAgent(agentId);
|
|
422
|
+
// Build userConfig from agent-level state
|
|
423
|
+
const userConfig = resolveUserConfig(agent.config, agent.config.allowedUsers[0] || 'default');
|
|
424
|
+
userConfig.repo = agent.repo;
|
|
425
|
+
userConfig.model = agent.model;
|
|
426
|
+
if (agentState.permissionMode) {
|
|
427
|
+
userConfig.permissionMode = agentState.permissionMode;
|
|
408
428
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
if (registryEntry?.model) {
|
|
414
|
-
userConfig.model = registryEntry.model;
|
|
415
|
-
}
|
|
416
|
-
else {
|
|
417
|
-
const sessions = discoverCCSessions(userConfig.repo, 20);
|
|
418
|
-
const sessionInfo = sessions.find(s => s.id === currentSessionId);
|
|
419
|
-
if (sessionInfo?.model) {
|
|
420
|
-
userConfig.model = sessionInfo.model;
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
if (userState.permissionMode) {
|
|
426
|
-
userConfig.permissionMode = userState.permissionMode;
|
|
427
|
-
}
|
|
428
|
-
// Generate MCP config
|
|
429
|
+
// Determine session ID: pending from /resume, or let CC create new
|
|
430
|
+
const sessionId = agent.pendingSessionId ?? undefined;
|
|
431
|
+
agent.pendingSessionId = null; // consumed
|
|
432
|
+
// Generate MCP config (use agentId as the "userId" for socket naming)
|
|
429
433
|
const mcpServerPath = resolveMcpServerPath();
|
|
430
|
-
const mcpConfigPath = generateMcpConfig(agentId,
|
|
431
|
-
|
|
432
|
-
|
|
434
|
+
const mcpConfigPath = generateMcpConfig(agentId, agentId, // single socket per agent
|
|
435
|
+
this.config.global.socketDir, mcpServerPath);
|
|
436
|
+
// Start MCP socket listener for this agent
|
|
437
|
+
const socketPath = join(this.config.global.socketDir, `${agentId}-${agentId}.sock`);
|
|
433
438
|
this.mcpServer.listen(socketPath);
|
|
434
439
|
const proc = new CCProcess({
|
|
435
440
|
agentId,
|
|
436
|
-
userId,
|
|
441
|
+
userId: agentId, // agent is the sole "user"
|
|
437
442
|
ccBinaryPath: this.config.global.ccBinaryPath,
|
|
438
443
|
userConfig,
|
|
439
444
|
mcpConfigPath,
|
|
440
|
-
sessionId
|
|
441
|
-
continueSession: !!
|
|
445
|
+
sessionId,
|
|
446
|
+
continueSession: !!sessionId,
|
|
442
447
|
logger: this.logger,
|
|
443
448
|
});
|
|
444
449
|
// Register in the process registry
|
|
445
|
-
const ownerRef = { agentId, userId, chatId };
|
|
446
|
-
|
|
447
|
-
// temporary entry for pre-init event routing. Use a placeholder sessionId
|
|
448
|
-
// that gets updated on init.
|
|
449
|
-
const tentativeSessionId = userState.currentSessionId ?? `pending-${Date.now()}`;
|
|
450
|
+
const ownerRef = { agentId, userId: agentId, chatId: 0 };
|
|
451
|
+
const tentativeSessionId = sessionId ?? `pending-${Date.now()}`;
|
|
450
452
|
const registryEntry = this.processRegistry.register(userConfig.repo, tentativeSessionId, userConfig.model || 'default', proc, ownerRef);
|
|
451
453
|
// ── Helper: get all subscribers for this process from the registry ──
|
|
452
454
|
const getEntry = () => this.processRegistry.findByProcess(proc);
|
|
453
455
|
// ── Wire up event handlers (broadcast to all subscribers) ──
|
|
454
456
|
proc.on('init', (event) => {
|
|
455
|
-
this.sessionStore.
|
|
457
|
+
this.sessionStore.updateLastActivity(agentId);
|
|
458
|
+
agent.eventBuffer.push({ ts: Date.now(), type: 'system', text: `Session initialized: ${event.session_id}` });
|
|
456
459
|
// Update registry key if session ID changed from tentative
|
|
457
460
|
if (event.session_id !== tentativeSessionId) {
|
|
458
|
-
// Re-register with the real session ID
|
|
459
461
|
const entry = getEntry();
|
|
460
462
|
if (entry) {
|
|
461
|
-
// Save subscribers before removing
|
|
462
|
-
const savedSubs = [...entry.subscribers.entries()];
|
|
463
463
|
this.processRegistry.remove(userConfig.repo, tentativeSessionId);
|
|
464
|
-
|
|
465
|
-
// Restore additional subscribers
|
|
466
|
-
for (const [, sub] of savedSubs) {
|
|
467
|
-
if (sub.client.agentId !== agentId || sub.client.userId !== userId || sub.client.chatId !== chatId) {
|
|
468
|
-
this.processRegistry.subscribe(userConfig.repo, event.session_id, sub.client);
|
|
469
|
-
const newSub = this.processRegistry.getSubscriber(newEntry, sub.client);
|
|
470
|
-
if (newSub) {
|
|
471
|
-
newSub.accumulator = sub.accumulator;
|
|
472
|
-
newSub.tracker = sub.tracker;
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
}
|
|
464
|
+
this.processRegistry.register(userConfig.repo, event.session_id, userConfig.model || 'default', proc, ownerRef);
|
|
476
465
|
}
|
|
477
466
|
}
|
|
478
467
|
});
|
|
479
468
|
proc.on('stream_event', (event) => {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
// Fallback: single subscriber mode (shouldn't happen)
|
|
483
|
-
this.handleStreamEvent(agentId, userId, chatId, event);
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
for (const sub of this.processRegistry.subscribers(entry)) {
|
|
487
|
-
this.handleStreamEvent(sub.client.agentId, sub.client.userId, sub.client.chatId, event);
|
|
488
|
-
}
|
|
469
|
+
this.highSignalDetector.handleStreamEvent(agentId, event);
|
|
470
|
+
this.handleStreamEvent(agentId, event);
|
|
489
471
|
});
|
|
490
472
|
proc.on('tool_result', (event) => {
|
|
491
|
-
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
// Set agent metadata from structured data or text fallback
|
|
528
|
-
if (event.tool_use_id && spawnMeta.name) {
|
|
529
|
-
tracker.setAgentMetadata(event.tool_use_id, {
|
|
530
|
-
agentName: spawnMeta.name,
|
|
531
|
-
agentType: spawnMeta.agent_type,
|
|
532
|
-
color: spawnMeta.color,
|
|
533
|
-
});
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
// Handle tool result (sets status, edits TG message)
|
|
537
|
-
if (event.tool_use_id) {
|
|
538
|
-
tracker.handleToolResult(event.tool_use_id, resultText);
|
|
473
|
+
// Log to event buffer
|
|
474
|
+
const toolName = event.tool_use_result?.name ?? 'unknown';
|
|
475
|
+
const isToolErr = event.is_error === true;
|
|
476
|
+
const toolContent = typeof event.content === 'string' ? event.content : JSON.stringify(event.content);
|
|
477
|
+
const toolSummary = toolContent.length > 200 ? toolContent.slice(0, 200) + '…' : toolContent;
|
|
478
|
+
agent.eventBuffer.push({ ts: Date.now(), type: 'tool', text: `${isToolErr ? '❌' : '✅'} ${toolName}: ${toolSummary}` });
|
|
479
|
+
// High-signal detection
|
|
480
|
+
this.highSignalDetector.handleToolResult(agentId, event.tool_use_id, toolContent, isToolErr, toolName !== 'unknown' ? toolName : undefined);
|
|
481
|
+
// Resolve tool indicator message with success/failure status
|
|
482
|
+
const acc = agent.accumulator;
|
|
483
|
+
if (acc && event.tool_use_id) {
|
|
484
|
+
const isError = event.is_error === true;
|
|
485
|
+
const contentStr = typeof event.content === 'string' ? event.content : JSON.stringify(event.content);
|
|
486
|
+
const errorMsg = isError ? contentStr : undefined;
|
|
487
|
+
acc.resolveToolMessage(event.tool_use_id, isError, errorMsg, contentStr, event.tool_use_result);
|
|
488
|
+
}
|
|
489
|
+
const tracker = agent.subAgentTracker;
|
|
490
|
+
if (!tracker)
|
|
491
|
+
return;
|
|
492
|
+
const resultText = typeof event.content === 'string' ? event.content : JSON.stringify(event.content);
|
|
493
|
+
const meta = event.tool_use_result;
|
|
494
|
+
// Log warning if structured metadata is missing
|
|
495
|
+
if (!meta && /agent_id:\s*\S+@\S+/.test(resultText)) {
|
|
496
|
+
this.logger.warn({ agentId, toolUseId: event.tool_use_id }, 'Spawn detected in text but no structured tool_use_result metadata - skipping');
|
|
497
|
+
}
|
|
498
|
+
const spawnMeta = meta?.status === 'teammate_spawned' ? meta : undefined;
|
|
499
|
+
if (spawnMeta?.status === 'teammate_spawned' && spawnMeta.team_name) {
|
|
500
|
+
if (!tracker.currentTeamName) {
|
|
501
|
+
this.logger.info({ agentId, teamName: spawnMeta.team_name, agentName: spawnMeta.name, agentType: spawnMeta.agent_type }, 'Spawn detected');
|
|
502
|
+
tracker.setTeamName(spawnMeta.team_name);
|
|
503
|
+
// Wire the "all agents reported" callback to send follow-up to CC
|
|
504
|
+
tracker.setOnAllReported(() => {
|
|
505
|
+
if (proc.state === 'active') {
|
|
506
|
+
proc.sendMessage(createTextMessage('[System] All background agents have reported back. Please read their results from the mailbox/files and provide a synthesis to the user.'));
|
|
507
|
+
}
|
|
508
|
+
});
|
|
539
509
|
}
|
|
540
|
-
//
|
|
541
|
-
if (
|
|
542
|
-
tracker.
|
|
510
|
+
// Set agent metadata from structured data or text fallback
|
|
511
|
+
if (event.tool_use_id && spawnMeta.name) {
|
|
512
|
+
tracker.setAgentMetadata(event.tool_use_id, {
|
|
513
|
+
agentName: spawnMeta.name,
|
|
514
|
+
agentType: spawnMeta.agent_type,
|
|
515
|
+
color: spawnMeta.color,
|
|
516
|
+
});
|
|
543
517
|
}
|
|
544
518
|
}
|
|
519
|
+
// Handle tool result (sets status, edits TG message)
|
|
520
|
+
if (event.tool_use_id) {
|
|
521
|
+
tracker.handleToolResult(event.tool_use_id, resultText);
|
|
522
|
+
}
|
|
523
|
+
// Start mailbox watch AFTER handleToolResult has set agent names
|
|
524
|
+
if (tracker.currentTeamName && tracker.hasDispatchedAgents && !tracker.isMailboxWatching) {
|
|
525
|
+
tracker.startMailboxWatch();
|
|
526
|
+
}
|
|
545
527
|
});
|
|
546
|
-
// System events for background task tracking
|
|
528
|
+
// System events for background task tracking
|
|
547
529
|
proc.on('task_started', (event) => {
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
for (const sub of subscriberList) {
|
|
551
|
-
const subAgent = this.agents.get(sub.client.agentId);
|
|
552
|
-
if (!subAgent)
|
|
553
|
-
continue;
|
|
554
|
-
const accKey = `${sub.client.userId}:${sub.client.chatId}`;
|
|
555
|
-
const tracker = subAgent.subAgentTrackers.get(accKey);
|
|
556
|
-
if (tracker) {
|
|
557
|
-
tracker.handleTaskStarted(event.tool_use_id, event.description, event.task_type);
|
|
558
|
-
}
|
|
530
|
+
if (agent.subAgentTracker) {
|
|
531
|
+
agent.subAgentTracker.handleTaskStarted(event.tool_use_id, event.description, event.task_type);
|
|
559
532
|
}
|
|
560
533
|
});
|
|
561
534
|
proc.on('task_progress', (event) => {
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
for (const sub of subscriberList) {
|
|
565
|
-
const subAgent = this.agents.get(sub.client.agentId);
|
|
566
|
-
if (!subAgent)
|
|
567
|
-
continue;
|
|
568
|
-
const accKey = `${sub.client.userId}:${sub.client.chatId}`;
|
|
569
|
-
const tracker = subAgent.subAgentTrackers.get(accKey);
|
|
570
|
-
if (tracker) {
|
|
571
|
-
tracker.handleTaskProgress(event.tool_use_id, event.description, event.last_tool_name);
|
|
572
|
-
}
|
|
535
|
+
if (agent.subAgentTracker) {
|
|
536
|
+
agent.subAgentTracker.handleTaskProgress(event.tool_use_id, event.description, event.last_tool_name);
|
|
573
537
|
}
|
|
574
538
|
});
|
|
575
539
|
proc.on('task_completed', (event) => {
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
for (const sub of subscriberList) {
|
|
579
|
-
const subAgent = this.agents.get(sub.client.agentId);
|
|
580
|
-
if (!subAgent)
|
|
581
|
-
continue;
|
|
582
|
-
const accKey = `${sub.client.userId}:${sub.client.chatId}`;
|
|
583
|
-
const tracker = subAgent.subAgentTrackers.get(accKey);
|
|
584
|
-
if (tracker) {
|
|
585
|
-
tracker.handleTaskCompleted(event.tool_use_id);
|
|
586
|
-
}
|
|
540
|
+
if (agent.subAgentTracker) {
|
|
541
|
+
agent.subAgentTracker.handleTaskCompleted(event.tool_use_id);
|
|
587
542
|
}
|
|
588
543
|
});
|
|
589
|
-
// Media from tool results (images, PDFs, etc.)
|
|
544
|
+
// Media from tool results (images, PDFs, etc.)
|
|
590
545
|
proc.on('media', (media) => {
|
|
591
546
|
const buf = Buffer.from(media.data, 'base64');
|
|
592
|
-
const
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
subAgent.tgBot.sendDocumentBuffer(sub.client.chatId, buf, `document${ext}`).catch(err => {
|
|
606
|
-
this.logger.error({ err, agentId: sub.client.agentId, userId: sub.client.userId }, 'Failed to send tool_result document');
|
|
607
|
-
});
|
|
608
|
-
}
|
|
547
|
+
const chatId = this.getAgentChatId(agent);
|
|
548
|
+
if (!chatId || !agent.tgBot)
|
|
549
|
+
return;
|
|
550
|
+
if (media.kind === 'image') {
|
|
551
|
+
agent.tgBot.sendPhotoBuffer(chatId, buf).catch(err => {
|
|
552
|
+
this.logger.error({ err, agentId }, 'Failed to send tool_result image');
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
else if (media.kind === 'document') {
|
|
556
|
+
const ext = media.media_type === 'application/pdf' ? '.pdf' : '';
|
|
557
|
+
agent.tgBot.sendDocumentBuffer(chatId, buf, `document${ext}`).catch(err => {
|
|
558
|
+
this.logger.error({ err, agentId }, 'Failed to send tool_result document');
|
|
559
|
+
});
|
|
609
560
|
}
|
|
610
561
|
});
|
|
611
562
|
proc.on('assistant', (event) => {
|
|
612
|
-
//
|
|
613
|
-
|
|
563
|
+
// Log text and thinking blocks to event buffer
|
|
564
|
+
if (event.message?.content) {
|
|
565
|
+
for (const block of event.message.content) {
|
|
566
|
+
if (block.type === 'thinking' && block.thinking) {
|
|
567
|
+
const truncated = block.thinking.length > 300 ? block.thinking.slice(0, 300) + '…' : block.thinking;
|
|
568
|
+
agent.eventBuffer.push({ ts: Date.now(), type: 'thinking', text: truncated });
|
|
569
|
+
}
|
|
570
|
+
else if (block.type === 'text' && block.text) {
|
|
571
|
+
agent.eventBuffer.push({ ts: Date.now(), type: 'text', text: block.text });
|
|
572
|
+
}
|
|
573
|
+
else if (block.type === 'tool_use') {
|
|
574
|
+
const toolBlock = block;
|
|
575
|
+
this.highSignalDetector.handleAssistantToolUse(agentId, toolBlock.name, toolBlock.id, toolBlock.input);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
614
579
|
});
|
|
615
580
|
proc.on('result', (event) => {
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
581
|
+
this.stopTypingIndicator(agent);
|
|
582
|
+
agent.eventBuffer.push({ ts: Date.now(), type: 'system', text: `Turn complete${event.is_error ? ' (error)' : ''}${event.total_cost_usd ? ` · $${event.total_cost_usd.toFixed(4)}` : ''}` });
|
|
583
|
+
this.handleResult(agentId, event);
|
|
584
|
+
// Forward to supervisor
|
|
585
|
+
if (this.isSupervisorSubscribed(agentId, proc.sessionId)) {
|
|
586
|
+
const resultText = event.result ? String(event.result) : '';
|
|
587
|
+
this.sendToSupervisor({
|
|
588
|
+
type: 'event',
|
|
589
|
+
event: 'result',
|
|
590
|
+
agentId,
|
|
591
|
+
sessionId: proc.sessionId,
|
|
592
|
+
text: resultText,
|
|
593
|
+
is_error: event.is_error ?? false,
|
|
594
|
+
});
|
|
627
595
|
}
|
|
628
596
|
});
|
|
629
597
|
proc.on('compact', (event) => {
|
|
630
|
-
// Notify all subscribers that compaction happened
|
|
631
598
|
const trigger = event.compact_metadata?.trigger ?? 'manual';
|
|
632
599
|
const preTokens = event.compact_metadata?.pre_tokens;
|
|
633
600
|
const tokenInfo = preTokens ? ` (was ${Math.round(preTokens / 1000)}k tokens)` : '';
|
|
634
|
-
const
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
continue;
|
|
640
|
-
const label = trigger === 'auto' ? '🗜️ Auto-compacted' : '🗜️ Compacted';
|
|
641
|
-
subAgent.tgBot.sendText(sub.client.chatId, `<blockquote>${escapeHtml(label + tokenInfo)}</blockquote>`, 'HTML').catch((err) => this.logger.error({ err }, 'Failed to send compact notification'));
|
|
601
|
+
const label = trigger === 'auto' ? '🗜️ Auto-compacted' : '🗜️ Compacted';
|
|
602
|
+
agent.eventBuffer.push({ ts: Date.now(), type: 'system', text: label + tokenInfo });
|
|
603
|
+
const chatId = this.getAgentChatId(agent);
|
|
604
|
+
if (chatId && agent.tgBot) {
|
|
605
|
+
agent.tgBot.sendText(chatId, `<blockquote>${escapeHtml(label + tokenInfo)}</blockquote>`, 'HTML', true).catch((err) => this.logger.error({ err }, 'Failed to send compact notification'));
|
|
642
606
|
}
|
|
643
607
|
});
|
|
644
608
|
proc.on('permission_request', (event) => {
|
|
645
|
-
// Send permission request only to the owner (first subscriber)
|
|
646
609
|
const req = event.request;
|
|
647
610
|
const requestId = event.request_id;
|
|
648
|
-
// Store pending permission on the owner's agent
|
|
649
611
|
agent.pendingPermissions.set(requestId, {
|
|
650
612
|
requestId,
|
|
651
|
-
userId,
|
|
613
|
+
userId: agentId,
|
|
652
614
|
toolName: req.tool_name,
|
|
653
615
|
input: req.input,
|
|
654
616
|
});
|
|
655
|
-
// Build description of what CC wants to do
|
|
656
617
|
const toolName = escapeHtml(req.tool_name);
|
|
657
618
|
const inputPreview = req.input
|
|
658
619
|
? escapeHtml(JSON.stringify(req.input).slice(0, 200))
|
|
@@ -663,9 +624,24 @@ export class Bridge extends EventEmitter {
|
|
|
663
624
|
const keyboard = new InlineKeyboard()
|
|
664
625
|
.text('✅ Allow', `perm_allow:${requestId}`)
|
|
665
626
|
.text('❌ Deny', `perm_deny:${requestId}`)
|
|
666
|
-
.text('✅ Allow All', `perm_allow_all:${
|
|
667
|
-
|
|
668
|
-
|
|
627
|
+
.text('✅ Allow All', `perm_allow_all:${agentId}`);
|
|
628
|
+
const permChatId = this.getAgentChatId(agent);
|
|
629
|
+
if (permChatId && agent.tgBot) {
|
|
630
|
+
agent.tgBot.sendTextWithKeyboard(permChatId, text, keyboard, 'HTML')
|
|
631
|
+
.catch(err => this.logger.error({ err }, 'Failed to send permission request'));
|
|
632
|
+
}
|
|
633
|
+
// Forward to supervisor so it can render approve/deny UI
|
|
634
|
+
if (this.isSupervisorSubscribed(agentId, proc.sessionId)) {
|
|
635
|
+
const description = req.decision_reason || `CC wants to use ${req.tool_name}`;
|
|
636
|
+
this.sendToSupervisor({
|
|
637
|
+
type: 'event',
|
|
638
|
+
event: 'permission_request',
|
|
639
|
+
agentId,
|
|
640
|
+
toolName: req.tool_name,
|
|
641
|
+
requestId,
|
|
642
|
+
description,
|
|
643
|
+
});
|
|
644
|
+
}
|
|
669
645
|
});
|
|
670
646
|
proc.on('api_error', (event) => {
|
|
671
647
|
const errMsg = event.error?.message || 'Unknown API error';
|
|
@@ -674,193 +650,165 @@ export class Bridge extends EventEmitter {
|
|
|
674
650
|
const retryInfo = event.retryAttempt != null && event.maxRetries != null
|
|
675
651
|
? ` (retry ${event.retryAttempt}/${event.maxRetries})`
|
|
676
652
|
: '';
|
|
653
|
+
agent.eventBuffer.push({ ts: Date.now(), type: 'error', text: `${errMsg}${retryInfo}` });
|
|
677
654
|
const text = isOverloaded
|
|
678
|
-
?
|
|
679
|
-
:
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
for (const sub of subscriberList) {
|
|
684
|
-
const subAgent = this.agents.get(sub.client.agentId);
|
|
685
|
-
if (!subAgent)
|
|
686
|
-
continue;
|
|
687
|
-
subAgent.tgBot.sendText(sub.client.chatId, text, 'HTML')
|
|
655
|
+
? formatSystemMessage('error', `API overloaded, retrying...${retryInfo}`)
|
|
656
|
+
: formatSystemMessage('error', `${escapeHtml(errMsg)}${retryInfo}`);
|
|
657
|
+
const errChatId = this.getAgentChatId(agent);
|
|
658
|
+
if (errChatId && agent.tgBot) {
|
|
659
|
+
agent.tgBot.sendText(errChatId, text, 'HTML', true) // silent
|
|
688
660
|
.catch(err => this.logger.error({ err }, 'Failed to send API error notification'));
|
|
689
661
|
}
|
|
690
662
|
});
|
|
691
663
|
proc.on('hang', () => {
|
|
692
|
-
|
|
693
|
-
const
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
const subAgent = this.agents.get(sub.client.agentId);
|
|
697
|
-
if (!subAgent)
|
|
698
|
-
continue;
|
|
699
|
-
this.stopTypingIndicator(subAgent, sub.client.userId, sub.client.chatId);
|
|
700
|
-
subAgent.tgBot.sendText(sub.client.chatId, '<blockquote>⏸ Session paused. Send a message to continue.</blockquote>', 'HTML')
|
|
664
|
+
this.stopTypingIndicator(agent);
|
|
665
|
+
const hangChatId = this.getAgentChatId(agent);
|
|
666
|
+
if (hangChatId && agent.tgBot) {
|
|
667
|
+
agent.tgBot.sendText(hangChatId, '<blockquote>⏸ Session paused. Send a message to continue.</blockquote>', 'HTML', true) // silent
|
|
701
668
|
.catch(err => this.logger.error({ err }, 'Failed to send hang notification'));
|
|
702
669
|
}
|
|
703
670
|
});
|
|
704
671
|
proc.on('takeover', () => {
|
|
705
|
-
this.logger.warn({ agentId
|
|
706
|
-
|
|
672
|
+
this.logger.warn({ agentId }, 'Session takeover detected — keeping session for roaming');
|
|
673
|
+
agent.eventBuffer.push({ ts: Date.now(), type: 'system', text: 'Session takeover detected' });
|
|
674
|
+
// Send TG system message for takeover
|
|
675
|
+
const takeoverChatId = this.getAgentChatId(agent);
|
|
676
|
+
if (takeoverChatId && agent.tgBot) {
|
|
677
|
+
agent.tgBot.sendText(takeoverChatId, '<blockquote>⚠️ Session taken over by another client</blockquote>', 'HTML', true)
|
|
678
|
+
.catch(err => this.logger.error({ err, agentId }, 'Failed to send takeover notification'));
|
|
679
|
+
}
|
|
680
|
+
// Notify supervisor and suppress subsequent exit event
|
|
681
|
+
if (this.isSupervisorSubscribed(agentId, proc.sessionId)) {
|
|
682
|
+
this.sendToSupervisor({ type: 'event', event: 'session_takeover', agentId, sessionId: proc.sessionId });
|
|
683
|
+
this.suppressExitForProcess.add(proc.sessionId ?? '');
|
|
684
|
+
}
|
|
685
|
+
this.stopTypingIndicator(agent);
|
|
707
686
|
const entry = getEntry();
|
|
708
687
|
if (entry) {
|
|
709
|
-
for (const sub of this.processRegistry.subscribers(entry)) {
|
|
710
|
-
const subAgent = this.agents.get(sub.client.agentId);
|
|
711
|
-
if (!subAgent)
|
|
712
|
-
continue;
|
|
713
|
-
this.stopTypingIndicator(subAgent, sub.client.userId, sub.client.chatId);
|
|
714
|
-
subAgent.processes.delete(sub.client.userId);
|
|
715
|
-
}
|
|
716
|
-
// Remove from registry without destroying (already handling exit)
|
|
717
688
|
this.processRegistry.remove(entry.repo, entry.sessionId);
|
|
718
689
|
}
|
|
719
|
-
|
|
690
|
+
agent.ccProcess = null;
|
|
720
691
|
proc.destroy();
|
|
721
692
|
});
|
|
722
693
|
proc.on('exit', () => {
|
|
723
|
-
|
|
694
|
+
agent.eventBuffer.push({ ts: Date.now(), type: 'system', text: 'Process exited' });
|
|
695
|
+
this.highSignalDetector.cleanup(agentId);
|
|
696
|
+
// Forward to supervisor (unless suppressed by takeover)
|
|
697
|
+
if (this.suppressExitForProcess.has(proc.sessionId ?? '')) {
|
|
698
|
+
this.suppressExitForProcess.delete(proc.sessionId ?? '');
|
|
699
|
+
}
|
|
700
|
+
else if (this.isSupervisorSubscribed(agentId, proc.sessionId)) {
|
|
701
|
+
this.sendToSupervisor({ type: 'event', event: 'process_exit', agentId, sessionId: proc.sessionId, exitCode: null });
|
|
702
|
+
}
|
|
703
|
+
this.stopTypingIndicator(agent);
|
|
704
|
+
if (agent.accumulator) {
|
|
705
|
+
agent.accumulator.finalize();
|
|
706
|
+
agent.accumulator = null;
|
|
707
|
+
}
|
|
708
|
+
if (agent.subAgentTracker) {
|
|
709
|
+
agent.subAgentTracker.stopMailboxWatch();
|
|
710
|
+
agent.subAgentTracker = null;
|
|
711
|
+
}
|
|
724
712
|
const entry = getEntry();
|
|
725
713
|
if (entry) {
|
|
726
|
-
for (const sub of this.processRegistry.subscribers(entry)) {
|
|
727
|
-
const subAgent = this.agents.get(sub.client.agentId);
|
|
728
|
-
if (!subAgent)
|
|
729
|
-
continue;
|
|
730
|
-
this.stopTypingIndicator(subAgent, sub.client.userId, sub.client.chatId);
|
|
731
|
-
const accKey = `${sub.client.userId}:${sub.client.chatId}`;
|
|
732
|
-
const acc = subAgent.accumulators.get(accKey);
|
|
733
|
-
if (acc) {
|
|
734
|
-
acc.finalize();
|
|
735
|
-
subAgent.accumulators.delete(accKey);
|
|
736
|
-
}
|
|
737
|
-
const exitTracker = subAgent.subAgentTrackers.get(accKey);
|
|
738
|
-
if (exitTracker) {
|
|
739
|
-
exitTracker.stopMailboxWatch();
|
|
740
|
-
}
|
|
741
|
-
subAgent.subAgentTrackers.delete(accKey);
|
|
742
|
-
}
|
|
743
|
-
// Remove from registry (process already exited)
|
|
744
714
|
this.processRegistry.remove(entry.repo, entry.sessionId);
|
|
745
715
|
}
|
|
746
|
-
|
|
747
|
-
// Fallback: clean up owner only
|
|
748
|
-
this.stopTypingIndicator(agent, userId, chatId);
|
|
749
|
-
const accKey = `${userId}:${chatId}`;
|
|
750
|
-
const acc = agent.accumulators.get(accKey);
|
|
751
|
-
if (acc) {
|
|
752
|
-
acc.finalize();
|
|
753
|
-
agent.accumulators.delete(accKey);
|
|
754
|
-
}
|
|
755
|
-
const exitTracker = agent.subAgentTrackers.get(accKey);
|
|
756
|
-
if (exitTracker) {
|
|
757
|
-
exitTracker.stopMailboxWatch();
|
|
758
|
-
}
|
|
759
|
-
agent.subAgentTrackers.delete(accKey);
|
|
760
|
-
}
|
|
716
|
+
agent.ccProcess = null;
|
|
761
717
|
});
|
|
762
718
|
proc.on('error', (err) => {
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
const
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
if (!subAgent)
|
|
769
|
-
continue;
|
|
770
|
-
this.stopTypingIndicator(subAgent, sub.client.userId, sub.client.chatId);
|
|
771
|
-
subAgent.tgBot.sendText(sub.client.chatId, `<blockquote>⚠️ ${escapeHtml(String(err.message))}</blockquote>`, 'HTML')
|
|
719
|
+
agent.eventBuffer.push({ ts: Date.now(), type: 'error', text: err.message });
|
|
720
|
+
this.stopTypingIndicator(agent);
|
|
721
|
+
const errChatId = this.getAgentChatId(agent);
|
|
722
|
+
if (errChatId && agent.tgBot) {
|
|
723
|
+
agent.tgBot.sendText(errChatId, formatSystemMessage('error', escapeHtml(String(err.message))), 'HTML', true) // silent
|
|
772
724
|
.catch(err2 => this.logger.error({ err: err2 }, 'Failed to send process error notification'));
|
|
773
725
|
}
|
|
774
726
|
});
|
|
775
727
|
return proc;
|
|
776
728
|
}
|
|
777
729
|
// ── Stream event handling ──
|
|
778
|
-
handleStreamEvent(agentId,
|
|
730
|
+
handleStreamEvent(agentId, event) {
|
|
779
731
|
const agent = this.agents.get(agentId);
|
|
780
732
|
if (!agent)
|
|
781
733
|
return;
|
|
782
|
-
const
|
|
783
|
-
|
|
784
|
-
|
|
734
|
+
const chatId = this.getAgentChatId(agent);
|
|
735
|
+
if (!chatId)
|
|
736
|
+
return;
|
|
737
|
+
if (!agent.accumulator && agent.tgBot) {
|
|
738
|
+
const tgBot = agent.tgBot; // capture for closures (non-null here)
|
|
785
739
|
const sender = {
|
|
786
|
-
sendMessage: (cid, text, parseMode) =>
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
740
|
+
sendMessage: (cid, text, parseMode) => {
|
|
741
|
+
this.logger.info({ agentId, chatId: cid, textLen: text.length }, 'TG accumulator sendMessage');
|
|
742
|
+
return tgBot.sendText(cid, text, parseMode, true); // silent — no push notification
|
|
743
|
+
},
|
|
744
|
+
editMessage: (cid, msgId, text, parseMode) => {
|
|
745
|
+
this.logger.info({ agentId, chatId: cid, msgId, textLen: text.length }, 'TG accumulator editMessage');
|
|
746
|
+
return tgBot.editText(cid, msgId, text, parseMode);
|
|
747
|
+
},
|
|
748
|
+
deleteMessage: (cid, msgId) => tgBot.deleteMessage(cid, msgId),
|
|
749
|
+
setReaction: (cid, msgId, emoji) => tgBot.setReaction(cid, msgId, emoji),
|
|
750
|
+
sendPhoto: (cid, buffer, caption) => tgBot.sendPhotoBuffer(cid, buffer, caption),
|
|
790
751
|
};
|
|
791
752
|
const onError = (err, context) => {
|
|
792
|
-
this.logger.error({ err, context, agentId
|
|
793
|
-
|
|
753
|
+
this.logger.error({ err, context, agentId }, 'Stream accumulator error');
|
|
754
|
+
tgBot.sendText(chatId, formatSystemMessage('error', escapeHtml(context)), 'HTML', true).catch(() => { }); // silent
|
|
794
755
|
};
|
|
795
|
-
|
|
796
|
-
agent.accumulators.set(accKey, acc);
|
|
756
|
+
agent.accumulator = new StreamAccumulator({ chatId, sender, logger: this.logger, onError });
|
|
797
757
|
}
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
if (!tracker) {
|
|
758
|
+
if (!agent.subAgentTracker && agent.tgBot) {
|
|
759
|
+
const tgBot = agent.tgBot; // capture for closures (non-null here)
|
|
801
760
|
const subAgentSender = {
|
|
802
|
-
sendMessage: (cid, text, parseMode) =>
|
|
803
|
-
editMessage: (cid, msgId, text, parseMode) =>
|
|
804
|
-
setReaction: (cid, msgId, emoji) =>
|
|
761
|
+
sendMessage: (cid, text, parseMode) => tgBot.sendText(cid, text, parseMode, true), // silent
|
|
762
|
+
editMessage: (cid, msgId, text, parseMode) => tgBot.editText(cid, msgId, text, parseMode),
|
|
763
|
+
setReaction: (cid, msgId, emoji) => tgBot.setReaction(cid, msgId, emoji),
|
|
805
764
|
};
|
|
806
|
-
|
|
765
|
+
agent.subAgentTracker = new SubAgentTracker({
|
|
807
766
|
chatId,
|
|
808
767
|
sender: subAgentSender,
|
|
809
768
|
});
|
|
810
|
-
agent.subAgentTrackers.set(accKey, tracker);
|
|
811
769
|
}
|
|
812
770
|
// On message_start: new CC turn — full reset for text accumulator.
|
|
813
|
-
// Tool indicator messages are independent and persist across turns.
|
|
814
771
|
if (event.type === 'message_start') {
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
tracker.reset();
|
|
772
|
+
agent.accumulator?.reset();
|
|
773
|
+
if (agent.subAgentTracker && !agent.subAgentTracker.hasDispatchedAgents) {
|
|
774
|
+
agent.subAgentTracker.reset();
|
|
819
775
|
}
|
|
820
776
|
}
|
|
821
|
-
|
|
822
|
-
|
|
777
|
+
agent.accumulator?.handleEvent(event);
|
|
778
|
+
agent.subAgentTracker?.handleEvent(event);
|
|
823
779
|
}
|
|
824
|
-
handleResult(agentId,
|
|
780
|
+
handleResult(agentId, event) {
|
|
825
781
|
const agent = this.agents.get(agentId);
|
|
826
782
|
if (!agent)
|
|
827
783
|
return;
|
|
784
|
+
const chatId = this.getAgentChatId(agent);
|
|
828
785
|
// Set usage stats on the accumulator before finalizing
|
|
829
|
-
const
|
|
830
|
-
const acc = agent.accumulators.get(accKey);
|
|
786
|
+
const acc = agent.accumulator;
|
|
831
787
|
if (acc) {
|
|
832
|
-
// Extract usage from result event
|
|
833
788
|
if (event.usage) {
|
|
789
|
+
const proc = agent.ccProcess;
|
|
790
|
+
const entry = proc ? this.processRegistry.findByProcess(proc) : null;
|
|
834
791
|
acc.setTurnUsage({
|
|
835
792
|
inputTokens: event.usage.input_tokens ?? 0,
|
|
836
793
|
outputTokens: event.usage.output_tokens ?? 0,
|
|
837
794
|
cacheReadTokens: event.usage.cache_read_input_tokens ?? 0,
|
|
838
795
|
cacheCreationTokens: event.usage.cache_creation_input_tokens ?? 0,
|
|
839
796
|
costUsd: event.total_cost_usd ?? null,
|
|
840
|
-
model: event.model
|
|
841
|
-
?? this.processRegistry.findByClient({ agentId, userId, chatId })?.model,
|
|
797
|
+
model: event.model ?? entry?.model,
|
|
842
798
|
});
|
|
843
799
|
}
|
|
844
800
|
acc.finalize();
|
|
845
|
-
// Don't delete — next turn will reset via message_start and create a new message
|
|
846
|
-
}
|
|
847
|
-
// Update session store with cost
|
|
848
|
-
if (event.total_cost_usd) {
|
|
849
801
|
}
|
|
850
|
-
//
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
if (event.is_error && event.result) {
|
|
854
|
-
agent.tgBot.sendText(chatId, `<blockquote>⚠️ ${escapeHtml(String(event.result))}</blockquote>`, 'HTML')
|
|
802
|
+
// Handle errors (only send to TG if bot available)
|
|
803
|
+
if (event.is_error && event.result && chatId && agent.tgBot) {
|
|
804
|
+
agent.tgBot.sendText(chatId, formatSystemMessage('error', escapeHtml(String(event.result))), 'HTML', true) // silent
|
|
855
805
|
.catch(err => this.logger.error({ err }, 'Failed to send result error notification'));
|
|
856
806
|
}
|
|
857
807
|
// If background sub-agents are still running, mailbox watcher handles them.
|
|
858
|
-
|
|
859
|
-
const tracker = agent.subAgentTrackers.get(accKey);
|
|
808
|
+
const tracker = agent.subAgentTracker;
|
|
860
809
|
if (tracker?.hasDispatchedAgents && tracker.currentTeamName) {
|
|
861
810
|
this.logger.info({ agentId }, 'Turn ended with background sub-agents still running');
|
|
862
|
-
|
|
863
|
-
const ccProcess = agent.processes.get(userId);
|
|
811
|
+
const ccProcess = agent.ccProcess;
|
|
864
812
|
if (ccProcess)
|
|
865
813
|
ccProcess.clearIdleTimer();
|
|
866
814
|
// Start mailbox watcher (works for general-purpose agents that have SendMessage)
|
|
@@ -871,12 +819,11 @@ export class Bridge extends EventEmitter {
|
|
|
871
819
|
tracker.hasPendingFollowUp = true;
|
|
872
820
|
setTimeout(() => {
|
|
873
821
|
if (!tracker.hasDispatchedAgents)
|
|
874
|
-
return;
|
|
875
|
-
const proc = agent.
|
|
822
|
+
return;
|
|
823
|
+
const proc = agent.ccProcess;
|
|
876
824
|
if (!proc)
|
|
877
825
|
return;
|
|
878
826
|
this.logger.info({ agentId }, 'Mailbox timeout — sending single follow-up for remaining agents');
|
|
879
|
-
// Mark all remaining dispatched agents as completed (CC already has the results)
|
|
880
827
|
for (const info of tracker.activeAgents) {
|
|
881
828
|
if (info.status === 'dispatched') {
|
|
882
829
|
tracker.markCompleted(info.toolUseId, '(results delivered in CC response)');
|
|
@@ -892,27 +839,14 @@ export class Bridge extends EventEmitter {
|
|
|
892
839
|
const agent = this.agents.get(agentId);
|
|
893
840
|
if (!agent)
|
|
894
841
|
return;
|
|
842
|
+
if (!agent.tgBot)
|
|
843
|
+
return; // ephemeral agents don't have TG bots
|
|
895
844
|
this.logger.debug({ agentId, command: cmd.command, args: cmd.args }, 'Slash command');
|
|
896
845
|
switch (cmd.command) {
|
|
897
846
|
case 'start': {
|
|
898
|
-
const
|
|
899
|
-
const
|
|
900
|
-
const
|
|
901
|
-
const session = userState.currentSessionId;
|
|
902
|
-
// Model: running process > session JSONL > user default
|
|
903
|
-
let model = userConf.model;
|
|
904
|
-
if (session && repo) {
|
|
905
|
-
const registryEntry = this.processRegistry.get(repo, session);
|
|
906
|
-
if (registryEntry?.model) {
|
|
907
|
-
model = registryEntry.model;
|
|
908
|
-
}
|
|
909
|
-
else {
|
|
910
|
-
const sessions = discoverCCSessions(repo, 20);
|
|
911
|
-
const sessionInfo = sessions.find(s => s.id === session);
|
|
912
|
-
if (sessionInfo?.model)
|
|
913
|
-
model = sessionInfo.model;
|
|
914
|
-
}
|
|
915
|
-
}
|
|
847
|
+
const repo = agent.repo;
|
|
848
|
+
const model = agent.model;
|
|
849
|
+
const session = agent.ccProcess?.sessionId;
|
|
916
850
|
const lines = ['👋 <b>TGCC</b> — Telegram ↔ Claude Code bridge'];
|
|
917
851
|
if (repo)
|
|
918
852
|
lines.push(`📂 <code>${escapeHtml(shortenRepoPath(repo))}</code>`);
|
|
@@ -934,14 +868,12 @@ export class Bridge extends EventEmitter {
|
|
|
934
868
|
await agent.tgBot.sendText(cmd.chatId, HELP_TEXT, 'HTML');
|
|
935
869
|
break;
|
|
936
870
|
case 'ping': {
|
|
937
|
-
const
|
|
938
|
-
const state = proc?.state ?? 'idle';
|
|
871
|
+
const state = agent.ccProcess?.state ?? 'idle';
|
|
939
872
|
await agent.tgBot.sendText(cmd.chatId, `pong — process: <b>${state.toUpperCase()}</b>`, 'HTML');
|
|
940
873
|
break;
|
|
941
874
|
}
|
|
942
875
|
case 'status': {
|
|
943
|
-
const proc = agent.
|
|
944
|
-
const userState = this.sessionStore.getUser(agentId, cmd.userId);
|
|
876
|
+
const proc = agent.ccProcess;
|
|
945
877
|
const uptime = proc?.spawnedAt
|
|
946
878
|
? formatDuration(Date.now() - proc.spawnedAt.getTime())
|
|
947
879
|
: 'N/A';
|
|
@@ -949,77 +881,56 @@ export class Bridge extends EventEmitter {
|
|
|
949
881
|
`<b>Agent:</b> ${escapeHtml(agentId)}`,
|
|
950
882
|
`<b>Process:</b> ${(proc?.state ?? 'idle').toUpperCase()} (uptime: ${uptime})`,
|
|
951
883
|
`<b>Session:</b> <code>${escapeHtml(proc?.sessionId?.slice(0, 8) ?? 'none')}</code>`,
|
|
952
|
-
`<b>Model:</b> ${escapeHtml(
|
|
953
|
-
`<b>Repo:</b> ${escapeHtml(
|
|
884
|
+
`<b>Model:</b> ${escapeHtml(agent.model)}`,
|
|
885
|
+
`<b>Repo:</b> ${escapeHtml(agent.repo)}`,
|
|
954
886
|
`<b>Cost:</b> $${(proc?.totalCostUsd ?? 0).toFixed(4)}`,
|
|
955
887
|
].join('\n');
|
|
956
888
|
await agent.tgBot.sendText(cmd.chatId, status, 'HTML');
|
|
957
889
|
break;
|
|
958
890
|
}
|
|
959
891
|
case 'cost': {
|
|
960
|
-
|
|
961
|
-
await agent.tgBot.sendText(cmd.chatId, `<b>Session cost:</b> $${(proc?.totalCostUsd ?? 0).toFixed(4)}`, 'HTML');
|
|
892
|
+
await agent.tgBot.sendText(cmd.chatId, `<b>Session cost:</b> $${(agent.ccProcess?.totalCostUsd ?? 0).toFixed(4)}`, 'HTML');
|
|
962
893
|
break;
|
|
963
894
|
}
|
|
964
895
|
case 'new': {
|
|
965
|
-
this.
|
|
966
|
-
|
|
967
|
-
const newConf = resolveUserConfig(agent.config, cmd.userId);
|
|
968
|
-
const newState = this.sessionStore.getUser(agentId, cmd.userId);
|
|
969
|
-
const newRepo = newState.repo || newConf.repo;
|
|
970
|
-
const newModel = newState.model || newConf.model;
|
|
896
|
+
this.killAgentProcess(agentId);
|
|
897
|
+
agent.pendingSessionId = null; // next message spawns fresh
|
|
971
898
|
const newLines = ['Session cleared. Next message starts fresh.'];
|
|
972
|
-
if (
|
|
973
|
-
newLines.push(`📂 <code>${escapeHtml(shortenRepoPath(
|
|
974
|
-
if (
|
|
975
|
-
newLines.push(`🤖 ${escapeHtml(
|
|
899
|
+
if (agent.repo)
|
|
900
|
+
newLines.push(`📂 <code>${escapeHtml(shortenRepoPath(agent.repo))}</code>`);
|
|
901
|
+
if (agent.model)
|
|
902
|
+
newLines.push(`🤖 ${escapeHtml(agent.model)}`);
|
|
976
903
|
await agent.tgBot.sendText(cmd.chatId, `<blockquote>${newLines.join('\n')}</blockquote>`, 'HTML');
|
|
977
904
|
break;
|
|
978
905
|
}
|
|
979
906
|
case 'continue': {
|
|
980
|
-
|
|
981
|
-
const
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
let
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
const recent = discoverCCSessions(contRepo, 1);
|
|
907
|
+
// Remember the current session before killing
|
|
908
|
+
const contSession = agent.ccProcess?.sessionId;
|
|
909
|
+
this.killAgentProcess(agentId);
|
|
910
|
+
// If no session, auto-pick the most recent one
|
|
911
|
+
let sessionToResume = contSession;
|
|
912
|
+
if (!sessionToResume && agent.repo) {
|
|
913
|
+
const recent = discoverCCSessions(agent.repo, 1);
|
|
988
914
|
if (recent.length > 0) {
|
|
989
|
-
|
|
990
|
-
this.sessionStore.setCurrentSession(agentId, cmd.userId, contSession);
|
|
915
|
+
sessionToResume = recent[0].id;
|
|
991
916
|
}
|
|
992
917
|
}
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
if (contSession && contRepo) {
|
|
996
|
-
// Check if a process is already running for this session
|
|
997
|
-
const registryEntry = this.processRegistry.get(contRepo, contSession);
|
|
998
|
-
if (registryEntry?.model) {
|
|
999
|
-
contModel = registryEntry.model;
|
|
1000
|
-
}
|
|
1001
|
-
else {
|
|
1002
|
-
const sessions = discoverCCSessions(contRepo, 20);
|
|
1003
|
-
const sessionInfo = sessions.find(s => s.id === contSession);
|
|
1004
|
-
if (sessionInfo?.model)
|
|
1005
|
-
contModel = sessionInfo.model;
|
|
1006
|
-
}
|
|
918
|
+
if (sessionToResume) {
|
|
919
|
+
agent.pendingSessionId = sessionToResume;
|
|
1007
920
|
}
|
|
1008
921
|
const contLines = ['Process respawned. Session kept.'];
|
|
1009
|
-
if (
|
|
1010
|
-
contLines.push(`📂 <code>${escapeHtml(shortenRepoPath(
|
|
1011
|
-
if (
|
|
1012
|
-
contLines.push(`🤖 ${escapeHtml(
|
|
1013
|
-
if (
|
|
1014
|
-
contLines.push(`📎 <code>${escapeHtml(
|
|
922
|
+
if (agent.repo)
|
|
923
|
+
contLines.push(`📂 <code>${escapeHtml(shortenRepoPath(agent.repo))}</code>`);
|
|
924
|
+
if (agent.model)
|
|
925
|
+
contLines.push(`🤖 ${escapeHtml(agent.model)}`);
|
|
926
|
+
if (sessionToResume)
|
|
927
|
+
contLines.push(`📎 <code>${escapeHtml(sessionToResume.slice(0, 8))}</code>`);
|
|
1015
928
|
await agent.tgBot.sendText(cmd.chatId, `<blockquote>${contLines.join('\n')}</blockquote>`, 'HTML');
|
|
1016
929
|
break;
|
|
1017
930
|
}
|
|
1018
931
|
case 'sessions': {
|
|
1019
|
-
const
|
|
1020
|
-
const
|
|
1021
|
-
const repo = userState.repo || userConf.repo;
|
|
1022
|
-
const currentSessionId = userState.currentSessionId;
|
|
932
|
+
const repo = agent.repo;
|
|
933
|
+
const currentSessionId = agent.ccProcess?.sessionId ?? null;
|
|
1023
934
|
// Discover sessions from CC's session directory
|
|
1024
935
|
const discovered = repo ? discoverCCSessions(repo, 5) : [];
|
|
1025
936
|
const merged = discovered.map(d => {
|
|
@@ -1051,7 +962,7 @@ export class Bridge extends EventEmitter {
|
|
|
1051
962
|
const kb = new InlineKeyboard();
|
|
1052
963
|
if (s.isCurrent) {
|
|
1053
964
|
const repoLine = repo ? `\n📂 <code>${escapeHtml(shortenRepoPath(repo))}</code>` : '';
|
|
1054
|
-
const sessModel =
|
|
965
|
+
const sessModel = agent.model;
|
|
1055
966
|
const modelLine = sessModel ? `\n🤖 ${escapeHtml(sessModel)}` : '';
|
|
1056
967
|
const sessionLine = `\n📎 <code>${escapeHtml(s.id.slice(0, 8))}</code>`;
|
|
1057
968
|
const text = `<blockquote><b>Current session:</b>\n${displayTitle}\n${s.detail} · ${s.age}${repoLine}${modelLine}${sessionLine}</blockquote>`;
|
|
@@ -1071,22 +982,21 @@ export class Bridge extends EventEmitter {
|
|
|
1071
982
|
await agent.tgBot.sendText(cmd.chatId, '<blockquote>Usage: /resume <session-id></blockquote>', 'HTML');
|
|
1072
983
|
break;
|
|
1073
984
|
}
|
|
1074
|
-
this.
|
|
1075
|
-
|
|
985
|
+
this.killAgentProcess(agentId);
|
|
986
|
+
agent.pendingSessionId = cmd.args.trim();
|
|
1076
987
|
await agent.tgBot.sendText(cmd.chatId, `Will resume session <code>${escapeHtml(cmd.args.trim().slice(0, 8))}</code> on next message.`, 'HTML');
|
|
1077
988
|
break;
|
|
1078
989
|
}
|
|
1079
990
|
case 'session': {
|
|
1080
|
-
const
|
|
1081
|
-
if (!
|
|
991
|
+
const currentSessionId = agent.ccProcess?.sessionId;
|
|
992
|
+
if (!currentSessionId) {
|
|
1082
993
|
await agent.tgBot.sendText(cmd.chatId, '<blockquote>No active session.</blockquote>', 'HTML');
|
|
1083
994
|
break;
|
|
1084
995
|
}
|
|
1085
|
-
const
|
|
1086
|
-
const
|
|
1087
|
-
const info = discovered.find(d => d.id === userState.currentSessionId);
|
|
996
|
+
const discovered = agent.repo ? discoverCCSessions(agent.repo, 20) : [];
|
|
997
|
+
const info = discovered.find(d => d.id === currentSessionId);
|
|
1088
998
|
if (!info) {
|
|
1089
|
-
await agent.tgBot.sendText(cmd.chatId, `<b>Session:</b> <code>${escapeHtml(
|
|
999
|
+
await agent.tgBot.sendText(cmd.chatId, `<b>Session:</b> <code>${escapeHtml(currentSessionId.slice(0, 8))}</code>`, 'HTML');
|
|
1090
1000
|
break;
|
|
1091
1001
|
}
|
|
1092
1002
|
const ctxLine = info.contextPct !== null ? `\n<b>Context:</b> ${info.contextPct}%` : '';
|
|
@@ -1097,23 +1007,7 @@ export class Bridge extends EventEmitter {
|
|
|
1097
1007
|
case 'model': {
|
|
1098
1008
|
const MODEL_OPTIONS = ['opus', 'sonnet', 'haiku'];
|
|
1099
1009
|
if (!cmd.args) {
|
|
1100
|
-
|
|
1101
|
-
const uState = this.sessionStore.getUser(agentId, cmd.userId);
|
|
1102
|
-
const uConf = resolveUserConfig(agent.config, cmd.userId);
|
|
1103
|
-
const uRepo = uState.repo || uConf.repo;
|
|
1104
|
-
const uSession = uState.currentSessionId;
|
|
1105
|
-
let current = uConf.model || 'default';
|
|
1106
|
-
if (uSession && uRepo) {
|
|
1107
|
-
const re = this.processRegistry.get(uRepo, uSession);
|
|
1108
|
-
if (re?.model)
|
|
1109
|
-
current = re.model;
|
|
1110
|
-
else {
|
|
1111
|
-
const ds = discoverCCSessions(uRepo, 20);
|
|
1112
|
-
const si = ds.find(s => s.id === uSession);
|
|
1113
|
-
if (si?.model)
|
|
1114
|
-
current = si.model;
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1010
|
+
const current = agent.model || 'default';
|
|
1117
1011
|
const keyboard = new InlineKeyboard();
|
|
1118
1012
|
for (const m of MODEL_OPTIONS) {
|
|
1119
1013
|
const isCurrent = current.includes(m);
|
|
@@ -1124,11 +1018,13 @@ export class Bridge extends EventEmitter {
|
|
|
1124
1018
|
break;
|
|
1125
1019
|
}
|
|
1126
1020
|
const newModel = cmd.args.trim();
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
this.
|
|
1130
|
-
this.
|
|
1021
|
+
const oldModel = agent.model;
|
|
1022
|
+
agent.model = newModel;
|
|
1023
|
+
this.sessionStore.setModel(agentId, newModel);
|
|
1024
|
+
this.killAgentProcess(agentId);
|
|
1131
1025
|
await agent.tgBot.sendText(cmd.chatId, `<blockquote>Model set to <code>${escapeHtml(newModel)}</code>. Process respawned.</blockquote>`, 'HTML');
|
|
1026
|
+
// Emit state_changed event
|
|
1027
|
+
this.emitStateChanged(agentId, 'model', oldModel, newModel, 'telegram');
|
|
1132
1028
|
break;
|
|
1133
1029
|
}
|
|
1134
1030
|
case 'repo': {
|
|
@@ -1247,8 +1143,7 @@ export class Bridge extends EventEmitter {
|
|
|
1247
1143
|
break;
|
|
1248
1144
|
}
|
|
1249
1145
|
if (!cmd.args) {
|
|
1250
|
-
const current =
|
|
1251
|
-
|| resolveUserConfig(agent.config, cmd.userId).repo;
|
|
1146
|
+
const current = agent.repo;
|
|
1252
1147
|
// Show available repos as inline keyboard buttons
|
|
1253
1148
|
const repoEntries = Object.entries(this.config.repos);
|
|
1254
1149
|
if (repoEntries.length > 0) {
|
|
@@ -1271,17 +1166,19 @@ export class Bridge extends EventEmitter {
|
|
|
1271
1166
|
break;
|
|
1272
1167
|
}
|
|
1273
1168
|
// Kill current process (different CWD needs new process)
|
|
1274
|
-
|
|
1275
|
-
this.
|
|
1276
|
-
|
|
1277
|
-
|
|
1169
|
+
const oldRepo = agent.repo;
|
|
1170
|
+
this.killAgentProcess(agentId);
|
|
1171
|
+
agent.repo = repoPath;
|
|
1172
|
+
agent.pendingSessionId = null; // clear session when repo changes
|
|
1173
|
+
this.sessionStore.setRepo(agentId, repoPath);
|
|
1278
1174
|
await agent.tgBot.sendText(cmd.chatId, `<blockquote>Repo set to <code>${escapeHtml(shortenRepoPath(repoPath))}</code>. Session cleared.</blockquote>`, 'HTML');
|
|
1175
|
+
// Emit state_changed event
|
|
1176
|
+
this.emitStateChanged(agentId, 'repo', oldRepo, repoPath, 'telegram');
|
|
1279
1177
|
break;
|
|
1280
1178
|
}
|
|
1281
1179
|
case 'cancel': {
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
proc.cancel();
|
|
1180
|
+
if (agent.ccProcess && agent.ccProcess.state === 'active') {
|
|
1181
|
+
agent.ccProcess.cancel();
|
|
1285
1182
|
await agent.tgBot.sendText(cmd.chatId, '<blockquote>Cancelled.</blockquote>', 'HTML');
|
|
1286
1183
|
}
|
|
1287
1184
|
else {
|
|
@@ -1290,18 +1187,15 @@ export class Bridge extends EventEmitter {
|
|
|
1290
1187
|
break;
|
|
1291
1188
|
}
|
|
1292
1189
|
case 'compact': {
|
|
1293
|
-
|
|
1294
|
-
const proc = agent.processes.get(cmd.userId);
|
|
1295
|
-
if (!proc || proc.state !== 'active') {
|
|
1190
|
+
if (!agent.ccProcess || agent.ccProcess.state !== 'active') {
|
|
1296
1191
|
await agent.tgBot.sendText(cmd.chatId, '<blockquote>No active session to compact. Start one first.</blockquote>', 'HTML');
|
|
1297
1192
|
break;
|
|
1298
1193
|
}
|
|
1299
|
-
// Build the compact message: "/compact [optional-instructions]"
|
|
1300
1194
|
const compactMsg = cmd.args?.trim()
|
|
1301
1195
|
? `/compact ${cmd.args.trim()}`
|
|
1302
1196
|
: '/compact';
|
|
1303
|
-
await agent.tgBot.sendText(cmd.chatId, '
|
|
1304
|
-
|
|
1197
|
+
await agent.tgBot.sendText(cmd.chatId, formatSystemMessage('status', 'Compacting…'), 'HTML');
|
|
1198
|
+
agent.ccProcess.sendMessage(createTextMessage(compactMsg));
|
|
1305
1199
|
break;
|
|
1306
1200
|
}
|
|
1307
1201
|
case 'catchup': {
|
|
@@ -1311,22 +1205,20 @@ export class Bridge extends EventEmitter {
|
|
|
1311
1205
|
}
|
|
1312
1206
|
case 'permissions': {
|
|
1313
1207
|
const validModes = ['dangerously-skip', 'acceptEdits', 'default', 'plan'];
|
|
1314
|
-
const userState = this.sessionStore.getUser(agentId, cmd.userId);
|
|
1315
1208
|
const agentDefault = agent.config.defaults.permissionMode;
|
|
1316
|
-
const
|
|
1209
|
+
const agentState = this.sessionStore.getAgent(agentId);
|
|
1210
|
+
const currentMode = agentState.permissionMode || agentDefault;
|
|
1317
1211
|
if (cmd.args) {
|
|
1318
1212
|
const mode = cmd.args.trim();
|
|
1319
1213
|
if (!validModes.includes(mode)) {
|
|
1320
1214
|
await agent.tgBot.sendText(cmd.chatId, `<blockquote>Invalid mode. Valid: ${validModes.join(', ')}</blockquote>`, 'HTML');
|
|
1321
1215
|
break;
|
|
1322
1216
|
}
|
|
1323
|
-
this.sessionStore.setPermissionMode(agentId,
|
|
1324
|
-
|
|
1325
|
-
this.disconnectClient(agentId, cmd.userId, cmd.chatId);
|
|
1217
|
+
this.sessionStore.setPermissionMode(agentId, mode);
|
|
1218
|
+
this.killAgentProcess(agentId);
|
|
1326
1219
|
await agent.tgBot.sendText(cmd.chatId, `Permission mode set to <code>${escapeHtml(mode)}</code>. Takes effect on next message.`, 'HTML');
|
|
1327
1220
|
break;
|
|
1328
1221
|
}
|
|
1329
|
-
// No args — show current mode + inline keyboard
|
|
1330
1222
|
const keyboard = new InlineKeyboard();
|
|
1331
1223
|
keyboard.text('🔓 Bypass', 'permissions:dangerously-skip').text('✏️ Accept Edits', 'permissions:acceptEdits').row();
|
|
1332
1224
|
keyboard.text('🔒 Default', 'permissions:default').text('📋 Plan', 'permissions:plan').row();
|
|
@@ -1340,22 +1232,23 @@ export class Bridge extends EventEmitter {
|
|
|
1340
1232
|
const agent = this.agents.get(agentId);
|
|
1341
1233
|
if (!agent)
|
|
1342
1234
|
return;
|
|
1235
|
+
if (!agent.tgBot)
|
|
1236
|
+
return; // ephemeral agents don't have TG bots
|
|
1343
1237
|
this.logger.debug({ agentId, action: query.action, data: query.data }, 'Callback query');
|
|
1344
1238
|
switch (query.action) {
|
|
1345
1239
|
case 'resume': {
|
|
1346
1240
|
const sessionId = query.data;
|
|
1347
|
-
this.
|
|
1348
|
-
|
|
1241
|
+
this.killAgentProcess(agentId);
|
|
1242
|
+
agent.pendingSessionId = sessionId;
|
|
1349
1243
|
await agent.tgBot.answerCallbackQuery(query.callbackQueryId, 'Session set');
|
|
1350
1244
|
await agent.tgBot.sendText(query.chatId, `Will resume session <code>${escapeHtml(sessionId.slice(0, 8))}</code> on next message.`, 'HTML');
|
|
1351
1245
|
break;
|
|
1352
1246
|
}
|
|
1353
1247
|
case 'delete': {
|
|
1354
1248
|
const sessionId = query.data;
|
|
1355
|
-
//
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
this.sessionStore.setCurrentSession(agentId, query.userId, '');
|
|
1249
|
+
// Kill process if it's running this session
|
|
1250
|
+
if (agent.ccProcess?.sessionId === sessionId) {
|
|
1251
|
+
this.killAgentProcess(agentId);
|
|
1359
1252
|
}
|
|
1360
1253
|
await agent.tgBot.answerCallbackQuery(query.callbackQueryId, 'Session cleared');
|
|
1361
1254
|
await agent.tgBot.sendText(query.chatId, `Session <code>${escapeHtml(sessionId.slice(0, 8))}</code> cleared.`, 'HTML');
|
|
@@ -1368,13 +1261,15 @@ export class Bridge extends EventEmitter {
|
|
|
1368
1261
|
await agent.tgBot.answerCallbackQuery(query.callbackQueryId, 'Path not found');
|
|
1369
1262
|
break;
|
|
1370
1263
|
}
|
|
1371
|
-
|
|
1372
|
-
this.
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
this.sessionStore.
|
|
1264
|
+
const oldRepoCb = agent.repo;
|
|
1265
|
+
this.killAgentProcess(agentId);
|
|
1266
|
+
agent.repo = repoPath;
|
|
1267
|
+
agent.pendingSessionId = null;
|
|
1268
|
+
this.sessionStore.setRepo(agentId, repoPath);
|
|
1376
1269
|
await agent.tgBot.answerCallbackQuery(query.callbackQueryId, `Repo: ${repoName}`);
|
|
1377
1270
|
await agent.tgBot.sendText(query.chatId, `<blockquote>Repo set to <code>${escapeHtml(shortenRepoPath(repoPath))}</code>. Session cleared.</blockquote>`, 'HTML');
|
|
1271
|
+
// Emit state_changed event
|
|
1272
|
+
this.emitStateChanged(agentId, 'repo', oldRepoCb, repoPath, 'telegram');
|
|
1378
1273
|
break;
|
|
1379
1274
|
}
|
|
1380
1275
|
case 'repo_add': {
|
|
@@ -1389,10 +1284,14 @@ export class Bridge extends EventEmitter {
|
|
|
1389
1284
|
await agent.tgBot.sendText(query.chatId, 'Send: <code>/model <model-name></code>', 'HTML');
|
|
1390
1285
|
break;
|
|
1391
1286
|
}
|
|
1392
|
-
|
|
1393
|
-
|
|
1287
|
+
const oldModelCb = agent.model;
|
|
1288
|
+
agent.model = model;
|
|
1289
|
+
this.sessionStore.setModel(agentId, model);
|
|
1290
|
+
this.killAgentProcess(agentId);
|
|
1394
1291
|
await agent.tgBot.answerCallbackQuery(query.callbackQueryId, `Model: ${model}`);
|
|
1395
1292
|
await agent.tgBot.sendText(query.chatId, `<blockquote>Model set to <code>${escapeHtml(model)}</code>. Process respawned.</blockquote>`, 'HTML');
|
|
1293
|
+
// Emit state_changed event
|
|
1294
|
+
this.emitStateChanged(agentId, 'model', oldModelCb, model, 'telegram');
|
|
1396
1295
|
break;
|
|
1397
1296
|
}
|
|
1398
1297
|
case 'permissions': {
|
|
@@ -1402,9 +1301,8 @@ export class Bridge extends EventEmitter {
|
|
|
1402
1301
|
await agent.tgBot.answerCallbackQuery(query.callbackQueryId, 'Invalid mode');
|
|
1403
1302
|
break;
|
|
1404
1303
|
}
|
|
1405
|
-
this.sessionStore.setPermissionMode(agentId,
|
|
1406
|
-
|
|
1407
|
-
this.disconnectClient(agentId, query.userId, query.chatId);
|
|
1304
|
+
this.sessionStore.setPermissionMode(agentId, mode);
|
|
1305
|
+
this.killAgentProcess(agentId);
|
|
1408
1306
|
await agent.tgBot.answerCallbackQuery(query.callbackQueryId, `Mode: ${mode}`);
|
|
1409
1307
|
await agent.tgBot.sendText(query.chatId, `Permission mode set to <code>${escapeHtml(mode)}</code>. Takes effect on next message.`, 'HTML');
|
|
1410
1308
|
break;
|
|
@@ -1431,9 +1329,8 @@ export class Bridge extends EventEmitter {
|
|
|
1431
1329
|
await agent.tgBot.answerCallbackQuery(query.callbackQueryId, 'Permission expired');
|
|
1432
1330
|
break;
|
|
1433
1331
|
}
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
proc.respondToPermission(requestId, true);
|
|
1332
|
+
if (agent.ccProcess) {
|
|
1333
|
+
agent.ccProcess.respondToPermission(requestId, true);
|
|
1437
1334
|
}
|
|
1438
1335
|
agent.pendingPermissions.delete(requestId);
|
|
1439
1336
|
await agent.tgBot.answerCallbackQuery(query.callbackQueryId, '✅ Allowed');
|
|
@@ -1446,27 +1343,22 @@ export class Bridge extends EventEmitter {
|
|
|
1446
1343
|
await agent.tgBot.answerCallbackQuery(query.callbackQueryId, 'Permission expired');
|
|
1447
1344
|
break;
|
|
1448
1345
|
}
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
proc.respondToPermission(requestId, false);
|
|
1346
|
+
if (agent.ccProcess) {
|
|
1347
|
+
agent.ccProcess.respondToPermission(requestId, false);
|
|
1452
1348
|
}
|
|
1453
1349
|
agent.pendingPermissions.delete(requestId);
|
|
1454
1350
|
await agent.tgBot.answerCallbackQuery(query.callbackQueryId, '❌ Denied');
|
|
1455
1351
|
break;
|
|
1456
1352
|
}
|
|
1457
1353
|
case 'perm_allow_all': {
|
|
1458
|
-
// Allow all pending permissions for this
|
|
1459
|
-
const targetUserId = query.data;
|
|
1354
|
+
// Allow all pending permissions for this agent
|
|
1460
1355
|
const toAllow = [];
|
|
1461
|
-
for (const [reqId
|
|
1462
|
-
|
|
1463
|
-
toAllow.push(reqId);
|
|
1464
|
-
}
|
|
1356
|
+
for (const [reqId] of agent.pendingPermissions) {
|
|
1357
|
+
toAllow.push(reqId);
|
|
1465
1358
|
}
|
|
1466
|
-
const proc = agent.processes.get(targetUserId);
|
|
1467
1359
|
for (const reqId of toAllow) {
|
|
1468
|
-
if (
|
|
1469
|
-
|
|
1360
|
+
if (agent.ccProcess)
|
|
1361
|
+
agent.ccProcess.respondToPermission(reqId, true);
|
|
1470
1362
|
agent.pendingPermissions.delete(reqId);
|
|
1471
1363
|
}
|
|
1472
1364
|
await agent.tgBot.answerCallbackQuery(query.callbackQueryId, `✅ Allowed ${toAllow.length} permission(s)`);
|
|
@@ -1480,24 +1372,18 @@ export class Bridge extends EventEmitter {
|
|
|
1480
1372
|
handleCtlMessage(agentId, text, sessionId) {
|
|
1481
1373
|
const agent = this.agents.get(agentId);
|
|
1482
1374
|
if (!agent) {
|
|
1483
|
-
// Return error via the CtlAckResponse shape won't work — but the ctl-server
|
|
1484
|
-
// protocol handles errors separately. We'll throw and let it catch.
|
|
1485
1375
|
throw new Error(`Unknown agent: ${agentId}`);
|
|
1486
1376
|
}
|
|
1487
|
-
//
|
|
1488
|
-
const userId = agent.config.allowedUsers[0];
|
|
1489
|
-
const chatId = Number(userId);
|
|
1490
|
-
// If explicit session requested, set it
|
|
1377
|
+
// If explicit session requested, set it as pending
|
|
1491
1378
|
if (sessionId) {
|
|
1492
|
-
|
|
1379
|
+
agent.pendingSessionId = sessionId;
|
|
1493
1380
|
}
|
|
1494
1381
|
// Route through the same sendToCC path as Telegram
|
|
1495
|
-
this.sendToCC(agentId,
|
|
1496
|
-
const proc = agent.processes.get(userId);
|
|
1382
|
+
this.sendToCC(agentId, { text }, { spawnSource: 'cli' });
|
|
1497
1383
|
return {
|
|
1498
1384
|
type: 'ack',
|
|
1499
|
-
sessionId:
|
|
1500
|
-
state:
|
|
1385
|
+
sessionId: agent.ccProcess?.sessionId ?? null,
|
|
1386
|
+
state: agent.ccProcess?.state ?? 'idle',
|
|
1501
1387
|
};
|
|
1502
1388
|
}
|
|
1503
1389
|
handleCtlStatus(agentId) {
|
|
@@ -1508,30 +1394,16 @@ export class Bridge extends EventEmitter {
|
|
|
1508
1394
|
const agent = this.agents.get(id);
|
|
1509
1395
|
if (!agent)
|
|
1510
1396
|
continue;
|
|
1511
|
-
|
|
1512
|
-
let state = 'idle';
|
|
1513
|
-
for (const [, proc] of agent.processes) {
|
|
1514
|
-
if (proc.state === 'active') {
|
|
1515
|
-
state = 'active';
|
|
1516
|
-
break;
|
|
1517
|
-
}
|
|
1518
|
-
if (proc.state === 'spawning')
|
|
1519
|
-
state = 'spawning';
|
|
1520
|
-
}
|
|
1521
|
-
const userId = agent.config.allowedUsers[0];
|
|
1522
|
-
const proc = agent.processes.get(userId);
|
|
1523
|
-
const userConfig = resolveUserConfig(agent.config, userId);
|
|
1397
|
+
const state = agent.ccProcess?.state ?? 'idle';
|
|
1524
1398
|
agents.push({
|
|
1525
1399
|
id,
|
|
1526
1400
|
state,
|
|
1527
|
-
sessionId:
|
|
1528
|
-
repo:
|
|
1401
|
+
sessionId: agent.ccProcess?.sessionId ?? null,
|
|
1402
|
+
repo: agent.repo,
|
|
1529
1403
|
});
|
|
1530
|
-
// List sessions
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
if (sessRepo) {
|
|
1534
|
-
for (const d of discoverCCSessions(sessRepo, 5)) {
|
|
1404
|
+
// List sessions from CC's session directory
|
|
1405
|
+
if (agent.repo) {
|
|
1406
|
+
for (const d of discoverCCSessions(agent.repo, 5)) {
|
|
1535
1407
|
sessions.push({
|
|
1536
1408
|
id: d.id,
|
|
1537
1409
|
agentId: id,
|
|
@@ -1549,10 +1421,59 @@ export class Bridge extends EventEmitter {
|
|
|
1549
1421
|
if (!agent) {
|
|
1550
1422
|
return { id: request.id, success: false, error: `Unknown agent: ${request.agentId}` };
|
|
1551
1423
|
}
|
|
1552
|
-
// Find the chat ID for this user (from the most recent message)
|
|
1553
|
-
// We use the userId to find which chat to send to
|
|
1554
|
-
const chatId = Number(request.userId); // In TG, private chat ID === user ID
|
|
1555
1424
|
try {
|
|
1425
|
+
// Supervisor-routed tools (don't need TG chatId)
|
|
1426
|
+
switch (request.tool) {
|
|
1427
|
+
case 'notify_parent': {
|
|
1428
|
+
if (!this.supervisorWrite) {
|
|
1429
|
+
return { id: request.id, success: false, error: 'No supervisor connected' };
|
|
1430
|
+
}
|
|
1431
|
+
this.sendToSupervisor({
|
|
1432
|
+
type: 'event',
|
|
1433
|
+
event: 'cc_message',
|
|
1434
|
+
agentId: request.agentId,
|
|
1435
|
+
text: request.params.message,
|
|
1436
|
+
priority: request.params.priority || 'info',
|
|
1437
|
+
});
|
|
1438
|
+
return { id: request.id, success: true };
|
|
1439
|
+
}
|
|
1440
|
+
case 'supervisor_exec': {
|
|
1441
|
+
if (!this.supervisorWrite) {
|
|
1442
|
+
return { id: request.id, success: false, error: 'No supervisor connected' };
|
|
1443
|
+
}
|
|
1444
|
+
const timeoutMs = request.params.timeoutMs || 60000;
|
|
1445
|
+
const result = await this.sendSupervisorRequest({
|
|
1446
|
+
type: 'command',
|
|
1447
|
+
requestId: randomUUID(),
|
|
1448
|
+
action: 'exec',
|
|
1449
|
+
params: {
|
|
1450
|
+
command: request.params.command,
|
|
1451
|
+
agentId: request.agentId,
|
|
1452
|
+
timeoutMs,
|
|
1453
|
+
},
|
|
1454
|
+
}, timeoutMs);
|
|
1455
|
+
return { id: request.id, success: true, result };
|
|
1456
|
+
}
|
|
1457
|
+
case 'supervisor_notify': {
|
|
1458
|
+
if (!this.supervisorWrite) {
|
|
1459
|
+
return { id: request.id, success: false, error: 'No supervisor connected' };
|
|
1460
|
+
}
|
|
1461
|
+
this.sendToSupervisor({
|
|
1462
|
+
type: 'event',
|
|
1463
|
+
event: 'notification',
|
|
1464
|
+
agentId: request.agentId,
|
|
1465
|
+
title: request.params.title,
|
|
1466
|
+
body: request.params.body,
|
|
1467
|
+
priority: request.params.priority || 'active',
|
|
1468
|
+
});
|
|
1469
|
+
return { id: request.id, success: true };
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
// TG tools (need chatId and tgBot)
|
|
1473
|
+
const chatId = this.getAgentChatId(agent);
|
|
1474
|
+
if (!chatId || !agent.tgBot) {
|
|
1475
|
+
return { id: request.id, success: false, error: `No chat ID for agent: ${request.agentId}` };
|
|
1476
|
+
}
|
|
1556
1477
|
switch (request.tool) {
|
|
1557
1478
|
case 'send_file':
|
|
1558
1479
|
await agent.tgBot.sendFile(chatId, request.params.path, request.params.caption);
|
|
@@ -1571,6 +1492,320 @@ export class Bridge extends EventEmitter {
|
|
|
1571
1492
|
return { id: request.id, success: false, error: err instanceof Error ? err.message : 'Unknown error' };
|
|
1572
1493
|
}
|
|
1573
1494
|
}
|
|
1495
|
+
// ── Supervisor protocol ──
|
|
1496
|
+
isSupervisorSubscribed(agentId, sessionId) {
|
|
1497
|
+
return this.supervisorSubscriptions.has(`${agentId}:*`) ||
|
|
1498
|
+
(sessionId !== null && this.supervisorSubscriptions.has(`${agentId}:${sessionId}`));
|
|
1499
|
+
}
|
|
1500
|
+
registerSupervisor(agentId, capabilities, writeFn) {
|
|
1501
|
+
this.supervisorAgentId = agentId;
|
|
1502
|
+
this.supervisorWrite = writeFn;
|
|
1503
|
+
this.supervisorSubscriptions.clear();
|
|
1504
|
+
this.logger.info({ agentId, capabilities }, 'Supervisor registered');
|
|
1505
|
+
}
|
|
1506
|
+
handleSupervisorDetach() {
|
|
1507
|
+
this.logger.info({ agentId: this.supervisorAgentId }, 'Supervisor detached');
|
|
1508
|
+
this.supervisorSubscriptions.clear();
|
|
1509
|
+
this.supervisorWrite = null;
|
|
1510
|
+
this.supervisorAgentId = null;
|
|
1511
|
+
}
|
|
1512
|
+
handleSupervisorLine(line) {
|
|
1513
|
+
let msg;
|
|
1514
|
+
try {
|
|
1515
|
+
msg = JSON.parse(line);
|
|
1516
|
+
}
|
|
1517
|
+
catch {
|
|
1518
|
+
this.sendToSupervisor({ type: 'error', message: 'Invalid JSON' });
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
// Handle responses to commands we sent to the supervisor (e.g. exec results)
|
|
1522
|
+
if (msg.type === 'response' && msg.requestId) {
|
|
1523
|
+
const pending = this.supervisorPendingRequests.get(msg.requestId);
|
|
1524
|
+
if (pending) {
|
|
1525
|
+
clearTimeout(pending.timer);
|
|
1526
|
+
this.supervisorPendingRequests.delete(msg.requestId);
|
|
1527
|
+
if (msg.error) {
|
|
1528
|
+
pending.reject(new Error(msg.error));
|
|
1529
|
+
}
|
|
1530
|
+
else {
|
|
1531
|
+
pending.resolve(msg.result);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
return;
|
|
1535
|
+
}
|
|
1536
|
+
if (msg.type !== 'command' || !msg.action) {
|
|
1537
|
+
this.sendToSupervisor({ type: 'error', message: 'Expected {type:"command", action:"..."}' });
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1540
|
+
const requestId = msg.requestId;
|
|
1541
|
+
const params = msg.params ?? {};
|
|
1542
|
+
try {
|
|
1543
|
+
const result = this.handleSupervisorCommand(msg.action, params);
|
|
1544
|
+
this.sendToSupervisor({ type: 'response', requestId, result });
|
|
1545
|
+
}
|
|
1546
|
+
catch (err) {
|
|
1547
|
+
this.sendToSupervisor({ type: 'response', requestId, error: err instanceof Error ? err.message : String(err) });
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
handleSupervisorCommand(action, params) {
|
|
1551
|
+
switch (action) {
|
|
1552
|
+
case 'ping':
|
|
1553
|
+
return { pong: true, uptime: process.uptime() };
|
|
1554
|
+
// ── Phase 2: Ephemeral agents ──
|
|
1555
|
+
case 'create_agent': {
|
|
1556
|
+
const agentId = params.agentId || `oc-spawn-${randomUUID().slice(0, 8)}`;
|
|
1557
|
+
const repo = params.repo;
|
|
1558
|
+
if (!repo)
|
|
1559
|
+
throw new Error('Missing required param: repo');
|
|
1560
|
+
if (this.agents.has(agentId)) {
|
|
1561
|
+
throw new Error(`Agent already exists: ${agentId}`);
|
|
1562
|
+
}
|
|
1563
|
+
// Map supervisor permissionMode to CC permissionMode
|
|
1564
|
+
let permMode = 'default';
|
|
1565
|
+
const reqPerm = params.permissionMode;
|
|
1566
|
+
if (reqPerm === 'bypassPermissions' || reqPerm === 'dangerously-skip')
|
|
1567
|
+
permMode = 'dangerously-skip';
|
|
1568
|
+
else if (reqPerm === 'acceptEdits')
|
|
1569
|
+
permMode = 'acceptEdits';
|
|
1570
|
+
else if (reqPerm === 'plan')
|
|
1571
|
+
permMode = 'plan';
|
|
1572
|
+
const ephemeralConfig = {
|
|
1573
|
+
botToken: '',
|
|
1574
|
+
allowedUsers: [],
|
|
1575
|
+
defaults: {
|
|
1576
|
+
model: params.model || 'sonnet',
|
|
1577
|
+
repo,
|
|
1578
|
+
maxTurns: 200,
|
|
1579
|
+
idleTimeoutMs: 300_000,
|
|
1580
|
+
hangTimeoutMs: 300_000,
|
|
1581
|
+
permissionMode: permMode,
|
|
1582
|
+
},
|
|
1583
|
+
};
|
|
1584
|
+
const instance = {
|
|
1585
|
+
id: agentId,
|
|
1586
|
+
config: ephemeralConfig,
|
|
1587
|
+
tgBot: null,
|
|
1588
|
+
ephemeral: true,
|
|
1589
|
+
repo,
|
|
1590
|
+
model: ephemeralConfig.defaults.model,
|
|
1591
|
+
ccProcess: null,
|
|
1592
|
+
accumulator: null,
|
|
1593
|
+
subAgentTracker: null,
|
|
1594
|
+
batcher: null,
|
|
1595
|
+
pendingPermissions: new Map(),
|
|
1596
|
+
typingInterval: null,
|
|
1597
|
+
typingChatId: null,
|
|
1598
|
+
pendingSessionId: null,
|
|
1599
|
+
destroyTimer: null,
|
|
1600
|
+
eventBuffer: new EventBuffer(),
|
|
1601
|
+
};
|
|
1602
|
+
// Auto-destroy timer
|
|
1603
|
+
const timeoutMs = params.timeoutMs;
|
|
1604
|
+
if (timeoutMs && timeoutMs > 0) {
|
|
1605
|
+
instance.destroyTimer = setTimeout(() => {
|
|
1606
|
+
this.logger.info({ agentId, timeoutMs }, 'Ephemeral agent timeout — auto-destroying');
|
|
1607
|
+
this.destroyEphemeralAgent(agentId);
|
|
1608
|
+
}, timeoutMs);
|
|
1609
|
+
}
|
|
1610
|
+
this.agents.set(agentId, instance);
|
|
1611
|
+
this.logger.info({ agentId, repo, model: instance.model, ephemeral: true }, 'Ephemeral agent created');
|
|
1612
|
+
// Emit agent_created event (always sent, not subscription-gated)
|
|
1613
|
+
this.sendToSupervisor({ type: 'event', event: 'agent_created', agentId, agentType: 'ephemeral', repo });
|
|
1614
|
+
return { agentId, state: 'idle' };
|
|
1615
|
+
}
|
|
1616
|
+
case 'destroy_agent': {
|
|
1617
|
+
const agentId = params.agentId;
|
|
1618
|
+
if (!agentId)
|
|
1619
|
+
throw new Error('Missing agentId');
|
|
1620
|
+
const agent = this.agents.get(agentId);
|
|
1621
|
+
if (!agent)
|
|
1622
|
+
throw new Error(`Unknown agent: ${agentId}`);
|
|
1623
|
+
if (!agent.ephemeral)
|
|
1624
|
+
throw new Error(`Cannot destroy persistent agent: ${agentId}`);
|
|
1625
|
+
this.destroyEphemeralAgent(agentId);
|
|
1626
|
+
return { destroyed: true };
|
|
1627
|
+
}
|
|
1628
|
+
// ── Phase 1: Send + Subscribe ──
|
|
1629
|
+
case 'send_message': {
|
|
1630
|
+
const agentId = params.agentId;
|
|
1631
|
+
const text = params.text;
|
|
1632
|
+
if (!agentId || !text)
|
|
1633
|
+
throw new Error('Missing agentId or text');
|
|
1634
|
+
const agent = this.agents.get(agentId);
|
|
1635
|
+
if (!agent)
|
|
1636
|
+
throw new Error(`Unknown agent: ${agentId}`);
|
|
1637
|
+
// Auto-subscribe supervisor
|
|
1638
|
+
this.supervisorSubscriptions.add(`${agentId}:*`);
|
|
1639
|
+
// For persistent agents: send TG system message BEFORE spawning CC
|
|
1640
|
+
const tgChatId = this.getAgentChatId(agent);
|
|
1641
|
+
if (tgChatId && agent.tgBot) {
|
|
1642
|
+
const preview = text.length > 500 ? text.slice(0, 500) + '…' : text;
|
|
1643
|
+
agent.tgBot.sendText(tgChatId, `<blockquote>🦞 <b>OpenClaw:</b> ${escapeHtml(preview)}</blockquote>`, 'HTML', true) // silent
|
|
1644
|
+
.catch(err => this.logger.error({ err, agentId }, 'Failed to send supervisor TG notification'));
|
|
1645
|
+
}
|
|
1646
|
+
// Send to agent's single CC process
|
|
1647
|
+
this.sendToCC(agentId, { text }, { spawnSource: 'supervisor' });
|
|
1648
|
+
return {
|
|
1649
|
+
sessionId: agent.ccProcess?.sessionId ?? null,
|
|
1650
|
+
state: agent.ccProcess?.state ?? 'spawning',
|
|
1651
|
+
subscribed: true,
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
case 'send_to_cc': {
|
|
1655
|
+
const agentId = params.agentId;
|
|
1656
|
+
const text = params.text;
|
|
1657
|
+
if (!agentId || !text)
|
|
1658
|
+
throw new Error('Missing agentId or text');
|
|
1659
|
+
const agent = this.agents.get(agentId);
|
|
1660
|
+
if (!agent)
|
|
1661
|
+
throw new Error(`Unknown agent: ${agentId}`);
|
|
1662
|
+
if (!agent.ccProcess || agent.ccProcess.state === 'idle') {
|
|
1663
|
+
throw new Error(`No active CC process for agent ${agentId}`);
|
|
1664
|
+
}
|
|
1665
|
+
agent.ccProcess.sendMessage(createTextMessage(text));
|
|
1666
|
+
return { sent: true };
|
|
1667
|
+
}
|
|
1668
|
+
case 'subscribe': {
|
|
1669
|
+
const agentId = params.agentId;
|
|
1670
|
+
const sessionId = params.sessionId;
|
|
1671
|
+
if (!agentId)
|
|
1672
|
+
throw new Error('Missing agentId');
|
|
1673
|
+
const key = sessionId ? `${agentId}:${sessionId}` : `${agentId}:*`;
|
|
1674
|
+
this.supervisorSubscriptions.add(key);
|
|
1675
|
+
return { subscribed: true, key };
|
|
1676
|
+
}
|
|
1677
|
+
case 'unsubscribe': {
|
|
1678
|
+
const agentId = params.agentId;
|
|
1679
|
+
if (!agentId)
|
|
1680
|
+
throw new Error('Missing agentId');
|
|
1681
|
+
// Remove all subscriptions for this agent
|
|
1682
|
+
for (const key of [...this.supervisorSubscriptions]) {
|
|
1683
|
+
if (key.startsWith(`${agentId}:`)) {
|
|
1684
|
+
this.supervisorSubscriptions.delete(key);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
return { unsubscribed: true };
|
|
1688
|
+
}
|
|
1689
|
+
case 'status': {
|
|
1690
|
+
const filterAgentId = params.agentId;
|
|
1691
|
+
const agents = [];
|
|
1692
|
+
const agentIds = filterAgentId ? [filterAgentId] : [...this.agents.keys()];
|
|
1693
|
+
for (const id of agentIds) {
|
|
1694
|
+
const agent = this.agents.get(id);
|
|
1695
|
+
if (!agent)
|
|
1696
|
+
continue;
|
|
1697
|
+
const state = agent.ccProcess?.state ?? 'idle';
|
|
1698
|
+
const sessionId = agent.ccProcess?.sessionId ?? null;
|
|
1699
|
+
agents.push({
|
|
1700
|
+
id,
|
|
1701
|
+
type: agent.ephemeral ? 'ephemeral' : 'persistent',
|
|
1702
|
+
state,
|
|
1703
|
+
repo: agent.repo,
|
|
1704
|
+
process: agent.ccProcess ? { sessionId, model: agent.model } : null,
|
|
1705
|
+
supervisorSubscribed: this.isSupervisorSubscribed(id, sessionId),
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
return { agents };
|
|
1709
|
+
}
|
|
1710
|
+
case 'kill_cc': {
|
|
1711
|
+
const agentId = params.agentId;
|
|
1712
|
+
if (!agentId)
|
|
1713
|
+
throw new Error('Missing agentId');
|
|
1714
|
+
const agent = this.agents.get(agentId);
|
|
1715
|
+
if (!agent)
|
|
1716
|
+
throw new Error(`Unknown agent: ${agentId}`);
|
|
1717
|
+
const killed = agent.ccProcess != null && agent.ccProcess.state !== 'idle';
|
|
1718
|
+
if (killed) {
|
|
1719
|
+
this.killAgentProcess(agentId);
|
|
1720
|
+
}
|
|
1721
|
+
return { killed };
|
|
1722
|
+
}
|
|
1723
|
+
// ── Phase A: Observability ──
|
|
1724
|
+
case 'get_log': {
|
|
1725
|
+
const agentId = params.agentId;
|
|
1726
|
+
if (!agentId)
|
|
1727
|
+
throw new Error('Missing agentId');
|
|
1728
|
+
const agent = this.agents.get(agentId);
|
|
1729
|
+
if (!agent)
|
|
1730
|
+
throw new Error(`Unknown agent: ${agentId}`);
|
|
1731
|
+
return agent.eventBuffer.query({
|
|
1732
|
+
offset: params.offset,
|
|
1733
|
+
limit: params.limit,
|
|
1734
|
+
grep: params.grep,
|
|
1735
|
+
since: params.since,
|
|
1736
|
+
type: params.type,
|
|
1737
|
+
});
|
|
1738
|
+
}
|
|
1739
|
+
case 'permission_response': {
|
|
1740
|
+
const agentId = params.agentId;
|
|
1741
|
+
const permissionRequestId = params.permissionRequestId;
|
|
1742
|
+
const decision = params.decision;
|
|
1743
|
+
if (!agentId || !permissionRequestId || !decision) {
|
|
1744
|
+
throw new Error('Missing agentId, permissionRequestId, or decision');
|
|
1745
|
+
}
|
|
1746
|
+
const agent = this.agents.get(agentId);
|
|
1747
|
+
if (!agent)
|
|
1748
|
+
throw new Error(`Unknown agent: ${agentId}`);
|
|
1749
|
+
const pending = agent.pendingPermissions.get(permissionRequestId);
|
|
1750
|
+
if (!pending)
|
|
1751
|
+
throw new Error(`No pending permission with id: ${permissionRequestId}`);
|
|
1752
|
+
const allow = decision === 'allow';
|
|
1753
|
+
if (agent.ccProcess) {
|
|
1754
|
+
agent.ccProcess.respondToPermission(permissionRequestId, allow);
|
|
1755
|
+
}
|
|
1756
|
+
agent.pendingPermissions.delete(permissionRequestId);
|
|
1757
|
+
return { responded: true, decision };
|
|
1758
|
+
}
|
|
1759
|
+
default:
|
|
1760
|
+
throw new Error(`Unknown supervisor action: ${action}`);
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
emitStateChanged(agentId, field, oldValue, newValue, source) {
|
|
1764
|
+
this.logger.info({ agentId, field, oldValue, newValue, source }, 'Agent state changed');
|
|
1765
|
+
const agent = this.agents.get(agentId);
|
|
1766
|
+
if (agent) {
|
|
1767
|
+
agent.eventBuffer.push({ ts: Date.now(), type: 'system', text: `State changed: ${field} → ${newValue}` });
|
|
1768
|
+
}
|
|
1769
|
+
if (this.isSupervisorSubscribed(agentId, agent?.ccProcess?.sessionId ?? null)) {
|
|
1770
|
+
this.sendToSupervisor({
|
|
1771
|
+
type: 'event',
|
|
1772
|
+
event: 'state_changed',
|
|
1773
|
+
agentId,
|
|
1774
|
+
field,
|
|
1775
|
+
oldValue,
|
|
1776
|
+
newValue,
|
|
1777
|
+
source,
|
|
1778
|
+
});
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
sendToSupervisor(msg) {
|
|
1782
|
+
if (this.supervisorWrite) {
|
|
1783
|
+
try {
|
|
1784
|
+
this.supervisorWrite(JSON.stringify(msg) + '\n');
|
|
1785
|
+
}
|
|
1786
|
+
catch { }
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
sendSupervisorRequest(msg, timeoutMs) {
|
|
1790
|
+
return new Promise((resolve, reject) => {
|
|
1791
|
+
const requestId = msg.requestId ?? '';
|
|
1792
|
+
const timer = setTimeout(() => {
|
|
1793
|
+
this.supervisorPendingRequests.delete(requestId);
|
|
1794
|
+
reject(new Error('Supervisor request timed out'));
|
|
1795
|
+
}, timeoutMs);
|
|
1796
|
+
this.supervisorPendingRequests.set(requestId, { resolve, reject, timer });
|
|
1797
|
+
this.sendToSupervisor(msg);
|
|
1798
|
+
});
|
|
1799
|
+
}
|
|
1800
|
+
destroyEphemeralAgent(agentId) {
|
|
1801
|
+
const agent = this.agents.get(agentId);
|
|
1802
|
+
if (!agent || !agent.ephemeral)
|
|
1803
|
+
return;
|
|
1804
|
+
this.killAgentProcess(agentId);
|
|
1805
|
+
this.agents.delete(agentId);
|
|
1806
|
+
this.logger.info({ agentId }, 'Ephemeral agent destroyed');
|
|
1807
|
+
this.sendToSupervisor({ type: 'event', event: 'agent_destroyed', agentId });
|
|
1808
|
+
}
|
|
1574
1809
|
// ── Shutdown ──
|
|
1575
1810
|
async stop() {
|
|
1576
1811
|
this.logger.info('Stopping bridge');
|
|
@@ -1578,6 +1813,7 @@ export class Bridge extends EventEmitter {
|
|
|
1578
1813
|
await this.stopAgent(agentId);
|
|
1579
1814
|
}
|
|
1580
1815
|
this.processRegistry.clear();
|
|
1816
|
+
this.highSignalDetector.destroy();
|
|
1581
1817
|
this.mcpServer.closeAll();
|
|
1582
1818
|
this.ctlServer.closeAll();
|
|
1583
1819
|
this.removeAllListeners();
|