@ctrl-spc/cli 1.1.11 → 1.1.13

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 CHANGED
@@ -36,6 +36,58 @@ export async function runControlScan(processor, log) {
36
36
  log(err instanceof Error ? err.message : 'Control request scan failed');
37
37
  }
38
38
  }
39
+ export async function markRecoveredSessionsDisconnected(session, now, log) {
40
+ const { data, error } = await session.client
41
+ .from('sessions')
42
+ .select('id, project_id')
43
+ .eq('device_id', session.machineId)
44
+ .eq('connection_state', 'connected')
45
+ .is('ended_at', null);
46
+ if (error) {
47
+ log(`Session recovery scan failed: ${error.message}`);
48
+ return;
49
+ }
50
+ const rows = (data ?? []);
51
+ if (rows.length === 0)
52
+ return;
53
+ const endedAt = new Date(now()).toISOString();
54
+ const ids = rows.map((row) => row.id);
55
+ const { error: updateError } = await session.client
56
+ .from('sessions')
57
+ .update({
58
+ connection_state: 'disconnected',
59
+ ended_at: endedAt,
60
+ exit_code: null,
61
+ stop_reason: 'daemon_restarted',
62
+ })
63
+ .in('id', ids);
64
+ if (updateError) {
65
+ log(`Session recovery update failed: ${updateError.message}`);
66
+ return;
67
+ }
68
+ const messages = rows.map((row) => ({
69
+ project_id: row.project_id,
70
+ session_id: row.id,
71
+ role: 'system',
72
+ kind: 'system',
73
+ content: 'This session was no longer owned by the current machine helper, so Control Space marked it disconnected. Start a fresh agent to continue.',
74
+ }));
75
+ const { error: messageError } = await session.client.from('agent_messages').insert(messages);
76
+ if (messageError)
77
+ log(`Session recovery message insert failed: ${messageError.message}`);
78
+ }
79
+ export async function touchOwnedSessions(session, ownedSessions, now, log) {
80
+ const ids = [...ownedSessions.keys()];
81
+ if (ids.length === 0)
82
+ return;
83
+ const { error } = await session.client
84
+ .from('sessions')
85
+ .update({ last_seen_at: new Date(now()).toISOString() })
86
+ .in('id', ids)
87
+ .is('ended_at', null);
88
+ if (error)
89
+ log(`Session heartbeat failed: ${error.message}`);
90
+ }
39
91
  export async function runDaemon() {
40
92
  const deps = {
41
93
  createSession: createSessionClient,
@@ -60,6 +112,7 @@ export async function runDaemon() {
60
112
  process.once('SIGINT', () => void shutdown());
61
113
  process.once('SIGTERM', () => void shutdown());
62
114
  session = await heartbeatOnce(session, deps);
115
+ await markRecoveredSessionsDisconnected(session, () => Date.now(), deps.log);
63
116
  let spawner = makeSpawner({ session, spawnAgent: nodeSpawnAgent, ownedSessions, now: () => Date.now(), log: deps.log });
64
117
  let controls = makeControlRequestProcessor({ session, spawnAgent: nodeSpawnAgent, ownedSessions, now: () => Date.now(), log: deps.log });
65
118
  await runSpawnScan(spawner, deps.log);
@@ -67,6 +120,7 @@ export async function runDaemon() {
67
120
  while (true) {
68
121
  await sleep(HEARTBEAT_INTERVAL_MS);
69
122
  session = await heartbeatOnce(session, deps);
123
+ await touchOwnedSessions(session, ownedSessions, () => Date.now(), deps.log);
70
124
  spawner = makeSpawner({ session, spawnAgent: nodeSpawnAgent, ownedSessions, now: () => Date.now(), log: deps.log });
71
125
  controls = makeControlRequestProcessor({ session, spawnAgent: nodeSpawnAgent, ownedSessions, now: () => Date.now(), log: deps.log });
72
126
  await runSpawnScan(spawner, deps.log);
@@ -5,13 +5,14 @@ export const PING_PROMPT = 'Reply with one short sentence confirming you are sti
5
5
  // Null columns are omitted so the CLI falls back to its own default.
6
6
  export function assembleCommand(req, prompt = READINESS_PROMPT) {
7
7
  if (req.agent_type === 'codex') {
8
- const args = ['exec', prompt];
8
+ const args = ['--ask-for-approval', 'on-request', 'exec'];
9
9
  if (req.model)
10
10
  args.push('--model', req.model);
11
11
  args.push('--sandbox', req.permission_mode ?? 'read-only');
12
- args.push('--ask-for-approval', 'on-request');
13
12
  if (req.effort)
14
13
  args.push('-c', `model_reasoning_effort=${req.effort}`);
14
+ args.push('--color', 'never');
15
+ args.push(prompt);
15
16
  return { cmd: 'codex', args };
16
17
  }
17
18
  // Default: Claude Code.
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}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ctrl-spc/cli",
3
- "version": "1.1.11",
3
+ "version": "1.1.13",
4
4
  "description": "Control Space CLI - machine setup and background daemon",
5
5
  "type": "module",
6
6
  "files": [