@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.
Files changed (52) hide show
  1. package/bundle/manifest.json +16 -11
  2. package/bundle/package.json +1 -0
  3. package/bundle/parall-claude-agent.js +27556 -339
  4. package/bundle/parall-codex-agent.js +27603 -372
  5. package/bundle/parall-daemon.js +30204 -1760
  6. package/bundle/parall-openclaw-agent.js +49 -24
  7. package/dist/cli.d.ts +1 -1
  8. package/dist/cli.d.ts.map +1 -1
  9. package/dist/cli.js +83 -83
  10. package/dist/clip-runtime/clip-installer.d.ts +44 -0
  11. package/dist/clip-runtime/clip-installer.d.ts.map +1 -0
  12. package/dist/clip-runtime/clip-installer.js +501 -0
  13. package/dist/clip-runtime/clip-provider.d.ts +76 -0
  14. package/dist/clip-runtime/clip-provider.d.ts.map +1 -0
  15. package/dist/clip-runtime/clip-provider.js +402 -0
  16. package/dist/clip-runtime/index.d.ts +7 -0
  17. package/dist/clip-runtime/index.d.ts.map +1 -0
  18. package/dist/clip-runtime/index.js +5 -0
  19. package/dist/clip-runtime/ipc.d.ts +94 -0
  20. package/dist/clip-runtime/ipc.d.ts.map +1 -0
  21. package/dist/clip-runtime/ipc.js +98 -0
  22. package/dist/clip-runtime/manifest.d.ts +74 -0
  23. package/dist/clip-runtime/manifest.d.ts.map +1 -0
  24. package/dist/clip-runtime/manifest.js +181 -0
  25. package/dist/clip-runtime/process-manager.d.ts +57 -0
  26. package/dist/clip-runtime/process-manager.d.ts.map +1 -0
  27. package/dist/clip-runtime/process-manager.js +354 -0
  28. package/dist/clip-runtime/process.d.ts +59 -0
  29. package/dist/clip-runtime/process.d.ts.map +1 -0
  30. package/dist/clip-runtime/process.js +350 -0
  31. package/dist/config.d.ts.map +1 -1
  32. package/dist/config.js +24 -24
  33. package/dist/filesystem.d.ts +1 -1
  34. package/dist/filesystem.d.ts.map +1 -1
  35. package/dist/filesystem.js +51 -53
  36. package/dist/index.js +20 -15
  37. package/dist/runtimes.d.ts +3 -2
  38. package/dist/runtimes.d.ts.map +1 -1
  39. package/dist/runtimes.js +24 -20
  40. package/dist/supervisor.d.ts +9 -4
  41. package/dist/supervisor.d.ts.map +1 -1
  42. package/dist/supervisor.js +244 -76
  43. package/dist/updater-manifest.d.ts +2 -0
  44. package/dist/updater-manifest.d.ts.map +1 -1
  45. package/dist/updater-manifest.js +6 -6
  46. package/dist/updater.d.ts +2 -2
  47. package/dist/updater.d.ts.map +1 -1
  48. package/dist/updater.js +55 -37
  49. package/dist/workspace.d.ts +2 -2
  50. package/dist/workspace.d.ts.map +1 -1
  51. package/dist/workspace.js +112 -112
  52. package/package.json +6 -6
@@ -1,15 +1,18 @@
1
- import { spawn } from "node:child_process";
2
- import * as fs from "node:fs";
3
- import * as path from "node:path";
4
- import { ParallWs } from "@parall/sdk";
5
- import { listDirectory } from "./filesystem.js";
6
- import { agentClaudeCredentialsFileFor, agentClaudeHomeFor, agentStateDirFor, agentWorkspaceDirFor, sharedClaudeCredentialsFileFor, } from "./config.js";
7
- import { assertAgentKey, getRuntimeAdapter } from "./runtimes.js";
8
- import { prepareWorkspace } from "./workspace.js";
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
- 'codex': '@parall/codex-agent',
12
- 'openclaw': '@parall/openclaw-agent',
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("abort", onAbort);
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("abort", onAbort, { once: true });
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 = "parall";
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("supervisor already running");
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("abort", onAbort, { once: true });
85
+ signal.addEventListener('abort', onAbort, { once: true });
80
86
  try {
81
87
  if (!(await this.bootstrapWithRetry(signal))) {
82
- signal.removeEventListener("abort", onAbort);
88
+ signal.removeEventListener('abort', onAbort);
83
89
  this.running = false;
84
90
  return;
85
91
  }
86
92
  }
