@parall/daemon 1.44.0 → 1.46.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 +15 -15
- package/bundle/parall-browser-pod.js +29726 -375
- package/bundle/parall-channel-exec.js +2 -0
- package/bundle/parall-claude-agent.js +26446 -384
- package/bundle/parall-codex-agent.js +27490 -1549
- package/bundle/parall-daemon.js +31558 -1617
- package/bundle/parall-openclaw-agent.js +1 -0
- package/dist/browser-pod.d.ts +23 -1
- package/dist/browser-pod.d.ts.map +1 -1
- package/dist/browser-pod.js +104 -34
- package/dist/browser-profile-reconcile.d.ts +21 -0
- package/dist/browser-profile-reconcile.d.ts.map +1 -0
- package/dist/browser-profile-reconcile.js +188 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +229 -2
- package/dist/clip-runtime/browser-cdp.d.ts +40 -0
- package/dist/clip-runtime/browser-cdp.d.ts.map +1 -0
- package/dist/clip-runtime/browser-cdp.js +218 -0
- package/dist/clip-runtime/browser-profile-manager.d.ts +97 -24
- package/dist/clip-runtime/browser-profile-manager.d.ts.map +1 -1
- package/dist/clip-runtime/browser-profile-manager.js +316 -182
- package/dist/clip-runtime/browser-profile-pool.d.ts +3 -0
- package/dist/clip-runtime/browser-profile-pool.d.ts.map +1 -1
- package/dist/clip-runtime/browser-profile-pool.js +18 -1
- package/dist/clip-runtime/browser-proxy-reconcile.d.ts +77 -0
- package/dist/clip-runtime/browser-proxy-reconcile.d.ts.map +1 -0
- package/dist/clip-runtime/browser-proxy-reconcile.js +139 -0
- package/dist/clip-runtime/browser-proxy-state.d.ts +55 -0
- package/dist/clip-runtime/browser-proxy-state.d.ts.map +1 -0
- package/dist/clip-runtime/browser-proxy-state.js +149 -0
- package/dist/clip-runtime/browser-quiescence.d.ts +71 -0
- package/dist/clip-runtime/browser-quiescence.d.ts.map +1 -0
- package/dist/clip-runtime/browser-quiescence.js +136 -0
- package/dist/clip-runtime/browser-readiness.d.ts +64 -0
- package/dist/clip-runtime/browser-readiness.d.ts.map +1 -0
- package/dist/clip-runtime/browser-readiness.js +161 -0
- package/dist/clip-runtime/browser-state-store.d.ts +13 -2
- package/dist/clip-runtime/browser-state-store.d.ts.map +1 -1
- package/dist/clip-runtime/browser-state-store.js +15 -6
- package/dist/clip-runtime/browser-target-registry.d.ts +143 -0
- package/dist/clip-runtime/browser-target-registry.d.ts.map +1 -0
- package/dist/clip-runtime/browser-target-registry.js +297 -0
- package/dist/clip-runtime/browser-viewer-streamer.d.ts +13 -14
- package/dist/clip-runtime/browser-viewer-streamer.d.ts.map +1 -1
- package/dist/clip-runtime/browser-viewer-streamer.js +11 -63
- package/dist/config.d.ts.map +1 -1
- package/dist/daemon-main.d.ts.map +1 -1
- package/dist/daemon-main.js +3 -1
- package/dist/runtime-bin-resolver.d.ts +7 -1
- package/dist/runtime-bin-resolver.d.ts.map +1 -1
- package/dist/runtime-bin-resolver.js +57 -22
- package/dist/runtimes.d.ts +15 -4
- package/dist/runtimes.d.ts.map +1 -1
- package/dist/runtimes.js +60 -5
- package/dist/supervisor.d.ts +6 -0
- package/dist/supervisor.d.ts.map +1 -1
- package/dist/supervisor.js +52 -188
- package/dist/win-lifecycle.d.ts +96 -0
- package/dist/win-lifecycle.d.ts.map +1 -0
- package/dist/win-lifecycle.js +229 -0
- package/dist/win-service.d.ts +119 -0
- package/dist/win-service.d.ts.map +1 -0
- package/dist/win-service.js +226 -0
- package/package.json +8 -6
|
@@ -15,10 +15,10 @@ export function runtimeBinaryEnvVar(runtimeType) {
|
|
|
15
15
|
}
|
|
16
16
|
const RESOLUTION_CACHE_TTL_MS = 5 * 60_000;
|
|
17
17
|
const resolutionCache = new Map();
|
|
18
|
-
export function applyRuntimeBinaryEnv(runtimeType, baseEnv, log) {
|
|
18
|
+
export function applyRuntimeBinaryEnv(runtimeType, baseEnv, log, platform = process.platform) {
|
|
19
19
|
const env = { ...baseEnv };
|
|
20
20
|
const originalPath = env.PATH;
|
|
21
|
-
const pathPlan = cachedCandidatePathPlan(env);
|
|
21
|
+
const pathPlan = cachedCandidatePathPlan(env, platform);
|
|
22
22
|
const primaryPath = mergePath(pathPlan.primaryDirs, originalPath);
|
|
23
23
|
env.PATH = mergePath([...pathPlan.primaryDirs, ...pathPlan.versionedFallbackDirs], originalPath);
|
|
24
24
|
const spec = RUNTIME_BINARIES[runtimeType];
|
|
@@ -26,7 +26,7 @@ export function applyRuntimeBinaryEnv(runtimeType, baseEnv, log) {
|
|
|
26
26
|
return env;
|
|
27
27
|
const configured = env[spec.envVar]?.trim();
|
|
28
28
|
if (configured) {
|
|
29
|
-
const resolved = resolveRuntimeCommand(configured, env, originalPath, primaryPath, env.PATH);
|
|
29
|
+
const resolved = resolveRuntimeCommand(configured, env, originalPath, primaryPath, env.PATH, platform);
|
|
30
30
|
if (resolved) {
|
|
31
31
|
env[spec.envVar] = resolved.binaryPath;
|
|
32
32
|
env.PATH = anchorResolvedPath(env.PATH, resolved);
|
|
@@ -37,7 +37,7 @@ export function applyRuntimeBinaryEnv(runtimeType, baseEnv, log) {
|
|
|
37
37
|
}
|
|
38
38
|
return env;
|
|
39
39
|
}
|
|
40
|
-
const resolved = resolveRuntimeCommand(spec.command, env, originalPath, primaryPath, env.PATH);
|
|
40
|
+
const resolved = resolveRuntimeCommand(spec.command, env, originalPath, primaryPath, env.PATH, platform);
|
|
41
41
|
if (resolved) {
|
|
42
42
|
env[spec.envVar] = resolved.binaryPath;
|
|
43
43
|
env.PATH = anchorResolvedPath(env.PATH, resolved);
|
|
@@ -49,8 +49,8 @@ export function applyRuntimeBinaryEnv(runtimeType, baseEnv, log) {
|
|
|
49
49
|
}
|
|
50
50
|
return env;
|
|
51
51
|
}
|
|
52
|
-
function resolveRuntimeCommand(command, env, inheritedPath, primaryPath, fallbackPath) {
|
|
53
|
-
const cacheKey = `runtime\0${command}\0${primaryPath}\0${fallbackPath}\0${env.SHELL ?? ''}\0${env.HOME ?? ''}`;
|
|
52
|
+
function resolveRuntimeCommand(command, env, inheritedPath, primaryPath, fallbackPath, platform) {
|
|
53
|
+
const cacheKey = `runtime\0${command}\0${primaryPath}\0${fallbackPath}\0${env.SHELL ?? ''}\0${env.HOME ?? ''}\0${platform}`;
|
|
54
54
|
const cached = getCachedResolution(cacheKey);
|
|
55
55
|
if (cached)
|
|
56
56
|
return cached;
|
|
@@ -63,24 +63,24 @@ function resolveRuntimeCommand(command, env, inheritedPath, primaryPath, fallbac
|
|
|
63
63
|
setCachedResolution(cacheKey, resolved);
|
|
64
64
|
return resolved;
|
|
65
65
|
}
|
|
66
|
-
const fromInheritedPath = resolveFromPath(command, inheritedPath, env);
|
|
66
|
+
const fromInheritedPath = resolveFromPath(command, inheritedPath, env, platform);
|
|
67
67
|
if (fromInheritedPath) {
|
|
68
68
|
setCachedResolution(cacheKey, fromInheritedPath);
|
|
69
69
|
return fromInheritedPath;
|
|
70
70
|
}
|
|
71
71
|
if (runLoginShell) {
|
|
72
|
-
const fromShell = resolveFromLoginShell(command, { ...env, PATH: inheritedPath });
|
|
72
|
+
const fromShell = resolveFromLoginShell(command, { ...env, PATH: inheritedPath }, platform);
|
|
73
73
|
if (fromShell) {
|
|
74
74
|
setCachedResolution(cacheKey, fromShell);
|
|
75
75
|
return fromShell;
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
|
-
const fromPrimaryPath = resolveFromPath(command, primaryPath, env);
|
|
78
|
+
const fromPrimaryPath = resolveFromPath(command, primaryPath, env, platform);
|
|
79
79
|
if (fromPrimaryPath) {
|
|
80
80
|
setCachedResolution(cacheKey, fromPrimaryPath);
|
|
81
81
|
return fromPrimaryPath;
|
|
82
82
|
}
|
|
83
|
-
const fromFallbackPath = fallbackPath === primaryPath ? null : resolveFromPath(command, fallbackPath, env);
|
|
83
|
+
const fromFallbackPath = fallbackPath === primaryPath ? null : resolveFromPath(command, fallbackPath, env, platform);
|
|
84
84
|
if (fromFallbackPath) {
|
|
85
85
|
setCachedResolution(cacheKey, fromFallbackPath);
|
|
86
86
|
return fromFallbackPath;
|
|
@@ -99,12 +99,12 @@ function resolveDirectPath(command) {
|
|
|
99
99
|
const abs = path.isAbsolute(command) ? command : path.resolve(process.cwd(), command);
|
|
100
100
|
return isExecutable(abs) ? abs : null;
|
|
101
101
|
}
|
|
102
|
-
function resolveFromPath(command, pathValue, env) {
|
|
102
|
+
function resolveFromPath(command, pathValue, env, platform) {
|
|
103
103
|
if (!pathValue || command.includes('/') || command.includes('\\'))
|
|
104
104
|
return null;
|
|
105
105
|
const dirs = pathValue.split(path.delimiter).filter(Boolean);
|
|
106
106
|
for (const dir of dirs) {
|
|
107
|
-
for (const file of commandCandidates(command, env)) {
|
|
107
|
+
for (const file of commandCandidates(command, env, platform)) {
|
|
108
108
|
const candidate = path.join(dir, file);
|
|
109
109
|
if (isExecutable(candidate))
|
|
110
110
|
return { binaryPath: candidate, pathValue };
|
|
@@ -112,12 +112,17 @@ function resolveFromPath(command, pathValue, env) {
|
|
|
112
112
|
}
|
|
113
113
|
return null;
|
|
114
114
|
}
|
|
115
|
-
function resolveFromLoginShell(command, env) {
|
|
115
|
+
function resolveFromLoginShell(command, env, platform) {
|
|
116
|
+
// No POSIX login shell to consult on Windows — and a Git-Bash user's
|
|
117
|
+
// SHELL=...bash.exe must not pull this synchronous probe (execFileSync,
|
|
118
|
+
// up to ~2.5s × 3 shells) into the spawn path.
|
|
119
|
+
if (platform === 'win32')
|
|
120
|
+
return null;
|
|
116
121
|
if (command.includes('/') || command.includes('\\'))
|
|
117
122
|
return null;
|
|
118
123
|
const shells = unique([
|
|
119
124
|
env.SHELL?.trim(),
|
|
120
|
-
|
|
125
|
+
platform === 'darwin' ? '/bin/zsh' : undefined,
|
|
121
126
|
'/bin/bash',
|
|
122
127
|
'/bin/sh',
|
|
123
128
|
]);
|
|
@@ -158,17 +163,42 @@ function resolveFromLoginShell(command, env) {
|
|
|
158
163
|
// TTL as resolutions. A freshly installed version manager dir therefore shows
|
|
159
164
|
// up within one detection interval, same as everything else here.
|
|
160
165
|
const pathPlanCache = new Map();
|
|
161
|
-
function cachedCandidatePathPlan(env) {
|
|
162
|
-
const key = `${env.HOME ?? ''}\0${env.PRLL_DAEMON_RUNTIME_PATH ?? ''}\0${env.PRLL_DAEMON_EXTRA_PATH ?? ''}`;
|
|
166
|
+
function cachedCandidatePathPlan(env, platform) {
|
|
167
|
+
const key = `${env.HOME ?? ''}\0${env.PRLL_DAEMON_RUNTIME_PATH ?? ''}\0${env.PRLL_DAEMON_EXTRA_PATH ?? ''}\0${env.APPDATA ?? ''}\0${env.LOCALAPPDATA ?? ''}\0${env.PNPM_HOME ?? ''}\0${platform}`;
|
|
163
168
|
const hit = pathPlanCache.get(key);
|
|
164
169
|
if (hit && hit.expiresAt > Date.now())
|
|
165
170
|
return hit.value;
|
|
166
|
-
const value = candidatePathPlan(env);
|
|
171
|
+
const value = candidatePathPlan(env, platform);
|
|
167
172
|
pathPlanCache.set(key, { value, expiresAt: Date.now() + RESOLUTION_CACHE_TTL_MS });
|
|
168
173
|
return value;
|
|
169
174
|
}
|
|
170
|
-
function candidatePathPlan(env) {
|
|
175
|
+
export function candidatePathPlan(env, platform = process.platform) {
|
|
171
176
|
const home = env.HOME || os.homedir();
|
|
177
|
+
if (platform === 'win32') {
|
|
178
|
+
// Windows install layouts. HOME is rarely set there (os.homedir() reads
|
|
179
|
+
// USERPROFILE); APPDATA/LOCALAPPDATA are always present in real sessions
|
|
180
|
+
// but keep derived fallbacks for stripped service environments.
|
|
181
|
+
const appData = env.APPDATA || path.join(home, 'AppData', 'Roaming');
|
|
182
|
+
const localAppData = env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
|
|
183
|
+
const winPrimaryDirs = [
|
|
184
|
+
...splitPath(env.PRLL_DAEMON_RUNTIME_PATH),
|
|
185
|
+
...splitPath(env.PRLL_DAEMON_EXTRA_PATH),
|
|
186
|
+
path.dirname(process.execPath),
|
|
187
|
+
env.PNPM_HOME,
|
|
188
|
+
path.join(appData, 'npm'),
|
|
189
|
+
path.join(localAppData, 'pnpm'),
|
|
190
|
+
path.join(localAppData, 'Volta', 'bin'),
|
|
191
|
+
path.join(home, '.volta', 'bin'),
|
|
192
|
+
path.join(home, '.bun', 'bin'),
|
|
193
|
+
];
|
|
194
|
+
// No versioned fallback scan on Windows: nvm-windows exposes the active
|
|
195
|
+
// version through the nodejs junction already on PATH (and execPath's
|
|
196
|
+
// dir above). Exotic layouts use PRLL_DAEMON_RUNTIME_PATH.
|
|
197
|
+
return {
|
|
198
|
+
primaryDirs: unique(winPrimaryDirs).filter((dir) => !!dir && isDirectory(dir)),
|
|
199
|
+
versionedFallbackDirs: [],
|
|
200
|
+
};
|
|
201
|
+
}
|
|
172
202
|
const primaryDirs = [
|
|
173
203
|
...splitPath(env.PRLL_DAEMON_RUNTIME_PATH),
|
|
174
204
|
...splitPath(env.PRLL_DAEMON_EXTRA_PATH),
|
|
@@ -263,17 +293,22 @@ function mergePath(prependDirs, existing) {
|
|
|
263
293
|
function anchorResolvedPath(pathValue, resolution) {
|
|
264
294
|
return mergePath([path.dirname(resolution.binaryPath), ...splitPath(resolution.pathValue)], pathValue);
|
|
265
295
|
}
|
|
266
|
-
function commandCandidates(command, env) {
|
|
267
|
-
if (
|
|
296
|
+
export function commandCandidates(command, env, platform = process.platform) {
|
|
297
|
+
if (platform !== 'win32')
|
|
268
298
|
return [command];
|
|
269
299
|
const hasExt = /\.[^\\/]+$/.test(command);
|
|
270
300
|
if (hasExt)
|
|
271
301
|
return [command];
|
|
272
|
-
|
|
302
|
+
// PATHEXT-suffixed candidates ONLY, in PATHEXT order (fallback mirrors the
|
|
303
|
+
// Windows default order). A bare-name candidate must not be offered:
|
|
304
|
+
// accessSync(X_OK) passes for any readable file on Windows, so it would
|
|
305
|
+
// resolve to npm's extension-less sh shim sitting next to the .cmd — a
|
|
306
|
+
// file nothing on Windows can actually execute.
|
|
307
|
+
const exts = (env.PATHEXT || '.COM;.EXE;.BAT;.CMD')
|
|
273
308
|
.split(';')
|
|
274
309
|
.filter(Boolean)
|
|
275
310
|
.map((ext) => ext.toLowerCase());
|
|
276
|
-
return
|
|
311
|
+
return exts.map((ext) => `${command}${ext}`);
|
|
277
312
|
}
|
|
278
313
|
/** undefined = no cache entry; null = cached miss (see ResolutionCacheEntry). */
|
|
279
314
|
function getCachedResolution(cacheKey) {
|
package/dist/runtimes.d.ts
CHANGED
|
@@ -19,11 +19,22 @@ export interface AgentDirs {
|
|
|
19
19
|
*/
|
|
20
20
|
homeDir?: string;
|
|
21
21
|
}
|
|
22
|
+
/** Injectable resolution inputs — production callers pass nothing. */
|
|
23
|
+
export interface RuntimeAdapterResolveOptions {
|
|
24
|
+
env?: NodeJS.ProcessEnv;
|
|
25
|
+
entryPath?: string;
|
|
26
|
+
}
|
|
22
27
|
/**
|
|
23
|
-
* Resolve a runtime adapter
|
|
24
|
-
*
|
|
25
|
-
*
|
|
28
|
+
* Resolve a runtime adapter. Bridge bins resolve in three tiers:
|
|
29
|
+
* 1. overlay bundle (~/.parall-daemon/bundle/current/) — a self-updated
|
|
30
|
+
* daemon must load bridges from the same overlay to keep versions in sync;
|
|
31
|
+
* 2. entry-sibling bundle (bundledSiblingBin above) — the npm/CDN package's
|
|
32
|
+
* own flat bundle directory;
|
|
33
|
+
* 3. bare bin name via PATH — dev checkouts running from dist/, where the
|
|
34
|
+
* workspace bin links exist and neither bundle layout does.
|
|
35
|
+
* Tiers 1–2 spawn `node <abs js>` directly, which never depends on PATH or
|
|
36
|
+
* on Windows .cmd shims.
|
|
26
37
|
*/
|
|
27
|
-
export declare function getRuntimeAdapter(runtimeType: string): RuntimeAdapter;
|
|
38
|
+
export declare function getRuntimeAdapter(runtimeType: string, opts?: RuntimeAdapterResolveOptions): RuntimeAdapter;
|
|
28
39
|
export declare function assertAgentKey(apiKey: string): void;
|
|
29
40
|
//# sourceMappingURL=runtimes.d.ts.map
|
package/dist/runtimes.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtimes.d.ts","sourceRoot":"","sources":["../src/runtimes.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"runtimes.d.ts","sourceRoot":"","sources":["../src/runtimes.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,qBAAqB,EAAa,MAAM,oBAAoB,CAAC;AACtE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAGzD,YAAY,EAAE,cAAc,EAAE,CAAC;AAC/B,OAAO,EAAE,qBAAqB,EAAE,CAAC;AAEjC,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,QAAQ,CACN,OAAO,EAAE,MAAM,CAAC,UAAU,EAC1B,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,SAAS,EACf,EAAE,CAAC,EAAE,cAAc,GAClB,MAAM,CAAC,UAAU,CAAC;CACtB;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAqGD,sEAAsE;AACtE,MAAM,WAAW,4BAA4B;IAC3C,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA2CD;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,MAAM,EACnB,IAAI,CAAC,EAAE,4BAA4B,GAClC,cAAc,CAoBhB;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAInD"}
|
package/dist/runtimes.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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 { fileURLToPath } from 'node:url';
|
|
4
5
|
import { clearAllProviderCreds, llmSource } from '@parall/agent-core';
|
|
5
6
|
import { resolveBundleDir } from './config.js';
|
|
6
7
|
export { clearAllProviderCreds };
|
|
@@ -93,17 +94,67 @@ const OVERLAY_BIN_NAMES = {
|
|
|
93
94
|
openclaw: 'parall-openclaw-agent.js',
|
|
94
95
|
};
|
|
95
96
|
/**
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
97
|
+
* Locate a bridge bundle shipped next to the daemon's own entry script.
|
|
98
|
+
* The npm package puts every bin in the same flat `bundle/` directory
|
|
99
|
+
* (parall-daemon.js beside parall-codex-agent.js), so the running entry's
|
|
100
|
+
* real directory is a complete bridge distribution. This is what makes bare
|
|
101
|
+
* `npx @parall/daemon` work on Windows, where the PATH fallback would hit an
|
|
102
|
+
* npm `.cmd` shim that Node refuses to spawn without a shell.
|
|
103
|
+
*
|
|
104
|
+
* The entry path must be realpath'd first: POSIX npm bins are symlinks into
|
|
105
|
+
* the package, and Windows setups may reach the bundle through a junction —
|
|
106
|
+
* dirname of the raw link points at a bin dir with no siblings (same class
|
|
107
|
+
* of bug as the browser-pod entrypoint guard, #1597). import.meta.url is the
|
|
108
|
+
* second candidate because a service launcher can import the daemon with
|
|
109
|
+
* argv[1] pointing outside the bundle.
|
|
99
110
|
*/
|
|
100
|
-
|
|
111
|
+
function bundledSiblingBin(overlayName, entryPath) {
|
|
112
|
+
const candidateDirs = [];
|
|
113
|
+
const entry = entryPath ?? process.argv[1];
|
|
114
|
+
if (entry) {
|
|
115
|
+
try {
|
|
116
|
+
candidateDirs.push(path.dirname(fs.realpathSync(entry)));
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
candidateDirs.push(path.dirname(path.resolve(entry)));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
candidateDirs.push(path.dirname(fileURLToPath(import.meta.url)));
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// non-file module URL; skip
|
|
127
|
+
}
|
|
128
|
+
for (const dir of candidateDirs) {
|
|
129
|
+
const bin = path.join(dir, overlayName);
|
|
130
|
+
try {
|
|
131
|
+
if (fs.existsSync(bin))
|
|
132
|
+
return bin;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// unreadable dir; try the next candidate
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Resolve a runtime adapter. Bridge bins resolve in three tiers:
|
|
142
|
+
* 1. overlay bundle (~/.parall-daemon/bundle/current/) — a self-updated
|
|
143
|
+
* daemon must load bridges from the same overlay to keep versions in sync;
|
|
144
|
+
* 2. entry-sibling bundle (bundledSiblingBin above) — the npm/CDN package's
|
|
145
|
+
* own flat bundle directory;
|
|
146
|
+
* 3. bare bin name via PATH — dev checkouts running from dist/, where the
|
|
147
|
+
* workspace bin links exist and neither bundle layout does.
|
|
148
|
+
* Tiers 1–2 spawn `node <abs js>` directly, which never depends on PATH or
|
|
149
|
+
* on Windows .cmd shims.
|
|
150
|
+
*/
|
|
151
|
+
export function getRuntimeAdapter(runtimeType, opts) {
|
|
101
152
|
const base = RUNTIME_ADAPTERS[runtimeType] ?? defaultAdapter;
|
|
102
153
|
const overlayName = OVERLAY_BIN_NAMES[runtimeType];
|
|
103
154
|
if (!overlayName)
|
|
104
155
|
return base;
|
|
105
156
|
try {
|
|
106
|
-
const bundleDir = resolveBundleDir();
|
|
157
|
+
const bundleDir = resolveBundleDir(opts?.env);
|
|
107
158
|
const overlayBin = path.join(bundleDir, 'current', overlayName);
|
|
108
159
|
if (fs.existsSync(overlayBin)) {
|
|
109
160
|
return { ...base, bin: process.execPath, args: [overlayBin] };
|
|
@@ -112,6 +163,10 @@ export function getRuntimeAdapter(runtimeType) {
|
|
|
112
163
|
catch {
|
|
113
164
|
// resolveBundleDir may fail in unusual setups; fall through
|
|
114
165
|
}
|
|
166
|
+
const siblingBin = bundledSiblingBin(overlayName, opts?.entryPath);
|
|
167
|
+
if (siblingBin) {
|
|
168
|
+
return { ...base, bin: process.execPath, args: [siblingBin] };
|
|
169
|
+
}
|
|
115
170
|
return base;
|
|
116
171
|
}
|
|
117
172
|
export function assertAgentKey(apiKey) {
|
package/dist/supervisor.d.ts
CHANGED
|
@@ -81,6 +81,12 @@ export declare class DaemonSupervisor {
|
|
|
81
81
|
* proxy-configured profile (the caller reports `error` and retries next tick).
|
|
82
82
|
*/
|
|
83
83
|
private resolveBrowserProfileProxy;
|
|
84
|
+
/** Proxy readiness probe endpoint for BYOC profiles (browser-readiness.ts): an
|
|
85
|
+
* explicit override, else the api-server's Parall-owned public `/generate_204`.
|
|
86
|
+
* The probe navigates a sacrificial tab THROUGH the profile's proxy, so the
|
|
87
|
+
* target must be public + proxy-resolvable — `config.apiUrl` is the public API
|
|
88
|
+
* host for BYOC. Falls back to a well-known public 204 if apiUrl is unusable. */
|
|
89
|
+
private resolveBrowserProxyProbeUrl;
|
|
84
90
|
/**
|
|
85
91
|
* Detects a legacy flat state layout (no agents/ subdir) and migrates it
|
|
86
92
|
* into the per-agent directory for the owning agent. Ownership is determined
|
package/dist/supervisor.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"supervisor.d.ts","sourceRoot":"","sources":["../src/supervisor.ts"],"names":[],"mappings":"AAIA,OAAO,EAAyC,KAAK,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAC/F,OAAO,EAgBL,KAAK,YAAY,EAElB,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"supervisor.d.ts","sourceRoot":"","sources":["../src/supervisor.ts"],"names":[],"mappings":"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"}
|
package/dist/supervisor.js
CHANGED
|
@@ -4,10 +4,12 @@ import * as os from 'node:os';
|
|
|
4
4
|
import * as path from 'node:path';
|
|
5
5
|
import { effectiveLLMSourceExplicit, llmSource } from '@parall/agent-core';
|
|
6
6
|
import { ParallWs, } from '@parall/sdk';
|
|
7
|
+
import { PUBLIC_PROXY_PROBE_FALLBACK_URL } from './clip-runtime/browser-readiness.js';
|
|
7
8
|
import { installClip, parseSource } from './clip-runtime/clip-installer.js';
|
|
8
9
|
import { parseIpcCommands } from './clip-runtime/manifest.js';
|
|
9
10
|
import { BrowserProfilePool, ClipProcessManager, ClipProvider, HubClient, } from './clip-runtime/index.js';
|
|
10
11
|
import { agentClaudeHomeFor, agentHomeDirFor, agentStateDirFor, agentWorkspaceDirFor, } from './config.js';
|
|
12
|
+
import { reconcileBrowserProfiles as runBrowserProfileReconcile } from './browser-profile-reconcile.js';
|
|
11
13
|
import { listDirectory } from './filesystem.js';
|
|
12
14
|
import { ensureIsolatedHome, ensureSharedCredentialLink, } from './home-isolation.js';
|
|
13
15
|
import { assertAgentKey, getRuntimeAdapter } from './runtimes.js';
|
|
@@ -178,6 +180,7 @@ export class DaemonSupervisor {
|
|
|
178
180
|
.catch((err) => this.log.warn(`browser profile status report failed: ${String(err)}`));
|
|
179
181
|
},
|
|
180
182
|
resolveProxy: (profileId) => this.resolveBrowserProfileProxy(profileId),
|
|
183
|
+
proxyProbeUrl: this.resolveBrowserProxyProbeUrl(),
|
|
181
184
|
});
|
|
182
185
|
this.clipManager = new ClipProcessManager({
|
|
183
186
|
clipsDir: path.join(this.config.rootStateDir, 'clips'),
|
|
@@ -517,195 +520,23 @@ export class DaemonSupervisor {
|
|
|
517
520
|
throw err;
|
|
518
521
|
}
|
|
519
522
|
}
|
|
520
|
-
|
|
523
|
+
reconcileBrowserProfiles() {
|
|
524
|
+
// Coordination body extracted move-only to browser-profile-reconcile.ts (this
|
|
525
|
+
// file crossed the 2000-line ceiling). The op queue / pool lifecycle / wipe
|
|
526
|
+
// helper are injected so the behavior is byte-for-byte identical.
|
|
521
527
|
const pool = this.browserProfilePool;
|
|
522
528
|
if (!pool)
|
|
523
|
-
return;
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
const pendingReviveBeforeList = new Set(this.browserProfilePendingRevives.keys());
|
|
535
|
-
// And ALL local profile state — per-profile homes AND legacy flat account files
|
|
536
|
-
// (the pool owns that storage layout). State with NO server row is a
|
|
537
|
-
// deleted/reassigned profile whose cookies/storage must be wiped from this host;
|
|
538
|
-
// a pre-upgrade profile never reopened after migration exists only as a legacy flat
|
|
539
|
-
// file, so home-only enumeration would miss it. Pre-fetch so state created during
|
|
540
|
-
// the fetch is left alone.
|
|
541
|
-
const localStateBeforeList = new Set(pool.localStateProfileIds());
|
|
542
|
-
let profiles;
|
|
543
|
-
try {
|
|
544
|
-
profiles = await this.client.listMachineBrowserProfiles();
|
|
545
|
-
}
|
|
546
|
-
catch (err) {
|
|
547
|
-
this.log.warn(`browser profile reconcile: list failed: ${String(err)}`);
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
550
|
-
// The pool must still be the same instance after the await — stop() may have
|
|
551
|
-
// nulled/replaced it during a concurrent shutdown; bail rather than act on a
|
|
552
|
-
// torn-down pool (also avoids NPE-ing through Promise.allSettled into warnings).
|
|
553
|
-
if (this.browserProfilePool !== pool)
|
|
554
|
-
return;
|
|
555
|
-
// Converge local runtimes to the server's desired-live set (the server is the
|
|
556
|
-
// SSOT). The supervisor is the desired-state coordinator; the pool is the
|
|
557
|
-
// per-profile runtime registry. Every op here goes through
|
|
558
|
-
// enqueueBrowserProfileOp so all supervisor-originated profile work shares one
|
|
559
|
-
// ordering layer (the pool's own queue is the final per-profile mutex).
|
|
560
|
-
const desiredLive = new Set();
|
|
561
|
-
const serverIds = new Set(); // every byoc profile the server still lists (any status)
|
|
562
|
-
const serverResetGen = new Map();
|
|
563
|
-
const ops = [];
|
|
564
|
-
// reset_generation (server) is the SSOT for reset intent; the daemon's per-profile
|
|
565
|
-
// sidecar (read via pool.appliedResetGeneration) is only a local ack of "already
|
|
566
|
-
// wiped up to this generation". A missed reset (server newer than the local ack) must
|
|
567
|
-
// wipe local state BEFORE any open/ensure, so a reopen starts clean and never
|
|
568
|
-
// resurrects the cookies the reset cleared. Returns the server reset_generation to
|
|
569
|
-
// apply, or null if nothing is missed. Pure (no side effects).
|
|
570
|
-
const missedResetGen = (profileId) => {
|
|
571
|
-
const gen = serverResetGen.get(profileId) ?? 0;
|
|
572
|
-
return gen > pool.appliedResetGeneration(profileId) ? gen : null;
|
|
573
|
-
};
|
|
574
|
-
// Negative-path helper (a stopped profile with a missed reset): fence synchronously
|
|
575
|
-
// then enqueue a STANDALONE wipe. No revive follows, so a wipe failure simply leaves
|
|
576
|
-
// the profile fenced and the sidecar unwritten → the next reconcile retries (the
|
|
577
|
-
// SSOT reset_generation is still unapplied locally). Fail-closed by construction.
|
|
578
|
-
const enqueueResetWipeIfMissed = (profileId) => {
|
|
579
|
-
const gen = missedResetGen(profileId);
|
|
580
|
-
if (gen === null)
|
|
581
|
-
return false;
|
|
582
|
-
// Fence SYNCHRONOUSLY before the async wipe — same contract as the WS stop/reset
|
|
583
|
-
// path. On a machine-WS reconnect whose clip-provider stream stays live, a hub
|
|
584
|
-
// invoke could otherwise run on the stale runtime in the gap before the wipe op
|
|
585
|
-
// starts. (In the negative loop this re-asserts an already-set fence within the same
|
|
586
|
-
// synchronous iteration — atomic, no revive can observe an intermediate generation.)
|
|
587
|
-
pool.fence(profileId);
|
|
588
|
-
ops.push(this.enqueueBrowserProfileOp(profileId, () => pool.applyResetWipe(profileId, gen)));
|
|
589
|
-
return true;
|
|
590
|
-
};
|
|
591
|
-
for (const profile of profiles) {
|
|
592
|
-
// The daemon supervises only machine-bound (byoc) profiles; hosted profiles
|
|
593
|
-
// (null machine_id) run on the platform pool and are never owned by a daemon.
|
|
594
|
-
if (!profile.machine_id)
|
|
595
|
-
continue;
|
|
596
|
-
serverIds.add(profile.id);
|
|
597
|
-
serverResetGen.set(profile.id, profile.reset_generation ?? 0);
|
|
598
|
-
if (profile.status !== 'running' && profile.status !== 'pending')
|
|
599
|
-
continue;
|
|
600
|
-
desiredLive.add(profile.id);
|
|
601
|
-
// A missed reset must wipe SUCCESSFULLY before this profile is revived. Fence
|
|
602
|
-
// synchronously now (reject hub invokes in the gap before the queued wipe runs).
|
|
603
|
-
const wipeGen = missedResetGen(profile.id);
|
|
604
|
-
if (wipeGen !== null)
|
|
605
|
-
pool.fence(profile.id);
|
|
606
|
-
// Capture the fence generation AFTER any fence bump so a stop arriving while the op
|
|
607
|
-
// waits makes it skip as stale; carry the lifecycle generation so the daemon's
|
|
608
|
-
// `running` report is fenced server-side. Drive desired state through the CAPTURED
|
|
609
|
-
// pool (not enqueueBrowserProfileLifecycle, which re-reads this.browserProfilePool
|
|
610
|
-
// and would negative-ACK `error` for a pending profile if stop() nulled the field).
|
|
611
|
-
const sinceSeq = pool.stopSeqOf(profile.id);
|
|
612
|
-
const generation = profile.lifecycle_generation;
|
|
613
|
-
// forceStatusReport ONLY for a `pending` row: the daemon's last report may already be
|
|
614
|
-
// `running` (Chromium survived the WS drop), so a deduped repeat would leave the
|
|
615
|
-
// server stuck at `pending`. A `running` row keeps the normal dedup.
|
|
616
|
-
const forceStatusReport = profile.status === 'pending';
|
|
617
|
-
ops.push(this.enqueueBrowserProfileRevive(profile.id, async () => {
|
|
618
|
-
// Reset-before-open, FAIL-CLOSED: a missed reset must wipe successfully BEFORE
|
|
619
|
-
// the revive un-fences and reopens. If applyResetWipe throws, the await
|
|
620
|
-
// propagates and ensureRuntime is SKIPPED — the profile stays fenced (the wipe
|
|
621
|
-
// fenced it and the applied-reset sidecar was NOT written), so the next reconcile
|
|
622
|
-
// retries because the SSOT reset_generation is still unapplied locally. This
|
|
623
|
-
// ordering is expressed locally; the enqueueBrowserProfileOp queue contract (a
|
|
624
|
-
// rejected op does not block later ops) is intentionally unchanged. A failed
|
|
625
|
-
// wipe reports `error` (visible) and re-throws here, so ensureRuntime is skipped.
|
|
626
|
-
if (wipeGen !== null)
|
|
627
|
-
await this.wipeBeforeRevive(pool, profile.id, wipeGen, generation);
|
|
628
|
-
// Reconnect recovery is liveness-only: ensure the runtime is live (account +
|
|
629
|
-
// tab) for both pending and running. The original open's `start_url` is NOT
|
|
630
|
-
// replayed (it rides only the live lifecycle event, not persisted); a `pending`
|
|
631
|
-
// profile whose open event the daemon missed comes up at the default page rather
|
|
632
|
-
// than a targetless `about:blank`. Persisting start_url is a separate follow-up.
|
|
633
|
-
await pool.ensureRuntime(profile.id, sinceSeq, generation, { forceStatusReport });
|
|
634
|
-
}));
|
|
635
|
-
}
|
|
636
|
-
// Negative convergence over everything locally known but NOT desired — active
|
|
637
|
-
// runtimes, queued revives (open/ensure not yet a manager), and local state (homes +
|
|
638
|
-
// legacy flat files) — all from the PRE-FETCH snapshots, so a profile that appeared
|
|
639
|
-
// DURING the fetch is left alone (no periodic reconcile would undo a wrong teardown).
|
|
640
|
-
const candidates = new Set([
|
|
641
|
-
...activeBeforeList,
|
|
642
|
-
...pendingReviveBeforeList,
|
|
643
|
-
...localStateBeforeList,
|
|
644
|
-
]);
|
|
645
|
-
for (const profileId of candidates) {
|
|
646
|
-
if (desiredLive.has(profileId))
|
|
647
|
-
continue;
|
|
648
|
-
const isActive = activeBeforeList.has(profileId);
|
|
649
|
-
const hasLocalState = localStateBeforeList.has(profileId);
|
|
650
|
-
// Pending-only revive (queued open/ensure, no runtime, no on-disk state at the
|
|
651
|
-
// pre-fetch snapshot): the synchronous fence alone neutralizes the stale open (it
|
|
652
|
-
// bumps stopSeq, so the queued revive skips as stale). Do NOT AWAIT a teardown — it
|
|
653
|
-
// would block behind the possibly-stuck queue and stall the whole reconcile (the
|
|
654
|
-
// queued-stale-open deadlock).
|
|
655
|
-
if (!isActive && !hasLocalState) {
|
|
656
|
-
pool.fence(profileId);
|
|
657
|
-
// Race: a pending-only revive can COMPLETE during the list fetch (create a manager
|
|
658
|
-
// + on-disk state) BEFORE this fence — the snapshot is then stale, so the fence
|
|
659
|
-
// alone leaves an orphaned runtime/disk the negative loop never converged. Converge
|
|
660
|
-
// it FIRE-AND-FORGET (NOT in `ops`: a still-stuck revive queue must not hang the
|
|
661
|
-
// reconcile; it runs once the queue drains — after the fenced open skips or
|
|
662
|
-
// completes — and is a no-op if nothing was created):
|
|
663
|
-
// - deleted/reassigned → wipeAbsent: the old host must keep no session data.
|
|
664
|
-
// - present + missed reset → applyResetWipe: the completed open may have applied
|
|
665
|
-
// only its event's (older) reset_generation, not the current one.
|
|
666
|
-
// - present + stopped → releaseRuntime: free the orphaned Chromium; a plain
|
|
667
|
-
// stop keeps its cookies on disk.
|
|
668
|
-
let cleanup;
|
|
669
|
-
if (!serverIds.has(profileId)) {
|
|
670
|
-
cleanup = pool.wipeAbsent(profileId);
|
|
671
|
-
}
|
|
672
|
-
else {
|
|
673
|
-
const wipeGen = missedResetGen(profileId);
|
|
674
|
-
cleanup =
|
|
675
|
-
wipeGen !== null
|
|
676
|
-
? pool.applyResetWipe(profileId, wipeGen)
|
|
677
|
-
: pool.releaseRuntime(profileId);
|
|
678
|
-
}
|
|
679
|
-
void cleanup.catch((err) => this.log.warn(`browser profile pending-revive cleanup failed for ${profileId}: ${String(err)}`));
|
|
680
|
-
continue;
|
|
681
|
-
}
|
|
682
|
-
if (!serverIds.has(profileId)) {
|
|
683
|
-
// Absent from the server list → deleted/reassigned → fence + wipe local state (the
|
|
684
|
-
// old host must not keep cookies/storage). No status report — server is the SSOT.
|
|
685
|
-
pool.fence(profileId);
|
|
686
|
-
ops.push(this.enqueueBrowserProfileOp(profileId, () => pool.wipeAbsent(profileId)));
|
|
687
|
-
}
|
|
688
|
-
else if (enqueueResetWipeIfMissed(profileId)) {
|
|
689
|
-
// Present (stopped) with a missed reset → fenced synchronously + wipe enqueued.
|
|
690
|
-
}
|
|
691
|
-
else if (isActive) {
|
|
692
|
-
// Present (stopped) but the runtime is still live → fence + release the runtime
|
|
693
|
-
// only; a plain stop keeps its cookies on disk.
|
|
694
|
-
pool.fence(profileId);
|
|
695
|
-
ops.push(this.enqueueBrowserProfileOp(profileId, () => pool.releaseRuntime(profileId)));
|
|
696
|
-
}
|
|
697
|
-
// else: present + stopped + on-disk state + no runtime + no missed reset → LEAVE
|
|
698
|
-
// ALONE (do NOT fence). Its stopped-fence is already held — the start-sequence
|
|
699
|
-
// pre-fence on a fresh daemon, or the persisted pool state on a WS reconnect — so a
|
|
700
|
-
// hub invoke is still rejected. Re-fencing here would bump stopSeq and drop an `open`
|
|
701
|
-
// that arrived during the list fetch (it captured an older sinceSeq, and the stale
|
|
702
|
-
// `stopped` snapshot doesn't reflect it yet), stranding the server at `pending`.
|
|
703
|
-
}
|
|
704
|
-
for (const result of await Promise.allSettled(ops)) {
|
|
705
|
-
if (result.status === 'rejected') {
|
|
706
|
-
this.log.warn(`browser profile reconcile op failed: ${String(result.reason)}`);
|
|
707
|
-
}
|
|
708
|
-
}
|
|
529
|
+
return Promise.resolve();
|
|
530
|
+
return runBrowserProfileReconcile({
|
|
531
|
+
pool,
|
|
532
|
+
currentPool: () => this.browserProfilePool,
|
|
533
|
+
client: this.client,
|
|
534
|
+
log: this.log,
|
|
535
|
+
enqueueOp: (id, op) => this.enqueueBrowserProfileOp(id, op),
|
|
536
|
+
enqueueRevive: (id, op) => this.enqueueBrowserProfileRevive(id, op),
|
|
537
|
+
wipeBeforeRevive: (p, id, rg, g) => this.wipeBeforeRevive(p, id, rg, g),
|
|
538
|
+
pendingReviveIds: () => new Set(this.browserProfilePendingRevives.keys()),
|
|
539
|
+
});
|
|
709
540
|
}
|
|
710
541
|
/**
|
|
711
542
|
* Resolve a profile's outbound proxy for bb-browser account creation (BYOC).
|
|
@@ -731,6 +562,22 @@ export class DaemonSupervisor {
|
|
|
731
562
|
password: profile.proxy_password ?? undefined,
|
|
732
563
|
};
|
|
733
564
|
}
|
|
565
|
+
/** Proxy readiness probe endpoint for BYOC profiles (browser-readiness.ts): an
|
|
566
|
+
* explicit override, else the api-server's Parall-owned public `/generate_204`.
|
|
567
|
+
* The probe navigates a sacrificial tab THROUGH the profile's proxy, so the
|
|
568
|
+
* target must be public + proxy-resolvable — `config.apiUrl` is the public API
|
|
569
|
+
* host for BYOC. Falls back to a well-known public 204 if apiUrl is unusable. */
|
|
570
|
+
resolveBrowserProxyProbeUrl() {
|
|
571
|
+
const override = process.env.PRLL_BROWSER_PROXY_PROBE_URL?.trim();
|
|
572
|
+
if (override)
|
|
573
|
+
return override;
|
|
574
|
+
try {
|
|
575
|
+
return new URL('/generate_204', this.config.apiUrl).toString();
|
|
576
|
+
}
|
|
577
|
+
catch {
|
|
578
|
+
return PUBLIC_PROXY_PROBE_FALLBACK_URL;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
734
581
|
// ---- Flat layout migration (self-hosted → daemon) ----
|
|
735
582
|
/**
|
|
736
583
|
* Detects a legacy flat state layout (no agents/ subdir) and migrates it
|
|
@@ -913,6 +760,20 @@ export class DaemonSupervisor {
|
|
|
913
760
|
}
|
|
914
761
|
catch (err) {
|
|
915
762
|
this.log.warn(`browser profile lifecycle failed (profile=${data.profile_id}, action=${data.action}): ${String(err)}`);
|
|
763
|
+
// Negative-ACK backstop for open/reset failures that never reached the
|
|
764
|
+
// manager's own error report (pool queue rejection, unsafe id, disposal
|
|
765
|
+
// failure): without it the server row is stranded at `pending` with no
|
|
766
|
+
// error_msg and the user sees a silent timeout. The manager-level report
|
|
767
|
+
// (same status, message may differ) already carries the structured cause
|
|
768
|
+
// when it fired — a second update is idempotent on status. Deliberately
|
|
769
|
+
// NOT for `stop`: its desired state is durable and reconcile converges it.
|
|
770
|
+
if ((data.action === 'open' || data.action === 'reset') && this.running) {
|
|
771
|
+
await this.client
|
|
772
|
+
.reportBrowserProfileStatus(data.profile_id, 'error', err instanceof Error ? err.message : String(err), data.generation)
|
|
773
|
+
.catch((reportErr) => {
|
|
774
|
+
this.log.warn(`browser profile lifecycle negative ACK failed for ${data.profile_id}: ${String(reportErr)}`);
|
|
775
|
+
});
|
|
776
|
+
}
|
|
916
777
|
}
|
|
917
778
|
}
|
|
918
779
|
enqueueBrowserProfileLifecycle(data) {
|
|
@@ -1799,7 +1660,10 @@ export class DaemonSupervisor {
|
|
|
1799
1660
|
child.once('error', (err) => {
|
|
1800
1661
|
if (err.code === 'ENOENT') {
|
|
1801
1662
|
if (adapter.args.length > 0) {
|
|
1802
|
-
|
|
1663
|
+
// Overlay / entry-sibling resolution: bin is the node executable
|
|
1664
|
+
// and args[0] is the bridge bundle js that just resolved on disk.
|
|
1665
|
+
this.log.error(`Node runtime "${adapter.bin}" not found while launching ${adapter.args[0]} — ` +
|
|
1666
|
+
`reinstall the daemon (npm install -g @parall/daemon) or the Desktop app`);
|
|
1803
1667
|
}
|
|
1804
1668
|
else {
|
|
1805
1669
|
const pkg = RUNTIME_PACKAGES[state.runtimeType] ?? `@parall/${state.runtimeType}-agent`;
|