@parall/daemon 1.28.0 → 1.28.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.
- package/bundle/manifest.json +22 -0
- package/bundle/parall-claude-agent.js +5685 -0
- package/bundle/parall-codex-agent.js +6640 -0
- package/bundle/parall-daemon.js +2786 -0
- package/bundle/parall-openclaw-agent.js +220 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +277 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +43 -2
- package/dist/index.js +12 -1
- package/dist/supervisor.d.ts +6 -0
- package/dist/supervisor.d.ts.map +1 -1
- package/dist/supervisor.js +80 -8
- package/package.json +16 -8
- package/src/config.ts +0 -146
- package/src/index.ts +0 -132
- package/src/runtimes.ts +0 -91
- package/src/supervisor.ts +0 -480
package/dist/supervisor.js
CHANGED
|
@@ -4,6 +4,11 @@ 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
|
+
const RUNTIME_PACKAGES = {
|
|
8
|
+
'claude-code': '@parall/claude-agent',
|
|
9
|
+
'codex': '@parall/codex-agent',
|
|
10
|
+
'openclaw': '@parall/openclaw-agent',
|
|
11
|
+
};
|
|
7
12
|
/**
|
|
8
13
|
* Sleep that wakes early on abort. Returns true if the full delay elapsed,
|
|
9
14
|
* false if aborted. Used by bootstrap retry and the outer keepalive in
|
|
@@ -71,6 +76,7 @@ export class DaemonSupervisor {
|
|
|
71
76
|
this.running = false;
|
|
72
77
|
throw err;
|
|
73
78
|
}
|
|
79
|
+
this.migrateFlatLayout();
|
|
74
80
|
await this.fullReconcile();
|
|
75
81
|
this.ws = new ParallWs({
|
|
76
82
|
getTicket: () => this.client.getMachineWsTicket(),
|
|
@@ -209,6 +215,54 @@ export class DaemonSupervisor {
|
|
|
209
215
|
}
|
|
210
216
|
}
|
|
211
217
|
}
|
|
218
|
+
// ---- Flat layout migration (self-hosted → daemon) ----
|
|
219
|
+
/**
|
|
220
|
+
* Detects a legacy flat state layout (no agents/ subdir) and migrates it
|
|
221
|
+
* into the per-agent directory for the owning agent. Ownership is determined
|
|
222
|
+
* by parsing session state files which embed the agent ID in the runtimeKey.
|
|
223
|
+
*/
|
|
224
|
+
migrateFlatLayout() {
|
|
225
|
+
const root = this.config.rootStateDir;
|
|
226
|
+
const agentsDir = path.join(root, "agents");
|
|
227
|
+
const flatWorkspace = path.join(root, "workspace");
|
|
228
|
+
if (!fs.existsSync(flatWorkspace) || fs.existsSync(agentsDir))
|
|
229
|
+
return;
|
|
230
|
+
let ownerAgentId;
|
|
231
|
+
const sessionsDir = path.join(root, "sessions");
|
|
232
|
+
if (fs.existsSync(sessionsDir)) {
|
|
233
|
+
try {
|
|
234
|
+
for (const file of fs.readdirSync(sessionsDir)) {
|
|
235
|
+
if (!file.endsWith(".json"))
|
|
236
|
+
continue;
|
|
237
|
+
const decoded = Buffer.from(file.replace(".json", ""), "base64url").toString();
|
|
238
|
+
// runtimeKey format: "agent:main:{runtime}:{agentId}:orchestrator"
|
|
239
|
+
const parts = decoded.split(":");
|
|
240
|
+
if (parts.length >= 4 && parts[3].startsWith("usr_")) {
|
|
241
|
+
ownerAgentId = parts[3];
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
// best-effort scan
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const targetId = ownerAgentId ?? "_orphan";
|
|
251
|
+
const targetDir = path.join(agentsDir, targetId);
|
|
252
|
+
try {
|
|
253
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
254
|
+
for (const sub of ["workspace", "sessions", "dispatch-context"]) {
|
|
255
|
+
const src = path.join(root, sub);
|
|
256
|
+
if (fs.existsSync(src)) {
|
|
257
|
+
fs.renameSync(src, path.join(targetDir, sub));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
this.log.info(`migrated legacy flat state → agents/${targetId}/`);
|
|
261
|
+
}
|
|
262
|
+
catch (err) {
|
|
263
|
+
this.log.warn(`flat layout migration failed: ${String(err)}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
212
266
|
// ---- WS event handlers (incremental) ----
|
|
213
267
|
async handleAgentAttached(agentId) {
|
|
214
268
|
if (this.children.has(agentId))
|
|
@@ -275,13 +329,23 @@ export class DaemonSupervisor {
|
|
|
275
329
|
return;
|
|
276
330
|
}
|
|
277
331
|
const stateDir = agentStateDirFor(this.config.rootStateDir, agentId);
|
|
278
|
-
const workspaceDir =
|
|
279
|
-
|
|
332
|
+
const workspaceDir = attached.daemon_config?.workspace_path
|
|
333
|
+
|| agentWorkspaceDirFor(this.config.rootStateDir, agentId);
|
|
334
|
+
const isK8s = !!process.env.KUBERNETES_SERVICE_HOST;
|
|
335
|
+
const claudeHome = isK8s
|
|
336
|
+
? agentClaudeHomeFor(this.config.rootClaudeHome, agentId)
|
|
337
|
+
: this.config.rootClaudeHome;
|
|
280
338
|
try {
|
|
281
339
|
fs.mkdirSync(stateDir, { recursive: true });
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
+
if (isK8s) {
|
|
346
|
+
fs.mkdirSync(claudeHome, { recursive: true });
|
|
347
|
+
this.ensureSharedCredentialLink(claudeHome, agentId);
|
|
348
|
+
}
|
|
285
349
|
}
|
|
286
350
|
catch (err) {
|
|
287
351
|
this.log.warn(`mkdir agent dirs (${agentId}) failed: ${String(err)}`);
|
|
@@ -291,6 +355,8 @@ export class DaemonSupervisor {
|
|
|
291
355
|
agentId,
|
|
292
356
|
orgId,
|
|
293
357
|
runtimeType,
|
|
358
|
+
workspacePath: workspaceDir,
|
|
359
|
+
claudeHome,
|
|
294
360
|
child: null,
|
|
295
361
|
credential,
|
|
296
362
|
restartAttempts: 0,
|
|
@@ -311,8 +377,8 @@ export class DaemonSupervisor {
|
|
|
311
377
|
const adapter = getRuntimeAdapter(state.runtimeType);
|
|
312
378
|
const dirs = {
|
|
313
379
|
stateDir: agentStateDirFor(this.config.rootStateDir, state.agentId),
|
|
314
|
-
workspaceDir:
|
|
315
|
-
claudeHome:
|
|
380
|
+
workspaceDir: state.workspacePath,
|
|
381
|
+
claudeHome: state.claudeHome,
|
|
316
382
|
};
|
|
317
383
|
const env = adapter.buildEnv({ ...process.env, PRLL_API_URL: this.config.apiUrl }, state.agentId, state.orgId, state.credential.api_key, dirs);
|
|
318
384
|
this.log.info(`spawning agent ${state.agentId} runtime=${state.runtimeType} bin=${adapter.bin} (attempt ${state.restartAttempts + 1})`);
|
|
@@ -347,7 +413,13 @@ export class DaemonSupervisor {
|
|
|
347
413
|
this.startChild(state);
|
|
348
414
|
}, delay);
|
|
349
415
|
};
|
|
350
|
-
child.once("error", (err) =>
|
|
416
|
+
child.once("error", (err) => {
|
|
417
|
+
if (err.code === 'ENOENT') {
|
|
418
|
+
const pkg = RUNTIME_PACKAGES[state.runtimeType] ?? `@parall/${state.runtimeType}-agent`;
|
|
419
|
+
this.log.error(`Runtime binary "${adapter.bin}" not found in PATH. Install: npm install -g ${pkg}`);
|
|
420
|
+
}
|
|
421
|
+
settleChild("error", null, null, err);
|
|
422
|
+
});
|
|
351
423
|
child.once("close", (code, signal) => settleChild("close", code, signal));
|
|
352
424
|
setTimeout(() => {
|
|
353
425
|
if (state.child === child) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parall/daemon",
|
|
3
|
-
"version": "1.28.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.28.1",
|
|
4
|
+
"description": "Parall local agent runtime — daemon supervisor + bridge runtimes, bundled as standalone JS files",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -9,10 +9,13 @@
|
|
|
9
9
|
"directory": "ts/daemon"
|
|
10
10
|
},
|
|
11
11
|
"type": "module",
|
|
12
|
-
"main": "./
|
|
12
|
+
"main": "./bundle/parall-daemon.js",
|
|
13
13
|
"types": "./dist/index.d.ts",
|
|
14
14
|
"bin": {
|
|
15
|
-
"parall-daemon": "./
|
|
15
|
+
"parall-daemon": "./bundle/parall-daemon.js",
|
|
16
|
+
"parall-claude-agent": "./bundle/parall-claude-agent.js",
|
|
17
|
+
"parall-codex-agent": "./bundle/parall-codex-agent.js",
|
|
18
|
+
"parall-openclaw-agent": "./bundle/parall-openclaw-agent.js"
|
|
16
19
|
},
|
|
17
20
|
"exports": {
|
|
18
21
|
".": {
|
|
@@ -21,18 +24,23 @@
|
|
|
21
24
|
}
|
|
22
25
|
},
|
|
23
26
|
"files": [
|
|
24
|
-
"
|
|
25
|
-
"
|
|
27
|
+
"bundle",
|
|
28
|
+
"dist"
|
|
26
29
|
],
|
|
27
30
|
"dependencies": {
|
|
28
|
-
"@parall/sdk": "1.28.
|
|
31
|
+
"@parall/sdk": "1.28.1",
|
|
32
|
+
"@parall/claude-agent": "1.28.1",
|
|
33
|
+
"@parall/codex-agent": "1.28.1",
|
|
34
|
+
"@parall/openclaw-agent": "1.28.1"
|
|
29
35
|
},
|
|
30
36
|
"devDependencies": {
|
|
31
37
|
"@types/node": "^22.0.0",
|
|
38
|
+
"esbuild": "^0.25.0",
|
|
32
39
|
"typescript": "^5.7.0"
|
|
33
40
|
},
|
|
34
41
|
"scripts": {
|
|
35
42
|
"build": "tsc -b",
|
|
36
|
-
"
|
|
43
|
+
"bundle": "node ../../scripts/bundle-daemon.mjs",
|
|
44
|
+
"start": "node bundle/parall-daemon.js"
|
|
37
45
|
}
|
|
38
46
|
}
|
package/src/config.ts
DELETED
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import * as os from "node:os";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Daemon-mode env. The daemon authenticates to the Parall API with an
|
|
6
|
-
* `mck_*` bearer token (PRLL_API_KEY) and supervises N per-agent
|
|
7
|
-
* `parall-claude-agent` subprocesses on this host.
|
|
8
|
-
*
|
|
9
|
-
* Compared to claude-agent's config:
|
|
10
|
-
* - `apiKey` is an mck_ bearer (Machine-scoped), not an agk_
|
|
11
|
-
* - there is no PRLL_ORG_ID — the Machine row's org_id is implicit;
|
|
12
|
-
* each spawned per-agent subprocess is given its own PRLL_ORG_ID
|
|
13
|
-
* resolved from the AttachedAgent's profile / org context
|
|
14
|
-
* - per-agent state lives at `<rootStateDir>/agents/<agent_id>/`
|
|
15
|
-
*/
|
|
16
|
-
export type ClaudeDaemonConfig = {
|
|
17
|
-
apiUrl: string;
|
|
18
|
-
/** mck_*-prefixed Machine bearer. */
|
|
19
|
-
apiKey: string;
|
|
20
|
-
/** @deprecated Superseded by RuntimeAdapter pattern (runtimes.ts). Kept for backward compat / fallback. */
|
|
21
|
-
agentBin: string;
|
|
22
|
-
/** Root for per-agent state dirs. Each agent gets `<rootStateDir>/agents/<agent_id>`. */
|
|
23
|
-
rootStateDir: string;
|
|
24
|
-
/** Shared host home root. Per-agent HOME dirs live under `<rootClaudeHome>/agents/<agent_id>`. */
|
|
25
|
-
rootClaudeHome: string;
|
|
26
|
-
/** Optional WS URL override for the machine control-plane WebSocket. */
|
|
27
|
-
wsUrl?: string;
|
|
28
|
-
/** @deprecated Superseded by WS event-driven model; kept for backward compat. */
|
|
29
|
-
pollIntervalMs: number;
|
|
30
|
-
/** @deprecated Superseded by WS event-driven model; kept for backward compat. */
|
|
31
|
-
heartbeatIntervalMs: number;
|
|
32
|
-
/** Backoff base after a subprocess crash, in ms. */
|
|
33
|
-
restartBackoffMs: number;
|
|
34
|
-
/** Hard cap on parallel restart attempts per agent before giving up for a cycle. */
|
|
35
|
-
restartBackoffMaxMs: number;
|
|
36
|
-
/**
|
|
37
|
-
* Bootstrap retry: when `getMachineSelf` fails on startup (network blip,
|
|
38
|
-
* mck_ key not yet propagated, server warming up), wait this long and
|
|
39
|
-
* retry instead of crashing the daemon. Exponential backoff up to
|
|
40
|
-
* `bootstrapBackoffMaxMs`. Set 0 to disable (fail-fast on first error).
|
|
41
|
-
*/
|
|
42
|
-
bootstrapBackoffMs: number;
|
|
43
|
-
bootstrapBackoffMaxMs: number;
|
|
44
|
-
/**
|
|
45
|
-
* Outer supervisor keepalive: if `supervisor.run()` rejects with an
|
|
46
|
-
* unexpected error, wait this long and reinstantiate. Same exp-backoff
|
|
47
|
-
* shape as bootstrap. Set 0 to disable (let main() exit, rely on K8s/PID-1).
|
|
48
|
-
*/
|
|
49
|
-
supervisorRestartBackoffMs: number;
|
|
50
|
-
supervisorRestartBackoffMaxMs: number;
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
function requireEnv(env: NodeJS.ProcessEnv, name: string): string {
|
|
54
|
-
const value = env[name]?.trim();
|
|
55
|
-
if (!value) {
|
|
56
|
-
throw new Error(`Missing required env var: ${name}`);
|
|
57
|
-
}
|
|
58
|
-
return value;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function resolvePath(value: string): string {
|
|
62
|
-
return path.isAbsolute(value) ? value : path.resolve(process.cwd(), value);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function parseMs(value: string | undefined, fallback: number): number {
|
|
66
|
-
if (!value) return fallback;
|
|
67
|
-
const n = Number(value);
|
|
68
|
-
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Like parseMs but allows 0 (operator opt-out). */
|
|
72
|
-
function parseMsAllowZero(value: string | undefined, fallback: number): number {
|
|
73
|
-
if (value === undefined) return fallback;
|
|
74
|
-
const n = Number(value);
|
|
75
|
-
return Number.isFinite(n) && n >= 0 ? n : fallback;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function resolveClaudeDaemonConfig(env: NodeJS.ProcessEnv = process.env): ClaudeDaemonConfig {
|
|
79
|
-
const apiUrl = requireEnv(env, "PRLL_API_URL");
|
|
80
|
-
const apiKey = requireEnv(env, "PRLL_API_KEY");
|
|
81
|
-
if (!apiKey.startsWith("mck_")) {
|
|
82
|
-
// Fatal startup validation: the daemon must never run with an agent or
|
|
83
|
-
// human key because child launch credentials are minted from this bearer.
|
|
84
|
-
throw new Error(
|
|
85
|
-
`PRLL_API_KEY does not look like a Machine bearer (expected prefix "mck_"). ` +
|
|
86
|
-
`Daemon mode requires a machine-scoped key issued via POST /machines/{id}/keys.`,
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
const rootClaudeHome = resolvePath(env.PRLL_CLAUDE_HOME?.trim() || env.HOME || os.homedir());
|
|
90
|
-
const rootStateDir = resolvePath(env.PRLL_CLAUDE_STATE_DIR?.trim() || path.join(rootClaudeHome, ".parall-agent"));
|
|
91
|
-
|
|
92
|
-
return {
|
|
93
|
-
apiUrl,
|
|
94
|
-
apiKey,
|
|
95
|
-
agentBin: env.PRLL_CLAUDE_AGENT_BIN?.trim() || "parall-claude-agent",
|
|
96
|
-
rootStateDir,
|
|
97
|
-
rootClaudeHome,
|
|
98
|
-
wsUrl: env.PRLL_WS_URL?.trim() || undefined,
|
|
99
|
-
pollIntervalMs: parseMs(env.PRLL_DAEMON_POLL_INTERVAL_MS, 30_000),
|
|
100
|
-
heartbeatIntervalMs: parseMs(env.PRLL_DAEMON_HEARTBEAT_INTERVAL_MS, 30_000),
|
|
101
|
-
restartBackoffMs: parseMs(env.PRLL_DAEMON_RESTART_BACKOFF_MS, 5_000),
|
|
102
|
-
restartBackoffMaxMs: parseMs(env.PRLL_DAEMON_RESTART_BACKOFF_MAX_MS, 5 * 60_000),
|
|
103
|
-
bootstrapBackoffMs: parseMsAllowZero(env.PRLL_DAEMON_BOOTSTRAP_BACKOFF_MS, 2_000),
|
|
104
|
-
bootstrapBackoffMaxMs: parseMs(env.PRLL_DAEMON_BOOTSTRAP_BACKOFF_MAX_MS, 60_000),
|
|
105
|
-
supervisorRestartBackoffMs: parseMsAllowZero(env.PRLL_DAEMON_SUPERVISOR_RESTART_BACKOFF_MS, 5_000),
|
|
106
|
-
supervisorRestartBackoffMaxMs: parseMs(env.PRLL_DAEMON_SUPERVISOR_RESTART_BACKOFF_MAX_MS, 5 * 60_000),
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function assertSafeAgentId(agentId: string): string {
|
|
111
|
-
if (!/^[A-Za-z0-9_-]+$/.test(agentId)) {
|
|
112
|
-
throw new Error(`Invalid agentId for filesystem path: ${agentId}`);
|
|
113
|
-
}
|
|
114
|
-
return agentId;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** Per-agent state dir under the shared host volume. */
|
|
118
|
-
export function agentStateDirFor(rootStateDir: string, agentId: string): string {
|
|
119
|
-
return path.join(rootStateDir, "agents", assertSafeAgentId(agentId));
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/** Per-agent HOME dir. Claude Code stores project/session state under
|
|
123
|
-
* `${HOME}/.claude`, so each agent gets its own HOME root while the daemon
|
|
124
|
-
* links shared OAuth credentials into that `.claude` directory. */
|
|
125
|
-
export function agentClaudeHomeFor(rootClaudeHome: string, agentId: string): string {
|
|
126
|
-
return path.join(rootClaudeHome, "agents", assertSafeAgentId(agentId));
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** Shared Claude Code OAuth credential written by server-side runtime auth. */
|
|
130
|
-
export function sharedClaudeCredentialsFileFor(rootClaudeHome: string): string {
|
|
131
|
-
return path.join(rootClaudeHome, ".claude", ".credentials.json");
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** Per-agent credential location inside that agent's isolated HOME. */
|
|
135
|
-
export function agentClaudeCredentialsFileFor(agentClaudeHome: string): string {
|
|
136
|
-
return path.join(agentClaudeHome, ".claude", ".credentials.json");
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/** Per-agent workspace dir where the agent runs git commands. */
|
|
140
|
-
export function agentWorkspaceDirFor(rootStateDir: string, agentId: string): string {
|
|
141
|
-
return path.join(rootStateDir, "agents", assertSafeAgentId(agentId), "workspace");
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export function resolveWsUrl(apiUrl: string, explicitWsUrl?: string): string {
|
|
145
|
-
return explicitWsUrl || `${apiUrl.replace(/\/$/, "").replace(/^http/, "ws")}/ws`;
|
|
146
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { ParallClient } from "@parall/sdk";
|
|
4
|
-
import { resolveClaudeDaemonConfig, resolveWsUrl, type ClaudeDaemonConfig } from "./config.js";
|
|
5
|
-
import { DaemonSupervisor, sleepCancellable, type DaemonLogger } from "./supervisor.js";
|
|
6
|
-
|
|
7
|
-
function createLogger(prefix: string): DaemonLogger {
|
|
8
|
-
return {
|
|
9
|
-
info: (msg: string) => console.log(`[${prefix}] ${msg}`),
|
|
10
|
-
warn: (msg: string) => console.warn(`[${prefix}] ${msg}`),
|
|
11
|
-
error: (msg: string) => console.error(`[${prefix}] ${msg}`),
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function formatError(reason: unknown): string {
|
|
16
|
-
if (reason instanceof Error) {
|
|
17
|
-
return reason.stack ?? reason.message;
|
|
18
|
-
}
|
|
19
|
-
return String(reason);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Outer keepalive: runs `supervisor.run()` in a loop, restarting it with
|
|
24
|
-
* exponential backoff if it rejects. The daemon process only exits when
|
|
25
|
-
* the abort signal fires (SIGINT/SIGTERM) — anything else is treated as
|
|
26
|
-
* a transient failure that we recover from in-process.
|
|
27
|
-
*
|
|
28
|
-
* Why this exists in addition to entrypoint.sh's `while :; do parall-daemon; done`:
|
|
29
|
-
* - In-process restart preserves the `client` (= same TCP/TLS pool) and
|
|
30
|
-
* skips the ~hundreds-of-ms cost of Node startup + module loading per
|
|
31
|
-
* cycle, which matters when the API is flapping.
|
|
32
|
-
* - The shell wrapper is the "process really crashed" backstop —
|
|
33
|
-
* segfault, OOM, uncaughtException, etc. The two layers are
|
|
34
|
-
* complementary: in-process for soft failures, shell for hard failures.
|
|
35
|
-
*
|
|
36
|
-
* Operators can disable the in-process loop by setting
|
|
37
|
-
* PRLL_DAEMON_SUPERVISOR_RESTART_BACKOFF_MS=0 — main() will then exit on
|
|
38
|
-
* any supervisor.run() rejection and rely entirely on the shell wrapper +
|
|
39
|
-
* K8s for restart.
|
|
40
|
-
*/
|
|
41
|
-
async function runForever(
|
|
42
|
-
config: ClaudeDaemonConfig,
|
|
43
|
-
client: ParallClient,
|
|
44
|
-
log: DaemonLogger,
|
|
45
|
-
signal: AbortSignal,
|
|
46
|
-
): Promise<void> {
|
|
47
|
-
let attempt = 0;
|
|
48
|
-
while (!signal.aborted) {
|
|
49
|
-
const supervisor = new DaemonSupervisor(config, client, log);
|
|
50
|
-
try {
|
|
51
|
-
await supervisor.run(signal);
|
|
52
|
-
// Clean exit (signal aborted) — done.
|
|
53
|
-
await supervisor.stop();
|
|
54
|
-
return;
|
|
55
|
-
} catch (err) {
|
|
56
|
-
// supervisor.run() rejected. The supervisor's per-tick handlers
|
|
57
|
-
// already swallow individual failures, so the only paths that reach
|
|
58
|
-
// here are (a) bootstrap fail-fast mode and (b) genuine programmer
|
|
59
|
-
// bugs. Restart anyway — operators rely on this daemon as the only
|
|
60
|
-
// thing keeping per-agent children alive on this host.
|
|
61
|
-
log.error(`supervisor crashed: ${String(err)}`);
|
|
62
|
-
try {
|
|
63
|
-
await supervisor.stop();
|
|
64
|
-
} catch (stopErr) {
|
|
65
|
-
log.warn(`supervisor.stop() after crash threw: ${String(stopErr)}`);
|
|
66
|
-
}
|
|
67
|
-
if (config.supervisorRestartBackoffMs === 0) {
|
|
68
|
-
log.error(
|
|
69
|
-
"supervisor keepalive disabled (PRLL_DAEMON_SUPERVISOR_RESTART_BACKOFF_MS=0) — exiting",
|
|
70
|
-
);
|
|
71
|
-
throw err;
|
|
72
|
-
}
|
|
73
|
-
const delay = Math.min(
|
|
74
|
-
config.supervisorRestartBackoffMs * Math.pow(2, attempt),
|
|
75
|
-
config.supervisorRestartBackoffMaxMs,
|
|
76
|
-
);
|
|
77
|
-
attempt += 1;
|
|
78
|
-
log.warn(`restarting supervisor in ${delay}ms (attempt ${attempt})`);
|
|
79
|
-
const slept = await sleepCancellable(delay, signal);
|
|
80
|
-
if (!slept) return;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async function main(): Promise<void> {
|
|
86
|
-
const config = resolveClaudeDaemonConfig(process.env);
|
|
87
|
-
const log = createLogger("daemon");
|
|
88
|
-
|
|
89
|
-
log.info(
|
|
90
|
-
`boot: api=${config.apiUrl} pollMs=${config.pollIntervalMs} heartbeatMs=${config.heartbeatIntervalMs} agentBin=${config.agentBin}`,
|
|
91
|
-
);
|
|
92
|
-
log.info(
|
|
93
|
-
`keepalive: bootstrapBackoffMs=${config.bootstrapBackoffMs} supervisorRestartBackoffMs=${config.supervisorRestartBackoffMs}`,
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
// The daemon talks to the API as a Machine — the bearer is mck_*.
|
|
97
|
-
// No orgId is configured here; per-agent subprocesses get their
|
|
98
|
-
// own org_id via the spawn env.
|
|
99
|
-
const client = new ParallClient({
|
|
100
|
-
baseUrl: config.apiUrl,
|
|
101
|
-
token: config.apiKey,
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
const abortController = new AbortController();
|
|
105
|
-
const onSignal = (sig: NodeJS.Signals) => {
|
|
106
|
-
log.info(`received ${sig} — initiating shutdown`);
|
|
107
|
-
abortController.abort();
|
|
108
|
-
};
|
|
109
|
-
process.on("SIGINT", () => onSignal("SIGINT"));
|
|
110
|
-
process.on("SIGTERM", () => onSignal("SIGTERM"));
|
|
111
|
-
|
|
112
|
-
// Defensive: unexpected async failures are logged explicitly. Rejections
|
|
113
|
-
// stay in-process so the supervisor loop can recover; uncaught exceptions
|
|
114
|
-
// exit so entrypoint.sh's shell-level keepalive restarts from a clean VM.
|
|
115
|
-
process.on("unhandledRejection", (reason) => {
|
|
116
|
-
log.error(`unhandledRejection: ${formatError(reason)}`);
|
|
117
|
-
});
|
|
118
|
-
process.on("uncaughtException", (err) => {
|
|
119
|
-
log.error(`uncaughtException: ${formatError(err)}`);
|
|
120
|
-
process.exitCode = 1;
|
|
121
|
-
process.exit(1);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
config.wsUrl = resolveWsUrl(config.apiUrl, config.wsUrl);
|
|
125
|
-
|
|
126
|
-
await runForever(config, client, log, abortController.signal);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
main().catch((err) => {
|
|
130
|
-
console.error(`[daemon] fatal: ${formatError(err)}`);
|
|
131
|
-
process.exitCode = 1;
|
|
132
|
-
});
|
package/src/runtimes.ts
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
export interface RuntimeAdapter {
|
|
2
|
-
bin: string;
|
|
3
|
-
buildEnv(baseEnv: NodeJS.ProcessEnv, agentId: string, orgId: string, apiKey: string, dirs: AgentDirs): NodeJS.ProcessEnv;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export interface AgentDirs {
|
|
7
|
-
stateDir: string;
|
|
8
|
-
workspaceDir: string;
|
|
9
|
-
claudeHome: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const claudeCodeAdapter: RuntimeAdapter = {
|
|
13
|
-
bin: "parall-claude-agent",
|
|
14
|
-
buildEnv(baseEnv, agentId, orgId, apiKey, dirs) {
|
|
15
|
-
const env = { ...baseEnv };
|
|
16
|
-
env.PRLL_API_KEY = apiKey;
|
|
17
|
-
env.PRLL_ORG_ID = orgId;
|
|
18
|
-
env.AGENT_ID = agentId;
|
|
19
|
-
env.PRLL_AGENT_ID = agentId;
|
|
20
|
-
env.PRLL_CLAUDE_HOME = dirs.claudeHome;
|
|
21
|
-
env.PRLL_CLAUDE_STATE_DIR = dirs.stateDir;
|
|
22
|
-
env.PRLL_CLAUDE_WORKSPACE_DIR = dirs.workspaceDir;
|
|
23
|
-
if ("ANTHROPIC_AUTH_TOKEN" in env) {
|
|
24
|
-
env.ANTHROPIC_AUTH_TOKEN = apiKey;
|
|
25
|
-
}
|
|
26
|
-
delete env.PRLL_DAEMON_MODE;
|
|
27
|
-
return env;
|
|
28
|
-
},
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const codexAdapter: RuntimeAdapter = {
|
|
32
|
-
bin: "parall-codex-agent",
|
|
33
|
-
buildEnv(baseEnv, agentId, orgId, apiKey, dirs) {
|
|
34
|
-
const env = { ...baseEnv };
|
|
35
|
-
env.PRLL_API_KEY = apiKey;
|
|
36
|
-
env.PRLL_ORG_ID = orgId;
|
|
37
|
-
env.AGENT_ID = agentId;
|
|
38
|
-
env.PRLL_AGENT_ID = agentId;
|
|
39
|
-
env.PRLL_CODEX_STATE_DIR = dirs.stateDir;
|
|
40
|
-
env.PRLL_CODEX_WORKSPACE_DIR = dirs.workspaceDir;
|
|
41
|
-
delete env.PRLL_DAEMON_MODE;
|
|
42
|
-
return env;
|
|
43
|
-
},
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
const defaultAdapter: RuntimeAdapter = {
|
|
47
|
-
bin: "parall-agent",
|
|
48
|
-
buildEnv(baseEnv, agentId, orgId, apiKey, dirs) {
|
|
49
|
-
const env = { ...baseEnv };
|
|
50
|
-
env.PRLL_API_KEY = apiKey;
|
|
51
|
-
env.PRLL_ORG_ID = orgId;
|
|
52
|
-
env.AGENT_ID = agentId;
|
|
53
|
-
env.PRLL_AGENT_ID = agentId;
|
|
54
|
-
env.PRLL_STATE_DIR = dirs.stateDir;
|
|
55
|
-
env.PRLL_WORKSPACE_DIR = dirs.workspaceDir;
|
|
56
|
-
delete env.PRLL_DAEMON_MODE;
|
|
57
|
-
return env;
|
|
58
|
-
},
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
const openclawAdapter: RuntimeAdapter = {
|
|
62
|
-
bin: "parall-openclaw-agent",
|
|
63
|
-
buildEnv(baseEnv, agentId, orgId, apiKey, dirs) {
|
|
64
|
-
const env = { ...baseEnv };
|
|
65
|
-
env.PRLL_API_KEY = apiKey;
|
|
66
|
-
env.PRLL_ORG_ID = orgId;
|
|
67
|
-
env.AGENT_ID = agentId;
|
|
68
|
-
env.PRLL_AGENT_ID = agentId;
|
|
69
|
-
env.PRLL_OPENCLAW_STATE_DIR = dirs.stateDir;
|
|
70
|
-
env.PRLL_OPENCLAW_WORKSPACE_DIR = dirs.workspaceDir;
|
|
71
|
-
env.OPENCLAW_GATEWAY_PORT = env.OPENCLAW_GATEWAY_PORT || "0";
|
|
72
|
-
delete env.PRLL_DAEMON_MODE;
|
|
73
|
-
return env;
|
|
74
|
-
},
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
const RUNTIME_ADAPTERS: Record<string, RuntimeAdapter> = {
|
|
78
|
-
"claude-code": claudeCodeAdapter,
|
|
79
|
-
"codex": codexAdapter,
|
|
80
|
-
"openclaw": openclawAdapter,
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
export function getRuntimeAdapter(runtimeType: string): RuntimeAdapter {
|
|
84
|
-
return RUNTIME_ADAPTERS[runtimeType] ?? defaultAdapter;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export function assertAgentKey(apiKey: string): void {
|
|
88
|
-
if (apiKey.startsWith("mck_")) {
|
|
89
|
-
throw new Error("BUG: machine key leaked to child process — expected agk_, got mck_");
|
|
90
|
-
}
|
|
91
|
-
}
|