@parall/daemon 1.29.0 → 1.29.2

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.
@@ -22,6 +22,8 @@ var ENDPOINTS = {
22
22
  AUTH_CHECK_EMAIL: `${API_BASE}/auth/check-email`,
23
23
  AUTH_VERIFY_EMAIL: `${API_BASE}/auth/verify-email`,
24
24
  AUTH_RESEND_CODE: `${API_BASE}/auth/resend-code`,
25
+ AUTH_FORGOT_PASSWORD: `${API_BASE}/auth/forgot-password`,
26
+ AUTH_RESET_PASSWORD: `${API_BASE}/auth/reset-password`,
25
27
  // Users
26
28
  USERS_ME: `${API_BASE}/users/me`,
27
29
  USER_AVATAR: `${API_BASE}/users/me/avatar`,
@@ -56,6 +58,9 @@ var ENDPOINTS = {
56
58
  // Messages (global, by message ID)
57
59
  MESSAGE: (id) => `${API_BASE}/messages/${id}`,
58
60
  MESSAGE_REPLIES: (id) => `${API_BASE}/messages/${id}/replies`,
61
+ MESSAGE_WATCH: (id) => `${API_BASE}/messages/${id}/watch`,
62
+ MESSAGE_WATCHERS: (id) => `${API_BASE}/messages/${id}/watchers`,
63
+ MESSAGE_WATCHING: (id) => `${API_BASE}/messages/${id}/watching`,
59
64
  // Upload (org-scoped)
60
65
  UPLOAD_PRESIGN: (orgId) => `${API_BASE}/orgs/${orgId}/upload/presign`,
61
66
  UPLOAD_COMPLETE: (orgId) => `${API_BASE}/orgs/${orgId}/upload/complete`,
@@ -106,6 +111,9 @@ var ENDPOINTS = {
106
111
  // Attach/Detach bind an agent to/from a daemon Machine.
107
112
  MACHINE_ATTACH_AGENT: (orgId, machineId, agentId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/agents/${agentId}`,
108
113
  MACHINE_DETACH_AGENT: (orgId, machineId, agentId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/agents/${agentId}`,
114
+ MACHINE_WORKSPACE_STATES: (orgId, machineId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/workspace-states`,
115
+ MACHINE_AGENT_DAEMON_CONFIG: (orgId, machineId, agentId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/agents/${agentId}/daemon-config`,
116
+ MACHINE_AGENT_WORKSPACE_SETUP: (orgId, machineId, agentId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/agents/${agentId}/workspace/setup`,
109
117
  MACHINE_LLM_SOURCE: (orgId, machineId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/llm-source`,
110
118
  MACHINE_RUNTIME_AUTH: (orgId, machineId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/runtime-auth`,
111
119
  MACHINE_KEYS: (orgId, machineId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/keys`,
@@ -119,6 +127,7 @@ var ENDPOINTS = {
119
127
  MACHINES_ME_AGENTS: `${API_BASE}/machines/me/agents`,
120
128
  MACHINES_ME_HEALTH: `${API_BASE}/machines/me/health`,
121
129
  MACHINES_ME_AGENT_LAUNCH_CREDENTIAL: (agentId) => `${API_BASE}/machines/me/agents/${agentId}/launch-credential`,
130
+ MACHINES_ME_AGENT_WORKSPACE_STATE: (agentId) => `${API_BASE}/machines/me/agents/${agentId}/workspace-state`,
122
131
  MACHINES_ME_WS_TICKET: `${API_BASE}/machines/me/ws/ticket`,
123
132
  // Tasks (org-scoped)
124
133
  TASKS: (orgId) => `${API_BASE}/orgs/${orgId}/tasks`,
@@ -298,7 +307,8 @@ var WS_EVENTS = {
298
307
  MACHINE_HELLO: "machine.hello",
299
308
  MACHINE_AGENT_ATTACHED: "machine.agent.attached",
300
309
  MACHINE_AGENT_DETACHED: "machine.agent.detached",
301
- MACHINE_STOP: "machine.stop"
310
+ MACHINE_STOP: "machine.stop",
311
+ MACHINE_WORKSPACE_SETUP_REQUESTED: "machine.workspace.setup.requested"
302
312
  };
303
313
 
304
314
  // ts/sdk/dist/client.js
@@ -318,7 +328,9 @@ var ParallClient = class _ParallClient {
318
328
  "/auth/logout",
319
329
  "/auth/verify-email",
320
330
  "/auth/resend-code",
321
- "/auth/check-email"
331
+ "/auth/check-email",
332
+ "/auth/forgot-password",
333
+ "/auth/reset-password"
322
334
  ]);
323
335
  /** Proactive refresh when token expires within this window (seconds). */
324
336
  static REFRESH_THRESHOLD_S = 5 * 60;
@@ -379,10 +391,10 @@ var ParallClient = class _ParallClient {
379
391
  * REFRESH_THRESHOLD_S, refresh it **before** sending the request.
380
392
  * No-op when the token is still fresh, missing, or un-parseable.
381
393
  */
382
- async ensureFreshToken(path5) {
394
+ async ensureFreshToken(path6) {
383
395
  if (!this.token || !this.getRefreshToken)
384
396
  return;
385
- const pathSuffix = path5.replace(/^\/api\/v1/, "");
397
+ const pathSuffix = path6.replace(/^\/api\/v1/, "");
386
398
  if (_ParallClient.AUTH_PATHS.has(pathSuffix))
387
399
  return;
388
400
  const exp = _ParallClient.decodeJwtExp(this.token);
@@ -414,11 +426,11 @@ var ParallClient = class _ParallClient {
414
426
  this.refreshPromise = null;
415
427
  }
416
428
  }
417
- async request(method, path5, body, query, retried = false, opts) {
429
+ async request(method, path6, body, query, retried = false, opts) {
418
430
  if (!retried) {
419
- await this.ensureFreshToken(path5);
431
+ await this.ensureFreshToken(path6);
420
432
  }
421
- let url = `${this.baseUrl}${path5}`;
433
+ let url = `${this.baseUrl}${path6}`;
422
434
  if (query) {
423
435
  const params = new URLSearchParams();
424
436
  for (const [key, value] of Object.entries(query)) {
@@ -443,12 +455,12 @@ var ParallClient = class _ParallClient {
443
455
  throw _ParallClient.normalizeFetchError(err);
444
456
  }
445
457
  if (res.status === 401) {
446
- const pathSuffix = path5.replace(/^\/api\/v1/, "");
458
+ const pathSuffix = path6.replace(/^\/api\/v1/, "");
447
459
  const isAuthPath = _ParallClient.AUTH_PATHS.has(pathSuffix);
448
460
  if (!retried && !isAuthPath && this.getRefreshToken) {
449
461
  const refreshed = await this.tryRefresh();
450
462
  if (refreshed) {
451
- return this.request(method, path5, body, query, true, opts);
463
+ return this.request(method, path6, body, query, true, opts);
452
464
  }
453
465
  }
454
466
  if (this.onTokenExpired && !isAuthPath) {
@@ -485,15 +497,15 @@ var ParallClient = class _ParallClient {
485
497
  * hit the 100 MiB cap, so a longer 5-minute timeout is used so a
486
498
  * 50 MiB blob on a slow connection doesn't get chopped at 15 s.
487
499
  */
488
- async multipartRequest(method, path5, body, retried = false) {
500
+ async multipartRequest(method, path6, body, retried = false) {
489
501
  if (!retried) {
490
- await this.ensureFreshToken(path5);
502
+ await this.ensureFreshToken(path6);
491
503
  }
492
504
  const { "Content-Type": _drop, ...headers } = this.buildHeaders();
493
505
  void _drop;
494
506
  let res;
495
507
  try {
496
- res = await fetch(`${this.baseUrl}${path5}`, {
508
+ res = await fetch(`${this.baseUrl}${path6}`, {
497
509
  method,
498
510
  headers,
499
511
  body,
@@ -503,12 +515,12 @@ var ParallClient = class _ParallClient {
503
515
  throw _ParallClient.normalizeFetchError(err);
504
516
  }
505
517
  if (res.status === 401) {
506
- const pathSuffix = path5.replace(/^\/api\/v1/, "");
518
+ const pathSuffix = path6.replace(/^\/api\/v1/, "");
507
519
  const isAuthPath = _ParallClient.AUTH_PATHS.has(pathSuffix);
508
520
  if (!retried && !isAuthPath && this.getRefreshToken) {
509
521
  const refreshed = await this.tryRefresh();
510
522
  if (refreshed) {
511
- return this.multipartRequest(method, path5, body, true);
523
+ return this.multipartRequest(method, path6, body, true);
512
524
  }
513
525
  }
514
526
  if (this.onTokenExpired && !isAuthPath) {
@@ -556,6 +568,12 @@ var ParallClient = class _ParallClient {
556
568
  async resendCode(email) {
557
569
  return this.request("POST", ENDPOINTS.AUTH_RESEND_CODE, { email });
558
570
  }
571
+ async forgotPassword(email) {
572
+ return this.request("POST", ENDPOINTS.AUTH_FORGOT_PASSWORD, { email });
573
+ }
574
+ async resetPassword(token, newPassword) {
575
+ return this.request("POST", ENDPOINTS.AUTH_RESET_PASSWORD, { token, new_password: newPassword });
576
+ }
559
577
  // ---- WebSocket ----
560
578
  async getWsTicket() {
561
579
  return this.request("POST", ENDPOINTS.WS_TICKET);
@@ -762,8 +780,8 @@ var ParallClient = class _ParallClient {
762
780
  async completeUpload(orgId, attachmentId) {
763
781
  return this.request("POST", ENDPOINTS.UPLOAD_COMPLETE(orgId), { attachment_id: attachmentId });
764
782
  }
765
- async getFileUrl(id) {
766
- return this.request("GET", ENDPOINTS.FILE(id));
783
+ async getFileUrl(id, opts) {
784
+ return this.request("GET", ENDPOINTS.FILE(id), void 0, opts?.download ? { download: true } : void 0);
767
785
  }
768
786
  // ---- Approvals ----
769
787
  async getApproval(id) {
@@ -960,6 +978,16 @@ var ParallClient = class _ParallClient {
960
978
  async detachAgent(orgId, machineId, agentId) {
961
979
  return this.request("DELETE", ENDPOINTS.MACHINE_DETACH_AGENT(orgId, machineId, agentId));
962
980
  }
981
+ async listAgentWorkspaceStates(orgId, machineId) {
982
+ const res = await this.request("GET", ENDPOINTS.MACHINE_WORKSPACE_STATES(orgId, machineId));
983
+ return res.data;
984
+ }
985
+ async patchAgentDaemonConfig(orgId, machineId, agentId, daemonConfig) {
986
+ return this.request("PATCH", ENDPOINTS.MACHINE_AGENT_DAEMON_CONFIG(orgId, machineId, agentId), { daemon_config: daemonConfig });
987
+ }
988
+ async retryAgentWorkspaceSetup(orgId, machineId, agentId) {
989
+ return this.request("POST", ENDPOINTS.MACHINE_AGENT_WORKSPACE_SETUP(orgId, machineId, agentId));
990
+ }
963
991
  async patchMachineLLMSource(orgId, machineId, llmSource2) {
964
992
  return this.request("PATCH", ENDPOINTS.MACHINE_LLM_SOURCE(orgId, machineId), { llm_source: llmSource2 });
965
993
  }
@@ -1005,6 +1033,12 @@ var ParallClient = class _ParallClient {
1005
1033
  async postMachineHeartbeat() {
1006
1034
  return this.request("POST", ENDPOINTS.MACHINES_ME_HEALTH);
1007
1035
  }
1036
+ async reportAgentWorkspaceState(agentId, state) {
1037
+ const res = await this.request("PUT", ENDPOINTS.MACHINES_ME_AGENT_WORKSPACE_STATE(agentId), state);
1038
+ if (res?.status === "ignored_stale") {
1039
+ throw new ApiError(409, "Workspace state report ignored as stale", "STALE_WORKSPACE_STATE");
1040
+ }
1041
+ }
1008
1042
  /**
1009
1043
  * `POST /machines/me/agents/{agentId}/launch-credential` — mint a
1010
1044
  * short-lived `agk_*` for one of this Machine's attached agents. The
@@ -1154,6 +1188,20 @@ var ParallClient = class _ParallClient {
1154
1188
  const res = await this.request("GET", ENDPOINTS.TASK_WATCHERS(orgId, taskId));
1155
1189
  return res.data;
1156
1190
  }
1191
+ async watchThread(threadRootId) {
1192
+ return this.request("POST", ENDPOINTS.MESSAGE_WATCH(threadRootId));
1193
+ }
1194
+ async unwatchThread(threadRootId) {
1195
+ return this.request("DELETE", ENDPOINTS.MESSAGE_WATCH(threadRootId));
1196
+ }
1197
+ async getThreadWatchers(threadRootId) {
1198
+ const res = await this.request("GET", ENDPOINTS.MESSAGE_WATCHERS(threadRootId));
1199
+ return res.data;
1200
+ }
1201
+ async isWatchingThread(threadRootId) {
1202
+ const res = await this.request("GET", ENDPOINTS.MESSAGE_WATCHING(threadRootId));
1203
+ return res.watching;
1204
+ }
1157
1205
  async getSubtasks(orgId, taskId) {
1158
1206
  const res = await this.request("GET", ENDPOINTS.TASK_SUBTASKS(orgId, taskId));
1159
1207
  return res.data;
@@ -1356,8 +1404,8 @@ var ParallClient = class _ParallClient {
1356
1404
  async deleteWikiPathScope(orgId, wikiId, scopeId) {
1357
1405
  await this.request("DELETE", ENDPOINTS.WIKI_PATH_SCOPE(orgId, wikiId, scopeId));
1358
1406
  }
1359
- async getWikiAccessStatus(orgId, wikiId, path5) {
1360
- return this.request("GET", ENDPOINTS.WIKI_ACCESS_STATUS(orgId, wikiId), void 0, path5 ? { path: path5 } : void 0);
1407
+ async getWikiAccessStatus(orgId, wikiId, path6) {
1408
+ return this.request("GET", ENDPOINTS.WIKI_ACCESS_STATUS(orgId, wikiId), void 0, path6 ? { path: path6 } : void 0);
1361
1409
  }
1362
1410
  async createWikiAccessRequest(orgId, wikiId, data) {
1363
1411
  await this.request("POST", ENDPOINTS.WIKI_ACCESS_REQUESTS(orgId, wikiId), data);
@@ -1366,11 +1414,11 @@ var ParallClient = class _ParallClient {
1366
1414
  async getWikiCommits(orgId, wikiId, params) {
1367
1415
  return this.request("GET", ENDPOINTS.WIKI_COMMITS(orgId, wikiId), void 0, params);
1368
1416
  }
1369
- async getWikiFileCommits(orgId, wikiId, path5, params) {
1370
- return this.request("GET", ENDPOINTS.WIKI_FILE_COMMITS(orgId, wikiId), void 0, { path: path5, ...params });
1417
+ async getWikiFileCommits(orgId, wikiId, path6, params) {
1418
+ return this.request("GET", ENDPOINTS.WIKI_FILE_COMMITS(orgId, wikiId), void 0, { path: path6, ...params });
1371
1419
  }
1372
- async getWikiBlame(orgId, wikiId, path5, ref) {
1373
- return this.request("GET", ENDPOINTS.WIKI_BLAME(orgId, wikiId), void 0, { path: path5, ref });
1420
+ async getWikiBlame(orgId, wikiId, path6, ref) {
1421
+ return this.request("GET", ENDPOINTS.WIKI_BLAME(orgId, wikiId), void 0, { path: path6, ref });
1374
1422
  }
1375
1423
  // ---- Wiki Operations (audit log) ----
1376
1424
  async getWikiOperations(orgId, wikiId, params) {
@@ -1450,14 +1498,28 @@ var ParallClient = class _ParallClient {
1450
1498
  params.set("machine_id", opts.machine_id);
1451
1499
  if (opts?.unresolved_machine)
1452
1500
  params.set("unresolved_machine", "true");
1501
+ if (opts?.from)
1502
+ params.set("from", opts.from);
1503
+ if (opts?.to)
1504
+ params.set("to", opts.to);
1453
1505
  const qs = params.toString();
1454
1506
  return this.request("GET", `${ENDPOINTS.BILLING_TRANSACTIONS(orgId)}${qs ? `?${qs}` : ""}`);
1455
1507
  }
1456
- async listBillingTransactionAgentGroups(orgId) {
1457
- return this.request("GET", `${ENDPOINTS.BILLING_TRANSACTIONS(orgId)}?group_by=agent`);
1458
- }
1459
- async listBillingTransactionMachineGroups(orgId) {
1460
- return this.request("GET", `${ENDPOINTS.BILLING_TRANSACTIONS(orgId)}?group_by=machine`);
1508
+ async listBillingTransactionAgentGroups(orgId, opts) {
1509
+ const params = new URLSearchParams({ group_by: "agent" });
1510
+ if (opts?.from)
1511
+ params.set("from", opts.from);
1512
+ if (opts?.to)
1513
+ params.set("to", opts.to);
1514
+ return this.request("GET", `${ENDPOINTS.BILLING_TRANSACTIONS(orgId)}?${params}`);
1515
+ }
1516
+ async listBillingTransactionMachineGroups(orgId, opts) {
1517
+ const params = new URLSearchParams({ group_by: "machine" });
1518
+ if (opts?.from)
1519
+ params.set("from", opts.from);
1520
+ if (opts?.to)
1521
+ params.set("to", opts.to);
1522
+ return this.request("GET", `${ENDPOINTS.BILLING_TRANSACTIONS(orgId)}?${params}`);
1461
1523
  }
1462
1524
  async createCheckout(orgId, req) {
1463
1525
  return this.request("POST", ENDPOINTS.BILLING_CHECKOUT(orgId), req);
@@ -1508,6 +1570,12 @@ var ApiError = class extends Error {
1508
1570
  };
1509
1571
 
1510
1572
  // ts/sdk/dist/ws.js
1573
+ function isRetryableNetworkError(err) {
1574
+ return err instanceof Error && err.name === "ApiError" && "status" in err && err.status === 0;
1575
+ }
1576
+ function isBrowserRuntime() {
1577
+ return typeof window !== "undefined";
1578
+ }
1511
1579
  var ParallWs = class {
1512
1580
  ws = null;
1513
1581
  options;
@@ -1543,7 +1611,11 @@ var ParallWs = class {
1543
1611
  try {
1544
1612
  ticket = await this.options.getTicket();
1545
1613
  } catch (err) {
1546
- console.error("Failed to get WS ticket:", err);
1614
+ if (isBrowserRuntime() && isRetryableNetworkError(err)) {
1615
+ console.warn("Failed to get WS ticket:", err);
1616
+ } else {
1617
+ console.error("Failed to get WS ticket:", err);
1618
+ }
1547
1619
  if (this.options.reconnect) {
1548
1620
  this.scheduleReconnect();
1549
1621
  } else {
@@ -1901,6 +1973,7 @@ function resolveClaudeDaemonConfig(env = process.env) {
1901
1973
  rootStateDir,
1902
1974
  rootClaudeHome,
1903
1975
  wsUrl: env.PRLL_WS_URL?.trim() || void 0,
1976
+ swimlaneName: env.PRLL_SWIMLANE_NAME?.trim() || void 0,
1904
1977
  pollIntervalMs: parseMs(env.PRLL_DAEMON_POLL_INTERVAL_MS, 3e4),
1905
1978
  heartbeatIntervalMs: parseMs(env.PRLL_DAEMON_HEARTBEAT_INTERVAL_MS, 3e4),
1906
1979
  restartBackoffMs: parseMs(env.PRLL_DAEMON_RESTART_BACKOFF_MS, 5e3),
@@ -1932,14 +2005,19 @@ function agentClaudeCredentialsFileFor(agentClaudeHome) {
1932
2005
  function agentWorkspaceDirFor(rootStateDir, agentId) {
1933
2006
  return path.join(rootStateDir, "agents", assertSafeAgentId(agentId), "workspace");
1934
2007
  }
1935
- function resolveWsUrl(apiUrl, explicitWsUrl) {
1936
- return explicitWsUrl || `${apiUrl.replace(/\/$/, "").replace(/^http/, "ws")}/ws`;
2008
+ function resolveWsUrl(apiUrl, explicitWsUrl, swimlaneName) {
2009
+ const base = explicitWsUrl || `${apiUrl.replace(/\/$/, "").replace(/^http/, "ws")}/ws`;
2010
+ if (!swimlaneName)
2011
+ return base;
2012
+ const url = new URL(base);
2013
+ url.searchParams.set("swimlane", swimlaneName);
2014
+ return url.toString();
1937
2015
  }
1938
2016
 
1939
2017
  // ts/daemon/dist/supervisor.js
1940
- import { spawn } from "node:child_process";
1941
- import * as fs2 from "node:fs";
1942
- import * as path3 from "node:path";
2018
+ import { spawn as spawn2 } from "node:child_process";
2019
+ import * as fs3 from "node:fs";
2020
+ import * as path4 from "node:path";
1943
2021
 
1944
2022
  // ts/daemon/dist/runtimes.js
1945
2023
  import * as path2 from "node:path";
@@ -2058,23 +2136,451 @@ function assertAgentKey(apiKey) {
2058
2136
  }
2059
2137
  }
2060
2138
 
2139
+ // ts/daemon/dist/workspace.js
2140
+ import { spawn } from "node:child_process";
2141
+ import { createHash } from "node:crypto";
2142
+ import * as fs2 from "node:fs";
2143
+ import * as path3 from "node:path";
2144
+ var OUTPUT_TAIL_LIMIT = 32 * 1024;
2145
+ var DEFAULT_SETUP_TIMEOUT_SEC = 600;
2146
+ async function prepareWorkspace(opts) {
2147
+ const prior = opts.attached.workspace_state;
2148
+ const plan = buildWorkspacePlan(opts.attached.daemon_config, opts.defaultWorkspaceDir, prior?.config_hash);
2149
+ if (!opts.attached.daemon_config && !prior) {
2150
+ return plan.workspaceDir;
2151
+ }
2152
+ let forceSetup = false;
2153
+ if (prior?.status === "ready" && prior.config_hash === plan.configHash) {
2154
+ if (await verifyExistingWorkspace(plan, opts.log)) {
2155
+ return plan.workspaceDir;
2156
+ }
2157
+ forceSetup = true;
2158
+ }
2159
+ await opts.client.reportAgentWorkspaceState(opts.agentId, {
2160
+ config_hash: plan.configHash,
2161
+ status: "preparing"
2162
+ });
2163
+ let outputTail = "";
2164
+ try {
2165
+ await ensureWorkspace(plan, opts.log);
2166
+ if (shouldRunSetup(plan.workspace.setup, prior, plan.configHash, forceSetup)) {
2167
+ outputTail = await runSetup(plan.workspaceDir, plan.workspace.setup.command, plan.workspace.setup?.timeout_sec);
2168
+ }
2169
+ await opts.client.reportAgentWorkspaceState(opts.agentId, {
2170
+ config_hash: plan.configHash,
2171
+ status: "ready",
2172
+ output_tail: outputTail || null
2173
+ });
2174
+ return plan.workspaceDir;
2175
+ } catch (err) {
2176
+ const message = err instanceof Error ? err.message : String(err);
2177
+ await opts.client.reportAgentWorkspaceState(opts.agentId, {
2178
+ config_hash: plan.configHash,
2179
+ status: "failed",
2180
+ last_error: message,
2181
+ output_tail: outputTail || null
2182
+ }).catch((reportErr) => {
2183
+ opts.log.warn(`workspace state report failed after setup error: ${String(reportErr)}`);
2184
+ });
2185
+ throw err;
2186
+ }
2187
+ }
2188
+ function buildWorkspacePlan(input, defaultWorkspaceDir, serverConfigHash) {
2189
+ const config = normalizeConfig(input, defaultWorkspaceDir);
2190
+ const workspace = config.workspace;
2191
+ const { workspaceDir, customWorkspaceField } = resolveWorkspaceDir(workspace, defaultWorkspaceDir);
2192
+ const configHash = serverConfigHash || createHash("sha256").update(JSON.stringify(config)).digest("hex");
2193
+ return { config, workspace, workspaceDir, defaultWorkspaceDir, customWorkspaceField, configHash };
2194
+ }
2195
+ function normalizeConfig(input, defaultWorkspaceDir) {
2196
+ const cfg = input ? JSON.parse(JSON.stringify(input)) : {};
2197
+ cfg.workspace_path = cfg.workspace_path?.trim() || void 0;
2198
+ if (!cfg.workspace && cfg.workspace_path) {
2199
+ cfg.workspace = { mode: "local_path", path: cfg.workspace_path };
2200
+ }
2201
+ if (!cfg.workspace) {
2202
+ cfg.workspace = { mode: "default" };
2203
+ }
2204
+ const ws = cfg.workspace;
2205
+ ws.mode = ws.mode?.trim() || void 0;
2206
+ ws.path = ws.path?.trim() || void 0;
2207
+ if (ws.git) {
2208
+ ws.git.remote = ws.git.remote?.trim() || void 0;
2209
+ ws.git.ref = ws.git.ref?.trim() || void 0;
2210
+ ws.git.target_path = ws.git.target_path?.trim() || void 0;
2211
+ }
2212
+ ws.mode ||= ws.git ? "git" : ws.path ? "local_path" : "default";
2213
+ if (ws.mode === "local_path" && !ws.path) {
2214
+ ws.mode = "default";
2215
+ }
2216
+ if (ws.mode === "git") {
2217
+ ws.git ||= {};
2218
+ if (!ws.git.target_path && ws.path) {
2219
+ ws.git.target_path = ws.path;
2220
+ }
2221
+ }
2222
+ if (ws.mode === "default") {
2223
+ ws.path = void 0;
2224
+ ws.git = void 0;
2225
+ }
2226
+ if (ws.mode === "local_path") {
2227
+ ws.git = void 0;
2228
+ }
2229
+ if (ws.setup) {
2230
+ ws.setup.command = ws.setup.command?.trim();
2231
+ ws.setup.run_on ||= "first_attach";
2232
+ if (!ws.setup.command) {
2233
+ ws.setup = void 0;
2234
+ }
2235
+ }
2236
+ void defaultWorkspaceDir;
2237
+ return cfg;
2238
+ }
2239
+ function resolveWorkspaceDir(workspace, defaultWorkspaceDir) {
2240
+ if (workspace.mode === "local_path") {
2241
+ if (!workspace.path)
2242
+ return { workspaceDir: defaultWorkspaceDir };
2243
+ return {
2244
+ workspaceDir: requireAbsolute(workspace.path, "workspace.path"),
2245
+ customWorkspaceField: "workspace.path"
2246
+ };
2247
+ }
2248
+ if (workspace.mode === "git") {
2249
+ return workspace.git?.target_path ? {
2250
+ workspaceDir: requireAbsolute(workspace.git.target_path, "workspace.git.target_path"),
2251
+ customWorkspaceField: "workspace.git.target_path"
2252
+ } : { workspaceDir: defaultWorkspaceDir };
2253
+ }
2254
+ return { workspaceDir: defaultWorkspaceDir };
2255
+ }
2256
+ async function ensureWorkspace(plan, log2) {
2257
+ const ws = plan.workspace;
2258
+ if (ws.mode === "default") {
2259
+ fs2.mkdirSync(plan.workspaceDir, { recursive: true });
2260
+ assertWritableWorkspaceDir(plan.workspaceDir);
2261
+ return;
2262
+ }
2263
+ if (ws.mode === "local_path") {
2264
+ assertSafeCustomWorkspacePath(plan);
2265
+ let st;
2266
+ try {
2267
+ st = fs2.statSync(plan.workspaceDir);
2268
+ } catch (err) {
2269
+ if (isNodeError(err) && err.code === "ENOENT") {
2270
+ throw new Error(`workspace path does not exist: ${plan.workspaceDir}`);
2271
+ }
2272
+ throw err;
2273
+ }
2274
+ if (!st.isDirectory()) {
2275
+ throw new Error(`workspace path is not a directory: ${plan.workspaceDir}`);
2276
+ }
2277
+ assertSafeCustomWorkspacePath(plan, fs2.realpathSync(plan.workspaceDir));
2278
+ assertWritableWorkspaceDir(plan.workspaceDir);
2279
+ return;
2280
+ }
2281
+ if (ws.mode === "git") {
2282
+ const remote = ws.git?.remote?.trim();
2283
+ if (!remote)
2284
+ throw new Error("workspace git remote is required");
2285
+ if (plan.customWorkspaceField) {
2286
+ assertSafeCustomWorkspacePath(plan);
2287
+ }
2288
+ if (!fs2.existsSync(plan.workspaceDir)) {
2289
+ fs2.mkdirSync(path3.dirname(plan.workspaceDir), { recursive: true });
2290
+ assertWritableWorkspaceDir(path3.dirname(plan.workspaceDir));
2291
+ await runCommand("git", ["clone", remote, plan.workspaceDir], process.cwd());
2292
+ } else {
2293
+ const st = fs2.statSync(plan.workspaceDir);
2294
+ if (!st.isDirectory()) {
2295
+ throw new Error(`workspace path is not a directory: ${plan.workspaceDir}`);
2296
+ }
2297
+ if (plan.customWorkspaceField) {
2298
+ assertSafeCustomWorkspacePath(plan, fs2.realpathSync(plan.workspaceDir));
2299
+ }
2300
+ assertWritableWorkspaceDir(plan.workspaceDir);
2301
+ await ensureGitWorktree(plan.workspaceDir);
2302
+ await ensureGitRemote(plan.workspaceDir, remote);
2303
+ }
2304
+ if (ws.git?.ref) {
2305
+ const dirty = await commandOutput("git", ["status", "--porcelain"], plan.workspaceDir);
2306
+ if (dirty.trim()) {
2307
+ throw new Error(`workspace git tree has local changes; refusing checkout: ${plan.workspaceDir}`);
2308
+ }
2309
+ await runCommand("git", ["fetch", "origin", "--prune"], plan.workspaceDir);
2310
+ await checkoutGitRef(plan.workspaceDir, ws.git.ref);
2311
+ }
2312
+ log2.info(`workspace ready: git ${remote} -> ${plan.workspaceDir}`);
2313
+ return;
2314
+ }
2315
+ throw new Error(`unsupported workspace mode: ${ws.mode ?? "(empty)"}`);
2316
+ }
2317
+ async function verifyExistingWorkspace(plan, log2) {
2318
+ try {
2319
+ if (plan.customWorkspaceField) {
2320
+ assertSafeCustomWorkspacePath(plan);
2321
+ }
2322
+ const st = fs2.statSync(plan.workspaceDir);
2323
+ if (!st.isDirectory()) {
2324
+ log2.warn(`workspace ready state ignored: path is not a directory: ${plan.workspaceDir}`);
2325
+ return false;
2326
+ }
2327
+ if (plan.customWorkspaceField) {
2328
+ assertSafeCustomWorkspacePath(plan, fs2.realpathSync(plan.workspaceDir));
2329
+ }
2330
+ assertWritableWorkspaceDir(plan.workspaceDir);
2331
+ if (plan.workspace.mode === "git") {
2332
+ await ensureGitWorktree(plan.workspaceDir);
2333
+ const remote = plan.workspace.git?.remote?.trim();
2334
+ if (remote) {
2335
+ await ensureGitRemote(plan.workspaceDir, remote);
2336
+ }
2337
+ if (plan.workspace.git?.ref) {
2338
+ const head = (await commandOutput("git", ["rev-parse", "HEAD"], plan.workspaceDir)).trim();
2339
+ const expected = await resolveConfiguredGitRef(plan.workspaceDir, plan.workspace.git.ref);
2340
+ if (head !== expected) {
2341
+ log2.warn(`workspace ready state ignored: git HEAD ${head} does not match ${plan.workspace.git.ref} (${expected})`);
2342
+ return false;
2343
+ }
2344
+ }
2345
+ }
2346
+ return true;
2347
+ } catch (err) {
2348
+ log2.warn(`workspace ready state ignored: ${String(err)}`);
2349
+ return false;
2350
+ }
2351
+ }
2352
+ async function ensureGitWorktree(cwd) {
2353
+ const result = await commandOutput("git", ["rev-parse", "--is-inside-work-tree"], cwd);
2354
+ if (result.trim() !== "true") {
2355
+ throw new Error(`workspace path exists but is not a git worktree: ${cwd}`);
2356
+ }
2357
+ }
2358
+ async function ensureGitRemote(cwd, expectedRemote) {
2359
+ const actualRemote = (await commandOutput("git", ["remote", "get-url", "origin"], cwd)).trim();
2360
+ if (actualRemote !== expectedRemote) {
2361
+ throw new Error(`workspace git remote mismatch: expected ${expectedRemote}, got ${actualRemote}`);
2362
+ }
2363
+ }
2364
+ async function checkoutGitRef(cwd, ref) {
2365
+ const remoteCommit = await resolveRemoteGitRef(cwd, ref);
2366
+ if (remoteCommit) {
2367
+ await runCommand("git", ["checkout", "-B", ref, remoteCommit], cwd);
2368
+ return;
2369
+ }
2370
+ const targetCommit = await resolveGitCommit(cwd, ref);
2371
+ await runCommand("git", ["checkout", "--detach", targetCommit], cwd);
2372
+ }
2373
+ async function resolveConfiguredGitRef(cwd, ref) {
2374
+ const remote = await resolveRemoteGitRef(cwd, ref);
2375
+ return remote || await resolveGitCommit(cwd, ref);
2376
+ }
2377
+ async function resolveRemoteGitRef(cwd, ref) {
2378
+ return (await tryGitOutput("git", ["rev-parse", "--verify", `refs/remotes/origin/${ref}^{commit}`], cwd)).trim();
2379
+ }
2380
+ async function resolveGitCommit(cwd, ref) {
2381
+ const candidates = [`refs/tags/${ref}^{commit}`, `${ref}^{commit}`];
2382
+ for (const candidate of candidates) {
2383
+ const commit = await tryGitOutput("git", ["rev-parse", "--verify", candidate], cwd);
2384
+ if (commit.trim()) {
2385
+ return commit.trim();
2386
+ }
2387
+ }
2388
+ throw new Error(`workspace git ref not found: ${ref}`);
2389
+ }
2390
+ function shouldRunSetup(setup, prior, configHash, forceSetup = false) {
2391
+ if (!setup?.command)
2392
+ return false;
2393
+ if (forceSetup)
2394
+ return true;
2395
+ if (setup.run_on === "config_change") {
2396
+ return prior?.config_hash !== configHash || prior.status === "pending" || prior.status === "failed";
2397
+ }
2398
+ return prior?.status !== "ready" || !prior.prepared_at;
2399
+ }
2400
+ async function runSetup(cwd, command, timeoutSec) {
2401
+ const effectiveTimeoutSec = timeoutSec && timeoutSec > 0 ? timeoutSec : DEFAULT_SETUP_TIMEOUT_SEC;
2402
+ return runCommand(process.env.SHELL || "/bin/sh", ["-lc", command], cwd, effectiveTimeoutSec * 1e3, scrubSetupEnv());
2403
+ }
2404
+ function scrubSetupEnv() {
2405
+ const env = { ...process.env };
2406
+ clearAllProviderCreds(env);
2407
+ delete env.PRLL_API_KEY;
2408
+ return env;
2409
+ }
2410
+ async function commandOutput(cmd, args, cwd) {
2411
+ return runCommand(cmd, args, cwd);
2412
+ }
2413
+ async function tryGitOutput(cmd, args, cwd) {
2414
+ try {
2415
+ return await runCommand(cmd, args, cwd);
2416
+ } catch {
2417
+ return "";
2418
+ }
2419
+ }
2420
+ function runCommand(cmd, args, cwd, timeoutMs = 12e4, env = process.env) {
2421
+ return new Promise((resolve4, reject) => {
2422
+ let tail = "";
2423
+ let timedOut = false;
2424
+ let settled = false;
2425
+ let timeoutTimer = null;
2426
+ let killTimer = null;
2427
+ const append = (chunk) => {
2428
+ tail += chunk.toString();
2429
+ if (tail.length > OUTPUT_TAIL_LIMIT) {
2430
+ tail = tail.slice(-OUTPUT_TAIL_LIMIT);
2431
+ }
2432
+ };
2433
+ const settle = (fn) => {
2434
+ if (settled)
2435
+ return;
2436
+ settled = true;
2437
+ if (timeoutTimer)
2438
+ clearTimeout(timeoutTimer);
2439
+ if (killTimer)
2440
+ clearTimeout(killTimer);
2441
+ fn();
2442
+ };
2443
+ const child = spawn(cmd, args, { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
2444
+ timeoutTimer = setTimeout(() => {
2445
+ timedOut = true;
2446
+ child.kill("SIGTERM");
2447
+ killTimer = setTimeout(() => {
2448
+ child.kill("SIGKILL");
2449
+ }, 5e3);
2450
+ }, timeoutMs);
2451
+ child.stdout?.on("data", append);
2452
+ child.stderr?.on("data", append);
2453
+ child.once("error", (err) => {
2454
+ settle(() => reject(err));
2455
+ });
2456
+ child.once("close", (code, signal) => {
2457
+ if (timedOut) {
2458
+ settle(() => reject(new Error(`command timed out after ${timeoutMs}ms: ${cmd} ${args.join(" ")}
2459
+ ${tail}`)));
2460
+ return;
2461
+ }
2462
+ if (code === 0) {
2463
+ settle(() => resolve4(tail));
2464
+ } else {
2465
+ settle(() => reject(new Error(`command failed (${code ?? signal}): ${cmd} ${args.join(" ")}
2466
+ ${tail}`)));
2467
+ }
2468
+ });
2469
+ });
2470
+ }
2471
+ function requireAbsolute(value, field) {
2472
+ if (!value || !path3.isAbsolute(value)) {
2473
+ throw new Error(`${field} must be an absolute path`);
2474
+ }
2475
+ return path3.resolve(value);
2476
+ }
2477
+ function assertSafeCustomWorkspacePath(plan, candidate = plan.workspaceDir) {
2478
+ if (!plan.customWorkspaceField)
2479
+ return;
2480
+ const normalized = requireAbsolute(candidate, plan.customWorkspaceField);
2481
+ const reason = workspacePathDenyReason(toPolicyPath(normalized));
2482
+ if (reason) {
2483
+ throw new Error(`${plan.customWorkspaceField} must point to a project folder, not ${reason}: ${normalized}`);
2484
+ }
2485
+ const defaultWorkspace = path3.resolve(plan.defaultWorkspaceDir);
2486
+ if (isAncestorPath(normalized, defaultWorkspace) || normalized === defaultWorkspace) {
2487
+ throw new Error(`${plan.customWorkspaceField} must not point at daemon state directories: ${normalized}`);
2488
+ }
2489
+ }
2490
+ function assertWritableWorkspaceDir(dir) {
2491
+ fs2.accessSync(dir, fs2.constants.R_OK | fs2.constants.W_OK | fs2.constants.X_OK);
2492
+ const probe = path3.join(dir, `.parall-workspace-check-${process.pid}-${Date.now()}`);
2493
+ const fd = fs2.openSync(probe, "wx", 384);
2494
+ fs2.closeSync(fd);
2495
+ fs2.unlinkSync(probe);
2496
+ }
2497
+ function workspacePathDenyReason(value) {
2498
+ if (value === "/")
2499
+ return "the filesystem root";
2500
+ if (["/tmp", "/private/tmp", "/var/tmp", "/Users", "/home"].includes(value)) {
2501
+ return "a shared or home root directory";
2502
+ }
2503
+ for (const root of [
2504
+ "/Applications",
2505
+ "/bin",
2506
+ "/boot",
2507
+ "/dev",
2508
+ "/etc",
2509
+ "/Library",
2510
+ "/private/etc",
2511
+ "/private/var/db",
2512
+ "/proc",
2513
+ "/root",
2514
+ "/run",
2515
+ "/sbin",
2516
+ "/System",
2517
+ "/sys",
2518
+ "/usr",
2519
+ "/var/db",
2520
+ "/var/root"
2521
+ ]) {
2522
+ if (value === root || value.startsWith(`${root}/`)) {
2523
+ return "a system directory";
2524
+ }
2525
+ }
2526
+ const parts = value.split("/").filter(Boolean);
2527
+ if (parts.includes(".git"))
2528
+ return "a git metadata directory";
2529
+ if (parts.length === 2 && (parts[0] === "Users" || parts[0] === "home")) {
2530
+ return "a home directory";
2531
+ }
2532
+ if (parts.length >= 3 && (parts[0] === "Users" || parts[0] === "home")) {
2533
+ const homeChild = parts[2];
2534
+ if ([
2535
+ ".aws",
2536
+ ".azure",
2537
+ ".claude",
2538
+ ".codex",
2539
+ ".config",
2540
+ ".docker",
2541
+ ".gnupg",
2542
+ ".kube",
2543
+ ".local",
2544
+ ".npm",
2545
+ ".ssh",
2546
+ ".parall-agent",
2547
+ ".parall-daemon",
2548
+ "Library"
2549
+ ].includes(homeChild)) {
2550
+ return "a credential or application state directory";
2551
+ }
2552
+ }
2553
+ return "";
2554
+ }
2555
+ function isAncestorPath(parent, child) {
2556
+ const relative2 = path3.relative(parent, child);
2557
+ return relative2 !== "" && !relative2.startsWith("..") && !path3.isAbsolute(relative2);
2558
+ }
2559
+ function toPolicyPath(value) {
2560
+ return path3.resolve(value).split(path3.sep).join("/");
2561
+ }
2562
+ function isNodeError(err) {
2563
+ return err instanceof Error && "code" in err;
2564
+ }
2565
+
2061
2566
  // ts/daemon/dist/supervisor.js
2062
2567
  var RUNTIME_PACKAGES = {
2063
2568
  "claude-code": "@parall/claude-agent",
2064
2569
  "codex": "@parall/codex-agent",
2065
2570
  "openclaw": "@parall/openclaw-agent"
2066
2571
  };
2572
+ var WORKSPACE_SETUP_RETRY_DELAY_MS = 5e3;
2067
2573
  function sleepCancellable(ms, signal) {
2068
2574
  if (signal.aborted)
2069
2575
  return Promise.resolve(false);
2070
- return new Promise((resolve3) => {
2576
+ return new Promise((resolve4) => {
2071
2577
  const timer = setTimeout(() => {
2072
2578
  signal.removeEventListener("abort", onAbort);
2073
- resolve3(true);
2579
+ resolve4(true);
2074
2580
  }, ms);
2075
2581
  const onAbort = () => {
2076
2582
  clearTimeout(timer);
2077
- resolve3(false);
2583
+ resolve4(false);
2078
2584
  };
2079
2585
  signal.addEventListener("abort", onAbort, { once: true });
2080
2586
  });
@@ -2084,6 +2590,10 @@ var DaemonSupervisor = class {
2084
2590
  client;
2085
2591
  log;
2086
2592
  children = /* @__PURE__ */ new Map();
2593
+ spawningAgents = /* @__PURE__ */ new Set();
2594
+ pendingWorkspaceSetup = /* @__PURE__ */ new Set();
2595
+ workspaceSetupRetryTimers = /* @__PURE__ */ new Map();
2596
+ cancelledSpawns = /* @__PURE__ */ new Set();
2087
2597
  ws = null;
2088
2598
  running = false;
2089
2599
  machineOrgId = null;
@@ -2144,6 +2654,10 @@ var DaemonSupervisor = class {
2144
2654
  void this.respawnAllChildren();
2145
2655
  }
2146
2656
  });
2657
+ this.ws.on("machine.workspace.setup.requested", (data) => {
2658
+ this.log.info(`WS: workspace setup requested for agent ${data.agent_id}`);
2659
+ void this.handleWorkspaceSetupRequested(data.agent_id);
2660
+ });
2147
2661
  this.ws.on("machine.stop", (data) => {
2148
2662
  this.log.info(`WS: machine.stop received (reason=${data.reason ?? "none"})`);
2149
2663
  void this.stop();
@@ -2154,8 +2668,8 @@ var DaemonSupervisor = class {
2154
2668
  }
2155
2669
  });
2156
2670
  await this.ws.connect();
2157
- await new Promise((resolve3) => {
2158
- this.stopResolve = resolve3;
2671
+ await new Promise((resolve4) => {
2672
+ this.stopResolve = resolve4;
2159
2673
  });
2160
2674
  signal.removeEventListener("abort", onAbort);
2161
2675
  }
@@ -2169,6 +2683,10 @@ var DaemonSupervisor = class {
2169
2683
  this.ws = null;
2170
2684
  }
2171
2685
  const exits = [];
2686
+ for (const timer of this.workspaceSetupRetryTimers.values()) {
2687
+ clearTimeout(timer);
2688
+ }
2689
+ this.workspaceSetupRetryTimers.clear();
2172
2690
  for (const state of this.children.values()) {
2173
2691
  state.shuttingDown = true;
2174
2692
  if (state.restartTimer) {
@@ -2270,15 +2788,15 @@ var DaemonSupervisor = class {
2270
2788
  */
2271
2789
  migrateFlatLayout() {
2272
2790
  const root = this.config.rootStateDir;
2273
- const agentsDir = path3.join(root, "agents");
2274
- const flatWorkspace = path3.join(root, "workspace");
2275
- if (!fs2.existsSync(flatWorkspace) || fs2.existsSync(agentsDir))
2791
+ const agentsDir = path4.join(root, "agents");
2792
+ const flatWorkspace = path4.join(root, "workspace");
2793
+ if (!fs3.existsSync(flatWorkspace) || fs3.existsSync(agentsDir))
2276
2794
  return;
2277
2795
  let ownerAgentId;
2278
- const sessionsDir = path3.join(root, "sessions");
2279
- if (fs2.existsSync(sessionsDir)) {
2796
+ const sessionsDir = path4.join(root, "sessions");
2797
+ if (fs3.existsSync(sessionsDir)) {
2280
2798
  try {
2281
- for (const file of fs2.readdirSync(sessionsDir)) {
2799
+ for (const file of fs3.readdirSync(sessionsDir)) {
2282
2800
  if (!file.endsWith(".json"))
2283
2801
  continue;
2284
2802
  const decoded = Buffer.from(file.replace(".json", ""), "base64url").toString();
@@ -2292,13 +2810,13 @@ var DaemonSupervisor = class {
2292
2810
  }
2293
2811
  }
2294
2812
  const targetId = ownerAgentId ?? "_orphan";
2295
- const targetDir = path3.join(agentsDir, targetId);
2813
+ const targetDir = path4.join(agentsDir, targetId);
2296
2814
  try {
2297
- fs2.mkdirSync(targetDir, { recursive: true });
2815
+ fs3.mkdirSync(targetDir, { recursive: true });
2298
2816
  for (const sub of ["workspace", "sessions", "dispatch-context"]) {
2299
- const src = path3.join(root, sub);
2300
- if (fs2.existsSync(src)) {
2301
- fs2.renameSync(src, path3.join(targetDir, sub));
2817
+ const src = path4.join(root, sub);
2818
+ if (fs3.existsSync(src)) {
2819
+ fs3.renameSync(src, path4.join(targetDir, sub));
2302
2820
  }
2303
2821
  }
2304
2822
  this.log.info(`migrated legacy flat state \u2192 agents/${targetId}/`);
@@ -2307,23 +2825,30 @@ var DaemonSupervisor = class {
2307
2825
  }
2308
2826
  }
2309
2827
  // ---- WS event handlers (incremental) ----
2310
- async handleAgentAttached(agentId) {
2311
- if (this.children.has(agentId))
2312
- return;
2828
+ async fetchAttachedAgent(agentId) {
2313
2829
  let attached;
2314
2830
  try {
2315
2831
  attached = await this.client.listAttachedAgents();
2316
2832
  } catch (err) {
2317
- this.log.warn(`handleAgentAttached: listAttachedAgents failed: ${String(err)}`);
2318
- return;
2833
+ this.log.warn(`fetchAttachedAgent: listAttachedAgents failed: ${String(err)}`);
2834
+ return { kind: "retryable" };
2319
2835
  }
2320
2836
  const entry = attached.find((a) => (a.user?.id ?? a.profile.user_id) === agentId);
2321
2837
  if (!entry) {
2322
- this.log.warn(`handleAgentAttached: agent ${agentId} not found in attached list`);
2323
- return;
2838
+ this.log.warn(`fetchAttachedAgent: agent ${agentId} not found in attached list`);
2839
+ return { kind: "skip" };
2324
2840
  }
2325
2841
  if (entry.user && entry.user.status !== "active") {
2326
2842
  this.log.info(`agent ${agentId} not active (status=${entry.user.status}) \u2014 skipping`);
2843
+ return { kind: "skip" };
2844
+ }
2845
+ return { kind: "found", entry };
2846
+ }
2847
+ async handleAgentAttached(agentId) {
2848
+ if (this.children.has(agentId) || this.spawningAgents.has(agentId))
2849
+ return;
2850
+ const result = await this.fetchAttachedAgent(agentId);
2851
+ if (result.kind !== "found") {
2327
2852
  return;
2328
2853
  }
2329
2854
  const orgId = this.machineOrgId;
@@ -2331,9 +2856,14 @@ var DaemonSupervisor = class {
2331
2856
  this.log.warn(`agent ${agentId}: no org_id available yet; skipping`);
2332
2857
  return;
2333
2858
  }
2334
- await this.spawnAgent(agentId, orgId, entry);
2859
+ await this.spawnAgent(agentId, orgId, result.entry);
2335
2860
  }
2336
2861
  async handleAgentDetached(agentId) {
2862
+ this.pendingWorkspaceSetup.delete(agentId);
2863
+ this.clearWorkspaceSetupRetry(agentId);
2864
+ if (this.spawningAgents.has(agentId)) {
2865
+ this.cancelledSpawns.add(agentId);
2866
+ }
2337
2867
  const state = this.children.get(agentId);
2338
2868
  if (!state)
2339
2869
  return;
@@ -2346,6 +2876,61 @@ var DaemonSupervisor = class {
2346
2876
  await this.terminateChild(state);
2347
2877
  this.children.delete(agentId);
2348
2878
  }
2879
+ async handleWorkspaceSetupRequested(agentId) {
2880
+ if (this.spawningAgents.has(agentId)) {
2881
+ this.log.info(`agent ${agentId}: workspace setup already in progress; queueing one restart`);
2882
+ this.pendingWorkspaceSetup.add(agentId);
2883
+ return;
2884
+ }
2885
+ const result = await this.fetchAttachedAgent(agentId);
2886
+ if (result.kind === "retryable") {
2887
+ this.pendingWorkspaceSetup.add(agentId);
2888
+ this.scheduleWorkspaceSetupRetry(agentId);
2889
+ return;
2890
+ }
2891
+ if (result.kind === "skip") {
2892
+ this.pendingWorkspaceSetup.delete(agentId);
2893
+ this.clearWorkspaceSetupRetry(agentId);
2894
+ return;
2895
+ }
2896
+ this.clearWorkspaceSetupRetry(agentId);
2897
+ const state = this.children.get(agentId);
2898
+ if (state) {
2899
+ this.log.info(`agent ${agentId}: restarting for workspace setup`);
2900
+ state.shuttingDown = true;
2901
+ if (state.restartTimer) {
2902
+ clearTimeout(state.restartTimer);
2903
+ state.restartTimer = null;
2904
+ }
2905
+ await this.terminateChild(state);
2906
+ this.children.delete(agentId);
2907
+ }
2908
+ const orgId = this.machineOrgId;
2909
+ if (!orgId) {
2910
+ this.log.warn(`agent ${agentId}: no org_id available yet; skipping`);
2911
+ return;
2912
+ }
2913
+ await this.spawnAgent(agentId, orgId, result.entry);
2914
+ }
2915
+ scheduleWorkspaceSetupRetry(agentId) {
2916
+ if (!this.running || this.workspaceSetupRetryTimers.has(agentId))
2917
+ return;
2918
+ const timer = setTimeout(() => {
2919
+ this.workspaceSetupRetryTimers.delete(agentId);
2920
+ if (this.running && this.pendingWorkspaceSetup.has(agentId)) {
2921
+ void this.handleWorkspaceSetupRequested(agentId);
2922
+ }
2923
+ }, WORKSPACE_SETUP_RETRY_DELAY_MS);
2924
+ timer.unref?.();
2925
+ this.workspaceSetupRetryTimers.set(agentId, timer);
2926
+ }
2927
+ clearWorkspaceSetupRetry(agentId) {
2928
+ const timer = this.workspaceSetupRetryTimers.get(agentId);
2929
+ if (timer) {
2930
+ clearTimeout(timer);
2931
+ this.workspaceSetupRetryTimers.delete(agentId);
2932
+ }
2933
+ }
2349
2934
  // ---- Spawn / restart ----
2350
2935
  async refreshMachineConfig() {
2351
2936
  try {
@@ -2385,6 +2970,25 @@ var DaemonSupervisor = class {
2385
2970
  }
2386
2971
  }
2387
2972
  async spawnAgent(agentId, orgId, attached) {
2973
+ if (this.children.has(agentId) || this.spawningAgents.has(agentId)) {
2974
+ return;
2975
+ }
2976
+ this.spawningAgents.add(agentId);
2977
+ let shouldReplaySetupRequest = false;
2978
+ try {
2979
+ await this.spawnAgentOnce(agentId, orgId, attached);
2980
+ } finally {
2981
+ this.spawningAgents.delete(agentId);
2982
+ this.cancelledSpawns.delete(agentId);
2983
+ shouldReplaySetupRequest = this.pendingWorkspaceSetup.delete(agentId);
2984
+ }
2985
+ if (shouldReplaySetupRequest && this.running) {
2986
+ queueMicrotask(() => {
2987
+ void this.handleWorkspaceSetupRequested(agentId);
2988
+ });
2989
+ }
2990
+ }
2991
+ async spawnAgentOnce(agentId, orgId, attached) {
2388
2992
  let credential;
2389
2993
  try {
2390
2994
  credential = await this.client.mintLaunchCredential(agentId);
@@ -2393,22 +2997,36 @@ var DaemonSupervisor = class {
2393
2997
  return;
2394
2998
  }
2395
2999
  const stateDir = agentStateDirFor(this.config.rootStateDir, agentId);
2396
- const workspaceDir = attached.daemon_config?.workspace_path || agentWorkspaceDirFor(this.config.rootStateDir, agentId);
3000
+ const defaultWorkspaceDir = agentWorkspaceDirFor(this.config.rootStateDir, agentId);
2397
3001
  const isK8s = !!process.env.KUBERNETES_SERVICE_HOST;
2398
3002
  const claudeHome = isK8s ? agentClaudeHomeFor(this.config.rootClaudeHome, agentId) : this.config.rootClaudeHome;
2399
3003
  try {
2400
- fs2.mkdirSync(stateDir, { recursive: true });
2401
- if (!attached.daemon_config?.workspace_path) {
2402
- fs2.mkdirSync(workspaceDir, { recursive: true });
2403
- }
3004
+ fs3.mkdirSync(stateDir, { recursive: true });
2404
3005
  if (isK8s) {
2405
- fs2.mkdirSync(claudeHome, { recursive: true });
3006
+ fs3.mkdirSync(claudeHome, { recursive: true });
2406
3007
  this.ensureSharedCredentialLink(claudeHome, agentId);
2407
3008
  }
2408
3009
  } catch (err) {
2409
3010
  this.log.warn(`mkdir agent dirs (${agentId}) failed: ${String(err)}`);
2410
3011
  }
2411
3012
  const runtimeType = attached.profile.runtime_type ?? "claude-code";
3013
+ let workspaceDir;
3014
+ try {
3015
+ workspaceDir = await prepareWorkspace({
3016
+ client: this.client,
3017
+ attached,
3018
+ agentId,
3019
+ defaultWorkspaceDir,
3020
+ log: this.log
3021
+ });
3022
+ } catch (err) {
3023
+ this.log.error(`agent ${agentId}: workspace setup failed: ${String(err)}`);
3024
+ return;
3025
+ }
3026
+ if (this.cancelledSpawns.delete(agentId)) {
3027
+ this.log.info(`agent ${agentId}: spawn cancelled after detach during workspace setup`);
3028
+ return;
3029
+ }
2412
3030
  const state = {
2413
3031
  agentId,
2414
3032
  orgId,
@@ -2441,7 +3059,7 @@ var DaemonSupervisor = class {
2441
3059
  };
2442
3060
  const env = adapter.buildEnv({ ...process.env, PRLL_API_URL: this.config.apiUrl }, state.agentId, state.orgId, state.credential.api_key, dirs, state.providerConfig);
2443
3061
  this.log.info(`spawning agent ${state.agentId} runtime=${state.runtimeType} bin=${adapter.bin} (attempt ${state.restartAttempts + 1})`);
2444
- const child = spawn(adapter.bin, [], {
3062
+ const child = spawn2(adapter.bin, [], {
2445
3063
  env,
2446
3064
  stdio: ["ignore", "inherit", "inherit"],
2447
3065
  detached: false
@@ -2479,25 +3097,26 @@ var DaemonSupervisor = class {
2479
3097
  settleChild("error", null, null, err);
2480
3098
  });
2481
3099
  child.once("close", (code, signal) => settleChild("close", code, signal));
2482
- setTimeout(() => {
3100
+ const stableTimer = setTimeout(() => {
2483
3101
  if (state.child === child) {
2484
3102
  state.restartAttempts = 0;
2485
3103
  }
2486
3104
  }, Math.max(this.config.restartBackoffMs, 3e4));
3105
+ stableTimer.unref?.();
2487
3106
  }
2488
3107
  async terminateChild(state) {
2489
3108
  const child = state.child;
2490
3109
  if (!child)
2491
3110
  return;
2492
- return new Promise((resolve3) => {
2493
- const onExit = () => resolve3();
3111
+ return new Promise((resolve4) => {
3112
+ const onExit = () => resolve4();
2494
3113
  child.once("exit", onExit);
2495
3114
  try {
2496
3115
  child.kill("SIGTERM");
2497
3116
  } catch (err) {
2498
3117
  this.log.warn(`SIGTERM ${state.agentId} threw: ${String(err)}`);
2499
3118
  child.off("exit", onExit);
2500
- resolve3();
3119
+ resolve4();
2501
3120
  return;
2502
3121
  }
2503
3122
  const hardKill = setTimeout(() => {
@@ -2510,60 +3129,60 @@ var DaemonSupervisor = class {
2510
3129
  });
2511
3130
  }
2512
3131
  ensureSharedCredentialLink(agentClaudeHome, agentId) {
2513
- const sharedCredentials = path3.resolve(sharedClaudeCredentialsFileFor(this.config.rootClaudeHome));
3132
+ const sharedCredentials = path4.resolve(sharedClaudeCredentialsFileFor(this.config.rootClaudeHome));
2514
3133
  const agentCredentials = agentClaudeCredentialsFileFor(agentClaudeHome);
2515
- const agentCredentialsDir = path3.dirname(agentCredentials);
2516
- fs2.mkdirSync(path3.dirname(sharedCredentials), { recursive: true });
2517
- fs2.mkdirSync(agentCredentialsDir, { recursive: true });
3134
+ const agentCredentialsDir = path4.dirname(agentCredentials);
3135
+ fs3.mkdirSync(path4.dirname(sharedCredentials), { recursive: true });
3136
+ fs3.mkdirSync(agentCredentialsDir, { recursive: true });
2518
3137
  try {
2519
- const existing = fs2.lstatSync(agentCredentials);
3138
+ const existing = fs3.lstatSync(agentCredentials);
2520
3139
  if (existing.isSymbolicLink()) {
2521
- const currentTarget = fs2.readlinkSync(agentCredentials);
2522
- if (path3.resolve(agentCredentialsDir, currentTarget) === sharedCredentials) {
3140
+ const currentTarget = fs3.readlinkSync(agentCredentials);
3141
+ if (path4.resolve(agentCredentialsDir, currentTarget) === sharedCredentials) {
2523
3142
  return;
2524
3143
  }
2525
- fs2.unlinkSync(agentCredentials);
3144
+ fs3.unlinkSync(agentCredentials);
2526
3145
  } else if (existing.isDirectory()) {
2527
3146
  this.log.warn(`agent ${agentId}: credential path is a directory, cannot link ${agentCredentials}`);
2528
3147
  return;
2529
3148
  } else {
2530
- fs2.unlinkSync(agentCredentials);
3149
+ fs3.unlinkSync(agentCredentials);
2531
3150
  }
2532
3151
  } catch (err) {
2533
3152
  if (err.code !== "ENOENT") {
2534
3153
  throw err;
2535
3154
  }
2536
3155
  }
2537
- fs2.symlinkSync(sharedCredentials, agentCredentials);
3156
+ fs3.symlinkSync(sharedCredentials, agentCredentials);
2538
3157
  }
2539
3158
  };
2540
3159
 
2541
3160
  // ts/daemon/dist/cli.js
2542
- import * as fs3 from "node:fs";
2543
- import * as path4 from "node:path";
3161
+ import * as fs4 from "node:fs";
3162
+ import * as path5 from "node:path";
2544
3163
  import * as os2 from "node:os";
2545
3164
  import * as readline from "node:readline";
2546
- import { spawn as spawn2, execSync } from "node:child_process";
3165
+ import { spawn as spawn3, execSync } from "node:child_process";
2547
3166
  var CONFIG_DIR = daemonConfigDir();
2548
3167
  var CONFIG_PATH = daemonConfigPath();
2549
3168
  function readConfig() {
2550
3169
  try {
2551
- return JSON.parse(fs3.readFileSync(CONFIG_PATH, "utf-8"));
3170
+ return JSON.parse(fs4.readFileSync(CONFIG_PATH, "utf-8"));
2552
3171
  } catch {
2553
3172
  return null;
2554
3173
  }
2555
3174
  }
2556
3175
  function writeConfig(config) {
2557
- fs3.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
2558
- fs3.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 384 });
2559
- fs3.chmodSync(CONFIG_PATH, 384);
3176
+ fs4.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
3177
+ fs4.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 384 });
3178
+ fs4.chmodSync(CONFIG_PATH, 384);
2560
3179
  }
2561
3180
  function prompt(question) {
2562
3181
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
2563
- return new Promise((resolve3) => {
3182
+ return new Promise((resolve4) => {
2564
3183
  rl.question(question, (answer) => {
2565
3184
  rl.close();
2566
- resolve3(answer.trim());
3185
+ resolve4(answer.trim());
2567
3186
  });
2568
3187
  });
2569
3188
  }
@@ -2575,10 +3194,10 @@ function isLinux() {
2575
3194
  }
2576
3195
  var PLIST_LABEL = "com.parall.daemon";
2577
3196
  function plistPath() {
2578
- return path4.join(os2.homedir(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
3197
+ return path5.join(os2.homedir(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
2579
3198
  }
2580
3199
  function systemdUnitPath() {
2581
- return path4.join(os2.homedir(), ".config", "systemd", "user", "parall-daemon.service");
3200
+ return path5.join(os2.homedir(), ".config", "systemd", "user", "parall-daemon.service");
2582
3201
  }
2583
3202
  function getDaemonBin() {
2584
3203
  try {
@@ -2588,7 +3207,7 @@ function getDaemonBin() {
2588
3207
  }
2589
3208
  }
2590
3209
  function generatePlist(daemonBin) {
2591
- const logPath = path4.join(os2.homedir(), "Library", "Logs", "parall-daemon.log");
3210
+ const logPath = path5.join(os2.homedir(), "Library", "Logs", "parall-daemon.log");
2592
3211
  return `<?xml version="1.0" encoding="UTF-8"?>
2593
3212
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2594
3213
  <plist version="1.0">
@@ -2635,16 +3254,16 @@ function installService() {
2635
3254
  }
2636
3255
  const bin = getDaemonBin();
2637
3256
  if (isMacOS()) {
2638
- const dir = path4.dirname(plistPath());
2639
- fs3.mkdirSync(dir, { recursive: true });
2640
- fs3.writeFileSync(plistPath(), generatePlist(bin));
3257
+ const dir = path5.dirname(plistPath());
3258
+ fs4.mkdirSync(dir, { recursive: true });
3259
+ fs4.writeFileSync(plistPath(), generatePlist(bin));
2641
3260
  execSync(`launchctl bootout gui/$(id -u) ${plistPath()} 2>/dev/null || true`);
2642
3261
  execSync(`launchctl bootstrap gui/$(id -u) ${plistPath()}`);
2643
3262
  console.log(`launchd agent installed: ${plistPath()}`);
2644
3263
  } else if (isLinux()) {
2645
- const dir = path4.dirname(systemdUnitPath());
2646
- fs3.mkdirSync(dir, { recursive: true });
2647
- fs3.writeFileSync(systemdUnitPath(), generateSystemdUnit(bin));
3264
+ const dir = path5.dirname(systemdUnitPath());
3265
+ fs4.mkdirSync(dir, { recursive: true });
3266
+ fs4.writeFileSync(systemdUnitPath(), generateSystemdUnit(bin));
2648
3267
  execSync("systemctl --user daemon-reload");
2649
3268
  execSync("systemctl --user enable --now parall-daemon");
2650
3269
  console.log(`systemd service installed: ${systemdUnitPath()}`);
@@ -2705,31 +3324,31 @@ function cmdStop() {
2705
3324
  }
2706
3325
  function cmdLogs(lines) {
2707
3326
  if (isLinux()) {
2708
- const child2 = spawn2("journalctl", ["--user-unit", "parall-daemon", "-n", lines, "-f"], {
3327
+ const child2 = spawn3("journalctl", ["--user-unit", "parall-daemon", "-n", lines, "-f"], {
2709
3328
  stdio: "inherit"
2710
3329
  });
2711
3330
  child2.on("exit", (code) => process.exit(code ?? 0));
2712
3331
  return;
2713
3332
  }
2714
- const logPath = path4.join(os2.homedir(), "Library", "Logs", "parall-daemon.log");
2715
- if (!fs3.existsSync(logPath)) {
3333
+ const logPath = path5.join(os2.homedir(), "Library", "Logs", "parall-daemon.log");
3334
+ if (!fs4.existsSync(logPath)) {
2716
3335
  console.log("No log file found at", logPath);
2717
3336
  return;
2718
3337
  }
2719
- const child = spawn2("tail", ["-n", lines, "-f", logPath], { stdio: "inherit" });
3338
+ const child = spawn3("tail", ["-n", lines, "-f", logPath], { stdio: "inherit" });
2720
3339
  child.on("exit", (code) => process.exit(code ?? 0));
2721
3340
  }
2722
3341
  function cmdServiceUninstall() {
2723
3342
  if (isMacOS()) {
2724
3343
  execSync(`launchctl bootout gui/$(id -u) ${plistPath()} 2>/dev/null || true`);
2725
- if (fs3.existsSync(plistPath()))
2726
- fs3.unlinkSync(plistPath());
3344
+ if (fs4.existsSync(plistPath()))
3345
+ fs4.unlinkSync(plistPath());
2727
3346
  console.log("launchd agent uninstalled.");
2728
3347
  } else if (isLinux()) {
2729
3348
  execSync("systemctl --user stop parall-daemon 2>/dev/null || true");
2730
3349
  execSync("systemctl --user disable parall-daemon 2>/dev/null || true");
2731
- if (fs3.existsSync(systemdUnitPath()))
2732
- fs3.unlinkSync(systemdUnitPath());
3350
+ if (fs4.existsSync(systemdUnitPath()))
3351
+ fs4.unlinkSync(systemdUnitPath());
2733
3352
  execSync("systemctl --user daemon-reload");
2734
3353
  console.log("systemd service uninstalled.");
2735
3354
  } else {
@@ -2846,7 +3465,8 @@ async function main() {
2846
3465
  log.info(`keepalive: bootstrapBackoffMs=${config.bootstrapBackoffMs} supervisorRestartBackoffMs=${config.supervisorRestartBackoffMs}`);
2847
3466
  const client = new ParallClient({
2848
3467
  baseUrl: config.apiUrl,
2849
- token: config.apiKey
3468
+ token: config.apiKey,
3469
+ swimlaneName: config.swimlaneName
2850
3470
  });
2851
3471
  const abortController = new AbortController();
2852
3472
  const onSignal = (sig) => {
@@ -2863,7 +3483,7 @@ async function main() {
2863
3483
  process.exitCode = 1;
2864
3484
  process.exit(1);
2865
3485
  });
2866
- config.wsUrl = resolveWsUrl(config.apiUrl, config.wsUrl);
3486
+ config.wsUrl = resolveWsUrl(config.apiUrl, config.wsUrl, config.swimlaneName);
2867
3487
  await runForever(config, client, log, abortController.signal);
2868
3488
  }
2869
3489
  var cliArgs = process.argv.slice(2);