@melaya/runner 1.1.68 → 1.1.70
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/dist/browserBridge.js +178 -0
- package/dist/browserFast.d.ts +144 -0
- package/dist/browserFast.js +363 -0
- package/dist/browserProvisioner.d.ts +39 -0
- package/dist/browserProvisioner.js +96 -0
- package/dist/cli.js +190 -65
- package/dist/codeWorker.js +33 -17
- package/dist/computerHelperManifest.d.ts +5 -0
- package/dist/computerHelperManifest.js +18 -0
- package/dist/computerHostCli.d.ts +233 -0
- package/dist/computerHostCli.js +815 -0
- package/dist/computerRelay.d.ts +73 -0
- package/dist/computerRelay.js +280 -0
- package/dist/connection.d.ts +18 -2
- package/dist/connection.js +322 -82
- package/dist/detect.d.ts +40 -1
- package/dist/detect.js +194 -36
- package/dist/hostChannel.d.ts +126 -0
- package/dist/hostChannel.js +117 -0
- package/dist/hostFiles.d.ts +25 -0
- package/dist/hostFiles.js +90 -0
- package/dist/linkedinLogin.js +13 -17
- package/dist/lumaLogin.js +11 -17
- package/dist/modelRescan.d.ts +36 -0
- package/dist/modelRescan.js +80 -0
- package/dist/processTree.d.ts +65 -0
- package/dist/processTree.js +237 -0
- package/dist/publishGate.js +14 -4
- package/dist/pythonEnv.js +6 -7
- package/dist/sharedVendor.js +3 -1
- package/dist/singleInstance.js +10 -1
- package/dist/startupCache.d.ts +22 -0
- package/dist/startupCache.js +45 -0
- package/dist/startupTimings.d.ts +34 -0
- package/dist/startupTimings.js +81 -0
- package/dist/uvBootstrap.js +32 -24
- package/dist/vaults.js +3 -1
- package/package.json +1 -1
package/dist/sharedVendor.js
CHANGED
|
@@ -13,6 +13,7 @@ import { homedir, setPriority } from "os";
|
|
|
13
13
|
import { spawn } from "child_process";
|
|
14
14
|
import { createHash, createHmac, timingSafeEqual } from "crypto";
|
|
15
15
|
import { venvPython } from "./pythonEnv.js";
|
|
16
|
+
import { trackChild } from "./processTree.js";
|
|
16
17
|
const CACHE_DIR = join(homedir(), ".melaya-runner");
|
|
17
18
|
const SHARED_DIR = join(CACHE_DIR, "shared");
|
|
18
19
|
const VERSION_FILE = join(CACHE_DIR, "shared-version.txt");
|
|
@@ -162,7 +163,8 @@ function precompileInBackground() {
|
|
|
162
163
|
const dirs = ["shared", "agentscope", "crews"].map((d) => join(CACHE_DIR, d)).filter((d) => existsSync(d));
|
|
163
164
|
if (!dirs.length)
|
|
164
165
|
return;
|
|
165
|
-
|
|
166
|
+
// Tracked: runner shutdown stops it instead of leaving it compiling.
|
|
167
|
+
const child = trackChild(spawn(py, ["-m", "compileall", "-q", "-j", "2", ...dirs], { stdio: "ignore", windowsHide: true, detached: false }));
|
|
166
168
|
child.on("error", () => { });
|
|
167
169
|
if (child.pid) {
|
|
168
170
|
try {
|
package/dist/singleInstance.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { mkdirSync, readFileSync, unlinkSync, openSync, writeSync, closeSync } from "fs";
|
|
21
21
|
import { join } from "path";
|
|
22
22
|
import { homedir } from "os";
|
|
23
|
+
import { gracefulShutdownClaimed } from "./processTree.js";
|
|
23
24
|
const CACHE_DIR = join(homedir(), ".melaya-runner");
|
|
24
25
|
const LOCK_PATH = join(CACHE_DIR, "runner.lock");
|
|
25
26
|
function pidAlive(pid) {
|
|
@@ -110,7 +111,15 @@ export function acquireSingleInstanceLock() {
|
|
|
110
111
|
};
|
|
111
112
|
process.on("exit", release);
|
|
112
113
|
for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
113
|
-
|
|
114
|
+
// Once connection.ts owns a graceful shutdown (SIGINT/SIGTERM), let it
|
|
115
|
+
// finish: it exits on its own and "exit" above releases the lock then, so
|
|
116
|
+
// a successor cannot start while this runner's children are still stopping.
|
|
117
|
+
process.on(sig, () => {
|
|
118
|
+
if (sig !== "SIGHUP" && gracefulShutdownClaimed())
|
|
119
|
+
return;
|
|
120
|
+
release();
|
|
121
|
+
process.exit(0);
|
|
122
|
+
});
|
|
114
123
|
}
|
|
115
124
|
return { ok: true };
|
|
116
125
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Last good startup facts, so the next start can connect at once:
|
|
3
|
+
* ~/.melaya-runner/startup-cache.json holds the last published model list and
|
|
4
|
+
* the Python interpreter that worked. The runner connects with these, then
|
|
5
|
+
* publishes fresh discovery through `runner:models` when it lands.
|
|
6
|
+
*
|
|
7
|
+
* Model names only (no credentials, no tokens). A missing or malformed file
|
|
8
|
+
* reads as empty; writes are best-effort and atomic (temp file + rename).
|
|
9
|
+
*/
|
|
10
|
+
import type { DetectedModel } from "./detect.js";
|
|
11
|
+
export interface StartupCache {
|
|
12
|
+
models?: DetectedModel[];
|
|
13
|
+
modelsAt?: number;
|
|
14
|
+
python?: {
|
|
15
|
+
cmd: string;
|
|
16
|
+
minor: number;
|
|
17
|
+
provisioned?: boolean;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export declare function startupCachePath(dir?: string): string;
|
|
21
|
+
export declare function readStartupCache(path?: string): StartupCache;
|
|
22
|
+
export declare function writeStartupCache(patch: StartupCache, path?: string): void;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Last good startup facts, so the next start can connect at once:
|
|
3
|
+
* ~/.melaya-runner/startup-cache.json holds the last published model list and
|
|
4
|
+
* the Python interpreter that worked. The runner connects with these, then
|
|
5
|
+
* publishes fresh discovery through `runner:models` when it lands.
|
|
6
|
+
*
|
|
7
|
+
* Model names only (no credentials, no tokens). A missing or malformed file
|
|
8
|
+
* reads as empty; writes are best-effort and atomic (temp file + rename).
|
|
9
|
+
*/
|
|
10
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
11
|
+
import { homedir } from "os";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
const PROVIDERS = new Set(["lmstudio", "ollama", "claude_code", "codex", "github_copilot"]);
|
|
14
|
+
export function startupCachePath(dir = join(homedir(), ".melaya-runner")) {
|
|
15
|
+
return join(dir, "startup-cache.json");
|
|
16
|
+
}
|
|
17
|
+
export function readStartupCache(path = startupCachePath()) {
|
|
18
|
+
try {
|
|
19
|
+
const j = JSON.parse(readFileSync(path, "utf-8"));
|
|
20
|
+
const out = {};
|
|
21
|
+
if (Array.isArray(j.models)) {
|
|
22
|
+
out.models = j.models
|
|
23
|
+
.filter((m) => m && PROVIDERS.has(String(m.provider)) && typeof m.name === "string" && m.name.length > 0 && m.name.length < 200)
|
|
24
|
+
.slice(0, 500);
|
|
25
|
+
out.modelsAt = Number(j.modelsAt) || 0;
|
|
26
|
+
}
|
|
27
|
+
if (j.python && typeof j.python.cmd === "string" && Number.isInteger(j.python.minor)) {
|
|
28
|
+
out.python = { cmd: j.python.cmd, minor: j.python.minor, ...(j.python.provisioned ? { provisioned: true } : {}) };
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function writeStartupCache(patch, path = startupCachePath()) {
|
|
37
|
+
try {
|
|
38
|
+
const next = { ...readStartupCache(path), ...patch };
|
|
39
|
+
mkdirSync(join(path, ".."), { recursive: true });
|
|
40
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
41
|
+
writeFileSync(tmp, JSON.stringify(next), { encoding: "utf-8", mode: 0o600 });
|
|
42
|
+
renameSync(tmp, path);
|
|
43
|
+
}
|
|
44
|
+
catch { /* best-effort: the next start just discovers again */ }
|
|
45
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Startup stage timestamps: where the time goes between process start and a
|
|
3
|
+
* runner that is online with its models and Python ready.
|
|
4
|
+
*
|
|
5
|
+
* Stages (first mark wins, epoch ms):
|
|
6
|
+
* process_start the Node process began (performance.timeOrigin)
|
|
7
|
+
* config options parsed and the token is in hand
|
|
8
|
+
* socket_connected the Socket.IO handshake succeeded
|
|
9
|
+
* online hello sent, relay up: the server can route work here
|
|
10
|
+
* python_ready a supported Python interpreter is resolved
|
|
11
|
+
* discovery_done the local model scan finished and was published
|
|
12
|
+
*
|
|
13
|
+
* The socket no longer waits for Python or discovery, so the last two usually
|
|
14
|
+
* land after `online`. Reported to the desktop app (log line + a "timings"
|
|
15
|
+
* host message) and printed with --diagnostics.
|
|
16
|
+
*/
|
|
17
|
+
export declare const STARTUP_STAGES: readonly ["process_start", "config", "socket_connected", "online", "python_ready", "discovery_done"];
|
|
18
|
+
export type StartupStage = (typeof STARTUP_STAGES)[number];
|
|
19
|
+
/** Record a stage once. Later marks of the same stage are ignored. */
|
|
20
|
+
export declare function markStage(stage: StartupStage, at?: number): void;
|
|
21
|
+
/** Called after every new stage mark. Returns an unsubscribe function. */
|
|
22
|
+
export declare function onStage(cb: (stage: StartupStage, at: number) => void): () => void;
|
|
23
|
+
export declare function stageMarked(stage: StartupStage): boolean;
|
|
24
|
+
/** True once every stage has been recorded. */
|
|
25
|
+
export declare function startupComplete(): boolean;
|
|
26
|
+
/** Epoch timestamps and offsets from process start, in stage order. */
|
|
27
|
+
export declare function startupTimings(): {
|
|
28
|
+
at: Partial<Record<StartupStage, number>>;
|
|
29
|
+
sinceStartMs: Partial<Record<StartupStage, number>>;
|
|
30
|
+
};
|
|
31
|
+
/** Human-readable timeline, one stage per line. */
|
|
32
|
+
export declare function formatStartupTimings(): string;
|
|
33
|
+
/** Test seam. */
|
|
34
|
+
export declare function _resetStartupTimings(): void;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Startup stage timestamps: where the time goes between process start and a
|
|
3
|
+
* runner that is online with its models and Python ready.
|
|
4
|
+
*
|
|
5
|
+
* Stages (first mark wins, epoch ms):
|
|
6
|
+
* process_start the Node process began (performance.timeOrigin)
|
|
7
|
+
* config options parsed and the token is in hand
|
|
8
|
+
* socket_connected the Socket.IO handshake succeeded
|
|
9
|
+
* online hello sent, relay up: the server can route work here
|
|
10
|
+
* python_ready a supported Python interpreter is resolved
|
|
11
|
+
* discovery_done the local model scan finished and was published
|
|
12
|
+
*
|
|
13
|
+
* The socket no longer waits for Python or discovery, so the last two usually
|
|
14
|
+
* land after `online`. Reported to the desktop app (log line + a "timings"
|
|
15
|
+
* host message) and printed with --diagnostics.
|
|
16
|
+
*/
|
|
17
|
+
import { sendToHost } from "./hostChannel.js";
|
|
18
|
+
export const STARTUP_STAGES = ["process_start", "config", "socket_connected", "online", "python_ready", "discovery_done"];
|
|
19
|
+
const marks = new Map();
|
|
20
|
+
const listeners = new Set();
|
|
21
|
+
function processStart() {
|
|
22
|
+
const origin = typeof performance !== "undefined" && performance.timeOrigin ? performance.timeOrigin : 0;
|
|
23
|
+
return Math.round(origin || Date.now() - process.uptime() * 1000);
|
|
24
|
+
}
|
|
25
|
+
/** Record a stage once. Later marks of the same stage are ignored. */
|
|
26
|
+
export function markStage(stage, at = Date.now()) {
|
|
27
|
+
if (!marks.has("process_start"))
|
|
28
|
+
marks.set("process_start", processStart());
|
|
29
|
+
if (marks.has(stage))
|
|
30
|
+
return;
|
|
31
|
+
marks.set(stage, at);
|
|
32
|
+
const start = marks.get("process_start");
|
|
33
|
+
sendToHost({ type: "log", level: "info", line: `startup: ${stage} +${Math.max(0, at - start)}ms` });
|
|
34
|
+
sendToHost({ type: "timings", stages: startupTimings().sinceStartMs });
|
|
35
|
+
for (const l of listeners) {
|
|
36
|
+
try {
|
|
37
|
+
l(stage, at);
|
|
38
|
+
}
|
|
39
|
+
catch { /* listener errors never matter */ }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Called after every new stage mark. Returns an unsubscribe function. */
|
|
43
|
+
export function onStage(cb) {
|
|
44
|
+
listeners.add(cb);
|
|
45
|
+
return () => { listeners.delete(cb); };
|
|
46
|
+
}
|
|
47
|
+
export function stageMarked(stage) {
|
|
48
|
+
return marks.has(stage);
|
|
49
|
+
}
|
|
50
|
+
/** True once every stage has been recorded. */
|
|
51
|
+
export function startupComplete() {
|
|
52
|
+
return STARTUP_STAGES.every((s) => marks.has(s));
|
|
53
|
+
}
|
|
54
|
+
/** Epoch timestamps and offsets from process start, in stage order. */
|
|
55
|
+
export function startupTimings() {
|
|
56
|
+
if (!marks.has("process_start"))
|
|
57
|
+
marks.set("process_start", processStart());
|
|
58
|
+
const start = marks.get("process_start");
|
|
59
|
+
const at = {};
|
|
60
|
+
const sinceStartMs = {};
|
|
61
|
+
for (const s of STARTUP_STAGES) {
|
|
62
|
+
const v = marks.get(s);
|
|
63
|
+
if (v === undefined)
|
|
64
|
+
continue;
|
|
65
|
+
at[s] = v;
|
|
66
|
+
sinceStartMs[s] = Math.max(0, v - start);
|
|
67
|
+
}
|
|
68
|
+
return { at, sinceStartMs };
|
|
69
|
+
}
|
|
70
|
+
/** Human-readable timeline, one stage per line. */
|
|
71
|
+
export function formatStartupTimings() {
|
|
72
|
+
const { sinceStartMs } = startupTimings();
|
|
73
|
+
return STARTUP_STAGES
|
|
74
|
+
.map((s) => `${s.padEnd(17)} ${sinceStartMs[s] === undefined ? "pending" : `+${sinceStartMs[s]}ms`}`)
|
|
75
|
+
.join("\n");
|
|
76
|
+
}
|
|
77
|
+
/** Test seam. */
|
|
78
|
+
export function _resetStartupTimings() {
|
|
79
|
+
marks.clear();
|
|
80
|
+
listeners.clear();
|
|
81
|
+
}
|
package/dist/uvBootstrap.js
CHANGED
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
import { existsSync, mkdirSync, writeFileSync, chmodSync, rmSync, readdirSync, statSync, copyFileSync } from "fs";
|
|
19
19
|
import { join } from "path";
|
|
20
20
|
import { homedir, platform, arch } from "os";
|
|
21
|
-
import {
|
|
21
|
+
import { execFile } from "child_process";
|
|
22
|
+
import { spawnTracked as spawn, registerSetupChild, terminateTree } from "./processTree.js";
|
|
22
23
|
const CACHE_DIR = join(homedir(), ".melaya-runner");
|
|
23
24
|
const BIN_DIR = join(CACHE_DIR, "bin");
|
|
24
25
|
const isWin = platform() === "win32";
|
|
@@ -40,27 +41,28 @@ function assetName() {
|
|
|
40
41
|
return a === "arm64" ? "uv-aarch64-pc-windows-msvc.zip" : "uv-x86_64-pc-windows-msvc.zip";
|
|
41
42
|
return null;
|
|
42
43
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return execSync(cmd, { encoding: "utf-8", timeout: 8000, stdio: ["pipe", "pipe", "pipe"] }).trim() || null;
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
44
|
+
// PATH lookup without a shell or a blocking subprocess: Python provisioning
|
|
45
|
+
// now runs after the socket connects, so nothing here may stall the event loop.
|
|
51
46
|
function findOnPath(bin) {
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
47
|
+
const sep = isWin ? ";" : ":";
|
|
48
|
+
for (const dir of (process.env.PATH || process.env.Path || "").split(sep)) {
|
|
49
|
+
if (!dir)
|
|
50
|
+
continue;
|
|
51
|
+
const cand = join(dir, bin);
|
|
52
|
+
try {
|
|
53
|
+
if (existsSync(cand) && statSync(cand).isFile())
|
|
54
|
+
return cand;
|
|
55
|
+
}
|
|
56
|
+
catch { /* unreadable dir */ }
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
57
59
|
}
|
|
58
60
|
// Inactivity-bounded child runner (mirrors pythonEnv.runProc): a managed-CPython
|
|
59
61
|
// download legitimately takes a while but streams progress, so we bound silence.
|
|
60
62
|
function run(cmd, args, onLine, opts = {}) {
|
|
61
63
|
const inactivityMs = opts.inactivityMs ?? 5 * 60_000;
|
|
62
64
|
return new Promise((resolve) => {
|
|
63
|
-
const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, ...opts.env }, cwd: opts.cwd });
|
|
65
|
+
const child = registerSetupChild(spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, ...opts.env }, cwd: opts.cwd }));
|
|
64
66
|
let done = false;
|
|
65
67
|
let timer;
|
|
66
68
|
const finish = (c) => { if (done)
|
|
@@ -69,10 +71,7 @@ function run(cmd, args, onLine, opts = {}) {
|
|
|
69
71
|
const bump = () => {
|
|
70
72
|
if (timer)
|
|
71
73
|
clearTimeout(timer);
|
|
72
|
-
timer = setTimeout(() => {
|
|
73
|
-
child.kill("SIGKILL");
|
|
74
|
-
}
|
|
75
|
-
catch { /* gone */ } onLine(`(no output for ${Math.round(inactivityMs / 60000)}m — killed ${cmd})`); finish(1); }, inactivityMs);
|
|
74
|
+
timer = setTimeout(() => { void terminateTree(child, { graceMs: 0 }); onLine(`(no output for ${Math.round(inactivityMs / 60000)}m — killed ${cmd})`); finish(1); }, inactivityMs);
|
|
76
75
|
};
|
|
77
76
|
const chew = (b) => { bump(); for (const ln of b.toString().split("\n")) {
|
|
78
77
|
const t = ln.trimEnd();
|
|
@@ -88,11 +87,14 @@ function run(cmd, args, onLine, opts = {}) {
|
|
|
88
87
|
}
|
|
89
88
|
function capture(cmd, args, env = {}) {
|
|
90
89
|
return new Promise((resolve) => {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
90
|
+
try {
|
|
91
|
+
execFile(cmd, args, { env: { ...process.env, ...env }, timeout: 30_000, windowsHide: true, encoding: "utf-8" }, (err, stdout) => {
|
|
92
|
+
resolve(err ? "" : String(stdout || "").trim());
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
resolve("");
|
|
97
|
+
}
|
|
96
98
|
});
|
|
97
99
|
}
|
|
98
100
|
// Extract the downloaded uv archive. uv ships .tar.gz for macOS/Linux and .zip
|
|
@@ -223,6 +225,12 @@ export async function provisionPython(version, onLine) {
|
|
|
223
225
|
const uv = await ensureUv(onLine);
|
|
224
226
|
if (!uv)
|
|
225
227
|
return null;
|
|
228
|
+
// Fast path: a Python of this version uv already knows (provisioned on an
|
|
229
|
+
// earlier start): no install step, no network.
|
|
230
|
+
// --system: never a virtual environment that happens to be active or in cwd.
|
|
231
|
+
const known = await capture(uv, ["python", "find", "--system", version], { UV_SYSTEM_CERTS: "1" });
|
|
232
|
+
if (known && existsSync(known))
|
|
233
|
+
return known;
|
|
226
234
|
onLine(`installing a managed Python ${version} (one-time; ~30-60s)`);
|
|
227
235
|
// UV_SYSTEM_CERTS makes uv verify against the OS trust store instead of its
|
|
228
236
|
// bundled webpki roots — required behind TLS-intercepting proxies/antivirus
|
package/dist/vaults.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
import { createPublicKey, verify as cryptoVerify, randomBytes } from "crypto";
|
|
22
22
|
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, watch as fsWatch, writeFileSync, } from "fs";
|
|
23
23
|
import { spawn } from "child_process";
|
|
24
|
+
import { trackChild } from "./processTree.js";
|
|
24
25
|
import { homedir, platform as osPlatform } from "os";
|
|
25
26
|
import path from "path";
|
|
26
27
|
import { Readable } from "stream";
|
|
@@ -482,7 +483,8 @@ export class VaultHostProcess {
|
|
|
482
483
|
return this.starting;
|
|
483
484
|
this.starting = (async () => {
|
|
484
485
|
const l = await this.o.launch();
|
|
485
|
-
|
|
486
|
+
// Tracked: runner shutdown stops it even if stop()'s delayed kill never fires.
|
|
487
|
+
const p = trackChild(spawn(l.pythonPath, ["-u", l.scriptPath], { env: l.env, cwd: l.cwd, stdio: ["pipe", "pipe", "pipe"] }));
|
|
486
488
|
// EPIPE after a crash surfaces here; the exit handler does the cleanup.
|
|
487
489
|
p.stdin?.on("error", () => { });
|
|
488
490
|
p.stdout?.setEncoding("utf-8");
|