@ctrl-spc/cli 1.1.13 → 1.1.15

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.
@@ -11,6 +11,14 @@ export function makeControlRequestProcessor(deps) {
11
11
  .limit(1);
12
12
  return data?.[0]?.value ?? null;
13
13
  }
14
+ async function resolveMachineLabel() {
15
+ const { data } = await deps.session.client
16
+ .from('devices')
17
+ .select('label')
18
+ .eq('id', deps.session.machineId)
19
+ .single();
20
+ return (data?.label ?? 'This machine').trim() || 'This machine';
21
+ }
14
22
  async function mark(row, patch) {
15
23
  await deps.session.client.from('agent_control_requests').update(patch).eq('id', row.id);
16
24
  }
@@ -44,7 +52,7 @@ export function makeControlRequestProcessor(deps) {
44
52
  async function findSession(row) {
45
53
  let query = deps.session.client
46
54
  .from('sessions')
47
- .select('id, project_id, agent_type, model, effort, permission_mode, allowed_tools')
55
+ .select('id, project_id, device_id, agent_type, model, effort, permission_mode, allowed_tools')
48
56
  .eq('project_id', row.project_id)
49
57
  .is('ended_at', null);
50
58
  if (row.session_id)
@@ -66,8 +74,7 @@ export function makeControlRequestProcessor(deps) {
66
74
  throw new Error(`Message insert failed: ${error.message}`);
67
75
  return data?.id ?? null;
68
76
  }
69
- async function processPing(row) {
70
- const session = await findSession(row);
77
+ async function processPing(row, session) {
71
78
  if (!session) {
72
79
  await mark(row, completedPatch('failed', 'No active agent session was found.'));
73
80
  return;
@@ -80,15 +87,15 @@ export function makeControlRequestProcessor(deps) {
80
87
  const { cmd, args } = assembleCommand(session, PING_PROMPT);
81
88
  const handle = deps.spawnAgent(cmd, args, cwd);
82
89
  const stdout = await collectStdout(handle);
83
- const responseId = await insertMessage(row, session.id, 'agent', 'ping', cleanAgentText(stdout));
90
+ const machineLabel = await resolveMachineLabel();
91
+ const responseId = await insertMessage(row, session.id, 'agent', 'ping', `${machineLabel} responded: ${cleanAgentText(stdout)}`);
84
92
  await mark(row, { ...completedPatch('succeeded'), response_message_id: responseId });
85
93
  }
86
- async function processStop(row) {
94
+ async function processStop(row, session) {
87
95
  const owned = row.session_id
88
96
  ? deps.ownedSessions.get(row.session_id)
89
97
  : [...deps.ownedSessions.values()].find((entry) => entry.projectId === row.project_id);
90
98
  if (!owned) {
91
- const session = await findSession(row);
92
99
  if (session) {
93
100
  const endedAt = new Date(deps.now()).toISOString();
94
101
  await deps.session.client.from('sessions').update({
@@ -117,13 +124,16 @@ export function makeControlRequestProcessor(deps) {
117
124
  await mark(row, { status: 'succeeded', completed_at: endedAt });
118
125
  }
119
126
  async function processOne(row) {
127
+ const session = await findSession(row);
128
+ if (session?.device_id && session.device_id !== deps.session.machineId)
129
+ return;
120
130
  await mark(row, { status: 'processing' });
121
131
  if (row.action === 'ping') {
122
- await processPing(row);
132
+ await processPing(row, session);
123
133
  return;
124
134
  }
125
135
  if (row.action === 'stop') {
126
- await processStop(row);
136
+ await processStop(row, session);
127
137
  return;
128
138
  }
129
139
  await mark(row, completedPatch('failed', 'This control request is not supported by this version of the daemon.'));
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 {
@@ -113,14 +114,19 @@ export async function runDaemon() {
113
114
  process.once('SIGTERM', () => void shutdown());
114
115
  session = await heartbeatOnce(session, deps);
115
116
  await markRecoveredSessionsDisconnected(session, () => Date.now(), deps.log);
117
+ let lastHeartbeatAt = Date.now();
116
118
  let spawner = makeSpawner({ session, spawnAgent: nodeSpawnAgent, ownedSessions, now: () => Date.now(), log: deps.log });
117
119
  let controls = makeControlRequestProcessor({ session, spawnAgent: nodeSpawnAgent, ownedSessions, now: () => Date.now(), log: deps.log });
118
120
  await runSpawnScan(spawner, deps.log);
119
121
  await runControlScan(controls, deps.log);
120
122
  while (true) {
121
- await sleep(HEARTBEAT_INTERVAL_MS);
122
- session = await heartbeatOnce(session, deps);
123
- await touchOwnedSessions(session, ownedSessions, () => Date.now(), deps.log);
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
+ }
124
130
  spawner = makeSpawner({ session, spawnAgent: nodeSpawnAgent, ownedSessions, now: () => Date.now(), log: deps.log });
125
131
  controls = makeControlRequestProcessor({ session, spawnAgent: nodeSpawnAgent, ownedSessions, now: () => Date.now(), log: deps.log });
126
132
  await runSpawnScan(spawner, deps.log);
package/dist/spawner.js CHANGED
@@ -77,6 +77,8 @@ export function makeSpawner(deps) {
77
77
  async function processOne(row) {
78
78
  if (!isRemote(row.agent_type))
79
79
  return; // manual types are never auto-fulfilled
80
+ if (row.target_device_id && row.target_device_id !== deps.session.machineId)
81
+ return;
80
82
  const last = lastAttempt.get(row.id);
81
83
  if (last !== undefined && deps.now() - last < backoffMs)
82
84
  return;
@@ -102,7 +104,7 @@ export function makeSpawner(deps) {
102
104
  async function processPending() {
103
105
  const { data, error } = await deps.session.client
104
106
  .from('spawn_requests')
105
- .select('id, project_id, agent_type, model, effort, permission_mode, allowed_tools')
107
+ .select('id, project_id, target_device_id, agent_type, model, effort, permission_mode, allowed_tools')
106
108
  .eq('status', 'pending');
107
109
  if (error) {
108
110
  deps.log(`spawn_requests scan failed: ${error.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ctrl-spc/cli",
3
- "version": "1.1.13",
3
+ "version": "1.1.15",
4
4
  "description": "Control Space CLI - machine setup and background daemon",
5
5
  "type": "module",
6
6
  "files": [