@vibelet/cli 0.1.38 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -0
- package/bin/cloudflared-quick-tunnel.mjs +11 -0
- package/bin/cloudflared-resolver.mjs +171 -0
- package/bin/vibelet-runtime-policy.mjs +36 -0
- package/bin/vibelet.cjs +12 -0
- package/bin/vibelet.mjs +1062 -0
- package/dist/index.cjs +126 -0
- package/package.json +24 -22
- package/app.json +0 -5
- package/dist/advertised-hosts.d.ts +0 -34
- package/dist/advertised-hosts.d.ts.map +0 -1
- package/dist/advertised-hosts.js +0 -176
- package/dist/advertised-hosts.js.map +0 -1
- package/dist/advertised-hosts.test.d.ts +0 -2
- package/dist/advertised-hosts.test.d.ts.map +0 -1
- package/dist/advertised-hosts.test.js +0 -96
- package/dist/advertised-hosts.test.js.map +0 -1
- package/dist/audit.d.ts +0 -30
- package/dist/audit.d.ts.map +0 -1
- package/dist/audit.js +0 -73
- package/dist/audit.js.map +0 -1
- package/dist/audit.test.d.ts +0 -2
- package/dist/audit.test.d.ts.map +0 -1
- package/dist/audit.test.js +0 -33
- package/dist/audit.test.js.map +0 -1
- package/dist/auth.d.ts +0 -6
- package/dist/auth.d.ts.map +0 -1
- package/dist/auth.js +0 -27
- package/dist/auth.js.map +0 -1
- package/dist/claude-hooks.d.ts +0 -58
- package/dist/claude-hooks.d.ts.map +0 -1
- package/dist/claude-hooks.js +0 -129
- package/dist/claude-hooks.js.map +0 -1
- package/dist/cli-version.d.ts +0 -3
- package/dist/cli-version.d.ts.map +0 -1
- package/dist/cli-version.js +0 -35
- package/dist/cli-version.js.map +0 -1
- package/dist/cli-version.test.d.ts +0 -2
- package/dist/cli-version.test.d.ts.map +0 -1
- package/dist/cli-version.test.js +0 -38
- package/dist/cli-version.test.js.map +0 -1
- package/dist/config.d.ts +0 -30
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -327
- package/dist/config.js.map +0 -1
- package/dist/config.test.d.ts +0 -2
- package/dist/config.test.d.ts.map +0 -1
- package/dist/config.test.js +0 -184
- package/dist/config.test.js.map +0 -1
- package/dist/dev-auth.test.d.ts +0 -2
- package/dist/dev-auth.test.d.ts.map +0 -1
- package/dist/dev-auth.test.js +0 -154
- package/dist/dev-auth.test.js.map +0 -1
- package/dist/dev-script.test.d.ts +0 -2
- package/dist/dev-script.test.d.ts.map +0 -1
- package/dist/dev-script.test.js +0 -412
- package/dist/dev-script.test.js.map +0 -1
- package/dist/drivers/claude.d.ts +0 -34
- package/dist/drivers/claude.d.ts.map +0 -1
- package/dist/drivers/claude.js +0 -413
- package/dist/drivers/claude.js.map +0 -1
- package/dist/drivers/claude.test.d.ts +0 -2
- package/dist/drivers/claude.test.d.ts.map +0 -1
- package/dist/drivers/claude.test.js +0 -951
- package/dist/drivers/claude.test.js.map +0 -1
- package/dist/drivers/codex.d.ts +0 -38
- package/dist/drivers/codex.d.ts.map +0 -1
- package/dist/drivers/codex.js +0 -771
- package/dist/drivers/codex.js.map +0 -1
- package/dist/drivers/codex.test.d.ts +0 -2
- package/dist/drivers/codex.test.d.ts.map +0 -1
- package/dist/drivers/codex.test.js +0 -939
- package/dist/drivers/codex.test.js.map +0 -1
- package/dist/drivers/types.d.ts +0 -14
- package/dist/drivers/types.d.ts.map +0 -1
- package/dist/drivers/types.js +0 -2
- package/dist/drivers/types.js.map +0 -1
- package/dist/e2e.test.d.ts +0 -2
- package/dist/e2e.test.d.ts.map +0 -1
- package/dist/e2e.test.js +0 -111
- package/dist/e2e.test.js.map +0 -1
- package/dist/identity.d.ts +0 -10
- package/dist/identity.d.ts.map +0 -1
- package/dist/identity.js +0 -66
- package/dist/identity.js.map +0 -1
- package/dist/identity.test.d.ts +0 -2
- package/dist/identity.test.d.ts.map +0 -1
- package/dist/identity.test.js +0 -25
- package/dist/identity.test.js.map +0 -1
- package/dist/index-entry.test.d.ts +0 -2
- package/dist/index-entry.test.d.ts.map +0 -1
- package/dist/index-entry.test.js +0 -272
- package/dist/index-entry.test.js.map +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -707
- package/dist/index.js.map +0 -1
- package/dist/logger.d.ts +0 -31
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -75
- package/dist/logger.js.map +0 -1
- package/dist/metrics.d.ts +0 -52
- package/dist/metrics.d.ts.map +0 -1
- package/dist/metrics.js +0 -89
- package/dist/metrics.js.map +0 -1
- package/dist/pairing-store.d.ts +0 -29
- package/dist/pairing-store.d.ts.map +0 -1
- package/dist/pairing-store.js +0 -131
- package/dist/pairing-store.js.map +0 -1
- package/dist/pairing-store.test.d.ts +0 -2
- package/dist/pairing-store.test.d.ts.map +0 -1
- package/dist/pairing-store.test.js +0 -47
- package/dist/pairing-store.test.js.map +0 -1
- package/dist/paths.d.ts +0 -16
- package/dist/paths.d.ts.map +0 -1
- package/dist/paths.js +0 -18
- package/dist/paths.js.map +0 -1
- package/dist/perf-compare.d.ts +0 -13
- package/dist/perf-compare.d.ts.map +0 -1
- package/dist/perf-compare.js +0 -125
- package/dist/perf-compare.js.map +0 -1
- package/dist/port-conflict.d.ts +0 -9
- package/dist/port-conflict.d.ts.map +0 -1
- package/dist/port-conflict.js +0 -33
- package/dist/port-conflict.js.map +0 -1
- package/dist/port-conflict.test.d.ts +0 -2
- package/dist/port-conflict.test.d.ts.map +0 -1
- package/dist/port-conflict.test.js +0 -38
- package/dist/port-conflict.test.js.map +0 -1
- package/dist/process-scanner.d.ts +0 -43
- package/dist/process-scanner.d.ts.map +0 -1
- package/dist/process-scanner.js +0 -453
- package/dist/process-scanner.js.map +0 -1
- package/dist/process-scanner.perf.test.d.ts +0 -2
- package/dist/process-scanner.perf.test.d.ts.map +0 -1
- package/dist/process-scanner.perf.test.js +0 -186
- package/dist/process-scanner.perf.test.js.map +0 -1
- package/dist/process-scanner.test.d.ts +0 -2
- package/dist/process-scanner.test.d.ts.map +0 -1
- package/dist/process-scanner.test.js +0 -399
- package/dist/process-scanner.test.js.map +0 -1
- package/dist/push-protocol.d.ts +0 -15
- package/dist/push-protocol.d.ts.map +0 -1
- package/dist/push-protocol.js +0 -23
- package/dist/push-protocol.js.map +0 -1
- package/dist/push-protocol.test.d.ts +0 -2
- package/dist/push-protocol.test.d.ts.map +0 -1
- package/dist/push-protocol.test.js +0 -57
- package/dist/push-protocol.test.js.map +0 -1
- package/dist/push-store.d.ts +0 -22
- package/dist/push-store.d.ts.map +0 -1
- package/dist/push-store.js +0 -103
- package/dist/push-store.js.map +0 -1
- package/dist/push-store.test.d.ts +0 -2
- package/dist/push-store.test.d.ts.map +0 -1
- package/dist/push-store.test.js +0 -79
- package/dist/push-store.test.js.map +0 -1
- package/dist/push.d.ts +0 -65
- package/dist/push.d.ts.map +0 -1
- package/dist/push.js +0 -202
- package/dist/push.js.map +0 -1
- package/dist/push.test.d.ts +0 -2
- package/dist/push.test.d.ts.map +0 -1
- package/dist/push.test.js +0 -199
- package/dist/push.test.js.map +0 -1
- package/dist/safe-stdio.d.ts +0 -3
- package/dist/safe-stdio.d.ts.map +0 -1
- package/dist/safe-stdio.js +0 -46
- package/dist/safe-stdio.js.map +0 -1
- package/dist/scanner.d.ts +0 -30
- package/dist/scanner.d.ts.map +0 -1
- package/dist/scanner.js +0 -859
- package/dist/scanner.js.map +0 -1
- package/dist/scanner.perf.test.d.ts +0 -2
- package/dist/scanner.perf.test.d.ts.map +0 -1
- package/dist/scanner.perf.test.js +0 -320
- package/dist/scanner.perf.test.js.map +0 -1
- package/dist/scanner.test.d.ts +0 -2
- package/dist/scanner.test.d.ts.map +0 -1
- package/dist/scanner.test.js +0 -948
- package/dist/scanner.test.js.map +0 -1
- package/dist/session-inventory.d.ts +0 -63
- package/dist/session-inventory.d.ts.map +0 -1
- package/dist/session-inventory.js +0 -525
- package/dist/session-inventory.js.map +0 -1
- package/dist/session-inventory.perf.test.d.ts +0 -2
- package/dist/session-inventory.perf.test.d.ts.map +0 -1
- package/dist/session-inventory.perf.test.js +0 -220
- package/dist/session-inventory.perf.test.js.map +0 -1
- package/dist/session-inventory.test.d.ts +0 -2
- package/dist/session-inventory.test.d.ts.map +0 -1
- package/dist/session-inventory.test.js +0 -712
- package/dist/session-inventory.test.js.map +0 -1
- package/dist/session-manager.d.ts +0 -75
- package/dist/session-manager.d.ts.map +0 -1
- package/dist/session-manager.js +0 -1515
- package/dist/session-manager.js.map +0 -1
- package/dist/session-manager.test.d.ts +0 -2
- package/dist/session-manager.test.d.ts.map +0 -1
- package/dist/session-manager.test.js +0 -2861
- package/dist/session-manager.test.js.map +0 -1
- package/dist/session-store.d.ts +0 -42
- package/dist/session-store.d.ts.map +0 -1
- package/dist/session-store.js +0 -163
- package/dist/session-store.js.map +0 -1
- package/dist/session-store.test.d.ts +0 -2
- package/dist/session-store.test.d.ts.map +0 -1
- package/dist/session-store.test.js +0 -236
- package/dist/session-store.test.js.map +0 -1
- package/dist/session-title.d.ts +0 -6
- package/dist/session-title.d.ts.map +0 -1
- package/dist/session-title.js +0 -105
- package/dist/session-title.js.map +0 -1
- package/dist/session-title.perf.test.d.ts +0 -2
- package/dist/session-title.perf.test.d.ts.map +0 -1
- package/dist/session-title.perf.test.js +0 -99
- package/dist/session-title.perf.test.js.map +0 -1
- package/dist/session-title.test.d.ts +0 -2
- package/dist/session-title.test.d.ts.map +0 -1
- package/dist/session-title.test.js +0 -199
- package/dist/session-title.test.js.map +0 -1
- package/dist/shutdown-endpoint.test.d.ts +0 -2
- package/dist/shutdown-endpoint.test.d.ts.map +0 -1
- package/dist/shutdown-endpoint.test.js +0 -93
- package/dist/shutdown-endpoint.test.js.map +0 -1
- package/dist/storage-housekeeping.d.ts +0 -28
- package/dist/storage-housekeeping.d.ts.map +0 -1
- package/dist/storage-housekeeping.js +0 -76
- package/dist/storage-housekeeping.js.map +0 -1
- package/dist/storage-housekeeping.test.d.ts +0 -2
- package/dist/storage-housekeeping.test.d.ts.map +0 -1
- package/dist/storage-housekeeping.test.js +0 -65
- package/dist/storage-housekeeping.test.js.map +0 -1
- package/dist/test-daemon-harness.d.ts +0 -31
- package/dist/test-daemon-harness.d.ts.map +0 -1
- package/dist/test-daemon-harness.js +0 -337
- package/dist/test-daemon-harness.js.map +0 -1
- package/dist/token-auth.test.d.ts +0 -2
- package/dist/token-auth.test.d.ts.map +0 -1
- package/dist/token-auth.test.js +0 -52
- package/dist/token-auth.test.js.map +0 -1
- package/dist/utils.d.ts +0 -4
- package/dist/utils.d.ts.map +0 -1
- package/dist/utils.js +0 -40
- package/dist/utils.js.map +0 -1
- package/dist/utils.test.d.ts +0 -2
- package/dist/utils.test.d.ts.map +0 -1
- package/dist/utils.test.js +0 -54
- package/dist/utils.test.js.map +0 -1
- package/dist/ws-data.d.ts +0 -4
- package/dist/ws-data.d.ts.map +0 -1
- package/dist/ws-data.js +0 -20
- package/dist/ws-data.js.map +0 -1
- package/dist/ws-data.test.d.ts +0 -2
- package/dist/ws-data.test.d.ts.map +0 -1
- package/dist/ws-data.test.js +0 -17
- package/dist/ws-data.test.js.map +0 -1
- package/perf-reporter.mjs +0 -138
- package/scripts/build-release.mjs +0 -41
- package/scripts/dev.mjs +0 -537
- package/src/advertised-hosts.test.ts +0 -125
- package/src/advertised-hosts.ts +0 -225
- package/src/audit.test.ts +0 -38
- package/src/audit.ts +0 -117
- package/src/auth.ts +0 -31
- package/src/claude-hooks.ts +0 -195
- package/src/cli-version.test.ts +0 -36
- package/src/cli-version.ts +0 -46
- package/src/config.test.ts +0 -254
- package/src/config.ts +0 -324
- package/src/dev-auth.test.ts +0 -183
- package/src/dev-script.test.ts +0 -511
- package/src/drivers/claude.test.ts +0 -1186
- package/src/drivers/claude.ts +0 -443
- package/src/drivers/codex.test.ts +0 -1096
- package/src/drivers/codex.ts +0 -879
- package/src/drivers/types.ts +0 -15
- package/src/e2e.test.ts +0 -139
- package/src/identity.test.ts +0 -26
- package/src/identity.ts +0 -82
- package/src/index-entry.test.ts +0 -336
- package/src/index.ts +0 -781
- package/src/logger.ts +0 -112
- package/src/metrics.ts +0 -117
- package/src/pairing-store.test.ts +0 -53
- package/src/pairing-store.ts +0 -154
- package/src/paths.ts +0 -19
- package/src/perf-compare.ts +0 -164
- package/src/port-conflict.test.ts +0 -45
- package/src/port-conflict.ts +0 -44
- package/src/process-scanner.perf.test.ts +0 -222
- package/src/process-scanner.test.ts +0 -575
- package/src/process-scanner.ts +0 -514
- package/src/push-protocol.test.ts +0 -74
- package/src/push-protocol.ts +0 -36
- package/src/push-store.test.ts +0 -89
- package/src/push-store.ts +0 -126
- package/src/push.test.ts +0 -234
- package/src/push.ts +0 -318
- package/src/safe-stdio.ts +0 -51
- package/src/scanner.perf.test.ts +0 -359
- package/src/scanner.test.ts +0 -1045
- package/src/scanner.ts +0 -924
- package/src/session-inventory.perf.test.ts +0 -250
- package/src/session-inventory.test.ts +0 -1002
- package/src/session-inventory.ts +0 -721
- package/src/session-manager.test.ts +0 -3430
- package/src/session-manager.ts +0 -1775
- package/src/session-store.test.ts +0 -276
- package/src/session-store.ts +0 -202
- package/src/session-title.perf.test.ts +0 -118
- package/src/session-title.test.ts +0 -286
- package/src/session-title.ts +0 -108
- package/src/shutdown-endpoint.test.ts +0 -95
- package/src/storage-housekeeping.test.ts +0 -78
- package/src/storage-housekeeping.ts +0 -111
- package/src/test-daemon-harness.ts +0 -410
- package/src/token-auth.test.ts +0 -67
- package/src/utils.test.ts +0 -65
- package/src/utils.ts +0 -47
- package/src/ws-data.test.ts +0 -20
- package/src/ws-data.ts +0 -26
- package/tsconfig.json +0 -12
package/dist/session-manager.js
DELETED
|
@@ -1,1515 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'crypto';
|
|
2
|
-
import { stat } from 'fs/promises';
|
|
3
|
-
import { ClaudeDriver, isClaudeSyntheticApprovalRequestId } from './drivers/claude.js';
|
|
4
|
-
import { CodexDriver } from './drivers/codex.js';
|
|
5
|
-
import { listSessions, readSessionHistory, readSessionRuntimeHints, invalidateScannerCache, } from './scanner.js';
|
|
6
|
-
import { invalidateProcessScanCache } from './process-scanner.js';
|
|
7
|
-
import { listSessionPage, onExternalInventoryBackfill, } from './session-inventory.js';
|
|
8
|
-
import { SessionStore } from './session-store.js';
|
|
9
|
-
import { expandPath } from './utils.js';
|
|
10
|
-
import { isFallbackSessionTitle, titleFromFirstSentence } from './session-title.js';
|
|
11
|
-
import { logger as rootLogger } from './logger.js';
|
|
12
|
-
import { metrics } from './metrics.js';
|
|
13
|
-
import { audit } from './audit.js';
|
|
14
|
-
import { sendPush } from './push.js';
|
|
15
|
-
import { config } from './config.js';
|
|
16
|
-
import { CLAUDE_HOOK_APPROVAL_PREFIX, DEFAULT_CLAUDE_PERMISSION_HOOK_RESPONSE, } from './claude-hooks.js';
|
|
17
|
-
const log = rootLogger.child({ module: 'manager' });
|
|
18
|
-
const DEFAULT_PUSH_BODY = 'Done.';
|
|
19
|
-
const APPROVAL_PUSH_TITLE = 'Approval required';
|
|
20
|
-
const MAX_PUSH_BODY_LENGTH = 180;
|
|
21
|
-
const MAX_ACCEPTED_CLIENT_MESSAGE_IDS = 200;
|
|
22
|
-
function mergeAcceptedClientMessageIds(...groups) {
|
|
23
|
-
const merged = [];
|
|
24
|
-
for (const group of groups) {
|
|
25
|
-
if (!group?.length)
|
|
26
|
-
continue;
|
|
27
|
-
for (const value of group) {
|
|
28
|
-
if (!value || merged.includes(value))
|
|
29
|
-
continue;
|
|
30
|
-
merged.push(value);
|
|
31
|
-
if (merged.length > MAX_ACCEPTED_CLIENT_MESSAGE_IDS) {
|
|
32
|
-
merged.splice(0, merged.length - MAX_ACCEPTED_CLIENT_MESSAGE_IDS);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return merged;
|
|
37
|
-
}
|
|
38
|
-
function acceptedClientMessageIdsPayload(acceptedClientMessageIds) {
|
|
39
|
-
return acceptedClientMessageIds?.length
|
|
40
|
-
? { acceptedClientMessageIds: mergeAcceptedClientMessageIds(acceptedClientMessageIds) }
|
|
41
|
-
: {};
|
|
42
|
-
}
|
|
43
|
-
function buildRecord(session) {
|
|
44
|
-
return {
|
|
45
|
-
sessionId: session.sessionId,
|
|
46
|
-
agent: session.agent,
|
|
47
|
-
cwd: session.cwd,
|
|
48
|
-
approvalMode: session.approvalMode,
|
|
49
|
-
acceptedClientMessageIds: mergeAcceptedClientMessageIds(session.acceptedClientMessageIds),
|
|
50
|
-
pendingApproval: session.agent === 'codex' ? session.pendingApproval : undefined,
|
|
51
|
-
title: session.title,
|
|
52
|
-
createdAt: session.createdAt,
|
|
53
|
-
lastActivityAt: session.lastActivityAt,
|
|
54
|
-
managed: session.managed,
|
|
55
|
-
isResponding: session.isResponding || undefined,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
function hasSyntheticPendingApproval(session) {
|
|
59
|
-
const requestId = session.pendingApproval?.requestId;
|
|
60
|
-
return typeof requestId === 'string' && isClaudeSyntheticApprovalRequestId(requestId);
|
|
61
|
-
}
|
|
62
|
-
function buildPendingApprovalSnapshot(sessionId, agent, pendingApproval) {
|
|
63
|
-
return {
|
|
64
|
-
sessionId,
|
|
65
|
-
agent,
|
|
66
|
-
...pendingApproval,
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
function isClaudeHookApprovalRequestId(requestId) {
|
|
70
|
-
return requestId.startsWith(CLAUDE_HOOK_APPROVAL_PREFIX);
|
|
71
|
-
}
|
|
72
|
-
function buildClaudeHookDecisionResponse(approved) {
|
|
73
|
-
return approved
|
|
74
|
-
? {
|
|
75
|
-
continue: true,
|
|
76
|
-
suppressOutput: true,
|
|
77
|
-
hookSpecificOutput: {
|
|
78
|
-
hookEventName: 'PreToolUse',
|
|
79
|
-
permissionDecision: 'allow',
|
|
80
|
-
},
|
|
81
|
-
}
|
|
82
|
-
: {
|
|
83
|
-
continue: true,
|
|
84
|
-
suppressOutput: true,
|
|
85
|
-
hookSpecificOutput: {
|
|
86
|
-
hookEventName: 'PreToolUse',
|
|
87
|
-
permissionDecision: 'deny',
|
|
88
|
-
permissionDecisionReason: 'Denied from Vibelet',
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
export class SessionManager {
|
|
93
|
-
pushSender;
|
|
94
|
-
sessions = new Map();
|
|
95
|
-
claudeHookSessions = new Map();
|
|
96
|
-
store = new SessionStore();
|
|
97
|
-
idleSweepInterval = null;
|
|
98
|
-
/** All authenticated WebSocket clients, used for global broadcasts (e.g. approval requests). */
|
|
99
|
-
globalClients = new Set();
|
|
100
|
-
inventoryVersion = 0;
|
|
101
|
-
removeInventoryBackfillListener;
|
|
102
|
-
constructor(pushSender = sendPush) {
|
|
103
|
-
this.pushSender = pushSender;
|
|
104
|
-
this.removeInventoryBackfillListener = onExternalInventoryBackfill(() => {
|
|
105
|
-
this.noteInventoryChanged('inventory_backfilled');
|
|
106
|
-
});
|
|
107
|
-
this.startIdleSweep();
|
|
108
|
-
}
|
|
109
|
-
/** Start the periodic idle-session sweep (every 60s). */
|
|
110
|
-
startIdleSweep() {
|
|
111
|
-
if (config.idleTimeoutMs <= 0 && config.turnStallTimeoutMs <= 0)
|
|
112
|
-
return;
|
|
113
|
-
this.idleSweepInterval = setInterval(() => this.sweepIdleSessions(), 60_000);
|
|
114
|
-
this.idleSweepInterval.unref();
|
|
115
|
-
}
|
|
116
|
-
/** Stop the idle sweep interval. */
|
|
117
|
-
stopIdleSweep() {
|
|
118
|
-
if (this.idleSweepInterval) {
|
|
119
|
-
clearInterval(this.idleSweepInterval);
|
|
120
|
-
this.idleSweepInterval = null;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
/** Check all active sessions and stop drivers that have been idle too long. */
|
|
124
|
-
sweepIdleSessions() {
|
|
125
|
-
const now = Date.now();
|
|
126
|
-
const idleTimeoutMs = config.idleTimeoutMs;
|
|
127
|
-
const turnStallTimeoutMs = config.turnStallTimeoutMs;
|
|
128
|
-
if (idleTimeoutMs <= 0 && turnStallTimeoutMs <= 0)
|
|
129
|
-
return;
|
|
130
|
-
const INACTIVE_CLEANUP_MS = 60 * 60 * 1000; // 1 hour
|
|
131
|
-
for (const [id, session] of this.sessions) {
|
|
132
|
-
// Clean up inactive sessions (no driver, no clients) after 1 hour
|
|
133
|
-
if (!session.active && !session.driver) {
|
|
134
|
-
if (session.clients.size === 0) {
|
|
135
|
-
const inactiveMs = now - session.lastActivityTs;
|
|
136
|
-
if (inactiveMs >= INACTIVE_CLEANUP_MS) {
|
|
137
|
-
log.info({ sessionId: session.sessionId, inactiveMs }, 'removing inactive session from memory');
|
|
138
|
-
metrics.increment('session.cleanup', { reason: 'inactive' });
|
|
139
|
-
this.unregisterClaudeHookSession(session);
|
|
140
|
-
this.sessions.delete(id);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
if (!session.active || !session.driver)
|
|
146
|
-
continue;
|
|
147
|
-
const inactiveMs = now - session.lastActivityTs;
|
|
148
|
-
if (session.isResponding) {
|
|
149
|
-
if (turnStallTimeoutMs <= 0 || session.pendingApproval)
|
|
150
|
-
continue;
|
|
151
|
-
if (inactiveMs < turnStallTimeoutMs)
|
|
152
|
-
continue;
|
|
153
|
-
log.warn({
|
|
154
|
-
sessionId: session.sessionId,
|
|
155
|
-
agent: session.agent,
|
|
156
|
-
inactiveMs,
|
|
157
|
-
turnStallTimeoutMs,
|
|
158
|
-
}, 'stopping stalled driver');
|
|
159
|
-
metrics.increment('driver.stall_timeout', { agent: session.agent });
|
|
160
|
-
audit.emit('driver.stall_timeout', {
|
|
161
|
-
sessionId: session.sessionId,
|
|
162
|
-
agent: session.agent,
|
|
163
|
-
idleMs: inactiveMs,
|
|
164
|
-
});
|
|
165
|
-
// Kill the stalled driver; user's next message will auto-reconnect
|
|
166
|
-
this.resolvePendingClaudeHookApprovals(session);
|
|
167
|
-
session.driver.stop();
|
|
168
|
-
session.active = false;
|
|
169
|
-
session.driver = null;
|
|
170
|
-
session.isResponding = false;
|
|
171
|
-
session.currentReplyText = '';
|
|
172
|
-
this.updateGauges();
|
|
173
|
-
this.broadcast(session.sessionId, {
|
|
174
|
-
type: 'error',
|
|
175
|
-
sessionId: session.sessionId,
|
|
176
|
-
message: `Agent stopped responding for over ${Math.ceil(turnStallTimeoutMs / 1000)}s. Send a new message to continue.`,
|
|
177
|
-
});
|
|
178
|
-
this.touchSession(session.sessionId);
|
|
179
|
-
this.noteInventoryChanged('session_updated');
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
|
-
if (session.pendingApproval)
|
|
183
|
-
continue;
|
|
184
|
-
if (idleTimeoutMs <= 0 || inactiveMs < idleTimeoutMs)
|
|
185
|
-
continue;
|
|
186
|
-
log.info({ sessionId: session.sessionId, agent: session.agent, idleMs: inactiveMs, timeoutMs: idleTimeoutMs }, 'stopping idle driver');
|
|
187
|
-
metrics.increment('driver.idle_timeout', { agent: session.agent });
|
|
188
|
-
audit.emit('driver.idle_timeout', {
|
|
189
|
-
sessionId: session.sessionId,
|
|
190
|
-
agent: session.agent,
|
|
191
|
-
idleMs: inactiveMs,
|
|
192
|
-
});
|
|
193
|
-
session.driver.stop();
|
|
194
|
-
session.active = false;
|
|
195
|
-
session.driver = null;
|
|
196
|
-
this.updateGauges();
|
|
197
|
-
this.touchSession(session.sessionId);
|
|
198
|
-
this.noteInventoryChanged('session_updated');
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
bindDriverLifecycle(session, agent, context, ws) {
|
|
202
|
-
session.driver?.onMessage((msg) => {
|
|
203
|
-
log.debug({ agent, context, msgType: msg.type, sessionId: session.sessionId }, 'driver message received');
|
|
204
|
-
if (msg.type !== 'response'
|
|
205
|
-
&& 'sessionId' in msg
|
|
206
|
-
&& msg.sessionId
|
|
207
|
-
&& msg.sessionId !== session.sessionId
|
|
208
|
-
&& !this.sessions.has(msg.sessionId)) {
|
|
209
|
-
this.remapSessionId(session, msg.sessionId, ws);
|
|
210
|
-
}
|
|
211
|
-
if (msg.type === 'session.done' || msg.type === 'session.interrupted') {
|
|
212
|
-
session.isResponding = false;
|
|
213
|
-
if (msg.type === 'session.interrupted' || !hasSyntheticPendingApproval(session)) {
|
|
214
|
-
session.pendingApproval = undefined;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
if (msg.type === 'approval.request') {
|
|
218
|
-
if (agent === 'codex' && session.approvalMode === 'autoApprove') {
|
|
219
|
-
const sent = session.driver?.respondApproval(msg.requestId, true) ?? false;
|
|
220
|
-
if (sent) {
|
|
221
|
-
audit.emit('approval.response', { sessionId: session.sessionId, requestId: msg.requestId, approved: true });
|
|
222
|
-
session.pendingApproval = undefined;
|
|
223
|
-
session.isResponding = true;
|
|
224
|
-
this.touchSession(session.sessionId);
|
|
225
|
-
this.noteInventoryChanged('session_updated');
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
log.warn({ sessionId: session.sessionId, requestId: msg.requestId }, 'failed to auto-approve codex request; falling back to pending approval flow');
|
|
229
|
-
}
|
|
230
|
-
session.isResponding = false;
|
|
231
|
-
session.pendingApproval = {
|
|
232
|
-
requestId: msg.requestId,
|
|
233
|
-
toolName: msg.toolName,
|
|
234
|
-
input: msg.input,
|
|
235
|
-
description: msg.description,
|
|
236
|
-
...(msg.approvalContext ? { approvalContext: msg.approvalContext } : {}),
|
|
237
|
-
};
|
|
238
|
-
void this.pushSender(APPROVAL_PUSH_TITLE, this.buildPushBody(`${session.title || session.sessionId}: ${msg.description || msg.toolName}`), {
|
|
239
|
-
sessionId: session.sessionId,
|
|
240
|
-
agent: session.agent,
|
|
241
|
-
requestId: msg.requestId,
|
|
242
|
-
eventType: 'approval_request',
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
if (agent === 'claude' && msg.type === 'approval.request' && isClaudeSyntheticApprovalRequestId(msg.requestId)) {
|
|
246
|
-
if (session.lastUserMessage) {
|
|
247
|
-
session.syntheticApprovalRetries[msg.requestId] = {
|
|
248
|
-
message: session.lastUserMessage,
|
|
249
|
-
toolName: msg.toolName,
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
else {
|
|
253
|
-
log.warn({ sessionId: session.sessionId, requestId: msg.requestId }, 'missing lastUserMessage for synthetic approval retry');
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
this.touchSession(session.sessionId, msg.type !== 'text.delta');
|
|
257
|
-
if (msg.type === 'approval.request' || msg.type === 'session.done' || msg.type === 'session.interrupted') {
|
|
258
|
-
this.noteInventoryChanged('session_updated');
|
|
259
|
-
}
|
|
260
|
-
this.broadcast(session.sessionId, msg);
|
|
261
|
-
if ((msg.type === 'session.done' || msg.type === 'session.interrupted') && session.bufferedPrompts.length > 0) {
|
|
262
|
-
this.flushBufferedPrompt(session);
|
|
263
|
-
}
|
|
264
|
-
});
|
|
265
|
-
session.driver?.onExit?.((code) => {
|
|
266
|
-
log.info({ agent, context, exitCode: code, sessionId: session.sessionId }, 'driver exited');
|
|
267
|
-
audit.emit('driver.exit', { sessionId: session.sessionId, agent, exitCode: code });
|
|
268
|
-
metrics.increment('driver.exit', { agent, abnormal: code && code !== 0 ? 'true' : 'false' });
|
|
269
|
-
const preservePendingApproval = Boolean(session.pendingApproval && (session.agent === 'codex' || (code === 0 && hasSyntheticPendingApproval(session))));
|
|
270
|
-
this.resolvePendingClaudeHookApprovals(session);
|
|
271
|
-
session.active = false;
|
|
272
|
-
session.driver = null;
|
|
273
|
-
session.isResponding = false;
|
|
274
|
-
session.currentReplyText = '';
|
|
275
|
-
if (!preservePendingApproval) {
|
|
276
|
-
session.pendingApproval = undefined;
|
|
277
|
-
}
|
|
278
|
-
this.updateGauges();
|
|
279
|
-
this.touchSession(session.sessionId);
|
|
280
|
-
this.noteInventoryChanged('session_updated');
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
remapSessionId(session, newSessionId, ws) {
|
|
284
|
-
if (!newSessionId || newSessionId === session.sessionId)
|
|
285
|
-
return;
|
|
286
|
-
const existing = this.sessions.get(newSessionId);
|
|
287
|
-
if (existing && existing !== session) {
|
|
288
|
-
log.warn({ oldSessionId: session.sessionId, newSessionId }, 'skipping session ID remap because target already exists');
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
const oldId = session.sessionId;
|
|
292
|
-
log.info({ oldSessionId: oldId, newSessionId }, 'session ID updated');
|
|
293
|
-
this.store.remove(oldId);
|
|
294
|
-
this.sessions.delete(oldId);
|
|
295
|
-
session.sessionId = newSessionId;
|
|
296
|
-
this.sessions.set(newSessionId, session);
|
|
297
|
-
this.store.upsert(buildRecord(session));
|
|
298
|
-
this.noteInventoryChanged('session_remapped');
|
|
299
|
-
if (ws) {
|
|
300
|
-
this.reply(ws, `id_update_${Date.now()}`, true, { sessionId: newSessionId, oldSessionId: oldId });
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
flushBufferedPrompt(session, force = false) {
|
|
304
|
-
if (!session.driver || !session.active || session.startupInProgress || session.pendingApproval) {
|
|
305
|
-
return false;
|
|
306
|
-
}
|
|
307
|
-
if (!force && session.isResponding) {
|
|
308
|
-
return false;
|
|
309
|
-
}
|
|
310
|
-
const nextPrompt = session.bufferedPrompts.shift();
|
|
311
|
-
if (!nextPrompt) {
|
|
312
|
-
return false;
|
|
313
|
-
}
|
|
314
|
-
session.isResponding = true;
|
|
315
|
-
session.currentReplyText = '';
|
|
316
|
-
session.lastUserMessage = nextPrompt;
|
|
317
|
-
this.touchSession(session.sessionId);
|
|
318
|
-
this.noteInventoryChanged('session_updated');
|
|
319
|
-
session.driver.sendPrompt(nextPrompt);
|
|
320
|
-
return true;
|
|
321
|
-
}
|
|
322
|
-
startPendingCodexSession(session, ws) {
|
|
323
|
-
if (session.agent !== 'codex' || !session.driver) {
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
const driver = session.driver;
|
|
327
|
-
const pendingSessionId = session.sessionId;
|
|
328
|
-
const startToken = session.startupToken;
|
|
329
|
-
const endTimer = metrics.startTimer('driver.spawn');
|
|
330
|
-
void (async () => {
|
|
331
|
-
try {
|
|
332
|
-
const actualSessionId = await driver.start(session.cwd, undefined, session.approvalMode);
|
|
333
|
-
const spawnMs = endTimer();
|
|
334
|
-
const current = this.sessions.get(pendingSessionId);
|
|
335
|
-
if (current !== session || session.startupToken !== startToken || session.driver !== driver) {
|
|
336
|
-
log.info({ pendingSessionId, actualSessionId }, 'discarding stale codex startup result');
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
session.startupInProgress = false;
|
|
340
|
-
log.info({ pendingSessionId, sessionId: actualSessionId, spawnMs }, 'codex session ready');
|
|
341
|
-
if (actualSessionId && actualSessionId !== pendingSessionId) {
|
|
342
|
-
this.remapSessionId(session, actualSessionId, ws);
|
|
343
|
-
}
|
|
344
|
-
this.flushBufferedPrompt(session, true);
|
|
345
|
-
}
|
|
346
|
-
catch (e) {
|
|
347
|
-
endTimer();
|
|
348
|
-
const current = this.sessions.get(pendingSessionId);
|
|
349
|
-
if (current !== session || session.startupToken !== startToken) {
|
|
350
|
-
log.info({ pendingSessionId, error: String(e) }, 'ignoring stale codex startup failure');
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
session.startupInProgress = false;
|
|
354
|
-
session.bufferedPrompts = [];
|
|
355
|
-
session.active = false;
|
|
356
|
-
session.driver = null;
|
|
357
|
-
session.isResponding = false;
|
|
358
|
-
session.currentReplyText = '';
|
|
359
|
-
this.updateGauges();
|
|
360
|
-
this.touchSession(session.sessionId);
|
|
361
|
-
this.noteInventoryChanged('session_updated');
|
|
362
|
-
log.error({ sessionId: pendingSessionId, cwd: session.cwd, error: String(e) }, 'async codex createSession error');
|
|
363
|
-
this.broadcast(session.sessionId, {
|
|
364
|
-
type: 'error',
|
|
365
|
-
sessionId: session.sessionId,
|
|
366
|
-
message: `Failed to start Codex session: ${String(e)}`,
|
|
367
|
-
});
|
|
368
|
-
}
|
|
369
|
-
})();
|
|
370
|
-
}
|
|
371
|
-
configureDriverBeforeStart(agent, driver, existingSecret) {
|
|
372
|
-
if (agent !== 'claude' || !(driver instanceof ClaudeDriver))
|
|
373
|
-
return undefined;
|
|
374
|
-
const secret = existingSecret ?? randomUUID().replace(/-/g, '');
|
|
375
|
-
driver.configureHookBridge(config.port, secret);
|
|
376
|
-
return secret;
|
|
377
|
-
}
|
|
378
|
-
registerClaudeHookSession(session) {
|
|
379
|
-
if (session.agent !== 'claude' || !session.claudeHookSecret)
|
|
380
|
-
return;
|
|
381
|
-
this.claudeHookSessions.set(session.claudeHookSecret, session);
|
|
382
|
-
}
|
|
383
|
-
unregisterClaudeHookSession(session) {
|
|
384
|
-
if (session.agent !== 'claude' || !session.claudeHookSecret)
|
|
385
|
-
return;
|
|
386
|
-
const current = this.claudeHookSessions.get(session.claudeHookSecret);
|
|
387
|
-
if (current === session) {
|
|
388
|
-
this.claudeHookSessions.delete(session.claudeHookSecret);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
resolvePendingClaudeHookApprovals(session, response = DEFAULT_CLAUDE_PERMISSION_HOOK_RESPONSE) {
|
|
392
|
-
for (const pending of session.pendingClaudeHookApprovals.values()) {
|
|
393
|
-
pending.resolve(response);
|
|
394
|
-
}
|
|
395
|
-
session.pendingClaudeHookApprovals.clear();
|
|
396
|
-
if (session.pendingApproval && isClaudeHookApprovalRequestId(session.pendingApproval.requestId)) {
|
|
397
|
-
session.pendingApproval = undefined;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
resolveClaudeHookSession(secret) {
|
|
401
|
-
if (!secret)
|
|
402
|
-
return undefined;
|
|
403
|
-
const normalized = secret.trim();
|
|
404
|
-
if (!normalized)
|
|
405
|
-
return undefined;
|
|
406
|
-
return this.claudeHookSessions.get(normalized);
|
|
407
|
-
}
|
|
408
|
-
handleClaudeSessionStartHook(secret, _data) {
|
|
409
|
-
const session = this.resolveClaudeHookSession(secret);
|
|
410
|
-
if (!session) {
|
|
411
|
-
log.warn({ secretPresent: Boolean(secret) }, 'received Claude session-start hook for unknown session');
|
|
412
|
-
return false;
|
|
413
|
-
}
|
|
414
|
-
this.touchSession(session.sessionId);
|
|
415
|
-
return true;
|
|
416
|
-
}
|
|
417
|
-
async handleClaudePermissionHook(secret, data) {
|
|
418
|
-
const session = this.resolveClaudeHookSession(secret);
|
|
419
|
-
if (!session || session.agent !== 'claude' || !session.active) {
|
|
420
|
-
log.warn({ secretPresent: Boolean(secret) }, 'received Claude permission hook for unknown or inactive session');
|
|
421
|
-
return DEFAULT_CLAUDE_PERMISSION_HOOK_RESPONSE;
|
|
422
|
-
}
|
|
423
|
-
if (session.approvalMode === 'autoApprove') {
|
|
424
|
-
return buildClaudeHookDecisionResponse(true);
|
|
425
|
-
}
|
|
426
|
-
const rawToolName = typeof data.tool_name === 'string'
|
|
427
|
-
? data.tool_name
|
|
428
|
-
: typeof data.toolName === 'string'
|
|
429
|
-
? data.toolName
|
|
430
|
-
: '';
|
|
431
|
-
const toolName = rawToolName.trim();
|
|
432
|
-
if (!toolName) {
|
|
433
|
-
return DEFAULT_CLAUDE_PERMISSION_HOOK_RESPONSE;
|
|
434
|
-
}
|
|
435
|
-
const toolInput = data.tool_input ?? data.toolInput;
|
|
436
|
-
const input = toolInput && typeof toolInput === 'object' && !Array.isArray(toolInput)
|
|
437
|
-
? toolInput
|
|
438
|
-
: {};
|
|
439
|
-
const rawToolUseId = typeof data.tool_use_id === 'string'
|
|
440
|
-
? data.tool_use_id
|
|
441
|
-
: typeof data.toolUseId === 'string'
|
|
442
|
-
? data.toolUseId
|
|
443
|
-
: '';
|
|
444
|
-
const requestId = `${CLAUDE_HOOK_APPROVAL_PREFIX}${rawToolUseId.trim() || randomUUID()}`;
|
|
445
|
-
const existing = session.pendingClaudeHookApprovals.get(requestId);
|
|
446
|
-
if (existing) {
|
|
447
|
-
return existing.promise;
|
|
448
|
-
}
|
|
449
|
-
const description = `Claude requested permissions to use ${toolName}.`;
|
|
450
|
-
let resolvePending;
|
|
451
|
-
const promise = new Promise((resolve) => {
|
|
452
|
-
resolvePending = resolve;
|
|
453
|
-
});
|
|
454
|
-
session.pendingClaudeHookApprovals.set(requestId, {
|
|
455
|
-
requestId,
|
|
456
|
-
promise,
|
|
457
|
-
resolve: (response) => {
|
|
458
|
-
session.pendingClaudeHookApprovals.delete(requestId);
|
|
459
|
-
resolvePending(response);
|
|
460
|
-
},
|
|
461
|
-
});
|
|
462
|
-
session.pendingApproval = {
|
|
463
|
-
requestId,
|
|
464
|
-
toolName,
|
|
465
|
-
input,
|
|
466
|
-
description,
|
|
467
|
-
};
|
|
468
|
-
audit.emit('approval.request', { agent: 'claude', sessionId: session.sessionId, toolName });
|
|
469
|
-
this.touchSession(session.sessionId);
|
|
470
|
-
this.noteInventoryChanged('session_updated');
|
|
471
|
-
this.broadcast(session.sessionId, {
|
|
472
|
-
type: 'approval.request',
|
|
473
|
-
sessionId: session.sessionId,
|
|
474
|
-
requestId,
|
|
475
|
-
toolName,
|
|
476
|
-
input,
|
|
477
|
-
description,
|
|
478
|
-
});
|
|
479
|
-
return promise;
|
|
480
|
-
}
|
|
481
|
-
async resolveReconnectSession(sessionId, fallbackAgent, existingSession) {
|
|
482
|
-
if (this.isDeletedSession(sessionId)) {
|
|
483
|
-
return undefined;
|
|
484
|
-
}
|
|
485
|
-
const now = new Date().toISOString();
|
|
486
|
-
let resolvedAgent = existingSession?.agent;
|
|
487
|
-
let resolvedCwd = existingSession?.cwd ?? '';
|
|
488
|
-
let resolvedApprovalMode = existingSession?.approvalMode;
|
|
489
|
-
let resolvedPendingApproval = existingSession?.pendingApproval;
|
|
490
|
-
let resolvedTitle = existingSession?.title ?? '';
|
|
491
|
-
let resolvedCreatedAt = existingSession?.createdAt ?? now;
|
|
492
|
-
let resolvedLastActivityAt = existingSession?.lastActivityAt ?? now;
|
|
493
|
-
let resolvedAcceptedClientMessageIds = mergeAcceptedClientMessageIds(existingSession?.acceptedClientMessageIds);
|
|
494
|
-
let resolvedIsResponding = existingSession?.isResponding;
|
|
495
|
-
let source = existingSession ? 'memory' : 'client';
|
|
496
|
-
const record = this.store.find(sessionId);
|
|
497
|
-
if (record) {
|
|
498
|
-
resolvedAgent = record.agent;
|
|
499
|
-
resolvedCwd = record.cwd || resolvedCwd;
|
|
500
|
-
resolvedApprovalMode = record.approvalMode ?? resolvedApprovalMode;
|
|
501
|
-
resolvedPendingApproval = record.pendingApproval ?? resolvedPendingApproval;
|
|
502
|
-
resolvedTitle = record.title || resolvedTitle;
|
|
503
|
-
resolvedCreatedAt = record.createdAt || resolvedCreatedAt;
|
|
504
|
-
resolvedLastActivityAt = record.lastActivityAt || resolvedLastActivityAt;
|
|
505
|
-
resolvedAcceptedClientMessageIds = mergeAcceptedClientMessageIds(record.acceptedClientMessageIds, resolvedAcceptedClientMessageIds);
|
|
506
|
-
source = 'record';
|
|
507
|
-
}
|
|
508
|
-
else if (!existingSession) {
|
|
509
|
-
const scanned = await listSessions(fallbackAgent ?? resolvedAgent);
|
|
510
|
-
const found = scanned.find((candidate) => candidate.sessionId === sessionId);
|
|
511
|
-
if (found) {
|
|
512
|
-
resolvedAgent = found.agent;
|
|
513
|
-
resolvedCwd = found.cwd || resolvedCwd;
|
|
514
|
-
resolvedTitle = found.title ?? resolvedTitle;
|
|
515
|
-
resolvedCreatedAt = found.createdAt;
|
|
516
|
-
resolvedLastActivityAt = found.lastActivityAt;
|
|
517
|
-
resolvedIsResponding = found.runtime.isResponding ?? resolvedIsResponding;
|
|
518
|
-
source = 'scanner';
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
if (!resolvedAgent && fallbackAgent) {
|
|
522
|
-
resolvedAgent = fallbackAgent;
|
|
523
|
-
source = existingSession ? 'memory' : 'client';
|
|
524
|
-
}
|
|
525
|
-
if (!resolvedAgent) {
|
|
526
|
-
return undefined;
|
|
527
|
-
}
|
|
528
|
-
return {
|
|
529
|
-
agent: resolvedAgent,
|
|
530
|
-
cwd: resolvedCwd,
|
|
531
|
-
approvalMode: resolvedApprovalMode,
|
|
532
|
-
pendingApproval: resolvedPendingApproval,
|
|
533
|
-
title: resolvedTitle,
|
|
534
|
-
createdAt: resolvedCreatedAt,
|
|
535
|
-
lastActivityAt: resolvedLastActivityAt,
|
|
536
|
-
acceptedClientMessageIds: resolvedAcceptedClientMessageIds,
|
|
537
|
-
source,
|
|
538
|
-
managed: existingSession?.managed ?? record?.managed,
|
|
539
|
-
isResponding: resolvedIsResponding || undefined,
|
|
540
|
-
};
|
|
541
|
-
}
|
|
542
|
-
collectReconnectPendingApprovals() {
|
|
543
|
-
const approvals = [];
|
|
544
|
-
const seen = new Set();
|
|
545
|
-
for (const session of this.sessions.values()) {
|
|
546
|
-
if (!session.pendingApproval)
|
|
547
|
-
continue;
|
|
548
|
-
const key = `${session.agent}:${session.sessionId}:${session.pendingApproval.requestId}`;
|
|
549
|
-
if (seen.has(key))
|
|
550
|
-
continue;
|
|
551
|
-
seen.add(key);
|
|
552
|
-
approvals.push(buildPendingApprovalSnapshot(session.sessionId, session.agent, session.pendingApproval));
|
|
553
|
-
}
|
|
554
|
-
for (const record of this.store.getAll()) {
|
|
555
|
-
if (!record.pendingApproval)
|
|
556
|
-
continue;
|
|
557
|
-
const key = `${record.agent}:${record.sessionId}:${record.pendingApproval.requestId}`;
|
|
558
|
-
if (seen.has(key))
|
|
559
|
-
continue;
|
|
560
|
-
seen.add(key);
|
|
561
|
-
approvals.push(buildPendingApprovalSnapshot(record.sessionId, record.agent, record.pendingApproval));
|
|
562
|
-
}
|
|
563
|
-
return approvals;
|
|
564
|
-
}
|
|
565
|
-
restoreDriverPendingApproval(driver, pendingApproval) {
|
|
566
|
-
if (!pendingApproval)
|
|
567
|
-
return;
|
|
568
|
-
driver.restorePendingApproval?.(pendingApproval);
|
|
569
|
-
}
|
|
570
|
-
async reviveSessionForApproval(session) {
|
|
571
|
-
if (session.driver)
|
|
572
|
-
return true;
|
|
573
|
-
if (!session.pendingApproval || session.agent !== 'codex')
|
|
574
|
-
return false;
|
|
575
|
-
const driver = this.createDriver(session.agent);
|
|
576
|
-
const claudeHookSecret = this.configureDriverBeforeStart(session.agent, driver, session.claudeHookSecret);
|
|
577
|
-
await driver.start(session.cwd, session.sessionId, session.approvalMode);
|
|
578
|
-
this.restoreDriverPendingApproval(driver, session.pendingApproval);
|
|
579
|
-
session.driver = driver;
|
|
580
|
-
session.active = true;
|
|
581
|
-
session.isResponding = false;
|
|
582
|
-
session.currentReplyText = '';
|
|
583
|
-
session.startupInProgress = false;
|
|
584
|
-
session.bufferedPrompts = session.bufferedPrompts ?? [];
|
|
585
|
-
session.startupToken = session.startupToken ?? 0;
|
|
586
|
-
session.claudeHookSecret = claudeHookSecret ?? session.claudeHookSecret;
|
|
587
|
-
session.pendingClaudeHookApprovals = session.pendingClaudeHookApprovals ?? new Map();
|
|
588
|
-
this.registerClaudeHookSession(session);
|
|
589
|
-
this.bindDriverLifecycle(session, session.agent, ' (approval resumed)');
|
|
590
|
-
this.store.upsert(buildRecord(session));
|
|
591
|
-
this.updateGauges();
|
|
592
|
-
this.touchSession(session.sessionId);
|
|
593
|
-
this.noteInventoryChanged('session_updated');
|
|
594
|
-
return true;
|
|
595
|
-
}
|
|
596
|
-
buildPushBody(replyText) {
|
|
597
|
-
const collapsed = replyText.replace(/\s+/g, ' ').trim();
|
|
598
|
-
if (!collapsed)
|
|
599
|
-
return DEFAULT_PUSH_BODY;
|
|
600
|
-
if (collapsed.length <= MAX_PUSH_BODY_LENGTH)
|
|
601
|
-
return collapsed;
|
|
602
|
-
return `${collapsed.slice(0, Math.max(1, MAX_PUSH_BODY_LENGTH - 3)).trimEnd()}...`;
|
|
603
|
-
}
|
|
604
|
-
currentPartialReplyText(session) {
|
|
605
|
-
if (!session?.isResponding)
|
|
606
|
-
return undefined;
|
|
607
|
-
const partial = session.currentReplyText.trim();
|
|
608
|
-
return partial ? session.currentReplyText : undefined;
|
|
609
|
-
}
|
|
610
|
-
touchSession(sessionId, persist = true) {
|
|
611
|
-
const now = new Date().toISOString();
|
|
612
|
-
const session = this.sessions.get(sessionId);
|
|
613
|
-
if (session) {
|
|
614
|
-
session.lastActivityAt = now;
|
|
615
|
-
session.lastActivityTs = Date.now();
|
|
616
|
-
if (persist) {
|
|
617
|
-
this.store.upsert(buildRecord(session));
|
|
618
|
-
}
|
|
619
|
-
return;
|
|
620
|
-
}
|
|
621
|
-
const record = this.store.find(sessionId);
|
|
622
|
-
if (record) {
|
|
623
|
-
this.store.upsert({ ...record, lastActivityAt: now });
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
updateGauges() {
|
|
627
|
-
let active = 0;
|
|
628
|
-
for (const s of this.sessions.values()) {
|
|
629
|
-
if (s.active)
|
|
630
|
-
active++;
|
|
631
|
-
}
|
|
632
|
-
metrics.gauge('session.active', active);
|
|
633
|
-
}
|
|
634
|
-
activeSessionSnapshots() {
|
|
635
|
-
const snapshots = [];
|
|
636
|
-
for (const session of this.sessions.values()) {
|
|
637
|
-
if (session.sessionId.startsWith('pending_'))
|
|
638
|
-
continue;
|
|
639
|
-
if (!session.active)
|
|
640
|
-
continue;
|
|
641
|
-
snapshots.push({
|
|
642
|
-
sessionId: session.sessionId,
|
|
643
|
-
agent: session.agent,
|
|
644
|
-
cwd: session.cwd,
|
|
645
|
-
approvalMode: session.approvalMode,
|
|
646
|
-
title: session.title,
|
|
647
|
-
createdAt: session.createdAt,
|
|
648
|
-
lastActivityAt: session.lastActivityAt,
|
|
649
|
-
...(session.pendingApproval ? { needsAttention: true } : {}),
|
|
650
|
-
...(session.isResponding ? { isResponding: true } : {}),
|
|
651
|
-
managed: session.managed,
|
|
652
|
-
});
|
|
653
|
-
}
|
|
654
|
-
return snapshots;
|
|
655
|
-
}
|
|
656
|
-
getDeletedSessionIds() {
|
|
657
|
-
return new Set(this.store.getDeletedSessionIds());
|
|
658
|
-
}
|
|
659
|
-
isDeletedSession(sessionId) {
|
|
660
|
-
return this.store.isDeleted(sessionId);
|
|
661
|
-
}
|
|
662
|
-
async listRecentSessionsForContinue(agent, cwd) {
|
|
663
|
-
return listSessions(agent, cwd);
|
|
664
|
-
}
|
|
665
|
-
async handle(ws, msg) {
|
|
666
|
-
log.debug({ action: msg.action }, 'handling client message');
|
|
667
|
-
switch (msg.action) {
|
|
668
|
-
case 'session.create':
|
|
669
|
-
await this.createSession(ws, msg.id, msg.agent, msg.cwd, msg.approvalMode, msg.continueSession);
|
|
670
|
-
break;
|
|
671
|
-
case 'session.resume':
|
|
672
|
-
await this.resumeSession(ws, msg.id, msg.sessionId, msg.agent);
|
|
673
|
-
break;
|
|
674
|
-
case 'session.send':
|
|
675
|
-
await this.sendMessage(ws, msg.id, msg.sessionId, msg.message, msg.agent, msg.clientMessageId, msg.images);
|
|
676
|
-
break;
|
|
677
|
-
case 'session.approve':
|
|
678
|
-
await this.approve(ws, msg.id, msg.sessionId, msg.requestId, msg.approved);
|
|
679
|
-
break;
|
|
680
|
-
case 'session.setApprovalMode':
|
|
681
|
-
await this.setApprovalMode(ws, msg.id, msg.sessionId, msg.approvalMode);
|
|
682
|
-
break;
|
|
683
|
-
case 'session.interrupt':
|
|
684
|
-
this.interrupt(ws, msg.id, msg.sessionId);
|
|
685
|
-
break;
|
|
686
|
-
case 'session.stop':
|
|
687
|
-
this.stopSession(ws, msg.id, msg.sessionId);
|
|
688
|
-
break;
|
|
689
|
-
case 'session.delete':
|
|
690
|
-
this.deleteSession(ws, msg.id, msg.sessionId);
|
|
691
|
-
break;
|
|
692
|
-
case 'session.history':
|
|
693
|
-
await this.sendHistory(ws, msg.id, msg.sessionId, msg.agent);
|
|
694
|
-
break;
|
|
695
|
-
case 'reconnect.snapshot':
|
|
696
|
-
await this.sendReconnectSnapshot(ws, msg.id, msg.agent, msg.cwd, msg.search, msg.activeSessionId, msg.activeAgent);
|
|
697
|
-
break;
|
|
698
|
-
case 'sessions.list':
|
|
699
|
-
await this.listSessions(ws, msg.id, msg.agent, msg.cwd, msg.search, msg.limit, msg.cursor);
|
|
700
|
-
break;
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
addGlobalClient(ws) {
|
|
704
|
-
this.globalClients.add(ws);
|
|
705
|
-
}
|
|
706
|
-
removeClient(ws) {
|
|
707
|
-
this.globalClients.delete(ws);
|
|
708
|
-
for (const session of this.sessions.values()) {
|
|
709
|
-
session.clients.delete(ws);
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
shutdown() {
|
|
713
|
-
this.stopIdleSweep();
|
|
714
|
-
this.removeInventoryBackfillListener();
|
|
715
|
-
this.globalClients.clear();
|
|
716
|
-
for (const session of this.sessions.values()) {
|
|
717
|
-
try {
|
|
718
|
-
this.resolvePendingClaudeHookApprovals(session);
|
|
719
|
-
session.driver?.stop();
|
|
720
|
-
}
|
|
721
|
-
catch (e) {
|
|
722
|
-
log.error({ sessionId: session.sessionId, agent: session.agent, error: String(e) }, 'failed to stop session on shutdown');
|
|
723
|
-
}
|
|
724
|
-
session.active = false;
|
|
725
|
-
session.driver = null;
|
|
726
|
-
session.clients.clear();
|
|
727
|
-
this.unregisterClaudeHookSession(session);
|
|
728
|
-
}
|
|
729
|
-
this.claudeHookSessions.clear();
|
|
730
|
-
this.store.flushSync();
|
|
731
|
-
}
|
|
732
|
-
/** Expose active session count for health endpoint. */
|
|
733
|
-
getActiveSessionCount() {
|
|
734
|
-
let count = 0;
|
|
735
|
-
for (const s of this.sessions.values()) {
|
|
736
|
-
if (s.active)
|
|
737
|
-
count++;
|
|
738
|
-
}
|
|
739
|
-
return count;
|
|
740
|
-
}
|
|
741
|
-
/** Fire-and-forget cache warm so the next reconnect.snapshot is fast. */
|
|
742
|
-
prewarmCaches() {
|
|
743
|
-
void listSessionPage({
|
|
744
|
-
limit: 50,
|
|
745
|
-
activeSessions: this.activeSessionSnapshots(),
|
|
746
|
-
sessionRecords: this.store.getAll(),
|
|
747
|
-
deletedSessionIds: this.getDeletedSessionIds(),
|
|
748
|
-
}).catch(() => { });
|
|
749
|
-
}
|
|
750
|
-
/** Expose driver breakdown for health endpoint. */
|
|
751
|
-
getDriverCounts() {
|
|
752
|
-
const counts = {};
|
|
753
|
-
for (const s of this.sessions.values()) {
|
|
754
|
-
if (s.active) {
|
|
755
|
-
counts[s.agent] = (counts[s.agent] ?? 0) + 1;
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
return counts;
|
|
759
|
-
}
|
|
760
|
-
noteInventoryChanged(reason) {
|
|
761
|
-
this.inventoryVersion += 1;
|
|
762
|
-
if (this.globalClients.size === 0) {
|
|
763
|
-
return;
|
|
764
|
-
}
|
|
765
|
-
const msg = {
|
|
766
|
-
type: 'sessions.changed',
|
|
767
|
-
version: this.inventoryVersion,
|
|
768
|
-
reason,
|
|
769
|
-
};
|
|
770
|
-
const data = JSON.stringify(msg);
|
|
771
|
-
for (const client of this.globalClients) {
|
|
772
|
-
if (client.readyState === 1) {
|
|
773
|
-
client.send(data);
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
async createSession(ws, reqId, agent, cwd, approvalMode, continueSession) {
|
|
778
|
-
cwd = expandPath(cwd);
|
|
779
|
-
log.info({ agent, cwd, approvalMode, continueSession }, 'creating session');
|
|
780
|
-
// Validate that the working directory exists and is a directory
|
|
781
|
-
try {
|
|
782
|
-
const cwdStat = await stat(cwd);
|
|
783
|
-
if (!cwdStat.isDirectory()) {
|
|
784
|
-
this.reply(ws, reqId, false, undefined, `Path is not a directory: ${cwd}`);
|
|
785
|
-
return;
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
catch {
|
|
789
|
-
this.reply(ws, reqId, false, undefined, `Directory does not exist: ${cwd}`);
|
|
790
|
-
return;
|
|
791
|
-
}
|
|
792
|
-
// For "continue last" mode, find the most recent session and resume it
|
|
793
|
-
if (continueSession) {
|
|
794
|
-
try {
|
|
795
|
-
const recentSessions = await this.listRecentSessionsForContinue(agent, cwd);
|
|
796
|
-
const lastSession = recentSessions.find((session) => !this.isDeletedSession(session.sessionId));
|
|
797
|
-
if (lastSession) {
|
|
798
|
-
log.info({ sessionId: lastSession.sessionId, cwd }, 'continue mode: resuming last session');
|
|
799
|
-
return this.resumeSession(ws, reqId, lastSession.sessionId, agent, cwd, approvalMode);
|
|
800
|
-
}
|
|
801
|
-
log.info('continue mode: no previous sessions found, creating new');
|
|
802
|
-
}
|
|
803
|
-
catch (e) {
|
|
804
|
-
log.warn({ error: String(e) }, 'continue mode: error finding sessions, creating new');
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
try {
|
|
808
|
-
const driver = this.createDriver(agent);
|
|
809
|
-
const claudeHookSecret = this.configureDriverBeforeStart(agent, driver);
|
|
810
|
-
if (agent === 'codex') {
|
|
811
|
-
const sessionId = `pending_${Date.now()}`;
|
|
812
|
-
log.info({ sessionId, agent, cwd }, 'session created (pending codex startup)');
|
|
813
|
-
metrics.increment('session.create', { agent });
|
|
814
|
-
audit.emit('session.create', { sessionId, agent, cwd, approvalMode });
|
|
815
|
-
const session = {
|
|
816
|
-
sessionId,
|
|
817
|
-
agent,
|
|
818
|
-
cwd,
|
|
819
|
-
approvalMode,
|
|
820
|
-
driver,
|
|
821
|
-
clients: new Set([ws]),
|
|
822
|
-
title: 'New session',
|
|
823
|
-
createdAt: new Date().toISOString(),
|
|
824
|
-
lastActivityAt: new Date().toISOString(),
|
|
825
|
-
active: true,
|
|
826
|
-
lastActivityTs: Date.now(),
|
|
827
|
-
isResponding: false,
|
|
828
|
-
currentReplyText: '',
|
|
829
|
-
acceptedClientMessageIds: [],
|
|
830
|
-
syntheticApprovalRetries: {},
|
|
831
|
-
startupInProgress: true,
|
|
832
|
-
bufferedPrompts: [],
|
|
833
|
-
startupToken: 1,
|
|
834
|
-
managed: true,
|
|
835
|
-
claudeHookSecret,
|
|
836
|
-
pendingClaudeHookApprovals: new Map(),
|
|
837
|
-
};
|
|
838
|
-
this.sessions.set(sessionId, session);
|
|
839
|
-
this.registerClaudeHookSession(session);
|
|
840
|
-
this.store.upsert(buildRecord(session));
|
|
841
|
-
this.updateGauges();
|
|
842
|
-
invalidateScannerCache();
|
|
843
|
-
invalidateProcessScanCache();
|
|
844
|
-
this.noteInventoryChanged('session_created');
|
|
845
|
-
this.bindDriverLifecycle(session, agent, '', ws);
|
|
846
|
-
this.reply(ws, reqId, true, { sessionId });
|
|
847
|
-
this.startPendingCodexSession(session, ws);
|
|
848
|
-
return;
|
|
849
|
-
}
|
|
850
|
-
const endTimer = metrics.startTimer('driver.spawn');
|
|
851
|
-
const sessionId = await driver.start(cwd, undefined, approvalMode);
|
|
852
|
-
const spawnMs = endTimer();
|
|
853
|
-
log.info({ sessionId, agent, spawnMs }, 'session created');
|
|
854
|
-
metrics.increment('session.create', { agent });
|
|
855
|
-
audit.emit('session.create', { sessionId, agent, cwd, approvalMode });
|
|
856
|
-
const session = {
|
|
857
|
-
sessionId,
|
|
858
|
-
agent,
|
|
859
|
-
cwd,
|
|
860
|
-
approvalMode,
|
|
861
|
-
driver,
|
|
862
|
-
clients: new Set([ws]),
|
|
863
|
-
title: 'New session',
|
|
864
|
-
createdAt: new Date().toISOString(),
|
|
865
|
-
lastActivityAt: new Date().toISOString(),
|
|
866
|
-
active: true,
|
|
867
|
-
lastActivityTs: Date.now(),
|
|
868
|
-
isResponding: false,
|
|
869
|
-
currentReplyText: '',
|
|
870
|
-
acceptedClientMessageIds: [],
|
|
871
|
-
syntheticApprovalRetries: {},
|
|
872
|
-
startupInProgress: false,
|
|
873
|
-
bufferedPrompts: [],
|
|
874
|
-
startupToken: 0,
|
|
875
|
-
managed: true,
|
|
876
|
-
claudeHookSecret,
|
|
877
|
-
pendingClaudeHookApprovals: new Map(),
|
|
878
|
-
};
|
|
879
|
-
this.sessions.set(sessionId, session);
|
|
880
|
-
this.registerClaudeHookSession(session);
|
|
881
|
-
this.store.upsert(buildRecord(session));
|
|
882
|
-
this.updateGauges();
|
|
883
|
-
invalidateScannerCache();
|
|
884
|
-
invalidateProcessScanCache();
|
|
885
|
-
this.noteInventoryChanged('session_created');
|
|
886
|
-
this.bindDriverLifecycle(session, agent, '', ws);
|
|
887
|
-
this.reply(ws, reqId, true, { sessionId });
|
|
888
|
-
}
|
|
889
|
-
catch (e) {
|
|
890
|
-
log.error({ agent, cwd, error: String(e) }, 'createSession error');
|
|
891
|
-
this.reply(ws, reqId, false, undefined, String(e));
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
async resumeSession(ws, reqId, sessionId, agent, cwdOverride, approvalMode) {
|
|
895
|
-
if (this.isDeletedSession(sessionId)) {
|
|
896
|
-
this.reply(ws, reqId, false, undefined, 'Session not found');
|
|
897
|
-
return;
|
|
898
|
-
}
|
|
899
|
-
// If already active, just attach client
|
|
900
|
-
const existing = this.sessions.get(sessionId);
|
|
901
|
-
if (existing && existing.active) {
|
|
902
|
-
existing.clients.add(ws);
|
|
903
|
-
this.touchSession(existing.sessionId);
|
|
904
|
-
this.noteInventoryChanged('session_updated');
|
|
905
|
-
// Send history
|
|
906
|
-
const history = await readSessionHistory(sessionId, agent, existing.cwd);
|
|
907
|
-
const partialReplyText = this.currentPartialReplyText(existing);
|
|
908
|
-
if (history.length > 0 || partialReplyText || existing.approvalMode || existing.pendingApproval) {
|
|
909
|
-
const historyMsg = {
|
|
910
|
-
type: 'session.history',
|
|
911
|
-
sessionId,
|
|
912
|
-
messages: history,
|
|
913
|
-
...acceptedClientMessageIdsPayload(existing.acceptedClientMessageIds),
|
|
914
|
-
isResponding: existing.isResponding || undefined,
|
|
915
|
-
partialReplyText,
|
|
916
|
-
approvalMode: existing.approvalMode,
|
|
917
|
-
pendingApproval: existing.pendingApproval,
|
|
918
|
-
};
|
|
919
|
-
if (ws.readyState === 1) {
|
|
920
|
-
ws.send(JSON.stringify(historyMsg));
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
this.reply(ws, reqId, true, { sessionId });
|
|
924
|
-
return;
|
|
925
|
-
}
|
|
926
|
-
// Find persisted record for cwd/title
|
|
927
|
-
const record = this.store.find(sessionId);
|
|
928
|
-
const cwd = cwdOverride || record?.cwd || '';
|
|
929
|
-
const sessionApprovalMode = approvalMode ?? record?.approvalMode;
|
|
930
|
-
const title = record?.title ?? 'Resumed session';
|
|
931
|
-
try {
|
|
932
|
-
const endTimer = metrics.startTimer('driver.spawn');
|
|
933
|
-
const driver = this.createDriver(agent);
|
|
934
|
-
const claudeHookSecret = this.configureDriverBeforeStart(agent, driver);
|
|
935
|
-
const actualSessionId = await driver.start(cwd, sessionId, sessionApprovalMode);
|
|
936
|
-
const spawnMs = endTimer();
|
|
937
|
-
log.info({ sessionId: actualSessionId, agent, spawnMs }, 'session resumed');
|
|
938
|
-
metrics.increment('session.resume', { agent });
|
|
939
|
-
audit.emit('session.resume', { sessionId: actualSessionId, agent, cwd });
|
|
940
|
-
const session = {
|
|
941
|
-
sessionId: actualSessionId, agent, cwd, approvalMode: sessionApprovalMode, driver,
|
|
942
|
-
clients: new Set([ws]),
|
|
943
|
-
title,
|
|
944
|
-
createdAt: record?.createdAt ?? new Date().toISOString(),
|
|
945
|
-
lastActivityAt: new Date().toISOString(),
|
|
946
|
-
active: true,
|
|
947
|
-
lastActivityTs: Date.now(),
|
|
948
|
-
isResponding: false,
|
|
949
|
-
currentReplyText: '',
|
|
950
|
-
acceptedClientMessageIds: record?.acceptedClientMessageIds ?? [],
|
|
951
|
-
syntheticApprovalRetries: {},
|
|
952
|
-
startupInProgress: false,
|
|
953
|
-
bufferedPrompts: [],
|
|
954
|
-
startupToken: 0,
|
|
955
|
-
managed: record?.managed,
|
|
956
|
-
pendingApproval: record?.pendingApproval,
|
|
957
|
-
claudeHookSecret,
|
|
958
|
-
pendingClaudeHookApprovals: new Map(),
|
|
959
|
-
};
|
|
960
|
-
this.sessions.set(actualSessionId, session);
|
|
961
|
-
this.registerClaudeHookSession(session);
|
|
962
|
-
this.store.upsert(buildRecord(session));
|
|
963
|
-
this.updateGauges();
|
|
964
|
-
this.noteInventoryChanged('session_updated');
|
|
965
|
-
this.bindDriverLifecycle(session, agent, ' (resumed)', ws);
|
|
966
|
-
this.restoreDriverPendingApproval(driver, session.pendingApproval);
|
|
967
|
-
// Send history from session file
|
|
968
|
-
const history = await readSessionHistory(sessionId, agent, cwd);
|
|
969
|
-
if (history.length > 0 || session.pendingApproval) {
|
|
970
|
-
log.info({ sessionId, historyCount: history.length }, 'sending history messages');
|
|
971
|
-
const historyMsg = {
|
|
972
|
-
type: 'session.history',
|
|
973
|
-
sessionId: actualSessionId,
|
|
974
|
-
messages: history,
|
|
975
|
-
...acceptedClientMessageIdsPayload(session.acceptedClientMessageIds),
|
|
976
|
-
approvalMode: sessionApprovalMode,
|
|
977
|
-
pendingApproval: session.pendingApproval,
|
|
978
|
-
};
|
|
979
|
-
if (ws.readyState === 1) {
|
|
980
|
-
ws.send(JSON.stringify(historyMsg));
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
this.reply(ws, reqId, true, { sessionId: actualSessionId });
|
|
984
|
-
}
|
|
985
|
-
catch (e) {
|
|
986
|
-
this.reply(ws, reqId, false, undefined, String(e));
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
hasAcceptedClientMessage(session, clientMessageId) {
|
|
990
|
-
return session.acceptedClientMessageIds.includes(clientMessageId);
|
|
991
|
-
}
|
|
992
|
-
rememberAcceptedClientMessage(session, clientMessageId) {
|
|
993
|
-
if (this.hasAcceptedClientMessage(session, clientMessageId))
|
|
994
|
-
return;
|
|
995
|
-
session.acceptedClientMessageIds.push(clientMessageId);
|
|
996
|
-
if (session.acceptedClientMessageIds.length > MAX_ACCEPTED_CLIENT_MESSAGE_IDS) {
|
|
997
|
-
session.acceptedClientMessageIds.splice(0, session.acceptedClientMessageIds.length - MAX_ACCEPTED_CLIENT_MESSAGE_IDS);
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
async sendMessage(ws, reqId, sessionId, message, agent, clientMessageId, images) {
|
|
1001
|
-
if (this.isDeletedSession(sessionId)) {
|
|
1002
|
-
this.reply(ws, reqId, false, undefined, 'Session not found');
|
|
1003
|
-
return;
|
|
1004
|
-
}
|
|
1005
|
-
let session = this.sessions.get(sessionId);
|
|
1006
|
-
// Auto-reconnect: if session not found or inactive, try to resume it
|
|
1007
|
-
if (!session || !session.driver) {
|
|
1008
|
-
const reconnect = await this.resolveReconnectSession(sessionId, agent, session);
|
|
1009
|
-
if (!reconnect) {
|
|
1010
|
-
log.warn({ sessionId }, 'session not found in records or scanner');
|
|
1011
|
-
this.reply(ws, reqId, false, undefined, 'Session not found');
|
|
1012
|
-
return;
|
|
1013
|
-
}
|
|
1014
|
-
log.info({ sessionId, agent: reconnect.agent, source: reconnect.source }, 'auto-reconnecting session');
|
|
1015
|
-
metrics.increment('session.reconnect', { agent: reconnect.agent, source: reconnect.source });
|
|
1016
|
-
audit.emit('session.reconnect', { sessionId, agent: reconnect.agent, source: reconnect.source });
|
|
1017
|
-
try {
|
|
1018
|
-
const driver = this.createDriver(reconnect.agent);
|
|
1019
|
-
const claudeHookSecret = this.configureDriverBeforeStart(reconnect.agent, driver, session?.claudeHookSecret);
|
|
1020
|
-
await driver.start(reconnect.cwd, sessionId, reconnect.approvalMode);
|
|
1021
|
-
this.restoreDriverPendingApproval(driver, reconnect.pendingApproval);
|
|
1022
|
-
if (session) {
|
|
1023
|
-
session.approvalMode = reconnect.approvalMode;
|
|
1024
|
-
session.pendingApproval = reconnect.pendingApproval;
|
|
1025
|
-
session.driver = driver;
|
|
1026
|
-
session.active = true;
|
|
1027
|
-
session.clients.add(ws);
|
|
1028
|
-
session.lastActivityAt = reconnect.lastActivityAt;
|
|
1029
|
-
session.lastActivityTs = Date.now();
|
|
1030
|
-
session.isResponding = false;
|
|
1031
|
-
session.currentReplyText = '';
|
|
1032
|
-
session.acceptedClientMessageIds = mergeAcceptedClientMessageIds(session.acceptedClientMessageIds, reconnect.acceptedClientMessageIds);
|
|
1033
|
-
session.syntheticApprovalRetries = session.syntheticApprovalRetries ?? {};
|
|
1034
|
-
session.startupInProgress = false;
|
|
1035
|
-
session.bufferedPrompts = session.bufferedPrompts ?? [];
|
|
1036
|
-
session.startupToken = session.startupToken ?? 0;
|
|
1037
|
-
session.claudeHookSecret = claudeHookSecret ?? session.claudeHookSecret;
|
|
1038
|
-
session.pendingClaudeHookApprovals = session.pendingClaudeHookApprovals ?? new Map();
|
|
1039
|
-
}
|
|
1040
|
-
else {
|
|
1041
|
-
session = {
|
|
1042
|
-
sessionId,
|
|
1043
|
-
agent: reconnect.agent,
|
|
1044
|
-
cwd: reconnect.cwd,
|
|
1045
|
-
approvalMode: reconnect.approvalMode,
|
|
1046
|
-
driver,
|
|
1047
|
-
clients: new Set([ws]),
|
|
1048
|
-
title: reconnect.title,
|
|
1049
|
-
createdAt: reconnect.createdAt,
|
|
1050
|
-
lastActivityAt: reconnect.lastActivityAt,
|
|
1051
|
-
active: true,
|
|
1052
|
-
lastActivityTs: Date.now(),
|
|
1053
|
-
isResponding: false,
|
|
1054
|
-
currentReplyText: '',
|
|
1055
|
-
acceptedClientMessageIds: reconnect.acceptedClientMessageIds,
|
|
1056
|
-
syntheticApprovalRetries: {},
|
|
1057
|
-
startupInProgress: false,
|
|
1058
|
-
bufferedPrompts: [],
|
|
1059
|
-
startupToken: 0,
|
|
1060
|
-
managed: reconnect.managed,
|
|
1061
|
-
pendingApproval: reconnect.pendingApproval,
|
|
1062
|
-
claudeHookSecret,
|
|
1063
|
-
pendingClaudeHookApprovals: new Map(),
|
|
1064
|
-
};
|
|
1065
|
-
this.sessions.set(sessionId, session);
|
|
1066
|
-
}
|
|
1067
|
-
this.registerClaudeHookSession(session);
|
|
1068
|
-
this.bindDriverLifecycle(session, reconnect.agent, ' (reconnected)');
|
|
1069
|
-
this.store.upsert(buildRecord(session));
|
|
1070
|
-
this.updateGauges();
|
|
1071
|
-
this.noteInventoryChanged('session_updated');
|
|
1072
|
-
}
|
|
1073
|
-
catch (e) {
|
|
1074
|
-
this.reply(ws, reqId, false, undefined, `Failed to reconnect: ${e}`);
|
|
1075
|
-
return;
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
session.clients.add(ws);
|
|
1079
|
-
if (session.pendingApproval) {
|
|
1080
|
-
this.reply(ws, reqId, false, undefined, 'Resolve the pending approval before sending another message.');
|
|
1081
|
-
return;
|
|
1082
|
-
}
|
|
1083
|
-
if (clientMessageId && this.hasAcceptedClientMessage(session, clientMessageId)) {
|
|
1084
|
-
this.reply(ws, reqId, true, { sessionId, clientMessageId, duplicate: true });
|
|
1085
|
-
return;
|
|
1086
|
-
}
|
|
1087
|
-
log.info({
|
|
1088
|
-
sessionId,
|
|
1089
|
-
clients: session.clients.size,
|
|
1090
|
-
hasDriver: !!session.driver,
|
|
1091
|
-
active: session.active,
|
|
1092
|
-
imageCount: images?.length ?? 0,
|
|
1093
|
-
}, 'sending message');
|
|
1094
|
-
audit.emit('session.send', {
|
|
1095
|
-
sessionId,
|
|
1096
|
-
agent: session.agent,
|
|
1097
|
-
messagePreview: message.slice(0, 100),
|
|
1098
|
-
imageCount: images?.length ?? 0,
|
|
1099
|
-
});
|
|
1100
|
-
if (isFallbackSessionTitle(session.title)) {
|
|
1101
|
-
const derivedTitle = titleFromFirstSentence(message, 50);
|
|
1102
|
-
if (derivedTitle && derivedTitle !== session.title) {
|
|
1103
|
-
session.title = derivedTitle;
|
|
1104
|
-
}
|
|
1105
|
-
this.store.upsert(buildRecord(session));
|
|
1106
|
-
}
|
|
1107
|
-
if (clientMessageId) {
|
|
1108
|
-
this.rememberAcceptedClientMessage(session, clientMessageId);
|
|
1109
|
-
}
|
|
1110
|
-
this.touchSession(session.sessionId);
|
|
1111
|
-
const attachmentBlock = images?.length
|
|
1112
|
-
? images.map((path) => `[Attached image: ${path}]`).join('\n')
|
|
1113
|
-
: '';
|
|
1114
|
-
const prompt = attachmentBlock
|
|
1115
|
-
? message
|
|
1116
|
-
? `${message}\n\n${attachmentBlock}`
|
|
1117
|
-
: attachmentBlock
|
|
1118
|
-
: message;
|
|
1119
|
-
if (session.startupInProgress) {
|
|
1120
|
-
session.isResponding = true;
|
|
1121
|
-
session.currentReplyText = '';
|
|
1122
|
-
session.lastUserMessage = message;
|
|
1123
|
-
session.bufferedPrompts.push(prompt);
|
|
1124
|
-
this.noteInventoryChanged('session_updated');
|
|
1125
|
-
this.reply(ws, reqId, true, clientMessageId ? { sessionId, clientMessageId } : undefined);
|
|
1126
|
-
return;
|
|
1127
|
-
}
|
|
1128
|
-
if (session.isResponding) {
|
|
1129
|
-
session.bufferedPrompts.push(prompt);
|
|
1130
|
-
this.reply(ws, reqId, true, clientMessageId ? { sessionId, clientMessageId } : undefined);
|
|
1131
|
-
return;
|
|
1132
|
-
}
|
|
1133
|
-
session.isResponding = true;
|
|
1134
|
-
session.currentReplyText = '';
|
|
1135
|
-
session.lastUserMessage = message;
|
|
1136
|
-
this.noteInventoryChanged('session_updated');
|
|
1137
|
-
session.driver.sendPrompt(prompt);
|
|
1138
|
-
this.reply(ws, reqId, true, clientMessageId ? { sessionId, clientMessageId } : undefined);
|
|
1139
|
-
}
|
|
1140
|
-
async sendReconnectSnapshot(ws, reqId, agent, cwd, search, activeSessionId, activeAgent) {
|
|
1141
|
-
try {
|
|
1142
|
-
const deletedSessionIds = this.getDeletedSessionIds();
|
|
1143
|
-
let page;
|
|
1144
|
-
try {
|
|
1145
|
-
page = await listSessionPage({
|
|
1146
|
-
agent,
|
|
1147
|
-
cwd,
|
|
1148
|
-
search,
|
|
1149
|
-
limit: 50,
|
|
1150
|
-
activeSessions: this.activeSessionSnapshots(),
|
|
1151
|
-
sessionRecords: this.store.getAll(),
|
|
1152
|
-
deletedSessionIds,
|
|
1153
|
-
});
|
|
1154
|
-
}
|
|
1155
|
-
catch (error) {
|
|
1156
|
-
log.warn({ error: String(error) }, 'failed to build reconnect snapshot session list');
|
|
1157
|
-
page = { sessions: [], nextCursor: undefined };
|
|
1158
|
-
}
|
|
1159
|
-
page = { ...page, inventoryVersion: this.inventoryVersion };
|
|
1160
|
-
let activeSession;
|
|
1161
|
-
if (activeSessionId && !deletedSessionIds.has(activeSessionId)) {
|
|
1162
|
-
const inferredAgent = activeAgent
|
|
1163
|
-
?? page.sessions.find((session) => session.sessionId === activeSessionId)?.agent;
|
|
1164
|
-
const liveSession = this.sessions.get(activeSessionId);
|
|
1165
|
-
// Subscribe this ws to session broadcasts so streaming updates reach reconnected clients
|
|
1166
|
-
if (liveSession) {
|
|
1167
|
-
liveSession.clients.add(ws);
|
|
1168
|
-
}
|
|
1169
|
-
const resolved = await this.resolveReconnectSession(activeSessionId, inferredAgent, liveSession);
|
|
1170
|
-
if (resolved) {
|
|
1171
|
-
const history = await readSessionHistory(activeSessionId, resolved.agent, liveSession?.cwd ?? resolved.cwd);
|
|
1172
|
-
const runtimeHints = liveSession
|
|
1173
|
-
? {}
|
|
1174
|
-
: await readSessionRuntimeHints(activeSessionId, resolved.agent, resolved.cwd);
|
|
1175
|
-
activeSession = {
|
|
1176
|
-
sessionId: activeSessionId,
|
|
1177
|
-
agent: resolved.agent,
|
|
1178
|
-
messages: history,
|
|
1179
|
-
...acceptedClientMessageIdsPayload(liveSession?.acceptedClientMessageIds ?? resolved.acceptedClientMessageIds),
|
|
1180
|
-
isResponding: liveSession?.isResponding ?? runtimeHints.isResponding ?? resolved.isResponding,
|
|
1181
|
-
partialReplyText: liveSession
|
|
1182
|
-
? this.currentPartialReplyText(liveSession)
|
|
1183
|
-
: runtimeHints.partialReplyText,
|
|
1184
|
-
approvalMode: liveSession?.approvalMode ?? resolved.approvalMode,
|
|
1185
|
-
...(liveSession?.pendingApproval ? { pendingApproval: liveSession.pendingApproval } : {}),
|
|
1186
|
-
};
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
const pendingApprovals = this.collectReconnectPendingApprovals();
|
|
1190
|
-
metrics.increment('reconnect.snapshot', { activeSession: activeSession ? 'true' : 'false' });
|
|
1191
|
-
this.reply(ws, reqId, true, {
|
|
1192
|
-
snapshot: {
|
|
1193
|
-
sessions: page.sessions,
|
|
1194
|
-
nextCursor: page.nextCursor,
|
|
1195
|
-
inventoryVersion: page.inventoryVersion,
|
|
1196
|
-
...(activeSession ? { activeSession } : {}),
|
|
1197
|
-
...(pendingApprovals.length > 0 ? { pendingApprovals } : {}),
|
|
1198
|
-
},
|
|
1199
|
-
});
|
|
1200
|
-
}
|
|
1201
|
-
catch (error) {
|
|
1202
|
-
this.reply(ws, reqId, false, undefined, `Failed to build reconnect snapshot: ${String(error)}`);
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
async approve(ws, reqId, sessionId, requestId, approved) {
|
|
1206
|
-
const session = this.sessions.get(sessionId);
|
|
1207
|
-
const pendingApproval = session?.pendingApproval;
|
|
1208
|
-
const pendingClaudeHookApproval = session?.pendingClaudeHookApprovals.get(requestId);
|
|
1209
|
-
if (session && pendingClaudeHookApproval) {
|
|
1210
|
-
session.pendingApproval = undefined;
|
|
1211
|
-
audit.emit('approval.response', { sessionId, requestId, approved });
|
|
1212
|
-
this.touchSession(session.sessionId);
|
|
1213
|
-
this.noteInventoryChanged('session_updated');
|
|
1214
|
-
pendingClaudeHookApproval.resolve(buildClaudeHookDecisionResponse(approved));
|
|
1215
|
-
session.isResponding = true;
|
|
1216
|
-
this.reply(ws, reqId, true);
|
|
1217
|
-
return;
|
|
1218
|
-
}
|
|
1219
|
-
const syntheticRetry = session?.syntheticApprovalRetries[requestId];
|
|
1220
|
-
if (session && syntheticRetry && isClaudeSyntheticApprovalRequestId(requestId)) {
|
|
1221
|
-
delete session.syntheticApprovalRetries[requestId];
|
|
1222
|
-
session.pendingApproval = undefined;
|
|
1223
|
-
audit.emit('approval.response', { sessionId, requestId, approved });
|
|
1224
|
-
this.touchSession(session.sessionId);
|
|
1225
|
-
this.noteInventoryChanged('session_updated');
|
|
1226
|
-
this.reply(ws, reqId, true);
|
|
1227
|
-
if (approved) {
|
|
1228
|
-
await this.retrySyntheticClaudeApproval(session, syntheticRetry);
|
|
1229
|
-
}
|
|
1230
|
-
return;
|
|
1231
|
-
}
|
|
1232
|
-
if (session && !session.driver && pendingApproval) {
|
|
1233
|
-
try {
|
|
1234
|
-
await this.reviveSessionForApproval(session);
|
|
1235
|
-
}
|
|
1236
|
-
catch (error) {
|
|
1237
|
-
this.reply(ws, reqId, false, undefined, `Failed to restore approval session: ${String(error)}`);
|
|
1238
|
-
return;
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1241
|
-
if (!session?.driver) {
|
|
1242
|
-
this.reply(ws, reqId, false, undefined, 'Session not found or inactive');
|
|
1243
|
-
return;
|
|
1244
|
-
}
|
|
1245
|
-
audit.emit('approval.response', { sessionId, requestId, approved });
|
|
1246
|
-
const sent = session.driver.respondApproval(requestId, approved);
|
|
1247
|
-
if (!sent) {
|
|
1248
|
-
this.reply(ws, reqId, false, undefined, 'Agent process is not running. Send a new message to continue.');
|
|
1249
|
-
return;
|
|
1250
|
-
}
|
|
1251
|
-
session.pendingApproval = undefined;
|
|
1252
|
-
this.touchSession(session.sessionId);
|
|
1253
|
-
session.isResponding = true;
|
|
1254
|
-
this.noteInventoryChanged('session_updated');
|
|
1255
|
-
this.reply(ws, reqId, true);
|
|
1256
|
-
}
|
|
1257
|
-
async retrySyntheticClaudeApproval(session, retry) {
|
|
1258
|
-
if (session.agent !== 'claude')
|
|
1259
|
-
return;
|
|
1260
|
-
try {
|
|
1261
|
-
if (session.driver) {
|
|
1262
|
-
session.driver.stop();
|
|
1263
|
-
}
|
|
1264
|
-
const retryApprovalMode = ['Write', 'Edit', 'NotebookEdit'].includes(retry.toolName)
|
|
1265
|
-
? 'acceptEdits'
|
|
1266
|
-
: 'autoApprove';
|
|
1267
|
-
const driver = this.createDriver(session.agent);
|
|
1268
|
-
const claudeHookSecret = this.configureDriverBeforeStart(session.agent, driver, session.claudeHookSecret);
|
|
1269
|
-
await driver.start(session.cwd, session.sessionId, retryApprovalMode);
|
|
1270
|
-
session.driver = driver;
|
|
1271
|
-
session.active = true;
|
|
1272
|
-
session.isResponding = true;
|
|
1273
|
-
session.currentReplyText = '';
|
|
1274
|
-
session.lastUserMessage = retry.message;
|
|
1275
|
-
session.startupInProgress = false;
|
|
1276
|
-
session.bufferedPrompts = [];
|
|
1277
|
-
session.startupToken = 0;
|
|
1278
|
-
session.claudeHookSecret = claudeHookSecret ?? session.claudeHookSecret;
|
|
1279
|
-
session.pendingClaudeHookApprovals = session.pendingClaudeHookApprovals ?? new Map();
|
|
1280
|
-
this.registerClaudeHookSession(session);
|
|
1281
|
-
this.bindDriverLifecycle(session, session.agent, ' (approval retry)');
|
|
1282
|
-
this.store.upsert(buildRecord(session));
|
|
1283
|
-
this.updateGauges();
|
|
1284
|
-
this.touchSession(session.sessionId);
|
|
1285
|
-
this.noteInventoryChanged('session_updated');
|
|
1286
|
-
log.info({ sessionId: session.sessionId, toolName: retry.toolName, retryApprovalMode }, 'retrying Claude turn after synthetic approval');
|
|
1287
|
-
driver.sendPrompt(retry.message);
|
|
1288
|
-
}
|
|
1289
|
-
catch (error) {
|
|
1290
|
-
session.driver = null;
|
|
1291
|
-
session.active = false;
|
|
1292
|
-
session.isResponding = false;
|
|
1293
|
-
session.startupInProgress = false;
|
|
1294
|
-
session.bufferedPrompts = [];
|
|
1295
|
-
session.startupToken += 1;
|
|
1296
|
-
this.updateGauges();
|
|
1297
|
-
this.touchSession(session.sessionId);
|
|
1298
|
-
this.noteInventoryChanged('session_updated');
|
|
1299
|
-
log.error({ sessionId: session.sessionId, error: String(error) }, 'failed to retry Claude turn after approval');
|
|
1300
|
-
this.broadcast(session.sessionId, {
|
|
1301
|
-
type: 'error',
|
|
1302
|
-
sessionId: session.sessionId,
|
|
1303
|
-
message: `Failed to continue after approval: ${String(error)}`,
|
|
1304
|
-
});
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
async setApprovalMode(ws, reqId, sessionId, approvalMode) {
|
|
1308
|
-
const session = this.sessions.get(sessionId);
|
|
1309
|
-
if (!session) {
|
|
1310
|
-
// Session not active in memory — update persisted record or create one
|
|
1311
|
-
const record = this.store.find(sessionId);
|
|
1312
|
-
if (record) {
|
|
1313
|
-
record.approvalMode = approvalMode;
|
|
1314
|
-
this.store.upsert(record);
|
|
1315
|
-
this.noteInventoryChanged('session_updated');
|
|
1316
|
-
log.info({ sessionId, agent: record.agent, newMode: approvalMode }, 'approval mode changed (inactive session)');
|
|
1317
|
-
}
|
|
1318
|
-
else {
|
|
1319
|
-
log.info({ sessionId, newMode: approvalMode }, 'approval mode changed (untracked session)');
|
|
1320
|
-
}
|
|
1321
|
-
this.reply(ws, reqId, true, { approvalMode });
|
|
1322
|
-
return;
|
|
1323
|
-
}
|
|
1324
|
-
const oldMode = session.approvalMode;
|
|
1325
|
-
session.approvalMode = approvalMode;
|
|
1326
|
-
this.store.upsert(buildRecord(session));
|
|
1327
|
-
this.noteInventoryChanged('session_updated');
|
|
1328
|
-
log.info({ sessionId, agent: session.agent, oldMode, newMode: approvalMode }, 'approval mode changed');
|
|
1329
|
-
if (session.driver) {
|
|
1330
|
-
session.driver.setApprovalMode(approvalMode);
|
|
1331
|
-
}
|
|
1332
|
-
this.reply(ws, reqId, true, { approvalMode });
|
|
1333
|
-
}
|
|
1334
|
-
interrupt(ws, reqId, sessionId) {
|
|
1335
|
-
const session = this.sessions.get(sessionId);
|
|
1336
|
-
if (!session?.driver) {
|
|
1337
|
-
this.reply(ws, reqId, false, undefined, 'Session not found or inactive');
|
|
1338
|
-
return;
|
|
1339
|
-
}
|
|
1340
|
-
audit.emit('session.interrupt', { sessionId, agent: session.agent });
|
|
1341
|
-
if (session.startupInProgress) {
|
|
1342
|
-
const hadBufferedTurn = session.isResponding || session.bufferedPrompts.length > 0;
|
|
1343
|
-
log.info({ sessionId, agent: session.agent, hadBufferedTurn }, 'interrupting pending startup session');
|
|
1344
|
-
session.bufferedPrompts = [];
|
|
1345
|
-
session.isResponding = false;
|
|
1346
|
-
session.currentReplyText = '';
|
|
1347
|
-
session.lastUserMessage = undefined;
|
|
1348
|
-
session.pendingApproval = undefined;
|
|
1349
|
-
this.touchSession(session.sessionId);
|
|
1350
|
-
this.noteInventoryChanged('session_updated');
|
|
1351
|
-
this.reply(ws, reqId, true);
|
|
1352
|
-
if (hadBufferedTurn) {
|
|
1353
|
-
this.broadcast(session.sessionId, { type: 'session.interrupted', sessionId: session.sessionId });
|
|
1354
|
-
}
|
|
1355
|
-
return;
|
|
1356
|
-
}
|
|
1357
|
-
session.driver.interrupt();
|
|
1358
|
-
this.reply(ws, reqId, true);
|
|
1359
|
-
}
|
|
1360
|
-
stopSession(ws, reqId, sessionId) {
|
|
1361
|
-
const session = this.sessions.get(sessionId);
|
|
1362
|
-
if (!session) {
|
|
1363
|
-
this.reply(ws, reqId, false, undefined, 'Session not found');
|
|
1364
|
-
return;
|
|
1365
|
-
}
|
|
1366
|
-
log.info({ sessionId, agent: session.agent }, 'stopping session');
|
|
1367
|
-
audit.emit('session.stop', { sessionId, agent: session.agent });
|
|
1368
|
-
this.resolvePendingClaudeHookApprovals(session);
|
|
1369
|
-
session.driver?.stop();
|
|
1370
|
-
session.active = false;
|
|
1371
|
-
session.driver = null;
|
|
1372
|
-
session.pendingApproval = undefined;
|
|
1373
|
-
session.startupInProgress = false;
|
|
1374
|
-
session.bufferedPrompts = [];
|
|
1375
|
-
session.startupToken += 1;
|
|
1376
|
-
this.updateGauges();
|
|
1377
|
-
this.touchSession(session.sessionId);
|
|
1378
|
-
this.noteInventoryChanged('session_updated');
|
|
1379
|
-
this.reply(ws, reqId, true);
|
|
1380
|
-
}
|
|
1381
|
-
deleteSession(ws, reqId, sessionId) {
|
|
1382
|
-
const session = this.sessions.get(sessionId);
|
|
1383
|
-
if (session) {
|
|
1384
|
-
log.info({ sessionId, agent: session.agent }, 'deleting session');
|
|
1385
|
-
this.resolvePendingClaudeHookApprovals(session);
|
|
1386
|
-
session.driver?.stop();
|
|
1387
|
-
session.startupInProgress = false;
|
|
1388
|
-
session.bufferedPrompts = [];
|
|
1389
|
-
session.startupToken += 1;
|
|
1390
|
-
this.unregisterClaudeHookSession(session);
|
|
1391
|
-
this.sessions.delete(sessionId);
|
|
1392
|
-
}
|
|
1393
|
-
audit.emit('session.delete', { sessionId });
|
|
1394
|
-
this.store.remove(sessionId);
|
|
1395
|
-
this.updateGauges();
|
|
1396
|
-
invalidateScannerCache();
|
|
1397
|
-
invalidateProcessScanCache();
|
|
1398
|
-
this.noteInventoryChanged('session_deleted');
|
|
1399
|
-
this.reply(ws, reqId, true);
|
|
1400
|
-
}
|
|
1401
|
-
async sendHistory(ws, reqId, sessionId, agent) {
|
|
1402
|
-
const session = this.sessions.get(sessionId);
|
|
1403
|
-
if (!session && this.isDeletedSession(sessionId)) {
|
|
1404
|
-
this.reply(ws, reqId, false, undefined, 'Session not found');
|
|
1405
|
-
return;
|
|
1406
|
-
}
|
|
1407
|
-
// Subscribe this ws to session broadcasts so streaming updates reach reconnected clients
|
|
1408
|
-
if (session) {
|
|
1409
|
-
session.clients.add(ws);
|
|
1410
|
-
}
|
|
1411
|
-
const cwd = session?.cwd ?? this.store.find(sessionId)?.cwd ?? '';
|
|
1412
|
-
const record = this.store.find(sessionId);
|
|
1413
|
-
const history = await readSessionHistory(sessionId, agent, cwd);
|
|
1414
|
-
const runtimeHints = session ? {} : await readSessionRuntimeHints(sessionId, agent, cwd);
|
|
1415
|
-
const isResponding = session?.isResponding ?? runtimeHints.isResponding;
|
|
1416
|
-
const partialReplyText = session
|
|
1417
|
-
? this.currentPartialReplyText(session)
|
|
1418
|
-
: runtimeHints.partialReplyText;
|
|
1419
|
-
log.info({ sessionId, historyCount: history.length, isResponding, approvalMode: session?.approvalMode ?? record?.approvalMode }, 'sending history');
|
|
1420
|
-
const historyMsg = {
|
|
1421
|
-
type: 'session.history',
|
|
1422
|
-
sessionId,
|
|
1423
|
-
messages: history,
|
|
1424
|
-
...acceptedClientMessageIdsPayload(session?.acceptedClientMessageIds ?? record?.acceptedClientMessageIds),
|
|
1425
|
-
isResponding,
|
|
1426
|
-
partialReplyText,
|
|
1427
|
-
approvalMode: session?.approvalMode ?? record?.approvalMode,
|
|
1428
|
-
pendingApproval: session?.pendingApproval ?? record?.pendingApproval,
|
|
1429
|
-
};
|
|
1430
|
-
if (ws.readyState === 1) {
|
|
1431
|
-
ws.send(JSON.stringify(historyMsg));
|
|
1432
|
-
}
|
|
1433
|
-
this.reply(ws, reqId, true);
|
|
1434
|
-
}
|
|
1435
|
-
async listSessions(ws, reqId, agent, cwd, search, limit = 50, cursor) {
|
|
1436
|
-
try {
|
|
1437
|
-
const page = await listSessionPage({
|
|
1438
|
-
agent,
|
|
1439
|
-
cwd,
|
|
1440
|
-
search,
|
|
1441
|
-
limit,
|
|
1442
|
-
cursor,
|
|
1443
|
-
activeSessions: this.activeSessionSnapshots(),
|
|
1444
|
-
sessionRecords: this.store.getAll(),
|
|
1445
|
-
deletedSessionIds: this.getDeletedSessionIds(),
|
|
1446
|
-
});
|
|
1447
|
-
this.reply(ws, reqId, true, { ...page, inventoryVersion: this.inventoryVersion });
|
|
1448
|
-
}
|
|
1449
|
-
catch (e) {
|
|
1450
|
-
this.reply(ws, reqId, false, undefined, String(e));
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
createDriver(agent) {
|
|
1454
|
-
switch (agent) {
|
|
1455
|
-
case 'claude': return new ClaudeDriver();
|
|
1456
|
-
case 'codex': return new CodexDriver();
|
|
1457
|
-
default: throw new Error(`Unknown agent: ${agent}`);
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
broadcast(sessionId, msg) {
|
|
1461
|
-
const session = this.sessions.get(sessionId);
|
|
1462
|
-
if (!session) {
|
|
1463
|
-
log.warn({ sessionId, msgType: msg.type }, 'broadcast target session not found');
|
|
1464
|
-
return;
|
|
1465
|
-
}
|
|
1466
|
-
const data = JSON.stringify(msg);
|
|
1467
|
-
// Approval requests must reach all connected clients, not just those
|
|
1468
|
-
// subscribed to this session, so the app can show the approval sheet
|
|
1469
|
-
// even when the user is viewing a different session.
|
|
1470
|
-
const targets = msg.type === 'approval.request' ? this.globalClients : session.clients;
|
|
1471
|
-
const total = targets.size;
|
|
1472
|
-
let sent = 0;
|
|
1473
|
-
let failed = 0;
|
|
1474
|
-
for (const client of targets) {
|
|
1475
|
-
if (client.readyState === 1) { // WebSocket.OPEN
|
|
1476
|
-
client.send(data);
|
|
1477
|
-
sent++;
|
|
1478
|
-
}
|
|
1479
|
-
else {
|
|
1480
|
-
failed++;
|
|
1481
|
-
}
|
|
1482
|
-
}
|
|
1483
|
-
if (failed > 0) {
|
|
1484
|
-
metrics.increment('broadcast.fail');
|
|
1485
|
-
}
|
|
1486
|
-
if (msg.type !== 'text.delta') {
|
|
1487
|
-
log.debug({ sessionId, msgType: msg.type, sent, total }, 'broadcast');
|
|
1488
|
-
}
|
|
1489
|
-
if (msg.type === 'text.delta') {
|
|
1490
|
-
session.currentReplyText += msg.content;
|
|
1491
|
-
return;
|
|
1492
|
-
}
|
|
1493
|
-
if (msg.type === 'session.done') {
|
|
1494
|
-
const title = session.title || sessionId;
|
|
1495
|
-
const body = this.buildPushBody(session.currentReplyText);
|
|
1496
|
-
session.currentReplyText = '';
|
|
1497
|
-
void this.pushSender(title, body, {
|
|
1498
|
-
sessionId,
|
|
1499
|
-
agent: session.agent,
|
|
1500
|
-
eventType: 'reply_ready',
|
|
1501
|
-
});
|
|
1502
|
-
return;
|
|
1503
|
-
}
|
|
1504
|
-
if (msg.type === 'session.interrupted') {
|
|
1505
|
-
session.currentReplyText = '';
|
|
1506
|
-
}
|
|
1507
|
-
}
|
|
1508
|
-
reply(ws, id, ok, data, error) {
|
|
1509
|
-
if (ws.readyState !== 1)
|
|
1510
|
-
return;
|
|
1511
|
-
const msg = { type: 'response', id, ok, data, error };
|
|
1512
|
-
ws.send(JSON.stringify(msg));
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
//# sourceMappingURL=session-manager.js.map
|