@agimon-ai/po-runner 0.0.0-alpha.101 → 0.0.0-alpha.102

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 (49) hide show
  1. package/dist/bin/cli.cjs +1 -1
  2. package/dist/bin/cli.mjs +1 -1
  3. package/dist/bin/runnerHost.cjs +18 -1
  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 +19 -2
  7. package/dist/bin/runnerHost.mjs.map +1 -1
  8. package/dist/extensions/server.cjs +4 -4
  9. package/dist/extensions/server.mjs +4 -4
  10. package/dist/{index-BKor0Jn_.d.mts → index-CJNsR-Ir.d.mts} +5 -2
  11. package/dist/index-CJNsR-Ir.d.mts.map +1 -0
  12. package/dist/index.cjs +2 -2
  13. package/dist/index.d.mts +1 -1
  14. package/dist/index.mjs +2 -2
  15. package/dist/{launcher-CwVJ55MC.mjs → launcher-3xMxw72I.mjs} +2 -2
  16. package/dist/{launcher-CwVJ55MC.mjs.map → launcher-3xMxw72I.mjs.map} +1 -1
  17. package/dist/{launcher-ksyHY-iG.cjs → launcher-D7qAhSRL.cjs} +2 -2
  18. package/dist/{launcher-ksyHY-iG.cjs.map → launcher-D7qAhSRL.cjs.map} +1 -1
  19. package/dist/launcher.cjs +1 -1
  20. package/dist/launcher.mjs +1 -1
  21. package/dist/{lifeline-doL4-_14.mjs → lifeline-C1v2ItCY.mjs} +2 -2
  22. package/dist/{lifeline-doL4-_14.mjs.map → lifeline-C1v2ItCY.mjs.map} +1 -1
  23. package/dist/{lifeline-CbxvM4uJ.cjs → lifeline-D4_ZRAhO.cjs} +2 -2
  24. package/dist/{lifeline-CbxvM4uJ.cjs.map → lifeline-D4_ZRAhO.cjs.map} +1 -1
  25. package/dist/lifeline.cjs +1 -1
  26. package/dist/lifeline.mjs +1 -1
  27. package/dist/{rmuxBackend-BooQvfXt.cjs → rmuxBackend-9tHTx1vf.cjs} +22 -6
  28. package/dist/rmuxBackend-9tHTx1vf.cjs.map +1 -0
  29. package/dist/{rmuxBackend-BCfO297l.mjs → rmuxBackend-BaK-R35I.mjs} +22 -6
  30. package/dist/rmuxBackend-BaK-R35I.mjs.map +1 -0
  31. package/dist/rmuxBackend.cjs +1 -1
  32. package/dist/rmuxBackend.d.mts +1 -1
  33. package/dist/rmuxBackend.mjs +1 -1
  34. package/dist/{runnerDependencies-0SbEl19h.mjs → runnerDependencies-CYlnUyix.mjs} +22 -8
  35. package/dist/runnerDependencies-CYlnUyix.mjs.map +1 -0
  36. package/dist/{runnerDependencies-DNxVf_Sg.cjs → runnerDependencies-IgPPvTcp.cjs} +22 -8
  37. package/dist/runnerDependencies-IgPPvTcp.cjs.map +1 -0
  38. package/dist/runnerDependencies.cjs +1 -1
  39. package/dist/runnerDependencies.mjs +1 -1
  40. package/dist/services/runnerSupervisor/index.cjs.map +1 -1
  41. package/dist/services/runnerSupervisor/index.d.mts +2 -0
  42. package/dist/services/runnerSupervisor/index.d.mts.map +1 -1
  43. package/dist/services/runnerSupervisor/index.mjs.map +1 -1
  44. package/package.json +16 -16
  45. package/dist/index-BKor0Jn_.d.mts.map +0 -1
  46. package/dist/rmuxBackend-BCfO297l.mjs.map +0 -1
  47. package/dist/rmuxBackend-BooQvfXt.cjs.map +0 -1
  48. package/dist/runnerDependencies-0SbEl19h.mjs.map +0 -1
  49. package/dist/runnerDependencies-DNxVf_Sg.cjs.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-BYfgP44s.cjs");
5
5
  const require_session = require("../session-PQd3azWf.cjs");
6
- const require_runnerDependencies = require("../runnerDependencies-DNxVf_Sg.cjs");
6
+ const require_runnerDependencies = require("../runnerDependencies-IgPPvTcp.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-DPbA8LkI.mjs";
4
4
  import { t as inheritedSessionId } from "../session-ISEGK4z9.mjs";
5
- import { t as createRunnerDependencies } from "../runnerDependencies-0SbEl19h.mjs";
5
+ import { t as createRunnerDependencies } from "../runnerDependencies-CYlnUyix.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";
@@ -39,6 +39,7 @@ async function main(argv = process.argv.slice(2)) {
39
39
  signal: null,
40
40
  ...OWNER_LOST_EXIT
41
41
  });
42
+ await cleanupOwnerSession(spec);
42
43
  return 1;
43
44
  }
44
45
  let child;
@@ -83,12 +84,27 @@ async function main(argv = process.argv.slice(2)) {
83
84
  ...result,
84
85
  ...ownerLost ? OWNER_LOST_EXIT : {}
85
86
  });
87
+ if (ownerLost) await cleanupOwnerSession(spec);
86
88
  return result.code !== null ? result.code : result.signal ? 128 + node_os.default.constants.signals[result.signal] : 1;
87
89
  } finally {
88
90
  detachLifeline();
89
91
  if (escalationTimer) clearTimeout(escalationTimer);
90
92
  }
91
93
  }
94
+ /** The backend supplies an exact kill-session argv, never a shared-server shutdown. */
95
+ async function cleanupOwnerSession(spec) {
96
+ const [binary, ...args] = spec.ownerLostCleanup ?? [];
97
+ if (!binary) return;
98
+ await new Promise((resolve, reject) => {
99
+ (0, node_child_process.execFile)(binary, args, {
100
+ env: spec.env,
101
+ timeout: 1e4
102
+ }, (error) => {
103
+ if (error) reject(error);
104
+ else resolve();
105
+ });
106
+ });
107
+ }
92
108
  async function readSpec(target) {
93
109
  const value = JSON.parse(await node_fs_promises.default.readFile(target, "utf8"));
94
110
  if (typeof value !== "object" || value === null) throw new Error(INVALID_COMMAND_SPEC_ERROR);
@@ -97,7 +113,8 @@ async function readSpec(target) {
97
113
  return {
98
114
  command: spec.command,
99
115
  cwd: spec.cwd,
100
- env: spec.env
116
+ env: spec.env,
117
+ ownerLostCleanup: spec.ownerLostCleanup
101
118
  };
102
119
  }
103
120
  /** False once the owner is gone, since only the owner opens the gate. */
@@ -1 +1 @@
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
+ {"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, execFile, 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 ownerLostCleanup?: 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 await cleanupOwnerSession(spec);\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 if (ownerLost) await cleanupOwnerSession(spec);\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\n/** The backend supplies an exact kill-session argv, never a shared-server shutdown. */\nasync function cleanupOwnerSession(spec: CommandSpec): Promise<void> {\n const [binary, ...args] = spec.ownerLostCleanup ?? [];\n if (!binary) return;\n await new Promise<void>((resolve, reject) => {\n execFile(binary, args, { env: spec.env, timeout: 10_000 }, (error) => {\n if (error) reject(error);\n else resolve();\n });\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, ownerLostCleanup: spec.ownerLostCleanup };\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":";wBA4BsB,KAAK,2BAAkD"}
1
+ {"version":3,"file":"runnerHost.d.mts","names":[],"sources":["../../src/bin/runnerHost.ts"],"mappings":";wBA6BsB,KAAK,2BAAkD"}
@@ -3,7 +3,7 @@ import "../lifeline-nPh3cx5A.mjs";
3
3
  import { a as REGISTRY_PATH_ENV } from "../runnerRegistry-l0C_55oO.mjs";
4
4
  import fs from "node:fs/promises";
5
5
  import { pathToFileURL } from "node:url";
6
- import { spawn } from "node:child_process";
6
+ import { execFile, spawn } from "node:child_process";
7
7
  import os from "node:os";
8
8
  //#region src/bin/runnerHost.ts
9
9
  const POLL_MS = 10;
@@ -35,6 +35,7 @@ async function main(argv = process.argv.slice(2)) {
35
35
  signal: null,
36
36
  ...OWNER_LOST_EXIT
37
37
  });
38
+ await cleanupOwnerSession(spec);
38
39
  return 1;
39
40
  }
40
41
  let child;
@@ -79,12 +80,27 @@ async function main(argv = process.argv.slice(2)) {
79
80
  ...result,
80
81
  ...ownerLost ? OWNER_LOST_EXIT : {}
81
82
  });
83
+ if (ownerLost) await cleanupOwnerSession(spec);
82
84
  return result.code !== null ? result.code : result.signal ? 128 + os.constants.signals[result.signal] : 1;
83
85
  } finally {
84
86
  detachLifeline();
85
87
  if (escalationTimer) clearTimeout(escalationTimer);
86
88
  }
87
89
  }
