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