ai-agent-session-center 2.3.3 → 2.3.5
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/client/assets/{AnalyticsView-DVDqblSH.js → AnalyticsView-qY6U6hm6.js} +1 -1
- package/dist/client/assets/{Charts.module-CQf7HQCb.js → Charts.module-DMrHdYE2.js} +1 -1
- package/dist/client/assets/{CyberdromeScene-w4bk6IXJ.js → CyberdromeScene-BAHIVPjO.js} +1 -1
- package/dist/client/assets/{HistoryView--XwKlCyd.js → HistoryView-DBPWSMVy.js} +1 -1
- package/dist/client/assets/{ProjectBrowserView-CTN5CmBa.js → ProjectBrowserView-BVsrZVHg.js} +1 -1
- package/dist/client/assets/{QueueView-Bj-e0m_U.js → QueueView-t313VSQZ.js} +1 -1
- package/dist/client/assets/{TimelineView-CoTsTAGd.js → TimelineView-COH0s3pN.js} +1 -1
- package/dist/client/assets/index-DcItPQrq.js +130 -0
- package/dist/client/assets/index-b03MoG49.css +1 -0
- package/dist/client/assets/{useQuery-5GNo2Ewt.js → useQuery-C6BUH11S.js} +1 -1
- package/dist/client/assets/{with-selector-DTnjuyBc.js → with-selector-Cw2vedS9.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/client/screenshot-mobile-history.png +0 -0
- package/dist/client/screenshot-mobile-home.png +0 -0
- package/dist/client/screenshot-mobile-project.png +0 -0
- package/dist/client/screenshot-mobile-terminal.png +0 -0
- package/package.json +1 -1
- package/server/apiRouter.ts +145 -30
- package/server/approvalDetector.ts +2 -2
- package/server/index.ts +51 -18
- package/server/mqReader.ts +4 -4
- package/server/processMonitor.ts +12 -6
- package/server/sessionMatcher.ts +39 -0
- package/server/sessionStore.ts +4 -3
- package/server/wsManager.ts +51 -10
- package/dist/client/assets/index-Dgi6T0Nt.js +0 -128
- package/dist/client/assets/index-DqtLpLIs.css +0 -1
package/server/sessionMatcher.ts
CHANGED
|
@@ -50,6 +50,29 @@ export function reKeyResumedSession(
|
|
|
50
50
|
oldSession.cachedPid = null;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
// Archive the old session data into previousSessions before resetting.
|
|
54
|
+
// Check dedup: resumeSession() already archives before calling this function,
|
|
55
|
+
// so only archive if the last entry doesn't match the old session ID.
|
|
56
|
+
const hasData = oldSession.promptHistory.length > 0 || oldSession.toolLog?.length > 0 || oldSession.events?.length > 0;
|
|
57
|
+
if (hasData) {
|
|
58
|
+
const lastPrev = oldSession.previousSessions?.[oldSession.previousSessions.length - 1];
|
|
59
|
+
if (!lastPrev || lastPrev.sessionId !== oldSessionId) {
|
|
60
|
+
if (!oldSession.previousSessions) oldSession.previousSessions = [];
|
|
61
|
+
oldSession.previousSessions.push({
|
|
62
|
+
sessionId: oldSessionId,
|
|
63
|
+
startedAt: oldSession.startedAt,
|
|
64
|
+
endedAt: oldSession.endedAt,
|
|
65
|
+
promptHistory: [...oldSession.promptHistory],
|
|
66
|
+
toolLog: [...(oldSession.toolLog || [])],
|
|
67
|
+
responseLog: [...(oldSession.responseLog || [])],
|
|
68
|
+
events: [...oldSession.events],
|
|
69
|
+
toolUsage: { ...oldSession.toolUsage },
|
|
70
|
+
totalToolCalls: oldSession.totalToolCalls,
|
|
71
|
+
});
|
|
72
|
+
if (oldSession.previousSessions.length > 5) oldSession.previousSessions.shift();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
53
76
|
oldSession.replacesId = oldSessionId;
|
|
54
77
|
oldSession.sessionId = newSessionId;
|
|
55
78
|
oldSession.status = SESSION_STATUS.IDLE;
|
|
@@ -294,6 +317,22 @@ export function matchSession(
|
|
|
294
317
|
}
|
|
295
318
|
}
|
|
296
319
|
|
|
320
|
+
// Priority 1.5: Match by cached PID — when Claude resumes with a new session_id
|
|
321
|
+
// but the same process (e.g., `claude --resume` creates a new session internally),
|
|
322
|
+
// link back to the same SSH terminal session instead of creating a duplicate card.
|
|
323
|
+
if (!session && hookData.claude_pid && hook_event_name === EVENT_TYPES.SESSION_START) {
|
|
324
|
+
const pid = Number(hookData.claude_pid);
|
|
325
|
+
const existingSessionId = pidToSession.get(pid);
|
|
326
|
+
if (existingSessionId && existingSessionId !== session_id) {
|
|
327
|
+
const existingSession = sessions.get(existingSessionId);
|
|
328
|
+
if (existingSession && existingSession.terminalId) {
|
|
329
|
+
session = reKeyResumedSession(sessions, existingSession, session_id, existingSessionId, pidToSession);
|
|
330
|
+
consumePendingLink(existingSession.projectPath || '');
|
|
331
|
+
log.info('session', `Re-keyed session ${existingSessionId?.slice(0, 8)} -> ${session_id?.slice(0, 8)} (via cached PID=${pid}, same process new session_id)`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
297
336
|
// Priority 2: Match via pending workDir link
|
|
298
337
|
if (!session) {
|
|
299
338
|
const linkedTerminalId = tryLinkByWorkDir(cwd || '', session_id);
|
package/server/sessionStore.ts
CHANGED
|
@@ -141,9 +141,9 @@ export function saveSnapshot(mqOffset?: number): void {
|
|
|
141
141
|
pidToSession: pidObj,
|
|
142
142
|
pendingResume: pendingResumeObj,
|
|
143
143
|
};
|
|
144
|
-
mkdirSync(SNAPSHOT_DIR, { recursive: true });
|
|
144
|
+
mkdirSync(SNAPSHOT_DIR, { recursive: true, mode: 0o700 });
|
|
145
145
|
const tmpFile = SNAPSHOT_FILE + '.tmp';
|
|
146
|
-
writeFileSync(tmpFile, JSON.stringify(snapshot));
|
|
146
|
+
writeFileSync(tmpFile, JSON.stringify(snapshot), { mode: 0o600 });
|
|
147
147
|
renameSync(tmpFile, SNAPSHOT_FILE);
|
|
148
148
|
log.debug('session', `Snapshot saved: ${Object.keys(sessionsObj).length} sessions`);
|
|
149
149
|
} catch (err: unknown) {
|
|
@@ -770,7 +770,8 @@ export function handleEvent(hookData: HookPayload): HandleEventResult | null {
|
|
|
770
770
|
if (session.source === 'ssh') {
|
|
771
771
|
session.isHistorical = true;
|
|
772
772
|
session.lastTerminalId = session.terminalId;
|
|
773
|
-
|
|
773
|
+
// Keep terminalId alive — the PTY shell is still running even though Claude exited.
|
|
774
|
+
// terminalId is nulled when the PTY actually dies (registerTerminalExitCallback).
|
|
774
775
|
}
|
|
775
776
|
// Non-SSH sessions are also kept (no auto-delete)
|
|
776
777
|
break;
|
package/server/wsManager.ts
CHANGED
|
@@ -8,9 +8,13 @@ import type WebSocket from 'ws';
|
|
|
8
8
|
interface WsClient extends WebSocket {
|
|
9
9
|
_terminalIds: Set<string>;
|
|
10
10
|
_isAlive: boolean;
|
|
11
|
+
_msgCount: number;
|
|
12
|
+
_msgWindowStart: number;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
const clients = new Set<WsClient>();
|
|
16
|
+
const MAX_WS_CONNECTIONS = 50;
|
|
17
|
+
const MAX_MSG_PER_SECOND = 100;
|
|
14
18
|
|
|
15
19
|
// Heartbeat: ping every 30s, terminate connections that don't pong within 10s
|
|
16
20
|
const HEARTBEAT_INTERVAL_MS = 30000;
|
|
@@ -57,10 +61,19 @@ export function stopHeartbeat(): void {
|
|
|
57
61
|
* Handle a new WebSocket connection: send snapshot and wire up message/close handlers.
|
|
58
62
|
*/
|
|
59
63
|
export function handleConnection(ws: WebSocket): void {
|
|
64
|
+
// Enforce connection limit
|
|
65
|
+
if (clients.size >= MAX_WS_CONNECTIONS) {
|
|
66
|
+
log.warn('ws', `Connection limit reached (${MAX_WS_CONNECTIONS}), rejecting`);
|
|
67
|
+
ws.close(4003, 'Too many connections');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
60
71
|
const client = ws as WsClient;
|
|
61
72
|
clients.add(client);
|
|
62
73
|
client._terminalIds = new Set();
|
|
63
74
|
client._isAlive = true;
|
|
75
|
+
client._msgCount = 0;
|
|
76
|
+
client._msgWindowStart = Date.now();
|
|
64
77
|
log.info('ws', `Client connected (total: ${clients.size})`);
|
|
65
78
|
|
|
66
79
|
// Start heartbeat on first connection
|
|
@@ -80,16 +93,43 @@ export function handleConnection(ws: WebSocket): void {
|
|
|
80
93
|
|
|
81
94
|
// Handle incoming messages (terminal input, resize, etc.)
|
|
82
95
|
client.on('message', (raw: WebSocket.RawData) => {
|
|
96
|
+
// Rate limit: max MAX_MSG_PER_SECOND messages per second per client
|
|
97
|
+
const now = Date.now();
|
|
98
|
+
if (now - client._msgWindowStart > 1000) {
|
|
99
|
+
client._msgWindowStart = now;
|
|
100
|
+
client._msgCount = 0;
|
|
101
|
+
}
|
|
102
|
+
client._msgCount++;
|
|
103
|
+
if (client._msgCount > MAX_MSG_PER_SECOND) {
|
|
104
|
+
log.warn('ws', 'Client message rate limit exceeded, closing');
|
|
105
|
+
client.close(4004, 'Rate limit exceeded');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
83
109
|
try {
|
|
84
|
-
const
|
|
110
|
+
const rawStr = raw.toString();
|
|
111
|
+
// Reject oversized messages early (64KB)
|
|
112
|
+
if (rawStr.length > 65536) {
|
|
113
|
+
log.warn('ws', 'Oversized WS message rejected');
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const msg = JSON.parse(rawStr);
|
|
85
117
|
switch (msg.type) {
|
|
86
118
|
case WS_TYPES.TERMINAL_INPUT:
|
|
87
|
-
|
|
119
|
+
// Only allow writing to terminals this client is subscribed to
|
|
120
|
+
if (typeof msg.terminalId === 'string' && typeof msg.data === 'string' && msg.data.length <= 8192) {
|
|
121
|
+
if (!client._terminalIds.has(msg.terminalId)) {
|
|
122
|
+
log.warn('ws', `Blocked terminal_input to unsubscribed terminal ${msg.terminalId}`);
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
88
125
|
writeToTerminal(msg.terminalId, msg.data);
|
|
89
126
|
}
|
|
90
127
|
break;
|
|
91
128
|
case WS_TYPES.TERMINAL_RESIZE:
|
|
92
|
-
if (msg.terminalId
|
|
129
|
+
if (typeof msg.terminalId === 'string'
|
|
130
|
+
&& Number.isInteger(msg.cols) && msg.cols > 0 && msg.cols <= 500
|
|
131
|
+
&& Number.isInteger(msg.rows) && msg.rows > 0 && msg.rows <= 200) {
|
|
132
|
+
if (!client._terminalIds.has(msg.terminalId)) break;
|
|
93
133
|
// #31: Relay resize errors back to client
|
|
94
134
|
const resizeErr = resizeTerminal(msg.terminalId, msg.cols, msg.rows);
|
|
95
135
|
if (resizeErr && client.readyState === 1) {
|
|
@@ -98,14 +138,14 @@ export function handleConnection(ws: WebSocket): void {
|
|
|
98
138
|
}
|
|
99
139
|
break;
|
|
100
140
|
case WS_TYPES.TERMINAL_DISCONNECT:
|
|
101
|
-
if (msg.terminalId) {
|
|
141
|
+
if (typeof msg.terminalId === 'string' && client._terminalIds.has(msg.terminalId)) {
|
|
102
142
|
closeTerminal(msg.terminalId);
|
|
103
143
|
client._terminalIds.delete(msg.terminalId);
|
|
104
144
|
}
|
|
105
145
|
break;
|
|
106
146
|
case WS_TYPES.TERMINAL_SUBSCRIBE:
|
|
107
147
|
// #30/#44: Only subscribe if terminal actually exists
|
|
108
|
-
if (msg.terminalId) {
|
|
148
|
+
if (typeof msg.terminalId === 'string') {
|
|
109
149
|
const exists = setWsClient(msg.terminalId, client);
|
|
110
150
|
if (exists) {
|
|
111
151
|
client._terminalIds.add(msg.terminalId);
|
|
@@ -115,7 +155,8 @@ export function handleConnection(ws: WebSocket): void {
|
|
|
115
155
|
}
|
|
116
156
|
break;
|
|
117
157
|
case WS_TYPES.UPDATE_QUEUE_COUNT:
|
|
118
|
-
if (msg.sessionId
|
|
158
|
+
if (typeof msg.sessionId === 'string' && typeof msg.count === 'number'
|
|
159
|
+
&& Number.isInteger(msg.count) && msg.count >= 0 && msg.count <= 10000) {
|
|
119
160
|
const updated = updateQueueCount(msg.sessionId, msg.count);
|
|
120
161
|
if (updated) {
|
|
121
162
|
broadcast({ type: WS_TYPES.SESSION_UPDATE, session: updated });
|
|
@@ -124,7 +165,7 @@ export function handleConnection(ws: WebSocket): void {
|
|
|
124
165
|
break;
|
|
125
166
|
case WS_TYPES.REPLAY:
|
|
126
167
|
// Client reconnected and wants events since a certain sequence number
|
|
127
|
-
if (typeof msg.sinceSeq === 'number') {
|
|
168
|
+
if (typeof msg.sinceSeq === 'number' && msg.sinceSeq >= 0) {
|
|
128
169
|
const missed = getEventsSince(msg.sinceSeq);
|
|
129
170
|
log.debug('ws', `Replaying ${missed.length} events since seq=${msg.sinceSeq}`);
|
|
130
171
|
for (const evt of missed) {
|
|
@@ -133,11 +174,11 @@ export function handleConnection(ws: WebSocket): void {
|
|
|
133
174
|
}
|
|
134
175
|
break;
|
|
135
176
|
default:
|
|
136
|
-
|
|
177
|
+
break; // Silently ignore unknown types
|
|
137
178
|
}
|
|
138
179
|
} catch (e: unknown) {
|
|
139
|
-
const
|
|
140
|
-
log.debug('ws', `Invalid WS message: ${
|
|
180
|
+
const errMsg = e instanceof Error ? e.message : String(e);
|
|
181
|
+
log.debug('ws', `Invalid WS message: ${errMsg}`);
|
|
141
182
|
}
|
|
142
183
|
});
|
|
143
184
|
|