87
93
  catch (err) {
88
- signal.removeEventListener("abort", onAbort);
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.postMachineHeartbeat(daemonVersion).catch((err) => this.log.warn(`daemon version report failed: ${String(err)}`));
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("machine.hello", (_data) => {
105
- this.log.info("machine WS connected (machine.hello)");
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("machine.update", (data) => {
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.triggerUpdate(data.new_version, data.mandatory).then(async (applied) => {
159
+ void this.updater
160
+ .triggerUpdate(data.new_version, data.mandatory)
161
+ .then(async (applied) => {
124
162
  if (applied) {
125
- this.log.info("daemon update applied — stopping supervisor before restart");
163
+ this.log.info('daemon update applied — stopping supervisor before restart');
126
164
  await this.stop();
127
165
  process.exit(42);
128
166
  }
129
- }).catch((err) => {
167
+ })
168
+ .catch((err) => {
130
169
  this.log.warn(`daemon update failed: ${String(err)}`);
131
170
  });
132
171
  }
133
172
  });
134
- this.ws.on("machine.agent.attached", (data) => {
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("machine.agent.detached", (data) => {
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("machine.config.updated", (data) => {
143
- const newSource = data.llm_source ?? "parall";
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("machine.workspace.setup.requested", (data) => {
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("machine.filesystem.browse", (data) => {
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("machine.stop", (data) => {
159
- this.log.info(`WS: machine.stop received (reason=${data.reason ?? "none"})`);
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 === "disconnected" || state === "reconnecting") {
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("abort", onAbort);
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("daemon supervisor stopped");
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 ?? "parall";
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 !== "active") {
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, "reconcile found no live child");
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, "agents");
292
- const flatWorkspace = path.join(root, "workspace");
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, "sessions");
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(".json"))
355
+ if (!file.endsWith('.json'))
301
356
  continue;
302
- const decoded = Buffer.from(file.replace(".json", ""), "base64url").toString();
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("usr_")) {
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 ?? "_orphan";
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 ["workspace", "sessions", "dispatch-context"]) {
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: "retryable" };
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: "skip" };
399
+ return { kind: 'skip' };
345
400
  }
346
- if (entry.user && entry.user.status !== "active") {
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: "skip" };
403
+ return { kind: 'skip' };
349
404
  }
350
- return { kind: "found", entry };
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 !== "found") {
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 === "retryable") {
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 === "skip") {
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 ?? "parall";
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, "llm_source changed");
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 ?? "claude-code";
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
- providerConfig: attached.provider_config ?? { llm_source: this.machineLlmSource },
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 env = adapter.buildEnv({ ...process.env, PRLL_API_URL: this.config.apiUrl }, state.agentId, state.orgId, state.credential.api_key, dirs, state.providerConfig);
599
- this.log.info(`spawning agent ${state.agentId} runtime=${state.runtimeType} bin=${adapter.bin} (attempt ${state.restartAttempts + 1})`);
600
- const child = spawn(adapter.bin, [], {
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: ["ignore", "inherit", "inherit"],
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 ? " (shutting down)" : ""}`);
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 ?? "null"} signal=${signal ?? "null"}${wasShutting ? " (shutting down)" : ""}`);
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("error", (err) => {
794
+ child.once('error', (err) => {
632
795
  if (err.code === 'ENOENT') {
633
- const pkg = RUNTIME_PACKAGES[state.runtimeType] ?? `@parall/${state.runtimeType}-agent`;
634
- this.log.error(`Runtime binary "${adapter.bin}" not found in PATH. Install: npm install -g ${pkg}`);
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("error", null, null, err);
804
+ settleChild('error', null, null, err);
637
805
  });
638
- child.once("close", (code, signal) => settleChild("close", code, signal));
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("exit", onExit);
820
+ child.once('exit', onExit);
653
821
  try {
654
- child.kill("SIGTERM");
822
+ child.kill('SIGTERM');
655
823
  }
656
824
  catch (err) {
657
825
  this.log.warn(`SIGTERM ${state.agentId} threw: ${String(err)}`);
658
- child.off("exit", onExit);
826
+ child.off('exit', onExit);
659
827
  resolve();
660
828
  return;
661
829
  }
662
830
  const hardKill = setTimeout(() => {
663
831
  try {
664
- child.kill("SIGKILL");
832
+ child.kill('SIGKILL');
665
833
  }
666
834
  catch {
667
835
  /* already gone */
668
836
  }
669
837
  }, 10_000);
670
- child.once("exit", () => clearTimeout(hardKill));
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 !== "ENOENT") {
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"}