@intentic/constants 1.396.0 → 1.398.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentic/constants",
3
- "version": "1.396.0",
3
+ "version": "1.398.0",
4
4
  "description": "Shared constants for the intentic packages, ports, paths, and image references the daemon, CLIs and desktop app all agree on",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -20,14 +20,16 @@ export const ROOM_SOCKET: string;
20
20
  export const READING_FILES: readonly string[];
21
21
 
22
22
  export interface MemoryReading {
23
- // memory.high, else memory.max, else the machine's memory; undefined where nothing bounds it.
23
+ // memory.high, else memory.max, else the machine's memory; undefined where nothing bounds it. Never above the workload
24
+ // leaf's memory.max plus what runs outside that leaf, where the leaf is bounded: admitted work runs in the leaf.
24
25
  readonly limitBytes: number | undefined;
25
26
  // Working set plus swap; undefined where nothing measures it. Free memory is counted against its resident part.
26
27
  readonly usedBytes: number | undefined;
27
28
  // The swapped part of `usedBytes`; 0 when swap is off or unaccounted.
28
29
  readonly swapBytes: number;
29
- // What swap can hold: memory.swap.max, never more than what is swapped plus the machine's SwapFree; at a root cgroup
30
- // the machine's SwapTotal. Undefined where nothing bounds it. Past SWAP_FULL_SHARE of it, swap counts as used.
30
+ // What swap can hold: memory.swap.max, never more than what is swapped plus the machine's SwapFree, nor than the
31
+ // workload leaf's memory.swap.max plus what is swapped outside it; at a root cgroup the machine's SwapTotal.
32
+ // Undefined where nothing bounds it. Past SWAP_FULL_SHARE of it, swap counts as used.
31
33
  readonly swapLimitBytes?: number | undefined;
32
34
  // The machine's MemAvailable, which free memory never exceeds; absent where the machine does not say.
33
35
  readonly availableBytes?: number | undefined;
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // Shared admission formula for ResourceBudget and scripts without a daemon; dependency-free for pre-install use.
3
- // Limit is memory.high, else memory.max, else host RAM, capped at host RAM; free resident room is also capped at MemAvailable.
3
+ // Limit is memory.high, else memory.max, else host RAM, capped at host RAM, and never above the workload leaf's own wall
4
+ // plus what runs outside that leaf; free resident room is also capped at MemAvailable.
4
5
  // Exclude reclaimable inactive cache; count swapped pages only near full swap. Memory full PSI avg10/avg60 catches paging pressure.
5
6
  // I/O full PSI holds background work only: a busy disk may slow a person's turn but must not refuse it.
6
7
  import { spawnSync } from "node:child_process";
@@ -86,6 +87,8 @@ export const RESERVATION_MS = 90_000;
86
87
  export const ROOM_SOCKET = process.env.INTENTIC_ROOM_SOCKET ?? "/run/intentic/room.sock";
87
88
 
88
89
  const CGROUP = "/sys/fs/cgroup";
90
+ // The leaf the entrypoint puts everything but netd and the daemon in, and bounds below the sandbox's cap (workloadWalls).
91
+ const WORKLOAD = `${CGROUP}/workload`;
89
92
  // Every file the reading is made of, for a caller that reads them itself (the daemon, asynchronously).
90
93
  export const READING_FILES = Object.freeze([
91
94
  `${CGROUP}/memory.high`,
@@ -97,6 +100,11 @@ export const READING_FILES = Object.freeze([
97
100
  `${CGROUP}/memory.pressure`,
98
101
  `${CGROUP}/memory.events`,
99
102
  `${CGROUP}/io.pressure`,
103
+ `${WORKLOAD}/memory.max`,
104
+ `${WORKLOAD}/memory.current`,
105
+ `${WORKLOAD}/memory.stat`,
106
+ `${WORKLOAD}/memory.swap.current`,
107
+ `${WORKLOAD}/memory.swap.max`,
100
108
  "/proc/meminfo",
101
109
  "/proc/pressure/memory",
102
110
  "/proc/pressure/io",
@@ -133,25 +141,75 @@ const stallOf = (read, resource) => {
133
141
  return { recent: fullAvg(text, "avg10") ?? 0, sustained: fullAvg(text, "avg60") ?? 0 };
134
142
  };
135
143
 
144
+ // A cgroup's working set (memory.current less the inactive file cache the kernel reclaims first) and its swap, or
145
+ // undefined where it keeps no memory.current. Unaccounted swap (swapaccount off) is none, never unknown: it must not
146
+ // blank a measurable ceiling.
147
+ const chargeOf = (read, dir) => {
148
+ const current = numeric(read(`${dir}/memory.current`));
149
+ return current === undefined
150
+ ? undefined
151
+ : { residentBytes: Math.max(0, current - (keyed(read(`${dir}/memory.stat`)).inactive_file ?? 0)), swapBytes: numeric(read(`${dir}/memory.swap.current`)) ?? 0 };
152
+ };
153
+
154
+ // (2026-11-05) THE WORKLOAD LEAF'S WALLS. Everything this gate admits (a turn, a build, a test run) runs in the workload
155
+ // leaf, which the entrypoint and daemon-cgroup.ts hold below the sandbox's cap (memory-records.ts workloadMemoryMax; the swap
156
+ // default is @intentic/sandbox-run defaultSandboxSwapGib).
157
+ // On a 16 GiB cap the container's memory.high is 14.4 GiB and the leaf's memory.max 12.5 GiB, so a gate
158
+ // judging by the container alone admitted work the leaf had no room for, which then ended in an OOM kill inside the leaf
159
+ // instead of a wait for room.
160
+ //
161
+ // The leaf's wall bounds only what runs in it, so as a ceiling on the whole sandbox it is that wall plus what runs
162
+ // outside the leaf now. Outside is the container's charge less the leaf's, not the daemon leaf's own figure: the
163
+ // sandbox's own Docker containers sit beside the two leaves too. The limit less what counts against it then comes to the
164
+ // leaf's wall less the leaf's own working set, the workload judged against its own max. That is kept as a lower limit
165
+ // rather than a second reading so that used stays the whole sandbox's (the gauge, the shortage sentence and the cap a
166
+ // client offers to raise all speak of the sandbox), and every caller of the limit sees the lower wall without knowing
167
+ // the leaf exists.
168
+ //
169
+ // Swap the same way: the leaf's memory.swap.max plus what is swapped outside it (normally nothing: the daemon's leaf
170
+ // allows none), so swap counts as full once the leaf's own is nearly spent. From then on the swapped pages count against
171
+ // the limit too (countedBytesOf), and the outside's swapped pages join the offset, so the leaf is still judged by its own.
172
+ //
173
+ // A leaf that is absent, or whose bound reads `max`, is no wall, and the reading is the container's as before. A root
174
+ // cgroup has none either: with no container charge there is no inside to tell from outside.
175
+ const workloadWalls = (read, sandbox, swapLimit) => {
176
+ const leaf = chargeOf(read, WORKLOAD);
177
+ if (leaf === undefined) {
178
+ return { limit: Number.POSITIVE_INFINITY, swapLimit };
179
+ }
180
+ const outsideResident = Math.max(0, sandbox.residentBytes - leaf.residentBytes);
181
+ const outsideSwap = Math.max(0, sandbox.swapBytes - leaf.swapBytes);
182
+ const swapWall = numeric(read(`${WORKLOAD}/memory.swap.max`));
183
+ const walledSwap = Math.min(swapLimit, swapWall === undefined ? Number.POSITIVE_INFINITY : swapWall + outsideSwap);
184
+ const wall = numeric(read(`${WORKLOAD}/memory.max`));
185
+ if (wall === undefined) {
186
+ return { limit: Number.POSITIVE_INFINITY, swapLimit: walledSwap };
187
+ }
188
+ const full = swapFullOf({ swapBytes: sandbox.swapBytes, swapLimitBytes: Number.isFinite(walledSwap) ? walledSwap : undefined });
189
+ return { limit: wall + outsideResident + (full ? outsideSwap : 0), swapLimit: walledSwap };
190
+ };
191
+
136
192
  /** The reading, from a reader that answers each of READING_FILES with its text or undefined. */
137
193
  export const readingFrom = (read) => {
138
194
  const meminfo = keyed(read("/proc/meminfo"));
139
195
  const machine = kib(meminfo, "MemTotal");
140
- const bound = Math.min(
196
+ let bound = Math.min(
141
197
  numeric(read(`${CGROUP}/memory.high`)) ?? Number.POSITIVE_INFINITY,
142
198
  numeric(read(`${CGROUP}/memory.max`)) ?? Number.POSITIVE_INFINITY,
143
199
  machine ?? Number.POSITIVE_INFINITY,
144
200
  );
145
- const current = numeric(read(`${CGROUP}/memory.current`));
201
+ const sandbox = chargeOf(read, CGROUP);
146
202
  let usedBytes;
147
203
  let swapBytes = 0;
148
204
  let swapLimit = Number.POSITIVE_INFINITY;
149
- if (current !== undefined) {
150
- // Unaccounted swap (swapaccount off) is none, never unknown: it must not blank a measurable ceiling.
151
- swapBytes = numeric(read(`${CGROUP}/memory.swap.current`)) ?? 0;
152
- usedBytes = Math.max(0, current - (keyed(read(`${CGROUP}/memory.stat`)).inactive_file ?? 0)) + swapBytes;
205
+ if (sandbox !== undefined) {
206
+ swapBytes = sandbox.swapBytes;
207
+ usedBytes = sandbox.residentBytes + swapBytes;
153
208
  // The cgroup's own swap ceiling, and never more than what it holds plus what the machine's swap can still take.
154
209
  swapLimit = Math.min(numeric(read(`${CGROUP}/memory.swap.max`)) ?? Number.POSITIVE_INFINITY, swapBytes + (kib(meminfo, "SwapFree") ?? Number.POSITIVE_INFINITY));
210
+ const walls = workloadWalls(read, sandbox, swapLimit);
211
+ bound = Math.min(bound, walls.limit);
212
+ swapLimit = walls.swapLimit;
155
213
  } else if (machine !== undefined && kib(meminfo, "MemAvailable") !== undefined) {
156
214
  swapBytes = Math.max(0, (kib(meminfo, "SwapTotal") ?? 0) - (kib(meminfo, "SwapFree") ?? 0));
157
215
  usedBytes = Math.max(0, machine - (kib(meminfo, "MemAvailable") ?? 0)) + swapBytes;