@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.
Files changed (44) hide show
  1. package/dist/bin/cli.cjs +1 -1
  2. package/dist/bin/cli.mjs +1 -1
  3. package/dist/bin/runnerHost.cjs +28 -15
  4. package/dist/bin/runnerHost.cjs.map +1 -1
  5. package/dist/bin/runnerHost.d.mts.map +1 -1
  6. package/dist/bin/runnerHost.mjs +28 -15
  7. package/dist/bin/runnerHost.mjs.map +1 -1
  8. package/dist/extensions/mcp.cjs +1 -0
  9. package/dist/extensions/mcp.cjs.map +1 -1
  10. package/dist/extensions/mcp.mjs +1 -0
  11. package/dist/extensions/mcp.mjs.map +1 -1
  12. package/dist/extensions/server.cjs +11 -5
  13. package/dist/extensions/server.cjs.map +1 -1
  14. package/dist/extensions/server.mjs +11 -5
  15. package/dist/extensions/server.mjs.map +1 -1
  16. package/dist/index-L4iX9nzL.d.mts.map +1 -1
  17. package/dist/index.cjs +2 -2
  18. package/dist/index.mjs +2 -2
  19. package/dist/{rmuxBackend-DazdRaxC.mjs → rmuxBackend-B-7aY8Kc.mjs} +31 -13
  20. package/dist/rmuxBackend-B-7aY8Kc.mjs.map +1 -0
  21. package/dist/{rmuxBackend-D4_-qOQz.cjs → rmuxBackend-CPVm0YUN.cjs} +31 -13
  22. package/dist/rmuxBackend-CPVm0YUN.cjs.map +1 -0
  23. package/dist/rmuxBackend.cjs +1 -1
  24. package/dist/rmuxBackend.mjs +1 -1
  25. package/dist/{runnerDependencies-BBjsrtTH.cjs → runnerDependencies-BjbN8BAO.cjs} +40 -21
  26. package/dist/runnerDependencies-BjbN8BAO.cjs.map +1 -0
  27. package/dist/{runnerDependencies-CVRUC9Gw.mjs → runnerDependencies-DKMpDyqi.mjs} +40 -21
  28. package/dist/runnerDependencies-DKMpDyqi.mjs.map +1 -0
  29. package/dist/runnerDependencies.cjs +1 -1
  30. package/dist/runnerDependencies.mjs +1 -1
  31. package/dist/{runnerPaths-bzlBTHKU.mjs → runnerPaths-DtT9RJxQ.mjs} +10 -12
  32. package/dist/runnerPaths-DtT9RJxQ.mjs.map +1 -0
  33. package/dist/{runnerPaths-NGHLeATQ.cjs → runnerPaths-sdEEgdM8.cjs} +9 -11
  34. package/dist/runnerPaths-sdEEgdM8.cjs.map +1 -0
  35. package/dist/runnerPaths.cjs +1 -1
  36. package/dist/runnerPaths.d.mts.map +1 -1
  37. package/dist/runnerPaths.mjs +1 -1
  38. package/package.json +17 -17
  39. package/dist/rmuxBackend-D4_-qOQz.cjs.map +0 -1
  40. package/dist/rmuxBackend-DazdRaxC.mjs.map +0 -1
  41. package/dist/runnerDependencies-BBjsrtTH.cjs.map +0 -1
  42. package/dist/runnerDependencies-CVRUC9Gw.mjs.map +0 -1
  43. package/dist/runnerPaths-NGHLeATQ.cjs.map +0 -1
  44. 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-BBjsrtTH.cjs");
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-CVRUC9Gw.mjs";
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";
@@ -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
- await waitForGate(gatePath);
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
- const detachLifeline = require_services_lifeline_client.watchOwner(() => {
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
- async function waitForGate(target) {
93
- for (;;) {
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-NGHLeATQ.cjs")).then((n) => n.runnerPaths_exports),
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":";wBA2BsB,KAAK,2BAAkD"}
1
+ {"version":3,"file":"runnerHost.d.mts","names":[],"sources":["../../src/bin/runnerHost.ts"],"mappings":";wBA4BsB,KAAK,2BAAkD"}
@@ -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
- await waitForGate(gatePath);
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
- const detachLifeline = watchOwner(() => {
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
- async function waitForGate(target) {
89
- for (;;) {
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-bzlBTHKU.mjs").then((n) => n.r),
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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"}
@@ -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 = [