@fonz/tgcc 0.6.13 → 0.6.15
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/dist/bridge.d.ts +8 -2
- package/dist/bridge.js +545 -287
- package/dist/bridge.js.map +1 -1
- package/dist/cc-process.d.ts +6 -0
- package/dist/cc-process.js +60 -3
- package/dist/cc-process.js.map +1 -1
- package/dist/cc-protocol.d.ts +1 -0
- package/dist/cc-protocol.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/process-registry.d.ts +60 -0
- package/dist/process-registry.js +176 -0
- package/dist/process-registry.js.map +1 -0
- package/dist/session.d.ts +11 -33
- package/dist/session.js +185 -309
- package/dist/session.js.map +1 -1
- package/dist/streaming.d.ts +30 -4
- package/dist/streaming.js +387 -38
- package/dist/streaming.js.map +1 -1
- package/dist/telegram.d.ts +1 -0
- package/dist/telegram.js +3 -0
- package/dist/telegram.js.map +1 -1
- package/package.json +1 -1
package/dist/bridge.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
-
import { existsSync, readFileSync
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { EventEmitter } from 'node:events';
|
|
5
5
|
import pino from 'pino';
|
|
@@ -10,8 +10,9 @@ import { StreamAccumulator, SubAgentTracker, escapeHtml } 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
|
-
import { SessionStore,
|
|
13
|
+
import { SessionStore, discoverCCSessions, } from './session.js';
|
|
14
14
|
import { CtlServer, } from './ctl-server.js';
|
|
15
|
+
import { ProcessRegistry } from './process-registry.js';
|
|
15
16
|
// ── Message Batcher ──
|
|
16
17
|
class MessageBatcher {
|
|
17
18
|
pending = [];
|
|
@@ -97,6 +98,8 @@ const HELP_TEXT = `<b>TGCC Commands</b>
|
|
|
97
98
|
export class Bridge extends EventEmitter {
|
|
98
99
|
config;
|
|
99
100
|
agents = new Map();
|
|
101
|
+
processRegistry = new ProcessRegistry();
|
|
102
|
+
sessionModelOverrides = new Map(); // "agentId:userId" → model (from /model cmd, not persisted)
|
|
100
103
|
mcpServer;
|
|
101
104
|
ctlServer;
|
|
102
105
|
sessionStore;
|
|
@@ -128,7 +131,6 @@ export class Bridge extends EventEmitter {
|
|
|
128
131
|
accumulators: new Map(),
|
|
129
132
|
subAgentTrackers: new Map(),
|
|
130
133
|
batchers: new Map(),
|
|
131
|
-
pendingTitles: new Map(),
|
|
132
134
|
pendingPermissions: new Map(),
|
|
133
135
|
typingIntervals: new Map(),
|
|
134
136
|
};
|
|
@@ -174,6 +176,12 @@ export class Bridge extends EventEmitter {
|
|
|
174
176
|
this.logger.info({ agentId }, 'Stopping agent');
|
|
175
177
|
// Stop bot
|
|
176
178
|
await agent.tgBot.stop();
|
|
179
|
+
// Unsubscribe all clients for this agent from the registry
|
|
180
|
+
for (const [userId] of agent.processes) {
|
|
181
|
+
const chatId = Number(userId); // In TG, private chat ID === user ID
|
|
182
|
+
const clientRef = { agentId, userId, chatId };
|
|
183
|
+
this.processRegistry.unsubscribe(clientRef);
|
|
184
|
+
}
|
|
177
185
|
// Kill all CC processes and wait for them to exit
|
|
178
186
|
const processExitPromises = [];
|
|
179
187
|
for (const [, proc] of agent.processes) {
|
|
@@ -223,7 +231,6 @@ export class Bridge extends EventEmitter {
|
|
|
223
231
|
clearInterval(interval);
|
|
224
232
|
}
|
|
225
233
|
agent.typingIntervals.clear();
|
|
226
|
-
agent.pendingTitles.clear();
|
|
227
234
|
agent.pendingPermissions.clear();
|
|
228
235
|
agent.processes.clear();
|
|
229
236
|
agent.batchers.clear();
|
|
@@ -273,13 +280,17 @@ export class Bridge extends EventEmitter {
|
|
|
273
280
|
else {
|
|
274
281
|
ccMsg = createTextMessage(data.text);
|
|
275
282
|
}
|
|
276
|
-
|
|
277
|
-
|
|
283
|
+
const clientRef = { agentId, userId, chatId };
|
|
284
|
+
// Check if this client already has a process via the registry
|
|
285
|
+
let existingEntry = this.processRegistry.findByClient(clientRef);
|
|
286
|
+
let proc = existingEntry?.ccProcess;
|
|
278
287
|
if (proc?.takenOver) {
|
|
279
288
|
// Session was taken over externally — discard old process
|
|
280
|
-
|
|
289
|
+
const entry = this.processRegistry.findByClient(clientRef);
|
|
290
|
+
this.processRegistry.destroy(entry.repo, entry.sessionId);
|
|
281
291
|
agent.processes.delete(userId);
|
|
282
292
|
proc = undefined;
|
|
293
|
+
existingEntry = null;
|
|
283
294
|
}
|
|
284
295
|
// Staleness check: detect if session was modified by another client
|
|
285
296
|
// Skip if background sub-agents are running — their results grow the JSONL
|
|
@@ -292,9 +303,11 @@ export class Bridge extends EventEmitter {
|
|
|
292
303
|
if (staleInfo) {
|
|
293
304
|
// Session was modified externally — silently reconnect for roaming support
|
|
294
305
|
this.logger.info({ agentId, userId }, 'Session modified externally — reconnecting for roaming');
|
|
295
|
-
|
|
306
|
+
const entry = this.processRegistry.findByClient(clientRef);
|
|
307
|
+
this.processRegistry.unsubscribe(clientRef);
|
|
296
308
|
agent.processes.delete(userId);
|
|
297
309
|
proc = undefined;
|
|
310
|
+
existingEntry = null;
|
|
298
311
|
}
|
|
299
312
|
}
|
|
300
313
|
if (!proc || proc.state === 'idle') {
|
|
@@ -302,11 +315,27 @@ export class Bridge extends EventEmitter {
|
|
|
302
315
|
const userState2 = this.sessionStore.getUser(agentId, userId);
|
|
303
316
|
const resolvedRepo = userState2.repo || resolveUserConfig(agent.config, userId).repo;
|
|
304
317
|
if (resolvedRepo === homedir()) {
|
|
305
|
-
agent.tgBot.sendText(chatId, '<blockquote>⚠️ No project selected. Use /repo to pick one, or CC will run in your home directory.</blockquote>', 'HTML');
|
|
318
|
+
agent.tgBot.sendText(chatId, '<blockquote>⚠️ No project selected. Use /repo to pick one, or CC will run in your home directory.</blockquote>', 'HTML').catch(err => this.logger.error({ err }, 'Failed to send no-repo warning'));
|
|
319
|
+
}
|
|
320
|
+
// Check if another client already has a process for this repo+session
|
|
321
|
+
const sessionId = userState2.currentSessionId;
|
|
322
|
+
if (sessionId && resolvedRepo) {
|
|
323
|
+
const sharedEntry = this.processRegistry.get(resolvedRepo, sessionId);
|
|
324
|
+
if (sharedEntry && sharedEntry.ccProcess.state !== 'idle') {
|
|
325
|
+
// Attach to existing process as subscriber
|
|
326
|
+
this.processRegistry.subscribe(resolvedRepo, sessionId, clientRef);
|
|
327
|
+
proc = sharedEntry.ccProcess;
|
|
328
|
+
agent.processes.set(userId, proc);
|
|
329
|
+
// Notify the user they've attached
|
|
330
|
+
agent.tgBot.sendText(chatId, '<blockquote>📎 Attached to existing session process.</blockquote>', 'HTML').catch(err => this.logger.error({ err }, 'Failed to send attach notification'));
|
|
331
|
+
// Show typing indicator and forward message
|
|
332
|
+
this.startTypingIndicator(agent, userId, chatId);
|
|
333
|
+
proc.sendMessage(ccMsg);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
306
336
|
}
|
|
307
337
|
// Save first message text as pending session title
|
|
308
338
|
if (data.text) {
|
|
309
|
-
agent.pendingTitles.set(userId, data.text);
|
|
310
339
|
}
|
|
311
340
|
proc = this.spawnCCProcess(agentId, userId, chatId);
|
|
312
341
|
agent.processes.set(userId, proc);
|
|
@@ -314,52 +343,33 @@ export class Bridge extends EventEmitter {
|
|
|
314
343
|
// Show typing indicator (repeated every 4s — TG typing expires after ~5s)
|
|
315
344
|
this.startTypingIndicator(agent, userId, chatId);
|
|
316
345
|
proc.sendMessage(ccMsg);
|
|
317
|
-
this.sessionStore.updateSessionActivity(agentId, userId);
|
|
318
346
|
}
|
|
319
347
|
/** Check if the session JSONL was modified externally since we last tracked it. */
|
|
320
|
-
checkSessionStaleness(
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
if (!sessionId)
|
|
324
|
-
return null;
|
|
325
|
-
const repo = userState.repo || resolveUserConfig(this.agents.get(agentId).config, userId).repo;
|
|
326
|
-
const jsonlPath = getSessionJsonlPath(sessionId, repo);
|
|
327
|
-
const tracking = this.sessionStore.getJsonlTracking(agentId, userId);
|
|
328
|
-
// No tracking yet (first message or new session) — not stale
|
|
329
|
-
if (!tracking)
|
|
330
|
-
return null;
|
|
331
|
-
try {
|
|
332
|
-
const stat = statSync(jsonlPath);
|
|
333
|
-
// File grew or was modified since we last tracked
|
|
334
|
-
if (stat.size <= tracking.size && stat.mtimeMs <= tracking.mtimeMs) {
|
|
335
|
-
return null;
|
|
336
|
-
}
|
|
337
|
-
// Session is stale — build a summary of what happened
|
|
338
|
-
const summary = summarizeJsonlDelta(jsonlPath, tracking.size)
|
|
339
|
-
?? '<i>ℹ️ Session was updated from another client. Reconnecting...</i>';
|
|
340
|
-
return { summary };
|
|
341
|
-
}
|
|
342
|
-
catch {
|
|
343
|
-
// File doesn't exist or stat failed — skip check
|
|
344
|
-
return null;
|
|
345
|
-
}
|
|
348
|
+
checkSessionStaleness(_agentId, _userId) {
|
|
349
|
+
// With shared process registry, staleness is handled by the registry itself
|
|
350
|
+
return null;
|
|
346
351
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
+
// ── Process cleanup helper ──
|
|
353
|
+
/**
|
|
354
|
+
* Disconnect a client from its CC process.
|
|
355
|
+
* If other subscribers remain, the process stays alive.
|
|
356
|
+
* If this was the last subscriber, the process is destroyed.
|
|
357
|
+
*/
|
|
358
|
+
disconnectClient(agentId, userId, chatId) {
|
|
359
|
+
const agent = this.agents.get(agentId);
|
|
360
|
+
if (!agent)
|
|
352
361
|
return;
|
|
353
|
-
const
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
this
|
|
362
|
+
const clientRef = { agentId, userId, chatId };
|
|
363
|
+
const proc = agent.processes.get(userId);
|
|
364
|
+
const destroyed = this.processRegistry.unsubscribe(clientRef);
|
|
365
|
+
if (!destroyed && proc) {
|
|
366
|
+
// Other subscribers still attached — just remove from this agent's map
|
|
358
367
|
}
|
|
359
|
-
|
|
360
|
-
//
|
|
361
|
-
|
|
368
|
+
else if (proc && !destroyed) {
|
|
369
|
+
// Not in registry but has a process — destroy directly (legacy path)
|
|
370
|
+
proc.destroy();
|
|
362
371
|
}
|
|
372
|
+
agent.processes.delete(userId);
|
|
363
373
|
}
|
|
364
374
|
// ── Typing indicator management ──
|
|
365
375
|
startTypingIndicator(agent, userId, chatId) {
|
|
@@ -385,12 +395,32 @@ export class Bridge extends EventEmitter {
|
|
|
385
395
|
spawnCCProcess(agentId, userId, chatId) {
|
|
386
396
|
const agent = this.agents.get(agentId);
|
|
387
397
|
const userConfig = resolveUserConfig(agent.config, userId);
|
|
388
|
-
// Check session store for
|
|
398
|
+
// Check session store for repo/permission overrides
|
|
389
399
|
const userState = this.sessionStore.getUser(agentId, userId);
|
|
390
|
-
if (userState.model)
|
|
391
|
-
userConfig.model = userState.model;
|
|
392
400
|
if (userState.repo)
|
|
393
401
|
userConfig.repo = userState.repo;
|
|
402
|
+
// Model priority: /model override > running process > session JSONL > agent config default
|
|
403
|
+
const modelOverride = this.sessionModelOverrides.get(`${agentId}:${userId}`);
|
|
404
|
+
if (modelOverride) {
|
|
405
|
+
userConfig.model = modelOverride;
|
|
406
|
+
// Don't delete yet — cleared when CC writes first assistant message
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
const currentSessionId = userState.currentSessionId;
|
|
410
|
+
if (currentSessionId && userConfig.repo) {
|
|
411
|
+
const registryEntry = this.processRegistry.get(userConfig.repo, currentSessionId);
|
|
412
|
+
if (registryEntry?.model) {
|
|
413
|
+
userConfig.model = registryEntry.model;
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
const sessions = discoverCCSessions(userConfig.repo, 20);
|
|
417
|
+
const sessionInfo = sessions.find(s => s.id === currentSessionId);
|
|
418
|
+
if (sessionInfo?.model) {
|
|
419
|
+
userConfig.model = sessionInfo.model;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
394
424
|
if (userState.permissionMode) {
|
|
395
425
|
userConfig.permissionMode = userState.permissionMode;
|
|
396
426
|
}
|
|
@@ -410,98 +440,171 @@ export class Bridge extends EventEmitter {
|
|
|
410
440
|
continueSession: !!userState.currentSessionId,
|
|
411
441
|
logger: this.logger,
|
|
412
442
|
});
|
|
413
|
-
//
|
|
443
|
+
// Register in the process registry
|
|
444
|
+
const ownerRef = { agentId, userId, chatId };
|
|
445
|
+
// We'll register once we know the sessionId (on init), but we need a
|
|
446
|
+
// temporary entry for pre-init event routing. Use a placeholder sessionId
|
|
447
|
+
// that gets updated on init.
|
|
448
|
+
const tentativeSessionId = userState.currentSessionId ?? `pending-${Date.now()}`;
|
|
449
|
+
const registryEntry = this.processRegistry.register(userConfig.repo, tentativeSessionId, userConfig.model || 'default', proc, ownerRef);
|
|
450
|
+
// ── Helper: get all subscribers for this process from the registry ──
|
|
451
|
+
const getEntry = () => this.processRegistry.findByProcess(proc);
|
|
452
|
+
// ── Wire up event handlers (broadcast to all subscribers) ──
|
|
414
453
|
proc.on('init', (event) => {
|
|
415
454
|
this.sessionStore.setCurrentSession(agentId, userId, event.session_id);
|
|
416
|
-
//
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
455
|
+
// Update registry key if session ID changed from tentative
|
|
456
|
+
if (event.session_id !== tentativeSessionId) {
|
|
457
|
+
// Re-register with the real session ID
|
|
458
|
+
const entry = getEntry();
|
|
459
|
+
if (entry) {
|
|
460
|
+
// Save subscribers before removing
|
|
461
|
+
const savedSubs = [...entry.subscribers.entries()];
|
|
462
|
+
this.processRegistry.remove(userConfig.repo, tentativeSessionId);
|
|
463
|
+
const newEntry = this.processRegistry.register(userConfig.repo, event.session_id, userConfig.model || 'default', proc, ownerRef);
|
|
464
|
+
// Restore additional subscribers
|
|
465
|
+
for (const [, sub] of savedSubs) {
|
|
466
|
+
if (sub.client.agentId !== agentId || sub.client.userId !== userId || sub.client.chatId !== chatId) {
|
|
467
|
+
this.processRegistry.subscribe(userConfig.repo, event.session_id, sub.client);
|
|
468
|
+
const newSub = this.processRegistry.getSubscriber(newEntry, sub.client);
|
|
469
|
+
if (newSub) {
|
|
470
|
+
newSub.accumulator = sub.accumulator;
|
|
471
|
+
newSub.tracker = sub.tracker;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
421
476
|
}
|
|
422
|
-
// Initialize JSONL tracking for staleness detection
|
|
423
|
-
this.updateJsonlTracking(agentId, userId);
|
|
424
477
|
});
|
|
425
478
|
proc.on('stream_event', (event) => {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
const tracker = agent.subAgentTrackers.get(accKey);
|
|
431
|
-
if (!tracker)
|
|
479
|
+
const entry = getEntry();
|
|
480
|
+
if (!entry) {
|
|
481
|
+
// Fallback: single subscriber mode (shouldn't happen)
|
|
482
|
+
this.handleStreamEvent(agentId, userId, chatId, event);
|
|
432
483
|
return;
|
|
433
|
-
const resultText = typeof event.content === 'string' ? event.content : JSON.stringify(event.content);
|
|
434
|
-
const meta = event.tool_use_result;
|
|
435
|
-
// Log warning if structured metadata is missing
|
|
436
|
-
if (!meta && /agent_id:\s*\S+@\S+/.test(resultText)) {
|
|
437
|
-
this.logger.warn({ agentId, toolUseId: event.tool_use_id }, 'Spawn detected in text but no structured tool_use_result metadata - skipping');
|
|
438
484
|
}
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
485
|
+
for (const sub of this.processRegistry.subscribers(entry)) {
|
|
486
|
+
this.handleStreamEvent(sub.client.agentId, sub.client.userId, sub.client.chatId, event);
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
proc.on('tool_result', (event) => {
|
|
490
|
+
const entry = getEntry();
|
|
491
|
+
const subscriberList = entry ? [...this.processRegistry.subscribers(entry)] : [{ client: ownerRef }];
|
|
492
|
+
for (const sub of subscriberList) {
|
|
493
|
+
const subAgent = this.agents.get(sub.client.agentId);
|
|
494
|
+
if (!subAgent)
|
|
495
|
+
continue;
|
|
496
|
+
const accKey = `${sub.client.userId}:${sub.client.chatId}`;
|
|
497
|
+
// Resolve tool indicator message with success/failure status
|
|
498
|
+
const acc2 = subAgent.accumulators.get(accKey);
|
|
499
|
+
if (acc2 && event.tool_use_id) {
|
|
500
|
+
const isError = event.is_error === true;
|
|
501
|
+
const contentStr = typeof event.content === 'string' ? event.content : JSON.stringify(event.content);
|
|
502
|
+
const errorMsg = isError ? contentStr : undefined;
|
|
503
|
+
acc2.resolveToolMessage(event.tool_use_id, isError, errorMsg, contentStr, event.tool_use_result);
|
|
451
504
|
}
|
|
452
|
-
|
|
453
|
-
if (
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
505
|
+
const tracker = subAgent.subAgentTrackers.get(accKey);
|
|
506
|
+
if (!tracker)
|
|
507
|
+
continue;
|
|
508
|
+
const resultText = typeof event.content === 'string' ? event.content : JSON.stringify(event.content);
|
|
509
|
+
const meta = event.tool_use_result;
|
|
510
|
+
// Log warning if structured metadata is missing
|
|
511
|
+
if (!meta && /agent_id:\s*\S+@\S+/.test(resultText)) {
|
|
512
|
+
this.logger.warn({ agentId: sub.client.agentId, toolUseId: event.tool_use_id }, 'Spawn detected in text but no structured tool_use_result metadata - skipping');
|
|
513
|
+
}
|
|
514
|
+
const spawnMeta = meta?.status === 'teammate_spawned' ? meta : undefined;
|
|
515
|
+
if (spawnMeta?.status === 'teammate_spawned' && spawnMeta.team_name) {
|
|
516
|
+
if (!tracker.currentTeamName) {
|
|
517
|
+
this.logger.info({ agentId: sub.client.agentId, teamName: spawnMeta.team_name, agentName: spawnMeta.name, agentType: spawnMeta.agent_type }, 'Spawn detected');
|
|
518
|
+
tracker.setTeamName(spawnMeta.team_name);
|
|
519
|
+
// Wire the "all agents reported" callback to send follow-up to CC
|
|
520
|
+
tracker.setOnAllReported(() => {
|
|
521
|
+
if (proc.state === 'active') {
|
|
522
|
+
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.'));
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
// Set agent metadata from structured data or text fallback
|
|
527
|
+
if (event.tool_use_id && spawnMeta.name) {
|
|
528
|
+
tracker.setAgentMetadata(event.tool_use_id, {
|
|
529
|
+
agentName: spawnMeta.name,
|
|
530
|
+
agentType: spawnMeta.agent_type,
|
|
531
|
+
color: spawnMeta.color,
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
// Handle tool result (sets status, edits TG message)
|
|
536
|
+
if (event.tool_use_id) {
|
|
537
|
+
tracker.handleToolResult(event.tool_use_id, resultText);
|
|
538
|
+
}
|
|
539
|
+
// Start mailbox watch AFTER handleToolResult has set agent names
|
|
540
|
+
if (tracker.currentTeamName && tracker.hasDispatchedAgents && !tracker.isMailboxWatching) {
|
|
541
|
+
tracker.startMailboxWatch();
|
|
459
542
|
}
|
|
460
|
-
}
|
|
461
|
-
// Handle tool result (sets status, edits TG message)
|
|
462
|
-
if (event.tool_use_id) {
|
|
463
|
-
tracker.handleToolResult(event.tool_use_id, resultText);
|
|
464
|
-
}
|
|
465
|
-
// Start mailbox watch AFTER handleToolResult has set agent names
|
|
466
|
-
if (tracker.currentTeamName && tracker.hasDispatchedAgents && !tracker.isMailboxWatching) {
|
|
467
|
-
tracker.startMailboxWatch();
|
|
468
543
|
}
|
|
469
544
|
});
|
|
470
|
-
// System events for background task tracking
|
|
545
|
+
// System events for background task tracking — broadcast to all subscribers
|
|
471
546
|
proc.on('task_started', (event) => {
|
|
472
|
-
const
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
547
|
+
const entry = getEntry();
|
|
548
|
+
const subscriberList = entry ? [...this.processRegistry.subscribers(entry)] : [{ client: ownerRef }];
|
|
549
|
+
for (const sub of subscriberList) {
|
|
550
|
+
const subAgent = this.agents.get(sub.client.agentId);
|
|
551
|
+
if (!subAgent)
|
|
552
|
+
continue;
|
|
553
|
+
const accKey = `${sub.client.userId}:${sub.client.chatId}`;
|
|
554
|
+
const tracker = subAgent.subAgentTrackers.get(accKey);
|
|
555
|
+
if (tracker) {
|
|
556
|
+
tracker.handleTaskStarted(event.tool_use_id, event.description, event.task_type);
|
|
557
|
+
}
|
|
476
558
|
}
|
|
477
559
|
});
|
|
478
560
|
proc.on('task_progress', (event) => {
|
|
479
|
-
const
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
561
|
+
const entry = getEntry();
|
|
562
|
+
const subscriberList = entry ? [...this.processRegistry.subscribers(entry)] : [{ client: ownerRef }];
|
|
563
|
+
for (const sub of subscriberList) {
|
|
564
|
+
const subAgent = this.agents.get(sub.client.agentId);
|
|
565
|
+
if (!subAgent)
|
|
566
|
+
continue;
|
|
567
|
+
const accKey = `${sub.client.userId}:${sub.client.chatId}`;
|
|
568
|
+
const tracker = subAgent.subAgentTrackers.get(accKey);
|
|
569
|
+
if (tracker) {
|
|
570
|
+
tracker.handleTaskProgress(event.tool_use_id, event.description, event.last_tool_name);
|
|
571
|
+
}
|
|
483
572
|
}
|
|
484
573
|
});
|
|
485
574
|
proc.on('task_completed', (event) => {
|
|
486
|
-
const
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
575
|
+
const entry = getEntry();
|
|
576
|
+
const subscriberList = entry ? [...this.processRegistry.subscribers(entry)] : [{ client: ownerRef }];
|
|
577
|
+
for (const sub of subscriberList) {
|
|
578
|
+
const subAgent = this.agents.get(sub.client.agentId);
|
|
579
|
+
if (!subAgent)
|
|
580
|
+
continue;
|
|
581
|
+
const accKey = `${sub.client.userId}:${sub.client.chatId}`;
|
|
582
|
+
const tracker = subAgent.subAgentTrackers.get(accKey);
|
|
583
|
+
if (tracker) {
|
|
584
|
+
tracker.handleTaskCompleted(event.tool_use_id);
|
|
585
|
+
}
|
|
490
586
|
}
|
|
491
587
|
});
|
|
492
|
-
// Media from tool results (images, PDFs, etc.)
|
|
588
|
+
// Media from tool results (images, PDFs, etc.) — broadcast to all subscribers
|
|
493
589
|
proc.on('media', (media) => {
|
|
494
590
|
const buf = Buffer.from(media.data, 'base64');
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
591
|
+
const entry = getEntry();
|
|
592
|
+
const subscriberList = entry ? [...this.processRegistry.subscribers(entry)] : [{ client: ownerRef }];
|
|
593
|
+
for (const sub of subscriberList) {
|
|
594
|
+
const subAgent = this.agents.get(sub.client.agentId);
|
|
595
|
+
if (!subAgent)
|
|
596
|
+
continue;
|
|
597
|
+
if (media.kind === 'image') {
|
|
598
|
+
subAgent.tgBot.sendPhotoBuffer(sub.client.chatId, buf).catch(err => {
|
|
599
|
+
this.logger.error({ err, agentId: sub.client.agentId, userId: sub.client.userId }, 'Failed to send tool_result image');
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
else if (media.kind === 'document') {
|
|
603
|
+
const ext = media.media_type === 'application/pdf' ? '.pdf' : '';
|
|
604
|
+
subAgent.tgBot.sendDocumentBuffer(sub.client.chatId, buf, `document${ext}`).catch(err => {
|
|
605
|
+
this.logger.error({ err, agentId: sub.client.agentId, userId: sub.client.userId }, 'Failed to send tool_result document');
|
|
606
|
+
});
|
|
607
|
+
}
|
|
505
608
|
}
|
|
506
609
|
});
|
|
507
610
|
proc.on('assistant', (event) => {
|
|
@@ -509,13 +612,24 @@ export class Bridge extends EventEmitter {
|
|
|
509
612
|
// In practice, stream_events handle the display
|
|
510
613
|
});
|
|
511
614
|
proc.on('result', (event) => {
|
|
512
|
-
|
|
513
|
-
this.
|
|
615
|
+
// Model override consumed — CC has written to JSONL
|
|
616
|
+
this.sessionModelOverrides.delete(`${agentId}:${userId}`);
|
|
617
|
+
// Broadcast result to all subscribers
|
|
618
|
+
const entry = getEntry();
|
|
619
|
+
const subscriberList = entry ? [...this.processRegistry.subscribers(entry)] : [{ client: ownerRef }];
|
|
620
|
+
for (const sub of subscriberList) {
|
|
621
|
+
const subAgent = this.agents.get(sub.client.agentId);
|
|
622
|
+
if (!subAgent)
|
|
623
|
+
continue;
|
|
624
|
+
this.stopTypingIndicator(subAgent, sub.client.userId, sub.client.chatId);
|
|
625
|
+
this.handleResult(sub.client.agentId, sub.client.userId, sub.client.chatId, event);
|
|
626
|
+
}
|
|
514
627
|
});
|
|
515
628
|
proc.on('permission_request', (event) => {
|
|
629
|
+
// Send permission request only to the owner (first subscriber)
|
|
516
630
|
const req = event.request;
|
|
517
631
|
const requestId = event.request_id;
|
|
518
|
-
// Store pending permission
|
|
632
|
+
// Store pending permission on the owner's agent
|
|
519
633
|
agent.pendingPermissions.set(requestId, {
|
|
520
634
|
requestId,
|
|
521
635
|
userId,
|
|
@@ -534,7 +648,8 @@ export class Bridge extends EventEmitter {
|
|
|
534
648
|
.text('✅ Allow', `perm_allow:${requestId}`)
|
|
535
649
|
.text('❌ Deny', `perm_deny:${requestId}`)
|
|
536
650
|
.text('✅ Allow All', `perm_allow_all:${userId}`);
|
|
537
|
-
agent.tgBot.sendTextWithKeyboard(chatId, text, keyboard, 'HTML')
|
|
651
|
+
agent.tgBot.sendTextWithKeyboard(chatId, text, keyboard, 'HTML')
|
|
652
|
+
.catch(err => this.logger.error({ err }, 'Failed to send permission request'));
|
|
538
653
|
});
|
|
539
654
|
proc.on('api_error', (event) => {
|
|
540
655
|
const errMsg = event.error?.message || 'Unknown API error';
|
|
@@ -546,39 +661,100 @@ export class Bridge extends EventEmitter {
|
|
|
546
661
|
const text = isOverloaded
|
|
547
662
|
? `<blockquote>⚠️ API overloaded, retrying...${retryInfo}</blockquote>`
|
|
548
663
|
: `<blockquote>⚠️ ${escapeHtml(errMsg)}${retryInfo}</blockquote>`;
|
|
549
|
-
|
|
664
|
+
// Broadcast API error to all subscribers
|
|
665
|
+
const entry = getEntry();
|
|
666
|
+
const subscriberList = entry ? [...this.processRegistry.subscribers(entry)] : [{ client: ownerRef }];
|
|
667
|
+
for (const sub of subscriberList) {
|
|
668
|
+
const subAgent = this.agents.get(sub.client.agentId);
|
|
669
|
+
if (!subAgent)
|
|
670
|
+
continue;
|
|
671
|
+
subAgent.tgBot.sendText(sub.client.chatId, text, 'HTML')
|
|
672
|
+
.catch(err => this.logger.error({ err }, 'Failed to send API error notification'));
|
|
673
|
+
}
|
|
550
674
|
});
|
|
551
675
|
proc.on('hang', () => {
|
|
552
|
-
|
|
553
|
-
|
|
676
|
+
// Broadcast hang notification to all subscribers
|
|
677
|
+
const entry = getEntry();
|
|
678
|
+
const subscriberList = entry ? [...this.processRegistry.subscribers(entry)] : [{ client: ownerRef }];
|
|
679
|
+
for (const sub of subscriberList) {
|
|
680
|
+
const subAgent = this.agents.get(sub.client.agentId);
|
|
681
|
+
if (!subAgent)
|
|
682
|
+
continue;
|
|
683
|
+
this.stopTypingIndicator(subAgent, sub.client.userId, sub.client.chatId);
|
|
684
|
+
subAgent.tgBot.sendText(sub.client.chatId, '<blockquote>⏸ Session paused. Send a message to continue.</blockquote>', 'HTML')
|
|
685
|
+
.catch(err => this.logger.error({ err }, 'Failed to send hang notification'));
|
|
686
|
+
}
|
|
554
687
|
});
|
|
555
688
|
proc.on('takeover', () => {
|
|
556
|
-
this.stopTypingIndicator(agent, userId, chatId);
|
|
557
689
|
this.logger.warn({ agentId, userId }, 'Session takeover detected — keeping session for roaming');
|
|
690
|
+
// Notify and clean up all subscribers
|
|
691
|
+
const entry = getEntry();
|
|
692
|
+
if (entry) {
|
|
693
|
+
for (const sub of this.processRegistry.subscribers(entry)) {
|
|
694
|
+
const subAgent = this.agents.get(sub.client.agentId);
|
|
695
|
+
if (!subAgent)
|
|
696
|
+
continue;
|
|
697
|
+
this.stopTypingIndicator(subAgent, sub.client.userId, sub.client.chatId);
|
|
698
|
+
subAgent.processes.delete(sub.client.userId);
|
|
699
|
+
}
|
|
700
|
+
// Remove from registry without destroying (already handling exit)
|
|
701
|
+
this.processRegistry.remove(entry.repo, entry.sessionId);
|
|
702
|
+
}
|
|
558
703
|
// Don't clear session — allow roaming between clients.
|
|
559
|
-
// Just kill the current process; next message will --resume the same session.
|
|
560
704
|
proc.destroy();
|
|
561
|
-
agent.processes.delete(userId);
|
|
562
705
|
});
|
|
563
706
|
proc.on('exit', () => {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
707
|
+
// Clean up all subscribers
|
|
708
|
+
const entry = getEntry();
|
|
709
|
+
if (entry) {
|
|
710
|
+
for (const sub of this.processRegistry.subscribers(entry)) {
|
|
711
|
+
const subAgent = this.agents.get(sub.client.agentId);
|
|
712
|
+
if (!subAgent)
|
|
713
|
+
continue;
|
|
714
|
+
this.stopTypingIndicator(subAgent, sub.client.userId, sub.client.chatId);
|
|
715
|
+
const accKey = `${sub.client.userId}:${sub.client.chatId}`;
|
|
716
|
+
const acc = subAgent.accumulators.get(accKey);
|
|
717
|
+
if (acc) {
|
|
718
|
+
acc.finalize();
|
|
719
|
+
subAgent.accumulators.delete(accKey);
|
|
720
|
+
}
|
|
721
|
+
const exitTracker = subAgent.subAgentTrackers.get(accKey);
|
|
722
|
+
if (exitTracker) {
|
|
723
|
+
exitTracker.stopMailboxWatch();
|
|
724
|
+
}
|
|
725
|
+
subAgent.subAgentTrackers.delete(accKey);
|
|
726
|
+
}
|
|
727
|
+
// Remove from registry (process already exited)
|
|
728
|
+
this.processRegistry.remove(entry.repo, entry.sessionId);
|
|
571
729
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
730
|
+
else {
|
|
731
|
+
// Fallback: clean up owner only
|
|
732
|
+
this.stopTypingIndicator(agent, userId, chatId);
|
|
733
|
+
const accKey = `${userId}:${chatId}`;
|
|
734
|
+
const acc = agent.accumulators.get(accKey);
|
|
735
|
+
if (acc) {
|
|
736
|
+
acc.finalize();
|
|
737
|
+
agent.accumulators.delete(accKey);
|
|
738
|
+
}
|
|
739
|
+
const exitTracker = agent.subAgentTrackers.get(accKey);
|
|
740
|
+
if (exitTracker) {
|
|
741
|
+
exitTracker.stopMailboxWatch();
|
|
742
|
+
}
|
|
743
|
+
agent.subAgentTrackers.delete(accKey);
|
|
576
744
|
}
|
|
577
|
-
agent.subAgentTrackers.delete(accKey);
|
|
578
745
|
});
|
|
579
746
|
proc.on('error', (err) => {
|
|
580
|
-
|
|
581
|
-
|
|
747
|
+
// Broadcast error to all subscribers
|
|
748
|
+
const entry = getEntry();
|
|
749
|
+
const subscriberList = entry ? [...this.processRegistry.subscribers(entry)] : [{ client: ownerRef }];
|
|
750
|
+
for (const sub of subscriberList) {
|
|
751
|
+
const subAgent = this.agents.get(sub.client.agentId);
|
|
752
|
+
if (!subAgent)
|
|
753
|
+
continue;
|
|
754
|
+
this.stopTypingIndicator(subAgent, sub.client.userId, sub.client.chatId);
|
|
755
|
+
subAgent.tgBot.sendText(sub.client.chatId, `<blockquote>⚠️ ${escapeHtml(String(err.message))}</blockquote>`, 'HTML')
|
|
756
|
+
.catch(err2 => this.logger.error({ err: err2 }, 'Failed to send process error notification'));
|
|
757
|
+
}
|
|
582
758
|
});
|
|
583
759
|
return proc;
|
|
584
760
|
}
|
|
@@ -593,9 +769,14 @@ export class Bridge extends EventEmitter {
|
|
|
593
769
|
const sender = {
|
|
594
770
|
sendMessage: (cid, text, parseMode) => agent.tgBot.sendText(cid, text, parseMode),
|
|
595
771
|
editMessage: (cid, msgId, text, parseMode) => agent.tgBot.editText(cid, msgId, text, parseMode),
|
|
772
|
+
deleteMessage: (cid, msgId) => agent.tgBot.deleteMessage(cid, msgId),
|
|
596
773
|
sendPhoto: (cid, buffer, caption) => agent.tgBot.sendPhotoBuffer(cid, buffer, caption),
|
|
597
774
|
};
|
|
598
|
-
|
|
775
|
+
const onError = (err, context) => {
|
|
776
|
+
this.logger.error({ err, context, agentId, userId }, 'Stream accumulator error');
|
|
777
|
+
agent.tgBot.sendText(chatId, `<blockquote>⚠️ ${escapeHtml(context)}</blockquote>`, 'HTML').catch(() => { });
|
|
778
|
+
};
|
|
779
|
+
acc = new StreamAccumulator({ chatId, sender, logger: this.logger, onError });
|
|
599
780
|
agent.accumulators.set(accKey, acc);
|
|
600
781
|
}
|
|
601
782
|
// Sub-agent tracker — create lazily alongside the accumulator
|
|
@@ -612,12 +793,9 @@ export class Bridge extends EventEmitter {
|
|
|
612
793
|
});
|
|
613
794
|
agent.subAgentTrackers.set(accKey, tracker);
|
|
614
795
|
}
|
|
615
|
-
// On message_start:
|
|
796
|
+
// On message_start: new CC turn — full reset for text accumulator.
|
|
797
|
+
// Tool indicator messages are independent and persist across turns.
|
|
616
798
|
if (event.type === 'message_start') {
|
|
617
|
-
// Full reset: each assistant turn gets its own TG message.
|
|
618
|
-
// This prevents the "rewriting the wrong bubble" problem where a multi-turn
|
|
619
|
-
// session (tool calls → responses → more tool calls) kept overwriting a single
|
|
620
|
-
// message positioned between user messages, losing all context.
|
|
621
799
|
acc.reset();
|
|
622
800
|
// Only reset tracker if no agents still dispatched
|
|
623
801
|
if (!tracker.hasDispatchedAgents) {
|
|
@@ -650,14 +828,13 @@ export class Bridge extends EventEmitter {
|
|
|
650
828
|
}
|
|
651
829
|
// Update session store with cost
|
|
652
830
|
if (event.total_cost_usd) {
|
|
653
|
-
this.sessionStore.updateSessionActivity(agentId, userId, event.total_cost_usd);
|
|
654
831
|
}
|
|
655
832
|
// Update JSONL tracking after our own CC turn completes
|
|
656
833
|
// This prevents false-positive staleness on our own writes
|
|
657
|
-
this.updateJsonlTracking(agentId, userId);
|
|
658
834
|
// Handle errors
|
|
659
835
|
if (event.is_error && event.result) {
|
|
660
|
-
agent.tgBot.sendText(chatId, `<blockquote>⚠️ ${escapeHtml(String(event.result))}</blockquote>`, 'HTML')
|
|
836
|
+
agent.tgBot.sendText(chatId, `<blockquote>⚠️ ${escapeHtml(String(event.result))}</blockquote>`, 'HTML')
|
|
837
|
+
.catch(err => this.logger.error({ err }, 'Failed to send result error notification'));
|
|
661
838
|
}
|
|
662
839
|
// If background sub-agents are still running, mailbox watcher handles them.
|
|
663
840
|
// Ensure mailbox watch is started if we have a team name and dispatched agents.
|
|
@@ -700,7 +877,33 @@ export class Bridge extends EventEmitter {
|
|
|
700
877
|
this.logger.debug({ agentId, command: cmd.command, args: cmd.args }, 'Slash command');
|
|
701
878
|
switch (cmd.command) {
|
|
702
879
|
case 'start': {
|
|
703
|
-
|
|
880
|
+
const userConf = resolveUserConfig(agent.config, cmd.userId);
|
|
881
|
+
const userState = this.sessionStore.getUser(agentId, cmd.userId);
|
|
882
|
+
const repo = userState.repo || userConf.repo;
|
|
883
|
+
const session = userState.currentSessionId;
|
|
884
|
+
// Model: running process > session JSONL > user default
|
|
885
|
+
let model = userConf.model;
|
|
886
|
+
if (session && repo) {
|
|
887
|
+
const registryEntry = this.processRegistry.get(repo, session);
|
|
888
|
+
if (registryEntry?.model) {
|
|
889
|
+
model = registryEntry.model;
|
|
890
|
+
}
|
|
891
|
+
else {
|
|
892
|
+
const sessions = discoverCCSessions(repo, 20);
|
|
893
|
+
const sessionInfo = sessions.find(s => s.id === session);
|
|
894
|
+
if (sessionInfo?.model)
|
|
895
|
+
model = sessionInfo.model;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
const lines = ['👋 <b>TGCC</b> — Telegram ↔ Claude Code bridge'];
|
|
899
|
+
if (repo)
|
|
900
|
+
lines.push(`📂 <code>${escapeHtml(shortenRepoPath(repo))}</code>`);
|
|
901
|
+
if (model)
|
|
902
|
+
lines.push(`🤖 ${escapeHtml(model)}`);
|
|
903
|
+
if (session)
|
|
904
|
+
lines.push(`📎 Session: <code>${escapeHtml(session.slice(0, 8))}</code>`);
|
|
905
|
+
lines.push('', 'Send a message to start, or use /help for commands.');
|
|
906
|
+
await agent.tgBot.sendText(cmd.chatId, lines.join('\n'), 'HTML');
|
|
704
907
|
// Re-register commands with BotFather to ensure menu is up to date
|
|
705
908
|
try {
|
|
706
909
|
const { COMMANDS } = await import('./telegram.js');
|
|
@@ -728,7 +931,7 @@ export class Bridge extends EventEmitter {
|
|
|
728
931
|
`<b>Agent:</b> ${escapeHtml(agentId)}`,
|
|
729
932
|
`<b>Process:</b> ${(proc?.state ?? 'idle').toUpperCase()} (uptime: ${uptime})`,
|
|
730
933
|
`<b>Session:</b> <code>${escapeHtml(proc?.sessionId?.slice(0, 8) ?? 'none')}</code>`,
|
|
731
|
-
`<b>Model:</b> ${escapeHtml(
|
|
934
|
+
`<b>Model:</b> ${escapeHtml(resolveUserConfig(agent.config, cmd.userId).model)}`,
|
|
732
935
|
`<b>Repo:</b> ${escapeHtml(userState.repo || resolveUserConfig(agent.config, cmd.userId).repo)}`,
|
|
733
936
|
`<b>Cost:</b> $${(proc?.totalCostUsd ?? 0).toFixed(4)}`,
|
|
734
937
|
].join('\n');
|
|
@@ -741,78 +944,158 @@ export class Bridge extends EventEmitter {
|
|
|
741
944
|
break;
|
|
742
945
|
}
|
|
743
946
|
case 'new': {
|
|
744
|
-
|
|
745
|
-
if (proc) {
|
|
746
|
-
proc.destroy();
|
|
747
|
-
agent.processes.delete(cmd.userId);
|
|
748
|
-
}
|
|
947
|
+
this.disconnectClient(agentId, cmd.userId, cmd.chatId);
|
|
749
948
|
this.sessionStore.clearSession(agentId, cmd.userId);
|
|
750
|
-
|
|
949
|
+
const newConf = resolveUserConfig(agent.config, cmd.userId);
|
|
950
|
+
const newState = this.sessionStore.getUser(agentId, cmd.userId);
|
|
951
|
+
const newRepo = newState.repo || newConf.repo;
|
|
952
|
+
const newModel = newState.model || newConf.model;
|
|
953
|
+
const newLines = ['Session cleared. Next message starts fresh.'];
|
|
954
|
+
if (newRepo)
|
|
955
|
+
newLines.push(`📂 <code>${escapeHtml(shortenRepoPath(newRepo))}</code>`);
|
|
956
|
+
if (newModel)
|
|
957
|
+
newLines.push(`🤖 ${escapeHtml(newModel)}`);
|
|
958
|
+
await agent.tgBot.sendText(cmd.chatId, `<blockquote>${newLines.join('\n')}</blockquote>`, 'HTML');
|
|
751
959
|
break;
|
|
752
960
|
}
|
|
753
961
|
case 'continue': {
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
962
|
+
this.disconnectClient(agentId, cmd.userId, cmd.chatId);
|
|
963
|
+
const contConf = resolveUserConfig(agent.config, cmd.userId);
|
|
964
|
+
const contState = this.sessionStore.getUser(agentId, cmd.userId);
|
|
965
|
+
const contRepo = contState.repo || contConf.repo;
|
|
966
|
+
let contSession = contState.currentSessionId;
|
|
967
|
+
// If no current session, auto-pick the most recent one
|
|
968
|
+
if (!contSession && contRepo) {
|
|
969
|
+
const recent = discoverCCSessions(contRepo, 1);
|
|
970
|
+
if (recent.length > 0) {
|
|
971
|
+
contSession = recent[0].id;
|
|
972
|
+
this.sessionStore.setCurrentSession(agentId, cmd.userId, contSession);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
// Model priority: running process registry > session JSONL > user default
|
|
976
|
+
let contModel = contConf.model;
|
|
977
|
+
if (contSession && contRepo) {
|
|
978
|
+
// Check if a process is already running for this session
|
|
979
|
+
const registryEntry = this.processRegistry.get(contRepo, contSession);
|
|
980
|
+
if (registryEntry?.model) {
|
|
981
|
+
contModel = registryEntry.model;
|
|
982
|
+
}
|
|
983
|
+
else {
|
|
984
|
+
const sessions = discoverCCSessions(contRepo, 20);
|
|
985
|
+
const sessionInfo = sessions.find(s => s.id === contSession);
|
|
986
|
+
if (sessionInfo?.model)
|
|
987
|
+
contModel = sessionInfo.model;
|
|
988
|
+
}
|
|
758
989
|
}
|
|
759
|
-
|
|
990
|
+
const contLines = ['Process respawned. Session kept.'];
|
|
991
|
+
if (contRepo)
|
|
992
|
+
contLines.push(`📂 <code>${escapeHtml(shortenRepoPath(contRepo))}</code>`);
|
|
993
|
+
if (contModel)
|
|
994
|
+
contLines.push(`🤖 ${escapeHtml(contModel)}`);
|
|
995
|
+
if (contSession)
|
|
996
|
+
contLines.push(`📎 <code>${escapeHtml(contSession.slice(0, 8))}</code>`);
|
|
997
|
+
await agent.tgBot.sendText(cmd.chatId, `<blockquote>${contLines.join('\n')}</blockquote>`, 'HTML');
|
|
760
998
|
break;
|
|
761
999
|
}
|
|
762
1000
|
case 'sessions': {
|
|
763
|
-
const
|
|
764
|
-
|
|
765
|
-
|
|
1001
|
+
const userConf = resolveUserConfig(agent.config, cmd.userId);
|
|
1002
|
+
const userState = this.sessionStore.getUser(agentId, cmd.userId);
|
|
1003
|
+
const repo = userState.repo || userConf.repo;
|
|
1004
|
+
const currentSessionId = userState.currentSessionId;
|
|
1005
|
+
// Discover sessions from CC's session directory
|
|
1006
|
+
const discovered = repo ? discoverCCSessions(repo, 5) : [];
|
|
1007
|
+
const merged = discovered.map(d => {
|
|
1008
|
+
const ctx = d.contextPct !== null ? ` · ${d.contextPct}% ctx` : '';
|
|
1009
|
+
const modelTag = d.model ? ` · ${shortModel(d.model)}` : '';
|
|
1010
|
+
return {
|
|
1011
|
+
id: d.id,
|
|
1012
|
+
title: d.title,
|
|
1013
|
+
age: formatAge(d.mtime),
|
|
1014
|
+
detail: `~${d.lineCount} entries${ctx}${modelTag}`,
|
|
1015
|
+
isCurrent: d.id === currentSessionId,
|
|
1016
|
+
};
|
|
1017
|
+
});
|
|
1018
|
+
if (merged.length === 0) {
|
|
1019
|
+
await agent.tgBot.sendText(cmd.chatId, '<blockquote>No sessions found.</blockquote>', 'HTML');
|
|
766
1020
|
break;
|
|
767
1021
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
const isCurrent = s.id === currentSessionId;
|
|
776
|
-
const current = isCurrent ? ' ✓' : '';
|
|
777
|
-
lines.push(`<b>${i + 1}.</b> ${displayTitle}${current}\n ${s.messageCount} msgs · $${s.totalCostUsd.toFixed(2)} · ${age}`);
|
|
778
|
-
// Button: full title (TG allows up to ~40 chars visible), no ellipsis
|
|
779
|
-
if (!isCurrent) {
|
|
780
|
-
const btnLabel = rawTitle.length > 35 ? rawTitle.slice(0, 35) : rawTitle;
|
|
781
|
-
keyboard.text(`${i + 1}. ${btnLabel}`, `resume:${s.id}`).row();
|
|
782
|
-
}
|
|
1022
|
+
// Sort: current session first, then by recency
|
|
1023
|
+
merged.sort((a, b) => {
|
|
1024
|
+
if (a.isCurrent && !b.isCurrent)
|
|
1025
|
+
return -1;
|
|
1026
|
+
if (!a.isCurrent && b.isCurrent)
|
|
1027
|
+
return 1;
|
|
1028
|
+
return 0; // already sorted by mtime from discovery
|
|
783
1029
|
});
|
|
784
|
-
|
|
1030
|
+
// One message per session, each with its own resume button
|
|
1031
|
+
for (const s of merged.slice(0, 5)) {
|
|
1032
|
+
const displayTitle = escapeHtml(s.title);
|
|
1033
|
+
const kb = new InlineKeyboard();
|
|
1034
|
+
if (s.isCurrent) {
|
|
1035
|
+
const repoLine = repo ? `\n📂 <code>${escapeHtml(shortenRepoPath(repo))}</code>` : '';
|
|
1036
|
+
const sessModel = userConf.model;
|
|
1037
|
+
const modelLine = sessModel ? `\n🤖 ${escapeHtml(sessModel)}` : '';
|
|
1038
|
+
const sessionLine = `\n📎 <code>${escapeHtml(s.id.slice(0, 8))}</code>`;
|
|
1039
|
+
const text = `<blockquote><b>Current session:</b>\n${displayTitle}\n${s.detail} · ${s.age}${repoLine}${modelLine}${sessionLine}</blockquote>`;
|
|
1040
|
+
await agent.tgBot.sendText(cmd.chatId, text, 'HTML');
|
|
1041
|
+
}
|
|
1042
|
+
else {
|
|
1043
|
+
const text = `${displayTitle}\n<code>${escapeHtml(s.id.slice(0, 8))}</code> · ${s.detail} · ${s.age}`;
|
|
1044
|
+
const btnTitle = s.title.length > 30 ? s.title.slice(0, 30) + '…' : s.title;
|
|
1045
|
+
kb.text(`▶ ${btnTitle}`, `resume:${s.id}`);
|
|
1046
|
+
await agent.tgBot.sendTextWithKeyboard(cmd.chatId, text, kb, 'HTML');
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
785
1049
|
break;
|
|
786
1050
|
}
|
|
787
1051
|
case 'resume': {
|
|
788
1052
|
if (!cmd.args) {
|
|
789
|
-
await agent.tgBot.sendText(cmd.chatId, 'Usage: /resume
|
|
1053
|
+
await agent.tgBot.sendText(cmd.chatId, '<blockquote>Usage: /resume <session-id></blockquote>', 'HTML');
|
|
790
1054
|
break;
|
|
791
1055
|
}
|
|
792
|
-
|
|
793
|
-
if (proc) {
|
|
794
|
-
proc.destroy();
|
|
795
|
-
agent.processes.delete(cmd.userId);
|
|
796
|
-
}
|
|
1056
|
+
this.disconnectClient(agentId, cmd.userId, cmd.chatId);
|
|
797
1057
|
this.sessionStore.setCurrentSession(agentId, cmd.userId, cmd.args.trim());
|
|
798
1058
|
await agent.tgBot.sendText(cmd.chatId, `Will resume session <code>${escapeHtml(cmd.args.trim().slice(0, 8))}</code> on next message.`, 'HTML');
|
|
799
1059
|
break;
|
|
800
1060
|
}
|
|
801
1061
|
case 'session': {
|
|
802
1062
|
const userState = this.sessionStore.getUser(agentId, cmd.userId);
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
await agent.tgBot.sendText(cmd.chatId, 'No active session.');
|
|
1063
|
+
if (!userState.currentSessionId) {
|
|
1064
|
+
await agent.tgBot.sendText(cmd.chatId, '<blockquote>No active session.</blockquote>', 'HTML');
|
|
806
1065
|
break;
|
|
807
1066
|
}
|
|
808
|
-
|
|
1067
|
+
const sessRepo = userState.repo || resolveUserConfig(agent.config, cmd.userId).repo;
|
|
1068
|
+
const discovered = sessRepo ? discoverCCSessions(sessRepo, 20) : [];
|
|
1069
|
+
const info = discovered.find(d => d.id === userState.currentSessionId);
|
|
1070
|
+
if (!info) {
|
|
1071
|
+
await agent.tgBot.sendText(cmd.chatId, `<b>Session:</b> <code>${escapeHtml(userState.currentSessionId.slice(0, 8))}</code>`, 'HTML');
|
|
1072
|
+
break;
|
|
1073
|
+
}
|
|
1074
|
+
const ctxLine = info.contextPct !== null ? `\n<b>Context:</b> ${info.contextPct}%` : '';
|
|
1075
|
+
const modelLine = info.model ? `\n<b>Model:</b> ${escapeHtml(info.model)}` : '';
|
|
1076
|
+
await agent.tgBot.sendText(cmd.chatId, `<b>Session:</b> <code>${escapeHtml(info.id.slice(0, 8))}</code>\n<b>Title:</b> ${escapeHtml(info.title)}${modelLine}${ctxLine}\n<b>Age:</b> ${formatAge(info.mtime)}`, 'HTML');
|
|
809
1077
|
break;
|
|
810
1078
|
}
|
|
811
1079
|
case 'model': {
|
|
812
1080
|
const MODEL_OPTIONS = ['opus', 'sonnet', 'haiku'];
|
|
813
1081
|
if (!cmd.args) {
|
|
814
|
-
|
|
815
|
-
|
|
1082
|
+
// Show model from: running process > JSONL > agent config
|
|
1083
|
+
const uState = this.sessionStore.getUser(agentId, cmd.userId);
|
|
1084
|
+
const uConf = resolveUserConfig(agent.config, cmd.userId);
|
|
1085
|
+
const uRepo = uState.repo || uConf.repo;
|
|
1086
|
+
const uSession = uState.currentSessionId;
|
|
1087
|
+
let current = uConf.model || 'default';
|
|
1088
|
+
if (uSession && uRepo) {
|
|
1089
|
+
const re = this.processRegistry.get(uRepo, uSession);
|
|
1090
|
+
if (re?.model)
|
|
1091
|
+
current = re.model;
|
|
1092
|
+
else {
|
|
1093
|
+
const ds = discoverCCSessions(uRepo, 20);
|
|
1094
|
+
const si = ds.find(s => s.id === uSession);
|
|
1095
|
+
if (si?.model)
|
|
1096
|
+
current = si.model;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
816
1099
|
const keyboard = new InlineKeyboard();
|
|
817
1100
|
for (const m of MODEL_OPTIONS) {
|
|
818
1101
|
const isCurrent = current.includes(m);
|
|
@@ -822,13 +1105,12 @@ export class Bridge extends EventEmitter {
|
|
|
822
1105
|
await agent.tgBot.sendTextWithKeyboard(cmd.chatId, `<b>Current model:</b> <code>${escapeHtml(current)}</code>`, keyboard, 'HTML');
|
|
823
1106
|
break;
|
|
824
1107
|
}
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
}
|
|
831
|
-
await agent.tgBot.sendText(cmd.chatId, `Model set to <code>${escapeHtml(cmd.args.trim())}</code>. Process respawned.`, 'HTML');
|
|
1108
|
+
const newModel = cmd.args.trim();
|
|
1109
|
+
// Store as session-level override (not user default)
|
|
1110
|
+
const curSession = this.sessionStore.getUser(agentId, cmd.userId).currentSessionId;
|
|
1111
|
+
this.sessionModelOverrides.set(`${agentId}:${cmd.userId}`, newModel);
|
|
1112
|
+
this.disconnectClient(agentId, cmd.userId, cmd.chatId);
|
|
1113
|
+
await agent.tgBot.sendText(cmd.chatId, `<blockquote>Model set to <code>${escapeHtml(newModel)}</code>. Process respawned.</blockquote>`, 'HTML');
|
|
832
1114
|
break;
|
|
833
1115
|
}
|
|
834
1116
|
case 'repo': {
|
|
@@ -840,11 +1122,11 @@ export class Bridge extends EventEmitter {
|
|
|
840
1122
|
const repoName = repoArgs[1];
|
|
841
1123
|
const repoAddPath = repoArgs[2];
|
|
842
1124
|
if (!repoName || !repoAddPath) {
|
|
843
|
-
await agent.tgBot.sendText(cmd.chatId, 'Usage: /repo add
|
|
1125
|
+
await agent.tgBot.sendText(cmd.chatId, '<blockquote>Usage: /repo add <name> <path></blockquote>', 'HTML');
|
|
844
1126
|
break;
|
|
845
1127
|
}
|
|
846
1128
|
if (!isValidRepoName(repoName)) {
|
|
847
|
-
await agent.tgBot.sendText(cmd.chatId, 'Invalid repo name. Use alphanumeric + hyphens only
|
|
1129
|
+
await agent.tgBot.sendText(cmd.chatId, '<blockquote>Invalid repo name. Use alphanumeric + hyphens only.</blockquote>', 'HTML');
|
|
848
1130
|
break;
|
|
849
1131
|
}
|
|
850
1132
|
if (!existsSync(repoAddPath)) {
|
|
@@ -867,7 +1149,7 @@ export class Bridge extends EventEmitter {
|
|
|
867
1149
|
// /repo remove <name>
|
|
868
1150
|
const repoName = repoArgs[1];
|
|
869
1151
|
if (!repoName) {
|
|
870
|
-
await agent.tgBot.sendText(cmd.chatId, 'Usage: /repo remove
|
|
1152
|
+
await agent.tgBot.sendText(cmd.chatId, '<blockquote>Usage: /repo remove <name></blockquote>', 'HTML');
|
|
871
1153
|
break;
|
|
872
1154
|
}
|
|
873
1155
|
if (!this.config.repos[repoName]) {
|
|
@@ -893,7 +1175,7 @@ export class Bridge extends EventEmitter {
|
|
|
893
1175
|
// /repo assign <name> — assign to THIS agent
|
|
894
1176
|
const repoName = repoArgs[1];
|
|
895
1177
|
if (!repoName) {
|
|
896
|
-
await agent.tgBot.sendText(cmd.chatId, 'Usage: /repo assign
|
|
1178
|
+
await agent.tgBot.sendText(cmd.chatId, '<blockquote>Usage: /repo assign <name></blockquote>', 'HTML');
|
|
897
1179
|
break;
|
|
898
1180
|
}
|
|
899
1181
|
if (!this.config.repos[repoName]) {
|
|
@@ -971,42 +1253,27 @@ export class Bridge extends EventEmitter {
|
|
|
971
1253
|
break;
|
|
972
1254
|
}
|
|
973
1255
|
// Kill current process (different CWD needs new process)
|
|
974
|
-
|
|
975
|
-
if (proc) {
|
|
976
|
-
proc.destroy();
|
|
977
|
-
agent.processes.delete(cmd.userId);
|
|
978
|
-
}
|
|
1256
|
+
this.disconnectClient(agentId, cmd.userId, cmd.chatId);
|
|
979
1257
|
this.sessionStore.setRepo(agentId, cmd.userId, repoPath);
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
if (Date.now() - lastActive > staleMs || !userState.currentSessionId) {
|
|
984
|
-
this.sessionStore.clearSession(agentId, cmd.userId);
|
|
985
|
-
await agent.tgBot.sendText(cmd.chatId, `Repo set to <code>${escapeHtml(repoPath)}</code>. Session cleared (stale).`, 'HTML');
|
|
986
|
-
}
|
|
987
|
-
else {
|
|
988
|
-
await agent.tgBot.sendText(cmd.chatId, `Repo set to <code>${escapeHtml(repoPath)}</code>. Session kept (active <24h).`, 'HTML');
|
|
989
|
-
}
|
|
1258
|
+
// Always clear session when repo changes — sessions are project-specific
|
|
1259
|
+
this.sessionStore.clearSession(agentId, cmd.userId);
|
|
1260
|
+
await agent.tgBot.sendText(cmd.chatId, `<blockquote>Repo set to <code>${escapeHtml(shortenRepoPath(repoPath))}</code>. Session cleared.</blockquote>`, 'HTML');
|
|
990
1261
|
break;
|
|
991
1262
|
}
|
|
992
1263
|
case 'cancel': {
|
|
993
1264
|
const proc = agent.processes.get(cmd.userId);
|
|
994
1265
|
if (proc && proc.state === 'active') {
|
|
995
1266
|
proc.cancel();
|
|
996
|
-
await agent.tgBot.sendText(cmd.chatId, 'Cancelled
|
|
1267
|
+
await agent.tgBot.sendText(cmd.chatId, '<blockquote>Cancelled.</blockquote>', 'HTML');
|
|
997
1268
|
}
|
|
998
1269
|
else {
|
|
999
|
-
await agent.tgBot.sendText(cmd.chatId, 'No active turn to cancel
|
|
1270
|
+
await agent.tgBot.sendText(cmd.chatId, '<blockquote>No active turn to cancel.</blockquote>', 'HTML');
|
|
1000
1271
|
}
|
|
1001
1272
|
break;
|
|
1002
1273
|
}
|
|
1003
1274
|
case 'catchup': {
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
const lastActivity = new Date(userState.lastActivity || 0);
|
|
1007
|
-
const missed = findMissedSessions(repo, userState.knownSessionIds, lastActivity);
|
|
1008
|
-
const message = formatCatchupMessage(repo, missed);
|
|
1009
|
-
await agent.tgBot.sendText(cmd.chatId, message, 'HTML');
|
|
1275
|
+
// Catchup now just shows recent sessions via /sessions
|
|
1276
|
+
await agent.tgBot.sendText(cmd.chatId, '<blockquote>Use /sessions to see recent sessions.</blockquote>', 'HTML');
|
|
1010
1277
|
break;
|
|
1011
1278
|
}
|
|
1012
1279
|
case 'permissions': {
|
|
@@ -1017,16 +1284,12 @@ export class Bridge extends EventEmitter {
|
|
|
1017
1284
|
if (cmd.args) {
|
|
1018
1285
|
const mode = cmd.args.trim();
|
|
1019
1286
|
if (!validModes.includes(mode)) {
|
|
1020
|
-
await agent.tgBot.sendText(cmd.chatId,
|
|
1287
|
+
await agent.tgBot.sendText(cmd.chatId, `<blockquote>Invalid mode. Valid: ${validModes.join(', ')}</blockquote>`, 'HTML');
|
|
1021
1288
|
break;
|
|
1022
1289
|
}
|
|
1023
1290
|
this.sessionStore.setPermissionMode(agentId, cmd.userId, mode);
|
|
1024
1291
|
// Kill current process so new mode takes effect on next spawn
|
|
1025
|
-
|
|
1026
|
-
if (proc) {
|
|
1027
|
-
proc.destroy();
|
|
1028
|
-
agent.processes.delete(cmd.userId);
|
|
1029
|
-
}
|
|
1292
|
+
this.disconnectClient(agentId, cmd.userId, cmd.chatId);
|
|
1030
1293
|
await agent.tgBot.sendText(cmd.chatId, `Permission mode set to <code>${escapeHtml(mode)}</code>. Takes effect on next message.`, 'HTML');
|
|
1031
1294
|
break;
|
|
1032
1295
|
}
|
|
@@ -1048,11 +1311,7 @@ export class Bridge extends EventEmitter {
|
|
|
1048
1311
|
switch (query.action) {
|
|
1049
1312
|
case 'resume': {
|
|
1050
1313
|
const sessionId = query.data;
|
|
1051
|
-
|
|
1052
|
-
if (proc) {
|
|
1053
|
-
proc.destroy();
|
|
1054
|
-
agent.processes.delete(query.userId);
|
|
1055
|
-
}
|
|
1314
|
+
this.disconnectClient(agentId, query.userId, query.chatId);
|
|
1056
1315
|
this.sessionStore.setCurrentSession(agentId, query.userId, sessionId);
|
|
1057
1316
|
await agent.tgBot.answerCallbackQuery(query.callbackQueryId, 'Session set');
|
|
1058
1317
|
await agent.tgBot.sendText(query.chatId, `Will resume session <code>${escapeHtml(sessionId.slice(0, 8))}</code> on next message.`, 'HTML');
|
|
@@ -1060,14 +1319,13 @@ export class Bridge extends EventEmitter {
|
|
|
1060
1319
|
}
|
|
1061
1320
|
case 'delete': {
|
|
1062
1321
|
const sessionId = query.data;
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
}
|
|
1068
|
-
else {
|
|
1069
|
-
await agent.tgBot.answerCallbackQuery(query.callbackQueryId, 'Session not found');
|
|
1322
|
+
// Clear current session if it matches
|
|
1323
|
+
const uState = this.sessionStore.getUser(agentId, query.userId);
|
|
1324
|
+
if (uState.currentSessionId === sessionId) {
|
|
1325
|
+
this.sessionStore.setCurrentSession(agentId, query.userId, '');
|
|
1070
1326
|
}
|
|
1327
|
+
await agent.tgBot.answerCallbackQuery(query.callbackQueryId, 'Session cleared');
|
|
1328
|
+
await agent.tgBot.sendText(query.chatId, `Session <code>${escapeHtml(sessionId.slice(0, 8))}</code> cleared.`, 'HTML');
|
|
1071
1329
|
break;
|
|
1072
1330
|
}
|
|
1073
1331
|
case 'repo': {
|
|
@@ -1078,23 +1336,12 @@ export class Bridge extends EventEmitter {
|
|
|
1078
1336
|
break;
|
|
1079
1337
|
}
|
|
1080
1338
|
// Kill current process (different CWD needs new process)
|
|
1081
|
-
|
|
1082
|
-
if (proc) {
|
|
1083
|
-
proc.destroy();
|
|
1084
|
-
agent.processes.delete(query.userId);
|
|
1085
|
-
}
|
|
1339
|
+
this.disconnectClient(agentId, query.userId, query.chatId);
|
|
1086
1340
|
this.sessionStore.setRepo(agentId, query.userId, repoPath);
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
const staleMs2 = 24 * 60 * 60 * 1000;
|
|
1341
|
+
// Always clear session when repo changes — sessions are project-specific
|
|
1342
|
+
this.sessionStore.clearSession(agentId, query.userId);
|
|
1090
1343
|
await agent.tgBot.answerCallbackQuery(query.callbackQueryId, `Repo: ${repoName}`);
|
|
1091
|
-
|
|
1092
|
-
this.sessionStore.clearSession(agentId, query.userId);
|
|
1093
|
-
await agent.tgBot.sendText(query.chatId, `Repo set to <code>${escapeHtml(repoPath)}</code>. Session cleared (stale).`, 'HTML');
|
|
1094
|
-
}
|
|
1095
|
-
else {
|
|
1096
|
-
await agent.tgBot.sendText(query.chatId, `Repo set to <code>${escapeHtml(repoPath)}</code>. Session kept (active <24h).`, 'HTML');
|
|
1097
|
-
}
|
|
1344
|
+
await agent.tgBot.sendText(query.chatId, `<blockquote>Repo set to <code>${escapeHtml(shortenRepoPath(repoPath))}</code>. Session cleared.</blockquote>`, 'HTML');
|
|
1098
1345
|
break;
|
|
1099
1346
|
}
|
|
1100
1347
|
case 'repo_add': {
|
|
@@ -1109,14 +1356,10 @@ export class Bridge extends EventEmitter {
|
|
|
1109
1356
|
await agent.tgBot.sendText(query.chatId, 'Send: <code>/model <model-name></code>', 'HTML');
|
|
1110
1357
|
break;
|
|
1111
1358
|
}
|
|
1112
|
-
this.
|
|
1113
|
-
|
|
1114
|
-
if (proc) {
|
|
1115
|
-
proc.destroy();
|
|
1116
|
-
agent.processes.delete(query.userId);
|
|
1117
|
-
}
|
|
1359
|
+
this.sessionModelOverrides.set(`${agentId}:${query.userId}`, model);
|
|
1360
|
+
this.disconnectClient(agentId, query.userId, query.chatId);
|
|
1118
1361
|
await agent.tgBot.answerCallbackQuery(query.callbackQueryId, `Model: ${model}`);
|
|
1119
|
-
await agent.tgBot.sendText(query.chatId,
|
|
1362
|
+
await agent.tgBot.sendText(query.chatId, `<blockquote>Model set to <code>${escapeHtml(model)}</code>. Process respawned.</blockquote>`, 'HTML');
|
|
1120
1363
|
break;
|
|
1121
1364
|
}
|
|
1122
1365
|
case 'permissions': {
|
|
@@ -1128,11 +1371,7 @@ export class Bridge extends EventEmitter {
|
|
|
1128
1371
|
}
|
|
1129
1372
|
this.sessionStore.setPermissionMode(agentId, query.userId, mode);
|
|
1130
1373
|
// Kill current process so new mode takes effect on next spawn
|
|
1131
|
-
|
|
1132
|
-
if (proc) {
|
|
1133
|
-
proc.destroy();
|
|
1134
|
-
agent.processes.delete(query.userId);
|
|
1135
|
-
}
|
|
1374
|
+
this.disconnectClient(agentId, query.userId, query.chatId);
|
|
1136
1375
|
await agent.tgBot.answerCallbackQuery(query.callbackQueryId, `Mode: ${mode}`);
|
|
1137
1376
|
await agent.tgBot.sendText(query.chatId, `Permission mode set to <code>${escapeHtml(mode)}</code>. Takes effect on next message.`, 'HTML');
|
|
1138
1377
|
break;
|
|
@@ -1255,15 +1494,18 @@ export class Bridge extends EventEmitter {
|
|
|
1255
1494
|
sessionId: proc?.sessionId ?? null,
|
|
1256
1495
|
repo: this.sessionStore.getUser(id, userId).repo || userConfig.repo,
|
|
1257
1496
|
});
|
|
1258
|
-
// List sessions for this agent
|
|
1497
|
+
// List sessions for this agent from CC's session directory
|
|
1259
1498
|
const userState = this.sessionStore.getUser(id, userId);
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1499
|
+
const sessRepo = userState.repo || userConfig.repo;
|
|
1500
|
+
if (sessRepo) {
|
|
1501
|
+
for (const d of discoverCCSessions(sessRepo, 5)) {
|
|
1502
|
+
sessions.push({
|
|
1503
|
+
id: d.id,
|
|
1504
|
+
agentId: id,
|
|
1505
|
+
messageCount: d.lineCount,
|
|
1506
|
+
totalCostUsd: 0,
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1267
1509
|
}
|
|
1268
1510
|
}
|
|
1269
1511
|
return { type: 'status', agents, sessions };
|
|
@@ -1302,6 +1544,7 @@ export class Bridge extends EventEmitter {
|
|
|
1302
1544
|
for (const agentId of [...this.agents.keys()]) {
|
|
1303
1545
|
await this.stopAgent(agentId);
|
|
1304
1546
|
}
|
|
1547
|
+
this.processRegistry.clear();
|
|
1305
1548
|
this.mcpServer.closeAll();
|
|
1306
1549
|
this.ctlServer.closeAll();
|
|
1307
1550
|
this.removeAllListeners();
|
|
@@ -1309,6 +1552,21 @@ export class Bridge extends EventEmitter {
|
|
|
1309
1552
|
}
|
|
1310
1553
|
}
|
|
1311
1554
|
// ── Helpers ──
|
|
1555
|
+
function shortModel(m) {
|
|
1556
|
+
if (m.includes('opus'))
|
|
1557
|
+
return 'opus';
|
|
1558
|
+
if (m.includes('sonnet'))
|
|
1559
|
+
return 'sonnet';
|
|
1560
|
+
if (m.includes('haiku'))
|
|
1561
|
+
return 'haiku';
|
|
1562
|
+
return m.length > 15 ? m.slice(0, 15) + '…' : m;
|
|
1563
|
+
}
|
|
1564
|
+
function shortenRepoPath(p) {
|
|
1565
|
+
return p
|
|
1566
|
+
.replace(/^\/home\/[^/]+\/Botverse\//, '')
|
|
1567
|
+
.replace(/^\/home\/[^/]+\/Projects\//, '')
|
|
1568
|
+
.replace(/^\/home\/[^/]+\//, '~/');
|
|
1569
|
+
}
|
|
1312
1570
|
function formatDuration(ms) {
|
|
1313
1571
|
const secs = Math.floor(ms / 1000);
|
|
1314
1572
|
if (secs < 60)
|