@parall/daemon 1.36.1 → 1.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/manifest.json +11 -11
- package/bundle/parall-browser-pod.js +307 -27
- package/bundle/parall-claude-agent.js +363 -50
- package/bundle/parall-codex-agent.js +362 -48
- package/bundle/parall-daemon.js +33473 -31706
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/clip-runtime/browser-daemon-env.d.ts +29 -0
- package/dist/clip-runtime/browser-daemon-env.d.ts.map +1 -0
- package/dist/clip-runtime/browser-daemon-env.js +70 -0
- package/dist/clip-runtime/browser-profile-manager.d.ts +18 -5
- package/dist/clip-runtime/browser-profile-manager.d.ts.map +1 -1
- package/dist/clip-runtime/browser-profile-manager.js +149 -24
- package/dist/clip-runtime/browser-profile-pool.d.ts +250 -0
- package/dist/clip-runtime/browser-profile-pool.d.ts.map +1 -0
- package/dist/clip-runtime/browser-profile-pool.js +581 -0
- package/dist/clip-runtime/index.d.ts +2 -2
- package/dist/clip-runtime/index.d.ts.map +1 -1
- package/dist/clip-runtime/index.js +2 -1
- package/dist/clip-runtime/process-manager.d.ts +5 -3
- package/dist/clip-runtime/process-manager.d.ts.map +1 -1
- package/dist/clip-runtime/subprocess.d.ts +14 -0
- package/dist/clip-runtime/subprocess.d.ts.map +1 -1
- package/dist/clip-runtime/subprocess.js +49 -0
- package/dist/config.d.ts +14 -15
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -26
- package/dist/daemon-main.d.ts +8 -0
- package/dist/daemon-main.d.ts.map +1 -0
- package/dist/daemon-main.js +165 -0
- package/dist/daemon-paths.d.ts +14 -0
- package/dist/daemon-paths.d.ts.map +1 -0
- package/dist/daemon-paths.js +26 -0
- package/dist/daemon-update-mode.d.ts +8 -0
- package/dist/daemon-update-mode.d.ts.map +1 -0
- package/dist/daemon-update-mode.js +18 -0
- package/dist/index.js +41 -167
- package/dist/runtime-bin-resolver.d.ts +4 -0
- package/dist/runtime-bin-resolver.d.ts.map +1 -1
- package/dist/runtime-bin-resolver.js +40 -7
- package/dist/runtime-detector.d.ts +30 -0
- package/dist/runtime-detector.d.ts.map +1 -0
- package/dist/runtime-detector.js +100 -0
- package/dist/supervisor.d.ts +53 -2
- package/dist/supervisor.d.ts.map +1 -1
- package/dist/supervisor.js +604 -67
- package/dist/update-health-gate.d.ts +66 -0
- package/dist/update-health-gate.d.ts.map +1 -0
- package/dist/update-health-gate.js +93 -0
- package/dist/updater-manifest.d.ts +2 -1
- package/dist/updater-manifest.d.ts.map +1 -1
- package/dist/updater-manifest.js +38 -7
- package/dist/updater.d.ts +13 -2
- package/dist/updater.d.ts.map +1 -1
- package/dist/updater.js +126 -17
- package/package.json +6 -6
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ChildProcess } from 'node:child_process';
|
|
1
2
|
/**
|
|
2
3
|
* Child-subprocess plumbing shared by the bb-browser daemon manager
|
|
3
4
|
* (browser-profile-manager.ts) and the bb-viewer streamer
|
|
@@ -8,4 +9,17 @@
|
|
|
8
9
|
export declare function formatErrorForLog(err: unknown): string;
|
|
9
10
|
export declare function findFreePort(): Promise<number>;
|
|
10
11
|
export declare function sleep(ms: number): Promise<void>;
|
|
12
|
+
/**
|
|
13
|
+
* Resolve once `child` has exited. Sends SIGTERM, then escalates to SIGKILL after
|
|
14
|
+
* `graceMs` so a child that ignores TERM can't block teardown forever, and resolves
|
|
15
|
+
* on the `exit` event. A `hardCapMs` backstop resolves anyway if the child never
|
|
16
|
+
* emits `exit` even after SIGKILL (kernel zombie / a missed event), so a caller's
|
|
17
|
+
* queue can't deadlock on a process that is gone for all practical purposes. The
|
|
18
|
+
* bb-browser pool awaits this before reusing a child's per-profile home (reopen /
|
|
19
|
+
* reset / recovery restart) so two Chromiums never share a user-data-dir and a reset
|
|
20
|
+
* never wipes a home a live Chromium still holds.
|
|
21
|
+
*/
|
|
22
|
+
export declare function waitForChildExit(child: ChildProcess, log?: {
|
|
23
|
+
warn(msg: string): void;
|
|
24
|
+
}, graceMs?: number, hardCapMs?: number): Promise<void>;
|
|
11
25
|
//# sourceMappingURL=subprocess.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subprocess.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/subprocess.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"subprocess.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/subprocess.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGvD;;;;;;GAMG;AAEH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAGtD;AAED,wBAAsB,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAgBpD;AAED,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/C;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,YAAY,EACnB,GAAG,CAAC,EAAE;IAAE,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EACjC,OAAO,SAAQ,EACf,SAAS,SAAS,GACjB,OAAO,CAAC,IAAI,CAAC,CAiCf"}
|
|
@@ -31,3 +31,52 @@ export async function findFreePort() {
|
|
|
31
31
|
export function sleep(ms) {
|
|
32
32
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Resolve once `child` has exited. Sends SIGTERM, then escalates to SIGKILL after
|
|
36
|
+
* `graceMs` so a child that ignores TERM can't block teardown forever, and resolves
|
|
37
|
+
* on the `exit` event. A `hardCapMs` backstop resolves anyway if the child never
|
|
38
|
+
* emits `exit` even after SIGKILL (kernel zombie / a missed event), so a caller's
|
|
39
|
+
* queue can't deadlock on a process that is gone for all practical purposes. The
|
|
40
|
+
* bb-browser pool awaits this before reusing a child's per-profile home (reopen /
|
|
41
|
+
* reset / recovery restart) so two Chromiums never share a user-data-dir and a reset
|
|
42
|
+
* never wipes a home a live Chromium still holds.
|
|
43
|
+
*/
|
|
44
|
+
export function waitForChildExit(child, log, graceMs = 5_000, hardCapMs = 15_000) {
|
|
45
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
46
|
+
return Promise.resolve();
|
|
47
|
+
return new Promise((resolve) => {
|
|
48
|
+
let killTimer;
|
|
49
|
+
let capTimer;
|
|
50
|
+
const finish = () => {
|
|
51
|
+
child.removeListener('exit', finish); // no-op if exit already fired; clears the dangling listener on the hard-cap path
|
|
52
|
+
if (killTimer)
|
|
53
|
+
clearTimeout(killTimer);
|
|
54
|
+
if (capTimer)
|
|
55
|
+
clearTimeout(capTimer);
|
|
56
|
+
resolve();
|
|
57
|
+
};
|
|
58
|
+
child.once('exit', finish);
|
|
59
|
+
try {
|
|
60
|
+
child.kill('SIGTERM');
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
/* already gone — the exit listener resolves */
|
|
64
|
+
}
|
|
65
|
+
killTimer = setTimeout(() => {
|
|
66
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
67
|
+
try {
|
|
68
|
+
child.kill('SIGKILL');
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
/* already gone */
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}, graceMs);
|
|
75
|
+
capTimer = setTimeout(() => {
|
|
76
|
+
log?.warn(`child did not exit within ${hardCapMs}ms after SIGKILL; proceeding`);
|
|
77
|
+
finish();
|
|
78
|
+
}, hardCapMs);
|
|
79
|
+
killTimer.unref?.();
|
|
80
|
+
capTimer.unref?.();
|
|
81
|
+
});
|
|
82
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { daemonConfigDir, resolveBundleDir } from './daemon-paths.js';
|
|
2
|
+
export { daemonConfigDir, resolveBundleDir };
|
|
1
3
|
/**
|
|
2
4
|
* Daemon-mode env. The daemon authenticates to the Parall API with an
|
|
3
5
|
* `mck_*` bearer token (PRLL_API_KEY) and supervises N per-agent
|
|
@@ -53,17 +55,15 @@ export type ClaudeDaemonConfig = {
|
|
|
53
55
|
updateIntervalMs: number;
|
|
54
56
|
/** Disable self-update entirely (K8s env auto-disables). */
|
|
55
57
|
updateDisabled: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Stability window for the update canary (PendingUpdateHealthGate): after the
|
|
60
|
+
* control-plane handshake (machine.hello), a freshly self-updated version must
|
|
61
|
+
* stay up this long before it is confirmed healthy and rollback protection
|
|
62
|
+
* clears. Until then a process-level unclean exit still rolls back. Default
|
|
63
|
+
* 60s; 0 = confirm immediately on the first machine.hello.
|
|
64
|
+
*/
|
|
65
|
+
updateConfirmDelayMs: number;
|
|
56
66
|
};
|
|
57
|
-
/**
|
|
58
|
-
* Root dir for all on-disk daemon state: `config.json`, the self-update
|
|
59
|
-
* `bundle/` overlay (versions + current symlink + update-state.json), and
|
|
60
|
-
* bridge-bin resolution all derive from this single path. `PRLL_DAEMON_CONFIG_DIR`
|
|
61
|
-
* overrides it so multiple daemons can coexist on one host without colliding —
|
|
62
|
-
* e.g. a staging desktop install points its launchd agent at
|
|
63
|
-
* `~/.parall-daemon-staging` while prod keeps the default `~/.parall-daemon`.
|
|
64
|
-
* Unset → default (no behavior change for existing single-env installs).
|
|
65
|
-
*/
|
|
66
|
-
export declare function daemonConfigDir(env?: NodeJS.ProcessEnv): string;
|
|
67
67
|
export declare function daemonConfigPath(env?: NodeJS.ProcessEnv): string;
|
|
68
68
|
export declare function resolveClaudeDaemonConfig(env?: NodeJS.ProcessEnv): ClaudeDaemonConfig;
|
|
69
69
|
/** Per-agent state dir under the shared host volume. */
|
|
@@ -79,11 +79,10 @@ export declare function agentClaudeCredentialsFileFor(agentClaudeHome: string):
|
|
|
79
79
|
/** Per-agent workspace dir where the agent runs git commands. */
|
|
80
80
|
export declare function agentWorkspaceDirFor(rootStateDir: string, agentId: string): string;
|
|
81
81
|
/**
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* never the npm global install dir (which may be root-owned or read-only).
|
|
82
|
+
* Whether manual CLI update verifies the Ed25519 bundle signature. Supervised
|
|
83
|
+
* daemon self-update is always fail-closed; this resolver is only a LOCAL-DEV
|
|
84
|
+
* escape hatch for `parall-daemon update` against unsigned local bundles.
|
|
86
85
|
*/
|
|
87
|
-
export declare function
|
|
86
|
+
export declare function resolveUpdateSigningEnabled(env?: NodeJS.ProcessEnv): boolean;
|
|
88
87
|
export declare function resolveWsUrl(apiUrl: string, explicitWsUrl?: string, swimlaneName?: string): string;
|
|
89
88
|
//# sourceMappingURL=config.d.ts.map
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAe,MAAM,mBAAmB,CAAC;AAEnF,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,CAAC;AAE7C;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,2GAA2G;IAC3G,QAAQ,EAAE,MAAM,CAAC;IACjB,yFAAyF;IACzF,YAAY,EAAE,MAAM,CAAC;IACrB,kGAAkG;IAClG,cAAc,EAAE,MAAM,CAAC;IACvB,wEAAwE;IACxE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oDAAoD;IACpD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iFAAiF;IACjF,cAAc,EAAE,MAAM,CAAC;IACvB,iFAAiF;IACjF,mBAAmB,EAAE,MAAM,CAAC;IAC5B,oDAAoD;IACpD,gBAAgB,EAAE,MAAM,CAAC;IACzB,oFAAoF;IACpF,mBAAmB,EAAE,MAAM,CAAC;IAC5B;;;;;OAKG;IACH,kBAAkB,EAAE,MAAM,CAAC;IAC3B,qBAAqB,EAAE,MAAM,CAAC;IAC9B;;;;OAIG;IACH,0BAA0B,EAAE,MAAM,CAAC;IACnC,6BAA6B,EAAE,MAAM,CAAC;IACtC,yEAAyE;IACzE,YAAY,EAAE,MAAM,CAAC;IACrB,gFAAgF;IAChF,gBAAgB,EAAE,MAAM,CAAC;IACzB,4DAA4D;IAC5D,cAAc,EAAE,OAAO,CAAC;IACxB;;;;;;OAMG;IACH,oBAAoB,EAAE,MAAM,CAAC;CAC9B,CAAC;AAuBF,wBAAgB,gBAAgB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAE7E;AAyBD,wBAAgB,yBAAyB,CACvC,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,kBAAkB,CA8DpB;AASD,wDAAwD;AACxD,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAE9E;AAED;;mEAEmE;AACnE,wBAAgB,kBAAkB,CAAC,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAElF;AAED,+EAA+E;AAC/E,wBAAgB,8BAA8B,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAE7E;AAED,uEAAuE;AACvE,wBAAgB,6BAA6B,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,CAE7E;AAED,iEAAiE;AACjE,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAElF;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAEzF;AAED,wBAAgB,YAAY,CAC1B,MAAM,EAAE,MAAM,EACd,aAAa,CAAC,EAAE,MAAM,EACtB,YAAY,CAAC,EAAE,MAAM,GACpB,MAAM,CAMR"}
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as os from 'node:os';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
|
+
import { daemonConfigDir, resolveBundleDir, resolvePath } from './daemon-paths.js';
|
|
5
|
+
export { daemonConfigDir, resolveBundleDir };
|
|
4
6
|
function requireEnv(env, name) {
|
|
5
7
|
const value = env[name]?.trim();
|
|
6
8
|
if (!value) {
|
|
@@ -8,9 +10,6 @@ function requireEnv(env, name) {
|
|
|
8
10
|
}
|
|
9
11
|
return value;
|
|
10
12
|
}
|
|
11
|
-
function resolvePath(value) {
|
|
12
|
-
return path.isAbsolute(value) ? value : path.resolve(process.cwd(), value);
|
|
13
|
-
}
|
|
14
13
|
function parseMs(value, fallback) {
|
|
15
14
|
if (!value)
|
|
16
15
|
return fallback;
|
|
@@ -24,21 +23,6 @@ function parseMsAllowZero(value, fallback) {
|
|
|
24
23
|
const n = Number(value);
|
|
25
24
|
return Number.isFinite(n) && n >= 0 ? n : fallback;
|
|
26
25
|
}
|
|
27
|
-
/**
|
|
28
|
-
* Root dir for all on-disk daemon state: `config.json`, the self-update
|
|
29
|
-
* `bundle/` overlay (versions + current symlink + update-state.json), and
|
|
30
|
-
* bridge-bin resolution all derive from this single path. `PRLL_DAEMON_CONFIG_DIR`
|
|
31
|
-
* overrides it so multiple daemons can coexist on one host without colliding —
|
|
32
|
-
* e.g. a staging desktop install points its launchd agent at
|
|
33
|
-
* `~/.parall-daemon-staging` while prod keeps the default `~/.parall-daemon`.
|
|
34
|
-
* Unset → default (no behavior change for existing single-env installs).
|
|
35
|
-
*/
|
|
36
|
-
export function daemonConfigDir(env = process.env) {
|
|
37
|
-
const override = env.PRLL_DAEMON_CONFIG_DIR?.trim();
|
|
38
|
-
if (override)
|
|
39
|
-
return resolvePath(override);
|
|
40
|
-
return path.join(env.HOME || os.homedir(), '.parall-daemon');
|
|
41
|
-
}
|
|
42
26
|
export function daemonConfigPath(env = process.env) {
|
|
43
27
|
return path.join(daemonConfigDir(env), 'config.json');
|
|
44
28
|
}
|
|
@@ -109,6 +93,7 @@ export function resolveClaudeDaemonConfig(env = process.env) {
|
|
|
109
93
|
: 'https://releases.parall.com/daemon/production'),
|
|
110
94
|
updateIntervalMs: parseMsAllowZero(env.PRLL_DAEMON_UPDATE_INTERVAL_MS, 6 * 60 * 60_000),
|
|
111
95
|
updateDisabled: env.PRLL_DAEMON_UPDATE_DISABLED === 'true' || !!env.KUBERNETES_SERVICE_HOST,
|
|
96
|
+
updateConfirmDelayMs: parseMsAllowZero(env.PRLL_DAEMON_UPDATE_CONFIRM_DELAY_MS?.trim() || undefined, 60_000),
|
|
112
97
|
};
|
|
113
98
|
}
|
|
114
99
|
function assertSafeAgentId(agentId) {
|
|
@@ -140,15 +125,12 @@ export function agentWorkspaceDirFor(rootStateDir, agentId) {
|
|
|
140
125
|
return path.join(rootStateDir, 'agents', assertSafeAgentId(agentId), 'workspace');
|
|
141
126
|
}
|
|
142
127
|
/**
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
* never the npm global install dir (which may be root-owned or read-only).
|
|
128
|
+
* Whether manual CLI update verifies the Ed25519 bundle signature. Supervised
|
|
129
|
+
* daemon self-update is always fail-closed; this resolver is only a LOCAL-DEV
|
|
130
|
+
* escape hatch for `parall-daemon update` against unsigned local bundles.
|
|
147
131
|
*/
|
|
148
|
-
export function
|
|
149
|
-
|
|
150
|
-
return resolvePath(env.PRLL_DAEMON_BUNDLE_DIR);
|
|
151
|
-
return path.join(daemonConfigDir(env), 'bundle');
|
|
132
|
+
export function resolveUpdateSigningEnabled(env = process.env) {
|
|
133
|
+
return env.PRLL_DAEMON_SIGNING_DISABLED !== '1';
|
|
152
134
|
}
|
|
153
135
|
export function resolveWsUrl(apiUrl, explicitWsUrl, swimlaneName) {
|
|
154
136
|
const base = explicitWsUrl || `${apiUrl.replace(/\/$/, '').replace(/^http/, 'ws')}/ws`;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
export type DaemonBootstrapState = {
|
|
3
|
+
lifecycleMarkerEnabled: boolean;
|
|
4
|
+
runningMarker: string;
|
|
5
|
+
uncleanPrevExit: boolean;
|
|
6
|
+
};
|
|
7
|
+
export declare function runDaemonEntry(bootstrap: DaemonBootstrapState, args?: string[]): Promise<void>;
|
|
8
|
+
//# sourceMappingURL=daemon-main.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"daemon-main.d.ts","sourceRoot":"","sources":["../src/daemon-main.ts"],"names":[],"mappings":";AAyBA,MAAM,MAAM,oBAAoB,GAAG;IACjC,sBAAsB,EAAE,OAAO,CAAC;IAChC,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,OAAO,CAAC;CAC1B,CAAC;AA4KF,wBAAsB,cAAc,CAClC,SAAS,EAAE,oBAAoB,EAC/B,IAAI,WAAwB,GAC3B,OAAO,CAAC,IAAI,CAAC,CASf"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createLogger, createOtelLogger, initAgentTelemetry, } from '@parall/agent-core';
|
|
3
|
+
import { ParallClient } from '@parall/sdk';
|
|
4
|
+
import { runCLI } from './cli.js';
|
|
5
|
+
import { resolveBundleDir, resolveClaudeDaemonConfig, resolveWsUrl, } from './config.js';
|
|
6
|
+
import { isSelfUpdateManaged } from './daemon-update-mode.js';
|
|
7
|
+
import { DaemonSupervisor, sleepCancellable } from './supervisor.js';
|
|
8
|
+
import { DaemonUpdater } from './updater.js';
|
|
9
|
+
import { PendingUpdateHealthGate } from './update-health-gate.js';
|
|
10
|
+
const UPDATE_EXIT_CODE = 42;
|
|
11
|
+
let log = createLogger('daemon');
|
|
12
|
+
function formatError(reason) {
|
|
13
|
+
if (reason instanceof Error) {
|
|
14
|
+
return reason.stack ?? reason.message;
|
|
15
|
+
}
|
|
16
|
+
return String(reason);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Outer keepalive: runs `supervisor.run()` in a loop, restarting it with
|
|
20
|
+
* exponential backoff if it rejects. The daemon process only exits when
|
|
21
|
+
* the abort signal fires (SIGINT/SIGTERM) — anything else is treated as
|
|
22
|
+
* a transient failure that we recover from in-process.
|
|
23
|
+
*
|
|
24
|
+
* Why this exists in addition to entrypoint.sh's `while :; do parall-daemon; done`:
|
|
25
|
+
* - In-process restart preserves the `client` (= same TCP/TLS pool) and
|
|
26
|
+
* skips the ~hundreds-of-ms cost of Node startup + module loading per
|
|
27
|
+
* cycle, which matters when the API is flapping.
|
|
28
|
+
* - The shell wrapper is the "process really crashed" backstop —
|
|
29
|
+
* segfault, OOM, uncaughtException, etc. The two layers are
|
|
30
|
+
* complementary: in-process for soft failures, shell for hard failures.
|
|
31
|
+
*
|
|
32
|
+
* Operators can disable the in-process loop by setting
|
|
33
|
+
* PRLL_DAEMON_SUPERVISOR_RESTART_BACKOFF_MS=0 — main() will then exit on
|
|
34
|
+
* any supervisor.run() rejection and rely entirely on the shell wrapper +
|
|
35
|
+
* K8s for restart.
|
|
36
|
+
*/
|
|
37
|
+
async function runForever(config, client, log, signal, updater, gate) {
|
|
38
|
+
let attempt = 0;
|
|
39
|
+
while (!signal.aborted) {
|
|
40
|
+
const supervisor = new DaemonSupervisor(config, client, log);
|
|
41
|
+
if (updater)
|
|
42
|
+
supervisor.setUpdater(updater);
|
|
43
|
+
if (gate)
|
|
44
|
+
supervisor.setHealthGate(gate);
|
|
45
|
+
try {
|
|
46
|
+
await supervisor.run(signal);
|
|
47
|
+
// Clean exit (signal aborted) — done.
|
|
48
|
+
await supervisor.stop();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
// supervisor.run() rejected. The supervisor's per-tick handlers
|
|
53
|
+
// already swallow individual failures, so the only paths that reach
|
|
54
|
+
// here are (a) bootstrap fail-fast mode and (b) genuine programmer
|
|
55
|
+
// bugs. Restart anyway — operators rely on this daemon as the only
|
|
56
|
+
// thing keeping per-agent children alive on this host.
|
|
57
|
+
log.error(`supervisor crashed: ${String(err)}`);
|
|
58
|
+
try {
|
|
59
|
+
await supervisor.stop();
|
|
60
|
+
}
|
|
61
|
+
catch (stopErr) {
|
|
62
|
+
log.warn(`supervisor.stop() after crash threw: ${String(stopErr)}`);
|
|
63
|
+
}
|
|
64
|
+
if (config.supervisorRestartBackoffMs === 0) {
|
|
65
|
+
log.error('supervisor keepalive disabled (PRLL_DAEMON_SUPERVISOR_RESTART_BACKOFF_MS=0) — exiting');
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
68
|
+
const delay = Math.min(config.supervisorRestartBackoffMs * 2 ** attempt, config.supervisorRestartBackoffMaxMs);
|
|
69
|
+
attempt += 1;
|
|
70
|
+
log.warn(`restarting supervisor in ${delay}ms (attempt ${attempt})`);
|
|
71
|
+
const slept = await sleepCancellable(delay, signal);
|
|
72
|
+
if (!slept)
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function main(bootstrap) {
|
|
78
|
+
const config = resolveClaudeDaemonConfig(process.env);
|
|
79
|
+
const telemetry = await initAgentTelemetry('parall-daemon', 'daemon');
|
|
80
|
+
log = createOtelLogger('daemon', 'daemon');
|
|
81
|
+
log.info(`boot: api=${config.apiUrl} pollMs=${config.pollIntervalMs} heartbeatMs=${config.heartbeatIntervalMs} agentBin=${config.agentBin}`);
|
|
82
|
+
log.info(`keepalive: bootstrapBackoffMs=${config.bootstrapBackoffMs} supervisorRestartBackoffMs=${config.supervisorRestartBackoffMs}`);
|
|
83
|
+
// --- Self-update: rollback check (before any network calls) ---
|
|
84
|
+
// Only run when a service manager supervises us (see isSelfUpdateManaged):
|
|
85
|
+
// a bare `npx` foreground run can't load the overlay and won't be restarted
|
|
86
|
+
// after exit(42), so self-update there would just kill the daemon.
|
|
87
|
+
let updater = null;
|
|
88
|
+
let healthGate = null;
|
|
89
|
+
const bundleDir = resolveBundleDir(process.env);
|
|
90
|
+
const selfUpdateEnabled = !config.updateDisabled && isSelfUpdateManaged(bundleDir, process.env);
|
|
91
|
+
if (selfUpdateEnabled) {
|
|
92
|
+
const signingEnabled = true;
|
|
93
|
+
updater = new DaemonUpdater(bundleDir, config.updateCdnUrl, log, signingEnabled);
|
|
94
|
+
// Process-level canary gate: machine.hello arms a stability window; the
|
|
95
|
+
// version is confirmed only if the process stays up through it. Owned here
|
|
96
|
+
// (not in the supervisor) so it survives supervisor in-process restarts.
|
|
97
|
+
healthGate = new PendingUpdateHealthGate(updater, config.updateConfirmDelayMs, log);
|
|
98
|
+
// The thin bootstrap writes the process-level running marker before loading
|
|
99
|
+
// this app module. That keeps rollback accounting outside the replaceable
|
|
100
|
+
// daemon app startup path while still letting updater.ts stay mechanism-only.
|
|
101
|
+
if (updater.checkRollback(bootstrap.uncleanPrevExit)) {
|
|
102
|
+
log.info('rollback applied — exiting for service manager restart');
|
|
103
|
+
await telemetry.shutdown();
|
|
104
|
+
process.exit(UPDATE_EXIT_CODE);
|
|
105
|
+
}
|
|
106
|
+
log.info(`update: bundleDir=${bundleDir} cdn=${config.updateCdnUrl} interval=${config.updateIntervalMs}ms signing=${signingEnabled}`);
|
|
107
|
+
}
|
|
108
|
+
else if (!config.updateDisabled) {
|
|
109
|
+
log.info('update: skipped (foreground mode) — run `parall-daemon service install` for managed auto-update');
|
|
110
|
+
}
|
|
111
|
+
// The daemon talks to the API as a Machine — the bearer is mck_*.
|
|
112
|
+
// No orgId is configured here; per-agent subprocesses get their
|
|
113
|
+
// own org_id via the spawn env.
|
|
114
|
+
const client = new ParallClient({
|
|
115
|
+
baseUrl: config.apiUrl,
|
|
116
|
+
token: config.apiKey,
|
|
117
|
+
swimlaneName: config.swimlaneName,
|
|
118
|
+
});
|
|
119
|
+
const abortController = new AbortController();
|
|
120
|
+
const onSignal = (sig) => {
|
|
121
|
+
log.info(`received ${sig} — initiating shutdown`);
|
|
122
|
+
abortController.abort();
|
|
123
|
+
};
|
|
124
|
+
process.on('SIGINT', () => onSignal('SIGINT'));
|
|
125
|
+
process.on('SIGTERM', () => onSignal('SIGTERM'));
|
|
126
|
+
// Defensive: unexpected async failures are logged explicitly. Rejections
|
|
127
|
+
// stay in-process so the supervisor loop can recover; uncaught exceptions
|
|
128
|
+
// exit so entrypoint.sh's shell-level keepalive restarts from a clean VM.
|
|
129
|
+
process.on('unhandledRejection', (reason) => {
|
|
130
|
+
log.error(`unhandledRejection: ${formatError(reason)}`);
|
|
131
|
+
});
|
|
132
|
+
process.on('uncaughtException', (err) => {
|
|
133
|
+
log.error(`uncaughtException: ${formatError(err)}`);
|
|
134
|
+
process.exitCode = 1;
|
|
135
|
+
process.exit(1);
|
|
136
|
+
});
|
|
137
|
+
config.wsUrl = resolveWsUrl(config.apiUrl, config.wsUrl, config.swimlaneName);
|
|
138
|
+
// --- Self-update: boot check + periodic timer ---
|
|
139
|
+
if (updater) {
|
|
140
|
+
const applied = await updater.checkAndApply().catch((err) => {
|
|
141
|
+
log.warn(`boot update check failed: ${String(err)}`);
|
|
142
|
+
return false;
|
|
143
|
+
});
|
|
144
|
+
if (applied) {
|
|
145
|
+
log.info('boot update applied — exiting for restart');
|
|
146
|
+
await telemetry.shutdown();
|
|
147
|
+
process.exit(UPDATE_EXIT_CODE);
|
|
148
|
+
}
|
|
149
|
+
updater.startPeriodicCheck(config.updateIntervalMs);
|
|
150
|
+
}
|
|
151
|
+
await runForever(config, client, log, abortController.signal, updater, healthGate);
|
|
152
|
+
await telemetry.shutdown();
|
|
153
|
+
}
|
|
154
|
+
export async function runDaemonEntry(bootstrap, args = process.argv.slice(2)) {
|
|
155
|
+
try {
|
|
156
|
+
const result = await runCLI(args);
|
|
157
|
+
if (result === 'handled')
|
|
158
|
+
return;
|
|
159
|
+
await main(bootstrap);
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
log.error(`fatal: ${formatError(err)}`);
|
|
163
|
+
process.exitCode = 1;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare function resolvePath(value: string): string;
|
|
2
|
+
/**
|
|
3
|
+
* Root directory for daemon-owned on-disk state: config.json, self-update
|
|
4
|
+
* bundles, and related process markers.
|
|
5
|
+
*/
|
|
6
|
+
export declare function daemonConfigDir(env?: NodeJS.ProcessEnv): string;
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the bundle directory for self-update storage.
|
|
9
|
+
* Desktop: ~/Library/Application Support/Parall/daemon/ (set via env)
|
|
10
|
+
* Default: ~/.parall-daemon/bundle/ — always a writable overlay directory,
|
|
11
|
+
* never the npm global install dir (which may be root-owned or read-only).
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveBundleDir(env?: NodeJS.ProcessEnv): string;
|
|
14
|
+
//# sourceMappingURL=daemon-paths.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"daemon-paths.d.ts","sourceRoot":"","sources":["../src/daemon-paths.ts"],"names":[],"mappings":"AAGA,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAI5E;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAG7E"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as os from 'node:os';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
export function resolvePath(value) {
|
|
4
|
+
return path.isAbsolute(value) ? value : path.resolve(process.cwd(), value);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Root directory for daemon-owned on-disk state: config.json, self-update
|
|
8
|
+
* bundles, and related process markers.
|
|
9
|
+
*/
|
|
10
|
+
export function daemonConfigDir(env = process.env) {
|
|
11
|
+
const override = env.PRLL_DAEMON_CONFIG_DIR?.trim();
|
|
12
|
+
if (override)
|
|
13
|
+
return resolvePath(override);
|
|
14
|
+
return path.join(env.HOME || os.homedir(), '.parall-daemon');
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Resolve the bundle directory for self-update storage.
|
|
18
|
+
* Desktop: ~/Library/Application Support/Parall/daemon/ (set via env)
|
|
19
|
+
* Default: ~/.parall-daemon/bundle/ — always a writable overlay directory,
|
|
20
|
+
* never the npm global install dir (which may be root-owned or read-only).
|
|
21
|
+
*/
|
|
22
|
+
export function resolveBundleDir(env = process.env) {
|
|
23
|
+
if (env.PRLL_DAEMON_BUNDLE_DIR)
|
|
24
|
+
return resolvePath(env.PRLL_DAEMON_BUNDLE_DIR);
|
|
25
|
+
return path.join(daemonConfigDir(env), 'bundle');
|
|
26
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function isSelfUpdateDisabledByEnv(env?: NodeJS.ProcessEnv): boolean;
|
|
2
|
+
/**
|
|
3
|
+
* Self-update should run only when a service manager supervises the daemon:
|
|
4
|
+
* PRLL_DAEMON_MANAGED is injected by service install, while an overlay entry
|
|
5
|
+
* path covers service installs predating that marker.
|
|
6
|
+
*/
|
|
7
|
+
export declare function isSelfUpdateManaged(bundleDir: string, env?: NodeJS.ProcessEnv, entry?: string): boolean;
|
|
8
|
+
//# sourceMappingURL=daemon-update-mode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"daemon-update-mode.d.ts","sourceRoot":"","sources":["../src/daemon-update-mode.ts"],"names":[],"mappings":"AAEA,wBAAgB,yBAAyB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAEvF;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,MAAM,EACjB,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,KAAK,SAAkB,GACtB,OAAO,CAOT"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
export function isSelfUpdateDisabledByEnv(env = process.env) {
|
|
3
|
+
return env.PRLL_DAEMON_UPDATE_DISABLED === 'true' || !!env.KUBERNETES_SERVICE_HOST;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Self-update should run only when a service manager supervises the daemon:
|
|
7
|
+
* PRLL_DAEMON_MANAGED is injected by service install, while an overlay entry
|
|
8
|
+
* path covers service installs predating that marker.
|
|
9
|
+
*/
|
|
10
|
+
export function isSelfUpdateManaged(bundleDir, env = process.env, entry = process.argv[1]) {
|
|
11
|
+
if (env.PRLL_DAEMON_MANAGED === '1' || env.PRLL_DAEMON_MANAGED === 'true')
|
|
12
|
+
return true;
|
|
13
|
+
if (!entry)
|
|
14
|
+
return false;
|
|
15
|
+
const resolvedEntry = path.resolve(entry);
|
|
16
|
+
const resolvedBundle = path.resolve(bundleDir);
|
|
17
|
+
return resolvedEntry === resolvedBundle || resolvedEntry.startsWith(resolvedBundle + path.sep);
|
|
18
|
+
}
|