@parall/daemon 1.30.0 → 1.32.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/bundle/manifest.json +17 -11
- package/bundle/package.json +1 -0
- package/bundle/parall-claude-agent.js +27671 -337
- package/bundle/parall-codex-agent.js +27715 -375
- package/bundle/parall-daemon.js +30421 -1355
- package/bundle/parall-openclaw-agent.js +51 -26
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +122 -72
- package/dist/clip-runtime/clip-installer.d.ts +44 -0
- package/dist/clip-runtime/clip-installer.d.ts.map +1 -0
- package/dist/clip-runtime/clip-installer.js +501 -0
- package/dist/clip-runtime/clip-provider.d.ts +76 -0
- package/dist/clip-runtime/clip-provider.d.ts.map +1 -0
- package/dist/clip-runtime/clip-provider.js +402 -0
- package/dist/clip-runtime/index.d.ts +7 -0
- package/dist/clip-runtime/index.d.ts.map +1 -0
- package/dist/clip-runtime/index.js +5 -0
- package/dist/clip-runtime/ipc.d.ts +94 -0
- package/dist/clip-runtime/ipc.d.ts.map +1 -0
- package/dist/clip-runtime/ipc.js +98 -0
- package/dist/clip-runtime/manifest.d.ts +74 -0
- package/dist/clip-runtime/manifest.d.ts.map +1 -0
- package/dist/clip-runtime/manifest.js +181 -0
- package/dist/clip-runtime/process-manager.d.ts +57 -0
- package/dist/clip-runtime/process-manager.d.ts.map +1 -0
- package/dist/clip-runtime/process-manager.js +354 -0
- package/dist/clip-runtime/process.d.ts +59 -0
- package/dist/clip-runtime/process.d.ts.map +1 -0
- package/dist/clip-runtime/process.js +350 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +36 -19
- package/dist/filesystem.d.ts +1 -1
- package/dist/filesystem.d.ts.map +1 -1
- package/dist/filesystem.js +51 -53
- package/dist/index.js +46 -14
- package/dist/runtimes.d.ts +10 -8
- package/dist/runtimes.d.ts.map +1 -1
- package/dist/runtimes.js +63 -95
- package/dist/supervisor.d.ts +12 -3
- package/dist/supervisor.d.ts.map +1 -1
- package/dist/supervisor.js +272 -71
- package/dist/updater-manifest.d.ts +41 -0
- package/dist/updater-manifest.d.ts.map +1 -0
- package/dist/updater-manifest.js +94 -0
- package/dist/updater.d.ts +60 -0
- package/dist/updater.d.ts.map +1 -0
- package/dist/updater.js +427 -0
- package/dist/workspace.d.ts +2 -2
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +112 -112
- package/package.json +6 -6
package/dist/supervisor.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
import { spawn } from
|
|
2
|
-
import * as fs from
|
|
3
|
-
import * as path from
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { effectiveLLMSourceExplicit } from '@parall/agent-core';
|
|
5
|
+
import { ParallWs, } from '@parall/sdk';
|
|
6
|
+
import { listDirectory } from './filesystem.js';
|
|
7
|
+
import { agentClaudeCredentialsFileFor, agentClaudeHomeFor, agentStateDirFor, agentWorkspaceDirFor, sharedClaudeCredentialsFileFor, } from './config.js';
|
|
8
|
+
import { assertAgentKey, getRuntimeAdapter } from './runtimes.js';
|
|
9
|
+
import { prepareWorkspace } from './workspace.js';
|
|
10
|
+
import { ClipProcessManager, ClipProvider } from './clip-runtime/index.js';
|
|
11
|
+
import { installClip, parseSource } from './clip-runtime/clip-installer.js';
|
|
9
12
|
const RUNTIME_PACKAGES = {
|
|
10
13
|
'claude-code': '@parall/claude-agent',
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
codex: '@parall/codex-agent',
|
|
15
|
+
openclaw: '@parall/openclaw-agent',
|
|
13
16
|
};
|
|
14
17
|
const WORKSPACE_SETUP_RETRY_DELAY_MS = 5_000;
|
|
15
18
|
/**
|
|
@@ -22,14 +25,14 @@ function sleepCancellable(ms, signal) {
|
|
|
22
25
|
return Promise.resolve(false);
|
|
23
26
|
return new Promise((resolve) => {
|
|
24
27
|
const timer = setTimeout(() => {
|
|
25
|
-
signal.removeEventListener(
|
|
28
|
+
signal.removeEventListener('abort', onAbort);
|
|
26
29
|
resolve(true);
|
|
27
30
|
}, ms);
|
|
28
31
|
const onAbort = () => {
|
|
29
32
|
clearTimeout(timer);
|
|
30
33
|
resolve(false);
|
|
31
34
|
};
|
|
32
|
-
signal.addEventListener(
|
|
35
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
33
36
|
});
|
|
34
37
|
}
|
|
35
38
|
export { sleepCancellable };
|
|
@@ -55,79 +58,152 @@ export class DaemonSupervisor {
|
|
|
55
58
|
cancelledSpawns = new Set();
|
|
56
59
|
ws = null;
|
|
57
60
|
running = false;
|
|
61
|
+
machineId = null;
|
|
58
62
|
machineOrgId = null;
|
|
59
|
-
machineLlmSource =
|
|
63
|
+
machineLlmSource = 'parall';
|
|
60
64
|
stopResolve = null;
|
|
65
|
+
updater = null;
|
|
66
|
+
healthConfirmed = false;
|
|
67
|
+
clipManager = null;
|
|
68
|
+
clipProvider = null;
|
|
61
69
|
constructor(config, client, log) {
|
|
62
70
|
this.config = config;
|
|
63
71
|
this.client = client;
|
|
64
72
|
this.log = log;
|
|
65
73
|
}
|
|
74
|
+
setUpdater(updater) {
|
|
75
|
+
this.updater = updater;
|
|
76
|
+
}
|
|
66
77
|
/** Start the supervisor. Returns a promise that resolves on `stop()`. */
|
|
67
78
|
async run(signal) {
|
|
68
79
|
if (this.running)
|
|
69
|
-
throw new Error(
|
|
80
|
+
throw new Error('supervisor already running');
|
|
70
81
|
this.running = true;
|
|
71
82
|
const onAbort = () => {
|
|
72
83
|
this.stop().catch((err) => this.log.error(`stop() failed: ${String(err)}`));
|
|
73
84
|
};
|
|
74
|
-
signal.addEventListener(
|
|
85
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
75
86
|
try {
|
|
76
87
|
if (!(await this.bootstrapWithRetry(signal))) {
|
|
77
|
-
signal.removeEventListener(
|
|
88
|
+
signal.removeEventListener('abort', onAbort);
|
|
78
89
|
this.running = false;
|
|
79
90
|
return;
|
|
80
91
|
}
|
|
81
92
|
}
|
|
82
93
|
catch (err) {
|
|
83
|
-
signal.removeEventListener(
|
|
94
|
+
signal.removeEventListener('abort', onAbort);
|
|
84
95
|
this.running = false;
|
|
85
96
|
throw err;
|
|
86
97
|
}
|
|
87
98
|
this.migrateFlatLayout();
|
|
99
|
+
// Initialize Clip runtime manager (optional, for Pinix Clip subprocess management)
|
|
100
|
+
if (process.env.PRLL_CLIP_RUNTIME_ENABLED === 'true') {
|
|
101
|
+
this.clipManager = new ClipProcessManager({
|
|
102
|
+
clipsDir: path.join(this.config.rootStateDir, 'clips'),
|
|
103
|
+
dataDir: path.join(this.config.rootStateDir, 'clip-data'),
|
|
104
|
+
});
|
|
105
|
+
try {
|
|
106
|
+
await this.clipManager.loadInstalledClips();
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
this.log.warn(`clip runtime init failed: ${String(err)}`);
|
|
110
|
+
}
|
|
111
|
+
// Connect to Clip Service as a Provider when URL is configured
|
|
112
|
+
const clipServiceUrl = process.env.PRLL_CLIP_SERVICE_URL?.trim();
|
|
113
|
+
if (clipServiceUrl && this.machineOrgId && this.machineId) {
|
|
114
|
+
this.clipProvider = new ClipProvider({
|
|
115
|
+
serviceUrl: clipServiceUrl,
|
|
116
|
+
authKey: this.config.apiKey,
|
|
117
|
+
orgId: this.machineOrgId,
|
|
118
|
+
providerName: `daemon-${this.machineId}`,
|
|
119
|
+
clipManager: this.clipManager,
|
|
120
|
+
log: this.log,
|
|
121
|
+
});
|
|
122
|
+
this.clipProvider
|
|
123
|
+
.connect()
|
|
124
|
+
.catch((err) => this.log.warn(`clip provider connect failed: ${String(err)}`));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Report daemon version via heartbeat (best-effort)
|
|
128
|
+
const daemonVersion = this.updater?.getLocalVersion();
|
|
129
|
+
if (daemonVersion) {
|
|
130
|
+
this.client
|
|
131
|
+
.postMachineHeartbeat(daemonVersion)
|
|
132
|
+
.catch((err) => this.log.warn(`daemon version report failed: ${String(err)}`));
|
|
133
|
+
}
|
|
88
134
|
await this.fullReconcile();
|
|
89
135
|
this.ws = new ParallWs({
|
|
90
136
|
getTicket: () => this.client.getMachineWsTicket(),
|
|
91
137
|
wsUrl: this.config.wsUrl,
|
|
92
138
|
reconnect: true,
|
|
93
139
|
});
|
|
94
|
-
this.ws.on(
|
|
95
|
-
this.log.info(
|
|
140
|
+
this.ws.on('machine.hello', (_data) => {
|
|
141
|
+
this.log.info('machine WS connected (machine.hello)');
|
|
142
|
+
if (!this.healthConfirmed && this.updater) {
|
|
143
|
+
try {
|
|
144
|
+
this.updater.confirmVersion();
|
|
145
|
+
this.healthConfirmed = true;
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
this.log.warn(`confirmVersion failed: ${String(err)}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
96
151
|
void (async () => {
|
|
97
152
|
await this.refreshMachineConfig();
|
|
98
153
|
await this.fullReconcile();
|
|
99
154
|
})();
|
|
100
155
|
});
|
|
101
|
-
this.ws.on(
|
|
156
|
+
this.ws.on('machine.update', (data) => {
|
|
157
|
+
this.log.info(`WS: daemon update available — version=${data.new_version} mandatory=${data.mandatory}`);
|
|
158
|
+
if (this.updater) {
|
|
159
|
+
void this.updater
|
|
160
|
+
.triggerUpdate(data.new_version, data.mandatory)
|
|
161
|
+
.then(async (applied) => {
|
|
162
|
+
if (applied) {
|
|
163
|
+
this.log.info('daemon update applied — stopping supervisor before restart');
|
|
164
|
+
await this.stop();
|
|
165
|
+
process.exit(42);
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
.catch((err) => {
|
|
169
|
+
this.log.warn(`daemon update failed: ${String(err)}`);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
this.ws.on('machine.agent.attached', (data) => {
|
|
102
174
|
this.log.info(`WS: agent ${data.agent_id} attached`);
|
|
103
175
|
void this.handleAgentAttached(data.agent_id);
|
|
104
176
|
});
|
|
105
|
-
this.ws.on(
|
|
177
|
+
this.ws.on('machine.agent.detached', (data) => {
|
|
106
178
|
this.log.info(`WS: agent ${data.agent_id} detached`);
|
|
107
179
|
void this.handleAgentDetached(data.agent_id);
|
|
108
180
|
});
|
|
109
|
-
this.ws.on(
|
|
110
|
-
const newSource = data.llm_source ??
|
|
181
|
+
this.ws.on('machine.config.updated', (data) => {
|
|
182
|
+
const newSource = data.llm_source ?? 'parall';
|
|
111
183
|
if (newSource !== this.machineLlmSource) {
|
|
112
184
|
this.log.info(`WS: llm_source changed ${this.machineLlmSource} → ${newSource}, respawning all agents`);
|
|
113
185
|
this.machineLlmSource = newSource;
|
|
114
186
|
void this.respawnAllChildren();
|
|
115
187
|
}
|
|
116
188
|
});
|
|
117
|
-
this.ws.on(
|
|
189
|
+
this.ws.on('machine.workspace.setup.requested', (data) => {
|
|
118
190
|
this.log.info(`WS: workspace setup requested for agent ${data.agent_id}`);
|
|
119
191
|
void this.handleWorkspaceSetupRequested(data.agent_id);
|
|
120
192
|
});
|
|
121
|
-
this.ws.on(
|
|
193
|
+
this.ws.on('machine.filesystem.browse', (data) => {
|
|
122
194
|
this.log.info(`WS: filesystem browse requested: ${data.path}`);
|
|
123
195
|
void this.handleFilesystemBrowse(data.request_id, data.path);
|
|
124
196
|
});
|
|
125
|
-
this.ws.on(
|
|
126
|
-
this.log.info(`WS: machine.stop received (reason=${data.reason ??
|
|
197
|
+
this.ws.on('machine.stop', (data) => {
|
|
198
|
+
this.log.info(`WS: machine.stop received (reason=${data.reason ?? 'none'})`);
|
|
127
199
|
void this.stop();
|
|
128
200
|
});
|
|
201
|
+
this.ws.on('machine.clip.install', (data) => {
|
|
202
|
+
this.log.info(`WS: clip install requested — alias=${data.alias} source=${data.source_ref}`);
|
|
203
|
+
void this.handleClipInstall(data);
|
|
204
|
+
});
|
|
129
205
|
this.ws.onStateChange((state) => {
|
|
130
|
-
if (state ===
|
|
206
|
+
if (state === 'disconnected' || state === 'reconnecting') {
|
|
131
207
|
this.log.warn(`machine WS state: ${state}`);
|
|
132
208
|
}
|
|
133
209
|
});
|
|
@@ -135,7 +211,7 @@ export class DaemonSupervisor {
|
|
|
135
211
|
await new Promise((resolve) => {
|
|
136
212
|
this.stopResolve = resolve;
|
|
137
213
|
});
|
|
138
|
-
signal.removeEventListener(
|
|
214
|
+
signal.removeEventListener('abort', onAbort);
|
|
139
215
|
}
|
|
140
216
|
/** Disconnect WS, cancel timers, SIGTERM all children, await exit. */
|
|
141
217
|
async stop() {
|
|
@@ -159,9 +235,17 @@ export class DaemonSupervisor {
|
|
|
159
235
|
}
|
|
160
236
|
exits.push(this.terminateChild(state));
|
|
161
237
|
}
|
|
238
|
+
if (this.clipProvider) {
|
|
239
|
+
exits.push(this.clipProvider.disconnect());
|
|
240
|
+
this.clipProvider = null;
|
|
241
|
+
}
|
|
242
|
+
if (this.clipManager) {
|
|
243
|
+
exits.push(this.clipManager.stopAll());
|
|
244
|
+
this.clipManager = null;
|
|
245
|
+
}
|
|
162
246
|
await Promise.allSettled(exits);
|
|
163
247
|
this.children.clear();
|
|
164
|
-
this.log.info(
|
|
248
|
+
this.log.info('daemon supervisor stopped');
|
|
165
249
|
if (this.stopResolve) {
|
|
166
250
|
this.stopResolve();
|
|
167
251
|
this.stopResolve = null;
|
|
@@ -173,8 +257,9 @@ export class DaemonSupervisor {
|
|
|
173
257
|
while (this.running && !signal.aborted) {
|
|
174
258
|
try {
|
|
175
259
|
const machine = await this.client.getMachineSelf();
|
|
260
|
+
this.machineId = machine.id;
|
|
176
261
|
this.machineOrgId = machine.org_id;
|
|
177
|
-
this.machineLlmSource = machine.llm_source ??
|
|
262
|
+
this.machineLlmSource = machine.llm_source ?? 'parall';
|
|
178
263
|
this.log.info(`daemon online — machine_id=${machine.id} org=${machine.org_id} label=${machine.label}`);
|
|
179
264
|
return true;
|
|
180
265
|
}
|
|
@@ -216,7 +301,7 @@ export class DaemonSupervisor {
|
|
|
216
301
|
this.log.warn(`skipping attached entry with no user_id (profile=${JSON.stringify(a.profile)})`);
|
|
217
302
|
continue;
|
|
218
303
|
}
|
|
219
|
-
if (a.user && a.user.status !==
|
|
304
|
+
if (a.user && a.user.status !== 'active') {
|
|
220
305
|
this.log.info(`agent ${userId} not active (status=${a.user.status}) — skipping`);
|
|
221
306
|
continue;
|
|
222
307
|
}
|
|
@@ -231,7 +316,7 @@ export class DaemonSupervisor {
|
|
|
231
316
|
await this.spawnAgent(userId, orgId, a);
|
|
232
317
|
}
|
|
233
318
|
else if (!existing.child && !existing.restartTimer && !existing.shuttingDown) {
|
|
234
|
-
await this.restartChildNow(existing,
|
|
319
|
+
await this.restartChildNow(existing, 'reconcile found no live child');
|
|
235
320
|
}
|
|
236
321
|
}
|
|
237
322
|
for (const [userId, state] of this.children) {
|
|
@@ -246,6 +331,9 @@ export class DaemonSupervisor {
|
|
|
246
331
|
this.children.delete(userId);
|
|
247
332
|
}
|
|
248
333
|
}
|
|
334
|
+
// Reconcile desired clip installs alongside agents (best-effort; never
|
|
335
|
+
// blocks agent reconcile).
|
|
336
|
+
await this.reconcileClipInstalls();
|
|
249
337
|
}
|
|
250
338
|
// ---- Flat layout migration (self-hosted → daemon) ----
|
|
251
339
|
/**
|
|
@@ -255,21 +343,21 @@ export class DaemonSupervisor {
|
|
|
255
343
|
*/
|
|
256
344
|
migrateFlatLayout() {
|
|
257
345
|
const root = this.config.rootStateDir;
|
|
258
|
-
const agentsDir = path.join(root,
|
|
259
|
-
const flatWorkspace = path.join(root,
|
|
346
|
+
const agentsDir = path.join(root, 'agents');
|
|
347
|
+
const flatWorkspace = path.join(root, 'workspace');
|
|
260
348
|
if (!fs.existsSync(flatWorkspace) || fs.existsSync(agentsDir))
|
|
261
349
|
return;
|
|
262
350
|
let ownerAgentId;
|
|
263
|
-
const sessionsDir = path.join(root,
|
|
351
|
+
const sessionsDir = path.join(root, 'sessions');
|
|
264
352
|
if (fs.existsSync(sessionsDir)) {
|
|
265
353
|
try {
|
|
266
354
|
for (const file of fs.readdirSync(sessionsDir)) {
|
|
267
|
-
if (!file.endsWith(
|
|
355
|
+
if (!file.endsWith('.json'))
|
|
268
356
|
continue;
|
|
269
|
-
const decoded = Buffer.from(file.replace(
|
|
357
|
+
const decoded = Buffer.from(file.replace('.json', ''), 'base64url').toString();
|
|
270
358
|
// runtimeKey format: "agent:main:{runtime}:{agentId}:orchestrator"
|
|
271
|
-
const parts = decoded.split(
|
|
272
|
-
if (parts.length >= 4 && parts[3].startsWith(
|
|
359
|
+
const parts = decoded.split(':');
|
|
360
|
+
if (parts.length >= 4 && parts[3].startsWith('usr_')) {
|
|
273
361
|
ownerAgentId = parts[3];
|
|
274
362
|
break;
|
|
275
363
|
}
|
|
@@ -279,11 +367,11 @@ export class DaemonSupervisor {
|
|
|
279
367
|
// best-effort scan
|
|
280
368
|
}
|
|
281
369
|
}
|
|
282
|
-
const targetId = ownerAgentId ??
|
|
370
|
+
const targetId = ownerAgentId ?? '_orphan';
|
|
283
371
|
const targetDir = path.join(agentsDir, targetId);
|
|
284
372
|
try {
|
|
285
373
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
286
|
-
for (const sub of [
|
|
374
|
+
for (const sub of ['workspace', 'sessions', 'dispatch-context']) {
|
|
287
375
|
const src = path.join(root, sub);
|
|
288
376
|
if (fs.existsSync(src)) {
|
|
289
377
|
fs.renameSync(src, path.join(targetDir, sub));
|
|
@@ -303,24 +391,24 @@ export class DaemonSupervisor {
|
|
|
303
391
|
}
|
|
304
392
|
catch (err) {
|
|
305
393
|
this.log.warn(`fetchAttachedAgent: listAttachedAgents failed: ${String(err)}`);
|
|
306
|
-
return { kind:
|
|
394
|
+
return { kind: 'retryable' };
|
|
307
395
|
}
|
|
308
396
|
const entry = attached.find((a) => (a.user?.id ?? a.profile.user_id) === agentId);
|
|
309
397
|
if (!entry) {
|
|
310
398
|
this.log.warn(`fetchAttachedAgent: agent ${agentId} not found in attached list`);
|
|
311
|
-
return { kind:
|
|
399
|
+
return { kind: 'skip' };
|
|
312
400
|
}
|
|
313
|
-
if (entry.user && entry.user.status !==
|
|
401
|
+
if (entry.user && entry.user.status !== 'active') {
|
|
314
402
|
this.log.info(`agent ${agentId} not active (status=${entry.user.status}) — skipping`);
|
|
315
|
-
return { kind:
|
|
403
|
+
return { kind: 'skip' };
|
|
316
404
|
}
|
|
317
|
-
return { kind:
|
|
405
|
+
return { kind: 'found', entry };
|
|
318
406
|
}
|
|
319
407
|
async handleAgentAttached(agentId) {
|
|
320
408
|
if (this.children.has(agentId) || this.spawningAgents.has(agentId))
|
|
321
409
|
return;
|
|
322
410
|
const result = await this.fetchAttachedAgent(agentId);
|
|
323
|
-
if (result.kind !==
|
|
411
|
+
if (result.kind !== 'found') {
|
|
324
412
|
return;
|
|
325
413
|
}
|
|
326
414
|
const orgId = this.machineOrgId;
|
|
@@ -373,6 +461,97 @@ export class DaemonSupervisor {
|
|
|
373
461
|
}
|
|
374
462
|
}
|
|
375
463
|
}
|
|
464
|
+
async handleClipInstall(data) {
|
|
465
|
+
if (!this.clipManager) {
|
|
466
|
+
this.log.warn('clip install event received but clip runtime is disabled');
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
try {
|
|
470
|
+
// Idempotency guard: if the clip is already installed and running
|
|
471
|
+
// locally, a prior install succeeded but its server ACK
|
|
472
|
+
// (reportClipInstall) may have failed — the desired-state row is still
|
|
473
|
+
// pending, so reconcile re-sent the same directive. Don't re-download or
|
|
474
|
+
// restart a healthy clip; just retry the report so pending → installed.
|
|
475
|
+
if (this.clipManager.isRunning(data.alias)) {
|
|
476
|
+
this.log.info(`clip already running, re-reporting install: ${data.alias}`);
|
|
477
|
+
await this.client.reportClipInstall(data.clip_id, data.version).catch((err) => {
|
|
478
|
+
this.log.warn(`clip install re-report failed for ${data.alias}: ${String(err)}`);
|
|
479
|
+
});
|
|
480
|
+
return true;
|
|
481
|
+
}
|
|
482
|
+
const clipsDir = path.join(this.config.rootStateDir, 'clips');
|
|
483
|
+
// The clip record stores a resolved version; pin the install to it when
|
|
484
|
+
// the source ref is unversioned, otherwise an unversioned source like
|
|
485
|
+
// "@pinix/github-tools" would silently fetch latest instead.
|
|
486
|
+
let source = data.source_ref;
|
|
487
|
+
if (data.version) {
|
|
488
|
+
let sourceHasVersion = false;
|
|
489
|
+
try {
|
|
490
|
+
sourceHasVersion = !!parseSource(source).version;
|
|
491
|
+
}
|
|
492
|
+
catch {
|
|
493
|
+
/* fall through to append */
|
|
494
|
+
}
|
|
495
|
+
if (!sourceHasVersion)
|
|
496
|
+
source = `${source}@${data.version}`;
|
|
497
|
+
}
|
|
498
|
+
const result = await installClip({
|
|
499
|
+
source,
|
|
500
|
+
alias: data.alias,
|
|
501
|
+
clipsDir,
|
|
502
|
+
// Pinix registry REST base; falls back to the installer default when unset.
|
|
503
|
+
registryUrl: process.env.PRLL_PINIX_REGISTRY_URL?.trim() || undefined,
|
|
504
|
+
});
|
|
505
|
+
this.log.info(`clip installed: ${result.alias} v${result.version} at ${result.path}`);
|
|
506
|
+
this.clipManager.registerClip({
|
|
507
|
+
name: result.alias,
|
|
508
|
+
package: result.name,
|
|
509
|
+
version: result.version,
|
|
510
|
+
source: result.path,
|
|
511
|
+
path: result.path,
|
|
512
|
+
});
|
|
513
|
+
await this.clipManager.startClip(result.alias);
|
|
514
|
+
this.log.info(`clip started: ${result.alias}`);
|
|
515
|
+
// Report success so the server flips the desired-state row pending →
|
|
516
|
+
// installed. Reporting here (not in the caller) covers both the live WS
|
|
517
|
+
// broadcast and the durable reconcile paths with one report.
|
|
518
|
+
await this.client.reportClipInstall(data.clip_id, data.version).catch((err) => {
|
|
519
|
+
this.log.warn(`clip install report failed for ${data.alias}: ${String(err)}`);
|
|
520
|
+
});
|
|
521
|
+
return true;
|
|
522
|
+
}
|
|
523
|
+
catch (err) {
|
|
524
|
+
this.log.error(`clip install failed: ${String(err)}`);
|
|
525
|
+
return false;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
// Durable reconcile: pull the registry clips this machine should have (covers
|
|
529
|
+
// installs broadcast while the daemon was offline, and clips targeted at
|
|
530
|
+
// machines that joined later), install the missing ones, and report success
|
|
531
|
+
// so the server flips the desired-state row pending → installed. Mirrors the
|
|
532
|
+
// listAttachedAgents reconcile pattern; runs on every fullReconcile.
|
|
533
|
+
async reconcileClipInstalls() {
|
|
534
|
+
if (!this.clipManager)
|
|
535
|
+
return; // clip runtime disabled — nothing to reconcile
|
|
536
|
+
let desired;
|
|
537
|
+
try {
|
|
538
|
+
desired = await this.client.listClipInstalls();
|
|
539
|
+
}
|
|
540
|
+
catch (err) {
|
|
541
|
+
this.log.warn(`reconcileClipInstalls: list failed: ${String(err)}`);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
for (const d of desired) {
|
|
545
|
+
// handleClipInstall reports success to the server on its own, so the
|
|
546
|
+
// reconcile loop just drives the install for each desired clip.
|
|
547
|
+
await this.handleClipInstall({
|
|
548
|
+
clip_id: d.clip_id,
|
|
549
|
+
alias: d.alias,
|
|
550
|
+
source_ref: d.source_ref ?? '',
|
|
551
|
+
version: d.version,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
}
|
|
376
555
|
async handleWorkspaceSetupRequested(agentId) {
|
|
377
556
|
if (this.spawningAgents.has(agentId)) {
|
|
378
557
|
this.log.info(`agent ${agentId}: workspace setup already in progress; queueing one restart`);
|
|
@@ -380,12 +559,12 @@ export class DaemonSupervisor {
|
|
|
380
559
|
return;
|
|
381
560
|
}
|
|
382
561
|
const result = await this.fetchAttachedAgent(agentId);
|
|
383
|
-
if (result.kind ===
|
|
562
|
+
if (result.kind === 'retryable') {
|
|
384
563
|
this.pendingWorkspaceSetup.add(agentId);
|
|
385
564
|
this.scheduleWorkspaceSetupRetry(agentId);
|
|
386
565
|
return;
|
|
387
566
|
}
|
|
388
|
-
if (result.kind ===
|
|
567
|
+
if (result.kind === 'skip') {
|
|
389
568
|
this.pendingWorkspaceSetup.delete(agentId);
|
|
390
569
|
this.clearWorkspaceSetupRetry(agentId);
|
|
391
570
|
return;
|
|
@@ -432,7 +611,7 @@ export class DaemonSupervisor {
|
|
|
432
611
|
async refreshMachineConfig() {
|
|
433
612
|
try {
|
|
434
613
|
const machine = await this.client.getMachineSelf();
|
|
435
|
-
const newSource = machine.llm_source ??
|
|
614
|
+
const newSource = machine.llm_source ?? 'parall';
|
|
436
615
|
if (newSource !== this.machineLlmSource) {
|
|
437
616
|
this.log.info(`machine config refreshed: llm_source ${this.machineLlmSource} → ${newSource}`);
|
|
438
617
|
this.machineLlmSource = newSource;
|
|
@@ -450,7 +629,7 @@ export class DaemonSupervisor {
|
|
|
450
629
|
}
|
|
451
630
|
for (const state of states) {
|
|
452
631
|
if (!state.shuttingDown && this.running) {
|
|
453
|
-
await this.restartChildNow(state,
|
|
632
|
+
await this.restartChildNow(state, 'llm_source changed');
|
|
454
633
|
}
|
|
455
634
|
}
|
|
456
635
|
}
|
|
@@ -513,7 +692,7 @@ export class DaemonSupervisor {
|
|
|
513
692
|
catch (err) {
|
|
514
693
|
this.log.warn(`mkdir agent dirs (${agentId}) failed: ${String(err)}`);
|
|
515
694
|
}
|
|
516
|
-
const runtimeType = attached.profile.runtime_type ??
|
|
695
|
+
const runtimeType = attached.profile.runtime_type ?? 'claude-code';
|
|
517
696
|
let workspaceDir;
|
|
518
697
|
try {
|
|
519
698
|
workspaceDir = await prepareWorkspace({
|
|
@@ -538,7 +717,10 @@ export class DaemonSupervisor {
|
|
|
538
717
|
runtimeType,
|
|
539
718
|
workspacePath: workspaceDir,
|
|
540
719
|
claudeHome,
|
|
541
|
-
|
|
720
|
+
// Store the RAW per-agent provider_config; the machine-level fallback is
|
|
721
|
+
// resolved at startChild time (see resolveProviderConfig) so a respawn
|
|
722
|
+
// after a machine llm_source change picks up the new source.
|
|
723
|
+
providerConfig: attached.provider_config,
|
|
542
724
|
child: null,
|
|
543
725
|
credential,
|
|
544
726
|
restartAttempts: 0,
|
|
@@ -562,11 +744,25 @@ export class DaemonSupervisor {
|
|
|
562
744
|
workspaceDir: state.workspacePath,
|
|
563
745
|
claudeHome: state.claudeHome,
|
|
564
746
|
};
|
|
565
|
-
const
|
|
566
|
-
|
|
567
|
-
|
|
747
|
+
const baseEnv = { ...process.env, PRLL_API_URL: this.config.apiUrl };
|
|
748
|
+
if (this.machineId)
|
|
749
|
+
baseEnv.PRLL_MACHINE_ID = this.machineId;
|
|
750
|
+
// An explicit per-agent provider_config (llm_source or BYO creds) wins;
|
|
751
|
+
// otherwise defer to the CURRENT machine-level llm_source. The server sends
|
|
752
|
+
// an empty `{}` for self-hosted agents (per-agent provider_config is
|
|
753
|
+
// rejected there), and `{} ?? machineLlmSource` would keep the `{}` and
|
|
754
|
+
// resolve to "parall", silently shadowing the machine's choice — so check
|
|
755
|
+
// for an explicit signal. Resolved here (not at spawn) so a respawn after a
|
|
756
|
+
// machine llm_source change applies the new source without a full restart.
|
|
757
|
+
const effectiveProviderConfig = effectiveLLMSourceExplicit(state.providerConfig)
|
|
758
|
+
? state.providerConfig
|
|
759
|
+
: { llm_source: this.machineLlmSource };
|
|
760
|
+
const env = adapter.buildEnv(baseEnv, state.agentId, state.orgId, state.credential.api_key, dirs, effectiveProviderConfig);
|
|
761
|
+
const spawnCmd = adapter.args.length > 0 ? `${adapter.bin} ${adapter.args.join(' ')}` : adapter.bin;
|
|
762
|
+
this.log.info(`spawning agent ${state.agentId} runtime=${state.runtimeType} cmd=${spawnCmd} (attempt ${state.restartAttempts + 1})`);
|
|
763
|
+
const child = spawn(adapter.bin, adapter.args, {
|
|
568
764
|
env,
|
|
569
|
-
stdio: [
|
|
765
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
570
766
|
detached: false,
|
|
571
767
|
});
|
|
572
768
|
state.child = child;
|
|
@@ -580,10 +776,10 @@ export class DaemonSupervisor {
|
|
|
580
776
|
const wasShutting = state.shuttingDown;
|
|
581
777
|
state.child = null;
|
|
582
778
|
if (err) {
|
|
583
|
-
this.log.error(`agent ${state.agentId} child ${event}: ${String(err)}${wasShutting ?
|
|
779
|
+
this.log.error(`agent ${state.agentId} child ${event}: ${String(err)}${wasShutting ? ' (shutting down)' : ''}`);
|
|
584
780
|
}
|
|
585
781
|
else {
|
|
586
|
-
this.log.info(`agent ${state.agentId} exited code=${code ??
|
|
782
|
+
this.log.info(`agent ${state.agentId} exited code=${code ?? 'null'} signal=${signal ?? 'null'}${wasShutting ? ' (shutting down)' : ''}`);
|
|
587
783
|
}
|
|
588
784
|
if (wasShutting || !this.running)
|
|
589
785
|
return;
|
|
@@ -595,14 +791,19 @@ export class DaemonSupervisor {
|
|
|
595
791
|
this.startChild(state);
|
|
596
792
|
}, delay);
|
|
597
793
|
};
|
|
598
|
-
child.once(
|
|
794
|
+
child.once('error', (err) => {
|
|
599
795
|
if (err.code === 'ENOENT') {
|
|
600
|
-
|
|
601
|
-
|
|
796
|
+
if (adapter.args.length > 0) {
|
|
797
|
+
this.log.error(`Node runtime "${adapter.bin}" not found — is the Desktop app installed?`);
|
|
798
|
+
}
|
|
799
|
+
else {
|
|
800
|
+
const pkg = RUNTIME_PACKAGES[state.runtimeType] ?? `@parall/${state.runtimeType}-agent`;
|
|
801
|
+
this.log.error(`Runtime binary "${adapter.bin}" not found in PATH. Install: npm install -g ${pkg}`);
|
|
802
|
+
}
|
|
602
803
|
}
|
|
603
|
-
settleChild(
|
|
804
|
+
settleChild('error', null, null, err);
|
|
604
805
|
});
|
|
605
|
-
child.once(
|
|
806
|
+
child.once('close', (code, signal) => settleChild('close', code, signal));
|
|
606
807
|
const stableTimer = setTimeout(() => {
|
|
607
808
|
if (state.child === child) {
|
|
608
809
|
state.restartAttempts = 0;
|
|
@@ -616,25 +817,25 @@ export class DaemonSupervisor {
|
|
|
616
817
|
return;
|
|
617
818
|
return new Promise((resolve) => {
|
|
618
819
|
const onExit = () => resolve();
|
|
619
|
-
child.once(
|
|
820
|
+
child.once('exit', onExit);
|
|
620
821
|
try {
|
|
621
|
-
child.kill(
|
|
822
|
+
child.kill('SIGTERM');
|
|
622
823
|
}
|
|
623
824
|
catch (err) {
|
|
624
825
|
this.log.warn(`SIGTERM ${state.agentId} threw: ${String(err)}`);
|
|
625
|
-
child.off(
|
|
826
|
+
child.off('exit', onExit);
|
|
626
827
|
resolve();
|
|
627
828
|
return;
|
|
628
829
|
}
|
|
629
830
|
const hardKill = setTimeout(() => {
|
|
630
831
|
try {
|
|
631
|
-
child.kill(
|
|
832
|
+
child.kill('SIGKILL');
|
|
632
833
|
}
|
|
633
834
|
catch {
|
|
634
835
|
/* already gone */
|
|
635
836
|
}
|
|
636
837
|
}, 10_000);
|
|
637
|
-
child.once(
|
|
838
|
+
child.once('exit', () => clearTimeout(hardKill));
|
|
638
839
|
});
|
|
639
840
|
}
|
|
640
841
|
ensureSharedCredentialLink(agentClaudeHome, agentId) {
|
|
@@ -661,7 +862,7 @@ export class DaemonSupervisor {
|
|
|
661
862
|
}
|
|
662
863
|
}
|
|
663
864
|
catch (err) {
|
|
664
|
-
if (err.code !==
|
|
865
|
+
if (err.code !== 'ENOENT') {
|
|
665
866
|
throw err;
|
|
666
867
|
}
|
|
667
868
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface ManifestFile {
|
|
2
|
+
sha256: string;
|
|
3
|
+
size: number;
|
|
4
|
+
}
|
|
5
|
+
export interface RemoteManifest {
|
|
6
|
+
version: string;
|
|
7
|
+
built_at: string;
|
|
8
|
+
min_daemon_version?: string;
|
|
9
|
+
min_node_version?: string;
|
|
10
|
+
files: Record<string, ManifestFile>;
|
|
11
|
+
signature: string;
|
|
12
|
+
}
|
|
13
|
+
export interface LocalManifest {
|
|
14
|
+
version: string;
|
|
15
|
+
built_at: string;
|
|
16
|
+
min_node_version?: string;
|
|
17
|
+
files: Record<string, ManifestFile>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Recursively sort all object keys at every depth for deterministic JSON output.
|
|
21
|
+
* Arrays preserve order; primitives pass through.
|
|
22
|
+
*/
|
|
23
|
+
export declare function canonicalize(obj: unknown): unknown;
|
|
24
|
+
/**
|
|
25
|
+
* Verify the Ed25519 signature of a remote manifest.
|
|
26
|
+
* Signs canonical JSON of all fields except `signature`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function verifyManifestSignature(manifest: RemoteManifest, publicKey: string): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Compare two semver strings (including prerelease tags). Returns:
|
|
31
|
+
* -1 if a < b
|
|
32
|
+
* 0 if a == b
|
|
33
|
+
* 1 if a > b
|
|
34
|
+
* null if either is not valid semver
|
|
35
|
+
*
|
|
36
|
+
* Prerelease ordering follows SemVer 2.0: a version with prerelease has
|
|
37
|
+
* lower precedence than the same version without prerelease. Prerelease
|
|
38
|
+
* identifiers are compared lexicographically when both present.
|
|
39
|
+
*/
|
|
40
|
+
export declare function semverCompare(a: string, b: string): number | null;
|
|
41
|
+
//# sourceMappingURL=updater-manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"updater-manifest.d.ts","sourceRoot":"","sources":["../src/updater-manifest.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACpC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;CACrC;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAUlD;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAQ5F;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAiBjE"}
|