@intentic/constants 1.389.0 → 1.391.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/README.md CHANGED
@@ -31,13 +31,20 @@ flowchart LR
31
31
  on any Node an agent's project pins. A rule may also match the program's working directory (`cwd`), name its memory
32
32
  price (`size`), and be judged under a program that already holds a slot (`nests`): the editor's `vue-tsc` takes the
33
33
  `vue-typecheck` lane, one at a time, from under a `pnpm -r typecheck`. Only a pool no `nests` rule names may wait on
34
- one that does, so the waits cannot form a cycle.
34
+ one that does, so the waits cannot form a cycle. `cargoBuildArgs` translates supported `cargo run` options into
35
+ a build; `heavy-exec.cjs` queues that build in a child and starts the original run once its slot is released.
36
+ Unknown arguments retain the original invocation. The prebuild stays local even when ordinary Cargo builds
37
+ are offloaded, so its artifacts warm the original run.
35
38
  - `memory-room`'s `SIZE_BYTES` prices a toolchain command by its measured shape instead of its class: `targeted` (a test
36
39
  run aimed at named files), `editor-typecheck` (the editor's full `vue-tsc`, 7 GiB) and `scoped-typecheck` (`lsp diag`
37
- on a Vue package, 4 GiB).
40
+ on a Vue package, 4 GiB), and `cargo-build` (7 GiB per admission, based on Shinobi's 2026-11-02 Rust build peaks).
41
+ Two Cargo reservations fit an 18 GiB sandbox with headroom; the price does not cap compiler RSS.
38
42
  - `memory-room` is the one formula for whether the sandbox has room (limit, used, free, stall, and what each workload
39
43
  class costs). The daemon's resource budget judges by it, and it is also the `memory-room` command `queue-run` asks:
40
- it asks the daemon's room socket, and applies the formula itself where no daemon answers.
44
+ it asks the daemon's room socket, and applies the formula itself where no daemon answers. (2026-11-02) A reading may
45
+ carry `host`, the machine the sandbox runs on as the daemon last heard from it: work nobody waits on then also waits
46
+ while its cost would take that machine under `host.reserveBytes`. Only the daemon adds it, so the fallback formula
47
+ never has a host to answer to.
41
48
  - `dev-previews` is the record of a conversation's managed dev preview (its demo server): where the records live
42
49
  (`/tmp/intentic-previews`), what makes one trustworthy (pid, start tick, conversation stamp, pid namespace), the cap
43
50
  of three and the ten-minute idle bound, the conversation's own port in 47150-47199, and how a preview is stopped
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentic/constants",
3
- "version": "1.389.0",
3
+ "version": "1.391.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",
@@ -7,7 +7,10 @@
7
7
 
8
8
  const { accessSync, constants } = require("node:fs");
9
9
  const { delimiter, join, resolve } = require("node:path");
10
- const { takeTurn } = require("./heavy-turn.cjs");
10
+ const { spawnSync } = require("node:child_process");
11
+ const { constants: osConstants } = require("node:os");
12
+ const { takeTurn, pendingSpec } = require("./heavy-turn.cjs");
13
+ const { cargoBuildArgs, matchInvocation } = require("./heavy-rules.cjs");
11
14
 
12
15
  const [shims = "", program = "", ...args] = process.argv.slice(2);
13
16
 
@@ -30,8 +33,33 @@ if (real === undefined) {
30
33
  process.stderr.write(`${program}: command not found\n`);
31
34
  process.exit(127);
32
35
  }
36
+ // The child holds the slot only while compiling. The original run then starts with its untouched environment,
37
+ // so a server inherits neither the build's slot nor its toolchain priority. This build stays local to warm its artifacts.
38
+ let build;
39
+ let buildEnv = process.env;
40
+ try {
41
+ const spec = pendingSpec();
42
+ const translated = program === "cargo" ? cargoBuildArgs(args) : undefined;
43
+ if (spec !== undefined && translated !== undefined &&
44
+ matchInvocation([program, ...translated].join(" "), spec.rules, undefined, { cwd: process.cwd() })?.id === "cargo") {
45
+ build = translated;
46
+ const offload = { ...spec.offload };
47
+ delete offload.cargo;
48
+ buildEnv = { ...process.env, INTENTIC_HEAVY: JSON.stringify({ ...spec, offload }) };
49
+ }
50
+ } catch {
51
+ // allow(silent-catch): an unreadable table preserves the original invocation
52
+ }
53
+ if (build !== undefined) {
54
+ const compiled = spawnSync(process.execPath, [__filename, shims, program, ...build], { stdio: "inherit", env: buildEnv });
55
+ if (compiled.status !== 0) {
56
+ process.exit(compiled.status ?? (compiled.signal === null ? 1 : 128 + (osConstants.signals[compiled.signal] ?? 0)));
57
+ }
58
+ }
33
59
  try {
34
- takeTurn({ program, args, command: [real, ...args] });
60
+ if (build === undefined) {
61
+ takeTurn({ program, args, command: [real, ...args] });
62
+ }
35
63
  } catch {
36
64
  // allow(silent-catch): a table that does not parse runs the program as if unjudged
37
65
  }
@@ -39,6 +67,5 @@ try {
39
67
  if (typeof process.execve === "function") {
40
68
  process.execve(real, [program, ...args], process.env);
41
69
  }
42
- const { spawnSync } = require("node:child_process");
43
70
  const run = spawnSync(real, args, { stdio: "inherit", env: process.env, argv0: program });
44
71
  process.exit(run.status ?? 1);
@@ -113,13 +113,14 @@ const SHIPPED_HEAVY_COMMANDS = Object.freeze({
113
113
  { id: "typechecker", pattern: "(?<![-.])\\b(tsc|tsgo|vue-tsc)\\b(?![-.])" },
114
114
  { id: "turbo-fanout", pattern: "\\bturbo\\b.*\\brun\\b.*\\b(build|test|typecheck|check)\\b" },
115
115
  { id: "package-script", pattern: "\\b(pnpm|npm|yarn|bun)\\b.*\\b(test|typecheck|verify|check|build)\\b" },
116
- { id: "cargo", pattern: "^cargo\\s+(\\+\\S+\\s+)?(build|test|check|clippy|bench|doc)\\b" },
116
+ { id: "cargo", pattern: "^cargo\\s+(\\+\\S+\\s+)?(build|test|check|clippy|bench|doc)\\b", size: "cargo-build" },
117
117
  ]),
118
118
  });
