teleportation-cli 1.1.5 → 1.2.1
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/.claude/hooks/permission_request.mjs +326 -59
- package/.claude/hooks/post_tool_use.mjs +90 -0
- package/.claude/hooks/pre_tool_use.mjs +212 -293
- package/.claude/hooks/session-register.mjs +89 -104
- package/.claude/hooks/session_end.mjs +41 -42
- package/.claude/hooks/session_start.mjs +45 -60
- package/.claude/hooks/stop.mjs +752 -99
- package/.claude/hooks/user_prompt_submit.mjs +26 -3
- package/lib/cli/daemon-commands.js +1 -1
- package/lib/cli/teleport-commands.js +469 -0
- package/lib/daemon/daemon-v2.js +104 -0
- package/lib/daemon/lifecycle.js +56 -171
- package/lib/daemon/services/index.js +3 -0
- package/lib/daemon/services/polling-service.js +173 -0
- package/lib/daemon/services/queue-service.js +318 -0
- package/lib/daemon/services/session-service.js +115 -0
- package/lib/daemon/state.js +35 -0
- package/lib/daemon/task-executor-v2.js +413 -0
- package/lib/daemon/task-executor.js +270 -96
- package/lib/daemon/teleportation-daemon.js +709 -126
- package/lib/daemon/timeline-analyzer.js +215 -0
- package/lib/daemon/transcript-ingestion.js +696 -0
- package/lib/daemon/utils.js +91 -0
- package/lib/install/installer.js +184 -20
- package/lib/install/uhr-installer.js +136 -0
- package/lib/remote/providers/base-provider.js +46 -0
- package/lib/remote/providers/daytona-provider.js +58 -0
- package/lib/remote/providers/provider-factory.js +90 -19
- package/lib/remote/providers/sprites-provider.js +711 -0
- package/lib/teleport/exporters/claude-exporter.js +302 -0
- package/lib/teleport/exporters/gemini-exporter.js +307 -0
- package/lib/teleport/exporters/index.js +93 -0
- package/lib/teleport/exporters/interface.js +153 -0
- package/lib/teleport/fork-tracker.js +415 -0
- package/lib/teleport/git-committer.js +337 -0
- package/lib/teleport/index.js +48 -0
- package/lib/teleport/manager.js +620 -0
- package/lib/teleport/session-capture.js +282 -0
- package/package.json +6 -2
- package/teleportation-cli.cjs +488 -453
- package/.claude/hooks/heartbeat.mjs +0 -396
- package/lib/daemon/pid-manager.js +0 -183
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Queue Service
|
|
3
|
+
*
|
|
4
|
+
* Manages approval queue and execution of approved tools.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawn } from 'child_process';
|
|
8
|
+
import { truncateOutput, validateApprovalId, validateSessionId, validateToolName, buildToolPrompt } from '../utils.js';
|
|
9
|
+
|
|
10
|
+
export class QueueService {
|
|
11
|
+
constructor(state, config) {
|
|
12
|
+
this.name = 'queue';
|
|
13
|
+
this.state = state;
|
|
14
|
+
this.config = config;
|
|
15
|
+
this.maxQueueSize = config.maxQueueSize || 1000;
|
|
16
|
+
this.maxExecutions = config.maxExecutions || 1000;
|
|
17
|
+
this.childTimeoutMs = config.childTimeoutMs || 600000;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async start(ctx) {
|
|
21
|
+
this.ctx = ctx;
|
|
22
|
+
const { server } = ctx;
|
|
23
|
+
|
|
24
|
+
// Register routes
|
|
25
|
+
server.addRoute('POST', '/approvals/handoff', (req) => this.handleHandoff(req));
|
|
26
|
+
server.addRoute('GET', '/executions/', (req) => this.handleGetExecution(req));
|
|
27
|
+
|
|
28
|
+
console.log('[QueueService] Started');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async stop() {
|
|
32
|
+
console.log('[QueueService] Stopped');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getStats() {
|
|
36
|
+
return {
|
|
37
|
+
queueSize: this.state.approvalQueue.length,
|
|
38
|
+
executionsCount: this.state.executions.size
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async handleHandoff(req) {
|
|
43
|
+
try {
|
|
44
|
+
const body = await req.json();
|
|
45
|
+
const { approval_id, session_id, tool_name, tool_input } = body;
|
|
46
|
+
|
|
47
|
+
validateApprovalId(approval_id);
|
|
48
|
+
validateSessionId(session_id);
|
|
49
|
+
validateToolName(tool_name);
|
|
50
|
+
|
|
51
|
+
if (this.state.approvalQueue.length >= this.maxQueueSize) {
|
|
52
|
+
return Response.json({
|
|
53
|
+
error: 'Approval queue full',
|
|
54
|
+
queue_size: this.state.approvalQueue.length
|
|
55
|
+
}, { status: 503 });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!this.state.approvalQueue.find(a => a.approval_id === approval_id)) {
|
|
59
|
+
this.state.approvalQueue.push({
|
|
60
|
+
approval_id,
|
|
61
|
+
session_id,
|
|
62
|
+
tool_name,
|
|
63
|
+
tool_input,
|
|
64
|
+
queued_at: Date.now()
|
|
65
|
+
});
|
|
66
|
+
console.log(`[QueueService] Approval queued: ${approval_id} (${tool_name})`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return Response.json({ ok: true, queued: true });
|
|
70
|
+
} catch (error) {
|
|
71
|
+
return Response.json({ error: error.message }, { status: 400 });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async handleGetExecution(req) {
|
|
76
|
+
const url = new URL(req.url);
|
|
77
|
+
const approval_id = url.pathname.split('/executions/')[1];
|
|
78
|
+
|
|
79
|
+
if (!approval_id) {
|
|
80
|
+
return Response.json({ error: 'approval_id is required' }, { status: 400 });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const execution = this.state.executions.get(approval_id);
|
|
84
|
+
if (!execution) {
|
|
85
|
+
return Response.json({ error: 'not_found' }, { status: 404 });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return Response.json(execution);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Process one item from the queue
|
|
93
|
+
*/
|
|
94
|
+
async processNext() {
|
|
95
|
+
if (this.state.approvalQueue.length === 0 || this.state.isShuttingDown) return;
|
|
96
|
+
|
|
97
|
+
const approval = this.state.approvalQueue.shift();
|
|
98
|
+
const { approval_id, session_id, tool_name, tool_input } = approval;
|
|
99
|
+
|
|
100
|
+
if (this.state.executions.has(approval_id)) {
|
|
101
|
+
if (this.state.executions.get(approval_id).status === 'executing') {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// LRU cleanup
|
|
107
|
+
if (this.state.executions.size >= this.maxExecutions) {
|
|
108
|
+
let oldestKey = null;
|
|
109
|
+
let oldestTime = Infinity;
|
|
110
|
+
for (const [id, exec] of this.state.executions) {
|
|
111
|
+
if (exec.completed_at && exec.completed_at < oldestTime) {
|
|
112
|
+
oldestTime = exec.completed_at;
|
|
113
|
+
oldestKey = id;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (oldestKey) this.state.executions.delete(oldestKey);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
this.state.executions.set(approval_id, {
|
|
120
|
+
approval_id,
|
|
121
|
+
status: 'executing',
|
|
122
|
+
started_at: Date.now(),
|
|
123
|
+
completed_at: null,
|
|
124
|
+
exit_code: null,
|
|
125
|
+
stdout: '',
|
|
126
|
+
stderr: '',
|
|
127
|
+
error: null,
|
|
128
|
+
child_process: null
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
// Acknowledge
|
|
133
|
+
await this.ackApproval(approval_id);
|
|
134
|
+
|
|
135
|
+
const prompt = buildToolPrompt(tool_name, tool_input);
|
|
136
|
+
const result = await this.spawnClaudeProcess(session_id, prompt, {
|
|
137
|
+
onSpawn: (child) => {
|
|
138
|
+
const exec = this.state.executions.get(approval_id);
|
|
139
|
+
if (exec) exec.child_process = child;
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
this.state.executions.set(approval_id, {
|
|
144
|
+
...this.state.executions.get(approval_id),
|
|
145
|
+
status: result.success ? 'completed' : 'failed',
|
|
146
|
+
completed_at: Date.now(),
|
|
147
|
+
exit_code: result.exit_code,
|
|
148
|
+
stdout: result.stdout,
|
|
149
|
+
stderr: result.stderr,
|
|
150
|
+
error: result.error
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
await this.reportExecutionStatus(approval_id, result);
|
|
154
|
+
await this.storeExecutionResult(session_id, approval_id, tool_name, tool_input?.command, result);
|
|
155
|
+
|
|
156
|
+
} catch (error) {
|
|
157
|
+
console.error(`[QueueService] Execution error: ${error.message}`);
|
|
158
|
+
this.state.executions.set(approval_id, {
|
|
159
|
+
...this.state.executions.get(approval_id),
|
|
160
|
+
status: 'failed',
|
|
161
|
+
completed_at: Date.now(),
|
|
162
|
+
error: error.message
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async ackApproval(approval_id) {
|
|
168
|
+
if (!this.config.relayApiUrl || !this.config.relayApiKey) return;
|
|
169
|
+
try {
|
|
170
|
+
await fetch(`${this.config.relayApiUrl}/api/approvals/${approval_id}/ack`, {
|
|
171
|
+
method: 'POST',
|
|
172
|
+
headers: {
|
|
173
|
+
'Content-Type': 'application/json',
|
|
174
|
+
'Authorization': `Bearer ${this.config.relayApiKey}`
|
|
175
|
+
},
|
|
176
|
+
body: JSON.stringify({ processed: true })
|
|
177
|
+
});
|
|
178
|
+
} catch (err) {
|
|
179
|
+
console.error(`[QueueService] Ack failed: ${err.message}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async reportExecutionStatus(approval_id, result) {
|
|
184
|
+
if (!this.config.relayApiUrl || !this.config.relayApiKey) return;
|
|
185
|
+
try {
|
|
186
|
+
await fetch(`${this.config.relayApiUrl}/api/approvals/${approval_id}/executed`, {
|
|
187
|
+
method: 'POST',
|
|
188
|
+
headers: {
|
|
189
|
+
'Content-Type': 'application/json',
|
|
190
|
+
'Authorization': `Bearer ${this.config.relayApiKey}`
|
|
191
|
+
},
|
|
192
|
+
body: JSON.stringify({
|
|
193
|
+
success: result.success,
|
|
194
|
+
exit_code: result.exit_code,
|
|
195
|
+
stdout: result.stdout?.slice(0, 10000),
|
|
196
|
+
stderr: result.stderr?.slice(0, 10000),
|
|
197
|
+
error: result.error,
|
|
198
|
+
duration_ms: result.duration_ms
|
|
199
|
+
})
|
|
200
|
+
});
|
|
201
|
+
} catch (err) {
|
|
202
|
+
console.error(`[QueueService] Status report failed: ${err.message}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async storeExecutionResult(session_id, approval_id, tool_name, command, result) {
|
|
207
|
+
if (!this.config.relayApiUrl || !this.config.relayApiKey) return;
|
|
208
|
+
const payload = {
|
|
209
|
+
approval_id,
|
|
210
|
+
command: command || '',
|
|
211
|
+
tool_name,
|
|
212
|
+
exit_code: result.exit_code ?? null,
|
|
213
|
+
stdout: (result.stdout || '').slice(0, 10000),
|
|
214
|
+
stderr: (result.stderr || '').slice(0, 10000),
|
|
215
|
+
executed_at: result.executed_at || Date.now()
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
await fetch(`${this.config.relayApiUrl}/api/sessions/${encodeURIComponent(session_id)}/results`, {
|
|
220
|
+
method: 'POST',
|
|
221
|
+
headers: {
|
|
222
|
+
'Content-Type': 'application/json',
|
|
223
|
+
'Authorization': `Bearer ${this.config.relayApiKey}`
|
|
224
|
+
},
|
|
225
|
+
body: JSON.stringify(payload)
|
|
226
|
+
});
|
|
227
|
+
} catch (err) {
|
|
228
|
+
console.error(`[QueueService] Result storage failed: ${err.message}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async spawnClaudeProcess(session_id, prompt, options = {}) {
|
|
233
|
+
const session = this.state.sessions.get(session_id);
|
|
234
|
+
if (!session) throw new Error(`Session not found: ${session_id}`);
|
|
235
|
+
|
|
236
|
+
return new Promise((resolve) => {
|
|
237
|
+
const cwd = session.cwd || process.cwd();
|
|
238
|
+
const startedAt = Date.now();
|
|
239
|
+
|
|
240
|
+
let isToolExecution = false;
|
|
241
|
+
let commandToRun = '';
|
|
242
|
+
let agentPrompt = '';
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
const promptObj = JSON.parse(prompt);
|
|
246
|
+
if (promptObj.parameters?.command) {
|
|
247
|
+
isToolExecution = true;
|
|
248
|
+
commandToRun = promptObj.parameters.command;
|
|
249
|
+
}
|
|
250
|
+
} catch (e) {}
|
|
251
|
+
|
|
252
|
+
if (!isToolExecution) agentPrompt = prompt;
|
|
253
|
+
|
|
254
|
+
let child;
|
|
255
|
+
if (isToolExecution) {
|
|
256
|
+
child = spawn('sh', ['-c', commandToRun], {
|
|
257
|
+
cwd,
|
|
258
|
+
stdio: 'pipe',
|
|
259
|
+
env: { ...process.env, TELEPORTATION_DAEMON_CHILD: 'true' }
|
|
260
|
+
});
|
|
261
|
+
} else {
|
|
262
|
+
const cliBin = process.env.CLAUDE_CLI_PATH || 'claude';
|
|
263
|
+
const resumeSessionId = session.claude_session_id || session_id;
|
|
264
|
+
const args = ['--resume', resumeSessionId, '-p', agentPrompt, '--dangerously-skip-permissions'];
|
|
265
|
+
|
|
266
|
+
child = spawn(cliBin, args, {
|
|
267
|
+
cwd,
|
|
268
|
+
stdio: 'pipe',
|
|
269
|
+
env: { ...process.env, TELEPORTATION_DAEMON_CHILD: 'true', CI: 'true' }
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (options.onSpawn) options.onSpawn(child);
|
|
274
|
+
if (child.stdin) child.stdin.end();
|
|
275
|
+
|
|
276
|
+
let stdout = '';
|
|
277
|
+
let stderr = '';
|
|
278
|
+
let timedOut = false;
|
|
279
|
+
|
|
280
|
+
child.stdout.on('data', (data) => { stdout += data.toString(); });
|
|
281
|
+
child.stderr.on('data', (data) => { stderr += data.toString(); });
|
|
282
|
+
|
|
283
|
+
const timeout = setTimeout(() => {
|
|
284
|
+
timedOut = true;
|
|
285
|
+
child.kill('SIGTERM');
|
|
286
|
+
}, this.childTimeoutMs);
|
|
287
|
+
|
|
288
|
+
child.on('close', (code) => {
|
|
289
|
+
clearTimeout(timeout);
|
|
290
|
+
const executedAt = Date.now();
|
|
291
|
+
resolve({
|
|
292
|
+
success: code === 0 && !timedOut,
|
|
293
|
+
exit_code: code,
|
|
294
|
+
stdout: truncateOutput(stdout, 'STDOUT'),
|
|
295
|
+
stderr: truncateOutput(stderr, 'STDERR'),
|
|
296
|
+
error: timedOut ? 'Execution timed out' : null,
|
|
297
|
+
duration_ms: executedAt - startedAt,
|
|
298
|
+
started_at: startedAt,
|
|
299
|
+
executed_at: executedAt
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
child.on('error', (err) => {
|
|
304
|
+
clearTimeout(timeout);
|
|
305
|
+
resolve({
|
|
306
|
+
success: false,
|
|
307
|
+
exit_code: -1,
|
|
308
|
+
stdout: '',
|
|
309
|
+
stderr: '',
|
|
310
|
+
error: err.message,
|
|
311
|
+
duration_ms: 0,
|
|
312
|
+
started_at: startedAt,
|
|
313
|
+
executed_at: Date.now()
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Service
|
|
3
|
+
*
|
|
4
|
+
* Manages Claude Code sessions, registration, and liveness monitoring.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// NOTE: HeartbeatManager removed in PRD-0025 — daemon handles heartbeats inline.
|
|
8
|
+
|
|
9
|
+
export class SessionService {
|
|
10
|
+
constructor(state, config) {
|
|
11
|
+
this.name = 'sessions';
|
|
12
|
+
this.state = state;
|
|
13
|
+
this.config = config;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async start(ctx) {
|
|
17
|
+
this.ctx = ctx;
|
|
18
|
+
const { server } = ctx;
|
|
19
|
+
|
|
20
|
+
// Register routes
|
|
21
|
+
server.addRoute('POST', '/sessions/register', (req) => this.handleRegister(req));
|
|
22
|
+
server.addRoute('POST', '/sessions/stop', (req) => this.handleStop(req));
|
|
23
|
+
|
|
24
|
+
console.log('[SessionService] Started');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async stop() {
|
|
28
|
+
console.log('[SessionService] Stopped');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getStats() {
|
|
32
|
+
return {
|
|
33
|
+
activeSessions: this.state.sessions.size,
|
|
34
|
+
stoppedSessions: this.state.stoppedSessions.size
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async handleRegister(req) {
|
|
39
|
+
try {
|
|
40
|
+
const body = await req.json();
|
|
41
|
+
const { session_id, claude_session_id, cwd, meta } = body;
|
|
42
|
+
|
|
43
|
+
if (!session_id) {
|
|
44
|
+
return Response.json({ error: 'session_id is required' }, { status: 400 });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (this.state.stoppedSessions.has(session_id)) {
|
|
48
|
+
return Response.json({
|
|
49
|
+
error: 'session_stopped',
|
|
50
|
+
message: 'This session has ended and cannot be re-activated'
|
|
51
|
+
}, { status: 403 });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.state.sessions.set(session_id, {
|
|
55
|
+
session_id,
|
|
56
|
+
claude_session_id: claude_session_id || session_id,
|
|
57
|
+
cwd: cwd || process.cwd(),
|
|
58
|
+
meta: {
|
|
59
|
+
...(meta || {}),
|
|
60
|
+
daemon_pid: process.pid
|
|
61
|
+
},
|
|
62
|
+
registered_at: Date.now()
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
this.state.sessionActivity.set(session_id, Date.now());
|
|
66
|
+
this.state.lastSessionActivityAt = Date.now();
|
|
67
|
+
|
|
68
|
+
console.log(`[SessionService] Session registered: ${session_id}`);
|
|
69
|
+
|
|
70
|
+
// Update daemon_state on relay
|
|
71
|
+
if (this.config.relayApiUrl && this.config.relayApiKey) {
|
|
72
|
+
fetch(
|
|
73
|
+
`${this.config.relayApiUrl}/api/sessions/${encodeURIComponent(session_id)}/daemon-state`,
|
|
74
|
+
{
|
|
75
|
+
method: 'PATCH',
|
|
76
|
+
headers: {
|
|
77
|
+
'Authorization': `Bearer ${this.config.relayApiKey}`,
|
|
78
|
+
'Content-Type': 'application/json'
|
|
79
|
+
},
|
|
80
|
+
body: JSON.stringify({
|
|
81
|
+
status: 'running',
|
|
82
|
+
started_reason: 'session_registered'
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
).catch(err => {
|
|
86
|
+
console.error(`[SessionService] Failed to update daemon_state for ${session_id}: ${err.message}`);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return Response.json({ ok: true });
|
|
91
|
+
} catch (error) {
|
|
92
|
+
return Response.json({ error: error.message }, { status: 500 });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async handleStop(req) {
|
|
97
|
+
try {
|
|
98
|
+
const body = await req.json();
|
|
99
|
+
const { session_id } = body;
|
|
100
|
+
|
|
101
|
+
if (!session_id) {
|
|
102
|
+
return Response.json({ error: 'session_id is required' }, { status: 400 });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
this.state.sessions.delete(session_id);
|
|
106
|
+
this.state.stoppedSessions.add(session_id);
|
|
107
|
+
|
|
108
|
+
console.log(`[SessionService] Session stopped: ${session_id}`);
|
|
109
|
+
|
|
110
|
+
return Response.json({ ok: true });
|
|
111
|
+
} catch (error) {
|
|
112
|
+
return Response.json({ error: error.message }, { status: 500 });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon State
|
|
3
|
+
*
|
|
4
|
+
* Shared in-memory state for the teleportation daemon.
|
|
5
|
+
* This object is passed to services to allow them to share data.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const createDaemonState = () => ({
|
|
9
|
+
// In-memory registry of active teleportation sessions handled by this daemon
|
|
10
|
+
sessions: new Map(),
|
|
11
|
+
|
|
12
|
+
// Track sessions that have been explicitly stopped and should not be re-activated
|
|
13
|
+
stoppedSessions: new Set(),
|
|
14
|
+
|
|
15
|
+
// Session activity tracking for cleanup
|
|
16
|
+
sessionActivity: new Map(), // sessionId -> lastActivityTimestamp
|
|
17
|
+
|
|
18
|
+
// Heartbeat tracking: session_id -> { count, lastSent }
|
|
19
|
+
heartbeatState: new Map(),
|
|
20
|
+
|
|
21
|
+
// Transcript ingestion throttling: Map<session_id, Promise>
|
|
22
|
+
ingestionInProgress: new Map(),
|
|
23
|
+
|
|
24
|
+
// Approval queue: FIFO queue of pending approvals
|
|
25
|
+
approvalQueue: [],
|
|
26
|
+
|
|
27
|
+
// Execution tracking: approval_id -> execution details
|
|
28
|
+
executions: new Map(),
|
|
29
|
+
|
|
30
|
+
// Tracking last time we had any registered sessions
|
|
31
|
+
lastSessionActivityAt: Date.now(),
|
|
32
|
+
|
|
33
|
+
// Shutdown flag
|
|
34
|
+
isShuttingDown: false
|
|
35
|
+
});
|