aegis-bridge 2.6.2 → 2.6.4
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/dashboard/dist/assets/{index-Bfabq3q-.js → index-G8fziBeQ.js} +25 -25
- package/dashboard/dist/index.html +1 -1
- package/dist/dashboard/assets/{index-Bfabq3q-.js → index-G8fziBeQ.js} +25 -25
- package/dist/dashboard/index.html +1 -1
- package/dist/hook-settings.js +4 -0
- package/dist/mcp-server.d.ts +72 -23
- package/dist/monitor.d.ts +10 -0
- package/dist/monitor.js +53 -31
- package/dist/session.d.ts +3 -0
- package/dist/session.js +16 -1
- package/dist/transcript.js +16 -6
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Aegis Dashboard</title>
|
|
7
7
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🛡️</text></svg>" />
|
|
8
|
-
<script type="module" crossorigin src="/dashboard/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/dashboard/assets/index-G8fziBeQ.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/dashboard/assets/index-9Hkkvm_I.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body class="bg-[#0a0a0f] text-gray-200 antialiased">
|
package/dist/hook-settings.js
CHANGED
|
@@ -135,6 +135,10 @@ export async function writeHookSettingsFile(baseUrl, sessionId, hookSecret, work
|
|
|
135
135
|
mergedHooks[event] = [...(existingHooks[event] ?? []), ...entries];
|
|
136
136
|
}
|
|
137
137
|
const combined = { ...merged, hooks: mergedHooks };
|
|
138
|
+
// Issue #931: Always inject MCP_CONNECTION_NONBLOCKING so CC does not block
|
|
139
|
+
// on MCP server connections when launched via Aegis orchestration.
|
|
140
|
+
(combined.env = (combined.env || {}));
|
|
141
|
+
(combined.env || {})["MCP_CONNECTION_NONBLOCKING"] = "true";
|
|
138
142
|
// Issue #648: Use unpredictable directory name and restrictive permissions
|
|
139
143
|
// to prevent symlink attacks and information disclosure in /tmp.
|
|
140
144
|
const suffix = randomBytes(4).toString('hex');
|
package/dist/mcp-server.d.ts
CHANGED
|
@@ -12,6 +12,51 @@
|
|
|
12
12
|
* Issue #48: https://github.com/OneStepAt4time/aegis/issues/48
|
|
13
13
|
*/
|
|
14
14
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
15
|
+
import { type SessionInfo } from './session.js';
|
|
16
|
+
import { type SessionMetrics, type SessionLatency, type SessionLatencySummary } from './metrics.js';
|
|
17
|
+
import { type PipelineState, type BatchResult } from './pipeline.js';
|
|
18
|
+
interface ServerHealthResponse {
|
|
19
|
+
status: string;
|
|
20
|
+
version: string;
|
|
21
|
+
uptime: number;
|
|
22
|
+
sessions: {
|
|
23
|
+
active: number;
|
|
24
|
+
total: number;
|
|
25
|
+
};
|
|
26
|
+
tmux: {
|
|
27
|
+
healthy: boolean;
|
|
28
|
+
[key: string]: unknown;
|
|
29
|
+
};
|
|
30
|
+
timestamp: string;
|
|
31
|
+
}
|
|
32
|
+
interface CreateSessionResponse {
|
|
33
|
+
id: string;
|
|
34
|
+
windowName: string;
|
|
35
|
+
workDir: string;
|
|
36
|
+
status: string;
|
|
37
|
+
promptDelivery?: {
|
|
38
|
+
delivered: boolean;
|
|
39
|
+
attempts: number;
|
|
40
|
+
};
|
|
41
|
+
reused?: boolean;
|
|
42
|
+
[key: string]: unknown;
|
|
43
|
+
}
|
|
44
|
+
interface SendMessageResponse {
|
|
45
|
+
ok: boolean;
|
|
46
|
+
delivered: boolean;
|
|
47
|
+
attempts: number;
|
|
48
|
+
}
|
|
49
|
+
interface OkResponse {
|
|
50
|
+
ok: boolean;
|
|
51
|
+
}
|
|
52
|
+
interface CapturePaneResponse {
|
|
53
|
+
pane: string;
|
|
54
|
+
}
|
|
55
|
+
interface SessionLatencyResponse {
|
|
56
|
+
sessionId: string;
|
|
57
|
+
realtime: SessionLatency | null;
|
|
58
|
+
aggregated: SessionLatencySummary | null;
|
|
59
|
+
}
|
|
15
60
|
export declare class AegisClient {
|
|
16
61
|
private baseUrl;
|
|
17
62
|
private authToken?;
|
|
@@ -21,40 +66,44 @@ export declare class AegisClient {
|
|
|
21
66
|
listSessions(filter?: {
|
|
22
67
|
status?: string;
|
|
23
68
|
workDir?: string;
|
|
24
|
-
}): Promise<
|
|
25
|
-
getSession(id: string): Promise<
|
|
26
|
-
getHealth(id: string): Promise<
|
|
27
|
-
getTranscript(id: string): Promise<
|
|
28
|
-
sendMessage(id: string, text: string): Promise<
|
|
69
|
+
}): Promise<SessionInfo[]>;
|
|
70
|
+
getSession(id: string): Promise<Record<string, unknown>>;
|
|
71
|
+
getHealth(id: string): Promise<Record<string, unknown>>;
|
|
72
|
+
getTranscript(id: string): Promise<Record<string, unknown>>;
|
|
73
|
+
sendMessage(id: string, text: string): Promise<SendMessageResponse>;
|
|
29
74
|
createSession(opts: {
|
|
30
75
|
workDir: string;
|
|
31
76
|
name?: string;
|
|
32
77
|
prompt?: string;
|
|
33
|
-
}): Promise<
|
|
34
|
-
killSession(id: string): Promise<
|
|
35
|
-
approvePermission(id: string): Promise<
|
|
36
|
-
rejectPermission(id: string): Promise<
|
|
37
|
-
getServerHealth(): Promise<
|
|
38
|
-
escapeSession(id: string): Promise<
|
|
39
|
-
interruptSession(id: string): Promise<
|
|
40
|
-
capturePane(id: string): Promise<
|
|
41
|
-
getSessionMetrics(id: string): Promise<
|
|
42
|
-
getSessionSummary(id: string): Promise<
|
|
43
|
-
sendBash(id: string, command: string): Promise<
|
|
44
|
-
sendCommand(id: string, command: string): Promise<
|
|
45
|
-
getSessionLatency(id: string): Promise<
|
|
78
|
+
}): Promise<CreateSessionResponse>;
|
|
79
|
+
killSession(id: string): Promise<OkResponse>;
|
|
80
|
+
approvePermission(id: string): Promise<OkResponse>;
|
|
81
|
+
rejectPermission(id: string): Promise<OkResponse>;
|
|
82
|
+
getServerHealth(): Promise<ServerHealthResponse>;
|
|
83
|
+
escapeSession(id: string): Promise<OkResponse>;
|
|
84
|
+
interruptSession(id: string): Promise<OkResponse>;
|
|
85
|
+
capturePane(id: string): Promise<CapturePaneResponse>;
|
|
86
|
+
getSessionMetrics(id: string): Promise<SessionMetrics>;
|
|
87
|
+
getSessionSummary(id: string): Promise<Record<string, unknown>>;
|
|
88
|
+
sendBash(id: string, command: string): Promise<OkResponse>;
|
|
89
|
+
sendCommand(id: string, command: string): Promise<OkResponse>;
|
|
90
|
+
getSessionLatency(id: string): Promise<SessionLatencyResponse>;
|
|
46
91
|
batchCreateSessions(sessions: Array<{
|
|
47
92
|
workDir: string;
|
|
48
93
|
name?: string;
|
|
49
94
|
prompt?: string;
|
|
50
|
-
}>): Promise<
|
|
51
|
-
listPipelines(): Promise<
|
|
95
|
+
}>): Promise<BatchResult>;
|
|
96
|
+
listPipelines(): Promise<PipelineState[]>;
|
|
52
97
|
createPipeline(config: {
|
|
53
98
|
name: string;
|
|
54
99
|
workDir: string;
|
|
55
|
-
steps:
|
|
56
|
-
|
|
57
|
-
|
|
100
|
+
steps: Array<{
|
|
101
|
+
name?: string;
|
|
102
|
+
prompt: string;
|
|
103
|
+
}>;
|
|
104
|
+
}): Promise<PipelineState>;
|
|
105
|
+
getSwarm(): Promise<Record<string, unknown>>;
|
|
58
106
|
}
|
|
59
107
|
export declare function createMcpServer(aegisPort: number, authToken?: string): McpServer;
|
|
60
108
|
export declare function startMcpServer(port: number, authToken?: string): Promise<void>;
|
|
109
|
+
export {};
|
package/dist/monitor.d.ts
CHANGED
|
@@ -31,6 +31,16 @@ export declare class SessionMonitor {
|
|
|
31
31
|
private lastStatus;
|
|
32
32
|
private lastBytesSeen;
|
|
33
33
|
private stallNotified;
|
|
34
|
+
/** Issue #663: O(1) stall notification check. */
|
|
35
|
+
private stallHas;
|
|
36
|
+
/** Issue #663: O(1) stall notification add. */
|
|
37
|
+
private stallAdd;
|
|
38
|
+
/** Issue #663: O(1) stall notification delete. */
|
|
39
|
+
private stallDelete;
|
|
40
|
+
/** Issue #663: Delete all stall notifications for a session. */
|
|
41
|
+
private stallDeleteAll;
|
|
42
|
+
/** Issue #663: Delete specific stall types for a session. */
|
|
43
|
+
private stallDeleteTypes;
|
|
34
44
|
private lastStallCheck;
|
|
35
45
|
private lastDeadCheck;
|
|
36
46
|
private idleNotified;
|
package/dist/monitor.js
CHANGED
|
@@ -34,7 +34,38 @@ export class SessionMonitor {
|
|
|
34
34
|
running = false;
|
|
35
35
|
lastStatus = new Map();
|
|
36
36
|
lastBytesSeen = new Map();
|
|
37
|
-
|
|
37
|
+
// Issue #663: Nested Map for O(1) per-session stall lookup (was Set with O(n) prefix scan)
|
|
38
|
+
stallNotified = new Map(); // sessionId → Set<stallType>
|
|
39
|
+
/** Issue #663: O(1) stall notification check. */
|
|
40
|
+
stallHas(sessionId, stallType) {
|
|
41
|
+
return this.stallNotified.get(sessionId)?.has(stallType) ?? false;
|
|
42
|
+
}
|
|
43
|
+
/** Issue #663: O(1) stall notification add. */
|
|
44
|
+
stallAdd(sessionId, stallType) {
|
|
45
|
+
const set = this.stallNotified.get(sessionId);
|
|
46
|
+
if (set) {
|
|
47
|
+
set.add(stallType);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
this.stallNotified.set(sessionId, new Set([stallType]));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Issue #663: O(1) stall notification delete. */
|
|
54
|
+
stallDelete(sessionId, stallType) {
|
|
55
|
+
this.stallNotified.get(sessionId)?.delete(stallType);
|
|
56
|
+
}
|
|
57
|
+
/** Issue #663: Delete all stall notifications for a session. */
|
|
58
|
+
stallDeleteAll(sessionId) {
|
|
59
|
+
this.stallNotified.delete(sessionId);
|
|
60
|
+
}
|
|
61
|
+
/** Issue #663: Delete specific stall types for a session. */
|
|
62
|
+
stallDeleteTypes(sessionId, types) {
|
|
63
|
+
const set = this.stallNotified.get(sessionId);
|
|
64
|
+
if (!set)
|
|
65
|
+
return;
|
|
66
|
+
for (const t of types)
|
|
67
|
+
set.delete(t);
|
|
68
|
+
}
|
|
38
69
|
lastStallCheck = 0;
|
|
39
70
|
lastDeadCheck = 0;
|
|
40
71
|
idleNotified = new Set(); // prevent idle spam
|
|
@@ -196,13 +227,13 @@ export class SessionMonitor {
|
|
|
196
227
|
}
|
|
197
228
|
if (currentBytes > prev.bytes) {
|
|
198
229
|
this.lastBytesSeen.set(session.id, { bytes: currentBytes, at: now });
|
|
199
|
-
this.
|
|
230
|
+
this.stallDelete(session.id, 'jsonl');
|
|
200
231
|
}
|
|
201
232
|
else {
|
|
202
233
|
const stallDuration = now - prev.at;
|
|
203
234
|
const threshold = session.stallThresholdMs || this.config.stallThresholdMs;
|
|
204
|
-
if (stallDuration >= threshold && !this.
|
|
205
|
-
this.
|
|
235
|
+
if (stallDuration >= threshold && !this.stallHas(session.id, 'jsonl')) {
|
|
236
|
+
this.stallAdd(session.id, 'jsonl');
|
|
206
237
|
const minutes = Math.round(stallDuration / 60000);
|
|
207
238
|
const detail = `Session stalled: "working" for ${minutes}min with no new output. ` +
|
|
208
239
|
`Last activity: ${new Date(session.lastActivity).toISOString()}`;
|
|
@@ -213,15 +244,15 @@ export class SessionMonitor {
|
|
|
213
244
|
}
|
|
214
245
|
else {
|
|
215
246
|
// Reset JSONL stall tracking when not working
|
|
216
|
-
this.
|
|
247
|
+
this.stallDelete(session.id, 'jsonl');
|
|
217
248
|
}
|
|
218
249
|
// --- Type 2: Permission stall (waiting for approval too long) ---
|
|
219
250
|
if (currentStatus === 'permission_prompt' || currentStatus === 'bash_approval') {
|
|
220
251
|
const entry = this.stateSince.get(session.id);
|
|
221
252
|
const permDuration = entry ? now - entry.since : 0;
|
|
222
253
|
if (permDuration >= this.config.permissionStallMs) {
|
|
223
|
-
if (!this.
|
|
224
|
-
this.
|
|
254
|
+
if (!this.stallHas(session.id, 'permission')) {
|
|
255
|
+
this.stallAdd(session.id, 'permission');
|
|
225
256
|
const minutes = Math.round(permDuration / 60000);
|
|
226
257
|
const detail = `Session stalled: waiting for permission approval for ${minutes}min. ` +
|
|
227
258
|
`Auto-approve this session or POST /v1/sessions/${session.id}/approve`;
|
|
@@ -231,8 +262,8 @@ export class SessionMonitor {
|
|
|
231
262
|
}
|
|
232
263
|
// L9: Auto-reject permission after timeout
|
|
233
264
|
if (permDuration >= this.config.permissionTimeoutMs) {
|
|
234
|
-
if (!this.
|
|
235
|
-
this.
|
|
265
|
+
if (!this.stallHas(session.id, 'permission_timeout')) {
|
|
266
|
+
this.stallAdd(session.id, 'permission_timeout');
|
|
236
267
|
const minutes = Math.round(permDuration / 60000);
|
|
237
268
|
logger.warn({
|
|
238
269
|
component: 'monitor',
|
|
@@ -264,8 +295,8 @@ export class SessionMonitor {
|
|
|
264
295
|
const entry = this.stateSince.get(session.id);
|
|
265
296
|
const unkDuration = entry ? now - entry.since : 0;
|
|
266
297
|
if (unkDuration >= this.config.unknownStallMs) {
|
|
267
|
-
if (!this.
|
|
268
|
-
this.
|
|
298
|
+
if (!this.stallHas(session.id, 'unknown')) {
|
|
299
|
+
this.stallAdd(session.id, 'unknown');
|
|
269
300
|
const minutes = Math.round(unkDuration / 60000);
|
|
270
301
|
const detail = `Session stalled: in "unknown" state for ${minutes}min. ` +
|
|
271
302
|
`CC may be stuck. Try: POST /v1/sessions/${session.id}/interrupt or /kill`;
|
|
@@ -280,8 +311,8 @@ export class SessionMonitor {
|
|
|
280
311
|
const stateDuration = entry ? now - entry.since : 0;
|
|
281
312
|
const extendedThreshold = this.config.stallThresholdMs * 2;
|
|
282
313
|
if (stateDuration >= extendedThreshold) {
|
|
283
|
-
if (!this.
|
|
284
|
-
this.
|
|
314
|
+
if (!this.stallHas(session.id, 'extended')) {
|
|
315
|
+
this.stallAdd(session.id, 'extended');
|
|
285
316
|
const minutes = Math.round(stateDuration / 60000);
|
|
286
317
|
const detail = `Session stalled: "${currentStatus}" state for ${minutes}min. ` +
|
|
287
318
|
`May need intervention: /interrupt, /approve, or /kill`;
|
|
@@ -297,8 +328,8 @@ export class SessionMonitor {
|
|
|
297
328
|
if (entry && entry.state === 'working') {
|
|
298
329
|
const workingDuration = now - entry.since;
|
|
299
330
|
const maxWorkingMs = this.config.stallThresholdMs * 3; // 15 min default
|
|
300
|
-
if (workingDuration >= maxWorkingMs && !this.
|
|
301
|
-
this.
|
|
331
|
+
if (workingDuration >= maxWorkingMs && !this.stallHas(session.id, 'extended_working')) {
|
|
332
|
+
this.stallAdd(session.id, 'extended_working');
|
|
302
333
|
const minutes = Math.round(workingDuration / 60000);
|
|
303
334
|
const detail = `Session stalled: in "working" state for ${minutes}min. ` +
|
|
304
335
|
`CC may be stuck in an internal loop (e.g., Misting). Consider: POST /v1/sessions/${session.id}/interrupt or /kill`;
|
|
@@ -312,23 +343,18 @@ export class SessionMonitor {
|
|
|
312
343
|
const exitedPermission = prevStallStatus === 'permission_prompt' || prevStallStatus === 'bash_approval';
|
|
313
344
|
const exitedUnknown = prevStallStatus === 'unknown';
|
|
314
345
|
if (exitedPermission) {
|
|
315
|
-
this.
|
|
316
|
-
this.stallNotified.delete(`${session.id}:stall:permission_timeout`);
|
|
346
|
+
this.stallDeleteTypes(session.id, ['permission', 'permission_timeout']);
|
|
317
347
|
}
|
|
318
348
|
if (exitedUnknown) {
|
|
319
|
-
this.
|
|
349
|
+
this.stallDelete(session.id, 'unknown');
|
|
320
350
|
}
|
|
321
351
|
}
|
|
322
352
|
// Clean up all state tracking when idle (catch-all)
|
|
323
353
|
if (currentStatus === 'idle') {
|
|
324
354
|
this.rateLimitedSessions.delete(session.id);
|
|
325
355
|
this.stateSince.delete(session.id);
|
|
326
|
-
// Clean stall notifications (session recovered)
|
|
327
|
-
|
|
328
|
-
if (key.startsWith(session.id)) {
|
|
329
|
-
this.stallNotified.delete(key);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
356
|
+
// Clean stall notifications (session recovered) — O(1) with Map
|
|
357
|
+
this.stallDeleteAll(session.id);
|
|
332
358
|
}
|
|
333
359
|
// Update prevStatusForStall for next cycle
|
|
334
360
|
if (currentStatus) {
|
|
@@ -432,7 +458,7 @@ export class SessionMonitor {
|
|
|
432
458
|
if (event.messages.length > 0) {
|
|
433
459
|
// Real output — reset stall timer
|
|
434
460
|
this.lastBytesSeen.set(event.sessionId, { bytes: event.newOffset, at: now });
|
|
435
|
-
this.
|
|
461
|
+
this.stallDelete(event.sessionId, 'jsonl');
|
|
436
462
|
}
|
|
437
463
|
else {
|
|
438
464
|
// File grew but no messages — only update bytes, keep timestamp
|
|
@@ -666,12 +692,8 @@ export class SessionMonitor {
|
|
|
666
692
|
clearTimeout(pending);
|
|
667
693
|
this.statusChangeDebounce.delete(sessionId);
|
|
668
694
|
}
|
|
669
|
-
// Clean all stall notifications for this session
|
|
670
|
-
|
|
671
|
-
if (key.startsWith(sessionId)) {
|
|
672
|
-
this.stallNotified.delete(key);
|
|
673
|
-
}
|
|
674
|
-
}
|
|
695
|
+
// Clean all stall notifications for this session — O(1) with Map
|
|
696
|
+
this.stallDeleteAll(sessionId);
|
|
675
697
|
this.idleNotified.delete(sessionId);
|
|
676
698
|
this.idleSince.delete(sessionId);
|
|
677
699
|
this.stateSince.delete(sessionId);
|
package/dist/session.d.ts
CHANGED
|
@@ -66,6 +66,7 @@ export declare class SessionManager {
|
|
|
66
66
|
private pendingQuestions;
|
|
67
67
|
private static readonly MAX_CACHE_ENTRIES_PER_SESSION;
|
|
68
68
|
private parsedEntriesCache;
|
|
69
|
+
private sessionsListCache;
|
|
69
70
|
private readonly sessionAcquireMutex;
|
|
70
71
|
constructor(tmux: TmuxManager, config: Config);
|
|
71
72
|
/**
|
|
@@ -165,6 +166,8 @@ export declare class SessionManager {
|
|
|
165
166
|
* so the current pane PID is the shell (alive). Checking ccPid catches
|
|
166
167
|
* the crash within seconds instead of waiting for the 5-min stall timer. */
|
|
167
168
|
isWindowAlive(id: string): Promise<boolean>;
|
|
169
|
+
/** Issue #657: Invalidate the sessions list cache. Call on any mutation. */
|
|
170
|
+
private invalidateSessionsListCache;
|
|
168
171
|
/** List all sessions. */
|
|
169
172
|
listSessions(): SessionInfo[];
|
|
170
173
|
/** Issue #607: Find an idle session for the given workDir.
|
package/dist/session.js
CHANGED
|
@@ -67,6 +67,8 @@ export class SessionManager {
|
|
|
67
67
|
// #424: Evict oldest entries when cache exceeds max to prevent unbounded growth
|
|
68
68
|
static MAX_CACHE_ENTRIES_PER_SESSION = 10_000;
|
|
69
69
|
parsedEntriesCache = new Map();
|
|
70
|
+
// Issue #657: Cached session list to avoid allocating a new array per call
|
|
71
|
+
sessionsListCache = null;
|
|
70
72
|
// Issue #840/#880: Explicit mutex to prevent TOCTOU races in session acquisition.
|
|
71
73
|
sessionAcquireMutex = new Mutex();
|
|
72
74
|
constructor(tmux, config) {
|
|
@@ -168,6 +170,8 @@ export class SessionManager {
|
|
|
168
170
|
await writeFile(`${this.stateFile}.bak`, JSON.stringify(this.state, null, 2));
|
|
169
171
|
}
|
|
170
172
|
catch { /* non-critical */ }
|
|
173
|
+
// Issue #657: Invalidate sessions list cache after loading state
|
|
174
|
+
this.invalidateSessionsListCache();
|
|
171
175
|
// Reconcile: verify tmux windows still exist, clean up dead sessions
|
|
172
176
|
await this.reconcile();
|
|
173
177
|
}
|
|
@@ -190,6 +194,7 @@ export class SessionManager {
|
|
|
190
194
|
await cleanOrphanedBackup(session.workDir);
|
|
191
195
|
}
|
|
192
196
|
delete this.state.sessions[id];
|
|
197
|
+
this.invalidateSessionsListCache();
|
|
193
198
|
changed = true;
|
|
194
199
|
}
|
|
195
200
|
else if (!windowIdAlive && windowNameAlive) {
|
|
@@ -245,6 +250,7 @@ export class SessionManager {
|
|
|
245
250
|
permissionMode: 'default',
|
|
246
251
|
};
|
|
247
252
|
this.state.sessions[id] = session;
|
|
253
|
+
this.invalidateSessionsListCache();
|
|
248
254
|
console.log(`Reconcile: adopted orphaned window ${win.windowName} (${win.windowId}) as ${id.slice(0, 8)}`);
|
|
249
255
|
this.startSessionIdDiscovery(id);
|
|
250
256
|
this.startFilesystemDiscovery(id, session.workDir);
|
|
@@ -545,6 +551,7 @@ export class SessionManager {
|
|
|
545
551
|
hookSecret,
|
|
546
552
|
};
|
|
547
553
|
this.state.sessions[id] = session;
|
|
554
|
+
this.invalidateSessionsListCache();
|
|
548
555
|
await this.save();
|
|
549
556
|
// Issue #353: Fetch CC process PID for swarm parent matching.
|
|
550
557
|
// Fire-and-forget — PID is not needed synchronously.
|
|
@@ -744,9 +751,16 @@ export class SessionManager {
|
|
|
744
751
|
return false;
|
|
745
752
|
}
|
|
746
753
|
}
|
|
754
|
+
/** Issue #657: Invalidate the sessions list cache. Call on any mutation. */
|
|
755
|
+
invalidateSessionsListCache() {
|
|
756
|
+
this.sessionsListCache = null;
|
|
757
|
+
}
|
|
747
758
|
/** List all sessions. */
|
|
748
759
|
listSessions() {
|
|
749
|
-
|
|
760
|
+
if (!this.sessionsListCache) {
|
|
761
|
+
this.sessionsListCache = Object.values(this.state.sessions);
|
|
762
|
+
}
|
|
763
|
+
return this.sessionsListCache;
|
|
750
764
|
}
|
|
751
765
|
/** Issue #607: Find an idle session for the given workDir.
|
|
752
766
|
* Returns the most recently active idle session, or null if none found.
|
|
@@ -1320,6 +1334,7 @@ export class SessionManager {
|
|
|
1320
1334
|
// #405: Clean up all tracking maps (pollTimers, pendingPermissions, pendingQuestions, parsedEntriesCache)
|
|
1321
1335
|
this.cleanupSession(id);
|
|
1322
1336
|
delete this.state.sessions[id];
|
|
1337
|
+
this.invalidateSessionsListCache();
|
|
1323
1338
|
// #357: Cancel any pending debounced save before doing an immediate save
|
|
1324
1339
|
if (this.saveDebounceTimer !== null) {
|
|
1325
1340
|
clearTimeout(this.saveDebounceTimer);
|
package/dist/transcript.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Port of CCBot's transcript_parser.py.
|
|
5
5
|
* Reads CC session JSONL files and extracts structured messages.
|
|
6
6
|
*/
|
|
7
|
-
import { readFile, open } from 'node:fs/promises';
|
|
8
|
-
import { createReadStream
|
|
7
|
+
import { readFile, open, access } from 'node:fs/promises';
|
|
8
|
+
import { createReadStream } from 'node:fs';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
import { homedir } from 'node:os';
|
|
11
11
|
import { readdir } from 'node:fs/promises';
|
|
@@ -218,10 +218,20 @@ export async function readNewEntries(filePath, fromOffset) {
|
|
|
218
218
|
await fd.close();
|
|
219
219
|
}
|
|
220
220
|
}
|
|
221
|
+
/** Check if a path exists (async). Issue #658: replaces sync existsSync. */
|
|
222
|
+
async function pathExists(filePath) {
|
|
223
|
+
try {
|
|
224
|
+
await access(filePath);
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
221
231
|
/** Find the JSONL file for a session ID. */
|
|
222
232
|
export async function findSessionFile(sessionId, claudeProjectsDir = DEFAULT_CLAUDE_PROJECTS_DIR) {
|
|
223
233
|
const projectsDir = claudeProjectsDir;
|
|
224
|
-
if (!
|
|
234
|
+
if (!(await pathExists(projectsDir)))
|
|
225
235
|
return null;
|
|
226
236
|
// Strategy 1: Direct glob across all project dirs
|
|
227
237
|
const dirs = await readdir(projectsDir, { withFileTypes: true });
|
|
@@ -229,7 +239,7 @@ export async function findSessionFile(sessionId, claudeProjectsDir = DEFAULT_CLA
|
|
|
229
239
|
if (!dir.isDirectory())
|
|
230
240
|
continue;
|
|
231
241
|
const jsonlPath = join(projectsDir, dir.name, `${sessionId}.jsonl`);
|
|
232
|
-
if (
|
|
242
|
+
if (await pathExists(jsonlPath))
|
|
233
243
|
return jsonlPath;
|
|
234
244
|
}
|
|
235
245
|
// Strategy 2: Check sessions-index.json files
|
|
@@ -237,7 +247,7 @@ export async function findSessionFile(sessionId, claudeProjectsDir = DEFAULT_CLA
|
|
|
237
247
|
if (!dir.isDirectory())
|
|
238
248
|
continue;
|
|
239
249
|
const indexPath = join(projectsDir, dir.name, 'sessions-index.json');
|
|
240
|
-
if (
|
|
250
|
+
if (await pathExists(indexPath)) {
|
|
241
251
|
try {
|
|
242
252
|
const indexRaw = await readFile(indexPath, 'utf-8');
|
|
243
253
|
const indexParsed = sessionsIndexSchema.safeParse(JSON.parse(indexRaw));
|
|
@@ -245,7 +255,7 @@ export async function findSessionFile(sessionId, claudeProjectsDir = DEFAULT_CLA
|
|
|
245
255
|
continue;
|
|
246
256
|
const entries = indexParsed.data.entries || [];
|
|
247
257
|
for (const entry of entries) {
|
|
248
|
-
if (entry.sessionId === sessionId && entry.fullPath &&
|
|
258
|
+
if (entry.sessionId === sessionId && entry.fullPath && (await pathExists(entry.fullPath))) {
|
|
249
259
|
return entry.fullPath;
|
|
250
260
|
}
|
|
251
261
|
}
|