119
119
 
120
120
  // Every rule an earlier release shipped, as it shipped it (matched against the agent's shell line then). A stored rule
121
121
  // equal to one of these was seeded, not chosen, so converting a stored file drops it and the current rule takes over.
122
122
  const EARLIER_SHIPPED_RULES = Object.freeze([
123
+ { id: "cargo", pattern: "^cargo\\s+(\\+\\S+\\s+)?(build|test|check|clippy|bench|doc)\\b" },
123
124
  {
124
125
  id: "read-only",
125
126
  pattern: "^\\s*(grep|rg|iq|ag|cat|bat|head|tail|less|ls|find|fd|wc|which|echo|git\\s+(log|status|diff|show|blame))\\b",
@@ -342,6 +343,44 @@ const sizeArgs = (match, invocation) => {
342
343
  return ["vitest", "package-script"].includes(match.id) && targetedRun(invocation) ? ["--size", "targeted"] : [];
343
344
  };
344
345
 
346
+ // Translate only known Cargo options. Unknown flags, aliases and help requests keep the original invocation.
347
+ // Runtime arguments after `--` belong only to `run`; a toolchain selector belongs before either verb.
348
+ const cargoBuildArgs = (args) => {
349
+ const prefix = args[0]?.startsWith("+") ? [args[0]] : [];
350
+ if (args[prefix.length] !== "run") {
351
+ return undefined;
352
+ }
353
+ const build = [...prefix, "build"];
354
+ const values = new Set([
355
+ "--package", "--bin", "--example", "--features", "--profile", "--target", "--target-dir", "--manifest-path",
356
+ "--jobs", "--color", "--message-format", "--config", "--lockfile-path", "-p", "-F", "-j", "-Z",
357
+ ]);
358
+ const switches = new Set([
359
+ "--release", "-r", "--all-features", "--no-default-features", "--locked", "--offline", "--frozen",
360
+ "--verbose", "--quiet", "-q", "--timings", "--future-incompat-report", "--ignore-rust-version", "--keep-going",
361
+ ]);
362
+ for (let i = prefix.length + 1; i < args.length; i++) {
363
+ const arg = args[i];
364
+ if (arg === "--") {
365
+ break;
366
+ }
367
+ if (switches.has(arg) || /^-v+$/u.test(arg) || /^-(p|F|j|Z).+/u.test(arg)) {
368
+ build.push(arg);
369
+ } else if (values.has(arg)) {
370
+ const value = args[++i];
371
+ if (value === undefined || value === "--" || (value.startsWith("-") && !(arg === "-j" || arg === "--jobs"))) {
372
+ return undefined;
373
+ }
374
+ build.push(arg, value);
375
+ } else if (arg.includes("=") && (values.has(arg.split("=")[0]) || arg.startsWith("--timings="))) {
376
+ build.push(arg);
377
+ } else {
378
+ return undefined;
379
+ }
380
+ }
381
+ return build;
382
+ };
383
+
345
384
  // When a slot held this long is worth a line in the log: half of what its own rule lets it hold, never for a rule that
346
385
  // lets it hold forever.
347
386
  const holdWarnSeconds = (maxHold) => (maxHold > 0 ? maxHold / 2 : undefined);
@@ -363,4 +402,5 @@ module.exports = {
363
402
  holdWarnSeconds,
364
403
  ruleById,
365
404
  heldPoolsOf,
405
+ cargoBuildArgs,
366
406
  };
@@ -95,3 +95,6 @@ export function holdWarnSeconds(maxHold: number): number | undefined;
95
95
  export function ruleById(config: HeavyCommands, id: string): HeavyCommandRule | undefined;
96
96
  // The pools the programs above one already hold slots of, from INTENTIC_HEAVY_HELD and INTENTIC_QUEUE_SLOT.
97
97
  export function heldPoolsOf(env: Readonly<Record<string, string | undefined>>, config: HeavyCommands): string[];
98
+
99
+ // The build before a cargo run, or undefined when its arguments cannot be translated safely.
100
+ export function cargoBuildArgs(args: readonly string[]): string[] | undefined;
@@ -5,8 +5,8 @@ export const COST_BYTES: Readonly<Record<RoomWorkload, number>>;
5
5
  export const PERSON_RESERVE_BYTES: number;
6
6
  export const TARGETED_RUN_BYTES: number;
7
7
  // A toolchain command priced by its named shape instead of its class's cost (SIZE_BYTES): one aimed at named files,
8
- // the editor's full typecheck, a changed-files Vue typecheck.
9
- export type RoomSize = "targeted" | "editor-typecheck" | "scoped-typecheck";
8
+ // the editor's full typecheck, a changed-files Vue typecheck, a Cargo build.
9
+ export type RoomSize = "targeted" | "editor-typecheck" | "scoped-typecheck" | "cargo-build";
10
10
  export const SIZE_BYTES: Readonly<Record<RoomSize, number>>;
11
11
  export function isRoomSize(size: string): size is RoomSize;
12
12
  export const TEST_PROCESS_BYTES: Readonly<{ fanOutWorker: number; standaloneWorker: number; typecheck: number; ceiling: number }>;
@@ -40,6 +40,18 @@ export interface MemoryReading {
40
40
  readonly ioStallSustainedPercent?: number | undefined;
41
41
  // memory.events `oom_kill`, cumulative; undefined without a cgroup.
42
42
  readonly oomKills: number | undefined;
43
+ // (2026-11-02) The machine this sandbox runs on, as its agent last said, added by the daemon only while that is
44
+ // fresh; absent is "no host to answer to", never "no room". Work nobody waits on waits while it would take the host
45
+ // under `reserveBytes`.
46
+ readonly host?: HostRoom | undefined;
47
+ }
48
+
49
+ export interface HostRoom {
50
+ // The machine's name as the owner knows it (its device card), for the sentence a held command is given.
51
+ readonly name: string;
52
+ readonly availableBytes: number;
53
+ readonly totalBytes: number;
54
+ readonly reserveBytes: number;
43
55
  }
44
56
 
45
57
  export interface ShortMemory {
@@ -34,6 +34,10 @@ export const TARGETED_RUN_BYTES = 512 * MIB;
34
34
  // `size`, which queue-run passes as `--size <name>`). One name per measured shape:
35
35
  export const SIZE_BYTES = Object.freeze({
36
36
  targeted: TARGETED_RUN_BYTES,
37
+ // Shinobi resource-metrics.jsonl, 2026-11-02: `other` RSS peaked at 12.70 GiB at 19:25 with one cargo slot held,
38
+ // and 13.88 GiB at 23:14 with two. Rust was unclassified then, so this includes other work and unqueued runs.
39
+ // Admission estimate per slot: two fit the 18 GiB sandbox with headroom. Cargo's jobs still bound its fan-out.
40
+ "cargo-build": 7 * GIB,
37
41
  // The editor's full typecheck, `vue-tsc` over `_editor/web`, or `_site/demo`'s, which compiles the same source:
38
42
  // 6.8 GiB at its peak (measured 2026-10-26, cold and warm alike), under the 8 GiB heap its script gives it.
39
43
  "editor-typecheck": 7 * GIB,
@@ -209,6 +213,11 @@ export const judge = (reading, { workload, attended, reservedBytes = 0, size })
209
213
  const stallSustainedPercent = reading.stallSustainedPercent ?? 0;
210
214
  const unreserved = freeBytesOf(reading);
211
215
  const counted = countedBytesOf(reading);
216
+ // Asked before the sandbox's own figures, which may measure nothing: a VM with room says nothing of the PC under it.
217
+ const hostHeld = hostWait(reading, { workload, attended, reservedBytes, size }, need, unreserved);
218
+ if (hostHeld !== undefined) {
219
+ return hostHeld;
220
+ }
212
221
  if (limitBytes === undefined || usedBytes === undefined || counted === undefined || unreserved === undefined) {
213
222
  return { verdict: "run", needBytes: need, freeBytes: undefined, reservedBytes };
214
223
  }
@@ -252,6 +261,29 @@ export const judge = (reading, { workload, attended, reservedBytes = 0, size })
252
261
  return shortOf(`Sandbox memory is low: ${used}${held}`, { limitBytes, residentBytes: usedBytes - swapBytes, swapBytes });
253
262
  };
254
263
 
264
+ // (2026-11-02) Why work nobody is waiting on should wait for the machine this sandbox runs on, or undefined while it need
265
+ // not. Everything else here reads the sandbox's own cgroup and the kernel under it, which on a Windows PC is Docker
266
+ // Desktop's WSL2 VM: on 2026-11-02 that VM read as roomy while Windows had 1.1 GB left and froze for 43 s. The daemon adds
267
+ // `reading.host` from what the host's agent last said (hosts/host-memory.ts), only while that is fresh, so a reading
268
+ // without it is "no host to answer to", never "no room". Work waits while starting it would take the host below its
269
+ // reserve, the line under which the host's agent starts giving the VM's cache back. Only the work's own cost counts here:
270
+ // the person's share in `needBytes` is room inside the sandbox, which the sandbox's own figures already keep.
271
+ const hostShortOf = (host, costBytes, reservedBytes) => {
272
+ if (host === undefined || host.availableBytes - host.reserveBytes - reservedBytes >= costBytes) {
273
+ return undefined;
274
+ }
275
+ const held = reservedBytes > 0 ? `, ${gib(reservedBytes)} of it held for work that just started` : "";
276
+ return `${host.name}, the machine this sandbox runs on, is low on memory: it has ${gib(host.availableBytes)} available of ${gib(host.totalBytes)}${held}, and heavy work waits while starting it would leave it under ${gib(host.reserveBytes)}`;
277
+ };
278
+
279
+ // The verdict that holds work nobody waits on for the host, or undefined while the host has room for it (hostShortOf).
280
+ const hostWait = (reading, { workload, attended, reservedBytes, size }, need, unreserved) => {
281
+ const diagnosis = attended ? undefined : hostShortOf(reading.host, costBytesOf(workload, size), reservedBytes);
282
+ return diagnosis === undefined
283
+ ? undefined
284
+ : { verdict: "wait", needBytes: need, freeBytes: unreserved === undefined ? undefined : Math.max(0, unreserved - reservedBytes), reservedBytes, diagnosis };
285
+ };
286
+
255
287
  // Why work nobody is waiting on should wait for the disk, or undefined while it need not (the `disk` clause).
256
288
  const diskStalled = (reading) => {
257
289
  const recent = reading.ioStallPercent ?? 0;