@runuai/host 0.9.91 → 0.9.93

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.
@@ -101,11 +101,18 @@ export function awsMachineName(taskId: string): string {
101
101
  return `uai-machine-${taskId.toLowerCase()}`;
102
102
  }
103
103
 
104
- /** Smallest Graviton type satisfying both dimensions. */
104
+ /** Smallest Graviton type satisfying both dimensions.
105
+ *
106
+ * FLOORS AT 16GiB (owner decision 2026-08-31, priced at $0.25/machine-up
107
+ * hour): an 8GiB machine thrashed to death twice in one day under the
108
+ * ordinary heavy shape — a 5-agent crew + Turbopack dev server + editor +
109
+ * VNC browser — and a memory wall reads as "the product is broken", which
110
+ * costs more than the larger instance. m7g (sustained) over t4g (burstable)
111
+ * deliberately: compile bursts on a drained CPU-credit balance are the same
112
+ * wall wearing a different mask. The floor is AWS-only — local VM sizing
113
+ * has its own constraints and keeps the spec-driven path. */
105
114
  export function defaultInstanceType(spec: MachineSpec): string {
106
115
  const ladder: Array<[string, number, number]> = [
107
- ["m7g.medium", 1, 4096],
108
- ["m7g.large", 2, 8192],
109
116
  ["m7g.xlarge", 4, 16384],
110
117
  ["m7g.2xlarge", 8, 32768],
111
118
  ["m7g.4xlarge", 16, 65536],
@@ -136,6 +143,24 @@ export function awsUserData(authorizedPublicKey: string): string {
136
143
  ' chmod 700 "$home/.ssh"',
137
144
  ' chmod 600 "$home/.ssh/authorized_keys"',
138
145
  "done",
146
+ // Swap BEFORE any workload: an Opus agent + a bundler + Chromium on a
147
+ // swapless machine hits the memory wall as a thrash (load 14, RAM 100%,
148
+ // sshd/code-server accept loops starved — live 2026-08-31 on the first
149
+ // real machine task; the HOSTS got their swapfile in 0.9.85, the
150
+ // machines never did). Best-effort: a machine without fallocate still
151
+ // boots.
152
+ // 16G (owner decision 2026-08-31): the graceful-degradation floor has to
153
+ // absorb the whole heavy shape, not soften its first half — 4G filled
154
+ // and the wall moved instead of vanishing. Disk is already paid for.
155
+ "(fallocate -l 16G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile && echo '/swapfile none swap sw 0 0' >> /etc/fstab) || true",
156
+ // The task OWNS its machine (ADR-121 — per-customer kernel isolation is
157
+ // the whole point); denying sudo just breaks project setups that
158
+ // container tasks handle fine (live 2026-08-31: a Hightop machine task
159
+ // could not install the Infisical CLI its repo's config expects).
160
+ "echo 'node ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/node && chmod 440 /etc/sudoers.d/node",
161
+ // Infisical CLI: baseline parity with the standard container image —
162
+ // pnpm-secrets-style project setups expect it on PATH. Best-effort.
163
+ "(V=0.43.110; case \$(uname -m) in aarch64|arm64) T=arm64;; *) T=amd64;; esac; curl -fsSL -o /tmp/inf.tgz https://github.com/Infisical/cli/releases/download/v\${V}/cli_\${V}_linux_\${T}.tar.gz && tar -xzf /tmp/inf.tgz -C /tmp infisical && install -m 0755 /tmp/infisical /usr/local/bin/infisical) || true",
139
164
  "",
140
165
  ].join("\n");
141
166
  // RAW script, not base64: AWS CLI v2 base64-encodes --user-data ITSELF.
@@ -30,12 +30,25 @@ const exec = promisify(execFile);
30
30
  const ADMISSION_THRESHOLD = 0.9;
31
31
  /** Warn-log above this fraction. */
32
32
  const WARN_THRESHOLD = 0.8;
33
+ /**
34
+ * The vnode counter ALONE cannot refuse: `kern.num_vnodes` includes the
35
+ * reclaimable vnode cache, and a healthy long-uptime Mac converges to
36
+ * num_vnodes == maxvnodes forever (live false-positive, 2026-08-31: the
37
+ * Studio refused every task at "100%" with ZERO VMs running and the file
38
+ * table at 1.2%). Real virtiofs pinning holds OPEN FILES — the Aug-24
39
+ * incident read 96% of `kern.num_files/kern.maxfiles`. So refusal
40
+ * requires the vnode ratio AND genuine file-table pressure together.
41
+ */
42
+ const FILE_TABLE_CORROBORATION_THRESHOLD = 0.5;
33
43
  const CACHE_MS = 15_000;
34
44
 
35
45
  export interface VnodePressure {
36
46
  current: number;
37
47
  max: number;
38
48
  ratio: number;
49
+ filesCurrent: number;
50
+ filesMax: number;
51
+ filesRatio: number;
39
52
  }
40
53
 
41
54
  let cached: { at: number; value: VnodePressure | null } | null = null;
@@ -47,14 +60,30 @@ export async function readVnodePressure(): Promise<VnodePressure | null> {
47
60
  try {
48
61
  const { stdout } = await exec(
49
62
  "/usr/sbin/sysctl",
50
- ["-n", "kern.num_vnodes", "kern.maxvnodes"],
63
+ ["-n", "kern.num_vnodes", "kern.maxvnodes", "kern.num_files", "kern.maxfiles"],
51
64
  { timeout: 5_000 },
52
65
  );
53
66
  const parts = stdout.trim().split(/\s+/).map(Number);
54
67
  const current = parts[0] ?? Number.NaN;
55
68
  const max = parts[1] ?? Number.NaN;
56
- if (Number.isFinite(current) && Number.isFinite(max) && max > 0) {
57
- value = { current, max, ratio: current / max };
69
+ const filesCurrent = parts[2] ?? Number.NaN;
70
+ const filesMax = parts[3] ?? Number.NaN;
71
+ if (
72
+ Number.isFinite(current) &&
73
+ Number.isFinite(max) &&
74
+ max > 0 &&
75
+ Number.isFinite(filesCurrent) &&
76
+ Number.isFinite(filesMax) &&
77
+ filesMax > 0
78
+ ) {
79
+ value = {
80
+ current,
81
+ max,
82
+ ratio: current / max,
83
+ filesCurrent,
84
+ filesMax,
85
+ filesRatio: filesCurrent / filesMax,
86
+ };
58
87
  }
59
88
  } catch {
60
89
  // Sensor failure is never a task failure; admission simply has no data.
@@ -67,11 +96,15 @@ export async function readVnodePressure(): Promise<VnodePressure | null> {
67
96
  export async function vnodeAdmissionProblem(): Promise<string | null> {
68
97
  const pressure = await readVnodePressure();
69
98
  if (!pressure || pressure.ratio < ADMISSION_THRESHOLD) return null;
99
+ // A full vnode table with an idle file table is a warm cache, not danger.
100
+ if (pressure.filesRatio < FILE_TABLE_CORROBORATION_THRESHOLD) return null;
70
101
  return (
71
102
  `the macOS vnode table is at ${Math.round(pressure.ratio * 100)}% ` +
72
- `(${pressure.current}/${pressure.max}); starting another task VM risks ` +
73
- "system-wide file-table exhaustion. Close a running task, or raise the " +
74
- "ceiling once with: sudo uai-host tune"
103
+ `(${pressure.current}/${pressure.max}) with the file table at ` +
104
+ `${Math.round(pressure.filesRatio * 100)}% ` +
105
+ `(${pressure.filesCurrent}/${pressure.filesMax}); starting another task ` +
106
+ "VM risks system-wide file-table exhaustion. Close a running task, or " +
107
+ "raise the ceiling once with: sudo uai-host tune"
75
108
  );
76
109
  }
77
110
 
@@ -82,15 +115,19 @@ let lastLoggedBand: "ok" | "warn" | "critical" = "ok";
82
115
  export async function logVnodePressure(): Promise<void> {
83
116
  const pressure = await readVnodePressure();
84
117
  if (!pressure) return;
85
- const band =
86
- pressure.ratio >= ADMISSION_THRESHOLD
87
- ? "critical"
88
- : pressure.ratio >= WARN_THRESHOLD
89
- ? "warn"
90
- : "ok";
118
+ const refusing =
119
+ pressure.ratio >= ADMISSION_THRESHOLD &&
120
+ pressure.filesRatio >= FILE_TABLE_CORROBORATION_THRESHOLD;
121
+ const band = refusing
122
+ ? "critical"
123
+ : pressure.filesRatio >= WARN_THRESHOLD || pressure.ratio >= WARN_THRESHOLD
124
+ ? "warn"
125
+ : "ok";
91
126
  if (band === lastLoggedBand) return;
92
127
  lastLoggedBand = band;
93
- const detail = `${pressure.current}/${pressure.max} (${Math.round(pressure.ratio * 100)}%)`;
128
+ const detail =
129
+ `vnodes ${pressure.current}/${pressure.max} (${Math.round(pressure.ratio * 100)}%), ` +
130
+ `files ${pressure.filesCurrent}/${pressure.filesMax} (${Math.round(pressure.filesRatio * 100)}%)`;
94
131
  if (band === "critical") {
95
132
  console.warn(
96
133
  `[vnode] table at ${detail} — new apple task VMs will be refused; run: sudo uai-host tune`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runuai/host",
3
- "version": "0.9.91",
3
+ "version": "0.9.93",
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>",