@agimon-ai/po-runner 0.0.0-alpha.63 → 0.0.0-alpha.65
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.
|
@@ -2,6 +2,8 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
2
2
|
const require_rolldown_runtime = require("../../rolldown-runtime-C0BPl7ul.cjs");
|
|
3
3
|
let node_net = require("node:net");
|
|
4
4
|
node_net = require_rolldown_runtime.__toESM(node_net, 1);
|
|
5
|
+
let node_process = require("node:process");
|
|
6
|
+
node_process = require_rolldown_runtime.__toESM(node_process, 1);
|
|
5
7
|
//#region src/services/lifeline/client.ts
|
|
6
8
|
/** Environment variable carrying the lifeline socket path to every runner supervisor. */
|
|
7
9
|
const LIFELINE_ENV = "PO_RUNNER_LIFELINE";
|
|
@@ -21,7 +23,7 @@ const LIFELINE_ENV = "PO_RUNNER_LIFELINE";
|
|
|
21
23
|
* Runners run unwatched when no lifeline is armed, so a missing environment
|
|
22
24
|
* variable can never be mistaken for a dead owner.
|
|
23
25
|
*/
|
|
24
|
-
function watchOwner(onOwnerLost, target =
|
|
26
|
+
function watchOwner(onOwnerLost, target = node_process.default.env[LIFELINE_ENV]) {
|
|
25
27
|
if (!target) return () => void 0;
|
|
26
28
|
let settled = false;
|
|
27
29
|
const settle = () => {
|
|
@@ -33,7 +35,7 @@ function watchOwner(onOwnerLost, target = process.env[LIFELINE_ENV]) {
|
|
|
33
35
|
try {
|
|
34
36
|
connection = connectLifeline(target);
|
|
35
37
|
} catch (error) {
|
|
36
|
-
|
|
38
|
+
node_process.default.emitWarning(`Could not watch the po-runner lifeline: ${String(error)}`);
|
|
37
39
|
return () => void 0;
|
|
38
40
|
}
|
|
39
41
|
connection.unref();
|
|
@@ -72,11 +74,11 @@ function observeSessions(cleanup) {
|
|
|
72
74
|
const closed = [];
|
|
73
75
|
let cleaning = Promise.resolve();
|
|
74
76
|
const drain = () => {
|
|
75
|
-
if (
|
|
77
|
+
if (node_process.default.connected || closed.length === 0) return;
|
|
76
78
|
const batch = closed.splice(0);
|
|
77
|
-
cleaning = cleaning.then(() => cleanup(batch).catch((error) =>
|
|
79
|
+
cleaning = cleaning.then(() => cleanup(batch).catch((error) => node_process.default.emitWarning(`Could not clean up runner sessions after their owner: ${String(error)}`)));
|
|
78
80
|
};
|
|
79
|
-
|
|
81
|
+
node_process.default.on("message", (message) => {
|
|
80
82
|
const watch = readWatch(message);
|
|
81
83
|
if (watch) return observe(watch, (ended) => {
|
|
82
84
|
closed.push(ended);
|
|
@@ -88,7 +90,7 @@ function observeSessions(cleanup) {
|
|
|
88
90
|
error: "Invalid runner observer watch"
|
|
89
91
|
});
|
|
90
92
|
});
|
|
91
|
-
|
|
93
|
+
node_process.default.once("disconnect", drain);
|
|
92
94
|
reply("ready");
|
|
93
95
|
}
|
|
94
96
|
function observe(watch, onClose) {
|
|
@@ -118,7 +120,7 @@ function observe(watch, onClose) {
|
|
|
118
120
|
}
|
|
119
121
|
/** The owner may already be gone, and a closed channel must not crash the watches still open. */
|
|
120
122
|
function reply(message) {
|
|
121
|
-
if (
|
|
123
|
+
if (node_process.default.connected) node_process.default.send?.(message, void 0, void 0, () => void 0);
|
|
122
124
|
}
|
|
123
125
|
function readWatch(value) {
|
|
124
126
|
if (typeof value !== "object" || value === null) return void 0;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.cjs","names":["net"],"sources":["../../../src/services/lifeline/client.ts"],"sourcesContent":["import net from 'node:net';\n\nimport type { LifelineAdmission, LifelineWatch } from '../../types/lifeline';\n\n/** Environment variable carrying the lifeline socket path to every runner supervisor. */\nexport const LIFELINE_ENV = 'PO_RUNNER_LIFELINE';\n\n/**\n * Reports the owning pi process dying to a runner's supervisor.\n *\n * A connected Unix socket drops the moment the owner's descriptors close, which\n * happens on every exit path including SIGKILL, so it is the one death signal a\n * graceful shutdown handler cannot provide. Connecting never blocks, the close\n * edge arrives through the event loop rather than a poll, and an unref'd socket\n * can never keep the supervisor alive: the watcher is therefore incapable of\n * outliving the process that installed it.\n *\n * A refused or missing socket means the owner died before this runner started,\n * which is reported the same way as a later death rather than left to hang.\n *\n * Runners run unwatched when no lifeline is armed, so a missing environment\n * variable can never be mistaken for a dead owner.\n */\nexport function watchOwner(\n onOwnerLost: () => void,\n target: string | undefined = process.env[LIFELINE_ENV],\n): () => void {\n if (!target) return () => undefined;\n\n let settled = false;\n const settle = (): void => {\n if (settled) return;\n settled = true;\n onOwnerLost();\n };\n\n let connection: net.Socket;\n try {\n connection = connectLifeline(target);\n } catch (error) {\n process.emitWarning(`Could not watch the po-runner lifeline: ${String(error)}`);\n return () => undefined;\n }\n\n // The supervisor's own exit must never wait on this socket.\n connection.unref();\n // The owner never sends anything, so drain rather than accumulate.\n connection.on('connect', () => connection.resume());\n connection.on('error', settle);\n connection.on('close', settle);\n\n return () => {\n settled = true;\n connection.destroy();\n };\n}\n\n/** Long session temp paths use a loopback socket, without creating files outside the session. */\nexport function connectLifeline(target: string): net.Socket {\n if (target.startsWith('tcp://')) {\n const address = new URL(target);\n if (address.hostname !== '127.0.0.1') throw new Error('Runner lifeline must be loopback');\n return net.connect({ host: address.hostname, port: Number(address.port) });\n }\n return net.connect(target);\n}\n\n/**\n * Watches every session lifeline of one owner from one observer process.\n *\n * The owner sends a watch per armed session over IPC. While the owner lives, it cleans up its own\n * sessions with its shared services, so a lifeline that closes then is only held back and this\n * process loads nothing. Once the owner is gone, SIGKILL included, the channel disconnects and\n * `cleanup` gets every lifeline held back, then each that closes later; it skips the sessions the\n * owner already cleaned up.\n * The message listener holds the IPC channel, and so this process, open while the owner lives;\n * once the owner is gone it exits after the last lifeline closes and its cleanup settles.\n */\nexport function observeSessions(cleanup: (watches: readonly LifelineWatch[]) => Promise<void>): void {\n // ponytail: an ended session stays held here until the owner exits, a few KB each; prune the ones\n // whose marker is gone if one owner ever outlives thousands of sessions.\n const closed: LifelineWatch[] = [];\n let cleaning = Promise.resolve();\n const drain = (): void => {\n if (process.connected || closed.length === 0) return;\n const batch = closed.splice(0);\n // Batches run one after another, so no two of them open the same registry at once.\n cleaning = cleaning.then(() =>\n cleanup(batch).catch((error: unknown) =>\n process.emitWarning(`Could not clean up runner sessions after their owner: ${String(error)}`),\n ),\n );\n };\n process.on('message', (message: unknown) => {\n const watch = readWatch(message);\n if (watch)\n return observe(watch, (ended) => {\n closed.push(ended);\n drain();\n });\n const generation = (message as Partial<LifelineWatch> | null)?.generation;\n if (typeof generation === 'string') reply({ generation, error: 'Invalid runner observer watch' });\n });\n process.once('disconnect', drain);\n reply('ready');\n}\n\nfunction observe(watch: LifelineWatch, onClose: (watch: LifelineWatch) => void): void {\n let connection: net.Socket;\n try {\n connection = connectLifeline(watch.target);\n } catch (error) {\n reply({ generation: watch.generation, error: String(error) });\n return;\n }\n let admitted = false;\n connection.once('connect', () => {\n admitted = true;\n // The owner never sends anything, so drain rather than accumulate.\n connection.resume();\n reply({ generation: watch.generation });\n });\n connection.once('error', (error) => {\n if (!admitted) reply({ generation: watch.generation, error: error.message });\n });\n connection.once('close', () => onClose(watch));\n}\n\n/** The owner may already be gone, and a closed channel must not crash the watches still open. */\nfunction reply(message: LifelineAdmission | 'ready'): void {\n if (process.connected) process.send?.(message, undefined, undefined, () => undefined);\n}\n\nfunction readWatch(value: unknown): LifelineWatch | undefined {\n if (typeof value !== 'object' || value === null) return undefined;\n const watch = value as Partial<LifelineWatch>;\n const valid =\n typeof watch.sessionId === 'string' &&\n typeof watch.target === 'string' &&\n typeof watch.repository === 'string' &&\n typeof watch.generation === 'string' &&\n typeof watch.marker === 'string' &&\n Number.isInteger(watch.ownerPid) &&\n typeof watch.environment === 'object' &&\n watch.environment !== null;\n return valid ? (watch as LifelineWatch) : undefined;\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"client.cjs","names":["process","net"],"sources":["../../../src/services/lifeline/client.ts"],"sourcesContent":["import net from 'node:net';\nimport process from 'node:process';\n\nimport type { LifelineAdmission, LifelineWatch } from '../../types/lifeline';\n\n/** Environment variable carrying the lifeline socket path to every runner supervisor. */\nexport const LIFELINE_ENV = 'PO_RUNNER_LIFELINE';\n\n/**\n * Reports the owning pi process dying to a runner's supervisor.\n *\n * A connected Unix socket drops the moment the owner's descriptors close, which\n * happens on every exit path including SIGKILL, so it is the one death signal a\n * graceful shutdown handler cannot provide. Connecting never blocks, the close\n * edge arrives through the event loop rather than a poll, and an unref'd socket\n * can never keep the supervisor alive: the watcher is therefore incapable of\n * outliving the process that installed it.\n *\n * A refused or missing socket means the owner died before this runner started,\n * which is reported the same way as a later death rather than left to hang.\n *\n * Runners run unwatched when no lifeline is armed, so a missing environment\n * variable can never be mistaken for a dead owner.\n */\nexport function watchOwner(\n onOwnerLost: () => void,\n target: string | undefined = process.env[LIFELINE_ENV],\n): () => void {\n if (!target) return () => undefined;\n\n let settled = false;\n const settle = (): void => {\n if (settled) return;\n settled = true;\n onOwnerLost();\n };\n\n let connection: net.Socket;\n try {\n connection = connectLifeline(target);\n } catch (error) {\n process.emitWarning(`Could not watch the po-runner lifeline: ${String(error)}`);\n return () => undefined;\n }\n\n // The supervisor's own exit must never wait on this socket.\n connection.unref();\n // The owner never sends anything, so drain rather than accumulate.\n connection.on('connect', () => connection.resume());\n connection.on('error', settle);\n connection.on('close', settle);\n\n return () => {\n settled = true;\n connection.destroy();\n };\n}\n\n/** Long session temp paths use a loopback socket, without creating files outside the session. */\nexport function connectLifeline(target: string): net.Socket {\n if (target.startsWith('tcp://')) {\n const address = new URL(target);\n if (address.hostname !== '127.0.0.1') throw new Error('Runner lifeline must be loopback');\n return net.connect({ host: address.hostname, port: Number(address.port) });\n }\n return net.connect(target);\n}\n\n/**\n * Watches every session lifeline of one owner from one observer process.\n *\n * The owner sends a watch per armed session over IPC. While the owner lives, it cleans up its own\n * sessions with its shared services, so a lifeline that closes then is only held back and this\n * process loads nothing. Once the owner is gone, SIGKILL included, the channel disconnects and\n * `cleanup` gets every lifeline held back, then each that closes later; it skips the sessions the\n * owner already cleaned up.\n * The message listener holds the IPC channel, and so this process, open while the owner lives;\n * once the owner is gone it exits after the last lifeline closes and its cleanup settles.\n */\nexport function observeSessions(cleanup: (watches: readonly LifelineWatch[]) => Promise<void>): void {\n // ponytail: an ended session stays held here until the owner exits, a few KB each; prune the ones\n // whose marker is gone if one owner ever outlives thousands of sessions.\n const closed: LifelineWatch[] = [];\n let cleaning = Promise.resolve();\n const drain = (): void => {\n if (process.connected || closed.length === 0) return;\n const batch = closed.splice(0);\n // Batches run one after another, so no two of them open the same registry at once.\n cleaning = cleaning.then(() =>\n cleanup(batch).catch((error: unknown) =>\n process.emitWarning(`Could not clean up runner sessions after their owner: ${String(error)}`),\n ),\n );\n };\n process.on('message', (message: unknown) => {\n const watch = readWatch(message);\n if (watch)\n return observe(watch, (ended) => {\n closed.push(ended);\n drain();\n });\n const generation = (message as Partial<LifelineWatch> | null)?.generation;\n if (typeof generation === 'string') reply({ generation, error: 'Invalid runner observer watch' });\n });\n process.once('disconnect', drain);\n reply('ready');\n}\n\nfunction observe(watch: LifelineWatch, onClose: (watch: LifelineWatch) => void): void {\n let connection: net.Socket;\n try {\n connection = connectLifeline(watch.target);\n } catch (error) {\n reply({ generation: watch.generation, error: String(error) });\n return;\n }\n let admitted = false;\n connection.once('connect', () => {\n admitted = true;\n // The owner never sends anything, so drain rather than accumulate.\n connection.resume();\n reply({ generation: watch.generation });\n });\n connection.once('error', (error) => {\n if (!admitted) reply({ generation: watch.generation, error: error.message });\n });\n connection.once('close', () => onClose(watch));\n}\n\n/** The owner may already be gone, and a closed channel must not crash the watches still open. */\nfunction reply(message: LifelineAdmission | 'ready'): void {\n if (process.connected) process.send?.(message, undefined, undefined, () => undefined);\n}\n\nfunction readWatch(value: unknown): LifelineWatch | undefined {\n if (typeof value !== 'object' || value === null) return undefined;\n const watch = value as Partial<LifelineWatch>;\n const valid =\n typeof watch.sessionId === 'string' &&\n typeof watch.target === 'string' &&\n typeof watch.repository === 'string' &&\n typeof watch.generation === 'string' &&\n typeof watch.marker === 'string' &&\n Number.isInteger(watch.ownerPid) &&\n typeof watch.environment === 'object' &&\n watch.environment !== null;\n return valid ? (watch as LifelineWatch) : undefined;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.mts","names":[],"sources":["../../../src/services/lifeline/client.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"client.d.mts","names":[],"sources":["../../../src/services/lifeline/client.ts"],"mappings":";;;;qBAMa;;;;;;;;;;;;;;;;;wBAkBG,WACd,yBACA;;wBAiCc,gBAAgB,iBAAiB,IAAI;;;;;;;;;;;;wBAoBrC,gBAAgB,UAAU,kBAAkB,oBAAoB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.mjs","names":[],"sources":["../../../src/services/lifeline/client.ts"],"sourcesContent":["import net from 'node:net';\n\nimport type { LifelineAdmission, LifelineWatch } from '../../types/lifeline';\n\n/** Environment variable carrying the lifeline socket path to every runner supervisor. */\nexport const LIFELINE_ENV = 'PO_RUNNER_LIFELINE';\n\n/**\n * Reports the owning pi process dying to a runner's supervisor.\n *\n * A connected Unix socket drops the moment the owner's descriptors close, which\n * happens on every exit path including SIGKILL, so it is the one death signal a\n * graceful shutdown handler cannot provide. Connecting never blocks, the close\n * edge arrives through the event loop rather than a poll, and an unref'd socket\n * can never keep the supervisor alive: the watcher is therefore incapable of\n * outliving the process that installed it.\n *\n * A refused or missing socket means the owner died before this runner started,\n * which is reported the same way as a later death rather than left to hang.\n *\n * Runners run unwatched when no lifeline is armed, so a missing environment\n * variable can never be mistaken for a dead owner.\n */\nexport function watchOwner(\n onOwnerLost: () => void,\n target: string | undefined = process.env[LIFELINE_ENV],\n): () => void {\n if (!target) return () => undefined;\n\n let settled = false;\n const settle = (): void => {\n if (settled) return;\n settled = true;\n onOwnerLost();\n };\n\n let connection: net.Socket;\n try {\n connection = connectLifeline(target);\n } catch (error) {\n process.emitWarning(`Could not watch the po-runner lifeline: ${String(error)}`);\n return () => undefined;\n }\n\n // The supervisor's own exit must never wait on this socket.\n connection.unref();\n // The owner never sends anything, so drain rather than accumulate.\n connection.on('connect', () => connection.resume());\n connection.on('error', settle);\n connection.on('close', settle);\n\n return () => {\n settled = true;\n connection.destroy();\n };\n}\n\n/** Long session temp paths use a loopback socket, without creating files outside the session. */\nexport function connectLifeline(target: string): net.Socket {\n if (target.startsWith('tcp://')) {\n const address = new URL(target);\n if (address.hostname !== '127.0.0.1') throw new Error('Runner lifeline must be loopback');\n return net.connect({ host: address.hostname, port: Number(address.port) });\n }\n return net.connect(target);\n}\n\n/**\n * Watches every session lifeline of one owner from one observer process.\n *\n * The owner sends a watch per armed session over IPC. While the owner lives, it cleans up its own\n * sessions with its shared services, so a lifeline that closes then is only held back and this\n * process loads nothing. Once the owner is gone, SIGKILL included, the channel disconnects and\n * `cleanup` gets every lifeline held back, then each that closes later; it skips the sessions the\n * owner already cleaned up.\n * The message listener holds the IPC channel, and so this process, open while the owner lives;\n * once the owner is gone it exits after the last lifeline closes and its cleanup settles.\n */\nexport function observeSessions(cleanup: (watches: readonly LifelineWatch[]) => Promise<void>): void {\n // ponytail: an ended session stays held here until the owner exits, a few KB each; prune the ones\n // whose marker is gone if one owner ever outlives thousands of sessions.\n const closed: LifelineWatch[] = [];\n let cleaning = Promise.resolve();\n const drain = (): void => {\n if (process.connected || closed.length === 0) return;\n const batch = closed.splice(0);\n // Batches run one after another, so no two of them open the same registry at once.\n cleaning = cleaning.then(() =>\n cleanup(batch).catch((error: unknown) =>\n process.emitWarning(`Could not clean up runner sessions after their owner: ${String(error)}`),\n ),\n );\n };\n process.on('message', (message: unknown) => {\n const watch = readWatch(message);\n if (watch)\n return observe(watch, (ended) => {\n closed.push(ended);\n drain();\n });\n const generation = (message as Partial<LifelineWatch> | null)?.generation;\n if (typeof generation === 'string') reply({ generation, error: 'Invalid runner observer watch' });\n });\n process.once('disconnect', drain);\n reply('ready');\n}\n\nfunction observe(watch: LifelineWatch, onClose: (watch: LifelineWatch) => void): void {\n let connection: net.Socket;\n try {\n connection = connectLifeline(watch.target);\n } catch (error) {\n reply({ generation: watch.generation, error: String(error) });\n return;\n }\n let admitted = false;\n connection.once('connect', () => {\n admitted = true;\n // The owner never sends anything, so drain rather than accumulate.\n connection.resume();\n reply({ generation: watch.generation });\n });\n connection.once('error', (error) => {\n if (!admitted) reply({ generation: watch.generation, error: error.message });\n });\n connection.once('close', () => onClose(watch));\n}\n\n/** The owner may already be gone, and a closed channel must not crash the watches still open. */\nfunction reply(message: LifelineAdmission | 'ready'): void {\n if (process.connected) process.send?.(message, undefined, undefined, () => undefined);\n}\n\nfunction readWatch(value: unknown): LifelineWatch | undefined {\n if (typeof value !== 'object' || value === null) return undefined;\n const watch = value as Partial<LifelineWatch>;\n const valid =\n typeof watch.sessionId === 'string' &&\n typeof watch.target === 'string' &&\n typeof watch.repository === 'string' &&\n typeof watch.generation === 'string' &&\n typeof watch.marker === 'string' &&\n Number.isInteger(watch.ownerPid) &&\n typeof watch.environment === 'object' &&\n watch.environment !== null;\n return valid ? (watch as LifelineWatch) : undefined;\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"client.mjs","names":[],"sources":["../../../src/services/lifeline/client.ts"],"sourcesContent":["import net from 'node:net';\nimport process from 'node:process';\n\nimport type { LifelineAdmission, LifelineWatch } from '../../types/lifeline';\n\n/** Environment variable carrying the lifeline socket path to every runner supervisor. */\nexport const LIFELINE_ENV = 'PO_RUNNER_LIFELINE';\n\n/**\n * Reports the owning pi process dying to a runner's supervisor.\n *\n * A connected Unix socket drops the moment the owner's descriptors close, which\n * happens on every exit path including SIGKILL, so it is the one death signal a\n * graceful shutdown handler cannot provide. Connecting never blocks, the close\n * edge arrives through the event loop rather than a poll, and an unref'd socket\n * can never keep the supervisor alive: the watcher is therefore incapable of\n * outliving the process that installed it.\n *\n * A refused or missing socket means the owner died before this runner started,\n * which is reported the same way as a later death rather than left to hang.\n *\n * Runners run unwatched when no lifeline is armed, so a missing environment\n * variable can never be mistaken for a dead owner.\n */\nexport function watchOwner(\n onOwnerLost: () => void,\n target: string | undefined = process.env[LIFELINE_ENV],\n): () => void {\n if (!target) return () => undefined;\n\n let settled = false;\n const settle = (): void => {\n if (settled) return;\n settled = true;\n onOwnerLost();\n };\n\n let connection: net.Socket;\n try {\n connection = connectLifeline(target);\n } catch (error) {\n process.emitWarning(`Could not watch the po-runner lifeline: ${String(error)}`);\n return () => undefined;\n }\n\n // The supervisor's own exit must never wait on this socket.\n connection.unref();\n // The owner never sends anything, so drain rather than accumulate.\n connection.on('connect', () => connection.resume());\n connection.on('error', settle);\n connection.on('close', settle);\n\n return () => {\n settled = true;\n connection.destroy();\n };\n}\n\n/** Long session temp paths use a loopback socket, without creating files outside the session. */\nexport function connectLifeline(target: string): net.Socket {\n if (target.startsWith('tcp://')) {\n const address = new URL(target);\n if (address.hostname !== '127.0.0.1') throw new Error('Runner lifeline must be loopback');\n return net.connect({ host: address.hostname, port: Number(address.port) });\n }\n return net.connect(target);\n}\n\n/**\n * Watches every session lifeline of one owner from one observer process.\n *\n * The owner sends a watch per armed session over IPC. While the owner lives, it cleans up its own\n * sessions with its shared services, so a lifeline that closes then is only held back and this\n * process loads nothing. Once the owner is gone, SIGKILL included, the channel disconnects and\n * `cleanup` gets every lifeline held back, then each that closes later; it skips the sessions the\n * owner already cleaned up.\n * The message listener holds the IPC channel, and so this process, open while the owner lives;\n * once the owner is gone it exits after the last lifeline closes and its cleanup settles.\n */\nexport function observeSessions(cleanup: (watches: readonly LifelineWatch[]) => Promise<void>): void {\n // ponytail: an ended session stays held here until the owner exits, a few KB each; prune the ones\n // whose marker is gone if one owner ever outlives thousands of sessions.\n const closed: LifelineWatch[] = [];\n let cleaning = Promise.resolve();\n const drain = (): void => {\n if (process.connected || closed.length === 0) return;\n const batch = closed.splice(0);\n // Batches run one after another, so no two of them open the same registry at once.\n cleaning = cleaning.then(() =>\n cleanup(batch).catch((error: unknown) =>\n process.emitWarning(`Could not clean up runner sessions after their owner: ${String(error)}`),\n ),\n );\n };\n process.on('message', (message: unknown) => {\n const watch = readWatch(message);\n if (watch)\n return observe(watch, (ended) => {\n closed.push(ended);\n drain();\n });\n const generation = (message as Partial<LifelineWatch> | null)?.generation;\n if (typeof generation === 'string') reply({ generation, error: 'Invalid runner observer watch' });\n });\n process.once('disconnect', drain);\n reply('ready');\n}\n\nfunction observe(watch: LifelineWatch, onClose: (watch: LifelineWatch) => void): void {\n let connection: net.Socket;\n try {\n connection = connectLifeline(watch.target);\n } catch (error) {\n reply({ generation: watch.generation, error: String(error) });\n return;\n }\n let admitted = false;\n connection.once('connect', () => {\n admitted = true;\n // The owner never sends anything, so drain rather than accumulate.\n connection.resume();\n reply({ generation: watch.generation });\n });\n connection.once('error', (error) => {\n if (!admitted) reply({ generation: watch.generation, error: error.message });\n });\n connection.once('close', () => onClose(watch));\n}\n\n/** The owner may already be gone, and a closed channel must not crash the watches still open. */\nfunction reply(message: LifelineAdmission | 'ready'): void {\n if (process.connected) process.send?.(message, undefined, undefined, () => undefined);\n}\n\nfunction readWatch(value: unknown): LifelineWatch | undefined {\n if (typeof value !== 'object' || value === null) return undefined;\n const watch = value as Partial<LifelineWatch>;\n const valid =\n typeof watch.sessionId === 'string' &&\n typeof watch.target === 'string' &&\n typeof watch.repository === 'string' &&\n typeof watch.generation === 'string' &&\n typeof watch.marker === 'string' &&\n Number.isInteger(watch.ownerPid) &&\n typeof watch.environment === 'object' &&\n watch.environment !== null;\n return valid ? (watch as LifelineWatch) : undefined;\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agimon-ai/po-runner",
|
|
3
|
-
"version": "0.0.0-alpha.
|
|
3
|
+
"version": "0.0.0-alpha.65",
|
|
4
4
|
"description": "Supervised shell execution, background process control, and run logs for Pi coding agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -214,10 +214,10 @@
|
|
|
214
214
|
},
|
|
215
215
|
"dependencies": {
|
|
216
216
|
"@agimon-ai/foundation-process-registry": "0.30.14",
|
|
217
|
-
"@agimon-ai/po-core": "0.0.0-alpha.
|
|
218
|
-
"@agimon-ai/po-ui": "0.0.0-alpha.
|
|
219
|
-
"@agimon-ai/po-web-components": "0.0.0-alpha.
|
|
220
|
-
"@agimon-ai/po-web-security": "0.0.0-alpha.
|
|
217
|
+
"@agimon-ai/po-core": "0.0.0-alpha.65",
|
|
218
|
+
"@agimon-ai/po-ui": "0.0.0-alpha.65",
|
|
219
|
+
"@agimon-ai/po-web-components": "0.0.0-alpha.65",
|
|
220
|
+
"@agimon-ai/po-web-security": "0.0.0-alpha.65",
|
|
221
221
|
"@deepseek-ai/cordis": "4.0.5",
|
|
222
222
|
"@rmux/sdk": "0.6.5",
|
|
223
223
|
"@xterm/headless": "6.0.0",
|
|
@@ -225,8 +225,8 @@
|
|
|
225
225
|
"typebox": "1.4.2"
|
|
226
226
|
},
|
|
227
227
|
"devDependencies": {
|
|
228
|
-
"@agimon-ai/po-build": "0.0.0-alpha.
|
|
229
|
-
"@agimon-ai/po-native-components": "0.0.0-alpha.
|
|
228
|
+
"@agimon-ai/po-build": "0.0.0-alpha.65",
|
|
229
|
+
"@agimon-ai/po-native-components": "0.0.0-alpha.65",
|
|
230
230
|
"@earendil-works/pi-coding-agent": "1.4.0",
|
|
231
231
|
"@tanstack/react-store": "0.11.2",
|
|
232
232
|
"@tanstack/store": "0.11.2",
|
|
@@ -242,7 +242,7 @@
|
|
|
242
242
|
"vitest": "5.0.4"
|
|
243
243
|
},
|
|
244
244
|
"peerDependencies": {
|
|
245
|
-
"@agimon-ai/po-native-components": "0.0.0-alpha.
|
|
245
|
+
"@agimon-ai/po-native-components": "0.0.0-alpha.65",
|
|
246
246
|
"@earendil-works/pi-coding-agent": "1.4.0",
|
|
247
247
|
"react-native": "0.88.0",
|
|
248
248
|
"react-native-unistyles": "^3.5.3"
|
|
@@ -262,14 +262,14 @@
|
|
|
262
262
|
}
|
|
263
263
|
},
|
|
264
264
|
"optionalDependencies": {
|
|
265
|
-
"@agimon-ai/po-runner-rmux-darwin-arm64": "0.0.0-alpha.
|
|
266
|
-
"@agimon-ai/po-runner-rmux-darwin-x64": "0.0.0-alpha.
|
|
267
|
-
"@agimon-ai/po-runner-rmux-linux-arm64": "0.0.0-alpha.
|
|
268
|
-
"@agimon-ai/po-runner-rmux-linux-x64": "0.0.0-alpha.
|
|
269
|
-
"@agimon-ai/po-runner-rtk-darwin-arm64": "0.0.0-alpha.
|
|
270
|
-
"@agimon-ai/po-runner-rtk-darwin-x64": "0.0.0-alpha.
|
|
271
|
-
"@agimon-ai/po-runner-rtk-linux-arm64": "0.0.0-alpha.
|
|
272
|
-
"@agimon-ai/po-runner-rtk-linux-x64": "0.0.0-alpha.
|
|
265
|
+
"@agimon-ai/po-runner-rmux-darwin-arm64": "0.0.0-alpha.65",
|
|
266
|
+
"@agimon-ai/po-runner-rmux-darwin-x64": "0.0.0-alpha.65",
|
|
267
|
+
"@agimon-ai/po-runner-rmux-linux-arm64": "0.0.0-alpha.65",
|
|
268
|
+
"@agimon-ai/po-runner-rmux-linux-x64": "0.0.0-alpha.65",
|
|
269
|
+
"@agimon-ai/po-runner-rtk-darwin-arm64": "0.0.0-alpha.65",
|
|
270
|
+
"@agimon-ai/po-runner-rtk-darwin-x64": "0.0.0-alpha.65",
|
|
271
|
+
"@agimon-ai/po-runner-rtk-linux-arm64": "0.0.0-alpha.65",
|
|
272
|
+
"@agimon-ai/po-runner-rtk-linux-x64": "0.0.0-alpha.65"
|
|
273
273
|
},
|
|
274
274
|
"engines": {
|
|
275
275
|
"node": ">=24"
|