@ctrl-spc/cli 1.1.12 → 1.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/daemon.js +62 -2
- package/dist/spawner.js +1 -0
- package/package.json +1 -1
package/dist/daemon.js
CHANGED
|
@@ -3,6 +3,7 @@ import { loadMachineState } from './session.js';
|
|
|
3
3
|
import { makeSpawner, nodeSpawnAgent } from './spawner.js';
|
|
4
4
|
import { makeControlRequestProcessor } from './controlRequests.js';
|
|
5
5
|
export const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
6
|
+
export const WORK_SCAN_INTERVAL_MS = 2_000;
|
|
6
7
|
const SESSION_RETRY_INTERVAL_MS = 5_000;
|
|
7
8
|
export async function heartbeatOnce(session, deps) {
|
|
8
9
|
try {
|
|
@@ -36,6 +37,58 @@ export async function runControlScan(processor, log) {
|
|
|
36
37
|
log(err instanceof Error ? err.message : 'Control request scan failed');
|
|
37
38
|
}
|
|
38
39
|
}
|
|
40
|
+
export async function markRecoveredSessionsDisconnected(session, now, log) {
|
|
41
|
+
const { data, error } = await session.client
|
|
42
|
+
.from('sessions')
|
|
43
|
+
.select('id, project_id')
|
|
44
|
+
.eq('device_id', session.machineId)
|
|
45
|
+
.eq('connection_state', 'connected')
|
|
46
|
+
.is('ended_at', null);
|
|
47
|
+
if (error) {
|
|
48
|
+
log(`Session recovery scan failed: ${error.message}`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const rows = (data ?? []);
|
|
52
|
+
if (rows.length === 0)
|
|
53
|
+
return;
|
|
54
|
+
const endedAt = new Date(now()).toISOString();
|
|
55
|
+
const ids = rows.map((row) => row.id);
|
|
56
|
+
const { error: updateError } = await session.client
|
|
57
|
+
.from('sessions')
|
|
58
|
+
.update({
|
|
59
|
+
connection_state: 'disconnected',
|
|
60
|
+
ended_at: endedAt,
|
|
61
|
+
exit_code: null,
|
|
62
|
+
stop_reason: 'daemon_restarted',
|
|
63
|
+
})
|
|
64
|
+
.in('id', ids);
|
|
65
|
+
if (updateError) {
|
|
66
|
+
log(`Session recovery update failed: ${updateError.message}`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const messages = rows.map((row) => ({
|
|
70
|
+
project_id: row.project_id,
|
|
71
|
+
session_id: row.id,
|
|
72
|
+
role: 'system',
|
|
73
|
+
kind: 'system',
|
|
74
|
+
content: 'This session was no longer owned by the current machine helper, so Control Space marked it disconnected. Start a fresh agent to continue.',
|
|
75
|
+
}));
|
|
76
|
+
const { error: messageError } = await session.client.from('agent_messages').insert(messages);
|
|
77
|
+
if (messageError)
|
|
78
|
+
log(`Session recovery message insert failed: ${messageError.message}`);
|
|
79
|
+
}
|
|
80
|
+
export async function touchOwnedSessions(session, ownedSessions, now, log) {
|
|
81
|
+
const ids = [...ownedSessions.keys()];
|
|
82
|
+
if (ids.length === 0)
|
|
83
|
+
return;
|
|
84
|
+
const { error } = await session.client
|
|
85
|
+
.from('sessions')
|
|
86
|
+
.update({ last_seen_at: new Date(now()).toISOString() })
|
|
87
|
+
.in('id', ids)
|
|
88
|
+
.is('ended_at', null);
|
|
89
|
+
if (error)
|
|
90
|
+
log(`Session heartbeat failed: ${error.message}`);
|
|
91
|
+
}
|
|
39
92
|
export async function runDaemon() {
|
|
40
93
|
const deps = {
|
|
41
94
|
createSession: createSessionClient,
|
|
@@ -60,13 +113,20 @@ export async function runDaemon() {
|
|
|
60
113
|
process.once('SIGINT', () => void shutdown());
|
|
61
114
|
process.once('SIGTERM', () => void shutdown());
|
|
62
115
|
session = await heartbeatOnce(session, deps);
|
|
116
|
+
await markRecoveredSessionsDisconnected(session, () => Date.now(), deps.log);
|
|
117
|
+
let lastHeartbeatAt = Date.now();
|
|
63
118
|
let spawner = makeSpawner({ session, spawnAgent: nodeSpawnAgent, ownedSessions, now: () => Date.now(), log: deps.log });
|
|
64
119
|
let controls = makeControlRequestProcessor({ session, spawnAgent: nodeSpawnAgent, ownedSessions, now: () => Date.now(), log: deps.log });
|
|
65
120
|
await runSpawnScan(spawner, deps.log);
|
|
66
121
|
await runControlScan(controls, deps.log);
|
|
67
122
|
while (true) {
|
|
68
|
-
await sleep(
|
|
69
|
-
|
|
123
|
+
await sleep(WORK_SCAN_INTERVAL_MS);
|
|
124
|
+
const now = Date.now();
|
|
125
|
+
if (now - lastHeartbeatAt >= HEARTBEAT_INTERVAL_MS) {
|
|
126
|
+
session = await heartbeatOnce(session, deps);
|
|
127
|
+
await touchOwnedSessions(session, ownedSessions, () => now, deps.log);
|
|
128
|
+
lastHeartbeatAt = now;
|
|
129
|
+
}
|
|
70
130
|
spawner = makeSpawner({ session, spawnAgent: nodeSpawnAgent, ownedSessions, now: () => Date.now(), log: deps.log });
|
|
71
131
|
controls = makeControlRequestProcessor({ session, spawnAgent: nodeSpawnAgent, ownedSessions, now: () => Date.now(), log: deps.log });
|
|
72
132
|
await runSpawnScan(spawner, deps.log);
|
package/dist/spawner.js
CHANGED
|
@@ -57,6 +57,7 @@ export function makeSpawner(deps) {
|
|
|
57
57
|
permission_mode: row.permission_mode,
|
|
58
58
|
allowed_tools: row.allowed_tools,
|
|
59
59
|
pid,
|
|
60
|
+
last_seen_at: new Date(deps.now()).toISOString(),
|
|
60
61
|
}).select('id').single();
|
|
61
62
|
if (error)
|
|
62
63
|
throw new Error(`Session insert failed: ${error.message}`);
|