@agimon-ai/po-runner 0.0.0-alpha.60 → 0.0.0-alpha.61
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/bin/cli.cjs +1 -1
- package/dist/bin/cli.mjs +1 -1
- package/dist/bin/runnerHost.cjs +28 -15
- package/dist/bin/runnerHost.cjs.map +1 -1
- package/dist/bin/runnerHost.d.mts.map +1 -1
- package/dist/bin/runnerHost.mjs +28 -15
- package/dist/bin/runnerHost.mjs.map +1 -1
- package/dist/extensions/mcp.cjs +1 -0
- package/dist/extensions/mcp.cjs.map +1 -1
- package/dist/extensions/mcp.mjs +1 -0
- package/dist/extensions/mcp.mjs.map +1 -1
- package/dist/extensions/server.cjs +11 -5
- package/dist/extensions/server.cjs.map +1 -1
- package/dist/extensions/server.mjs +11 -5
- package/dist/extensions/server.mjs.map +1 -1
- package/dist/index-L4iX9nzL.d.mts.map +1 -1
- package/dist/index.cjs +2 -2
- package/dist/index.mjs +2 -2
- package/dist/{rmuxBackend-DazdRaxC.mjs → rmuxBackend-B-7aY8Kc.mjs} +31 -13
- package/dist/rmuxBackend-B-7aY8Kc.mjs.map +1 -0
- package/dist/{rmuxBackend-D4_-qOQz.cjs → rmuxBackend-CPVm0YUN.cjs} +31 -13
- package/dist/rmuxBackend-CPVm0YUN.cjs.map +1 -0
- package/dist/rmuxBackend.cjs +1 -1
- package/dist/rmuxBackend.mjs +1 -1
- package/dist/{runnerDependencies-BBjsrtTH.cjs → runnerDependencies-BjbN8BAO.cjs} +40 -21
- package/dist/runnerDependencies-BjbN8BAO.cjs.map +1 -0
- package/dist/{runnerDependencies-CVRUC9Gw.mjs → runnerDependencies-DKMpDyqi.mjs} +40 -21
- package/dist/runnerDependencies-DKMpDyqi.mjs.map +1 -0
- package/dist/runnerDependencies.cjs +1 -1
- package/dist/runnerDependencies.mjs +1 -1
- package/dist/{runnerPaths-bzlBTHKU.mjs → runnerPaths-DtT9RJxQ.mjs} +10 -12
- package/dist/runnerPaths-DtT9RJxQ.mjs.map +1 -0
- package/dist/{runnerPaths-NGHLeATQ.cjs → runnerPaths-sdEEgdM8.cjs} +9 -11
- package/dist/runnerPaths-sdEEgdM8.cjs.map +1 -0
- package/dist/runnerPaths.cjs +1 -1
- package/dist/runnerPaths.d.mts.map +1 -1
- package/dist/runnerPaths.mjs +1 -1
- package/package.json +17 -17
- package/dist/rmuxBackend-D4_-qOQz.cjs.map +0 -1
- package/dist/rmuxBackend-DazdRaxC.mjs.map +0 -1
- package/dist/runnerDependencies-BBjsrtTH.cjs.map +0 -1
- package/dist/runnerDependencies-CVRUC9Gw.mjs.map +0 -1
- package/dist/runnerPaths-NGHLeATQ.cjs.map +0 -1
- package/dist/runnerPaths-bzlBTHKU.mjs.map +0 -1
package/dist/bin/cli.cjs
CHANGED
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
3
3
|
const require_runnerSequenceStore = require("../runnerSequenceStore-Ck6M-8r_.cjs");
|
|
4
4
|
const require_cliApp = require("../cliApp-GKcqD6CT.cjs");
|
|
5
5
|
const require_session = require("../session-D_nH4XhL.cjs");
|
|
6
|
-
const require_runnerDependencies = require("../runnerDependencies-
|
|
6
|
+
const require_runnerDependencies = require("../runnerDependencies-BjbN8BAO.cjs");
|
|
7
7
|
let node_fs_promises = require("node:fs/promises");
|
|
8
8
|
let node_url = require("node:url");
|
|
9
9
|
let _agimon_ai_po_core_telemetry = require("@agimon-ai/po-core/telemetry");
|
package/dist/bin/cli.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { t as createRunnerSequenceStore } from "../runnerSequenceStore-CaY7S_Sd.mjs";
|
|
3
3
|
import { n as runnerCliHelp, t as runCli } from "../cliApp-g7APOwcU.mjs";
|
|
4
4
|
import { t as inheritedSessionId } from "../session-3oSU3QjI.mjs";
|
|
5
|
-
import { t as createRunnerDependencies } from "../runnerDependencies-
|
|
5
|
+
import { t as createRunnerDependencies } from "../runnerDependencies-DKMpDyqi.mjs";
|
|
6
6
|
import { realpath } from "node:fs/promises";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { createPoTelemetry } from "@agimon-ai/po-core/telemetry";
|
package/dist/bin/runnerHost.cjs
CHANGED
|
@@ -13,6 +13,11 @@ node_os = require_rolldown_runtime.__toESM(node_os, 1);
|
|
|
13
13
|
const POLL_MS = 10;
|
|
14
14
|
const FORWARDED_SIGNAL_GRACE_MS = 2e3;
|
|
15
15
|
const INVALID_COMMAND_SPEC_ERROR = "invalid runner command spec";
|
|
16
|
+
const OWNER_LOST_EXIT = {
|
|
17
|
+
reason: "stopped",
|
|
18
|
+
terminationReason: "owner_lost",
|
|
19
|
+
stopReason: "owner session ended"
|
|
20
|
+
};
|
|
16
21
|
async function main(argv = process.argv.slice(2)) {
|
|
17
22
|
if (argv[0] === "--observe-sessions") {
|
|
18
23
|
require_services_lifeline_client.observeSessions(cleanupSessions);
|
|
@@ -21,11 +26,24 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
21
26
|
const [specPath, gatePath, exitPath] = argv;
|
|
22
27
|
if (!specPath || !gatePath || !exitPath) throw new Error("runnerHost requires spec, gate, and exit paths");
|
|
23
28
|
const spec = await readSpec(specPath);
|
|
24
|
-
|
|
29
|
+
let ownerLost = false;
|
|
30
|
+
let onOwnerLost = () => void 0;
|
|
31
|
+
const detachLifeline = require_services_lifeline_client.watchOwner(() => {
|
|
32
|
+
ownerLost = true;
|
|
33
|
+
onOwnerLost();
|
|
34
|
+
}, spec.env[require_services_lifeline_client.LIFELINE_ENV]);
|
|
35
|
+
if (!await waitForGate(gatePath, () => ownerLost)) {
|
|
36
|
+
detachLifeline();
|
|
37
|
+
await writeAtomic(exitPath, {
|
|
38
|
+
code: null,
|
|
39
|
+
signal: null,
|
|
40
|
+
...OWNER_LOST_EXIT
|
|
41
|
+
});
|
|
42
|
+
return 1;
|
|
43
|
+
}
|
|
25
44
|
let child;
|
|
26
45
|
let forwardedSignal = null;
|
|
27
46
|
let escalationTimer;
|
|
28
|
-
let ownerLost = false;
|
|
29
47
|
const signalChild = (signal) => {
|
|
30
48
|
if (!child?.pid) return;
|
|
31
49
|
try {
|
|
@@ -52,10 +70,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
52
70
|
detached: true
|
|
53
71
|
});
|
|
54
72
|
if (forwardedSignal) forward(forwardedSignal);
|
|
55
|
-
|
|
56
|
-
ownerLost = true;
|
|
57
|
-
forward("SIGTERM");
|
|
58
|
-
}, spec.env[require_services_lifeline_client.LIFELINE_ENV]);
|
|
73
|
+
onOwnerLost = () => forward("SIGTERM");
|
|
59
74
|
try {
|
|
60
75
|
const result = await new Promise((resolve, reject) => {
|
|
61
76
|
child?.once("error", reject);
|
|
@@ -66,11 +81,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
66
81
|
});
|
|
67
82
|
await writeAtomic(exitPath, {
|
|
68
83
|
...result,
|
|
69
|
-
...ownerLost ? {
|
|
70
|
-
reason: "stopped",
|
|
71
|
-
terminationReason: "owner_lost",
|
|
72
|
-
stopReason: "owner session ended"
|
|
73
|
-
} : {}
|
|
84
|
+
...ownerLost ? OWNER_LOST_EXIT : {}
|
|
74
85
|
});
|
|
75
86
|
return result.code !== null ? result.code : result.signal ? 128 + node_os.default.constants.signals[result.signal] : 1;
|
|
76
87
|
} finally {
|
|
@@ -89,16 +100,18 @@ async function readSpec(target) {
|
|
|
89
100
|
env: spec.env
|
|
90
101
|
};
|
|
91
102
|
}
|
|
92
|
-
|
|
93
|
-
|
|
103
|
+
/** False once the owner is gone, since only the owner opens the gate. */
|
|
104
|
+
async function waitForGate(target, abandoned) {
|
|
105
|
+
while (!abandoned()) {
|
|
94
106
|
try {
|
|
95
107
|
await node_fs_promises.default.access(target);
|
|
96
|
-
return;
|
|
108
|
+
return true;
|
|
97
109
|
} catch (error) {
|
|
98
110
|
if (error.code !== "ENOENT") throw error;
|
|
99
111
|
}
|
|
100
112
|
await new Promise((resolve) => setTimeout(resolve, POLL_MS));
|
|
101
113
|
}
|
|
114
|
+
return false;
|
|
102
115
|
}
|
|
103
116
|
async function writeAtomic(target, value) {
|
|
104
117
|
const temporary = `${target}.${process.pid}.tmp`;
|
|
@@ -149,7 +162,7 @@ async function readMarker(marker) {
|
|
|
149
162
|
async function loadCleaner() {
|
|
150
163
|
const [{ cleanupRunnerSession }, { RunnerPaths }, { createDefaultProcessRegistry }] = await Promise.all([
|
|
151
164
|
Promise.resolve().then(() => require("../runnerHost-BXk4sPBP.cjs")).then((n) => n.runnerHost_exports),
|
|
152
|
-
Promise.resolve().then(() => require("../runnerPaths-
|
|
165
|
+
Promise.resolve().then(() => require("../runnerPaths-sdEEgdM8.cjs")).then((n) => n.runnerPaths_exports),
|
|
153
166
|
Promise.resolve().then(() => require("../runnerRegistry-BucrRJsG.cjs")).then((n) => n.runnerRegistry_exports)
|
|
154
167
|
]);
|
|
155
168
|
const registries = /* @__PURE__ */ new Map();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runnerHost.cjs","names":["spawn","watchOwner","LIFELINE_ENV","os","fs","pathToFileURL","REGISTRY_PATH_ENV"],"sources":["../../src/bin/runnerHost.ts"],"sourcesContent":["import { type ChildProcess, spawn } from 'node:child_process';\nimport fs from 'node:fs/promises';\nimport os from 'node:os';\nimport { pathToFileURL } from 'node:url';\n\nimport type { PoTelemetry } from '@agimon-ai/po-core/telemetry';\n\nimport { OBSERVE_SESSIONS_FLAG } from '../constants/lifeline';\nimport { REGISTRY_PATH_ENV } from '../constants/runnerRegistry';\nimport { LIFELINE_ENV, observeSessions, watchOwner } from '../services/lifeline/client';\nimport type { ProcessRegistryPort } from '../services/runnerRegistry';\nimport type { LifelineWatch } from '../types/lifeline';\n\n// Everything heavier loads on first use: the observer idles for its owner's whole life and\n// these imports alone would more than double its memory. A command's host loads none of it:\n// the server records the runner when it completes the record from this host's exit file.\n\ninterface CommandSpec {\n command: string;\n cwd: string;\n env: Record<string, string>;\n}\n\nconst POLL_MS = 10;\nconst FORWARDED_SIGNAL_GRACE_MS = 2_000;\nconst INVALID_COMMAND_SPEC_ERROR = 'invalid runner command spec';\n\nexport async function main(argv: readonly string[] = process.argv.slice(2)): Promise<number> {\n if (argv[0] === OBSERVE_SESSIONS_FLAG) {\n observeSessions(cleanupSessions);\n return 0;\n }\n const [specPath, gatePath, exitPath] = argv;\n if (!specPath || !gatePath || !exitPath) throw new Error('runnerHost requires spec, gate, and exit paths');\n const spec = await readSpec(specPath);\n await waitForGate(gatePath);\n let child: ChildProcess | undefined;\n let forwardedSignal: NodeJS.Signals | null = null;\n let escalationTimer: NodeJS.Timeout | undefined;\n let ownerLost = false;\n const signalChild = (signal: NodeJS.Signals): void => {\n if (!child?.pid) return;\n try {\n process.kill(-child.pid, signal);\n } catch {\n child.kill(signal);\n }\n };\n const forward = (signal: NodeJS.Signals): void => {\n forwardedSignal = signal;\n signalChild(signal);\n escalationTimer ??= setTimeout(() => signalChild('SIGKILL'), FORWARDED_SIGNAL_GRACE_MS);\n };\n const signals: NodeJS.Signals[] = ['SIGTERM', 'SIGINT', 'SIGHUP'];\n for (const signal of signals) process.once(signal, () => forward(signal));\n\n child = spawn(spec.command, {\n cwd: spec.cwd,\n env: spec.env,\n shell: '/bin/bash',\n stdio: 'inherit',\n detached: true,\n });\n if (forwardedSignal) forward(forwardedSignal);\n\n // This process supervises the command for its whole life, so it is also the\n // right place to notice an owner that died without stopping it.\n //\n // The lifeline comes from the spec rather than this process's environment: an\n // rmux server outlives the session that started it, so its own environment can\n // still name a previous session's socket.\n const detachLifeline = watchOwner(() => {\n ownerLost = true;\n forward('SIGTERM');\n }, spec.env[LIFELINE_ENV]);\n\n try {\n const result = await new Promise<{ code: number | null; signal: NodeJS.Signals | null }>((resolve, reject) => {\n child?.once('error', reject);\n child?.once('exit', (code, signal) => resolve({ code, signal: signal ?? forwardedSignal }));\n });\n await writeAtomic(exitPath, {\n ...result,\n ...(ownerLost ? { reason: 'stopped', terminationReason: 'owner_lost', stopReason: 'owner session ended' } : {}),\n });\n return result.code !== null ? result.code : result.signal ? 128 + os.constants.signals[result.signal] : 1;\n } finally {\n detachLifeline();\n if (escalationTimer) clearTimeout(escalationTimer);\n }\n}\n\nasync function readSpec(target: string): Promise<CommandSpec> {\n const value: unknown = JSON.parse(await fs.readFile(target, 'utf8'));\n if (typeof value !== 'object' || value === null) throw new Error(INVALID_COMMAND_SPEC_ERROR);\n const spec = value as Partial<CommandSpec>;\n if (typeof spec.command !== 'string' || typeof spec.cwd !== 'string' || !spec.env) {\n throw new Error(INVALID_COMMAND_SPEC_ERROR);\n }\n return { command: spec.command, cwd: spec.cwd, env: spec.env };\n}\n\nasync function waitForGate(target: string): Promise<void> {\n for (;;) {\n try {\n await fs.access(target);\n return;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;\n }\n await new Promise((resolve) => setTimeout(resolve, POLL_MS));\n }\n}\n\nasync function writeAtomic(target: string, value: unknown): Promise<void> {\n const temporary = `${target}.${process.pid}.tmp`;\n await fs.writeFile(temporary, `${JSON.stringify(value)}\\n`, { mode: 0o600 });\n await fs.rename(temporary, target);\n}\n\nif (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {\n main().then(\n (code) => {\n process.exitCode = code;\n },\n (error: unknown) => {\n process.stderr.write(`${error instanceof Error ? error.message : String(error)}\\n`);\n process.exitCode = 1;\n },\n );\n}\n\n/** Loaded on the first session an owner leaves behind, then shared by every later one. */\nlet cleaner: Promise<(watch: LifelineWatch) => Promise<void>> | undefined;\nlet telemetry: Promise<PoTelemetry> | undefined;\n\n/**\n * A separate process survives SIGKILL of the owner, even if that session has no active runners.\n * The owner cleans up each session it ends with its own shared services and removes its marker,\n * so only a session still marked with its generation is left here; the rest load nothing.\n */\nasync function cleanupSessions(watches: readonly LifelineWatch[]): Promise<void> {\n const failures: { watch: LifelineWatch; error: unknown }[] = [];\n // One session failing to clean up must not stop the others.\n for (const watch of watches) {\n try {\n // The same fence cleanupRunnerSession applies, checked before anything loads for it.\n if ((await readMarker(watch.marker)) !== watch.generation) continue;\n const clean = await (cleaner ??= loadCleaner());\n await clean(watch);\n } catch (error) {\n failures.push({ watch, error });\n }\n }\n const [first] = failures;\n if (!first) return;\n const log = await (telemetry ??= loadTelemetry(first.watch.environment));\n for (const { watch, error } of failures) {\n await log.recordError('po_runner.observer_cleanup_failed', error, { 'session.id': watch.sessionId });\n }\n await log.flush();\n throw new AggregateError(\n failures.map(({ error }) => error),\n `Could not clean up runner sessions ${failures.map(({ watch }) => watch.sessionId).join(', ')}`,\n );\n}\n\nasync function readMarker(marker: string): Promise<string | undefined> {\n try {\n return await fs.readFile(marker, 'utf8');\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined;\n throw error;\n }\n}\n\n/** One registry per registry path, held until the observer runs out of work. */\nasync function loadCleaner(): Promise<(watch: LifelineWatch) => Promise<void>> {\n const [{ cleanupRunnerSession }, { RunnerPaths }, { createDefaultProcessRegistry }] = await Promise.all([\n import('../services/runnerHost'),\n import('../services/runnerPaths'),\n import('../services/runnerRegistry'),\n ]);\n const registries = new Map<string | undefined, ProcessRegistryPort>();\n process.once('beforeExit', () => {\n for (const registry of registries.values()) registry.close();\n });\n return (watch) => {\n const registryPath = watch.environment[REGISTRY_PATH_ENV];\n let registry = registries.get(registryPath);\n if (!registry) {\n registry = createDefaultProcessRegistry(watch.environment);\n registries.set(registryPath, registry);\n }\n return cleanupRunnerSession({\n sessionId: watch.sessionId,\n paths: new RunnerPaths(watch.repository, watch.environment),\n processRegistry: registry,\n ownerPid: watch.ownerPid,\n generation: watch.generation,\n });\n };\n}\n\nasync function loadTelemetry(environment: LifelineWatch['environment']): Promise<PoTelemetry> {\n const { createPoTelemetry } = await import('@agimon-ai/po-core/telemetry');\n const shared = createPoTelemetry({\n serviceName: 'po-runner-host',\n packageName: '@agimon-ai/po-runner',\n env: environment,\n enableLogs: true,\n });\n process.once('beforeExit', () => void shared.shutdown());\n return shared;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"runnerHost.cjs","names":["watchOwner","LIFELINE_ENV","spawn","os","fs","pathToFileURL","REGISTRY_PATH_ENV"],"sources":["../../src/bin/runnerHost.ts"],"sourcesContent":["import { type ChildProcess, spawn } from 'node:child_process';\nimport fs from 'node:fs/promises';\nimport os from 'node:os';\nimport { pathToFileURL } from 'node:url';\n\nimport type { PoTelemetry } from '@agimon-ai/po-core/telemetry';\n\nimport { OBSERVE_SESSIONS_FLAG } from '../constants/lifeline';\nimport { REGISTRY_PATH_ENV } from '../constants/runnerRegistry';\nimport { LIFELINE_ENV, observeSessions, watchOwner } from '../services/lifeline/client';\nimport type { ProcessRegistryPort } from '../services/runnerRegistry';\nimport type { LifelineWatch } from '../types/lifeline';\n\n// Everything heavier loads on first use: the observer idles for its owner's whole life and\n// these imports alone would more than double its memory. A command's host loads none of it:\n// the server records the runner when it completes the record from this host's exit file.\n\ninterface CommandSpec {\n command: string;\n cwd: string;\n env: Record<string, string>;\n}\n\nconst POLL_MS = 10;\nconst FORWARDED_SIGNAL_GRACE_MS = 2_000;\nconst INVALID_COMMAND_SPEC_ERROR = 'invalid runner command spec';\nconst OWNER_LOST_EXIT = { reason: 'stopped', terminationReason: 'owner_lost', stopReason: 'owner session ended' };\n\nexport async function main(argv: readonly string[] = process.argv.slice(2)): Promise<number> {\n if (argv[0] === OBSERVE_SESSIONS_FLAG) {\n observeSessions(cleanupSessions);\n return 0;\n }\n const [specPath, gatePath, exitPath] = argv;\n if (!specPath || !gatePath || !exitPath) throw new Error('runnerHost requires spec, gate, and exit paths');\n const spec = await readSpec(specPath);\n let ownerLost = false;\n let onOwnerLost = (): void => undefined;\n // Watched from before the gate: an owner that dies before opening it never will, and nothing\n // else would end that wait.\n //\n // The lifeline comes from the spec rather than this process's environment: an\n // rmux server outlives the session that started it, so its own environment can\n // still name a previous session's socket.\n const detachLifeline = watchOwner(() => {\n ownerLost = true;\n onOwnerLost();\n }, spec.env[LIFELINE_ENV]);\n if (!(await waitForGate(gatePath, () => ownerLost))) {\n detachLifeline();\n await writeAtomic(exitPath, { code: null, signal: null, ...OWNER_LOST_EXIT });\n return 1;\n }\n let child: ChildProcess | undefined;\n let forwardedSignal: NodeJS.Signals | null = null;\n let escalationTimer: NodeJS.Timeout | undefined;\n const signalChild = (signal: NodeJS.Signals): void => {\n if (!child?.pid) return;\n try {\n process.kill(-child.pid, signal);\n } catch {\n child.kill(signal);\n }\n };\n const forward = (signal: NodeJS.Signals): void => {\n forwardedSignal = signal;\n signalChild(signal);\n escalationTimer ??= setTimeout(() => signalChild('SIGKILL'), FORWARDED_SIGNAL_GRACE_MS);\n };\n const signals: NodeJS.Signals[] = ['SIGTERM', 'SIGINT', 'SIGHUP'];\n for (const signal of signals) process.once(signal, () => forward(signal));\n\n child = spawn(spec.command, {\n cwd: spec.cwd,\n env: spec.env,\n shell: '/bin/bash',\n stdio: 'inherit',\n detached: true,\n });\n if (forwardedSignal) forward(forwardedSignal);\n\n // This process supervises the command for its whole life, so it is also the\n // right place to notice an owner that died without stopping it.\n onOwnerLost = () => forward('SIGTERM');\n\n try {\n const result = await new Promise<{ code: number | null; signal: NodeJS.Signals | null }>((resolve, reject) => {\n child?.once('error', reject);\n child?.once('exit', (code, signal) => resolve({ code, signal: signal ?? forwardedSignal }));\n });\n await writeAtomic(exitPath, { ...result, ...(ownerLost ? OWNER_LOST_EXIT : {}) });\n return result.code !== null ? result.code : result.signal ? 128 + os.constants.signals[result.signal] : 1;\n } finally {\n detachLifeline();\n if (escalationTimer) clearTimeout(escalationTimer);\n }\n}\n\nasync function readSpec(target: string): Promise<CommandSpec> {\n const value: unknown = JSON.parse(await fs.readFile(target, 'utf8'));\n if (typeof value !== 'object' || value === null) throw new Error(INVALID_COMMAND_SPEC_ERROR);\n const spec = value as Partial<CommandSpec>;\n if (typeof spec.command !== 'string' || typeof spec.cwd !== 'string' || !spec.env) {\n throw new Error(INVALID_COMMAND_SPEC_ERROR);\n }\n return { command: spec.command, cwd: spec.cwd, env: spec.env };\n}\n\n/** False once the owner is gone, since only the owner opens the gate. */\nasync function waitForGate(target: string, abandoned: () => boolean): Promise<boolean> {\n while (!abandoned()) {\n try {\n await fs.access(target);\n return true;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;\n }\n await new Promise((resolve) => setTimeout(resolve, POLL_MS));\n }\n return false;\n}\n\nasync function writeAtomic(target: string, value: unknown): Promise<void> {\n const temporary = `${target}.${process.pid}.tmp`;\n await fs.writeFile(temporary, `${JSON.stringify(value)}\\n`, { mode: 0o600 });\n await fs.rename(temporary, target);\n}\n\nif (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {\n main().then(\n (code) => {\n process.exitCode = code;\n },\n (error: unknown) => {\n process.stderr.write(`${error instanceof Error ? error.message : String(error)}\\n`);\n process.exitCode = 1;\n },\n );\n}\n\n/** Loaded on the first session an owner leaves behind, then shared by every later one. */\nlet cleaner: Promise<(watch: LifelineWatch) => Promise<void>> | undefined;\nlet telemetry: Promise<PoTelemetry> | undefined;\n\n/**\n * A separate process survives SIGKILL of the owner, even if that session has no active runners.\n * The owner cleans up each session it ends with its own shared services and removes its marker,\n * so only a session still marked with its generation is left here; the rest load nothing.\n */\nasync function cleanupSessions(watches: readonly LifelineWatch[]): Promise<void> {\n const failures: { watch: LifelineWatch; error: unknown }[] = [];\n // One session failing to clean up must not stop the others.\n for (const watch of watches) {\n try {\n // The same fence cleanupRunnerSession applies, checked before anything loads for it.\n if ((await readMarker(watch.marker)) !== watch.generation) continue;\n const clean = await (cleaner ??= loadCleaner());\n await clean(watch);\n } catch (error) {\n failures.push({ watch, error });\n }\n }\n const [first] = failures;\n if (!first) return;\n const log = await (telemetry ??= loadTelemetry(first.watch.environment));\n for (const { watch, error } of failures) {\n await log.recordError('po_runner.observer_cleanup_failed', error, { 'session.id': watch.sessionId });\n }\n await log.flush();\n throw new AggregateError(\n failures.map(({ error }) => error),\n `Could not clean up runner sessions ${failures.map(({ watch }) => watch.sessionId).join(', ')}`,\n );\n}\n\nasync function readMarker(marker: string): Promise<string | undefined> {\n try {\n return await fs.readFile(marker, 'utf8');\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined;\n throw error;\n }\n}\n\n/** One registry per registry path, held until the observer runs out of work. */\nasync function loadCleaner(): Promise<(watch: LifelineWatch) => Promise<void>> {\n const [{ cleanupRunnerSession }, { RunnerPaths }, { createDefaultProcessRegistry }] = await Promise.all([\n import('../services/runnerHost'),\n import('../services/runnerPaths'),\n import('../services/runnerRegistry'),\n ]);\n const registries = new Map<string | undefined, ProcessRegistryPort>();\n process.once('beforeExit', () => {\n for (const registry of registries.values()) registry.close();\n });\n return (watch) => {\n const registryPath = watch.environment[REGISTRY_PATH_ENV];\n let registry = registries.get(registryPath);\n if (!registry) {\n registry = createDefaultProcessRegistry(watch.environment);\n registries.set(registryPath, registry);\n }\n return cleanupRunnerSession({\n sessionId: watch.sessionId,\n paths: new RunnerPaths(watch.repository, watch.environment),\n processRegistry: registry,\n ownerPid: watch.ownerPid,\n generation: watch.generation,\n });\n };\n}\n\nasync function loadTelemetry(environment: LifelineWatch['environment']): Promise<PoTelemetry> {\n const { createPoTelemetry } = await import('@agimon-ai/po-core/telemetry');\n const shared = createPoTelemetry({\n serviceName: 'po-runner-host',\n packageName: '@agimon-ai/po-runner',\n env: environment,\n enableLogs: true,\n });\n process.once('beforeExit', () => void shared.shutdown());\n return shared;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runnerHost.d.mts","names":[],"sources":["../../src/bin/runnerHost.ts"],"mappings":";
|
|
1
|
+
{"version":3,"file":"runnerHost.d.mts","names":[],"sources":["../../src/bin/runnerHost.ts"],"mappings":";wBA4BsB,KAAK,2BAAkD"}
|
package/dist/bin/runnerHost.mjs
CHANGED
|
@@ -9,6 +9,11 @@ import os from "node:os";
|
|
|
9
9
|
const POLL_MS = 10;
|
|
10
10
|
const FORWARDED_SIGNAL_GRACE_MS = 2e3;
|
|
11
11
|
const INVALID_COMMAND_SPEC_ERROR = "invalid runner command spec";
|
|
12
|
+
const OWNER_LOST_EXIT = {
|
|
13
|
+
reason: "stopped",
|
|
14
|
+
terminationReason: "owner_lost",
|
|
15
|
+
stopReason: "owner session ended"
|
|
16
|
+
};
|
|
12
17
|
async function main(argv = process.argv.slice(2)) {
|
|
13
18
|
if (argv[0] === "--observe-sessions") {
|
|
14
19
|
observeSessions(cleanupSessions);
|
|
@@ -17,11 +22,24 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
17
22
|
const [specPath, gatePath, exitPath] = argv;
|
|
18
23
|
if (!specPath || !gatePath || !exitPath) throw new Error("runnerHost requires spec, gate, and exit paths");
|
|
19
24
|
const spec = await readSpec(specPath);
|
|
20
|
-
|
|
25
|
+
let ownerLost = false;
|
|
26
|
+
let onOwnerLost = () => void 0;
|
|
27
|
+
const detachLifeline = watchOwner(() => {
|
|
28
|
+
ownerLost = true;
|
|
29
|
+
onOwnerLost();
|
|
30
|
+
}, spec.env[LIFELINE_ENV]);
|
|
31
|
+
if (!await waitForGate(gatePath, () => ownerLost)) {
|
|
32
|
+
detachLifeline();
|
|
33
|
+
await writeAtomic(exitPath, {
|
|
34
|
+
code: null,
|
|
35
|
+
signal: null,
|
|
36
|
+
...OWNER_LOST_EXIT
|
|
37
|
+
});
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
21
40
|
let child;
|
|
22
41
|
let forwardedSignal = null;
|
|
23
42
|
let escalationTimer;
|
|
24
|
-
let ownerLost = false;
|
|
25
43
|
const signalChild = (signal) => {
|
|
26
44
|
if (!child?.pid) return;
|
|
27
45
|
try {
|
|
@@ -48,10 +66,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
48
66
|
detached: true
|
|
49
67
|
});
|
|
50
68
|
if (forwardedSignal) forward(forwardedSignal);
|
|
51
|
-
|
|
52
|
-
ownerLost = true;
|
|
53
|
-
forward("SIGTERM");
|
|
54
|
-
}, spec.env[LIFELINE_ENV]);
|
|
69
|
+
onOwnerLost = () => forward("SIGTERM");
|
|
55
70
|
try {
|
|
56
71
|
const result = await new Promise((resolve, reject) => {
|
|
57
72
|
child?.once("error", reject);
|
|
@@ -62,11 +77,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
62
77
|
});
|
|
63
78
|
await writeAtomic(exitPath, {
|
|
64
79
|
...result,
|
|
65
|
-
...ownerLost ? {
|
|
66
|
-
reason: "stopped",
|
|
67
|
-
terminationReason: "owner_lost",
|
|
68
|
-
stopReason: "owner session ended"
|
|
69
|
-
} : {}
|
|
80
|
+
...ownerLost ? OWNER_LOST_EXIT : {}
|
|
70
81
|
});
|
|
71
82
|
return result.code !== null ? result.code : result.signal ? 128 + os.constants.signals[result.signal] : 1;
|
|
72
83
|
} finally {
|
|
@@ -85,16 +96,18 @@ async function readSpec(target) {
|
|
|
85
96
|
env: spec.env
|
|
86
97
|
};
|
|
87
98
|
}
|
|
88
|
-
|
|
89
|
-
|
|
99
|
+
/** False once the owner is gone, since only the owner opens the gate. */
|
|
100
|
+
async function waitForGate(target, abandoned) {
|
|
101
|
+
while (!abandoned()) {
|
|
90
102
|
try {
|
|
91
103
|
await fs.access(target);
|
|
92
|
-
return;
|
|
104
|
+
return true;
|
|
93
105
|
} catch (error) {
|
|
94
106
|
if (error.code !== "ENOENT") throw error;
|
|
95
107
|
}
|
|
96
108
|
await new Promise((resolve) => setTimeout(resolve, POLL_MS));
|
|
97
109
|
}
|
|
110
|
+
return false;
|
|
98
111
|
}
|
|
99
112
|
async function writeAtomic(target, value) {
|
|
100
113
|
const temporary = `${target}.${process.pid}.tmp`;
|
|
@@ -145,7 +158,7 @@ async function readMarker(marker) {
|
|
|
145
158
|
async function loadCleaner() {
|
|
146
159
|
const [{ cleanupRunnerSession }, { RunnerPaths }, { createDefaultProcessRegistry }] = await Promise.all([
|
|
147
160
|
import("../runnerHost-B2Jiba1U.mjs").then((n) => n.r),
|
|
148
|
-
import("../runnerPaths-
|
|
161
|
+
import("../runnerPaths-DtT9RJxQ.mjs").then((n) => n.r),
|
|
149
162
|
import("../runnerRegistry-_B6LXMhS.mjs").then((n) => n.r)
|
|
150
163
|
]);
|
|
151
164
|
const registries = /* @__PURE__ */ new Map();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runnerHost.mjs","names":[],"sources":["../../src/bin/runnerHost.ts"],"sourcesContent":["import { type ChildProcess, spawn } from 'node:child_process';\nimport fs from 'node:fs/promises';\nimport os from 'node:os';\nimport { pathToFileURL } from 'node:url';\n\nimport type { PoTelemetry } from '@agimon-ai/po-core/telemetry';\n\nimport { OBSERVE_SESSIONS_FLAG } from '../constants/lifeline';\nimport { REGISTRY_PATH_ENV } from '../constants/runnerRegistry';\nimport { LIFELINE_ENV, observeSessions, watchOwner } from '../services/lifeline/client';\nimport type { ProcessRegistryPort } from '../services/runnerRegistry';\nimport type { LifelineWatch } from '../types/lifeline';\n\n// Everything heavier loads on first use: the observer idles for its owner's whole life and\n// these imports alone would more than double its memory. A command's host loads none of it:\n// the server records the runner when it completes the record from this host's exit file.\n\ninterface CommandSpec {\n command: string;\n cwd: string;\n env: Record<string, string>;\n}\n\nconst POLL_MS = 10;\nconst FORWARDED_SIGNAL_GRACE_MS = 2_000;\nconst INVALID_COMMAND_SPEC_ERROR = 'invalid runner command spec';\n\nexport async function main(argv: readonly string[] = process.argv.slice(2)): Promise<number> {\n if (argv[0] === OBSERVE_SESSIONS_FLAG) {\n observeSessions(cleanupSessions);\n return 0;\n }\n const [specPath, gatePath, exitPath] = argv;\n if (!specPath || !gatePath || !exitPath) throw new Error('runnerHost requires spec, gate, and exit paths');\n const spec = await readSpec(specPath);\n await waitForGate(gatePath);\n let child: ChildProcess | undefined;\n let forwardedSignal: NodeJS.Signals | null = null;\n let escalationTimer: NodeJS.Timeout | undefined;\n let ownerLost = false;\n const signalChild = (signal: NodeJS.Signals): void => {\n if (!child?.pid) return;\n try {\n process.kill(-child.pid, signal);\n } catch {\n child.kill(signal);\n }\n };\n const forward = (signal: NodeJS.Signals): void => {\n forwardedSignal = signal;\n signalChild(signal);\n escalationTimer ??= setTimeout(() => signalChild('SIGKILL'), FORWARDED_SIGNAL_GRACE_MS);\n };\n const signals: NodeJS.Signals[] = ['SIGTERM', 'SIGINT', 'SIGHUP'];\n for (const signal of signals) process.once(signal, () => forward(signal));\n\n child = spawn(spec.command, {\n cwd: spec.cwd,\n env: spec.env,\n shell: '/bin/bash',\n stdio: 'inherit',\n detached: true,\n });\n if (forwardedSignal) forward(forwardedSignal);\n\n // This process supervises the command for its whole life, so it is also the\n // right place to notice an owner that died without stopping it.\n //\n // The lifeline comes from the spec rather than this process's environment: an\n // rmux server outlives the session that started it, so its own environment can\n // still name a previous session's socket.\n const detachLifeline = watchOwner(() => {\n ownerLost = true;\n forward('SIGTERM');\n }, spec.env[LIFELINE_ENV]);\n\n try {\n const result = await new Promise<{ code: number | null; signal: NodeJS.Signals | null }>((resolve, reject) => {\n child?.once('error', reject);\n child?.once('exit', (code, signal) => resolve({ code, signal: signal ?? forwardedSignal }));\n });\n await writeAtomic(exitPath, {\n ...result,\n ...(ownerLost ? { reason: 'stopped', terminationReason: 'owner_lost', stopReason: 'owner session ended' } : {}),\n });\n return result.code !== null ? result.code : result.signal ? 128 + os.constants.signals[result.signal] : 1;\n } finally {\n detachLifeline();\n if (escalationTimer) clearTimeout(escalationTimer);\n }\n}\n\nasync function readSpec(target: string): Promise<CommandSpec> {\n const value: unknown = JSON.parse(await fs.readFile(target, 'utf8'));\n if (typeof value !== 'object' || value === null) throw new Error(INVALID_COMMAND_SPEC_ERROR);\n const spec = value as Partial<CommandSpec>;\n if (typeof spec.command !== 'string' || typeof spec.cwd !== 'string' || !spec.env) {\n throw new Error(INVALID_COMMAND_SPEC_ERROR);\n }\n return { command: spec.command, cwd: spec.cwd, env: spec.env };\n}\n\nasync function waitForGate(target: string): Promise<void> {\n for (;;) {\n try {\n await fs.access(target);\n return;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;\n }\n await new Promise((resolve) => setTimeout(resolve, POLL_MS));\n }\n}\n\nasync function writeAtomic(target: string, value: unknown): Promise<void> {\n const temporary = `${target}.${process.pid}.tmp`;\n await fs.writeFile(temporary, `${JSON.stringify(value)}\\n`, { mode: 0o600 });\n await fs.rename(temporary, target);\n}\n\nif (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {\n main().then(\n (code) => {\n process.exitCode = code;\n },\n (error: unknown) => {\n process.stderr.write(`${error instanceof Error ? error.message : String(error)}\\n`);\n process.exitCode = 1;\n },\n );\n}\n\n/** Loaded on the first session an owner leaves behind, then shared by every later one. */\nlet cleaner: Promise<(watch: LifelineWatch) => Promise<void>> | undefined;\nlet telemetry: Promise<PoTelemetry> | undefined;\n\n/**\n * A separate process survives SIGKILL of the owner, even if that session has no active runners.\n * The owner cleans up each session it ends with its own shared services and removes its marker,\n * so only a session still marked with its generation is left here; the rest load nothing.\n */\nasync function cleanupSessions(watches: readonly LifelineWatch[]): Promise<void> {\n const failures: { watch: LifelineWatch; error: unknown }[] = [];\n // One session failing to clean up must not stop the others.\n for (const watch of watches) {\n try {\n // The same fence cleanupRunnerSession applies, checked before anything loads for it.\n if ((await readMarker(watch.marker)) !== watch.generation) continue;\n const clean = await (cleaner ??= loadCleaner());\n await clean(watch);\n } catch (error) {\n failures.push({ watch, error });\n }\n }\n const [first] = failures;\n if (!first) return;\n const log = await (telemetry ??= loadTelemetry(first.watch.environment));\n for (const { watch, error } of failures) {\n await log.recordError('po_runner.observer_cleanup_failed', error, { 'session.id': watch.sessionId });\n }\n await log.flush();\n throw new AggregateError(\n failures.map(({ error }) => error),\n `Could not clean up runner sessions ${failures.map(({ watch }) => watch.sessionId).join(', ')}`,\n );\n}\n\nasync function readMarker(marker: string): Promise<string | undefined> {\n try {\n return await fs.readFile(marker, 'utf8');\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined;\n throw error;\n }\n}\n\n/** One registry per registry path, held until the observer runs out of work. */\nasync function loadCleaner(): Promise<(watch: LifelineWatch) => Promise<void>> {\n const [{ cleanupRunnerSession }, { RunnerPaths }, { createDefaultProcessRegistry }] = await Promise.all([\n import('../services/runnerHost'),\n import('../services/runnerPaths'),\n import('../services/runnerRegistry'),\n ]);\n const registries = new Map<string | undefined, ProcessRegistryPort>();\n process.once('beforeExit', () => {\n for (const registry of registries.values()) registry.close();\n });\n return (watch) => {\n const registryPath = watch.environment[REGISTRY_PATH_ENV];\n let registry = registries.get(registryPath);\n if (!registry) {\n registry = createDefaultProcessRegistry(watch.environment);\n registries.set(registryPath, registry);\n }\n return cleanupRunnerSession({\n sessionId: watch.sessionId,\n paths: new RunnerPaths(watch.repository, watch.environment),\n processRegistry: registry,\n ownerPid: watch.ownerPid,\n generation: watch.generation,\n });\n };\n}\n\nasync function loadTelemetry(environment: LifelineWatch['environment']): Promise<PoTelemetry> {\n const { createPoTelemetry } = await import('@agimon-ai/po-core/telemetry');\n const shared = createPoTelemetry({\n serviceName: 'po-runner-host',\n packageName: '@agimon-ai/po-runner',\n env: environment,\n enableLogs: true,\n });\n process.once('beforeExit', () => void shared.shutdown());\n return shared;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"runnerHost.mjs","names":[],"sources":["../../src/bin/runnerHost.ts"],"sourcesContent":["import { type ChildProcess, spawn } from 'node:child_process';\nimport fs from 'node:fs/promises';\nimport os from 'node:os';\nimport { pathToFileURL } from 'node:url';\n\nimport type { PoTelemetry } from '@agimon-ai/po-core/telemetry';\n\nimport { OBSERVE_SESSIONS_FLAG } from '../constants/lifeline';\nimport { REGISTRY_PATH_ENV } from '../constants/runnerRegistry';\nimport { LIFELINE_ENV, observeSessions, watchOwner } from '../services/lifeline/client';\nimport type { ProcessRegistryPort } from '../services/runnerRegistry';\nimport type { LifelineWatch } from '../types/lifeline';\n\n// Everything heavier loads on first use: the observer idles for its owner's whole life and\n// these imports alone would more than double its memory. A command's host loads none of it:\n// the server records the runner when it completes the record from this host's exit file.\n\ninterface CommandSpec {\n command: string;\n cwd: string;\n env: Record<string, string>;\n}\n\nconst POLL_MS = 10;\nconst FORWARDED_SIGNAL_GRACE_MS = 2_000;\nconst INVALID_COMMAND_SPEC_ERROR = 'invalid runner command spec';\nconst OWNER_LOST_EXIT = { reason: 'stopped', terminationReason: 'owner_lost', stopReason: 'owner session ended' };\n\nexport async function main(argv: readonly string[] = process.argv.slice(2)): Promise<number> {\n if (argv[0] === OBSERVE_SESSIONS_FLAG) {\n observeSessions(cleanupSessions);\n return 0;\n }\n const [specPath, gatePath, exitPath] = argv;\n if (!specPath || !gatePath || !exitPath) throw new Error('runnerHost requires spec, gate, and exit paths');\n const spec = await readSpec(specPath);\n let ownerLost = false;\n let onOwnerLost = (): void => undefined;\n // Watched from before the gate: an owner that dies before opening it never will, and nothing\n // else would end that wait.\n //\n // The lifeline comes from the spec rather than this process's environment: an\n // rmux server outlives the session that started it, so its own environment can\n // still name a previous session's socket.\n const detachLifeline = watchOwner(() => {\n ownerLost = true;\n onOwnerLost();\n }, spec.env[LIFELINE_ENV]);\n if (!(await waitForGate(gatePath, () => ownerLost))) {\n detachLifeline();\n await writeAtomic(exitPath, { code: null, signal: null, ...OWNER_LOST_EXIT });\n return 1;\n }\n let child: ChildProcess | undefined;\n let forwardedSignal: NodeJS.Signals | null = null;\n let escalationTimer: NodeJS.Timeout | undefined;\n const signalChild = (signal: NodeJS.Signals): void => {\n if (!child?.pid) return;\n try {\n process.kill(-child.pid, signal);\n } catch {\n child.kill(signal);\n }\n };\n const forward = (signal: NodeJS.Signals): void => {\n forwardedSignal = signal;\n signalChild(signal);\n escalationTimer ??= setTimeout(() => signalChild('SIGKILL'), FORWARDED_SIGNAL_GRACE_MS);\n };\n const signals: NodeJS.Signals[] = ['SIGTERM', 'SIGINT', 'SIGHUP'];\n for (const signal of signals) process.once(signal, () => forward(signal));\n\n child = spawn(spec.command, {\n cwd: spec.cwd,\n env: spec.env,\n shell: '/bin/bash',\n stdio: 'inherit',\n detached: true,\n });\n if (forwardedSignal) forward(forwardedSignal);\n\n // This process supervises the command for its whole life, so it is also the\n // right place to notice an owner that died without stopping it.\n onOwnerLost = () => forward('SIGTERM');\n\n try {\n const result = await new Promise<{ code: number | null; signal: NodeJS.Signals | null }>((resolve, reject) => {\n child?.once('error', reject);\n child?.once('exit', (code, signal) => resolve({ code, signal: signal ?? forwardedSignal }));\n });\n await writeAtomic(exitPath, { ...result, ...(ownerLost ? OWNER_LOST_EXIT : {}) });\n return result.code !== null ? result.code : result.signal ? 128 + os.constants.signals[result.signal] : 1;\n } finally {\n detachLifeline();\n if (escalationTimer) clearTimeout(escalationTimer);\n }\n}\n\nasync function readSpec(target: string): Promise<CommandSpec> {\n const value: unknown = JSON.parse(await fs.readFile(target, 'utf8'));\n if (typeof value !== 'object' || value === null) throw new Error(INVALID_COMMAND_SPEC_ERROR);\n const spec = value as Partial<CommandSpec>;\n if (typeof spec.command !== 'string' || typeof spec.cwd !== 'string' || !spec.env) {\n throw new Error(INVALID_COMMAND_SPEC_ERROR);\n }\n return { command: spec.command, cwd: spec.cwd, env: spec.env };\n}\n\n/** False once the owner is gone, since only the owner opens the gate. */\nasync function waitForGate(target: string, abandoned: () => boolean): Promise<boolean> {\n while (!abandoned()) {\n try {\n await fs.access(target);\n return true;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;\n }\n await new Promise((resolve) => setTimeout(resolve, POLL_MS));\n }\n return false;\n}\n\nasync function writeAtomic(target: string, value: unknown): Promise<void> {\n const temporary = `${target}.${process.pid}.tmp`;\n await fs.writeFile(temporary, `${JSON.stringify(value)}\\n`, { mode: 0o600 });\n await fs.rename(temporary, target);\n}\n\nif (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {\n main().then(\n (code) => {\n process.exitCode = code;\n },\n (error: unknown) => {\n process.stderr.write(`${error instanceof Error ? error.message : String(error)}\\n`);\n process.exitCode = 1;\n },\n );\n}\n\n/** Loaded on the first session an owner leaves behind, then shared by every later one. */\nlet cleaner: Promise<(watch: LifelineWatch) => Promise<void>> | undefined;\nlet telemetry: Promise<PoTelemetry> | undefined;\n\n/**\n * A separate process survives SIGKILL of the owner, even if that session has no active runners.\n * The owner cleans up each session it ends with its own shared services and removes its marker,\n * so only a session still marked with its generation is left here; the rest load nothing.\n */\nasync function cleanupSessions(watches: readonly LifelineWatch[]): Promise<void> {\n const failures: { watch: LifelineWatch; error: unknown }[] = [];\n // One session failing to clean up must not stop the others.\n for (const watch of watches) {\n try {\n // The same fence cleanupRunnerSession applies, checked before anything loads for it.\n if ((await readMarker(watch.marker)) !== watch.generation) continue;\n const clean = await (cleaner ??= loadCleaner());\n await clean(watch);\n } catch (error) {\n failures.push({ watch, error });\n }\n }\n const [first] = failures;\n if (!first) return;\n const log = await (telemetry ??= loadTelemetry(first.watch.environment));\n for (const { watch, error } of failures) {\n await log.recordError('po_runner.observer_cleanup_failed', error, { 'session.id': watch.sessionId });\n }\n await log.flush();\n throw new AggregateError(\n failures.map(({ error }) => error),\n `Could not clean up runner sessions ${failures.map(({ watch }) => watch.sessionId).join(', ')}`,\n );\n}\n\nasync function readMarker(marker: string): Promise<string | undefined> {\n try {\n return await fs.readFile(marker, 'utf8');\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined;\n throw error;\n }\n}\n\n/** One registry per registry path, held until the observer runs out of work. */\nasync function loadCleaner(): Promise<(watch: LifelineWatch) => Promise<void>> {\n const [{ cleanupRunnerSession }, { RunnerPaths }, { createDefaultProcessRegistry }] = await Promise.all([\n import('../services/runnerHost'),\n import('../services/runnerPaths'),\n import('../services/runnerRegistry'),\n ]);\n const registries = new Map<string | undefined, ProcessRegistryPort>();\n process.once('beforeExit', () => {\n for (const registry of registries.values()) registry.close();\n });\n return (watch) => {\n const registryPath = watch.environment[REGISTRY_PATH_ENV];\n let registry = registries.get(registryPath);\n if (!registry) {\n registry = createDefaultProcessRegistry(watch.environment);\n registries.set(registryPath, registry);\n }\n return cleanupRunnerSession({\n sessionId: watch.sessionId,\n paths: new RunnerPaths(watch.repository, watch.environment),\n processRegistry: registry,\n ownerPid: watch.ownerPid,\n generation: watch.generation,\n });\n };\n}\n\nasync function loadTelemetry(environment: LifelineWatch['environment']): Promise<PoTelemetry> {\n const { createPoTelemetry } = await import('@agimon-ai/po-core/telemetry');\n const shared = createPoTelemetry({\n serviceName: 'po-runner-host',\n packageName: '@agimon-ai/po-runner',\n env: environment,\n enableLogs: true,\n });\n process.once('beforeExit', () => void shared.shutdown());\n return shared;\n}\n"],"mappings":"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"}
|
package/dist/extensions/mcp.cjs
CHANGED
|
@@ -82,6 +82,7 @@ new RegExp([
|
|
|
82
82
|
/** Enough history for the overlay to scroll without holding a whole build. */
|
|
83
83
|
const { Terminal: HeadlessTerminal } = _xterm_headless.default;
|
|
84
84
|
(0, node_util.promisify)(node_child_process.execFile);
|
|
85
|
+
(0, node_util.promisify)(node_child_process.execFile);
|
|
85
86
|
//#endregion
|
|
86
87
|
//#region src/services/runnerSettings/index.ts
|
|
87
88
|
const COUNT_KEYS = [
|