@fonz/tgcc 0.6.15 → 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 +898 -629
- package/dist/bridge.js.map +1 -1
- package/dist/cc-process.js +11 -0
- package/dist/cc-process.js.map +1 -1
- package/dist/cc-protocol.d.ts +11 -1
- package/dist/cc-protocol.js.map +1 -1
- package/dist/cli.js +0 -0
- 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 +63 -39
- package/dist/session.js.map +1 -1
- package/dist/streaming.d.ts +30 -8
- package/dist/streaming.js +270 -118
- package/dist/streaming.js.map +1 -1
- package/dist/telegram.d.ts +3 -1
- package/dist/telegram.js +19 -1
- package/dist/telegram.js.map +1 -1
- package/package.json +1 -1
- package/dist/telegram-html.d.ts +0 -5
- package/dist/telegram-html.js +0 -126
- package/dist/telegram-html.js.map +0 -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 = [];
|
|
@@ -84,6 +87,7 @@ const HELP_TEXT = `<b>TGCC Commands</b>
|
|
|
84
87
|
|
|
85
88
|
<b>Control</b>
|
|
86
89
|
/cancel — Abort current CC turn
|
|
90
|
+
/compact [instructions] — Compact conversation context
|
|
87
91
|
/model <name> — Switch model
|
|
88
92
|
/permissions — Set permission mode
|
|
89
93
|
/repo — List repos (buttons)
|
|
@@ -99,11 +103,18 @@ export class Bridge extends EventEmitter {
|
|
|
99
103
|
config;
|
|
100
104
|
agents = new Map();
|
|
101
105
|
processRegistry = new ProcessRegistry();
|
|
102
|
-
sessionModelOverrides = new Map(); // "agentId:userId" → model (from /model cmd, not persisted)
|
|
103
106
|
mcpServer;
|
|
104
107
|
ctlServer;
|
|
105
108
|
sessionStore;
|
|
106
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();
|
|
107
118
|
constructor(config, logger) {
|
|
108
119
|
super();
|
|
109
120
|
this.config = config;
|
|
@@ -111,6 +122,18 @@ export class Bridge extends EventEmitter {
|
|
|
111
122
|
this.sessionStore = new SessionStore(config.global.stateFile, this.logger);
|
|
112
123
|
this.mcpServer = new McpBridgeServer((req) => this.handleMcpToolRequest(req), this.logger);
|
|
113
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
|
+
});
|
|
114
137
|
}
|
|
115
138
|
// ── Startup ──
|
|
116
139
|
async start() {
|
|
@@ -119,20 +142,37 @@ export class Bridge extends EventEmitter {
|
|
|
119
142
|
await this.startAgent(agentId, agentConfig);
|
|
120
143
|
}
|
|
121
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
|
+
});
|
|
122
152
|
}
|
|
123
153
|
async startAgent(agentId, agentConfig) {
|
|
124
154
|
this.logger.info({ agentId }, 'Starting agent');
|
|
125
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;
|
|
126
159
|
const instance = {
|
|
127
160
|
id: agentId,
|
|
128
161
|
config: agentConfig,
|
|
129
162
|
tgBot,
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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,
|
|
134
170
|
pendingPermissions: new Map(),
|
|
135
|
-
|
|
171
|
+
typingInterval: null,
|
|
172
|
+
typingChatId: null,
|
|
173
|
+
pendingSessionId: null,
|
|
174
|
+
destroyTimer: null,
|
|
175
|
+
eventBuffer: new EventBuffer(),
|
|
136
176
|
};
|
|
137
177
|
this.agents.set(agentId, instance);
|
|
138
178
|
await tgBot.start();
|
|
@@ -173,67 +213,62 @@ export class Bridge extends EventEmitter {
|
|
|
173
213
|
const agent = this.agents.get(agentId);
|
|
174
214
|
if (!agent)
|
|
175
215
|
return;
|
|
176
|
-
this.logger.info({ agentId }, 'Stopping agent');
|
|
177
|
-
// Stop bot
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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;
|
|
184
224
|
}
|
|
185
|
-
// Kill
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
// 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;
|
|
189
228
|
const exitPromise = new Promise((resolve) => {
|
|
190
229
|
const onExit = () => {
|
|
191
230
|
proc.off('exit', onExit);
|
|
192
231
|
resolve();
|
|
193
232
|
};
|
|
194
233
|
proc.on('exit', onExit);
|
|
195
|
-
// Timeout after 3 seconds if process doesn't exit
|
|
196
234
|
const timeoutId = setTimeout(() => {
|
|
197
235
|
proc.off('exit', onExit);
|
|
198
236
|
resolve();
|
|
199
237
|
}, 3000);
|
|
200
|
-
// Clear timeout if process exits before timeout
|
|
201
238
|
proc.on('exit', () => clearTimeout(timeoutId));
|
|
202
239
|
});
|
|
203
|
-
processExitPromises.push(exitPromise);
|
|
204
240
|
proc.destroy();
|
|
241
|
+
await exitPromise;
|
|
242
|
+
// Unsubscribe from registry
|
|
243
|
+
const clientRef = { agentId, userId: agentId, chatId: 0 };
|
|
244
|
+
this.processRegistry.unsubscribe(clientRef);
|
|
205
245
|
}
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
batcher.cancel();
|
|
211
|
-
batcher.destroy();
|
|
212
|
-
}
|
|
213
|
-
// Close MCP sockets
|
|
214
|
-
for (const userId of agent.processes.keys()) {
|
|
215
|
-
const socketPath = join(this.config.global.socketDir, `${agentId}-${userId}.sock`);
|
|
216
|
-
this.mcpServer.close(socketPath);
|
|
246
|
+
// Cancel batcher
|
|
247
|
+
if (agent.batcher) {
|
|
248
|
+
agent.batcher.cancel();
|
|
249
|
+
agent.batcher.destroy();
|
|
217
250
|
}
|
|
218
|
-
//
|
|
219
|
-
|
|
220
|
-
|
|
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;
|
|
221
258
|
}
|
|
222
|
-
agent
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
259
|
+
// Clean up sub-agent tracker
|
|
260
|
+
if (agent.subAgentTracker) {
|
|
261
|
+
agent.subAgentTracker.reset();
|
|
262
|
+
agent.subAgentTracker = null;
|
|
226
263
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
clearInterval(interval);
|
|
264
|
+
// Clear typing indicator
|
|
265
|
+
if (agent.typingInterval) {
|
|
266
|
+
clearInterval(agent.typingInterval);
|
|
267
|
+
agent.typingInterval = null;
|
|
232
268
|
}
|
|
233
|
-
agent.typingIntervals.clear();
|
|
234
269
|
agent.pendingPermissions.clear();
|
|
235
|
-
agent.
|
|
236
|
-
agent.
|
|
270
|
+
agent.ccProcess = null;
|
|
271
|
+
agent.batcher = null;
|
|
237
272
|
// Close control socket
|
|
238
273
|
const ctlSocketPath = join('/tmp/tgcc/ctl', `${agentId}.sock`);
|
|
239
274
|
this.ctlServer.close(ctlSocketPath);
|
|
@@ -245,19 +280,18 @@ export class Bridge extends EventEmitter {
|
|
|
245
280
|
if (!agent)
|
|
246
281
|
return;
|
|
247
282
|
this.logger.debug({ agentId, userId: msg.userId, type: msg.type }, 'TG message received');
|
|
248
|
-
// Ensure batcher exists
|
|
249
|
-
if (!agent.
|
|
250
|
-
agent.
|
|
251
|
-
this.sendToCC(agentId,
|
|
252
|
-
})
|
|
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
|
+
});
|
|
253
288
|
}
|
|
254
289
|
// Prepare text with reply context
|
|
255
290
|
let text = msg.text;
|
|
256
291
|
if (msg.replyToText) {
|
|
257
292
|
text = `[Replying to: '${msg.replyToText}']\n\n${text}`;
|
|
258
293
|
}
|
|
259
|
-
|
|
260
|
-
batcher.add({
|
|
294
|
+
agent.batcher.add({
|
|
261
295
|
text,
|
|
262
296
|
imageBase64: msg.imageBase64,
|
|
263
297
|
imageMediaType: msg.imageMediaType,
|
|
@@ -265,7 +299,7 @@ export class Bridge extends EventEmitter {
|
|
|
265
299
|
fileName: msg.fileName,
|
|
266
300
|
});
|
|
267
301
|
}
|
|
268
|
-
sendToCC(agentId,
|
|
302
|
+
sendToCC(agentId, data, source) {
|
|
269
303
|
const agent = this.agents.get(agentId);
|
|
270
304
|
if (!agent)
|
|
271
305
|
return;
|
|
@@ -280,363 +314,306 @@ export class Bridge extends EventEmitter {
|
|
|
280
314
|
else {
|
|
281
315
|
ccMsg = createTextMessage(data.text);
|
|
282
316
|
}
|
|
283
|
-
|
|
284
|
-
// Check if this client already has a process via the registry
|
|
285
|
-
let existingEntry = this.processRegistry.findByClient(clientRef);
|
|
286
|
-
let proc = existingEntry?.ccProcess;
|
|
317
|
+
let proc = agent.ccProcess;
|
|
287
318
|
if (proc?.takenOver) {
|
|
288
319
|
// Session was taken over externally — discard old process
|
|
289
|
-
const entry = this.processRegistry.
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
}
|
|
295
|
-
// Staleness check: detect if session was modified by another client
|
|
296
|
-
// Skip if background sub-agents are running — their results grow the JSONL
|
|
297
|
-
// and would cause false-positive staleness detection
|
|
298
|
-
const accKey2 = `${userId}:${chatId}`;
|
|
299
|
-
const activeTracker = agent.subAgentTrackers.get(accKey2);
|
|
300
|
-
const hasBackgroundAgents = activeTracker?.hasDispatchedAgents ?? false;
|
|
301
|
-
if (proc && proc.state !== 'idle' && !hasBackgroundAgents) {
|
|
302
|
-
const staleInfo = this.checkSessionStaleness(agentId, userId);
|
|
303
|
-
if (staleInfo) {
|
|
304
|
-
// Session was modified externally — silently reconnect for roaming support
|
|
305
|
-
this.logger.info({ agentId, userId }, 'Session modified externally — reconnecting for roaming');
|
|
306
|
-
const entry = this.processRegistry.findByClient(clientRef);
|
|
307
|
-
this.processRegistry.unsubscribe(clientRef);
|
|
308
|
-
agent.processes.delete(userId);
|
|
309
|
-
proc = undefined;
|
|
310
|
-
existingEntry = null;
|
|
311
|
-
}
|
|
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;
|
|
312
325
|
}
|
|
313
326
|
if (!proc || proc.state === 'idle') {
|
|
314
|
-
// Warn if no repo is configured
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
}
|
|
320
|
-
// Check if another client already has a process for this repo+session
|
|
321
|
-
const sessionId = userState2.currentSessionId;
|
|
322
|
-
if (sessionId && resolvedRepo) {
|
|
323
|
-
const sharedEntry = this.processRegistry.get(resolvedRepo, sessionId);
|
|
324
|
-
if (sharedEntry && sharedEntry.ccProcess.state !== 'idle') {
|
|
325
|
-
// Attach to existing process as subscriber
|
|
326
|
-
this.processRegistry.subscribe(resolvedRepo, sessionId, clientRef);
|
|
327
|
-
proc = sharedEntry.ccProcess;
|
|
328
|
-
agent.processes.set(userId, proc);
|
|
329
|
-
// Notify the user they've attached
|
|
330
|
-
agent.tgBot.sendText(chatId, '<blockquote>📎 Attached to existing session process.</blockquote>', 'HTML').catch(err => this.logger.error({ err }, 'Failed to send attach notification'));
|
|
331
|
-
// Show typing indicator and forward message
|
|
332
|
-
this.startTypingIndicator(agent, userId, chatId);
|
|
333
|
-
proc.sendMessage(ccMsg);
|
|
334
|
-
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'));
|
|
335
332
|
}
|
|
336
333
|
}
|
|
337
|
-
|
|
338
|
-
|
|
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
|
+
});
|
|
339
347
|
}
|
|
340
|
-
proc = this.spawnCCProcess(agentId, userId, chatId);
|
|
341
|
-
agent.processes.set(userId, proc);
|
|
342
348
|
}
|
|
343
|
-
// Show typing indicator
|
|
344
|
-
|
|
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 });
|
|
345
355
|
proc.sendMessage(ccMsg);
|
|
346
356
|
}
|
|
347
|
-
/** Check if the session JSONL was modified externally since we last tracked it. */
|
|
348
|
-
checkSessionStaleness(_agentId, _userId) {
|
|
349
|
-
// With shared process registry, staleness is handled by the registry itself
|
|
350
|
-
return null;
|
|
351
|
-
}
|
|
352
357
|
// ── Process cleanup helper ──
|
|
353
358
|
/**
|
|
354
|
-
*
|
|
355
|
-
* If other subscribers remain, the process stays alive.
|
|
356
|
-
* If this was the last subscriber, the process is destroyed.
|
|
359
|
+
* Kill the agent's CC process and clean up.
|
|
357
360
|
*/
|
|
358
|
-
|
|
361
|
+
killAgentProcess(agentId) {
|
|
359
362
|
const agent = this.agents.get(agentId);
|
|
360
363
|
if (!agent)
|
|
361
364
|
return;
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
+
}
|
|
367
374
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
375
|
+
agent.ccProcess = null;
|
|
376
|
+
// Clean up accumulator & tracker
|
|
377
|
+
if (agent.accumulator) {
|
|
378
|
+
agent.accumulator.finalize();
|
|
379
|
+
agent.accumulator = null;
|
|
380
|
+
}
|
|
381
|
+
if (agent.subAgentTracker) {
|
|
382
|
+
agent.subAgentTracker.reset();
|
|
383
|
+
agent.subAgentTracker = null;
|
|
371
384
|
}
|
|
372
|
-
|
|
385
|
+
this.stopTypingIndicator(agent);
|
|
373
386
|
}
|
|
374
387
|
// ── Typing indicator management ──
|
|
375
|
-
|
|
376
|
-
|
|
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) {
|
|
377
398
|
// Don't create duplicate intervals
|
|
378
|
-
if (agent.
|
|
399
|
+
if (agent.typingInterval)
|
|
400
|
+
return;
|
|
401
|
+
if (!agent.tgBot)
|
|
379
402
|
return;
|
|
403
|
+
agent.typingChatId = chatId;
|
|
380
404
|
// Send immediately, then repeat every 4s (TG typing badge lasts ~5s)
|
|
381
405
|
agent.tgBot.sendTyping(chatId);
|
|
382
406
|
const interval = setInterval(() => {
|
|
383
|
-
agent.
|
|
407
|
+
if (agent.typingChatId)
|
|
408
|
+
agent.tgBot?.sendTyping(agent.typingChatId);
|
|
384
409
|
}, 4_000);
|
|
385
|
-
agent.
|
|
410
|
+
agent.typingInterval = interval;
|
|
386
411
|
}
|
|
387
|
-
stopTypingIndicator(agent
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
agent.typingIntervals.delete(key);
|
|
412
|
+
stopTypingIndicator(agent) {
|
|
413
|
+
if (agent.typingInterval) {
|
|
414
|
+
clearInterval(agent.typingInterval);
|
|
415
|
+
agent.typingInterval = null;
|
|
416
|
+
agent.typingChatId = null;
|
|
393
417
|
}
|
|
394
418
|
}
|
|
395
|
-
spawnCCProcess(agentId
|
|
419
|
+
spawnCCProcess(agentId) {
|
|
396
420
|
const agent = this.agents.get(agentId);
|
|
397
|
-
const
|
|
398
|
-
//
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
if (modelOverride) {
|
|
405
|
-
userConfig.model = modelOverride;
|
|
406
|
-
// 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;
|
|
407
428
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
if (registryEntry?.model) {
|
|
413
|
-
userConfig.model = registryEntry.model;
|
|
414
|
-
}
|
|
415
|
-
else {
|
|
416
|
-
const sessions = discoverCCSessions(userConfig.repo, 20);
|
|
417
|
-
const sessionInfo = sessions.find(s => s.id === currentSessionId);
|
|
418
|
-
if (sessionInfo?.model) {
|
|
419
|
-
userConfig.model = sessionInfo.model;
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
if (userState.permissionMode) {
|
|
425
|
-
userConfig.permissionMode = userState.permissionMode;
|
|
426
|
-
}
|
|
427
|
-
// 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)
|
|
428
433
|
const mcpServerPath = resolveMcpServerPath();
|
|
429
|
-
const mcpConfigPath = generateMcpConfig(agentId,
|
|
430
|
-
|
|
431
|
-
|
|
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`);
|
|
432
438
|
this.mcpServer.listen(socketPath);
|
|
433
439
|
const proc = new CCProcess({
|
|
434
440
|
agentId,
|
|
435
|
-
userId,
|
|
441
|
+
userId: agentId, // agent is the sole "user"
|
|
436
442
|
ccBinaryPath: this.config.global.ccBinaryPath,
|
|
437
443
|
userConfig,
|
|
438
444
|
mcpConfigPath,
|
|
439
|
-
sessionId
|
|
440
|
-
continueSession: !!
|
|
445
|
+
sessionId,
|
|
446
|
+
continueSession: !!sessionId,
|
|
441
447
|
logger: this.logger,
|
|
442
448
|
});
|
|
443
449
|
// Register in the process registry
|
|
444
|
-
const ownerRef = { agentId, userId, chatId };
|
|
445
|
-
|
|
446
|
-
// temporary entry for pre-init event routing. Use a placeholder sessionId
|
|
447
|
-
// that gets updated on init.
|
|
448
|
-
const tentativeSessionId = userState.currentSessionId ?? `pending-${Date.now()}`;
|
|
450
|
+
const ownerRef = { agentId, userId: agentId, chatId: 0 };
|
|
451
|
+
const tentativeSessionId = sessionId ?? `pending-${Date.now()}`;
|
|
449
452
|
const registryEntry = this.processRegistry.register(userConfig.repo, tentativeSessionId, userConfig.model || 'default', proc, ownerRef);
|
|
450
453
|
// ── Helper: get all subscribers for this process from the registry ──
|
|
451
454
|
const getEntry = () => this.processRegistry.findByProcess(proc);
|
|
452
455
|
// ── Wire up event handlers (broadcast to all subscribers) ──
|
|
453
456
|
proc.on('init', (event) => {
|
|
454
|
-
this.sessionStore.
|
|
457
|
+
this.sessionStore.updateLastActivity(agentId);
|
|
458
|
+
agent.eventBuffer.push({ ts: Date.now(), type: 'system', text: `Session initialized: ${event.session_id}` });
|
|
455
459
|
// Update registry key if session ID changed from tentative
|
|
456
460
|
if (event.session_id !== tentativeSessionId) {
|
|
457
|
-
// Re-register with the real session ID
|
|
458
461
|
const entry = getEntry();
|
|
459
462
|
if (entry) {
|
|
460
|
-
// Save subscribers before removing
|
|
461
|
-
const savedSubs = [...entry.subscribers.entries()];
|
|
462
463
|
this.processRegistry.remove(userConfig.repo, tentativeSessionId);
|
|
463
|
-
|
|
464
|
-
// Restore additional subscribers
|
|
465
|
-
for (const [, sub] of savedSubs) {
|
|
466
|
-
if (sub.client.agentId !== agentId || sub.client.userId !== userId || sub.client.chatId !== chatId) {
|
|
467
|
-
this.processRegistry.subscribe(userConfig.repo, event.session_id, sub.client);
|
|
468
|
-
const newSub = this.processRegistry.getSubscriber(newEntry, sub.client);
|
|
469
|
-
if (newSub) {
|
|
470
|
-
newSub.accumulator = sub.accumulator;
|
|
471
|
-
newSub.tracker = sub.tracker;
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
}
|
|
464
|
+
this.processRegistry.register(userConfig.repo, event.session_id, userConfig.model || 'default', proc, ownerRef);
|
|
475
465
|
}
|
|
476
466
|
}
|
|
477
467
|
});
|
|
478
468
|
proc.on('stream_event', (event) => {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
// Fallback: single subscriber mode (shouldn't happen)
|
|
482
|
-
this.handleStreamEvent(agentId, userId, chatId, event);
|
|
483
|
-
return;
|
|
484
|
-
}
|
|
485
|
-
for (const sub of this.processRegistry.subscribers(entry)) {
|
|
486
|
-
this.handleStreamEvent(sub.client.agentId, sub.client.userId, sub.client.chatId, event);
|
|
487
|
-
}
|
|
469
|
+
this.highSignalDetector.handleStreamEvent(agentId, event);
|
|
470
|
+
this.handleStreamEvent(agentId, event);
|
|
488
471
|
});
|
|
489
472
|
proc.on('tool_result', (event) => {
|
|
490
|
-
|
|
491
|
-
const
|
|
492
|
-
|
|
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
|
-
// Set agent metadata from structured data or text fallback
|
|
527
|
-
if (event.tool_use_id && spawnMeta.name) {
|
|
528
|
-
tracker.setAgentMetadata(event.tool_use_id, {
|
|
529
|
-
agentName: spawnMeta.name,
|
|
530
|
-
agentType: spawnMeta.agent_type,
|
|
531
|
-
color: spawnMeta.color,
|
|
532
|
-
});
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
// Handle tool result (sets status, edits TG message)
|
|
536
|
-
if (event.tool_use_id) {
|
|
537
|
-
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
|
+
});
|
|
538
509
|
}
|
|
539
|
-
//
|
|
540
|
-
if (
|
|
541
|
-
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
|
+
});
|
|
542
517
|
}
|
|
543
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
|
+
}
|
|
544
527
|
});
|
|
545
|
-
// System events for background task tracking
|
|
528
|
+
// System events for background task tracking
|
|
546
529
|
proc.on('task_started', (event) => {
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
for (const sub of subscriberList) {
|
|
550
|
-
const subAgent = this.agents.get(sub.client.agentId);
|
|
551
|
-
if (!subAgent)
|
|
552
|
-
continue;
|
|
553
|
-
const accKey = `${sub.client.userId}:${sub.client.chatId}`;
|
|
554
|
-
const tracker = subAgent.subAgentTrackers.get(accKey);
|
|
555
|
-
if (tracker) {
|
|
556
|
-
tracker.handleTaskStarted(event.tool_use_id, event.description, event.task_type);
|
|
557
|
-
}
|
|
530
|
+
if (agent.subAgentTracker) {
|
|
531
|
+
agent.subAgentTracker.handleTaskStarted(event.tool_use_id, event.description, event.task_type);
|
|
558
532
|
}
|
|
559
533
|
});
|
|
560
534
|
proc.on('task_progress', (event) => {
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
for (const sub of subscriberList) {
|
|
564
|
-
const subAgent = this.agents.get(sub.client.agentId);
|
|
565
|
-
if (!subAgent)
|
|
566
|
-
continue;
|
|
567
|
-
const accKey = `${sub.client.userId}:${sub.client.chatId}`;
|
|
568
|
-
const tracker = subAgent.subAgentTrackers.get(accKey);
|
|
569
|
-
if (tracker) {
|
|
570
|
-
tracker.handleTaskProgress(event.tool_use_id, event.description, event.last_tool_name);
|
|
571
|
-
}
|
|
535
|
+
if (agent.subAgentTracker) {
|
|
536
|
+
agent.subAgentTracker.handleTaskProgress(event.tool_use_id, event.description, event.last_tool_name);
|
|
572
537
|
}
|
|
573
538
|
});
|
|
574
539
|
proc.on('task_completed', (event) => {
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
for (const sub of subscriberList) {
|
|
578
|
-
const subAgent = this.agents.get(sub.client.agentId);
|
|
579
|
-
if (!subAgent)
|
|
580
|
-
continue;
|
|
581
|
-
const accKey = `${sub.client.userId}:${sub.client.chatId}`;
|
|
582
|
-
const tracker = subAgent.subAgentTrackers.get(accKey);
|
|
583
|
-
if (tracker) {
|
|
584
|
-
tracker.handleTaskCompleted(event.tool_use_id);
|
|
585
|
-
}
|
|
540
|
+
if (agent.subAgentTracker) {
|
|
541
|
+
agent.subAgentTracker.handleTaskCompleted(event.tool_use_id);
|
|
586
542
|
}
|
|
587
543
|
});
|
|
588
|
-
// Media from tool results (images, PDFs, etc.)
|
|
544
|
+
// Media from tool results (images, PDFs, etc.)
|
|
589
545
|
proc.on('media', (media) => {
|
|
590
546
|
const buf = Buffer.from(media.data, 'base64');
|
|
591
|
-
const
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
subAgent.tgBot.sendDocumentBuffer(sub.client.chatId, buf, `document${ext}`).catch(err => {
|
|
605
|
-
this.logger.error({ err, agentId: sub.client.agentId, userId: sub.client.userId }, 'Failed to send tool_result document');
|
|
606
|
-
});
|
|
607
|
-
}
|
|
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
|
+
});
|
|
608
560
|
}
|
|
609
561
|
});
|
|
610
562
|
proc.on('assistant', (event) => {
|
|
611
|
-
//
|
|
612
|
-
|
|
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
|
+
}
|
|
613
579
|
});
|
|
614
580
|
proc.on('result', (event) => {
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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
|
+
});
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
proc.on('compact', (event) => {
|
|
598
|
+
const trigger = event.compact_metadata?.trigger ?? 'manual';
|
|
599
|
+
const preTokens = event.compact_metadata?.pre_tokens;
|
|
600
|
+
const tokenInfo = preTokens ? ` (was ${Math.round(preTokens / 1000)}k tokens)` : '';
|
|
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'));
|
|
626
606
|
}
|
|
627
607
|
});
|
|
628
608
|
proc.on('permission_request', (event) => {
|
|
629
|
-
// Send permission request only to the owner (first subscriber)
|
|
630
609
|
const req = event.request;
|
|
631
610
|
const requestId = event.request_id;
|
|
632
|
-
// Store pending permission on the owner's agent
|
|
633
611
|
agent.pendingPermissions.set(requestId, {
|
|
634
612
|
requestId,
|
|
635
|
-
userId,
|
|
613
|
+
userId: agentId,
|
|
636
614
|
toolName: req.tool_name,
|
|
637
615
|
input: req.input,
|
|
638
616
|
});
|
|
639
|
-
// Build description of what CC wants to do
|
|
640
617
|
const toolName = escapeHtml(req.tool_name);
|
|
641
618
|
const inputPreview = req.input
|
|
642
619
|
? escapeHtml(JSON.stringify(req.input).slice(0, 200))
|
|
@@ -647,9 +624,24 @@ export class Bridge extends EventEmitter {
|
|
|
647
624
|
const keyboard = new InlineKeyboard()
|
|
648
625
|
.text('✅ Allow', `perm_allow:${requestId}`)
|
|
649
626
|
.text('❌ Deny', `perm_deny:${requestId}`)
|
|
650
|
-
.text('✅ Allow All', `perm_allow_all:${
|
|
651
|
-
|
|
652
|
-
|
|
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
|
+
}
|
|
653
645
|
});
|
|
654
646
|
proc.on('api_error', (event) => {
|
|
655
647
|
const errMsg = event.error?.message || 'Unknown API error';
|
|
@@ -658,191 +650,165 @@ export class Bridge extends EventEmitter {
|
|
|
658
650
|
const retryInfo = event.retryAttempt != null && event.maxRetries != null
|
|
659
651
|
? ` (retry ${event.retryAttempt}/${event.maxRetries})`
|
|
660
652
|
: '';
|
|
653
|
+
agent.eventBuffer.push({ ts: Date.now(), type: 'error', text: `${errMsg}${retryInfo}` });
|
|
661
654
|
const text = isOverloaded
|
|
662
|
-
?
|
|
663
|
-
:
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
for (const sub of subscriberList) {
|
|
668
|
-
const subAgent = this.agents.get(sub.client.agentId);
|
|
669
|
-
if (!subAgent)
|
|
670
|
-
continue;
|
|
671
|
-
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
|
|
672
660
|
.catch(err => this.logger.error({ err }, 'Failed to send API error notification'));
|
|
673
661
|
}
|
|
674
662
|
});
|
|
675
663
|
proc.on('hang', () => {
|
|
676
|
-
|
|
677
|
-
const
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
const subAgent = this.agents.get(sub.client.agentId);
|
|
681
|
-
if (!subAgent)
|
|
682
|
-
continue;
|
|
683
|
-
this.stopTypingIndicator(subAgent, sub.client.userId, sub.client.chatId);
|
|
684
|
-
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
|
|
685
668
|
.catch(err => this.logger.error({ err }, 'Failed to send hang notification'));
|
|
686
669
|
}
|
|
687
670
|
});
|
|
688
671
|
proc.on('takeover', () => {
|
|
689
|
-
this.logger.warn({ agentId
|
|
690
|
-
|
|
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);
|
|
691
686
|
const entry = getEntry();
|
|
692
687
|
if (entry) {
|
|
693
|
-
for (const sub of this.processRegistry.subscribers(entry)) {
|
|
694
|
-
const subAgent = this.agents.get(sub.client.agentId);
|
|
695
|
-
if (!subAgent)
|
|
696
|
-
continue;
|
|
697
|
-
this.stopTypingIndicator(subAgent, sub.client.userId, sub.client.chatId);
|
|
698
|
-
subAgent.processes.delete(sub.client.userId);
|
|
699
|
-
}
|
|
700
|
-
// Remove from registry without destroying (already handling exit)
|
|
701
688
|
this.processRegistry.remove(entry.repo, entry.sessionId);
|
|
702
689
|
}
|
|
703
|
-
|
|
690
|
+
agent.ccProcess = null;
|
|
704
691
|
proc.destroy();
|
|
705
692
|
});
|
|
706
693
|
proc.on('exit', () => {
|
|
707
|
-
|
|
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
|
+
}
|
|
708
712
|
const entry = getEntry();
|
|
709
713
|
if (entry) {
|
|
710
|
-
for (const sub of this.processRegistry.subscribers(entry)) {
|
|
711
|
-
const subAgent = this.agents.get(sub.client.agentId);
|
|
712
|
-
if (!subAgent)
|
|
713
|
-
continue;
|
|
714
|
-
this.stopTypingIndicator(subAgent, sub.client.userId, sub.client.chatId);
|
|
715
|
-
const accKey = `${sub.client.userId}:${sub.client.chatId}`;
|
|
716
|
-
const acc = subAgent.accumulators.get(accKey);
|
|
717
|
-
if (acc) {
|
|
718
|
-
acc.finalize();
|
|
719
|
-
subAgent.accumulators.delete(accKey);
|
|
720
|
-
}
|
|
721
|
-
const exitTracker = subAgent.subAgentTrackers.get(accKey);
|
|
722
|
-
if (exitTracker) {
|
|
723
|
-
exitTracker.stopMailboxWatch();
|
|
724
|
-
}
|
|
725
|
-
subAgent.subAgentTrackers.delete(accKey);
|
|
726
|
-
}
|
|
727
|
-
// Remove from registry (process already exited)
|
|
728
714
|
this.processRegistry.remove(entry.repo, entry.sessionId);
|
|
729
715
|
}
|
|
730
|
-
|
|
731
|
-
// Fallback: clean up owner only
|
|
732
|
-
this.stopTypingIndicator(agent, userId, chatId);
|
|
733
|
-
const accKey = `${userId}:${chatId}`;
|
|
734
|
-
const acc = agent.accumulators.get(accKey);
|
|
735
|
-
if (acc) {
|
|
736
|
-
acc.finalize();
|
|
737
|
-
agent.accumulators.delete(accKey);
|
|
738
|
-
}
|
|
739
|
-
const exitTracker = agent.subAgentTrackers.get(accKey);
|
|
740
|
-
if (exitTracker) {
|
|
741
|
-
exitTracker.stopMailboxWatch();
|
|
742
|
-
}
|
|
743
|
-
agent.subAgentTrackers.delete(accKey);
|
|
744
|
-
}
|
|
716
|
+
agent.ccProcess = null;
|
|
745
717
|
});
|
|
746
718
|
proc.on('error', (err) => {
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
const
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
if (!subAgent)
|
|
753
|
-
continue;
|
|
754
|
-
this.stopTypingIndicator(subAgent, sub.client.userId, sub.client.chatId);
|
|
755
|
-
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
|
|
756
724
|
.catch(err2 => this.logger.error({ err: err2 }, 'Failed to send process error notification'));
|
|
757
725
|
}
|
|
758
726
|
});
|
|
759
727
|
return proc;
|
|
760
728
|
}
|
|
761
729
|
// ── Stream event handling ──
|
|
762
|
-
handleStreamEvent(agentId,
|
|
730
|
+
handleStreamEvent(agentId, event) {
|
|
763
731
|
const agent = this.agents.get(agentId);
|
|
764
732
|
if (!agent)
|
|
765
733
|
return;
|
|
766
|
-
const
|
|
767
|
-
|
|
768
|
-
|
|
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)
|
|
769
739
|
const sender = {
|
|
770
|
-
sendMessage: (cid, text, parseMode) =>
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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),
|
|
774
751
|
};
|
|
775
752
|
const onError = (err, context) => {
|
|
776
|
-
this.logger.error({ err, context, agentId
|
|
777
|
-
|
|
753
|
+
this.logger.error({ err, context, agentId }, 'Stream accumulator error');
|
|
754
|
+
tgBot.sendText(chatId, formatSystemMessage('error', escapeHtml(context)), 'HTML', true).catch(() => { }); // silent
|
|
778
755
|
};
|
|
779
|
-
|
|
780
|
-
agent.accumulators.set(accKey, acc);
|
|
756
|
+
agent.accumulator = new StreamAccumulator({ chatId, sender, logger: this.logger, onError });
|
|
781
757
|
}
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
if (!tracker) {
|
|
758
|
+
if (!agent.subAgentTracker && agent.tgBot) {
|
|
759
|
+
const tgBot = agent.tgBot; // capture for closures (non-null here)
|
|
785
760
|
const subAgentSender = {
|
|
786
|
-
sendMessage: (cid, text, parseMode) =>
|
|
787
|
-
editMessage: (cid, msgId, text, parseMode) =>
|
|
788
|
-
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),
|
|
789
764
|
};
|
|
790
|
-
|
|
765
|
+
agent.subAgentTracker = new SubAgentTracker({
|
|
791
766
|
chatId,
|
|
792
767
|
sender: subAgentSender,
|
|
793
768
|
});
|
|
794
|
-
agent.subAgentTrackers.set(accKey, tracker);
|
|
795
769
|
}
|
|
796
770
|
// On message_start: new CC turn — full reset for text accumulator.
|
|
797
|
-
// Tool indicator messages are independent and persist across turns.
|
|
798
771
|
if (event.type === 'message_start') {
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
tracker.reset();
|
|
772
|
+
agent.accumulator?.reset();
|
|
773
|
+
if (agent.subAgentTracker && !agent.subAgentTracker.hasDispatchedAgents) {
|
|
774
|
+
agent.subAgentTracker.reset();
|
|
803
775
|
}
|
|
804
776
|
}
|
|
805
|
-
|
|
806
|
-
|
|
777
|
+
agent.accumulator?.handleEvent(event);
|
|
778
|
+
agent.subAgentTracker?.handleEvent(event);
|
|
807
779
|
}
|
|
808
|
-
handleResult(agentId,
|
|
780
|
+
handleResult(agentId, event) {
|
|
809
781
|
const agent = this.agents.get(agentId);
|
|
810
782
|
if (!agent)
|
|
811
783
|
return;
|
|
784
|
+
const chatId = this.getAgentChatId(agent);
|
|
812
785
|
// Set usage stats on the accumulator before finalizing
|
|
813
|
-
const
|
|
814
|
-
const acc = agent.accumulators.get(accKey);
|
|
786
|
+
const acc = agent.accumulator;
|
|
815
787
|
if (acc) {
|
|
816
|
-
// Extract usage from result event
|
|
817
788
|
if (event.usage) {
|
|
789
|
+
const proc = agent.ccProcess;
|
|
790
|
+
const entry = proc ? this.processRegistry.findByProcess(proc) : null;
|
|
818
791
|
acc.setTurnUsage({
|
|
819
792
|
inputTokens: event.usage.input_tokens ?? 0,
|
|
820
793
|
outputTokens: event.usage.output_tokens ?? 0,
|
|
821
794
|
cacheReadTokens: event.usage.cache_read_input_tokens ?? 0,
|
|
822
795
|
cacheCreationTokens: event.usage.cache_creation_input_tokens ?? 0,
|
|
823
796
|
costUsd: event.total_cost_usd ?? null,
|
|
797
|
+
model: event.model ?? entry?.model,
|
|
824
798
|
});
|
|
825
799
|
}
|
|
826
800
|
acc.finalize();
|
|
827
|
-
// Don't delete — next turn will reset via message_start and create a new message
|
|
828
|
-
}
|
|
829
|
-
// Update session store with cost
|
|
830
|
-
if (event.total_cost_usd) {
|
|
831
801
|
}
|
|
832
|
-
//
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
if (event.is_error && event.result) {
|
|
836
|
-
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
|
|
837
805
|
.catch(err => this.logger.error({ err }, 'Failed to send result error notification'));
|
|
838
806
|
}
|
|
839
807
|
// If background sub-agents are still running, mailbox watcher handles them.
|
|
840
|
-
|
|
841
|
-
const tracker = agent.subAgentTrackers.get(accKey);
|
|
808
|
+
const tracker = agent.subAgentTracker;
|
|
842
809
|
if (tracker?.hasDispatchedAgents && tracker.currentTeamName) {
|
|
843
810
|
this.logger.info({ agentId }, 'Turn ended with background sub-agents still running');
|
|
844
|
-
|
|
845
|
-
const ccProcess = agent.processes.get(userId);
|
|
811
|
+
const ccProcess = agent.ccProcess;
|
|
846
812
|
if (ccProcess)
|
|
847
813
|
ccProcess.clearIdleTimer();
|
|
848
814
|
// Start mailbox watcher (works for general-purpose agents that have SendMessage)
|
|
@@ -853,12 +819,11 @@ export class Bridge extends EventEmitter {
|
|
|
853
819
|
tracker.hasPendingFollowUp = true;
|
|
854
820
|
setTimeout(() => {
|
|
855
821
|
if (!tracker.hasDispatchedAgents)
|
|
856
|
-
return;
|
|
857
|
-
const proc = agent.
|
|
822
|
+
return;
|
|
823
|
+
const proc = agent.ccProcess;
|
|
858
824
|
if (!proc)
|
|
859
825
|
return;
|
|
860
826
|
this.logger.info({ agentId }, 'Mailbox timeout — sending single follow-up for remaining agents');
|
|
861
|
-
// Mark all remaining dispatched agents as completed (CC already has the results)
|
|
862
827
|
for (const info of tracker.activeAgents) {
|
|
863
828
|
if (info.status === 'dispatched') {
|
|
864
829
|
tracker.markCompleted(info.toolUseId, '(results delivered in CC response)');
|
|
@@ -874,27 +839,14 @@ export class Bridge extends EventEmitter {
|
|
|
874
839
|
const agent = this.agents.get(agentId);
|
|
875
840
|
if (!agent)
|
|
876
841
|
return;
|
|
842
|
+
if (!agent.tgBot)
|
|
843
|
+
return; // ephemeral agents don't have TG bots
|
|
877
844
|
this.logger.debug({ agentId, command: cmd.command, args: cmd.args }, 'Slash command');
|
|
878
845
|
switch (cmd.command) {
|
|
879
846
|
case 'start': {
|
|
880
|
-
const
|
|
881
|
-
const
|
|
882
|
-
const
|
|
883
|
-
const session = userState.currentSessionId;
|
|
884
|
-
// Model: running process > session JSONL > user default
|
|
885
|
-
let model = userConf.model;
|
|
886
|
-
if (session && repo) {
|
|
887
|
-
const registryEntry = this.processRegistry.get(repo, session);
|
|
888
|
-
if (registryEntry?.model) {
|
|
889
|
-
model = registryEntry.model;
|
|
890
|
-
}
|
|
891
|
-
else {
|
|
892
|
-
const sessions = discoverCCSessions(repo, 20);
|
|
893
|
-
const sessionInfo = sessions.find(s => s.id === session);
|
|
894
|
-
if (sessionInfo?.model)
|
|
895
|
-
model = sessionInfo.model;
|
|
896
|
-
}
|
|
897
|
-
}
|
|
847
|
+
const repo = agent.repo;
|
|
848
|
+
const model = agent.model;
|
|
849
|
+
const session = agent.ccProcess?.sessionId;
|
|
898
850
|
const lines = ['👋 <b>TGCC</b> — Telegram ↔ Claude Code bridge'];
|
|
899
851
|
if (repo)
|
|
900
852
|
lines.push(`📂 <code>${escapeHtml(shortenRepoPath(repo))}</code>`);
|
|
@@ -916,14 +868,12 @@ export class Bridge extends EventEmitter {
|
|
|
916
868
|
await agent.tgBot.sendText(cmd.chatId, HELP_TEXT, 'HTML');
|
|
917
869
|
break;
|
|
918
870
|
case 'ping': {
|
|
919
|
-
const
|
|
920
|
-
const state = proc?.state ?? 'idle';
|
|
871
|
+
const state = agent.ccProcess?.state ?? 'idle';
|
|
921
872
|
await agent.tgBot.sendText(cmd.chatId, `pong — process: <b>${state.toUpperCase()}</b>`, 'HTML');
|
|
922
873
|
break;
|
|
923
874
|
}
|
|
924
875
|
case 'status': {
|
|
925
|
-
const proc = agent.
|
|
926
|
-
const userState = this.sessionStore.getUser(agentId, cmd.userId);
|
|
876
|
+
const proc = agent.ccProcess;
|
|
927
877
|
const uptime = proc?.spawnedAt
|
|
928
878
|
? formatDuration(Date.now() - proc.spawnedAt.getTime())
|
|
929
879
|
: 'N/A';
|
|
@@ -931,77 +881,56 @@ export class Bridge extends EventEmitter {
|
|
|
931
881
|
`<b>Agent:</b> ${escapeHtml(agentId)}`,
|
|
932
882
|
`<b>Process:</b> ${(proc?.state ?? 'idle').toUpperCase()} (uptime: ${uptime})`,
|
|
933
883
|
`<b>Session:</b> <code>${escapeHtml(proc?.sessionId?.slice(0, 8) ?? 'none')}</code>`,
|
|
934
|
-
`<b>Model:</b> ${escapeHtml(
|
|
935
|
-
`<b>Repo:</b> ${escapeHtml(
|
|
884
|
+
`<b>Model:</b> ${escapeHtml(agent.model)}`,
|
|
885
|
+
`<b>Repo:</b> ${escapeHtml(agent.repo)}`,
|
|
936
886
|
`<b>Cost:</b> $${(proc?.totalCostUsd ?? 0).toFixed(4)}`,
|
|
937
887
|
].join('\n');
|
|
938
888
|
await agent.tgBot.sendText(cmd.chatId, status, 'HTML');
|
|
939
889
|
break;
|
|
940
890
|
}
|
|
941
891
|
case 'cost': {
|
|
942
|
-
|
|
943
|
-
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');
|
|
944
893
|
break;
|
|
945
894
|
}
|
|
946
895
|
case 'new': {
|
|
947
|
-
this.
|
|
948
|
-
|
|
949
|
-
const newConf = resolveUserConfig(agent.config, cmd.userId);
|
|
950
|
-
const newState = this.sessionStore.getUser(agentId, cmd.userId);
|
|
951
|
-
const newRepo = newState.repo || newConf.repo;
|
|
952
|
-
const newModel = newState.model || newConf.model;
|
|
896
|
+
this.killAgentProcess(agentId);
|
|
897
|
+
agent.pendingSessionId = null; // next message spawns fresh
|
|
953
898
|
const newLines = ['Session cleared. Next message starts fresh.'];
|
|
954
|
-
if (
|
|
955
|
-
newLines.push(`📂 <code>${escapeHtml(shortenRepoPath(
|
|
956
|
-
if (
|
|
957
|
-
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)}`);
|
|
958
903
|
await agent.tgBot.sendText(cmd.chatId, `<blockquote>${newLines.join('\n')}</blockquote>`, 'HTML');
|
|
959
904
|
break;
|
|
960
905
|
}
|
|
961
906
|
case 'continue': {
|
|
962
|
-
|
|
963
|
-
const
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
let
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
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);
|
|
970
914
|
if (recent.length > 0) {
|
|
971
|
-
|
|
972
|
-
this.sessionStore.setCurrentSession(agentId, cmd.userId, contSession);
|
|
915
|
+
sessionToResume = recent[0].id;
|
|
973
916
|
}
|
|
974
917
|
}
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
if (contSession && contRepo) {
|
|
978
|
-
// Check if a process is already running for this session
|
|
979
|
-
const registryEntry = this.processRegistry.get(contRepo, contSession);
|
|
980
|
-
if (registryEntry?.model) {
|
|
981
|
-
contModel = registryEntry.model;
|
|
982
|
-
}
|
|
983
|
-
else {
|
|
984
|
-
const sessions = discoverCCSessions(contRepo, 20);
|
|
985
|
-
const sessionInfo = sessions.find(s => s.id === contSession);
|
|
986
|
-
if (sessionInfo?.model)
|
|
987
|
-
contModel = sessionInfo.model;
|
|
988
|
-
}
|
|
918
|
+
if (sessionToResume) {
|
|
919
|
+
agent.pendingSessionId = sessionToResume;
|
|
989
920
|
}
|
|
990
921
|
const contLines = ['Process respawned. Session kept.'];
|
|
991
|
-
if (
|
|
992
|
-
contLines.push(`📂 <code>${escapeHtml(shortenRepoPath(
|
|
993
|
-
if (
|
|
994
|
-
contLines.push(`🤖 ${escapeHtml(
|
|
995
|
-
if (
|
|
996
|
-
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>`);
|
|
997
928
|
await agent.tgBot.sendText(cmd.chatId, `<blockquote>${contLines.join('\n')}</blockquote>`, 'HTML');
|
|
998
929
|
break;
|
|
999
930
|
}
|
|
1000
931
|
case 'sessions': {
|
|
1001
|
-
const
|
|
1002
|
-
const
|
|
1003
|
-
const repo = userState.repo || userConf.repo;
|
|
1004
|
-
const currentSessionId = userState.currentSessionId;
|
|
932
|
+
const repo = agent.repo;
|
|
933
|
+
const currentSessionId = agent.ccProcess?.sessionId ?? null;
|
|
1005
934
|
// Discover sessions from CC's session directory
|
|
1006
935
|
const discovered = repo ? discoverCCSessions(repo, 5) : [];
|
|
1007
936
|
const merged = discovered.map(d => {
|
|
@@ -1033,7 +962,7 @@ export class Bridge extends EventEmitter {
|
|
|
1033
962
|
const kb = new InlineKeyboard();
|
|
1034
963
|
if (s.isCurrent) {
|
|
1035
964
|
const repoLine = repo ? `\n📂 <code>${escapeHtml(shortenRepoPath(repo))}</code>` : '';
|
|
1036
|
-
const sessModel =
|
|
965
|
+
const sessModel = agent.model;
|
|
1037
966
|
const modelLine = sessModel ? `\n🤖 ${escapeHtml(sessModel)}` : '';
|
|
1038
967
|
const sessionLine = `\n📎 <code>${escapeHtml(s.id.slice(0, 8))}</code>`;
|
|
1039
968
|
const text = `<blockquote><b>Current session:</b>\n${displayTitle}\n${s.detail} · ${s.age}${repoLine}${modelLine}${sessionLine}</blockquote>`;
|
|
@@ -1053,22 +982,21 @@ export class Bridge extends EventEmitter {
|
|
|
1053
982
|
await agent.tgBot.sendText(cmd.chatId, '<blockquote>Usage: /resume <session-id></blockquote>', 'HTML');
|
|
1054
983
|
break;
|
|
1055
984
|
}
|
|
1056
|
-
this.
|
|
1057
|
-
|
|
985
|
+
this.killAgentProcess(agentId);
|
|
986
|
+
agent.pendingSessionId = cmd.args.trim();
|
|
1058
987
|
await agent.tgBot.sendText(cmd.chatId, `Will resume session <code>${escapeHtml(cmd.args.trim().slice(0, 8))}</code> on next message.`, 'HTML');
|
|
1059
988
|
break;
|
|
1060
989
|
}
|
|
1061
990
|
case 'session': {
|
|
1062
|
-
const
|
|
1063
|
-
if (!
|
|
991
|
+
const currentSessionId = agent.ccProcess?.sessionId;
|
|
992
|
+
if (!currentSessionId) {
|
|
1064
993
|
await agent.tgBot.sendText(cmd.chatId, '<blockquote>No active session.</blockquote>', 'HTML');
|
|
1065
994
|
break;
|
|
1066
995
|
}
|
|
1067
|
-
const
|
|
1068
|
-
const
|
|
1069
|
-
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);
|
|
1070
998
|
if (!info) {
|
|
1071
|
-
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');
|
|
1072
1000
|
break;
|
|
1073
1001
|
}
|
|
1074
1002
|
const ctxLine = info.contextPct !== null ? `\n<b>Context:</b> ${info.contextPct}%` : '';
|
|
@@ -1079,23 +1007,7 @@ export class Bridge extends EventEmitter {
|
|
|
1079
1007
|
case 'model': {
|
|
1080
1008
|
const MODEL_OPTIONS = ['opus', 'sonnet', 'haiku'];
|
|
1081
1009
|
if (!cmd.args) {
|
|
1082
|
-
|
|
1083
|
-
const uState = this.sessionStore.getUser(agentId, cmd.userId);
|
|
1084
|
-
const uConf = resolveUserConfig(agent.config, cmd.userId);
|
|
1085
|
-
const uRepo = uState.repo || uConf.repo;
|
|
1086
|
-
const uSession = uState.currentSessionId;
|
|
1087
|
-
let current = uConf.model || 'default';
|
|
1088
|
-
if (uSession && uRepo) {
|
|
1089
|
-
const re = this.processRegistry.get(uRepo, uSession);
|
|
1090
|
-
if (re?.model)
|
|
1091
|
-
current = re.model;
|
|
1092
|
-
else {
|
|
1093
|
-
const ds = discoverCCSessions(uRepo, 20);
|
|
1094
|
-
const si = ds.find(s => s.id === uSession);
|
|
1095
|
-
if (si?.model)
|
|
1096
|
-
current = si.model;
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1010
|
+
const current = agent.model || 'default';
|
|
1099
1011
|
const keyboard = new InlineKeyboard();
|
|
1100
1012
|
for (const m of MODEL_OPTIONS) {
|
|
1101
1013
|
const isCurrent = current.includes(m);
|
|
@@ -1106,11 +1018,13 @@ export class Bridge extends EventEmitter {
|
|
|
1106
1018
|
break;
|
|
1107
1019
|
}
|
|
1108
1020
|
const newModel = cmd.args.trim();
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
this.
|
|
1112
|
-
this.
|
|
1021
|
+
const oldModel = agent.model;
|
|
1022
|
+
agent.model = newModel;
|
|
1023
|
+
this.sessionStore.setModel(agentId, newModel);
|
|
1024
|
+
this.killAgentProcess(agentId);
|
|
1113
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');
|
|
1114
1028
|
break;
|
|
1115
1029
|
}
|
|
1116
1030
|
case 'repo': {
|
|
@@ -1229,8 +1143,7 @@ export class Bridge extends EventEmitter {
|
|
|
1229
1143
|
break;
|
|
1230
1144
|
}
|
|
1231
1145
|
if (!cmd.args) {
|
|
1232
|
-
const current =
|
|
1233
|
-
|| resolveUserConfig(agent.config, cmd.userId).repo;
|
|
1146
|
+
const current = agent.repo;
|
|
1234
1147
|
// Show available repos as inline keyboard buttons
|
|
1235
1148
|
const repoEntries = Object.entries(this.config.repos);
|
|
1236
1149
|
if (repoEntries.length > 0) {
|
|
@@ -1253,17 +1166,19 @@ export class Bridge extends EventEmitter {
|
|
|
1253
1166
|
break;
|
|
1254
1167
|
}
|
|
1255
1168
|
// Kill current process (different CWD needs new process)
|
|
1256
|
-
|
|
1257
|
-
this.
|
|
1258
|
-
|
|
1259
|
-
|
|
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);
|
|
1260
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');
|
|
1261
1177
|
break;
|
|
1262
1178
|
}
|
|
1263
1179
|
case 'cancel': {
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
proc.cancel();
|
|
1180
|
+
if (agent.ccProcess && agent.ccProcess.state === 'active') {
|
|
1181
|
+
agent.ccProcess.cancel();
|
|
1267
1182
|
await agent.tgBot.sendText(cmd.chatId, '<blockquote>Cancelled.</blockquote>', 'HTML');
|
|
1268
1183
|
}
|
|
1269
1184
|
else {
|
|
@@ -1271,6 +1186,18 @@ export class Bridge extends EventEmitter {
|
|
|
1271
1186
|
}
|
|
1272
1187
|
break;
|
|
1273
1188
|
}
|
|
1189
|
+
case 'compact': {
|
|
1190
|
+
if (!agent.ccProcess || agent.ccProcess.state !== 'active') {
|
|
1191
|
+
await agent.tgBot.sendText(cmd.chatId, '<blockquote>No active session to compact. Start one first.</blockquote>', 'HTML');
|
|
1192
|
+
break;
|
|
1193
|
+
}
|
|
1194
|
+
const compactMsg = cmd.args?.trim()
|
|
1195
|
+
? `/compact ${cmd.args.trim()}`
|
|
1196
|
+
: '/compact';
|
|
1197
|
+
await agent.tgBot.sendText(cmd.chatId, formatSystemMessage('status', 'Compacting…'), 'HTML');
|
|
1198
|
+
agent.ccProcess.sendMessage(createTextMessage(compactMsg));
|
|
1199
|
+
break;
|
|
1200
|
+
}
|
|
1274
1201
|
case 'catchup': {
|
|
1275
1202
|
// Catchup now just shows recent sessions via /sessions
|
|
1276
1203
|
await agent.tgBot.sendText(cmd.chatId, '<blockquote>Use /sessions to see recent sessions.</blockquote>', 'HTML');
|
|
@@ -1278,22 +1205,20 @@ export class Bridge extends EventEmitter {
|
|
|
1278
1205
|
}
|
|
1279
1206
|
case 'permissions': {
|
|
1280
1207
|
const validModes = ['dangerously-skip', 'acceptEdits', 'default', 'plan'];
|
|
1281
|
-
const userState = this.sessionStore.getUser(agentId, cmd.userId);
|
|
1282
1208
|
const agentDefault = agent.config.defaults.permissionMode;
|
|
1283
|
-
const
|
|
1209
|
+
const agentState = this.sessionStore.getAgent(agentId);
|
|
1210
|
+
const currentMode = agentState.permissionMode || agentDefault;
|
|
1284
1211
|
if (cmd.args) {
|
|
1285
1212
|
const mode = cmd.args.trim();
|
|
1286
1213
|
if (!validModes.includes(mode)) {
|
|
1287
1214
|
await agent.tgBot.sendText(cmd.chatId, `<blockquote>Invalid mode. Valid: ${validModes.join(', ')}</blockquote>`, 'HTML');
|
|
1288
1215
|
break;
|
|
1289
1216
|
}
|
|
1290
|
-
this.sessionStore.setPermissionMode(agentId,
|
|
1291
|
-
|
|
1292
|
-
this.disconnectClient(agentId, cmd.userId, cmd.chatId);
|
|
1217
|
+
this.sessionStore.setPermissionMode(agentId, mode);
|
|
1218
|
+
this.killAgentProcess(agentId);
|
|
1293
1219
|
await agent.tgBot.sendText(cmd.chatId, `Permission mode set to <code>${escapeHtml(mode)}</code>. Takes effect on next message.`, 'HTML');
|
|
1294
1220
|
break;
|
|
1295
1221
|
}
|
|
1296
|
-
// No args — show current mode + inline keyboard
|
|
1297
1222
|
const keyboard = new InlineKeyboard();
|
|
1298
1223
|
keyboard.text('🔓 Bypass', 'permissions:dangerously-skip').text('✏️ Accept Edits', 'permissions:acceptEdits').row();
|
|
1299
1224
|
keyboard.text('🔒 Default', 'permissions:default').text('📋 Plan', 'permissions:plan').row();
|
|
@@ -1307,22 +1232,23 @@ export class Bridge extends EventEmitter {
|
|
|
1307
1232
|
const agent = this.agents.get(agentId);
|
|
1308
1233
|
if (!agent)
|
|
1309
1234
|
return;
|
|
1235
|
+
if (!agent.tgBot)
|
|
1236
|
+
return; // ephemeral agents don't have TG bots
|
|
1310
1237
|
this.logger.debug({ agentId, action: query.action, data: query.data }, 'Callback query');
|
|
1311
1238
|
switch (query.action) {
|
|
1312
1239
|
case 'resume': {
|
|
1313
1240
|
const sessionId = query.data;
|
|
1314
|
-
this.
|
|
1315
|
-
|
|
1241
|
+
this.killAgentProcess(agentId);
|
|
1242
|
+
agent.pendingSessionId = sessionId;
|
|
1316
1243
|
await agent.tgBot.answerCallbackQuery(query.callbackQueryId, 'Session set');
|
|
1317
1244
|
await agent.tgBot.sendText(query.chatId, `Will resume session <code>${escapeHtml(sessionId.slice(0, 8))}</code> on next message.`, 'HTML');
|
|
1318
1245
|
break;
|
|
1319
1246
|
}
|
|
1320
1247
|
case 'delete': {
|
|
1321
1248
|
const sessionId = query.data;
|
|
1322
|
-
//
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
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);
|
|
1326
1252
|
}
|
|
1327
1253
|
await agent.tgBot.answerCallbackQuery(query.callbackQueryId, 'Session cleared');
|
|
1328
1254
|
await agent.tgBot.sendText(query.chatId, `Session <code>${escapeHtml(sessionId.slice(0, 8))}</code> cleared.`, 'HTML');
|
|
@@ -1335,13 +1261,15 @@ export class Bridge extends EventEmitter {
|
|
|
1335
1261
|
await agent.tgBot.answerCallbackQuery(query.callbackQueryId, 'Path not found');
|
|
1336
1262
|
break;
|
|
1337
1263
|
}
|
|
1338
|
-
|
|
1339
|
-
this.
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
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);
|
|
1343
1269
|
await agent.tgBot.answerCallbackQuery(query.callbackQueryId, `Repo: ${repoName}`);
|
|
1344
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');
|
|
1345
1273
|
break;
|
|
1346
1274
|
}
|
|
1347
1275
|
case 'repo_add': {
|
|
@@ -1356,10 +1284,14 @@ export class Bridge extends EventEmitter {
|
|
|
1356
1284
|
await agent.tgBot.sendText(query.chatId, 'Send: <code>/model <model-name></code>', 'HTML');
|
|
1357
1285
|
break;
|
|
1358
1286
|
}
|
|
1359
|
-
|
|
1360
|
-
|
|
1287
|
+
const oldModelCb = agent.model;
|
|
1288
|
+
agent.model = model;
|
|
1289
|
+
this.sessionStore.setModel(agentId, model);
|
|
1290
|
+
this.killAgentProcess(agentId);
|
|
1361
1291
|
await agent.tgBot.answerCallbackQuery(query.callbackQueryId, `Model: ${model}`);
|
|
1362
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');
|
|
1363
1295
|
break;
|
|
1364
1296
|
}
|
|
1365
1297
|
case 'permissions': {
|
|
@@ -1369,9 +1301,8 @@ export class Bridge extends EventEmitter {
|
|
|
1369
1301
|
await agent.tgBot.answerCallbackQuery(query.callbackQueryId, 'Invalid mode');
|
|
1370
1302
|
break;
|
|
1371
1303
|
}
|
|
1372
|
-
this.sessionStore.setPermissionMode(agentId,
|
|
1373
|
-
|
|
1374
|
-
this.disconnectClient(agentId, query.userId, query.chatId);
|
|
1304
|
+
this.sessionStore.setPermissionMode(agentId, mode);
|
|
1305
|
+
this.killAgentProcess(agentId);
|
|
1375
1306
|
await agent.tgBot.answerCallbackQuery(query.callbackQueryId, `Mode: ${mode}`);
|
|
1376
1307
|
await agent.tgBot.sendText(query.chatId, `Permission mode set to <code>${escapeHtml(mode)}</code>. Takes effect on next message.`, 'HTML');
|
|
1377
1308
|
break;
|
|
@@ -1398,9 +1329,8 @@ export class Bridge extends EventEmitter {
|
|
|
1398
1329
|
await agent.tgBot.answerCallbackQuery(query.callbackQueryId, 'Permission expired');
|
|
1399
1330
|
break;
|
|
1400
1331
|
}
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
proc.respondToPermission(requestId, true);
|
|
1332
|
+
if (agent.ccProcess) {
|
|
1333
|
+
agent.ccProcess.respondToPermission(requestId, true);
|
|
1404
1334
|
}
|
|
1405
1335
|
agent.pendingPermissions.delete(requestId);
|
|
1406
1336
|
await agent.tgBot.answerCallbackQuery(query.callbackQueryId, '✅ Allowed');
|
|
@@ -1413,27 +1343,22 @@ export class Bridge extends EventEmitter {
|
|
|
1413
1343
|
await agent.tgBot.answerCallbackQuery(query.callbackQueryId, 'Permission expired');
|
|
1414
1344
|
break;
|
|
1415
1345
|
}
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
proc.respondToPermission(requestId, false);
|
|
1346
|
+
if (agent.ccProcess) {
|
|
1347
|
+
agent.ccProcess.respondToPermission(requestId, false);
|
|
1419
1348
|
}
|
|
1420
1349
|
agent.pendingPermissions.delete(requestId);
|
|
1421
1350
|
await agent.tgBot.answerCallbackQuery(query.callbackQueryId, '❌ Denied');
|
|
1422
1351
|
break;
|
|
1423
1352
|
}
|
|
1424
1353
|
case 'perm_allow_all': {
|
|
1425
|
-
// Allow all pending permissions for this
|
|
1426
|
-
const targetUserId = query.data;
|
|
1354
|
+
// Allow all pending permissions for this agent
|
|
1427
1355
|
const toAllow = [];
|
|
1428
|
-
for (const [reqId
|
|
1429
|
-
|
|
1430
|
-
toAllow.push(reqId);
|
|
1431
|
-
}
|
|
1356
|
+
for (const [reqId] of agent.pendingPermissions) {
|
|
1357
|
+
toAllow.push(reqId);
|
|
1432
1358
|
}
|
|
1433
|
-
const proc = agent.processes.get(targetUserId);
|
|
1434
1359
|
for (const reqId of toAllow) {
|
|
1435
|
-
if (
|
|
1436
|
-
|
|
1360
|
+
if (agent.ccProcess)
|
|
1361
|
+
agent.ccProcess.respondToPermission(reqId, true);
|
|
1437
1362
|
agent.pendingPermissions.delete(reqId);
|
|
1438
1363
|
}
|
|
1439
1364
|
await agent.tgBot.answerCallbackQuery(query.callbackQueryId, `✅ Allowed ${toAllow.length} permission(s)`);
|
|
@@ -1447,24 +1372,18 @@ export class Bridge extends EventEmitter {
|
|
|
1447
1372
|
handleCtlMessage(agentId, text, sessionId) {
|
|
1448
1373
|
const agent = this.agents.get(agentId);
|
|
1449
1374
|
if (!agent) {
|
|
1450
|
-
// Return error via the CtlAckResponse shape won't work — but the ctl-server
|
|
1451
|
-
// protocol handles errors separately. We'll throw and let it catch.
|
|
1452
1375
|
throw new Error(`Unknown agent: ${agentId}`);
|
|
1453
1376
|
}
|
|
1454
|
-
//
|
|
1455
|
-
const userId = agent.config.allowedUsers[0];
|
|
1456
|
-
const chatId = Number(userId);
|
|
1457
|
-
// If explicit session requested, set it
|
|
1377
|
+
// If explicit session requested, set it as pending
|
|
1458
1378
|
if (sessionId) {
|
|
1459
|
-
|
|
1379
|
+
agent.pendingSessionId = sessionId;
|
|
1460
1380
|
}
|
|
1461
1381
|
// Route through the same sendToCC path as Telegram
|
|
1462
|
-
this.sendToCC(agentId,
|
|
1463
|
-
const proc = agent.processes.get(userId);
|
|
1382
|
+
this.sendToCC(agentId, { text }, { spawnSource: 'cli' });
|
|
1464
1383
|
return {
|
|
1465
1384
|
type: 'ack',
|
|
1466
|
-
sessionId:
|
|
1467
|
-
state:
|
|
1385
|
+
sessionId: agent.ccProcess?.sessionId ?? null,
|
|
1386
|
+
state: agent.ccProcess?.state ?? 'idle',
|
|
1468
1387
|
};
|
|
1469
1388
|
}
|
|
1470
1389
|
handleCtlStatus(agentId) {
|
|
@@ -1475,30 +1394,16 @@ export class Bridge extends EventEmitter {
|
|
|
1475
1394
|
const agent = this.agents.get(id);
|
|
1476
1395
|
if (!agent)
|
|
1477
1396
|
continue;
|
|
1478
|
-
|
|
1479
|
-
let state = 'idle';
|
|
1480
|
-
for (const [, proc] of agent.processes) {
|
|
1481
|
-
if (proc.state === 'active') {
|
|
1482
|
-
state = 'active';
|
|
1483
|
-
break;
|
|
1484
|
-
}
|
|
1485
|
-
if (proc.state === 'spawning')
|
|
1486
|
-
state = 'spawning';
|
|
1487
|
-
}
|
|
1488
|
-
const userId = agent.config.allowedUsers[0];
|
|
1489
|
-
const proc = agent.processes.get(userId);
|
|
1490
|
-
const userConfig = resolveUserConfig(agent.config, userId);
|
|
1397
|
+
const state = agent.ccProcess?.state ?? 'idle';
|
|
1491
1398
|
agents.push({
|
|
1492
1399
|
id,
|
|
1493
1400
|
state,
|
|
1494
|
-
sessionId:
|
|
1495
|
-
repo:
|
|
1401
|
+
sessionId: agent.ccProcess?.sessionId ?? null,
|
|
1402
|
+
repo: agent.repo,
|
|
1496
1403
|
});
|
|
1497
|
-
// List sessions
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
if (sessRepo) {
|
|
1501
|
-
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)) {
|
|
1502
1407
|
sessions.push({
|
|
1503
1408
|
id: d.id,
|
|
1504
1409
|
agentId: id,
|
|
@@ -1516,10 +1421,59 @@ export class Bridge extends EventEmitter {
|
|
|
1516
1421
|
if (!agent) {
|
|
1517
1422
|
return { id: request.id, success: false, error: `Unknown agent: ${request.agentId}` };
|
|
1518
1423
|
}
|
|
1519
|
-
// Find the chat ID for this user (from the most recent message)
|
|
1520
|
-
// We use the userId to find which chat to send to
|
|
1521
|
-
const chatId = Number(request.userId); // In TG, private chat ID === user ID
|
|
1522
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
|
+
}
|
|
1523
1477
|
switch (request.tool) {
|
|
1524
1478
|
case 'send_file':
|
|
1525
1479
|
await agent.tgBot.sendFile(chatId, request.params.path, request.params.caption);
|
|
@@ -1538,6 +1492,320 @@ export class Bridge extends EventEmitter {
|
|
|
1538
1492
|
return { id: request.id, success: false, error: err instanceof Error ? err.message : 'Unknown error' };
|
|
1539
1493
|
}
|
|
1540
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
|
+
}
|
|
1541
1809
|
// ── Shutdown ──
|
|
1542
1810
|
async stop() {
|
|
1543
1811
|
this.logger.info('Stopping bridge');
|
|
@@ -1545,6 +1813,7 @@ export class Bridge extends EventEmitter {
|
|
|
1545
1813
|
await this.stopAgent(agentId);
|
|
1546
1814
|
}
|
|
1547
1815
|
this.processRegistry.clear();
|
|
1816
|
+
this.highSignalDetector.destroy();
|
|
1548
1817
|
this.mcpServer.closeAll();
|
|
1549
1818
|
this.ctlServer.closeAll();
|
|
1550
1819
|
this.removeAllListeners();
|