@parall/daemon 1.36.0 → 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.
Files changed (59) hide show
  1. package/bundle/manifest.json +11 -11
  2. package/bundle/parall-browser-pod.js +384 -32
  3. package/bundle/parall-claude-agent.js +426 -51
  4. package/bundle/parall-codex-agent.js +425 -49
  5. package/bundle/parall-daemon.js +33336 -31520
  6. package/dist/browser-pod.d.ts +7 -0
  7. package/dist/browser-pod.d.ts.map +1 -1
  8. package/dist/browser-pod.js +73 -3
  9. package/dist/cli.d.ts.map +1 -1
  10. package/dist/cli.js +2 -2
  11. package/dist/clip-runtime/browser-daemon-env.d.ts +29 -0
  12. package/dist/clip-runtime/browser-daemon-env.d.ts.map +1 -0
  13. package/dist/clip-runtime/browser-daemon-env.js +70 -0
  14. package/dist/clip-runtime/browser-profile-manager.d.ts +34 -5
  15. package/dist/clip-runtime/browser-profile-manager.d.ts.map +1 -1
  16. package/dist/clip-runtime/browser-profile-manager.js +166 -24
  17. package/dist/clip-runtime/browser-profile-pool.d.ts +250 -0
  18. package/dist/clip-runtime/browser-profile-pool.d.ts.map +1 -0
  19. package/dist/clip-runtime/browser-profile-pool.js +581 -0
  20. package/dist/clip-runtime/index.d.ts +2 -1
  21. package/dist/clip-runtime/index.d.ts.map +1 -1
  22. package/dist/clip-runtime/index.js +2 -1
  23. package/dist/clip-runtime/process-manager.d.ts +5 -3
  24. package/dist/clip-runtime/process-manager.d.ts.map +1 -1
  25. package/dist/clip-runtime/subprocess.d.ts +14 -0
  26. package/dist/clip-runtime/subprocess.d.ts.map +1 -1
  27. package/dist/clip-runtime/subprocess.js +49 -0
  28. package/dist/config.d.ts +14 -15
  29. package/dist/config.d.ts.map +1 -1
  30. package/dist/config.js +8 -26
  31. package/dist/daemon-main.d.ts +8 -0
  32. package/dist/daemon-main.d.ts.map +1 -0
  33. package/dist/daemon-main.js +165 -0
  34. package/dist/daemon-paths.d.ts +14 -0
  35. package/dist/daemon-paths.d.ts.map +1 -0
  36. package/dist/daemon-paths.js +26 -0
  37. package/dist/daemon-update-mode.d.ts +8 -0
  38. package/dist/daemon-update-mode.d.ts.map +1 -0
  39. package/dist/daemon-update-mode.js +18 -0
  40. package/dist/index.js +41 -167
  41. package/dist/runtime-bin-resolver.d.ts +4 -0
  42. package/dist/runtime-bin-resolver.d.ts.map +1 -1
  43. package/dist/runtime-bin-resolver.js +40 -7
  44. package/dist/runtime-detector.d.ts +30 -0
  45. package/dist/runtime-detector.d.ts.map +1 -0
  46. package/dist/runtime-detector.js +100 -0
  47. package/dist/supervisor.d.ts +64 -2
  48. package/dist/supervisor.d.ts.map +1 -1
  49. package/dist/supervisor.js +629 -67
  50. package/dist/update-health-gate.d.ts +66 -0
  51. package/dist/update-health-gate.d.ts.map +1 -0
  52. package/dist/update-health-gate.js +93 -0
  53. package/dist/updater-manifest.d.ts +2 -1
  54. package/dist/updater-manifest.d.ts.map +1 -1
  55. package/dist/updater-manifest.js +38 -7
  56. package/dist/updater.d.ts +13 -2
  57. package/dist/updater.d.ts.map +1 -1
  58. package/dist/updater.js +126 -17
  59. 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":"AAEA;;;;;;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"}
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
- * Resolve the bundle directory for self-update storage.
83
- * Desktop: ~/Library/Application Support/Parall/daemon/ (set via env)
84
- * Default: ~/.parall-daemon/bundle/ always a writable overlay directory,
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 resolveBundleDir(env?: NodeJS.ProcessEnv): string;
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
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;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;CACzB,CAAC;AA2BF;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAI5E;AAED,wBAAgB,gBAAgB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAE7E;AAyBD,wBAAgB,yBAAyB,CACvC,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,kBAAkB,CA0DpB;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;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAG7E;AAED,wBAAgB,YAAY,CAC1B,MAAM,EAAE,MAAM,EACd,aAAa,CAAC,EAAE,MAAM,EACtB,YAAY,CAAC,EAAE,MAAM,GACpB,MAAM,CAMR"}
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
- * Resolve the bundle directory for self-update storage.
144
- * Desktop: ~/Library/Application Support/Parall/daemon/ (set via env)
145
- * Default: ~/.parall-daemon/bundle/ always a writable overlay directory,
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 resolveBundleDir(env = process.env) {
149
- if (env.PRLL_DAEMON_BUNDLE_DIR)
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
+ }