@runuai/host 0.9.159 → 0.9.161

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.
@@ -1004,6 +1004,11 @@ async function managedInstallState(paths: ManagedRuntimePaths): Promise<ManagedI
1004
1004
  ] as const) {
1005
1005
  try {
1006
1006
  await lstat(state);
1007
+ // ensureManagedDirectories (a capability-floor claim) creates the bare
1008
+ // runtime directory on an npm install too. Empty, it holds no
1009
+ // generation: still no managed install. Every npm-installed task
1010
+ // machine sat pending on this (2026-10-20).
1011
+ if (state === paths.runtime && (await readdir(state)).length === 0) continue;
1007
1012
  return { kind: "damaged", detail: `the managed runtime has a ${what} but no current runtime; repair it first` };
1008
1013
  } catch (error) {
1009
1014
  if (!isMissing(error)) throw error;
@@ -22,8 +22,11 @@ import { join } from "node:path";
22
22
  /** Image label carrying the build-context content hash (rebuild trigger). */
23
23
  export const CONTEXT_HASH_LABEL = "com.runuai.context-hash";
24
24
 
25
- /** The public registry repository CI publishes the prebuilt image to. */
26
- export const STANDARD_IMAGE_RELEASE_REF = "ghcr.io/runuai/uai-standard";
25
+ /** The public registry repository CI publishes the prebuilt image to. The
26
+ * GitHub org was renamed runuai → moondeckio on 2026-10-20; GHCR, unlike
27
+ * github.com, does not redirect the old owner (pushes fail "owner not
28
+ * found", pulls 404). */
29
+ export const STANDARD_IMAGE_RELEASE_REF = "ghcr.io/moondeckio/uai-standard";
27
30
 
28
31
  /** Pin file basename; packaged at `agent/images/<basename>` in the payload,
29
32
  * one directory above the build context so it never perturbs the hash. */
@@ -243,6 +243,77 @@ export function taskMachinePeerProcessIds(): number[] {
243
243
  );
244
244
  }
245
245
 
246
+ /** A process's parent from /proc; null once the process is gone. */
247
+ export function processParentId(pid: number): number | null {
248
+ let stat: string;
249
+ try {
250
+ stat = readFileSync(`/proc/${pid}/stat`, "utf8");
251
+ } catch (error) {
252
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
253
+ throw error;
254
+ }
255
+ // `pid (comm) state ppid ...`: comm may itself hold spaces or parens.
256
+ const ppid = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
257
+ if (!Number.isSafeInteger(ppid) || ppid < 0) {
258
+ throw new Error(`process ${pid} has an unreadable parent`);
259
+ }
260
+ return ppid;
261
+ }
262
+
263
+ /**
264
+ * Every process in the sidecar service cgroup that this runner did not start.
265
+ * The fresh-incarnation proof asks this, not `taskMachinePeerProcessIds`: the
266
+ * runner's own helpers share its cgroup from boot, tsx's esbuild service above
267
+ * all (a long-lived child of every runner that compiled a module; live
268
+ * 2026-10-20, every marked machine stayed pending "not quiescent" on it), and
269
+ * a pending runner admits no task, so nothing it started is a task process.
270
+ * What an earlier runner process left behind (KillMode=process) was reparented
271
+ * when that process ended: it is never this runner's descendant, so it counts.
272
+ */
273
+ export function taskMachineForeignProcessIds(): number[] {
274
+ return foreignProcessIdsWith({
275
+ list: taskMachinePeerProcessIds,
276
+ parent: processParentId,
277
+ self: process.pid,
278
+ });
279
+ }
280
+
281
+ export interface ForeignProcessDeps {
282
+ list(): readonly number[];
283
+ parent(pid: number): number | null;
284
+ self: number;
285
+ }
286
+
287
+ /** Exported seam keeps the ancestry walk executable in tests. */
288
+ export function foreignProcessIdsWith(deps: ForeignProcessDeps): number[] {
289
+ const foreign: number[] = [];
290
+ for (const pid of deps.list()) {
291
+ if (pid === deps.self) continue;
292
+ const seen = new Set<number>();
293
+ let current: number | null = pid;
294
+ let ancestry: "ours" | "foreign" | "gone" = "foreign";
295
+ for (;;) {
296
+ const parent = deps.parent(current);
297
+ if (parent === null) {
298
+ // The process itself is gone: nothing to count. An ancestor gone
299
+ // mid-walk leaves an orphan the next look sees reparented: fail
300
+ // closed now, as that look would.
301
+ if (current === pid) ancestry = "gone";
302
+ break;
303
+ }
304
+ if (parent === deps.self) {
305
+ ancestry = "ours";
306
+ break;
307
+ }
308
+ if (parent <= 1 || seen.has(parent)) break;
309
+ seen.add(parent);
310
+ current = parent;
311
+ }
312
+ if (ancestry === "foreign") foreign.push(pid);
313
+ }
314
+ return foreign;
315
+ }
316
+
246
317
  /** Kill everything in the sidecar service cgroup except this root runner.
247
318
  * The task's agents, editor, terminals and sudo children all descend from the
248
319
  * service; KillMode=process is precisely why they otherwise survive. */
@@ -26,6 +26,8 @@ export interface TaskMachineCredentialsActivationDeps {
26
26
  filesReady(): boolean;
27
27
  retirementReady(): boolean;
28
28
  taskRows(): readonly { taskId: string }[];
29
+ /** Processes in the sidecar's service cgroup that this runner did not
30
+ * start (its own helpers, such as tsx's esbuild service, never count). */
29
31
  peerProcessIds(): readonly number[];
30
32
  }
31
33
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runuai/host",
3
- "version": "0.9.159",
3
+ "version": "0.9.161",
4
4
  "description": "Uai host — runs ephemeral AI tasks in containers on a machine you control.",
5
5
  "license": "MIT",
6
6
  "author": "Uai Tech <team@runuai.com>",
package/src/main.ts CHANGED
@@ -428,7 +428,7 @@ import {
428
428
  kernelBootId,
429
429
  TaskFilesMachineRetirementOutbox,
430
430
  TASK_FILES_MACHINE_RETIREMENT_RECORD,
431
- taskMachinePeerProcessIds,
431
+ taskMachineForeignProcessIds,
432
432
  } from "../lib/task-files-machine-retirement-outbox";
433
433
  import {
434
434
  planFreshTaskMachineCredentialsActivation,
@@ -1819,7 +1819,8 @@ function taskMachineCredentialsActivationDeps(): TaskMachineCredentialsActivatio
1819
1819
  .select({ taskId: schema.hostTasks.taskId })
1820
1820
  .from(schema.hostTasks)
1821
1821
  .all(),
1822
- peerProcessIds: taskMachinePeerProcessIds,
1822
+ // Not the runner's own children (tsx's esbuild service): see the helper.
1823
+ peerProcessIds: taskMachineForeignProcessIds,
1823
1824
  };
1824
1825
  }
1825
1826