@parall/daemon 1.31.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 +16 -11
- package/bundle/package.json +1 -0
- package/bundle/parall-claude-agent.js +27556 -339
- package/bundle/parall-codex-agent.js +27603 -372
- package/bundle/parall-daemon.js +30204 -1760
- package/bundle/parall-openclaw-agent.js +49 -24
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +83 -83
- 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.map +1 -1
- package/dist/config.js +24 -24
- 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 +20 -15
- package/dist/runtimes.d.ts +3 -2
- package/dist/runtimes.d.ts.map +1 -1
- package/dist/runtimes.js +24 -20
- package/dist/supervisor.d.ts +9 -4
- package/dist/supervisor.d.ts.map +1 -1
- package/dist/supervisor.js +244 -76
- package/dist/updater-manifest.d.ts +2 -0
- package/dist/updater-manifest.d.ts.map +1 -1
- package/dist/updater-manifest.js +6 -6
- package/dist/updater.d.ts +2 -2
- package/dist/updater.d.ts.map +1 -1
- package/dist/updater.js +55 -37
- 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,11 +58,14 @@ 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;
|
|
61
65
|
updater = null;
|
|
62
66
|
healthConfirmed = false;
|
|
67
|
+
clipManager = null;
|
|
68
|
+
clipProvider = null;
|
|
63
69
|
constructor(config, client, log) {
|
|
64
70
|
this.config = config;
|
|
65
71
|
this.client = client;
|
|
@@ -71,29 +77,59 @@ export class DaemonSupervisor {
|
|
|
71
77
|
/** Start the supervisor. Returns a promise that resolves on `stop()`. */
|
|
72
78
|
async run(signal) {
|
|
73
79
|
if (this.running)
|
|
74
|
-
throw new Error(
|
|
80
|
+
throw new Error('supervisor already running');
|
|
75
81
|
this.running = true;
|
|
76
82
|
const onAbort = () => {
|
|
77
83
|
this.stop().catch((err) => this.log.error(`stop() failed: ${String(err)}`));
|
|
78
84
|
};
|
|
79
|
-
signal.addEventListener(
|
|
85
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
80
86
|
try {
|
|
81
87
|
if (!(await this.bootstrapWithRetry(signal))) {
|
|
82
|
-
signal.removeEventListener(
|
|
88
|
+
signal.removeEventListener('abort', onAbort);
|
|
83
89
|
this.running = false;
|
|
84
90
|
return;
|
|
85
91
|
}
|
|
86
92
|
}
|
|
87
93
|
catch (err) {
|
|
88
|
-
signal.removeEventListener(
|
|
94
|
+
signal.removeEventListener('abort', onAbort);
|
|
89
95
|
this.running = false;
|
|
90
96
|
throw err;
|
|
91
97
|
}
|
|
92
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
|
+
}
|
|
93
127
|
// Report daemon version via heartbeat (best-effort)
|
|
94
128
|
const daemonVersion = this.updater?.getLocalVersion();
|
|
95
129
|
if (daemonVersion) {
|
|
96
|
-
this.client
|
|
130
|
+
this.client
|
|
131
|
+
.postMachineHeartbeat(daemonVersion)
|
|
132
|
+
.catch((err) => this.log.warn(`daemon version report failed: ${String(err)}`));
|
|
97
133
|
}
|
|
98
134
|
await this.fullReconcile();
|
|
99
135
|
this.ws = new ParallWs({
|
|
@@ -101,8 +137,8 @@ export class DaemonSupervisor {
|
|
|
101
137
|
wsUrl: this.config.wsUrl,
|
|
102
138
|
reconnect: true,
|
|
103
139
|
});
|
|
104
|
-
this.ws.on(
|
|
105
|
-
this.log.info(
|
|
140
|
+
this.ws.on('machine.hello', (_data) => {
|
|
141
|
+
this.log.info('machine WS connected (machine.hello)');
|
|
106
142
|
if (!this.healthConfirmed && this.updater) {
|
|
107
143
|
try {
|
|
108
144
|
this.updater.confirmVersion();
|
|
@@ -117,50 +153,57 @@ export class DaemonSupervisor {
|
|
|
117
153
|
await this.fullReconcile();
|
|
118
154
|
})();
|
|
119
155
|
});
|
|
120
|
-
this.ws.on(
|
|
156
|
+
this.ws.on('machine.update', (data) => {
|
|
121
157
|
this.log.info(`WS: daemon update available — version=${data.new_version} mandatory=${data.mandatory}`);
|
|
122
158
|
if (this.updater) {
|
|
123
|
-
void this.updater
|
|
159
|
+
void this.updater
|
|
160
|
+
.triggerUpdate(data.new_version, data.mandatory)
|
|
161
|
+
.then(async (applied) => {
|
|
124
162
|
if (applied) {
|
|
125
|
-
this.log.info(
|
|
163
|
+
this.log.info('daemon update applied — stopping supervisor before restart');
|
|
126
164
|
await this.stop();
|
|
127
165
|
process.exit(42);
|
|
128
166
|
}
|
|
129
|
-
})
|
|
167
|
+
})
|
|
168
|
+
.catch((err) => {
|
|
130
169
|
this.log.warn(`daemon update failed: ${String(err)}`);
|
|
131
170
|
});
|
|
132
171
|
}
|
|
133
172
|
});
|
|
134
|
-
this.ws.on(
|
|
173
|
+
this.ws.on('machine.agent.attached', (data) => {
|
|
135
174
|
this.log.info(`WS: agent ${data.agent_id} attached`);
|
|
136
175
|
void this.handleAgentAttached(data.agent_id);
|
|
137
176
|
});
|
|
138
|
-
this.ws.on(
|
|
177
|
+
this.ws.on('machine.agent.detached', (data) => {
|
|
139
178
|
this.log.info(`WS: agent ${data.agent_id} detached`);
|
|
140
179
|
void this.handleAgentDetached(data.agent_id);
|
|
141
180
|
});
|
|
142
|
-
this.ws.on(
|
|
143
|
-
const newSource = data.llm_source ??
|
|
181
|
+
this.ws.on('machine.config.updated', (data) => {
|
|
182
|
+
const newSource = data.llm_source ?? 'parall';
|
|
144
183
|
if (newSource !== this.machineLlmSource) {
|
|
145
184
|
this.log.info(`WS: llm_source changed ${this.machineLlmSource} → ${newSource}, respawning all agents`);
|
|
146
185
|
this.machineLlmSource = newSource;
|
|
147
186
|
void this.respawnAllChildren();
|
|
148
187
|
}
|
|
149
188
|
});
|
|
150
|
-
this.ws.on(
|
|
189
|
+
this.ws.on('machine.workspace.setup.requested', (data) => {
|
|
151
190
|
this.log.info(`WS: workspace setup requested for agent ${data.agent_id}`);
|
|
152
191
|
void this.handleWorkspaceSetupRequested(data.agent_id);
|
|
153
192
|
});
|
|
154
|
-
this.ws.on(
|
|
193
|
+
this.ws.on('machine.filesystem.browse', (data) => {
|
|
155
194
|
this.log.info(`WS: filesystem browse requested: ${data.path}`);
|
|
156
195
|
void this.handleFilesystemBrowse(data.request_id, data.path);
|
|
157
196
|
});
|
|
158
|
-
this.ws.on(
|
|
159
|
-
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'})`);
|
|
160
199
|
void this.stop();
|
|
161
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
|
+
});
|
|
162
205
|
this.ws.onStateChange((state) => {
|
|
163
|
-
if (state ===
|
|
206
|
+
if (state === 'disconnected' || state === 'reconnecting') {
|
|
164
207
|
this.log.warn(`machine WS state: ${state}`);
|
|
165
208
|
}
|
|
166
209
|
});
|
|
@@ -168,7 +211,7 @@ export class DaemonSupervisor {
|
|
|
168
211
|
await new Promise((resolve) => {
|
|
169
212
|
this.stopResolve = resolve;
|
|
170
213
|
});
|
|
171
|
-
signal.removeEventListener(
|
|
214
|
+
signal.removeEventListener('abort', onAbort);
|
|
172
215
|
}
|
|
173
216
|
/** Disconnect WS, cancel timers, SIGTERM all children, await exit. */
|
|
174
217
|
async stop() {
|
|
@@ -192,9 +235,17 @@ export class DaemonSupervisor {
|
|
|
192
235
|
}
|
|
193
236
|
exits.push(this.terminateChild(state));
|
|
194
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
|
+
}
|
|
195
246
|
await Promise.allSettled(exits);
|
|
196
247
|
this.children.clear();
|
|
197
|
-
this.log.info(
|
|
248
|
+
this.log.info('daemon supervisor stopped');
|
|
198
249
|
if (this.stopResolve) {
|
|
199
250
|
this.stopResolve();
|
|
200
251
|
this.stopResolve = null;
|
|
@@ -206,8 +257,9 @@ export class DaemonSupervisor {
|
|
|
206
257
|
while (this.running && !signal.aborted) {
|
|
207
258
|
try {
|
|
208
259
|
const machine = await this.client.getMachineSelf();
|
|
260
|
+
this.machineId = machine.id;
|
|
209
261
|
this.machineOrgId = machine.org_id;
|
|
210
|
-
this.machineLlmSource = machine.llm_source ??
|
|
262
|
+
this.machineLlmSource = machine.llm_source ?? 'parall';
|
|
211
263
|
this.log.info(`daemon online — machine_id=${machine.id} org=${machine.org_id} label=${machine.label}`);
|
|
212
264
|
return true;
|
|
213
265
|
}
|
|
@@ -249,7 +301,7 @@ export class DaemonSupervisor {
|
|
|
249
301
|
this.log.warn(`skipping attached entry with no user_id (profile=${JSON.stringify(a.profile)})`);
|
|
250
302
|
continue;
|
|
251
303
|
}
|
|
252
|
-
if (a.user && a.user.status !==
|
|
304
|
+
if (a.user && a.user.status !== 'active') {
|
|
253
305
|
this.log.info(`agent ${userId} not active (status=${a.user.status}) — skipping`);
|
|
254
306
|
continue;
|
|
255
307
|
}
|
|
@@ -264,7 +316,7 @@ export class DaemonSupervisor {
|
|
|
264
316
|
await this.spawnAgent(userId, orgId, a);
|
|
265
317
|
}
|
|
266
318
|
else if (!existing.child && !existing.restartTimer && !existing.shuttingDown) {
|
|
267
|
-
await this.restartChildNow(existing,
|
|
319
|
+
await this.restartChildNow(existing, 'reconcile found no live child');
|
|
268
320
|
}
|
|
269
321
|
}
|
|
270
322
|
for (const [userId, state] of this.children) {
|
|
@@ -279,6 +331,9 @@ export class DaemonSupervisor {
|
|
|
279
331
|
this.children.delete(userId);
|
|
280
332
|
}
|
|
281
333
|
}
|
|
334
|
+
// Reconcile desired clip installs alongside agents (best-effort; never
|
|
335
|
+
// blocks agent reconcile).
|
|
336
|
+
await this.reconcileClipInstalls();
|
|
282
337
|
}
|
|
283
338
|
// ---- Flat layout migration (self-hosted → daemon) ----
|
|
284
339
|
/**
|
|
@@ -288,21 +343,21 @@ export class DaemonSupervisor {
|
|
|
288
343
|
*/
|
|
289
344
|
migrateFlatLayout() {
|
|
290
345
|
const root = this.config.rootStateDir;
|
|
291
|
-
const agentsDir = path.join(root,
|
|
292
|
-
const flatWorkspace = path.join(root,
|
|
346
|
+
const agentsDir = path.join(root, 'agents');
|
|
347
|
+
const flatWorkspace = path.join(root, 'workspace');
|
|
293
348
|
if (!fs.existsSync(flatWorkspace) || fs.existsSync(agentsDir))
|
|
294
349
|
return;
|
|
295
350
|
let ownerAgentId;
|
|
296
|
-
const sessionsDir = path.join(root,
|
|
351
|
+
const sessionsDir = path.join(root, 'sessions');
|
|
297
352
|
if (fs.existsSync(sessionsDir)) {
|
|
298
353
|
try {
|
|
299
354
|
for (const file of fs.readdirSync(sessionsDir)) {
|
|
300
|
-
if (!file.endsWith(
|
|
355
|
+
if (!file.endsWith('.json'))
|
|
301
356
|
continue;
|
|
302
|
-
const decoded = Buffer.from(file.replace(
|
|
357
|
+
const decoded = Buffer.from(file.replace('.json', ''), 'base64url').toString();
|
|
303
358
|
// runtimeKey format: "agent:main:{runtime}:{agentId}:orchestrator"
|
|
304
|
-
const parts = decoded.split(
|
|
305
|
-
if (parts.length >= 4 && parts[3].startsWith(
|
|
359
|
+
const parts = decoded.split(':');
|
|
360
|
+
if (parts.length >= 4 && parts[3].startsWith('usr_')) {
|
|
306
361
|
ownerAgentId = parts[3];
|
|
307
362
|
break;
|
|
308
363
|
}
|
|
@@ -312,11 +367,11 @@ export class DaemonSupervisor {
|
|
|
312
367
|
// best-effort scan
|
|
313
368
|
}
|
|
314
369
|
}
|
|
315
|
-
const targetId = ownerAgentId ??
|
|
370
|
+
const targetId = ownerAgentId ?? '_orphan';
|
|
316
371
|
const targetDir = path.join(agentsDir, targetId);
|
|
317
372
|
try {
|
|
318
373
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
319
|
-
for (const sub of [
|
|
374
|
+
for (const sub of ['workspace', 'sessions', 'dispatch-context']) {
|
|
320
375
|
const src = path.join(root, sub);
|
|
321
376
|
if (fs.existsSync(src)) {
|
|
322
377
|
fs.renameSync(src, path.join(targetDir, sub));
|
|
@@ -336,24 +391,24 @@ export class DaemonSupervisor {
|
|
|
336
391
|
}
|
|
337
392
|
catch (err) {
|
|
338
393
|
this.log.warn(`fetchAttachedAgent: listAttachedAgents failed: ${String(err)}`);
|
|
339
|
-
return { kind:
|
|
394
|
+
return { kind: 'retryable' };
|
|
340
395
|
}
|
|
341
396
|
const entry = attached.find((a) => (a.user?.id ?? a.profile.user_id) === agentId);
|
|
342
397
|
if (!entry) {
|
|
343
398
|
this.log.warn(`fetchAttachedAgent: agent ${agentId} not found in attached list`);
|
|
344
|
-
return { kind:
|
|
399
|
+
return { kind: 'skip' };
|
|
345
400
|
}
|
|
346
|
-
if (entry.user && entry.user.status !==
|
|
401
|
+
if (entry.user && entry.user.status !== 'active') {
|
|
347
402
|
this.log.info(`agent ${agentId} not active (status=${entry.user.status}) — skipping`);
|
|
348
|
-
return { kind:
|
|
403
|
+
return { kind: 'skip' };
|
|
349
404
|
}
|
|
350
|
-
return { kind:
|
|
405
|
+
return { kind: 'found', entry };
|
|
351
406
|
}
|
|
352
407
|
async handleAgentAttached(agentId) {
|
|
353
408
|
if (this.children.has(agentId) || this.spawningAgents.has(agentId))
|
|
354
409
|
return;
|
|
355
410
|
const result = await this.fetchAttachedAgent(agentId);
|
|
356
|
-
if (result.kind !==
|
|
411
|
+
if (result.kind !== 'found') {
|
|
357
412
|
return;
|
|
358
413
|
}
|
|
359
414
|
const orgId = this.machineOrgId;
|
|
@@ -406,6 +461,97 @@ export class DaemonSupervisor {
|
|
|
406
461
|
}
|
|
407
462
|
}
|
|
408
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
|
+
}
|
|
409
555
|
async handleWorkspaceSetupRequested(agentId) {
|
|
410
556
|
if (this.spawningAgents.has(agentId)) {
|
|
411
557
|
this.log.info(`agent ${agentId}: workspace setup already in progress; queueing one restart`);
|
|
@@ -413,12 +559,12 @@ export class DaemonSupervisor {
|
|
|
413
559
|
return;
|
|
414
560
|
}
|
|
415
561
|
const result = await this.fetchAttachedAgent(agentId);
|
|
416
|
-
if (result.kind ===
|
|
562
|
+
if (result.kind === 'retryable') {
|
|
417
563
|
this.pendingWorkspaceSetup.add(agentId);
|
|
418
564
|
this.scheduleWorkspaceSetupRetry(agentId);
|
|
419
565
|
return;
|
|
420
566
|
}
|
|
421
|
-
if (result.kind ===
|
|
567
|
+
if (result.kind === 'skip') {
|
|
422
568
|
this.pendingWorkspaceSetup.delete(agentId);
|
|
423
569
|
this.clearWorkspaceSetupRetry(agentId);
|
|
424
570
|
return;
|
|
@@ -465,7 +611,7 @@ export class DaemonSupervisor {
|
|
|
465
611
|
async refreshMachineConfig() {
|
|
466
612
|
try {
|
|
467
613
|
const machine = await this.client.getMachineSelf();
|
|
468
|
-
const newSource = machine.llm_source ??
|
|
614
|
+
const newSource = machine.llm_source ?? 'parall';
|
|
469
615
|
if (newSource !== this.machineLlmSource) {
|
|
470
616
|
this.log.info(`machine config refreshed: llm_source ${this.machineLlmSource} → ${newSource}`);
|
|
471
617
|
this.machineLlmSource = newSource;
|
|
@@ -483,7 +629,7 @@ export class DaemonSupervisor {
|
|
|
483
629
|
}
|
|
484
630
|
for (const state of states) {
|
|
485
631
|
if (!state.shuttingDown && this.running) {
|
|
486
|
-
await this.restartChildNow(state,
|
|
632
|
+
await this.restartChildNow(state, 'llm_source changed');
|
|
487
633
|
}
|
|
488
634
|
}
|
|
489
635
|
}
|
|
@@ -546,7 +692,7 @@ export class DaemonSupervisor {
|
|
|
546
692
|
catch (err) {
|
|
547
693
|
this.log.warn(`mkdir agent dirs (${agentId}) failed: ${String(err)}`);
|
|
548
694
|
}
|
|
549
|
-
const runtimeType = attached.profile.runtime_type ??
|
|
695
|
+
const runtimeType = attached.profile.runtime_type ?? 'claude-code';
|
|
550
696
|
let workspaceDir;
|
|
551
697
|
try {
|
|
552
698
|
workspaceDir = await prepareWorkspace({
|
|
@@ -571,7 +717,10 @@ export class DaemonSupervisor {
|
|
|
571
717
|
runtimeType,
|
|
572
718
|
workspacePath: workspaceDir,
|
|
573
719
|
claudeHome,
|
|
574
|
-
|
|
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,
|
|
575
724
|
child: null,
|
|
576
725
|
credential,
|
|
577
726
|
restartAttempts: 0,
|
|
@@ -595,11 +744,25 @@ export class DaemonSupervisor {
|
|
|
595
744
|
workspaceDir: state.workspacePath,
|
|
596
745
|
claudeHome: state.claudeHome,
|
|
597
746
|
};
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
|
|
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, {
|
|
601
764
|
env,
|
|
602
|
-
stdio: [
|
|
765
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
603
766
|
detached: false,
|
|
604
767
|
});
|
|
605
768
|
state.child = child;
|
|
@@ -613,10 +776,10 @@ export class DaemonSupervisor {
|
|
|
613
776
|
const wasShutting = state.shuttingDown;
|
|
614
777
|
state.child = null;
|
|
615
778
|
if (err) {
|
|
616
|
-
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)' : ''}`);
|
|
617
780
|
}
|
|
618
781
|
else {
|
|
619
|
-
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)' : ''}`);
|
|
620
783
|
}
|
|
621
784
|
if (wasShutting || !this.running)
|
|
622
785
|
return;
|
|
@@ -628,14 +791,19 @@ export class DaemonSupervisor {
|
|
|
628
791
|
this.startChild(state);
|
|
629
792
|
}, delay);
|
|
630
793
|
};
|
|
631
|
-
child.once(
|
|
794
|
+
child.once('error', (err) => {
|
|
632
795
|
if (err.code === 'ENOENT') {
|
|
633
|
-
|
|
634
|
-
|
|
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
|
+
}
|
|
635
803
|
}
|
|
636
|
-
settleChild(
|
|
804
|
+
settleChild('error', null, null, err);
|
|
637
805
|
});
|
|
638
|
-
child.once(
|
|
806
|
+
child.once('close', (code, signal) => settleChild('close', code, signal));
|
|
639
807
|
const stableTimer = setTimeout(() => {
|
|
640
808
|
if (state.child === child) {
|
|
641
809
|
state.restartAttempts = 0;
|
|
@@ -649,25 +817,25 @@ export class DaemonSupervisor {
|
|
|
649
817
|
return;
|
|
650
818
|
return new Promise((resolve) => {
|
|
651
819
|
const onExit = () => resolve();
|
|
652
|
-
child.once(
|
|
820
|
+
child.once('exit', onExit);
|
|
653
821
|
try {
|
|
654
|
-
child.kill(
|
|
822
|
+
child.kill('SIGTERM');
|
|
655
823
|
}
|
|
656
824
|
catch (err) {
|
|
657
825
|
this.log.warn(`SIGTERM ${state.agentId} threw: ${String(err)}`);
|
|
658
|
-
child.off(
|
|
826
|
+
child.off('exit', onExit);
|
|
659
827
|
resolve();
|
|
660
828
|
return;
|
|
661
829
|
}
|
|
662
830
|
const hardKill = setTimeout(() => {
|
|
663
831
|
try {
|
|
664
|
-
child.kill(
|
|
832
|
+
child.kill('SIGKILL');
|
|
665
833
|
}
|
|
666
834
|
catch {
|
|
667
835
|
/* already gone */
|
|
668
836
|
}
|
|
669
837
|
}, 10_000);
|
|
670
|
-
child.once(
|
|
838
|
+
child.once('exit', () => clearTimeout(hardKill));
|
|
671
839
|
});
|
|
672
840
|
}
|
|
673
841
|
ensureSharedCredentialLink(agentClaudeHome, agentId) {
|
|
@@ -694,7 +862,7 @@ export class DaemonSupervisor {
|
|
|
694
862
|
}
|
|
695
863
|
}
|
|
696
864
|
catch (err) {
|
|
697
|
-
if (err.code !==
|
|
865
|
+
if (err.code !== 'ENOENT') {
|
|
698
866
|
throw err;
|
|
699
867
|
}
|
|
700
868
|
}
|
|
@@ -6,12 +6,14 @@ export interface RemoteManifest {
|
|
|
6
6
|
version: string;
|
|
7
7
|
built_at: string;
|
|
8
8
|
min_daemon_version?: string;
|
|
9
|
+
min_node_version?: string;
|
|
9
10
|
files: Record<string, ManifestFile>;
|
|
10
11
|
signature: string;
|
|
11
12
|
}
|
|
12
13
|
export interface LocalManifest {
|
|
13
14
|
version: string;
|
|
14
15
|
built_at: string;
|
|
16
|
+
min_node_version?: string;
|
|
15
17
|
files: Record<string, ManifestFile>;
|
|
16
18
|
}
|
|
17
19
|
/**
|
|
@@ -1 +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,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,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"}
|
|
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"}
|