machine-bridge-mcp 3.0.0-beta.21 → 3.0.0-beta.26
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/CHANGELOG.md +134 -0
- package/CONTRIBUTING.md +3 -3
- package/GOVERNANCE.md +2 -2
- package/README.md +24 -6
- package/browser-extension/manifest.json +1 -1
- package/docs/AGENT_CONTEXT.md +10 -7
- package/docs/ARCHITECTURE.md +35 -22
- package/docs/AUDIT.md +85 -1
- package/docs/CLIENTS.md +6 -2
- package/docs/ENGINEERING.md +31 -9
- package/docs/LOCAL_AUTOMATION.md +4 -2
- package/docs/LOGGING.md +8 -8
- package/docs/OPERATIONS.md +43 -17
- package/docs/PRIVACY.md +18 -4
- package/docs/PROJECT_STANDARDS.md +2 -2
- package/docs/RELEASING.md +35 -11
- package/docs/TESTING.md +36 -16
- package/docs/THREAT_MODEL.md +20 -5
- package/docs/TOOL_REFERENCE.md +18 -12
- package/docs/UPGRADING.md +32 -0
- package/package.json +15 -6
- package/scripts/check-plan.mjs +8 -0
- package/scripts/coverage-check.mjs +30 -1
- package/scripts/foreground-daemon-recovery.mjs +88 -0
- package/scripts/github-release.mjs +22 -16
- package/scripts/install-published-prerelease.mjs +7 -7
- package/scripts/official-mcp-conformance.mjs +243 -0
- package/scripts/persistent-activation-process.mjs +36 -0
- package/scripts/release-candidate-manifest.mjs +12 -0
- package/scripts/release-publication-guard.mjs +65 -0
- package/scripts/release-state.mjs +1 -1
- package/scripts/sbom-check.mjs +99 -0
- package/scripts/start-release-candidate.mjs +39 -13
- package/src/local/agent-context-projection.mjs +26 -7
- package/src/local/agent-context.mjs +25 -4
- package/src/local/autostart-log-maintenance.mjs +36 -0
- package/src/local/capability-observer.mjs +5 -0
- package/src/local/child-process-settlement.mjs +103 -0
- package/src/local/cli-activate.mjs +42 -5
- package/src/local/cli-service.mjs +55 -5
- package/src/local/cli.mjs +59 -10
- package/src/local/daemon-process.mjs +24 -3
- package/src/local/delegated-process-sandbox.mjs +1 -0
- package/src/local/execution-routing.mjs +231 -0
- package/src/local/git-service.mjs +3 -1
- package/src/local/job-runner.mjs +55 -19
- package/src/local/macos-trust-broker.mjs +7 -0
- package/src/local/managed-job-runner-claim.mjs +54 -0
- package/src/local/managed-job-runner.mjs +13 -2
- package/src/local/process-execution.mjs +2 -2
- package/src/local/process-identity.mjs +11 -0
- package/src/local/process-tree-ownership-types.d.ts +37 -0
- package/src/local/process-tree-ownership.mjs +49 -41
- package/src/local/process-tree.mjs +1 -1
- package/src/local/relay-call-recovery.mjs +40 -21
- package/src/local/runtime-activation.mjs +357 -38
- package/src/local/runtime-capabilities.mjs +22 -6
- package/src/local/runtime-diagnostics.mjs +9 -2
- package/src/local/runtime.mjs +18 -4
- package/src/local/service-convergence.mjs +33 -0
- package/src/local/service-owner.mjs +147 -0
- package/src/local/service-restart-handoff.mjs +22 -8
- package/src/local/service-runtime.mjs +145 -0
- package/src/local/service.mjs +143 -25
- package/src/local/state.mjs +104 -7
- package/src/local/stdio.mjs +139 -45
- package/src/local/system-network-route.mjs +76 -0
- package/src/local/tool-executor.mjs +24 -6
- package/src/local/tools.mjs +6 -5
- package/src/local/windows-service-convergence.mjs +49 -0
- package/src/local/windows-service.mjs +30 -53
- package/src/shared/mcp-protocol.d.mts +27 -0
- package/src/shared/mcp-protocol.mjs +256 -0
- package/src/shared/mcp-subscriptions.d.mts +4 -0
- package/src/shared/mcp-subscriptions.mjs +59 -0
- package/src/shared/relay-contract.json +1 -0
- package/src/shared/result-projection.d.mts +2 -1
- package/src/shared/result-projection.mjs +13 -2
- package/src/shared/server-metadata.json +11 -4
- package/src/shared/tool-argument-validation.d.mts +17 -0
- package/src/shared/tool-argument-validation.mjs +325 -0
- package/src/shared/tool-catalog.json +18 -12
- package/src/worker/durable-stream-calls.ts +12 -24
- package/src/worker/http.ts +36 -2
- package/src/worker/index.ts +181 -165
- package/src/worker/mcp-http-contract.ts +276 -0
- package/src/worker/mcp-jsonrpc.ts +12 -6
- package/src/worker/mcp-legacy-dispatch.ts +104 -0
- package/src/worker/mcp-modern-controller.ts +199 -0
- package/src/worker/mcp-modern-proxy.ts +126 -0
- package/src/worker/mcp-modern-stream.ts +71 -0
- package/src/worker/mcp-session.ts +12 -3
- package/src/worker/mcp-stream-proxy-contract.ts +67 -0
- package/src/worker/mcp-stream-proxy.ts +17 -60
- package/src/worker/mcp-tool-call-input.ts +23 -0
- package/src/worker/tool-catalog.ts +29 -1
- package/src/worker/tool-timeout.ts +53 -12
- package/src/worker/worker-mcp-config.ts +23 -0
- package/src/worker/worker-metadata.ts +10 -1
- package/src/worker/worker-runtime-config.ts +19 -0
- package/src/worker/worker-static-routes.ts +7 -2
- package/tsconfig.local.json +7 -1
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { createMonotonicDeadline } from "./monotonic-deadline.mjs";
|
|
4
|
+
import { createExclusiveFileSync, replaceFileAtomicallySync } from "./exclusive-file.mjs";
|
|
5
|
+
import { ownerOnlyFile } from "./state.mjs";
|
|
6
|
+
import { readBoundedFile } from "./managed-job-storage.mjs";
|
|
7
|
+
|
|
8
|
+
const RUNNER_CLAIM_BYTES = 1024;
|
|
9
|
+
const RUNNER_CLAIM_WAIT_MS = 30_000;
|
|
10
|
+
|
|
11
|
+
export function publishProvisionalRunnerClaim(dir, pid, launchToken) {
|
|
12
|
+
const file = join(dir, "runner.pid");
|
|
13
|
+
const claim = { pid, startedAt: new Date().toISOString(), launchToken };
|
|
14
|
+
try {
|
|
15
|
+
createExclusiveFileSync(file, `${JSON.stringify(claim)}\n`, { mode: 0o600 });
|
|
16
|
+
} catch (error) {
|
|
17
|
+
if (error?.code !== "EEXIST") throw error;
|
|
18
|
+
const existing = readRunnerClaim(file, "managed job runner claim already exists but is unreadable");
|
|
19
|
+
if (Number(existing?.pid) !== pid) throw new Error("managed job runner claim is owned by another process");
|
|
20
|
+
}
|
|
21
|
+
ownerOnlyFile(file);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function confirmRunnerClaim({ file, pid, processStartedAt, launchToken }) {
|
|
25
|
+
const exact = { pid, startedAt: new Date().toISOString(), processStartedAt };
|
|
26
|
+
if (!launchToken) {
|
|
27
|
+
createExclusiveFileSync(file, `${JSON.stringify(exact)}\n`, { mode: 0o600 });
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (!/^[a-f0-9]{32}$/.test(launchToken)) throw new Error("runner launch token is invalid");
|
|
31
|
+
const deadline = createMonotonicDeadline(RUNNER_CLAIM_WAIT_MS);
|
|
32
|
+
while (!deadline.expired()) {
|
|
33
|
+
if (!existsSync(file)) {
|
|
34
|
+
await new Promise((resolvePromise) => { setTimeout(resolvePromise, 10); });
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const provisional = readRunnerClaim(file, "runner ownership claim is unreadable");
|
|
38
|
+
if (Number(provisional?.pid) !== pid || provisional?.launchToken !== launchToken) {
|
|
39
|
+
throw new Error("runner ownership claim does not match the spawned process");
|
|
40
|
+
}
|
|
41
|
+
exact.startedAt = typeof provisional.startedAt === "string" && provisional.startedAt
|
|
42
|
+
? provisional.startedAt
|
|
43
|
+
: exact.startedAt;
|
|
44
|
+
replaceFileAtomicallySync(file, `${JSON.stringify(exact)}\n`, { mode: 0o600 });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
throw new Error("runner ownership claim was not published before startup deadline");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readRunnerClaim(file, message) {
|
|
51
|
+
try { return JSON.parse(readBoundedFile(file, RUNNER_CLAIM_BYTES).toString("utf8")); } catch {
|
|
52
|
+
throw new Error(message);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
2
3
|
import { closeSync } from "node:fs";
|
|
3
4
|
import { basename, join } from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -6,10 +7,12 @@ import { inspectProcessInstance } from "./process-identity.mjs";
|
|
|
6
7
|
import { classifyOperationalError } from "./log.mjs";
|
|
7
8
|
import { ownerOnlyFile } from "./state.mjs";
|
|
8
9
|
import { openPrivateAppendFile, readBoundedFile, trimDiagnosticFile } from "./managed-job-storage.mjs";
|
|
10
|
+
import { publishProvisionalRunnerClaim } from "./managed-job-runner-claim.mjs";
|
|
9
11
|
|
|
10
12
|
const RUNNER_PATH = fileURLToPath(new URL("./job-runner.mjs", import.meta.url));
|
|
11
13
|
|
|
12
14
|
export function launchRunner(dir, recover = false, recoveryToken = "", options = {}) {
|
|
15
|
+
const launchToken = randomBytes(16).toString("hex");
|
|
13
16
|
const args = [RUNNER_PATH, "--job-dir", dir];
|
|
14
17
|
if (recover) args.push("--recover");
|
|
15
18
|
const stdoutFile = join(dir, "runner.out.log");
|
|
@@ -28,7 +31,7 @@ export function launchRunner(dir, recover = false, recoveryToken = "", options =
|
|
|
28
31
|
stdio: ["ignore", stdoutFd, stderrFd],
|
|
29
32
|
windowsHide: true,
|
|
30
33
|
shell: false,
|
|
31
|
-
env: managedRunnerEnvironment({ fullEnv: options.fullEnv === true, recoveryToken, source: options.env || process.env }),
|
|
34
|
+
env: managedRunnerEnvironment({ fullEnv: options.fullEnv === true, recoveryToken, launchToken, source: options.env || process.env }),
|
|
32
35
|
});
|
|
33
36
|
} finally {
|
|
34
37
|
if (stdoutFd !== undefined) closeSync(stdoutFd);
|
|
@@ -46,6 +49,12 @@ export function launchRunner(dir, recover = false, recoveryToken = "", options =
|
|
|
46
49
|
});
|
|
47
50
|
const pid = Number(child.pid);
|
|
48
51
|
if (!Number.isInteger(pid) || pid <= 0) throw new Error("managed job runner did not receive a process id");
|
|
52
|
+
try {
|
|
53
|
+
publishProvisionalRunnerClaim(dir, pid, launchToken);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
try { child.kill?.("SIGKILL"); } catch {}
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
49
58
|
child.unref();
|
|
50
59
|
return pid;
|
|
51
60
|
}
|
|
@@ -72,7 +81,7 @@ function readRunnerOwner(dir, fallback = {}) {
|
|
|
72
81
|
}
|
|
73
82
|
}
|
|
74
83
|
|
|
75
|
-
export function managedRunnerEnvironment({ fullEnv = false, recoveryToken = "", source = process.env } = {}) {
|
|
84
|
+
export function managedRunnerEnvironment({ fullEnv = false, recoveryToken = "", launchToken = "", source = process.env } = {}) {
|
|
76
85
|
const env = fullEnv ? { ...source } : {};
|
|
77
86
|
if (!fullEnv) {
|
|
78
87
|
for (const key of ["PATH", "HOME", "USERPROFILE", "SystemRoot", "WINDIR", "COMSPEC", "PATHEXT", "LANG", "LC_ALL", "LC_CTYPE", "TMPDIR", "TEMP", "TMP"]) {
|
|
@@ -81,5 +90,7 @@ export function managedRunnerEnvironment({ fullEnv = false, recoveryToken = "",
|
|
|
81
90
|
}
|
|
82
91
|
if (recoveryToken) env.MBM_RECOVERY_LOCK_TOKEN = recoveryToken;
|
|
83
92
|
else delete env.MBM_RECOVERY_LOCK_TOKEN;
|
|
93
|
+
if (launchToken) env.MBM_RUNNER_LAUNCH_TOKEN = launchToken;
|
|
94
|
+
else delete env.MBM_RUNNER_LAUNCH_TOKEN;
|
|
84
95
|
return env;
|
|
85
96
|
}
|
|
@@ -78,9 +78,9 @@ export class ProcessExecutionService {
|
|
|
78
78
|
return publicProcessToolResult({ name: command.name, cwd: this.displayPath(cwd, context), timeout_seconds: timeoutSeconds, ...result });
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
async probeShell(context = {}) {
|
|
81
|
+
async probeShell(context = {}, timeoutMs = 5_000) {
|
|
82
82
|
const shell = workspaceShellCommand(process.platform === "win32" ? "cd" : "pwd");
|
|
83
|
-
return this.run(shell.cmd, shell.args,
|
|
83
|
+
return this.run(shell.cmd, shell.args, timeoutMs, true, 64 * 1024, context);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
async runFixedInternal(cmd, args, timeoutMs, allowFailure = false, maxOutputBytes = DEFAULT_PROCESS_OUTPUT_BYTES, context = {}, cwd = this.workspace) {
|
|
@@ -34,6 +34,16 @@ export function processStartTimeMs(pid) {
|
|
|
34
34
|
return result.ok ? parseTime(result.stdout) : null;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
export function processState(pid) {
|
|
38
|
+
const parsed = normalizePid(pid);
|
|
39
|
+
if (!parsed || process.platform === "win32") return "unknown";
|
|
40
|
+
const result = runBounded("ps", ["-p", String(parsed), "-o", "state="]);
|
|
41
|
+
if (!result.ok) return "unknown";
|
|
42
|
+
const state = result.stdout.trim().charAt(0).toUpperCase();
|
|
43
|
+
if (state === "Z") return "zombie";
|
|
44
|
+
return state ? "running" : "unknown";
|
|
45
|
+
}
|
|
46
|
+
|
|
37
47
|
export function processCommandLine(pid) {
|
|
38
48
|
const parsed = normalizePid(pid);
|
|
39
49
|
if (!parsed) return "";
|
|
@@ -123,6 +133,7 @@ function runBounded(command, args) {
|
|
|
123
133
|
const result = spawnSync(command, args, {
|
|
124
134
|
encoding: "utf8",
|
|
125
135
|
timeout: COMMAND_TIMEOUT_MS,
|
|
136
|
+
killSignal: "SIGKILL",
|
|
126
137
|
maxBuffer: COMMAND_OUTPUT_BYTES,
|
|
127
138
|
windowsHide: true,
|
|
128
139
|
env: process.platform === "win32" ? process.env : { ...process.env, LC_ALL: "C", LANG: "C" },
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface ChildProcessIdentity {
|
|
2
|
+
pid?: unknown;
|
|
3
|
+
exitCode?: unknown;
|
|
4
|
+
signalCode?: unknown;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface ProcessGroupEntry {
|
|
8
|
+
pid: number;
|
|
9
|
+
pgid: number;
|
|
10
|
+
startedAt: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ProcessOwnershipMember {
|
|
14
|
+
pid: number;
|
|
15
|
+
startedAt: number | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ProcessOwnershipSnapshot {
|
|
19
|
+
platform: string;
|
|
20
|
+
pid: number;
|
|
21
|
+
members: ProcessOwnershipMember[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ProcessSnapshotResult {
|
|
25
|
+
error?: unknown;
|
|
26
|
+
status?: number | null;
|
|
27
|
+
stdout?: string | Buffer;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ProcessOwnershipOptions {
|
|
31
|
+
platform?: string;
|
|
32
|
+
listProcessGroups?: (options: ProcessOwnershipOptions, pid: number, timeoutMs: number) => ProcessGroupEntry[];
|
|
33
|
+
spawnSyncProcess?: (command: string, args: string[], options: Record<string, unknown>) => ProcessSnapshotResult;
|
|
34
|
+
ownershipCheckBudgetMs?: unknown;
|
|
35
|
+
processSnapshotTimeoutMs?: unknown;
|
|
36
|
+
monotonicNow?: () => number;
|
|
37
|
+
}
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
1
3
|
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { createMonotonicDeadline } from "./monotonic-deadline.mjs";
|
|
2
5
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
+
export const DEFAULT_PROCESS_OWNERSHIP_CHECK_BUDGET_MS = 3000;
|
|
7
|
+
/** @typedef {import("./process-tree-ownership-types.d.ts").ChildProcessIdentity} ChildProcessIdentity */
|
|
8
|
+
/** @typedef {import("./process-tree-ownership-types.d.ts").ProcessGroupEntry} ProcessGroupEntry */
|
|
9
|
+
/** @typedef {import("./process-tree-ownership-types.d.ts").ProcessOwnershipMember} ProcessOwnershipMember */
|
|
10
|
+
/** @typedef {import("./process-tree-ownership-types.d.ts").ProcessOwnershipSnapshot} ProcessOwnershipSnapshot */
|
|
11
|
+
/** @typedef {import("./process-tree-ownership-types.d.ts").ProcessOwnershipOptions} ProcessOwnershipOptions */
|
|
6
12
|
|
|
7
|
-
export function captureProcessTreeOwnership(child, options = {}) {
|
|
13
|
+
export function captureProcessTreeOwnership(/** @type {ChildProcessIdentity} */ child, /** @type {ProcessOwnershipOptions} */ options = {}) {
|
|
8
14
|
const pid = positivePid(child?.pid);
|
|
9
15
|
const platform = String(options.platform || process.platform);
|
|
10
16
|
if (!pid) return { platform, pid: 0, members: [] };
|
|
@@ -12,7 +18,7 @@ export function captureProcessTreeOwnership(child, options = {}) {
|
|
|
12
18
|
return { platform, pid, members: processGroupMembers(pid, options) };
|
|
13
19
|
}
|
|
14
20
|
|
|
15
|
-
export function refreshProcessTreeOwnership(snapshot, options = {}) {
|
|
21
|
+
export function refreshProcessTreeOwnership(/** @type {ProcessOwnershipSnapshot | null | undefined} */ snapshot, /** @type {ProcessOwnershipOptions} */ options = {}) {
|
|
16
22
|
if (!snapshot?.pid || snapshot.platform === "win32") return snapshot;
|
|
17
23
|
const members = [...(snapshot.members || [])];
|
|
18
24
|
for (const observed of processGroupMembers(snapshot.pid, options)) {
|
|
@@ -21,51 +27,53 @@ export function refreshProcessTreeOwnership(snapshot, options = {}) {
|
|
|
21
27
|
return { ...snapshot, members };
|
|
22
28
|
}
|
|
23
29
|
|
|
24
|
-
export function processTreeOwnershipStillCurrent(snapshot, child, options = {}) {
|
|
30
|
+
export function processTreeOwnershipStillCurrent(/** @type {ProcessOwnershipSnapshot | null | undefined} */ snapshot, /** @type {ChildProcessIdentity} */ child, /** @type {ProcessOwnershipOptions} */ options = {}) {
|
|
25
31
|
if (!snapshot?.pid) return false;
|
|
26
32
|
if (snapshot.platform === "win32") return !childHasExited(child);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
const members = Array.isArray(snapshot.members) ? snapshot.members : [];
|
|
34
|
+
if (members.length === 0) return false;
|
|
35
|
+
const budget = createSnapshotBudget(options);
|
|
36
|
+
const fullSnapshotTimeoutMs = budget.take(members.length + 1);
|
|
37
|
+
if (!fullSnapshotTimeoutMs) return false;
|
|
38
|
+
const current = processGroupMembers(snapshot.pid, options, 0, fullSnapshotTimeoutMs);
|
|
39
|
+
if (members.some((expected) => current.some((observed) => sameProcessIdentity(expected, observed)))) return true;
|
|
40
|
+
for (let index = 0; index < members.length; index += 1) {
|
|
41
|
+
const timeoutMs = budget.take(members.length - index);
|
|
42
|
+
if (!timeoutMs) return false;
|
|
43
|
+
const expected = members[index];
|
|
44
|
+
if (processGroupMembers(snapshot.pid, options, expected.pid, timeoutMs).some((observed) => sameProcessIdentity(expected, observed))) return true;
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
32
47
|
}
|
|
33
48
|
|
|
34
|
-
function processGroupMembers(groupId, options = {}, pid = 0) {
|
|
35
|
-
const
|
|
36
|
-
|
|
49
|
+
function processGroupMembers(/** @type {number} */ groupId, /** @type {ProcessOwnershipOptions} */ options = {}, /** @type {number} */ pid = 0, /** @type {number} */ timeoutMs = snapshotTimeout(options)) {
|
|
50
|
+
const entries = typeof options.listProcessGroups === "function"
|
|
51
|
+
? options.listProcessGroups(options, pid, timeoutMs) : listProcessGroups(options, pid, timeoutMs, groupId);
|
|
52
|
+
return entries.filter((entry) => entry.pgid === groupId).map(({ pid: memberPid, startedAt }) => ({ pid: memberPid, startedAt }));
|
|
37
53
|
}
|
|
38
54
|
|
|
39
|
-
function listProcessGroups(options = {}, pid = 0) {
|
|
40
|
-
const run = typeof options.spawnSyncProcess === "function" ? options.spawnSyncProcess :
|
|
41
|
-
const
|
|
55
|
+
function listProcessGroups(/** @type {ProcessOwnershipOptions} */ options = {}, /** @type {number} */ pid = 0, /** @type {number} */ timeoutMs = snapshotTimeout(options), /** @type {number} */ groupId = 0) {
|
|
56
|
+
const run = typeof options.spawnSyncProcess === "function" ? options.spawnSyncProcess : defaultSpawnSyncProcess;
|
|
57
|
+
const platform = String(options.platform || process.platform);
|
|
58
|
+
const args = pid ? ["-p", String(pid), "-o", "pid=,pgid=,lstart="]
|
|
59
|
+
: platform === "darwin" ? ["-g", String(groupId), "-o", "pid=,pgid=,lstart="] : ["-axo", "pid=,pgid=,lstart="];
|
|
42
60
|
const result = run("ps", args, {
|
|
43
|
-
encoding: "utf8",
|
|
44
|
-
|
|
45
|
-
maxBuffer: PROCESS_SNAPSHOT_BYTES,
|
|
46
|
-
windowsHide: true,
|
|
47
|
-
env: { ...process.env, LC_ALL: "C", LANG: "C" },
|
|
48
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
61
|
+
encoding: "utf8", timeout: timeoutMs, killSignal: "SIGKILL", maxBuffer: 512 * 1024,
|
|
62
|
+
windowsHide: true, env: { ...process.env, LC_ALL: "C", LANG: "C" }, stdio: ["ignore", "pipe", "ignore"],
|
|
49
63
|
});
|
|
50
64
|
if (result?.error || result?.status !== 0) return [];
|
|
51
|
-
return String(result.stdout || "").split(/\r?\n/).map(parseProcessRow).filter(
|
|
65
|
+
return String(result.stdout || "").split(/\r?\n/).map(parseProcessRow).filter(isProcessGroupEntry);
|
|
52
66
|
}
|
|
53
67
|
|
|
54
|
-
function
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
const pid = positivePid(match[1]);
|
|
58
|
-
const pgid = positivePid(match[2]);
|
|
59
|
-
const startedAt = Date.parse(match[3]);
|
|
60
|
-
return pid && pgid && Number.isFinite(startedAt) ? { pid, pgid, startedAt } : null;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function sameProcessIdentity(left, right) {
|
|
64
|
-
return left.pid === right.pid && Number.isFinite(left.startedAt) && Number.isFinite(right.startedAt)
|
|
65
|
-
&& Math.abs(left.startedAt - right.startedAt) <= START_TIME_TOLERANCE_MS;
|
|
66
|
-
}
|
|
67
|
-
function positivePid(value) { const parsed = Number(value); return Number.isInteger(parsed) && parsed > 0 ? parsed : 0; }
|
|
68
|
-
function childHasExited(child) {
|
|
69
|
-
return child?.exitCode !== null && child?.exitCode !== undefined
|
|
70
|
-
|| child?.signalCode !== null && child?.signalCode !== undefined;
|
|
68
|
+
function defaultSpawnSyncProcess(/** @type {string} */ command, /** @type {string[]} */ args, /** @type {Record<string, unknown>} */ options) {
|
|
69
|
+
const result = spawnSync(command, args, /** @type {import("node:child_process").SpawnSyncOptionsWithStringEncoding} */ (/** @type {unknown} */ (options)));
|
|
70
|
+
return { error: result.error, status: result.status, stdout: result.stdout };
|
|
71
71
|
}
|
|
72
|
+
function createSnapshotBudget(/** @type {ProcessOwnershipOptions} */ options) { const total = boundedPositive(options.ownershipCheckBudgetMs, DEFAULT_PROCESS_OWNERSHIP_CHECK_BUDGET_MS); const deadline = createMonotonicDeadline(total, options.monotonicNow); let unallocated = total; return { take(/** @type {number} */ slots) { const remaining = Math.min(unallocated, Math.floor(deadline.remainingMs())); if (remaining < 1) return 0; const value = Math.max(1, Math.min(DEFAULT_PROCESS_OWNERSHIP_CHECK_BUDGET_MS, Math.floor(remaining / Math.max(1, slots)))); unallocated -= value; return value; } }; }
|
|
73
|
+
function snapshotTimeout(/** @type {ProcessOwnershipOptions} */ options) { return boundedPositive(options.processSnapshotTimeoutMs, DEFAULT_PROCESS_OWNERSHIP_CHECK_BUDGET_MS); }
|
|
74
|
+
function boundedPositive(/** @type {unknown} */ value, /** @type {number} */ fallback) { const parsed = Number(value); return Number.isFinite(parsed) && parsed >= 1 ? Math.min(DEFAULT_PROCESS_OWNERSHIP_CHECK_BUDGET_MS, Math.floor(parsed)) : fallback; }
|
|
75
|
+
function parseProcessRow(/** @type {unknown} */ line) { const match = /^\s*(\d+)\s+(\d+)\s+(.+?)\s*$/.exec(String(line || "")); if (!match) return null; const pid = positivePid(match[1]); const pgid = positivePid(match[2]); const startedAt = Date.parse(match[3]); return pid && pgid && Number.isFinite(startedAt) ? { pid, pgid, startedAt } : null; }
|
|
76
|
+
function isProcessGroupEntry(/** @type {ProcessGroupEntry | null} */ value) { return value !== null; }
|
|
77
|
+
function sameProcessIdentity(/** @type {ProcessOwnershipMember} */ left, /** @type {ProcessOwnershipMember} */ right) { return left.pid === right.pid && typeof left.startedAt === "number" && Number.isFinite(left.startedAt) && typeof right.startedAt === "number" && Number.isFinite(right.startedAt) && left.startedAt === right.startedAt; }
|
|
78
|
+
function positivePid(/** @type {unknown} */ value) { const parsed = Number(value); return Number.isInteger(parsed) && parsed > 0 ? parsed : 0; }
|
|
79
|
+
function childHasExited(/** @type {ChildProcessIdentity} */ child) { return child?.exitCode !== null && child?.exitCode !== undefined || child?.signalCode !== null && child?.signalCode !== undefined; }
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
processTreeOwnershipStillCurrent,
|
|
5
5
|
refreshProcessTreeOwnership,
|
|
6
6
|
} from "./process-tree-ownership.mjs";
|
|
7
|
-
export { captureProcessTreeOwnership, processTreeOwnershipStillCurrent, refreshProcessTreeOwnership } from "./process-tree-ownership.mjs";
|
|
7
|
+
export { DEFAULT_PROCESS_OWNERSHIP_CHECK_BUDGET_MS, captureProcessTreeOwnership, processTreeOwnershipStillCurrent, refreshProcessTreeOwnership } from "./process-tree-ownership.mjs";
|
|
8
8
|
|
|
9
9
|
export const DEFAULT_PROCESS_TERMINATION_GRACE_MS = 2000;
|
|
10
10
|
|
|
@@ -43,28 +43,36 @@ export class RelayCallRecovery {
|
|
|
43
43
|
/** @param {RelayResult} response */
|
|
44
44
|
deliver(response) {
|
|
45
45
|
const callId = String(response?.id || "");
|
|
46
|
-
if (this.send(response)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
this.pendingResults.set(callId, response);
|
|
52
|
-
this.scheduleExpiry();
|
|
53
|
-
this.logger.event?.("debug", "relay.tool_result.queued", {
|
|
54
|
-
call_id: shortCallId(callId), queued_results: this.pendingResults.size,
|
|
55
|
-
}, "Queued a completed tool result while the relay reconnects");
|
|
46
|
+
if (!callId) return this.send(response);
|
|
47
|
+
if (!this.isRecoverable()) {
|
|
48
|
+
this.logger.event?.("debug", "relay.tool_result.discarded", {
|
|
49
|
+
call_id: shortCallId(callId), reason: "transport_unavailable",
|
|
50
|
+
}, "Discarded a tool result because the relay is no longer recoverable");
|
|
56
51
|
return false;
|
|
57
52
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
53
|
+
|
|
54
|
+
// Retain sent results until the Worker commits and acknowledges them.
|
|
55
|
+
this.pendingResults.set(callId, response);
|
|
56
|
+
const sent = this.send(response);
|
|
57
|
+
if (sent) {
|
|
58
|
+
this.logger.event?.("debug", "relay.tool_result.awaiting_ack", {
|
|
59
|
+
call_id: shortCallId(callId), unacknowledged_results: this.pendingResults.size,
|
|
60
|
+
}, "Delivered a tool result and retained it until Worker acknowledgement");
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.scheduleExpiry();
|
|
65
|
+
this.logger.event?.("debug", "relay.tool_result.queued", {
|
|
66
|
+
call_id: shortCallId(callId), queued_results: this.pendingResults.size,
|
|
67
|
+
}, "Queued a completed tool result while the relay reconnects");
|
|
61
68
|
return false;
|
|
62
69
|
}
|
|
63
70
|
|
|
64
71
|
/** @param {unknown} callId */
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
72
|
+
acknowledge(callId) { return this.pendingResults.delete(String(callId)); }
|
|
73
|
+
|
|
74
|
+
/** @param {unknown} callId */
|
|
75
|
+
discard(callId) { return this.pendingResults.delete(String(callId)); }
|
|
68
76
|
|
|
69
77
|
/** @param {Iterable<string>} resumedCallIds @param {(callId: string) => boolean} cancelCall */
|
|
70
78
|
reconcile(resumedCallIds, cancelCall) {
|
|
@@ -94,18 +102,29 @@ export class RelayCallRecovery {
|
|
|
94
102
|
|
|
95
103
|
ready() {
|
|
96
104
|
this.clearTimer();
|
|
105
|
+
this.retryUnacknowledged("reconnected");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
pulse() {
|
|
109
|
+
this.retryUnacknowledged("heartbeat");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** @param {string} reason */
|
|
113
|
+
retryUnacknowledged(reason) {
|
|
97
114
|
let delivered = 0;
|
|
98
|
-
for (const
|
|
115
|
+
for (const response of this.pendingResults.values()) {
|
|
99
116
|
if (!this.send(response)) {
|
|
100
117
|
this.scheduleExpiry();
|
|
101
118
|
break;
|
|
102
119
|
}
|
|
103
|
-
this.pendingResults.delete(callId);
|
|
104
120
|
delivered += 1;
|
|
105
121
|
}
|
|
106
122
|
if (delivered > 0) {
|
|
107
|
-
this.logger.event?.("info", "relay.tool_results.replayed", {
|
|
108
|
-
|
|
123
|
+
this.logger.event?.(reason === "reconnected" ? "info" : "debug", "relay.tool_results.replayed", {
|
|
124
|
+
delivered_results: delivered,
|
|
125
|
+
reason,
|
|
126
|
+
unacknowledged_results: this.pendingResults.size,
|
|
127
|
+
}, "Replayed completed tool results awaiting Worker acknowledgement");
|
|
109
128
|
}
|
|
110
129
|
}
|
|
111
130
|
|
|
@@ -122,7 +141,7 @@ export class RelayCallRecovery {
|
|
|
122
141
|
const cancelled = this.cancelOrigin("remote relay reconnect grace expired");
|
|
123
142
|
const discarded = this.pendingResults.size;
|
|
124
143
|
this.pendingResults.clear();
|
|
125
|
-
this.terminate();
|
|
144
|
+
if (cancelled > 0) this.terminate();
|
|
126
145
|
if (cancelled > 0 || discarded > 0) {
|
|
127
146
|
this.logger.warn?.(`remote relay did not recover within ${this.graceMs / 1000} seconds; cancelled ${cancelled} call(s) and discarded ${discarded} queued result(s)`);
|
|
128
147
|
}
|