@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.
- package/bundle/manifest.json +11 -11
- package/bundle/parall-browser-pod.js +384 -32
- package/bundle/parall-claude-agent.js +426 -51
- package/bundle/parall-codex-agent.js +425 -49
- package/bundle/parall-daemon.js +33336 -31520
- package/dist/browser-pod.d.ts +7 -0
- package/dist/browser-pod.d.ts.map +1 -1
- package/dist/browser-pod.js +73 -3
- 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 +34 -5
- package/dist/clip-runtime/browser-profile-manager.d.ts.map +1 -1
- package/dist/clip-runtime/browser-profile-manager.js +166 -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 -1
- 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 +64 -2
- package/dist/supervisor.d.ts.map +1 -1
- package/dist/supervisor.js +629 -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
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { GatewayLogger } from '@parall/agent-core';
|
|
2
|
+
import type { DaemonUpdater } from './updater.js';
|
|
3
|
+
type TimerHandle = {
|
|
4
|
+
unref?: () => void;
|
|
5
|
+
};
|
|
6
|
+
type TimerApi = {
|
|
7
|
+
setTimeout(callback: () => void, ms: number): TimerHandle;
|
|
8
|
+
clearTimeout(handle: TimerHandle): void;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Canary gate for a freshly self-updated daemon. Centralizes the "is the new
|
|
12
|
+
* version healthy enough to confirm?" policy in ONE place so it isn't smeared
|
|
13
|
+
* across the supervisor's WS handlers.
|
|
14
|
+
*
|
|
15
|
+
* A pending update is confirmed only when BOTH hold:
|
|
16
|
+
* - the control-plane handshake (machine.hello) is seen at least once, AND
|
|
17
|
+
* - the process + supervisor then stay up for `stabilityWindowMs` without the
|
|
18
|
+
* supervisor soft-crashing or the process exiting.
|
|
19
|
+
*
|
|
20
|
+
* Lifecycle:
|
|
21
|
+
* - `onMachineHello()` — the first hello with a pending update arms the
|
|
22
|
+
* stability timer. The timer is process-level (lives in this gate, which the
|
|
23
|
+
* process owns), so it survives WS reconnects; a disconnect does NOT cancel
|
|
24
|
+
* it.
|
|
25
|
+
* - timer elapses with the process still alive → `confirmVersion()`. (A
|
|
26
|
+
* process exit/crash before the timer means it simply never fires.)
|
|
27
|
+
* - `onSupervisorEnded()` — the supervisor instance stopped (clean shutdown OR
|
|
28
|
+
* a runForever-caught soft crash). Cancel the timer so a stale timer can't
|
|
29
|
+
* confirm across supervisor instances; the next supervisor's machine.hello
|
|
30
|
+
* re-arms it. This RESETS the health window but NEVER records a rollback
|
|
31
|
+
* failure — soft crashes are not rollback signals. Rollback `boot_count` is
|
|
32
|
+
* incremented only by process-level unclean exits (crash / kill / power loss
|
|
33
|
+
* / uncaught fatal exit), tracked via the `daemon-running` marker in
|
|
34
|
+
* `index.ts`, not here. A pure in-process soft-crash loop is therefore left
|
|
35
|
+
* to runForever backoff + observability, by design.
|
|
36
|
+
*
|
|
37
|
+
* The updater is unaware of all this: it only exposes `hasPendingActivation()` +
|
|
38
|
+
* `confirmVersion()` and never learns what a "supervisor soft crash" is. Agent
|
|
39
|
+
* child / clip / browser workload failures never reach this gate and never count
|
|
40
|
+
* as rollback failures.
|
|
41
|
+
*/
|
|
42
|
+
export declare class PendingUpdateHealthGate {
|
|
43
|
+
private readonly updater;
|
|
44
|
+
private readonly stabilityWindowMs;
|
|
45
|
+
private readonly log;
|
|
46
|
+
private readonly timers;
|
|
47
|
+
private timer;
|
|
48
|
+
private confirmed;
|
|
49
|
+
constructor(updater: DaemonUpdater, stabilityWindowMs: number, log: GatewayLogger, timers?: TimerApi);
|
|
50
|
+
/**
|
|
51
|
+
* Control-plane handshake. Arms the stability timer on the first hello that
|
|
52
|
+
* sees a pending (unconfirmed) update. No-op once confirmed, while a timer is
|
|
53
|
+
* already running, or when there is nothing pending to confirm.
|
|
54
|
+
*/
|
|
55
|
+
onMachineHello(): void;
|
|
56
|
+
/**
|
|
57
|
+
* The current supervisor instance ended — a clean shutdown or a
|
|
58
|
+
* runForever-caught soft crash. Reset the health window (cancel the pending
|
|
59
|
+
* confirm timer) without recording any rollback failure; the next supervisor
|
|
60
|
+
* re-arms via `onMachineHello`.
|
|
61
|
+
*/
|
|
62
|
+
onSupervisorEnded(): void;
|
|
63
|
+
private confirm;
|
|
64
|
+
}
|
|
65
|
+
export {};
|
|
66
|
+
//# sourceMappingURL=update-health-gate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"update-health-gate.d.ts","sourceRoot":"","sources":["../src/update-health-gate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD,KAAK,WAAW,GAAG;IAAE,KAAK,CAAC,EAAE,MAAM,IAAI,CAAA;CAAE,CAAC;AAE1C,KAAK,QAAQ,GAAG;IACd,UAAU,CAAC,QAAQ,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,WAAW,CAAC;IAC1D,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;CACzC,CAAC;AAOF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,qBAAa,uBAAuB;IAKhC,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAClC,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAPzB,OAAO,CAAC,KAAK,CAA4B;IACzC,OAAO,CAAC,SAAS,CAAS;gBAGP,OAAO,EAAE,aAAa,EACtB,iBAAiB,EAAE,MAAM,EACzB,GAAG,EAAE,aAAa,EAClB,MAAM,GAAE,QAAqB;IAGhD;;;;OAIG;IACH,cAAc,IAAI,IAAI;IActB;;;;;OAKG;IACH,iBAAiB,IAAI,IAAI;IAOzB,OAAO,CAAC,OAAO;CAShB"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const realTimers = {
|
|
2
|
+
setTimeout: (callback, ms) => setTimeout(callback, ms),
|
|
3
|
+
clearTimeout: (handle) => clearTimeout(handle),
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Canary gate for a freshly self-updated daemon. Centralizes the "is the new
|
|
7
|
+
* version healthy enough to confirm?" policy in ONE place so it isn't smeared
|
|
8
|
+
* across the supervisor's WS handlers.
|
|
9
|
+
*
|
|
10
|
+
* A pending update is confirmed only when BOTH hold:
|
|
11
|
+
* - the control-plane handshake (machine.hello) is seen at least once, AND
|
|
12
|
+
* - the process + supervisor then stay up for `stabilityWindowMs` without the
|
|
13
|
+
* supervisor soft-crashing or the process exiting.
|
|
14
|
+
*
|
|
15
|
+
* Lifecycle:
|
|
16
|
+
* - `onMachineHello()` — the first hello with a pending update arms the
|
|
17
|
+
* stability timer. The timer is process-level (lives in this gate, which the
|
|
18
|
+
* process owns), so it survives WS reconnects; a disconnect does NOT cancel
|
|
19
|
+
* it.
|
|
20
|
+
* - timer elapses with the process still alive → `confirmVersion()`. (A
|
|
21
|
+
* process exit/crash before the timer means it simply never fires.)
|
|
22
|
+
* - `onSupervisorEnded()` — the supervisor instance stopped (clean shutdown OR
|
|
23
|
+
* a runForever-caught soft crash). Cancel the timer so a stale timer can't
|
|
24
|
+
* confirm across supervisor instances; the next supervisor's machine.hello
|
|
25
|
+
* re-arms it. This RESETS the health window but NEVER records a rollback
|
|
26
|
+
* failure — soft crashes are not rollback signals. Rollback `boot_count` is
|
|
27
|
+
* incremented only by process-level unclean exits (crash / kill / power loss
|
|
28
|
+
* / uncaught fatal exit), tracked via the `daemon-running` marker in
|
|
29
|
+
* `index.ts`, not here. A pure in-process soft-crash loop is therefore left
|
|
30
|
+
* to runForever backoff + observability, by design.
|
|
31
|
+
*
|
|
32
|
+
* The updater is unaware of all this: it only exposes `hasPendingActivation()` +
|
|
33
|
+
* `confirmVersion()` and never learns what a "supervisor soft crash" is. Agent
|
|
34
|
+
* child / clip / browser workload failures never reach this gate and never count
|
|
35
|
+
* as rollback failures.
|
|
36
|
+
*/
|
|
37
|
+
export class PendingUpdateHealthGate {
|
|
38
|
+
updater;
|
|
39
|
+
stabilityWindowMs;
|
|
40
|
+
log;
|
|
41
|
+
timers;
|
|
42
|
+
timer = null;
|
|
43
|
+
confirmed = false;
|
|
44
|
+
constructor(updater, stabilityWindowMs, log, timers = realTimers) {
|
|
45
|
+
this.updater = updater;
|
|
46
|
+
this.stabilityWindowMs = stabilityWindowMs;
|
|
47
|
+
this.log = log;
|
|
48
|
+
this.timers = timers;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Control-plane handshake. Arms the stability timer on the first hello that
|
|
52
|
+
* sees a pending (unconfirmed) update. No-op once confirmed, while a timer is
|
|
53
|
+
* already running, or when there is nothing pending to confirm.
|
|
54
|
+
*/
|
|
55
|
+
onMachineHello() {
|
|
56
|
+
if (this.confirmed || this.timer)
|
|
57
|
+
return;
|
|
58
|
+
if (!this.updater.hasPendingActivation())
|
|
59
|
+
return;
|
|
60
|
+
if (this.stabilityWindowMs <= 0) {
|
|
61
|
+
this.confirm();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
this.timer = this.timers.setTimeout(() => {
|
|
65
|
+
this.timer = null;
|
|
66
|
+
this.confirm();
|
|
67
|
+
}, this.stabilityWindowMs);
|
|
68
|
+
this.timer.unref?.();
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The current supervisor instance ended — a clean shutdown or a
|
|
72
|
+
* runForever-caught soft crash. Reset the health window (cancel the pending
|
|
73
|
+
* confirm timer) without recording any rollback failure; the next supervisor
|
|
74
|
+
* re-arms via `onMachineHello`.
|
|
75
|
+
*/
|
|
76
|
+
onSupervisorEnded() {
|
|
77
|
+
if (this.timer) {
|
|
78
|
+
this.timers.clearTimeout(this.timer);
|
|
79
|
+
this.timer = null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
confirm() {
|
|
83
|
+
if (this.confirmed)
|
|
84
|
+
return;
|
|
85
|
+
try {
|
|
86
|
+
this.updater.confirmVersion();
|
|
87
|
+
this.confirmed = true;
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
this.log.warn(`update: confirmVersion failed: ${String(err)}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -35,7 +35,8 @@ export declare function verifyManifestSignature(manifest: RemoteManifest, public
|
|
|
35
35
|
*
|
|
36
36
|
* Prerelease ordering follows SemVer 2.0: a version with prerelease has
|
|
37
37
|
* lower precedence than the same version without prerelease. Prerelease
|
|
38
|
-
* identifiers are compared
|
|
38
|
+
* identifiers are compared per SemVer §11.4 when both are present: dot
|
|
39
|
+
* separated, numeric identifiers numerically, non-numeric lexically.
|
|
39
40
|
*/
|
|
40
41
|
export declare function semverCompare(a: string, b: string): number | null;
|
|
41
42
|
//# sourceMappingURL=updater-manifest.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"updater-manifest.d.ts","sourceRoot":"","sources":["../src/updater-manifest.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACpC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;CACrC;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAUlD;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAQ5F;AAED
|
|
1
|
+
{"version":3,"file":"updater-manifest.d.ts","sourceRoot":"","sources":["../src/updater-manifest.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACpC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;CACrC;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAUlD;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAQ5F;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAejE"}
|
package/dist/updater-manifest.js
CHANGED
|
@@ -38,7 +38,8 @@ export function verifyManifestSignature(manifest, publicKey) {
|
|
|
38
38
|
*
|
|
39
39
|
* Prerelease ordering follows SemVer 2.0: a version with prerelease has
|
|
40
40
|
* lower precedence than the same version without prerelease. Prerelease
|
|
41
|
-
* identifiers are compared
|
|
41
|
+
* identifiers are compared per SemVer §11.4 when both are present: dot
|
|
42
|
+
* separated, numeric identifiers numerically, non-numeric lexically.
|
|
42
43
|
*/
|
|
43
44
|
export function semverCompare(a, b) {
|
|
44
45
|
const pa = parseSemver(a);
|
|
@@ -59,12 +60,8 @@ export function semverCompare(a, b) {
|
|
|
59
60
|
return 1;
|
|
60
61
|
if (!pb.pre)
|
|
61
62
|
return -1;
|
|
62
|
-
// Both have prerelease:
|
|
63
|
-
|
|
64
|
-
return -1;
|
|
65
|
-
if (pa.pre > pb.pre)
|
|
66
|
-
return 1;
|
|
67
|
-
return 0;
|
|
63
|
+
// Both have prerelease: compare dot-separated identifiers per SemVer §11.4.
|
|
64
|
+
return comparePrerelease(pa.pre, pb.pre);
|
|
68
65
|
}
|
|
69
66
|
function parseSemver(v) {
|
|
70
67
|
// Split off prerelease at the first hyphen in the patch component.
|
|
@@ -92,3 +89,37 @@ function parseSemver(v) {
|
|
|
92
89
|
return null;
|
|
93
90
|
return { nums: [major, minor, patch], pre };
|
|
94
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Compare two prerelease strings per SemVer 2.0 §11.4: dot-separated identifiers
|
|
94
|
+
* left-to-right — numeric identifiers compared numerically, alphanumeric ones by
|
|
95
|
+
* ASCII order, a numeric identifier always lower precedence than an alphanumeric
|
|
96
|
+
* one, and (all prior identifiers equal) the longer set outranks the shorter.
|
|
97
|
+
*/
|
|
98
|
+
function comparePrerelease(a, b) {
|
|
99
|
+
const aIds = a.split('.');
|
|
100
|
+
const bIds = b.split('.');
|
|
101
|
+
const len = Math.min(aIds.length, bIds.length);
|
|
102
|
+
for (let i = 0; i < len; i++) {
|
|
103
|
+
const ai = aIds[i];
|
|
104
|
+
const bi = bIds[i];
|
|
105
|
+
const aNum = /^\d+$/.test(ai);
|
|
106
|
+
const bNum = /^\d+$/.test(bi);
|
|
107
|
+
if (aNum && bNum) {
|
|
108
|
+
const diff = Number(ai) - Number(bi);
|
|
109
|
+
if (diff !== 0)
|
|
110
|
+
return diff < 0 ? -1 : 1;
|
|
111
|
+
}
|
|
112
|
+
else if (aNum) {
|
|
113
|
+
return -1; // numeric identifiers have lower precedence than alphanumeric
|
|
114
|
+
}
|
|
115
|
+
else if (bNum) {
|
|
116
|
+
return 1;
|
|
117
|
+
}
|
|
118
|
+
else if (ai !== bi) {
|
|
119
|
+
return ai < bi ? -1 : 1;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (aIds.length === bIds.length)
|
|
123
|
+
return 0;
|
|
124
|
+
return aIds.length < bIds.length ? -1 : 1;
|
|
125
|
+
}
|
package/dist/updater.d.ts
CHANGED
|
@@ -24,10 +24,12 @@ export declare class DaemonUpdater {
|
|
|
24
24
|
triggerUpdate(targetVersion: string, mandatory: boolean): Promise<boolean>;
|
|
25
25
|
/**
|
|
26
26
|
* Check if we need to roll back from a failed update.
|
|
27
|
-
* Call on every daemon startup, before bootstrap.
|
|
27
|
+
* Call on every daemon startup, before bootstrap. `uncleanPrevExit` is true
|
|
28
|
+
* when the previous run did not exit cleanly (crash / kill / power loss) — the
|
|
29
|
+
* only signal that counts toward the rollback threshold.
|
|
28
30
|
* Returns true if rollback was performed (caller should exit immediately).
|
|
29
31
|
*/
|
|
30
|
-
checkRollback(): boolean;
|
|
32
|
+
checkRollback(uncleanPrevExit: boolean): boolean;
|
|
31
33
|
/**
|
|
32
34
|
* Confirm the current version after successful health check.
|
|
33
35
|
* Clears pending state so rollback won't trigger.
|
|
@@ -37,6 +39,12 @@ export declare class DaemonUpdater {
|
|
|
37
39
|
* Get the current local manifest version (for heartbeat reporting).
|
|
38
40
|
*/
|
|
39
41
|
getLocalVersion(): string | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* Whether an update has been applied but not yet confirmed healthy. The health
|
|
44
|
+
* gate only arms its stability timer when this is true — a daemon already
|
|
45
|
+
* running a confirmed version needs no canary.
|
|
46
|
+
*/
|
|
47
|
+
hasPendingActivation(): boolean;
|
|
40
48
|
/**
|
|
41
49
|
* Check CDN for available update without downloading.
|
|
42
50
|
* Returns remote version info for CLI --check display.
|
|
@@ -53,6 +61,9 @@ export declare class DaemonUpdater {
|
|
|
53
61
|
loadLocalManifest(): LocalManifest | null;
|
|
54
62
|
private loadUpdateState;
|
|
55
63
|
private saveUpdateState;
|
|
64
|
+
private fsyncDir;
|
|
65
|
+
private isRedirect;
|
|
66
|
+
private resolveRedirectUrl;
|
|
56
67
|
private httpGet;
|
|
57
68
|
private downloadFile;
|
|
58
69
|
private cleanDir;
|
package/dist/updater.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"updater.d.ts","sourceRoot":"","sources":["../src/updater.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAEL,KAAK,aAAa,EAGnB,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"updater.d.ts","sourceRoot":"","sources":["../src/updater.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAEL,KAAK,aAAa,EAGnB,MAAM,uBAAuB,CAAC;AA2B/B,qBAAa,aAAa;IAKtB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,cAAc;IAPjC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,aAAa,CAA+B;gBAGjC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,aAAa,EAClB,cAAc,EAAE,OAAO;IAK1C;;OAEG;IACH,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAU5C,iBAAiB,IAAI,IAAI;IAOzB;;;OAGG;IACG,aAAa,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAa7D;;OAEG;IACG,aAAa,CAAC,aAAa,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAQhF;;;;;;OAMG;IACH,aAAa,CAAC,eAAe,EAAE,OAAO,GAAG,OAAO;IAqDhD;;;OAGG;IACH,cAAc,IAAI,IAAI;IAYtB;;OAEG;IACH,eAAe,IAAI,MAAM,GAAG,SAAS;IAIrC;;;;OAIG;IACH,oBAAoB,IAAI,OAAO;IAM/B;;;OAGG;IACG,cAAc,IAAI,OAAO,CAAC;QAC9B,SAAS,EAAE,OAAO,CAAC;QACnB,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC;YAuBY,QAAQ;IAmJtB,OAAO,CAAC,UAAU;IAmClB,OAAO,CAAC,WAAW;IAUnB,OAAO,CAAC,gBAAgB;IAmBxB,iBAAiB,IAAI,aAAa,GAAG,IAAI;IAUzC,OAAO,CAAC,eAAe;IASvB,OAAO,CAAC,eAAe;IA+BvB,OAAO,CAAC,QAAQ;IAYhB,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,kBAAkB;IAI1B,OAAO,CAAC,OAAO;IAuCf,OAAO,CAAC,YAAY;IA6CpB,OAAO,CAAC,QAAQ;CAKjB"}
|
package/dist/updater.js
CHANGED
|
@@ -5,6 +5,10 @@ import * as http from 'node:http';
|
|
|
5
5
|
import { createHash } from 'node:crypto';
|
|
6
6
|
import { verifyManifestSignature, semverCompare, } from './updater-manifest.js';
|
|
7
7
|
const UPDATE_EXIT_CODE = 42;
|
|
8
|
+
// Number of UNCLEAN boots (crash / kill / power loss) a pending update may
|
|
9
|
+
// accumulate before the daemon reverts to the previous version. Clean restarts
|
|
10
|
+
// and offline-but-healthy restarts don't count — see checkRollback().
|
|
11
|
+
const ROLLBACK_BOOT_THRESHOLD = 3;
|
|
8
12
|
// Ed25519 public key for verifying daemon bundle manifests.
|
|
9
13
|
// The corresponding private key lives in GitHub Actions secrets (DAEMON_BUNDLE_SIGNING_KEY).
|
|
10
14
|
const DEFAULT_SIGNING_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
|
|
@@ -75,19 +79,29 @@ export class DaemonUpdater {
|
|
|
75
79
|
}
|
|
76
80
|
/**
|
|
77
81
|
* Check if we need to roll back from a failed update.
|
|
78
|
-
* Call on every daemon startup, before bootstrap.
|
|
82
|
+
* Call on every daemon startup, before bootstrap. `uncleanPrevExit` is true
|
|
83
|
+
* when the previous run did not exit cleanly (crash / kill / power loss) — the
|
|
84
|
+
* only signal that counts toward the rollback threshold.
|
|
79
85
|
* Returns true if rollback was performed (caller should exit immediately).
|
|
80
86
|
*/
|
|
81
|
-
checkRollback() {
|
|
87
|
+
checkRollback(uncleanPrevExit) {
|
|
82
88
|
const state = this.loadUpdateState();
|
|
83
89
|
const current = this.loadLocalManifest();
|
|
84
90
|
if (!state || !current)
|
|
85
91
|
return false;
|
|
86
92
|
if (!state.pending_version || state.pending_version !== current.version)
|
|
87
93
|
return false;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
94
|
+
// Count this boot toward the rollback threshold ONLY if the previous run
|
|
95
|
+
// ended abnormally (crash / kill / power loss — index.ts left the running
|
|
96
|
+
// marker behind). A clean shutdown or an offline-but-healthy restart must
|
|
97
|
+
// not push a working version toward rollback: a healthy version that simply
|
|
98
|
+
// can't reach the server (so confirmVersion never fires) would otherwise be
|
|
99
|
+
// reverted after a few normal reboots (the offline-churn bug).
|
|
100
|
+
if (uncleanPrevExit) {
|
|
101
|
+
state.boot_count = (state.boot_count ?? 0) + 1;
|
|
102
|
+
this.saveUpdateState(state);
|
|
103
|
+
}
|
|
104
|
+
if ((state.boot_count ?? 0) < ROLLBACK_BOOT_THRESHOLD)
|
|
91
105
|
return false;
|
|
92
106
|
if (!state.previous_version)
|
|
93
107
|
return false;
|
|
@@ -141,6 +155,16 @@ export class DaemonUpdater {
|
|
|
141
155
|
getLocalVersion() {
|
|
142
156
|
return this.loadLocalManifest()?.version;
|
|
143
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Whether an update has been applied but not yet confirmed healthy. The health
|
|
160
|
+
* gate only arms its stability timer when this is true — a daemon already
|
|
161
|
+
* running a confirmed version needs no canary.
|
|
162
|
+
*/
|
|
163
|
+
hasPendingActivation() {
|
|
164
|
+
const state = this.loadUpdateState();
|
|
165
|
+
const current = this.loadLocalManifest();
|
|
166
|
+
return !!state?.pending_version && !!current && state.pending_version === current.version;
|
|
167
|
+
}
|
|
144
168
|
/**
|
|
145
169
|
* Check CDN for available update without downloading.
|
|
146
170
|
* Returns remote version info for CLI --check display.
|
|
@@ -169,6 +193,15 @@ export class DaemonUpdater {
|
|
|
169
193
|
// --- Internal ---
|
|
170
194
|
async doUpdate(targetVersion) {
|
|
171
195
|
const local = this.loadLocalManifest();
|
|
196
|
+
const state = this.loadUpdateState();
|
|
197
|
+
// Keep the canary state machine single-flight: while the currently running
|
|
198
|
+
// bundle is still pending health confirmation, do not stack another update
|
|
199
|
+
// on top of it. Rollback must always target the last confirmed version, not
|
|
200
|
+
// an unconfirmed intermediate bundle.
|
|
201
|
+
if (state?.pending_version && local?.version === state.pending_version) {
|
|
202
|
+
this.log.info(`update: ${local.version} is still pending activation — skipping new update until confirmed`);
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
172
205
|
const manifestUrl = targetVersion
|
|
173
206
|
? `${this.cdnBaseUrl}/${targetVersion}/manifest.json`
|
|
174
207
|
: `${this.cdnBaseUrl}/latest/manifest.json`;
|
|
@@ -195,7 +228,6 @@ export class DaemonUpdater {
|
|
|
195
228
|
return false;
|
|
196
229
|
}
|
|
197
230
|
// Skip versions we already rolled back from
|
|
198
|
-
const state = this.loadUpdateState();
|
|
199
231
|
if (state?.rollback_from === remote.version) {
|
|
200
232
|
this.log.info(`update: skipping ${remote.version} (previously rolled back)`);
|
|
201
233
|
return false;
|
|
@@ -255,17 +287,29 @@ export class DaemonUpdater {
|
|
|
255
287
|
}
|
|
256
288
|
// Write manifest to staging
|
|
257
289
|
fs.writeFileSync(path.join(stagingDir, 'manifest.json'), JSON.stringify(remote, null, 2));
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
//
|
|
290
|
+
// Record the pending update state BEFORE swapping `current`. The state
|
|
291
|
+
// write and the swap are two separate filesystem operations, not one atomic
|
|
292
|
+
// unit, so a crash can land between them — order them so either landing is
|
|
293
|
+
// safe (crash-consistency):
|
|
294
|
+
// - crash after saveUpdateState, before swap → `current` still points at
|
|
295
|
+
// the OLD version while state names the new one as pending. On next boot
|
|
296
|
+
// checkRollback() sees pending_version !== current.version and does
|
|
297
|
+
// nothing; a later check simply re-attempts the update.
|
|
298
|
+
// - crash after swap → `current` is the NEW version AND state already has
|
|
299
|
+
// the matching pending_version, so rollback protection is intact.
|
|
300
|
+
// The reverse order (swap first) could leave `current` on the new version
|
|
301
|
+
// with no matching pending_version, silently voiding rollback.
|
|
302
|
+
const rollbackVersion = state?.confirmed_version ?? local?.version;
|
|
261
303
|
const newState = {
|
|
262
|
-
confirmed_version:
|
|
263
|
-
previous_version:
|
|
304
|
+
confirmed_version: rollbackVersion,
|
|
305
|
+
previous_version: rollbackVersion,
|
|
264
306
|
pending_version: remote.version,
|
|
265
307
|
pending_at: new Date().toISOString(),
|
|
266
308
|
boot_count: 0,
|
|
267
309
|
};
|
|
268
310
|
this.saveUpdateState(newState);
|
|
311
|
+
// Atomic swap — only after the pending state is durably recorded.
|
|
312
|
+
this.atomicSwap(stagingDir, remote.version);
|
|
269
313
|
this.log.info(`update: ${local?.version ?? 'unknown'} → ${remote.version} applied, restarting`);
|
|
270
314
|
return true;
|
|
271
315
|
}
|
|
@@ -350,17 +394,72 @@ export class DaemonUpdater {
|
|
|
350
394
|
}
|
|
351
395
|
saveUpdateState(state) {
|
|
352
396
|
const statePath = path.join(this.bundleDir, 'update-state.json');
|
|
353
|
-
|
|
354
|
-
fs.
|
|
397
|
+
const stateDir = path.dirname(statePath);
|
|
398
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
399
|
+
// Atomic write: stage to a unique temp file in the same directory, then
|
|
400
|
+
// rename(2) over the target (atomic on POSIX). A crash mid-write can never
|
|
401
|
+
// leave a truncated update-state.json — a reader sees either the old or the
|
|
402
|
+
// new content, never a half-written mix.
|
|
403
|
+
const tmpPath = `${statePath}.tmp-${process.pid}-${Date.now()}`;
|
|
404
|
+
let fd;
|
|
405
|
+
try {
|
|
406
|
+
fd = fs.openSync(tmpPath, 'w');
|
|
407
|
+
fs.writeFileSync(fd, JSON.stringify(state, null, 2));
|
|
408
|
+
fs.fsyncSync(fd);
|
|
409
|
+
fs.closeSync(fd);
|
|
410
|
+
fd = undefined;
|
|
411
|
+
fs.renameSync(tmpPath, statePath);
|
|
412
|
+
this.fsyncDir(stateDir);
|
|
413
|
+
}
|
|
414
|
+
catch (err) {
|
|
415
|
+
if (fd !== undefined) {
|
|
416
|
+
try {
|
|
417
|
+
fs.closeSync(fd);
|
|
418
|
+
}
|
|
419
|
+
catch { }
|
|
420
|
+
}
|
|
421
|
+
try {
|
|
422
|
+
fs.unlinkSync(tmpPath);
|
|
423
|
+
}
|
|
424
|
+
catch { }
|
|
425
|
+
throw err;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
fsyncDir(dir) {
|
|
429
|
+
if (process.platform === 'win32')
|
|
430
|
+
return;
|
|
431
|
+
const fd = fs.openSync(dir, 'r');
|
|
432
|
+
try {
|
|
433
|
+
fs.fsyncSync(fd);
|
|
434
|
+
}
|
|
435
|
+
finally {
|
|
436
|
+
fs.closeSync(fd);
|
|
437
|
+
}
|
|
355
438
|
}
|
|
356
439
|
// --- HTTP helpers ---
|
|
440
|
+
isRedirect(statusCode) {
|
|
441
|
+
return statusCode === 301 || statusCode === 302 || statusCode === 307 || statusCode === 308;
|
|
442
|
+
}
|
|
443
|
+
resolveRedirectUrl(location, fromUrl) {
|
|
444
|
+
return new URL(location, fromUrl).toString();
|
|
445
|
+
}
|
|
357
446
|
httpGet(url, maxRedirects = 5) {
|
|
358
447
|
return new Promise((resolve, reject) => {
|
|
359
448
|
const mod = url.startsWith('https') ? https : http;
|
|
360
449
|
const req = mod.get(url, (res) => {
|
|
361
|
-
if (
|
|
450
|
+
if (this.isRedirect(res.statusCode)) {
|
|
362
451
|
if (res.headers.location && maxRedirects > 0) {
|
|
363
|
-
|
|
452
|
+
let redirectUrl;
|
|
453
|
+
try {
|
|
454
|
+
redirectUrl = this.resolveRedirectUrl(res.headers.location, url);
|
|
455
|
+
}
|
|
456
|
+
catch (err) {
|
|
457
|
+
res.resume();
|
|
458
|
+
reject(new Error(`invalid redirect location for ${url}: ${String(err)}`));
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
res.resume();
|
|
462
|
+
this.httpGet(redirectUrl, maxRedirects - 1).then(resolve, reject);
|
|
364
463
|
return;
|
|
365
464
|
}
|
|
366
465
|
res.resume();
|
|
@@ -387,9 +486,19 @@ export class DaemonUpdater {
|
|
|
387
486
|
return new Promise((resolve, reject) => {
|
|
388
487
|
const mod = url.startsWith('https') ? https : http;
|
|
389
488
|
const req = mod.get(url, (res) => {
|
|
390
|
-
if (
|
|
489
|
+
if (this.isRedirect(res.statusCode)) {
|
|
391
490
|
if (res.headers.location && maxRedirects > 0) {
|
|
392
|
-
|
|
491
|
+
let redirectUrl;
|
|
492
|
+
try {
|
|
493
|
+
redirectUrl = this.resolveRedirectUrl(res.headers.location, url);
|
|
494
|
+
}
|
|
495
|
+
catch (err) {
|
|
496
|
+
res.resume();
|
|
497
|
+
reject(new Error(`invalid redirect location for ${url}: ${String(err)}`));
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
res.resume();
|
|
501
|
+
this.downloadFile(redirectUrl, dest, maxRedirects - 1).then(resolve, reject);
|
|
393
502
|
return;
|
|
394
503
|
}
|
|
395
504
|
res.resume();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parall/daemon",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.37.0",
|
|
4
4
|
"description": "Parall local agent runtime — daemon supervisor + bridge runtimes, bundled as standalone JS files",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -32,11 +32,11 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@aws-sdk/client-s3": "3.984.0",
|
|
34
34
|
"@pinixai/bb-browser-pro": "0.15.0",
|
|
35
|
-
"@parall/
|
|
36
|
-
"@parall/
|
|
37
|
-
"@parall/
|
|
38
|
-
"@parall/agent
|
|
39
|
-
"@parall/
|
|
35
|
+
"@parall/agent-core": "1.37.0",
|
|
36
|
+
"@parall/sdk": "1.37.0",
|
|
37
|
+
"@parall/claude-agent": "1.37.0",
|
|
38
|
+
"@parall/codex-agent": "1.37.0",
|
|
39
|
+
"@parall/openclaw-agent": "1.37.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/node": "^22.0.0",
|