aegis-bridge 2.2.2
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/LICENSE +21 -0
- package/README.md +244 -0
- package/dashboard/dist/assets/index-CijFoeRu.css +32 -0
- package/dashboard/dist/assets/index-QtT4j0ht.js +262 -0
- package/dashboard/dist/index.html +14 -0
- package/dist/auth.d.ts +76 -0
- package/dist/auth.js +219 -0
- package/dist/channels/index.d.ts +8 -0
- package/dist/channels/index.js +9 -0
- package/dist/channels/manager.d.ts +39 -0
- package/dist/channels/manager.js +101 -0
- package/dist/channels/telegram-style.d.ts +118 -0
- package/dist/channels/telegram-style.js +203 -0
- package/dist/channels/telegram.d.ts +76 -0
- package/dist/channels/telegram.js +1396 -0
- package/dist/channels/types.d.ts +77 -0
- package/dist/channels/types.js +9 -0
- package/dist/channels/webhook.d.ts +58 -0
- package/dist/channels/webhook.js +162 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +223 -0
- package/dist/config.d.ts +60 -0
- package/dist/config.js +188 -0
- package/dist/dashboard/assets/index-CijFoeRu.css +32 -0
- package/dist/dashboard/assets/index-QtT4j0ht.js +262 -0
- package/dist/dashboard/index.html +14 -0
- package/dist/events.d.ts +86 -0
- package/dist/events.js +258 -0
- package/dist/hook-settings.d.ts +67 -0
- package/dist/hook-settings.js +138 -0
- package/dist/hook.d.ts +18 -0
- package/dist/hook.js +199 -0
- package/dist/hooks.d.ts +32 -0
- package/dist/hooks.js +279 -0
- package/dist/jsonl-watcher.d.ts +57 -0
- package/dist/jsonl-watcher.js +159 -0
- package/dist/mcp-server.d.ts +60 -0
- package/dist/mcp-server.js +788 -0
- package/dist/metrics.d.ts +104 -0
- package/dist/metrics.js +226 -0
- package/dist/monitor.d.ts +84 -0
- package/dist/monitor.js +553 -0
- package/dist/permission-guard.d.ts +51 -0
- package/dist/permission-guard.js +197 -0
- package/dist/pipeline.d.ts +84 -0
- package/dist/pipeline.js +218 -0
- package/dist/screenshot.d.ts +26 -0
- package/dist/screenshot.js +57 -0
- package/dist/server.d.ts +10 -0
- package/dist/server.js +1577 -0
- package/dist/session.d.ts +297 -0
- package/dist/session.js +1275 -0
- package/dist/sse-limiter.d.ts +47 -0
- package/dist/sse-limiter.js +62 -0
- package/dist/sse-writer.d.ts +31 -0
- package/dist/sse-writer.js +95 -0
- package/dist/ssrf.d.ts +57 -0
- package/dist/ssrf.js +169 -0
- package/dist/swarm-monitor.d.ts +114 -0
- package/dist/swarm-monitor.js +267 -0
- package/dist/terminal-parser.d.ts +16 -0
- package/dist/terminal-parser.js +343 -0
- package/dist/tmux.d.ts +161 -0
- package/dist/tmux.js +725 -0
- package/dist/transcript.d.ts +47 -0
- package/dist/transcript.js +244 -0
- package/dist/validation.d.ts +222 -0
- package/dist/validation.js +268 -0
- package/dist/ws-terminal.d.ts +32 -0
- package/dist/ws-terminal.js +297 -0
- package/package.json +71 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session.ts — Session state manager.
|
|
3
|
+
*
|
|
4
|
+
* Manages the lifecycle of CC sessions running in tmux windows.
|
|
5
|
+
* Tracks: session ID, window ID, byte offset for JSONL reading, status.
|
|
6
|
+
*/
|
|
7
|
+
import { TmuxManager } from './tmux.js';
|
|
8
|
+
import { type ParsedEntry } from './transcript.js';
|
|
9
|
+
import { type UIState } from './terminal-parser.js';
|
|
10
|
+
import type { Config } from './config.js';
|
|
11
|
+
export interface SessionInfo {
|
|
12
|
+
id: string;
|
|
13
|
+
windowId: string;
|
|
14
|
+
windowName: string;
|
|
15
|
+
workDir: string;
|
|
16
|
+
claudeSessionId?: string;
|
|
17
|
+
jsonlPath?: string;
|
|
18
|
+
byteOffset: number;
|
|
19
|
+
monitorOffset: number;
|
|
20
|
+
status: UIState;
|
|
21
|
+
createdAt: number;
|
|
22
|
+
lastActivity: number;
|
|
23
|
+
stallThresholdMs: number;
|
|
24
|
+
permissionStallMs: number;
|
|
25
|
+
permissionMode: string;
|
|
26
|
+
settingsPatched?: boolean;
|
|
27
|
+
hookSettingsFile?: string;
|
|
28
|
+
lastHookAt?: number;
|
|
29
|
+
activeSubagents?: Set<string>;
|
|
30
|
+
permissionPromptAt?: number;
|
|
31
|
+
permissionRespondedAt?: number;
|
|
32
|
+
lastHookReceivedAt?: number;
|
|
33
|
+
lastHookEventAt?: number;
|
|
34
|
+
model?: string;
|
|
35
|
+
lastDeadAt?: number;
|
|
36
|
+
ccPid?: number;
|
|
37
|
+
}
|
|
38
|
+
export interface SessionState {
|
|
39
|
+
sessions: Record<string, SessionInfo>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Detect whether CC is showing numbered permission options (e.g. "1. Yes, 2. No")
|
|
43
|
+
* vs a simple y/N prompt. Returns the approval method to use.
|
|
44
|
+
*
|
|
45
|
+
* CC's permission UI uses indented numbered lines with "Esc to cancel" nearby.
|
|
46
|
+
* We look for the pattern " <N>. <option>" where N is 1-3, which distinguishes
|
|
47
|
+
* permission options from regular numbered lists in output.
|
|
48
|
+
*/
|
|
49
|
+
export declare function detectApprovalMethod(paneText: string): 'numbered' | 'yes';
|
|
50
|
+
/** Resolves a pending PermissionRequest hook with a decision. */
|
|
51
|
+
export type PermissionDecision = 'allow' | 'deny';
|
|
52
|
+
export declare class SessionManager {
|
|
53
|
+
private tmux;
|
|
54
|
+
private config;
|
|
55
|
+
private state;
|
|
56
|
+
private stateFile;
|
|
57
|
+
private sessionMapFile;
|
|
58
|
+
private pollTimers;
|
|
59
|
+
private saveQueue;
|
|
60
|
+
private saveDebounceTimer;
|
|
61
|
+
private static readonly SAVE_DEBOUNCE_MS;
|
|
62
|
+
private pendingPermissions;
|
|
63
|
+
private pendingQuestions;
|
|
64
|
+
private parsedEntriesCache;
|
|
65
|
+
constructor(tmux: TmuxManager, config: Config);
|
|
66
|
+
/** Validate that parsed data looks like a valid SessionState. */
|
|
67
|
+
private isValidState;
|
|
68
|
+
/** Clean up stale .tmp files left by crashed writes. */
|
|
69
|
+
private cleanTmpFiles;
|
|
70
|
+
/** Load state from disk. */
|
|
71
|
+
load(): Promise<void>;
|
|
72
|
+
/** Reconcile state with actual tmux windows. Remove dead sessions, restart discovery for live ones. */
|
|
73
|
+
private reconcile;
|
|
74
|
+
/** Save state to disk atomically (write to temp, then rename).
|
|
75
|
+
* #218: Uses a write queue to serialize concurrent saves and prevent corruption. */
|
|
76
|
+
save(): Promise<void>;
|
|
77
|
+
/** #357: Debounced save — skips immediate save for offset-only changes.
|
|
78
|
+
* Coalesces rapid successive reads into a single disk write. */
|
|
79
|
+
debouncedSave(): void;
|
|
80
|
+
private doSave;
|
|
81
|
+
/** Default stall threshold: 5 minutes (Issue #4: reduced from 60 min). */
|
|
82
|
+
static readonly DEFAULT_STALL_THRESHOLD_MS: number;
|
|
83
|
+
static readonly DEFAULT_PERMISSION_STALL_MS: number;
|
|
84
|
+
/** Create a new CC session. */
|
|
85
|
+
/** Default timeout for waiting CC to become ready (60s for cold starts). */
|
|
86
|
+
static readonly DEFAULT_PROMPT_TIMEOUT_MS = 60000;
|
|
87
|
+
/** Max retries if CC doesn't become ready in time. */
|
|
88
|
+
static readonly DEFAULT_PROMPT_MAX_RETRIES = 2;
|
|
89
|
+
/**
|
|
90
|
+
* Wait for CC to show its idle prompt in the tmux pane, then send the initial prompt.
|
|
91
|
+
* Uses exponential backoff on retry: first attempt waits timeoutMs, subsequent attempts
|
|
92
|
+
* wait 1.5x the previous timeout.
|
|
93
|
+
*
|
|
94
|
+
* Returns delivery result. Logs warnings on each retry for observability.
|
|
95
|
+
*/
|
|
96
|
+
sendInitialPrompt(sessionId: string, prompt: string, timeoutMs?: number, maxRetries?: number): Promise<{
|
|
97
|
+
delivered: boolean;
|
|
98
|
+
attempts: number;
|
|
99
|
+
}>;
|
|
100
|
+
/** Wait for CC idle prompt, then send. Single attempt. */
|
|
101
|
+
private waitForReadyAndSend;
|
|
102
|
+
createSession(opts: {
|
|
103
|
+
workDir: string;
|
|
104
|
+
name?: string;
|
|
105
|
+
resumeSessionId?: string;
|
|
106
|
+
claudeCommand?: string;
|
|
107
|
+
env?: Record<string, string>;
|
|
108
|
+
stallThresholdMs?: number;
|
|
109
|
+
permissionStallMs?: number;
|
|
110
|
+
permissionMode?: string;
|
|
111
|
+
/** @deprecated Use permissionMode instead. Maps true→bypassPermissions, false→default. */
|
|
112
|
+
autoApprove?: boolean;
|
|
113
|
+
}): Promise<SessionInfo>;
|
|
114
|
+
/** Get a session by ID. */
|
|
115
|
+
getSession(id: string): SessionInfo | null;
|
|
116
|
+
/** Issue #169 Phase 3: Update session status from a hook event.
|
|
117
|
+
* Returns the previous status for change detection.
|
|
118
|
+
* Issue #87: Also records hook latency timestamps. */
|
|
119
|
+
updateStatusFromHook(id: string, hookEvent: string, hookTimestamp?: number): UIState | null;
|
|
120
|
+
/** Issue #88: Add an active subagent to a session. */
|
|
121
|
+
addSubagent(id: string, name: string): void;
|
|
122
|
+
/** Issue #88: Remove an active subagent from a session. */
|
|
123
|
+
removeSubagent(id: string, name: string): void;
|
|
124
|
+
/** Issue #89 L25: Update the model field on a session from hook payload. */
|
|
125
|
+
updateSessionModel(id: string, model: string): void;
|
|
126
|
+
/** Issue #87: Get latency metrics for a session. */
|
|
127
|
+
getLatencyMetrics(id: string): {
|
|
128
|
+
hook_latency_ms: number | null;
|
|
129
|
+
state_change_detection_ms: number | null;
|
|
130
|
+
permission_response_ms: number | null;
|
|
131
|
+
} | null;
|
|
132
|
+
/** Check if a session's tmux window still exists and has a live process.
|
|
133
|
+
* Issue #69: A window can exist with a crashed/zombie CC process (zombie window).
|
|
134
|
+
* After checking window exists, also verify the pane PID is alive.
|
|
135
|
+
* Issue #390: Check stored ccPid first for immediate crash detection.
|
|
136
|
+
* When CC crashes (SIGKILL, OOM), the shell prompt returns in the pane,
|
|
137
|
+
* so the current pane PID is the shell (alive). Checking ccPid catches
|
|
138
|
+
* the crash within seconds instead of waiting for the 5-min stall timer. */
|
|
139
|
+
isWindowAlive(id: string): Promise<boolean>;
|
|
140
|
+
/** List all sessions. */
|
|
141
|
+
listSessions(): SessionInfo[];
|
|
142
|
+
/** Get health info for a session.
|
|
143
|
+
* Issue #2: Returns comprehensive health status for orchestrators.
|
|
144
|
+
*/
|
|
145
|
+
getHealth(id: string): Promise<{
|
|
146
|
+
alive: boolean;
|
|
147
|
+
windowExists: boolean;
|
|
148
|
+
claudeRunning: boolean;
|
|
149
|
+
paneCommand: string | null;
|
|
150
|
+
status: UIState;
|
|
151
|
+
hasTranscript: boolean;
|
|
152
|
+
lastActivity: number;
|
|
153
|
+
lastActivityAgo: number;
|
|
154
|
+
sessionAge: number;
|
|
155
|
+
details: string;
|
|
156
|
+
actionHints?: Record<string, {
|
|
157
|
+
method: string;
|
|
158
|
+
url: string;
|
|
159
|
+
description: string;
|
|
160
|
+
}>;
|
|
161
|
+
}>;
|
|
162
|
+
/** Send a message to a session with delivery verification.
|
|
163
|
+
* Issue #1: Uses capture-pane to verify the prompt was delivered.
|
|
164
|
+
* Returns delivery status for API response.
|
|
165
|
+
*/
|
|
166
|
+
sendMessage(id: string, text: string): Promise<{
|
|
167
|
+
delivered: boolean;
|
|
168
|
+
attempts: number;
|
|
169
|
+
}>;
|
|
170
|
+
/** Send message bypassing the tmux serialize queue.
|
|
171
|
+
* Used by sendInitialPrompt for critical-path prompt delivery.
|
|
172
|
+
*
|
|
173
|
+
* Issue #285: Changed from sendKeysDirect (unverified) to sendKeysVerified
|
|
174
|
+
* with 3 retry attempts. tmux send-keys can silently fail even at session
|
|
175
|
+
* creation time, causing ~20% prompt delivery failure rate.
|
|
176
|
+
*
|
|
177
|
+
* We still bypass the serialize queue (using capturePaneDirect in verifyDelivery)
|
|
178
|
+
* but now verify actual delivery to CC.
|
|
179
|
+
*/
|
|
180
|
+
private sendMessageDirect;
|
|
181
|
+
/** Record that a permission prompt was detected for this session. */
|
|
182
|
+
recordPermissionPrompt(id: string): void;
|
|
183
|
+
/** Approve a permission prompt. Resolves pending hook permission first, falls back to tmux send-keys. */
|
|
184
|
+
approve(id: string): Promise<void>;
|
|
185
|
+
/** Reject a permission prompt. Resolves pending hook permission first, falls back to tmux send-keys. */
|
|
186
|
+
reject(id: string): Promise<void>;
|
|
187
|
+
/**
|
|
188
|
+
* Issue #284: Store a pending permission request and return a promise that
|
|
189
|
+
* resolves when the client approves/rejects via the API.
|
|
190
|
+
*
|
|
191
|
+
* @param sessionId - Aegis session ID
|
|
192
|
+
* @param timeoutMs - Timeout before auto-rejecting (default 10_000ms, matching CC's hook timeout)
|
|
193
|
+
* @param toolName - Optional tool name from the hook payload
|
|
194
|
+
* @param prompt - Optional permission prompt text
|
|
195
|
+
* @returns Promise that resolves with the client's decision
|
|
196
|
+
*/
|
|
197
|
+
waitForPermissionDecision(sessionId: string, timeoutMs?: number, toolName?: string, prompt?: string): Promise<PermissionDecision>;
|
|
198
|
+
/** Check if a session has a pending permission request. */
|
|
199
|
+
hasPendingPermission(sessionId: string): boolean;
|
|
200
|
+
/** Get info about a pending permission (for API responses). */
|
|
201
|
+
getPendingPermissionInfo(sessionId: string): {
|
|
202
|
+
toolName?: string;
|
|
203
|
+
prompt?: string;
|
|
204
|
+
} | null;
|
|
205
|
+
/**
|
|
206
|
+
* Resolve a pending permission. Returns true if there was a pending permission to resolve.
|
|
207
|
+
*/
|
|
208
|
+
private resolvePendingPermission;
|
|
209
|
+
/** Clean up any pending permission for a session (e.g. on session delete). */
|
|
210
|
+
cleanupPendingPermission(sessionId: string): void;
|
|
211
|
+
/**
|
|
212
|
+
* Issue #336: Store a pending AskUserQuestion and return a promise that
|
|
213
|
+
* resolves when the external client provides an answer via POST /answer.
|
|
214
|
+
*/
|
|
215
|
+
waitForAnswer(sessionId: string, toolUseId: string, question: string, timeoutMs?: number): Promise<string | null>;
|
|
216
|
+
/** Issue #336: Submit an answer to a pending question. Returns true if resolved. */
|
|
217
|
+
submitAnswer(sessionId: string, questionId: string, answer: string): boolean;
|
|
218
|
+
/** Issue #336: Check if a session has a pending question. */
|
|
219
|
+
hasPendingQuestion(sessionId: string): boolean;
|
|
220
|
+
/** Issue #336: Get info about a pending question. */
|
|
221
|
+
getPendingQuestionInfo(sessionId: string): {
|
|
222
|
+
toolUseId: string;
|
|
223
|
+
question: string;
|
|
224
|
+
} | null;
|
|
225
|
+
/** Issue #336: Clean up any pending question for a session. */
|
|
226
|
+
cleanupPendingQuestion(sessionId: string): void;
|
|
227
|
+
/** Send Escape key. */
|
|
228
|
+
escape(id: string): Promise<void>;
|
|
229
|
+
/** Send Ctrl+C. */
|
|
230
|
+
interrupt(id: string): Promise<void>;
|
|
231
|
+
/** Read new messages from a session. */
|
|
232
|
+
readMessages(id: string): Promise<{
|
|
233
|
+
messages: ParsedEntry[];
|
|
234
|
+
status: UIState;
|
|
235
|
+
statusText: string | null;
|
|
236
|
+
interactiveContent: string | null;
|
|
237
|
+
}>;
|
|
238
|
+
/** Read new messages for the monitor (separate offset from API reads). */
|
|
239
|
+
readMessagesForMonitor(id: string): Promise<{
|
|
240
|
+
messages: ParsedEntry[];
|
|
241
|
+
status: UIState;
|
|
242
|
+
statusText: string | null;
|
|
243
|
+
interactiveContent: string | null;
|
|
244
|
+
}>;
|
|
245
|
+
/** #357: Get all parsed entries for a session, using a cache to avoid full reparse.
|
|
246
|
+
* Reads only the delta from the last cached offset. */
|
|
247
|
+
private getCachedEntries;
|
|
248
|
+
/** Issue #35: Get a condensed summary of a session's transcript. */
|
|
249
|
+
getSummary(id: string, maxMessages?: number): Promise<{
|
|
250
|
+
sessionId: string;
|
|
251
|
+
windowName: string;
|
|
252
|
+
status: UIState;
|
|
253
|
+
totalMessages: number;
|
|
254
|
+
messages: Array<{
|
|
255
|
+
role: string;
|
|
256
|
+
contentType: string;
|
|
257
|
+
text: string;
|
|
258
|
+
}>;
|
|
259
|
+
createdAt: number;
|
|
260
|
+
lastActivity: number;
|
|
261
|
+
permissionMode: string;
|
|
262
|
+
}>;
|
|
263
|
+
/** Paginated transcript read — does NOT advance the session's byteOffset. */
|
|
264
|
+
readTranscript(id: string, page?: number, limit?: number, roleFilter?: 'user' | 'assistant' | 'system'): Promise<{
|
|
265
|
+
messages: ParsedEntry[];
|
|
266
|
+
total: number;
|
|
267
|
+
page: number;
|
|
268
|
+
limit: number;
|
|
269
|
+
hasMore: boolean;
|
|
270
|
+
}>;
|
|
271
|
+
/** #405: Clean up all tracking maps for a session to prevent memory leaks. */
|
|
272
|
+
private cleanupSession;
|
|
273
|
+
/** Kill a session. */
|
|
274
|
+
killSession(id: string): Promise<void>;
|
|
275
|
+
/** Remove stale entries from session_map.json for a given window.
|
|
276
|
+
* P0 fix: After aegis service restarts, old session_map entries with stale windowIds
|
|
277
|
+
* can survive and cause new sessions to inherit context from old sessions.
|
|
278
|
+
* We must clean by BOTH windowName AND windowId to prevent collisions.
|
|
279
|
+
*
|
|
280
|
+
* After archiving old .jsonl files, old hook entries would cause discovery
|
|
281
|
+
* to map the new session to a ghost claudeSessionId whose file no longer exists.
|
|
282
|
+
*/
|
|
283
|
+
private cleanSessionMapForWindow;
|
|
284
|
+
/** P0 fix: Purge session_map entries that don't correspond to active aegis sessions.
|
|
285
|
+
* After aegis restarts, old session_map entries with stale windowIds can survive
|
|
286
|
+
* and cause new sessions to inherit context from old sessions.
|
|
287
|
+
*/
|
|
288
|
+
private purgeStaleSessionMapEntries;
|
|
289
|
+
/** Try to discover the CC session ID and JSONL path. */
|
|
290
|
+
private startSessionIdDiscovery;
|
|
291
|
+
/** Issue #16: Filesystem-based discovery for --bare mode (no hooks).
|
|
292
|
+
* Scans the Claude projects directory for new .jsonl files created after the session.
|
|
293
|
+
*/
|
|
294
|
+
private startFilesystemDiscovery;
|
|
295
|
+
/** Sync CC session IDs from the hook-written session_map.json. */
|
|
296
|
+
private syncSessionMap;
|
|
297
|
+
}
|