90
+ /** The backend supplies an exact kill-session argv, never a shared-server shutdown. */
91
+ async function cleanupOwnerSession(spec) {
92
+ const [binary, ...args] = spec.ownerLostCleanup ?? [];
93
+ if (!binary) return;
94
+ await new Promise((resolve, reject) => {
95
+ execFile(binary, args, {
96
+ env: spec.env,
97
+ timeout: 1e4
98
+ }, (error) => {
99
+ if (error) reject(error);
100
+ else resolve();
101
+ });
102
+ });
103
+ }
88
104
  async function readSpec(target) {
89
105
  const value = JSON.parse(await fs.readFile(target, "utf8"));
90
106
  if (typeof value !== "object" || value === null) throw new Error(INVALID_COMMAND_SPEC_ERROR);
@@ -93,7 +109,8 @@ async function readSpec(target) {
93
109
  return {
94
110
  command: spec.command,
95
111
  cwd: spec.cwd,
96
- env: spec.env
112
+ env: spec.env,
113
+ ownerLostCleanup: spec.ownerLostCleanup
97
114
  };
98
115
  }
99
116
  /** False once the owner is gone, since only the owner opens the gate. */
@@ -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';\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":";;;;;;;;AAuBA,MAAM,UAAU;AAChB,MAAM,4BAA4B;AAClC,MAAM,6BAA6B;AACnC,MAAM,kBAAkB;CAAE,QAAQ;CAAW,mBAAmB;CAAc,YAAY;AAAsB;AAEhH,eAAsB,KAAK,OAA0B,QAAQ,KAAK,MAAM,CAAC,GAAoB;CAC3F,IAAI,KAAK,OAAA,sBAA8B;EACrC,gBAAgB,eAAe;EAC/B,OAAO;CACT;CACA,MAAM,CAAC,UAAU,UAAU,YAAY;CACvC,IAAI,CAAC,YAAY,CAAC,YAAY,CAAC,UAAU,MAAM,IAAI,MAAM,gDAAgD;CACzG,MAAM,OAAO,MAAM,SAAS,QAAQ;CACpC,IAAI,YAAY;CAChB,IAAI,oBAA0B,KAAA;CAO9B,MAAM,iBAAiB,iBAAiB;EACtC,YAAY;EACZ,YAAY;CACd,GAAG,KAAK,IAAI,aAAa;CACzB,IAAI,CAAE,MAAM,YAAY,gBAAgB,SAAS,GAAI;EACnD,eAAe;EACf,MAAM,YAAY,UAAU;GAAE,MAAM;GAAM,QAAQ;GAAM,GAAG;EAAgB,CAAC;EAC5E,OAAO;CACT;CACA,IAAI;CACJ,IAAI,kBAAyC;CAC7C,IAAI;CACJ,MAAM,eAAe,WAAiC;EACpD,IAAI,CAAC,OAAO,KAAK;EACjB,IAAI;GACF,QAAQ,KAAK,CAAC,MAAM,KAAK,MAAM;EACjC,QAAQ;GACN,MAAM,KAAK,MAAM;EACnB;CACF;CACA,MAAM,WAAW,WAAiC;EAChD,kBAAkB;EAClB,YAAY,MAAM;EAClB,oBAAoB,iBAAiB,YAAY,SAAS,GAAG,yBAAyB;CACxF;CAEA,KAAK,MAAM,UAAU;EADc;EAAW;EAAU;CAC7B,GAAG,QAAQ,KAAK,cAAc,QAAQ,MAAM,CAAC;CAExE,QAAQ,MAAM,KAAK,SAAS;EAC1B,KAAK,KAAK;EACV,KAAK,KAAK;EACV,OAAO;EACP,OAAO;EACP,UAAU;CACZ,CAAC;CACD,IAAI,iBAAiB,QAAQ,eAAe;CAI5C,oBAAoB,QAAQ,SAAS;CAErC,IAAI;EACF,MAAM,SAAS,MAAM,IAAI,SAAiE,SAAS,WAAW;GAC5G,OAAO,KAAK,SAAS,MAAM;GAC3B,OAAO,KAAK,SAAS,MAAM,WAAW,QAAQ;IAAE;IAAM,QAAQ,UAAU;GAAgB,CAAC,CAAC;EAC5F,CAAC;EACD,MAAM,YAAY,UAAU;GAAE,GAAG;GAAQ,GAAI,YAAY,kBAAkB,CAAC;EAAG,CAAC;EAChF,OAAO,OAAO,SAAS,OAAO,OAAO,OAAO,OAAO,SAAS,MAAM,GAAG,UAAU,QAAQ,OAAO,UAAU;CAC1G,UAAU;EACR,eAAe;EACf,IAAI,iBAAiB,aAAa,eAAe;CACnD;AACF;AAEA,eAAe,SAAS,QAAsC;CAC5D,MAAM,QAAiB,KAAK,MAAM,MAAM,GAAG,SAAS,QAAQ,MAAM,CAAC;CACnE,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM,MAAM,IAAI,MAAM,0BAA0B;CAC3F,MAAM,OAAO;CACb,IAAI,OAAO,KAAK,YAAY,YAAY,OAAO,KAAK,QAAQ,YAAY,CAAC,KAAK,KAC5E,MAAM,IAAI,MAAM,0BAA0B;CAE5C,OAAO;EAAE,SAAS,KAAK;EAAS,KAAK,KAAK;EAAK,KAAK,KAAK;CAAI;AAC/D;;AAGA,eAAe,YAAY,QAAgB,WAA4C;CACrF,OAAO,CAAC,UAAU,GAAG;EACnB,IAAI;GACF,MAAM,GAAG,OAAO,MAAM;GACtB,OAAO;EACT,SAAS,OAAO;GACd,IAAK,MAAgC,SAAS,UAAU,MAAM;EAChE;EACA,MAAM,IAAI,SAAS,YAAY,WAAW,SAAS,OAAO,CAAC;CAC7D;CACA,OAAO;AACT;AAEA,eAAe,YAAY,QAAgB,OAA+B;CACxE,MAAM,YAAY,GAAG,OAAO,GAAG,QAAQ,IAAI;CAC3C,MAAM,GAAG,UAAU,WAAW,GAAG,KAAK,UAAU,KAAK,EAAE,KAAK,EAAE,MAAM,IAAM,CAAC;CAC3E,MAAM,GAAG,OAAO,WAAW,MAAM;AACnC;AAEA,IAAI,QAAQ,KAAK,MAAM,YAAY,QAAQ,cAAc,QAAQ,KAAK,EAAE,CAAC,CAAC,MACxE,KAAK,CAAC,CAAC,MACJ,SAAS;CACR,QAAQ,WAAW;AACrB,IACC,UAAmB;CAClB,QAAQ,OAAO,MAAM,GAAG,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE,GAAG;CAClF,QAAQ,WAAW;AACrB,CACF;;AAIF,IAAI;AACJ,IAAI;;;;;;AAOJ,eAAe,gBAAgB,SAAkD;CAC/E,MAAM,WAAuD,CAAC;CAE9D,KAAK,MAAM,SAAS,SAClB,IAAI;EAEF,IAAK,MAAM,WAAW,MAAM,MAAM,MAAO,MAAM,YAAY;EAE3D,OAAM,OADe,YAAY,YAAY,GAAA,CACjC,KAAK;CACnB,SAAS,OAAO;EACd,SAAS,KAAK;GAAE;GAAO;EAAM,CAAC;CAChC;CAEF,MAAM,CAAC,SAAS;CAChB,IAAI,CAAC,OAAO;CACZ,MAAM,MAAM,OAAO,cAAc,cAAc,MAAM,MAAM,WAAW;CACtE,KAAK,MAAM,EAAE,OAAO,WAAW,UAC7B,MAAM,IAAI,YAAY,qCAAqC,OAAO,EAAE,cAAc,MAAM,UAAU,CAAC;CAErG,MAAM,IAAI,MAAM;CAChB,MAAM,IAAI,eACR,SAAS,KAAK,EAAE,YAAY,KAAK,GACjC,sCAAsC,SAAS,KAAK,EAAE,YAAY,MAAM,SAAS,CAAC,CAAC,KAAK,IAAI,GAC9F;AACF;AAEA,eAAe,WAAW,QAA6C;CACrE,IAAI;EACF,OAAO,MAAM,GAAG,SAAS,QAAQ,MAAM;CACzC,SAAS,OAAO;EACd,IAAK,MAAgC,SAAS,UAAU,OAAO,KAAA;EAC/D,MAAM;CACR;AACF;;AAGA,eAAe,cAAgE;CAC7E,MAAM,CAAC,EAAE,wBAAwB,EAAE,eAAe,EAAE,kCAAkC,MAAM,QAAQ,IAAI;EACtG,OAAO,6BAAyB,CAAA,MAAA,MAAA,EAAA,CAAA;EAChC,OAAO,8BAA0B,CAAA,MAAA,MAAA,EAAA,CAAA;EACjC,OAAO,iCAA6B,CAAA,MAAA,MAAA,EAAA,CAAA;CACtC,CAAC;CACD,MAAM,6BAAa,IAAI,IAA6C;CACpE,QAAQ,KAAK,oBAAoB;EAC/B,KAAK,MAAM,YAAY,WAAW,OAAO,GAAG,SAAS,MAAM;CAC7D,CAAC;CACD,QAAQ,UAAU;EAChB,MAAM,eAAe,MAAM,YAAY;EACvC,IAAI,WAAW,WAAW,IAAI,YAAY;EAC1C,IAAI,CAAC,UAAU;GACb,WAAW,6BAA6B,MAAM,WAAW;GACzD,WAAW,IAAI,cAAc,QAAQ;EACvC;EACA,OAAO,qBAAqB;GAC1B,WAAW,MAAM;GACjB,OAAO,IAAI,YAAY,MAAM,YAAY,MAAM,WAAW;GAC1D,iBAAiB;GACjB,UAAU,MAAM;GAChB,YAAY,MAAM;EACpB,CAAC;CACH;AACF;AAEA,eAAe,cAAc,aAAiE;CAC5F,MAAM,EAAE,sBAAsB,MAAM,OAAO;CAC3C,MAAM,SAAS,kBAAkB;EAC/B,aAAa;EACb,aAAa;EACb,KAAK;EACL,YAAY;CACd,CAAC;CACD,QAAQ,KAAK,oBAAoB,KAAK,OAAO,SAAS,CAAC;CACvD,OAAO;AACT"}
1
+ {"version":3,"file":"runnerHost.mjs","names":[],"sources":["../../src/bin/runnerHost.ts"],"sourcesContent":["import { type ChildProcess, execFile, 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 ownerLostCleanup?: 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 await cleanupOwnerSession(spec);\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 if (ownerLost) await cleanupOwnerSession(spec);\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\n/** The backend supplies an exact kill-session argv, never a shared-server shutdown. */\nasync function cleanupOwnerSession(spec: CommandSpec): Promise<void> {\n const [binary, ...args] = spec.ownerLostCleanup ?? [];\n if (!binary) return;\n await new Promise<void>((resolve, reject) => {\n execFile(binary, args, { env: spec.env, timeout: 10_000 }, (error) => {\n if (error) reject(error);\n else resolve();\n });\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, ownerLostCleanup: spec.ownerLostCleanup };\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":";;;;;;;;AAwBA,MAAM,UAAU;AAChB,MAAM,4BAA4B;AAClC,MAAM,6BAA6B;AACnC,MAAM,kBAAkB;CAAE,QAAQ;CAAW,mBAAmB;CAAc,YAAY;AAAsB;AAEhH,eAAsB,KAAK,OAA0B,QAAQ,KAAK,MAAM,CAAC,GAAoB;CAC3F,IAAI,KAAK,OAAA,sBAA8B;EACrC,gBAAgB,eAAe;EAC/B,OAAO;CACT;CACA,MAAM,CAAC,UAAU,UAAU,YAAY;CACvC,IAAI,CAAC,YAAY,CAAC,YAAY,CAAC,UAAU,MAAM,IAAI,MAAM,gDAAgD;CACzG,MAAM,OAAO,MAAM,SAAS,QAAQ;CACpC,IAAI,YAAY;CAChB,IAAI,oBAA0B,KAAA;CAO9B,MAAM,iBAAiB,iBAAiB;EACtC,YAAY;EACZ,YAAY;CACd,GAAG,KAAK,IAAI,aAAa;CACzB,IAAI,CAAE,MAAM,YAAY,gBAAgB,SAAS,GAAI;EACnD,eAAe;EACf,MAAM,YAAY,UAAU;GAAE,MAAM;GAAM,QAAQ;GAAM,GAAG;EAAgB,CAAC;EAC5E,MAAM,oBAAoB,IAAI;EAC9B,OAAO;CACT;CACA,IAAI;CACJ,IAAI,kBAAyC;CAC7C,IAAI;CACJ,MAAM,eAAe,WAAiC;EACpD,IAAI,CAAC,OAAO,KAAK;EACjB,IAAI;GACF,QAAQ,KAAK,CAAC,MAAM,KAAK,MAAM;EACjC,QAAQ;GACN,MAAM,KAAK,MAAM;EACnB;CACF;CACA,MAAM,WAAW,WAAiC;EAChD,kBAAkB;EAClB,YAAY,MAAM;EAClB,oBAAoB,iBAAiB,YAAY,SAAS,GAAG,yBAAyB;CACxF;CAEA,KAAK,MAAM,UAAU;EADc;EAAW;EAAU;CAC7B,GAAG,QAAQ,KAAK,cAAc,QAAQ,MAAM,CAAC;CAExE,QAAQ,MAAM,KAAK,SAAS;EAC1B,KAAK,KAAK;EACV,KAAK,KAAK;EACV,OAAO;EACP,OAAO;EACP,UAAU;CACZ,CAAC;CACD,IAAI,iBAAiB,QAAQ,eAAe;CAI5C,oBAAoB,QAAQ,SAAS;CAErC,IAAI;EACF,MAAM,SAAS,MAAM,IAAI,SAAiE,SAAS,WAAW;GAC5G,OAAO,KAAK,SAAS,MAAM;GAC3B,OAAO,KAAK,SAAS,MAAM,WAAW,QAAQ;IAAE;IAAM,QAAQ,UAAU;GAAgB,CAAC,CAAC;EAC5F,CAAC;EACD,MAAM,YAAY,UAAU;GAAE,GAAG;GAAQ,GAAI,YAAY,kBAAkB,CAAC;EAAG,CAAC;EAChF,IAAI,WAAW,MAAM,oBAAoB,IAAI;EAC7C,OAAO,OAAO,SAAS,OAAO,OAAO,OAAO,OAAO,SAAS,MAAM,GAAG,UAAU,QAAQ,OAAO,UAAU;CAC1G,UAAU;EACR,eAAe;EACf,IAAI,iBAAiB,aAAa,eAAe;CACnD;AACF;;AAGA,eAAe,oBAAoB,MAAkC;CACnE,MAAM,CAAC,QAAQ,GAAG,QAAQ,KAAK,oBAAoB,CAAC;CACpD,IAAI,CAAC,QAAQ;CACb,MAAM,IAAI,SAAe,SAAS,WAAW;EAC3C,SAAS,QAAQ,MAAM;GAAE,KAAK,KAAK;GAAK,SAAS;EAAO,IAAI,UAAU;GACpE,IAAI,OAAO,OAAO,KAAK;QAClB,QAAQ;EACf,CAAC;CACH,CAAC;AACH;AAEA,eAAe,SAAS,QAAsC;CAC5D,MAAM,QAAiB,KAAK,MAAM,MAAM,GAAG,SAAS,QAAQ,MAAM,CAAC;CACnE,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM,MAAM,IAAI,MAAM,0BAA0B;CAC3F,MAAM,OAAO;CACb,IAAI,OAAO,KAAK,YAAY,YAAY,OAAO,KAAK,QAAQ,YAAY,CAAC,KAAK,KAC5E,MAAM,IAAI,MAAM,0BAA0B;CAE5C,OAAO;EAAE,SAAS,KAAK;EAAS,KAAK,KAAK;EAAK,KAAK,KAAK;EAAK,kBAAkB,KAAK;CAAiB;AACxG;;AAGA,eAAe,YAAY,QAAgB,WAA4C;CACrF,OAAO,CAAC,UAAU,GAAG;EACnB,IAAI;GACF,MAAM,GAAG,OAAO,MAAM;GACtB,OAAO;EACT,SAAS,OAAO;GACd,IAAK,MAAgC,SAAS,UAAU,MAAM;EAChE;EACA,MAAM,IAAI,SAAS,YAAY,WAAW,SAAS,OAAO,CAAC;CAC7D;CACA,OAAO;AACT;AAEA,eAAe,YAAY,QAAgB,OAA+B;CACxE,MAAM,YAAY,GAAG,OAAO,GAAG,QAAQ,IAAI;CAC3C,MAAM,GAAG,UAAU,WAAW,GAAG,KAAK,UAAU,KAAK,EAAE,KAAK,EAAE,MAAM,IAAM,CAAC;CAC3E,MAAM,GAAG,OAAO,WAAW,MAAM;AACnC;AAEA,IAAI,QAAQ,KAAK,MAAM,YAAY,QAAQ,cAAc,QAAQ,KAAK,EAAE,CAAC,CAAC,MACxE,KAAK,CAAC,CAAC,MACJ,SAAS;CACR,QAAQ,WAAW;AACrB,IACC,UAAmB;CAClB,QAAQ,OAAO,MAAM,GAAG,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE,GAAG;CAClF,QAAQ,WAAW;AACrB,CACF;;AAIF,IAAI;AACJ,IAAI;;;;;;AAOJ,eAAe,gBAAgB,SAAkD;CAC/E,MAAM,WAAuD,CAAC;CAE9D,KAAK,MAAM,SAAS,SAClB,IAAI;EAEF,IAAK,MAAM,WAAW,MAAM,MAAM,MAAO,MAAM,YAAY;EAE3D,OAAM,OADe,YAAY,YAAY,GAAA,CACjC,KAAK;CACnB,SAAS,OAAO;EACd,SAAS,KAAK;GAAE;GAAO;EAAM,CAAC;CAChC;CAEF,MAAM,CAAC,SAAS;CAChB,IAAI,CAAC,OAAO;CACZ,MAAM,MAAM,OAAO,cAAc,cAAc,MAAM,MAAM,WAAW;CACtE,KAAK,MAAM,EAAE,OAAO,WAAW,UAC7B,MAAM,IAAI,YAAY,qCAAqC,OAAO,EAAE,cAAc,MAAM,UAAU,CAAC;CAErG,MAAM,IAAI,MAAM;CAChB,MAAM,IAAI,eACR,SAAS,KAAK,EAAE,YAAY,KAAK,GACjC,sCAAsC,SAAS,KAAK,EAAE,YAAY,MAAM,SAAS,CAAC,CAAC,KAAK,IAAI,GAC9F;AACF;AAEA,eAAe,WAAW,QAA6C;CACrE,IAAI;EACF,OAAO,MAAM,GAAG,SAAS,QAAQ,MAAM;CACzC,SAAS,OAAO;EACd,IAAK,MAAgC,SAAS,UAAU,OAAO,KAAA;EAC/D,MAAM;CACR;AACF;;AAGA,eAAe,cAAgE;CAC7E,MAAM,CAAC,EAAE,wBAAwB,EAAE,eAAe,EAAE,kCAAkC,MAAM,QAAQ,IAAI;EACtG,OAAO,6BAAyB,CAAA,MAAA,MAAA,EAAA,CAAA;EAChC,OAAO,8BAA0B,CAAA,MAAA,MAAA,EAAA,CAAA;EACjC,OAAO,iCAA6B,CAAA,MAAA,MAAA,EAAA,CAAA;CACtC,CAAC;CACD,MAAM,6BAAa,IAAI,IAA6C;CACpE,QAAQ,KAAK,oBAAoB;EAC/B,KAAK,MAAM,YAAY,WAAW,OAAO,GAAG,SAAS,MAAM;CAC7D,CAAC;CACD,QAAQ,UAAU;EAChB,MAAM,eAAe,MAAM,YAAY;EACvC,IAAI,WAAW,WAAW,IAAI,YAAY;EAC1C,IAAI,CAAC,UAAU;GACb,WAAW,6BAA6B,MAAM,WAAW;GACzD,WAAW,IAAI,cAAc,QAAQ;EACvC;EACA,OAAO,qBAAqB;GAC1B,WAAW,MAAM;GACjB,OAAO,IAAI,YAAY,MAAM,YAAY,MAAM,WAAW;GAC1D,iBAAiB;GACjB,UAAU,MAAM;GAChB,YAAY,MAAM;EACpB,CAAC;CACH;AACF;AAEA,eAAe,cAAc,aAAiE;CAC5F,MAAM,EAAE,sBAAsB,MAAM,OAAO;CAC3C,MAAM,SAAS,kBAAkB;EAC/B,aAAa;EACb,aAAa;EACb,KAAK;EACL,YAAY;CACd,CAAC;CACD,QAAQ,KAAK,oBAAoB,KAAK,OAAO,SAAS,CAAC;CACvD,OAAO;AACT"}
@@ -13,10 +13,10 @@ const require_reconcile = require("../reconcile-DjF9T1hN.cjs");
13
13
  const require_runnerSequenceStore = require("../runnerSequenceStore-Ck6M-8r_.cjs");
14
14
  const require_clock = require("../clock-jqnLWziT.cjs");
15
15
  const require_compaction = require("../compaction-Cduebib_.cjs");
16
- const require_rmuxBackend = require("../rmuxBackend-BooQvfXt.cjs");
17
- const require_launcher = require("../launcher-ksyHY-iG.cjs");
18
- const require_lifeline = require("../lifeline-CbxvM4uJ.cjs");
19
- const require_runnerDependencies = require("../runnerDependencies-DNxVf_Sg.cjs");
16
+ const require_rmuxBackend = require("../rmuxBackend-9tHTx1vf.cjs");
17
+ const require_launcher = require("../launcher-D7qAhSRL.cjs");
18
+ const require_lifeline = require("../lifeline-D4_ZRAhO.cjs");
19
+ const require_runnerDependencies = require("../runnerDependencies-IgPPvTcp.cjs");
20
20
  const require_runnerPaths = require("../runnerPaths-CSkswyjR.cjs");
21
21
  require("../runnerRegistry-NW6oAJ71.cjs");
22
22
  const require_runnerRegistry$1 = require("../runnerRegistry-B8K1L3lq.cjs");
@@ -8,10 +8,10 @@ import { n as stopRunnerProcess, t as reconcileActiveRunners } from "../reconcil
8
8
  import { n as isSequenceId, t as createRunnerSequenceStore } from "../runnerSequenceStore-CaY7S_Sd.mjs";
9
9
  import { t as SystemClock } from "../clock-Cv2pddgo.mjs";
10
10
  import { t as activeRunnerRecovery } from "../compaction-nNlE6e5I.mjs";
11
- import { t as RmuxBackend } from "../rmuxBackend-BCfO297l.mjs";
12
- import { n as TERM_GRACE_MS } from "../launcher-CwVJ55MC.mjs";
13
- import { t as LifelineObserver } from "../lifeline-doL4-_14.mjs";
14
- import { n as TmuxBackend, r as PtyBackendChain, t as createRunnerDependencies } from "../runnerDependencies-0SbEl19h.mjs";
11
+ import { t as RmuxBackend } from "../rmuxBackend-BaK-R35I.mjs";
12
+ import { n as TERM_GRACE_MS } from "../launcher-3xMxw72I.mjs";
13
+ import { t as LifelineObserver } from "../lifeline-C1v2ItCY.mjs";
14
+ import { n as TmuxBackend, r as PtyBackendChain, t as createRunnerDependencies } from "../runnerDependencies-CYlnUyix.mjs";
15
15
  import { i as runnerStateDirFor, n as resolveRunnerStoreDirectory, t as RunnerPaths } from "../runnerPaths-D3P7_EhW.mjs";
16
16
  import "../runnerRegistry-l0C_55oO.mjs";
17
17
  import { a as formatRunnerFinishedNotice, i as formatRunnerFinished, n as createDefaultProcessRegistry, o as formatRunnerStatus, s as isRunnerRecord } from "../runnerRegistry-Dqca6SD3.mjs";
@@ -2,6 +2,7 @@ import { t as ExitResult } from "./spawner-CdrN_HNk.mjs";
2
2
  import { r as RunHandle } from "./launcher-CDDBTvZq.mjs";
3
3
  import { r as PtyRun } from "./ptyHost-C_6x2xuJ.mjs";
4
4
  import { n as RmuxLaunchRequest, t as IRmuxBackend } from "./rmuxBackend-xFzlANM9.mjs";
5
+ import { t as ILifeline } from "./lifeline-26IaW5w7.mjs";
5
6
  import { t as RunnerSettings } from "./runnerSettings-DiV1lvzx.mjs";
6
7
  import { t as IRunnerPaths } from "./type-BWY9vvSq.mjs";
7
8
  //#region src/services/rmuxBackend/index.d.ts
@@ -9,10 +10,12 @@ declare class RmuxBackend implements IRmuxBackend {
9
10
  private readonly paths;
10
11
  private readonly env;
11
12
  private readonly settings?;
13
+ private readonly lifeline?;
14
+ private ownerLostCleanup;
12
15
  private clientPromise;
13
16
  private readonly runs;
14
17
  private readonly completions;
15
- constructor(paths: IRunnerPaths, env?: Readonly<Record<string, string | undefined>>, settings?: RunnerSettings | undefined);
18
+ constructor(paths: IRunnerPaths, env?: Readonly<Record<string, string | undefined>>, settings?: RunnerSettings | undefined, lifeline?: ILifeline | undefined);
16
19
  launch(request: RmuxLaunchRequest): Promise<RunHandle | undefined>;
17
20
  stop(target: string, expectedPid: number): Promise<boolean>;
18
21
  watch(id: string, target: string, sessionId?: string): Promise<ExitResult | undefined>;
@@ -40,4 +43,4 @@ declare class RmuxBackend implements IRmuxBackend {
40
43
  declare function rmuxPackageForTarget(platform: string, architecture: string): string | undefined;
41
44
  //#endregion
42
45
  export { rmuxPackageForTarget as n, RmuxBackend as t };
43
- //# sourceMappingURL=index-BKor0Jn_.d.mts.map
46
+ //# sourceMappingURL=index-CJNsR-Ir.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index-CJNsR-Ir.d.mts","names":[],"sources":["../src/services/rmuxBackend/index.ts"],"mappings":";;;;;;;;cAuDa,uBAAuB;mBAOf;mBACA;mBACA;mBACA;UATX;UACA;mBACS;mBACA;EAEjB,YACmB,OAAO,cACP,MAAK,SAAS,qCACd,WAAW,4BACX,WAAW;EAGxB,OAAO,SAAS,oBAAoB,QAAQ;EAyF5C,KAAK,gBAAgB,sBAAsB;EA6B3C,MAAM,YAAY,gBAAgB,qBAAqB,QAAQ;EAe/D,MAAM,gBAAgB,eAAe;EAcrC,QAAQ,iBAAiB;EAa/B,IAAI,eAAe;EAInB,YAAY,YAAY,oBAAoB,QAAQ;UAI5C;UAOA;UAKM;UAoBA;UAmBN;;;;;;;;;UAYM;UAaA;UA+BN;;iBAyDM,qBAAqB,kBAAkB"}
package/dist/index.cjs CHANGED
@@ -1,9 +1,9 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_runnerConfig = require("./runnerConfig-BoLMinlx.cjs");
3
3
  const require_headless = require("./headless-DPj9mk2G.cjs");
4
- const require_rmuxBackend = require("./rmuxBackend-BooQvfXt.cjs");
4
+ const require_rmuxBackend = require("./rmuxBackend-9tHTx1vf.cjs");
5
5
  const require_rtkProcessor = require("./rtkProcessor-J2gLQvM4.cjs");
6
- const require_runnerDependencies = require("./runnerDependencies-DNxVf_Sg.cjs");
6
+ const require_runnerDependencies = require("./runnerDependencies-IgPPvTcp.cjs");
7
7
  exports.DEFAULT_BG_THRESHOLD_MS = require_runnerConfig.DEFAULT_BG_THRESHOLD_MS;
8
8
  exports.DEFAULT_LOG_MAX_BYTES = require_runnerConfig.DEFAULT_LOG_MAX_BYTES;
9
9
  exports.DEFAULT_LOG_TTL_MS = require_runnerConfig.DEFAULT_LOG_TTL_MS;
package/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { n as RmuxLaunchRequest, t as IRmuxBackend } from "./rmuxBackend-xFzlANM9.mjs";
2
2
  import { n as createHeadlessRunnersCommand, t as createHeadlessBashTool } from "./index-DFORykA2.mjs";
3
3
  import { A as getLogTtlMs, C as getBackgroundThresholdMs, M as getResultMaxBytes, c as DEFAULT_LOG_TTL_MS, k as getLogMaxBytes, n as DEFAULT_BG_THRESHOLD_MS, s as DEFAULT_LOG_MAX_BYTES, u as DEFAULT_RESULT_MAX_BYTES } from "./runnerConfig-DuWL6TJx.mjs";
4
- import { t as RmuxBackend } from "./index-BKor0Jn_.mjs";
4
+ import { t as RmuxBackend } from "./index-CJNsR-Ir.mjs";
5
5
  import { r as rtkPackageForTarget } from "./index-BCCF1sGD.mjs";
6
6
  import { t as createRunnerDependencies } from "./index-Bhh0LCcN.mjs";
7
7
  export { DEFAULT_BG_THRESHOLD_MS, DEFAULT_LOG_MAX_BYTES, DEFAULT_LOG_TTL_MS, DEFAULT_RESULT_MAX_BYTES, type IRmuxBackend, RmuxBackend, type RmuxLaunchRequest, createHeadlessBashTool, createHeadlessRunnersCommand, createRunnerDependencies, getBackgroundThresholdMs, getLogMaxBytes, getLogTtlMs, getResultMaxBytes, rtkPackageForTarget };
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { D as DEFAULT_RESULT_MAX_BYTES, T as DEFAULT_LOG_TTL_MS, c as getLogTtlMs, s as getLogMaxBytes, t as getBackgroundThresholdMs, u as getResultMaxBytes, w as DEFAULT_LOG_MAX_BYTES, y as DEFAULT_BG_THRESHOLD_MS } from "./runnerConfig-XXMq4VQA.mjs";
2
2
  import { n as createHeadlessRunnersCommand, t as createHeadlessBashTool } from "./headless-CG2i0vqB.mjs";
3
- import { t as RmuxBackend } from "./rmuxBackend-BCfO297l.mjs";
3
+ import { t as RmuxBackend } from "./rmuxBackend-BaK-R35I.mjs";
4
4
  import { r as rtkPackageForTarget } from "./rtkProcessor-DOXcp51z.mjs";
5
- import { t as createRunnerDependencies } from "./runnerDependencies-0SbEl19h.mjs";
5
+ import { t as createRunnerDependencies } from "./runnerDependencies-CYlnUyix.mjs";
6
6
  export { DEFAULT_BG_THRESHOLD_MS, DEFAULT_LOG_MAX_BYTES, DEFAULT_LOG_TTL_MS, DEFAULT_RESULT_MAX_BYTES, RmuxBackend, createHeadlessBashTool, createHeadlessRunnersCommand, createRunnerDependencies, getBackgroundThresholdMs, getLogMaxBytes, getLogTtlMs, getResultMaxBytes, rtkPackageForTarget };
@@ -1,7 +1,7 @@
1
1
  import { g as noninteractiveEnvironment, u as getResultMaxBytes } from "./runnerConfig-XXMq4VQA.mjs";
2
2
  import { t as PO_SESSION_ID_ENV } from "./session-CdVeSD7F.mjs";
3
- import { NO_TERMINAL_INPUT_ENV, cleanupSupervisorFiles, readExitMetadata, supervisorCommand, supervisorPaths, writeCommandSpec } from "./services/runnerSupervisor/index.mjs";
4
3
  import { LIFELINE_ENV } from "./services/lifeline/client.mjs";
4
+ import { NO_TERMINAL_INPUT_ENV, cleanupSupervisorFiles, readExitMetadata, supervisorCommand, supervisorPaths, writeCommandSpec } from "./services/runnerSupervisor/index.mjs";
5
5
  import fs from "node:fs/promises";
6
6
  //#region src/constants/launcher.ts
7
7
  const NON_INTERACTIVE_ENV = {
@@ -161,4 +161,4 @@ function defined(env) {
161
161
  //#endregion
162
162
  export { TERM_GRACE_MS as n, Launcher as t };
163
163
 
164
- //# sourceMappingURL=launcher-CwVJ55MC.mjs.map
164
+ //# sourceMappingURL=launcher-3xMxw72I.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"launcher-CwVJ55MC.mjs","names":[],"sources":["../src/constants/launcher.ts","../src/services/launcher/index.ts"],"sourcesContent":["export const NON_INTERACTIVE_ENV = { FORCE_COLOR: '1', CI: '1' };\nexport const TERM_GRACE_MS = 2_000;\nexport const LIVENESS_POLL_MS = 100;\nexport const MEMORY_BUFFER_FACTOR = 2;\nexport const GATE_MODE = 0o600;\n","import fs from 'node:fs/promises';\n\nimport {\n GATE_MODE,\n LIVENESS_POLL_MS,\n MEMORY_BUFFER_FACTOR,\n NON_INTERACTIVE_ENV,\n TERM_GRACE_MS,\n} from '../../constants/launcher';\nimport { PO_SESSION_ID_ENV } from '../../constants/session';\nimport type { IClock } from '../../types/clock';\nimport type { ILauncher, LaunchRequest, RunHandle } from '../../types/launcher';\nimport type { ILifeline } from '../../types/lifeline';\nimport type { ILogFile } from '../../types/logFile';\nimport type { IProcessControl } from '../../types/processControl';\nimport type { RunnerSettings } from '../../types/runnerSettings';\nimport type { ExitResult, ISpawner, SpawnedProcess } from '../../types/spawner';\nimport { LIFELINE_ENV } from '../lifeline/client';\nimport { getResultMaxBytes, noninteractiveEnvironment } from '../runnerConfig';\nimport type { IRunnerPaths } from '../runnerPaths/type';\nimport {\n cleanupSupervisorFiles,\n NO_TERMINAL_INPUT_ENV,\n readExitMetadata,\n supervisorCommand,\n supervisorPaths,\n writeCommandSpec,\n} from '../runnerSupervisor';\n\n/**\n * Prompts belong on a screen, not in a log file, so `CI` stays set.\n *\n * Colour is kept: the log is replayed into the transcript, and the scrub keeps\n * SGR while still removing the cursor moves that made raw output unreadable.\n */\n/** In-memory output is a multiple of the result ceiling; the log holds the rest. */\n\nexport class Launcher implements ILauncher {\n constructor(\n private readonly spawner: ISpawner,\n private readonly processControl: IProcessControl,\n private readonly logFile: ILogFile,\n private readonly clock: IClock,\n private readonly paths: IRunnerPaths,\n private readonly lifeline?: ILifeline,\n private readonly environment: Readonly<Record<string, string | undefined>> = process.env,\n private readonly settings?: RunnerSettings,\n ) {}\n\n async launch(request: LaunchRequest): Promise<RunHandle> {\n await this.paths.ensureDirectories(request.sessionId);\n const writer = await this.logFile.open(request.id);\n const memoryLimit = getResultMaxBytes(this.environment, this.settings) * MEMORY_BUFFER_FACTOR;\n\n let buffer = '';\n let buffering = true;\n let settled = false;\n let resolveCompletion: (result: ExitResult) => void = () => undefined;\n let rejectCompletion: (error: Error) => void = () => undefined;\n const completion = new Promise<ExitResult>((resolve, reject) => {\n resolveCompletion = resolve;\n rejectCompletion = reject;\n });\n\n const supervisor = supervisorPaths(this.paths.stateDirectory(request.sessionId), request.id);\n const { [LIFELINE_ENV]: _inheritedLifeline, ...environment } = this.environment;\n const env = {\n ...environment,\n ...request.env,\n ...NON_INTERACTIVE_ENV,\n ...noninteractiveEnvironment(environment),\n ...NO_TERMINAL_INPUT_ENV,\n [PO_SESSION_ID_ENV]: request.sessionId,\n [LIFELINE_ENV]: this.lifeline?.path(),\n };\n let child: SpawnedProcess;\n try {\n await cleanupSupervisorFiles(supervisor);\n await writeCommandSpec(supervisor.spec, { command: request.command, cwd: request.cwd, env: defined(env) });\n // Nothing has to be attached before this runner starts, so its gate opens at once.\n await fs.writeFile(supervisor.gate, '', { mode: GATE_MODE });\n const command = await supervisorCommand(supervisor);\n child = this.spawner.spawn({\n // runnerHost supervises the command and kills its group if the owner dies.\n command,\n cwd: request.cwd,\n env,\n detached: true,\n });\n } catch (error) {\n await writer.close().catch((closeError: unknown) => process.emitWarning(String(closeError)));\n await cleanupSupervisorFiles(supervisor);\n throw error;\n }\n\n const failLaunch = async (error: unknown): Promise<void> => {\n if (settled) return;\n settled = true;\n await writer.close().catch((closeError: unknown) => process.emitWarning(String(closeError)));\n await cleanupSupervisorFiles(supervisor);\n try {\n child.kill('SIGTERM');\n } catch (killError) {\n process.emitWarning(`Could not stop runner after log failure: ${String(killError)}`);\n }\n rejectCompletion(error instanceof Error ? error : new Error(String(error)));\n };\n child.onOutput((chunk) => {\n if (settled) return;\n try {\n void writer.append(chunk).catch(failLaunch);\n } catch (error) {\n void failLaunch(error);\n return;\n }\n if (!buffering) return;\n buffer += chunk;\n if (buffer.length > memoryLimit) buffer = buffer.slice(-memoryLimit);\n });\n\n child.onError(failLaunch);\n\n child.onExit(async (result) => {\n if (settled) return;\n settled = true;\n try {\n await writer.close();\n // The supervisor's sidecar preserves the original signal.\n const outcome = (await readExitMetadata(supervisor.exit)) ?? result;\n await cleanupSupervisorFiles(supervisor);\n resolveCompletion(outcome);\n } catch (error) {\n await cleanupSupervisorFiles(supervisor);\n rejectCompletion(error instanceof Error ? error : new Error(String(error)));\n }\n });\n\n // The launcher must never hold the pi process open on a runner's behalf.\n child.unref();\n\n const stop = async (): Promise<boolean> => (child.pid === undefined ? false : this.stop(child.pid));\n\n return {\n id: request.id,\n name: request.name,\n pid: child.pid,\n logPath: writer.path,\n backend: 'native',\n output: async () => buffer,\n completion: () => completion,\n detach: () => {\n buffering = false;\n buffer = '';\n },\n stop,\n };\n }\n\n async stop(pid: number): Promise<boolean> {\n if (!this.processControl.isAlive(pid)) return false;\n\n this.processControl.signalGroup(pid, 'SIGTERM');\n const deadline = this.clock.now() + TERM_GRACE_MS;\n while (this.clock.now() < deadline) {\n if (!this.processControl.isAlive(pid)) return true;\n await this.sleep(LIVENESS_POLL_MS);\n }\n\n this.processControl.signalGroup(pid, 'SIGKILL');\n return true;\n }\n\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => {\n this.clock.after(ms, resolve);\n });\n }\n}\n\n/** The spec is JSON, which has no way to carry an unset variable. */\nfunction defined(env: Readonly<Record<string, string | undefined>>): Record<string, string> {\n return Object.fromEntries(Object.entries(env).filter((entry): entry is [string, string] => entry[1] !== undefined));\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"launcher-3xMxw72I.mjs","names":[],"sources":["../src/constants/launcher.ts","../src/services/launcher/index.ts"],"sourcesContent":["export const NON_INTERACTIVE_ENV = { FORCE_COLOR: '1', CI: '1' };\nexport const TERM_GRACE_MS = 2_000;\nexport const LIVENESS_POLL_MS = 100;\nexport const MEMORY_BUFFER_FACTOR = 2;\nexport const GATE_MODE = 0o600;\n","import fs from 'node:fs/promises';\n\nimport {\n GATE_MODE,\n LIVENESS_POLL_MS,\n MEMORY_BUFFER_FACTOR,\n NON_INTERACTIVE_ENV,\n TERM_GRACE_MS,\n} from '../../constants/launcher';\nimport { PO_SESSION_ID_ENV } from '../../constants/session';\nimport type { IClock } from '../../types/clock';\nimport type { ILauncher, LaunchRequest, RunHandle } from '../../types/launcher';\nimport type { ILifeline } from '../../types/lifeline';\nimport type { ILogFile } from '../../types/logFile';\nimport type { IProcessControl } from '../../types/processControl';\nimport type { RunnerSettings } from '../../types/runnerSettings';\nimport type { ExitResult, ISpawner, SpawnedProcess } from '../../types/spawner';\nimport { LIFELINE_ENV } from '../lifeline/client';\nimport { getResultMaxBytes, noninteractiveEnvironment } from '../runnerConfig';\nimport type { IRunnerPaths } from '../runnerPaths/type';\nimport {\n cleanupSupervisorFiles,\n NO_TERMINAL_INPUT_ENV,\n readExitMetadata,\n supervisorCommand,\n supervisorPaths,\n writeCommandSpec,\n} from '../runnerSupervisor';\n\n/**\n * Prompts belong on a screen, not in a log file, so `CI` stays set.\n *\n * Colour is kept: the log is replayed into the transcript, and the scrub keeps\n * SGR while still removing the cursor moves that made raw output unreadable.\n */\n/** In-memory output is a multiple of the result ceiling; the log holds the rest. */\n\nexport class Launcher implements ILauncher {\n constructor(\n private readonly spawner: ISpawner,\n private readonly processControl: IProcessControl,\n private readonly logFile: ILogFile,\n private readonly clock: IClock,\n private readonly paths: IRunnerPaths,\n private readonly lifeline?: ILifeline,\n private readonly environment: Readonly<Record<string, string | undefined>> = process.env,\n private readonly settings?: RunnerSettings,\n ) {}\n\n async launch(request: LaunchRequest): Promise<RunHandle> {\n await this.paths.ensureDirectories(request.sessionId);\n const writer = await this.logFile.open(request.id);\n const memoryLimit = getResultMaxBytes(this.environment, this.settings) * MEMORY_BUFFER_FACTOR;\n\n let buffer = '';\n let buffering = true;\n let settled = false;\n let resolveCompletion: (result: ExitResult) => void = () => undefined;\n let rejectCompletion: (error: Error) => void = () => undefined;\n const completion = new Promise<ExitResult>((resolve, reject) => {\n resolveCompletion = resolve;\n rejectCompletion = reject;\n });\n\n const supervisor = supervisorPaths(this.paths.stateDirectory(request.sessionId), request.id);\n const { [LIFELINE_ENV]: _inheritedLifeline, ...environment } = this.environment;\n const env = {\n ...environment,\n ...request.env,\n ...NON_INTERACTIVE_ENV,\n ...noninteractiveEnvironment(environment),\n ...NO_TERMINAL_INPUT_ENV,\n [PO_SESSION_ID_ENV]: request.sessionId,\n [LIFELINE_ENV]: this.lifeline?.path(),\n };\n let child: SpawnedProcess;\n try {\n await cleanupSupervisorFiles(supervisor);\n await writeCommandSpec(supervisor.spec, { command: request.command, cwd: request.cwd, env: defined(env) });\n // Nothing has to be attached before this runner starts, so its gate opens at once.\n await fs.writeFile(supervisor.gate, '', { mode: GATE_MODE });\n const command = await supervisorCommand(supervisor);\n child = this.spawner.spawn({\n // runnerHost supervises the command and kills its group if the owner dies.\n command,\n cwd: request.cwd,\n env,\n detached: true,\n });\n } catch (error) {\n await writer.close().catch((closeError: unknown) => process.emitWarning(String(closeError)));\n await cleanupSupervisorFiles(supervisor);\n throw error;\n }\n\n const failLaunch = async (error: unknown): Promise<void> => {\n if (settled) return;\n settled = true;\n await writer.close().catch((closeError: unknown) => process.emitWarning(String(closeError)));\n await cleanupSupervisorFiles(supervisor);\n try {\n child.kill('SIGTERM');\n } catch (killError) {\n process.emitWarning(`Could not stop runner after log failure: ${String(killError)}`);\n }\n rejectCompletion(error instanceof Error ? error : new Error(String(error)));\n };\n child.onOutput((chunk) => {\n if (settled) return;\n try {\n void writer.append(chunk).catch(failLaunch);\n } catch (error) {\n void failLaunch(error);\n return;\n }\n if (!buffering) return;\n buffer += chunk;\n if (buffer.length > memoryLimit) buffer = buffer.slice(-memoryLimit);\n });\n\n child.onError(failLaunch);\n\n child.onExit(async (result) => {\n if (settled) return;\n settled = true;\n try {\n await writer.close();\n // The supervisor's sidecar preserves the original signal.\n const outcome = (await readExitMetadata(supervisor.exit)) ?? result;\n await cleanupSupervisorFiles(supervisor);\n resolveCompletion(outcome);\n } catch (error) {\n await cleanupSupervisorFiles(supervisor);\n rejectCompletion(error instanceof Error ? error : new Error(String(error)));\n }\n });\n\n // The launcher must never hold the pi process open on a runner's behalf.\n child.unref();\n\n const stop = async (): Promise<boolean> => (child.pid === undefined ? false : this.stop(child.pid));\n\n return {\n id: request.id,\n name: request.name,\n pid: child.pid,\n logPath: writer.path,\n backend: 'native',\n output: async () => buffer,\n completion: () => completion,\n detach: () => {\n buffering = false;\n buffer = '';\n },\n stop,\n };\n }\n\n async stop(pid: number): Promise<boolean> {\n if (!this.processControl.isAlive(pid)) return false;\n\n this.processControl.signalGroup(pid, 'SIGTERM');\n const deadline = this.clock.now() + TERM_GRACE_MS;\n while (this.clock.now() < deadline) {\n if (!this.processControl.isAlive(pid)) return true;\n await this.sleep(LIVENESS_POLL_MS);\n }\n\n this.processControl.signalGroup(pid, 'SIGKILL');\n return true;\n }\n\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => {\n this.clock.after(ms, resolve);\n });\n }\n}\n\n/** The spec is JSON, which has no way to carry an unset variable. */\nfunction defined(env: Readonly<Record<string, string | undefined>>): Record<string, string> {\n return Object.fromEntries(Object.entries(env).filter((entry): entry is [string, string] => entry[1] !== undefined));\n}\n"],"mappings":"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"}
@@ -1,8 +1,8 @@
1
1
  const require_rolldown_runtime = require("./rolldown-runtime-C0BPl7ul.cjs");
2
2
  const require_runnerConfig = require("./runnerConfig-BoLMinlx.cjs");
3
3
  const require_session = require("./session-DnUj8Q71.cjs");
4
- const require_services_runnerSupervisor_index = require("./services/runnerSupervisor/index.cjs");
5
4
  const require_services_lifeline_client = require("./services/lifeline/client.cjs");
5
+ const require_services_runnerSupervisor_index = require("./services/runnerSupervisor/index.cjs");
6
6
  let node_fs_promises = require("node:fs/promises");
7
7
  node_fs_promises = require_rolldown_runtime.__toESM(node_fs_promises, 1);
8
8
  //#region src/constants/launcher.ts
@@ -174,4 +174,4 @@ Object.defineProperty(exports, "TERM_GRACE_MS", {
174
174
  }
175
175
  });
176
176
 
177
- //# sourceMappingURL=launcher-ksyHY-iG.cjs.map
177
+ //# sourceMappingURL=launcher-D7qAhSRL.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"launcher-ksyHY-iG.cjs","names":["getResultMaxBytes","supervisorPaths","LIFELINE_ENV","noninteractiveEnvironment","NO_TERMINAL_INPUT_ENV","PO_SESSION_ID_ENV","cleanupSupervisorFiles","writeCommandSpec","fs","supervisorCommand","readExitMetadata"],"sources":["../src/constants/launcher.ts","../src/services/launcher/index.ts"],"sourcesContent":["export const NON_INTERACTIVE_ENV = { FORCE_COLOR: '1', CI: '1' };\nexport const TERM_GRACE_MS = 2_000;\nexport const LIVENESS_POLL_MS = 100;\nexport const MEMORY_BUFFER_FACTOR = 2;\nexport const GATE_MODE = 0o600;\n","import fs from 'node:fs/promises';\n\nimport {\n GATE_MODE,\n LIVENESS_POLL_MS,\n MEMORY_BUFFER_FACTOR,\n NON_INTERACTIVE_ENV,\n TERM_GRACE_MS,\n} from '../../constants/launcher';\nimport { PO_SESSION_ID_ENV } from '../../constants/session';\nimport type { IClock } from '../../types/clock';\nimport type { ILauncher, LaunchRequest, RunHandle } from '../../types/launcher';\nimport type { ILifeline } from '../../types/lifeline';\nimport type { ILogFile } from '../../types/logFile';\nimport type { IProcessControl } from '../../types/processControl';\nimport type { RunnerSettings } from '../../types/runnerSettings';\nimport type { ExitResult, ISpawner, SpawnedProcess } from '../../types/spawner';\nimport { LIFELINE_ENV } from '../lifeline/client';\nimport { getResultMaxBytes, noninteractiveEnvironment } from '../runnerConfig';\nimport type { IRunnerPaths } from '../runnerPaths/type';\nimport {\n cleanupSupervisorFiles,\n NO_TERMINAL_INPUT_ENV,\n readExitMetadata,\n supervisorCommand,\n supervisorPaths,\n writeCommandSpec,\n} from '../runnerSupervisor';\n\n/**\n * Prompts belong on a screen, not in a log file, so `CI` stays set.\n *\n * Colour is kept: the log is replayed into the transcript, and the scrub keeps\n * SGR while still removing the cursor moves that made raw output unreadable.\n */\n/** In-memory output is a multiple of the result ceiling; the log holds the rest. */\n\nexport class Launcher implements ILauncher {\n constructor(\n private readonly spawner: ISpawner,\n private readonly processControl: IProcessControl,\n private readonly logFile: ILogFile,\n private readonly clock: IClock,\n private readonly paths: IRunnerPaths,\n private readonly lifeline?: ILifeline,\n private readonly environment: Readonly<Record<string, string | undefined>> = process.env,\n private readonly settings?: RunnerSettings,\n ) {}\n\n async launch(request: LaunchRequest): Promise<RunHandle> {\n await this.paths.ensureDirectories(request.sessionId);\n const writer = await this.logFile.open(request.id);\n const memoryLimit = getResultMaxBytes(this.environment, this.settings) * MEMORY_BUFFER_FACTOR;\n\n let buffer = '';\n let buffering = true;\n let settled = false;\n let resolveCompletion: (result: ExitResult) => void = () => undefined;\n let rejectCompletion: (error: Error) => void = () => undefined;\n const completion = new Promise<ExitResult>((resolve, reject) => {\n resolveCompletion = resolve;\n rejectCompletion = reject;\n });\n\n const supervisor = supervisorPaths(this.paths.stateDirectory(request.sessionId), request.id);\n const { [LIFELINE_ENV]: _inheritedLifeline, ...environment } = this.environment;\n const env = {\n ...environment,\n ...request.env,\n ...NON_INTERACTIVE_ENV,\n ...noninteractiveEnvironment(environment),\n ...NO_TERMINAL_INPUT_ENV,\n [PO_SESSION_ID_ENV]: request.sessionId,\n [LIFELINE_ENV]: this.lifeline?.path(),\n };\n let child: SpawnedProcess;\n try {\n await cleanupSupervisorFiles(supervisor);\n await writeCommandSpec(supervisor.spec, { command: request.command, cwd: request.cwd, env: defined(env) });\n // Nothing has to be attached before this runner starts, so its gate opens at once.\n await fs.writeFile(supervisor.gate, '', { mode: GATE_MODE });\n const command = await supervisorCommand(supervisor);\n child = this.spawner.spawn({\n // runnerHost supervises the command and kills its group if the owner dies.\n command,\n cwd: request.cwd,\n env,\n detached: true,\n });\n } catch (error) {\n await writer.close().catch((closeError: unknown) => process.emitWarning(String(closeError)));\n await cleanupSupervisorFiles(supervisor);\n throw error;\n }\n\n const failLaunch = async (error: unknown): Promise<void> => {\n if (settled) return;\n settled = true;\n await writer.close().catch((closeError: unknown) => process.emitWarning(String(closeError)));\n await cleanupSupervisorFiles(supervisor);\n try {\n child.kill('SIGTERM');\n } catch (killError) {\n process.emitWarning(`Could not stop runner after log failure: ${String(killError)}`);\n }\n rejectCompletion(error instanceof Error ? error : new Error(String(error)));\n };\n child.onOutput((chunk) => {\n if (settled) return;\n try {\n void writer.append(chunk).catch(failLaunch);\n } catch (error) {\n void failLaunch(error);\n return;\n }\n if (!buffering) return;\n buffer += chunk;\n if (buffer.length > memoryLimit) buffer = buffer.slice(-memoryLimit);\n });\n\n child.onError(failLaunch);\n\n child.onExit(async (result) => {\n if (settled) return;\n settled = true;\n try {\n await writer.close();\n // The supervisor's sidecar preserves the original signal.\n const outcome = (await readExitMetadata(supervisor.exit)) ?? result;\n await cleanupSupervisorFiles(supervisor);\n resolveCompletion(outcome);\n } catch (error) {\n await cleanupSupervisorFiles(supervisor);\n rejectCompletion(error instanceof Error ? error : new Error(String(error)));\n }\n });\n\n // The launcher must never hold the pi process open on a runner's behalf.\n child.unref();\n\n const stop = async (): Promise<boolean> => (child.pid === undefined ? false : this.stop(child.pid));\n\n return {\n id: request.id,\n name: request.name,\n pid: child.pid,\n logPath: writer.path,\n backend: 'native',\n output: async () => buffer,\n completion: () => completion,\n detach: () => {\n buffering = false;\n buffer = '';\n },\n stop,\n };\n }\n\n async stop(pid: number): Promise<boolean> {\n if (!this.processControl.isAlive(pid)) return false;\n\n this.processControl.signalGroup(pid, 'SIGTERM');\n const deadline = this.clock.now() + TERM_GRACE_MS;\n while (this.clock.now() < deadline) {\n if (!this.processControl.isAlive(pid)) return true;\n await this.sleep(LIVENESS_POLL_MS);\n }\n\n this.processControl.signalGroup(pid, 'SIGKILL');\n return true;\n }\n\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => {\n this.clock.after(ms, resolve);\n });\n }\n}\n\n/** The spec is JSON, which has no way to carry an unset variable. */\nfunction defined(env: Readonly<Record<string, string | undefined>>): Record<string, string> {\n return Object.fromEntries(Object.entries(env).filter((entry): entry is [string, string] => entry[1] !== undefined));\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"launcher-D7qAhSRL.cjs","names":["getResultMaxBytes","supervisorPaths","LIFELINE_ENV","noninteractiveEnvironment","NO_TERMINAL_INPUT_ENV","PO_SESSION_ID_ENV","cleanupSupervisorFiles","writeCommandSpec","fs","supervisorCommand","readExitMetadata"],"sources":["../src/constants/launcher.ts","../src/services/launcher/index.ts"],"sourcesContent":["export const NON_INTERACTIVE_ENV = { FORCE_COLOR: '1', CI: '1' };\nexport const TERM_GRACE_MS = 2_000;\nexport const LIVENESS_POLL_MS = 100;\nexport const MEMORY_BUFFER_FACTOR = 2;\nexport const GATE_MODE = 0o600;\n","import fs from 'node:fs/promises';\n\nimport {\n GATE_MODE,\n LIVENESS_POLL_MS,\n MEMORY_BUFFER_FACTOR,\n NON_INTERACTIVE_ENV,\n TERM_GRACE_MS,\n} from '../../constants/launcher';\nimport { PO_SESSION_ID_ENV } from '../../constants/session';\nimport type { IClock } from '../../types/clock';\nimport type { ILauncher, LaunchRequest, RunHandle } from '../../types/launcher';\nimport type { ILifeline } from '../../types/lifeline';\nimport type { ILogFile } from '../../types/logFile';\nimport type { IProcessControl } from '../../types/processControl';\nimport type { RunnerSettings } from '../../types/runnerSettings';\nimport type { ExitResult, ISpawner, SpawnedProcess } from '../../types/spawner';\nimport { LIFELINE_ENV } from '../lifeline/client';\nimport { getResultMaxBytes, noninteractiveEnvironment } from '../runnerConfig';\nimport type { IRunnerPaths } from '../runnerPaths/type';\nimport {\n cleanupSupervisorFiles,\n NO_TERMINAL_INPUT_ENV,\n readExitMetadata,\n supervisorCommand,\n supervisorPaths,\n writeCommandSpec,\n} from '../runnerSupervisor';\n\n/**\n * Prompts belong on a screen, not in a log file, so `CI` stays set.\n *\n * Colour is kept: the log is replayed into the transcript, and the scrub keeps\n * SGR while still removing the cursor moves that made raw output unreadable.\n */\n/** In-memory output is a multiple of the result ceiling; the log holds the rest. */\n\nexport class Launcher implements ILauncher {\n constructor(\n private readonly spawner: ISpawner,\n private readonly processControl: IProcessControl,\n private readonly logFile: ILogFile,\n private readonly clock: IClock,\n private readonly paths: IRunnerPaths,\n private readonly lifeline?: ILifeline,\n private readonly environment: Readonly<Record<string, string | undefined>> = process.env,\n private readonly settings?: RunnerSettings,\n ) {}\n\n async launch(request: LaunchRequest): Promise<RunHandle> {\n await this.paths.ensureDirectories(request.sessionId);\n const writer = await this.logFile.open(request.id);\n const memoryLimit = getResultMaxBytes(this.environment, this.settings) * MEMORY_BUFFER_FACTOR;\n\n let buffer = '';\n let buffering = true;\n let settled = false;\n let resolveCompletion: (result: ExitResult) => void = () => undefined;\n let rejectCompletion: (error: Error) => void = () => undefined;\n const completion = new Promise<ExitResult>((resolve, reject) => {\n resolveCompletion = resolve;\n rejectCompletion = reject;\n });\n\n const supervisor = supervisorPaths(this.paths.stateDirectory(request.sessionId), request.id);\n const { [LIFELINE_ENV]: _inheritedLifeline, ...environment } = this.environment;\n const env = {\n ...environment,\n ...request.env,\n ...NON_INTERACTIVE_ENV,\n ...noninteractiveEnvironment(environment),\n ...NO_TERMINAL_INPUT_ENV,\n [PO_SESSION_ID_ENV]: request.sessionId,\n [LIFELINE_ENV]: this.lifeline?.path(),\n };\n let child: SpawnedProcess;\n try {\n await cleanupSupervisorFiles(supervisor);\n await writeCommandSpec(supervisor.spec, { command: request.command, cwd: request.cwd, env: defined(env) });\n // Nothing has to be attached before this runner starts, so its gate opens at once.\n await fs.writeFile(supervisor.gate, '', { mode: GATE_MODE });\n const command = await supervisorCommand(supervisor);\n child = this.spawner.spawn({\n // runnerHost supervises the command and kills its group if the owner dies.\n command,\n cwd: request.cwd,\n env,\n detached: true,\n });\n } catch (error) {\n await writer.close().catch((closeError: unknown) => process.emitWarning(String(closeError)));\n await cleanupSupervisorFiles(supervisor);\n throw error;\n }\n\n const failLaunch = async (error: unknown): Promise<void> => {\n if (settled) return;\n settled = true;\n await writer.close().catch((closeError: unknown) => process.emitWarning(String(closeError)));\n await cleanupSupervisorFiles(supervisor);\n try {\n child.kill('SIGTERM');\n } catch (killError) {\n process.emitWarning(`Could not stop runner after log failure: ${String(killError)}`);\n }\n rejectCompletion(error instanceof Error ? error : new Error(String(error)));\n };\n child.onOutput((chunk) => {\n if (settled) return;\n try {\n void writer.append(chunk).catch(failLaunch);\n } catch (error) {\n void failLaunch(error);\n return;\n }\n if (!buffering) return;\n buffer += chunk;\n if (buffer.length > memoryLimit) buffer = buffer.slice(-memoryLimit);\n });\n\n child.onError(failLaunch);\n\n child.onExit(async (result) => {\n if (settled) return;\n settled = true;\n try {\n await writer.close();\n // The supervisor's sidecar preserves the original signal.\n const outcome = (await readExitMetadata(supervisor.exit)) ?? result;\n await cleanupSupervisorFiles(supervisor);\n resolveCompletion(outcome);\n } catch (error) {\n await cleanupSupervisorFiles(supervisor);\n rejectCompletion(error instanceof Error ? error : new Error(String(error)));\n }\n });\n\n // The launcher must never hold the pi process open on a runner's behalf.\n child.unref();\n\n const stop = async (): Promise<boolean> => (child.pid === undefined ? false : this.stop(child.pid));\n\n return {\n id: request.id,\n name: request.name,\n pid: child.pid,\n logPath: writer.path,\n backend: 'native',\n output: async () => buffer,\n completion: () => completion,\n detach: () => {\n buffering = false;\n buffer = '';\n },\n stop,\n };\n }\n\n async stop(pid: number): Promise<boolean> {\n if (!this.processControl.isAlive(pid)) return false;\n\n this.processControl.signalGroup(pid, 'SIGTERM');\n const deadline = this.clock.now() + TERM_GRACE_MS;\n while (this.clock.now() < deadline) {\n if (!this.processControl.isAlive(pid)) return true;\n await this.sleep(LIVENESS_POLL_MS);\n }\n\n this.processControl.signalGroup(pid, 'SIGKILL');\n return true;\n }\n\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => {\n this.clock.after(ms, resolve);\n });\n }\n}\n\n/** The spec is JSON, which has no way to carry an unset variable. */\nfunction defined(env: Readonly<Record<string, string | undefined>>): Record<string, string> {\n return Object.fromEntries(Object.entries(env).filter((entry): entry is [string, string] => entry[1] !== undefined));\n}\n"],"mappings":"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"}
package/dist/launcher.cjs CHANGED
@@ -1,3 +1,3 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_launcher = require("./launcher-ksyHY-iG.cjs");
2
+ const require_launcher = require("./launcher-D7qAhSRL.cjs");
3
3
  exports.Launcher = require_launcher.Launcher;
package/dist/launcher.mjs CHANGED
@@ -1 +1 @@
1
- export { t as Launcher } from "./launcher-CwVJ55MC.mjs";
1
+ export { t as Launcher } from "./launcher-3xMxw72I.mjs";
@@ -3,8 +3,8 @@ import { n as OBSERVE_SESSIONS_FLAG, r as SOCKET_NAME, t as OBSERVER_TIMEOUT_MS
3
3
  import { randomUUID } from "node:crypto";
4
4
  import fs from "node:fs/promises";
5
5
  import path from "node:path";
6
- import { spawn } from "node:child_process";
7
6
  import net from "node:net";
7
+ import { spawn } from "node:child_process";
8
8
  //#region src/services/lifeline/index.ts
9
9
  /** Session lifeline and external cleanup observer. */
10
10
  var NodeLifeline = class {
@@ -211,4 +211,4 @@ function listen(server, target) {
211
211
  //#endregion
212
212
  export { NodeLifeline as n, LifelineObserver as t };
213
213
 
214
- //# sourceMappingURL=lifeline-doL4-_14.mjs.map
214
+ //# sourceMappingURL=lifeline-C1v2ItCY.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"lifeline-doL4-_14.mjs","names":["files"],"sources":["../src/services/lifeline/index.ts"],"sourcesContent":["import { type ChildProcess, spawn } from 'node:child_process';\nimport { randomUUID } from 'node:crypto';\nimport files from 'node:fs/promises';\nimport net from 'node:net';\nimport path from 'node:path';\n\nimport {\n OBSERVE_SESSIONS_FLAG,\n OBSERVER_TIMEOUT_MS,\n SOCKET_MODE,\n SOCKET_NAME,\n SOCKET_PATH_LIMIT,\n} from '../../constants/lifeline';\nimport type { ILifeline, LifelineAdmission, LifelineWatch } from '../../types/lifeline';\nimport type { IRunnerPaths } from '../runnerPaths/type';\nimport { runtimeEntry } from '../runnerSupervisor';\n\n/** Session lifeline and external cleanup observer. */\n\nexport class NodeLifeline implements ILifeline {\n private server: net.Server | undefined;\n private target: string | undefined;\n private ownerGeneration: string | undefined;\n /** Closing a server stops it accepting; only the connections carry the death signal. */\n private readonly connections = new Set<net.Socket>();\n /** Started for this lifeline alone when no shared observer was given, and closed with it. */\n private ownObserver: LifelineObserver | undefined;\n\n /** Without `environment` nothing observes the lifeline; `observer` is shared by every session of this owner. */\n constructor(\n private readonly paths: IRunnerPaths,\n private readonly environment?: Readonly<Record<string, string | undefined>>,\n private readonly observer?: LifelineObserver,\n ) {}\n\n async arm(sessionId: string): Promise<string | undefined> {\n if (this.target) return this.target;\n try {\n await this.paths.ensureDirectories(sessionId);\n const generation = randomUUID();\n const marker = path.join(this.paths.stateDirectory(sessionId), '.owner');\n await files.writeFile(marker, generation, { mode: SOCKET_MODE });\n this.ownerGeneration = generation;\n let target = this.socketPath(sessionId, generation);\n // A socket left behind by a SIGKILLed predecessor refuses connections, never accepts them.\n if (target) await files.rm(target, { force: true });\n const server = net.createServer((connection) => {\n // Runners need only the close edge, so their liveness and their data are equally irrelevant.\n connection.unref();\n connection.resume();\n this.connections.add(connection);\n connection.on('close', () => this.connections.delete(connection));\n });\n await listen(server, target);\n if (target) await files.chmod(target, SOCKET_MODE);\n else {\n const address = server.address();\n if (!address || typeof address === 'string') throw new Error('Missing runner lifeline address');\n target = `tcp://127.0.0.1:${address.port}`;\n }\n // The lifeline must never be the reason pi stays alive.\n server.unref();\n this.server = server;\n this.target = target;\n if (this.environment !== undefined) {\n const observer = this.observer ?? (this.ownObserver ??= new LifelineObserver(this.environment));\n await observer.watch({\n sessionId,\n target,\n repository: await this.paths.repositoryPath(),\n ownerPid: process.pid,\n generation,\n marker,\n environment: this.environment,\n });\n }\n return target;\n } catch (error) {\n await this.dispose();\n // Runners stay unwatched rather than unlaunchable, so a session without a\n // lifeline degrades to running them unsupervised instead of failing.\n process.emitWarning(`Could not arm the po-runner lifeline: ${String(error)}`);\n return undefined;\n }\n }\n\n path(): string | undefined {\n return this.target;\n }\n\n generation(): string | undefined {\n return this.ownerGeneration;\n }\n\n async dispose(): Promise<void> {\n // Dropping the connections is what a runner sees, and it is the whole point\n // of disposing: closing the server alone would leave every runner waiting.\n for (const connection of this.connections) connection.destroy();\n this.connections.clear();\n this.ownObserver?.close();\n this.ownObserver = undefined;\n if (this.server) {\n try {\n this.server.close();\n } catch (error) {\n process.emitWarning(`Could not close the po-runner lifeline: ${String(error)}`);\n }\n this.server = undefined;\n }\n const target = this.target;\n this.target = undefined;\n if (target) {\n try {\n if (!target.startsWith('tcp://')) await files.rm(target, { force: true });\n } catch (error) {\n process.emitWarning(`Could not remove the po-runner lifeline: ${String(error)}`);\n }\n }\n }\n\n /** The socket must never escape the admitted session temp directory. */\n private socketPath(sessionId: string, generation: string): string {\n const target = path.join(this.paths.stateDirectory(sessionId), `${generation.slice(0, 8)}-${SOCKET_NAME}`);\n if (Buffer.byteLength(target) > SOCKET_PATH_LIMIT) {\n return ''; // Bind loopback instead of writing a socket outside the session.\n }\n return target;\n }\n}\n\n/**\n * One detached process that cleans up after every session of this owner, SIGKILL included.\n *\n * Sessions share it rather than each starting their own: an idle observer costs a whole Node\n * process, and it only waits on sockets. Each armed lifeline is sent to it over IPC and\n * confirmed once it is connected. Neither the process nor its channel keeps the owner alive.\n */\nexport class LifelineObserver {\n /** One child at a time: set from spawn until that child exits. */\n private starting: Promise<ChildProcess> | undefined;\n private closed = false;\n private readonly admissions = new Map<string, (error?: Error) => void>();\n\n /** `environment` is the observer process's own; each watch carries its session's. */\n constructor(private readonly environment: Readonly<Record<string, string | undefined>>) {}\n\n /** Resolves once the observer holds the lifeline, so an owner death from then on is cleaned up. */\n async watch(watch: LifelineWatch): Promise<void> {\n const child = await this.start();\n await new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => settle(new Error('Runner observer admission timed out')), OBSERVER_TIMEOUT_MS);\n const settle = (error?: Error): void => {\n clearTimeout(timeout);\n this.admissions.delete(watch.generation);\n if (error) reject(error);\n else resolve();\n };\n this.admissions.set(watch.generation, settle);\n child.send(watch, undefined, undefined, (error) => {\n if (error) settle(error);\n });\n });\n }\n\n /** The observer exits once every lifeline it still holds has closed and been cleaned up. */\n close(): void {\n this.closed = true;\n void this.starting?.then(\n (child) => {\n if (child.connected) child.disconnect();\n },\n () => undefined,\n );\n }\n\n private start(): Promise<ChildProcess> {\n if (this.closed) return Promise.reject(new Error('Runner observer is closed'));\n this.starting ??= this.spawn().catch((error: unknown) => {\n this.starting = undefined;\n throw error;\n });\n return this.starting;\n }\n\n private async spawn(): Promise<ChildProcess> {\n const child = spawn(process.execPath, [await runtimeEntry('runnerHost'), OBSERVE_SESSIONS_FLAG], {\n env: this.environment,\n detached: true,\n stdio: ['ignore', 'ignore', 'ignore', 'ipc'],\n });\n await new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => {\n child.kill();\n reject(new Error('Runner observer startup timed out'));\n }, OBSERVER_TIMEOUT_MS);\n const fail = (error: Error): void => {\n clearTimeout(timeout);\n reject(error);\n };\n child.on('error', (error) => {\n fail(error);\n process.emitWarning(`po-runner observer failed: ${error.message}`);\n });\n child.once('exit', () => {\n fail(new Error('Runner observer exited before admission'));\n // A dead observer fails what it was admitting, and the next arm starts a fresh one.\n this.starting = undefined;\n for (const settle of this.admissions.values()) settle(new Error('Runner observer exited'));\n });\n child.on('message', (message: unknown) => {\n if (message === 'ready') {\n clearTimeout(timeout);\n resolve();\n return;\n }\n const admission = message as Partial<LifelineAdmission> | null;\n const settle =\n typeof admission?.generation === 'string' ? this.admissions.get(admission.generation) : undefined;\n settle?.(typeof admission?.error === 'string' ? new Error(admission.error) : undefined);\n });\n });\n // The observer must never be the reason the owner stays alive.\n child.unref();\n child.channel?.unref();\n if (this.closed) child.disconnect();\n return child;\n }\n}\n\n/** Binding must complete before the first runner launches, so arming waits for it. */\nfunction listen(server: net.Server, target: string): Promise<void> {\n return new Promise((resolve, reject) => {\n server.once('error', reject);\n server.listen(target ? { path: target } : { host: '127.0.0.1', port: 0 }, () => {\n server.removeListener('error', reject);\n // Later errors would otherwise reach the process as unhandled.\n server.on('error', (error: Error) => process.emitWarning(`po-runner lifeline failed: ${error.message}`));\n resolve();\n });\n });\n}\n"],"mappings":";;;;;;;;;AAmBA,IAAa,eAAb,MAA+C;CAW1B;CACA;CACA;CAZnB;CACA;CACA;;CAEA,8BAA+B,IAAI,IAAgB;;CAEnD;;CAGA,YACE,OACA,aACA,UACA;EAHiB,KAAA,QAAA;EACA,KAAA,cAAA;EACA,KAAA,WAAA;CAChB;CAEH,MAAM,IAAI,WAAgD;EACxD,IAAI,KAAK,QAAQ,OAAO,KAAK;EAC7B,IAAI;GACF,MAAM,KAAK,MAAM,kBAAkB,SAAS;GAC5C,MAAM,aAAa,WAAW;GAC9B,MAAM,SAAS,KAAK,KAAK,KAAK,MAAM,eAAe,SAAS,GAAG,QAAQ;GACvE,MAAMA,GAAM,UAAU,QAAQ,YAAY,EAAE,MAAA,IAAkB,CAAC;GAC/D,KAAK,kBAAkB;GACvB,IAAI,SAAS,KAAK,WAAW,WAAW,UAAU;GAElD,IAAI,QAAQ,MAAMA,GAAM,GAAG,QAAQ,EAAE,OAAO,KAAK,CAAC;GAClD,MAAM,SAAS,IAAI,cAAc,eAAe;IAE9C,WAAW,MAAM;IACjB,WAAW,OAAO;IAClB,KAAK,YAAY,IAAI,UAAU;IAC/B,WAAW,GAAG,eAAe,KAAK,YAAY,OAAO,UAAU,CAAC;GAClE,CAAC;GACD,MAAM,OAAO,QAAQ,MAAM;GAC3B,IAAI,QAAQ,MAAMA,GAAM,MAAM,QAAA,GAAmB;QAC5C;IACH,MAAM,UAAU,OAAO,QAAQ;IAC/B,IAAI,CAAC,WAAW,OAAO,YAAY,UAAU,MAAM,IAAI,MAAM,iCAAiC;IAC9F,SAAS,mBAAmB,QAAQ;GACtC;GAEA,OAAO,MAAM;GACb,KAAK,SAAS;GACd,KAAK,SAAS;GACd,IAAI,KAAK,gBAAgB,KAAA,GAEvB,OADiB,KAAK,aAAa,KAAK,gBAAgB,IAAI,iBAAiB,KAAK,WAAW,GAAA,CAC9E,MAAM;IACnB;IACA;IACA,YAAY,MAAM,KAAK,MAAM,eAAe;IAC5C,UAAU,QAAQ;IAClB;IACA;IACA,aAAa,KAAK;GACpB,CAAC;GAEH,OAAO;EACT,SAAS,OAAO;GACd,MAAM,KAAK,QAAQ;GAGnB,QAAQ,YAAY,yCAAyC,OAAO,KAAK,GAAG;GAC5E;EACF;CACF;CAEA,OAA2B;EACzB,OAAO,KAAK;CACd;CAEA,aAAiC;EAC/B,OAAO,KAAK;CACd;CAEA,MAAM,UAAyB;EAG7B,KAAK,MAAM,cAAc,KAAK,aAAa,WAAW,QAAQ;EAC9D,KAAK,YAAY,MAAM;EACvB,KAAK,aAAa,MAAM;EACxB,KAAK,cAAc,KAAA;EACnB,IAAI,KAAK,QAAQ;GACf,IAAI;IACF,KAAK,OAAO,MAAM;GACpB,SAAS,OAAO;IACd,QAAQ,YAAY,2CAA2C,OAAO,KAAK,GAAG;GAChF;GACA,KAAK,SAAS,KAAA;EAChB;EACA,MAAM,SAAS,KAAK;EACpB,KAAK,SAAS,KAAA;EACd,IAAI,QACF,IAAI;GACF,IAAI,CAAC,OAAO,WAAW,QAAQ,GAAG,MAAMA,GAAM,GAAG,QAAQ,EAAE,OAAO,KAAK,CAAC;EAC1E,SAAS,OAAO;GACd,QAAQ,YAAY,4CAA4C,OAAO,KAAK,GAAG;EACjF;CAEJ;;CAGA,WAAmB,WAAmB,YAA4B;EAChE,MAAM,SAAS,KAAK,KAAK,KAAK,MAAM,eAAe,SAAS,GAAG,GAAG,WAAW,MAAM,GAAG,CAAC,EAAE,GAAG,aAAa;EACzG,IAAI,OAAO,WAAW,MAAM,IAAA,KAC1B,OAAO;EAET,OAAO;CACT;AACF;;;;;;;;AASA,IAAa,mBAAb,MAA8B;CAOC;;CAL7B;CACA,SAAiB;CACjB,6BAA8B,IAAI,IAAqC;;CAGvE,YAAY,aAA4E;EAA3D,KAAA,cAAA;CAA4D;;CAGzF,MAAM,MAAM,OAAqC;EAC/C,MAAM,QAAQ,MAAM,KAAK,MAAM;EAC/B,MAAM,IAAI,SAAe,SAAS,WAAW;GAC3C,MAAM,UAAU,iBAAiB,uBAAO,IAAI,MAAM,qCAAqC,CAAC,GAAG,mBAAmB;GAC9G,MAAM,UAAU,UAAwB;IACtC,aAAa,OAAO;IACpB,KAAK,WAAW,OAAO,MAAM,UAAU;IACvC,IAAI,OAAO,OAAO,KAAK;SAClB,QAAQ;GACf;GACA,KAAK,WAAW,IAAI,MAAM,YAAY,MAAM;GAC5C,MAAM,KAAK,OAAO,KAAA,GAAW,KAAA,IAAY,UAAU;IACjD,IAAI,OAAO,OAAO,KAAK;GACzB,CAAC;EACH,CAAC;CACH;;CAGA,QAAc;EACZ,KAAK,SAAS;EACd,KAAU,UAAU,MACjB,UAAU;GACT,IAAI,MAAM,WAAW,MAAM,WAAW;EACxC,SACM,KAAA,CACR;CACF;CAEA,QAAuC;EACrC,IAAI,KAAK,QAAQ,OAAO,QAAQ,uBAAO,IAAI,MAAM,2BAA2B,CAAC;EAC7E,KAAK,aAAa,KAAK,MAAM,CAAC,CAAC,OAAO,UAAmB;GACvD,KAAK,WAAW,KAAA;GAChB,MAAM;EACR,CAAC;EACD,OAAO,KAAK;CACd;CAEA,MAAc,QAA+B;EAC3C,MAAM,QAAQ,MAAM,QAAQ,UAAU,CAAC,MAAM,aAAa,YAAY,GAAG,qBAAqB,GAAG;GAC/F,KAAK,KAAK;GACV,UAAU;GACV,OAAO;IAAC;IAAU;IAAU;IAAU;GAAK;EAC7C,CAAC;EACD,MAAM,IAAI,SAAe,SAAS,WAAW;GAC3C,MAAM,UAAU,iBAAiB;IAC/B,MAAM,KAAK;IACX,uBAAO,IAAI,MAAM,mCAAmC,CAAC;GACvD,GAAG,mBAAmB;GACtB,MAAM,QAAQ,UAAuB;IACnC,aAAa,OAAO;IACpB,OAAO,KAAK;GACd;GACA,MAAM,GAAG,UAAU,UAAU;IAC3B,KAAK,KAAK;IACV,QAAQ,YAAY,8BAA8B,MAAM,SAAS;GACnE,CAAC;GACD,MAAM,KAAK,cAAc;IACvB,qBAAK,IAAI,MAAM,yCAAyC,CAAC;IAEzD,KAAK,WAAW,KAAA;IAChB,KAAK,MAAM,UAAU,KAAK,WAAW,OAAO,GAAG,uBAAO,IAAI,MAAM,wBAAwB,CAAC;GAC3F,CAAC;GACD,MAAM,GAAG,YAAY,YAAqB;IACxC,IAAI,YAAY,SAAS;KACvB,aAAa,OAAO;KACpB,QAAQ;KACR;IACF;IACA,MAAM,YAAY;IAGlB,CADE,OAAO,WAAW,eAAe,WAAW,KAAK,WAAW,IAAI,UAAU,UAAU,IAAI,KAAA,EAAA,GACjF,OAAO,WAAW,UAAU,WAAW,IAAI,MAAM,UAAU,KAAK,IAAI,KAAA,CAAS;GACxF,CAAC;EACH,CAAC;EAED,MAAM,MAAM;EACZ,MAAM,SAAS,MAAM;EACrB,IAAI,KAAK,QAAQ,MAAM,WAAW;EAClC,OAAO;CACT;AACF;;AAGA,SAAS,OAAO,QAAoB,QAA+B;CACjE,OAAO,IAAI,SAAS,SAAS,WAAW;EACtC,OAAO,KAAK,SAAS,MAAM;EAC3B,OAAO,OAAO,SAAS,EAAE,MAAM,OAAO,IAAI;GAAE,MAAM;GAAa,MAAM;EAAE,SAAS;GAC9E,OAAO,eAAe,SAAS,MAAM;GAErC,OAAO,GAAG,UAAU,UAAiB,QAAQ,YAAY,8BAA8B,MAAM,SAAS,CAAC;GACvG,QAAQ;EACV,CAAC;CACH,CAAC;AACH"}
1
+ {"version":3,"file":"lifeline-C1v2ItCY.mjs","names":["files"],"sources":["../src/services/lifeline/index.ts"],"sourcesContent":["import { type ChildProcess, spawn } from 'node:child_process';\nimport { randomUUID } from 'node:crypto';\nimport files from 'node:fs/promises';\nimport net from 'node:net';\nimport path from 'node:path';\n\nimport {\n OBSERVE_SESSIONS_FLAG,\n OBSERVER_TIMEOUT_MS,\n SOCKET_MODE,\n SOCKET_NAME,\n SOCKET_PATH_LIMIT,\n} from '../../constants/lifeline';\nimport type { ILifeline, LifelineAdmission, LifelineWatch } from '../../types/lifeline';\nimport type { IRunnerPaths } from '../runnerPaths/type';\nimport { runtimeEntry } from '../runnerSupervisor';\n\n/** Session lifeline and external cleanup observer. */\n\nexport class NodeLifeline implements ILifeline {\n private server: net.Server | undefined;\n private target: string | undefined;\n private ownerGeneration: string | undefined;\n /** Closing a server stops it accepting; only the connections carry the death signal. */\n private readonly connections = new Set<net.Socket>();\n /** Started for this lifeline alone when no shared observer was given, and closed with it. */\n private ownObserver: LifelineObserver | undefined;\n\n /** Without `environment` nothing observes the lifeline; `observer` is shared by every session of this owner. */\n constructor(\n private readonly paths: IRunnerPaths,\n private readonly environment?: Readonly<Record<string, string | undefined>>,\n private readonly observer?: LifelineObserver,\n ) {}\n\n async arm(sessionId: string): Promise<string | undefined> {\n if (this.target) return this.target;\n try {\n await this.paths.ensureDirectories(sessionId);\n const generation = randomUUID();\n const marker = path.join(this.paths.stateDirectory(sessionId), '.owner');\n await files.writeFile(marker, generation, { mode: SOCKET_MODE });\n this.ownerGeneration = generation;\n let target = this.socketPath(sessionId, generation);\n // A socket left behind by a SIGKILLed predecessor refuses connections, never accepts them.\n if (target) await files.rm(target, { force: true });\n const server = net.createServer((connection) => {\n // Runners need only the close edge, so their liveness and their data are equally irrelevant.\n connection.unref();\n connection.resume();\n this.connections.add(connection);\n connection.on('close', () => this.connections.delete(connection));\n });\n await listen(server, target);\n if (target) await files.chmod(target, SOCKET_MODE);\n else {\n const address = server.address();\n if (!address || typeof address === 'string') throw new Error('Missing runner lifeline address');\n target = `tcp://127.0.0.1:${address.port}`;\n }\n // The lifeline must never be the reason pi stays alive.\n server.unref();\n this.server = server;\n this.target = target;\n if (this.environment !== undefined) {\n const observer = this.observer ?? (this.ownObserver ??= new LifelineObserver(this.environment));\n await observer.watch({\n sessionId,\n target,\n repository: await this.paths.repositoryPath(),\n ownerPid: process.pid,\n generation,\n marker,\n environment: this.environment,\n });\n }\n return target;\n } catch (error) {\n await this.dispose();\n // Runners stay unwatched rather than unlaunchable, so a session without a\n // lifeline degrades to running them unsupervised instead of failing.\n process.emitWarning(`Could not arm the po-runner lifeline: ${String(error)}`);\n return undefined;\n }\n }\n\n path(): string | undefined {\n return this.target;\n }\n\n generation(): string | undefined {\n return this.ownerGeneration;\n }\n\n async dispose(): Promise<void> {\n // Dropping the connections is what a runner sees, and it is the whole point\n // of disposing: closing the server alone would leave every runner waiting.\n for (const connection of this.connections) connection.destroy();\n this.connections.clear();\n this.ownObserver?.close();\n this.ownObserver = undefined;\n if (this.server) {\n try {\n this.server.close();\n } catch (error) {\n process.emitWarning(`Could not close the po-runner lifeline: ${String(error)}`);\n }\n this.server = undefined;\n }\n const target = this.target;\n this.target = undefined;\n if (target) {\n try {\n if (!target.startsWith('tcp://')) await files.rm(target, { force: true });\n } catch (error) {\n process.emitWarning(`Could not remove the po-runner lifeline: ${String(error)}`);\n }\n }\n }\n\n /** The socket must never escape the admitted session temp directory. */\n private socketPath(sessionId: string, generation: string): string {\n const target = path.join(this.paths.stateDirectory(sessionId), `${generation.slice(0, 8)}-${SOCKET_NAME}`);\n if (Buffer.byteLength(target) > SOCKET_PATH_LIMIT) {\n return ''; // Bind loopback instead of writing a socket outside the session.\n }\n return target;\n }\n}\n\n/**\n * One detached process that cleans up after every session of this owner, SIGKILL included.\n *\n * Sessions share it rather than each starting their own: an idle observer costs a whole Node\n * process, and it only waits on sockets. Each armed lifeline is sent to it over IPC and\n * confirmed once it is connected. Neither the process nor its channel keeps the owner alive.\n */\nexport class LifelineObserver {\n /** One child at a time: set from spawn until that child exits. */\n private starting: Promise<ChildProcess> | undefined;\n private closed = false;\n private readonly admissions = new Map<string, (error?: Error) => void>();\n\n /** `environment` is the observer process's own; each watch carries its session's. */\n constructor(private readonly environment: Readonly<Record<string, string | undefined>>) {}\n\n /** Resolves once the observer holds the lifeline, so an owner death from then on is cleaned up. */\n async watch(watch: LifelineWatch): Promise<void> {\n const child = await this.start();\n await new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => settle(new Error('Runner observer admission timed out')), OBSERVER_TIMEOUT_MS);\n const settle = (error?: Error): void => {\n clearTimeout(timeout);\n this.admissions.delete(watch.generation);\n if (error) reject(error);\n else resolve();\n };\n this.admissions.set(watch.generation, settle);\n child.send(watch, undefined, undefined, (error) => {\n if (error) settle(error);\n });\n });\n }\n\n /** The observer exits once every lifeline it still holds has closed and been cleaned up. */\n close(): void {\n this.closed = true;\n void this.starting?.then(\n (child) => {\n if (child.connected) child.disconnect();\n },\n () => undefined,\n );\n }\n\n private start(): Promise<ChildProcess> {\n if (this.closed) return Promise.reject(new Error('Runner observer is closed'));\n this.starting ??= this.spawn().catch((error: unknown) => {\n this.starting = undefined;\n throw error;\n });\n return this.starting;\n }\n\n private async spawn(): Promise<ChildProcess> {\n const child = spawn(process.execPath, [await runtimeEntry('runnerHost'), OBSERVE_SESSIONS_FLAG], {\n env: this.environment,\n detached: true,\n stdio: ['ignore', 'ignore', 'ignore', 'ipc'],\n });\n await new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => {\n child.kill();\n reject(new Error('Runner observer startup timed out'));\n }, OBSERVER_TIMEOUT_MS);\n const fail = (error: Error): void => {\n clearTimeout(timeout);\n reject(error);\n };\n child.on('error', (error) => {\n fail(error);\n process.emitWarning(`po-runner observer failed: ${error.message}`);\n });\n child.once('exit', () => {\n fail(new Error('Runner observer exited before admission'));\n // A dead observer fails what it was admitting, and the next arm starts a fresh one.\n this.starting = undefined;\n for (const settle of this.admissions.values()) settle(new Error('Runner observer exited'));\n });\n child.on('message', (message: unknown) => {\n if (message === 'ready') {\n clearTimeout(timeout);\n resolve();\n return;\n }\n const admission = message as Partial<LifelineAdmission> | null;\n const settle =\n typeof admission?.generation === 'string' ? this.admissions.get(admission.generation) : undefined;\n settle?.(typeof admission?.error === 'string' ? new Error(admission.error) : undefined);\n });\n });\n // The observer must never be the reason the owner stays alive.\n child.unref();\n child.channel?.unref();\n if (this.closed) child.disconnect();\n return child;\n }\n}\n\n/** Binding must complete before the first runner launches, so arming waits for it. */\nfunction listen(server: net.Server, target: string): Promise<void> {\n return new Promise((resolve, reject) => {\n server.once('error', reject);\n server.listen(target ? { path: target } : { host: '127.0.0.1', port: 0 }, () => {\n server.removeListener('error', reject);\n // Later errors would otherwise reach the process as unhandled.\n server.on('error', (error: Error) => process.emitWarning(`po-runner lifeline failed: ${error.message}`));\n resolve();\n });\n });\n}\n"],"mappings":"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"}