@parall/daemon 1.28.1 → 1.29.1

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.
@@ -4,11 +4,13 @@ import * as path from "node:path";
4
4
  import { ParallWs } from "@parall/sdk";
5
5
  import { agentClaudeCredentialsFileFor, agentClaudeHomeFor, agentStateDirFor, agentWorkspaceDirFor, sharedClaudeCredentialsFileFor, } from "./config.js";
6
6
  import { assertAgentKey, getRuntimeAdapter } from "./runtimes.js";
7
+ import { prepareWorkspace } from "./workspace.js";
7
8
  const RUNTIME_PACKAGES = {
8
9
  'claude-code': '@parall/claude-agent',
9
10
  'codex': '@parall/codex-agent',
10
11
  'openclaw': '@parall/openclaw-agent',
11
12
  };
13
+ const WORKSPACE_SETUP_RETRY_DELAY_MS = 5_000;
12
14
  /**
13
15
  * Sleep that wakes early on abort. Returns true if the full delay elapsed,
14
16
  * false if aborted. Used by bootstrap retry and the outer keepalive in
@@ -46,9 +48,14 @@ export class DaemonSupervisor {
46
48
  client;
47
49
  log;
48
50
  children = new Map();
51
+ spawningAgents = new Set();
52
+ pendingWorkspaceSetup = new Set();
53
+ workspaceSetupRetryTimers = new Map();
54
+ cancelledSpawns = new Set();
49
55
  ws = null;
50
56
  running = false;
51
57
  machineOrgId = null;
58
+ machineLlmSource = "parall";
52
59
  stopResolve = null;
53
60
  constructor(config, client, log) {
54
61
  this.config = config;
@@ -85,7 +92,10 @@ export class DaemonSupervisor {
85
92
  });
86
93
  this.ws.on("machine.hello", (_data) => {
87
94
  this.log.info("machine WS connected (machine.hello)");
88
- void this.fullReconcile();
95
+ void (async () => {
96
+ await this.refreshMachineConfig();
97
+ await this.fullReconcile();
98
+ })();
89
99
  });
90
100
  this.ws.on("machine.agent.attached", (data) => {
91
101
  this.log.info(`WS: agent ${data.agent_id} attached`);
@@ -95,6 +105,18 @@ export class DaemonSupervisor {
95
105
  this.log.info(`WS: agent ${data.agent_id} detached`);
96
106
  void this.handleAgentDetached(data.agent_id);
97
107
  });
108
+ this.ws.on("machine.config.updated", (data) => {
109
+ const newSource = data.llm_source ?? "parall";
110
+ if (newSource !== this.machineLlmSource) {
111
+ this.log.info(`WS: llm_source changed ${this.machineLlmSource} → ${newSource}, respawning all agents`);
112
+ this.machineLlmSource = newSource;
113
+ void this.respawnAllChildren();
114
+ }
115
+ });
116
+ this.ws.on("machine.workspace.setup.requested", (data) => {
117
+ this.log.info(`WS: workspace setup requested for agent ${data.agent_id}`);
118
+ void this.handleWorkspaceSetupRequested(data.agent_id);
119
+ });
98
120
  this.ws.on("machine.stop", (data) => {
99
121
  this.log.info(`WS: machine.stop received (reason=${data.reason ?? "none"})`);
100
122
  void this.stop();
@@ -120,6 +142,10 @@ export class DaemonSupervisor {
120
142
  this.ws = null;
121
143
  }
122
144
  const exits = [];
145
+ for (const timer of this.workspaceSetupRetryTimers.values()) {
146
+ clearTimeout(timer);
147
+ }
148
+ this.workspaceSetupRetryTimers.clear();
123
149
  for (const state of this.children.values()) {
124
150
  state.shuttingDown = true;
125
151
  if (state.restartTimer) {
@@ -143,6 +169,7 @@ export class DaemonSupervisor {
143
169
  try {
144
170
  const machine = await this.client.getMachineSelf();
145
171
  this.machineOrgId = machine.org_id;
172
+ this.machineLlmSource = machine.llm_source ?? "parall";
146
173
  this.log.info(`daemon online — machine_id=${machine.id} org=${machine.org_id} label=${machine.label}`);
147
174
  return true;
148
175
  }
@@ -264,24 +291,31 @@ export class DaemonSupervisor {
264
291
  }
265
292
  }
266
293
  // ---- WS event handlers (incremental) ----
267
- async handleAgentAttached(agentId) {
268
- if (this.children.has(agentId))
269
- return;
294
+ async fetchAttachedAgent(agentId) {
270
295
  let attached;
271
296
  try {
272
297
  attached = await this.client.listAttachedAgents();
273
298
  }
274
299
  catch (err) {
275
- this.log.warn(`handleAgentAttached: listAttachedAgents failed: ${String(err)}`);
276
- return;
300
+ this.log.warn(`fetchAttachedAgent: listAttachedAgents failed: ${String(err)}`);
301
+ return { kind: "retryable" };
277
302
  }
278
303
  const entry = attached.find((a) => (a.user?.id ?? a.profile.user_id) === agentId);
279
304
  if (!entry) {
280
- this.log.warn(`handleAgentAttached: agent ${agentId} not found in attached list`);
281
- return;
305
+ this.log.warn(`fetchAttachedAgent: agent ${agentId} not found in attached list`);
306
+ return { kind: "skip" };
282
307
  }
283
308
  if (entry.user && entry.user.status !== "active") {
284
309
  this.log.info(`agent ${agentId} not active (status=${entry.user.status}) — skipping`);
310
+ return { kind: "skip" };
311
+ }
312
+ return { kind: "found", entry };
313
+ }
314
+ async handleAgentAttached(agentId) {
315
+ if (this.children.has(agentId) || this.spawningAgents.has(agentId))
316
+ return;
317
+ const result = await this.fetchAttachedAgent(agentId);
318
+ if (result.kind !== "found") {
285
319
  return;
286
320
  }
287
321
  const orgId = this.machineOrgId;
@@ -289,9 +323,14 @@ export class DaemonSupervisor {
289
323
  this.log.warn(`agent ${agentId}: no org_id available yet; skipping`);
290
324
  return;
291
325
  }
292
- await this.spawnAgent(agentId, orgId, entry);
326
+ await this.spawnAgent(agentId, orgId, result.entry);
293
327
  }
294
328
  async handleAgentDetached(agentId) {
329
+ this.pendingWorkspaceSetup.delete(agentId);
330
+ this.clearWorkspaceSetupRetry(agentId);
331
+ if (this.spawningAgents.has(agentId)) {
332
+ this.cancelledSpawns.add(agentId);
333
+ }
295
334
  const state = this.children.get(agentId);
296
335
  if (!state)
297
336
  return;
@@ -304,7 +343,87 @@ export class DaemonSupervisor {
304
343
  await this.terminateChild(state);
305
344
  this.children.delete(agentId);
306
345
  }
346
+ async handleWorkspaceSetupRequested(agentId) {
347
+ if (this.spawningAgents.has(agentId)) {
348
+ this.log.info(`agent ${agentId}: workspace setup already in progress; queueing one restart`);
349
+ this.pendingWorkspaceSetup.add(agentId);
350
+ return;
351
+ }
352
+ const result = await this.fetchAttachedAgent(agentId);
353
+ if (result.kind === "retryable") {
354
+ this.pendingWorkspaceSetup.add(agentId);
355
+ this.scheduleWorkspaceSetupRetry(agentId);
356
+ return;
357
+ }
358
+ if (result.kind === "skip") {
359
+ this.pendingWorkspaceSetup.delete(agentId);
360
+ this.clearWorkspaceSetupRetry(agentId);
361
+ return;
362
+ }
363
+ this.clearWorkspaceSetupRetry(agentId);
364
+ const state = this.children.get(agentId);
365
+ if (state) {
366
+ this.log.info(`agent ${agentId}: restarting for workspace setup`);
367
+ state.shuttingDown = true;
368
+ if (state.restartTimer) {
369
+ clearTimeout(state.restartTimer);
370
+ state.restartTimer = null;
371
+ }
372
+ await this.terminateChild(state);
373
+ this.children.delete(agentId);
374
+ }
375
+ const orgId = this.machineOrgId;
376
+ if (!orgId) {
377
+ this.log.warn(`agent ${agentId}: no org_id available yet; skipping`);
378
+ return;
379
+ }
380
+ await this.spawnAgent(agentId, orgId, result.entry);
381
+ }
382
+ scheduleWorkspaceSetupRetry(agentId) {
383
+ if (!this.running || this.workspaceSetupRetryTimers.has(agentId))
384
+ return;
385
+ const timer = setTimeout(() => {
386
+ this.workspaceSetupRetryTimers.delete(agentId);
387
+ if (this.running && this.pendingWorkspaceSetup.has(agentId)) {
388
+ void this.handleWorkspaceSetupRequested(agentId);
389
+ }
390
+ }, WORKSPACE_SETUP_RETRY_DELAY_MS);
391
+ timer.unref?.();
392
+ this.workspaceSetupRetryTimers.set(agentId, timer);
393
+ }
394
+ clearWorkspaceSetupRetry(agentId) {
395
+ const timer = this.workspaceSetupRetryTimers.get(agentId);
396
+ if (timer) {
397
+ clearTimeout(timer);
398
+ this.workspaceSetupRetryTimers.delete(agentId);
399
+ }
400
+ }
307
401
  // ---- Spawn / restart ----
402
+ async refreshMachineConfig() {
403
+ try {
404
+ const machine = await this.client.getMachineSelf();
405
+ const newSource = machine.llm_source ?? "parall";
406
+ if (newSource !== this.machineLlmSource) {
407
+ this.log.info(`machine config refreshed: llm_source ${this.machineLlmSource} → ${newSource}`);
408
+ this.machineLlmSource = newSource;
409
+ await this.respawnAllChildren();
410
+ }
411
+ }
412
+ catch (err) {
413
+ this.log.warn(`refreshMachineConfig failed: ${String(err)}`);
414
+ }
415
+ }
416
+ async respawnAllChildren() {
417
+ const states = [...this.children.values()];
418
+ for (const state of states) {
419
+ await this.terminateChild(state);
420
+ }
421
+ for (const state of states) {
422
+ if (!state.shuttingDown && this.running) {
423
+ await this.restartChildNow(state, "llm_source changed");
424
+ }
425
+ }
426
+ }
308
427
  async restartChildNow(state, reason) {
309
428
  if (!this.running || state.shuttingDown || state.child || state.restartTimer) {
310
429
  return;
@@ -320,6 +439,26 @@ export class DaemonSupervisor {
320
439
  }
321
440
  }
322
441
  async spawnAgent(agentId, orgId, attached) {
442
+ if (this.children.has(agentId) || this.spawningAgents.has(agentId)) {
443
+ return;
444
+ }
445
+ this.spawningAgents.add(agentId);
446
+ let shouldReplaySetupRequest = false;
447
+ try {
448
+ await this.spawnAgentOnce(agentId, orgId, attached);
449
+ }
450
+ finally {
451
+ this.spawningAgents.delete(agentId);
452
+ this.cancelledSpawns.delete(agentId);
453
+ shouldReplaySetupRequest = this.pendingWorkspaceSetup.delete(agentId);
454
+ }
455
+ if (shouldReplaySetupRequest && this.running) {
456
+ queueMicrotask(() => {
457
+ void this.handleWorkspaceSetupRequested(agentId);
458
+ });
459
+ }
460
+ }
461
+ async spawnAgentOnce(agentId, orgId, attached) {
323
462
  let credential;
324
463
  try {
325
464
  credential = await this.client.mintLaunchCredential(agentId);
@@ -329,19 +468,13 @@ export class DaemonSupervisor {
329
468
  return;
330
469
  }
331
470
  const stateDir = agentStateDirFor(this.config.rootStateDir, agentId);
332
- const workspaceDir = attached.daemon_config?.workspace_path
333
- || agentWorkspaceDirFor(this.config.rootStateDir, agentId);
471
+ const defaultWorkspaceDir = agentWorkspaceDirFor(this.config.rootStateDir, agentId);
334
472
  const isK8s = !!process.env.KUBERNETES_SERVICE_HOST;
335
473
  const claudeHome = isK8s
336
474
  ? agentClaudeHomeFor(this.config.rootClaudeHome, agentId)
337
475
  : this.config.rootClaudeHome;
338
476
  try {
339
477
  fs.mkdirSync(stateDir, { recursive: true });
340
- // Only create workspace dir when using the default isolated path —
341
- // user-specified workspace_path should already exist.
342
- if (!attached.daemon_config?.workspace_path) {
343
- fs.mkdirSync(workspaceDir, { recursive: true });
344
- }
345
478
  if (isK8s) {
346
479
  fs.mkdirSync(claudeHome, { recursive: true });
347
480
  this.ensureSharedCredentialLink(claudeHome, agentId);
@@ -351,12 +484,31 @@ export class DaemonSupervisor {
351
484
  this.log.warn(`mkdir agent dirs (${agentId}) failed: ${String(err)}`);
352
485
  }
353
486
  const runtimeType = attached.profile.runtime_type ?? "claude-code";
487
+ let workspaceDir;
488
+ try {
489
+ workspaceDir = await prepareWorkspace({
490
+ client: this.client,
491
+ attached,
492
+ agentId,
493
+ defaultWorkspaceDir,
494
+ log: this.log,
495
+ });
496
+ }
497
+ catch (err) {
498
+ this.log.error(`agent ${agentId}: workspace setup failed: ${String(err)}`);
499
+ return;
500
+ }
501
+ if (this.cancelledSpawns.delete(agentId)) {
502
+ this.log.info(`agent ${agentId}: spawn cancelled after detach during workspace setup`);
503
+ return;
504
+ }
354
505
  const state = {
355
506
  agentId,
356
507
  orgId,
357
508
  runtimeType,
358
509
  workspacePath: workspaceDir,
359
510
  claudeHome,
511
+ providerConfig: attached.provider_config ?? { llm_source: this.machineLlmSource },
360
512
  child: null,
361
513
  credential,
362
514
  restartAttempts: 0,
@@ -380,7 +532,7 @@ export class DaemonSupervisor {
380
532
  workspaceDir: state.workspacePath,
381
533
  claudeHome: state.claudeHome,
382
534
  };
383
- const env = adapter.buildEnv({ ...process.env, PRLL_API_URL: this.config.apiUrl }, state.agentId, state.orgId, state.credential.api_key, dirs);
535
+ const env = adapter.buildEnv({ ...process.env, PRLL_API_URL: this.config.apiUrl }, state.agentId, state.orgId, state.credential.api_key, dirs, state.providerConfig);
384
536
  this.log.info(`spawning agent ${state.agentId} runtime=${state.runtimeType} bin=${adapter.bin} (attempt ${state.restartAttempts + 1})`);
385
537
  const child = spawn(adapter.bin, [], {
386
538
  env,
@@ -421,11 +573,12 @@ export class DaemonSupervisor {
421
573
  settleChild("error", null, null, err);
422
574
  });
423
575
  child.once("close", (code, signal) => settleChild("close", code, signal));
424
- setTimeout(() => {
576
+ const stableTimer = setTimeout(() => {
425
577
  if (state.child === child) {
426
578
  state.restartAttempts = 0;
427
579
  }
428
580
  }, Math.max(this.config.restartBackoffMs, 30_000));
581
+ stableTimer.unref?.();
429
582
  }
430
583
  async terminateChild(state) {
431
584
  const child = state.child;
@@ -0,0 +1,11 @@
1
+ import type { GatewayLogger } from "@parall/agent-core";
2
+ import type { AttachedAgent, ParallClient } from "@parall/sdk";
3
+ export interface PrepareWorkspaceOpts {
4
+ client: ParallClient;
5
+ attached: AttachedAgent;
6
+ agentId: string;
7
+ defaultWorkspaceDir: string;
8
+ log: GatewayLogger;
9
+ }
10
+ export declare function prepareWorkspace(opts: PrepareWorkspaceOpts): Promise<string>;
11
+ //# sourceMappingURL=workspace.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,KAAK,EAAE,aAAa,EAA4C,YAAY,EAAE,MAAM,aAAa,CAAC;AAiBzG,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,YAAY,CAAC;IACrB,QAAQ,EAAE,aAAa,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,GAAG,EAAE,aAAa,CAAC;CACpB;AAED,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC,MAAM,CAAC,CAwClF"}